rspace-online/lib/mi-canvas-bridge.ts

145 lines
3.8 KiB
TypeScript

/**
* MiCanvasBridge — Singleton that connects canvas state to the MI assistant.
*
* Listens to selection, viewport, and connection changes on the canvas
* and exposes a structured context snapshot for the MI system prompt.
*/
export interface ShapeInfo {
id: string;
type: string;
x: number;
y: number;
width: number;
height: number;
content?: string;
title?: string;
}
export interface Connection {
arrowId: string;
sourceId: string;
targetId: string;
}
export interface ShapeGroup {
shapeIds: string[];
}
export interface CanvasContext {
selectedShapes: ShapeInfo[];
allShapes: ShapeInfo[];
connections: Connection[];
viewport: { x: number; y: number; scale: number };
shapeGroups: ShapeGroup[];
shapeCountByType: Record<string, number>;
}
export class MiCanvasBridge {
static instance: MiCanvasBridge | null = null;
selectedShapeIds: string[] = [];
viewport = { x: 0, y: 0, scale: 1 };
private canvasContent: HTMLElement | null = null;
constructor(canvasContent?: HTMLElement) {
if (MiCanvasBridge.instance) return MiCanvasBridge.instance;
this.canvasContent = canvasContent || document.getElementById("canvas-content");
MiCanvasBridge.instance = this;
}
setSelection(ids: string[]) {
this.selectedShapeIds = ids;
}
setViewport(x: number, y: number, scale: number) {
this.viewport = { x, y, scale };
}
/** Build full context snapshot for the MI system prompt. */
getCanvasContext(): CanvasContext {
const allShapes = this.#collectShapes();
const connections = this.#collectConnections();
const selectedShapes = allShapes.filter((s) => this.selectedShapeIds.includes(s.id));
const shapeGroups = this.#buildShapeGroups(allShapes, connections);
const shapeCountByType: Record<string, number> = {};
for (const s of allShapes) {
shapeCountByType[s.type] = (shapeCountByType[s.type] || 0) + 1;
}
return {
selectedShapes,
allShapes,
connections,
viewport: { ...this.viewport },
shapeGroups,
shapeCountByType,
};
}
#collectShapes(): ShapeInfo[] {
if (!this.canvasContent) return [];
return [...this.canvasContent.children]
.filter(
(el) =>
el.tagName?.includes("-") &&
el.id &&
!el.tagName.toLowerCase().includes("arrow"),
)
.map((el: any) => ({
id: el.id,
type: el.tagName.toLowerCase(),
x: el.x ?? 0,
y: el.y ?? 0,
width: el.width ?? 0,
height: el.height ?? 0,
...(el.content ? { content: String(el.content).slice(0, 120) } : {}),
...(el.title ? { title: String(el.title).slice(0, 80) } : {}),
}));
}
#collectConnections(): Connection[] {
if (!this.canvasContent) return [];
return [...this.canvasContent.querySelectorAll("folk-arrow")]
.filter((el: any) => el.id && el.sourceId && el.targetId)
.map((el: any) => ({
arrowId: el.id,
sourceId: el.sourceId,
targetId: el.targetId,
}));
}
/** BFS on the arrow graph to find clusters of connected shapes. */
#buildShapeGroups(shapes: ShapeInfo[], connections: Connection[]): ShapeGroup[] {
const adj = new Map<string, Set<string>>();
for (const c of connections) {
if (!adj.has(c.sourceId)) adj.set(c.sourceId, new Set());
if (!adj.has(c.targetId)) adj.set(c.targetId, new Set());
adj.get(c.sourceId)!.add(c.targetId);
adj.get(c.targetId)!.add(c.sourceId);
}
const visited = new Set<string>();
const groups: ShapeGroup[] = [];
for (const shape of shapes) {
if (visited.has(shape.id) || !adj.has(shape.id)) continue;
const group: string[] = [];
const queue = [shape.id];
while (queue.length) {
const id = queue.shift()!;
if (visited.has(id)) continue;
visited.add(id);
group.push(id);
for (const neighbor of adj.get(id) || []) {
if (!visited.has(neighbor)) queue.push(neighbor);
}
}
if (group.length > 1) groups.push({ shapeIds: group });
}
return groups;
}
}