@jupyter/chat 0.20.0-alpha.2 → 0.20.0-alpha.3
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/widgets/chat-selector-popup.d.ts +102 -0
- package/lib/widgets/chat-selector-popup.js +293 -0
- package/lib/widgets/chat-widget.js +1 -0
- package/lib/widgets/index.d.ts +1 -0
- package/lib/widgets/index.js +1 -0
- package/lib/widgets/multichat-panel.d.ts +100 -73
- package/lib/widgets/multichat-panel.js +356 -159
- package/package.json +1 -1
- package/src/widgets/chat-selector-popup.tsx +440 -0
- package/src/widgets/chat-widget.tsx +1 -0
- package/src/widgets/index.ts +1 -0
- package/src/widgets/multichat-panel.tsx +434 -207
- package/style/base.css +1 -0
- package/style/chat-selector.css +161 -0
- package/style/chat.css +34 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Message } from '@lumino/messaging';
|
|
4
|
+
/**
|
|
5
|
+
* A popup widget for selecting a chat from a filtered list.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ChatSelectorPopup extends ReactWidget {
|
|
8
|
+
constructor(options: ChatSelectorPopup.IOptions);
|
|
9
|
+
/**
|
|
10
|
+
* Getter/setter of the anchor element.
|
|
11
|
+
*/
|
|
12
|
+
get anchor(): HTMLElement | null;
|
|
13
|
+
set anchor(element: HTMLElement | null);
|
|
14
|
+
/**
|
|
15
|
+
* Update the list of available chats.
|
|
16
|
+
*/
|
|
17
|
+
updateChats(chatNames: string[]): void;
|
|
18
|
+
/**
|
|
19
|
+
* Update the list of loaded models.
|
|
20
|
+
*/
|
|
21
|
+
setLoadedModels(loadedModels: string[]): void;
|
|
22
|
+
/**
|
|
23
|
+
* Set the currently displayed chat.
|
|
24
|
+
*/
|
|
25
|
+
setCurrentChat(chatName: string | null): void;
|
|
26
|
+
/**
|
|
27
|
+
* Set the search query and filter the list.
|
|
28
|
+
*/
|
|
29
|
+
setQuery(query: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Get the currently selected chat value.
|
|
32
|
+
*/
|
|
33
|
+
getSelectedValue(): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Move selection down.
|
|
36
|
+
*/
|
|
37
|
+
selectNext(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Move selection up.
|
|
40
|
+
*/
|
|
41
|
+
selectPrevious(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Show the popup and position it.
|
|
44
|
+
*/
|
|
45
|
+
show(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Hide the popup.
|
|
48
|
+
*/
|
|
49
|
+
hide(): void;
|
|
50
|
+
render(): JSX.Element;
|
|
51
|
+
protected onAfterShow(msg: Message): void;
|
|
52
|
+
protected onAfterHide(msg: Message): void;
|
|
53
|
+
/**
|
|
54
|
+
* Check if the popup should move (anchor has moved).
|
|
55
|
+
*/
|
|
56
|
+
private _checkPosition;
|
|
57
|
+
/**
|
|
58
|
+
* Position the popup below the search element.
|
|
59
|
+
*/
|
|
60
|
+
private _positionPopup;
|
|
61
|
+
/**
|
|
62
|
+
* Update the filtered and sorted chats based on current state.
|
|
63
|
+
*/
|
|
64
|
+
private _updateFilteredChats;
|
|
65
|
+
private _handleItemClick;
|
|
66
|
+
private _handleUpdateSelectedName;
|
|
67
|
+
private _handleClose;
|
|
68
|
+
private _handleOutsideClick;
|
|
69
|
+
private _chatNames;
|
|
70
|
+
private _loadedModels;
|
|
71
|
+
private _currentChat;
|
|
72
|
+
private _query;
|
|
73
|
+
private _selectedName;
|
|
74
|
+
private _filteredChats;
|
|
75
|
+
private _onSelect?;
|
|
76
|
+
private _onClose?;
|
|
77
|
+
private _anchor;
|
|
78
|
+
private _anchorRect;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Namespace for ChatSelectorPopup.
|
|
82
|
+
*/
|
|
83
|
+
export declare namespace ChatSelectorPopup {
|
|
84
|
+
interface IOptions {
|
|
85
|
+
/**
|
|
86
|
+
* Object mapping display names to values used to identify/open chats.
|
|
87
|
+
*/
|
|
88
|
+
chatNames: string[];
|
|
89
|
+
/**
|
|
90
|
+
* Callback when a chat is selected.
|
|
91
|
+
*/
|
|
92
|
+
onSelect?: (name: string) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Callback when a chat is closed/disposed.
|
|
95
|
+
*/
|
|
96
|
+
onClose?: (name: string) => void;
|
|
97
|
+
/**
|
|
98
|
+
* The element to anchor the popup to.
|
|
99
|
+
*/
|
|
100
|
+
anchor?: HTMLElement;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { Button } from '@jupyter/react-components';
|
|
6
|
+
import { closeIcon, ReactWidget } from '@jupyterlab/ui-components';
|
|
7
|
+
import React, { useEffect, useRef } from 'react';
|
|
8
|
+
const POPUP_CLASS = 'jp-chat-selector-popup';
|
|
9
|
+
const POPUP_LIST_CLASS = 'jp-chat-selector-popup-list';
|
|
10
|
+
const POPUP_ITEM_CLASS = 'jp-chat-selector-popup-item';
|
|
11
|
+
const POPUP_ITEM_ACTIVE_CLASS = 'jp-chat-selector-popup-item-active';
|
|
12
|
+
const POPUP_ITEM_LABEL_CLASS = 'jp-chat-selector-popup-item-label';
|
|
13
|
+
const POPUP_EMPTY_CLASS = 'jp-chat-selector-popup-empty';
|
|
14
|
+
/**
|
|
15
|
+
* A popup widget for selecting a chat from a filtered list.
|
|
16
|
+
*/
|
|
17
|
+
export class ChatSelectorPopup extends ReactWidget {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
var _a;
|
|
20
|
+
super();
|
|
21
|
+
/**
|
|
22
|
+
* Check if the popup should move (anchor has moved).
|
|
23
|
+
*/
|
|
24
|
+
this._checkPosition = () => {
|
|
25
|
+
var _a;
|
|
26
|
+
if (!this._anchor) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const rect = this._anchor.getBoundingClientRect();
|
|
30
|
+
if (rect.bottom !== ((_a = this._anchorRect) === null || _a === void 0 ? void 0 : _a.bottom) ||
|
|
31
|
+
rect.left !== this._anchorRect.left ||
|
|
32
|
+
rect.width !== this._anchorRect.width) {
|
|
33
|
+
this._positionPopup();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
this._handleItemClick = (name) => {
|
|
37
|
+
if (this._onSelect) {
|
|
38
|
+
this._onSelect(name);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
this._handleUpdateSelectedName = (name) => {
|
|
42
|
+
this._selectedName = name;
|
|
43
|
+
};
|
|
44
|
+
this._handleClose = (name) => {
|
|
45
|
+
if (this._onClose) {
|
|
46
|
+
this._onClose(name);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
this._handleOutsideClick = (event) => {
|
|
50
|
+
if (this.isHidden) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const target = event.target;
|
|
54
|
+
if (!this.node.contains(target) &&
|
|
55
|
+
this._anchor &&
|
|
56
|
+
!this._anchor.contains(target)) {
|
|
57
|
+
this.hide();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
this._chatNames = [];
|
|
61
|
+
this._loadedModels = new Set();
|
|
62
|
+
this._currentChat = null;
|
|
63
|
+
this._query = '';
|
|
64
|
+
this._selectedName = null;
|
|
65
|
+
this._filteredChats = [];
|
|
66
|
+
this._anchor = null;
|
|
67
|
+
this._anchorRect = null;
|
|
68
|
+
this.addClass(POPUP_CLASS);
|
|
69
|
+
this._chatNames = options.chatNames;
|
|
70
|
+
this._onSelect = options.onSelect;
|
|
71
|
+
this._onClose = options.onClose;
|
|
72
|
+
this._anchor = (_a = options.anchor) !== null && _a !== void 0 ? _a : null;
|
|
73
|
+
// Start hidden
|
|
74
|
+
this.hide();
|
|
75
|
+
// Initialize filtered chats
|
|
76
|
+
this._updateFilteredChats();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Getter/setter of the anchor element.
|
|
80
|
+
*/
|
|
81
|
+
get anchor() {
|
|
82
|
+
return this._anchor;
|
|
83
|
+
}
|
|
84
|
+
set anchor(element) {
|
|
85
|
+
this._anchor = element;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Update the list of available chats.
|
|
89
|
+
*/
|
|
90
|
+
updateChats(chatNames) {
|
|
91
|
+
this._chatNames = chatNames;
|
|
92
|
+
this._updateFilteredChats();
|
|
93
|
+
this.update();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Update the list of loaded models.
|
|
97
|
+
*/
|
|
98
|
+
setLoadedModels(loadedModels) {
|
|
99
|
+
this._loadedModels = new Set(loadedModels);
|
|
100
|
+
this._updateFilteredChats();
|
|
101
|
+
this.update();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Set the currently displayed chat.
|
|
105
|
+
*/
|
|
106
|
+
setCurrentChat(chatName) {
|
|
107
|
+
this._currentChat = chatName;
|
|
108
|
+
this.update();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Set the search query and filter the list.
|
|
112
|
+
*/
|
|
113
|
+
setQuery(query) {
|
|
114
|
+
this._query = query;
|
|
115
|
+
this._updateFilteredChats();
|
|
116
|
+
// When filtering, select first in filtered list
|
|
117
|
+
if (query.trim()) {
|
|
118
|
+
this._selectedName =
|
|
119
|
+
this._filteredChats.length > 0 ? this._filteredChats[0] : null;
|
|
120
|
+
}
|
|
121
|
+
this.update();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the currently selected chat value.
|
|
125
|
+
*/
|
|
126
|
+
getSelectedValue() {
|
|
127
|
+
return this._selectedName;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Move selection down.
|
|
131
|
+
*/
|
|
132
|
+
selectNext() {
|
|
133
|
+
if (this._filteredChats.length === 0) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const currentIndex = this._filteredChats.findIndex(name => name === this._selectedName);
|
|
137
|
+
// If not found or at the end, wrap to first or move down
|
|
138
|
+
if (currentIndex === -1) {
|
|
139
|
+
// No selection yet, select first
|
|
140
|
+
this._selectedName = this._filteredChats[0];
|
|
141
|
+
}
|
|
142
|
+
else if (currentIndex < this._filteredChats.length - 1) {
|
|
143
|
+
// Move to next
|
|
144
|
+
this._selectedName = this._filteredChats[currentIndex + 1];
|
|
145
|
+
}
|
|
146
|
+
this.update();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Move selection up.
|
|
150
|
+
*/
|
|
151
|
+
selectPrevious() {
|
|
152
|
+
if (this._filteredChats.length === 0) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const currentIndex = this._filteredChats.findIndex(name => name === this._selectedName);
|
|
156
|
+
// If not found, select last; otherwise move up
|
|
157
|
+
if (currentIndex === -1) {
|
|
158
|
+
// No selection yet, select last
|
|
159
|
+
this._selectedName = this._filteredChats[this._filteredChats.length - 1];
|
|
160
|
+
}
|
|
161
|
+
else if (currentIndex > 0) {
|
|
162
|
+
// Move to previous
|
|
163
|
+
this._selectedName = this._filteredChats[currentIndex - 1];
|
|
164
|
+
}
|
|
165
|
+
this.update();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Show the popup and position it.
|
|
169
|
+
*/
|
|
170
|
+
show() {
|
|
171
|
+
let needsUpdate = false;
|
|
172
|
+
if (this._filteredChats.length > 0) {
|
|
173
|
+
const oldSelection = this._selectedName;
|
|
174
|
+
// If there's a current chat and no query, select it
|
|
175
|
+
if (this._currentChat && !this._query.trim()) {
|
|
176
|
+
this._selectedName = this._currentChat;
|
|
177
|
+
}
|
|
178
|
+
else if (!this._selectedName) {
|
|
179
|
+
// Otherwise select first chat if nothing is selected
|
|
180
|
+
this._selectedName = this._filteredChats[0];
|
|
181
|
+
}
|
|
182
|
+
needsUpdate = oldSelection !== this._selectedName;
|
|
183
|
+
}
|
|
184
|
+
super.show();
|
|
185
|
+
this._positionPopup();
|
|
186
|
+
// Only update if selection changed
|
|
187
|
+
if (needsUpdate) {
|
|
188
|
+
this.update();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Hide the popup.
|
|
193
|
+
*/
|
|
194
|
+
hide() {
|
|
195
|
+
this._anchorRect = null;
|
|
196
|
+
super.hide();
|
|
197
|
+
}
|
|
198
|
+
render() {
|
|
199
|
+
return (React.createElement(ChatSelectorList, { names: this._filteredChats, selectedName: this._selectedName, loadedModels: this._loadedModels, onSelect: this._handleItemClick, onUpdateSelectedName: this._handleUpdateSelectedName, onClose: this._handleClose }));
|
|
200
|
+
}
|
|
201
|
+
onAfterShow(msg) {
|
|
202
|
+
super.onAfterShow(msg);
|
|
203
|
+
document.addEventListener('pointerdown', this._handleOutsideClick, true);
|
|
204
|
+
window.addEventListener('resize', this._checkPosition);
|
|
205
|
+
}
|
|
206
|
+
onAfterHide(msg) {
|
|
207
|
+
document.removeEventListener('pointerdown', this._handleOutsideClick, true);
|
|
208
|
+
window.removeEventListener('resize', this._checkPosition);
|
|
209
|
+
super.onAfterHide(msg);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Position the popup below the search element.
|
|
213
|
+
*/
|
|
214
|
+
_positionPopup() {
|
|
215
|
+
if (!this._anchor) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this._anchorRect = this._anchor.getBoundingClientRect();
|
|
219
|
+
const rect = this.node.getBoundingClientRect();
|
|
220
|
+
const margin = 8;
|
|
221
|
+
let left = this._anchorRect.left;
|
|
222
|
+
if (this._anchorRect.left + rect.width > window.innerWidth - margin) {
|
|
223
|
+
left = window.innerWidth - margin - rect.width;
|
|
224
|
+
}
|
|
225
|
+
let top = this._anchorRect.bottom;
|
|
226
|
+
if (this._anchorRect.bottom + rect.height > window.innerHeight - margin) {
|
|
227
|
+
top = window.innerHeight - margin - rect.height;
|
|
228
|
+
}
|
|
229
|
+
this.node.style.minWidth = `${this._anchorRect.width}px`;
|
|
230
|
+
this.node.style.top = `${top}px`;
|
|
231
|
+
this.node.style.left = `${left}px`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Update the filtered and sorted chats based on current state.
|
|
235
|
+
*/
|
|
236
|
+
_updateFilteredChats() {
|
|
237
|
+
let filteredChats = Array.from(new Set([...this._chatNames, ...this._loadedModels]));
|
|
238
|
+
// Filter by query if present
|
|
239
|
+
if (this._query.trim()) {
|
|
240
|
+
const queryLower = this._query.toLowerCase();
|
|
241
|
+
filteredChats = filteredChats.filter(name => name.toLowerCase().includes(queryLower));
|
|
242
|
+
}
|
|
243
|
+
// Separate into loaded and non-loaded
|
|
244
|
+
const loadedChats = [];
|
|
245
|
+
const nonLoadedChats = [];
|
|
246
|
+
filteredChats.forEach(name => {
|
|
247
|
+
if (this._loadedModels.has(name)) {
|
|
248
|
+
loadedChats.push(name);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
nonLoadedChats.push(name);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// Sort each group alphabetically by name
|
|
255
|
+
loadedChats.sort();
|
|
256
|
+
nonLoadedChats.sort();
|
|
257
|
+
// Combine: loaded first, then non-loaded
|
|
258
|
+
this._filteredChats = [...loadedChats, ...nonLoadedChats];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* React component for rendering the chat list.
|
|
263
|
+
*/
|
|
264
|
+
function ChatSelectorList({ names, selectedName, loadedModels, onSelect, onUpdateSelectedName, onClose }) {
|
|
265
|
+
const listRef = useRef(null);
|
|
266
|
+
// Scroll selected item into view
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (listRef.current && selectedName) {
|
|
269
|
+
const selectedItem = listRef.current.querySelector(`[data-chat-name="${CSS.escape(selectedName)}"]`);
|
|
270
|
+
if (selectedItem) {
|
|
271
|
+
selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}, [selectedName]);
|
|
275
|
+
const handleCloseClick = (event, name) => {
|
|
276
|
+
event.stopPropagation();
|
|
277
|
+
onClose(name);
|
|
278
|
+
};
|
|
279
|
+
if (names.length === 0) {
|
|
280
|
+
return React.createElement("div", { className: POPUP_EMPTY_CLASS }, "No chats found");
|
|
281
|
+
}
|
|
282
|
+
return (React.createElement("ul", { ref: listRef, className: POPUP_LIST_CLASS }, names.map(name => {
|
|
283
|
+
const isLoaded = loadedModels.has(name);
|
|
284
|
+
return (React.createElement("li", { key: name, "data-chat-name": name, className: `${POPUP_ITEM_CLASS} ${name === selectedName ? POPUP_ITEM_ACTIVE_CLASS : ''}`, onClick: () => onSelect(name), onMouseEnter: () => onUpdateSelectedName(name) },
|
|
285
|
+
React.createElement("div", { className: "jp-chat-selector-popup-item-content" },
|
|
286
|
+
React.createElement("div", { className: "jp-chat-selector-popup-item-text" },
|
|
287
|
+
React.createElement("div", { className: POPUP_ITEM_LABEL_CLASS },
|
|
288
|
+
React.createElement("span", { className: "jp-chat-selector-popup-item-name", title: name }, name),
|
|
289
|
+
isLoaded && (React.createElement("span", { className: "jp-chat-selector-popup-item-indicator" }, "\u25CF")))),
|
|
290
|
+
isLoaded && (React.createElement(Button, { onClick: e => handleCloseClick(e, name), appearance: "stealth", title: "Close and dispose this chat", className: "jp-chat-selector-popup-item-close" },
|
|
291
|
+
React.createElement(closeIcon.react, { tag: null }))))));
|
|
292
|
+
})));
|
|
293
|
+
}
|
|
@@ -26,6 +26,7 @@ export class ChatWidget extends ReactWidget {
|
|
|
26
26
|
this.title.caption = 'Jupyter Chat'; // TODO: i18n
|
|
27
27
|
this._chatOptions = options;
|
|
28
28
|
this.id = `jupyter-chat::widget::${options.model.name}`;
|
|
29
|
+
this.addClass('jp-chat-widget');
|
|
29
30
|
this.node.addEventListener('click', (event) => {
|
|
30
31
|
const target = event.target;
|
|
31
32
|
if (this.node.contains(document.activeElement)) {
|
package/lib/widgets/index.d.ts
CHANGED
package/lib/widgets/index.js
CHANGED