feat: rebuild Process Studio into multi-phase Process Creation Wizard\n\n- Replace toy Studio.tsx with proper Wizard.tsx canvas\n- Build 6-step flow: Intake -> Analyze -> Generate -> Validate -> Draft saved -> Publish\n- Implement incremental EA2 writes via wizardApi.ts (flow, batch apply)\n- Use localStorage for draft recovery across reloads\n- Apply industrial-blueprint UI styling matching core doctrine\n- Add Vitest tests for Wizard components and API wrappers
This commit is contained in:
+47
-7
@@ -6,7 +6,7 @@ import { buildLiveScenariosFromApi } from "../lib/buildScenarios";
|
||||
import { api, type ApiCall, type Actor } from "../lib/api";
|
||||
import type { ProcessScenario } from "../data/types";
|
||||
|
||||
export type SceneId = "landing" | "mission" | "history" | "studio" | "settings";
|
||||
export type SceneId = "landing" | "mission" | "history" | "studio" | "settings" | "login" | "sso-callback";
|
||||
export type DataMode = "snapshot" | "live";
|
||||
export type Theme = "dark" | "light";
|
||||
|
||||
@@ -82,15 +82,18 @@ interface AppState {
|
||||
actor: Actor | null;
|
||||
userEmail: string;
|
||||
userDisplayName: string | null;
|
||||
isAuthed: boolean;
|
||||
setUserEmail: (e: string) => void;
|
||||
loginAs: (email: string) => Promise<void>;
|
||||
loginAs: (email: string, password?: string) => Promise<void>;
|
||||
|
||||
/** Live polling */
|
||||
/** Live polling — lightweight tick that only refreshes the active
|
||||
* scenario's work-items + headline runtime. Full refresh is manual. */
|
||||
pollEverySec: number;
|
||||
setPollEverySec: (n: number) => void;
|
||||
pollTimer: number | null;
|
||||
startPolling: () => void;
|
||||
stopPolling: () => void;
|
||||
pollLiveTick: () => Promise<void>;
|
||||
|
||||
/** API call log */
|
||||
consoleOpen: boolean;
|
||||
@@ -240,28 +243,32 @@ export const useApp = create<AppState>((set, get) => {
|
||||
actor: null,
|
||||
userEmail: prefs.email ?? "dev@flow-master.ai",
|
||||
userDisplayName: null,
|
||||
isAuthed: typeof sessionStorage !== "undefined" && !!sessionStorage.getItem("fm.mc.token.v1"),
|
||||
setUserEmail: (e) => { set({ userEmail: e }); persist(); },
|
||||
loginAs: async (email) => {
|
||||
loginAs: async (email, password) => {
|
||||
api.clearToken();
|
||||
api.config = { ...api.config, email };
|
||||
set({ userEmail: email });
|
||||
persist();
|
||||
try {
|
||||
await api.signIn(email, password);
|
||||
const me = await api.me();
|
||||
set({
|
||||
actor: { mode: "direct_user", user_id: me.user_id },
|
||||
userDisplayName: me.display_name ?? null,
|
||||
isAuthed: true,
|
||||
});
|
||||
get().pushToast("ok", `Signed in as ${me.email}`);
|
||||
if (get().mode === "live") await get().refreshLive();
|
||||
} catch (e) {
|
||||
get().pushToast("err", `Login failed: ${(e as Error).message.slice(0, 80)}`);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
pollEverySec: prefs.pollEverySec ?? 8,
|
||||
pollEverySec: prefs.pollEverySec ?? 30,
|
||||
setPollEverySec: (n) => {
|
||||
set({ pollEverySec: Math.max(2, Math.min(120, n)) });
|
||||
set({ pollEverySec: Math.max(5, Math.min(300, n)) });
|
||||
persist();
|
||||
if (get().pollTimer) { get().stopPolling(); get().startPolling(); }
|
||||
},
|
||||
@@ -273,7 +280,7 @@ export const useApp = create<AppState>((set, get) => {
|
||||
const id = window.setInterval(() => {
|
||||
const s = get();
|
||||
if (s.mode !== "live" || s.liveLoading) return;
|
||||
void s.refreshLive();
|
||||
void s.pollLiveTick();
|
||||
}, get().pollEverySec * 1000);
|
||||
set({ pollTimer: id });
|
||||
},
|
||||
@@ -283,6 +290,39 @@ export const useApp = create<AppState>((set, get) => {
|
||||
if (id) window.clearInterval(id);
|
||||
set({ pollTimer: null });
|
||||
},
|
||||
pollLiveTick: async () => {
|
||||
const s = get();
|
||||
if (s.mode !== "live" || s.liveLoading) return;
|
||||
try {
|
||||
const workItems = await api.workItems();
|
||||
const sc = s.scenarios.find((x) => x.id === s.scenarioId);
|
||||
let headlineRt = null;
|
||||
if (sc?.headlineTx) {
|
||||
headlineRt = await api.transaction(sc.headlineTx);
|
||||
}
|
||||
const updatedScenarios = s.scenarios.map((scen) => {
|
||||
if (!scen.live) return scen;
|
||||
const myCases = workItems.filter((w) => w.definition_key === scen.defKey);
|
||||
const newQueue = myCases.slice(0, 8).map((c, i) => ({
|
||||
id: `${c.transaction_id || scen.defKey}-${i}`,
|
||||
stepId: scen.queue[i]?.stepId ?? scen.defaultStepId,
|
||||
title: `${c.short_id ?? c.transaction_id?.slice(0, 8) ?? "case"} · ${c.active_step_display_name || c.next_action || "case"}`,
|
||||
waitingOn: (c.status === "running" || c.status === "waiting_for_user") ? "approval" as const
|
||||
: c.status === "waiting_for_agent" ? "agent" as const
|
||||
: "input" as const,
|
||||
ageDays: c.age_days ?? 0,
|
||||
status: c.status,
|
||||
}));
|
||||
if (scen.id === s.scenarioId && headlineRt) {
|
||||
return { ...scen, queue: newQueue, raw: { ...(scen.raw as object), headlineRt } };
|
||||
}
|
||||
return { ...scen, queue: newQueue };
|
||||
});
|
||||
set({ scenarios: updatedScenarios, liveTotals: { workItems: workItems.length, distinctDefs: s.liveTotals?.distinctDefs ?? 0 }, liveFetchedAt: Date.now() });
|
||||
} catch {
|
||||
// silent — full refresh will retry
|
||||
}
|
||||
},
|
||||
|
||||
consoleOpen: prefs.consoleOpen ?? false,
|
||||
setConsoleOpen: (v) => { set({ consoleOpen: v }); persist(); },
|
||||
|
||||
Reference in New Issue
Block a user