@jupyter/chat 0.20.0-alpha.2 → 0.20.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.
@@ -3,10 +3,11 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import CloseIcon from '@mui/icons-material/Close';
6
- import { Box, Button, Tooltip } from '@mui/material';
6
+ import { Box, Tooltip } from '@mui/material';
7
7
  import React from 'react';
8
8
  import { PathExt } from '@jupyterlab/coreutils';
9
9
  import { UUID } from '@lumino/coreutils';
10
+ import { TooltippedIconButton } from './mui-extras';
10
11
  import { useChatContext } from '../context';
11
12
  const ATTACHMENT_CLASS = 'jp-chat-attachment';
12
13
  const ATTACHMENT_CLICKABLE_CLASS = 'jp-chat-attachment-clickable';
@@ -79,17 +80,11 @@ export function AttachmentPreview(props) {
79
80
  }
80
81
  : {}
81
82
  } }, getAttachmentDisplayName(props.attachment))),
82
- props.onRemove && (React.createElement(Tooltip, { title: remove_tooltip, placement: "top", arrow: true },
83
- React.createElement("span", null,
84
- React.createElement(Button, { onClick: () => props.onRemove(props.attachment), size: "small", className: REMOVE_BUTTON_CLASS, "aria-label": remove_tooltip, sx: {
85
- minWidth: 'unset',
86
- padding: 0,
87
- lineHeight: 0,
88
- color: 'var(--jp-ui-font-color2)',
89
- '&:hover': {
90
- color: 'var(--jp-ui-font-color0)',
91
- backgroundColor: 'transparent'
92
- }
93
- } },
94
- React.createElement(CloseIcon, { fontSize: "small" })))))));
83
+ props.onRemove && (React.createElement(TooltippedIconButton, { tooltip: remove_tooltip, onClick: () => props.onRemove(props.attachment), className: REMOVE_BUTTON_CLASS, inputToolbar: false, sx: {
84
+ width: 'unset',
85
+ minWidth: 'unset',
86
+ height: 'unset',
87
+ padding: 0
88
+ } },
89
+ React.createElement(CloseIcon, { fontSize: "small" })))));
95
90
  }
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './active-cell-manager';
2
2
  export * from './components';
3
+ export * from './context';
3
4
  export * from './icons';
4
5
  export * from './input-model';
5
6
  export * from './model';
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export * from './active-cell-manager';
6
6
  export * from './components';
7
+ export * from './context';
7
8
  export * from './icons';
8
9
  export * from './input-model';
9
10
  export * from './model';
package/lib/message.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { IRenderMime } from '@jupyterlab/rendermime';
2
1
  import { ISignal } from '@lumino/signaling';
3
- import { IAttachment, IMessageContent, IMessage, IMessageMetadata, IUser } from './types';
2
+ import { IAttachment, IMessageContent, IMessage, IMessageMetadata, IUser, IMimeModelBody } from './types';
4
3
  /**
5
4
  * The message object.
6
5
  */
@@ -19,7 +18,7 @@ export declare class Message implements IMessage {
19
18
  * Getters for each attribute individually.
20
19
  */
21
20
  get type(): string;
22
- get body(): string | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
21
+ get body(): string | IMimeModelBody;
23
22
  get id(): string;
24
23
  get time(): number;
25
24
  get sender(): IUser;
@@ -159,10 +159,6 @@ export async function getJupyterLabTheme() {
159
159
  }
160
160
  },
161
161
  palette: {
162
- background: {
163
- paper: getCSSVariable('--jp-layout-color1'),
164
- default: getCSSVariable('--jp-layout-color1')
165
- },
166
162
  mode: light ? 'light' : 'dark',
167
163
  primary: {
168
164
  main: getCSSVariable(`--jp-brand-color${light ? '1' : '2'}`),
package/lib/tokens.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { IWidgetTracker, MainAreaWidget } from '@jupyterlab/apputils';
2
2
  import { Token } from '@lumino/coreutils';
3
3
  import { ChatWidget } from './widgets';
4
+ import { IChatModel } from './model';
5
+ /**
6
+ * The main area chat widget type.
7
+ */
8
+ export type MainAreaChat = MainAreaWidget<ChatWidget> & {
9
+ model: IChatModel;
10
+ };
4
11
  /**
5
12
  * the chat tracker type.
6
13
  */
7
- export type IChatTracker = IWidgetTracker<ChatWidget | MainAreaWidget<ChatWidget>>;
14
+ export type IChatTracker = IWidgetTracker<ChatWidget | MainAreaChat>;
8
15
  /**
9
16
  * A chat tracker token.
10
17
  */
package/lib/types.d.ts CHANGED
@@ -53,6 +53,10 @@ export interface IConfig {
53
53
  */
54
54
  showDeleted?: boolean;
55
55
  }
56
+ /**
57
+ * Mime model body type, a partial mime bundle model containing at least the data.
58
+ */
59
+ export type IMimeModelBody = Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>;
56
60
  /**
57
61
  * An empty interface to describe optional metadata attached to a chat message.
58
62
  * Extensions can augment this interface to add custom fields:
@@ -72,7 +76,7 @@ export interface IMessageMetadata {
72
76
  */
73
77
  export type IMessageContent<T = IUser, U = IAttachment> = {
74
78
  type: string;
75
- body: string | (Partial<IRenderMime.IMimeModel> & Pick<IRenderMime.IMimeModel, 'data'>);
79
+ body: string | IMimeModelBody;
76
80
  id: string;
77
81
  time: number;
78
82
  sender: T;
@@ -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 chat 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)) {
@@ -1,4 +1,5 @@
1
1
  export * from './chat-error';
2
+ export * from './chat-selector-popup';
2
3
  export * from './chat-sidebar';
3
4
  export * from './chat-widget';
4
5
  export * from './multichat-panel';
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  export * from './chat-error';
6
+ export * from './chat-selector-popup';
6
7
  export * from './chat-sidebar';
7
8
  export * from './chat-widget';
8
9
  export * from './multichat-panel';