@jupyter/chat 0.23.0-alpha.2 → 0.23.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.
@@ -10,7 +10,13 @@ import { ISignal, Signal } from '@lumino/signaling';
10
10
  import { IActiveCellManager } from './active-cell-manager';
11
11
  import { ISelectionWatcher } from './selection-watcher';
12
12
  import { IChatContext } from './model';
13
- import { IAttachment, INotebookAttachment, IUser } from './types';
13
+ import {
14
+ IAttachment,
15
+ IMessageMetadata,
16
+ INewMessage,
17
+ INotebookAttachment,
18
+ IUser
19
+ } from './types';
14
20
 
15
21
  /**
16
22
  * The chat input interface.
@@ -152,6 +158,31 @@ export interface IInputModel extends IDisposable {
152
158
  */
153
159
  clearMentions(): void;
154
160
 
161
+ /**
162
+ * Get the metadata to attach to the next message to send.
163
+ */
164
+ getMetadata(): IMessageMetadata;
165
+
166
+ /**
167
+ * Merge a patch into the metadata to attach to the next message to send.
168
+ *
169
+ * This is a *shallow* merge: each key in `patch` replaces the existing value
170
+ * at that key wholesale, rather than being merged recursively. To change a
171
+ * nested field, pass the whole new value for its top-level key. The patch is
172
+ * deep-copied, so mutating it afterwards won't affect the stored metadata.
173
+ */
174
+ updateMetadata(patch: IMessageMetadata): void;
175
+
176
+ /**
177
+ * Clear the metadata.
178
+ */
179
+ clearMetadata(): void;
180
+
181
+ /**
182
+ * A signal emitting when the metadata has changed.
183
+ */
184
+ readonly metadataChanged?: ISignal<IInputModel, IMessageMetadata>;
185
+
155
186
  /**
156
187
  * A signal emitting when disposing of the model.
157
188
  */
@@ -169,6 +200,7 @@ export class InputModel implements IInputModel {
169
200
  this._value = options.value || '';
170
201
  this._attachments = options.attachments || [];
171
202
  this._mentions = options.mentions || [];
203
+ this._metadata = options.metadata || {};
172
204
  this.cursorIndex = options.cursorIndex || this.value.length;
173
205
  this._activeCellManager = options.activeCellManager ?? null;
174
206
  this._selectionWatcher = options.selectionWatcher ?? null;
@@ -194,8 +226,32 @@ export class InputModel implements IInputModel {
194
226
  * Function to send a message.
195
227
  */
196
228
  send = (input: string): void => {
197
- this._onSend(input, this);
229
+ const message: INewMessage = {
230
+ body: input
231
+ };
232
+
233
+ // Add the attachments
234
+ if (this.attachments.length) {
235
+ message.attachments = [...this.attachments];
236
+ }
237
+
238
+ // Add the mentions
239
+ if (this.mentions.length) {
240
+ message.mentions = [...this.mentions];
241
+ }
242
+
243
+ // Add the metadata
244
+ if (Object.keys(this._metadata).length) {
245
+ message.metadata = { ...this._metadata };
246
+ }
247
+
248
+ // Send the message
249
+ this._onSend(message);
250
+
251
+ // Clear the input
198
252
  this.value = '';
253
+ this.clearAttachments();
254
+ this.clearMentions();
199
255
  };
200
256
 
201
257
  /**
@@ -460,6 +516,45 @@ export class InputModel implements IInputModel {
460
516
  this._mentions = [];
461
517
  };
462
518
 
519
+ /**
520
+ * Get the metadata to attach to the next message to send.
521
+ */
522
+ getMetadata(): IMessageMetadata {
523
+ return this._metadata;
524
+ }
525
+
526
+ /**
527
+ * Merge a patch into the metadata to attach to the next message to send.
528
+ *
529
+ * This is a *shallow* merge: each key in `patch` replaces the existing value
530
+ * at that key wholesale, rather than being merged recursively. To change a
531
+ * nested field, pass the whole new value for its top-level key. The patch is
532
+ * deep-copied, so mutating it afterwards won't affect the stored metadata.
533
+ */
534
+ updateMetadata = (patch: IMessageMetadata): void => {
535
+ // Deep-copy the patch so a caller mutating a nested field afterwards can't
536
+ // reach into the stored metadata. `this._metadata` only ever holds values
537
+ // that were themselves deep-copied on the way in, so a shallow spread of it
538
+ // is safe.
539
+ this._metadata = { ...this._metadata, ...structuredClone(patch) };
540
+ this._metadataChanged.emit(this._metadata);
541
+ };
542
+
543
+ /**
544
+ * Clear the metadata.
545
+ */
546
+ clearMetadata = (): void => {
547
+ this._metadata = {};
548
+ this._metadataChanged.emit(this._metadata);
549
+ };
550
+
551
+ /**
552
+ * A signal emitting when the metadata has changed.
553
+ */
554
+ get metadataChanged(): ISignal<IInputModel, IMessageMetadata> {
555
+ return this._metadataChanged;
556
+ }
557
+
463
558
  /**
464
559
  * Dispose the input model.
465
560
  */
@@ -486,13 +581,14 @@ export class InputModel implements IInputModel {
486
581
  }
487
582
 
488
583
  private _id: string;
489
- private _onSend: (input: string, model?: InputModel) => void;
584
+ private _onSend: (message: INewMessage) => void;
490
585
  private _chatContext?: IChatContext;
491
586
  private _value: string;
492
587
  private _cursorIndex: number | null = null;
493
588
  private _currentWord: string | null = null;
494
589
  private _attachments: IAttachment[];
495
590
  private _mentions: IUser[];
591
+ private _metadata: IMessageMetadata;
496
592
  private _activeCellManager: IActiveCellManager | null;
497
593
  private _selectionWatcher: ISelectionWatcher | null;
498
594
  private _documentManager: IDocumentManager | null;
@@ -503,6 +599,7 @@ export class InputModel implements IInputModel {
503
599
  private _configChanged = new Signal<IInputModel, InputModel.IConfig>(this);
504
600
  private _focusInputSignal = new Signal<InputModel, void>(this);
505
601
  private _attachmentsChanged = new Signal<InputModel, IAttachment[]>(this);
602
+ private _metadataChanged = new Signal<InputModel, IMessageMetadata>(this);
506
603
  private _onDisposed = new Signal<InputModel, void>(this);
507
604
  private _isDisposed = false;
508
605
  }
@@ -519,7 +616,7 @@ export namespace InputModel {
519
616
  * @param content - the content of the message.
520
617
  * @param model - the model of the input sending the message.
521
618
  */
522
- onSend: (content: string, model?: InputModel) => void;
619
+ onSend: (message: INewMessage) => void;
523
620
 
524
621
  /**
525
622
  * Function that should cancel the message edition.
@@ -541,6 +638,11 @@ export namespace InputModel {
541
638
  */
542
639
  mentions?: IUser[];
543
640
 
641
+ /**
642
+ * The initial metadata.
643
+ */
644
+ metadata?: IMessageMetadata;
645
+
544
646
  /**
545
647
  * The current cursor index.
546
648
  * This refers to the index of the character in front of the cursor.
package/src/model.ts CHANGED
@@ -264,7 +264,7 @@ export abstract class AbstractChatModel implements IChatModel {
264
264
  config: {
265
265
  sendWithShiftEnter: config.sendWithShiftEnter
266
266
  },
267
- onSend: (input: string) => this.sendMessage({ body: input })
267
+ onSend: this.sendMessage.bind(this)
268
268
  });
269
269
 
270
270
  this._commands = options.commands;
package/src/tokens.ts CHANGED
@@ -3,24 +3,40 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import { IWidgetTracker, MainAreaWidget } from '@jupyterlab/apputils';
6
+ import { IWidgetTracker } from '@jupyterlab/apputils';
7
7
  import { Token } from '@lumino/coreutils';
8
8
  import { Widget } from '@lumino/widgets';
9
9
 
10
10
  import { ChatWidget, Placeholder } from './widgets';
11
11
  import { IChatModel } from './model';
12
+ import { ChatArea } from './types';
12
13
 
13
14
  /**
14
- * The main area chat widget type.
15
+ * The interface for any widget displaying a chat (main area or side panel).
15
16
  */
16
- export type MainAreaChat = MainAreaWidget<ChatWidget> & {
17
+ export interface IChatPanel extends Widget {
18
+ /**
19
+ * The chat widget embedded in the panel.
20
+ */
21
+ widget: ChatWidget;
22
+ /**
23
+ * The model of the chat widget.
24
+ */
17
25
  model: IChatModel;
18
- };
26
+ /**
27
+ * The area of the panel.
28
+ */
29
+ area: ChatArea;
30
+ /**
31
+ * The chat panel toolbar.
32
+ */
33
+ toolbar: Widget;
34
+ }
19
35
 
20
36
  /**
21
37
  * the chat tracker type.
22
38
  */
23
- export type IChatTracker = IWidgetTracker<ChatWidget | MainAreaChat>;
39
+ export type IChatTracker = IWidgetTracker<IChatPanel>;
24
40
 
25
41
  /**
26
42
  * A chat tracker token.
package/src/types.ts CHANGED
@@ -174,10 +174,12 @@ export interface IChatHistory {
174
174
  /**
175
175
  * The content of a new message.
176
176
  */
177
- export interface INewMessage {
178
- body: string;
179
- id?: string;
180
- }
177
+ export type INewMessage<T = IUser, U = IAttachment> = Partial<
178
+ Pick<
179
+ IMessageContent<T, U>,
180
+ 'body' | 'attachments' | 'mentions' | 'metadata' | 'mime_model' | 'sender'
181
+ >
182
+ >;
181
183
 
182
184
  /**
183
185
  * The attachment type. Jupyter Chat allows for two types of attachments
package/src/utils.ts CHANGED
@@ -3,13 +3,17 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ import { CommandToolbarButton, ToolbarRegistry } from '@jupyterlab/apputils';
6
7
  import { CodeEditor } from '@jupyterlab/codeeditor';
7
8
  import { CodeMirrorEditor } from '@jupyterlab/codemirror';
8
9
  import { DocumentWidget } from '@jupyterlab/docregistry';
9
10
  import { FileEditor } from '@jupyterlab/fileeditor';
10
11
  import { Notebook } from '@jupyterlab/notebook';
12
+ import { IObservableList, ObservableList } from '@jupyterlab/observables';
13
+ import { CommandRegistry } from '@lumino/commands';
11
14
  import { Widget } from '@lumino/widgets';
12
15
 
16
+ import { IChatPanel } from './tokens';
13
17
  import { IUser } from './types';
14
18
 
15
19
  const MENTION_CLASS = 'jp-chat-mention';
@@ -81,3 +85,58 @@ export function replaceSpanToMention(content: string, user: IUser): string {
81
85
  const regex = new RegExp(mentionEl, 'g');
82
86
  return content.replace(regex, mention);
83
87
  }
88
+
89
+ /**
90
+ * Wraps a toolbar factory so that every CommandToolbarButton it creates
91
+ * automatically receives the panel's area as an arg.
92
+ * This lets commands branch on `args.area` without each plugin having to
93
+ * register a per-item toolbarRegistry.addFactory call.
94
+ */
95
+ export function injectAreaArg(
96
+ baseFactory: (
97
+ panel: IChatPanel
98
+ ) => IObservableList<ToolbarRegistry.IToolbarItem>,
99
+ commands: CommandRegistry
100
+ ): (panel: IChatPanel) => IObservableList<ToolbarRegistry.IToolbarItem> {
101
+ return (panel: IChatPanel) => {
102
+ const base = baseFactory(panel);
103
+
104
+ const inject = (
105
+ item: ToolbarRegistry.IToolbarItem
106
+ ): ToolbarRegistry.IToolbarItem => {
107
+ const { widget } = item;
108
+ if (!(widget instanceof CommandToolbarButton)) {
109
+ return item;
110
+ }
111
+ return {
112
+ name: item.name,
113
+ widget: new CommandToolbarButton({
114
+ commands,
115
+ id: widget.commandId,
116
+ args: { area: panel.area }
117
+ })
118
+ };
119
+ };
120
+
121
+ const wrapped = new ObservableList<ToolbarRegistry.IToolbarItem>({
122
+ values: Array.from({ length: base.length }, (_, i) => inject(base.get(i)))
123
+ });
124
+
125
+ base.changed.connect((_, change) => {
126
+ if (change.type === 'add') {
127
+ wrapped.insertAll(change.newIndex, change.newValues.map(inject));
128
+ } else if (change.type === 'remove') {
129
+ wrapped.removeRange(
130
+ change.oldIndex,
131
+ change.oldIndex + change.oldValues.length
132
+ );
133
+ } else if (change.type === 'set') {
134
+ change.newValues.forEach((item, i) =>
135
+ wrapped.set(change.newIndex + i, inject(item))
136
+ );
137
+ }
138
+ });
139
+
140
+ return wrapped;
141
+ };
142
+ }
@@ -8,12 +8,12 @@
8
8
  * Originally adapted from jupyterlab-chat's ChatPanel
9
9
  */
10
10
 
11
- import { InputDialog } from '@jupyterlab/apputils';
11
+ import { InputDialog, ToolbarRegistry } from '@jupyterlab/apputils';
12
+ import { IObservableList } from '@jupyterlab/observables';
12
13
  import { nullTranslator, TranslationBundle } from '@jupyterlab/translation';
13
14
  import {
14
15
  addIcon,
15
16
  closeIcon,
16
- launchIcon,
17
17
  PanelWithToolbar,
18
18
  ReactiveToolbar,
19
19
  ReactWidget,
@@ -37,9 +37,10 @@ import {
37
37
  IInputToolbarRegistryFactory
38
38
  } from '../components';
39
39
  import { TRANSLATION_DOMAIN } from '../context';
40
- import { chatIcon, readIcon } from '../icons';
40
+ import { chatIcon } from '../icons';
41
41
  import { IChatModel } from '../model';
42
- import { IChatPlaceholderFactory } from '../tokens';
42
+ import { IChatPanel, IChatPlaceholderFactory } from '../tokens';
43
+ import { ChatArea } from '../types';
43
44
 
44
45
  const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
45
46
  const ADD_BUTTON_CLASS = 'jp-chat-add';
@@ -86,10 +87,10 @@ export class MultiChatPanel extends PanelWithToolbar {
86
87
 
87
88
  this._chatOptions = options;
88
89
  this._inputToolbarFactory = options.inputToolbarFactory;
90
+ this._chatToolbarFactory = options.chatToolbarFactory;
89
91
 
90
92
  this._getChatNames = options.getChatNames;
91
93
  this._createModel = options.createModel;
92
- this._openInMain = options.openInMain;
93
94
  this._renameChat = options.renameChat;
94
95
  this._placeholderFactory = options.placeholderFactory;
95
96
 
@@ -149,9 +150,9 @@ export class MultiChatPanel extends PanelWithToolbar {
149
150
  }
150
151
 
151
152
  /**
152
- * A signal emitting when a chat widget is opened in the panel.
153
+ * A signal emitting when a chat panel is opened in the sidepanel.
153
154
  */
154
- get chatOpened(): ISignal<MultiChatPanel, ChatWidget> {
155
+ get chatOpened(): ISignal<MultiChatPanel, IChatPanel> {
155
156
  return this._chatOpened;
156
157
  }
157
158
 
@@ -293,8 +294,8 @@ export class MultiChatPanel extends PanelWithToolbar {
293
294
  const widget = new SidePanelWidget({
294
295
  widget: chatWidget,
295
296
  displayName: name,
296
- openInMain: this._openInMain,
297
297
  renameChat: this._renameChat,
298
+ toolbarFactory: this._chatToolbarFactory,
298
299
  onClose: (name: string, disposeModel = true) => {
299
300
  this.unsetLoadedModel(name, disposeModel);
300
301
  },
@@ -313,7 +314,7 @@ export class MultiChatPanel extends PanelWithToolbar {
313
314
  this._chatSelectorPopup.setCurrentChat(name);
314
315
  }
315
316
 
316
- this._chatOpened.emit(chatWidget);
317
+ this._chatOpened.emit(widget);
317
318
  return chatWidget;
318
319
  }
319
320
 
@@ -437,20 +438,22 @@ export class MultiChatPanel extends PanelWithToolbar {
437
438
  this._chatSelectorPopup?.hide();
438
439
  };
439
440
 
440
- private _chatOpened = new Signal<MultiChatPanel, ChatWidget>(this);
441
+ private _chatOpened = new Signal<MultiChatPanel, IChatPanel>(this);
441
442
  private _chatNamesChanged = new Signal<
442
443
  MultiChatPanel,
443
444
  { [name: string]: string }
444
445
  >(this);
445
446
  private _chatOptions: Omit<Chat.IOptions, 'model' | 'inputToolbarRegistry'>;
446
447
  private _inputToolbarFactory?: IInputToolbarRegistryFactory;
448
+ private _chatToolbarFactory?: (
449
+ panel: IChatPanel
450
+ ) => IObservableList<ToolbarRegistry.IToolbarItem>;
447
451
  private _updateChatListDebouncer: Debouncer;
448
452
 
449
453
  private _createModel?: (
450
454
  name?: string
451
455
  ) => Promise<MultiChatPanel.IOpenChatArgs>;
452
456
  private _getChatNames?: () => Promise<{ [name: string]: string }>;
453
- private _openInMain?: (name: string) => Promise<boolean>;
454
457
  private _renameChat?: boolean | ((oldName: string) => Promise<string | null>);
455
458
  private _placeholderFactory?: IChatPlaceholderFactory;
456
459
  private _openChatWidget?: ReactWidget;
@@ -476,6 +479,12 @@ export namespace MultiChatPanel {
476
479
  * The input toolbar factory;
477
480
  */
478
481
  inputToolbarFactory?: IInputToolbarRegistryFactory;
482
+ /**
483
+ * An optional toolbar factory for each opened chat.
484
+ */
485
+ chatToolbarFactory?: (
486
+ panel: IChatPanel
487
+ ) => IObservableList<ToolbarRegistry.IToolbarItem>;
479
488
  /**
480
489
  * An optional callback to create a chat model.
481
490
  *
@@ -489,12 +498,6 @@ export namespace MultiChatPanel {
489
498
  * @returns an object mapping chat display names to identifiers.
490
499
  */
491
500
  getChatNames?: () => Promise<{ [name: string]: string }>;
492
- /**
493
- * An optional callback to open the chat in the main area.
494
- *
495
- * @param name - the name of the chat to move.
496
- */
497
- openInMain?: (name: string) => Promise<boolean>;
498
501
  /**
499
502
  * An optional callback to rename a chat.
500
503
  *
@@ -531,7 +534,7 @@ export namespace MultiChatPanel {
531
534
  /**
532
535
  * A widget containing the chat and its toolbar.
533
536
  */
534
- class SidePanelWidget extends ReactivePanelWithToolbar {
537
+ class SidePanelWidget extends ReactivePanelWithToolbar implements IChatPanel {
535
538
  constructor(options: SidePanelWidget.IOptions) {
536
539
  super();
537
540
  this._chatWidget = options.widget;
@@ -557,19 +560,6 @@ class SidePanelWidget extends ReactivePanelWithToolbar {
557
560
  // Add the chat widget
558
561
  this.addWidget(this._chatWidget);
559
562
 
560
- // Add toolbar buttons
561
- this._markAsRead = new ToolbarButton({
562
- icon: readIcon,
563
- iconLabel: trans.__('Mark chat as read'),
564
- className: 'jp-mod-styled',
565
- onClick: () => {
566
- if (this.model) {
567
- this.model.unreadMessages = [];
568
- }
569
- }
570
- });
571
- this.toolbar.addItem('markRead', this._markAsRead);
572
-
573
563
  if (options.renameChat) {
574
564
  const renameButton = new ToolbarButton({
575
565
  iconClass: 'jp-EditIcon',
@@ -606,21 +596,6 @@ class SidePanelWidget extends ReactivePanelWithToolbar {
606
596
  this.toolbar.addItem('rename', renameButton);
607
597
  }
608
598
 
609
- if (options.openInMain) {
610
- const moveToMain = new ToolbarButton({
611
- icon: launchIcon,
612
- iconLabel: trans.__('Move the chat to the main area'),
613
- className: 'jp-mod-styled',
614
- onClick: async () => {
615
- const name = this.model.name;
616
- if (await options.openInMain?.(name)) {
617
- options.onClose(this._displayName, false);
618
- }
619
- }
620
- });
621
- this.toolbar.addItem('moveMain', moveToMain);
622
- }
623
-
624
599
  const closeButton = new ToolbarButton({
625
600
  icon: closeIcon,
626
601
  iconLabel: trans.__('Close the chat'),
@@ -631,9 +606,24 @@ class SidePanelWidget extends ReactivePanelWithToolbar {
631
606
  });
632
607
  this.toolbar.addItem('close', closeButton);
633
608
 
634
- // Update mark as read button state
635
- this.model.unreadChanged?.connect(this._unreadChanged);
636
- this._markAsRead.enabled = (this.model?.unreadMessages.length ?? 0) > 0;
609
+ if (options.toolbarFactory) {
610
+ const items = options.toolbarFactory(this);
611
+ for (let i = 0; i < items.length; i++) {
612
+ const { name, widget } = items.get(i);
613
+ this.toolbar.insertBefore('close', name, widget);
614
+ }
615
+ items.changed.connect((_, change) => {
616
+ if (change.type === 'add') {
617
+ for (const { name, widget } of change.newValues) {
618
+ this.toolbar.insertBefore('close', name, widget);
619
+ }
620
+ } else if (change.type === 'remove') {
621
+ for (const { widget } of change.oldValues) {
622
+ widget.dispose();
623
+ }
624
+ }
625
+ });
626
+ }
637
627
  }
638
628
 
639
629
  protected onAfterAttach(msg: Message): void {
@@ -646,6 +636,13 @@ class SidePanelWidget extends ReactivePanelWithToolbar {
646
636
  this._updateReactiveToolbar();
647
637
  }
648
638
 
639
+ /**
640
+ * The area of the widget.
641
+ */
642
+ get area(): ChatArea {
643
+ return 'sidebar';
644
+ }
645
+
649
646
  /**
650
647
  * The chat widget embedded in the sidepanel widget.
651
648
  */
@@ -687,10 +684,6 @@ class SidePanelWidget extends ReactivePanelWithToolbar {
687
684
  * Dispose of the resources held by the widget.
688
685
  */
689
686
  dispose(): void {
690
- const model = this.model;
691
- if (model) {
692
- model.unreadChanged?.disconnect(this._unreadChanged);
693
- }
694
687
  super.dispose();
695
688
  }
696
689
 
@@ -716,13 +709,6 @@ class SidePanelWidget extends ReactivePanelWithToolbar {
716
709
  this.toolbar.insertItem(0, 'title', this._titleWidget);
717
710
  }
718
711
 
719
- /**
720
- * Enable/disable unread icon.
721
- */
722
- private _unreadChanged = (_: IChatModel, unread: number[]) => {
723
- this._markAsRead.enabled = unread.length > 0;
724
- };
725
-
726
712
  /**
727
713
  * Trigger reactive toolbar overflow computation from rendered toolbar size.
728
714
  */
@@ -739,7 +725,6 @@ class SidePanelWidget extends ReactivePanelWithToolbar {
739
725
  }
740
726
 
741
727
  private _chatWidget: ChatWidget;
742
- private _markAsRead: ToolbarButton;
743
728
  private _displayName: string;
744
729
  private _titleWidget: Widget | undefined;
745
730
  private _nameChanged = new Signal<
@@ -768,14 +753,16 @@ namespace SidePanelWidget {
768
753
  * The displayed name of the chat.
769
754
  */
770
755
  displayName?: string;
771
- /**
772
- * The callback to open the chat in main area.
773
- */
774
- openInMain?: (name: string) => Promise<boolean>;
775
756
  /**
776
757
  * The callback to rename the chat.
777
758
  */
778
759
  renameChat?: boolean | ((oldName: string) => Promise<string | null>);
760
+ /**
761
+ * An optional toolbar factory.
762
+ */
763
+ toolbarFactory?: (
764
+ panel: IChatPanel
765
+ ) => IObservableList<ToolbarRegistry.IToolbarItem>;
779
766
  /**
780
767
  * The translation bundle.
781
768
  */
@@ -795,7 +782,7 @@ type ChatSearchInputProps = {
795
782
  /**
796
783
  * Signal emitting when a chat is opened.
797
784
  */
798
- chatOpened: ISignal<MultiChatPanel, ChatWidget>;
785
+ chatOpened: ISignal<MultiChatPanel, IChatPanel>;
799
786
  /**
800
787
  * The translation bundle.
801
788
  */