From cb04fa473c4b151505c36fcccfde9686811058a0 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet <32258950+brichet@users.noreply.github.com> Date: Fri, 6 Dec 2024 01:54:05 +0100 Subject: [PATCH] [v3-dev] Initial migration to `jupyterlab-chat` (#1043) * Very first version of the AI working in jupyterlab_collaborative_chat * Allows both collaborative and regular chat to work with AI * handle the help message in the chat too * Autocompletion (#2) * Fix handler methods' parameters * Add slash commands (autocompletion) to the chat input * Stream messages (#3) * Allow for stream messages * update jupyter collaborative chat dependency * AI settings (#4) * Add a menu option to open the AI settings * Remove the input option from the setting widget * pre-commit * linting * Homogeneize typing for optional arguments * Fix import * Showing that the bot is writing (answering) (#5) * Show that the bot is writing (answering) * Update jupyter chat dependency * Some typing * Update extension to jupyterlab_chat (0.6.0) (#8) * Fix linting * Remove try/except to import jupyterlab_chat (not optional anymore), and fix typing * linter * Python unit tests * Fix typing * lint * Fix lint and mypy all together * Fix web_app settings accessor * Fix jupyter_collaboration version Co-authored-by: david qiu <44106031+dlqqq@users.noreply.github.com> * Remove unecessary try/except * Dedicate one set of chat handlers per room (#9) * create new set of chat handlers per room * make YChat an instance attribute on BaseChatHandler * revert changes to chat handlers * pre-commit * use room_id local var Co-authored-by: Nicolas Brichet <32258950+brichet@users.noreply.github.com> --------- Co-authored-by: Nicolas Brichet <32258950+brichet@users.noreply.github.com> --------- Co-authored-by: david qiu <44106031+dlqqq@users.noreply.github.com> Co-authored-by: david qiu --- .../slash_command.py | 3 +- .../jupyter_ai/chat_handlers/__init__.py | 5 + .../jupyter_ai/chat_handlers/base.py | 147 ++++++--- .../jupyter_ai/chat_handlers/learn.py | 7 +- packages/jupyter-ai/jupyter_ai/constants.py | 8 + packages/jupyter-ai/jupyter_ai/extension.py | 181 ++++++++++- packages/jupyter-ai/jupyter_ai/models.py | 2 +- .../jupyter_ai/tests/test_handlers.py | 1 + packages/jupyter-ai/package.json | 1 + packages/jupyter-ai/pyproject.toml | 1 + packages/jupyter-ai/schema/plugin.json | 21 ++ .../src/components/chat-settings.tsx | 69 +++-- packages/jupyter-ai/src/index.ts | 93 +++++- .../jupyter-ai/src/slash-autocompletion.tsx | 93 ++++++ .../src/widgets/settings-widget.tsx | 26 ++ yarn.lock | 287 +++++++++++++++++- 16 files changed, 849 insertions(+), 96 deletions(-) create mode 100644 packages/jupyter-ai/jupyter_ai/constants.py create mode 100644 packages/jupyter-ai/src/slash-autocompletion.tsx create mode 100644 packages/jupyter-ai/src/widgets/settings-widget.tsx diff --git a/packages/jupyter-ai-module-cookiecutter/{{cookiecutter.root_dir_name}}/{{cookiecutter.python_name}}/slash_command.py b/packages/jupyter-ai-module-cookiecutter/{{cookiecutter.root_dir_name}}/{{cookiecutter.python_name}}/slash_command.py index f82bd5531..d2a858a3f 100644 --- a/packages/jupyter-ai-module-cookiecutter/{{cookiecutter.root_dir_name}}/{{cookiecutter.python_name}}/slash_command.py +++ b/packages/jupyter-ai-module-cookiecutter/{{cookiecutter.root_dir_name}}/{{cookiecutter.python_name}}/slash_command.py @@ -1,5 +1,6 @@ from jupyter_ai.chat_handlers.base import BaseChatHandler, SlashCommandRoutingType from jupyter_ai.models import HumanChatMessage +from jupyterlab_chat.ychat import YChat class TestSlashCommand(BaseChatHandler): @@ -25,5 +26,5 @@ class TestSlashCommand(BaseChatHandler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - async def process_message(self, message: HumanChatMessage): + async def process_message(self, message: HumanChatMessage, chat: YChat): self.reply("This is the `/test` slash command.") diff --git a/packages/jupyter-ai/jupyter_ai/chat_handlers/__init__.py b/packages/jupyter-ai/jupyter_ai/chat_handlers/__init__.py index a8fe9eb50..463369244 100644 --- a/packages/jupyter-ai/jupyter_ai/chat_handlers/__init__.py +++ b/packages/jupyter-ai/jupyter_ai/chat_handlers/__init__.py @@ -1,3 +1,8 @@ +# The following import is to make sure jupyter_ydoc is imported before +# jupyterlab_chat, otherwise it leads to circular import because of the +# YChat relying on YBaseDoc, and jupyter_ydoc registering YChat from the entry point. +import jupyter_ydoc + from .ask import AskChatHandler from .base import BaseChatHandler, SlashCommandRoutingType from .clear import ClearChatHandler diff --git a/packages/jupyter-ai/jupyter_ai/chat_handlers/base.py b/packages/jupyter-ai/jupyter_ai/chat_handlers/base.py index c844650ad..233099151 100644 --- a/packages/jupyter-ai/jupyter_ai/chat_handlers/base.py +++ b/packages/jupyter-ai/jupyter_ai/chat_handlers/base.py @@ -23,6 +23,7 @@ from dask.distributed import Client as DaskClient from jupyter_ai.callback_handlers import MetadataCallbackHandler from jupyter_ai.config_manager import ConfigManager, Logger +from jupyter_ai.constants import BOT from jupyter_ai.history import WrappedBoundedChatHistory from jupyter_ai.models import ( AgentChatMessage, @@ -36,6 +37,7 @@ ) from jupyter_ai_magics import Persona from jupyter_ai_magics.providers import BaseProvider +from jupyterlab_chat.ychat import YChat from langchain.pydantic_v1 import BaseModel from langchain_core.messages import AIMessageChunk from langchain_core.runnables import Runnable @@ -156,6 +158,7 @@ def __init__( chat_handlers: Dict[str, "BaseChatHandler"], context_providers: Dict[str, "BaseCommandContextProvider"], message_interrupted: Dict[str, asyncio.Event], + ychat: Optional[YChat], ): self.log = log self.config_manager = config_manager @@ -178,6 +181,14 @@ def __init__( self.chat_handlers = chat_handlers self.context_providers = context_providers self.message_interrupted = message_interrupted + self.ychat = ychat + self.indexes_by_id: Dict[str, str] = {} + """ + Indexes of messages in the YChat document by message ID. + + TODO: Remove this once `jupyterlab-chat` can update messages by ID + without an index. + """ self.llm: Optional[BaseProvider] = None self.llm_params: Optional[dict] = None @@ -198,7 +209,7 @@ async def on_message(self, message: HumanChatMessage): slash_command = "/" + routing_type.slash_id if routing_type.slash_id else "" if slash_command in lm_provider_klass.unsupported_slash_commands: self.reply( - "Sorry, the selected language model does not support this slash command." + "Sorry, the selected language model does not support this slash command.", ) return @@ -238,7 +249,7 @@ async def process_message(self, message: HumanChatMessage): """ Processes a human message routed to this chat handler. Chat handlers (subclasses) must implement this method. Don't forget to call - `self.reply(, message)` at the end! + `self.reply(, chat, message)` at the end! The method definition does not need to be wrapped in a try/except block; any exceptions raised here are caught by `self.handle_exc()`. @@ -271,11 +282,41 @@ async def _default_handle_exc(self, e: Exception, message: HumanChatMessage): ) self.reply(response, message) + def write_message(self, body: str, id: Optional[str] = None) -> None: + """[Jupyter Chat only] Writes a message to the YChat shared document + that this chat handler is assigned to.""" + if not self.ychat: + return + + bot = self.ychat.get_user(BOT["username"]) + if not bot: + self.ychat.set_user(BOT) + + index = self.indexes_by_id.get(id, None) + id = id if id else str(uuid4()) + new_index = self.ychat.set_message( + { + "type": "msg", + "body": body, + "id": id if id else str(uuid4()), + "time": time.time(), + "sender": BOT["username"], + "raw_time": False, + }, + index=index, + append=True, + ) + + self.indexes_by_id[id] = new_index + return id + def broadcast_message(self, message: Message): """ Broadcasts a message to all WebSocket connections. If there are no WebSocket connections and the message is a chat message, this method directly appends to `self.chat_history`. + + TODO: Remove this after Jupyter Chat migration is complete. """ broadcast = False for websocket in self._root_chat_handlers.values(): @@ -291,20 +332,26 @@ def broadcast_message(self, message: Message): cast(ChatMessage, message) self._chat_history.append(message) - def reply(self, response: str, human_msg: Optional[HumanChatMessage] = None): + def reply( + self, + response: str, + human_msg: Optional[HumanChatMessage] = None, + ): """ Sends an agent message, usually in response to a received `HumanChatMessage`. """ - agent_msg = AgentChatMessage( - id=uuid4().hex, - time=time.time(), - body=response, - reply_to=human_msg.id if human_msg else "", - persona=self.persona, - ) - - self.broadcast_message(agent_msg) + if self.ychat is not None: + self.write_message(response, None) + else: + agent_msg = AgentChatMessage( + id=uuid4().hex, + time=time.time(), + body=response, + reply_to=human_msg.id if human_msg else "", + persona=self.persona, + ) + self.broadcast_message(agent_msg) @property def persona(self): @@ -333,7 +380,10 @@ def start_pending( ellipsis=ellipsis, ) - self.broadcast_message(pending_msg) + if self.ychat is not None and self.ychat.awareness is not None: + self.ychat.awareness.set_local_state_field("isWriting", True) + else: + self.broadcast_message(pending_msg) return pending_msg def close_pending(self, pending_msg: PendingMessage): @@ -347,7 +397,10 @@ def close_pending(self, pending_msg: PendingMessage): id=pending_msg.id, ) - self.broadcast_message(close_pending_msg) + if self.ychat is not None and self.ychat.awareness is not None: + self.ychat.awareness.set_local_state_field("isWriting", False) + else: + self.broadcast_message(close_pending_msg) pending_msg.closed = True @contextlib.contextmanager @@ -361,6 +414,9 @@ def pending( """ Context manager that sends a pending message to the client, and closes it after the block is executed. + + TODO: Simplify it by only modifying the awareness as soon as jupyterlab chat + is the only used chat. """ pending_msg = self.start_pending(text, human_msg=human_msg, ellipsis=ellipsis) try: @@ -470,32 +526,39 @@ def send_help_message(self, human_msg: Optional[HumanChatMessage] = None) -> Non slash_commands_list=slash_commands_list, context_commands_list=context_commands_list, ) - help_message = AgentChatMessage( - id=uuid4().hex, - time=time.time(), - body=help_message_body, - reply_to=human_msg.id if human_msg else "", - persona=self.persona, - ) - self.broadcast_message(help_message) + if self.ychat is not None: + self.write_message(help_message_body, None) + else: + help_message = AgentChatMessage( + id=uuid4().hex, + time=time.time(), + body=help_message_body, + reply_to=human_msg.id if human_msg else "", + persona=self.persona, + ) + self.broadcast_message(help_message) def _start_stream(self, human_msg: HumanChatMessage) -> str: """ Sends an `agent-stream` message to indicate the start of a response stream. Returns the ID of the message, denoted as the `stream_id`. """ - stream_id = uuid4().hex - stream_msg = AgentStreamMessage( - id=stream_id, - time=time.time(), - body="", - reply_to=human_msg.id, - persona=self.persona, - complete=False, - ) + if self.ychat is not None: + stream_id = self.write_message("", None) + else: + stream_id = uuid4().hex + stream_msg = AgentStreamMessage( + id=stream_id, + time=time.time(), + body="", + reply_to=human_msg.id, + persona=self.persona, + complete=False, + ) + + self.broadcast_message(stream_msg) - self.broadcast_message(stream_msg) return stream_id def _send_stream_chunk( @@ -509,13 +572,19 @@ def _send_stream_chunk( Sends an `agent-stream-chunk` message containing content that should be appended to an existing `agent-stream` message with ID `stream_id`. """ - if not metadata: - metadata = {} - - stream_chunk_msg = AgentStreamChunkMessage( - id=stream_id, content=content, stream_complete=complete, metadata=metadata - ) - self.broadcast_message(stream_chunk_msg) + if self.ychat is not None: + self.write_message(content, stream_id) + else: + if not metadata: + metadata = {} + + stream_chunk_msg = AgentStreamChunkMessage( + id=stream_id, + content=content, + stream_complete=complete, + metadata=metadata, + ) + self.broadcast_message(stream_chunk_msg) async def stream_reply( self, diff --git a/packages/jupyter-ai/jupyter_ai/chat_handlers/learn.py b/packages/jupyter-ai/jupyter_ai/chat_handlers/learn.py index e0c6139c0..c350dd1b8 100644 --- a/packages/jupyter-ai/jupyter_ai/chat_handlers/learn.py +++ b/packages/jupyter-ai/jupyter_ai/chat_handlers/learn.py @@ -133,7 +133,7 @@ async def process_message(self, message: HumanChatMessage): em_provider_cls, em_provider_args = self.get_embedding_provider() if not em_provider_cls: self.reply( - "Sorry, please select an embedding provider before using the `/learn` command." + "Sorry, please select an embedding provider before using the `/learn` command.", ) return @@ -163,14 +163,15 @@ async def process_message(self, message: HumanChatMessage): except ModuleNotFoundError as e: self.log.error(e) self.reply( - "No `arxiv` package found. " "Install with `pip install arxiv`." + "No `arxiv` package found. " + "Install with `pip install arxiv`.", ) return except Exception as e: self.log.error(e) self.reply( "An error occurred while processing the arXiv file. " - f"Please verify that the arxiv id {id} is correct." + f"Please verify that the arxiv id {id} is correct.", ) return diff --git a/packages/jupyter-ai/jupyter_ai/constants.py b/packages/jupyter-ai/jupyter_ai/constants.py new file mode 100644 index 000000000..ab212fb23 --- /dev/null +++ b/packages/jupyter-ai/jupyter_ai/constants.py @@ -0,0 +1,8 @@ +# The BOT currently has a fixed username, because this username is used has key in chats, +# it needs to constant. Do we need to change it ? +BOT = { + "username": "5f6a7570-7974-6572-6e61-75742d626f74", + "name": "Jupyternaut", + "display_name": "Jupyternaut", + "initials": "J", +} diff --git a/packages/jupyter-ai/jupyter_ai/extension.py b/packages/jupyter-ai/jupyter_ai/extension.py index 08c8c5a47..31a2e4fe0 100644 --- a/packages/jupyter-ai/jupyter_ai/extension.py +++ b/packages/jupyter-ai/jupyter_ai/extension.py @@ -2,18 +2,28 @@ import re import time import types +import uuid +from functools import partial +from typing import Dict, Optional +import traitlets from dask.distributed import Client as DaskClient from importlib_metadata import entry_points from jupyter_ai.chat_handlers.learn import Retriever +from jupyter_ai.models import HumanChatMessage from jupyter_ai_magics import BaseProvider, JupyternautPersona from jupyter_ai_magics.utils import get_em_providers, get_lm_providers +from jupyter_events import EventLogger from jupyter_server.extension.application import ExtensionApp +from jupyter_server.utils import url_path_join +from jupyterlab_chat.ychat import YChat +from pycrdt import ArrayEvent from tornado.web import StaticFileHandler -from traitlets import Dict, Integer, List, Unicode +from traitlets import Integer, List, Unicode from .chat_handlers import ( AskChatHandler, + BaseChatHandler, ClearChatHandler, DefaultChatHandler, ExportChatHandler, @@ -24,6 +34,7 @@ ) from .completions.handlers import DefaultInlineCompletionHandler from .config_manager import ConfigManager +from .constants import BOT from .context_providers import BaseCommandContextProvider, FileContextProvider from .handlers import ( ApiKeysHandler, @@ -37,11 +48,26 @@ ) from .history import BoundedChatHistory +from jupyter_collaboration import ( # type:ignore[import-untyped] # isort:skip + __version__ as jupyter_collaboration_version, +) + + JUPYTERNAUT_AVATAR_ROUTE = JupyternautPersona.avatar_route JUPYTERNAUT_AVATAR_PATH = str( os.path.join(os.path.dirname(__file__), "static", "jupyternaut.svg") ) +JCOLLAB_VERSION = int(jupyter_collaboration_version[0]) + +if JCOLLAB_VERSION >= 3: + from jupyter_server_ydoc.utils import ( # type:ignore[import-untyped] + JUPYTER_COLLABORATION_EVENTS_URI, + ) +else: + from jupyter_collaboration.utils import ( # type:ignore[import-untyped] + JUPYTER_COLLABORATION_EVENTS_URI, + ) DEFAULT_HELP_MESSAGE_TEMPLATE = """Hi there! I'm {persona_name}, your programming assistant. You can ask me a question using the text box below. You can also use these commands: @@ -121,9 +147,9 @@ class AiExtension(ExtensionApp): config=True, ) - model_parameters = Dict( + model_parameters = traitlets.Dict( key_trait=Unicode(), - value_trait=Dict(), + value_trait=traitlets.Dict(), default_value={}, help="""Key-value pairs for model id and corresponding parameters that are passed to the provider class. The values are unpacked and passed to @@ -161,7 +187,7 @@ class AiExtension(ExtensionApp): config=True, ) - default_api_keys = Dict( + default_api_keys = traitlets.Dict( key_trait=Unicode(), value_trait=Unicode(), default_value=None, @@ -204,6 +230,135 @@ class AiExtension(ExtensionApp): config=True, ) + def initialize(self): + super().initialize() + + self.chat_handlers_by_room: Dict[str, Dict[str, BaseChatHandler]] = {} + """ + Nested dictionary that returns the dedicated chat handler instance that + should be used, given the room ID and command ID respectively. + + Example: `self.chat_handlers_by_room[]` yields the set of chat + handlers dedicated to the room identified by ``. + """ + + self.ychats_by_room: Dict[str, YChat] = {} + """Cache of YChat instances, indexed by room ID.""" + + self.event_logger = self.serverapp.web_app.settings["event_logger"] + self.event_logger.add_listener( + schema_id=JUPYTER_COLLABORATION_EVENTS_URI, listener=self.connect_chat + ) + + async def connect_chat( + self, logger: EventLogger, schema_id: str, data: dict + ) -> None: + # ignore events that are not chat room initialization events + if not ( + data["room"].startswith("text:chat:") + and data["action"] == "initialize" + and data["msg"] == "Room initialized" + ): + return + + # log room ID + room_id = data["room"] + self.log.info(f"Connecting to a chat room with room ID: {room_id}.") + + # get YChat document associated with the room + ychat = await self.get_chat(room_id) + if ychat is None: + return + + # Add the bot user to the chat document awareness. + BOT["avatar_url"] = url_path_join( + self.settings.get("base_url", "/"), "api/ai/static/jupyternaut.svg" + ) + if ychat.awareness is not None: + ychat.awareness.set_local_state_field("user", BOT) + + # initialize chat handlers for new chat + self.chat_handlers_by_room[room_id] = self._init_chat_handlers(ychat) + + callback = partial(self.on_change, room_id) + ychat.ymessages.observe(callback) + + async def get_chat(self, room_id: str) -> Optional[YChat]: + """ + Retrieves the YChat instance associated with a room ID. This method + is cached, i.e. successive calls with the same room ID quickly return a + cached value. + + TODO: Determine if get_chat() should ever fail under normal usage + scenarios. If not, we should just raise an exception if chat is `None`, + and indicate the return type as just `YChat` instead of + `Optional[YChat]`. This will simplify the code by removing redundant + null checks. + """ + if room_id in self.ychats_by_room: + return self.ychats_by_room[room_id] + + if JCOLLAB_VERSION >= 3: + collaboration = self.serverapp.web_app.settings["jupyter_server_ydoc"] + document = await collaboration.get_document(room_id=room_id, copy=False) + else: + collaboration = self.serverapp.web_app.settings["jupyter_collaboration"] + server = collaboration.ywebsocket_server + + room = await server.get_room(room_id) + document = room._document + + self.ychats_by_room[room_id] = document + return document + + def on_change(self, room_id: str, events: ArrayEvent) -> None: + for change in events.delta: # type:ignore[attr-defined] + if not "insert" in change.keys(): + continue + messages = change["insert"] + for message in messages: + + if message["sender"] == BOT["username"] or message["raw_time"]: + continue + chat_message = HumanChatMessage( + id=message["id"], + time=time.time(), + body=message["body"], + prompt="", + selection=None, + client=None, + ) + if self.serverapp is not None: + self.serverapp.io_loop.asyncio_loop.create_task( # type:ignore[attr-defined] + self.route_human_message(room_id, chat_message) + ) + + async def route_human_message(self, room_id: str, message: HumanChatMessage): + """ + Method that routes an incoming `HumanChatMessage` to the appropriate + chat handler. + """ + chat_handlers = self.chat_handlers_by_room[room_id] + default = chat_handlers["default"] + # Split on any whitespace, either spaces or newlines + maybe_command = message.body.split(None, 1)[0] + is_command = ( + message.body.startswith("/") + and maybe_command in chat_handlers.keys() + and maybe_command != "default" + ) + command = maybe_command if is_command else "default" + + start = time.time() + if is_command: + await chat_handlers[command].on_message(message) + else: + await default.on_message(message) + + latency_ms = round((time.time() - start) * 1000) + command_readable = "Default" if command == "default" else command + self.log.info(f"{command_readable} chat handler resolved in {latency_ms} ms.") + def initialize_settings(self): start = time.time() @@ -298,7 +453,7 @@ def initialize_settings(self): self.settings["jai_message_interrupted"] = {} # initialize chat handlers - self._init_chat_handlers() + self.settings["jai_chat_handlers"] = self._init_chat_handlers() # initialize context providers self._init_context_provders() @@ -320,7 +475,7 @@ def _show_help_message(self): default_chat_handler: DefaultChatHandler = self.settings["jai_chat_handlers"][ "default" ] - default_chat_handler.send_help_message() + default_chat_handler.send_help_message(None) async def _get_dask_client(self): return DaskClient(processes=False, asynchronous=True) @@ -349,7 +504,15 @@ async def _stop_extension(self): await dask_client.close() self.log.debug("Closed Dask client.") - def _init_chat_handlers(self): + def _init_chat_handlers( + self, ychat: Optional[YChat] = None + ) -> Dict[str, BaseChatHandler]: + """ + Initializes a set of chat handlers. May accept a YChat instance for + collaborative chats. + + TODO: Make `ychat` required once Jupyter Chat migration is complete. + """ eps = entry_points() chat_handler_eps = eps.select(group="jupyter_ai.chat_handlers") chat_handlers = {} @@ -367,6 +530,7 @@ def _init_chat_handlers(self): "chat_handlers": chat_handlers, "context_providers": self.settings["jai_context_providers"], "message_interrupted": self.settings["jai_message_interrupted"], + "ychat": ychat, } default_chat_handler = DefaultChatHandler(**chat_handler_kwargs) clear_chat_handler = ClearChatHandler(**chat_handler_kwargs) @@ -439,8 +603,7 @@ def _init_chat_handlers(self): # Make help always appear as the last command chat_handlers["/help"] = HelpChatHandler(**chat_handler_kwargs) - # bind chat handlers to settings - self.settings["jai_chat_handlers"] = chat_handlers + return chat_handlers def _init_context_provders(self): eps = entry_points() diff --git a/packages/jupyter-ai/jupyter_ai/models.py b/packages/jupyter-ai/jupyter_ai/models.py index 6bd7d4e06..6874b45d0 100644 --- a/packages/jupyter-ai/jupyter_ai/models.py +++ b/packages/jupyter-ai/jupyter_ai/models.py @@ -150,7 +150,7 @@ class HumanChatMessage(BaseModel): """The prompt typed into the chat input by the user.""" selection: Optional[Selection] """The selection included with the prompt, if any.""" - client: ChatClient + client: Optional[ChatClient] class ClearMessage(BaseModel): diff --git a/packages/jupyter-ai/jupyter_ai/tests/test_handlers.py b/packages/jupyter-ai/jupyter_ai/tests/test_handlers.py index 81108bdb7..4d851b2b2 100644 --- a/packages/jupyter-ai/jupyter_ai/tests/test_handlers.py +++ b/packages/jupyter-ai/jupyter_ai/tests/test_handlers.py @@ -78,6 +78,7 @@ def broadcast_message(message: Message) -> None: chat_handlers={}, context_providers={}, message_interrupted={}, + ychat=None, ) diff --git a/packages/jupyter-ai/package.json b/packages/jupyter-ai/package.json index db71f52ae..d0c899662 100644 --- a/packages/jupyter-ai/package.json +++ b/packages/jupyter-ai/package.json @@ -61,6 +61,7 @@ "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", + "@jupyter/chat": "^0.6.0", "@jupyter/collaboration": "^1", "@jupyterlab/application": "^4.2.0", "@jupyterlab/apputils": "^4.2.0", diff --git a/packages/jupyter-ai/pyproject.toml b/packages/jupyter-ai/pyproject.toml index c9d1b5d53..90581c718 100644 --- a/packages/jupyter-ai/pyproject.toml +++ b/packages/jupyter-ai/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "typing_extensions>=4.5.0", "traitlets>=5.0", "deepmerge>=2.0,<3", + "jupyterlab-chat>=0.6.0", ] dynamic = ["version", "description", "authors", "urls", "keywords"] diff --git a/packages/jupyter-ai/schema/plugin.json b/packages/jupyter-ai/schema/plugin.json index 78804b5c6..37e0a4671 100644 --- a/packages/jupyter-ai/schema/plugin.json +++ b/packages/jupyter-ai/schema/plugin.json @@ -12,6 +12,27 @@ "preventDefault": false } ], + "jupyter.lab.menus": { + "main": [ + { + "id": "jp-mainmenu-settings", + "items": [ + { + "type": "separator", + "rank": 110 + }, + { + "command": "jupyter-ai:open-settings", + "rank": 110 + }, + { + "type": "separator", + "rank": 110 + } + ] + } + ] + }, "additionalProperties": false, "type": "object" } diff --git a/packages/jupyter-ai/src/components/chat-settings.tsx b/packages/jupyter-ai/src/components/chat-settings.tsx index a1ad0a9b6..78312edee 100644 --- a/packages/jupyter-ai/src/components/chat-settings.tsx +++ b/packages/jupyter-ai/src/components/chat-settings.tsx @@ -34,6 +34,9 @@ type ChatSettingsProps = { rmRegistry: IRenderMimeRegistry; completionProvider: IJaiCompletionProvider | null; openInlineCompleterSettings: () => void; + // The temporary input options, should be removed when jupyterlab chat is + // the only chat. + inputOptions?: boolean; }; /** @@ -511,36 +514,42 @@ export function ChatSettings(props: ChatSettingsProps): JSX.Element { onSuccess={server.refetchApiKeys} /> - {/* Input */} -

Input

- - - When writing a message, press Enter to: - - { - setSendWse(e.target.value === 'newline'); - }} - > - } - label="Send the message" - /> - } - label={ - <> - Start a new line (use Shift+Enter to send) - - } - /> - - + {/* Input - to remove when jupyterlab chat is the only chat */} + {(props.inputOptions ?? true) && ( + <> +

Input

+ + + When writing a message, press Enter to: + + { + setSendWse(e.target.value === 'newline'); + }} + > + } + label="Send the message" + /> + } + label={ + <> + Start a new line (use Shift+Enter to + send) + + } + /> + + + + )} +