@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,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGeminiStream — Direct Gemini API streaming for Decido Studio chat.
|
|
3
|
+
*
|
|
4
|
+
* Uses the REST SSE endpoint to stream responses from Gemini,
|
|
5
|
+
* emitting ChatMessage V2 fields (thinking, executionSteps) in real time.
|
|
6
|
+
*
|
|
7
|
+
* 📚 Inspired by Gemini CLI (geminiChat.ts):
|
|
8
|
+
* - Retry with exponential backoff on 429/500/503
|
|
9
|
+
* - Content retry on empty/invalid responses
|
|
10
|
+
* - Abort signal propagation for clean cancellation
|
|
11
|
+
* - Chunk validation (finishReason check)
|
|
12
|
+
*
|
|
13
|
+
* API key is read from:
|
|
14
|
+
* 1. localStorage 'google_gemini_api_key'
|
|
15
|
+
* 2. import.meta.env.VITE_GEMINI_API_KEY
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* const { sendMessage, abort, isStreaming } = useGeminiStream();
|
|
19
|
+
* sendMessage('Explica qué es un grafo DSD');
|
|
20
|
+
*/
|
|
21
|
+
import { useState, useCallback, useRef } from 'react';
|
|
22
|
+
import { usePlaygroundStore } from '../store/usePlaygroundStore';
|
|
23
|
+
import type { ExecutionStep } from '../store/playgroundTypes';
|
|
24
|
+
|
|
25
|
+
interface UseGeminiStreamOptions {
|
|
26
|
+
model?: string;
|
|
27
|
+
systemInstruction?: string;
|
|
28
|
+
maxRetries?: number; // Max retry attempts (default: 3)
|
|
29
|
+
initialRetryDelay?: number; // Initial delay in ms (default: 500)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Retry configuration inspired by Gemini CLI's retryWithBackoff */
|
|
33
|
+
interface RetryConfig {
|
|
34
|
+
maxAttempts: number;
|
|
35
|
+
initialDelay: number;
|
|
36
|
+
maxDelay: number;
|
|
37
|
+
retryableStatusCodes: number[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_RETRY: RetryConfig = {
|
|
41
|
+
maxAttempts: 3,
|
|
42
|
+
initialDelay: 500,
|
|
43
|
+
maxDelay: 30000,
|
|
44
|
+
retryableStatusCodes: [429, 500, 502, 503],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function useGeminiStream(options: UseGeminiStreamOptions = {}) {
|
|
48
|
+
const {
|
|
49
|
+
model = 'gemini-flash-lite-latest',
|
|
50
|
+
systemInstruction,
|
|
51
|
+
maxRetries = DEFAULT_RETRY.maxAttempts,
|
|
52
|
+
initialRetryDelay = DEFAULT_RETRY.initialDelay,
|
|
53
|
+
} = options;
|
|
54
|
+
|
|
55
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
56
|
+
const [error, setError] = useState<string | null>(null);
|
|
57
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
58
|
+
const addChatMessage = usePlaygroundStore(s => s.addChatMessage);
|
|
59
|
+
|
|
60
|
+
const getApiKey = useCallback(() => {
|
|
61
|
+
return localStorage.getItem('google_gemini_api_key')
|
|
62
|
+
|| (import.meta as any).env?.VITE_GEMINI_API_KEY
|
|
63
|
+
|| '';
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const sendMessage = useCallback(async (prompt: string) => {
|
|
67
|
+
const apiKey = getApiKey();
|
|
68
|
+
if (!apiKey) {
|
|
69
|
+
setError('No Gemini API key found. Set it in localStorage "google_gemini_api_key" or VITE_GEMINI_API_KEY.');
|
|
70
|
+
addChatMessage({
|
|
71
|
+
type: 'error', sender: 'system',
|
|
72
|
+
text: '⚠️ No se encontró API key de Gemini. Configúrala en localStorage como "google_gemini_api_key".',
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setIsStreaming(true);
|
|
78
|
+
setError(null);
|
|
79
|
+
|
|
80
|
+
// Add user message
|
|
81
|
+
addChatMessage({ type: 'text', sender: 'user', text: prompt });
|
|
82
|
+
|
|
83
|
+
const executionSteps: ExecutionStep[] = [];
|
|
84
|
+
const thinkingStart = Date.now();
|
|
85
|
+
executionSteps.push({ type: 'thinking', label: 'Processing', startTime: thinkingStart });
|
|
86
|
+
|
|
87
|
+
// Create AbortController
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
abortRef.current = controller;
|
|
90
|
+
|
|
91
|
+
const retryConfig: RetryConfig = {
|
|
92
|
+
...DEFAULT_RETRY,
|
|
93
|
+
maxAttempts: maxRetries,
|
|
94
|
+
initialDelay: initialRetryDelay,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`;
|
|
99
|
+
|
|
100
|
+
const body: any = {
|
|
101
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
102
|
+
generationConfig: { temperature: 0.7, maxOutputTokens: 4096 },
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (systemInstruction) {
|
|
106
|
+
body.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Retry loop with exponential backoff (from Gemini CLI) ──
|
|
110
|
+
let response: Response | null = null;
|
|
111
|
+
let delay = retryConfig.initialDelay;
|
|
112
|
+
|
|
113
|
+
for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
|
|
114
|
+
if (controller.signal.aborted) break;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
response = await fetch(url, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify(body),
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (response.ok) break; // Success, exit retry loop
|
|
125
|
+
|
|
126
|
+
// Check retryable status
|
|
127
|
+
if (retryConfig.retryableStatusCodes.includes(response.status)) {
|
|
128
|
+
if (attempt < retryConfig.maxAttempts) {
|
|
129
|
+
executionSteps.push({
|
|
130
|
+
type: 'retry' as any,
|
|
131
|
+
label: `Retry ${attempt}/${retryConfig.maxAttempts} (HTTP ${response.status})`,
|
|
132
|
+
startTime: Date.now(),
|
|
133
|
+
});
|
|
134
|
+
// Update UI with retry step
|
|
135
|
+
const retryMsgId = `retry-${Date.now()}`;
|
|
136
|
+
usePlaygroundStore.getState().upsertChatMessage(retryMsgId, {
|
|
137
|
+
id: retryMsgId, type: 'alert', sender: 'system',
|
|
138
|
+
text: `🔄 Reintentando (${attempt}/${retryConfig.maxAttempts})... HTTP ${response.status}`,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
143
|
+
delay = Math.min(delay * 2, retryConfig.maxDelay);
|
|
144
|
+
response = null; // Reset for next attempt
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Non-retryable error
|
|
150
|
+
const errText = await response.text();
|
|
151
|
+
throw new Error(`Gemini API error ${response.status}: ${errText.slice(0, 200)}`);
|
|
152
|
+
} catch (fetchErr: any) {
|
|
153
|
+
if (fetchErr.name === 'AbortError') throw fetchErr;
|
|
154
|
+
if (attempt >= retryConfig.maxAttempts) throw fetchErr;
|
|
155
|
+
|
|
156
|
+
// Network error → retry
|
|
157
|
+
executionSteps.push({
|
|
158
|
+
type: 'retry' as any,
|
|
159
|
+
label: `Retry ${attempt}/${retryConfig.maxAttempts} (network)`,
|
|
160
|
+
startTime: Date.now(),
|
|
161
|
+
});
|
|
162
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
163
|
+
delay = Math.min(delay * 2, retryConfig.maxDelay);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!response || !response.ok) {
|
|
168
|
+
throw new Error('All retry attempts exhausted');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Mark thinking done, start text streaming
|
|
172
|
+
executionSteps[0].endTime = Date.now();
|
|
173
|
+
const textStart = Date.now();
|
|
174
|
+
executionSteps.push({ type: 'text_delta', label: 'Streaming', startTime: textStart });
|
|
175
|
+
|
|
176
|
+
// Read SSE stream
|
|
177
|
+
const reader = response.body?.getReader();
|
|
178
|
+
if (!reader) throw new Error('No response body reader');
|
|
179
|
+
|
|
180
|
+
const decoder = new TextDecoder();
|
|
181
|
+
let fullText = '';
|
|
182
|
+
const msgId = `gemini-${Date.now()}`;
|
|
183
|
+
|
|
184
|
+
while (true) {
|
|
185
|
+
const { done, value } = await reader.read();
|
|
186
|
+
if (done) break;
|
|
187
|
+
|
|
188
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
189
|
+
const lines = chunk.split('\n');
|
|
190
|
+
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
if (!line.startsWith('data: ')) continue;
|
|
193
|
+
const jsonStr = line.slice(6).trim();
|
|
194
|
+
if (!jsonStr || jsonStr === '[DONE]') continue;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const data = JSON.parse(jsonStr);
|
|
198
|
+
|
|
199
|
+
// ── Chunk validation (from Gemini CLI) ──
|
|
200
|
+
const candidate = data?.candidates?.[0];
|
|
201
|
+
if (!candidate) continue; // Skip empty candidates
|
|
202
|
+
|
|
203
|
+
const text = candidate?.content?.parts?.[0]?.text;
|
|
204
|
+
if (text) {
|
|
205
|
+
fullText += text;
|
|
206
|
+
|
|
207
|
+
// Upsert the streaming message
|
|
208
|
+
usePlaygroundStore.getState().upsertChatMessage(msgId, {
|
|
209
|
+
id: msgId,
|
|
210
|
+
type: 'text',
|
|
211
|
+
sender: 'agent',
|
|
212
|
+
text: fullText,
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
executionSteps: [...executionSteps],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check finishReason (Gemini CLI validates this)
|
|
219
|
+
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
|
|
220
|
+
executionSteps.push({
|
|
221
|
+
type: 'text_complete',
|
|
222
|
+
label: `Finished: ${candidate.finishReason}`,
|
|
223
|
+
startTime: Date.now(),
|
|
224
|
+
endTime: Date.now(),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Skip invalid JSON lines
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Content retry: if stream ended with empty text, retry once ──
|
|
234
|
+
if (!fullText.trim() && !controller.signal.aborted) {
|
|
235
|
+
executionSteps.push({
|
|
236
|
+
type: 'retry' as any,
|
|
237
|
+
label: 'Content retry (empty response)',
|
|
238
|
+
startTime: Date.now(),
|
|
239
|
+
});
|
|
240
|
+
addChatMessage({
|
|
241
|
+
type: 'alert', sender: 'system',
|
|
242
|
+
text: '🔄 Respuesta vacía, reintentando...',
|
|
243
|
+
});
|
|
244
|
+
// Recursive single retry
|
|
245
|
+
setIsStreaming(false);
|
|
246
|
+
abortRef.current = null;
|
|
247
|
+
return sendMessage(prompt);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Finalize execution steps
|
|
251
|
+
executionSteps[executionSteps.length - 1].endTime ??= Date.now();
|
|
252
|
+
executionSteps.push({ type: 'text_complete', label: 'Complete', startTime: Date.now(), endTime: Date.now() });
|
|
253
|
+
|
|
254
|
+
// Final update
|
|
255
|
+
usePlaygroundStore.getState().upsertChatMessage(msgId, {
|
|
256
|
+
id: msgId,
|
|
257
|
+
type: 'text',
|
|
258
|
+
sender: 'agent',
|
|
259
|
+
text: fullText,
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
executionSteps,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
} catch (err: any) {
|
|
265
|
+
if (err.name === 'AbortError') {
|
|
266
|
+
addChatMessage({ type: 'alert', sender: 'system', text: '⏹️ Generación cancelada por el usuario.' });
|
|
267
|
+
} else {
|
|
268
|
+
setError(err.message);
|
|
269
|
+
addChatMessage({ type: 'error', sender: 'system', text: `❌ Error: ${err.message}` });
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
setIsStreaming(false);
|
|
273
|
+
abortRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
}, [model, systemInstruction, maxRetries, initialRetryDelay, getApiKey, addChatMessage]);
|
|
276
|
+
|
|
277
|
+
const abort = useCallback(() => {
|
|
278
|
+
abortRef.current?.abort();
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
return { sendMessage, abort, isStreaming, error };
|
|
282
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useIntentLens — Real-time intent detection for chat input.
|
|
3
|
+
*
|
|
4
|
+
* As the user types, this hook detects intent-triggering words/phrases
|
|
5
|
+
* and returns colored tokens for visual feedback in the input field.
|
|
6
|
+
*
|
|
7
|
+
* Only uses regex (Level ①, 0ms). The LLM (Level ②) is called only on submit.
|
|
8
|
+
*/
|
|
9
|
+
import { useMemo } from 'react';
|
|
10
|
+
|
|
11
|
+
// ═══════════════════════════════════════════
|
|
12
|
+
// Intent Types
|
|
13
|
+
// ═══════════════════════════════════════════
|
|
14
|
+
|
|
15
|
+
export type IntentType =
|
|
16
|
+
| 'CREATE_GRAPH'
|
|
17
|
+
| 'EDIT_GRAPH'
|
|
18
|
+
| 'UI_ACTION'
|
|
19
|
+
| 'MCP_ACTION'
|
|
20
|
+
| 'NAVIGATE'
|
|
21
|
+
| 'NONE';
|
|
22
|
+
|
|
23
|
+
export interface IntentToken {
|
|
24
|
+
text: string;
|
|
25
|
+
intent: IntentType;
|
|
26
|
+
color: string | null;
|
|
27
|
+
isMatch: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DetectedIntent {
|
|
31
|
+
type: IntentType;
|
|
32
|
+
label: string;
|
|
33
|
+
color: string;
|
|
34
|
+
icon: string;
|
|
35
|
+
matchedPhrases: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ═══════════════════════════════════════════
|
|
39
|
+
// Intent Patterns (mirrors usePlaygroundCommander regex)
|
|
40
|
+
// ═══════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
export const INTENT_PATTERNS: Array<{
|
|
43
|
+
type: IntentType;
|
|
44
|
+
pattern: RegExp;
|
|
45
|
+
label: string;
|
|
46
|
+
icon: string;
|
|
47
|
+
color: string;
|
|
48
|
+
examples: string[];
|
|
49
|
+
}> = [
|
|
50
|
+
{
|
|
51
|
+
type: 'EDIT_GRAPH',
|
|
52
|
+
pattern: /\b(agrega|suma|añade|conecta|modifica|cambia|elimina|borra|parchea|actualiza|inserta|mueve)\b/gi,
|
|
53
|
+
label: 'Editar Grafo',
|
|
54
|
+
icon: '✏️',
|
|
55
|
+
color: '#f59e0b',
|
|
56
|
+
examples: [
|
|
57
|
+
'Agrega un nodo de validación después de login',
|
|
58
|
+
'Conecta el nodo de pago con el de confirmación',
|
|
59
|
+
'Elimina el paso de verificación duplicado',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'CREATE_GRAPH',
|
|
64
|
+
pattern: /\b(grafo|flujo|nodos|blueprint|escenario|onboarding|proceso|workflow|pipeline|trigger|subflujo|orquest)\b|\b(crea|diseña|genera|construye)\s+(un|una|el|la|los|las)\b/gi,
|
|
65
|
+
label: 'Crear Grafo',
|
|
66
|
+
icon: '🔗',
|
|
67
|
+
color: '#10b981',
|
|
68
|
+
examples: [
|
|
69
|
+
'Crea un flujo de onboarding para nuevos usuarios',
|
|
70
|
+
'Diseña un pipeline de procesamiento de datos',
|
|
71
|
+
'Genera un blueprint con 5 nodos de decisión',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'UI_ACTION',
|
|
76
|
+
pattern: /\b(shell|tema|theme|sidebar|panel|activity\s*bar|status\s*bar|top\s*bar|notificaci[oó]n|shortcut|atajo|layout)\b|\b(configura)\s+(el|la|los|las)\b/gi,
|
|
77
|
+
label: 'Acción UI',
|
|
78
|
+
icon: '🎨',
|
|
79
|
+
color: '#3b82f6',
|
|
80
|
+
examples: [
|
|
81
|
+
'Abre el panel de sidebar',
|
|
82
|
+
'Configura el tema oscuro',
|
|
83
|
+
'Cambia el layout a pantalla completa',
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'MCP_ACTION',
|
|
88
|
+
pattern: /\b(ejecuta|invoca|llama|herramienta|tool|api|endpoint)\b/gi,
|
|
89
|
+
label: 'Acción MCP',
|
|
90
|
+
icon: '⚡',
|
|
91
|
+
color: '#a855f7',
|
|
92
|
+
examples: [
|
|
93
|
+
'Ejecuta la herramienta de análisis de código',
|
|
94
|
+
'Invoca el endpoint de validación',
|
|
95
|
+
'Llama al API de búsqueda semántica',
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'NAVIGATE',
|
|
100
|
+
pattern: /\b(landing|página\s*web|web\s*page|website|diseño\s*web|sitio|homepage|portfolio|dashboard\s*ui|ui\s*design|mockup|prototipo\s*web)\b/gi,
|
|
101
|
+
label: 'Web Preview',
|
|
102
|
+
icon: '🌐',
|
|
103
|
+
color: '#06b6d4',
|
|
104
|
+
examples: [
|
|
105
|
+
'Crea una landing page para un SaaS de IA',
|
|
106
|
+
'Diseña un portfolio moderno con tema oscuro',
|
|
107
|
+
'Genera un dashboard UI para métricas de ventas',
|
|
108
|
+
'Crea una página de pricing con 3 planes',
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// Dynamic pattern registry for SDK extensibility
|
|
114
|
+
let _extraPatterns: typeof INTENT_PATTERNS = [];
|
|
115
|
+
export function registerIntentPattern(p: typeof INTENT_PATTERNS[0]) { _extraPatterns.push(p); }
|
|
116
|
+
export function getIntentPatterns() { return [...INTENT_PATTERNS, ..._extraPatterns]; }
|
|
117
|
+
|
|
118
|
+
// ═══════════════════════════════════════════
|
|
119
|
+
// Tokenizer
|
|
120
|
+
// ═══════════════════════════════════════════
|
|
121
|
+
|
|
122
|
+
function tokenizeWithIntents(input: string): IntentToken[] {
|
|
123
|
+
if (!input.trim()) return [];
|
|
124
|
+
|
|
125
|
+
// Find all matches across all patterns
|
|
126
|
+
const matches: Array<{ start: number; end: number; intent: IntentType; color: string }> = [];
|
|
127
|
+
|
|
128
|
+
for (const { type, pattern, color } of INTENT_PATTERNS) {
|
|
129
|
+
// Reset regex lastIndex
|
|
130
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
131
|
+
let match;
|
|
132
|
+
while ((match = re.exec(input)) !== null) {
|
|
133
|
+
matches.push({
|
|
134
|
+
start: match.index,
|
|
135
|
+
end: match.index + match[0].length,
|
|
136
|
+
intent: type,
|
|
137
|
+
color,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Sort by position
|
|
143
|
+
matches.sort((a, b) => a.start - b.start);
|
|
144
|
+
|
|
145
|
+
// Build tokens
|
|
146
|
+
const tokens: IntentToken[] = [];
|
|
147
|
+
let cursor = 0;
|
|
148
|
+
|
|
149
|
+
for (const m of matches) {
|
|
150
|
+
// Skip overlapping matches
|
|
151
|
+
if (m.start < cursor) continue;
|
|
152
|
+
|
|
153
|
+
// Non-matching text before this match
|
|
154
|
+
if (m.start > cursor) {
|
|
155
|
+
tokens.push({
|
|
156
|
+
text: input.slice(cursor, m.start),
|
|
157
|
+
intent: 'NONE',
|
|
158
|
+
color: null,
|
|
159
|
+
isMatch: false,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The matched token
|
|
164
|
+
tokens.push({
|
|
165
|
+
text: input.slice(m.start, m.end),
|
|
166
|
+
intent: m.intent,
|
|
167
|
+
color: m.color,
|
|
168
|
+
isMatch: true,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
cursor = m.end;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Remaining text
|
|
175
|
+
if (cursor < input.length) {
|
|
176
|
+
tokens.push({
|
|
177
|
+
text: input.slice(cursor),
|
|
178
|
+
intent: 'NONE',
|
|
179
|
+
color: null,
|
|
180
|
+
isMatch: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return tokens;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ═══════════════════════════════════════════
|
|
188
|
+
// Hook
|
|
189
|
+
// ═══════════════════════════════════════════
|
|
190
|
+
|
|
191
|
+
export function useIntentLens(input: string) {
|
|
192
|
+
const result = useMemo(() => {
|
|
193
|
+
const tokens = tokenizeWithIntents(input);
|
|
194
|
+
|
|
195
|
+
// Deduplicate detected intents
|
|
196
|
+
const intentMap = new Map<IntentType, DetectedIntent>();
|
|
197
|
+
for (const token of tokens) {
|
|
198
|
+
if (token.isMatch && token.intent !== 'NONE') {
|
|
199
|
+
const existing = intentMap.get(token.intent);
|
|
200
|
+
if (existing) {
|
|
201
|
+
existing.matchedPhrases.push(token.text);
|
|
202
|
+
} else {
|
|
203
|
+
const pattern = INTENT_PATTERNS.find((p) => p.type === token.intent)!;
|
|
204
|
+
intentMap.set(token.intent, {
|
|
205
|
+
type: token.intent,
|
|
206
|
+
label: pattern.label,
|
|
207
|
+
color: pattern.color,
|
|
208
|
+
icon: pattern.icon,
|
|
209
|
+
matchedPhrases: [token.text],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
tokens,
|
|
217
|
+
detectedIntents: Array.from(intentMap.values()),
|
|
218
|
+
hasIntents: intentMap.size > 0,
|
|
219
|
+
primaryIntent: intentMap.size > 0 ? Array.from(intentMap.values())[0] : null,
|
|
220
|
+
};
|
|
221
|
+
}, [input]);
|
|
222
|
+
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useShellStore } from '../store/engine';
|
|
3
|
+
import { useCommandRegistry, useShortcutManager } from '@decido/commands';
|
|
4
|
+
|
|
5
|
+
export const useKeyboardShortcuts = () => {
|
|
6
|
+
const registerCommand = useCommandRegistry((state) => state.registerCommand);
|
|
7
|
+
const registerDefaultShortcut = useShortcutManager((state) => state.registerDefaultShortcut);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
// Register Core Commands
|
|
11
|
+
registerCommand({
|
|
12
|
+
id: 'shell.toggleCommandPalette',
|
|
13
|
+
title: 'Toggle Command Palette',
|
|
14
|
+
category: 'Shell',
|
|
15
|
+
handler: () => {
|
|
16
|
+
const state = useShellStore.getState();
|
|
17
|
+
state.setCommandPaletteOpen(!state.isCommandPaletteOpen);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
registerDefaultShortcut('shell.toggleCommandPalette', '$mod+k');
|
|
21
|
+
|
|
22
|
+
registerCommand({
|
|
23
|
+
id: 'shell.openShortcutSettings',
|
|
24
|
+
title: 'Configurar Atajos',
|
|
25
|
+
category: 'Shell',
|
|
26
|
+
handler: () => {
|
|
27
|
+
const state = useShellStore.getState();
|
|
28
|
+
state.setShortcutSettingsOpen(!state.isShortcutSettingsOpen);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
registerDefaultShortcut('shell.openShortcutSettings', '$mod+Shift+K');
|
|
32
|
+
|
|
33
|
+
registerCommand({
|
|
34
|
+
id: 'shell.toggleForgeTerminal',
|
|
35
|
+
title: 'Toggle Forge Terminal',
|
|
36
|
+
category: 'Shell',
|
|
37
|
+
handler: () => {
|
|
38
|
+
const state = useShellStore.getState();
|
|
39
|
+
state.setForgeTerminalOpen(!state.isForgeTerminalOpen);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
registerDefaultShortcut('shell.toggleForgeTerminal', '$mod+j');
|
|
43
|
+
|
|
44
|
+
registerCommand({
|
|
45
|
+
id: 'shell.toggleGodMode',
|
|
46
|
+
title: 'Toggle God Mode',
|
|
47
|
+
category: 'Shell',
|
|
48
|
+
handler: () => {
|
|
49
|
+
const state = useShellStore.getState();
|
|
50
|
+
state.setGodModePanelOpen(!state.isGodModePanelOpen);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
registerDefaultShortcut('shell.toggleGodMode', '$mod+Shift+D');
|
|
54
|
+
|
|
55
|
+
registerCommand({
|
|
56
|
+
id: 'shell.closePanels',
|
|
57
|
+
title: 'Close Active Panels',
|
|
58
|
+
category: 'Shell',
|
|
59
|
+
handler: () => {
|
|
60
|
+
const state = useShellStore.getState();
|
|
61
|
+
if (state.isCommandPaletteOpen) state.setCommandPaletteOpen(false);
|
|
62
|
+
if (state.isForgeTerminalOpen) state.setForgeTerminalOpen(false);
|
|
63
|
+
if (state.isGodModePanelOpen) state.setGodModePanelOpen(false);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
registerDefaultShortcut('shell.closePanels', 'Escape');
|
|
67
|
+
|
|
68
|
+
}, [registerCommand, registerDefaultShortcut]);
|
|
69
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { logger } from '@decido/logger';
|
|
3
|
+
import { usePlaygroundStore } from '../store/usePlaygroundStore';
|
|
4
|
+
import { useDebugPanelStore } from '../store/useDebugPanelStore';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* useLoggerBridge — Subscribes to @decido/logger and pipes entries to both:
|
|
8
|
+
* 1. usePlaygroundStore.addLog (legacy terminal)
|
|
9
|
+
* 2. useDebugPanelStore.addLogEntry (structured debug panel)
|
|
10
|
+
*
|
|
11
|
+
* Should be called once, at the top-level component (PDC or AppShell).
|
|
12
|
+
*/
|
|
13
|
+
export function useLoggerBridge() {
|
|
14
|
+
const addLog = usePlaygroundStore((s) => s.addLog);
|
|
15
|
+
const addDebugLogEntry = useDebugPanelStore((s) => s.addLogEntry);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const unsubscribe = logger.subscribe((entry) => {
|
|
19
|
+
const catPrefix = entry.categories.length > 0 ? `[${entry.categories.join(',')}] ` : '';
|
|
20
|
+
addLog(`${catPrefix}${entry.message}`, entry.level);
|
|
21
|
+
addDebugLogEntry({
|
|
22
|
+
timestamp: entry.timestamp,
|
|
23
|
+
level: entry.level,
|
|
24
|
+
categories: entry.categories,
|
|
25
|
+
message: entry.message,
|
|
26
|
+
data: entry.data,
|
|
27
|
+
source: entry.source,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
return () => unsubscribe();
|
|
31
|
+
}, [addLog, addDebugLogEntry]);
|
|
32
|
+
}
|