Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
151 lines
5.4 KiB
TypeScript
151 lines
5.4 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { api } from '@/lib/api';
|
|
import { useSession } from '@/lib/auth-client';
|
|
|
|
interface ProviderInfo {
|
|
name: string;
|
|
enabled: boolean;
|
|
modelCount: number;
|
|
}
|
|
|
|
interface ModelInfo {
|
|
id: string;
|
|
name: string;
|
|
provider: string;
|
|
contextWindow: number;
|
|
reasoning: boolean;
|
|
cost: { input: number; output: number };
|
|
}
|
|
|
|
export default function SettingsPage(): React.ReactElement {
|
|
const { data: session } = useSession();
|
|
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
const [models, setModels] = useState<ModelInfo[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
api<ProviderInfo[]>('/api/providers').catch(() => []),
|
|
api<ModelInfo[]>('/api/providers/models').catch(() => []),
|
|
])
|
|
.then(([p, m]) => {
|
|
setProviders(p);
|
|
setModels(m);
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
return (
|
|
<div className="mx-auto max-w-3xl space-y-8">
|
|
<h1 className="text-2xl font-semibold">Settings</h1>
|
|
|
|
{/* Profile */}
|
|
<section>
|
|
<h2 className="mb-4 text-lg font-medium text-text-secondary">Profile</h2>
|
|
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
|
{session?.user ? (
|
|
<div className="space-y-3">
|
|
<Field label="Name" value={session.user.name ?? '—'} />
|
|
<Field label="Email" value={session.user.email} />
|
|
<Field label="User ID" value={session.user.id} />
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-text-muted">Not signed in</p>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Providers */}
|
|
<section>
|
|
<h2 className="mb-4 text-lg font-medium text-text-secondary">LLM Providers</h2>
|
|
{loading ? (
|
|
<p className="text-sm text-text-muted">Loading providers...</p>
|
|
) : providers.length === 0 ? (
|
|
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
|
<p className="text-sm text-text-muted">No providers configured</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{providers.map((p) => (
|
|
<div
|
|
key={p.name}
|
|
className="flex items-center justify-between rounded-lg border border-surface-border bg-surface-card p-4"
|
|
>
|
|
<div>
|
|
<p className="text-sm font-medium text-text-primary">{p.name}</p>
|
|
<p className="text-xs text-text-muted">{p.modelCount} models available</p>
|
|
</div>
|
|
<span
|
|
className={`rounded-full px-2 py-0.5 text-xs ${
|
|
p.enabled ? 'bg-success/20 text-success' : 'bg-gray-600/20 text-gray-400'
|
|
}`}
|
|
>
|
|
{p.enabled ? 'Active' : 'Disabled'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* Models */}
|
|
<section>
|
|
<h2 className="mb-4 text-lg font-medium text-text-secondary">Available Models</h2>
|
|
{loading ? (
|
|
<p className="text-sm text-text-muted">Loading models...</p>
|
|
) : models.length === 0 ? (
|
|
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
|
<p className="text-sm text-text-muted">No models available</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-hidden rounded-lg border border-surface-border">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
|
<th className="px-4 py-2 font-medium">Model</th>
|
|
<th className="px-4 py-2 font-medium">Provider</th>
|
|
<th className="hidden px-4 py-2 font-medium md:table-cell">Context</th>
|
|
<th className="hidden px-4 py-2 font-medium md:table-cell">Cost (in/out)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{models.map((m) => (
|
|
<tr
|
|
key={`${m.provider}-${m.id}`}
|
|
className="border-b border-surface-border last:border-b-0"
|
|
>
|
|
<td className="px-4 py-2 text-sm text-text-primary">
|
|
{m.name}
|
|
{m.reasoning && (
|
|
<span className="ml-2 text-xs text-purple-400">reasoning</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-2 text-xs text-text-muted">{m.provider}</td>
|
|
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
|
{(m.contextWindow / 1000).toFixed(0)}k
|
|
</td>
|
|
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
|
${m.cost.input} / ${m.cost.output}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Field({ label, value }: { label: string; value: string }): React.ReactElement {
|
|
return (
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-text-muted">{label}</span>
|
|
<span className="text-sm text-text-primary">{value}</span>
|
|
</div>
|
|
);
|
|
}
|