From 5fd57ba70fe789290fce609bbdd26ca58c8995e8 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 14 Feb 2026 20:07:25 -0600 Subject: [PATCH] feat: Add DOM inspection and manipulation tools for canvas - inspect_window: Inspect HTML content of a window with selector filter - get_canvas: Read canvas elements using CSS selectors (like Read tool) - edit_canvas: Edit canvas elements with old/new value replacement (like Edit tool) - canvas_css: Inject/update/remove CSS blocks with ID tracking - canvas_js: Execute JavaScript in canvas context with helper functions - get_canvas_css: List or get specific injected CSS blocks --- frontend/src/services/toolRegistry.ts | 2 +- .../services/tools/handlers/canvasHandlers.ts | 308 ++++++++++++++++++ .../src/services/tools/toolDefinitions.ts | 6 + 3 files changed, 315 insertions(+), 1 deletion(-) diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index bb92a78..6ca5dd5 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -140,7 +140,7 @@ function getToolConfigs(): Map { // Category to tool names mapping const categoryTools: Record = { global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'], - canvas: ['render_html', 'render_vue_component', 'move_window', 'resize_window', 'close_window', 'list_windows'], + canvas: ['render_html', 'render_vue_component', 'move_window', 'resize_window', 'close_window', 'list_windows', 'inspect_window', 'get_canvas', 'edit_canvas', 'canvas_css', 'canvas_js', 'get_canvas_css'], component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component'], theme: ['get_design_tokens', 'get_active_theme', 'set_theme_variable', 'save_theme', 'list_themes', 'switch_theme', 'reset_theme'], database: ['list_tables', 'get_table_schema', 'get_table_data', 'get_database_stats', 'execute_query'], diff --git a/frontend/src/services/tools/handlers/canvasHandlers.ts b/frontend/src/services/tools/handlers/canvasHandlers.ts index 448ef6e..30d0c85 100644 --- a/frontend/src/services/tools/handlers/canvasHandlers.ts +++ b/frontend/src/services/tools/handlers/canvasHandlers.ts @@ -273,6 +273,314 @@ export function createCanvasHandlers(): ToolConfig[] { return `Ventanas abiertas (${windows.length}):\n${list}` } + }, + { + name: 'inspect_window', + description: 'Inspecciona el contenido HTML de una ventana. Usa selector para filtrar elementos internos.', + category: 'canvas', + schema: { + type: 'object', + properties: { + id: { type: 'string', description: 'ID de la ventana a inspeccionar' }, + selector: { type: 'string', description: 'Selector CSS para filtrar elementos dentro de la ventana (opcional)' }, + attribute: { + type: 'string', + enum: ['innerHTML', 'outerHTML', 'textContent', 'attributes'], + description: 'Que retornar: innerHTML (default), outerHTML, textContent, attributes' + }, + limit: { type: 'number', description: 'Limitar caracteres de salida (default: 2000)' } + }, + required: ['id'] + }, + handler: (args: { id: string; selector?: string; attribute?: string; limit?: number }) => { + const windowEl = document.querySelector(`[data-window-id="${args.id}"]`) + if (!windowEl) { + return `Error: Ventana "${args.id}" no encontrada` + } + + const content = windowEl.querySelector('.window-content') + if (!content) { + return `Error: Contenido de ventana "${args.id}" no encontrado` + } + + let target: Element | null = content + if (args.selector) { + target = content.querySelector(args.selector) + if (!target) { + return `Error: Selector "${args.selector}" no encontrado en ventana "${args.id}"` + } + } + + const attr = args.attribute || 'innerHTML' + const limit = args.limit || 2000 + let result: string + + if (attr === 'attributes') { + const attrs: Record = {} + for (const a of Array.from(target.attributes)) { + attrs[a.name] = a.value + } + result = JSON.stringify(attrs, null, 2) + } else { + result = (target as any)[attr] || '' + } + + if (result.length > limit) { + result = result.slice(0, limit) + `\n... (truncado, ${result.length - limit} chars mas)` + } + + return result + } + }, + { + name: 'get_canvas', + description: 'Lee elementos del canvas usando selectores CSS. Similar a Read pero para el DOM.', + category: 'canvas', + schema: { + type: 'object', + properties: { + selector: { type: 'string', description: 'Selector CSS para encontrar elementos' }, + attribute: { + type: 'string', + enum: ['innerHTML', 'outerHTML', 'textContent', 'attributes', 'style', 'classList'], + description: 'Que retornar (default: innerHTML)' + }, + all: { type: 'boolean', description: 'Si true, retorna todos los matches (default: false, solo el primero)' }, + limit: { type: 'number', description: 'Limitar caracteres de salida (default: 3000)' } + }, + required: ['selector'] + }, + handler: (args: { selector: string; attribute?: string; all?: boolean; limit?: number }) => { + const container = getCanvasContainer() + if (!container) return 'Error: canvas no encontrado' + + const attr = args.attribute || 'innerHTML' + const limit = args.limit || 3000 + + const extractValue = (el: Element): string => { + if (attr === 'attributes') { + const attrs: Record = {} + for (const a of Array.from(el.attributes)) { + attrs[a.name] = a.value + } + return JSON.stringify(attrs) + } else if (attr === 'style') { + return (el as HTMLElement).style.cssText + } else if (attr === 'classList') { + return Array.from(el.classList).join(' ') + } else { + return (el as any)[attr] || '' + } + } + + let result: string + + if (args.all) { + const elements = container.querySelectorAll(args.selector) + if (elements.length === 0) { + return `No se encontraron elementos con selector "${args.selector}"` + } + const values = Array.from(elements).map((el, i) => `[${i}] ${extractValue(el)}`) + result = `Encontrados ${elements.length} elementos:\n${values.join('\n---\n')}` + } else { + const el = container.querySelector(args.selector) + if (!el) { + return `No se encontro elemento con selector "${args.selector}"` + } + result = extractValue(el) + } + + if (result.length > limit) { + result = result.slice(0, limit) + `\n... (truncado, ${result.length - limit} chars mas)` + } + + return result + } + }, + { + name: 'edit_canvas', + description: 'Edita elementos del canvas. Similar a Edit pero para el DOM. Reemplaza old_value por new_value.', + category: 'canvas', + schema: { + type: 'object', + properties: { + selector: { type: 'string', description: 'Selector CSS del elemento a editar' }, + attribute: { + type: 'string', + enum: ['innerHTML', 'textContent', 'outerHTML', 'style', 'className'], + description: 'Que atributo editar (default: innerHTML)' + }, + old_value: { type: 'string', description: 'Valor a buscar y reemplazar (si no se pasa, reemplaza todo)' }, + new_value: { type: 'string', description: 'Nuevo valor' }, + all: { type: 'boolean', description: 'Si true, edita todos los matches (default: false)' } + }, + required: ['selector', 'new_value'] + }, + handler: (args: { selector: string; attribute?: string; old_value?: string; new_value: string; all?: boolean }) => { + const container = getCanvasContainer() + if (!container) return 'Error: canvas no encontrado' + + const attr = args.attribute || 'innerHTML' + const elements = args.all + ? Array.from(container.querySelectorAll(args.selector)) + : [container.querySelector(args.selector)].filter(Boolean) as Element[] + + if (elements.length === 0) { + return `Error: No se encontro elemento con selector "${args.selector}"` + } + + let editedCount = 0 + for (const el of elements) { + const htmlEl = el as HTMLElement + + if (args.old_value) { + // Modo Edit: buscar y reemplazar + const current = attr === 'style' ? htmlEl.style.cssText : (htmlEl as any)[attr] + if (current && current.includes(args.old_value)) { + const newVal = current.replace(args.old_value, args.new_value) + if (attr === 'style') { + htmlEl.style.cssText = newVal + } else { + (htmlEl as any)[attr] = newVal + } + editedCount++ + } + } else { + // Modo Write: reemplazar todo + if (attr === 'style') { + htmlEl.style.cssText = args.new_value + } else { + (htmlEl as any)[attr] = args.new_value + } + editedCount++ + } + } + + if (editedCount === 0) { + return `Error: old_value "${args.old_value}" no encontrado en ningun elemento` + } + + return `Editado ${editedCount} elemento(s) con selector "${args.selector}"` + } + }, + { + name: 'canvas_css', + description: 'Inyecta CSS en el canvas. Usa id para poder actualizar o remover despues.', + category: 'canvas', + schema: { + type: 'object', + properties: { + css: { type: 'string', description: 'Codigo CSS a inyectar' }, + id: { type: 'string', description: 'ID unico para el bloque de estilos (default: auto)' }, + mode: { + type: 'string', + enum: ['inject', 'replace', 'remove'], + description: 'inject (default), replace (reemplaza si existe), remove (elimina por id)' + } + }, + required: ['css'] + }, + handler: (args: { css: string; id?: string; mode?: string }) => { + const mode = args.mode || 'inject' + const styleId = args.id ? `canvas-css-${args.id}` : `canvas-css-${Date.now()}` + + if (mode === 'remove') { + if (!args.id) return 'Error: Se requiere id para remover' + const existing = document.getElementById(`canvas-css-${args.id}`) + if (existing) { + existing.remove() + return `CSS "${args.id}" removido` + } + return `Error: CSS "${args.id}" no encontrado` + } + + let styleEl = args.id ? document.getElementById(`canvas-css-${args.id}`) : null + + if (mode === 'replace' && styleEl) { + styleEl.textContent = args.css + return `CSS "${args.id}" actualizado` + } + + if (!styleEl) { + styleEl = document.createElement('style') + styleEl.id = styleId + document.head.appendChild(styleEl) + } + + styleEl.textContent = args.css + return `CSS inyectado con id "${styleId.replace('canvas-css-', '')}"` + } + }, + { + name: 'canvas_js', + description: 'Ejecuta JavaScript en el contexto del canvas. Tiene acceso a las ventanas y el DOM.', + category: 'canvas', + schema: { + type: 'object', + properties: { + code: { type: 'string', description: 'Codigo JavaScript a ejecutar' }, + async: { type: 'boolean', description: 'Si true, ejecuta como async y espera resultado' } + }, + required: ['code'] + }, + handler: async (args: { code: string; async?: boolean }) => { + try { + const canvas = getCanvasContainer() + const windowsStore = useWindowsStore() + + // Contexto disponible para el codigo + const context = { + canvas, + windows: windowsStore.windowsList, + getWindow: (id: string) => document.querySelector(`[data-window-id="${id}"]`), + $: (selector: string) => canvas?.querySelector(selector), + $$: (selector: string) => canvas?.querySelectorAll(selector) + } + + const fn = args.async + ? new Function('ctx', `return (async () => { with(ctx) { ${args.code} } })()`) + : new Function('ctx', `with(ctx) { ${args.code} }`) + + const result = args.async ? await fn(context) : fn(context) + + return result !== undefined ? String(result) : 'Ejecutado' + } catch (e: any) { + return `Error: ${e.message}` + } + } + }, + { + name: 'get_canvas_css', + description: 'Obtiene los bloques CSS inyectados en el canvas.', + category: 'canvas', + schema: { + type: 'object', + properties: { + id: { type: 'string', description: 'ID especifico del bloque CSS (opcional, si no se pasa lista todos)' } + }, + required: [] + }, + handler: (args: { id?: string }) => { + if (args.id) { + const styleEl = document.getElementById(`canvas-css-${args.id}`) + if (!styleEl) return `Error: CSS "${args.id}" no encontrado` + return styleEl.textContent || '(vacio)' + } + + // Listar todos + const styles = document.querySelectorAll('style[id^="canvas-css-"]') + if (styles.length === 0) { + return 'No hay CSS inyectado en el canvas' + } + + const list = Array.from(styles).map(s => { + const id = s.id.replace('canvas-css-', '') + const preview = (s.textContent || '').slice(0, 100) + return `- ${id}: ${preview}${(s.textContent?.length || 0) > 100 ? '...' : ''}` + }).join('\n') + + return `CSS inyectados (${styles.length}):\n${list}` + } } ] } diff --git a/frontend/src/services/tools/toolDefinitions.ts b/frontend/src/services/tools/toolDefinitions.ts index d0ddab1..1d8cce7 100644 --- a/frontend/src/services/tools/toolDefinitions.ts +++ b/frontend/src/services/tools/toolDefinitions.ts @@ -24,6 +24,12 @@ export const ALL_TOOL_METAS: ToolMeta[] = [ { name: 'resize_window', description: 'Cambia el tamano de una ventana', category: 'canvas' }, { name: 'close_window', description: 'Cierra una ventana del canvas', category: 'canvas' }, { name: 'list_windows', description: 'Lista todas las ventanas abiertas', category: 'canvas' }, + { name: 'inspect_window', description: 'Inspecciona el HTML de una ventana', category: 'canvas' }, + { name: 'get_canvas', description: 'Lee elementos del canvas con selector CSS', category: 'canvas' }, + { name: 'edit_canvas', description: 'Edita elementos del canvas (old_value -> new_value)', category: 'canvas' }, + { name: 'canvas_css', description: 'Inyecta CSS en el canvas', category: 'canvas' }, + { name: 'canvas_js', description: 'Ejecuta JavaScript en el canvas', category: 'canvas' }, + { name: 'get_canvas_css', description: 'Obtiene CSS inyectado en el canvas', category: 'canvas' }, // Component tools { name: 'save_vue_component', description: 'Guarda un componente Vue en la base de datos', category: 'component' },