Files
canvas-frontend/src/scenes/Agent.tsx
T
shad 05be9ca8dc fix(oracle-r4-caveats): wizard rewires presentation edges, chat QA proves, Pi leak
Three Oracle round-7 caveats:

1. Wizard view replacement now deletes old presentation edges before
   creating new field-based ones. Previously handleDataSave added new
   edges additively, so runtime could still hit the generic Notes view.
2. Chat sidebar QA assertion was vacuously true (previewRows >= 0 &&
   timeBadges >= 0). Now: when threads exist, preview count must equal
   thread count AND at least one relative timestamp must render.
3. Agent composer placeholder rebranded from 'Ask Pi...' to 'Ask the
   assistant...'. Closes residual Pi/LLM framing leak.
2026-06-14 15:14:59 +04:00

125 lines
4.6 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { useApp } from "../state/store";
import { routeAgentInput, agentToolList, type ToolResult } from "../lib/agentTools";
import { Bot, Branch } from "../components/icons";
interface Turn {
id: string;
role: "user" | "agent";
text: string;
ok?: boolean;
}
const SUGGESTIONS = [
"Open mission control",
"List processes",
"Run procurement to pay for store-204",
"Tell Rohan that the laptop quote is approved",
"Create a new process",
"Open the IT hub",
];
export default function Agent() {
const userEmail = useApp((s) => s.userEmail);
const setScene = useApp((s) => s.setScene as (s: string) => void);
const [turns, setTurns] = useState<Turn[]>([
{
id: "welcome",
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.`,
},
]);
const [draft, setDraft] = useState("");
const [working, setWorking] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setTimeout(() => scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }), 50);
}, [turns]);
const submit = async (text: string) => {
const t = text.trim();
if (!t || working) return;
const userTurn: Turn = { id: `u-${Date.now()}`, role: "user", text: t };
setTurns((prev) => [...prev, userTurn]);
setDraft("");
setWorking(true);
try {
const res: ToolResult = await routeAgentInput(t, {
navigate: (s) => setScene(s),
userEmail,
});
setTurns((prev) => [...prev, { id: `a-${Date.now()}`, role: "agent", text: res.display, ok: res.ok }]);
} catch (e: any) {
setTurns((prev) => [...prev, { id: `a-${Date.now()}`, role: "agent", text: `Error: ${e.message}`, ok: false }]);
} finally {
setWorking(false);
}
};
return (
<div className="agent-scene">
<aside className="agent-sidebar">
<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. The LLM adapter ships separately when a provider key is configured.</p>
</header>
<div className="agent-tool-list">
<div className="agent-tool-label">CAPABILITIES</div>
{agentToolList().map((t) => (
<div key={t.name} className="agent-tool-row">
<Branch size={11} />
<div>
<div className="agent-tool-name">{t.name.replace(/_/g, " ")}</div>
<div className="agent-tool-desc">{t.description}</div>
</div>
</div>
))}
</div>
</aside>
<section className="agent-main">
<div className="agent-messages" ref={scrollRef}>
{turns.map((t) => (
<div key={t.id} className={`agent-turn agent-turn-${t.role}${t.ok === false ? " agent-turn-err" : ""}`}>
<div className="agent-turn-author">{t.role === "user" ? userEmail.split("@")[0] : "Assistant"}</div>
<div className="agent-turn-body">{t.text}</div>
</div>
))}
{working && <div className="agent-turn agent-turn-agent agent-turn-thinking">Working</div>}
</div>
{turns.length <= 1 && (
<div className="agent-suggestions">
{SUGGESTIONS.map((s) => (
<button key={s} className="agent-suggestion" onClick={() => submit(s)}>{s}</button>
))}
</div>
)}
<footer className="agent-composer">
<textarea
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submit(draft);
}
}}
placeholder="Ask the assistant to navigate, start a process, or message a teammate. Enter to send."
rows={2}
disabled={working}
/>
<button className="btn btn-primary" onClick={() => submit(draft)} disabled={!draft.trim() || working}>
{working ? "…" : "Send"}
</button>
</footer>
</section>
</div>
);
}