diff --git a/enjoy/src/commands/json.command.ts b/enjoy/src/commands/json.command.ts index ec4ea089..f0a08920 100644 --- a/enjoy/src/commands/json.command.ts +++ b/enjoy/src/commands/json.command.ts @@ -1,5 +1,5 @@ import { ChatOpenAI } from "@langchain/openai"; -import { RESPONSE_JSON_FORMAT_MODELS } from "@/constants"; +import { NOT_SUPPORTE_JSON_FORMAT_MODELS } from "@/constants"; import { zodToJsonSchema } from "zod-to-json-schema"; export const jsonCommand = async ( @@ -15,7 +15,7 @@ export const jsonCommand = async ( const { key, temperature = 0, baseUrl, schema } = options; let { modelName = "gpt-4o" } = options; - if (RESPONSE_JSON_FORMAT_MODELS.indexOf(modelName) === -1) { + if (NOT_SUPPORTE_JSON_FORMAT_MODELS.indexOf(modelName) > -1) { modelName = "gpt-4o"; } diff --git a/enjoy/src/constants.ts b/enjoy/src/constants.ts index 1aebd5f5..adf35cc6 100644 --- a/enjoy/src/constants.ts +++ b/enjoy/src/constants.ts @@ -125,16 +125,10 @@ export const PROCESS_TIMEOUT = 1000 * 60 * 15; export const AI_GATEWAY_ENDPOINT = "https://gateway.ai.cloudflare.com/v1/11d43ab275eb7e1b271ba4089ecc3864/enjoy"; -export const RESPONSE_JSON_FORMAT_MODELS = [ - "gpt-3.5-turbo-0125", - "gpt-3.5-turbo", - "gpt-3.5-turbo-1106", - "gpt-4o", - "gpt-4-turbo", - "gpt-4-turbo-2024-04-09", - "gpt-4-0125-preview", - "gpt-4-turbo-preview", - "gpt-4-1106-preview", +export const NOT_SUPPORTE_JSON_FORMAT_MODELS = [ + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-32k", ]; export const CONVERSATION_PRESETS = [ diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json index 1ca078c1..ad271760 100644 --- a/enjoy/src/i18n/en.json +++ b/enjoy/src/i18n/en.json @@ -368,6 +368,12 @@ "whisperModelIsNotWorking": "Whisper model is not working", "relaunchIsNeededAfterChanged": "Relaunch is needed after changed", "defaultAiEngine": "Default AI engine", + "aiEngine": "AI engine", + "defaultAiModel": "Default AI model", + "lookupAiModel": "AI Lookup", + "translateAiModel": "AI Translate", + "analyzeAiModel": "AI Analyze", + "extractStoryAiModel": "AI extract vocabulary", "openAiEngineTips": "Use OpenAI with your own key as default AI engine.", "enjoyAiEngineTips": "Use EnjoyAI as default AI engine. It is a paid service.", "openaiKeySaved": "OpenAI key saved", diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json index 23f89ef9..97829d6a 100644 --- a/enjoy/src/i18n/zh-CN.json +++ b/enjoy/src/i18n/zh-CN.json @@ -368,6 +368,12 @@ "whisperModelIsNotWorking": "Whisper 模型无法正常工作,请尝试更换模型后重试,或联系开发者", "relaunchIsNeededAfterChanged": "更改后需要重新启动", "defaultAiEngine": "默认 AI 引擎", + "aiEngine": "AI 引擎", + "defaultAiModel": "默认 AI 模型", + "lookupAiModel": "智能词典模型", + "translateAiModel": "智能翻译模型", + "analyzeAiModel": "句子分析模型", + "extractStoryAiModel": "提取生词模型", "openAiEngineTips": "使用 OpenAI 作为默认 AI 引擎,需要配置 API 密钥。", "enjoyAiEngineTips": "使用 EnjoyAI 作为默认 AI 引擎,收费服务。", "openaiKeySaved": "OpenAI 密钥已保存", diff --git a/enjoy/src/main/settings.ts b/enjoy/src/main/settings.ts index 5b3cb677..e768eec5 100644 --- a/enjoy/src/main/settings.ts +++ b/enjoy/src/main/settings.ts @@ -154,6 +154,14 @@ export default { return settings.setSync("defaultEngine", engine); }); + ipcMain.handle("settings-get-gpt-engine", (_event) => { + return settings.getSync("engine.gpt"); + }); + + ipcMain.handle("settings-set-gpt-engine", (_event, engine) => { + return settings.setSync("engine.gpt", engine); + }); + ipcMain.handle("settings-get-default-hotkeys", (_event) => { return settings.getSync("defaultHotkeys"); }); diff --git a/enjoy/src/preload.ts b/enjoy/src/preload.ts index 8a3b98a8..3445ef84 100644 --- a/enjoy/src/preload.ts +++ b/enjoy/src/preload.ts @@ -177,6 +177,12 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", { setDefaultEngine: (engine: "enjoyai" | "openai") => { return ipcRenderer.invoke("settings-set-default-engine", engine); }, + getGptEngine: () => { + return ipcRenderer.invoke("settings-get-gpt-engine"); + }, + setGptEngine: (engine: GptEngineSettingType) => { + return ipcRenderer.invoke("settings-set-gpt-engine", engine); + }, getLlm: (provider: string) => { return ipcRenderer.invoke("settings-get-llm", provider); }, diff --git a/enjoy/src/renderer/components/preferences/default-engine-settings.tsx b/enjoy/src/renderer/components/preferences/default-engine-settings.tsx index 86f8ec6a..42c9c34f 100644 --- a/enjoy/src/renderer/components/preferences/default-engine-settings.tsx +++ b/enjoy/src/renderer/components/preferences/default-engine-settings.tsx @@ -1,3 +1,6 @@ +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { t } from "i18next"; import { Select, @@ -6,47 +9,314 @@ import { SelectItem, SelectValue, toast, + Form, + Button, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@renderer/components/ui"; -import { AISettingsProviderContext } from "@renderer/context"; -import { useContext } from "react"; +import { + AISettingsProviderContext, + AppSettingsProviderContext, +} from "@renderer/context"; +import { useContext, useEffect, useState } from "react"; +import { GPT_PROVIDERS } from "@renderer/components"; export const DefaultEngineSettings = () => { - const { defaultEngine, setDefaultEngine, openai } = useContext( + const { currentEngine, setGptEngine, openai } = useContext( AISettingsProviderContext ); + const { webApi } = useContext(AppSettingsProviderContext); + const [providers, setProviders] = useState(GPT_PROVIDERS); + const [editing, setEditing] = useState(false); + + const gptEngineSchema = z + .object({ + name: z.enum(["enjoyai", "openai"]), + models: z.object({ + default: z.string(), + lookup: z.string().optional(), + translate: z.string().optional(), + analyze: z.string().optional(), + extractStory: z.string().optional(), + }), + }) + .required(); + + const form = useForm>({ + resolver: zodResolver(gptEngineSchema), + values: { + name: currentEngine.name as "enjoyai" | "openai", + models: currentEngine.models || {}, + }, + }); + + const onSubmit = async (data: z.infer) => { + const { name, models } = data; + + models.default ||= providers[name].models[0]; + Object.keys(models).forEach((key: keyof typeof models) => { + if (!providers[name].models.includes(models[key])) { + delete models[key]; + } + }); + + setGptEngine(data as GptEngineSettingType); + setEditing(false); + }; + + useEffect(() => { + webApi + .config("gpt_providers") + .then((data) => { + setProviders(data); + }) + .catch((error) => { + console.error(error); + }); + }, []); return ( -
-
-
- {t("defaultAiEngine")} -
-
- {defaultEngine === "openai" && t("openAiEngineTips")} - {defaultEngine === "enjoyai" && t("enjoyAiEngineTips")} -
-
+
+ +
+
+
+ {t("defaultAiEngine")} +
+
+ ( + +
+ + {t("aiEngine")}: + + +
+ +
+ {form.watch("name") === "openai" && t("openAiEngineTips")} + {form.watch("name") === "enjoyai" && + t("enjoyAiEngineTips")} +
+
+ )} + /> + ( + +
+ + {t("defaultAiModel")}: + + +
+
+ )} + /> + {editing && ( + <> + ( + +
+ + {t("lookupAiModel")}: + + +
+
+ )} + /> + ( + +
+ + {t("translateAiModel")}: + + +
+
+ )} + /> + ( + +
+ + {t("analyzeAiModel")}: + + +
+
+ )} + /> + ( + +
+ + {t("extractStoryAiModel")}: + + +
+
+ )} + /> + + )} +
+
-
- -
-
+
+ + +
+
+ + ); }; diff --git a/enjoy/src/renderer/components/preferences/google-generative-ai-settings.tsx b/enjoy/src/renderer/components/preferences/google-generative-ai-settings.tsx index 7682a5bc..b1a32636 100644 --- a/enjoy/src/renderer/components/preferences/google-generative-ai-settings.tsx +++ b/enjoy/src/renderer/components/preferences/google-generative-ai-settings.tsx @@ -38,7 +38,7 @@ export const GoogleGenerativeAiSettings = () => { ref={ref} type="password" defaultValue={googleGenerativeAi?.key} - placeholder="*********" + placeholder="" disabled={!editing} className="focus-visible:outline-0 focus-visible:ring-0 shadow-none" /> diff --git a/enjoy/src/renderer/components/preferences/openai-settings.tsx b/enjoy/src/renderer/components/preferences/openai-settings.tsx index d1ddd18c..8869cb42 100644 --- a/enjoy/src/renderer/components/preferences/openai-settings.tsx +++ b/enjoy/src/renderer/components/preferences/openai-settings.tsx @@ -8,17 +8,10 @@ import { Form, FormItem, FormLabel, - FormControl, FormMessage, Input, toast, - Select, - SelectTrigger, - SelectItem, - SelectValue, - SelectContent, } from "@renderer/components/ui"; -import { GPT_PROVIDERS } from "@renderer/components"; import { AISettingsProviderContext } from "@renderer/context"; import { useContext, useState } from "react"; @@ -28,7 +21,6 @@ export const OpenaiSettings = () => { const openAiConfigSchema = z.object({ key: z.string().optional(), - model: z.enum(GPT_PROVIDERS.openai.models), baseUrl: z.string().optional(), }); @@ -36,7 +28,6 @@ export const OpenaiSettings = () => { resolver: zodResolver(openAiConfigSchema), values: { key: openai?.key, - model: openai?.model, baseUrl: openai?.baseUrl, }, }); @@ -55,7 +46,7 @@ export const OpenaiSettings = () => {
Open AI
-
+
{ @@ -75,45 +66,15 @@ export const OpenaiSettings = () => { )} /> - ( - -
- {t("model")}: - -
- -
- )} - /> (
- {t("baseUrl")}: + + {t("baseUrl")}: + {
{t("basicSettings")}
- - @@ -69,6 +67,8 @@ export const Preferences = () => {
+ + diff --git a/enjoy/src/renderer/context/ai-settings-provider.tsx b/enjoy/src/renderer/context/ai-settings-provider.tsx index 4abc1da2..aaf25276 100644 --- a/enjoy/src/renderer/context/ai-settings-provider.tsx +++ b/enjoy/src/renderer/context/ai-settings-provider.tsx @@ -10,9 +10,8 @@ type AISettingsProviderState = { setOpenai?: (config: LlmProviderType) => void; googleGenerativeAi?: LlmProviderType; setGoogleGenerativeAi?: (config: LlmProviderType) => void; - defaultEngine?: string; - setDefaultEngine?: (engine: string) => void; - currentEngine?: LlmProviderType; + setGptEngine?: (engine: GptEngineSettingType) => void; + currentEngine?: GptEngineSettingType; }; const initialState: AISettingsProviderState = {}; @@ -25,12 +24,17 @@ export const AISettingsProvider = ({ }: { children: React.ReactNode; }) => { - const [defaultEngine, setDefaultEngine] = useState("openai"); + const [gptEngine, setGptEngine] = useState({ + name: "enjoyai", + models: { + default: "gpt-4o", + }, + }); const [openai, setOpenai] = useState(null); const [googleGenerativeAi, setGoogleGenerativeAi] = useState(null); const [whisperConfig, setWhisperConfig] = useState(null); - const { EnjoyApp, apiUrl, user, libraryPath } = useContext( + const { EnjoyApp, libraryPath, user, apiUrl } = useContext( AppSettingsProviderContext ); @@ -79,15 +83,39 @@ export const AISettingsProvider = ({ } const _defaultEngine = await EnjoyApp.settings.getDefaultEngine(); - if (_defaultEngine) { - setDefaultEngine(_defaultEngine); + const _gptEngine = await EnjoyApp.settings.getGptEngine(); + if (_gptEngine) { + setGptEngine(_gptEngine); + } else if (_defaultEngine) { + // Migrate default engine to gpt engine + const engine = { + name: _defaultEngine, + models: { + default: "gpt-4o", + }, + }; + EnjoyApp.settings.setGptEngine(engine).then(() => { + setGptEngine(engine); + }); } else if (_openai?.key) { - EnjoyApp.settings.setDefaultEngine("openai").then(() => { - setDefaultEngine("openai"); + const engine = { + name: "openai", + models: { + default: "gpt-4o", + }, + }; + EnjoyApp.settings.setGptEngine(engine).then(() => { + setGptEngine(engine); }); } else { - EnjoyApp.settings.setDefaultEngine("enjoyai").then(() => { - setDefaultEngine("enjoyai"); + const engine = { + name: "enjoyai", + models: { + default: "gpt-4o", + }, + }; + EnjoyApp.settings.setGptEngine(engine).then(() => { + setGptEngine(engine); }); } }; @@ -114,20 +142,21 @@ export const AISettingsProvider = ({ return ( { - EnjoyApp.settings.setDefaultEngine(engine).then(() => { - setDefaultEngine(engine); + setGptEngine: (engine: GptEngineSettingType) => { + EnjoyApp.settings.setGptEngine(engine).then(() => { + setGptEngine(engine); }); }, - currentEngine: { - openai: openai, - enjoyai: { - name: "enjoyai" as LlmProviderType["name"], - key: user?.accessToken, - baseUrl: `${apiUrl}/api/ai`, - }, - }[defaultEngine], + currentEngine: + gptEngine.name === "openai" + ? Object.assign(gptEngine, { + key: openai.key, + baseUrl: openai.baseUrl, + }) + : Object.assign(gptEngine, { + key: user?.accessToken, + baseUrl: `${apiUrl}/api/ai`, + }), openai, setOpenai: (config: LlmProviderType) => handleSetLlm("openai", config), googleGenerativeAi, diff --git a/enjoy/src/renderer/hooks/use-ai-command.tsx b/enjoy/src/renderer/hooks/use-ai-command.tsx index 7c36a096..7faa5183 100644 --- a/enjoy/src/renderer/hooks/use-ai-command.tsx +++ b/enjoy/src/renderer/hooks/use-ai-command.tsx @@ -38,6 +38,9 @@ export const useAiCommand = () => { return lookup; } + const modelName = + currentEngine.models.lookup || currentEngine.models.default; + const res = await lookupCommand( { word, @@ -46,12 +49,13 @@ export const useAiCommand = () => { }, { key: currentEngine.key, - modelName: currentEngine.model, + modelName, baseUrl: currentEngine.baseUrl, } ); - if (res.context_translation?.trim()) { + // Accept result from gpt-3/4 models + if (modelName.match(/^gpt-(3|4)\S*/i) && res.context_translation?.trim()) { return webApi.updateLookup(lookup.id, { meaning: res, sourceId, @@ -61,17 +65,17 @@ export const useAiCommand = () => { }; const extractStory = async (story: StoryType) => { - return extractStoryCommand(story.content, { + const res = await extractStoryCommand(story.content, { key: currentEngine.key, - modelName: currentEngine.model, + modelName: + currentEngine.models.extractStory || currentEngine.models.default, baseUrl: currentEngine.baseUrl, - }).then((res) => { - const { words = [], idioms = [] } = res; + }); + const { words = [], idioms = [] } = res; - return webApi.extractVocabularyFromStory(story.id, { - words, - idioms, - }); + return webApi.extractVocabularyFromStory(story.id, { + words, + idioms, }); }; @@ -81,7 +85,7 @@ export const useAiCommand = () => { ): Promise => { return translateCommand(text, { key: currentEngine.key, - modelName: currentEngine.model, + modelName: currentEngine.models.translate || currentEngine.models.default, baseUrl: currentEngine.baseUrl, }).then((res) => { if (cacheKey) { @@ -92,22 +96,22 @@ export const useAiCommand = () => { }; const analyzeText = async (text: string, cacheKey?: string) => { - return analyzeCommand(text, { + const res = await analyzeCommand(text, { key: currentEngine.key, - modelName: currentEngine.model, + modelName: currentEngine.models.analyze || currentEngine.models.default, baseUrl: currentEngine.baseUrl, - }).then((res) => { - if (cacheKey) { - EnjoyApp.cacheObjects.set(cacheKey, res); - } - return res; }); + + if (cacheKey) { + EnjoyApp.cacheObjects.set(cacheKey, res); + } + return res; }; const punctuateText = async (text: string) => { return punctuateCommand(text, { key: currentEngine.key, - modelName: currentEngine.model, + modelName: currentEngine.models.default, baseUrl: currentEngine.baseUrl, }); }; @@ -115,7 +119,7 @@ export const useAiCommand = () => { const summarizeTopic = async (text: string) => { return summarizeTopicCommand(text, { key: currentEngine.key, - modelName: currentEngine.model, + modelName: currentEngine.models.default, baseUrl: currentEngine.baseUrl, }); }; diff --git a/enjoy/src/renderer/hooks/use-conversation.tsx b/enjoy/src/renderer/hooks/use-conversation.tsx index c93f4c4f..c72352ae 100644 --- a/enjoy/src/renderer/hooks/use-conversation.tsx +++ b/enjoy/src/renderer/hooks/use-conversation.tsx @@ -39,7 +39,7 @@ export const useConversation = () => { configuration: { baseURL: `${apiUrl}/api/ai`, }, - maxRetries: 2, + maxRetries: 0, modelName: model, temperature, maxTokens, @@ -55,7 +55,7 @@ export const useConversation = () => { configuration: { baseURL: baseUrl || openai.baseUrl, }, - maxRetries: 2, + maxRetries: 0, modelName: model, temperature, maxTokens, diff --git a/enjoy/src/renderer/pages/conversations.tsx b/enjoy/src/renderer/pages/conversations.tsx index 4a189da7..e2b26476 100644 --- a/enjoy/src/renderer/pages/conversations.tsx +++ b/enjoy/src/renderer/pages/conversations.tsx @@ -22,7 +22,6 @@ import { } from "@renderer/context"; import { conversationsReducer } from "@renderer/reducers"; import { CONVERSATION_PRESETS } from "@/constants"; -import { set } from "lodash"; export default () => { const [searchParams] = useSearchParams(); @@ -124,9 +123,10 @@ export default () => { name: t("custom"), configuration: { type: "gpt", - engine: currentEngine?.name || "enjoyai", + engine: currentEngine.name, + model: currentEngine.models.default, tts: { - engine: currentEngine?.name || "enjoyai", + engine: currentEngine.name, }, }, }; @@ -137,7 +137,7 @@ export default () => { configuration: { type: "tts", tts: { - engine: currentEngine?.name || "enjoyai", + engine: currentEngine.name, }, }, }; @@ -150,12 +150,13 @@ export default () => { presets = gptPresets; defaultGpt.key = "custom"; defaultGpt.name = t("custom"); - defaultGpt.engine = currentEngine?.name || "enjoyai"; - defaultGpt.configuration.tts.engine = currentEngine?.name || "enjoyai"; + defaultGpt.engine = currentEngine.name; + defaultGpt.configuration.model = currentEngine.models.default; + defaultGpt.configuration.tts.engine = currentEngine.name; defaultGptPreset = defaultGpt; - defaultTts.engine = currentEngine?.name || "enjoyai"; - defaultTts.configuration.tts.engine = currentEngine?.name || "enjoyai"; + defaultTts.engine = currentEngine.name; + defaultTts.configuration.tts.engine = currentEngine.name; defaultTtsPreset = defaultTts; } catch (error) { console.error(error); @@ -166,9 +167,10 @@ export default () => { engine: currentEngine?.name, configuration: { ...preset.configuration, + model: currentEngine.models.default, tts: { ...preset.configuration.tts, - engine: currentEngine?.name, + engine: currentEngine.name, }, }, }) diff --git a/enjoy/src/types/enjoy-app.d.ts b/enjoy/src/types/enjoy-app.d.ts index 1967c990..1766af22 100644 --- a/enjoy/src/types/enjoy-app.d.ts +++ b/enjoy/src/types/enjoy-app.d.ts @@ -111,7 +111,9 @@ type EnjoyAppType = { setUser: (user: UserType) => Promise; getUserDataPath: () => Promise; getDefaultEngine: () => Promise; - setDefaultEngine: (engine: "enjoyai" | "openai") => Promise; + setDefaultEngine: (string) => Promise; + getGptEngine: () => Promise; + setGptEngine: (GptEngineSettingType) => Promise; getLlm: (provider: SupportedLlmProviderType) => Promise; setLlm: ( provider: SupportedLlmProviderType, diff --git a/enjoy/src/types/index.d.ts b/enjoy/src/types/index.d.ts index 2067ef5c..0e653d9e 100644 --- a/enjoy/src/types/index.d.ts +++ b/enjoy/src/types/index.d.ts @@ -161,3 +161,16 @@ type YoutubeVideoType = { videoId: string; duration: string; }; + +type GptEngineSettingType = { + name: string; + models: { + default: string; + lookup?: string; + translate?: string; + analyze?: string; + extractStory?: string; + }; + baseUrl?: string; + key?: string; +};