@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.
- package/.agents/skills/editor-capabilities/SKILL.md +52 -0
- package/.agents/skills/vidfarm/SKILL.md +58 -5
- package/.agents/skills/vidfarm/recipes/bulk-scripting-with-a-regime.md +65 -0
- package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +78 -7
- package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +4 -3
- package/.agents/skills/vidfarm/recipes/retheme-template.md +1 -1
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +3 -3
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +92 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +135 -6
- package/.agents/skills/vidfarm/references/hooks-and-virality.md +237 -0
- package/.agents/skills/vidfarm/references/onboarding.md +5 -5
- package/.agents/skills/vidfarm/references/primitives.md +5 -1
- package/.agents/skills/vidfarm/regimes/README.md +77 -0
- package/.agents/skills/vidfarm/regimes/explainer.QA_REGIME.md +82 -0
- package/.agents/skills/vidfarm/regimes/hooks.QA_REGIME.md +117 -0
- package/.agents/skills/vidfarm/regimes/product-demo.QA_REGIME.md +92 -0
- package/.agents/skills/vidfarm/regimes/short-form.QA_REGIME.md +163 -0
- package/.agents/skills/vidfarm/regimes/ugc-testimonial.QA_REGIME.md +82 -0
- package/SKILL.director.md +685 -32
- package/SKILL.md +22 -3
- package/demo/dist/app.js +103 -103
- package/dist/src/cli.js +987 -11
- package/dist/src/devcli/handoff.js +162 -0
- package/dist/src/devcli/interaction-mode.js +154 -0
- package/dist/src/devcli/qa-check.js +593 -0
- package/dist/src/devcli/qa-regime.js +396 -0
- package/dist/src/devcli/sticker-pack.js +396 -0
- package/dist/src/devcli/storyboard.js +243 -0
- package/package.json +8 -1
- package/public/serve-shells/tools-image.html +378 -265
- 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-
|
|
1101
|
-
.rk-inpaint-panel,.rk-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
.rk-ip-
|
|
1207
|
-
.rk-ip-
|
|
1208
|
-
.rk-ip-
|
|
1209
|
-
.rk-ip-
|
|
1210
|
-
.rk-ip-
|
|
1211
|
-
.rk-ip-
|
|
1212
|
-
.rk-ip-
|
|
1213
|
-
.rk-ip-
|
|
1214
|
-
.rk-ip-
|
|
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-
|
|
1222
|
-
.rk-ip-gen-
|
|
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
|
|
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
|
-
/*
|
|
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
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
<
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
<
|
|
1484
|
-
<
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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-
|
|
1495
|
-
<
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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-
|
|
1501
|
-
|
|
1502
|
-
<
|
|
1503
|
-
<
|
|
1504
|
-
|
|
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 = '×';
|
|
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 = '×';
|
|
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
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
var
|
|
2252
|
-
if(
|
|
2253
|
-
|
|
2254
|
-
|
|
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
|
|
2557
|
-
var
|
|
2558
|
-
var
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
function
|
|
2564
|
-
function
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
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
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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
|
-
|
|
2592
|
-
function
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
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
|
|
2619
|
-
|
|
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){
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
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();
|