@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,86 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { AlertTriangle, XCircle, Info, CheckCircle, Circle } from 'lucide-react';
|
|
3
|
+
import { usePlaygroundStore } from '../../store/usePlaygroundStore';
|
|
4
|
+
import { useTimelineStore } from '@decido/engine';
|
|
5
|
+
import { TRACK_CONFIG } from './debugConfig';
|
|
6
|
+
|
|
7
|
+
export function FlowHealthTab() {
|
|
8
|
+
const prototypeBrand = usePlaygroundStore(s => s.prototypeBrand);
|
|
9
|
+
const timelines = useTimelineStore(s => s.timelines);
|
|
10
|
+
const flow = timelines[prototypeBrand];
|
|
11
|
+
|
|
12
|
+
const analysis = useMemo(() => {
|
|
13
|
+
if (!flow) return null;
|
|
14
|
+
const kfs = flow.keyframes || [];
|
|
15
|
+
const edges = flow.edges || [];
|
|
16
|
+
const nodeIds = new Set(kfs.map((k: any) => k.id));
|
|
17
|
+
const trackCounts: Record<string, number> = {};
|
|
18
|
+
kfs.forEach((k: any) => { const t = k.track || 'unknown'; trackCounts[t] = (trackCounts[t] || 0) + 1; });
|
|
19
|
+
const hasIncoming = new Set(edges.map((e: any) => e.target));
|
|
20
|
+
const hasOutgoing = new Set(edges.map((e: any) => e.source));
|
|
21
|
+
const orphans = kfs.filter((k: any) => { if (k.track === 'trigger' || k.track === 'entry') return false; return !hasIncoming.has(k.id) && edges.length > 0; });
|
|
22
|
+
const terminals = kfs.filter((k: any) => hasIncoming.has(k.id) && !hasOutgoing.has(k.id) && k.track !== 'return');
|
|
23
|
+
const deadEdges = edges.filter((e: any) => !nodeIds.has(e.target) || !nodeIds.has(e.source));
|
|
24
|
+
const unlabeled = kfs.filter((k: any) => !k.label && !k.state && k.track !== 'trigger');
|
|
25
|
+
const issues = orphans.length + deadEdges.length + unlabeled.length;
|
|
26
|
+
const score = Math.max(0, Math.min(100, 100 - issues * 10));
|
|
27
|
+
return { kfs, edges, trackCounts, orphans, terminals, deadEdges, unlabeled, score, issues };
|
|
28
|
+
}, [flow]);
|
|
29
|
+
|
|
30
|
+
if (!flow || !analysis) return <div className="flex items-center justify-center h-full text-text-muted text-xs font-mono">Sin blueprint activo</div>;
|
|
31
|
+
|
|
32
|
+
const scoreColor = analysis.score >= 80 ? 'text-green-400' : analysis.score >= 50 ? 'text-amber-400' : 'text-red-400';
|
|
33
|
+
const scoreBg = analysis.score >= 80 ? 'bg-green-500/10' : analysis.score >= 50 ? 'bg-amber-500/10' : 'bg-red-500/10';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="h-full overflow-y-auto custom-scrollbar p-4 space-y-4">
|
|
37
|
+
<div className={`flex items-center gap-4 p-4 rounded-xl ${scoreBg} border border-border-subtle`}>
|
|
38
|
+
<div className={`text-4xl font-black ${scoreColor} font-mono`}>{analysis.score}</div>
|
|
39
|
+
<div>
|
|
40
|
+
<div className={`text-sm font-bold ${scoreColor}`}>{analysis.score >= 80 ? '✅ Saludable' : analysis.score >= 50 ? '⚠️ Revisar' : '🔴 Problemas detectados'}</div>
|
|
41
|
+
<div className="text-[11px] text-text-muted">{analysis.kfs.length} nodos · {analysis.edges.length} conexiones · {analysis.issues} problemas</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="space-y-2">
|
|
45
|
+
<div className="text-[11px] font-bold text-text-secondary uppercase tracking-widest">Distribución por Track</div>
|
|
46
|
+
<div className="grid grid-cols-2 lg:grid-cols-3 gap-2">
|
|
47
|
+
{Object.entries(analysis.trackCounts).map(([track, count]) => {
|
|
48
|
+
const cfg = TRACK_CONFIG[track] || { icon: Circle, color: 'text-text-secondary', label: track };
|
|
49
|
+
const Icon = cfg.icon;
|
|
50
|
+
return (
|
|
51
|
+
<div key={track} className="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface-glass border border-border-subtle">
|
|
52
|
+
<Icon size={14} className={cfg.color} />
|
|
53
|
+
<span className="text-[11px] text-text-primary font-semibold">{cfg.label}</span>
|
|
54
|
+
<span className="ml-auto text-[11px] font-mono text-text-muted">{count}</span>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
{analysis.orphans.length > 0 && (
|
|
61
|
+
<div className="space-y-1">
|
|
62
|
+
<div className="text-[11px] font-bold text-amber-400 flex items-center gap-1"><AlertTriangle size={12} /> Nodos Huérfanos ({analysis.orphans.length})</div>
|
|
63
|
+
{analysis.orphans.map((k: any) => <div key={k.id} className="pl-5 text-[11px] text-text-muted font-mono">· {k.label || k.id} <span className="text-text-muted">({k.track})</span></div>)}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
{analysis.deadEdges.length > 0 && (
|
|
67
|
+
<div className="space-y-1">
|
|
68
|
+
<div className="text-[11px] font-bold text-red-400 flex items-center gap-1"><XCircle size={12} /> Conexiones Rotas ({analysis.deadEdges.length})</div>
|
|
69
|
+
{analysis.deadEdges.map((e: any, i: number) => <div key={i} className="pl-5 text-[11px] text-text-muted font-mono">· {e.source} → {e.target}</div>)}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
{analysis.unlabeled.length > 0 && (
|
|
73
|
+
<div className="space-y-1">
|
|
74
|
+
<div className="text-[11px] font-bold text-text-muted flex items-center gap-1"><Info size={12} /> Sin Etiqueta ({analysis.unlabeled.length})</div>
|
|
75
|
+
{analysis.unlabeled.slice(0, 5).map((k: any) => <div key={k.id} className="pl-5 text-[11px] text-text-muted font-mono">· {k.id} <span className="text-text-muted">({k.track})</span></div>)}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
{analysis.terminals.length > 0 && (
|
|
79
|
+
<div className="space-y-1">
|
|
80
|
+
<div className="text-[11px] font-bold text-text-secondary flex items-center gap-1"><CheckCircle size={12} /> Nodos Terminales ({analysis.terminals.length})</div>
|
|
81
|
+
{analysis.terminals.map((k: any) => <div key={k.id} className="pl-5 text-[11px] text-text-muted font-mono">· {k.label || k.id} <span className="text-text-muted">({k.track})</span></div>)}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useMemo, useState } from 'react';
|
|
2
|
+
import { AnimatePresence, motion } from 'motion/react';
|
|
3
|
+
import { Filter, Trash2, Copy, Check } from 'lucide-react';
|
|
4
|
+
import { useDebugPanelStore, LogLevel } from '../../store/useDebugPanelStore';
|
|
5
|
+
import { LEVEL_CONFIG, LogRow } from './debugConfig';
|
|
6
|
+
|
|
7
|
+
export function LogsTab() {
|
|
8
|
+
const logEntries = useDebugPanelStore(s => s.logEntries);
|
|
9
|
+
const logFilters = useDebugPanelStore(s => s.logFilters);
|
|
10
|
+
const allCategories = useDebugPanelStore(s => s.allCategories);
|
|
11
|
+
const clearLogs = useDebugPanelStore(s => s.clearLogs);
|
|
12
|
+
const toggleLevel = useDebugPanelStore(s => s.toggleLevel);
|
|
13
|
+
const toggleCategory = useDebugPanelStore(s => s.toggleCategory);
|
|
14
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
15
|
+
const [copied, setCopied] = useState(false);
|
|
16
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
20
|
+
}, [logEntries.length]);
|
|
21
|
+
|
|
22
|
+
const filteredEntries = useMemo(() => {
|
|
23
|
+
return logEntries.filter(e => {
|
|
24
|
+
if (!logFilters.levels.has(e.level)) return false;
|
|
25
|
+
if (logFilters.categories.size > 0 && !e.categories.some(c => logFilters.categories.has(c))) return false;
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
}, [logEntries, logFilters]);
|
|
29
|
+
|
|
30
|
+
const catArray = useMemo(() => Array.from(allCategories).sort(), [allCategories]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-col h-full">
|
|
34
|
+
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-border-subtle shrink-0 bg-surface-primary/50">
|
|
35
|
+
<div className="flex gap-1">
|
|
36
|
+
{(Object.keys(LEVEL_CONFIG) as LogLevel[]).map(lvl => {
|
|
37
|
+
const cfg = LEVEL_CONFIG[lvl];
|
|
38
|
+
const active = logFilters.levels.has(lvl);
|
|
39
|
+
const count = logEntries.filter(e => e.level === lvl).length;
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
key={lvl}
|
|
43
|
+
onClick={() => toggleLevel(lvl)}
|
|
44
|
+
className={`flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold transition-all border ${
|
|
45
|
+
active ? `${cfg.bg} ${cfg.color} border-current/20` : 'bg-transparent text-text-muted border-transparent hover:text-text-secondary'
|
|
46
|
+
}`}
|
|
47
|
+
>
|
|
48
|
+
{cfg.label}
|
|
49
|
+
{count > 0 && <span className="text-[9px] opacity-60">{count}</span>}
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex-1" />
|
|
55
|
+
{catArray.length > 0 && (
|
|
56
|
+
<button
|
|
57
|
+
onClick={() => setShowFilters(!showFilters)}
|
|
58
|
+
className={`p-1 rounded transition-colors ${showFilters ? 'bg-cyan-500/20 text-cyan-400' : 'text-text-muted hover:text-text-primary hover:bg-surface-glass'}`}
|
|
59
|
+
title="Filtrar categorías"
|
|
60
|
+
>
|
|
61
|
+
<Filter size={12} />
|
|
62
|
+
</button>
|
|
63
|
+
)}
|
|
64
|
+
<span className="text-[10px] text-text-muted font-mono">{filteredEntries.length}/{logEntries.length}</span>
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => {
|
|
67
|
+
const text = filteredEntries.map(e => {
|
|
68
|
+
const cats = e.categories.length > 0 ? ` [${e.categories.join(',')}]` : '';
|
|
69
|
+
const data = e.data !== undefined ? ` | ${typeof e.data === 'object' ? JSON.stringify(e.data) : String(e.data)}` : '';
|
|
70
|
+
return `[${e.timestamp}] ${LEVEL_CONFIG[e.level].label}${cats} ${e.message}${data}`;
|
|
71
|
+
}).join('\n');
|
|
72
|
+
navigator.clipboard.writeText(text).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); });
|
|
73
|
+
}}
|
|
74
|
+
className={`p-1 rounded transition-colors ${copied ? 'text-green-400 bg-green-500/10' : 'text-text-muted hover:text-cyan-400 hover:bg-cyan-500/10'}`}
|
|
75
|
+
title="Copiar logs visibles"
|
|
76
|
+
>
|
|
77
|
+
{copied ? <Check size={12} /> : <Copy size={12} />}
|
|
78
|
+
</button>
|
|
79
|
+
<button onClick={clearLogs} className="p-1 rounded text-text-muted hover:text-red-400 hover:bg-red-500/10 transition-colors" title="Limpiar logs">
|
|
80
|
+
<Trash2 size={12} />
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<AnimatePresence>
|
|
85
|
+
{showFilters && catArray.length > 0 && (
|
|
86
|
+
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden border-b border-border-subtle">
|
|
87
|
+
<div className="flex flex-wrap gap-1 px-3 py-2 bg-surface-primary/80">
|
|
88
|
+
{catArray.map(cat => {
|
|
89
|
+
const active = logFilters.categories.size === 0 || logFilters.categories.has(cat);
|
|
90
|
+
return (
|
|
91
|
+
<button key={cat} onClick={() => toggleCategory(cat)} className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider transition-all border ${active ? 'bg-cyan-500/10 text-cyan-400 border-cyan-500/20' : 'bg-transparent text-text-muted border-transparent hover:text-text-secondary'}`}>
|
|
92
|
+
{cat}
|
|
93
|
+
</button>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
</motion.div>
|
|
98
|
+
)}
|
|
99
|
+
</AnimatePresence>
|
|
100
|
+
|
|
101
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto custom-scrollbar">
|
|
102
|
+
{filteredEntries.length === 0 ? (
|
|
103
|
+
<div className="flex items-center justify-center h-full text-text-muted text-xs font-mono">Sin entradas de log</div>
|
|
104
|
+
) : (
|
|
105
|
+
filteredEntries.map(entry => <LogRow key={entry.id} entry={entry} />)
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import React, { useMemo, useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { useMorphologyStore } from '../../store/useMorphologyStore';
|
|
3
|
+
import { useLayoutStore } from '../../store/useLayoutStore';
|
|
4
|
+
import { Layers, AlertTriangle, ArrowDown, ArrowUp, Clock, RotateCcw } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface MorphEvent {
|
|
7
|
+
id: number;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
action: string;
|
|
10
|
+
stageType: string | null;
|
|
11
|
+
label: string;
|
|
12
|
+
data?: any;
|
|
13
|
+
level: 'info' | 'warn' | 'error';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let _eventId = 0;
|
|
17
|
+
const MAX_EVENTS = 100;
|
|
18
|
+
|
|
19
|
+
const STAGE_ICONS: Record<string, string> = {
|
|
20
|
+
'2d': '📊',
|
|
21
|
+
'3d': '🧊',
|
|
22
|
+
workbench: '🔧',
|
|
23
|
+
liquid: '💧',
|
|
24
|
+
artifact: '📄',
|
|
25
|
+
custom: '⚙️',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const STAGE_COLORS: Record<string, string> = {
|
|
29
|
+
'2d': 'border-cyan-500 bg-cyan-500/10',
|
|
30
|
+
'3d': 'border-violet-500 bg-violet-500/10',
|
|
31
|
+
workbench: 'border-amber-500 bg-amber-500/10',
|
|
32
|
+
liquid: 'border-blue-500 bg-blue-500/10',
|
|
33
|
+
artifact: 'border-emerald-500 bg-emerald-500/10',
|
|
34
|
+
custom: 'border-border-default bg-surface-glass',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function getTimestamp(): string {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function MorphStackTab() {
|
|
43
|
+
const morphActiveStage = useMorphologyStore(s => s.activeStage);
|
|
44
|
+
const morphHistory = useMorphologyStore(s => s.stageHistory);
|
|
45
|
+
const layoutActiveStage = useLayoutStore(s => s.activeStage);
|
|
46
|
+
const layoutHistory = useLayoutStore(s => s.stageHistory);
|
|
47
|
+
|
|
48
|
+
const [events, setEvents] = useState<MorphEvent[]>([]);
|
|
49
|
+
const eventLogRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
|
|
51
|
+
// Detect desync between morphology and layout stores
|
|
52
|
+
const isDesync = useMemo(() => {
|
|
53
|
+
const morphType = morphActiveStage?.type || null;
|
|
54
|
+
const layoutType = layoutActiveStage?.type || null;
|
|
55
|
+
return morphType !== layoutType;
|
|
56
|
+
}, [morphActiveStage, layoutActiveStage]);
|
|
57
|
+
|
|
58
|
+
// Stack depth warning
|
|
59
|
+
const stackDepth = morphHistory.length;
|
|
60
|
+
const isDeepStack = stackDepth > 5;
|
|
61
|
+
|
|
62
|
+
// Subscribe to morphology store changes to build event log
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
let prevStage = useMorphologyStore.getState().activeStage;
|
|
65
|
+
let prevDepth = useMorphologyStore.getState().stageHistory.length;
|
|
66
|
+
|
|
67
|
+
const unsub = useMorphologyStore.subscribe((state) => {
|
|
68
|
+
const newStage = state.activeStage;
|
|
69
|
+
const newDepth = state.stageHistory.length;
|
|
70
|
+
|
|
71
|
+
if (newStage !== prevStage || newDepth !== prevDepth) {
|
|
72
|
+
let action = 'change';
|
|
73
|
+
let level: MorphEvent['level'] = 'info';
|
|
74
|
+
|
|
75
|
+
if (newDepth > prevDepth) {
|
|
76
|
+
action = '⬇️ pushStage';
|
|
77
|
+
} else if (newDepth < prevDepth && newStage) {
|
|
78
|
+
action = '⬆️ popStage';
|
|
79
|
+
} else if (!newStage && prevStage) {
|
|
80
|
+
action = '🗑️ clearStages';
|
|
81
|
+
level = 'warn';
|
|
82
|
+
} else if (newStage && !prevStage) {
|
|
83
|
+
action = '📌 setStage';
|
|
84
|
+
} else {
|
|
85
|
+
action = '🔄 setStage';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setEvents(prev => {
|
|
89
|
+
const updated = [...prev, {
|
|
90
|
+
id: ++_eventId,
|
|
91
|
+
timestamp: getTimestamp(),
|
|
92
|
+
action,
|
|
93
|
+
stageType: newStage?.type || null,
|
|
94
|
+
label: newStage?.label || newStage?.type || 'null',
|
|
95
|
+
data: { depth: newDepth, sourceNodeId: newStage?.sourceNodeId },
|
|
96
|
+
level,
|
|
97
|
+
}];
|
|
98
|
+
return updated.length > MAX_EVENTS ? updated.slice(-MAX_EVENTS) : updated;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
prevStage = newStage;
|
|
102
|
+
prevDepth = newDepth;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return unsub;
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
// Auto-scroll event log
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (eventLogRef.current) {
|
|
111
|
+
eventLogRef.current.scrollTop = eventLogRef.current.scrollHeight;
|
|
112
|
+
}
|
|
113
|
+
}, [events]);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="flex h-full text-[10px]">
|
|
117
|
+
{/* Left: Stack Visualization */}
|
|
118
|
+
<div className="w-48 shrink-0 border-r border-border-subtle p-3 space-y-2 overflow-y-auto">
|
|
119
|
+
<div className="text-[9px] font-bold text-text-muted uppercase tracking-widest mb-2 flex items-center gap-1">
|
|
120
|
+
<Layers size={10} /> Stack ({stackDepth + (morphActiveStage ? 1 : 0)})
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Warnings */}
|
|
124
|
+
{isDesync && (
|
|
125
|
+
<div className="px-2 py-1.5 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-[9px] font-bold flex items-center gap-1 animate-pulse">
|
|
126
|
+
<AlertTriangle size={10} /> Stores desincronizados
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
{isDeepStack && (
|
|
130
|
+
<div className="px-2 py-1.5 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-400 text-[9px] font-bold flex items-center gap-1">
|
|
131
|
+
<AlertTriangle size={10} /> Stack depth: {stackDepth} (posible leak)
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Active Stage */}
|
|
136
|
+
{morphActiveStage ? (
|
|
137
|
+
<div className={`px-3 py-2 rounded-xl border-2 ${STAGE_COLORS[morphActiveStage.type] || 'border-border-default bg-surface-glass'} relative`}>
|
|
138
|
+
<div className="absolute -top-2 right-2 px-1.5 py-0.5 rounded text-[7px] font-bold bg-cyan-500 text-text-inverse uppercase">Activo</div>
|
|
139
|
+
<div className="flex items-center gap-1.5">
|
|
140
|
+
<span className="text-lg">{STAGE_ICONS[morphActiveStage.type] || '❓'}</span>
|
|
141
|
+
<div>
|
|
142
|
+
<div className="font-bold text-text-primary text-[11px]">{morphActiveStage.type.toUpperCase()}</div>
|
|
143
|
+
{morphActiveStage.label && <div className="text-text-secondary text-[9px]">{morphActiveStage.label}</div>}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
{morphActiveStage.sourceNodeId && (
|
|
147
|
+
<div className="mt-1 text-[8px] text-text-muted font-mono truncate">src: {morphActiveStage.sourceNodeId}</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
) : (
|
|
151
|
+
<div className="px-3 py-2 rounded-xl border border-dashed border-border-default text-text-muted text-center italic">
|
|
152
|
+
Sin stage activo
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* Arrow */}
|
|
157
|
+
{morphHistory.length > 0 && (
|
|
158
|
+
<div className="flex justify-center text-text-muted"><ArrowUp size={12} /></div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* History Stack (reversed: most recent on top) */}
|
|
162
|
+
{[...morphHistory].reverse().map((stage, i) => (
|
|
163
|
+
<div
|
|
164
|
+
key={i}
|
|
165
|
+
className={`px-3 py-1.5 rounded-lg border ${STAGE_COLORS[stage.type] || 'border-border-default bg-surface-tertiary/50'} opacity-50 hover:opacity-80 cursor-pointer transition-all`}
|
|
166
|
+
onClick={() => {
|
|
167
|
+
// Pop back to this level
|
|
168
|
+
const popsNeeded = i + 1;
|
|
169
|
+
const ms = useMorphologyStore.getState();
|
|
170
|
+
const ls = useLayoutStore.getState();
|
|
171
|
+
for (let j = 0; j < popsNeeded; j++) { ms.popStage(); ls.popStage(); }
|
|
172
|
+
}}
|
|
173
|
+
title={`Click para volver a: ${stage.label || stage.type}`}
|
|
174
|
+
>
|
|
175
|
+
<div className="flex items-center gap-1.5">
|
|
176
|
+
<span>{STAGE_ICONS[stage.type] || '❓'}</span>
|
|
177
|
+
<span className="font-semibold text-text-primary">{stage.type}</span>
|
|
178
|
+
{stage.label && <span className="text-text-muted truncate max-w-[80px]"> · {stage.label}</span>}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Right: Event Log */}
|
|
185
|
+
<div className="flex-1 flex flex-col">
|
|
186
|
+
<div className="px-3 py-1.5 border-b border-border-subtle flex items-center justify-between">
|
|
187
|
+
<div className="text-[9px] font-bold text-text-muted uppercase tracking-widest flex items-center gap-1">
|
|
188
|
+
<Clock size={10} /> Eventos ({events.length})
|
|
189
|
+
</div>
|
|
190
|
+
<button onClick={() => setEvents([])} className="p-0.5 rounded hover:bg-surface-glass text-text-muted hover:text-text-secondary" title="Limpiar eventos">
|
|
191
|
+
<RotateCcw size={10} />
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
<div ref={eventLogRef} className="flex-1 overflow-y-auto custom-scrollbar">
|
|
195
|
+
{events.length === 0 ? (
|
|
196
|
+
<div className="flex items-center justify-center h-full text-text-muted italic">
|
|
197
|
+
Esperando eventos morph...
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
events.map((evt) => (
|
|
201
|
+
<div
|
|
202
|
+
key={evt.id}
|
|
203
|
+
className={`flex items-start gap-2 px-3 py-1 border-b border-border-subtle hover:bg-surface-glass ${
|
|
204
|
+
evt.level === 'warn' ? 'bg-amber-500/5' : evt.level === 'error' ? 'bg-red-500/5' : ''
|
|
205
|
+
}`}
|
|
206
|
+
>
|
|
207
|
+
<span className="text-text-muted shrink-0 font-mono w-[50px]">{evt.timestamp}</span>
|
|
208
|
+
<span className="shrink-0 w-[90px] font-semibold text-text-secondary">{evt.action}</span>
|
|
209
|
+
<span className={`font-mono ${evt.stageType ? 'text-cyan-400' : 'text-text-muted'}`}>
|
|
210
|
+
{evt.label}
|
|
211
|
+
</span>
|
|
212
|
+
{evt.data?.depth !== undefined && (
|
|
213
|
+
<span className="text-text-muted ml-auto shrink-0">depth: {evt.data.depth}</span>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
))
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* Desync Detail */}
|
|
221
|
+
{isDesync && (
|
|
222
|
+
<div className="px-3 py-2 border-t border-red-500/20 bg-red-500/5">
|
|
223
|
+
<div className="text-[9px] font-bold text-red-400 mb-1">⚠️ Store Desync Detectado</div>
|
|
224
|
+
<div className="grid grid-cols-2 gap-2 text-[9px] font-mono">
|
|
225
|
+
<div>
|
|
226
|
+
<span className="text-text-muted">morph:</span>{' '}
|
|
227
|
+
<span className="text-violet-400">{morphActiveStage?.type || 'null'}</span>
|
|
228
|
+
<span className="text-text-muted"> (depth: {morphHistory.length})</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div>
|
|
231
|
+
<span className="text-text-muted">layout:</span>{' '}
|
|
232
|
+
<span className="text-emerald-400">{layoutActiveStage?.type || 'null'}</span>
|
|
233
|
+
<span className="text-text-muted"> (depth: {layoutHistory.length})</span>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { useState, useSyncExternalStore, useCallback, useMemo } from 'react';
|
|
2
|
+
import { Globe, ChevronDown, ChevronRight, RotateCcw, Filter } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
getNetworkEntries, clearNetworkEntries, subscribeNetwork, installNetworkInterceptor,
|
|
5
|
+
type NetworkEntry,
|
|
6
|
+
} from '../../lib/networkInterceptor';
|
|
7
|
+
|
|
8
|
+
// Install interceptor on import
|
|
9
|
+
installNetworkInterceptor();
|
|
10
|
+
|
|
11
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
12
|
+
'2': 'text-emerald-400',
|
|
13
|
+
'3': 'text-amber-400',
|
|
14
|
+
'4': 'text-red-400',
|
|
15
|
+
'5': 'text-red-500',
|
|
16
|
+
'0': 'text-text-muted',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getStatusColor(status: number): string {
|
|
20
|
+
return STATUS_COLORS[String(status).charAt(0)] || 'text-text-secondary';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function truncateUrl(url: string): string {
|
|
24
|
+
try {
|
|
25
|
+
const u = new URL(url);
|
|
26
|
+
const path = u.pathname + u.search;
|
|
27
|
+
return path.length > 60 ? path.slice(0, 57) + '...' : path;
|
|
28
|
+
} catch {
|
|
29
|
+
return url.length > 60 ? url.slice(0, 57) + '...' : url;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getDomain(url: string): string {
|
|
34
|
+
try { return new URL(url).hostname; } catch { return ''; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatSize(bytes: number): string {
|
|
38
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
39
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
40
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function NetworkRow({ entry, isExpanded, onToggle }: { entry: NetworkEntry; isExpanded: boolean; onToggle: () => void }) {
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<div
|
|
47
|
+
onClick={onToggle}
|
|
48
|
+
className={`flex items-center gap-2 px-3 py-1 text-[10px] font-mono cursor-pointer hover:bg-surface-glass border-b border-border-subtle ${
|
|
49
|
+
entry.error ? 'bg-red-500/5' : ''
|
|
50
|
+
}`}
|
|
51
|
+
>
|
|
52
|
+
{isExpanded ? <ChevronDown size={10} className="text-text-muted" /> : <ChevronRight size={10} className="text-text-muted" />}
|
|
53
|
+
<span className="text-text-muted w-[50px] shrink-0">{entry.timestamp}</span>
|
|
54
|
+
<span className={`w-[40px] shrink-0 font-bold ${entry.method === 'POST' ? 'text-amber-400' : 'text-cyan-400'}`}>{entry.method}</span>
|
|
55
|
+
<span className={`w-[30px] shrink-0 font-bold ${getStatusColor(entry.status)}`}>{entry.status || 'ERR'}</span>
|
|
56
|
+
<span className="flex-1 text-text-primary truncate" title={entry.url}>{truncateUrl(entry.url)}</span>
|
|
57
|
+
<span className="text-text-muted w-[50px] shrink-0 text-right">{entry.duration}ms</span>
|
|
58
|
+
<span className="text-text-muted w-[40px] shrink-0 text-right">{formatSize(entry.size)}</span>
|
|
59
|
+
</div>
|
|
60
|
+
{isExpanded && (
|
|
61
|
+
<div className="px-6 py-2 bg-surface-primary/80 border-b border-border-subtle text-[9px] font-mono space-y-2">
|
|
62
|
+
<div><span className="text-text-muted">URL:</span> <span className="text-text-primary break-all">{entry.url}</span></div>
|
|
63
|
+
{entry.error && <div className="text-red-400">Error: {entry.error}</div>}
|
|
64
|
+
{entry.requestBody && (
|
|
65
|
+
<div>
|
|
66
|
+
<div className="text-text-muted mb-0.5">Request Body:</div>
|
|
67
|
+
<pre className="bg-surface-secondary rounded p-2 text-text-secondary overflow-x-auto max-h-[150px] overflow-y-auto whitespace-pre-wrap">{
|
|
68
|
+
(() => { try { return JSON.stringify(JSON.parse(entry.requestBody), null, 2); } catch { return entry.requestBody; } })()
|
|
69
|
+
}</pre>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
{entry.responseBody && (
|
|
73
|
+
<div>
|
|
74
|
+
<div className="text-text-muted mb-0.5">Response Body:</div>
|
|
75
|
+
<pre className="bg-surface-secondary rounded p-2 text-text-secondary overflow-x-auto max-h-[150px] overflow-y-auto whitespace-pre-wrap">{
|
|
76
|
+
(() => { try { return JSON.stringify(JSON.parse(entry.responseBody), null, 2); } catch { return entry.responseBody; } })()
|
|
77
|
+
}</pre>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function NetworkTab() {
|
|
87
|
+
const entries = useSyncExternalStore(
|
|
88
|
+
useCallback((cb) => subscribeNetwork(cb), []),
|
|
89
|
+
useCallback(() => getNetworkEntries(), [])
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
|
|
93
|
+
const [domainFilter, setDomainFilter] = useState<string>('');
|
|
94
|
+
|
|
95
|
+
// Unique domains for filter
|
|
96
|
+
const domains = useMemo(() => {
|
|
97
|
+
const set = new Set<string>();
|
|
98
|
+
entries.forEach(e => { const d = getDomain(e.url); if (d) set.add(d); });
|
|
99
|
+
return Array.from(set).sort();
|
|
100
|
+
}, [entries]);
|
|
101
|
+
|
|
102
|
+
const filtered = useMemo(() => {
|
|
103
|
+
if (!domainFilter) return entries;
|
|
104
|
+
return entries.filter(e => getDomain(e.url).includes(domainFilter));
|
|
105
|
+
}, [entries, domainFilter]);
|
|
106
|
+
|
|
107
|
+
const toggleExpand = useCallback((id: number) => {
|
|
108
|
+
setExpandedIds(prev => {
|
|
109
|
+
const next = new Set(prev);
|
|
110
|
+
if (next.has(id)) next.delete(id); else next.add(id);
|
|
111
|
+
return next;
|
|
112
|
+
});
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const totalErrors = useMemo(() => entries.filter(e => e.status >= 400 || e.error).length, [entries]);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex flex-col h-full">
|
|
119
|
+
{/* Toolbar */}
|
|
120
|
+
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-border-subtle shrink-0">
|
|
121
|
+
<Globe size={12} className="text-cyan-400" />
|
|
122
|
+
<span className="text-[10px] font-bold text-text-secondary uppercase tracking-widest">Network</span>
|
|
123
|
+
<span className="text-[9px] text-text-muted font-mono">{filtered.length}/{entries.length}</span>
|
|
124
|
+
{totalErrors > 0 && (
|
|
125
|
+
<span className="px-1.5 py-0.5 rounded-full text-[8px] font-bold bg-red-500/20 text-red-400">{totalErrors} err</span>
|
|
126
|
+
)}
|
|
127
|
+
<div className="flex-1" />
|
|
128
|
+
{domains.length > 0 && (
|
|
129
|
+
<select
|
|
130
|
+
value={domainFilter}
|
|
131
|
+
onChange={e => setDomainFilter(e.target.value)}
|
|
132
|
+
className="bg-surface-secondary border border-border-default rounded px-1.5 py-0.5 text-[9px] text-text-secondary outline-hidden"
|
|
133
|
+
>
|
|
134
|
+
<option value="">All domains</option>
|
|
135
|
+
{domains.map(d => <option key={d} value={d}>{d}</option>)}
|
|
136
|
+
</select>
|
|
137
|
+
)}
|
|
138
|
+
<button onClick={clearNetworkEntries} className="p-0.5 rounded hover:bg-surface-glass text-text-muted hover:text-text-secondary" title="Limpiar">
|
|
139
|
+
<RotateCcw size={10} />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Header Row */}
|
|
144
|
+
<div className="flex items-center gap-2 px-3 py-0.5 text-[8px] font-bold text-text-muted uppercase tracking-widest border-b border-border-subtle shrink-0">
|
|
145
|
+
<span className="w-[14px]" />
|
|
146
|
+
<span className="w-[50px]">Time</span>
|
|
147
|
+
<span className="w-[40px]">Method</span>
|
|
148
|
+
<span className="w-[30px]">Status</span>
|
|
149
|
+
<span className="flex-1">URL</span>
|
|
150
|
+
<span className="w-[50px] text-right">Duration</span>
|
|
151
|
+
<span className="w-[40px] text-right">Size</span>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Entries */}
|
|
155
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
|
156
|
+
{filtered.length === 0 ? (
|
|
157
|
+
<div className="flex items-center justify-center h-full text-text-muted text-[10px] italic">
|
|
158
|
+
Sin peticiones de red capturadas
|
|
159
|
+
</div>
|
|
160
|
+
) : (
|
|
161
|
+
filtered.map(entry => (
|
|
162
|
+
<NetworkRow
|
|
163
|
+
key={entry.id}
|
|
164
|
+
entry={entry}
|
|
165
|
+
isExpanded={expandedIds.has(entry.id)}
|
|
166
|
+
onToggle={() => toggleExpand(entry.id)}
|
|
167
|
+
/>
|
|
168
|
+
))
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|