// pages.jsx — non-kiosk page components. // MarkdownPage fetches from GitHub raw, parses via marked CDN, // sanitizes via DOMPurify CDN before injecting. // DashboardPage renders KPI cards + chart + activity feed + Liquid-style chat bar. const { useState: usePageState, useEffect: usePageEffect, useRef: usePageRef } = React; const RAW_BASE = "https://raw.githubusercontent.com/carrickcheah/ai-feedme/main/"; const CHAT_API_URL = (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") ? "http://localhost:8002/api/chat/sync" : "/api/chat/sync"; function renderMarkdownSafely(md) { if (!window.marked) return { __html: "" }; const raw = window.marked.parse(md); const clean = window.DOMPurify ? window.DOMPurify.sanitize(raw) : raw; return { __html: clean }; } // ─── SVG page (architecture diagrams etc) ────────────────────── // Cache-bust the SVG URL with a per-page-load timestamp so iterative // diagram edits surface immediately without forcing a hard-reload. function SvgPage({ src, title }) { const cb = usePageRef(Date.now()).current; const url = `${RAW_BASE}${src}?cb=${cb}`; return (
{title &&

{title}

}
{title
); } // ─── Markdown page ────────────────────────────────────────────── function MarkdownPage({ file, title }) { const [md, setMd] = usePageState(""); const [error, setError] = usePageState(null); usePageEffect(() => { if (!file) return; setMd(""); setError(null); fetch(RAW_BASE + file) .then((r) => r.ok ? r.text() : Promise.reject(new Error("HTTP " + r.status))) .then(setMd) .catch((e) => setError(e.message)); }, [file]); return (
{title &&

{title}

} {error ? (
Could not load {file}: {error}
) : md ? (
) : (
Loading {file}…
)}
); } // ─── Liquid-style sticky chat bar with pop-up thread ──────────── function DashboardChatBar({ agentLabel }) { const [input, setInput] = usePageState(""); const [messages, setMessages] = usePageState([]); const [loading, setLoading] = usePageState(false); const [sessionId, setSessionId] = usePageState(null); const [open, setOpen] = usePageState(false); const threadRef = usePageRef(null); const inputRef = usePageRef(null); // Auto-scroll thread on new content. usePageEffect(() => { if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight; }, [messages, loading]); // Refocus the input after the LLM response lands, so the user can // immediately type the next message without re-clicking. usePageEffect(() => { if (!loading && open && inputRef.current) { const id = setTimeout(() => inputRef.current && inputRef.current.focus(), 50); return () => clearTimeout(id); } }, [loading, open]); const send = async () => { const text = input.trim(); if (!text || loading) return; setInput(""); setMessages((m) => [...m, { role: "user", text }]); setOpen(true); setLoading(true); try { const res = await fetch(CHAT_API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: text, session_id: sessionId, channel: "web" }), }); if (!res.ok) throw new Error("HTTP " + res.status); const data = await res.json(); if (data.session_id) setSessionId(data.session_id); setMessages((m) => [...m, { role: "assistant", text: data.output || "(no response)" }]); } catch (err) { setMessages((m) => [...m, { role: "assistant", text: "Connection error: " + (err.message || err) }]); } finally { setLoading(false); } }; const onKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }; return ( {open && (messages.length > 0 || loading) && (
Chat with {agentLabel}
{messages.map((m, i) => (
{m.text}
))} {loading &&
}
)}
setInput(e.target.value)} onFocus={() => { if (messages.length > 0) setOpen(true); }} onKeyDown={onKeyDown} placeholder={loading ? "Waiting for response…" : `Chat with ${agentLabel}…`} />
); } // ─── KPI card grid + chart + activity feed dashboard ──────────── // Small inline icon set — stroke-based, scale to currentColor. const KPI_ICONS = { ticket: , clock: , check: , queue: , pkg: , alert: , truck: , off: , }; function TrendDelta({ trend }) { if (!trend) return null; const dir = trend.dir || "flat"; const arrow = dir === "up" ? "↑" : dir === "down" ? "↓" : "→"; const good = (trend.good !== undefined) ? trend.good : (dir === "up"); const cls = "fm-kpi-trend " + (dir === "flat" ? "flat" : (good ? "good" : "bad")); return (
{arrow} {trend.delta} {trend.vs && {trend.vs}}
); } function KPI({ icon, value, label, tone, trend }) { return (
{icon && KPI_ICONS[icon] && (
{KPI_ICONS[icon]}
)}
{value}
{label}
); } const CHART_PX = 130; // max bar height in pixels — keeps bars visible regardless of container layout function barTone(v, scale) { // For 0–100 scales: <25 = red, 25–50 = amber, >50 = healthy. For other scales, // fall back to "neutral" so the chart stays orange-brand by default. if (!scale || scale !== "percent") return "neutral"; if (v < 25) return "low"; if (v < 50) return "mid"; return "ok"; } function BarChart({ title, values, labels, scale }) { const max = Math.max(...values, 1); return (
{title}
{values.map((v, i) => { const tone = barTone(v, scale); const px = Math.max(4, Math.round((v / max) * CHART_PX)); return (
{v}{scale === "percent" ? "%" : ""}
{labels[i]}
); })}
); } function ActivityFeed({ title, rows }) { return (
{title}
{rows.map((r, i) => (
{r.time} {r.status && ( {r.status} )} {r.id} {r.text}
))}
); } const ADMIN_API_BASE = (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") ? "http://localhost:8002/api/admin" : "/api/admin"; /** * DashboardPage now accepts a `statsUrl` to fetch live data from. The * static props (agent, tagline, agentLabel) come from the parent; KPIs, * chart, activity, and status are server-fed and refresh every 30s. */ function DashboardPage({ agent, tagline, agentLabel, statsUrl, initial }) { const [data, setData] = usePageState(initial || {}); const [loading, setLoading] = usePageState(true); const [error, setError] = usePageState(null); const load = () => { if (!statsUrl) return; fetch(ADMIN_API_BASE + statsUrl) .then((r) => r.ok ? r.json() : Promise.reject(new Error("HTTP " + r.status))) .then((d) => { setData(d); setError(null); setLoading(false); }) .catch((e) => { setError(e.message); setLoading(false); }); }; usePageEffect(() => { load(); const id = setInterval(load, 30_000); return () => clearInterval(id); }, [statsUrl]); const kpis = data.kpis || initial?.kpis || []; const chart = data.chart || initial?.chart; const activity = data.activity || initial?.activity || []; const status = data.status || initial?.status; return (

{agent}

{tagline}
{error &&
live data unavailable: {error} — showing seed values
}
{status && (
{status.label}
{status.since}
)}
{kpis.map((k, i) => )}
{chart && }
); } // ─── Mock data for each agent dashboard ───────────────────────── const KITCHEN_DATA = { agent: "Kitchen Agent (Internal Support)", agentLabel: "Kitchen Agent", tagline: "Event-driven · triggers on order.created · MCPs: pos, kitchen-display, supplier", status: { label: "Live", since: "updated 2m ago" }, kpis: [ { icon: "ticket", value: "24", label: "tickets today", trend: { delta: "+12%", dir: "up", vs: "vs yesterday" } }, { icon: "clock", value: "7m 22s", label: "avg cook time", trend: { delta: "-30s", dir: "down", vs: "vs yesterday", good: true } }, { icon: "check", value: "95%", label: "on-time rate", trend: { delta: "+2pp", dir: "up", vs: "vs yesterday" } }, { icon: "queue", value: "3", label: "in queue", tone: "warn", trend: { delta: "near par", dir: "flat" } }, ], chart: { title: "Tickets per hour", labels: ["9a","10a","11a","12p","1p","2p","3p","4p","5p","6p"], values: [1, 2, 3, 4, 6, 8, 6, 3, 2, 1], }, activity: [ { time: "14:32", id: "ORD-9871", text: "Mango Iceyoo × 2", status: "SENT" }, { time: "14:28", id: "ORD-9870", text: "(Any 2) YooYoo Saver", status: "COOKING" }, { time: "14:25", id: "ORD-9869", text: "Korean Chicken Wings (6 pcs)", status: "COOKING" }, { time: "14:21", id: "ORD-9868", text: "Oreo Cheesecake Bingsu", status: "READY" }, { time: "14:17", id: "ORD-9867", text: "Mango Iceyoo × 1", status: "DONE" }, { time: "14:11", id: "ORD-9866", text: "Tutti Frutti Ice Blended", status: "DONE" }, ], }; const INVENTORY_DATA = { agent: "Inventory Agent (Internal Support)", agentLabel: "Inventory Agent", tagline: "Event-driven · triggers on ingredient.consumed · MCP: supplier", status: { label: "Live", since: "updated 1m ago" }, kpis: [ { icon: "pkg", value: "47", label: "ingredients", trend: { delta: "+2", dir: "up", vs: "this week" } }, { icon: "alert", value: "3", label: "below par", tone: "warn", trend: { delta: "+1", dir: "up", vs: "since 1pm", good: false } }, { icon: "truck", value: "2", label: "reorders today", trend: { delta: "on track", dir: "flat" } }, { icon: "off", value: "5", label: "items 86'd", tone: "warn", trend: { delta: "+2", dir: "up", vs: "vs yesterday", good: false } }, ], chart: { title: "Stock levels (% of par)", scale: "percent", labels: ["Mango","Milk","Oreo","Ice","Cream","Chicken","Sugar","Strawberry","Coffee","Mint"], values: [95, 78, 32, 88, 12, 65, 22, 82, 58, 42], }, activity: [ { time: "14:30", id: "ING-204", text: "Cream cheese: 0.5kg / 3kg par", status: "STOCK.LOW" }, { time: "14:25", id: "PO-1142", text: "Mango syrup × 5kg · supplier B", status: "REORDER" }, { time: "13:50", id: "ING-118", text: "Oreo crumbs: 0.2kg / 1kg par", status: "STOCK.LOW" }, { time: "13:45", id: "MENU-22", text: "Oreo Cheesecake Bingsu disabled", status: "AUTO-86" }, { time: "13:40", id: "PO-1141", text: "Chicken wings × 10kg · supplier A", status: "REORDER" }, { time: "13:22", id: "ING-067", text: "Mint leaves: 80g / 200g par", status: "STOCK.LOW" }, ], }; function KitchenAgentPage() { return ; } function InventoryAgentPage() { return ; } // ─── Placeholder for items without a backing doc ──────────────── function ComingSoonPage({ what }) { return (

{what}

This section isn't part of the interview prototype scope. The build artifacts are in the repo:

For interview demo, the eval loop in bun run eval stands in for full CI/CD — it's the test gate that would block a merge.

); } Object.assign(window, { MarkdownPage, SvgPage, KitchenAgentPage, InventoryAgentPage, ComingSoonPage, DashboardPage, DashboardChatBar, });