diff --git a/.changeset/eight-melons-cheer.md b/.changeset/eight-melons-cheer.md new file mode 100644 index 00000000..cade47f1 --- /dev/null +++ b/.changeset/eight-melons-cheer.md @@ -0,0 +1,6 @@ +--- +"syncia": patch +--- + +- Dynamic modal names from /modals +- Deprecates usage of ollama modals directly, we can now use them via openai compatible endpoint diff --git a/src/components/Settings/Sections/ChatSettings.tsx b/src/components/Settings/Sections/ChatSettings.tsx index 988d2784..1e272ab0 100644 --- a/src/components/Settings/Sections/ChatSettings.tsx +++ b/src/components/Settings/Sections/ChatSettings.tsx @@ -1,19 +1,17 @@ -import * as Switch from '@radix-ui/react-switch' import React, { useState } from 'react' import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai' -import { useSettings } from '../../../hooks/useSettings' +import { Mode } from '../../../config/settings' import { useChatModels } from '../../../hooks/useChatModels' +import { useSettings } from '../../../hooks/useSettings' import { capitalizeText } from '../../../lib/capitalizeText' import { validateApiKey } from '../../../lib/validApiKey' import FieldWrapper from '../Elements/FieldWrapper' import SectionHeading from '../Elements/SectionHeading' -import { type AvailableModels, Mode } from '../../../config/settings' -import { getReadableModelName } from '../../../lib/getReadableModelName' const ChatSettings = () => { const [settings, setSettings] = useSettings() const [showPassword, setShowPassword] = useState(false) - const { availableModels, fetchLocalModels } = useChatModels() + const { models, setActiveChatModel } = useChatModels() const OpenAiApiKeyInputRef = React.useRef(null) const OpenAiBaseUrlInputRef = React.useRef(null) @@ -23,10 +21,8 @@ const ChatSettings = () => { event: React.FormEvent, ) => { event.preventDefault() - const target = event.target as HTMLFormElement - - const apiKeyValue = target.openAiApiKey.value - const baseurlValue = target.openAiBaseUrl.value + const apiKeyValue = OpenAiApiKeyInputRef.current?.value || '' + const baseurlValue = OpenAiBaseUrlInputRef.current?.value || '' if (OpenAiApiKeyInputRef.current) { const isOpenAiKeyValid: boolean = await validateApiKey( @@ -120,69 +116,24 @@ const ChatSettings = () => { Update - {' '} - {/* ========================= - Model Setting - ===========================*/} - - { - setSettings({ - ...settings, - chat: { - ...chatSettings, - showLocalModels: value, - }, - }) - fetchLocalModels() - }} - className="cdx-w-[42px] cdx-h-[25px] cdx-bg-neutral-500 cdx-rounded-full cdx-relative data-[state=checked]:cdx-bg-blue-500 cdx-outline-none cdx-cursor-default" - > - - - {chatSettings.showLocalModels && ( -
- 🚧 NOTE: You must run this command for this to work: - - OLLAMA_ORIGINS= - {window.location.origin} ollama start - -
- )} - {/* ========================= - Mode Setting - ===========================*/} { const [, setSettings] = useSettings() - const [error, setError] = React.useState(null) - const [showAdvanced, setShowAdvanced] = React.useState(false) + const { models, setActiveChatModel, fetchAvailableModels } = useChatModels() + const [isLoadingModels, setIsLoadingModels] = useState(false) + const [error, setError] = useState(null) + const [formData, setFormData] = useState({ + apiKey: '', + baseUrl: 'https://api.openai.com/v1', + }) useEffect(() => { if (error) { - setTimeout(() => { - setError(null) - }, 3000) + const timer = setTimeout(() => setError(null), 3000) + return () => clearTimeout(timer) } }, [error]) - const handleOpenAiKeySubmit = async (e: React.FormEvent) => { - e.preventDefault() - const data = new FormData(e.currentTarget) - const key = data.get('openAiKey') as string | null - const openAiBaseUrl = - (data.get('openAiBaseUrl') as string) || 'https://api.openai.com/v1' + const handleInputChange = async (e: React.ChangeEvent) => { + const { id, value } = e.target + const newFormData = { ...formData, [id]: value } + setFormData(newFormData) + + if (id === 'apiKey' && value.startsWith('sk-') && value.length > 40) { + await validateAndUpdateSettings(value, formData.baseUrl) + } else if (id === 'baseUrl' && formData.apiKey) { + await validateAndUpdateSettings(formData.apiKey, value) + } + } - if (key && (await validateApiKey(key, openAiBaseUrl))) { - setSettings((prev) => ({ - ...prev, - chat: { - ...prev.chat, - openAIKey: key as string, - openAiBaseUrl: openAiBaseUrl, - }, - })) - } else { - setError('Invalid API key. Please try with a valid one.') + const validateAndUpdateSettings = async (key: string, url: string) => { + setIsLoadingModels(true) + try { + if (await validateApiKey(key, url)) { + setSettings((prev) => ({ + ...prev, + chat: { ...prev.chat, openAIKey: key, openAiBaseUrl: url }, + })) + await fetchAvailableModels() + } else { + setError('Invalid API key. Please try with a valid one.') + } + } finally { + setIsLoadingModels(false) } } + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!formData.apiKey || !formData.baseUrl) { + setError('Please fill in all required fields') + return + } + validateAndUpdateSettings(formData.apiKey, formData.baseUrl) + } + return (
e.preventDefault()} >
Enter your OpenAI API key
@@ -53,68 +76,62 @@ const Auth = () => { here
-
- It should look something like this: -
-
- sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -
-
- +
+ +
- {showAdvanced && ( -
- - -
- )} +
+ +
{error && (
{error}
)} -
- (Note: we only store your key locally. We do not send it anywhere. You +
+ Note: we only store your key locally. We do not send it anywhere. You can check the{' '} { > source code {' '} - and inspect network tab to verify this.) + and inspect network tab to verify this.
) diff --git a/src/components/Sidebar/chat/ChangeChatModel.tsx b/src/components/Sidebar/chat/ChangeChatModel.tsx index d55fb608..94975a94 100644 --- a/src/components/Sidebar/chat/ChangeChatModel.tsx +++ b/src/components/Sidebar/chat/ChangeChatModel.tsx @@ -1,24 +1,21 @@ import { BsRobot } from 'react-icons/bs' -import type { AvailableModels } from '../../../config/settings' import { useChatModels } from '../../../hooks/useChatModels' -import { getReadableModelName } from '../../../lib/getReadableModelName' const ChangeChatModel = () => { - const { availableModels, activeChatModel, setActiveChatModel } = - useChatModels() + const { models, activeChatModel, setActiveChatModel } = useChatModels() return (
diff --git a/src/components/Sidebar/chat/index.tsx b/src/components/Sidebar/chat/index.tsx index 079c387a..9880b0fa 100644 --- a/src/components/Sidebar/chat/index.tsx +++ b/src/components/Sidebar/chat/index.tsx @@ -3,7 +3,7 @@ import ChatList from './ChatList' import { SidebarInput } from './ChatInput' import { useChatCompletion } from '../../../hooks/useChatCompletion' import { SYSTEM_PROMPT } from '../../../config/prompts' -import { AvailableModels, type Settings } from '../../../config/settings' +import type { Settings } from '../../../config/settings' interface ChatProps { settings: Settings @@ -19,7 +19,7 @@ const Chat = ({ settings }: ChatProps) => { removeMessagePair, error, } = useChatCompletion({ - model: settings.chat.model, + model: settings.chat.model!, apiKey: settings.chat.openAIKey!, mode: settings.chat.mode, systemPrompt: SYSTEM_PROMPT, @@ -58,11 +58,7 @@ const Chat = ({ settings }: ChatProps) => { clearMessages={clearMessages} cancelRequest={cancelRequest} isWebpageContextOn={settings.general.webpageContext} - isVisionModel={ - settings.chat.model === AvailableModels.GPT_4_TURBO || - settings.chat.model === AvailableModels.GPT_4O || - settings.chat.model === AvailableModels.GPT_4O_MINI - } + isVisionModel={true} /> ) diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 61889847..08108c20 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -10,7 +10,11 @@ function Sidebar() { return (
- {settings.chat.openAIKey ? : } + {settings.chat.openAIKey && settings.chat.model ? ( + + ) : ( + + )}
) } diff --git a/src/config/settings/index.ts b/src/config/settings/index.ts index 8f428a32..0066646c 100644 --- a/src/config/settings/index.ts +++ b/src/config/settings/index.ts @@ -6,14 +6,6 @@ export enum ThemeOptions { SYSTEM = 'system', } -export enum AvailableModels { - GPT_4O = 'gpt-4o', - GPT_4_TURBO = 'gpt-4-turbo', - GPT_4 = 'gpt-4', - GPT_3_5_TURBO = 'gpt-3.5-turbo', - GPT_4O_MINI = 'gpt-4o-mini', -} - export enum Mode { HIGHLY_PRECISE = 0, PRECISE = 0.5, @@ -29,9 +21,8 @@ export type Settings = { } chat: { openAIKey: string | null - model: AvailableModels + model: string | null mode: Mode - showLocalModels: boolean openAiBaseUrl: string | null } general: { @@ -48,9 +39,8 @@ export const defaultSettings: Settings = { }, chat: { openAIKey: null, - model: AvailableModels.GPT_4O_MINI, + model: null, mode: Mode.BALANCED, - showLocalModels: false, openAiBaseUrl: null, }, general: { diff --git a/src/hooks/useChatCompletion.ts b/src/hooks/useChatCompletion.ts index f717e973..791ba157 100644 --- a/src/hooks/useChatCompletion.ts +++ b/src/hooks/useChatCompletion.ts @@ -1,35 +1,24 @@ import endent from 'endent' import { ChatOpenAI } from '@langchain/openai' -import { Ollama } from '@langchain/community/llms/ollama' import { AIMessage, HumanMessage, SystemMessage, } from '@langchain/core/messages' import { useMemo, useState } from 'react' -import { AvailableModels, type Mode } from '../config/settings' +import type { Mode } from '../config/settings' import { getMatchedContent } from '../lib/getMatchedContent' import { ChatRole, useCurrentChat } from './useCurrentChat' import type { MessageDraft } from './useMessageDraft' interface UseChatCompletionProps { - model: AvailableModels + model: string apiKey: string mode: Mode systemPrompt: string baseURL: string } -/** - * This hook is responsible for managing the chat completion - * functionality by using the useCurrentChat hook - * - * It adds functions for - * - submitting a query to the chat - * - cancelling a query - * - * And returns them along with useful state from useCurrentChat hook - */ let controller: AbortController export const useChatCompletion = ({ @@ -51,20 +40,15 @@ export const useChatCompletion = ({ const [error, setError] = useState(null) const llm = useMemo(() => { - const isOpenAIModel = Object.values(AvailableModels).includes(model) - if (isOpenAIModel) { - return new ChatOpenAI({ - streaming: true, - openAIApiKey: apiKey, - modelName: model, - configuration: { - baseURL: baseURL, - }, - temperature: Number(mode), - maxTokens: 4_096, - }) - } - return new Ollama({ model: model.replace('ollama-', '') }) + return new ChatOpenAI({ + streaming: true, + openAIApiKey: apiKey, + modelName: model, + configuration: { + baseURL: baseURL, + }, + temperature: Number(mode), + }) }, [apiKey, model, mode, baseURL]) const previousMessages = messages.map((msg) => { @@ -90,11 +74,6 @@ export const useChatCompletion = ({ setGenerating(true) try { - /** - * If context is provided, we need to use the LLM to get the relevant documents - * and then run the LLM on those documents. We use in memory vector store to - * get the relevant documents - */ let matchedContext: string | undefined if (context) { matchedContext = await getMatchedContent( diff --git a/src/hooks/useChatModels.ts b/src/hooks/useChatModels.ts index d2feefec..83ed5387 100644 --- a/src/hooks/useChatModels.ts +++ b/src/hooks/useChatModels.ts @@ -1,52 +1,57 @@ import { useCallback, useEffect, useState } from 'react' import { useSettings } from './useSettings' import axios from 'axios' -import { AvailableModels } from '../config/settings' + +type OpenAIModel = { + id: string + object: string + created: number + owned_by: string +} export const useChatModels = () => { const [settings, setSettings] = useSettings() - const [dynamicModels, setDynamicModels] = useState([]) + const [models, setModels] = useState([]) const chatSettings = settings.chat const activeChatModel = chatSettings.model - const fetchLocalModels = useCallback(async () => { - if (chatSettings.showLocalModels) { - const { - data: { models }, - } = await axios<{ models: { name: string }[] }>( - 'http://localhost:11434/api/tags', - ) - if (models) { - setDynamicModels(models.map((m) => m.name)) + const fetchAvailableModels = useCallback(async () => { + if (chatSettings.openAIKey) { + try { + const baseUrl = + chatSettings.openAiBaseUrl || 'https://api.openai.com/v1' + const { data } = await axios.get(`${baseUrl}/models`, { + headers: { + Authorization: `Bearer ${chatSettings.openAIKey}`, + }, + }) + + setModels(data.data) + } catch (error) { + console.log('Failed to fetch models:', error) + setModels([]) } - } else { - setDynamicModels([]) } - }, [chatSettings.showLocalModels]) + }, [chatSettings.openAIKey, chatSettings.openAiBaseUrl]) useEffect(() => { - fetchLocalModels() - }, [fetchLocalModels]) - - const availableModels = [ - ...Object.entries(AvailableModels), - ...dynamicModels.map((m) => [m, m]), - ] + fetchAvailableModels() + }, [fetchAvailableModels]) - const setActiveChatModel = (model: AvailableModels) => { + const setActiveChatModel = (modelId: string) => { setSettings({ ...settings, chat: { ...chatSettings, - model: model, + model: modelId, }, }) } return { - availableModels, + models, activeChatModel, setActiveChatModel, - fetchLocalModels, + fetchAvailableModels, } } diff --git a/src/pages/content/sidebar.tsx b/src/pages/content/sidebar.tsx index f9fb123a..a976a79a 100644 --- a/src/pages/content/sidebar.tsx +++ b/src/pages/content/sidebar.tsx @@ -1,4 +1,4 @@ -import { AvailableModels, type Settings } from '../../config/settings' +import type { Settings } from '../../config/settings' import { getScreenshotImage } from '../../lib/getScreenshotImage' import { contentScriptLog } from '../../logs' @@ -35,18 +35,6 @@ chrome.runtime.onMessage.addListener((msg) => { } }) -/** - * Convert local data `modal` typo to `model` - */ -chrome.storage.sync.get(['SETTINGS'], (result) => { - const chatSettings = (result.SETTINGS as Settings)?.chat - if ('modal' in chatSettings) { - chatSettings.model = AvailableModels.GPT_4_TURBO - delete chatSettings.modal - } - chrome.storage.sync.set({ SETTINGS: result.SETTINGS }) -}) - /** * SIDEBAR <-> CONTENT SCRIPT * Event listener for messages from the sidebar.