@officexapp/vidfarm-devcli 0.21.26 → 0.21.28
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 +26 -0
- package/.agents/skills/vidfarm/SKILL.md +31 -9
- package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +3 -2
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +29 -3
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +59 -6
- package/.agents/skills/vidfarm/references/core-workflows.md +1 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +85 -7
- package/.agents/skills/vidfarm/references/onboarding.md +4 -4
- package/.agents/skills/vidfarm/references/primitives.md +33 -1
- package/.agents/skills/vidfarm-media/SKILL.md +3 -1
- package/.agents/skills/vidfarm-media/references/tts.md +1 -1
- package/SKILL.director.md +245 -33
- package/SKILL.md +11 -4
- package/clipper.md +3 -2
- package/dist/src/cli.js +478 -19
- package/dist/src/devcli/cost-mode.js +46 -18
- package/dist/src/devcli/qa-check.js +420 -0
- package/dist/src/devcli/sequence.js +619 -0
- package/dist/src/services/sequence-prompts.js +463 -0
- package/package.json +5 -1
- package/public/assets/file-directory-app.js +26 -26
- package/public/assets/homepage-client-app.js +12 -12
- package/public/serve-shells/editor.html +66 -3
- package/public/serve-shells/library-files.html +60 -3
- package/public/serve-shells/library-raws.html +60 -3
- package/public/serve-shells/tools-clipper.html +60 -3
- package/public/serve-shells/tools-image.html +1316 -617
- package/public/serve-shells/tools-video.html +880 -161
|
@@ -591,6 +591,7 @@
|
|
|
591
591
|
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:4px 11px;text-decoration:none;line-height:1.4;
|
|
592
592
|
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
593
593
|
.rk-aichat-preview-act:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
594
|
+
.rk-aichat-preview-act[hidden]{display:none}
|
|
594
595
|
|
|
595
596
|
/* ── markdown pipe tables (assistant messages + .md previews) ── */
|
|
596
597
|
.rk-md-tablewrap{max-width:100%;overflow-x:auto;margin:8px 0;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg)}
|
|
@@ -622,6 +623,14 @@
|
|
|
622
623
|
.rk-aichat-file-thumb--markdown,.rk-aichat-file-thumb--text{background:var(--rk-n-50)}.rk-aichat-file-thumb--audio{background:var(--rk-gold-tint2)}
|
|
623
624
|
.rk-aichat-file-play{position:absolute;inset:0;display:grid;place-items:center;font-size:22px;color:#fff;text-shadow:0 1px 6px rgba(0,0,0,.55);pointer-events:none}
|
|
624
625
|
.rk-aichat-file-name{font-size:11px;font-weight:600;color:var(--rk-n-600);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:112px}
|
|
626
|
+
/* "Edit in the tool" pencil — only on image/video cards; opens /tools/image or
|
|
627
|
+
/tools/video in a new tab with this file preloaded as the working file. */
|
|
628
|
+
.rk-aichat-file-open{position:absolute;top:5px;right:5px;width:24px;height:24px;border-radius:var(--rk-r-md);
|
|
629
|
+
display:grid;place-items:center;background:rgba(255,255,255,.94);border:1px solid var(--rk-border);color:var(--rk-ink);
|
|
630
|
+
opacity:0;transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease);z-index:2}
|
|
631
|
+
.rk-aichat-file-open svg{width:13px;height:13px}
|
|
632
|
+
.rk-aichat-file:hover .rk-aichat-file-open,.rk-aichat-file-open:focus-visible{opacity:1}
|
|
633
|
+
.rk-aichat-file-open:hover{background:var(--rk-gold-tint)}
|
|
625
634
|
|
|
626
635
|
/* create_video pipeline: live status row + clickable result links */
|
|
627
636
|
.rk-aichat-flow{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--rk-text-muted);padding:2px 2px}
|
|
@@ -1088,8 +1097,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1088
1097
|
.rk-inpaint-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter)}
|
|
1089
1098
|
.rk-inpaint-workbench{display:grid;grid-template-columns:320px minmax(0,1fr) 320px;gap:20px;align-items:start}
|
|
1090
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}}
|
|
1091
|
-
@media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-
|
|
1092
|
-
.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)}
|
|
1093
1102
|
.rk-inpaint-panel{padding:18px;display:grid;gap:16px;align-content:start}
|
|
1094
1103
|
.rk-inpaint-block{display:grid;gap:9px}
|
|
1095
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}
|
|
@@ -1164,8 +1173,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1164
1173
|
.rk-inpaint-status[data-tone="success"]{color:#1f7a44}
|
|
1165
1174
|
|
|
1166
1175
|
/* center video stage */
|
|
1167
|
-
.rk-inpaint-stage{min-height:420px;display:grid;place-items:center;padding:20px;overflow:hidden}
|
|
1168
|
-
.rk-ipv-stage{align-content:center}
|
|
1169
1176
|
.rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:30px 20px}
|
|
1170
1177
|
.rk-inpaint-empty[hidden]{display:none}
|
|
1171
1178
|
.rk-inpaint-empty-ic{color:var(--rk-n-400)}
|
|
@@ -1192,44 +1199,110 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1192
1199
|
.rk-ipv-timecode-copy{width:30px;height:30px}
|
|
1193
1200
|
.rk-ipv-duration{font-size:11.5px;font-weight:700;color:var(--rk-text-faint);font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
1194
1201
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
.rk-ip-
|
|
1198
|
-
.rk-ip-
|
|
1199
|
-
.rk-ip-
|
|
1200
|
-
.rk-ip-
|
|
1201
|
-
.rk-ip-
|
|
1202
|
-
.rk-ip-
|
|
1203
|
-
.rk-ip-
|
|
1204
|
-
.rk-ip-
|
|
1205
|
-
.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 ---------------------------------------------------- */
|
|
1206
1262
|
.rk-ip-gen-refs{display:flex;flex-wrap:wrap;gap:6px}
|
|
1207
1263
|
.rk-ip-gen-refs[hidden]{display:none}
|
|
1208
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}
|
|
1209
1265
|
.rk-ip-gen-ref img{width:22px;height:22px;border-radius:4px;object-fit:cover;flex:none}
|
|
1210
1266
|
.rk-ip-gen-ref span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1211
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}
|
|
1212
|
-
.rk-ip-gen-
|
|
1213
|
-
.rk-ip-gen-
|
|
1214
|
-
.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)}
|
|
1215
1271
|
.rk-ip-gen-clip{border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
|
|
1216
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}
|
|
1217
1274
|
.rk-ip-gen-send{border:0;background:var(--rk-gold-500,var(--rk-gold-600));color:var(--rk-ink)}
|
|
1218
1275
|
.rk-ip-gen-send:hover:not(:disabled){background:var(--rk-gold-600)}
|
|
1219
1276
|
.rk-ip-gen-send:disabled{opacity:.5;cursor:default}
|
|
1220
|
-
.rk-ip-gen-send
|
|
1221
|
-
.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}
|
|
1222
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}
|
|
1223
1280
|
.rk-ip-gen-status[data-tone="error"]{color:#b42318}
|
|
1224
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)}
|
|
1225
1285
|
|
|
1226
|
-
/*
|
|
1227
|
-
.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)}
|
|
1228
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}
|
|
1229
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)}
|
|
1230
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}
|
|
1231
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)}
|
|
1232
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}
|
|
1233
1306
|
|
|
1234
1307
|
/* right: result + history */
|
|
1235
1308
|
.rk-inpaint-output{padding:18px;display:grid;gap:18px;align-content:start}
|
|
@@ -1378,6 +1451,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1378
1451
|
</div>
|
|
1379
1452
|
<div class="rk-inpaint-toolbar">
|
|
1380
1453
|
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-ipv-start-new><svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4v12"/><path d="M4 10h12"/></svg> Start New</button>
|
|
1454
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-copy-chat aria-label="Copy chat ID" title="Copy chat ID"><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>
|
|
1381
1455
|
<button type="button" class="rk-ip-iconbtn" data-ipv-settings-button aria-label="Settings" title="Settings"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M8.8 3.4 9.4 2h1.2l.6 1.4 1.2.5 1.4-.6.9.9-.6 1.4.5 1.2 1.4.6v1.2l-1.4.6-.5 1.2.6 1.4-.9.9-1.4-.6-1.2.5-.6 1.4H9.4l-.6-1.4-1.2-.5-1.4.6-.9-.9.6-1.4-.5-1.2L4 8.6V7.4l1.4-.6.5-1.2-.6-1.4.9-.9 1.4.6 1.2-.5Z"/><circle cx="10" cy="8" r="2.4"/></svg></button>
|
|
1382
1456
|
<button type="button" class="rk-ip-iconbtn" data-ipv-history-toggle aria-label="Past sessions" title="Past sessions"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 10a6.8 6.8 0 1 1 2 4.8"/><path d="M3.2 14.4v-3.6h3.6"/><path d="M10 6.2V10l2.6 1.6"/></svg></button>
|
|
1383
1457
|
</div>
|
|
@@ -1458,44 +1532,61 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1458
1532
|
</section>
|
|
1459
1533
|
|
|
1460
1534
|
<!-- CENTER: scrubbable video stage -->
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
<
|
|
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>
|
|
1468
1548
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
<
|
|
1474
|
-
<
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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>
|
|
1478
1564
|
</div>
|
|
1479
1565
|
</div>
|
|
1480
|
-
<p class="rk-ip-gen-status" id="rk-ipv-genstatus" data-tone="" aria-live="polite"></p>
|
|
1481
1566
|
</div>
|
|
1482
1567
|
</div>
|
|
1483
1568
|
|
|
1484
|
-
<div class="rk-ipv-
|
|
1485
|
-
<
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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>
|
|
1489
1582
|
</div>
|
|
1490
|
-
<div class="rk-
|
|
1491
|
-
|
|
1492
|
-
<
|
|
1493
|
-
<
|
|
1494
|
-
|
|
1495
|
-
<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>
|
|
1496
|
-
<span class="rk-ipv-duration" data-ipv-duration>/ 00:00:000</span>
|
|
1497
|
-
</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>
|
|
1498
1588
|
</div>
|
|
1589
|
+
<p class="rk-ip-gen-status" id="rk-ipv-genstatus" data-tone="" aria-live="polite" hidden></p>
|
|
1499
1590
|
</div>
|
|
1500
1591
|
</section>
|
|
1501
1592
|
|
|
@@ -1673,6 +1764,282 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1673
1764
|
</aside>
|
|
1674
1765
|
</div>
|
|
1675
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
|
+
|
|
1676
2043
|
(function(){
|
|
1677
2044
|
var videoEl = document.querySelector("[data-ipv-video]");
|
|
1678
2045
|
var canvas = document.querySelector("[data-ipv-canvas]");
|
|
@@ -1733,6 +2100,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1733
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>";
|
|
1734
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>";
|
|
1735
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>";
|
|
1736
2107
|
|
|
1737
2108
|
// ===== state =====
|
|
1738
2109
|
var sourceVideoUrl = ""; // playable URL (may be a blob: object URL for uploads)
|
|
@@ -1797,7 +2168,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1797
2168
|
sessionTracer = value; tracerAutoPending = false; if(tracerInput) tracerInput.value = value; return value;
|
|
1798
2169
|
}
|
|
1799
2170
|
function resolveTracer(){ if(sessionTracer) return sessionTracer; var v = tracerInput ? tracerInput.value.trim() : ""; return v; }
|
|
1800
|
-
function getTracerValue(){ if(sessionTracer) return sessionTracer; var v = tracerInput ? tracerInput.value.trim() : ""; if(v){ sessionTracer = v; tracerAutoPending = false; return v; } return
|
|
2171
|
+
function getTracerValue(){ if(sessionTracer) return sessionTracer; var v = tracerInput ? tracerInput.value.trim() : ""; if(v){ sessionTracer = v; tracerAutoPending = false; return v; } return commitRandomTracer(); }
|
|
2172
|
+
// A session with nothing to name it from yet (e.g. Copy chat ID before the first
|
|
2173
|
+
// prompt) gets a plain id — seeding nameTracerFromSeed("video-edit") here produced
|
|
2174
|
+
// the doubled "video-edit-video-edit-…".
|
|
2175
|
+
function commitRandomTracer(){
|
|
2176
|
+
var value = "video-edit-" + tracerSuffix();
|
|
2177
|
+
sessionTracer = value; tracerAutoPending = false; if(tracerInput) tracerInput.value = value; return value;
|
|
2178
|
+
}
|
|
1801
2179
|
|
|
1802
2180
|
// ===== provider list =====
|
|
1803
2181
|
function loadProviders(){
|
|
@@ -1993,6 +2371,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1993
2371
|
videoEl.onerror = function(){ if(!crossOriginRetried && videoEl.getAttribute("crossorigin")){ crossOriginRetried = true; videoEl.removeAttribute("crossorigin"); videoEl.src = url; videoEl.load(); } };
|
|
1994
2372
|
videoEl.src = url;
|
|
1995
2373
|
videoEl.load();
|
|
2374
|
+
ensureSourceTurn(url, name || "Video");
|
|
1996
2375
|
setStatus("Video ready. Scrub the timeline and paint timed masks.", "success");
|
|
1997
2376
|
}
|
|
1998
2377
|
function loadSourceFile(file){
|
|
@@ -2005,6 +2384,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2005
2384
|
videoEl.src = sourceObjectUrl;
|
|
2006
2385
|
videoEl.addEventListener("loadedmetadata", function(){ syncCanvasSize(); if(durationEl) durationEl.textContent = "/ " + formatTimecode(videoEl.duration || 0); redraw(); }, { once: true });
|
|
2007
2386
|
videoEl.load();
|
|
2387
|
+
ensureSourceTurn(sourceObjectUrl, file.name || "Uploaded video");
|
|
2008
2388
|
setStatus("Video loaded. Scrub the timeline and paint timed masks.", "success");
|
|
2009
2389
|
}
|
|
2010
2390
|
|
|
@@ -2186,6 +2566,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2186
2566
|
var path = String((entry && entry.path) || "").toLowerCase();
|
|
2187
2567
|
return tid === "primitive:video_generate" || path.indexOf("/primitives/videos/generate") >= 0 || path.indexOf("/primitives/video_generate") >= 0;
|
|
2188
2568
|
}
|
|
2569
|
+
// The user's message for a run-history entry. New turns carry chat_message on the
|
|
2570
|
+
// envelope; older ones are un-composed back out of the instruction we built.
|
|
2571
|
+
function userTextFromEntry(entry){
|
|
2572
|
+
var body = (entry && entry.request && entry.request.body) || {};
|
|
2573
|
+
if(typeof body.chat_message === "string" && body.chat_message.trim()) return body.chat_message.trim();
|
|
2574
|
+
var payload = body.payload || {};
|
|
2575
|
+
var raw = String(payload.prompt || payload.instruction || "");
|
|
2576
|
+
if(!raw) return "";
|
|
2577
|
+
var whole = raw.split("Whole clip: ")[1];
|
|
2578
|
+
if(whole) return whole.split(". Timed region edits:")[0].split(". Match the surrounding lighting")[0].trim();
|
|
2579
|
+
var timed = raw.split("Timed region edits: ")[1];
|
|
2580
|
+
if(timed) return timed.split(". Match the surrounding lighting")[0].trim();
|
|
2581
|
+
return raw.length > 240 ? raw.slice(0, 240) + "\u2026" : raw;
|
|
2582
|
+
}
|
|
2189
2583
|
function extractHistoryItems(entries){
|
|
2190
2584
|
var items = []; var seen = {};
|
|
2191
2585
|
(entries || []).forEach(function(entry){
|
|
@@ -2204,16 +2598,46 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2204
2598
|
});
|
|
2205
2599
|
return items;
|
|
2206
2600
|
}
|
|
2601
|
+
// Set while opening a past session: the next history load restores that session's
|
|
2602
|
+
// conversation state (its last prompt back in the composer, its newest video in the
|
|
2603
|
+
// Result panel) instead of just repainting the rail.
|
|
2604
|
+
var sessionRestorePending = false;
|
|
2605
|
+
function restoreSessionFromEntries(entries){
|
|
2606
|
+
var rows = (entries || []).filter(isVideoEntry)
|
|
2607
|
+
.sort(function(a, b){ return Date.parse(a.createdAt || 0) - Date.parse(b.createdAt || 0); });
|
|
2608
|
+
if(!rows.length){ setStatus("Opened " + resolveTracer() + " \u2014 no messages in it yet.", ""); return; }
|
|
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");
|
|
2626
|
+
}
|
|
2627
|
+
renderChat();
|
|
2628
|
+
setStatus("Session restored \u2014 " + rows.length + " message" + (rows.length === 1 ? "" : "s") + ". Keep editing to pick up where you left off.", "success");
|
|
2629
|
+
}
|
|
2207
2630
|
function loadHistory(){
|
|
2208
2631
|
var tracer = resolveTracer();
|
|
2209
|
-
if(!tracer){ historyItems = []; reinjectSources(); renderHistory(); return; }
|
|
2632
|
+
if(!tracer){ historyItems = []; reinjectSources(); renderHistory(); sessionRestorePending = false; return; }
|
|
2210
2633
|
if(historyRefresh) historyRefresh.disabled = true;
|
|
2211
2634
|
var endpoint = new URL("/job-runs/history", window.location.origin);
|
|
2212
2635
|
endpoint.searchParams.set("limit", "120"); endpoint.searchParams.set("template_id", "primitive:video_generate"); endpoint.searchParams.set("tracer", tracer);
|
|
2213
2636
|
fetch(endpoint, { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
|
|
2214
2637
|
if(!res.ok || (res.p && res.p.error)) throw new Error((res.p && res.p.error) || "Unable to load history.");
|
|
2215
2638
|
historyItems = extractHistoryItems(res.p.entries); reinjectSources(); renderHistory();
|
|
2216
|
-
|
|
2639
|
+
if(sessionRestorePending){ sessionRestorePending = false; restoreSessionFromEntries(res.p.entries); }
|
|
2640
|
+
}).catch(function(err){ sessionRestorePending = false; setHistoryEmpty(err && err.message ? err.message : "Unable to load history."); }).then(function(){ if(historyRefresh) historyRefresh.disabled = false; });
|
|
2217
2641
|
}
|
|
2218
2642
|
function applyCompletedJob(payload){
|
|
2219
2643
|
var out = extractVideoUrl(payload); if(!out) return "";
|
|
@@ -2257,29 +2681,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2257
2681
|
if(frames.last && frames.last.url) arr.push({ image_url: frames.last.url, frame_type: "last_frame" });
|
|
2258
2682
|
return arr;
|
|
2259
2683
|
}
|
|
2260
|
-
function setSubmitLoading(loading){ submitBtn.setAttribute("data-loading", loading ? "true" : "false"); submitBtn.disabled = loading; if(submitLabel) submitLabel.textContent = loading ? "Generating\u2026" : "Generate video"; }
|
|
2261
|
-
|
|
2262
|
-
function submitGenerate(){
|
|
2263
|
-
if(submitBtn.disabled) return;
|
|
2264
|
-
var instruction = buildVideoInstruction();
|
|
2265
|
-
if(!instruction){ setStatus("Add a video prompt or a mask instruction first.", "error"); return; }
|
|
2266
|
-
var frameImgs = frameImagesPayload();
|
|
2267
|
-
setSubmitLoading(true);
|
|
2268
|
-
var seed = (globalPrompt && globalPrompt.value.trim()) || (masks.find(function(m){ return String(m.prompt || "").trim(); }) || {}).prompt || "video-edit";
|
|
2269
|
-
var tracer = tracerAutoPending ? nameTracerFromSeed(seed) : getTracerValue();
|
|
2270
|
-
var payload = { prompt: instruction, output_format: "mp4", aspect_ratio: getAspectRatio() };
|
|
2271
|
-
if(frameImgs.length) payload.frame_images = frameImgs;
|
|
2272
|
-
if(providerSelect && providerSelect.value) payload.provider = providerSelect.value;
|
|
2273
|
-
if(durationSelect && durationSelect.value) payload.duration = Number(durationSelect.value);
|
|
2274
|
-
if(audioToggle && audioToggle.checked) payload.generate_audio = true;
|
|
2275
|
-
setStatus("Creating video job\u2026", "");
|
|
2276
|
-
fetch("/api/v1/primitives/videos/generate", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ tracer: tracer, payload: payload }) })
|
|
2277
|
-
.then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); })
|
|
2278
|
-
.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); })
|
|
2279
|
-
.then(function(){ loadHistory(); loadSessions(); })
|
|
2280
|
-
.catch(function(err){ setStatus(err && err.message ? err.message : "Unable to create video.", "error"); })
|
|
2281
|
-
.then(function(){ setSubmitLoading(false); });
|
|
2282
|
-
}
|
|
2283
2684
|
|
|
2284
2685
|
// ===== lightbox =====
|
|
2285
2686
|
function openVideoPreview(url, label, downloadName){
|
|
@@ -2306,8 +2707,16 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2306
2707
|
(res.p.entries || []).forEach(function(entry){
|
|
2307
2708
|
if(!isVideoEntry(entry)) return; var tracer = String(entry.tracer || "").trim(); if(!tracer) return;
|
|
2308
2709
|
var urls = historyEntryUrls(entry);
|
|
2309
|
-
if(!groups[tracer]){ groups[tracer] = { tracer: tracer, thumb: "", count: 0, createdAt: entry.createdAt || "" }; order.push(tracer); }
|
|
2310
|
-
var g = groups[tracer];
|
|
2710
|
+
if(!groups[tracer]){ groups[tracer] = { tracer: tracer, thumb: "", title: "", count: 0, createdAt: entry.createdAt || "", firstAt: entry.createdAt || "" }; order.push(tracer); }
|
|
2711
|
+
var g = groups[tracer];
|
|
2712
|
+
// One history entry = one message in this session's conversation.
|
|
2713
|
+
g.count += 1;
|
|
2714
|
+
if(!g.thumb && urls[0]){ try { g.thumb = absoluteUrl(urls[0]); } catch(_){} }
|
|
2715
|
+
// Oldest message names the session (same rule as /tools/image).
|
|
2716
|
+
if(!g.title || Date.parse(entry.createdAt || 0) <= Date.parse(g.firstAt || 0)){
|
|
2717
|
+
var text = userTextFromEntry(entry);
|
|
2718
|
+
if(text){ g.title = text.length > 60 ? text.slice(0, 60) + "\u2026" : text; g.firstAt = entry.createdAt || g.firstAt; }
|
|
2719
|
+
}
|
|
2311
2720
|
if(Date.parse(entry.createdAt || 0) > Date.parse(g.createdAt || 0)) g.createdAt = entry.createdAt;
|
|
2312
2721
|
});
|
|
2313
2722
|
var list = order.map(function(t){ return groups[t]; }).sort(function(a, b){ return Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0); });
|
|
@@ -2324,16 +2733,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2324
2733
|
if(group.thumb){ var v = document.createElement("video"); v.className = "rk-ip-session-thumb"; v.src = group.thumb + "#t=0.1"; v.muted = true; v.preload = "metadata"; row.appendChild(v); }
|
|
2325
2734
|
else { var ph = document.createElement("span"); ph.className = "rk-ip-session-thumb is-empty"; ph.innerHTML = PLAY_SVG; row.appendChild(ph); }
|
|
2326
2735
|
var meta = document.createElement("div"); meta.className = "rk-ip-session-meta";
|
|
2327
|
-
var title = document.createElement("div"); title.className = "rk-ip-session-title"; title.textContent = group.tracer;
|
|
2736
|
+
var title = document.createElement("div"); title.className = "rk-ip-session-title"; title.textContent = group.title || group.tracer;
|
|
2737
|
+
title.title = group.title ? group.title + " (" + group.tracer + ")" : group.tracer;
|
|
2328
2738
|
var sub = document.createElement("div"); sub.className = "rk-ip-session-sub"; var when = relTimeShort(group.createdAt);
|
|
2329
|
-
sub.textContent = (group.count ? group.count + "
|
|
2739
|
+
sub.textContent = (group.count ? group.count + " message" + (group.count === 1 ? "" : "s") : "session") + (when ? " \u00b7 " + when : "");
|
|
2330
2740
|
meta.appendChild(title); meta.appendChild(sub); row.appendChild(meta); sessionsList.appendChild(row);
|
|
2331
2741
|
});
|
|
2332
2742
|
}
|
|
2333
2743
|
function switchToTracer(tracer){
|
|
2334
2744
|
var value = String(tracer || "").trim(); if(!value) return;
|
|
2335
|
-
|
|
2336
|
-
|
|
2745
|
+
clearUrlParams(); resetWorkspace();
|
|
2746
|
+
sessionTracer = value; if(tracerInput) tracerInput.value = value; tracerAutoPending = false; closeSessions();
|
|
2747
|
+
setStatus("Opening " + value + "\u2026", "");
|
|
2748
|
+
sessionRestorePending = true;
|
|
2749
|
+
loadHistory(); loadSessions();
|
|
2337
2750
|
}
|
|
2338
2751
|
|
|
2339
2752
|
// ===== reset =====
|
|
@@ -2344,6 +2757,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2344
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 = ""; });
|
|
2345
2758
|
if(globalPrompt) globalPrompt.value = ""; attachments = []; renderAttachChips();
|
|
2346
2759
|
outputPanel.hidden = true; resultVideo.removeAttribute("src");
|
|
2760
|
+
chatTurns = []; activeTurnId = ""; turnSeq = 0; renderChat();
|
|
2347
2761
|
emptyState.hidden = false; if(player) player.hidden = true;
|
|
2348
2762
|
canvas.width = 1; canvas.height = 1; renderMasks();
|
|
2349
2763
|
if(scrub) scrub.value = "0"; if(timecodeInput) timecodeInput.value = "00:00:000"; if(durationEl) durationEl.textContent = "/ 00:00:000";
|
|
@@ -2378,13 +2792,19 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2378
2792
|
if(brushInput){ brushInput.addEventListener("input", function(){ if(brushLabel) brushLabel.textContent = brushInput.value; updateBrushCursorSize(); }); }
|
|
2379
2793
|
if(uploadBtn && fileInput){ uploadBtn.addEventListener("click", function(){ fileInput.click(); }); fileInput.addEventListener("change", function(){ if(fileInput.files && fileInput.files[0]) loadSourceFile(fileInput.files[0]); }); }
|
|
2380
2794
|
if(startNewBtn) startNewBtn.addEventListener("click", startNew);
|
|
2795
|
+
// Copy chat ID — the session tracer IS this chat's thread id (what /job-runs/history
|
|
2796
|
+
// groups on and what ?tracer= reopens). An unnamed session mints its id here.
|
|
2797
|
+
var copyChatBtn = document.querySelector("[data-ipv-copy-chat]");
|
|
2798
|
+
if(copyChatBtn) copyChatBtn.addEventListener("click", function(){
|
|
2799
|
+
var tracer = resolveTracer() || getTracerValue();
|
|
2800
|
+
copyText(tracer).then(function(ok){ setStatus(ok ? "Chat ID copied \u2014 " + tracer : "Chat ID: " + tracer, ok ? "success" : ""); });
|
|
2801
|
+
});
|
|
2381
2802
|
if(addMaskBtn) addMaskBtn.addEventListener("click", function(){ addMask(videoEl.currentTime || 0, colorPicker ? colorPicker.value : null); });
|
|
2382
2803
|
if(colorPicker) colorPicker.addEventListener("input", function(){ var m = activeMask(); if(m){ m.color = normalizeHex(colorPicker.value); renderMasks(); redraw(); } });
|
|
2383
2804
|
if(undoBtn) undoBtn.addEventListener("click", function(){ var m = activeMask(); if(m && m.strokes.length){ m.redo.push(m.strokes.pop()); redraw(); syncUndoRedo(); } });
|
|
2384
2805
|
if(redoBtn) redoBtn.addEventListener("click", function(){ var m = activeMask(); if(m && m.redo.length){ m.strokes.push(m.redo.pop()); redraw(); syncUndoRedo(); } });
|
|
2385
2806
|
if(clearBtn) clearBtn.addEventListener("click", function(){ var m = activeMask(); if(m){ m.redo = m.strokes.concat(m.redo); m.strokes = []; redraw(); syncUndoRedo(); } });
|
|
2386
2807
|
if(attachBtn && attachInput){ attachBtn.addEventListener("click", function(){ attachInput.click(); }); attachInput.addEventListener("change", function(){ addAttachmentFiles(attachInput.files); attachInput.value = ""; }); }
|
|
2387
|
-
if(submitBtn) submitBtn.addEventListener("click", submitGenerate);
|
|
2388
2808
|
|
|
2389
2809
|
masksList.addEventListener("click", function(event){
|
|
2390
2810
|
var t = event.target; if(!(t instanceof Element)) return;
|
|
@@ -2470,94 +2890,345 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2470
2890
|
}
|
|
2471
2891
|
|
|
2472
2892
|
// ===== text-to-video composer =====
|
|
2893
|
+
// ===== chat feed + shared composer =====
|
|
2473
2894
|
var genPrompt = document.getElementById("rk-ipv-genprompt");
|
|
2474
2895
|
var genClip = document.getElementById("rk-ipv-genclip");
|
|
2475
2896
|
var genSend = document.getElementById("rk-ipv-gensend");
|
|
2476
2897
|
var genRefsEl = document.getElementById("rk-ipv-genrefs");
|
|
2477
2898
|
var genStatusEl = document.getElementById("rk-ipv-genstatus");
|
|
2478
|
-
var
|
|
2479
|
-
var
|
|
2480
|
-
var
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
function
|
|
2486
|
-
function
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
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
|
+
}
|
|
2507
2997
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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;
|
|
2512
3022
|
}
|
|
2513
|
-
|
|
2514
|
-
function
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
attempt++; if(attempt >= 120){ setGenStatus("Still running \u2014 check History shortly.", ""); return ""; }
|
|
2524
|
-
return new Promise(function(res2){ setTimeout(res2, attempt < 8 ? 2000 : 4000); }).then(step);
|
|
2525
|
-
});
|
|
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;
|
|
2526
3033
|
}
|
|
2527
|
-
|
|
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;
|
|
2528
3094
|
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
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;
|
|
2537
3159
|
if(providerSelect && providerSelect.value) payload.provider = providerSelect.value;
|
|
2538
3160
|
if(durationSelect && durationSelect.value) payload.duration = Number(durationSelect.value);
|
|
2539
3161
|
if(audioToggle && audioToggle.checked) payload.generate_audio = true;
|
|
2540
|
-
var
|
|
2541
|
-
|
|
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
|
+
})
|
|
2542
3169
|
.then(function(r){ return parseJsonResponse(r).then(function(j){ return { ok: r.ok, j: j }; }); })
|
|
2543
|
-
.then(function(res){
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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: "" }; });
|
|
2552
3197
|
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
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();
|
|
2557
3225
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
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();
|
|
2561
3232
|
|
|
2562
3233
|
// ===== boot =====
|
|
2563
3234
|
renderMasks();
|
|
@@ -3116,6 +3787,34 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3116
3787
|
}
|
|
3117
3788
|
function fileIcon(kind) { return kind === 'pdf' ? '📕' : kind === 'json' ? '{ }' : kind === 'markdown' ? '📝' : kind === 'audio' ? '🎵' : kind === 'text' ? '📄' : '📄'; }
|
|
3118
3789
|
|
|
3790
|
+
// ── "edit this media" hand-off: dock chat → the matching creative tool ──
|
|
3791
|
+
// Images open /tools/image, videos /tools/video, each with the file preloaded as
|
|
3792
|
+
// that tool's working file (its chat then edits THAT media). Deliberately a fresh
|
|
3793
|
+
// tool session — no ?tracer — so the tool names its own chat from the first prompt.
|
|
3794
|
+
var PENCIL_SVG = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.6 3.6a1.9 1.9 0 0 1 2.8 2.8L7.7 15.1l-3.6.8.8-3.6z"/></svg>';
|
|
3795
|
+
function toolEditUrl(file) {
|
|
3796
|
+
if (!file || !file.viewUrl) return '';
|
|
3797
|
+
var k = fileKind(file.name, file.contentType);
|
|
3798
|
+
var name = file.name || '';
|
|
3799
|
+
if (k === 'image') return '/tools/image?source_image_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_image_name=' + encodeURIComponent(name) : '');
|
|
3800
|
+
if (k === 'video') return '/tools/video?source_video_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_video_name=' + encodeURIComponent(name) : '');
|
|
3801
|
+
return '';
|
|
3802
|
+
}
|
|
3803
|
+
function toolEditLink(file, className) {
|
|
3804
|
+
var href = toolEditUrl(file);
|
|
3805
|
+
if (!href) return null;
|
|
3806
|
+
var a = document.createElement('a');
|
|
3807
|
+
a.className = className;
|
|
3808
|
+
a.href = href;
|
|
3809
|
+
a.target = '_blank';
|
|
3810
|
+
a.rel = 'noopener noreferrer';
|
|
3811
|
+
a.title = 'Edit in the ' + (fileKind(file.name, file.contentType) === 'video' ? 'video' : 'image') + ' tool';
|
|
3812
|
+
a.setAttribute('aria-label', a.title);
|
|
3813
|
+
// The card itself opens the lightbox; the pencil must not also trigger it.
|
|
3814
|
+
a.addEventListener('click', function (e) { e.stopPropagation(); });
|
|
3815
|
+
return a;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3119
3818
|
// Compact markdown → HTML for .md previews (headings, lists, quotes, rules,
|
|
3120
3819
|
// fenced + inline code, bold/italic/links). Escapes first, then injects tags
|
|
3121
3820
|
// (same order as linkify). Backticks are built via char codes to avoid
|
|
@@ -3176,7 +3875,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3176
3875
|
var dlBtn = document.createElement('a'); dlBtn.className = 'rk-aichat-preview-act'; dlBtn.target = '_blank'; dlBtn.rel = 'noopener noreferrer'; dlBtn.textContent = 'Download';
|
|
3177
3876
|
var linkBtn = document.createElement('button'); linkBtn.type = 'button'; linkBtn.className = 'rk-aichat-preview-act'; linkBtn.textContent = 'Copy link';
|
|
3178
3877
|
var xBtn = document.createElement('button'); xBtn.type = 'button'; xBtn.className = 'rk-aichat-modal-x'; xBtn.setAttribute('aria-label', 'Close'); xBtn.innerHTML = '×';
|
|
3179
|
-
|
|
3878
|
+
// Same hand-off as the card pencil, spelled out as a labelled action here.
|
|
3879
|
+
var editBtn = document.createElement('a'); editBtn.className = 'rk-aichat-preview-act'; editBtn.target = '_blank'; editBtn.rel = 'noopener noreferrer'; editBtn.textContent = 'Edit';
|
|
3880
|
+
head.appendChild(nameEl); head.appendChild(editBtn); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
|
|
3180
3881
|
function flashAct(btn, label) { var o = btn.getAttribute('data-label') || btn.textContent; btn.setAttribute('data-label', o); btn.textContent = label; setTimeout(function () { btn.textContent = o; }, 1400); }
|
|
3181
3882
|
// Copy = file text (json/md/txt) or its URL for binaries. Copy link = the
|
|
3182
3883
|
// durable public S3 URL. Download opens in a new tab when the browser can't
|
|
@@ -3225,6 +3926,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3225
3926
|
var it = items[idx];
|
|
3226
3927
|
nameEl.textContent = it.name || 'file'; nameEl.title = it.name || '';
|
|
3227
3928
|
dlBtn.href = it.viewUrl; dlBtn.setAttribute('download', it.name || '');
|
|
3929
|
+
var editHref = toolEditUrl(it);
|
|
3930
|
+
editBtn.hidden = !editHref;
|
|
3931
|
+
if (editHref) { editBtn.href = editHref; editBtn.title = 'Edit in the ' + (fileKind(it.name, it.contentType) === 'video' ? 'video' : 'image') + ' tool'; }
|
|
3228
3932
|
linkBtn.style.display = it.publicUrl ? '' : 'none';
|
|
3229
3933
|
body.innerHTML = '';
|
|
3230
3934
|
var kind = fileKind(it.name, it.contentType);
|
|
@@ -3279,7 +3983,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3279
3983
|
var files = [], seen = {};
|
|
3280
3984
|
var arts = (job && job.artifacts) || [];
|
|
3281
3985
|
for (var i = 0; i < arts.length; i++) { var f = rkFileFromArtifact(arts[i]); if (f && !seen[f.viewUrl]) { seen[f.viewUrl] = true; files.push(f); } }
|
|
3282
|
-
|
|
3986
|
+
// Name the direct result URL after the FILE, not the tracer — fileKind() reads the
|
|
3987
|
+
// extension, and a tracer-named entry ("my-poster") classifies as 'other', losing
|
|
3988
|
+
// the image/video thumbnail and the edit-in-tool hand-off.
|
|
3989
|
+
var mu = rkResultMediaUrl(job);
|
|
3990
|
+
if (mu && !seen[mu]) {
|
|
3991
|
+
seen[mu] = true;
|
|
3992
|
+
var seg = String(mu).split('?')[0].split('/').pop() || '';
|
|
3993
|
+
files.push({ name: (/.[a-z0-9]{2,5}$/i.test(seg) ? seg : (job.tracer || 'output')), contentType: '', viewUrl: mu, publicUrl: mu });
|
|
3994
|
+
}
|
|
3283
3995
|
var res = job && job.result;
|
|
3284
3996
|
if (res && typeof res === 'object' && !Array.isArray(res) && Object.keys(res).length) {
|
|
3285
3997
|
try { var blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' }); files.push({ name: (job.tracer || 'result') + '.json', contentType: 'application/json', viewUrl: URL.createObjectURL(blob), publicUrl: null }); } catch (e) {}
|
|
@@ -3332,14 +4044,21 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3332
4044
|
var grid = document.createElement('div'); grid.className = 'rk-aichat-files-grid';
|
|
3333
4045
|
list.forEach(function (f, ix) {
|
|
3334
4046
|
var k = fileKind(f.name, f.contentType);
|
|
3335
|
-
|
|
4047
|
+
// A div, not a button: media cards carry a nested "edit in tool" link, and
|
|
4048
|
+
// an anchor inside a button is invalid (and unclickable in some browsers).
|
|
4049
|
+
var card = document.createElement('div'); card.className = 'rk-aichat-file'; card.tabIndex = 0; card.setAttribute('role', 'button');
|
|
3336
4050
|
var thumb = document.createElement('div'); thumb.className = 'rk-aichat-file-thumb rk-aichat-file-thumb--' + k;
|
|
3337
4051
|
if (k === 'image') { var im = document.createElement('img'); im.src = f.viewUrl; im.alt = ''; im.loading = 'lazy'; thumb.appendChild(im); }
|
|
3338
4052
|
else if (k === 'video') { var v = document.createElement('video'); v.src = f.viewUrl; v.muted = true; v.preload = 'metadata'; v.playsInline = true; thumb.appendChild(v); var pl = document.createElement('span'); pl.className = 'rk-aichat-file-play'; pl.textContent = '▶'; thumb.appendChild(pl); }
|
|
3339
4053
|
else { var ic = document.createElement('span'); ic.className = 'rk-aichat-file-ic'; ic.textContent = fileIcon(k); thumb.appendChild(ic); }
|
|
4054
|
+
var edit = toolEditLink(f, 'rk-aichat-file-open');
|
|
4055
|
+
if (edit) { edit.innerHTML = PENCIL_SVG; thumb.appendChild(edit); }
|
|
3340
4056
|
var nm = document.createElement('div'); nm.className = 'rk-aichat-file-name'; nm.textContent = f.name || 'file'; nm.title = f.name || '';
|
|
3341
4057
|
card.appendChild(thumb); card.appendChild(nm);
|
|
3342
4058
|
card.addEventListener('click', function () { openFilePreview(list, ix); });
|
|
4059
|
+
card.addEventListener('keydown', function (e) {
|
|
4060
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFilePreview(list, ix); }
|
|
4061
|
+
});
|
|
3343
4062
|
grid.appendChild(card);
|
|
3344
4063
|
});
|
|
3345
4064
|
wrap.appendChild(grid); turn.appendChild(wrap); logBottom();
|