feat: Add floating window system for canvas components
- Add WindowContainer.vue with Liquid Glass styling, drag, resize, close - Add windows store for managing window state (position, size, z-index) - Modify dynamicComponents.ts to wrap Vue components in floating windows - Add MCP tools: move_window, resize_window, close_window, list_windows - Add isolated Claude profiles (ejecutor, nucleo000) with versioned configs
This commit is contained in:
21
.claude-ejecutor/.gitignore
vendored
Normal file
21
.claude-ejecutor/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Credenciales (NUNCA versionar)
|
||||||
|
.credentials.json
|
||||||
|
|
||||||
|
# Estado interno de Claude (regenerable)
|
||||||
|
.claude.json
|
||||||
|
.claude.json.backup*
|
||||||
|
|
||||||
|
# Conversaciones y logs (muy grandes, privados)
|
||||||
|
projects/**/*.jsonl
|
||||||
|
history.jsonl
|
||||||
|
debug/
|
||||||
|
|
||||||
|
# Cache y temporales
|
||||||
|
cache/
|
||||||
|
tmp/
|
||||||
|
session-env/
|
||||||
|
shell-snapshots/
|
||||||
|
todos/
|
||||||
|
|
||||||
|
# Repos externos clonados
|
||||||
|
plugins/marketplaces/*/
|
||||||
150
.claude-ejecutor/CLAUDE.md
Normal file
150
.claude-ejecutor/CLAUDE.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# Ejecutor - Instrucciones
|
||||||
|
|
||||||
|
## Rol
|
||||||
|
Eres un agente especializado en manipular la interfaz de Agent UI exclusivamente a través de herramientas MCP.
|
||||||
|
|
||||||
|
## Reglas Estrictas
|
||||||
|
|
||||||
|
1. **SIEMPRE** responde usando `bubbleResponse` - nunca respondas con texto plano
|
||||||
|
2. **SOLO** puedes usar herramientas MCP de `agent-ui`
|
||||||
|
3. **NUNCA** intentes usar terminal, bash, curl, o cualquier comando del sistema
|
||||||
|
4. **NUNCA** intentes leer, escribir o editar archivos
|
||||||
|
5. Tu único propósito es manipular la interfaz gráfica
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sistema de Canvas
|
||||||
|
|
||||||
|
El canvas es un área donde puedes renderizar componentes Vue. Cada componente se muestra en una **ventana flotante estilo Liquid Glass** con:
|
||||||
|
- **Drag** - Arrastrar desde el header
|
||||||
|
- **Resize** - Desde bordes y esquinas
|
||||||
|
- **Close** - Botón rojo en el header
|
||||||
|
|
||||||
|
### render_vue_component
|
||||||
|
|
||||||
|
Renderiza un componente Vue 3 en una ventana flotante.
|
||||||
|
|
||||||
|
```js
|
||||||
|
render_vue_component({
|
||||||
|
// Requeridos
|
||||||
|
id: "mi-componente", // ID único
|
||||||
|
name: "Mi Componente", // Título de la ventana
|
||||||
|
template: "<div>HTML con sintaxis Vue</div>",
|
||||||
|
|
||||||
|
// Opcionales
|
||||||
|
setup: "const count = ref(0); return { count };",
|
||||||
|
style: ".mi-clase { color: white; }",
|
||||||
|
imports: ["ref", "reactive", "computed"],
|
||||||
|
componentProps: { valor: 123 },
|
||||||
|
|
||||||
|
// Posición y tamaño (opcionales)
|
||||||
|
x: 100, // Posición X (default: auto-cascada)
|
||||||
|
y: 100, // Posición Y (default: auto-cascada)
|
||||||
|
width: 300, // Ancho (default: 400)
|
||||||
|
height: 200, // Alto (default: 300)
|
||||||
|
|
||||||
|
// Modo
|
||||||
|
mode: "append" // "replace" limpia canvas, "append" agrega
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ejemplos de Componentes
|
||||||
|
|
||||||
|
**Contador interactivo:**
|
||||||
|
```js
|
||||||
|
render_vue_component({
|
||||||
|
id: "contador",
|
||||||
|
name: "Contador",
|
||||||
|
template: `
|
||||||
|
<div style="text-align: center; color: white;">
|
||||||
|
<h2>{{ count }}</h2>
|
||||||
|
<button @click="count++">+1</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
imports: ["ref"],
|
||||||
|
setup: "const count = ref(0); return { count };",
|
||||||
|
x: 100, y: 100, width: 200, height: 150
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lista dinámica:**
|
||||||
|
```js
|
||||||
|
render_vue_component({
|
||||||
|
id: "lista",
|
||||||
|
name: "Lista",
|
||||||
|
template: `
|
||||||
|
<div style="color: white;">
|
||||||
|
<input v-model="nuevo" @keyup.enter="agregar" placeholder="Agregar..."/>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(item, i) in items" :key="i">{{ item }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
imports: ["ref"],
|
||||||
|
setup: `
|
||||||
|
const items = ref(['Item 1', 'Item 2']);
|
||||||
|
const nuevo = ref('');
|
||||||
|
const agregar = () => {
|
||||||
|
if (nuevo.value) {
|
||||||
|
items.value.push(nuevo.value);
|
||||||
|
nuevo.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { items, nuevo, agregar };
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vue Composition API
|
||||||
|
|
||||||
|
Imports disponibles:
|
||||||
|
- `ref` - Valores reactivos
|
||||||
|
- `reactive` - Objetos reactivos
|
||||||
|
- `computed` - Valores computados
|
||||||
|
- `watch` - Observar cambios
|
||||||
|
- `onMounted` - Hook de montaje
|
||||||
|
- `onUnmounted` - Hook de desmontaje
|
||||||
|
|
||||||
|
### Helpers Globales
|
||||||
|
|
||||||
|
En el setup tienes acceso a:
|
||||||
|
- `$emit(event, ...args)` - Emitir eventos
|
||||||
|
- `$on(event, callback)` - Escuchar eventos
|
||||||
|
- `$fetch(url)` - Hacer requests HTTP
|
||||||
|
- `$theme.getVariable(name)` - Obtener variable CSS
|
||||||
|
- `$theme.setVariable(name, value)` - Cambiar variable CSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Otras Herramientas
|
||||||
|
|
||||||
|
| Herramienta | Uso |
|
||||||
|
|-------------|-----|
|
||||||
|
| `bubbleResponse` | Responder al usuario (OBLIGATORIO) |
|
||||||
|
| `render_html` | Renderizar HTML plano |
|
||||||
|
| `navigate_to` | Cambiar de página |
|
||||||
|
| `page_refresh` | Refrescar página |
|
||||||
|
| `get_design_tokens` | Obtener tokens del tema |
|
||||||
|
| `set_theme_variable` | Cambiar variable del tema |
|
||||||
|
| `switch_theme` | Cambiar tema activo |
|
||||||
|
| `list_available_tools` | Ver herramientas disponibles |
|
||||||
|
| `activate_tool` | Activar una herramienta |
|
||||||
|
| `pin_tool` | Fijar herramienta |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Formato de Respuesta
|
||||||
|
|
||||||
|
**SIEMPRE** usa bubbleResponse para comunicarte:
|
||||||
|
```
|
||||||
|
bubbleResponse({ message: "Tu mensaje aquí" })
|
||||||
|
```
|
||||||
|
|
||||||
|
Nunca escribas texto directamente - todo debe ir a través de bubbleResponse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Preferencias del Usuario
|
||||||
|
|
||||||
|
- **Detalles sutiles**: Agregar pequeños toques creativos que mejoren el ambiente SIN estorbar el trabajo normal. No widgets completos ni elementos que ocupen espacio - solo detalles casi imperceptibles que den personalidad (ej: un emoji contextual, un color que cambie según la hora, un micro-detalle temático).
|
||||||
|
- La clave es que el detalle **no interrumpa** ni **ocupe espacio útil**.
|
||||||
10
.claude-ejecutor/plugins/known_marketplaces.json
Normal file
10
.claude-ejecutor/plugins/known_marketplaces.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"claude-plugins-official": {
|
||||||
|
"source": {
|
||||||
|
"source": "github",
|
||||||
|
"repo": "anthropics/claude-plugins-official"
|
||||||
|
},
|
||||||
|
"installLocation": "C:\\Users\\jodar\\agent-ui\\.claude-ejecutor\\plugins\\marketplaces\\claude-plugins-official",
|
||||||
|
"lastUpdated": "2026-02-15T01:40:47.405Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
.claude-ejecutor/settings.json
Normal file
22
.claude-ejecutor/settings.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"DISABLE_TELEMETRY": "1"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"mcp__agent-ui*"
|
||||||
|
],
|
||||||
|
"deny": [
|
||||||
|
"Bash",
|
||||||
|
"Edit",
|
||||||
|
"Write",
|
||||||
|
"Read",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"WebFetch",
|
||||||
|
"WebSearch",
|
||||||
|
"Task",
|
||||||
|
"NotebookEdit"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
10
.claude-isolated/.gitignore
vendored
Normal file
10
.claude-isolated/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# No versionar credenciales
|
||||||
|
.credentials.json
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Estado de sesión (regenerable)
|
||||||
|
.claude.json
|
||||||
|
.claude.json.backup
|
||||||
|
|
||||||
|
# Archivos temporales
|
||||||
|
tmp/
|
||||||
9
.claude-isolated/settings.json
Normal file
9
.claude-isolated/settings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"DISABLE_TELEMETRY": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
.claude-nucleo000/.gitignore
vendored
Normal file
21
.claude-nucleo000/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Credenciales (NUNCA versionar)
|
||||||
|
.credentials.json
|
||||||
|
|
||||||
|
# Estado interno de Claude (regenerable)
|
||||||
|
.claude.json
|
||||||
|
.claude.json.backup*
|
||||||
|
|
||||||
|
# Conversaciones y logs (muy grandes, privados)
|
||||||
|
projects/**/*.jsonl
|
||||||
|
history.jsonl
|
||||||
|
debug/
|
||||||
|
|
||||||
|
# Cache y temporales
|
||||||
|
cache/
|
||||||
|
tmp/
|
||||||
|
session-env/
|
||||||
|
shell-snapshots/
|
||||||
|
todos/
|
||||||
|
|
||||||
|
# Repos externos clonados
|
||||||
|
plugins/marketplaces/*/
|
||||||
9
.claude-nucleo000/settings.json
Normal file
9
.claude-nucleo000/settings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"DISABLE_TELEMETRY": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,13 +63,15 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-content {
|
.canvas-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 1.5rem;
|
position: relative;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-placeholder {
|
.canvas-placeholder {
|
||||||
|
|||||||
407
frontend/src/components/WindowContainer.vue
Normal file
407
frontend/src/components/WindowContainer.vue
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
x?: number
|
||||||
|
y?: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
minWidth?: number
|
||||||
|
minHeight?: number
|
||||||
|
}>(), {
|
||||||
|
title: 'Window',
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
minWidth: 200,
|
||||||
|
minHeight: 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
move: [pos: { x: number; y: number }]
|
||||||
|
resize: [size: { width: number; height: number }]
|
||||||
|
focus: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Estado interno de la ventana
|
||||||
|
const currentX = ref(props.x)
|
||||||
|
const currentY = ref(props.y)
|
||||||
|
const currentWidth = ref(props.width)
|
||||||
|
const currentHeight = ref(props.height)
|
||||||
|
const zIndex = ref(100)
|
||||||
|
|
||||||
|
// Estado de drag
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const dragStartX = ref(0)
|
||||||
|
const dragStartY = ref(0)
|
||||||
|
const dragOffsetX = ref(0)
|
||||||
|
const dragOffsetY = ref(0)
|
||||||
|
|
||||||
|
// Estado de resize
|
||||||
|
const isResizing = ref(false)
|
||||||
|
const resizeDirection = ref('')
|
||||||
|
const resizeStartX = ref(0)
|
||||||
|
const resizeStartY = ref(0)
|
||||||
|
const resizeStartWidth = ref(0)
|
||||||
|
const resizeStartHeight = ref(0)
|
||||||
|
|
||||||
|
const windowStyle = computed(() => ({
|
||||||
|
left: `${currentX.value}px`,
|
||||||
|
top: `${currentY.value}px`,
|
||||||
|
width: `${currentWidth.value}px`,
|
||||||
|
height: `${currentHeight.value}px`,
|
||||||
|
zIndex: zIndex.value
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Drag handlers
|
||||||
|
function startDrag(e: MouseEvent) {
|
||||||
|
if ((e.target as HTMLElement).closest('.window-controls')) return
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
dragStartX.value = e.clientX
|
||||||
|
dragStartY.value = e.clientY
|
||||||
|
dragOffsetX.value = currentX.value
|
||||||
|
dragOffsetY.value = currentY.value
|
||||||
|
|
||||||
|
bringToFront()
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onDrag)
|
||||||
|
document.addEventListener('mouseup', stopDrag)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrag(e: MouseEvent) {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
|
||||||
|
const deltaX = e.clientX - dragStartX.value
|
||||||
|
const deltaY = e.clientY - dragStartY.value
|
||||||
|
|
||||||
|
currentX.value = Math.max(0, dragOffsetX.value + deltaX)
|
||||||
|
currentY.value = Math.max(0, dragOffsetY.value + deltaY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrag() {
|
||||||
|
if (isDragging.value) {
|
||||||
|
isDragging.value = false
|
||||||
|
emit('move', { x: currentX.value, y: currentY.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', onDrag)
|
||||||
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize handlers
|
||||||
|
function startResize(direction: string, e: MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
isResizing.value = true
|
||||||
|
resizeDirection.value = direction
|
||||||
|
resizeStartX.value = e.clientX
|
||||||
|
resizeStartY.value = e.clientY
|
||||||
|
resizeStartWidth.value = currentWidth.value
|
||||||
|
resizeStartHeight.value = currentHeight.value
|
||||||
|
|
||||||
|
bringToFront()
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onResize)
|
||||||
|
document.addEventListener('mouseup', stopResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize(e: MouseEvent) {
|
||||||
|
if (!isResizing.value) return
|
||||||
|
|
||||||
|
const deltaX = e.clientX - resizeStartX.value
|
||||||
|
const deltaY = e.clientY - resizeStartY.value
|
||||||
|
|
||||||
|
if (resizeDirection.value.includes('e')) {
|
||||||
|
currentWidth.value = Math.max(props.minWidth, resizeStartWidth.value + deltaX)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeDirection.value.includes('s')) {
|
||||||
|
currentHeight.value = Math.max(props.minHeight, resizeStartHeight.value + deltaY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeDirection.value.includes('w')) {
|
||||||
|
const newWidth = Math.max(props.minWidth, resizeStartWidth.value - deltaX)
|
||||||
|
if (newWidth !== currentWidth.value) {
|
||||||
|
currentX.value = resizeStartX.value + (resizeStartWidth.value - newWidth) + (e.clientX - resizeStartX.value) - deltaX
|
||||||
|
currentWidth.value = newWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeDirection.value.includes('n')) {
|
||||||
|
const newHeight = Math.max(props.minHeight, resizeStartHeight.value - deltaY)
|
||||||
|
if (newHeight !== currentHeight.value) {
|
||||||
|
currentY.value = resizeStartY.value + (resizeStartHeight.value - newHeight) + (e.clientY - resizeStartY.value) - deltaY
|
||||||
|
currentHeight.value = newHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopResize() {
|
||||||
|
if (isResizing.value) {
|
||||||
|
isResizing.value = false
|
||||||
|
emit('resize', { width: currentWidth.value, height: currentHeight.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', onResize)
|
||||||
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Z-index management
|
||||||
|
let maxZIndex = 100
|
||||||
|
|
||||||
|
function bringToFront() {
|
||||||
|
maxZIndex++
|
||||||
|
zIndex.value = maxZIndex
|
||||||
|
emit('focus')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWindowClick() {
|
||||||
|
bringToFront()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousemove', onDrag)
|
||||||
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
|
document.removeEventListener('mousemove', onResize)
|
||||||
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="window-container"
|
||||||
|
:style="windowStyle"
|
||||||
|
:data-window-id="id"
|
||||||
|
@mousedown="handleWindowClick"
|
||||||
|
>
|
||||||
|
<!-- Header / Title bar -->
|
||||||
|
<div class="window-header" @mousedown="startDrag">
|
||||||
|
<span class="window-title">{{ title }}</span>
|
||||||
|
<div class="window-controls">
|
||||||
|
<button @click.stop="emit('close')" class="btn-close" title="Cerrar">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
|
||||||
|
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content area -->
|
||||||
|
<div class="window-content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resize handles -->
|
||||||
|
<div class="resize-handle resize-n" @mousedown="startResize('n', $event)"></div>
|
||||||
|
<div class="resize-handle resize-e" @mousedown="startResize('e', $event)"></div>
|
||||||
|
<div class="resize-handle resize-s" @mousedown="startResize('s', $event)"></div>
|
||||||
|
<div class="resize-handle resize-w" @mousedown="startResize('w', $event)"></div>
|
||||||
|
<div class="resize-handle resize-ne" @mousedown="startResize('ne', $event)"></div>
|
||||||
|
<div class="resize-handle resize-se" @mousedown="startResize('se', $event)"></div>
|
||||||
|
<div class="resize-handle resize-sw" @mousedown="startResize('sw', $event)"></div>
|
||||||
|
<div class="resize-handle resize-nw" @mousedown="startResize('nw', $event)"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.window-container {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.2),
|
||||||
|
inset 0 0.5px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 200px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 59, 48, 0.7);
|
||||||
|
color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close svg {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
background: rgba(255, 59, 48, 1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover svg {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 10px;
|
||||||
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content::-webkit-scrollbar {
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resize handles */
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-n {
|
||||||
|
top: 0;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
height: 4px;
|
||||||
|
cursor: n-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-e {
|
||||||
|
right: 0;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 4px;
|
||||||
|
cursor: e-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-s {
|
||||||
|
bottom: 0;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
height: 4px;
|
||||||
|
cursor: s-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-w {
|
||||||
|
left: 0;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 4px;
|
||||||
|
cursor: w-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-ne {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: ne-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-se {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: se-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-sw {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: sw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-nw {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: nw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicador visual de resize en esquinas */
|
||||||
|
.resize-se::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3px;
|
||||||
|
right: 3px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-right: 2px solid #444;
|
||||||
|
border-bottom: 2px solid #444;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-container:hover .resize-se::after {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
import { setActivePinia, type Pinia } from 'pinia'
|
import { setActivePinia, type Pinia } from 'pinia'
|
||||||
import { useCanvasStore } from '../stores/canvas'
|
import { useCanvasStore } from '../stores/canvas'
|
||||||
import { useThemeStore } from '../stores/theme'
|
import { useThemeStore } from '../stores/theme'
|
||||||
|
import { useWindowsStore } from '../stores/windows'
|
||||||
|
import WindowContainer from '../components/WindowContainer.vue'
|
||||||
|
|
||||||
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
|
||||||
const API_URL = ''
|
const API_URL = ''
|
||||||
@@ -183,6 +185,12 @@ function getThemeStore() {
|
|||||||
return useThemeStore()
|
return useThemeStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWindowsStore() {
|
||||||
|
const globalPinia = (window as any).__pinia as Pinia | undefined
|
||||||
|
if (globalPinia) setActivePinia(globalPinia)
|
||||||
|
return useWindowsStore()
|
||||||
|
}
|
||||||
|
|
||||||
const dynamicHelpers = {
|
const dynamicHelpers = {
|
||||||
$emit: (event: string, ...args: any[]) => eventBus.emit(event, ...args),
|
$emit: (event: string, ...args: any[]) => eventBus.emit(event, ...args),
|
||||||
$on: (event: string, cb: EventCallback) => eventBus.on(event, cb),
|
$on: (event: string, cb: EventCallback) => eventBus.on(event, cb),
|
||||||
@@ -271,25 +279,47 @@ export function buildComponent(definition: VueComponentDefinition): Component {
|
|||||||
|
|
||||||
const renderedContainers: Map<string, HTMLElement> = new Map()
|
const renderedContainers: Map<string, HTMLElement> = new Map()
|
||||||
|
|
||||||
|
export interface LayoutOptions {
|
||||||
|
x?: number
|
||||||
|
y?: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
export function renderInlineComponent(
|
export function renderInlineComponent(
|
||||||
definition: VueComponentDefinition,
|
definition: VueComponentDefinition,
|
||||||
target: HTMLElement,
|
target: HTMLElement,
|
||||||
props: Record<string, any> = {},
|
props: Record<string, any> = {},
|
||||||
append: boolean = false
|
append: boolean = false,
|
||||||
|
layout?: LayoutOptions
|
||||||
): { unmount: () => void } {
|
): { unmount: () => void } {
|
||||||
const scopeId = generateScopeId(definition.id)
|
const scopeId = generateScopeId(definition.id)
|
||||||
|
const windowsStore = getWindowsStore()
|
||||||
|
|
||||||
|
// Registrar ventana en el store
|
||||||
|
const windowState = windowsStore.register(definition.id, {
|
||||||
|
title: definition.name,
|
||||||
|
x: layout?.x,
|
||||||
|
y: layout?.y,
|
||||||
|
width: layout?.width ?? 400,
|
||||||
|
height: layout?.height ?? 300
|
||||||
|
})
|
||||||
|
|
||||||
const container = document.createElement('div')
|
const container = document.createElement('div')
|
||||||
container.id = `inline-${definition.id}`
|
container.id = `inline-${definition.id}`
|
||||||
container.className = 'dynamic-component-wrapper'
|
container.className = 'dynamic-component-wrapper'
|
||||||
container.setAttribute(`data-${scopeId}`, '')
|
container.setAttribute(`data-${scopeId}`, '')
|
||||||
|
|
||||||
|
// Siempre hacer append para ventanas flotantes
|
||||||
if (append) {
|
if (append) {
|
||||||
target.appendChild(container)
|
target.appendChild(container)
|
||||||
} else {
|
} else {
|
||||||
|
// En modo replace, limpiar contenedor anterior si existe
|
||||||
const oldContainer = renderedContainers.get(definition.id)
|
const oldContainer = renderedContainers.get(definition.id)
|
||||||
if (oldContainer) render(null, oldContainer)
|
if (oldContainer) {
|
||||||
target.innerHTML = ''
|
render(null, oldContainer)
|
||||||
|
oldContainer.remove()
|
||||||
|
}
|
||||||
target.appendChild(container)
|
target.appendChild(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,25 +330,52 @@ export function renderInlineComponent(
|
|||||||
const isAsync = definition.setup ? /\bawait\b/.test(definition.setup) : false
|
const isAsync = definition.setup ? /\bawait\b/.test(definition.setup) : false
|
||||||
const component = buildComponent(definition)
|
const component = buildComponent(definition)
|
||||||
|
|
||||||
const vnode = isAsync
|
// Función unmount que se llamará al cerrar la ventana
|
||||||
|
const unmount = () => {
|
||||||
|
render(null, container)
|
||||||
|
renderedContainers.delete(definition.id)
|
||||||
|
windowsStore.remove(definition.id)
|
||||||
|
container.remove()
|
||||||
|
document.getElementById(`style-${definition.id}`)?.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear el componente interno
|
||||||
|
const innerComponent = isAsync
|
||||||
? createVNode(Suspense, null, {
|
? createVNode(Suspense, null, {
|
||||||
default: () => createVNode(component, props),
|
default: () => createVNode(component, props),
|
||||||
fallback: () => createVNode('div', { class: 'loading' }, 'Loading...')
|
fallback: () => createVNode('div', { class: 'loading' }, 'Loading...')
|
||||||
})
|
})
|
||||||
: createVNode(component, props)
|
: createVNode(component, props)
|
||||||
|
|
||||||
const mainApp = (window as any).__vueApp as App | undefined
|
// Envolver en WindowContainer
|
||||||
if (mainApp?._context) vnode.appContext = mainApp._context
|
const windowVNode = createVNode(
|
||||||
|
WindowContainer,
|
||||||
|
{
|
||||||
|
id: definition.id,
|
||||||
|
title: definition.name,
|
||||||
|
x: windowState.x,
|
||||||
|
y: windowState.y,
|
||||||
|
width: windowState.width,
|
||||||
|
height: windowState.height,
|
||||||
|
onClose: unmount,
|
||||||
|
onMove: (pos: { x: number; y: number }) => {
|
||||||
|
windowsStore.updatePosition(definition.id, pos.x, pos.y)
|
||||||
|
},
|
||||||
|
onResize: (size: { width: number; height: number }) => {
|
||||||
|
windowsStore.updateSize(definition.id, size.width, size.height)
|
||||||
|
},
|
||||||
|
onFocus: () => {
|
||||||
|
windowsStore.bringToFront(definition.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => innerComponent }
|
||||||
|
)
|
||||||
|
|
||||||
render(vnode, container)
|
const mainApp = (window as any).__vueApp as App | undefined
|
||||||
|
if (mainApp?._context) windowVNode.appContext = mainApp._context
|
||||||
|
|
||||||
|
render(windowVNode, container)
|
||||||
renderedContainers.set(definition.id, container)
|
renderedContainers.set(definition.id, container)
|
||||||
|
|
||||||
return {
|
return { unmount }
|
||||||
unmount: () => {
|
|
||||||
render(null, container)
|
|
||||||
renderedContainers.delete(definition.id)
|
|
||||||
container.remove()
|
|
||||||
document.getElementById(`style-${definition.id}`)?.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ function getToolConfigs(): Map<string, ToolConfig> {
|
|||||||
// Category to tool names mapping
|
// Category to tool names mapping
|
||||||
const categoryTools: Record<ToolCategory, string[]> = {
|
const categoryTools: Record<ToolCategory, string[]> = {
|
||||||
global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'],
|
global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'],
|
||||||
canvas: ['render_html', 'render_vue_component'],
|
canvas: ['render_html', 'render_vue_component', 'move_window', 'resize_window', 'close_window', 'list_windows'],
|
||||||
component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component'],
|
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'],
|
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'],
|
database: ['list_tables', 'get_table_schema', 'get_table_data', 'get_database_stats', 'execute_query'],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ToolConfig } from './index'
|
import type { ToolConfig } from './index'
|
||||||
import { useCanvasStore } from '../../../stores/canvas'
|
import { useCanvasStore } from '../../../stores/canvas'
|
||||||
|
import { useWindowsStore } from '../../../stores/windows'
|
||||||
import {
|
import {
|
||||||
renderInlineComponent,
|
renderInlineComponent,
|
||||||
type VueComponentDefinition
|
type VueComponentDefinition
|
||||||
@@ -82,7 +83,7 @@ export function createCanvasHandlers(): ToolConfig[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'render_vue_component',
|
name: 'render_vue_component',
|
||||||
description: 'Renderiza un componente Vue 3 completo con ref, reactive, computed, etc.',
|
description: 'Renderiza un componente Vue 3 en una ventana flotante con drag, resize y close.',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -95,7 +96,11 @@ export function createCanvasHandlers(): ToolConfig[] {
|
|||||||
props: { type: 'array', items: { type: 'string' }, description: 'Lista de props' },
|
props: { type: 'array', items: { type: 'string' }, description: 'Lista de props' },
|
||||||
imports: { type: 'array', items: { type: 'string' }, description: 'Funciones de Vue a importar' },
|
imports: { type: 'array', items: { type: 'string' }, description: 'Funciones de Vue a importar' },
|
||||||
componentProps: { type: 'object', description: 'Valores para las props' },
|
componentProps: { type: 'object', description: 'Valores para las props' },
|
||||||
mode: { type: 'string', enum: ['replace', 'append'], description: 'Modo de renderizado' }
|
mode: { type: 'string', enum: ['replace', 'append'], description: 'Modo de renderizado' },
|
||||||
|
x: { type: 'number', description: 'Posicion X inicial de la ventana' },
|
||||||
|
y: { type: 'number', description: 'Posicion Y inicial de la ventana' },
|
||||||
|
width: { type: 'number', description: 'Ancho inicial de la ventana' },
|
||||||
|
height: { type: 'number', description: 'Alto inicial de la ventana' }
|
||||||
},
|
},
|
||||||
required: ['id', 'name', 'template']
|
required: ['id', 'name', 'template']
|
||||||
},
|
},
|
||||||
@@ -109,6 +114,10 @@ export function createCanvasHandlers(): ToolConfig[] {
|
|||||||
imports?: string[]
|
imports?: string[]
|
||||||
componentProps?: Record<string, any>
|
componentProps?: Record<string, any>
|
||||||
mode?: string
|
mode?: string
|
||||||
|
x?: number
|
||||||
|
y?: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
}) => {
|
}) => {
|
||||||
const container = getCanvasContainer()
|
const container = getCanvasContainer()
|
||||||
if (!container) return 'Error: canvas no encontrado'
|
if (!container) return 'Error: canvas no encontrado'
|
||||||
@@ -126,7 +135,14 @@ export function createCanvasHandlers(): ToolConfig[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAppend = args.mode === 'append'
|
const isAppend = args.mode === 'append'
|
||||||
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend)
|
const layout = {
|
||||||
|
x: args.x,
|
||||||
|
y: args.y,
|
||||||
|
width: args.width,
|
||||||
|
height: args.height
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend, layout)
|
||||||
|
|
||||||
;(window as any).__vueComponentUnmount = result.unmount
|
;(window as any).__vueComponentUnmount = result.unmount
|
||||||
|
|
||||||
@@ -134,7 +150,128 @@ export function createCanvasHandlers(): ToolConfig[] {
|
|||||||
|
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
canvasStore.addToHistory({ tool: 'render_vue_component', args, timestamp: Date.now() })
|
canvasStore.addToHistory({ tool: 'render_vue_component', args, timestamp: Date.now() })
|
||||||
return `Componente Vue "${args.name}" renderizado`
|
return `Componente Vue "${args.name}" renderizado en ventana flotante`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'move_window',
|
||||||
|
description: 'Mueve una ventana a una posicion especifica en el canvas.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID de la ventana a mover' },
|
||||||
|
x: { type: 'number', description: 'Nueva posicion X' },
|
||||||
|
y: { type: 'number', description: 'Nueva posicion Y' }
|
||||||
|
},
|
||||||
|
required: ['id', 'x', 'y']
|
||||||
|
},
|
||||||
|
handler: (args: { id: string; x: number; y: number }) => {
|
||||||
|
const windowsStore = useWindowsStore()
|
||||||
|
|
||||||
|
if (!windowsStore.has(args.id)) {
|
||||||
|
return `Error: Ventana "${args.id}" no encontrada`
|
||||||
|
}
|
||||||
|
|
||||||
|
windowsStore.updatePosition(args.id, args.x, args.y)
|
||||||
|
|
||||||
|
// Actualizar el DOM directamente
|
||||||
|
const windowEl = document.querySelector(`[data-window-id="${args.id}"]`) as HTMLElement
|
||||||
|
if (windowEl) {
|
||||||
|
windowEl.style.left = `${args.x}px`
|
||||||
|
windowEl.style.top = `${args.y}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Ventana "${args.id}" movida a (${args.x}, ${args.y})`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resize_window',
|
||||||
|
description: 'Cambia el tamano de una ventana en el canvas.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID de la ventana a redimensionar' },
|
||||||
|
width: { type: 'number', description: 'Nuevo ancho' },
|
||||||
|
height: { type: 'number', description: 'Nuevo alto' }
|
||||||
|
},
|
||||||
|
required: ['id', 'width', 'height']
|
||||||
|
},
|
||||||
|
handler: (args: { id: string; width: number; height: number }) => {
|
||||||
|
const windowsStore = useWindowsStore()
|
||||||
|
|
||||||
|
if (!windowsStore.has(args.id)) {
|
||||||
|
return `Error: Ventana "${args.id}" no encontrada`
|
||||||
|
}
|
||||||
|
|
||||||
|
windowsStore.updateSize(args.id, args.width, args.height)
|
||||||
|
|
||||||
|
// Actualizar el DOM directamente
|
||||||
|
const windowEl = document.querySelector(`[data-window-id="${args.id}"]`) as HTMLElement
|
||||||
|
if (windowEl) {
|
||||||
|
windowEl.style.width = `${args.width}px`
|
||||||
|
windowEl.style.height = `${args.height}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Ventana "${args.id}" redimensionada a ${args.width}x${args.height}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'close_window',
|
||||||
|
description: 'Cierra una ventana del canvas.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID de la ventana a cerrar' }
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
|
handler: (args: { id: string }) => {
|
||||||
|
const windowsStore = useWindowsStore()
|
||||||
|
|
||||||
|
if (!windowsStore.has(args.id)) {
|
||||||
|
return `Error: Ventana "${args.id}" no encontrada`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar y eliminar el contenedor
|
||||||
|
const container = document.getElementById(`inline-${args.id}`)
|
||||||
|
if (container) {
|
||||||
|
container.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar estilos
|
||||||
|
document.getElementById(`style-${args.id}`)?.remove()
|
||||||
|
|
||||||
|
// Eliminar del store
|
||||||
|
windowsStore.remove(args.id)
|
||||||
|
|
||||||
|
return `Ventana "${args.id}" cerrada`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list_windows',
|
||||||
|
description: 'Lista todas las ventanas abiertas en el canvas.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: []
|
||||||
|
},
|
||||||
|
handler: () => {
|
||||||
|
const windowsStore = useWindowsStore()
|
||||||
|
const windows = windowsStore.windowsList
|
||||||
|
|
||||||
|
if (windows.length === 0) {
|
||||||
|
return 'No hay ventanas abiertas'
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = windows.map(w =>
|
||||||
|
`- ${w.id}: "${w.title}" en (${w.x}, ${w.y}) - ${w.width}x${w.height}`
|
||||||
|
).join('\n')
|
||||||
|
|
||||||
|
return `Ventanas abiertas (${windows.length}):\n${list}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export const ALL_TOOL_METAS: ToolMeta[] = [
|
|||||||
// Canvas tools
|
// Canvas tools
|
||||||
{ name: 'render_html', description: 'Renderiza HTML en el canvas', category: 'canvas' },
|
{ name: 'render_html', description: 'Renderiza HTML en el canvas', category: 'canvas' },
|
||||||
{ name: 'render_vue_component', description: 'Renderiza un componente Vue 3 completo', category: 'canvas' },
|
{ name: 'render_vue_component', description: 'Renderiza un componente Vue 3 completo', category: 'canvas' },
|
||||||
|
{ name: 'move_window', description: 'Mueve una ventana a una posicion especifica', category: 'canvas' },
|
||||||
|
{ 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' },
|
||||||
|
|
||||||
// Component tools
|
// Component tools
|
||||||
{ name: 'save_vue_component', description: 'Guarda un componente Vue en la base de datos', category: 'component' },
|
{ name: 'save_vue_component', description: 'Guarda un componente Vue en la base de datos', category: 'component' },
|
||||||
@@ -110,5 +114,6 @@ export const CATEGORY_INFO: Record<ToolCategory, { label: string; color: string;
|
|||||||
source: { label: 'Source', color: '#8b5cf6', icon: 'M16 18l6-6-6-6M8 6l-6 6 6 6' },
|
source: { label: 'Source', color: '#8b5cf6', icon: 'M16 18l6-6-6-6M8 6l-6 6 6 6' },
|
||||||
project: { label: 'Project', color: '#06b6d4', icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z' },
|
project: { label: 'Project', color: '#06b6d4', icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z' },
|
||||||
terminal: { label: 'Terminal', color: '#22c55e', icon: 'M4 17l6-6-6-6M12 19h8' },
|
terminal: { label: 'Terminal', color: '#22c55e', icon: 'M4 17l6-6-6-6M12 19h8' },
|
||||||
git: { label: 'Git', color: '#f97316', icon: 'M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9' }
|
git: { label: 'Git', color: '#f97316', icon: 'M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9' },
|
||||||
|
torch: { label: 'Torch', color: '#eab308', icon: 'M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z' }
|
||||||
}
|
}
|
||||||
|
|||||||
125
frontend/src/stores/windows.ts
Normal file
125
frontend/src/stores/windows.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
export interface WindowState {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
zIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWindowsStore = defineStore('windows', () => {
|
||||||
|
const windows = ref<Map<string, WindowState>>(new Map())
|
||||||
|
const maxZIndex = ref(100)
|
||||||
|
const windowOffset = ref(0)
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const windowsList = computed(() => Array.from(windows.value.values()))
|
||||||
|
const windowCount = computed(() => windows.value.size)
|
||||||
|
|
||||||
|
// Obtener siguiente posición para ventana nueva (cascada)
|
||||||
|
function getNextPosition() {
|
||||||
|
const offset = windowOffset.value * 30
|
||||||
|
windowOffset.value = (windowOffset.value + 1) % 10
|
||||||
|
return { x: 50 + offset, y: 50 + offset }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registrar nueva ventana
|
||||||
|
function register(id: string, state: Partial<WindowState> = {}) {
|
||||||
|
const pos = state.x !== undefined && state.y !== undefined
|
||||||
|
? { x: state.x, y: state.y }
|
||||||
|
: getNextPosition()
|
||||||
|
|
||||||
|
windows.value.set(id, {
|
||||||
|
id,
|
||||||
|
title: state.title ?? id,
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
width: state.width ?? 400,
|
||||||
|
height: state.height ?? 300,
|
||||||
|
zIndex: ++maxZIndex.value
|
||||||
|
})
|
||||||
|
|
||||||
|
return windows.value.get(id)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traer ventana al frente
|
||||||
|
function bringToFront(id: string) {
|
||||||
|
const win = windows.value.get(id)
|
||||||
|
if (win) {
|
||||||
|
win.zIndex = ++maxZIndex.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar posición
|
||||||
|
function updatePosition(id: string, x: number, y: number) {
|
||||||
|
const win = windows.value.get(id)
|
||||||
|
if (win) {
|
||||||
|
win.x = x
|
||||||
|
win.y = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar tamaño
|
||||||
|
function updateSize(id: string, width: number, height: number) {
|
||||||
|
const win = windows.value.get(id)
|
||||||
|
if (win) {
|
||||||
|
win.width = width
|
||||||
|
win.height = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar título
|
||||||
|
function updateTitle(id: string, title: string) {
|
||||||
|
const win = windows.value.get(id)
|
||||||
|
if (win) {
|
||||||
|
win.title = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar ventana
|
||||||
|
function remove(id: string) {
|
||||||
|
windows.value.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener ventana por ID
|
||||||
|
function get(id: string) {
|
||||||
|
return windows.value.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si existe
|
||||||
|
function has(id: string) {
|
||||||
|
return windows.value.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar todas las ventanas
|
||||||
|
function clear() {
|
||||||
|
windows.value.clear()
|
||||||
|
windowOffset.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
windows,
|
||||||
|
maxZIndex,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
windowsList,
|
||||||
|
windowCount,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
register,
|
||||||
|
bringToFront,
|
||||||
|
updatePosition,
|
||||||
|
updateSize,
|
||||||
|
updateTitle,
|
||||||
|
remove,
|
||||||
|
get,
|
||||||
|
has,
|
||||||
|
clear,
|
||||||
|
getNextPosition
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user