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.
103 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
}
|