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

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #88.
This commit is contained in:
2026-03-13 13:31:51 +00:00
committed by jason.woltje
parent fd4b7c2ba2
commit e44cb7e56a
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 |