Files
Reactive-Resume/apps/client/src/pages/dashboard/settings/_sections/openai.tsx
2025-10-01 11:11:59 +02:00

326 lines
10 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { FloppyDiskIcon, TrashSimpleIcon } from "@phosphor-icons/react";
import {
Alert,
Button,
Checkbox,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@reactive-resume/ui";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
DEFAULT_AZURE_API_VERSION,
DEFAULT_MAX_TOKENS,
DEFAULT_MODEL,
} from "@/client/constants/llm";
import { useOpenAiStore } from "@/client/stores/openai";
const formSchema = z.object({
apiKey: z
.string()
// eslint-disable-next-line lingui/no-unlocalized-strings
.min(1, "API key cannot be empty.")
.default(""),
baseURL: z
.string()
// eslint-disable-next-line lingui/no-unlocalized-strings
.regex(/^https?:\/\/\S+$/, "That doesn't look like a valid URL")
.or(z.literal(""))
.default(""),
model: z.string().default(DEFAULT_MODEL),
maxTokens: z.number().default(DEFAULT_MAX_TOKENS),
isAzure: z.boolean().default(false),
azureApiVersion: z.string().default(DEFAULT_AZURE_API_VERSION),
});
type FormValues = z.infer<typeof formSchema>;
export const OpenAISettings = () => {
const {
apiKey,
setApiKey,
baseURL,
setBaseURL,
model,
setModel,
maxTokens,
setMaxTokens,
isAzure,
setIsAzure,
azureApiVersion,
setAzureApiVersion,
} = useOpenAiStore();
const isEnabled = !!apiKey;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
apiKey: apiKey ?? "",
baseURL: baseURL ?? "",
model: model ?? DEFAULT_MODEL,
maxTokens: maxTokens ?? DEFAULT_MAX_TOKENS,
isAzure,
azureApiVersion: azureApiVersion ?? DEFAULT_AZURE_API_VERSION,
},
});
const onSubmit = ({
apiKey,
baseURL,
model,
maxTokens,
isAzure,
azureApiVersion,
}: FormValues) => {
setApiKey(apiKey);
setIsAzure(isAzure);
if (baseURL) {
setBaseURL(baseURL);
}
if (model) {
setModel(model);
}
if (maxTokens) {
setMaxTokens(maxTokens);
}
if (azureApiVersion) {
setAzureApiVersion(azureApiVersion);
}
};
const onRemove = () => {
setApiKey(null);
setBaseURL(null);
setModel(DEFAULT_MODEL);
setMaxTokens(DEFAULT_MAX_TOKENS);
setIsAzure(false);
setAzureApiVersion(DEFAULT_AZURE_API_VERSION);
form.reset({
apiKey: "",
baseURL: "",
model: DEFAULT_MODEL,
maxTokens: DEFAULT_MAX_TOKENS,
isAzure: false,
azureApiVersion: DEFAULT_AZURE_API_VERSION,
});
};
return (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`OpenAI/Azure OpenAI/Ollama Integration`}</h3>
<p className="leading-relaxed opacity-75">
{t`You can make use of the OpenAI API, Azure OpenAI, or Ollama to help you generate content, or improve your writing while composing your resume.`}
</p>
</div>
<div className="prose prose-sm prose-zinc max-w-full dark:prose-invert">
<p>
<Trans>
You have the option to{" "}
<a
target="_blank"
rel="noopener noreferrer nofollow"
href="https://www.howtogeek.com/885918/how-to-get-an-openai-api-key/"
>
obtain your own OpenAI API key
</a>
. This key empowers you to leverage the API as you see fit. Alternatively, if you wish
to disable the AI features in Reactive Resume altogether, you can simply remove the key
from your settings.
</Trans>
</p>
<p>
<Trans>
You can also integrate with Azure OpenAI by enabling the <code>Use Azure OpenAI</code>{" "}
checkbox and setting the Resource URL to your Azure OpenAI resource:{" "}
<code>https://your-resource.openai.azure.com</code>. Set the deployment name in the
Model field and specify the appropriate API version for your Azure deployment.
</Trans>
</p>
<p>
<Trans>
You can also integrate with Ollama simply by setting the API key to
<code>sk-1234567890abcdef</code> and the Base URL to your Ollama URL, i.e.
<code>http://localhost:11434/v1</code>. You can also pick and choose models and set the
max tokens as per your preference.
</Trans>
</p>
</div>
<Form {...form}>
<form className="grid gap-6 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<FormField
name="apiKey"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t`OpenAI/Ollama API Key`}</FormLabel>
<FormControl>
<Input type="password" placeholder="sk-..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="baseURL"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>
{form.watch("isAzure") ? t`Azure OpenAI Resource URL` : t`Base URL`}
</FormLabel>
<FormControl>
<Input
type="text"
placeholder={
form.watch("isAzure")
? "https://your-resource.openai.azure.com"
: "http://localhost:11434/v1"
}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="model"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{form.watch("isAzure") ? t`Deployment Name` : t`Model`}</FormLabel>
<FormControl>
<Input type="text" placeholder={DEFAULT_MODEL} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="maxTokens"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t`Max Tokens`}</FormLabel>
<FormControl>
<Input
type="number"
placeholder={`${DEFAULT_MAX_TOKENS}`}
{...field}
onChange={(e) => {
field.onChange(e.target.valueAsNumber);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="isAzure"
control={form.control}
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(value) => {
field.onChange(Boolean(value));
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>{t`Use Azure OpenAI`}</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="azureApiVersion"
control={form.control}
render={({ field }) => (
<FormItem className={form.watch("isAzure") ? "" : "opacity-50"}>
<FormLabel>{t`Azure API Version`}</FormLabel>
<FormControl>
<Input
type="text"
placeholder={DEFAULT_AZURE_API_VERSION}
disabled={!form.watch("isAzure")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center space-x-2 self-end sm:col-start-2">
<Button type="submit" disabled={!form.formState.isValid}>
{isEnabled && <FloppyDiskIcon className="mr-2" />}
{isEnabled ? t`Saved` : t`Save Locally`}
</Button>
{isEnabled && (
<Button type="reset" variant="ghost" onClick={onRemove}>
<TrashSimpleIcon className="mr-2" />
{t`Forget`}
</Button>
)}
</div>
</form>
</Form>
<div className="prose prose-sm prose-zinc max-w-full dark:prose-invert">
<p>
<Trans>
Your API key is securely stored in the browser's local storage and is only utilized when
making requests to OpenAI via their official SDK. Rest assured that your key is not
transmitted to any external server except when interacting with OpenAI's services.
</Trans>
</p>
</div>
<Alert variant="warning">
<div className="prose prose-neutral max-w-full text-xs leading-relaxed text-primary dark:prose-invert">
<Trans>
<span className="font-medium">Note: </span>
By utilizing the OpenAI API, you acknowledge and accept the{" "}
<a
href="https://openai.com/policies/terms-of-use"
rel="noopener noreferrer nofollow"
target="_blank"
>
terms of use
</a>{" "}
and{" "}
<a
href="https://openai.com/policies/privacy-policy"
rel="noopener noreferrer nofollow"
target="_blank"
>
privacy policy
</a>{" "}
outlined by OpenAI. Please note that Reactive Resume bears no responsibility for any
improper or unauthorized utilization of the service, and any resulting repercussions or
liabilities solely rest on the user.
</Trans>
</div>
</Alert>
</div>
);
};