nitrostack 1.0.71 → 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 (253) 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 +33 -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 +9 -8
  203. package/src/studio/app/chat/page.tsx +1535 -468
  204. package/src/studio/app/chat/page.tsx.backup +1046 -187
  205. package/src/studio/app/globals.css +361 -191
  206. package/src/studio/app/health/page.tsx +73 -77
  207. package/src/studio/app/layout.tsx +9 -11
  208. package/src/studio/app/logs/page.tsx +31 -32
  209. package/src/studio/app/page.tsx +136 -232
  210. package/src/studio/app/prompts/page.tsx +115 -97
  211. package/src/studio/app/resources/page.tsx +115 -124
  212. package/src/studio/app/settings/page.tsx +1083 -127
  213. package/src/studio/app/tools/page.tsx +343 -0
  214. package/src/studio/components/EnlargeModal.tsx +76 -65
  215. package/src/studio/components/LogMessage.tsx +6 -6
  216. package/src/studio/components/MarkdownRenderer.tsx +246 -349
  217. package/src/studio/components/Sidebar.tsx +165 -210
  218. package/src/studio/components/SplashScreen.tsx +109 -0
  219. package/src/studio/components/ToolCard.tsx +50 -41
  220. package/src/studio/components/VoiceOrbOverlay.tsx +475 -0
  221. package/src/studio/components/WidgetErrorBoundary.tsx +48 -0
  222. package/src/studio/components/WidgetRenderer.tsx +169 -211
  223. package/src/studio/components/ops/OpsCanvas.tsx +748 -0
  224. package/src/studio/components/ops/OpsNodeDetailPanel.tsx +150 -0
  225. package/src/studio/components/ops/OpsSummaryBar.tsx +90 -0
  226. package/src/studio/components/ops/index.ts +5 -0
  227. package/src/studio/components/ops/nodes/BaseNode.tsx +65 -0
  228. package/src/studio/components/ops/nodes/LLMCallNode.tsx +34 -0
  229. package/src/studio/components/ops/nodes/LLMResponseNode.tsx +33 -0
  230. package/src/studio/components/ops/nodes/ToolCallNode.tsx +30 -0
  231. package/src/studio/components/ops/nodes/ToolResultNode.tsx +43 -0
  232. package/src/studio/components/ops/nodes/UserPromptNode.tsx +34 -0
  233. package/src/studio/components/ops/nodes/WidgetRenderNode.tsx +23 -0
  234. package/src/studio/components/ops/nodes/index.ts +8 -0
  235. package/src/studio/components/tools/ToolsCanvas.tsx +327 -0
  236. package/src/studio/lib/api.ts +61 -42
  237. package/src/studio/lib/http-client-transport.ts +2 -2
  238. package/src/studio/lib/llm-service.ts +126 -47
  239. package/src/studio/lib/mcp-client.ts +9 -6
  240. package/src/studio/lib/ops-store.ts +427 -0
  241. package/src/studio/lib/ops-tracker.ts +416 -0
  242. package/src/studio/lib/ops-types.ts +164 -0
  243. package/src/studio/lib/store.ts +23 -11
  244. package/src/studio/lib/types.ts +228 -38
  245. package/src/studio/lib/widget-loader.ts +2 -2
  246. package/src/studio/package-lock.json +3303 -0
  247. package/src/studio/package.json +3 -1
  248. package/src/studio/public/NitroStudio Isotype Color.png +0 -0
  249. package/src/studio/tailwind.config.ts +63 -17
  250. package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +19 -22
  251. package/dist/cli/build-widgets.mjs +0 -165
  252. package/src/studio/app/auth/page.tsx +0 -560
  253. package/src/studio/app/ping/page.tsx +0 -209
@@ -4,7 +4,7 @@ import type { Tool } from '@/lib/types';
4
4
  import { useStudioStore } from '@/lib/store';
5
5
  import { WidgetRenderer } from './WidgetRenderer';
6
6
  import { useRouter } from 'next/navigation';
7
- import { Zap, Palette, Maximize2, Play, Sparkles, MessageSquare } from 'lucide-react';
7
+ import { BoltIcon, PaintBrushIcon, ArrowsPointingOutIcon, PlayIcon, SparklesIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/outline';
8
8
 
9
9
  interface ToolCardProps {
10
10
  tool: Tool;
@@ -16,16 +16,16 @@ export function ToolCard({ tool, onExecute }: ToolCardProps) {
16
16
  const router = useRouter();
17
17
 
18
18
  // Check if tool has widget - check multiple sources
19
- const widgetUri =
20
- tool.widget?.route ||
21
- tool.outputTemplate ||
22
- tool._meta?.['ui/template'] ||
19
+ const widgetUri =
20
+ tool.widget?.route ||
21
+ tool.outputTemplate ||
22
+ tool._meta?.['ui/template'] ||
23
23
  tool._meta?.['openai/outputTemplate'];
24
24
  const hasWidget = !!widgetUri && widgetUri.trim().length > 0;
25
-
25
+
26
26
  // Get example data for preview - check both examples and _meta
27
27
  const exampleData = tool.examples?.response || tool._meta?.['tool/examples']?.response;
28
-
28
+
29
29
  // Debug logging for widget detection
30
30
  if (hasWidget) {
31
31
  console.log('ToolCard - Widget detected:', {
@@ -41,16 +41,16 @@ export function ToolCard({ tool, onExecute }: ToolCardProps) {
41
41
 
42
42
  const handleUseInChat = (e: React.MouseEvent) => {
43
43
  e.stopPropagation();
44
-
44
+
45
45
  // Build the tool execution message
46
46
  const toolMessage = `Use the ${tool.name} tool`;
47
-
47
+
48
48
  // Store the message in localStorage
49
49
  if (typeof window !== 'undefined') {
50
50
  window.localStorage.setItem('chatInput', toolMessage);
51
51
  window.localStorage.setItem('suggestedTool', tool.name);
52
52
  }
53
-
53
+
54
54
  router.push('/chat');
55
55
  };
56
56
 
@@ -64,24 +64,29 @@ export function ToolCard({ tool, onExecute }: ToolCardProps) {
64
64
  className="card card-hover p-6 animate-fade-in cursor-pointer"
65
65
  onClick={() => onExecute(tool)}
66
66
  >
67
- {/* Header */}
68
- <div className="flex items-start justify-between mb-4">
69
- <div className="flex items-center gap-3">
70
- <div className={`w-12 h-12 rounded-lg flex items-center justify-center ${hasWidget ? 'bg-purple-500/10' : 'bg-primary/10'}`}>
71
- {hasWidget ? (
72
- <Palette className="w-6 h-6 text-purple-500" />
73
- ) : (
74
- <Zap className="w-6 h-6 text-primary" />
67
+ {/* Clean Minimal Header */}
68
+ <div className="flex items-center gap-3 mb-3">
69
+ {/* Icon - MCP Brand Colors Only */}
70
+ {hasWidget ? (
71
+ <PaintBrushIcon className="h-5 w-5 text-secondary flex-shrink-0" />
72
+ ) : (
73
+ <BoltIcon className="h-5 w-5 text-muted-foreground flex-shrink-0" />
74
+ )}
75
+
76
+ {/* Content */}
77
+ <div className="flex-1 min-w-0">
78
+ <h3 className="font-medium text-[15px] text-foreground leading-tight truncate">
79
+ {tool.name}
80
+ </h3>
81
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground leading-none mt-1">
82
+ <span>Tool</span>
83
+ {hasWidget && (
84
+ <>
85
+ <span className="text-muted-foreground/40">•</span>
86
+ <span className="text-secondary">Widget</span>
87
+ </>
75
88
  )}
76
89
  </div>
77
- <div>
78
- <h3 className="font-semibold text-lg text-foreground">
79
- {tool.name}
80
- </h3>
81
- <span className={`badge ${hasWidget ? 'badge-secondary' : 'badge-primary'} text-xs mt-1`}>
82
- {hasWidget ? 'tool + widget' : 'tool'}
83
- </span>
84
- </div>
85
90
  </div>
86
91
  </div>
87
92
 
@@ -94,7 +99,7 @@ export function ToolCard({ tool, onExecute }: ToolCardProps) {
94
99
  {hasWidget && widgetUri && exampleData && (
95
100
  <div className="relative mb-4 rounded-lg overflow-hidden border border-border bg-muted/20">
96
101
  <div className="absolute top-2 left-2 z-10 flex items-center gap-1 bg-primary/90 backdrop-blur-sm text-black px-2 py-1 rounded-md text-xs font-semibold shadow-lg">
97
- <Sparkles className="w-3 h-3" />
102
+ <SparklesIcon className="h-3 w-3" />
98
103
  Widget Preview
99
104
  </div>
100
105
  <div className="h-64 relative">
@@ -107,31 +112,35 @@ export function ToolCard({ tool, onExecute }: ToolCardProps) {
107
112
  </div>
108
113
  )}
109
114
 
110
- {/* Action Buttons */}
111
- <div className="flex flex-wrap items-center gap-2" onClick={(e) => e.stopPropagation()}>
115
+ {/* Premium Action Toolbar */}
116
+ <div className="flex items-center gap-1.5 pt-4 border-t border-border/50" onClick={(e) => e.stopPropagation()}>
117
+ {/* Secondary: Enlarge Widget (Icon-only) */}
112
118
  {hasWidget && (
113
119
  <button
114
120
  onClick={handleEnlarge}
115
- className="btn btn-secondary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
121
+ className="group relative h-8 w-8 rounded hover:bg-muted transition-all duration-200 flex items-center justify-center"
122
+ title="View fullscreen"
116
123
  >
117
- <Maximize2 className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
118
- <span className="truncate">Enlarge</span>
124
+ <ArrowsPointingOutIcon className="h-4 w-4 text-muted-foreground group-hover:text-foreground group-hover:scale-110 transition-all" />
119
125
  </button>
120
126
  )}
121
- <button
122
- onClick={() => onExecute(tool)}
123
- className="btn btn-primary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
127
+
128
+ {/* Primary: Execute Tool (Prominent Button) */}
129
+ <button
130
+ onClick={() => onExecute(tool)}
131
+ className="group relative flex-1 h-8 rounded bg-primary hover:bg-primary/90 transition-all duration-200 flex items-center justify-center gap-1.5 shadow-sm hover:shadow-md"
124
132
  >
125
- <Play className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
126
- <span className="truncate">Execute</span>
133
+ <PlayIcon className="h-4 w-4 text-white" />
134
+ <span className="text-sm font-medium text-white">Execute</span>
127
135
  </button>
136
+
137
+ {/* Secondary: Use in Chat (Icon-only) */}
128
138
  <button
129
139
  onClick={handleUseInChat}
130
- className="btn btn-secondary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
131
- title="Use in Chat"
140
+ className="group relative h-8 w-8 rounded hover:bg-muted transition-all duration-200 flex items-center justify-center"
141
+ title="Use in AI chat"
132
142
  >
133
- <MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
134
- <span className="truncate">Chat</span>
143
+ <ChatBubbleLeftIcon className="h-4 w-4 text-muted-foreground group-hover:text-foreground group-hover:scale-110 transition-all" />
135
144
  </button>
136
145
  </div>
137
146
  </div>
@@ -0,0 +1,475 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import {
5
+ MicrophoneIcon,
6
+ XMarkIcon,
7
+ Cog6ToothIcon,
8
+ ChatBubbleLeftRightIcon,
9
+ SpeakerWaveIcon
10
+ } from '@heroicons/react/24/outline';
11
+
12
+ // LLM State type
13
+ type LLMState = 'idle' | 'listening' | 'thinking' | 'speaking';
14
+
15
+ interface VoiceOrbOverlayProps {
16
+ isOpen: boolean;
17
+ onClose: () => void;
18
+ onSendMessage: (text: string) => void;
19
+ elevenLabsApiKey: string;
20
+ llmState: LLMState;
21
+ spokenText?: string;
22
+ onGreet?: () => void;
23
+ onSettingsClick?: () => void;
24
+ displayMode?: 'voice-only' | 'voice-chat';
25
+ onDisplayModeChange?: (mode: 'voice-only' | 'voice-chat') => void;
26
+ inputLanguage?: string;
27
+ onInterrupt?: () => void;
28
+ voiceModeActive?: boolean; // Keep speech recognition active even when overlay closed
29
+ }
30
+
31
+ export function VoiceOrbOverlay({
32
+ isOpen,
33
+ onClose,
34
+ onSendMessage,
35
+ elevenLabsApiKey,
36
+ llmState,
37
+ spokenText,
38
+ onGreet,
39
+ onSettingsClick,
40
+ displayMode = 'voice-only',
41
+ onDisplayModeChange,
42
+ inputLanguage = 'en-US',
43
+ onInterrupt,
44
+ voiceModeActive = false
45
+ }: VoiceOrbOverlayProps) {
46
+ const [transcript, setTranscript] = useState('');
47
+ const [hasGreeted, setHasGreeted] = useState(false);
48
+
49
+ const recognitionRef = useRef<SpeechRecognition | null>(null);
50
+ const silenceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
51
+ const isListeningRef = useRef(false);
52
+
53
+ // Keep ref in sync
54
+ useEffect(() => {
55
+ isListeningRef.current = llmState === 'listening';
56
+ }, [llmState]);
57
+
58
+ // Handle sending message
59
+ const handleSend = useCallback((text: string) => {
60
+ if (!text.trim()) return;
61
+
62
+ if (recognitionRef.current) {
63
+ try {
64
+ recognitionRef.current.stop();
65
+ } catch (e) { }
66
+ }
67
+ setTranscript('');
68
+ onSendMessage(text.trim());
69
+ }, [onSendMessage]);
70
+
71
+ // Initialize Speech Recognition
72
+ useEffect(() => {
73
+ if (typeof window === 'undefined') return;
74
+
75
+ // Web Speech API types
76
+ interface WebWindow extends Window {
77
+ SpeechRecognition?: typeof SpeechRecognition;
78
+ webkitSpeechRecognition?: typeof SpeechRecognition;
79
+ }
80
+ const webWindow = window as WebWindow;
81
+ const SpeechRecognitionCtor = webWindow.SpeechRecognition || webWindow.webkitSpeechRecognition;
82
+ if (!SpeechRecognitionCtor) {
83
+ console.error('Speech Recognition not supported');
84
+ return;
85
+ }
86
+
87
+ const recognition = new SpeechRecognitionCtor();
88
+ recognition.continuous = false;
89
+ recognition.interimResults = true;
90
+ recognition.lang = inputLanguage; // Use configured input language
91
+
92
+ let currentTranscript = '';
93
+
94
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
95
+ let finalTranscript = '';
96
+ let interimTranscript = '';
97
+
98
+ for (let i = event.resultIndex; i < event.results.length; i++) {
99
+ const text = event.results[i][0].transcript;
100
+ if (event.results[i].isFinal) {
101
+ finalTranscript += text;
102
+ } else {
103
+ interimTranscript += text;
104
+ }
105
+ }
106
+
107
+ // Talk-to-interrupt: if user speaks during TTS, stop it immediately
108
+ if ((finalTranscript || interimTranscript) && llmState === 'speaking' && onInterrupt) {
109
+ console.log('🛑 User interrupted TTS');
110
+ onInterrupt();
111
+ }
112
+
113
+ currentTranscript = finalTranscript || interimTranscript;
114
+ setTranscript(currentTranscript);
115
+
116
+ if (finalTranscript.trim()) {
117
+ if (silenceTimeoutRef.current) {
118
+ clearTimeout(silenceTimeoutRef.current);
119
+ }
120
+
121
+ silenceTimeoutRef.current = setTimeout(() => {
122
+ if (currentTranscript.trim() && llmState === 'listening') {
123
+ handleSend(currentTranscript.trim());
124
+ currentTranscript = '';
125
+ }
126
+ }, 800);
127
+ }
128
+ };
129
+
130
+ recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
131
+ console.error('Speech recognition error:', event.error);
132
+ };
133
+
134
+ recognition.onend = () => {
135
+ setTimeout(() => {
136
+ if (isListeningRef.current && llmState === 'listening') {
137
+ try {
138
+ recognition.start();
139
+ } catch (e) { }
140
+ }
141
+ }, 100);
142
+ };
143
+
144
+ recognitionRef.current = recognition;
145
+
146
+ return () => {
147
+ if (silenceTimeoutRef.current) {
148
+ clearTimeout(silenceTimeoutRef.current);
149
+ }
150
+ try {
151
+ recognition.stop();
152
+ } catch (e) { }
153
+ };
154
+ }, [handleSend, llmState]);
155
+
156
+ // Start listening
157
+ const startListening = useCallback(() => {
158
+ if (recognitionRef.current && llmState !== 'speaking' && llmState !== 'thinking') {
159
+ try {
160
+ recognitionRef.current.start();
161
+ } catch (e) {
162
+ console.error('Failed to start speech recognition:', e);
163
+ }
164
+ }
165
+ }, [llmState]);
166
+
167
+ // Stop listening
168
+ const stopListening = useCallback(() => {
169
+ if (recognitionRef.current) {
170
+ try {
171
+ recognitionRef.current.stop();
172
+ } catch (e) { }
173
+ }
174
+ if (silenceTimeoutRef.current) {
175
+ clearTimeout(silenceTimeoutRef.current);
176
+ }
177
+ }, []);
178
+
179
+ // Greet on open
180
+ useEffect(() => {
181
+ if (isOpen && !hasGreeted && onGreet) {
182
+ setHasGreeted(true);
183
+ onGreet();
184
+ }
185
+ if (!isOpen && !voiceModeActive) {
186
+ setHasGreeted(false);
187
+ setTranscript('');
188
+ }
189
+ }, [isOpen, hasGreeted, onGreet, voiceModeActive]);
190
+
191
+ // Start listening after greeting or when state becomes listening
192
+ // Also listen when voiceModeActive is true (voice+chat mode)
193
+ useEffect(() => {
194
+ const shouldListen = (isOpen || voiceModeActive) && llmState === 'listening';
195
+ if (shouldListen) {
196
+ startListening();
197
+ } else if (!isOpen && !voiceModeActive) {
198
+ stopListening();
199
+ }
200
+ }, [isOpen, voiceModeActive, llmState, startListening, stopListening]);
201
+
202
+ // Cleanup on close - only if voice mode is completely off
203
+ useEffect(() => {
204
+ if (!isOpen && !voiceModeActive) {
205
+ stopListening();
206
+ }
207
+ }, [isOpen, voiceModeActive, stopListening]);
208
+
209
+ // Handle mic click
210
+ const handleMicClick = () => {
211
+ if (llmState === 'listening' && transcript.trim()) {
212
+ handleSend(transcript.trim());
213
+ }
214
+ };
215
+
216
+ // Handle close
217
+ const handleClose = () => {
218
+ stopListening();
219
+ setHasGreeted(false);
220
+ onClose();
221
+ };
222
+
223
+ if (!isOpen) return null;
224
+
225
+ // Status text based on state
226
+ const getStatusText = () => {
227
+ switch (llmState) {
228
+ case 'speaking':
229
+ return spokenText || 'Speaking...';
230
+ case 'thinking':
231
+ return 'Processing your request...';
232
+ case 'listening':
233
+ return transcript || 'Listening...';
234
+ default:
235
+ return 'Ready';
236
+ }
237
+ };
238
+
239
+ return (
240
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#0a0a0a]/98 backdrop-blur-xl" style={{ left: 'var(--sidebar-width, 15rem)' }}>
241
+ {/* Main Container - Properly Centered */}
242
+ <div className="flex flex-col items-center justify-center gap-10 w-full max-w-lg px-6 h-full">
243
+
244
+ {/* Professional Orb */}
245
+ <div className="relative flex items-center justify-center">
246
+ {/* Ambient glow */}
247
+ <div
248
+ className={`absolute w-64 h-64 rounded-full transition-all duration-700 ${llmState === 'speaking'
249
+ ? 'bg-gradient-to-br from-blue-500/30 via-violet-500/20 to-cyan-500/30 scale-110 blur-3xl animate-pulse'
250
+ : llmState === 'thinking'
251
+ ? 'bg-gradient-to-br from-amber-500/20 via-orange-500/15 to-yellow-500/20 scale-100 blur-3xl animate-spin-slow'
252
+ : llmState === 'listening'
253
+ ? 'bg-gradient-to-br from-blue-500/25 via-cyan-500/20 to-blue-600/25 scale-105 blur-3xl animate-pulse'
254
+ : 'bg-gradient-to-br from-slate-500/15 via-slate-600/10 to-slate-500/15 scale-100 blur-3xl'
255
+ }`}
256
+ />
257
+
258
+ {/* Orb container */}
259
+ <div
260
+ className={`relative w-44 h-44 rounded-full transition-transform duration-500 ${llmState === 'speaking' ? 'scale-110'
261
+ : llmState === 'thinking' ? 'scale-95'
262
+ : llmState === 'listening' ? 'scale-105'
263
+ : 'scale-100'
264
+ }`}
265
+ >
266
+ {/* Rotating gradient ring */}
267
+ <div
268
+ className={`absolute inset-0 rounded-full ${llmState === 'thinking' ? 'animate-spin-slow' : ''
269
+ }`}
270
+ style={{
271
+ background: llmState === 'thinking'
272
+ ? 'conic-gradient(from 0deg, #f59e0b, #f97316, #ef4444, #f59e0b)'
273
+ : llmState === 'speaking'
274
+ ? 'conic-gradient(from 0deg, #3b82f6, #8b5cf6, #06b6d4, #3b82f6)'
275
+ : llmState === 'listening'
276
+ ? 'conic-gradient(from 0deg, #3b82f6, #60a5fa, #3b82f6)'
277
+ : 'conic-gradient(from 0deg, #475569, #64748b, #475569)',
278
+ padding: '3px',
279
+ borderRadius: '50%'
280
+ }}
281
+ >
282
+ {/* Inner orb */}
283
+ <div
284
+ className="w-full h-full rounded-full bg-[#0a0a0a] flex items-center justify-center"
285
+ style={{
286
+ boxShadow: llmState === 'speaking'
287
+ ? '0 0 60px 10px rgba(59, 130, 246, 0.3), inset 0 0 30px rgba(139, 92, 246, 0.2)'
288
+ : llmState === 'thinking'
289
+ ? '0 0 40px 5px rgba(245, 158, 11, 0.2), inset 0 0 20px rgba(249, 115, 22, 0.1)'
290
+ : llmState === 'listening'
291
+ ? '0 0 50px 8px rgba(59, 130, 246, 0.25), inset 0 0 25px rgba(96, 165, 250, 0.15)'
292
+ : '0 0 30px 5px rgba(71, 85, 105, 0.15)'
293
+ }}
294
+ >
295
+ {/* Center gradient */}
296
+ <div
297
+ className={`w-32 h-32 rounded-full transition-all duration-500 ${llmState === 'speaking' ? 'animate-pulse-fast'
298
+ : llmState === 'listening' ? 'animate-pulse'
299
+ : ''
300
+ }`}
301
+ style={{
302
+ background: llmState === 'thinking'
303
+ ? 'radial-gradient(circle, #f59e0b 0%, #0a0a0a 70%)'
304
+ : llmState === 'speaking'
305
+ ? 'radial-gradient(circle, #8b5cf6 0%, #3b82f6 40%, #0a0a0a 70%)'
306
+ : llmState === 'listening'
307
+ ? 'radial-gradient(circle, #60a5fa 0%, #3b82f6 40%, #0a0a0a 70%)'
308
+ : 'radial-gradient(circle, #64748b 0%, #0a0a0a 60%)'
309
+ }}
310
+ />
311
+ </div>
312
+ </div>
313
+ </div>
314
+ </div>
315
+
316
+ {/* Status Text */}
317
+ <div className="text-center max-w-md min-h-[80px] flex items-center justify-center">
318
+ <p
319
+ className={`text-lg font-light leading-relaxed transition-all duration-300 ${llmState === 'speaking'
320
+ ? 'text-white/90'
321
+ : llmState === 'thinking'
322
+ ? 'text-amber-400/80 animate-pulse'
323
+ : llmState === 'listening' && transcript
324
+ ? 'text-white/80'
325
+ : 'text-white/50'
326
+ }`}
327
+ >
328
+ {getStatusText()}
329
+ </p>
330
+ </div>
331
+
332
+ {/* Control Bar */}
333
+ <div className="flex items-center gap-3">
334
+ {/* Settings */}
335
+ {onSettingsClick && (
336
+ <button
337
+ onClick={onSettingsClick}
338
+ className="w-12 h-12 rounded-full bg-white/5 hover:bg-white/10 border border-white/10 flex items-center justify-center transition-all"
339
+ title="Voice Settings"
340
+ >
341
+ <Cog6ToothIcon className="w-5 h-5 text-white/60" />
342
+ </button>
343
+ )}
344
+
345
+ {/* Main mic button */}
346
+ <button
347
+ onClick={handleMicClick}
348
+ disabled={llmState === 'thinking' || llmState === 'speaking'}
349
+ className={`w-16 h-16 rounded-full flex items-center justify-center transition-all duration-300 ${llmState === 'listening'
350
+ ? 'bg-blue-500 text-white shadow-lg shadow-blue-500/30 scale-110'
351
+ : llmState === 'thinking'
352
+ ? 'bg-amber-500/20 text-amber-400 cursor-wait'
353
+ : llmState === 'speaking'
354
+ ? 'bg-violet-500/20 text-violet-400 cursor-not-allowed'
355
+ : 'bg-white/10 text-white/60 hover:bg-white/20'
356
+ }`}
357
+ >
358
+ {llmState === 'speaking' ? (
359
+ <SpeakerWaveIcon className="w-7 h-7 animate-pulse" />
360
+ ) : (
361
+ <MicrophoneIcon className="w-7 h-7" />
362
+ )}
363
+ </button>
364
+
365
+ {/* Display mode toggle */}
366
+ {onDisplayModeChange && (
367
+ <button
368
+ onClick={() => onDisplayModeChange(displayMode === 'voice-only' ? 'voice-chat' : 'voice-only')}
369
+ className={`w-12 h-12 rounded-full border border-white/10 flex items-center justify-center transition-all ${displayMode === 'voice-chat'
370
+ ? 'bg-blue-500/20 text-blue-400'
371
+ : 'bg-white/5 hover:bg-white/10 text-white/60'
372
+ }`}
373
+ title={displayMode === 'voice-only' ? 'Show Chat' : 'Voice Only'}
374
+ >
375
+ <ChatBubbleLeftRightIcon className="w-5 h-5" />
376
+ </button>
377
+ )}
378
+
379
+ {/* Close */}
380
+ <button
381
+ onClick={handleClose}
382
+ className="w-12 h-12 rounded-full bg-white/5 hover:bg-red-500/20 border border-white/10 hover:border-red-500/30 flex items-center justify-center transition-all group"
383
+ title="End Voice Mode"
384
+ >
385
+ <XMarkIcon className="w-5 h-5 text-white/60 group-hover:text-red-400" />
386
+ </button>
387
+ </div>
388
+
389
+ {/* State Indicator Pills */}
390
+ <div className="flex items-center gap-2 text-xs">
391
+ <div className={`px-3 py-1 rounded-full border transition-all ${llmState === 'listening'
392
+ ? 'bg-blue-500/20 border-blue-500/30 text-blue-400'
393
+ : 'bg-white/5 border-white/10 text-white/30'
394
+ }`}>
395
+ Listening
396
+ </div>
397
+ <div className={`px-3 py-1 rounded-full border transition-all ${llmState === 'thinking'
398
+ ? 'bg-amber-500/20 border-amber-500/30 text-amber-400'
399
+ : 'bg-white/5 border-white/10 text-white/30'
400
+ }`}>
401
+ Processing
402
+ </div>
403
+ <div className={`px-3 py-1 rounded-full border transition-all ${llmState === 'speaking'
404
+ ? 'bg-violet-500/20 border-violet-500/30 text-violet-400'
405
+ : 'bg-white/5 border-white/10 text-white/30'
406
+ }`}>
407
+ Speaking
408
+ </div>
409
+ </div>
410
+ </div>
411
+
412
+ {/* Custom CSS animations */}
413
+ <style jsx>{`
414
+ @keyframes spin-slow {
415
+ from { transform: rotate(0deg); }
416
+ to { transform: rotate(360deg); }
417
+ }
418
+ @keyframes pulse-fast {
419
+ 0%, 100% { opacity: 1; transform: scale(1); }
420
+ 50% { opacity: 0.8; transform: scale(1.05); }
421
+ }
422
+ .animate-spin-slow {
423
+ animation: spin-slow 3s linear infinite;
424
+ }
425
+ .animate-pulse-fast {
426
+ animation: pulse-fast 0.8s ease-in-out infinite;
427
+ }
428
+ `}</style>
429
+ </div>
430
+ );
431
+ }
432
+
433
+ // Header Voice Badge for Voice+Chat mode - shows orb + state text
434
+ export function MiniVoiceOrb({
435
+ llmState,
436
+ onClick
437
+ }: {
438
+ llmState: LLMState;
439
+ onClick?: () => void;
440
+ }) {
441
+ const getStateInfo = () => {
442
+ switch (llmState) {
443
+ case 'speaking':
444
+ return { text: 'Speaking', bgClass: 'bg-violet-500/10 border-violet-500/30', dotClass: 'bg-violet-500', textClass: 'text-violet-400' };
445
+ case 'thinking':
446
+ return { text: 'Processing', bgClass: 'bg-amber-500/10 border-amber-500/30', dotClass: 'bg-amber-500', textClass: 'text-amber-400' };
447
+ case 'listening':
448
+ return { text: 'Listening', bgClass: 'bg-blue-500/10 border-blue-500/30', dotClass: 'bg-blue-500', textClass: 'text-blue-400' };
449
+ default:
450
+ return { text: 'Ready', bgClass: 'bg-slate-500/10 border-slate-500/30', dotClass: 'bg-slate-500', textClass: 'text-slate-400' };
451
+ }
452
+ };
453
+
454
+ const stateInfo = getStateInfo();
455
+
456
+ return (
457
+ <button
458
+ onClick={onClick}
459
+ className={`flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all hover:scale-105 ${stateInfo.bgClass}`}
460
+ title="Click to expand voice mode"
461
+ >
462
+ {/* Mini orb */}
463
+ <div className="relative">
464
+ <div className={`w-2.5 h-2.5 rounded-full ${stateInfo.dotClass} ${llmState === 'listening' || llmState === 'speaking' ? 'animate-pulse' : llmState === 'thinking' ? 'animate-spin' : ''}`} />
465
+ {(llmState === 'listening' || llmState === 'speaking') && (
466
+ <div className={`absolute inset-0 w-2.5 h-2.5 rounded-full ${stateInfo.dotClass} animate-ping opacity-75`} />
467
+ )}
468
+ </div>
469
+ {/* State text */}
470
+ <span className={`text-xs font-medium ${stateInfo.textClass}`}>
471
+ {stateInfo.text}
472
+ </span>
473
+ </button>
474
+ );
475
+ }
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
4
+
5
+ interface Props {
6
+ children: ReactNode;
7
+ fallback?: ReactNode;
8
+ }
9
+
10
+ interface State {
11
+ hasError: boolean;
12
+ error?: Error;
13
+ }
14
+
15
+ /**
16
+ * Error boundary for widgets - silently catches errors and renders nothing
17
+ * to prevent broken widgets from affecting the rest of the UI
18
+ */
19
+ export class WidgetErrorBoundary extends Component<Props, State> {
20
+ constructor(props: Props) {
21
+ super(props);
22
+ this.state = { hasError: false };
23
+ }
24
+
25
+ static getDerivedStateFromError(error: Error): State {
26
+ // Update state so the next render will show the fallback UI.
27
+ return { hasError: true, error };
28
+ }
29
+
30
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
31
+ // Log the error silently - don't show to user
32
+ console.error('🚫 Widget error caught by boundary:', {
33
+ error: error.message,
34
+ stack: error.stack,
35
+ componentStack: errorInfo.componentStack,
36
+ });
37
+ }
38
+
39
+ render() {
40
+ if (this.state.hasError) {
41
+ // Return fallback or nothing - don't show error UI
42
+ return this.props.fallback ?? null;
43
+ }
44
+
45
+ return this.props.children;
46
+ }
47
+ }
48
+