@mevdragon/vidfarm-devcli 0.2.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +11 -4
  2. package/SKILL.director.md +97 -0
  3. package/SKILL.platform.md +66 -6
  4. package/dist/src/cli.js +137 -1524
  5. package/dist/src/dev-serve.js +169 -0
  6. package/package.json +5 -24
  7. package/.env.example +0 -60
  8. package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +0 -275
  9. package/auto-create-hyperframe-templates/extractor-system-prompt.md +0 -128
  10. package/auto-create-hyperframe-templates/input-manifest.schema.json +0 -56
  11. package/auto-create-hyperframe-templates/lambda-memory-estimator.md +0 -118
  12. package/auto-create-hyperframe-templates/production-graph.schema.json +0 -314
  13. package/auto-create-hyperframe-templates/runbook.md +0 -198
  14. package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +0 -368
  15. package/auto-create-hyperframe-templates/template-plan.schema.json +0 -311
  16. package/auto-create-hyperframe-templates/template-planner-prompt.md +0 -144
  17. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +0 -599
  18. package/auto-create-templates/extractor-system-prompt.md +0 -119
  19. package/auto-create-templates/production-graph.schema.json +0 -276
  20. package/auto-create-templates/runbook.md +0 -140
  21. package/auto-create-templates/template-plan.schema.json +0 -301
  22. package/auto-create-templates/template-planner-prompt.md +0 -137
  23. package/dist/src/account-pages-legacy.js +0 -9336
  24. package/dist/src/account-pages-legacy.js.map +0 -1
  25. package/dist/src/account-pages.js +0 -61
  26. package/dist/src/account-pages.js.map +0 -1
  27. package/dist/src/app.js +0 -14023
  28. package/dist/src/app.js.map +0 -1
  29. package/dist/src/cli.js.map +0 -1
  30. package/dist/src/composition-runtime.js +0 -581
  31. package/dist/src/composition-runtime.js.map +0 -1
  32. package/dist/src/config.js +0 -140
  33. package/dist/src/config.js.map +0 -1
  34. package/dist/src/context.js +0 -492
  35. package/dist/src/context.js.map +0 -1
  36. package/dist/src/dev-app-legacy.js +0 -694
  37. package/dist/src/dev-app-legacy.js.map +0 -1
  38. package/dist/src/dev-app.js +0 -6
  39. package/dist/src/dev-app.js.map +0 -1
  40. package/dist/src/domain.js +0 -2
  41. package/dist/src/domain.js.map +0 -1
  42. package/dist/src/editor-chat-history.js +0 -82
  43. package/dist/src/editor-chat-history.js.map +0 -1
  44. package/dist/src/editor-chat.js +0 -311
  45. package/dist/src/editor-chat.js.map +0 -1
  46. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  47. package/dist/src/frontend/flockposter-cache-store.js.map +0 -1
  48. package/dist/src/frontend/homepage-client.js +0 -117
  49. package/dist/src/frontend/homepage-client.js.map +0 -1
  50. package/dist/src/frontend/homepage-shared.js +0 -4
  51. package/dist/src/frontend/homepage-shared.js.map +0 -1
  52. package/dist/src/frontend/homepage-store.js +0 -28
  53. package/dist/src/frontend/homepage-store.js.map +0 -1
  54. package/dist/src/frontend/homepage-view.js +0 -260
  55. package/dist/src/frontend/homepage-view.js.map +0 -1
  56. package/dist/src/frontend/page-runtime-client.js +0 -103
  57. package/dist/src/frontend/page-runtime-client.js.map +0 -1
  58. package/dist/src/frontend/page-runtime-store.js +0 -9
  59. package/dist/src/frontend/page-runtime-store.js.map +0 -1
  60. package/dist/src/frontend/sentry.js +0 -42
  61. package/dist/src/frontend/sentry.js.map +0 -1
  62. package/dist/src/frontend/template-editor-chat.js +0 -3767
  63. package/dist/src/frontend/template-editor-chat.js.map +0 -1
  64. package/dist/src/help-page.js +0 -333
  65. package/dist/src/help-page.js.map +0 -1
  66. package/dist/src/homepage.js +0 -919
  67. package/dist/src/homepage.js.map +0 -1
  68. package/dist/src/hyperframes/composition.js +0 -180
  69. package/dist/src/hyperframes/composition.js.map +0 -1
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/index.js.map +0 -1
  72. package/dist/src/instrument.js +0 -30
  73. package/dist/src/instrument.js.map +0 -1
  74. package/dist/src/lib/crypto.js +0 -45
  75. package/dist/src/lib/crypto.js.map +0 -1
  76. package/dist/src/lib/dev-log.js +0 -54
  77. package/dist/src/lib/dev-log.js.map +0 -1
  78. package/dist/src/lib/display-name.js +0 -11
  79. package/dist/src/lib/display-name.js.map +0 -1
  80. package/dist/src/lib/ids.js +0 -24
  81. package/dist/src/lib/ids.js.map +0 -1
  82. package/dist/src/lib/images.js +0 -19
  83. package/dist/src/lib/images.js.map +0 -1
  84. package/dist/src/lib/json.js +0 -15
  85. package/dist/src/lib/json.js.map +0 -1
  86. package/dist/src/lib/template-dna.js +0 -543
  87. package/dist/src/lib/template-dna.js.map +0 -1
  88. package/dist/src/lib/template-paths.js +0 -28
  89. package/dist/src/lib/template-paths.js.map +0 -1
  90. package/dist/src/lib/template-style-options.js +0 -203
  91. package/dist/src/lib/template-style-options.js.map +0 -1
  92. package/dist/src/lib/time.js +0 -7
  93. package/dist/src/lib/time.js.map +0 -1
  94. package/dist/src/lib/video-quality-harness.js +0 -60
  95. package/dist/src/lib/video-quality-harness.js.map +0 -1
  96. package/dist/src/page-runtime.js +0 -2
  97. package/dist/src/page-runtime.js.map +0 -1
  98. package/dist/src/page-shell.js +0 -2393
  99. package/dist/src/page-shell.js.map +0 -1
  100. package/dist/src/primitive-context.js +0 -299
  101. package/dist/src/primitive-context.js.map +0 -1
  102. package/dist/src/primitive-registry.js +0 -2181
  103. package/dist/src/primitive-registry.js.map +0 -1
  104. package/dist/src/primitive-sdk.js +0 -4
  105. package/dist/src/primitive-sdk.js.map +0 -1
  106. package/dist/src/primitives/remotion/html-image.js +0 -30
  107. package/dist/src/primitives/remotion/html-image.js.map +0 -1
  108. package/dist/src/primitives/remotion/media-slideshow.js +0 -60
  109. package/dist/src/primitives/remotion/media-slideshow.js.map +0 -1
  110. package/dist/src/react-page-shell.js +0 -35
  111. package/dist/src/react-page-shell.js.map +0 -1
  112. package/dist/src/ready-post-schedule-component.js +0 -1515
  113. package/dist/src/ready-post-schedule-component.js.map +0 -1
  114. package/dist/src/registry.js +0 -296
  115. package/dist/src/registry.js.map +0 -1
  116. package/dist/src/runtime.js +0 -31
  117. package/dist/src/runtime.js.map +0 -1
  118. package/dist/src/services/api-call-history.js +0 -249
  119. package/dist/src/services/api-call-history.js.map +0 -1
  120. package/dist/src/services/auth.js +0 -152
  121. package/dist/src/services/auth.js.map +0 -1
  122. package/dist/src/services/billing.js +0 -231
  123. package/dist/src/services/billing.js.map +0 -1
  124. package/dist/src/services/chat-threads.js +0 -92
  125. package/dist/src/services/chat-threads.js.map +0 -1
  126. package/dist/src/services/fork-access.js +0 -94
  127. package/dist/src/services/fork-access.js.map +0 -1
  128. package/dist/src/services/ghostcut.js +0 -132
  129. package/dist/src/services/ghostcut.js.map +0 -1
  130. package/dist/src/services/hyperframes.js +0 -1014
  131. package/dist/src/services/hyperframes.js.map +0 -1
  132. package/dist/src/services/job-capacity.js +0 -14
  133. package/dist/src/services/job-capacity.js.map +0 -1
  134. package/dist/src/services/job-logs.js +0 -197
  135. package/dist/src/services/job-logs.js.map +0 -1
  136. package/dist/src/services/jobs.js +0 -136
  137. package/dist/src/services/jobs.js.map +0 -1
  138. package/dist/src/services/media-processing.js +0 -743
  139. package/dist/src/services/media-processing.js.map +0 -1
  140. package/dist/src/services/primitive-media-lambda.js +0 -280
  141. package/dist/src/services/primitive-media-lambda.js.map +0 -1
  142. package/dist/src/services/providers.js +0 -2870
  143. package/dist/src/services/providers.js.map +0 -1
  144. package/dist/src/services/rate-limits.js +0 -237
  145. package/dist/src/services/rate-limits.js.map +0 -1
  146. package/dist/src/services/remotion.js +0 -688
  147. package/dist/src/services/remotion.js.map +0 -1
  148. package/dist/src/services/serverless-auth.js +0 -374
  149. package/dist/src/services/serverless-auth.js.map +0 -1
  150. package/dist/src/services/serverless-jobs.js +0 -1074
  151. package/dist/src/services/serverless-jobs.js.map +0 -1
  152. package/dist/src/services/serverless-provider-keys.js +0 -401
  153. package/dist/src/services/serverless-provider-keys.js.map +0 -1
  154. package/dist/src/services/serverless-records.js +0 -1086
  155. package/dist/src/services/serverless-records.js.map +0 -1
  156. package/dist/src/services/serverless-template-configs.js +0 -67
  157. package/dist/src/services/serverless-template-configs.js.map +0 -1
  158. package/dist/src/services/storage.js +0 -382
  159. package/dist/src/services/storage.js.map +0 -1
  160. package/dist/src/services/template-certification.js +0 -475
  161. package/dist/src/services/template-certification.js.map +0 -1
  162. package/dist/src/services/template-loader.js +0 -99
  163. package/dist/src/services/template-loader.js.map +0 -1
  164. package/dist/src/services/template-runtime-bundles.js +0 -217
  165. package/dist/src/services/template-runtime-bundles.js.map +0 -1
  166. package/dist/src/services/template-sources.js +0 -1017
  167. package/dist/src/services/template-sources.js.map +0 -1
  168. package/dist/src/services/video-normalization.js +0 -2
  169. package/dist/src/services/video-normalization.js.map +0 -1
  170. package/dist/src/services/webhooks.js +0 -62
  171. package/dist/src/services/webhooks.js.map +0 -1
  172. package/dist/src/template-editor-pages.js +0 -6624
  173. package/dist/src/template-editor-pages.js.map +0 -1
  174. package/dist/src/template-editor-shell.js +0 -2713
  175. package/dist/src/template-editor-shell.js.map +0 -1
  176. package/dist/src/template-sdk.js +0 -4
  177. package/dist/src/template-sdk.js.map +0 -1
  178. package/dist/src/worker.js +0 -17
  179. package/dist/src/worker.js.map +0 -1
  180. package/public/assets/homepage-app.js +0 -54
  181. package/public/assets/homepage-client-app.js +0 -80
  182. package/public/assets/page-runtime-client-app.js +0 -94
  183. package/readme.secret.md +0 -89
  184. package/templates/vidfarm_template_0000/README.md +0 -106
  185. package/templates/vidfarm_template_0000/SKILL.md +0 -266
  186. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  187. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  188. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  189. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  190. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  191. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  192. package/templates/vidfarm_template_0000/composition.json +0 -11
  193. package/templates/vidfarm_template_0000/src/lib/images.js +0 -221
  194. package/templates/vidfarm_template_0000/src/remotion/Root.js +0 -33
  195. package/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
  196. package/templates/vidfarm_template_0000/src/sdk.js +0 -3
  197. package/templates/vidfarm_template_0000/src/style-options.js +0 -200
  198. package/templates/vidfarm_template_0000/src/template-dna.js +0 -9
  199. package/templates/vidfarm_template_0000/src/template.js +0 -1566
  200. package/templates/vidfarm_template_0000/template.config.json +0 -21
@@ -1,3767 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { createPortal } from "react-dom";
4
- const MB = 1024 * 1024;
5
- const MY_FILES_UPLOAD_LIMIT_BYTES = 50 * MB;
6
- const CHAT_INLINE_IMAGE_TARGET_BYTES = 2 * MB;
7
- const CHAT_INLINE_IMAGE_HARD_LIMIT_BYTES = 3 * MB;
8
- const CHAT_INLINE_TOTAL_BUDGET_BYTES = Math.floor(3.5 * MB);
9
- const CHAT_INLINE_IMAGE_MAX_EDGE = 2048;
10
- const CHAT_TEMPORARY_UPLOAD_FOLDER = "chat";
11
- function normalizeUploadedAttachment(raw) {
12
- const id = raw.id || raw.attachment_id || raw.file_id || "";
13
- const fileName = raw.fileName || raw.file_name;
14
- const contentType = raw.contentType || raw.content_type;
15
- const viewUrl = raw.viewUrl || raw.view_url || "";
16
- if (!id || !fileName || !contentType || !viewUrl) {
17
- return null;
18
- }
19
- return {
20
- id,
21
- fileName,
22
- contentType,
23
- viewUrl,
24
- sizeBytes: typeof raw.sizeBytes === "number" ? raw.sizeBytes : raw.size_bytes,
25
- folderPath: raw.folderPath ?? raw.folder_path ?? ""
26
- };
27
- }
28
- function isImageAttachment(attachment) {
29
- return attachment.contentType.startsWith("image/");
30
- }
31
- function isVideoAttachment(attachment) {
32
- return attachment.contentType.startsWith("video/");
33
- }
34
- const INLINE_MEDIA_IMAGE_EXTENSIONS = new Set([
35
- "apng",
36
- "avif",
37
- "gif",
38
- "jpeg",
39
- "jpg",
40
- "png",
41
- "svg",
42
- "webp"
43
- ]);
44
- const INLINE_MEDIA_VIDEO_EXTENSIONS = new Set([
45
- "m4v",
46
- "mov",
47
- "mp4",
48
- "mpeg",
49
- "mpg",
50
- "ogv",
51
- "webm"
52
- ]);
53
- const MAX_CHAT_PAYLOAD_STRING_LENGTH = 1200;
54
- const MAX_CHAT_PAYLOAD_ARRAY_ITEMS = 24;
55
- const MAX_CHAT_PAYLOAD_OBJECT_KEYS = 80;
56
- const DATA_URL_PREFIX_REGEX = /^data:([^;,]+)[^,]*,/i;
57
- const SECRET_HEADER_REGEX = /authorization|api[-_]?key|secret|token|vidfarm-api-key/i;
58
- const CHAT_TRANSPORT_COMPACT_CHAR_THRESHOLD = 48_000;
59
- const CHAT_TRANSPORT_RECENT_MESSAGES = 14;
60
- const CHAT_TRANSPORT_MAX_SUMMARY_CHARS = 12_000;
61
- const CHAT_TRANSPORT_MAX_MESSAGE_CHARS = 1_200;
62
- const LOCAL_THREAD_MESSAGE_CACHE_LIMIT = 30;
63
- function formatJsonBody(value) {
64
- if (value === undefined || value === null || value === "") {
65
- return "";
66
- }
67
- if (typeof value === "string") {
68
- try {
69
- return JSON.stringify(JSON.parse(value), null, 2);
70
- }
71
- catch {
72
- return value;
73
- }
74
- }
75
- return JSON.stringify(value, null, 2);
76
- }
77
- function summarizeLargeString(value) {
78
- const dataUrlMatch = DATA_URL_PREFIX_REGEX.exec(value);
79
- if (dataUrlMatch) {
80
- return `[${dataUrlMatch[1]} data URL omitted, ${value.length.toLocaleString()} chars]`;
81
- }
82
- if (value.length > MAX_CHAT_PAYLOAD_STRING_LENGTH) {
83
- return `${value.slice(0, MAX_CHAT_PAYLOAD_STRING_LENGTH)}… [truncated ${value.length.toLocaleString()} chars]`;
84
- }
85
- return value;
86
- }
87
- function sanitizeChatPayload(value, depth = 0) {
88
- if (value === null || value === undefined) {
89
- return value;
90
- }
91
- if (typeof value === "string") {
92
- return summarizeLargeString(value);
93
- }
94
- if (typeof value === "number" || typeof value === "boolean") {
95
- return value;
96
- }
97
- if (typeof value === "bigint") {
98
- return value.toString();
99
- }
100
- if (typeof value === "function" || typeof value === "symbol") {
101
- return undefined;
102
- }
103
- if (depth >= 8) {
104
- return "[nested payload omitted]";
105
- }
106
- if (Array.isArray(value)) {
107
- const items = value
108
- .slice(0, MAX_CHAT_PAYLOAD_ARRAY_ITEMS)
109
- .map((entry) => sanitizeChatPayload(entry, depth + 1));
110
- if (value.length > MAX_CHAT_PAYLOAD_ARRAY_ITEMS) {
111
- items.push(`[${value.length - MAX_CHAT_PAYLOAD_ARRAY_ITEMS} more items omitted]`);
112
- }
113
- return items;
114
- }
115
- if (typeof value === "object") {
116
- const output = {};
117
- const entries = Object.entries(value);
118
- for (const [key, entry] of entries.slice(0, MAX_CHAT_PAYLOAD_OBJECT_KEYS)) {
119
- const sanitized = sanitizeChatPayload(entry, depth + 1);
120
- if (sanitized !== undefined) {
121
- output[key] = sanitized;
122
- }
123
- }
124
- if (entries.length > MAX_CHAT_PAYLOAD_OBJECT_KEYS) {
125
- output.__omitted_keys = entries.length - MAX_CHAT_PAYLOAD_OBJECT_KEYS;
126
- }
127
- return output;
128
- }
129
- return String(value);
130
- }
131
- function sanitizeToolResult(result) {
132
- return sanitizeChatPayload(result);
133
- }
134
- function sanitizeHttpExchange(exchange) {
135
- return {
136
- ...exchange,
137
- request: {
138
- ...exchange.request,
139
- headers: Object.fromEntries(Object.entries(exchange.request.headers).map(([key, value]) => [
140
- key,
141
- SECRET_HEADER_REGEX.test(key) ? "[redacted]" : summarizeLargeString(value)
142
- ])),
143
- body: sanitizeChatPayload(exchange.request.body)
144
- },
145
- result: {
146
- ...exchange.result,
147
- headers: Object.fromEntries(Object.entries(exchange.result.headers).map(([key, value]) => [
148
- key,
149
- SECRET_HEADER_REGEX.test(key) ? "[redacted]" : summarizeLargeString(value)
150
- ])),
151
- body: sanitizeChatPayload(exchange.result.body),
152
- error: exchange.result.error ? summarizeLargeString(exchange.result.error) : exchange.result.error
153
- }
154
- };
155
- }
156
- function buildBrowserDateContext() {
157
- const now = new Date();
158
- let localDateTime = null;
159
- let timeZone = null;
160
- try {
161
- localDateTime = new Intl.DateTimeFormat(undefined, {
162
- dateStyle: "full",
163
- timeStyle: "long"
164
- }).format(now);
165
- }
166
- catch {
167
- localDateTime = now.toString();
168
- }
169
- try {
170
- timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
171
- }
172
- catch {
173
- timeZone = null;
174
- }
175
- return {
176
- currentDateTime: now.toISOString(),
177
- localDateTime,
178
- timeZone
179
- };
180
- }
181
- function escapeHtml(value) {
182
- return value
183
- .replaceAll("&", "&")
184
- .replaceAll("<", "&lt;")
185
- .replaceAll(">", "&gt;")
186
- .replaceAll("\"", "&quot;")
187
- .replaceAll("'", "&#39;");
188
- }
189
- function renderInlineMarkdown(value) {
190
- let html = escapeHtml(value);
191
- html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
192
- html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
193
- html = html.replace(/(^|[^\*])\*([^*]+)\*(?!\*)/g, "$1<em>$2</em>");
194
- html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
195
- html = html.replace(/(^|[\s(])(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/g, '$1<a href="$2" target="_blank" rel="noopener noreferrer">$2</a>');
196
- return html;
197
- }
198
- function renderMarkdownToHtml(value) {
199
- const normalized = value.replace(/\r\n/g, "\n").trim();
200
- if (!normalized) {
201
- return "";
202
- }
203
- const blocks = normalized.split(/\n{2,}/);
204
- return blocks.map((block) => {
205
- const trimmed = block.trim();
206
- if (!trimmed) {
207
- return "";
208
- }
209
- if (trimmed.startsWith("```") && trimmed.endsWith("```")) {
210
- const code = trimmed.replace(/^```[^\n]*\n?/, "").replace(/\n?```$/, "");
211
- return `<pre><code>${escapeHtml(code)}</code></pre>`;
212
- }
213
- const lines = trimmed.split("\n");
214
- const isBulletList = lines.every((line) => /^[-*]\s+/.test(line.trim()));
215
- if (isBulletList) {
216
- const items = lines
217
- .map((line) => line.trim().replace(/^[-*]\s+/, ""))
218
- .map((line) => `<li>${renderInlineMarkdown(line)}</li>`)
219
- .join("");
220
- return `<ul>${items}</ul>`;
221
- }
222
- const isOrderedList = lines.every((line) => /^\d+\.\s+/.test(line.trim()));
223
- if (isOrderedList) {
224
- const items = lines
225
- .map((line) => line.trim().replace(/^\d+\.\s+/, ""))
226
- .map((line) => `<li>${renderInlineMarkdown(line)}</li>`)
227
- .join("");
228
- return `<ol>${items}</ol>`;
229
- }
230
- return `<p>${lines.map((line) => renderInlineMarkdown(line.trim())).join("<br />")}</p>`;
231
- }).filter(Boolean).join("");
232
- }
233
- function titleCaseLabel(value) {
234
- return value
235
- .replace(/[_-]+/g, " ")
236
- .replace(/\s+/g, " ")
237
- .trim()
238
- .replace(/\b\w/g, (char) => char.toUpperCase());
239
- }
240
- function isGenericPathLabel(value) {
241
- return /^item \d+$/i.test(value.trim()) || /^output$/i.test(value.trim());
242
- }
243
- function extractStorageKeyFromUrl(url) {
244
- try {
245
- const parsed = new URL(url);
246
- const queryKey = parsed.searchParams.get("key");
247
- if (queryKey?.trim()) {
248
- return queryKey.trim().replace(/^\/+/, "");
249
- }
250
- return null;
251
- }
252
- catch {
253
- return null;
254
- }
255
- }
256
- function safeDecodeURIComponent(value) {
257
- try {
258
- return decodeURIComponent(value);
259
- }
260
- catch {
261
- return value;
262
- }
263
- }
264
- function extractFilenameFromUrl(url) {
265
- const storageKey = extractStorageKeyFromUrl(url);
266
- if (storageKey) {
267
- const fileName = safeDecodeURIComponent(storageKey.split("/").filter(Boolean).pop() || "");
268
- if (fileName) {
269
- return fileName;
270
- }
271
- }
272
- try {
273
- const pathname = new URL(url).pathname;
274
- return safeDecodeURIComponent(pathname.split("/").filter(Boolean).pop() || "");
275
- }
276
- catch {
277
- return "";
278
- }
279
- }
280
- function labelFromUrl(url) {
281
- const filename = extractFilenameFromUrl(url);
282
- return filename || "Output";
283
- }
284
- function downloadNameFromUrl(url, fallback) {
285
- const filename = extractFilenameFromUrl(url);
286
- if (filename) {
287
- return filename;
288
- }
289
- const normalizedFallback = fallback.trim().replace(/[\\/:*?"<>|]+/g, "-");
290
- return normalizedFallback || "download";
291
- }
292
- function getMediaKindFromUrl(url) {
293
- const fileName = extractFilenameFromUrl(url);
294
- const extension = fileName.split(".").pop()?.trim().toLowerCase() || "";
295
- if (INLINE_MEDIA_IMAGE_EXTENSIONS.has(extension)) {
296
- return "image";
297
- }
298
- if (INLINE_MEDIA_VIDEO_EXTENSIONS.has(extension)) {
299
- return "video";
300
- }
301
- return null;
302
- }
303
- function extractCandidateUrls(value) {
304
- const urls = new Set();
305
- const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
306
- const bareUrlPattern = /(^|[\s(])(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/gm;
307
- let match = null;
308
- while ((match = markdownLinkPattern.exec(value)) !== null) {
309
- urls.add(match[2]);
310
- }
311
- while ((match = bareUrlPattern.exec(value)) !== null) {
312
- urls.add(match[2]);
313
- }
314
- return [...urls];
315
- }
316
- function extractInlineMediaPreviews(text, excludedUrls = []) {
317
- const excluded = new Set(excludedUrls);
318
- const seen = new Set();
319
- const previews = [];
320
- for (const url of extractCandidateUrls(text)) {
321
- if (excluded.has(url) || seen.has(url)) {
322
- continue;
323
- }
324
- const kind = getMediaKindFromUrl(url);
325
- if (!kind) {
326
- continue;
327
- }
328
- seen.add(url);
329
- previews.push({
330
- kind,
331
- url,
332
- label: labelFromUrl(url)
333
- });
334
- }
335
- return previews;
336
- }
337
- function collectLabeledUrls(value, output, seen = new Set(), path = []) {
338
- if (output.length >= 12 || value == null) {
339
- return;
340
- }
341
- if (typeof value === "string") {
342
- const trimmed = value.trim();
343
- if (/^https?:\/\//i.test(trimmed) && !seen.has(trimmed)) {
344
- seen.add(trimmed);
345
- const rawPathLabel = path.length ? path[path.length - 1] || "Output" : "";
346
- const pathLabel = titleCaseLabel(rawPathLabel);
347
- const filenameLabel = labelFromUrl(trimmed);
348
- output.push({
349
- label: filenameLabel !== "Output" ? filenameLabel : pathLabel && !isGenericPathLabel(rawPathLabel) ? pathLabel : filenameLabel,
350
- url: trimmed
351
- });
352
- }
353
- return;
354
- }
355
- if (Array.isArray(value)) {
356
- for (const [index, entry] of value.entries()) {
357
- collectLabeledUrls(entry, output, seen, [...path, `item ${index + 1}`]);
358
- if (output.length >= 12) {
359
- return;
360
- }
361
- }
362
- return;
363
- }
364
- if (typeof value === "object") {
365
- for (const [key, entry] of Object.entries(value)) {
366
- collectLabeledUrls(entry, output, seen, [...path, key]);
367
- if (output.length >= 12) {
368
- return;
369
- }
370
- }
371
- }
372
- }
373
- function buildJobQueuedMessage(job) {
374
- return [
375
- `Started render \`${job.job_id}\`${job.tracer ? ` with tracer \`${job.tracer}\`` : ""}.`,
376
- "If it takes a while, ask for an update later and I can check it for you."
377
- ].join("\n\n");
378
- }
379
- function isBrainstormPrimitiveId(value) {
380
- return typeof value === "string" && value.startsWith("primitive:brainstorm_");
381
- }
382
- function normalizeJobArtifacts(value) {
383
- if (!Array.isArray(value)) {
384
- return [];
385
- }
386
- const artifacts = [];
387
- for (const entry of value) {
388
- if (!entry || typeof entry !== "object") {
389
- continue;
390
- }
391
- const artifact = entry;
392
- const normalized = {
393
- artifact_id: typeof artifact.artifact_id === "string" ? artifact.artifact_id : undefined,
394
- kind: typeof artifact.kind === "string" ? artifact.kind : undefined,
395
- storage_key: typeof artifact.storage_key === "string" ? artifact.storage_key : undefined,
396
- public_url: typeof artifact.public_url === "string" ? artifact.public_url : undefined,
397
- metadata: artifact.metadata && typeof artifact.metadata === "object"
398
- ? artifact.metadata
399
- : null,
400
- created_at: typeof artifact.created_at === "string" ? artifact.created_at : undefined
401
- };
402
- if (normalized.public_url || normalized.storage_key) {
403
- artifacts.push(normalized);
404
- }
405
- }
406
- return artifacts;
407
- }
408
- function artifactLabelFromStorageKey(storageKey) {
409
- if (!storageKey) {
410
- return "JSON result";
411
- }
412
- const name = safeDecodeURIComponent(storageKey.split("/").filter(Boolean).pop() || "result.json");
413
- return name;
414
- }
415
- function artifactFileName(storageKey) {
416
- return safeDecodeURIComponent(storageKey?.split("/").filter(Boolean).pop() || "");
417
- }
418
- function brainstormLabelFromPrimitiveId(primitiveId) {
419
- switch (primitiveId) {
420
- case "primitive:brainstorm_hooks":
421
- return "Hooks JSON";
422
- case "primitive:brainstorm_angles":
423
- return "Angles JSON";
424
- case "primitive:brainstorm_awareness_stages":
425
- return "Awareness JSON";
426
- case "primitive:brainstorm_coldstart":
427
- return "Coldstart JSON";
428
- default:
429
- return "Brainstorm JSON";
430
- }
431
- }
432
- function extractBrainstormJsonArtifactsFromJob(job) {
433
- if (job.status !== "succeeded" || !isBrainstormPrimitiveId(job.primitive_id)) {
434
- return [];
435
- }
436
- const previews = [];
437
- for (const [index, artifact] of normalizeJobArtifacts(job.artifacts).entries()) {
438
- const publicUrl = artifact.public_url?.trim();
439
- const storageKey = artifact.storage_key?.trim();
440
- const isJsonArtifact = artifact.kind === "json"
441
- || Boolean(publicUrl && /\.json(?:[?#]|$)/i.test(publicUrl))
442
- || Boolean(storageKey && /\.json$/i.test(storageKey));
443
- if (!isJsonArtifact || !publicUrl) {
444
- continue;
445
- }
446
- const fileLabel = artifactLabelFromStorageKey(artifact.storage_key);
447
- const prefixLabel = brainstormLabelFromPrimitiveId(job.primitive_id);
448
- previews.push({
449
- key: artifact.artifact_id || `${job.job_id}:${publicUrl}:${index}`,
450
- url: publicUrl,
451
- label: fileLabel === "result.json" ? prefixLabel : `${prefixLabel} • ${fileLabel}`,
452
- primitiveId: job.primitive_id,
453
- primitiveType: job.primitive_type,
454
- jobId: job.job_id
455
- });
456
- }
457
- return previews;
458
- }
459
- function brainstormDocumentLabelFromPrimitiveId(primitiveId) {
460
- switch (primitiveId) {
461
- case "primitive:brainstorm_awareness_stages":
462
- return "Awareness response";
463
- default:
464
- return "Brainstorm response";
465
- }
466
- }
467
- function extractBrainstormDocumentArtifactsFromJob(job) {
468
- if (job.status !== "succeeded" || !isBrainstormPrimitiveId(job.primitive_id)) {
469
- return [];
470
- }
471
- const previews = [];
472
- for (const [index, artifact] of normalizeJobArtifacts(job.artifacts).entries()) {
473
- const publicUrl = artifact.public_url?.trim();
474
- const storageKey = artifact.storage_key?.trim();
475
- const fileName = artifactFileName(storageKey);
476
- const isPrompt = /^prompt\./i.test(fileName);
477
- const isMarkdown = artifact.kind === "text"
478
- ? Boolean(publicUrl && /\.md(?:[?#]|$)/i.test(publicUrl) || storageKey && /\.md$/i.test(storageKey))
479
- : Boolean(publicUrl && /\.md(?:[?#]|$)/i.test(publicUrl) || storageKey && /\.md$/i.test(storageKey));
480
- const isText = Boolean(publicUrl && /\.txt(?:[?#]|$)/i.test(publicUrl) || storageKey && /\.txt$/i.test(storageKey));
481
- if (!publicUrl || isPrompt || (!isMarkdown && !isText)) {
482
- continue;
483
- }
484
- const defaultLabel = brainstormDocumentLabelFromPrimitiveId(job.primitive_id);
485
- previews.push({
486
- key: artifact.artifact_id || `${job.job_id}:${publicUrl}:${index}`,
487
- url: publicUrl,
488
- label: fileName === "response.md" || fileName === "response.txt" ? defaultLabel : artifactLabelFromStorageKey(storageKey),
489
- jobId: job.job_id,
490
- format: isMarkdown ? "markdown" : "text"
491
- });
492
- }
493
- return previews;
494
- }
495
- function buildJobCompletionMessage(job) {
496
- if (job.status === "succeeded") {
497
- const urls = [];
498
- collectLabeledUrls(job.result, urls);
499
- const lines = [`Render \`${job.job_id}\` succeeded.`];
500
- if (isBrainstormPrimitiveId(job.primitive_id)
501
- && (extractBrainstormJsonArtifactsFromJob(job).length || extractBrainstormDocumentArtifactsFromJob(job).length)) {
502
- lines.push("");
503
- lines.push("Open the result below. It contains the brainstorm output you can review, copy, and reuse.");
504
- }
505
- if (urls.length) {
506
- lines.push("");
507
- lines.push("Outputs:");
508
- lines.push(...urls.map(({ url, label }) => `- [${label}](${url})`));
509
- }
510
- return lines.join("\n");
511
- }
512
- if (job.status === "failed") {
513
- return `Render \`${job.job_id}\` failed.${job.error?.message ? ` ${job.error.message}` : ""}`;
514
- }
515
- if (job.status === "cancelled") {
516
- return `Render \`${job.job_id}\` was cancelled.`;
517
- }
518
- return `Render \`${job.job_id}\` is ${job.status}.`;
519
- }
520
- function appendDistinctMessageText(current, addition) {
521
- const normalizedCurrent = current.trim();
522
- const normalizedAddition = addition.trim();
523
- if (!normalizedAddition) {
524
- return current;
525
- }
526
- if (normalizedCurrent.includes(normalizedAddition)) {
527
- return current;
528
- }
529
- return normalizedCurrent ? `${normalizedCurrent}\n\n${normalizedAddition}` : normalizedAddition;
530
- }
531
- const CHAT_BOTTOM_STICKY_THRESHOLD_PX = 48;
532
- function isScrolledNearBottom(element) {
533
- return element.scrollHeight - element.scrollTop - element.clientHeight <= CHAT_BOTTOM_STICKY_THRESHOLD_PX;
534
- }
535
- function upsertAutomaticPollExchange(exchanges, nextExchange) {
536
- const current = [...(exchanges ?? [])];
537
- const matchIndex = current.findIndex((exchange) => (exchange.pollMode === "automatic" && exchange.pollJobId === nextExchange.pollJobId));
538
- if (matchIndex === -1) {
539
- current.push(nextExchange);
540
- return current;
541
- }
542
- current[matchIndex] = nextExchange;
543
- return current;
544
- }
545
- const JOB_POLL_INTERVAL_MS = 30_000;
546
- const JOB_POLL_BUSY_INTERVAL_MS = 180_000;
547
- const JOB_POLL_BUSY_THRESHOLD = 4;
548
- const JOB_POLL_MAX_ATTEMPTS = 40;
549
- const JOB_POLL_GLOBAL_MIN_GAP_MS = 1_500;
550
- const JOB_POLL_MIN_TIMER_MS = 250;
551
- function tryReadJobStatusPayload(value) {
552
- if (!value || typeof value !== "object") {
553
- return null;
554
- }
555
- const record = value;
556
- if (typeof record.job_id !== "string" || typeof record.status !== "string") {
557
- return null;
558
- }
559
- return {
560
- job_id: record.job_id,
561
- tracer: typeof record.tracer === "string" ? record.tracer : undefined,
562
- status: record.status,
563
- primitive_id: typeof record.primitive_id === "string" ? record.primitive_id : undefined,
564
- primitive_type: typeof record.primitive_type === "string" ? record.primitive_type : undefined,
565
- operation_name: typeof record.operation_name === "string" ? record.operation_name : undefined,
566
- progress: typeof record.progress === "number" ? record.progress : null,
567
- result: record.result,
568
- artifacts: normalizeJobArtifacts(record.artifacts),
569
- error: record.error && typeof record.error === "object"
570
- ? {
571
- message: typeof record.error.message === "string"
572
- ? record.error.message
573
- : undefined,
574
- type: typeof record.error.type === "string"
575
- ? record.error.type
576
- : undefined
577
- }
578
- : null
579
- };
580
- }
581
- function isPrimitiveRestUrl(value) {
582
- if (!value) {
583
- return false;
584
- }
585
- try {
586
- const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
587
- return new URL(value, base).pathname.startsWith("/api/v1/primitives");
588
- }
589
- catch {
590
- return false;
591
- }
592
- }
593
- function isTerminalJobStatus(status) {
594
- return status === "succeeded" || status === "failed" || status === "cancelled";
595
- }
596
- function makeJobPollerKey(threadId, assistantMessageId, jobId) {
597
- return `${threadId}::${assistantMessageId}::${jobId}`;
598
- }
599
- function buildAuthHeaders(apiKey, contentType) {
600
- const headers = new Headers();
601
- if (apiKey) {
602
- headers.set("vidfarm-api-key", apiKey);
603
- }
604
- if (contentType) {
605
- headers.set("content-type", contentType);
606
- }
607
- return headers;
608
- }
609
- async function parseJson(response) {
610
- return await response.json();
611
- }
612
- async function fileToDataUrl(file) {
613
- return await new Promise((resolve, reject) => {
614
- const reader = new FileReader();
615
- reader.onload = () => {
616
- if (typeof reader.result === "string") {
617
- resolve(reader.result);
618
- return;
619
- }
620
- reject(new Error("Unable to read attachment contents."));
621
- };
622
- reader.onerror = () => reject(reader.error ?? new Error("Unable to read attachment contents."));
623
- reader.readAsDataURL(file);
624
- });
625
- }
626
- async function blobToDataUrl(blob) {
627
- return await new Promise((resolve, reject) => {
628
- const reader = new FileReader();
629
- reader.onload = () => {
630
- if (typeof reader.result === "string") {
631
- resolve(reader.result);
632
- return;
633
- }
634
- reject(new Error("Unable to read compressed attachment contents."));
635
- };
636
- reader.onerror = () => reject(reader.error ?? new Error("Unable to read compressed attachment contents."));
637
- reader.readAsDataURL(blob);
638
- });
639
- }
640
- function estimateDataUrlBytes(value) {
641
- if (!value) {
642
- return 0;
643
- }
644
- const marker = ";base64,";
645
- const markerIndex = value.indexOf(marker);
646
- if (markerIndex < 0) {
647
- return value.length;
648
- }
649
- const base64 = value.slice(markerIndex + marker.length).replace(/\s/g, "");
650
- const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
651
- return Math.max(0, Math.floor(base64.length * 3 / 4) - padding);
652
- }
653
- function estimateInlineAttachmentBytes(attachments) {
654
- return attachments.reduce((total, attachment) => total + estimateDataUrlBytes(attachment.transportData), 0);
655
- }
656
- async function canvasToBlob(canvas, contentType, quality) {
657
- return await new Promise((resolve) => {
658
- canvas.toBlob((blob) => resolve(blob), contentType, quality);
659
- });
660
- }
661
- async function decodeImageForCanvas(file) {
662
- if (typeof createImageBitmap === "function") {
663
- try {
664
- const bitmap = await createImageBitmap(file);
665
- return {
666
- source: bitmap,
667
- width: bitmap.width,
668
- height: bitmap.height,
669
- close: () => bitmap.close()
670
- };
671
- }
672
- catch {
673
- // Fall back to HTMLImageElement decoding below.
674
- }
675
- }
676
- return await new Promise((resolve) => {
677
- const image = new Image();
678
- const objectUrl = URL.createObjectURL(file);
679
- image.onload = () => {
680
- URL.revokeObjectURL(objectUrl);
681
- resolve({
682
- source: image,
683
- width: image.naturalWidth || image.width,
684
- height: image.naturalHeight || image.height
685
- });
686
- };
687
- image.onerror = () => {
688
- URL.revokeObjectURL(objectUrl);
689
- resolve(null);
690
- };
691
- image.src = objectUrl;
692
- });
693
- }
694
- async function createCompressedImageDataUrl(file, remainingBudgetBytes) {
695
- if (!file.type.startsWith("image/") || remainingBudgetBytes <= 0) {
696
- return undefined;
697
- }
698
- const hardLimitBytes = Math.min(CHAT_INLINE_IMAGE_HARD_LIMIT_BYTES, remainingBudgetBytes);
699
- const targetBytes = Math.min(CHAT_INLINE_IMAGE_TARGET_BYTES, hardLimitBytes);
700
- if (file.size <= targetBytes) {
701
- return fileToDataUrl(file);
702
- }
703
- const decoded = await decodeImageForCanvas(file);
704
- if (!decoded || decoded.width <= 0 || decoded.height <= 0) {
705
- return file.size <= hardLimitBytes ? fileToDataUrl(file) : undefined;
706
- }
707
- try {
708
- let bestBlob = null;
709
- const maxEdges = [CHAT_INLINE_IMAGE_MAX_EDGE, 1600, 1280, 1024];
710
- const qualities = [0.86, 0.78, 0.7, 0.62, 0.54];
711
- for (const maxEdge of maxEdges) {
712
- const scale = Math.min(1, maxEdge / Math.max(decoded.width, decoded.height));
713
- const width = Math.max(1, Math.round(decoded.width * scale));
714
- const height = Math.max(1, Math.round(decoded.height * scale));
715
- const canvas = document.createElement("canvas");
716
- canvas.width = width;
717
- canvas.height = height;
718
- const context = canvas.getContext("2d");
719
- if (!context) {
720
- continue;
721
- }
722
- context.fillStyle = "#fff";
723
- context.fillRect(0, 0, width, height);
724
- context.drawImage(decoded.source, 0, 0, width, height);
725
- for (const quality of qualities) {
726
- const blob = await canvasToBlob(canvas, "image/jpeg", quality);
727
- if (!blob) {
728
- continue;
729
- }
730
- if (blob.size <= hardLimitBytes && (!bestBlob || blob.size < bestBlob.size)) {
731
- bestBlob = blob;
732
- }
733
- if (blob.size <= targetBytes) {
734
- return blobToDataUrl(blob);
735
- }
736
- }
737
- }
738
- if (bestBlob && (file.size > hardLimitBytes || bestBlob.size < file.size)) {
739
- return blobToDataUrl(bestBlob);
740
- }
741
- return file.size <= hardLimitBytes ? fileToDataUrl(file) : undefined;
742
- }
743
- finally {
744
- decoded.close?.();
745
- }
746
- }
747
- async function addInlinePreviewForChatUpload(input) {
748
- const remainingBudgetBytes = CHAT_INLINE_TOTAL_BUDGET_BYTES - input.currentInlineBytes;
749
- const transportData = await createCompressedImageDataUrl(input.file, remainingBudgetBytes).catch(() => undefined);
750
- return transportData ? { ...input.attachment, transportData } : input.attachment;
751
- }
752
- async function uploadAttachment(input) {
753
- const maxBytes = input.maxBytes ?? MY_FILES_UPLOAD_LIMIT_BYTES;
754
- const limitLabel = input.limitLabel ?? "50 MB";
755
- if (input.file.size > maxBytes) {
756
- throw new Error(`File uploads can be at most ${limitLabel}.`);
757
- }
758
- const uploadFormData = new FormData();
759
- uploadFormData.append("file", input.file);
760
- uploadFormData.append("folder_path", input.folderPath || "");
761
- const uploadResponse = await fetch("/api/v1/user/me/attachments/upload", {
762
- method: "POST",
763
- headers: buildAuthHeaders(input.apiKey),
764
- body: uploadFormData
765
- });
766
- const uploadPayload = await parseJson(uploadResponse);
767
- if (!uploadResponse.ok || !uploadPayload.attachment) {
768
- throw new Error(uploadPayload.error || "Unable to upload attachment.");
769
- }
770
- return normalizeUploadedAttachment(uploadPayload.attachment) ?? (() => {
771
- throw new Error("Attachment response was incomplete.");
772
- })();
773
- }
774
- async function listMyFiles(apiKey) {
775
- const response = await fetch("/api/v1/user/me/attachments", {
776
- headers: buildAuthHeaders(apiKey)
777
- });
778
- const payload = await parseJson(response);
779
- if (!response.ok) {
780
- throw new Error(payload.error || "Unable to load My Files.");
781
- }
782
- const attachments = [];
783
- for (const entry of payload.attachments ?? []) {
784
- const normalized = normalizeUploadedAttachment(entry);
785
- if (normalized) {
786
- attachments.push({ ...normalized, createdAt: entry.createdAt ?? entry.created_at });
787
- }
788
- }
789
- return {
790
- attachments,
791
- folders: payload.folders ?? []
792
- };
793
- }
794
- async function uploadTemporaryFile(input) {
795
- if (input.file.size > 100 * 1024 * 1024) {
796
- throw new Error("Temporary files can be at most 100 MB.");
797
- }
798
- const presignResponse = await fetch("/api/v1/user/me/temporary-files/presign", {
799
- method: "POST",
800
- headers: buildAuthHeaders(input.apiKey, "application/json"),
801
- body: JSON.stringify({
802
- file_name: input.file.name,
803
- content_type: input.file.type || undefined,
804
- size_bytes: input.file.size,
805
- folder_path: input.folderPath || undefined
806
- })
807
- });
808
- const presignPayload = await parseJson(presignResponse);
809
- if (!presignResponse.ok || !("transport" in presignPayload)) {
810
- throw new Error(("error" in presignPayload && presignPayload.error) || "Unable to prepare temporary upload.");
811
- }
812
- if (presignPayload.transport === "server") {
813
- const uploadFormData = new FormData();
814
- uploadFormData.append(presignPayload.upload.form_field || "file", input.file);
815
- uploadFormData.append("folder_path", input.folderPath || "");
816
- const uploadResponse = await fetch(presignPayload.upload.url, {
817
- method: presignPayload.upload.method || "POST",
818
- headers: buildAuthHeaders(input.apiKey),
819
- body: uploadFormData
820
- });
821
- const uploadPayload = await parseJson(uploadResponse);
822
- if (!uploadResponse.ok || !uploadPayload.file) {
823
- throw new Error(uploadPayload.error || "Unable to upload temporary file.");
824
- }
825
- const normalized = normalizeUploadedAttachment(uploadPayload.file);
826
- if (!normalized) {
827
- throw new Error("Temporary file response was incomplete.");
828
- }
829
- return {
830
- ...normalized,
831
- createdAt: uploadPayload.file.createdAt ?? uploadPayload.file.created_at,
832
- expiresAt: uploadPayload.file.expiresAt ?? uploadPayload.file.expires_at,
833
- s3Url: uploadPayload.file.s3Url ?? uploadPayload.file.s3_url
834
- };
835
- }
836
- const uploadResponse = await fetch(presignPayload.upload.url, {
837
- method: presignPayload.upload.method,
838
- headers: presignPayload.upload.headers,
839
- body: input.file
840
- });
841
- if (!uploadResponse.ok) {
842
- throw new Error(`Upload failed with status ${uploadResponse.status}.`);
843
- }
844
- const finalizeResponse = await fetch("/api/v1/user/me/temporary-files", {
845
- method: "POST",
846
- headers: buildAuthHeaders(input.apiKey, "application/json"),
847
- body: JSON.stringify({
848
- file_id: presignPayload.file_id,
849
- file_name: presignPayload.file_name,
850
- content_type: presignPayload.content_type,
851
- size_bytes: input.file.size,
852
- storage_key: presignPayload.storage_key,
853
- folder_path: presignPayload.folder_path || input.folderPath || ""
854
- })
855
- });
856
- const finalizePayload = await parseJson(finalizeResponse);
857
- if (!finalizeResponse.ok || !finalizePayload.file) {
858
- throw new Error(finalizePayload.error || "Unable to save temporary file.");
859
- }
860
- const normalized = normalizeUploadedAttachment(finalizePayload.file);
861
- if (!normalized) {
862
- throw new Error("Temporary file response was incomplete.");
863
- }
864
- return {
865
- ...normalized,
866
- createdAt: finalizePayload.file.createdAt ?? finalizePayload.file.created_at,
867
- expiresAt: finalizePayload.file.expiresAt ?? finalizePayload.file.expires_at,
868
- s3Url: finalizePayload.file.s3Url ?? finalizePayload.file.s3_url
869
- };
870
- }
871
- async function listTemporaryFiles(apiKey) {
872
- const response = await fetch("/api/v1/user/me/temporary-files", {
873
- headers: buildAuthHeaders(apiKey)
874
- });
875
- const payload = await parseJson(response);
876
- if (!response.ok) {
877
- throw new Error(payload.error || "Unable to load temporary files.");
878
- }
879
- const files = [];
880
- for (const entry of payload.files ?? []) {
881
- const normalized = normalizeUploadedAttachment(entry);
882
- if (normalized) {
883
- files.push({
884
- ...normalized,
885
- createdAt: entry.createdAt ?? entry.created_at,
886
- expiresAt: entry.expiresAt ?? entry.expires_at,
887
- s3Url: entry.s3Url ?? entry.s3_url
888
- });
889
- }
890
- }
891
- return {
892
- files,
893
- folders: payload.folders ?? []
894
- };
895
- }
896
- async function listGeneratedMedia(apiKey, cursor) {
897
- const url = new URL("/api/v1/user/me/generated-media", window.location.origin);
898
- url.searchParams.set("limit", "24");
899
- if (cursor) {
900
- url.searchParams.set("cursor", cursor);
901
- }
902
- const response = await fetch(`${url.pathname}${url.search}`, {
903
- headers: buildAuthHeaders(apiKey)
904
- });
905
- const payload = await parseJson(response);
906
- if (!response.ok) {
907
- throw new Error(payload.error || "Unable to load recent media.");
908
- }
909
- const media = [];
910
- for (const entry of payload.media ?? []) {
911
- const normalized = normalizeUploadedAttachment(entry);
912
- if (normalized) {
913
- media.push({
914
- ...normalized,
915
- createdAt: entry.createdAt ?? entry.created_at,
916
- jobId: typeof entry.job_id === "string" ? entry.job_id : undefined,
917
- tracer: typeof entry.tracer === "string" ? entry.tracer : undefined,
918
- templateId: typeof entry.template_id === "string" ? entry.template_id : undefined
919
- });
920
- }
921
- }
922
- return {
923
- media,
924
- nextCursor: payload.next_cursor ?? null
925
- };
926
- }
927
- function estimateChatMessageChars(messages) {
928
- return messages.reduce((total, message) => {
929
- const attachmentChars = message.attachments.reduce((sum, attachment) => (sum + attachment.fileName.length + attachment.contentType.length + 32), 0);
930
- return total + message.text.length + attachmentChars;
931
- }, 0);
932
- }
933
- function truncateTransportSummaryText(value, maxChars) {
934
- const normalized = value.trim().replace(/\s+/g, " ");
935
- if (normalized.length <= maxChars) {
936
- return normalized;
937
- }
938
- return `${normalized.slice(0, Math.max(0, maxChars - 32)).trimEnd()}… [truncated]`;
939
- }
940
- function compactTransportHistory(messages) {
941
- if (messages.length <= CHAT_TRANSPORT_RECENT_MESSAGES
942
- || estimateChatMessageChars(messages) <= CHAT_TRANSPORT_COMPACT_CHAR_THRESHOLD) {
943
- return messages;
944
- }
945
- const recentMessages = messages.slice(-CHAT_TRANSPORT_RECENT_MESSAGES);
946
- const oldMessages = messages.slice(0, -CHAT_TRANSPORT_RECENT_MESSAGES);
947
- const summaryLines = [];
948
- let summaryChars = 0;
949
- for (const message of oldMessages) {
950
- const attachmentsText = message.attachments.length
951
- ? ` Attachments: ${message.attachments.map((attachment) => `${attachment.fileName} (${attachment.contentType})`).join(", ")}.`
952
- : "";
953
- const line = `[${message.role}]: ${truncateTransportSummaryText(`${message.text}${attachmentsText}`, CHAT_TRANSPORT_MAX_MESSAGE_CHARS)}`;
954
- if (summaryChars + line.length > CHAT_TRANSPORT_MAX_SUMMARY_CHARS) {
955
- summaryLines.push(`[${oldMessages.length - summaryLines.length} earlier messages omitted]`);
956
- break;
957
- }
958
- summaryLines.push(line);
959
- summaryChars += line.length;
960
- }
961
- return [
962
- {
963
- id: makeMessageId("assistant"),
964
- role: "assistant",
965
- text: [
966
- "[Earlier in this conversation - compacted summary]",
967
- "Older turns were summarized before sending this request so the chat can continue efficiently.",
968
- "",
969
- summaryLines.join("\n\n")
970
- ].join("\n"),
971
- attachments: []
972
- },
973
- ...recentMessages
974
- ];
975
- }
976
- function buildAttachmentTransportText(attachments) {
977
- if (!attachments.length) {
978
- return null;
979
- }
980
- const lines = attachments.map((attachment) => (`- ${attachment.fileName} (${attachment.contentType}): ${attachment.viewUrl}`));
981
- return [
982
- "Attached file URLs for REST payloads:",
983
- ...lines,
984
- "Use these exact URLs as prompt_attachments for primitive image generation, source_image_url for primitive image edits, media_url for primitive video slides, or reference attachment URLs when calling template routes."
985
- ].join("\n");
986
- }
987
- function toTransportMessages(messages) {
988
- const latestIndex = messages.length - 1;
989
- return messages.map((message, messageIndex) => {
990
- const attachmentText = buildAttachmentTransportText(message.attachments);
991
- return {
992
- role: message.role,
993
- content: [
994
- ...(message.text ? [{ type: "text", text: message.text }] : []),
995
- ...(attachmentText ? [{ type: "text", text: attachmentText }] : []),
996
- ...message.attachments.map((attachment) => ({
997
- type: "file",
998
- data: messageIndex === latestIndex ? attachment.transportData || attachment.viewUrl : attachment.viewUrl,
999
- mediaType: attachment.contentType
1000
- }))
1001
- ]
1002
- };
1003
- });
1004
- }
1005
- function readCachedScheduleChannels() {
1006
- const cache = window.__vidfarmFlockPosterCache;
1007
- const snapshot = cache?.readSnapshot?.();
1008
- if (!snapshot) {
1009
- return {};
1010
- }
1011
- const channels = Array.isArray(snapshot?.channels)
1012
- ? snapshot.channels
1013
- .map((channel) => channel && typeof channel === "object" ? channel : null)
1014
- .filter((channel) => typeof channel?.id === "string" && Boolean(channel.id.trim()))
1015
- .map((channel) => ({
1016
- id: String(channel?.id || ""),
1017
- name: typeof channel?.name === "string" ? channel.name : null,
1018
- title: typeof channel?.title === "string" ? channel.title : null,
1019
- handle: typeof channel?.handle === "string" ? channel.handle : null,
1020
- platform: typeof channel?.platform === "string" ? channel.platform : null,
1021
- status: typeof channel?.status === "string" ? channel.status : null
1022
- }))
1023
- : [];
1024
- return channels.length || snapshot?.channelsUpdatedAt
1025
- ? {
1026
- scheduleChannels: channels,
1027
- scheduleChannelsUpdatedAt: snapshot?.channelsUpdatedAt ?? null
1028
- }
1029
- : {};
1030
- }
1031
- function readSelectedLibraryPosts() {
1032
- const snapshot = window.__vidfarmLibrarySelection?.readSelectedPosts?.();
1033
- const posts = Array.isArray(snapshot?.posts)
1034
- ? snapshot.posts
1035
- .map((post) => post && typeof post === "object" ? post : null)
1036
- .filter((post) => typeof post?.post_id === "string" && Boolean(post.post_id.trim()))
1037
- .map((post) => ({
1038
- post_id: String(post?.post_id || ""),
1039
- title: typeof post?.title === "string" ? post.title : null,
1040
- caption: typeof post?.caption === "string" ? post.caption : null,
1041
- tracer: typeof post?.tracer === "string" ? post.tracer : null,
1042
- status: typeof post?.status === "string" ? post.status : null,
1043
- share_url: typeof post?.share_url === "string" ? post.share_url : null,
1044
- media: Array.isArray(post?.media)
1045
- ? post.media.slice(0, 8).map((asset) => asset && typeof asset === "object" ? asset : null).filter(Boolean).map((asset) => ({
1046
- url: typeof asset?.url === "string" ? asset.url : null,
1047
- kind: typeof asset?.kind === "string" ? asset.kind : null,
1048
- role: typeof asset?.role === "string" ? asset.role : null,
1049
- content_type: typeof asset?.content_type === "string" ? asset.content_type : null
1050
- }))
1051
- : []
1052
- }))
1053
- : [];
1054
- return {
1055
- selectedPosts: posts,
1056
- selectedPostsUpdatedAt: snapshot?.updatedAt ?? null
1057
- };
1058
- }
1059
- async function readStreamText(response, handlers) {
1060
- if (!response.body) {
1061
- throw new Error("Chat response did not include a body.");
1062
- }
1063
- const reader = response.body.getReader();
1064
- const decoder = new TextDecoder();
1065
- let buffer = "";
1066
- while (true) {
1067
- const { done, value } = await reader.read();
1068
- if (done) {
1069
- break;
1070
- }
1071
- buffer += decoder.decode(value, { stream: true });
1072
- const events = buffer.split("\n\n");
1073
- buffer = events.pop() ?? "";
1074
- for (const event of events) {
1075
- const line = event
1076
- .split("\n")
1077
- .map((entry) => entry.trim())
1078
- .find((entry) => entry.startsWith("data:"));
1079
- if (!line) {
1080
- continue;
1081
- }
1082
- const payload = line.slice(5).trim();
1083
- if (payload === "[DONE]") {
1084
- continue;
1085
- }
1086
- let parsed;
1087
- try {
1088
- parsed = JSON.parse(payload);
1089
- }
1090
- catch {
1091
- continue;
1092
- }
1093
- if (!parsed || typeof parsed !== "object") {
1094
- continue;
1095
- }
1096
- const chunk = parsed;
1097
- if (chunk.type === "text-delta" && typeof chunk.delta === "string") {
1098
- handlers.onDelta(chunk.delta);
1099
- }
1100
- else if (chunk.type === "status") {
1101
- const statusMessage = typeof chunk.status === "string"
1102
- ? chunk.status
1103
- : typeof chunk.message === "string"
1104
- ? chunk.message
1105
- : null;
1106
- if (statusMessage && handlers.onStatus) {
1107
- handlers.onStatus(statusMessage);
1108
- }
1109
- }
1110
- else if (chunk.type === "tool-call"
1111
- && typeof chunk.toolCallId === "string"
1112
- && typeof chunk.toolName === "string"
1113
- && handlers.onToolCall) {
1114
- handlers.onToolCall({
1115
- toolCallId: chunk.toolCallId,
1116
- toolName: chunk.toolName,
1117
- args: chunk.args && typeof chunk.args === "object" ? chunk.args : undefined
1118
- });
1119
- }
1120
- else if (chunk.type === "tool-result"
1121
- && typeof chunk.toolCallId === "string"
1122
- && typeof chunk.toolName === "string"
1123
- && handlers.onToolResult) {
1124
- handlers.onToolResult({
1125
- toolCallId: chunk.toolCallId,
1126
- toolName: chunk.toolName,
1127
- result: chunk.result
1128
- });
1129
- }
1130
- else if (chunk.type === "error" && handlers.onError) {
1131
- handlers.onError?.(typeof chunk.errorText === "string"
1132
- ? chunk.errorText
1133
- : typeof chunk.error?.message === "string"
1134
- ? chunk.error.message
1135
- : "Unable to read assistant response.");
1136
- }
1137
- }
1138
- }
1139
- }
1140
- function makeStorageKeys(templateId) {
1141
- return {
1142
- threads: `vidfarm-editor-chat-threads:${templateId}`,
1143
- activeThread: `vidfarm-editor-chat-active-thread:${templateId}`
1144
- };
1145
- }
1146
- function getChatStorage() {
1147
- if (typeof window === "undefined") {
1148
- return null;
1149
- }
1150
- try {
1151
- return window.sessionStorage;
1152
- }
1153
- catch {
1154
- try {
1155
- return window.localStorage;
1156
- }
1157
- catch {
1158
- return null;
1159
- }
1160
- }
1161
- }
1162
- function makeThreadId() {
1163
- return `thread-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1164
- }
1165
- function makeMessageId(prefix) {
1166
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1167
- return `${prefix}-${crypto.randomUUID()}`;
1168
- }
1169
- return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1170
- }
1171
- function normalizeTracerList(values) {
1172
- const seen = new Set();
1173
- const tracers = [];
1174
- for (const value of values) {
1175
- const tracer = value.trim();
1176
- if (!tracer || seen.has(tracer)) {
1177
- continue;
1178
- }
1179
- seen.add(tracer);
1180
- tracers.push(tracer);
1181
- }
1182
- return tracers;
1183
- }
1184
- function mergeTracerLists(...groups) {
1185
- return normalizeTracerList(groups.flat());
1186
- }
1187
- function archiveTracerValue(tracer) {
1188
- const normalized = tracer.trim();
1189
- if (!normalized || normalized.startsWith("archived_")) {
1190
- return normalized;
1191
- }
1192
- return `archived_${normalized}`;
1193
- }
1194
- function makeTracerSuffix() {
1195
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1196
- return crypto.randomUUID().replace(/-/g, "").slice(-8);
1197
- }
1198
- return Math.random().toString(16).slice(2, 10).padEnd(8, "0").slice(0, 8);
1199
- }
1200
- function slugifyTracerPrefix(value) {
1201
- const normalized = value
1202
- .trim()
1203
- .toLowerCase()
1204
- .replace(/[^a-z0-9]+/g, "-")
1205
- .replace(/^-+|-+$/g, "");
1206
- return (normalized || "tracer").slice(0, 28);
1207
- }
1208
- function makeTracerId(templateSlug, existing) {
1209
- const prefix = slugifyTracerPrefix(templateSlug);
1210
- let candidate = "";
1211
- do {
1212
- candidate = `${prefix}-${makeTracerSuffix()}`;
1213
- } while (existing.includes(candidate));
1214
- return candidate;
1215
- }
1216
- function ensureTracerHasRandomSuffix(tracer, existing) {
1217
- const normalized = tracer.trim();
1218
- if (!normalized) {
1219
- return "";
1220
- }
1221
- if (/-[a-z0-9]{8}$/i.test(normalized)) {
1222
- return normalized;
1223
- }
1224
- let candidate = `${normalized}-${makeTracerSuffix()}`;
1225
- while (existing.includes(candidate)) {
1226
- candidate = `${normalized}-${makeTracerSuffix()}`;
1227
- }
1228
- return candidate;
1229
- }
1230
- function readTracersFromLocation() {
1231
- const params = new URLSearchParams(window.location.search);
1232
- const values = [...params.getAll("tracers"), ...params.getAll("tracer")];
1233
- const output = [];
1234
- for (const rawValue of values) {
1235
- const normalized = rawValue.trim();
1236
- if (!normalized) {
1237
- continue;
1238
- }
1239
- const inner = normalized.startsWith("[") && normalized.endsWith("]")
1240
- ? normalized.slice(1, -1)
1241
- : normalized;
1242
- output.push(...inner.split(",").map((value) => value.trim().replace(/^["']|["']$/g, "")));
1243
- }
1244
- return normalizeTracerList(output);
1245
- }
1246
- function readThreadIdFromLocation() {
1247
- const params = new URLSearchParams(window.location.search);
1248
- return (params.get("thread_id") ||
1249
- params.get("threadId") ||
1250
- params.get("chat_id") ||
1251
- params.get("chatId") ||
1252
- "").trim();
1253
- }
1254
- function formatThreadTitle(text) {
1255
- const normalized = text.trim().replace(/\s+/g, " ");
1256
- if (!normalized) {
1257
- return "New chat";
1258
- }
1259
- return normalized.length > 42 ? `${normalized.slice(0, 42).trimEnd()}…` : normalized;
1260
- }
1261
- function serializeThreads(threads, activeThreadId) {
1262
- return threads.map((thread) => ({
1263
- id: thread.id,
1264
- templateId: thread.templateId,
1265
- title: thread.title,
1266
- createdAt: thread.createdAt,
1267
- updatedAt: thread.updatedAt,
1268
- lastMessageAt: thread.lastMessageAt ?? null,
1269
- messageCount: Math.max(thread.messageCount ?? 0, thread.messages.length),
1270
- tracers: thread.tracers,
1271
- hiddenFromView: thread.hiddenFromView,
1272
- messages: (thread.id === activeThreadId ? thread.messages.slice(-LOCAL_THREAD_MESSAGE_CACHE_LIMIT) : []).map((message) => ({
1273
- id: message.id,
1274
- role: message.role,
1275
- text: message.text,
1276
- attachments: message.attachments.map((attachment) => ({
1277
- id: attachment.id,
1278
- fileName: attachment.fileName,
1279
- contentType: attachment.contentType,
1280
- viewUrl: attachment.viewUrl
1281
- })),
1282
- toolExchanges: message.toolExchanges?.map((exchange) => ({
1283
- ...exchange,
1284
- args: sanitizeChatPayload(exchange.args),
1285
- result: sanitizeToolResult(exchange.result)
1286
- })),
1287
- httpExchanges: message.httpExchanges?.map(sanitizeHttpExchange)
1288
- }))
1289
- }));
1290
- }
1291
- function hydrateThreads(raw, fallbackTemplateId) {
1292
- if (!raw) {
1293
- return [];
1294
- }
1295
- try {
1296
- const parsed = JSON.parse(raw);
1297
- if (!Array.isArray(parsed)) {
1298
- return [];
1299
- }
1300
- return parsed
1301
- .filter((thread) => thread && typeof thread.id === "string")
1302
- .map((thread) => ({
1303
- id: thread.id,
1304
- templateId: typeof thread.templateId === "string" && thread.templateId.trim() ? thread.templateId : fallbackTemplateId,
1305
- title: typeof thread.title === "string" && thread.title.trim() ? thread.title : "New chat",
1306
- createdAt: typeof thread.createdAt === "string" ? thread.createdAt : new Date().toISOString(),
1307
- updatedAt: typeof thread.updatedAt === "string" ? thread.updatedAt : new Date().toISOString(),
1308
- lastMessageAt: typeof thread.lastMessageAt === "string" ? thread.lastMessageAt : null,
1309
- messageCount: Math.max(typeof thread.messageCount === "number" ? thread.messageCount : 0, Array.isArray(thread.messages) ? thread.messages.length : 0),
1310
- tracers: normalizeTracerList(Array.isArray(thread.tracers) ? thread.tracers.filter((value) => typeof value === "string") : []),
1311
- hiddenFromView: thread.hiddenFromView === true,
1312
- messages: Array.isArray(thread.messages)
1313
- ? thread.messages.map((message) => ({
1314
- id: message.id,
1315
- role: message.role,
1316
- text: message.text,
1317
- attachments: Array.isArray(message.attachments) ? message.attachments : [],
1318
- toolExchanges: Array.isArray(message.toolExchanges) ? message.toolExchanges : [],
1319
- httpExchanges: Array.isArray(message.httpExchanges) ? message.httpExchanges : []
1320
- }))
1321
- : []
1322
- }));
1323
- }
1324
- catch {
1325
- return [];
1326
- }
1327
- }
1328
- function hydrateThreadRecord(thread, fallbackTemplateId) {
1329
- if (!thread || typeof thread.id !== "string") {
1330
- return null;
1331
- }
1332
- return {
1333
- id: thread.id,
1334
- templateId: typeof thread.templateId === "string" && thread.templateId.trim() ? thread.templateId : fallbackTemplateId,
1335
- title: typeof thread.title === "string" && thread.title.trim() ? thread.title : "New chat",
1336
- createdAt: typeof thread.createdAt === "string" ? thread.createdAt : new Date().toISOString(),
1337
- updatedAt: typeof thread.updatedAt === "string" ? thread.updatedAt : new Date().toISOString(),
1338
- lastMessageAt: typeof thread.lastMessageAt === "string" ? thread.lastMessageAt : null,
1339
- messageCount: Math.max(typeof thread.messageCount === "number" ? thread.messageCount : 0, Array.isArray(thread.messages) ? thread.messages.length : 0),
1340
- tracers: normalizeTracerList(Array.isArray(thread.tracers) ? thread.tracers.filter((value) => typeof value === "string") : []),
1341
- hiddenFromView: thread.hiddenFromView === true,
1342
- messages: Array.isArray(thread.messages)
1343
- ? thread.messages.map((message) => ({
1344
- id: message.id,
1345
- role: message.role,
1346
- text: message.text,
1347
- attachments: Array.isArray(message.attachments) ? message.attachments : [],
1348
- toolExchanges: Array.isArray(message.toolExchanges) ? message.toolExchanges : [],
1349
- httpExchanges: Array.isArray(message.httpExchanges) ? message.httpExchanges : []
1350
- }))
1351
- : []
1352
- };
1353
- }
1354
- function mergeThreadSets(localThreads, remoteThreads) {
1355
- const byId = new Map();
1356
- for (const thread of remoteThreads) {
1357
- const localThread = localThreads.find((entry) => entry.id === thread.id);
1358
- byId.set(thread.id, {
1359
- ...thread,
1360
- lastMessageAt: thread.lastMessageAt ?? localThread?.lastMessageAt ?? null,
1361
- messageCount: Math.max(thread.messageCount ?? 0, localThread?.messageCount ?? 0, thread.messages.length, localThread?.messages.length ?? 0),
1362
- messages: thread.messages.length ? thread.messages : (localThread?.messages ?? [])
1363
- });
1364
- }
1365
- for (const thread of localThreads) {
1366
- if (!byId.has(thread.id)) {
1367
- byId.set(thread.id, thread);
1368
- }
1369
- }
1370
- return [...byId.values()].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
1371
- }
1372
- function sortThreadsByUpdatedAt(threads) {
1373
- return [...threads].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
1374
- }
1375
- function threadMatchesTracers(thread, selectedTracers) {
1376
- if (!selectedTracers.length) {
1377
- return true;
1378
- }
1379
- return thread.tracers.some((threadTracer) => selectedTracers.some((selectedTracer) => doesTracerMatchSelection(threadTracer, selectedTracer)));
1380
- }
1381
- function doesTracerMatchSelection(threadTracer, selectedTracer) {
1382
- const normalizedThreadTracer = threadTracer.trim();
1383
- const normalizedSelectedTracer = selectedTracer.trim();
1384
- if (!normalizedThreadTracer || !normalizedSelectedTracer) {
1385
- return false;
1386
- }
1387
- if (normalizedThreadTracer === normalizedSelectedTracer) {
1388
- return true;
1389
- }
1390
- return normalizedThreadTracer.startsWith(`${normalizedSelectedTracer}-`) && /-[a-z0-9]{8}$/i.test(normalizedThreadTracer);
1391
- }
1392
- function summarizeThread(thread) {
1393
- const lastMessage = [...thread.messages].reverse().find((message) => message.text.trim());
1394
- return lastMessage?.text.trim() || "No messages yet";
1395
- }
1396
- function summarizeThreadPreview(thread) {
1397
- const preview = summarizeThread(thread);
1398
- if (!thread.messages.length) {
1399
- return "No messages yet";
1400
- }
1401
- const normalizedTitle = thread.title.trim().toLowerCase();
1402
- const normalizedPreview = preview.trim().toLowerCase();
1403
- if (!preview || normalizedPreview === normalizedTitle) {
1404
- return `${thread.messages.length} message${thread.messages.length === 1 ? "" : "s"}`;
1405
- }
1406
- return preview;
1407
- }
1408
- function AttachmentChip(props) {
1409
- return (_jsxs("div", { className: "vf-editor-chat-attachment", children: [_jsx("span", { className: "vf-editor-chat-attachment-name", children: props.attachment.fileName }), props.onRemove ? (_jsx("button", { type: "button", className: "vf-editor-chat-attachment-remove", "aria-label": "Remove attachment", onClick: props.onRemove, children: "\u00D7" })) : null] }));
1410
- }
1411
- function DownloadIcon() {
1412
- return (_jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: [_jsx("path", { d: "M10 3v9" }), _jsx("path", { d: "m6.5 8.5 3.5 3.5 3.5-3.5" }), _jsx("path", { d: "M4 16h12" })] }));
1413
- }
1414
- function PaintBrushIcon() {
1415
- return (_jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: [_jsx("path", { d: "M4.2 15.8c1.6.5 3.1-.1 3.7-1.7.6-1.4 1.8-2.1 3.5-2.1" }), _jsx("path", { d: "m10.7 11.6 5.2-5.2a1.9 1.9 0 0 0-2.7-2.7L8 8.9" }), _jsx("path", { d: "m7.9 9 3.1 3.1" }), _jsx("path", { d: "M3.5 16.5c1.4.5 3.6.5 4.9-.8" })] }));
1416
- }
1417
- function LinkIcon() {
1418
- return (_jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: [_jsx("path", { d: "M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1" }), _jsx("path", { d: "M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2" })] }));
1419
- }
1420
- function CopyIcon() {
1421
- return (_jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: [_jsx("rect", { x: "7", y: "7", width: "9", height: "9", rx: "2" }), _jsx("path", { d: "M5 12H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1" })] }));
1422
- }
1423
- function ChevronIcon(props) {
1424
- return (_jsx("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", style: { transform: props.open ? "rotate(90deg)" : "rotate(0deg)" }, children: _jsx("path", { d: "m7 4 6 6-6 6" }) }));
1425
- }
1426
- function mediaDownloadName(preview) {
1427
- const cleanLabel = preview.label.trim().replace(/[\\/:*?"<>|]+/g, "-");
1428
- if (cleanLabel) {
1429
- return cleanLabel;
1430
- }
1431
- try {
1432
- const pathname = new URL(preview.url, window.location.href).pathname;
1433
- const name = pathname.split("/").filter(Boolean).pop();
1434
- return name || "media";
1435
- }
1436
- catch {
1437
- return "media";
1438
- }
1439
- }
1440
- function mediaInpaintHref(preview, ensureTracer = false) {
1441
- if (typeof window === "undefined" || preview.kind !== "image" || !preview.url) {
1442
- return "";
1443
- }
1444
- const pageUrl = new URL("/inpaint", window.location.origin);
1445
- const bridge = window.__vidfarmEditorTracers;
1446
- const existingTracers = bridge?.get() ?? readTracersFromLocation();
1447
- const tracer = existingTracers[0] || (ensureTracer ? bridge?.add() ?? "" : "");
1448
- if (tracer) {
1449
- pageUrl.searchParams.set("tracer", tracer);
1450
- }
1451
- pageUrl.searchParams.set("source_image_url", preview.url);
1452
- pageUrl.searchParams.set("source_image_name", preview.label || mediaDownloadName(preview));
1453
- return pageUrl.toString();
1454
- }
1455
- function MediaPreviewModal(props) {
1456
- const [isMounted, setIsMounted] = useState(false);
1457
- const [copyStatus, setCopyStatus] = useState("");
1458
- const { onClose } = props;
1459
- const gallery = props.gallery?.length ? props.gallery : [props.preview];
1460
- const initialIndex = Math.max(0, gallery.findIndex((preview) => preview.url === props.preview.url));
1461
- const [activeIndex, setActiveIndex] = useState(initialIndex);
1462
- const activePreview = gallery[activeIndex] || props.preview;
1463
- const canNavigate = gallery.length > 1;
1464
- const canInpaint = activePreview.kind === "image";
1465
- const initialInpaintHref = canInpaint ? mediaInpaintHref(activePreview, false) || "/inpaint" : "/inpaint";
1466
- useEffect(() => {
1467
- setIsMounted(true);
1468
- }, []);
1469
- useEffect(() => {
1470
- setCopyStatus("");
1471
- }, [activePreview.url]);
1472
- const handleCopyLink = async () => {
1473
- try {
1474
- if (navigator.clipboard?.writeText) {
1475
- await navigator.clipboard.writeText(activePreview.url);
1476
- }
1477
- else {
1478
- const field = document.createElement("textarea");
1479
- field.value = activePreview.url;
1480
- field.style.position = "fixed";
1481
- field.style.opacity = "0";
1482
- document.body.appendChild(field);
1483
- field.focus();
1484
- field.select();
1485
- document.execCommand("copy");
1486
- document.body.removeChild(field);
1487
- }
1488
- setCopyStatus("done");
1489
- }
1490
- catch {
1491
- setCopyStatus("error");
1492
- }
1493
- window.setTimeout(() => setCopyStatus(""), 1600);
1494
- };
1495
- const handleOpenInpaint = (event) => {
1496
- const href = mediaInpaintHref(activePreview, true);
1497
- if (!href) {
1498
- event.preventDefault();
1499
- return;
1500
- }
1501
- event.preventDefault();
1502
- window.open(href, "_blank", "noopener,noreferrer");
1503
- };
1504
- useEffect(() => {
1505
- const handleKeyDown = (event) => {
1506
- if (event.key === "Escape") {
1507
- onClose();
1508
- return;
1509
- }
1510
- if (!canNavigate) {
1511
- return;
1512
- }
1513
- if (event.key === "ArrowLeft") {
1514
- event.preventDefault();
1515
- setActiveIndex((current) => (current - 1 + gallery.length) % gallery.length);
1516
- }
1517
- else if (event.key === "ArrowRight") {
1518
- event.preventDefault();
1519
- setActiveIndex((current) => (current + 1) % gallery.length);
1520
- }
1521
- };
1522
- window.addEventListener("keydown", handleKeyDown);
1523
- return () => window.removeEventListener("keydown", handleKeyDown);
1524
- }, [canNavigate, gallery.length, onClose]);
1525
- if (!isMounted) {
1526
- return null;
1527
- }
1528
- return createPortal(_jsx("div", { className: "vf-editor-chat-media-modal-backdrop", onClick: props.onClose, children: _jsxs("div", { className: "vf-editor-chat-media-modal", onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "vf-editor-chat-media-modal-header", children: [_jsx("a", { className: "vf-editor-chat-media-modal-link", href: activePreview.url, target: "_blank", rel: "noopener noreferrer", children: canNavigate ? `${activePreview.label} (${activeIndex + 1} of ${gallery.length})` : activePreview.label }), _jsxs("div", { className: "vf-editor-chat-media-modal-actions", children: [canNavigate ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "vf-editor-chat-media-modal-control", onClick: () => setActiveIndex((current) => (current - 1 + gallery.length) % gallery.length), "aria-label": "Previous image", title: "Previous image", children: "\u2039" }), _jsx("button", { type: "button", className: "vf-editor-chat-media-modal-control", onClick: () => setActiveIndex((current) => (current + 1) % gallery.length), "aria-label": "Next image", title: "Next image", children: "\u203A" })] })) : null, canInpaint ? (_jsx("a", { className: "vf-editor-chat-media-modal-control", href: initialInpaintHref, target: "_blank", rel: "noopener noreferrer", onClick: handleOpenInpaint, "aria-label": "Open image editor", title: "Image edit", children: _jsx(PaintBrushIcon, {}) })) : null, _jsx("a", { className: "vf-editor-chat-media-modal-control", href: activePreview.url, download: mediaDownloadName(activePreview), "aria-label": "Download media", title: "Download", children: _jsx(DownloadIcon, {}) }), _jsx("button", { type: "button", className: "vf-editor-chat-media-modal-control", "data-state": copyStatus, onClick: () => void handleCopyLink(), "aria-label": copyStatus === "done" ? "Media URL copied" : copyStatus === "error" ? "Unable to copy media URL" : "Copy media URL", title: copyStatus === "done" ? "Copied" : copyStatus === "error" ? "Copy failed" : "Copy link", children: _jsx(LinkIcon, {}) }), _jsx("button", { type: "button", className: "vf-editor-chat-media-modal-control", onClick: props.onClose, "aria-label": "Close media preview", title: "Close", children: "\u00D7" })] })] }), _jsx("div", { className: "vf-editor-chat-media-modal-body", children: activePreview.kind === "image" ? (_jsx("img", { src: activePreview.url, alt: activePreview.label, className: "vf-editor-chat-media-modal-image" })) : (_jsx("video", { src: activePreview.url, className: "vf-editor-chat-media-modal-video", controls: true, autoPlay: true, playsInline: true })) })] }) }), document.body);
1529
- }
1530
- function ImagePreviewButton(props) {
1531
- const [hasError, setHasError] = useState(false);
1532
- return (_jsx("button", { type: "button", className: "vf-editor-chat-message-thumb-button is-image", onClick: () => props.onOpenModal(props.preview, props.previews), "aria-label": `Preview ${props.preview.label}`, title: props.preview.label, "data-error": hasError ? "true" : "false", children: hasError ? (_jsx("span", { className: "vf-editor-chat-message-thumb-fallback", children: "Preview unavailable" })) : (_jsx("img", { src: props.preview.url, alt: props.preview.label, className: "vf-editor-chat-message-attachment-thumb", loading: "lazy", onError: () => setHasError(true) })) }));
1533
- }
1534
- function VideoPreviewCard(props) {
1535
- return (_jsx("div", { className: "vf-editor-chat-message-attachment-row", children: _jsxs("button", { type: "button", className: "vf-editor-chat-message-thumb-button", onClick: () => props.onOpenModal(props.preview), "aria-label": `Preview ${props.preview.label}`, children: [_jsx("video", { src: props.preview.url, className: "vf-editor-chat-message-attachment-thumb vf-editor-chat-message-video-thumb", preload: "metadata", muted: true, playsInline: true }), _jsx("span", { className: "vf-editor-chat-message-video-play", "aria-hidden": "true" })] }) }));
1536
- }
1537
- function ImagePreviewRail(props) {
1538
- if (!props.previews.length) {
1539
- return null;
1540
- }
1541
- return (_jsx("div", { className: "vf-editor-chat-message-image-rail", "aria-label": "Image previews", children: props.previews.map((preview) => (_jsx(ImagePreviewButton, { preview: preview, previews: props.previews, onOpenModal: props.onOpenModal }, preview.url))) }));
1542
- }
1543
- function ComposerAttachmentItem(props) {
1544
- return (_jsxs("div", { className: "vf-editor-chat-composer-attachment-item", children: [isImageAttachment(props.attachment) ? (_jsx("img", { src: props.attachment.viewUrl, alt: props.attachment.fileName, className: "vf-editor-chat-composer-attachment-thumb" })) : (_jsx("div", { className: "vf-editor-chat-composer-attachment-file", "aria-hidden": "true", children: props.attachment.fileName.slice(0, 1).toUpperCase() })), _jsx("div", { className: "vf-editor-chat-composer-attachment-meta", children: _jsx("span", { className: "vf-editor-chat-composer-attachment-name", children: props.attachment.fileName }) }), _jsx("button", { type: "button", className: "vf-editor-chat-composer-attachment-remove", "aria-label": `Remove ${props.attachment.fileName}`, onClick: props.onRemove, children: "\u00D7" })] }));
1545
- }
1546
- function HttpExchangeCard(props) {
1547
- const [isOpen, setIsOpen] = useState(false);
1548
- const [isMounted, setIsMounted] = useState(false);
1549
- const requestBody = formatJsonBody(props.exchange.request.body);
1550
- const responseBody = formatJsonBody(props.exchange.result.body);
1551
- const requestUrl = props.exchange.request.url;
1552
- const displayUrl = requestUrl.length > 52
1553
- ? `${requestUrl.slice(0, 52).trimEnd()}...`
1554
- : requestUrl;
1555
- const isError = props.exchange.result.status >= 400 || Boolean(props.exchange.result.error);
1556
- const isSuccess = props.exchange.result.status >= 200 && props.exchange.result.status < 300;
1557
- useEffect(() => {
1558
- setIsMounted(true);
1559
- }, []);
1560
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "vf-editor-chat-http-card", children: _jsxs("button", { type: "button", className: "vf-editor-chat-http-card-header", onClick: () => setIsOpen(true), "aria-label": `Open HTTP details for ${props.exchange.request.method} ${requestUrl}`, children: [_jsx("span", { className: "vf-editor-chat-http-pill", children: props.exchange.request.method }), _jsx("span", { className: "vf-editor-chat-http-url", title: requestUrl, children: displayUrl }), _jsxs("span", { className: "vf-editor-chat-http-status", "data-error": isError ? "true" : "false", "data-success": isSuccess ? "true" : "false", children: [props.exchange.result.status, props.exchange.result.statusText ? ` ${props.exchange.result.statusText}` : ""] }), _jsx("span", { className: "vf-editor-chat-http-expand", "aria-hidden": "true", children: "+" })] }) }), isMounted && isOpen ? createPortal(_jsx("div", { className: "vf-editor-chat-http-modal-backdrop", onClick: () => setIsOpen(false), children: _jsxs("div", { className: "vf-editor-chat-http-modal", onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "vf-editor-chat-http-modal-header", children: [_jsxs("div", { className: "vf-editor-chat-http-modal-header-main", children: [_jsx("span", { className: "vf-editor-chat-http-pill", children: props.exchange.request.method }), _jsx("span", { className: "vf-editor-chat-http-modal-url", children: requestUrl }), _jsxs("span", { className: "vf-editor-chat-http-status", "data-error": isError ? "true" : "false", "data-success": isSuccess ? "true" : "false", children: [props.exchange.result.status, props.exchange.result.statusText ? ` ${props.exchange.result.statusText}` : ""] })] }), _jsx("button", { type: "button", className: "vf-editor-chat-http-modal-close", onClick: () => setIsOpen(false), "aria-label": "Close HTTP details", children: "\u00D7" })] }), _jsxs("div", { className: "vf-editor-chat-http-card-body is-modal", children: [props.exchange.explanation ? (_jsx("div", { className: "vf-editor-chat-http-note", children: props.exchange.explanation })) : null, _jsxs("div", { className: "vf-editor-chat-http-block", children: [_jsx("div", { className: "vf-editor-chat-http-label", children: "URL" }), _jsx("pre", { className: "vf-editor-chat-http-pre", children: requestUrl })] }), requestBody ? (_jsxs("div", { className: "vf-editor-chat-http-block", children: [_jsx("div", { className: "vf-editor-chat-http-label", children: "Request" }), _jsx("pre", { className: "vf-editor-chat-http-pre", children: requestBody })] })) : null, _jsxs("div", { className: "vf-editor-chat-http-block", children: [_jsx("div", { className: "vf-editor-chat-http-label", children: "Response" }), _jsx("pre", { className: "vf-editor-chat-http-pre", children: props.exchange.result.error || responseBody || "(empty)" })] }), Object.keys(props.exchange.result.headers).length ? (_jsxs("div", { className: "vf-editor-chat-http-block", children: [_jsx("div", { className: "vf-editor-chat-http-label", children: "Headers" }), _jsx("pre", { className: "vf-editor-chat-http-pre", children: formatJsonBody(props.exchange.result.headers) })] })) : null] })] }) }), document.body) : null] }));
1561
- }
1562
- function formatJsonInlineValue(value) {
1563
- if (typeof value === "string") {
1564
- return JSON.stringify(value);
1565
- }
1566
- if (typeof value === "number" || typeof value === "boolean") {
1567
- return String(value);
1568
- }
1569
- if (value === null) {
1570
- return "null";
1571
- }
1572
- return JSON.stringify(value);
1573
- }
1574
- function copyPlainText(text) {
1575
- if (navigator.clipboard?.writeText) {
1576
- return navigator.clipboard.writeText(text);
1577
- }
1578
- const field = document.createElement("textarea");
1579
- field.value = text;
1580
- field.style.position = "fixed";
1581
- field.style.opacity = "0";
1582
- document.body.appendChild(field);
1583
- field.focus();
1584
- field.select();
1585
- document.execCommand("copy");
1586
- document.body.removeChild(field);
1587
- return Promise.resolve();
1588
- }
1589
- function JsonViewerLine(props) {
1590
- return (_jsxs("div", { className: "vf-editor-chat-json-line", style: { paddingLeft: `${props.depth * 18 + 12}px` }, children: [_jsx("button", { type: "button", className: "vf-editor-chat-json-copy", "aria-label": "Copy line", title: "Copy line", onClick: () => props.onCopy(props.text), children: _jsx(CopyIcon, {}) }), _jsx("span", { className: "vf-editor-chat-json-line-text", children: props.text })] }));
1591
- }
1592
- function JsonViewerNode(props) {
1593
- const value = props.value;
1594
- const suffix = props.isLast ? "" : ",";
1595
- if (Array.isArray(value)) {
1596
- const path = props.path;
1597
- const isCollapsed = props.collapsedPaths.has(path);
1598
- const headerText = `${props.linePrefix ?? ""}[`;
1599
- if (!value.length) {
1600
- return _jsx(JsonViewerLine, { text: `${props.linePrefix ?? ""}[]${suffix}`, depth: props.depth, onCopy: props.onCopy });
1601
- }
1602
- if (isCollapsed) {
1603
- return (_jsxs("div", { className: "vf-editor-chat-json-toggle-row", children: [_jsx("button", { type: "button", className: "vf-editor-chat-json-toggle-icon", onClick: () => props.togglePath(path), "aria-label": "Expand JSON block", children: _jsx(ChevronIcon, { open: false }) }), _jsx(JsonViewerLine, { text: `${props.linePrefix ?? ""}[ … ]${suffix}`, depth: props.depth, onCopy: props.onCopy })] }));
1604
- }
1605
- return (_jsxs("div", { children: [_jsxs("div", { className: "vf-editor-chat-json-toggle-row", children: [_jsx("button", { type: "button", className: "vf-editor-chat-json-toggle-icon", onClick: () => props.togglePath(path), "aria-label": "Collapse JSON block", children: _jsx(ChevronIcon, { open: true }) }), _jsx(JsonViewerLine, { text: headerText, depth: props.depth, onCopy: props.onCopy })] }), value.map((entry, index) => (_jsx(JsonViewerNode, { value: entry, depth: props.depth + 1, path: `${path}.${index}`, isLast: index === value.length - 1, collapsedPaths: props.collapsedPaths, togglePath: props.togglePath, onCopy: props.onCopy }, `${path}:${index}`))), _jsx(JsonViewerLine, { text: `]${suffix}`, depth: props.depth, onCopy: props.onCopy })] }));
1606
- }
1607
- if (value && typeof value === "object") {
1608
- const entries = Object.entries(value);
1609
- const path = props.path;
1610
- const isCollapsed = props.collapsedPaths.has(path);
1611
- const headerText = `${props.linePrefix ?? ""}{`;
1612
- if (!entries.length) {
1613
- return _jsx(JsonViewerLine, { text: `${props.linePrefix ?? ""}{}${suffix}`, depth: props.depth, onCopy: props.onCopy });
1614
- }
1615
- if (isCollapsed) {
1616
- return (_jsxs("div", { className: "vf-editor-chat-json-toggle-row", children: [_jsx("button", { type: "button", className: "vf-editor-chat-json-toggle-icon", onClick: () => props.togglePath(path), "aria-label": "Expand JSON block", children: _jsx(ChevronIcon, { open: false }) }), _jsx(JsonViewerLine, { text: `${props.linePrefix ?? ""}{ … }${suffix}`, depth: props.depth, onCopy: props.onCopy })] }));
1617
- }
1618
- return (_jsxs("div", { children: [_jsxs("div", { className: "vf-editor-chat-json-toggle-row", children: [_jsx("button", { type: "button", className: "vf-editor-chat-json-toggle-icon", onClick: () => props.togglePath(path), "aria-label": "Collapse JSON block", children: _jsx(ChevronIcon, { open: true }) }), _jsx(JsonViewerLine, { text: headerText, depth: props.depth, onCopy: props.onCopy })] }), entries.map(([key, entry], index) => {
1619
- const childPrefix = `${JSON.stringify(key)}: `;
1620
- if (entry && typeof entry === "object") {
1621
- return (_jsx(JsonViewerNode, { value: entry, depth: props.depth + 1, linePrefix: childPrefix, path: `${path}.${key}`, isLast: index === entries.length - 1, collapsedPaths: props.collapsedPaths, togglePath: props.togglePath, onCopy: props.onCopy }, `${path}:${key}`));
1622
- }
1623
- return (_jsx(JsonViewerLine, { text: `${childPrefix}${formatJsonInlineValue(entry)}${index === entries.length - 1 ? "" : ","}`, depth: props.depth + 1, onCopy: props.onCopy }, `${path}:${key}`));
1624
- }), _jsx(JsonViewerLine, { text: `}${suffix}`, depth: props.depth, onCopy: props.onCopy })] }));
1625
- }
1626
- return (_jsx(JsonViewerLine, { text: `${props.linePrefix ?? ""}${formatJsonInlineValue(value)}${suffix}`, depth: props.depth, onCopy: props.onCopy }));
1627
- }
1628
- function JsonViewer(props) {
1629
- const [collapsedPaths, setCollapsedPaths] = useState(new Set());
1630
- const [copyState, setCopyState] = useState("");
1631
- const togglePath = (path) => {
1632
- setCollapsedPaths((current) => {
1633
- const next = new Set(current);
1634
- if (next.has(path)) {
1635
- next.delete(path);
1636
- }
1637
- else {
1638
- next.add(path);
1639
- }
1640
- return next;
1641
- });
1642
- };
1643
- const handleCopy = async (text) => {
1644
- try {
1645
- await copyPlainText(text);
1646
- setCopyState("done");
1647
- }
1648
- catch {
1649
- setCopyState("error");
1650
- }
1651
- window.setTimeout(() => setCopyState(""), 1400);
1652
- };
1653
- return (_jsxs("div", { className: "vf-editor-chat-json-viewer", children: [_jsxs("div", { className: "vf-editor-chat-json-toolbar", children: [_jsx("span", { className: "vf-editor-chat-json-file", children: props.storageKey }), _jsx("span", { className: "vf-editor-chat-json-copy-state", "data-state": copyState, children: copyState === "done" ? "Copied line" : copyState === "error" ? "Copy failed" : "Hover a line to copy" })] }), _jsx("div", { className: "vf-editor-chat-json-scroll", children: _jsx(JsonViewerNode, { value: props.value, depth: 0, path: "root", collapsedPaths: collapsedPaths, togglePath: togglePath, onCopy: handleCopy }) })] }));
1654
- }
1655
- function JsonArtifactModal(props) {
1656
- const [isMounted, setIsMounted] = useState(false);
1657
- const [copyStatus, setCopyStatus] = useState("");
1658
- const downloadName = downloadNameFromUrl(props.artifact.url, `${props.artifact.label}.json`);
1659
- const formattedData = formatJsonBody(props.data);
1660
- useEffect(() => {
1661
- setIsMounted(true);
1662
- }, []);
1663
- const handleCopy = async () => {
1664
- try {
1665
- await copyPlainText(formattedData);
1666
- setCopyStatus("done");
1667
- }
1668
- catch {
1669
- setCopyStatus("error");
1670
- }
1671
- window.setTimeout(() => setCopyStatus(""), 1600);
1672
- };
1673
- useEffect(() => {
1674
- const handleKeyDown = (event) => {
1675
- if (event.key === "Escape") {
1676
- props.onClose();
1677
- }
1678
- };
1679
- window.addEventListener("keydown", handleKeyDown);
1680
- return () => window.removeEventListener("keydown", handleKeyDown);
1681
- }, [props]);
1682
- if (!isMounted) {
1683
- return null;
1684
- }
1685
- return createPortal(_jsx("div", { className: "vf-editor-chat-media-modal-backdrop", onClick: props.onClose, children: _jsxs("div", { className: "vf-editor-chat-json-modal", onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "vf-editor-chat-media-modal-header", children: [_jsx("a", { className: "vf-editor-chat-media-modal-link", href: props.artifact.url, target: "_blank", rel: "noopener noreferrer", children: props.artifact.label }), _jsxs("div", { className: "vf-editor-chat-media-modal-actions", children: [_jsx("button", { type: "button", className: "vf-editor-chat-media-modal-control", "data-state": copyStatus, onClick: () => void handleCopy(), title: copyStatus === "done" ? "Copied" : copyStatus === "error" ? "Copy failed" : "Copy JSON", "aria-label": copyStatus === "done" ? "JSON copied" : copyStatus === "error" ? "Unable to copy JSON" : "Copy JSON", children: _jsx(CopyIcon, {}) }), _jsx("a", { className: "vf-editor-chat-media-modal-control", href: props.artifact.url, download: downloadName, title: "Download JSON", "aria-label": "Download JSON", children: _jsx(DownloadIcon, {}) }), _jsx("a", { className: "vf-editor-chat-media-modal-control", href: props.artifact.url, target: "_blank", rel: "noopener noreferrer", title: "Open raw JSON", "aria-label": "Open raw JSON", children: _jsx(LinkIcon, {}) }), _jsx("button", { type: "button", className: "vf-editor-chat-media-modal-control", onClick: props.onClose, "aria-label": "Close JSON preview", title: "Close", children: "\u00D7" })] })] }), _jsx("div", { className: "vf-editor-chat-json-modal-body", children: _jsx(JsonViewer, { value: props.data, storageKey: props.artifact.label }) })] }) }), document.body);
1686
- }
1687
- function JsonArtifactCard(props) {
1688
- const [isExpanded, setIsExpanded] = useState(false);
1689
- const [isModalOpen, setIsModalOpen] = useState(false);
1690
- const [data, setData] = useState(null);
1691
- const [error, setError] = useState(null);
1692
- const [loading, setLoading] = useState(false);
1693
- const ensureLoaded = async () => {
1694
- if (data !== null || loading) {
1695
- return;
1696
- }
1697
- setLoading(true);
1698
- setError(null);
1699
- try {
1700
- const response = await fetch(props.artifact.url, { credentials: "omit" });
1701
- const payload = await response.json();
1702
- setData(payload);
1703
- }
1704
- catch (nextError) {
1705
- setError(nextError instanceof Error ? nextError.message : "Unable to load JSON artifact.");
1706
- }
1707
- finally {
1708
- setLoading(false);
1709
- }
1710
- };
1711
- const handleToggleExpand = () => {
1712
- const next = !isExpanded;
1713
- setIsExpanded(next);
1714
- if (next) {
1715
- void ensureLoaded();
1716
- }
1717
- };
1718
- const handleOpenModal = () => {
1719
- void ensureLoaded();
1720
- setIsModalOpen(true);
1721
- };
1722
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "vf-editor-chat-json-card", children: [_jsxs("div", { className: "vf-editor-chat-json-card-head", children: [_jsxs("div", { children: [_jsx("div", { className: "vf-editor-chat-json-card-kicker", children: "JSON result" }), _jsx("div", { className: "vf-editor-chat-json-card-title", children: props.artifact.label })] }), _jsxs("div", { className: "vf-editor-chat-json-card-actions", children: [_jsx("button", { type: "button", className: "vf-editor-chat-json-card-action", onClick: handleToggleExpand, children: isExpanded ? "Hide preview" : "Preview JSON" }), _jsx("button", { type: "button", className: "vf-editor-chat-json-card-action is-strong", onClick: handleOpenModal, children: "Open" })] })] }), isExpanded ? (_jsx("div", { className: "vf-editor-chat-json-card-body", children: loading ? (_jsx("div", { className: "vf-editor-chat-json-state", children: "Loading JSON\u2026" })) : error ? (_jsx("div", { className: "vf-editor-chat-json-state is-error", children: error })) : data !== null ? (_jsx(JsonViewer, { value: data, storageKey: props.artifact.label })) : null })) : null] }), isModalOpen && data !== null ? (_jsx(JsonArtifactModal, { artifact: props.artifact, data: data, onClose: () => setIsModalOpen(false) })) : null] }));
1723
- }
1724
- function TextArtifactViewer(props) {
1725
- if (props.format === "markdown") {
1726
- return (_jsx("div", { className: "vf-editor-chat-doc-markdown", dangerouslySetInnerHTML: { __html: renderMarkdownToHtml(props.text) } }));
1727
- }
1728
- return _jsx("pre", { className: "vf-editor-chat-doc-pre", children: props.text });
1729
- }
1730
- function DocumentArtifactModal(props) {
1731
- const [isMounted, setIsMounted] = useState(false);
1732
- const [copyStatus, setCopyStatus] = useState("");
1733
- const fallbackExtension = props.artifact.format === "markdown" ? ".md" : ".txt";
1734
- const downloadName = downloadNameFromUrl(props.artifact.url, `${props.artifact.label}${fallbackExtension}`);
1735
- useEffect(() => {
1736
- setIsMounted(true);
1737
- }, []);
1738
- const handleCopy = async () => {
1739
- try {
1740
- await copyPlainText(props.text);
1741
- setCopyStatus("done");
1742
- }
1743
- catch {
1744
- setCopyStatus("error");
1745
- }
1746
- window.setTimeout(() => setCopyStatus(""), 1600);
1747
- };
1748
- useEffect(() => {
1749
- const handleKeyDown = (event) => {
1750
- if (event.key === "Escape") {
1751
- props.onClose();
1752
- }
1753
- };
1754
- window.addEventListener("keydown", handleKeyDown);
1755
- return () => window.removeEventListener("keydown", handleKeyDown);
1756
- }, [props]);
1757
- if (!isMounted) {
1758
- return null;
1759
- }
1760
- return createPortal(_jsx("div", { className: "vf-editor-chat-media-modal-backdrop", onClick: props.onClose, children: _jsxs("div", { className: "vf-editor-chat-json-modal", onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "vf-editor-chat-media-modal-header", children: [_jsx("a", { className: "vf-editor-chat-media-modal-link", href: props.artifact.url, target: "_blank", rel: "noopener noreferrer", children: props.artifact.label }), _jsxs("div", { className: "vf-editor-chat-media-modal-actions", children: [_jsx("button", { type: "button", className: "vf-editor-chat-media-modal-control", "data-state": copyStatus, onClick: () => void handleCopy(), title: copyStatus === "done" ? "Copied" : copyStatus === "error" ? "Copy failed" : props.artifact.format === "markdown" ? "Copy markdown" : "Copy text", "aria-label": copyStatus === "done" ? "Document copied" : copyStatus === "error" ? "Unable to copy document" : props.artifact.format === "markdown" ? "Copy markdown" : "Copy text", children: _jsx(CopyIcon, {}) }), _jsx("a", { className: "vf-editor-chat-media-modal-control", href: props.artifact.url, download: downloadName, title: props.artifact.format === "markdown" ? "Download markdown" : "Download text", "aria-label": props.artifact.format === "markdown" ? "Download markdown" : "Download text", children: _jsx(DownloadIcon, {}) }), _jsx("a", { className: "vf-editor-chat-media-modal-control", href: props.artifact.url, target: "_blank", rel: "noopener noreferrer", title: "Open raw file", "aria-label": "Open raw file", children: _jsx(LinkIcon, {}) }), _jsx("button", { type: "button", className: "vf-editor-chat-media-modal-control", onClick: props.onClose, "aria-label": "Close document preview", title: "Close", children: "\u00D7" })] })] }), _jsx("div", { className: "vf-editor-chat-json-modal-body", children: _jsx("div", { className: "vf-editor-chat-doc-viewer", children: _jsx(TextArtifactViewer, { text: props.text, format: props.artifact.format }) }) })] }) }), document.body);
1761
- }
1762
- function DocumentArtifactCard(props) {
1763
- const [isExpanded, setIsExpanded] = useState(false);
1764
- const [isModalOpen, setIsModalOpen] = useState(false);
1765
- const [text, setText] = useState(null);
1766
- const [error, setError] = useState(null);
1767
- const [loading, setLoading] = useState(false);
1768
- const ensureLoaded = async () => {
1769
- if (text !== null || loading) {
1770
- return;
1771
- }
1772
- setLoading(true);
1773
- setError(null);
1774
- try {
1775
- const response = await fetch(props.artifact.url, { credentials: "omit" });
1776
- const payload = await response.text();
1777
- setText(payload);
1778
- }
1779
- catch (nextError) {
1780
- setError(nextError instanceof Error ? nextError.message : "Unable to load document artifact.");
1781
- }
1782
- finally {
1783
- setLoading(false);
1784
- }
1785
- };
1786
- const handleToggleExpand = () => {
1787
- const next = !isExpanded;
1788
- setIsExpanded(next);
1789
- if (next) {
1790
- void ensureLoaded();
1791
- }
1792
- };
1793
- const handleOpenModal = () => {
1794
- void ensureLoaded();
1795
- setIsModalOpen(true);
1796
- };
1797
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "vf-editor-chat-json-card", children: [_jsxs("div", { className: "vf-editor-chat-json-card-head", children: [_jsxs("div", { children: [_jsx("div", { className: "vf-editor-chat-json-card-kicker", children: props.artifact.format === "markdown" ? "Markdown result" : "Text result" }), _jsx("div", { className: "vf-editor-chat-json-card-title", children: props.artifact.label })] }), _jsxs("div", { className: "vf-editor-chat-json-card-actions", children: [_jsx("button", { type: "button", className: "vf-editor-chat-json-card-action", onClick: handleToggleExpand, children: isExpanded ? "Hide preview" : "Preview" }), _jsx("button", { type: "button", className: "vf-editor-chat-json-card-action is-strong", onClick: handleOpenModal, children: "Open" })] })] }), isExpanded ? (_jsx("div", { className: "vf-editor-chat-json-card-body", children: loading ? (_jsx("div", { className: "vf-editor-chat-json-state", children: "Loading document\u2026" })) : error ? (_jsx("div", { className: "vf-editor-chat-json-state is-error", children: error })) : text !== null ? (_jsx("div", { className: "vf-editor-chat-doc-viewer is-inline", children: _jsx(TextArtifactViewer, { text: text, format: props.artifact.format }) })) : null })) : null] }), isModalOpen && text !== null ? (_jsx(DocumentArtifactModal, { artifact: props.artifact, text: text, onClose: () => setIsModalOpen(false) })) : null] }));
1798
- }
1799
- function ChatBubble(props) {
1800
- const isAssistant = props.message.role === "assistant";
1801
- const hasVisibleText = props.message.text.trim().length > 0;
1802
- const renderedText = hasVisibleText ? renderMarkdownToHtml(props.message.text) : "";
1803
- const [mediaModalState, setMediaModalState] = useState(null);
1804
- const imageAttachments = props.message.attachments.filter(isImageAttachment);
1805
- const videoAttachments = props.message.attachments.filter(isVideoAttachment);
1806
- const fileAttachments = props.message.attachments.filter((attachment) => !isImageAttachment(attachment) && !isVideoAttachment(attachment));
1807
- const inlineMediaPreviews = hasVisibleText
1808
- ? extractInlineMediaPreviews(props.message.text, props.message.attachments.map((attachment) => attachment.viewUrl))
1809
- : [];
1810
- const inlineImagePreviews = inlineMediaPreviews.filter((preview) => preview.kind === "image");
1811
- const inlineVideoPreviews = inlineMediaPreviews.filter((preview) => preview.kind === "video");
1812
- const brainstormJsonArtifacts = (props.message.httpExchanges ?? []).flatMap((exchange) => {
1813
- const job = tryReadJobStatusPayload(exchange.result.body);
1814
- return job ? extractBrainstormJsonArtifactsFromJob(job) : [];
1815
- }).filter((artifact, index, items) => items.findIndex((entry) => entry.url === artifact.url) === index);
1816
- const brainstormDocumentArtifacts = (props.message.httpExchanges ?? []).flatMap((exchange) => {
1817
- const job = tryReadJobStatusPayload(exchange.result.body);
1818
- return job ? extractBrainstormDocumentArtifactsFromJob(job) : [];
1819
- }).filter((artifact, index, items) => items.findIndex((entry) => entry.url === artifact.url) === index);
1820
- return (_jsxs("div", { className: `vf-editor-chat-message ${isAssistant ? "is-assistant" : "is-user"}`, children: [_jsx("div", { className: "vf-editor-chat-message-meta", children: isAssistant ? "Copilot" : "You" }), _jsxs("div", { className: "vf-editor-chat-bubble", "data-pending": props.message.isPending ? "true" : "false", children: [props.message.attachments.length ? (_jsxs("div", { className: "vf-editor-chat-message-attachments", children: [fileAttachments.map((attachment) => (_jsx("div", { className: "vf-editor-chat-message-attachment-row", children: _jsx("a", { className: "vf-editor-chat-message-preview-link", href: attachment.viewUrl, target: "_blank", rel: "noopener noreferrer", children: _jsx(AttachmentChip, { attachment: attachment }) }) }, attachment.id))), _jsx(ImagePreviewRail, { previews: imageAttachments.map((attachment) => ({
1821
- kind: "image",
1822
- url: attachment.viewUrl,
1823
- label: attachment.fileName
1824
- })), onOpenModal: (preview, gallery) => setMediaModalState({ preview, gallery }) }), videoAttachments.map((attachment) => (_jsx(VideoPreviewCard, { preview: {
1825
- kind: "video",
1826
- url: attachment.viewUrl,
1827
- label: attachment.fileName
1828
- }, onOpenModal: (preview) => setMediaModalState({ preview, gallery: [preview] }) }, attachment.id)))] })) : null, props.message.statusText ? (_jsxs("div", { className: "vf-editor-chat-message-status", "aria-live": "polite", children: [_jsx("span", { className: "vf-editor-chat-status-dot" }), props.message.statusText] })) : null, props.message.httpExchanges?.length ? (_jsx("div", { className: "vf-editor-chat-http-list", children: props.message.httpExchanges.map((exchange, index) => (_jsx(HttpExchangeCard, { exchange: exchange }, `${exchange.request.method}:${exchange.request.url}:${index}`))) })) : null, hasVisibleText ? (_jsx("div", { className: "vf-editor-chat-message-body", dangerouslySetInnerHTML: { __html: renderedText } })) : props.message.isPending ? (_jsxs("div", { className: "vf-editor-chat-message-loading", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] })) : null, inlineMediaPreviews.length ? (_jsxs("div", { className: "vf-editor-chat-message-attachments", children: [_jsx(ImagePreviewRail, { previews: inlineImagePreviews, onOpenModal: (preview, gallery) => setMediaModalState({ preview, gallery }) }), inlineVideoPreviews.map((preview) => (_jsx(VideoPreviewCard, { preview: preview, onOpenModal: (activePreview) => setMediaModalState({ preview: activePreview, gallery: [activePreview] }) }, preview.url)))] })) : null, brainstormJsonArtifacts.length ? (_jsx("div", { className: "vf-editor-chat-json-list", children: brainstormJsonArtifacts.map((artifact) => (_jsx(JsonArtifactCard, { artifact: artifact }, artifact.key))) })) : null, brainstormDocumentArtifacts.length ? (_jsx("div", { className: "vf-editor-chat-json-list", children: brainstormDocumentArtifacts.map((artifact) => (_jsx(DocumentArtifactCard, { artifact: artifact }, artifact.key))) })) : null] }), mediaModalState ? (_jsx(MediaPreviewModal, { preview: mediaModalState.preview, gallery: mediaModalState.gallery, onClose: () => setMediaModalState(null) })) : null] }));
1829
- }
1830
- function formatBytes(bytes) {
1831
- if (!bytes || bytes <= 0) {
1832
- return "Unknown size";
1833
- }
1834
- const units = ["B", "KB", "MB", "GB"];
1835
- let value = bytes;
1836
- let unitIndex = 0;
1837
- while (value >= 1024 && unitIndex < units.length - 1) {
1838
- value /= 1024;
1839
- unitIndex += 1;
1840
- }
1841
- return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
1842
- }
1843
- function FilePreview(props) {
1844
- if (isImageAttachment(props.file)) {
1845
- return _jsx("img", { className: "vf-editor-upload-file-thumb", src: props.file.viewUrl, alt: props.file.fileName });
1846
- }
1847
- if (isVideoAttachment(props.file)) {
1848
- return _jsx("video", { className: "vf-editor-upload-file-thumb", src: props.file.viewUrl, muted: true, playsInline: true });
1849
- }
1850
- return _jsx("div", { className: "vf-editor-upload-file-icon", "aria-hidden": "true", children: props.file.fileName.slice(0, 1).toUpperCase() });
1851
- }
1852
- function normalizeFolderPath(value) {
1853
- return value
1854
- .split("/")
1855
- .map((segment) => segment.trim())
1856
- .filter(Boolean)
1857
- .join("/");
1858
- }
1859
- function joinFolderPath(parent, name) {
1860
- return normalizeFolderPath([parent, name].filter(Boolean).join("/"));
1861
- }
1862
- function parentFolderPath(value) {
1863
- const parts = normalizeFolderPath(value).split("/").filter(Boolean);
1864
- parts.pop();
1865
- return parts.join("/");
1866
- }
1867
- function folderDisplayName(value) {
1868
- const parts = normalizeFolderPath(value).split("/").filter(Boolean);
1869
- return parts.at(-1) || "All files";
1870
- }
1871
- function getImmediateFolders(input) {
1872
- const current = normalizeFolderPath(input.currentFolder);
1873
- const prefix = current ? `${current}/` : "";
1874
- const names = new Map();
1875
- for (const folder of input.folders) {
1876
- const normalized = normalizeFolderPath(folder);
1877
- if (!normalized || (current && normalized === current) || !normalized.startsWith(prefix)) {
1878
- continue;
1879
- }
1880
- const rest = normalized.slice(prefix.length);
1881
- const childName = rest.split("/")[0];
1882
- if (childName) {
1883
- names.set(childName, joinFolderPath(current, childName));
1884
- }
1885
- }
1886
- for (const file of input.files) {
1887
- const normalized = normalizeFolderPath(file.folderPath ?? "");
1888
- if (!normalized || (current && normalized === current) || !normalized.startsWith(prefix)) {
1889
- continue;
1890
- }
1891
- const rest = normalized.slice(prefix.length);
1892
- const childName = rest.split("/")[0];
1893
- if (childName) {
1894
- names.set(childName, joinFolderPath(current, childName));
1895
- }
1896
- }
1897
- return Array.from(names.entries())
1898
- .map(([name, path]) => ({ name, path }))
1899
- .sort((a, b) => a.name.localeCompare(b.name));
1900
- }
1901
- function UploadActionIcon(props) {
1902
- if (props.name === "share") {
1903
- return (_jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: [_jsx("path", { d: "M14 4h6v6" }), _jsx("path", { d: "M10 14 20 4" }), _jsx("path", { d: "M20 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4" })] }));
1904
- }
1905
- if (props.name === "link") {
1906
- return (_jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: [_jsx("path", { d: "M10 13a5 5 0 0 0 7.07 0l2.12-2.12a5 5 0 0 0-7.07-7.07L11 4.93" }), _jsx("path", { d: "M14 11a5 5 0 0 0-7.07 0L4.81 13.12a5 5 0 0 0 7.07 7.07L13 19.07" })] }));
1907
- }
1908
- return (_jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: [_jsx("path", { d: "M3 6h18" }), _jsx("path", { d: "M8 6V4h8v2" }), _jsx("path", { d: "M19 6 18 20H6L5 6" }), _jsx("path", { d: "M10 11v5" }), _jsx("path", { d: "M14 11v5" })] }));
1909
- }
1910
- function UploadDrawer(props) {
1911
- const [tab, setTab] = useState("my-files");
1912
- const [uploadSelected, setUploadSelected] = useState([]);
1913
- const [myFiles, setMyFiles] = useState([]);
1914
- const [myFolders, setMyFolders] = useState([]);
1915
- const [mySelectedIds, setMySelectedIds] = useState(() => new Set());
1916
- const [recentMedia, setRecentMedia] = useState([]);
1917
- const [recentTemporaryFiles, setRecentTemporaryFiles] = useState([]);
1918
- const [recentSelectedIds, setRecentSelectedIds] = useState(() => new Set());
1919
- const [recentNextCursor, setRecentNextCursor] = useState(null);
1920
- const [recentLoading, setRecentLoading] = useState(false);
1921
- const [myFolderPath, setMyFolderPath] = useState("");
1922
- const [openFolderMenu, setOpenFolderMenu] = useState(null);
1923
- const [folderDialog, setFolderDialog] = useState(null);
1924
- const [status, setStatus] = useState(null);
1925
- const [error, setError] = useState(null);
1926
- const [confirmDelete, setConfirmDelete] = useState(null);
1927
- const uploadInputRef = useRef(null);
1928
- const myInputRef = useRef(null);
1929
- const recentFiles = useMemo(() => {
1930
- const generated = recentMedia.map((file) => ({ ...file, recentSource: "generated" }));
1931
- const temporary = recentTemporaryFiles.map((file) => ({ ...file, recentSource: "temporary" }));
1932
- return [...temporary, ...generated].sort((a, b) => {
1933
- const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
1934
- const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
1935
- return bTime - aTime;
1936
- });
1937
- }, [recentMedia, recentTemporaryFiles]);
1938
- const confirmAttachments = tab === "upload"
1939
- ? uploadSelected
1940
- : tab === "my-files"
1941
- ? myFiles.filter((file) => mySelectedIds.has(file.id))
1942
- : tab === "recents"
1943
- ? recentFiles.filter((file) => recentSelectedIds.has(file.id))
1944
- : [];
1945
- async function refreshMyFiles() {
1946
- const next = await listMyFiles(props.apiKey);
1947
- setMyFiles(next.attachments);
1948
- setMyFolders(next.folders);
1949
- }
1950
- async function refreshRecentMedia(cursor) {
1951
- setRecentLoading(true);
1952
- try {
1953
- const [next, temporary] = cursor
1954
- ? [await listGeneratedMedia(props.apiKey, cursor), null]
1955
- : await Promise.all([
1956
- listGeneratedMedia(props.apiKey),
1957
- listTemporaryFiles(props.apiKey)
1958
- ]);
1959
- setRecentMedia((current) => cursor ? [...current, ...next.media] : next.media);
1960
- if (temporary) {
1961
- setRecentTemporaryFiles(temporary.files);
1962
- }
1963
- setRecentNextCursor(next.nextCursor);
1964
- if (!cursor) {
1965
- setRecentSelectedIds(new Set());
1966
- }
1967
- }
1968
- finally {
1969
- setRecentLoading(false);
1970
- }
1971
- }
1972
- useEffect(() => {
1973
- if (!props.open) {
1974
- return;
1975
- }
1976
- setError(null);
1977
- setStatus(null);
1978
- void refreshMyFiles().catch((nextError) => setError(nextError instanceof Error ? nextError.message : "Unable to load My Files."));
1979
- void refreshRecentMedia().catch((nextError) => setError(nextError instanceof Error ? nextError.message : "Unable to load recent media."));
1980
- }, [props.open]);
1981
- function resetAndClose() {
1982
- setUploadSelected([]);
1983
- setMySelectedIds(new Set());
1984
- setRecentSelectedIds(new Set());
1985
- setStatus(null);
1986
- setError(null);
1987
- setOpenFolderMenu(null);
1988
- setFolderDialog(null);
1989
- props.onClose();
1990
- }
1991
- async function copyText(value) {
1992
- try {
1993
- if (navigator.clipboard?.writeText) {
1994
- await navigator.clipboard.writeText(value);
1995
- }
1996
- else {
1997
- const textarea = document.createElement("textarea");
1998
- textarea.value = value;
1999
- textarea.style.position = "fixed";
2000
- textarea.style.opacity = "0";
2001
- document.body.appendChild(textarea);
2002
- textarea.focus();
2003
- textarea.select();
2004
- document.execCommand("copy");
2005
- document.body.removeChild(textarea);
2006
- }
2007
- setStatus("File URL copied.");
2008
- }
2009
- catch {
2010
- setError("Unable to copy file URL.");
2011
- }
2012
- }
2013
- async function uploadFiles(files, scope) {
2014
- if (!files.length) {
2015
- return;
2016
- }
2017
- setError(null);
2018
- const uploaded = [];
2019
- for (const file of files) {
2020
- try {
2021
- setStatus(`Uploading ${file.name}...`);
2022
- if (scope === "upload") {
2023
- const temporary = await uploadTemporaryFile({
2024
- apiKey: props.apiKey,
2025
- file,
2026
- folderPath: CHAT_TEMPORARY_UPLOAD_FOLDER
2027
- });
2028
- const attachment = await addInlinePreviewForChatUpload({
2029
- attachment: temporary,
2030
- file,
2031
- currentInlineBytes: estimateInlineAttachmentBytes([...uploadSelected, ...uploaded])
2032
- });
2033
- uploaded.push(attachment);
2034
- continue;
2035
- }
2036
- const attachment = await uploadAttachment({
2037
- apiKey: props.apiKey,
2038
- file,
2039
- folderPath: myFolderPath,
2040
- maxBytes: MY_FILES_UPLOAD_LIMIT_BYTES,
2041
- limitLabel: "50 MB"
2042
- });
2043
- uploaded.push(attachment);
2044
- }
2045
- catch (nextError) {
2046
- setError(nextError instanceof Error ? nextError.message : "Upload failed.");
2047
- }
2048
- }
2049
- if (scope === "upload") {
2050
- setUploadSelected((current) => [...current, ...uploaded]);
2051
- await refreshRecentMedia().catch((nextError) => setError(nextError instanceof Error ? nextError.message : "Unable to refresh recent files."));
2052
- }
2053
- else if (scope === "my-files") {
2054
- await refreshMyFiles();
2055
- setMySelectedIds((current) => new Set([...current, ...uploaded.map((file) => file.id)]));
2056
- }
2057
- if (uploaded.length) {
2058
- setStatus(`${uploaded.length} file${uploaded.length === 1 ? "" : "s"} uploaded.`);
2059
- }
2060
- }
2061
- async function deleteFile(file, scope) {
2062
- const url = scope === "my-files"
2063
- ? `/api/v1/user/me/attachments/${encodeURIComponent(file.id)}`
2064
- : `/api/v1/user/me/temporary-files/${encodeURIComponent(file.id)}`;
2065
- const response = await fetch(url, {
2066
- method: "DELETE",
2067
- headers: buildAuthHeaders(props.apiKey, "application/json")
2068
- });
2069
- const payload = await response.json().catch(() => ({}));
2070
- if (!response.ok) {
2071
- throw new Error(payload.error || "Unable to delete file.");
2072
- }
2073
- if (scope === "my-files") {
2074
- setMyFiles((current) => current.filter((entry) => entry.id !== file.id));
2075
- setMySelectedIds((current) => {
2076
- const next = new Set(current);
2077
- next.delete(file.id);
2078
- return next;
2079
- });
2080
- }
2081
- setConfirmDelete(null);
2082
- setStatus("File deleted.");
2083
- }
2084
- async function createFolder(scope) {
2085
- if (scope !== "my-files" || !folderDialog || folderDialog.mode !== "create") {
2086
- return;
2087
- }
2088
- const folderPath = joinFolderPath(folderDialog.baseFolder, folderDialog.value);
2089
- if (!folderPath) {
2090
- setError("Choose a folder name.");
2091
- return;
2092
- }
2093
- const response = await fetch("/api/v1/user/me/attachments/folders", {
2094
- method: "POST",
2095
- headers: buildAuthHeaders(props.apiKey, "application/json"),
2096
- body: JSON.stringify({ folder_path: folderPath })
2097
- });
2098
- const payload = await response.json().catch(() => ({}));
2099
- if (!response.ok) {
2100
- setError(payload.error || "Unable to create folder.");
2101
- return;
2102
- }
2103
- setMyFolders((current) => Array.from(new Set([...current, folderPath])).sort());
2104
- setFolderDialog(null);
2105
- setStatus("Folder created.");
2106
- }
2107
- async function renameFolder(scope, fromFolder) {
2108
- if (scope !== "my-files" || !folderDialog || folderDialog.mode !== "rename") {
2109
- return;
2110
- }
2111
- const renamedLeaf = folderDialog.value.trim();
2112
- const toFolder = joinFolderPath(parentFolderPath(fromFolder), renamedLeaf);
2113
- if (!toFolder || toFolder === fromFolder) {
2114
- return;
2115
- }
2116
- const response = await fetch("/api/v1/user/me/attachments/folders", {
2117
- method: "PATCH",
2118
- headers: buildAuthHeaders(props.apiKey, "application/json"),
2119
- body: JSON.stringify({ from_folder_path: fromFolder, to_folder_path: toFolder })
2120
- });
2121
- const payload = await response.json().catch(() => ({}));
2122
- if (!response.ok) {
2123
- setError(payload.error || "Unable to rename folder.");
2124
- return;
2125
- }
2126
- setMyFolderPath((current) => current === fromFolder || current.startsWith(`${fromFolder}/`)
2127
- ? `${toFolder}${current.slice(fromFolder.length)}`
2128
- : current);
2129
- await refreshMyFiles();
2130
- setOpenFolderMenu(null);
2131
- setFolderDialog(null);
2132
- setStatus("Folder renamed.");
2133
- }
2134
- async function deleteFolder(scope, folderPath) {
2135
- if (scope !== "my-files") {
2136
- return;
2137
- }
2138
- const response = await fetch("/api/v1/user/me/attachments/folders", {
2139
- method: "DELETE",
2140
- headers: buildAuthHeaders(props.apiKey, "application/json"),
2141
- body: JSON.stringify({ folder_path: folderPath })
2142
- });
2143
- const payload = await response.json().catch(() => ({}));
2144
- if (!response.ok) {
2145
- setError(payload.error || "Unable to delete folder.");
2146
- return;
2147
- }
2148
- setMyFolderPath((current) => current === folderPath || current.startsWith(`${folderPath}/`) ? parentFolderPath(folderPath) : current);
2149
- await refreshMyFiles();
2150
- setOpenFolderMenu(null);
2151
- setFolderDialog(null);
2152
- setStatus("Folder deleted.");
2153
- }
2154
- function renderFolderBrowser(scope) {
2155
- const folders = myFolders;
2156
- const files = myFiles;
2157
- const currentFolder = myFolderPath;
2158
- const setCurrentFolder = setMyFolderPath;
2159
- const childFolders = getImmediateFolders({ folders, files, currentFolder });
2160
- const parts = normalizeFolderPath(currentFolder).split("/").filter(Boolean);
2161
- return (_jsxs("div", { className: "vf-editor-upload-browser", children: [_jsxs("div", { className: "vf-editor-upload-browser-toolbar", children: [_jsxs("div", { className: "vf-editor-upload-breadcrumb", "aria-label": "Current folder", children: [_jsx("button", { type: "button", "data-current": !currentFolder, onClick: () => setCurrentFolder(""), children: "files" }), parts.map((part, index) => {
2162
- const path = parts.slice(0, index + 1).join("/");
2163
- return (_jsxs("span", { className: "vf-editor-upload-breadcrumb-segment", children: [_jsx("span", { "aria-hidden": "true", children: "/" }), _jsx("button", { type: "button", "data-current": path === currentFolder, onClick: () => setCurrentFolder(path), children: part })] }, path));
2164
- })] }), _jsx("button", { type: "button", onClick: () => setFolderDialog({ mode: "create", scope: "my-files", baseFolder: currentFolder, value: "" }), children: "New folder" })] }), childFolders.length ? (_jsx("div", { className: "vf-editor-upload-folder-grid", children: childFolders.map((folder) => (_jsxs("article", { className: "vf-editor-upload-folder-row", children: [_jsxs("button", { type: "button", className: "vf-editor-upload-folder-open", onClick: () => setCurrentFolder(folder.path), children: [_jsx("span", { className: "vf-editor-upload-folder-icon", "aria-hidden": "true" }), _jsx("span", { children: folder.name })] }), _jsx("button", { type: "button", className: "vf-editor-upload-folder-menu-button", "aria-label": `Folder actions for ${folder.name}`, onClick: () => setOpenFolderMenu(openFolderMenu === folder.path ? null : folder.path), children: "..." }), openFolderMenu === folder.path ? (_jsxs("div", { className: "vf-editor-upload-folder-menu", children: [_jsx("button", { type: "button", onClick: () => setFolderDialog({ mode: "rename", scope: "my-files", folderPath: folder.path, value: folderDisplayName(folder.path) }), children: "Rename" }), _jsx("button", { type: "button", "data-danger": "true", onClick: () => setFolderDialog({ mode: "delete", scope: "my-files", folderPath: folder.path, value: "" }), children: "Delete" })] })) : null] }, folder.path))) })) : null] }));
2165
- }
2166
- function renderFileCard(file, scope) {
2167
- const selected = scope === "my-files"
2168
- ? mySelectedIds.has(file.id)
2169
- : scope === "recents"
2170
- ? recentSelectedIds.has(file.id)
2171
- : false;
2172
- const copyUrl = "s3Url" in file && file.s3Url ? file.s3Url : file.viewUrl;
2173
- return (_jsxs("article", { className: "vf-editor-upload-file-card", "data-selected": selected ? "true" : "false", children: [_jsxs("button", { type: "button", className: "vf-editor-upload-file-select", onClick: () => {
2174
- if (scope === "temporary") {
2175
- void copyText(copyUrl);
2176
- return;
2177
- }
2178
- const setSelectedIds = scope === "recents" ? setRecentSelectedIds : setMySelectedIds;
2179
- setSelectedIds((current) => {
2180
- const next = new Set(current);
2181
- if (next.has(file.id)) {
2182
- next.delete(file.id);
2183
- }
2184
- else {
2185
- next.add(file.id);
2186
- }
2187
- return next;
2188
- });
2189
- }, children: [_jsx(FilePreview, { file: file }), _jsxs("span", { className: "vf-editor-upload-file-main", children: [_jsx("span", { className: "vf-editor-upload-file-name", children: file.fileName }), _jsxs("span", { className: "vf-editor-upload-file-meta", children: [formatBytes(file.sizeBytes), file.folderPath ? ` · ${file.folderPath}` : ""] })] })] }), _jsxs("div", { className: "vf-editor-upload-file-actions", children: [scope === "my-files" || scope === "recents" ? (_jsx("span", { className: "vf-editor-upload-selection-indicator", "data-selected": selected ? "true" : "false", "aria-hidden": "true", children: selected ? "✓" : "" })) : null, _jsx("a", { href: file.viewUrl, target: "_blank", rel: "noreferrer", "aria-label": `View ${file.fileName}`, children: _jsx(UploadActionIcon, { name: "share" }) }), _jsx("button", { type: "button", "aria-label": `Copy URL for ${file.fileName}`, onClick: () => void copyText(copyUrl), children: _jsx(UploadActionIcon, { name: "link" }) }), scope === "recents" ? null : (_jsx("button", { type: "button", "aria-label": `Delete ${file.fileName}`, onClick: () => setConfirmDelete(confirmDelete === file.id ? null : file.id), children: _jsx(UploadActionIcon, { name: "trash" }) }))] }), scope !== "recents" && confirmDelete === file.id ? (_jsxs("div", { className: "vf-editor-upload-popconfirm", children: [_jsx("span", { children: "Delete this file?" }), _jsx("button", { type: "button", onClick: () => setConfirmDelete(null), children: "Cancel" }), _jsx("button", { type: "button", "data-danger": "true", onClick: () => void deleteFile(file, scope), children: "Delete" })] })) : null] }, file.id));
2190
- }
2191
- function renderFolderDialog() {
2192
- if (!folderDialog) {
2193
- return null;
2194
- }
2195
- const title = folderDialog.mode === "create"
2196
- ? "New folder"
2197
- : folderDialog.mode === "rename"
2198
- ? "Rename folder"
2199
- : "Delete folder";
2200
- const targetPath = folderDialog.mode === "delete" ? folderDialog.folderPath : "";
2201
- return (_jsxs("div", { className: "vf-editor-upload-folder-dialog", role: "dialog", "aria-label": title, children: [_jsx("div", { className: "vf-editor-upload-folder-dialog-title", children: title }), folderDialog.mode === "delete" ? (_jsxs("div", { className: "vf-editor-upload-folder-dialog-copy", children: ["Delete ", _jsx("strong", { children: targetPath }), " and all files inside it?"] })) : (_jsx("input", { value: folderDialog.value, autoFocus: true, placeholder: "folder-name", onChange: (event) => setFolderDialog({ ...folderDialog, value: event.target.value }), onKeyDown: (event) => {
2202
- if (event.key === "Enter") {
2203
- event.preventDefault();
2204
- if (folderDialog.mode === "create") {
2205
- void createFolder(folderDialog.scope);
2206
- }
2207
- else {
2208
- void renameFolder(folderDialog.scope, folderDialog.folderPath);
2209
- }
2210
- }
2211
- } })), _jsxs("div", { className: "vf-editor-upload-folder-dialog-actions", children: [_jsx("button", { type: "button", onClick: () => setFolderDialog(null), children: "Cancel" }), folderDialog.mode === "create" ? (_jsx("button", { type: "button", className: "vf-editor-upload-confirm", onClick: () => void createFolder(folderDialog.scope), children: "Create" })) : folderDialog.mode === "rename" ? (_jsx("button", { type: "button", className: "vf-editor-upload-confirm", onClick: () => void renameFolder(folderDialog.scope, folderDialog.folderPath), children: "Rename" })) : (_jsx("button", { type: "button", "data-danger": "true", onClick: () => void deleteFolder(folderDialog.scope, folderDialog.folderPath), children: "Delete" }))] })] }));
2212
- }
2213
- if (!props.open) {
2214
- return null;
2215
- }
2216
- const visibleMyFiles = myFiles.filter((file) => normalizeFolderPath(file.folderPath ?? "") === normalizeFolderPath(myFolderPath));
2217
- return createPortal(_jsx("div", { className: "vf-editor-upload-drawer-backdrop", children: _jsxs("aside", { className: "vf-editor-upload-drawer", "aria-label": "Upload drawer", children: [_jsxs("div", { className: "vf-editor-upload-header", children: [_jsxs("div", { children: [_jsx("div", { className: "vf-editor-upload-title", children: "Upload" }), _jsx("div", { className: "vf-editor-upload-subtitle", children: "Upload, attach, and copy file URLs." })] }), _jsx("button", { type: "button", className: "vf-editor-upload-close", "aria-label": "Close upload drawer", onClick: resetAndClose, children: "\u00D7" })] }), _jsxs("div", { className: "vf-editor-upload-tabs", role: "tablist", children: [_jsx("button", { type: "button", "data-active": tab === "upload", onClick: () => setTab("upload"), children: "Upload file" }), _jsx("button", { type: "button", "data-active": tab === "my-files", onClick: () => setTab("my-files"), children: "My Files" }), _jsx("button", { type: "button", "data-active": tab === "recents", onClick: () => setTab("recents"), children: "Recents" })] }), _jsxs("div", { className: "vf-editor-upload-body", children: [error ? _jsx("div", { className: "vf-editor-upload-status", "data-tone": "error", children: error }) : null, status ? _jsx("div", { className: "vf-editor-upload-status", children: status }) : null, renderFolderDialog(), tab === "upload" ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "vf-editor-upload-drop", onClick: () => uploadInputRef.current?.click(), children: ["Upload files to chat", _jsx("span", { children: "Stored in Recents and attached after Confirm. Images get compressed inline previews when possible; other files attach by URL. Max 100 MB each." })] }), _jsx("input", { ref: uploadInputRef, type: "file", multiple: true, hidden: true, onChange: (event) => {
2218
- void uploadFiles(Array.from(event.target.files ?? []), "upload");
2219
- event.target.value = "";
2220
- } }), _jsx("div", { className: "vf-editor-upload-list", children: uploadSelected.map((file) => (_jsxs("article", { className: "vf-editor-upload-file-card", "data-selected": "true", children: [_jsx(FilePreview, { file: file }), _jsxs("span", { className: "vf-editor-upload-file-main", children: [_jsx("span", { className: "vf-editor-upload-file-name", children: file.fileName }), _jsx("span", { className: "vf-editor-upload-file-meta", children: formatBytes(file.sizeBytes) })] }), _jsx("button", { type: "button", onClick: () => setUploadSelected((current) => current.filter((entry) => entry.id !== file.id)), children: "\u00D7" })] }, file.id))) })] })) : tab === "my-files" ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "vf-editor-upload-drop", onClick: () => myInputRef.current?.click(), children: ["Add to My Files", _jsxs("span", { children: ["Uploads into ", folderDisplayName(myFolderPath), " and auto-selects. Max 50 MB each."] })] }), _jsx("input", { ref: myInputRef, type: "file", multiple: true, hidden: true, onChange: (event) => {
2221
- void uploadFiles(Array.from(event.target.files ?? []), "my-files");
2222
- event.target.value = "";
2223
- } }), renderFolderBrowser("my-files"), _jsx("div", { className: "vf-editor-upload-list", children: visibleMyFiles.length ? visibleMyFiles.map((file) => renderFileCard(file, "my-files")) : _jsx("div", { className: "vf-editor-upload-empty", children: "No files in this folder." }) })] })) : tab === "recents" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "vf-editor-upload-list", children: recentFiles.length ? recentFiles.map((file) => renderFileCard(file, "recents")) : (_jsx("div", { className: "vf-editor-upload-empty", children: recentLoading ? "Loading recent files..." : "No recent files yet." })) }), recentNextCursor ? (_jsx("button", { type: "button", className: "vf-editor-upload-more", disabled: recentLoading, onClick: () => void refreshRecentMedia(recentNextCursor).catch((nextError) => setError(nextError instanceof Error ? nextError.message : "Unable to load more recent media.")), children: recentLoading ? "Loading..." : "Load more generated media" })) : null] })) : null] }), _jsxs("div", { className: "vf-editor-upload-footer", children: [_jsx("button", { type: "button", onClick: resetAndClose, children: "Cancel" }), _jsxs("div", { className: "vf-editor-upload-footer-confirm", children: [_jsxs("span", { children: [confirmAttachments.length, " file", confirmAttachments.length === 1 ? "" : "s", " selected"] }), _jsx("button", { type: "button", className: "vf-editor-upload-confirm", disabled: !confirmAttachments.length, onClick: () => {
2224
- props.onConfirm(confirmAttachments);
2225
- resetAndClose();
2226
- }, children: "Confirm" })] })] })] }) }), document.body);
2227
- }
2228
- export function TemplateEditorChat({ boot }) {
2229
- const isBrainstormSurface = boot.template.templateId === "chat-brainstorm";
2230
- const storageKeys = useMemo(() => makeStorageKeys(boot.template.templateId), [boot.template.templateId]);
2231
- const chatStorage = useMemo(() => getChatStorage(), []);
2232
- const fileInputRef = useRef(null);
2233
- const viewportRef = useRef(null);
2234
- const shouldStickToChatBottomRef = useRef(true);
2235
- const forceChatBottomOnNextRenderRef = useRef(false);
2236
- const previousActiveThreadIdRef = useRef(null);
2237
- const activeSendRef = useRef(null);
2238
- const activeJobPollersRef = useRef(new Map());
2239
- const jobPollGateRef = useRef({ nextAvailableAt: 0 });
2240
- const threadsRef = useRef([]);
2241
- const loadedThreadDetailsRef = useRef(new Set());
2242
- const [threads, setThreads] = useState([]);
2243
- const [activeThreadId, setActiveThreadId] = useState(null);
2244
- const [loadingThreadId, setLoadingThreadId] = useState(null);
2245
- const [openThreadMenu, setOpenThreadMenu] = useState(null);
2246
- const [draft, setDraft] = useState("");
2247
- const [pendingAttachments, setPendingAttachments] = useState([]);
2248
- const [lastError, setLastError] = useState(null);
2249
- const [sendingThreadId, setSendingThreadId] = useState(null);
2250
- const [loadingStatus, setLoadingStatus] = useState(null);
2251
- const [hasHydrated, setHasHydrated] = useState(false);
2252
- const [isDropTarget, setIsDropTarget] = useState(false);
2253
- const [isMobileChatOpen, setIsMobileChatOpen] = useState(false);
2254
- const [showHistoryPanel, setShowHistoryPanel] = useState(false);
2255
- const [isUploadDrawerOpen, setIsUploadDrawerOpen] = useState(false);
2256
- const [tracers, setTracers] = useState(() => {
2257
- const bridge = typeof window !== "undefined" ? window.__vidfarmEditorTracers : null;
2258
- if (bridge) {
2259
- return bridge.get();
2260
- }
2261
- if (typeof window !== "undefined") {
2262
- const fromUrl = readTracersFromLocation();
2263
- if (fromUrl.length) {
2264
- return fromUrl;
2265
- }
2266
- }
2267
- return normalizeTracerList(boot.template.tracers || []);
2268
- });
2269
- const threadListUrl = useMemo(() => {
2270
- if (!boot.threadsUrl) {
2271
- return null;
2272
- }
2273
- const url = new URL(boot.threadsUrl, window.location.origin);
2274
- if (!isBrainstormSurface) {
2275
- url.searchParams.set("template_id", boot.template.templateId);
2276
- }
2277
- url.searchParams.set("include_messages", "false");
2278
- url.searchParams.set("limit", isBrainstormSurface ? "200" : "50");
2279
- return url.toString();
2280
- }, [boot.template.templateId, boot.threadsUrl, isBrainstormSurface]);
2281
- const threadDetailUrl = (threadId) => (boot.threadsUrl
2282
- ? new URL(`${boot.threadsUrl.replace(/\/$/, "")}/${encodeURIComponent(threadId)}`, window.location.origin).toString()
2283
- : null);
2284
- useEffect(() => {
2285
- const hydratedThreads = hydrateThreads(chatStorage?.getItem(storageKeys.threads) ?? null, boot.template.templateId);
2286
- const savedActiveThread = chatStorage?.getItem(storageKeys.activeThread) ?? null;
2287
- const requestedThreadId = readThreadIdFromLocation();
2288
- const currentTracers = window.__vidfarmEditorTracers?.get() ?? readTracersFromLocation();
2289
- const nextHydratedThreads = requestedThreadId && !hydratedThreads.some((thread) => thread.id === requestedThreadId)
2290
- ? [
2291
- {
2292
- id: requestedThreadId,
2293
- templateId: boot.template.templateId,
2294
- title: "Loading chat",
2295
- createdAt: new Date().toISOString(),
2296
- updatedAt: new Date().toISOString(),
2297
- lastMessageAt: null,
2298
- messageCount: 0,
2299
- tracers: currentTracers,
2300
- hiddenFromView: false,
2301
- messages: []
2302
- },
2303
- ...hydratedThreads
2304
- ]
2305
- : hydratedThreads;
2306
- const visibleHydratedThreads = nextHydratedThreads.filter((thread) => !thread.hiddenFromView);
2307
- const newestMatchingThread = visibleHydratedThreads.find((thread) => threadMatchesTracers(thread, currentTracers));
2308
- threadsRef.current = nextHydratedThreads;
2309
- setThreads(nextHydratedThreads);
2310
- if (requestedThreadId
2311
- && visibleHydratedThreads.some((thread) => thread.id === requestedThreadId)) {
2312
- setActiveThreadId(requestedThreadId);
2313
- }
2314
- else if (savedActiveThread
2315
- && visibleHydratedThreads.some((thread) => thread.id === savedActiveThread)
2316
- && (!currentTracers.length || visibleHydratedThreads.some((thread) => thread.id === savedActiveThread && threadMatchesTracers(thread, currentTracers)))) {
2317
- setActiveThreadId(savedActiveThread);
2318
- }
2319
- else {
2320
- setActiveThreadId(newestMatchingThread?.id ?? visibleHydratedThreads[0]?.id ?? null);
2321
- }
2322
- setHasHydrated(true);
2323
- }, [boot.template.templateId, chatStorage, storageKeys]);
2324
- useEffect(() => {
2325
- if (!boot.vidfarmApiKey || !threadListUrl) {
2326
- return;
2327
- }
2328
- const abortController = new AbortController();
2329
- void (async () => {
2330
- try {
2331
- const remoteThreads = [];
2332
- let nextCursor = null;
2333
- const seenCursors = new Set();
2334
- do {
2335
- const pageUrl = new URL(threadListUrl);
2336
- if (nextCursor) {
2337
- pageUrl.searchParams.set("cursor", nextCursor);
2338
- }
2339
- const response = await fetch(pageUrl.toString(), {
2340
- method: "GET",
2341
- headers: buildAuthHeaders(boot.vidfarmApiKey),
2342
- signal: abortController.signal
2343
- });
2344
- if (!response.ok) {
2345
- return;
2346
- }
2347
- const payload = await response.json().catch(() => null);
2348
- if (Array.isArray(payload?.threads)) {
2349
- remoteThreads.push(...payload.threads
2350
- .map((thread) => hydrateThreadRecord(thread, boot.template.templateId))
2351
- .filter((thread) => Boolean(thread)));
2352
- }
2353
- const candidateCursor = typeof payload?.next_cursor === "string" && payload.next_cursor ? payload.next_cursor : null;
2354
- if (!candidateCursor || seenCursors.has(candidateCursor)) {
2355
- nextCursor = null;
2356
- }
2357
- else {
2358
- seenCursors.add(candidateCursor);
2359
- nextCursor = candidateCursor;
2360
- }
2361
- } while (nextCursor && !abortController.signal.aborted);
2362
- if (!remoteThreads.length) {
2363
- return;
2364
- }
2365
- setThreads((current) => {
2366
- const nextThreads = mergeThreadSets(current, remoteThreads);
2367
- threadsRef.current = nextThreads;
2368
- return nextThreads;
2369
- });
2370
- setActiveThreadId((current) => {
2371
- if (current && remoteThreads.some((thread) => thread.id === current)) {
2372
- return current;
2373
- }
2374
- return remoteThreads[0]?.id ?? current;
2375
- });
2376
- }
2377
- catch (error) {
2378
- if (error instanceof DOMException && error.name === "AbortError") {
2379
- return;
2380
- }
2381
- }
2382
- })();
2383
- return () => {
2384
- abortController.abort();
2385
- };
2386
- }, [boot.template.templateId, boot.vidfarmApiKey, threadListUrl]);
2387
- useEffect(() => {
2388
- threadsRef.current = threads;
2389
- }, [threads]);
2390
- useEffect(() => {
2391
- const bridge = window.__vidfarmEditorTracers;
2392
- if (bridge) {
2393
- setTracers(bridge.get());
2394
- return bridge.subscribe((nextTracers) => setTracers(nextTracers));
2395
- }
2396
- const syncFromLocation = () => {
2397
- const fromUrl = readTracersFromLocation();
2398
- setTracers(fromUrl.length ? fromUrl : normalizeTracerList(boot.template.tracers || []));
2399
- };
2400
- syncFromLocation();
2401
- window.addEventListener("popstate", syncFromLocation);
2402
- return () => {
2403
- window.removeEventListener("popstate", syncFromLocation);
2404
- };
2405
- }, [boot.template.tracers]);
2406
- useEffect(() => {
2407
- if (!hasHydrated) {
2408
- return;
2409
- }
2410
- try {
2411
- chatStorage?.setItem(storageKeys.threads, JSON.stringify(serializeThreads(threads, activeThreadId)));
2412
- }
2413
- catch (error) {
2414
- console.warn("Unable to persist editor chat threads.", error);
2415
- }
2416
- }, [activeThreadId, chatStorage, hasHydrated, storageKeys, threads]);
2417
- useEffect(() => {
2418
- if (!hasHydrated) {
2419
- return;
2420
- }
2421
- try {
2422
- if (activeThreadId) {
2423
- chatStorage?.setItem(storageKeys.activeThread, activeThreadId);
2424
- }
2425
- else {
2426
- chatStorage?.removeItem(storageKeys.activeThread);
2427
- }
2428
- }
2429
- catch (error) {
2430
- console.warn("Unable to persist active editor chat thread.", error);
2431
- }
2432
- }, [activeThreadId, chatStorage, hasHydrated, storageKeys]);
2433
- const activeThread = useMemo(() => threads.find((thread) => thread.id === activeThreadId) ?? null, [activeThreadId, threads]);
2434
- const openThreadMenuThread = useMemo(() => threads.find((thread) => thread.id === openThreadMenu?.threadId) ?? null, [openThreadMenu?.threadId, threads]);
2435
- useEffect(() => {
2436
- if (!hasHydrated || !boot.vidfarmApiKey || !activeThreadId) {
2437
- return;
2438
- }
2439
- const existingThread = threadsRef.current.find((thread) => thread.id === activeThreadId);
2440
- if (loadedThreadDetailsRef.current.has(activeThreadId)) {
2441
- return;
2442
- }
2443
- const requestedThreadId = readThreadIdFromLocation();
2444
- if (existingThread
2445
- && activeThreadId !== requestedThreadId
2446
- && (existingThread.messageCount ?? 0) === 0) {
2447
- loadedThreadDetailsRef.current.add(activeThreadId);
2448
- return;
2449
- }
2450
- const url = threadDetailUrl(activeThreadId);
2451
- if (!url) {
2452
- return;
2453
- }
2454
- const abortController = new AbortController();
2455
- const shouldShowLoadingState = !existingThread?.messages.length;
2456
- if (shouldShowLoadingState) {
2457
- setLoadingThreadId(activeThreadId);
2458
- }
2459
- void (async () => {
2460
- try {
2461
- const response = await fetch(url, {
2462
- method: "GET",
2463
- headers: buildAuthHeaders(boot.vidfarmApiKey),
2464
- signal: abortController.signal
2465
- });
2466
- if (!response.ok) {
2467
- const isLocalOnlyThread = Boolean(existingThread && activeThreadId !== requestedThreadId);
2468
- if (response.status === 404 && !isLocalOnlyThread) {
2469
- setLastError(`Chat ID ${activeThreadId} was not found for this account.`);
2470
- }
2471
- loadedThreadDetailsRef.current.add(activeThreadId);
2472
- return;
2473
- }
2474
- const payload = await response.json().catch(() => null);
2475
- const remoteThread = payload?.thread ? hydrateThreadRecord(payload.thread, boot.template.templateId) : null;
2476
- if (!remoteThread) {
2477
- return;
2478
- }
2479
- loadedThreadDetailsRef.current.add(remoteThread.id);
2480
- setLastError(null);
2481
- setThreads((current) => {
2482
- const nextThreads = mergeThreadSets(current, [remoteThread]);
2483
- threadsRef.current = nextThreads;
2484
- return nextThreads;
2485
- });
2486
- }
2487
- catch (error) {
2488
- if (error instanceof DOMException && error.name === "AbortError") {
2489
- return;
2490
- }
2491
- }
2492
- finally {
2493
- if (!abortController.signal.aborted) {
2494
- setLoadingThreadId((current) => (current === activeThreadId ? null : current));
2495
- }
2496
- }
2497
- })();
2498
- return () => {
2499
- abortController.abort();
2500
- setLoadingThreadId((current) => (current === activeThreadId ? null : current));
2501
- };
2502
- }, [activeThreadId, boot.vidfarmApiKey, boot.threadsUrl, hasHydrated]);
2503
- useEffect(() => {
2504
- if (!hasHydrated || activeThread && !activeThread.hiddenFromView) {
2505
- return;
2506
- }
2507
- const newestMatchingThread = threads.find((thread) => !thread.hiddenFromView && threadMatchesTracers(thread, tracers));
2508
- setActiveThreadId(newestMatchingThread?.id ?? threads.find((thread) => !thread.hiddenFromView)?.id ?? null);
2509
- }, [activeThread, hasHydrated, threads, tracers]);
2510
- const requestTracers = useMemo(() => activeThread?.tracers.length ? activeThread.tracers : tracers, [activeThread?.tracers, tracers]);
2511
- const visibleThreads = useMemo(() => (threads.filter((thread) => !thread.hiddenFromView).sort((a, b) => {
2512
- const aMatches = threadMatchesTracers(a, tracers);
2513
- const bMatches = threadMatchesTracers(b, tracers);
2514
- if (aMatches !== bMatches) {
2515
- return aMatches ? -1 : 1;
2516
- }
2517
- return Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
2518
- })), [threads, tracers]);
2519
- const defaultRequestTracer = requestTracers[0] ?? null;
2520
- const messages = activeThread?.messages ?? [];
2521
- const isAnyThreadSending = sendingThreadId !== null;
2522
- const isActiveThreadSending = Boolean(activeThreadId && sendingThreadId === activeThreadId);
2523
- const canSend = useMemo(() => (draft.trim() || pendingAttachments.length) && !isAnyThreadSending, [draft, pendingAttachments.length, isAnyThreadSending]);
2524
- useEffect(() => {
2525
- if (!hasHydrated || !boot.vidfarmApiKey || !activeThread || !activeThread.messages.length) {
2526
- return;
2527
- }
2528
- resumePersistedJobPolling(activeThread);
2529
- }, [activeThread, boot.vidfarmApiKey, hasHydrated]);
2530
- async function updateRemoteThreadState(thread, overrides) {
2531
- if (!boot.vidfarmApiKey || !boot.threadsUrl || !thread.messages.length) {
2532
- return;
2533
- }
2534
- const url = threadDetailUrl(thread.id);
2535
- if (!url) {
2536
- return;
2537
- }
2538
- await fetch(url, {
2539
- method: "PATCH",
2540
- headers: buildAuthHeaders(boot.vidfarmApiKey, "application/json"),
2541
- body: JSON.stringify({
2542
- template_id: boot.template.templateId,
2543
- title: overrides?.title ?? thread.title,
2544
- tracers: overrides?.tracers ?? thread.tracers,
2545
- hidden_from_view: overrides?.hiddenFromView ?? (thread.hiddenFromView === true),
2546
- archived: overrides?.archived
2547
- })
2548
- }).catch(() => { });
2549
- }
2550
- async function appendRemoteThreadExchange(input) {
2551
- if (!boot.vidfarmApiKey || !boot.threadsUrl || !input.messages.length) {
2552
- return;
2553
- }
2554
- const url = threadDetailUrl(input.threadId);
2555
- if (!url) {
2556
- return;
2557
- }
2558
- await fetch(`${url}/exchanges`, {
2559
- method: "POST",
2560
- headers: buildAuthHeaders(boot.vidfarmApiKey, "application/json"),
2561
- body: JSON.stringify({
2562
- template_id: input.templateId ?? boot.template.templateId,
2563
- title: input.title || "New chat",
2564
- tracers: input.tracers,
2565
- messages: input.messages.map((message) => ({
2566
- id: message.id,
2567
- role: message.role,
2568
- text: message.text,
2569
- attachments: message.attachments.map((attachment) => ({
2570
- id: attachment.id,
2571
- fileName: attachment.fileName,
2572
- contentType: attachment.contentType,
2573
- viewUrl: attachment.viewUrl
2574
- })),
2575
- toolExchanges: message.toolExchanges,
2576
- httpExchanges: message.httpExchanges?.map(sanitizeHttpExchange)
2577
- }))
2578
- })
2579
- }).catch(() => { });
2580
- }
2581
- async function deleteRemoteThreadState(threadId) {
2582
- if (!boot.vidfarmApiKey || !boot.threadsUrl) {
2583
- return;
2584
- }
2585
- const url = threadDetailUrl(threadId);
2586
- if (!url) {
2587
- return;
2588
- }
2589
- await fetch(url, {
2590
- method: "DELETE",
2591
- headers: buildAuthHeaders(boot.vidfarmApiKey)
2592
- }).catch(() => { });
2593
- }
2594
- function syncTracerUrl(nextTracers) {
2595
- const url = new URL(window.location.href);
2596
- if (nextTracers.length) {
2597
- url.searchParams.set("tracers", nextTracers.join(","));
2598
- }
2599
- else {
2600
- url.searchParams.delete("tracers");
2601
- }
2602
- window.history.replaceState({}, "", url.toString());
2603
- window.dispatchEvent(new CustomEvent("vidfarm:editor-tracers-changed", {
2604
- detail: {
2605
- tracers: nextTracers
2606
- }
2607
- }));
2608
- }
2609
- function applyTracerList(nextTracers) {
2610
- const normalized = normalizeTracerList(nextTracers);
2611
- const bridge = window.__vidfarmEditorTracers;
2612
- if (bridge) {
2613
- bridge.set(normalized);
2614
- return;
2615
- }
2616
- setTracers(normalized);
2617
- syncTracerUrl(normalized);
2618
- }
2619
- function createTracer() {
2620
- const bridge = window.__vidfarmEditorTracers;
2621
- if (bridge) {
2622
- return bridge.add();
2623
- }
2624
- const tracer = makeTracerId(boot.template.templateSlug, tracers);
2625
- applyTracerList([tracer, ...tracers]);
2626
- return tracer;
2627
- }
2628
- function addAiTracer(tracer) {
2629
- const existing = window.__vidfarmEditorTracers?.get() ?? tracers;
2630
- const normalized = tracer ? ensureTracerHasRandomSuffix(tracer, existing) : null;
2631
- const bridge = window.__vidfarmEditorTracers;
2632
- if (bridge) {
2633
- return bridge.add(normalized ?? undefined);
2634
- }
2635
- if (normalized) {
2636
- applyTracerList([normalized, ...existing.filter((entry) => entry !== normalized)]);
2637
- return normalized;
2638
- }
2639
- return createTracer();
2640
- }
2641
- function setDefaultRequestTracerSelection(tracer) {
2642
- const normalized = tracer.trim();
2643
- if (!normalized) {
2644
- return;
2645
- }
2646
- const bridge = window.__vidfarmEditorTracers;
2647
- if (bridge) {
2648
- bridge.add(normalized);
2649
- }
2650
- else if (!tracers.includes(normalized)) {
2651
- applyTracerList([normalized, ...tracers]);
2652
- }
2653
- if (activeThread) {
2654
- updateThread(activeThread.id, (thread) => ({
2655
- ...thread,
2656
- tracers: [normalized, ...thread.tracers.filter((entry) => entry !== normalized)],
2657
- updatedAt: new Date().toISOString()
2658
- }));
2659
- }
2660
- }
2661
- function removeTracer(tracer) {
2662
- const bridge = window.__vidfarmEditorTracers;
2663
- if (bridge) {
2664
- bridge.remove(tracer);
2665
- return;
2666
- }
2667
- const remaining = tracers.filter((entry) => entry !== tracer);
2668
- applyTracerList(remaining);
2669
- }
2670
- useEffect(() => {
2671
- syncTracerUrl(tracers);
2672
- // eslint-disable-next-line react-hooks/exhaustive-deps
2673
- }, []);
2674
- useEffect(() => {
2675
- const viewport = viewportRef.current;
2676
- if (!viewport) {
2677
- return;
2678
- }
2679
- const handleScroll = () => {
2680
- shouldStickToChatBottomRef.current = isScrolledNearBottom(viewport);
2681
- if (!shouldStickToChatBottomRef.current) {
2682
- forceChatBottomOnNextRenderRef.current = false;
2683
- }
2684
- };
2685
- handleScroll();
2686
- viewport.addEventListener("scroll", handleScroll, { passive: true });
2687
- return () => {
2688
- viewport.removeEventListener("scroll", handleScroll);
2689
- };
2690
- }, []);
2691
- useLayoutEffect(() => {
2692
- const viewport = viewportRef.current;
2693
- if (!viewport) {
2694
- return;
2695
- }
2696
- const activeThreadChanged = previousActiveThreadIdRef.current !== activeThreadId;
2697
- previousActiveThreadIdRef.current = activeThreadId;
2698
- if (activeThreadChanged || forceChatBottomOnNextRenderRef.current || shouldStickToChatBottomRef.current) {
2699
- viewport.scrollTo({ top: viewport.scrollHeight, behavior: "auto" });
2700
- shouldStickToChatBottomRef.current = true;
2701
- forceChatBottomOnNextRenderRef.current = false;
2702
- }
2703
- }, [messages, activeThreadId]);
2704
- useEffect(() => {
2705
- return () => {
2706
- activeSendRef.current?.abortController.abort();
2707
- activeSendRef.current = null;
2708
- for (const poller of activeJobPollersRef.current.values()) {
2709
- if (poller.timeoutId !== null) {
2710
- window.clearTimeout(poller.timeoutId);
2711
- }
2712
- poller.abortController?.abort();
2713
- }
2714
- activeJobPollersRef.current.clear();
2715
- };
2716
- }, []);
2717
- useEffect(() => {
2718
- if (!openThreadMenu) {
2719
- return;
2720
- }
2721
- const closeMenu = () => setOpenThreadMenu(null);
2722
- document.addEventListener("click", closeMenu);
2723
- window.addEventListener("resize", closeMenu);
2724
- window.addEventListener("scroll", closeMenu, true);
2725
- return () => {
2726
- document.removeEventListener("click", closeMenu);
2727
- window.removeEventListener("resize", closeMenu);
2728
- window.removeEventListener("scroll", closeMenu, true);
2729
- };
2730
- }, [openThreadMenu]);
2731
- useEffect(() => {
2732
- const resumePolling = () => {
2733
- if (document.visibilityState !== "visible" || !navigator.onLine) {
2734
- return;
2735
- }
2736
- const now = Date.now();
2737
- for (const poller of activeJobPollersRef.current.values()) {
2738
- if (poller.inFlight) {
2739
- continue;
2740
- }
2741
- if (poller.nextPollAt !== null && poller.nextPollAt <= now && poller.runPoll) {
2742
- scheduleNextJobPollForPoller(poller, poller.runPoll, poller.nextPollAt - now);
2743
- }
2744
- }
2745
- };
2746
- window.addEventListener("online", resumePolling);
2747
- document.addEventListener("visibilitychange", resumePolling);
2748
- return () => {
2749
- window.removeEventListener("online", resumePolling);
2750
- document.removeEventListener("visibilitychange", resumePolling);
2751
- };
2752
- }, []);
2753
- function ensureActiveThread(seedText) {
2754
- if (activeThread) {
2755
- return activeThread.id;
2756
- }
2757
- const now = new Date().toISOString();
2758
- const threadId = makeThreadId();
2759
- const nextThread = {
2760
- id: threadId,
2761
- templateId: boot.template.templateId,
2762
- title: formatThreadTitle(seedText || ""),
2763
- createdAt: now,
2764
- updatedAt: now,
2765
- lastMessageAt: null,
2766
- messageCount: 0,
2767
- tracers: normalizeTracerList(tracers),
2768
- messages: []
2769
- };
2770
- threadsRef.current = [nextThread, ...threadsRef.current];
2771
- setThreads((current) => [nextThread, ...current]);
2772
- setActiveThreadId(threadId);
2773
- return threadId;
2774
- }
2775
- function createNewThread(options) {
2776
- const now = new Date().toISOString();
2777
- const threadTracers = normalizeTracerList(options?.tracers ?? tracers);
2778
- const threadId = makeThreadId();
2779
- const nextThread = {
2780
- id: threadId,
2781
- templateId: boot.template.templateId,
2782
- title: options?.title || "New chat",
2783
- createdAt: now,
2784
- updatedAt: now,
2785
- lastMessageAt: null,
2786
- messageCount: 0,
2787
- tracers: threadTracers,
2788
- messages: []
2789
- };
2790
- threadsRef.current = [nextThread, ...threadsRef.current];
2791
- setThreads((current) => [nextThread, ...current]);
2792
- if (options?.activate !== false) {
2793
- setActiveThreadId(threadId);
2794
- setDraft("");
2795
- setPendingAttachments([]);
2796
- setLastError(null);
2797
- }
2798
- setOpenThreadMenu(null);
2799
- return threadId;
2800
- }
2801
- function createNewThreadFromClick() {
2802
- createNewThread();
2803
- }
2804
- function deleteThread(threadId) {
2805
- const thread = threads.find((entry) => entry.id === threadId);
2806
- if (!thread) {
2807
- return;
2808
- }
2809
- if (!window.confirm(`Delete conversation "${thread.title}"?`)) {
2810
- return;
2811
- }
2812
- const remainingThreads = threads.filter((entry) => entry.id !== threadId);
2813
- threadsRef.current = threadsRef.current.filter((entry) => entry.id !== threadId);
2814
- for (const [pollerKey, poller] of activeJobPollersRef.current.entries()) {
2815
- if (poller.threadId === threadId) {
2816
- stopJobPoller(pollerKey);
2817
- }
2818
- }
2819
- setThreads(remainingThreads);
2820
- if (activeThreadId === threadId) {
2821
- setActiveThreadId(remainingThreads[0]?.id ?? null);
2822
- }
2823
- setLastError(null);
2824
- setOpenThreadMenu(null);
2825
- void deleteRemoteThreadState(threadId);
2826
- }
2827
- function updateThread(threadId, updater) {
2828
- const nextThreads = sortThreadsByUpdatedAt(threadsRef.current.map((thread) => (thread.id === threadId ? updater(thread) : thread)));
2829
- threadsRef.current = nextThreads;
2830
- setThreads(nextThreads);
2831
- }
2832
- function attachTracersToThread(threadId, nextTracers) {
2833
- const normalized = normalizeTracerList(nextTracers);
2834
- updateThread(threadId, (thread) => ({
2835
- ...thread,
2836
- tracers: mergeTracerLists(thread.tracers, normalized),
2837
- updatedAt: new Date().toISOString()
2838
- }));
2839
- const thread = threadsRef.current.find((entry) => entry.id === threadId);
2840
- if (thread) {
2841
- void updateRemoteThreadState({
2842
- ...thread,
2843
- tracers: mergeTracerLists(thread.tracers, normalized),
2844
- updatedAt: new Date().toISOString()
2845
- }, {
2846
- tracers: mergeTracerLists(thread.tracers, normalized)
2847
- });
2848
- }
2849
- }
2850
- function findThreadIdForTracer(tracer, exceptThreadId) {
2851
- const normalized = tracer.trim();
2852
- if (!normalized) {
2853
- return null;
2854
- }
2855
- return threadsRef.current.find((thread) => (!thread.hiddenFromView && thread.id !== exceptThreadId && thread.tracers.includes(normalized)))?.id ?? null;
2856
- }
2857
- function ensureThreadForTracer(tracer, options) {
2858
- const existing = findThreadIdForTracer(tracer);
2859
- if (existing) {
2860
- return existing;
2861
- }
2862
- return createNewThread({
2863
- tracers: [tracer],
2864
- title: options?.title || `Tracer ${tracer}`,
2865
- activate: options?.activate ?? false
2866
- });
2867
- }
2868
- function userRequestedSeparateTracerThreads(text) {
2869
- const normalized = text.toLowerCase();
2870
- return /(?:own|separate|different|new)\s+(?:tracer\s+and\s+)?chat\s+threads?/.test(normalized)
2871
- || /chat\s+threads?.{0,80}(?:own|separate|different|new)\s+tracers?/.test(normalized)
2872
- || /each.{0,80}(?:own|separate|different|new).{0,80}chat\s+threads?/.test(normalized)
2873
- || /(?:own|separate|different|new)\s+tracers?/.test(normalized)
2874
- || /(?:each|every|per|one\s+per).{0,80}(?:own|separate|different|new).{0,80}tracers?/.test(normalized)
2875
- || /(?:own|separate|different|new)\s+tracers?.{0,80}(?:each|every|per|outputs?|pieces?|posts?|jobs?|generations?)/.test(normalized)
2876
- || /(?:outputs?|pieces?|posts?|jobs?|generations?).{0,80}(?:own|separate|different|new)\s+tracers?/.test(normalized);
2877
- }
2878
- function mirrorJobToTracerThread(input) {
2879
- const existingThreadId = findThreadIdForTracer(input.tracer, input.sourceThreadId);
2880
- if (!existingThreadId && !input.shouldCreateThread) {
2881
- return null;
2882
- }
2883
- const targetThreadId = existingThreadId ?? ensureThreadForTracer(input.tracer, {
2884
- title: `Tracer ${input.tracer}`,
2885
- activate: false
2886
- });
2887
- if (!targetThreadId || targetThreadId === input.sourceThreadId) {
2888
- return null;
2889
- }
2890
- const messageId = makeMessageId("assistant");
2891
- const now = new Date().toISOString();
2892
- const messageText = input.job.status === "succeeded" || input.job.status === "failed" || input.job.status === "cancelled"
2893
- ? buildJobCompletionMessage(input.job)
2894
- : buildJobQueuedMessage(input.job);
2895
- const statusText = input.job.status === "succeeded" || input.job.status === "failed" || input.job.status === "cancelled"
2896
- ? null
2897
- : `Job ${input.job.job_id} is being tracked. ${getJobPollCadenceText()}`;
2898
- const mirroredMessage = {
2899
- id: messageId,
2900
- role: "assistant",
2901
- text: messageText,
2902
- attachments: [],
2903
- httpExchanges: input.httpExchange ? [input.httpExchange] : [],
2904
- statusText,
2905
- isPending: false
2906
- };
2907
- updateThread(targetThreadId, (thread) => ({
2908
- ...thread,
2909
- updatedAt: now,
2910
- messages: [
2911
- ...thread.messages,
2912
- mirroredMessage
2913
- ]
2914
- }));
2915
- const targetThread = threadsRef.current.find((thread) => thread.id === targetThreadId);
2916
- if (targetThread) {
2917
- void appendRemoteThreadExchange({
2918
- threadId: targetThreadId,
2919
- templateId: targetThread.templateId,
2920
- title: targetThread.title,
2921
- tracers: targetThread.tracers,
2922
- messages: [mirroredMessage]
2923
- });
2924
- }
2925
- return { threadId: targetThreadId, messageId };
2926
- }
2927
- function editThreadTracers(threadId) {
2928
- const thread = threads.find((entry) => entry.id === threadId);
2929
- if (!thread) {
2930
- return;
2931
- }
2932
- const value = window.prompt("Thread tracers, comma separated", thread.tracers.join(", "));
2933
- if (value === null) {
2934
- return;
2935
- }
2936
- const nextTracers = normalizeTracerList(value.split(","));
2937
- updateThread(threadId, (current) => ({
2938
- ...current,
2939
- tracers: nextTracers,
2940
- updatedAt: new Date().toISOString()
2941
- }));
2942
- void updateRemoteThreadState(thread, { tracers: nextTracers });
2943
- setOpenThreadMenu(null);
2944
- }
2945
- function archiveThread(threadId) {
2946
- const thread = threads.find((entry) => entry.id === threadId);
2947
- if (!thread) {
2948
- return;
2949
- }
2950
- const confirmed = window.confirm(`Archive conversation "${thread.title}"? Its tracer links will be prefixed with archived_.`);
2951
- if (!confirmed) {
2952
- return;
2953
- }
2954
- const nextTracers = normalizeTracerList(thread.tracers.map(archiveTracerValue));
2955
- updateThread(threadId, (current) => ({
2956
- ...current,
2957
- tracers: nextTracers,
2958
- updatedAt: new Date().toISOString()
2959
- }));
2960
- void updateRemoteThreadState(thread, { tracers: nextTracers, archived: true });
2961
- setOpenThreadMenu(null);
2962
- }
2963
- function removeThreadFromView(threadId) {
2964
- const thread = threads.find((entry) => entry.id === threadId);
2965
- if (!thread) {
2966
- return;
2967
- }
2968
- const confirmed = window.confirm(`Remove conversation "${thread.title}" from this frontend view? The thread and tracer links stay available; Delete removes the thread completely.`);
2969
- if (!confirmed) {
2970
- return;
2971
- }
2972
- for (const [pollerKey, poller] of activeJobPollersRef.current.entries()) {
2973
- if (poller.threadId === threadId) {
2974
- stopJobPoller(pollerKey);
2975
- }
2976
- }
2977
- updateThread(threadId, (current) => ({
2978
- ...current,
2979
- hiddenFromView: true,
2980
- updatedAt: new Date().toISOString()
2981
- }));
2982
- if (activeThreadId === threadId) {
2983
- const nextThread = visibleThreads.find((entry) => entry.id !== threadId);
2984
- setActiveThreadId(nextThread?.id ?? null);
2985
- }
2986
- setOpenThreadMenu(null);
2987
- setLastError(null);
2988
- void updateRemoteThreadState(thread, { hiddenFromView: true });
2989
- }
2990
- async function copyThreadId(threadId) {
2991
- try {
2992
- if (navigator.clipboard?.writeText) {
2993
- await navigator.clipboard.writeText(threadId);
2994
- }
2995
- else {
2996
- const textarea = document.createElement("textarea");
2997
- textarea.value = threadId;
2998
- textarea.setAttribute("readonly", "");
2999
- textarea.style.position = "fixed";
3000
- textarea.style.opacity = "0";
3001
- document.body.appendChild(textarea);
3002
- textarea.select();
3003
- document.execCommand("copy");
3004
- document.body.removeChild(textarea);
3005
- }
3006
- setLastError(null);
3007
- }
3008
- catch {
3009
- setLastError("Unable to copy chat ID.");
3010
- }
3011
- finally {
3012
- setOpenThreadMenu(null);
3013
- }
3014
- }
3015
- function toggleThreadMenu(threadId, button) {
3016
- if (openThreadMenu?.threadId === threadId) {
3017
- setOpenThreadMenu(null);
3018
- return;
3019
- }
3020
- const rect = button.getBoundingClientRect();
3021
- const menuWidth = 148;
3022
- setOpenThreadMenu({
3023
- threadId,
3024
- top: rect.bottom + 6,
3025
- left: Math.max(8, Math.min(window.innerWidth - menuWidth - 8, rect.right - menuWidth))
3026
- });
3027
- }
3028
- function stopJobPoller(pollerKey) {
3029
- const poller = activeJobPollersRef.current.get(pollerKey);
3030
- if (!poller) {
3031
- return;
3032
- }
3033
- if (poller.timeoutId !== null) {
3034
- window.clearTimeout(poller.timeoutId);
3035
- }
3036
- poller.abortController?.abort();
3037
- activeJobPollersRef.current.delete(pollerKey);
3038
- }
3039
- function scheduleNextJobPoll(pollerKey, runPoll, delayMs) {
3040
- const poller = activeJobPollersRef.current.get(pollerKey);
3041
- if (!poller) {
3042
- return;
3043
- }
3044
- scheduleNextJobPollForPoller(poller, runPoll, delayMs);
3045
- }
3046
- function scheduleNextJobPollForPoller(poller, runPoll, delayMs) {
3047
- if (poller.timeoutId !== null) {
3048
- window.clearTimeout(poller.timeoutId);
3049
- }
3050
- const safeDelayMs = Math.max(JOB_POLL_MIN_TIMER_MS, delayMs);
3051
- poller.nextPollAt = Date.now() + safeDelayMs;
3052
- poller.timeoutId = window.setTimeout(() => {
3053
- poller.timeoutId = null;
3054
- void runPoll();
3055
- }, safeDelayMs);
3056
- }
3057
- function getJobPollIntervalMs() {
3058
- return activeJobPollersRef.current.size >= JOB_POLL_BUSY_THRESHOLD
3059
- ? JOB_POLL_BUSY_INTERVAL_MS
3060
- : JOB_POLL_INTERVAL_MS;
3061
- }
3062
- function formatJobPollInterval(ms = getJobPollIntervalMs()) {
3063
- if (ms >= 60_000) {
3064
- const minutes = Math.round(ms / 60_000);
3065
- return `${minutes} min${minutes === 1 ? "" : "s"}`;
3066
- }
3067
- return `${Math.round(ms / 1_000)}s`;
3068
- }
3069
- function getJobPollCadenceText(ms = getJobPollIntervalMs()) {
3070
- return activeJobPollersRef.current.size >= JOB_POLL_BUSY_THRESHOLD
3071
- ? `Many renders are active, so the next check is in ${formatJobPollInterval(ms)}.`
3072
- : `Checking again gently every ${formatJobPollInterval(ms)}.`;
3073
- }
3074
- function rescheduleActiveJobPollersForLoad() {
3075
- const intervalMs = getJobPollIntervalMs();
3076
- if (intervalMs === JOB_POLL_INTERVAL_MS) {
3077
- return;
3078
- }
3079
- const now = Date.now();
3080
- for (const poller of activeJobPollersRef.current.values()) {
3081
- if (poller.inFlight || !poller.runPoll) {
3082
- continue;
3083
- }
3084
- const remainingMs = poller.nextPollAt === null ? 0 : poller.nextPollAt - now;
3085
- if (remainingMs < intervalMs) {
3086
- scheduleNextJobPollForPoller(poller, poller.runPoll, intervalMs);
3087
- }
3088
- }
3089
- }
3090
- function reserveJobPollSlot(now) {
3091
- const slotAt = Math.max(now, jobPollGateRef.current.nextAvailableAt);
3092
- jobPollGateRef.current.nextAvailableAt = slotAt + JOB_POLL_GLOBAL_MIN_GAP_MS;
3093
- return slotAt - now;
3094
- }
3095
- function scheduleJobPolling(input) {
3096
- const pollerKey = makeJobPollerKey(input.threadId, input.assistantMessageId, input.jobId);
3097
- if (activeJobPollersRef.current.has(pollerKey)) {
3098
- return;
3099
- }
3100
- activeJobPollersRef.current.set(pollerKey, {
3101
- timeoutId: null,
3102
- abortController: null,
3103
- runPoll: null,
3104
- inFlight: false,
3105
- nextPollAt: null,
3106
- lastPollAt: null,
3107
- attempts: 0,
3108
- threadId: input.threadId
3109
- });
3110
- const runPoll = async () => {
3111
- const poller = activeJobPollersRef.current.get(pollerKey);
3112
- if (!poller) {
3113
- return;
3114
- }
3115
- if (poller.inFlight) {
3116
- return;
3117
- }
3118
- const now = Date.now();
3119
- const pollIntervalMs = getJobPollIntervalMs();
3120
- if (poller.lastPollAt !== null && now - poller.lastPollAt < pollIntervalMs) {
3121
- scheduleNextJobPoll(pollerKey, runPoll, pollIntervalMs - (now - poller.lastPollAt));
3122
- return;
3123
- }
3124
- if (poller.attempts >= JOB_POLL_MAX_ATTEMPTS) {
3125
- updateThread(input.threadId, (thread) => ({
3126
- ...thread,
3127
- updatedAt: new Date().toISOString(),
3128
- messages: thread.messages.map((message) => (message.id === input.assistantMessageId
3129
- ? {
3130
- ...message,
3131
- statusText: "Background polling stopped after multiple checks. Open the job route when you want to refresh again."
3132
- }
3133
- : message))
3134
- }));
3135
- stopJobPoller(pollerKey);
3136
- return;
3137
- }
3138
- if (document.visibilityState !== "visible" || !navigator.onLine) {
3139
- scheduleNextJobPoll(pollerKey, runPoll, pollIntervalMs);
3140
- return;
3141
- }
3142
- const globalDelayMs = reserveJobPollSlot(Date.now());
3143
- if (globalDelayMs > 0) {
3144
- scheduleNextJobPoll(pollerKey, runPoll, globalDelayMs);
3145
- return;
3146
- }
3147
- poller.inFlight = true;
3148
- poller.lastPollAt = now;
3149
- poller.nextPollAt = null;
3150
- poller.attempts += 1;
3151
- poller.abortController = new AbortController();
3152
- try {
3153
- updateThread(input.threadId, (thread) => ({
3154
- ...thread,
3155
- updatedAt: new Date().toISOString(),
3156
- messages: thread.messages.map((message) => (message.id === input.assistantMessageId
3157
- ? {
3158
- ...message,
3159
- statusText: `Job ${input.jobId} is being tracked. ${getJobPollCadenceText(pollIntervalMs)}`
3160
- }
3161
- : message))
3162
- }));
3163
- const url = input.primitiveId || isPrimitiveRestUrl(input.sourceRequestUrl)
3164
- ? `/api/v1/primitives/jobs/${encodeURIComponent(input.jobId)}`
3165
- : `/api/v1/templates/${encodeURIComponent(boot.template.templateId)}/jobs/${encodeURIComponent(input.jobId)}`;
3166
- const response = await fetch(url, {
3167
- method: "GET",
3168
- headers: buildAuthHeaders(boot.vidfarmApiKey),
3169
- signal: poller.abortController.signal
3170
- });
3171
- const responseBody = await response.json().catch(() => null);
3172
- const responseHeaders = Object.fromEntries(response.headers.entries());
3173
- const payload = tryReadJobStatusPayload(responseBody);
3174
- updateThread(input.threadId, (thread) => ({
3175
- ...thread,
3176
- updatedAt: new Date().toISOString(),
3177
- messages: thread.messages.map((message) => {
3178
- if (message.id !== input.assistantMessageId) {
3179
- return message;
3180
- }
3181
- const nextHttpExchanges = upsertAutomaticPollExchange(message.httpExchanges, sanitizeHttpExchange({
3182
- explanation: `Automatic ${formatJobPollInterval(pollIntervalMs)} job status check for ${input.jobId}`,
3183
- pollJobId: input.jobId,
3184
- pollMode: "automatic",
3185
- request: {
3186
- method: "GET",
3187
- url,
3188
- headers: Object.fromEntries(buildAuthHeaders(boot.vidfarmApiKey).entries())
3189
- },
3190
- result: {
3191
- status: response.status,
3192
- statusText: response.statusText,
3193
- headers: responseHeaders,
3194
- body: responseBody
3195
- }
3196
- }));
3197
- const nextStatusText = payload
3198
- ? payload.status === "succeeded"
3199
- ? `Render ${payload.job_id} succeeded.`
3200
- : payload.status === "failed" || payload.status === "cancelled"
3201
- ? `Render ${payload.job_id} ${payload.status}.${payload.error?.message ? ` ${payload.error.message}` : ""}`
3202
- : `Render ${payload.job_id} is ${payload.status}. Next check in ${formatJobPollInterval(pollIntervalMs)}.`
3203
- : `Render ${input.jobId} checked. Next check in ${formatJobPollInterval(pollIntervalMs)}.`;
3204
- return {
3205
- ...message,
3206
- httpExchanges: nextHttpExchanges,
3207
- statusText: nextStatusText,
3208
- text: payload && (payload.status === "succeeded" || payload.status === "failed" || payload.status === "cancelled")
3209
- ? appendDistinctMessageText(message.text, buildJobCompletionMessage(payload))
3210
- : message.text
3211
- };
3212
- })
3213
- }));
3214
- if (payload && (payload.status === "succeeded" || payload.status === "failed" || payload.status === "cancelled")) {
3215
- stopJobPoller(pollerKey);
3216
- return;
3217
- }
3218
- }
3219
- catch (error) {
3220
- if (error instanceof DOMException && error.name === "AbortError") {
3221
- return;
3222
- }
3223
- updateThread(input.threadId, (thread) => ({
3224
- ...thread,
3225
- updatedAt: new Date().toISOString(),
3226
- messages: thread.messages.map((message) => (message.id === input.assistantMessageId
3227
- ? {
3228
- ...message,
3229
- statusText: `Automatic polling paused after an error. It will retry in ${formatJobPollInterval()}.`
3230
- }
3231
- : message))
3232
- }));
3233
- }
3234
- finally {
3235
- const latestPoller = activeJobPollersRef.current.get(pollerKey);
3236
- if (!latestPoller) {
3237
- return;
3238
- }
3239
- latestPoller.inFlight = false;
3240
- latestPoller.abortController = null;
3241
- if (latestPoller.attempts < JOB_POLL_MAX_ATTEMPTS
3242
- && latestPoller.timeoutId === null
3243
- && activeJobPollersRef.current.has(pollerKey)) {
3244
- scheduleNextJobPoll(pollerKey, runPoll, getJobPollIntervalMs());
3245
- }
3246
- }
3247
- };
3248
- const initialPoller = activeJobPollersRef.current.get(pollerKey);
3249
- if (initialPoller) {
3250
- initialPoller.runPoll = runPoll;
3251
- }
3252
- const poller = activeJobPollersRef.current.get(pollerKey);
3253
- if (poller) {
3254
- scheduleNextJobPoll(pollerKey, runPoll, input.initialDelayMs ?? getJobPollIntervalMs());
3255
- rescheduleActiveJobPollersForLoad();
3256
- }
3257
- }
3258
- function resumePersistedJobPolling(thread) {
3259
- for (const message of thread.messages) {
3260
- if (message.role !== "assistant" || !message.httpExchanges?.length) {
3261
- continue;
3262
- }
3263
- for (const exchange of message.httpExchanges) {
3264
- if (exchange.request.method !== "POST") {
3265
- continue;
3266
- }
3267
- const payload = tryReadJobStatusPayload(exchange.result.body);
3268
- const pollerKey = makeJobPollerKey(thread.id, message.id, payload?.job_id ?? "");
3269
- if (!payload || isTerminalJobStatus(payload.status) || activeJobPollersRef.current.has(pollerKey)) {
3270
- continue;
3271
- }
3272
- const hasTerminalFollowup = message.httpExchanges.some((candidate) => {
3273
- const candidatePayload = tryReadJobStatusPayload(candidate.result.body);
3274
- return candidatePayload?.job_id === payload.job_id && isTerminalJobStatus(candidatePayload.status);
3275
- });
3276
- if (hasTerminalFollowup) {
3277
- continue;
3278
- }
3279
- scheduleJobPolling({
3280
- threadId: thread.id,
3281
- assistantMessageId: message.id,
3282
- jobId: payload.job_id,
3283
- tracer: payload.tracer,
3284
- initialStatus: payload.status,
3285
- primitiveId: payload.primitive_id,
3286
- sourceRequestUrl: exchange.request.url,
3287
- initialDelayMs: JOB_POLL_MIN_TIMER_MS
3288
- });
3289
- }
3290
- }
3291
- }
3292
- async function appendFiles(files) {
3293
- if (!files.length) {
3294
- return;
3295
- }
3296
- setLastError(null);
3297
- const uploadedAttachments = [];
3298
- let lastUploadError = null;
3299
- for (const file of files) {
3300
- try {
3301
- const uploaded = await uploadTemporaryFile({
3302
- apiKey: boot.vidfarmApiKey,
3303
- file,
3304
- folderPath: CHAT_TEMPORARY_UPLOAD_FOLDER
3305
- });
3306
- const attachment = await addInlinePreviewForChatUpload({
3307
- attachment: uploaded,
3308
- file,
3309
- currentInlineBytes: estimateInlineAttachmentBytes([...pendingAttachments, ...uploadedAttachments])
3310
- });
3311
- uploadedAttachments.push(attachment);
3312
- }
3313
- catch (error) {
3314
- lastUploadError = error instanceof Error ? error.message : "Unable to upload attachment.";
3315
- }
3316
- }
3317
- if (lastUploadError && uploadedAttachments.length === 0) {
3318
- setLastError(lastUploadError);
3319
- }
3320
- if (uploadedAttachments.length) {
3321
- setPendingAttachments((current) => [...current, ...uploadedAttachments]);
3322
- if (lastUploadError) {
3323
- setLastError(lastUploadError);
3324
- }
3325
- }
3326
- }
3327
- async function handlePickFiles(event) {
3328
- const files = Array.from(event.target.files ?? []);
3329
- await appendFiles(files);
3330
- event.target.value = "";
3331
- }
3332
- async function handleComposerPaste(event) {
3333
- const files = Array.from(event.clipboardData.files ?? []).filter((file) => file.size > 0);
3334
- if (!files.length) {
3335
- return;
3336
- }
3337
- event.preventDefault();
3338
- await appendFiles(files);
3339
- }
3340
- async function handleComposerDrop(event) {
3341
- event.preventDefault();
3342
- setIsDropTarget(false);
3343
- const files = Array.from(event.dataTransfer.files ?? []).filter((file) => file.size > 0);
3344
- if (!files.length) {
3345
- return;
3346
- }
3347
- await appendFiles(files);
3348
- }
3349
- async function handleSend() {
3350
- if (!canSend) {
3351
- return;
3352
- }
3353
- forceChatBottomOnNextRenderRef.current = true;
3354
- const threadId = ensureActiveThread(draft);
3355
- const now = new Date().toISOString();
3356
- const threadForSend = threadsRef.current.find((thread) => thread.id === threadId);
3357
- const userMessage = {
3358
- id: makeMessageId("user"),
3359
- role: "user",
3360
- text: draft.trim(),
3361
- attachments: pendingAttachments
3362
- };
3363
- const assistantMessage = {
3364
- id: makeMessageId("assistant"),
3365
- role: "assistant",
3366
- text: "",
3367
- attachments: [],
3368
- statusText: "Waiting for AI response…",
3369
- isPending: true
3370
- };
3371
- const existingMessages = threadForSend?.messages ?? [];
3372
- const historyForTransport = compactTransportHistory([...existingMessages, userMessage]);
3373
- updateThread(threadId, (thread) => ({
3374
- ...thread,
3375
- title: thread.messages.length === 0 ? formatThreadTitle(userMessage.text) : thread.title,
3376
- updatedAt: now,
3377
- messages: [...thread.messages, userMessage, assistantMessage]
3378
- }));
3379
- setDraft("");
3380
- setPendingAttachments([]);
3381
- setLastError(null);
3382
- setSendingThreadId(threadId);
3383
- setLoadingStatus("Waiting for AI response…");
3384
- const abortController = new AbortController();
3385
- activeSendRef.current = {
3386
- abortController,
3387
- threadId,
3388
- assistantMessageId: assistantMessage.id
3389
- };
3390
- try {
3391
- const requestBody = JSON.stringify({
3392
- messages: toTransportMessages(historyForTransport),
3393
- thread_id: threadId,
3394
- thread_template_id: threadForSend?.templateId ?? boot.template.templateId,
3395
- thread_title: existingMessages.length === 0 ? formatThreadTitle(userMessage.text) : (threadForSend?.title ?? formatThreadTitle(userMessage.text)),
3396
- thread_tracers: requestTracers,
3397
- client_context: buildBrowserDateContext(),
3398
- user_message: {
3399
- id: userMessage.id,
3400
- role: userMessage.role,
3401
- text: userMessage.text,
3402
- attachments: userMessage.attachments.map((attachment) => ({
3403
- id: attachment.id,
3404
- fileName: attachment.fileName,
3405
- contentType: attachment.contentType,
3406
- viewUrl: attachment.viewUrl
3407
- }))
3408
- },
3409
- assistant_message: {
3410
- id: assistantMessage.id
3411
- },
3412
- template: {
3413
- ...boot.template,
3414
- tracers: requestTracers,
3415
- defaultRequestTracer,
3416
- activeTracer: defaultRequestTracer
3417
- },
3418
- cached_context: {
3419
- ...boot.cachedContext,
3420
- ...readCachedScheduleChannels(),
3421
- ...readSelectedLibraryPosts()
3422
- }
3423
- });
3424
- const response = await fetch(boot.apiUrl || "/api/v1/editor-chat", {
3425
- method: "POST",
3426
- headers: buildAuthHeaders(boot.vidfarmApiKey, "application/json"),
3427
- signal: abortController.signal,
3428
- body: requestBody
3429
- });
3430
- if (!response.ok) {
3431
- const payload = await response.json().catch(() => ({}));
3432
- throw new Error(typeof payload?.error === "string" ? payload.error : `Chat failed with status ${response.status}.`);
3433
- }
3434
- const pendingToolCalls = new Map();
3435
- const shouldSplitJobsIntoThreads = userRequestedSeparateTracerThreads(userMessage.text);
3436
- await readStreamText(response, {
3437
- onDelta: (chunk) => {
3438
- setLoadingStatus("AI is responding…");
3439
- updateThread(threadId, (thread) => ({
3440
- ...thread,
3441
- updatedAt: new Date().toISOString(),
3442
- messages: thread.messages.map((message) => (message.id === assistantMessage.id
3443
- ? {
3444
- ...message,
3445
- text: `${message.text}${chunk}`,
3446
- statusText: "AI is responding…",
3447
- isPending: false
3448
- }
3449
- : message))
3450
- }));
3451
- },
3452
- onStatus: (status) => {
3453
- setLoadingStatus(status);
3454
- updateThread(threadId, (thread) => ({
3455
- ...thread,
3456
- messages: thread.messages.map((message) => (message.id === assistantMessage.id
3457
- ? {
3458
- ...message,
3459
- statusText: status,
3460
- isPending: !message.text
3461
- }
3462
- : message))
3463
- }));
3464
- },
3465
- onToolCall: (toolCall) => {
3466
- pendingToolCalls.set(toolCall.toolCallId, {
3467
- toolName: toolCall.toolName,
3468
- args: toolCall.args
3469
- });
3470
- updateThread(threadId, (thread) => ({
3471
- ...thread,
3472
- updatedAt: new Date().toISOString(),
3473
- messages: thread.messages.map((message) => (message.id === assistantMessage.id
3474
- ? {
3475
- ...message,
3476
- toolExchanges: [
3477
- ...(message.toolExchanges ?? []),
3478
- {
3479
- toolCallId: toolCall.toolCallId,
3480
- toolName: toolCall.toolName,
3481
- args: sanitizeChatPayload(toolCall.args),
3482
- status: "pending"
3483
- }
3484
- ]
3485
- }
3486
- : message))
3487
- }));
3488
- },
3489
- onToolResult: (toolResult) => {
3490
- const sanitizedToolResult = sanitizeToolResult(toolResult.result);
3491
- const pendingCall = pendingToolCalls.get(toolResult.toolCallId);
3492
- if (toolResult.toolName === "frontend_action") {
3493
- const actionResult = toolResult.result;
3494
- if (actionResult?.action === "add_tracer") {
3495
- addAiTracer(actionResult.tracer ?? undefined);
3496
- }
3497
- else if (actionResult?.action === "remove_tracer" && actionResult.tracer) {
3498
- removeTracer(actionResult.tracer);
3499
- }
3500
- else if (actionResult?.action === "activate_tracer" && actionResult.tracer) {
3501
- setDefaultRequestTracerSelection(actionResult.tracer);
3502
- }
3503
- else if (actionResult?.action === "create_thread") {
3504
- const threadTracer = actionResult.tracer ? addAiTracer(actionResult.tracer) : createTracer();
3505
- createNewThread({
3506
- tracers: threadTracer ? [threadTracer] : [],
3507
- title: threadTracer ? `Tracer ${threadTracer}` : "New chat",
3508
- activate: false
3509
- });
3510
- }
3511
- }
3512
- const toolResultRecordForMirror = toolResult.result;
3513
- const jobStatusPayloadForMirror = tryReadJobStatusPayload(toolResultRecordForMirror?.response?.body);
3514
- const mirrorHttpExchange = toolResultRecordForMirror?.request && toolResultRecordForMirror?.response
3515
- ? sanitizeHttpExchange({
3516
- explanation: toolResultRecordForMirror.explanation ?? null,
3517
- request: toolResultRecordForMirror.request,
3518
- result: toolResultRecordForMirror.response
3519
- })
3520
- : undefined;
3521
- const mirroredJobTarget = toolResultRecordForMirror?.request?.method === "POST" && jobStatusPayloadForMirror?.tracer
3522
- ? mirrorJobToTracerThread({
3523
- sourceThreadId: threadId,
3524
- tracer: jobStatusPayloadForMirror.tracer,
3525
- job: jobStatusPayloadForMirror,
3526
- httpExchange: mirrorHttpExchange,
3527
- shouldCreateThread: shouldSplitJobsIntoThreads
3528
- })
3529
- : null;
3530
- if (mirroredJobTarget
3531
- && jobStatusPayloadForMirror
3532
- && !isTerminalJobStatus(jobStatusPayloadForMirror.status)) {
3533
- scheduleJobPolling({
3534
- threadId: mirroredJobTarget.threadId,
3535
- assistantMessageId: mirroredJobTarget.messageId,
3536
- jobId: jobStatusPayloadForMirror.job_id,
3537
- tracer: jobStatusPayloadForMirror.tracer,
3538
- initialStatus: jobStatusPayloadForMirror.status,
3539
- primitiveId: jobStatusPayloadForMirror.primitive_id,
3540
- sourceRequestUrl: toolResultRecordForMirror?.request?.url
3541
- });
3542
- }
3543
- updateThread(threadId, (thread) => ({
3544
- ...thread,
3545
- updatedAt: new Date().toISOString(),
3546
- messages: thread.messages.map((message) => {
3547
- if (message.id !== assistantMessage.id) {
3548
- return message;
3549
- }
3550
- const nextToolExchanges = (message.toolExchanges ?? []).map((exchange) => (exchange.toolCallId === toolResult.toolCallId
3551
- ? {
3552
- ...exchange,
3553
- result: sanitizedToolResult,
3554
- status: (typeof toolResult.result?.error === "string" ? "error" : "complete")
3555
- }
3556
- : exchange));
3557
- const toolResultRecord = toolResult.result;
3558
- const jobStatusPayload = tryReadJobStatusPayload(toolResultRecord?.response?.body);
3559
- const nextHttpExchanges = toolResultRecord?.request && toolResultRecord?.response
3560
- ? [
3561
- ...(message.httpExchanges ?? []),
3562
- sanitizeHttpExchange({
3563
- explanation: toolResultRecord.explanation ?? null,
3564
- request: toolResultRecord.request,
3565
- result: toolResultRecord.response
3566
- })
3567
- ]
3568
- : message.httpExchanges;
3569
- if (pendingCall) {
3570
- pendingToolCalls.delete(toolResult.toolCallId);
3571
- }
3572
- if (toolResultRecord?.request?.method === "POST" && jobStatusPayload?.tracer) {
3573
- const bridge = window.__vidfarmEditorTracers;
3574
- if (bridge) {
3575
- bridge.add(jobStatusPayload.tracer);
3576
- }
3577
- else {
3578
- applyTracerList([jobStatusPayload.tracer, ...tracers.filter((entry) => entry !== jobStatusPayload.tracer)]);
3579
- }
3580
- attachTracersToThread(threadId, [jobStatusPayload.tracer]);
3581
- }
3582
- if (jobStatusPayload
3583
- && !mirroredJobTarget
3584
- && !isTerminalJobStatus(jobStatusPayload.status)) {
3585
- scheduleJobPolling({
3586
- threadId,
3587
- assistantMessageId: assistantMessage.id,
3588
- jobId: jobStatusPayload.job_id,
3589
- tracer: jobStatusPayload.tracer,
3590
- initialStatus: jobStatusPayload.status,
3591
- primitiveId: jobStatusPayload.primitive_id,
3592
- sourceRequestUrl: toolResultRecord?.request?.url
3593
- });
3594
- }
3595
- return {
3596
- ...message,
3597
- toolExchanges: nextToolExchanges,
3598
- httpExchanges: nextHttpExchanges,
3599
- statusText: jobStatusPayload
3600
- && !isTerminalJobStatus(jobStatusPayload.status)
3601
- ? `Job ${jobStatusPayload.job_id} is being tracked. ${getJobPollCadenceText()}`
3602
- : message.statusText,
3603
- isPending: false,
3604
- text: jobStatusPayload && toolResultRecord?.request?.method === "POST" && toolResultRecord?.response?.status === 202
3605
- ? appendDistinctMessageText(message.text, buildJobQueuedMessage(jobStatusPayload))
3606
- : jobStatusPayload && isTerminalJobStatus(jobStatusPayload.status)
3607
- ? appendDistinctMessageText(message.text, buildJobCompletionMessage(jobStatusPayload))
3608
- : jobStatusPayload
3609
- ? appendDistinctMessageText(message.text, buildJobCompletionMessage(jobStatusPayload))
3610
- : message.text
3611
- };
3612
- })
3613
- }));
3614
- },
3615
- onError: (errorMessage) => {
3616
- setLastError(errorMessage);
3617
- setLoadingStatus(null);
3618
- updateThread(threadId, (thread) => ({
3619
- ...thread,
3620
- messages: thread.messages.filter((message) => message.id !== assistantMessage.id)
3621
- }));
3622
- }
3623
- });
3624
- updateThread(threadId, (thread) => ({
3625
- ...thread,
3626
- updatedAt: new Date().toISOString(),
3627
- messages: thread.messages.map((message) => (message.id === assistantMessage.id
3628
- ? {
3629
- ...message,
3630
- statusText: message.statusText?.includes("being tracked") ? message.statusText : null,
3631
- isPending: false
3632
- }
3633
- : message))
3634
- }));
3635
- }
3636
- catch (error) {
3637
- if (error instanceof DOMException && error.name === "AbortError") {
3638
- setLastError(null);
3639
- setLoadingStatus(null);
3640
- updateThread(threadId, (thread) => ({
3641
- ...thread,
3642
- updatedAt: new Date().toISOString(),
3643
- messages: thread.messages.map((message) => {
3644
- if (message.id !== assistantMessage.id) {
3645
- return message;
3646
- }
3647
- const hasContent = Boolean(message.text.trim()
3648
- || message.toolExchanges?.length
3649
- || message.httpExchanges?.length);
3650
- return hasContent
3651
- ? {
3652
- ...message,
3653
- statusText: "Stopped.",
3654
- isPending: false
3655
- }
3656
- : {
3657
- ...message,
3658
- text: "Stopped.",
3659
- statusText: null,
3660
- isPending: false
3661
- };
3662
- })
3663
- }));
3664
- return;
3665
- }
3666
- setLastError(error instanceof Error ? error.message : "Unable to send chat message.");
3667
- setLoadingStatus(null);
3668
- updateThread(threadId, (thread) => ({
3669
- ...thread,
3670
- messages: thread.messages.map((message) => (message.id === assistantMessage.id
3671
- ? {
3672
- ...message,
3673
- text: error instanceof Error ? `Error: ${error.message}` : "Error: Unable to send chat message.",
3674
- statusText: null,
3675
- isPending: false
3676
- }
3677
- : message))
3678
- }));
3679
- }
3680
- finally {
3681
- if (activeSendRef.current?.assistantMessageId === assistantMessage.id) {
3682
- activeSendRef.current = null;
3683
- }
3684
- setSendingThreadId((current) => (current === threadId ? null : current));
3685
- setLoadingStatus(null);
3686
- }
3687
- }
3688
- function stopActiveSend() {
3689
- setLoadingStatus("Stopping…");
3690
- const activeSend = activeSendRef.current;
3691
- if (activeSend) {
3692
- updateThread(activeSend.threadId, (thread) => ({
3693
- ...thread,
3694
- updatedAt: new Date().toISOString(),
3695
- messages: thread.messages.map((message) => (message.id === activeSend.assistantMessageId
3696
- ? {
3697
- ...message,
3698
- statusText: "Stopping…"
3699
- }
3700
- : message))
3701
- }));
3702
- }
3703
- activeSendRef.current?.abortController.abort();
3704
- }
3705
- useEffect(() => {
3706
- return () => {
3707
- activeSendRef.current?.abortController.abort();
3708
- for (const pollerKey of activeJobPollersRef.current.keys()) {
3709
- stopJobPoller(pollerKey);
3710
- }
3711
- };
3712
- }, []);
3713
- function handleComposerKeyDown(event) {
3714
- if (event.key === "Enter" && !event.shiftKey) {
3715
- event.preventDefault();
3716
- void handleSend();
3717
- }
3718
- }
3719
- const threadListContent = (_jsx("div", { className: `vf-editor-chat-thread-list${isBrainstormSurface ? " vf-editor-chat-thread-list-sidebar" : ""}`, role: "tablist", "aria-label": "Conversation threads", children: visibleThreads.length ? visibleThreads.map((thread) => (_jsxs("div", { role: "tab", "aria-selected": thread.id === activeThreadId, className: "vf-editor-chat-thread-chip", "data-active": thread.id === activeThreadId ? "true" : "false", children: [_jsxs("button", { type: "button", className: "vf-editor-chat-thread-chip-main", onClick: () => {
3720
- setActiveThreadId(thread.id);
3721
- setLastError(null);
3722
- setOpenThreadMenu(null);
3723
- if (isBrainstormSurface) {
3724
- setIsMobileChatOpen(false);
3725
- }
3726
- }, children: [_jsx("span", { className: "vf-editor-chat-thread-chip-title", children: thread.title }), thread.tracers.length ? (_jsxs("span", { className: "vf-editor-chat-thread-chip-tracers", children: [thread.tracers.slice(0, 2).join(", "), thread.tracers.length > 2 ? ` +${thread.tracers.length - 2}` : ""] })) : null] }), _jsx("div", { className: "vf-editor-chat-thread-menu-wrap", children: _jsx("button", { type: "button", className: "vf-editor-chat-thread-chip-menu", "aria-label": `Actions for ${thread.title}`, onClick: (event) => {
3727
- event.stopPropagation();
3728
- toggleThreadMenu(thread.id, event.currentTarget);
3729
- }, children: "..." }) })] }, thread.id))) : (_jsx("div", { className: "vf-editor-chat-thread-empty", children: "Start a chat to create your first conversation thread." })) }));
3730
- const openHistoryDrawer = () => setIsMobileChatOpen(true);
3731
- const closeHistoryDrawer = () => setIsMobileChatOpen(false);
3732
- const goBack = () => {
3733
- if (window.history.length > 1) {
3734
- window.history.back();
3735
- return;
3736
- }
3737
- window.location.href = "/discover";
3738
- };
3739
- return (_jsxs("div", { className: "vf-editor-chat-shell", "data-mobile-open": isMobileChatOpen ? "true" : "false", "data-layout": isBrainstormSurface ? "brainstorm" : "default", children: [_jsx("button", { type: "button", className: "vf-editor-chat-mobile-fab", "aria-label": "Open chat", "aria-expanded": isMobileChatOpen, onClick: () => setIsMobileChatOpen(true), children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M20 11.5a7.5 7.5 0 0 1-9.8 7.1L4 20l1.5-5.3A7.5 7.5 0 1 1 20 11.5Z" }) }) }), lastError ? (_jsx("div", { className: "vf-editor-chat-error", role: "status", children: lastError })) : null, _jsxs("div", { className: isBrainstormSurface ? "vf-editor-chat-body vf-editor-chat-brainstorm-layout" : "vf-editor-chat-body", children: [isBrainstormSurface ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "vf-editor-chat-history-scrim", "aria-label": "Close chat history", onClick: closeHistoryDrawer }), _jsxs("aside", { className: "vf-editor-chat-history-pane", children: [_jsxs("div", { className: "vf-editor-chat-history-header", children: [_jsxs("div", { className: "vf-editor-chat-history-header-main", children: [_jsx("button", { type: "button", className: "vf-editor-chat-back-button", onClick: goBack, children: "Back" }), _jsx("div", { className: "vf-editor-chat-history-kicker", children: "Threads" })] }), _jsx("button", { type: "button", className: "vf-editor-chat-history-close", "aria-label": "Close chat history", onClick: closeHistoryDrawer, children: "\u00D7" })] }), _jsx("button", { type: "button", className: "vf-editor-chat-new-thread vf-editor-chat-new-thread-sidebar", onClick: createNewThreadFromClick, children: "New chat" }), threadListContent] })] })) : null, _jsxs("div", { className: "vf-editor-chat-thread", children: [_jsxs("div", { className: "vf-editor-chat-header", children: [isBrainstormSurface ? (_jsxs("div", { className: "vf-editor-chat-brainstorm-topbar", children: [_jsxs("div", { className: "vf-editor-chat-brainstorm-topbar-left", children: [_jsx("button", { type: "button", className: "vf-editor-chat-history-toggle", onClick: openHistoryDrawer, children: "History" }), _jsx("div", { className: "vf-editor-chat-brainstorm-title", children: boot.template.templateTitle })] }), _jsx("button", { type: "button", className: "vf-editor-chat-new-thread", onClick: createNewThreadFromClick, children: "New chat" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "vf-editor-chat-header-row", children: [_jsxs("div", { children: [_jsx("div", { className: "vf-editor-chat-header-title", children: "Copilot" }), _jsx("div", { className: "vf-editor-chat-header-subtitle", children: boot.template.templateTitle })] }), _jsx("button", { type: "button", className: "vf-editor-chat-history-icon", "aria-label": showHistoryPanel ? "Close chat history" : "Open chat history", "aria-pressed": showHistoryPanel, onClick: () => setShowHistoryPanel((prev) => !prev), children: _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "10", cy: "10", r: "7.2" }), _jsx("path", { d: "M10 6v4l2.6 1.6" })] }) }), _jsx("button", { type: "button", className: "vf-editor-chat-new-thread", onClick: createNewThreadFromClick, children: "New chat" }), _jsx("button", { type: "button", className: "vf-editor-chat-minimize", "aria-label": "Minimize chat", onClick: () => setIsMobileChatOpen(false), children: "\u00D7" })] }), showHistoryPanel ? null : threadListContent] })), openThreadMenu && openThreadMenuThread ? createPortal(_jsxs("div", { className: "vf-editor-chat-thread-menu", role: "menu", style: {
3740
- top: openThreadMenu.top,
3741
- left: openThreadMenu.left
3742
- }, onClick: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", onClick: () => copyThreadId(openThreadMenuThread.id), children: "Copy Chat ID" }), _jsx("button", { type: "button", onClick: () => editThreadTracers(openThreadMenuThread.id), children: "Edit tracers" }), _jsx("button", { type: "button", onClick: () => removeThreadFromView(openThreadMenuThread.id), children: "Remove from view" }), _jsx("button", { type: "button", onClick: () => archiveThread(openThreadMenuThread.id), children: "Archive thread" }), _jsx("button", { type: "button", "data-danger": "true", onClick: () => deleteThread(openThreadMenuThread.id), children: "Delete" })] }), document.body) : null] }), _jsx("div", { className: "vf-editor-chat-viewport", ref: viewportRef, children: showHistoryPanel && !isBrainstormSurface ? (_jsxs("div", { className: "vf-editor-chat-history-panel", children: [_jsxs("div", { className: "vf-editor-chat-history-panel-header", children: [_jsx("div", { className: "vf-editor-chat-kicker", children: "Chat history" }), _jsx("button", { type: "button", className: "vf-editor-chat-new-thread", onClick: createNewThreadFromClick, children: "New chat" })] }), threadListContent] })) : messages.length ? messages.map((message) => (_jsx(ChatBubble, { message: message }, message.id))) : activeThreadId && loadingThreadId === activeThreadId ? (_jsxs("div", { className: "vf-editor-chat-empty", children: [_jsx("div", { className: "vf-editor-chat-kicker", children: "Loading chat" }), _jsxs("h2", { children: ["Opening ", activeThreadId] })] })) : (_jsxs("div", { className: "vf-editor-chat-empty", children: [_jsx("div", { className: "vf-editor-chat-kicker", children: "AI Copilot" }), _jsx("h2", { children: isBrainstormSurface ? "Start a new chat." : "Build a HyperFrames timeline." }), _jsx("p", { children: isBrainstormSurface
3743
- ? "Use the brainstorm tools to go from zero context to angles, hooks, and a strategy you can actually test."
3744
- : "Use the Copilot here to reason about layers, draft overlays, prepare assets, and run HyperFrames render jobs. You can also connect from your own AI coding agent, including Claude Code or OpenAI Codex, and ask it to work against this composition." }), _jsx("div", { className: "vf-editor-chat-empty-prompts", "aria-label": "Example prompts", children: isBrainstormSurface ? (_jsxs(_Fragment, { children: [_jsx("div", { children: "I don't know where to start" }), _jsx("div", { children: "Help me think of persuasive angles" }), _jsx("div", { children: "Help me think of viral hooks" })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { children: "teach me how this composition works" }), _jsx("div", { children: "where should the overlay layers go?" }), _jsx("div", { children: "create a timeline render using my video..." })] })) })] })) }), _jsx("div", { className: "vf-editor-chat-footer", children: _jsxs("div", { className: "vf-editor-chat-composer", "data-drop-target": isDropTarget ? "true" : "false", onDragEnter: (event) => {
3745
- event.preventDefault();
3746
- setIsDropTarget(true);
3747
- }, onDragOver: (event) => {
3748
- event.preventDefault();
3749
- if (!isDropTarget) {
3750
- setIsDropTarget(true);
3751
- }
3752
- }, onDragLeave: (event) => {
3753
- event.preventDefault();
3754
- const nextTarget = event.relatedTarget;
3755
- if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) {
3756
- return;
3757
- }
3758
- setIsDropTarget(false);
3759
- }, onDrop: (event) => void handleComposerDrop(event), children: [isDropTarget ? (_jsx("div", { className: "vf-editor-chat-dropzone-hint", children: "Drop files to attach" })) : null, pendingAttachments.length ? (_jsx("div", { className: "vf-editor-chat-composer-attachments-strip", children: pendingAttachments.map((attachment) => (_jsx(ComposerAttachmentItem, { attachment: attachment, onRemove: () => setPendingAttachments((current) => current.filter((entry) => entry.id !== attachment.id)) }, attachment.id))) })) : null, _jsxs("div", { className: "vf-editor-chat-composer-row", children: [_jsx("button", { type: "button", className: "vf-editor-chat-attach-button", "aria-label": "Open upload drawer", onClick: () => setIsUploadDrawerOpen(true), children: "\uD83D\uDCCE" }), _jsx("textarea", { rows: 1, placeholder: isBrainstormSurface ? "Describe the offer, audience, or strategy problem you want help with..." : "Ask it to call a route, inspect jobs, save config, or continue this thread...", className: "vf-editor-chat-input", value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: handleComposerKeyDown, onPaste: (event) => void handleComposerPaste(event) }), _jsx("button", { type: "button", className: "vf-editor-chat-send-button", onClick: () => {
3760
- if (isActiveThreadSending) {
3761
- stopActiveSend();
3762
- return;
3763
- }
3764
- void handleSend();
3765
- }, disabled: isActiveThreadSending ? false : !canSend, children: isActiveThreadSending ? "Stop" : "Send" })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*,video/*,audio/*,application/pdf,text/plain,text/markdown", multiple: true, hidden: true, onChange: (event) => void handlePickFiles(event) })] }) })] })] }), _jsx(UploadDrawer, { apiKey: boot.vidfarmApiKey, open: isUploadDrawerOpen, onClose: () => setIsUploadDrawerOpen(false), onConfirm: (attachments) => setPendingAttachments((current) => [...current, ...attachments]) })] }));
3766
- }
3767
- //# sourceMappingURL=template-editor-chat.js.map