Skip to content

Commit 64d01f7

Browse files
authored
Collaborative chat sidepanel (#11)
* Includes a panel to manage the chats * Add integration tests * lint * Use the jupyter events sytem to update the chat list * Fix the chat select initialization * Fix factory name after rebase * Add buttons to move the chat between sidepanel and main area * Code cleaning
1 parent 80b8c6d commit 64d01f7

File tree

14 files changed

+771
-65
lines changed

14 files changed

+771
-65
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ jobs:
103103
run: |
104104
set -eux
105105
python -m pip install "jupyterlab>=4.0.0,<5" jupyterlab_${{ matrix.extension }}_chat*.whl
106+
106107
- name: Install dependencies
107108
working-directory: packages/jupyterlab-${{ matrix.extension }}-chat/ui-tests
108109
env:
@@ -125,6 +126,7 @@ jobs:
125126
working-directory: packages/jupyterlab-${{ matrix.extension }}-chat/ui-tests
126127
run: |
127128
jlpm playwright test
129+
128130
- name: Upload Playwright Test report
129131
if: always()
130132
uses: actions/upload-artifact@v3

packages/jupyterlab-collaborative-chat/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,11 @@
6969
"@jupyterlab/services": "^7.0.0",
7070
"@jupyterlab/settingregistry": "^4.0.0",
7171
"@jupyterlab/translation": "^4.0.0",
72+
"@jupyterlab/ui-components": "^4.0.0",
7273
"@lumino/coreutils": "^2.0.0",
7374
"@lumino/signaling": "^2.0.0",
75+
"@lumino/widgets": "^2.0.0",
76+
"react": "^18.2.0",
7477
"y-protocols": "^1.0.5",
7578
"yjs": "^13.5.40"
7679
},
@@ -79,7 +82,7 @@
7982
"@jupyterlab/testutils": "^4.0.0",
8083
"@types/jest": "^29.2.0",
8184
"@types/json-schema": "^7.0.11",
82-
"@types/react": "^18.0.26",
85+
"@types/react": "^18.2.0",
8386
"@types/react-addons-linked-state-mixin": "^0.14.22",
8487
"@typescript-eslint/eslint-plugin": "^6.1.0",
8588
"@typescript-eslint/parser": "^6.1.0",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"title": "Jupyter collaborative chat",
3+
"description": "Configuration for the chat commands",
4+
"type": "object",
5+
"jupyter.lab.toolbars": {
6+
"Chat": [
7+
{
8+
"name": "moveToSide",
9+
"command": "collaborative-chat:moveToSide"
10+
}
11+
]
12+
},
13+
"properties": {},
14+
"additionalProperties": false
15+
}

packages/jupyterlab-collaborative-chat/src/factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { IWidgetConfig } from './token';
2323
*/
2424
export class WidgetConfig implements IWidgetConfig {
2525
/**
26-
* The constructor of the ChatDocument.
26+
* The constructor of the WidgetConfig.
2727
*/
2828
constructor(config: Partial<IConfig>) {
2929
this.config = config;

packages/jupyterlab-collaborative-chat/src/index.ts

Lines changed: 146 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,33 @@ import {
2020
createToolbarFactory,
2121
showErrorMessage
2222
} from '@jupyterlab/apputils';
23-
import { ILauncher } from '@jupyterlab/launcher';
23+
import { PathExt } from '@jupyterlab/coreutils';
2424
import { DocumentRegistry } from '@jupyterlab/docregistry';
25+
import { ILauncher } from '@jupyterlab/launcher';
2526
import { IObservableList } from '@jupyterlab/observables';
2627
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2728
import { Contents } from '@jupyterlab/services';
2829
import { ISettingRegistry } from '@jupyterlab/settingregistry';
2930
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
31+
import { launchIcon } from '@jupyterlab/ui-components';
3032
import { Awareness } from 'y-protocols/awareness';
3133

3234
import {
3335
WidgetConfig,
3436
ChatWidgetFactory,
3537
CollaborativeChatModelFactory
3638
} from './factory';
37-
import { chatFileType, CommandIDs, IWidgetConfig } from './token';
38-
import { CollaborativeChatWidget } from './widget';
39+
import { CollaborativeChatModel } from './model';
40+
import { chatFileType, CommandIDs, IChatPanel, IWidgetConfig } from './token';
41+
import { ChatPanel, CollaborativeChatWidget } from './widget';
3942
import { YChat } from './ychat';
4043

4144
const FACTORY = 'Chat';
4245

4346
const pluginIds = {
4447
chatCommands: 'jupyterlab-collaborative-chat:commands',
45-
docFactories: 'jupyterlab-collaborative-chat:factories'
48+
docFactories: 'jupyterlab-collaborative-chat:factories',
49+
chatPanel: 'jupyterlab-collaborative-chat:chat-panel'
4650
};
4751

4852
/**
@@ -106,7 +110,6 @@ export const docFactories: JupyterFrontEndPlugin<IWidgetConfig> = {
106110
pluginIds.docFactories,
107111
translator
108112
);
109-
console.log('Create toolbarFactory', toolbarFactory);
110113
}
111114

112115
// Wait for the application to be restored and
@@ -194,17 +197,20 @@ export const docFactories: JupyterFrontEndPlugin<IWidgetConfig> = {
194197
};
195198

196199
/**
197-
* Extension registering the chat file type.
200+
* Extension providing the commands, menu and laucher.
198201
*/
199-
export const chatCommands: JupyterFrontEndPlugin<void> = {
202+
const chatCommands: JupyterFrontEndPlugin<void> = {
200203
id: pluginIds.chatCommands,
201204
description: 'The commands to create or open a chat',
202205
autoStart: true,
203-
requires: [ICollaborativeDrive],
204-
optional: [ICommandPalette, ILauncher],
206+
requires: [ICollaborativeDrive, IGlobalAwareness, IWidgetConfig],
207+
optional: [IChatPanel, ICommandPalette, ILauncher],
205208
activate: (
206209
app: JupyterFrontEnd,
207210
drive: ICollaborativeDrive,
211+
awareness: Awareness,
212+
widgetConfig: IWidgetConfig,
213+
chatPanel: ChatPanel | null,
208214
commandPalette: ICommandPalette | null,
209215
launcher: ILauncher | null
210216
) => {
@@ -221,6 +227,7 @@ export const chatCommands: JupyterFrontEndPlugin<void> = {
221227
caption: 'Create a chat',
222228
icon: args => (args.isPalette ? undefined : chatIcon),
223229
execute: async args => {
230+
const inSidePanel: boolean = (args.inSidePanel as boolean) ?? false;
224231
let name: string | null = (args.name as string) ?? null;
225232
let filepath = '';
226233
if (!name) {
@@ -276,11 +283,11 @@ export const chatCommands: JupyterFrontEndPlugin<void> = {
276283
filepath = model.path;
277284
}
278285

279-
return commands.execute(CommandIDs.openChat, { filepath });
286+
return commands.execute(CommandIDs.openChat, { filepath, inSidePanel });
280287
}
281288
});
282289

283-
/**
290+
/*
284291
* Command to open a chat.
285292
*
286293
* args:
@@ -289,6 +296,7 @@ export const chatCommands: JupyterFrontEndPlugin<void> = {
289296
commands.addCommand(CommandIDs.openChat, {
290297
label: 'Open a chat',
291298
execute: async args => {
299+
const inSidePanel: boolean = (args.inSidePanel as boolean) ?? false;
292300
let filepath: string | null = (args.filepath as string) ?? null;
293301
if (filepath === null) {
294302
filepath = (
@@ -317,10 +325,44 @@ export const chatCommands: JupyterFrontEndPlugin<void> = {
317325
return;
318326
}
319327

320-
commands.execute('docmanager:open', {
321-
path: `RTC:${filepath}`,
322-
factory: FACTORY
323-
});
328+
if (inSidePanel && chatPanel) {
329+
// The chat is opened in the chat panel.
330+
app.shell.activateById(chatPanel.id);
331+
const model = await drive.get(filepath);
332+
333+
/**
334+
* Create a share model from the chat file
335+
*/
336+
const sharedModel = drive.sharedModelFactory.createNew({
337+
path: model.path,
338+
format: model.format,
339+
contentType: chatFileType.contentType,
340+
collaborative: true
341+
}) as YChat;
342+
343+
/**
344+
* Initialize the chat model with the share model
345+
*/
346+
const chat = new CollaborativeChatModel({
347+
awareness,
348+
sharedModel,
349+
widgetConfig
350+
});
351+
352+
/**
353+
* Add a chat widget to the side panel.
354+
*/
355+
chatPanel.addChat(
356+
chat,
357+
PathExt.basename(model.name, chatFileType.extensions[0])
358+
);
359+
} else {
360+
// The chat is opened in the main area
361+
commands.execute('docmanager:open', {
362+
path: `RTC:${filepath}`,
363+
factory: FACTORY
364+
});
365+
}
324366
}
325367
});
326368

@@ -348,4 +390,92 @@ export const chatCommands: JupyterFrontEndPlugin<void> = {
348390
}
349391
};
350392

351-
export default [chatCommands, docFactories];
393+
/*
394+
* Extension providing a chat panel.
395+
*/
396+
const chatPanel: JupyterFrontEndPlugin<ChatPanel> = {
397+
id: pluginIds.chatPanel,
398+
description: 'A chat extension for Jupyter',
399+
autoStart: true,
400+
provides: IChatPanel,
401+
requires: [ICollaborativeDrive, IRenderMimeRegistry],
402+
optional: [ILayoutRestorer, IThemeManager],
403+
activate: (
404+
app: JupyterFrontEnd,
405+
drive: ICollaborativeDrive,
406+
rmRegistry: IRenderMimeRegistry,
407+
restorer: ILayoutRestorer | null,
408+
themeManager: IThemeManager | null
409+
): ChatPanel => {
410+
const { commands } = app;
411+
412+
/**
413+
* Add Chat widget to left sidebar
414+
*/
415+
const chatPanel = new ChatPanel({
416+
commands,
417+
drive,
418+
rmRegistry,
419+
themeManager
420+
});
421+
chatPanel.id = 'JupyterCollaborationChat:sidepanel';
422+
chatPanel.title.icon = chatIcon;
423+
chatPanel.title.caption = 'Jupyter Chat'; // TODO: i18n/
424+
425+
app.shell.add(chatPanel, 'left', {
426+
rank: 2000
427+
});
428+
429+
if (restorer) {
430+
restorer.add(chatPanel, 'jupyter-chat');
431+
}
432+
433+
// Use events system to watch changes on files.
434+
const schemaID =
435+
'https://events.jupyter.org/jupyter_server/contents_service/v1';
436+
const actions = ['create', 'delete', 'rename'];
437+
app.serviceManager.events.stream.connect((_, emission) => {
438+
if (emission.schema_id === schemaID) {
439+
const action = emission.action as string;
440+
if (actions.includes(action)) {
441+
chatPanel.updateChatNames();
442+
}
443+
}
444+
});
445+
446+
/*
447+
* Command to move a chat from the main area to the side panel.
448+
*
449+
*/
450+
commands.addCommand(CommandIDs.moveToSide, {
451+
label: 'Move the chat to the side panel',
452+
caption: 'Move the chat to the side panel',
453+
icon: launchIcon,
454+
execute: async () => {
455+
const widget = app.shell.currentWidget;
456+
// Ensure widget is a CollaborativeChatWidget and is in main area
457+
if (
458+
!widget ||
459+
!(widget instanceof CollaborativeChatWidget) ||
460+
!Array.from(app.shell.widgets('main')).includes(widget)
461+
) {
462+
console.error(
463+
`The command '${CommandIDs.moveToSide}' should be executed from the toolbar button only`
464+
);
465+
return;
466+
}
467+
// Remove potential drive prefix
468+
const filepath = widget.context.path.split(':').pop();
469+
commands.execute(CommandIDs.openChat, {
470+
filepath,
471+
inSidePanel: true
472+
});
473+
widget.dispose();
474+
}
475+
});
476+
477+
return chatPanel;
478+
}
479+
};
480+
481+
export default [chatCommands, docFactories, chatPanel];

packages/jupyterlab-collaborative-chat/src/token.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const chatFileType: DocumentRegistry.IFileType = {
1818
icon: chatIcon
1919
};
2020

21+
import { ChatPanel } from './widget';
22+
2123
/**
2224
* The token for the chat widget config
2325
*/
@@ -57,5 +59,16 @@ export const CommandIDs = {
5759
/**
5860
* Open a chat file.
5961
*/
60-
openChat: 'collaborative-chat:open'
62+
openChat: 'collaborative-chat:open',
63+
/**
64+
* Move a main widget to the side panel
65+
*/
66+
moveToSide: 'collaborative-chat:moveToSide'
6167
};
68+
69+
/**
70+
* The chat panel token.
71+
*/
72+
export const IChatPanel = new Token<ChatPanel>(
73+
'@jupyter/collaboration:IChatPanel'
74+
);

packages/jupyterlab-collaborative-chat/src/widget.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)