Files
ttstd-infinite-canvas/web/src/lib/creation/creative-plan.ts

69 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { CreativePlan, CreativePlanStep } from "./creative-agent-contract";
import type { CreativeAgentState } from "./creative-agent-state";
export type CreativePlannedStep = {
id: string;
ref: string;
title: string;
phase: "questions" | "proposal" | "canvas" | "media";
mediaRefs: string[];
// Only previous observed execution may freeze a completed step.
completed?: boolean;
};
export type CreativeDynamicPlan = { id: string; revision: number; reason?: string; steps: CreativePlannedStep[] };
export function updateCreativePlan(raw: unknown, previous: CreativeDynamicPlan | undefined, id: string): CreativeDynamicPlan | undefined {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return previous;
const input = raw as Record<string, unknown>;
if (!Array.isArray(input.steps)) return previous;
const steps: CreativePlannedStep[] = [];
const refs = new Set<string>();
for (const value of input.steps.slice(0, 12)) {
if (!value || typeof value !== "object") continue;
const item = value as Record<string, unknown>;
const ref = typeof item.ref === "string" ? item.ref.trim().slice(0, 80) : "";
const title = typeof item.title === "string" ? item.title.trim().slice(0, 160) : "";
if (!ref || !title || refs.has(ref) || !["questions", "proposal", "canvas", "media"].includes(String(item.phase))) continue;
refs.add(ref);
const old = previous?.steps.find((step) => step.ref === ref);
steps.push(old?.completed ? old : {
id: old?.id || `${id}:${steps.length + 1}`, ref, title,
phase: item.phase as CreativePlannedStep["phase"],
mediaRefs: Array.isArray(item.mediaRefs) ? [...new Set(item.mediaRefs.filter((entry): entry is string => typeof entry === "string" && Boolean(entry.trim())))].slice(0, 20) : [],
});
}
if (!steps.length) return previous;
// Finished work stays in the plan even when a model omits it in a revision.
const completed = previous?.steps.filter((step) => step.completed && !refs.has(step.ref)) || [];
return { id: previous?.id || id, revision: (previous?.revision || 0) + 1,
reason: typeof input.reason === "string" ? input.reason.slice(0, 500) : undefined, steps: [...completed, ...steps] };
}
export function dynamicCreativePlan(state: CreativeAgentState): CreativePlan | undefined {
const plan = state.dynamicPlan;
if (!plan) return;
return { id: plan.id, steps: plan.steps.map((step): CreativePlanStep => {
let status: CreativePlanStep["status"] = step.completed ? "completed" : "pending";
let nodeIds: string[] = [];
if (!step.completed) {
if (step.phase === "questions" && state.questions) status = state.questions.status === "submitted" ? "completed" : state.questions.status === "pending" ? "waiting" : "cancelled";
if (step.phase === "proposal" && state.proposal) status = state.canvasApplied ? "completed" : "waiting";
if (step.phase === "canvas" && state.canvasApplied) status = "completed";
if (step.phase === "media" && step.mediaRefs.length) {
const media = state.media.filter((item) => step.mediaRefs.includes(item.ref));
nodeIds = media.map((item) => item.nodeId);
if (media.some((item) => ["failed", "write_failed"].includes(item.status))) status = "failed";
else if (media.length === step.mediaRefs.length && media.every((item) => item.status === "ready")) status = "completed";
else if (media.some((item) => ["running", "queued"].includes(item.status))) status = "running";
else if (media.some((item) => item.submissionId)) status = "waiting";
}
}
return { id: step.id, title: step.title, status, nodeIds, detail: step.phase === "media" ? state.media.filter((item) => step.mediaRefs.includes(item.ref)).map((item) => item.error).filter(Boolean).join(";") || undefined : undefined };
}) };
}
export function snapshotCreativePlan(state: CreativeAgentState): CreativeDynamicPlan | undefined {
const actual = dynamicCreativePlan(state);
return state.dynamicPlan && { ...state.dynamicPlan, steps: state.dynamicPlan.steps.map((step) => ({ ...step, completed: step.completed || actual?.steps.find((item) => item.id === step.id)?.status === "completed" })) };
}