@jupyter/chat 0.4.0 → 0.6.0
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/lib/active-cell-manager.d.ts +3 -0
- package/lib/components/chat-input.d.ts +4 -0
- package/lib/components/chat-input.js +32 -15
- package/lib/components/chat-messages.d.ts +31 -1
- package/lib/components/chat-messages.js +57 -19
- package/lib/components/chat.js +1 -1
- package/lib/components/code-blocks/code-toolbar.js +51 -17
- package/lib/components/input/cancel-button.d.ts +12 -0
- package/lib/components/input/cancel-button.js +27 -0
- package/lib/components/input/send-button.d.ts +18 -0
- package/lib/components/input/send-button.js +143 -0
- package/lib/components/mui-extras/tooltipped-button.d.ts +41 -0
- package/lib/components/mui-extras/tooltipped-button.js +43 -0
- package/lib/components/mui-extras/tooltipped-icon-button.js +5 -1
- package/lib/components/rendermime-markdown.js +15 -6
- package/lib/icons.d.ts +1 -0
- package/lib/icons.js +5 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/model.d.ts +51 -8
- package/lib/model.js +44 -12
- package/lib/selection-watcher.d.ts +62 -0
- package/lib/selection-watcher.js +134 -0
- package/lib/types.d.ts +22 -0
- package/lib/utils.d.ts +11 -0
- package/lib/utils.js +37 -0
- package/package.json +2 -1
- package/src/active-cell-manager.ts +3 -0
- package/src/components/chat-input.tsx +48 -30
- package/src/components/chat-messages.tsx +112 -32
- package/src/components/chat.tsx +1 -1
- package/src/components/code-blocks/code-toolbar.tsx +56 -18
- package/src/components/input/cancel-button.tsx +47 -0
- package/src/components/input/send-button.tsx +210 -0
- package/src/components/mui-extras/tooltipped-button.tsx +92 -0
- package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -1
- package/src/components/rendermime-markdown.tsx +16 -5
- package/src/icons.ts +6 -0
- package/src/index.ts +2 -1
- package/src/model.ts +77 -13
- package/src/selection-watcher.ts +221 -0
- package/src/types.ts +25 -0
- package/src/utils.ts +47 -0
- package/style/chat.css +13 -0
- package/style/icons/include-selection.svg +5 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
7
|
+
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
8
|
+
import { CodeEditor } from '@jupyterlab/codeeditor';
|
|
9
|
+
import { Notebook } from '@jupyterlab/notebook';
|
|
10
|
+
|
|
11
|
+
import { find } from '@lumino/algorithm';
|
|
12
|
+
import { Widget } from '@lumino/widgets';
|
|
13
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
14
|
+
|
|
15
|
+
import { getCellIndex, getEditor } from './utils';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The selection watcher namespace.
|
|
19
|
+
*/
|
|
20
|
+
export namespace SelectionWatcher {
|
|
21
|
+
/**
|
|
22
|
+
* The constructor options.
|
|
23
|
+
*/
|
|
24
|
+
export interface IOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The current shell of the application.
|
|
27
|
+
*/
|
|
28
|
+
shell: JupyterFrontEnd.IShell;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The selection type.
|
|
33
|
+
*/
|
|
34
|
+
export type Selection = CodeEditor.ITextSelection & {
|
|
35
|
+
/**
|
|
36
|
+
* The text within the selection as a string.
|
|
37
|
+
*/
|
|
38
|
+
text: string;
|
|
39
|
+
/**
|
|
40
|
+
* Number of lines contained by the text selection.
|
|
41
|
+
*/
|
|
42
|
+
numLines: number;
|
|
43
|
+
/**
|
|
44
|
+
* The ID of the document widget in which the selection was made.
|
|
45
|
+
*/
|
|
46
|
+
widgetId: string;
|
|
47
|
+
/**
|
|
48
|
+
* The ID of the cell in which the selection was made, if the original widget
|
|
49
|
+
* was a notebook.
|
|
50
|
+
*/
|
|
51
|
+
cellId?: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The selection watcher interface.
|
|
57
|
+
*/
|
|
58
|
+
export interface ISelectionWatcher {
|
|
59
|
+
readonly selection: SelectionWatcher.Selection | null;
|
|
60
|
+
readonly selectionChanged: ISignal<
|
|
61
|
+
ISelectionWatcher,
|
|
62
|
+
SelectionWatcher.Selection | null
|
|
63
|
+
>;
|
|
64
|
+
replaceSelection(selection: SelectionWatcher.Selection): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The selection watcher, read/write selected text in a DocumentWidget.
|
|
69
|
+
*/
|
|
70
|
+
export class SelectionWatcher {
|
|
71
|
+
constructor(options: SelectionWatcher.IOptions) {
|
|
72
|
+
this._shell = options.shell;
|
|
73
|
+
this._shell.currentChanged?.connect((sender, args) => {
|
|
74
|
+
// Do not change the main area widget if the new one has no editor, for example
|
|
75
|
+
// a chat panel. However, the selected text is only available if the main area
|
|
76
|
+
// widget is visible. (to avoid confusion in inclusion/replacement).
|
|
77
|
+
const widget = args.newValue;
|
|
78
|
+
|
|
79
|
+
// if there is no main area widget, set it to null.
|
|
80
|
+
if (widget === null) {
|
|
81
|
+
this._mainAreaDocumentWidget = null;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const editor = getEditor(widget);
|
|
86
|
+
if (
|
|
87
|
+
widget instanceof DocumentWidget &&
|
|
88
|
+
(editor || widget.content instanceof Notebook)
|
|
89
|
+
) {
|
|
90
|
+
// if the new widget is a DocumentWidget and has an editor, set it.
|
|
91
|
+
// NOTE: special case for notebook which do not has an active cell at that stage,
|
|
92
|
+
// and so the editor can't be retrieved too.
|
|
93
|
+
this._mainAreaDocumentWidget = widget;
|
|
94
|
+
} else if (this._mainAreaDocumentWidget?.isDisposed) {
|
|
95
|
+
// if the previous document widget has been closed, set it to null.
|
|
96
|
+
this._mainAreaDocumentWidget = null;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
setInterval(this._poll.bind(this), 200);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get selection(): SelectionWatcher.Selection | null {
|
|
104
|
+
return this._selection;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get selectionChanged(): ISignal<this, SelectionWatcher.Selection | null> {
|
|
108
|
+
return this._selectionChanged;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
replaceSelection(selection: SelectionWatcher.Selection): void {
|
|
112
|
+
// unfortunately shell.currentWidget doesn't update synchronously after
|
|
113
|
+
// shell.activateById(), which is why we have to get a reference to the
|
|
114
|
+
// widget manually.
|
|
115
|
+
const widget = find(
|
|
116
|
+
this._shell.widgets(),
|
|
117
|
+
widget => widget.id === selection.widgetId
|
|
118
|
+
);
|
|
119
|
+
// Do not allow replacement on non visible widget (to avoid confusion).
|
|
120
|
+
if (!widget?.isVisible || !(widget instanceof DocumentWidget)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// activate the widget if not already active
|
|
125
|
+
this._shell.activateById(selection.widgetId);
|
|
126
|
+
|
|
127
|
+
// activate notebook cell if specified
|
|
128
|
+
if (widget.content instanceof Notebook && selection.cellId) {
|
|
129
|
+
const cellIndex = getCellIndex(widget.content, selection.cellId);
|
|
130
|
+
if (cellIndex !== -1) {
|
|
131
|
+
widget.content.activeCellIndex = cellIndex;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// get editor instance
|
|
136
|
+
const editor = getEditor(widget);
|
|
137
|
+
if (!editor) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
editor.model.sharedModel.updateSource(
|
|
142
|
+
editor.getOffsetAt(selection.start),
|
|
143
|
+
editor.getOffsetAt(selection.end),
|
|
144
|
+
selection.text
|
|
145
|
+
);
|
|
146
|
+
const newPosition = editor.getPositionAt(
|
|
147
|
+
editor.getOffsetAt(selection.start) + selection.text.length
|
|
148
|
+
);
|
|
149
|
+
editor.setSelection({ start: newPosition, end: newPosition });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
protected _poll(): void {
|
|
153
|
+
let currSelection: SelectionWatcher.Selection | null = null;
|
|
154
|
+
const prevSelection = this._selection;
|
|
155
|
+
// Do not return selected text if the main area widget is hidden.
|
|
156
|
+
if (this._mainAreaDocumentWidget?.isVisible) {
|
|
157
|
+
currSelection = getTextSelection(this._mainAreaDocumentWidget);
|
|
158
|
+
}
|
|
159
|
+
if (prevSelection?.text !== currSelection?.text) {
|
|
160
|
+
this._selection = currSelection;
|
|
161
|
+
this._selectionChanged.emit(currSelection);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected _shell: JupyterFrontEnd.IShell;
|
|
166
|
+
protected _mainAreaDocumentWidget: Widget | null = null;
|
|
167
|
+
protected _selection: SelectionWatcher.Selection | null = null;
|
|
168
|
+
protected _selectionChanged = new Signal<
|
|
169
|
+
this,
|
|
170
|
+
SelectionWatcher.Selection | null
|
|
171
|
+
>(this);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gets a Selection object from a document widget. Returns `null` if unable.
|
|
176
|
+
*/
|
|
177
|
+
function getTextSelection(
|
|
178
|
+
widget: Widget | null
|
|
179
|
+
): SelectionWatcher.Selection | null {
|
|
180
|
+
const editor = getEditor(widget);
|
|
181
|
+
// widget type check is redundant but hints the type to TypeScript
|
|
182
|
+
if (!editor || !(widget instanceof DocumentWidget)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let cellId: string | undefined = undefined;
|
|
187
|
+
if (widget.content instanceof Notebook) {
|
|
188
|
+
cellId = widget.content.activeCell?.model.id;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const selectionObj = editor.getSelection();
|
|
192
|
+
let { start, end } = selectionObj;
|
|
193
|
+
const startOffset = editor.getOffsetAt(start);
|
|
194
|
+
const endOffset = editor.getOffsetAt(end);
|
|
195
|
+
const text = editor.model.sharedModel
|
|
196
|
+
.getSource()
|
|
197
|
+
.substring(startOffset, endOffset);
|
|
198
|
+
|
|
199
|
+
// Do not return a Selection object if no text is selected
|
|
200
|
+
if (!text) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ensure start <= end
|
|
205
|
+
// required for editor.model.sharedModel.updateSource()
|
|
206
|
+
if (startOffset > endOffset) {
|
|
207
|
+
[start, end] = [end, start];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...selectionObj,
|
|
212
|
+
start,
|
|
213
|
+
end,
|
|
214
|
+
text,
|
|
215
|
+
numLines: text.split('\n').length,
|
|
216
|
+
widgetId: widget.id,
|
|
217
|
+
...(cellId && {
|
|
218
|
+
cellId
|
|
219
|
+
})
|
|
220
|
+
};
|
|
221
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -39,6 +39,10 @@ export interface IConfig {
|
|
|
39
39
|
* Whether to enable or not the code toolbar.
|
|
40
40
|
*/
|
|
41
41
|
enableCodeToolbar?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether to send typing notification.
|
|
44
|
+
*/
|
|
45
|
+
sendTypingNotification?: boolean;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
@@ -83,6 +87,27 @@ export type AutocompleteCommand = {
|
|
|
83
87
|
label: string;
|
|
84
88
|
};
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Representation of a selected text.
|
|
92
|
+
*/
|
|
93
|
+
export type TextSelection = {
|
|
94
|
+
type: 'text';
|
|
95
|
+
source: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Representation of a selected cell.
|
|
100
|
+
*/
|
|
101
|
+
export type CellSelection = {
|
|
102
|
+
type: 'cell';
|
|
103
|
+
source: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Selection object (text or cell).
|
|
108
|
+
*/
|
|
109
|
+
export type Selection = TextSelection | CellSelection;
|
|
110
|
+
|
|
86
111
|
/**
|
|
87
112
|
* The properties of the autocompletion.
|
|
88
113
|
*
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* Gets the editor instance used by a document widget. Returns `null` if unable.
|
|
15
|
+
*/
|
|
16
|
+
export function getEditor(
|
|
17
|
+
widget: Widget | null
|
|
18
|
+
): CodeMirrorEditor | null | undefined {
|
|
19
|
+
if (!(widget instanceof DocumentWidget)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let editor: CodeEditor.IEditor | null | undefined;
|
|
24
|
+
const { content } = widget;
|
|
25
|
+
|
|
26
|
+
if (content instanceof FileEditor) {
|
|
27
|
+
editor = content.editor;
|
|
28
|
+
} else if (content instanceof Notebook) {
|
|
29
|
+
editor = content.activeCell?.editor;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!(editor instanceof CodeMirrorEditor)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return editor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets the index of the cell associated with `cellId`.
|
|
41
|
+
*/
|
|
42
|
+
export function getCellIndex(notebook: Notebook, cellId: string): number {
|
|
43
|
+
const idx = notebook.model?.sharedModel.cells.findIndex(
|
|
44
|
+
cell => cell.getId() === cellId
|
|
45
|
+
);
|
|
46
|
+
return idx === undefined ? -1 : idx;
|
|
47
|
+
}
|
package/style/chat.css
CHANGED
|
@@ -63,6 +63,19 @@
|
|
|
63
63
|
margin-top: 0px;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
.jp-chat-writers {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.jp-chat-writers > div {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 0.2em;
|
|
75
|
+
white-space: pre;
|
|
76
|
+
padding-left: 0.5em;
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
.jp-chat-navigation {
|
|
67
80
|
position: absolute;
|
|
68
81
|
right: 10px;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="18" height="9" viewBox="0 0 18 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
3
|
+
d="M-0.0018417 1.33824L0.837379 0.499023L1.35717 1.01871C1.35741 1.01862 1.35764 1.01853 1.35787 1.01845L3.411 3.07158H3.41049L4.83909 4.49987L3.41011 5.92872H3.41075L1.35769 7.98178C1.35749 7.98171 1.35729 7.98164 1.35709 7.98156L0.837023 8.50158L-0.00219727 7.66236L3.1547 4.49987L-0.0018417 1.33824ZM2.6821 8.07158H16.143C16.932 8.07158 17.5716 7.43201 17.5716 6.64301V2.35729C17.5716 1.56832 16.932 0.928721 16.143 0.928721H2.68236L4.82522 3.07158H15.4287V5.92872H4.82496L2.6821 8.07158ZM1.74238 4.50002L0.428719 5.81655V3.18349L1.74238 4.50002Z"
|
|
4
|
+
fill="#757575" />
|
|
5
|
+
</svg>
|