@jupyterlite/ai 0.8.1 → 0.9.0-a1

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.
Files changed (162) hide show
  1. package/lib/agent.d.ts +243 -0
  2. package/lib/agent.js +627 -0
  3. package/lib/chat-model.d.ts +195 -0
  4. package/lib/chat-model.js +591 -0
  5. package/lib/completion/completion-provider.d.ts +93 -0
  6. package/lib/completion/completion-provider.js +235 -0
  7. package/lib/completion/index.d.ts +1 -0
  8. package/lib/completion/index.js +1 -0
  9. package/lib/components/clear-button.d.ts +18 -0
  10. package/lib/components/clear-button.js +31 -0
  11. package/lib/components/index.d.ts +3 -0
  12. package/lib/components/index.js +3 -0
  13. package/lib/components/model-select.d.ts +19 -0
  14. package/lib/components/model-select.js +154 -0
  15. package/lib/components/stop-button.d.ts +3 -3
  16. package/lib/components/stop-button.js +8 -9
  17. package/lib/components/token-usage-display.d.ts +45 -0
  18. package/lib/components/token-usage-display.js +74 -0
  19. package/lib/components/tool-select.d.ts +27 -0
  20. package/lib/components/tool-select.js +130 -0
  21. package/lib/icons.d.ts +3 -1
  22. package/lib/icons.js +10 -13
  23. package/lib/index.d.ts +5 -5
  24. package/lib/index.js +341 -169
  25. package/lib/mcp/browser.d.ts +68 -0
  26. package/lib/mcp/browser.js +132 -0
  27. package/lib/models/settings-model.d.ts +70 -0
  28. package/lib/models/settings-model.js +296 -0
  29. package/lib/providers/built-in-providers.d.ts +9 -0
  30. package/lib/providers/built-in-providers.js +266 -0
  31. package/lib/providers/models.d.ts +37 -0
  32. package/lib/providers/models.js +28 -0
  33. package/lib/providers/provider-registry.d.ts +94 -0
  34. package/lib/providers/provider-registry.js +155 -0
  35. package/lib/tokens.d.ts +167 -86
  36. package/lib/tokens.js +25 -12
  37. package/lib/tools/commands.d.ts +11 -0
  38. package/lib/tools/commands.js +126 -0
  39. package/lib/tools/file.d.ts +27 -0
  40. package/lib/tools/file.js +262 -0
  41. package/lib/tools/notebook.d.ts +41 -0
  42. package/lib/tools/notebook.js +779 -0
  43. package/lib/tools/tool-registry.d.ts +35 -0
  44. package/lib/tools/tool-registry.js +55 -0
  45. package/lib/widgets/ai-settings.d.ts +49 -0
  46. package/lib/widgets/ai-settings.js +580 -0
  47. package/lib/widgets/chat-wrapper.d.ts +144 -0
  48. package/lib/widgets/chat-wrapper.js +390 -0
  49. package/lib/widgets/provider-config-dialog.d.ts +14 -0
  50. package/lib/widgets/provider-config-dialog.js +112 -0
  51. package/package.json +151 -40
  52. package/schema/settings-model.json +159 -0
  53. package/src/agent.ts +836 -0
  54. package/src/chat-model.ts +771 -0
  55. package/src/completion/completion-provider.ts +346 -0
  56. package/src/completion/index.ts +1 -0
  57. package/src/components/clear-button.tsx +56 -0
  58. package/src/components/index.ts +3 -0
  59. package/src/components/model-select.tsx +245 -0
  60. package/src/components/stop-button.tsx +11 -11
  61. package/src/components/token-usage-display.tsx +130 -0
  62. package/src/components/tool-select.tsx +218 -0
  63. package/src/icons.ts +12 -14
  64. package/src/index.ts +485 -232
  65. package/src/mcp/browser.ts +213 -0
  66. package/src/models/settings-model.ts +413 -0
  67. package/src/providers/built-in-providers.ts +294 -0
  68. package/src/providers/models.ts +79 -0
  69. package/src/providers/provider-registry.ts +189 -0
  70. package/src/tokens.ts +217 -90
  71. package/src/tools/commands.ts +151 -0
  72. package/src/tools/file.ts +307 -0
  73. package/src/tools/notebook.ts +987 -0
  74. package/src/tools/tool-registry.ts +63 -0
  75. package/src/types.d.ts +4 -0
  76. package/src/widgets/ai-settings.tsx +1233 -0
  77. package/src/widgets/chat-wrapper.tsx +543 -0
  78. package/src/widgets/provider-config-dialog.tsx +272 -0
  79. package/style/base.css +335 -14
  80. package/style/icons/jupyternaut-lite.svg +1 -1
  81. package/lib/base-completer.d.ts +0 -49
  82. package/lib/base-completer.js +0 -14
  83. package/lib/chat-handler.d.ts +0 -56
  84. package/lib/chat-handler.js +0 -201
  85. package/lib/completion-provider.d.ts +0 -34
  86. package/lib/completion-provider.js +0 -32
  87. package/lib/default-prompts.d.ts +0 -2
  88. package/lib/default-prompts.js +0 -31
  89. package/lib/default-providers/Anthropic/completer.d.ts +0 -12
  90. package/lib/default-providers/Anthropic/completer.js +0 -46
  91. package/lib/default-providers/Anthropic/settings-schema.json +0 -70
  92. package/lib/default-providers/ChromeAI/completer.d.ts +0 -12
  93. package/lib/default-providers/ChromeAI/completer.js +0 -56
  94. package/lib/default-providers/ChromeAI/instructions.d.ts +0 -6
  95. package/lib/default-providers/ChromeAI/instructions.js +0 -42
  96. package/lib/default-providers/ChromeAI/settings-schema.json +0 -18
  97. package/lib/default-providers/Gemini/completer.d.ts +0 -12
  98. package/lib/default-providers/Gemini/completer.js +0 -48
  99. package/lib/default-providers/Gemini/instructions.d.ts +0 -2
  100. package/lib/default-providers/Gemini/instructions.js +0 -9
  101. package/lib/default-providers/Gemini/settings-schema.json +0 -64
  102. package/lib/default-providers/MistralAI/completer.d.ts +0 -13
  103. package/lib/default-providers/MistralAI/completer.js +0 -52
  104. package/lib/default-providers/MistralAI/instructions.d.ts +0 -2
  105. package/lib/default-providers/MistralAI/instructions.js +0 -18
  106. package/lib/default-providers/MistralAI/settings-schema.json +0 -75
  107. package/lib/default-providers/Ollama/completer.d.ts +0 -12
  108. package/lib/default-providers/Ollama/completer.js +0 -43
  109. package/lib/default-providers/Ollama/instructions.d.ts +0 -2
  110. package/lib/default-providers/Ollama/instructions.js +0 -70
  111. package/lib/default-providers/Ollama/settings-schema.json +0 -143
  112. package/lib/default-providers/OpenAI/completer.d.ts +0 -12
  113. package/lib/default-providers/OpenAI/completer.js +0 -43
  114. package/lib/default-providers/OpenAI/settings-schema.json +0 -628
  115. package/lib/default-providers/WebLLM/completer.d.ts +0 -21
  116. package/lib/default-providers/WebLLM/completer.js +0 -127
  117. package/lib/default-providers/WebLLM/instructions.d.ts +0 -6
  118. package/lib/default-providers/WebLLM/instructions.js +0 -32
  119. package/lib/default-providers/WebLLM/settings-schema.json +0 -19
  120. package/lib/default-providers/index.d.ts +0 -2
  121. package/lib/default-providers/index.js +0 -179
  122. package/lib/provider.d.ts +0 -144
  123. package/lib/provider.js +0 -412
  124. package/lib/settings/base.json +0 -7
  125. package/lib/settings/index.d.ts +0 -3
  126. package/lib/settings/index.js +0 -3
  127. package/lib/settings/panel.d.ts +0 -226
  128. package/lib/settings/panel.js +0 -510
  129. package/lib/settings/textarea.d.ts +0 -2
  130. package/lib/settings/textarea.js +0 -18
  131. package/lib/settings/utils.d.ts +0 -2
  132. package/lib/settings/utils.js +0 -4
  133. package/lib/types/ai-model.d.ts +0 -24
  134. package/lib/types/ai-model.js +0 -5
  135. package/schema/chat.json +0 -28
  136. package/schema/provider-registry.json +0 -29
  137. package/schema/system-prompts.json +0 -22
  138. package/src/base-completer.ts +0 -75
  139. package/src/chat-handler.ts +0 -262
  140. package/src/completion-provider.ts +0 -64
  141. package/src/default-prompts.ts +0 -33
  142. package/src/default-providers/Anthropic/completer.ts +0 -59
  143. package/src/default-providers/ChromeAI/completer.ts +0 -73
  144. package/src/default-providers/ChromeAI/instructions.ts +0 -45
  145. package/src/default-providers/Gemini/completer.ts +0 -61
  146. package/src/default-providers/Gemini/instructions.ts +0 -9
  147. package/src/default-providers/MistralAI/completer.ts +0 -69
  148. package/src/default-providers/MistralAI/instructions.ts +0 -18
  149. package/src/default-providers/Ollama/completer.ts +0 -54
  150. package/src/default-providers/Ollama/instructions.ts +0 -70
  151. package/src/default-providers/OpenAI/completer.ts +0 -54
  152. package/src/default-providers/WebLLM/completer.ts +0 -151
  153. package/src/default-providers/WebLLM/instructions.ts +0 -33
  154. package/src/default-providers/index.ts +0 -211
  155. package/src/global.d.ts +0 -9
  156. package/src/provider.ts +0 -514
  157. package/src/settings/index.ts +0 -3
  158. package/src/settings/panel.tsx +0 -773
  159. package/src/settings/textarea.tsx +0 -33
  160. package/src/settings/utils.ts +0 -5
  161. package/src/types/ai-model.ts +0 -37
  162. package/src/types/service-worker.d.ts +0 -6
@@ -0,0 +1,346 @@
1
+ import {
2
+ CompletionHandler,
3
+ IInlineCompletionContext,
4
+ IInlineCompletionList,
5
+ IInlineCompletionProvider
6
+ } from '@jupyterlab/completer';
7
+ import { NotebookPanel } from '@jupyterlab/notebook';
8
+ import { generateText, LanguageModel } from 'ai';
9
+ import { ISecretsManager } from 'jupyter-secrets-manager';
10
+
11
+ import { AISettingsModel } from '../models/settings-model';
12
+ import { createCompletionModel } from '../providers/models';
13
+ import { SECRETS_NAMESPACE, type ICompletionProviderRegistry } from '../tokens';
14
+
15
+ /**
16
+ * Configuration interface for provider-specific completion behavior
17
+ */
18
+ export interface IProviderCompletionConfig {
19
+ /**
20
+ * Temperature setting for the provider
21
+ */
22
+ temperature?: number;
23
+
24
+ /**
25
+ * Whether the provider supports fill-in-the-middle completion
26
+ */
27
+ supportsFillInMiddle?: boolean;
28
+
29
+ /**
30
+ * Whether to set filterText for this provider
31
+ */
32
+ useFilterText?: boolean;
33
+
34
+ /**
35
+ * Custom prompt formatter for provider-specific requirements
36
+ */
37
+ customPromptFormat?: (prompt: string, suffix: string) => string;
38
+
39
+ /**
40
+ * Function to clean up provider-specific artifacts from completion text
41
+ */
42
+ cleanupCompletion?: (completion: string) => string;
43
+ }
44
+
45
+ /**
46
+ * Default system prompt for code completion
47
+ */
48
+ const DEFAULT_COMPLETION_SYSTEM_PROMPT = `You are an AI code completion assistant. Complete the given code fragment with appropriate code.
49
+ Rules:
50
+ - Return only the completion text, no explanations or comments
51
+ - Do not include code block markers (\`\`\` or similar)
52
+ - Make completions contextually relevant to the surrounding code and notebook context
53
+ - Follow the language-specific conventions and style guidelines for the detected programming language
54
+ - Keep completions concise but functional
55
+ - Do not repeat the existing code that comes before the cursor
56
+ - Use variables, imports, functions, and other definitions from previous notebook cells when relevant`;
57
+
58
+ /**
59
+ * The generic completion provider to register to the completion provider manager.
60
+ */
61
+ export class AICompletionProvider implements IInlineCompletionProvider {
62
+ /**
63
+ * Construct a new completion provider.
64
+ */
65
+ constructor(options: AICompletionProvider.IOptions) {
66
+ Private.setToken(options.token);
67
+ this._settingsModel = options.settingsModel;
68
+ this._completionProviderRegistry = options.completionProviderRegistry;
69
+ this._secretsManager = options.secretsManager;
70
+ this._settingsModel.stateChanged.connect(() => {
71
+ this._updateModel();
72
+ });
73
+ this._updateModel();
74
+ }
75
+
76
+ /**
77
+ * The unique identifier of the provider.
78
+ */
79
+ readonly identifier = '@jupyterlite/ai:completer';
80
+
81
+ /**
82
+ * Get the current completer name based on settings.
83
+ */
84
+ get name(): string {
85
+ const activeProvider = this._settingsModel.getCompleterProvider();
86
+ return activeProvider ? `${activeProvider.provider}-completer` : 'none';
87
+ }
88
+
89
+ /**
90
+ * Get the system prompt for the completion.
91
+ */
92
+ get systemPrompt(): string {
93
+ return DEFAULT_COMPLETION_SYSTEM_PROMPT;
94
+ }
95
+
96
+ /**
97
+ * Fetch completion items based on the request and context.
98
+ */
99
+ async fetch(
100
+ request: CompletionHandler.IRequest,
101
+ context: IInlineCompletionContext
102
+ ): Promise<IInlineCompletionList> {
103
+ if (!this._model) {
104
+ return { items: [] };
105
+ }
106
+
107
+ const { text, offset: cursorOffset } = request;
108
+ const prompt = text.slice(0, cursorOffset);
109
+ const suffix = text.slice(cursorOffset);
110
+
111
+ // Get current provider settings
112
+ const activeProvider = this._settingsModel.getCompleterProvider();
113
+ if (!activeProvider) {
114
+ return { items: [] };
115
+ }
116
+
117
+ const provider = activeProvider.provider;
118
+ const providerConfig = this._getProviderCompletionConfig(provider);
119
+
120
+ try {
121
+ let completionPrompt: string;
122
+
123
+ // Check if we're in a notebook or file and handle context accordingly
124
+ if (context.widget instanceof NotebookPanel) {
125
+ // Extract notebook context with surrounding cells
126
+ const contextString = this._extractNotebookContext(context, request);
127
+ completionPrompt = contextString;
128
+ } else {
129
+ // For files, use simpler approach
130
+ completionPrompt = prompt.trim();
131
+ if (providerConfig.customPromptFormat && suffix.trim()) {
132
+ completionPrompt = providerConfig.customPromptFormat(prompt, suffix);
133
+ }
134
+ }
135
+
136
+ const { text: completion } = await generateText({
137
+ model: this._model,
138
+ prompt: completionPrompt,
139
+ system: this.systemPrompt,
140
+ temperature: providerConfig.temperature || 0.3
141
+ });
142
+
143
+ // Clean up provider-specific artifacts if cleanup function is provided
144
+ let cleanCompletion = completion;
145
+ if (providerConfig.cleanupCompletion) {
146
+ cleanCompletion = providerConfig.cleanupCompletion(completion);
147
+ }
148
+
149
+ const items = [
150
+ {
151
+ insertText: cleanCompletion,
152
+ filterText: providerConfig.useFilterText
153
+ ? prompt.substring(completionPrompt.length)
154
+ : undefined
155
+ }
156
+ ];
157
+
158
+ return { items };
159
+ } catch (error) {
160
+ console.error(`Error fetching completions from ${provider}:`, error);
161
+ return { items: [] };
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Update the language model based on current settings.
167
+ */
168
+ private async _updateModel(): Promise<void> {
169
+ const activeProvider = this._settingsModel.getCompleterProvider();
170
+ if (!activeProvider) {
171
+ this._model = null;
172
+ return;
173
+ }
174
+
175
+ const provider = activeProvider.provider;
176
+ const model = activeProvider.model;
177
+
178
+ let apiKey: string;
179
+ if (this._secretsManager && this._settingsModel.config.useSecretsManager) {
180
+ apiKey =
181
+ (
182
+ await this._secretsManager.get(
183
+ Private.getToken(),
184
+ SECRETS_NAMESPACE,
185
+ `${provider}:apiKey`
186
+ )
187
+ )?.value ?? '';
188
+ } else {
189
+ apiKey = this._settingsModel.getApiKey(activeProvider.id);
190
+ }
191
+
192
+ try {
193
+ this._model = createCompletionModel(
194
+ {
195
+ provider,
196
+ model,
197
+ apiKey
198
+ },
199
+ this._completionProviderRegistry
200
+ );
201
+ } catch (error) {
202
+ console.error(`Error creating model for ${provider}:`, error);
203
+ this._model = null;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Extract context from notebook cells
209
+ */
210
+ private _extractNotebookContext(
211
+ context: IInlineCompletionContext,
212
+ request: CompletionHandler.IRequest
213
+ ): string {
214
+ const { text, offset: cursorOffset } = request;
215
+ let codeBeforeCursor = text.slice(0, cursorOffset);
216
+ let codeAfterCursor = text.slice(cursorOffset);
217
+
218
+ const notebookPanel = context.widget as NotebookPanel;
219
+ const notebook = notebookPanel.content;
220
+ const currentCellIndex = notebook.activeCellIndex;
221
+ const cells = notebook.widgets;
222
+
223
+ // For notebooks, include context from surrounding cells
224
+ const cellsAbove: string[] = [];
225
+ const cellsBelow: string[] = [];
226
+
227
+ // Get content from cells above current cell
228
+ for (let i = 0; i < currentCellIndex; i++) {
229
+ const cell = cells[i];
230
+ if (cell.model.type === 'code') {
231
+ const source = cell.model.sharedModel.source;
232
+ if (source.trim()) {
233
+ cellsAbove.push(source.trim());
234
+ }
235
+ }
236
+ }
237
+
238
+ // Get content from cells below current cell
239
+ for (let i = currentCellIndex + 1; i < cells.length; i++) {
240
+ const cell = cells[i];
241
+ if (cell.model.type === 'code') {
242
+ const source = cell.model.sharedModel.source;
243
+ if (source.trim()) {
244
+ cellsBelow.push(source.trim());
245
+ }
246
+ }
247
+ }
248
+
249
+ // Include cells above in the code before cursor
250
+ if (cellsAbove.length > 0) {
251
+ const cellsAboveText = cellsAbove
252
+ .map((cell, index) => `# Cell ${index + 1}:\n${cell}`)
253
+ .join('\n\n');
254
+ codeBeforeCursor = `${cellsAboveText}\n\n# Current cell:\n${codeBeforeCursor}`;
255
+ }
256
+
257
+ // Include cells below in the code after cursor
258
+ if (cellsBelow.length > 0) {
259
+ const cellsBelowText = cellsBelow
260
+ .map((cell, index) => `# Cell ${index + 1}:\n${cell}`)
261
+ .join('\n\n');
262
+ codeAfterCursor = `${codeAfterCursor}\n\n# Cells below:\n${cellsBelowText}`;
263
+ }
264
+
265
+ const parts: string[] = [];
266
+
267
+ // Add code before cursor
268
+ if (codeBeforeCursor) {
269
+ parts.push('# Code before cursor:');
270
+ parts.push(codeBeforeCursor);
271
+ }
272
+
273
+ // Add completion instruction
274
+ parts.push('# Complete the code at cursor position');
275
+
276
+ // Add code after cursor
277
+ if (codeAfterCursor) {
278
+ parts.push('# Code after cursor:');
279
+ parts.push(codeAfterCursor);
280
+ }
281
+
282
+ return parts.length > 1 ? parts.join('\n\n') + '\n\n' : '';
283
+ }
284
+
285
+ /**
286
+ * Get provider-specific completion configuration from registry
287
+ */
288
+ private _getProviderCompletionConfig(
289
+ provider: string
290
+ ): IProviderCompletionConfig {
291
+ const providerInfo =
292
+ this._completionProviderRegistry?.getProviderInfo(provider);
293
+ const completionConfig = providerInfo?.customSettings?.completionConfig;
294
+
295
+ // Return provider config or default config
296
+ return (
297
+ completionConfig || {
298
+ temperature: 0.3,
299
+ supportsFillInMiddle: false,
300
+ useFilterText: false
301
+ }
302
+ );
303
+ }
304
+
305
+ private _settingsModel: AISettingsModel;
306
+ private _completionProviderRegistry?: ICompletionProviderRegistry;
307
+ private _model: LanguageModel | null = null;
308
+ private _secretsManager?: ISecretsManager;
309
+ }
310
+
311
+ export namespace AICompletionProvider {
312
+ /**
313
+ * The options for the constructor of the completion provider.
314
+ */
315
+ export interface IOptions {
316
+ /**
317
+ * The AI settings model.
318
+ */
319
+ settingsModel: AISettingsModel;
320
+ /**
321
+ * The completion provider registry.
322
+ */
323
+ completionProviderRegistry?: ICompletionProviderRegistry;
324
+ /**
325
+ * The secrets manager.
326
+ */
327
+ secretsManager?: ISecretsManager;
328
+ /**
329
+ * The token used to request the secrets manager.
330
+ */
331
+ token: symbol;
332
+ }
333
+ }
334
+
335
+ namespace Private {
336
+ /**
337
+ * The token to use with the secrets manager, setter and getter.
338
+ */
339
+ let secretsToken: symbol;
340
+ export function setToken(value: symbol): void {
341
+ secretsToken = value;
342
+ }
343
+ export function getToken(): symbol {
344
+ return secretsToken;
345
+ }
346
+ }
@@ -0,0 +1 @@
1
+ export * from './completion-provider';
@@ -0,0 +1,56 @@
1
+ import { InputToolbarRegistry, TooltippedButton } from '@jupyter/chat';
2
+
3
+ import ClearIcon from '@mui/icons-material/Clear';
4
+
5
+ import React from 'react';
6
+
7
+ import { AIChatModel } from '../chat-model';
8
+
9
+ /**
10
+ * Properties of the clear button.
11
+ */
12
+ export interface IClearButtonProps
13
+ extends InputToolbarRegistry.IToolbarItemProps {
14
+ /**
15
+ * The function to clear messages.
16
+ */
17
+ clearMessages: () => void;
18
+ }
19
+
20
+ /**
21
+ * The clear button component.
22
+ */
23
+ export function ClearButton(props: IClearButtonProps): JSX.Element {
24
+ const tooltip = 'Clear chat';
25
+ return (
26
+ <TooltippedButton
27
+ onClick={props.clearMessages}
28
+ tooltip={tooltip}
29
+ buttonProps={{
30
+ size: 'small',
31
+ variant: 'outlined',
32
+ color: 'secondary',
33
+ title: tooltip
34
+ }}
35
+ >
36
+ <ClearIcon />
37
+ </TooltippedButton>
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Factory returning the clear button toolbar item.
43
+ */
44
+ export function clearItem(): InputToolbarRegistry.IToolbarItem {
45
+ return {
46
+ element: (props: InputToolbarRegistry.IToolbarItemProps) => {
47
+ const { model } = props;
48
+ const clearMessages = () =>
49
+ (model.chatContext as AIChatModel.IAIChatContext).clearMessages();
50
+ const clearProps: IClearButtonProps = { ...props, clearMessages };
51
+ return ClearButton(clearProps);
52
+ },
53
+ position: 0,
54
+ hidden: false
55
+ };
56
+ }
@@ -0,0 +1,3 @@
1
+ export * from './clear-button';
2
+ export * from './stop-button';
3
+ export * from './tool-select';
@@ -0,0 +1,245 @@
1
+ import { InputToolbarRegistry, TooltippedButton } from '@jupyter/chat';
2
+ import CheckIcon from '@mui/icons-material/Check';
3
+ import { Menu, MenuItem, Typography } from '@mui/material';
4
+ import React, { useCallback, useEffect, useState } from 'react';
5
+ import { AISettingsModel } from '../models/settings-model';
6
+
7
+ const SELECT_ITEM_CLASS = 'labai-model-select-item';
8
+
9
+ /**
10
+ * Properties for the model select component.
11
+ */
12
+ export interface IModelSelectProps
13
+ extends InputToolbarRegistry.IToolbarItemProps {
14
+ /**
15
+ * The settings model to get available models and current selection from.
16
+ */
17
+ settingsModel: AISettingsModel;
18
+ }
19
+
20
+ /**
21
+ * The model select component for choosing AI models.
22
+ */
23
+ export function ModelSelect(props: IModelSelectProps): JSX.Element {
24
+ const { settingsModel } = props;
25
+
26
+ const [currentProvider, setCurrentProvider] = useState<string>('');
27
+ const [currentModel, setCurrentModel] = useState<string>('');
28
+ const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
29
+ const [menuOpen, setMenuOpen] = useState(false);
30
+
31
+ // Get configured providers from settings model
32
+ const configuredProviders = settingsModel.providers;
33
+
34
+ const openMenu = useCallback((el: HTMLElement | null) => {
35
+ setMenuAnchorEl(el);
36
+ setMenuOpen(true);
37
+ }, []);
38
+
39
+ const closeMenu = useCallback(() => {
40
+ setMenuOpen(false);
41
+ }, []);
42
+
43
+ const selectModel = useCallback(
44
+ async (providerId: string) => {
45
+ // Set the active provider using the provider ID
46
+ await settingsModel.setActiveProvider(providerId);
47
+ closeMenu();
48
+
49
+ // Provider selected successfully
50
+ },
51
+ [settingsModel, closeMenu]
52
+ );
53
+
54
+ // Update current selection when settings model changes
55
+ useEffect(() => {
56
+ const updateCurrentSelection = () => {
57
+ const activeProvider = settingsModel.getActiveProvider();
58
+ if (activeProvider) {
59
+ setCurrentProvider(activeProvider.id);
60
+ setCurrentModel(activeProvider.model);
61
+ }
62
+ };
63
+
64
+ updateCurrentSelection();
65
+ settingsModel.stateChanged.connect(updateCurrentSelection);
66
+ return () => {
67
+ settingsModel.stateChanged.disconnect(updateCurrentSelection);
68
+ };
69
+ }, [settingsModel]);
70
+
71
+ // Get current provider label for display
72
+ const activeProvider = settingsModel.getActiveProvider();
73
+ const currentProviderLabel = activeProvider?.name || currentProvider;
74
+
75
+ // Use all configured providers (they're already validated when added)
76
+ const availableProviders = configuredProviders;
77
+
78
+ // Get available model combinations from configured providers
79
+ const availableModels = availableProviders.map(provider => ({
80
+ provider: provider.id,
81
+ providerLabel: provider.name,
82
+ model: provider.model,
83
+ isSelected:
84
+ provider.id === currentProvider && provider.model === currentModel
85
+ }));
86
+
87
+ // Show a message if no providers are configured
88
+ if (availableModels.length === 0) {
89
+ return (
90
+ <TooltippedButton
91
+ onClick={() => {}}
92
+ tooltip="No providers configured. Please go to AI Settings to add a provider."
93
+ buttonProps={{
94
+ size: 'small',
95
+ variant: 'outlined',
96
+ color: 'warning',
97
+ disabled: true,
98
+ title: 'No Providers Available'
99
+ }}
100
+ sx={{
101
+ minWidth: 'auto',
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ height: '29px'
105
+ }}
106
+ >
107
+ <Typography
108
+ variant="caption"
109
+ sx={{ fontSize: '0.7rem', fontWeight: 500 }}
110
+ >
111
+ No Providers
112
+ </Typography>
113
+ </TooltippedButton>
114
+ );
115
+ }
116
+
117
+ return (
118
+ <>
119
+ <TooltippedButton
120
+ onClick={e => {
121
+ openMenu(e.currentTarget);
122
+ }}
123
+ tooltip={`Current Model: ${currentProviderLabel} - ${currentModel}`}
124
+ buttonProps={{
125
+ size: 'small',
126
+ variant: 'contained',
127
+ color: 'primary',
128
+ title: 'Select AI Model',
129
+ onKeyDown: e => {
130
+ if (e.key !== 'Enter' && e.key !== ' ') {
131
+ return;
132
+ }
133
+ openMenu(e.currentTarget);
134
+ // Stop propagation to prevent sending message
135
+ e.stopPropagation();
136
+ }
137
+ }}
138
+ sx={{
139
+ minWidth: 'auto',
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ height: '29px'
143
+ }}
144
+ >
145
+ <Typography
146
+ variant="caption"
147
+ sx={{ fontSize: '0.7rem', fontWeight: 500, textTransform: 'none' }}
148
+ >
149
+ {currentProviderLabel}
150
+ </Typography>
151
+ </TooltippedButton>
152
+
153
+ <Menu
154
+ open={menuOpen}
155
+ onClose={closeMenu}
156
+ anchorEl={menuAnchorEl}
157
+ anchorOrigin={{
158
+ vertical: 'top',
159
+ horizontal: 'right'
160
+ }}
161
+ transformOrigin={{
162
+ vertical: 'bottom',
163
+ horizontal: 'right'
164
+ }}
165
+ sx={{
166
+ '& .MuiPaper-root': {
167
+ maxHeight: '300px',
168
+ overflowY: 'auto'
169
+ },
170
+ '& .MuiMenuItem-root': {
171
+ padding: '0.5em',
172
+ paddingRight: '2em',
173
+ minWidth: '200px'
174
+ }
175
+ }}
176
+ >
177
+ {availableModels.map(({ provider, providerLabel, isSelected }) => (
178
+ <MenuItem
179
+ key={provider}
180
+ className={SELECT_ITEM_CLASS}
181
+ onClick={async e => {
182
+ await selectModel(provider);
183
+ // Prevent sending message on model selection
184
+ e.stopPropagation();
185
+ }}
186
+ sx={{
187
+ backgroundColor: isSelected
188
+ ? 'var(--jp-brand-color3, rgba(33, 150, 243, 0.1))'
189
+ : 'transparent',
190
+ '&:hover': {
191
+ backgroundColor: isSelected
192
+ ? 'var(--jp-brand-color3, rgba(33, 150, 243, 0.15))'
193
+ : 'var(--jp-layout-color1)'
194
+ },
195
+ display: 'flex',
196
+ alignItems: 'center',
197
+ gap: '8px'
198
+ }}
199
+ >
200
+ {isSelected ? (
201
+ <CheckIcon
202
+ sx={{
203
+ color: 'var(--jp-brand-color1, #2196F3)',
204
+ fontSize: 16
205
+ }}
206
+ />
207
+ ) : (
208
+ <div style={{ width: '16px' }} />
209
+ )}
210
+ <Typography
211
+ variant="body2"
212
+ component="div"
213
+ sx={{
214
+ fontWeight: isSelected ? 600 : 400,
215
+ color: isSelected
216
+ ? 'var(--jp-brand-color1, #2196F3)'
217
+ : 'inherit'
218
+ }}
219
+ >
220
+ {providerLabel}
221
+ </Typography>
222
+ </MenuItem>
223
+ ))}
224
+ </Menu>
225
+ </>
226
+ );
227
+ }
228
+
229
+ /**
230
+ * Factory function returning the toolbar item for model selection.
231
+ */
232
+ export function createModelSelectItem(
233
+ settingsModel: AISettingsModel
234
+ ): InputToolbarRegistry.IToolbarItem {
235
+ return {
236
+ element: (props: InputToolbarRegistry.IToolbarItemProps) => {
237
+ const modelSelectProps: IModelSelectProps = {
238
+ ...props,
239
+ settingsModel
240
+ };
241
+ return <ModelSelect {...modelSelectProps} />;
242
+ },
243
+ position: 0.5
244
+ };
245
+ }
@@ -1,12 +1,10 @@
1
- /*
2
- * Copyright (c) Jupyter Development Team.
3
- * Distributed under the terms of the Modified BSD License.
4
- */
1
+ import { InputToolbarRegistry, TooltippedButton } from '@jupyter/chat';
5
2
 
6
3
  import StopIcon from '@mui/icons-material/Stop';
4
+
7
5
  import React from 'react';
8
6
 
9
- import { InputToolbarRegistry, TooltippedButton } from '@jupyter/chat';
7
+ import { AIChatModel } from '../chat-model';
10
8
 
11
9
  /**
12
10
  * Properties of the stop button.
@@ -20,7 +18,7 @@ export interface IStopButtonProps
20
18
  }
21
19
 
22
20
  /**
23
- * The stop button.
21
+ * The stop button component.
24
22
  */
25
23
  export function StopButton(props: IStopButtonProps): JSX.Element {
26
24
  const tooltip = 'Stop streaming';
@@ -31,6 +29,7 @@ export function StopButton(props: IStopButtonProps): JSX.Element {
31
29
  buttonProps={{
32
30
  size: 'small',
33
31
  variant: 'contained',
32
+ color: 'error',
34
33
  title: tooltip
35
34
  }}
36
35
  >
@@ -40,17 +39,18 @@ export function StopButton(props: IStopButtonProps): JSX.Element {
40
39
  }
41
40
 
42
41
  /**
43
- * factory returning the toolbar item.
42
+ * Factory returning the stop button toolbar item.
44
43
  */
45
- export function stopItem(
46
- stopStreaming: () => void
47
- ): InputToolbarRegistry.IToolbarItem {
44
+ export function stopItem(): InputToolbarRegistry.IToolbarItem {
48
45
  return {
49
46
  element: (props: InputToolbarRegistry.IToolbarItemProps) => {
47
+ const { model } = props;
48
+ const stopStreaming = () =>
49
+ (model.chatContext as AIChatModel.IAIChatContext).stopStreaming();
50
50
  const stopProps: IStopButtonProps = { ...props, stopStreaming };
51
51
  return StopButton(stopProps);
52
52
  },
53
53
  position: 50,
54
- hidden: true /* hidden by default */
54
+ hidden: true // Hidden by default, shown when streaming
55
55
  };
56
56
  }