diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 6f633cbb8f0..dcd1ba5696d 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['model-map-staleness'] # put your current branch to create a build. Core team only. + branches: ['2095-model-swap-in-chat'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/frontend/src/components/ModalWrapper/index.jsx b/frontend/src/components/ModalWrapper/index.jsx index 7c45c22c5ab..ac6b8ea1bb1 100644 --- a/frontend/src/components/ModalWrapper/index.jsx +++ b/frontend/src/components/ModalWrapper/index.jsx @@ -9,8 +9,11 @@ import { createPortal } from "react-dom"; */ /** + * * @param {ModalWrapperProps} props - ModalWrapperProps to pass * @returns {import("react").ReactNode} + * + * @todo Add a closeModal prop to the ModalWrapper component so we can escape dismiss anywhere this is used */ export default function ModalWrapper({ children, isOpen, noPortal = false }) { if (!isOpen) return null; diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/ChatModelSelection/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/ChatModelSelection/index.jsx new file mode 100644 index 00000000000..d05689748fe --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/ChatModelSelection/index.jsx @@ -0,0 +1,120 @@ +import useGetProviderModels, { + DISABLED_PROVIDERS, +} from "@/hooks/useGetProvidersModels"; +import { useTranslation } from "react-i18next"; + +export default function ChatModelSelection({ + provider, + setHasChanges, + selectedLLMModel, + setSelectedLLMModel, +}) { + const { defaultModels, customModels, loading } = + useGetProviderModels(provider); + const { t } = useTranslation(); + if (DISABLED_PROVIDERS.includes(provider)) return null; + + if (loading) { + return ( +
+
+ +

+ {t( + "chat_window.workspace_llm_manager.available_models_description" + )} +

+
+ +
+ ); + } + + return ( +
+
+ +

+ {t("chat_window.workspace_llm_manager.available_models_description")} +

+
+ + +
+ ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/LLMSelector/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/LLMSelector/index.jsx new file mode 100644 index 00000000000..66c018505ee --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/LLMSelector/index.jsx @@ -0,0 +1,42 @@ +import { useTranslation } from "react-i18next"; + +export default function LLMSelectorSidePanel({ + availableProviders, + selectedLLMProvider, + onSearchChange, + onProviderClick, +}) { + const { t } = useTranslation(); + + return ( +
+ +
+ {availableProviders.map((llm) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/SetupProvider/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/SetupProvider/index.jsx new file mode 100644 index 00000000000..2fb69c79653 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/SetupProvider/index.jsx @@ -0,0 +1,109 @@ +import { createPortal } from "react-dom"; +import ModalWrapper from "@/components/ModalWrapper"; +import { X } from "@phosphor-icons/react"; +import System from "@/models/system"; +import showToast from "@/utils/toast"; +import { useTranslation } from "react-i18next"; + +export default function SetupProvider({ + isOpen, + closeModal, + postSubmit, + settings, + llmProvider, +}) { + if (!isOpen) return null; + + async function handleUpdate(e) { + e.preventDefault(); + e.stopPropagation(); + const data = {}; + const form = new FormData(e.target); + for (var [key, value] of form.entries()) data[key] = value; + const { error } = await System.updateSystem(data); + if (error) { + showToast( + `Failed to save ${llmProvider.name} settings: ${error}`, + "error" + ); + return; + } + + closeModal(); + postSubmit(); + return false; + } + + return createPortal( + +
+
+
+
+

+ {llmProvider.name} Settings +

+
+ +
+
+
+
+

+ To use {llmProvider.name} as this workspace's LLM you need to + set it up first. +

+
+ {llmProvider.options(settings, { credentialsOnly: true })} +
+
+
+
+ + +
+
+
+
+
, + document.body + ); +} + +export function NoSetupWarning({ showing, onSetupClick }) { + const { t } = useTranslation(); + if (!showing) return null; + + return ( + + ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/action.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/action.jsx new file mode 100644 index 00000000000..8988f120942 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/action.jsx @@ -0,0 +1,131 @@ +import { Tooltip } from "react-tooltip"; +import { Brain, CheckCircle } from "@phosphor-icons/react"; +import LLMSelectorModal from "./index"; +import { useTheme } from "@/hooks/useTheme"; +import { useRef, useEffect, useState } from "react"; +import useUser from "@/hooks/useUser"; +import { useModal } from "@/hooks/useModal"; +import SetupProvider from "./SetupProvider"; + +export const TOGGLE_LLM_SELECTOR_EVENT = "toggle_llm_selector"; +export const SAVE_LLM_SELECTOR_EVENT = "save_llm_selector"; +export const PROVIDER_SETUP_EVENT = "provider_setup_requested"; + +export default function LLMSelectorAction() { + const tooltipRef = useRef(null); + const { theme } = useTheme(); + const { user } = useUser(); + const [saved, setSaved] = useState(false); + const { + isOpen: isSetupProviderOpen, + openModal: openSetupProviderModal, + closeModal: closeSetupProviderModal, + } = useModal(); + const [config, setConfig] = useState({ + settings: {}, + provider: null, + }); + + function toggleLLMSelectorTooltip() { + if (!tooltipRef.current) return; + tooltipRef.current.isOpen + ? tooltipRef.current.close() + : tooltipRef.current.open(); + } + + function handleSaveLLMSelector() { + if (!tooltipRef.current) return; + tooltipRef.current.close(); + setSaved(true); + } + + useEffect(() => { + window.addEventListener( + TOGGLE_LLM_SELECTOR_EVENT, + toggleLLMSelectorTooltip + ); + window.addEventListener(SAVE_LLM_SELECTOR_EVENT, handleSaveLLMSelector); + return () => { + window.removeEventListener( + TOGGLE_LLM_SELECTOR_EVENT, + toggleLLMSelectorTooltip + ); + window.removeEventListener( + SAVE_LLM_SELECTOR_EVENT, + handleSaveLLMSelector + ); + }; + }, []); + + useEffect(() => { + if (!saved) return; + setTimeout(() => { + setSaved(false); + }, 1500); + }, [saved]); + + useEffect(() => { + function handleProviderSetupEvent(e) { + const { provider, settings } = e.detail; + setConfig({ + settings, + provider, + }); + setTimeout(() => { + openSetupProviderModal(); + }, 300); + } + + window.addEventListener(PROVIDER_SETUP_EVENT, handleProviderSetupEvent); + return () => + window.removeEventListener( + PROVIDER_SETUP_EVENT, + handleProviderSetupEvent + ); + }, []); + + // This feature is disabled for multi-user instances where the user is not an admin + // This is because of the limitations of model selection currently and other nuances in controls. + if (!!user && user.role !== "admin") return null; + + return ( + <> +
+ {saved ? ( + + ) : ( + + )} +
+ + + + closeSetupProviderModal()} + settings={config.settings} + llmProvider={config.provider} + /> + + ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/index.jsx new file mode 100644 index 00000000000..a09fcc0cd9d --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/index.jsx @@ -0,0 +1,151 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import PreLoader from "@/components/Preloader"; +import ChatModelSelection from "./ChatModelSelection"; +import { useTranslation } from "react-i18next"; +import { PROVIDER_SETUP_EVENT, SAVE_LLM_SELECTOR_EVENT } from "./action"; +import { + WORKSPACE_LLM_PROVIDERS, + autoScrollToSelectedLLMProvider, + hasMissingCredentials, + validatedModelSelection, +} from "./utils"; +import LLMSelectorSidePanel from "./LLMSelector"; +import { NoSetupWarning } from "./SetupProvider"; +import showToast from "@/utils/toast"; +import Workspace from "@/models/workspace"; +import System from "@/models/system"; + +export default function LLMSelectorModal() { + const { slug } = useParams(); + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [settings, setSettings] = useState(null); + const [selectedLLMProvider, setSelectedLLMProvider] = useState(null); + const [selectedLLMModel, setSelectedLLMModel] = useState(""); + const [availableProviders, setAvailableProviders] = useState( + WORKSPACE_LLM_PROVIDERS + ); + const [hasChanges, setHasChanges] = useState(false); + const [saving, setSaving] = useState(false); + const [missingCredentials, setMissingCredentials] = useState(false); + + useEffect(() => { + if (!slug) return; + setLoading(true); + Promise.all([Workspace.bySlug(slug), System.keys()]) + .then(([workspace, systemSettings]) => { + const selectedLLMProvider = + workspace.chatProvider ?? systemSettings.LLMProvider; + const selectedLLMModel = workspace.chatModel ?? systemSettings.LLMModel; + + setSettings(systemSettings); + setSelectedLLMProvider(selectedLLMProvider); + autoScrollToSelectedLLMProvider(selectedLLMProvider); + setSelectedLLMModel(selectedLLMModel); + }) + .finally(() => setLoading(false)); + }, [slug]); + + function handleSearch(e) { + const searchTerm = e.target.value.toLowerCase(); + const filteredProviders = WORKSPACE_LLM_PROVIDERS.filter((provider) => + provider.name.toLowerCase().includes(searchTerm) + ); + setAvailableProviders(filteredProviders); + } + + function handleProviderSelection(provider) { + setSelectedLLMProvider(provider); + setAvailableProviders(WORKSPACE_LLM_PROVIDERS); + autoScrollToSelectedLLMProvider(provider, 50); + document.getElementById("llm-search-input").value = ""; + setHasChanges(true); + setMissingCredentials(hasMissingCredentials(settings, provider)); + } + + async function handleSave() { + setSaving(true); + try { + setHasChanges(false); + const validatedModel = validatedModelSelection(selectedLLMModel); + if (!validatedModel) throw new Error("Invalid model selection"); + + const { message } = await Workspace.update(slug, { + chatProvider: selectedLLMProvider, + chatModel: validatedModel, + }); + + if (!!message) throw new Error(message); + window.dispatchEvent(new Event(SAVE_LLM_SELECTOR_EVENT)); + } catch (error) { + console.error(error); + showToast(error.message, "error", { clear: true }); + } finally { + setSaving(false); + } + } + + if (loading) { + return ( +
+ +

+ {t("chat_window.workspace_llm_manager.loading_workspace_settings")} +

+
+ ); + } + + return ( +
+ +
+ { + window.dispatchEvent( + new CustomEvent(PROVIDER_SETUP_EVENT, { + detail: { + provider: WORKSPACE_LLM_PROVIDERS.find( + (p) => p.value === selectedLLMProvider + ), + settings, + }, + }) + ); + }} + /> + + {hasChanges && ( + + )} +
+
+ ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/utils.js b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/utils.js new file mode 100644 index 00000000000..b03e1b40a91 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/utils.js @@ -0,0 +1,61 @@ +import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference"; +import { DISABLED_PROVIDERS } from "@/hooks/useGetProvidersModels"; + +export function autoScrollToSelectedLLMProvider( + selectedLLMProvider, + timeout = 500 +) { + setTimeout(() => { + const selectedButton = document.querySelector( + `[data-llm-value="${selectedLLMProvider}"]` + ); + if (!selectedButton) return; + selectedButton.scrollIntoView({ behavior: "smooth", block: "nearest" }); + }, timeout); +} + +/** + * Validates the model selection by checking if the model is in the select option in the available models + * dropdown. If the model is not in the dropdown, it will return the first model in the dropdown. + * + * This exists when the user swaps providers, but did not select a model in the new provider's dropdown + * and assumed the first model in the picker was OK. This prevents invalid provider<>model selection issues + * @param {string} model - The model to validate + * @returns {string} - The validated model + */ +export function validatedModelSelection(model) { + try { + // If the entire select element is not found, return the model as is and cross our fingers + const selectOption = document.getElementById(`workspace-llm-model-select`); + if (!selectOption) return model; + + // If the model is not in the dropdown, return the first model in the dropdown + // to prevent invalid provider<>model selection issues + const selectedOption = selectOption.querySelector( + `option[value="${model}"]` + ); + if (!selectedOption) return selectOption.querySelector(`option`).value; + + // If the model is in the dropdown, return the model as is + return model; + } catch (error) { + return null; // If the dropdown was empty or something else went wrong, return null to abort the save + } +} + +export function hasMissingCredentials(settings, provider) { + const providerEntry = AVAILABLE_LLM_PROVIDERS.find( + (p) => p.value === provider + ); + if (!providerEntry) return false; + + for (const requiredKey of providerEntry.requiredConfig) { + if (!settings.hasOwnProperty(requiredKey)) return true; + if (!settings[requiredKey]) return true; + } + return false; +} + +export const WORKSPACE_LLM_PROVIDERS = AVAILABLE_LLM_PROVIDERS.filter( + (provider) => !DISABLED_PROVIDERS.includes(provider.value) +); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx index 14c1b173479..8a441293041 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx @@ -11,6 +11,7 @@ import AvailableAgentsButton, { useAvailableAgents, } from "./AgentMenu"; import TextSizeButton from "./TextSizeMenu"; +import LLMSelectorAction from "./LLMSelector/action"; import SpeechToText from "./SpeechToText"; import { Tooltip } from "react-tooltip"; import AttachmentManager from "./Attachments"; @@ -323,6 +324,7 @@ export default function PromptInput({ setShowAgents={setShowAgents} /> +
diff --git a/frontend/src/hooks/useGetProvidersModels.js b/frontend/src/hooks/useGetProvidersModels.js index ad12da8bf29..82ef427cfd5 100644 --- a/frontend/src/hooks/useGetProvidersModels.js +++ b/frontend/src/hooks/useGetProvidersModels.js @@ -61,6 +61,7 @@ export default function useGetProviderModels(provider = null) { useEffect(() => { async function fetchProviderModels() { if (!provider) return; + setLoading(true); const { models = [] } = await System.customModels(provider); if ( PROVIDER_DEFAULT_MODELS.hasOwnProperty(provider) && diff --git a/frontend/src/locales/ar/common.js b/frontend/src/locales/ar/common.js index 315d81bc29f..71ed7092a3a 100644 --- a/frontend/src/locales/ar/common.js +++ b/frontend/src/locales/ar/common.js @@ -710,6 +710,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -913,6 +923,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/da/common.js b/frontend/src/locales/da/common.js index 30fbf86552d..42a0c09a8a4 100644 --- a/frontend/src/locales/da/common.js +++ b/frontend/src/locales/da/common.js @@ -748,6 +748,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: "Rediger konto", @@ -952,6 +962,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/de/common.js b/frontend/src/locales/de/common.js index 23435faff0e..77b1a0aa1ac 100644 --- a/frontend/src/locales/de/common.js +++ b/frontend/src/locales/de/common.js @@ -746,6 +746,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: "Account bearbeiten", @@ -957,6 +967,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/en/common.js b/frontend/src/locales/en/common.js index 422ab91520f..b6693a79196 100644 --- a/frontend/src/locales/en/common.js +++ b/frontend/src/locales/en/common.js @@ -978,6 +978,16 @@ const TRANSLATIONS = { small: "Small", normal: "Normal", large: "Large", + workspace_llm_manager: { + search: "Search LLM providers", + loading_workspace_settings: "Loading workspace settings...", + available_models: "Available Models for {{provider}}", + available_models_description: "Select a model to use for this workspace.", + save: "Use this model", + saving: "Setting model as workspace default...", + missing_credentials: "This provider is missing credentials!", + missing_credentials_description: "Click to set up credentials", + }, }, profile_settings: { @@ -1014,6 +1024,7 @@ const TRANSLATIONS = { llmPreferences: "LLM Preferences", chatSettings: "Chat Settings", help: "Show keyboard shortcuts help", + showLLMSelector: "Show workspace LLM Selector", }, }, }; diff --git a/frontend/src/locales/es/common.js b/frontend/src/locales/es/common.js index 21e1afb0e8c..bb72a433d13 100644 --- a/frontend/src/locales/es/common.js +++ b/frontend/src/locales/es/common.js @@ -709,6 +709,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -917,6 +927,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/fa/common.js b/frontend/src/locales/fa/common.js index 401da20dd0a..08599498c3e 100644 --- a/frontend/src/locales/fa/common.js +++ b/frontend/src/locales/fa/common.js @@ -702,6 +702,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -905,6 +915,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/fr/common.js b/frontend/src/locales/fr/common.js index f66e53fc24b..cf43f2e9627 100644 --- a/frontend/src/locales/fr/common.js +++ b/frontend/src/locales/fr/common.js @@ -710,6 +710,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -913,6 +923,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/he/common.js b/frontend/src/locales/he/common.js index 0fda5628f38..bcb2ab5f969 100644 --- a/frontend/src/locales/he/common.js +++ b/frontend/src/locales/he/common.js @@ -695,6 +695,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -898,6 +908,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/it/common.js b/frontend/src/locales/it/common.js index f59be7f804f..bac64617ec1 100644 --- a/frontend/src/locales/it/common.js +++ b/frontend/src/locales/it/common.js @@ -708,6 +708,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -911,6 +921,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/ja/common.js b/frontend/src/locales/ja/common.js index c8c2df458e9..e6766c3ffc9 100644 --- a/frontend/src/locales/ja/common.js +++ b/frontend/src/locales/ja/common.js @@ -740,6 +740,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: "アカウントを編集", @@ -947,6 +957,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/ko/common.js b/frontend/src/locales/ko/common.js index ab85f557167..30ef7fa528d 100644 --- a/frontend/src/locales/ko/common.js +++ b/frontend/src/locales/ko/common.js @@ -695,6 +695,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -898,6 +908,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/lv/common.js b/frontend/src/locales/lv/common.js index bca0cb8654e..aaab2ffaeea 100644 --- a/frontend/src/locales/lv/common.js +++ b/frontend/src/locales/lv/common.js @@ -935,6 +935,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: "Rediģēt kontu", @@ -969,6 +979,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/nl/common.js b/frontend/src/locales/nl/common.js index b4c9625fa0a..fec9e96d7dd 100644 --- a/frontend/src/locales/nl/common.js +++ b/frontend/src/locales/nl/common.js @@ -705,6 +705,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -908,6 +918,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/pt_BR/common.js b/frontend/src/locales/pt_BR/common.js index d2ad4f0ba92..343b9d27a28 100644 --- a/frontend/src/locales/pt_BR/common.js +++ b/frontend/src/locales/pt_BR/common.js @@ -916,6 +916,16 @@ const TRANSLATIONS = { small: "Pequeno", normal: "Normal", large: "Grande", + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: "Editar conta", @@ -950,6 +960,7 @@ const TRANSLATIONS = { llmPreferences: "Preferências do LLM", chatSettings: "Ajustes do chat", help: "Exibe ajuda e atalhos", + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/ru/common.js b/frontend/src/locales/ru/common.js index 84e247abbc2..fa80dbd13fd 100644 --- a/frontend/src/locales/ru/common.js +++ b/frontend/src/locales/ru/common.js @@ -749,6 +749,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: "Редактировать учётную запись", @@ -953,6 +963,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/tr/common.js b/frontend/src/locales/tr/common.js index 784aa829168..cf94d5308ee 100644 --- a/frontend/src/locales/tr/common.js +++ b/frontend/src/locales/tr/common.js @@ -705,6 +705,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -908,6 +918,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/vn/common.js b/frontend/src/locales/vn/common.js index ed01dd8f3b4..a92e3f23fd1 100644 --- a/frontend/src/locales/vn/common.js +++ b/frontend/src/locales/vn/common.js @@ -704,6 +704,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -907,6 +917,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/zh/common.js b/frontend/src/locales/zh/common.js index 9bdf04f32d0..dd20b892fce 100644 --- a/frontend/src/locales/zh/common.js +++ b/frontend/src/locales/zh/common.js @@ -875,6 +875,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: "编辑帐户", @@ -909,6 +919,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/locales/zh_TW/common.js b/frontend/src/locales/zh_TW/common.js index 721f33697f2..2abd8567e67 100644 --- a/frontend/src/locales/zh_TW/common.js +++ b/frontend/src/locales/zh_TW/common.js @@ -707,6 +707,16 @@ const TRANSLATIONS = { small: null, normal: null, large: null, + workspace_llm_manager: { + search: null, + loading_workspace_settings: null, + available_models: null, + available_models_description: null, + save: null, + saving: null, + missing_credentials: null, + missing_credentials_description: null, + }, }, profile_settings: { edit_account: null, @@ -910,6 +920,7 @@ const TRANSLATIONS = { llmPreferences: null, chatSettings: null, help: null, + showLLMSelector: null, }, }, }; diff --git a/frontend/src/utils/keyboardShortcuts.js b/frontend/src/utils/keyboardShortcuts.js index 024afc4db52..75da08926e0 100644 --- a/frontend/src/utils/keyboardShortcuts.js +++ b/frontend/src/utils/keyboardShortcuts.js @@ -1,6 +1,7 @@ import paths from "./paths"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { userFromStorage } from "./request"; +import { TOGGLE_LLM_SELECTOR_EVENT } from "@/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/action"; export const KEYBOARD_SHORTCUTS_HELP_EVENT = "keyboard-shortcuts-help"; export const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; @@ -61,6 +62,12 @@ export const SHORTCUTS = { ); }, }, + "⌘ + Shift + L": { + translationKey: "showLLMSelector", + action: () => { + window.dispatchEvent(new Event(TOGGLE_LLM_SELECTOR_EVENT)); + }, + }, }; const LISTENERS = {}; diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 89e397ea64e..a7a3e752c6e 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -8,6 +8,7 @@ const prisma = require("../utils/prisma"); const { v4 } = require("uuid"); const { MetaGenerator } = require("../utils/boot/MetaGenerator"); const { PGVector } = require("../utils/vectorDbProviders/pgvector"); +const { getBaseLLMProviderModel } = require("../utils/helpers"); function isNullOrNaN(value) { if (value === null) return true; @@ -227,6 +228,7 @@ const SystemSettings = { // LLM Provider Selection Settings & Configs // -------------------------------------------------------- LLMProvider: llmProvider, + LLMModel: getBaseLLMProviderModel({ provider: llmProvider }) || null, ...this.llmPreferenceKeys(), // -------------------------------------------------------- diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index a069b0dd3ca..2017c618fac 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -358,6 +358,72 @@ function getLLMProviderClass({ provider = null } = {}) { } } +/** + * Returns the defined model (if available) for the given provider. + * @param {{provider: string | null} | null} params - Initialize params for LLMs provider + * @returns {string | null} + */ +function getBaseLLMProviderModel({ provider = null } = {}) { + switch (provider) { + case "openai": + return process.env.OPEN_MODEL_PREF; + case "azure": + return process.env.OPEN_MODEL_PREF; + case "anthropic": + return process.env.ANTHROPIC_MODEL_PREF; + case "gemini": + return process.env.GEMINI_LLM_MODEL_PREF; + case "lmstudio": + return process.env.LMSTUDIO_MODEL_PREF; + case "localai": + return process.env.LOCAL_AI_MODEL_PREF; + case "ollama": + return process.env.OLLAMA_MODEL_PREF; + case "togetherai": + return process.env.TOGETHER_AI_MODEL_PREF; + case "fireworksai": + return process.env.FIREWORKS_AI_LLM_MODEL_PREF; + case "perplexity": + return process.env.PERPLEXITY_MODEL_PREF; + case "openrouter": + return process.env.OPENROUTER_MODEL_PREF; + case "mistral": + return process.env.MISTRAL_MODEL_PREF; + case "huggingface": + return null; + case "groq": + return process.env.GROQ_MODEL_PREF; + case "koboldcpp": + return process.env.KOBOLD_CPP_MODEL_PREF; + case "textgenwebui": + return process.env.TEXT_GEN_WEB_UI_API_KEY; + case "cohere": + return process.env.COHERE_MODEL_PREF; + case "litellm": + return process.env.LITE_LLM_MODEL_PREF; + case "generic-openai": + return process.env.GENERIC_OPEN_AI_EMBEDDING_API_KEY; + case "bedrock": + return process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE; + case "deepseek": + return process.env.DEEPSEEK_MODEL_PREF; + case "apipie": + return process.env.APIPIE_LLM_API_KEY; + case "novita": + return process.env.NOVITA_LLM_MODEL_PREF; + case "xai": + return process.env.XAI_LLM_MODEL_PREF; + case "nvidia-nim": + return process.env.NVIDIA_NIM_LLM_MODEL_PREF; + case "ppio": + return process.env.PPIO_API_KEY; + case "dpais": + return process.env.DPAIS_LLM_MODEL_PREF; + default: + return null; + } +} + // Some models have lower restrictions on chars that can be encoded in a single pass // and by default we assume it can handle 1,000 chars, but some models use work with smaller // chars so here we can override that value when embedding information. @@ -383,6 +449,7 @@ module.exports = { maximumChunkLength, getVectorDbClass, getLLMProviderClass, + getBaseLLMProviderModel, getLLMProvider, toChunks, };