vibefast-cli 0.1.1

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 (250) hide show
  1. package/FINAL-STATUS.md +144 -0
  2. package/HOW-IT-WORKS.md +559 -0
  3. package/PLAN.md +453 -0
  4. package/README.md +129 -0
  5. package/RECIPES-READY.md +172 -0
  6. package/STATUS.md +199 -0
  7. package/SUCCESS.md +259 -0
  8. package/TESTING-CHECKLIST.md +450 -0
  9. package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
  10. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
  11. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
  12. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
  13. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
  14. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
  15. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
  16. package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
  17. package/cloudflare-worker/MIGRATION.md +160 -0
  18. package/cloudflare-worker/QUICKSTART.md +200 -0
  19. package/cloudflare-worker/README.md +242 -0
  20. package/cloudflare-worker/generate-token.js +32 -0
  21. package/cloudflare-worker/mini-native@latest.zip +0 -0
  22. package/cloudflare-worker/setup.sh +143 -0
  23. package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
  24. package/cloudflare-worker/test-recipe/recipe.json +16 -0
  25. package/cloudflare-worker/worker.js +308 -0
  26. package/cloudflare-worker/wrangler.toml +13 -0
  27. package/dist/commands/add.d.ts +3 -0
  28. package/dist/commands/add.d.ts.map +1 -0
  29. package/dist/commands/add.js +149 -0
  30. package/dist/commands/add.js.map +1 -0
  31. package/dist/commands/devices.d.ts +3 -0
  32. package/dist/commands/devices.d.ts.map +1 -0
  33. package/dist/commands/devices.js +35 -0
  34. package/dist/commands/devices.js.map +1 -0
  35. package/dist/commands/doctor.d.ts +3 -0
  36. package/dist/commands/doctor.d.ts.map +1 -0
  37. package/dist/commands/doctor.js +67 -0
  38. package/dist/commands/doctor.js.map +1 -0
  39. package/dist/commands/list.d.ts +3 -0
  40. package/dist/commands/list.d.ts.map +1 -0
  41. package/dist/commands/list.js +40 -0
  42. package/dist/commands/list.js.map +1 -0
  43. package/dist/commands/login.d.ts +3 -0
  44. package/dist/commands/login.d.ts.map +1 -0
  45. package/dist/commands/login.js +23 -0
  46. package/dist/commands/login.js.map +1 -0
  47. package/dist/commands/logout.d.ts +3 -0
  48. package/dist/commands/logout.d.ts.map +1 -0
  49. package/dist/commands/logout.js +16 -0
  50. package/dist/commands/logout.js.map +1 -0
  51. package/dist/commands/remove.d.ts +3 -0
  52. package/dist/commands/remove.d.ts.map +1 -0
  53. package/dist/commands/remove.js +67 -0
  54. package/dist/commands/remove.js.map +1 -0
  55. package/dist/core/__tests__/journal.test.d.ts +2 -0
  56. package/dist/core/__tests__/journal.test.d.ts.map +1 -0
  57. package/dist/core/__tests__/journal.test.js +101 -0
  58. package/dist/core/__tests__/journal.test.js.map +1 -0
  59. package/dist/core/__tests__/validate.test.d.ts +2 -0
  60. package/dist/core/__tests__/validate.test.d.ts.map +1 -0
  61. package/dist/core/__tests__/validate.test.js +53 -0
  62. package/dist/core/__tests__/validate.test.js.map +1 -0
  63. package/dist/core/archive.d.ts +2 -0
  64. package/dist/core/archive.d.ts.map +1 -0
  65. package/dist/core/archive.js +59 -0
  66. package/dist/core/archive.js.map +1 -0
  67. package/dist/core/auth.d.ts +15 -0
  68. package/dist/core/auth.d.ts.map +1 -0
  69. package/dist/core/auth.js +76 -0
  70. package/dist/core/auth.js.map +1 -0
  71. package/dist/core/codemod.d.ts +20 -0
  72. package/dist/core/codemod.d.ts.map +1 -0
  73. package/dist/core/codemod.js +150 -0
  74. package/dist/core/codemod.js.map +1 -0
  75. package/dist/core/fsx.d.ts +12 -0
  76. package/dist/core/fsx.d.ts.map +1 -0
  77. package/dist/core/fsx.js +70 -0
  78. package/dist/core/fsx.js.map +1 -0
  79. package/dist/core/http.d.ts +30 -0
  80. package/dist/core/http.d.ts.map +1 -0
  81. package/dist/core/http.js +95 -0
  82. package/dist/core/http.js.map +1 -0
  83. package/dist/core/journal.d.ts +18 -0
  84. package/dist/core/journal.d.ts.map +1 -0
  85. package/dist/core/journal.js +34 -0
  86. package/dist/core/journal.js.map +1 -0
  87. package/dist/core/log.d.ts +8 -0
  88. package/dist/core/log.d.ts.map +1 -0
  89. package/dist/core/log.js +9 -0
  90. package/dist/core/log.js.map +1 -0
  91. package/dist/core/pathGuard.d.ts +3 -0
  92. package/dist/core/pathGuard.d.ts.map +1 -0
  93. package/dist/core/pathGuard.js +18 -0
  94. package/dist/core/pathGuard.js.map +1 -0
  95. package/dist/core/paths.d.ts +11 -0
  96. package/dist/core/paths.d.ts.map +1 -0
  97. package/dist/core/paths.js +22 -0
  98. package/dist/core/paths.js.map +1 -0
  99. package/dist/core/validate.d.ts +8 -0
  100. package/dist/core/validate.d.ts.map +1 -0
  101. package/dist/core/validate.js +27 -0
  102. package/dist/core/validate.js.map +1 -0
  103. package/dist/index.d.ts +3 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +23 -0
  106. package/dist/index.js.map +1 -0
  107. package/docs/decisions.md +55 -0
  108. package/package.json +39 -0
  109. package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
  110. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
  111. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
  112. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
  113. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
  114. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
  115. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
  116. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
  117. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
  118. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
  119. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
  120. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
  121. package/recipes/audio-recorder/recipe.json +22 -0
  122. package/recipes/audio-recorder@latest.zip +0 -0
  123. package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
  124. package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
  125. package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
  126. package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
  127. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
  128. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
  129. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
  130. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
  131. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
  132. package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
  133. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
  134. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
  135. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
  136. package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
  137. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
  138. package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
  139. package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
  140. package/recipes/charts/recipe.json +22 -0
  141. package/recipes/charts@latest.zip +0 -0
  142. package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
  143. package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +302 -0
  144. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
  145. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
  146. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
  147. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
  148. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
  149. package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
  150. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
  151. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
  152. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
  153. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
  154. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
  155. package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
  156. package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
  157. package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
  158. package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
  159. package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
  160. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
  161. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
  162. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
  163. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
  164. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
  165. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
  166. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
  167. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
  168. package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
  169. package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
  170. package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
  171. package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
  172. package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
  173. package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
  174. package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
  175. package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
  176. package/recipes/chatbot/recipe.json +22 -0
  177. package/recipes/chatbot@latest.zip +0 -0
  178. package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
  179. package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
  180. package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
  181. package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
  182. package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
  183. package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
  184. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
  185. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
  186. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
  187. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
  188. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
  189. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
  190. package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
  191. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
  192. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
  193. package/recipes/image-generator/recipe.json +22 -0
  194. package/recipes/image-generator@latest.zip +0 -0
  195. package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
  196. package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
  197. package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
  198. package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
  199. package/recipes/quiz/recipe.json +22 -0
  200. package/recipes/quiz@latest.zip +0 -0
  201. package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
  202. package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
  203. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
  204. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
  205. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
  206. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
  207. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
  208. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
  209. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
  210. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
  211. package/recipes/tracker-app/recipe.json +22 -0
  212. package/recipes/tracker-app@latest.zip +0 -0
  213. package/recipes/upload-all.sh +32 -0
  214. package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
  215. package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
  216. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
  217. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
  218. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
  219. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
  220. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
  221. package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
  222. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
  223. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
  224. package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
  225. package/recipes/voice-bot/recipe.json +22 -0
  226. package/recipes/voice-bot@latest.zip +0 -0
  227. package/scripts/create-recipes.mjs +189 -0
  228. package/src/commands/add.ts +183 -0
  229. package/src/commands/devices.ts +38 -0
  230. package/src/commands/doctor.ts +67 -0
  231. package/src/commands/list.ts +45 -0
  232. package/src/commands/login.ts +24 -0
  233. package/src/commands/logout.ts +15 -0
  234. package/src/commands/remove.ts +78 -0
  235. package/src/core/__tests__/journal.test.ts +119 -0
  236. package/src/core/__tests__/validate.test.ts +64 -0
  237. package/src/core/archive.ts +69 -0
  238. package/src/core/auth.ts +103 -0
  239. package/src/core/codemod.ts +211 -0
  240. package/src/core/fsx.ts +80 -0
  241. package/src/core/http.ts +136 -0
  242. package/src/core/journal.ts +64 -0
  243. package/src/core/log.ts +9 -0
  244. package/src/core/pathGuard.ts +22 -0
  245. package/src/core/paths.ts +33 -0
  246. package/src/core/validate.ts +44 -0
  247. package/src/index.ts +27 -0
  248. package/test-critical-cases.mjs +258 -0
  249. package/tsconfig.json +21 -0
  250. package/vitest.config.mts +12 -0
@@ -0,0 +1,664 @@
1
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+
4
+ import { chatbotApi } from '@/api-client/chatbot';
5
+ import type { AgentStatus, AppMessage } from '@/features/chatbot/types';
6
+ import {
7
+ type ChatTelemetryTracker,
8
+ createChatTelemetryTracker,
9
+ } from '@/features/chatbot/utils/chat-telemetry';
10
+
11
+ type AttachmentInput = {
12
+ type: 'image';
13
+ storageId: string;
14
+ fileName?: string;
15
+ mimeType: string;
16
+ };
17
+
18
+ type SubmitOptions = {
19
+ body?: {
20
+ conversationId?: string;
21
+ userMessageId?: string;
22
+ preferredModel?: string;
23
+ attachments?: AttachmentInput[];
24
+ text?: string;
25
+ };
26
+ };
27
+
28
+ type UseChatConfigOptions = {
29
+ conversationId: string | null;
30
+ authToken?: string;
31
+ initialMessages?: AppMessage[];
32
+ preferredProvider?: string;
33
+ preferredModel?: string;
34
+ searchMode?: string;
35
+ onStreamFailure?: (details: {
36
+ userMessageId: string | null;
37
+ text: string;
38
+ attachments?: AttachmentInput[];
39
+ errorMessage?: string;
40
+ }) => void;
41
+ };
42
+
43
+ const STREAM_TIMEOUT_INITIAL_MS = 45000;
44
+ const STREAM_TIMEOUT_POLL_MS = 15000;
45
+ const STREAM_TIMEOUT_MAX_MS = 180000;
46
+ const STREAM_TIMEOUT_ERROR_MESSAGE =
47
+ 'Request timed out. Please check your connection and try again.';
48
+
49
+ const extractStreamText = (streamBody: unknown): string => {
50
+ if (!streamBody) return '';
51
+ if (typeof streamBody === 'string') {
52
+ return streamBody;
53
+ }
54
+
55
+ if (typeof streamBody === 'object') {
56
+ const body = streamBody as Record<string, unknown>;
57
+ if (typeof body.text === 'string') return body.text;
58
+ if (typeof body.body === 'string') return body.body;
59
+ }
60
+
61
+ return '';
62
+ };
63
+
64
+ export const useChatConfig = (options: UseChatConfigOptions) => {
65
+ const { conversationId } = options;
66
+
67
+ const startAgentStream = chatbotApi.useStartAgentStream();
68
+
69
+ const sessionData = chatbotApi.useAgentSession(
70
+ conversationId ? (conversationId as Id<'conversations'>) : undefined,
71
+ );
72
+
73
+ const [input, setInput] = useState('');
74
+ const [messages, setMessages] = useState<AppMessage[]>(
75
+ options.initialMessages ?? [],
76
+ );
77
+ const [isLoading, setIsLoading] = useState(false);
78
+ const [error, setError] = useState<Error | null>(null);
79
+ const [activeStreamId, setActiveStreamId] = useState<string | null>(null);
80
+ const [streamingMessageId, setStreamingMessageId] = useState<string | null>(
81
+ null,
82
+ );
83
+ const telemetryRef = useRef<ChatTelemetryTracker | null>(null);
84
+ const firstStreamChunkLoggedRef = useRef(false);
85
+ const streamTimingRef = useRef<{
86
+ requestStart?: number;
87
+ firstChunk?: number;
88
+ }>({});
89
+ const pendingSubmissionRef = useRef<{
90
+ userMessageId: string;
91
+ text: string;
92
+ attachments?: AttachmentInput[];
93
+ placeholderId?: string;
94
+ } | null>(null);
95
+ const lastErrorSeenRef = useRef<number | null>(null);
96
+ const errorTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
97
+ const latestStatusRef = useRef<AgentStatus | null>(null);
98
+ const [optimisticStatus, setOptimisticStatus] = useState<AgentStatus | null>(
99
+ null,
100
+ );
101
+
102
+ const finalizeStreamFailure = useCallback(
103
+ (errorMessage?: string) => {
104
+ // Clear any pending error timeout
105
+ if (errorTimeoutRef.current) {
106
+ clearTimeout(errorTimeoutRef.current);
107
+ errorTimeoutRef.current = null;
108
+ }
109
+
110
+ const pending = pendingSubmissionRef.current;
111
+ const placeholderId =
112
+ pending?.placeholderId ?? streamingMessageId ?? null;
113
+
114
+ console.log('[useChatConfig] finalizeStreamFailure called', {
115
+ errorMessage,
116
+ hasPending: !!pending,
117
+ placeholderId,
118
+ });
119
+
120
+ setMessages((prev) =>
121
+ prev.filter((message) => {
122
+ if (pending && message.id === pending.userMessageId) {
123
+ return false;
124
+ }
125
+ if (placeholderId && message.id === placeholderId) {
126
+ return false;
127
+ }
128
+ return true;
129
+ }),
130
+ );
131
+
132
+ setStreamingMessageId(null);
133
+ setActiveStreamId(null);
134
+ setIsLoading(false);
135
+ if (errorMessage) {
136
+ setError(new Error(errorMessage));
137
+ }
138
+ if (telemetryRef.current) {
139
+ telemetryRef.current.mark('stream.failure', {
140
+ reason: errorMessage ?? 'unknown',
141
+ });
142
+ telemetryRef.current.finalize('error', {
143
+ reason: 'streamFailed',
144
+ errorMessage: errorMessage ?? null,
145
+ });
146
+ telemetryRef.current = null;
147
+ }
148
+ firstStreamChunkLoggedRef.current = false;
149
+ streamTimingRef.current = {};
150
+ latestStatusRef.current = null;
151
+ setOptimisticStatus(null);
152
+
153
+ const restoredText = pending?.text ?? '';
154
+ if (pending) {
155
+ setInput(restoredText);
156
+ }
157
+
158
+ options.onStreamFailure?.({
159
+ userMessageId: pending?.userMessageId ?? null,
160
+ text: restoredText,
161
+ attachments: pending?.attachments,
162
+ errorMessage,
163
+ });
164
+
165
+ pendingSubmissionRef.current = null;
166
+ },
167
+ [options, streamingMessageId],
168
+ );
169
+
170
+ const scheduleStreamTimeoutCheck = useCallback(
171
+ (delayMs: number) => {
172
+ if (errorTimeoutRef.current) {
173
+ clearTimeout(errorTimeoutRef.current);
174
+ }
175
+
176
+ errorTimeoutRef.current = setTimeout(() => {
177
+ const now = Date.now();
178
+ const requestStart = streamTimingRef.current.requestStart ?? now;
179
+ const elapsed = now - requestStart;
180
+ const status = latestStatusRef.current;
181
+ const isSessionActive =
182
+ status?.type === 'thinking' || status?.type === 'tool';
183
+ const withinMaxWait = elapsed < STREAM_TIMEOUT_MAX_MS;
184
+
185
+ if (isSessionActive && withinMaxWait) {
186
+ scheduleStreamTimeoutCheck(STREAM_TIMEOUT_POLL_MS);
187
+ return;
188
+ }
189
+
190
+ console.warn('[useChatConfig] Stream timeout - no data received', {
191
+ elapsedMs: elapsed,
192
+ statusType: status?.type ?? 'none',
193
+ });
194
+ finalizeStreamFailure(STREAM_TIMEOUT_ERROR_MESSAGE);
195
+ }, delayMs);
196
+ },
197
+ [finalizeStreamFailure],
198
+ );
199
+
200
+ useEffect(() => {
201
+ setMessages(options.initialMessages ?? []);
202
+ setActiveStreamId(null);
203
+ setStreamingMessageId(null);
204
+ setIsLoading(false);
205
+ setError(null);
206
+ if (telemetryRef.current) {
207
+ telemetryRef.current.finalize('error', { reason: 'conversationChanged' });
208
+ telemetryRef.current = null;
209
+ }
210
+ if (errorTimeoutRef.current) {
211
+ clearTimeout(errorTimeoutRef.current);
212
+ errorTimeoutRef.current = null;
213
+ }
214
+ firstStreamChunkLoggedRef.current = false;
215
+ streamTimingRef.current = {};
216
+ pendingSubmissionRef.current = null;
217
+ lastErrorSeenRef.current = null;
218
+ setOptimisticStatus(null);
219
+ }, [conversationId, options.initialMessages]);
220
+
221
+ useEffect(() => {
222
+ if (sessionData) {
223
+ setActiveStreamId(sessionData.activeStreamId ?? null);
224
+ }
225
+ }, [sessionData]);
226
+
227
+ useEffect(() => {
228
+ if (!options.initialMessages) {
229
+ return;
230
+ }
231
+
232
+ setMessages((prev) => {
233
+ const persistedMap = new Map(
234
+ options.initialMessages.map((msg) => [msg.id, msg]),
235
+ );
236
+ const preserved = prev.filter((msg) => !persistedMap.has(msg.id));
237
+ return [...options.initialMessages, ...preserved];
238
+ });
239
+ }, [options.initialMessages]);
240
+
241
+ const effectiveStreamId = useMemo(
242
+ () => activeStreamId ?? sessionData?.activeStreamId ?? null,
243
+ [activeStreamId, sessionData?.activeStreamId],
244
+ );
245
+
246
+ const streamBody = chatbotApi.useAgentStreamBody(
247
+ conversationId ? (conversationId as Id<'conversations'>) : undefined,
248
+ effectiveStreamId,
249
+ );
250
+
251
+ const sessionStatus: AgentStatus | null = useMemo(() => {
252
+ const status = sessionData?.currentStatus;
253
+ if (!status) {
254
+ return null;
255
+ }
256
+
257
+ if (status.type === 'tool') {
258
+ return {
259
+ type: 'tool',
260
+ toolName: status.toolName ?? 'tool',
261
+ label: status.label ?? null,
262
+ startedAt: status.startedAt ?? Date.now(),
263
+ };
264
+ }
265
+
266
+ return {
267
+ type: 'thinking',
268
+ label: status.label ?? null,
269
+ startedAt: status.startedAt ?? Date.now(),
270
+ };
271
+ }, [sessionData?.currentStatus]);
272
+
273
+ const computedStatus = useMemo(
274
+ () => sessionStatus ?? optimisticStatus,
275
+ [sessionStatus, optimisticStatus],
276
+ );
277
+
278
+ useEffect(() => {
279
+ latestStatusRef.current = computedStatus;
280
+ if (sessionStatus) {
281
+ setOptimisticStatus(null);
282
+ }
283
+ }, [computedStatus, sessionStatus]);
284
+
285
+ const handleInputChange = useCallback((event: any) => {
286
+ if (typeof event === 'string') {
287
+ setInput(event);
288
+ return;
289
+ }
290
+
291
+ const value = event?.target?.value ?? '';
292
+ setInput(value);
293
+ }, []);
294
+
295
+ const handleSubmit = useCallback(
296
+ async (
297
+ event?: React.FormEvent<HTMLFormElement>,
298
+ submitOptions?: SubmitOptions,
299
+ ) => {
300
+ event?.preventDefault?.();
301
+
302
+ if (!conversationId) {
303
+ const err = new Error('Conversation is not initialised.');
304
+ setError(err);
305
+ return;
306
+ }
307
+
308
+ const body = submitOptions?.body;
309
+ const userMessageId = body?.userMessageId;
310
+ const preferredModelOverride =
311
+ body?.preferredModel ?? options.preferredModel ?? null;
312
+
313
+ if (!userMessageId) {
314
+ const err = new Error('Missing user message identifier.');
315
+ setError(err);
316
+ return;
317
+ }
318
+
319
+ const currentInput = body?.text ?? input;
320
+ if (telemetryRef.current) {
321
+ telemetryRef.current.finalize('error', {
322
+ reason: 'supersededByNewSubmit',
323
+ });
324
+ }
325
+ const telemetryTracker = createChatTelemetryTracker('chat.handleSubmit', {
326
+ conversationId,
327
+ userMessageId,
328
+ preferredModel: preferredModelOverride,
329
+ });
330
+ telemetryRef.current = telemetryTracker;
331
+ firstStreamChunkLoggedRef.current = false;
332
+ telemetryTracker?.mark('optimistic.enqueue');
333
+ pendingSubmissionRef.current = {
334
+ userMessageId,
335
+ text: currentInput ?? '',
336
+ attachments: body?.attachments,
337
+ };
338
+ lastErrorSeenRef.current = null;
339
+
340
+ setMessages((prev) => {
341
+ if (prev.some((m) => m.id === userMessageId)) {
342
+ return prev;
343
+ }
344
+
345
+ return [
346
+ ...prev,
347
+ {
348
+ id: userMessageId,
349
+ role: 'user',
350
+ content: currentInput ?? '',
351
+ createdAt: new Date(),
352
+ },
353
+ ];
354
+ });
355
+ telemetryTracker?.mark('optimistic.userMessageAdded');
356
+
357
+ setInput('');
358
+ setIsLoading(true);
359
+ setError(null);
360
+ const requestStart = Date.now();
361
+ streamTimingRef.current = { requestStart };
362
+ console.log('[CHATBOT_STREAM_TIMING] request_start', {
363
+ at: requestStart,
364
+ conversationId,
365
+ userMessageId,
366
+ });
367
+
368
+ try {
369
+ console.log('[CHATBOT_FRONTEND] 🚀 startAgentStream', {
370
+ conversationId,
371
+ userMessageId,
372
+ preferredModel: preferredModelOverride,
373
+ });
374
+ telemetryTracker?.mark('mutation.started');
375
+ latestStatusRef.current = {
376
+ type: 'thinking',
377
+ label: null,
378
+ startedAt: Date.now(),
379
+ };
380
+ setOptimisticStatus({
381
+ type: 'thinking',
382
+ label: 'Thinking…',
383
+ startedAt: Date.now(),
384
+ });
385
+ const result = await startAgentStream({
386
+ conversationId: conversationId as Id<'conversations'>,
387
+ userMessageId: userMessageId as Id<'messages'>,
388
+ preferredModel: body?.preferredModel ?? options.preferredModel,
389
+ });
390
+
391
+ const streamId = result.streamId;
392
+ const tempStreamingId = `stream-${streamId}`;
393
+
394
+ console.log('[CHATBOT_FRONTEND] ✅ startAgentStream response', {
395
+ streamId,
396
+ conversationId,
397
+ userMessageId,
398
+ });
399
+ telemetryTracker?.mark('mutation.succeeded', { streamId });
400
+
401
+ setActiveStreamId(streamId);
402
+ setStreamingMessageId(tempStreamingId);
403
+ pendingSubmissionRef.current = pendingSubmissionRef.current
404
+ ? {
405
+ ...pendingSubmissionRef.current,
406
+ placeholderId: tempStreamingId,
407
+ }
408
+ : {
409
+ userMessageId,
410
+ text: currentInput ?? '',
411
+ attachments: body?.attachments,
412
+ placeholderId: tempStreamingId,
413
+ };
414
+ telemetryTracker?.mark('stream.placeholderCreated', {
415
+ streamingMessageId: tempStreamingId,
416
+ });
417
+
418
+ setMessages((prev) => {
419
+ if (prev.some((m) => m.id === tempStreamingId)) {
420
+ return prev;
421
+ }
422
+
423
+ return [
424
+ ...prev,
425
+ {
426
+ id: tempStreamingId,
427
+ role: 'assistant',
428
+ content: '',
429
+ createdAt: new Date(),
430
+ },
431
+ ];
432
+ });
433
+ telemetryTracker?.mark('stream.placeholderInserted');
434
+
435
+ scheduleStreamTimeoutCheck(STREAM_TIMEOUT_INITIAL_MS);
436
+ } catch (err) {
437
+ console.error('[useChatConfig] Failed to start agent stream', err);
438
+ const message =
439
+ err instanceof Error
440
+ ? err.message
441
+ : typeof err === 'string'
442
+ ? err
443
+ : undefined;
444
+ finalizeStreamFailure(
445
+ message ?? 'Failed to start agent stream. Please try again.',
446
+ );
447
+ throw err;
448
+ }
449
+ },
450
+ [
451
+ conversationId,
452
+ input,
453
+ options.preferredModel,
454
+ startAgentStream,
455
+ scheduleStreamTimeoutCheck,
456
+ finalizeStreamFailure,
457
+ ],
458
+ );
459
+
460
+ useEffect(() => {
461
+ if (!effectiveStreamId || !streamBody) {
462
+ return;
463
+ }
464
+
465
+ const text = extractStreamText(streamBody);
466
+ if (!text) {
467
+ return;
468
+ }
469
+
470
+ // CRITICAL FIX: Always clear timeout when we receive ANY data, regardless of telemetry state
471
+ if (errorTimeoutRef.current) {
472
+ clearTimeout(errorTimeoutRef.current);
473
+ errorTimeoutRef.current = null;
474
+ }
475
+
476
+ if (telemetryRef.current && !firstStreamChunkLoggedRef.current) {
477
+ const now = Date.now();
478
+ const requestStart = streamTimingRef.current.requestStart;
479
+ streamTimingRef.current.firstChunk = now;
480
+ telemetryRef.current.mark('stream.firstChunk', {
481
+ length: text.length,
482
+ latencyMs: requestStart ? now - requestStart : undefined,
483
+ });
484
+ console.log('[CHATBOT_STREAM_TIMING] first_chunk', {
485
+ at: now,
486
+ conversationId,
487
+ streamId: effectiveStreamId,
488
+ latencyMs: requestStart ? now - requestStart : null,
489
+ });
490
+ firstStreamChunkLoggedRef.current = true;
491
+ }
492
+
493
+ const streamMessageId = streamingMessageId ?? `stream-${effectiveStreamId}`;
494
+
495
+ setStreamingMessageId(streamMessageId);
496
+
497
+ setMessages((prev) => {
498
+ const existingIndex = prev.findIndex((m) => m.id === streamMessageId);
499
+ if (existingIndex === -1) {
500
+ return [
501
+ ...prev,
502
+ {
503
+ id: streamMessageId,
504
+ role: 'assistant',
505
+ content: text,
506
+ createdAt: new Date(),
507
+ },
508
+ ];
509
+ }
510
+
511
+ const existing = prev[existingIndex];
512
+ if (existing.content === text) {
513
+ return prev;
514
+ }
515
+
516
+ const updated = [...prev];
517
+ updated[existingIndex] = {
518
+ ...existing,
519
+ content: text,
520
+ };
521
+ return updated;
522
+ });
523
+ }, [conversationId, effectiveStreamId, streamBody, streamingMessageId]);
524
+
525
+ useEffect(() => {
526
+ if (!streamingMessageId) {
527
+ return;
528
+ }
529
+
530
+ if (effectiveStreamId) {
531
+ return;
532
+ }
533
+
534
+ const hasPersistedResponse = messages.some((message) => {
535
+ if (message.id === streamingMessageId || message.role !== 'assistant') {
536
+ return false;
537
+ }
538
+
539
+ const hasText =
540
+ typeof message.content === 'string'
541
+ ? message.content.trim().length > 0
542
+ : Array.isArray(message.content) && message.content.length > 0;
543
+ const hasAttachments =
544
+ Array.isArray(message.attachments) && message.attachments.length > 0;
545
+
546
+ return hasText || hasAttachments;
547
+ });
548
+
549
+ if (!hasPersistedResponse) {
550
+ return;
551
+ }
552
+
553
+ // CRITICAL FIX: Clear timeout on successful completion
554
+ if (errorTimeoutRef.current) {
555
+ clearTimeout(errorTimeoutRef.current);
556
+ errorTimeoutRef.current = null;
557
+ }
558
+
559
+ setMessages((prev) =>
560
+ prev.filter((message) => message.id !== streamingMessageId),
561
+ );
562
+ setStreamingMessageId(null);
563
+ setIsLoading(false);
564
+ pendingSubmissionRef.current = null;
565
+ lastErrorSeenRef.current = null;
566
+ setOptimisticStatus(null);
567
+ if (telemetryRef.current) {
568
+ telemetryRef.current.mark('stream.persisted', {
569
+ streamingMessageId,
570
+ });
571
+ telemetryRef.current.finalize('ok', { streamingMessageId });
572
+ telemetryRef.current = null;
573
+ }
574
+ const persistedAt = Date.now();
575
+ const { requestStart, firstChunk } = streamTimingRef.current;
576
+ console.log('[CHATBOT_STREAM_TIMING] stream_complete', {
577
+ at: persistedAt,
578
+ conversationId,
579
+ streamingMessageId,
580
+ totalMs: requestStart ? persistedAt - requestStart : null,
581
+ postFirstChunkMs:
582
+ firstChunk && requestStart ? persistedAt - firstChunk : null,
583
+ });
584
+ streamTimingRef.current = {};
585
+ firstStreamChunkLoggedRef.current = false;
586
+ }, [conversationId, effectiveStreamId, streamingMessageId, messages]);
587
+
588
+ useEffect(() => {
589
+ if (streamingMessageId) {
590
+ return;
591
+ }
592
+ if (!telemetryRef.current) {
593
+ return;
594
+ }
595
+ if (isLoading) {
596
+ return;
597
+ }
598
+
599
+ telemetryRef.current.finalize('error', {
600
+ reason: 'streamingMessageClearedWithoutPersistedResponse',
601
+ });
602
+ telemetryRef.current = null;
603
+ firstStreamChunkLoggedRef.current = false;
604
+ streamTimingRef.current = {};
605
+ }, [streamingMessageId, isLoading]);
606
+
607
+ useEffect(() => {
608
+ const lastErrorAt = sessionData?.lastErrorAt ?? null;
609
+ if (!lastErrorAt) {
610
+ return;
611
+ }
612
+
613
+ if (lastErrorSeenRef.current && lastErrorSeenRef.current === lastErrorAt) {
614
+ return;
615
+ }
616
+
617
+ lastErrorSeenRef.current = lastErrorAt;
618
+ finalizeStreamFailure(sessionData?.lastErrorMessage ?? undefined);
619
+ }, [
620
+ sessionData?.lastErrorAt,
621
+ sessionData?.lastErrorMessage,
622
+ finalizeStreamFailure,
623
+ ]);
624
+
625
+ useEffect(() => {
626
+ if (!pendingSubmissionRef.current) {
627
+ return;
628
+ }
629
+ // Don't trigger failure if we're still loading (stream just started)
630
+ if (isLoading) {
631
+ return;
632
+ }
633
+ if (sessionData?.activeStreamId || activeStreamId || effectiveStreamId) {
634
+ return;
635
+ }
636
+ finalizeStreamFailure(sessionData?.lastErrorMessage ?? undefined);
637
+ }, [
638
+ sessionData?.activeStreamId,
639
+ sessionData?.lastErrorMessage,
640
+ activeStreamId,
641
+ effectiveStreamId,
642
+ isLoading,
643
+ finalizeStreamFailure,
644
+ ]);
645
+
646
+ const resetMessages = useCallback(() => {
647
+ setMessages([]);
648
+ setActiveStreamId(null);
649
+ setStreamingMessageId(null);
650
+ setIsLoading(false);
651
+ setError(null);
652
+ }, []);
653
+
654
+ return {
655
+ messages,
656
+ input,
657
+ isLoading,
658
+ error,
659
+ handleInputChange,
660
+ handleSubmit,
661
+ resetMessages,
662
+ currentStatus: computedStatus,
663
+ };
664
+ };