@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,14 +1,18 @@
1
- import { FormComponent } from '@jupyterlab/ui-components';
1
+ import { Button, FormComponent } from '@jupyterlab/ui-components';
2
2
  import { JSONExt } from '@lumino/coreutils';
3
3
  import validator from '@rjsf/validator-ajv8';
4
4
  import React from 'react';
5
- import { getSecretId, SettingConnector } from '.';
5
+ import { getSecretId, SECRETS_REPLACEMENT } from '.';
6
6
  import baseSettings from './base.json';
7
7
  import { PLUGIN_IDS } from '../tokens';
8
8
  const MD_MIME_TYPE = 'text/markdown';
9
- const STORAGE_NAME = '@jupyterlite/ai:settings';
10
9
  const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';
10
+ const ERROR_CLASS = 'jp-AISettingsError';
11
11
  const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry;
12
+ const STORAGE_KEYS = {
13
+ chat: '@jupyterlite/ai:chat-settings',
14
+ completer: '@jupyterlite/ai:completer-settings'
15
+ };
12
16
  export const aiSettingsRenderer = (options) => {
13
17
  const { secretsToken } = options;
14
18
  delete options.secretsToken;
@@ -27,103 +31,107 @@ const WrappedFormComponent = (props) => {
27
31
  };
28
32
  export class AiSettings extends React.Component {
29
33
  constructor(props) {
30
- var _a, _b, _c, _d, _e, _f;
31
34
  super(props);
32
- /**
33
- * Update the settings whether the secrets manager is used or not.
34
- *
35
- * @param value - whether to use the secrets manager or not.
36
- */
37
- this._updateUseSecretsManager = (value) => {
38
- var _a;
39
- this._useSecretsManager = value;
40
- if (!value) {
41
- // Detach all the password inputs attached to the secrets manager, and save the
42
- // current settings to the local storage to save the password.
43
- (_a = this._secretsManager) === null || _a === void 0 ? void 0 : _a.detachAll(Private.getToken(), SECRETS_NAMESPACE);
44
- if (this._settingConnector instanceof SettingConnector) {
45
- this._settingConnector.doNotSave = [];
46
- }
47
- this.saveSettings(this._currentSettings);
48
- }
49
- else {
50
- // Remove all the keys stored locally.
51
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
52
- Object.keys(settings).forEach(provider => {
53
- Object.keys(settings[provider])
54
- .filter(key => key.toLowerCase().includes('key'))
55
- .forEach(key => {
56
- delete settings[provider][key];
57
- });
58
- });
59
- localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
60
- // Update the fields not to save in settings.
61
- if (this._settingConnector instanceof SettingConnector) {
62
- this._settingConnector.doNotSave = this._secretFields;
63
- }
64
- // Attach the password inputs to the secrets manager.
65
- this.componentDidUpdate();
66
- }
67
- this._settings
68
- .set('AIprovider', { provider: this._provider, ...this._currentSettings })
69
- .catch(console.error);
70
- };
71
- /**
72
- * Triggered when the provider hes changed, to update the schema and values.
73
- * Update the Jupyterlab settings accordingly.
74
- */
75
- this._onProviderChanged = (e) => {
76
- const provider = e.formData.provider;
77
- if (provider === this._currentSettings.provider) {
78
- return;
79
- }
80
- this._provider = provider;
81
- this.saveCurrentProvider();
82
- this._currentSettings = this.getSettings();
83
- this._updateSchema();
84
- this._renderInstruction();
85
- this._settings
86
- .set('AIprovider', { provider: this._provider, ...this._currentSettings })
87
- .catch(console.error);
88
- };
89
- /**
90
- * Callback function called when the password input has been programmatically updated
91
- * with the secret manager.
92
- */
93
- this._onPasswordUpdated = (fieldName, value) => {
94
- this._currentSettings[fieldName] = value;
95
- this._settings
96
- .set('AIprovider', { provider: this._provider, ...this._currentSettings })
97
- .catch(console.error);
98
- };
99
- /**
100
- * Triggered when the form value has changed, to update the current settings and save
101
- * it in local storage.
102
- * Update the Jupyterlab settings accordingly.
103
- */
104
- this._onFormChange = (e) => {
105
- this._currentSettings = JSONExt.deepCopy(e.formData);
106
- this.saveSettings(this._currentSettings);
107
- this._settings
108
- .set('AIprovider', { provider: this._provider, ...this._currentSettings })
109
- .catch(console.error);
110
- };
111
- this._currentSettings = { provider: 'None' };
112
- this._uiSchema = {};
113
- this._formRef = React.createRef();
114
- this._secretFields = [];
35
+ this._settings = props.formContext.settings;
36
+ const uniqueProvider = this._settings.get('UniqueProvider').composite ?? true;
37
+ this.state = { uniqueProvider };
38
+ this._settings.changed.connect(this._settingsChanged);
39
+ }
40
+ _settingsChanged = () => {
41
+ const uniqueProvider = this._settings.get('UniqueProvider').composite ?? true;
42
+ if (this.state.uniqueProvider === uniqueProvider) {
43
+ return;
44
+ }
45
+ if (uniqueProvider) {
46
+ // Copy chat settings to the completer settings if there should be a unique
47
+ // provider for both.
48
+ this.setLocalStorageItem('completer', null, this.getLocalStorage('chat'));
49
+ this.saveSettingsToRegistry('completer', this.getSettingsFromRegistry('chat'));
50
+ }
51
+ this.setState({ uniqueProvider });
52
+ };
53
+ /**
54
+ * Get the local storage settings for a specific role (chat or completer).
55
+ */
56
+ getLocalStorage = (role) => {
57
+ const storageKey = STORAGE_KEYS[role];
58
+ return JSON.parse(localStorage.getItem(storageKey) ?? '{}');
59
+ };
60
+ /**
61
+ * Set the local storage item for a specific role (chat or completer).
62
+ * If the key is not provider (null) we assume the value should replace the whole
63
+ * local storage for this role.
64
+ */
65
+ setLocalStorageItem = (role, key, value) => {
66
+ const storageKey = STORAGE_KEYS[role];
67
+ let settings;
68
+ if (key !== null) {
69
+ settings = JSON.parse(localStorage.getItem(storageKey) ?? '{}');
70
+ settings[key] = value;
71
+ }
72
+ else {
73
+ settings = value;
74
+ }
75
+ localStorage.setItem(storageKey, JSON.stringify(settings));
76
+ // If both chat and completer use the same settings, only the chat settings should
77
+ // be editable for user, so we should duplicate its values to the completer
78
+ // local storage.
79
+ if (this.state.uniqueProvider && role === 'chat') {
80
+ const storageKeyCompleter = STORAGE_KEYS['completer'];
81
+ localStorage.setItem(storageKeyCompleter, JSON.stringify(settings));
82
+ }
83
+ };
84
+ /**
85
+ * Get the settings from the registry (jupyterlab settings system) for a given role.
86
+ */
87
+ getSettingsFromRegistry = (role) => {
88
+ const settings = this._settings.get('AIproviders')
89
+ .composite;
90
+ return settings && Object.keys(settings).includes(role)
91
+ ? settings[role]
92
+ : { provider: 'None' };
93
+ };
94
+ /**
95
+ * Save the settings to the setting registry.
96
+ */
97
+ saveSettingsToRegistry = (role, settings) => {
98
+ const fullSettings = this._settings.get('AIproviders')
99
+ .composite;
100
+ fullSettings[role] = { ...settings };
101
+ // If both chat and completer use the same settings, only the chat settings should
102
+ // be editable for user, so we should duplicate its values to the completer
103
+ // settings.
104
+ if (this.state.uniqueProvider && role === 'chat') {
105
+ fullSettings['completer'] = { ...settings };
106
+ }
107
+ this._settings.set('AIproviders', { ...fullSettings }).catch(console.error);
108
+ };
109
+ render() {
110
+ return (React.createElement("div", null,
111
+ React.createElement("h3", null, this.state.uniqueProvider
112
+ ? 'Chat and completer provider'
113
+ : 'Chat provider'),
114
+ React.createElement(AiProviderSettings, { ...this.props, role: 'chat', aiSettings: this }),
115
+ !this.state.uniqueProvider && (React.createElement(React.Fragment, null,
116
+ React.createElement("h3", null, "Completer provider"),
117
+ React.createElement(AiProviderSettings, { ...this.props, role: 'completer', aiSettings: this })))));
118
+ }
119
+ _settings;
120
+ }
121
+ export class AiProviderSettings extends React.Component {
122
+ constructor(props) {
123
+ super(props);
115
124
  if (!props.formContext.providerRegistry) {
116
125
  throw new Error('The provider registry is needed to enable the jupyterlite-ai settings panel');
117
126
  }
127
+ this._role = props.role;
118
128
  this._providerRegistry = props.formContext.providerRegistry;
119
- this._rmRegistry = (_a = props.formContext.rmRegistry) !== null && _a !== void 0 ? _a : null;
120
- this._secretsManager = (_b = props.formContext.secretsManager) !== null && _b !== void 0 ? _b : null;
121
- this._settingConnector = (_c = props.formContext.settingConnector) !== null && _c !== void 0 ? _c : null;
129
+ this._rmRegistry = props.formContext.rmRegistry ?? null;
130
+ this._secretsManager = props.formContext.secretsManager ?? null;
122
131
  this._settings = props.formContext.settings;
132
+ const useSecretsManagerSetting = this._settings.get('UseSecretsManager').composite ?? true;
123
133
  this._useSecretsManager =
124
- (_d = this._settings.get('UseSecretsManager').composite) !== null && _d !== void 0 ? _d : true;
125
- this._hideSecretFields =
126
- (_e = this._settings.get('HideSecretFields').composite) !== null && _e !== void 0 ? _e : true;
134
+ useSecretsManagerSetting && this._secretsManager !== null;
127
135
  // Initialize the providers schema.
128
136
  const providerSchema = JSONExt.deepCopy(baseSettings);
129
137
  providerSchema.properties.provider = {
@@ -135,53 +143,51 @@ export class AiSettings extends React.Component {
135
143
  };
136
144
  this._providerSchema = providerSchema;
137
145
  // Check if there is saved values in local storage, otherwise use the settings from
138
- // the setting registry (led to default if there are no user settings).
139
- const storageSettings = localStorage.getItem(STORAGE_NAME);
146
+ // the setting registry (leads to default if there are no user settings).
147
+ const storageKey = STORAGE_KEYS[this._role];
148
+ const storageSettings = localStorage.getItem(storageKey);
140
149
  if (storageSettings === null) {
141
- const labSettings = this._settings.get('AIprovider').composite;
142
- if (labSettings && Object.keys(labSettings).includes('provider')) {
150
+ const labSettings = this.props.aiSettings.getSettingsFromRegistry(this._role);
151
+ if (Object.keys(labSettings).includes('provider')) {
143
152
  // Get the provider name.
144
- const provider = (_f = Object.entries(labSettings).find(v => v[0] === 'provider')) === null || _f === void 0 ? void 0 : _f[1];
153
+ const provider = Object.entries(labSettings).find(v => v[0] === 'provider')?.[1];
145
154
  // Save the settings.
146
155
  const settings = {
147
156
  _current: provider
148
157
  };
149
158
  settings[provider] = labSettings;
150
- localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
159
+ this.props.aiSettings.setLocalStorageItem(this._role, null, settings);
151
160
  }
152
161
  }
153
162
  // Initialize the settings from the saved ones.
154
163
  this._provider = this.getCurrentProvider();
155
- this._currentSettings = this.getSettings();
156
164
  // Initialize the schema.
157
165
  const schema = this._buildSchema();
158
- this.state = { schema, instruction: null };
166
+ // Initialize the current settings.
167
+ const isModified = this._updatedFormData(this.getSettingsFromLocalStorage());
168
+ this.state = {
169
+ schema,
170
+ instruction: null,
171
+ compatibilityError: null,
172
+ isModified: isModified
173
+ };
159
174
  this._renderInstruction();
175
+ this._checkProviderCompatibility();
160
176
  // Update the setting registry.
161
- this._settings
162
- .set('AIprovider', this._currentSettings)
163
- .catch(console.error);
164
- this._settings.changed.connect(() => {
165
- var _a, _b;
166
- const useSecretsManager = (_a = this._settings.get('UseSecretsManager').composite) !== null && _a !== void 0 ? _a : true;
167
- if (useSecretsManager !== this._useSecretsManager) {
168
- this._updateUseSecretsManager(useSecretsManager);
169
- }
170
- const hideSecretFields = (_b = this._settings.get('HideSecretFields').composite) !== null && _b !== void 0 ? _b : true;
171
- if (hideSecretFields !== this._hideSecretFields) {
172
- this._hideSecretFields = hideSecretFields;
173
- this._updateSchema();
174
- }
175
- });
177
+ this.saveSettingsToRegistry();
178
+ this._secretsManager?.fieldVisibilityChanged.connect(this._fieldVisibilityChanged);
179
+ this._settings.changed.connect(this._settingsChanged);
180
+ }
181
+ componentDidMount() {
182
+ this.componentDidUpdate();
176
183
  }
177
184
  async componentDidUpdate() {
178
- var _a;
179
185
  if (!this._secretsManager || !this._useSecretsManager) {
180
186
  return;
181
187
  }
182
188
  // Attach the password inputs to the secrets manager.
183
189
  await this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE);
184
- const inputs = ((_a = this._formRef.current) === null || _a === void 0 ? void 0 : _a.getElementsByTagName('input')) || [];
190
+ const inputs = this._formRef.current?.getElementsByTagName('input') || [];
185
191
  for (let i = 0; i < inputs.length; i++) {
186
192
  if (inputs[i].type.toLowerCase() === 'password') {
187
193
  const label = inputs[i].getAttribute('label');
@@ -193,6 +199,8 @@ export class AiSettings extends React.Component {
193
199
  }
194
200
  }
195
201
  componentWillUnmount() {
202
+ this._settings.changed.disconnect(this._settingsChanged);
203
+ this._secretsManager?.fieldVisibilityChanged.disconnect(this._fieldVisibilityChanged);
196
204
  if (!this._secretsManager || !this._useSecretsManager) {
197
205
  return;
198
206
  }
@@ -202,40 +210,95 @@ export class AiSettings extends React.Component {
202
210
  * Get the current provider from the local storage.
203
211
  */
204
212
  getCurrentProvider() {
205
- var _a;
206
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
207
- return (_a = settings['_current']) !== null && _a !== void 0 ? _a : 'None';
213
+ const settings = this.props.aiSettings.getLocalStorage(this._role);
214
+ return settings['_current'] ?? 'None';
208
215
  }
209
216
  /**
210
217
  * Save the current provider to the local storage.
211
218
  */
212
219
  saveCurrentProvider() {
213
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
214
- settings['_current'] = this._provider;
215
- localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
220
+ this.props.aiSettings.setLocalStorageItem(this._role, '_current', this._provider);
216
221
  }
217
222
  /**
218
- * Get settings from local storage for a given provider.
223
+ * Get settings from local storage for the current provider provider.
219
224
  */
220
- getSettings() {
221
- var _a;
222
- const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) || '{}');
223
- return (_a = settings[this._provider]) !== null && _a !== void 0 ? _a : { provider: this._provider };
225
+ getSettingsFromLocalStorage() {
226
+ const settings = this.props.aiSettings.getLocalStorage(this._role);
227
+ return settings[this._provider] ?? { provider: this._provider };
224
228
  }
225
229
  /**
226
230
  * Save settings in local storage for a given provider.
227
231
  */
228
- saveSettings(value) {
229
- var _a;
230
- const currentSettings = { ...value };
231
- const settings = JSON.parse((_a = localStorage.getItem(STORAGE_NAME)) !== null && _a !== void 0 ? _a : '{}');
232
+ saveSettingsToLocalStorage() {
233
+ const currentSettings = { ...this._currentSettings };
232
234
  // Do not save secrets in local storage if using the secrets manager.
233
- if (this._secretsManager && this._useSecretsManager) {
235
+ if (this._useSecretsManager) {
234
236
  this._secretFields.forEach(field => delete currentSettings[field]);
235
237
  }
236
- settings[this._provider] = currentSettings;
237
- localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
238
+ this.props.aiSettings.setLocalStorageItem(this._role, this._provider, currentSettings);
239
+ }
240
+ /**
241
+ * Save the settings to the setting registry.
242
+ */
243
+ saveSettingsToRegistry() {
244
+ const sanitizedSettings = { ...this._currentSettings };
245
+ if (this._useSecretsManager) {
246
+ this._secretFields.forEach(field => {
247
+ sanitizedSettings[field] = SECRETS_REPLACEMENT;
248
+ });
249
+ }
250
+ this.props.aiSettings.saveSettingsToRegistry(this._role, {
251
+ provider: this._provider,
252
+ ...sanitizedSettings
253
+ });
238
254
  }
255
+ /**
256
+ * Triggered when the settings has changed.
257
+ */
258
+ _settingsChanged = (settings) => {
259
+ this._updateUseSecretsManager(this._settings.get('UseSecretsManager').composite ?? true);
260
+ };
261
+ /**
262
+ * Triggered when the secret fields visibility has changed.
263
+ */
264
+ _fieldVisibilityChanged = (_, value) => {
265
+ if (this._useSecretsManager) {
266
+ this._updateSchema();
267
+ }
268
+ };
269
+ /**
270
+ * Update the settings whether the secrets manager is used or not.
271
+ *
272
+ * @param value - whether to use the secrets manager or not.
273
+ */
274
+ _updateUseSecretsManager = (value) => {
275
+ // No-op if the value did not change or the secrets manager has not been provided.
276
+ if (value === this._useSecretsManager || this._secretsManager === null) {
277
+ return;
278
+ }
279
+ // Update the secrets manager.
280
+ this._useSecretsManager = value;
281
+ if (!value) {
282
+ // Detach all the password inputs attached to the secrets manager, and save the
283
+ // current settings to the local storage to save the password.
284
+ this._secretsManager.detachAll(Private.getToken(), SECRETS_NAMESPACE);
285
+ }
286
+ else {
287
+ // Remove all the keys stored locally.
288
+ const settings = this.props.aiSettings.getLocalStorage(this._role);
289
+ Object.keys(settings).forEach(provider => {
290
+ Object.keys(settings[provider])
291
+ .filter(key => key.toLowerCase().includes('key'))
292
+ .forEach(key => {
293
+ delete settings[provider][key];
294
+ });
295
+ });
296
+ this.props.aiSettings.setLocalStorageItem(this._role, null, settings);
297
+ }
298
+ this._updateSchema();
299
+ this.saveSettingsToLocalStorage();
300
+ this.saveSettingsToRegistry();
301
+ };
239
302
  /**
240
303
  * Build the schema for a given provider.
241
304
  */
@@ -244,24 +307,26 @@ export class AiSettings extends React.Component {
244
307
  this._uiSchema = {};
245
308
  const settingsSchema = this._providerRegistry.getSettingsSchema(this._provider);
246
309
  this._secretFields = [];
310
+ this._defaultFormData = {};
247
311
  if (settingsSchema) {
248
312
  Object.entries(settingsSchema).forEach(([key, value]) => {
249
313
  if (key.toLowerCase().includes('key')) {
250
314
  this._secretFields.push(key);
251
- if (this._hideSecretFields) {
315
+ // If the secrets manager is not used, show the secrets fields.
316
+ // If the secrets manager is used, check if the fields should be visible.
317
+ const showSecretFields = !this._useSecretsManager ||
318
+ (this._secretsManager?.secretFieldsVisibility ?? true);
319
+ if (!showSecretFields) {
252
320
  return;
253
321
  }
254
322
  this._uiSchema[key] = { 'ui:widget': 'password' };
255
323
  }
256
324
  schema.properties[key] = value;
325
+ if (value.default !== undefined) {
326
+ this._defaultFormData[key] = value.default;
327
+ }
257
328
  });
258
329
  }
259
- // Do not save secrets in settings if using the secrets manager.
260
- if (this._secretsManager &&
261
- this._useSecretsManager &&
262
- this._settingConnector instanceof SettingConnector) {
263
- this._settingConnector.doNotSave = this._secretFields;
264
- }
265
330
  return schema;
266
331
  }
267
332
  /**
@@ -289,14 +354,138 @@ export class AiSettings extends React.Component {
289
354
  await renderer.renderModel(model);
290
355
  this.setState({ instruction: renderer.node });
291
356
  }
357
+ /**
358
+ * Check for compatibility of the provider with the current environment.
359
+ * If the provider is not compatible, display an error message.
360
+ */
361
+ async _checkProviderCompatibility() {
362
+ const compatibilityCheck = this._providerRegistry.getCompatibilityCheck(this._provider);
363
+ if (!compatibilityCheck) {
364
+ this.setState({ compatibilityError: null });
365
+ return;
366
+ }
367
+ const error = await compatibilityCheck();
368
+ if (!error) {
369
+ this.setState({ compatibilityError: null });
370
+ return;
371
+ }
372
+ const errorDiv = document.createElement('div');
373
+ errorDiv.className = ERROR_CLASS;
374
+ errorDiv.innerHTML = error;
375
+ this.setState({ compatibilityError: error });
376
+ }
377
+ /**
378
+ * Triggered when the provider has changed, to update the schema and values.
379
+ * Update the Jupyterlab settings accordingly.
380
+ */
381
+ _onProviderChanged = (e) => {
382
+ const provider = e.formData.provider;
383
+ if (provider === this._currentSettings.provider) {
384
+ return;
385
+ }
386
+ this._provider = provider;
387
+ this.saveCurrentProvider();
388
+ this._updateSchema();
389
+ this._renderInstruction();
390
+ this._checkProviderCompatibility();
391
+ // Initialize the current settings.
392
+ const isModified = this._updatedFormData(this.getSettingsFromLocalStorage());
393
+ if (isModified !== this.state.isModified) {
394
+ this.setState({ isModified });
395
+ }
396
+ this.saveSettingsToRegistry();
397
+ };
398
+ /**
399
+ * Callback function called when the password input has been programmatically updated
400
+ * with the secret manager.
401
+ */
402
+ _onPasswordUpdated = (fieldName, value) => {
403
+ this._currentSettings[fieldName] = value;
404
+ this.saveSettingsToRegistry();
405
+ };
406
+ /**
407
+ * Update the current settings with the new values from the form.
408
+ *
409
+ * @param data - The form data to update.
410
+ * @returns - Boolean whether the form is not the default one.
411
+ */
412
+ _updatedFormData(data) {
413
+ let isModified = false;
414
+ Object.entries(data).forEach(([key, value]) => {
415
+ if (this._defaultFormData[key] !== undefined) {
416
+ if (value === undefined) {
417
+ const schemaProperty = this.state.schema.properties?.[key];
418
+ if (schemaProperty.type === 'string') {
419
+ data[key] = '';
420
+ }
421
+ }
422
+ if (value !== this._defaultFormData[key]) {
423
+ isModified = true;
424
+ }
425
+ }
426
+ });
427
+ this._currentSettings = JSONExt.deepCopy(data);
428
+ return isModified;
429
+ }
430
+ /**
431
+ * Triggered when the form value has changed, to update the current settings and save
432
+ * it in local storage.
433
+ * Update the Jupyterlab settings accordingly.
434
+ */
435
+ _onFormChanged = (e) => {
436
+ const { formData } = e;
437
+ const isModified = this._updatedFormData(formData);
438
+ this.saveSettingsToLocalStorage();
439
+ this.saveSettingsToRegistry();
440
+ if (isModified !== this.state.isModified) {
441
+ this.setState({ isModified });
442
+ }
443
+ };
444
+ /**
445
+ * Handler for the "Restore to defaults" button - clears all
446
+ * modified settings then calls `setFormData` to restore the
447
+ * values.
448
+ */
449
+ _reset = async (event) => {
450
+ event.stopPropagation();
451
+ this._currentSettings = {
452
+ ...this._currentSettings,
453
+ ...this._defaultFormData
454
+ };
455
+ this.saveSettingsToLocalStorage();
456
+ this.saveSettingsToRegistry();
457
+ this.setState({ isModified: false });
458
+ };
292
459
  render() {
293
460
  return (React.createElement("div", { ref: this._formRef },
294
- React.createElement(WrappedFormComponent, { formData: { provider: this._provider }, schema: this._providerSchema, onChange: this._onProviderChanged }),
461
+ React.createElement(WrappedFormComponent, { formData: { provider: this._provider }, schema: this._providerSchema, onChange: this._onProviderChanged, idPrefix: `jp-SettingsEditor-${PLUGIN_IDS.providerRegistry}-${this._role}` }),
462
+ this.state.compatibilityError !== null && (React.createElement("div", { className: ERROR_CLASS },
463
+ React.createElement("i", { className: 'fas fa-exclamation-triangle' }),
464
+ React.createElement("span", null, this.state.compatibilityError))),
295
465
  this.state.instruction !== null && (React.createElement("details", null,
296
466
  React.createElement("summary", { className: INSTRUCTION_CLASS }, "Instructions"),
297
467
  React.createElement("span", { ref: node => node && node.replaceChildren(this.state.instruction) }))),
298
- React.createElement(WrappedFormComponent, { formData: this._currentSettings, schema: this.state.schema, onChange: this._onFormChange, uiSchema: this._uiSchema })));
468
+ React.createElement("div", { className: "jp-SettingsHeader" },
469
+ React.createElement("h3", { title: this._provider }, this._provider),
470
+ React.createElement("div", { className: "jp-SettingsHeader-buttonbar" }, this.state.isModified && (React.createElement(Button, { className: "jp-RestoreButton", onClick: this._reset }, "Restore to Defaults")))),
471
+ React.createElement(WrappedFormComponent, { formData: this._currentSettings, schema: this.state.schema, onChange: this._onFormChanged, uiSchema: this._uiSchema, idPrefix: `jp-SettingsEditor-${PLUGIN_IDS.providerRegistry}-${this._role}`, formContext: {
472
+ ...this.props.formContext,
473
+ defaultFormData: this._defaultFormData
474
+ } })));
299
475
  }
476
+ _role;
477
+ _providerRegistry;
478
+ _provider;
479
+ _providerSchema;
480
+ _useSecretsManager;
481
+ _rmRegistry;
482
+ _secretsManager;
483
+ _currentSettings = { provider: 'None' };
484
+ _uiSchema = {};
485
+ _settings;
486
+ _formRef = React.createRef();
487
+ _secretFields = [];
488
+ _defaultFormData = {};
300
489
  }
301
490
  var Private;
302
491
  (function (Private) {
@@ -0,0 +1,2 @@
1
+ import { IFormRenderer } from '@jupyterlab/ui-components';
2
+ export declare const textArea: IFormRenderer;
@@ -0,0 +1,18 @@
1
+ import React, { useState } from 'react';
2
+ const TEXTAREA_CLASS = 'jp-AISettingsTextArea';
3
+ export const textArea = {
4
+ fieldRenderer: (props) => {
5
+ const settings = props.formContext.settings;
6
+ const schema = settings.schema.properties?.[props.name];
7
+ const [formData, setFormData] = useState(props.formData);
8
+ settings.changed.connect(() => {
9
+ setFormData(settings.get(props.name).composite);
10
+ });
11
+ const onChange = (event) => {
12
+ settings.set(props.name, event.target.value);
13
+ };
14
+ return (React.createElement(React.Fragment, null,
15
+ schema?.title && (React.createElement("h3", { className: "jp-FormGroup-fieldLabel jp-FormGroup-contentItem" }, schema.title)),
16
+ React.createElement("textarea", { className: TEXTAREA_CLASS, onChange: onChange }, formData)));
17
+ }
18
+ };