@jupyterlite/ai 0.17.0 → 0.18.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.
package/lib/chat-model.js CHANGED
@@ -5,6 +5,7 @@ import { UUID } from '@lumino/coreutils';
5
5
  import { Debouncer } from '@lumino/polling';
6
6
  import { Signal } from '@lumino/signaling';
7
7
  import { AI_AVATAR } from './icons';
8
+ import { modelSupportsAudio, modelSupportsImages, modelSupportsPdf } from './providers/model-info';
8
9
  /**
9
10
  * AI Chat Model implementation that provides chat functionality tool integration,
10
11
  * and MCP server support.
@@ -27,10 +28,14 @@ export class AIChatModel extends AbstractChatModel {
27
28
  this._user = options.user;
28
29
  this._agentManager = options.agentManager;
29
30
  this._contentsManager = options.contentsManager;
31
+ this._providerRegistry = options.providerRegistry;
30
32
  // Listen for agent events
31
33
  this._agentManager.agentEvent.connect(this._onAgentEvent, this);
32
34
  // Listen for settings changes to update chat behavior
33
35
  this._settingsModel.stateChanged.connect(this._onSettingsChanged, this);
36
+ // Rebuild history when the model changes
37
+ this._agentManager.activeProviderChanged.connect(this._onModelChanged, this);
38
+ this._settingsModel.stateChanged.connect(this._onModelChanged, this);
34
39
  this._autosaveDebouncer = new Debouncer(this.save, 3000);
35
40
  }
36
41
  /**
@@ -81,17 +86,20 @@ export class AIChatModel extends AbstractChatModel {
81
86
  return this._autosave;
82
87
  }
83
88
  set autosave(value) {
89
+ if (value === this._autosave) {
90
+ return;
91
+ }
84
92
  this._autosave = value;
85
93
  this._autosaveChanged.emit(value);
86
94
  if (value) {
87
95
  this.messagesUpdated.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
88
96
  this.messageChanged.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
89
- this._autosaveDebouncer.invoke();
90
97
  }
91
98
  else {
92
99
  this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
93
100
  this.messageChanged.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
94
101
  }
102
+ this._autosaveDebouncer.invoke();
95
103
  }
96
104
  /**
97
105
  * A signal emitting when the autosave flag changed.
@@ -112,7 +120,7 @@ export class AIChatModel extends AbstractChatModel {
112
120
  return this._agentManager.tokenUsageChanged;
113
121
  }
114
122
  /**
115
- * Get the agent manager associated to the model.
123
+ * The agent manager used in the model.
116
124
  */
117
125
  get agentManager() {
118
126
  return this._agentManager;
@@ -127,6 +135,7 @@ export class AIChatModel extends AbstractChatModel {
127
135
  * Dispose of the model.
128
136
  */
129
137
  dispose() {
138
+ this.stopStreaming();
130
139
  this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
131
140
  super.dispose();
132
141
  }
@@ -142,7 +151,7 @@ export class AIChatModel extends AbstractChatModel {
142
151
  stopStreaming: () => this.stopStreaming(),
143
152
  clearMessages: () => this.clearMessages(),
144
153
  agentManager: this._agentManager,
145
- addSystemMessage: (body) => this.addSystemMessage(body)
154
+ addSystemMessage: (body) => this._addSystemMessage(body)
146
155
  };
147
156
  }
148
157
  /**
@@ -155,14 +164,29 @@ export class AIChatModel extends AbstractChatModel {
155
164
  * Clears all messages from the chat and resets conversation state.
156
165
  */
157
166
  clearMessages = async () => {
167
+ this.stopStreaming();
168
+ this._messageQueue = [];
169
+ this._isBusy = false;
170
+ this._queueMessageId = null;
171
+ this._currentStreamingMessage = null;
158
172
  this.messagesDeleted(0, this.messages.length);
173
+ this.title = null;
159
174
  this._toolContexts.clear();
160
175
  await this._agentManager.clearHistory();
161
176
  };
177
+ /**
178
+ * Overrides messageAdded to ensure queued messages stay at the bottom.
179
+ */
180
+ messageAdded(message) {
181
+ super.messageAdded(message);
182
+ if (this._queueMessageId && message.id !== this._queueMessageId) {
183
+ this._updateQueueUI();
184
+ }
185
+ }
162
186
  /**
163
187
  * Adds a non-user message to the chat (used by chat commands).
164
188
  */
165
- addSystemMessage(body) {
189
+ _addSystemMessage(body) {
166
190
  const message = {
167
191
  body,
168
192
  sender: this._getAIUser(),
@@ -193,9 +217,9 @@ export class AIChatModel extends AbstractChatModel {
193
217
  raw_time: false,
194
218
  attachments: [...this.input.attachments]
195
219
  };
196
- this.messageAdded(userMessage);
197
220
  // Check if we have valid configuration
198
221
  if (!this._agentManager.hasValidConfig()) {
222
+ this.messageAdded(userMessage);
199
223
  const errorMessage = {
200
224
  body: 'Please configure your AI settings first. Open the AI Settings to set your API key and model.',
201
225
  sender: this._getAIUser(),
@@ -207,30 +231,48 @@ export class AIChatModel extends AbstractChatModel {
207
231
  this.messageAdded(errorMessage);
208
232
  return;
209
233
  }
234
+ if (this._isBusy) {
235
+ this._messageQueue.push({
236
+ id: UUID.uuid4(),
237
+ body: message.body,
238
+ _originalMsg: userMessage
239
+ });
240
+ this.input.clearAttachments();
241
+ this._updateQueueUI();
242
+ return;
243
+ }
244
+ this._isBusy = true;
245
+ this.messageAdded(userMessage);
246
+ this.input.clearAttachments();
247
+ await this._processMessage(userMessage);
248
+ }
249
+ /**
250
+ * Internal method to process attachments and send the message to the agent.
251
+ */
252
+ async _processMessage(userMessage) {
210
253
  try {
211
- // Process attachments and add their content to the message
212
- let enhancedMessage = message.body;
213
- if (this.input.attachments.length > 0) {
214
- const { textContents, binaryParts } = await Private.processAttachments(this.input.attachments, this.input.documentManager);
215
- this.input.clearAttachments();
216
- let textPart = message.body;
217
- if (textContents.length > 0) {
218
- textPart +=
219
- '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
220
- }
221
- if (binaryParts.length > 0) {
222
- enhancedMessage = [{ type: 'text', text: textPart }, ...binaryParts];
223
- }
224
- else {
225
- enhancedMessage = textPart;
226
- }
227
- }
228
254
  this.updateWriters([{ user: this._getAIUser() }]);
255
+ let enhancedMessage = userMessage.body;
256
+ if (userMessage.attachments && userMessage.attachments.length > 0) {
257
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
258
+ const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
259
+ const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
260
+ const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
261
+ enhancedMessage = await Private.processAttachments(userMessage.attachments, this.input.documentManager, userMessage.body, supportsImages, supportsPdf, supportsAudio);
262
+ }
229
263
  await this._agentManager.generateResponse(enhancedMessage);
230
264
  }
231
265
  catch (error) {
232
266
  const errorMessage = {
233
- body: `Error generating AI response: ${error.message}`,
267
+ body: '',
268
+ mime_model: {
269
+ data: {
270
+ 'application/vnd.jupyter.chat.components': 'error'
271
+ },
272
+ metadata: {
273
+ errorMessage: `Error generating AI response: ${error.message}`
274
+ }
275
+ },
234
276
  sender: this._getAIUser(),
235
277
  id: UUID.uuid4(),
236
278
  time: Date.now() / 1000,
@@ -240,8 +282,89 @@ export class AIChatModel extends AbstractChatModel {
240
282
  this.messageAdded(errorMessage);
241
283
  }
242
284
  finally {
285
+ this._drainQueue();
286
+ if (this._settingsModel.config.autoTitle &&
287
+ (this.messages.length <= 5 || this.title === null)) {
288
+ try {
289
+ this.title = await this.requestTitle();
290
+ }
291
+ catch (e) {
292
+ console.warn('Error while generating a title\n', e);
293
+ }
294
+ }
295
+ }
296
+ }
297
+ /**
298
+ * Removes the message-queue chat component.
299
+ */
300
+ _removeQueueUI() {
301
+ if (this._queueMessageId) {
302
+ const existingMsg = this.messages.find(msg => msg.id === this._queueMessageId);
303
+ if (existingMsg) {
304
+ const idx = this.messages.indexOf(existingMsg);
305
+ if (idx !== -1) {
306
+ this.messagesDeleted(idx, 1);
307
+ }
308
+ }
309
+ this._queueMessageId = null;
310
+ }
311
+ }
312
+ /**
313
+ * Creates or updates the message-queue chat component.
314
+ */
315
+ _updateQueueUI() {
316
+ this._removeQueueUI();
317
+ if (this._messageQueue.length === 0) {
318
+ return;
319
+ }
320
+ const queueBody = {
321
+ data: {
322
+ 'application/vnd.jupyter.chat.components': 'message-queue'
323
+ },
324
+ metadata: {
325
+ messages: this._messageQueue.map(m => ({
326
+ id: m.id,
327
+ body: m.body,
328
+ attachments: m._originalMsg.attachments
329
+ })),
330
+ targetId: this.name
331
+ }
332
+ };
333
+ this._queueMessageId = UUID.uuid4();
334
+ const queueMessage = {
335
+ body: '',
336
+ mime_model: queueBody,
337
+ sender: { username: 'system', display_name: '' },
338
+ id: this._queueMessageId,
339
+ time: Date.now() / 1000,
340
+ type: 'msg',
341
+ raw_time: false
342
+ };
343
+ this.messageAdded(queueMessage);
344
+ }
345
+ /**
346
+ * Processes the next message in the queue, or marks the agent as idle.
347
+ */
348
+ async _drainQueue() {
349
+ if (this._messageQueue.length === 0) {
350
+ this._isBusy = false;
243
351
  this.updateWriters([]);
352
+ this._removeQueueUI();
353
+ return;
244
354
  }
355
+ // Dequeue and push to chat
356
+ const next = this._messageQueue.shift();
357
+ next._originalMsg.time = Date.now() / 1000;
358
+ this.messageAdded(next._originalMsg);
359
+ await this._processMessage(next._originalMsg);
360
+ }
361
+ /**
362
+ * Removes a queued message by its ID.
363
+ * @param messageId The ID of the queued message to remove
364
+ */
365
+ removeQueuedMessage(messageId) {
366
+ this._messageQueue = this._messageQueue.filter(msg => msg.id !== messageId);
367
+ this._updateQueueUI();
245
368
  }
246
369
  /**
247
370
  * Save the chat as json file.
@@ -327,7 +450,7 @@ export class AIChatModel extends AbstractChatModel {
327
450
  });
328
451
  await this.clearMessages();
329
452
  this.messagesInserted(0, messages);
330
- this._agentManager.setHistory(messages);
453
+ await this._rebuildHistory();
331
454
  this.autosave = content.metadata?.autosave ?? false;
332
455
  this.title = content.metadata?.title ?? null;
333
456
  return true;
@@ -343,7 +466,7 @@ export class AIChatModel extends AbstractChatModel {
343
466
  const messages = [
344
467
  {
345
468
  role: 'system',
346
- content: "Generate a concise title (no more than 10 words) for the following conversation. Do not use formatting. Focus on the user's main intent."
469
+ content: "Generate a concise title (no more than 10 words) for the following conversation. Do not use formatting, quotes, or punctuation. Focus on the subject matter and specific content the user is working on, not on the actions taken (e.g. prefer 'Pandas DataFrame filtering' over 'Opening a notebook'). The title should be a noun phrase describing the topic."
347
470
  },
348
471
  {
349
472
  role: 'user',
@@ -362,6 +485,9 @@ export class AIChatModel extends AbstractChatModel {
362
485
  const attachmentMap = new Map(); // JSON → index
363
486
  const attachmentsList = []; // Actual attachments
364
487
  this.messages.forEach(message => {
488
+ if (message.content?.mime_model?.data?.['application/vnd.jupyter.chat.components'] === 'message-queue') {
489
+ return;
490
+ }
365
491
  let attachmentIndexes = [];
366
492
  if (message.attachments) {
367
493
  attachmentIndexes = message.attachments.map(attachment => {
@@ -420,6 +546,49 @@ export class AIChatModel extends AbstractChatModel {
420
546
  this.config = { ...config, enableCodeToolbar: true };
421
547
  // Agent manager handles agent recreation automatically via its own settings listener
422
548
  }
549
+ /**
550
+ * Rebuild history when the active model changes.
551
+ */
552
+ _onModelChanged() {
553
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
554
+ const modelKey = providerConfig
555
+ ? `${providerConfig.provider}:${providerConfig.model}`
556
+ : undefined;
557
+ if (modelKey && modelKey !== this._currentModelKey) {
558
+ this._currentModelKey = modelKey;
559
+ this._rebuildHistory().catch(e => console.warn('Failed to rebuild history on model change:', e));
560
+ }
561
+ }
562
+ /**
563
+ * Rebuilds the agent history from the current messages.
564
+ * For vision-capable models, re-reads binary attachments from disk.
565
+ * For text-only models, uses message text only.
566
+ */
567
+ async _rebuildHistory() {
568
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
569
+ const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
570
+ const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
571
+ const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
572
+ const modelMessages = [];
573
+ for (const msg of this.messages) {
574
+ const isAI = msg.sender.username === 'ai-assistant';
575
+ if (!isAI && msg.attachments?.length) {
576
+ const enhancedContent = await Private.processAttachments(msg.attachments, this.input.documentManager, msg.body, supportsImages, supportsPdf, supportsAudio);
577
+ modelMessages.push({
578
+ role: 'user',
579
+ content: enhancedContent
580
+ });
581
+ }
582
+ else if (msg.body) {
583
+ modelMessages.push({
584
+ role: isAI ? 'assistant' : 'user',
585
+ content: msg.body
586
+ });
587
+ }
588
+ // Skip messages with empty body like tool calls
589
+ }
590
+ this._agentManager.setHistory(modelMessages);
591
+ }
423
592
  /**
424
593
  * Handles events emitted by the agent manager.
425
594
  * @param event The event data containing type and payload
@@ -583,13 +752,18 @@ export class AIChatModel extends AbstractChatModel {
583
752
  body: '',
584
753
  mime_model: {
585
754
  data: {
586
- 'application/vnd.jupyter.chat.components': 'tool-call'
755
+ 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
587
756
  },
588
757
  metadata: {
589
- toolName: context.toolName,
590
- input: context.input,
591
- status: context.status,
592
- summary: context.summary
758
+ toolCalls: [
759
+ {
760
+ toolCallId: context.toolCallId,
761
+ title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
762
+ kind: context.toolName,
763
+ status: 'in_progress',
764
+ rawInput: context.input
765
+ }
766
+ ]
593
767
  }
594
768
  },
595
769
  sender: this._getAIUser(),
@@ -642,7 +816,15 @@ export class AIChatModel extends AbstractChatModel {
642
816
  */
643
817
  _handleErrorEvent(event) {
644
818
  this.messageAdded({
645
- body: `Error generating response: ${event.data.error.message}`,
819
+ body: '',
820
+ mime_model: {
821
+ data: {
822
+ 'application/vnd.jupyter.chat.components': 'error'
823
+ },
824
+ metadata: {
825
+ errorMessage: `Error generating response: ${event.data.error.message}`
826
+ }
827
+ },
646
828
  sender: this._getAIUser(),
647
829
  id: UUID.uuid4(),
648
830
  time: Date.now() / 1000,
@@ -658,7 +840,6 @@ export class AIChatModel extends AbstractChatModel {
658
840
  if (!context) {
659
841
  return;
660
842
  }
661
- context.approvalId = event.data.approvalId;
662
843
  context.input = JSON.stringify(event.data.args, null, 2);
663
844
  this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
664
845
  }
@@ -666,12 +847,12 @@ export class AIChatModel extends AbstractChatModel {
666
847
  * Handles tool approval resolved events from the AI agent.
667
848
  */
668
849
  _handleToolApprovalResolved(event) {
669
- const context = Array.from(this._toolContexts.values()).find(ctx => ctx.approvalId === event.data.approvalId);
850
+ const context = this._toolContexts.get(event.data.toolCallId);
670
851
  if (!context) {
671
852
  return;
672
853
  }
673
854
  const status = event.data.approved ? 'approved' : 'rejected';
674
- this._updateToolCallUI(context.toolCallId, status);
855
+ this._updateToolCallUI(event.data.toolCallId, status);
675
856
  if (!event.data.approved) {
676
857
  this._toolContexts.delete(context.toolCallId);
677
858
  }
@@ -692,31 +873,69 @@ export class AIChatModel extends AbstractChatModel {
692
873
  existingMessage.update({
693
874
  mime_model: {
694
875
  data: {
695
- 'application/vnd.jupyter.chat.components': 'tool-call'
876
+ 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
696
877
  },
697
878
  metadata: {
698
- toolName: context.toolName,
699
- input: context.input,
700
- status: context.status,
701
- summary: context.summary,
702
- output,
703
- targetId: this.name,
704
- approvalId: context.approvalId
879
+ toolCalls: [
880
+ {
881
+ toolCallId: context.toolCallId,
882
+ title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
883
+ kind: context.toolName,
884
+ status: context.status,
885
+ rawInput: context.input,
886
+ rawOutput: output,
887
+ sessionId: this.name,
888
+ permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
889
+ ...(status === 'awaiting_approval' && {
890
+ permissionOptions: [
891
+ { optionId: 'approve', name: 'Approve', kind: 'allow_once' },
892
+ { optionId: 'reject', name: 'Reject', kind: 'reject_once' }
893
+ ]
894
+ })
895
+ }
896
+ ]
705
897
  }
706
898
  }
707
899
  });
708
900
  }
901
+ /**
902
+ * The current message queue
903
+ */
904
+ get messageQueue() {
905
+ return this._messageQueue;
906
+ }
907
+ set messageQueue(value) {
908
+ this._messageQueue = value;
909
+ this._updateQueueUI();
910
+ if (this._messageQueue.length > 0 && !this._isBusy) {
911
+ this._drainQueue();
912
+ }
913
+ }
914
+ /**
915
+ * Whether the chat is busy
916
+ */
917
+ get isBusy() {
918
+ return this._isBusy;
919
+ }
920
+ set isBusy(value) {
921
+ this._isBusy = value;
922
+ }
709
923
  // Private fields
710
924
  _settingsModel;
711
925
  _user;
712
926
  _toolContexts = new Map();
713
927
  _agentManager;
928
+ _providerRegistry;
929
+ _currentModelKey;
714
930
  _currentStreamingMessage = null;
715
931
  _nameChanged = new Signal(this);
716
932
  _contentsManager;
717
933
  _autosave = false;
718
934
  _autosaveChanged = new Signal(this);
719
935
  _autosaveDebouncer;
936
+ _messageQueue = [];
937
+ _isBusy = false;
938
+ _queueMessageId = null;
720
939
  _title = null;
721
940
  _titleChanged = new Signal(this);
722
941
  }
@@ -804,16 +1023,21 @@ var Private;
804
1023
  }
805
1024
  Private.formatToolOutput = formatToolOutput;
806
1025
  /**
807
- * Processes file attachments and returns text contents and binary parts separately.
1026
+ * Processes file attachments and returns the message content with the attachments.
808
1027
  * @param attachments Array of file attachments to process
809
1028
  * @param documentManager Optional document manager for file operations
810
- * @returns Text contents and binary parts
1029
+ * @param body The message body
1030
+ * @param supportsImages Whether the model supports images
1031
+ * @param supportsPdf Whether the model supports pdfs
1032
+ * @param supportsAudio Whether the model supports audio
1033
+ * @returns Enhanced message content
811
1034
  */
812
- async function processAttachments(attachments, documentManager) {
1035
+ async function processAttachments(attachments, documentManager, body, supportsImages, supportsPdf, supportsAudio) {
813
1036
  const textContents = [];
814
- const binaryParts = [];
1037
+ const includedParts = [];
1038
+ const omittedNames = [];
815
1039
  if (!documentManager) {
816
- return { textContents, binaryParts };
1040
+ return body;
817
1041
  }
818
1042
  for (const attachment of attachments) {
819
1043
  try {
@@ -837,24 +1061,50 @@ var Private;
837
1061
  }
838
1062
  }
839
1063
  if (mimetype?.startsWith('image/')) {
840
- const data = await readBinaryAttachment(attachment, documentManager);
841
- if (data) {
842
- binaryParts.push({
843
- type: 'image',
844
- image: data,
845
- mediaType: mimetype
846
- });
1064
+ if (supportsImages) {
1065
+ const data = await readBinaryAttachment(attachment, documentManager);
1066
+ if (data) {
1067
+ includedParts.push({
1068
+ type: 'image',
1069
+ image: data,
1070
+ mediaType: mimetype
1071
+ });
1072
+ }
1073
+ }
1074
+ else {
1075
+ omittedNames.push(PathExt.basename(attachment.value));
847
1076
  }
848
1077
  }
849
1078
  else if (mimetype === 'application/pdf') {
850
- const data = await readBinaryAttachment(attachment, documentManager);
851
- if (data) {
852
- binaryParts.push({
853
- type: 'file',
854
- data,
855
- mediaType: mimetype,
856
- filename: PathExt.basename(attachment.value)
857
- });
1079
+ if (supportsPdf) {
1080
+ const data = await readBinaryAttachment(attachment, documentManager);
1081
+ if (data) {
1082
+ includedParts.push({
1083
+ type: 'file',
1084
+ data,
1085
+ mediaType: mimetype,
1086
+ filename: PathExt.basename(attachment.value)
1087
+ });
1088
+ }
1089
+ }
1090
+ else {
1091
+ omittedNames.push(PathExt.basename(attachment.value));
1092
+ }
1093
+ }
1094
+ else if (mimetype?.startsWith('audio/')) {
1095
+ if (supportsAudio) {
1096
+ const data = await readBinaryAttachment(attachment, documentManager);
1097
+ if (data) {
1098
+ includedParts.push({
1099
+ type: 'file',
1100
+ data,
1101
+ mediaType: mimetype,
1102
+ filename: PathExt.basename(attachment.value)
1103
+ });
1104
+ }
1105
+ }
1106
+ else {
1107
+ omittedNames.push(PathExt.basename(attachment.value));
858
1108
  }
859
1109
  }
860
1110
  else {
@@ -874,7 +1124,16 @@ var Private;
874
1124
  textContents.push(`**File: ${attachment.value}** (Could not read file)`);
875
1125
  }
876
1126
  }
877
- return { textContents, binaryParts };
1127
+ let textPart = body;
1128
+ if (textContents.length > 0) {
1129
+ textPart += '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
1130
+ }
1131
+ if (omittedNames.length > 0) {
1132
+ textPart += `\n[Attachments omitted (not supported by this model): ${omittedNames.join(', ')}.]`;
1133
+ }
1134
+ return includedParts.length > 0
1135
+ ? [{ type: 'text', text: textPart }, ...includedParts]
1136
+ : textPart;
878
1137
  }
879
1138
  Private.processAttachments = processAttachments;
880
1139
  /**
@@ -1,7 +1,7 @@
1
1
  import { ReactWidget } from '@jupyterlab/ui-components';
2
2
  import type { TranslationBundle } from '@jupyterlab/translation';
3
3
  import React from 'react';
4
- import { AIChatModel } from '../chat-model';
4
+ import { IAIChatModel } from '../tokens';
5
5
  /**
6
6
  * Properties for the SaveButton component.
7
7
  */
@@ -9,7 +9,7 @@ export interface ISaveButtonProps {
9
9
  /**
10
10
  * The chat model, used to listen for message changes for auto-save.
11
11
  */
12
- model: AIChatModel;
12
+ model: IAIChatModel;
13
13
  /**
14
14
  * The application language translator.
15
15
  */