import {DiagramEntity, DiagramEntityType, DiagramTemplates} from "../../diagram/types/diagramEntity"; import { ClearDiagramParams, COLOR_NAME_TO_HEX, ConnectEntitiesParams, CreateEntityParams, ModifyEntityParams, RemoveEntityParams, SHAPE_TO_TEMPLATE, ToolResult } from "../types/chatTypes"; import {v4 as uuidv4} from 'uuid'; import log from 'loglevel'; const logger = log.getLogger('entityBridge'); logger.setLevel('debug'); function resolveColor(color?: string): string { if (!color) return '#0000ff'; const lower = color.toLowerCase(); if (COLOR_NAME_TO_HEX[lower]) { return COLOR_NAME_TO_HEX[lower]; } if (color.startsWith('#')) { return color; } return '#0000ff'; } interface ResolvedEntity { id: string | null; label: string | null; } /** * Resolve an entity label or ID to actual entity ID and label */ function resolveEntity(target: string): Promise { logger.debug('[resolveEntity] Resolving:', target); return new Promise((resolve) => { const requestId = 'req-' + Date.now() + '-' + Math.random(); const responseHandler = (e: CustomEvent) => { if (e.detail.requestId !== requestId) return; logger.debug('[resolveEntity] Got response:', e.detail); document.removeEventListener('chatResolveEntityResponse', responseHandler as EventListener); resolve({ id: e.detail.entityId, label: e.detail.entityLabel }); }; document.addEventListener('chatResolveEntityResponse', responseHandler as EventListener); const event = new CustomEvent('chatResolveEntity', { detail: {target, requestId}, bubbles: true }); document.dispatchEvent(event); setTimeout(() => { logger.warn('[resolveEntity] Timeout resolving:', target); document.removeEventListener('chatResolveEntityResponse', responseHandler as EventListener); resolve({id: null, label: null}); }, 5000); }); } export function createEntity(params: CreateEntityParams): ToolResult { logger.debug('[createEntity] Creating entity:', params); const id = 'id' + uuidv4(); const template = SHAPE_TO_TEMPLATE[params.shape]; const color = resolveColor(params.color); const position = params.position || {x: 0, y: 1.5, z: 2}; const entity: DiagramEntity = { id, template, type: DiagramEntityType.ENTITY, color, text: params.label || '', position, rotation: {x: 0, y: Math.PI, z: 0}, scale: {x: 0.1, y: 0.1, z: 0.1}, }; logger.debug('[createEntity] Dispatching chatCreateEntity event:', entity); const event = new CustomEvent('chatCreateEntity', { detail: {entity}, bubbles: true }); document.dispatchEvent(event); const result = { toolName: 'create_entity', success: true, message: `Created ${params.shape}${params.label ? ` labeled "${params.label}"` : ''} at position (${position.x.toFixed(1)}, ${position.y.toFixed(1)}, ${position.z.toFixed(1)})`, entityId: id }; logger.debug('[createEntity] Done:', result); return result; } export async function connectEntities(params: ConnectEntitiesParams): Promise { logger.debug('[connectEntities] Connecting:', params); // Resolve labels to actual entity IDs and get their labels const fromEntity = await resolveEntity(params.from); const toEntity = await resolveEntity(params.to); logger.debug('[connectEntities] Resolved from:', fromEntity, 'to:', toEntity); if (!fromEntity.id) { return { toolName: 'connect_entities', success: false, message: `Could not find entity "${params.from}"` }; } if (!toEntity.id) { return { toolName: 'connect_entities', success: false, message: `Could not find entity "${params.to}"` }; } const id = 'id' + uuidv4(); const color = resolveColor(params.color); // Generate default label from entity labels: "{from label} to {to label}" const fromLabel = fromEntity.label || params.from; const toLabel = toEntity.label || params.to; const connectionLabel = `${fromLabel} to ${toLabel}`; const entity: DiagramEntity = { id, template: DiagramTemplates.CONNECTION, type: DiagramEntityType.ENTITY, color, text: connectionLabel, from: fromEntity.id, to: toEntity.id, }; const event = new CustomEvent('chatCreateEntity', { detail: {entity}, bubbles: true }); document.dispatchEvent(event); return { toolName: 'connect_entities', success: true, message: `Connected "${fromLabel}" to "${toLabel}"`, entityId: id }; } export function removeEntity(params: RemoveEntityParams): ToolResult { const event = new CustomEvent('chatRemoveEntity', { detail: {target: params.target}, bubbles: true }); document.dispatchEvent(event); return { toolName: 'remove_entity', success: true, message: `Removed entity "${params.target}"` }; } export function modifyEntity(params: ModifyEntityParams): ToolResult { const updates: Partial = {}; if (params.color) { updates.color = resolveColor(params.color); } if (params.label !== undefined) { updates.text = params.label; } if (params.position) { updates.position = params.position; } const event = new CustomEvent('chatModifyEntity', { detail: {target: params.target, updates}, bubbles: true }); document.dispatchEvent(event); return { toolName: 'modify_entity', success: true, message: `Modified entity "${params.target}"` }; } export function listEntities(): Promise { logger.debug('[listEntities] Listing entities...'); return new Promise((resolve) => { const responseHandler = (e: CustomEvent) => { document.removeEventListener('chatListEntitiesResponse', responseHandler as EventListener); const entities = e.detail.entities as Array<{ id: string; template: string; text: string; position: { x: number; y: number; z: number } }>; logger.debug('[listEntities] Got response, entities:', entities.length); if (entities.length === 0) { resolve({ toolName: 'list_entities', success: true, message: 'The diagram is empty.' }); return; } const list = entities.map(e => { const shape = e.template.replace('#', '').replace('-template', ''); return `- ${e.text || '(no label)'} (${shape}) at (${e.position?.x?.toFixed(1) || 0}, ${e.position?.y?.toFixed(1) || 0}, ${e.position?.z?.toFixed(1) || 0}) [id: ${e.id}]`; }).join('\n'); resolve({ toolName: 'list_entities', success: true, message: `Current entities in the diagram:\n${list}` }); }; document.addEventListener('chatListEntitiesResponse', responseHandler as EventListener); const event = new CustomEvent('chatListEntities', {bubbles: true}); document.dispatchEvent(event); setTimeout(() => { logger.warn('[listEntities] Timeout waiting for response'); document.removeEventListener('chatListEntitiesResponse', responseHandler as EventListener); resolve({ toolName: 'list_entities', success: false, message: 'Failed to list entities (timeout)' }); }, 5000); }); } /** * Get all entities for session syncing (returns raw entity data) */ export function getEntitiesForSync(): Promise> { return new Promise((resolve) => { const responseHandler = (e: CustomEvent) => { document.removeEventListener('chatListEntitiesResponse', responseHandler as EventListener); resolve(e.detail.entities || []); }; document.addEventListener('chatListEntitiesResponse', responseHandler as EventListener); const event = new CustomEvent('chatListEntities', {bubbles: true}); document.dispatchEvent(event); setTimeout(() => { document.removeEventListener('chatListEntitiesResponse', responseHandler as EventListener); resolve([]); }, 5000); }); } /** * Clear all entities from the diagram */ export async function clearDiagram(params: ClearDiagramParams): Promise { if (!params.confirmed) { return { toolName: 'clear_diagram', success: false, message: 'Clearing the diagram requires confirmation. Please ask the user to confirm before proceeding.' }; } // Get all entities first const entities = await getEntitiesForSync(); if (entities.length === 0) { return { toolName: 'clear_diagram', success: true, message: 'The diagram is already empty.' }; } // Dispatch clear event to remove all entities at once const event = new CustomEvent('chatClearDiagram', { bubbles: true }); document.dispatchEvent(event); return { toolName: 'clear_diagram', success: true, message: `Cleared ${entities.length} entities from the diagram.` }; } /** * Get the current camera position and orientation */ export function getCameraPosition(): Promise { logger.debug('[getCameraPosition] Getting camera position...'); return new Promise((resolve) => { const responseHandler = (e: CustomEvent) => { document.removeEventListener('chatGetCameraResponse', responseHandler as EventListener); const {position, forward, up} = e.detail; logger.debug('[getCameraPosition] Got response:', e.detail); const message = `Camera position: (${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}) Forward direction: (${forward.x.toFixed(2)}, ${forward.y.toFixed(2)}, ${forward.z.toFixed(2)}) Up direction: (${up.x.toFixed(2)}, ${up.y.toFixed(2)}, ${up.z.toFixed(2)}) To place an entity in front of the camera, add the forward direction scaled by desired distance to the camera position. Example: For an entity 2 units in front: position = (${(position.x + forward.x * 2).toFixed(2)}, ${(position.y + forward.y * 2).toFixed(2)}, ${(position.z + forward.z * 2).toFixed(2)})`; resolve({ toolName: 'get_camera_position', success: true, message }); }; document.addEventListener('chatGetCameraResponse', responseHandler as EventListener); const event = new CustomEvent('chatGetCamera', {bubbles: true}); document.dispatchEvent(event); setTimeout(() => { logger.warn('[getCameraPosition] Timeout waiting for response'); document.removeEventListener('chatGetCameraResponse', responseHandler as EventListener); resolve({ toolName: 'get_camera_position', success: false, message: 'Failed to get camera position (timeout)' }); }, 5000); }); }