From 0902e784db6c3da822b972d59ae56f4264b955b2 Mon Sep 17 00:00:00 2001 From: Mars Liu Date: Wed, 28 Aug 2024 23:32:49 +0800 Subject: [PATCH] add settings in jupyterlab system --- README.md | 4 + package.json | 4 +- schema/jupyter-litchi.json | 34 +++++++++ schema/plugin.json | 9 ++- setup.py | 3 +- src/index.ts | 145 +++++++++++++++++++++++------------ src/ollama.ts | 15 ++-- src/settings.tsx | 152 +++++++++++++++++++++++++++++++++++++ src/toolbar.tsx | 6 +- 9 files changed, 305 insertions(+), 67 deletions(-) create mode 100644 schema/jupyter-litchi.json create mode 100644 src/settings.tsx diff --git a/README.md b/README.md index f9c33db..77fa239 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,10 @@ See [RELEASE](RELEASE.md) * installer fixed +### 0.1.4 + +* add settings + ## About Me My name is Liu Xin, and my English name is Mars Liu and previously used March Liu. I translated the Python diff --git a/package.json b/package.json index 0775c08..f2b415f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jupyter-litchi", - "version": "0.1.3", + "version": "0.1.4", "description": "litchi is a ai client for jupyter lab", "keywords": [ "jupyter", @@ -61,7 +61,7 @@ "dependencies": { "@jupyterlab/application": "^4.2.4", "@jupyterlab/apputils": "^4.3.4", - "@jupyterlab/settingregistry": "^4.0.0", + "@jupyterlab/settingregistry": "^4.2.5", "@lumino/widgets": "^2.5.0" }, "devDependencies": { diff --git a/schema/jupyter-litchi.json b/schema/jupyter-litchi.json new file mode 100644 index 0000000..f4c22da --- /dev/null +++ b/schema/jupyter-litchi.json @@ -0,0 +1,34 @@ +{ + "title": "Litchi", + "description": "AI Plugin for JupyterLab.", + "jupyter.lab.menus": { + "main": [ + { + "id": "litchi-settings", + "label": "Litchi", + "rank": 80, + "items": [ + { + "command": "jupyter-litchi:toggle-flag" + } + ] + } + ] + }, + "properties": { + "ollama:host": { + "type": "string", + "title": "Ollama Host:", + "description": "Host or IP address of ollama api.", + "default": "localhost" + }, + "ollama:port": { + "type": "integer", + "title": "Ollama Port:", + "description": "Port of ollama api.", + "default": 11434 + } + }, + "additionalProperties": false, + "type": "object" +} diff --git a/schema/plugin.json b/schema/plugin.json index ae7eb52..02a5f5f 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -1,8 +1,9 @@ { "jupyter.lab.shortcuts": [], - "title": "jupyter-litchi", - "description": "litchi settings.", + "title": "Jupyter Litchi", + "description": "AI Plugin for JupyterLab.", "type": "object", - "properties": {}, - "additionalProperties": false + "properties": { + }, + "additionalProperties": true } diff --git a/setup.py b/setup.py index c6428d2..50e1516 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ long_description = (this_directory / "README.md").read_text() __import__("setuptools").setup(name="jupyter-litchi", - version="0.1.3", + version="0.1.4", description="Litchi is a Jupyterlab extension for AI Client", long_description=long_description, long_description_content_type='text/markdown', @@ -17,7 +17,6 @@ keywords= ["jupyter", "jupyterlab", "jupyterlab-extension", "ai", "ollama"], packages=find_packages(), package_data={ - # 将特定于包的资源文件包含进来 'labextension': ['litchi/labextension/*'], }, ) diff --git a/src/index.ts b/src/index.ts index 65c39a4..3792ac7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,16 +4,21 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { ICommandPalette } from '@jupyterlab/apputils'; +import { ICommandPalette, MainAreaWidget } from '@jupyterlab/apputils'; import { IStateDB } from '@jupyterlab/statedb'; import { chat, IMessage, Message } from './ollama'; -import { MarkdownCellModel } from '@jupyterlab/cells' -import { INotebookTracker } from "@jupyterlab/notebook"; +import { MarkdownCellModel } from '@jupyterlab/cells'; +import { INotebookTracker } from '@jupyterlab/notebook'; +import { SettingWidget } from './settings'; +import { ISettingRegistry} from '@jupyterlab/settingregistry'; +const LITCHI_ID = 'jupyter-litchi:jupyter-litchi'; const ACTIVATE_COMMAND_ID = 'litchi:chat'; +const SETTINGS_COMMAND_ID = 'litchi:settings'; namespace CommandIDs { export const CHAT = ACTIVATE_COMMAND_ID; + export const SETTINGS = SETTINGS_COMMAND_ID; } const LITCHI_SESSION = 'litchi:session'; const LITCHI_LATEST = 'litchi:latest'; @@ -22,76 +27,61 @@ const LITCHI_LATEST = 'litchi:latest'; * The plugin registration information. */ const plugin: JupyterFrontEndPlugin = { - id: 'jupyter-litchi', + id: LITCHI_ID, description: 'Add a widget to the notebook header.', autoStart: true, activate: activate, - requires: [ICommandPalette, IStateDB, INotebookTracker] + requires: [ICommandPalette, IStateDB, INotebookTracker, ISettingRegistry], }; -export function activate( +export async function activate( app: JupyterFrontEnd, palette: ICommandPalette, state: IStateDB, tracker: INotebookTracker, + registry: ISettingRegistry ) { const widget = new WidgetExtension(app, state); widget.addClass('jp-litchi-toolbar'); - app.docRegistry.addWidgetExtension('Notebook', widget); - // const tracker = new NotebookTracker({ namespace: 'jupyter-litchi' }); - console.log('add command litchi:chat'); app.commands.addCommand(CommandIDs.CHAT, { label: 'Litchi Chat', execute: async () => { - const session: IMessage[] = await fetchState(state, LITCHI_SESSION, [ - Message.startUp() - ]); - const cell = tracker.activeCell; - if (cell === null) { - console.error('litchi:chat exit because any cell not been selected'); - return; - } + await chatActivate(app, registry, tracker, state); + } + }); + // Add the command to the palette. + palette.addItem({ command: CommandIDs.CHAT, category: 'jupyter-Litchi' }); - const notebook = tracker.currentWidget?.content; - if (notebook === undefined) { - console.error('litchi:chat exit because the notebook not found'); - return; - } + const settingsCreator = () => { + const content = new SettingWidget(LITCHI_ID, app, registry); + const widget = new MainAreaWidget({ content }); + widget.id = 'litchi-settings'; + widget.title.label = 'Litchi Settings'; + widget.title.closable = true; + return widget; + }; - const content = cell.model.sharedModel.source; - // eslint-disable-next-line eqeqeq - if (content === null) { - console.error('litchi:chat exit because the content of cell is null'); - return; + let settingsWidget = settingsCreator(); + app.commands.addCommand(CommandIDs.SETTINGS, { + label: 'Litchi Settings', + execute: () => { + // Regenerate the widget if disposed + if (settingsWidget.isDisposed) { + settingsWidget = settingsCreator(); } - const model = (await state.fetch('litchi:model'))?.toString(); - if (model === null || model === undefined) { - console.error('litchi:chat exit because not any model selected'); - return; + if (!settingsWidget.isAttached) { + // Attach the widget to the main work area if it's not there + app.shell.add(settingsWidget, 'settings editor'); } - - const latest = new Message('user', content); - await state.save(LITCHI_LATEST, JSON.stringify(latest)); - - const message = await chat(session, latest, model!); - console.log(`received message ${JSON.stringify(message)}`); - await state.save(LITCHI_SESSION, JSON.stringify([...session, message])); - const cellModel = new MarkdownCellModel(); - cellModel.sharedModel.setSource(message.content); - - const { commands } = app; - commands.execute('notebook:insert-cell-below').then(() => { - commands.execute('notebook:change-cell-to-markdown'); - }); - - const newCell = notebook.activeCell!; - newCell.model.sharedModel.setSource(message.content); + // Activate the widget + app.shell.activateById(settingsWidget.id); } }); + // Add the command to the palette. - palette.addItem({ command: CommandIDs.CHAT, category: 'jupyter-Litchi' }); + // palette.addItem({ command: CommandIDs.SETTINGS, category: 'jupyter-Litchi' }); } async function fetchState( @@ -109,6 +99,63 @@ async function fetchState( } } +async function chatActivate( + app: JupyterFrontEnd, + registry: ISettingRegistry, + tracker: INotebookTracker, + state: IStateDB +) { + const session: IMessage[] = await fetchState(state, LITCHI_SESSION, [ + Message.startUp() + ]); + const cell = tracker.activeCell; + if (cell === null) { + console.error('litchi:chat exit because any cell not been selected'); + return; + } + + const notebook = tracker.currentWidget?.content; + if (notebook === undefined) { + console.error('litchi:chat exit because the notebook not found'); + return; + } + + const content = cell.model.sharedModel.source; + // eslint-disable-next-line eqeqeq + if (content === null) { + console.error('litchi:chat exit because the content of cell is null'); + return; + } + const model = (await state.fetch('litchi:model'))?.toString(); + if (model === null || model === undefined) { + console.error('litchi:chat exit because not any model selected'); + return; + } + + const settings = await registry.load(LITCHI_ID); + const host = settings.get('ollama:host')!.composite!.toString(); + const port = Number.parseInt( + settings.get('ollama:port')!.composite!.toString() + ); + + const latest = new Message('user', content); + await state.save(LITCHI_LATEST, JSON.stringify(latest)); + + const message = await chat(host, port, session, latest, model!); + console.log(`received message ${JSON.stringify(message)}`); + await state.save(LITCHI_SESSION, JSON.stringify([...session, message])); + const cellModel = new MarkdownCellModel(); + cellModel.sharedModel.setSource(message.content); + + const { commands } = app; + commands.execute('notebook:insert-cell-below').then(() => { + commands.execute('notebook:change-cell-to-markdown'); + }); + + const newCell = notebook.activeCell!; + newCell.model.sharedModel.setSource(message.content); +} + /** * Export the plugin as default. */ diff --git a/src/ollama.ts b/src/ollama.ts index d2d3873..67ae06a 100644 --- a/src/ollama.ts +++ b/src/ollama.ts @@ -1,5 +1,6 @@ // SendRequestComponent.tsx + export interface IMessage { role: string; content: string; @@ -15,7 +16,7 @@ export class Message implements IMessage { } static startUp(): Message { - return new Message("system", "your are a python and jupyter export"); + return new Message('system', 'your are a python and jupyter export'); } } @@ -46,6 +47,8 @@ interface IChatResponse { } export async function chat( + host: string, + port: number, session: Message[], message: Message, model: string @@ -53,18 +56,18 @@ export async function chat( try { const messages = [...session, message]; const request = new ChatRequest(model, messages); - const resp = await fetch("http://localhost:11434/api/chat", { - method: "POST", + + const resp = await fetch(`http://${host}:${port}/api/chat`, { + method: 'POST', headers: { - "content-type": "application/json" + 'content-type': 'application/json' }, body: JSON.stringify(request) }); const data = (await resp.json()) as IChatResponse; - console.log(JSON.stringify(data)); return data.message; } catch (error) { - console.error("Error sending request to server:", error); + console.error('Error sending request to server:', error); throw error; } } diff --git a/src/settings.tsx b/src/settings.tsx new file mode 100644 index 0000000..7fc42ba --- /dev/null +++ b/src/settings.tsx @@ -0,0 +1,152 @@ +import { ReactWidget } from '@jupyterlab/ui-components'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import {} from 'json5' +import * as React from 'react'; +import { JupyterFrontEnd } from "@jupyterlab/application"; + +function FormComponent(props: { appId: string; app: JupyterFrontEnd, registry: ISettingRegistry }) { + // const host = 'localhost'; + // const port = 11434; + const [host, setHost] = React.useState('localhost'); + const [port, setPort] = React.useState(11434); + const [key, setKey] = React.useState(''); + + // 定义一个事件处理函数,用于在输入值改变时更新状态 + const handleHostChange = async ( + event: React.ChangeEvent + ) => { + const h = event.target.value; + console.log(`set host:${h}`); + setHost(h); // 使用事件对象更新状态 + await props.registry.set(props.appId, 'ollama:host', h); + }; + + const handlePortChange = async ( + event: React.ChangeEvent + ) => { + const p = Number.parseInt(event.target.value); + setPort(p); // 使用事件对象更新状态 + await props.registry.set(props.appId, 'ollama:port', p); + }; + + const handleKeyChange = async ( + event: React.ChangeEvent + ) => { + const k = event.target.value; + setKey(k); // 使用事件对象更新状态 + await props.registry.set(props.appId, 'ollama:key', k); + }; + + React.useEffect(() => { + const app = props.app; + const registry = props.registry; + async function loadSettings() { + await registry.connector + .save(props.appId, '{}') + .then(r => { + console.log(r); + }) + .catch(console.error); + if (!(props.appId in registry.plugins)) { + await app.serviceManager.settings + .save(props.appId, '{}') + .catch(console.error); + app.serviceManager.settings + .fetch(props.appId) + .then(plugin => { + console.log(`litchi in settings: ${JSON.stringify(plugin)}`); + }) + .catch(console.error); + + props.registry + .load(props.appId) + .then(s => { + console.log(`settings loaded: ${JSON.stringify(s)}`); + s.set('ollama:host', 'localhost') + .then(v => s.set('ollama:port', 11434)) + .then(v => s.set('ollama:key', '')); + }) + .catch(reason => { + console.error(reason); + }) + .then(v => { + console.log('litchi settings uploaded'); + }); + } + props.registry + .get(props.appId, 'ollama:host') + .then(h => setHost(h!.toString())) + .catch(reason => { + console.error(reason); + setHost('localhost'); + }); + props.registry + .get(props.appId, 'ollama:port') + .then(p => Number.parseInt(p!.toString())) + .then(n => setPort(n)) + .catch(reason => { + console.error(reason); + setPort(11434); + }); + props.registry + .get(props.appId, 'ollama:key') + .then(k => k!.toString()) + .then(k => setKey(k)) + .catch(reason => { + console.error(reason); + setKey(key); + }); + } + + loadSettings(); + }, []); + + return ( +
+ Ollama: +
+ + +
+ + +
+ + +
+
+ ); +} + +export class SettingWidget extends ReactWidget { + app: JupyterFrontEnd; + registry: ISettingRegistry; + appId: string; + + constructor(appId: string, app: JupyterFrontEnd, registry: ISettingRegistry) { + super(); + this.app = app; + this.appId = appId; + this.registry = registry; + } + + protected render() { + return ; + } +} diff --git a/src/toolbar.tsx b/src/toolbar.tsx index 37093e3..1628639 100644 --- a/src/toolbar.tsx +++ b/src/toolbar.tsx @@ -16,7 +16,6 @@ function ModelsComponent(props: { app: JupyterFrontEnd; state: IStateDB }) { // 使用useEffect来在组件加载时获取模型列表 React.useEffect(() => { async function loadModels() { - console.log('start load models'); try { const modelList = await listModels('localhost', 11434); setModels(modelList); @@ -34,12 +33,12 @@ function ModelsComponent(props: { app: JupyterFrontEnd; state: IStateDB }) { // 处理下拉列表选项变化的事件 const handleChange = async (event: React.ChangeEvent) => { setSelectedModel(event.target.value); - props.state.save('litchi:model', event.target.value); + await props.state.save('litchi:model', event.target.value); }; const handleChatClick = async (event: React.MouseEvent) => { const { commands } = props.app; - commands.execute('litchi:chat'); + await commands.execute('litchi:chat'); }; return ( @@ -69,7 +68,6 @@ export class WidgetExtension private readonly app: JupyterFrontEnd; protected render() { - console.log('rend new litchi widget'); return ; }