Skip to content

Commit c7d428d

Browse files
authored
Settings UI improvement (#48)
* First version of custom react widget in the setting panel * fix lint and dependencies * Password input for field with name containing 'key' * Use the local storage to have persistent settings * Add an index.ts file when building schema to easily import them * Fix the settings schema * Initialize the settings with the last used provider * lint * Rename the main setting to avoid confusion (it was the same name as the provider select) * Split the form in (1) provider select and (2) provider options, to be able to add some helper between them * Add an instruction DOM in the settings * Use default values if they are provided and the local storage does not exist * Rename attribute for consistency * Use a factory to send the rendermime registry to the AISettings component * Use the user settings (or default) if none in local storage * Apply suggestions from review
1 parent 336fb46 commit c7d428d

File tree

12 files changed

+600
-73
lines changed

12 files changed

+600
-73
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ jupyterlite_ai/labextension
1111
# Version file is handled by hatchling
1212
jupyterlite_ai/_version.py
1313

14-
# Settings schema are built
15-
src/_provider-settings
14+
# Schema and module built at build time
15+
src/settings/schemas/index.ts
16+
src/settings/schemas/_generated
1617

1718
# Created by https://www.gitignore.io/api/python
1819
# Edit at https://www.gitignore.io/?templates=python

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@jupyterlab/notebook": "^4.4.0-alpha.0",
6262
"@jupyterlab/rendermime": "^4.4.0-alpha.0",
6363
"@jupyterlab/settingregistry": "^4.4.0-alpha.0",
64+
"@jupyterlab/ui-components": "^4.4.0-alpha.0",
6465
"@langchain/anthropic": "^0.3.9",
6566
"@langchain/community": "^0.3.31",
6667
"@langchain/core": "^0.3.40",
@@ -71,6 +72,9 @@
7172
"@lumino/signaling": "^2.1.2",
7273
"@mui/icons-material": "^5.11.0",
7374
"@mui/material": "^5.11.0",
75+
"@rjsf/core": "^4.2.0",
76+
"@rjsf/utils": "^5.18.4",
77+
"@rjsf/validator-ajv8": "^5.18.4",
7478
"react": "^18.2.0",
7579
"react-dom": "^18.2.0"
7680
},

schema/ai-provider.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
"jupyter.lab.setting-icon-label": "JupyterLite AI Chat",
66
"type": "object",
77
"properties": {
8-
"provider": {
9-
"type": "string",
10-
"title": "The AI provider",
11-
"description": "The AI provider to use for chat and completion",
12-
"default": "None",
13-
"enum": ["None", "Anthropic", "ChromeAI", "MistralAI", "OpenAI"]
8+
"AIprovider": {
9+
"type": "object",
10+
"title": "AI provider",
11+
"description": "The AI provider configuration",
12+
"default": {},
13+
"additionalProperties": true
1414
}
1515
},
16-
"additionalProperties": true
16+
"additionalProperties": false
1717
}

scripts/settings-generator.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ const path = require('path');
44

55
console.log('Building settings schema\n');
66

7-
const outputDir = 'src/_provider-settings';
7+
const schemasDir = 'src/settings/schemas';
8+
const outputDir = path.join(schemasDir, '/_generated');
89
if (!fs.existsSync(outputDir)) {
910
fs.mkdirSync(outputDir);
1011
}
@@ -31,20 +32,20 @@ const schemaBase = tsj
3132
* to exclude them at the moment, to be able to build other settings.
3233
*/
3334
const providers = {
34-
chromeAI: {
35+
ChromeAI: {
3536
path: 'node_modules/@langchain/community/experimental/llms/chrome_ai.d.ts',
3637
type: 'ChromeAIInputs'
3738
},
38-
mistralAI: {
39+
MistralAI: {
3940
path: 'node_modules/@langchain/mistralai/dist/chat_models.d.ts',
4041
type: 'ChatMistralAIInput'
4142
},
42-
anthropic: {
43+
Anthropic: {
4344
path: 'node_modules/@langchain/anthropic/dist/chat_models.d.ts',
4445
type: 'AnthropicInput',
4546
excludedProps: ['clientOptions']
4647
},
47-
openAI: {
48+
OpenAI: {
4849
path: 'node_modules/@langchain/openai/dist/chat_models.d.ts',
4950
type: 'ChatOpenAIFields',
5051
excludedProps: ['configuration']
@@ -138,5 +139,29 @@ Object.entries(providers).forEach(([name, desc], index) => {
138139
});
139140
});
140141

142+
// Build the index.ts file
143+
const indexContent = [];
144+
Object.keys(providers).forEach(name => {
145+
indexContent.push(`import ${name} from './_generated/${name}.json';`);
146+
});
147+
148+
indexContent.push('', 'const ProviderSettings: { [name: string]: any } = {');
149+
150+
Object.keys(providers).forEach((name, index) => {
151+
indexContent.push(
152+
` ${name}` + (index < Object.keys(providers).length - 1 ? ',' : '')
153+
);
154+
});
155+
indexContent.push('};', '', 'export default ProviderSettings;', '');
156+
fs.writeFile(
157+
path.join(schemasDir, 'index.ts'),
158+
indexContent.join('\n'),
159+
err => {
160+
if (err) {
161+
throw err;
162+
}
163+
}
164+
);
165+
141166
console.log('Settings schema built\n');
142167
console.log('=====================\n');

src/index.ts

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import { ICompletionProviderManager } from '@jupyterlab/completer';
1616
import { INotebookTracker } from '@jupyterlab/notebook';
1717
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
1818
import { ISettingRegistry } from '@jupyterlab/settingregistry';
19+
import { IFormRendererRegistry } from '@jupyterlab/ui-components';
20+
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
1921

2022
import { ChatHandler } from './chat-handler';
21-
import { getSettings } from './llm-models';
23+
import { CompletionProvider } from './completion-provider';
2224
import { AIProvider } from './provider';
25+
import { aiSettingsRenderer } from './settings/panel';
2326
import { renderSlashCommandOption } from './slash-commands';
2427
import { IAIProvider } from './token';
25-
import { CompletionProvider } from './completion-provider';
2628

2729
const autocompletionRegistryPlugin: JupyterFrontEndPlugin<IAutocompletionRegistry> =
2830
{
@@ -150,43 +152,33 @@ const completerPlugin: JupyterFrontEndPlugin<void> = {
150152
const aiProviderPlugin: JupyterFrontEndPlugin<IAIProvider> = {
151153
id: '@jupyterlite/ai:ai-provider',
152154
autoStart: true,
153-
requires: [ISettingRegistry],
155+
requires: [IFormRendererRegistry, ISettingRegistry],
156+
optional: [IRenderMimeRegistry],
154157
provides: IAIProvider,
155158
activate: (
156159
app: JupyterFrontEnd,
157-
settingRegistry: ISettingRegistry
160+
editorRegistry: IFormRendererRegistry,
161+
settingRegistry: ISettingRegistry,
162+
rmRegistry?: IRenderMimeRegistry
158163
): IAIProvider => {
159164
const aiProvider = new AIProvider();
160165

161-
let currentProvider = 'None';
166+
editorRegistry.addRenderer(
167+
'@jupyterlite/ai:ai-provider.AIprovider',
168+
aiSettingsRenderer({ rmRegistry })
169+
);
162170
settingRegistry
163171
.load(aiProviderPlugin.id)
164172
.then(settings => {
165173
const updateProvider = () => {
166-
const provider = settings.get('provider').composite as string;
167-
if (provider !== currentProvider) {
168-
// Update the settings panel.
169-
currentProvider = provider;
170-
const settingsProperties = settings.schema.properties;
171-
if (settingsProperties) {
172-
const schemaKeys = Object.keys(settingsProperties);
173-
schemaKeys.forEach(key => {
174-
if (key !== 'provider') {
175-
delete settings.schema.properties?.[key];
176-
}
177-
});
178-
const properties = getSettings(provider);
179-
if (properties === null) {
180-
return;
181-
}
182-
Object.entries(properties).forEach(([name, value], index) => {
183-
settingsProperties[name] = value as ISettingRegistry.IProperty;
184-
});
185-
}
186-
}
187-
188174
// Update the settings to the AI providers.
189-
aiProvider.setProvider(provider, settings.composite);
175+
const providerSettings = (settings.get('AIprovider').composite ?? {
176+
provider: 'None'
177+
}) as ReadonlyPartialJSONObject;
178+
aiProvider.setProvider(
179+
providerSettings.provider as string,
180+
providerSettings
181+
);
190182
};
191183

192184
settings.changed.connect(() => updateProvider());

src/llm-models/utils.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
1111
import { ChromeCompleter } from './chrome-completer';
1212
import { OpenAICompleter } from './openai-completer';
1313

14-
import chromeAI from '../_provider-settings/chromeAI.json';
15-
import mistralAI from '../_provider-settings/mistralAI.json';
16-
import anthropic from '../_provider-settings/anthropic.json';
17-
import openAI from '../_provider-settings/openAI.json';
18-
1914
/**
2015
* Get an LLM completer from the name.
2116
*/
@@ -71,20 +66,3 @@ export function getErrorMessage(name: string, error: any): string {
7166
}
7267
return 'Unknown provider';
7368
}
74-
75-
/*
76-
* Get an LLM completer from the name.
77-
*/
78-
export function getSettings(name: string): any {
79-
if (name === 'MistralAI') {
80-
return mistralAI.properties;
81-
} else if (name === 'Anthropic') {
82-
return anthropic.properties;
83-
} else if (name === 'ChromeAI') {
84-
return chromeAI.properties;
85-
} else if (name === 'OpenAI') {
86-
return openAI.properties;
87-
}
88-
89-
return null;
90-
}

src/settings/instructions.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export interface IDict<T = any> {
2+
[key: string]: T;
3+
}
4+
5+
const chromeAiInstructions = `
6+
<i class="fas fa-exclamation-triangle"></i> Support for ChromeAI is still experimental and only available in Google Chrome.
7+
8+
You can test ChromeAI is enabled in your browser by going to the following URL: https://chromeai.org/
9+
10+
Enable the proper flags in Google Chrome.
11+
12+
- chrome://flags/#prompt-api-for-gemini-nano
13+
- Select: \`Enabled\`
14+
- chrome://flags/#optimization-guide-on-device-model
15+
- Select: \`Enabled BypassPrefRequirement\`
16+
- chrome://components
17+
- Click \`Check for Update\` on Optimization Guide On Device Model to download the model
18+
- [Optional] chrome://flags/#text-safety-classifier
19+
20+
<img src="https://github.com/user-attachments/assets/d48f46cc-52ee-4ce5-9eaf-c763cdbee04c" alt="A screenshot showing how to enable the ChromeAI flag in Google Chrome" width="500px">
21+
22+
Then restart Chrome for these changes to take effect.
23+
24+
<i class="fas fa-exclamation-triangle"></i> On first use, Chrome will download the on-device model, which can be as large as 22GB (according to their docs and at the time of writing).
25+
During the download, ChromeAI may not be available via the extension.
26+
27+
<i class="fa fa-info-circle" aria-hidden="true"></i> For more information about Chrome Built-in AI: https://developer.chrome.com/docs/ai/get-started
28+
`;
29+
30+
const mistralAIInstructions = `
31+
<i class="fas fa-exclamation-triangle"></i> This extension is still very much experimental. It is not an official MistralAI extension.
32+
33+
1. Go to https://console.mistral.ai/api-keys/ and create an API key.
34+
35+
<img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/1-api-key.png" alt="Screenshot showing how to create an API key" width="500px">
36+
37+
2. Open the JupyterLab settings and go to the **Ai providers** section to select the \`MistralAI\`
38+
provider and the API key (required).
39+
40+
<img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/2-jupyterlab-settings.png" alt="Screenshot showing how to add the API key to the settings" width="500px">
41+
42+
3. Open the chat, or use the inline completer
43+
44+
<img src="https://raw.githubusercontent.com/jupyterlite/ai/refs/heads/main/img/3-usage.png" alt="Screenshot showing how to use the chat" width="500px">
45+
`;
46+
47+
export const instructions: IDict = {
48+
ChromeAI: chromeAiInstructions,
49+
MistralAI: mistralAIInstructions
50+
};

0 commit comments

Comments
 (0)