@mevdragon/vidfarm-devcli 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/vidfarm-media/SKILL.md +3 -3
- package/SKILL.director.md +20 -20
- package/SKILL.platform.md +4 -4
- package/dist/src/app.js +301 -45
- package/dist/src/cli.js +11 -10
- package/dist/src/devcli/clips.js +64 -64
- package/dist/src/reskin/agency-page.js +214 -170
- package/dist/src/reskin/calendar-page.js +502 -187
- package/dist/src/reskin/chat-page.js +470 -277
- package/dist/src/reskin/discover-page.js +988 -272
- package/dist/src/reskin/document.js +602 -13
- package/dist/src/reskin/help-page.js +138 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +541 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +435 -262
- package/dist/src/reskin/login-page.js +118 -81
- package/dist/src/reskin/pricing-page.js +195 -166
- package/dist/src/reskin/settings-page.js +520 -186
- package/dist/src/reskin/theme.js +106 -9
- package/package.json +1 -1
|
@@ -22,38 +22,627 @@ export function rkEscape(value) {
|
|
|
22
22
|
.replace(/"/g, """)
|
|
23
23
|
.replace(/'/g, "'");
|
|
24
24
|
}
|
|
25
|
+
/** The sidebar shows only the core app pages (not every reskinned page).
|
|
26
|
+
* Other pages stay reachable from the /reskin gallery (brand link). */
|
|
27
|
+
const SIDEBAR_NAV = [
|
|
28
|
+
{ slug: "discover", label: "Discover" },
|
|
29
|
+
{ slug: "chat", label: "Chat" },
|
|
30
|
+
{ slug: "library", label: "Library" },
|
|
31
|
+
{ slug: "calendar", label: "Calendar" },
|
|
32
|
+
{ slug: "help", label: "Guides" },
|
|
33
|
+
{ slug: "settings", label: "Settings" }
|
|
34
|
+
];
|
|
25
35
|
function renderSidebar(activeSlug) {
|
|
26
|
-
const items =
|
|
36
|
+
const items = SIDEBAR_NAV.map((p) => {
|
|
27
37
|
const active = p.slug === activeSlug ? " is-active" : "";
|
|
28
|
-
return `<a class="rk-sidebar-item${active}" href="
|
|
38
|
+
return `<a class="rk-sidebar-item${active}" href="/${p.slug}"${p.slug === activeSlug ? ` aria-current="page"` : ""}>${p.label}</a>`;
|
|
29
39
|
}).join("");
|
|
30
|
-
const live = RESKIN_PAGES.find((p) => p.slug === activeSlug)?.live ?? "/";
|
|
31
|
-
const homeActive = activeSlug === null ? " is-active" : "";
|
|
32
40
|
return `<aside class="rk-sidebar">
|
|
33
|
-
<a class="rk-sidebar-brand" href="/
|
|
41
|
+
<a class="rk-sidebar-brand" href="/discover"><span class="rk-brand-mark">V</span>vidfarm</a>
|
|
34
42
|
<nav class="rk-sidebar-nav" aria-label="Reskin pages">
|
|
35
|
-
<a class="rk-sidebar-item${homeActive}" href="/reskin">Gallery</a>
|
|
36
|
-
<div class="rk-sidebar-sep"></div>
|
|
37
43
|
${items}
|
|
38
44
|
</nav>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
</aside>`;
|
|
46
|
+
}
|
|
47
|
+
/** Draggable chat FAB (defaults to the old sidebar-foot spot, bottom-left) +
|
|
48
|
+
* the popup AI chat panel it toggles. Sample chat only — this is the design
|
|
49
|
+
* sandbox, so the panel demos the UI rather than hitting a live agent. The
|
|
50
|
+
* old "compare with live page" link survives as the ↗ tool in the panel head. */
|
|
51
|
+
function renderChatDock(activeSlug) {
|
|
52
|
+
const page = RESKIN_PAGES.find((p) => p.slug === activeSlug);
|
|
53
|
+
const label = rkEscape(page?.label ?? "vidfarm");
|
|
54
|
+
return `<button class="rk-fab" id="rkFab" type="button" aria-label="Open AI chat" aria-expanded="false" aria-controls="rkAichat" title="Chat with vidfarm AI — drag to move">
|
|
55
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
|
|
56
|
+
</button>
|
|
57
|
+
<aside class="rk-aichat has-files" id="rkAichat" hidden aria-label="AI chat panel">
|
|
58
|
+
<div class="rk-aichat-files" id="rkAichatFiles" aria-label="Files">
|
|
59
|
+
<div class="rk-aichat-crumbs" id="rkAichatCrumbs" aria-label="Current folder"></div>
|
|
60
|
+
<div class="rk-aichat-files-body" id="rkAichatFilesBody"></div>
|
|
61
|
+
<div class="rk-aichat-drop" id="rkAichatDrop" hidden>
|
|
62
|
+
<input type="file" id="rkAichatUploadInput" multiple hidden>
|
|
63
|
+
<div class="rk-aichat-drop-text" id="rkAichatDropText">Drop files to upload</div>
|
|
64
|
+
<button type="button" class="rk-aichat-drop-btn" id="rkAichatUploadBtn">Upload files</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="rk-aichat-main">
|
|
68
|
+
<header class="rk-aichat-head">
|
|
69
|
+
<span class="rk-brand-mark">V</span>
|
|
70
|
+
<div class="rk-aichat-head-main">
|
|
71
|
+
<div class="rk-aichat-title">Vidfarm AI</div>
|
|
72
|
+
<div class="rk-aichat-sub">sample chat</div>
|
|
73
|
+
</div>
|
|
74
|
+
<button class="rk-aichat-tool is-on" id="rkFilesToggle" type="button" title="Toggle My Files" aria-label="Toggle My Files" aria-pressed="true">
|
|
75
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
|
76
|
+
</button>
|
|
77
|
+
<button class="rk-aichat-tool" id="rkAichatMin" type="button" title="Minimize" aria-label="Minimize chat">—</button>
|
|
78
|
+
</header>
|
|
79
|
+
<div class="rk-aichat-log" id="rkAichatLog">
|
|
80
|
+
<div class="rk-aichat-msg is-ai">Hi! I'm the vidfarm assistant. Ask me anything about the ${label} page — or attach a file from the Files panel on the left.</div>
|
|
81
|
+
<div class="rk-aichat-msg is-user">What changed in this reskin?</div>
|
|
82
|
+
<div class="rk-aichat-msg is-ai">This page was ported into the farmville design system — honey-gold accent, soft rounded cards, the shared rk- primitives — while keeping the layout one-to-one with the live page.</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="rk-aichat-chips" id="rkAichatChips"></div>
|
|
85
|
+
<form class="rk-aichat-composer" id="rkAichatForm">
|
|
86
|
+
<input class="rk-input" id="rkAichatInput" placeholder="Ask the vidfarm AI…" autocomplete="off">
|
|
87
|
+
<button class="rk-btn rk-btn-gold rk-btn-sm" type="submit">Send</button>
|
|
88
|
+
</form>
|
|
42
89
|
</div>
|
|
43
90
|
</aside>`;
|
|
44
91
|
}
|
|
92
|
+
/** Chrome behavior: FAB drag (click-vs-drag threshold, viewport clamp, position
|
|
93
|
+
* persisted in localStorage) + panel toggle. The panel anchors to whichever
|
|
94
|
+
* half of the viewport the FAB sits in so it is never cut off; when it opens
|
|
95
|
+
* on the left it replaces the sidebar nav (body.rk-chat-left). Open state
|
|
96
|
+
* survives page navigation via sessionStorage. Plain IIFE — no backticks or
|
|
97
|
+
* dollar-brace inside (it is embedded in a template literal). */
|
|
98
|
+
const RESKIN_CHROME_SCRIPT = `(() => {
|
|
99
|
+
var fab = document.getElementById('rkFab');
|
|
100
|
+
var panel = document.getElementById('rkAichat');
|
|
101
|
+
if (!fab || !panel) return;
|
|
102
|
+
var minBtn = document.getElementById('rkAichatMin');
|
|
103
|
+
var form = document.getElementById('rkAichatForm');
|
|
104
|
+
var input = document.getElementById('rkAichatInput');
|
|
105
|
+
var log = document.getElementById('rkAichatLog');
|
|
106
|
+
var MARGIN = 10;
|
|
107
|
+
|
|
108
|
+
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
109
|
+
|
|
110
|
+
// offsetWidth/offsetLeft (not getBoundingClientRect) so the hover/drag/open
|
|
111
|
+
// transforms never skew the clamp math
|
|
112
|
+
function placeFab(x, y) {
|
|
113
|
+
x = clamp(x, MARGIN, window.innerWidth - fab.offsetWidth - MARGIN);
|
|
114
|
+
y = clamp(y, MARGIN, window.innerHeight - fab.offsetHeight - MARGIN);
|
|
115
|
+
fab.style.left = x + 'px';
|
|
116
|
+
fab.style.top = y + 'px';
|
|
117
|
+
fab.style.bottom = 'auto';
|
|
118
|
+
return { x: x, y: y };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
var saved = JSON.parse(localStorage.getItem('rk-fab-pos') || 'null');
|
|
123
|
+
if (saved && typeof saved.x === 'number') placeFab(saved.x, saved.y);
|
|
124
|
+
} catch (e) {}
|
|
125
|
+
|
|
126
|
+
function anchorPanel() {
|
|
127
|
+
var r = fab.getBoundingClientRect();
|
|
128
|
+
var onRight = r.left + r.width / 2 > window.innerWidth / 2;
|
|
129
|
+
panel.classList.toggle('is-right', onRight);
|
|
130
|
+
document.body.classList.toggle('rk-chat-left', !panel.hidden && !onRight);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function openPanel() {
|
|
134
|
+
panel.hidden = false;
|
|
135
|
+
fab.classList.add('is-open');
|
|
136
|
+
fab.setAttribute('aria-expanded', 'true');
|
|
137
|
+
anchorPanel();
|
|
138
|
+
try { sessionStorage.setItem('rk-chat-open', '1'); } catch (e) {}
|
|
139
|
+
refreshFilesOnOpen();
|
|
140
|
+
if (input) input.focus();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function closePanel() {
|
|
144
|
+
panel.hidden = true;
|
|
145
|
+
fab.classList.remove('is-open');
|
|
146
|
+
fab.setAttribute('aria-expanded', 'false');
|
|
147
|
+
document.body.classList.remove('rk-chat-left');
|
|
148
|
+
try { sessionStorage.removeItem('rk-chat-open'); } catch (e) {}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var drag = null;
|
|
152
|
+
fab.addEventListener('pointerdown', function (e) {
|
|
153
|
+
drag = { sx: e.clientX, sy: e.clientY, ox: fab.offsetLeft, oy: fab.offsetTop, moved: false, last: null };
|
|
154
|
+
fab.setPointerCapture(e.pointerId);
|
|
155
|
+
});
|
|
156
|
+
fab.addEventListener('pointermove', function (e) {
|
|
157
|
+
if (!drag) return;
|
|
158
|
+
var dx = e.clientX - drag.sx;
|
|
159
|
+
var dy = e.clientY - drag.sy;
|
|
160
|
+
if (!drag.moved && Math.hypot(dx, dy) < 6) return;
|
|
161
|
+
drag.moved = true;
|
|
162
|
+
fab.classList.add('is-dragging');
|
|
163
|
+
drag.last = placeFab(drag.ox + dx, drag.oy + dy);
|
|
164
|
+
});
|
|
165
|
+
fab.addEventListener('pointerup', function () {
|
|
166
|
+
if (!drag) return;
|
|
167
|
+
var moved = drag.moved;
|
|
168
|
+
if (drag.last) {
|
|
169
|
+
try { localStorage.setItem('rk-fab-pos', JSON.stringify(drag.last)); } catch (e) {}
|
|
170
|
+
}
|
|
171
|
+
fab.classList.remove('is-dragging');
|
|
172
|
+
drag = null;
|
|
173
|
+
if (!moved) openPanel();
|
|
174
|
+
});
|
|
175
|
+
fab.addEventListener('pointercancel', function () {
|
|
176
|
+
fab.classList.remove('is-dragging');
|
|
177
|
+
drag = null;
|
|
178
|
+
});
|
|
179
|
+
fab.addEventListener('click', function (e) {
|
|
180
|
+
if (e.detail === 0 && panel.hidden) openPanel();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (minBtn) minBtn.addEventListener('click', closePanel);
|
|
184
|
+
document.addEventListener('keydown', function (e) {
|
|
185
|
+
if (e.key === 'Escape' && !panel.hidden) closePanel();
|
|
186
|
+
});
|
|
187
|
+
window.addEventListener('resize', function () {
|
|
188
|
+
if (fab.style.top) {
|
|
189
|
+
var pos = placeFab(fab.offsetLeft, fab.offsetTop);
|
|
190
|
+
try { localStorage.setItem('rk-fab-pos', JSON.stringify(pos)); } catch (e) {}
|
|
191
|
+
}
|
|
192
|
+
if (!panel.hidden) anchorPanel();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
function bubble(kind, text) {
|
|
196
|
+
var el = document.createElement('div');
|
|
197
|
+
el.className = 'rk-aichat-msg ' + kind;
|
|
198
|
+
el.textContent = text;
|
|
199
|
+
log.appendChild(el);
|
|
200
|
+
log.scrollTop = log.scrollHeight;
|
|
201
|
+
}
|
|
202
|
+
if (form) form.addEventListener('submit', function (e) {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
var v = (input.value || '').trim();
|
|
205
|
+
var picked = selected.slice();
|
|
206
|
+
if (!v && !picked.length) return;
|
|
207
|
+
var line = v;
|
|
208
|
+
if (picked.length) {
|
|
209
|
+
var names = picked.map(function (f) { return f.name; }).join(', ');
|
|
210
|
+
line = (v ? v + ' ' : '') + '(attached: ' + names + ')';
|
|
211
|
+
}
|
|
212
|
+
bubble('is-user', line);
|
|
213
|
+
input.value = '';
|
|
214
|
+
clearChips();
|
|
215
|
+
setTimeout(function () {
|
|
216
|
+
bubble('is-ai', picked.length
|
|
217
|
+
? 'Got the ' + picked.length + ' file' + (picked.length > 1 ? 's' : '') + ' from your ' + rootLabel(fstate.root) + ' — on the live app I would use ' + (picked.length > 1 ? 'them' : 'it') + ' in the conversation. This reskin panel is a sample, so replies are canned.'
|
|
218
|
+
: "I'm the sample chat in the reskin sandbox, so replies here are canned — on the live app this panel wires up to the vidfarm agent.");
|
|
219
|
+
}, 450);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ─── My Files drawer: real directory navigation over my_files/ + temp/ ───
|
|
223
|
+
var filesPane = document.getElementById('rkAichatFiles');
|
|
224
|
+
var crumbsEl = document.getElementById('rkAichatCrumbs');
|
|
225
|
+
var filesBody = document.getElementById('rkAichatFilesBody');
|
|
226
|
+
var chipsEl = document.getElementById('rkAichatChips');
|
|
227
|
+
var filesToggle = document.getElementById('rkFilesToggle');
|
|
228
|
+
var dropEl = document.getElementById('rkAichatDrop');
|
|
229
|
+
var dropText = document.getElementById('rkAichatDropText');
|
|
230
|
+
var uploadInput = document.getElementById('rkAichatUploadInput');
|
|
231
|
+
var uploadBtn = document.getElementById('rkAichatUploadBtn');
|
|
232
|
+
var selected = [];
|
|
233
|
+
var uploading = false, uploadMsg = '';
|
|
234
|
+
|
|
235
|
+
var ROOTS = {
|
|
236
|
+
my_files: { url: '/api/v1/user/me/attachments', key: 'attachments', label: 'My Files' },
|
|
237
|
+
temp: { url: '/api/v1/user/me/temporary-files', key: 'files', label: 'Temp' },
|
|
238
|
+
// Raws = the reusable clip/source library (renamed clips→raws). Flat list
|
|
239
|
+
// (no folders); items have a clip shape, so they get their own normalizer.
|
|
240
|
+
raws: { url: '/raws/feed', key: 'clips', label: 'Raws', norm: normRaw }
|
|
241
|
+
};
|
|
242
|
+
// The sources are surfaced as top-level FOLDERS of one virtual root (no source
|
|
243
|
+
// tabs) — navigate into them natively like any folder.
|
|
244
|
+
var VROOTS = [{ key: 'my_files', label: 'My Files' }, { key: 'temp', label: 'Temp' }, { key: 'raws', label: 'Raws' }];
|
|
245
|
+
var fstate = {
|
|
246
|
+
root: '', // '' = virtual root (lists My Files + Temp + Raws); else a ROOTS key
|
|
247
|
+
path: { my_files: '', temp: '', raws: '' },
|
|
248
|
+
data: { my_files: null, temp: null, raws: null },
|
|
249
|
+
status: { my_files: 'idle', temp: 'idle', raws: 'idle' } // idle|loading|ready|auth|error
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
function rootLabel(root) { return ROOTS[root] ? ROOTS[root].label : 'Files'; }
|
|
253
|
+
function normFolder(v) {
|
|
254
|
+
return String(v || '').split('/').map(function (s) { return s.trim(); }).filter(Boolean).join('/');
|
|
255
|
+
}
|
|
256
|
+
function normItem(raw) {
|
|
257
|
+
var id = raw.id || raw.attachment_id || raw.file_id || '';
|
|
258
|
+
var name = raw.fileName || raw.file_name || '';
|
|
259
|
+
if (!id || !name) return null;
|
|
260
|
+
return {
|
|
261
|
+
id: id, name: name,
|
|
262
|
+
contentType: raw.contentType || raw.content_type || '',
|
|
263
|
+
viewUrl: raw.viewUrl || raw.view_url || '',
|
|
264
|
+
sizeBytes: typeof raw.sizeBytes === 'number' ? raw.sizeBytes : raw.size_bytes,
|
|
265
|
+
folderPath: normFolder(raw.folderPath || raw.folder_path || '')
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function fmtBytes(b) {
|
|
269
|
+
if (!b || b <= 0) return '';
|
|
270
|
+
var u = ['B', 'KB', 'MB', 'GB'], v = b, i = 0;
|
|
271
|
+
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
|
|
272
|
+
return (v >= 10 || i === 0 ? Math.round(v) : v.toFixed(1)) + ' ' + u[i];
|
|
273
|
+
}
|
|
274
|
+
function fmtDur(s) {
|
|
275
|
+
if (typeof s !== 'number' || !(s > 0)) return '';
|
|
276
|
+
s = Math.round(s); var m = Math.floor(s / 60), r = s % 60;
|
|
277
|
+
return m + ':' + (r < 10 ? '0' + r : r);
|
|
278
|
+
}
|
|
279
|
+
// Raws feed items are clips (clip_id / source_filename / view_url / duration),
|
|
280
|
+
// a different shape than attachments — map them onto the file-row model.
|
|
281
|
+
function normRaw(raw) {
|
|
282
|
+
var id = raw.clip_id || raw.id || '';
|
|
283
|
+
if (!id) return null;
|
|
284
|
+
return {
|
|
285
|
+
id: id,
|
|
286
|
+
name: raw.source_filename || raw.description || 'clip',
|
|
287
|
+
contentType: 'video/mp4',
|
|
288
|
+
viewUrl: raw.view_url || raw.viewUrl || '',
|
|
289
|
+
thumbUrl: raw.thumbnail_url || raw.thumbnailUrl || '',
|
|
290
|
+
metaText: fmtDur(raw.duration_sec),
|
|
291
|
+
sizeBytes: undefined,
|
|
292
|
+
folderPath: ''
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function immediateFolders(data, current) {
|
|
296
|
+
var cur = normFolder(current);
|
|
297
|
+
var prefix = cur ? cur + '/' : '';
|
|
298
|
+
var names = {};
|
|
299
|
+
function consider(fp) {
|
|
300
|
+
var n = normFolder(fp);
|
|
301
|
+
if (!n || (cur && n === cur) || n.indexOf(prefix) !== 0) return;
|
|
302
|
+
var child = n.slice(prefix.length).split('/')[0];
|
|
303
|
+
if (child) names[child] = prefix + child;
|
|
304
|
+
}
|
|
305
|
+
(data.folders || []).forEach(consider);
|
|
306
|
+
(data.items || []).forEach(function (it) { consider(it.folderPath); });
|
|
307
|
+
return Object.keys(names).sort().map(function (n) { return { name: n, path: names[n] }; });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function loadRoot(root, force) {
|
|
311
|
+
if (!ROOTS[root]) return;
|
|
312
|
+
if (!force && fstate.status[root] === 'ready') { renderFiles(); return; }
|
|
313
|
+
fstate.status[root] = 'loading';
|
|
314
|
+
renderFiles();
|
|
315
|
+
fetch(ROOTS[root].url, { credentials: 'same-origin', headers: { accept: 'application/json' } })
|
|
316
|
+
.then(function (r) {
|
|
317
|
+
if (r.status === 401 || r.status === 403) { fstate.status[root] = 'auth'; return null; }
|
|
318
|
+
if (!r.ok) throw new Error('http ' + r.status);
|
|
319
|
+
return r.json();
|
|
320
|
+
})
|
|
321
|
+
.then(function (j) {
|
|
322
|
+
if (!j) { renderFiles(); return; }
|
|
323
|
+
var raw = j[ROOTS[root].key] || [];
|
|
324
|
+
var norm = ROOTS[root].norm || normItem;
|
|
325
|
+
var items = [];
|
|
326
|
+
for (var i = 0; i < raw.length; i++) { var n = norm(raw[i]); if (n) items.push(n); }
|
|
327
|
+
fstate.data[root] = { items: items, folders: j.folders || [] };
|
|
328
|
+
fstate.status[root] = 'ready';
|
|
329
|
+
renderFiles();
|
|
330
|
+
})
|
|
331
|
+
.catch(function () { fstate.status[root] = 'error'; renderFiles(); });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function fstateNode(text, opts) {
|
|
335
|
+
var wrap = document.createElement('div');
|
|
336
|
+
wrap.className = 'rk-aichat-fstate';
|
|
337
|
+
var p = document.createElement('div');
|
|
338
|
+
p.textContent = text;
|
|
339
|
+
wrap.appendChild(p);
|
|
340
|
+
if (opts && opts.href) {
|
|
341
|
+
var a = document.createElement('a');
|
|
342
|
+
a.href = opts.href;
|
|
343
|
+
a.textContent = opts.linkText || 'Open';
|
|
344
|
+
wrap.appendChild(a);
|
|
345
|
+
}
|
|
346
|
+
if (opts && opts.retry) {
|
|
347
|
+
var btn = document.createElement('button');
|
|
348
|
+
btn.type = 'button';
|
|
349
|
+
btn.textContent = 'Try again';
|
|
350
|
+
btn.addEventListener('click', function () { loadRoot(fstate.root, true); });
|
|
351
|
+
wrap.appendChild(btn);
|
|
352
|
+
}
|
|
353
|
+
return wrap;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Generic folder row. onOpen is called on click; sub is an optional caption
|
|
357
|
+
// under the name (used for the virtual roots to show their file count / hint).
|
|
358
|
+
function folderRow(label, onOpen, sub) {
|
|
359
|
+
var b = document.createElement('button');
|
|
360
|
+
b.type = 'button';
|
|
361
|
+
b.className = 'rk-aichat-frow';
|
|
362
|
+
var ic = document.createElement('span');
|
|
363
|
+
ic.className = 'rk-aichat-fic rk-aichat-fic-folder';
|
|
364
|
+
ic.textContent = '/';
|
|
365
|
+
var main = document.createElement('span');
|
|
366
|
+
main.className = 'rk-aichat-fmain';
|
|
367
|
+
var name = document.createElement('span');
|
|
368
|
+
name.className = 'rk-aichat-fname';
|
|
369
|
+
name.textContent = label;
|
|
370
|
+
main.appendChild(name);
|
|
371
|
+
if (sub) {
|
|
372
|
+
var meta = document.createElement('span');
|
|
373
|
+
meta.className = 'rk-aichat-fmeta';
|
|
374
|
+
meta.textContent = sub;
|
|
375
|
+
main.appendChild(meta);
|
|
376
|
+
}
|
|
377
|
+
var chev = document.createElement('span');
|
|
378
|
+
chev.className = 'rk-aichat-fadd';
|
|
379
|
+
chev.textContent = 'Open ›';
|
|
380
|
+
b.appendChild(ic); b.appendChild(main); b.appendChild(chev);
|
|
381
|
+
b.addEventListener('click', onOpen);
|
|
382
|
+
return b;
|
|
383
|
+
}
|
|
384
|
+
// A virtual-root entry (My Files / Temp) — shows a live count once loaded.
|
|
385
|
+
function vrootRow(v) {
|
|
386
|
+
var sub = '';
|
|
387
|
+
var st = fstate.status[v.key];
|
|
388
|
+
var d = fstate.data[v.key];
|
|
389
|
+
if (st === 'ready' && d) sub = d.items.length + (d.items.length === 1 ? ' file' : ' files');
|
|
390
|
+
else if (st === 'loading') sub = 'Loading…';
|
|
391
|
+
else if (st === 'auth') sub = 'Sign in to view';
|
|
392
|
+
return folderRow(v.label, function () { enterRoot(v.key); }, sub);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function fileRow(it) {
|
|
396
|
+
var b = document.createElement('button');
|
|
397
|
+
b.type = 'button';
|
|
398
|
+
b.className = 'rk-aichat-frow';
|
|
399
|
+
var ic = document.createElement('span');
|
|
400
|
+
ic.className = 'rk-aichat-fic rk-aichat-fic-file';
|
|
401
|
+
if (it.thumbUrl) {
|
|
402
|
+
var thumb = document.createElement('img'); thumb.src = it.thumbUrl; thumb.alt = ''; thumb.loading = 'lazy'; ic.appendChild(thumb);
|
|
403
|
+
} else if (it.viewUrl && it.contentType.indexOf('image/') === 0) {
|
|
404
|
+
var img = document.createElement('img'); img.src = it.viewUrl; img.alt = ''; img.loading = 'lazy'; ic.appendChild(img);
|
|
405
|
+
} else if (it.viewUrl && it.contentType.indexOf('video/') === 0) {
|
|
406
|
+
var vid = document.createElement('video'); vid.src = it.viewUrl; vid.muted = true; vid.playsInline = true; ic.appendChild(vid);
|
|
407
|
+
} else {
|
|
408
|
+
ic.textContent = (it.name.split('.').pop() || it.name.slice(0, 1)).slice(0, 3).toUpperCase();
|
|
409
|
+
}
|
|
410
|
+
var main = document.createElement('span');
|
|
411
|
+
main.className = 'rk-aichat-fmain';
|
|
412
|
+
var name = document.createElement('span');
|
|
413
|
+
name.className = 'rk-aichat-fname';
|
|
414
|
+
name.textContent = it.name;
|
|
415
|
+
var meta = document.createElement('span');
|
|
416
|
+
meta.className = 'rk-aichat-fmeta';
|
|
417
|
+
meta.textContent = it.metaText || fmtBytes(it.sizeBytes) || it.contentType || 'file';
|
|
418
|
+
main.appendChild(name); main.appendChild(meta);
|
|
419
|
+
var add = document.createElement('span');
|
|
420
|
+
add.className = 'rk-aichat-fadd';
|
|
421
|
+
add.textContent = '+ Attach';
|
|
422
|
+
b.appendChild(ic); b.appendChild(main); b.appendChild(add);
|
|
423
|
+
b.addEventListener('click', function () { attachFile(it); });
|
|
424
|
+
return b;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function crumb(label, current, onClick) {
|
|
428
|
+
var b = document.createElement('button');
|
|
429
|
+
b.type = 'button';
|
|
430
|
+
b.className = 'rk-aichat-crumb' + (current ? ' is-current' : '');
|
|
431
|
+
b.textContent = label;
|
|
432
|
+
b.addEventListener('click', onClick);
|
|
433
|
+
return b;
|
|
434
|
+
}
|
|
435
|
+
function crumbSep() {
|
|
436
|
+
var s = document.createElement('span');
|
|
437
|
+
s.className = 'rk-aichat-crumb-sep';
|
|
438
|
+
s.textContent = '/';
|
|
439
|
+
return s;
|
|
440
|
+
}
|
|
441
|
+
function renderCrumbs() {
|
|
442
|
+
crumbsEl.innerHTML = '';
|
|
443
|
+
// "Files" (virtual root) is always the first crumb
|
|
444
|
+
var atVirtual = !fstate.root;
|
|
445
|
+
crumbsEl.appendChild(crumb('Files', atVirtual, goVirtualRoot));
|
|
446
|
+
if (atVirtual) return;
|
|
447
|
+
if (fstate.status[fstate.root] !== 'ready') return;
|
|
448
|
+
var cur = normFolder(fstate.path[fstate.root]);
|
|
449
|
+
crumbsEl.appendChild(crumbSep());
|
|
450
|
+
crumbsEl.appendChild(crumb(ROOTS[fstate.root].label, !cur, function () { goPath(''); }));
|
|
451
|
+
var parts = cur.split('/').filter(Boolean);
|
|
452
|
+
var acc = '';
|
|
453
|
+
parts.forEach(function (part, idx) {
|
|
454
|
+
crumbsEl.appendChild(crumbSep());
|
|
455
|
+
acc = acc ? acc + '/' + part : part;
|
|
456
|
+
var pth = acc;
|
|
457
|
+
crumbsEl.appendChild(crumb(part, idx === parts.length - 1, function () { goPath(pth); }));
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
var rendering = false;
|
|
462
|
+
function renderFiles() {
|
|
463
|
+
if (!filesPane) return;
|
|
464
|
+
// loadRoot() calls renderFiles() synchronously to show a loading state; a
|
|
465
|
+
// guard stops that re-entrant call from duplicating rows mid-render.
|
|
466
|
+
if (rendering) return;
|
|
467
|
+
rendering = true;
|
|
468
|
+
try { renderFilesInner(); } finally { rendering = false; }
|
|
469
|
+
}
|
|
470
|
+
function renderFilesInner() {
|
|
471
|
+
renderCrumbs();
|
|
472
|
+
updateDropzone();
|
|
473
|
+
filesBody.innerHTML = '';
|
|
474
|
+
// Virtual root: the two sources appear as folders. Kick off a background
|
|
475
|
+
// load of each so the counts fill in, but render the folders immediately.
|
|
476
|
+
if (!fstate.root) {
|
|
477
|
+
VROOTS.forEach(function (v) { if (fstate.status[v.key] === 'idle') loadRoot(v.key, false); });
|
|
478
|
+
VROOTS.forEach(function (v) { filesBody.appendChild(vrootRow(v)); });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
var st = fstate.status[fstate.root];
|
|
482
|
+
if (st === 'idle') { loadRoot(fstate.root, false); return; }
|
|
483
|
+
if (st === 'loading') { filesBody.appendChild(fstateNode('Loading your files…')); return; }
|
|
484
|
+
if (st === 'auth') { filesBody.appendChild(fstateNode('Sign in to browse your files.', { href: '/login', linkText: 'Sign in' })); return; }
|
|
485
|
+
if (st === 'error') { filesBody.appendChild(fstateNode('Couldn’t reach your files.', { retry: true })); return; }
|
|
486
|
+
var data = fstate.data[fstate.root] || { items: [], folders: [] };
|
|
487
|
+
var cur = normFolder(fstate.path[fstate.root]);
|
|
488
|
+
var folders = immediateFolders(data, cur);
|
|
489
|
+
var here = data.items.filter(function (it) { return normFolder(it.folderPath) === cur; });
|
|
490
|
+
if (!folders.length && !here.length) {
|
|
491
|
+
filesBody.appendChild(fstateNode(cur ? 'This folder is empty.' : 'No files in ' + ROOTS[fstate.root].label + ' yet.'));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
folders.forEach(function (f) { filesBody.appendChild(folderRow(f.name, function () { goPath(f.path); })); });
|
|
495
|
+
here.forEach(function (it) { filesBody.appendChild(fileRow(it)); });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function goPath(p) { uploadMsg = ''; fstate.path[fstate.root] = normFolder(p); renderFiles(); }
|
|
499
|
+
function goVirtualRoot() { uploadMsg = ''; fstate.root = ''; renderFiles(); }
|
|
500
|
+
function enterRoot(root) {
|
|
501
|
+
if (!ROOTS[root]) return;
|
|
502
|
+
uploadMsg = '';
|
|
503
|
+
fstate.root = root;
|
|
504
|
+
fstate.path[root] = '';
|
|
505
|
+
loadRoot(root, false);
|
|
506
|
+
renderFiles();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ── upload: drop files or click the button; posts to the current source +
|
|
510
|
+
// folder's real upload endpoint (attachments/upload or temporary-files/upload).
|
|
511
|
+
function dropCaption() {
|
|
512
|
+
if (!fstate.root) return '';
|
|
513
|
+
var f = normFolder(fstate.path[fstate.root]);
|
|
514
|
+
return 'Drop files into ' + ROOTS[fstate.root].label + (f ? ' / ' + f : '');
|
|
515
|
+
}
|
|
516
|
+
function updateDropzone() {
|
|
517
|
+
if (!dropEl) return;
|
|
518
|
+
// Raws are imported/scanned, not plain-uploaded — no dropzone in that root.
|
|
519
|
+
var show = !!fstate.root && fstate.root !== 'raws' && fstate.status[fstate.root] !== 'auth';
|
|
520
|
+
dropEl.hidden = !show;
|
|
521
|
+
if (show && dropText && !uploading) { dropText.textContent = uploadMsg || dropCaption(); }
|
|
522
|
+
}
|
|
523
|
+
function uploadFiles(fileList) {
|
|
524
|
+
if (!fstate.root || fstate.root === 'raws' || uploading) return;
|
|
525
|
+
var files = [];
|
|
526
|
+
for (var i = 0; i < fileList.length; i++) files.push(fileList[i]);
|
|
527
|
+
if (!files.length) return;
|
|
528
|
+
uploading = true; uploadMsg = '';
|
|
529
|
+
var root = fstate.root;
|
|
530
|
+
var folder = normFolder(fstate.path[root]);
|
|
531
|
+
var url = ROOTS[root].url + '/upload';
|
|
532
|
+
if (dropEl) dropEl.classList.remove('is-drag');
|
|
533
|
+
if (uploadBtn) uploadBtn.disabled = true;
|
|
534
|
+
var total = files.length, failed = 0;
|
|
535
|
+
if (dropText) dropText.textContent = 'Uploading ' + total + ' file' + (total > 1 ? 's' : '') + '…';
|
|
536
|
+
function next(idx) {
|
|
537
|
+
if (idx >= files.length) {
|
|
538
|
+
uploading = false;
|
|
539
|
+
if (uploadBtn) uploadBtn.disabled = false;
|
|
540
|
+
uploadMsg = failed ? (failed + ' of ' + total + ' failed to upload') : '';
|
|
541
|
+
loadRoot(root, true); // refresh listing; its re-render resets the caption
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
var fd = new FormData();
|
|
545
|
+
fd.append('file', files[idx]);
|
|
546
|
+
fd.append('folder_path', folder);
|
|
547
|
+
fetch(url, { method: 'POST', credentials: 'same-origin', body: fd })
|
|
548
|
+
.then(function (r) { if (!r.ok) throw new Error('http ' + r.status); })
|
|
549
|
+
.catch(function () { failed++; })
|
|
550
|
+
.then(function () { next(idx + 1); });
|
|
551
|
+
}
|
|
552
|
+
next(0);
|
|
553
|
+
}
|
|
554
|
+
if (uploadBtn) uploadBtn.addEventListener('click', function () { if (uploadInput) uploadInput.click(); });
|
|
555
|
+
if (uploadInput) uploadInput.addEventListener('change', function () { uploadFiles(uploadInput.files); uploadInput.value = ''; });
|
|
556
|
+
if (dropEl) {
|
|
557
|
+
dropEl.addEventListener('dragover', function (e) { e.preventDefault(); dropEl.classList.add('is-drag'); });
|
|
558
|
+
dropEl.addEventListener('dragleave', function (e) { if (e.target === dropEl) dropEl.classList.remove('is-drag'); });
|
|
559
|
+
dropEl.addEventListener('drop', function (e) {
|
|
560
|
+
e.preventDefault(); dropEl.classList.remove('is-drag');
|
|
561
|
+
if (e.dataTransfer && e.dataTransfer.files) uploadFiles(e.dataTransfer.files);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function attachFile(it) {
|
|
566
|
+
if (selected.some(function (f) { return f.id === it.id; })) { flashChip(it.id); return; }
|
|
567
|
+
selected.push(it);
|
|
568
|
+
renderChips();
|
|
569
|
+
if (input) input.focus();
|
|
570
|
+
}
|
|
571
|
+
function renderChips() {
|
|
572
|
+
if (!chipsEl) return;
|
|
573
|
+
chipsEl.innerHTML = '';
|
|
574
|
+
selected.forEach(function (f) {
|
|
575
|
+
var chip = document.createElement('span');
|
|
576
|
+
chip.className = 'rk-aichat-chip';
|
|
577
|
+
chip.setAttribute('data-id', f.id);
|
|
578
|
+
var label = document.createElement('span');
|
|
579
|
+
label.textContent = f.name;
|
|
580
|
+
var x = document.createElement('button');
|
|
581
|
+
x.type = 'button';
|
|
582
|
+
x.setAttribute('aria-label', 'Remove ' + f.name);
|
|
583
|
+
x.textContent = '×';
|
|
584
|
+
x.addEventListener('click', function () {
|
|
585
|
+
selected = selected.filter(function (s) { return s.id !== f.id; });
|
|
586
|
+
renderChips();
|
|
587
|
+
});
|
|
588
|
+
chip.appendChild(label); chip.appendChild(x);
|
|
589
|
+
chipsEl.appendChild(chip);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
function flashChip(id) {
|
|
593
|
+
var chip = chipsEl && chipsEl.querySelector('[data-id="' + id + '"]');
|
|
594
|
+
if (!chip) return;
|
|
595
|
+
chip.style.transition = 'none';
|
|
596
|
+
chip.style.transform = 'scale(1.08)';
|
|
597
|
+
setTimeout(function () { chip.style.transition = 'transform .18s'; chip.style.transform = 'none'; }, 20);
|
|
598
|
+
}
|
|
599
|
+
function clearChips() { selected = []; renderChips(); }
|
|
600
|
+
|
|
601
|
+
function filesOpen() { return panel.classList.contains('has-files'); }
|
|
602
|
+
function setFilesOpen(open) {
|
|
603
|
+
panel.classList.toggle('has-files', open);
|
|
604
|
+
if (filesToggle) {
|
|
605
|
+
filesToggle.classList.toggle('is-on', open);
|
|
606
|
+
filesToggle.setAttribute('aria-pressed', open ? 'true' : 'false');
|
|
607
|
+
}
|
|
608
|
+
try { localStorage.setItem('rk-files-open', open ? '1' : '0'); } catch (e) {}
|
|
609
|
+
if (open) renderFiles();
|
|
610
|
+
if (!panel.hidden) anchorPanel();
|
|
611
|
+
}
|
|
612
|
+
function refreshFilesOnOpen() {
|
|
613
|
+
if (filesOpen()) {
|
|
614
|
+
// re-fetch whichever source(s) are in view so counts/listings are fresh
|
|
615
|
+
if (fstate.root) loadRoot(fstate.root, true);
|
|
616
|
+
else VROOTS.forEach(function (v) { loadRoot(v.key, true); });
|
|
617
|
+
renderFiles();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (filesToggle) {
|
|
622
|
+
filesToggle.addEventListener('click', function () { setFilesOpen(!filesOpen()); });
|
|
623
|
+
}
|
|
624
|
+
// Honor the saved visibility preference (default: open).
|
|
625
|
+
try {
|
|
626
|
+
setFilesOpen(localStorage.getItem('rk-files-open') !== '0');
|
|
627
|
+
} catch (e) { setFilesOpen(true); }
|
|
628
|
+
renderFiles();
|
|
629
|
+
|
|
630
|
+
try { if (sessionStorage.getItem('rk-chat-open') === '1') openPanel(); } catch (e) {}
|
|
631
|
+
})();`;
|
|
45
632
|
/** Wrap page content in a complete standalone reskin HTML document. */
|
|
46
633
|
export function reskinDocument(input) {
|
|
47
634
|
const chrome = input.chrome ?? "full";
|
|
48
635
|
const active = input.activeSlug ?? null;
|
|
49
636
|
const pageCss = input.pageCss ? `\n/* page-scoped */\n${input.pageCss}\n` : "";
|
|
50
|
-
const
|
|
637
|
+
const chromeScript = chrome === "full" ? `\n<script>${RESKIN_CHROME_SCRIPT}</script>` : "";
|
|
638
|
+
const script = (input.script ? `\n<script>${input.script}</script>` : "") + chromeScript;
|
|
51
639
|
const shell = chrome === "full"
|
|
52
640
|
? `<div class="rk-app">
|
|
53
641
|
${renderSidebar(active)}
|
|
54
642
|
<div class="rk-content">
|
|
55
643
|
${input.body}
|
|
56
644
|
</div>
|
|
645
|
+
${renderChatDock(active)}
|
|
57
646
|
</div>`
|
|
58
647
|
: input.body;
|
|
59
648
|
return `<!doctype html>
|
|
@@ -61,8 +650,8 @@ ${input.body}
|
|
|
61
650
|
<head>
|
|
62
651
|
<meta charset="utf-8">
|
|
63
652
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
64
|
-
<title>${rkEscape(input.title)}</title>
|
|
65
|
-
<meta name="description" content="${rkEscape(input.description ?? input.title)}">
|
|
653
|
+
<title>${rkEscape(input.title.replace("vidfarm reskin", "vidfarm"))}</title>
|
|
654
|
+
<meta name="description" content="${rkEscape((input.description ?? input.title).replace("vidfarm reskin", "vidfarm"))}">
|
|
66
655
|
${RESKIN_FONT_LINKS}
|
|
67
656
|
<style>${RESKIN_CSS}${pageCss}</style>
|
|
68
657
|
</head>
|