feat(dogfood-wave1): Studio retries, Chat error, LLM gateway, kill snapshot, topbar cleanup (#14)
This commit was merged in pull request #14.
This commit is contained in:
+2
-19
@@ -28,26 +28,13 @@ export default function Agent() {
|
||||
role: "agent",
|
||||
ok: true,
|
||||
text:
|
||||
`Hi ${userEmail.split("@")[0]}. I'm the FlowMaster Command Assistant. I understand a fixed set of commands and run them against EA2 — navigate, list processes, start a process, message a teammate, remember a note, recall notes. Memory is text-only with keyword recall; an LLM backend is a separate component, off by default.`,
|
||||
`Hi ${userEmail.split("@")[0]}. I'm the FlowMaster Command Assistant. Tell me what to do in plain English — start a process, list what's published, message a teammate, jump to a hub, remember a note. I run on EA2 and the FlowMaster LLM gateway.`,
|
||||
},
|
||||
]);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [working, setWorking] = useState(false);
|
||||
const [llmState, setLlmState] = useState<"unknown" | "ready" | "off">("unknown");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetch("/internal/canvas-llm/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ messages: [{ role: "user", content: "probe" }], max_tokens: 1 }),
|
||||
})
|
||||
.then((r) => { if (!cancelled) setLlmState(r.status === 503 ? "off" : r.ok ? "ready" : "off"); })
|
||||
.catch(() => { if (!cancelled) setLlmState("off"); });
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }), 50);
|
||||
}, [turns]);
|
||||
@@ -78,11 +65,7 @@ export default function Agent() {
|
||||
<header className="agent-side-head">
|
||||
<div className="mc-hero-eyebrow"><Bot size={12} /> FlowMaster Command Assistant</div>
|
||||
<h2 className="mc-hero-title">Tell the cockpit what to do</h2>
|
||||
<p className="agent-side-sub">Deterministic command router with EA2-backed text memory.</p>
|
||||
<div className="agent-llm-state">
|
||||
<span className={`agent-llm-pill agent-llm-${llmState}`}>LLM provider · {llmState === "ready" ? "ON" : llmState === "off" ? "OFF" : "…"}</span>
|
||||
{llmState === "off" && <span className="agent-llm-hint">Natural language replies are not enabled in this environment.</span>}
|
||||
</div>
|
||||
<p className="agent-side-sub">Tools + plain-language replies, both backed by EA2.</p>
|
||||
</header>
|
||||
<div className="agent-tool-list">
|
||||
<div className="agent-tool-label">CAPABILITIES</div>
|
||||
|
||||
+17
-4
@@ -52,9 +52,11 @@ export default function Chat() {
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const [threadsErr, setThreadsErr] = useState<string | null>(null);
|
||||
const reloadThreads = () => {
|
||||
let cancelled = false;
|
||||
setLoadingThreads(true);
|
||||
setThreadsErr(null);
|
||||
chatApi
|
||||
.listThreads(me)
|
||||
.then((rows) => {
|
||||
@@ -62,12 +64,16 @@ export default function Chat() {
|
||||
setThreads(rows);
|
||||
if (!activeKey && rows.length > 0) setActiveKey(rows[0]._key);
|
||||
})
|
||||
.catch((err) => pushToast("err", `Threads failed: ${err.message}`))
|
||||
.catch((err) => {
|
||||
setThreadsErr(err.message);
|
||||
pushToast("err", `Threads failed: ${err.message}`);
|
||||
})
|
||||
.finally(() => !cancelled && setLoadingThreads(false));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
useEffect(() => reloadThreads(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeKey) return;
|
||||
@@ -202,7 +208,14 @@ export default function Chat() {
|
||||
<div className="chat-unread-summary">{unreadCount} unread thread{unreadCount === 1 ? "" : "s"}</div>
|
||||
)}
|
||||
{loadingThreads && <div className="chat-empty">Loading threads…</div>}
|
||||
{!loadingThreads && threads.length === 0 && <div className="chat-empty">No conversations yet. Start one above.</div>}
|
||||
{!loadingThreads && threadsErr && (
|
||||
<div className="chat-empty chat-err">
|
||||
<strong>Could not load threads.</strong>
|
||||
<div>{threadsErr}</div>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => reloadThreads()}>Try again</button>
|
||||
</div>
|
||||
)}
|
||||
{!loadingThreads && !threadsErr && threads.length === 0 && <div className="chat-empty">No conversations yet. Start one above.</div>}
|
||||
{threads.map((t) => {
|
||||
const preview = previews[t._key];
|
||||
const lastFromOther = preview && preview.author_email !== me;
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("Login Scene", () => {
|
||||
fireEvent.click(screen.getAllByRole("button", { name: /^SIGN IN$/ })[0]);
|
||||
await waitFor(() => {
|
||||
expect(mockLoginAs).toHaveBeenCalledWith('test@example.com', 'password123', { method: 'password' });
|
||||
expect(mockSetScene).toHaveBeenCalledWith('landing');
|
||||
expect(mockSetScene).toHaveBeenCalledWith('mission');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Login() {
|
||||
setError(null);
|
||||
try {
|
||||
await loginAs(email, password, { method: "password" });
|
||||
setScene("landing");
|
||||
setScene("mission");
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
@@ -77,7 +77,7 @@ export default function Login() {
|
||||
setError(null);
|
||||
try {
|
||||
await loginAs(email, undefined, { method: "dev" });
|
||||
setScene("landing");
|
||||
setScene("mission");
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
@@ -237,7 +237,7 @@ export default function Login() {
|
||||
setError(null);
|
||||
try {
|
||||
await loginAs(p.email, undefined, { method: "dev" });
|
||||
setScene("landing");
|
||||
setScene("mission");
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
|
||||
+8
-13
@@ -21,8 +21,8 @@ export default function Settings() {
|
||||
const setTheme = useApp((s) => s.setTheme);
|
||||
const consoleOpen = useApp((s) => s.consoleOpen);
|
||||
const setConsoleOpen = useApp((s) => s.setConsoleOpen);
|
||||
const mode = useApp((s) => s.mode);
|
||||
const setMode = useApp((s) => s.setMode);
|
||||
|
||||
|
||||
const refreshLive = useApp((s) => s.refreshLive);
|
||||
const pushToast = useApp((s) => s.pushToast);
|
||||
|
||||
@@ -108,20 +108,15 @@ export default function Settings() {
|
||||
</section>
|
||||
|
||||
<section className="studio-panel">
|
||||
<h3 className="panel-h"><Pulse size={12} /> Data mode & polling</h3>
|
||||
<h3 className="panel-h"><Pulse size={12} /> Live data</h3>
|
||||
<div className="i-field">
|
||||
<span>Current mode</span>
|
||||
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
|
||||
<span>Backend</span>
|
||||
<span className="mono">EA2 · live always</span>
|
||||
</div>
|
||||
<div className="settings-row">
|
||||
<button className="btn btn-primary" onClick={() => setMode(mode === "live" ? "snapshot" : "live")} disabled={signingIn}>
|
||||
<Refresh size={12} /> {mode === "live" ? "Switch to snapshot" : "Switch to live"}
|
||||
<button className="btn btn-ghost" onClick={refreshLive} disabled={signingIn}>
|
||||
<Refresh size={12} /> Refresh now
|
||||
</button>
|
||||
{mode === "live" && (
|
||||
<button className="btn btn-ghost" onClick={refreshLive}>
|
||||
<Refresh size={12} /> Refresh now
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<label className="studio-field" style={{ marginTop: 12 }}>
|
||||
<span>Auto-refresh every (seconds)</span>
|
||||
@@ -134,7 +129,7 @@ export default function Settings() {
|
||||
onChange={(e) => setPollEverySec(Number(e.target.value) || 8)}
|
||||
/>
|
||||
</label>
|
||||
<div className="settings-hint">Polling only runs in LIVE mode. Currently {mode === "live" ? "polling" : "paused"}.</div>
|
||||
<div className="settings-hint">Polling is always active in this build (live-only).</div>
|
||||
</section>
|
||||
|
||||
<section className="studio-panel">
|
||||
|
||||
Reference in New Issue
Block a user