├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── binder-badge.yml │ ├── build.yml │ ├── check-release.yml │ ├── enforce-label.yml │ ├── license-header.yml │ ├── prep-release.yml │ ├── publish-release.yml │ ├── ui-tests.yml │ └── update-integration-tests.yml ├── .gitignore ├── .licenserc.yaml ├── .prettierignore ├── .prettierrc ├── .readthedocs.yaml ├── .stylelintrc ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── binder ├── environment.yml └── postBuild ├── conftest.py ├── docs ├── Makefile ├── jupyter-chat-example │ ├── README.md │ ├── install.json │ ├── jupyter_chat_example │ │ └── __init__.py │ ├── package.json │ ├── pyproject.toml │ ├── schema │ │ └── plugin.json │ ├── src │ │ └── index.ts │ ├── style │ │ ├── base.css │ │ ├── index.css │ │ └── index.js │ └── tsconfig.json ├── make.bat ├── requirements.txt └── source │ ├── README.ipynb │ ├── _static │ ├── css │ │ └── custom.css │ └── images │ │ ├── chat-widgets.png │ │ ├── code-toolbar-above.png │ │ ├── code-toolbar-below.png │ │ ├── code-toolbar-copy.png │ │ ├── code-toolbar-replace.png │ │ └── left-panel-new-chat.png │ ├── conf.py │ ├── developers │ ├── contributing │ │ ├── index.md │ │ ├── jupyter-chat.md │ │ └── jupyterlab-chat-extension.md │ ├── developing_extensions │ │ ├── extension-providing-chat.md │ │ ├── index.md │ │ └── using-chat-extension.md │ └── index.md │ ├── index.md │ └── users │ └── index.md ├── lerna.json ├── package.json ├── packages ├── jupyter-chat │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── mocks.ts │ │ │ ├── model.spec.ts │ │ │ └── widgets.spec.ts │ │ ├── active-cell-manager.ts │ │ ├── chat-commands │ │ │ ├── index.ts │ │ │ ├── registry.ts │ │ │ └── types.ts │ │ ├── components │ │ │ ├── attachments.tsx │ │ │ ├── chat-input.tsx │ │ │ ├── chat-messages.tsx │ │ │ ├── chat.tsx │ │ │ ├── code-blocks │ │ │ │ ├── code-toolbar.tsx │ │ │ │ ├── copy-button.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── input │ │ │ │ ├── buttons │ │ │ │ │ ├── attach-button.tsx │ │ │ │ │ ├── cancel-button.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── send-button.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── toolbar-registry.tsx │ │ │ │ └── use-chat-commands.tsx │ │ │ ├── jl-theme-provider.tsx │ │ │ ├── markdown-renderer.tsx │ │ │ ├── messages │ │ │ │ └── footer.tsx │ │ │ ├── mui-extras │ │ │ │ ├── contrasting-tooltip.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── tooltipped-button.tsx │ │ │ │ └── tooltipped-icon-button.tsx │ │ │ ├── scroll-container.tsx │ │ │ └── toolbar.tsx │ │ ├── context.ts │ │ ├── footers │ │ │ ├── index.ts │ │ │ ├── registry.ts │ │ │ └── types.ts │ │ ├── icons.ts │ │ ├── index.ts │ │ ├── input-model.ts │ │ ├── model.ts │ │ ├── registry.ts │ │ ├── selection-watcher.ts │ │ ├── theme-provider.ts │ │ ├── types.ts │ │ ├── types │ │ │ ├── mui.d.ts │ │ │ └── svg.d.ts │ │ ├── utils.ts │ │ └── widgets │ │ │ ├── chat-error.tsx │ │ │ ├── chat-sidebar.tsx │ │ │ └── chat-widget.tsx │ ├── style │ │ ├── base.css │ │ ├── chat-settings.css │ │ ├── chat.css │ │ ├── icons │ │ │ ├── chat.svg │ │ │ ├── include-selection.svg │ │ │ ├── read.svg │ │ │ └── replace-cell.svg │ │ ├── index.css │ │ ├── index.js │ │ └── input.css │ ├── tsconfig.json │ └── tsconfig.test.json ├── jupyterlab-chat-extension │ ├── package.json │ ├── schema │ │ ├── chat-panel.json │ │ ├── commands.json │ │ └── factory.json │ ├── src │ │ ├── chat-commands │ │ │ ├── plugins.ts │ │ │ └── providers │ │ │ │ ├── emoji.ts │ │ │ │ └── user-mention.tsx │ │ └── index.ts │ ├── style │ │ ├── base.css │ │ ├── index.css │ │ └── index.js │ └── tsconfig.json └── jupyterlab-chat │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── __tests__ │ │ └── jupyterlab_collaborative_chat.spec.ts │ ├── factory.ts │ ├── index.ts │ ├── model.ts │ ├── token.ts │ ├── widget.tsx │ └── ychat.ts │ ├── style │ ├── base.css │ ├── index.css │ └── index.js │ ├── tsconfig.json │ └── tsconfig.test.json ├── pyproject.toml ├── python └── jupyterlab-chat │ ├── .copier-answers.yml │ ├── .prettierignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── RELEASE.md │ ├── binder │ ├── environment.yml │ └── postBuild │ ├── install.json │ ├── jupyter-config │ └── server-config │ │ └── jupyterlab_chat.json │ ├── jupyterlab_chat │ ├── __init__.py │ ├── _version.py │ ├── models.py │ ├── py.typed │ ├── tests │ │ ├── __init__.py │ │ └── test_ychat.py │ └── ychat.py │ ├── pyproject.toml │ ├── screenshot.gif │ └── setup.py ├── scripts ├── bump_version.py └── dev_install.py ├── tsconfig.json ├── ui-tests ├── README.md ├── jupyter_server_test_config.py ├── jupyter_server_test_config_notebook.py ├── package.json ├── playwright.config.js ├── playwright.notebook.config.js ├── tests │ ├── attachments.spec.ts │ ├── chat-file.spec.ts │ ├── code-toolbar.spec.ts │ ├── commands.spec.ts │ ├── commands.spec.ts-snapshots │ │ ├── launcher-tile-linux.png │ │ └── menu-new-linux.png │ ├── general.spec.ts │ ├── input-toolbar.spec.ts │ ├── message-toolbar.spec.ts │ ├── notebook-application.spec.ts │ ├── notifications.spec.ts │ ├── notifications.spec.ts-snapshots │ │ ├── tab-with-unread-linux.png │ │ └── tab-without-unread-linux.png │ ├── raw-time.spec.ts │ ├── send-message.spec.ts │ ├── side-panel.spec.ts │ ├── side-panel.spec.ts-snapshots │ │ ├── chat-icon-linux.png │ │ ├── moveToMain-linux.png │ │ └── moveToSide-linux.png │ ├── test-utils.ts │ ├── ui-config.spec.ts │ ├── ui-config.spec.ts-snapshots │ │ ├── not-stacked-messages-linux.png │ │ └── stacked-messages-linux.png │ ├── unread.spec.ts │ ├── unread.spec.ts-snapshots │ │ ├── navigation-bottom-linux.png │ │ ├── navigation-bottom-unread-linux.png │ │ └── navigation-top-linux.png │ └── user-mention.spec.ts └── yarn.lock └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | **/*.d.ts 5 | tests 6 | **/__tests__ 7 | ui-tests 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended' 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: 'tsconfig.json', 11 | sourceType: 'module' 12 | }, 13 | plugins: ['@stylistic', '@typescript-eslint'], 14 | rules: { 15 | '@typescript-eslint/naming-convention': [ 16 | 'error', 17 | { 18 | selector: 'interface', 19 | format: ['PascalCase'], 20 | custom: { 21 | regex: '^I[A-Z]', 22 | match: true 23 | } 24 | } 25 | ], 26 | '@typescript-eslint/no-unused-vars': [ 27 | 'warn', 28 | { 29 | args: 'none' 30 | } 31 | ], 32 | '@typescript-eslint/no-explicit-any': 'off', 33 | '@typescript-eslint/no-namespace': 'off', 34 | '@typescript-eslint/no-use-before-define': 'off', 35 | '@stylistic/quotes': [ 36 | 'error', 37 | 'single', 38 | { 39 | avoidEscape: true, 40 | allowTemplateLiterals: false 41 | } 42 | ], 43 | curly: ['error', 'all'], 44 | eqeqeq: 'error', 45 | 'no-restricted-imports': [ 46 | 'error', 47 | { 48 | paths: [ 49 | { 50 | name: '@mui/icons-material', 51 | message: 52 | "Please import icons using path imports, e.g. `import AddIcon from '@mui/icons-material/Add'`" 53 | } 54 | ], 55 | patterns: [ 56 | { 57 | group: ['@mui/*/*/*'], 58 | message: '3rd level imports in mui are considered private' 59 | } 60 | ] 61 | } 62 | ], 63 | 'prefer-arrow-callback': 'error' 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /.github/workflows/binder-badge.yml: -------------------------------------------------------------------------------- 1 | name: Binder Badge 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | 6 | jobs: 7 | binder: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - uses: jupyterlab/maintainer-tools/.github/actions/binder-link@v1 13 | with: 14 | github_token: ${{ secrets.github_token }} 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build_jupyter-chat: 15 | runs-on: ubuntu-latest 16 | name: Build jupyter_chat 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Base Setup 23 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 24 | 25 | - name: Install dependencies 26 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 27 | 28 | - name: Lint the packages 29 | run: | 30 | set -eux 31 | jlpm 32 | jlpm run lint:check 33 | 34 | - name: Build the core package 35 | run: jlpm build:core 36 | 37 | - name: Test the packages 38 | run: | 39 | set -eux 40 | jlpm run test 41 | 42 | test_extensions: 43 | name: Python unit tests (Python ${{ matrix.python-version }}) 44 | runs-on: ubuntu-latest 45 | needs: build_jupyter-chat 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | include: 50 | - python-version: '3.9' 51 | - python-version: '3.12' 52 | 53 | steps: 54 | - name: Checkout 55 | uses: actions/checkout@v4 56 | 57 | - name: Base Setup 58 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 59 | with: 60 | python-version: ${{ matrix.python-version }} 61 | 62 | - name: Install dependencies 63 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 64 | 65 | - name: Build the extensions 66 | run: | 67 | set -eux 68 | python ./scripts/dev_install.py 69 | pytest -vv -r ap --cov 70 | 71 | build_extension: 72 | runs-on: ubuntu-latest 73 | needs: build_jupyter-chat 74 | name: Build chat extension 75 | 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v4 79 | 80 | - name: Base Setup 81 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 82 | 83 | - name: Install dependencies 84 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 85 | 86 | - name: Package the extensions 87 | run: | 88 | jlpm install 89 | jlpm build 90 | 91 | - name: Package extension 92 | run: | 93 | set -eux 94 | pip install build python/jupyterlab-chat 95 | python -m build python/jupyterlab-chat 96 | pip uninstall -y "jupyterlab_chat" jupyterlab 97 | 98 | - name: Upload package 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: jupyterlab_chat-artifacts 102 | path: python/jupyterlab-chat/dist/jupyterlab_chat* 103 | if-no-files-found: error 104 | 105 | typing-tests: 106 | name: Typing test 107 | runs-on: ubuntu-latest 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | 112 | - name: Base Setup 113 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 114 | 115 | - name: Install extension dependencies and build the extension 116 | run: python ./scripts/dev_install.py 117 | 118 | - name: Run mypy 119 | run: | 120 | set -eux 121 | mypy --version 122 | mypy python/jupyterlab-chat 123 | 124 | check_links: 125 | name: Check Links 126 | runs-on: ubuntu-latest 127 | timeout-minutes: 15 128 | steps: 129 | - uses: actions/checkout@v4 130 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 131 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 132 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ['main'] 5 | pull_request: 6 | branches: ['*'] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | check_release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Base Setup 20 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 21 | 22 | - name: Check Release 23 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Upload Distributions 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: jupyter_chat-releaser-dist-${{ github.run_number }} 31 | path: .jupyter_releaser_checkout/dist 32 | -------------------------------------------------------------------------------- /.github/workflows/enforce-label.yml: -------------------------------------------------------------------------------- 1 | name: Enforce PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, edited, synchronize] 6 | jobs: 7 | enforce-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - name: enforce-triage-label 13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 14 | -------------------------------------------------------------------------------- /.github/workflows/license-header.yml: -------------------------------------------------------------------------------- 1 | name: Fix License Headers 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | header-license-fix: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | - name: Configure git to use https 21 | run: git config --global hub.protocol https 22 | 23 | - name: Checkout the branch from the PR that triggered the job 24 | run: gh pr checkout ${{ github.event.pull_request.number }} 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Fix License Header 29 | uses: apache/skywalking-eyes/header@v0.4.0 30 | with: 31 | mode: fix 32 | 33 | - name: List files changed 34 | id: files-changed 35 | shell: bash -l {0} 36 | run: | 37 | set -ex 38 | export CHANGES=$(git status --porcelain | tee modified.log | wc -l) 39 | cat modified.log 40 | # Remove the log otherwise it will be committed 41 | rm modified.log 42 | 43 | echo "N_CHANGES=${CHANGES}" >> $GITHUB_OUTPUT 44 | 45 | git diff 46 | 47 | - name: Commit any changes 48 | if: steps.files-changed.outputs.N_CHANGES != '0' 49 | shell: bash -l {0} 50 | run: | 51 | git config user.name "github-actions[bot]" 52 | git config user.email "github-actions[bot]@users.noreply.github.com" 53 | 54 | git pull --no-tags 55 | 56 | git add * 57 | git commit -m "Automatic application of license header" 58 | 59 | git config push.default upstream 60 | git push 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: 'Step 1: Prep Release' 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: 'New Version Specifier' 7 | default: 'next' 8 | required: false 9 | branch: 10 | description: 'The branch to target' 11 | required: false 12 | post_version_spec: 13 | description: 'Post Version Specifier' 14 | required: false 15 | silent: 16 | description: "Set a placeholder in the changelog and don't publish the release." 17 | required: false 18 | type: boolean 19 | since: 20 | description: 'Use PRs with activity since this date or git reference' 21 | required: false 22 | since_last_stable: 23 | description: 'Use PRs with activity since the last stable git tag' 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | target: ${{ github.event.inputs.target }} 43 | branch: ${{ github.event.inputs.branch }} 44 | since: ${{ github.event.inputs.since }} 45 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 46 | 47 | - name: '** Next Step **' 48 | run: | 49 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 50 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: 'Step 2: Publish Release' 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: 'The target branch' 7 | required: false 8 | release_url: 9 | description: 'The URL of the draft GitHub release' 10 | required: false 11 | steps_to_skip: 12 | description: 'Comma separated list of steps to skip' 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v1 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 44 | with: 45 | token: ${{ steps.app-token.outputs.token }} 46 | release_url: ${{ steps.populate-release.outputs.release_url }} 47 | 48 | - name: '** Next Step **' 49 | if: ${{ success() }} 50 | run: | 51 | echo "Verify the final release" 52 | echo ${{ steps.finalize-release.outputs.release_url }} 53 | 54 | - name: '** Failure Message **' 55 | if: ${{ failure() }} 56 | run: | 57 | echo "Failed to Publish the Draft Release Url:" 58 | echo ${{ steps.populate-release.outputs.release_url }} 59 | -------------------------------------------------------------------------------- /.github/workflows/ui-tests.yml: -------------------------------------------------------------------------------- 1 | name: UI tests 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | jupyterlab: 15 | name: jupyterlab 16 | runs-on: ubuntu-latest 17 | 18 | env: 19 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Base Setup 26 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 27 | 28 | - name: Install the extension 29 | run: | 30 | set -eux 31 | python ./scripts/dev_install.py 32 | 33 | - name: Install tests dependencies 34 | working-directory: ui-tests 35 | env: 36 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0 37 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 38 | run: jlpm install 39 | 40 | - name: Set up browser cache 41 | uses: actions/cache@v3 42 | with: 43 | path: | 44 | ${{ github.workspace }}/pw-browsers 45 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 46 | 47 | - name: Install browser 48 | run: jlpm playwright install chromium 49 | working-directory: ui-tests 50 | 51 | - name: Execute integration tests 52 | working-directory: ui-tests 53 | run: | 54 | jlpm test --retries=2 55 | 56 | - name: Upload Playwright Test report 57 | if: always() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: integration-jupyterlab 61 | path: | 62 | ui-tests/test-results 63 | ui-tests/playwright-report 64 | 65 | notebook: 66 | name: notebook 67 | runs-on: ubuntu-latest 68 | 69 | env: 70 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers 71 | 72 | steps: 73 | - name: Checkout 74 | uses: actions/checkout@v4 75 | 76 | - name: Base Setup 77 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 78 | 79 | - name: Install the extension 80 | run: | 81 | set -eux 82 | python -m pip install "notebook>=7.0.0,<8" 83 | python ./scripts/dev_install.py 84 | 85 | - name: Install dependencies 86 | working-directory: ui-tests 87 | env: 88 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0 89 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 90 | run: jlpm install 91 | 92 | - name: Set up browser cache 93 | uses: actions/cache@v3 94 | with: 95 | path: | 96 | ${{ github.workspace }}/pw-browsers 97 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 98 | 99 | - name: Install browser 100 | run: jlpm playwright install chromium 101 | working-directory: ui-tests 102 | 103 | - name: Execute integration tests 104 | working-directory: ui-tests 105 | run: | 106 | jlpm test:notebook --retries=2 107 | 108 | - name: Upload Playwright Test report 109 | if: always() 110 | uses: actions/upload-artifact@v4 111 | with: 112 | name: integration-notebook 113 | path: | 114 | ui-tests/test-results 115 | ui-tests/playwright-report 116 | -------------------------------------------------------------------------------- /.github/workflows/update-integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Update Playwright Snapshots 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | update-snapshots: 13 | if: > 14 | ( 15 | github.event.issue.author_association == 'OWNER' || 16 | github.event.issue.author_association == 'COLLABORATOR' || 17 | github.event.issue.author_association == 'MEMBER' 18 | ) && github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: React to the triggering comment 22 | run: | 23 | gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions --raw-field 'content=+1' 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Get PR Info 33 | id: pr 34 | env: 35 | PR_NUMBER: ${{ github.event.issue.number }} 36 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | GH_REPO: ${{ github.repository }} 38 | COMMENT_AT: ${{ github.event.comment.created_at }} 39 | run: | 40 | pr="$(gh api /repos/${GH_REPO}/pulls/${PR_NUMBER})" 41 | head_sha="$(echo "$pr" | jq -r .head.sha)" 42 | pushed_at="$(echo "$pr" | jq -r .pushed_at)" 43 | 44 | if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then 45 | echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)" 46 | exit 1 47 | fi 48 | 49 | echo "head_sha=$head_sha" >> $GITHUB_OUTPUT 50 | 51 | - name: Checkout the branch from the PR that triggered the job 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | run: gh pr checkout ${{ github.event.issue.number }} 55 | 56 | - name: Validate the fetched branch HEAD revision 57 | env: 58 | EXPECTED_SHA: ${{ steps.pr.outputs.head_sha }} 59 | run: | 60 | actual_sha="$(git rev-parse HEAD)" 61 | 62 | if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then 63 | echo "The HEAD of the checked out branch ($actual_sha) differs from the HEAD commit available at the time when trigger comment was submitted ($EXPECTED_SHA)" 64 | exit 1 65 | fi 66 | 67 | - name: Base Setup 68 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 69 | 70 | - name: Install dependencies 71 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 72 | 73 | - name: Install extension 74 | run: python ./scripts/dev_install.py 75 | 76 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 77 | with: 78 | github_token: ${{ secrets.GITHUB_TOKEN }} 79 | # Playwright knows how to start JupyterLab server 80 | start_server_script: 'null' 81 | test_folder: ui-tests 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | .jupyter_ystore.db 11 | labextension 12 | 13 | # Version file is handled by hatchling 14 | docs/**/_version.py 15 | 16 | # jest test reports 17 | packages/jupyter-chat/junit.xml 18 | packages/jupyterlab-chat/junit.xml 19 | 20 | # Integration tests 21 | ui-tests/test-results/ 22 | ui-tests/playwright-report/ 23 | 24 | # Created by https://www.gitignore.io/api/python 25 | # Edit at https://www.gitignore.io/?templates=python 26 | 27 | ### Python ### 28 | # Byte-compiled / optimized / DLL files 29 | __pycache__/ 30 | *.py[cod] 31 | *$py.class 32 | 33 | # C extensions 34 | *.so 35 | 36 | # Distribution / packaging 37 | .Python 38 | build/ 39 | develop-eggs/ 40 | dist/ 41 | downloads/ 42 | eggs/ 43 | .eggs/ 44 | lib/ 45 | lib64/ 46 | parts/ 47 | sdist/ 48 | var/ 49 | wheels/ 50 | pip-wheel-metadata/ 51 | share/python-wheels/ 52 | .installed.cfg 53 | *.egg 54 | MANIFEST 55 | 56 | # PyInstaller 57 | # Usually these files are written by a python script from a template 58 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 59 | *.manifest 60 | *.spec 61 | 62 | # Installer logs 63 | pip-log.txt 64 | pip-delete-this-directory.txt 65 | 66 | # Unit test / coverage reports 67 | htmlcov/ 68 | .tox/ 69 | .nox/ 70 | .coverage 71 | .coverage.* 72 | .cache 73 | nosetests.xml 74 | coverage/ 75 | coverage.xml 76 | *.cover 77 | .hypothesis/ 78 | .pytest_cache/ 79 | 80 | # Translations 81 | *.mo 82 | *.pot 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | target/ 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # Mr Developer 110 | .mr.developer.cfg 111 | .project 112 | .pydevproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # End of https://www.gitignore.io/api/python 126 | 127 | # OSX files 128 | .DS_Store 129 | 130 | # Yarn cache 131 | .yarn/ 132 | 133 | # Jupyter Notebooks 134 | Untitled*.ipynb 135 | 136 | # Chat files 137 | *.chat 138 | -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- 1 | header: 2 | license: 3 | spdx-id: BSD-3-Clause 4 | copyright-owner: Jupyter Development Team 5 | software-name: Jupyter-chat 6 | content: | 7 | Copyright (c) Jupyter Development Team. 8 | Distributed under the terms of the Modified BSD License. 9 | 10 | paths-ignore: 11 | - '**/*.typed' 12 | - '**/*.ipynb' 13 | - '**/*.json' 14 | - '**/*.md' 15 | - '**/*.svg' 16 | - '**/*.yml' 17 | - '**/*.yaml' 18 | - '**/build' 19 | - '**/lib' 20 | - '**/node_modules' 21 | - '*.map.js' 22 | - '*.bundle.js' 23 | - '**/.*' 24 | - '**/binder/postBuild' 25 | - 'coverage' 26 | - 'LICENSE' 27 | - 'yarn.lock' 28 | - '**/_version.py' 29 | 30 | comment: on-failure 31 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | jupyter_chat 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid", 5 | "endOfLine": "auto", 6 | "overrides": [ 7 | { 8 | "files": "package.json", 9 | "options": { 10 | "tabWidth": 2 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-24.04 8 | tools: 9 | python: "3.12" 10 | nodejs: "20" 11 | 12 | jobs: 13 | pre_build: 14 | - /bin/bash -c "jlpm install && jlpm build:lite-docs" 15 | - /bin/bash -c "pip install docs/jupyter-chat-example" 16 | 17 | # Build documentation in the docs/ directory with Sphinx 18 | sphinx: 19 | configuration: docs/source/conf.py 20 | 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt 24 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-standard", 5 | "stylelint-prettier/recommended" 6 | ], 7 | "plugins": [ 8 | "stylelint-csstree-validator" 9 | ], 10 | "rules": { 11 | "csstree/validator": true, 12 | "property-no-vendor-prefix": null, 13 | "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$", 14 | "selector-no-vendor-prefix": null, 15 | "value-no-vendor-prefix": null 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Jupyter Development Team 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyter-chat 2 | 3 | [![Github Actions Status](https://github.com/jupyterlab/jupyter-chat/workflows/Build/badge.svg)](https://github.com/jupyterlab/jupyter-chat/actions/workflows/build.yml) 4 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab/jupyter-chat/main?urlpath=lab) 5 | 6 | This project is a monorepo containing: 7 | 8 | - an extension to add a chat in jupyterlab 9 | - the frontend components to build a chat extension for Jupyter application 10 | 11 | Many components of this chat project come from [jupyter-ai](https://github.com/jupyterlab/jupyter-ai). 12 | 13 | ![a screenshot showing the jupyter-chat extension used in two browser windows](https://github.com/jupyterlab/jupyter-chat/assets/591645/5dac0b00-43ed-4458-ab67-18207644b92b) 14 | 15 | > [!WARNING] 16 | > This project is still in early development stage and its API may change often before 17 | > a stable release. 18 | 19 | ## Install chat extension 20 | 21 | The chat extension is available on [PyPI](https://pypi.org/project/jupyterlab-chat/). 22 | 23 | ```bash 24 | pip install jupyterlab-chat 25 | ``` 26 | 27 | To uninstall the package: 28 | 29 | ```bash 30 | pip uninstall jupyterlab-chat 31 | ``` 32 | 33 | > [!NOTE] 34 | > The extension was released as [jupyterlab-collaborative-chat](https://pypi.org/project/jupyterlab-collaborative-chat/) until version 0.5.0. 35 | 36 | ## Composition 37 | 38 | ### Typescript package 39 | 40 | #### @jupyter/chat 41 | 42 | The typescript package is located in _packages/jupyter-chat_ and builds an NPM 43 | package named `@jupyter/chat`. 44 | 45 | This package provides a frontend library (using react), and is intended to be 46 | used by a jupyterlab extension to create a chat. 47 | 48 | #### jupyterlab-chat 49 | 50 | The typescript package is located in _packages/jupyterlab-chat_ and 51 | builds an NPM package named `jupyterlab-chat`. 52 | 53 | This package relies on `@jupyter/chat` and provides a typescript library. 54 | It is intended to be used by a jupyterlab extension to create a chat. 55 | 56 | ### Jupyterlab extensions 57 | 58 | #### Chat extension based on shared document: _python/jupyterlab-chat_ 59 | 60 | This extension is an implementation of the `jupyter-chat` package, relying 61 | on shared document (see [jupyter_ydoc](https://github.com/jupyter-server/jupyter_ydoc)). 62 | 63 | It is composed of: 64 | 65 | - a Python package named `jupyterlab_chat`, which register 66 | the `YChat` shared document in jupyter_ydoc 67 | - a NPM package named `jupyterlab-chat-extension`. 68 | 69 | #### REMOVED - Chat extension based on websocket 70 | 71 | This extension has been moved to its own [repository](https://github.com/brichet/jupyterlab-ws-chat) 72 | 73 | ## Contributing 74 | 75 | See the contributing part of the [documentation](https://jupyter-chat.readthedocs.io/en/latest/developers/contributing/index.html). 76 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | name: jupyterlab-chat 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - jupyterlab >=4.0.0 6 | - nodejs >=18,<19 7 | - python >=3.11,<3.12 8 | - yarn >=3,<4 9 | # build 10 | - hatchling >=1.5.0 11 | - hatch-jupyter-builder >=0.3.2 12 | - hatch-nodejs-version 13 | # Use pip to get the the latest version 14 | - pip: 15 | - jupyter_server >=2.0.1,<3 16 | - jupyter_collaboration >=2.1.4,<3 17 | - jupyter_ydoc 18 | - pycrdt 19 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) Jupyter Development Team. 4 | # Distributed under the terms of the Modified BSD License. 5 | 6 | # Install chat extension 7 | ./scripts/dev_install.sh 8 | 9 | jupyter troubleshoot 10 | jupyter notebook --show-config 11 | jupyter lab --show-config 12 | jupyter labextension list 13 | jupyter server extension list 14 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | import pytest 4 | 5 | pytest_plugins = ("pytest_jupyter.jupyter_server", ) 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # Minimal makefile for Sphinx documentation 5 | # 6 | 7 | # You can set these variables from the command line, and also 8 | # from the environment for the first two. 9 | SPHINXOPTS ?= 10 | SPHINXBUILD ?= sphinx-build 11 | SOURCEDIR = source 12 | BUILDDIR = build 13 | 14 | # Put it first so that "make" without argument is like "make help". 15 | help: 16 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 17 | 18 | .PHONY: help Makefile 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/README.md: -------------------------------------------------------------------------------- 1 | # jupyter-chat-example 2 | 3 | A simple extension providing a chat side bar. 4 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyter_chat_example", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_chat" 5 | } 6 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/jupyter_chat_example/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | try: 5 | from ._version import __version__ 6 | except ImportError: 7 | # Fallback when using the package in dev mode without installing 8 | # in editable mode with pip. It is highly recommended to install 9 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs 10 | import warnings 11 | warnings.warn("Importing 'jupyter-chat-example' outside a proper installation.") 12 | __version__ = "dev" 13 | 14 | 15 | def _jupyter_labextension_paths(): 16 | return [{ 17 | "src": "labextension", 18 | "dest": "jupyter-chat-example" 19 | }] 20 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyter-chat-example", 3 | "version": "0.11.0", 4 | "description": "A chat extension providing a chat as example", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/jupyterlab/jupyter-chat", 11 | "bugs": { 12 | "url": "https://github.com/jupyterlab/jupyter-chat/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Jupyter Development Team", 17 | "email": "jupyter@googlegroups.com" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 22 | "src/**/*.{ts,tsx}", 23 | "schema/*.json" 24 | ], 25 | "main": "lib/index.js", 26 | "types": "lib/index.d.ts", 27 | "style": "style/index.css", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/jupyterlab/jupyter-chat.git" 31 | }, 32 | "scripts": { 33 | "build": "jlpm build:lib && jlpm build:labextension:dev", 34 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 35 | "build:labextension": "jupyter labextension build .", 36 | "build:labextension:dev": "jupyter labextension build --development True .", 37 | "build:lib": "tsc --sourceMap", 38 | "build:lib:prod": "tsc", 39 | "clean": "jlpm clean:lib", 40 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 41 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 42 | "clean:labextension": "rimraf jupyterlab_chat/labextension jupyterlab_chat/_version.py", 43 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 44 | "install:extension": "jlpm build" 45 | }, 46 | "dependencies": { 47 | "@jupyter/chat": "^0.11.0", 48 | "@jupyterlab/application": "^4.2.0", 49 | "@jupyterlab/apputils": "^4.3.0", 50 | "@jupyterlab/notebook": "^4.2.0", 51 | "@jupyterlab/rendermime": "^4.2.0", 52 | "@jupyterlab/settingregistry": "^4.2.0", 53 | "@lumino/coreutils": "^2.0.0" 54 | }, 55 | "devDependencies": { 56 | "@jupyterlab/builder": "^4.2.0", 57 | "@types/json-schema": "^7.0.11", 58 | "@types/react": "^18.2.0", 59 | "@types/react-addons-linked-state-mixin": "^0.14.22", 60 | "css-loader": "^6.7.1", 61 | "mkdirp": "^1.0.3", 62 | "npm-run-all": "^4.1.5", 63 | "rimraf": "^5.0.1", 64 | "source-map-loader": "^1.0.2", 65 | "style-loader": "^3.3.1", 66 | "typescript": "~5.0.2" 67 | }, 68 | "sideEffects": [ 69 | "style/*.css", 70 | "style/index.js" 71 | ], 72 | "styleModule": "style/index.js", 73 | "publishConfig": { 74 | "access": "public" 75 | }, 76 | "jupyterlab": { 77 | "discovery": { 78 | "server": { 79 | "managers": [ 80 | "pip" 81 | ], 82 | "base": { 83 | "name": "jupyter_chat_example" 84 | } 85 | } 86 | }, 87 | "extension": true, 88 | "outputDir": "jupyter_chat_example/labextension", 89 | "schemaDir": "schema", 90 | "sharedPackages": { 91 | "@jupyter/chat": { 92 | "bundled": true, 93 | "singleton": true 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | [build-system] 5 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "jupyter-chat-example" 10 | readme = "README.md" 11 | license = { file = "../../LICENSE" } 12 | requires-python = ">=3.8" 13 | classifiers = [ 14 | "Framework :: Jupyter", 15 | "Framework :: Jupyter :: JupyterLab", 16 | "Framework :: Jupyter :: JupyterLab :: 4", 17 | "Framework :: Jupyter :: JupyterLab :: Extensions", 18 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 19 | "License :: OSI Approved :: BSD License", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | ] 28 | dependencies = [] 29 | dynamic = ["version", "description", "authors", "urls", "keywords"] 30 | 31 | [tool.hatch.version] 32 | source = "nodejs" 33 | 34 | [tool.hatch.metadata.hooks.nodejs] 35 | fields = ["description", "authors", "urls"] 36 | 37 | [tool.hatch.build.targets.wheel.shared-data] 38 | "jupyter_chat_example/labextension" = "share/jupyter/labextensions/jupyter-chat-example" 39 | "install.json" = "share/jupyter/labextensions/jupyter-chat-example/install.json" 40 | 41 | [tool.hatch.build.hooks.version] 42 | path = "jupyter_chat_example/_version.py" 43 | 44 | [tool.hatch.build.hooks.jupyter-builder] 45 | dependencies = ["hatch-jupyter-builder>=0.5"] 46 | build-function = "hatch_jupyter_builder.npm_builder" 47 | ensured-targets = [ 48 | "jupyter_chat_example/labextension/static/style.js", 49 | "jupyter_chat_example/labextension/package.json", 50 | ] 51 | skip-if-exists = ["jupyter_chat_example/labextension/static/style.js"] 52 | 53 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 54 | build_cmd = "build:prod" 55 | npm = ["jlpm"] 56 | 57 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 58 | build_cmd = "install:extension" 59 | npm = ["jlpm"] 60 | source_dir = "src" 61 | build_dir = "jupyter_chat_example/labextension" 62 | 63 | [tool.jupyter-releaser.hooks] 64 | before-build-npm = [ 65 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 66 | "jlpm", 67 | "jlpm build:prod" 68 | ] 69 | before-build-python = ["jlpm clean:lib"] 70 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/schema/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Chat", 3 | "description": "Configuration for the chat widgets", 4 | "type": "object", 5 | "properties": { 6 | "sendWithShiftEnter": { 7 | "description": "Whether to send a message via Shift-Enter instead of Enter.", 8 | "type": "boolean", 9 | "default": false, 10 | "readOnly": false 11 | }, 12 | "stackMessages": { 13 | "description": "Whether to stack consecutive messages from same user.", 14 | "type": "boolean", 15 | "default": true, 16 | "readOnly": false 17 | }, 18 | "unreadNotifications": { 19 | "description": "Whether to enable or not the notifications on unread messages.", 20 | "type": "boolean", 21 | "default": true, 22 | "readOnly": false 23 | }, 24 | "enableCodeToolbar": { 25 | "description": "Whether to enable or not the code toolbar.", 26 | "type": "boolean", 27 | "default": true, 28 | "readOnly": false 29 | } 30 | }, 31 | "additionalProperties": false 32 | } 33 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /* 7 | See the JupyterLab Developer Guide for useful CSS Patterns: 8 | 9 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 10 | */ 11 | 12 | @import url('~jupyterlab-chat/style/index.css'); 13 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/style/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | @import url('base.css'); 7 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/style/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import './base.css'; 7 | -------------------------------------------------------------------------------- /docs/jupyter-chat-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/*"] 8 | } 9 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | rem Copyright (c) Jupyter Development Team. 2 | rem Distributed under the terms of the Modified BSD License. 3 | 4 | @ECHO OFF 5 | 6 | pushd %~dp0 7 | 8 | REM Command file for Sphinx documentation 9 | 10 | if "%SPHINXBUILD%" == "" ( 11 | set SPHINXBUILD=sphinx-build 12 | ) 13 | set SOURCEDIR=source 14 | set BUILDDIR=build 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.https://www.sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | if "%1" == "" goto help 30 | 31 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 32 | goto end 33 | 34 | :help 35 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 36 | 37 | :end 38 | popd 39 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | jupyterlab>=4 2 | jupyterlite-core 3 | jupyterlite-pyodide-kernel 4 | jupyterlite-sphinx 5 | myst-parser 6 | pydata-sphinx-theme 7 | sphinx 8 | sphinx-copybutton 9 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | .try-in-jupyterlite-button a { 7 | background-color: #f7dc1e; 8 | color: black; 9 | text-decoration: none; 10 | 11 | border: none; 12 | padding: 5px 10px; 13 | border-radius: 15px; 14 | font-family: vibur; 15 | font-size: larger; 16 | box-shadow: 0 2px 5px rgba(108, 108, 108, 0.2); 17 | } 18 | 19 | .try-in-jupyterlite-button a:hover { 20 | color: white; 21 | box-shadow: 0 2px 5px #f7dc1e; 22 | } 23 | -------------------------------------------------------------------------------- /docs/source/_static/images/chat-widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/docs/source/_static/images/chat-widgets.png -------------------------------------------------------------------------------- /docs/source/_static/images/code-toolbar-above.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/docs/source/_static/images/code-toolbar-above.png -------------------------------------------------------------------------------- /docs/source/_static/images/code-toolbar-below.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/docs/source/_static/images/code-toolbar-below.png -------------------------------------------------------------------------------- /docs/source/_static/images/code-toolbar-copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/docs/source/_static/images/code-toolbar-copy.png -------------------------------------------------------------------------------- /docs/source/_static/images/code-toolbar-replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/docs/source/_static/images/code-toolbar-replace.png -------------------------------------------------------------------------------- /docs/source/_static/images/left-panel-new-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/docs/source/_static/images/left-panel-new-chat.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # Configuration file for the Sphinx documentation builder. 5 | # 6 | # For the full list of built-in configuration values, see the documentation: 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 8 | 9 | # -- Project information ----------------------------------------------------- 10 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 11 | 12 | project = "jupyter-chat" 13 | copyright = "2024, Jupyter Development Team" 14 | author = "Jupyter Development Team" 15 | 16 | # -- General configuration --------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 18 | 19 | extensions = [ 20 | "jupyterlite_sphinx", 21 | "myst_parser", 22 | "sphinx_copybutton" 23 | ] 24 | 25 | templates_path = ["_templates"] 26 | exclude_patterns = [] 27 | 28 | myst_enable_extensions = ["attrs_block", "attrs_inline"] 29 | 30 | # -- Options for HTML output ------------------------------------------------- 31 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 32 | 33 | html_theme = "pydata_sphinx_theme" 34 | html_static_path = ["_static"] 35 | html_css_files = ["css/custom.css"] 36 | 37 | jupyterlite_contents = "README.ipynb" 38 | -------------------------------------------------------------------------------- /docs/source/developers/contributing/index.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We would be glad to review all incoming contributions. 4 | 5 | This project is included in _jupyterlab_ organization, please read the 6 | [jupyter contributing guide](https://docs.jupyter.org/en/latest/contributing/content-contributor.html). 7 | 8 | ```{toctree} 9 | --- 10 | maxdepth: 2 11 | --- 12 | 13 | ./jupyter-chat 14 | ./jupyterlab-chat-extension 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/source/developers/contributing/jupyter-chat.md: -------------------------------------------------------------------------------- 1 | # @jupyter/chat 2 | 3 | The `@jupyter/chat` package is a frontend package (React) compatible with jupyterlab. 4 | It is not an extension, and cannot be used on its own. It is designed to be used in an 5 | extension.\ 6 | In this repository, it is currently used in `jupyterlab-chat` extension. 7 | 8 | ## Building the package 9 | 10 | From the root of the repository: 11 | 12 | ```bash 13 | # In the following command, 'jlpm' can be replaced with 'yarn' 14 | jlpm build:core 15 | ``` 16 | 17 | From the package itself (`./packages/jupyter-chat`): 18 | 19 | ```bash 20 | jlpm build 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/source/developers/contributing/jupyterlab-chat-extension.md: -------------------------------------------------------------------------------- 1 | # Jupyterlab chat 2 | 3 | The `jupyterlab-chat` extension adds chats to jupyterlab based on collaborative documents. 4 | 5 | ## Development installation 6 | 7 | Installing this extension in development mode requires an environment with _python_ and 8 | _nodejs_. 9 | 10 | ```bash 11 | # In the following commands, 'mamba' can be replaced with 'conda' 12 | mamba create -n jupyter-chat python nodejs 13 | mamba activate jupyter-chat 14 | ``` 15 | 16 | The following commands install the extension in development mode: 17 | 18 | ```bash 19 | # Install the extension 20 | python ./scripts/dev_install.py 21 | ``` 22 | 23 | To uninstall it, run: 24 | 25 | ```bash 26 | pip uninstall jupyterlab-chat 27 | ``` 28 | 29 | ## Building the assets 30 | 31 | Changes in typescript sources of `@jupyter/chat`, `jupyterlab-chat` or 32 | `jupyterlab-chat-extension` must be built again to be available in jupyterlab. 33 | 34 | ```bash 35 | jlpm build 36 | ``` 37 | 38 | ## Testing locally the extension 39 | 40 | `jupyterlab-chat` package has unit tests and integration tests. 41 | 42 | ### Unit tests 43 | 44 | There are a few unit tests in `python/jupyterlab-chat/src/\_\_tests\_\_`. 45 | 46 | They make use of [jest](https://jestjs.io/). 47 | 48 | The following commands run them: 49 | 50 | ```bash 51 | cd ./python/jupyterlab-chat 52 | jlpm test 53 | ``` 54 | 55 | ### Integration tests 56 | 57 | The integration tests are located in _ui-tests_. 58 | 59 | They make use of [playwright](https://playwright.dev/). 60 | 61 | The following commands run them: 62 | 63 | ```bash 64 | cd ./ui-tests 65 | 66 | # Install the tests dependencies 67 | jlpm install 68 | 69 | # Install the tests browser 70 | jlpm playwright install 71 | 72 | # Run the tests 73 | jlpm test 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/source/developers/developing_extensions/index.md: -------------------------------------------------------------------------------- 1 | # Developing extensions 2 | 3 | Other extensions can depend on one or other of the packages. 4 | 5 | This section describes how an extension can provide a chat, and how to make use of one 6 | of the chat extensions in another extension. 7 | 8 | ```{toctree} 9 | --- 10 | maxdepth: 2 11 | --- 12 | 13 | ./extension-providing-chat 14 | ./using-chat-extension 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/source/developers/index.md: -------------------------------------------------------------------------------- 1 | # Developers 2 | 3 | This section is aimed at developers and contains documentation on : 4 | 5 | - how to develop a chat extension or include one of the chat extensions 6 | - how to contribute to this project 7 | 8 | ```{warning} 9 | This project is still in early development stage and its API may change often before a 10 | stable release. 11 | ``` 12 | 13 | ```{toctree} 14 | --- 15 | maxdepth: 2 16 | --- 17 | 18 | ./developing_extensions/index.md 19 | ./contributing/index.md 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | # jupyter-chat 2 | 3 | Jupyter-chat allows you to easily include a chat in a jupyter based application. 4 | 5 | This documentation includes several projects all together: 6 | 7 | - `@jupyter/chat`, a front-end package (typescript), including all the components 8 | required to build a chat. This package is designed to be used in an extension. 9 | 10 | - `jupyterlab-chat`, an extension built on top of `@jupyter/chat`, using 11 | the collaborative edition as messaging system. 12 | 13 | ```{toctree} 14 | --- 15 | maxdepth: 3 16 | --- 17 | 18 | users/index 19 | developers/index 20 | ``` 21 | 22 | ```{eval-rst} 23 | .. cssclass:: try-in-jupyterlite-button 24 | 25 | `Try it with JupyterLite! `_ 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/source/users/index.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | ## Chat extension 4 | 5 | The `jupyterlab-chat` extension adds chats to jupyterlab. 6 | 7 | ![chat widgets](../_static/images/chat-widgets.png) 8 | 9 | These chats use [jupyter_collaboration](https://jupyterlab-realtime-collaboration.readthedocs.io/en/latest/), 10 | the collaborative edition of documents in jupyterlab. 11 | 12 | ### Install chat extension 13 | 14 | The chat extension is available on [PyPI](https://pypi.org/project/jupyterlab-chat/). 15 | 16 | ```bash 17 | pip install jupyterlab-chat 18 | ``` 19 | 20 | To uninstall the package: 21 | 22 | ```bash 23 | pip uninstall jupyterlab-chat 24 | ``` 25 | 26 | ### Create a chat 27 | 28 | There are several ways to create a chat: 29 | 30 | - using the menu : _file -> new -> chat_ 31 | - using the commands palette (`Ctrl+Shift+C`) -> _Create a new chat_ 32 | - from the left panel ![chat icon](../../../packages/jupyter-chat/style/icons/chat.svg){w=24px}, 33 | click on the button ![left panel new chat](../_static/images/left-panel-new-chat.png){h=24px} 34 | 35 | Validating the dialog will create the new chat. 36 | 37 | Creating a chat actually creates a file (shared document) in the tree files. 38 | 39 | ```{warning} 40 | Currently, the left panel can only discover chat files in the root directory (to avoid 41 | computation issues in large nested tree files), so it only creates chat files in the 42 | root directory. 43 | 44 | On the other hand, creating a chat using the menu or the command palette will create 45 | the file in the current directory of the file browser. 46 | ``` 47 | 48 | ### Open a chat 49 | 50 | There are also several ways to open a chat: 51 | 52 | - opening the file from the file browser (double click on it) 53 | - using the commands palette (`Ctrl+Shift+C`) -> _Open a chat_. It opens a dialog to 54 | type the file path 55 | - from the left panel ![chat icon](../../../packages/jupyter-chat/style/icons/chat.svg){w=24px}, 56 | there is a dropdown listing the chat files, in the root directory only. 57 | 58 | ```{note} 59 | Opening the chat from the file browser or the command palette will open it in the main 60 | area, like any other document. 61 | 62 | Opening a chat from the left panel will open it in the left panel. 63 | ``` 64 | 65 | ## Chat usage 66 | 67 | The chat UI is composed of a list of messages and an input to send new messages. 68 | 69 | A message can be edited or deleted by its author, using a dedicated toolbar in the 70 | message. 71 | 72 | ### Notifications and navigation 73 | 74 | If enabled in [settings](#chat-settings), new unread messages generate a notification. 75 | 76 | A down arrow in the messages list allow to navigate to the last message. This button is 77 | highlighted if some new messages are unread. 78 | 79 | (code-toolbar)= 80 | 81 | ### Code toolbar 82 | 83 | When code is inserted in a message, a toolbar is displayed under the code section (the 84 | options must be set up from the [settings](#chat-settings)). 85 | 86 | From this toolbar, the code can be copied to the clipboard: 87 | ![code toolbar copy](../_static/images/code-toolbar-copy.png){w=24px}. 88 | 89 | If a notebook is opened and visible (and has an active cell), other actions are 90 | available: 91 | 92 | - copy the code to a new cell above the active one: 93 | ![code toolbar cell above](../_static/images/code-toolbar-above.png){w=24px} 94 | - copy the the code to a new cell below the active one: 95 | ![code toolbar cell below](../_static/images/code-toolbar-below.png){w=24px} 96 | - replace the content of the active cell with the code: 97 | ![code toolbar cell replace](../_static/images/code-toolbar-replace.png){w=24px} 98 | 99 | (chat-settings)= 100 | 101 | ### Attachments 102 | 103 | Files can be attached to the messages using the clip icon next to the send icon in the input. 104 | It opens a Dialog allowing to select and atach files. 105 | 106 | Attachments can then be opened by clicking on the preview icon. 107 | 108 | ## Chat settings 109 | 110 | Some jupyterlab settings are available for the chats in the setting panel 111 | (menu `Settings->Settings Editor`), with the entry _Chat_. 112 | 113 | These settings includes: 114 | 115 | - **sendWithShiftEnter** 116 | 117 | Whether to send a message using Shift-Enter instead of Enter.\ 118 | Default: false 119 | 120 | - **stackMessages** 121 | 122 | Whether to stack consecutive messages from same user.\ 123 | Default: true 124 | 125 | - **unreadNotifications** 126 | 127 | Whether to enable or not the notifications on unread messages.\ 128 | Default: true 129 | 130 | - **enableCodeToolbar** 131 | 132 | Whether to enable or not the code toolbar.\ 133 | Default: true 134 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "useWorkspaces": true, 4 | "version": "0.11.0", 5 | "npmClient": "jlpm", 6 | "useNx": true 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyter-chat-root", 3 | "version": "0.11.0", 4 | "description": "A chat package for Jupyterlab extension", 5 | "private": true, 6 | "keywords": [ 7 | "jupyter", 8 | "jupyterlab", 9 | "jupyterlab-extension" 10 | ], 11 | "homepage": "https://github.com/jupyterlab/jupyter-chat", 12 | "bugs": { 13 | "url": "https://github.com/jupyterlab/jupyter-chat/issues" 14 | }, 15 | "license": "BSD-3-Clause", 16 | "author": { 17 | "name": "Jupyter Development Team", 18 | "email": "jupyter@googlegroups.com" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/jupyterlab/jupyter-chat.git" 23 | }, 24 | "workspaces": [ 25 | "packages/*", 26 | "docs/jupyter-chat-example" 27 | ], 28 | "scripts": { 29 | "build": "lerna run build --stream --scope jupyterlab-chat-extension --include-filtered-dependencies", 30 | "build:core": "lerna run build --stream --scope \"@jupyter/chat\"", 31 | "build:lite-docs": "lerna run build --stream --scope jupyter-chat-example --include-filtered-dependencies", 32 | "build:prod": "lerna run build:prod --stream", 33 | "clean": "lerna run clean", 34 | "clean:all": "lerna run clean:all", 35 | "dev": "jupyter lab --config playground/config.py", 36 | "eslint": "eslint . --ext .ts,.tsx --cache --fix", 37 | "eslint:check": "eslint . --ext .ts,.tsx", 38 | "install:extension": "lerna run install:extension", 39 | "lint:check": "jlpm run prettier:check && jlpm run eslint:check", 40 | "lint": "jlpm run prettier && jlpm run eslint && jlpm stylelint", 41 | "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md,.yml}\"", 42 | "prettier:check": "prettier --list-different \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md,.yml}\"", 43 | "stylelint": "jlpm stylelint:check --fix", 44 | "stylelint:check": "stylelint --cache \"**/style/**/*.css\"", 45 | "test": "lerna run test", 46 | "watch": "lerna run watch --parallel --stream" 47 | }, 48 | "devDependencies": { 49 | "@stylistic/eslint-plugin": "^3.0.1", 50 | "@typescript-eslint/eslint-plugin": "^8.0.0", 51 | "@typescript-eslint/parser": "^8.0.0", 52 | "eslint": "^8.56.0", 53 | "eslint-config-prettier": "^8.8.0", 54 | "eslint-plugin-prettier": "^5.0.0", 55 | "lerna": "^6.4.1", 56 | "prettier": "^3.0.0", 57 | "style-loader": "^3.3.1", 58 | "stylelint": "^15.10.1", 59 | "stylelint-config-recommended": "^13.0.0", 60 | "stylelint-config-standard": "^34.0.0", 61 | "stylelint-csstree-validator": "^3.0.0", 62 | "stylelint-prettier": "^4.0.0" 63 | }, 64 | "packageManager": "yarn@3.5.0" 65 | } 66 | -------------------------------------------------------------------------------- /packages/jupyter-chat/babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | module.exports = require('@jupyterlab/testing/lib/babel-config'); 7 | -------------------------------------------------------------------------------- /packages/jupyter-chat/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | const jestJupyterLab = require('@jupyterlab/testing/lib/jest-config'); 7 | 8 | const esModules = [ 9 | '@codemirror', 10 | '@microsoft', 11 | '@jupyter/react-components', 12 | '@jupyter/web-components', 13 | '@jupyter/ydoc', 14 | '@jupyterlab/', 15 | 'exenv-es6', 16 | 'lib0', 17 | 'nanoid', 18 | 'vscode-ws-jsonrpc', 19 | 'y-protocols', 20 | 'y-websocket', 21 | 'yjs' 22 | ].join('|'); 23 | 24 | const baseConfig = jestJupyterLab(__dirname); 25 | 26 | module.exports = { 27 | ...baseConfig, 28 | automock: false, 29 | collectCoverageFrom: [ 30 | 'src/**/*.{ts,tsx}', 31 | '!src/**/*.d.ts', 32 | '!src/**/.ipynb_checkpoints/*' 33 | ], 34 | coverageReporters: ['lcov', 'text'], 35 | testRegex: 'src/.*/.*.spec.ts[x]?$', 36 | transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] 37 | }; 38 | -------------------------------------------------------------------------------- /packages/jupyter-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter/chat", 3 | "version": "0.11.0", 4 | "description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/jupyterlab/jupyter-chat", 11 | "bugs": { 12 | "url": "https://github.com/jupyterlab/jupyter-chat/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Jupyter Development Team", 17 | "email": "jupyter@googlegroups.com" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 22 | "src/**/*.{ts,tsx}", 23 | "schema/*.json" 24 | ], 25 | "main": "lib/index.js", 26 | "types": "lib/index.d.ts", 27 | "style": "style/index.css", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/jupyterlab/jupyter-chat.git" 31 | }, 32 | "scripts": { 33 | "build": "jlpm build:lib", 34 | "build:prod": "jlpm clean && jlpm build:lib:prod", 35 | "build:lib": "tsc --sourceMap", 36 | "build:lib:prod": "tsc", 37 | "clean": "jlpm clean:lib", 38 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 39 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 40 | "clean:all": "jlpm clean:lib && jlpm clean:lintcache", 41 | "install:extension": "jlpm build", 42 | "test": "jest --coverage", 43 | "watch:src": "tsc -w --sourceMap" 44 | }, 45 | "dependencies": { 46 | "@emotion/react": "^11.10.5", 47 | "@emotion/styled": "^11.10.5", 48 | "@jupyter/react-components": "^0.15.2", 49 | "@jupyterlab/application": "^4.2.0", 50 | "@jupyterlab/apputils": "^4.3.0", 51 | "@jupyterlab/docmanager": "^4.2.0", 52 | "@jupyterlab/filebrowser": "^4.2.0", 53 | "@jupyterlab/fileeditor": "^4.2.0", 54 | "@jupyterlab/notebook": "^4.2.0", 55 | "@jupyterlab/rendermime": "^4.2.0", 56 | "@jupyterlab/ui-components": "^4.2.0", 57 | "@lumino/commands": "^2.0.0", 58 | "@lumino/coreutils": "^2.0.0", 59 | "@lumino/disposable": "^2.0.0", 60 | "@lumino/signaling": "^2.0.0", 61 | "@mui/icons-material": "^5.11.0", 62 | "@mui/material": "^5.11.0", 63 | "clsx": "^2.1.0", 64 | "react": "^18.2.0", 65 | "react-dom": "^18.2.0" 66 | }, 67 | "devDependencies": { 68 | "@jupyterlab/testing": "^4.2.0", 69 | "@types/jest": "^29.2.0", 70 | "@types/json-schema": "^7.0.11", 71 | "@types/react": "^18.2.0", 72 | "@types/react-addons-linked-state-mixin": "^0.14.22", 73 | "@types/react-dom": "^18.2.0", 74 | "css-loader": "^6.7.1", 75 | "jest": "^29.2.0", 76 | "npm-run-all": "^4.1.5", 77 | "rimraf": "^5.0.1", 78 | "source-map-loader": "^1.0.2", 79 | "style-loader": "^3.3.1", 80 | "typescript": "~5.0.2" 81 | }, 82 | "sideEffects": [ 83 | "style/*.css", 84 | "style/index.js" 85 | ], 86 | "styleModule": "style/index.js", 87 | "publishConfig": { 88 | "access": "public" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/__tests__/mocks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { 7 | AbstractChatContext, 8 | AbstractChatModel, 9 | IChatModel, 10 | IChatContext 11 | } from '../model'; 12 | import { INewMessage } from '../types'; 13 | 14 | export class MockChatContext 15 | extends AbstractChatContext 16 | implements IChatContext 17 | { 18 | get users() { 19 | return []; 20 | } 21 | } 22 | 23 | export class MockChatModel extends AbstractChatModel implements IChatModel { 24 | sendMessage(message: INewMessage): Promise | boolean | void { 25 | // No-op 26 | } 27 | 28 | createChatContext(): IChatContext { 29 | return new MockChatContext({ model: this }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/__tests__/model.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /** 7 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 8 | */ 9 | 10 | import { AbstractChatModel, IChatContext, IChatModel } from '../model'; 11 | import { IChatMessage, INewMessage } from '../types'; 12 | import { MockChatModel, MockChatContext } from './mocks'; 13 | 14 | describe('test chat model', () => { 15 | describe('model instantiation', () => { 16 | it('should create an AbstractChatModel', () => { 17 | const model = new MockChatModel(); 18 | expect(model).toBeInstanceOf(AbstractChatModel); 19 | }); 20 | 21 | it('should dispose an AbstractChatModel', () => { 22 | const model = new MockChatModel(); 23 | model.dispose(); 24 | expect(model.isDisposed).toBeTruthy(); 25 | }); 26 | }); 27 | 28 | describe('incoming message', () => { 29 | class TestChat extends AbstractChatModel implements IChatModel { 30 | protected formatChatMessage(message: IChatMessage): IChatMessage { 31 | message.body = 'formatted msg'; 32 | return message; 33 | } 34 | sendMessage( 35 | message: INewMessage 36 | ): Promise | boolean | void { 37 | // No-op 38 | } 39 | 40 | createChatContext(): IChatContext { 41 | return new MockChatContext({ model: this }); 42 | } 43 | } 44 | 45 | let model: IChatModel; 46 | let messages: IChatMessage[]; 47 | const msg = { 48 | type: 'msg', 49 | id: 'message1', 50 | time: Date.now() / 1000, 51 | body: 'message test', 52 | sender: { username: 'user' } 53 | } as IChatMessage; 54 | 55 | beforeEach(() => { 56 | messages = []; 57 | }); 58 | 59 | it('should signal incoming message', () => { 60 | model = new MockChatModel(); 61 | model.messagesUpdated.connect((sender: IChatModel) => { 62 | expect(sender).toBe(model); 63 | messages = model.messages; 64 | }); 65 | model.messageAdded(msg); 66 | expect(messages).toHaveLength(1); 67 | expect(messages[0]).toBe(msg); 68 | }); 69 | 70 | it('should format message', () => { 71 | model = new TestChat(); 72 | model.messagesUpdated.connect((sender: IChatModel) => { 73 | expect(sender).toBe(model); 74 | messages = model.messages; 75 | }); 76 | model.messageAdded({ ...msg }); 77 | expect(messages).toHaveLength(1); 78 | expect(messages[0]).not.toBe(msg); 79 | expect((messages[0] as IChatMessage).body).toBe('formatted msg'); 80 | }); 81 | }); 82 | 83 | describe('model config', () => { 84 | it('should have empty config', () => { 85 | const model = new MockChatModel(); 86 | expect(model.config.sendWithShiftEnter).toBeUndefined(); 87 | }); 88 | 89 | it('should allow config', () => { 90 | const model = new MockChatModel({ config: { sendWithShiftEnter: true } }); 91 | expect(model.config.sendWithShiftEnter).toBeTruthy(); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/__tests__/widgets.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /** 7 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 8 | */ 9 | 10 | import { 11 | IRenderMimeRegistry, 12 | RenderMimeRegistry 13 | } from '@jupyterlab/rendermime'; 14 | import { IChatModel } from '../model'; 15 | import { ChatWidget } from '../widgets/chat-widget'; 16 | import { MockChatModel } from './mocks'; 17 | 18 | describe('test chat widget', () => { 19 | let model: IChatModel; 20 | let rmRegistry: IRenderMimeRegistry; 21 | 22 | beforeEach(() => { 23 | model = new MockChatModel(); 24 | rmRegistry = new RenderMimeRegistry(); 25 | }); 26 | 27 | describe('model instantiation', () => { 28 | it('should create an AbstractChatModel', () => { 29 | const widget = new ChatWidget({ model, rmRegistry }); 30 | expect(widget).toBeInstanceOf(ChatWidget); 31 | }); 32 | 33 | it('should dispose an AbstractChatModel', () => { 34 | const widget = new ChatWidget({ model, rmRegistry }); 35 | widget.dispose(); 36 | expect(widget.isDisposed).toBeTruthy(); 37 | }); 38 | 39 | it('should provides the model', () => { 40 | const widget = new ChatWidget({ model, rmRegistry }); 41 | expect(widget.model).toBe(model); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/chat-commands/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export * from './types'; 7 | export * from './registry'; 8 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/chat-commands/registry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { Token } from '@lumino/coreutils'; 7 | import { ChatCommand, IChatCommandProvider } from './types'; 8 | import { IInputModel } from '../input-model'; 9 | 10 | /** 11 | * Interface of a chat command registry, which tracks a list of chat command 12 | * providers. Providers provide a list of commands given a user's partial input, 13 | * and define how commands are handled when accepted in the chat commands menu. 14 | */ 15 | export interface IChatCommandRegistry { 16 | addProvider(provider: IChatCommandProvider): void; 17 | getProviders(): IChatCommandProvider[]; 18 | 19 | /** 20 | * Handles a chat command by calling `handleChatCommand()` on the provider 21 | * corresponding to this chat command. 22 | */ 23 | handleChatCommand(command: ChatCommand, inputModel: IInputModel): void; 24 | } 25 | 26 | /** 27 | * Default chat command registry implementation. 28 | */ 29 | export class ChatCommandRegistry implements IChatCommandRegistry { 30 | constructor() { 31 | this._providers = new Map(); 32 | } 33 | 34 | addProvider(provider: IChatCommandProvider): void { 35 | this._providers.set(provider.id, provider); 36 | } 37 | 38 | getProviders(): IChatCommandProvider[] { 39 | return Array.from(this._providers.values()); 40 | } 41 | 42 | handleChatCommand(command: ChatCommand, inputModel: IInputModel) { 43 | const provider = this._providers.get(command.providerId); 44 | if (!provider) { 45 | console.error( 46 | 'Error in handling chat command: No command provider has an ID of ' + 47 | command.providerId 48 | ); 49 | return; 50 | } 51 | 52 | provider.handleChatCommand(command, inputModel); 53 | } 54 | 55 | private _providers: Map; 56 | } 57 | 58 | export const IChatCommandRegistry = new Token( 59 | '@jupyter/chat:IChatCommandRegistry' 60 | ); 61 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/chat-commands/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { LabIcon } from '@jupyterlab/ui-components'; 7 | import { IInputModel } from '../input-model'; 8 | 9 | export type ChatCommand = { 10 | /** 11 | * The name of the command. This defines what the user should type in the 12 | * input to have the command appear in the chat commands menu. 13 | */ 14 | name: string; 15 | 16 | /** 17 | * ID of the provider the command originated from. 18 | */ 19 | providerId: string; 20 | 21 | /** 22 | * If set, this will be rendered as the icon for the command in the chat 23 | * commands menu. Jupyter Chat will choose a default if this is unset. 24 | */ 25 | icon?: LabIcon | JSX.Element | string | null; 26 | 27 | /** 28 | * If set, this will be rendered as the description for the command in the 29 | * chat commands menu. Jupyter Chat will choose a default if this is unset. 30 | */ 31 | description?: string; 32 | 33 | /** 34 | * If set, Jupyter Chat will replace the current word with this string after 35 | * the command is run from the chat commands menu. 36 | * 37 | * If all commands from a provider have this property set, then 38 | * `handleChatCommands()` can just return on the first line. 39 | */ 40 | replaceWith?: string; 41 | }; 42 | 43 | /** 44 | * Interface of a command provider. 45 | */ 46 | export interface IChatCommandProvider { 47 | /** 48 | * ID of this command provider. 49 | */ 50 | id: string; 51 | 52 | /** 53 | * Async function which accepts the input model and returns a list of 54 | * valid chat commands that match the current word. The current word is 55 | * space-separated word at the user's cursor. 56 | */ 57 | getChatCommands(inputModel: IInputModel): Promise; 58 | 59 | /** 60 | * Function called when a chat command is run by the user through the chat 61 | * commands menu. 62 | */ 63 | handleChatCommand( 64 | command: ChatCommand, 65 | inputModel: IInputModel 66 | ): Promise; 67 | } 68 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/attachments.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | // import { IDocumentManager } from '@jupyterlab/docmanager'; 7 | import CloseIcon from '@mui/icons-material/Close'; 8 | import { Box } from '@mui/material'; 9 | import React, { useContext } from 'react'; 10 | 11 | import { TooltippedButton } from './mui-extras/tooltipped-button'; 12 | import { IAttachment } from '../types'; 13 | import { AttachmentOpenerContext } from '../context'; 14 | 15 | const ATTACHMENTS_CLASS = 'jp-chat-attachments'; 16 | const ATTACHMENT_CLASS = 'jp-chat-attachment'; 17 | const ATTACHMENT_CLICKABLE_CLASS = 'jp-chat-attachment-clickable'; 18 | const REMOVE_BUTTON_CLASS = 'jp-chat-attachment-remove'; 19 | 20 | /** 21 | * The attachments props. 22 | */ 23 | export type AttachmentsProps = { 24 | attachments: IAttachment[]; 25 | onRemove?: (attachment: IAttachment) => void; 26 | }; 27 | 28 | /** 29 | * The Attachments component. 30 | */ 31 | export function AttachmentPreviewList(props: AttachmentsProps): JSX.Element { 32 | return ( 33 | 34 | {props.attachments.map(attachment => ( 35 | 36 | ))} 37 | 38 | ); 39 | } 40 | 41 | /** 42 | * The attachment props. 43 | */ 44 | export type AttachmentProps = AttachmentsProps & { 45 | attachment: IAttachment; 46 | }; 47 | 48 | /** 49 | * The Attachment component. 50 | */ 51 | export function AttachmentPreview(props: AttachmentProps): JSX.Element { 52 | const remove_tooltip = 'Remove attachment'; 53 | const attachmentOpenerRegistry = useContext(AttachmentOpenerContext); 54 | 55 | return ( 56 | 57 | 64 | attachmentOpenerRegistry?.get(props.attachment.type)?.( 65 | props.attachment 66 | ) 67 | } 68 | > 69 | {props.attachment.value} 70 | 71 | {props.onRemove && ( 72 | props.onRemove!(props.attachment)} 74 | tooltip={remove_tooltip} 75 | buttonProps={{ 76 | size: 'small', 77 | title: remove_tooltip, 78 | className: REMOVE_BUTTON_CLASS 79 | }} 80 | sx={{ 81 | minWidth: 'unset', 82 | padding: '0', 83 | color: 'inherit' 84 | }} 85 | > 86 | 87 | 88 | )} 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/code-blocks/copy-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import React, { useState, useCallback, useRef } from 'react'; 7 | 8 | import { copyIcon } from '@jupyterlab/ui-components'; 9 | 10 | import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button'; 11 | 12 | enum CopyStatus { 13 | None, 14 | Copying, 15 | Copied, 16 | Disabled 17 | } 18 | 19 | const COPYBTN_TEXT_BY_STATUS: Record = { 20 | [CopyStatus.None]: 'Copy to clipboard', 21 | [CopyStatus.Copying]: 'Copying…', 22 | [CopyStatus.Copied]: 'Copied!', 23 | [CopyStatus.Disabled]: 'Copy to clipboard disabled in insecure context' 24 | }; 25 | 26 | type CopyButtonProps = { 27 | value: string; 28 | className?: string; 29 | }; 30 | 31 | export function CopyButton(props: CopyButtonProps): JSX.Element { 32 | const isCopyDisabled = navigator.clipboard === undefined; 33 | const [copyStatus, setCopyStatus] = useState( 34 | isCopyDisabled ? CopyStatus.Disabled : CopyStatus.None 35 | ); 36 | const timeoutId = useRef(null); 37 | 38 | const copy = useCallback(async () => { 39 | // ignore if we are already copying 40 | if (copyStatus === CopyStatus.Copying) { 41 | return; 42 | } 43 | 44 | try { 45 | await navigator.clipboard.writeText(props.value); 46 | } catch (err) { 47 | console.error('Failed to copy text: ', err); 48 | setCopyStatus(CopyStatus.None); 49 | return; 50 | } 51 | 52 | setCopyStatus(CopyStatus.Copied); 53 | if (timeoutId.current) { 54 | clearTimeout(timeoutId.current); 55 | } 56 | timeoutId.current = window.setTimeout( 57 | () => setCopyStatus(CopyStatus.None), 58 | 1000 59 | ); 60 | }, [copyStatus, props.value]); 61 | 62 | return ( 63 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/code-blocks/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export * from './code-toolbar'; 7 | export * from './copy-button'; 8 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export * from './chat'; 7 | export * from './chat-input'; 8 | export * from './chat-messages'; 9 | export * from './code-blocks'; 10 | export * from './input'; 11 | export * from './jl-theme-provider'; 12 | export * from './markdown-renderer'; 13 | export * from './mui-extras'; 14 | export * from './scroll-container'; 15 | export * from './toolbar'; 16 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/input/buttons/attach-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { FileDialog } from '@jupyterlab/filebrowser'; 7 | import AttachFileIcon from '@mui/icons-material/AttachFile'; 8 | import React from 'react'; 9 | 10 | import { InputToolbarRegistry } from '../toolbar-registry'; 11 | import { TooltippedButton } from '../../mui-extras/tooltipped-button'; 12 | 13 | const ATTACH_BUTTON_CLASS = 'jp-chat-attach-button'; 14 | 15 | /** 16 | * The attach button. 17 | */ 18 | export function AttachButton( 19 | props: InputToolbarRegistry.IToolbarItemProps 20 | ): JSX.Element { 21 | const { model } = props; 22 | const tooltip = 'Add attachment'; 23 | 24 | if (!model.documentManager || !model.addAttachment) { 25 | return <>; 26 | } 27 | 28 | const onclick = async () => { 29 | if (!model.documentManager || !model.addAttachment) { 30 | return; 31 | } 32 | try { 33 | const files = await FileDialog.getOpenFiles({ 34 | title: 'Select files to attach', 35 | manager: model.documentManager 36 | }); 37 | if (files.value) { 38 | files.value.forEach(file => { 39 | if (file.type !== 'directory') { 40 | model.addAttachment?.({ type: 'file', value: file.path }); 41 | } 42 | }); 43 | } 44 | } catch (e) { 45 | console.warn('Error selecting files to attach', e); 46 | } 47 | }; 48 | 49 | return ( 50 | 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/input/buttons/cancel-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import CancelIcon from '@mui/icons-material/Cancel'; 7 | import React from 'react'; 8 | 9 | import { InputToolbarRegistry } from '../toolbar-registry'; 10 | import { TooltippedButton } from '../../mui-extras/tooltipped-button'; 11 | 12 | const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button'; 13 | 14 | /** 15 | * The cancel button. 16 | */ 17 | export function CancelButton( 18 | props: InputToolbarRegistry.IToolbarItemProps 19 | ): JSX.Element { 20 | if (!props.model.cancel) { 21 | return <>; 22 | } 23 | const tooltip = 'Cancel edition'; 24 | return ( 25 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/input/buttons/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export { AttachButton } from './attach-button'; 7 | export { CancelButton } from './cancel-button'; 8 | export { SendButton } from './send-button'; 9 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/input/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export * from './buttons'; 7 | export * from './toolbar-registry'; 8 | export * from './use-chat-commands'; 9 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/input/toolbar-registry.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | import * as React from 'react'; 6 | 7 | import { AttachButton, CancelButton, SendButton } from './buttons'; 8 | import { IInputModel } from '../../input-model'; 9 | import { ISignal, Signal } from '@lumino/signaling'; 10 | 11 | /** 12 | * The toolbar registry interface. 13 | */ 14 | export interface IInputToolbarRegistry { 15 | /** 16 | * A signal emitting when the items has changed. 17 | */ 18 | readonly itemsChanged: ISignal; 19 | /** 20 | * Get a toolbar item. 21 | */ 22 | get(name: string): InputToolbarRegistry.IToolbarItem | undefined; 23 | 24 | /** 25 | * Get the list of the visible toolbar items in order. 26 | */ 27 | getItems(): InputToolbarRegistry.IToolbarItem[]; 28 | 29 | /** 30 | * Add a toolbar item. 31 | */ 32 | addItem(name: string, item: InputToolbarRegistry.IToolbarItem): void; 33 | 34 | /** 35 | * Hide an element. 36 | */ 37 | hide(name: string): void; 38 | 39 | /** 40 | * Show an element. 41 | */ 42 | show(name: string): void; 43 | } 44 | 45 | /** 46 | * The toolbar registry implementation. 47 | */ 48 | export class InputToolbarRegistry implements IInputToolbarRegistry { 49 | /** 50 | * A signal emitting when the items has changed. 51 | */ 52 | get itemsChanged(): ISignal { 53 | return this._itemsChanged; 54 | } 55 | 56 | /** 57 | * Get a toolbar item. 58 | */ 59 | get(name: string): InputToolbarRegistry.IToolbarItem | undefined { 60 | return this._items.get(name); 61 | } 62 | 63 | /** 64 | * Get the list of the visible toolbar items in order. 65 | */ 66 | getItems(): InputToolbarRegistry.IToolbarItem[] { 67 | return Array.from(this._items.values()) 68 | .filter(item => !item.hidden) 69 | .sort((a, b) => a.position - b.position); 70 | } 71 | 72 | /** 73 | * Add a toolbar item. 74 | */ 75 | addItem(name: string, item: InputToolbarRegistry.IToolbarItem): void { 76 | if (!this._items.has(name)) { 77 | this._items.set(name, item); 78 | this._itemsChanged.emit(); 79 | } else { 80 | console.warn(`A chat input toolbar item '${name}' is already registered`); 81 | } 82 | } 83 | 84 | /** 85 | * Hide an element. 86 | */ 87 | hide(name: string): void { 88 | const item = this._items.get(name); 89 | if (item) { 90 | item.hidden = true; 91 | this._itemsChanged.emit(); 92 | } 93 | } 94 | 95 | /** 96 | * Show an element. 97 | */ 98 | show(name: string): void { 99 | const item = this._items.get(name); 100 | if (item) { 101 | item.hidden = false; 102 | this._itemsChanged.emit(); 103 | } 104 | } 105 | 106 | private _items = new Map(); 107 | private _itemsChanged = new Signal(this); 108 | } 109 | 110 | export namespace InputToolbarRegistry { 111 | /** 112 | * The toolbar item interface. 113 | */ 114 | export interface IToolbarItem { 115 | /** 116 | * The react functional component with the button. 117 | * 118 | * NOTE: 119 | * This component must be a TooltippedButton for a good integration in the toolbar. 120 | */ 121 | element: React.FunctionComponent; 122 | /** 123 | * The position of the button in the toolbar. 124 | */ 125 | position: number; 126 | /** 127 | * Whether the button is hidden or not. 128 | */ 129 | hidden?: boolean; 130 | } 131 | 132 | /** 133 | * The toolbar item properties, send to the button. 134 | */ 135 | export interface IToolbarItemProps { 136 | /** 137 | * The input model of the input component including the button. 138 | */ 139 | model: IInputModel; 140 | } 141 | 142 | /** 143 | * The default toolbar registry if none is provided. 144 | */ 145 | export function defaultToolbarRegistry(): InputToolbarRegistry { 146 | const registry = new InputToolbarRegistry(); 147 | 148 | registry.addItem('send', { 149 | element: SendButton, 150 | position: 100 151 | }); 152 | registry.addItem('attach', { 153 | element: AttachButton, 154 | position: 20 155 | }); 156 | registry.addItem('cancel', { 157 | element: CancelButton, 158 | position: 10 159 | }); 160 | return registry; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/jl-theme-provider.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import React, { useState, useEffect } from 'react'; 7 | import type { IThemeManager } from '@jupyterlab/apputils'; 8 | import { Theme, ThemeProvider, createTheme } from '@mui/material/styles'; 9 | 10 | import { getJupyterLabTheme } from '../theme-provider'; 11 | 12 | export function JlThemeProvider(props: { 13 | themeManager: IThemeManager | null; 14 | children: React.ReactNode; 15 | }): JSX.Element { 16 | const [theme, setTheme] = useState(createTheme()); 17 | 18 | useEffect(() => { 19 | async function setJlTheme() { 20 | setTheme(await getJupyterLabTheme()); 21 | } 22 | 23 | setJlTheme(); 24 | props.themeManager?.themeChanged.connect(setJlTheme); 25 | }, []); 26 | 27 | return {props.children}; 28 | } 29 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/messages/footer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { Box } from '@mui/material'; 7 | import React from 'react'; 8 | import { 9 | IMessageFooterRegistry, 10 | MessageFooterSectionProps 11 | } from '../../footers'; 12 | 13 | /** 14 | * The chat footer component properties. 15 | */ 16 | export interface IMessageFootersProps extends MessageFooterSectionProps { 17 | /** 18 | * The chat footer registry. 19 | */ 20 | registry: IMessageFooterRegistry; 21 | } 22 | 23 | /** 24 | * The chat footer component, which displays footer components on a row according to 25 | * their respective positions. 26 | */ 27 | export function MessageFooter(props: IMessageFootersProps): JSX.Element { 28 | const { message, model, registry } = props; 29 | const footer = registry.getFooter(); 30 | 31 | return ( 32 | 33 | {footer.left?.component ? ( 34 | 35 | ) : ( 36 |
37 | )} 38 | {footer.center?.component ? ( 39 | 40 | ) : ( 41 |
42 | )} 43 | {footer.right?.component ? ( 44 | 45 | ) : ( 46 |
47 | )} 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/mui-extras/contrasting-tooltip.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import React from 'react'; 7 | import { styled, Tooltip, TooltipProps, tooltipClasses } from '@mui/material'; 8 | 9 | /** 10 | * A restyled MUI tooltip component that is dark by default to improve contrast 11 | * against JupyterLab's default light theme. TODO: support dark themes. 12 | */ 13 | export const ContrastingTooltip = styled( 14 | ({ className, ...props }: TooltipProps) => ( 15 | 16 | ) 17 | )(({ theme }) => ({ 18 | [`& .${tooltipClasses.tooltip}`]: { 19 | backgroundColor: theme.palette.common.black, 20 | color: theme.palette.common.white, 21 | boxShadow: theme.shadows[1], 22 | fontSize: 11 23 | }, 24 | [`& .${tooltipClasses.arrow}`]: { 25 | color: theme.palette.common.black 26 | } 27 | })); 28 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/mui-extras/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export * from './contrasting-tooltip'; 7 | export * from './tooltipped-button'; 8 | export * from './tooltipped-icon-button'; 9 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/mui-extras/tooltipped-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { Button, ButtonProps, SxProps, TooltipProps } from '@mui/material'; 7 | import React from 'react'; 8 | 9 | import { ContrastingTooltip } from './contrasting-tooltip'; 10 | 11 | const TOOLTIPPED_WRAP_CLASS = 'jp-chat-tooltipped-wrap'; 12 | 13 | export type TooltippedButtonProps = { 14 | onClick: React.MouseEventHandler; 15 | tooltip: string; 16 | children: JSX.Element; 17 | disabled?: boolean; 18 | placement?: TooltipProps['placement']; 19 | /** 20 | * The offset of the tooltip popup. 21 | * 22 | * The expected syntax is defined by the Popper library: 23 | * https://popper.js.org/docs/v2/modifiers/offset/ 24 | */ 25 | offset?: [number, number]; 26 | 'aria-label'?: string; 27 | /** 28 | * Props passed directly to the MUI `Button` component. 29 | */ 30 | buttonProps?: ButtonProps; 31 | /** 32 | * Styles applied to the MUI `Button` component. 33 | */ 34 | sx?: SxProps; 35 | }; 36 | 37 | /** 38 | * A component that renders an MUI `Button` with a high-contrast tooltip 39 | * provided by `ContrastingTooltip`. This component differs from the MUI 40 | * defaults in the following ways: 41 | * 42 | * - Shows the tooltip on hover even if disabled. 43 | * - Renders the tooltip above the button by default. 44 | * - Renders the tooltip closer to the button by default. 45 | * - Lowers the opacity of the Button when disabled. 46 | * - Renders the Button with `line-height: 0` to avoid showing extra 47 | * vertical space in SVG icons. 48 | * 49 | * NOTE TO DEVS: Please keep this component's features synchronized with 50 | * features available to `TooltippedIconButton`. 51 | */ 52 | export function TooltippedButton(props: TooltippedButtonProps): JSX.Element { 53 | return ( 54 | 70 | {/* 71 | By default, tooltips never appear when the Button is disabled. The 72 | official way to support this feature in MUI is to wrap the child Button 73 | element in a `span` element. 74 | 75 | See: https://mui.com/material-ui/react-tooltip/#disabled-elements 76 | */} 77 | 78 | 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/mui-extras/tooltipped-icon-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { classes } from '@jupyterlab/ui-components'; 7 | import { IconButton, IconButtonProps, TooltipProps } from '@mui/material'; 8 | import React from 'react'; 9 | 10 | import { ContrastingTooltip } from './contrasting-tooltip'; 11 | 12 | const TOOLTIPPED_WRAP_CLASS = 'jp-chat-tooltipped-wrap'; 13 | 14 | export type TooltippedIconButtonProps = { 15 | onClick: () => unknown; 16 | tooltip: string; 17 | children: JSX.Element; 18 | className?: string; 19 | disabled?: boolean; 20 | placement?: TooltipProps['placement']; 21 | /** 22 | * The offset of the tooltip popup. 23 | * 24 | * The expected syntax is defined by the Popper library: 25 | * https://popper.js.org/docs/v2/modifiers/offset/ 26 | */ 27 | offset?: [number, number]; 28 | 'aria-label'?: string; 29 | /** 30 | * Props passed directly to the MUI `IconButton` component. 31 | */ 32 | iconButtonProps?: IconButtonProps; 33 | }; 34 | 35 | /** 36 | * A component that renders an MUI `IconButton` with a high-contrast tooltip 37 | * provided by `ContrastingTooltip`. This component differs from the MUI 38 | * defaults in the following ways: 39 | * 40 | * - Shows the tooltip on hover even if disabled. 41 | * - Renders the tooltip above the button by default. 42 | * - Renders the tooltip closer to the button by default. 43 | * - Lowers the opacity of the IconButton when disabled. 44 | * - Renders the IconButton with `line-height: 0` to avoid showing extra 45 | * vertical space in SVG icons. 46 | */ 47 | export function TooltippedIconButton( 48 | props: TooltippedIconButtonProps 49 | ): JSX.Element { 50 | return ( 51 | 67 | {/* 68 | By default, tooltips never appear when the IconButton is disabled. The 69 | official way to support this feature in MUI is to wrap the child Button 70 | element in a `span` element. 71 | 72 | See: https://mui.com/material-ui/react-tooltip/#disabled-elements 73 | */} 74 | 75 | 86 | {props.children} 87 | 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/scroll-container.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import React, { useMemo } from 'react'; 7 | import { Box, SxProps, Theme } from '@mui/material'; 8 | 9 | type ScrollContainerProps = { 10 | children: React.ReactNode; 11 | sx?: SxProps; 12 | }; 13 | 14 | /** 15 | * Component that handles intelligent scrolling. 16 | * 17 | * - If viewport is at the bottom of the overflow container, appending new 18 | * children keeps the viewport on the bottom of the overflow container. 19 | * 20 | * - If viewport is in the middle of the overflow container, appending new 21 | * children leaves the viewport unaffected. 22 | * 23 | * Currently only works for Chrome and Firefox due to reliance on 24 | * `overflow-anchor`. 25 | * 26 | * **References** 27 | * - https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/ 28 | */ 29 | export function ScrollContainer(props: ScrollContainerProps): JSX.Element { 30 | const id = useMemo( 31 | () => 'jupyter-chat-scroll-container-' + Date.now().toString(), 32 | [] 33 | ); 34 | 35 | return ( 36 | 46 | {props.children} 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/components/toolbar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { 7 | ToolbarButtonComponent, 8 | deleteIcon, 9 | editIcon 10 | } from '@jupyterlab/ui-components'; 11 | import React from 'react'; 12 | 13 | const TOOLBAR_CLASS = 'jp-chat-toolbar'; 14 | 15 | /** 16 | * The toolbar attached to a message. 17 | */ 18 | export function MessageToolbar(props: MessageToolbar.IProps): JSX.Element { 19 | const buttons: JSX.Element[] = []; 20 | 21 | if (props.edit !== undefined) { 22 | const editButton = ToolbarButtonComponent({ 23 | icon: editIcon, 24 | onClick: props.edit, 25 | tooltip: 'Edit' 26 | }); 27 | buttons.push(editButton); 28 | } 29 | if (props.delete !== undefined) { 30 | const deleteButton = ToolbarButtonComponent({ 31 | icon: deleteIcon, 32 | onClick: props.delete, 33 | tooltip: 'Delete' 34 | }); 35 | buttons.push(deleteButton); 36 | } 37 | 38 | return ( 39 |
40 | {buttons.map(toolbarButton => toolbarButton)} 41 |
42 | ); 43 | } 44 | 45 | export namespace MessageToolbar { 46 | export interface IProps { 47 | edit?: () => void; 48 | delete?: () => void; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/context.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | import { createContext } from 'react'; 6 | import { IAttachmentOpenerRegistry } from './registry'; 7 | 8 | export const AttachmentOpenerContext = createContext< 9 | IAttachmentOpenerRegistry | undefined 10 | >(undefined); 11 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/footers/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export * from './registry'; 7 | export * from './types'; 8 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/footers/registry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { Token } from '@lumino/coreutils'; 7 | import { MessageFooter, MessageFooterSection } from './types'; 8 | 9 | /** 10 | * The interface of a registry to provide chat footer. 11 | */ 12 | export interface IMessageFooterRegistry { 13 | /** 14 | * Get the message footer. 15 | */ 16 | getFooter(): MessageFooter; 17 | /** 18 | * Add a message footer section. 19 | * If multiple labextensions add a section in the same region, only 20 | * the last one will be displayed. 21 | */ 22 | addSection(section: MessageFooterSection): void; 23 | } 24 | 25 | /** 26 | * The default implementation of the message footer registry. 27 | */ 28 | export class MessageFooterRegistry implements IMessageFooterRegistry { 29 | /** 30 | * Get the footer from the registry. 31 | */ 32 | getFooter(): MessageFooter { 33 | return this._footers; 34 | } 35 | 36 | /** 37 | * Add a message footer. 38 | * If several extension add footers, only the last one will be displayed. 39 | */ 40 | addSection(footer: MessageFooterSection): void { 41 | this._footers[footer.position] = footer; 42 | } 43 | 44 | private _footers: MessageFooter = {}; 45 | } 46 | 47 | /** 48 | * The token providing the chat footer registry. 49 | */ 50 | export const IMessageFooterRegistry = new Token( 51 | '@jupyter/chat:ChatFooterRegistry' 52 | ); 53 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/footers/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { IChatModel } from '../model'; 7 | import { IChatMessage } from '../types'; 8 | 9 | /** 10 | * The props sent passed to each `MessageFooterSection` React component. 11 | */ 12 | export type MessageFooterSectionProps = { 13 | model: IChatModel; 14 | message: IChatMessage; 15 | }; 16 | 17 | /** 18 | * A message footer section which can be added to the footer registry. 19 | */ 20 | export type MessageFooterSection = { 21 | component: React.FC; 22 | position: 'left' | 'center' | 'right'; 23 | }; 24 | 25 | /** 26 | * The message footer returned by the registry, composed of 'left', 'center', 27 | * and 'right' sections. 28 | */ 29 | export type MessageFooter = { 30 | left?: MessageFooterSection; 31 | center?: MessageFooterSection; 32 | right?: MessageFooterSection; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/icons.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | // This file is based on iconimports.ts in @jupyterlab/ui-components, but is manually generated. 7 | 8 | import { LabIcon } from '@jupyterlab/ui-components'; 9 | 10 | import chatSvgStr from '../style/icons/chat.svg'; 11 | import includeSelectionIconStr from '../style/icons/include-selection.svg'; 12 | import readSvgStr from '../style/icons/read.svg'; 13 | import replaceCellSvg from '../style/icons/replace-cell.svg'; 14 | 15 | export const chatIcon = new LabIcon({ 16 | name: 'jupyter-chat::chat', 17 | svgstr: chatSvgStr 18 | }); 19 | 20 | export const readIcon = new LabIcon({ 21 | name: 'jupyter-chat::read', 22 | svgstr: readSvgStr 23 | }); 24 | 25 | export const replaceCellIcon = new LabIcon({ 26 | name: 'jupyter-ai::replace-cell', 27 | svgstr: replaceCellSvg 28 | }); 29 | 30 | export const includeSelectionIcon = new LabIcon({ 31 | name: 'jupyter-chat::include', 32 | svgstr: includeSelectionIconStr 33 | }); 34 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export * from './active-cell-manager'; 7 | export * from './chat-commands'; 8 | export * from './components'; 9 | export * from './footers'; 10 | export * from './icons'; 11 | export * from './input-model'; 12 | export * from './model'; 13 | export * from './registry'; 14 | export * from './selection-watcher'; 15 | export * from './types'; 16 | export * from './widgets/chat-error'; 17 | export * from './widgets/chat-sidebar'; 18 | export * from './widgets/chat-widget'; 19 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/registry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | import { Token } from '@lumino/coreutils'; 6 | import { IAttachment } from './types'; 7 | 8 | /** 9 | * The token for the attachments opener registry, which can be provided by an extension 10 | * using @jupyter/chat package. 11 | */ 12 | export const IAttachmentOpenerRegistry = new Token( 13 | '@jupyter/chat:IAttachmentOpenerRegistry' 14 | ); 15 | 16 | /** 17 | * The interface of a registry to provide attachments opener. 18 | */ 19 | export interface IAttachmentOpenerRegistry { 20 | /** 21 | * Get the function opening an attachment for a given type. 22 | */ 23 | get(type: string): ((attachment: IAttachment) => void) | undefined; 24 | /** 25 | * Register a function to open an attachment type. 26 | */ 27 | set(type: string, opener: (attachment: IAttachment) => void): void; 28 | } 29 | 30 | /** 31 | * The default registry, a Map object. 32 | */ 33 | export class AttachmentOpenerRegistry 34 | extends Map void> 35 | implements IAttachmentOpenerRegistry {} 36 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/theme-provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { Theme, createTheme } from '@mui/material/styles'; 7 | 8 | function getCSSVariable(name: string): string { 9 | return getComputedStyle(document.body).getPropertyValue(name).trim(); 10 | } 11 | 12 | export async function pollUntilReady(): Promise { 13 | while (!document.body.hasAttribute('data-jp-theme-light')) { 14 | await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms 15 | } 16 | } 17 | 18 | export async function getJupyterLabTheme(): Promise { 19 | await pollUntilReady(); 20 | const light = document.body.getAttribute('data-jp-theme-light'); 21 | return createTheme({ 22 | spacing: 4, 23 | components: { 24 | MuiButton: { 25 | defaultProps: { 26 | size: 'small' 27 | } 28 | }, 29 | MuiFilledInput: { 30 | defaultProps: { 31 | margin: 'dense' 32 | } 33 | }, 34 | MuiFormControl: { 35 | defaultProps: { 36 | margin: 'dense', 37 | size: 'small' 38 | } 39 | }, 40 | MuiFormHelperText: { 41 | defaultProps: { 42 | margin: 'dense' 43 | } 44 | }, 45 | MuiIconButton: { 46 | defaultProps: { 47 | size: 'small' 48 | } 49 | }, 50 | MuiInputBase: { 51 | defaultProps: { 52 | margin: 'dense', 53 | size: 'small' 54 | } 55 | }, 56 | MuiInputLabel: { 57 | defaultProps: { 58 | margin: 'dense' 59 | } 60 | }, 61 | MuiListItem: { 62 | defaultProps: { 63 | dense: true 64 | } 65 | }, 66 | MuiOutlinedInput: { 67 | defaultProps: { 68 | margin: 'dense' 69 | } 70 | }, 71 | MuiFab: { 72 | defaultProps: { 73 | size: 'small' 74 | } 75 | }, 76 | MuiTable: { 77 | defaultProps: { 78 | size: 'small' 79 | } 80 | }, 81 | MuiTextField: { 82 | defaultProps: { 83 | margin: 'dense', 84 | size: 'small' 85 | } 86 | }, 87 | MuiToolbar: { 88 | defaultProps: { 89 | variant: 'dense' 90 | } 91 | } 92 | }, 93 | palette: { 94 | background: { 95 | paper: getCSSVariable('--jp-layout-color1'), 96 | default: getCSSVariable('--jp-layout-color1') 97 | }, 98 | mode: light === 'true' ? 'light' : 'dark', 99 | primary: { 100 | main: getCSSVariable('--jp-brand-color1'), 101 | light: getCSSVariable('--jp-brand-color2'), 102 | dark: getCSSVariable('--jp-brand-color0') 103 | }, 104 | error: { 105 | main: getCSSVariable('--jp-error-color1'), 106 | light: getCSSVariable('--jp-error-color2'), 107 | dark: getCSSVariable('--jp-error-color0') 108 | }, 109 | warning: { 110 | main: getCSSVariable('--jp-warn-color1'), 111 | light: getCSSVariable('--jp-warn-color2'), 112 | dark: getCSSVariable('--jp-warn-color0') 113 | }, 114 | success: { 115 | main: getCSSVariable('--jp-success-color1'), 116 | light: getCSSVariable('--jp-success-color2'), 117 | dark: getCSSVariable('--jp-success-color0') 118 | }, 119 | text: { 120 | primary: getCSSVariable('--jp-ui-font-color1'), 121 | secondary: getCSSVariable('--jp-ui-font-color2'), 122 | disabled: getCSSVariable('--jp-ui-font-color3') 123 | } 124 | }, 125 | shape: { 126 | borderRadius: 2 127 | }, 128 | typography: { 129 | fontFamily: getCSSVariable('--jp-ui-font-family'), 130 | fontSize: 12, 131 | htmlFontSize: 16, 132 | button: { 133 | textTransform: 'capitalize' 134 | } 135 | } 136 | }); 137 | } 138 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /** 7 | * The user description. 8 | */ 9 | export interface IUser { 10 | username: string; 11 | name?: string; 12 | display_name?: string; 13 | initials?: string; 14 | color?: string; 15 | avatar_url?: string; 16 | /** 17 | * The string to use to mention a user in the chat. 18 | */ 19 | mention_name?: string; 20 | /** 21 | * Boolean identifying if user is a bot. 22 | */ 23 | bot?: boolean; 24 | } 25 | 26 | /** 27 | * The configuration interface. 28 | */ 29 | export interface IConfig { 30 | /** 31 | * Whether to send a message via Shift-Enter instead of Enter. 32 | */ 33 | sendWithShiftEnter?: boolean; 34 | /** 35 | * Whether to stack consecutive messages from same user. 36 | */ 37 | stackMessages?: boolean; 38 | /** 39 | * Whether to enable or not the notifications on unread messages. 40 | */ 41 | unreadNotifications?: boolean; 42 | /** 43 | * Whether to enable or not the code toolbar. 44 | */ 45 | enableCodeToolbar?: boolean; 46 | /** 47 | * Whether to send typing notification. 48 | */ 49 | sendTypingNotification?: boolean; 50 | } 51 | 52 | /** 53 | * The chat message description. 54 | */ 55 | export interface IChatMessage { 56 | type: 'msg'; 57 | body: string; 58 | id: string; 59 | time: number; 60 | sender: T; 61 | attachments?: U[]; 62 | mentions?: T[]; 63 | raw_time?: boolean; 64 | deleted?: boolean; 65 | edited?: boolean; 66 | stacked?: boolean; 67 | } 68 | 69 | /** 70 | * The chat history interface. 71 | */ 72 | export interface IChatHistory { 73 | messages: IChatMessage[]; 74 | } 75 | 76 | /** 77 | * The content of a new message. 78 | */ 79 | export interface INewMessage { 80 | body: string; 81 | id?: string; 82 | } 83 | 84 | /** 85 | * The attachment interface. 86 | */ 87 | export interface IAttachment { 88 | /** 89 | * The type of the attachment (basically 'file', 'variable', 'image') 90 | */ 91 | type: string; 92 | /** 93 | * The value, i.e. the file path, the variable name or image content. 94 | */ 95 | value: string; 96 | /** 97 | * The mimetype of the attachment, optional. 98 | */ 99 | mimetype?: string; 100 | } 101 | 102 | /** 103 | * An empty interface to describe optional settings that could be fetched from server. 104 | */ 105 | export interface ISettings {} /* eslint-disable-line @typescript-eslint/no-empty-object-type */ 106 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/types/mui.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { ReactEventHandler } from 'react'; 7 | 8 | /** 9 | * Workaround for https://github.com/mui/material-ui/issues/35287. 10 | */ 11 | declare global { 12 | namespace React { 13 | interface DOMAttributes { 14 | onResize?: ReactEventHandler | undefined; 15 | onResizeCapture?: ReactEventHandler | undefined; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/types/svg.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | // Excerpted from @jupyterlab/ui-components 7 | 8 | // including this file in a package allows for the use of import statements 9 | // with svg files. Example: `import xSvg from 'path/xSvg.svg'` 10 | 11 | // for use with raw-loader in Webpack. 12 | // The svg will be imported as a raw string 13 | 14 | declare module '*.svg' { 15 | const value: string; // @ts-ignore 16 | export default value; 17 | } 18 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { CodeEditor } from '@jupyterlab/codeeditor'; 7 | import { CodeMirrorEditor } from '@jupyterlab/codemirror'; 8 | import { DocumentWidget } from '@jupyterlab/docregistry'; 9 | import { FileEditor } from '@jupyterlab/fileeditor'; 10 | import { Notebook } from '@jupyterlab/notebook'; 11 | import { Widget } from '@lumino/widgets'; 12 | 13 | import { IUser } from './types'; 14 | 15 | const MENTION_CLASS = 'jp-chat-mention'; 16 | 17 | /** 18 | * Gets the editor instance used by a document widget. Returns `null` if unable. 19 | */ 20 | export function getEditor( 21 | widget: Widget | null 22 | ): CodeMirrorEditor | null | undefined { 23 | if (!(widget instanceof DocumentWidget)) { 24 | return null; 25 | } 26 | 27 | let editor: CodeEditor.IEditor | null | undefined; 28 | const { content } = widget; 29 | 30 | if (content instanceof FileEditor) { 31 | editor = content.editor; 32 | } else if (content instanceof Notebook) { 33 | editor = content.activeCell?.editor; 34 | } 35 | 36 | if (!(editor instanceof CodeMirrorEditor)) { 37 | return undefined; 38 | } 39 | 40 | return editor; 41 | } 42 | 43 | /** 44 | * Gets the index of the cell associated with `cellId`. 45 | */ 46 | export function getCellIndex(notebook: Notebook, cellId: string): number { 47 | const idx = notebook.model?.sharedModel.cells.findIndex( 48 | cell => cell.getId() === cellId 49 | ); 50 | return idx === undefined ? -1 : idx; 51 | } 52 | 53 | /** 54 | * Replace a mention to user (@someone) to a span, for markdown renderer. 55 | * 56 | * @param content - the content to update. 57 | * @param user - the user mentioned. 58 | */ 59 | export function replaceMentionToSpan(content: string, user: IUser): string { 60 | if (!user.mention_name) { 61 | return content; 62 | } 63 | const regex = new RegExp(user.mention_name, 'g'); 64 | const mention = `${user.mention_name}`; 65 | return content.replace(regex, mention); 66 | } 67 | 68 | /** 69 | * Replace a span to a mentioned to user string (@someone). 70 | * 71 | * @param content - the content to update. 72 | * @param user - the user mentioned. 73 | */ 74 | export function replaceSpanToMention(content: string, user: IUser): string { 75 | if (!user.mention_name) { 76 | return content; 77 | } 78 | const span = `${user.mention_name}`; 79 | const regex = new RegExp(span, 'g'); 80 | return content.replace(regex, user.mention_name); 81 | } 82 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/widgets/chat-error.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { IThemeManager, ReactWidget } from '@jupyterlab/apputils'; 7 | import { Alert, Box } from '@mui/material'; 8 | import React from 'react'; 9 | 10 | import { JlThemeProvider } from '../components/jl-theme-provider'; 11 | import { chatIcon } from '../icons'; 12 | 13 | export function buildErrorWidget( 14 | themeManager: IThemeManager | null 15 | ): ReactWidget { 16 | const ErrorWidget = ReactWidget.create( 17 | 18 | 28 | 29 | 30 | There seems to be a problem with the Chat backend, please look at 31 | the JupyterLab server logs or contact your administrator to correct 32 | this problem. 33 | 34 | 35 | 36 | 37 | ); 38 | ErrorWidget.id = 'jupyter-chat::chat'; 39 | ErrorWidget.title.icon = chatIcon; 40 | ErrorWidget.title.caption = 'Jupyter Chat'; // TODO: i18n 41 | 42 | return ErrorWidget; 43 | } 44 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/widgets/chat-sidebar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { ReactWidget } from '@jupyterlab/apputils'; 7 | import React from 'react'; 8 | 9 | import { Chat } from '../components/chat'; 10 | import { chatIcon } from '../icons'; 11 | 12 | export function buildChatSidebar(options: Chat.IOptions): ReactWidget { 13 | const ChatWidget = ReactWidget.create(); 14 | ChatWidget.id = 'jupyter-chat::side-panel'; 15 | ChatWidget.title.icon = chatIcon; 16 | ChatWidget.title.caption = 'Jupyter Chat'; // TODO: i18n 17 | return ChatWidget; 18 | } 19 | -------------------------------------------------------------------------------- /packages/jupyter-chat/src/widgets/chat-widget.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { ReactWidget } from '@jupyterlab/apputils'; 7 | import React from 'react'; 8 | 9 | import { Chat, IInputToolbarRegistry } from '../components'; 10 | import { chatIcon } from '../icons'; 11 | import { IChatModel } from '../model'; 12 | 13 | export class ChatWidget extends ReactWidget { 14 | constructor(options: Chat.IOptions) { 15 | super(); 16 | 17 | this.title.icon = chatIcon; 18 | this.title.caption = 'Jupyter Chat'; // TODO: i18n 19 | 20 | this._chatOptions = options; 21 | this.id = `jupyter-chat::widget::${options.model.name}`; 22 | this.node.onclick = () => this.model.input.focus(); 23 | } 24 | 25 | /** 26 | * Get the model of the widget. 27 | */ 28 | get model(): IChatModel { 29 | return this._chatOptions.model; 30 | } 31 | 32 | /** 33 | * Get the input toolbar registry (if it has been provided when creating the widget). 34 | */ 35 | get inputToolbarRegistry(): IInputToolbarRegistry | undefined { 36 | return this._chatOptions.inputToolbarRegistry; 37 | } 38 | 39 | render() { 40 | // The model need to be passed, otherwise it is undefined in the widget in 41 | // the case of collaborative document. 42 | return ; 43 | } 44 | 45 | private _chatOptions: Chat.IOptions; 46 | } 47 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /* 7 | See the JupyterLab Developer Guide for useful CSS Patterns: 8 | 9 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 10 | */ 11 | 12 | @import url('./chat.css'); 13 | @import url('./chat-settings.css'); 14 | @import url('./input.css'); 15 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/chat-settings.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | .jp-chat-SettingsHeader { 7 | font-size: var(--jp-ui-font-size3); 8 | font-weight: 400; 9 | color: var(--jp-ui-font-color1); 10 | } 11 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/chat.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | .jp-chat-message:not(.jp-chat-message-stacked) { 6 | padding: 1em 1em 0; 7 | } 8 | 9 | .jp-chat-message:not(:first-child, .jp-chat-message-stacked) { 10 | border-top: 1px solid var(--jp-border-color2); 11 | } 12 | 13 | .jp-chat-message.jp-chat-message-stacked { 14 | padding: 0 1em; 15 | } 16 | 17 | .jp-chat-rendered-markdown { 18 | position: relative; 19 | } 20 | 21 | /* 22 | * 23 | * Selectors must be nested in `.jp-ThemedContainer` to have a higher 24 | * specificity than selectors in rules provided by JupyterLab. 25 | * 26 | * See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling 27 | * See also: https://github.com/jupyterlab/jupyter-ai/issues/1090 28 | */ 29 | .jp-ThemedContainer .jp-chat-rendered-markdown .jp-RenderedHTMLCommon { 30 | padding-right: 0; 31 | } 32 | 33 | .jp-ThemedContainer .jp-chat-rendered-markdown pre { 34 | background-color: var(--jp-cell-editor-background); 35 | overflow-x: auto; 36 | white-space: pre; 37 | margin: 0; 38 | padding: 4px 2px 0 6px; 39 | border: var(--jp-border-width) solid var(--jp-cell-editor-border-color); 40 | } 41 | 42 | .jp-ThemedContainer .jp-chat-rendered-markdown pre > code { 43 | background-color: inherit; 44 | overflow-x: inherit; 45 | white-space: inherit; 46 | } 47 | 48 | .jp-ThemedContainer .jp-chat-rendered-markdown mjx-container { 49 | font-size: 119%; 50 | } 51 | 52 | .jp-chat-toolbar { 53 | display: none; 54 | position: absolute; 55 | right: 2px; 56 | top: 2px; 57 | font-size: var(--jp-ui-font-size0); 58 | color: var(--jp-ui-font-color3); 59 | } 60 | 61 | .jp-chat-toolbar:hover { 62 | cursor: pointer; 63 | color: var(--jp-ui-font-color2); 64 | } 65 | 66 | .jp-chat-rendered-markdown:hover .jp-chat-toolbar { 67 | display: inherit; 68 | } 69 | 70 | .jp-chat-toolbar > .jp-ToolbarButtonComponent { 71 | margin-top: 0; 72 | } 73 | 74 | .jp-chat-writers { 75 | display: flex; 76 | flex-wrap: wrap; 77 | } 78 | 79 | .jp-chat-writers > div { 80 | display: flex; 81 | align-items: center; 82 | gap: 0.2em; 83 | white-space: pre; 84 | padding-left: 0.5em; 85 | } 86 | 87 | .jp-chat-navigation { 88 | position: absolute; 89 | right: 10px; 90 | width: 24px; 91 | height: 24px; 92 | border-radius: 50%; 93 | min-width: 0; 94 | } 95 | 96 | .jp-chat-navigation-unread { 97 | border: solid 2px var(--jp-cell-inprompt-font-color); 98 | } 99 | 100 | .jp-chat-navigation::part(control) { 101 | padding: 0; 102 | } 103 | 104 | .jp-chat-navigation-top { 105 | top: 10px; 106 | } 107 | 108 | .jp-chat-navigation-top svg { 109 | transform: rotate(180deg); 110 | } 111 | 112 | .jp-chat-navigation-bottom { 113 | bottom: 100px; 114 | } 115 | 116 | .jp-chat-attachments { 117 | display: flex; 118 | min-height: 1.5em; 119 | } 120 | 121 | .jp-chat-attachment { 122 | border: solid 1px; 123 | border-radius: 10px; 124 | margin: 0 0.2em; 125 | padding: 0 0.3em; 126 | align-content: center; 127 | background-color: var(--jp-border-color3); 128 | } 129 | 130 | .jp-chat-attachment .jp-chat-attachment-clickable:hover { 131 | cursor: pointer; 132 | } 133 | 134 | .jp-chat-command-name { 135 | font-weight: normal; 136 | margin: 5px; 137 | } 138 | 139 | .jp-chat-command-description { 140 | color: gray; 141 | margin: 5px; 142 | } 143 | 144 | .jp-chat-mention { 145 | border-radius: 10px; 146 | padding: 0 0.2em; 147 | background-color: var(--jp-brand-color4); 148 | } 149 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/icons/chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/icons/include-selection.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/icons/read.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/icons/replace-cell.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | @import url('base.css'); 7 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import './base.css'; 7 | -------------------------------------------------------------------------------- /packages/jupyter-chat/style/input.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | .jp-chat-input-toolbar { 7 | gap: 1px; 8 | } 9 | 10 | .jp-chat-input-toolbar .jp-chat-tooltipped-wrap button { 11 | border-radius: 0; 12 | min-width: unset; 13 | } 14 | 15 | .jp-chat-input-toolbar .jp-chat-tooltipped-wrap:first-child button { 16 | border-top-left-radius: 2px; 17 | border-bottom-left-radius: 2px; 18 | } 19 | 20 | .jp-chat-input-toolbar .jp-chat-tooltipped-wrap:last-child button { 21 | border-top-right-radius: 2px; 22 | border-bottom-right-radius: 2px; 23 | } 24 | 25 | .jp-chat-input-toolbar .jp-chat-attach-button, 26 | .jp-chat-input-toolbar .jp-chat-cancel-button { 27 | padding: 4px; 28 | } 29 | 30 | .jp-chat-input-toolbar .jp-chat-send-include-opener { 31 | padding: 4px 0; 32 | } 33 | -------------------------------------------------------------------------------- /packages/jupyter-chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/jupyter-chat/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab-chat-extension", 3 | "version": "0.11.0", 4 | "description": "A chat extension based on shared documents", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/jupyterlab/jupyter-chat", 11 | "bugs": { 12 | "url": "https://github.com/jupyterlab/jupyter-chat/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Jupyter Development Team", 17 | "email": "jupyter@googlegroups.com" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 22 | "src/**/*.{ts,tsx}", 23 | "schema/*.json" 24 | ], 25 | "main": "lib/index.js", 26 | "types": "lib/index.d.ts", 27 | "style": "style/index.css", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/jupyterlab/jupyter-chat.git" 31 | }, 32 | "scripts": { 33 | "build": "jlpm build:lib && jlpm build:labextension:dev", 34 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 35 | "build:labextension": "jupyter labextension build .", 36 | "build:labextension:dev": "jupyter labextension build --development True .", 37 | "build:lib": "tsc --sourceMap", 38 | "build:lib:prod": "tsc", 39 | "clean": "jlpm clean:lib", 40 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 41 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 42 | "clean:labextension": "rimraf jupyterlab_chat/labextension jupyterlab_chat/_version.py", 43 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 44 | "install:extension": "jlpm build", 45 | "watch": "run-p watch:src watch:labextension", 46 | "watch:src": "tsc -w --sourceMap", 47 | "watch:labextension": "jupyter labextension watch ." 48 | }, 49 | "dependencies": { 50 | "@jupyter-notebook/application": "^7.2.0", 51 | "@jupyter/collaborative-drive": "^3.0.0", 52 | "@jupyter/ydoc": "^2.0.0 || ^3.0.0", 53 | "@jupyterlab/application": "^4.2.0", 54 | "@jupyterlab/apputils": "^4.3.0", 55 | "@jupyterlab/coreutils": "^6.2.0", 56 | "@jupyterlab/docregistry": "^4.2.0", 57 | "@jupyterlab/filebrowser": "^4.2.0", 58 | "@jupyterlab/launcher": "^4.2.0", 59 | "@jupyterlab/notebook": "^4.2.0", 60 | "@jupyterlab/rendermime": "^4.2.0", 61 | "@jupyterlab/services": "^7.2.0", 62 | "@jupyterlab/settingregistry": "^4.2.0", 63 | "@jupyterlab/translation": "^4.2.0", 64 | "@jupyterlab/ui-components": "^4.2.0", 65 | "@lumino/commands": "^2.0.0", 66 | "@lumino/coreutils": "^2.0.0", 67 | "@lumino/signaling": "^2.0.0", 68 | "@lumino/widgets": "^2.0.0", 69 | "jupyterlab-chat": "^0.11.0", 70 | "react": "^18.2.0", 71 | "y-protocols": "^1.0.5", 72 | "yjs": "^13.5.40" 73 | }, 74 | "devDependencies": { 75 | "@jupyterlab/builder": "^4.2.0", 76 | "@types/json-schema": "^7.0.11", 77 | "@types/react": "^18.2.0", 78 | "@types/react-addons-linked-state-mixin": "^0.14.22", 79 | "css-loader": "^6.7.1", 80 | "mkdirp": "^1.0.3", 81 | "npm-run-all": "^4.1.5", 82 | "rimraf": "^5.0.1", 83 | "source-map-loader": "^1.0.2", 84 | "style-loader": "^3.3.1", 85 | "typescript": "~5.0.2", 86 | "yjs": "^13.5.0" 87 | }, 88 | "sideEffects": [ 89 | "style/*.css", 90 | "style/index.js" 91 | ], 92 | "styleModule": "style/index.js", 93 | "publishConfig": { 94 | "access": "public" 95 | }, 96 | "jupyterlab": { 97 | "discovery": { 98 | "server": { 99 | "managers": [ 100 | "pip" 101 | ], 102 | "base": { 103 | "name": "jupyterlab_chat" 104 | } 105 | } 106 | }, 107 | "extension": true, 108 | "outputDir": "../../python/jupyterlab-chat/jupyterlab_chat/labextension", 109 | "schemaDir": "schema", 110 | "sharedPackages": { 111 | "@jupyter/chat": { 112 | "bundled": true, 113 | "singleton": true 114 | }, 115 | "@jupyter/collaborative-drive": { 116 | "bundled": false, 117 | "singleton": true 118 | }, 119 | "jupyterlab-chat": { 120 | "bundled": true, 121 | "singleton": true 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/schema/chat-panel.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Jupyterlab chat", 3 | "description": "Configuration for the chat commands", 4 | "type": "object", 5 | "jupyter.lab.toolbars": { 6 | "Chat": [ 7 | { 8 | "name": "moveToSide", 9 | "command": "jupyterlab-chat:moveToSide" 10 | }, 11 | { 12 | "name": "markAsRead", 13 | "command": "jupyterlab-chat:markAsRead" 14 | } 15 | ] 16 | }, 17 | "properties": {}, 18 | "additionalProperties": false 19 | } 20 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/schema/commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Jupyterlab chat", 3 | "description": "Configuration for the chat commands", 4 | "type": "object", 5 | "jupyter.lab.menus": { 6 | "main": [ 7 | { 8 | "id": "jp-mainmenu-file", 9 | "items": [ 10 | { 11 | "type": "submenu", 12 | "submenu": { 13 | "id": "jp-mainmenu-file-new", 14 | "items": [ 15 | { 16 | "command": "jupyterlab-chat:create", 17 | "rank": 50 18 | } 19 | ] 20 | } 21 | } 22 | ] 23 | } 24 | ] 25 | }, 26 | "jupyter.lab.shortcuts": [ 27 | { 28 | "command": "jupyterlab-chat:focusInput", 29 | "keys": ["Accel Shift 1"], 30 | "selector": "body", 31 | "preventDefault": false 32 | } 33 | ], 34 | "properties": {}, 35 | "additionalProperties": false 36 | } 37 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/schema/factory.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Chat", 3 | "description": "Configuration for the chat widgets", 4 | "type": "object", 5 | "jupyter.lab.toolbars": { 6 | "Chat": [] 7 | }, 8 | "jupyter.lab.transform": true, 9 | "properties": { 10 | "sendWithShiftEnter": { 11 | "description": "Whether to send a message via Shift-Enter instead of Enter.", 12 | "type": "boolean", 13 | "default": false, 14 | "readOnly": false 15 | }, 16 | "stackMessages": { 17 | "description": "Whether to stack consecutive messages from same user.", 18 | "type": "boolean", 19 | "default": true, 20 | "readOnly": false 21 | }, 22 | "unreadNotifications": { 23 | "description": "Whether to enable or not the notifications on unread messages.", 24 | "type": "boolean", 25 | "default": true, 26 | "readOnly": false 27 | }, 28 | "enableCodeToolbar": { 29 | "description": "Whether to enable or not the code toolbar.", 30 | "type": "boolean", 31 | "default": true, 32 | "readOnly": false 33 | }, 34 | "sendTypingNotification": { 35 | "description": "Send typing notification.", 36 | "type": "boolean", 37 | "default": true, 38 | "readOnly": false 39 | }, 40 | "defaultDirectory": { 41 | "description": "Default directory where to create and look for chat, the jupyter root directory if empty.", 42 | "type": "string", 43 | "default": "", 44 | "readOnly": false 45 | }, 46 | "toolbar": { 47 | "title": "File browser toolbar items", 48 | "description": "Note: To disable a toolbar item,\ncopy it to User Preferences and add the\n\"disabled\" key. The following example will disable the uploader button:\n{\n \"toolbar\": [\n {\n \"name\": \"uploader\",\n \"disabled\": true\n }\n ]\n}\n\nToolbar description:", 49 | "items": { 50 | "$ref": "#/definitions/toolbarItem" 51 | }, 52 | "type": "array", 53 | "default": [] 54 | } 55 | }, 56 | "additionalProperties": false, 57 | "definitions": { 58 | "toolbarItem": { 59 | "properties": { 60 | "name": { 61 | "title": "Unique name", 62 | "type": "string" 63 | }, 64 | "args": { 65 | "title": "Command arguments", 66 | "type": "object" 67 | }, 68 | "command": { 69 | "title": "Command id", 70 | "type": "string", 71 | "default": "" 72 | }, 73 | "disabled": { 74 | "title": "Whether the item is ignored or not", 75 | "type": "boolean", 76 | "default": false 77 | }, 78 | "icon": { 79 | "title": "Item icon id", 80 | "description": "If defined, it will override the command icon", 81 | "type": "string" 82 | }, 83 | "label": { 84 | "title": "Item label", 85 | "description": "If defined, it will override the command label", 86 | "type": "string" 87 | }, 88 | "caption": { 89 | "title": "Item caption", 90 | "description": "If defined, it will override the command caption", 91 | "type": "string" 92 | }, 93 | "type": { 94 | "title": "Item type", 95 | "type": "string", 96 | "enum": ["command", "spacer"] 97 | }, 98 | "rank": { 99 | "title": "Item rank", 100 | "type": "number", 101 | "minimum": 0, 102 | "default": 50 103 | } 104 | }, 105 | "required": ["name"], 106 | "additionalProperties": false, 107 | "type": "object" 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/src/chat-commands/plugins.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { JupyterFrontEndPlugin } from '@jupyterlab/application'; 7 | 8 | import { IChatCommandRegistry, ChatCommandRegistry } from '@jupyter/chat'; 9 | 10 | export const chatCommandRegistryPlugin: JupyterFrontEndPlugin = 11 | { 12 | id: 'jupyterlab-chat-extension:chatCommandRegistry', 13 | description: 14 | 'The chat command registry used by the jupyterlab-chat-extension.', 15 | autoStart: true, 16 | provides: IChatCommandRegistry, 17 | activate: app => { 18 | return new ChatCommandRegistry(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/src/chat-commands/providers/emoji.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { JupyterFrontEndPlugin } from '@jupyterlab/application'; 7 | import { 8 | IChatCommandProvider, 9 | IChatCommandRegistry, 10 | ChatCommand, 11 | IInputModel 12 | } from '@jupyter/chat'; 13 | 14 | export class EmojiCommandProvider implements IChatCommandProvider { 15 | public id: string = 'jupyter-chat:emoji-commands'; 16 | private _slash_commands: ChatCommand[] = [ 17 | { 18 | name: ':heart:', 19 | replaceWith: '❤ ', 20 | providerId: this.id, 21 | description: 'Emoji', 22 | icon: '❤' 23 | }, 24 | { 25 | name: ':smile:', 26 | replaceWith: '🙂 ', 27 | providerId: this.id, 28 | description: 'Emoji', 29 | icon: '🙂' 30 | }, 31 | { 32 | name: ':thinking:', 33 | replaceWith: '🤔 ', 34 | providerId: this.id, 35 | description: 'Emoji', 36 | icon: '🤔' 37 | }, 38 | { 39 | name: ':cool:', 40 | replaceWith: '😎 ', 41 | providerId: this.id, 42 | description: 'Emoji', 43 | icon: '😎' 44 | } 45 | ]; 46 | 47 | // regex used to test the current word 48 | private _regex: RegExp = /^:\w*:?/; 49 | 50 | async getChatCommands(inputModel: IInputModel) { 51 | const match = inputModel.currentWord?.match(this._regex)?.[0]; 52 | if (!match) { 53 | return []; 54 | } 55 | 56 | const commands = this._slash_commands.filter(cmd => 57 | cmd.name.startsWith(match) 58 | ); 59 | return commands; 60 | } 61 | 62 | async handleChatCommand( 63 | command: ChatCommand, 64 | inputModel: IInputModel 65 | ): Promise { 66 | // no handling needed because `replaceWith` is set in each command. 67 | return; 68 | } 69 | } 70 | 71 | export const emojiCommandsPlugin: JupyterFrontEndPlugin = { 72 | id: 'jupyterlab-chat-extension:emojiCommandsPlugin', 73 | description: 'Plugin which adds emoji commands to the chat.', 74 | autoStart: true, 75 | requires: [IChatCommandRegistry], 76 | activate: (app, registry: IChatCommandRegistry) => { 77 | registry.addProvider(new EmojiCommandProvider()); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/src/chat-commands/providers/user-mention.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import React from 'react'; 7 | import { 8 | IChatCommandProvider, 9 | IChatCommandRegistry, 10 | ChatCommand, 11 | IInputModel, 12 | Avatar, 13 | IUser 14 | } from '@jupyter/chat'; 15 | import { JupyterFrontEndPlugin } from '@jupyterlab/application'; 16 | 17 | export const mentionCommandsPlugin: JupyterFrontEndPlugin = { 18 | id: 'jupyterlab-chat-extension:mentionCommandsPlugin', 19 | description: 'Plugin which adds user mention commands.', 20 | autoStart: true, 21 | requires: [IChatCommandRegistry], 22 | activate: (app, registry: IChatCommandRegistry) => { 23 | registry.addProvider(new MentionCommandProvider()); 24 | } 25 | }; 26 | 27 | class MentionCommandProvider implements IChatCommandProvider { 28 | public id: string = 'jupyter-chat:mention-commands'; 29 | 30 | // regex used to test the current word 31 | private _regex: RegExp = /^@[\w-]*:?/; 32 | 33 | async getChatCommands(inputModel: IInputModel) { 34 | this._users.clear(); 35 | const match = inputModel.currentWord?.match(this._regex)?.[0]; 36 | if (!match) { 37 | return []; 38 | } 39 | 40 | const users = inputModel.chatContext.users; 41 | users.forEach(user => { 42 | let mentionName = user.mention_name; 43 | if (!mentionName) { 44 | mentionName = Private.getMentionName(user); 45 | user.mention_name = mentionName; 46 | } 47 | 48 | this._users.set(mentionName, { 49 | user, 50 | icon: 51 | }); 52 | }); 53 | 54 | // Build the commands for each user. 55 | const commands: ChatCommand[] = Array.from(this._users) 56 | .sort() 57 | .filter(user => user[0].toLowerCase().startsWith(match.toLowerCase())) 58 | .map(user => { 59 | return { 60 | name: user[0], 61 | providerId: this.id, 62 | icon: user[1].icon 63 | }; 64 | }); 65 | return commands; 66 | } 67 | 68 | async handleChatCommand( 69 | command: ChatCommand, 70 | inputModel: IInputModel 71 | ): Promise { 72 | inputModel.replaceCurrentWord(`${command.name} `); 73 | if (this._users.has(command.name)) { 74 | inputModel.addMention?.(this._users.get(command.name)!.user); 75 | } 76 | } 77 | 78 | private _users = new Map(); 79 | } 80 | 81 | namespace Private { 82 | export type CommandUser = { 83 | user: IUser; 84 | icon: JSX.Element | null; 85 | }; 86 | 87 | /** 88 | * Build the mention name from a User object. 89 | */ 90 | export function getMentionName(user: IUser): string { 91 | const username = (user.display_name ?? user.name ?? user.username).replace( 92 | / /g, 93 | '-' 94 | ); 95 | return `@${username}`; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /* 7 | See the JupyterLab Developer Guide for useful CSS Patterns: 8 | 9 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 10 | */ 11 | 12 | @import url('~jupyterlab-chat/style/index.css'); 13 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/style/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | @import url('base.css'); 7 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/style/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import './base.css'; 7 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | module.exports = require('@jupyterlab/testing/lib/babel-config'); 7 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | const jestJupyterLab = require('@jupyterlab/testing/lib/jest-config'); 7 | 8 | const esModules = [ 9 | '@codemirror', 10 | '@jupyter/ydoc', 11 | '@jupyterlab/', 12 | 'lib0', 13 | 'nanoid', 14 | 'vscode-ws-jsonrpc', 15 | 'y-protocols', 16 | 'y-websocket', 17 | 'yjs' 18 | ].join('|'); 19 | 20 | const baseConfig = jestJupyterLab(__dirname); 21 | 22 | module.exports = { 23 | ...baseConfig, 24 | automock: false, 25 | collectCoverageFrom: [ 26 | 'src/**/*.{ts,tsx}', 27 | '!src/**/*.d.ts', 28 | '!src/**/.ipynb_checkpoints/*' 29 | ], 30 | coverageReporters: ['lcov', 'text'], 31 | testRegex: 'src/.*/.*.spec.ts[x]?$', 32 | transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] 33 | }; 34 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab-chat", 3 | "version": "0.11.0", 4 | "description": "The library to build a chat based on shared document", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/jupyterlab/jupyter-chat", 11 | "bugs": { 12 | "url": "https://github.com/jupyterlab/jupyter-chat/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Jupyter Development Team", 17 | "email": "jupyter@googlegroups.com" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 22 | "src/**/*.{ts,tsx}" 23 | ], 24 | "main": "lib/index.js", 25 | "types": "lib/index.d.ts", 26 | "style": "style/index.css", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/jupyterlab/jupyter-chat.git" 30 | }, 31 | "scripts": { 32 | "build": "jlpm build:lib", 33 | "build:prod": "jlpm clean && jlpm build:lib:prod", 34 | "build:lib": "tsc --sourceMap", 35 | "build:lib:prod": "tsc", 36 | "clean": "jlpm clean:lib", 37 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 38 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 39 | "clean:all": "jlpm clean:lib && jlpm clean:lintcache", 40 | "install:extension": "jlpm build", 41 | "test": "jest --coverage", 42 | "watch:src": "tsc -w --sourceMap" 43 | }, 44 | "dependencies": { 45 | "@jupyter/chat": "^0.11.0", 46 | "@jupyter/collaborative-drive": "^3.0.0", 47 | "@jupyter/ydoc": "^2.0.0 || ^3.0.0", 48 | "@jupyterlab/application": "^4.2.0", 49 | "@jupyterlab/apputils": "^4.3.0", 50 | "@jupyterlab/coreutils": "^6.2.0", 51 | "@jupyterlab/docmanager": "^4.2.0", 52 | "@jupyterlab/docregistry": "^4.2.0", 53 | "@jupyterlab/launcher": "^4.2.0", 54 | "@jupyterlab/notebook": "^4.2.0", 55 | "@jupyterlab/rendermime": "^4.2.0", 56 | "@jupyterlab/services": "^7.2.0", 57 | "@jupyterlab/settingregistry": "^4.2.0", 58 | "@jupyterlab/translation": "^4.2.0", 59 | "@jupyterlab/ui-components": "^4.2.0", 60 | "@lumino/commands": "^2.0.0", 61 | "@lumino/coreutils": "^2.0.0", 62 | "@lumino/signaling": "^2.0.0", 63 | "@lumino/widgets": "^2.0.0", 64 | "react": "^18.2.0", 65 | "y-protocols": "^1.0.5", 66 | "yjs": "^13.5.40" 67 | }, 68 | "devDependencies": { 69 | "@jupyterlab/testing": "^4.2.0", 70 | "@types/jest": "^29.2.0", 71 | "@types/json-schema": "^7.0.11", 72 | "@types/react": "^18.2.0", 73 | "@types/react-addons-linked-state-mixin": "^0.14.22", 74 | "css-loader": "^6.7.1", 75 | "jest": "^29.2.0", 76 | "mkdirp": "^1.0.3", 77 | "npm-run-all": "^4.1.5", 78 | "rimraf": "^5.0.1", 79 | "source-map-loader": "^1.0.2", 80 | "style-loader": "^3.3.1", 81 | "typescript": "~5.0.2", 82 | "yjs": "^13.5.0" 83 | }, 84 | "sideEffects": [ 85 | "style/*.css", 86 | "style/index.js" 87 | ], 88 | "styleModule": "style/index.js", 89 | "publishConfig": { 90 | "access": "public" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/src/__tests__/jupyterlab_collaborative_chat.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /** 7 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 8 | */ 9 | 10 | describe('jupyterlab-chat', () => { 11 | it('should be tested', () => { 12 | expect(1 + 1).toEqual(2); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | export * from './factory'; 7 | export * from './model'; 8 | export * from './token'; 9 | export * from './widget'; 10 | export * from './ychat'; 11 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/src/token.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { 7 | IConfig, 8 | chatIcon, 9 | IActiveCellManager, 10 | ISelectionWatcher, 11 | ChatWidget, 12 | IInputToolbarRegistry 13 | } from '@jupyter/chat'; 14 | import { WidgetTracker } from '@jupyterlab/apputils'; 15 | import { DocumentRegistry } from '@jupyterlab/docregistry'; 16 | import { Token } from '@lumino/coreutils'; 17 | import { ISignal } from '@lumino/signaling'; 18 | import { ChatPanel, LabChatPanel } from './widget'; 19 | 20 | /** 21 | * The file type for a chat document. 22 | */ 23 | export const chatFileType: DocumentRegistry.IFileType = { 24 | name: 'chat', 25 | displayName: 'Chat', 26 | mimeTypes: ['text/json', 'application/json'], 27 | extensions: ['.chat'], 28 | fileFormat: 'text', 29 | contentType: 'chat', 30 | icon: chatIcon 31 | }; 32 | 33 | /** 34 | * The token for the chat widget factory. 35 | */ 36 | export const IChatFactory = new Token( 37 | 'jupyterlab-chat:IChatFactory' 38 | ); 39 | 40 | /** 41 | * The chat configs. 42 | */ 43 | export interface ILabChatConfig extends IConfig { 44 | /** 45 | * The default directory where to create and look for chat. 46 | */ 47 | defaultDirectory?: string; 48 | } 49 | 50 | /** 51 | * The interface for the chat factory objects. 52 | */ 53 | export interface IChatFactory { 54 | /** 55 | * The chat widget config. 56 | */ 57 | widgetConfig: IWidgetConfig; 58 | /** 59 | * The chat panel tracker. 60 | */ 61 | tracker: WidgetTracker; 62 | } 63 | 64 | /** 65 | * The interface for the chats config. 66 | */ 67 | export interface IWidgetConfig { 68 | /** 69 | * The widget config 70 | */ 71 | config: Partial; 72 | 73 | /** 74 | * A signal emitting when the configuration for the chats has changed. 75 | */ 76 | configChanged: IConfigChanged; 77 | } 78 | 79 | /** 80 | * A signal emitting when the configuration for the chats has changed. 81 | */ 82 | export interface IConfigChanged /* eslint-disable-line @typescript-eslint/no-empty-object-type */ 83 | extends ISignal> {} 84 | 85 | /** 86 | * Command ids. 87 | */ 88 | export const CommandIDs = { 89 | /** 90 | * Create a chat file. 91 | */ 92 | createChat: 'jupyterlab-chat:create', 93 | /** 94 | * Open a chat file. 95 | */ 96 | openChat: 'jupyterlab-chat:open', 97 | /** 98 | * Move a main widget to the side panel. 99 | */ 100 | moveToSide: 'jupyterlab-chat:moveToSide', 101 | /** 102 | * Mark as read. 103 | */ 104 | markAsRead: 'jupyterlab-chat:markAsRead', 105 | /** 106 | * Focus the input of the current chat. 107 | */ 108 | focusInput: 'jupyterlab-chat:focusInput' 109 | }; 110 | 111 | /** 112 | * The chat panel token. 113 | */ 114 | export const IChatPanel = new Token('jupyterlab-chat:IChatPanel'); 115 | 116 | /** 117 | * The active cell manager plugin. 118 | */ 119 | export const IActiveCellManagerToken = new Token( 120 | 'jupyterlab-chat:IActiveCellManager' 121 | ); 122 | 123 | /** 124 | * The selection watcher plugin. 125 | */ 126 | export const ISelectionWatcherToken = new Token( 127 | 'jupyterlab-chat:ISelectionWatcher' 128 | ); 129 | 130 | /** 131 | * The input toolbar registry factory. 132 | */ 133 | export interface IInputToolbarRegistryFactory { 134 | /** 135 | * Create an input toolbar registry. 136 | */ 137 | create: () => IInputToolbarRegistry; 138 | } 139 | 140 | /** 141 | * The token of the factory to create an input toolbar registry. 142 | */ 143 | export const IInputToolbarRegistryFactory = 144 | new Token( 145 | 'jupyterlab-chat:IInputToolbarRegistryFactory' 146 | ); 147 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /* 7 | See the JupyterLab Developer Guide for useful CSS Patterns: 8 | 9 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 10 | */ 11 | 12 | @import url('~@jupyter/chat/style/index.css'); 13 | 14 | .jp-lab-chat-main-panel 15 | .jp-ToolbarButtonComponent[data-command='jupyterlab-chat:moveToSide'] 16 | svg { 17 | transform: rotate(180deg); 18 | } 19 | 20 | .jp-lab-chat-title-unread .lm-TabBar-tabLabel::before { 21 | content: '* '; 22 | } 23 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/style/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | @import url('base.css'); 7 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/style/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import './base.css'; 7 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/jupyterlab-chat/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | [build-system] 5 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "jupyter_chat_root" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | requires-python = ">=3.8" 13 | classifiers = [ 14 | "Framework :: Jupyter", 15 | "Framework :: Jupyter :: JupyterLab", 16 | "Framework :: Jupyter :: JupyterLab :: 4", 17 | "Framework :: Jupyter :: JupyterLab :: Extensions", 18 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 19 | "License :: OSI Approved :: BSD License", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | ] 28 | dynamic = ["version", "description", "authors", "urls", "keywords"] 29 | 30 | [tool.hatch.version] 31 | source = "nodejs" 32 | path = "package.json" 33 | 34 | [tool.hatch.metadata.hooks.nodejs] 35 | fields = ["description", "authors", "urls"] 36 | 37 | [tool.jupyter-releaser.options] 38 | version-cmd = "cd ../.. && python scripts/bump_version.py --force --skip-if-dirty" 39 | python_packages = [ 40 | "python/jupyterlab-chat" 41 | ] 42 | 43 | [tool.jupyter-releaser.hooks] 44 | before-build-npm = [ 45 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 46 | "YARN_ENABLE_IMMUTABLE_INSTALLS=0 jlpm", 47 | "jlpm build:prod" 48 | ] 49 | before-build-python = ["jlpm clean"] 50 | before-bump-version = [ 51 | "python -m pip install -U jupyterlab", 52 | "jlpm" 53 | ] 54 | 55 | [tool.check-wheel-contents] 56 | ignore = ["W002"] 57 | 58 | [tool.mypy] 59 | check_untyped_defs = true 60 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY 2 | _commit: v4.2.5 3 | _src_path: https://github.com/jupyterlab/extension-template 4 | author_email: jupyter@googlegroups.com 5 | author_name: Jupyter Development Team 6 | data_format: string 7 | file_extension: '' 8 | has_binder: true 9 | has_settings: true 10 | kind: server 11 | labextension_name: jupyterlab-chat-extension 12 | mimetype: '' 13 | mimetype_name: '' 14 | project_short_description: A chat extension based on shared documents 15 | python_name: jupyterlab_chat 16 | repository: https://github.com/jupyterlab/jupyter-chat 17 | test: true 18 | viewer_name: '' 19 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | jupyterlab_chat 7 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Jupyter Development Team 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab_chat 2 | 3 | [![Github Actions Status](https://github.com/jupyterlab/jupyter-chat/workflows/Build/badge.svg)](https://github.com/jupyterlab/jupyter-chat/actions/workflows/build.yml)[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab/jupyter-chat/main?urlpath=lab) 4 | 5 | A chat extension based on shared documents. 6 | 7 | This extension is composed of a Python package named `jupyterlab_chat` 8 | for the server extension and a NPM package named `jupyterlab-chat-extension` 9 | for the frontend extension. 10 | 11 | This extension registers a `YChat` shared document, and associate the document to a 12 | chat widget in the front end. 13 | 14 | ![screenshot](screenshot.gif 'jupyterlab chat extension') 15 | 16 | ## Requirements 17 | 18 | - JupyterLab >= 4.0.0 19 | 20 | ## Install 21 | 22 | To install the extension, execute: 23 | 24 | ```bash 25 | pip install jupyterlab_chat 26 | ``` 27 | 28 | ## Uninstall 29 | 30 | To remove the extension, execute: 31 | 32 | ```bash 33 | pip uninstall jupyterlab_chat 34 | ``` 35 | 36 | ## Troubleshoot 37 | 38 | If you are seeing the frontend extension, but it is not working, check 39 | that the server extension is enabled: 40 | 41 | ```bash 42 | jupyter server extension list 43 | ``` 44 | 45 | If the server extension is installed and enabled, but you are not seeing 46 | the frontend extension, check the frontend extension is installed: 47 | 48 | ```bash 49 | jupyter labextension list 50 | ``` 51 | 52 | ## Contributing 53 | 54 | ### Development install 55 | 56 | Note: You will need NodeJS to build the extension package. 57 | 58 | The `jlpm` command is JupyterLab's pinned version of 59 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 60 | `yarn` or `npm` in lieu of `jlpm` below. 61 | 62 | ```bash 63 | # Clone the repo to your local environment 64 | # Change directory to the jupyterlab_chat directory 65 | # Install package in development mode 66 | pip install -e ".[test]" 67 | # Link your development version of the extension with JupyterLab 68 | jupyter labextension develop . --overwrite 69 | # Rebuild extension Typescript source after making changes 70 | jlpm build 71 | ``` 72 | 73 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. 74 | 75 | ```bash 76 | # Watch the source directory in one terminal, automatically rebuilding when needed 77 | jlpm watch 78 | # Run JupyterLab in another terminal 79 | jupyter lab 80 | ``` 81 | 82 | With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). 83 | 84 | By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: 85 | 86 | ```bash 87 | jupyter lab build --minimize=False 88 | ``` 89 | 90 | ### Development uninstall 91 | 92 | ```bash 93 | pip uninstall jupyterlab_chat 94 | ``` 95 | 96 | In development mode, you will also need to remove the symlink created by `jupyter labextension develop` 97 | command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` 98 | folder is located. Then you can remove the symlink named `jupyterlab-chat-extension` within that folder. 99 | 100 | ### Testing the extension 101 | 102 | #### Frontend tests 103 | 104 | This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. 105 | 106 | To execute them, execute: 107 | 108 | ```sh 109 | jlpm 110 | jlpm test 111 | ``` 112 | 113 | #### Integration tests 114 | 115 | This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). 116 | More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. 117 | 118 | More information are provided within the [ui-tests](../../ui-tests/README.md) README. 119 | 120 | ### Packaging the extension 121 | 122 | See [RELEASE](RELEASE.md) 123 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of jupyterlab_chat 2 | 3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 4 | 5 | ## Manual release 6 | 7 | ### Python package 8 | 9 | This extension can be distributed as Python packages. All of the Python 10 | packaging instructions are in the `pyproject.toml` file to wrap your extension in a 11 | Python package. Before generating a package, you first need to install some tools: 12 | 13 | ```bash 14 | pip install build twine hatch 15 | ``` 16 | 17 | Bump the version using `hatch`. By default this will create a tag. 18 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. 19 | 20 | ```bash 21 | hatch version 22 | ``` 23 | 24 | Make sure to clean up all the development files before building the package: 25 | 26 | ```bash 27 | jlpm clean:all 28 | ``` 29 | 30 | You could also clean up the local git repository: 31 | 32 | ```bash 33 | git clean -dfX 34 | ``` 35 | 36 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 37 | 38 | ```bash 39 | python -m build 40 | ``` 41 | 42 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. 43 | 44 | Then to upload the package to PyPI, do: 45 | 46 | ```bash 47 | twine upload dist/* 48 | ``` 49 | 50 | ### NPM package 51 | 52 | To publish the frontend part of the extension as a NPM package, do: 53 | 54 | ```bash 55 | npm login 56 | npm publish --access public 57 | ``` 58 | 59 | ## Automated releases with the Jupyter Releaser 60 | 61 | The extension repository should already be compatible with the Jupyter Releaser. 62 | 63 | Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Add tokens to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository: 68 | - `ADMIN_GITHUB_TOKEN` (with "public_repo" and "repo:status" permissions); see the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) 69 | - `NPM_TOKEN` (with "automation" permission); see the [documentation](https://docs.npmjs.com/creating-and-viewing-access-tokens) 70 | - Set up PyPI 71 | 72 |
Using PyPI trusted publisher (modern way) 73 | 74 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) 75 | - The _workflow name_ is `publish-release.yml` and the _environment_ should be left blank. 76 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/)) 77 | 78 |
79 | 80 |
Using PyPI token (legacy way) 81 | 82 | - If the repo generates PyPI release(s), create a scoped PyPI [token](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github). We recommend using a scoped token for security reasons. 83 | 84 | - You can store the token as `PYPI_TOKEN` in your fork's `Secrets`. 85 | 86 | - Advanced usage: if you are releasing multiple repos, you can create a secret named `PYPI_TOKEN_MAP` instead of `PYPI_TOKEN` that is formatted as follows: 87 | 88 | ```text 89 | owner1/repo1,token1 90 | owner2/repo2,token2 91 | ``` 92 | 93 | If you have multiple Python packages in the same repository, you can point to them as follows: 94 | 95 | ```text 96 | owner1/repo1/path/to/package1,token1 97 | owner1/repo1/path/to/package2,token2 98 | ``` 99 | 100 |
101 | 102 | - Go to the Actions panel 103 | - Run the "Step 1: Prep Release" workflow 104 | - Check the draft changelog 105 | - Run the "Step 2: Publish Release" workflow 106 | 107 | ## Publishing to `conda-forge` 108 | 109 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html 110 | 111 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 112 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/binder/environment.yml: -------------------------------------------------------------------------------- 1 | # a mybinder.org-ready environment for demoing jupyterlab_chat 2 | # this environment may also be used locally on Linux/MacOS/Windows, e.g. 3 | # 4 | # conda env update --file binder/environment.yml 5 | # conda activate jupyterlab-chat-demo 6 | # 7 | name: jupyterlab-chat-demo 8 | 9 | channels: 10 | - conda-forge 11 | 12 | dependencies: 13 | # runtime dependencies 14 | - python >=3.10,<3.11.0a0 15 | - jupyterlab >=4.0.0,<5 16 | # labextension build dependencies 17 | - nodejs >=18,<19 18 | - pip 19 | - wheel 20 | # additional packages for demos 21 | # - ipywidgets 22 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ perform a development install of jupyterlab_chat 3 | 4 | On Binder, this will run _after_ the environment has been fully created from 5 | the environment.yml in this directory. 6 | 7 | This script should also run locally on Linux/MacOS/Windows: 8 | 9 | python3 binder/postBuild 10 | """ 11 | import subprocess 12 | import sys 13 | from pathlib import Path 14 | 15 | 16 | ROOT = Path.cwd() 17 | 18 | def _(*args, **kwargs): 19 | """ Run a command, echoing the args 20 | 21 | fails hard if something goes wrong 22 | """ 23 | print("\n\t", " ".join(args), "\n") 24 | return_code = subprocess.call(args, **kwargs) 25 | if return_code != 0: 26 | print("\nERROR", return_code, " ".join(args)) 27 | sys.exit(return_code) 28 | 29 | # verify the environment is self-consistent before even starting 30 | _(sys.executable, "-m", "pip", "check") 31 | 32 | # install the labextension 33 | _(sys.executable, "-m", "pip", "install", "-e", ".") 34 | _(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".") 35 | _( 36 | sys.executable, 37 | "-m", 38 | "jupyter", 39 | "server", 40 | "extension", 41 | "enable", 42 | "jupyterlab_chat", 43 | ) 44 | 45 | # verify the environment the extension didn't break anything 46 | _(sys.executable, "-m", "pip", "check") 47 | 48 | # list the extensions 49 | _("jupyter", "server", "extension", "list") 50 | 51 | # initially list installed extensions to determine if there are any surprises 52 | _("jupyter", "labextension", "list") 53 | 54 | 55 | print("JupyterLab with jupyterlab_chat is ready to run with:\n") 56 | print("\tjupyter lab\n") 57 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyterlab_chat", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_chat" 5 | } 6 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/jupyter-config/server-config/jupyterlab_chat.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyterlab_chat": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/jupyterlab_chat/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | try: 5 | from ._version import __version__ 6 | except ImportError: 7 | # Fallback when using the package in dev mode without installing 8 | # in editable mode with pip. It is highly recommended to install 9 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs 10 | import warnings 11 | warnings.warn("Importing 'jupyterlab_chat' outside a proper installation.") 12 | __version__ = "dev" 13 | 14 | 15 | def _jupyter_labextension_paths(): 16 | return [{ 17 | "src": "labextension", 18 | "dest": "jupyterlab-chat-extension" 19 | }] 20 | 21 | 22 | def _jupyter_server_extension_points(): 23 | return [{ 24 | "module": "jupyterlab_chat" 25 | }] 26 | 27 | 28 | def _load_jupyter_server_extension(server_app): 29 | """Registers the API handler to receive HTTP requests from the frontend extension. 30 | Parameters 31 | ---------- 32 | server_app: jupyterlab.labapp.LabApp 33 | JupyterLab application instance 34 | """ 35 | name = "jupyterlab_chat" 36 | server_app.log.info(f"Registered {name} server extension") 37 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/jupyterlab_chat/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.11.0" 2 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/jupyterlab_chat/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from dataclasses import dataclass, field 5 | from typing import Literal, Optional 6 | from jupyter_server.auth import User as JupyterUser 7 | 8 | 9 | def message_asdict_factory(data): 10 | """ Remove None values when converting Message to dict """ 11 | return dict(x for x in data if x[1] is not None) 12 | 13 | 14 | @dataclass 15 | class Message: 16 | """ Object representing a message """ 17 | 18 | # required arguments 19 | body: str 20 | """ The content of the message """ 21 | 22 | id: str 23 | """ Unique ID """ 24 | 25 | time: float 26 | """ Timestamp in second since epoch """ 27 | 28 | sender: str 29 | """ The message sender unique id """ 30 | 31 | # optional arguments, with defaults. 32 | # 33 | # These must be listed after all required arguments, unless `kw_only` is 34 | # specified in the `@dataclass` decorator. This can only be done once Python 35 | # 3.9 reaches EOL. 36 | type: Literal["msg"] = "msg" 37 | 38 | attachments: Optional[list[str]] = None 39 | """ The message attachments, a list of attachment ID """ 40 | 41 | mentions: Optional[list[str]] = field(default_factory=list) 42 | """ Users mentioned in the message """ 43 | 44 | raw_time: Optional[bool] = None 45 | """ 46 | Whether the timestamp is raw (from client) or not (from server, unified) 47 | Default to None 48 | """ 49 | 50 | deleted: Optional[bool] = None 51 | """ 52 | Whether the message has been deleted or not (body should be empty if True) 53 | Default to None. 54 | """ 55 | 56 | edited: Optional[bool] = None 57 | """ 58 | Whether the message has been edited or not 59 | Default to None. 60 | """ 61 | 62 | 63 | @dataclass 64 | class NewMessage: 65 | """ Object representing a new message """ 66 | 67 | body: str 68 | """ The content of the message """ 69 | 70 | sender: str 71 | """ The message sender unique id """ 72 | 73 | 74 | @dataclass 75 | class User(JupyterUser): 76 | """ Object representing a user """ 77 | 78 | mention_name: Optional[str] = None 79 | """ The string to use as mention in chat """ 80 | 81 | bot: Optional[bool] = None 82 | """ Boolean identifying if user is a bot """ 83 | 84 | 85 | @dataclass 86 | class Attachment: 87 | """ Object representing an attachment """ 88 | 89 | type: str 90 | """ The type of attachment (i.e. "file", "variable", "image") """ 91 | 92 | value: str 93 | """ The value (i.e. a path, a variable name, an image content) """ 94 | 95 | mimetype: Optional[str] = None 96 | """ 97 | The mime type of the attachment 98 | Default to None. 99 | """ 100 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/jupyterlab_chat/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/python/jupyterlab-chat/jupyterlab_chat/py.typed -------------------------------------------------------------------------------- /python/jupyterlab-chat/jupyterlab_chat/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | """Python unit tests for jupyterlab_chat.""" 5 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/jupyterlab_chat/tests/test_ychat.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # import jupyter_ydoc before YChat to avoid circular error 5 | import jupyter_ydoc 6 | 7 | from dataclasses import asdict 8 | import pytest 9 | from uuid import uuid4 10 | from ..models import message_asdict_factory, Message, NewMessage, User 11 | from ..ychat import YChat 12 | 13 | USER = User( 14 | username=str(uuid4()), 15 | name="Test user", 16 | display_name="Test user" 17 | ) 18 | 19 | USER2 = User( 20 | username=str(uuid4()), 21 | name="Test user 2", 22 | display_name="Test user 2" 23 | ) 24 | 25 | 26 | def create_new_message(body="This is a test message") -> NewMessage: 27 | return NewMessage( 28 | body=body, 29 | sender=USER.username 30 | ) 31 | 32 | 33 | def test_initialize_ychat(): 34 | chat = YChat() 35 | assert chat._get_messages() == [] 36 | assert chat._get_users() == {} 37 | assert chat.get_metadata() == {} 38 | 39 | 40 | def test_add_user(): 41 | chat = YChat() 42 | chat.set_user(USER) 43 | assert USER.username in chat._get_users().keys() 44 | assert chat._get_users()[USER.username] == asdict(USER) 45 | 46 | 47 | def test_get_user_type(): 48 | chat = YChat() 49 | chat.set_user(USER) 50 | assert isinstance(chat.get_user(USER.username), User) 51 | 52 | 53 | def test_get_user(): 54 | chat = YChat() 55 | chat.set_user(USER) 56 | chat.set_user(USER2) 57 | assert chat.get_user(USER.username) == USER 58 | assert chat.get_user(USER2.username) == USER2 59 | assert chat.get_user(str(uuid4())) == None 60 | 61 | 62 | def test_get_user_by_name_type(): 63 | chat = YChat() 64 | chat.set_user(USER) 65 | assert isinstance(chat.get_user_by_name(USER.name), User) 66 | 67 | 68 | def test_get_user_by_name(): 69 | chat = YChat() 70 | chat.set_user(USER) 71 | chat.set_user(USER2) 72 | assert chat.get_user_by_name(USER.name) == USER 73 | assert chat.get_user_by_name(USER2.name) == USER2 74 | assert chat.get_user_by_name(str(uuid4())) == None 75 | 76 | 77 | def test_add_message(): 78 | chat = YChat() 79 | msg = create_new_message() 80 | chat.add_message(msg) 81 | assert len(chat._get_messages()) == 1 82 | message_dict = chat._get_messages()[0] 83 | assert message_dict["body"] == msg.body 84 | assert message_dict["sender"] == msg.sender 85 | 86 | 87 | def test_get_message_should_return_the_message(): 88 | chat = YChat() 89 | msg = create_new_message() 90 | id = chat.add_message(msg) 91 | msg2 = create_new_message("Another message") 92 | id2 = chat.add_message(msg2) 93 | message = chat.get_message(id) 94 | assert isinstance(message, Message) 95 | assert message.body == msg.body 96 | assert message.sender == msg.sender 97 | message2 = chat.get_message(id2) 98 | assert isinstance(message2, Message) 99 | assert message2.body == msg2.body 100 | 101 | 102 | def test_get_message_should_return_None(): 103 | chat = YChat() 104 | msg = create_new_message() 105 | chat.add_message(msg) 106 | assert chat.get_message(str(uuid4())) is None 107 | 108 | 109 | def test_update_message(): 110 | chat = YChat() 111 | msg = create_new_message() 112 | chat.add_message(msg) 113 | new_msg = Message(**chat._get_messages()[0]) 114 | new_msg.body = "Updated content" 115 | chat.update_message(new_msg) 116 | assert len(chat._get_messages()) == 1 117 | message_dict = chat._get_messages()[0] 118 | assert message_dict["body"] == new_msg.body 119 | assert message_dict["sender"] == new_msg.sender 120 | 121 | 122 | def test_update_message_should_append_content(): 123 | content_to_append = " with updated content" 124 | chat = YChat() 125 | msg = create_new_message() 126 | chat.add_message(msg) 127 | new_msg = Message(**chat._get_messages()[0]) 128 | new_msg.body = content_to_append 129 | chat.update_message(new_msg, True) 130 | assert len(chat._get_messages()) == 1 131 | message_dict = chat._get_messages()[0] 132 | assert message_dict["body"] == msg.body + content_to_append 133 | assert message_dict["sender"] == msg.sender 134 | 135 | 136 | def test_indexes_by_id(): 137 | chat = YChat() 138 | msg = create_new_message() 139 | id = chat.add_message(msg) 140 | id2 = chat.add_message(msg) 141 | assert chat._indexes_by_id == { 142 | id: 0, 143 | id2: 1 144 | } 145 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | [build-system] 5 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5"] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "jupyterlab_chat" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | requires-python = ">=3.8" 13 | description = "A chat extension based on shared documents" 14 | classifiers = [ 15 | "Framework :: Jupyter", 16 | "Framework :: Jupyter :: JupyterLab", 17 | "Framework :: Jupyter :: JupyterLab :: 4", 18 | "Framework :: Jupyter :: JupyterLab :: Extensions", 19 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 20 | "License :: OSI Approved :: BSD License", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | ] 29 | dependencies = [ 30 | "jupyterlab~=4.0", 31 | "jupyter_collaboration>=3,<4", 32 | "jupyter_server>=2.0.1,<3", 33 | "jupyter_ydoc", 34 | "pycrdt" 35 | ] 36 | keywords = [ 37 | "jupyter", 38 | "jupyterlab", 39 | "jupyterlab-extension" 40 | ] 41 | authors = [ 42 | { name = "Jupyter Development Team", email = "jupyter@googlegroups.com" } 43 | ] 44 | dynamic = ["version"] 45 | 46 | [project.optional-dependencies] 47 | test = [ 48 | "coverage", 49 | "mypy", 50 | "pytest", 51 | "pytest-asyncio", 52 | "pytest-cov", 53 | "pytest-jupyter[server]>=0.6.0" 54 | ] 55 | 56 | [project.urls] 57 | documentation = "https://jupyter-chat.readthedocs.io/" 58 | homepage = "https://github.com/jupyterlab/jupyter-chat" 59 | "Bug Tracker" = "https://github.com/jupyterlab/jupyter-chat/issues" 60 | 61 | [tool.hatch.version] 62 | path = "jupyterlab_chat/_version.py" 63 | 64 | [tool.hatch.build.targets.sdist] 65 | artifacts = ["jupyterlab_chat/labextension"] 66 | exclude = [".github", "binder"] 67 | 68 | [tool.hatch.build.targets.wheel.shared-data] 69 | "jupyterlab_chat/labextension" = "share/jupyter/labextensions/jupyterlab-chat-extension" 70 | "install.json" = "share/jupyter/labextensions/jupyterlab-chat-extension/install.json" 71 | "jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d" 72 | 73 | [tool.hatch.build.hooks.jupyter-builder] 74 | dependencies = ["hatch-jupyter-builder>=0.5"] 75 | build-function = "hatch_jupyter_builder.npm_builder" 76 | ensured-targets = [ 77 | "jupyterlab_chat/labextension/static/style.js", 78 | "jupyterlab_chat/labextension/package.json", 79 | ] 80 | skip-if-exists = ["jupyterlab_chat/labextension/static/style.js"] 81 | 82 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 83 | npm = ["jlpm"] 84 | path = "../.." 85 | build_cmd = "build:prod" 86 | editable_build_cmd = "install:extension" 87 | 88 | [tool.check-wheel-contents] 89 | ignore = ["W002"] 90 | 91 | [project.entry-points.jupyter_ydoc] 92 | chat = "jupyterlab_chat.ychat:YChat" 93 | -------------------------------------------------------------------------------- /python/jupyterlab-chat/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/python/jupyterlab-chat/screenshot.gif -------------------------------------------------------------------------------- /python/jupyterlab-chat/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | __import__("setuptools").setup() 5 | -------------------------------------------------------------------------------- /scripts/bump_version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import json 5 | from pathlib import Path 6 | 7 | import click 8 | from jupyter_releaser.util import get_version, run 9 | from pkg_resources import parse_version # type: ignore 10 | 11 | LERNA_CMD = "jlpm run lerna version --no-push --force-publish --no-git-tag-version" 12 | 13 | VERSION_SPEC = ["major", "minor", "release", "next", "patch"] 14 | 15 | 16 | def increment_version(current, spec): 17 | curr = parse_version(current) 18 | 19 | if spec == "major": 20 | spec = f"{curr.major + 1}.0.0.a0" 21 | 22 | elif spec == "minor": 23 | spec = f"{curr.major}.{curr.minor + 1}.0.a0" 24 | 25 | elif spec == "release": 26 | p, x = curr.pre 27 | if p == "a": 28 | p = "b" 29 | elif p == "b": 30 | p = "rc" 31 | elif p == "rc": 32 | p = None 33 | suffix = f"{p}0" if p else "" 34 | spec = f"{curr.major}.{curr.minor}.{curr.micro}{suffix}" 35 | 36 | elif spec == "next": 37 | spec = f"{curr.major}.{curr.minor}." 38 | if curr.pre: 39 | p, x = curr.pre 40 | spec += f"{curr.micro}{p}{x + 1}" 41 | else: 42 | spec += f"{curr.micro + 1}" 43 | 44 | elif spec == "patch": 45 | spec = f"{curr.major}.{curr.minor}." 46 | if curr.pre: 47 | spec += f"{curr.micro}" 48 | else: 49 | spec += f"{curr.micro + 1}" 50 | else: 51 | raise ValueError("Unknown version spec") 52 | 53 | return spec 54 | 55 | 56 | @click.command() 57 | @click.option("--force", default=False, is_flag=True) 58 | @click.option("--skip-if-dirty", default=False, is_flag=True) 59 | @click.argument("spec", nargs=1) 60 | def bump(force, skip_if_dirty, spec): 61 | status = run("git status --porcelain").strip() 62 | if len(status) > 0: 63 | if skip_if_dirty: 64 | return 65 | raise Exception("Must be in a clean git state with no untracked files") 66 | 67 | current = get_version() 68 | 69 | if spec in VERSION_SPEC: 70 | version = parse_version(increment_version(current, spec)) 71 | else: 72 | version = parse_version(spec) 73 | 74 | # convert the Python version 75 | js_version = f"{version.major}.{version.minor}.{version.micro}" 76 | if version.pre: 77 | p, x = version.pre 78 | p = p.replace("a", "alpha").replace("b", "beta") 79 | js_version += f"-{p}.{x}" 80 | 81 | # bump the JS packages 82 | lerna_cmd = f"{LERNA_CMD} {js_version}" 83 | if force: 84 | lerna_cmd += " --yes" 85 | run(lerna_cmd) 86 | 87 | HERE = Path(__file__).parent.parent.resolve() 88 | 89 | # bump the Python packages 90 | for version_file in HERE.glob("python/**/_version.py"): 91 | content = version_file.read_text().splitlines() 92 | variable, current = content[0].split(" = ") 93 | if variable != "__version__": 94 | raise ValueError( 95 | f"Version file {version_file} has unexpected content;" 96 | f" expected __version__ assignment in the first line, found {variable}" 97 | ) 98 | current = current.strip("'\"") 99 | if spec in VERSION_SPEC: 100 | version_spec = increment_version(current, spec) 101 | else: 102 | version_spec = spec 103 | version_file.write_text(f'__version__ = "{version_spec}"\n') 104 | 105 | # bump the local package.json file 106 | path = HERE.joinpath("package.json") 107 | if path.exists(): 108 | with path.open(mode="r") as f: 109 | data = json.load(f) 110 | 111 | data["version"] = js_version 112 | 113 | with path.open(mode="w") as f: 114 | json.dump(data, f, indent=2) 115 | 116 | else: 117 | raise FileNotFoundError(f"Could not find package.json under dir {path!s}") 118 | 119 | 120 | if __name__ == "__main__": 121 | bump() 122 | -------------------------------------------------------------------------------- /scripts/dev_install.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import subprocess 5 | from pathlib import Path 6 | from typing import Optional 7 | 8 | 9 | def execute(cmd: str, cwd: Optional[Path] = None) -> None: 10 | subprocess.run(cmd.split(" "), check=True, cwd=cwd) 11 | 12 | 13 | def install_dev() -> None: 14 | execute( "python -m pip install jupyterlab~=4.0") 15 | execute("jlpm install") 16 | 17 | execute("pip uninstall jupyterlab_chat -y") 18 | execute("pip install -e python/jupyterlab-chat[test]") 19 | execute("jupyter labextension develop --overwrite python/jupyterlab-chat --overwrite") 20 | 21 | 22 | if __name__ == "__main__": 23 | install_dev() 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "strict": true, 17 | "strictNullChecks": true, 18 | "target": "ES2018" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui-tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Testing 2 | 3 | This folder contains the integration tests of the extension. 4 | 5 | They are defined using [Playwright](https://playwright.dev/docs/intro) test runner 6 | and [Galata](https://github.com/jupyterlab/jupyterlab/tree/main/galata) helper. 7 | 8 | The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). 9 | 10 | The JupyterLab server configuration to use for the integration test is defined 11 | in [jupyter_server_test_config.py](./jupyter_server_test_config.py). 12 | 13 | The default configuration will produce video for failing tests and an HTML report. 14 | 15 | > There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). 16 | 17 | ## Run the tests 18 | 19 | > All commands are assumed to be executed from the root directory 20 | 21 | To run the tests, you need to: 22 | 23 | 1. Compile the extension: 24 | 25 | ```sh 26 | jlpm install 27 | jlpm build:prod 28 | ``` 29 | 30 | > Check the extension is installed in JupyterLab. 31 | 32 | 2. Install test dependencies (needed only once): 33 | 34 | ```sh 35 | cd ./ui-tests 36 | jlpm install 37 | jlpm playwright install 38 | cd .. 39 | ``` 40 | 41 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: 42 | 43 | ```sh 44 | cd ./ui-tests 45 | jlpm playwright test 46 | ``` 47 | 48 | Test results will be shown in the terminal. In case of any test failures, the test report 49 | will be opened in your browser at the end of the tests execution; see 50 | [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) 51 | for configuring that behavior. 52 | 53 | ## Update the tests snapshots 54 | 55 | > All commands are assumed to be executed from the root directory 56 | 57 | If you are comparing snapshots to validate your tests, you may need to update 58 | the reference snapshots stored in the repository. To do that, you need to: 59 | 60 | 1. Compile the extension: 61 | 62 | ```sh 63 | jlpm install 64 | jlpm build:prod 65 | ``` 66 | 67 | > Check the extension is installed in JupyterLab. 68 | 69 | 2. Install test dependencies (needed only once): 70 | 71 | ```sh 72 | cd ./ui-tests 73 | jlpm install 74 | jlpm playwright install 75 | cd .. 76 | ``` 77 | 78 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) command: 79 | 80 | ```sh 81 | cd ./ui-tests 82 | jlpm playwright test -u 83 | ``` 84 | 85 | > Some discrepancy may occurs between the snapshots generated on your computer and 86 | > the one generated on the CI. To ease updating the snapshots on a PR, you can 87 | > type `please update playwright snapshots` to trigger the update by a bot on the CI. 88 | > Once the bot has computed new snapshots, it will commit them to the PR branch. 89 | 90 | ## Create tests 91 | 92 | > All commands are assumed to be executed from the root directory 93 | 94 | To create tests, the easiest way is to use the code generator tool of playwright: 95 | 96 | 1. Compile the extension: 97 | 98 | ```sh 99 | jlpm install 100 | jlpm build:prod 101 | ``` 102 | 103 | > Check the extension is installed in JupyterLab. 104 | 105 | 2. Install test dependencies (needed only once): 106 | 107 | ```sh 108 | cd ./ui-tests 109 | jlpm install 110 | jlpm playwright install 111 | cd .. 112 | ``` 113 | 114 | 3. Start the server: 115 | 116 | ```sh 117 | cd ./ui-tests 118 | jlpm start 119 | ``` 120 | 121 | 4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: 122 | 123 | ```sh 124 | cd ./ui-tests 125 | jlpm playwright codegen localhost:8888 126 | ``` 127 | 128 | ## Debug tests 129 | 130 | > All commands are assumed to be executed from the root directory 131 | 132 | To debug tests, a good way is to use the inspector tool of playwright: 133 | 134 | 1. Compile the extension: 135 | 136 | ```sh 137 | jlpm install 138 | jlpm build:prod 139 | ``` 140 | 141 | > Check the extension is installed in JupyterLab. 142 | 143 | 2. Install test dependencies (needed only once): 144 | 145 | ```sh 146 | cd ./ui-tests 147 | jlpm install 148 | jlpm playwright install 149 | cd .. 150 | ``` 151 | 152 | 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): 153 | 154 | ```sh 155 | cd ./ui-tests 156 | jlpm playwright test --debug 157 | ``` 158 | 159 | ## Upgrade Playwright and the browsers 160 | 161 | To update the web browser versions, you must update the package `@playwright/test`: 162 | 163 | ```sh 164 | cd ./ui-tests 165 | jlpm up "@playwright/test" 166 | jlpm playwright install 167 | ``` 168 | -------------------------------------------------------------------------------- /ui-tests/jupyter_server_test_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | """Server configuration for integration tests. 5 | 6 | !! Never use this configuration in production because it 7 | opens the server to the world and provide access to JupyterLab 8 | JavaScript objects through the global window variable. 9 | """ 10 | from jupyterlab.galata import configure_jupyter_server 11 | 12 | configure_jupyter_server(c) 13 | 14 | c.FileContentsManager.delete_to_trash = False 15 | 16 | # Uncomment to set server log level to debug level 17 | # c.ServerApp.log_level = "DEBUG" 18 | -------------------------------------------------------------------------------- /ui-tests/jupyter_server_test_config_notebook.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | """Server configuration for integration tests. 5 | !! Never use this configuration in production because it 6 | opens the server to the world and provide access to JupyterLab 7 | JavaScript objects through the global window variable. 8 | """ 9 | from pathlib import Path 10 | import jupyterlab 11 | 12 | c.ServerApp.port = 8888 13 | c.ServerApp.port_retries = 0 14 | c.ServerApp.open_browser = False 15 | 16 | c.ServerApp.token = "" 17 | c.ServerApp.password = "" 18 | c.ServerApp.disable_check_xsrf = True 19 | 20 | c.JupyterNotebookApp.expose_app_in_browser = True 21 | 22 | c.LabServerApp.extra_labextensions_path = str(Path(jupyterlab.__file__).parent / "galata") 23 | 24 | c.FileContentsManager.delete_to_trash = False 25 | 26 | # Uncomment to set server log level to debug level 27 | # c.ServerApp.log_level = "DEBUG" 28 | -------------------------------------------------------------------------------- /ui-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab-chat-ui-tests", 3 | "version": "1.0.0", 4 | "description": "JupyterLab jupyterlab-chat Integration Tests", 5 | "private": true, 6 | "scripts": { 7 | "start": "jupyter lab --config jupyter_server_test_config.py", 8 | "start:notebook": "jupyter notebook --config jupyter_server_test_config_notebook.py", 9 | "test": "jlpm playwright test", 10 | "test:notebook": "jlpm playwright test --config=playwright.notebook.config.js", 11 | "test:update": "jlpm playwright test --update-snapshots" 12 | }, 13 | "devDependencies": { 14 | "@jupyterlab/galata": "^5.0.0", 15 | "@playwright/test": "^1.43.0" 16 | }, 17 | "packageManager": "yarn@3.5.0" 18 | } 19 | -------------------------------------------------------------------------------- /ui-tests/playwright.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /** 7 | * Configuration for Playwright using default from @jupyterlab/galata 8 | */ 9 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); 10 | 11 | module.exports = { 12 | ...baseConfig, 13 | webServer: { 14 | command: 'jlpm start', 15 | url: 'http://localhost:8888/lab', 16 | timeout: 120 * 1000, 17 | reuseExistingServer: !process.env.CI 18 | }, 19 | testIgnore: 'tests/notebook-application.spec.ts', 20 | use: { 21 | ...baseConfig.use, 22 | contextOptions: { 23 | permissions: ['clipboard-read', 'clipboard-write'] 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /ui-tests/playwright.notebook.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /** 7 | * Configuration for Playwright using default from @jupyterlab/galata 8 | */ 9 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); 10 | 11 | module.exports = { 12 | ...baseConfig, 13 | webServer: { 14 | command: 'jlpm start:notebook', 15 | url: 'http://localhost:8888/tree', 16 | timeout: 120 * 1000, 17 | reuseExistingServer: !process.env.CI 18 | }, 19 | testMatch: 'tests/notebook-application.spec.ts' 20 | }; 21 | -------------------------------------------------------------------------------- /ui-tests/tests/commands.spec.ts-snapshots/launcher-tile-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/commands.spec.ts-snapshots/launcher-tile-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/commands.spec.ts-snapshots/menu-new-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/commands.spec.ts-snapshots/menu-new-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/general.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { expect, test } from '@jupyterlab/galata'; 7 | import { openChat, openChatToSide, openSidePanel } from './test-utils'; 8 | 9 | const CHAT1 = 'test1.chat'; 10 | const CHAT2 = 'test2.chat'; 11 | 12 | test.describe('#restorer', () => { 13 | test.beforeEach(async ({ page }) => { 14 | // Create chat filed 15 | await page.filebrowser.contents.uploadContent('{}', 'text', CHAT1); 16 | await page.filebrowser.contents.uploadContent('{}', 'text', CHAT2); 17 | }); 18 | 19 | test.afterEach(async ({ page }) => { 20 | [CHAT1, CHAT2].forEach(async file => { 21 | if (await page.filebrowser.contents.fileExists(file)) { 22 | await page.filebrowser.contents.deleteFile(file); 23 | } 24 | }); 25 | }); 26 | 27 | test('should restore the previous session', async ({ page }) => { 28 | const chat1 = await openChat(page, CHAT1); 29 | const chat2 = await openChatToSide(page, CHAT2); 30 | await page.reload({ waitForIsReady: false }); 31 | 32 | await expect(chat1).toBeVisible(); 33 | // open the side panel if it is not 34 | await openSidePanel(page); 35 | await expect(chat2).toBeVisible(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /ui-tests/tests/input-toolbar.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | import { PathExt } from '@jupyterlab/coreutils'; 6 | import { expect, test } from '@jupyterlab/galata'; 7 | 8 | import { 9 | createChat, 10 | exposeDepsJs, 11 | getPlugin, 12 | openChat, 13 | openChatToSide 14 | } from './test-utils'; 15 | 16 | const CHAT = 'test.chat'; 17 | 18 | test.describe('#inputToolbar', () => { 19 | test('Should hide toolbar item for main area chat only', async ({ 20 | page, 21 | tmpPath 22 | }) => { 23 | // Expose a function to get a plugin. 24 | await page.evaluate(exposeDepsJs({ getPlugin })); 25 | 26 | // Modify the input toolbar when a chat is opened. 27 | await page.evaluate(async () => { 28 | const tracker = ( 29 | await window.getPlugin('jupyterlab-chat-extension:factory') 30 | ).tracker; 31 | 32 | const updateToolbar = registry => { 33 | registry.hide('attach'); 34 | }; 35 | 36 | // Should update the input toolbar of main area widgets only 37 | tracker.widgetAdded.connect((_, widget) => { 38 | if (widget.context) { 39 | let registry = widget.context.inputToolbarRegistry; 40 | if (registry) { 41 | updateToolbar(registry); 42 | } 43 | } 44 | }); 45 | }); 46 | 47 | const chatPath = PathExt.join(tmpPath, CHAT); 48 | await createChat(page, chatPath); 49 | const chatPanel = await openChat(page, chatPath); 50 | 51 | // The main area chat input should not contain the 'attach' button. 52 | const inputToolbar = chatPanel.locator( 53 | '.jp-chat-input-container .jp-chat-input-toolbar' 54 | ); 55 | await expect(inputToolbar).toBeVisible(); 56 | await expect(inputToolbar.locator('button')).toHaveCount(2); 57 | expect(inputToolbar.locator('.jp-chat-attach-button')).not.toBeAttached(); 58 | 59 | // The side panel chat input should contain the 'attach' button. 60 | const chatSidePanel = await openChatToSide(page, chatPath); 61 | const inputToolbarSide = chatSidePanel.locator( 62 | '.jp-chat-input-container .jp-chat-input-toolbar' 63 | ); 64 | await expect(inputToolbarSide).toBeVisible(); 65 | await expect(inputToolbarSide.locator('button')).toHaveCount(3); 66 | expect(inputToolbarSide.locator('.jp-chat-attach-button')).toBeAttached(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /ui-tests/tests/notebook-application.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { expect, test as base } from '@jupyterlab/galata'; 7 | 8 | export const test = base.extend({ 9 | waitForApplication: async ({ baseURL }, use, testInfo) => { 10 | const waitIsReady = async (page): Promise => { 11 | await page.waitForSelector('#main-panel'); 12 | }; 13 | await use(waitIsReady); 14 | } 15 | }); 16 | 17 | test.use({ 18 | autoGoto: false, 19 | appPath: '', 20 | viewport: { width: 1024, height: 900 } 21 | }); 22 | 23 | const NAME = 'my_chat'; 24 | const FILENAME = `${NAME}.chat`; 25 | 26 | test.describe('#NotebookApp', () => { 27 | test.beforeEach(async ({ page }) => { 28 | // Create a chat file 29 | await page.filebrowser.contents.uploadContent('{}', 'text', FILENAME); 30 | }); 31 | 32 | test.afterEach(async ({ page }) => { 33 | if (await page.filebrowser.contents.fileExists(FILENAME)) { 34 | await page.filebrowser.contents.deleteFile(FILENAME); 35 | } 36 | }); 37 | 38 | test('Should open side panel and list existing chats', async ({ page }) => { 39 | await page.goto('tree'); 40 | await page.menu.clickMenuItem('View>Left Sidebar>Show Jupyter Chat'); 41 | const panel = page.locator('#jp-left-stack'); 42 | await expect(panel).toBeVisible(); 43 | await expect(panel.locator('.jp-lab-chat-sidepanel')).toBeVisible(); 44 | 45 | const select = panel.locator( 46 | '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-open select' 47 | ); 48 | 49 | await expect(select.locator('option')).toHaveCount(2); 50 | await expect(select.locator('option').last()).toHaveText(NAME); 51 | }); 52 | 53 | test('Should open main panel in a separate tab', async ({ 54 | page, 55 | context 56 | }) => { 57 | await page.goto('tree'); 58 | 59 | const pagePromise = context.waitForEvent('page'); 60 | await page.dblclick(`.jp-FileBrowser-listing >> text=${FILENAME}`); 61 | 62 | const newPage = await pagePromise; 63 | //wait for Load 64 | // await newPage.waitForLoadState(); 65 | 66 | await expect(newPage.locator('.jp-MainAreaWidget')).toHaveClass( 67 | /jp-lab-chat-main-panel/ 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /ui-tests/tests/notifications.spec.ts-snapshots/tab-with-unread-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/notifications.spec.ts-snapshots/tab-with-unread-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/notifications.spec.ts-snapshots/tab-without-unread-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/notifications.spec.ts-snapshots/tab-without-unread-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/raw-time.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { expect, galata, test } from '@jupyterlab/galata'; 7 | import { UUID } from '@lumino/coreutils'; 8 | 9 | import { openChat, sendMessage, USER } from './test-utils'; 10 | 11 | const FILENAME = 'my-chat.chat'; 12 | const MSG_CONTENT = 'Hello World!'; 13 | const USERNAME = USER.identity.username; 14 | 15 | test.use({ 16 | mockUser: USER, 17 | mockSettings: { ...galata.DEFAULT_SETTINGS } 18 | }); 19 | 20 | test.describe('#raw_time', () => { 21 | const msg_raw_time = { 22 | type: 'msg', 23 | id: UUID.uuid4(), 24 | sender: USERNAME, 25 | body: MSG_CONTENT, 26 | time: 1714116341, 27 | raw_time: true 28 | }; 29 | const msg_verif = { 30 | type: 'msg', 31 | id: UUID.uuid4(), 32 | sender: USERNAME, 33 | body: MSG_CONTENT, 34 | time: 1714116341, 35 | raw_time: false 36 | }; 37 | const chatContent = { 38 | messages: [msg_raw_time, msg_verif], 39 | users: {} 40 | }; 41 | chatContent.users[USERNAME] = USER.identity; 42 | 43 | test.beforeEach(async ({ page }) => { 44 | // Create a chat file with content 45 | await page.filebrowser.contents.uploadContent( 46 | JSON.stringify(chatContent), 47 | 'text', 48 | FILENAME 49 | ); 50 | }); 51 | 52 | test.afterEach(async ({ page }) => { 53 | if (await page.filebrowser.contents.fileExists(FILENAME)) { 54 | await page.filebrowser.contents.deleteFile(FILENAME); 55 | } 56 | }); 57 | 58 | test('message timestamp should be raw according to file content', async ({ 59 | page 60 | }) => { 61 | const chatPanel = await openChat(page, FILENAME); 62 | const messages = chatPanel.locator( 63 | '.jp-chat-messages-container .jp-chat-message' 64 | ); 65 | 66 | const raw_time = messages.locator('.jp-chat-message-time').first(); 67 | expect(await raw_time.getAttribute('title')).toBe('Unverified time'); 68 | expect(await raw_time.textContent()).toMatch(/\*$/); 69 | 70 | const verified_time = messages.locator('.jp-chat-message-time').last(); 71 | expect(await verified_time.getAttribute('title')).toBe(''); 72 | expect(await verified_time.textContent()).toMatch(/[^\*]$/); 73 | }); 74 | 75 | test('time for new message should not be raw', async ({ page }) => { 76 | const chatPanel = await openChat(page, FILENAME); 77 | const messages = chatPanel.locator( 78 | '.jp-chat-messages-container .jp-chat-message' 79 | ); 80 | 81 | // Send a new message 82 | await sendMessage(page, FILENAME, MSG_CONTENT); 83 | 84 | expect(messages).toHaveCount(3); 85 | await expect( 86 | messages.locator('.jp-chat-message-time').last() 87 | ).toHaveAttribute('title', ''); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /ui-tests/tests/side-panel.spec.ts-snapshots/chat-icon-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/side-panel.spec.ts-snapshots/chat-icon-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/side-panel.spec.ts-snapshots/moveToMain-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/side-panel.spec.ts-snapshots/moveToMain-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/side-panel.spec.ts-snapshots/moveToSide-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/side-panel.spec.ts-snapshots/moveToSide-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/ui-config.spec.ts-snapshots/not-stacked-messages-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/ui-config.spec.ts-snapshots/not-stacked-messages-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/ui-config.spec.ts-snapshots/stacked-messages-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/ui-config.spec.ts-snapshots/stacked-messages-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/unread.spec.ts-snapshots/navigation-bottom-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/unread.spec.ts-snapshots/navigation-bottom-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/unread.spec.ts-snapshots/navigation-bottom-unread-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/unread.spec.ts-snapshots/navigation-bottom-unread-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/unread.spec.ts-snapshots/navigation-top-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-chat/860cd9d807dc6010c5830d604bab0235b2a07f7a/ui-tests/tests/unread.spec.ts-snapshots/navigation-top-linux.png --------------------------------------------------------------------------------