diff --git a/qa/audit_agent.mjs b/qa/audit_agent.mjs new file mode 100644 index 0000000..c31609e --- /dev/null +++ b/qa/audit_agent.mjs @@ -0,0 +1,41 @@ +import { chromium } from "playwright"; + +const URL = "https://canvas.flow-master.ai/"; +const args = ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"]; + +const b = await chromium.launch({ headless: true, args }); +const p = await (await b.newContext({ viewport: { width: 1440, height: 900 } })).newPage(); + +await p.goto(URL, { waitUntil: "networkidle" }); +await p.waitForTimeout(800); +await p.locator(".persona-chip", { hasText: /Mariana/ }).click(); +await p.locator(".dev-login-btn").click(); +await p.waitForTimeout(2500); +const enter = p.locator("button", { hasText: /Enter Mission Control/i }).first(); +if (await enter.count()) { await enter.click(); await p.waitForTimeout(1200); } + +const tabs = await p.locator(".tab").allTextContents(); +console.log("[debug] visible tabs:", tabs); +await p.locator(".tab", { hasText: /Pi/ }).first().click({ timeout: 5000 }).catch(e => console.log("Pi click err:", e.message)); +await p.waitForTimeout(1200); + +const welcome = await p.locator(".agent-turn-agent .agent-turn-body").first().textContent(); +console.log("[agent] welcome:", welcome?.slice(0, 120)); + +const tools = await p.locator(".agent-tool-name").allTextContents(); +console.log("[agent] visible tools:", tools); + +async function ask(text) { + await p.locator(".agent-composer textarea").fill(text); + await p.locator(".agent-composer button", { hasText: /Send/i }).click(); + await p.waitForTimeout(2200); + const all = await p.locator(".agent-turn-agent .agent-turn-body").allTextContents(); + return all[all.length - 1] || ""; +} + +console.log("\n[ask] list processes →", (await ask("list processes")).slice(0, 200)); +console.log("[ask] open mission →", (await ask("open mission")).slice(0, 120)); +console.log("[ask] tell Aisha that the laptop quote is approved →", + (await ask("tell Aisha that the laptop quote is approved")).slice(0, 200)); + +await b.close(); diff --git a/src/App.tsx b/src/App.tsx index d56a0df..af47172 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import Login from "./scenes/Login"; import SsoCallback from "./scenes/SsoCallback"; import Chat from "./scenes/Chat"; import Agent from "./scenes/Agent"; +import Hub from "./scenes/Hub"; import CommandBar from "./components/CommandBar"; import Toaster from "./components/Toaster"; import Console from "./components/Console"; @@ -159,6 +160,9 @@ export default function App() { {scene === "studio" && } {scene === "chat" && } {scene === "agent" && } + {scene === "hub-procurement" && } + {scene === "hub-hr" && } + {scene === "hub-it" && } {scene === "settings" && } diff --git a/src/index.css b/src/index.css index a71052d..750141b 100644 --- a/src/index.css +++ b/src/index.css @@ -1937,3 +1937,25 @@ select.studio-input { background: var(--bp-paper); } .agent-scene { grid-template-columns: 1fr; height: auto; } .agent-sidebar { max-height: 240px; } } + +.hub-scene { padding: 24px; max-width: 1200px; margin: 0 auto; background: var(--bp-paper); min-height: calc(100vh - 60px); } +.hub-head { margin-bottom: 24px; } +.hub-intro { font-size: 13px; color: var(--bp-muted); max-width: 720px; line-height: 1.5; margin-top: 6px; } +.hub-section-label { font-family: var(--bp-mono); font-size: 10px; letter-spacing: 0.08em; color: var(--bp-muted); margin-bottom: 10px; text-transform: uppercase; } +.hub-quick { margin-bottom: 32px; } +.hub-quick-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; } +.hub-quick-card { text-align: left; padding: 14px; background: var(--bp-paper); border: 1px solid var(--bp-navy); cursor: pointer; transition: background 0.1s; display: flex; flex-direction: column; gap: 6px; } +.hub-quick-card:hover:not(:disabled) { background: color-mix(in srgb, var(--bp-amber) 15%, var(--bp-paper)); } +.hub-quick-card:disabled { opacity: 0.5; cursor: not-allowed; } +.hub-quick-title { font-size: 12px; font-weight: 700; color: var(--bp-navy); display: flex; align-items: center; gap: 6px; } +.hub-quick-meta { font-family: var(--bp-mono); font-size: 10px; color: var(--bp-muted); } +.hub-catalogue-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; } +.hub-flow-card { padding: 12px; background: var(--bp-paper); border: 1px solid var(--bp-navy); display: flex; flex-direction: column; gap: 8px; } +.hub-flow-title { font-size: 12px; font-weight: 700; color: var(--bp-navy); display: flex; align-items: center; gap: 6px; } +.hub-flow-desc { font-size: 11px; color: var(--bp-muted); line-height: 1.4; } +.hub-empty { padding: 16px; font-size: 11px; color: var(--bp-muted); } +.link-inline { background: transparent; border: 0; padding: 0; color: var(--bp-navy); text-decoration: underline; cursor: pointer; font: inherit; } + +.hero-hub-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; } +.hub-chip { padding: 8px 14px; background: var(--bp-paper); border: 1px solid var(--bp-navy); color: var(--bp-navy); font-family: var(--bp-mono); font-size: 11px; letter-spacing: 0.04em; cursor: pointer; transition: background 0.1s; } +.hub-chip:hover { background: color-mix(in srgb, var(--bp-amber) 20%, var(--bp-paper)); } diff --git a/src/scenes/Hub.tsx b/src/scenes/Hub.tsx new file mode 100644 index 0000000..f8d6cac --- /dev/null +++ b/src/scenes/Hub.tsx @@ -0,0 +1,173 @@ +import { useEffect, useState } from "react"; +import { useApp } from "../state/store"; +import { api } from "../lib/api"; +import { wizardApi } from "../lib/wizardApi"; +import { Branch, Layers, Pulse } from "../components/icons"; + +export type HubKey = "procurement" | "hr" | "it"; + +interface HubSpec { + title: string; + eyebrow: string; + intro: string; + match: RegExp; + quickActions: { label: string; subject: string; processHint: string }[]; +} + +const HUBS: Record = { + procurement: { + title: "Procurement Hub", + eyebrow: "Purchase requests, vendor approvals, three-way match", + intro: + "Everything a store manager needs to buy something — from a laptop to a forklift — and watch it move through the approval chain.", + match: /procure|purchase|vendor|po\b|payment/i, + quickActions: [ + { label: "Request a new laptop", subject: "store-204-laptop", processHint: "procurement" }, + { label: "Submit a quote for review", subject: "vendor-quote-Q3", processHint: "procurement" }, + ], + }, + hr: { + title: "People Hub", + eyebrow: "Hiring, onboarding, leave, performance", + intro: + "All people movements run here. Start an onboarding for a new hire, file a leave request, or kick off a review cycle.", + match: /onboard|leave|people|hr\b|hiring|payroll/i, + quickActions: [ + { label: "Onboard a new hire", subject: "new-hire-2026Q3", processHint: "onboard" }, + { label: "Request annual leave", subject: "leave-7d-may", processHint: "leave" }, + ], + }, + it: { + title: "IT Hub", + eyebrow: "Tickets, access requests, incident response", + intro: + "When something breaks, request access, or you need a new account — file it here and route it to the right on-call.", + match: /it\b|ticket|incident|access|account|service/i, + quickActions: [ + { label: "Open an incident", subject: "incident-store-204", processHint: "service" }, + { label: "Request system access", subject: "access-sap-RW", processHint: "access" }, + ], + }, +}; + +interface PublishedFlow { + _key: string; + display_name: string; + description?: string; + name?: string; +} + +async function loadPublishedFlows(): Promise { + const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, { + headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` }, + }); + if (!res.ok) throw new Error(`processes ${res.status}`); + const body = await res.json(); + return ((body?.items || []) as any[]) + .filter((it) => it.status === "published" && it.kind === "definition" && it.display_name) + .map((it) => ({ + _key: it._key, + display_name: it.display_name, + description: it.description, + name: it.name, + })); +} + +export default function Hub({ hub }: { hub: HubKey }) { + const spec = HUBS[hub]; + const pushToast = useApp((s) => s.pushToast); + const setScene = useApp((s) => s.setScene); + const [flows, setFlows] = useState(null); + const [busy, setBusy] = useState(null); + + useEffect(() => { + let cancelled = false; + loadPublishedFlows() + .then((rows) => !cancelled && setFlows(rows)) + .catch((err) => !cancelled && pushToast("err", `Catalogue failed: ${err.message}`)); + return () => { + cancelled = true; + }; + }, [hub]); + + const matching = (flows || []).filter((f) => spec.match.test(`${f.display_name} ${f.description || ""} ${f.name || ""}`)).slice(0, 8); + const fallback = (flows || []).slice(0, 6); + const visible = matching.length > 0 ? matching : fallback; + + const startInstance = async (flowKey: string, subject: string, label: string) => { + setBusy(flowKey); + try { + const res = await wizardApi.startInstance(flowKey, subject); + pushToast("ok", `Started ${label}${subject ? ` for ${subject}` : ""} — ${res.transaction_id?.slice(0, 8) || "ok"}`); + setScene("mission"); + } catch (err: any) { + pushToast("err", `Start failed: ${err.message}`); + } finally { + setBusy(null); + } + }; + + const tryQuickAction = async (label: string, subject: string, processHint: string) => { + if (!flows) return; + const match = flows.find((f) => + new RegExp(processHint, "i").test(`${f.display_name} ${f.description || ""} ${f.name || ""}`) + ); + if (!match) { + pushToast("err", `No published process matching "${processHint}". Build one in Studio.`); + return; + } + await startInstance(match._key, subject, label); + }; + + return ( +
+
+
{spec.eyebrow}
+

{spec.title}

+

{spec.intro}

+
+ +
+
QUICK ACTIONS
+
+ {spec.quickActions.map((qa) => ( + + ))} +
+
+ +
+
RELATED PROCESSES
+ {flows === null &&
Loading catalogue…
} + {flows !== null && visible.length === 0 && ( +
+ No matching published processes yet. Open the to build one. +
+ )} +
+ {visible.map((f) => ( +
+
{f.display_name}
+ {f.description &&
{f.description.slice(0, 160)}
} + +
+ ))} +
+
+
+ ); +} diff --git a/src/scenes/Landing.tsx b/src/scenes/Landing.tsx index c8459bc..20fcd44 100644 --- a/src/scenes/Landing.tsx +++ b/src/scenes/Landing.tsx @@ -75,6 +75,14 @@ export default function Landing() { +
+ + + + + +
+