Add web UI with Smokey's crow as the stand-in mascot

- ui/: Vite + plain TypeScript, builds into ./web for `crows serve`
- CrowView mirrors CrowModel in RIVE_CONTRACT.md (pose, trust,
  reactions) so the .riv can replace the SVG later
- Event handling follows the contract's mapping table; server text
  is only ever inserted as text
- Prank: toast plus a send-button cooldown countdown
- art/crow.svg: source art for Rive import

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
SmokeyandClaude Opus 5.5 committed 2026-10-27 11:44:54 -06:00
1 parent 940c74672a
commit a89157803b
12 files changed
+1796

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
// Thin client for the HTTP API in EVENTS.md.
export type PostListResult =
| { ok: true; listId: string }
| { ok: false; pranked: true; message: string; cooldownS: number }
| { ok: false; pranked: false; message: string };
export async function postList(text: string, requester: string): Promise<PostListResult> {
const res = await fetch("/api/lists", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, requester }),
});
const body = await res.json().catch(() => ({}));
if (res.ok) return { ok: true, listId: String(body.list_id) };
if (res.status === 429) return { ok: false, pranked: true, message: String(body.message ?? "Caw."), cooldownS: Number(body.cooldown_s ?? 30) };
return { ok: false, pranked: false, message: String(body.message ?? `The flock said no (${res.status}).`) };
}
export async function postVerdict(itemId: string, target: "triage" | "output", approve: boolean): Promise<void> {
const res = await fetch("/api/verdicts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ item_id: itemId, target, approve }),
});
if (!res.ok) throw new Error(res.status === 404 ? "That question already has an answer." : `Verdict failed (${res.status}).`);
}
export type TrustLevels = Map<string, number>; // "Name|task_type" -> level
export async function getTrust(): Promise<TrustLevels> {
const out: TrustLevels = new Map();
try {
const res = await fetch("/api/trust");
const body = await res.json();
for (const [key, rec] of Object.entries<{ level?: number }>(body.crows ?? {})) out.set(key, rec.level ?? 0);
} catch {
// Badges start at zero if the ledger can't be read.
}
return out;
}
export function listEvents(listId: string): EventSource {
return new EventSource(`/api/events?list_id=${encodeURIComponent(listId)}`);
}