@officexapp/vidfarm-devcli 0.21.27 → 0.21.29

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 (31) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +52 -0
  2. package/.agents/skills/vidfarm/SKILL.md +58 -5
  3. package/.agents/skills/vidfarm/recipes/bulk-scripting-with-a-regime.md +65 -0
  4. package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +78 -7
  5. package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +4 -3
  6. package/.agents/skills/vidfarm/recipes/retheme-template.md +1 -1
  7. package/.agents/skills/vidfarm/references/assets-and-sourcing.md +3 -3
  8. package/.agents/skills/vidfarm/references/automation-and-local-dev.md +92 -1
  9. package/.agents/skills/vidfarm/references/editor-workflows.md +135 -6
  10. package/.agents/skills/vidfarm/references/hooks-and-virality.md +237 -0
  11. package/.agents/skills/vidfarm/references/onboarding.md +5 -5
  12. package/.agents/skills/vidfarm/references/primitives.md +5 -1
  13. package/.agents/skills/vidfarm/regimes/README.md +77 -0
  14. package/.agents/skills/vidfarm/regimes/explainer.QA_REGIME.md +82 -0
  15. package/.agents/skills/vidfarm/regimes/hooks.QA_REGIME.md +117 -0
  16. package/.agents/skills/vidfarm/regimes/product-demo.QA_REGIME.md +92 -0
  17. package/.agents/skills/vidfarm/regimes/short-form.QA_REGIME.md +163 -0
  18. package/.agents/skills/vidfarm/regimes/ugc-testimonial.QA_REGIME.md +82 -0
  19. package/SKILL.director.md +685 -32
  20. package/SKILL.md +22 -3
  21. package/demo/dist/app.js +103 -103
  22. package/dist/src/cli.js +987 -11
  23. package/dist/src/devcli/handoff.js +162 -0
  24. package/dist/src/devcli/interaction-mode.js +154 -0
  25. package/dist/src/devcli/qa-check.js +593 -0
  26. package/dist/src/devcli/qa-regime.js +396 -0
  27. package/dist/src/devcli/sticker-pack.js +396 -0
  28. package/dist/src/devcli/storyboard.js +243 -0
  29. package/package.json +8 -1
  30. package/public/serve-shells/tools-image.html +378 -265
  31. package/public/serve-shells/tools-video.html +760 -167
@@ -1097,8 +1097,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1097
1097
  .rk-inpaint-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter)}
1098
1098
  .rk-inpaint-workbench{display:grid;grid-template-columns:320px minmax(0,1fr) 320px;gap:20px;align-items:start}
1099
1099
  @media(max-width:1200px){.rk-inpaint-workbench{grid-template-columns:320px minmax(0,1fr)}.rk-inpaint-output{grid-column:1 / -1;grid-row:2}}
1100
- @media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-inpaint-stage,.rk-inpaint-panel{grid-column:auto}.rk-inpaint-stage{order:-1}}
1101
- .rk-inpaint-panel,.rk-inpaint-stage,.rk-inpaint-output{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card)}
1100
+ @media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-ip-chat,.rk-inpaint-panel{grid-column:auto}.rk-ip-chat{order:-1}}
1101
+ .rk-inpaint-panel,.rk-ip-chat,.rk-inpaint-output{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card)}
1102
1102
  .rk-inpaint-panel{padding:18px;display:grid;gap:16px;align-content:start}
1103
1103
  .rk-inpaint-block{display:grid;gap:9px}
1104
1104
  .rk-inpaint-h2{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:800;color:var(--rk-ink);letter-spacing:-.01em}
@@ -1173,8 +1173,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1173
1173
  .rk-inpaint-status[data-tone="success"]{color:#1f7a44}
1174
1174
 
1175
1175
  /* center video stage */
1176
- .rk-inpaint-stage{min-height:420px;display:grid;place-items:center;padding:20px;overflow:hidden}
1177
- .rk-ipv-stage{align-content:center}
1178
1176
  .rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:30px 20px}
1179
1177
  .rk-inpaint-empty[hidden]{display:none}
1180
1178
  .rk-inpaint-empty-ic{color:var(--rk-n-400)}
@@ -1201,44 +1199,110 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1201
1199
  .rk-ipv-timecode-copy{width:30px;height:30px}
1202
1200
  .rk-ipv-duration{font-size:11.5px;font-weight:700;color:var(--rk-text-faint);font-variant-numeric:tabular-nums;white-space:nowrap}
1203
1201
 
1204
- /* text-to-video composer (empty state) */
1205
- .rk-ip-gen{width:100%;max-width:460px;margin-top:16px;display:grid;gap:10px;justify-items:stretch}
1206
- .rk-ip-gen-or{position:relative;font-size:11.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--rk-text-faint)}
1207
- .rk-ip-gen-or::before,.rk-ip-gen-or::after{content:"";position:absolute;top:50%;width:calc(50% - 26px);height:1px;background:var(--rk-border)}
1208
- .rk-ip-gen-or::before{left:0}.rk-ip-gen-or::after{right:0}
1209
- .rk-ip-gen-box{background:#fff;border:1.5px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);padding:10px 10px 8px;display:grid;gap:8px;box-shadow:var(--rk-shadow-sm);text-align:left}
1210
- .rk-ip-gen-box:focus-within{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
1211
- .rk-ip-gen.is-dragref .rk-ip-gen-box{border-color:var(--rk-gold-600);border-style:dashed;background:var(--rk-gold-tint)}
1212
- .rk-ip-gen.is-busy .rk-ip-gen-box{opacity:.75}
1213
- .rk-ip-gen-input{width:100%;border:0;outline:0;resize:none;background:transparent;font:inherit;font-size:14px;line-height:1.45;color:var(--rk-ink);min-height:44px;padding:2px 4px}
1214
- .rk-ip-gen-input::placeholder{color:var(--rk-text-faint)}
1202
+
1203
+ /* ---- shared chat surface (tools/image + tools/video) ---------------------- */
1204
+ .rk-ip-chat{display:grid;grid-template-rows:minmax(0,1fr) auto;height:min(76vh,860px);min-height:520px;overflow:hidden}
1205
+ .rk-ip-feed{min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:18px 18px 6px;display:flex;flex-direction:column;gap:14px}
1206
+ .rk-ip-canvasholder{display:none}
1207
+ .rk-ip-turn{display:flex;flex-direction:column;gap:6px;max-width:100%}
1208
+ .rk-ip-turn[data-role="user"]{align-items:flex-end}
1209
+ .rk-ip-turn[data-role="assistant"]{align-items:flex-start}
1210
+ .rk-ip-bubble{max-width:min(88%,640px);padding:10px 13px;border-radius:var(--rk-r-2xl);font-size:13.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
1211
+ .rk-ip-turn[data-role="user"] .rk-ip-bubble{background:var(--rk-gold-tint);border:1px solid var(--rk-gold-600);color:var(--rk-ink);border-bottom-right-radius:var(--rk-r-md,8px)}
1212
+ .rk-ip-turn[data-role="assistant"] .rk-ip-bubble{background:var(--rk-n-50);border:1px solid var(--rk-border);color:var(--rk-text-muted);border-bottom-left-radius:var(--rk-r-md,8px)}
1213
+ .rk-ip-turn-tag{font-size:10.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--rk-text-faint)}
1214
+ .rk-ip-turn-refs{display:flex;flex-wrap:wrap;gap:5px;justify-content:flex-end}
1215
+ .rk-ip-turn-refs img{width:34px;height:34px;border-radius:var(--rk-r-md,8px);border:1px solid var(--rk-border);object-fit:cover}
1216
+ .rk-ip-media{width:fit-content;max-width:min(100%,640px);display:grid;gap:7px}
1217
+ .rk-ip-media-frame{position:relative;width:fit-content;max-width:100%;border-radius:var(--rk-r-2xl);overflow:hidden;background:var(--rk-n-100);border:1px solid var(--rk-border);line-height:0}
1218
+ .rk-ip-media-frame.is-latest{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
1219
+ .rk-ip-media-frame img,.rk-ip-media-frame video{display:block;max-width:100%;max-height:min(52vh,520px);width:auto;height:auto}
1220
+ .rk-ip-media-actions{display:flex;flex-wrap:wrap;align-items:center;gap:5px}
1221
+ .rk-ip-act{display:inline-grid;place-items:center;width:27px;height:27px;padding:0;border:1px solid var(--rk-border);border-radius:50%;background:#fff;color:var(--rk-n-700);cursor:pointer;text-decoration:none;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
1222
+ .rk-ip-act:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
1223
+ .rk-ip-act-note{font-size:11.5px;font-weight:700;color:var(--rk-text-faint);margin-left:2px}
1224
+ .rk-ip-pending{display:flex;align-items:center;gap:9px;padding:10px 13px;border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);font-size:12.5px;font-weight:700;color:var(--rk-text-muted)}
1225
+ .rk-ip-pending::before{content:"";flex:none;width:16px;height:16px;border-radius:50%;border:2.5px solid var(--rk-gold-tint2,rgba(252,185,0,.35));border-top-color:var(--rk-gold-700);animation:rk-ip-spin .8s linear infinite}
1226
+ .rk-ip-turn.is-flash .rk-ip-bubble,.rk-ip-turn.is-flash .rk-ip-media-frame{box-shadow:0 0 0 3px var(--rk-gold-600)}
1227
+ .rk-ip-turn[data-kind="stopped"] .rk-ip-bubble{background:var(--rk-n-100);border-color:var(--rk-border-strong);color:var(--rk-text-muted);font-weight:700}
1228
+ .rk-ip-turn[data-kind="error"] .rk-ip-bubble{background:#fdf3f2;border-color:#e6a8a0;color:#b42318;font-weight:600}
1229
+
1230
+ /* ---- composer ------------------------------------------------------------ */
1231
+ .rk-ip-composer{flex:none;margin:0;padding:12px 18px 16px;border-top:1px solid var(--rk-border);background:var(--rk-surface);display:grid;gap:8px;
1232
+ border-bottom-left-radius:var(--rk-r-3xl);border-bottom-right-radius:var(--rk-r-3xl)}
1233
+ .rk-ip-composer.is-dragref{background:var(--rk-gold-tint)}
1234
+ .rk-ip-composer.is-busy .rk-ip-comp-status{color:var(--rk-gold-700)}
1235
+ /* The grip is anchored to this wrapper, not the textarea, so it never scrolls
1236
+ away with the textarea's own overflow once the message passes 10 rows. */
1237
+ .rk-ip-comp-field{position:relative;display:block}
1238
+ .rk-ip-comp-input{display:block;width:100%;border:1.5px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);background:#fff;outline:0;resize:none;font:inherit;font-size:14px;line-height:1.45;
1239
+ color:var(--rk-ink);min-height:56px;overflow-y:hidden;padding:10px 12px;transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
1240
+ .rk-ip-comp-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
1241
+ .rk-ip-comp-input::placeholder{color:var(--rk-text-faint)}
1242
+ .rk-ip-comp-input:disabled{cursor:not-allowed;color:var(--rk-text-muted);background:var(--rk-n-50)}
1243
+ .rk-ip-comp-grip{position:absolute;right:5px;bottom:5px;width:16px;height:16px;display:grid;place-items:center;color:var(--rk-n-400);cursor:ns-resize;touch-action:none;
1244
+ border-radius:4px;opacity:.75;transition:color var(--rk-dur) var(--rk-ease),opacity var(--rk-dur) var(--rk-ease)}
1245
+ .rk-ip-comp-grip:hover,.rk-ip-comp-field.is-resizing .rk-ip-comp-grip{color:var(--rk-gold-700);opacity:1}
1246
+ .rk-ip-comp-field.is-resizing{user-select:none}
1247
+ .rk-ip-comp-bar{display:flex;align-items:center;gap:8px}
1248
+ .rk-ip-comp-modeselect{appearance:none;-webkit-appearance:none;border:1px solid var(--rk-border-strong);border-radius:999px;background:var(--rk-n-50);color:var(--rk-ink);
1249
+ font:inherit;font-size:12px;font-weight:800;padding:7px 26px 7px 12px;cursor:pointer;outline:0;
1250
+ background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 20 20' fill='none' stroke='%2378716c' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='m5 8 5 5 5-5'/%3E%3C/svg%3E");
1251
+ background-repeat:no-repeat;background-position:right 9px center}
1252
+ .rk-ip-comp-modeselect:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
1253
+ .rk-ip-comp-status{flex:1;min-width:0;margin:0;font-size:11.5px;font-weight:600;line-height:1.35;color:var(--rk-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1254
+ .rk-ip-comp-status[data-tone="error"]{color:#b42318}
1255
+ .rk-ip-comp-status[data-tone="success"]{color:#1f7a44}
1256
+ .rk-ip-comp-mask{display:flex;align-items:center;gap:8px;padding:6px 8px 6px 11px;border:1px solid var(--rk-gold-600);border-radius:999px;background:var(--rk-gold-tint);
1257
+ font-size:11.5px;font-weight:700;color:var(--rk-gold-700);justify-self:start;max-width:100%}
1258
+ .rk-ip-comp-mask[hidden]{display:none}
1259
+ .rk-ip-comp-mask button{border:0;background:transparent;color:inherit;font:inherit;font-size:11.5px;font-weight:800;text-decoration:underline;cursor:pointer;padding:0}
1260
+
1261
+ /* ---- attachment chips ---------------------------------------------------- */
1215
1262
  .rk-ip-gen-refs{display:flex;flex-wrap:wrap;gap:6px}
1216
1263
  .rk-ip-gen-refs[hidden]{display:none}
1217
1264
  .rk-ip-gen-ref{display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-50);font-size:11.5px;color:var(--rk-n-700);max-width:180px}
1218
1265
  .rk-ip-gen-ref img{width:22px;height:22px;border-radius:4px;object-fit:cover;flex:none}
1219
1266
  .rk-ip-gen-ref span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1220
1267
  .rk-ip-gen-ref button{flex:none;border:0;background:transparent;cursor:pointer;color:var(--rk-text-faint);font-size:15px;line-height:1;padding:0 2px}
1221
- .rk-ip-gen-bar{display:flex;align-items:center;gap:8px}
1222
- .rk-ip-gen-hint{flex:1;font-size:11.5px;color:var(--rk-text-faint);line-height:1.35}
1223
- .rk-ip-gen-clip,.rk-ip-gen-send{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;cursor:pointer}
1268
+ .rk-ip-gen-ref button:hover{color:var(--rk-ink)}
1269
+ .rk-ip-gen-ref.is-pending{opacity:.6}
1270
+ .rk-ip-gen-clip,.rk-ip-gen-send{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),opacity var(--rk-dur) var(--rk-ease)}
1224
1271
  .rk-ip-gen-clip{border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
1225
1272
  .rk-ip-gen-clip:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
1273
+ .rk-ip-gen-clip:disabled{opacity:.5;cursor:not-allowed}
1226
1274
  .rk-ip-gen-send{border:0;background:var(--rk-gold-500,var(--rk-gold-600));color:var(--rk-ink)}
1227
1275
  .rk-ip-gen-send:hover:not(:disabled){background:var(--rk-gold-600)}
1228
1276
  .rk-ip-gen-send:disabled{opacity:.5;cursor:default}
1229
- .rk-ip-gen-send.is-busy{background:var(--rk-gold-tint);pointer-events:none}
1230
- .rk-ip-gen-spin{width:16px;height:16px;border-radius:50%;border:2.5px solid var(--rk-gold-tint2,rgba(252,185,0,.35));border-top-color:var(--rk-gold-700);animation:rk-ip-spin .8s linear infinite}
1277
+ .rk-ip-gen-send[data-mode="stop"]{background:var(--rk-ink);color:#fff}
1231
1278
  .rk-ip-gen-status{font-size:12px;font-weight:600;min-height:16px;color:var(--rk-text-muted);text-align:left;padding-left:2px}
1279
+ .rk-ip-gen-status[hidden]{display:none}
1232
1280
  .rk-ip-gen-status[data-tone="error"]{color:#b42318}
1233
1281
  .rk-ip-gen-status[data-tone="success"]{color:#1f7a44}
1282
+ .rk-ip-gen-presets{display:flex;flex-wrap:wrap;gap:6px}
1283
+ .rk-ip-preset{flex:none;border:1px dashed var(--rk-border-strong,var(--rk-border));background:var(--rk-n-50);color:var(--rk-n-700);border-radius:999px;padding:4px 10px;font-size:11.5px;font-weight:600;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
1284
+ .rk-ip-preset:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
1234
1285
 
1235
- /* file-directory pop panel (reference picker) */
1236
- .rk-ip-filemodal{position:fixed;inset:0;z-index:60;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px}
1286
+ /* ---- attach pop panel (shared file explorer) ----------------------------- */
1287
+ .rk-ip-filemodal{position:fixed;inset:0;z-index:60;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px;animation:rk-ip-fade .16s var(--rk-ease)}
1237
1288
  .rk-ip-filemodal-panel{width:min(560px,100%);max-height:min(80vh,680px);display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
1238
1289
  .rk-ip-filemodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
1239
1290
  .rk-ip-filemodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
1291
+ .rk-ip-filemodal-actions{display:flex;align-items:center;gap:10px}
1292
+ .rk-ip-filemodal-upload{border:1px solid var(--rk-border-strong,var(--rk-border));background:var(--rk-n-50);color:var(--rk-n-700);border-radius:999px;padding:5px 12px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
1293
+ .rk-ip-filemodal-upload:hover:not(:disabled){background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
1294
+ .rk-ip-filemodal-upload:disabled{opacity:.6;cursor:default}
1240
1295
  .rk-ip-filemodal-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted);cursor:pointer;padding:0 4px}
1296
+ .rk-ip-filemodal-close:hover{color:var(--rk-ink)}
1241
1297
  .rk-ip-filemodal-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}
1298
+ @keyframes rk-ip-spin{to{transform:rotate(360deg)}}
1299
+ @keyframes rk-ip-fade{from{opacity:0}to{opacity:1}}
1300
+ @media(max-width:820px){.rk-ip-chat{height:auto;min-height:0}.rk-ip-feed{max-height:62vh}}
1301
+
1302
+ .rk-ipv-quick{display:flex;flex-wrap:wrap;gap:6px;padding:10px 18px 0;background:var(--rk-surface)}
1303
+ .rk-ipv-quick .rk-ip-preset{display:inline-flex;align-items:center;gap:5px}
1304
+ .rk-ipv-quick .rk-ip-preset svg{width:12px;height:12px}
1305
+ .rk-ipv-quick + .rk-ip-composer{padding-top:8px;border-top:0}
1242
1306
 
1243
1307
  /* right: result + history */
1244
1308
  .rk-inpaint-output{padding:18px;display:grid;gap:18px;align-content:start}
@@ -1468,44 +1532,61 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1468
1532
  </section>
1469
1533
 
1470
1534
  <!-- CENTER: scrubbable video stage -->
1471
- <section class="rk-inpaint-stage rk-ipv-stage" aria-label="Video canvas">
1472
- <div class="rk-inpaint-empty" data-ipv-empty>
1473
- <span class="rk-inpaint-empty-ic" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg></span>
1474
- <p class="rk-inpaint-empty-title">Load a video to edit</p>
1475
- <p class="rk-inpaint-empty-sub">Drop a video here, or set a start &amp; end frame and a prompt to generate one.</p>
1476
- <button type="button" class="rk-btn rk-btn-ink rk-btn-sm rk-ipv-upload" data-ipv-upload><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg> Upload video</button>
1477
- <input data-ipv-file type="file" accept="video/mp4,video/webm,video/quicktime,video/*" hidden />
1535
+ <!-- CENTER: chat feed + composer, mirroring /tools/image. The scrubbable
1536
+ player + mask canvas is ONE node that gets re-parented into the newest
1537
+ video bubble (exactly how the image page moves its paint stack), so a
1538
+ full feed re-render never tears down the <video> mid-playback. -->
1539
+ <section class="rk-ip-chat" aria-label="Create Video chat">
1540
+ <div class="rk-ip-feed" data-ipv-feed>
1541
+ <div class="rk-inpaint-empty" data-ipv-empty>
1542
+ <span class="rk-inpaint-empty-ic" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg></span>
1543
+ <p class="rk-inpaint-empty-title">Describe the video you want</p>
1544
+ <p class="rk-inpaint-empty-sub">Then keep chatting to refine it — every message regenerates from the newest clip. Upload a clip to edit instead, and paint timed masks on it to steer one region at one moment.</p>
1545
+ <button type="button" class="rk-btn rk-btn-ink rk-btn-sm rk-ipv-upload" data-ipv-upload><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg> Upload video</button>
1546
+ </div>
1547
+ </div>
1478
1548
 
1479
- <div class="rk-ip-gen" id="rk-ipv-gen">
1480
- <span class="rk-ip-gen-or">or</span>
1481
- <div class="rk-ip-gen-box">
1482
- <textarea id="rk-ipv-genprompt" class="rk-ip-gen-input" rows="2" placeholder="Or describe the video you want"></textarea>
1483
- <div class="rk-ip-gen-refs" id="rk-ipv-genrefs" hidden></div>
1484
- <div class="rk-ip-gen-bar">
1485
- <button type="button" class="rk-ip-gen-clip" id="rk-ipv-genclip" aria-label="Attach reference images from your files" title="Attach reference images from your files"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"/><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"/></svg></button>
1486
- <span class="rk-ip-gen-hint">Generate a brand-new clip from a prompt (+ optional frames).</span>
1487
- <button type="button" class="rk-ip-gen-send" id="rk-ipv-gensend" aria-label="Generate video"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg></button>
1549
+ <!-- Parked here between renders; renderChat() moves it into the newest clip. -->
1550
+ <div class="rk-ip-canvasholder" data-ipv-canvas-holder>
1551
+ <div class="rk-ipv-player" data-ipv-player hidden>
1552
+ <div class="rk-ipv-canvaswrap" data-ipv-canvaswrap>
1553
+ <video data-ipv-video class="rk-ipv-video" playsinline></video>
1554
+ <canvas data-ipv-canvas class="rk-ipv-canvas"></canvas>
1555
+ <div class="editor-inpaint-brush-cursor" data-ipv-brush-cursor aria-hidden="true"></div>
1556
+ </div>
1557
+ <div class="rk-ipv-controls">
1558
+ <button type="button" class="rk-ipv-playpause" data-ipv-playpause aria-label="Play/pause"><svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 7 5.5Z"/></svg></button>
1559
+ <input type="range" class="rk-ipv-scrub" data-ipv-scrub min="0" max="1000" step="1" value="0" aria-label="Seek" />
1560
+ <div class="rk-ipv-timecode">
1561
+ <input class="rk-ipv-timecode-input" data-ipv-timecode-input type="text" inputmode="numeric" spellcheck="false" value="00:00:000" aria-label="Current time MM:SS:mmm" />
1562
+ <button type="button" class="rk-ip-iconbtn rk-ipv-timecode-copy" data-ipv-timecode-copy aria-label="Copy current timecode" title="Copy timecode"><svg width="13" height="13" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1"/><path d="M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2"/></svg></button>
1563
+ <span class="rk-ipv-duration" data-ipv-duration>/ 00:00:000</span>
1488
1564
  </div>
1489
1565
  </div>
1490
- <p class="rk-ip-gen-status" id="rk-ipv-genstatus" data-tone="" aria-live="polite"></p>
1491
1566
  </div>
1492
1567
  </div>
1493
1568
 
1494
- <div class="rk-ipv-player" data-ipv-player hidden>
1495
- <div class="rk-ipv-canvaswrap" data-ipv-canvaswrap>
1496
- <video data-ipv-video class="rk-ipv-video" playsinline></video>
1497
- <canvas data-ipv-canvas class="rk-ipv-canvas"></canvas>
1498
- <div class="editor-inpaint-brush-cursor" data-ipv-brush-cursor aria-hidden="true"></div>
1569
+ <div class="rk-ipv-quick" data-ipv-quick>
1570
+ <button type="button" class="rk-ip-preset" data-ipv-quick-video title="Load a clip to edit"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg> Upload Video</button>
1571
+ <button type="button" class="rk-ip-preset" data-ipv-quick-storyboard title="Attach a storyboard image as a visual reference"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg> Upload Storyboard</button>
1572
+ <input data-ipv-file type="file" accept="video/mp4,video/webm,video/quicktime,video/*" hidden />
1573
+ <input data-ipv-storyboard-input type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" multiple hidden />
1574
+ </div>
1575
+
1576
+ <div class="rk-ip-composer" data-ip-composer>
1577
+
1578
+ <div class="rk-ip-gen-refs" id="rk-ipv-genrefs" hidden></div>
1579
+ <div class="rk-ip-comp-field">
1580
+ <textarea id="rk-ipv-genprompt" class="rk-ip-comp-input" rows="2" placeholder="Describe the video you want…"></textarea>
1581
+ <span class="rk-ip-comp-grip" data-ip-grip role="separator" aria-label="Drag to resize the message box" title="Drag to resize"><svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M11 4 4 11"/><path d="M11 8.5 8.5 11"/></svg></span>
1499
1582
  </div>
1500
- <div class="rk-ipv-controls">
1501
- <button type="button" class="rk-ipv-playpause" data-ipv-playpause aria-label="Play/pause"><svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 7 5.5Z"/></svg></button>
1502
- <input type="range" class="rk-ipv-scrub" data-ipv-scrub min="0" max="1000" step="1" value="0" aria-label="Seek" />
1503
- <div class="rk-ipv-timecode">
1504
- <input class="rk-ipv-timecode-input" data-ipv-timecode-input type="text" inputmode="numeric" spellcheck="false" value="00:00:000" aria-label="Current time MM:SS:mmm" />
1505
- <button type="button" class="rk-ip-iconbtn rk-ipv-timecode-copy" data-ipv-timecode-copy aria-label="Copy current timecode" title="Copy timecode"><svg width="13" height="13" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1"/><path d="M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2"/></svg></button>
1506
- <span class="rk-ipv-duration" data-ipv-duration>/ 00:00:000</span>
1507
- </div>
1583
+ <div class="rk-ip-comp-bar">
1584
+
1585
+ <button type="button" class="rk-ip-gen-clip" id="rk-ipv-genclip" aria-label="Attach reference images from your files" title="Attach reference images"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"/><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"/></svg></button>
1586
+ <p class="rk-ip-comp-status" data-ip-status data-ipv-comp-status data-tone="" aria-live="polite"></p>
1587
+ <button type="button" class="rk-ip-gen-send" id="rk-ipv-gensend" aria-label="Send"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg></button>
1508
1588
  </div>
1589
+ <p class="rk-ip-gen-status" id="rk-ipv-genstatus" data-tone="" aria-live="polite" hidden></p>
1509
1590
  </div>
1510
1591
  </section>
1511
1592
 
@@ -1683,6 +1764,282 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1683
1764
  </aside>
1684
1765
  </div>
1685
1766
  <script>
1767
+
1768
+ (function(){
1769
+ var IMAGE_RE = /\.(png|jpe?g|gif|webp|avif)$/i;
1770
+ var SEND_ICON = "<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5\"/><path d=\"m5 12 7-7 7 7\"/></svg>";
1771
+ var STOP_ICON = "<svg width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"5\" y=\"5\" width=\"10\" height=\"10\" rx=\"2\"/></svg>";
1772
+ var MAX_ROWS = 10;
1773
+
1774
+ function isImageFile(f){ return /^image\//i.test((f && f.type) || '') || IMAGE_RE.test((f && f.name) || ''); }
1775
+ function imagesFromClipboard(e){
1776
+ var items = (e.clipboardData && e.clipboardData.items) || [], out = [];
1777
+ for(var i = 0; i < items.length; i++){
1778
+ if(items[i].kind === 'file'){ var f = items[i].getAsFile(); if(f && /^image\//i.test(f.type || '')) out.push(f); }
1779
+ }
1780
+ return out;
1781
+ }
1782
+ function dtHasFiles(e){
1783
+ var t = (e.dataTransfer && e.dataTransfer.types) || [];
1784
+ return Array.prototype.indexOf.call(t, 'Files') !== -1;
1785
+ }
1786
+ function plural(n, word){ return n + ' ' + word + (n > 1 ? 's' : ''); }
1787
+
1788
+ window.__rkChatComposer = function(cfg){
1789
+ cfg = cfg || {};
1790
+ var root = cfg.root, ta = cfg.textarea, refsEl = cfg.refsEl;
1791
+ var sendBtn = cfg.sendBtn, clipBtn = cfg.clipBtn;
1792
+ var uploadBlob = cfg.uploadBlob || function(){ return Promise.reject(new Error('Uploads are unavailable.')); };
1793
+ var status = cfg.onStatus || function(){};
1794
+ var canSendExtra = cfg.canSend || function(){ return false; };
1795
+ var refs = [];
1796
+ var busy = false;
1797
+ // Once the user drags the grip, their height wins: auto-grow stops fighting
1798
+ // them. Sending clears it so the next message starts compact again.
1799
+ var manualHeight = 0;
1800
+
1801
+ // ---- height ------------------------------------------------------------
1802
+ function maxHeight(){
1803
+ if(!ta) return 0;
1804
+ var s = window.getComputedStyle(ta);
1805
+ var line = parseFloat(s.lineHeight) || 20;
1806
+ var pad = (parseFloat(s.paddingTop) || 0) + (parseFloat(s.paddingBottom) || 0)
1807
+ + (parseFloat(s.borderTopWidth) || 0) + (parseFloat(s.borderBottomWidth) || 0);
1808
+ return Math.round(line * MAX_ROWS + pad);
1809
+ }
1810
+ function applyHeight(px, cap){
1811
+ ta.style.height = px + 'px';
1812
+ ta.style.overflowY = ta.scrollHeight > px + 1 ? 'auto' : (cap ? 'auto' : 'hidden');
1813
+ }
1814
+ function autoGrow(){
1815
+ if(!ta) return;
1816
+ if(manualHeight){ applyHeight(manualHeight, true); return; }
1817
+ var max = maxHeight();
1818
+ ta.style.height = 'auto';
1819
+ applyHeight(Math.min(max, Math.max(56, ta.scrollHeight)), ta.scrollHeight > max);
1820
+ }
1821
+ function resetHeight(){ manualHeight = 0; if(ta){ ta.style.height = 'auto'; } autoGrow(); }
1822
+
1823
+ // Corner grip: pointer-drag the textarea taller/shorter. Bounded so it can
1824
+ // never collapse under one row or grow past the viewport.
1825
+ var field = ta && ta.parentElement;
1826
+ var grip = root && root.querySelector('[data-ip-grip]');
1827
+ if(grip && ta){
1828
+ grip.addEventListener('pointerdown', function(e){
1829
+ e.preventDefault();
1830
+ var startY = e.clientY, startH = ta.getBoundingClientRect().height;
1831
+ var maxPx = Math.max(120, Math.round(window.innerHeight * 0.6));
1832
+ try{ grip.setPointerCapture(e.pointerId); }catch(_){}
1833
+ if(field) field.classList.add('is-resizing');
1834
+ function move(ev){
1835
+ var next = Math.min(maxPx, Math.max(56, Math.round(startH + (ev.clientY - startY))));
1836
+ manualHeight = next;
1837
+ applyHeight(next, true);
1838
+ }
1839
+ function up(ev){
1840
+ try{ grip.releasePointerCapture(e.pointerId); }catch(_){}
1841
+ if(field) field.classList.remove('is-resizing');
1842
+ grip.removeEventListener('pointermove', move);
1843
+ grip.removeEventListener('pointerup', up);
1844
+ grip.removeEventListener('pointercancel', up);
1845
+ // A click with no travel resets to auto-grow — an escape hatch out of
1846
+ // a height you dragged and no longer want.
1847
+ if(Math.abs(ev.clientY - startY) < 3) resetHeight();
1848
+ }
1849
+ grip.addEventListener('pointermove', move);
1850
+ grip.addEventListener('pointerup', up);
1851
+ grip.addEventListener('pointercancel', up);
1852
+ });
1853
+ grip.addEventListener('dblclick', function(){ resetHeight(); });
1854
+ }
1855
+
1856
+ // ---- send / stop button ------------------------------------------------
1857
+ function syncSend(){
1858
+ if(!sendBtn) return;
1859
+ if(busy){
1860
+ sendBtn.disabled = false;
1861
+ sendBtn.dataset.mode = 'stop';
1862
+ sendBtn.setAttribute('aria-label', 'Stop generating');
1863
+ sendBtn.setAttribute('title', 'Stop generating');
1864
+ sendBtn.innerHTML = STOP_ICON;
1865
+ return;
1866
+ }
1867
+ sendBtn.dataset.mode = 'send';
1868
+ sendBtn.setAttribute('aria-label', 'Send');
1869
+ sendBtn.setAttribute('title', 'Send');
1870
+ sendBtn.disabled = !(Boolean((ta && ta.value || '').trim()) || canSendExtra());
1871
+ sendBtn.innerHTML = SEND_ICON;
1872
+ }
1873
+ function setBusy(v){
1874
+ busy = Boolean(v);
1875
+ if(root) root.classList.toggle('is-busy', busy);
1876
+ syncSend();
1877
+ }
1878
+
1879
+ // ---- reference chips ---------------------------------------------------
1880
+ function renderRefs(){
1881
+ if(!refsEl) return;
1882
+ refsEl.hidden = refs.length === 0;
1883
+ refsEl.innerHTML = '';
1884
+ refs.forEach(function(url, idx){
1885
+ var chip = document.createElement('span'); chip.className = 'rk-ip-gen-ref';
1886
+ var im = document.createElement('img'); im.src = url; im.alt = ''; im.loading = 'lazy'; chip.appendChild(im);
1887
+ var lbl = document.createElement('span');
1888
+ var nm = url.split('/').pop() || 'reference';
1889
+ lbl.textContent = decodeURIComponent(nm).slice(0, 28);
1890
+ chip.appendChild(lbl);
1891
+ var x = document.createElement('button'); x.type = 'button';
1892
+ x.setAttribute('aria-label', 'Remove reference'); x.innerHTML = '&times;';
1893
+ x.addEventListener('click', function(){ refs.splice(idx, 1); renderRefs(); });
1894
+ chip.appendChild(x);
1895
+ refsEl.appendChild(chip);
1896
+ });
1897
+ }
1898
+
1899
+ // Paste or drop image files straight into the composer -> temp upload -> ref.
1900
+ function addFiles(fileList){
1901
+ var files = Array.prototype.slice.call(fileList || []).filter(isImageFile);
1902
+ if(!files.length) return Promise.resolve(0);
1903
+ status('Uploading ' + plural(files.length, 'reference') + '\u2026', '');
1904
+ var added = 0;
1905
+ return files.reduce(function(chain, f){
1906
+ return chain.then(function(){
1907
+ return uploadBlob(f, f.name || 'pasted-image.png', f.type || 'image/png').then(function(url){
1908
+ if(url && refs.indexOf(url) === -1){ refs.push(url); added++; renderRefs(); }
1909
+ });
1910
+ });
1911
+ }, Promise.resolve()).then(function(){
1912
+ if(added) status(plural(added, 'reference') + ' attached.', 'success');
1913
+ return added;
1914
+ }).catch(function(err){
1915
+ status((err && err.message) ? err.message : 'Upload failed.', 'error');
1916
+ return added;
1917
+ });
1918
+ }
1919
+
1920
+ // ---- attach pop panel (shared file-directory explorer) -----------------
1921
+ var modal = null;
1922
+ function onEsc(e){ if(e.key === 'Escape') closePanel(); }
1923
+ function closePanel(){
1924
+ if(!modal) return;
1925
+ try{ modal._handle && modal._handle.destroy && modal._handle.destroy(); }catch(_){}
1926
+ modal.remove(); modal = null;
1927
+ document.removeEventListener('keydown', onEsc, true);
1928
+ }
1929
+ function openPanel(){
1930
+ if(modal) return;
1931
+ var overlay = document.createElement('div'); overlay.className = 'rk-ip-filemodal';
1932
+ var panel = document.createElement('div'); panel.className = 'rk-ip-filemodal-panel';
1933
+ var head = document.createElement('div'); head.className = 'rk-ip-filemodal-head';
1934
+ var title = document.createElement('span'); title.className = 'rk-ip-filemodal-title';
1935
+ title.textContent = cfg.attachTitle || 'Attach reference images';
1936
+ var actions = document.createElement('div'); actions.className = 'rk-ip-filemodal-actions';
1937
+ var uploadBtn = document.createElement('button'); uploadBtn.type = 'button';
1938
+ uploadBtn.className = 'rk-ip-filemodal-upload'; uploadBtn.textContent = '\u2191 Upload from computer';
1939
+ var uploadInput = document.createElement('input'); uploadInput.type = 'file';
1940
+ uploadInput.accept = 'image/png,image/jpeg,image/webp,image/gif,image/avif';
1941
+ uploadInput.multiple = true; uploadInput.hidden = true;
1942
+ var close = document.createElement('button'); close.type = 'button';
1943
+ close.className = 'rk-ip-filemodal-close'; close.setAttribute('aria-label', 'Close'); close.innerHTML = '&times;';
1944
+ actions.appendChild(uploadBtn); actions.appendChild(close);
1945
+ head.appendChild(title); head.appendChild(actions); head.appendChild(uploadInput);
1946
+ uploadBtn.addEventListener('click', function(){ if(!uploadBtn.disabled) uploadInput.click(); });
1947
+ uploadInput.addEventListener('change', function(){
1948
+ var files = Array.prototype.slice.call(uploadInput.files || []).filter(isImageFile);
1949
+ uploadInput.value = '';
1950
+ if(!files.length){ status('Pick an image file to upload as a reference.', 'error'); return; }
1951
+ var original = uploadBtn.textContent;
1952
+ uploadBtn.disabled = true; uploadBtn.textContent = 'Uploading\u2026';
1953
+ addFiles(files).then(function(added){
1954
+ uploadBtn.disabled = false; uploadBtn.textContent = original;
1955
+ if(added) closePanel();
1956
+ else status('Those files were already attached.', '');
1957
+ });
1958
+ });
1959
+ var mountEl = document.createElement('div'); mountEl.className = 'rk-ip-filemodal-mount';
1960
+ panel.appendChild(head); panel.appendChild(mountEl); overlay.appendChild(panel);
1961
+ document.body.appendChild(overlay); modal = overlay;
1962
+ close.addEventListener('click', closePanel);
1963
+ overlay.addEventListener('click', function(e){ if(e.target === overlay) closePanel(); });
1964
+ document.addEventListener('keydown', onEsc, true);
1965
+ if(window.__vidfarmDirectory && window.__vidfarmDirectory.mount){
1966
+ modal._handle = window.__vidfarmDirectory.mount(mountEl, {
1967
+ mode: 'attach', showMultiSelect: true, primaryClick: 'attach',
1968
+ onAttach: function(items){
1969
+ var added = 0;
1970
+ (items || []).forEach(function(it){
1971
+ var ct = it.contentType || '';
1972
+ var isImg = ct.indexOf('image/') === 0 || /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(it.name || '');
1973
+ if(it.viewUrl && isImg && refs.indexOf(it.viewUrl) === -1){ refs.push(it.viewUrl); added++; }
1974
+ });
1975
+ renderRefs();
1976
+ if(added){ status(plural(added, 'reference') + ' attached.', 'success'); closePanel(); }
1977
+ else status('Pick an image file to use as a reference.', 'error');
1978
+ }
1979
+ });
1980
+ } else {
1981
+ mountEl.innerHTML = '<p style="padding:16px;color:var(--rk-text-muted);font-size:13px">File browser is unavailable right now.</p>';
1982
+ }
1983
+ }
1984
+
1985
+ // ---- wiring ------------------------------------------------------------
1986
+ if(ta){
1987
+ ta.addEventListener('input', function(){ autoGrow(); syncSend(); });
1988
+ ta.addEventListener('keydown', function(e){
1989
+ if(e.key !== 'Enter' || e.shiftKey) return;
1990
+ e.preventDefault();
1991
+ // Enter is ignored mid-turn so an impatient double-send cannot queue a
1992
+ // second job; the draft stays put.
1993
+ if(busy) return;
1994
+ if(cfg.onSend) cfg.onSend();
1995
+ });
1996
+ ta.addEventListener('paste', function(e){
1997
+ var imgs = imagesFromClipboard(e);
1998
+ if(imgs.length){ e.preventDefault(); addFiles(imgs); }
1999
+ });
2000
+ }
2001
+ if(clipBtn) clipBtn.addEventListener('click', openPanel);
2002
+ if(sendBtn) sendBtn.addEventListener('click', function(){
2003
+ if(busy){ if(cfg.onStop) cfg.onStop(); return; }
2004
+ if(cfg.onSend) cfg.onSend();
2005
+ });
2006
+ if(root){
2007
+ ['dragenter', 'dragover'].forEach(function(ev){
2008
+ root.addEventListener(ev, function(e){
2009
+ if(dtHasFiles(e)){ e.preventDefault(); e.stopPropagation(); root.classList.add('is-dragref'); }
2010
+ });
2011
+ });
2012
+ ['dragleave', 'dragend'].forEach(function(ev){
2013
+ root.addEventListener(ev, function(){ root.classList.remove('is-dragref'); });
2014
+ });
2015
+ root.addEventListener('drop', function(e){
2016
+ if(!dtHasFiles(e)) return;
2017
+ e.preventDefault(); e.stopPropagation();
2018
+ root.classList.remove('is-dragref');
2019
+ addFiles(e.dataTransfer.files);
2020
+ });
2021
+ }
2022
+
2023
+ autoGrow();
2024
+ syncSend();
2025
+
2026
+ return {
2027
+ refs: function(){ return refs.slice(); },
2028
+ setRefs: function(list){ refs = (list || []).slice(); renderRefs(); },
2029
+ clearRefs: function(){ refs = []; renderRefs(); },
2030
+ addFiles: addFiles,
2031
+ renderRefs: renderRefs,
2032
+ autoGrow: autoGrow,
2033
+ resetHeight: resetHeight,
2034
+ setBusy: setBusy,
2035
+ isBusy: function(){ return busy; },
2036
+ syncSend: syncSend,
2037
+ closePanel: closePanel,
2038
+ setPlaceholder: function(s){ if(ta) ta.placeholder = s; }
2039
+ };
2040
+ };
2041
+ })();
2042
+
1686
2043
  (function(){
1687
2044
  var videoEl = document.querySelector("[data-ipv-video]");
1688
2045
  var canvas = document.querySelector("[data-ipv-canvas]");
@@ -1743,6 +2100,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1743
2100
  var PLAY_SVG = "<svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M7 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 7 5.5Z\"/></svg>";
1744
2101
  var PAUSE_SVG = "<svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\"/><rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\"/></svg>";
1745
2102
  var CLOCK_SVG = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M10 6v4l2.6 1.6\"/></svg>";
2103
+ var EXPAND_SVG = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3h5v5\"/><path d=\"M8 17H3v-5\"/><path d=\"M17 3l-6 6\"/><path d=\"M3 17l6-6\"/></svg>";
2104
+ var SAVE_SVG = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 4h8l2 2v10H5z\"/><path d=\"M8 4v4h4V4\"/><path d=\"M8 12h4\"/></svg>";
2105
+ var DL_SVG = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3v9\"/><path d=\"m6.5 8.5 3.5 3.5 3.5-3.5\"/><path d=\"M4 16h12\"/></svg>";
2106
+ var COPY_SVG = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1\"/><path d=\"M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2\"/></svg>";
1746
2107
 
1747
2108
  // ===== state =====
1748
2109
  var sourceVideoUrl = ""; // playable URL (may be a blob: object URL for uploads)
@@ -2010,6 +2371,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2010
2371
  videoEl.onerror = function(){ if(!crossOriginRetried && videoEl.getAttribute("crossorigin")){ crossOriginRetried = true; videoEl.removeAttribute("crossorigin"); videoEl.src = url; videoEl.load(); } };
2011
2372
  videoEl.src = url;
2012
2373
  videoEl.load();
2374
+ ensureSourceTurn(url, name || "Video");
2013
2375
  setStatus("Video ready. Scrub the timeline and paint timed masks.", "success");
2014
2376
  }
2015
2377
  function loadSourceFile(file){
@@ -2022,6 +2384,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2022
2384
  videoEl.src = sourceObjectUrl;
2023
2385
  videoEl.addEventListener("loadedmetadata", function(){ syncCanvasSize(); if(durationEl) durationEl.textContent = "/ " + formatTimecode(videoEl.duration || 0); redraw(); }, { once: true });
2024
2386
  videoEl.load();
2387
+ ensureSourceTurn(sourceObjectUrl, file.name || "Uploaded video");
2025
2388
  setStatus("Video loaded. Scrub the timeline and paint timed masks.", "success");
2026
2389
  }
2027
2390
 
@@ -2243,17 +2606,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2243
2606
  var rows = (entries || []).filter(isVideoEntry)
2244
2607
  .sort(function(a, b){ return Date.parse(a.createdAt || 0) - Date.parse(b.createdAt || 0); });
2245
2608
  if(!rows.length){ setStatus("Opened " + resolveTracer() + " \u2014 no messages in it yet.", ""); return; }
2246
- var last = rows[rows.length - 1];
2247
- var text = userTextFromEntry(last);
2248
- if(text && globalPrompt && !globalPrompt.value.trim()) globalPrompt.value = text;
2249
- // Newest output of the session becomes the visible result.
2250
- for(var i = rows.length - 1; i >= 0; i--){
2251
- var urls = historyEntryUrls(rows[i]);
2252
- if(urls[0]){
2253
- try { resultVideo.src = absoluteUrl(urls[0]); outputPanel.hidden = false; } catch(_){}
2254
- break;
2255
- }
2609
+ // Replay the whole thread: each history entry is one user message and, when
2610
+ // it produced output, the clip that answered it.
2611
+ chatTurns = []; activeTurnId = ""; turnSeq = 0;
2612
+ var newestUrl = "";
2613
+ rows.forEach(function(row){
2614
+ var msg = userTextFromEntry(row);
2615
+ if(msg) chatTurns.push({ id: nextTurnId(), at: row.createdAt || nowIso(), role: "user", kind: "text", text: msg, refs: [], isEdit: false, masked: false });
2616
+ var urls = historyEntryUrls(row);
2617
+ if(!urls[0]) return;
2618
+ var abs; try { abs = absoluteUrl(urls[0]); } catch(_){ return; }
2619
+ newestUrl = abs;
2620
+ chatTurns.push({ id: nextTurnId(), at: row.createdAt || nowIso(), role: "assistant", kind: "media", url: abs });
2621
+ });
2622
+ if(newestUrl){
2623
+ try { resultVideo.src = newestUrl; outputPanel.hidden = false; } catch(_){}
2624
+ // Make it the source so the next message edits it and it gets the player.
2625
+ loadSourceUrl(newestUrl, "Session video");
2256
2626
  }
2627
+ renderChat();
2257
2628
  setStatus("Session restored \u2014 " + rows.length + " message" + (rows.length === 1 ? "" : "s") + ". Keep editing to pick up where you left off.", "success");
2258
2629
  }
2259
2630
  function loadHistory(){
@@ -2310,35 +2681,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2310
2681
  if(frames.last && frames.last.url) arr.push({ image_url: frames.last.url, frame_type: "last_frame" });
2311
2682
  return arr;
2312
2683
  }
2313
- function setSubmitLoading(loading){ submitBtn.setAttribute("data-loading", loading ? "true" : "false"); submitBtn.disabled = loading; if(submitLabel) submitLabel.textContent = loading ? "Generating\u2026" : "Generate video"; }
2314
-
2315
- function submitGenerate(){
2316
- if(submitBtn.disabled) return;
2317
- var instruction = buildVideoInstruction();
2318
- if(!instruction){ setStatus("Add a video prompt or a mask instruction first.", "error"); return; }
2319
- var frameImgs = frameImagesPayload();
2320
- setSubmitLoading(true);
2321
- var seed = (globalPrompt && globalPrompt.value.trim()) || (masks.find(function(m){ return String(m.prompt || "").trim(); }) || {}).prompt || "video-edit";
2322
- var tracer = tracerAutoPending ? nameTracerFromSeed(seed) : getTracerValue();
2323
- // The message the USER actually wrote (not the compiled instruction). It rides
2324
- // along on the primitive envelope as chat_message — zod strips it server-side,
2325
- // but the api-call-history middleware records the raw body, so past sessions can
2326
- // be titled/replayed by their conversation exactly like /tools/image.
2327
- var chatMessage = (globalPrompt && globalPrompt.value.trim())
2328
- || (masks.map(function(m){ return String(m.prompt || "").trim(); }).filter(Boolean)[0] || "");
2329
- var payload = { prompt: instruction, output_format: "mp4", aspect_ratio: getAspectRatio() };
2330
- if(frameImgs.length) payload.frame_images = frameImgs;
2331
- if(providerSelect && providerSelect.value) payload.provider = providerSelect.value;
2332
- if(durationSelect && durationSelect.value) payload.duration = Number(durationSelect.value);
2333
- if(audioToggle && audioToggle.checked) payload.generate_audio = true;
2334
- setStatus("Creating video job\u2026", "");
2335
- fetch("/api/v1/primitives/videos/generate", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ tracer: tracer, payload: payload, chat_message: chatMessage, chat_mode: "video", chat_masked: masks.length > 0 }) })
2336
- .then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); })
2337
- .then(function(res){ if(!res.ok || !res.p.job_id) throw new Error(res.p.error || "Unable to create video job."); setStatus("Video job " + res.p.job_id + " started \u2014 generating\u2026", "success"); upsertHistory(makePending(res.p)); return pollJob(res.p.job_id); })
2338
- .then(function(){ loadHistory(); loadSessions(); })
2339
- .catch(function(err){ setStatus(err && err.message ? err.message : "Unable to create video.", "error"); })
2340
- .then(function(){ setSubmitLoading(false); });
2341
- }
2342
2684
 
2343
2685
  // ===== lightbox =====
2344
2686
  function openVideoPreview(url, label, downloadName){
@@ -2415,6 +2757,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2415
2757
  ["first","last"].forEach(function(w){ var img = document.querySelector('[data-ipv-frame-img="' + w + '"]'); var ph = document.querySelector('[data-ipv-frame-ph="' + w + '"]'); if(img){ img.hidden = true; img.removeAttribute("src"); } if(ph) ph.style.display = ""; });
2416
2758
  if(globalPrompt) globalPrompt.value = ""; attachments = []; renderAttachChips();
2417
2759
  outputPanel.hidden = true; resultVideo.removeAttribute("src");
2760
+ chatTurns = []; activeTurnId = ""; turnSeq = 0; renderChat();
2418
2761
  emptyState.hidden = false; if(player) player.hidden = true;
2419
2762
  canvas.width = 1; canvas.height = 1; renderMasks();
2420
2763
  if(scrub) scrub.value = "0"; if(timecodeInput) timecodeInput.value = "00:00:000"; if(durationEl) durationEl.textContent = "/ 00:00:000";
@@ -2462,7 +2805,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2462
2805
  if(redoBtn) redoBtn.addEventListener("click", function(){ var m = activeMask(); if(m && m.redo.length){ m.strokes.push(m.redo.pop()); redraw(); syncUndoRedo(); } });
2463
2806
  if(clearBtn) clearBtn.addEventListener("click", function(){ var m = activeMask(); if(m){ m.redo = m.strokes.concat(m.redo); m.strokes = []; redraw(); syncUndoRedo(); } });
2464
2807
  if(attachBtn && attachInput){ attachBtn.addEventListener("click", function(){ attachInput.click(); }); attachInput.addEventListener("change", function(){ addAttachmentFiles(attachInput.files); attachInput.value = ""; }); }
2465
- if(submitBtn) submitBtn.addEventListener("click", submitGenerate);
2466
2808
 
2467
2809
  masksList.addEventListener("click", function(event){
2468
2810
  var t = event.target; if(!(t instanceof Element)) return;
@@ -2548,94 +2890,345 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2548
2890
  }
2549
2891
 
2550
2892
  // ===== text-to-video composer =====
2893
+ // ===== chat feed + shared composer =====
2551
2894
  var genPrompt = document.getElementById("rk-ipv-genprompt");
2552
2895
  var genClip = document.getElementById("rk-ipv-genclip");
2553
2896
  var genSend = document.getElementById("rk-ipv-gensend");
2554
2897
  var genRefsEl = document.getElementById("rk-ipv-genrefs");
2555
2898
  var genStatusEl = document.getElementById("rk-ipv-genstatus");
2556
- var genWrap = document.getElementById("rk-ipv-gen");
2557
- var GEN_SEND_HTML = genSend ? genSend.innerHTML : "";
2558
- var genRefs = []; var genBusy = false; var genFileModal = null;
2559
- function setGenStatus(m, t){ if(!genStatusEl) return; genStatusEl.textContent = m || ""; genStatusEl.setAttribute("data-tone", t || ""); }
2560
- function syncGenSend(){ if(genPrompt) genPrompt.disabled = genBusy; if(genClip) genClip.disabled = genBusy; if(!genSend) return; if(genBusy){ genSend.classList.add("is-busy"); genSend.innerHTML = "<span class='rk-ip-gen-spin'></span>"; } else { genSend.classList.remove("is-busy"); genSend.innerHTML = GEN_SEND_HTML; } genSend.disabled = genBusy || !(genPrompt && genPrompt.value.trim()); }
2561
- function renderGenRefs(){ if(!genRefsEl) return; genRefsEl.innerHTML = ""; if(!genRefs.length){ genRefsEl.hidden = true; return; } genRefsEl.hidden = false; genRefs.forEach(function(url, idx){ var chip = document.createElement("span"); chip.className = "rk-ip-gen-ref"; var img = document.createElement("img"); img.src = url; img.alt = ""; chip.appendChild(img); var label = document.createElement("span"); label.textContent = sourceNameFromUrl(url); chip.appendChild(label); var x = document.createElement("button"); x.type = "button"; x.textContent = "\u00d7"; x.addEventListener("click", function(){ genRefs.splice(idx, 1); renderGenRefs(); }); chip.appendChild(x); genRefsEl.appendChild(chip); }); }
2562
- function closeGenFilePanel(){ if(genFileModal){ try { genFileModal._handle && genFileModal._handle.destroy && genFileModal._handle.destroy(); } catch(_){} genFileModal.remove(); genFileModal = null; document.removeEventListener("keydown", onGenFileEsc, true); } }
2563
- function onGenFileEsc(e){ if(e.key === "Escape") closeGenFilePanel(); }
2564
- function openGenFilePanel(){
2565
- var dir = window.__vidfarmDirectory; if(!dir || !dir.pick){ setGenStatus("File browser isn't available right now.", "error"); return; }
2566
- dir.pick({ select: "file", roots: ["files","temp","raws"], title: "Attach reference images", hint: "Pick one or more images to guide the generation \u2014 or drop / upload them from your computer.", confirmLabel: "Attach", multiple: true, allowUpload: true,
2567
- onPick: function(picked){ var arr = Array.isArray(picked) ? picked : [picked]; arr.forEach(function(f){ var url = f && (f.url || f.viewUrl); if(url){ try { genRefs.push(absoluteUrl(url)); } catch(_){} } }); renderGenRefs(); } });
2568
- }
2569
- // Paste or drop image files straight into the composer -> temp upload -> reference.
2570
- function addGenReferenceFiles(fileList){
2571
- var files = Array.prototype.slice.call(fileList || []).filter(function(f){ return /^image\//i.test(f.type || "") || /\.(png|jpe?g|gif|webp|avif)$/i.test(f.name || ""); });
2572
- if(!files.length) return Promise.resolve(0);
2573
- setGenStatus("Uploading " + files.length + " reference" + (files.length > 1 ? "s" : "") + "\u2026", "");
2574
- var added = 0;
2575
- return files.reduce(function(chain, f){
2576
- return chain.then(function(){
2577
- return uploadTemporaryBlob(f, f.name || ("pasted-image.png"), f.type || "image/png").then(function(uploaded){
2578
- if(uploaded){ var url = absoluteUrl(uploaded); if(genRefs.indexOf(url) === -1){ genRefs.push(url); added++; renderGenRefs(); } }
2579
- });
2580
- });
2581
- }, Promise.resolve()).then(function(){
2582
- if(added) setGenStatus(added + " reference" + (added > 1 ? "s" : "") + " attached.", "success");
2583
- return added;
2584
- }).catch(function(err){ setGenStatus((err && err.message) ? err.message : "Upload failed.", "error"); return added; });
2899
+ var composerEl = document.querySelector("[data-ip-composer]");
2900
+ var chatFeed = document.querySelector("[data-ipv-feed]");
2901
+ var chatEmpty = document.querySelector("[data-ipv-empty]");
2902
+ var canvasHolder = document.querySelector("[data-ipv-canvas-holder]");
2903
+ var playerStack = document.querySelector("[data-ipv-player]");
2904
+ var compStatusEl = document.querySelector("[data-ipv-comp-status]");
2905
+
2906
+ function setGenStatus(m, t){ setStatus(m, t); }
2907
+ function maskHasInstruction(){ return masks.some(function(m){ return String(m.prompt || "").trim() && (m.strokes || []).length; }); }
2908
+
2909
+ // The composer is the shared implementation from chat-composer.ts the exact
2910
+ // same auto-grow, drag-to-resize grip, chips, paste/drop and attach panel as
2911
+ // /tools/image.
2912
+ var composer = window.__rkChatComposer({
2913
+ root: composerEl,
2914
+ textarea: genPrompt,
2915
+ refsEl: genRefsEl,
2916
+ sendBtn: genSend,
2917
+ clipBtn: genClip,
2918
+ attachTitle: "Attach reference images",
2919
+ uploadBlob: function(f, name, type){ return uploadTemporaryBlob(f, name, type); },
2920
+ onStatus: function(m, t){ setStatus(m, t); },
2921
+ canSend: function(){ return maskHasInstruction(); },
2922
+ onSend: function(){ sendChatMessage(); },
2923
+ onStop: function(){ stopActiveTurn(); }
2924
+ });
2925
+ // ---- quick actions -----------------------------------------------------
2926
+ // Upload Video swaps the clip being edited; Upload Storyboard is just a
2927
+ // reference image (it rides along as input_references, it does NOT split the
2928
+ // clip into per-panel shots).
2929
+ var quickVideoBtn = document.querySelector("[data-ipv-quick-video]");
2930
+ var quickStoryBtn = document.querySelector("[data-ipv-quick-storyboard]");
2931
+ var storyboardInput = document.querySelector("[data-ipv-storyboard-input]");
2932
+ if(quickVideoBtn && fileInput) quickVideoBtn.addEventListener("click", function(){ fileInput.click(); });
2933
+ if(quickStoryBtn && storyboardInput) quickStoryBtn.addEventListener("click", function(){ storyboardInput.click(); });
2934
+ if(storyboardInput) storyboardInput.addEventListener("change", function(){
2935
+ var files = Array.prototype.slice.call(storyboardInput.files || []);
2936
+ storyboardInput.value = "";
2937
+ if(!files.length) return;
2938
+ // Same funnel as paste/drop — upload, then show as a composer chip.
2939
+ composer.addFiles(files);
2940
+ });
2941
+
2942
+ function renderGenRefs(){ composer.renderRefs(); }
2943
+ function syncGenSend(){ composer.syncSend(); }
2944
+
2945
+ // ===== turn model =====
2946
+ var chatTurns = [];
2947
+ var activeTurnId = "";
2948
+ var turnSeq = 0;
2949
+ function nextTurnId(){ turnSeq += 1; return "t" + turnSeq; }
2950
+ function addTurn(turn){
2951
+ var item = Object.assign({ id: nextTurnId(), at: nowIso(), role: "assistant" }, turn);
2952
+ chatTurns.push(item); renderChat(); return item;
2953
+ }
2954
+ function updateTurn(id, patch){
2955
+ if(!id) return;
2956
+ chatTurns = chatTurns.map(function(t){ return t.id === id ? Object.assign({}, t, patch) : t; });
2957
+ renderChat();
2958
+ }
2959
+ // Patch pending labels in place — a full re-render would restart a clip that is
2960
+ // playing further up the feed.
2961
+ function updatePendingLabel(id, label){
2962
+ if(!id) return;
2963
+ chatTurns = chatTurns.map(function(t){ return t.id === id ? Object.assign({}, t, { label: label }) : t; });
2964
+ var node = chatFeed && chatFeed.querySelector("[data-pending-id='" + id + "']");
2965
+ if(node) node.textContent = label;
2966
+ }
2967
+ function resolveActiveTurn(patch){
2968
+ if(activeTurnId){ updateTurn(activeTurnId, patch); activeTurnId = ""; return; }
2969
+ addTurn(Object.assign({ role: "assistant" }, patch));
2970
+ }
2971
+ function turnById(id){ return chatTurns.filter(function(t){ return t.id === id; })[0] || null; }
2972
+ // A clip that arrives WITHOUT a message (an upload, a ?source_video_url deep
2973
+ // link) still needs a bubble — renderChat only hosts the player inside a media
2974
+ // turn whose url is the current source.
2975
+ function ensureSourceTurn(url, name){
2976
+ if(!url) return;
2977
+ var already = chatTurns.some(function(t){ return t.kind === "media" && t.url === url; });
2978
+ if(already){ renderChat(); return; }
2979
+ addTurn({ role: "assistant", kind: "media", url: url, note: name || "" });
2980
+ }
2981
+
2982
+ // ===== stop =====
2983
+ var activeJob = { kind: "", jobId: "" };
2984
+ var stopRequested = false;
2985
+ function stopError(){ var e = new Error("__vidfarm_stopped__"); e.stopped = true; return e; }
2986
+ function throwIfStopped(){ if(stopRequested) throw stopError(); }
2987
+ function stopActiveTurn(){
2988
+ if(!composer.isBusy() || stopRequested) return;
2989
+ stopRequested = true;
2990
+ setStatus("Stopping\u2026", "");
2991
+ if(activeTurnId) updatePendingLabel(activeTurnId, "Stopping\u2026");
2992
+ if(activeJob.kind && activeJob.jobId){
2993
+ fetch("/api/v1/primitives/" + activeJob.kind + "/jobs/" + encodeURIComponent(activeJob.jobId) + "/cancel", {
2994
+ method: "POST", credentials: "same-origin"
2995
+ }).catch(function(){});
2996
+ }
2585
2997
  }
2586
- function imagesFromClipboard(e){
2587
- var items = (e.clipboardData && e.clipboardData.items) || []; var out = [];
2588
- for(var i = 0; i < items.length; i++){ if(items[i].kind === "file"){ var f = items[i].getAsFile(); if(f && /^image\//i.test(f.type || "")) out.push(f); } }
2589
- return out;
2998
+
2999
+ // ===== feed rendering =====
3000
+ function mediaActionsRow(turn){
3001
+ var row = document.createElement("div");
3002
+ row.className = "rk-ip-media-actions";
3003
+ var full = document.createElement("button");
3004
+ full.type = "button"; full.className = "rk-ip-act"; full.dataset.ipvOpen = turn.id;
3005
+ full.setAttribute("aria-label", "Open fullscreen"); full.title = "Fullscreen";
3006
+ full.innerHTML = EXPAND_SVG;
3007
+ var download = document.createElement("a");
3008
+ download.className = "rk-ip-act"; download.href = turn.url; download.target = "_blank"; download.rel = "noopener noreferrer";
3009
+ download.setAttribute("download", videoDownloadName("video-chat", turn.at));
3010
+ download.setAttribute("aria-label", "Download this clip"); download.title = "Download";
3011
+ download.innerHTML = DL_SVG;
3012
+ var copy = document.createElement("button");
3013
+ copy.type = "button"; copy.className = "rk-ip-act"; copy.dataset.ipvCopy = turn.id;
3014
+ copy.setAttribute("aria-label", "Copy the file URL"); copy.title = "Copy URL";
3015
+ copy.innerHTML = COPY_SVG;
3016
+ var save = document.createElement("button");
3017
+ save.type = "button"; save.className = "rk-ip-act"; save.dataset.ipvSave = turn.id;
3018
+ save.setAttribute("aria-label", "Save to My Files"); save.title = "Save to My Files";
3019
+ save.innerHTML = SAVE_SVG;
3020
+ row.append(full, download, copy, save);
3021
+ return row;
2590
3022
  }
2591
- function dtHasFiles(e){ var t = (e.dataTransfer && e.dataTransfer.types) || []; return Array.prototype.indexOf.call(t, "Files") !== -1; }
2592
- function pollGenerateJob(jobId){
2593
- var attempt = 0;
2594
- function step(){
2595
- return fetch("/api/v1/primitives/video_generate/jobs/" + encodeURIComponent(jobId), { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
2596
- var payload = res.p; if(!res.ok) throw new Error(payload.error || "Unable to poll job.");
2597
- if(payload.status === "failed" || payload.status === "cancelled") throw new Error((payload.error && payload.error.message) || ("Video job " + payload.status + "."));
2598
- var out = extractVideoUrl(payload);
2599
- if(payload.status === "succeeded" || payload.status === "complete" || out){ if(!out) throw new Error("Completed without an output URL."); return absoluteUrl(out); }
2600
- setGenStatus("Generating video (" + (payload.status || "running") + ")\u2026 this can take a few minutes", "");
2601
- attempt++; if(attempt >= 120){ setGenStatus("Still running \u2014 check History shortly.", ""); return ""; }
2602
- return new Promise(function(res2){ setTimeout(res2, attempt < 8 ? 2000 : 4000); }).then(step);
2603
- });
3023
+
3024
+ function renderChat(){
3025
+ if(!chatFeed) return;
3026
+ // Park the player before the innerHTML rebuild, or it would be destroyed.
3027
+ if(playerStack && playerStack.parentElement !== canvasHolder && canvasHolder) canvasHolder.appendChild(playerStack);
3028
+ var wasNearBottom = (chatFeed.scrollHeight - chatFeed.scrollTop - chatFeed.clientHeight) < 120;
3029
+ chatFeed.innerHTML = "";
3030
+ if(!chatTurns.length){
3031
+ if(chatEmpty){ chatEmpty.hidden = false; chatFeed.appendChild(chatEmpty); }
3032
+ return;
2604
3033
  }
2605
- return step();
3034
+ if(chatEmpty) chatEmpty.hidden = true;
3035
+ // The newest clip that IS the current source gets the scrubbable player, so
3036
+ // timed masks are always painted on the clip the next message will edit.
3037
+ var latestPlayableId = "";
3038
+ for(var i = chatTurns.length - 1; i >= 0; i--){
3039
+ var c = chatTurns[i];
3040
+ if(c.kind === "media" && c.url && c.url === sourceVideoUrl){ latestPlayableId = c.id; break; }
3041
+ }
3042
+ chatTurns.forEach(function(turn){
3043
+ var row = document.createElement("div");
3044
+ row.className = "rk-ip-turn";
3045
+ row.dataset.role = turn.role; row.dataset.kind = turn.kind; row.dataset.turnId = turn.id;
3046
+ if(turn.role === "user"){
3047
+ if(turn.refs && turn.refs.length){
3048
+ var refs = document.createElement("div"); refs.className = "rk-ip-turn-refs";
3049
+ turn.refs.forEach(function(url){ var im = document.createElement("img"); im.src = url; im.alt = ""; im.loading = "lazy"; refs.appendChild(im); });
3050
+ row.appendChild(refs);
3051
+ }
3052
+ var tag = document.createElement("span");
3053
+ tag.className = "rk-ip-turn-tag";
3054
+ tag.textContent = turn.masked ? "Masked edit" : (turn.isEdit ? "Edit" : "New video");
3055
+ row.appendChild(tag);
3056
+ var bubble = document.createElement("div"); bubble.className = "rk-ip-bubble";
3057
+ bubble.textContent = turn.text || ""; row.appendChild(bubble);
3058
+ } else if(turn.kind === "pending"){
3059
+ var pending = document.createElement("div"); pending.className = "rk-ip-pending";
3060
+ pending.dataset.pendingId = turn.id; pending.textContent = turn.label || "Working\u2026";
3061
+ row.appendChild(pending);
3062
+ } else if(turn.kind === "error" || turn.kind === "stopped"){
3063
+ var err = document.createElement("div"); err.className = "rk-ip-bubble";
3064
+ err.textContent = turn.text || "Something went wrong."; row.appendChild(err);
3065
+ } else if(turn.kind === "media" && turn.url){
3066
+ var media = document.createElement("div"); media.className = "rk-ip-media";
3067
+ var isLatest = turn.id === latestPlayableId;
3068
+ var frame = document.createElement("div");
3069
+ frame.className = "rk-ip-media-frame" + (isLatest ? " is-latest" : "");
3070
+ if(isLatest && playerStack){
3071
+ playerStack.hidden = false;
3072
+ frame.appendChild(playerStack);
3073
+ } else {
3074
+ var v = document.createElement("video");
3075
+ v.src = turn.url; v.controls = true; v.playsInline = true; v.preload = "metadata";
3076
+ frame.appendChild(v);
3077
+ }
3078
+ media.appendChild(frame);
3079
+ var actions = mediaActionsRow(turn);
3080
+ var note = document.createElement("span"); note.className = "rk-ip-act-note";
3081
+ note.textContent = isLatest
3082
+ ? (masks.length ? "Newest \u00b7 timed masks active" : "Newest \u00b7 scrub and paint a timed mask")
3083
+ : (turn.note || "");
3084
+ if(note.textContent) actions.appendChild(note);
3085
+ media.appendChild(actions);
3086
+ row.appendChild(media);
3087
+ } else {
3088
+ var text = document.createElement("div"); text.className = "rk-ip-bubble";
3089
+ text.textContent = turn.text || ""; row.appendChild(text);
3090
+ }
3091
+ chatFeed.appendChild(row);
3092
+ });
3093
+ if(wasNearBottom) chatFeed.scrollTop = chatFeed.scrollHeight;
2606
3094
  }
2607
- function submitGenerateNew(){
2608
- var p = (genPrompt && genPrompt.value || "").trim();
2609
- if(!p){ setGenStatus("Describe the video to generate.", "error"); if(genPrompt) genPrompt.focus(); return; }
2610
- if(genBusy) return; genBusy = true; syncGenSend(); if(genWrap) genWrap.classList.add("is-busy");
2611
- setGenStatus("Creating video\u2026", "");
2612
- var payload = { prompt: p, output_format: "mp4" };
2613
- var frameImgs = frameImagesPayload(); if(frameImgs.length) payload.frame_images = frameImgs;
2614
- if(genRefs.length) payload.input_references = genRefs.slice();
3095
+
3096
+ if(chatFeed){
3097
+ chatFeed.addEventListener("click", function(event){
3098
+ var target = event.target;
3099
+ if(!(target instanceof Element)) return;
3100
+ var open = target.closest("[data-ipv-open]");
3101
+ if(open instanceof HTMLElement){
3102
+ var t = turnById(open.dataset.ipvOpen || "");
3103
+ if(t && t.url) openVideoPreview(t.url, "Video output", videoDownloadName("video-chat", t.at));
3104
+ return;
3105
+ }
3106
+ var copy = target.closest("[data-ipv-copy]");
3107
+ if(copy instanceof HTMLElement){
3108
+ var ct = turnById(copy.dataset.ipvCopy || "");
3109
+ if(ct && ct.url){ try { navigator.clipboard.writeText(ct.url); setStatus("File URL copied.", "success"); } catch(_){} }
3110
+ return;
3111
+ }
3112
+ var save = target.closest("[data-ipv-save]");
3113
+ if(save instanceof HTMLElement){
3114
+ var st = turnById(save.dataset.ipvSave || "");
3115
+ if(st && st.url) saveMediaToFiles(st.url, videoDownloadName("video-chat", st.at));
3116
+ }
3117
+ });
3118
+ }
3119
+ // ===== send =====
3120
+ // ONE send path for every message, so the page is a single conversation rather
3121
+ // than two disconnected surfaces. With a clip loaded, the message + timed masks
3122
+ // + frames compile into the rich instruction buildVideoInstruction() produces;
3123
+ // with no clip it is plain text->video. Both are POST /videos/generate.
3124
+ function sendChatMessage(){
3125
+ if(composer.isBusy()) return;
3126
+ var text = (genPrompt && genPrompt.value || "").trim();
3127
+ var masked = maskHasInstruction();
3128
+ if(!text && !masked){
3129
+ setStatus("Describe the video you want.", "error");
3130
+ if(genPrompt) genPrompt.focus();
3131
+ return;
3132
+ }
3133
+ var refs = composer.refs();
3134
+ var editing = Boolean(sourceVideoUrl);
3135
+ // The whole-clip prompt field is what buildVideoInstruction() compiles from,
3136
+ // so the composer text becomes that field for this turn.
3137
+ if(globalPrompt) globalPrompt.value = text;
3138
+ var instruction = editing ? (buildVideoInstruction() || text) : text;
3139
+ if(!instruction){ setStatus("Describe the video you want.", "error"); return; }
3140
+
3141
+ addTurn({
3142
+ role: "user", kind: "text",
3143
+ text: text || "Apply the painted mask instructions.",
3144
+ refs: refs, isEdit: editing, masked: masks.length > 0
3145
+ });
3146
+ if(genPrompt){ genPrompt.value = ""; composer.resetHeight(); }
3147
+ composer.clearRefs();
3148
+ stopRequested = false;
3149
+ activeJob = { kind: "", jobId: "" };
3150
+ composer.setBusy(true);
3151
+ var pending = addTurn({ role: "assistant", kind: "pending", label: "Creating video\u2026" });
3152
+ activeTurnId = pending.id;
3153
+ setStatus("Creating video\u2026", "");
3154
+
3155
+ var payload = { prompt: instruction, output_format: "mp4", aspect_ratio: getAspectRatio() };
3156
+ var frameImgs = frameImagesPayload();
3157
+ if(frameImgs.length) payload.frame_images = frameImgs;
3158
+ if(refs.length) payload.input_references = refs;
2615
3159
  if(providerSelect && providerSelect.value) payload.provider = providerSelect.value;
2616
3160
  if(durationSelect && durationSelect.value) payload.duration = Number(durationSelect.value);
2617
3161
  if(audioToggle && audioToggle.checked) payload.generate_audio = true;
2618
- var genTracer = tracerAutoPending ? nameTracerFromSeed(p) : getTracerValue();
2619
- fetch("/api/v1/primitives/videos/generate", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ tracer: genTracer, payload: payload }) })
3162
+ var seed = text || "video-edit";
3163
+ var tracer = tracerAutoPending ? nameTracerFromSeed(seed) : getTracerValue();
3164
+
3165
+ fetch("/api/v1/primitives/videos/generate", {
3166
+ method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" },
3167
+ body: JSON.stringify({ tracer: tracer, payload: payload, chat_message: text, chat_mode: "video", chat_masked: masks.length > 0 })
3168
+ })
2620
3169
  .then(function(r){ return parseJsonResponse(r).then(function(j){ return { ok: r.ok, j: j }; }); })
2621
- .then(function(res){ if(!res.ok || !res.j.job_id) throw new Error(res.j.error || "Unable to start video generation."); upsertHistory(makePending(res.j)); return pollGenerateJob(res.j.job_id); })
2622
- .then(function(out){ if(out){ loadSourceUrl(out, "Generated video"); recordSourceHistoryItem(out, "Generated video"); setGenStatus("Video generated. Scrub and paint timed masks to refine.", "success"); setStatus("Generated video loaded.", "success"); loadHistory(); } })
2623
- .catch(function(err){ var msg = (err && err.message) ? err.message : "Unable to generate video."; if(/401|403|sign in|unauth/i.test(msg)) msg = "Sign in to generate videos."; setGenStatus(msg, "error"); })
2624
- .then(function(){ genBusy = false; if(genWrap) genWrap.classList.remove("is-busy"); syncGenSend(); });
2625
- }
2626
- if(genPrompt){
2627
- genPrompt.addEventListener("input", syncGenSend);
2628
- genPrompt.addEventListener("keydown", function(e){ if(e.key === "Enter" && !e.shiftKey){ e.preventDefault(); submitGenerateNew(); } });
2629
- genPrompt.addEventListener("paste", function(e){ var imgs = imagesFromClipboard(e); if(imgs.length){ e.preventDefault(); addGenReferenceFiles(imgs); } });
2630
- }
2631
- if(genWrap){
2632
- ["dragenter","dragover"].forEach(function(ev){ genWrap.addEventListener(ev, function(e){ if(dtHasFiles(e)){ e.preventDefault(); e.stopPropagation(); genWrap.classList.add("is-dragref"); } }); });
2633
- ["dragleave","dragend"].forEach(function(ev){ genWrap.addEventListener(ev, function(){ genWrap.classList.remove("is-dragref"); }); });
2634
- genWrap.addEventListener("drop", function(e){ if(dtHasFiles(e)){ e.preventDefault(); e.stopPropagation(); genWrap.classList.remove("is-dragref"); addGenReferenceFiles(e.dataTransfer.files); } });
2635
- }
2636
- if(genClip) genClip.addEventListener("click", openGenFilePanel);
2637
- if(genSend) genSend.addEventListener("click", submitGenerateNew);
2638
- renderGenRefs(); syncGenSend();
3170
+ .then(function(res){
3171
+ if(!res.ok || !res.j.job_id) throw new Error(res.j.error || "Unable to start video generation.");
3172
+ throwIfStopped();
3173
+ activeJob = { kind: "video_generate", jobId: String(res.j.job_id) };
3174
+ upsertHistory(makePending(res.j));
3175
+ return pollChatJob(res.j.job_id);
3176
+ })
3177
+ .then(function(out){
3178
+ if(!out) return;
3179
+ // Resolve the pending bubble into the clip FIRST — loadSourceUrl calls
3180
+ // ensureSourceTurn, which would otherwise add a second bubble for the
3181
+ // same clip because this turn is still "pending" at that moment.
3182
+ resolveActiveTurn({ kind: "media", url: out });
3183
+ // The new clip becomes the source, so the NEXT message edits it.
3184
+ loadSourceUrl(out, "Generated video");
3185
+ recordSourceHistoryItem(out, "Generated video");
3186
+ setStatus("Video ready \u2014 scrub and paint a timed mask to refine it.", "success");
3187
+ loadHistory(); loadSessions();
3188
+ })
3189
+ .catch(function(err){
3190
+ if(err && err.stopped){ resolveActiveTurn({ kind: "stopped", text: "Stopped." }); setStatus("Stopped.", ""); return; }
3191
+ var msg = (err && err.message) ? err.message : "Unable to generate video.";
3192
+ if(/401|403|sign in|unauth/i.test(msg)) msg = "Sign in to generate videos.";
3193
+ resolveActiveTurn({ kind: "error", text: msg });
3194
+ setStatus(msg, "error");
3195
+ })
3196
+ .then(function(){ composer.setBusy(false); stopRequested = false; activeJob = { kind: "", jobId: "" }; });
3197
+ }
3198
+
3199
+ // Poll that reports progress into the pending bubble and honours Stop.
3200
+ function pollChatJob(jobId){
3201
+ var attempt = 0;
3202
+ function step(){
3203
+ throwIfStopped();
3204
+ return fetch("/api/v1/primitives/video_generate/jobs/" + encodeURIComponent(jobId), { credentials: "same-origin" })
3205
+ .then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); })
3206
+ .then(function(res){
3207
+ throwIfStopped();
3208
+ var payload = res.p;
3209
+ if(!res.ok) throw new Error(payload.error || "Unable to poll job.");
3210
+ if(payload.status === "failed" || payload.status === "cancelled") throw new Error((payload.error && payload.error.message) || ("Video job " + payload.status + "."));
3211
+ var out = extractVideoUrl(payload);
3212
+ if(payload.status === "succeeded" || payload.status === "complete" || out){
3213
+ if(!out) throw new Error("Completed without an output URL.");
3214
+ applyCompletedJob(payload);
3215
+ return absoluteUrl(out);
3216
+ }
3217
+ upsertHistory(makePending(payload));
3218
+ if(activeTurnId) updatePendingLabel(activeTurnId, "Generating video (" + (payload.status || "running") + ")\u2026 this can take a few minutes");
3219
+ attempt++;
3220
+ if(attempt >= 120){ setStatus("Still running \u2014 check History shortly.", ""); return ""; }
3221
+ return new Promise(function(res2){ setTimeout(res2, attempt < 8 ? 2000 : 4000); }).then(step);
3222
+ });
3223
+ }
3224
+ return step();
3225
+ }
3226
+
3227
+ // The left panel's Generate button now routes through the same conversation.
3228
+ if(submitBtn) submitBtn.addEventListener("click", function(){ sendChatMessage(); });
3229
+ // Everything else on the composer (input, Enter, paste, drag-drop, paperclip,
3230
+ // send/stop) is wired by the shared composer runtime.
3231
+ renderChat();
2639
3232
 
2640
3233
  // ===== boot =====
2641
3234
  renderMasks();