@decido/shell 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 +65 -0
- package/src/AgentPlayer.tsx +105 -0
- package/src/DecidoPlayer.tsx +117 -0
- package/src/bridge/BridgeAgent.ts +443 -0
- package/src/components/DecidoIcon.tsx +56 -0
- package/src/components/JsonTreeEditor.tsx +117 -0
- package/src/components/PanelSplitter.tsx +71 -0
- package/src/components/PluginErrorBoundary.tsx +69 -0
- package/src/components/SafeLiquidUI.tsx +114 -0
- package/src/components/TransientLayer.tsx +92 -0
- package/src/components/agent/AgentChat.tsx +134 -0
- package/src/components/chat-extensions/IntentCatalogPanel.tsx +81 -0
- package/src/components/chat-extensions/chatSlashCommands.ts +101 -0
- package/src/components/controls/CreatorInputBar.tsx +144 -0
- package/src/components/controls/OSToolbar.tsx +90 -0
- package/src/components/controls/TimelineTape.tsx +43 -0
- package/src/components/debug/ActionTimelineTab.tsx +111 -0
- package/src/components/debug/CSSInspectorTab.tsx +436 -0
- package/src/components/debug/ExportTab.tsx +192 -0
- package/src/components/debug/FlowHealthTab.tsx +86 -0
- package/src/components/debug/LogsTab.tsx +110 -0
- package/src/components/debug/MorphStackTab.tsx +241 -0
- package/src/components/debug/NetworkTab.tsx +173 -0
- package/src/components/debug/PerformanceTab.tsx +171 -0
- package/src/components/debug/ProfilesTab.tsx +238 -0
- package/src/components/debug/ReplayTab.tsx +70 -0
- package/src/components/debug/StoresTab.tsx +255 -0
- package/src/components/debug/TopologyTab.tsx +59 -0
- package/src/components/debug/debugConfig.tsx +66 -0
- package/src/components/playground/DebugPanel.tsx +112 -0
- package/src/components/playground/HeaderCenterControls.tsx +92 -0
- package/src/components/playground/KeyframeListItem.tsx +70 -0
- package/src/components/playground/PlaygroundAppSidebar.tsx +171 -0
- package/src/components/playground/PlaygroundBottomControls.tsx +132 -0
- package/src/components/playground/PlaygroundCanvas.tsx +87 -0
- package/src/components/playground/PlaygroundChat.tsx +236 -0
- package/src/components/playground/PlaygroundErrorBoundary.tsx +63 -0
- package/src/components/playground/PlaygroundFloatingInput.tsx +352 -0
- package/src/components/playground/PlaygroundHeader.tsx +222 -0
- package/src/components/playground/PlaygroundSidebar.tsx +136 -0
- package/src/components/playground/PlaygroundTerminal.tsx +44 -0
- package/src/components/playground/SuggestionCards.tsx +29 -0
- package/src/components/playground/demos/ClinicaAINode.tsx +221 -0
- package/src/components/playground/demos/FinanceAINode.tsx +226 -0
- package/src/components/playground/demos/KiaAcademyNode.tsx +250 -0
- package/src/components/playground/demos/KiaBotNode.tsx +207 -0
- package/src/components/playground/demos/KiaCampaignNode.tsx +191 -0
- package/src/components/playground/demos/KiaComplianceNode.tsx +140 -0
- package/src/components/playground/demos/KiaCustomerJourneyNode.tsx +220 -0
- package/src/components/playground/demos/KiaCyberNode.tsx +203 -0
- package/src/components/playground/demos/KiaDashboardNode.tsx +399 -0
- package/src/components/playground/demos/KiaEmbudoOverviewNode.tsx +168 -0
- package/src/components/playground/demos/KiaExecutiveNode.tsx +169 -0
- package/src/components/playground/demos/KiaGamificationNode.tsx +229 -0
- package/src/components/playground/demos/KiaIntelligenceHubNode.tsx +165 -0
- package/src/components/playground/demos/KiaInventoryNode.tsx +183 -0
- package/src/components/playground/demos/KiaLeadScoringNode.tsx +226 -0
- package/src/components/playground/demos/KiaLiveSimulationNode.tsx +177 -0
- package/src/components/playground/demos/KiaMultiDealerNode.tsx +223 -0
- package/src/components/playground/demos/KiaNPSVoiceNode.tsx +214 -0
- package/src/components/playground/demos/KiaOmnichannelNode.tsx +162 -0
- package/src/components/playground/demos/KiaPBIBudgetNode.tsx +152 -0
- package/src/components/playground/demos/KiaPBIConversionNode.tsx +206 -0
- package/src/components/playground/demos/KiaPBIFunnelNode.tsx +184 -0
- package/src/components/playground/demos/KiaPBIOwnershipNode.tsx +113 -0
- package/src/components/playground/demos/KiaPBIPartnerNode.tsx +143 -0
- package/src/components/playground/demos/KiaPBIPreciosNode.tsx +120 -0
- package/src/components/playground/demos/KiaPBIRuntNode.tsx +205 -0
- package/src/components/playground/demos/KiaPartnerScoreNode.tsx +206 -0
- package/src/components/playground/demos/KiaPredictiveNode.tsx +226 -0
- package/src/components/playground/demos/KiaShowroomNode.tsx +194 -0
- package/src/components/playground/demos/KiaStoreNode.tsx +215 -0
- package/src/components/playground/demos/KiaSustainabilityNode.tsx +173 -0
- package/src/components/playground/demos/KiaUsedVehiclesNode.tsx +163 -0
- package/src/components/playground/demos/KiaWorkshopNode.tsx +221 -0
- package/src/components/playground/demos/SmartCityNode.tsx +205 -0
- package/src/components/playground/demos/kia_campaign_manifest.json +112 -0
- package/src/components/playground/input-parts/AIModelSelector.tsx +156 -0
- package/src/components/playground/input-parts/InputActions.tsx +80 -0
- package/src/components/playground/input-parts/InputToolbar.tsx +245 -0
- package/src/components/playground/input-parts/ResourceLibraryPanel.tsx +287 -0
- package/src/components/playground/sidebarDsdIO.ts +82 -0
- package/src/components/settings/SettingsPanel.tsx +267 -0
- package/src/components/shell/AppHeader.tsx +9 -0
- package/src/components/shell/AppShell.tsx +139 -0
- package/src/components/shell/ArtifactBar.tsx +97 -0
- package/src/components/shell/BootScreen.tsx +19 -0
- package/src/components/shell/CenterComposite.tsx +87 -0
- package/src/components/shell/CodeEditorPanel.tsx +88 -0
- package/src/components/shell/GlobalOverlays.tsx +228 -0
- package/src/components/shell/LayoutConfigurator.tsx +209 -0
- package/src/components/shell/LayoutGrid.tsx +178 -0
- package/src/components/shell/MorphShell.tsx +368 -0
- package/src/components/shell/PluginViewer.tsx +147 -0
- package/src/components/shell/ShellNexusPreview.tsx +458 -0
- package/src/components/shell/SlotRenderer.tsx +115 -0
- package/src/components/shell/TabBar.tsx +94 -0
- package/src/components/shell/TemplateLibrary.tsx +195 -0
- package/src/components/shell/layoutConstants.ts +35 -0
- package/src/components/shell/morphStageMeta.ts +15 -0
- package/src/components/shell/shells/BuiltInShells.tsx +443 -0
- package/src/components/shell/shells/DatawayChatShell.tsx +42 -0
- package/src/components/shell/shells/TokenPreview.tsx +339 -0
- package/src/components/shell/shells/bootShells.ts +31 -0
- package/src/components/shells/CreatorShell.tsx +37 -0
- package/src/components/shells/DecidoShell.tsx +447 -0
- package/src/components/shells/ExperimentalChatShell.tsx +245 -0
- package/src/components/shells/UserCanvas.tsx +44 -0
- package/src/components/studio/BlueprintManagerPanel.tsx +137 -0
- package/src/components/studio/DependencyTreePanel.tsx +192 -0
- package/src/components/studio/NodePalette.tsx +92 -0
- package/src/components/studio/NodePropertiesPanel.tsx +81 -0
- package/src/components/studio/ReactFlowEditor.tsx +242 -0
- package/src/components/studio/TimelineEditor.tsx +122 -0
- package/src/components/studio/TimelineKeyframeCard.tsx +99 -0
- package/src/components/studio/VariablePanel.tsx +181 -0
- package/src/components/studio/blueprint/BlueprintCard.tsx +82 -0
- package/src/components/studio/editor/CanvasContextMenu.tsx +107 -0
- package/src/components/studio/editor/EditorToolbar.tsx +80 -0
- package/src/components/studio/editor/StageContentRenderer.tsx +134 -0
- package/src/components/studio/editor/TrackPropertyEditors.tsx +133 -0
- package/src/components/studio/editor/TreeNodeItem.tsx +91 -0
- package/src/components/studio/editor/edgeStyles.ts +43 -0
- package/src/components/studio/editor/editorKeyHandler.ts +95 -0
- package/src/components/studio/editor/nodeTypeRegistry.ts +137 -0
- package/src/components/studio/editor/paletteCatalog.tsx +84 -0
- package/src/components/studio/nodes/shell/InteractionNodes.tsx +82 -0
- package/src/components/studio/nodes/shell/LayoutControlNodes.tsx +69 -0
- package/src/components/studio/nodes/shell/RegisterActionNode.tsx +20 -0
- package/src/components/studio/nodes/shell/RegisterButtonNode.tsx +22 -0
- package/src/components/studio/nodes/shell/RegisterPanelNode.tsx +19 -0
- package/src/components/studio/nodes/shell/RegisterSidebarNode.tsx +19 -0
- package/src/components/studio/nodes/shell/RegisterStatusBarNode.tsx +22 -0
- package/src/components/studio/nodes/shell/RegisterTabNode.tsx +21 -0
- package/src/components/studio/nodes/shell/RegisterTopBarNode.tsx +22 -0
- package/src/components/studio/nodes/shell/ShellConfigNode.tsx +51 -0
- package/src/components/studio/nodes/shell/ShellNodeBase.tsx +100 -0
- package/src/components/studio/nodes/shell/ThemeNodes.tsx +51 -0
- package/src/components/studio/nodes/shell/index.ts +12 -0
- package/src/components/widgets/BroadcastWidget.tsx +93 -0
- package/src/components/widgets/MarketplaceWidget.tsx +298 -0
- package/src/components/widgets/McpToolsWidget.tsx +231 -0
- package/src/components/widgets/OpsDashboard.tsx +59 -0
- package/src/components/widgets/QuickActionsWidget.tsx +60 -0
- package/src/components/widgets/UsageWidget.tsx +112 -0
- package/src/components/widgets/WidgetRenderer.tsx +892 -0
- package/src/components/widgets/WidgetSlotPanel.tsx +213 -0
- package/src/config/IconRegistry.ts +126 -0
- package/src/contexts/NetworkProvider.tsx +162 -0
- package/src/core/AIDirector.ts +71 -0
- package/src/core/EventBus.ts +37 -0
- package/src/core/PluginContext.tsx +141 -0
- package/src/hooks/listeners/useUIStateListener.ts +59 -0
- package/src/hooks/listeners/useWhatsAppListener.ts +110 -0
- package/src/hooks/morphBridge.ts +82 -0
- package/src/hooks/useAIModelSelector.ts +144 -0
- package/src/hooks/useAgentStream.ts +220 -0
- package/src/hooks/useAutoUpdater.ts +89 -0
- package/src/hooks/useBootSequence.ts +20 -0
- package/src/hooks/useExportDSD.ts +53 -0
- package/src/hooks/useFullscreen.ts +35 -0
- package/src/hooks/useGeminiStream.ts +282 -0
- package/src/hooks/useIntentLens.ts +224 -0
- package/src/hooks/useKeyboardShortcuts.ts +69 -0
- package/src/hooks/useLoggerBridge.ts +32 -0
- package/src/hooks/useMcpClient.ts +112 -0
- package/src/hooks/useNexusaiDeploy.ts +118 -0
- package/src/hooks/usePlaybackEngine.ts +21 -0
- package/src/hooks/usePlaygroundCommander.ts +475 -0
- package/src/hooks/usePluginEngine.ts +165 -0
- package/src/hooks/useScreenRecorder.ts +73 -0
- package/src/hooks/useShellKeyboard.ts +40 -0
- package/src/hooks/useShellShortcuts.ts +118 -0
- package/src/hooks/useSoundEffects.ts +35 -0
- package/src/hooks/useStudioConfig.ts +72 -0
- package/src/hooks/useSystemBoot.ts +84 -0
- package/src/hooks/useSystemTelemetry.ts +62 -0
- package/src/index.ts +97 -0
- package/src/lib/debugLogger.ts +80 -0
- package/src/lib/networkInterceptor.ts +100 -0
- package/src/mocks/decido.tsx +41 -0
- package/src/plugins/pluginAPI.ts +190 -0
- package/src/store/McpStore.ts +69 -0
- package/src/store/UpdaterStore.ts +60 -0
- package/src/store/engine.ts +392 -0
- package/src/store/index.ts +4 -0
- package/src/store/layoutPresets.ts +66 -0
- package/src/store/playgroundTypes.ts +98 -0
- package/src/store/useActionTimelineStore.ts +48 -0
- package/src/store/useDebugPanelStore.ts +98 -0
- package/src/store/useDebugProfileStore.ts +130 -0
- package/src/store/useLayoutStore.ts +205 -0
- package/src/store/useMorphInstanceStore.ts +289 -0
- package/src/store/useMorphologyStore.ts +103 -0
- package/src/store/usePlaygroundStore.ts +236 -0
- package/src/store/useShellRegistry.ts +123 -0
- package/src/store/useSuggestionsStore.ts +57 -0
- package/src/store/useThemeStore.ts +399 -0
- package/src/store/useUIComponentStore.ts +179 -0
- package/src/types/DecidoStoryDefinition.ts +43 -0
- package/src/utils/ai/ai-architect.ts +92 -0
- package/src/utils/ai/ai-code.ts +187 -0
- package/src/utils/ai/ai-core.ts +50 -0
- package/src/utils/ai/ai-media.ts +292 -0
- package/src/utils/layoutGraph.ts +67 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback, useSyncExternalStore } from 'react';
|
|
2
|
+
import { Database, Zap, ChevronRight, ChevronDown, ToggleLeft, ToggleRight, Camera, ArrowLeftRight, Download, Upload } from 'lucide-react';
|
|
3
|
+
import { usePlaygroundStore } from '../../store/usePlaygroundStore';
|
|
4
|
+
import { useEngineStore, useTimelineStore, useUserStateStore } from '@decido/engine';
|
|
5
|
+
import { useMorphologyStore } from '../../store/useMorphologyStore';
|
|
6
|
+
import { useUIComponentStore } from '../../store/useUIComponentStore';
|
|
7
|
+
|
|
8
|
+
// ─── Store Registry ───
|
|
9
|
+
interface StoreEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
group: 'shell' | 'studio';
|
|
12
|
+
store: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let _shellStores: StoreEntry[] | null = null;
|
|
16
|
+
function getShellStores(): StoreEntry[] {
|
|
17
|
+
if (_shellStores) return _shellStores;
|
|
18
|
+
// Shell stores are injected by the host app via a global registry
|
|
19
|
+
const registry = (globalThis as any).__DECIDO_SHELL_STORES__ as StoreEntry[] | undefined;
|
|
20
|
+
_shellStores = registry || [];
|
|
21
|
+
return _shellStores;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const STUDIO_STORES: StoreEntry[] = [
|
|
25
|
+
{ name: 'Playground', group: 'studio', store: usePlaygroundStore },
|
|
26
|
+
{ name: 'Engine', group: 'studio', store: useEngineStore },
|
|
27
|
+
{ name: 'Timeline', group: 'studio', store: useTimelineStore },
|
|
28
|
+
{ name: 'Morphology', group: 'studio', store: useMorphologyStore },
|
|
29
|
+
{ name: 'UserState', group: 'studio', store: useUserStateStore },
|
|
30
|
+
{ name: 'UIComponents', group: 'studio', store: useUIComponentStore },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ─── Recursive Value Editor ───
|
|
34
|
+
function ValueEditor({ value, path, onUpdate, depth = 0 }: { value: any; path: string; onUpdate: (path: string, newValue: any) => void; depth?: number }) {
|
|
35
|
+
const [collapsed, setCollapsed] = useState(depth > 1);
|
|
36
|
+
if (value === null || value === undefined) return <span className="text-text-muted italic text-[10px] font-mono">{String(value)}</span>;
|
|
37
|
+
if (typeof value === 'function') return null;
|
|
38
|
+
if (typeof value === 'boolean') {
|
|
39
|
+
return (
|
|
40
|
+
<button onClick={() => onUpdate(path, !value)} className={`flex items-center gap-1 text-[10px] font-mono transition-colors ${value ? 'text-emerald-400' : 'text-text-muted'}`}>
|
|
41
|
+
{value ? <ToggleRight size={14} /> : <ToggleLeft size={14} />}{String(value)}
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === 'number') return <input type="number" value={value} onChange={e => onUpdate(path, Number(e.target.value))} className="bg-surface-tertiary border border-border-default rounded px-1.5 py-0.5 text-[10px] font-mono text-cyan-300 w-24 outline-hidden focus:border-cyan-500/50" />;
|
|
46
|
+
if (typeof value === 'string') return <input type="text" value={value} onChange={e => onUpdate(path, e.target.value)} className="bg-surface-tertiary border border-border-default rounded px-1.5 py-0.5 text-[10px] font-mono text-amber-300 flex-1 min-w-[80px] max-w-[300px] outline-hidden focus:border-amber-500/50" />;
|
|
47
|
+
|
|
48
|
+
const tag = Object.prototype.toString.call(value);
|
|
49
|
+
if (tag === '[object Map]') {
|
|
50
|
+
const entries: [any, any][] = Array.from((value as globalThis.Map<any, any>).entries());
|
|
51
|
+
return (
|
|
52
|
+
<div className="pl-3 border-l border-border-subtle">
|
|
53
|
+
<button onClick={() => setCollapsed(!collapsed)} className="flex items-center gap-1 text-[10px] text-text-muted hover:text-text-primary">{collapsed ? <ChevronRight size={10} /> : <ChevronDown size={10} />}<span className="text-violet-400">Map</span>({entries.length})</button>
|
|
54
|
+
{!collapsed && entries.map((entry, i) => <div key={i} className="flex items-start gap-2 py-0.5 pl-2"><span className="text-[10px] text-text-muted font-mono shrink-0">{String(entry[0])}:</span><ValueEditor value={entry[1]} path={`${path}.${String(entry[0])}`} onUpdate={() => {}} depth={depth + 1} /></div>)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (tag === '[object Set]') {
|
|
59
|
+
const items: any[] = Array.from(value as globalThis.Set<any>);
|
|
60
|
+
return (
|
|
61
|
+
<div className="pl-3 border-l border-border-subtle">
|
|
62
|
+
<button onClick={() => setCollapsed(!collapsed)} className="flex items-center gap-1 text-[10px] text-text-muted hover:text-text-primary">{collapsed ? <ChevronRight size={10} /> : <ChevronDown size={10} />}<span className="text-violet-400">Set</span>({items.length})</button>
|
|
63
|
+
{!collapsed && items.map((v, i) => <div key={i} className="pl-2 py-0.5"><ValueEditor value={v} path={`${path}[${i}]`} onUpdate={() => {}} depth={depth + 1} /></div>)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="pl-3 border-l border-border-subtle">
|
|
70
|
+
<button onClick={() => setCollapsed(!collapsed)} className="flex items-center gap-1 text-[10px] text-text-muted hover:text-text-primary">{collapsed ? <ChevronRight size={10} /> : <ChevronDown size={10} />}<span className="text-blue-400">Array</span><span className="text-text-muted">[{value.length}]</span></button>
|
|
71
|
+
{!collapsed && value.slice(0, 50).map((item, i) => <div key={i} className="flex items-start gap-2 py-0.5 pl-2"><span className="text-[9px] text-text-muted font-mono shrink-0 w-4 text-right">{i}</span><ValueEditor value={item} path={`${path}[${i}]`} onUpdate={onUpdate} depth={depth + 1} /></div>)}
|
|
72
|
+
{value.length > 50 && <span className="text-[9px] text-text-muted pl-2">... +{value.length - 50} más</span>}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === 'object') {
|
|
77
|
+
const entries = Object.entries(value).filter(([, v]) => typeof v !== 'function');
|
|
78
|
+
return (
|
|
79
|
+
<div className="pl-3 border-l border-border-subtle">
|
|
80
|
+
<button onClick={() => setCollapsed(!collapsed)} className="flex items-center gap-1 text-[10px] text-text-muted hover:text-text-primary">{collapsed ? <ChevronRight size={10} /> : <ChevronDown size={10} />}<span className="text-orange-400">{`{${entries.length}}`}</span></button>
|
|
81
|
+
{!collapsed && entries.map(([k, v]) => <div key={k} className="flex items-start gap-2 py-0.5 pl-2"><span className="text-[10px] text-text-secondary font-mono shrink-0">{k}:</span><ValueEditor value={v} path={`${path}.${k}`} onUpdate={onUpdate} depth={depth + 1} /></div>)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return <span className="text-text-muted text-[10px] font-mono">{String(value)}</span>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Stores Tab ───
|
|
89
|
+
export function StoresTab() {
|
|
90
|
+
const allStores = useMemo(() => [...getShellStores(), ...STUDIO_STORES], []);
|
|
91
|
+
const [activeStore, setActiveStore] = useState(allStores[0]?.name || '');
|
|
92
|
+
const entry = useMemo(() => allStores.find(s => s.name === activeStore), [allStores, activeStore]);
|
|
93
|
+
const storeSnapshot = useSyncExternalStore(
|
|
94
|
+
useCallback((cb: () => void) => entry?.store?.subscribe?.(cb) || (() => {}), [entry]),
|
|
95
|
+
useCallback(() => entry?.store?.getState?.() ?? {}, [entry])
|
|
96
|
+
);
|
|
97
|
+
const { stateEntries, actionEntries } = useMemo(() => {
|
|
98
|
+
const stateE: [string, any][] = []; const actionE: [string, Function][] = [];
|
|
99
|
+
for (const [k, v] of Object.entries(storeSnapshot)) { if (typeof v === 'function') actionE.push([k, v]); else stateE.push([k, v]); }
|
|
100
|
+
return { stateEntries: stateE, actionEntries: actionE };
|
|
101
|
+
}, [storeSnapshot]);
|
|
102
|
+
const handleUpdate = useCallback((path: string, newValue: any) => {
|
|
103
|
+
if (!entry) return;
|
|
104
|
+
const parts = path.split('.'); const topKey = parts[1]; if (!topKey) return;
|
|
105
|
+
if (parts.length === 2) { entry.store.setState({ [topKey]: newValue }); } else {
|
|
106
|
+
const current = entry.store.getState()[topKey];
|
|
107
|
+
try { const clone = JSON.parse(JSON.stringify(current)); let target = clone; for (let i = 2; i < parts.length - 1; i++) target = target[parts[i].replace(/\[(\d+)\]/, '.$1')]; const lastKey = parts[parts.length - 1].replace(/\[(\d+)\]/, '$1'); target[lastKey] = newValue; entry.store.setState({ [topKey]: clone }); } catch { entry.store.setState({ [topKey]: newValue }); }
|
|
108
|
+
}
|
|
109
|
+
}, [entry]);
|
|
110
|
+
const [actionFeedback, setActionFeedback] = useState<string | null>(null);
|
|
111
|
+
|
|
112
|
+
// Sprint AU: Snapshot/Diff
|
|
113
|
+
interface Snapshot { id: number; label: string; data: Record<string, any> }
|
|
114
|
+
const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
|
|
115
|
+
const [showDiff, setShowDiff] = useState<number | null>(null);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex h-full">
|
|
119
|
+
<div className="w-32 shrink-0 border-r border-border-subtle overflow-y-auto bg-surface-primary/50">
|
|
120
|
+
{['shell', 'studio'].map(group => {
|
|
121
|
+
const groupStores = allStores.filter(s => s.group === group);
|
|
122
|
+
if (groupStores.length === 0) return null;
|
|
123
|
+
return (
|
|
124
|
+
<div key={group}>
|
|
125
|
+
<div className="px-2 py-1 text-[8px] font-bold text-text-muted uppercase tracking-widest">{group}</div>
|
|
126
|
+
{groupStores.map(s => (
|
|
127
|
+
<button key={s.name} onClick={() => setActiveStore(s.name)} className={`w-full text-left px-2 py-1.5 text-[10px] font-semibold transition-all ${activeStore === s.name ? 'bg-emerald-500/10 text-emerald-400 border-r-2 border-emerald-400' : 'text-text-muted hover:text-text-primary hover:bg-surface-glass'}`}>{s.name}</button>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar p-3 space-y-3">
|
|
134
|
+
<div>
|
|
135
|
+
<div className="text-[10px] font-bold text-text-secondary uppercase tracking-widest mb-2 flex items-center gap-1"><Database size={10} /> Estado ({stateEntries.length})</div>
|
|
136
|
+
<div className="space-y-1">
|
|
137
|
+
{stateEntries.map(([key, value]) => (
|
|
138
|
+
<div key={key} className="flex items-start gap-2 py-0.5">
|
|
139
|
+
<span className="text-[10px] text-text-primary font-mono font-semibold shrink-0 min-w-[100px]">{key}</span>
|
|
140
|
+
<ValueEditor value={value} path={`root.${key}`} onUpdate={handleUpdate} depth={0} />
|
|
141
|
+
</div>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
{actionEntries.length > 0 && (
|
|
146
|
+
<div>
|
|
147
|
+
<div className="text-[10px] font-bold text-text-secondary uppercase tracking-widest mb-2 flex items-center gap-1"><Zap size={10} /> Acciones ({actionEntries.length})</div>
|
|
148
|
+
<div className="flex flex-wrap gap-1">
|
|
149
|
+
{actionEntries.map(([name, fn]) => (
|
|
150
|
+
<button key={name} onClick={() => { try { const r = fn(); if (r instanceof Promise) r.catch((e: any) => setActionFeedback(`❌ ${name}: ${e.message}`)); setActionFeedback(`✅ ${name}()`); } catch (e: any) { setActionFeedback(`❌ ${name}: ${e.message}`); } setTimeout(() => setActionFeedback(null), 2000); }} className="px-2 py-1 rounded text-[9px] font-mono font-bold bg-surface-glass text-text-secondary hover:text-emerald-400 hover:bg-emerald-500/10 border border-border-subtle hover:border-emerald-500/20 transition-all" title={`Ejecutar ${name}()`}>▶ {name}</button>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
{actionFeedback && <div className="mt-2 text-[10px] font-mono text-text-secondary animate-pulse">{actionFeedback}</div>}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* Sprint AU: Snapshot/Diff */}
|
|
158
|
+
<div className="border-t border-border-subtle pt-3 mt-3">
|
|
159
|
+
<div className="text-[10px] font-bold text-text-secondary uppercase tracking-widest mb-2 flex items-center gap-1">
|
|
160
|
+
<Camera size={10} /> Snapshots
|
|
161
|
+
</div>
|
|
162
|
+
<div className="flex flex-wrap gap-1 mb-2">
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => {
|
|
165
|
+
if (!entry) return;
|
|
166
|
+
const state = entry.store.getState();
|
|
167
|
+
const snap: Record<string, any> = {};
|
|
168
|
+
for (const [k, v] of Object.entries(state)) {
|
|
169
|
+
if (typeof v !== 'function') snap[k] = v;
|
|
170
|
+
}
|
|
171
|
+
setSnapshots(prev => [...prev, {
|
|
172
|
+
id: Date.now(),
|
|
173
|
+
label: `${activeStore} @ ${new Date().toLocaleTimeString()}`,
|
|
174
|
+
data: JSON.parse(JSON.stringify(snap, (_, v) => {
|
|
175
|
+
if (v instanceof Map) return Object.fromEntries(v);
|
|
176
|
+
if (v instanceof Set) return Array.from(v);
|
|
177
|
+
return v;
|
|
178
|
+
})),
|
|
179
|
+
}]);
|
|
180
|
+
}}
|
|
181
|
+
className="px-2 py-1 rounded text-[9px] font-mono font-bold bg-violet-500/10 text-violet-400 hover:bg-violet-500/20 border border-violet-500/20"
|
|
182
|
+
>
|
|
183
|
+
📸 Snapshot
|
|
184
|
+
</button>
|
|
185
|
+
{snapshots.length > 0 && showDiff === null && (
|
|
186
|
+
<button
|
|
187
|
+
onClick={() => setShowDiff(snapshots[snapshots.length - 1].id)}
|
|
188
|
+
className="px-2 py-1 rounded text-[9px] font-mono font-bold bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500/20 border border-cyan-500/20"
|
|
189
|
+
>
|
|
190
|
+
<ArrowLeftRight size={10} className="inline mr-1" />Diff vs último
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
{showDiff !== null && (
|
|
194
|
+
<button onClick={() => setShowDiff(null)} className="px-2 py-1 rounded text-[9px] font-mono bg-surface-tertiary text-text-secondary hover:bg-surface-tertiary">
|
|
195
|
+
Cerrar diff
|
|
196
|
+
</button>
|
|
197
|
+
)}
|
|
198
|
+
{snapshots.length > 0 && (
|
|
199
|
+
<button
|
|
200
|
+
onClick={() => {
|
|
201
|
+
const blob = new Blob([JSON.stringify(snapshots, null, 2)], { type: 'application/json' });
|
|
202
|
+
const url = URL.createObjectURL(blob);
|
|
203
|
+
const a = document.createElement('a'); a.href = url; a.download = `snapshots-${activeStore}.json`; a.click();
|
|
204
|
+
URL.revokeObjectURL(url);
|
|
205
|
+
}}
|
|
206
|
+
className="px-2 py-1 rounded text-[9px] font-mono text-text-muted hover:text-text-primary hover:bg-surface-glass"
|
|
207
|
+
>
|
|
208
|
+
<Download size={10} className="inline mr-1" />Export
|
|
209
|
+
</button>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
{/* Snapshot list */}
|
|
213
|
+
{snapshots.map((snap, i) => (
|
|
214
|
+
<div key={snap.id} className="flex items-center gap-2 py-0.5 text-[9px]">
|
|
215
|
+
<span className="text-text-muted font-mono">{snap.label}</span>
|
|
216
|
+
<button onClick={() => setShowDiff(showDiff === snap.id ? null : snap.id)} className={`px-1.5 py-0.5 rounded font-bold ${showDiff === snap.id ? 'bg-cyan-500/20 text-cyan-400' : 'text-text-muted hover:text-cyan-400'}`}>
|
|
217
|
+
diff
|
|
218
|
+
</button>
|
|
219
|
+
<button onClick={() => setSnapshots(prev => prev.filter(s => s.id !== snap.id))} className="text-text-muted hover:text-red-400">×</button>
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
{/* Diff view */}
|
|
223
|
+
{showDiff !== null && (() => {
|
|
224
|
+
const snap = snapshots.find(s => s.id === showDiff);
|
|
225
|
+
if (!snap) return null;
|
|
226
|
+
const current: Record<string, any> = {};
|
|
227
|
+
for (const [k, v] of stateEntries) current[k] = v;
|
|
228
|
+
const allKeys = new Set([...Object.keys(snap.data), ...Object.keys(current)]);
|
|
229
|
+
return (
|
|
230
|
+
<div className="mt-2 bg-surface-primary rounded border border-border-subtle p-2 max-h-[200px] overflow-y-auto custom-scrollbar">
|
|
231
|
+
<div className="text-[8px] text-text-muted mb-1">Diff: {snap.label} → actual</div>
|
|
232
|
+
{Array.from(allKeys).map(key => {
|
|
233
|
+
const oldVal = JSON.stringify(snap.data[key]);
|
|
234
|
+
const newVal = JSON.stringify(current[key], (_, v) => {
|
|
235
|
+
if (v instanceof Map) return Object.fromEntries(v);
|
|
236
|
+
if (v instanceof Set) return Array.from(v);
|
|
237
|
+
return v;
|
|
238
|
+
});
|
|
239
|
+
if (oldVal === newVal) return null;
|
|
240
|
+
return (
|
|
241
|
+
<div key={key} className="py-0.5 font-mono text-[9px]">
|
|
242
|
+
<span className="text-text-secondary font-semibold">{key}:</span>
|
|
243
|
+
{oldVal !== undefined && <div className="text-red-400/70 pl-3">- {oldVal?.slice(0, 100)}</div>}
|
|
244
|
+
{newVal !== undefined && <div className="text-emerald-400/70 pl-3">+ {newVal?.slice(0, 100)}</div>}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
})}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
})()}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ArrowRight, Circle } from 'lucide-react';
|
|
3
|
+
import { usePlaygroundStore } from '../../store/usePlaygroundStore';
|
|
4
|
+
import { useTimelineStore } from '@decido/engine';
|
|
5
|
+
import { useEngineStore } from '@decido/engine';
|
|
6
|
+
import { TRACK_CONFIG } from './debugConfig';
|
|
7
|
+
|
|
8
|
+
export function TopologyTab() {
|
|
9
|
+
const prototypeBrand = usePlaygroundStore(s => s.prototypeBrand);
|
|
10
|
+
const timelines = useTimelineStore(s => s.timelines);
|
|
11
|
+
const activeNodeIds = useEngineStore(s => s.activeNodeIds);
|
|
12
|
+
const flow = timelines[prototypeBrand];
|
|
13
|
+
|
|
14
|
+
if (!flow) return <div className="flex items-center justify-center h-full text-text-muted text-xs font-mono">Sin blueprint activo</div>;
|
|
15
|
+
|
|
16
|
+
const kfs = flow.keyframes || [];
|
|
17
|
+
const edges = flow.edges || [];
|
|
18
|
+
const activeSet = new Set(activeNodeIds);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="h-full overflow-y-auto custom-scrollbar p-3">
|
|
22
|
+
<div className="text-[10px] font-bold text-text-muted uppercase tracking-widest mb-3">
|
|
23
|
+
Grafo: {(flow as any).name || prototypeBrand} · {kfs.length} nodos · {edges.length} aristas
|
|
24
|
+
</div>
|
|
25
|
+
<div className="space-y-1">
|
|
26
|
+
{kfs.map((kf: any) => {
|
|
27
|
+
const cfg = TRACK_CONFIG[kf.track] || { icon: Circle, color: 'text-text-secondary', label: kf.track };
|
|
28
|
+
const Icon = cfg.icon;
|
|
29
|
+
const isActive = activeSet.has(kf.id);
|
|
30
|
+
const outEdges = edges.filter((e: any) => e.source === kf.id);
|
|
31
|
+
return (
|
|
32
|
+
<div key={kf.id} className={`group rounded-lg border transition-all ${isActive ? 'bg-cyan-500/10 border-cyan-500/30 ring-1 ring-cyan-500/20' : 'bg-surface-glass border-border-subtle hover:bg-surface-glass'}`}>
|
|
33
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
34
|
+
<Icon size={13} className={cfg.color} />
|
|
35
|
+
<span className={`text-[11px] font-semibold flex-1 ${isActive ? 'text-cyan-300' : 'text-text-primary'}`}>{kf.label || kf.state || kf.id}</span>
|
|
36
|
+
<span className="text-[9px] text-text-muted font-mono">{kf.track}</span>
|
|
37
|
+
{isActive && <span className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse" />}
|
|
38
|
+
</div>
|
|
39
|
+
{outEdges.length > 0 && (
|
|
40
|
+
<div className="px-3 pb-2 flex flex-wrap gap-1">
|
|
41
|
+
{outEdges.map((e: any, i: number) => {
|
|
42
|
+
const target = kfs.find((k: any) => k.id === e.target);
|
|
43
|
+
return (
|
|
44
|
+
<span key={i} className="inline-flex items-center gap-1 text-[9px] text-text-muted bg-surface-glass rounded px-1.5 py-0.5">
|
|
45
|
+
<ArrowRight size={8} className="text-text-muted" />
|
|
46
|
+
{target?.label || target?.state || e.target}
|
|
47
|
+
{e.sourceHandle && e.sourceHandle !== 'success' && <span className="text-amber-500/70">({e.sourceHandle})</span>}
|
|
48
|
+
</span>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Info, AlertTriangle, AlertCircle, Bug,
|
|
4
|
+
Zap, MessageSquare, Settings, GitBranch,
|
|
5
|
+
ArrowRight, Circle, Map
|
|
6
|
+
} from 'lucide-react';
|
|
7
|
+
import { DebugTab, LogLevel, DebugLogEntry } from '../../store/useDebugPanelStore';
|
|
8
|
+
|
|
9
|
+
// ─── Tab Config ───
|
|
10
|
+
export const TABS: { id: DebugTab; label: string; icon: React.ComponentType<any>; color: string }[] = [
|
|
11
|
+
{ id: 'logs', label: 'Logs', icon: Bug, color: 'text-green-400' },
|
|
12
|
+
{ id: 'flow-health', label: 'Health', icon: () => <span>❤</span>, color: 'text-cyan-400' },
|
|
13
|
+
{ id: 'topology', label: 'Topology', icon: Map, color: 'text-purple-400' },
|
|
14
|
+
{ id: 'replay', label: 'Replay', icon: () => <span>▶</span>, color: 'text-amber-400' },
|
|
15
|
+
{ id: 'stores', label: 'Stores', icon: () => <span>🗄</span>, color: 'text-emerald-400' },
|
|
16
|
+
{ id: 'morph-stack', label: 'Morph', icon: () => <span>🔮</span>, color: 'text-violet-400' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// ─── Level Config ───
|
|
20
|
+
export const LEVEL_CONFIG: Record<LogLevel, { icon: React.ComponentType<any>; color: string; bg: string; label: string }> = {
|
|
21
|
+
info: { icon: Info, color: 'text-blue-400', bg: 'bg-blue-500/10', label: 'INFO' },
|
|
22
|
+
warn: { icon: AlertTriangle, color: 'text-amber-400', bg: 'bg-amber-500/10', label: 'WARN' },
|
|
23
|
+
error: { icon: AlertCircle, color: 'text-red-400', bg: 'bg-red-500/10', label: 'ERROR' },
|
|
24
|
+
debug: { icon: Bug, color: 'text-purple-400', bg: 'bg-purple-500/10', label: 'DEBUG' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ─── Track visual config ───
|
|
28
|
+
export const TRACK_CONFIG: Record<string, { icon: React.ComponentType<any>; color: string; label: string }> = {
|
|
29
|
+
trigger: { icon: Zap, color: 'text-yellow-400', label: 'Trigger' },
|
|
30
|
+
dialogue: { icon: MessageSquare, color: 'text-blue-400', label: 'Diálogo' },
|
|
31
|
+
logic: { icon: Settings, color: 'text-orange-400', label: 'Lógica' },
|
|
32
|
+
ui: { icon: Map, color: 'text-green-400', label: 'UI' },
|
|
33
|
+
render: { icon: Map, color: 'text-pink-400', label: 'Render' },
|
|
34
|
+
condition: { icon: GitBranch, color: 'text-amber-400', label: 'Condición' },
|
|
35
|
+
subflow: { icon: ArrowRight, color: 'text-purple-400', label: 'Subflow' },
|
|
36
|
+
entry: { icon: Circle, color: 'text-emerald-400', label: 'Entry' },
|
|
37
|
+
return: { icon: ArrowRight, color: 'text-red-400', label: 'Return' },
|
|
38
|
+
set: { icon: Settings, color: 'text-cyan-400', label: 'Variable' },
|
|
39
|
+
morph: { icon: Map, color: 'text-violet-400', label: 'Morph' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ─── Log Entry Row ───
|
|
43
|
+
export const LogRow = React.memo(function LogRow({ entry }: { entry: DebugLogEntry }) {
|
|
44
|
+
const cfg = LEVEL_CONFIG[entry.level];
|
|
45
|
+
const Icon = cfg.icon;
|
|
46
|
+
return (
|
|
47
|
+
<div className={`flex items-start gap-2 px-3 py-1 hover:bg-surface-glass font-mono text-[11px] leading-relaxed border-b border-border-subtle ${cfg.bg}`}>
|
|
48
|
+
<span className="text-text-muted shrink-0 w-[70px] pt-0.5">{entry.timestamp}</span>
|
|
49
|
+
<Icon size={12} className={`${cfg.color} shrink-0 mt-0.5`} />
|
|
50
|
+
<span className={`font-bold shrink-0 w-[38px] ${cfg.color}`}>{cfg.label}</span>
|
|
51
|
+
{entry.categories.length > 0 && (
|
|
52
|
+
<span className="shrink-0 flex gap-1">
|
|
53
|
+
{entry.categories.map((c, i) => (
|
|
54
|
+
<span key={i} className="px-1.5 py-0.5 rounded text-[9px] font-bold bg-surface-glass text-text-secondary uppercase tracking-widest">{c}</span>
|
|
55
|
+
))}
|
|
56
|
+
</span>
|
|
57
|
+
)}
|
|
58
|
+
<span className="text-text-primary flex-1 break-all">{entry.message}</span>
|
|
59
|
+
{entry.data !== undefined && (
|
|
60
|
+
<span className="text-text-muted shrink-0 truncate max-w-[200px]">
|
|
61
|
+
{typeof entry.data === 'object' ? JSON.stringify(entry.data) : String(entry.data)}
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'motion/react';
|
|
3
|
+
import { X, Bug, Activity, Map, PlayCircle, Database, Layers, Globe, UserCog, Clock, Gauge, Download, Palette } from 'lucide-react';
|
|
4
|
+
import { useDebugPanelStore, DebugTab } from '../../store/useDebugPanelStore';
|
|
5
|
+
import { LogsTab } from '../debug/LogsTab';
|
|
6
|
+
import { FlowHealthTab } from '../debug/FlowHealthTab';
|
|
7
|
+
import { TopologyTab } from '../debug/TopologyTab';
|
|
8
|
+
import { ReplayTab } from '../debug/ReplayTab';
|
|
9
|
+
import { StoresTab } from '../debug/StoresTab';
|
|
10
|
+
import { MorphStackTab } from '../debug/MorphStackTab';
|
|
11
|
+
import { NetworkTab } from '../debug/NetworkTab';
|
|
12
|
+
import { ProfilesTab } from '../debug/ProfilesTab';
|
|
13
|
+
import { ActionTimelineTab } from '../debug/ActionTimelineTab';
|
|
14
|
+
import { PerformanceTab } from '../debug/PerformanceTab';
|
|
15
|
+
import { ExportTab } from '../debug/ExportTab';
|
|
16
|
+
import { CSSInspectorTab } from '../debug/CSSInspectorTab';
|
|
17
|
+
|
|
18
|
+
// ─── Tab Config ───
|
|
19
|
+
const TABS: { id: DebugTab; label: string; icon: React.ComponentType<any>; color: string }[] = [
|
|
20
|
+
{ id: 'logs', label: 'Logs', icon: Bug, color: 'text-green-400' },
|
|
21
|
+
{ id: 'flow-health', label: 'Health', icon: Activity, color: 'text-cyan-400' },
|
|
22
|
+
{ id: 'topology', label: 'Topology', icon: Map, color: 'text-purple-400' },
|
|
23
|
+
{ id: 'replay', label: 'Replay', icon: PlayCircle, color: 'text-amber-400' },
|
|
24
|
+
{ id: 'stores', label: 'Stores', icon: Database, color: 'text-emerald-400' },
|
|
25
|
+
{ id: 'morph-stack', label: 'Morph', icon: Layers, color: 'text-violet-400' },
|
|
26
|
+
{ id: 'network', label: 'Network', icon: Globe, color: 'text-cyan-400' },
|
|
27
|
+
{ id: 'profiles', label: 'Profiles', icon: UserCog, color: 'text-rose-400' },
|
|
28
|
+
{ id: 'timeline', label: 'Timeline', icon: Clock, color: 'text-amber-400' },
|
|
29
|
+
{ id: 'performance', label: 'Perf', icon: Gauge, color: 'text-lime-400' },
|
|
30
|
+
{ id: 'export', label: 'Export', icon: Download, color: 'text-text-secondary' },
|
|
31
|
+
{ id: 'css-inspector', label: 'CSS', icon: Palette, color: 'text-indigo-400' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// ─── Tab Content Router ───
|
|
35
|
+
const TAB_COMPONENTS: Record<DebugTab, React.ComponentType> = {
|
|
36
|
+
'logs': LogsTab,
|
|
37
|
+
'flow-health': FlowHealthTab,
|
|
38
|
+
'topology': TopologyTab,
|
|
39
|
+
'replay': ReplayTab,
|
|
40
|
+
'stores': StoresTab,
|
|
41
|
+
'morph-stack': MorphStackTab,
|
|
42
|
+
'network': NetworkTab,
|
|
43
|
+
'profiles': ProfilesTab,
|
|
44
|
+
'timeline': ActionTimelineTab,
|
|
45
|
+
'performance': PerformanceTab,
|
|
46
|
+
'export': ExportTab,
|
|
47
|
+
'css-inspector': CSSInspectorTab,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface DebugPanelProps {
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DebugPanel = React.memo(function DebugPanel({ className }: DebugPanelProps) {
|
|
55
|
+
const isOpen = useDebugPanelStore(s => s.isOpen);
|
|
56
|
+
const activeTab = useDebugPanelStore(s => s.activeTab);
|
|
57
|
+
const setActiveTab = useDebugPanelStore(s => s.setActiveTab);
|
|
58
|
+
const togglePanel = useDebugPanelStore(s => s.togglePanel);
|
|
59
|
+
const errorCount = useDebugPanelStore(s => s.errorCount);
|
|
60
|
+
|
|
61
|
+
if (!isOpen) return null;
|
|
62
|
+
|
|
63
|
+
const ActiveComponent = TAB_COMPONENTS[activeTab];
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<AnimatePresence>
|
|
67
|
+
<motion.div
|
|
68
|
+
initial={{ height: 0, opacity: 0 }}
|
|
69
|
+
animate={{ height: '100%', opacity: 1 }}
|
|
70
|
+
exit={{ height: 0, opacity: 0 }}
|
|
71
|
+
transition={{ duration: 0.15, ease: 'easeOut' }}
|
|
72
|
+
className={`flex flex-col bg-surface-primary border-t border-border-default overflow-hidden ${className || ''}`}
|
|
73
|
+
>
|
|
74
|
+
{/* Tab Bar */}
|
|
75
|
+
<div className="flex items-center justify-between px-2 py-1 border-b border-border-subtle shrink-0 bg-surface-primary/80">
|
|
76
|
+
<div className="flex items-center gap-0.5 overflow-x-auto flex-1 min-w-0" style={{ scrollbarWidth: 'none' }}>
|
|
77
|
+
{TABS.map(tab => {
|
|
78
|
+
const Icon = tab.icon;
|
|
79
|
+
const isActive = activeTab === tab.id;
|
|
80
|
+
return (
|
|
81
|
+
<button
|
|
82
|
+
key={tab.id}
|
|
83
|
+
onClick={() => setActiveTab(tab.id)}
|
|
84
|
+
className={`flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-semibold transition-all whitespace-nowrap shrink-0 ${
|
|
85
|
+
isActive ? `bg-surface-glass text-text-primary shadow-xs` : 'text-text-muted hover:text-text-primary hover:bg-surface-glass'
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
<Icon size={11} className={isActive ? tab.color : ''} />
|
|
89
|
+
{tab.label}
|
|
90
|
+
{tab.id === 'logs' && errorCount > 0 && (
|
|
91
|
+
<span className="ml-0.5 px-1 py-0.5 rounded-full text-[8px] font-bold bg-red-500/20 text-red-400">{errorCount}</span>
|
|
92
|
+
)}
|
|
93
|
+
</button>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex items-center gap-1 shrink-0 ml-1">
|
|
98
|
+
<span className="text-[9px] text-text-muted font-mono mr-1">⌃⇧D</span>
|
|
99
|
+
<button onClick={togglePanel} className="p-1 rounded text-text-muted hover:text-text-primary hover:bg-surface-glass transition-colors" title="Cerrar panel">
|
|
100
|
+
<X size={14} />
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Tab Content */}
|
|
106
|
+
<div className="flex-1 overflow-hidden">
|
|
107
|
+
<ActiveComponent />
|
|
108
|
+
</div>
|
|
109
|
+
</motion.div>
|
|
110
|
+
</AnimatePresence>
|
|
111
|
+
);
|
|
112
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'motion/react';
|
|
3
|
+
import { Network } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface HeaderCenterControlsProps {
|
|
6
|
+
isSpeaking: boolean;
|
|
7
|
+
isCreatorMode: boolean;
|
|
8
|
+
creatorViewMode: 'timeline' | 'graph';
|
|
9
|
+
/** @deprecated — use internal useAIModelSelector instead */
|
|
10
|
+
modelLabel?: string;
|
|
11
|
+
onModelClick?: () => void;
|
|
12
|
+
onSyncCreatorMode: (val: boolean) => void;
|
|
13
|
+
onSetCreatorViewMode: (mode: 'timeline' | 'graph') => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const HeaderCenterControls = React.memo(function HeaderCenterControls({
|
|
17
|
+
isSpeaking, isCreatorMode, creatorViewMode, modelLabel, onModelClick, onSyncCreatorMode, onSetCreatorViewMode
|
|
18
|
+
}: HeaderCenterControlsProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div data-tauri-drag-region className="flex-1 flex items-center justify-center gap-2 min-w-0">
|
|
21
|
+
{/* Voice Live Indicator */}
|
|
22
|
+
<AnimatePresence>
|
|
23
|
+
{isSpeaking && (
|
|
24
|
+
<motion.div
|
|
25
|
+
initial={{ opacity: 0, width: 0 }}
|
|
26
|
+
animate={{ opacity: 1, width: 'auto' }}
|
|
27
|
+
exit={{ opacity: 0, width: 0 }}
|
|
28
|
+
className="flex items-center gap-0.5 mr-2 overflow-hidden"
|
|
29
|
+
>
|
|
30
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
31
|
+
<motion.div
|
|
32
|
+
key={i}
|
|
33
|
+
animate={{ height: [6, Math.random() * 16 + 8, 6] }}
|
|
34
|
+
transition={{ repeat: Infinity, duration: 0.5, delay: i * 0.08 }}
|
|
35
|
+
className="w-1 bg-accent-cyan rounded-full"
|
|
36
|
+
/>
|
|
37
|
+
))}
|
|
38
|
+
</motion.div>
|
|
39
|
+
)}
|
|
40
|
+
</AnimatePresence>
|
|
41
|
+
|
|
42
|
+
{/* Mode Toggle — Premium Segmented Control */}
|
|
43
|
+
<div className="flex bg-surface-tertiary border border-border-default rounded-lg p-0.5 shadow-sm">
|
|
44
|
+
<button onClick={() => { if (isCreatorMode) onSyncCreatorMode(false); }}
|
|
45
|
+
className={`px-3 py-1 text-[11px] font-semibold rounded-md transition-all duration-200 ${
|
|
46
|
+
!isCreatorMode
|
|
47
|
+
? 'bg-surface-elevated text-text-primary shadow-sm border border-border-default'
|
|
48
|
+
: 'text-text-muted hover:text-text-secondary border border-transparent'
|
|
49
|
+
}`}>
|
|
50
|
+
Player
|
|
51
|
+
</button>
|
|
52
|
+
<button onClick={() => { if (!isCreatorMode) onSyncCreatorMode(true); }}
|
|
53
|
+
className={`px-3 py-1 text-[11px] font-semibold rounded-md transition-all duration-200 flex items-center gap-1 ${
|
|
54
|
+
isCreatorMode
|
|
55
|
+
? 'bg-surface-elevated text-accent-purple shadow-sm border border-accent-purple'
|
|
56
|
+
: 'text-text-muted hover:text-text-secondary border border-transparent'
|
|
57
|
+
}`}>
|
|
58
|
+
Creator
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Creator Sub-Tabs */}
|
|
63
|
+
<AnimatePresence>
|
|
64
|
+
{isCreatorMode && (
|
|
65
|
+
<motion.div
|
|
66
|
+
initial={{ opacity: 0, scale: 0.9, width: 0 }}
|
|
67
|
+
animate={{ opacity: 1, scale: 1, width: 'auto' }}
|
|
68
|
+
exit={{ opacity: 0, scale: 0.9, width: 0 }}
|
|
69
|
+
className="hidden lg:flex bg-surface-tertiary border border-border-default rounded-lg p-0.5 overflow-hidden shadow-sm"
|
|
70
|
+
>
|
|
71
|
+
<button onClick={() => onSetCreatorViewMode('timeline')}
|
|
72
|
+
className={`px-2.5 py-1 text-[10px] font-bold tracking-wider rounded-md transition-all duration-200 ${
|
|
73
|
+
creatorViewMode === 'timeline'
|
|
74
|
+
? 'bg-surface-elevated text-text-primary shadow-sm border border-border-default'
|
|
75
|
+
: 'text-text-muted hover:text-text-secondary border border-transparent'
|
|
76
|
+
}`}>
|
|
77
|
+
TIMELINE
|
|
78
|
+
</button>
|
|
79
|
+
<button onClick={() => onSetCreatorViewMode('graph')}
|
|
80
|
+
className={`px-2.5 py-1 text-[10px] font-bold tracking-wider rounded-md transition-all duration-200 flex items-center gap-1 ${
|
|
81
|
+
creatorViewMode === 'graph'
|
|
82
|
+
? 'bg-surface-elevated text-text-primary shadow-sm border border-border-default'
|
|
83
|
+
: 'text-text-muted hover:text-text-secondary border border-transparent'
|
|
84
|
+
}`}>
|
|
85
|
+
<Network size={10} /> GRAPH
|
|
86
|
+
</button>
|
|
87
|
+
</motion.div>
|
|
88
|
+
)}
|
|
89
|
+
</AnimatePresence>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
});
|