feat: more llm provider options (google, mistral) (#28)

* feat: add google provider

* fix: default for google model

* feat: multiple providers

* fix: defaults from env for login form

* fix: add mistral to env files

* chore: delete unused code

* chore: revert database url to original

* fix: render default value for api key from env on server

* fix: type errors during compilation

---------

Co-authored-by: Vasily Zubarev <me@vas3k.ru>
This commit is contained in:
Dmitrii Anfimov
2025-07-22 21:49:54 +02:00
committed by GitHub
parent 9903325f17
commit dee915ffd6
21 changed files with 1185 additions and 150 deletions

View File

@@ -13,11 +13,20 @@ export async function selfHostedGetStartedAction(formData: FormData) {
await createUserDefaults(user.id)
}
const openaiApiKey = formData.get("openai_api_key")
if (openaiApiKey) {
await updateSettings(user.id, "openai_api_key", openaiApiKey as string)
const apiKeys = [
"openai_api_key",
"google_api_key",
"mistral_api_key"
]
for (const key of apiKeys) {
const value = formData.get(key)
if (value) {
await updateSettings(user.id, key, value as string)
}
}
const defaultCurrency = formData.get("default_currency")
if (defaultCurrency) {
await updateSettings(user.id, "default_currency", defaultCurrency as string)

View File

@@ -1,15 +1,12 @@
import { FormSelectCurrency } from "@/components/forms/select-currency"
import { FormInput } from "@/components/forms/simple"
import { Button } from "@/components/ui/button"
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
import { ColoredText } from "@/components/ui/colored-text"
import config from "@/lib/config"
import { DEFAULT_CURRENCIES, DEFAULT_SETTINGS } from "@/models/defaults"
import { getSelfHostedUser } from "@/models/users"
import { ShieldAlert } from "lucide-react"
import Image from "next/image"
import { redirect } from "next/navigation"
import { selfHostedGetStartedAction } from "../actions"
import { PROVIDERS } from "@/lib/llm-providers"
import SelfHostedSetupFormClient from "./setup-form-client"
export default async function SelfHostedWelcomePage() {
if (!config.selfHosted.isEnabled) {
@@ -35,6 +32,14 @@ export default async function SelfHostedWelcomePage() {
redirect(config.selfHosted.redirectUrl)
}
// Собираем дефолтные ключи для всех провайдеров
const defaultProvider = PROVIDERS[0].key
const defaultApiKeys: Record<string, string> = {
openai: config.ai.openaiApiKey ?? "",
google: config.ai.googleApiKey ?? "",
mistral: config.ai.mistralApiKey ?? "",
}
return (
<Card className="w-full max-w-xl mx-auto p-8 flex flex-col items-center justify-center gap-4">
<Image src="/logo/512.png" alt="Logo" width={144} height={144} className="w-36 h-36" />
@@ -43,36 +48,7 @@ export default async function SelfHostedWelcomePage() {
</CardTitle>
<CardDescription className="flex flex-col gap-4 text-center text-lg">
<p>Welcome to your own instance of TaxHacker. Let&apos;s set up a couple of settings to get started.</p>
<form action={selfHostedGetStartedAction} className="flex flex-col gap-8 pt-8">
<div>
<FormInput title="OpenAI API Key" name="openai_api_key" defaultValue={config.ai.openaiApiKey} />
<small className="text-xs text-muted-foreground">
Get your API key from{" "}
<a
href="https://platform.openai.com/settings/organization/api-keys"
target="_blank"
className="underline"
>
OpenAI Platform Console
</a>
</small>
</div>
<div className="flex flex-row items-center justify-center gap-2">
<FormSelectCurrency
title="Default Currency"
name="default_currency"
defaultValue={DEFAULT_SETTINGS.find((s) => s.code === "default_currency")?.value ?? "EUR"}
currencies={DEFAULT_CURRENCIES}
/>
</div>
<Button type="submit" className="w-auto p-6">
Get Started
</Button>
</form>
<SelfHostedSetupFormClient defaultProvider={defaultProvider} defaultApiKeys={defaultApiKeys} />
</CardDescription>
</Card>
)

View File

@@ -0,0 +1,76 @@
"use client"
import { useState, useRef, useEffect, useCallback } from "react"
import { FormSelectCurrency } from "@/components/forms/select-currency"
import { FormInput } from "@/components/forms/simple"
import { Button } from "@/components/ui/button"
import { DEFAULT_CURRENCIES, DEFAULT_SETTINGS } from "@/models/defaults"
import { selfHostedGetStartedAction } from "../actions"
import { FormSelect } from "@/components/forms/simple"
import { PROVIDERS } from "@/lib/llm-providers"
type Props = {
defaultProvider: string
defaultApiKeys: Record<string, string>
}
export default function SelfHostedSetupFormClient({ defaultProvider, defaultApiKeys }: Props) {
const [provider, setProvider] = useState(defaultProvider)
const selected = PROVIDERS.find(p => p.key === provider)!
const getDefaultApiKey = useCallback((providerKey: string) => defaultApiKeys[providerKey] ?? "", [defaultApiKeys])
const [apiKey, setApiKey] = useState(getDefaultApiKey(provider))
const userTyped = useRef(false)
useEffect(() => {
if (!userTyped.current) {
setApiKey(getDefaultApiKey(provider))
}
userTyped.current = false
}, [provider, getDefaultApiKey])
return (
<form action={selfHostedGetStartedAction} className="flex flex-col gap-8 pt-8">
<div className="flex flex-row gap-4 items-center justify-center">
<FormSelect
title="LLM provider"
name="provider"
value={provider}
onValueChange={setProvider}
items={PROVIDERS.map(p => ({
code: p.key,
name: p.label,
logo: p.logo
}))}
/>
<FormSelectCurrency
title="Default Currency"
name="default_currency"
defaultValue={DEFAULT_SETTINGS.find((s) => s.code === "default_currency")?.value ?? "EUR"}
currencies={DEFAULT_CURRENCIES}
/>
</div>
<div>
<FormInput
title={`${selected.label} API Key`}
name={selected.apiKeyName}
value={apiKey ?? ""}
onChange={e => {
setApiKey(e.target.value)
userTyped.current = true
}}
placeholder={selected.placeholder}
/>
<small className="text-xs text-muted-foreground flex justify-center mt-2">
Get key from
{"\u00A0"}
<a href={selected.help.url} target="_blank" className="underline">
{selected.help.label}
</a>
</small>
</div>
<Button type="submit" className="w-auto p-6">
Get Started
</Button>
</form>
)
}