@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.
@@ -7,38 +7,90 @@
7
7
  * Originally adapted from jupyterlab-chat's ChatPanel
8
8
  */
9
9
  import { InputDialog } from '@jupyterlab/apputils';
10
- import { addIcon, closeIcon, HTMLSelect, launchIcon, PanelWithToolbar, ReactWidget, SidePanel, Spinner, ToolbarButton } from '@jupyterlab/ui-components';
10
+ import { addIcon, closeIcon, launchIcon, PanelWithToolbar, ReactWidget, Spinner, ToolbarButton } from '@jupyterlab/ui-components';
11
+ import { ArrayExt } from '@lumino/algorithm';
11
12
  import { Debouncer } from '@lumino/polling';
12
13
  import { Signal } from '@lumino/signaling';
13
- import React, { useState } from 'react';
14
+ import { Widget } from '@lumino/widgets';
15
+ import React, { useEffect, useRef, useState } from 'react';
14
16
  import { ChatWidget } from './chat-widget';
17
+ import { ChatSelectorPopup } from './chat-selector-popup';
15
18
  import { chatIcon, readIcon } from '../icons';
16
19
  const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
17
20
  const ADD_BUTTON_CLASS = 'jp-chat-add';
18
21
  const OPEN_SELECT_CLASS = 'jp-chat-open';
19
- const SECTION_CLASS = 'jp-chat-section';
20
- const TOOLBAR_CLASS = 'jp-chat-section-toolbar';
22
+ const SIDEPANEL_WIDGET_CLASS = 'jp-chat-sidepanel-widget';
23
+ const TOOLBAR_CLASS = 'jp-chat-sidepanel-widget-toolbar';
21
24
  /**
22
25
  * Generic sidepanel widget including multiple chats and the add chat button.
23
26
  */
24
- export class MultiChatPanel extends SidePanel {
27
+ export class MultiChatPanel extends PanelWithToolbar {
28
+ /**
29
+ * The constructor of the multichat panel.
30
+ */
25
31
  constructor(options) {
26
32
  super(options);
33
+ /**
34
+ * Invoke the update of the list of available chats.
35
+ */
36
+ this.updateChatList = () => {
37
+ this._updateChatListDebouncer.invoke();
38
+ };
27
39
  /**
28
40
  * Update the list of available chats.
29
41
  */
30
42
  this._updateChatList = async () => {
31
- var _a;
43
+ var _a, _b;
32
44
  try {
33
45
  const chatNames = await ((_a = this._getChatNames) === null || _a === void 0 ? void 0 : _a.call(this));
34
- this._chatNamesChanged.emit(chatNames !== null && chatNames !== void 0 ? chatNames : {});
46
+ if (!ArrayExt.shallowEqual(Object.keys(chatNames !== null && chatNames !== void 0 ? chatNames : {}), Object.keys(this._chatNames))) {
47
+ this._chatNames = chatNames !== null && chatNames !== void 0 ? chatNames : {};
48
+ (_b = this._chatSelectorPopup) === null || _b === void 0 ? void 0 : _b.updateChats(Object.keys(this._chatNames));
49
+ }
35
50
  }
36
51
  catch (e) {
37
52
  console.error('Error getting chat files', e);
38
53
  }
39
54
  };
40
- this._chatNamesChanged = new Signal(this);
41
- this._sectionAdded = new Signal(this);
55
+ /**
56
+ * Update loaded model when the current widget updates its name.
57
+ */
58
+ this._modelNameChanged = (_, change) => {
59
+ var _a, _b, _c;
60
+ const model = this.getLoadedModel(change.old);
61
+ if (model) {
62
+ this._loadedModels.set(change.new, model);
63
+ this._loadedModels.delete(change.old);
64
+ (_a = this._chatSelectorPopup) === null || _a === void 0 ? void 0 : _a.setLoadedModels(this.getLoadedModelNames());
65
+ if (((_b = this._currentWidget) === null || _b === void 0 ? void 0 : _b.model.name) === model.name) {
66
+ (_c = this._chatSelectorPopup) === null || _c === void 0 ? void 0 : _c.setCurrentChat(change.new);
67
+ }
68
+ }
69
+ };
70
+ /**
71
+ * Handle chat selection from the popup.
72
+ */
73
+ this._onSelectChat = async (name) => {
74
+ var _a;
75
+ // Check if model is already loaded
76
+ let openChatArgs = {
77
+ model: this.getLoadedModel(name),
78
+ displayName: name
79
+ };
80
+ // If not, create the model.
81
+ if (!openChatArgs.model && this._createModel) {
82
+ const chatID = this._chatNames[name];
83
+ openChatArgs = await this._createModel(chatID);
84
+ }
85
+ if (openChatArgs.model) {
86
+ this.open(openChatArgs);
87
+ }
88
+ (_a = this._chatSelectorPopup) === null || _a === void 0 ? void 0 : _a.hide();
89
+ };
90
+ this._chatOpened = new Signal(this);
91
+ this._loadedModels = new Map();
92
+ this._chatNames = {};
93
+ this._visibilityChanged = new Signal(this);
42
94
  this.id = 'jupyter-chat::multi-chat-panel';
43
95
  this.title.icon = chatIcon;
44
96
  this.title.caption = 'Jupyter Chat'; // TODO: i18n/
@@ -54,96 +106,173 @@ export class MultiChatPanel extends SidePanel {
54
106
  const addChat = new ToolbarButton({
55
107
  onClick: async () => {
56
108
  const addChatArgs = await this._createModel();
57
- this.addChat(addChatArgs);
109
+ this.open(addChatArgs);
58
110
  },
59
111
  icon: addIcon,
60
- label: 'Chat',
61
- tooltip: 'Add a new chat'
112
+ tooltip: 'Create a new chat'
62
113
  });
63
114
  addChat.addClass(ADD_BUTTON_CLASS);
64
115
  this.toolbar.addItem('createChat', addChat);
65
116
  }
66
117
  if (this._getChatNames && this._createModel) {
67
- // Chat select dropdown
68
- this._openChatWidget = ReactWidget.create(React.createElement(ChatSelect, { chatNamesChanged: this._chatNamesChanged, handleChange: this._chatSelected.bind(this) }));
118
+ // Chat selector with search input
119
+ this._openChatWidget = ReactWidget.create(React.createElement(ChatSearchInput, { selectChat: this._onSelectChat, getPopup: () => this._chatSelectorPopup, chatOpened: this._chatOpened }));
69
120
  this._openChatWidget.addClass(OPEN_SELECT_CLASS);
70
121
  this.toolbar.addItem('openChat', this._openChatWidget);
122
+ // Create the popup widget (attached to document body)
123
+ this._chatSelectorPopup = new ChatSelectorPopup({
124
+ chatNames: [],
125
+ onSelect: this._onSelectChat,
126
+ onClose: (name) => {
127
+ this.disposeLoadedModel(name);
128
+ }
129
+ });
71
130
  }
72
- const content = this.content;
73
- content.expansionToggled.connect(this._onExpansionToggled, this);
131
+ // Insert the toolbar as first child.
132
+ this.insertWidget(0, this.toolbar);
74
133
  this._updateChatListDebouncer = new Debouncer(this._updateChatList, 200);
75
134
  }
76
135
  /**
77
- * The sections of the side panel.
136
+ * The currently displayed chat widget.
137
+ */
138
+ get current() {
139
+ return this._currentWidget;
140
+ }
141
+ /**
142
+ * A signal emitting when a chat widget is opened in the panel.
78
143
  */
79
- get sections() {
80
- return this.widgets;
144
+ get chatOpened() {
145
+ return this._chatOpened;
81
146
  }
82
147
  /**
83
- * A signal emitting when a section is added to the panel.
148
+ * A signal emitting when the panel visibility changed.
84
149
  */
85
- get sectionAdded() {
86
- return this._sectionAdded;
150
+ get visibilityChanged() {
151
+ return this._visibilityChanged;
87
152
  }
88
153
  /**
89
- * Add a new widget to the chat panel.
154
+ * Add a chat to the panel by creating or showing its widget.
90
155
  *
91
- * @param model - the model of the chat widget
92
- * @param displayName - the name of the chat.
156
+ * @param args - the chat args including model and display name.
93
157
  */
94
- addChat(args) {
95
- const { model, displayName } = args;
158
+ open(args) {
159
+ var _a, _b;
160
+ const { model } = args;
96
161
  if (!model) {
97
162
  return;
98
163
  }
99
- if (this.openIfExists(model.name)) {
164
+ const displayName = (_a = args.displayName) !== null && _a !== void 0 ? _a : model.name;
165
+ // Add model to loaded models
166
+ if (!this._loadedModels.has(displayName)) {
167
+ this._loadedModels.set(displayName, model);
168
+ (_b = this._chatSelectorPopup) === null || _b === void 0 ? void 0 : _b.setLoadedModels(this.getLoadedModelNames());
169
+ }
170
+ // Open this chat (will create widget)
171
+ return this._open(displayName);
172
+ }
173
+ /**
174
+ * Get a loaded model by name, or undefined if not loaded.
175
+ */
176
+ getLoadedModel(name) {
177
+ return this._loadedModels.get(name);
178
+ }
179
+ /**
180
+ * Get all loaded model names.
181
+ */
182
+ getLoadedModelNames() {
183
+ return Array.from(this._loadedModels.keys());
184
+ }
185
+ /**
186
+ * Dispose a model, removing it from loaded models.
187
+ */
188
+ disposeLoadedModel(name) {
189
+ var _a, _b;
190
+ const model = this._loadedModels.get(name);
191
+ if (model) {
192
+ // If this is the currently displayed chat, remove it.
193
+ if (((_a = this._currentWidget) === null || _a === void 0 ? void 0 : _a.model) === model) {
194
+ this._currentWidget.nameChanged.disconnect(this._modelNameChanged);
195
+ this._currentWidget.dispose();
196
+ this._currentWidget = undefined;
197
+ // Clear current chat in selector
198
+ if (this._chatSelectorPopup) {
199
+ this._chatSelectorPopup.setCurrentChat(null);
200
+ }
201
+ }
202
+ model.dispose();
203
+ this._loadedModels.delete(name);
204
+ (_b = this._chatSelectorPopup) === null || _b === void 0 ? void 0 : _b.setLoadedModels(this.getLoadedModelNames());
205
+ }
206
+ }
207
+ /**
208
+ * Emit a signal when the panel visibility changed.
209
+ */
210
+ onAfterShow(msg) {
211
+ this._visibilityChanged.emit(true);
212
+ }
213
+ onBeforeHide(msg) {
214
+ this._visibilityChanged.emit(false);
215
+ }
216
+ /**
217
+ * Open a specific chat by name, creating a new sidepanel widget.
218
+ */
219
+ _open(name) {
220
+ const model = this._loadedModels.get(name);
221
+ if (!model) {
100
222
  return;
101
223
  }
102
- const content = this.content;
103
- for (let i = 0; i < this.widgets.length; i++) {
104
- content.collapse(i);
224
+ // Dispose current chat widget if any
225
+ if (this._currentWidget) {
226
+ this._currentWidget.nameChanged.disconnect(this._modelNameChanged);
227
+ this._currentWidget.dispose();
105
228
  }
106
229
  // Create the toolbar registry.
107
230
  let inputToolbarRegistry;
108
231
  if (this._inputToolbarFactory) {
109
232
  inputToolbarRegistry = this._inputToolbarFactory.create();
110
233
  }
111
- // Create a new widget.
112
- const widget = new ChatWidget({
234
+ // Create a new widget for this model
235
+ const chatWidget = new ChatWidget({
113
236
  model,
114
237
  ...this._chatOptions,
115
238
  inputToolbarRegistry,
116
239
  area: 'sidebar'
117
240
  });
118
- const section = new ChatSection({
119
- widget,
241
+ // Create a chat with toolbar
242
+ const widget = new SidePanelWidget({
243
+ widget: chatWidget,
244
+ displayName: name,
120
245
  openInMain: this._openInMain,
121
246
  renameChat: this._renameChat,
122
- displayName
247
+ onClose: (name) => {
248
+ this.disposeLoadedModel(name);
249
+ }
123
250
  });
124
- this.addWidget(section);
125
- content.expand(this.widgets.length - 1);
126
- this._sectionAdded.emit(section);
127
- return widget;
128
- }
129
- /**
130
- * Invoke the update of the list of available chats.
131
- */
132
- updateChatList() {
133
- this._updateChatListDebouncer.invoke();
251
+ // Add to content panel
252
+ this.addWidget(widget);
253
+ this.update();
254
+ this._currentWidget = widget;
255
+ this._currentWidget.nameChanged.connect(this._modelNameChanged);
256
+ // Update selector to show current chat
257
+ if (this._chatSelectorPopup) {
258
+ this._chatSelectorPopup.setCurrentChat(name);
259
+ }
260
+ this._chatOpened.emit(chatWidget);
261
+ return chatWidget;
134
262
  }
135
263
  /**
136
- * Open a chat if it exists in the side panel.
264
+ * Open a chat if its model is already loaded.
137
265
  *
138
266
  * @param name - the name of the chat.
139
- * @returns a boolean, whether the chat existed in the side panel or not.
267
+ * @returns a boolean, whether the chat model was already loaded or not.
140
268
  */
141
- openIfExists(name) {
142
- const index = this._getChatIndex(name);
143
- if (index > -1) {
144
- this._expandChat(index);
269
+ openIfLoaded(name) {
270
+ const model = this._loadedModels.get(name);
271
+ if (model) {
272
+ this._open(name);
273
+ return true;
145
274
  }
146
- return index > -1;
275
+ return false;
147
276
  }
148
277
  /**
149
278
  * A message handler invoked on an `'after-attach'` message.
@@ -151,82 +280,65 @@ export class MultiChatPanel extends SidePanel {
151
280
  onAfterAttach() {
152
281
  var _a, _b;
153
282
  (_b = (_a = this._openChatWidget) === null || _a === void 0 ? void 0 : _a.renderPromise) === null || _b === void 0 ? void 0 : _b.then(() => this.updateChatList());
154
- }
155
- /**
156
- * Return the index of the chat in the list (-1 if not opened).
157
- *
158
- * @param name - the chat name.
159
- */
160
- _getChatIndex(name) {
161
- return this.sections.findIndex(section => { var _a; return ((_a = section.model) === null || _a === void 0 ? void 0 : _a.name) === name; });
162
- }
163
- /**
164
- * Expand the chat from its index.
165
- */
166
- _expandChat(index) {
167
- if (!this.widgets[index].isVisible) {
168
- this.content.expand(index);
283
+ // Attach the popup to the document body
284
+ if (this._chatSelectorPopup && !this._chatSelectorPopup.isAttached) {
285
+ Widget.attach(this._chatSelectorPopup, document.body);
169
286
  }
170
287
  }
171
288
  /**
172
- * Handle `change` events for the HTMLSelect component.
289
+ * A message handler invoked on an `'before-detach'` message.
173
290
  */
174
- async _chatSelected(event) {
175
- const selection = event.target.value;
176
- if (selection === '-') {
177
- return;
178
- }
179
- if (this._createModel) {
180
- const addChatArgs = await this._createModel(selection);
181
- this.addChat(addChatArgs);
291
+ onBeforeDetach() {
292
+ // Detach the popup
293
+ if (this._chatSelectorPopup && this._chatSelectorPopup.isAttached) {
294
+ Widget.detach(this._chatSelectorPopup);
182
295
  }
183
- event.target.selectedIndex = 0;
184
296
  }
185
297
  /**
186
- * Triggered when a section is toggled. If the section is opened, all others
187
- * sections are closed.
298
+ * Dispose of the resources held by the widget.
188
299
  */
189
- _onExpansionToggled(panel, index) {
190
- if (!this.widgets[index].isVisible) {
191
- return;
300
+ dispose() {
301
+ // Dispose all loaded models
302
+ for (const model of this._loadedModels.values()) {
303
+ model.dispose();
192
304
  }
193
- for (let i = 0; i < this.widgets.length; i++) {
194
- if (i !== index) {
195
- panel.collapse(i);
196
- }
305
+ this._loadedModels.clear();
306
+ if (this._chatSelectorPopup) {
307
+ this._chatSelectorPopup.dispose();
308
+ this._chatSelectorPopup = undefined;
197
309
  }
310
+ super.dispose();
198
311
  }
199
312
  }
200
313
  /**
201
- * The chat section containing a chat widget.
314
+ * A widget containing the chat and its toolbar.
202
315
  */
203
- export class ChatSection extends PanelWithToolbar {
204
- /**
205
- * Constructor of the chat section.
206
- */
316
+ class SidePanelWidget extends PanelWithToolbar {
207
317
  constructor(options) {
208
- var _a, _b, _c, _d, _e;
209
- super(options);
318
+ var _a, _b, _c, _d;
319
+ super();
210
320
  /**
211
- * Change the title when messages are unread.
212
- *
213
- * TODO: fix it upstream in @jupyterlab/ui-components.
214
- * Updating the title create a new Title widget, but does not attach again the
215
- * toolbar. The toolbar is attached only when the title widget is attached the first
216
- * time.
321
+ * Enable/disable unread icon.
217
322
  */
218
323
  this._unreadChanged = (_, unread) => {
219
324
  this._markAsRead.enabled = unread.length > 0;
220
325
  };
221
- this._spinner = new Spinner();
326
+ this._nameChanged = new Signal(this);
222
327
  this._chatWidget = options.widget;
223
- this.addWidget(this._chatWidget);
224
- this.addWidget(this._spinner);
225
- this.addClass(SECTION_CLASS);
328
+ this._displayName = (_a = options.displayName) !== null && _a !== void 0 ? _a : options.widget.model.name;
329
+ this.addClass(SIDEPANEL_WIDGET_CLASS);
226
330
  this.toolbar.addClass(TOOLBAR_CLASS);
227
- this._displayName =
228
- (_b = (_a = options.displayName) !== null && _a !== void 0 ? _a : options.widget.model.name) !== null && _b !== void 0 ? _b : 'Chat';
229
331
  this._updateTitle();
332
+ this.addWidget(this.toolbar);
333
+ // Add spinner while loading
334
+ const spinner = new Spinner();
335
+ this.addWidget(spinner);
336
+ this._chatWidget.model.ready.then(() => {
337
+ spinner.dispose();
338
+ });
339
+ // Add the chat widget
340
+ this.addWidget(this._chatWidget);
341
+ // Add toolbar buttons
230
342
  this._markAsRead = new ToolbarButton({
231
343
  icon: readIcon,
232
344
  iconLabel: 'Mark chat as read',
@@ -244,24 +356,32 @@ export class ChatSection extends PanelWithToolbar {
244
356
  iconLabel: 'Rename chat',
245
357
  className: 'jp-mod-styled',
246
358
  onClick: async () => {
247
- var _a, _b;
248
- const oldName = (_a = this.model.name) !== null && _a !== void 0 ? _a : 'Chat';
249
- const result = await InputDialog.getText({
250
- title: 'Rename Chat',
251
- text: this.model.name,
252
- placeholder: 'new-name'
253
- });
254
- if (!result.button.accept) {
255
- return; // user cancelled
359
+ if (!options.renameChat) {
360
+ return;
256
361
  }
257
- const newName = result.value;
258
- if (this.model && newName && newName !== oldName) {
259
- if (await ((_b = options.renameChat) === null || _b === void 0 ? void 0 : _b.call(options, oldName, newName))) {
362
+ let newName;
363
+ if (options.renameChat === true) {
364
+ // If rename chat is true, let's provide a input to select new name.
365
+ const result = await InputDialog.getText({
366
+ title: 'Rename Chat',
367
+ text: this.model.name,
368
+ placeholder: 'new-name'
369
+ });
370
+ if (!result.button.accept && result.value) {
371
+ return;
372
+ }
373
+ newName = result.value;
374
+ if (newName) {
260
375
  this.model.name = newName;
261
- this._displayName = newName;
262
- this._updateTitle();
263
376
  }
264
377
  }
378
+ else {
379
+ // Rename chat is a function, let's call it.
380
+ newName = await options.renameChat(this.model.name);
381
+ }
382
+ if (newName) {
383
+ this.name = newName;
384
+ }
265
385
  }
266
386
  });
267
387
  this.toolbar.addItem('rename', renameButton);
@@ -275,8 +395,7 @@ export class ChatSection extends PanelWithToolbar {
275
395
  var _a;
276
396
  const name = this.model.name;
277
397
  if (await ((_a = options.openInMain) === null || _a === void 0 ? void 0 : _a.call(options, name))) {
278
- this.model.dispose();
279
- this.dispose();
398
+ options.onClose(this._displayName);
280
399
  }
281
400
  }
282
401
  });
@@ -287,33 +406,16 @@ export class ChatSection extends PanelWithToolbar {
287
406
  iconLabel: 'Close the chat',
288
407
  className: 'jp-mod-styled',
289
408
  onClick: () => {
290
- this.model.dispose();
291
- this.dispose();
409
+ options.onClose(this._displayName);
292
410
  }
293
411
  });
294
412
  this.toolbar.addItem('close', closeButton);
295
- (_c = this.model.unreadChanged) === null || _c === void 0 ? void 0 : _c.connect(this._unreadChanged);
296
- this._markAsRead.enabled = ((_e = (_d = this.model) === null || _d === void 0 ? void 0 : _d.unreadMessages.length) !== null && _e !== void 0 ? _e : 0) > 0;
297
- options.widget.node.style.height = '100%';
298
- /**
299
- * Remove the spinner when the chat is ready.
300
- */
301
- this.model.ready.then(() => {
302
- this._spinner.dispose();
303
- });
304
- }
305
- /**
306
- * The display name.
307
- */
308
- get displayName() {
309
- return this._displayName;
310
- }
311
- set displayName(value) {
312
- this._displayName = value;
313
- this._updateTitle();
413
+ // Update mark as read button state
414
+ (_b = this.model.unreadChanged) === null || _b === void 0 ? void 0 : _b.connect(this._unreadChanged);
415
+ this._markAsRead.enabled = ((_d = (_c = this.model) === null || _c === void 0 ? void 0 : _c.unreadMessages.length) !== null && _d !== void 0 ? _d : 0) > 0;
314
416
  }
315
417
  /**
316
- * The chat widget of the section.
418
+ * The chat widget embedded in the sidepanel widget.
317
419
  */
318
420
  get widget() {
319
421
  return this._chatWidget;
@@ -324,6 +426,27 @@ export class ChatSection extends PanelWithToolbar {
324
426
  get model() {
325
427
  return this._chatWidget.model;
326
428
  }
429
+ /**
430
+ * The displayed name of the widget.
431
+ */
432
+ get name() {
433
+ return this._displayName;
434
+ }
435
+ set name(value) {
436
+ const old = this._displayName;
437
+ if (old === value) {
438
+ return;
439
+ }
440
+ this._displayName = value;
441
+ this._updateTitle();
442
+ this._nameChanged.emit({ old, new: value });
443
+ }
444
+ /**
445
+ * A signal emitting when the name has changed.
446
+ */
447
+ get nameChanged() {
448
+ return this._nameChanged;
449
+ }
327
450
  /**
328
451
  * Dispose of the resources held by the widget.
329
452
  */
@@ -336,25 +459,99 @@ export class ChatSection extends PanelWithToolbar {
336
459
  super.dispose();
337
460
  }
338
461
  /**
339
- * * Update the section’s title based on the chat name.
340
- * */
462
+ * Update the title based on the chat name.
463
+ */
341
464
  _updateTitle() {
342
- this.title.label = this._displayName;
465
+ this.title.label = this.model.name;
343
466
  this.title.caption = this._displayName;
467
+ const titleElement = document.createElement('span');
468
+ titleElement.classList.add('jp-chat-sidepanel-widget-title');
469
+ titleElement.title = this._displayName;
470
+ titleElement.textContent = this._displayName;
471
+ // Dispose of the previous widget.
472
+ if (this._titleWidget) {
473
+ this._titleWidget.dispose();
474
+ }
475
+ // Insert the new title widget in toolbar.
476
+ this._titleWidget = new Widget({ node: titleElement });
477
+ this.toolbar.insertItem(0, 'title', this._titleWidget);
344
478
  }
345
479
  }
346
480
  /**
347
- * A component to select a chat from the drive.
481
+ * A search input component for selecting a chat.
348
482
  */
349
- function ChatSelect({ chatNamesChanged, handleChange }) {
350
- // An object associating a chat name to its path. Both are purely indicative, the name
351
- // is the section title and the path is used as caption.
352
- const [chatNames, setChatNames] = useState({});
353
- // Update the chat list.
354
- chatNamesChanged.connect((_, chatNames) => {
355
- setChatNames(chatNames);
356
- });
357
- return (React.createElement(HTMLSelect, { key: Object.keys(chatNames).join(), onChange: handleChange, value: "-" },
358
- React.createElement("option", { value: "-", disabled: true, hidden: true }, "Open a chat"),
359
- Object.keys(chatNames).map(name => (React.createElement("option", { value: chatNames[name] }, name)))));
483
+ function ChatSearchInput({ selectChat, getPopup, chatOpened }) {
484
+ const [query, setQuery] = useState('');
485
+ const inputRef = useRef(null);
486
+ useEffect(() => {
487
+ const resetQuery = () => {
488
+ setQuery('');
489
+ };
490
+ chatOpened.connect(resetQuery);
491
+ return () => {
492
+ chatOpened.disconnect(resetQuery);
493
+ };
494
+ }, [chatOpened]);
495
+ const handleInputChange = (event) => {
496
+ const value = event.target.value;
497
+ setQuery(value);
498
+ const popup = getPopup();
499
+ if (popup) {
500
+ popup.setQuery(value);
501
+ if (!popup.isVisible && value) {
502
+ popup.show();
503
+ }
504
+ }
505
+ };
506
+ const handleInputFocus = () => {
507
+ const popup = getPopup();
508
+ if (popup && inputRef.current) {
509
+ // Set anchor element before showing
510
+ popup.anchor = inputRef.current;
511
+ popup.setQuery(query);
512
+ popup.show();
513
+ }
514
+ };
515
+ const handleInputClick = () => {
516
+ var _a;
517
+ const popup = getPopup();
518
+ if (popup && inputRef.current && !popup.isVisible) {
519
+ popup.anchor = inputRef.current;
520
+ popup.setQuery(query);
521
+ popup.show();
522
+ }
523
+ // Force focus on input.
524
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
525
+ };
526
+ const handleKeyDown = (event) => {
527
+ const popup = getPopup();
528
+ if (!popup || !popup.isVisible) {
529
+ return;
530
+ }
531
+ let value;
532
+ switch (event.key) {
533
+ case 'ArrowDown':
534
+ event.preventDefault();
535
+ popup.selectNext();
536
+ break;
537
+ case 'ArrowUp':
538
+ event.preventDefault();
539
+ popup.selectPrevious();
540
+ break;
541
+ case 'Enter':
542
+ event.preventDefault();
543
+ value = popup.getSelectedValue();
544
+ if (value) {
545
+ selectChat(value);
546
+ popup.hide();
547
+ }
548
+ break;
549
+ case 'Escape':
550
+ event.preventDefault();
551
+ popup.hide();
552
+ setQuery('');
553
+ break;
554
+ }
555
+ };
556
+ return (React.createElement("input", { ref: inputRef, type: "text", placeholder: "Select a chat", value: query, onChange: handleInputChange, onFocus: handleInputFocus, onClick: handleInputClick, onKeyDown: handleKeyDown, className: "jp-chat-search-input" }));
360
557
  }