Feat: more settings for ai engine & models (#611)

* may setup gpt ai engine & model

* ai models setting works

* update openai setting

* validate engine setting before save

* fail fast

* clean code

* refactor gpt preset
This commit is contained in:
an-lee
2024-05-15 15:52:07 +08:00
committed by GitHub
parent b4d7fb837e
commit 61c76006fd
16 changed files with 449 additions and 148 deletions

View File

@@ -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<any>(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<z.infer<typeof gptEngineSchema>>({
resolver: zodResolver(gptEngineSchema),
values: {
name: currentEngine.name as "enjoyai" | "openai",
models: currentEngine.models || {},
},
});
const onSubmit = async (data: z.infer<typeof gptEngineSchema>) => {
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 (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="flex items-center mb-2">
<span>{t("defaultAiEngine")}</span>
</div>
<div className="text-sm text-muted-foreground">
{defaultEngine === "openai" && t("openAiEngineTips")}
{defaultEngine === "enjoyai" && t("enjoyAiEngineTips")}
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="flex items-center mb-2">
<span>{t("defaultAiEngine")}</span>
</div>
<div className="text-sm text-muted-foreground space-y-3">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
{t("aiEngine")}:
</FormLabel>
<Select
value={field.value}
disabled={!editing}
onValueChange={(value) => {
if (value === "openai" && !openai?.key) {
toast.warning(t("openaiKeyRequired"));
} else {
field.onChange(value);
}
}}
>
<SelectTrigger className="min-w-fit">
<SelectValue
placeholder={t("defaultAiEngine")}
></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="enjoyai">EnjoyAI</SelectItem>
<SelectItem value="openai">OpenAI</SelectItem>
</SelectContent>
</Select>
</div>
<FormMessage />
<div className="text-xs text-muted-foreground">
{form.watch("name") === "openai" && t("openAiEngineTips")}
{form.watch("name") === "enjoyai" &&
t("enjoyAiEngineTips")}
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="models.default"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
{t("defaultAiModel")}:
</FormLabel>
<Select
value={field.value}
disabled={!editing}
onValueChange={field.onChange}
>
<SelectTrigger className="min-w-fit">
<SelectValue
placeholder={t("defaultAiModel")}
></SelectValue>
</SelectTrigger>
<SelectContent>
{providers[form.watch("name")].models.map(
(model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
</FormItem>
)}
/>
{editing && (
<>
<FormField
control={form.control}
name="models.lookup"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
{t("lookupAiModel")}:
</FormLabel>
<Select
value={field.value}
disabled={!editing}
onValueChange={field.onChange}
>
<SelectTrigger className="min-w-fit">
<SelectValue
placeholder={t("leaveEmptyToUseDefault")}
></SelectValue>
</SelectTrigger>
<SelectContent>
{providers[form.watch("name")].models.map(
(model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="models.translate"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
{t("translateAiModel")}:
</FormLabel>
<Select
value={field.value}
disabled={!editing}
onValueChange={field.onChange}
>
<SelectTrigger className="min-w-fit">
<SelectValue
placeholder={t("leaveEmptyToUseDefault")}
></SelectValue>
</SelectTrigger>
<SelectContent>
{providers[form.watch("name")].models.map(
(model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="models.analyze"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
{t("analyzeAiModel")}:
</FormLabel>
<Select
value={field.value}
disabled={!editing}
onValueChange={field.onChange}
>
<SelectTrigger className="min-w-fit">
<SelectValue
placeholder={t("leaveEmptyToUseDefault")}
></SelectValue>
</SelectTrigger>
<SelectContent>
{providers[form.watch("name")].models.map(
(model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="models.extractStory"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">
{t("extractStoryAiModel")}:
</FormLabel>
<Select
value={field.value}
disabled={!editing}
onValueChange={field.onChange}
>
<SelectTrigger className="min-w-fit">
<SelectValue
placeholder={t("leaveEmptyToUseDefault")}
></SelectValue>
</SelectTrigger>
<SelectContent>
{providers[form.watch("name")].models.map(
(model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
</FormItem>
)}
/>
</>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<Select
value={defaultEngine}
onValueChange={(value) => {
if (value === "openai" && !openai?.key) {
toast.warning(t("openaiKeyRequired"));
} else {
setDefaultEngine(value);
}
}}
>
<SelectTrigger className="min-w-fit">
<SelectValue placeholder={t("defaultAiEngine")}></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="enjoyai">EnjoyAI</SelectItem>
<SelectItem value="openai">OpenAI</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
variant={editing ? "outline" : "secondary"}
size="sm"
type="reset"
onClick={(event) => {
event.preventDefault();
form.reset();
setEditing(!editing);
}}
>
{editing ? t("cancel") : t("edit")}
</Button>
<Button className={editing ? "" : "hidden"} size="sm" type="submit">
{t("save")}
</Button>
</div>
</div>
</form>
</Form>
);
};

View File

@@ -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"
/>

View File

@@ -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 = () => {
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">Open AI</div>
<div className="text-sm text-muted-foreground space-y-1">
<div className="text-sm text-muted-foreground space-y-3">
<FormField
control={form.control}
name="key"
@@ -66,7 +57,7 @@ export const OpenaiSettings = () => {
<Input
disabled={!editing}
type="password"
placeholder="*********"
placeholder=""
value={field.value}
onChange={field.onChange}
/>
@@ -75,45 +66,15 @@ export const OpenaiSettings = () => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="model"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">{t("model")}:</FormLabel>
<Select
disabled={!editing}
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectAiModel")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(GPT_PROVIDERS.openai.models || []).map(
(option: string) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="baseUrl"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<FormLabel className="min-w-max">{t("baseUrl")}:</FormLabel>
<FormLabel className="min-w-max">
{t("baseUrl")}:
</FormLabel>
<Input
disabled={!editing}
placeholder={t("leaveEmptyToUseDefault")}

View File

@@ -29,8 +29,6 @@ export const Preferences = () => {
<div className="font-semibold mb-4 capitilized">
{t("basicSettings")}
</div>
<LibrarySettings />
<Separator />
<WhisperSettings />
<Separator />
<DefaultEngineSettings />
@@ -69,6 +67,8 @@ export const Preferences = () => {
</div>
<UserSettings />
<Separator />
<LibrarySettings />
<Separator />
<EmailSettings />
<Separator />
<BalanceSettings />