Skip to content

Commit

Permalink
Using the secrets manager
Browse files Browse the repository at this point in the history
  • Loading branch information
brichet committed Mar 10, 2025
1 parent 272381a commit 857ba01
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 6 deletions.
8 changes: 5 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { IFormRendererRegistry } from '@jupyterlab/ui-components';
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
import { ISecretsManager } from 'jupyter-secrets-manager';

import { ChatHandler } from './chat-handler';
import { CompletionProvider } from './completion-provider';
Expand Down Expand Up @@ -154,19 +155,20 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> = {
id: '@jupyterlite/ai:provider-registry',
autoStart: true,
requires: [IFormRendererRegistry, ISettingRegistry],
optional: [IRenderMimeRegistry],
optional: [IRenderMimeRegistry, ISecretsManager],
provides: IAIProviderRegistry,
activate: (
app: JupyterFrontEnd,
editorRegistry: IFormRendererRegistry,
settingRegistry: ISettingRegistry,
rmRegistry?: IRenderMimeRegistry
rmRegistry?: IRenderMimeRegistry,
secretsManager?: ISecretsManager
): IAIProviderRegistry => {
const providerRegistry = new AIProviderRegistry();

editorRegistry.addRenderer(
'@jupyterlite/ai:provider-registry.AIprovider',
aiSettingsRenderer({ providerRegistry, rmRegistry })
aiSettingsRenderer({ providerRegistry, rmRegistry, secretsManager })
);
settingRegistry
.load(providerRegistryPlugin.id)
Expand Down
70 changes: 67 additions & 3 deletions src/settings/panel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { FormComponent, IFormRenderer } from '@jupyterlab/ui-components';
import { ArrayExt } from '@lumino/algorithm';
import { JSONExt } from '@lumino/coreutils';
import { IChangeEvent } from '@rjsf/core';
import type { FieldProps } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
import { JSONSchema7 } from 'json-schema';
import { ISecretsManager } from 'jupyter-secrets-manager';
import React from 'react';

import baseSettings from './schemas/base.json';
import { IAIProviderRegistry, IDict } from '../tokens';

const SECRETS_NAMESPACE = '@jupyterlite/ai';
const MD_MIME_TYPE = 'text/markdown';
const STORAGE_NAME = '@jupyterlite/ai:settings';
const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';

export const aiSettingsRenderer = (options: {
providerRegistry: IAIProviderRegistry;
rmRegistry?: IRenderMimeRegistry;
secretsManager?: ISecretsManager;
}): IFormRenderer => {
return {
fieldRenderer: (props: FieldProps) => {
Expand Down Expand Up @@ -49,6 +53,7 @@ export class AiSettings extends React.Component<
}
this._providerRegistry = props.formContext.providerRegistry;
this._rmRegistry = props.formContext.rmRegistry ?? null;
this._secretsManager = props.formContext.secretsManager ?? null;
this._settings = props.formContext.settings;

// Initialize the providers schema.
Expand Down Expand Up @@ -97,6 +102,48 @@ export class AiSettings extends React.Component<
.catch(console.error);
}

async componentWillUpdate(): Promise<void> {
if (!this._secretsManager) {
return;
}
const inputs = this._formRef.current?.getElementsByTagName('input') || [];
for (let i = 0; i < inputs.length; i++) {
if (inputs[i].type.toLowerCase() === 'password') {
(await this._secretsManager.list(SECRETS_NAMESPACE)).forEach(id =>
this._secretsManager?.detach(SECRETS_NAMESPACE, id)
);
}
}
}

componentDidUpdate(): void {
if (!this._secretsManager) {
return;
}
// Attach the password inputs to the secrets manager only if they have changed.
const inputs = this._formRef.current?.getElementsByTagName('input') || [];
if (ArrayExt.shallowEqual(inputs, this._formInputs)) {
return;
}
this._formInputs = [...inputs];
this._unsavedFields = [];
for (let i = 0; i < inputs.length; i++) {
if (inputs[i].type.toLowerCase() === 'password') {
const label = inputs[i].getAttribute('label');
if (label) {
const id = `${this._provider}-${label}`;
this._secretsManager.attach(
SECRETS_NAMESPACE,
id,
inputs[i],
(value: string) => this._onPasswordUpdated(label, value)
);
this._unsavedFields.push(label);
}
}
}
}

/**
* Get the current provider from the local storage.
*/
Expand Down Expand Up @@ -126,8 +173,10 @@ export class AiSettings extends React.Component<
* Save settings in local storage for a given provider.
*/
saveSettings(value: IDict<any>) {
const currentSettings = { ...value };
const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) ?? '{}');
settings[this._provider] = value;
this._unsavedFields.forEach(field => delete currentSettings[field]);
settings[this._provider] = currentSettings;
localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
}

Expand Down Expand Up @@ -206,6 +255,17 @@ export class AiSettings extends React.Component<
.catch(console.error);
};

/**
* Callback function called when the password input has been programmatically updated
* with the secret manager.
*/
private _onPasswordUpdated = (fieldName: string, value: string) => {
this._currentSettings[fieldName] = value;
this._settings
.set('AIprovider', { provider: this._provider, ...this._currentSettings })
.catch(console.error);
};

/**
* Triggered when the form value has changed, to update the current settings and save
* it in local storage.
Expand All @@ -221,7 +281,7 @@ export class AiSettings extends React.Component<

render(): JSX.Element {
return (
<>
<div ref={this._formRef}>
<WrappedFormComponent
formData={{ provider: this._provider }}
schema={this._providerSchema}
Expand All @@ -243,15 +303,19 @@ export class AiSettings extends React.Component<
onChange={this._onFormChange}
uiSchema={this._uiSchema}
/>
</>
</div>
);
}

private _providerRegistry: IAIProviderRegistry;
private _provider: string;
private _providerSchema: JSONSchema7;
private _rmRegistry: IRenderMimeRegistry | null;
private _secretsManager: ISecretsManager | null;
private _currentSettings: IDict<any> = { provider: 'None' };
private _uiSchema: IDict<any> = {};
private _settings: ISettingRegistry.ISettings;
private _formRef = React.createRef<HTMLDivElement>();
private _unsavedFields: string[] = [];
private _formInputs: HTMLInputElement[] = [];
}

0 comments on commit 857ba01

Please sign in to comment.