@decido/plugin-dev-console 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +13 -0
- package/package.json +30 -0
- package/src/components/SafeComponentWrapper.tsx +120 -0
- package/src/components/react-shadow.d.ts +5 -0
- package/src/components/ui/CodeSnippet.tsx +39 -0
- package/src/components/ui/OSNode.tsx +25 -0
- package/src/data/tutorialSteps.tsx +120 -0
- package/src/engine/DependencyRegistry.ts +29 -0
- package/src/engine/PlaygroundEngine.ts +272 -0
- package/src/engine/SecurityProxy.ts +34 -0
- package/src/index.ts +142 -0
- package/src/widgets/DeveloperPlaygroundWidget.tsx +211 -0
- package/src/widgets/ForgeTerminalWidget.tsx +202 -0
- package/src/widgets/SDKShowcaseWidget.tsx +289 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
Debugger listening on ws://127.0.0.1:62233/8c9e1551-3f82-44ce-842b-b58a12921ab2
|
|
3
|
+
For help, see: https://nodejs.org/en/docs/inspector
|
|
4
|
+
Debugger attached.
|
|
5
|
+
|
|
6
|
+
> @decido/plugin-dev-console@1.0.0 build /Users/julioramirez/dev/active/OnBoardingDecido/plugins/official/dev-console
|
|
7
|
+
> tsc
|
|
8
|
+
|
|
9
|
+
Debugger listening on ws://127.0.0.1:62247/13f2e458-f583-4510-9bed-17f01f07ffc2
|
|
10
|
+
For help, see: https://nodejs.org/en/docs/inspector
|
|
11
|
+
Debugger attached.
|
|
12
|
+
Waiting for the debugger to disconnect...
|
|
13
|
+
Waiting for the debugger to disconnect...
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@decido/plugin-dev-console",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"types": "src/index.ts",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@xyflow/react": "^12.0.0",
|
|
8
|
+
"framer-motion": "^12.34.3",
|
|
9
|
+
"lucide-react": "^0.370.0",
|
|
10
|
+
"react-shadow": "^20.6.0",
|
|
11
|
+
"recharts": "^3.7.0",
|
|
12
|
+
"zustand": "^4.5.2"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/react": "^18.3.1",
|
|
16
|
+
"typescript": "^5.0.0",
|
|
17
|
+
"react": "^18.3.1",
|
|
18
|
+
"react-dom": "^18.3.1"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@decido/kernel-bridge": "1.0.0",
|
|
22
|
+
"@decido/sdk": "1.0.0",
|
|
23
|
+
"@decido/shell": "1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"license": "UNLICENSED",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
28
|
+
"build": "tsup src/index.ts --format esm --dts --minify --clean"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { Component, ErrorInfo, ReactNode, useState } from 'react';
|
|
2
|
+
import root from 'react-shadow';
|
|
3
|
+
import { kernel } from '@decido/kernel-bridge';
|
|
4
|
+
|
|
5
|
+
// 1. Error Boundary para atrapar errores de ejecución de la IA
|
|
6
|
+
interface ErrorProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
title: string;
|
|
9
|
+
onCrash?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class GenerativeErrorBoundary extends Component<ErrorProps, { hasError: boolean, error: any }> {
|
|
13
|
+
constructor(props: any) {
|
|
14
|
+
super(props);
|
|
15
|
+
this.state = { hasError: false, error: null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getDerivedStateFromError(error: any) {
|
|
19
|
+
return { hasError: true, error };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
23
|
+
console.error("Generative UI Crash:", error, errorInfo);
|
|
24
|
+
|
|
25
|
+
// Notificamos al motor del Playground que hubo un fallo
|
|
26
|
+
if (this.props.onCrash) {
|
|
27
|
+
this.props.onCrash(error, errorInfo);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render() {
|
|
32
|
+
if (this.state.hasError) {
|
|
33
|
+
return (
|
|
34
|
+
<div style={{ padding: '16px', background: 'rgba(100,0,0,0.3)', border: '1px solid rgba(255,0,0,0.5)', borderRadius: '8px', color: '#ffb3b3', fontFamily: 'monospace' }}>
|
|
35
|
+
<h3 style={{ fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: '8px', margin: 0 }}>
|
|
36
|
+
⚠️ Fallo Crítico en Interfaz Generada
|
|
37
|
+
</h3>
|
|
38
|
+
<p style={{ fontSize: '12px', marginTop: '8px', opacity: 0.8 }}>{this.state.error?.message}</p>
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => this.setState({ hasError: false })}
|
|
41
|
+
style={{ marginTop: '12px', fontSize: '12px', background: 'rgba(255,0,0,0.2)', border: 'none', color: '#fff', cursor: 'pointer', padding: '4px 8px', borderRadius: '4px' }}
|
|
42
|
+
>
|
|
43
|
+
Intentar Reiniciar
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return this.props.children;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Wrapper con Shadow DOM
|
|
53
|
+
const ShadowDiv = root.div;
|
|
54
|
+
|
|
55
|
+
export const SafeZone = ({ children, title, rawCode, onCrash }: { children: ReactNode, title: string, rawCode?: string, onCrash?: (error: Error, errInfo: any) => void }) => {
|
|
56
|
+
const [isMaterializing, setIsMaterializing] = useState(false);
|
|
57
|
+
|
|
58
|
+
const handleMaterialize = async () => {
|
|
59
|
+
if (!rawCode) return;
|
|
60
|
+
const name = prompt("Nombre del componente (ej: InventoryChart):", title.replace(/[^a-zA-Z0-9]/g, ''));
|
|
61
|
+
if (!name) return;
|
|
62
|
+
|
|
63
|
+
setIsMaterializing(true);
|
|
64
|
+
try {
|
|
65
|
+
const filePath = await kernel.execute('materialize_construct', {
|
|
66
|
+
pluginId: 'madefront-ai',
|
|
67
|
+
name,
|
|
68
|
+
code: rawCode
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
kernel.notify('💎 Materialización Exitosa', `Archivo creado en: ${filePath}`);
|
|
72
|
+
kernel.execute('emit_xp', { label: `Materializado: ${name}`, xp: 250 });
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
kernel.notify('❌ Error', 'No se pudo escribir en el disco el constructo. ' + e.toString());
|
|
75
|
+
} finally {
|
|
76
|
+
setIsMaterializing(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="group relative h-full w-full overflow-hidden">
|
|
82
|
+
{isMaterializing && (
|
|
83
|
+
<div className="absolute inset-0 z-[100] bg-cyan-500/20 animate-pulse border-2 border-cyan-400 pointer-events-none">
|
|
84
|
+
<div className="h-1 bg-cyan-400 w-full absolute top-0 animate-scan"></div>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
{/* Botón Flotante de Guardado (Construct Marketplace) */}
|
|
88
|
+
{rawCode && (
|
|
89
|
+
<button
|
|
90
|
+
onClick={handleMaterialize}
|
|
91
|
+
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 z-[60] bg-emerald-500 text-black px-3 py-1 rounded-full text-[10px] font-bold shadow-lg transition-all hover:scale-110"
|
|
92
|
+
>
|
|
93
|
+
MATERIALIZAR CONSTRUCTO
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
<GenerativeErrorBoundary title={title} onCrash={onCrash}>
|
|
98
|
+
{/*
|
|
99
|
+
Usamos 'react-shadow' para crear un Shadow Root.
|
|
100
|
+
Esto evita que el CSS del componente generado afecte al resto de Decido-OS
|
|
101
|
+
*/}
|
|
102
|
+
<ShadowDiv style={{ height: '100%', width: '100%', display: 'block' }}>
|
|
103
|
+
{/*
|
|
104
|
+
IMPORTANTE: Inyectamos Tailwind en el Shadow DOM.
|
|
105
|
+
Podemos usar el CDN para runtime total.
|
|
106
|
+
*/}
|
|
107
|
+
<link rel="stylesheet" href="https://cdn.tailwindcss.com" />
|
|
108
|
+
<style>{`
|
|
109
|
+
:host { display: block; height: 100%; width: 100%; }
|
|
110
|
+
.generative-content { all: initial; font-family: sans-serif; }
|
|
111
|
+
`}</style>
|
|
112
|
+
|
|
113
|
+
<div className="generative-content" style={{ height: '100%', width: '100%' }}>
|
|
114
|
+
{children}
|
|
115
|
+
</div>
|
|
116
|
+
</ShadowDiv>
|
|
117
|
+
</GenerativeErrorBoundary>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
export const CodeSnippet = ({ code }: { code: string }) => {
|
|
4
|
+
// Memoized regex-based highlighter to prevent re-renders degradation
|
|
5
|
+
const highlightedCode = useMemo(() => {
|
|
6
|
+
return code.split('\n').map((line, i) => {
|
|
7
|
+
let highlightedLine = line
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/('.*?'|".*?"|`.*?`)/g, '<span class="text-emerald-400">$1</span>')
|
|
11
|
+
.replace(/(import|from|const|await|async|let|var|function|return|if|else)\b/g, '<span class="text-purple-400">$1</span>')
|
|
12
|
+
.replace(/\b([A-Z][a-zA-Z0-9_]*)\b/g, '<span class="text-cyan-400">$1</span>') // Classes/Interfaces
|
|
13
|
+
.replace(/\b(true|false|null|undefined)\b/g, '<span class="text-orange-400">$1</span>')
|
|
14
|
+
.replace(/(\/\/.*)/g, '<span class="text-zinc-500 italic">$1</span>') // Comments
|
|
15
|
+
.replace(/([{}()[\]])/g, '<span class="text-zinc-400">$1</span>'); // Brackets
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div key={i} className="table-row">
|
|
19
|
+
<span className="table-cell text-right select-none text-zinc-600 pr-4 text-xs opacity-50">{i + 1}</span>
|
|
20
|
+
<span className="table-cell whitespace-pre" dangerouslySetInnerHTML={{ __html: highlightedLine }} />
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
}, [code]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="bg-[#0D0D0D] border border-zinc-800 rounded-xl p-4 font-mono text-sm overflow-hidden text-zinc-300 shadow-2xl">
|
|
28
|
+
<div className="flex items-center gap-2 mb-4 border-b border-zinc-800 pb-3">
|
|
29
|
+
<div className="w-2.5 h-2.5 rounded-full bg-red-500/80"></div>
|
|
30
|
+
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/80"></div>
|
|
31
|
+
<div className="w-2.5 h-2.5 rounded-full bg-green-500/80"></div>
|
|
32
|
+
<span className="ml-2 text-xs text-zinc-500 font-sans">decido-sdk-demo.ts</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="table w-full">
|
|
35
|
+
{highlightedCode}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Handle, Position } from '@xyflow/react';
|
|
3
|
+
|
|
4
|
+
export interface OSNodeData {
|
|
5
|
+
label: string;
|
|
6
|
+
status: string;
|
|
7
|
+
icon: React.ReactNode;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const OSNode: React.FC<{ data: OSNodeData }> = ({ data }) => (
|
|
12
|
+
<div className={`px-4 py-3 rounded-xl bg-black/80 backdrop-blur-md border-2 ${data.isActive ? 'border-cyan-500 shadow-[0_0_20px_rgba(6,182,212,0.4)]' : 'border-zinc-800'} text-white flex flex-col items-center justify-center min-w-[140px] transition-all duration-700`}>
|
|
13
|
+
<Handle type="target" position={Position.Top} className="!bg-zinc-700" />
|
|
14
|
+
<div className={`p-2 rounded-full mb-2 ${data.isActive ? 'bg-cyan-500/20 text-cyan-400' : 'bg-zinc-800 text-zinc-500'}`}>
|
|
15
|
+
{data.icon}
|
|
16
|
+
</div>
|
|
17
|
+
<div className="font-bold text-sm tracking-wide">{data.label}</div>
|
|
18
|
+
<div className={`text-[10px] mt-1 font-mono ${data.isActive ? 'text-cyan-400' : 'text-zinc-600'}`}>
|
|
19
|
+
{data.status}
|
|
20
|
+
</div>
|
|
21
|
+
<Handle type="source" position={Position.Bottom} className="!bg-zinc-700" />
|
|
22
|
+
<Handle type="source" position={Position.Right} id="right" className="!bg-zinc-700" />
|
|
23
|
+
<Handle type="target" position={Position.Left} id="left" className="!bg-zinc-700" />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Cpu, Terminal, Network, BookOpen } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export const Sparkles = ({ className }: { className?: string }) => (
|
|
5
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
6
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
|
7
|
+
</svg>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
export interface StepNodeUpdate {
|
|
11
|
+
id: string;
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
status: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TutorialStep {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
code: string;
|
|
21
|
+
actionLabel: string;
|
|
22
|
+
nodes: any[]; // Using any to be compatible with ReactFlow Node type
|
|
23
|
+
edges: any[]; // Using any to be compatible with ReactFlow Edge type
|
|
24
|
+
nodeUpdatesOnExecute: StepNodeUpdate[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Tutorial Steps Data ---
|
|
28
|
+
export const TUTORIAL_STEPS: TutorialStep[] = [
|
|
29
|
+
{
|
|
30
|
+
id: 'intro',
|
|
31
|
+
title: 'Inicializando el Kernel Bridge',
|
|
32
|
+
description: 'Decido OS se basa en un Kernel central. Para interactuar con él desde una UI (React/Vue/Svelte), usas el `kernel-bridge`. Inicializarlo es el paso 0 de cualquier app.',
|
|
33
|
+
code: `import { kernel } from '@decido/kernel-bridge';
|
|
34
|
+
|
|
35
|
+
// 1. Iniciar la conexión segura
|
|
36
|
+
await kernel.boot();
|
|
37
|
+
|
|
38
|
+
// 2. Verificar estado del Kernel
|
|
39
|
+
const status = kernel.getStatus();
|
|
40
|
+
console.log("Kernel is:", status); // "ONLINE"
|
|
41
|
+
`,
|
|
42
|
+
actionLabel: 'Ejecutar Boot',
|
|
43
|
+
nodes: [
|
|
44
|
+
{ id: 'kernel', type: 'osNode', position: { x: 250, y: 150 }, data: { label: 'Decido Kernel', status: 'OFFLINE', icon: <Cpu className="w-5 h-5" />, isActive: false } }
|
|
45
|
+
],
|
|
46
|
+
edges: [],
|
|
47
|
+
nodeUpdatesOnExecute: [
|
|
48
|
+
{ id: 'kernel', isActive: true, status: 'ONLINE [WS Mux]' }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'plugin',
|
|
53
|
+
title: 'Registrando un Plugin UI',
|
|
54
|
+
description: 'Los plugins inyectan Web Components (Widgets) o CORTEX dinámicos dentro del Shell en tiempo de ejecución. Así es como extiendes el sistema.',
|
|
55
|
+
code: `import { usePluginStore } from '@decido/shell';
|
|
56
|
+
|
|
57
|
+
// 1. Construir Manifesto
|
|
58
|
+
const MyPlugin = {
|
|
59
|
+
id: 'stellar-plugin',
|
|
60
|
+
name: 'Stellar Analytics',
|
|
61
|
+
widgets: [{
|
|
62
|
+
id: 'stellar-dashboard',
|
|
63
|
+
component: MyDashboardReactComponent,
|
|
64
|
+
defaultZone: 'main-canvas'
|
|
65
|
+
}]
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 2. Registrar en el Store Descentralizado
|
|
69
|
+
usePluginStore.getState().registerPlugin(MyPlugin);
|
|
70
|
+
`,
|
|
71
|
+
actionLabel: 'Inyectar Plugin',
|
|
72
|
+
nodes: [
|
|
73
|
+
{ id: 'kernel', type: 'osNode', position: { x: 250, y: 150 }, data: { label: 'Decido Kernel', status: 'ONLINE', icon: <Cpu className="w-5 h-5" />, isActive: true } },
|
|
74
|
+
{ id: 'plugin', type: 'osNode', position: { x: 50, y: 300 }, data: { label: 'Stellar Plugin', status: 'UNLOADED', icon: <Terminal className="w-5 h-5" />, isActive: false } },
|
|
75
|
+
{ id: 'shell', type: 'osNode', position: { x: 450, y: 300 }, data: { label: 'React Shell', status: 'WAITING', icon: <BookOpen className="w-5 h-5" />, isActive: false } }
|
|
76
|
+
],
|
|
77
|
+
edges: [
|
|
78
|
+
{ id: 'e-kp', source: 'plugin', target: 'kernel', sourceHandle: 'right', targetHandle: 'left', animated: false, style: { stroke: '#52525b', strokeWidth: 2 } },
|
|
79
|
+
{ id: 'e-ks', source: 'kernel', target: 'shell', sourceHandle: 'right', targetHandle: 'left', animated: false, style: { stroke: '#52525b', strokeWidth: 2 } }
|
|
80
|
+
],
|
|
81
|
+
nodeUpdatesOnExecute: [
|
|
82
|
+
{ id: 'plugin', isActive: true, status: 'INJECTED' },
|
|
83
|
+
{ id: 'shell', isActive: true, status: 'MOUNTED WIDGET' }
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'agent',
|
|
88
|
+
title: 'Orquestando Swarms / MCP',
|
|
89
|
+
description: 'La magia de Decido OS es su AGI Nativa. Puedes conectar herramientas dinámicamente o escuchar cuando el modelo cognitivo "piensa".',
|
|
90
|
+
code: `import { kernel } from '@decido/kernel-bridge';
|
|
91
|
+
|
|
92
|
+
// Conectar un Servidor MCP (Tooling Externo)
|
|
93
|
+
await kernel.execute('connect_mcp_server', {
|
|
94
|
+
id: 'db-agent',
|
|
95
|
+
name: 'Database Explorer MCP'
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Escuchar los "pensamientos" (Cognitive Frames)
|
|
99
|
+
kernel.onEvent((payload) => {
|
|
100
|
+
if (payload.event_type === 'cognitive_frame') {
|
|
101
|
+
ui.toast(\`🤖 Agente pensando: \${payload.data.thought}\`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
`,
|
|
105
|
+
actionLabel: 'Activar Swarm',
|
|
106
|
+
nodes: [
|
|
107
|
+
{ id: 'kernel', type: 'osNode', position: { x: 250, y: 150 }, data: { label: 'Decido Kernel', status: 'ONLINE', icon: <Cpu className="w-5 h-5" />, isActive: true } },
|
|
108
|
+
{ id: 'mcp', type: 'osNode', position: { x: 50, y: 50 }, data: { label: 'MCP DB Agent', status: 'DISCONNECTED', icon: <Network className="w-5 h-5" />, isActive: false } },
|
|
109
|
+
{ id: 'agent', type: 'osNode', position: { x: 450, y: 50 }, data: { label: 'Cognitive Engine', status: 'IDLE', icon: <Sparkles className="w-5 h-5" />, isActive: false } }
|
|
110
|
+
],
|
|
111
|
+
edges: [
|
|
112
|
+
{ id: 'e-mk', source: 'mcp', target: 'kernel', animated: false, style: { stroke: '#52525b', strokeWidth: 2 } },
|
|
113
|
+
{ id: 'e-ak', source: 'kernel', target: 'agent', animated: false, style: { stroke: '#52525b', strokeWidth: 2 } }
|
|
114
|
+
],
|
|
115
|
+
nodeUpdatesOnExecute: [
|
|
116
|
+
{ id: 'mcp', isActive: true, status: 'CONNECTED (stdio)' },
|
|
117
|
+
{ id: 'agent', isActive: true, status: 'THINKING...' }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Recharts from 'recharts';
|
|
2
|
+
import * as FramerMotion from 'framer-motion';
|
|
3
|
+
import * as LucideIcons from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Map of authorized libraries that the OS can inject into the Generative UI.
|
|
7
|
+
*/
|
|
8
|
+
export const AVAILABLE_DEPENDENCIES: Record<string, any> = {
|
|
9
|
+
'recharts': Recharts,
|
|
10
|
+
'framer-motion': FramerMotion,
|
|
11
|
+
'lucide': LucideIcons,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Analyzes the generated code to detect which libraries the AI requests.
|
|
16
|
+
*/
|
|
17
|
+
export const detectRequiredDependencies = (code: string): string[] => {
|
|
18
|
+
const deps: string[] = [];
|
|
19
|
+
if (code.includes('LineChart') || code.includes('BarChart') || code.includes('PieChart') || code.includes('AreaChart') || code.includes('ComposedChart')) {
|
|
20
|
+
deps.push('recharts');
|
|
21
|
+
}
|
|
22
|
+
if (code.includes('motion.') || code.includes('AnimatePresence')) {
|
|
23
|
+
deps.push('framer-motion');
|
|
24
|
+
}
|
|
25
|
+
if (code.includes('Lucide') || code.includes('createLucideIcon')) {
|
|
26
|
+
deps.push('lucide');
|
|
27
|
+
}
|
|
28
|
+
return deps;
|
|
29
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { createStore } from 'zustand';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import * as LucideIcons from 'lucide-react';
|
|
4
|
+
import { useShellStore, LayoutNode, SafeLiquidUI } from '@decido/shell';
|
|
5
|
+
import { SafeZone } from '../components/SafeComponentWrapper';
|
|
6
|
+
import { kernel } from '@decido/kernel-bridge';
|
|
7
|
+
import { createSanitizedKernel } from './SecurityProxy';
|
|
8
|
+
import { AVAILABLE_DEPENDENCIES, detectRequiredDependencies } from './DependencyRegistry';
|
|
9
|
+
|
|
10
|
+
export interface PlaygroundState {
|
|
11
|
+
logs: string[];
|
|
12
|
+
isExecuting: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const playgroundStore = createStore<PlaygroundState>(() => ({
|
|
16
|
+
logs: ['Sistema en línea. Esperando directivas.'],
|
|
17
|
+
isExecuting: false
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Escuchar actualizaciones de datos en vivo y transmutaciones generativas desde Rust/Redis
|
|
21
|
+
kernel.onEvent((payload: any) => {
|
|
22
|
+
if (payload.event_type === 'ui:update_widget_data') {
|
|
23
|
+
const { widgetId, newData } = payload.data;
|
|
24
|
+
useShellStore.getState().setWidgetData(widgetId, newData);
|
|
25
|
+
console.log(`[LiveUpdate] 🔄 Datos actualizados para ${widgetId}`);
|
|
26
|
+
} else if (payload.event_type === 'ui:spawn_dynamic_widget') {
|
|
27
|
+
const title = payload.data?.title || "Misión Completada (Liquid UI)";
|
|
28
|
+
const code = payload.data?.code || payload.data?.content || payload.data || "";
|
|
29
|
+
console.log(`[LiquidUI] 🌊 Materializando interfaz efímera: ${title}`);
|
|
30
|
+
PlaygroundEngine.spawnGenerativeUI(code, title);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const PlaygroundEngine = {
|
|
35
|
+
log(msg: string) {
|
|
36
|
+
playgroundStore.setState((state) => ({
|
|
37
|
+
logs: [...state.logs, `[${new Date().toLocaleTimeString()}] ${msg}`].slice(-10) // Mantener últimos 10 logs
|
|
38
|
+
}));
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async dispatchMission(objective: string) {
|
|
42
|
+
const { isThinClient } = useShellStore.getState();
|
|
43
|
+
|
|
44
|
+
if (playgroundStore.getState().isExecuting) return;
|
|
45
|
+
|
|
46
|
+
playgroundStore.setState({ isExecuting: true });
|
|
47
|
+
this.log(`🚀 Misión enviada ${isThinClient ? 'remotamente' : 'al enjambre local'}: "${objective}"`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Enviamos el mensaje al agente 'dev-master' (que crearemos en Rust)
|
|
51
|
+
await kernel.execute('send_message', {
|
|
52
|
+
from: isThinClient ? "Mobile_User" : "Desktop_User",
|
|
53
|
+
to: "dev-master", // ID del agente en Rust
|
|
54
|
+
intent: "execute_task",
|
|
55
|
+
payload: objective,
|
|
56
|
+
priority: "high"
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.log('🛰️ Mensaje propagado por Redis. Esperando respuesta del Agente...');
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
this.log(`❌ Error de conexión: ${e.message}`);
|
|
62
|
+
} finally {
|
|
63
|
+
playgroundStore.setState({ isExecuting: false });
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// --- Phase 10: Generative & Self-Healing Engine ---
|
|
68
|
+
|
|
69
|
+
lastAttemptedCode: "",
|
|
70
|
+
lastErrorSignature: "", // Add signature tracker
|
|
71
|
+
currentMission: "",
|
|
72
|
+
retryCount: 0,
|
|
73
|
+
MAX_RETRIES: 2,
|
|
74
|
+
|
|
75
|
+
sanitizeJSX(raw: string): string {
|
|
76
|
+
let clean = raw.trim();
|
|
77
|
+
// Intentar extraer bloque de código con lenguaje especificado (jsx, tsx, etc.)
|
|
78
|
+
const langMatch = clean.match(/```(?:jsx|tsx|javascript|typescript|js|ts)\n([\s\S]*?)\n```/);
|
|
79
|
+
if (langMatch && langMatch[1]) {
|
|
80
|
+
return langMatch[1].trim();
|
|
81
|
+
}
|
|
82
|
+
// Intentar extraer bloque genérico sin lenguaje
|
|
83
|
+
const genericMatch = clean.match(/```\n([\s\S]*?)\n```/);
|
|
84
|
+
if (genericMatch && genericMatch[1]) {
|
|
85
|
+
return genericMatch[1].trim();
|
|
86
|
+
}
|
|
87
|
+
// Si no hay bloques markdown, retornar todo asumiendo que es código puro
|
|
88
|
+
return clean;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async spawnGenerativeUI(rawCode: string, missionTitle: string = 'Componente Generado', missionData: any = {}) {
|
|
92
|
+
const code = this.sanitizeJSX(rawCode);
|
|
93
|
+
this.log(`🛡️ Preparando Zona Segura (Shadow DOM) para: ${missionTitle}`);
|
|
94
|
+
this.lastAttemptedCode = code;
|
|
95
|
+
this.currentMission = missionTitle;
|
|
96
|
+
|
|
97
|
+
const widgetId = `gen-live-${Date.now()}`;
|
|
98
|
+
|
|
99
|
+
// Guardar datos iniciales
|
|
100
|
+
useShellStore.getState().setWidgetData(widgetId, missionData);
|
|
101
|
+
|
|
102
|
+
const handleCrash = async (error: Error, errorInfo: any) => {
|
|
103
|
+
this.log(`⚠️ Detectado crash en UI. Iniciando auto-curación...`);
|
|
104
|
+
|
|
105
|
+
if (this.retryCount >= this.MAX_RETRIES) {
|
|
106
|
+
this.log(`❌ Demasiados intentos de reparación. Abortando misión.`);
|
|
107
|
+
kernel.notify('Fallo de Auto-Curación', 'El agente no pudo reparar la interfaz tras varios intentos.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.retryCount++;
|
|
112
|
+
this.lastErrorSignature = error.message;
|
|
113
|
+
|
|
114
|
+
// Prepared repair prompt
|
|
115
|
+
const repairPrompt = `
|
|
116
|
+
SISTEMA DE AUTO-CURACIÓN DE DECIDO-OS
|
|
117
|
+
-------------------------------------
|
|
118
|
+
El código React que generaste causó un error de ejecución en tiempo real.
|
|
119
|
+
|
|
120
|
+
ERROR: ${error.message}
|
|
121
|
+
LOCALIZACIÓN: ${JSON.stringify(errorInfo.componentStack?.split('\\n').slice(0, 3))}
|
|
122
|
+
|
|
123
|
+
CÓDIGO FALLIDO:
|
|
124
|
+
\`\`\`jsx
|
|
125
|
+
${this.lastAttemptedCode}
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
TAREA: Corrige el error en tu lógica. Asegúrate de que todas las variables y accesos a mapas/arrays sean seguros (ej. usa optional chaining).
|
|
129
|
+
REGLA CRÍTICA: No uses librerías externas que no sean React ni intentes renderizar objetos como si fueran elementos React.
|
|
130
|
+
Usa únicamente Tailwind inline classes.
|
|
131
|
+
Asegúrate de que el componente exportado dinámicamente se asigne a 'DynamicComponent'.
|
|
132
|
+
Responde devolviendo de nuevo el intento \`ui:spawn_dynamic_widget\` pero con el código completamente solventado. No repitas el mismo error.
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
await kernel.execute('send_message', {
|
|
136
|
+
from: "System_UI_Sentinel",
|
|
137
|
+
to: "dev-master",
|
|
138
|
+
intent: "repair_ui",
|
|
139
|
+
payload: repairPrompt,
|
|
140
|
+
priority: "high"
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.log(`🛰️ Feedback enviado al Orquestador Rust. Re-intentando constructo (Intento ${this.retryCount})...`);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const requiredDeps = detectRequiredDependencies(code);
|
|
148
|
+
this.log(`📦 Dependencias inyectadas: ${requiredDeps.join(', ') || 'Ninguna'}`);
|
|
149
|
+
|
|
150
|
+
const dependencyArgs: Record<string, any> = {
|
|
151
|
+
'React': React,
|
|
152
|
+
'kernel': createSanitizedKernel(missionTitle),
|
|
153
|
+
'Lucide': LucideIcons,
|
|
154
|
+
'window': undefined,
|
|
155
|
+
'document': undefined,
|
|
156
|
+
'localStorage': undefined,
|
|
157
|
+
'fetch': undefined,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
requiredDeps.forEach(dep => {
|
|
161
|
+
dependencyArgs[dep.replace('-', '_')] = AVAILABLE_DEPENDENCIES[dep];
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const argNames = Object.keys(dependencyArgs);
|
|
165
|
+
const argValues = Object.values(dependencyArgs);
|
|
166
|
+
|
|
167
|
+
// 1. DSL Transpilation via Safe AST Engine
|
|
168
|
+
// Bloqueamos el motor generativo para enforzar el AST SafeLiquidUI. No más ejecución libre.
|
|
169
|
+
const RawDynamicComponent: React.FC<any> = (props) => {
|
|
170
|
+
return React.createElement(SafeLiquidUI, {
|
|
171
|
+
uiSchema: code,
|
|
172
|
+
tenantId: 'generative-playground',
|
|
173
|
+
missionData: props.data
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// 2. Wrap via ErrorBoundary and Security Context with Live Data
|
|
178
|
+
const ProtectedComponent = (props: any) => {
|
|
179
|
+
const liveData = useShellStore((state) => state.widgetDataBus[widgetId]);
|
|
180
|
+
|
|
181
|
+
// EFECTO DE LIMPIEZA: Handshake de Cierre
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
return () => {
|
|
184
|
+
console.log(`[Shell] 🛑 Cerrando canal de datos para ${widgetId}`);
|
|
185
|
+
kernel.execute('stop_live_monitor', { widgetId }).catch(e => console.warn(e));
|
|
186
|
+
};
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
return React.createElement(
|
|
190
|
+
SafeZone,
|
|
191
|
+
{
|
|
192
|
+
title: missionTitle,
|
|
193
|
+
rawCode: code,
|
|
194
|
+
onCrash: handleCrash,
|
|
195
|
+
children: React.createElement(RawDynamicComponent, { ...props, data: liveData })
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// 3. Register Temporary Layout Morphological Widget
|
|
201
|
+
const pluginId = `plugin-${widgetId}`;
|
|
202
|
+
|
|
203
|
+
useShellStore.getState().registerPlugin({
|
|
204
|
+
id: pluginId,
|
|
205
|
+
name: missionTitle,
|
|
206
|
+
version: '1.0.0',
|
|
207
|
+
description: 'Generado vía Auto-Curación',
|
|
208
|
+
widgets: [{
|
|
209
|
+
id: widgetId,
|
|
210
|
+
pluginId: pluginId,
|
|
211
|
+
name: missionTitle,
|
|
212
|
+
capabilities: ['generative:canvas'],
|
|
213
|
+
views: { expanded: ProtectedComponent },
|
|
214
|
+
layoutConfig: { preferredZone: 'main-canvas' },
|
|
215
|
+
component: ProtectedComponent,
|
|
216
|
+
isTransient: true
|
|
217
|
+
}],
|
|
218
|
+
intents: []
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 4. Force Structural Layout Transmutation
|
|
222
|
+
const newLayout: LayoutNode = {
|
|
223
|
+
id: `layout-${widgetId}`,
|
|
224
|
+
type: 'split-horizontal',
|
|
225
|
+
children: [
|
|
226
|
+
{
|
|
227
|
+
id: 'side-terminal',
|
|
228
|
+
type: 'widget',
|
|
229
|
+
widgetId: 'forge-terminal',
|
|
230
|
+
size: 30
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: `main-${widgetId}`,
|
|
234
|
+
type: 'widget',
|
|
235
|
+
widgetId: widgetId,
|
|
236
|
+
size: 70
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
useShellStore.getState().setLayout(newLayout);
|
|
242
|
+
|
|
243
|
+
kernel.notify('✨ Canvas Actualizado', `Constructo "${missionTitle}" renderizado.`);
|
|
244
|
+
this.log(`✅ UI Montada con éxito en el canvas principal.`);
|
|
245
|
+
|
|
246
|
+
// If we successfully rendered and this was a fix, store the UILesson
|
|
247
|
+
if (this.retryCount > 0 && this.lastErrorSignature) {
|
|
248
|
+
this.log(`🧠 Guardando Lección UI (Memoria Cognitiva) en Qdrant...`);
|
|
249
|
+
kernel.execute('save_lesson', {
|
|
250
|
+
lesson: {
|
|
251
|
+
error_signature: this.lastErrorSignature,
|
|
252
|
+
failed_code: this.lastAttemptedCode,
|
|
253
|
+
fixed_code: code
|
|
254
|
+
}
|
|
255
|
+
}).catch(e => {
|
|
256
|
+
console.warn("Failed to save UILesson:", e);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Reset retries and update current baseline
|
|
261
|
+
this.retryCount = 0;
|
|
262
|
+
this.lastErrorSignature = "";
|
|
263
|
+
this.lastAttemptedCode = code;
|
|
264
|
+
|
|
265
|
+
} catch (error: any) {
|
|
266
|
+
this.log(`❌ Error preventivo de Ensamblaje Estático: ${error.message}`);
|
|
267
|
+
|
|
268
|
+
// Initiate auto-healing even for static syntax errors
|
|
269
|
+
handleCrash(error, { componentStack: "Eval/Transpilation Phase" });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|