@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}
|
|
@@ -1086,13 +1095,16 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1086
1095
|
.rk-ip-tab svg{flex:none}
|
|
1087
1096
|
|
|
1088
1097
|
.rk-inpaint-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter)}
|
|
1089
|
-
.rk-inpaint-workbench{display:grid;grid-template-columns:
|
|
1090
|
-
@media(max-width:1200px){.rk-inpaint-workbench{grid-template-columns:
|
|
1091
|
-
@media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-
|
|
1098
|
+
.rk-inpaint-workbench{display:grid;grid-template-columns:300px minmax(0,1fr) 300px;gap:20px;align-items:start}
|
|
1099
|
+
@media(max-width:1200px){.rk-inpaint-workbench{grid-template-columns:300px minmax(0,1fr)}.rk-inpaint-output{grid-column:1 / -1;grid-row:2}}
|
|
1100
|
+
@media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-ip-chat,.rk-inpaint-panel{grid-column:auto}.rk-ip-chat{order:-1}}
|
|
1092
1101
|
|
|
1093
1102
|
/* panels (no side borders — uniform hairline) */
|
|
1094
|
-
.rk-inpaint-panel,.rk-
|
|
1103
|
+
.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)}
|
|
1095
1104
|
.rk-inpaint-panel{padding:18px;display:grid;gap:16px;align-content:start}
|
|
1105
|
+
/* fields the chat drives programmatically — present in the DOM, never shown */
|
|
1106
|
+
.rk-ip-legacy{display:none!important}
|
|
1107
|
+
.rk-ip-maskblock[hidden]{display:none}
|
|
1096
1108
|
.rk-inpaint-block{display:grid;gap:9px}
|
|
1097
1109
|
.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}
|
|
1098
1110
|
|
|
@@ -1173,40 +1185,66 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1173
1185
|
.rk-inpaint-workbench.is-locked .rk-inpaint-generate{opacity:1;cursor:default;background:var(--rk-n-50);color:var(--rk-text-muted);border-color:var(--rk-border);box-shadow:none;font-size:12.5px}
|
|
1174
1186
|
.rk-inpaint-workbench.is-locked .rk-inpaint-generate svg{display:none}
|
|
1175
1187
|
|
|
1176
|
-
/* center stage + canvas */
|
|
1177
|
-
.rk-inpaint-stage{min-height:420px;display:grid;place-items:center;padding:20px;overflow:hidden}
|
|
1178
|
-
.rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:34px 20px}
|
|
1179
|
-
.rk-inpaint-empty[hidden]{display:none}
|
|
1180
|
-
.rk-inpaint-empty-ic{color:var(--rk-n-400)}
|
|
1181
|
-
.rk-inpaint-empty-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink)}
|
|
1182
|
-
.rk-inpaint-empty-sub{font-size:13px}
|
|
1183
|
-
.editor-inpaint-canvas-wrap{position:relative;display:inline-block;line-height:0;max-width:100%;border-radius:var(--rk-r-2xl);overflow:hidden;box-shadow:var(--rk-shadow-md);
|
|
1184
|
-
--inpaint-brush-cursor-size:70px;--inpaint-brush-cursor-x:0px;--inpaint-brush-cursor-y:0px}
|
|
1185
|
-
.editor-inpaint-canvas-wrap[hidden]{display:none}
|
|
1186
|
-
.editor-inpaint-canvas-wrap img{display:block;max-width:100%;max-height:min(64vh,620px);width:auto;height:auto;object-fit:contain;user-select:none}
|
|
1187
|
-
.editor-inpaint-canvas-wrap canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.86;cursor:none;touch-action:none}
|
|
1188
|
-
.editor-inpaint-brush-cursor{position:absolute;z-index:2;left:0;top:0;width:var(--inpaint-brush-cursor-size);height:var(--inpaint-brush-cursor-size);
|
|
1189
|
-
border:2px solid rgba(255,255,255,.95);border-radius:50%;box-shadow:0 0 0 1px rgba(28,25,23,.7),0 0 18px rgba(28,25,23,.25);opacity:0;pointer-events:none;
|
|
1190
|
-
transform:translate(var(--inpaint-brush-cursor-x),var(--inpaint-brush-cursor-y)) translate(-50%,-50%)}
|
|
1191
|
-
.editor-inpaint-brush-cursor[data-visible="true"]{opacity:1}
|
|
1192
1188
|
|
|
1193
|
-
/*
|
|
1194
|
-
.rk-ip-
|
|
1195
|
-
.rk-ip-
|
|
1196
|
-
.rk-ip-
|
|
1197
|
-
.rk-ip-
|
|
1198
|
-
.rk-ip-
|
|
1199
|
-
|
|
1200
|
-
.rk-ip-
|
|
1201
|
-
.rk-ip-
|
|
1202
|
-
.rk-ip-
|
|
1203
|
-
.rk-ip-
|
|
1204
|
-
.rk-ip-
|
|
1205
|
-
.rk-ip-
|
|
1206
|
-
.rk-ip-
|
|
1207
|
-
.rk-ip-
|
|
1208
|
-
.rk-ip-
|
|
1209
|
-
.rk-ip-
|
|
1189
|
+
/* ---- shared chat surface (tools/image + tools/video) ---------------------- */
|
|
1190
|
+
.rk-ip-chat{display:grid;grid-template-rows:minmax(0,1fr) auto;height:min(76vh,860px);min-height:520px;overflow:hidden}
|
|
1191
|
+
.rk-ip-feed{min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:18px 18px 6px;display:flex;flex-direction:column;gap:14px}
|
|
1192
|
+
.rk-ip-canvasholder{display:none}
|
|
1193
|
+
.rk-ip-turn{display:flex;flex-direction:column;gap:6px;max-width:100%}
|
|
1194
|
+
.rk-ip-turn[data-role="user"]{align-items:flex-end}
|
|
1195
|
+
.rk-ip-turn[data-role="assistant"]{align-items:flex-start}
|
|
1196
|
+
.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}
|
|
1197
|
+
.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)}
|
|
1198
|
+
.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)}
|
|
1199
|
+
.rk-ip-turn-tag{font-size:10.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--rk-text-faint)}
|
|
1200
|
+
.rk-ip-turn-refs{display:flex;flex-wrap:wrap;gap:5px;justify-content:flex-end}
|
|
1201
|
+
.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}
|
|
1202
|
+
.rk-ip-media{width:fit-content;max-width:min(100%,640px);display:grid;gap:7px}
|
|
1203
|
+
.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}
|
|
1204
|
+
.rk-ip-media-frame.is-latest{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1205
|
+
.rk-ip-media-frame img,.rk-ip-media-frame video{display:block;max-width:100%;max-height:min(52vh,520px);width:auto;height:auto}
|
|
1206
|
+
.rk-ip-media-actions{display:flex;flex-wrap:wrap;align-items:center;gap:5px}
|
|
1207
|
+
.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)}
|
|
1208
|
+
.rk-ip-act:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1209
|
+
.rk-ip-act-note{font-size:11.5px;font-weight:700;color:var(--rk-text-faint);margin-left:2px}
|
|
1210
|
+
.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)}
|
|
1211
|
+
.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}
|
|
1212
|
+
.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)}
|
|
1213
|
+
.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}
|
|
1214
|
+
.rk-ip-turn[data-kind="error"] .rk-ip-bubble{background:#fdf3f2;border-color:#e6a8a0;color:#b42318;font-weight:600}
|
|
1215
|
+
|
|
1216
|
+
/* ---- composer ------------------------------------------------------------ */
|
|
1217
|
+
.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;
|
|
1218
|
+
border-bottom-left-radius:var(--rk-r-3xl);border-bottom-right-radius:var(--rk-r-3xl)}
|
|
1219
|
+
.rk-ip-composer.is-dragref{background:var(--rk-gold-tint)}
|
|
1220
|
+
.rk-ip-composer.is-busy .rk-ip-comp-status{color:var(--rk-gold-700)}
|
|
1221
|
+
/* The grip is anchored to this wrapper, not the textarea, so it never scrolls
|
|
1222
|
+
away with the textarea's own overflow once the message passes 10 rows. */
|
|
1223
|
+
.rk-ip-comp-field{position:relative;display:block}
|
|
1224
|
+
.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;
|
|
1225
|
+
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)}
|
|
1226
|
+
.rk-ip-comp-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1227
|
+
.rk-ip-comp-input::placeholder{color:var(--rk-text-faint)}
|
|
1228
|
+
.rk-ip-comp-input:disabled{cursor:not-allowed;color:var(--rk-text-muted);background:var(--rk-n-50)}
|
|
1229
|
+
.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;
|
|
1230
|
+
border-radius:4px;opacity:.75;transition:color var(--rk-dur) var(--rk-ease),opacity var(--rk-dur) var(--rk-ease)}
|
|
1231
|
+
.rk-ip-comp-grip:hover,.rk-ip-comp-field.is-resizing .rk-ip-comp-grip{color:var(--rk-gold-700);opacity:1}
|
|
1232
|
+
.rk-ip-comp-field.is-resizing{user-select:none}
|
|
1233
|
+
.rk-ip-comp-bar{display:flex;align-items:center;gap:8px}
|
|
1234
|
+
.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);
|
|
1235
|
+
font:inherit;font-size:12px;font-weight:800;padding:7px 26px 7px 12px;cursor:pointer;outline:0;
|
|
1236
|
+
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");
|
|
1237
|
+
background-repeat:no-repeat;background-position:right 9px center}
|
|
1238
|
+
.rk-ip-comp-modeselect:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1239
|
+
.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}
|
|
1240
|
+
.rk-ip-comp-status[data-tone="error"]{color:#b42318}
|
|
1241
|
+
.rk-ip-comp-status[data-tone="success"]{color:#1f7a44}
|
|
1242
|
+
.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);
|
|
1243
|
+
font-size:11.5px;font-weight:700;color:var(--rk-gold-700);justify-self:start;max-width:100%}
|
|
1244
|
+
.rk-ip-comp-mask[hidden]{display:none}
|
|
1245
|
+
.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}
|
|
1246
|
+
|
|
1247
|
+
/* ---- attachment chips ---------------------------------------------------- */
|
|
1210
1248
|
.rk-ip-gen-refs{display:flex;flex-wrap:wrap;gap:6px}
|
|
1211
1249
|
.rk-ip-gen-refs[hidden]{display:none}
|
|
1212
1250
|
.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}
|
|
@@ -1214,23 +1252,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1214
1252
|
.rk-ip-gen-ref span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1215
1253
|
.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}
|
|
1216
1254
|
.rk-ip-gen-ref button:hover{color:var(--rk-ink)}
|
|
1217
|
-
.rk-ip-gen-
|
|
1218
|
-
.rk-ip-gen-hint{flex:1;font-size:11.5px;color:var(--rk-text-faint);line-height:1.35}
|
|
1255
|
+
.rk-ip-gen-ref.is-pending{opacity:.6}
|
|
1219
1256
|
.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)}
|
|
1220
1257
|
.rk-ip-gen-clip{border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
|
|
1221
1258
|
.rk-ip-gen-clip:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1259
|
+
.rk-ip-gen-clip:disabled{opacity:.5;cursor:not-allowed}
|
|
1222
1260
|
.rk-ip-gen-send{border:0;background:var(--rk-gold-500,var(--rk-gold-600));color:var(--rk-ink)}
|
|
1223
1261
|
.rk-ip-gen-send:hover:not(:disabled){background:var(--rk-gold-600)}
|
|
1224
1262
|
.rk-ip-gen-send:disabled{opacity:.5;cursor:default}
|
|
1225
|
-
.rk-ip-gen-send
|
|
1226
|
-
.rk-ip-gen-spin,.rk-ip-gen-send .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}
|
|
1263
|
+
.rk-ip-gen-send[data-mode="stop"]{background:var(--rk-ink);color:#fff}
|
|
1227
1264
|
.rk-ip-gen-status{font-size:12px;font-weight:600;min-height:16px;color:var(--rk-text-muted);text-align:left;padding-left:2px}
|
|
1265
|
+
.rk-ip-gen-status[hidden]{display:none}
|
|
1228
1266
|
.rk-ip-gen-status[data-tone="error"]{color:#b42318}
|
|
1229
1267
|
.rk-ip-gen-status[data-tone="success"]{color:#1f7a44}
|
|
1268
|
+
.rk-ip-gen-presets{display:flex;flex-wrap:wrap;gap:6px}
|
|
1269
|
+
.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)}
|
|
1270
|
+
.rk-ip-preset:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1230
1271
|
|
|
1231
|
-
/*
|
|
1272
|
+
/* ---- attach pop panel (shared file explorer) ----------------------------- */
|
|
1232
1273
|
.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)}
|
|
1233
|
-
@keyframes rk-ip-fade{from{opacity:0}to{opacity:1}}
|
|
1234
1274
|
.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}
|
|
1235
1275
|
.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)}
|
|
1236
1276
|
.rk-ip-filemodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
|
|
@@ -1241,6 +1281,37 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1241
1281
|
.rk-ip-filemodal-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted);cursor:pointer;padding:0 4px}
|
|
1242
1282
|
.rk-ip-filemodal-close:hover{color:var(--rk-ink)}
|
|
1243
1283
|
.rk-ip-filemodal-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}
|
|
1284
|
+
@keyframes rk-ip-spin{to{transform:rotate(360deg)}}
|
|
1285
|
+
@keyframes rk-ip-fade{from{opacity:0}to{opacity:1}}
|
|
1286
|
+
@media(max-width:820px){.rk-ip-chat{height:auto;min-height:0}.rk-ip-feed{max-height:62vh}}
|
|
1287
|
+
|
|
1288
|
+
.rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:34px 20px;margin:auto 0}
|
|
1289
|
+
.rk-inpaint-empty[hidden]{display:none}
|
|
1290
|
+
.rk-inpaint-empty-ic{color:var(--rk-n-400)}
|
|
1291
|
+
.rk-inpaint-empty-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink)}
|
|
1292
|
+
.rk-inpaint-empty-sub{font-size:13px}
|
|
1293
|
+
.editor-inpaint-canvas-wrap{position:relative;display:inline-block;line-height:0;max-width:100%;border-radius:var(--rk-r-2xl);overflow:hidden;box-shadow:var(--rk-shadow-md);
|
|
1294
|
+
--inpaint-brush-cursor-size:70px;--inpaint-brush-cursor-x:0px;--inpaint-brush-cursor-y:0px}
|
|
1295
|
+
.editor-inpaint-canvas-wrap[hidden]{display:none}
|
|
1296
|
+
.editor-inpaint-canvas-wrap img{display:block;max-width:100%;max-height:min(52vh,520px);width:auto;height:auto;object-fit:contain;user-select:none}
|
|
1297
|
+
/* paint toolbar — sits directly under the image it paints, never over it */
|
|
1298
|
+
/* align-items:center keeps the canvas wrap at the image's intrinsic width — stretching
|
|
1299
|
+
it would stretch the absolutely-positioned mask canvas and skew stroke coordinates. */
|
|
1300
|
+
.rk-ip-paintstack{display:inline-flex;flex-direction:column;align-items:center;max-width:100%}
|
|
1301
|
+
.rk-ip-painttools{width:100%;box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:6px;padding:6px 8px;border-top:1px solid var(--rk-border);background:var(--rk-n-50);line-height:1;flex-wrap:wrap}
|
|
1302
|
+
.rk-ip-painttools-brush{display:flex;align-items:center;gap:6px}
|
|
1303
|
+
.rk-ip-painttools-label{font-size:11px;font-weight:800;color:var(--rk-text-muted);white-space:nowrap}
|
|
1304
|
+
.rk-ip-painttools .rk-inpaint-range{width:86px}
|
|
1305
|
+
.rk-ip-painttool{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-700);cursor:pointer}
|
|
1306
|
+
.rk-ip-painttool:hover:not(:disabled){background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1307
|
+
.rk-ip-painttool:disabled{opacity:.45;cursor:default}
|
|
1308
|
+
.rk-ip-painttool svg{width:13px;height:13px}
|
|
1309
|
+
.editor-inpaint-canvas-wrap canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.86;cursor:none;touch-action:none}
|
|
1310
|
+
.editor-inpaint-brush-cursor{position:absolute;z-index:2;left:0;top:0;width:var(--inpaint-brush-cursor-size);height:var(--inpaint-brush-cursor-size);
|
|
1311
|
+
border:2px solid rgba(255,255,255,.95);border-radius:50%;box-shadow:0 0 0 1px rgba(28,25,23,.7),0 0 18px rgba(28,25,23,.25);opacity:0;pointer-events:none;
|
|
1312
|
+
transform:translate(var(--inpaint-brush-cursor-x),var(--inpaint-brush-cursor-y)) translate(-50%,-50%)}
|
|
1313
|
+
.editor-inpaint-brush-cursor[data-visible="true"]{opacity:1}
|
|
1314
|
+
|
|
1244
1315
|
|
|
1245
1316
|
/* right: result + history */
|
|
1246
1317
|
.rk-inpaint-output{padding:18px;display:grid;gap:18px;align-content:start}
|
|
@@ -1256,6 +1327,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1256
1327
|
.custom-inpaint-history-item[data-kind="source"]{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
1257
1328
|
.custom-inpaint-history-thumb-button{width:54px;height:54px;display:grid;padding:0;border:0;border-radius:var(--rk-r-lg);background:transparent;cursor:zoom-in}
|
|
1258
1329
|
.custom-inpaint-history-thumb{width:54px;height:54px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);object-fit:cover;background:var(--rk-n-200)}
|
|
1330
|
+
.custom-inpaint-history-videothumb{display:grid;place-items:center;font-size:16px;color:var(--rk-n-600);background:var(--rk-n-100)}
|
|
1331
|
+
.custom-inpaint-history-thumb-button{cursor:pointer}
|
|
1259
1332
|
.custom-inpaint-history-generating{width:54px;height:54px;display:grid;place-items:center;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-100)}
|
|
1260
1333
|
.custom-inpaint-history-generating::before{content:"";width:22px;height:22px;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}
|
|
1261
1334
|
.custom-inpaint-history-meta{min-width:0;display:grid;gap:5px}
|
|
@@ -1283,7 +1356,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1283
1356
|
.custom-inpaint-lightbox-title{min-width:0;margin:0;color:var(--rk-ink);font-size:14px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1284
1357
|
.custom-inpaint-lightbox-actions{display:flex;align-items:center;gap:8px}
|
|
1285
1358
|
.custom-inpaint-lightbox-stage{min-height:0;display:grid;place-items:center;overflow:auto}
|
|
1286
|
-
.custom-inpaint-lightbox-stage img{max-width:100%;max-height:calc(94svh - 92px);object-fit:contain;border-radius:var(--rk-r-xl);background:var(--rk-n-100)}
|
|
1359
|
+
.custom-inpaint-lightbox-stage img,.custom-inpaint-lightbox-stage video{max-width:100%;max-height:calc(94svh - 92px);object-fit:contain;border-radius:var(--rk-r-xl);background:var(--rk-n-100)}
|
|
1360
|
+
.custom-inpaint-lightbox-stage img[hidden],.custom-inpaint-lightbox-stage video[hidden]{display:none}
|
|
1287
1361
|
|
|
1288
1362
|
@keyframes rk-ip-spin{to{transform:rotate(360deg)}}
|
|
1289
1363
|
|
|
@@ -1394,17 +1468,18 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1394
1468
|
<div class="rk-inpaint-head-text">
|
|
1395
1469
|
<div class="rk-ip-tabs" role="tablist" aria-label="Editor mode"><a class="rk-ip-tab is-active" href="/tools/image" role="tab" aria-selected="true"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.5"/><path d="m4 17 4.5-4.5 3 3L16 11l4 4"/></svg><span>Image</span></a><a class="rk-ip-tab" href="/tools/video" role="tab" aria-selected="false"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="14" height="14" rx="2"/><path d="m17 9 4-2v10l-4-2"/></svg><span>Video</span></a><a class="rk-ip-tab" href="/tools/clipper" role="tab" aria-selected="false"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M8.1 8.1 20 18M8.1 15.9 20 6"/></svg><span>Clipper</span></a></div>
|
|
1396
1470
|
<h1 class="rk-h1">Create Image</h1>
|
|
1397
|
-
<p class="rk-sub">
|
|
1471
|
+
<p class="rk-sub">Chat to create an image, then keep chatting to refine it — every message edits the newest image, and only that image, so instructions stay sharp. Paint a mask on it to change only that region. To animate an image, take it to the Video tool.</p>
|
|
1398
1472
|
</div>
|
|
1399
1473
|
<div class="rk-inpaint-toolbar">
|
|
1400
1474
|
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-custom-inpaint-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>
|
|
1475
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-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>
|
|
1401
1476
|
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-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>
|
|
1402
1477
|
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-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>
|
|
1403
1478
|
</div>
|
|
1404
1479
|
</div>
|
|
1405
1480
|
|
|
1406
1481
|
<div class="rk-inpaint-workbench">
|
|
1407
|
-
<!-- LEFT: source +
|
|
1482
|
+
<!-- LEFT: source + per-color mask instructions (mask block appears once you paint) -->
|
|
1408
1483
|
<section class="rk-inpaint-panel" aria-label="Controls">
|
|
1409
1484
|
<div class="rk-inpaint-block">
|
|
1410
1485
|
<label class="rk-label">Source image</label>
|
|
@@ -1414,98 +1489,97 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1414
1489
|
<span class="rk-inpaint-drop-name" data-custom-inpaint-source-name>No image selected</span>
|
|
1415
1490
|
</button>
|
|
1416
1491
|
<input data-custom-inpaint-file type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" hidden />
|
|
1492
|
+
<span class="rk-hint">Optional — just type in the chat and the first generated image becomes your working image.</span>
|
|
1417
1493
|
</div>
|
|
1418
1494
|
|
|
1419
|
-
<div class="rk-inpaint-block">
|
|
1420
|
-
<label class="rk-label">Whole image prompt</label>
|
|
1421
|
-
<div class="rk-ip-promptfield">
|
|
1422
|
-
<textarea class="rk-textarea rk-ip-global" data-editor-inpaint-global-prompt placeholder="Describe the overall edit, style, constraints, or preservation notes (optional)."></textarea>
|
|
1423
|
-
<button type="button" class="rk-ip-attach" data-editor-inpaint-attach-scope="global" aria-label="Attach reference image" title="Attach reference image"><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>
|
|
1424
|
-
</div>
|
|
1425
|
-
<span class="editor-inpaint-attachment-chips rk-ip-attach-chips" data-editor-inpaint-attachments="global"></span>
|
|
1426
|
-
<input data-editor-inpaint-attachment-input type="file" accept="image/jpeg,image/png,image/webp" multiple hidden />
|
|
1427
|
-
<span class="rk-hint">Guidance for the masked edit and overall consistency. It never permits changing unmasked pixels.</span>
|
|
1428
|
-
</div>
|
|
1429
|
-
|
|
1430
|
-
<div class="rk-inpaint-block">
|
|
1495
|
+
<div class="rk-inpaint-block rk-ip-maskblock" data-ip-mask-block hidden>
|
|
1431
1496
|
<div class="rk-inpaint-brushrow">
|
|
1432
|
-
<label class="rk-label">
|
|
1433
|
-
<div class="rk-ip-toolbtns">
|
|
1434
|
-
<button type="button" class="rk-ip-iconbtn" data-editor-inpaint-undo aria-label="Undo stroke" title="Undo"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7.2 7H3.8V3.6"/><path d="M4 7a7 7 0 1 1 1.9 6.6"/></svg></button>
|
|
1435
|
-
<button type="button" class="rk-ip-iconbtn" data-editor-inpaint-redo aria-label="Redo stroke" title="Redo"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12.8 7h3.4V3.6"/><path d="M16 7a7 7 0 1 0-1.9 6.6"/></svg></button>
|
|
1436
|
-
<button type="button" class="rk-ip-iconbtn" data-editor-inpaint-clear aria-label="Clear mask" title="Clear mask"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10M15 5 5 15"/></svg></button>
|
|
1437
|
-
</div>
|
|
1438
|
-
</div>
|
|
1439
|
-
<input data-editor-inpaint-brush class="rk-inpaint-range" type="range" min="6" max="180" step="1" value="70" />
|
|
1440
|
-
</div>
|
|
1441
|
-
|
|
1442
|
-
<div class="rk-inpaint-block">
|
|
1443
|
-
<div class="rk-inpaint-brushrow">
|
|
1444
|
-
<label class="rk-label">Mask colors</label>
|
|
1497
|
+
<label class="rk-label">Mask regions</label>
|
|
1445
1498
|
<input class="rk-ip-colorpicker" data-editor-inpaint-color-picker type="color" value="#ff2d55" aria-label="Mask color" />
|
|
1446
1499
|
</div>
|
|
1447
|
-
<span class="rk-hint">Paint
|
|
1500
|
+
<span class="rk-hint">Paint on the image in the chat. Give each color its own replacement instruction — the chat box stays the whole-image prompt.</span>
|
|
1448
1501
|
<div class="editor-inpaint-swatches" data-editor-inpaint-swatches></div>
|
|
1449
1502
|
<div class="editor-inpaint-prompts" data-editor-inpaint-prompts></div>
|
|
1450
1503
|
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-ip-addcolor" data-editor-inpaint-add-color>+ Add color</button>
|
|
1451
1504
|
</div>
|
|
1452
1505
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1506
|
+
<!-- Legacy fields the chat drives behind the scenes: the composer text is written
|
|
1507
|
+
into the whole-image prompt, and the composer's send press clicks submit. -->
|
|
1508
|
+
<div class="rk-ip-legacy" aria-hidden="true">
|
|
1509
|
+
<textarea class="rk-textarea rk-ip-global" data-editor-inpaint-global-prompt></textarea>
|
|
1510
|
+
<button type="button" class="rk-ip-attach" data-editor-inpaint-attach-scope="global" aria-label="Attach reference image"><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>
|
|
1511
|
+
<span class="editor-inpaint-attachment-chips rk-ip-attach-chips" data-editor-inpaint-attachments="global"></span>
|
|
1512
|
+
<input data-editor-inpaint-attachment-input type="file" accept="image/jpeg,image/png,image/webp" multiple hidden />
|
|
1513
|
+
<button type="button" data-editor-inpaint-submit><svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l1.9 5.1 5.1 1.9-5.1 1.9L12 16.5l-1.9-5.1L5 9.5l5.1-1.9z"/></svg> <span data-editor-inpaint-submit-label>Generate edit</span></button>
|
|
1514
|
+
<div class="custom-inpaint-output" data-custom-inpaint-output hidden>
|
|
1515
|
+
<button type="button" data-custom-inpaint-result-open aria-label="View image edit result">
|
|
1516
|
+
<img data-custom-inpaint-result alt="Image edit result" />
|
|
1517
|
+
</button>
|
|
1518
|
+
<button type="button" data-custom-inpaint-save>Save to Files</button>
|
|
1519
|
+
</div>
|
|
1456
1520
|
</div>
|
|
1457
1521
|
</section>
|
|
1458
1522
|
|
|
1459
|
-
<!-- CENTER:
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
<
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
<textarea id="rk-ip-genprompt" class="rk-ip-gen-input" rows="2" placeholder="Or type your image prompt"></textarea>
|
|
1471
|
-
<div class="rk-ip-gen-presets" id="rk-ip-genpresets">
|
|
1472
|
-
<button type="button" class="rk-ip-preset" data-preset="character-card" title="Fill the prompt with a character reference sheet (multi-angle, expressions, poses)">+ Character card</button>
|
|
1473
|
-
<button type="button" class="rk-ip-preset" data-preset="storyboard" title="Fill the prompt with a numbered storyboard (sequential shot panels)">+ Storyboard</button>
|
|
1474
|
-
</div>
|
|
1475
|
-
<div class="rk-ip-gen-refs" id="rk-ip-genrefs" hidden></div>
|
|
1476
|
-
<div class="rk-ip-gen-bar">
|
|
1477
|
-
<button type="button" class="rk-ip-gen-clip" id="rk-ip-genclip" aria-label="Attach reference images from your files" title="Attach reference images from your files"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"/><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"/></svg></button>
|
|
1478
|
-
<span class="rk-ip-gen-hint">Generate a brand-new image, then paint to refine it.</span>
|
|
1479
|
-
<button type="button" class="rk-ip-gen-send" id="rk-ip-gensend" aria-label="Generate image"><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>
|
|
1480
|
-
</div>
|
|
1523
|
+
<!-- CENTER: chat feed + composer. Every turn appends to the feed; edits always
|
|
1524
|
+
apply to the LATEST image, which is the one you paint on in-feed. -->
|
|
1525
|
+
<section class="rk-ip-chat" aria-label="Create Image chat">
|
|
1526
|
+
<div class="rk-ip-feed" data-ip-feed>
|
|
1527
|
+
<div class="rk-inpaint-empty" data-custom-inpaint-empty>
|
|
1528
|
+
<span class="rk-inpaint-empty-ic" aria-hidden="true"><svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l1.9 5.1 5.1 1.9-5.1 1.9L12 16.5l-1.9-5.1L5 9.5l5.1-1.9z"/></svg></span>
|
|
1529
|
+
<p class="rk-inpaint-empty-title">Describe the image you want</p>
|
|
1530
|
+
<p class="rk-inpaint-empty-sub">Then keep chatting to refine it — every message edits the newest image. Paint a mask on it first to edit just that region.</p>
|
|
1531
|
+
<div class="rk-ip-gen-presets" id="rk-ip-genpresets">
|
|
1532
|
+
<button type="button" class="rk-ip-preset" data-preset="character-card" title="Fill the prompt with a character reference sheet (multi-angle, expressions, poses)">+ Character card</button>
|
|
1533
|
+
<button type="button" class="rk-ip-preset" data-preset="storyboard" title="Fill the prompt with a numbered storyboard (sequential shot panels)">+ Storyboard</button>
|
|
1481
1534
|
</div>
|
|
1482
|
-
<p class="rk-ip-gen-status" id="rk-ip-genstatus" data-tone="" aria-live="polite"></p>
|
|
1483
1535
|
</div>
|
|
1484
1536
|
</div>
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1537
|
+
|
|
1538
|
+
<!-- The paint surface. JS moves this whole stack into the newest image bubble,
|
|
1539
|
+
so the brush toolbar always travels with the image it paints. -->
|
|
1540
|
+
<div class="rk-ip-canvasholder" data-ip-canvas-holder hidden>
|
|
1541
|
+
<div class="rk-ip-paintstack" data-ip-paint-stack>
|
|
1542
|
+
<div class="rk-inpaint-canvaswrap editor-inpaint-canvas-wrap" data-custom-inpaint-canvas-wrap hidden>
|
|
1543
|
+
<img data-editor-inpaint-image class="rk-inpaint-img" alt="" />
|
|
1544
|
+
<canvas data-editor-inpaint-canvas class="rk-inpaint-canvas"></canvas>
|
|
1545
|
+
<div class="editor-inpaint-brush-cursor" data-editor-inpaint-brush-cursor aria-hidden="true"></div>
|
|
1546
|
+
</div>
|
|
1547
|
+
<div class="rk-ip-painttools" data-ip-painttools>
|
|
1548
|
+
<span class="rk-ip-painttools-brush">
|
|
1549
|
+
<span class="rk-ip-painttools-label">Brush <span data-editor-inpaint-brush-label>70</span></span>
|
|
1550
|
+
<input data-editor-inpaint-brush class="rk-inpaint-range" type="range" min="6" max="180" step="1" value="70" aria-label="Brush size" />
|
|
1551
|
+
</span>
|
|
1552
|
+
<button type="button" class="rk-ip-painttool" data-editor-inpaint-undo aria-label="Undo stroke" title="Undo stroke (⌘Z)"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7.2 7H3.8V3.6"/><path d="M4 7a7 7 0 1 1 1.9 6.6"/></svg></button>
|
|
1553
|
+
<button type="button" class="rk-ip-painttool" data-editor-inpaint-redo aria-label="Redo stroke" title="Redo stroke (⇧⌘Z)"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12.8 7h3.4V3.6"/><path d="M16 7a7 7 0 1 0-1.9 6.6"/></svg></button>
|
|
1554
|
+
<button type="button" class="rk-ip-painttool" data-editor-inpaint-clear aria-label="Clear mask" title="Clear mask"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10M15 5 5 15"/></svg></button>
|
|
1555
|
+
</div>
|
|
1556
|
+
</div>
|
|
1489
1557
|
</div>
|
|
1490
|
-
</section>
|
|
1491
1558
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
<div class="rk-
|
|
1496
|
-
<
|
|
1497
|
-
<
|
|
1559
|
+
<div class="rk-ip-composer" data-ip-composer>
|
|
1560
|
+
<div class="rk-ip-comp-mask" data-ip-comp-mask hidden></div>
|
|
1561
|
+
<div class="rk-ip-gen-refs" id="rk-ip-genrefs" hidden></div>
|
|
1562
|
+
<div class="rk-ip-comp-field">
|
|
1563
|
+
<textarea id="rk-ip-genprompt" class="rk-ip-comp-input" rows="2" placeholder="Describe the image you want…"></textarea>
|
|
1564
|
+
<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>
|
|
1498
1565
|
</div>
|
|
1499
|
-
<
|
|
1500
|
-
|
|
1501
|
-
|
|
1566
|
+
<div class="rk-ip-comp-bar">
|
|
1567
|
+
|
|
1568
|
+
<button type="button" class="rk-ip-gen-clip" id="rk-ip-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>
|
|
1569
|
+
<p class="rk-ip-comp-status custom-inpaint-status" data-ip-status data-custom-inpaint-status data-tone="" aria-live="polite"></p>
|
|
1570
|
+
<button type="button" class="rk-ip-gen-send" id="rk-ip-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>
|
|
1571
|
+
</div>
|
|
1572
|
+
<p class="rk-ip-gen-status" id="rk-ip-genstatus" data-tone="" aria-live="polite" hidden></p>
|
|
1502
1573
|
</div>
|
|
1574
|
+
</section>
|
|
1503
1575
|
|
|
1576
|
+
<!-- RIGHT: tracer-grouped session history -->
|
|
1577
|
+
<section class="rk-inpaint-output" aria-label="Session history">
|
|
1504
1578
|
<div class="rk-inpaint-history">
|
|
1505
1579
|
<div class="rk-inpaint-history-head">
|
|
1506
1580
|
<h2 class="rk-inpaint-h2">This session</h2>
|
|
1507
1581
|
<div class="rk-ip-histtools">
|
|
1508
|
-
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-history-refresh aria-label="
|
|
1582
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-history-refresh aria-label="Reload this session from the server" title="Reload this session"><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="M16.5 10a6.5 6.5 0 1 1-1.9-4.6"/><path d="M16.5 4.2v4.2h-4.2"/></svg></button>
|
|
1509
1583
|
</div>
|
|
1510
1584
|
</div>
|
|
1511
1585
|
<div class="custom-inpaint-history-list rk-inpaint-historylist" data-custom-inpaint-history-list>
|
|
@@ -1553,13 +1627,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1553
1627
|
<div class="custom-inpaint-lightbox-head">
|
|
1554
1628
|
<p class="custom-inpaint-lightbox-title" id="custom-inpaint-lightbox-title" data-custom-inpaint-lightbox-title>Image edit output</p>
|
|
1555
1629
|
<div class="custom-inpaint-lightbox-actions">
|
|
1556
|
-
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-lightbox-copy aria-label="Copy
|
|
1630
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-lightbox-copy aria-label="Copy file URL" title="Copy URL"><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>
|
|
1631
|
+
<a class="rk-ip-iconbtn" data-custom-inpaint-lightbox-link href="#" target="_blank" rel="noopener noreferrer" aria-label="Open the direct file URL" title="Open direct file URL"><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.6 11.4a3 3 0 0 0 4.3 0l2.4-2.4a3 3 0 1 0-4.3-4.3l-1 1"/><path d="M11.4 8.6a3 3 0 0 0-4.3 0l-2.4 2.4a3 3 0 1 0 4.3 4.3l1-1"/></svg></a>
|
|
1557
1632
|
<a class="rk-btn rk-btn-ink rk-btn-sm" data-custom-inpaint-lightbox-download href="#" target="_blank" rel="noopener noreferrer" download><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> Download</a>
|
|
1558
1633
|
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-lightbox-close aria-label="Close preview" title="Close"><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="M5 5l10 10"/><path d="M15 5 5 15"/></svg></button>
|
|
1559
1634
|
</div>
|
|
1560
1635
|
</div>
|
|
1561
1636
|
<div class="custom-inpaint-lightbox-stage">
|
|
1562
1637
|
<img data-custom-inpaint-lightbox-image alt="Image edit output preview" />
|
|
1638
|
+
<video data-custom-inpaint-lightbox-video controls playsinline hidden></video>
|
|
1563
1639
|
</div>
|
|
1564
1640
|
</div>
|
|
1565
1641
|
</div>
|
|
@@ -1651,6 +1727,282 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1651
1727
|
</aside>
|
|
1652
1728
|
</div>
|
|
1653
1729
|
<script>
|
|
1730
|
+
|
|
1731
|
+
(function(){
|
|
1732
|
+
var IMAGE_RE = /\.(png|jpe?g|gif|webp|avif)$/i;
|
|
1733
|
+
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>";
|
|
1734
|
+
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>";
|
|
1735
|
+
var MAX_ROWS = 10;
|
|
1736
|
+
|
|
1737
|
+
function isImageFile(f){ return /^image\//i.test((f && f.type) || '') || IMAGE_RE.test((f && f.name) || ''); }
|
|
1738
|
+
function imagesFromClipboard(e){
|
|
1739
|
+
var items = (e.clipboardData && e.clipboardData.items) || [], out = [];
|
|
1740
|
+
for(var i = 0; i < items.length; i++){
|
|
1741
|
+
if(items[i].kind === 'file'){ var f = items[i].getAsFile(); if(f && /^image\//i.test(f.type || '')) out.push(f); }
|
|
1742
|
+
}
|
|
1743
|
+
return out;
|
|
1744
|
+
}
|
|
1745
|
+
function dtHasFiles(e){
|
|
1746
|
+
var t = (e.dataTransfer && e.dataTransfer.types) || [];
|
|
1747
|
+
return Array.prototype.indexOf.call(t, 'Files') !== -1;
|
|
1748
|
+
}
|
|
1749
|
+
function plural(n, word){ return n + ' ' + word + (n > 1 ? 's' : ''); }
|
|
1750
|
+
|
|
1751
|
+
window.__rkChatComposer = function(cfg){
|
|
1752
|
+
cfg = cfg || {};
|
|
1753
|
+
var root = cfg.root, ta = cfg.textarea, refsEl = cfg.refsEl;
|
|
1754
|
+
var sendBtn = cfg.sendBtn, clipBtn = cfg.clipBtn;
|
|
1755
|
+
var uploadBlob = cfg.uploadBlob || function(){ return Promise.reject(new Error('Uploads are unavailable.')); };
|
|
1756
|
+
var status = cfg.onStatus || function(){};
|
|
1757
|
+
var canSendExtra = cfg.canSend || function(){ return false; };
|
|
1758
|
+
var refs = [];
|
|
1759
|
+
var busy = false;
|
|
1760
|
+
// Once the user drags the grip, their height wins: auto-grow stops fighting
|
|
1761
|
+
// them. Sending clears it so the next message starts compact again.
|
|
1762
|
+
var manualHeight = 0;
|
|
1763
|
+
|
|
1764
|
+
// ---- height ------------------------------------------------------------
|
|
1765
|
+
function maxHeight(){
|
|
1766
|
+
if(!ta) return 0;
|
|
1767
|
+
var s = window.getComputedStyle(ta);
|
|
1768
|
+
var line = parseFloat(s.lineHeight) || 20;
|
|
1769
|
+
var pad = (parseFloat(s.paddingTop) || 0) + (parseFloat(s.paddingBottom) || 0)
|
|
1770
|
+
+ (parseFloat(s.borderTopWidth) || 0) + (parseFloat(s.borderBottomWidth) || 0);
|
|
1771
|
+
return Math.round(line * MAX_ROWS + pad);
|
|
1772
|
+
}
|
|
1773
|
+
function applyHeight(px, cap){
|
|
1774
|
+
ta.style.height = px + 'px';
|
|
1775
|
+
ta.style.overflowY = ta.scrollHeight > px + 1 ? 'auto' : (cap ? 'auto' : 'hidden');
|
|
1776
|
+
}
|
|
1777
|
+
function autoGrow(){
|
|
1778
|
+
if(!ta) return;
|
|
1779
|
+
if(manualHeight){ applyHeight(manualHeight, true); return; }
|
|
1780
|
+
var max = maxHeight();
|
|
1781
|
+
ta.style.height = 'auto';
|
|
1782
|
+
applyHeight(Math.min(max, Math.max(56, ta.scrollHeight)), ta.scrollHeight > max);
|
|
1783
|
+
}
|
|
1784
|
+
function resetHeight(){ manualHeight = 0; if(ta){ ta.style.height = 'auto'; } autoGrow(); }
|
|
1785
|
+
|
|
1786
|
+
// Corner grip: pointer-drag the textarea taller/shorter. Bounded so it can
|
|
1787
|
+
// never collapse under one row or grow past the viewport.
|
|
1788
|
+
var field = ta && ta.parentElement;
|
|
1789
|
+
var grip = root && root.querySelector('[data-ip-grip]');
|
|
1790
|
+
if(grip && ta){
|
|
1791
|
+
grip.addEventListener('pointerdown', function(e){
|
|
1792
|
+
e.preventDefault();
|
|
1793
|
+
var startY = e.clientY, startH = ta.getBoundingClientRect().height;
|
|
1794
|
+
var maxPx = Math.max(120, Math.round(window.innerHeight * 0.6));
|
|
1795
|
+
try{ grip.setPointerCapture(e.pointerId); }catch(_){}
|
|
1796
|
+
if(field) field.classList.add('is-resizing');
|
|
1797
|
+
function move(ev){
|
|
1798
|
+
var next = Math.min(maxPx, Math.max(56, Math.round(startH + (ev.clientY - startY))));
|
|
1799
|
+
manualHeight = next;
|
|
1800
|
+
applyHeight(next, true);
|
|
1801
|
+
}
|
|
1802
|
+
function up(ev){
|
|
1803
|
+
try{ grip.releasePointerCapture(e.pointerId); }catch(_){}
|
|
1804
|
+
if(field) field.classList.remove('is-resizing');
|
|
1805
|
+
grip.removeEventListener('pointermove', move);
|
|
1806
|
+
grip.removeEventListener('pointerup', up);
|
|
1807
|
+
grip.removeEventListener('pointercancel', up);
|
|
1808
|
+
// A click with no travel resets to auto-grow — an escape hatch out of
|
|
1809
|
+
// a height you dragged and no longer want.
|
|
1810
|
+
if(Math.abs(ev.clientY - startY) < 3) resetHeight();
|
|
1811
|
+
}
|
|
1812
|
+
grip.addEventListener('pointermove', move);
|
|
1813
|
+
grip.addEventListener('pointerup', up);
|
|
1814
|
+
grip.addEventListener('pointercancel', up);
|
|
1815
|
+
});
|
|
1816
|
+
grip.addEventListener('dblclick', function(){ resetHeight(); });
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// ---- send / stop button ------------------------------------------------
|
|
1820
|
+
function syncSend(){
|
|
1821
|
+
if(!sendBtn) return;
|
|
1822
|
+
if(busy){
|
|
1823
|
+
sendBtn.disabled = false;
|
|
1824
|
+
sendBtn.dataset.mode = 'stop';
|
|
1825
|
+
sendBtn.setAttribute('aria-label', 'Stop generating');
|
|
1826
|
+
sendBtn.setAttribute('title', 'Stop generating');
|
|
1827
|
+
sendBtn.innerHTML = STOP_ICON;
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
sendBtn.dataset.mode = 'send';
|
|
1831
|
+
sendBtn.setAttribute('aria-label', 'Send');
|
|
1832
|
+
sendBtn.setAttribute('title', 'Send');
|
|
1833
|
+
sendBtn.disabled = !(Boolean((ta && ta.value || '').trim()) || canSendExtra());
|
|
1834
|
+
sendBtn.innerHTML = SEND_ICON;
|
|
1835
|
+
}
|
|
1836
|
+
function setBusy(v){
|
|
1837
|
+
busy = Boolean(v);
|
|
1838
|
+
if(root) root.classList.toggle('is-busy', busy);
|
|
1839
|
+
syncSend();
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// ---- reference chips ---------------------------------------------------
|
|
1843
|
+
function renderRefs(){
|
|
1844
|
+
if(!refsEl) return;
|
|
1845
|
+
refsEl.hidden = refs.length === 0;
|
|
1846
|
+
refsEl.innerHTML = '';
|
|
1847
|
+
refs.forEach(function(url, idx){
|
|
1848
|
+
var chip = document.createElement('span'); chip.className = 'rk-ip-gen-ref';
|
|
1849
|
+
var im = document.createElement('img'); im.src = url; im.alt = ''; im.loading = 'lazy'; chip.appendChild(im);
|
|
1850
|
+
var lbl = document.createElement('span');
|
|
1851
|
+
var nm = url.split('/').pop() || 'reference';
|
|
1852
|
+
lbl.textContent = decodeURIComponent(nm).slice(0, 28);
|
|
1853
|
+
chip.appendChild(lbl);
|
|
1854
|
+
var x = document.createElement('button'); x.type = 'button';
|
|
1855
|
+
x.setAttribute('aria-label', 'Remove reference'); x.innerHTML = '×';
|
|
1856
|
+
x.addEventListener('click', function(){ refs.splice(idx, 1); renderRefs(); });
|
|
1857
|
+
chip.appendChild(x);
|
|
1858
|
+
refsEl.appendChild(chip);
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Paste or drop image files straight into the composer -> temp upload -> ref.
|
|
1863
|
+
function addFiles(fileList){
|
|
1864
|
+
var files = Array.prototype.slice.call(fileList || []).filter(isImageFile);
|
|
1865
|
+
if(!files.length) return Promise.resolve(0);
|
|
1866
|
+
status('Uploading ' + plural(files.length, 'reference') + '\u2026', '');
|
|
1867
|
+
var added = 0;
|
|
1868
|
+
return files.reduce(function(chain, f){
|
|
1869
|
+
return chain.then(function(){
|
|
1870
|
+
return uploadBlob(f, f.name || 'pasted-image.png', f.type || 'image/png').then(function(url){
|
|
1871
|
+
if(url && refs.indexOf(url) === -1){ refs.push(url); added++; renderRefs(); }
|
|
1872
|
+
});
|
|
1873
|
+
});
|
|
1874
|
+
}, Promise.resolve()).then(function(){
|
|
1875
|
+
if(added) status(plural(added, 'reference') + ' attached.', 'success');
|
|
1876
|
+
return added;
|
|
1877
|
+
}).catch(function(err){
|
|
1878
|
+
status((err && err.message) ? err.message : 'Upload failed.', 'error');
|
|
1879
|
+
return added;
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// ---- attach pop panel (shared file-directory explorer) -----------------
|
|
1884
|
+
var modal = null;
|
|
1885
|
+
function onEsc(e){ if(e.key === 'Escape') closePanel(); }
|
|
1886
|
+
function closePanel(){
|
|
1887
|
+
if(!modal) return;
|
|
1888
|
+
try{ modal._handle && modal._handle.destroy && modal._handle.destroy(); }catch(_){}
|
|
1889
|
+
modal.remove(); modal = null;
|
|
1890
|
+
document.removeEventListener('keydown', onEsc, true);
|
|
1891
|
+
}
|
|
1892
|
+
function openPanel(){
|
|
1893
|
+
if(modal) return;
|
|
1894
|
+
var overlay = document.createElement('div'); overlay.className = 'rk-ip-filemodal';
|
|
1895
|
+
var panel = document.createElement('div'); panel.className = 'rk-ip-filemodal-panel';
|
|
1896
|
+
var head = document.createElement('div'); head.className = 'rk-ip-filemodal-head';
|
|
1897
|
+
var title = document.createElement('span'); title.className = 'rk-ip-filemodal-title';
|
|
1898
|
+
title.textContent = cfg.attachTitle || 'Attach reference images';
|
|
1899
|
+
var actions = document.createElement('div'); actions.className = 'rk-ip-filemodal-actions';
|
|
1900
|
+
var uploadBtn = document.createElement('button'); uploadBtn.type = 'button';
|
|
1901
|
+
uploadBtn.className = 'rk-ip-filemodal-upload'; uploadBtn.textContent = '\u2191 Upload from computer';
|
|
1902
|
+
var uploadInput = document.createElement('input'); uploadInput.type = 'file';
|
|
1903
|
+
uploadInput.accept = 'image/png,image/jpeg,image/webp,image/gif,image/avif';
|
|
1904
|
+
uploadInput.multiple = true; uploadInput.hidden = true;
|
|
1905
|
+
var close = document.createElement('button'); close.type = 'button';
|
|
1906
|
+
close.className = 'rk-ip-filemodal-close'; close.setAttribute('aria-label', 'Close'); close.innerHTML = '×';
|
|
1907
|
+
actions.appendChild(uploadBtn); actions.appendChild(close);
|
|
1908
|
+
head.appendChild(title); head.appendChild(actions); head.appendChild(uploadInput);
|
|
1909
|
+
uploadBtn.addEventListener('click', function(){ if(!uploadBtn.disabled) uploadInput.click(); });
|
|
1910
|
+
uploadInput.addEventListener('change', function(){
|
|
1911
|
+
var files = Array.prototype.slice.call(uploadInput.files || []).filter(isImageFile);
|
|
1912
|
+
uploadInput.value = '';
|
|
1913
|
+
if(!files.length){ status('Pick an image file to upload as a reference.', 'error'); return; }
|
|
1914
|
+
var original = uploadBtn.textContent;
|
|
1915
|
+
uploadBtn.disabled = true; uploadBtn.textContent = 'Uploading\u2026';
|
|
1916
|
+
addFiles(files).then(function(added){
|
|
1917
|
+
uploadBtn.disabled = false; uploadBtn.textContent = original;
|
|
1918
|
+
if(added) closePanel();
|
|
1919
|
+
else status('Those files were already attached.', '');
|
|
1920
|
+
});
|
|
1921
|
+
});
|
|
1922
|
+
var mountEl = document.createElement('div'); mountEl.className = 'rk-ip-filemodal-mount';
|
|
1923
|
+
panel.appendChild(head); panel.appendChild(mountEl); overlay.appendChild(panel);
|
|
1924
|
+
document.body.appendChild(overlay); modal = overlay;
|
|
1925
|
+
close.addEventListener('click', closePanel);
|
|
1926
|
+
overlay.addEventListener('click', function(e){ if(e.target === overlay) closePanel(); });
|
|
1927
|
+
document.addEventListener('keydown', onEsc, true);
|
|
1928
|
+
if(window.__vidfarmDirectory && window.__vidfarmDirectory.mount){
|
|
1929
|
+
modal._handle = window.__vidfarmDirectory.mount(mountEl, {
|
|
1930
|
+
mode: 'attach', showMultiSelect: true, primaryClick: 'attach',
|
|
1931
|
+
onAttach: function(items){
|
|
1932
|
+
var added = 0;
|
|
1933
|
+
(items || []).forEach(function(it){
|
|
1934
|
+
var ct = it.contentType || '';
|
|
1935
|
+
var isImg = ct.indexOf('image/') === 0 || /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(it.name || '');
|
|
1936
|
+
if(it.viewUrl && isImg && refs.indexOf(it.viewUrl) === -1){ refs.push(it.viewUrl); added++; }
|
|
1937
|
+
});
|
|
1938
|
+
renderRefs();
|
|
1939
|
+
if(added){ status(plural(added, 'reference') + ' attached.', 'success'); closePanel(); }
|
|
1940
|
+
else status('Pick an image file to use as a reference.', 'error');
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
} else {
|
|
1944
|
+
mountEl.innerHTML = '<p style="padding:16px;color:var(--rk-text-muted);font-size:13px">File browser is unavailable right now.</p>';
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// ---- wiring ------------------------------------------------------------
|
|
1949
|
+
if(ta){
|
|
1950
|
+
ta.addEventListener('input', function(){ autoGrow(); syncSend(); });
|
|
1951
|
+
ta.addEventListener('keydown', function(e){
|
|
1952
|
+
if(e.key !== 'Enter' || e.shiftKey) return;
|
|
1953
|
+
e.preventDefault();
|
|
1954
|
+
// Enter is ignored mid-turn so an impatient double-send cannot queue a
|
|
1955
|
+
// second job; the draft stays put.
|
|
1956
|
+
if(busy) return;
|
|
1957
|
+
if(cfg.onSend) cfg.onSend();
|
|
1958
|
+
});
|
|
1959
|
+
ta.addEventListener('paste', function(e){
|
|
1960
|
+
var imgs = imagesFromClipboard(e);
|
|
1961
|
+
if(imgs.length){ e.preventDefault(); addFiles(imgs); }
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
if(clipBtn) clipBtn.addEventListener('click', openPanel);
|
|
1965
|
+
if(sendBtn) sendBtn.addEventListener('click', function(){
|
|
1966
|
+
if(busy){ if(cfg.onStop) cfg.onStop(); return; }
|
|
1967
|
+
if(cfg.onSend) cfg.onSend();
|
|
1968
|
+
});
|
|
1969
|
+
if(root){
|
|
1970
|
+
['dragenter', 'dragover'].forEach(function(ev){
|
|
1971
|
+
root.addEventListener(ev, function(e){
|
|
1972
|
+
if(dtHasFiles(e)){ e.preventDefault(); e.stopPropagation(); root.classList.add('is-dragref'); }
|
|
1973
|
+
});
|
|
1974
|
+
});
|
|
1975
|
+
['dragleave', 'dragend'].forEach(function(ev){
|
|
1976
|
+
root.addEventListener(ev, function(){ root.classList.remove('is-dragref'); });
|
|
1977
|
+
});
|
|
1978
|
+
root.addEventListener('drop', function(e){
|
|
1979
|
+
if(!dtHasFiles(e)) return;
|
|
1980
|
+
e.preventDefault(); e.stopPropagation();
|
|
1981
|
+
root.classList.remove('is-dragref');
|
|
1982
|
+
addFiles(e.dataTransfer.files);
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
autoGrow();
|
|
1987
|
+
syncSend();
|
|
1988
|
+
|
|
1989
|
+
return {
|
|
1990
|
+
refs: function(){ return refs.slice(); },
|
|
1991
|
+
setRefs: function(list){ refs = (list || []).slice(); renderRefs(); },
|
|
1992
|
+
clearRefs: function(){ refs = []; renderRefs(); },
|
|
1993
|
+
addFiles: addFiles,
|
|
1994
|
+
renderRefs: renderRefs,
|
|
1995
|
+
autoGrow: autoGrow,
|
|
1996
|
+
resetHeight: resetHeight,
|
|
1997
|
+
setBusy: setBusy,
|
|
1998
|
+
isBusy: function(){ return busy; },
|
|
1999
|
+
syncSend: syncSend,
|
|
2000
|
+
closePanel: closePanel,
|
|
2001
|
+
setPlaceholder: function(s){ if(ta) ta.placeholder = s; }
|
|
2002
|
+
};
|
|
2003
|
+
};
|
|
2004
|
+
})();
|
|
2005
|
+
|
|
1654
2006
|
(function(){
|
|
1655
2007
|
const uploadButton = document.querySelector("[data-custom-inpaint-upload]");
|
|
1656
2008
|
const startNewButton = document.querySelector("[data-custom-inpaint-start-new]");
|
|
@@ -1697,7 +2049,16 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1697
2049
|
const lightboxImage = document.querySelector("[data-custom-inpaint-lightbox-image]");
|
|
1698
2050
|
const lightboxCopy = document.querySelector("[data-custom-inpaint-lightbox-copy]");
|
|
1699
2051
|
const lightboxDownload = document.querySelector("[data-custom-inpaint-lightbox-download]");
|
|
2052
|
+
const lightboxLink = document.querySelector("[data-custom-inpaint-lightbox-link]");
|
|
2053
|
+
const lightboxVideo = document.querySelector("[data-custom-inpaint-lightbox-video]");
|
|
1700
2054
|
const lightboxClose = document.querySelector("[data-custom-inpaint-lightbox-close]");
|
|
2055
|
+
// chat surface
|
|
2056
|
+
const chatFeed = document.querySelector("[data-ip-feed]");
|
|
2057
|
+
const canvasHolder = document.querySelector("[data-ip-canvas-holder]");
|
|
2058
|
+
const composerEl = document.querySelector("[data-ip-composer]");
|
|
2059
|
+
const maskBlock = document.querySelector("[data-ip-mask-block]");
|
|
2060
|
+
const maskChip = document.querySelector("[data-ip-comp-mask]");
|
|
2061
|
+
const paintStack = document.querySelector("[data-ip-paint-stack]");
|
|
1701
2062
|
if (!(uploadButton instanceof HTMLButtonElement)
|
|
1702
2063
|
|| !(startNewButton instanceof HTMLButtonElement)
|
|
1703
2064
|
|| !(fileInput instanceof HTMLInputElement)
|
|
@@ -1737,7 +2098,13 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1737
2098
|
|| !(lightboxImage instanceof HTMLImageElement)
|
|
1738
2099
|
|| !(lightboxCopy instanceof HTMLButtonElement)
|
|
1739
2100
|
|| !(lightboxDownload instanceof HTMLAnchorElement)
|
|
1740
|
-
|| !(lightboxClose instanceof HTMLButtonElement)
|
|
2101
|
+
|| !(lightboxClose instanceof HTMLButtonElement)
|
|
2102
|
+
|| !(chatFeed instanceof HTMLElement)
|
|
2103
|
+
|| !(canvasHolder instanceof HTMLElement)
|
|
2104
|
+
|| !(composerEl instanceof HTMLElement)
|
|
2105
|
+
|| !(maskBlock instanceof HTMLElement)
|
|
2106
|
+
|| !(maskChip instanceof HTMLElement)
|
|
2107
|
+
|| !(paintStack instanceof HTMLElement)) {
|
|
1741
2108
|
return;
|
|
1742
2109
|
}
|
|
1743
2110
|
|
|
@@ -1752,13 +2119,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1752
2119
|
let inpaintAttachments = [];
|
|
1753
2120
|
let pendingInpaintAttachmentScope = null;
|
|
1754
2121
|
let inpaintHistoryItems = [];
|
|
2122
|
+
// Chat state. Declared up here because the legacy helpers below (loadSourceUrl,
|
|
2123
|
+
// applyCompletedInpaintJob, resetWorkspace) push turns into it.
|
|
2124
|
+
let chatTurns = [];
|
|
2125
|
+
let activeTurnId = "";
|
|
1755
2126
|
// Local "starting image" history entries (uploaded / generated / deep-linked
|
|
1756
2127
|
// sources). They are not job runs, so they survive server-history reloads by
|
|
1757
2128
|
// being re-injected after each rebuild — this lets users see where a session
|
|
1758
2129
|
// started from.
|
|
1759
|
-
let sourceHistoryItems = [];
|
|
1760
2130
|
let historyTracerRefreshTimer = 0;
|
|
1761
|
-
const hiddenInpaintHistoryKeys = new Set();
|
|
1762
2131
|
const maxInpaintAttachments = 8;
|
|
1763
2132
|
const allowedInpaintAttachmentTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
1764
2133
|
|
|
@@ -1831,12 +2200,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1831
2200
|
: (sessionLocked ? "Edit generated \u2014 Start New to edit again" : "Generate edit");
|
|
1832
2201
|
}
|
|
1833
2202
|
|
|
1834
|
-
//
|
|
1835
|
-
//
|
|
1836
|
-
//
|
|
2203
|
+
// The chat surface iterates forever on the newest image, so the old
|
|
2204
|
+
// freeze-after-one-edit lock is gone: setSessionLocked() is kept as a no-op
|
|
2205
|
+
// so every legacy call site still type-checks and simply never locks.
|
|
1837
2206
|
let sessionLocked = false;
|
|
1838
2207
|
function applySessionLock() {
|
|
1839
|
-
const locked =
|
|
2208
|
+
const locked = false;
|
|
1840
2209
|
const workbench = document.querySelector(".rk-inpaint-workbench");
|
|
1841
2210
|
if (workbench instanceof HTMLElement) workbench.classList.toggle("is-locked", locked);
|
|
1842
2211
|
inpaintGlobalPrompt.readOnly = locked;
|
|
@@ -1854,8 +2223,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1854
2223
|
// Keep the submit button + label consistent with the lock while not loading.
|
|
1855
2224
|
if (inpaintSubmit.getAttribute("data-loading") !== "true") setSubmitLoading(false);
|
|
1856
2225
|
}
|
|
1857
|
-
function setSessionLocked(
|
|
1858
|
-
sessionLocked =
|
|
2226
|
+
function setSessionLocked() {
|
|
2227
|
+
sessionLocked = false;
|
|
1859
2228
|
applySessionLock();
|
|
1860
2229
|
}
|
|
1861
2230
|
|
|
@@ -1873,15 +2242,31 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1873
2242
|
return prefix + "-" + stamp + ".png";
|
|
1874
2243
|
}
|
|
1875
2244
|
|
|
1876
|
-
|
|
2245
|
+
// Fullscreen preview for any feed item — images and generated videos alike.
|
|
2246
|
+
function openImagePreview(url, label, downloadName, mediaKind) {
|
|
1877
2247
|
if (!url) return;
|
|
1878
|
-
|
|
1879
|
-
|
|
2248
|
+
const isVideo = mediaKind === "video";
|
|
2249
|
+
if (isVideo && lightboxVideo instanceof HTMLVideoElement) {
|
|
2250
|
+
lightboxVideo.src = url;
|
|
2251
|
+
lightboxVideo.hidden = false;
|
|
2252
|
+
lightboxImage.hidden = true;
|
|
2253
|
+
lightboxImage.removeAttribute("src");
|
|
2254
|
+
} else {
|
|
2255
|
+
if (lightboxVideo instanceof HTMLVideoElement) {
|
|
2256
|
+
lightboxVideo.pause();
|
|
2257
|
+
lightboxVideo.removeAttribute("src");
|
|
2258
|
+
lightboxVideo.hidden = true;
|
|
2259
|
+
}
|
|
2260
|
+
lightboxImage.hidden = false;
|
|
2261
|
+
lightboxImage.src = url;
|
|
2262
|
+
lightboxImage.alt = label || "Image edit output preview";
|
|
2263
|
+
}
|
|
1880
2264
|
lightboxTitle.textContent = label || "Image edit output";
|
|
1881
2265
|
lightboxCopy.dataset.copyUrl = url;
|
|
1882
2266
|
lightboxCopy.setAttribute("title", "Copy URL");
|
|
1883
2267
|
lightboxDownload.href = url;
|
|
1884
|
-
lightboxDownload.setAttribute("download", downloadName || imageDownloadName("inpaint-output"));
|
|
2268
|
+
lightboxDownload.setAttribute("download", downloadName || imageDownloadName(isVideo ? "video-output" : "inpaint-output"));
|
|
2269
|
+
if (lightboxLink instanceof HTMLAnchorElement) lightboxLink.href = url;
|
|
1885
2270
|
lightbox.hidden = false;
|
|
1886
2271
|
lightbox.setAttribute("data-open", "true");
|
|
1887
2272
|
document.body.style.overflow = "hidden";
|
|
@@ -1891,9 +2276,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1891
2276
|
lightbox.setAttribute("data-open", "false");
|
|
1892
2277
|
lightbox.hidden = true;
|
|
1893
2278
|
lightboxImage.removeAttribute("src");
|
|
2279
|
+
if (lightboxVideo instanceof HTMLVideoElement) {
|
|
2280
|
+
lightboxVideo.pause();
|
|
2281
|
+
lightboxVideo.removeAttribute("src");
|
|
2282
|
+
lightboxVideo.hidden = true;
|
|
2283
|
+
}
|
|
1894
2284
|
lightboxCopy.dataset.copyUrl = "";
|
|
1895
2285
|
lightboxCopy.setAttribute("title", "Copy URL");
|
|
1896
2286
|
lightboxDownload.href = "#";
|
|
2287
|
+
if (lightboxLink instanceof HTMLAnchorElement) lightboxLink.href = "#";
|
|
1897
2288
|
document.body.style.overflow = "";
|
|
1898
2289
|
}
|
|
1899
2290
|
|
|
@@ -2104,6 +2495,31 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2104
2495
|
}
|
|
2105
2496
|
renderAllAttachmentChips();
|
|
2106
2497
|
applySessionLock();
|
|
2498
|
+
syncMaskUi();
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// The mask controls only exist while a mask exists: the left block holds the
|
|
2502
|
+
// per-color instructions, and the composer grows a chip so it is obvious the
|
|
2503
|
+
// next message is a MASKED edit rather than a whole-image one.
|
|
2504
|
+
function syncMaskUi() {
|
|
2505
|
+
const painted = inpaintStrokes.length > 0;
|
|
2506
|
+
const showBlock = Boolean(sourceImageUrl) && (painted || redoStrokes.length > 0);
|
|
2507
|
+
maskBlock.hidden = !showBlock;
|
|
2508
|
+
const usedColors = new Set(inpaintStrokes.map((stroke) => normalizeHexColor(stroke.color)));
|
|
2509
|
+
maskChip.hidden = !painted;
|
|
2510
|
+
if (painted) {
|
|
2511
|
+
maskChip.innerHTML = "";
|
|
2512
|
+
const label = document.createElement("span");
|
|
2513
|
+
label.textContent = "Masked edit \u00b7 " + usedColors.size + " region" + (usedColors.size === 1 ? "" : "s") + " painted";
|
|
2514
|
+
const clear = document.createElement("button");
|
|
2515
|
+
clear.type = "button";
|
|
2516
|
+
clear.dataset.ipClearMask = "true";
|
|
2517
|
+
clear.textContent = "Clear mask";
|
|
2518
|
+
maskChip.append(label, clear);
|
|
2519
|
+
}
|
|
2520
|
+
inpaintUndo.disabled = !inpaintStrokes.length;
|
|
2521
|
+
inpaintRedo.disabled = !redoStrokes.length;
|
|
2522
|
+
inpaintClear.disabled = !painted;
|
|
2107
2523
|
}
|
|
2108
2524
|
|
|
2109
2525
|
function redrawInpaintMask() {
|
|
@@ -2161,6 +2577,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2161
2577
|
inpaintStrokes.push(currentInpaintStroke);
|
|
2162
2578
|
redoStrokes = [];
|
|
2163
2579
|
redrawInpaintMask();
|
|
2580
|
+
syncMaskUi();
|
|
2164
2581
|
}
|
|
2165
2582
|
|
|
2166
2583
|
function continueInpaintStroke(event) {
|
|
@@ -2451,22 +2868,17 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2451
2868
|
setStatus("The source image URL is invalid.", "error");
|
|
2452
2869
|
return;
|
|
2453
2870
|
}
|
|
2454
|
-
setSessionLocked(false);
|
|
2455
|
-
sourceImageUrl = url;
|
|
2456
2871
|
sourceImageName = firstSearchParam(["source_image_name", "image_name", "file_name"]) || sourceNameFromUrl(url);
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
inpaintImage.src = sourceImageUrl;
|
|
2468
|
-
recordSourceHistoryItem(sourceImageUrl, origin || "url", sourceImageName);
|
|
2469
|
-
setStatus("Source ready from URL.", "success");
|
|
2872
|
+
adoptLatestImage(url, sourceImageName);
|
|
2873
|
+
addTurn({
|
|
2874
|
+
role: "assistant",
|
|
2875
|
+
kind: "media",
|
|
2876
|
+
mediaKind: "image",
|
|
2877
|
+
url,
|
|
2878
|
+
note: origin === "generated" ? "generated" : "starting image"
|
|
2879
|
+
});
|
|
2880
|
+
recordSourceHistoryItem(url, origin || "url", sourceImageName);
|
|
2881
|
+
setStatus("Working image ready — describe an edit, or paint a mask on it first.", "success");
|
|
2470
2882
|
}
|
|
2471
2883
|
|
|
2472
2884
|
function clearInpaintUrlParams() {
|
|
@@ -2539,6 +2951,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2539
2951
|
currentInpaintStroke = null;
|
|
2540
2952
|
isInpaintPainting = false;
|
|
2541
2953
|
redrawInpaintMask();
|
|
2954
|
+
syncMaskUi();
|
|
2542
2955
|
}
|
|
2543
2956
|
|
|
2544
2957
|
function resetWorkspace() {
|
|
@@ -2546,7 +2959,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2546
2959
|
sourceImageUrl = "";
|
|
2547
2960
|
sourceImageName = "";
|
|
2548
2961
|
sourceName.textContent = "No image selected";
|
|
2549
|
-
emptyState.hidden = false;
|
|
2550
2962
|
canvasWrap.hidden = true;
|
|
2551
2963
|
outputPanel.hidden = true;
|
|
2552
2964
|
resultImage.removeAttribute("src");
|
|
@@ -2559,9 +2971,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2559
2971
|
pendingInpaintAttachmentScope = null;
|
|
2560
2972
|
activeInpaintColor = "#ff2d55";
|
|
2561
2973
|
inpaintColorPicker.value = activeInpaintColor;
|
|
2562
|
-
|
|
2974
|
+
chatTurns = [];
|
|
2975
|
+
activeTurnId = "";
|
|
2563
2976
|
resetMask();
|
|
2564
2977
|
renderInpaintControls();
|
|
2978
|
+
renderChat();
|
|
2565
2979
|
setStatus("", "");
|
|
2566
2980
|
}
|
|
2567
2981
|
|
|
@@ -2580,26 +2994,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2580
2994
|
const wasAutoPending = tracerAutoPending;
|
|
2581
2995
|
if (wasAutoPending) nameTracerFromSeed(fileName.replace(/\.[a-z0-9]+$/i, ""));
|
|
2582
2996
|
const uploadedUrl = await uploadTemporaryBlob(file, fileName, file.type || "image/png", "inpaints/" + getTracerValue());
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
sourceName.textContent = fileName;
|
|
2587
|
-
emptyState.hidden = true;
|
|
2588
|
-
canvasWrap.hidden = false;
|
|
2589
|
-
outputPanel.hidden = true;
|
|
2590
|
-
resultImage.removeAttribute("src");
|
|
2591
|
-
resetMask();
|
|
2592
|
-
inpaintImage.onload = () => {
|
|
2593
|
-
syncInpaintCanvasSize();
|
|
2594
|
-
resetMask();
|
|
2595
|
-
};
|
|
2596
|
-
inpaintImage.src = sourceImageUrl;
|
|
2997
|
+
const uploaded = absoluteUrl(uploadedUrl);
|
|
2998
|
+
adoptLatestImage(uploaded, fileName);
|
|
2999
|
+
addTurn({ role: "assistant", kind: "media", mediaKind: "image", url: uploaded, note: "uploaded image" });
|
|
2597
3000
|
if (wasAutoPending) {
|
|
2598
3001
|
void loadInpaintHistory();
|
|
2599
3002
|
void loadTracerSessions();
|
|
2600
3003
|
}
|
|
2601
|
-
recordSourceHistoryItem(
|
|
2602
|
-
setStatus("
|
|
3004
|
+
recordSourceHistoryItem(uploaded, "upload", fileName);
|
|
3005
|
+
setStatus("Uploaded — describe an edit, or paint a mask on it first.", "success");
|
|
2603
3006
|
} catch (error) {
|
|
2604
3007
|
console.error(error);
|
|
2605
3008
|
setStatus(error instanceof Error ? error.message : "Unable to upload source image.", "error");
|
|
@@ -2641,48 +3044,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2641
3044
|
};
|
|
2642
3045
|
}
|
|
2643
3046
|
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
));
|
|
2650
|
-
inpaintHistoryItems.unshift(item);
|
|
2651
|
-
inpaintHistoryItems.sort((a, b) => Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0));
|
|
2652
|
-
renderInpaintHistory();
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
// Record the image a session started from (upload / generation / deep-link) so
|
|
2656
|
-
// it appears in History. De-duplicated by URL; kept in sourceHistoryItems so a
|
|
2657
|
-
// subsequent server-history reload doesn't drop it.
|
|
2658
|
-
function recordSourceHistoryItem(url, origin, name) {
|
|
2659
|
-
if (!url) return;
|
|
2660
|
-
let abs = "";
|
|
2661
|
-
try { abs = absoluteUrl(url); } catch { return; }
|
|
2662
|
-
const key = "source:" + abs;
|
|
2663
|
-
let item = sourceHistoryItems.find((entry) => entry.key === key);
|
|
2664
|
-
if (!item) {
|
|
2665
|
-
item = {
|
|
2666
|
-
key,
|
|
2667
|
-
kind: "source",
|
|
2668
|
-
origin: origin || "upload",
|
|
2669
|
-
url: abs,
|
|
2670
|
-
name: name || sourceNameFromUrl(abs),
|
|
2671
|
-
createdAt: new Date().toISOString()
|
|
2672
|
-
};
|
|
2673
|
-
sourceHistoryItems.push(item);
|
|
2674
|
-
}
|
|
2675
|
-
upsertInpaintHistoryItem(item);
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
function reinjectSourceHistoryItems() {
|
|
2679
|
-
for (const item of sourceHistoryItems) {
|
|
2680
|
-
if (!inpaintHistoryItems.some((entry) => entry.key === item.key)) {
|
|
2681
|
-
inpaintHistoryItems.push(item);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
inpaintHistoryItems.sort((a, b) => Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0));
|
|
2685
|
-
}
|
|
3047
|
+
// The right-hand session rail is DERIVED from the chat (see syncSessionAssets),
|
|
3048
|
+
// so the old push-based item store is gone. These two shims stay because the
|
|
3049
|
+
// legacy job-polling paths still call them; the chat is the source of truth.
|
|
3050
|
+
function upsertInpaintHistoryItem() {}
|
|
3051
|
+
function recordSourceHistoryItem() {}
|
|
2686
3052
|
|
|
2687
3053
|
// Mirror each completed inpaint output into /temp/inpaints/<tracer>/ so a
|
|
2688
3054
|
// session's outputs are browsable as files (grouped by the same tracer that
|
|
@@ -2713,16 +3079,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2713
3079
|
const url = absoluteUrl(outputUrl);
|
|
2714
3080
|
const jobId = String(payload?.job_id || payload?.jobId || "");
|
|
2715
3081
|
resultImage.src = url;
|
|
2716
|
-
|
|
2717
|
-
|
|
3082
|
+
// The finished edit becomes the newest image in the chat — and therefore
|
|
3083
|
+
// the thing the next message edits and the surface you paint on.
|
|
3084
|
+
resolveActiveTurn({ kind: "media", mediaKind: "image", url, jobId });
|
|
3085
|
+
adoptLatestImage(url);
|
|
2718
3086
|
void persistInpaintOutputToTemp(url, jobId);
|
|
2719
|
-
upsertInpaintHistoryItem({
|
|
2720
|
-
key: historyItemKey({ jobId: jobId || "inpaint" }, url, 0),
|
|
2721
|
-
kind: "output",
|
|
2722
|
-
jobId,
|
|
2723
|
-
url,
|
|
2724
|
-
createdAt: String(payload?.completed_at || payload?.updated_at || new Date().toISOString())
|
|
2725
|
-
});
|
|
2726
3087
|
return url;
|
|
2727
3088
|
}
|
|
2728
3089
|
|
|
@@ -2736,12 +3097,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2736
3097
|
try {
|
|
2737
3098
|
const parsed = new URL(value, window.location.href);
|
|
2738
3099
|
const storageKey = parsed.searchParams.get("key") || decodeURIComponent(parsed.pathname).replace(/^\/+/, "");
|
|
2739
|
-
return !/\/temporary\/inpaints\/
|
|
3100
|
+
return !/\/temporary\/inpaints\/_(?:inputs|context)(?:\/|$)/i.test(storageKey)
|
|
2740
3101
|
&& !/\/temporary\/inpaint(?:-|\/|$)/i.test(storageKey)
|
|
2741
3102
|
&& !/\/temporary\/inpaint-masks(?:\/|$)/i.test(storageKey)
|
|
2742
3103
|
&& !/inpaint-mask-[^/]+\.png/i.test(storageKey);
|
|
2743
3104
|
} catch {
|
|
2744
|
-
return !/\/temporary\/inpaints\/
|
|
3105
|
+
return !/\/temporary\/inpaints\/_(?:inputs|context)(?:\/|$)/i.test(value)
|
|
2745
3106
|
&& !/\/temporary\/inpaint(?:-|\/|$)/i.test(value)
|
|
2746
3107
|
&& !/\/temporary\/inpaint-masks(?:\/|$)/i.test(value)
|
|
2747
3108
|
&& !/inpaint-mask-[^/]+\.png/i.test(value);
|
|
@@ -2749,16 +3110,21 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2749
3110
|
});
|
|
2750
3111
|
}
|
|
2751
3112
|
|
|
2752
|
-
|
|
2753
|
-
|
|
3113
|
+
// Which chat turn an api-history entry represents. These four routes ARE the
|
|
3114
|
+
// conversation — everything else in the customer's run history is ignored here.
|
|
3115
|
+
function chatEntryKind(entry) {
|
|
2754
3116
|
const path = String(entry?.path || "").toLowerCase();
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
3117
|
+
const templateId = String(entry?.linkedJob?.template_id || entry?.linkedJob?.templateId || "").toLowerCase();
|
|
3118
|
+
if (path.includes("/primitives/images/inpaint") || templateId === "primitive:image_inpaint") return "inpaint";
|
|
3119
|
+
if (path.includes("/primitives/images/edit") || templateId === "primitive:image_edit") return "edit";
|
|
3120
|
+
if (path.includes("/primitives/images/generate") || templateId === "primitive:image_generate") return "generate";
|
|
3121
|
+
if (path.includes("/primitives/videos/generate") || templateId === "primitive:video_generate") return "video";
|
|
3122
|
+
return "";
|
|
2758
3123
|
}
|
|
2759
3124
|
|
|
2760
|
-
function
|
|
2761
|
-
|
|
3125
|
+
function historyEntryVideoUrls(entry) {
|
|
3126
|
+
const urls = Array.isArray(entry?.outputMediaUrls) ? entry.outputMediaUrls : [];
|
|
3127
|
+
return urls.filter((url) => /\.(mp4|mov|webm|m4v)(\?|#|$)/i.test(String(url || "")));
|
|
2762
3128
|
}
|
|
2763
3129
|
|
|
2764
3130
|
function setHistoryEmpty(message) {
|
|
@@ -2769,14 +3135,46 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2769
3135
|
historyList.append(empty);
|
|
2770
3136
|
}
|
|
2771
3137
|
|
|
3138
|
+
// ---- right rail: every asset this session produced, derived from the chat ----
|
|
3139
|
+
function syncSessionAssets() {
|
|
3140
|
+
const items = [];
|
|
3141
|
+
for (let i = chatTurns.length - 1; i >= 0; i--) {
|
|
3142
|
+
const turn = chatTurns[i];
|
|
3143
|
+
if (turn.role !== "assistant") continue;
|
|
3144
|
+
if (turn.kind === "media" && turn.url) {
|
|
3145
|
+
items.push({
|
|
3146
|
+
key: turn.id,
|
|
3147
|
+
kind: "output",
|
|
3148
|
+
mediaKind: turn.mediaKind === "video" ? "video" : "image",
|
|
3149
|
+
url: turn.url,
|
|
3150
|
+
jobId: turn.jobId || "",
|
|
3151
|
+
title: turn.prompt || turn.note || (turn.mediaKind === "video" ? "Video" : "Image"),
|
|
3152
|
+
createdAt: turn.at
|
|
3153
|
+
});
|
|
3154
|
+
} else if (turn.kind === "pending") {
|
|
3155
|
+
items.push({
|
|
3156
|
+
key: turn.id,
|
|
3157
|
+
kind: "pending",
|
|
3158
|
+
jobId: turn.jobId || "",
|
|
3159
|
+
title: turn.prompt || "Working…",
|
|
3160
|
+
status: turn.label || "",
|
|
3161
|
+
createdAt: turn.at
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
inpaintHistoryItems = items;
|
|
3166
|
+
renderInpaintHistory();
|
|
3167
|
+
}
|
|
3168
|
+
|
|
2772
3169
|
function renderInpaintHistory() {
|
|
2773
3170
|
historyList.innerHTML = "";
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
3171
|
+
if (!inpaintHistoryItems.length) {
|
|
3172
|
+
setHistoryEmpty(resolveTracer()
|
|
3173
|
+
? "Nothing in this session yet. Send a message to create your first image."
|
|
3174
|
+
: "New session — outputs land here as you chat.");
|
|
2777
3175
|
return;
|
|
2778
3176
|
}
|
|
2779
|
-
for (const item of
|
|
3177
|
+
for (const item of inpaintHistoryItems) {
|
|
2780
3178
|
const row = document.createElement("div");
|
|
2781
3179
|
row.className = "custom-inpaint-history-item";
|
|
2782
3180
|
row.dataset.historyKey = item.key;
|
|
@@ -2785,158 +3183,83 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2785
3183
|
if (item.kind === "pending") {
|
|
2786
3184
|
thumbButton = document.createElement("div");
|
|
2787
3185
|
thumbButton.className = "custom-inpaint-history-generating";
|
|
2788
|
-
thumbButton.setAttribute("aria-label", "
|
|
3186
|
+
thumbButton.setAttribute("aria-label", "Generating");
|
|
2789
3187
|
thumbButton.setAttribute("role", "img");
|
|
2790
3188
|
} else {
|
|
2791
3189
|
thumbButton = document.createElement("button");
|
|
2792
3190
|
thumbButton.type = "button";
|
|
2793
3191
|
thumbButton.className = "custom-inpaint-history-thumb-button";
|
|
2794
3192
|
thumbButton.dataset.historyPreview = item.key;
|
|
2795
|
-
thumbButton.setAttribute("aria-label", "
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3193
|
+
thumbButton.setAttribute("aria-label", "Jump to this message");
|
|
3194
|
+
if (item.mediaKind === "video") {
|
|
3195
|
+
const badge = document.createElement("span");
|
|
3196
|
+
badge.className = "custom-inpaint-history-thumb custom-inpaint-history-videothumb";
|
|
3197
|
+
badge.textContent = "▶";
|
|
3198
|
+
thumbButton.append(badge);
|
|
3199
|
+
} else {
|
|
3200
|
+
const thumb = document.createElement("img");
|
|
3201
|
+
thumb.className = "custom-inpaint-history-thumb";
|
|
3202
|
+
thumb.src = item.url;
|
|
3203
|
+
thumb.alt = "";
|
|
3204
|
+
thumb.loading = "lazy";
|
|
3205
|
+
thumbButton.append(thumb);
|
|
3206
|
+
}
|
|
2802
3207
|
}
|
|
2803
3208
|
const meta = document.createElement("div");
|
|
2804
3209
|
meta.className = "custom-inpaint-history-meta";
|
|
2805
3210
|
const title = document.createElement("div");
|
|
2806
3211
|
title.className = "custom-inpaint-history-title";
|
|
2807
|
-
title.textContent = item.
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
: (item.createdAt ?
|
|
2814
|
-
|
|
2815
|
-
const subtitle = document.createElement("div");
|
|
2816
|
-
subtitle.className = "custom-inpaint-history-subtitle";
|
|
2817
|
-
const progress = Number(item.progress || 0);
|
|
2818
|
-
subtitle.textContent = "Job " + (item.status || "queued") + (progress > 0 ? " · " + Math.round(progress * 100) + "%" : "");
|
|
2819
|
-
meta.append(title, subtitle);
|
|
2820
|
-
} else if (item.kind === "source") {
|
|
2821
|
-
const subtitle = document.createElement("div");
|
|
2822
|
-
subtitle.className = "custom-inpaint-history-subtitle";
|
|
2823
|
-
subtitle.textContent = "Where you started" + (item.createdAt ? " · " + new Date(item.createdAt).toLocaleString() : "");
|
|
2824
|
-
meta.append(title, subtitle);
|
|
2825
|
-
} else {
|
|
2826
|
-
meta.append(title);
|
|
2827
|
-
}
|
|
3212
|
+
title.textContent = item.title;
|
|
3213
|
+
title.title = item.title;
|
|
3214
|
+
const subtitle = document.createElement("div");
|
|
3215
|
+
subtitle.className = "custom-inpaint-history-subtitle";
|
|
3216
|
+
subtitle.textContent = item.kind === "pending"
|
|
3217
|
+
? (item.status || "Working…")
|
|
3218
|
+
: (item.mediaKind === "video" ? "Video" : "Image") + (item.createdAt ? " · " + relTimeShort(item.createdAt) : "");
|
|
3219
|
+
meta.append(title, subtitle);
|
|
2828
3220
|
const actions = document.createElement("div");
|
|
2829
3221
|
actions.className = "custom-inpaint-history-actions";
|
|
2830
3222
|
if (item.kind === "pending") {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3223
|
+
if (item.jobId) {
|
|
3224
|
+
const check = document.createElement("button");
|
|
3225
|
+
check.type = "button";
|
|
3226
|
+
check.className = "custom-inpaint-history-check";
|
|
3227
|
+
check.dataset.historyCheck = item.jobId;
|
|
3228
|
+
check.textContent = "Check Status";
|
|
3229
|
+
actions.append(check);
|
|
3230
|
+
}
|
|
2837
3231
|
} else {
|
|
2838
3232
|
const download = document.createElement("a");
|
|
2839
3233
|
download.className = "custom-inpaint-history-icon";
|
|
2840
3234
|
download.href = item.url;
|
|
2841
3235
|
download.target = "_blank";
|
|
2842
3236
|
download.rel = "noopener noreferrer";
|
|
2843
|
-
download.download = imageDownloadName("
|
|
2844
|
-
download.setAttribute("aria-label", "Download
|
|
3237
|
+
download.download = imageDownloadName(item.mediaKind === "video" ? "vidfarm-video" : "vidfarm-image", item.createdAt);
|
|
3238
|
+
download.setAttribute("aria-label", "Download");
|
|
2845
3239
|
download.setAttribute("title", "Download");
|
|
2846
3240
|
download.innerHTML = '<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="M10 3v9"></path><path d="m6.5 8.5 3.5 3.5 3.5-3.5"></path><path d="M4 16h12"></path></svg>';
|
|
2847
3241
|
const copy = document.createElement("button");
|
|
2848
3242
|
copy.type = "button";
|
|
2849
3243
|
copy.className = "custom-inpaint-history-icon";
|
|
2850
3244
|
copy.dataset.historyCopy = item.key;
|
|
2851
|
-
copy.setAttribute("aria-label", "Copy
|
|
3245
|
+
copy.setAttribute("aria-label", "Copy the file URL");
|
|
2852
3246
|
copy.setAttribute("title", "Copy URL");
|
|
2853
3247
|
copy.innerHTML = '<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="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><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"></path></svg>';
|
|
2854
3248
|
const save = document.createElement("button");
|
|
2855
3249
|
save.type = "button";
|
|
2856
3250
|
save.className = "custom-inpaint-history-icon";
|
|
2857
3251
|
save.dataset.historySave = item.key;
|
|
2858
|
-
save.setAttribute("aria-label", "Save
|
|
2859
|
-
save.setAttribute("title", "Save to Files");
|
|
3252
|
+
save.setAttribute("aria-label", "Save to My Files");
|
|
3253
|
+
save.setAttribute("title", "Save to My Files");
|
|
2860
3254
|
save.innerHTML = '<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="M5 4h8l2 2v10H5z"></path><path d="M8 4v4h4V4"></path><path d="M8 12h4"></path></svg>';
|
|
2861
3255
|
actions.append(download, copy, save);
|
|
2862
3256
|
}
|
|
2863
|
-
const remove = document.createElement("button");
|
|
2864
|
-
remove.type = "button";
|
|
2865
|
-
remove.className = "custom-inpaint-history-icon";
|
|
2866
|
-
remove.dataset.historyRemove = item.key;
|
|
2867
|
-
remove.setAttribute("aria-label", "Remove inpaint output from history strip");
|
|
2868
|
-
remove.setAttribute("title", "Remove");
|
|
2869
|
-
remove.innerHTML = '<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="M5 5l10 10"></path><path d="M15 5 5 15"></path></svg>';
|
|
2870
|
-
actions.append(remove);
|
|
2871
3257
|
meta.append(actions);
|
|
2872
|
-
|
|
2873
|
-
confirm.className = "custom-inpaint-history-popconfirm";
|
|
2874
|
-
confirm.hidden = true;
|
|
2875
|
-
confirm.dataset.historyConfirm = item.key;
|
|
2876
|
-
const confirmText = document.createElement("p");
|
|
2877
|
-
confirmText.textContent = "Remove from this history strip?";
|
|
2878
|
-
const confirmActions = document.createElement("div");
|
|
2879
|
-
confirmActions.className = "custom-inpaint-history-popconfirm-actions";
|
|
2880
|
-
const cancel = document.createElement("button");
|
|
2881
|
-
cancel.type = "button";
|
|
2882
|
-
cancel.dataset.historyCancel = item.key;
|
|
2883
|
-
cancel.textContent = "Cancel";
|
|
2884
|
-
const confirmRemove = document.createElement("button");
|
|
2885
|
-
confirmRemove.type = "button";
|
|
2886
|
-
confirmRemove.dataset.historyConfirmRemove = item.key;
|
|
2887
|
-
confirmRemove.dataset.danger = "true";
|
|
2888
|
-
confirmRemove.textContent = "Remove";
|
|
2889
|
-
confirmActions.append(cancel, confirmRemove);
|
|
2890
|
-
confirm.append(confirmText, confirmActions);
|
|
2891
|
-
row.append(thumbButton, meta, confirm);
|
|
3258
|
+
row.append(thumbButton, meta);
|
|
2892
3259
|
historyList.append(row);
|
|
2893
3260
|
}
|
|
2894
3261
|
}
|
|
2895
3262
|
|
|
2896
|
-
function closeInpaintHistoryConfirms(exceptKey) {
|
|
2897
|
-
historyList.querySelectorAll("[data-history-confirm]").forEach((entry) => {
|
|
2898
|
-
if (!(entry instanceof HTMLElement)) return;
|
|
2899
|
-
entry.hidden = exceptKey ? entry.dataset.historyConfirm !== exceptKey : true;
|
|
2900
|
-
});
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
function extractInpaintHistoryItems(entries) {
|
|
2904
|
-
const items = [];
|
|
2905
|
-
const seen = new Set();
|
|
2906
|
-
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
2907
|
-
if (!isInpaintHistoryEntry(entry)) continue;
|
|
2908
|
-
const linkedJob = entry?.linkedJob || null;
|
|
2909
|
-
if (linkedJob && isPendingJobStatus(linkedJob.status)) {
|
|
2910
|
-
const pending = makePendingInpaintHistoryItem({
|
|
2911
|
-
job_id: linkedJob.job_id,
|
|
2912
|
-
status: linkedJob.status,
|
|
2913
|
-
progress: linkedJob.progress,
|
|
2914
|
-
created_at: linkedJob.created_at || entry.createdAt,
|
|
2915
|
-
updated_at: linkedJob.updated_at || entry.createdAt
|
|
2916
|
-
});
|
|
2917
|
-
if (pending && !seen.has(pending.key)) {
|
|
2918
|
-
seen.add(pending.key);
|
|
2919
|
-
items.push(pending);
|
|
2920
|
-
}
|
|
2921
|
-
continue;
|
|
2922
|
-
}
|
|
2923
|
-
historyEntryUrls(entry).forEach((rawUrl, index) => {
|
|
2924
|
-
const url = absoluteUrl(rawUrl);
|
|
2925
|
-
const key = historyItemKey(entry, url, index);
|
|
2926
|
-
if (seen.has(key)) return;
|
|
2927
|
-
seen.add(key);
|
|
2928
|
-
items.push({
|
|
2929
|
-
key,
|
|
2930
|
-
kind: "output",
|
|
2931
|
-
jobId: String(entry?.jobId || entry?.job_id || entry?.linkedJob?.job_id || ""),
|
|
2932
|
-
url,
|
|
2933
|
-
createdAt: String(entry.createdAt || new Date().toISOString())
|
|
2934
|
-
});
|
|
2935
|
-
});
|
|
2936
|
-
}
|
|
2937
|
-
return items;
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
3263
|
async function copyText(value) {
|
|
2941
3264
|
if (navigator.clipboard && window.isSecureContext) {
|
|
2942
3265
|
try {
|
|
@@ -2960,36 +3283,121 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2960
3283
|
return copied;
|
|
2961
3284
|
}
|
|
2962
3285
|
|
|
2963
|
-
|
|
3286
|
+
// ---- session restore -----------------------------------------------------
|
|
3287
|
+
// A session IS its chat. Every turn was a POST to one of the four primitive
|
|
3288
|
+
// routes, and the api-call history stores each request body — including the
|
|
3289
|
+
// the chat_message field we attach purely so the transcript rebuilds verbatim
|
|
3290
|
+
// (the API strips unknown envelope fields, so it never reaches a provider).
|
|
3291
|
+
function userTextFromEntry(entry, kind) {
|
|
3292
|
+
const body = (entry && entry.request && entry.request.body) || {};
|
|
3293
|
+
if (typeof body.chat_message === "string" && body.chat_message.trim()) return body.chat_message.trim();
|
|
3294
|
+
// Fallback for turns made before chat_message existed: recover what we can
|
|
3295
|
+
// from the prompt/instruction we composed.
|
|
3296
|
+
const payload = body.payload || {};
|
|
3297
|
+
const raw = String(payload.prompt || payload.instruction || "");
|
|
3298
|
+
if (!raw) return kind === "video" ? "(video)" : "(edit)";
|
|
3299
|
+
const latest = raw.split("LATEST INSTRUCTION: ").pop();
|
|
3300
|
+
if (latest && latest !== raw) return latest.trim();
|
|
3301
|
+
const edited = raw.split("Edit the SOURCE IMAGE as follows: ")[1];
|
|
3302
|
+
if (edited) return edited.split(" Keep everything else about the source image")[0].trim();
|
|
3303
|
+
const whole = raw.split("Whole image prompt: ")[1];
|
|
3304
|
+
if (whole) return whole.split(". Apply this only as guidance")[0].trim();
|
|
3305
|
+
return raw.length > 240 ? raw.slice(0, 240) + "…" : raw;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
function turnsFromHistoryEntries(entries) {
|
|
3309
|
+
const rows = (Array.isArray(entries) ? entries : [])
|
|
3310
|
+
.map((entry) => ({ entry, kind: chatEntryKind(entry) }))
|
|
3311
|
+
.filter((row) => row.kind)
|
|
3312
|
+
.sort((a, b) => Date.parse(a.entry.createdAt || 0) - Date.parse(b.entry.createdAt || 0));
|
|
3313
|
+
const turns = [];
|
|
3314
|
+
const seenJobs = new Set();
|
|
3315
|
+
for (const row of rows) {
|
|
3316
|
+
const entry = row.entry;
|
|
3317
|
+
const jobId = String(entry.jobId || (entry.linkedJob && entry.linkedJob.job_id) || "");
|
|
3318
|
+
if (jobId && seenJobs.has(jobId)) continue;
|
|
3319
|
+
if (jobId) seenJobs.add(jobId);
|
|
3320
|
+
const body = (entry.request && entry.request.body) || {};
|
|
3321
|
+
const text = userTextFromEntry(entry, row.kind);
|
|
3322
|
+
const at = String(entry.createdAt || new Date().toISOString());
|
|
3323
|
+
turns.push({
|
|
3324
|
+
id: nextTurnId(), at, role: "user", kind: "text", text,
|
|
3325
|
+
mode: row.kind === "video" ? "video" : "image",
|
|
3326
|
+
masked: row.kind === "inpaint" || body.chat_masked === true,
|
|
3327
|
+
isEdit: row.kind === "edit" || row.kind === "inpaint"
|
|
3328
|
+
});
|
|
3329
|
+
const status = String((entry.linkedJob && entry.linkedJob.status) || "");
|
|
3330
|
+
const urls = row.kind === "video" ? historyEntryVideoUrls(entry) : historyEntryUrls(entry);
|
|
3331
|
+
if (urls.length) {
|
|
3332
|
+
let url = "";
|
|
3333
|
+
try { url = absoluteUrl(urls[0]); } catch { url = String(urls[0]); }
|
|
3334
|
+
turns.push({
|
|
3335
|
+
id: nextTurnId(), at, role: "assistant", kind: "media",
|
|
3336
|
+
mediaKind: row.kind === "video" ? "video" : "image",
|
|
3337
|
+
url, jobId, prompt: text
|
|
3338
|
+
});
|
|
3339
|
+
} else if (status === "failed" || status === "cancelled") {
|
|
3340
|
+
turns.push({ id: nextTurnId(), at, role: "assistant", kind: "error", text: "That job " + status + "." });
|
|
3341
|
+
} else if (isPendingJobStatus(status)) {
|
|
3342
|
+
turns.push({
|
|
3343
|
+
id: nextTurnId(), at, role: "assistant", kind: "pending", jobId, prompt: text,
|
|
3344
|
+
label: "Still running (" + status + ") — use Check Status."
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
return turns;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
// Rebuild the whole conversation for the active tracer from the server.
|
|
3352
|
+
async function restoreSession(options) {
|
|
2964
3353
|
const tracer = resolveTracer();
|
|
2965
|
-
// Brand-new (unnamed) session: nothing on the server yet — just show any
|
|
2966
|
-
// local starting image, without minting a random tracer.
|
|
2967
3354
|
if (!tracer) {
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
renderInpaintHistory();
|
|
3355
|
+
// Brand-new unnamed session: nothing on the server yet.
|
|
3356
|
+
renderChat();
|
|
2971
3357
|
return;
|
|
2972
3358
|
}
|
|
3359
|
+
// Never stomp on a turn that is mid-flight locally.
|
|
3360
|
+
if (isGenBusy() || activeTurnId) { syncSessionAssets(); return; }
|
|
2973
3361
|
historyRefresh.disabled = true;
|
|
2974
3362
|
try {
|
|
2975
3363
|
const endpoint = new URL("/job-runs/history", window.location.origin);
|
|
2976
|
-
endpoint.searchParams.set("limit", "
|
|
2977
|
-
endpoint.searchParams.set("template_id", "primitive:image_inpaint");
|
|
3364
|
+
endpoint.searchParams.set("limit", "250");
|
|
2978
3365
|
endpoint.searchParams.set("tracer", tracer);
|
|
2979
3366
|
const response = await fetch(endpoint, { credentials: "same-origin" });
|
|
2980
3367
|
const payload = await parseJsonResponse(response);
|
|
2981
|
-
if (!response.ok || (payload && payload.error)) throw new Error(payload.error || "Unable to load
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
3368
|
+
if (!response.ok || (payload && payload.error)) throw new Error(payload.error || "Unable to load this session.");
|
|
3369
|
+
const turns = turnsFromHistoryEntries(payload.entries);
|
|
3370
|
+
// A refresh must never LOSE messages: run history can lag a job by a moment,
|
|
3371
|
+
// and a fresh upload-only session has nothing on the server at all. Only a
|
|
3372
|
+
// deliberate session switch (replaceEmpty) may shrink the transcript.
|
|
3373
|
+
if (turns.length < chatTurns.length && !(options && options.replaceEmpty)) {
|
|
3374
|
+
renderChat();
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
chatTurns = turns;
|
|
3378
|
+
activeTurnId = "";
|
|
3379
|
+
// The newest IMAGE in the restored conversation becomes the paint surface
|
|
3380
|
+
// and the source of the next edit.
|
|
3381
|
+
let latest = "";
|
|
3382
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
3383
|
+
const turn = turns[i];
|
|
3384
|
+
if (turn.kind === "media" && turn.mediaKind === "image" && turn.url) { latest = turn.url; break; }
|
|
3385
|
+
}
|
|
3386
|
+
if (latest) adoptLatestImage(latest);
|
|
3387
|
+
else renderChat();
|
|
2985
3388
|
} catch (error) {
|
|
2986
3389
|
console.error(error);
|
|
2987
|
-
|
|
3390
|
+
setStatus(error instanceof Error ? error.message : "Unable to load this session.", "error");
|
|
2988
3391
|
} finally {
|
|
2989
3392
|
historyRefresh.disabled = false;
|
|
2990
3393
|
}
|
|
2991
3394
|
}
|
|
2992
3395
|
|
|
3396
|
+
// Legacy call sites (job polling, submit) refresh the session through this name.
|
|
3397
|
+
async function loadInpaintHistory() {
|
|
3398
|
+
await restoreSession();
|
|
3399
|
+
}
|
|
3400
|
+
|
|
2993
3401
|
async function checkInpaintJobStatus(jobId, button) {
|
|
2994
3402
|
if (!jobId) return null;
|
|
2995
3403
|
if (button instanceof HTMLButtonElement) {
|
|
@@ -3035,6 +3443,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3035
3443
|
|
|
3036
3444
|
async function pollInpaintJob(jobId) {
|
|
3037
3445
|
for (let attempt = 0; attempt < 90; attempt += 1) {
|
|
3446
|
+
throwIfStopped();
|
|
3038
3447
|
const response = await fetch("/api/v1/primitives/image_inpaint/jobs/" + encodeURIComponent(jobId), {
|
|
3039
3448
|
credentials: "same-origin"
|
|
3040
3449
|
});
|
|
@@ -3051,6 +3460,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3051
3460
|
return outputUrl;
|
|
3052
3461
|
}
|
|
3053
3462
|
upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
|
|
3463
|
+
updatePendingLabel(activeTurnId, "Applying the masked edit (" + (payload.status || "running") + ")\u2026");
|
|
3054
3464
|
setStatus("Image edit job " + jobId + " is " + (payload.status || "running") + ".", "");
|
|
3055
3465
|
await new Promise((resolve) => window.setTimeout(resolve, attempt < 8 ? 1500 : 3000));
|
|
3056
3466
|
}
|
|
@@ -3059,8 +3469,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3059
3469
|
return null;
|
|
3060
3470
|
}
|
|
3061
3471
|
|
|
3062
|
-
async function submitInpaintJob() {
|
|
3063
|
-
|
|
3472
|
+
async function submitInpaintJob(options) {
|
|
3473
|
+
const extraRefs = (options && Array.isArray(options.extraRefs)) ? options.extraRefs : [];
|
|
3474
|
+
// The user's own words, stored alongside the request so the chat can be
|
|
3475
|
+
// rebuilt later without un-parsing the composed instruction.
|
|
3476
|
+
const chatMessage = (options && options.chatMessage) ? String(options.chatMessage) : inpaintGlobalPrompt.value.trim();
|
|
3477
|
+
if (!sourceImageUrl) {
|
|
3064
3478
|
setStatus("Upload a source image first.", "error");
|
|
3065
3479
|
return;
|
|
3066
3480
|
}
|
|
@@ -3075,11 +3489,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3075
3489
|
setStatus("Add a replacement prompt for " + missingPromptColor.color.toUpperCase() + ".", "error");
|
|
3076
3490
|
return;
|
|
3077
3491
|
}
|
|
3078
|
-
const
|
|
3079
|
-
if (!
|
|
3492
|
+
const builtInstruction = buildInpaintInstruction();
|
|
3493
|
+
if (!builtInstruction) {
|
|
3080
3494
|
setStatus("Add a replacement prompt for at least one color.", "error");
|
|
3081
3495
|
return;
|
|
3082
3496
|
}
|
|
3497
|
+
const instruction = builtInstruction;
|
|
3083
3498
|
setSubmitLoading(true);
|
|
3084
3499
|
uploadButton.disabled = true;
|
|
3085
3500
|
setStatus("Uploading masks...", "");
|
|
@@ -3095,11 +3510,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3095
3510
|
headers: { "content-type": "application/json" },
|
|
3096
3511
|
body: JSON.stringify({
|
|
3097
3512
|
tracer,
|
|
3513
|
+
chat_message: chatMessage,
|
|
3514
|
+
chat_mode: "image",
|
|
3515
|
+
chat_masked: true,
|
|
3098
3516
|
payload: {
|
|
3099
3517
|
source_image_url: sourceImageUrl,
|
|
3100
3518
|
mask_url: absoluteUrl(maskUrl),
|
|
3101
3519
|
instruction,
|
|
3102
|
-
reference_attachments: inpaintAttachments.map((attachment) => attachment.url),
|
|
3520
|
+
reference_attachments: inpaintAttachments.map((attachment) => attachment.url).concat(extraRefs),
|
|
3103
3521
|
...(inpaintProviderSelect.value ? { provider: inpaintProviderSelect.value } : {}),
|
|
3104
3522
|
size: "auto",
|
|
3105
3523
|
...(getInpaintAspectRatio() ? { aspect_ratio: getInpaintAspectRatio() } : {}),
|
|
@@ -3110,13 +3528,19 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3110
3528
|
const payload = await parseJsonResponse(response);
|
|
3111
3529
|
if (!response.ok) throw new Error(payload.error || "Unable to create image edit job.");
|
|
3112
3530
|
setStatus("Image edit job " + (payload.job_id || "") + " started.", "success");
|
|
3531
|
+
activeJob = { kind: "image_inpaint", jobId: String(payload.job_id || "") };
|
|
3113
3532
|
upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
|
|
3114
3533
|
await pollInpaintJob(payload.job_id);
|
|
3534
|
+
if (activeTurnId) {
|
|
3535
|
+
updatePendingLabel(activeTurnId, "Still running \u2014 use Check Status in this session\u2019s history.");
|
|
3536
|
+
activeTurnId = "";
|
|
3537
|
+
}
|
|
3115
3538
|
await loadInpaintHistory();
|
|
3116
3539
|
void loadTracerSessions();
|
|
3117
3540
|
} catch (error) {
|
|
3118
3541
|
console.error(error);
|
|
3119
3542
|
setStatus(error instanceof Error ? error.message : "Unable to create image edit.", "error");
|
|
3543
|
+
throw error;
|
|
3120
3544
|
} finally {
|
|
3121
3545
|
setSubmitLoading(false);
|
|
3122
3546
|
// Respect the post-generate lock — don't re-enable source upload if the
|
|
@@ -3133,25 +3557,23 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3133
3557
|
historyRefresh.addEventListener("click", () => {
|
|
3134
3558
|
void loadInpaintHistory();
|
|
3135
3559
|
});
|
|
3136
|
-
historyList.addEventListener("pointerdown", (event) => {
|
|
3137
|
-
const target = event.target;
|
|
3138
|
-
if (target instanceof Element && target.closest("[data-history-confirm]")) {
|
|
3139
|
-
event.stopPropagation();
|
|
3140
|
-
}
|
|
3141
|
-
});
|
|
3142
3560
|
historyList.addEventListener("click", (event) => {
|
|
3143
3561
|
const target = event.target;
|
|
3144
3562
|
if (!(target instanceof Element)) return;
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
event.stopPropagation();
|
|
3148
|
-
}
|
|
3563
|
+
// The rail mirrors the chat, so its thumbnail scrolls the feed to that turn
|
|
3564
|
+
// instead of opening a second, competing viewer.
|
|
3149
3565
|
const previewButton = target.closest("[data-history-preview]");
|
|
3150
3566
|
if (previewButton instanceof HTMLElement) {
|
|
3151
3567
|
const key = previewButton.dataset.historyPreview || "";
|
|
3568
|
+
const row = chatFeed.querySelector('[data-turn-id="' + CSS.escape(key) + '"]');
|
|
3569
|
+
if (row instanceof HTMLElement) {
|
|
3570
|
+
row.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
3571
|
+
row.classList.add("is-flash");
|
|
3572
|
+
window.setTimeout(() => row.classList.remove("is-flash"), 1200);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3152
3575
|
const item = inpaintHistoryItems.find((entry) => entry.key === key);
|
|
3153
|
-
if (
|
|
3154
|
-
openImagePreview(item.url, item.createdAt ? new Date(item.createdAt).toLocaleString() : "Image edit output", imageDownloadName("image-edit-history", item.createdAt));
|
|
3576
|
+
if (item && item.url) openImagePreview(item.url, item.title, imageDownloadName("vidfarm-output", item.createdAt), item.mediaKind);
|
|
3155
3577
|
return;
|
|
3156
3578
|
}
|
|
3157
3579
|
const copyButton = target.closest("[data-history-copy]");
|
|
@@ -3169,40 +3591,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3169
3591
|
if (saveButton instanceof HTMLElement) {
|
|
3170
3592
|
const key = saveButton.dataset.historySave || "";
|
|
3171
3593
|
const item = inpaintHistoryItems.find((entry) => entry.key === key);
|
|
3172
|
-
if (item && item.url) saveMediaToFiles(item.url, imageDownloadName("
|
|
3594
|
+
if (item && item.url) saveMediaToFiles(item.url, imageDownloadName("vidfarm-output", item.createdAt));
|
|
3173
3595
|
return;
|
|
3174
3596
|
}
|
|
3175
3597
|
const checkButton = target.closest("[data-history-check]");
|
|
3176
3598
|
if (checkButton instanceof HTMLButtonElement) {
|
|
3177
3599
|
void checkInpaintJobStatus(checkButton.dataset.historyCheck || "", checkButton);
|
|
3178
|
-
return;
|
|
3179
|
-
}
|
|
3180
|
-
const removeButton = target.closest("[data-history-remove]");
|
|
3181
|
-
if (removeButton instanceof HTMLElement) {
|
|
3182
|
-
const key = removeButton.dataset.historyRemove || "";
|
|
3183
|
-
const confirm = historyList.querySelector('[data-history-confirm="' + CSS.escape(key) + '"]');
|
|
3184
|
-
const shouldOpen = !(confirm instanceof HTMLElement) || confirm.hidden;
|
|
3185
|
-
closeInpaintHistoryConfirms();
|
|
3186
|
-
if (confirm instanceof HTMLElement) {
|
|
3187
|
-
confirm.hidden = !shouldOpen;
|
|
3188
|
-
}
|
|
3189
|
-
removeButton.setAttribute("aria-expanded", shouldOpen ? "true" : "false");
|
|
3190
|
-
return;
|
|
3191
|
-
}
|
|
3192
|
-
const cancelButton = target.closest("[data-history-cancel]");
|
|
3193
|
-
if (cancelButton instanceof HTMLElement) {
|
|
3194
|
-
const confirm = cancelButton.closest("[data-history-confirm]");
|
|
3195
|
-
if (confirm instanceof HTMLElement) confirm.hidden = true;
|
|
3196
|
-
return;
|
|
3197
|
-
}
|
|
3198
|
-
const confirmRemove = target.closest("[data-history-confirm-remove]");
|
|
3199
|
-
if (confirmRemove instanceof HTMLElement) {
|
|
3200
|
-
const key = confirmRemove.dataset.historyConfirmRemove || "";
|
|
3201
|
-
if (key) {
|
|
3202
|
-
hiddenInpaintHistoryKeys.add(key);
|
|
3203
|
-
renderInpaintHistory();
|
|
3204
|
-
}
|
|
3205
|
-
return;
|
|
3206
3600
|
}
|
|
3207
3601
|
});
|
|
3208
3602
|
resultOpenButton.addEventListener("click", () => {
|
|
@@ -3272,18 +3666,22 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3272
3666
|
inpaintGlobalPrompt.addEventListener("paste", (event) => {
|
|
3273
3667
|
handleAttachmentPaste(event, "global");
|
|
3274
3668
|
});
|
|
3275
|
-
|
|
3669
|
+
function undoStroke() {
|
|
3276
3670
|
const stroke = inpaintStrokes.pop();
|
|
3277
3671
|
if (stroke) redoStrokes.push(stroke);
|
|
3278
3672
|
redrawInpaintMask();
|
|
3673
|
+
syncMaskUi();
|
|
3279
3674
|
setStatus("", "");
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3675
|
+
}
|
|
3676
|
+
function redoStroke() {
|
|
3282
3677
|
const stroke = redoStrokes.pop();
|
|
3283
3678
|
if (stroke) inpaintStrokes.push(stroke);
|
|
3284
3679
|
redrawInpaintMask();
|
|
3680
|
+
syncMaskUi();
|
|
3285
3681
|
setStatus("", "");
|
|
3286
|
-
}
|
|
3682
|
+
}
|
|
3683
|
+
inpaintUndo.addEventListener("click", undoStroke);
|
|
3684
|
+
inpaintRedo.addEventListener("click", redoStroke);
|
|
3287
3685
|
inpaintClear.addEventListener("click", () => {
|
|
3288
3686
|
resetMask();
|
|
3289
3687
|
setStatus("", "");
|
|
@@ -3349,7 +3747,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3349
3747
|
hideBrushCursor();
|
|
3350
3748
|
});
|
|
3351
3749
|
inpaintSubmit.addEventListener("click", () => {
|
|
3352
|
-
|
|
3750
|
+
submitInpaintJob().catch(() => {});
|
|
3353
3751
|
});
|
|
3354
3752
|
function openSettingsModal() {
|
|
3355
3753
|
settingsPopover.hidden = false;
|
|
@@ -3366,6 +3764,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3366
3764
|
if (settingsPopover.hidden) openSettingsModal(); else closeSettingsModal();
|
|
3367
3765
|
});
|
|
3368
3766
|
if (settingsClose) settingsClose.addEventListener("click", () => closeSettingsModal());
|
|
3767
|
+
|
|
3768
|
+
// Copy chat ID — the session tracer IS this chat's thread id (it's what
|
|
3769
|
+
// /job-runs/history groups on, what ?tracer= reopens, and what the past-
|
|
3770
|
+
// sessions rail keys off). A still-unnamed session mints its id here so
|
|
3771
|
+
// there is always something concrete to copy/share.
|
|
3772
|
+
const copyChatButton = document.querySelector("[data-custom-inpaint-copy-chat]");
|
|
3773
|
+
if (copyChatButton instanceof HTMLButtonElement) {
|
|
3774
|
+
copyChatButton.addEventListener("click", () => {
|
|
3775
|
+
const tracer = resolveTracer() || getTracerValue();
|
|
3776
|
+
void copyText(tracer).then((copied) => {
|
|
3777
|
+
setStatus(copied ? "Chat ID copied — " + tracer : "Chat ID: " + tracer, copied ? "success" : "");
|
|
3778
|
+
});
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3369
3781
|
settingsPopover.addEventListener("click", (event) => {
|
|
3370
3782
|
if (event.target === settingsPopover) closeSettingsModal();
|
|
3371
3783
|
});
|
|
@@ -3384,16 +3796,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3384
3796
|
if (sessionsBackdrop) sessionsBackdrop.hidden = true;
|
|
3385
3797
|
if (settingsPopover.hidden) document.body.style.overflow = "";
|
|
3386
3798
|
}
|
|
3387
|
-
function isImageSessionEntry(entry) {
|
|
3388
|
-
const templateId = String(entry && entry.linkedJob && (entry.linkedJob.template_id || entry.linkedJob.templateId) || "").toLowerCase();
|
|
3389
|
-
const path = String(entry && entry.path || "").toLowerCase();
|
|
3390
|
-
return templateId.includes("image_inpaint")
|
|
3391
|
-
|| templateId.includes("image_generate")
|
|
3392
|
-
|| path.includes("/primitives/images/inpaint")
|
|
3393
|
-
|| path.includes("/primitives/image_inpaint")
|
|
3394
|
-
|| path.includes("/primitives/images/generate")
|
|
3395
|
-
|| path.includes("/primitives/image_generate");
|
|
3396
|
-
}
|
|
3397
3799
|
function relTimeShort(value) {
|
|
3398
3800
|
const then = Date.parse(value || 0);
|
|
3399
3801
|
if (!then) return "";
|
|
@@ -3454,13 +3856,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3454
3856
|
meta.className = "rk-ip-session-meta";
|
|
3455
3857
|
const title = document.createElement("div");
|
|
3456
3858
|
title.className = "rk-ip-session-title";
|
|
3457
|
-
title.textContent = isImage ? (group.name || "Standalone image") : group.tracer;
|
|
3859
|
+
title.textContent = isImage ? (group.name || "Standalone image") : (group.title || group.tracer);
|
|
3860
|
+
title.title = isImage ? "" : (group.title ? group.title + " (" + group.tracer + ")" : group.tracer);
|
|
3458
3861
|
const sub = document.createElement("div");
|
|
3459
3862
|
sub.className = "rk-ip-session-sub";
|
|
3460
3863
|
const when = relTimeShort(group.createdAt);
|
|
3461
3864
|
sub.textContent = isImage
|
|
3462
|
-
? "Standalone image — tap to
|
|
3463
|
-
: (group.count ? group.count + "
|
|
3865
|
+
? "Standalone image — tap to start a chat from it" + (when ? " · " + when : "")
|
|
3866
|
+
: (group.count ? group.count + " message" + (group.count === 1 ? "" : "s") : "session") + (when ? " · " + when : "");
|
|
3464
3867
|
meta.append(title, sub);
|
|
3465
3868
|
row.append(meta);
|
|
3466
3869
|
sessionsList.append(row);
|
|
@@ -3477,16 +3880,17 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3477
3880
|
const payload = await parseJsonResponse(response);
|
|
3478
3881
|
if (!response.ok || (payload && payload.error)) throw new Error((payload && payload.error) || "Unable to load sessions.");
|
|
3479
3882
|
const groups = new Map();
|
|
3480
|
-
// Past image gens
|
|
3481
|
-
//
|
|
3482
|
-
// a standalone image the user can load fresh into a new inpaint session.
|
|
3883
|
+
// Past one-off image gens with NO session tracer can't be reopened as a
|
|
3884
|
+
// conversation, so each is offered as a standalone image to start one from.
|
|
3483
3885
|
const loose = [];
|
|
3484
3886
|
const looseSeen = new Set();
|
|
3485
3887
|
for (const entry of Array.isArray(payload.entries) ? payload.entries : []) {
|
|
3486
|
-
|
|
3487
|
-
|
|
3888
|
+
const kind = chatEntryKind(entry);
|
|
3889
|
+
if (!kind) continue;
|
|
3890
|
+
const urls = kind === "video" ? historyEntryVideoUrls(entry) : historyEntryUrls(entry);
|
|
3488
3891
|
const tracer = String(entry.tracer || "").trim();
|
|
3489
3892
|
if (!tracer) {
|
|
3893
|
+
if (kind === "video") continue;
|
|
3490
3894
|
for (const raw of urls) {
|
|
3491
3895
|
let url = "";
|
|
3492
3896
|
try { url = absoluteUrl(raw); } catch { continue; }
|
|
@@ -3497,9 +3901,16 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3497
3901
|
continue;
|
|
3498
3902
|
}
|
|
3499
3903
|
let group = groups.get(tracer);
|
|
3500
|
-
if (!group) { group = { kind: "session", tracer, thumb: "", count: 0, createdAt: entry.createdAt || "" }; groups.set(tracer, group); }
|
|
3501
|
-
|
|
3502
|
-
|
|
3904
|
+
if (!group) { group = { kind: "session", tracer, thumb: "", title: "", count: 0, createdAt: entry.createdAt || "", firstAt: entry.createdAt || "" }; groups.set(tracer, group); }
|
|
3905
|
+
group.count += 1;
|
|
3906
|
+
// Newest image in the session is its cover; oldest message is its name.
|
|
3907
|
+
if (kind !== "video" && urls[0] && Date.parse(entry.createdAt || 0) >= Date.parse(group.createdAt || 0)) {
|
|
3908
|
+
try { group.thumb = absoluteUrl(urls[0]); } catch {}
|
|
3909
|
+
}
|
|
3910
|
+
if (!group.title || Date.parse(entry.createdAt || 0) <= Date.parse(group.firstAt || 0)) {
|
|
3911
|
+
const text = userTextFromEntry(entry, kind);
|
|
3912
|
+
if (text) { group.title = text.length > 60 ? text.slice(0, 60) + "…" : text; group.firstAt = entry.createdAt || group.firstAt; }
|
|
3913
|
+
}
|
|
3503
3914
|
if (Date.parse(entry.createdAt || 0) > Date.parse(group.createdAt || 0)) group.createdAt = entry.createdAt;
|
|
3504
3915
|
}
|
|
3505
3916
|
const list = Array.from(groups.values()).concat(loose)
|
|
@@ -3512,7 +3923,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3512
3923
|
sessionsLoading = false;
|
|
3513
3924
|
}
|
|
3514
3925
|
}
|
|
3515
|
-
function switchToTracer(tracer
|
|
3926
|
+
async function switchToTracer(tracer) {
|
|
3516
3927
|
const value = String(tracer || "").trim();
|
|
3517
3928
|
if (!value) return;
|
|
3518
3929
|
clearInpaintUrlParams();
|
|
@@ -3521,16 +3932,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3521
3932
|
tracerInput.value = value;
|
|
3522
3933
|
tracerAutoPending = false;
|
|
3523
3934
|
closeSessionsPanel();
|
|
3524
|
-
|
|
3935
|
+
setStatus("Opening " + value + "…", "");
|
|
3936
|
+
// Rebuild the whole conversation — messages, edits, outputs — and put the
|
|
3937
|
+
// newest image back under the brush.
|
|
3938
|
+
await restoreSession({ replaceEmpty: true });
|
|
3525
3939
|
void loadTracerSessions();
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
loadSourceUrl(sourceUrl, "history");
|
|
3530
|
-
setStatus("Opened session " + value + " — paint mask regions to keep editing.", "success");
|
|
3531
|
-
} else {
|
|
3532
|
-
setStatus("Opened session " + value + ".", "");
|
|
3533
|
-
}
|
|
3940
|
+
setStatus(chatTurns.length
|
|
3941
|
+
? "Session restored — keep chatting to pick up where you left off."
|
|
3942
|
+
: "Opened " + value + " — no messages in it yet.", chatTurns.length ? "success" : "");
|
|
3534
3943
|
}
|
|
3535
3944
|
// A past image gen with no session tracer: start a brand-new session and drop
|
|
3536
3945
|
// that image onto the editor as the source so it can be edited immediately.
|
|
@@ -3545,7 +3954,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3545
3954
|
closeSessionsPanel();
|
|
3546
3955
|
loadSourceUrl(value, "history");
|
|
3547
3956
|
void loadTracerSessions();
|
|
3548
|
-
setStatus("New session started from that image —
|
|
3957
|
+
setStatus("New session started from that image — describe an edit, or paint a mask first.", "success");
|
|
3549
3958
|
}
|
|
3550
3959
|
if (sessionsToggle) sessionsToggle.addEventListener("click", () => {
|
|
3551
3960
|
if (sessionsPanel && sessionsPanel.classList.contains("is-open")) closeSessionsPanel(); else openSessionsPanel();
|
|
@@ -3562,19 +3971,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3562
3971
|
}
|
|
3563
3972
|
const row = target.closest("[data-session-tracer]");
|
|
3564
3973
|
if (row instanceof HTMLElement && row.dataset.sessionTracer) {
|
|
3565
|
-
|
|
3566
|
-
switchToTracer(row.dataset.sessionTracer, thumb instanceof HTMLImageElement ? thumb.src : "");
|
|
3974
|
+
void switchToTracer(row.dataset.sessionTracer);
|
|
3567
3975
|
}
|
|
3568
3976
|
});
|
|
3569
3977
|
|
|
3570
|
-
window.addEventListener("click", (event) => {
|
|
3571
|
-
const target = event.target;
|
|
3572
|
-
if (!(target instanceof Node)) return;
|
|
3573
|
-
const isHistoryConfirmTarget = target instanceof Element && Boolean(target.closest("[data-history-confirm]"));
|
|
3574
|
-
if (!historyList.contains(target) && !isHistoryConfirmTarget) {
|
|
3575
|
-
closeInpaintHistoryConfirms();
|
|
3576
|
-
}
|
|
3577
|
-
});
|
|
3578
3978
|
window.addEventListener("resize", () => syncInpaintCanvasSize());
|
|
3579
3979
|
window.addEventListener("keydown", (event) => {
|
|
3580
3980
|
if (event.key !== "Escape") return;
|
|
@@ -3583,153 +3983,71 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3583
3983
|
if (sessionsPanel && sessionsPanel.classList.contains("is-open")) { closeSessionsPanel(); return; }
|
|
3584
3984
|
});
|
|
3585
3985
|
|
|
3586
|
-
// =====
|
|
3986
|
+
// ===== chat composer + feed ================================================
|
|
3587
3987
|
// Layered on top of the legacy inpaint logic; reuses its in-scope helpers
|
|
3588
|
-
// (setStatus,
|
|
3589
|
-
// absoluteUrl, extractOutputUrl, inpaintProviderSelect).
|
|
3590
|
-
//
|
|
3988
|
+
// (setStatus, uploadTemporaryBlob, getTracerValue, parseJsonResponse,
|
|
3989
|
+
// absoluteUrl, extractOutputUrl, inpaintProviderSelect, submitInpaintJob).
|
|
3990
|
+
// Every finished image becomes the newest image, which is both the paint
|
|
3991
|
+
// surface and the source the NEXT message edits.
|
|
3591
3992
|
var genPrompt = document.getElementById('rk-ip-genprompt');
|
|
3592
3993
|
var genClip = document.getElementById('rk-ip-genclip');
|
|
3593
3994
|
var genSend = document.getElementById('rk-ip-gensend');
|
|
3594
3995
|
var genRefsEl = document.getElementById('rk-ip-genrefs');
|
|
3595
3996
|
var genStatusEl = document.getElementById('rk-ip-genstatus');
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
var
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3997
|
+
// The composer itself — auto-grow, the drag-to-resize grip, reference chips,
|
|
3998
|
+
// paste/drop, and the attach pop panel — is the shared implementation from
|
|
3999
|
+
// chat-composer.ts, byte-identical to the one on /tools/video.
|
|
4000
|
+
var composer = window.__rkChatComposer({
|
|
4001
|
+
root: composerEl,
|
|
4002
|
+
textarea: genPrompt,
|
|
4003
|
+
refsEl: genRefsEl,
|
|
4004
|
+
sendBtn: genSend,
|
|
4005
|
+
clipBtn: genClip,
|
|
4006
|
+
attachTitle: "Attach reference images",
|
|
4007
|
+
uploadBlob: function(f, name, type){ return uploadTemporaryBlob(f, name, type); },
|
|
4008
|
+
onStatus: function(m, t){ setStatus(m, t); },
|
|
4009
|
+
canSend: function(){ return maskHasInstruction(); },
|
|
4010
|
+
onSend: function(){ sendChatMessage(); },
|
|
4011
|
+
onStop: function(){ stopActiveTurn(); }
|
|
4012
|
+
});
|
|
4013
|
+
// Aliases so the rest of the page keeps its original call sites.
|
|
4014
|
+
function isGenBusy(){ return Boolean(composer && composer.isBusy()); }
|
|
4015
|
+
function renderGenRefs(){ composer.renderRefs(); }
|
|
4016
|
+
function autoGrowComposer(){ composer.autoGrow(); }
|
|
4017
|
+
function addGenReferenceFiles(fl){ return composer.addFiles(fl); }
|
|
4018
|
+
function closeGenFilePanel(){ composer.closePanel(); }
|
|
4019
|
+
function syncGenSend(){ composer.syncSend(); }
|
|
4020
|
+
function setGenBusy(v){ composer.setBusy(v); }
|
|
4021
|
+
|
|
4022
|
+
// One status line for the whole surface — the composer's.
|
|
4023
|
+
function setGenStatus(m, t){ setStatus(m, t); }
|
|
4024
|
+
function maskHasInstruction(){
|
|
4025
|
+
return inpaintStrokes.length > 0 && inpaintMaskColors.some(function(entry){ return String(entry.prompt || '').trim(); });
|
|
3623
4026
|
}
|
|
3624
4027
|
|
|
3625
|
-
//
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
uploadBtn.addEventListener('click', function(){ if(!uploadBtn.disabled) uploadInput.click(); });
|
|
3642
|
-
uploadInput.addEventListener('change', function(){
|
|
3643
|
-
var files = Array.prototype.slice.call(uploadInput.files || []).filter(function(f){ return /^image\//i.test(f.type || '') || /\.(png|jpe?g|gif|webp|avif)$/i.test(f.name || ''); });
|
|
3644
|
-
uploadInput.value = '';
|
|
3645
|
-
if(!files.length){ setGenStatus('Pick an image file to upload as a reference.', 'error'); return; }
|
|
3646
|
-
uploadBtn.disabled = true; var original = uploadBtn.textContent; uploadBtn.textContent = 'Uploading\u2026';
|
|
3647
|
-
setGenStatus('Uploading ' + files.length + ' reference' + (files.length > 1 ? 's' : '') + '\u2026', '');
|
|
3648
|
-
var added = 0;
|
|
3649
|
-
files.reduce(function(chain, f){
|
|
3650
|
-
return chain.then(function(){
|
|
3651
|
-
return uploadTemporaryBlob(f, f.name || 'reference.png', f.type || 'image/png').then(function(url){
|
|
3652
|
-
if(url && genRefs.indexOf(url) === -1){ genRefs.push(url); added++; renderGenRefs(); }
|
|
3653
|
-
});
|
|
3654
|
-
});
|
|
3655
|
-
}, Promise.resolve()).then(function(){
|
|
3656
|
-
uploadBtn.disabled = false; uploadBtn.textContent = original;
|
|
3657
|
-
if(added){ setGenStatus(added + ' reference' + (added > 1 ? 's' : '') + ' uploaded.', 'success'); closeGenFilePanel(); }
|
|
3658
|
-
else setGenStatus('Those files were already attached.', '');
|
|
3659
|
-
}).catch(function(err){
|
|
3660
|
-
uploadBtn.disabled = false; uploadBtn.textContent = original;
|
|
3661
|
-
setGenStatus((err && err.message) ? err.message : 'Upload failed.', 'error');
|
|
3662
|
-
});
|
|
3663
|
-
});
|
|
3664
|
-
var mountEl = document.createElement('div'); mountEl.className = 'rk-ip-filemodal-mount';
|
|
3665
|
-
panel.appendChild(head); panel.appendChild(mountEl); overlay.appendChild(panel);
|
|
3666
|
-
document.body.appendChild(overlay); genFileModal = overlay;
|
|
3667
|
-
close.addEventListener('click', closeGenFilePanel);
|
|
3668
|
-
overlay.addEventListener('click', function(e){ if(e.target === overlay) closeGenFilePanel(); });
|
|
3669
|
-
document.addEventListener('keydown', onGenFileEsc, true);
|
|
3670
|
-
if(window.__vidfarmDirectory && window.__vidfarmDirectory.mount){
|
|
3671
|
-
genFileModal._handle = window.__vidfarmDirectory.mount(mountEl, {
|
|
3672
|
-
mode: 'attach', showMultiSelect: true, primaryClick: 'attach',
|
|
3673
|
-
onAttach: function(items){
|
|
3674
|
-
var added = 0;
|
|
3675
|
-
(items || []).forEach(function(it){
|
|
3676
|
-
var ct = (it.contentType || ''); var isImg = ct.indexOf('image/') === 0 || /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(it.name || '');
|
|
3677
|
-
if(it.viewUrl && isImg && genRefs.indexOf(it.viewUrl) === -1){ genRefs.push(it.viewUrl); added++; }
|
|
3678
|
-
});
|
|
3679
|
-
renderGenRefs();
|
|
3680
|
-
if(added){ setGenStatus(added + ' reference' + (added > 1 ? 's' : '') + ' attached.', 'success'); closeGenFilePanel(); }
|
|
3681
|
-
else setGenStatus('Pick an image file to use as a reference.', 'error');
|
|
3682
|
-
}
|
|
3683
|
-
});
|
|
3684
|
-
} else {
|
|
3685
|
-
mountEl.innerHTML = '<p style="padding:16px;color:var(--rk-text-muted);font-size:13px">File browser is unavailable right now.</p>';
|
|
4028
|
+
// ---- stop --------------------------------------------------------------
|
|
4029
|
+
// Stop aborts the client poll AND best-effort cancels the job server-side so a
|
|
4030
|
+
// long video render stops burning provider time.
|
|
4031
|
+
var activeJob = { kind: '', jobId: '' };
|
|
4032
|
+
var stopRequested = false;
|
|
4033
|
+
function stopError(){ var e = new Error('__vidfarm_stopped__'); e.stopped = true; return e; }
|
|
4034
|
+
function throwIfStopped(){ if(stopRequested) throw stopError(); }
|
|
4035
|
+
function stopActiveTurn(){
|
|
4036
|
+
if(!isGenBusy() || stopRequested) return;
|
|
4037
|
+
stopRequested = true;
|
|
4038
|
+
setStatus('Stopping…', '');
|
|
4039
|
+
if(activeTurnId) updatePendingLabel(activeTurnId, 'Stopping…');
|
|
4040
|
+
if(activeJob.kind && activeJob.jobId){
|
|
4041
|
+
fetch('/api/v1/primitives/' + activeJob.kind + '/jobs/' + encodeURIComponent(activeJob.jobId) + '/cancel', {
|
|
4042
|
+
method: 'POST', credentials: 'same-origin'
|
|
4043
|
+
}).catch(function(){});
|
|
3686
4044
|
}
|
|
3687
4045
|
}
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
setGenStatus('Uploading ' + files.length + ' reference' + (files.length > 1 ? 's' : '') + '\u2026', '');
|
|
3694
|
-
var added = 0;
|
|
3695
|
-
return files.reduce(function(chain, f){
|
|
3696
|
-
return chain.then(function(){
|
|
3697
|
-
return uploadTemporaryBlob(f, f.name || 'pasted-image.png', f.type || 'image/png').then(function(url){
|
|
3698
|
-
if(url && genRefs.indexOf(url) === -1){ genRefs.push(url); added++; renderGenRefs(); }
|
|
3699
|
-
});
|
|
3700
|
-
});
|
|
3701
|
-
}, Promise.resolve()).then(function(){
|
|
3702
|
-
if(added) setGenStatus(added + ' reference' + (added > 1 ? 's' : '') + ' attached.', 'success');
|
|
3703
|
-
return added;
|
|
3704
|
-
}).catch(function(err){ setGenStatus((err && err.message) ? err.message : 'Upload failed.', 'error'); return added; });
|
|
3705
|
-
}
|
|
3706
|
-
function imagesFromClipboard(e){
|
|
3707
|
-
var items = (e.clipboardData && e.clipboardData.items) || []; var out = [];
|
|
3708
|
-
for(var i = 0; i < items.length; i++){ if(items[i].kind === 'file'){ var f = items[i].getAsFile(); if(f && /^image\//i.test(f.type || '')) out.push(f); } }
|
|
3709
|
-
return out;
|
|
3710
|
-
}
|
|
3711
|
-
function dtHasFiles(e){ var t = (e.dataTransfer && e.dataTransfer.types) || []; return Array.prototype.indexOf.call(t, 'Files') !== -1; }
|
|
3712
|
-
|
|
3713
|
-
function pollImageGenerateJob(jobId){
|
|
3714
|
-
var attempt = 0;
|
|
3715
|
-
function step(){
|
|
3716
|
-
return fetch('/api/v1/primitives/image_generate/jobs/' + encodeURIComponent(jobId), { credentials: 'same-origin' }).then(function(r){
|
|
3717
|
-
return parseJsonResponse(r).then(function(payload){
|
|
3718
|
-
if(!r.ok) throw new Error(payload.error || 'Unable to poll image job.');
|
|
3719
|
-
if(payload.status === 'failed' || payload.status === 'cancelled'){ throw new Error((payload.error && payload.error.message) || ('Image job ' + payload.status + '.')); }
|
|
3720
|
-
var out = extractOutputUrl(payload);
|
|
3721
|
-
if(payload.status === 'succeeded' || payload.status === 'complete' || out){
|
|
3722
|
-
if(!out) throw new Error('Image job completed without an output URL.');
|
|
3723
|
-
return absoluteUrl(out);
|
|
3724
|
-
}
|
|
3725
|
-
setGenStatus('Generating image (' + (payload.status || 'running') + ')\u2026', '');
|
|
3726
|
-
attempt++;
|
|
3727
|
-
if(attempt >= 90){ setGenStatus('Still running \u2014 check back shortly.', ''); return ''; }
|
|
3728
|
-
return new Promise(function(res){ setTimeout(res, attempt < 8 ? 1500 : 3000); }).then(step);
|
|
3729
|
-
});
|
|
3730
|
-
});
|
|
3731
|
-
}
|
|
3732
|
-
return step();
|
|
4046
|
+
function composerPlaceholder(){
|
|
4047
|
+
if(!genPrompt) return;
|
|
4048
|
+
genPrompt.placeholder = sourceImageUrl
|
|
4049
|
+
? (inpaintStrokes.length ? 'Describe the masked edit (whole-image guidance)\u2026' : 'Describe the next edit to the newest image\u2026')
|
|
4050
|
+
: 'Describe the image you want\u2026';
|
|
3733
4051
|
}
|
|
3734
4052
|
|
|
3735
4053
|
// "character card" / "storyboard" shorthand — spell out the full layout
|
|
@@ -3765,47 +4083,380 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3765
4083
|
btn.addEventListener('click', function(){ applyGenPreset(btn.getAttribute('data-preset') || 'character-card'); });
|
|
3766
4084
|
});
|
|
3767
4085
|
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
4086
|
+
// ---- chat state ----------------------------------------------------------
|
|
4087
|
+
// chatTurns/activeTurnId/renderChat/addTurn/adoptLatestImage are declared with
|
|
4088
|
+
// var + function declarations so the earlier legacy helpers (loadSourceUrl,
|
|
4089
|
+
// applyCompletedInpaintJob, resetWorkspace) can call them safely.
|
|
4090
|
+
var turnSeq = 0;
|
|
4091
|
+
function nextTurnId(){ turnSeq += 1; return 't' + turnSeq; }
|
|
4092
|
+
function addTurn(turn){
|
|
4093
|
+
var item = Object.assign({ id: nextTurnId(), at: new Date().toISOString(), role: 'assistant' }, turn);
|
|
4094
|
+
chatTurns.push(item);
|
|
4095
|
+
renderChat();
|
|
4096
|
+
return item;
|
|
4097
|
+
}
|
|
4098
|
+
function updateTurn(id, patch){
|
|
4099
|
+
if(!id) return;
|
|
4100
|
+
chatTurns = chatTurns.map(function(t){ return t.id === id ? Object.assign({}, t, patch) : t; });
|
|
4101
|
+
renderChat();
|
|
4102
|
+
}
|
|
4103
|
+
// Pending labels tick every poll; patch the node in place so a full re-render
|
|
4104
|
+
// never restarts a video that is playing further up the feed.
|
|
4105
|
+
function updatePendingLabel(id, label){
|
|
4106
|
+
if(!id) return;
|
|
4107
|
+
chatTurns = chatTurns.map(function(t){ return t.id === id ? Object.assign({}, t, { label: label }) : t; });
|
|
4108
|
+
var node = chatFeed.querySelector('[data-pending-id="' + id + '"]');
|
|
4109
|
+
if(node) node.textContent = label;
|
|
4110
|
+
}
|
|
4111
|
+
function resolveActiveTurn(patch){
|
|
4112
|
+
if(activeTurnId){ updateTurn(activeTurnId, patch); activeTurnId = ''; return; }
|
|
4113
|
+
addTurn(Object.assign({ role: 'assistant' }, patch));
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
// Make the given url the newest image: the paint surface AND the source of the next edit.
|
|
4117
|
+
function adoptLatestImage(url, name){
|
|
4118
|
+
if(!url) return;
|
|
4119
|
+
sourceImageUrl = url;
|
|
4120
|
+
sourceImageName = name || sourceNameFromUrl(url);
|
|
4121
|
+
sourceName.textContent = sourceImageName;
|
|
4122
|
+
canvasWrap.hidden = false;
|
|
4123
|
+
outputPanel.hidden = true;
|
|
4124
|
+
inpaintImage.onload = function(){ syncInpaintCanvasSize(); resetMask(); };
|
|
4125
|
+
inpaintImage.src = url;
|
|
4126
|
+
resetMask();
|
|
4127
|
+
renderChat();
|
|
4128
|
+
composerPlaceholder();
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
// ---- feed rendering ------------------------------------------------------
|
|
4132
|
+
function mediaActionsRow(turn){
|
|
4133
|
+
var row = document.createElement('div');
|
|
4134
|
+
row.className = 'rk-ip-media-actions';
|
|
4135
|
+
var full = document.createElement('button');
|
|
4136
|
+
full.type = 'button'; full.className = 'rk-ip-act'; full.dataset.ipOpen = turn.id;
|
|
4137
|
+
full.setAttribute('aria-label', 'Open fullscreen'); full.title = 'Fullscreen';
|
|
4138
|
+
full.innerHTML = "<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>";
|
|
4139
|
+
var download = document.createElement('a');
|
|
4140
|
+
download.className = 'rk-ip-act'; download.href = turn.url; download.target = '_blank'; download.rel = 'noopener noreferrer';
|
|
4141
|
+
download.setAttribute('download', imageDownloadName(turn.mediaKind === 'video' ? 'vidfarm-video' : 'vidfarm-image', turn.at));
|
|
4142
|
+
download.setAttribute('aria-label', 'Download this file'); download.title = 'Download';
|
|
4143
|
+
download.innerHTML = "<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>";
|
|
4144
|
+
var link = document.createElement('a');
|
|
4145
|
+
link.className = 'rk-ip-act'; link.href = turn.url; link.target = '_blank'; link.rel = 'noopener noreferrer';
|
|
4146
|
+
link.setAttribute('aria-label', 'Open the direct file URL'); link.title = 'Open direct file URL';
|
|
4147
|
+
link.innerHTML = "<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.6 11.4a3 3 0 0 0 4.3 0l2.4-2.4a3 3 0 1 0-4.3-4.3l-1 1\"/><path d=\"M11.4 8.6a3 3 0 0 0-4.3 0l-2.4 2.4a3 3 0 1 0 4.3 4.3l1-1\"/></svg>";
|
|
4148
|
+
var copy = document.createElement('button');
|
|
4149
|
+
copy.type = 'button'; copy.className = 'rk-ip-act'; copy.dataset.ipCopy = turn.id;
|
|
4150
|
+
copy.setAttribute('aria-label', 'Copy the file URL'); copy.title = 'Copy URL';
|
|
4151
|
+
copy.innerHTML = "<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>";
|
|
4152
|
+
var save = document.createElement('button');
|
|
4153
|
+
save.type = 'button'; save.className = 'rk-ip-act'; save.dataset.ipSave = turn.id;
|
|
4154
|
+
save.setAttribute('aria-label', 'Save to My Files'); save.title = 'Save to My Files';
|
|
4155
|
+
save.innerHTML = "<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>";
|
|
4156
|
+
row.append(full, download, link, copy, save);
|
|
4157
|
+
return row;
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
function renderChat(){
|
|
4161
|
+
// Detach the paint surface first — innerHTML rebuild would otherwise drop it.
|
|
4162
|
+
if(paintStack.parentElement !== canvasHolder) canvasHolder.appendChild(paintStack);
|
|
4163
|
+
var wasNearBottom = (chatFeed.scrollHeight - chatFeed.scrollTop - chatFeed.clientHeight) < 120;
|
|
4164
|
+
chatFeed.innerHTML = '';
|
|
4165
|
+
if(!chatTurns.length){
|
|
4166
|
+
emptyState.hidden = false;
|
|
4167
|
+
chatFeed.appendChild(emptyState);
|
|
4168
|
+
return;
|
|
4169
|
+
}
|
|
4170
|
+
emptyState.hidden = true;
|
|
4171
|
+
var latestPaintableId = '';
|
|
4172
|
+
for(var i = chatTurns.length - 1; i >= 0; i--){
|
|
4173
|
+
var candidate = chatTurns[i];
|
|
4174
|
+
if(candidate.kind === 'media' && candidate.mediaKind !== 'video' && candidate.url && candidate.url === sourceImageUrl){ latestPaintableId = candidate.id; break; }
|
|
4175
|
+
}
|
|
4176
|
+
chatTurns.forEach(function(turn){
|
|
4177
|
+
var row = document.createElement('div');
|
|
4178
|
+
row.className = 'rk-ip-turn';
|
|
4179
|
+
row.dataset.role = turn.role;
|
|
4180
|
+
row.dataset.kind = turn.kind;
|
|
4181
|
+
row.dataset.turnId = turn.id;
|
|
4182
|
+
if(turn.role === 'user'){
|
|
4183
|
+
if(turn.refs && turn.refs.length){
|
|
4184
|
+
var refs = document.createElement('div');
|
|
4185
|
+
refs.className = 'rk-ip-turn-refs';
|
|
4186
|
+
turn.refs.forEach(function(url){ var im = document.createElement('img'); im.src = url; im.alt = ''; im.loading = 'lazy'; refs.appendChild(im); });
|
|
4187
|
+
row.appendChild(refs);
|
|
4188
|
+
}
|
|
4189
|
+
var tag = document.createElement('span');
|
|
4190
|
+
tag.className = 'rk-ip-turn-tag';
|
|
4191
|
+
tag.textContent = turn.mode === 'video' ? 'Video' : (turn.masked ? 'Masked edit' : (turn.isEdit ? 'Edit' : 'New image'));
|
|
4192
|
+
row.appendChild(tag);
|
|
4193
|
+
var bubble = document.createElement('div');
|
|
4194
|
+
bubble.className = 'rk-ip-bubble';
|
|
4195
|
+
bubble.textContent = turn.text || '';
|
|
4196
|
+
row.appendChild(bubble);
|
|
4197
|
+
} else if(turn.kind === 'pending'){
|
|
4198
|
+
var pending = document.createElement('div');
|
|
4199
|
+
pending.className = 'rk-ip-pending';
|
|
4200
|
+
pending.dataset.pendingId = turn.id;
|
|
4201
|
+
pending.textContent = turn.label || 'Working\u2026';
|
|
4202
|
+
row.appendChild(pending);
|
|
4203
|
+
} else if(turn.kind === 'error' || turn.kind === 'stopped'){
|
|
4204
|
+
var err = document.createElement('div');
|
|
4205
|
+
err.className = 'rk-ip-bubble';
|
|
4206
|
+
err.textContent = turn.text || 'Something went wrong.';
|
|
4207
|
+
row.appendChild(err);
|
|
4208
|
+
} else if(turn.kind === 'media' && turn.url){
|
|
4209
|
+
var media = document.createElement('div');
|
|
4210
|
+
media.className = 'rk-ip-media';
|
|
4211
|
+
var isLatest = turn.id === latestPaintableId;
|
|
4212
|
+
var frame = document.createElement('div');
|
|
4213
|
+
frame.className = 'rk-ip-media-frame' + (isLatest ? ' is-latest' : '');
|
|
4214
|
+
if(isLatest){
|
|
4215
|
+
canvasWrap.hidden = false;
|
|
4216
|
+
frame.appendChild(paintStack);
|
|
4217
|
+
} else if(turn.mediaKind === 'video'){
|
|
4218
|
+
var video = document.createElement('video');
|
|
4219
|
+
video.src = turn.url; video.controls = true; video.playsInline = true; video.preload = 'metadata';
|
|
4220
|
+
frame.appendChild(video);
|
|
4221
|
+
} else {
|
|
4222
|
+
var img = document.createElement('img');
|
|
4223
|
+
img.src = turn.url; img.alt = turn.note || 'Generated image'; img.loading = 'lazy';
|
|
4224
|
+
frame.appendChild(img);
|
|
4225
|
+
}
|
|
4226
|
+
media.appendChild(frame);
|
|
4227
|
+
var actions = mediaActionsRow(turn);
|
|
4228
|
+
if(isLatest){
|
|
4229
|
+
var note = document.createElement('span');
|
|
4230
|
+
note.className = 'rk-ip-act-note';
|
|
4231
|
+
note.textContent = inpaintStrokes.length ? 'Newest \u00b7 masked' : 'Newest \u00b7 paint to mask a region';
|
|
4232
|
+
actions.appendChild(note);
|
|
4233
|
+
} else if(turn.note){
|
|
4234
|
+
var tagNote = document.createElement('span');
|
|
4235
|
+
tagNote.className = 'rk-ip-act-note';
|
|
4236
|
+
tagNote.textContent = turn.note;
|
|
4237
|
+
actions.appendChild(tagNote);
|
|
4238
|
+
}
|
|
4239
|
+
media.appendChild(actions);
|
|
4240
|
+
row.appendChild(media);
|
|
4241
|
+
} else {
|
|
4242
|
+
var text = document.createElement('div');
|
|
4243
|
+
text.className = 'rk-ip-bubble';
|
|
4244
|
+
text.textContent = turn.text || '';
|
|
4245
|
+
row.appendChild(text);
|
|
4246
|
+
}
|
|
4247
|
+
chatFeed.appendChild(row);
|
|
4248
|
+
});
|
|
4249
|
+
if(wasNearBottom) chatFeed.scrollTop = chatFeed.scrollHeight;
|
|
4250
|
+
updateBrushCursorSize();
|
|
4251
|
+
syncSessionAssets();
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
function turnById(id){ return chatTurns.filter(function(t){ return t.id === id; })[0] || null; }
|
|
4255
|
+
|
|
4256
|
+
chatFeed.addEventListener('click', function(event){
|
|
4257
|
+
var target = event.target;
|
|
4258
|
+
if(!(target instanceof Element)) return;
|
|
4259
|
+
var open = target.closest('[data-ip-open]');
|
|
4260
|
+
if(open instanceof HTMLElement){
|
|
4261
|
+
var openTurn = turnById(open.dataset.ipOpen || '');
|
|
4262
|
+
if(openTurn) openImagePreview(openTurn.url, openTurn.mediaKind === 'video' ? 'Generated video' : 'Generated image', imageDownloadName(openTurn.mediaKind === 'video' ? 'vidfarm-video' : 'vidfarm-image', openTurn.at), openTurn.mediaKind);
|
|
4263
|
+
return;
|
|
4264
|
+
}
|
|
4265
|
+
var copy = target.closest('[data-ip-copy]');
|
|
4266
|
+
if(copy instanceof HTMLElement){
|
|
4267
|
+
var copyTurn = turnById(copy.dataset.ipCopy || '');
|
|
4268
|
+
if(!copyTurn) return;
|
|
4269
|
+
void copyText(copyTurn.url).then(function(copied){
|
|
4270
|
+
copy.title = copied ? 'Copied' : 'Copy failed';
|
|
4271
|
+
setStatus(copied ? 'File URL copied.' : 'Copy failed.', copied ? 'success' : 'error');
|
|
4272
|
+
window.setTimeout(function(){ copy.title = 'Copy URL'; }, 1200);
|
|
4273
|
+
});
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
4276
|
+
var save = target.closest('[data-ip-save]');
|
|
4277
|
+
if(save instanceof HTMLElement){
|
|
4278
|
+
var saveTurn = turnById(save.dataset.ipSave || '');
|
|
4279
|
+
if(saveTurn) saveMediaToFiles(saveTurn.url, imageDownloadName(saveTurn.mediaKind === 'video' ? 'vidfarm-video' : 'vidfarm-image', saveTurn.at));
|
|
4280
|
+
}
|
|
4281
|
+
});
|
|
4282
|
+
|
|
4283
|
+
maskChip.addEventListener('click', function(event){
|
|
4284
|
+
var target = event.target;
|
|
4285
|
+
if(target instanceof Element && target.closest('[data-ip-clear-mask]')){
|
|
4286
|
+
resetMask();
|
|
4287
|
+
renderChat();
|
|
4288
|
+
composerPlaceholder();
|
|
4289
|
+
}
|
|
4290
|
+
});
|
|
4291
|
+
|
|
4292
|
+
// ---- what each request carries -------------------------------------------
|
|
4293
|
+
// DELIBERATELY no conversation context. Each turn sends ONLY the user's latest
|
|
4294
|
+
// message and (for edits) the current image. Feeding back a transcript plus
|
|
4295
|
+
// earlier versions of the same picture made the models blend instructions
|
|
4296
|
+
// across turns and drift back toward superseded versions. The newest image
|
|
4297
|
+
// already encodes every edit before it, so it is the only history worth
|
|
4298
|
+
// sending. References the user attaches by hand still go through untouched.
|
|
4299
|
+
// ---- job polling ---------------------------------------------------------
|
|
4300
|
+
function extractVideoOutputUrl(payload){
|
|
4301
|
+
var result = (payload && payload.result) || {};
|
|
4302
|
+
var out = result.output || {};
|
|
4303
|
+
return (out.video && out.video.file_url) || out.primary_file_url || (result.video && result.video.file_url) || result.primary_file_url
|
|
4304
|
+
|| (Array.isArray(out.files) ? out.files[0] : '') || (Array.isArray(result.files) ? result.files[0] : '') || '';
|
|
4305
|
+
}
|
|
4306
|
+
function pollPrimitiveJob(kind, jobId, label, isVideo){
|
|
4307
|
+
var attempt = 0;
|
|
4308
|
+
var maxAttempts = isVideo ? 150 : 90;
|
|
4309
|
+
function step(){
|
|
4310
|
+
throwIfStopped();
|
|
4311
|
+
return fetch('/api/v1/primitives/' + kind + '/jobs/' + encodeURIComponent(jobId), { credentials: 'same-origin' }).then(function(r){
|
|
4312
|
+
return parseJsonResponse(r).then(function(payload){
|
|
4313
|
+
if(!r.ok) throw new Error(payload.error || ('Unable to poll ' + label + '.'));
|
|
4314
|
+
if(payload.status === 'failed' || payload.status === 'cancelled'){ throw new Error((payload.error && payload.error.message) || (label + ' ' + payload.status + '.')); }
|
|
4315
|
+
var out = isVideo ? extractVideoOutputUrl(payload) : extractOutputUrl(payload);
|
|
4316
|
+
if(payload.status === 'succeeded' || payload.status === 'complete' || out){
|
|
4317
|
+
if(!out) throw new Error(label + ' finished without an output URL.');
|
|
4318
|
+
return absoluteUrl(out);
|
|
4319
|
+
}
|
|
4320
|
+
var note = label + ' (' + (payload.status || 'running') + ')\u2026';
|
|
4321
|
+
updatePendingLabel(activeTurnId, note);
|
|
4322
|
+
setStatus(note, '');
|
|
4323
|
+
attempt++;
|
|
4324
|
+
if(attempt >= maxAttempts){ return ''; }
|
|
4325
|
+
return new Promise(function(res){ setTimeout(res, attempt < 8 ? 1500 : (isVideo ? 4000 : 3000)); }).then(step);
|
|
4326
|
+
});
|
|
4327
|
+
});
|
|
4328
|
+
}
|
|
4329
|
+
return step();
|
|
3793
4330
|
}
|
|
3794
4331
|
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
4332
|
+
// ---- the four kinds of turn ---------------------------------------------
|
|
4333
|
+
function runGenerateTurn(text, refs){
|
|
4334
|
+
return Promise.resolve().then(function(){
|
|
4335
|
+
var pending = addTurn({ kind: 'pending', label: 'Generating image\u2026', prompt: text });
|
|
4336
|
+
activeTurnId = pending.id;
|
|
4337
|
+
var payload = { prompt: text, output_format: 'png' };
|
|
4338
|
+
if(refs.length) payload.prompt_attachments = refs;
|
|
4339
|
+
if(inpaintProviderSelect.value) payload.provider = inpaintProviderSelect.value;
|
|
4340
|
+
var tracer = tracerAutoPending ? nameTracerFromSeed(text) : getTracerValue();
|
|
4341
|
+
return fetch('/api/v1/primitives/images/generate', {
|
|
4342
|
+
method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' },
|
|
4343
|
+
body: JSON.stringify({ tracer: tracer, payload: payload, chat_message: text, chat_mode: 'image' })
|
|
4344
|
+
}).then(parseJsonResponse).then(function(j){
|
|
4345
|
+
if(!j || !j.job_id) throw new Error((j && j.error) || 'Unable to start image generation.');
|
|
4346
|
+
activeJob = { kind: 'image_generate', jobId: j.job_id };
|
|
4347
|
+
return pollPrimitiveJob('image_generate', j.job_id, 'Generating image', false).then(function(out){
|
|
4348
|
+
if(!out){ updatePendingLabel(activeTurnId, 'Still generating \u2014 it will appear in this session\u2019s history when it lands.'); activeTurnId = ''; return; }
|
|
4349
|
+
resolveActiveTurn({ kind: 'media', mediaKind: 'image', url: out, jobId: j.job_id });
|
|
4350
|
+
adoptLatestImage(out);
|
|
4351
|
+
recordSourceHistoryItem(out, 'generated', 'Generated image');
|
|
4352
|
+
setStatus('Image ready \u2014 keep chatting to refine it.', 'success');
|
|
4353
|
+
void loadTracerSessions();
|
|
4354
|
+
});
|
|
4355
|
+
});
|
|
4356
|
+
});
|
|
3799
4357
|
}
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
4358
|
+
|
|
4359
|
+
function runEditTurn(text, refs){
|
|
4360
|
+
return Promise.resolve().then(function(){
|
|
4361
|
+
var pending = addTurn({ kind: 'pending', label: 'Editing the newest image\u2026', prompt: text });
|
|
4362
|
+
activeTurnId = pending.id;
|
|
4363
|
+
var instruction = 'Edit the SOURCE IMAGE as follows: ' + text
|
|
4364
|
+
+ ' Keep everything else about the source image \u2014 subject identity, composition, framing, style, and unrelated details \u2014 unchanged unless the instruction asks otherwise.';
|
|
4365
|
+
var payload = { source_image_url: sourceImageUrl, instruction: instruction, output_format: 'png' };
|
|
4366
|
+
if(refs.length) payload.reference_attachments = refs;
|
|
4367
|
+
if(inpaintProviderSelect.value) payload.provider = inpaintProviderSelect.value;
|
|
4368
|
+
var tracer = tracerAutoPending ? nameTracerFromSeed(text) : getTracerValue();
|
|
4369
|
+
return fetch('/api/v1/primitives/images/edit', {
|
|
4370
|
+
method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' },
|
|
4371
|
+
body: JSON.stringify({ tracer: tracer, payload: payload, chat_message: text, chat_mode: 'image' })
|
|
4372
|
+
}).then(parseJsonResponse).then(function(j){
|
|
4373
|
+
if(!j || !j.job_id) throw new Error((j && j.error) || 'Unable to start the edit.');
|
|
4374
|
+
activeJob = { kind: 'image_edit', jobId: j.job_id };
|
|
4375
|
+
return pollPrimitiveJob('image_edit', j.job_id, 'Editing image', false).then(function(out){
|
|
4376
|
+
if(!out){ updatePendingLabel(activeTurnId, 'Still editing \u2014 it will appear in this session\u2019s history when it lands.'); activeTurnId = ''; return; }
|
|
4377
|
+
resolveActiveTurn({ kind: 'media', mediaKind: 'image', url: out, jobId: j.job_id });
|
|
4378
|
+
adoptLatestImage(out);
|
|
4379
|
+
void persistInpaintOutputToTemp(out, j.job_id);
|
|
4380
|
+
setStatus('Edit applied \u2014 the newest image is now the one you are editing.', 'success');
|
|
4381
|
+
void loadTracerSessions();
|
|
4382
|
+
});
|
|
4383
|
+
});
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
function runMaskedTurn(text, refs){
|
|
4388
|
+
return Promise.resolve().then(function(){
|
|
4389
|
+
// The composer text is the legacy "whole image prompt"; per-color prompts
|
|
4390
|
+
// stay in the left panel and are merged by buildInpaintInstruction().
|
|
4391
|
+
inpaintGlobalPrompt.value = text;
|
|
4392
|
+
var pending = addTurn({ kind: 'pending', label: 'Applying the masked edit\u2026', prompt: text });
|
|
4393
|
+
activeTurnId = pending.id;
|
|
4394
|
+
return submitInpaintJob({ extraRefs: refs, chatMessage: text });
|
|
4395
|
+
});
|
|
3804
4396
|
}
|
|
3805
|
-
|
|
3806
|
-
|
|
4397
|
+
|
|
4398
|
+
// ---- send ----------------------------------------------------------------
|
|
4399
|
+
function sendChatMessage(){
|
|
4400
|
+
if(isGenBusy()) return;
|
|
4401
|
+
var text = expandShorthand((genPrompt && genPrompt.value || '').trim());
|
|
4402
|
+
var masked = inpaintStrokes.length > 0;
|
|
4403
|
+
if(!text && !(masked && maskHasInstruction())){
|
|
4404
|
+
setStatus('Type what you want to create or change.', 'error');
|
|
4405
|
+
if(genPrompt) genPrompt.focus();
|
|
4406
|
+
return;
|
|
4407
|
+
}
|
|
4408
|
+
var refs = composer.refs();
|
|
4409
|
+
addTurn({
|
|
4410
|
+
role: 'user', kind: 'text',
|
|
4411
|
+
text: text || 'Apply the painted mask instructions.',
|
|
4412
|
+
refs: refs, mode: 'image', masked: masked,
|
|
4413
|
+
isEdit: Boolean(sourceImageUrl)
|
|
4414
|
+
});
|
|
4415
|
+
if(genPrompt){ genPrompt.value = ''; composer.resetHeight(); }
|
|
4416
|
+
composer.clearRefs();
|
|
4417
|
+
stopRequested = false;
|
|
4418
|
+
activeJob = { kind: '', jobId: '' };
|
|
4419
|
+
setGenBusy(true);
|
|
4420
|
+
setStatus('Working\u2026', '');
|
|
4421
|
+
var work;
|
|
4422
|
+
if(!sourceImageUrl) work = runGenerateTurn(text, refs);
|
|
4423
|
+
else if(masked) work = runMaskedTurn(text, refs);
|
|
4424
|
+
else work = runEditTurn(text, refs);
|
|
4425
|
+
work.catch(function(err){
|
|
4426
|
+
if(err && (err.stopped || err.message === '__vidfarm_stopped__')){
|
|
4427
|
+
resolveActiveTurn({ kind: 'stopped', text: 'Stopped.' });
|
|
4428
|
+
setStatus('Stopped.', '');
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4431
|
+
var msg = (err && err.message) ? err.message : 'That request failed.';
|
|
4432
|
+
if(/401|403|sign in|unauth/i.test(msg)) msg = 'Sign in to generate media.';
|
|
4433
|
+
resolveActiveTurn({ kind: 'error', text: msg });
|
|
4434
|
+
setStatus(msg, 'error');
|
|
4435
|
+
}).then(function(){
|
|
4436
|
+
stopRequested = false;
|
|
4437
|
+
activeJob = { kind: '', jobId: '' };
|
|
4438
|
+
setGenBusy(false);
|
|
4439
|
+
composerPlaceholder();
|
|
4440
|
+
syncGenSend();
|
|
4441
|
+
void loadInpaintHistory();
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
// input / Enter-to-send / paste / drag-drop / paperclip / send-stop are all
|
|
4446
|
+
// wired by the shared composer runtime above.
|
|
4447
|
+
// ⌘Z / ⇧⌘Z anywhere outside a text field undoes and redoes the paint.
|
|
4448
|
+
window.addEventListener('keydown', function(e){
|
|
4449
|
+
if(!(e.metaKey || e.ctrlKey) || String(e.key).toLowerCase() !== 'z') return;
|
|
4450
|
+
var el = document.activeElement;
|
|
4451
|
+
if(el && (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT')) return;
|
|
4452
|
+
if(!inpaintStrokes.length && !redoStrokes.length) return;
|
|
4453
|
+
e.preventDefault();
|
|
4454
|
+
if(e.shiftKey) redoStroke(); else undoStroke();
|
|
4455
|
+
});
|
|
3807
4456
|
renderGenRefs();
|
|
3808
4457
|
syncGenSend();
|
|
4458
|
+
composerPlaceholder();
|
|
4459
|
+
renderChat();
|
|
3809
4460
|
|
|
3810
4461
|
// Drag-and-drop onto the upload dropzone (legacy only wired click).
|
|
3811
4462
|
var dropZone = document.querySelector('[data-custom-inpaint-upload]');
|
|
@@ -4365,6 +5016,34 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4365
5016
|
}
|
|
4366
5017
|
function fileIcon(kind) { return kind === 'pdf' ? '📕' : kind === 'json' ? '{ }' : kind === 'markdown' ? '📝' : kind === 'audio' ? '🎵' : kind === 'text' ? '📄' : '📄'; }
|
|
4367
5018
|
|
|
5019
|
+
// ── "edit this media" hand-off: dock chat → the matching creative tool ──
|
|
5020
|
+
// Images open /tools/image, videos /tools/video, each with the file preloaded as
|
|
5021
|
+
// that tool's working file (its chat then edits THAT media). Deliberately a fresh
|
|
5022
|
+
// tool session — no ?tracer — so the tool names its own chat from the first prompt.
|
|
5023
|
+
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>';
|
|
5024
|
+
function toolEditUrl(file) {
|
|
5025
|
+
if (!file || !file.viewUrl) return '';
|
|
5026
|
+
var k = fileKind(file.name, file.contentType);
|
|
5027
|
+
var name = file.name || '';
|
|
5028
|
+
if (k === 'image') return '/tools/image?source_image_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_image_name=' + encodeURIComponent(name) : '');
|
|
5029
|
+
if (k === 'video') return '/tools/video?source_video_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_video_name=' + encodeURIComponent(name) : '');
|
|
5030
|
+
return '';
|
|
5031
|
+
}
|
|
5032
|
+
function toolEditLink(file, className) {
|
|
5033
|
+
var href = toolEditUrl(file);
|
|
5034
|
+
if (!href) return null;
|
|
5035
|
+
var a = document.createElement('a');
|
|
5036
|
+
a.className = className;
|
|
5037
|
+
a.href = href;
|
|
5038
|
+
a.target = '_blank';
|
|
5039
|
+
a.rel = 'noopener noreferrer';
|
|
5040
|
+
a.title = 'Edit in the ' + (fileKind(file.name, file.contentType) === 'video' ? 'video' : 'image') + ' tool';
|
|
5041
|
+
a.setAttribute('aria-label', a.title);
|
|
5042
|
+
// The card itself opens the lightbox; the pencil must not also trigger it.
|
|
5043
|
+
a.addEventListener('click', function (e) { e.stopPropagation(); });
|
|
5044
|
+
return a;
|
|
5045
|
+
}
|
|
5046
|
+
|
|
4368
5047
|
// Compact markdown → HTML for .md previews (headings, lists, quotes, rules,
|
|
4369
5048
|
// fenced + inline code, bold/italic/links). Escapes first, then injects tags
|
|
4370
5049
|
// (same order as linkify). Backticks are built via char codes to avoid
|
|
@@ -4425,7 +5104,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4425
5104
|
var dlBtn = document.createElement('a'); dlBtn.className = 'rk-aichat-preview-act'; dlBtn.target = '_blank'; dlBtn.rel = 'noopener noreferrer'; dlBtn.textContent = 'Download';
|
|
4426
5105
|
var linkBtn = document.createElement('button'); linkBtn.type = 'button'; linkBtn.className = 'rk-aichat-preview-act'; linkBtn.textContent = 'Copy link';
|
|
4427
5106
|
var xBtn = document.createElement('button'); xBtn.type = 'button'; xBtn.className = 'rk-aichat-modal-x'; xBtn.setAttribute('aria-label', 'Close'); xBtn.innerHTML = '×';
|
|
4428
|
-
|
|
5107
|
+
// Same hand-off as the card pencil, spelled out as a labelled action here.
|
|
5108
|
+
var editBtn = document.createElement('a'); editBtn.className = 'rk-aichat-preview-act'; editBtn.target = '_blank'; editBtn.rel = 'noopener noreferrer'; editBtn.textContent = 'Edit';
|
|
5109
|
+
head.appendChild(nameEl); head.appendChild(editBtn); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
|
|
4429
5110
|
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); }
|
|
4430
5111
|
// Copy = file text (json/md/txt) or its URL for binaries. Copy link = the
|
|
4431
5112
|
// durable public S3 URL. Download opens in a new tab when the browser can't
|
|
@@ -4474,6 +5155,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4474
5155
|
var it = items[idx];
|
|
4475
5156
|
nameEl.textContent = it.name || 'file'; nameEl.title = it.name || '';
|
|
4476
5157
|
dlBtn.href = it.viewUrl; dlBtn.setAttribute('download', it.name || '');
|
|
5158
|
+
var editHref = toolEditUrl(it);
|
|
5159
|
+
editBtn.hidden = !editHref;
|
|
5160
|
+
if (editHref) { editBtn.href = editHref; editBtn.title = 'Edit in the ' + (fileKind(it.name, it.contentType) === 'video' ? 'video' : 'image') + ' tool'; }
|
|
4477
5161
|
linkBtn.style.display = it.publicUrl ? '' : 'none';
|
|
4478
5162
|
body.innerHTML = '';
|
|
4479
5163
|
var kind = fileKind(it.name, it.contentType);
|
|
@@ -4528,7 +5212,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4528
5212
|
var files = [], seen = {};
|
|
4529
5213
|
var arts = (job && job.artifacts) || [];
|
|
4530
5214
|
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); } }
|
|
4531
|
-
|
|
5215
|
+
// Name the direct result URL after the FILE, not the tracer — fileKind() reads the
|
|
5216
|
+
// extension, and a tracer-named entry ("my-poster") classifies as 'other', losing
|
|
5217
|
+
// the image/video thumbnail and the edit-in-tool hand-off.
|
|
5218
|
+
var mu = rkResultMediaUrl(job);
|
|
5219
|
+
if (mu && !seen[mu]) {
|
|
5220
|
+
seen[mu] = true;
|
|
5221
|
+
var seg = String(mu).split('?')[0].split('/').pop() || '';
|
|
5222
|
+
files.push({ name: (/.[a-z0-9]{2,5}$/i.test(seg) ? seg : (job.tracer || 'output')), contentType: '', viewUrl: mu, publicUrl: mu });
|
|
5223
|
+
}
|
|
4532
5224
|
var res = job && job.result;
|
|
4533
5225
|
if (res && typeof res === 'object' && !Array.isArray(res) && Object.keys(res).length) {
|
|
4534
5226
|
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) {}
|
|
@@ -4581,14 +5273,21 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4581
5273
|
var grid = document.createElement('div'); grid.className = 'rk-aichat-files-grid';
|
|
4582
5274
|
list.forEach(function (f, ix) {
|
|
4583
5275
|
var k = fileKind(f.name, f.contentType);
|
|
4584
|
-
|
|
5276
|
+
// A div, not a button: media cards carry a nested "edit in tool" link, and
|
|
5277
|
+
// an anchor inside a button is invalid (and unclickable in some browsers).
|
|
5278
|
+
var card = document.createElement('div'); card.className = 'rk-aichat-file'; card.tabIndex = 0; card.setAttribute('role', 'button');
|
|
4585
5279
|
var thumb = document.createElement('div'); thumb.className = 'rk-aichat-file-thumb rk-aichat-file-thumb--' + k;
|
|
4586
5280
|
if (k === 'image') { var im = document.createElement('img'); im.src = f.viewUrl; im.alt = ''; im.loading = 'lazy'; thumb.appendChild(im); }
|
|
4587
5281
|
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); }
|
|
4588
5282
|
else { var ic = document.createElement('span'); ic.className = 'rk-aichat-file-ic'; ic.textContent = fileIcon(k); thumb.appendChild(ic); }
|
|
5283
|
+
var edit = toolEditLink(f, 'rk-aichat-file-open');
|
|
5284
|
+
if (edit) { edit.innerHTML = PENCIL_SVG; thumb.appendChild(edit); }
|
|
4589
5285
|
var nm = document.createElement('div'); nm.className = 'rk-aichat-file-name'; nm.textContent = f.name || 'file'; nm.title = f.name || '';
|
|
4590
5286
|
card.appendChild(thumb); card.appendChild(nm);
|
|
4591
5287
|
card.addEventListener('click', function () { openFilePreview(list, ix); });
|
|
5288
|
+
card.addEventListener('keydown', function (e) {
|
|
5289
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFilePreview(list, ix); }
|
|
5290
|
+
});
|
|
4592
5291
|
grid.appendChild(card);
|
|
4593
5292
|
});
|
|
4594
5293
|
wrap.appendChild(grid); turn.appendChild(wrap); logBottom();
|