@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.
Files changed (208) hide show
  1. package/.turbo/turbo-build.log +13 -0
  2. package/package.json +65 -0
  3. package/src/AgentPlayer.tsx +105 -0
  4. package/src/DecidoPlayer.tsx +117 -0
  5. package/src/bridge/BridgeAgent.ts +443 -0
  6. package/src/components/DecidoIcon.tsx +56 -0
  7. package/src/components/JsonTreeEditor.tsx +117 -0
  8. package/src/components/PanelSplitter.tsx +71 -0
  9. package/src/components/PluginErrorBoundary.tsx +69 -0
  10. package/src/components/SafeLiquidUI.tsx +114 -0
  11. package/src/components/TransientLayer.tsx +92 -0
  12. package/src/components/agent/AgentChat.tsx +134 -0
  13. package/src/components/chat-extensions/IntentCatalogPanel.tsx +81 -0
  14. package/src/components/chat-extensions/chatSlashCommands.ts +101 -0
  15. package/src/components/controls/CreatorInputBar.tsx +144 -0
  16. package/src/components/controls/OSToolbar.tsx +90 -0
  17. package/src/components/controls/TimelineTape.tsx +43 -0
  18. package/src/components/debug/ActionTimelineTab.tsx +111 -0
  19. package/src/components/debug/CSSInspectorTab.tsx +436 -0
  20. package/src/components/debug/ExportTab.tsx +192 -0
  21. package/src/components/debug/FlowHealthTab.tsx +86 -0
  22. package/src/components/debug/LogsTab.tsx +110 -0
  23. package/src/components/debug/MorphStackTab.tsx +241 -0
  24. package/src/components/debug/NetworkTab.tsx +173 -0
  25. package/src/components/debug/PerformanceTab.tsx +171 -0
  26. package/src/components/debug/ProfilesTab.tsx +238 -0
  27. package/src/components/debug/ReplayTab.tsx +70 -0
  28. package/src/components/debug/StoresTab.tsx +255 -0
  29. package/src/components/debug/TopologyTab.tsx +59 -0
  30. package/src/components/debug/debugConfig.tsx +66 -0
  31. package/src/components/playground/DebugPanel.tsx +112 -0
  32. package/src/components/playground/HeaderCenterControls.tsx +92 -0
  33. package/src/components/playground/KeyframeListItem.tsx +70 -0
  34. package/src/components/playground/PlaygroundAppSidebar.tsx +171 -0
  35. package/src/components/playground/PlaygroundBottomControls.tsx +132 -0
  36. package/src/components/playground/PlaygroundCanvas.tsx +87 -0
  37. package/src/components/playground/PlaygroundChat.tsx +236 -0
  38. package/src/components/playground/PlaygroundErrorBoundary.tsx +63 -0
  39. package/src/components/playground/PlaygroundFloatingInput.tsx +352 -0
  40. package/src/components/playground/PlaygroundHeader.tsx +222 -0
  41. package/src/components/playground/PlaygroundSidebar.tsx +136 -0
  42. package/src/components/playground/PlaygroundTerminal.tsx +44 -0
  43. package/src/components/playground/SuggestionCards.tsx +29 -0
  44. package/src/components/playground/demos/ClinicaAINode.tsx +221 -0
  45. package/src/components/playground/demos/FinanceAINode.tsx +226 -0
  46. package/src/components/playground/demos/KiaAcademyNode.tsx +250 -0
  47. package/src/components/playground/demos/KiaBotNode.tsx +207 -0
  48. package/src/components/playground/demos/KiaCampaignNode.tsx +191 -0
  49. package/src/components/playground/demos/KiaComplianceNode.tsx +140 -0
  50. package/src/components/playground/demos/KiaCustomerJourneyNode.tsx +220 -0
  51. package/src/components/playground/demos/KiaCyberNode.tsx +203 -0
  52. package/src/components/playground/demos/KiaDashboardNode.tsx +399 -0
  53. package/src/components/playground/demos/KiaEmbudoOverviewNode.tsx +168 -0
  54. package/src/components/playground/demos/KiaExecutiveNode.tsx +169 -0
  55. package/src/components/playground/demos/KiaGamificationNode.tsx +229 -0
  56. package/src/components/playground/demos/KiaIntelligenceHubNode.tsx +165 -0
  57. package/src/components/playground/demos/KiaInventoryNode.tsx +183 -0
  58. package/src/components/playground/demos/KiaLeadScoringNode.tsx +226 -0
  59. package/src/components/playground/demos/KiaLiveSimulationNode.tsx +177 -0
  60. package/src/components/playground/demos/KiaMultiDealerNode.tsx +223 -0
  61. package/src/components/playground/demos/KiaNPSVoiceNode.tsx +214 -0
  62. package/src/components/playground/demos/KiaOmnichannelNode.tsx +162 -0
  63. package/src/components/playground/demos/KiaPBIBudgetNode.tsx +152 -0
  64. package/src/components/playground/demos/KiaPBIConversionNode.tsx +206 -0
  65. package/src/components/playground/demos/KiaPBIFunnelNode.tsx +184 -0
  66. package/src/components/playground/demos/KiaPBIOwnershipNode.tsx +113 -0
  67. package/src/components/playground/demos/KiaPBIPartnerNode.tsx +143 -0
  68. package/src/components/playground/demos/KiaPBIPreciosNode.tsx +120 -0
  69. package/src/components/playground/demos/KiaPBIRuntNode.tsx +205 -0
  70. package/src/components/playground/demos/KiaPartnerScoreNode.tsx +206 -0
  71. package/src/components/playground/demos/KiaPredictiveNode.tsx +226 -0
  72. package/src/components/playground/demos/KiaShowroomNode.tsx +194 -0
  73. package/src/components/playground/demos/KiaStoreNode.tsx +215 -0
  74. package/src/components/playground/demos/KiaSustainabilityNode.tsx +173 -0
  75. package/src/components/playground/demos/KiaUsedVehiclesNode.tsx +163 -0
  76. package/src/components/playground/demos/KiaWorkshopNode.tsx +221 -0
  77. package/src/components/playground/demos/SmartCityNode.tsx +205 -0
  78. package/src/components/playground/demos/kia_campaign_manifest.json +112 -0
  79. package/src/components/playground/input-parts/AIModelSelector.tsx +156 -0
  80. package/src/components/playground/input-parts/InputActions.tsx +80 -0
  81. package/src/components/playground/input-parts/InputToolbar.tsx +245 -0
  82. package/src/components/playground/input-parts/ResourceLibraryPanel.tsx +287 -0
  83. package/src/components/playground/sidebarDsdIO.ts +82 -0
  84. package/src/components/settings/SettingsPanel.tsx +267 -0
  85. package/src/components/shell/AppHeader.tsx +9 -0
  86. package/src/components/shell/AppShell.tsx +139 -0
  87. package/src/components/shell/ArtifactBar.tsx +97 -0
  88. package/src/components/shell/BootScreen.tsx +19 -0
  89. package/src/components/shell/CenterComposite.tsx +87 -0
  90. package/src/components/shell/CodeEditorPanel.tsx +88 -0
  91. package/src/components/shell/GlobalOverlays.tsx +228 -0
  92. package/src/components/shell/LayoutConfigurator.tsx +209 -0
  93. package/src/components/shell/LayoutGrid.tsx +178 -0
  94. package/src/components/shell/MorphShell.tsx +368 -0
  95. package/src/components/shell/PluginViewer.tsx +147 -0
  96. package/src/components/shell/ShellNexusPreview.tsx +458 -0
  97. package/src/components/shell/SlotRenderer.tsx +115 -0
  98. package/src/components/shell/TabBar.tsx +94 -0
  99. package/src/components/shell/TemplateLibrary.tsx +195 -0
  100. package/src/components/shell/layoutConstants.ts +35 -0
  101. package/src/components/shell/morphStageMeta.ts +15 -0
  102. package/src/components/shell/shells/BuiltInShells.tsx +443 -0
  103. package/src/components/shell/shells/DatawayChatShell.tsx +42 -0
  104. package/src/components/shell/shells/TokenPreview.tsx +339 -0
  105. package/src/components/shell/shells/bootShells.ts +31 -0
  106. package/src/components/shells/CreatorShell.tsx +37 -0
  107. package/src/components/shells/DecidoShell.tsx +447 -0
  108. package/src/components/shells/ExperimentalChatShell.tsx +245 -0
  109. package/src/components/shells/UserCanvas.tsx +44 -0
  110. package/src/components/studio/BlueprintManagerPanel.tsx +137 -0
  111. package/src/components/studio/DependencyTreePanel.tsx +192 -0
  112. package/src/components/studio/NodePalette.tsx +92 -0
  113. package/src/components/studio/NodePropertiesPanel.tsx +81 -0
  114. package/src/components/studio/ReactFlowEditor.tsx +242 -0
  115. package/src/components/studio/TimelineEditor.tsx +122 -0
  116. package/src/components/studio/TimelineKeyframeCard.tsx +99 -0
  117. package/src/components/studio/VariablePanel.tsx +181 -0
  118. package/src/components/studio/blueprint/BlueprintCard.tsx +82 -0
  119. package/src/components/studio/editor/CanvasContextMenu.tsx +107 -0
  120. package/src/components/studio/editor/EditorToolbar.tsx +80 -0
  121. package/src/components/studio/editor/StageContentRenderer.tsx +134 -0
  122. package/src/components/studio/editor/TrackPropertyEditors.tsx +133 -0
  123. package/src/components/studio/editor/TreeNodeItem.tsx +91 -0
  124. package/src/components/studio/editor/edgeStyles.ts +43 -0
  125. package/src/components/studio/editor/editorKeyHandler.ts +95 -0
  126. package/src/components/studio/editor/nodeTypeRegistry.ts +137 -0
  127. package/src/components/studio/editor/paletteCatalog.tsx +84 -0
  128. package/src/components/studio/nodes/shell/InteractionNodes.tsx +82 -0
  129. package/src/components/studio/nodes/shell/LayoutControlNodes.tsx +69 -0
  130. package/src/components/studio/nodes/shell/RegisterActionNode.tsx +20 -0
  131. package/src/components/studio/nodes/shell/RegisterButtonNode.tsx +22 -0
  132. package/src/components/studio/nodes/shell/RegisterPanelNode.tsx +19 -0
  133. package/src/components/studio/nodes/shell/RegisterSidebarNode.tsx +19 -0
  134. package/src/components/studio/nodes/shell/RegisterStatusBarNode.tsx +22 -0
  135. package/src/components/studio/nodes/shell/RegisterTabNode.tsx +21 -0
  136. package/src/components/studio/nodes/shell/RegisterTopBarNode.tsx +22 -0
  137. package/src/components/studio/nodes/shell/ShellConfigNode.tsx +51 -0
  138. package/src/components/studio/nodes/shell/ShellNodeBase.tsx +100 -0
  139. package/src/components/studio/nodes/shell/ThemeNodes.tsx +51 -0
  140. package/src/components/studio/nodes/shell/index.ts +12 -0
  141. package/src/components/widgets/BroadcastWidget.tsx +93 -0
  142. package/src/components/widgets/MarketplaceWidget.tsx +298 -0
  143. package/src/components/widgets/McpToolsWidget.tsx +231 -0
  144. package/src/components/widgets/OpsDashboard.tsx +59 -0
  145. package/src/components/widgets/QuickActionsWidget.tsx +60 -0
  146. package/src/components/widgets/UsageWidget.tsx +112 -0
  147. package/src/components/widgets/WidgetRenderer.tsx +892 -0
  148. package/src/components/widgets/WidgetSlotPanel.tsx +213 -0
  149. package/src/config/IconRegistry.ts +126 -0
  150. package/src/contexts/NetworkProvider.tsx +162 -0
  151. package/src/core/AIDirector.ts +71 -0
  152. package/src/core/EventBus.ts +37 -0
  153. package/src/core/PluginContext.tsx +141 -0
  154. package/src/hooks/listeners/useUIStateListener.ts +59 -0
  155. package/src/hooks/listeners/useWhatsAppListener.ts +110 -0
  156. package/src/hooks/morphBridge.ts +82 -0
  157. package/src/hooks/useAIModelSelector.ts +144 -0
  158. package/src/hooks/useAgentStream.ts +220 -0
  159. package/src/hooks/useAutoUpdater.ts +89 -0
  160. package/src/hooks/useBootSequence.ts +20 -0
  161. package/src/hooks/useExportDSD.ts +53 -0
  162. package/src/hooks/useFullscreen.ts +35 -0
  163. package/src/hooks/useGeminiStream.ts +282 -0
  164. package/src/hooks/useIntentLens.ts +224 -0
  165. package/src/hooks/useKeyboardShortcuts.ts +69 -0
  166. package/src/hooks/useLoggerBridge.ts +32 -0
  167. package/src/hooks/useMcpClient.ts +112 -0
  168. package/src/hooks/useNexusaiDeploy.ts +118 -0
  169. package/src/hooks/usePlaybackEngine.ts +21 -0
  170. package/src/hooks/usePlaygroundCommander.ts +475 -0
  171. package/src/hooks/usePluginEngine.ts +165 -0
  172. package/src/hooks/useScreenRecorder.ts +73 -0
  173. package/src/hooks/useShellKeyboard.ts +40 -0
  174. package/src/hooks/useShellShortcuts.ts +118 -0
  175. package/src/hooks/useSoundEffects.ts +35 -0
  176. package/src/hooks/useStudioConfig.ts +72 -0
  177. package/src/hooks/useSystemBoot.ts +84 -0
  178. package/src/hooks/useSystemTelemetry.ts +62 -0
  179. package/src/index.ts +97 -0
  180. package/src/lib/debugLogger.ts +80 -0
  181. package/src/lib/networkInterceptor.ts +100 -0
  182. package/src/mocks/decido.tsx +41 -0
  183. package/src/plugins/pluginAPI.ts +190 -0
  184. package/src/store/McpStore.ts +69 -0
  185. package/src/store/UpdaterStore.ts +60 -0
  186. package/src/store/engine.ts +392 -0
  187. package/src/store/index.ts +4 -0
  188. package/src/store/layoutPresets.ts +66 -0
  189. package/src/store/playgroundTypes.ts +98 -0
  190. package/src/store/useActionTimelineStore.ts +48 -0
  191. package/src/store/useDebugPanelStore.ts +98 -0
  192. package/src/store/useDebugProfileStore.ts +130 -0
  193. package/src/store/useLayoutStore.ts +205 -0
  194. package/src/store/useMorphInstanceStore.ts +289 -0
  195. package/src/store/useMorphologyStore.ts +103 -0
  196. package/src/store/usePlaygroundStore.ts +236 -0
  197. package/src/store/useShellRegistry.ts +123 -0
  198. package/src/store/useSuggestionsStore.ts +57 -0
  199. package/src/store/useThemeStore.ts +399 -0
  200. package/src/store/useUIComponentStore.ts +179 -0
  201. package/src/types/DecidoStoryDefinition.ts +43 -0
  202. package/src/utils/ai/ai-architect.ts +92 -0
  203. package/src/utils/ai/ai-code.ts +187 -0
  204. package/src/utils/ai/ai-core.ts +50 -0
  205. package/src/utils/ai/ai-media.ts +292 -0
  206. package/src/utils/layoutGraph.ts +67 -0
  207. package/tsconfig.json +17 -0
  208. 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
+ }