diff --git a/src/LocalAIModelSelection.ts b/src/LocalAIModelSelection.ts new file mode 100644 index 0000000..13d54f8 --- /dev/null +++ b/src/LocalAIModelSelection.ts @@ -0,0 +1,108 @@ +import React, { useState, useEffect } from "react" + +interface LocalAIModelSelectionProps { + settings: { + LocalAIBasePath?: string + LocalAIApiKey?: string + LocalAISelectedModel?: string + } + onUpdateSettings: (updatedSettings: Partial) => void +} + +const LocalAIModelSelection: React.FC = ({ settings, onUpdateSettings }) => { + const [customModels, setCustomModels] = useState>([]) + const [loading, setLoading] = useState(false) + const [basePath, setBasePath] = useState(settings?.LocalAIBasePath || "") + const [apiKey, setApiKey] = useState(settings?.LocalAIApiKey || "") + + useEffect(() => { + async function fetchModels() { + if (!basePath || !basePath.includes("/v1")) { + setCustomModels([]) + setLoading(false) + return + } + + try { + setLoading(true) + const response = await fetch(`${basePath}/v1/models`, { + headers: { + "Content-Type": "application/json", + Authorization: apiKey ? `Bearer ${apiKey}` : undefined, + }, + }) + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`) + } + const data = await response.json() + setCustomModels(data.models || []) + } catch (error) { + console.error("Failed to fetch models from LocalAI:", error) + setCustomModels([]) + } finally { + setLoading(false) + } + } + + fetchModels() + }, [basePath, apiKey]) + + return ( +
+ {/* Base Path Input */} + + { + const value = e.target.value + setBasePath(value) + onUpdateSettings({ LocalAIBasePath: value }) + }} + placeholder="e.g., http://localhost:8080/v1" + className="border border-gray-300 rounded-lg p-2 mb-4 w-full" + /> + + {/* API Key Input */} + + { + const value = e.target.value + setApiKey(value) + onUpdateSettings({ LocalAIApiKey: value }) + }} + placeholder="Enter API Key if required" + className="border border-gray-300 rounded-lg p-2 mb-4 w-full" + /> + + {/* Models Dropdown */} + + {loading ? ( +
Loading models...
+ ) : customModels.length === 0 ? ( +
No models found.
+ ) : ( + + )} +
+ ) +} + +export default LocalAIModelSelection diff --git a/src/settings/preferences.ts b/src/settings/preferences.ts index 53c0f3f..1702bf0 100644 --- a/src/settings/preferences.ts +++ b/src/settings/preferences.ts @@ -1,7 +1,28 @@ import { config, homepage } from "../../package.json" import { getString } from "../utils/locale" +import React from "react" +import ReactDOM from "react-dom" +import LocalAIModelSelection from "../LocalAIModelSelection" -export function registerPrefs() { +interface AddonPreferences { + window: Window | null + llmProvider: "openai" | "localai" + localAISettings: { + LocalAIBasePath?: string + LocalAIApiKey?: string + LocalAISelectedModel?: string + } +} + +// Declare global addon object +declare const addon: { + data: { + prefs: AddonPreferences + } +} + +// Register the preferences pane with Zotero +export function registerPrefs(): void { Zotero.PreferencePanes.register({ pluginID: config.addonID, src: rootURI + "chrome/content/preferences.xhtml", @@ -11,189 +32,122 @@ export function registerPrefs() { }) } -export function registerPrefsScripts(_window: Window) { - // This function is called when the prefs window is opened - // See addon/chrome/content/preferences.xul onpaneload +// Initialize the preferences pane when opened +export function registerPrefsScripts(_window: Window): void { if (!addon.data.prefs) { addon.data.prefs = { window: _window, + llmProvider: "openai", // Default to OpenAI + localAISettings: { + LocalAIBasePath: "", + LocalAIApiKey: "", + LocalAISelectedModel: "", + }, } } else { addon.data.prefs.window = _window } + updatePrefsUI() bindPrefEvents() } -async function updatePrefsUI() { - // You can initialize some UI elements on prefs window - // with addon.data.prefs.window.document - // Or bind some events to the elements - const renderLock = ztoolkit.getGlobal("Zotero").Promise.defer() - // const tableHelper = new ztoolkit.VirtualizedTable(addon.data.prefs?.window!) - // .setContainerId(`${config.addonRef}-table-container`) - // .setProp({ - // id: `${config.addonRef}-prefs-table`, - // // Do not use setLocale, as it modifies the Zotero.Intl.strings - // // Set locales directly to columns - // columns: addon.data.prefs?.columns.map(column => - // Object.assign(column, { - // label: getString(column.label) || column.label, - // }) - // ), - // showHeader: true, - // multiSelect: true, - // staticColumns: true, - // disableFontSizeScaling: true, - // }) - // .setProp('getRowCount', () => addon.data.prefs?.rows.length || 0) - // .setProp( - // 'getRowData', - // index => - // addon.data.prefs?.rows[index] || { - // title: 'no data', - // detail: 'no data', - // } - // ) - // // Show a progress window when selection changes - // .setProp('onSelectionChange', selection => { - // new ztoolkit.ProgressWindow(config.addonName) - // .createLine({ - // text: `Selected line: ${addon.data.prefs?.rows - // .filter((v, i) => selection.isSelected(i)) - // .map(row => row.title) - // .join(',')}`, - // progress: 100, - // }) - // .show() - // }) - // // When pressing delete, delete selected line and refresh table. - // // Returning false to prevent default event. - // .setProp('onKeyDown', (event: KeyboardEvent) => { - // if (event.key == 'Delete' || (Zotero.isMac && event.key == 'Backspace')) { - // addon.data.prefs!.rows = - // addon.data.prefs?.rows.filter((v, i) => !tableHelper.treeInstance.selection.isSelected(i)) || [] - // tableHelper.render() - // return false - // } - // return true - // }) - // // For find-as-you-type - // .setProp('getRowString', index => addon.data.prefs?.rows[index].title || '') - // // Render the table. - // .render(-1, () => { - // renderLock.resolve() - // }) - await renderLock.promise - ztoolkit.log("Preference table rendered!") +// Render the preferences pane UI +async function updatePrefsUI(): Promise { + const prefsWindow = addon.data.prefs.window + if (!prefsWindow) { + console.error("Preferences window is not available.") + return + } + + // Get the container element for the preferences pane + const container = prefsWindow.document.querySelector( + `#zotero-prefpane-${config.addonRef}-container` + ) + if (!container) { + console.error("Preferences container not found!") + return + } + + // Render the React UI + ReactDOM.render( +
+ {/* LLM Provider Selection */} +

{getString("prefs-llm-provider-title")}

+ + + + {/* LocalAI Settings */} +
+ { + addon.data.prefs.localAISettings = { + ...addon.data.prefs.localAISettings, + ...updatedSettings, + } + }} + /> +
+
, + container + ) } -function bindPrefEvents() { - // addon.data - // .prefs!.window.document.querySelector(`#zotero-prefpane-${config.addonRef}-enable`) - // ?.addEventListener('command', e => { - // ztoolkit.log(e) - // addon.data.prefs!.window.alert(`Successfully changed to ${(e.target as XUL.Checkbox).checked}!`) - // }) - // addon.data - // .prefs!!.window.document.querySelector(`#zotero-prefpane-${config.addonRef}-input`) - // ?.addEventListener('change', e => { - // ztoolkit.log(e) - // addon.data.prefs!.window.alert(`Successfully changed to ${(e.target as HTMLInputElement).value}!`) - // }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-OPENAI_MODEL-0`, - ) - ?.addEventListener("command", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new OPENAI Model to take effect.`, - ) - }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-OPENAI_MODEL-1`, - ) - ?.addEventListener("command", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new OPENAI Model to take effect.`, - ) - }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-OPENAI_MODEL-2`, - ) - ?.addEventListener("command", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new OPENAI Model to take effect.`, - ) - }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-OPENAI_MODEL-3`, - ) - ?.addEventListener("command", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new OPENAI Model to take effect.`, - ) - }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-OPENAI_BASE_URL`, - ) - ?.addEventListener("change", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new OPENAI Base URL to take effect.`, - ) - }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-SHORTCUT_MODIFIER-shift`, - ) - ?.addEventListener("command", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new shortcut combo to take effect.`, - ) - }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-SHORTCUT_MODIFIER-ctrl-shift`, - ) - ?.addEventListener("command", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new shortcut combo to take effect.`, - ) - }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-SHORTCUT_MODIFIER-alt-shift`, - ) - ?.addEventListener("command", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new shortcut combo to take effect.`, - ) - }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-SHORTCUT_KEY`, - ) - ?.addEventListener("change", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new shortcut combo to take effect.`, +// Toggle the visibility of the LocalAI-specific settings +function updateLocalAIVisibility(isLocalAI: boolean): void { + const prefsWindow = addon.data.prefs.window + if (!prefsWindow) { + console.error("Preferences window is not available.") + return + } + + const localAISettingsDiv = prefsWindow.document.querySelector("#localai-settings") + if (localAISettingsDiv) { + localAISettingsDiv.style.display = isLocalAI ? "block" : "none" + } +} + +// Bind events for non-React elements +function bindPrefEvents(): void { + const prefsWindow = addon.data.prefs.window + if (!prefsWindow) { + console.error("Preferences window is not available.") + return + } + + // Example event binding for OpenAI settings + prefsWindow.document + .querySelector(`#zotero-prefpane-${config.addonRef}-OPENAI_API_KEY`) + ?.addEventListener("change", (e: Event) => { + const target = e.target as HTMLInputElement + addon.data.prefs.window?.alert( + `Please restart Zotero for your new OpenAI API Key (${target.value}) to take effect.` ) }) - // addon.data - // .prefs!!.window.document.querySelector(`#zotero-prefpane-${config.addonRef}-OPENAI_MODEL-2`) - // ?.addEventListener('command', e => { - // addon.data.prefs!.window.alert(`Please restart Zotero for your new OPENAI Model to take effect.`) - // }) - addon.data - .prefs!!.window.document.querySelector( - `#zotero-prefpane-${config.addonRef}-OPENAI_API_KEY`, - ) - ?.addEventListener("change", (e) => { - addon.data.prefs!.window.alert( - `Please restart Zotero for your new OPENAI API Key to take effect.`, + + // Example event binding for LocalAI settings + prefsWindow.document + .querySelector(`#zotero-prefpane-${config.addonRef}-LocalAI_BASE_URL`) + ?.addEventListener("change", (e: Event) => { + const target = e.target as HTMLInputElement + addon.data.prefs.window?.alert( + `Please restart Zotero for your new LocalAI Base URL (${target.value}) to take effect.` ) }) }