@mevdragon/vidfarm-devcli 0.18.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 (53) hide show
  1. package/.agents/skills/vidfarm-media/SKILL.md +41 -4
  2. package/SKILL.director.md +17 -3
  3. package/SKILL.platform.md +3 -3
  4. package/demo/dist/app.js +69 -69
  5. package/demo/dist/favicon.ico +0 -0
  6. package/dist/src/app.js +1550 -187
  7. package/dist/src/cli.js +227 -7
  8. package/dist/src/devcli/clip-store.js +29 -2
  9. package/dist/src/devcli/clips.js +52 -9
  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 +1 -1
  20. package/dist/src/reskin/calendar-page.js +2 -1
  21. package/dist/src/reskin/chat-page.js +319 -72
  22. package/dist/src/reskin/discover-page.js +486 -31
  23. package/dist/src/reskin/document.js +1171 -384
  24. package/dist/src/reskin/help-page.js +1 -0
  25. package/dist/src/reskin/inpaint-page.js +10 -4
  26. package/dist/src/reskin/library-page.js +918 -220
  27. package/dist/src/reskin/login-page.js +6 -6
  28. package/dist/src/reskin/pricing-page.js +2 -0
  29. package/dist/src/reskin/settings-page.js +55 -10
  30. package/dist/src/reskin/theme.js +337 -17
  31. package/dist/src/services/clip-curation/gemini.js +5 -0
  32. package/dist/src/services/clip-curation/hunt.js +79 -1
  33. package/dist/src/services/clip-curation/index.js +2 -1
  34. package/dist/src/services/clip-curation/local-agent.js +4 -3
  35. package/dist/src/services/clip-curation/media-select.js +85 -0
  36. package/dist/src/services/clip-curation/query.js +5 -1
  37. package/dist/src/services/clip-curation/refine.js +50 -20
  38. package/dist/src/services/clip-curation/scan.js +10 -3
  39. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  40. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  41. package/dist/src/services/clip-records.js +14 -1
  42. package/dist/src/services/clip-search.js +43 -13
  43. package/dist/src/services/file-directory.js +114 -0
  44. package/dist/src/services/storage.js +24 -1
  45. package/dist/src/services/upstream.js +5 -5
  46. package/dist/src/template-editor-shell.js +16 -2
  47. package/package.json +1 -1
  48. package/public/assets/discover-client-app.js +1 -0
  49. package/public/assets/file-directory-app.js +2 -0
  50. package/public/assets/homepage-client-app.js +12 -12
  51. package/public/assets/page-runtime-client-app.js +24 -24
  52. package/src/assets/favicon.ico +0 -0
  53. package/src/assets/logo-vidfarm.png +0 -0
@@ -18,7 +18,7 @@
18
18
  // accent over a neutral base, heavy rounded display type, soft cards, glass
19
19
  // pill nav, warm-dark footer).
20
20
  export const RESKIN_PAGES = [
21
- { slug: "settings", label: "Settings", emoji: "⚙️", blurb: "Profile, wallet, channels & developer keys", live: "/settings", done: true },
21
+ { slug: "settings", label: "Settings", emoji: "⚙️", blurb: "Profile, wallet, channels & developer keys", live: "/settings/profile", done: true },
22
22
  { slug: "library", label: "Library", emoji: "🎬", blurb: "Your projects, renders & mined clips (tabbed)", live: "/library", done: true },
23
23
  { slug: "discover", label: "Discover", emoji: "✨", blurb: "Template feed · swipe · categories", live: "/discover", done: true },
24
24
  { slug: "chat", label: "Brainstorm", emoji: "💬", blurb: "Full AI brainstorm chat", live: "/chat", done: true },
@@ -106,8 +106,9 @@ export const RESKIN_CSS = `
106
106
  .rk-sidebar{position:fixed;left:0;top:0;bottom:0;width:248px;z-index:50;display:flex;flex-direction:column;gap:4px;
107
107
  padding:20px 14px 16px;background:#fff;border-right:1px solid var(--rk-border);overflow-y:auto}
108
108
  .rk-content{margin-left:248px;min-height:100vh;background:var(--rk-bg)}
109
- .rk-sidebar-brand{display:flex;align-items:center;gap:8px;padding:6px 10px 18px;font-family:var(--rk-font-display);
109
+ .rk-sidebar-brand{display:flex;align-items:center;gap:10px;padding:6px 10px 18px;font-family:var(--rk-font-display);
110
110
  font-weight:800;font-size:20px;color:var(--rk-ink);letter-spacing:-.02em}
111
+ .rk-sidebar-brand .rk-brand-mark{width:36px;height:36px;border-radius:10px}
111
112
  .rk-sidebar-nav{display:flex;flex-direction:column;gap:3px;flex:1;min-height:0}
112
113
  .rk-sidebar-sep{height:1px;background:var(--rk-border);margin:8px}
113
114
  .rk-sidebar-item{display:flex;align-items:center;padding:9px 13px;border-radius:var(--rk-r-lg);
@@ -115,12 +116,22 @@ export const RESKIN_CSS = `
115
116
  transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
116
117
  .rk-sidebar-item:hover{background:var(--rk-n-100);color:var(--rk-ink)}
117
118
  .rk-sidebar-item.is-active{background:var(--rk-gold-tint);color:var(--rk-ink)}
119
+ /* signed-in identity — subtle, small, pinned to the sidebar foot so you can
120
+ * tell which account you're in at a glance */
121
+ /* padding-bottom reserves room for the floating chat FAB (bottom-left,
122
+ * ~56px + 16px margin) so the name/email are never hidden behind it */
123
+ .rk-sidebar-account{margin-top:8px;padding:8px 13px 80px;border-top:1px solid var(--rk-border);
124
+ color:var(--rk-n-500);line-height:1.3;overflow:hidden}
125
+ .rk-sidebar-account-name{font-size:12px;font-weight:600;color:var(--rk-n-600);
126
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
127
+ .rk-sidebar-account-email{font-size:11px;font-weight:500;color:var(--rk-n-400);
128
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
118
129
  /* AI chat dock — draggable FAB (defaults to the old sidebar-foot spot) + popup panel.
119
130
  * The panel is 2x the sidebar width and, when it opens on the left, visually
120
131
  * REPLACES the nav (sidebar goes visibility:hidden via body.rk-chat-left so the
121
132
  * layout never jumps). If the FAB was dragged to the right half of the viewport
122
133
  * the panel anchors right instead, so it is never cut off. */
123
- .rk-fab{position:fixed;left:16px;bottom:16px;z-index:90;width:56px;height:56px;border-radius:var(--rk-r-full);
134
+ .rk-fab{position:fixed;left:16px;bottom:calc(16px + env(safe-area-inset-bottom));z-index:90;width:56px;height:56px;border-radius:var(--rk-r-full);
124
135
  border:0;cursor:grab;background:var(--rk-gold-500);color:var(--rk-ink);display:grid;place-items:center;
125
136
  box-shadow:var(--rk-shadow-lg);touch-action:none;user-select:none;-webkit-user-select:none;
126
137
  transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),opacity var(--rk-dur) var(--rk-ease)}
@@ -132,7 +143,7 @@ export const RESKIN_CSS = `
132
143
  display:flex;flex-direction:row;background:var(--rk-surface);border:1px solid var(--rk-border);
133
144
  border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-xl);overflow:hidden;
134
145
  animation:rk-aichat-in .22s var(--rk-ease)}
135
- .rk-aichat.has-files{width:min(772px,calc(100vw - 24px))}
146
+ .rk-aichat.has-files,.rk-aichat.has-history{width:min(772px,calc(100vw - 24px))}
136
147
  .rk-aichat.is-right{left:auto;right:12px}
137
148
  .rk-aichat[hidden]{display:none}
138
149
  @keyframes rk-aichat-in{from{opacity:0;transform:translateY(12px) scale(.98)}to{opacity:1;transform:none}}
@@ -143,7 +154,31 @@ export const RESKIN_CSS = `
143
154
  .rk-aichat-files{flex:none;width:266px;display:none;flex-direction:column;min-height:0;
144
155
  border-right:1px solid var(--rk-border);background:var(--rk-n-50)}
145
156
  .rk-aichat.has-files .rk-aichat-files{display:flex}
146
- .rk-aichat-files .rk-aichat-crumbs{padding-top:22px}
157
+ .rk-aichat-files .rk-aichat-crumbs{padding-top:2px}
158
+
159
+ /* files-only mode — opened from the /chat composer's attach button. Shows ONLY
160
+ the file explorer (no chat column / history), as a slim right-hand drawer. */
161
+ .rk-aichat.is-files-only{width:min(384px,calc(100vw - 24px))}
162
+ .rk-aichat.is-files-only .rk-aichat-main,
163
+ .rk-aichat.is-files-only .rk-aichat-history{display:none}
164
+ .rk-aichat.is-files-only .rk-aichat-files{display:flex;flex:1;border-right:0}
165
+ .rk-aichat.is-files-only .rk-aichat-files .rk-aichat-crumbs{padding-top:2px}
166
+ .rk-aichat-files-head{display:flex;align-items:center;gap:8px;
167
+ padding:14px 14px 10px;flex:none}
168
+ .rk-aichat-files-title{flex:1;min-width:0;font-family:var(--rk-font-display);font-weight:800;
169
+ font-size:15px;color:var(--rk-ink);letter-spacing:-.01em}
170
+
171
+ /* left Chat-history pane — shares the same left slot as Files (only one shows
172
+ at a time); reuses the rk-aichat-frow row visuals for each past chat. */
173
+ .rk-aichat-history{flex:none;width:266px;display:none;flex-direction:column;min-height:0;
174
+ border-right:1px solid var(--rk-border);background:var(--rk-n-50)}
175
+ .rk-aichat.has-history .rk-aichat-history{display:flex}
176
+ .rk-aichat-hist-head{padding:22px 14px 8px;font-family:var(--rk-font-display);font-weight:800;
177
+ font-size:13px;color:var(--rk-ink);letter-spacing:-.01em;flex:none}
178
+ .rk-aichat-hist-body{flex:1;min-height:0;overflow-y:auto;padding:2px 8px 12px;display:grid;gap:2px;align-content:start}
179
+ .rk-aichat-hist-body .rk-aichat-frow.is-active{background:#fff;box-shadow:var(--rk-shadow-xs);
180
+ outline:1.5px solid var(--rk-gold-600);outline-offset:-1.5px}
181
+ .rk-aichat-hist-body .rk-aichat-frow.is-active .rk-aichat-fic{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
147
182
  .rk-aichat-crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:1px;padding:0 12px 8px;flex:none}
148
183
  .rk-aichat-crumb{padding:2px 6px;border-radius:var(--rk-r-sm);cursor:pointer;background:transparent;border:0;
149
184
  color:var(--rk-text-muted);font-size:12px;font-weight:600;font-family:inherit;max-width:120px;overflow:hidden;
@@ -156,6 +191,9 @@ export const RESKIN_CSS = `
156
191
  border:0;background:transparent;cursor:pointer;text-align:left;font-family:inherit;min-width:0;
157
192
  transition:background var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
158
193
  .rk-aichat-frow:hover{background:#fff;box-shadow:var(--rk-shadow-xs)}
194
+ /* preview trigger fills the row; attach button sits to its right */
195
+ .rk-aichat-fmainbtn{flex:1;min-width:0;display:flex;align-items:center;gap:9px;border:0;background:transparent;
196
+ padding:0;cursor:pointer;text-align:left;font-family:inherit}
159
197
  .rk-aichat-fic{width:30px;height:30px;border-radius:var(--rk-r-md);flex:none;display:grid;place-items:center;
160
198
  font-size:11px;font-weight:700;overflow:hidden}
161
199
  .rk-aichat-fic-folder{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
@@ -164,8 +202,12 @@ export const RESKIN_CSS = `
164
202
  .rk-aichat-fmain{flex:1;min-width:0;display:grid;gap:1px}
165
203
  .rk-aichat-fname{font-size:12.5px;font-weight:600;color:var(--rk-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
166
204
  .rk-aichat-fmeta{font-size:11px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
167
- .rk-aichat-fadd{flex:none;font-size:11px;font-weight:700;color:var(--rk-gold-700);opacity:0;transition:opacity var(--rk-dur) var(--rk-ease)}
205
+ .rk-aichat-fadd{flex:none;font-size:11px;font-weight:700;color:var(--rk-gold-700);opacity:0;cursor:pointer;
206
+ border:1px solid transparent;border-radius:var(--rk-r-full);background:transparent;padding:3px 9px;font-family:inherit;
207
+ transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
168
208
  .rk-aichat-frow:hover .rk-aichat-fadd{opacity:1}
209
+ .rk-aichat-fadd:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
210
+ .rk-aichat-fadd:focus-visible{opacity:1}
169
211
  .rk-aichat-fstate{padding:22px 14px;text-align:center;font-size:12.5px;color:var(--rk-text-muted);line-height:1.5;display:grid;gap:8px;justify-items:center}
170
212
  .rk-aichat-fstate a{color:var(--rk-gold-700);font-weight:600}
171
213
  .rk-aichat-fstate button{font-family:inherit;font-size:12px;font-weight:600;color:var(--rk-ink);cursor:pointer;
@@ -184,12 +226,76 @@ export const RESKIN_CSS = `
184
226
  .rk-aichat-drop-btn:hover{background:var(--rk-gold-tint2)}
185
227
  .rk-aichat-drop-btn:disabled{opacity:.6;cursor:default}
186
228
 
229
+ /* ── reusable directory explorer (createDirectoryExplorer) ──
230
+ Shares the .rk-aichat-frow/-fic/-fmain row look; adds search, multi-select,
231
+ a folder Open button, a per-row "…" kebab menu, and a copy-path toast.
232
+ Reused verbatim by the chat dock (attach mode) and /library/raws (standalone). */
233
+ .rk-dir{display:flex;flex-direction:column;min-height:0;flex:1}
234
+ .rk-dir-search{flex:none;padding:0 12px 8px}
235
+ .rk-dir-search-input{width:100%;font-family:inherit;font-size:12.5px;color:var(--rk-ink);
236
+ background:var(--rk-surface,#fff);border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:7px 12px;
237
+ transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
238
+ .rk-dir-search-input:focus{outline:none;border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
239
+ .rk-dir-search-input::placeholder{color:var(--rk-n-400)}
240
+ .rk-dir-body{flex:1 1 auto;min-height:0}
241
+ .rk-dir-crumbs{align-items:center}
242
+ .rk-dir-newfolder{margin-left:auto;flex:none;font-family:inherit;font-size:11.5px;font-weight:700;cursor:pointer;
243
+ color:var(--rk-gold-700);background:transparent;border:1px solid transparent;border-radius:var(--rk-r-full);padding:3px 10px;
244
+ transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
245
+ .rk-dir-newfolder:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
246
+ /* selection toolbar (shown when 1+ rows are selected) */
247
+ .rk-dir-selbar{flex:none;display:flex;align-items:center;gap:8px;margin:0 10px 8px;padding:7px 10px;
248
+ background:var(--rk-gold-tint);border:1px solid var(--rk-gold-600);border-radius:var(--rk-r-xl)}
249
+ .rk-dir-selbar[hidden]{display:none}
250
+ .rk-dir-selcount{flex:1;font-size:12px;font-weight:700;color:var(--rk-gold-700)}
251
+ .rk-dir-selbtn{font-family:inherit;font-size:12px;font-weight:700;color:var(--rk-ink);cursor:pointer;
252
+ background:#fff;border:1px solid var(--rk-gold-600);border-radius:var(--rk-r-full);padding:4px 12px}
253
+ .rk-dir-selbtn:hover{background:var(--rk-gold-tint2)}
254
+ .rk-dir-selbtn.is-ghost{border-color:var(--rk-border);color:var(--rk-text-muted);background:transparent}
255
+ .rk-dir-selbtn.is-ghost:hover{background:var(--rk-n-100);color:var(--rk-ink)}
256
+ /* per-row select checkbox — discoverable on hover, sticky when selected */
257
+ .rk-dir-check{flex:none;width:15px;height:15px;margin:0;accent-color:var(--rk-gold-600);cursor:pointer;opacity:0;
258
+ transition:opacity var(--rk-dur) var(--rk-ease)}
259
+ .rk-aichat-frow:hover .rk-dir-check,.rk-dir-row.is-selected .rk-dir-check,.rk-dir-check:focus-visible{opacity:1}
260
+ .rk-dir-row.is-selected{background:var(--rk-gold-tint);outline:1.5px solid var(--rk-gold-600);outline-offset:-1.5px}
261
+ /* folder Open button — always visible (it's the navigation affordance) */
262
+ .rk-dir-open{opacity:1;color:var(--rk-text-muted)}
263
+ .rk-aichat-frow:hover .rk-dir-open{color:var(--rk-gold-700)}
264
+ /* per-row "…" kebab */
265
+ .rk-dir-kebab{flex:none;width:26px;height:26px;border-radius:var(--rk-r-md);border:1px solid transparent;background:transparent;
266
+ cursor:pointer;color:var(--rk-n-500);font-size:16px;line-height:1;display:grid;place-items:center;opacity:0;
267
+ transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
268
+ .rk-aichat-frow:hover .rk-dir-kebab,.rk-dir-kebab:focus-visible{opacity:1}
269
+ .rk-dir-kebab:hover{background:var(--rk-n-100);color:var(--rk-ink);border-color:var(--rk-border)}
270
+ /* kebab popup + copy toast (appended to <body>, so plain colors not tokens for
271
+ portability onto any host page) */
272
+ .rk-dir-menu{position:fixed;z-index:2147483000;min-width:150px;background:#fff;border:1px solid var(--rk-border,#e6e1d8);
273
+ border-radius:var(--rk-r-lg,12px);box-shadow:var(--rk-shadow-md,0 10px 30px rgba(40,30,10,.16));padding:5px;display:grid;gap:1px}
274
+ .rk-dir-menuitem{font-family:inherit;font-size:12.5px;font-weight:600;color:var(--rk-ink,#2b2620);cursor:pointer;
275
+ text-align:left;background:transparent;border:0;border-radius:var(--rk-r-md,8px);padding:7px 10px}
276
+ .rk-dir-menuitem:hover{background:var(--rk-n-100,#f2efe9)}
277
+ .rk-dir-menuitem.is-danger{color:#b4341f}
278
+ .rk-dir-menuitem.is-danger:hover{background:#fbeae6}
279
+ .rk-dir-toast{position:fixed;z-index:2147483001;left:50%;bottom:26px;transform:translate(-50%,12px);
280
+ background:#2b2620;color:#fff;font-family:var(--rk-font-body,inherit);font-size:12.5px;font-weight:600;
281
+ padding:9px 15px;border-radius:999px;box-shadow:0 10px 30px rgba(0,0,0,.25);max-width:80vw;overflow:hidden;
282
+ text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none;
283
+ transition:opacity .18s ease,transform .18s ease}
284
+ .rk-dir-toast.is-on{opacity:1;transform:translate(-50%,0)}
285
+
187
286
  /* chat column */
188
287
  .rk-aichat-main{flex:1;min-width:0;display:flex;flex-direction:column}
189
288
  .rk-aichat-head{display:flex;align-items:center;gap:8px;padding:14px 14px;border-bottom:1px solid var(--rk-border);flex:none}
190
289
  .rk-aichat-head-main{flex:1;min-width:0;display:grid;gap:1px}
191
290
  .rk-aichat-title{font-family:var(--rk-font-display);font-weight:800;font-size:15px;color:var(--rk-ink);letter-spacing:-.01em}
192
291
  .rk-aichat-sub{font-size:11.5px;color:var(--rk-text-muted)}
292
+ .rk-aichat-back{flex:1;min-width:0;display:inline-flex;align-items:center;gap:8px;padding:7px 12px 7px 9px;
293
+ border-radius:999px;border:1px solid var(--rk-border);background:#fff;cursor:pointer;text-decoration:none;
294
+ font-family:var(--rk-font-display);font-weight:700;font-size:13.5px;color:var(--rk-ink);letter-spacing:-.01em;
295
+ transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
296
+ .rk-aichat-back:hover{background:var(--rk-n-100);border-color:var(--rk-n-300)}
297
+ .rk-aichat-back svg{width:17px;height:17px;flex:none;color:var(--rk-n-600)}
298
+ .rk-aichat-back span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
193
299
  .rk-aichat-tool{width:30px;height:30px;border-radius:var(--rk-r-lg);border:1px solid var(--rk-border);background:#fff;
194
300
  cursor:pointer;color:var(--rk-n-600);display:grid;place-items:center;font-size:13px;flex:none;
195
301
  transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
@@ -201,19 +307,195 @@ export const RESKIN_CSS = `
201
307
  .rk-aichat-msg.is-ai{background:#fff;border:1px solid var(--rk-border);color:var(--rk-text);justify-self:start;
202
308
  border-bottom-left-radius:var(--rk-r-sm);box-shadow:var(--rk-shadow-xs)}
203
309
  .rk-aichat-msg.is-user{background:var(--rk-gold-tint2);color:var(--rk-ink);justify-self:end;border-bottom-right-radius:var(--rk-r-sm)}
310
+ /* rendered markdown inside assistant replies (bold / italic / links) */
311
+ .rk-aichat-msg.is-ai a,.rk-aichat-stream a{color:var(--rk-gold-700);font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
312
+ .rk-aichat-msg.is-ai strong,.rk-aichat-stream strong{color:var(--rk-ink);font-weight:700}
313
+ .rk-aichat-msg.is-ai em,.rk-aichat-stream em{font-style:italic}
204
314
  .rk-aichat-chips{display:flex;flex-wrap:wrap;gap:6px;padding:10px 12px 0;flex:none}
205
315
  .rk-aichat-chips:empty{display:none}
206
316
  .rk-aichat-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 6px 4px 10px;border-radius:var(--rk-r-full);
207
317
  background:var(--rk-gold-tint2);color:var(--rk-gold-700);font-size:11.5px;font-weight:600;max-width:190px}
208
318
  .rk-aichat-chip span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
209
319
  .rk-aichat-chip button{border:0;background:transparent;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 2px}
210
- .rk-aichat-composer{display:flex;gap:8px;padding:12px;border-top:1px solid var(--rk-border);flex:none;background:#fff}
211
- .rk-aichat-composer .rk-input{flex:1;border-radius:var(--rk-r-full)}
320
+ .rk-aichat-chip-label{max-width:150px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;
321
+ font-size:11.5px;font-weight:600;text-align:left;text-decoration:underline;text-underline-offset:2px;text-decoration-color:transparent}
322
+ .rk-aichat-chip-label:hover{text-decoration-color:currentColor}
323
+ /* composer = one bordered "text area" box: the textarea fills the top and the
324
+ attach + send controls sit inside it along the bottom row. */
325
+ .rk-aichat-composer{display:flex;flex-direction:column;gap:8px;margin:12px;padding:10px 10px 9px;flex:none;background:#fff;
326
+ border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xs);
327
+ transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
328
+ .rk-aichat-composer:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
329
+ .rk-aichat-field{width:100%;min-height:70px;max-height:210px;resize:none;border:0;background:transparent;
330
+ color:var(--rk-ink);font-family:var(--rk-font-body);font-size:14px;line-height:1.5;padding:4px 4px 0}
331
+ .rk-aichat-field:focus{outline:none;box-shadow:none}
332
+ .rk-aichat-field::placeholder{color:var(--rk-n-400)}
333
+ .rk-aichat-composer-actions{display:flex;align-items:center;gap:8px}
334
+ .rk-aichat-composer-actions .rk-btn{margin-left:auto}
335
+ /* composer paperclip — opens the folder directory nav to browse + attach files
336
+ (tokens so it re-themes on the dark editor dock automatically) */
337
+ .rk-aichat-attach{flex:none;width:38px;height:38px;border-radius:var(--rk-r-full);border:1px solid var(--rk-border);
338
+ background:var(--rk-surface);color:var(--rk-n-500);cursor:pointer;display:grid;place-items:center;
339
+ transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
340
+ .rk-aichat-attach:hover,.rk-aichat-attach.is-on{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink)}
341
+ .rk-aichat-attach svg{width:18px;height:18px}
342
+
343
+ /* ── live agentic turn: status → actions (tool notes + HTTP cards) → text ── */
344
+ .rk-aichat-turn{display:grid;gap:8px;justify-items:start;width:100%}
345
+ .rk-aichat-stream{white-space:pre-wrap}
346
+ .rk-aichat-stream[hidden]{display:none}
347
+ .rk-aichat-actions{display:grid;gap:6px;width:100%}
348
+ .rk-aichat-actions:empty{display:none}
349
+ .rk-aichat-status{display:flex;align-items:center;padding:2px 2px}
350
+ .rk-aichat-status[hidden]{display:none}
351
+ .rk-aichat-typing{display:inline-flex;gap:4px;align-items:center}
352
+ .rk-aichat-typing span{width:6px;height:6px;border-radius:50%;background:var(--rk-n-400);
353
+ animation:rk-aichat-blink 1.2s var(--rk-ease) infinite}
354
+ .rk-aichat-typing span:nth-child(2){animation-delay:.18s}
355
+ .rk-aichat-typing span:nth-child(3){animation-delay:.36s}
356
+ @keyframes rk-aichat-blink{0%,80%,100%{opacity:.28;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
357
+ .rk-aichat-toolnote{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;color:var(--rk-text-muted);
358
+ font-family:var(--rk-font-mono);background:#fff;border:1px solid var(--rk-border);
359
+ border-radius:var(--rk-r-lg);padding:5px 10px;width:fit-content}
360
+ .rk-aichat-toolnote::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--rk-gold-500);flex:none}
361
+ .rk-aichat-loadmore{width:100%;margin-top:6px;padding:9px 12px;font-size:12.5px;font-weight:600;
362
+ color:var(--rk-text);background:var(--rk-white);border:1px solid var(--rk-border);
363
+ border-radius:var(--rk-r-lg);cursor:pointer;font-family:var(--rk-font-mono)}
364
+ .rk-aichat-loadmore:hover{background:var(--rk-n-50);border-color:var(--rk-border-strong)}
365
+ .rk-aichat-loadmore:disabled{opacity:.6;cursor:default}
366
+ .rk-aichat-toolnote.is-ok{color:#0a7c3f}
367
+ .rk-aichat-toolnote.is-ok::before{background:#12a150}
368
+ .rk-aichat-toolnote.is-err{color:#b42318}
369
+ .rk-aichat-toolnote.is-err::before{background:#d92d20}
370
+ .rk-aichat-err{font-size:12.5px;color:#b42318;background:#fff;border:1px solid var(--rk-border);
371
+ border-radius:var(--rk-r-lg);padding:9px 11px;white-space:pre-wrap;width:100%}
372
+
373
+ /* first-class HTTP action card (chip → modal) */
374
+ .rk-aichat-http{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px;cursor:pointer;text-align:left;
375
+ background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);box-shadow:var(--rk-shadow-xs);
376
+ transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
377
+ .rk-aichat-http:hover{border-color:var(--rk-gold-600);box-shadow:var(--rk-shadow-sm)}
378
+ .rk-aichat-http-method{flex:none;font-family:var(--rk-font-mono);font-size:10.5px;font-weight:700;letter-spacing:.03em;
379
+ color:var(--rk-gold-700);background:var(--rk-gold-tint);border-radius:var(--rk-r-sm);padding:2px 7px}
380
+ .rk-aichat-http-url{flex:1;min-width:0;font-family:var(--rk-font-mono);font-size:11.5px;color:var(--rk-n-700);
381
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
382
+ .rk-aichat-http-status{flex:none;font-family:var(--rk-font-mono);font-size:11px;font-weight:700;color:var(--rk-green);
383
+ background:var(--rk-green-tint);border-radius:var(--rk-r-sm);padding:2px 7px}
384
+ .rk-aichat-http-status[data-error="true"]{color:var(--rk-red);background:var(--rk-red-tint)}
385
+
386
+ /* HTTP request/response modal (portalled to body, above the fixed panel) */
387
+ .rk-aichat-modal-back{position:fixed;inset:0;z-index:120;display:grid;place-items:center;padding:20px;
388
+ background:rgba(23,23,23,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
389
+ animation:rk-aichat-in .16s var(--rk-ease)}
390
+ .rk-aichat-modal{width:min(720px,100%);max-height:min(82vh,calc(100vh - 24px));display:flex;flex-direction:column;background:var(--rk-surface);
391
+ border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xl);overflow:hidden}
392
+ .rk-aichat-modal-head{display:flex;align-items:center;gap:9px;padding:14px 16px;border-bottom:1px solid var(--rk-border);flex:none}
393
+ .rk-aichat-modal-head .rk-aichat-http-url{font-size:12.5px}
394
+ .rk-aichat-modal-x{flex:none;margin-left:2px;width:28px;height:28px;border-radius:var(--rk-r-lg);border:1px solid var(--rk-border);
395
+ background:#fff;color:var(--rk-n-500);cursor:pointer;font-size:17px;line-height:1;display:grid;place-items:center}
396
+ .rk-aichat-modal-x:hover{background:var(--rk-n-100);color:var(--rk-ink)}
397
+ .rk-aichat-modal-body{padding:14px 16px;overflow-y:auto;display:grid;gap:14px}
398
+ .rk-aichat-http-note{font-size:12.5px;color:var(--rk-text-muted);line-height:1.5}
399
+ .rk-aichat-http-block{display:grid;gap:5px}
400
+ .rk-aichat-http-blabel{font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--rk-text-faint)}
401
+ .rk-aichat-http-pre{margin:0;font-family:var(--rk-font-mono);font-size:11.5px;line-height:1.55;color:var(--rk-n-700);
402
+ background:var(--rk-n-50);border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);padding:10px 12px;
403
+ max-height:220px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere}
404
+
405
+ /* collapsible JSON tree (request/response/headers inspection in the modal) */
406
+ .rk-aichat-json{border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:#fff;overflow:hidden}
407
+ .rk-aichat-json-bar{display:flex;align-items:center;gap:8px;padding:7px 10px;border-bottom:1px solid var(--rk-border);
408
+ background:var(--rk-n-50)}
409
+ .rk-aichat-json-all{font-family:var(--rk-font-body);font-size:11px;font-weight:700;color:var(--rk-n-600);cursor:pointer;
410
+ background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:3px 10px}
411
+ .rk-aichat-json-all:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600);color:var(--rk-ink)}
412
+ .rk-aichat-json-scroll{max-height:300px;overflow:auto;padding:8px 0}
413
+ .rk-aichat-json-line{position:relative;display:flex;align-items:flex-start;width:100%;min-height:22px;padding-right:34px}
414
+ .rk-aichat-json-line:hover{background:var(--rk-gold-tint)}
415
+ .rk-aichat-json-line.is-toggle{padding-left:0}
416
+ .rk-aichat-json-caret{flex:none;width:20px;height:20px;margin-top:1px;display:grid;place-items:center;border:0;
417
+ background:transparent;color:var(--rk-n-500);cursor:pointer}
418
+ .rk-aichat-json-caret svg{transition:transform var(--rk-dur) var(--rk-ease)}
419
+ .rk-aichat-json-caret[data-open="true"] svg{transform:rotate(90deg)}
420
+ .rk-aichat-json-text{display:block;width:100%;font-family:var(--rk-font-mono);font-size:11.5px;line-height:1.5;
421
+ color:var(--rk-n-700);white-space:pre-wrap;word-break:break-word}
422
+ .rk-aichat-json-copy{position:absolute;right:8px;top:2px;width:20px;height:20px;border-radius:var(--rk-r-sm);
423
+ display:grid;place-items:center;border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-500);cursor:pointer;
424
+ opacity:0;box-shadow:var(--rk-shadow-xs);transition:opacity var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
425
+ .rk-aichat-json-line:hover .rk-aichat-json-copy,.rk-aichat-json-copy:focus-visible{opacity:1}
426
+ .rk-aichat-json-copy:hover{color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
212
427
 
213
- /* narrow: stack the files drawer above the chat so neither pane is cut off */
428
+ /* ── file preview modal (images carousel, video, audio, pdf, md/txt/json) ── */
429
+ .rk-aichat-preview{width:min(880px,100%);max-height:min(88vh,calc(100vh - 24px))}
430
+ .rk-aichat-preview-name{flex:1;min-width:0;font-family:var(--rk-font-body);font-weight:700;font-size:13px;color:var(--rk-ink);
431
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
432
+ .rk-aichat-preview-open{flex:none;font-size:11.5px;font-weight:700;color:var(--rk-gold-700);text-decoration:none;
433
+ border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:4px 11px}
434
+ .rk-aichat-preview-open:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
435
+ .rk-aichat-preview-body{padding:0;display:block;min-height:120px;background:var(--rk-n-50)}
436
+ .rk-aichat-preview-img{display:block;max-width:100%;max-height:70vh;margin:0 auto;object-fit:contain;background:var(--rk-n-100)}
437
+ .rk-aichat-preview-media{display:block;width:100%;max-height:70vh;background:#000}
438
+ .rk-aichat-preview-audiowrap{padding:34px 20px;display:grid;place-items:center}
439
+ .rk-aichat-preview-audio{width:100%;max-width:460px}
440
+ .rk-aichat-preview-frame{display:block;width:100%;height:70vh;border:0;background:#fff}
441
+ .rk-aichat-preview-loading{padding:34px 16px;text-align:center;font-size:12.5px;color:var(--rk-text-muted)}
442
+ .rk-aichat-preview-text{margin:0;border:0;border-radius:0;max-height:70vh;background:#fff}
443
+ .rk-aichat-preview .rk-aichat-json{border:0;border-radius:0}
444
+ .rk-aichat-preview .rk-aichat-json-scroll{max-height:70vh}
445
+ .rk-aichat-preview-md{padding:18px 22px;max-height:70vh;overflow:auto;background:#fff;font-size:13.5px;line-height:1.6;color:var(--rk-n-700)}
446
+ .rk-aichat-preview-md h1,.rk-aichat-preview-md h2,.rk-aichat-preview-md h3,.rk-aichat-preview-md h4,.rk-aichat-preview-md h5,.rk-aichat-preview-md h6{
447
+ font-family:var(--rk-font-display);color:var(--rk-ink);font-weight:800;line-height:1.25;margin:16px 0 8px}
448
+ .rk-aichat-preview-md h1{font-size:20px}.rk-aichat-preview-md h2{font-size:17px}.rk-aichat-preview-md h3{font-size:15px}
449
+ .rk-aichat-preview-md h4,.rk-aichat-preview-md h5,.rk-aichat-preview-md h6{font-size:13.5px}
450
+ .rk-aichat-preview-md p{margin:0 0 10px}
451
+ .rk-aichat-preview-md ul,.rk-aichat-preview-md ol{margin:0 0 10px;padding-left:22px}
452
+ .rk-aichat-preview-md li{margin:3px 0}
453
+ .rk-aichat-preview-md a{color:var(--rk-gold-700);font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
454
+ .rk-aichat-preview-md strong{color:var(--rk-ink);font-weight:700}.rk-aichat-preview-md em{font-style:italic}
455
+ .rk-aichat-preview-md code{font-family:var(--rk-font-mono);font-size:12px;background:var(--rk-n-100);border-radius:5px;padding:1px 5px;color:var(--rk-n-700)}
456
+ .rk-aichat-preview-md blockquote{margin:0 0 10px;padding:2px 0 2px 12px;border-left:3px solid var(--rk-border-strong);color:var(--rk-text-muted)}
457
+ .rk-aichat-preview-md hr{border:0;border-top:1px solid var(--rk-border);margin:14px 0}
458
+ .rk-aichat-preview-code{margin:0 0 10px;font-family:var(--rk-font-mono);font-size:11.5px;line-height:1.5;color:var(--rk-n-700);
459
+ background:var(--rk-n-50);border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);padding:10px 12px;overflow:auto;white-space:pre}
460
+ .rk-aichat-preview-code code{background:transparent;padding:0}
461
+ .rk-aichat-preview-empty{padding:40px 16px;display:grid;gap:12px;justify-items:center;text-align:center;font-size:12.5px;color:var(--rk-text-muted)}
462
+ .rk-aichat-preview-dl{font-size:12.5px;font-weight:700;color:var(--rk-ink);cursor:pointer;background:var(--rk-gold-tint);
463
+ border:1px solid var(--rk-gold-600);border-radius:var(--rk-r-full);padding:7px 15px;text-decoration:none}
464
+ .rk-aichat-preview-dl:hover{background:var(--rk-gold-tint2)}
465
+ .rk-aichat-preview-nav{display:flex;align-items:center;justify-content:center;gap:14px;padding:11px 16px;
466
+ border-top:1px solid var(--rk-border);flex:none;background:var(--rk-surface)}
467
+ .rk-aichat-preview-nav[hidden]{display:none}
468
+ .rk-aichat-preview-btn{font-family:inherit;font-size:12px;font-weight:700;color:var(--rk-ink);cursor:pointer;
469
+ background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:6px 14px;
470
+ transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
471
+ .rk-aichat-preview-btn:hover:not(:disabled){background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
472
+ .rk-aichat-preview-btn:disabled{opacity:.4;cursor:default}
473
+ .rk-aichat-preview-count{font-size:11.5px;font-weight:600;color:var(--rk-text-muted);font-variant-numeric:tabular-nums;min-width:52px;text-align:center}
474
+
475
+ /* create_video pipeline: live status row + clickable result links */
476
+ .rk-aichat-flow{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--rk-text-muted);padding:2px 2px}
477
+ .rk-aichat-flow[hidden]{display:none}
478
+ .rk-aichat-flow-label{line-height:1.4}
479
+ .rk-aichat-msg a,.rk-aichat-stream a{color:var(--rk-gold-700);font-weight:700;text-decoration:underline;
480
+ text-underline-offset:2px;overflow-wrap:anywhere}
481
+ .rk-aichat-msg a:hover,.rk-aichat-stream a:hover{color:var(--rk-ink)}
482
+
483
+ /* narrow: stack the left drawer above the chat so neither pane is cut off.
484
+ Cap the drawer to a fraction of the *viewport* (not 44% of the panel) and
485
+ floor the chat log so the composer is never crushed on short/landscape phones. */
214
486
  @media(max-width:700px){
215
- .rk-aichat.has-files{flex-direction:column}
216
- .rk-aichat.has-files .rk-aichat-files{width:auto;max-height:44%;border-right:0;border-bottom:1px solid var(--rk-border)}
487
+ .rk-aichat.has-files,.rk-aichat.has-history{flex-direction:column}
488
+ .rk-aichat.has-files .rk-aichat-files,
489
+ .rk-aichat.has-history .rk-aichat-history{width:auto;max-height:min(44%,34vh);border-right:0;border-bottom:1px solid var(--rk-border)}
490
+ .rk-aichat-log{min-height:60px}
491
+ }
492
+
493
+ /* touch devices have no hover, so controls that reveal on :hover would stay
494
+ invisible. Un-hide them on coarse pointers: the row kebab "…" (Copy path /
495
+ Delete), the multi-select checkbox, the folder Open chevron, the chat +Attach
496
+ button, and the JSON copy button. (Hover-only decorative states stay as-is.) */
497
+ @media(hover:none){
498
+ .rk-dir-kebab,.rk-dir-check,.rk-dir-open,.rk-aichat-fadd,.rk-aichat-json-copy{opacity:1}
217
499
  }
218
500
 
219
501
  /* brand + glass helpers (kept — reused by the sidebar brand + the login page) */
@@ -221,9 +503,9 @@ export const RESKIN_CSS = `
221
503
  -webkit-backdrop-filter:blur(var(--rk-glass-blur));border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-nav)}
222
504
  .rk-brand{display:inline-flex;align-items:center;gap:8px;font-family:var(--rk-font-display);
223
505
  font-weight:800;font-size:20px;color:var(--rk-ink);letter-spacing:-.02em;flex:none}
224
- .rk-brand-mark{width:26px;height:26px;border-radius:8px;background:var(--rk-gold-500);
225
- display:grid;place-items:center;font-family:var(--rk-font-display);font-weight:800;font-size:15px;
226
- color:var(--rk-ink);box-shadow:var(--rk-shadow-xs);flex:none}
506
+ .rk-brand-mark{display:inline-block;vertical-align:middle;width:26px;height:26px;border-radius:8px;
507
+ background:center/contain no-repeat url("/assets/logo-vidfarm.png");
508
+ font-size:0;color:transparent;flex:none;text-indent:-9999px;overflow:hidden}
227
509
  .rk-brand-tag{font-family:var(--rk-font-body);font-size:11px;font-weight:600;color:var(--rk-gold-700);
228
510
  background:var(--rk-gold-tint2);padding:2px 8px;border-radius:var(--rk-r-full);letter-spacing:.02em}
229
511
 
@@ -235,10 +517,35 @@ export const RESKIN_CSS = `
235
517
  .rk-sidebar-nav{flex-direction:row;gap:4px;flex:1}
236
518
  .rk-sidebar-sep{display:none}
237
519
  .rk-sidebar-item{padding:8px 12px}
520
+ /* horizontal strip: FAB no longer collides with the foot, drop the reserve */
521
+ /* horizontal strip: kill the foot border + top offset so the account block
522
+ sits centered in the row instead of showing a stray hairline above it */
523
+ .rk-sidebar-account{padding:8px 13px 2px;border-top:0;margin-top:0}
238
524
  .rk-content{margin-left:0}
239
525
  .rk-chat-left .rk-sidebar{visibility:visible}
240
526
  }
241
527
 
528
+ /* hamburger button — hidden except on phones (see ≤640 block below) */
529
+ .rk-nav-burger{display:none;margin-left:auto;flex:none;width:40px;height:40px;border-radius:var(--rk-r-lg);
530
+ border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-700);cursor:pointer;place-items:center;
531
+ transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
532
+ .rk-nav-burger:hover{background:var(--rk-n-100);border-color:var(--rk-border-strong)}
533
+
534
+ /* phones: swap the horizontal-scroll strip for a real hamburger dropdown so all
535
+ nav items + the account block are reachable (the strip pushes them off-screen). */
536
+ @media(max-width:640px){
537
+ .rk-sidebar{flex-wrap:wrap;overflow:visible;position:sticky;top:0;z-index:60;gap:8px;
538
+ background:#fff;border-bottom:1px solid var(--rk-border)}
539
+ .rk-nav-burger{display:grid}
540
+ .rk-sidebar-nav{order:3;flex-basis:100%;flex-direction:column;gap:3px;display:none;padding-bottom:4px}
541
+ .rk-nav-toggle-cb:checked~.rk-sidebar-nav{display:flex}
542
+ .rk-sidebar-item{width:100%;padding:11px 13px}
543
+ /* account block only inside the open dropdown (it's just noise in the collapsed bar) */
544
+ .rk-sidebar-account{display:none;order:4;flex-basis:100%;border-top:1px solid var(--rk-border);
545
+ margin-top:2px;padding-top:8px}
546
+ .rk-nav-toggle-cb:checked~.rk-sidebar-account{display:block}
547
+ }
548
+
242
549
  /* buttons */
243
550
  .rk-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:11px 18px;
244
551
  border-radius:var(--rk-r-full);font-family:var(--rk-font-body);font-size:14px;font-weight:600;
@@ -250,6 +557,12 @@ export const RESKIN_CSS = `
250
557
  .rk-btn-ink:hover{background:#000;box-shadow:var(--rk-shadow-md);transform:translateY(-1px)}
251
558
  .rk-btn-gold{background:var(--rk-gold-500);color:var(--rk-ink);font-weight:700;box-shadow:var(--rk-shadow-sm)}
252
559
  .rk-btn-gold:hover{background:var(--rk-gold-700);box-shadow:0 6px 18px -6px rgba(252,185,0,.6);transform:translateY(-1px)}
560
+ /* interruptive Stop — the composer send button while a reply is streaming.
561
+ Fixed charcoal (not token-based) so it reads on both the light /chat page
562
+ and the dark editor dock. */
563
+ .rk-btn.is-stop{background:#2f2f2f;color:#fff;box-shadow:var(--rk-shadow-sm)}
564
+ .rk-btn.is-stop:hover{background:#1a1a1a;box-shadow:var(--rk-shadow-md);transform:none}
565
+ .rk-btn.is-stop svg{width:14px;height:14px}
253
566
  .rk-btn-ghost{background:#fff;color:var(--rk-ink);border-color:var(--rk-border);box-shadow:var(--rk-shadow-xs)}
254
567
  .rk-btn-ghost:hover{border-color:var(--rk-border-strong);box-shadow:var(--rk-shadow-sm)}
255
568
  .rk-btn-danger{background:#fff;color:var(--rk-red);border-color:var(--rk-red-tint)}
@@ -257,6 +570,8 @@ export const RESKIN_CSS = `
257
570
  .rk-btn-sm{padding:7px 13px;font-size:13px}
258
571
  .rk-btn-lg{padding:14px 24px;font-size:15px}
259
572
  .rk-btn-block{width:100%}
573
+ /* touch: guarantee a comfortable tap size on the small pill buttons */
574
+ @media(hover:none){.rk-btn-sm{min-height:40px}}
260
575
 
261
576
  /* badges + pills */
262
577
  .rk-badge{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:var(--rk-r-full);
@@ -275,6 +590,11 @@ export const RESKIN_CSS = `
275
590
  padding:28px;box-shadow:var(--rk-shadow-card)}
276
591
  .rk-card-hover{transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
277
592
  .rk-card-hover:hover{transform:translateY(-2px);box-shadow:var(--rk-shadow-lg)}
593
+ /* subtle loading spinner injected over any <video> that is still buffering (see document.ts) */
594
+ .rk-vspin{position:absolute;top:50%;left:50%;width:24px;height:24px;margin:-12px 0 0 -12px;border-radius:50%;border:2.5px solid rgba(255,255,255,.28);border-top-color:rgba(255,255,255,.92);box-shadow:0 1px 5px rgba(0,0,0,.28);animation:rk-vspin-rot .72s linear infinite;pointer-events:none;opacity:0;transition:opacity .2s var(--rk-ease);z-index:4}
595
+ .rk-vspin.is-on{opacity:1}
596
+ @keyframes rk-vspin-rot{to{transform:rotate(360deg)}}
597
+ @media(prefers-reduced-motion:reduce){.rk-vspin{animation-duration:1.5s}}
278
598
  .rk-card-pad-sm{padding:20px}
279
599
  .rk-card h2{font-size:var(--rk-text-2xl);letter-spacing:-.02em}
280
600
  .rk-card h3{font-size:var(--rk-text-lg);font-weight:700;font-family:var(--rk-font-display)}
@@ -315,10 +635,10 @@ export const RESKIN_CSS = `
315
635
 
316
636
  /* two-column shell with sticky side rail (settings, etc.) */
317
637
  .rk-shell{display:grid;grid-template-columns:248px minmax(0,1fr);gap:32px;align-items:start}
318
- @media(max-width:860px){.rk-shell{grid-template-columns:1fr}}
638
+ @media(max-width:900px){.rk-shell{grid-template-columns:1fr}}
319
639
  .rk-side{position:sticky;top:88px;display:grid;gap:4px;background:#fff;border:1px solid var(--rk-border);
320
640
  border-radius:var(--rk-r-2xl);padding:12px;box-shadow:var(--rk-shadow-card)}
321
- @media(max-width:860px){.rk-side{position:static;grid-auto-flow:column;overflow:auto}}
641
+ @media(max-width:900px){.rk-side{position:static;grid-auto-flow:column;overflow:auto}}
322
642
  .rk-side-item{display:flex;align-items:center;gap:10px;padding:11px 14px;border-radius:var(--rk-r-xl);
323
643
  font-size:14px;font-weight:600;color:var(--rk-n-600);cursor:pointer;border:0;background:transparent;
324
644
  box-sizing:border-box;text-decoration:none;
@@ -354,7 +674,7 @@ export const RESKIN_CSS = `
354
674
  .rk-hero-art{position:absolute;inset:0;z-index:0;background:
355
675
  radial-gradient(58% 46% at 78% 20%,rgba(255,241,204,.9),transparent 60%),
356
676
  linear-gradient(180deg,#3f9fe0 0%,#6cc0f5 55%,#a9dbfb 100%)}
357
- .rk-hero{position:relative;z-index:1;text-align:center;padding:119px var(--rk-gutter) 72px}
677
+ .rk-hero{position:relative;z-index:1;text-align:center;padding:clamp(56px,12vh,119px) var(--rk-gutter) clamp(44px,8vh,72px)}
358
678
 
359
679
  :where(.rk-root a,.rk-root button,.rk-root input,.rk-root textarea,.rk-root select):focus-visible{outline:none;box-shadow:var(--rk-ring-gold)}
360
680
  .rk-reveal[hidden]{display:none}
@@ -107,6 +107,7 @@ export class ClipModelClient {
107
107
  const cats = getTaxonomy(this.taxonomyVersion).categories;
108
108
  const enumArray = (values) => z.array(z.enum(values)).default([]);
109
109
  return z.object({
110
+ content_type: enumArray(cats.content_type),
110
111
  subject: enumArray(cats.subject),
111
112
  action: enumArray(cats.action),
112
113
  emotion: enumArray(cats.emotion),
@@ -161,6 +162,7 @@ export class ClipModelClient {
161
162
  console.warn(`[clips] tag failed (${this.provider}) @${input.scene.start_time_sec.toFixed(1)}s: ${message}`);
162
163
  return {
163
164
  tags: {
165
+ content_type: [],
164
166
  subject: [],
165
167
  action: [],
166
168
  emotion: [],
@@ -238,6 +240,7 @@ export class ClipModelClient {
238
240
  const cats = getTaxonomy(this.taxonomyVersion).categories;
239
241
  const opt = (values) => z.array(z.enum(values)).optional();
240
242
  const schema = z.object({
243
+ content_type: opt(cats.content_type),
241
244
  subject: opt(cats.subject),
242
245
  action: opt(cats.action),
243
246
  emotion: opt(cats.emotion),
@@ -307,6 +310,7 @@ export function buildClipEmbeddingText(tags, description) {
307
310
  if (tags.transcript)
308
311
  parts.push(`Transcript: ${tags.transcript}`);
309
312
  const tagBits = [
313
+ ...(tags.content_type ?? []),
310
314
  ...tags.subject,
311
315
  ...tags.action,
312
316
  ...tags.emotion,
@@ -339,6 +343,7 @@ export function buildTaggingPrompt(transcript, version, hasAudio = false, guidan
339
343
  taxonomyForPrompt(version),
340
344
  "",
341
345
  "Rules:",
346
+ "- `content_type` is the single most important tag: what KIND of shot is this? Pick the 1-2 best from talking_head (a person speaking to camera), b_roll (supporting/cutaway footage with no one addressing the camera), product_shot (a product is the focus), screen_recording (a screen/app/UI capture), demo (showing how something works), reaction (someone reacting), interview (two+ people in conversation), establishing (a wide scene-setting shot), lifestyle (ambient real-life moment), text_graphic (text card / motion graphic). Always set at least one when you can tell.",
342
347
  "- Pick only tags you are confident about from the keyframes/audio. Omit a category (empty list) rather than guess.",
343
348
  "- If there is no clear person/subject, use subject: [\"no_subject\"].",
344
349
  "- If there is no speech, set audio_type appropriately (e.g. music/ambient/silence) and leave transcript empty.",
@@ -6,6 +6,66 @@
6
6
  // NEVER GhostCut on the long-form source). Shared by devcli (local), the Hono
7
7
  // API, and the cloud scan Lambda so every surface parses and normalizes
8
8
  // identically. All helpers are pure/deterministic — no AI calls here.
9
+ // ── Unspecified-prompt defaults ─────────────────────────────────────────────
10
+ // When a user imports a source video without saying what they want, we bias
11
+ // toward the short-form product default: a handful of DISTINCT, vertical,
12
+ // text-free 5–15s clips rather than every raw cut. These fill only the gaps —
13
+ // any field the user (or their prompt) sets always wins.
14
+ /** Default soft clip length when unspecified: ~10s, 5–15s band. */
15
+ export const DEFAULT_CLIP_DURATION_BAND = { min_sec: 5, max_sec: 15, target_sec: 10 };
16
+ /** Default mining density when neither max_clips nor clips_per_10min is set. */
17
+ export const DEFAULT_CLIPS_PER_10_MIN = 10;
18
+ /** Default output aspect when unspecified (vertical / shorts). */
19
+ export const DEFAULT_CLIP_ASPECT = "9:16";
20
+ /**
21
+ * Fill the unspecified-prompt defaults on a normalized hunt spec: a 5–15s
22
+ * duration band, vertical 9:16 crop, and avoid-text ON. Only fills gaps — an
23
+ * explicit user choice (including avoid_text:false) is preserved.
24
+ */
25
+ export function applyHuntSpecDefaults(spec) {
26
+ const out = { ...spec };
27
+ if (!out.duration_band)
28
+ out.duration_band = { ...DEFAULT_CLIP_DURATION_BAND };
29
+ if (out.avoid_text === undefined)
30
+ out.avoid_text = true;
31
+ if (!out.aspect)
32
+ out.aspect = DEFAULT_CLIP_ASPECT;
33
+ return out;
34
+ }
35
+ /**
36
+ * The number of clips to aim for given the (effective, windowed) source
37
+ * duration. A hard max_clips wins; otherwise density (default 10 per 10 min ≈
38
+ * one clip per minute) scales with length. Always ≥ 1.
39
+ */
40
+ export function resolveTargetClipCount(effectiveDurationSec, spec) {
41
+ if (spec.max_clips && spec.max_clips > 0)
42
+ return Math.max(1, Math.round(spec.max_clips));
43
+ const per10 = spec.clips_per_10min && spec.clips_per_10min > 0 ? spec.clips_per_10min : DEFAULT_CLIPS_PER_10_MIN;
44
+ const tenMinBlocks = Math.max(0, effectiveDurationSec) / 600;
45
+ return Math.max(1, Math.round(tenMinBlocks * per10));
46
+ }
47
+ /**
48
+ * Deterministic count cap / uniqueness backstop: when more scenes survived than
49
+ * the target, keep an EVENLY-SPACED subset across the timeline. Spread-out
50
+ * scenes are far less likely to be near-duplicates of each other than adjacent
51
+ * ones, so this doubles as a cheap "distinct clips" filter when the AI refine
52
+ * pass is skipped (>60 scenes) or over-returns. Re-indexes the survivors.
53
+ */
54
+ export function capScenesEvenlyToCount(scenes, target) {
55
+ if (!Number.isFinite(target) || target <= 0 || scenes.length <= target)
56
+ return scenes;
57
+ const step = scenes.length / target;
58
+ const picked = [];
59
+ const seen = new Set();
60
+ for (let i = 0; i < target; i++) {
61
+ const idx = Math.min(scenes.length - 1, Math.floor(i * step + step / 2));
62
+ if (seen.has(idx))
63
+ continue;
64
+ seen.add(idx);
65
+ picked.push(scenes[idx]);
66
+ }
67
+ return picked.map((s, i) => ({ ...s, index: i }));
68
+ }
9
69
  // ── Duration band ───────────────────────────────────────────────────────────
10
70
  /**
11
71
  * Map a spoken target ("10 sec", "30 sec", "60 sec", "120 sec") to a soft
@@ -242,7 +302,7 @@ export function buildEffectiveGuidance(input) {
242
302
  */
243
303
  export function parseClipHuntPrompt(prompt) {
244
304
  const text = (prompt ?? "").trim();
245
- const result = { windows: [], target_duration_sec: null, aspect: null, avoid_text: false };
305
+ const result = { windows: [], target_duration_sec: null, aspect: null, avoid_text: false, max_clips: null };
246
306
  if (!text)
247
307
  return result;
248
308
  const tc = "\\d{1,2}:\\d{2}(?::\\d{2})?(?:\\.\\d+)?";
@@ -277,6 +337,14 @@ export function parseClipHuntPrompt(prompt) {
277
337
  /(without|no)\s+(?:any\s+)?(captions?|subtitles?|(?:on[- ]?screen\s+)?text)/i.test(text) ||
278
338
  /\b(caption|text)-free\b/i.test(text) ||
279
339
  /no\s+text\s+on\s+screen/i.test(text);
340
+ // "10 clips" / "give me 20 raws" / "top 5 cuts" → a clip-count target.
341
+ const countMatch = /(\d{1,3})\s*(?:clips?|raws?|shots?|cuts?|segments?)\b/i.exec(text) ??
342
+ /\b(?:top|best|give me|find|get|pull|grab)\s+(\d{1,3})\b/i.exec(text);
343
+ if (countMatch) {
344
+ const n = Number(countMatch[1]);
345
+ if (Number.isFinite(n) && n > 0)
346
+ result.max_clips = Math.min(500, Math.round(n));
347
+ }
280
348
  return result;
281
349
  }
282
350
  // ── Wire normalization (API / SFN / tool payloads) ──────────────────────────
@@ -311,8 +379,18 @@ export function normalizeHuntSpec(raw) {
311
379
  spec.aspect = aspect;
312
380
  if (rec.crop_focus != null)
313
381
  spec.crop_focus = normalizeCropFocus(rec.crop_focus);
382
+ // Explicit true/false both honored so a caller can opt OUT of the avoid-text
383
+ // default; only an absent field falls through to applyHuntSpecDefaults.
314
384
  if (rec.avoid_text === true || rec.no_text === true || rec.no_captions === true)
315
385
  spec.avoid_text = true;
386
+ else if (rec.avoid_text === false || rec.no_text === false || rec.allow_text === true)
387
+ spec.avoid_text = false;
388
+ const maxClips = Number(rec.max_clips ?? rec.clip_count ?? rec.target_clip_count);
389
+ if (Number.isFinite(maxClips) && maxClips > 0)
390
+ spec.max_clips = Math.min(500, Math.round(maxClips));
391
+ const perTen = Number(rec.clips_per_10min ?? rec.clips_per_10_min ?? rec.density);
392
+ if (Number.isFinite(perTen) && perTen > 0)
393
+ spec.clips_per_10min = Math.min(200, perTen);
316
394
  if (typeof rec.source_url === "string" && rec.source_url.trim())
317
395
  spec.source_url = rec.source_url.trim();
318
396
  return spec;