nitrostack 1.0.72 → 1.0.73

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 (240) hide show
  1. package/dist/auth/api-key.js.map +1 -1
  2. package/dist/auth/client.js.map +1 -1
  3. package/dist/auth/index.d.ts +2 -1
  4. package/dist/auth/index.d.ts.map +1 -1
  5. package/dist/auth/index.js +3 -0
  6. package/dist/auth/index.js.map +1 -1
  7. package/dist/auth/middleware.d.ts +1 -1
  8. package/dist/auth/middleware.d.ts.map +1 -1
  9. package/dist/auth/middleware.js.map +1 -1
  10. package/dist/auth/secure-secret.d.ts +136 -0
  11. package/dist/auth/secure-secret.d.ts.map +1 -0
  12. package/dist/auth/secure-secret.js +182 -0
  13. package/dist/auth/secure-secret.js.map +1 -0
  14. package/dist/auth/server-metadata.d.ts.map +1 -1
  15. package/dist/auth/server-metadata.js.map +1 -1
  16. package/dist/auth/simple-jwt.d.ts +100 -14
  17. package/dist/auth/simple-jwt.d.ts.map +1 -1
  18. package/dist/auth/simple-jwt.js +19 -9
  19. package/dist/auth/simple-jwt.js.map +1 -1
  20. package/dist/auth/token-store.js +1 -1
  21. package/dist/auth/token-store.js.map +1 -1
  22. package/dist/auth/token-validation.js +1 -1
  23. package/dist/auth/token-validation.js.map +1 -1
  24. package/dist/cli/commands/build.js +1 -1
  25. package/dist/cli/commands/build.js.map +1 -1
  26. package/dist/cli/commands/generate-types.js +12 -12
  27. package/dist/cli/commands/generate-types.js.map +1 -1
  28. package/dist/cli/commands/generate.d.ts +8 -1
  29. package/dist/cli/commands/generate.d.ts.map +1 -1
  30. package/dist/cli/commands/generate.js +13 -12
  31. package/dist/cli/commands/generate.js.map +1 -1
  32. package/dist/cli/commands/init.js +1 -1
  33. package/dist/cli/commands/init.js.map +1 -1
  34. package/dist/cli/commands/upgrade.d.ts +10 -0
  35. package/dist/cli/commands/upgrade.d.ts.map +1 -0
  36. package/dist/cli/commands/upgrade.js +221 -0
  37. package/dist/cli/commands/upgrade.js.map +1 -0
  38. package/dist/cli/index.js +7 -0
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/core/app-decorator.d.ts +4 -3
  41. package/dist/core/app-decorator.d.ts.map +1 -1
  42. package/dist/core/app-decorator.js +67 -28
  43. package/dist/core/app-decorator.js.map +1 -1
  44. package/dist/core/builders.d.ts +19 -7
  45. package/dist/core/builders.d.ts.map +1 -1
  46. package/dist/core/builders.js +15 -8
  47. package/dist/core/builders.js.map +1 -1
  48. package/dist/core/component.d.ts +8 -8
  49. package/dist/core/component.d.ts.map +1 -1
  50. package/dist/core/component.js +3 -2
  51. package/dist/core/component.js.map +1 -1
  52. package/dist/core/config-module.d.ts +11 -4
  53. package/dist/core/config-module.d.ts.map +1 -1
  54. package/dist/core/config-module.js +1 -1
  55. package/dist/core/config-module.js.map +1 -1
  56. package/dist/core/decorators/cache.decorator.d.ts +9 -9
  57. package/dist/core/decorators/cache.decorator.d.ts.map +1 -1
  58. package/dist/core/decorators/cache.decorator.js +3 -3
  59. package/dist/core/decorators/cache.decorator.js.map +1 -1
  60. package/dist/core/decorators/health-check.decorator.d.ts +3 -3
  61. package/dist/core/decorators/health-check.decorator.d.ts.map +1 -1
  62. package/dist/core/decorators/health-check.decorator.js +2 -2
  63. package/dist/core/decorators/health-check.decorator.js.map +1 -1
  64. package/dist/core/decorators/rate-limit.decorator.d.ts +5 -4
  65. package/dist/core/decorators/rate-limit.decorator.d.ts.map +1 -1
  66. package/dist/core/decorators/rate-limit.decorator.js +3 -3
  67. package/dist/core/decorators/rate-limit.decorator.js.map +1 -1
  68. package/dist/core/decorators.d.ts +47 -29
  69. package/dist/core/decorators.d.ts.map +1 -1
  70. package/dist/core/decorators.js +9 -9
  71. package/dist/core/decorators.js.map +1 -1
  72. package/dist/core/di/container.d.ts +21 -4
  73. package/dist/core/di/container.d.ts.map +1 -1
  74. package/dist/core/di/container.js +11 -7
  75. package/dist/core/di/container.js.map +1 -1
  76. package/dist/core/di/injectable.decorator.d.ts +5 -3
  77. package/dist/core/di/injectable.decorator.d.ts.map +1 -1
  78. package/dist/core/di/injectable.decorator.js.map +1 -1
  79. package/dist/core/errors.d.ts +4 -4
  80. package/dist/core/errors.d.ts.map +1 -1
  81. package/dist/core/errors.js.map +1 -1
  82. package/dist/core/events/event-emitter.d.ts +3 -3
  83. package/dist/core/events/event-emitter.d.ts.map +1 -1
  84. package/dist/core/events/event-emitter.js.map +1 -1
  85. package/dist/core/events/event.decorator.d.ts +5 -5
  86. package/dist/core/events/event.decorator.d.ts.map +1 -1
  87. package/dist/core/events/event.decorator.js +10 -6
  88. package/dist/core/events/event.decorator.js.map +1 -1
  89. package/dist/core/events/log-emitter.d.ts +7 -1
  90. package/dist/core/events/log-emitter.d.ts.map +1 -1
  91. package/dist/core/events/log-emitter.js.map +1 -1
  92. package/dist/core/filters/exception-filter.decorator.d.ts +5 -5
  93. package/dist/core/filters/exception-filter.decorator.d.ts.map +1 -1
  94. package/dist/core/filters/exception-filter.decorator.js +3 -3
  95. package/dist/core/filters/exception-filter.decorator.js.map +1 -1
  96. package/dist/core/filters/exception-filter.interface.d.ts +14 -5
  97. package/dist/core/filters/exception-filter.interface.d.ts.map +1 -1
  98. package/dist/core/guards/apikey.guard.d.ts +1 -1
  99. package/dist/core/guards/apikey.guard.d.ts.map +1 -1
  100. package/dist/core/guards/guard.interface.d.ts +1 -1
  101. package/dist/core/guards/guard.interface.d.ts.map +1 -1
  102. package/dist/core/guards/jwt.guard.d.ts +1 -1
  103. package/dist/core/guards/jwt.guard.d.ts.map +1 -1
  104. package/dist/core/guards/oauth.guard.d.ts +1 -1
  105. package/dist/core/guards/oauth.guard.d.ts.map +1 -1
  106. package/dist/core/guards/use-guards.decorator.d.ts +3 -3
  107. package/dist/core/guards/use-guards.decorator.d.ts.map +1 -1
  108. package/dist/core/guards/use-guards.decorator.js +1 -1
  109. package/dist/core/guards/use-guards.decorator.js.map +1 -1
  110. package/dist/core/index.d.ts +2 -2
  111. package/dist/core/index.d.ts.map +1 -1
  112. package/dist/core/index.js.map +1 -1
  113. package/dist/core/interceptors/interceptor.decorator.d.ts +4 -4
  114. package/dist/core/interceptors/interceptor.decorator.d.ts.map +1 -1
  115. package/dist/core/interceptors/interceptor.decorator.js +2 -2
  116. package/dist/core/interceptors/interceptor.decorator.js.map +1 -1
  117. package/dist/core/interceptors/interceptor.interface.d.ts +3 -3
  118. package/dist/core/interceptors/interceptor.interface.d.ts.map +1 -1
  119. package/dist/core/logger.d.ts.map +1 -1
  120. package/dist/core/logger.js.map +1 -1
  121. package/dist/core/middleware/middleware.decorator.d.ts +4 -4
  122. package/dist/core/middleware/middleware.decorator.d.ts.map +1 -1
  123. package/dist/core/middleware/middleware.decorator.js +2 -2
  124. package/dist/core/middleware/middleware.decorator.js.map +1 -1
  125. package/dist/core/middleware/middleware.interface.d.ts +3 -3
  126. package/dist/core/middleware/middleware.interface.d.ts.map +1 -1
  127. package/dist/core/module.d.ts +33 -14
  128. package/dist/core/module.d.ts.map +1 -1
  129. package/dist/core/module.js +11 -6
  130. package/dist/core/module.js.map +1 -1
  131. package/dist/core/oauth-module.d.ts +9 -3
  132. package/dist/core/oauth-module.d.ts.map +1 -1
  133. package/dist/core/oauth-module.js +4 -3
  134. package/dist/core/oauth-module.js.map +1 -1
  135. package/dist/core/pipes/pipe.decorator.d.ts +14 -5
  136. package/dist/core/pipes/pipe.decorator.d.ts.map +1 -1
  137. package/dist/core/pipes/pipe.decorator.js +2 -2
  138. package/dist/core/pipes/pipe.decorator.js.map +1 -1
  139. package/dist/core/pipes/pipe.interface.d.ts +9 -4
  140. package/dist/core/pipes/pipe.interface.d.ts.map +1 -1
  141. package/dist/core/prompt.d.ts +13 -4
  142. package/dist/core/prompt.d.ts.map +1 -1
  143. package/dist/core/prompt.js +2 -2
  144. package/dist/core/prompt.js.map +1 -1
  145. package/dist/core/resource.d.ts +7 -2
  146. package/dist/core/resource.d.ts.map +1 -1
  147. package/dist/core/resource.js +2 -2
  148. package/dist/core/resource.js.map +1 -1
  149. package/dist/core/server.d.ts +49 -3
  150. package/dist/core/server.d.ts.map +1 -1
  151. package/dist/core/server.js +61 -34
  152. package/dist/core/server.js.map +1 -1
  153. package/dist/core/tool.d.ts +44 -16
  154. package/dist/core/tool.d.ts.map +1 -1
  155. package/dist/core/tool.js +19 -6
  156. package/dist/core/tool.js.map +1 -1
  157. package/dist/core/transports/discovery-http-server.d.ts +7 -1
  158. package/dist/core/transports/discovery-http-server.d.ts.map +1 -1
  159. package/dist/core/transports/discovery-http-server.js.map +1 -1
  160. package/dist/core/transports/http-server.d.ts +2 -2
  161. package/dist/core/transports/http-server.d.ts.map +1 -1
  162. package/dist/core/transports/http-server.js +1 -1
  163. package/dist/core/transports/http-server.js.map +1 -1
  164. package/dist/core/transports/streamable-http.d.ts +4 -4
  165. package/dist/core/transports/streamable-http.d.ts.map +1 -1
  166. package/dist/core/transports/streamable-http.js +1 -1
  167. package/dist/core/transports/streamable-http.js.map +1 -1
  168. package/dist/core/types.d.ts +87 -15
  169. package/dist/core/types.d.ts.map +1 -1
  170. package/dist/core/widgets/widget-registry.d.ts +2 -2
  171. package/dist/core/widgets/widget-registry.d.ts.map +1 -1
  172. package/dist/core/widgets/widget-registry.js +1 -1
  173. package/dist/core/widgets/widget-registry.js.map +1 -1
  174. package/dist/testing/index.d.ts +44 -17
  175. package/dist/testing/index.d.ts.map +1 -1
  176. package/dist/testing/index.js +5 -8
  177. package/dist/testing/index.js.map +1 -1
  178. package/dist/ui-next/index.d.ts +1 -1
  179. package/dist/ui-next/index.d.ts.map +1 -1
  180. package/dist/ui-next/index.js.map +1 -1
  181. package/dist/widgets/hooks/useWidgetSDK.d.ts +5 -5
  182. package/dist/widgets/runtime/WidgetLayout.js.map +1 -1
  183. package/dist/widgets/sdk.d.ts +5 -5
  184. package/dist/widgets/sdk.d.ts.map +1 -1
  185. package/dist/widgets/sdk.js.map +1 -1
  186. package/package.json +1 -1
  187. package/src/studio/app/api/auth/fetch-metadata/route.ts +3 -2
  188. package/src/studio/app/api/auth/register-client/route.ts +3 -2
  189. package/src/studio/app/api/chat/route.ts +31 -17
  190. package/src/studio/app/api/health/checks/route.ts +5 -4
  191. package/src/studio/app/api/init/route.ts +3 -2
  192. package/src/studio/app/api/ping/route.ts +3 -2
  193. package/src/studio/app/api/prompts/[name]/route.ts +4 -3
  194. package/src/studio/app/api/prompts/route.ts +3 -2
  195. package/src/studio/app/api/resources/[...uri]/route.ts +3 -2
  196. package/src/studio/app/api/resources/route.ts +3 -2
  197. package/src/studio/app/api/roots/route.ts +3 -2
  198. package/src/studio/app/api/sampling/route.ts +3 -2
  199. package/src/studio/app/api/tools/[name]/call/route.ts +3 -2
  200. package/src/studio/app/api/tools/route.ts +4 -3
  201. package/src/studio/app/api/widget-examples/route.ts +5 -4
  202. package/src/studio/app/auth/callback/page.tsx +3 -2
  203. package/src/studio/app/chat/page.tsx +481 -105
  204. package/src/studio/app/health/page.tsx +1 -1
  205. package/src/studio/app/logs/page.tsx +2 -2
  206. package/src/studio/app/page.tsx +5 -5
  207. package/src/studio/app/prompts/page.tsx +2 -2
  208. package/src/studio/app/settings/page.tsx +3 -2
  209. package/src/studio/app/tools/page.tsx +3 -3
  210. package/src/studio/components/LogMessage.tsx +1 -1
  211. package/src/studio/components/MarkdownRenderer.tsx +245 -348
  212. package/src/studio/components/Sidebar.tsx +18 -3
  213. package/src/studio/components/VoiceOrbOverlay.tsx +12 -6
  214. package/src/studio/components/WidgetErrorBoundary.tsx +48 -0
  215. package/src/studio/components/WidgetRenderer.tsx +168 -215
  216. package/src/studio/components/ops/OpsCanvas.tsx +748 -0
  217. package/src/studio/components/ops/OpsNodeDetailPanel.tsx +150 -0
  218. package/src/studio/components/ops/OpsSummaryBar.tsx +90 -0
  219. package/src/studio/components/ops/index.ts +5 -0
  220. package/src/studio/components/ops/nodes/BaseNode.tsx +65 -0
  221. package/src/studio/components/ops/nodes/LLMCallNode.tsx +34 -0
  222. package/src/studio/components/ops/nodes/LLMResponseNode.tsx +33 -0
  223. package/src/studio/components/ops/nodes/ToolCallNode.tsx +30 -0
  224. package/src/studio/components/ops/nodes/ToolResultNode.tsx +43 -0
  225. package/src/studio/components/ops/nodes/UserPromptNode.tsx +34 -0
  226. package/src/studio/components/ops/nodes/WidgetRenderNode.tsx +23 -0
  227. package/src/studio/components/ops/nodes/index.ts +8 -0
  228. package/src/studio/components/tools/ToolsCanvas.tsx +2 -2
  229. package/src/studio/lib/api.ts +61 -42
  230. package/src/studio/lib/http-client-transport.ts +2 -2
  231. package/src/studio/lib/llm-service.ts +126 -47
  232. package/src/studio/lib/mcp-client.ts +9 -6
  233. package/src/studio/lib/ops-store.ts +427 -0
  234. package/src/studio/lib/ops-tracker.ts +416 -0
  235. package/src/studio/lib/ops-types.ts +164 -0
  236. package/src/studio/lib/store.ts +8 -11
  237. package/src/studio/lib/types.ts +228 -38
  238. package/src/studio/lib/widget-loader.ts +2 -2
  239. package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +19 -22
  240. package/dist/cli/build-widgets.mjs +0 -165
@@ -0,0 +1,748 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import ReactFlow, {
6
+ Background,
7
+ Controls,
8
+ useNodesState,
9
+ useEdgesState,
10
+ Panel,
11
+ BackgroundVariant,
12
+ } from 'reactflow';
13
+ import type { Node, Edge, NodeTypes } from 'reactflow';
14
+ import 'reactflow/dist/style.css';
15
+
16
+ import { useOpsStore } from '@/lib/ops-store';
17
+ import type { OpsNode, OpsEdge, OpsNodeType, OpsTurn } from '@/lib/ops-types';
18
+ import { OPS_NODE_HEX_COLORS, OPS_NODE_LABELS } from '@/lib/ops-types';
19
+ import {
20
+ UserPromptNode,
21
+ LLMCallNode,
22
+ ToolCallNode,
23
+ ToolResultNode,
24
+ WidgetRenderNode,
25
+ LLMResponseNode,
26
+ } from './nodes';
27
+ import { OpsSummaryBar } from './OpsSummaryBar';
28
+ import { OpsNodeDetailPanel } from './OpsNodeDetailPanel';
29
+ import {
30
+ ArrowsPointingOutIcon,
31
+ XMarkIcon,
32
+ PlayIcon,
33
+ PauseIcon,
34
+ ArrowPathIcon,
35
+ Squares2X2Icon,
36
+ BoltIcon,
37
+ } from '@heroicons/react/24/outline';
38
+
39
+ // Define custom node types
40
+ const nodeTypes: NodeTypes = {
41
+ user_prompt: UserPromptNode,
42
+ llm_call: LLMCallNode,
43
+ tool_call: ToolCallNode,
44
+ tool_result: ToolResultNode,
45
+ widget_render: WidgetRenderNode,
46
+ llm_response: LLMResponseNode,
47
+ };
48
+
49
+ // Convert OpsNode to React Flow Node - HORIZONTAL LAYOUT
50
+ function convertToFlowNode(opsNode: OpsNode, index: number, isAnimating: boolean = false): Node {
51
+ const baseX = 60;
52
+ const baseY = 120;
53
+ const horizontalSpacing = 200;
54
+
55
+ const position = opsNode.position || {
56
+ x: baseX + (index * horizontalSpacing),
57
+ y: baseY,
58
+ };
59
+
60
+ return {
61
+ id: opsNode.id,
62
+ type: opsNode.type,
63
+ position,
64
+ data: {
65
+ ...opsNode.data,
66
+ duration: opsNode.duration,
67
+ isAnimating,
68
+ },
69
+ className: isAnimating ? 'ops-node-animate' : '',
70
+ };
71
+ }
72
+
73
+ // Convert OpsEdge to React Flow Edge
74
+ function convertToFlowEdge(opsEdge: OpsEdge, isAnimating: boolean = false): Edge {
75
+ return {
76
+ id: opsEdge.id,
77
+ source: opsEdge.source,
78
+ target: opsEdge.target,
79
+ animated: isAnimating,
80
+ style: {
81
+ strokeWidth: 1.5,
82
+ stroke: isAnimating ? '#6366f1' : '#3f3f46',
83
+ },
84
+ type: 'smoothstep',
85
+ };
86
+ }
87
+
88
+ // Turn selector - minimal pill style
89
+ function TurnSelector({
90
+ turns,
91
+ selectedTurnId,
92
+ onSelect,
93
+ disabled,
94
+ }: {
95
+ turns: OpsTurn[];
96
+ selectedTurnId: string | null;
97
+ onSelect: (turnId: string | null) => void;
98
+ disabled?: boolean;
99
+ }) {
100
+ if (turns.length === 0) return null;
101
+
102
+ return (
103
+ <div className={`flex items-center gap-1 ${disabled ? 'opacity-40 pointer-events-none' : ''}`}>
104
+ <button
105
+ onClick={() => onSelect(null)}
106
+ disabled={disabled}
107
+ className={`h-7 px-3 rounded-full text-xs font-medium transition-all ${
108
+ selectedTurnId === null
109
+ ? 'bg-white text-zinc-900'
110
+ : 'bg-zinc-800/80 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/80'
111
+ }`}
112
+ >
113
+ All
114
+ </button>
115
+ {turns.map((turn, index) => (
116
+ <button
117
+ key={turn.id}
118
+ onClick={() => onSelect(turn.id)}
119
+ disabled={disabled}
120
+ className={`h-7 min-w-7 px-2.5 rounded-full text-xs font-medium transition-all flex items-center justify-center ${
121
+ selectedTurnId === turn.id
122
+ ? 'bg-white text-zinc-900'
123
+ : 'bg-zinc-800/80 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/80'
124
+ } ${turn.hasError ? 'ring-1 ring-red-500/40' : ''}`}
125
+ title={turn.promptPreview}
126
+ >
127
+ {index + 1}
128
+ </button>
129
+ ))}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ // Minimal playback controls
135
+ function PlaybackControls({
136
+ isPlaying,
137
+ onPlay,
138
+ onPause,
139
+ onReset,
140
+ currentNodeIndex,
141
+ totalNodes,
142
+ playbackSpeed,
143
+ onSpeedChange,
144
+ }: {
145
+ isPlaying: boolean;
146
+ onPlay: () => void;
147
+ onPause: () => void;
148
+ onReset: () => void;
149
+ currentNodeIndex: number;
150
+ totalNodes: number;
151
+ playbackSpeed: number;
152
+ onSpeedChange: (speed: number) => void;
153
+ }) {
154
+ if (totalNodes === 0) return null;
155
+
156
+ const progress = totalNodes > 0 ? (currentNodeIndex / totalNodes) * 100 : 0;
157
+
158
+ return (
159
+ <div className="flex items-center gap-2">
160
+ <button
161
+ onClick={isPlaying ? onPause : onPlay}
162
+ className="h-7 w-7 rounded-full bg-indigo-500 hover:bg-indigo-400 flex items-center justify-center transition-colors"
163
+ title={isPlaying ? "Pause" : "Play"}
164
+ >
165
+ {isPlaying ? (
166
+ <PauseIcon className="w-3.5 h-3.5 text-white" />
167
+ ) : (
168
+ <PlayIcon className="w-3.5 h-3.5 text-white ml-0.5" />
169
+ )}
170
+ </button>
171
+
172
+ <button
173
+ onClick={onReset}
174
+ className="h-7 w-7 rounded-full bg-zinc-800/80 hover:bg-zinc-700/80 flex items-center justify-center transition-colors"
175
+ title="Reset"
176
+ >
177
+ <ArrowPathIcon className="w-3.5 h-3.5 text-zinc-400" />
178
+ </button>
179
+
180
+ {/* Progress */}
181
+ <div className="flex items-center gap-2 ml-1">
182
+ <div className="w-16 h-1 bg-zinc-800 rounded-full overflow-hidden">
183
+ <div
184
+ className="h-full bg-indigo-500 transition-all duration-200"
185
+ style={{ width: `${progress}%` }}
186
+ />
187
+ </div>
188
+ <span className="text-[10px] text-zinc-500 font-mono tabular-nums w-8">
189
+ {currentNodeIndex}/{totalNodes}
190
+ </span>
191
+ </div>
192
+
193
+ {/* Speed */}
194
+ <select
195
+ value={playbackSpeed}
196
+ onChange={(e) => onSpeedChange(Number(e.target.value))}
197
+ className="h-6 bg-zinc-800/80 border-0 rounded text-[10px] text-zinc-400 focus:ring-1 focus:ring-indigo-500/50 cursor-pointer px-1.5"
198
+ >
199
+ <option value={2000}>0.5x</option>
200
+ <option value={1000}>1x</option>
201
+ <option value={500}>2x</option>
202
+ <option value={250}>4x</option>
203
+ </select>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ // Compact legend
209
+ function OpsLegend() {
210
+ const items: { type: OpsNodeType; label: string }[] = [
211
+ { type: 'user_prompt', label: 'Prompt' },
212
+ { type: 'llm_call', label: 'LLM' },
213
+ { type: 'tool_call', label: 'Tool' },
214
+ { type: 'tool_result', label: 'Result' },
215
+ { type: 'widget_render', label: 'Widget' },
216
+ { type: 'llm_response', label: 'Response' },
217
+ ];
218
+
219
+ return (
220
+ <div className="flex items-center gap-3">
221
+ {items.map(({ type, label }) => (
222
+ <div key={type} className="flex items-center gap-1.5">
223
+ <div
224
+ className="w-2 h-2 rounded-sm"
225
+ style={{ backgroundColor: OPS_NODE_HEX_COLORS[type] }}
226
+ />
227
+ <span className="text-[10px] text-zinc-500">{label}</span>
228
+ </div>
229
+ ))}
230
+ </div>
231
+ );
232
+ }
233
+
234
+ // Empty state
235
+ function OpsEmptyState({ className = '' }: { className?: string }) {
236
+ return (
237
+ <div className={`flex items-center justify-center h-full ${className}`}>
238
+ <div className="text-center">
239
+ <div className="w-10 h-10 mx-auto mb-3 rounded-xl bg-zinc-800/50 flex items-center justify-center">
240
+ <BoltIcon className="w-5 h-5 text-zinc-600" />
241
+ </div>
242
+ <p className="text-sm text-zinc-500">No operations yet</p>
243
+ <p className="text-xs text-zinc-600 mt-1">Start a conversation to see the workflow</p>
244
+ </div>
245
+ </div>
246
+ );
247
+ }
248
+
249
+ // Hook for progressive playback
250
+ function useProgressivePlayback(
251
+ allNodes: OpsNode[],
252
+ allEdges: OpsEdge[],
253
+ selectedTurnId: string | null
254
+ ) {
255
+ const [isPlaying, setIsPlaying] = useState(false);
256
+ const [visibleNodeCount, setVisibleNodeCount] = useState<number | null>(null);
257
+ const [playbackSpeed, setPlaybackSpeed] = useState(1000);
258
+ const [lastAnimatedIndex, setLastAnimatedIndex] = useState(-1);
259
+ const playbackTimerRef = useRef<NodeJS.Timeout | null>(null);
260
+
261
+ const filteredNodes = useMemo(() => {
262
+ if (selectedTurnId === null) return allNodes;
263
+ return allNodes.filter(n => n.turnId === selectedTurnId);
264
+ }, [allNodes, selectedTurnId]);
265
+
266
+ const filteredEdges = useMemo(() => {
267
+ if (selectedTurnId === null) return allEdges;
268
+ return allEdges.filter(e => e.turnId === selectedTurnId);
269
+ }, [allEdges, selectedTurnId]);
270
+
271
+ const visibleNodes = useMemo(() => {
272
+ if (visibleNodeCount === null) {
273
+ return filteredNodes.map((node, idx) => convertToFlowNode(node, idx, false));
274
+ }
275
+ return filteredNodes
276
+ .slice(0, visibleNodeCount)
277
+ .map((node, idx) => convertToFlowNode(node, idx, idx === lastAnimatedIndex));
278
+ }, [filteredNodes, visibleNodeCount, lastAnimatedIndex]);
279
+
280
+ const visibleEdges = useMemo(() => {
281
+ if (visibleNodeCount === null) {
282
+ return filteredEdges.map(edge => convertToFlowEdge(edge, false));
283
+ }
284
+
285
+ const visibleNodeIds = new Set(visibleNodes.map(n => n.id));
286
+ return filteredEdges
287
+ .filter(edge => visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target))
288
+ .map((edge) => convertToFlowEdge(edge, true));
289
+ }, [filteredEdges, visibleNodes, visibleNodeCount]);
290
+
291
+ const startPlayback = useCallback(() => {
292
+ setVisibleNodeCount(0);
293
+ setLastAnimatedIndex(-1);
294
+ setIsPlaying(true);
295
+ }, []);
296
+
297
+ const pausePlayback = useCallback(() => {
298
+ setIsPlaying(false);
299
+ if (playbackTimerRef.current) {
300
+ clearTimeout(playbackTimerRef.current);
301
+ playbackTimerRef.current = null;
302
+ }
303
+ }, []);
304
+
305
+ const resetPlayback = useCallback(() => {
306
+ pausePlayback();
307
+ setVisibleNodeCount(null);
308
+ setLastAnimatedIndex(-1);
309
+ }, [pausePlayback]);
310
+
311
+ useEffect(() => {
312
+ if (!isPlaying || visibleNodeCount === null) return;
313
+
314
+ if (visibleNodeCount >= filteredNodes.length) {
315
+ setIsPlaying(false);
316
+ return;
317
+ }
318
+
319
+ playbackTimerRef.current = setTimeout(() => {
320
+ setLastAnimatedIndex(visibleNodeCount);
321
+ setVisibleNodeCount(prev => (prev ?? 0) + 1);
322
+ }, playbackSpeed);
323
+
324
+ return () => {
325
+ if (playbackTimerRef.current) {
326
+ clearTimeout(playbackTimerRef.current);
327
+ }
328
+ };
329
+ }, [isPlaying, visibleNodeCount, filteredNodes.length, playbackSpeed]);
330
+
331
+ useEffect(() => {
332
+ resetPlayback();
333
+ }, [selectedTurnId, resetPlayback]);
334
+
335
+ return {
336
+ isPlaying,
337
+ visibleNodes,
338
+ visibleEdges,
339
+ visibleNodeCount: visibleNodeCount ?? filteredNodes.length,
340
+ totalNodes: filteredNodes.length,
341
+ playbackSpeed,
342
+ startPlayback,
343
+ pausePlayback,
344
+ resetPlayback,
345
+ setPlaybackSpeed,
346
+ isInPlaybackMode: visibleNodeCount !== null,
347
+ };
348
+ }
349
+
350
+ // Main canvas component
351
+ interface OpsFlowCanvasProps {
352
+ nodes: Node[];
353
+ edges: Edge[];
354
+ onNodesChange: ReturnType<typeof useNodesState>[2];
355
+ onEdgesChange: ReturnType<typeof useEdgesState>[2];
356
+ onNodeClick: (event: React.MouseEvent, node: Node) => void;
357
+ onPaneClick: () => void;
358
+ selectedNode: OpsNode | undefined;
359
+ onCloseDetail: () => void;
360
+ turns: OpsTurn[];
361
+ selectedTurnId: string | null;
362
+ onSelectTurn: (turnId: string | null) => void;
363
+ isPlaying: boolean;
364
+ visibleNodeCount: number;
365
+ totalNodes: number;
366
+ playbackSpeed: number;
367
+ onPlay: () => void;
368
+ onPause: () => void;
369
+ onReset: () => void;
370
+ onSpeedChange: (speed: number) => void;
371
+ onExpand?: () => void;
372
+ isCompact?: boolean;
373
+ isExpanded?: boolean;
374
+ }
375
+
376
+ function OpsFlowCanvas({
377
+ nodes,
378
+ edges,
379
+ onNodesChange,
380
+ onEdgesChange,
381
+ onNodeClick,
382
+ onPaneClick,
383
+ selectedNode,
384
+ onCloseDetail,
385
+ turns,
386
+ selectedTurnId,
387
+ onSelectTurn,
388
+ isPlaying,
389
+ visibleNodeCount,
390
+ totalNodes,
391
+ playbackSpeed,
392
+ onPlay,
393
+ onPause,
394
+ onReset,
395
+ onSpeedChange,
396
+ onExpand,
397
+ isCompact = false,
398
+ isExpanded = false,
399
+ }: OpsFlowCanvasProps) {
400
+ // Different zoom settings for split view vs expanded view
401
+ const fitViewOptions = isExpanded
402
+ ? { padding: 0.2, minZoom: 1.2, maxZoom: 1.5 } // Expanded: zoomed in
403
+ : { padding: 0.3, minZoom: 0.5, maxZoom: 0.8 }; // Split: zoomed out to see more
404
+
405
+ const defaultZoom = isExpanded ? 1.5 : 0.6;
406
+
407
+ return (
408
+ <div className="flex-1 relative bg-zinc-950">
409
+ <style jsx global>{`
410
+ .ops-node-animate {
411
+ animation: opsNodeIn 0.3s ease-out forwards;
412
+ }
413
+ @keyframes opsNodeIn {
414
+ from { opacity: 0; transform: scale(0.8) translateY(-8px); }
415
+ to { opacity: 1; transform: scale(1) translateY(0); }
416
+ }
417
+ .react-flow__controls {
418
+ background: #18181b !important;
419
+ border: 1px solid #27272a !important;
420
+ border-radius: 8px !important;
421
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
422
+ }
423
+ .react-flow__controls-button {
424
+ background: transparent !important;
425
+ border: none !important;
426
+ color: #71717a !important;
427
+ }
428
+ .react-flow__controls-button:hover {
429
+ background: #27272a !important;
430
+ color: #a1a1aa !important;
431
+ }
432
+ .react-flow__controls-button svg {
433
+ fill: currentColor !important;
434
+ }
435
+ `}</style>
436
+
437
+ <ReactFlow
438
+ nodes={nodes}
439
+ edges={edges}
440
+ onNodesChange={onNodesChange}
441
+ onEdgesChange={onEdgesChange}
442
+ onNodeClick={onNodeClick}
443
+ onPaneClick={onPaneClick}
444
+ nodeTypes={nodeTypes}
445
+ fitView
446
+ fitViewOptions={fitViewOptions}
447
+ minZoom={0.3}
448
+ maxZoom={2.5}
449
+ defaultViewport={{ x: 0, y: 0, zoom: defaultZoom }}
450
+ proOptions={{ hideAttribution: true }}
451
+ >
452
+ <Background
453
+ variant={BackgroundVariant.Dots}
454
+ gap={20}
455
+ size={1}
456
+ color="#27272a"
457
+ />
458
+ <Controls
459
+ showInteractive={false}
460
+ position="bottom-right"
461
+ />
462
+
463
+ {/* Top controls */}
464
+ <Panel position="top-left" className="!m-3 flex items-center gap-3">
465
+ <TurnSelector
466
+ turns={turns}
467
+ selectedTurnId={selectedTurnId}
468
+ onSelect={onSelectTurn}
469
+ disabled={isPlaying}
470
+ />
471
+ <div className="h-5 w-px bg-zinc-800" />
472
+ <PlaybackControls
473
+ isPlaying={isPlaying}
474
+ onPlay={onPlay}
475
+ onPause={onPause}
476
+ onReset={onReset}
477
+ currentNodeIndex={visibleNodeCount}
478
+ totalNodes={totalNodes}
479
+ playbackSpeed={playbackSpeed}
480
+ onSpeedChange={onSpeedChange}
481
+ />
482
+ </Panel>
483
+
484
+ {/* Bottom left - Legend */}
485
+ <Panel position="bottom-left" className="!m-3">
486
+ <OpsLegend />
487
+ </Panel>
488
+
489
+ {/* Expand button for compact view */}
490
+ {isCompact && onExpand && (
491
+ <Panel position="top-right" className="!m-3">
492
+ <button
493
+ onClick={onExpand}
494
+ className="h-7 w-7 rounded-lg bg-zinc-800/80 hover:bg-zinc-700/80 flex items-center justify-center transition-colors"
495
+ title="Expand"
496
+ >
497
+ <ArrowsPointingOutIcon className="w-3.5 h-3.5 text-zinc-400" />
498
+ </button>
499
+ </Panel>
500
+ )}
501
+ </ReactFlow>
502
+
503
+ {/* Detail Panel */}
504
+ {selectedNode && (
505
+ <OpsNodeDetailPanel node={selectedNode} onClose={onCloseDetail} />
506
+ )}
507
+ </div>
508
+ );
509
+ }
510
+
511
+ // Modal component
512
+ interface OpsModalProps {
513
+ isOpen: boolean;
514
+ onClose: () => void;
515
+ allNodes: OpsNode[];
516
+ allEdges: OpsEdge[];
517
+ selectedNodeId: string | null;
518
+ setSelectedNode: (nodeId: string | null) => void;
519
+ turns: OpsTurn[];
520
+ selectedTurnId: string | null;
521
+ onSelectTurn: (turnId: string | null) => void;
522
+ summary: ReturnType<typeof useOpsStore.getState>['getSummary'] extends () => infer R ? R : never;
523
+ }
524
+
525
+ function OpsModal({
526
+ isOpen,
527
+ onClose,
528
+ allNodes,
529
+ allEdges,
530
+ selectedNodeId,
531
+ setSelectedNode,
532
+ turns,
533
+ selectedTurnId,
534
+ onSelectTurn,
535
+ summary,
536
+ }: OpsModalProps) {
537
+ const [mounted, setMounted] = useState(false);
538
+
539
+ const {
540
+ isPlaying,
541
+ visibleNodes,
542
+ visibleEdges,
543
+ visibleNodeCount,
544
+ totalNodes,
545
+ playbackSpeed,
546
+ startPlayback,
547
+ pausePlayback,
548
+ resetPlayback,
549
+ setPlaybackSpeed,
550
+ } = useProgressivePlayback(allNodes, allEdges, selectedTurnId);
551
+
552
+ const [nodes, setNodes, onNodesChange] = useNodesState(visibleNodes);
553
+ const [edges, setEdges, onEdgesChange] = useEdgesState(visibleEdges);
554
+
555
+ useEffect(() => {
556
+ setNodes(visibleNodes);
557
+ setEdges(visibleEdges);
558
+ }, [visibleNodes, visibleEdges, setNodes, setEdges]);
559
+
560
+ const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
561
+ setSelectedNode(node.id);
562
+ }, [setSelectedNode]);
563
+
564
+ const onPaneClick = useCallback(() => {
565
+ setSelectedNode(null);
566
+ }, [setSelectedNode]);
567
+
568
+ const selectedNode = allNodes.find(n => n.id === selectedNodeId);
569
+
570
+ useEffect(() => {
571
+ setMounted(true);
572
+ return () => setMounted(false);
573
+ }, []);
574
+
575
+ // Handle escape key
576
+ useEffect(() => {
577
+ const handleEsc = (e: KeyboardEvent) => {
578
+ if (e.key === 'Escape') onClose();
579
+ };
580
+ if (isOpen) {
581
+ window.addEventListener('keydown', handleEsc);
582
+ return () => window.removeEventListener('keydown', handleEsc);
583
+ }
584
+ }, [isOpen, onClose]);
585
+
586
+ if (!mounted || !isOpen) return null;
587
+
588
+ const modalContent = (
589
+ <div
590
+ className="fixed inset-0 z-[9999] bg-black/95"
591
+ onClick={onClose}
592
+ >
593
+ <div
594
+ className="absolute inset-4 bg-zinc-900 rounded-2xl border border-zinc-800 shadow-2xl overflow-hidden flex flex-col"
595
+ onClick={(e) => e.stopPropagation()}
596
+ >
597
+ {/* Header */}
598
+ <div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800/80">
599
+ <div className="flex items-center gap-3">
600
+ <div className="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center">
601
+ <Squares2X2Icon className="w-4 h-4 text-indigo-400" />
602
+ </div>
603
+ <div>
604
+ <h2 className="text-sm font-semibold text-zinc-100">Operations</h2>
605
+ <p className="text-xs text-zinc-500">{allNodes.length} steps across {turns.length} turns</p>
606
+ </div>
607
+ </div>
608
+ <button
609
+ onClick={onClose}
610
+ className="w-8 h-8 rounded-lg hover:bg-zinc-800 flex items-center justify-center transition-colors"
611
+ >
612
+ <XMarkIcon className="w-4 h-4 text-zinc-500" />
613
+ </button>
614
+ </div>
615
+
616
+ {/* Canvas */}
617
+ <OpsFlowCanvas
618
+ nodes={nodes}
619
+ edges={edges}
620
+ onNodesChange={onNodesChange}
621
+ onEdgesChange={onEdgesChange}
622
+ onNodeClick={onNodeClick}
623
+ onPaneClick={onPaneClick}
624
+ selectedNode={selectedNode}
625
+ onCloseDetail={() => setSelectedNode(null)}
626
+ turns={turns}
627
+ selectedTurnId={selectedTurnId}
628
+ onSelectTurn={onSelectTurn}
629
+ isPlaying={isPlaying}
630
+ visibleNodeCount={visibleNodeCount}
631
+ totalNodes={totalNodes}
632
+ playbackSpeed={playbackSpeed}
633
+ onPlay={startPlayback}
634
+ onPause={pausePlayback}
635
+ onReset={resetPlayback}
636
+ onSpeedChange={setPlaybackSpeed}
637
+ isExpanded
638
+ />
639
+
640
+ {/* Footer */}
641
+ <OpsSummaryBar summary={summary} />
642
+ </div>
643
+ </div>
644
+ );
645
+
646
+ return createPortal(modalContent, document.body);
647
+ }
648
+
649
+ interface OpsCanvasProps {
650
+ className?: string;
651
+ }
652
+
653
+ export function OpsCanvas({ className = '' }: OpsCanvasProps) {
654
+ const {
655
+ currentSession,
656
+ selectedNodeId,
657
+ setSelectedNode,
658
+ getSummary,
659
+ selectedTurnId,
660
+ setSelectedTurn,
661
+ } = useOpsStore();
662
+
663
+ const [isModalOpen, setIsModalOpen] = useState(false);
664
+
665
+ const allNodes = currentSession?.nodes || [];
666
+ const allEdges = currentSession?.edges || [];
667
+ const turns = currentSession?.turns || [];
668
+
669
+ const {
670
+ isPlaying,
671
+ visibleNodes,
672
+ visibleEdges,
673
+ visibleNodeCount,
674
+ totalNodes,
675
+ playbackSpeed,
676
+ startPlayback,
677
+ pausePlayback,
678
+ resetPlayback,
679
+ setPlaybackSpeed,
680
+ } = useProgressivePlayback(allNodes, allEdges, selectedTurnId);
681
+
682
+ const [nodes, setNodes, onNodesChange] = useNodesState(visibleNodes);
683
+ const [edges, setEdges, onEdgesChange] = useEdgesState(visibleEdges);
684
+
685
+ useEffect(() => {
686
+ setNodes(visibleNodes);
687
+ setEdges(visibleEdges);
688
+ }, [visibleNodes, visibleEdges, setNodes, setEdges]);
689
+
690
+ const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
691
+ setSelectedNode(node.id);
692
+ }, [setSelectedNode]);
693
+
694
+ const onPaneClick = useCallback(() => {
695
+ setSelectedNode(null);
696
+ }, [setSelectedNode]);
697
+
698
+ const summary = getSummary();
699
+ const selectedNode = allNodes.find(n => n.id === selectedNodeId);
700
+
701
+ if (!currentSession || allNodes.length === 0) {
702
+ return <OpsEmptyState className={className} />;
703
+ }
704
+
705
+ return (
706
+ <>
707
+ <div className={`flex flex-col h-full bg-zinc-950 ${className}`}>
708
+ <OpsFlowCanvas
709
+ nodes={nodes}
710
+ edges={edges}
711
+ onNodesChange={onNodesChange}
712
+ onEdgesChange={onEdgesChange}
713
+ onNodeClick={onNodeClick}
714
+ onPaneClick={onPaneClick}
715
+ selectedNode={selectedNode}
716
+ onCloseDetail={() => setSelectedNode(null)}
717
+ turns={turns}
718
+ selectedTurnId={selectedTurnId}
719
+ onSelectTurn={setSelectedTurn}
720
+ isPlaying={isPlaying}
721
+ visibleNodeCount={visibleNodeCount}
722
+ totalNodes={totalNodes}
723
+ playbackSpeed={playbackSpeed}
724
+ onPlay={startPlayback}
725
+ onPause={pausePlayback}
726
+ onReset={resetPlayback}
727
+ onSpeedChange={setPlaybackSpeed}
728
+ onExpand={() => setIsModalOpen(true)}
729
+ isCompact
730
+ />
731
+ <OpsSummaryBar summary={summary} compact />
732
+ </div>
733
+
734
+ <OpsModal
735
+ isOpen={isModalOpen}
736
+ onClose={() => setIsModalOpen(false)}
737
+ allNodes={allNodes}
738
+ allEdges={allEdges}
739
+ selectedNodeId={selectedNodeId}
740
+ setSelectedNode={setSelectedNode}
741
+ turns={turns}
742
+ selectedTurnId={selectedTurnId}
743
+ onSelectTurn={setSelectedTurn}
744
+ summary={summary}
745
+ />
746
+ </>
747
+ );
748
+ }