diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eed3d1e..de5a3c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,143 +15,143 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - - name: Install dependencies - run: python -m pip install -U "jupyterlab>=4.0.0,<5" - - - name: Lint the extension - run: | - set -eux - jlpm - jlpm run lint:check - - - name: Test the extension - run: | - set -eux - jlpm run test - - - name: Build the extension - run: | - set -eux - python -m pip install .[test] - - pytest -vv -r ap --cov jupyter_ai_chat_commands - jupyter server extension list - jupyter server extension list 2>&1 | grep -ie "jupyter_ai_chat_commands.*OK" - - jupyter labextension list - jupyter labextension list 2>&1 | grep -ie "@jupyter-ai/chat-commands.*OK" - python -m jupyterlab.browser_check - - - name: Package the extension - run: | - set -eux - - pip install build - python -m build - pip uninstall -y "jupyter_ai_chat_commands" jupyterlab - - - name: Upload extension packages - uses: actions/upload-artifact@v4 - with: - name: extension-artifacts - path: dist/jupyter_ai_chat_commands* - if-no-files-found: error + - name: Checkout + uses: actions/checkout@v4 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Install dependencies + run: python -m pip install -U "jupyterlab>=4.0.0,<5" + + - name: Lint the extension + run: | + set -eux + jlpm + jlpm run lint:check + + - name: Test the extension + run: | + set -eux + jlpm run test + + - name: Build the extension + run: | + set -eux + python -m pip install .[test] + + pytest -vv -r ap --cov jupyter_ai_chat_commands + jupyter server extension list + jupyter server extension list 2>&1 | grep -ie "jupyter_ai_chat_commands.*OK" + + jupyter labextension list + jupyter labextension list 2>&1 | grep -ie "@jupyter-ai/chat-commands.*OK" + python -m jupyterlab.browser_check + + - name: Package the extension + run: | + set -eux + + pip install build + python -m build + pip uninstall -y "jupyter_ai_chat_commands" jupyterlab + + - name: Upload extension packages + uses: actions/upload-artifact@v4 + with: + name: extension-artifacts + path: dist/jupyter_ai_chat_commands* + if-no-files-found: error test_isolated: needs: build runs-on: ubuntu-latest steps: - - name: Install Python - uses: actions/setup-python@v5 - with: - python-version: '3.9' - architecture: 'x64' - - uses: actions/download-artifact@v4 - with: - name: extension-artifacts - - name: Install and Test - run: | - set -eux - # Remove NodeJS, twice to take care of system and locally installed node versions. - sudo rm -rf $(which node) - sudo rm -rf $(which node) - - pip install "jupyterlab>=4.0.0,<5" jupyter_ai_chat_commands*.whl - - - jupyter server extension list - jupyter server extension list 2>&1 | grep -ie "jupyter_ai_chat_commands.*OK" - - jupyter labextension list - jupyter labextension list 2>&1 | grep -ie "@jupyter-ai/chat-commands.*OK" - python -m jupyterlab.browser_check --no-browser-test - - integration-tests: - name: Integration tests - needs: build - runs-on: ubuntu-latest - - env: - PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - - name: Download extension package - uses: actions/download-artifact@v4 - with: - name: extension-artifacts - - - name: Install the extension - run: | - set -eux - python -m pip install "jupyterlab>=4.0.0,<5" jupyter_ai_chat_commands*.whl - - - name: Install dependencies - working-directory: ui-tests - env: - YARN_ENABLE_IMMUTABLE_INSTALLS: 0 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - run: jlpm install - - - name: Set up browser cache - uses: actions/cache@v4 - with: - path: | - ${{ github.workspace }}/pw-browsers - key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} - - - name: Install browser - run: | - set -eux - jlpm playwright install-deps - jlpm playwright install chromium - working-directory: ui-tests - - - name: Execute integration tests - working-directory: ui-tests - run: | - jlpm playwright test - - - name: Upload Playwright Test report - if: always() - uses: actions/upload-artifact@v4 - with: - name: jupyter_ai_chat_commands-playwright-tests - path: | - ui-tests/test-results - ui-tests/playwright-report + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + architecture: 'x64' + - uses: actions/download-artifact@v4 + with: + name: extension-artifacts + - name: Install and Test + run: | + set -eux + # Remove NodeJS, twice to take care of system and locally installed node versions. + sudo rm -rf $(which node) + sudo rm -rf $(which node) + + pip install "jupyterlab>=4.0.0,<5" jupyter_ai_chat_commands*.whl + + + jupyter server extension list + jupyter server extension list 2>&1 | grep -ie "jupyter_ai_chat_commands.*OK" + + jupyter labextension list + jupyter labextension list 2>&1 | grep -ie "@jupyter-ai/chat-commands.*OK" + python -m jupyterlab.browser_check --no-browser-test + + # integration-tests: + # name: Integration tests + # needs: build + # runs-on: ubuntu-latest + + # env: + # PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers + + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + + # - name: Base Setup + # uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + # - name: Download extension package + # uses: actions/download-artifact@v4 + # with: + # name: extension-artifacts + + # - name: Install the extension + # run: | + # set -eux + # python -m pip install "jupyterlab>=4.0.0,<5" jupyter_ai_chat_commands*.whl + + # - name: Install dependencies + # working-directory: ui-tests + # env: + # YARN_ENABLE_IMMUTABLE_INSTALLS: 0 + # PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + # run: jlpm install + + # - name: Set up browser cache + # uses: actions/cache@v4 + # with: + # path: | + # ${{ github.workspace }}/pw-browsers + # key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} + + # - name: Install browser + # run: | + # set -eux + # jlpm playwright install-deps + # jlpm playwright install chromium + # working-directory: ui-tests + + # - name: Execute integration tests + # working-directory: ui-tests + # run: | + # jlpm playwright test + + # - name: Upload Playwright Test report + # if: always() + # uses: actions/upload-artifact@v4 + # with: + # name: jupyter_ai_chat_commands-playwright-tests + # path: | + # ui-tests/test-results + # ui-tests/playwright-report check_links: name: Check Links diff --git a/.gitignore b/.gitignore index 37cea1e..9cfa26a 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,6 @@ dmypy.json # Yarn cache .yarn/ + +*.chat +.jupyter_ystore.db \ No newline at end of file diff --git a/README.md b/README.md index ebd382f..279141e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ This extension is composed of a Python package named `jupyter_ai_chat_commands` for the server extension and a NPM package named `@jupyter-ai/chat-commands` for the frontend extension. +This package provides 2 commands: + +- `@file:`: Add a file as an attachment to a message. + +- `/refresh-personas`: Reload local personas defined in `.jupyter/personas`. + ## QUICK START Everything that follows after this section was from the extension template. We diff --git a/jupyter_ai_chat_commands/__init__.py b/jupyter_ai_chat_commands/__init__.py index 5963be7..82ca1d2 100644 --- a/jupyter_ai_chat_commands/__init__.py +++ b/jupyter_ai_chat_commands/__init__.py @@ -7,7 +7,7 @@ import warnings warnings.warn("Importing 'jupyter_ai_chat_commands' outside a proper installation.") __version__ = "dev" -from .handlers import setup_handlers +from .extension_app import JaiChatCommandsExtension def _jupyter_labextension_paths(): @@ -19,18 +19,7 @@ def _jupyter_labextension_paths(): def _jupyter_server_extension_points(): return [{ - "module": "jupyter_ai_chat_commands" + "module": "jupyter_ai_chat_commands", + "app": JaiChatCommandsExtension }] - -def _load_jupyter_server_extension(server_app): - """Registers the API handler to receive HTTP requests from the frontend extension. - - Parameters - ---------- - server_app: jupyterlab.labapp.LabApp - JupyterLab application instance - """ - setup_handlers(server_app.web_app) - name = "jupyter_ai_chat_commands" - server_app.log.info(f"Registered {name} server extension") diff --git a/jupyter_ai_chat_commands/extension_app.py b/jupyter_ai_chat_commands/extension_app.py new file mode 100644 index 0000000..813bbba --- /dev/null +++ b/jupyter_ai_chat_commands/extension_app.py @@ -0,0 +1,107 @@ +import asyncio +from jupyter_server.extension.application import ExtensionApp +from .handlers import RouteHandler + +from jupyterlab_chat.models import Message +from jupyter_ai_router.router import MessageRouter +from jupyter_ai_persona_manager import PersonaManager + +class JaiChatCommandsExtension(ExtensionApp): + + name = "jupyter_ai_chat_commands" + handlers = [ + (r"jupyter-ai-chat-commands/get-example/?", RouteHandler) + ] + + @property + def event_loop(self) -> asyncio.AbstractEventLoop: + """ + Returns a reference to the asyncio event loop. + """ + return asyncio.get_event_loop_policy().get_event_loop() + + def initialize_settings(self): + """ + See `self.initialize_async()`. + """ + super().initialize_settings() + self.event_loop.create_task(self.initialize_async()) + + async def initialize_async(self): + """ + This method waits for the `MessageRouter` singleton to be initialized by + `jupyter_ai_router`. It attaches an observer to listen to when a chat is + initialized, which in turn attaches an observer to listen to when a + slash command is called in any chat. + """ + router: MessageRouter = await self._get_router() + self.log.debug("Obtained reference to router.") + + router.observe_chat_init(lambda room_id, ychat: self.on_chat_init(router, room_id)) + + def on_chat_init(self, router: MessageRouter, room_id: str): + router.observe_slash_cmd_msg(room_id, self.on_slash_command) + self.log.info("Attached router observer.") + + def on_slash_command(self, room_id: str, message: Message): + first_word = get_first_word(message.body) + assert first_word and first_word.startswith("/") + + command_id = first_word[1:] + if command_id == "refresh-personas": + self.event_loop.create_task(self.handle_refresh_personas(room_id)) + return True + + # If command is unrecognized, log an error + self.log.warning(f"Unrecognized slash command: '/{command_id}'") + return False + + async def handle_refresh_personas(self, room_id: str): + self.log.info(f"Received /refresh-personas in room '{room_id}'.") + persona_manager = await self._get_persona_manager(room_id) + self.log.info(f"Retrieved PersonaManager in room '{room_id}'.") + await persona_manager.refresh_personas() + self.log.info(f"Handled /refresh-personas in room '{room_id}'.") + + + async def _get_router(self) -> MessageRouter: + """ + Returns the `MessageRouter` singleton initialized by + `jupyter_ai_router`. + """ + while True: + router = self.serverapp.web_app.settings.get("jupyter-ai", {}).get("router") + if router is not None: + return router + await asyncio.sleep(0.1) # Check every 100ms + + async def _get_persona_manager(self, room_id: str) -> PersonaManager: + """ + Returns the `PersonaManager` for a chat given its room ID. + """ + while True: + persona_managers_by_room = self.serverapp.web_app.settings.get("jupyter-ai", {}).get("persona-managers", {}) + manager = persona_managers_by_room.get(room_id) + if manager is not None: + return manager + await asyncio.sleep(0.1) # Check every 100ms + + +def get_first_word(input_str: str) -> str | None: + """ + Finds the first word in a given string, ignoring leading whitespace. + Returns the first word, or None if there is no first word. + """ + start = 0 + + # Skip leading whitespace + while start < len(input_str) and input_str[start].isspace(): + start += 1 + + # Find end of first word + end = start + while end < len(input_str) and not input_str[end].isspace(): + end += 1 + + first_word = input_str[start:end] + return first_word if first_word else None \ No newline at end of file diff --git a/package.json b/package.json index 9768ab4..8783e23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jupyter-ai/chat-commands", - "version": "0.1.0-alpha.0", + "version": "0.0.0", "description": "Package providing the set of default chat commands in Jupyter AI.", "keywords": [ "jupyter", @@ -58,9 +58,11 @@ "watch:labextension": "jupyter labextension watch ." }, "dependencies": { + "@jupyter/chat": "^0.17.0", "@jupyterlab/application": "^4.0.0", "@jupyterlab/coreutils": "^6.0.0", - "@jupyterlab/services": "^7.0.0" + "@jupyterlab/services": "^7.0.0", + "@mui/icons-material": "^5.11.0" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", diff --git a/pyproject.toml b/pyproject.toml index 7c8f623..9a661ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,16 @@ [build-system] -requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"] +requires = [ + "hatchling>=1.5.0", + "jupyterlab>=4.0.0,<5", + "hatch-nodejs-version>=0.3.2", +] build-backend = "hatchling.build" [project] name = "jupyter_ai_chat_commands" readme = "README.md" license = { file = "LICENSE" } -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Framework :: Jupyter", "Framework :: Jupyter :: JupyterLab", @@ -16,14 +20,16 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] dependencies = [ - "jupyter_server>=2.4.0,<3" + "jupyter_server>=2.4.0,<3", + "jupyterlab_chat>=0.17.0,<0.18.0", + "jupyter_ai_router>=0.0.1", + "jupyter_ai_persona_manager>=0.0.3", ] dynamic = ["version", "description", "authors", "urls", "keywords"] @@ -33,7 +39,7 @@ test = [ "pytest", "pytest-asyncio", "pytest-cov", - "pytest-jupyter[server]>=0.6.0" + "pytest-jupyter[server]>=0.6.0", ] [tool.hatch.version] @@ -77,10 +83,11 @@ build_dir = "jupyter_ai_chat_commands/labextension" version_cmd = "hatch version" [tool.jupyter-releaser.hooks] +before-bump-version = ["pip install hatch virtualenv!=20.35.0"] before-build-npm = [ "python -m pip install 'jupyterlab>=4.0.0,<5'", "jlpm", - "jlpm build:prod" + "jlpm build:prod", ] before-build-python = ["jlpm clean:all"] diff --git a/src/chat-command-plugins/file-command.tsx b/src/chat-command-plugins/file-command.tsx new file mode 100644 index 0000000..6c6a2dd --- /dev/null +++ b/src/chat-command-plugins/file-command.tsx @@ -0,0 +1,232 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import React from 'react'; +import { JupyterFrontEndPlugin } from '@jupyterlab/application'; +import type { Contents } from '@jupyterlab/services'; +import type { DocumentRegistry } from '@jupyterlab/docregistry'; +import { + IChatCommandProvider, + IChatCommandRegistry, + IInputModel, + ChatCommand +} from '@jupyter/chat'; +import FindInPage from '@mui/icons-material/FindInPage'; + +const FILE_COMMAND_PROVIDER_ID = '@jupyter-ai/core:file-command-provider'; + +/** + * A command provider that provides completions for `@file` commands and handles + * `@file` command calls. + */ +export class FileCommandProvider implements IChatCommandProvider { + public id: string = FILE_COMMAND_PROVIDER_ID; + + /** + * Regex that matches all potential `@file` commands. The first capturing + * group captures the path specified by the user. Paths may contain any + * combination of: + * + * `[a-zA-Z0-9], '/', '-', '_', '.', '@', '\\ ' (escaped space)` + * + * IMPORTANT: `+` ensures this regex only matches an occurrence of "@file:" if + * the captured path is non-empty. + */ + _regex: RegExp = /@file:(([\w/\-_.@]|\\ )+)/g; + + constructor( + contentsManager: Contents.IManager, + docRegistry: DocumentRegistry + ) { + this._contentsManager = contentsManager; + this._docRegistry = docRegistry; + } + + async listCommandCompletions( + inputModel: IInputModel + ): Promise { + // do nothing if the current word does not start with '@'. + const currentWord = inputModel.currentWord; + if (!currentWord || !currentWord.startsWith('@')) { + return []; + } + + // if the current word starts with `@file:`, return a list of valid file + // paths that complete the currently specified path. + if (currentWord.startsWith('@file:')) { + const searchPath = currentWord.split('@file:')[1]; + const commands = await getPathCompletions( + this._contentsManager, + this._docRegistry, + searchPath + ); + return commands; + } + + // if the current word matches the start of @file, complete it + if ('@file'.startsWith(currentWord)) { + return [ + { + name: '@file:', + providerId: this.id, + description: 'Include a file with your prompt', + icon: + } + ]; + } + + // otherwise, return nothing as this provider cannot provide any completions + // for the current word. + return []; + } + + async onSubmit(inputModel: IInputModel): Promise { + // search entire input for valid @file commands using `this._regex` + const matches = Array.from(inputModel.value.matchAll(this._regex)); + + // aggregate all file paths specified by @file commands in the input + const paths: string[] = []; + for (const match of matches) { + if (match.length < 2) { + continue; + } + // `this._regex` contains exactly 1 group that captures the path, so + // match[1] will contain the path specified by a @file command. + paths.push(match[1]); + } + + // add each specified file path as an attachment, unescaping ' ' characters + // before doing so + for (let path of paths) { + path = path.replaceAll('\\ ', ' '); + inputModel.addAttachment?.({ + type: 'file', + value: path + }); + } + + // replace each @file command with the path in an inline Markdown code block + // for readability, both to humans & to the AI. + inputModel.value = inputModel.value.replaceAll( + this._regex, + (_, path) => `\`${path.replaceAll('\\ ', ' ')}\`` + ); + + return; + } + + private _contentsManager: Contents.IManager; + private _docRegistry: DocumentRegistry; +} + +/** + * Returns the parent path and base name given a path. The parent path will + * always include a trailing "/" if non-empty. + * + * Examples: + * - "package.json" => ["", "package.json"] + * - "foo/bar" => ["foo/", "bar"] + * - "a/b/c/d.txt" => ["a/b/c/", "d.txt"] + * + */ +function getParentAndBase(path: string): [string, string] { + const components = path.split('/'); + let parentPath: string; + let basename: string; + if (components.length === 1) { + parentPath = ''; + basename = components[0]; + } else { + parentPath = components.slice(0, -1).join('/') + '/'; + basename = components[components.length - 1] ?? ''; + } + + return [parentPath, basename]; +} + +async function getPathCompletions( + contentsManager: Contents.IManager, + docRegistry: DocumentRegistry, + searchPath: string +): Promise { + // get parent directory & the partial basename to be completed + const [parentPath, basename] = getParentAndBase(searchPath); + + // query the parent directory through the CM, un-escaping spaces beforehand + const parentDir = await contentsManager.get( + parentPath.replaceAll('\\ ', ' ') + ); + + const commands: ChatCommand[] = []; + + if (!Array.isArray(parentDir.content)) { + // return nothing if parentDir is invalid / points to a non-directory file + return []; + } + + const children = parentDir.content + // filter the children of the parent directory to only include file names that + // start with the specified base name (case-insensitive). + .filter((a: Contents.IModel) => { + return a.name.toLowerCase().startsWith(basename.toLowerCase()); + }) + // sort the list, showing directories first while ensuring entries are shown + // in alphabetic (lexicographically ascending) order. + .sort((a: Contents.IModel, b: Contents.IModel) => { + const aPrimaryKey = a.type === 'directory' ? -1 : 1; + const bPrimaryKey = b.type === 'directory' ? -1 : 1; + const primaryKey = aPrimaryKey - bPrimaryKey; + const secondaryKey = a.name < b.name ? -1 : 1; + + return primaryKey || secondaryKey; + }); + + for (const child of children) { + // get icon + const { icon } = docRegistry.getFileTypeForModel(child); + + // calculate completion string, escaping any unescaped spaces + let completion = '@file:' + parentPath + child.name; + completion = completion.replaceAll(/(? = { + id: '@jupyter-ai/core:file-command-plugin', + description: 'Adds support for the @file command in Jupyter AI.', + autoStart: true, + requires: [IChatCommandRegistry], + activate: (app, registry: IChatCommandRegistry) => { + const { serviceManager, docRegistry } = app; + registry.addProvider( + new FileCommandProvider(serviceManager.contents, docRegistry) + ); + } +}; diff --git a/src/chat-command-plugins/index.ts b/src/chat-command-plugins/index.ts new file mode 100644 index 0000000..ba996f6 --- /dev/null +++ b/src/chat-command-plugins/index.ts @@ -0,0 +1,4 @@ +import { fileCommandPlugin } from './file-command'; +import { slashCommandPlugin } from './slash-commands'; + +export const chatCommandPlugins = [fileCommandPlugin, slashCommandPlugin]; diff --git a/src/chat-command-plugins/slash-commands.tsx b/src/chat-command-plugins/slash-commands.tsx new file mode 100644 index 0000000..9f17dc4 --- /dev/null +++ b/src/chat-command-plugins/slash-commands.tsx @@ -0,0 +1,117 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import React from 'react'; +import { JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { + IChatCommandProvider, + IChatCommandRegistry, + IInputModel, + ChatCommand +} from '@jupyter/chat'; +import RefreshIcon from '@mui/icons-material/Refresh'; + +const SLASH_COMMAND_PROVIDER_ID = '@jupyter-ai/core:slash-command-provider'; + +/** + * A command provider that provides completions for slash commands and handles + * slash command calls. + * + * - Slash commands are intended for "chat-level" operations that aren't + * specific to any persona. + * + * - Slash commands may only appear one-at-a-time, and only do something if the + * first word of the input specifies a slash command `/{slash-command-id}`. + * + * - Note: In v2, slash commands were reserved for specific tasks like + * 'generate' or 'learn'. But because tasks are handled by AI personas via agent + * tools in v3, slash commands in v3 are reserved for "chat-level" operations + * that are not specific to an AI persona. + */ +export class SlashCommandProvider implements IChatCommandProvider { + public id: string = SLASH_COMMAND_PROVIDER_ID; + + /** + * Regex that matches a potential slash command. The first capturing group + * captures the ID of the slash command. Slash command IDs may be any + * combination of: `\`, `-`. + */ + _regex: RegExp = /\/([\w-]*)/g; + + _slash_commands: ChatCommand[] = [ + { + name: '/refresh-personas', + providerId: this.id, + description: 'Refresh available personas', + icon: , + spaceOnAccept: true + } + ]; + + constructor() {} + + /** + * Returns slash command completions for the current input. + */ + async listCommandCompletions( + inputModel: IInputModel + ): Promise { + // do nothing if not on first word + const firstWord = getFirstWord(inputModel.value); + if (inputModel.currentWord !== firstWord) { + return []; + } + + // do nothing if first word is not a slash command + if (!firstWord || !firstWord.startsWith('/')) { + return []; + } + + // Return list of commands that complete the first word + return this._slash_commands.filter(cmd => cmd.name.startsWith(firstWord)); + } + + async onSubmit(inputModel: IInputModel): Promise { + // no-op. slash commands are handled in the backend + return; + } +} + +/** + * Finds the first word in a given string `input`. + * + * Returns the first word, or `null` if there is no first word. + */ +function getFirstWord(input: string): string | null { + let start = 0; + + // Skip leading whitespace + while (start < input.length && /\s/.test(input[start])) { + start++; + } + + // Find end of first word + let end = start; + while (end < input.length && !/\s/.test(input[end])) { + end++; + } + + const firstWord = input.substring(start, end); + if (firstWord) { + return firstWord; + } else { + return null; + } +} + +export const slashCommandPlugin: JupyterFrontEndPlugin = { + id: '@jupyter-ai/core:slash-command-plugin', + description: 'Adds support for slash commands in Jupyter AI.', + autoStart: true, + requires: [IChatCommandRegistry], + activate: (app, registry: IChatCommandRegistry) => { + registry.addProvider(new SlashCommandProvider()); + } +}; diff --git a/src/index.ts b/src/index.ts index 79ab659..265bd28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,13 +4,15 @@ import { } from '@jupyterlab/application'; import { requestAPI } from './handler'; +import { chatCommandPlugins } from './chat-command-plugins'; /** * Initialization data for the @jupyter-ai/chat-commands extension. */ const plugin: JupyterFrontEndPlugin = { id: '@jupyter-ai/chat-commands:plugin', - description: 'Package providing the set of default chat commands in Jupyter AI.', + description: + 'Package providing the set of default chat commands in Jupyter AI.', autoStart: true, activate: (app: JupyterFrontEnd) => { console.log('JupyterLab extension @jupyter-ai/chat-commands is activated!'); @@ -27,4 +29,4 @@ const plugin: JupyterFrontEndPlugin = { } }; -export default plugin; +export default [plugin, ...chatCommandPlugins]; diff --git a/tsconfig.json b/tsconfig.json index 25af040..dac0bcd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "esModuleInterop": true, "incremental": true, "jsx": "react", - "lib": ["DOM", "ES2018", "ES2020.Intl"], + "lib": ["dom", "ES2021"], "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, @@ -21,5 +21,5 @@ "target": "ES2018", "types": ["jest"] }, - "include": ["src/*"] + "include": ["src/**/*"] } diff --git a/ui-tests/tests/jupyter_ai_chat_commands.spec.ts b/ui-tests/tests/jupyter_ai_chat_commands.spec.ts index 37e1675..eb38fe9 100644 --- a/ui-tests/tests/jupyter_ai_chat_commands.spec.ts +++ b/ui-tests/tests/jupyter_ai_chat_commands.spec.ts @@ -16,6 +16,8 @@ test('should emit an activation console message', async ({ page }) => { await page.goto(); expect( - logs.filter(s => s === 'JupyterLab extension @jupyter-ai/chat-commands is activated!') + logs.filter( + s => s === 'JupyterLab extension @jupyter-ai/chat-commands is activated!' + ) ).toHaveLength(1); }); diff --git a/yarn.lock b/yarn.lock index 9f957e5..c019f26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -143,7 +143,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.27.1": +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-module-imports@npm:7.27.1" dependencies: @@ -1243,6 +1243,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": + version: 7.28.4 + resolution: "@babel/runtime@npm:7.28.4" + checksum: 934b0a0460f7d06637d93fcd1a44ac49adc33518d17253b5a0b55ff4cb90a45d8fe78bf034b448911dbec7aff2a90b918697559f78d21c99ff8dbadae9565b55 + languageName: node + linkType: hard + "@babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" @@ -1584,6 +1591,152 @@ __metadata: languageName: node linkType: hard +"@emotion/babel-plugin@npm:^11.13.5": + version: 11.13.5 + resolution: "@emotion/babel-plugin@npm:11.13.5" + dependencies: + "@babel/helper-module-imports": ^7.16.7 + "@babel/runtime": ^7.18.3 + "@emotion/hash": ^0.9.2 + "@emotion/memoize": ^0.9.0 + "@emotion/serialize": ^1.3.3 + babel-plugin-macros: ^3.1.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^4.0.0 + find-root: ^1.1.0 + source-map: ^0.5.7 + stylis: 4.2.0 + checksum: c41df7e6c19520e76d1939f884be878bf88b5ba00bd3de9d05c5b6c5baa5051686ab124d7317a0645de1b017b574d8139ae1d6390ec267fbe8e85a5252afb542 + languageName: node + linkType: hard + +"@emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.14.0": + version: 11.14.0 + resolution: "@emotion/cache@npm:11.14.0" + dependencies: + "@emotion/memoize": ^0.9.0 + "@emotion/sheet": ^1.4.0 + "@emotion/utils": ^1.4.2 + "@emotion/weak-memoize": ^0.4.0 + stylis: 4.2.0 + checksum: 0a81591541ea43bc7851742e6444b7800d72e98006f94e775ae6ea0806662d14e0a86ff940f5f19d33b4bb2c427c882aa65d417e7322a6e0d5f20fe65ed920c9 + languageName: node + linkType: hard + +"@emotion/hash@npm:^0.9.2": + version: 0.9.2 + resolution: "@emotion/hash@npm:0.9.2" + checksum: 379bde2830ccb0328c2617ec009642321c0e009a46aa383dfbe75b679c6aea977ca698c832d225a893901f29d7b3eef0e38cf341f560f6b2b56f1ff23c172387 + languageName: node + linkType: hard + +"@emotion/is-prop-valid@npm:^1.3.0": + version: 1.4.0 + resolution: "@emotion/is-prop-valid@npm:1.4.0" + dependencies: + "@emotion/memoize": ^0.9.0 + checksum: 6b003cdc62106c2d5d12207c2d1352d674339252a2d7ac8d96974781d7c639833f35d22e7e331411795daaafa62f126c2824a4983584292b431e08b42877d51e + languageName: node + linkType: hard + +"@emotion/memoize@npm:^0.9.0": + version: 0.9.0 + resolution: "@emotion/memoize@npm:0.9.0" + checksum: 038132359397348e378c593a773b1148cd0cf0a2285ffd067a0f63447b945f5278860d9de718f906a74c7c940ba1783ac2ca18f1c06a307b01cc0e3944e783b1 + languageName: node + linkType: hard + +"@emotion/react@npm:^11.10.5": + version: 11.14.0 + resolution: "@emotion/react@npm:11.14.0" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.13.5 + "@emotion/cache": ^11.14.0 + "@emotion/serialize": ^1.3.3 + "@emotion/use-insertion-effect-with-fallbacks": ^1.2.0 + "@emotion/utils": ^1.4.2 + "@emotion/weak-memoize": ^0.4.0 + hoist-non-react-statics: ^3.3.1 + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 3cf023b11d132b56168713764d6fced8e5a1f0687dfe0caa2782dfd428c8f9e30f9826a919965a311d87b523cd196722aaf75919cd0f6bd0fd57f8a6a0281500 + languageName: node + linkType: hard + +"@emotion/serialize@npm:^1.3.3": + version: 1.3.3 + resolution: "@emotion/serialize@npm:1.3.3" + dependencies: + "@emotion/hash": ^0.9.2 + "@emotion/memoize": ^0.9.0 + "@emotion/unitless": ^0.10.0 + "@emotion/utils": ^1.4.2 + csstype: ^3.0.2 + checksum: 510331233767ae4e09e925287ca2c7269b320fa1d737ea86db5b3c861a734483ea832394c0c1fe5b21468fe335624a75e72818831d303ba38125f54f44ba02e7 + languageName: node + linkType: hard + +"@emotion/sheet@npm:^1.4.0": + version: 1.4.0 + resolution: "@emotion/sheet@npm:1.4.0" + checksum: eeb1212e3289db8e083e72e7e401cd6d1a84deece87e9ce184f7b96b9b5dbd6f070a89057255a6ff14d9865c3ce31f27c39248a053e4cdd875540359042586b4 + languageName: node + linkType: hard + +"@emotion/styled@npm:^11.10.5": + version: 11.14.1 + resolution: "@emotion/styled@npm:11.14.1" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.13.5 + "@emotion/is-prop-valid": ^1.3.0 + "@emotion/serialize": ^1.3.3 + "@emotion/use-insertion-effect-with-fallbacks": ^1.2.0 + "@emotion/utils": ^1.4.2 + peerDependencies: + "@emotion/react": ^11.0.0-rc.0 + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 86238d9f5c41232a73499e441fa9112e855545519d6772f489fa885634bb8f31b422a9ba9d1e8bc0b4ad66132f9d398b1c309d92c19c5ee21356b41671ec984a + languageName: node + linkType: hard + +"@emotion/unitless@npm:^0.10.0": + version: 0.10.0 + resolution: "@emotion/unitless@npm:0.10.0" + checksum: d79346df31a933e6d33518e92636afeb603ce043f3857d0a39a2ac78a09ef0be8bedff40130930cb25df1beeee12d96ee38613963886fa377c681a89970b787c + languageName: node + linkType: hard + +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.2.0": + version: 1.2.0 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.2.0" + peerDependencies: + react: ">=16.8.0" + checksum: 8ff6aec7f2924526ff8c8f8f93d4b8236376e2e12c435314a18c9a373016e24dfdf984e82bbc83712b8e90ff4783cd765eb39fc7050d1a43245e5728740ddd71 + languageName: node + linkType: hard + +"@emotion/utils@npm:^1.4.2": + version: 1.4.2 + resolution: "@emotion/utils@npm:1.4.2" + checksum: 04cf76849c6401205c058b82689fd0ec5bf501aed6974880fe9681a1d61543efb97e848f4c38664ac4a9068c7ad2d1cb84f73bde6cf95f1208aa3c28e0190321 + languageName: node + linkType: hard + +"@emotion/weak-memoize@npm:^0.4.0": + version: 0.4.0 + resolution: "@emotion/weak-memoize@npm:0.4.0" + checksum: db5da0e89bd752c78b6bd65a1e56231f0abebe2f71c0bd8fc47dff96408f7065b02e214080f99924f6a3bfe7ee15afc48dad999d76df86b39b16e513f7a94f52 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.9.0 resolution: "@eslint-community/eslint-utils@npm:4.9.0" @@ -1989,11 +2142,13 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-ai/chat-commands@workspace:." dependencies: + "@jupyter/chat": ^0.17.0 "@jupyterlab/application": ^4.0.0 "@jupyterlab/builder": ^4.0.0 "@jupyterlab/coreutils": ^6.0.0 "@jupyterlab/services": ^7.0.0 "@jupyterlab/testutils": ^4.0.0 + "@mui/icons-material": ^5.11.0 "@types/jest": ^29.2.0 "@types/json-schema": ^7.0.11 "@types/react": ^18.0.26 @@ -2021,6 +2176,48 @@ __metadata: languageName: unknown linkType: soft +"@jupyter/chat@npm:^0.17.0": + version: 0.17.0 + resolution: "@jupyter/chat@npm:0.17.0" + dependencies: + "@emotion/react": ^11.10.5 + "@emotion/styled": ^11.10.5 + "@jupyter/react-components": ^0.15.2 + "@jupyterlab/application": ^4.2.0 + "@jupyterlab/apputils": ^4.3.0 + "@jupyterlab/codeeditor": ^4.2.0 + "@jupyterlab/codemirror": ^4.2.0 + "@jupyterlab/docmanager": ^4.2.0 + "@jupyterlab/filebrowser": ^4.2.0 + "@jupyterlab/fileeditor": ^4.2.0 + "@jupyterlab/notebook": ^4.2.0 + "@jupyterlab/rendermime": ^4.2.0 + "@jupyterlab/ui-components": ^4.2.0 + "@lumino/algorithm": ^2.0.0 + "@lumino/commands": ^2.0.0 + "@lumino/coreutils": ^2.0.0 + "@lumino/disposable": ^2.0.0 + "@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 + checksum: 75d906e59c28ae34132011583769bab2ce7e2d727888c7e644f2db0ddda60d9bf46a9c16bac67e55d06ad82319078ec0f6eed5468534bde5fd3c15a279e8be45 + languageName: node + linkType: hard + +"@jupyter/react-components@npm:^0.15.2": + version: 0.15.3 + resolution: "@jupyter/react-components@npm:0.15.3" + dependencies: + "@jupyter/web-components": ^0.15.3 + "@microsoft/fast-react-wrapper": ^0.3.22 + react: ">=17.0.0 <19.0.0" + checksum: 1a6b256314259c6465c4b6d958575710536b82234a7bf0fba3e889a07e1f19ff8ab321450be354359876f92c45dbcc9d21a840237ff4a619806d9de696f55496 + languageName: node + linkType: hard + "@jupyter/react-components@npm:^0.16.6": version: 0.16.7 resolution: "@jupyter/react-components@npm:0.16.7" @@ -2031,6 +2228,18 @@ __metadata: languageName: node linkType: hard +"@jupyter/web-components@npm:^0.15.3": + version: 0.15.3 + resolution: "@jupyter/web-components@npm:0.15.3" + dependencies: + "@microsoft/fast-colors": ^5.3.1 + "@microsoft/fast-element": ^1.12.0 + "@microsoft/fast-foundation": ^2.49.4 + "@microsoft/fast-web-utilities": ^5.4.1 + checksum: a0980af934157bfdbdb6cc169c0816c1b2e57602d524c56bdcef746a4c25dfeb8f505150d83207c8695ed89b5486cf53d35a3382584d25ef64db666e4e16e45b + languageName: node + linkType: hard + "@jupyter/web-components@npm:^0.16.6, @jupyter/web-components@npm:^0.16.7": version: 0.16.7 resolution: "@jupyter/web-components@npm:0.16.7" @@ -2057,7 +2266,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/application@npm:^4.0.0, @jupyterlab/application@npm:^4.4.9": +"@jupyterlab/application@npm:^4.0.0, @jupyterlab/application@npm:^4.2.0, @jupyterlab/application@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/application@npm:4.4.9" dependencies: @@ -2085,7 +2294,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/apputils@npm:^4.5.9": +"@jupyterlab/apputils@npm:^4.3.0, @jupyterlab/apputils@npm:^4.5.9": version: 4.5.9 resolution: "@jupyterlab/apputils@npm:4.5.9" dependencies: @@ -2205,7 +2414,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/codeeditor@npm:^4.4.9": +"@jupyterlab/codeeditor@npm:^4.2.0, @jupyterlab/codeeditor@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/codeeditor@npm:4.4.9" dependencies: @@ -2229,7 +2438,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/codemirror@npm:^4.4.9": +"@jupyterlab/codemirror@npm:^4.2.0, @jupyterlab/codemirror@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/codemirror@npm:4.4.9" dependencies: @@ -2285,7 +2494,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/docmanager@npm:^4.4.9": +"@jupyterlab/docmanager@npm:^4.2.0, @jupyterlab/docmanager@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/docmanager@npm:4.4.9" dependencies: @@ -2355,7 +2564,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/filebrowser@npm:^4.4.9": +"@jupyterlab/filebrowser@npm:^4.2.0, @jupyterlab/filebrowser@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/filebrowser@npm:4.4.9" dependencies: @@ -2384,6 +2593,32 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/fileeditor@npm:^4.2.0": + version: 4.4.9 + resolution: "@jupyterlab/fileeditor@npm:4.4.9" + dependencies: + "@jupyter/ydoc": ^3.1.0 + "@jupyterlab/apputils": ^4.5.9 + "@jupyterlab/codeeditor": ^4.4.9 + "@jupyterlab/codemirror": ^4.4.9 + "@jupyterlab/coreutils": ^6.4.9 + "@jupyterlab/docregistry": ^4.4.9 + "@jupyterlab/documentsearch": ^4.4.9 + "@jupyterlab/lsp": ^4.4.9 + "@jupyterlab/statusbar": ^4.4.9 + "@jupyterlab/toc": ^6.4.9 + "@jupyterlab/translation": ^4.4.9 + "@jupyterlab/ui-components": ^4.4.9 + "@lumino/commands": ^2.3.2 + "@lumino/coreutils": ^2.2.1 + "@lumino/messaging": ^2.0.3 + "@lumino/widgets": ^2.7.1 + react: ^18.2.0 + regexp-match-indices: ^1.0.2 + checksum: 0b0cd227610105e7bc742d864fcb969ca243707eac4882d1c69dab32f7232bdaaa3b83ebf994ba6aedcadd581ac96d7a07663345f5bca3401809c564bdc7d6f3 + languageName: node + linkType: hard + "@jupyterlab/lsp@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/lsp@npm:4.4.9" @@ -2416,7 +2651,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/notebook@npm:^4.4.9": +"@jupyterlab/notebook@npm:^4.2.0, @jupyterlab/notebook@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/notebook@npm:4.4.9" dependencies: @@ -2499,7 +2734,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/rendermime@npm:^4.4.9": +"@jupyterlab/rendermime@npm:^4.2.0, @jupyterlab/rendermime@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/rendermime@npm:4.4.9" dependencies: @@ -2658,7 +2893,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/ui-components@npm:^4.4.9": +"@jupyterlab/ui-components@npm:^4.2.0, @jupyterlab/ui-components@npm:^4.4.9": version: 4.4.9 resolution: "@jupyterlab/ui-components@npm:4.4.9" dependencies: @@ -2846,7 +3081,7 @@ __metadata: languageName: node linkType: hard -"@lumino/algorithm@npm:^2.0.3": +"@lumino/algorithm@npm:^2.0.0, @lumino/algorithm@npm:^2.0.3": version: 2.0.3 resolution: "@lumino/algorithm@npm:2.0.3" checksum: 03932cdc39d612a00579ee40bafb0b1d8bf5f8a12449f777a1ae7201843ddefb557bc3f9260aa6b9441d87bfc43e53cced854e71c4737de59e32cd00d4ac1394 @@ -2873,7 +3108,7 @@ __metadata: languageName: node linkType: hard -"@lumino/commands@npm:^2.3.2": +"@lumino/commands@npm:^2.0.0, @lumino/commands@npm:^2.3.2": version: 2.3.2 resolution: "@lumino/commands@npm:2.3.2" dependencies: @@ -2888,7 +3123,7 @@ __metadata: languageName: node linkType: hard -"@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.2.1, @lumino/coreutils@npm:^2.2.1": +"@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.2.1, @lumino/coreutils@npm:^2.0.0, @lumino/coreutils@npm:^2.2.1": version: 2.2.1 resolution: "@lumino/coreutils@npm:2.2.1" dependencies: @@ -2897,7 +3132,7 @@ __metadata: languageName: node linkType: hard -"@lumino/disposable@npm:^1.10.0 || ^2.0.0, @lumino/disposable@npm:^2.1.4": +"@lumino/disposable@npm:^1.10.0 || ^2.0.0, @lumino/disposable@npm:^2.0.0, @lumino/disposable@npm:^2.1.4": version: 2.1.4 resolution: "@lumino/disposable@npm:2.1.4" dependencies: @@ -2958,7 +3193,7 @@ __metadata: languageName: node linkType: hard -"@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2.1.4": +"@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2.0.0, @lumino/signaling@npm:^2.1.4": version: 2.1.4 resolution: "@lumino/signaling@npm:2.1.4" dependencies: @@ -3017,7 +3252,7 @@ __metadata: languageName: node linkType: hard -"@microsoft/fast-foundation@npm:^2.49.4": +"@microsoft/fast-foundation@npm:^2.49.4, @microsoft/fast-foundation@npm:^2.50.0": version: 2.50.0 resolution: "@microsoft/fast-foundation@npm:2.50.0" dependencies: @@ -3029,6 +3264,18 @@ __metadata: languageName: node linkType: hard +"@microsoft/fast-react-wrapper@npm:^0.3.22": + version: 0.3.25 + resolution: "@microsoft/fast-react-wrapper@npm:0.3.25" + dependencies: + "@microsoft/fast-element": ^1.14.0 + "@microsoft/fast-foundation": ^2.50.0 + peerDependencies: + react: ">=16.9.0" + checksum: 4c8e597eefd51c3091c25d0df28018b3283139178d6fd759532b40deb189f0422877308894fe55670edb82ed2a5b0138f2be8ef0b3df3ae518c14f75d6d0d577 + languageName: node + linkType: hard + "@microsoft/fast-web-utilities@npm:^5.4.1": version: 5.4.1 resolution: "@microsoft/fast-web-utilities@npm:5.4.1" @@ -3038,6 +3285,161 @@ __metadata: languageName: node linkType: hard +"@mui/core-downloads-tracker@npm:^5.18.0": + version: 5.18.0 + resolution: "@mui/core-downloads-tracker@npm:5.18.0" + checksum: 065b46739d2bd84b880ad2f6a0a2062d60e3a296ce18ff380cad22ab5b2cb3de396755f322f4bea3a422ffffe1a9244536fc3c9623056ff3873c996e6664b1b9 + languageName: node + linkType: hard + +"@mui/icons-material@npm:^5.11.0": + version: 5.18.0 + resolution: "@mui/icons-material@npm:5.18.0" + dependencies: + "@babel/runtime": ^7.23.9 + peerDependencies: + "@mui/material": ^5.0.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 1ee4ee2817278c72095860c81e742270ee01fd320e48f865d371abfeacf83e43a7c6edbfd060ee66ef2a4f02c6cf79de0aca1e73a4d5f6320112bbfcac8176c3 + languageName: node + linkType: hard + +"@mui/material@npm:^5.11.0": + version: 5.18.0 + resolution: "@mui/material@npm:5.18.0" + dependencies: + "@babel/runtime": ^7.23.9 + "@mui/core-downloads-tracker": ^5.18.0 + "@mui/system": ^5.18.0 + "@mui/types": ~7.2.15 + "@mui/utils": ^5.17.1 + "@popperjs/core": ^2.11.8 + "@types/react-transition-group": ^4.4.10 + clsx: ^2.1.0 + csstype: ^3.1.3 + prop-types: ^15.8.1 + react-is: ^19.0.0 + react-transition-group: ^4.4.5 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: b9d6cf638774e65924adf6f58ad50639c55050b33dc6223aa74686f733e137d173b4594b28be70e8ea3baf413a7fcd3bee40d35e3af12a42b7ba03def71ea217 + languageName: node + linkType: hard + +"@mui/private-theming@npm:^5.17.1": + version: 5.17.1 + resolution: "@mui/private-theming@npm:5.17.1" + dependencies: + "@babel/runtime": ^7.23.9 + "@mui/utils": ^5.17.1 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: ab755e96c8adb6c12f2c7e567154a8d873d56f1e35d0fb897b568123dfc0e9d1d770ca0992410433c728f9f4403f556f3c1e1b5b57b001ef1948ddfc8c1717bd + languageName: node + linkType: hard + +"@mui/styled-engine@npm:^5.18.0": + version: 5.18.0 + resolution: "@mui/styled-engine@npm:5.18.0" + dependencies: + "@babel/runtime": ^7.23.9 + "@emotion/cache": ^11.13.5 + "@emotion/serialize": ^1.3.3 + csstype: ^3.1.3 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: ab2d260ad5eea94993bc7706b164ae4ec11bd37dd7be36b93755d18da5b7859d39ad44173adced0e111e8b1b7ef65c0e369df3f2908144237c5e78f793eebb5a + languageName: node + linkType: hard + +"@mui/system@npm:^5.18.0": + version: 5.18.0 + resolution: "@mui/system@npm:5.18.0" + dependencies: + "@babel/runtime": ^7.23.9 + "@mui/private-theming": ^5.17.1 + "@mui/styled-engine": ^5.18.0 + "@mui/types": ~7.2.15 + "@mui/utils": ^5.17.1 + clsx: ^2.1.0 + csstype: ^3.1.3 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 451f43889c2638da7c52b898e6174eafcdbcbcaaf3a79f954fdd58d9a043786d76fa4fca902cfdd6ab1aa250f5b1f932ef93d789c5a15987d893c0741bf7a1ad + languageName: node + linkType: hard + +"@mui/types@npm:~7.2.15": + version: 7.2.24 + resolution: "@mui/types@npm:7.2.24" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 3a7367f9503e90fc3cce78885b57b54a00f7a04108be8af957fdc882c1bc0af68390920ea3d6aef855e704651ffd4a530e36ccbec4d0f421a176a2c3c432bb95 + languageName: node + linkType: hard + +"@mui/utils@npm:^5.17.1": + version: 5.17.1 + resolution: "@mui/utils@npm:5.17.1" + dependencies: + "@babel/runtime": ^7.23.9 + "@mui/types": ~7.2.15 + "@types/prop-types": ^15.7.12 + clsx: ^2.1.1 + prop-types: ^15.8.1 + react-is: ^19.0.0 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 06f9da7025b9291f1052c0af012fd0f00ff1539bc99880e15f483a85c27bff0e9c3d047511dc5c3177d546c21978227ece2e6f7a7bd91903c72b48b89c45a677 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -3101,6 +3503,13 @@ __metadata: languageName: node linkType: hard +"@popperjs/core@npm:^2.11.8": + version: 2.11.8 + resolution: "@popperjs/core@npm:2.11.8" + checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 + languageName: node + linkType: hard + "@rjsf/core@npm:^5.13.4": version: 5.24.13 resolution: "@rjsf/core@npm:5.24.13" @@ -3325,7 +3734,14 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*": +"@types/parse-json@npm:^4.0.0": + version: 4.0.2 + resolution: "@types/parse-json@npm:4.0.2" + checksum: 5bf62eec37c332ad10059252fc0dab7e7da730764869c980b0714777ad3d065e490627be9f40fc52f238ffa3ac4199b19de4127196910576c2fe34dd47c7a470 + languageName: node + linkType: hard + +"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.12": version: 15.7.15 resolution: "@types/prop-types@npm:15.7.15" checksum: 31aa2f59b28f24da6fb4f1d70807dae2aedfce090ec63eaf9ea01727a9533ef6eaf017de5bff99fbccad7d1c9e644f52c6c2ba30869465dd22b1a7221c29f356 @@ -3342,6 +3758,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^4.4.10": + version: 4.4.12 + resolution: "@types/react-transition-group@npm:4.4.12" + peerDependencies: + "@types/react": "*" + checksum: 13d36396cae4d3c316b03d4a0ba299f0d039c59368ba65e04b0c3dc06fd0a16f59d2c669c3e32d6d525a95423f156b84e550d26bff0bdd8df285f305f8f3a0ed + languageName: node + linkType: hard + "@types/react@npm:*": version: 19.1.16 resolution: "@types/react@npm:19.1.16" @@ -4043,6 +4468,17 @@ __metadata: languageName: node linkType: hard +"babel-plugin-macros@npm:^3.1.0": + version: 3.1.0 + resolution: "babel-plugin-macros@npm:3.1.0" + dependencies: + "@babel/runtime": ^7.12.5 + cosmiconfig: ^7.0.0 + resolve: ^1.19.0 + checksum: 765de4abebd3e4688ebdfbff8571ddc8cd8061f839bb6c3e550b0344a4027b04c60491f843296ce3f3379fb356cc873d57a9ee6694262547eb822c14a25be9a6 + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs2@npm:^0.4.14": version: 0.4.14 resolution: "babel-plugin-polyfill-corejs2@npm:0.4.14" @@ -4362,6 +4798,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.1.0, clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -4482,6 +4925,13 @@ __metadata: languageName: node linkType: hard +"convert-source-map@npm:^1.5.0": + version: 1.9.0 + resolution: "convert-source-map@npm:1.9.0" + checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 + languageName: node + linkType: hard + "convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" @@ -4498,6 +4948,19 @@ __metadata: languageName: node linkType: hard +"cosmiconfig@npm:^7.0.0": + version: 7.1.0 + resolution: "cosmiconfig@npm:7.1.0" + dependencies: + "@types/parse-json": ^4.0.0 + import-fresh: ^3.2.1 + parse-json: ^5.0.0 + path-type: ^4.0.0 + yaml: ^1.10.0 + checksum: c53bf7befc1591b2651a22414a5e786cd5f2eeaa87f3678a3d49d6069835a9d8d1aef223728e98aa8fec9a95bf831120d245096db12abe019fecb51f5696c96f + languageName: node + linkType: hard + "cosmiconfig@npm:^8.2.0": version: 8.3.6 resolution: "cosmiconfig@npm:8.3.6" @@ -4630,7 +5093,7 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.2": +"csstype@npm:^3.0.2, csstype@npm:^3.1.3": version: 3.1.3 resolution: "csstype@npm:3.1.3" checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 @@ -4767,6 +5230,16 @@ __metadata: languageName: node linkType: hard +"dom-helpers@npm:^5.0.1": + version: 5.2.1 + resolution: "dom-helpers@npm:5.2.1" + dependencies: + "@babel/runtime": ^7.8.7 + csstype: ^3.0.2 + checksum: 863ba9e086f7093df3376b43e74ce4422571d404fc9828bf2c56140963d5edf0e56160f9b2f3bb61b282c07f8fc8134f023c98fd684bddcb12daf7b0f14d951c + languageName: node + linkType: hard + "dom-serializer@npm:^2.0.0": version: 2.0.0 resolution: "dom-serializer@npm:2.0.0" @@ -5368,7 +5841,7 @@ __metadata: languageName: node linkType: hard -"find-root@npm:^1.0.0": +"find-root@npm:^1.0.0, find-root@npm:^1.1.0": version: 1.1.0 resolution: "find-root@npm:1.1.0" checksum: b2a59fe4b6c932eef36c45a048ae8f93c85640212ebe8363164814990ee20f154197505965f3f4f102efc33bfb1cbc26fd17c4a2fc739ebc51b886b137cbefaf @@ -5782,6 +6255,15 @@ __metadata: languageName: node linkType: hard +"hoist-non-react-statics@npm:^3.3.1": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: ^16.7.0 + checksum: b1538270429b13901ee586aa44f4cc3ecd8831c061d06cb8322e50ea17b3f5ce4d0e2e66394761e6c8e152cd8c34fb3b4b690116c6ce2bd45b18c746516cb9e8 + languageName: node + linkType: hard + "hosted-git-info@npm:^4.0.1": version: 4.1.0 resolution: "hosted-git-info@npm:4.1.0" @@ -7622,7 +8104,7 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^5.2.0": +"parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" dependencies: @@ -7916,7 +8398,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -7992,7 +8474,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -8006,6 +8488,28 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^19.0.0": + version: 19.2.0 + resolution: "react-is@npm:19.2.0" + checksum: 9a23e1c2d0bbc13b383bc59a05f54e6eb95dd87e01aec8aa92a88618364b7b0ee8a5b057ad813cf61e2f7ae7d24503b624706acb609d07c54754e5ad2c522568 + languageName: node + linkType: hard + +"react-transition-group@npm:^4.4.5": + version: 4.4.5 + resolution: "react-transition-group@npm:4.4.5" + dependencies: + "@babel/runtime": ^7.5.5 + dom-helpers: ^5.0.1 + loose-envify: ^1.4.0 + prop-types: ^15.6.2 + peerDependencies: + react: ">=16.6.0" + react-dom: ">=16.6.0" + checksum: 75602840106aa9c6545149d6d7ae1502fb7b7abadcce70a6954c4b64a438ff1cd16fc77a0a1e5197cdd72da398f39eb929ea06f9005c45b132ed34e056ebdeb1 + languageName: node + linkType: hard + "react@npm:>=17.0.0 <19.0.0, react@npm:^18.2.0": version: 18.3.1 resolution: "react@npm:18.3.1" @@ -8083,6 +8587,24 @@ __metadata: languageName: node linkType: hard +"regexp-match-indices@npm:^1.0.2": + version: 1.0.2 + resolution: "regexp-match-indices@npm:1.0.2" + dependencies: + regexp-tree: ^0.1.11 + checksum: 8cc779f6cf8f404ead828d09970a7d4bd66bd78d43ab9eb2b5e65f2ef2ba1ed53536f5b5fa839fb90b350365fb44b6a851c7f16289afc3f37789c113ab2a7916 + languageName: node + linkType: hard + +"regexp-tree@npm:^0.1.11": + version: 0.1.27 + resolution: "regexp-tree@npm:0.1.27" + bin: + regexp-tree: bin/regexp-tree + checksum: 129aebb34dae22d6694ab2ac328be3f99105143737528ab072ef624d599afecbcfae1f5c96a166fa9e5f64fa1ecf30b411c4691e7924c3e11bbaf1712c260c54 + languageName: node + linkType: hard + "regexpu-core@npm:^6.2.0": version: 6.4.0 resolution: "regexpu-core@npm:6.4.0" @@ -8166,7 +8688,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.20.0, resolve@npm:^1.22.10": +"resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.10": version: 1.22.10 resolution: "resolve@npm:1.22.10" dependencies: @@ -8179,7 +8701,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.10#~builtin": +"resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.10#~builtin": version: 1.22.10 resolution: "resolve@patch:resolve@npm%3A1.22.10#~builtin::version=1.22.10&hash=c3c19d" dependencies: @@ -8526,6 +9048,13 @@ __metadata: languageName: node linkType: hard +"source-map@npm:^0.5.7": + version: 0.5.7 + resolution: "source-map@npm:0.5.7" + checksum: 5dc2043b93d2f194142c7f38f74a24670cd7a0063acdaf4bf01d2964b402257ae843c2a8fa822ad5b71013b5fcafa55af7421383da919752f22ff488bc553f4d + languageName: node + linkType: hard + "source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" @@ -8786,6 +9315,13 @@ __metadata: languageName: node linkType: hard +"stylis@npm:4.2.0": + version: 4.2.0 + resolution: "stylis@npm:4.2.0" + checksum: 0eb6cc1b866dc17a6037d0a82ac7fa877eba6a757443e79e7c4f35bacedbf6421fadcab4363b39667b43355cbaaa570a3cde850f776498e5450f32ed2f9b7584 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -9764,6 +10300,13 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^1.10.0": + version: 1.10.2 + resolution: "yaml@npm:1.10.2" + checksum: ce4ada136e8a78a0b08dc10b4b900936912d15de59905b2bf415b4d33c63df1d555d23acb2a41b23cf9fb5da41c256441afca3d6509de7247daa062fd2c5ea5f + languageName: node + linkType: hard + "yargs-parser@npm:^20.2.9": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9"