@mevdragon/vidfarm-devcli 0.2.11 → 0.2.13
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.
- package/.env.example +37 -1
- package/README.md +27 -183
- package/SKILL.director.md +293 -0
- package/SKILL.platform.md +312 -0
- package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +275 -0
- package/auto-create-hyperframe-templates/extractor-system-prompt.md +128 -0
- package/auto-create-hyperframe-templates/input-manifest.schema.json +56 -0
- package/auto-create-hyperframe-templates/lambda-memory-estimator.md +118 -0
- package/auto-create-hyperframe-templates/production-graph.schema.json +314 -0
- package/auto-create-hyperframe-templates/runbook.md +198 -0
- package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +368 -0
- package/auto-create-hyperframe-templates/template-plan.schema.json +311 -0
- package/auto-create-hyperframe-templates/template-planner-prompt.md +144 -0
- package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +124 -0
- package/auto-create-templates/extractor-system-prompt.md +8 -2
- package/auto-create-templates/template-plan.schema.json +73 -2
- package/auto-create-templates/template-planner-prompt.md +63 -1
- package/dist/src/account-pages-legacy.js +361 -89
- package/dist/src/account-pages-legacy.js.map +1 -0
- package/dist/src/account-pages.js +13 -3
- package/dist/src/account-pages.js.map +1 -0
- package/dist/src/app.js +7060 -946
- package/dist/src/app.js.map +1 -0
- package/dist/src/cli.js +111 -29
- package/dist/src/cli.js.map +1 -0
- package/dist/src/composition-runtime.js +581 -0
- package/dist/src/composition-runtime.js.map +1 -0
- package/dist/src/config.js +69 -11
- package/dist/src/config.js.map +1 -0
- package/dist/src/context.js +280 -21
- package/dist/src/context.js.map +1 -0
- package/dist/src/dev-app-legacy.js +18 -17
- package/dist/src/dev-app-legacy.js.map +1 -0
- package/dist/src/dev-app.js +1 -0
- package/dist/src/dev-app.js.map +1 -0
- package/dist/src/domain.js +2 -1
- package/dist/src/domain.js.map +1 -0
- package/dist/src/editor-chat-history.js +10 -0
- package/dist/src/editor-chat-history.js.map +1 -0
- package/dist/src/editor-chat.js +133 -24
- package/dist/src/editor-chat.js.map +1 -0
- package/dist/src/frontend/flockposter-cache-store.js +10 -2
- package/dist/src/frontend/flockposter-cache-store.js.map +1 -0
- package/dist/src/frontend/homepage-client.js +6 -3
- package/dist/src/frontend/homepage-client.js.map +1 -0
- package/dist/src/frontend/homepage-shared.js +1 -0
- package/dist/src/frontend/homepage-shared.js.map +1 -0
- package/dist/src/frontend/homepage-store.js +1 -0
- package/dist/src/frontend/homepage-store.js.map +1 -0
- package/dist/src/frontend/homepage-view.js +131 -18
- package/dist/src/frontend/homepage-view.js.map +1 -0
- package/dist/src/frontend/page-runtime-client.js +5 -2
- package/dist/src/frontend/page-runtime-client.js.map +1 -0
- package/dist/src/frontend/page-runtime-store.js +1 -0
- package/dist/src/frontend/page-runtime-store.js.map +1 -0
- package/dist/src/frontend/sentry.js +42 -0
- package/dist/src/frontend/sentry.js.map +1 -0
- package/dist/src/frontend/template-editor-chat.js +1719 -213
- package/dist/src/frontend/template-editor-chat.js.map +1 -0
- package/dist/src/help-page.js +333 -0
- package/dist/src/help-page.js.map +1 -0
- package/dist/src/homepage.js +169 -47
- package/dist/src/homepage.js.map +1 -0
- package/dist/src/hyperframes/composition.js +180 -0
- package/dist/src/hyperframes/composition.js.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/instrument.js +30 -0
- package/dist/src/instrument.js.map +1 -0
- package/dist/src/lib/crypto.js +1 -0
- package/dist/src/lib/crypto.js.map +1 -0
- package/dist/src/lib/dev-log.js +54 -0
- package/dist/src/lib/dev-log.js.map +1 -0
- package/dist/src/lib/display-name.js +11 -0
- package/dist/src/lib/display-name.js.map +1 -0
- package/dist/src/lib/ids.js +21 -1
- package/dist/src/lib/ids.js.map +1 -0
- package/dist/src/lib/images.js +1 -0
- package/dist/src/lib/images.js.map +1 -0
- package/dist/src/lib/json.js +1 -0
- package/dist/src/lib/json.js.map +1 -0
- package/dist/src/lib/template-dna.js +1 -0
- package/dist/src/lib/template-dna.js.map +1 -0
- package/dist/src/lib/template-paths.js +1 -0
- package/dist/src/lib/template-paths.js.map +1 -0
- package/dist/src/lib/template-style-options.js +40 -2
- package/dist/src/lib/template-style-options.js.map +1 -0
- package/dist/src/lib/time.js +1 -0
- package/dist/src/lib/time.js.map +1 -0
- package/dist/src/lib/video-quality-harness.js +60 -0
- package/dist/src/lib/video-quality-harness.js.map +1 -0
- package/dist/src/page-runtime.js +1 -0
- package/dist/src/page-runtime.js.map +1 -0
- package/dist/src/page-shell.js +1501 -49
- package/dist/src/page-shell.js.map +1 -0
- package/dist/src/primitive-context.js +144 -8
- package/dist/src/primitive-context.js.map +1 -0
- package/dist/src/primitive-registry.js +1919 -78
- package/dist/src/primitive-registry.js.map +1 -0
- package/dist/src/primitive-sdk.js +1 -0
- package/dist/src/primitive-sdk.js.map +1 -0
- package/dist/src/primitives/remotion/html-image.js +3 -1
- package/dist/src/primitives/remotion/html-image.js.map +1 -0
- package/dist/src/primitives/remotion/media-slideshow.js +60 -0
- package/dist/src/primitives/remotion/media-slideshow.js.map +1 -0
- package/dist/src/react-page-shell.js +1 -0
- package/dist/src/react-page-shell.js.map +1 -0
- package/dist/src/ready-post-schedule-component.js +1 -0
- package/dist/src/ready-post-schedule-component.js.map +1 -0
- package/dist/src/registry.js +237 -24
- package/dist/src/registry.js.map +1 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/services/api-call-history.js +4 -0
- package/dist/src/services/api-call-history.js.map +1 -0
- package/dist/src/services/auth.js +40 -50
- package/dist/src/services/auth.js.map +1 -0
- package/dist/src/services/billing.js +20 -44
- package/dist/src/services/billing.js.map +1 -0
- package/dist/src/services/chat-threads.js +10 -6
- package/dist/src/services/chat-threads.js.map +1 -0
- package/dist/src/services/fork-access.js +94 -0
- package/dist/src/services/fork-access.js.map +1 -0
- package/dist/src/services/ghostcut.js +132 -0
- package/dist/src/services/ghostcut.js.map +1 -0
- package/dist/src/services/hyperframes.js +1014 -0
- package/dist/src/services/hyperframes.js.map +1 -0
- package/dist/src/services/job-capacity.js +14 -0
- package/dist/src/services/job-capacity.js.map +1 -0
- package/dist/src/services/job-logs.js +4 -0
- package/dist/src/services/job-logs.js.map +1 -0
- package/dist/src/services/jobs.js +99 -91
- package/dist/src/services/jobs.js.map +1 -0
- package/dist/src/services/media-processing.js +743 -0
- package/dist/src/services/media-processing.js.map +1 -0
- package/dist/src/services/primitive-media-lambda.js +280 -0
- package/dist/src/services/primitive-media-lambda.js.map +1 -0
- package/dist/src/services/providers.js +1560 -178
- package/dist/src/services/providers.js.map +1 -0
- package/dist/src/services/rate-limits.js +3 -2
- package/dist/src/services/rate-limits.js.map +1 -0
- package/dist/src/services/remotion.js +495 -92
- package/dist/src/services/remotion.js.map +1 -0
- package/dist/src/services/serverless-auth.js +374 -0
- package/dist/src/services/serverless-auth.js.map +1 -0
- package/dist/src/services/serverless-jobs.js +1074 -0
- package/dist/src/services/serverless-jobs.js.map +1 -0
- package/dist/src/services/serverless-provider-keys.js +401 -0
- package/dist/src/services/serverless-provider-keys.js.map +1 -0
- package/dist/src/services/serverless-records.js +1086 -0
- package/dist/src/services/serverless-records.js.map +1 -0
- package/dist/src/services/serverless-template-configs.js +67 -0
- package/dist/src/services/serverless-template-configs.js.map +1 -0
- package/dist/src/services/storage.js +170 -35
- package/dist/src/services/storage.js.map +1 -0
- package/dist/src/services/template-certification.js +295 -3
- package/dist/src/services/template-certification.js.map +1 -0
- package/dist/src/services/template-loader.js +45 -1
- package/dist/src/services/template-loader.js.map +1 -0
- package/dist/src/services/template-runtime-bundles.js +217 -0
- package/dist/src/services/template-runtime-bundles.js.map +1 -0
- package/dist/src/services/template-sources.js +452 -87
- package/dist/src/services/template-sources.js.map +1 -0
- package/dist/src/services/video-normalization.js +2 -0
- package/dist/src/services/video-normalization.js.map +1 -0
- package/dist/src/services/webhooks.js +7 -6
- package/dist/src/services/webhooks.js.map +1 -0
- package/dist/src/template-editor-pages.js +5637 -1322
- package/dist/src/template-editor-pages.js.map +1 -0
- package/dist/src/template-editor-shell.js +1376 -170
- package/dist/src/template-editor-shell.js.map +1 -0
- package/dist/src/template-sdk.js +1 -0
- package/dist/src/template-sdk.js.map +1 -0
- package/dist/src/worker.js +11 -226
- package/dist/src/worker.js.map +1 -0
- package/package.json +47 -12
- package/public/assets/homepage-client-app.js +34 -8
- package/public/assets/page-runtime-client-app.js +44 -18
- package/readme.secret.md +89 -0
- package/templates/vidfarm_template_0000/README.md +106 -0
- package/templates/vidfarm_template_0000/SKILL.md +266 -0
- package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/composition.json +11 -0
- package/templates/vidfarm_template_0000/src/lib/images.js +221 -0
- package/templates/vidfarm_template_0000/src/remotion/Root.js +33 -0
- package/templates/vidfarm_template_0000/src/remotion/index.js +3 -0
- package/templates/vidfarm_template_0000/src/sdk.js +3 -0
- package/templates/vidfarm_template_0000/src/style-options.js +200 -0
- package/templates/vidfarm_template_0000/src/template-dna.js +9 -0
- package/templates/vidfarm_template_0000/src/template.js +1566 -0
- package/templates/vidfarm_template_0000/template.config.json +21 -0
- package/GETTING_STARTED.developers.md +0 -87
- package/SKILL.developer.md +0 -1177
- package/dist/src/db.js +0 -2007
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
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";
|
|
4
11
|
function normalizeUploadedAttachment(raw) {
|
|
5
|
-
const id = raw.id || raw.attachment_id || "";
|
|
12
|
+
const id = raw.id || raw.attachment_id || raw.file_id || "";
|
|
6
13
|
const fileName = raw.fileName || raw.file_name;
|
|
7
14
|
const contentType = raw.contentType || raw.content_type;
|
|
8
15
|
const viewUrl = raw.viewUrl || raw.view_url || "";
|
|
9
16
|
if (!id || !fileName || !contentType || !viewUrl) {
|
|
10
17
|
return null;
|
|
11
18
|
}
|
|
12
|
-
return {
|
|
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
|
+
};
|
|
13
27
|
}
|
|
14
28
|
function isImageAttachment(attachment) {
|
|
15
29
|
return attachment.contentType.startsWith("image/");
|
|
@@ -40,6 +54,12 @@ const MAX_CHAT_PAYLOAD_STRING_LENGTH = 1200;
|
|
|
40
54
|
const MAX_CHAT_PAYLOAD_ARRAY_ITEMS = 24;
|
|
41
55
|
const MAX_CHAT_PAYLOAD_OBJECT_KEYS = 80;
|
|
42
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;
|
|
43
63
|
function formatJsonBody(value) {
|
|
44
64
|
if (value === undefined || value === null || value === "") {
|
|
45
65
|
return "";
|
|
@@ -116,10 +136,18 @@ function sanitizeHttpExchange(exchange) {
|
|
|
116
136
|
...exchange,
|
|
117
137
|
request: {
|
|
118
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
|
+
])),
|
|
119
143
|
body: sanitizeChatPayload(exchange.request.body)
|
|
120
144
|
},
|
|
121
145
|
result: {
|
|
122
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
|
+
])),
|
|
123
151
|
body: sanitizeChatPayload(exchange.result.body),
|
|
124
152
|
error: exchange.result.error ? summarizeLargeString(exchange.result.error) : exchange.result.error
|
|
125
153
|
}
|
|
@@ -225,17 +253,25 @@ function extractStorageKeyFromUrl(url) {
|
|
|
225
253
|
return null;
|
|
226
254
|
}
|
|
227
255
|
}
|
|
256
|
+
function safeDecodeURIComponent(value) {
|
|
257
|
+
try {
|
|
258
|
+
return decodeURIComponent(value);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
228
264
|
function extractFilenameFromUrl(url) {
|
|
229
265
|
const storageKey = extractStorageKeyFromUrl(url);
|
|
230
266
|
if (storageKey) {
|
|
231
|
-
const fileName =
|
|
267
|
+
const fileName = safeDecodeURIComponent(storageKey.split("/").filter(Boolean).pop() || "");
|
|
232
268
|
if (fileName) {
|
|
233
269
|
return fileName;
|
|
234
270
|
}
|
|
235
271
|
}
|
|
236
272
|
try {
|
|
237
273
|
const pathname = new URL(url).pathname;
|
|
238
|
-
return
|
|
274
|
+
return safeDecodeURIComponent(pathname.split("/").filter(Boolean).pop() || "");
|
|
239
275
|
}
|
|
240
276
|
catch {
|
|
241
277
|
return "";
|
|
@@ -245,6 +281,14 @@ function labelFromUrl(url) {
|
|
|
245
281
|
const filename = extractFilenameFromUrl(url);
|
|
246
282
|
return filename || "Output";
|
|
247
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
|
+
}
|
|
248
292
|
function getMediaKindFromUrl(url) {
|
|
249
293
|
const fileName = extractFilenameFromUrl(url);
|
|
250
294
|
const extension = fileName.split(".").pop()?.trim().toLowerCase() || "";
|
|
@@ -328,15 +372,136 @@ function collectLabeledUrls(value, output, seen = new Set(), path = []) {
|
|
|
328
372
|
}
|
|
329
373
|
function buildJobQueuedMessage(job) {
|
|
330
374
|
return [
|
|
331
|
-
`Started
|
|
375
|
+
`Started render \`${job.job_id}\`${job.tracer ? ` with tracer \`${job.tracer}\`` : ""}.`,
|
|
332
376
|
"If it takes a while, ask for an update later and I can check it for you."
|
|
333
377
|
].join("\n\n");
|
|
334
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
|
+
}
|
|
335
495
|
function buildJobCompletionMessage(job) {
|
|
336
496
|
if (job.status === "succeeded") {
|
|
337
497
|
const urls = [];
|
|
338
498
|
collectLabeledUrls(job.result, urls);
|
|
339
|
-
const lines = [`
|
|
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
|
+
}
|
|
340
505
|
if (urls.length) {
|
|
341
506
|
lines.push("");
|
|
342
507
|
lines.push("Outputs:");
|
|
@@ -345,12 +510,12 @@ function buildJobCompletionMessage(job) {
|
|
|
345
510
|
return lines.join("\n");
|
|
346
511
|
}
|
|
347
512
|
if (job.status === "failed") {
|
|
348
|
-
return `
|
|
513
|
+
return `Render \`${job.job_id}\` failed.${job.error?.message ? ` ${job.error.message}` : ""}`;
|
|
349
514
|
}
|
|
350
515
|
if (job.status === "cancelled") {
|
|
351
|
-
return `
|
|
516
|
+
return `Render \`${job.job_id}\` was cancelled.`;
|
|
352
517
|
}
|
|
353
|
-
return `
|
|
518
|
+
return `Render \`${job.job_id}\` is ${job.status}.`;
|
|
354
519
|
}
|
|
355
520
|
function appendDistinctMessageText(current, addition) {
|
|
356
521
|
const normalizedCurrent = current.trim();
|
|
@@ -395,8 +560,12 @@ function tryReadJobStatusPayload(value) {
|
|
|
395
560
|
job_id: record.job_id,
|
|
396
561
|
tracer: typeof record.tracer === "string" ? record.tracer : undefined,
|
|
397
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,
|
|
398
566
|
progress: typeof record.progress === "number" ? record.progress : null,
|
|
399
567
|
result: record.result,
|
|
568
|
+
artifacts: normalizeJobArtifacts(record.artifacts),
|
|
400
569
|
error: record.error && typeof record.error === "object"
|
|
401
570
|
? {
|
|
402
571
|
message: typeof record.error.message === "string"
|
|
@@ -409,6 +578,24 @@ function tryReadJobStatusPayload(value) {
|
|
|
409
578
|
: null
|
|
410
579
|
};
|
|
411
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
|
+
}
|
|
412
599
|
function buildAuthHeaders(apiKey, contentType) {
|
|
413
600
|
const headers = new Headers();
|
|
414
601
|
if (apiKey) {
|
|
@@ -436,35 +623,215 @@ async function fileToDataUrl(file) {
|
|
|
436
623
|
reader.readAsDataURL(file);
|
|
437
624
|
});
|
|
438
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
|
+
}
|
|
439
752
|
async function uploadAttachment(input) {
|
|
440
|
-
const
|
|
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", {
|
|
441
799
|
method: "POST",
|
|
442
800
|
headers: buildAuthHeaders(input.apiKey, "application/json"),
|
|
443
801
|
body: JSON.stringify({
|
|
444
802
|
file_name: input.file.name,
|
|
445
803
|
content_type: input.file.type || undefined,
|
|
446
|
-
size_bytes: input.file.size
|
|
804
|
+
size_bytes: input.file.size,
|
|
805
|
+
folder_path: input.folderPath || undefined
|
|
447
806
|
})
|
|
448
807
|
});
|
|
449
808
|
const presignPayload = await parseJson(presignResponse);
|
|
450
809
|
if (!presignResponse.ok || !("transport" in presignPayload)) {
|
|
451
|
-
throw new Error(("error" in presignPayload && presignPayload.error) || "Unable to prepare upload.");
|
|
810
|
+
throw new Error(("error" in presignPayload && presignPayload.error) || "Unable to prepare temporary upload.");
|
|
452
811
|
}
|
|
453
812
|
if (presignPayload.transport === "server") {
|
|
454
813
|
const uploadFormData = new FormData();
|
|
455
814
|
uploadFormData.append(presignPayload.upload.form_field || "file", input.file);
|
|
815
|
+
uploadFormData.append("folder_path", input.folderPath || "");
|
|
456
816
|
const uploadResponse = await fetch(presignPayload.upload.url, {
|
|
457
817
|
method: presignPayload.upload.method || "POST",
|
|
458
818
|
headers: buildAuthHeaders(input.apiKey),
|
|
459
819
|
body: uploadFormData
|
|
460
820
|
});
|
|
461
821
|
const uploadPayload = await parseJson(uploadResponse);
|
|
462
|
-
if (!uploadResponse.ok || !uploadPayload.
|
|
463
|
-
throw new Error(uploadPayload.error || "Unable to upload
|
|
822
|
+
if (!uploadResponse.ok || !uploadPayload.file) {
|
|
823
|
+
throw new Error(uploadPayload.error || "Unable to upload temporary file.");
|
|
464
824
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
+
};
|
|
468
835
|
}
|
|
469
836
|
const uploadResponse = await fetch(presignPayload.upload.url, {
|
|
470
837
|
method: presignPayload.upload.method,
|
|
@@ -474,45 +841,166 @@ async function uploadAttachment(input) {
|
|
|
474
841
|
if (!uploadResponse.ok) {
|
|
475
842
|
throw new Error(`Upload failed with status ${uploadResponse.status}.`);
|
|
476
843
|
}
|
|
477
|
-
const finalizeResponse = await fetch("/api/v1/user/me/
|
|
844
|
+
const finalizeResponse = await fetch("/api/v1/user/me/temporary-files", {
|
|
478
845
|
method: "POST",
|
|
479
846
|
headers: buildAuthHeaders(input.apiKey, "application/json"),
|
|
480
847
|
body: JSON.stringify({
|
|
481
|
-
|
|
848
|
+
file_id: presignPayload.file_id,
|
|
482
849
|
file_name: presignPayload.file_name,
|
|
483
850
|
content_type: presignPayload.content_type,
|
|
484
851
|
size_bytes: input.file.size,
|
|
485
|
-
storage_key: presignPayload.storage_key
|
|
852
|
+
storage_key: presignPayload.storage_key,
|
|
853
|
+
folder_path: presignPayload.folder_path || input.folderPath || ""
|
|
486
854
|
})
|
|
487
855
|
});
|
|
488
856
|
const finalizePayload = await parseJson(finalizeResponse);
|
|
489
|
-
if (!finalizeResponse.ok || !finalizePayload.
|
|
490
|
-
throw new Error(finalizePayload.error || "Unable to save
|
|
857
|
+
if (!finalizeResponse.ok || !finalizePayload.file) {
|
|
858
|
+
throw new Error(finalizePayload.error || "Unable to save temporary file.");
|
|
491
859
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
+
];
|
|
495
975
|
}
|
|
496
|
-
function
|
|
976
|
+
function buildAttachmentTransportText(attachments) {
|
|
977
|
+
if (!attachments.length) {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
const lines = attachments.map((attachment) => (`- ${attachment.fileName} (${attachment.contentType}): ${attachment.viewUrl}`));
|
|
497
981
|
return [
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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 {
|
|
505
992
|
role: message.role,
|
|
506
993
|
content: [
|
|
507
994
|
...(message.text ? [{ type: "text", text: message.text }] : []),
|
|
995
|
+
...(attachmentText ? [{ type: "text", text: attachmentText }] : []),
|
|
508
996
|
...message.attachments.map((attachment) => ({
|
|
509
997
|
type: "file",
|
|
510
|
-
data: attachment.transportData || attachment.viewUrl,
|
|
998
|
+
data: messageIndex === latestIndex ? attachment.transportData || attachment.viewUrl : attachment.viewUrl,
|
|
511
999
|
mediaType: attachment.contentType
|
|
512
1000
|
}))
|
|
513
1001
|
]
|
|
514
|
-
}
|
|
515
|
-
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
516
1004
|
}
|
|
517
1005
|
function readCachedScheduleChannels() {
|
|
518
1006
|
const cache = window.__vidfarmFlockPosterCache;
|
|
@@ -755,6 +1243,14 @@ function readTracersFromLocation() {
|
|
|
755
1243
|
}
|
|
756
1244
|
return normalizeTracerList(output);
|
|
757
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
|
+
}
|
|
758
1254
|
function formatThreadTitle(text) {
|
|
759
1255
|
const normalized = text.trim().replace(/\s+/g, " ");
|
|
760
1256
|
if (!normalized) {
|
|
@@ -762,15 +1258,18 @@ function formatThreadTitle(text) {
|
|
|
762
1258
|
}
|
|
763
1259
|
return normalized.length > 42 ? `${normalized.slice(0, 42).trimEnd()}…` : normalized;
|
|
764
1260
|
}
|
|
765
|
-
function serializeThreads(threads) {
|
|
1261
|
+
function serializeThreads(threads, activeThreadId) {
|
|
766
1262
|
return threads.map((thread) => ({
|
|
767
1263
|
id: thread.id,
|
|
1264
|
+
templateId: thread.templateId,
|
|
768
1265
|
title: thread.title,
|
|
769
1266
|
createdAt: thread.createdAt,
|
|
770
1267
|
updatedAt: thread.updatedAt,
|
|
1268
|
+
lastMessageAt: thread.lastMessageAt ?? null,
|
|
1269
|
+
messageCount: Math.max(thread.messageCount ?? 0, thread.messages.length),
|
|
771
1270
|
tracers: thread.tracers,
|
|
772
1271
|
hiddenFromView: thread.hiddenFromView,
|
|
773
|
-
messages: thread.messages.map((message) => ({
|
|
1272
|
+
messages: (thread.id === activeThreadId ? thread.messages.slice(-LOCAL_THREAD_MESSAGE_CACHE_LIMIT) : []).map((message) => ({
|
|
774
1273
|
id: message.id,
|
|
775
1274
|
role: message.role,
|
|
776
1275
|
text: message.text,
|
|
@@ -789,7 +1288,7 @@ function serializeThreads(threads) {
|
|
|
789
1288
|
}))
|
|
790
1289
|
}));
|
|
791
1290
|
}
|
|
792
|
-
function hydrateThreads(raw) {
|
|
1291
|
+
function hydrateThreads(raw, fallbackTemplateId) {
|
|
793
1292
|
if (!raw) {
|
|
794
1293
|
return [];
|
|
795
1294
|
}
|
|
@@ -802,9 +1301,12 @@ function hydrateThreads(raw) {
|
|
|
802
1301
|
.filter((thread) => thread && typeof thread.id === "string")
|
|
803
1302
|
.map((thread) => ({
|
|
804
1303
|
id: thread.id,
|
|
1304
|
+
templateId: typeof thread.templateId === "string" && thread.templateId.trim() ? thread.templateId : fallbackTemplateId,
|
|
805
1305
|
title: typeof thread.title === "string" && thread.title.trim() ? thread.title : "New chat",
|
|
806
1306
|
createdAt: typeof thread.createdAt === "string" ? thread.createdAt : new Date().toISOString(),
|
|
807
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),
|
|
808
1310
|
tracers: normalizeTracerList(Array.isArray(thread.tracers) ? thread.tracers.filter((value) => typeof value === "string") : []),
|
|
809
1311
|
hiddenFromView: thread.hiddenFromView === true,
|
|
810
1312
|
messages: Array.isArray(thread.messages)
|
|
@@ -823,15 +1325,18 @@ function hydrateThreads(raw) {
|
|
|
823
1325
|
return [];
|
|
824
1326
|
}
|
|
825
1327
|
}
|
|
826
|
-
function hydrateThreadRecord(thread) {
|
|
1328
|
+
function hydrateThreadRecord(thread, fallbackTemplateId) {
|
|
827
1329
|
if (!thread || typeof thread.id !== "string") {
|
|
828
1330
|
return null;
|
|
829
1331
|
}
|
|
830
1332
|
return {
|
|
831
1333
|
id: thread.id,
|
|
1334
|
+
templateId: typeof thread.templateId === "string" && thread.templateId.trim() ? thread.templateId : fallbackTemplateId,
|
|
832
1335
|
title: typeof thread.title === "string" && thread.title.trim() ? thread.title : "New chat",
|
|
833
1336
|
createdAt: typeof thread.createdAt === "string" ? thread.createdAt : new Date().toISOString(),
|
|
834
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),
|
|
835
1340
|
tracers: normalizeTracerList(Array.isArray(thread.tracers) ? thread.tracers.filter((value) => typeof value === "string") : []),
|
|
836
1341
|
hiddenFromView: thread.hiddenFromView === true,
|
|
837
1342
|
messages: Array.isArray(thread.messages)
|
|
@@ -849,7 +1354,13 @@ function hydrateThreadRecord(thread) {
|
|
|
849
1354
|
function mergeThreadSets(localThreads, remoteThreads) {
|
|
850
1355
|
const byId = new Map();
|
|
851
1356
|
for (const thread of remoteThreads) {
|
|
852
|
-
|
|
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
|
+
});
|
|
853
1364
|
}
|
|
854
1365
|
for (const thread of localThreads) {
|
|
855
1366
|
if (!byId.has(thread.id)) {
|
|
@@ -858,6 +1369,9 @@ function mergeThreadSets(localThreads, remoteThreads) {
|
|
|
858
1369
|
}
|
|
859
1370
|
return [...byId.values()].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
860
1371
|
}
|
|
1372
|
+
function sortThreadsByUpdatedAt(threads) {
|
|
1373
|
+
return [...threads].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
1374
|
+
}
|
|
861
1375
|
function threadMatchesTracers(thread, selectedTracers) {
|
|
862
1376
|
if (!selectedTracers.length) {
|
|
863
1377
|
return true;
|
|
@@ -894,19 +1408,128 @@ function summarizeThreadPreview(thread) {
|
|
|
894
1408
|
function AttachmentChip(props) {
|
|
895
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] }));
|
|
896
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
|
+
}
|
|
897
1455
|
function MediaPreviewModal(props) {
|
|
898
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";
|
|
899
1466
|
useEffect(() => {
|
|
900
1467
|
setIsMounted(true);
|
|
901
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]);
|
|
902
1525
|
if (!isMounted) {
|
|
903
1526
|
return null;
|
|
904
1527
|
}
|
|
905
|
-
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:
|
|
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);
|
|
906
1529
|
}
|
|
907
1530
|
function ImagePreviewButton(props) {
|
|
908
1531
|
const [hasError, setHasError] = useState(false);
|
|
909
|
-
return (_jsx("button", { type: "button", className: "vf-editor-chat-message-thumb-button is-image", onClick: () => props.onOpenModal(props.preview), "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) })) }));
|
|
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) })) }));
|
|
910
1533
|
}
|
|
911
1534
|
function VideoPreviewCard(props) {
|
|
912
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" })] }) }));
|
|
@@ -915,7 +1538,7 @@ function ImagePreviewRail(props) {
|
|
|
915
1538
|
if (!props.previews.length) {
|
|
916
1539
|
return null;
|
|
917
1540
|
}
|
|
918
|
-
return (_jsx("div", { className: "vf-editor-chat-message-image-rail", "aria-label": "Image previews", children: props.previews.map((preview) => (_jsx(ImagePreviewButton, { preview: preview, onOpenModal: props.onOpenModal }, preview.url))) }));
|
|
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))) }));
|
|
919
1542
|
}
|
|
920
1543
|
function ComposerAttachmentItem(props) {
|
|
921
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" })] }));
|
|
@@ -936,11 +1559,248 @@ function HttpExchangeCard(props) {
|
|
|
936
1559
|
}, []);
|
|
937
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] }));
|
|
938
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
|
+
}
|
|
939
1799
|
function ChatBubble(props) {
|
|
940
1800
|
const isAssistant = props.message.role === "assistant";
|
|
941
1801
|
const hasVisibleText = props.message.text.trim().length > 0;
|
|
942
1802
|
const renderedText = hasVisibleText ? renderMarkdownToHtml(props.message.text) : "";
|
|
943
|
-
const [
|
|
1803
|
+
const [mediaModalState, setMediaModalState] = useState(null);
|
|
944
1804
|
const imageAttachments = props.message.attachments.filter(isImageAttachment);
|
|
945
1805
|
const videoAttachments = props.message.attachments.filter(isVideoAttachment);
|
|
946
1806
|
const fileAttachments = props.message.attachments.filter((attachment) => !isImageAttachment(attachment) && !isVideoAttachment(attachment));
|
|
@@ -949,29 +1809,426 @@ function ChatBubble(props) {
|
|
|
949
1809
|
: [];
|
|
950
1810
|
const inlineImagePreviews = inlineMediaPreviews.filter((preview) => preview.kind === "image");
|
|
951
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);
|
|
952
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) => ({
|
|
953
1821
|
kind: "image",
|
|
954
1822
|
url: attachment.viewUrl,
|
|
955
1823
|
label: attachment.fileName
|
|
956
|
-
})), onOpenModal:
|
|
1824
|
+
})), onOpenModal: (preview, gallery) => setMediaModalState({ preview, gallery }) }), videoAttachments.map((attachment) => (_jsx(VideoPreviewCard, { preview: {
|
|
957
1825
|
kind: "video",
|
|
958
1826
|
url: attachment.viewUrl,
|
|
959
1827
|
label: attachment.fileName
|
|
960
|
-
}, onOpenModal:
|
|
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);
|
|
961
2227
|
}
|
|
962
2228
|
export function TemplateEditorChat({ boot }) {
|
|
2229
|
+
const isBrainstormSurface = boot.template.templateId === "chat-brainstorm";
|
|
963
2230
|
const storageKeys = useMemo(() => makeStorageKeys(boot.template.templateId), [boot.template.templateId]);
|
|
964
2231
|
const chatStorage = useMemo(() => getChatStorage(), []);
|
|
965
|
-
const systemMessages = useMemo(() => {
|
|
966
|
-
const messages = [];
|
|
967
|
-
if (boot.cachedContext.directorSkill?.trim()) {
|
|
968
|
-
messages.push(`Director skill context:\n\n${boot.cachedContext.directorSkill.trim()}`);
|
|
969
|
-
}
|
|
970
|
-
if (boot.cachedContext.templateSkill?.trim()) {
|
|
971
|
-
messages.push(`Template skill context:\n\n${boot.cachedContext.templateSkill.trim()}`);
|
|
972
|
-
}
|
|
973
|
-
return messages;
|
|
974
|
-
}, [boot.cachedContext.directorSkill, boot.cachedContext.templateSkill]);
|
|
975
2232
|
const fileInputRef = useRef(null);
|
|
976
2233
|
const viewportRef = useRef(null);
|
|
977
2234
|
const shouldStickToChatBottomRef = useRef(true);
|
|
@@ -981,8 +2238,10 @@ export function TemplateEditorChat({ boot }) {
|
|
|
981
2238
|
const activeJobPollersRef = useRef(new Map());
|
|
982
2239
|
const jobPollGateRef = useRef({ nextAvailableAt: 0 });
|
|
983
2240
|
const threadsRef = useRef([]);
|
|
2241
|
+
const loadedThreadDetailsRef = useRef(new Set());
|
|
984
2242
|
const [threads, setThreads] = useState([]);
|
|
985
2243
|
const [activeThreadId, setActiveThreadId] = useState(null);
|
|
2244
|
+
const [loadingThreadId, setLoadingThreadId] = useState(null);
|
|
986
2245
|
const [openThreadMenu, setOpenThreadMenu] = useState(null);
|
|
987
2246
|
const [draft, setDraft] = useState("");
|
|
988
2247
|
const [pendingAttachments, setPendingAttachments] = useState([]);
|
|
@@ -992,6 +2251,8 @@ export function TemplateEditorChat({ boot }) {
|
|
|
992
2251
|
const [hasHydrated, setHasHydrated] = useState(false);
|
|
993
2252
|
const [isDropTarget, setIsDropTarget] = useState(false);
|
|
994
2253
|
const [isMobileChatOpen, setIsMobileChatOpen] = useState(false);
|
|
2254
|
+
const [showHistoryPanel, setShowHistoryPanel] = useState(false);
|
|
2255
|
+
const [isUploadDrawerOpen, setIsUploadDrawerOpen] = useState(false);
|
|
995
2256
|
const [tracers, setTracers] = useState(() => {
|
|
996
2257
|
const bridge = typeof window !== "undefined" ? window.__vidfarmEditorTracers : null;
|
|
997
2258
|
if (bridge) {
|
|
@@ -1010,21 +2271,47 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1010
2271
|
return null;
|
|
1011
2272
|
}
|
|
1012
2273
|
const url = new URL(boot.threadsUrl, window.location.origin);
|
|
1013
|
-
|
|
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");
|
|
1014
2279
|
return url.toString();
|
|
1015
|
-
}, [boot.template.templateId, boot.threadsUrl]);
|
|
2280
|
+
}, [boot.template.templateId, boot.threadsUrl, isBrainstormSurface]);
|
|
1016
2281
|
const threadDetailUrl = (threadId) => (boot.threadsUrl
|
|
1017
2282
|
? new URL(`${boot.threadsUrl.replace(/\/$/, "")}/${encodeURIComponent(threadId)}`, window.location.origin).toString()
|
|
1018
2283
|
: null);
|
|
1019
2284
|
useEffect(() => {
|
|
1020
|
-
const hydratedThreads = hydrateThreads(chatStorage?.getItem(storageKeys.threads) ?? null);
|
|
2285
|
+
const hydratedThreads = hydrateThreads(chatStorage?.getItem(storageKeys.threads) ?? null, boot.template.templateId);
|
|
1021
2286
|
const savedActiveThread = chatStorage?.getItem(storageKeys.activeThread) ?? null;
|
|
2287
|
+
const requestedThreadId = readThreadIdFromLocation();
|
|
1022
2288
|
const currentTracers = window.__vidfarmEditorTracers?.get() ?? readTracersFromLocation();
|
|
1023
|
-
const
|
|
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);
|
|
1024
2307
|
const newestMatchingThread = visibleHydratedThreads.find((thread) => threadMatchesTracers(thread, currentTracers));
|
|
1025
|
-
threadsRef.current =
|
|
1026
|
-
setThreads(
|
|
1027
|
-
if (
|
|
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
|
|
1028
2315
|
&& visibleHydratedThreads.some((thread) => thread.id === savedActiveThread)
|
|
1029
2316
|
&& (!currentTracers.length || visibleHydratedThreads.some((thread) => thread.id === savedActiveThread && threadMatchesTracers(thread, currentTracers)))) {
|
|
1030
2317
|
setActiveThreadId(savedActiveThread);
|
|
@@ -1033,7 +2320,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1033
2320
|
setActiveThreadId(newestMatchingThread?.id ?? visibleHydratedThreads[0]?.id ?? null);
|
|
1034
2321
|
}
|
|
1035
2322
|
setHasHydrated(true);
|
|
1036
|
-
}, [chatStorage, storageKeys]);
|
|
2323
|
+
}, [boot.template.templateId, chatStorage, storageKeys]);
|
|
1037
2324
|
useEffect(() => {
|
|
1038
2325
|
if (!boot.vidfarmApiKey || !threadListUrl) {
|
|
1039
2326
|
return;
|
|
@@ -1041,22 +2328,45 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1041
2328
|
const abortController = new AbortController();
|
|
1042
2329
|
void (async () => {
|
|
1043
2330
|
try {
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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);
|
|
1056
2362
|
if (!remoteThreads.length) {
|
|
1057
2363
|
return;
|
|
1058
2364
|
}
|
|
1059
|
-
setThreads((current) =>
|
|
2365
|
+
setThreads((current) => {
|
|
2366
|
+
const nextThreads = mergeThreadSets(current, remoteThreads);
|
|
2367
|
+
threadsRef.current = nextThreads;
|
|
2368
|
+
return nextThreads;
|
|
2369
|
+
});
|
|
1060
2370
|
setActiveThreadId((current) => {
|
|
1061
2371
|
if (current && remoteThreads.some((thread) => thread.id === current)) {
|
|
1062
2372
|
return current;
|
|
@@ -1073,7 +2383,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1073
2383
|
return () => {
|
|
1074
2384
|
abortController.abort();
|
|
1075
2385
|
};
|
|
1076
|
-
}, [boot.vidfarmApiKey, threadListUrl]);
|
|
2386
|
+
}, [boot.template.templateId, boot.vidfarmApiKey, threadListUrl]);
|
|
1077
2387
|
useEffect(() => {
|
|
1078
2388
|
threadsRef.current = threads;
|
|
1079
2389
|
}, [threads]);
|
|
@@ -1098,12 +2408,12 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1098
2408
|
return;
|
|
1099
2409
|
}
|
|
1100
2410
|
try {
|
|
1101
|
-
chatStorage?.setItem(storageKeys.threads, JSON.stringify(serializeThreads(threads)));
|
|
2411
|
+
chatStorage?.setItem(storageKeys.threads, JSON.stringify(serializeThreads(threads, activeThreadId)));
|
|
1102
2412
|
}
|
|
1103
2413
|
catch (error) {
|
|
1104
2414
|
console.warn("Unable to persist editor chat threads.", error);
|
|
1105
2415
|
}
|
|
1106
|
-
}, [chatStorage, hasHydrated, storageKeys, threads]);
|
|
2416
|
+
}, [activeThreadId, chatStorage, hasHydrated, storageKeys, threads]);
|
|
1107
2417
|
useEffect(() => {
|
|
1108
2418
|
if (!hasHydrated) {
|
|
1109
2419
|
return;
|
|
@@ -1122,6 +2432,74 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1122
2432
|
}, [activeThreadId, chatStorage, hasHydrated, storageKeys]);
|
|
1123
2433
|
const activeThread = useMemo(() => threads.find((thread) => thread.id === activeThreadId) ?? null, [activeThreadId, threads]);
|
|
1124
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]);
|
|
1125
2503
|
useEffect(() => {
|
|
1126
2504
|
if (!hasHydrated || activeThread && !activeThread.hiddenFromView) {
|
|
1127
2505
|
return;
|
|
@@ -1143,6 +2521,12 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1143
2521
|
const isAnyThreadSending = sendingThreadId !== null;
|
|
1144
2522
|
const isActiveThreadSending = Boolean(activeThreadId && sendingThreadId === activeThreadId);
|
|
1145
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]);
|
|
1146
2530
|
async function updateRemoteThreadState(thread, overrides) {
|
|
1147
2531
|
if (!boot.vidfarmApiKey || !boot.threadsUrl || !thread.messages.length) {
|
|
1148
2532
|
return;
|
|
@@ -1163,6 +2547,37 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1163
2547
|
})
|
|
1164
2548
|
}).catch(() => { });
|
|
1165
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
|
+
}
|
|
1166
2581
|
async function deleteRemoteThreadState(threadId) {
|
|
1167
2582
|
if (!boot.vidfarmApiKey || !boot.threadsUrl) {
|
|
1168
2583
|
return;
|
|
@@ -1343,12 +2758,16 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1343
2758
|
const threadId = makeThreadId();
|
|
1344
2759
|
const nextThread = {
|
|
1345
2760
|
id: threadId,
|
|
2761
|
+
templateId: boot.template.templateId,
|
|
1346
2762
|
title: formatThreadTitle(seedText || ""),
|
|
1347
2763
|
createdAt: now,
|
|
1348
2764
|
updatedAt: now,
|
|
2765
|
+
lastMessageAt: null,
|
|
2766
|
+
messageCount: 0,
|
|
1349
2767
|
tracers: normalizeTracerList(tracers),
|
|
1350
2768
|
messages: []
|
|
1351
2769
|
};
|
|
2770
|
+
threadsRef.current = [nextThread, ...threadsRef.current];
|
|
1352
2771
|
setThreads((current) => [nextThread, ...current]);
|
|
1353
2772
|
setActiveThreadId(threadId);
|
|
1354
2773
|
return threadId;
|
|
@@ -1359,9 +2778,12 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1359
2778
|
const threadId = makeThreadId();
|
|
1360
2779
|
const nextThread = {
|
|
1361
2780
|
id: threadId,
|
|
2781
|
+
templateId: boot.template.templateId,
|
|
1362
2782
|
title: options?.title || "New chat",
|
|
1363
2783
|
createdAt: now,
|
|
1364
2784
|
updatedAt: now,
|
|
2785
|
+
lastMessageAt: null,
|
|
2786
|
+
messageCount: 0,
|
|
1365
2787
|
tracers: threadTracers,
|
|
1366
2788
|
messages: []
|
|
1367
2789
|
};
|
|
@@ -1388,9 +2810,10 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1388
2810
|
return;
|
|
1389
2811
|
}
|
|
1390
2812
|
const remainingThreads = threads.filter((entry) => entry.id !== threadId);
|
|
1391
|
-
|
|
2813
|
+
threadsRef.current = threadsRef.current.filter((entry) => entry.id !== threadId);
|
|
2814
|
+
for (const [pollerKey, poller] of activeJobPollersRef.current.entries()) {
|
|
1392
2815
|
if (poller.threadId === threadId) {
|
|
1393
|
-
stopJobPoller(
|
|
2816
|
+
stopJobPoller(pollerKey);
|
|
1394
2817
|
}
|
|
1395
2818
|
}
|
|
1396
2819
|
setThreads(remainingThreads);
|
|
@@ -1402,10 +2825,9 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1402
2825
|
void deleteRemoteThreadState(threadId);
|
|
1403
2826
|
}
|
|
1404
2827
|
function updateThread(threadId, updater) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
});
|
|
2828
|
+
const nextThreads = sortThreadsByUpdatedAt(threadsRef.current.map((thread) => (thread.id === threadId ? updater(thread) : thread)));
|
|
2829
|
+
threadsRef.current = nextThreads;
|
|
2830
|
+
setThreads(nextThreads);
|
|
1409
2831
|
}
|
|
1410
2832
|
function attachTracersToThread(threadId, nextTracers) {
|
|
1411
2833
|
const normalized = normalizeTracerList(nextTracers);
|
|
@@ -1447,7 +2869,11 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1447
2869
|
const normalized = text.toLowerCase();
|
|
1448
2870
|
return /(?:own|separate|different|new)\s+(?:tracer\s+and\s+)?chat\s+threads?/.test(normalized)
|
|
1449
2871
|
|| /chat\s+threads?.{0,80}(?:own|separate|different|new)\s+tracers?/.test(normalized)
|
|
1450
|
-
|| /each.{0,80}(?:own|separate|different|new).{0,80}chat\s+threads?/.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);
|
|
1451
2877
|
}
|
|
1452
2878
|
function mirrorJobToTracerThread(input) {
|
|
1453
2879
|
const existingThreadId = findThreadIdForTracer(input.tracer, input.sourceThreadId);
|
|
@@ -1469,22 +2895,33 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1469
2895
|
const statusText = input.job.status === "succeeded" || input.job.status === "failed" || input.job.status === "cancelled"
|
|
1470
2896
|
? null
|
|
1471
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
|
+
};
|
|
1472
2907
|
updateThread(targetThreadId, (thread) => ({
|
|
1473
2908
|
...thread,
|
|
1474
2909
|
updatedAt: now,
|
|
1475
2910
|
messages: [
|
|
1476
2911
|
...thread.messages,
|
|
1477
|
-
|
|
1478
|
-
id: messageId,
|
|
1479
|
-
role: "assistant",
|
|
1480
|
-
text: messageText,
|
|
1481
|
-
attachments: [],
|
|
1482
|
-
httpExchanges: input.httpExchange ? [input.httpExchange] : [],
|
|
1483
|
-
statusText,
|
|
1484
|
-
isPending: false
|
|
1485
|
-
}
|
|
2912
|
+
mirroredMessage
|
|
1486
2913
|
]
|
|
1487
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
|
+
}
|
|
1488
2925
|
return { threadId: targetThreadId, messageId };
|
|
1489
2926
|
}
|
|
1490
2927
|
function editThreadTracers(threadId) {
|
|
@@ -1532,9 +2969,9 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1532
2969
|
if (!confirmed) {
|
|
1533
2970
|
return;
|
|
1534
2971
|
}
|
|
1535
|
-
for (const [
|
|
2972
|
+
for (const [pollerKey, poller] of activeJobPollersRef.current.entries()) {
|
|
1536
2973
|
if (poller.threadId === threadId) {
|
|
1537
|
-
stopJobPoller(
|
|
2974
|
+
stopJobPoller(pollerKey);
|
|
1538
2975
|
}
|
|
1539
2976
|
}
|
|
1540
2977
|
updateThread(threadId, (current) => ({
|
|
@@ -1588,8 +3025,8 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1588
3025
|
left: Math.max(8, Math.min(window.innerWidth - menuWidth - 8, rect.right - menuWidth))
|
|
1589
3026
|
});
|
|
1590
3027
|
}
|
|
1591
|
-
function stopJobPoller(
|
|
1592
|
-
const poller = activeJobPollersRef.current.get(
|
|
3028
|
+
function stopJobPoller(pollerKey) {
|
|
3029
|
+
const poller = activeJobPollersRef.current.get(pollerKey);
|
|
1593
3030
|
if (!poller) {
|
|
1594
3031
|
return;
|
|
1595
3032
|
}
|
|
@@ -1597,10 +3034,10 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1597
3034
|
window.clearTimeout(poller.timeoutId);
|
|
1598
3035
|
}
|
|
1599
3036
|
poller.abortController?.abort();
|
|
1600
|
-
activeJobPollersRef.current.delete(
|
|
3037
|
+
activeJobPollersRef.current.delete(pollerKey);
|
|
1601
3038
|
}
|
|
1602
|
-
function scheduleNextJobPoll(
|
|
1603
|
-
const poller = activeJobPollersRef.current.get(
|
|
3039
|
+
function scheduleNextJobPoll(pollerKey, runPoll, delayMs) {
|
|
3040
|
+
const poller = activeJobPollersRef.current.get(pollerKey);
|
|
1604
3041
|
if (!poller) {
|
|
1605
3042
|
return;
|
|
1606
3043
|
}
|
|
@@ -1631,7 +3068,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1631
3068
|
}
|
|
1632
3069
|
function getJobPollCadenceText(ms = getJobPollIntervalMs()) {
|
|
1633
3070
|
return activeJobPollersRef.current.size >= JOB_POLL_BUSY_THRESHOLD
|
|
1634
|
-
? `Many
|
|
3071
|
+
? `Many renders are active, so the next check is in ${formatJobPollInterval(ms)}.`
|
|
1635
3072
|
: `Checking again gently every ${formatJobPollInterval(ms)}.`;
|
|
1636
3073
|
}
|
|
1637
3074
|
function rescheduleActiveJobPollersForLoad() {
|
|
@@ -1656,10 +3093,11 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1656
3093
|
return slotAt - now;
|
|
1657
3094
|
}
|
|
1658
3095
|
function scheduleJobPolling(input) {
|
|
1659
|
-
|
|
3096
|
+
const pollerKey = makeJobPollerKey(input.threadId, input.assistantMessageId, input.jobId);
|
|
3097
|
+
if (activeJobPollersRef.current.has(pollerKey)) {
|
|
1660
3098
|
return;
|
|
1661
3099
|
}
|
|
1662
|
-
activeJobPollersRef.current.set(
|
|
3100
|
+
activeJobPollersRef.current.set(pollerKey, {
|
|
1663
3101
|
timeoutId: null,
|
|
1664
3102
|
abortController: null,
|
|
1665
3103
|
runPoll: null,
|
|
@@ -1670,7 +3108,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1670
3108
|
threadId: input.threadId
|
|
1671
3109
|
});
|
|
1672
3110
|
const runPoll = async () => {
|
|
1673
|
-
const poller = activeJobPollersRef.current.get(
|
|
3111
|
+
const poller = activeJobPollersRef.current.get(pollerKey);
|
|
1674
3112
|
if (!poller) {
|
|
1675
3113
|
return;
|
|
1676
3114
|
}
|
|
@@ -1680,7 +3118,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1680
3118
|
const now = Date.now();
|
|
1681
3119
|
const pollIntervalMs = getJobPollIntervalMs();
|
|
1682
3120
|
if (poller.lastPollAt !== null && now - poller.lastPollAt < pollIntervalMs) {
|
|
1683
|
-
scheduleNextJobPoll(
|
|
3121
|
+
scheduleNextJobPoll(pollerKey, runPoll, pollIntervalMs - (now - poller.lastPollAt));
|
|
1684
3122
|
return;
|
|
1685
3123
|
}
|
|
1686
3124
|
if (poller.attempts >= JOB_POLL_MAX_ATTEMPTS) {
|
|
@@ -1694,16 +3132,16 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1694
3132
|
}
|
|
1695
3133
|
: message))
|
|
1696
3134
|
}));
|
|
1697
|
-
stopJobPoller(
|
|
3135
|
+
stopJobPoller(pollerKey);
|
|
1698
3136
|
return;
|
|
1699
3137
|
}
|
|
1700
3138
|
if (document.visibilityState !== "visible" || !navigator.onLine) {
|
|
1701
|
-
scheduleNextJobPoll(
|
|
3139
|
+
scheduleNextJobPoll(pollerKey, runPoll, pollIntervalMs);
|
|
1702
3140
|
return;
|
|
1703
3141
|
}
|
|
1704
3142
|
const globalDelayMs = reserveJobPollSlot(Date.now());
|
|
1705
3143
|
if (globalDelayMs > 0) {
|
|
1706
|
-
scheduleNextJobPoll(
|
|
3144
|
+
scheduleNextJobPoll(pollerKey, runPoll, globalDelayMs);
|
|
1707
3145
|
return;
|
|
1708
3146
|
}
|
|
1709
3147
|
poller.inFlight = true;
|
|
@@ -1722,7 +3160,9 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1722
3160
|
}
|
|
1723
3161
|
: message))
|
|
1724
3162
|
}));
|
|
1725
|
-
const url =
|
|
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)}`;
|
|
1726
3166
|
const response = await fetch(url, {
|
|
1727
3167
|
method: "GET",
|
|
1728
3168
|
headers: buildAuthHeaders(boot.vidfarmApiKey),
|
|
@@ -1756,11 +3196,11 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1756
3196
|
}));
|
|
1757
3197
|
const nextStatusText = payload
|
|
1758
3198
|
? payload.status === "succeeded"
|
|
1759
|
-
? `
|
|
3199
|
+
? `Render ${payload.job_id} succeeded.`
|
|
1760
3200
|
: payload.status === "failed" || payload.status === "cancelled"
|
|
1761
|
-
? `
|
|
1762
|
-
: `
|
|
1763
|
-
: `
|
|
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)}.`;
|
|
1764
3204
|
return {
|
|
1765
3205
|
...message,
|
|
1766
3206
|
httpExchanges: nextHttpExchanges,
|
|
@@ -1772,7 +3212,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1772
3212
|
})
|
|
1773
3213
|
}));
|
|
1774
3214
|
if (payload && (payload.status === "succeeded" || payload.status === "failed" || payload.status === "cancelled")) {
|
|
1775
|
-
stopJobPoller(
|
|
3215
|
+
stopJobPoller(pollerKey);
|
|
1776
3216
|
return;
|
|
1777
3217
|
}
|
|
1778
3218
|
}
|
|
@@ -1792,7 +3232,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1792
3232
|
}));
|
|
1793
3233
|
}
|
|
1794
3234
|
finally {
|
|
1795
|
-
const latestPoller = activeJobPollersRef.current.get(
|
|
3235
|
+
const latestPoller = activeJobPollersRef.current.get(pollerKey);
|
|
1796
3236
|
if (!latestPoller) {
|
|
1797
3237
|
return;
|
|
1798
3238
|
}
|
|
@@ -1800,21 +3240,55 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1800
3240
|
latestPoller.abortController = null;
|
|
1801
3241
|
if (latestPoller.attempts < JOB_POLL_MAX_ATTEMPTS
|
|
1802
3242
|
&& latestPoller.timeoutId === null
|
|
1803
|
-
&& activeJobPollersRef.current.has(
|
|
1804
|
-
scheduleNextJobPoll(
|
|
3243
|
+
&& activeJobPollersRef.current.has(pollerKey)) {
|
|
3244
|
+
scheduleNextJobPoll(pollerKey, runPoll, getJobPollIntervalMs());
|
|
1805
3245
|
}
|
|
1806
3246
|
}
|
|
1807
3247
|
};
|
|
1808
|
-
const initialPoller = activeJobPollersRef.current.get(
|
|
3248
|
+
const initialPoller = activeJobPollersRef.current.get(pollerKey);
|
|
1809
3249
|
if (initialPoller) {
|
|
1810
3250
|
initialPoller.runPoll = runPoll;
|
|
1811
3251
|
}
|
|
1812
|
-
const poller = activeJobPollersRef.current.get(
|
|
3252
|
+
const poller = activeJobPollersRef.current.get(pollerKey);
|
|
1813
3253
|
if (poller) {
|
|
1814
|
-
scheduleNextJobPoll(
|
|
3254
|
+
scheduleNextJobPoll(pollerKey, runPoll, input.initialDelayMs ?? getJobPollIntervalMs());
|
|
1815
3255
|
rescheduleActiveJobPollersForLoad();
|
|
1816
3256
|
}
|
|
1817
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
|
+
}
|
|
1818
3292
|
async function appendFiles(files) {
|
|
1819
3293
|
if (!files.length) {
|
|
1820
3294
|
return;
|
|
@@ -1824,15 +3298,17 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1824
3298
|
let lastUploadError = null;
|
|
1825
3299
|
for (const file of files) {
|
|
1826
3300
|
try {
|
|
1827
|
-
const
|
|
1828
|
-
const uploaded = await uploadAttachment({
|
|
3301
|
+
const uploaded = await uploadTemporaryFile({
|
|
1829
3302
|
apiKey: boot.vidfarmApiKey,
|
|
1830
|
-
file
|
|
3303
|
+
file,
|
|
3304
|
+
folderPath: CHAT_TEMPORARY_UPLOAD_FOLDER
|
|
1831
3305
|
});
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
3306
|
+
const attachment = await addInlinePreviewForChatUpload({
|
|
3307
|
+
attachment: uploaded,
|
|
3308
|
+
file,
|
|
3309
|
+
currentInlineBytes: estimateInlineAttachmentBytes([...pendingAttachments, ...uploadedAttachments])
|
|
1835
3310
|
});
|
|
3311
|
+
uploadedAttachments.push(attachment);
|
|
1836
3312
|
}
|
|
1837
3313
|
catch (error) {
|
|
1838
3314
|
lastUploadError = error instanceof Error ? error.message : "Unable to upload attachment.";
|
|
@@ -1877,6 +3353,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1877
3353
|
forceChatBottomOnNextRenderRef.current = true;
|
|
1878
3354
|
const threadId = ensureActiveThread(draft);
|
|
1879
3355
|
const now = new Date().toISOString();
|
|
3356
|
+
const threadForSend = threadsRef.current.find((thread) => thread.id === threadId);
|
|
1880
3357
|
const userMessage = {
|
|
1881
3358
|
id: makeMessageId("user"),
|
|
1882
3359
|
role: "user",
|
|
@@ -1891,8 +3368,8 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1891
3368
|
statusText: "Waiting for AI response…",
|
|
1892
3369
|
isPending: true
|
|
1893
3370
|
};
|
|
1894
|
-
const existingMessages =
|
|
1895
|
-
const historyForTransport = [...existingMessages, userMessage];
|
|
3371
|
+
const existingMessages = threadForSend?.messages ?? [];
|
|
3372
|
+
const historyForTransport = compactTransportHistory([...existingMessages, userMessage]);
|
|
1896
3373
|
updateThread(threadId, (thread) => ({
|
|
1897
3374
|
...thread,
|
|
1898
3375
|
title: thread.messages.length === 0 ? formatThreadTitle(userMessage.text) : thread.title,
|
|
@@ -1911,42 +3388,44 @@ export function TemplateEditorChat({ boot }) {
|
|
|
1911
3388
|
assistantMessageId: assistantMessage.id
|
|
1912
3389
|
};
|
|
1913
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
|
+
});
|
|
1914
3424
|
const response = await fetch(boot.apiUrl || "/api/v1/editor-chat", {
|
|
1915
3425
|
method: "POST",
|
|
1916
3426
|
headers: buildAuthHeaders(boot.vidfarmApiKey, "application/json"),
|
|
1917
3427
|
signal: abortController.signal,
|
|
1918
|
-
body:
|
|
1919
|
-
messages: toTransportMessages(historyForTransport, systemMessages),
|
|
1920
|
-
thread_id: threadId,
|
|
1921
|
-
thread_title: existingMessages.length === 0 ? formatThreadTitle(userMessage.text) : (threads.find((entry) => entry.id === threadId)?.title ?? formatThreadTitle(userMessage.text)),
|
|
1922
|
-
thread_tracers: requestTracers,
|
|
1923
|
-
client_context: buildBrowserDateContext(),
|
|
1924
|
-
user_message: {
|
|
1925
|
-
id: userMessage.id,
|
|
1926
|
-
role: userMessage.role,
|
|
1927
|
-
text: userMessage.text,
|
|
1928
|
-
attachments: userMessage.attachments.map((attachment) => ({
|
|
1929
|
-
id: attachment.id,
|
|
1930
|
-
fileName: attachment.fileName,
|
|
1931
|
-
contentType: attachment.contentType,
|
|
1932
|
-
viewUrl: attachment.viewUrl
|
|
1933
|
-
}))
|
|
1934
|
-
},
|
|
1935
|
-
assistant_message: {
|
|
1936
|
-
id: assistantMessage.id
|
|
1937
|
-
},
|
|
1938
|
-
template: {
|
|
1939
|
-
...boot.template,
|
|
1940
|
-
tracers: requestTracers,
|
|
1941
|
-
defaultRequestTracer,
|
|
1942
|
-
activeTracer: defaultRequestTracer
|
|
1943
|
-
},
|
|
1944
|
-
cached_context: {
|
|
1945
|
-
...boot.cachedContext,
|
|
1946
|
-
...readCachedScheduleChannels(),
|
|
1947
|
-
...readSelectedLibraryPosts()
|
|
1948
|
-
}
|
|
1949
|
-
})
|
|
3428
|
+
body: requestBody
|
|
1950
3429
|
});
|
|
1951
3430
|
if (!response.ok) {
|
|
1952
3431
|
const payload = await response.json().catch(() => ({}));
|
|
@@ -2049,18 +3528,16 @@ export function TemplateEditorChat({ boot }) {
|
|
|
2049
3528
|
})
|
|
2050
3529
|
: null;
|
|
2051
3530
|
if (mirroredJobTarget
|
|
2052
|
-
&& toolResultRecordForMirror?.request?.method === "POST"
|
|
2053
|
-
&& toolResultRecordForMirror?.response?.status === 202
|
|
2054
3531
|
&& jobStatusPayloadForMirror
|
|
2055
|
-
&& jobStatusPayloadForMirror.status
|
|
2056
|
-
&& jobStatusPayloadForMirror.status !== "failed"
|
|
2057
|
-
&& jobStatusPayloadForMirror.status !== "cancelled") {
|
|
3532
|
+
&& !isTerminalJobStatus(jobStatusPayloadForMirror.status)) {
|
|
2058
3533
|
scheduleJobPolling({
|
|
2059
3534
|
threadId: mirroredJobTarget.threadId,
|
|
2060
3535
|
assistantMessageId: mirroredJobTarget.messageId,
|
|
2061
3536
|
jobId: jobStatusPayloadForMirror.job_id,
|
|
2062
3537
|
tracer: jobStatusPayloadForMirror.tracer,
|
|
2063
|
-
initialStatus: jobStatusPayloadForMirror.status
|
|
3538
|
+
initialStatus: jobStatusPayloadForMirror.status,
|
|
3539
|
+
primitiveId: jobStatusPayloadForMirror.primitive_id,
|
|
3540
|
+
sourceRequestUrl: toolResultRecordForMirror?.request?.url
|
|
2064
3541
|
});
|
|
2065
3542
|
}
|
|
2066
3543
|
updateThread(threadId, (thread) => ({
|
|
@@ -2102,30 +3579,35 @@ export function TemplateEditorChat({ boot }) {
|
|
|
2102
3579
|
}
|
|
2103
3580
|
attachTracersToThread(threadId, [jobStatusPayload.tracer]);
|
|
2104
3581
|
}
|
|
2105
|
-
if (
|
|
2106
|
-
&& toolResultRecord?.response?.status === 202
|
|
2107
|
-
&& jobStatusPayload
|
|
3582
|
+
if (jobStatusPayload
|
|
2108
3583
|
&& !mirroredJobTarget
|
|
2109
|
-
&& jobStatusPayload.status
|
|
2110
|
-
&& jobStatusPayload.status !== "failed"
|
|
2111
|
-
&& jobStatusPayload.status !== "cancelled") {
|
|
3584
|
+
&& !isTerminalJobStatus(jobStatusPayload.status)) {
|
|
2112
3585
|
scheduleJobPolling({
|
|
2113
3586
|
threadId,
|
|
2114
3587
|
assistantMessageId: assistantMessage.id,
|
|
2115
3588
|
jobId: jobStatusPayload.job_id,
|
|
2116
3589
|
tracer: jobStatusPayload.tracer,
|
|
2117
|
-
initialStatus: jobStatusPayload.status
|
|
3590
|
+
initialStatus: jobStatusPayload.status,
|
|
3591
|
+
primitiveId: jobStatusPayload.primitive_id,
|
|
3592
|
+
sourceRequestUrl: toolResultRecord?.request?.url
|
|
2118
3593
|
});
|
|
2119
3594
|
}
|
|
2120
3595
|
return {
|
|
2121
3596
|
...message,
|
|
2122
3597
|
toolExchanges: nextToolExchanges,
|
|
2123
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,
|
|
2124
3604
|
text: jobStatusPayload && toolResultRecord?.request?.method === "POST" && toolResultRecord?.response?.status === 202
|
|
2125
3605
|
? appendDistinctMessageText(message.text, buildJobQueuedMessage(jobStatusPayload))
|
|
2126
|
-
: jobStatusPayload && (jobStatusPayload.status
|
|
3606
|
+
: jobStatusPayload && isTerminalJobStatus(jobStatusPayload.status)
|
|
2127
3607
|
? appendDistinctMessageText(message.text, buildJobCompletionMessage(jobStatusPayload))
|
|
2128
|
-
:
|
|
3608
|
+
: jobStatusPayload
|
|
3609
|
+
? appendDistinctMessageText(message.text, buildJobCompletionMessage(jobStatusPayload))
|
|
3610
|
+
: message.text
|
|
2129
3611
|
};
|
|
2130
3612
|
})
|
|
2131
3613
|
}));
|
|
@@ -2145,7 +3627,7 @@ export function TemplateEditorChat({ boot }) {
|
|
|
2145
3627
|
messages: thread.messages.map((message) => (message.id === assistantMessage.id
|
|
2146
3628
|
? {
|
|
2147
3629
|
...message,
|
|
2148
|
-
statusText: null,
|
|
3630
|
+
statusText: message.statusText?.includes("being tracked") ? message.statusText : null,
|
|
2149
3631
|
isPending: false
|
|
2150
3632
|
}
|
|
2151
3633
|
: message))
|
|
@@ -2220,42 +3702,66 @@ export function TemplateEditorChat({ boot }) {
|
|
|
2220
3702
|
}
|
|
2221
3703
|
activeSendRef.current?.abortController.abort();
|
|
2222
3704
|
}
|
|
3705
|
+
useEffect(() => {
|
|
3706
|
+
return () => {
|
|
3707
|
+
activeSendRef.current?.abortController.abort();
|
|
3708
|
+
for (const pollerKey of activeJobPollersRef.current.keys()) {
|
|
3709
|
+
stopJobPoller(pollerKey);
|
|
3710
|
+
}
|
|
3711
|
+
};
|
|
3712
|
+
}, []);
|
|
2223
3713
|
function handleComposerKeyDown(event) {
|
|
2224
3714
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
2225
3715
|
event.preventDefault();
|
|
2226
3716
|
void handleSend();
|
|
2227
3717
|
}
|
|
2228
3718
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
}
|
|
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
|