@jupyterlite/ai 0.6.2 → 0.8.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.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/lib/base-completer.d.ts +22 -5
  3. package/lib/base-completer.js +14 -1
  4. package/lib/chat-handler.d.ts +23 -11
  5. package/lib/chat-handler.js +66 -45
  6. package/lib/completion-provider.d.ts +2 -2
  7. package/lib/completion-provider.js +5 -4
  8. package/lib/components/stop-button.d.ts +0 -1
  9. package/lib/default-prompts.d.ts +2 -0
  10. package/lib/default-prompts.js +31 -0
  11. package/lib/default-providers/Anthropic/completer.d.ts +4 -11
  12. package/lib/default-providers/Anthropic/completer.js +5 -16
  13. package/lib/default-providers/ChromeAI/completer.d.ts +4 -11
  14. package/lib/default-providers/ChromeAI/completer.js +5 -16
  15. package/lib/default-providers/ChromeAI/instructions.d.ts +4 -0
  16. package/lib/default-providers/ChromeAI/instructions.js +18 -0
  17. package/lib/default-providers/ChromeAI/settings-schema.json +0 -3
  18. package/lib/default-providers/Gemini/completer.d.ts +12 -0
  19. package/lib/default-providers/Gemini/completer.js +48 -0
  20. package/lib/default-providers/Gemini/instructions.d.ts +2 -0
  21. package/lib/default-providers/Gemini/instructions.js +9 -0
  22. package/lib/default-providers/Gemini/settings-schema.json +64 -0
  23. package/lib/default-providers/MistralAI/completer.d.ts +10 -13
  24. package/lib/default-providers/MistralAI/completer.js +42 -52
  25. package/lib/default-providers/MistralAI/instructions.d.ts +1 -1
  26. package/lib/default-providers/MistralAI/instructions.js +2 -0
  27. package/lib/default-providers/Ollama/completer.d.ts +12 -0
  28. package/lib/default-providers/Ollama/completer.js +43 -0
  29. package/lib/default-providers/Ollama/instructions.d.ts +2 -0
  30. package/lib/default-providers/Ollama/instructions.js +70 -0
  31. package/lib/default-providers/Ollama/settings-schema.json +143 -0
  32. package/lib/default-providers/OpenAI/completer.d.ts +4 -11
  33. package/lib/default-providers/OpenAI/completer.js +8 -16
  34. package/lib/default-providers/OpenAI/settings-schema.json +88 -128
  35. package/lib/default-providers/WebLLM/completer.d.ts +21 -0
  36. package/lib/default-providers/WebLLM/completer.js +127 -0
  37. package/lib/default-providers/WebLLM/instructions.d.ts +6 -0
  38. package/lib/default-providers/WebLLM/instructions.js +32 -0
  39. package/lib/default-providers/WebLLM/settings-schema.json +19 -0
  40. package/lib/default-providers/index.js +127 -8
  41. package/lib/index.d.ts +3 -2
  42. package/lib/index.js +80 -36
  43. package/lib/provider.d.ts +48 -22
  44. package/lib/provider.js +254 -101
  45. package/lib/settings/index.d.ts +1 -1
  46. package/lib/settings/index.js +1 -1
  47. package/lib/settings/panel.d.ts +151 -14
  48. package/lib/settings/panel.js +334 -145
  49. package/lib/settings/textarea.d.ts +2 -0
  50. package/lib/settings/textarea.js +18 -0
  51. package/lib/tokens.d.ts +45 -22
  52. package/lib/tokens.js +2 -1
  53. package/lib/types/ai-model.d.ts +24 -0
  54. package/lib/types/ai-model.js +5 -0
  55. package/package.json +19 -15
  56. package/schema/chat.json +1 -1
  57. package/schema/provider-registry.json +8 -8
  58. package/schema/system-prompts.json +22 -0
  59. package/src/base-completer.ts +38 -6
  60. package/src/chat-handler.ts +62 -31
  61. package/src/completion-provider.ts +3 -3
  62. package/src/default-prompts.ts +33 -0
  63. package/src/default-providers/Anthropic/completer.ts +5 -21
  64. package/src/default-providers/ChromeAI/completer.ts +5 -21
  65. package/src/default-providers/ChromeAI/instructions.ts +21 -0
  66. package/src/default-providers/Gemini/completer.ts +61 -0
  67. package/src/default-providers/Gemini/instructions.ts +9 -0
  68. package/src/default-providers/MistralAI/completer.ts +47 -65
  69. package/src/default-providers/MistralAI/instructions.ts +2 -0
  70. package/src/default-providers/Ollama/completer.ts +54 -0
  71. package/src/default-providers/Ollama/instructions.ts +70 -0
  72. package/src/default-providers/OpenAI/completer.ts +8 -21
  73. package/src/default-providers/WebLLM/completer.ts +151 -0
  74. package/src/default-providers/WebLLM/instructions.ts +33 -0
  75. package/src/default-providers/index.ts +158 -18
  76. package/src/index.ts +108 -40
  77. package/src/provider.ts +300 -109
  78. package/src/settings/index.ts +1 -1
  79. package/src/settings/panel.tsx +463 -101
  80. package/src/settings/textarea.tsx +33 -0
  81. package/src/tokens.ts +49 -24
  82. package/src/types/ai-model.ts +37 -0
  83. package/src/types/service-worker.d.ts +6 -0
  84. package/style/base.css +34 -0
  85. package/lib/settings/settings-connector.d.ts +0 -31
  86. package/lib/settings/settings-connector.js +0 -61
  87. package/src/settings/settings-connector.ts +0 -88
@@ -1,10 +1,11 @@
1
1
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
2
3
  import {
3
- ISettingConnector,
4
- ISettingRegistry
5
- } from '@jupyterlab/settingregistry';
6
- import { FormComponent, IFormRenderer } from '@jupyterlab/ui-components';
7
- import { JSONExt } from '@lumino/coreutils';
4
+ Button,
5
+ FormComponent,
6
+ IFormRenderer
7
+ } from '@jupyterlab/ui-components';
8
+ import { JSONExt, ReadonlyPartialJSONObject } from '@lumino/coreutils';
8
9
  import { IChangeEvent } from '@rjsf/core';
9
10
  import type { FieldProps } from '@rjsf/utils';
10
11
  import validator from '@rjsf/validator-ajv8';
@@ -12,21 +13,24 @@ import { JSONSchema7 } from 'json-schema';
12
13
  import { ISecretsManager } from 'jupyter-secrets-manager';
13
14
  import React from 'react';
14
15
 
15
- import { getSecretId, SettingConnector } from '.';
16
+ import { getSecretId, SECRETS_REPLACEMENT } from '.';
16
17
  import baseSettings from './base.json';
17
- import { IAIProviderRegistry, IDict, PLUGIN_IDS } from '../tokens';
18
+ import { IAIProviderRegistry, IDict, ModelRole, PLUGIN_IDS } from '../tokens';
18
19
 
19
20
  const MD_MIME_TYPE = 'text/markdown';
20
- const STORAGE_NAME = '@jupyterlite/ai:settings';
21
21
  const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
22
+ const ERROR_CLASS = 'jp-AISettingsError';
22
23
  const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry;
24
+ const STORAGE_KEYS = {
25
+ chat: '@jupyterlite/ai:chat-settings',
26
+ completer: '@jupyterlite/ai:completer-settings'
27
+ };
23
28
 
24
29
  export const aiSettingsRenderer = (options: {
25
30
  providerRegistry: IAIProviderRegistry;
26
31
  secretsToken?: symbol;
27
32
  rmRegistry?: IRenderMimeRegistry;
28
33
  secretsManager?: ISecretsManager;
29
- settingConnector?: ISettingConnector;
30
34
  }): IFormRenderer => {
31
35
  const { secretsToken } = options;
32
36
  delete options.secretsToken;
@@ -41,36 +45,205 @@ export const aiSettingsRenderer = (options: {
41
45
  };
42
46
  };
43
47
 
44
- export interface ISettingsFormStates {
45
- schema: JSONSchema7;
46
- instruction: HTMLElement | null;
47
- }
48
-
49
48
  const WrappedFormComponent = (props: any): JSX.Element => {
50
49
  return <FormComponent {...props} validator={validator} />;
51
50
  };
52
51
 
53
- export class AiSettings extends React.Component<
54
- FieldProps,
55
- ISettingsFormStates
56
- > {
52
+ export interface IAiSettings {
53
+ /**
54
+ * Get the local storage settings for a specific role (chat or completer).
55
+ */
56
+ getLocalStorage(role: ModelRole): IDict<any>;
57
+ /**
58
+ * Set the local storage item for a specific role (chat or completer).
59
+ * If the key is not provider (null) we assume the value should replace the whole
60
+ * local storage for this role.
61
+ */
62
+ setLocalStorageItem(role: ModelRole, key: string | null, value: any): void;
63
+ /**
64
+ * Get the settings from the registry (jupyterlab settings system) for a given role.
65
+ */
66
+ getSettingsFromRegistry(role: ModelRole): IDict<any>;
67
+ /**
68
+ * Save the settings to the setting registry.
69
+ */
70
+ saveSettingsToRegistry(role: ModelRole, settings: IDict<any>): void;
71
+ }
72
+
73
+ export class AiSettings
74
+ extends React.Component<FieldProps, AiSettings.states>
75
+ implements IAiSettings
76
+ {
57
77
  constructor(props: FieldProps) {
78
+ super(props);
79
+ this._settings = props.formContext.settings;
80
+ const uniqueProvider =
81
+ (this._settings.get('UniqueProvider').composite as boolean) ?? true;
82
+
83
+ this.state = { uniqueProvider };
84
+
85
+ this._settings.changed.connect(this._settingsChanged);
86
+ }
87
+
88
+ private _settingsChanged = () => {
89
+ const uniqueProvider =
90
+ (this._settings.get('UniqueProvider').composite as boolean) ?? true;
91
+ if (this.state.uniqueProvider === uniqueProvider) {
92
+ return;
93
+ }
94
+ if (uniqueProvider) {
95
+ // Copy chat settings to the completer settings if there should be a unique
96
+ // provider for both.
97
+ this.setLocalStorageItem('completer', null, this.getLocalStorage('chat'));
98
+ this.saveSettingsToRegistry(
99
+ 'completer',
100
+ this.getSettingsFromRegistry('chat')
101
+ );
102
+ }
103
+ this.setState({ uniqueProvider });
104
+ };
105
+
106
+ /**
107
+ * Get the local storage settings for a specific role (chat or completer).
108
+ */
109
+ getLocalStorage = (role: ModelRole): IDict<any> => {
110
+ const storageKey = STORAGE_KEYS[role];
111
+ return JSON.parse(localStorage.getItem(storageKey) ?? '{}');
112
+ };
113
+
114
+ /**
115
+ * Set the local storage item for a specific role (chat or completer).
116
+ * If the key is not provider (null) we assume the value should replace the whole
117
+ * local storage for this role.
118
+ */
119
+ setLocalStorageItem = (
120
+ role: ModelRole,
121
+ key: string | null,
122
+ value: any
123
+ ): void => {
124
+ const storageKey = STORAGE_KEYS[role];
125
+ let settings: IDict<any>;
126
+
127
+ if (key !== null) {
128
+ settings = JSON.parse(localStorage.getItem(storageKey) ?? '{}');
129
+ settings[key] = value;
130
+ } else {
131
+ settings = value;
132
+ }
133
+
134
+ localStorage.setItem(storageKey, JSON.stringify(settings));
135
+
136
+ // If both chat and completer use the same settings, only the chat settings should
137
+ // be editable for user, so we should duplicate its values to the completer
138
+ // local storage.
139
+ if (this.state.uniqueProvider && role === 'chat') {
140
+ const storageKeyCompleter = STORAGE_KEYS['completer'];
141
+ localStorage.setItem(storageKeyCompleter, JSON.stringify(settings));
142
+ }
143
+ };
144
+
145
+ /**
146
+ * Get the settings from the registry (jupyterlab settings system) for a given role.
147
+ */
148
+ getSettingsFromRegistry = (role: ModelRole): IDict<any> => {
149
+ const settings = this._settings.get('AIproviders')
150
+ .composite as ReadonlyPartialJSONObject;
151
+ return settings && Object.keys(settings).includes(role)
152
+ ? (settings[role] as IDict<any>)
153
+ : { provider: 'None' };
154
+ };
155
+
156
+ /**
157
+ * Save the settings to the setting registry.
158
+ */
159
+ saveSettingsToRegistry = (role: ModelRole, settings: IDict<any>): void => {
160
+ const fullSettings = this._settings.get('AIproviders')
161
+ .composite as IDict<any>;
162
+ fullSettings[role] = { ...settings };
163
+
164
+ // If both chat and completer use the same settings, only the chat settings should
165
+ // be editable for user, so we should duplicate its values to the completer
166
+ // settings.
167
+ if (this.state.uniqueProvider && role === 'chat') {
168
+ fullSettings['completer'] = { ...settings };
169
+ }
170
+ this._settings.set('AIproviders', { ...fullSettings }).catch(console.error);
171
+ };
172
+
173
+ render(): JSX.Element {
174
+ return (
175
+ <div>
176
+ <h3>
177
+ {this.state.uniqueProvider
178
+ ? 'Chat and completer provider'
179
+ : 'Chat provider'}
180
+ </h3>
181
+ <AiProviderSettings {...this.props} role={'chat'} aiSettings={this} />
182
+ {!this.state.uniqueProvider && (
183
+ <>
184
+ <h3>Completer provider</h3>
185
+ <AiProviderSettings
186
+ {...this.props}
187
+ role={'completer'}
188
+ aiSettings={this}
189
+ />
190
+ </>
191
+ )}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ private _settings: ISettingRegistry.ISettings;
197
+ }
198
+
199
+ /**
200
+ * The AI settings component namespace.
201
+ */
202
+ namespace AiSettings {
203
+ /**
204
+ * The AI settings component states.
205
+ */
206
+ export type states = {
207
+ /**
208
+ * Whether there is only one provider for chat and completion.
209
+ */
210
+ uniqueProvider: boolean;
211
+ };
212
+ /**
213
+ * The provider names object.
214
+ */
215
+ export type providers = {
216
+ [key in ModelRole]: string;
217
+ };
218
+ /**
219
+ * The provider schemas object.
220
+ */
221
+ export type schemas = {
222
+ [key in ModelRole]: JSONSchema7;
223
+ };
224
+ }
225
+
226
+ export class AiProviderSettings extends React.Component<
227
+ AiProviderSettings.props,
228
+ AiProviderSettings.states
229
+ > {
230
+ constructor(props: AiProviderSettings.props) {
58
231
  super(props);
59
232
  if (!props.formContext.providerRegistry) {
60
233
  throw new Error(
61
234
  'The provider registry is needed to enable the jupyterlite-ai settings panel'
62
235
  );
63
236
  }
237
+ this._role = props.role;
64
238
  this._providerRegistry = props.formContext.providerRegistry;
65
239
  this._rmRegistry = props.formContext.rmRegistry ?? null;
66
240
  this._secretsManager = props.formContext.secretsManager ?? null;
67
- this._settingConnector = props.formContext.settingConnector ?? null;
68
241
  this._settings = props.formContext.settings;
69
242
 
70
- this._useSecretsManager =
243
+ const useSecretsManagerSetting =
71
244
  (this._settings.get('UseSecretsManager').composite as boolean) ?? true;
72
- this._hideSecretFields =
73
- (this._settings.get('HideSecretFields').composite as boolean) ?? true;
245
+ this._useSecretsManager =
246
+ useSecretsManagerSetting && this._secretsManager !== null;
74
247
 
75
248
  // Initialize the providers schema.
76
249
  const providerSchema = JSONExt.deepCopy(baseSettings) as any;
@@ -84,11 +257,14 @@ export class AiSettings extends React.Component<
84
257
  this._providerSchema = providerSchema as JSONSchema7;
85
258
 
86
259
  // Check if there is saved values in local storage, otherwise use the settings from
87
- // the setting registry (led to default if there are no user settings).
88
- const storageSettings = localStorage.getItem(STORAGE_NAME);
260
+ // the setting registry (leads to default if there are no user settings).
261
+ const storageKey = STORAGE_KEYS[this._role];
262
+ const storageSettings = localStorage.getItem(storageKey);
89
263
  if (storageSettings === null) {
90
- const labSettings = this._settings.get('AIprovider').composite;
91
- if (labSettings && Object.keys(labSettings).includes('provider')) {
264
+ const labSettings = this.props.aiSettings.getSettingsFromRegistry(
265
+ this._role
266
+ );
267
+ if (Object.keys(labSettings).includes('provider')) {
92
268
  // Get the provider name.
93
269
  const provider = Object.entries(labSettings).find(
94
270
  v => v[0] === 'provider'
@@ -98,38 +274,43 @@ export class AiSettings extends React.Component<
98
274
  _current: provider
99
275
  };
100
276
  settings[provider] = labSettings;
101
- localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
277
+ this.props.aiSettings.setLocalStorageItem(this._role, null, settings);
102
278
  }
103
279
  }
104
280
 
105
281
  // Initialize the settings from the saved ones.
106
282
  this._provider = this.getCurrentProvider();
107
- this._currentSettings = this.getSettings();
108
283
 
109
284
  // Initialize the schema.
110
285
  const schema = this._buildSchema();
111
- this.state = { schema, instruction: null };
112
286
 
287
+ // Initialize the current settings.
288
+ const isModified = this._updatedFormData(
289
+ this.getSettingsFromLocalStorage()
290
+ );
291
+
292
+ this.state = {
293
+ schema,
294
+ instruction: null,
295
+ compatibilityError: null,
296
+ isModified: isModified
297
+ };
113
298
  this._renderInstruction();
114
299
 
300
+ this._checkProviderCompatibility();
301
+
115
302
  // Update the setting registry.
116
- this._settings
117
- .set('AIprovider', this._currentSettings)
118
- .catch(console.error);
119
-
120
- this._settings.changed.connect(() => {
121
- const useSecretsManager =
122
- (this._settings.get('UseSecretsManager').composite as boolean) ?? true;
123
- if (useSecretsManager !== this._useSecretsManager) {
124
- this._updateUseSecretsManager(useSecretsManager);
125
- }
126
- const hideSecretFields =
127
- (this._settings.get('HideSecretFields').composite as boolean) ?? true;
128
- if (hideSecretFields !== this._hideSecretFields) {
129
- this._hideSecretFields = hideSecretFields;
130
- this._updateSchema();
131
- }
132
- });
303
+ this.saveSettingsToRegistry();
304
+
305
+ this._secretsManager?.fieldVisibilityChanged.connect(
306
+ this._fieldVisibilityChanged
307
+ );
308
+
309
+ this._settings.changed.connect(this._settingsChanged);
310
+ }
311
+
312
+ componentDidMount(): void {
313
+ this.componentDidUpdate();
133
314
  }
134
315
 
135
316
  async componentDidUpdate(): Promise<void> {
@@ -158,6 +339,10 @@ export class AiSettings extends React.Component<
158
339
  }
159
340
 
160
341
  componentWillUnmount(): void {
342
+ this._settings.changed.disconnect(this._settingsChanged);
343
+ this._secretsManager?.fieldVisibilityChanged.disconnect(
344
+ this._fieldVisibilityChanged
345
+ );
161
346
  if (!this._secretsManager || !this._useSecretsManager) {
162
347
  return;
163
348
  }
@@ -168,7 +353,7 @@ export class AiSettings extends React.Component<
168
353
  * Get the current provider from the local storage.
169
354
  */
170
355
  getCurrentProvider(): string {
171
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
356
+ const settings = this.props.aiSettings.getLocalStorage(this._role);
172
357
  return settings['_current'] ?? 'None';
173
358
  }
174
359
 
@@ -176,51 +361,95 @@ export class AiSettings extends React.Component<
176
361
  * Save the current provider to the local storage.
177
362
  */
178
363
  saveCurrentProvider(): void {
179
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
180
- settings['_current'] = this._provider;
181
- localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
364
+ this.props.aiSettings.setLocalStorageItem(
365
+ this._role,
366
+ '_current',
367
+ this._provider
368
+ );
182
369
  }
183
370
 
184
371
  /**
185
- * Get settings from local storage for a given provider.
372
+ * Get settings from local storage for the current provider provider.
186
373
  */
187
- getSettings(): IDict<any> {
188
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
374
+ getSettingsFromLocalStorage(): IDict<any> {
375
+ const settings = this.props.aiSettings.getLocalStorage(this._role);
189
376
  return settings[this._provider] ?? { provider: this._provider };
190
377
  }
191
378
 
192
379
  /**
193
380
  * Save settings in local storage for a given provider.
194
381
  */
195
- saveSettings(value: IDict<any>) {
196
- const currentSettings = { ...value };
197
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) ?? '{}');
382
+ saveSettingsToLocalStorage() {
383
+ const currentSettings = { ...this._currentSettings };
198
384
  // Do not save secrets in local storage if using the secrets manager.
199
- if (this._secretsManager && this._useSecretsManager) {
385
+ if (this._useSecretsManager) {
200
386
  this._secretFields.forEach(field => delete currentSettings[field]);
201
387
  }
202
- settings[this._provider] = currentSettings;
203
- localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
388
+ this.props.aiSettings.setLocalStorageItem(
389
+ this._role,
390
+ this._provider,
391
+ currentSettings
392
+ );
393
+ }
394
+
395
+ /**
396
+ * Save the settings to the setting registry.
397
+ */
398
+ saveSettingsToRegistry(): void {
399
+ const sanitizedSettings = { ...this._currentSettings };
400
+ if (this._useSecretsManager) {
401
+ this._secretFields.forEach(field => {
402
+ sanitizedSettings[field] = SECRETS_REPLACEMENT;
403
+ });
404
+ }
405
+
406
+ this.props.aiSettings.saveSettingsToRegistry(this._role, {
407
+ provider: this._provider,
408
+ ...sanitizedSettings
409
+ });
204
410
  }
205
411
 
412
+ /**
413
+ * Triggered when the settings has changed.
414
+ */
415
+ private _settingsChanged = (settings: ISettingRegistry.ISettings) => {
416
+ this._updateUseSecretsManager(
417
+ (this._settings.get('UseSecretsManager').composite as boolean) ?? true
418
+ );
419
+ };
420
+
421
+ /**
422
+ * Triggered when the secret fields visibility has changed.
423
+ */
424
+ private _fieldVisibilityChanged = (
425
+ _: ISecretsManager,
426
+ value: boolean
427
+ ): void => {
428
+ if (this._useSecretsManager) {
429
+ this._updateSchema();
430
+ }
431
+ };
432
+
206
433
  /**
207
434
  * Update the settings whether the secrets manager is used or not.
208
435
  *
209
436
  * @param value - whether to use the secrets manager or not.
210
437
  */
211
438
  private _updateUseSecretsManager = (value: boolean) => {
439
+ // No-op if the value did not change or the secrets manager has not been provided.
440
+ if (value === this._useSecretsManager || this._secretsManager === null) {
441
+ return;
442
+ }
443
+
444
+ // Update the secrets manager.
212
445
  this._useSecretsManager = value;
213
446
  if (!value) {
214
447
  // Detach all the password inputs attached to the secrets manager, and save the
215
448
  // current settings to the local storage to save the password.
216
- this._secretsManager?.detachAll(Private.getToken(), SECRETS_NAMESPACE);
217
- if (this._settingConnector instanceof SettingConnector) {
218
- this._settingConnector.doNotSave = [];
219
- }
220
- this.saveSettings(this._currentSettings);
449
+ this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE);
221
450
  } else {
222
451
  // Remove all the keys stored locally.
223
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
452
+ const settings = this.props.aiSettings.getLocalStorage(this._role);
224
453
  Object.keys(settings).forEach(provider => {
225
454
  Object.keys(settings[provider])
226
455
  .filter(key => key.toLowerCase().includes('key'))
@@ -228,17 +457,11 @@ export class AiSettings extends React.Component<
228
457
  delete settings[provider][key];
229
458
  });
230
459
  });
231
- localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
232
- // Update the fields not to save in settings.
233
- if (this._settingConnector instanceof SettingConnector) {
234
- this._settingConnector.doNotSave = this._secretFields;
235
- }
236
- // Attach the password inputs to the secrets manager.
237
- this.componentDidUpdate();
460
+ this.props.aiSettings.setLocalStorageItem(this._role, null, settings);
238
461
  }
239
- this._settings
240
- .set('AIprovider', { provider: this._provider, ...this._currentSettings })
241
- .catch(console.error);
462
+ this._updateSchema();
463
+ this.saveSettingsToLocalStorage();
464
+ this.saveSettingsToRegistry();
242
465
  };
243
466
 
244
467
  /**
@@ -252,27 +475,30 @@ export class AiSettings extends React.Component<
252
475
  );
253
476
 
254
477
  this._secretFields = [];
478
+ this._defaultFormData = {};
255
479
  if (settingsSchema) {
256
480
  Object.entries(settingsSchema).forEach(([key, value]) => {
257
481
  if (key.toLowerCase().includes('key')) {
258
482
  this._secretFields.push(key);
259
- if (this._hideSecretFields) {
483
+
484
+ // If the secrets manager is not used, show the secrets fields.
485
+ // If the secrets manager is used, check if the fields should be visible.
486
+ const showSecretFields =
487
+ !this._useSecretsManager ||
488
+ (this._secretsManager?.secretFieldsVisibility ?? true);
489
+ if (!showSecretFields) {
260
490
  return;
261
491
  }
492
+
262
493
  this._uiSchema[key] = { 'ui:widget': 'password' };
263
494
  }
264
495
  schema.properties[key] = value;
496
+ if (value.default !== undefined) {
497
+ this._defaultFormData[key] = value.default;
498
+ }
265
499
  });
266
500
  }
267
501
 
268
- // Do not save secrets in settings if using the secrets manager.
269
- if (
270
- this._secretsManager &&
271
- this._useSecretsManager &&
272
- this._settingConnector instanceof SettingConnector
273
- ) {
274
- this._settingConnector.doNotSave = this._secretFields;
275
- }
276
502
  return schema as JSONSchema7;
277
503
  }
278
504
 
@@ -304,7 +530,30 @@ export class AiSettings extends React.Component<
304
530
  }
305
531
 
306
532
  /**
307
- * Triggered when the provider hes changed, to update the schema and values.
533
+ * Check for compatibility of the provider with the current environment.
534
+ * If the provider is not compatible, display an error message.
535
+ */
536
+ private async _checkProviderCompatibility(): Promise<void> {
537
+ const compatibilityCheck = this._providerRegistry.getCompatibilityCheck(
538
+ this._provider
539
+ );
540
+ if (!compatibilityCheck) {
541
+ this.setState({ compatibilityError: null });
542
+ return;
543
+ }
544
+ const error = await compatibilityCheck();
545
+ if (!error) {
546
+ this.setState({ compatibilityError: null });
547
+ return;
548
+ }
549
+ const errorDiv = document.createElement('div');
550
+ errorDiv.className = ERROR_CLASS;
551
+ errorDiv.innerHTML = error;
552
+ this.setState({ compatibilityError: error });
553
+ }
554
+
555
+ /**
556
+ * Triggered when the provider has changed, to update the schema and values.
308
557
  * Update the Jupyterlab settings accordingly.
309
558
  */
310
559
  private _onProviderChanged = (e: IChangeEvent) => {
@@ -314,12 +563,18 @@ export class AiSettings extends React.Component<
314
563
  }
315
564
  this._provider = provider;
316
565
  this.saveCurrentProvider();
317
- this._currentSettings = this.getSettings();
318
566
  this._updateSchema();
319
567
  this._renderInstruction();
320
- this._settings
321
- .set('AIprovider', { provider: this._provider, ...this._currentSettings })
322
- .catch(console.error);
568
+ this._checkProviderCompatibility();
569
+
570
+ // Initialize the current settings.
571
+ const isModified = this._updatedFormData(
572
+ this.getSettingsFromLocalStorage()
573
+ );
574
+ if (isModified !== this.state.isModified) {
575
+ this.setState({ isModified });
576
+ }
577
+ this.saveSettingsToRegistry();
323
578
  };
324
579
 
325
580
  /**
@@ -328,22 +583,65 @@ export class AiSettings extends React.Component<
328
583
  */
329
584
  private _onPasswordUpdated = (fieldName: string, value: string) => {
330
585
  this._currentSettings[fieldName] = value;
331
- this._settings
332
- .set('AIprovider', { provider: this._provider, ...this._currentSettings })
333
- .catch(console.error);
586
+ this.saveSettingsToRegistry();
334
587
  };
335
588
 
589
+ /**
590
+ * Update the current settings with the new values from the form.
591
+ *
592
+ * @param data - The form data to update.
593
+ * @returns - Boolean whether the form is not the default one.
594
+ */
595
+ private _updatedFormData(data: IDict): boolean {
596
+ let isModified = false;
597
+ Object.entries(data).forEach(([key, value]) => {
598
+ if (this._defaultFormData[key] !== undefined) {
599
+ if (value === undefined) {
600
+ const schemaProperty = this.state.schema.properties?.[
601
+ key
602
+ ] as JSONSchema7;
603
+ if (schemaProperty.type === 'string') {
604
+ data[key] = '';
605
+ }
606
+ }
607
+ if (value !== this._defaultFormData[key]) {
608
+ isModified = true;
609
+ }
610
+ }
611
+ });
612
+ this._currentSettings = JSONExt.deepCopy(data);
613
+ return isModified;
614
+ }
615
+
336
616
  /**
337
617
  * Triggered when the form value has changed, to update the current settings and save
338
618
  * it in local storage.
339
619
  * Update the Jupyterlab settings accordingly.
340
620
  */
341
- private _onFormChange = (e: IChangeEvent) => {
342
- this._currentSettings = JSONExt.deepCopy(e.formData);
343
- this.saveSettings(this._currentSettings);
344
- this._settings
345
- .set('AIprovider', { provider: this._provider, ...this._currentSettings })
346
- .catch(console.error);
621
+ private _onFormChanged = (e: IChangeEvent): void => {
622
+ const { formData } = e;
623
+ const isModified = this._updatedFormData(formData);
624
+ this.saveSettingsToLocalStorage();
625
+ this.saveSettingsToRegistry();
626
+ if (isModified !== this.state.isModified) {
627
+ this.setState({ isModified });
628
+ }
629
+ };
630
+
631
+ /**
632
+ * Handler for the "Restore to defaults" button - clears all
633
+ * modified settings then calls `setFormData` to restore the
634
+ * values.
635
+ */
636
+ private _reset = async (event: React.MouseEvent): Promise<void> => {
637
+ event.stopPropagation();
638
+ this._currentSettings = {
639
+ ...this._currentSettings,
640
+ ...this._defaultFormData
641
+ };
642
+ this.saveSettingsToLocalStorage();
643
+ this.saveSettingsToRegistry();
644
+ this.setState({ isModified: false });
347
645
  };
348
646
 
349
647
  render(): JSX.Element {
@@ -353,7 +651,14 @@ export class AiSettings extends React.Component<
353
651
  formData={{ provider: this._provider }}
354
652
  schema={this._providerSchema}
355
653
  onChange={this._onProviderChanged}
654
+ idPrefix={`jp-SettingsEditor-${PLUGIN_IDS.providerRegistry}-${this._role}`}
356
655
  />
656
+ {this.state.compatibilityError !== null && (
657
+ <div className={ERROR_CLASS}>
658
+ <i className={'fas fa-exclamation-triangle'}></i>
659
+ <span>{this.state.compatibilityError}</span>
660
+ </div>
661
+ )}
357
662
  {this.state.instruction !== null && (
358
663
  <details>
359
664
  <summary className={INSTRUCTION_CLASS}>Instructions</summary>
@@ -364,29 +669,86 @@ export class AiSettings extends React.Component<
364
669
  />
365
670
  </details>
366
671
  )}
672
+ <div className="jp-SettingsHeader">
673
+ <h3 title={this._provider}>{this._provider}</h3>
674
+ <div className="jp-SettingsHeader-buttonbar">
675
+ {this.state.isModified && (
676
+ <Button className="jp-RestoreButton" onClick={this._reset}>
677
+ Restore to Defaults
678
+ </Button>
679
+ )}
680
+ </div>
681
+ </div>
367
682
  <WrappedFormComponent
368
683
  formData={this._currentSettings}
369
684
  schema={this.state.schema}
370
- onChange={this._onFormChange}
685
+ onChange={this._onFormChanged}
371
686
  uiSchema={this._uiSchema}
687
+ idPrefix={`jp-SettingsEditor-${PLUGIN_IDS.providerRegistry}-${this._role}`}
688
+ formContext={{
689
+ ...this.props.formContext,
690
+ defaultFormData: this._defaultFormData
691
+ }}
372
692
  />
373
693
  </div>
374
694
  );
375
695
  }
376
696
 
697
+ private _role: ModelRole;
377
698
  private _providerRegistry: IAIProviderRegistry;
378
699
  private _provider: string;
379
700
  private _providerSchema: JSONSchema7;
380
701
  private _useSecretsManager: boolean;
381
- private _hideSecretFields: boolean;
382
702
  private _rmRegistry: IRenderMimeRegistry | null;
383
703
  private _secretsManager: ISecretsManager | null;
384
- private _settingConnector: ISettingConnector | null;
385
704
  private _currentSettings: IDict<any> = { provider: 'None' };
386
705
  private _uiSchema: IDict<any> = {};
387
706
  private _settings: ISettingRegistry.ISettings;
388
707
  private _formRef = React.createRef<HTMLDivElement>();
389
708
  private _secretFields: string[] = [];
709
+ private _defaultFormData: IDict<any> = {};
710
+ }
711
+
712
+ /**
713
+ * The AI provider settings component namespace.
714
+ */
715
+ export namespace AiProviderSettings {
716
+ /**
717
+ * The AI provider settings component props.
718
+ */
719
+ export type props = FieldProps & {
720
+ /**
721
+ * Why this model is used for (chat or completion).
722
+ */
723
+ role: ModelRole;
724
+ /**
725
+ * The parent component which should handle:
726
+ * - the get/set functions for local storage
727
+ * - save settings using jupyter settings system
728
+ */
729
+ aiSettings: IAiSettings;
730
+ };
731
+ /**
732
+ * The AI provider settings component states.
733
+ */
734
+ export type states = {
735
+ /**
736
+ * The schema of the settings.
737
+ */
738
+ schema: JSONSchema7;
739
+ /**
740
+ * The instructions for this provider.
741
+ */
742
+ instruction: HTMLElement | null;
743
+ /**
744
+ * An error if the model in not compatible with the current environment.
745
+ */
746
+ compatibilityError: string | null;
747
+ /**
748
+ * Whether the settings are modified from default or not.
749
+ */
750
+ isModified?: boolean;
751
+ };
390
752
  }
391
753
 
392
754
  namespace Private {