diff --git a/src/components/Options.tsx b/src/components/Options.tsx index 1b7ba33..0b4dc31 100644 --- a/src/components/Options.tsx +++ b/src/components/Options.tsx @@ -1,14 +1,18 @@ import { render, Fragment } from 'preact'; import { useEffect, useState } from 'preact/hooks' import 'purecss/build/pure-min.css'; -import { LOCAL_STORAGE_KEY_OPTIONS, MODELS } from '../constants'; +import { LOCAL_STORAGE_KEY_OPTIONS, MODELS, DEFAULT_MODEL } from '../constants'; import { Options } from '../types'; import { getOptions } from '../utils/helper'; import { IconSelect } from './IconSelect'; +import OpenAI from 'openai'; const OptionsForm = () => { const [state, setState] = useState({}); const [message, setMessage] = useState(); + const [newModel, setNewModel] = useState(''); + const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle'); + const [testMessage, setTestMessage] = useState(''); useEffect(() => { getOptions().then((options) => { @@ -39,11 +43,88 @@ const OptionsForm = () => { window.close(); } + const onAddCustomModel = () => { + if (!newModel.trim()) return; + const customModels = state.customModels ?? []; + if (customModels.includes(newModel) || MODELS.includes(newModel)) { + alert('Model already exists'); + return; + } + customModels.push(newModel.trim()); + onOptionsChange({ ...state, customModels }); + setNewModel(''); + }; + + const onDeleteCustomModel = (modelToDelete: string) => { + const customModels = state.customModels?.filter(m => m !== modelToDelete) ?? []; + // If the deleted model was selected, switch to default + const newState = { ...state, customModels }; + if (state.model === modelToDelete) { + newState.model = MODELS[0]; + } + onOptionsChange(newState); + }; + const onOptionsChange = (options: Options) => { setMessage(''); setState(options); } + const onTestConnection = async () => { + if (!state.apiKey) { + setTestStatus('error'); + setTestMessage('Please enter an API key first.'); + return; + } + + setTestStatus('testing'); + setTestMessage('Testing connection...'); + + try { + const openai = new OpenAI({ + apiKey: state.apiKey, + baseURL: state.apiBaseUrl || undefined, + dangerouslyAllowBrowser: true, + }); + + const modelToTest = state.model || DEFAULT_MODEL; + + const response = await openai.chat.completions.create({ + messages: [ + { + role: 'user', + content: 'Hello, this is a test. Please respond with "OK".', + }, + ], + model: modelToTest, + max_tokens: 10, + }); + + if (response.choices && response.choices.length > 0) { + setTestStatus('success'); + setTestMessage(`✓ Connection successful! Model "${modelToTest}" is working.`); + } else { + setTestStatus('error'); + setTestMessage('Connection failed: No response received.'); + } + } catch (error: any) { + setTestStatus('error'); + let errorMessage = 'Connection failed: '; + if (error.message) { + errorMessage += error.message; + } else if (error.status === 401) { + errorMessage += 'Invalid API key.'; + } else if (error.status === 404) { + errorMessage += 'Model not found. Please check the model name.'; + } else if (error.status === 429) { + errorMessage += 'Rate limit exceeded. Please try again later.'; + } else { + errorMessage += String(error); + } + setTestMessage(errorMessage); + } + }; + const version = chrome.runtime.getManifest().version; return ( @@ -79,10 +160,58 @@ const OptionsForm = () => { Select the model you want to use. + +
+ +
+ {state.customModels?.map((model) => ( +
+ {model} + +
+ ))} +
+ setNewModel(e.currentTarget.value)} + style="flex-grow: 1; margin-right: 5px;" + /> + +
+
+ Add or remove custom models. +
+ +
+ + + {testMessage && ( + + {testMessage} + + )} +

Suggestion

This section customizes how suggestions work. Suggestions are triggered when you type a space at the end of a diff --git a/src/iso/contentScript.ts b/src/iso/contentScript.ts index a8f3caf..042baf2 100644 --- a/src/iso/contentScript.ts +++ b/src/iso/contentScript.ts @@ -20,6 +20,7 @@ async function onEditorUpdate( suggestionAbortController?.abort(); if (options == undefined || options.suggestionDisabled) return; + suggestionAbortController = new AbortController(); const existing = Suggestion.getCurrent(); diff --git a/src/types.ts b/src/types.ts index 35792f4..44dedd3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export interface Options { apiKey?: string; apiBaseUrl?: string; model?: string; + customModels?: string[]; suggestionMaxOutputToken?: number; suggestionPrompt?: string;