feat: add LLM provider settings page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
54
web/src/lib/api/settings.remote.ts
Normal file
54
web/src/lib/api/settings.remote.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { form, query, getRequestEvent } from '$app/server';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { llmConfig } from '$lib/server/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { llmConfigSchema } from '$lib/schema/settings';
|
||||||
|
|
||||||
|
export const getConfig = query(async () => {
|
||||||
|
const { locals } = getRequestEvent();
|
||||||
|
if (!locals.user) return null;
|
||||||
|
|
||||||
|
const config = await db
|
||||||
|
.select()
|
||||||
|
.from(llmConfig)
|
||||||
|
.where(eq(llmConfig.userId, locals.user.id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (config.length === 0) return null;
|
||||||
|
|
||||||
|
// mask the API key for display
|
||||||
|
return {
|
||||||
|
...config[0],
|
||||||
|
apiKey: config[0].apiKey.slice(0, 8) + '...' + config[0].apiKey.slice(-4)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateConfig = form(llmConfigSchema, async (data) => {
|
||||||
|
const { locals } = getRequestEvent();
|
||||||
|
if (!locals.user) return;
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(llmConfig)
|
||||||
|
.where(eq(llmConfig.userId, locals.user.id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
await db
|
||||||
|
.update(llmConfig)
|
||||||
|
.set({
|
||||||
|
provider: data.provider,
|
||||||
|
apiKey: data.apiKey,
|
||||||
|
model: data.model ?? null
|
||||||
|
})
|
||||||
|
.where(eq(llmConfig.userId, locals.user.id));
|
||||||
|
} else {
|
||||||
|
await db.insert(llmConfig).values({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
userId: locals.user.id,
|
||||||
|
provider: data.provider,
|
||||||
|
apiKey: data.apiKey,
|
||||||
|
model: data.model ?? null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
7
web/src/lib/schema/settings.ts
Normal file
7
web/src/lib/schema/settings.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { object, string, pipe, minLength, optional } from 'valibot';
|
||||||
|
|
||||||
|
export const llmConfigSchema = object({
|
||||||
|
provider: pipe(string(), minLength(1)),
|
||||||
|
apiKey: pipe(string(), minLength(1)),
|
||||||
|
model: optional(string())
|
||||||
|
});
|
||||||
62
web/src/routes/dashboard/settings/+page.svelte
Normal file
62
web/src/routes/dashboard/settings/+page.svelte
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getConfig, updateConfig } from '$lib/api/settings.remote';
|
||||||
|
|
||||||
|
const config = await getConfig();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2 class="mb-6 text-2xl font-bold">Settings</h2>
|
||||||
|
|
||||||
|
<div class="max-w-lg rounded-lg border border-neutral-200 p-6">
|
||||||
|
<h3 class="mb-4 font-semibold">LLM Provider</h3>
|
||||||
|
|
||||||
|
<form {...updateConfig} class="space-y-4">
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm text-neutral-600">Provider</span>
|
||||||
|
<select
|
||||||
|
{...updateConfig.fields.provider.as('text')}
|
||||||
|
class="mt-1 block w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="openai">OpenAI</option>
|
||||||
|
<option value="groq">Groq</option>
|
||||||
|
<option value="ollama">Ollama</option>
|
||||||
|
<option value="bedrock">AWS Bedrock</option>
|
||||||
|
<option value="openrouter">OpenRouter</option>
|
||||||
|
</select>
|
||||||
|
{#each updateConfig.fields.provider.issues() ?? [] as issue (issue.message)}
|
||||||
|
<p class="text-sm text-red-600">{issue.message}</p>
|
||||||
|
{/each}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm text-neutral-600">API Key</span>
|
||||||
|
<input
|
||||||
|
{...updateConfig.fields.apiKey.as('password')}
|
||||||
|
placeholder={config?.apiKey ?? 'Enter your API key'}
|
||||||
|
class="mt-1 block w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
{#each updateConfig.fields.apiKey.issues() ?? [] as issue (issue.message)}
|
||||||
|
<p class="text-sm text-red-600">{issue.message}</p>
|
||||||
|
{/each}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm text-neutral-600">Model (optional)</span>
|
||||||
|
<input
|
||||||
|
{...updateConfig.fields.model.as('text')}
|
||||||
|
placeholder="e.g., gpt-4o, llama-3.3-70b-versatile"
|
||||||
|
class="mt-1 block w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="submit" class="rounded bg-neutral-800 px-4 py-2 text-sm text-white hover:bg-neutral-700">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if config}
|
||||||
|
<p class="mt-4 text-sm text-neutral-500">
|
||||||
|
Current: {config.provider} · Key: {config.apiKey}
|
||||||
|
{#if config.model} · Model: {config.model}{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user