/** * MI Content Triage — orchestrates content analysis and shape creation. * * Sends raw pasted/dropped content to `/api/mi/triage`, stores the proposal, * and commits approved shapes via MiActionExecutor. */ import type { MiAction } from "./mi-actions"; import { MiActionExecutor } from "./mi-action-executor"; export interface TriageProposedShape { tagName: string; label: string; props: Record; snippet: string; } export interface TriageConnection { fromIndex: number; toIndex: number; reason: string; } export interface TriageProposal { summary: string; shapes: TriageProposedShape[]; connections: TriageConnection[]; } export type TriageStatus = "idle" | "analyzing" | "ready" | "error"; /** Extract URLs from raw text. */ function extractUrls(text: string): string[] { const urlPattern = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g; return [...new Set(text.match(urlPattern) || [])]; } export class TriageManager { proposal: TriageProposal | null = null; status: TriageStatus = "idle"; error: string | null = null; private onChange: (() => void) | null = null; constructor(onChange?: () => void) { this.onChange = onChange || null; } private notify() { this.onChange?.(); } async analyze(raw: string, type: "paste" | "drop" = "paste"): Promise { if (!raw.trim()) { this.status = "error"; this.error = "No content to analyze"; this.notify(); return; } this.status = "analyzing"; this.error = null; this.proposal = null; this.notify(); try { const res = await fetch("/api/mi/triage", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: raw.slice(0, 50000), contentType: type }), }); if (!res.ok) { const err = await res.json().catch(() => ({ error: "Request failed" })); throw new Error(err.error || `HTTP ${res.status}`); } const data = await res.json(); this.proposal = { summary: data.summary || "Content analyzed", shapes: Array.isArray(data.shapes) ? data.shapes : [], connections: Array.isArray(data.connections) ? data.connections : [], }; this.status = this.proposal.shapes.length > 0 ? "ready" : "error"; if (this.proposal.shapes.length === 0) { this.error = "No shapes identified in content"; } } catch (e: any) { this.status = "error"; this.error = e.message || "Analysis failed"; this.proposal = null; } this.notify(); } removeShape(index: number): void { if (!this.proposal) return; this.proposal.shapes.splice(index, 1); // Reindex connections — remove any referencing deleted index, adjust higher indices this.proposal.connections = this.proposal.connections .filter((c) => c.fromIndex !== index && c.toIndex !== index) .map((c) => ({ ...c, fromIndex: c.fromIndex > index ? c.fromIndex - 1 : c.fromIndex, toIndex: c.toIndex > index ? c.toIndex - 1 : c.toIndex, })); this.notify(); } commitAll(): void { if (!this.proposal || this.proposal.shapes.length === 0) return; const actions: MiAction[] = []; // Create shapes with $N refs for (let i = 0; i < this.proposal.shapes.length; i++) { const shape = this.proposal.shapes[i]; actions.push({ type: "create-shape", tagName: shape.tagName, props: { ...shape.props }, ref: `$${i + 1}`, }); } // Create connections for (const conn of this.proposal.connections) { actions.push({ type: "connect", sourceId: `$${conn.fromIndex + 1}`, targetId: `$${conn.toIndex + 1}`, }); } const executor = new MiActionExecutor(); executor.execute(actions); this.status = "idle"; this.proposal = null; } }