218 lines
9.5 KiB
TypeScript
218 lines
9.5 KiB
TypeScript
// Command palette — scenarios, steps, scenes, recents, real actions.
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { Command } from "cmdk";
|
|
import { useApp, scenarioById } from "../state/store";
|
|
import { Play, Branch, Bot, Check, Home, HistoryIcon, Layers, Search, Refresh, Cog, User } from "./icons";
|
|
|
|
export default function CommandBar() {
|
|
const open = useApp((s) => s.cmdOpen);
|
|
const setOpen = useApp((s) => s.setCmdOpen);
|
|
const setScenarioId = useApp((s) => s.setScenarioId);
|
|
const setSelectedStepId = useApp((s) => s.setSelectedStepId);
|
|
const setScene = useApp((s) => s.setScene);
|
|
const recents = useApp((s) => s.recents);
|
|
const scenarioId = useApp((s) => s.scenarioId);
|
|
const pushRecent = useApp((s) => s.pushRecent);
|
|
const scenarios = useApp((s) => s.scenarios);
|
|
|
|
const refreshLive = useApp((s) => s.refreshLive);
|
|
const startInstance = useApp((s) => s.startInstance);
|
|
const executeAction = useApp((s) => s.executeAction);
|
|
const pushToast = useApp((s) => s.pushToast);
|
|
const setConsoleOpen = useApp((s) => s.setConsoleOpen);
|
|
const setTheme = useApp((s) => s.setTheme);
|
|
const theme = useApp((s) => s.theme);
|
|
const actor = useApp((s) => s.actor);
|
|
|
|
const sc = scenarioById(scenarioId);
|
|
const [query, setQuery] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (!open) setQuery("");
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
|
e.preventDefault();
|
|
setOpen(!open);
|
|
}
|
|
if (e.key === "Escape" && open) setOpen(false);
|
|
};
|
|
document.addEventListener("keydown", onKey);
|
|
return () => document.removeEventListener("keydown", onKey);
|
|
}, [open, setOpen]);
|
|
|
|
const stepItems = useMemo(() => sc?.steps ?? [], [sc]);
|
|
|
|
if (!open) return null;
|
|
|
|
const close = () => setOpen(false);
|
|
|
|
const canActLive = !!actor?.user_id && sc?.live && sc.headlineTx;
|
|
const firstActionId = sc?.steps.find((s) => s.state === "running")?.actions?.[0]?.id;
|
|
|
|
return (
|
|
<div className="cmd-overlay" onClick={close}>
|
|
<Command className="cmd" onClick={(e) => e.stopPropagation()} label="Command bar">
|
|
<div className="cmd-input-row">
|
|
<Search size={14} />
|
|
<Command.Input autoFocus placeholder="Switch scenario · jump to step · start instance · execute action…" value={query} onValueChange={setQuery} />
|
|
<kbd className="kbd-hint">esc</kbd>
|
|
</div>
|
|
{!query && (
|
|
<div className="cmd-try-saying" aria-hidden>
|
|
<span className="cmd-try-label">Try saying</span>
|
|
<ul className="cmd-try-list">
|
|
<li><kbd>start laptop procurement</kbd> <span className="cmd-try-hint">opens Studio with intake filled in</span></li>
|
|
<li><kbd>open procurement hub</kbd> <span className="cmd-try-hint">jumps to the Procurement hub</span></li>
|
|
<li><kbd>find case INC-4427</kbd> <span className="cmd-try-hint">searches active runs by subject</span></li>
|
|
<li><kbd>my approvals</kbd> <span className="cmd-try-hint">opens the queue waiting on you</span></li>
|
|
<li><kbd>switch theme</kbd> <span className="cmd-try-hint">light ↔ dark</span></li>
|
|
<li><kbd>open settings</kbd> <span className="cmd-try-hint">identity, polling, console</span></li>
|
|
</ul>
|
|
</div>
|
|
)}
|
|
<Command.List>
|
|
<Command.Empty>No matches.</Command.Empty>
|
|
|
|
{recents.length > 0 && (
|
|
<Command.Group heading="Recent">
|
|
{recents.map((r, i) => (
|
|
<Command.Item key={`r-${i}-${r}`} onSelect={() => { pushRecent(r); close(); }}>
|
|
<HistoryIcon size={13} /> {r}
|
|
</Command.Item>
|
|
))}
|
|
</Command.Group>
|
|
)}
|
|
|
|
<Command.Group heading="Go to">
|
|
<Command.Item onSelect={() => { setScene("landing"); close(); }}>
|
|
<Home size={13} /> Home
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("mission"); close(); }}>
|
|
<Layers size={13} /> Mission Control
|
|
<span className="cmd-hint">live work-items + process graph</span>
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("approvals"); close(); }}>
|
|
<Layers size={13} /> Approvals queue
|
|
<span className="cmd-hint">work waiting on you</span>
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("studio"); close(); }}>
|
|
<Branch size={13} /> Process Creation Wizard
|
|
<span className="cmd-hint">describe → publish → run</span>
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("documents"); close(); }}>
|
|
<Layers size={13} /> Documents
|
|
<span className="cmd-hint">data shapes per process</span>
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("hub-procurement"); close(); }}>
|
|
<Layers size={13} /> Procurement hub
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("hub-hr"); close(); }}>
|
|
<Layers size={13} /> People hub
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("hub-it"); close(); }}>
|
|
<Layers size={13} /> IT hub
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("geo-attendance"); close(); }}>
|
|
<Layers size={13} /> Attendance map
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("chat"); close(); }}>
|
|
<Bot size={13} /> Team chat
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("agent"); close(); }}>
|
|
<Bot size={13} /> Command Assistant
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("explainer"); close(); }}>
|
|
<Layers size={13} /> What is FlowMaster?
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("history"); close(); }}>
|
|
<HistoryIcon size={13} /> Run history
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("settings"); close(); }}>
|
|
<Cog size={13} /> Settings
|
|
</Command.Item>
|
|
</Command.Group>
|
|
|
|
<Command.Group heading="Preferences">
|
|
<Command.Item onSelect={() => { refreshLive(); close(); }}>
|
|
<Refresh size={13} /> Refresh live data
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setTheme(theme === "dark" ? "light" : "dark"); close(); }}>
|
|
<Cog size={13} /> Switch to {theme === "dark" ? "light" : "dark"} theme
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setConsoleOpen(true); close(); }}>
|
|
<Layers size={13} /> Open API console
|
|
<span className="cmd-hint">for developers</span>
|
|
</Command.Item>
|
|
</Command.Group>
|
|
|
|
<Command.Group heading="Actions">
|
|
{sc?.live ? (
|
|
<Command.Item
|
|
onSelect={async () => {
|
|
close();
|
|
if (!actor?.user_id) { pushToast("warn", "Sign in to start a real instance."); return; }
|
|
await startInstance(sc.defKey, `Started via Mission Control · ${new Date().toLocaleString()}`);
|
|
}}
|
|
>
|
|
<Play size={13} /> Start an instance of "{sc.defName}"
|
|
<span className="cmd-hint">writes to EA2</span>
|
|
</Command.Item>
|
|
) : (
|
|
<Command.Item disabled>
|
|
<Play size={13} /> Start new instance
|
|
<span className="cmd-hint">switch to a live scenario first</span>
|
|
</Command.Item>
|
|
)}
|
|
{canActLive && firstActionId && (
|
|
<Command.Item
|
|
onSelect={async () => { close(); await executeAction(sc!.headlineTx!, firstActionId); }}
|
|
>
|
|
<Check size={13} /> Submit "{firstActionId}" on the headline case
|
|
<span className="cmd-hint">live · writes to EA2</span>
|
|
</Command.Item>
|
|
)}
|
|
<Command.Item onSelect={() => { setScene("settings"); close(); }}>
|
|
<User size={13} /> Sign in as someone else
|
|
<span className="cmd-hint">Settings → identity</span>
|
|
</Command.Item>
|
|
</Command.Group>
|
|
|
|
<Command.Group heading="Scenarios">
|
|
{scenarios.map((s) => (
|
|
<Command.Item
|
|
key={s.id}
|
|
value={`scenario ${s.id} ${s.family.label} ${s.defName}`}
|
|
onSelect={() => { setScenarioId(s.id); setScene("mission"); close(); }}
|
|
>
|
|
<Branch size={13} style={{ color: s.family.accent }} />
|
|
{s.family.label}
|
|
<span className="cmd-hint">{s.live ? "live" : "blueprint"} · {s.defName}</span>
|
|
</Command.Item>
|
|
))}
|
|
</Command.Group>
|
|
|
|
{sc && (
|
|
<Command.Group heading="Jump to step">
|
|
{stepItems.map((st) => (
|
|
<Command.Item
|
|
key={st.id}
|
|
value={`step ${st.id} ${st.name} ${st.kind}`}
|
|
onSelect={() => { setSelectedStepId(st.id); setScene("mission"); close(); }}
|
|
>
|
|
<span className={`dot dot-${st.state}`} aria-hidden /> {st.name}
|
|
<span className="cmd-hint mono">{st.kind}</span>
|
|
</Command.Item>
|
|
))}
|
|
</Command.Group>
|
|
)}
|
|
|
|
|
|
</Command.List>
|
|
</Command>
|
|
</div>
|
|
);
|
|
}
|