learning_ai_notes/web/src/app/(app)/chat/page.tsx
saravanakumardb1 8d484c30d1 feat(web/ui6): migrate dashboard, workspaces, search, chat pages
Phase UI6 — the three highest-traffic operator surfaces plus chat
move off legacy globals onto @bytelyst/ui Card + Badge + Input +
Select + Textarea + Button primitives via the local adapter.

dashboard/page.tsx:
- Welcome card, Saved views card, Quick links card, Operator workflows
  card, Recent note activity card — all section.surface-card → Card.
- All saved-view/quick-link/workflow/note rows: surface-muted with
  inline styles → grid+rounded+bg utility classes with hover state.
- All inline 'badge' spans (scope, status, tags) → Badge with
  semantic variants (workflow status maps to warning/success).

workspaces/page.tsx:
- Saved-views aside, filter section, workspace article rows, error
  banner — all surface-card → Card.
- Filter input → Input. Visibility/owner/tag chips → Badge.
- Workspace-note rows → utility-class hover panels.

search/page.tsx:
- POST /notes/search action chip → Badge.
- Saved searches aside + results pane — both surface-card → Card.
- '+ Save current' button + per-view Remove button — raw <button>
  → Button (size sm, ghost variant for Remove).
- Search input + filter chips + result rows — Input + Badge +
  utility-class panels.

chat/page.tsx:
- Workspace <select> → Select with options=[{value,label}].
- Question <textarea> → Textarea.

Ratchet impact for this commit:
  raw interactive controls       25 → 19  (-6)
  legacy global surface classes  67 → 38  (-29)

Cumulative since session start (38/92 baseline):
  raw   38 → 19  (-19)
  legacy 92 → 38  (-54)

Verified: pnpm typecheck, test (96/96), audit:ui:ratchet at new
baseline.
2026-05-23 01:38:35 -07:00

103 lines
3.8 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { AppShell } from "@/components/AppShell";
import { Badge, Button, Card, Select, Textarea } from "@/components/ui/Primitives";
import { chatOverWorkspace, listWorkspaceSummaries } from "@/lib/notes-client";
import type { WorkspaceSummary } from "@/lib/types";
import { toast } from "@/lib/toast";
export default function ChatPage() {
const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
const [workspaceId, setWorkspaceId] = useState("");
const [message, setMessage] = useState("");
const [answer, setAnswer] = useState<string | null>(null);
const [citations, setCitations] = useState<Array<{ noteId: string; title: string; snippet: string }>>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
void listWorkspaceSummaries().then((w) => {
setWorkspaces(w);
setWorkspaceId((id) => id || w[0]?.id || "");
});
}, []);
async function handleAsk() {
if (!workspaceId.trim() || !message.trim()) return;
setLoading(true);
setAnswer(null);
setCitations([]);
try {
const res = await chatOverWorkspace(workspaceId, message.trim());
setAnswer(res.answer);
setCitations(res.citations);
} catch (e) {
toast.error(e instanceof Error ? e.message : "Chat failed");
} finally {
setLoading(false);
}
}
return (
<AppShell
title="Workspace chat"
description="Retrieval over your notes (citations below). This is not a general-purpose LLM — answers are assembled from indexed note text."
actions={<Badge>Feature-flagged on backend (chat.rag_enabled)</Badge>}
>
<Card style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)", maxWidth: 720 }}>
<label style={{ display: "grid", gap: 8 }}>
<span style={{ fontWeight: 600 }}>Workspace</span>
{workspaces.length === 0 ? (
<p style={{ margin: 0, color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
Create a workspace first (Dashboard seed or Workspaces), then return here.
</p>
) : (
<Select
value={workspaceId}
onChange={(e) => setWorkspaceId(e.target.value)}
options={workspaces.map((w) => ({ value: w.id, label: w.name }))}
/>
)}
</label>
<label style={{ display: "grid", gap: 8 }}>
<span style={{ fontWeight: 600 }}>Question</span>
<Textarea
rows={4}
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="What did we decide about launch?"
/>
</label>
<Button
type="button"
disabled={loading || !workspaceId.trim() || workspaces.length === 0}
loading={loading}
onClick={() => void handleAsk()}
>
Ask
</Button>
{answer ? (
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
<strong>Answer</strong>
<div style={{ whiteSpace: "pre-wrap", lineHeight: 1.6 }}>{answer}</div>
{citations.length ? (
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}>
<strong>Citations</strong>
<ul style={{ margin: 0, paddingLeft: 20 }}>
{citations.map((c) => (
<li key={c.noteId} style={{ marginBottom: 8 }}>
<Link href={`/notes/${c.noteId}`}>{c.title}</Link>
<div style={{ color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>{c.snippet}</div>
</li>
))}
</ul>
</div>
) : null}
</div>
) : null}
</Card>
</AppShell>
);
}