jupyter-chat-components 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +136 -0
- package/lib/index.d.ts +32 -0
- package/lib/index.js +76 -0
- package/lib/token.d.ts +35 -0
- package/lib/token.js +5 -0
- package/lib/tool-call.d.ts +14 -0
- package/lib/tool-call.js +152 -0
- package/package.json +202 -0
- package/src/__tests__/jupyter_ai_chat_components.spec.ts +9 -0
- package/src/index.ts +115 -0
- package/src/token.ts +53 -0
- package/src/tool-call.ts +224 -0
- package/style/base.css +207 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Project Jupyter
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# jupyter_chat_components
|
|
2
|
+
|
|
3
|
+
[](https://github.com/brichet/jupyter-chat-components/actions/workflows/build.yml)
|
|
4
|
+
|
|
5
|
+
Components to displayed in jupyter chat
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- JupyterLab >= 4.0.0
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
To install the extension, execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install jupyter_chat_components
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Uninstall
|
|
20
|
+
|
|
21
|
+
To remove the extension, execute:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip uninstall jupyter_chat_components
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Contributing
|
|
28
|
+
|
|
29
|
+
### Development install
|
|
30
|
+
|
|
31
|
+
Note: You will need NodeJS to build the extension package.
|
|
32
|
+
|
|
33
|
+
The `jlpm` command is JupyterLab's pinned version of
|
|
34
|
+
[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
|
|
35
|
+
`yarn` or `npm` in lieu of `jlpm` below.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Clone the repo to your local environment
|
|
39
|
+
# Change directory to the jupyter_chat_components directory
|
|
40
|
+
|
|
41
|
+
# Set up a virtual environment and install package in development mode
|
|
42
|
+
python -m venv .venv
|
|
43
|
+
source .venv/bin/activate
|
|
44
|
+
pip install --editable "."
|
|
45
|
+
|
|
46
|
+
# Link your development version of the extension with JupyterLab
|
|
47
|
+
jupyter labextension develop . --overwrite
|
|
48
|
+
|
|
49
|
+
# Rebuild extension Typescript source after making changes
|
|
50
|
+
# IMPORTANT: Unlike the steps above which are performed only once, do this step
|
|
51
|
+
# every time you make a change.
|
|
52
|
+
jlpm build
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Watch the source directory in one terminal, automatically rebuilding when needed
|
|
59
|
+
jlpm watch
|
|
60
|
+
# Run JupyterLab in another terminal
|
|
61
|
+
jupyter lab
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
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).
|
|
65
|
+
|
|
66
|
+
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:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
jupyter lab build --minimize=False
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Development uninstall
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip uninstall jupyter_chat_components
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
|
|
79
|
+
command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
|
|
80
|
+
folder is located. Then you can remove the symlink named `jupyter-chat-components` within that folder.
|
|
81
|
+
|
|
82
|
+
### Testing the extension
|
|
83
|
+
|
|
84
|
+
#### Frontend tests
|
|
85
|
+
|
|
86
|
+
This extension is using [Jest](https://jestjs.io/) for JavaScript code testing.
|
|
87
|
+
|
|
88
|
+
To execute them, execute:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
jlpm
|
|
92
|
+
jlpm test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Integration tests
|
|
96
|
+
|
|
97
|
+
This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests).
|
|
98
|
+
More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab.
|
|
99
|
+
|
|
100
|
+
More information are provided within the [ui-tests](./ui-tests/README.md) README.
|
|
101
|
+
|
|
102
|
+
## AI Coding Assistant Support
|
|
103
|
+
|
|
104
|
+
This project includes an `AGENTS.md` file with coding standards and best practices for JupyterLab extension development. The file follows the [AGENTS.md standard](https://agents.md) for cross-tool compatibility.
|
|
105
|
+
|
|
106
|
+
### Compatible AI Tools
|
|
107
|
+
|
|
108
|
+
`AGENTS.md` works with AI coding assistants that support the standard, including Cursor, GitHub Copilot, Windsurf, Aider, and others. For a current list of compatible tools, see [the AGENTS.md standard](https://agents.md).
|
|
109
|
+
|
|
110
|
+
Other conventions you might encounter:
|
|
111
|
+
|
|
112
|
+
- `.cursorrules` - Cursor's YAML/JSON format (Cursor also supports AGENTS.md natively)
|
|
113
|
+
- `CONVENTIONS.md` / `CONTRIBUTING.md` - For CodeConventions.ai and GitHub bots
|
|
114
|
+
- Project-specific rules in JetBrains AI Assistant settings
|
|
115
|
+
|
|
116
|
+
All tool-specific files should be symlinks to `AGENTS.md` as the single source of truth.
|
|
117
|
+
|
|
118
|
+
### What's Included
|
|
119
|
+
|
|
120
|
+
The `AGENTS.md` file provides guidance on:
|
|
121
|
+
|
|
122
|
+
- Code quality rules and file-scoped validation commands
|
|
123
|
+
- Naming conventions for packages, plugins, and files
|
|
124
|
+
- Coding standards (TypeScript)
|
|
125
|
+
- Development workflow and debugging
|
|
126
|
+
- Common pitfalls and how to avoid them
|
|
127
|
+
|
|
128
|
+
### Customization
|
|
129
|
+
|
|
130
|
+
You can edit `AGENTS.md` to add project-specific conventions or adjust guidelines to match your team's practices. The file uses plain Markdown with Do/Don't patterns and references to actual project files.
|
|
131
|
+
|
|
132
|
+
**Note**: `AGENTS.md` is living documentation. Update it when you change conventions, add dependencies, or discover new patterns. Include `AGENTS.md` updates in commits that modify workflows or coding standards.
|
|
133
|
+
|
|
134
|
+
### Packaging the extension
|
|
135
|
+
|
|
136
|
+
See [RELEASE](RELEASE.md)
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
|
|
3
|
+
import { Widget } from '@lumino/widgets';
|
|
4
|
+
import { IComponentsRendererFactory, ToolCallApproval } from './token';
|
|
5
|
+
/**
|
|
6
|
+
* The options for the chat components renderer.
|
|
7
|
+
*/
|
|
8
|
+
interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The callback to approve or reject a tool.
|
|
11
|
+
*/
|
|
12
|
+
toolCallApproval: ToolCallApproval;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A widget for rendering .
|
|
16
|
+
*/
|
|
17
|
+
export declare class ComponentsRenderer extends Widget implements IRenderMime.IRenderer {
|
|
18
|
+
/**
|
|
19
|
+
* Construct a new output widget.
|
|
20
|
+
*/
|
|
21
|
+
constructor(options: IComponentsRendererOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Render into this widget's node.
|
|
24
|
+
*/
|
|
25
|
+
renderModel(model: IRenderMime.IMimeModel): Promise<void>;
|
|
26
|
+
private _trans;
|
|
27
|
+
private _mimeType;
|
|
28
|
+
private _toolCallApproval;
|
|
29
|
+
}
|
|
30
|
+
declare const plugin: JupyterFrontEndPlugin<IComponentsRendererFactory>;
|
|
31
|
+
export * from './token';
|
|
32
|
+
export default plugin;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
3
|
+
import { Widget } from '@lumino/widgets';
|
|
4
|
+
import { IComponentsRendererFactory } from './token';
|
|
5
|
+
import { buildToolCallHtml } from './tool-call';
|
|
6
|
+
/**
|
|
7
|
+
* The default mime type for the extension.
|
|
8
|
+
*/
|
|
9
|
+
const MIME_TYPE = 'application/vnd.jupyter.chat.components';
|
|
10
|
+
/**
|
|
11
|
+
* The class name added to the extension.
|
|
12
|
+
*/
|
|
13
|
+
const CLASS_NAME = 'jp-RenderedChatComponents';
|
|
14
|
+
/**
|
|
15
|
+
* A widget for rendering .
|
|
16
|
+
*/
|
|
17
|
+
export class ComponentsRenderer extends Widget {
|
|
18
|
+
/**
|
|
19
|
+
* Construct a new output widget.
|
|
20
|
+
*/
|
|
21
|
+
constructor(options) {
|
|
22
|
+
var _a;
|
|
23
|
+
super();
|
|
24
|
+
this._trans = ((_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator).load('jupyterlab');
|
|
25
|
+
this._mimeType = options.mimeType;
|
|
26
|
+
this._toolCallApproval = options.toolCallApproval;
|
|
27
|
+
this.addClass(CLASS_NAME);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Render into this widget's node.
|
|
31
|
+
*/
|
|
32
|
+
renderModel(model) {
|
|
33
|
+
const data = model.data[this._mimeType];
|
|
34
|
+
const metadata = { ...model.metadata };
|
|
35
|
+
if (data === 'tool-call') {
|
|
36
|
+
const toolCallOptions = {
|
|
37
|
+
...metadata,
|
|
38
|
+
trans: this._trans,
|
|
39
|
+
toolCallApproval: this._toolCallApproval
|
|
40
|
+
};
|
|
41
|
+
this.node.appendChild(buildToolCallHtml(toolCallOptions));
|
|
42
|
+
}
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A mime renderer factory for chat components.
|
|
48
|
+
*/
|
|
49
|
+
class RendererFactory {
|
|
50
|
+
constructor() {
|
|
51
|
+
this.safe = true;
|
|
52
|
+
this.mimeTypes = [MIME_TYPE];
|
|
53
|
+
this.defaultRank = 100;
|
|
54
|
+
this.toolCallApproval = null;
|
|
55
|
+
this.createRenderer = (options) => {
|
|
56
|
+
return new ComponentsRenderer({
|
|
57
|
+
...options,
|
|
58
|
+
toolCallApproval: this.toolCallApproval
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const plugin = {
|
|
64
|
+
id: 'jupyter-chat-components:plugin',
|
|
65
|
+
description: 'Adds MIME type renderer for chat components',
|
|
66
|
+
autoStart: true,
|
|
67
|
+
provides: IComponentsRendererFactory,
|
|
68
|
+
requires: [IRenderMimeRegistry],
|
|
69
|
+
activate: (app, rendermime) => {
|
|
70
|
+
const rendererFactory = new RendererFactory();
|
|
71
|
+
rendermime.addFactory(rendererFactory);
|
|
72
|
+
return rendererFactory;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
export * from './token';
|
|
76
|
+
export default plugin;
|
package/lib/token.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
|
|
2
|
+
import { Token } from '@lumino/coreutils';
|
|
3
|
+
/**
|
|
4
|
+
* The token providing the chat components renderer.
|
|
5
|
+
*/
|
|
6
|
+
export declare const IComponentsRendererFactory: Token<IComponentsRendererFactory>;
|
|
7
|
+
/**
|
|
8
|
+
* The callback to approve or reject a tool.
|
|
9
|
+
*/
|
|
10
|
+
export type ToolCallApproval = ((targetId: string, approvalId: string, approve: boolean) => void) | null;
|
|
11
|
+
/**
|
|
12
|
+
* The interface for components renderer factory.
|
|
13
|
+
*/
|
|
14
|
+
export interface IComponentsRendererFactory extends IRenderMime.IRendererFactory {
|
|
15
|
+
/**
|
|
16
|
+
* The callback to approve or reject a tool.
|
|
17
|
+
*/
|
|
18
|
+
toolCallApproval: ToolCallApproval;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Tool call status types.
|
|
22
|
+
*/
|
|
23
|
+
export type ToolCallStatus = 'pending' | 'awaiting_approval' | 'approved' | 'rejected' | 'completed' | 'error';
|
|
24
|
+
/**
|
|
25
|
+
* Options for building tool call HTML.
|
|
26
|
+
*/
|
|
27
|
+
export interface IToolCallMetadata {
|
|
28
|
+
toolName: string;
|
|
29
|
+
input: string;
|
|
30
|
+
status: ToolCallStatus;
|
|
31
|
+
summary?: string;
|
|
32
|
+
output?: string;
|
|
33
|
+
targetId?: string;
|
|
34
|
+
approvalId?: string;
|
|
35
|
+
}
|
package/lib/token.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
2
|
+
import { IToolCallMetadata, ToolCallApproval } from './token';
|
|
3
|
+
/**
|
|
4
|
+
* Options for building tool call HTML.
|
|
5
|
+
*/
|
|
6
|
+
export interface IToolCallHtmlOptions extends IToolCallMetadata {
|
|
7
|
+
trans: TranslationBundle;
|
|
8
|
+
toolCallApproval: ToolCallApproval;
|
|
9
|
+
}
|
|
10
|
+
export declare function escapeHtml(value: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Builds HTML for a tool call display.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildToolCallHtml(options: IToolCallHtmlOptions): HTMLDetailsElement;
|
package/lib/tool-call.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const STATUS_CONFIG = {
|
|
2
|
+
pending: {
|
|
3
|
+
cssClass: 'jp-ai-tool-pending',
|
|
4
|
+
statusClass: 'jp-ai-tool-status-pending'
|
|
5
|
+
},
|
|
6
|
+
awaiting_approval: {
|
|
7
|
+
cssClass: 'jp-ai-tool-pending',
|
|
8
|
+
statusClass: 'jp-ai-tool-status-approval',
|
|
9
|
+
open: true
|
|
10
|
+
},
|
|
11
|
+
approved: {
|
|
12
|
+
cssClass: 'jp-ai-tool-pending',
|
|
13
|
+
statusClass: 'jp-ai-tool-status-completed'
|
|
14
|
+
},
|
|
15
|
+
rejected: {
|
|
16
|
+
cssClass: 'jp-ai-tool-error',
|
|
17
|
+
statusClass: 'jp-ai-tool-status-error'
|
|
18
|
+
},
|
|
19
|
+
completed: {
|
|
20
|
+
cssClass: 'jp-ai-tool-completed',
|
|
21
|
+
statusClass: 'jp-ai-tool-status-completed'
|
|
22
|
+
},
|
|
23
|
+
error: {
|
|
24
|
+
cssClass: 'jp-ai-tool-error',
|
|
25
|
+
statusClass: 'jp-ai-tool-status-error'
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export function escapeHtml(value) {
|
|
29
|
+
// Prefer the same native escaping approach used in JupyterLab itself
|
|
30
|
+
// (e.g. `@jupyterlab/completer`).
|
|
31
|
+
if (typeof document !== 'undefined') {
|
|
32
|
+
const node = document.createElement('span');
|
|
33
|
+
node.textContent = value;
|
|
34
|
+
return node.innerHTML;
|
|
35
|
+
}
|
|
36
|
+
// Fallback
|
|
37
|
+
return value
|
|
38
|
+
.replace(/&/g, '&')
|
|
39
|
+
.replace(/</g, '<')
|
|
40
|
+
.replace(/>/g, '>')
|
|
41
|
+
.replace(/"/g, '"')
|
|
42
|
+
.replace(/'/g, ''');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns the translated status text for a given tool status.
|
|
46
|
+
*/
|
|
47
|
+
const getStatusText = (status, trans) => {
|
|
48
|
+
switch (status) {
|
|
49
|
+
case 'pending':
|
|
50
|
+
return trans.__('Running...');
|
|
51
|
+
case 'awaiting_approval':
|
|
52
|
+
return trans.__('Awaiting Approval');
|
|
53
|
+
case 'approved':
|
|
54
|
+
return trans.__('Approved - Executing...');
|
|
55
|
+
case 'rejected':
|
|
56
|
+
return trans.__('Rejected');
|
|
57
|
+
case 'completed':
|
|
58
|
+
return trans.__('Completed');
|
|
59
|
+
case 'error':
|
|
60
|
+
return trans.__('Error');
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Builds HTML for a tool call display.
|
|
65
|
+
*/
|
|
66
|
+
export function buildToolCallHtml(options) {
|
|
67
|
+
const { toolName, input, status, summary, output, targetId, approvalId, trans } = options;
|
|
68
|
+
const config = STATUS_CONFIG[status];
|
|
69
|
+
const statusText = getStatusText(status, trans);
|
|
70
|
+
const escapedToolName = escapeHtml(toolName);
|
|
71
|
+
const escapedInput = escapeHtml(input);
|
|
72
|
+
const details = document.createElement('details');
|
|
73
|
+
details.classList.add('jp-ai-tool-call', config.cssClass);
|
|
74
|
+
if (config.open) {
|
|
75
|
+
details.setAttribute('open', 'true');
|
|
76
|
+
}
|
|
77
|
+
const summaryElement = document.createElement('summary');
|
|
78
|
+
summaryElement.classList.add('jp-ai-tool-header');
|
|
79
|
+
// Build summary header
|
|
80
|
+
const icon = document.createElement('div');
|
|
81
|
+
icon.classList.add('jp-ai-tool-icon');
|
|
82
|
+
icon.textContent = '⚡';
|
|
83
|
+
const title = document.createElement('div');
|
|
84
|
+
title.classList.add('jp-ai-tool-title');
|
|
85
|
+
title.textContent = escapedToolName;
|
|
86
|
+
if (summary) {
|
|
87
|
+
const summarySpan = document.createElement('span');
|
|
88
|
+
summarySpan.classList.add('jp-ai-tool-summary');
|
|
89
|
+
summarySpan.textContent = summary;
|
|
90
|
+
title.appendChild(summarySpan);
|
|
91
|
+
}
|
|
92
|
+
const statusDiv = document.createElement('div');
|
|
93
|
+
statusDiv.classList.add('jp-ai-tool-status', config.statusClass);
|
|
94
|
+
statusDiv.textContent = statusText;
|
|
95
|
+
summaryElement.appendChild(icon);
|
|
96
|
+
summaryElement.appendChild(title);
|
|
97
|
+
summaryElement.appendChild(statusDiv);
|
|
98
|
+
// Build body
|
|
99
|
+
const body = document.createElement('div');
|
|
100
|
+
body.classList.add('jp-ai-tool-body');
|
|
101
|
+
// Add input section
|
|
102
|
+
const inputSection = document.createElement('div');
|
|
103
|
+
inputSection.classList.add('jp-ai-tool-section');
|
|
104
|
+
const inputLabel = document.createElement('div');
|
|
105
|
+
inputLabel.classList.add('jp-ai-tool-label');
|
|
106
|
+
inputLabel.textContent = trans.__('Input');
|
|
107
|
+
const inputPre = document.createElement('pre');
|
|
108
|
+
inputPre.classList.add('jp-ai-tool-code');
|
|
109
|
+
const inputCode = document.createElement('code');
|
|
110
|
+
inputCode.textContent = escapedInput;
|
|
111
|
+
inputPre.appendChild(inputCode);
|
|
112
|
+
inputSection.appendChild(inputLabel);
|
|
113
|
+
inputSection.appendChild(inputPre);
|
|
114
|
+
body.appendChild(inputSection);
|
|
115
|
+
// Add approval buttons if awaiting approval
|
|
116
|
+
if (status === 'awaiting_approval' && approvalId && targetId) {
|
|
117
|
+
const approvalButtonsDiv = document.createElement('div');
|
|
118
|
+
approvalButtonsDiv.classList.add('jp-ai-tool-approval-buttons', `jp-ai-approval-id--${approvalId}`);
|
|
119
|
+
const approveBtn = document.createElement('button');
|
|
120
|
+
approveBtn.classList.add('jp-ai-approval-btn', 'jp-ai-approval-approve');
|
|
121
|
+
approveBtn.textContent = trans.__('Approve');
|
|
122
|
+
const rejectBtn = document.createElement('button');
|
|
123
|
+
rejectBtn.classList.add('jp-ai-approval-btn', 'jp-ai-approval-reject');
|
|
124
|
+
rejectBtn.textContent = trans.__('Reject');
|
|
125
|
+
approvalButtonsDiv.appendChild(approveBtn);
|
|
126
|
+
approvalButtonsDiv.appendChild(rejectBtn);
|
|
127
|
+
body.appendChild(approvalButtonsDiv);
|
|
128
|
+
approveBtn.addEventListener('click', () => { var _a; return (_a = options.toolCallApproval) === null || _a === void 0 ? void 0 : _a.call(options, targetId, approvalId, true); });
|
|
129
|
+
rejectBtn.addEventListener('click', () => { var _a; return (_a = options.toolCallApproval) === null || _a === void 0 ? void 0 : _a.call(options, targetId, approvalId, false); });
|
|
130
|
+
}
|
|
131
|
+
// Add output/result section if provided
|
|
132
|
+
if (output !== undefined) {
|
|
133
|
+
const escapedOutput = escapeHtml(output);
|
|
134
|
+
const label = status === 'error' ? trans.__('Error') : trans.__('Result');
|
|
135
|
+
const outputSection = document.createElement('div');
|
|
136
|
+
outputSection.classList.add('jp-ai-tool-section');
|
|
137
|
+
const outputLabel = document.createElement('div');
|
|
138
|
+
outputLabel.classList.add('jp-ai-tool-label');
|
|
139
|
+
outputLabel.textContent = label;
|
|
140
|
+
const outputPre = document.createElement('pre');
|
|
141
|
+
outputPre.classList.add('jp-ai-tool-code');
|
|
142
|
+
const outputCode = document.createElement('code');
|
|
143
|
+
outputCode.textContent = escapedOutput;
|
|
144
|
+
outputPre.appendChild(outputCode);
|
|
145
|
+
outputSection.appendChild(outputLabel);
|
|
146
|
+
outputSection.appendChild(outputPre);
|
|
147
|
+
body.appendChild(outputSection);
|
|
148
|
+
}
|
|
149
|
+
details.appendChild(summaryElement);
|
|
150
|
+
details.appendChild(body);
|
|
151
|
+
return details;
|
|
152
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jupyter-chat-components",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Components to displayed in jupyter chat",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jupyter",
|
|
7
|
+
"jupyterlab",
|
|
8
|
+
"jupyterlab-extension"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/brichet/jupyter-chat-components",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/brichet/jupyter-chat-components/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "BSD-3-Clause",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Project Jupyter",
|
|
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/brichet/jupyter-chat-components.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "jlpm build:lib && jlpm build:labextension:dev",
|
|
33
|
+
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
34
|
+
"build:labextension": "jupyter labextension build .",
|
|
35
|
+
"build:labextension:dev": "jupyter labextension build --development True .",
|
|
36
|
+
"build:lib": "tsc --sourceMap",
|
|
37
|
+
"build:lib:prod": "tsc",
|
|
38
|
+
"clean": "jlpm clean:lib",
|
|
39
|
+
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
40
|
+
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|
|
41
|
+
"clean:labextension": "rimraf jupyter_chat_components/labextension jupyter_chat_components/_version.py",
|
|
42
|
+
"clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
|
|
43
|
+
"eslint": "jlpm eslint:check --fix",
|
|
44
|
+
"eslint:check": "eslint . --cache --ext .ts,.tsx",
|
|
45
|
+
"install:extension": "jlpm build",
|
|
46
|
+
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
47
|
+
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
48
|
+
"prettier": "jlpm prettier:base --write --list-different",
|
|
49
|
+
"prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
|
|
50
|
+
"prettier:check": "jlpm prettier:base --check",
|
|
51
|
+
"stylelint": "jlpm stylelint:check --fix",
|
|
52
|
+
"stylelint:check": "stylelint --cache \"style/**/*.css\"",
|
|
53
|
+
"test": "jest --coverage",
|
|
54
|
+
"watch": "run-p watch:src watch:labextension",
|
|
55
|
+
"watch:src": "tsc -w --sourceMap",
|
|
56
|
+
"watch:labextension": "jupyter labextension watch ."
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@jupyterlab/application": "^4.5.0",
|
|
60
|
+
"@jupyterlab/rendermime": "^4.5.0",
|
|
61
|
+
"@jupyterlab/rendermime-interfaces": "^3.8.0",
|
|
62
|
+
"@jupyterlab/translation": "^4.5.0",
|
|
63
|
+
"@lumino/coreutils": "^2.2.2",
|
|
64
|
+
"@lumino/widgets": "^2.1.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@jupyterlab/builder": "^4.0.0",
|
|
68
|
+
"@jupyterlab/testutils": "^4.0.0",
|
|
69
|
+
"@types/jest": "^29.2.0",
|
|
70
|
+
"@types/json-schema": "^7.0.11",
|
|
71
|
+
"@types/react": "^18.0.26",
|
|
72
|
+
"@types/react-addons-linked-state-mixin": "^0.14.22",
|
|
73
|
+
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
74
|
+
"@typescript-eslint/parser": "^6.1.0",
|
|
75
|
+
"css-loader": "^6.7.1",
|
|
76
|
+
"eslint": "^8.36.0",
|
|
77
|
+
"eslint-config-prettier": "^8.8.0",
|
|
78
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
79
|
+
"jest": "^29.2.0",
|
|
80
|
+
"npm-run-all2": "^7.0.1",
|
|
81
|
+
"prettier": "^3.0.0",
|
|
82
|
+
"rimraf": "^5.0.1",
|
|
83
|
+
"source-map-loader": "^1.0.2",
|
|
84
|
+
"style-loader": "^3.3.1",
|
|
85
|
+
"stylelint": "^15.10.1",
|
|
86
|
+
"stylelint-config-recommended": "^13.0.0",
|
|
87
|
+
"stylelint-config-standard": "^34.0.0",
|
|
88
|
+
"stylelint-csstree-validator": "^3.0.0",
|
|
89
|
+
"stylelint-prettier": "^4.0.0",
|
|
90
|
+
"typescript": "~5.5.4",
|
|
91
|
+
"yjs": "^13.5.0"
|
|
92
|
+
},
|
|
93
|
+
"resolutions": {
|
|
94
|
+
"lib0": "0.2.111"
|
|
95
|
+
},
|
|
96
|
+
"sideEffects": [
|
|
97
|
+
"style/*.css",
|
|
98
|
+
"style/index.js"
|
|
99
|
+
],
|
|
100
|
+
"styleModule": "style/index.js",
|
|
101
|
+
"publishConfig": {
|
|
102
|
+
"access": "public"
|
|
103
|
+
},
|
|
104
|
+
"jupyterlab": {
|
|
105
|
+
"extension": true,
|
|
106
|
+
"outputDir": "jupyter_chat_components/labextension"
|
|
107
|
+
},
|
|
108
|
+
"eslintIgnore": [
|
|
109
|
+
"node_modules",
|
|
110
|
+
"dist",
|
|
111
|
+
"coverage",
|
|
112
|
+
"**/*.d.ts",
|
|
113
|
+
"tests",
|
|
114
|
+
"**/__tests__",
|
|
115
|
+
"ui-tests"
|
|
116
|
+
],
|
|
117
|
+
"eslintConfig": {
|
|
118
|
+
"extends": [
|
|
119
|
+
"eslint:recommended",
|
|
120
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
121
|
+
"plugin:@typescript-eslint/recommended",
|
|
122
|
+
"plugin:prettier/recommended"
|
|
123
|
+
],
|
|
124
|
+
"parser": "@typescript-eslint/parser",
|
|
125
|
+
"parserOptions": {
|
|
126
|
+
"project": "tsconfig.json",
|
|
127
|
+
"sourceType": "module"
|
|
128
|
+
},
|
|
129
|
+
"plugins": [
|
|
130
|
+
"@typescript-eslint"
|
|
131
|
+
],
|
|
132
|
+
"rules": {
|
|
133
|
+
"@typescript-eslint/naming-convention": [
|
|
134
|
+
"error",
|
|
135
|
+
{
|
|
136
|
+
"selector": "interface",
|
|
137
|
+
"format": [
|
|
138
|
+
"PascalCase"
|
|
139
|
+
],
|
|
140
|
+
"custom": {
|
|
141
|
+
"regex": "^I[A-Z]",
|
|
142
|
+
"match": true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"@typescript-eslint/no-unused-vars": [
|
|
147
|
+
"warn",
|
|
148
|
+
{
|
|
149
|
+
"args": "none"
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
153
|
+
"@typescript-eslint/no-namespace": "off",
|
|
154
|
+
"@typescript-eslint/no-use-before-define": "off",
|
|
155
|
+
"@typescript-eslint/quotes": [
|
|
156
|
+
"error",
|
|
157
|
+
"single",
|
|
158
|
+
{
|
|
159
|
+
"avoidEscape": true,
|
|
160
|
+
"allowTemplateLiterals": false
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"curly": [
|
|
164
|
+
"error",
|
|
165
|
+
"all"
|
|
166
|
+
],
|
|
167
|
+
"eqeqeq": "error",
|
|
168
|
+
"prefer-arrow-callback": "error"
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"prettier": {
|
|
172
|
+
"singleQuote": true,
|
|
173
|
+
"trailingComma": "none",
|
|
174
|
+
"arrowParens": "avoid",
|
|
175
|
+
"endOfLine": "auto",
|
|
176
|
+
"overrides": [
|
|
177
|
+
{
|
|
178
|
+
"files": "package.json",
|
|
179
|
+
"options": {
|
|
180
|
+
"tabWidth": 4
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
"stylelint": {
|
|
186
|
+
"extends": [
|
|
187
|
+
"stylelint-config-recommended",
|
|
188
|
+
"stylelint-config-standard",
|
|
189
|
+
"stylelint-prettier/recommended"
|
|
190
|
+
],
|
|
191
|
+
"plugins": [
|
|
192
|
+
"stylelint-csstree-validator"
|
|
193
|
+
],
|
|
194
|
+
"rules": {
|
|
195
|
+
"csstree/validator": true,
|
|
196
|
+
"property-no-vendor-prefix": null,
|
|
197
|
+
"selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
|
|
198
|
+
"selector-no-vendor-prefix": null,
|
|
199
|
+
"value-no-vendor-prefix": null
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
|
|
6
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
7
|
+
|
|
8
|
+
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
|
|
9
|
+
|
|
10
|
+
import { nullTranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
11
|
+
|
|
12
|
+
import { Widget } from '@lumino/widgets';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
IComponentsRendererFactory,
|
|
16
|
+
IToolCallMetadata,
|
|
17
|
+
ToolCallApproval
|
|
18
|
+
} from './token';
|
|
19
|
+
|
|
20
|
+
import { buildToolCallHtml, IToolCallHtmlOptions } from './tool-call';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The default mime type for the extension.
|
|
24
|
+
*/
|
|
25
|
+
const MIME_TYPE = 'application/vnd.jupyter.chat.components';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The class name added to the extension.
|
|
29
|
+
*/
|
|
30
|
+
const CLASS_NAME = 'jp-RenderedChatComponents';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The options for the chat components renderer.
|
|
34
|
+
*/
|
|
35
|
+
interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
|
|
36
|
+
/**
|
|
37
|
+
* The callback to approve or reject a tool.
|
|
38
|
+
*/
|
|
39
|
+
toolCallApproval: ToolCallApproval;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A widget for rendering .
|
|
44
|
+
*/
|
|
45
|
+
export class ComponentsRenderer
|
|
46
|
+
extends Widget
|
|
47
|
+
implements IRenderMime.IRenderer
|
|
48
|
+
{
|
|
49
|
+
/**
|
|
50
|
+
* Construct a new output widget.
|
|
51
|
+
*/
|
|
52
|
+
constructor(options: IComponentsRendererOptions) {
|
|
53
|
+
super();
|
|
54
|
+
this._trans = (options.translator ?? nullTranslator).load('jupyterlab');
|
|
55
|
+
this._mimeType = options.mimeType;
|
|
56
|
+
this._toolCallApproval = options.toolCallApproval;
|
|
57
|
+
this.addClass(CLASS_NAME);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Render into this widget's node.
|
|
62
|
+
*/
|
|
63
|
+
renderModel(model: IRenderMime.IMimeModel): Promise<void> {
|
|
64
|
+
const data = model.data[this._mimeType] as string;
|
|
65
|
+
const metadata = { ...model.metadata };
|
|
66
|
+
if (data === 'tool-call') {
|
|
67
|
+
const toolCallOptions: IToolCallHtmlOptions = {
|
|
68
|
+
...(metadata as unknown as IToolCallMetadata),
|
|
69
|
+
trans: this._trans,
|
|
70
|
+
toolCallApproval: this._toolCallApproval
|
|
71
|
+
};
|
|
72
|
+
this.node.appendChild(buildToolCallHtml(toolCallOptions));
|
|
73
|
+
}
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private _trans: TranslationBundle;
|
|
78
|
+
private _mimeType: string;
|
|
79
|
+
private _toolCallApproval: ToolCallApproval;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A mime renderer factory for chat components.
|
|
84
|
+
*/
|
|
85
|
+
class RendererFactory implements IComponentsRendererFactory {
|
|
86
|
+
readonly safe = true;
|
|
87
|
+
readonly mimeTypes = [MIME_TYPE];
|
|
88
|
+
readonly defaultRank = 100;
|
|
89
|
+
toolCallApproval: ToolCallApproval = null;
|
|
90
|
+
createRenderer = (options: IRenderMime.IRendererOptions) => {
|
|
91
|
+
return new ComponentsRenderer({
|
|
92
|
+
...options,
|
|
93
|
+
toolCallApproval: this.toolCallApproval
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const plugin: JupyterFrontEndPlugin<IComponentsRendererFactory> = {
|
|
99
|
+
id: 'jupyter-chat-components:plugin',
|
|
100
|
+
description: 'Adds MIME type renderer for chat components',
|
|
101
|
+
autoStart: true,
|
|
102
|
+
provides: IComponentsRendererFactory,
|
|
103
|
+
requires: [IRenderMimeRegistry],
|
|
104
|
+
activate: (
|
|
105
|
+
app: JupyterFrontEnd,
|
|
106
|
+
rendermime: IRenderMimeRegistry
|
|
107
|
+
): IComponentsRendererFactory => {
|
|
108
|
+
const rendererFactory = new RendererFactory();
|
|
109
|
+
rendermime.addFactory(rendererFactory);
|
|
110
|
+
return rendererFactory;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export * from './token';
|
|
115
|
+
export default plugin;
|
package/src/token.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
|
|
2
|
+
|
|
3
|
+
import { Token } from '@lumino/coreutils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The token providing the chat components renderer.
|
|
7
|
+
*/
|
|
8
|
+
export const IComponentsRendererFactory = new Token<IComponentsRendererFactory>(
|
|
9
|
+
'jupyter-chat-components:IComponentsRendererFactory',
|
|
10
|
+
'The chat components renderer factory'
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The callback to approve or reject a tool.
|
|
15
|
+
*/
|
|
16
|
+
export type ToolCallApproval =
|
|
17
|
+
| ((targetId: string, approvalId: string, approve: boolean) => void)
|
|
18
|
+
| null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The interface for components renderer factory.
|
|
22
|
+
*/
|
|
23
|
+
export interface IComponentsRendererFactory
|
|
24
|
+
extends IRenderMime.IRendererFactory {
|
|
25
|
+
/**
|
|
26
|
+
* The callback to approve or reject a tool.
|
|
27
|
+
*/
|
|
28
|
+
toolCallApproval: ToolCallApproval;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tool call status types.
|
|
33
|
+
*/
|
|
34
|
+
export type ToolCallStatus =
|
|
35
|
+
| 'pending'
|
|
36
|
+
| 'awaiting_approval'
|
|
37
|
+
| 'approved'
|
|
38
|
+
| 'rejected'
|
|
39
|
+
| 'completed'
|
|
40
|
+
| 'error';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for building tool call HTML.
|
|
44
|
+
*/
|
|
45
|
+
export interface IToolCallMetadata {
|
|
46
|
+
toolName: string;
|
|
47
|
+
input: string;
|
|
48
|
+
status: ToolCallStatus;
|
|
49
|
+
summary?: string;
|
|
50
|
+
output?: string;
|
|
51
|
+
targetId?: string;
|
|
52
|
+
approvalId?: string;
|
|
53
|
+
}
|
package/src/tool-call.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
2
|
+
|
|
3
|
+
import { IToolCallMetadata, ToolCallApproval, ToolCallStatus } from './token';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for rendering tool call status.
|
|
7
|
+
*/
|
|
8
|
+
interface IStatusConfig {
|
|
9
|
+
cssClass: string;
|
|
10
|
+
statusClass: string;
|
|
11
|
+
open?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const STATUS_CONFIG: Record<ToolCallStatus, IStatusConfig> = {
|
|
15
|
+
pending: {
|
|
16
|
+
cssClass: 'jp-ai-tool-pending',
|
|
17
|
+
statusClass: 'jp-ai-tool-status-pending'
|
|
18
|
+
},
|
|
19
|
+
awaiting_approval: {
|
|
20
|
+
cssClass: 'jp-ai-tool-pending',
|
|
21
|
+
statusClass: 'jp-ai-tool-status-approval',
|
|
22
|
+
open: true
|
|
23
|
+
},
|
|
24
|
+
approved: {
|
|
25
|
+
cssClass: 'jp-ai-tool-pending',
|
|
26
|
+
statusClass: 'jp-ai-tool-status-completed'
|
|
27
|
+
},
|
|
28
|
+
rejected: {
|
|
29
|
+
cssClass: 'jp-ai-tool-error',
|
|
30
|
+
statusClass: 'jp-ai-tool-status-error'
|
|
31
|
+
},
|
|
32
|
+
completed: {
|
|
33
|
+
cssClass: 'jp-ai-tool-completed',
|
|
34
|
+
statusClass: 'jp-ai-tool-status-completed'
|
|
35
|
+
},
|
|
36
|
+
error: {
|
|
37
|
+
cssClass: 'jp-ai-tool-error',
|
|
38
|
+
statusClass: 'jp-ai-tool-status-error'
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for building tool call HTML.
|
|
44
|
+
*/
|
|
45
|
+
export interface IToolCallHtmlOptions extends IToolCallMetadata {
|
|
46
|
+
trans: TranslationBundle;
|
|
47
|
+
toolCallApproval: ToolCallApproval;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function escapeHtml(value: string): string {
|
|
51
|
+
// Prefer the same native escaping approach used in JupyterLab itself
|
|
52
|
+
// (e.g. `@jupyterlab/completer`).
|
|
53
|
+
if (typeof document !== 'undefined') {
|
|
54
|
+
const node = document.createElement('span');
|
|
55
|
+
node.textContent = value;
|
|
56
|
+
return node.innerHTML;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback
|
|
60
|
+
return value
|
|
61
|
+
.replace(/&/g, '&')
|
|
62
|
+
.replace(/</g, '<')
|
|
63
|
+
.replace(/>/g, '>')
|
|
64
|
+
.replace(/"/g, '"')
|
|
65
|
+
.replace(/'/g, ''');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns the translated status text for a given tool status.
|
|
70
|
+
*/
|
|
71
|
+
const getStatusText = (
|
|
72
|
+
status: ToolCallStatus,
|
|
73
|
+
trans: TranslationBundle
|
|
74
|
+
): string => {
|
|
75
|
+
switch (status) {
|
|
76
|
+
case 'pending':
|
|
77
|
+
return trans.__('Running...');
|
|
78
|
+
case 'awaiting_approval':
|
|
79
|
+
return trans.__('Awaiting Approval');
|
|
80
|
+
case 'approved':
|
|
81
|
+
return trans.__('Approved - Executing...');
|
|
82
|
+
case 'rejected':
|
|
83
|
+
return trans.__('Rejected');
|
|
84
|
+
case 'completed':
|
|
85
|
+
return trans.__('Completed');
|
|
86
|
+
case 'error':
|
|
87
|
+
return trans.__('Error');
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Builds HTML for a tool call display.
|
|
93
|
+
*/
|
|
94
|
+
export function buildToolCallHtml(
|
|
95
|
+
options: IToolCallHtmlOptions
|
|
96
|
+
): HTMLDetailsElement {
|
|
97
|
+
const {
|
|
98
|
+
toolName,
|
|
99
|
+
input,
|
|
100
|
+
status,
|
|
101
|
+
summary,
|
|
102
|
+
output,
|
|
103
|
+
targetId,
|
|
104
|
+
approvalId,
|
|
105
|
+
trans
|
|
106
|
+
} = options;
|
|
107
|
+
const config = STATUS_CONFIG[status];
|
|
108
|
+
const statusText = getStatusText(status, trans);
|
|
109
|
+
const escapedToolName = escapeHtml(toolName);
|
|
110
|
+
const escapedInput = escapeHtml(input);
|
|
111
|
+
|
|
112
|
+
const details = document.createElement('details');
|
|
113
|
+
details.classList.add('jp-ai-tool-call', config.cssClass);
|
|
114
|
+
if (config.open) {
|
|
115
|
+
details.setAttribute('open', 'true');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const summaryElement = document.createElement('summary');
|
|
119
|
+
summaryElement.classList.add('jp-ai-tool-header');
|
|
120
|
+
|
|
121
|
+
// Build summary header
|
|
122
|
+
const icon = document.createElement('div');
|
|
123
|
+
icon.classList.add('jp-ai-tool-icon');
|
|
124
|
+
icon.textContent = '⚡';
|
|
125
|
+
|
|
126
|
+
const title = document.createElement('div');
|
|
127
|
+
title.classList.add('jp-ai-tool-title');
|
|
128
|
+
title.textContent = escapedToolName;
|
|
129
|
+
|
|
130
|
+
if (summary) {
|
|
131
|
+
const summarySpan = document.createElement('span');
|
|
132
|
+
summarySpan.classList.add('jp-ai-tool-summary');
|
|
133
|
+
summarySpan.textContent = summary;
|
|
134
|
+
title.appendChild(summarySpan);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const statusDiv = document.createElement('div');
|
|
138
|
+
statusDiv.classList.add('jp-ai-tool-status', config.statusClass);
|
|
139
|
+
statusDiv.textContent = statusText;
|
|
140
|
+
|
|
141
|
+
summaryElement.appendChild(icon);
|
|
142
|
+
summaryElement.appendChild(title);
|
|
143
|
+
summaryElement.appendChild(statusDiv);
|
|
144
|
+
|
|
145
|
+
// Build body
|
|
146
|
+
const body = document.createElement('div');
|
|
147
|
+
body.classList.add('jp-ai-tool-body');
|
|
148
|
+
|
|
149
|
+
// Add input section
|
|
150
|
+
const inputSection = document.createElement('div');
|
|
151
|
+
inputSection.classList.add('jp-ai-tool-section');
|
|
152
|
+
|
|
153
|
+
const inputLabel = document.createElement('div');
|
|
154
|
+
inputLabel.classList.add('jp-ai-tool-label');
|
|
155
|
+
inputLabel.textContent = trans.__('Input');
|
|
156
|
+
|
|
157
|
+
const inputPre = document.createElement('pre');
|
|
158
|
+
inputPre.classList.add('jp-ai-tool-code');
|
|
159
|
+
|
|
160
|
+
const inputCode = document.createElement('code');
|
|
161
|
+
inputCode.textContent = escapedInput;
|
|
162
|
+
|
|
163
|
+
inputPre.appendChild(inputCode);
|
|
164
|
+
inputSection.appendChild(inputLabel);
|
|
165
|
+
inputSection.appendChild(inputPre);
|
|
166
|
+
body.appendChild(inputSection);
|
|
167
|
+
|
|
168
|
+
// Add approval buttons if awaiting approval
|
|
169
|
+
if (status === 'awaiting_approval' && approvalId && targetId) {
|
|
170
|
+
const approvalButtonsDiv = document.createElement('div');
|
|
171
|
+
approvalButtonsDiv.classList.add(
|
|
172
|
+
'jp-ai-tool-approval-buttons',
|
|
173
|
+
`jp-ai-approval-id--${approvalId}`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const approveBtn = document.createElement('button');
|
|
177
|
+
approveBtn.classList.add('jp-ai-approval-btn', 'jp-ai-approval-approve');
|
|
178
|
+
approveBtn.textContent = trans.__('Approve');
|
|
179
|
+
|
|
180
|
+
const rejectBtn = document.createElement('button');
|
|
181
|
+
rejectBtn.classList.add('jp-ai-approval-btn', 'jp-ai-approval-reject');
|
|
182
|
+
rejectBtn.textContent = trans.__('Reject');
|
|
183
|
+
|
|
184
|
+
approvalButtonsDiv.appendChild(approveBtn);
|
|
185
|
+
approvalButtonsDiv.appendChild(rejectBtn);
|
|
186
|
+
body.appendChild(approvalButtonsDiv);
|
|
187
|
+
|
|
188
|
+
approveBtn.addEventListener('click', () =>
|
|
189
|
+
options.toolCallApproval?.(targetId, approvalId, true)
|
|
190
|
+
);
|
|
191
|
+
rejectBtn.addEventListener('click', () =>
|
|
192
|
+
options.toolCallApproval?.(targetId, approvalId, false)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Add output/result section if provided
|
|
197
|
+
if (output !== undefined) {
|
|
198
|
+
const escapedOutput = escapeHtml(output);
|
|
199
|
+
const label = status === 'error' ? trans.__('Error') : trans.__('Result');
|
|
200
|
+
|
|
201
|
+
const outputSection = document.createElement('div');
|
|
202
|
+
outputSection.classList.add('jp-ai-tool-section');
|
|
203
|
+
|
|
204
|
+
const outputLabel = document.createElement('div');
|
|
205
|
+
outputLabel.classList.add('jp-ai-tool-label');
|
|
206
|
+
outputLabel.textContent = label;
|
|
207
|
+
|
|
208
|
+
const outputPre = document.createElement('pre');
|
|
209
|
+
outputPre.classList.add('jp-ai-tool-code');
|
|
210
|
+
|
|
211
|
+
const outputCode = document.createElement('code');
|
|
212
|
+
outputCode.textContent = escapedOutput;
|
|
213
|
+
|
|
214
|
+
outputPre.appendChild(outputCode);
|
|
215
|
+
outputSection.appendChild(outputLabel);
|
|
216
|
+
outputSection.appendChild(outputPre);
|
|
217
|
+
body.appendChild(outputSection);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
details.appendChild(summaryElement);
|
|
221
|
+
details.appendChild(body);
|
|
222
|
+
|
|
223
|
+
return details;
|
|
224
|
+
}
|
package/style/base.css
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/*
|
|
2
|
+
See the JupyterLab Developer Guide for useful CSS Patterns:
|
|
3
|
+
|
|
4
|
+
https://jupyterlab.readthedocs.io/en/stable/developer/css.html
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* Modern Tool Call Card Styling */
|
|
8
|
+
.jp-ai-tool-call {
|
|
9
|
+
margin: 8px 0;
|
|
10
|
+
border: 1px solid var(--jp-border-color1);
|
|
11
|
+
border-radius: 6px;
|
|
12
|
+
background: var(--jp-layout-color0);
|
|
13
|
+
box-shadow: var(--jp-elevation-z2);
|
|
14
|
+
transition: all 0.2s ease;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.jp-ai-tool-call:hover {
|
|
19
|
+
border-color: var(--jp-border-color2);
|
|
20
|
+
box-shadow: var(--jp-elevation-z4);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Tool Header - clickable summary */
|
|
24
|
+
.jp-ai-tool-header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
padding: 4px 8px;
|
|
28
|
+
background: var(--jp-layout-color1);
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
user-select: none;
|
|
31
|
+
gap: 8px;
|
|
32
|
+
transition: background-color 0.2s ease;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.jp-ai-tool-header:hover {
|
|
36
|
+
background: var(--jp-layout-color2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.jp-ai-tool-header::before {
|
|
40
|
+
content: '';
|
|
41
|
+
width: 0;
|
|
42
|
+
height: 0;
|
|
43
|
+
border-left: 5px solid var(--jp-ui-font-color2);
|
|
44
|
+
border-top: 3px solid transparent;
|
|
45
|
+
border-bottom: 3px solid transparent;
|
|
46
|
+
transition: transform 0.2s ease;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.jp-ai-tool-call[open] .jp-ai-tool-header::before {
|
|
50
|
+
transform: rotate(90deg);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.jp-ai-tool-icon {
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
opacity: 0.8;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.jp-ai-tool-title {
|
|
59
|
+
font-family: var(--jp-ui-font-family);
|
|
60
|
+
font-size: var(--jp-ui-font-size1);
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
color: var(--jp-ui-font-color1);
|
|
63
|
+
flex: 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.jp-ai-tool-summary {
|
|
67
|
+
font-weight: 400;
|
|
68
|
+
opacity: 0.7;
|
|
69
|
+
font-size: var(--jp-ui-font-size0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.jp-ai-tool-summary::before {
|
|
73
|
+
content: ' ';
|
|
74
|
+
white-space: pre;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.jp-ai-tool-status {
|
|
78
|
+
font-size: var(--jp-ui-font-size0);
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
padding: 2px 6px;
|
|
81
|
+
border-radius: 3px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.jp-ai-tool-status-pending {
|
|
85
|
+
background: rgb(var(--jp-warn-color1-rgb) / 15%);
|
|
86
|
+
color: var(--jp-warn-color1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.jp-ai-tool-status-completed {
|
|
90
|
+
background: rgb(var(--jp-success-color1-rgb) / 15%);
|
|
91
|
+
color: var(--jp-success-color1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.jp-ai-tool-status-error {
|
|
95
|
+
background: rgb(var(--jp-error-color1-rgb) / 15%);
|
|
96
|
+
color: var(--jp-error-color1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.jp-ai-tool-status-approval {
|
|
100
|
+
background: rgb(var(--jp-warn-color1-rgb) / 15%);
|
|
101
|
+
color: var(--jp-warn-color1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Tool Body */
|
|
105
|
+
.jp-ai-tool-body {
|
|
106
|
+
padding: 8px 12px 12px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.jp-ai-tool-section {
|
|
110
|
+
margin-bottom: 8px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.jp-ai-tool-section:last-child {
|
|
114
|
+
margin-bottom: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.jp-ai-tool-label {
|
|
118
|
+
font-family: var(--jp-ui-font-family);
|
|
119
|
+
font-size: var(--jp-ui-font-size0);
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
color: var(--jp-ui-font-color2);
|
|
122
|
+
margin-bottom: 4px;
|
|
123
|
+
text-transform: uppercase;
|
|
124
|
+
letter-spacing: 0.5px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.jp-ai-tool-code {
|
|
128
|
+
background: var(--jp-layout-color2);
|
|
129
|
+
border: 1px solid var(--jp-border-color1);
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
padding: 8px;
|
|
132
|
+
margin: 0;
|
|
133
|
+
font-family: var(--jp-code-font-family);
|
|
134
|
+
font-size: var(--jp-code-font-size);
|
|
135
|
+
line-height: 1.4;
|
|
136
|
+
overflow: auto auto;
|
|
137
|
+
max-height: 200px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.jp-ai-tool-code code {
|
|
141
|
+
background: none;
|
|
142
|
+
padding: 0;
|
|
143
|
+
border: none;
|
|
144
|
+
font-family: inherit;
|
|
145
|
+
font-size: inherit;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* State-specific styling */
|
|
149
|
+
.jp-ai-tool-pending {
|
|
150
|
+
border-left: 4px solid var(--jp-warn-color1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.jp-ai-tool-completed {
|
|
154
|
+
border-left: 4px solid var(--jp-success-color1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.jp-ai-tool-error {
|
|
158
|
+
border-left: 4px solid var(--jp-error-color1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Tool Approval Button Styles */
|
|
162
|
+
.jp-ai-tool-approval-buttons {
|
|
163
|
+
display: flex;
|
|
164
|
+
gap: 8px;
|
|
165
|
+
margin-top: 12px;
|
|
166
|
+
justify-content: flex-end;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.jp-ai-approval-btn {
|
|
170
|
+
padding: 6px 12px;
|
|
171
|
+
border: none;
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
font-family: var(--jp-ui-font-family);
|
|
174
|
+
font-size: var(--jp-ui-font-size0);
|
|
175
|
+
font-weight: 500;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
transition: all 0.2s ease;
|
|
178
|
+
min-width: 70px;
|
|
179
|
+
display: inline-block;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.jp-ai-approval-approve {
|
|
183
|
+
background: var(--jp-success-color1);
|
|
184
|
+
color: var(--jp-ui-inverse-font-color1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.jp-ai-approval-approve:hover:not(:disabled) {
|
|
188
|
+
background: var(--jp-success-color0);
|
|
189
|
+
transform: translateY(-1px);
|
|
190
|
+
box-shadow: var(--jp-elevation-z4);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.jp-ai-approval-reject {
|
|
194
|
+
background: var(--jp-error-color1);
|
|
195
|
+
color: var(--jp-ui-inverse-font-color1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.jp-ai-approval-reject:hover:not(:disabled) {
|
|
199
|
+
background: var(--jp-error-color0);
|
|
200
|
+
transform: translateY(-1px);
|
|
201
|
+
box-shadow: var(--jp-elevation-z4);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.jp-ai-approval-btn:disabled {
|
|
205
|
+
cursor: not-allowed;
|
|
206
|
+
opacity: 0.5;
|
|
207
|
+
}
|
package/style/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url('base.css');
|
package/style/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './base.css';
|