@jupyter/chat 0.4.0 → 0.5.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.
Files changed (41) hide show
  1. package/lib/active-cell-manager.d.ts +3 -0
  2. package/lib/components/chat-input.d.ts +4 -0
  3. package/lib/components/chat-input.js +32 -15
  4. package/lib/components/chat-messages.d.ts +31 -1
  5. package/lib/components/chat-messages.js +55 -19
  6. package/lib/components/chat.js +1 -1
  7. package/lib/components/code-blocks/code-toolbar.js +50 -16
  8. package/lib/components/input/cancel-button.d.ts +12 -0
  9. package/lib/components/input/cancel-button.js +27 -0
  10. package/lib/components/input/send-button.d.ts +18 -0
  11. package/lib/components/input/send-button.js +143 -0
  12. package/lib/components/mui-extras/tooltipped-button.d.ts +41 -0
  13. package/lib/components/mui-extras/tooltipped-button.js +43 -0
  14. package/lib/icons.d.ts +1 -0
  15. package/lib/icons.js +5 -0
  16. package/lib/index.d.ts +1 -0
  17. package/lib/index.js +1 -0
  18. package/lib/model.d.ts +51 -8
  19. package/lib/model.js +44 -12
  20. package/lib/selection-watcher.d.ts +62 -0
  21. package/lib/selection-watcher.js +134 -0
  22. package/lib/types.d.ts +22 -0
  23. package/lib/utils.d.ts +11 -0
  24. package/lib/utils.js +37 -0
  25. package/package.json +2 -1
  26. package/src/active-cell-manager.ts +3 -0
  27. package/src/components/chat-input.tsx +48 -30
  28. package/src/components/chat-messages.tsx +106 -32
  29. package/src/components/chat.tsx +1 -1
  30. package/src/components/code-blocks/code-toolbar.tsx +55 -17
  31. package/src/components/input/cancel-button.tsx +47 -0
  32. package/src/components/input/send-button.tsx +210 -0
  33. package/src/components/mui-extras/tooltipped-button.tsx +92 -0
  34. package/src/icons.ts +6 -0
  35. package/src/index.ts +1 -0
  36. package/src/model.ts +77 -13
  37. package/src/selection-watcher.ts +221 -0
  38. package/src/types.ts +25 -0
  39. package/src/utils.ts +47 -0
  40. package/style/chat.css +13 -0
  41. 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>