From 48e9c42975ffa2acf583de459f1276789d791d02 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Tue, 20 Feb 2024 12:30:12 +0100 Subject: [PATCH] Uses the jupyterlab_chat package (back end and front end) to add a collaborative chat --- jupyter_collaboration/__init__.py | 8 ++- jupyter_collaboration/app.py | 16 ++++- jupyter_collaboration/handlers.py | 25 ++++++++ packages/collaboration-extension/package.json | 1 + packages/collaboration-extension/src/chat.ts | 60 +++++++++++++++++++ packages/collaboration-extension/src/index.ts | 2 + 6 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 packages/collaboration-extension/src/chat.ts diff --git a/jupyter_collaboration/__init__.py b/jupyter_collaboration/__init__.py index db2e3e8a..67278f2e 100644 --- a/jupyter_collaboration/__init__.py +++ b/jupyter_collaboration/__init__.py @@ -4,12 +4,14 @@ from typing import Any, Dict, List from ._version import __version__ # noqa -from .app import YDocExtension - +from .app import CollaborativeChatExtension, YDocExtension def _jupyter_labextension_paths(): return [{"src": "labextension", "dest": "@jupyter/collaboration-extension"}] def _jupyter_server_extension_points() -> List[Dict[str, Any]]: - return [{"module": "jupyter_collaboration", "app": YDocExtension}] + return [ + {"module": "jupyter_collaboration", "app": YDocExtension}, + {"module": "jupyterlab_chat", "app": CollaborativeChatExtension}, + ] diff --git a/jupyter_collaboration/app.py b/jupyter_collaboration/app.py index 27c78a6c..a183dba7 100644 --- a/jupyter_collaboration/app.py +++ b/jupyter_collaboration/app.py @@ -7,8 +7,10 @@ from jupyter_server.extension.application import ExtensionApp from pycrdt_websocket.ystore import BaseYStore from traitlets import Bool, Float, Type +from jupyterlab_chat import ChatExtension +from jupyterlab_chat.handlers import GlobalConfigHandler, ChatHistoryHandler -from .handlers import DocSessionHandler, YDocWebSocketHandler +from .handlers import CollaborativeChatHandler, DocSessionHandler, YDocWebSocketHandler from .loaders import FileLoaderMapping from .stores import SQLiteYStore from .utils import EVENTS_SCHEMA_PATH @@ -120,3 +122,15 @@ async def stop_extension(self): ], timeout=3, ) + + +class CollaborativeChatExtension(ChatExtension): + + def initialize_handlers(self): + self.handlers.extend([ + (fr"{self.base_url}?", CollaborativeChatHandler), + (fr"{self.base_url}config/?", GlobalConfigHandler), + (fr"{self.base_url}history/?", ChatHistoryHandler) + ]) + + self.log.debug("Collaborative Chat Handlers initialized") diff --git a/jupyter_collaboration/handlers.py b/jupyter_collaboration/handlers.py index 5a760b1b..eeb62c03 100644 --- a/jupyter_collaboration/handlers.py +++ b/jupyter_collaboration/handlers.py @@ -7,6 +7,7 @@ import json import time import uuid +from dataclasses import asdict from typing import Any from jupyter_server.auth import authorized @@ -17,6 +18,8 @@ from pycrdt_websocket.yutils import YMessageType, write_var_uint from tornado import web from tornado.websocket import WebSocketHandler +from jupyterlab_chat.handlers import ChatHandler +from jupyterlab_chat.models import ChatUser from .loaders import FileLoaderMapping from .rooms import DocumentRoom, TransientRoom @@ -381,3 +384,25 @@ async def put(self, path): ) self.set_status(201) return self.finish(data) + + +class CollaborativeChatHandler(ChatHandler): + + def get_chat_user(self) -> ChatUser: + """Retrieves the current user from collaborative context.""" + + names = self.current_user.name.split(" ", maxsplit=2) + initials = getattr(self.current_user, "initials", None) + if not initials: + # compute default initials in case IdentityProvider doesn't + # return initials, e.g. JupyterHub (#302) + names = self.current_user.name.split(" ", maxsplit=2) + initials = "".join( + [(name.capitalize()[0] if len(name) > 0 else "") for name in names] + ) + chat_user_kwargs = { + **asdict(self.current_user), + "initials": initials, + } + + return ChatUser(**chat_user_kwargs) diff --git a/packages/collaboration-extension/package.json b/packages/collaboration-extension/package.json index 3767778c..874bdab0 100644 --- a/packages/collaboration-extension/package.json +++ b/packages/collaboration-extension/package.json @@ -58,6 +58,7 @@ "@jupyter/ydoc": "^1.1.0-a0", "@jupyterlab/application": "^4.0.5", "@jupyterlab/apputils": "^4.0.5", + "@jupyterlab/chat": "0.1.0", "@jupyterlab/codemirror": "^4.0.5", "@jupyterlab/coreutils": "^6.0.5", "@jupyterlab/docregistry": "^4.0.5", diff --git a/packages/collaboration-extension/src/chat.ts b/packages/collaboration-extension/src/chat.ts new file mode 100644 index 00000000..735757a1 --- /dev/null +++ b/packages/collaboration-extension/src/chat.ts @@ -0,0 +1,60 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +/** + * @packageDocumentation + * @module collaboration-extension + */ + +import { + ILayoutRestorer, + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; +import { ReactWidget, IThemeManager } from '@jupyterlab/apputils'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; + +import { + buildChatSidebar, + buildErrorWidget, + ChatHandler +} from '@jupyterlab/chat'; + +/** + * Initialization of the @jupyterlab/chat extension. + */ +export const chat: JupyterFrontEndPlugin = { + id: '@jupyterlab-extension:chat', + description: 'A chat extension for Jupyterlab', + autoStart: true, + optional: [ILayoutRestorer, IThemeManager], + requires: [IRenderMimeRegistry], + activate: async ( + app: JupyterFrontEnd, + rmRegistry: IRenderMimeRegistry, + restorer: ILayoutRestorer | null, + themeManager: IThemeManager | null + ) => { + /** + * Initialize chat handler, open WS connection + */ + const chatHandler = new ChatHandler(); + + let chatWidget: ReactWidget | null = null; + try { + await chatHandler.initialize(); + chatWidget = buildChatSidebar(chatHandler, themeManager, rmRegistry); + } catch (e) { + chatWidget = buildErrorWidget(themeManager); + } + + /** + * Add Chat widget to right sidebar + */ + app.shell.add(chatWidget, 'left', { rank: 2000 }); + + if (restorer) { + restorer.add(chatWidget, 'jupyterlab-chat'); + } + console.log('Collaboration chat initialized'); + } +}; diff --git a/packages/collaboration-extension/src/index.ts b/packages/collaboration-extension/src/index.ts index 513befa1..d873b025 100644 --- a/packages/collaboration-extension/src/index.ts +++ b/packages/collaboration-extension/src/index.ts @@ -7,6 +7,7 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { chat } from './chat'; import { drive, yfile, @@ -27,6 +28,7 @@ import { sharedLink } from './sharedlink'; * Export the plugins as default. */ const plugins: JupyterFrontEndPlugin[] = [ + chat, drive, yfile, ynotebook,