@mevdragon/vidfarm-devcli 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.agents/skills/vidfarm-media/SKILL.md +43 -6
  2. package/SKILL.director.md +37 -23
  3. package/SKILL.platform.md +6 -6
  4. package/demo/dist/app.js +69 -69
  5. package/demo/dist/favicon.ico +0 -0
  6. package/dist/src/app.js +1832 -213
  7. package/dist/src/cli.js +238 -17
  8. package/dist/src/devcli/clip-store.js +29 -2
  9. package/dist/src/devcli/clips.js +116 -73
  10. package/dist/src/devcli/composition-edit.js +262 -0
  11. package/dist/src/devcli/timeline-edit.js +283 -0
  12. package/dist/src/editor-chat.js +29 -6
  13. package/dist/src/frontend/discover-client.js +130 -0
  14. package/dist/src/frontend/discover-store.js +23 -0
  15. package/dist/src/frontend/file-directory.js +744 -0
  16. package/dist/src/frontend/template-editor-chat.js +22 -19
  17. package/dist/src/landing-page.js +24 -7
  18. package/dist/src/page-shell.js +25 -1
  19. package/dist/src/reskin/agency-page.js +214 -170
  20. package/dist/src/reskin/calendar-page.js +503 -187
  21. package/dist/src/reskin/chat-page.js +739 -299
  22. package/dist/src/reskin/discover-page.js +1450 -279
  23. package/dist/src/reskin/document.js +1392 -16
  24. package/dist/src/reskin/help-page.js +139 -100
  25. package/dist/src/reskin/index-page.js +1 -1
  26. package/dist/src/reskin/inpaint-page.js +547 -0
  27. package/dist/src/reskin/job-runs-page.js +355 -127
  28. package/dist/src/reskin/library-page.js +1188 -317
  29. package/dist/src/reskin/login-page.js +124 -87
  30. package/dist/src/reskin/pricing-page.js +197 -166
  31. package/dist/src/reskin/settings-page.js +566 -187
  32. package/dist/src/reskin/theme.js +434 -17
  33. package/dist/src/services/clip-curation/gemini.js +5 -0
  34. package/dist/src/services/clip-curation/hunt.js +79 -1
  35. package/dist/src/services/clip-curation/index.js +2 -1
  36. package/dist/src/services/clip-curation/local-agent.js +4 -3
  37. package/dist/src/services/clip-curation/media-select.js +85 -0
  38. package/dist/src/services/clip-curation/query.js +5 -1
  39. package/dist/src/services/clip-curation/refine.js +50 -20
  40. package/dist/src/services/clip-curation/scan.js +10 -3
  41. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  42. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  43. package/dist/src/services/clip-records.js +14 -1
  44. package/dist/src/services/clip-search.js +43 -13
  45. package/dist/src/services/file-directory.js +114 -0
  46. package/dist/src/services/storage.js +24 -1
  47. package/dist/src/services/upstream.js +5 -5
  48. package/dist/src/template-editor-shell.js +16 -2
  49. package/package.json +1 -1
  50. package/public/assets/discover-client-app.js +1 -0
  51. package/public/assets/file-directory-app.js +2 -0
  52. package/public/assets/homepage-client-app.js +12 -12
  53. package/public/assets/page-runtime-client-app.js +24 -24
  54. package/src/assets/favicon.ico +0 -0
  55. package/src/assets/logo-vidfarm.png +0 -0
@@ -0,0 +1,744 @@
1
+ // Reusable file-directory explorer component (browser client).
2
+ //
3
+ // One framework-free component that browses the unified virtual directory —
4
+ // /files, /temp, /raws — as a real folder tree, with a per-row "…" menu
5
+ // (Copy Path / Attach / View / Preview), default-click = Copy Path, folder
6
+ // Open, multi-file + folder select, and unified search (semantic + substring +
7
+ // absolute-path). It talks to the /me/directory[/search] endpoints and returns
8
+ // the one canonical DirectoryItem shape from every backend.
9
+ //
10
+ // It's a COMPONENT, not a one-off: mount it anywhere with
11
+ // `window.__vidfarmDirectory.mount(el, opts)`, or drop a `<div data-rk-directory>`
12
+ // and it auto-mounts. The chat dock hosts it in "attach" mode; /library/raws
13
+ // hosts the same code in "standalone" mode scoped to /raws — no duplication.
14
+ //
15
+ // Host coupling (chat dock chips / preview modal) is passed IN via opts
16
+ // callbacks, or, for the auto-mounted dock instance, read from a
17
+ // `window.__rkDirectoryHost` hook the dock's inline script sets synchronously
18
+ // before this deferred module runs.
19
+ import { buildDirectoryPath, DIRECTORY_ROOTS, normalizeFolderPath, parseDirectoryPath } from "../services/file-directory.js";
20
+ const DEFAULT_API_BASE = "/api/v1/user/me";
21
+ // ── small DOM helpers ────────────────────────────────────────────────────────
22
+ function el(tag, cls, text) {
23
+ const node = document.createElement(tag);
24
+ if (cls)
25
+ node.className = cls;
26
+ if (text != null)
27
+ node.textContent = text;
28
+ return node;
29
+ }
30
+ function fmtBytes(b) {
31
+ if (!b || b <= 0)
32
+ return "";
33
+ const u = ["B", "KB", "MB", "GB"];
34
+ let v = b, i = 0;
35
+ while (v >= 1024 && i < u.length - 1) {
36
+ v /= 1024;
37
+ i++;
38
+ }
39
+ return (v >= 10 || i === 0 ? Math.round(v) : v.toFixed(1)) + " " + u[i];
40
+ }
41
+ function fmtDur(s) {
42
+ if (typeof s !== "number" || !(s > 0))
43
+ return "";
44
+ s = Math.round(s);
45
+ const m = Math.floor(s / 60), r = s % 60;
46
+ return m + ":" + (r < 10 ? "0" + r : r);
47
+ }
48
+ function iconFor(it) {
49
+ const ic = el("span", "rk-aichat-fic " + (it.kind === "folder" ? "rk-aichat-fic-folder" : "rk-aichat-fic-file"));
50
+ if (it.kind === "folder") {
51
+ ic.textContent = "/";
52
+ return ic;
53
+ }
54
+ if (it.thumbUrl) {
55
+ const t = el("img");
56
+ t.src = it.thumbUrl;
57
+ t.alt = "";
58
+ t.loading = "lazy";
59
+ ic.appendChild(t);
60
+ return ic;
61
+ }
62
+ const ct = it.contentType || "";
63
+ if (it.viewUrl && ct.indexOf("image/") === 0) {
64
+ const im = el("img");
65
+ im.src = it.viewUrl;
66
+ im.alt = "";
67
+ im.loading = "lazy";
68
+ ic.appendChild(im);
69
+ return ic;
70
+ }
71
+ if (it.viewUrl && ct.indexOf("video/") === 0) {
72
+ const v = el("video");
73
+ v.src = it.viewUrl;
74
+ v.muted = true;
75
+ v.playsInline = true;
76
+ ic.appendChild(v);
77
+ return ic;
78
+ }
79
+ ic.textContent = ((it.name.split(".").pop() || it.name.slice(0, 1)) || "?").slice(0, 3).toUpperCase();
80
+ return ic;
81
+ }
82
+ // ── shared floating layers (toast + open kebab menu) ─────────────────────────
83
+ let toastTimer;
84
+ function showToast(msg) {
85
+ let t = document.getElementById("rk-dir-toast");
86
+ if (!t) {
87
+ t = el("div");
88
+ t.id = "rk-dir-toast";
89
+ t.className = "rk-dir-toast";
90
+ document.body.appendChild(t);
91
+ }
92
+ t.textContent = msg;
93
+ t.classList.add("is-on");
94
+ if (toastTimer)
95
+ window.clearTimeout(toastTimer);
96
+ toastTimer = window.setTimeout(() => t && t.classList.remove("is-on"), 1800);
97
+ }
98
+ function copyToClipboard(text) {
99
+ if (navigator.clipboard && navigator.clipboard.writeText)
100
+ return navigator.clipboard.writeText(text);
101
+ return new Promise((resolve, reject) => {
102
+ try {
103
+ const ta = el("textarea");
104
+ ta.value = text;
105
+ ta.style.position = "fixed";
106
+ ta.style.opacity = "0";
107
+ document.body.appendChild(ta);
108
+ ta.select();
109
+ document.execCommand("copy");
110
+ document.body.removeChild(ta);
111
+ resolve();
112
+ }
113
+ catch (e) {
114
+ reject(e);
115
+ }
116
+ });
117
+ }
118
+ let closeOpenMenu = null;
119
+ function openMenuAt(anchor, actions) {
120
+ if (closeOpenMenu)
121
+ closeOpenMenu();
122
+ const menu = el("div", "rk-dir-menu");
123
+ actions.forEach((a) => {
124
+ const b = el("button", "rk-dir-menuitem" + (a.danger ? " is-danger" : ""), a.label);
125
+ b.type = "button";
126
+ b.addEventListener("click", (e) => { e.stopPropagation(); close(); a.run(); });
127
+ menu.appendChild(b);
128
+ });
129
+ document.body.appendChild(menu);
130
+ const r = anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor;
131
+ // Prefer below-right of the anchor; flip up/left if it would overflow.
132
+ const mw = menu.offsetWidth, mh = menu.offsetHeight;
133
+ let top = r.bottom + 4, left = r.right - mw;
134
+ if (left < 8)
135
+ left = r.left;
136
+ if (top + mh > window.innerHeight - 8)
137
+ top = Math.max(8, r.top - mh - 4);
138
+ menu.style.top = top + "px";
139
+ menu.style.left = Math.max(8, left) + "px";
140
+ function close() {
141
+ menu.remove();
142
+ document.removeEventListener("click", onDoc, true);
143
+ document.removeEventListener("keydown", onKey, true);
144
+ window.removeEventListener("scroll", close, true);
145
+ if (closeOpenMenu === close)
146
+ closeOpenMenu = null;
147
+ }
148
+ function onDoc(e) { if (!menu.contains(e.target))
149
+ close(); }
150
+ function onKey(e) { if (e.key === "Escape")
151
+ close(); }
152
+ setTimeout(() => {
153
+ document.addEventListener("click", onDoc, true);
154
+ document.addEventListener("keydown", onKey, true);
155
+ window.addEventListener("scroll", close, true);
156
+ }, 0);
157
+ closeOpenMenu = close;
158
+ }
159
+ // ── the explorer ─────────────────────────────────────────────────────────────
160
+ export function createDirectoryExplorer(mount, opts = {}) {
161
+ const apiBase = opts.apiBase || DEFAULT_API_BASE;
162
+ const roots = (opts.roots && opts.roots.length ? opts.roots : ["files", "temp", "raws"]);
163
+ const singleRoot = roots.length === 1 ? roots[0] : null;
164
+ const mode = opts.mode || "attach";
165
+ const showSearch = opts.showSearch !== false;
166
+ const showMultiSelect = opts.showMultiSelect !== false;
167
+ const showUpload = opts.showUpload != null ? opts.showUpload : mode === "attach";
168
+ const primaryClick = opts.primaryClick || "copyPath";
169
+ const host = window.__rkDirectoryHost || {};
170
+ const emitAttach = (items) => (opts.onAttach || host.onAttach || (() => showToast("Attached " + items.length + " item(s)")))(items);
171
+ const emitPreview = (items, i) => {
172
+ const cb = opts.onPreview || host.onPreview;
173
+ if (cb)
174
+ cb(items, i);
175
+ else if (items[i] && items[i].viewUrl)
176
+ window.open(items[i].viewUrl, "_blank", "noopener");
177
+ };
178
+ const toast = (msg) => (opts.onToast || host.onToast || showToast)(msg);
179
+ const copyPath = (p) => copyToClipboard(p).then(() => toast("Copied " + p)).catch(() => toast("Couldn't copy path"));
180
+ // ── state ──
181
+ let currentPath = normalizeInitial(opts.initialPath, singleRoot);
182
+ let status = "idle";
183
+ let folders = [];
184
+ let files = [];
185
+ let nextOffset = null;
186
+ let loadingMore = false;
187
+ const selection = new Map();
188
+ let searchMode = false;
189
+ let searchResults = [];
190
+ let searchStatus = "idle";
191
+ let destroyed = false;
192
+ function normalizeInitial(p, single) {
193
+ const parsed = parseDirectoryPath(p || "");
194
+ if (parsed.root)
195
+ return buildDirectoryPath(parsed.root, parsed.folderPath);
196
+ return single ? "/" + single : "/";
197
+ }
198
+ // ── scaffold ──
199
+ mount.classList.add("rk-dir");
200
+ mount.innerHTML = "";
201
+ const searchWrap = el("div", "rk-dir-search");
202
+ const searchInput = el("input", "rk-dir-search-input");
203
+ searchInput.type = "search";
204
+ searchInput.placeholder = "Search files — meaning, name, or path…";
205
+ searchWrap.appendChild(searchInput);
206
+ const crumbsEl = el("div", "rk-aichat-crumbs rk-dir-crumbs");
207
+ const selbar = el("div", "rk-dir-selbar");
208
+ selbar.hidden = true;
209
+ const bodyEl = el("div", "rk-aichat-files-body rk-dir-body");
210
+ const dropEl = el("label", "rk-aichat-drop rk-dir-drop");
211
+ dropEl.hidden = true;
212
+ const dropText = el("span", "rk-aichat-drop-text", "Drop files here");
213
+ const dropInput = el("input");
214
+ dropInput.type = "file";
215
+ dropInput.multiple = true;
216
+ dropInput.hidden = true;
217
+ dropEl.appendChild(dropText);
218
+ dropEl.appendChild(dropInput);
219
+ if (showSearch)
220
+ mount.appendChild(searchWrap);
221
+ mount.appendChild(crumbsEl);
222
+ if (showMultiSelect)
223
+ mount.appendChild(selbar);
224
+ mount.appendChild(bodyEl);
225
+ if (showUpload)
226
+ mount.appendChild(dropEl);
227
+ // ── fetch ──
228
+ function dirUrl(path, offset) {
229
+ const u = new URL(apiBase + "/directory", window.location.href);
230
+ u.searchParams.set("path", path);
231
+ if (offset)
232
+ u.searchParams.set("offset", String(offset));
233
+ return u.toString();
234
+ }
235
+ function load(force) {
236
+ if (destroyed)
237
+ return;
238
+ if (!force && status === "ready") {
239
+ render();
240
+ return;
241
+ }
242
+ status = "loading";
243
+ render();
244
+ fetch(dirUrl(currentPath), { credentials: "same-origin", headers: { accept: "application/json" } })
245
+ .then((r) => {
246
+ if (r.status === 401 || r.status === 403) {
247
+ status = "auth";
248
+ return null;
249
+ }
250
+ if (!r.ok)
251
+ throw new Error("http " + r.status);
252
+ return r.json();
253
+ })
254
+ .then((j) => {
255
+ if (destroyed)
256
+ return;
257
+ if (!j) {
258
+ render();
259
+ return;
260
+ }
261
+ folders = filterRoots(j.folders || []);
262
+ files = j.files || [];
263
+ nextOffset = typeof j.next_offset === "number" ? j.next_offset : null;
264
+ status = "ready";
265
+ render();
266
+ })
267
+ .catch(() => { if (!destroyed) {
268
+ status = "error";
269
+ render();
270
+ } });
271
+ }
272
+ // At the virtual root with a roots subset, hide roots we weren't asked to show.
273
+ function filterRoots(list) {
274
+ if (currentPath !== "/")
275
+ return list;
276
+ return list.filter((f) => roots.indexOf(f.root) !== -1);
277
+ }
278
+ function loadMore() {
279
+ if (nextOffset == null || loadingMore)
280
+ return;
281
+ loadingMore = true;
282
+ render();
283
+ fetch(dirUrl(currentPath, nextOffset), { credentials: "same-origin", headers: { accept: "application/json" } })
284
+ .then((r) => (r.ok ? r.json() : null))
285
+ .then((j) => {
286
+ loadingMore = false;
287
+ if (destroyed || !j) {
288
+ render();
289
+ return;
290
+ }
291
+ files = files.concat(j.files || []);
292
+ nextOffset = typeof j.next_offset === "number" ? j.next_offset : null;
293
+ render();
294
+ })
295
+ .catch(() => { loadingMore = false; render(); });
296
+ }
297
+ // ── search ──
298
+ let searchDebounce;
299
+ function onSearchInput() {
300
+ const q = searchInput.value.trim();
301
+ if (searchDebounce)
302
+ window.clearTimeout(searchDebounce);
303
+ if (!q) {
304
+ searchMode = false;
305
+ searchResults = [];
306
+ render();
307
+ return;
308
+ }
309
+ searchDebounce = window.setTimeout(() => runSearch(q), 260);
310
+ }
311
+ function runSearch(q) {
312
+ searchMode = true;
313
+ searchStatus = "loading";
314
+ render();
315
+ const scope = singleRoot ? "/" + singleRoot : (currentPath !== "/" ? currentPath : "");
316
+ fetch(apiBase + "/directory/search", {
317
+ method: "POST",
318
+ credentials: "same-origin",
319
+ headers: { "content-type": "application/json", accept: "application/json" },
320
+ body: JSON.stringify({ query: q, path: scope, mode: "auto", limit: 40 })
321
+ })
322
+ .then((r) => (r.ok ? r.json() : null))
323
+ .then((j) => {
324
+ if (destroyed)
325
+ return;
326
+ searchResults = (j && j.results) || [];
327
+ searchStatus = "ready";
328
+ render();
329
+ })
330
+ .catch(() => { if (!destroyed) {
331
+ searchStatus = "error";
332
+ render();
333
+ } });
334
+ }
335
+ // ── navigation ──
336
+ function navigate(path) {
337
+ const parsed = parseDirectoryPath(path);
338
+ currentPath = parsed.root ? buildDirectoryPath(parsed.root, parsed.folderPath) : "/";
339
+ status = "idle";
340
+ searchMode = false;
341
+ searchInput.value = "";
342
+ load(true);
343
+ }
344
+ function currentRoot() { return parseDirectoryPath(currentPath).root; }
345
+ function currentFolder() { return parseDirectoryPath(currentPath).folderPath; }
346
+ // Create an empty folder under `parentFolder` (defaults to the current folder).
347
+ // Only valid inside a root — at the virtual root there's nowhere to put it.
348
+ function createFolder(parentFolder) {
349
+ const root = currentRoot();
350
+ if (!root) {
351
+ toast("Open a root (/files, /temp, /raws) first");
352
+ return;
353
+ }
354
+ const name = (window.prompt("New folder name") || "").trim();
355
+ if (!name)
356
+ return;
357
+ const parent = normalizeFolderPath(parentFolder != null ? parentFolder : currentFolder());
358
+ const target = buildDirectoryPath(root, parent ? parent + "/" + name : name);
359
+ fetch(apiBase + "/directory/folders", {
360
+ method: "POST",
361
+ credentials: "same-origin",
362
+ headers: { "content-type": "application/json", accept: "application/json" },
363
+ body: JSON.stringify({ path: target })
364
+ })
365
+ .then((r) => (r.ok ? r.json() : r.json().then((j) => Promise.reject(new Error(j.error || "http " + r.status)))))
366
+ .then((res) => { navigate(res.path || (root === currentRoot() ? currentPath : "/" + root)); toast("Created " + (res.path || name)); })
367
+ .catch((e) => toast(e.message || "Couldn't create folder"));
368
+ }
369
+ // ── selection ──
370
+ function toggleSelect(it, on) {
371
+ if (on)
372
+ selection.set(it.path, it);
373
+ else
374
+ selection.delete(it.path);
375
+ (opts.onSelectionChange || (() => { }))(Array.from(selection.values()));
376
+ renderSelbar();
377
+ // reflect checkbox state without full re-render churn
378
+ const row = bodyEl.querySelector('[data-path="' + cssEscape(it.path) + '"]');
379
+ if (row)
380
+ row.classList.toggle("is-selected", on);
381
+ }
382
+ function clearSelection() {
383
+ selection.clear();
384
+ (opts.onSelectionChange || (() => { }))([]);
385
+ render();
386
+ }
387
+ // ── rendering ──
388
+ function crumb(label, current, onClick) {
389
+ const b = el("button", "rk-aichat-crumb" + (current ? " is-current" : ""), label);
390
+ b.type = "button";
391
+ b.addEventListener("click", onClick);
392
+ return b;
393
+ }
394
+ function renderCrumbs() {
395
+ crumbsEl.innerHTML = "";
396
+ const root = currentRoot();
397
+ const folder = currentFolder();
398
+ if (!singleRoot) {
399
+ crumbsEl.appendChild(crumb("Files", currentPath === "/", () => navigate("/")));
400
+ }
401
+ if (!root)
402
+ return;
403
+ const rootLabel = (DIRECTORY_ROOTS.find((r) => r.key === root) || { label: root }).label;
404
+ if (!singleRoot)
405
+ crumbsEl.appendChild(sep());
406
+ crumbsEl.appendChild(crumb(rootLabel, !folder, () => navigate("/" + root)));
407
+ let acc = "";
408
+ folder.split("/").filter(Boolean).forEach((part, idx, arr) => {
409
+ crumbsEl.appendChild(sep());
410
+ acc = acc ? acc + "/" + part : part;
411
+ const target = "/" + root + "/" + acc;
412
+ crumbsEl.appendChild(crumb(part, idx === arr.length - 1, () => navigate(target)));
413
+ });
414
+ // Discoverable "New folder" affordance (right-click also works anywhere).
415
+ const nf = el("button", "rk-dir-newfolder", "+ Folder");
416
+ nf.type = "button";
417
+ nf.title = "New folder here (or right-click)";
418
+ nf.addEventListener("click", () => createFolder());
419
+ crumbsEl.appendChild(nf);
420
+ }
421
+ function sep() { return el("span", "rk-aichat-crumb-sep", "/"); }
422
+ function renderSelbar() {
423
+ if (!showMultiSelect)
424
+ return;
425
+ const n = selection.size;
426
+ selbar.hidden = n === 0;
427
+ if (n === 0)
428
+ return;
429
+ selbar.innerHTML = "";
430
+ selbar.appendChild(el("span", "rk-dir-selcount", n + " selected"));
431
+ if (mode === "attach") {
432
+ const att = el("button", "rk-dir-selbtn", "Attach");
433
+ att.type = "button";
434
+ att.addEventListener("click", () => { emitAttach(Array.from(selection.values())); clearSelection(); });
435
+ selbar.appendChild(att);
436
+ }
437
+ const cp = el("button", "rk-dir-selbtn", "Copy paths");
438
+ cp.type = "button";
439
+ cp.addEventListener("click", () => copyPath(Array.from(selection.values()).map((s) => s.path).join("\n")));
440
+ const clr = el("button", "rk-dir-selbtn is-ghost", "Clear");
441
+ clr.type = "button";
442
+ clr.addEventListener("click", clearSelection);
443
+ selbar.appendChild(cp);
444
+ selbar.appendChild(clr);
445
+ }
446
+ function checkbox(it) {
447
+ const box = el("input", "rk-dir-check");
448
+ box.type = "checkbox";
449
+ box.checked = selection.has(it.path);
450
+ box.setAttribute("aria-label", "Select " + it.name);
451
+ box.addEventListener("click", (e) => e.stopPropagation());
452
+ box.addEventListener("change", () => toggleSelect(it, box.checked));
453
+ return box;
454
+ }
455
+ function folderRowEl(it) {
456
+ const row = el("div", "rk-aichat-frow rk-dir-row" + (selection.has(it.path) ? " is-selected" : ""));
457
+ row.setAttribute("data-path", it.path);
458
+ if (showMultiSelect)
459
+ row.appendChild(checkbox(it));
460
+ // Name button: default click COPIES the (relative) folder path.
461
+ const b = el("button", "rk-aichat-fmainbtn");
462
+ b.type = "button";
463
+ b.title = "Copy path " + it.path;
464
+ b.appendChild(iconFor(it));
465
+ const main = el("span", "rk-aichat-fmain");
466
+ main.appendChild(el("span", "rk-aichat-fname", it.name));
467
+ const desc = it.meta && typeof it.meta.description === "string" ? it.meta.description : "";
468
+ main.appendChild(el("span", "rk-aichat-fmeta", desc || "folder"));
469
+ b.appendChild(main);
470
+ b.addEventListener("click", () => copyPath(it.path));
471
+ row.appendChild(b);
472
+ // Open button (the primary navigation affordance).
473
+ const open = el("button", "rk-aichat-fadd rk-dir-open", "Open ›");
474
+ open.type = "button";
475
+ open.title = "Open folder";
476
+ open.addEventListener("click", (e) => { e.stopPropagation(); navigate(it.path); });
477
+ row.appendChild(open);
478
+ // Kebab.
479
+ row.appendChild(kebab(it, [
480
+ { label: "Open folder", run: () => navigate(it.path) },
481
+ { label: "New subfolder", run: () => createFolder(it.folderPath) },
482
+ { label: "Copy path", run: () => copyPath(it.path) }
483
+ ]));
484
+ return row;
485
+ }
486
+ function fileRowEl(it, siblings, index, showPath) {
487
+ const row = el("div", "rk-aichat-frow rk-dir-row" + (selection.has(it.path) ? " is-selected" : ""));
488
+ row.setAttribute("data-path", it.path);
489
+ if (showMultiSelect)
490
+ row.appendChild(checkbox(it));
491
+ const b = el("button", "rk-aichat-fmainbtn");
492
+ b.type = "button";
493
+ const primaryLabel = primaryClick === "copyPath" ? "Copy path " : primaryClick === "attach" ? "Attach " : "Preview ";
494
+ b.title = primaryLabel + it.name;
495
+ b.appendChild(iconFor(it));
496
+ const main = el("span", "rk-aichat-fmain");
497
+ main.appendChild(el("span", "rk-aichat-fname", it.name));
498
+ const metaText = showPath ? it.path : (fmtDur(it.durationSec) || fmtBytes(it.sizeBytes) || it.contentType || "file");
499
+ main.appendChild(el("span", "rk-aichat-fmeta", metaText));
500
+ b.appendChild(main);
501
+ b.addEventListener("click", () => {
502
+ if (primaryClick === "attach")
503
+ emitAttach([it]);
504
+ else if (primaryClick === "preview")
505
+ emitPreview(siblings, index);
506
+ else
507
+ copyPath(it.path);
508
+ });
509
+ row.appendChild(b);
510
+ // Kebab: Attach / View / Preview / Copy path.
511
+ const actions = [];
512
+ if (mode === "attach")
513
+ actions.push({ label: "Attach", run: () => emitAttach([it]) });
514
+ if (it.viewUrl)
515
+ actions.push({ label: "View", run: () => window.open(it.viewUrl, "_blank", "noopener") });
516
+ if (it.viewUrl)
517
+ actions.push({ label: "Preview", run: () => emitPreview(siblings, index) });
518
+ actions.push({ label: "Copy path", run: () => copyPath(it.path) });
519
+ row.appendChild(kebab(it, actions));
520
+ return row;
521
+ }
522
+ function kebab(it, actions) {
523
+ void it;
524
+ const k = el("button", "rk-dir-kebab", "⋯");
525
+ k.type = "button";
526
+ k.setAttribute("aria-label", "More actions");
527
+ k.addEventListener("click", (e) => { e.stopPropagation(); openMenuAt(k, actions); });
528
+ return k;
529
+ }
530
+ function note(text, link, retry) {
531
+ const wrap = el("div", "rk-aichat-fstate");
532
+ wrap.appendChild(el("div", undefined, text));
533
+ if (link) {
534
+ const a = el("a");
535
+ a.href = link.href;
536
+ a.textContent = link.label;
537
+ wrap.appendChild(a);
538
+ }
539
+ if (retry) {
540
+ const btn = el("button", undefined, "Try again");
541
+ btn.type = "button";
542
+ btn.addEventListener("click", () => load(true));
543
+ wrap.appendChild(btn);
544
+ }
545
+ return wrap;
546
+ }
547
+ function render() {
548
+ if (destroyed)
549
+ return;
550
+ renderCrumbs();
551
+ renderSelbar();
552
+ updateDrop();
553
+ bodyEl.innerHTML = "";
554
+ if (searchMode) {
555
+ renderSearch();
556
+ return;
557
+ }
558
+ if (status === "loading" && !files.length && !folders.length) {
559
+ bodyEl.appendChild(note("Loading your files…"));
560
+ return;
561
+ }
562
+ if (status === "auth") {
563
+ bodyEl.appendChild(note("Sign in to browse your files.", { href: "/login", label: "Sign in" }));
564
+ return;
565
+ }
566
+ if (status === "error") {
567
+ bodyEl.appendChild(note("Couldn't reach your files.", undefined, true));
568
+ return;
569
+ }
570
+ if (!folders.length && !files.length) {
571
+ bodyEl.appendChild(note(currentPath === "/" ? "No files yet." : "This folder is empty."));
572
+ return;
573
+ }
574
+ folders.forEach((f) => bodyEl.appendChild(folderRowEl(f)));
575
+ files.forEach((f, i) => bodyEl.appendChild(fileRowEl(f, files, i)));
576
+ if (nextOffset != null) {
577
+ const more = el("button", "rk-aichat-loadmore", loadingMore ? "Loading…" : "Load more");
578
+ more.type = "button";
579
+ more.disabled = loadingMore;
580
+ more.addEventListener("click", loadMore);
581
+ bodyEl.appendChild(more);
582
+ }
583
+ }
584
+ function renderSearch() {
585
+ if (searchStatus === "loading" && !searchResults.length) {
586
+ bodyEl.appendChild(note("Searching…"));
587
+ return;
588
+ }
589
+ if (searchStatus === "error") {
590
+ bodyEl.appendChild(note("Search failed."));
591
+ return;
592
+ }
593
+ if (!searchResults.length) {
594
+ bodyEl.appendChild(note("No matches."));
595
+ return;
596
+ }
597
+ const fileHits = searchResults.filter((r) => r.kind !== "folder");
598
+ searchResults.forEach((r, i) => {
599
+ if (r.kind === "folder")
600
+ bodyEl.appendChild(folderRowEl(r));
601
+ else
602
+ bodyEl.appendChild(fileRowEl(r, fileHits, fileHits.indexOf(r), true));
603
+ });
604
+ }
605
+ // ── upload (attach hosts only; not for raws) ──
606
+ let uploading = false;
607
+ function uploadEndpoint() {
608
+ const root = currentRoot();
609
+ if (root === "files")
610
+ return apiBase + "/attachments/upload";
611
+ if (root === "temp")
612
+ return apiBase + "/temporary-files/upload";
613
+ return null; // raws are scanned/imported, not plain-uploaded
614
+ }
615
+ function updateDrop() {
616
+ if (!showUpload)
617
+ return;
618
+ const endpoint = uploadEndpoint();
619
+ dropEl.hidden = !endpoint || status === "auth";
620
+ if (!dropEl.hidden && !uploading) {
621
+ const folder = currentFolder();
622
+ const rootLabel = currentRoot() ? (DIRECTORY_ROOTS.find((r) => r.key === currentRoot()) || { label: "" }).label : "";
623
+ dropText.textContent = "Drop files into " + rootLabel + (folder ? " / " + folder : "");
624
+ }
625
+ }
626
+ function uploadFiles(list) {
627
+ const endpoint = uploadEndpoint();
628
+ if (!endpoint || uploading || !list || !list.length)
629
+ return;
630
+ uploading = true;
631
+ const folder = currentFolder();
632
+ const arr = Array.from(list);
633
+ let done = 0, failed = 0;
634
+ dropText.textContent = "Uploading " + arr.length + " file(s)…";
635
+ const next = (i) => {
636
+ if (i >= arr.length) {
637
+ uploading = false;
638
+ if (failed)
639
+ toast(failed + " of " + arr.length + " failed");
640
+ load(true);
641
+ return;
642
+ }
643
+ const fd = new FormData();
644
+ fd.append("file", arr[i]);
645
+ fd.append("folder_path", folder);
646
+ fetch(endpoint, { method: "POST", credentials: "same-origin", body: fd })
647
+ .then((r) => { if (!r.ok)
648
+ throw new Error("http " + r.status); done++; })
649
+ .catch(() => { failed++; })
650
+ .then(() => next(i + 1));
651
+ };
652
+ next(0);
653
+ }
654
+ // ── right-click context menu: New folder anywhere, plus row actions ──
655
+ function findItem(path) {
656
+ return folders.concat(files, searchResults).find((it) => it.path === path);
657
+ }
658
+ bodyEl.addEventListener("contextmenu", (e) => {
659
+ const rootSet = !!currentRoot();
660
+ const rowEl = e.target.closest(".rk-dir-row");
661
+ const at = { top: e.clientY, bottom: e.clientY, left: e.clientX, right: e.clientX };
662
+ const actions = [];
663
+ const it = rowEl ? findItem(rowEl.getAttribute("data-path") || "") : undefined;
664
+ if (it && it.kind === "folder") {
665
+ actions.push({ label: "Open folder", run: () => navigate(it.path) });
666
+ if (rootSet)
667
+ actions.push({ label: "New subfolder", run: () => createFolder(it.folderPath) });
668
+ actions.push({ label: "Copy path", run: () => copyPath(it.path) });
669
+ }
670
+ else if (it) {
671
+ if (mode === "attach")
672
+ actions.push({ label: "Attach", run: () => emitAttach([it]) });
673
+ if (it.viewUrl)
674
+ actions.push({ label: "View", run: () => window.open(it.viewUrl, "_blank", "noopener") });
675
+ if (it.viewUrl)
676
+ actions.push({ label: "Preview", run: () => emitPreview(files, files.indexOf(it)) });
677
+ actions.push({ label: "Copy path", run: () => copyPath(it.path) });
678
+ }
679
+ // Always offer "New folder" in the current folder when inside a root.
680
+ if (rootSet)
681
+ actions.push({ label: "New folder here", run: () => createFolder() });
682
+ if (!actions.length)
683
+ return; // virtual root, empty space → let the browser menu show
684
+ e.preventDefault();
685
+ openMenuAt(at, actions);
686
+ });
687
+ // ── wire events ──
688
+ searchInput.addEventListener("input", onSearchInput);
689
+ dropEl.addEventListener("click", (e) => { if (e.target === dropEl || e.target === dropText)
690
+ dropInput.click(); });
691
+ dropInput.addEventListener("change", () => { uploadFiles(dropInput.files); dropInput.value = ""; });
692
+ dropEl.addEventListener("dragover", (e) => { e.preventDefault(); dropEl.classList.add("is-drag"); });
693
+ dropEl.addEventListener("dragleave", (e) => { if (e.target === dropEl)
694
+ dropEl.classList.remove("is-drag"); });
695
+ dropEl.addEventListener("drop", (e) => {
696
+ e.preventDefault();
697
+ dropEl.classList.remove("is-drag");
698
+ if (e.dataTransfer && e.dataTransfer.files)
699
+ uploadFiles(e.dataTransfer.files);
700
+ });
701
+ load(true);
702
+ return {
703
+ refresh: () => load(true),
704
+ navigate,
705
+ getSelection: () => Array.from(selection.values()),
706
+ destroy: () => { destroyed = true; if (closeOpenMenu)
707
+ closeOpenMenu(); mount.innerHTML = ""; }
708
+ };
709
+ }
710
+ function cssEscape(s) {
711
+ if (window.CSS && window.CSS.escape)
712
+ return window.CSS.escape(s);
713
+ return s.replace(/["\\]/g, "\\$&");
714
+ }
715
+ // ── auto-mount + global bridge ───────────────────────────────────────────────
716
+ function readDataOpts(node) {
717
+ const d = node.dataset;
718
+ const rootsAttr = (d.roots || "").split(",").map((s) => s.trim()).filter(Boolean);
719
+ return {
720
+ roots: rootsAttr.length ? rootsAttr : undefined,
721
+ initialPath: d.initialPath || undefined,
722
+ mode: d.mode === "standalone" ? "standalone" : "attach",
723
+ showSearch: d.showSearch !== "false",
724
+ showMultiSelect: d.showMultiselect !== "false",
725
+ showUpload: d.showUpload != null ? d.showUpload === "true" : undefined,
726
+ primaryClick: d.primaryClick || "copyPath"
727
+ };
728
+ }
729
+ function autoMount() {
730
+ const nodes = Array.prototype.slice.call(document.querySelectorAll("[data-rk-directory]"));
731
+ nodes.forEach((node) => {
732
+ if (node.getAttribute("data-rk-dir-mounted") === "1")
733
+ return;
734
+ node.setAttribute("data-rk-dir-mounted", "1");
735
+ window.__rkDirectoryInstance = createDirectoryExplorer(node, readDataOpts(node));
736
+ });
737
+ }
738
+ window.__vidfarmDirectory = { mount: createDirectoryExplorer };
739
+ if (document.readyState === "loading")
740
+ document.addEventListener("DOMContentLoaded", autoMount);
741
+ else
742
+ autoMount();
743
+ window.dispatchEvent(new CustomEvent("vidfarm:directory-ready"));
744
+ //# sourceMappingURL=file-directory.js.map