diff --git a/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx b/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx index feb62f123a7..b3aba4899cf 100644 --- a/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx +++ b/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx @@ -1,7 +1,242 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; import System from "@/models/system"; +import GenericOpenAiConnections from "@/models/genericOpenAiConnections"; +import showToast from "@/utils/toast"; +import { LLM_PREFERENCE_CHANGED_EVENT } from "@/pages/GeneralSettings/LLMPreference"; + +const MANUAL_CONNECTION_VALUE = "__manual__"; export default function GenericOpenAiOptions({ settings }) { + const { t } = useTranslation(); + const [localSettings, setLocalSettings] = useState(settings); + const [connections, setConnections] = useState( + settings?.GenericOpenAiSavedConnections || [] + ); + const [selectedConnectionId, setSelectedConnectionId] = useState( + settings?.GenericOpenAiActiveConnectionId || MANUAL_CONNECTION_VALUE + ); + const [formKey, setFormKey] = useState(0); + const [loadingConnections, setLoadingConnections] = useState(false); + const [applyingConnection, setApplyingConnection] = useState(false); + const formRef = useRef(null); + + const refreshConnections = async () => { + setLoadingConnections(true); + const result = await GenericOpenAiConnections.list(); + if (result.success) { + setConnections(result.connections || []); + setSelectedConnectionId( + result.activeConnectionId || MANUAL_CONNECTION_VALUE + ); + } + setLoadingConnections(false); + return result; + }; + + useEffect(() => { + setLocalSettings(settings); + }, [settings]); + + useEffect(() => { + refreshConnections(); + }, []); + + const reloadSettings = async () => { + const updatedSettings = await System.keys(); + if (!updatedSettings) return null; + setLocalSettings(updatedSettings); + setConnections(updatedSettings.GenericOpenAiSavedConnections || []); + setSelectedConnectionId( + updatedSettings.GenericOpenAiActiveConnectionId || MANUAL_CONNECTION_VALUE + ); + setFormKey((key) => key + 1); + window.dispatchEvent(new Event(LLM_PREFERENCE_CHANGED_EVENT)); + return updatedSettings; + }; + + const readFormValues = () => { + const container = formRef.current; + if (!container) return null; + const getValue = (name) => + container.querySelector(`[name="${name}"]`)?.value ?? ""; + return { + basePath: String(getValue("GenericOpenAiBasePath")).trim(), + apiKey: String(getValue("GenericOpenAiKey")).trim(), + modelPref: String(getValue("GenericOpenAiModelPref")).trim(), + tokenLimit: Number(getValue("GenericOpenAiTokenLimit")), + maxTokens: Number(getValue("GenericOpenAiMaxTokens")), + }; + }; + + const handleConnectionSelect = async (event) => { + const value = event.target.value; + setSelectedConnectionId(value); + + if (value === MANUAL_CONNECTION_VALUE) return; + + setApplyingConnection(true); + const result = await GenericOpenAiConnections.activate(value); + setApplyingConnection(false); + + if (!result.success) { + showToast( + t("llm.providers.generic_openai.saved_connection_apply_failed", { + error: result.error || "Unknown error", + }), + "error" + ); + return; + } + + await reloadSettings(); + showToast( + t("llm.providers.generic_openai.saved_connection_applied"), + "success" + ); + }; + + const handleSaveConnection = async () => { + const values = readFormValues(); + if (!values) return; + + const defaultName = + connections.find((c) => c.id === selectedConnectionId)?.name || + values.modelPref || + values.basePath; + + const name = window.prompt( + t("llm.providers.generic_openai.saved_connection_name_prompt"), + defaultName + ); + if (!name?.trim()) return; + + const payload = { + ...values, + name: name.trim(), + id: + selectedConnectionId !== MANUAL_CONNECTION_VALUE + ? selectedConnectionId + : undefined, + }; + + if ( + payload.apiKey && + payload.apiKey.length > 0 && + payload.apiKey === "*".repeat(20) + ) { + delete payload.apiKey; + } + + const result = await GenericOpenAiConnections.save(payload); + if (!result.success) { + showToast( + t("llm.providers.generic_openai.saved_connection_save_failed", { + error: result.error || "Unknown error", + }), + "error" + ); + return; + } + + setConnections(result.connections || []); + setSelectedConnectionId( + result.activeConnectionId || MANUAL_CONNECTION_VALUE + ); + await reloadSettings(); + showToast( + t("llm.providers.generic_openai.saved_connection_saved"), + "success" + ); + }; + + const handleDeleteConnection = async () => { + if (selectedConnectionId === MANUAL_CONNECTION_VALUE) return; + if ( + !window.confirm( + t("llm.providers.generic_openai.saved_connection_delete_confirm") + ) + ) { + return; + } + + const result = await GenericOpenAiConnections.delete(selectedConnectionId); + if (!result.success) { + showToast( + t("llm.providers.generic_openai.saved_connection_delete_failed", { + error: result.error || "Unknown error", + }), + "error" + ); + return; + } + + setConnections(result.connections || []); + setSelectedConnectionId(MANUAL_CONNECTION_VALUE); + showToast( + t("llm.providers.generic_openai.saved_connection_deleted"), + "success" + ); + }; + + return ( +
+ {t("llm.providers.generic_openai.saved_connections_label")} +
+ {t("llm.providers.generic_openai.saved_connections_help")} +