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
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

This commit is contained in:
2026-06-14 03:47:26 +04:00
parent b2c1e57c86
commit 4bc7ae228a
27 changed files with 2274 additions and 275 deletions
+47 -7
View File
@@ -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(); },