143 lines
3.6 KiB
TypeScript
143 lines
3.6 KiB
TypeScript
/**
|
|
* 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<string, any>;
|
|
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<void> {
|
|
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;
|
|
}
|
|
}
|