feat(web): settings page with profile, providers, and models

Add settings page displaying user profile, configured LLM providers
with status badges, and available models table with context window
size, cost, and reasoning capability indicators.

Refs #31

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 08:31:23 -05:00
parent fd4b7c2ba2
commit 3d985accf5
2 changed files with 152 additions and 2 deletions

View File

@@ -0,0 +1,150 @@
'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>
);
}

View File

@@ -33,8 +33,8 @@
| P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 |
| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 |
| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
| P3-005 | in-progress | Phase 3 | Project & mission views — dashboard + PRD viewer | | #30 |
| P3-006 | not-started | Phase 3 | Settings — provider config, profile, integrations | — | #31 |
| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 |
| P3-006 | in-progress | Phase 3 | Settings — provider config, profile, integrations | — | #31 |
| P3-007 | not-started | Phase 3 | Admin panel — user management, RBAC | — | #32 |
| P3-008 | not-started | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
| P4-001 | not-started | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |