Skip to content

Commit

Permalink
Propagate the config to all existing and newly created chat widget (#14)
Browse files Browse the repository at this point in the history
* Propagate the config to all existing and newly created chat widget

* Adds some classes in components

* add tests on sending message

* Add tests on settings

* lint

* fix tests

* Fix openSettings in tests

* lint

* Try to fix openSettings 2nd
  • Loading branch information
brichet authored Apr 12, 2024
1 parent 54983b8 commit 5420221
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 42 deletions.
1 change: 1 addition & 0 deletions packages/jupyter-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@lumino/signaling": "^2.0.0",
"@mui/icons-material": "^5.11.0",
"@mui/material": "^5.11.0",
"clsx": "^2.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
9 changes: 7 additions & 2 deletions packages/jupyter-chat/src/components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
InputAdornment
} from '@mui/material';
import SendIcon from '@mui/icons-material/Send';
import clsx from 'clsx';

const INPUT_BOX_CLASS = 'jp-chat_input-container';
const SEND_BUTTON_CLASS = 'jp-chat_send-button';

export function ChatInput(props: ChatInput.IProps): JSX.Element {
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
Expand All @@ -40,7 +44,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
);

return (
<Box sx={props.sx}>
<Box sx={props.sx} className={clsx(INPUT_BOX_CLASS)}>
<Box sx={{ display: 'flex' }}>
<TextField
value={props.value}
Expand All @@ -58,7 +62,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
color="primary"
onClick={props.onSend}
disabled={!props.value.trim().length}
title="Send message (SHIFT+ENTER)"
title={`Send message ${props.sendWithShiftEnter ? '(SHIFT+ENTER)' : '(ENTER)'}`}
className={clsx(SEND_BUTTON_CLASS)}
>
<SendIcon />
</IconButton>
Expand Down
7 changes: 6 additions & 1 deletion packages/jupyter-chat/src/components/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { Avatar, Box, Typography } from '@mui/material';
import type { SxProps, Theme } from '@mui/material';
import clsx from 'clsx';
import React, { useState, useEffect } from 'react';

import { RendermimeMarkdown } from './rendermime-markdown';
import { IChatMessage, IUser } from '../types';

const MESSAGES_BOX_CLASS = 'jp-chat_messages-container';
const MESSAGE_CLASS = 'jp-chat_message';

type ChatMessagesProps = {
rmRegistry: IRenderMimeRegistry;
messages: IChatMessage[];
Expand Down Expand Up @@ -129,10 +133,11 @@ export function ChatMessages(props: ChatMessagesProps): JSX.Element {
borderBottom: '1px solid var(--jp-border-color2)'
}
}}
className={clsx(MESSAGES_BOX_CLASS)}
>
{props.messages.map((message, i) => (
// extra div needed to ensure each bubble is on a new line
<Box key={i} sx={{ padding: 4 }}>
<Box key={i} sx={{ padding: 4 }} className={clsx(MESSAGE_CLASS)}>
<ChatMessageHeader
{...message.sender}
timestamp={timestamps[message.id]}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"title": "Jupyter chat configuration",
"description": "Configuration for the chat panel",
"title": "Chat",
"description": "Configuration for the chat widgets",
"type": "object",
"properties": {
"sendWithShiftEnter": {
Expand Down
29 changes: 27 additions & 2 deletions packages/jupyterlab-collaborative-chat/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,38 @@
* Distributed under the terms of the Modified BSD License.
*/

import { ChatWidget, IChatModel } from 'chat-jupyter';
import { ChatWidget, IChatModel, IConfig } from 'chat-jupyter';
import { IThemeManager } from '@jupyterlab/apputils';
import { ABCWidgetFactory, DocumentRegistry } from '@jupyterlab/docregistry';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { Contents } from '@jupyterlab/services';
import { Signal } from '@lumino/signaling';
import { Awareness } from 'y-protocols/awareness';

import { CollaborativeChatModel } from './model';
import { CollaborativeChatWidget } from './widget';
import { YChat } from './ychat';
import { IWidgetConfig } from './token';

/**
* The object provided by the chatDocument extension.
* It is used to set the current config (from settings) to newly created chat widget,
* and to propagate every changes to the existing chat widgets.
*/
export class WidgetConfig implements IWidgetConfig {
/**
* The constructor of the ChatDocument.
*/
constructor(config: Partial<IConfig>) {
this.config = config;
this.configChanged.connect((_, config) => {
this.config = { ...this.config, ...config };
});
}

config: Partial<IConfig>;
configChanged = new Signal<this, Partial<IConfig>>(this);
}

/**
* A widget factory to create new instances of CollaborativeChatWidget.
Expand Down Expand Up @@ -73,6 +95,7 @@ export class CollaborativeChatModelFactory
{
constructor(options: CollaborativeChatModel.IOptions) {
this._awareness = options.awareness;
this._widgetConfig = options.widgetConfig;
}

collaborative = true;
Expand Down Expand Up @@ -143,10 +166,12 @@ export class CollaborativeChatModelFactory
): CollaborativeChatModel {
return new CollaborativeChatModel({
...options,
awareness: this._awareness
awareness: this._awareness,
widgetConfig: this._widgetConfig
});
}

private _disposed = false;
private _awareness: Awareness;
private _widgetConfig: IWidgetConfig;
}
89 changes: 64 additions & 25 deletions packages/jupyterlab-collaborative-chat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,53 +19,90 @@ import {
} from '@jupyterlab/apputils';
import { ILauncher } from '@jupyterlab/launcher';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { Contents } from '@jupyterlab/services';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { chatIcon } from 'chat-jupyter';
import { Awareness } from 'y-protocols/awareness';

import { ChatWidgetFactory, CollaborativeChatModelFactory } from './factory';
import { CommandIDs, IChatFileType } from './token';
import {
WidgetConfig,
ChatWidgetFactory,
CollaborativeChatModelFactory
} from './factory';
import { chatFileType, CommandIDs, IWidgetConfig } from './token';
import { CollaborativeChatWidget } from './widget';
import { YChat } from './ychat';
import { Contents } from '@jupyterlab/services';
import { chatIcon } from 'chat-jupyter';

const pluginIds = {
chatCreation: 'jupyterlab-collaborative-chat:commands',
chatCommands: 'jupyterlab-collaborative-chat:commands',
chatDocument: 'jupyterlab-collaborative-chat:chat-document'
};

/**
* Extension registering the chat file type.
*/
export const chatDocument: JupyterFrontEndPlugin<IChatFileType> = {
export const chatDocument: JupyterFrontEndPlugin<IWidgetConfig> = {
id: pluginIds.chatDocument,
description: 'A document registration for collaborative chat',
autoStart: true,
requires: [IGlobalAwareness, IRenderMimeRegistry],
optional: [ICollaborativeDrive, ILayoutRestorer, IThemeManager],
provides: IChatFileType,
optional: [
ICollaborativeDrive,
ILayoutRestorer,
ISettingRegistry,
IThemeManager
],
provides: IWidgetConfig,
activate: (
app: JupyterFrontEnd,
awareness: Awareness,
rmRegistry: IRenderMimeRegistry,
drive: ICollaborativeDrive | null,
restorer: ILayoutRestorer | null,
settingRegistry: ISettingRegistry | null,
themeManager: IThemeManager | null
): IChatFileType => {
): IWidgetConfig => {
/**
* Load the settings for the chat widgets.
*/
let sendWithShiftEnter = false;

/**
* The ChatDocument object.
*/
const widgetConfig = new WidgetConfig({ sendWithShiftEnter });

function loadSetting(setting: ISettingRegistry.ISettings): void {
// Read the settings and convert to the correct type
sendWithShiftEnter = setting.get('sendWithShiftEnter')
.composite as boolean;
widgetConfig.configChanged.emit({ sendWithShiftEnter });
}

if (settingRegistry) {
// Wait for the application to be restored and
// for the settings to be loaded
Promise.all([app.restored, settingRegistry.load(pluginIds.chatDocument)])
.then(([, setting]) => {
// Read the settings
loadSetting(setting);

// Listen for the plugin setting changes
setting.changed.connect(loadSetting);
})
.catch(reason => {
console.error(
`Something went wrong when reading the settings.\n${reason}`
);
});
}

// Namespace for the tracker
const namespace = 'chat';

// Creating the tracker for the document
const tracker = new WidgetTracker<CollaborativeChatWidget>({ namespace });

const chatFileType: IChatFileType = {
name: 'chat',
displayName: 'Chat',
mimeTypes: ['text/json', 'application/json'],
extensions: ['.chat'],
fileFormat: 'text',
contentType: 'chat'
};

app.docRegistry.addFileType(chatFileType);

if (drive) {
Expand All @@ -76,7 +113,10 @@ export const chatDocument: JupyterFrontEndPlugin<IChatFileType> = {
}

// Creating and registering the model factory for our custom DocumentModel
const modelFactory = new CollaborativeChatModelFactory({ awareness });
const modelFactory = new CollaborativeChatModelFactory({
awareness,
widgetConfig
});
app.docRegistry.addModelFactory(modelFactory);

// Creating the widget factory to register it so the document manager knows about
Expand Down Expand Up @@ -112,22 +152,21 @@ export const chatDocument: JupyterFrontEndPlugin<IChatFileType> = {
});
}

return chatFileType;
return widgetConfig;
}
};

/**
* Extension registering the chat file type.
*/
export const chatCreation: JupyterFrontEndPlugin<void> = {
id: pluginIds.chatCreation,
export const chatCommands: JupyterFrontEndPlugin<void> = {
id: pluginIds.chatCommands,
description: 'The commands to create or open a chat',
autoStart: true,
requires: [IChatFileType, ICollaborativeDrive],
requires: [ICollaborativeDrive],
optional: [ICommandPalette, ILauncher],
activate: (
app: JupyterFrontEnd,
chatFileType: IChatFileType,
drive: ICollaborativeDrive,
commandPalette: ICommandPalette | null,
launcher: ILauncher | null
Expand Down Expand Up @@ -272,4 +311,4 @@ export const chatCreation: JupyterFrontEndPlugin<void> = {
}
};

export default [chatDocument, chatCreation];
export default [chatDocument, chatCommands];
11 changes: 10 additions & 1 deletion packages/jupyterlab-collaborative-chat/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { IChangedArgs } from '@jupyterlab/coreutils';
import { PartialJSONObject, UUID } from '@lumino/coreutils';
import { ISignal, Signal } from '@lumino/signaling';
import { Awareness } from 'y-protocols/awareness';

import { IWidgetConfig } from './token';
import { ChatChange, YChat } from './ychat';

/**
Expand All @@ -17,6 +19,7 @@ import { ChatChange, YChat } from './ychat';
export namespace CollaborativeChatModel {
export interface IOptions extends ChatModel.IOptions {
awareness: Awareness;
widgetConfig: IWidgetConfig;
sharedModel?: YChat;
languagePreference?: string;
}
Expand All @@ -33,7 +36,7 @@ export class CollaborativeChatModel
super(options);

this._awareness = options.awareness;
const { sharedModel } = options;
const { widgetConfig, sharedModel } = options;

if (sharedModel) {
this._sharedModel = sharedModel;
Expand All @@ -43,7 +46,13 @@ export class CollaborativeChatModel

this.sharedModel.changed.connect(this._onchange, this);

this.config = widgetConfig.config;

this._user = this._awareness.states.get(this._awareness.clientID)?.user;

widgetConfig.configChanged.connect((_, config) => {
this.config = config;
});
}

readonly collaborative = true;
Expand Down
37 changes: 32 additions & 5 deletions packages/jupyterlab-collaborative-chat/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,46 @@
*/

import { Token } from '@lumino/coreutils';
import { ISignal } from '@lumino/signaling';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { IConfig } from 'chat-jupyter';

export const chatFileType: DocumentRegistry.IFileType = {
name: 'chat',
displayName: 'Chat',
mimeTypes: ['text/json', 'application/json'],
extensions: ['.chat'],
fileFormat: 'text',
contentType: 'chat'
};

/**
* The token for the chat file type.
* The token for the chat widget config
*/
export const IChatFileType = new Token<IChatFileType>(
'@jupyter/collaboration:IChatFileType'
export const IWidgetConfig = new Token<IWidgetConfig>(
'@jupyter/collaboration:IChatDocument'
);

/**
* Chat file type.
* Chat widget config
*/
export interface IWidgetConfig {
/**
* The widget config
*/
config: Partial<IConfig>;

/**
* A signal emitting when the configuration for the chats has changed.
*/
configChanged: IConfigChanged;
}

/**
* A signal emitting when the configuration for the chats has changed.
*/
export type IChatFileType = DocumentRegistry.IFileType;
export interface IConfigChanged
extends ISignal<IWidgetConfig, Partial<IConfig>> {}

/**
* Command ids.
Expand Down
Loading

0 comments on commit 5420221

Please sign in to comment.