@officexapp/vidfarm-devcli 0.21.24 → 0.21.27
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/vidfarm/SKILL.md +35 -7
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +26 -0
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +37 -6
- package/.agents/skills/vidfarm/references/core-workflows.md +1 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +44 -2
- package/.agents/skills/vidfarm/references/primitives.md +28 -0
- package/.agents/skills/vidfarm-media/SKILL.md +3 -1
- package/.agents/skills/vidfarm-media/references/tts.md +1 -1
- package/SKILL.director.md +171 -16
- package/SKILL.md +14 -2
- package/clipper.md +3 -2
- package/dist/src/cli.js +412 -17
- package/dist/src/devcli/cost-mode.js +66 -26
- package/dist/src/devcli/sequence.js +619 -0
- package/dist/src/services/sequence-prompts.js +463 -0
- package/package.json +4 -2
- 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 +1069 -483
- package/public/serve-shells/tools-video.html +140 -14
|
@@ -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,9 +1185,33 @@ 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
|
|
1177
|
-
.rk-
|
|
1178
|
-
.rk-
|
|
1188
|
+
/* center: chat feed + composer */
|
|
1189
|
+
.rk-ip-chat{display:grid;grid-template-rows:minmax(0,1fr) auto;height:min(76vh,860px);min-height:520px;overflow:hidden}
|
|
1190
|
+
.rk-ip-feed{min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:18px 18px 6px;display:flex;flex-direction:column;gap:14px}
|
|
1191
|
+
.rk-ip-canvasholder{display:none}
|
|
1192
|
+
.rk-ip-turn{display:flex;flex-direction:column;gap:6px;max-width:100%}
|
|
1193
|
+
.rk-ip-turn[data-role="user"]{align-items:flex-end}
|
|
1194
|
+
.rk-ip-turn[data-role="assistant"]{align-items:flex-start}
|
|
1195
|
+
.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}
|
|
1196
|
+
.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)}
|
|
1197
|
+
.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)}
|
|
1198
|
+
.rk-ip-turn-tag{font-size:10.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--rk-text-faint)}
|
|
1199
|
+
.rk-ip-turn-refs{display:flex;flex-wrap:wrap;gap:5px;justify-content:flex-end}
|
|
1200
|
+
.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}
|
|
1201
|
+
.rk-ip-media{width:fit-content;max-width:min(100%,640px);display:grid;gap:7px}
|
|
1202
|
+
.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}
|
|
1203
|
+
.rk-ip-media-frame.is-latest{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1204
|
+
.rk-ip-media-frame img,.rk-ip-media-frame video{display:block;max-width:100%;max-height:min(52vh,520px);width:auto;height:auto}
|
|
1205
|
+
.rk-ip-media-actions{display:flex;flex-wrap:wrap;align-items:center;gap:5px}
|
|
1206
|
+
.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)}
|
|
1207
|
+
.rk-ip-act:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1208
|
+
.rk-ip-act-note{font-size:11.5px;font-weight:700;color:var(--rk-text-faint);margin-left:2px}
|
|
1209
|
+
.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)}
|
|
1210
|
+
.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}
|
|
1211
|
+
.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)}
|
|
1212
|
+
.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}
|
|
1213
|
+
.rk-ip-turn[data-kind="error"] .rk-ip-bubble{background:#fdf3f2;border-color:#e6a8a0;color:#b42318;font-weight:600}
|
|
1214
|
+
.rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:34px 20px;margin:auto 0}
|
|
1179
1215
|
.rk-inpaint-empty[hidden]{display:none}
|
|
1180
1216
|
.rk-inpaint-empty-ic{color:var(--rk-n-400)}
|
|
1181
1217
|
.rk-inpaint-empty-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink)}
|
|
@@ -1183,26 +1219,48 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1183
1219
|
.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
1220
|
--inpaint-brush-cursor-size:70px;--inpaint-brush-cursor-x:0px;--inpaint-brush-cursor-y:0px}
|
|
1185
1221
|
.editor-inpaint-canvas-wrap[hidden]{display:none}
|
|
1186
|
-
.editor-inpaint-canvas-wrap img{display:block;max-width:100%;max-height:min(
|
|
1222
|
+
.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}
|
|
1223
|
+
/* paint toolbar — sits directly under the image it paints, never over it */
|
|
1224
|
+
/* align-items:center keeps the canvas wrap at the image's intrinsic width — stretching
|
|
1225
|
+
it would stretch the absolutely-positioned mask canvas and skew stroke coordinates. */
|
|
1226
|
+
.rk-ip-paintstack{display:inline-flex;flex-direction:column;align-items:center;max-width:100%}
|
|
1227
|
+
.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}
|
|
1228
|
+
.rk-ip-painttools-brush{display:flex;align-items:center;gap:6px}
|
|
1229
|
+
.rk-ip-painttools-label{font-size:11px;font-weight:800;color:var(--rk-text-muted);white-space:nowrap}
|
|
1230
|
+
.rk-ip-painttools .rk-inpaint-range{width:86px}
|
|
1231
|
+
.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}
|
|
1232
|
+
.rk-ip-painttool:hover:not(:disabled){background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1233
|
+
.rk-ip-painttool:disabled{opacity:.45;cursor:default}
|
|
1234
|
+
.rk-ip-painttool svg{width:13px;height:13px}
|
|
1187
1235
|
.editor-inpaint-canvas-wrap canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.86;cursor:none;touch-action:none}
|
|
1188
1236
|
.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
1237
|
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
1238
|
transform:translate(var(--inpaint-brush-cursor-x),var(--inpaint-brush-cursor-y)) translate(-50%,-50%)}
|
|
1191
1239
|
.editor-inpaint-brush-cursor[data-visible="true"]{opacity:1}
|
|
1192
1240
|
|
|
1193
|
-
/*
|
|
1194
|
-
.rk-ip-
|
|
1195
|
-
|
|
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
|
-
|
|
1241
|
+
/* chat composer */
|
|
1242
|
+
.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;
|
|
1243
|
+
border-bottom-left-radius:var(--rk-r-3xl);border-bottom-right-radius:var(--rk-r-3xl)}
|
|
1244
|
+
.rk-ip-composer.is-dragref{background:var(--rk-gold-tint)}
|
|
1245
|
+
.rk-ip-composer.is-busy .rk-ip-comp-status{color:var(--rk-gold-700)}
|
|
1246
|
+
.rk-ip-comp-input{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;
|
|
1247
|
+
color:var(--rk-ink);min-height:56px;max-height:232px;overflow-y:hidden;padding:10px 12px;transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
1248
|
+
.rk-ip-comp-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1249
|
+
.rk-ip-comp-input::placeholder{color:var(--rk-text-faint)}
|
|
1250
|
+
.rk-ip-comp-input:disabled{cursor:not-allowed;color:var(--rk-text-muted);background:var(--rk-n-50)}
|
|
1251
|
+
.rk-ip-comp-bar{display:flex;align-items:center;gap:8px}
|
|
1252
|
+
.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);
|
|
1253
|
+
font:inherit;font-size:12px;font-weight:800;padding:7px 26px 7px 12px;cursor:pointer;outline:0;
|
|
1254
|
+
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");
|
|
1255
|
+
background-repeat:no-repeat;background-position:right 9px center}
|
|
1256
|
+
.rk-ip-comp-modeselect:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1257
|
+
.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}
|
|
1258
|
+
.rk-ip-comp-status[data-tone="error"]{color:#b42318}
|
|
1259
|
+
.rk-ip-comp-status[data-tone="success"]{color:#1f7a44}
|
|
1260
|
+
.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);
|
|
1261
|
+
font-size:11.5px;font-weight:700;color:var(--rk-gold-700);justify-self:start;max-width:100%}
|
|
1262
|
+
.rk-ip-comp-mask[hidden]{display:none}
|
|
1263
|
+
.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}
|
|
1206
1264
|
.rk-ip-gen-clip:disabled{opacity:.5;cursor:not-allowed}
|
|
1207
1265
|
.rk-ip-gen-presets{display:flex;flex-wrap:wrap;gap:6px}
|
|
1208
1266
|
.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)}
|
|
@@ -1222,6 +1280,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1222
1280
|
.rk-ip-gen-send{border:0;background:var(--rk-gold-500,var(--rk-gold-600));color:var(--rk-ink)}
|
|
1223
1281
|
.rk-ip-gen-send:hover:not(:disabled){background:var(--rk-gold-600)}
|
|
1224
1282
|
.rk-ip-gen-send:disabled{opacity:.5;cursor:default}
|
|
1283
|
+
.rk-ip-gen-send[data-mode="stop"]{background:var(--rk-ink);color:#fff}
|
|
1284
|
+
.rk-ip-gen-send[data-mode="stop"]:hover{background:#000}
|
|
1225
1285
|
.rk-ip-gen-send.is-busy{background:var(--rk-gold-tint);pointer-events:none}
|
|
1226
1286
|
.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}
|
|
1227
1287
|
.rk-ip-gen-status{font-size:12px;font-weight:600;min-height:16px;color:var(--rk-text-muted);text-align:left;padding-left:2px}
|
|
@@ -1256,6 +1316,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1256
1316
|
.custom-inpaint-history-item[data-kind="source"]{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
1257
1317
|
.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
1318
|
.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)}
|
|
1319
|
+
.custom-inpaint-history-videothumb{display:grid;place-items:center;font-size:16px;color:var(--rk-n-600);background:var(--rk-n-100)}
|
|
1320
|
+
.custom-inpaint-history-thumb-button{cursor:pointer}
|
|
1259
1321
|
.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
1322
|
.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
1323
|
.custom-inpaint-history-meta{min-width:0;display:grid;gap:5px}
|
|
@@ -1283,7 +1345,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1283
1345
|
.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
1346
|
.custom-inpaint-lightbox-actions{display:flex;align-items:center;gap:8px}
|
|
1285
1347
|
.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)}
|
|
1348
|
+
.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)}
|
|
1349
|
+
.custom-inpaint-lightbox-stage img[hidden],.custom-inpaint-lightbox-stage video[hidden]{display:none}
|
|
1287
1350
|
|
|
1288
1351
|
@keyframes rk-ip-spin{to{transform:rotate(360deg)}}
|
|
1289
1352
|
|
|
@@ -1394,17 +1457,18 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1394
1457
|
<div class="rk-inpaint-head-text">
|
|
1395
1458
|
<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
1459
|
<h1 class="rk-h1">Create Image</h1>
|
|
1397
|
-
<p class="rk-sub">
|
|
1460
|
+
<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, or switch the composer to Video to animate it.</p>
|
|
1398
1461
|
</div>
|
|
1399
1462
|
<div class="rk-inpaint-toolbar">
|
|
1400
1463
|
<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>
|
|
1464
|
+
<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
1465
|
<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
1466
|
<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
1467
|
</div>
|
|
1404
1468
|
</div>
|
|
1405
1469
|
|
|
1406
1470
|
<div class="rk-inpaint-workbench">
|
|
1407
|
-
<!-- LEFT: source +
|
|
1471
|
+
<!-- LEFT: source + per-color mask instructions (mask block appears once you paint) -->
|
|
1408
1472
|
<section class="rk-inpaint-panel" aria-label="Controls">
|
|
1409
1473
|
<div class="rk-inpaint-block">
|
|
1410
1474
|
<label class="rk-label">Source image</label>
|
|
@@ -1414,98 +1478,99 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1414
1478
|
<span class="rk-inpaint-drop-name" data-custom-inpaint-source-name>No image selected</span>
|
|
1415
1479
|
</button>
|
|
1416
1480
|
<input data-custom-inpaint-file type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" hidden />
|
|
1481
|
+
<span class="rk-hint">Optional — just type in the chat and the first generated image becomes your working image.</span>
|
|
1417
1482
|
</div>
|
|
1418
1483
|
|
|
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">
|
|
1484
|
+
<div class="rk-inpaint-block rk-ip-maskblock" data-ip-mask-block hidden>
|
|
1431
1485
|
<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>
|
|
1486
|
+
<label class="rk-label">Mask regions</label>
|
|
1445
1487
|
<input class="rk-ip-colorpicker" data-editor-inpaint-color-picker type="color" value="#ff2d55" aria-label="Mask color" />
|
|
1446
1488
|
</div>
|
|
1447
|
-
<span class="rk-hint">Paint
|
|
1489
|
+
<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
1490
|
<div class="editor-inpaint-swatches" data-editor-inpaint-swatches></div>
|
|
1449
1491
|
<div class="editor-inpaint-prompts" data-editor-inpaint-prompts></div>
|
|
1450
1492
|
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-ip-addcolor" data-editor-inpaint-add-color>+ Add color</button>
|
|
1451
1493
|
</div>
|
|
1452
1494
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1495
|
+
<!-- Legacy fields the chat drives behind the scenes: the composer text is written
|
|
1496
|
+
into the whole-image prompt, and the composer's send press clicks submit. -->
|
|
1497
|
+
<div class="rk-ip-legacy" aria-hidden="true">
|
|
1498
|
+
<textarea class="rk-textarea rk-ip-global" data-editor-inpaint-global-prompt></textarea>
|
|
1499
|
+
<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>
|
|
1500
|
+
<span class="editor-inpaint-attachment-chips rk-ip-attach-chips" data-editor-inpaint-attachments="global"></span>
|
|
1501
|
+
<input data-editor-inpaint-attachment-input type="file" accept="image/jpeg,image/png,image/webp" multiple hidden />
|
|
1502
|
+
<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>
|
|
1503
|
+
<div class="custom-inpaint-output" data-custom-inpaint-output hidden>
|
|
1504
|
+
<button type="button" data-custom-inpaint-result-open aria-label="View image edit result">
|
|
1505
|
+
<img data-custom-inpaint-result alt="Image edit result" />
|
|
1506
|
+
</button>
|
|
1507
|
+
<button type="button" data-custom-inpaint-save>Save to Files</button>
|
|
1508
|
+
</div>
|
|
1456
1509
|
</div>
|
|
1457
1510
|
</section>
|
|
1458
1511
|
|
|
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>
|
|
1512
|
+
<!-- CENTER: chat feed + composer. Every turn appends to the feed; edits always
|
|
1513
|
+
apply to the LATEST image, which is the one you paint on in-feed. -->
|
|
1514
|
+
<section class="rk-ip-chat" aria-label="Create Image chat">
|
|
1515
|
+
<div class="rk-ip-feed" data-ip-feed>
|
|
1516
|
+
<div class="rk-inpaint-empty" data-custom-inpaint-empty>
|
|
1517
|
+
<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>
|
|
1518
|
+
<p class="rk-inpaint-empty-title">Describe the image you want</p>
|
|
1519
|
+
<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, or switch the composer to Video to animate it.</p>
|
|
1520
|
+
<div class="rk-ip-gen-presets" id="rk-ip-genpresets">
|
|
1521
|
+
<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>
|
|
1522
|
+
<button type="button" class="rk-ip-preset" data-preset="storyboard" title="Fill the prompt with a numbered storyboard (sequential shot panels)">+ Storyboard</button>
|
|
1481
1523
|
</div>
|
|
1482
|
-
<p class="rk-ip-gen-status" id="rk-ip-genstatus" data-tone="" aria-live="polite"></p>
|
|
1483
1524
|
</div>
|
|
1484
1525
|
</div>
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1526
|
+
|
|
1527
|
+
<!-- The paint surface. JS moves this whole stack into the newest image bubble,
|
|
1528
|
+
so the brush toolbar always travels with the image it paints. -->
|
|
1529
|
+
<div class="rk-ip-canvasholder" data-ip-canvas-holder hidden>
|
|
1530
|
+
<div class="rk-ip-paintstack" data-ip-paint-stack>
|
|
1531
|
+
<div class="rk-inpaint-canvaswrap editor-inpaint-canvas-wrap" data-custom-inpaint-canvas-wrap hidden>
|
|
1532
|
+
<img data-editor-inpaint-image class="rk-inpaint-img" alt="" />
|
|
1533
|
+
<canvas data-editor-inpaint-canvas class="rk-inpaint-canvas"></canvas>
|
|
1534
|
+
<div class="editor-inpaint-brush-cursor" data-editor-inpaint-brush-cursor aria-hidden="true"></div>
|
|
1535
|
+
</div>
|
|
1536
|
+
<div class="rk-ip-painttools" data-ip-painttools>
|
|
1537
|
+
<span class="rk-ip-painttools-brush">
|
|
1538
|
+
<span class="rk-ip-painttools-label">Brush <span data-editor-inpaint-brush-label>70</span></span>
|
|
1539
|
+
<input data-editor-inpaint-brush class="rk-inpaint-range" type="range" min="6" max="180" step="1" value="70" aria-label="Brush size" />
|
|
1540
|
+
</span>
|
|
1541
|
+
<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>
|
|
1542
|
+
<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>
|
|
1543
|
+
<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>
|
|
1544
|
+
</div>
|
|
1545
|
+
</div>
|
|
1489
1546
|
</div>
|
|
1490
|
-
</section>
|
|
1491
1547
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
<
|
|
1496
|
-
|
|
1497
|
-
<
|
|
1548
|
+
<div class="rk-ip-composer" data-ip-composer>
|
|
1549
|
+
<div class="rk-ip-comp-mask" data-ip-comp-mask hidden></div>
|
|
1550
|
+
<div class="rk-ip-gen-refs" id="rk-ip-genrefs" hidden></div>
|
|
1551
|
+
<textarea id="rk-ip-genprompt" class="rk-ip-comp-input" rows="2" placeholder="Describe the image you want…"></textarea>
|
|
1552
|
+
<div class="rk-ip-comp-bar">
|
|
1553
|
+
<span class="rk-ip-comp-mode" data-ip-mode-wrap>
|
|
1554
|
+
<select class="rk-ip-comp-modeselect" data-ip-mode aria-label="What to create">
|
|
1555
|
+
<option value="image">Image</option>
|
|
1556
|
+
<option value="video">Video</option>
|
|
1557
|
+
</select>
|
|
1558
|
+
</span>
|
|
1559
|
+
<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>
|
|
1560
|
+
<p class="custom-inpaint-status rk-ip-comp-status" data-custom-inpaint-status data-tone="" aria-live="polite"></p>
|
|
1561
|
+
<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>
|
|
1498
1562
|
</div>
|
|
1499
|
-
<
|
|
1500
|
-
<img class="rk-inpaint-result-img" data-custom-inpaint-result alt="Image edit result" />
|
|
1501
|
-
</button>
|
|
1563
|
+
<p class="rk-ip-gen-status" id="rk-ip-genstatus" data-tone="" aria-live="polite" hidden></p>
|
|
1502
1564
|
</div>
|
|
1565
|
+
</section>
|
|
1503
1566
|
|
|
1567
|
+
<!-- RIGHT: tracer-grouped session history -->
|
|
1568
|
+
<section class="rk-inpaint-output" aria-label="Session history">
|
|
1504
1569
|
<div class="rk-inpaint-history">
|
|
1505
1570
|
<div class="rk-inpaint-history-head">
|
|
1506
1571
|
<h2 class="rk-inpaint-h2">This session</h2>
|
|
1507
1572
|
<div class="rk-ip-histtools">
|
|
1508
|
-
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-history-refresh aria-label="
|
|
1573
|
+
<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
1574
|
</div>
|
|
1510
1575
|
</div>
|
|
1511
1576
|
<div class="custom-inpaint-history-list rk-inpaint-historylist" data-custom-inpaint-history-list>
|
|
@@ -1553,13 +1618,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1553
1618
|
<div class="custom-inpaint-lightbox-head">
|
|
1554
1619
|
<p class="custom-inpaint-lightbox-title" id="custom-inpaint-lightbox-title" data-custom-inpaint-lightbox-title>Image edit output</p>
|
|
1555
1620
|
<div class="custom-inpaint-lightbox-actions">
|
|
1556
|
-
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-lightbox-copy aria-label="Copy
|
|
1621
|
+
<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>
|
|
1622
|
+
<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
1623
|
<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
1624
|
<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
1625
|
</div>
|
|
1560
1626
|
</div>
|
|
1561
1627
|
<div class="custom-inpaint-lightbox-stage">
|
|
1562
1628
|
<img data-custom-inpaint-lightbox-image alt="Image edit output preview" />
|
|
1629
|
+
<video data-custom-inpaint-lightbox-video controls playsinline hidden></video>
|
|
1563
1630
|
</div>
|
|
1564
1631
|
</div>
|
|
1565
1632
|
</div>
|
|
@@ -1697,7 +1764,17 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1697
1764
|
const lightboxImage = document.querySelector("[data-custom-inpaint-lightbox-image]");
|
|
1698
1765
|
const lightboxCopy = document.querySelector("[data-custom-inpaint-lightbox-copy]");
|
|
1699
1766
|
const lightboxDownload = document.querySelector("[data-custom-inpaint-lightbox-download]");
|
|
1767
|
+
const lightboxLink = document.querySelector("[data-custom-inpaint-lightbox-link]");
|
|
1768
|
+
const lightboxVideo = document.querySelector("[data-custom-inpaint-lightbox-video]");
|
|
1700
1769
|
const lightboxClose = document.querySelector("[data-custom-inpaint-lightbox-close]");
|
|
1770
|
+
// chat surface
|
|
1771
|
+
const chatFeed = document.querySelector("[data-ip-feed]");
|
|
1772
|
+
const canvasHolder = document.querySelector("[data-ip-canvas-holder]");
|
|
1773
|
+
const composerEl = document.querySelector("[data-ip-composer]");
|
|
1774
|
+
const modeSelect = document.querySelector("[data-ip-mode]");
|
|
1775
|
+
const maskBlock = document.querySelector("[data-ip-mask-block]");
|
|
1776
|
+
const maskChip = document.querySelector("[data-ip-comp-mask]");
|
|
1777
|
+
const paintStack = document.querySelector("[data-ip-paint-stack]");
|
|
1701
1778
|
if (!(uploadButton instanceof HTMLButtonElement)
|
|
1702
1779
|
|| !(startNewButton instanceof HTMLButtonElement)
|
|
1703
1780
|
|| !(fileInput instanceof HTMLInputElement)
|
|
@@ -1737,7 +1814,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1737
1814
|
|| !(lightboxImage instanceof HTMLImageElement)
|
|
1738
1815
|
|| !(lightboxCopy instanceof HTMLButtonElement)
|
|
1739
1816
|
|| !(lightboxDownload instanceof HTMLAnchorElement)
|
|
1740
|
-
|| !(lightboxClose instanceof HTMLButtonElement)
|
|
1817
|
+
|| !(lightboxClose instanceof HTMLButtonElement)
|
|
1818
|
+
|| !(chatFeed instanceof HTMLElement)
|
|
1819
|
+
|| !(canvasHolder instanceof HTMLElement)
|
|
1820
|
+
|| !(composerEl instanceof HTMLElement)
|
|
1821
|
+
|| !(modeSelect instanceof HTMLSelectElement)
|
|
1822
|
+
|| !(maskBlock instanceof HTMLElement)
|
|
1823
|
+
|| !(maskChip instanceof HTMLElement)
|
|
1824
|
+
|| !(paintStack instanceof HTMLElement)) {
|
|
1741
1825
|
return;
|
|
1742
1826
|
}
|
|
1743
1827
|
|
|
@@ -1752,13 +1836,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1752
1836
|
let inpaintAttachments = [];
|
|
1753
1837
|
let pendingInpaintAttachmentScope = null;
|
|
1754
1838
|
let inpaintHistoryItems = [];
|
|
1839
|
+
// Chat state. Declared up here because the legacy helpers below (loadSourceUrl,
|
|
1840
|
+
// applyCompletedInpaintJob, resetWorkspace) push turns into it.
|
|
1841
|
+
let chatTurns = [];
|
|
1842
|
+
let activeTurnId = "";
|
|
1755
1843
|
// Local "starting image" history entries (uploaded / generated / deep-linked
|
|
1756
1844
|
// sources). They are not job runs, so they survive server-history reloads by
|
|
1757
1845
|
// being re-injected after each rebuild — this lets users see where a session
|
|
1758
1846
|
// started from.
|
|
1759
|
-
let sourceHistoryItems = [];
|
|
1760
1847
|
let historyTracerRefreshTimer = 0;
|
|
1761
|
-
const hiddenInpaintHistoryKeys = new Set();
|
|
1762
1848
|
const maxInpaintAttachments = 8;
|
|
1763
1849
|
const allowedInpaintAttachmentTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
1764
1850
|
|
|
@@ -1831,12 +1917,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1831
1917
|
: (sessionLocked ? "Edit generated \u2014 Start New to edit again" : "Generate edit");
|
|
1832
1918
|
}
|
|
1833
1919
|
|
|
1834
|
-
//
|
|
1835
|
-
//
|
|
1836
|
-
//
|
|
1920
|
+
// The chat surface iterates forever on the newest image, so the old
|
|
1921
|
+
// freeze-after-one-edit lock is gone: setSessionLocked() is kept as a no-op
|
|
1922
|
+
// so every legacy call site still type-checks and simply never locks.
|
|
1837
1923
|
let sessionLocked = false;
|
|
1838
1924
|
function applySessionLock() {
|
|
1839
|
-
const locked =
|
|
1925
|
+
const locked = false;
|
|
1840
1926
|
const workbench = document.querySelector(".rk-inpaint-workbench");
|
|
1841
1927
|
if (workbench instanceof HTMLElement) workbench.classList.toggle("is-locked", locked);
|
|
1842
1928
|
inpaintGlobalPrompt.readOnly = locked;
|
|
@@ -1854,8 +1940,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1854
1940
|
// Keep the submit button + label consistent with the lock while not loading.
|
|
1855
1941
|
if (inpaintSubmit.getAttribute("data-loading") !== "true") setSubmitLoading(false);
|
|
1856
1942
|
}
|
|
1857
|
-
function setSessionLocked(
|
|
1858
|
-
sessionLocked =
|
|
1943
|
+
function setSessionLocked() {
|
|
1944
|
+
sessionLocked = false;
|
|
1859
1945
|
applySessionLock();
|
|
1860
1946
|
}
|
|
1861
1947
|
|
|
@@ -1873,15 +1959,31 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1873
1959
|
return prefix + "-" + stamp + ".png";
|
|
1874
1960
|
}
|
|
1875
1961
|
|
|
1876
|
-
|
|
1962
|
+
// Fullscreen preview for any feed item — images and generated videos alike.
|
|
1963
|
+
function openImagePreview(url, label, downloadName, mediaKind) {
|
|
1877
1964
|
if (!url) return;
|
|
1878
|
-
|
|
1879
|
-
|
|
1965
|
+
const isVideo = mediaKind === "video";
|
|
1966
|
+
if (isVideo && lightboxVideo instanceof HTMLVideoElement) {
|
|
1967
|
+
lightboxVideo.src = url;
|
|
1968
|
+
lightboxVideo.hidden = false;
|
|
1969
|
+
lightboxImage.hidden = true;
|
|
1970
|
+
lightboxImage.removeAttribute("src");
|
|
1971
|
+
} else {
|
|
1972
|
+
if (lightboxVideo instanceof HTMLVideoElement) {
|
|
1973
|
+
lightboxVideo.pause();
|
|
1974
|
+
lightboxVideo.removeAttribute("src");
|
|
1975
|
+
lightboxVideo.hidden = true;
|
|
1976
|
+
}
|
|
1977
|
+
lightboxImage.hidden = false;
|
|
1978
|
+
lightboxImage.src = url;
|
|
1979
|
+
lightboxImage.alt = label || "Image edit output preview";
|
|
1980
|
+
}
|
|
1880
1981
|
lightboxTitle.textContent = label || "Image edit output";
|
|
1881
1982
|
lightboxCopy.dataset.copyUrl = url;
|
|
1882
1983
|
lightboxCopy.setAttribute("title", "Copy URL");
|
|
1883
1984
|
lightboxDownload.href = url;
|
|
1884
|
-
lightboxDownload.setAttribute("download", downloadName || imageDownloadName("inpaint-output"));
|
|
1985
|
+
lightboxDownload.setAttribute("download", downloadName || imageDownloadName(isVideo ? "video-output" : "inpaint-output"));
|
|
1986
|
+
if (lightboxLink instanceof HTMLAnchorElement) lightboxLink.href = url;
|
|
1885
1987
|
lightbox.hidden = false;
|
|
1886
1988
|
lightbox.setAttribute("data-open", "true");
|
|
1887
1989
|
document.body.style.overflow = "hidden";
|
|
@@ -1891,9 +1993,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
1891
1993
|
lightbox.setAttribute("data-open", "false");
|
|
1892
1994
|
lightbox.hidden = true;
|
|
1893
1995
|
lightboxImage.removeAttribute("src");
|
|
1996
|
+
if (lightboxVideo instanceof HTMLVideoElement) {
|
|
1997
|
+
lightboxVideo.pause();
|
|
1998
|
+
lightboxVideo.removeAttribute("src");
|
|
1999
|
+
lightboxVideo.hidden = true;
|
|
2000
|
+
}
|
|
1894
2001
|
lightboxCopy.dataset.copyUrl = "";
|
|
1895
2002
|
lightboxCopy.setAttribute("title", "Copy URL");
|
|
1896
2003
|
lightboxDownload.href = "#";
|
|
2004
|
+
if (lightboxLink instanceof HTMLAnchorElement) lightboxLink.href = "#";
|
|
1897
2005
|
document.body.style.overflow = "";
|
|
1898
2006
|
}
|
|
1899
2007
|
|
|
@@ -2104,6 +2212,31 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2104
2212
|
}
|
|
2105
2213
|
renderAllAttachmentChips();
|
|
2106
2214
|
applySessionLock();
|
|
2215
|
+
syncMaskUi();
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// The mask controls only exist while a mask exists: the left block holds the
|
|
2219
|
+
// per-color instructions, and the composer grows a chip so it is obvious the
|
|
2220
|
+
// next message is a MASKED edit rather than a whole-image one.
|
|
2221
|
+
function syncMaskUi() {
|
|
2222
|
+
const painted = inpaintStrokes.length > 0;
|
|
2223
|
+
const showBlock = Boolean(sourceImageUrl) && (painted || redoStrokes.length > 0);
|
|
2224
|
+
maskBlock.hidden = !showBlock;
|
|
2225
|
+
const usedColors = new Set(inpaintStrokes.map((stroke) => normalizeHexColor(stroke.color)));
|
|
2226
|
+
maskChip.hidden = !painted;
|
|
2227
|
+
if (painted) {
|
|
2228
|
+
maskChip.innerHTML = "";
|
|
2229
|
+
const label = document.createElement("span");
|
|
2230
|
+
label.textContent = "Masked edit \u00b7 " + usedColors.size + " region" + (usedColors.size === 1 ? "" : "s") + " painted";
|
|
2231
|
+
const clear = document.createElement("button");
|
|
2232
|
+
clear.type = "button";
|
|
2233
|
+
clear.dataset.ipClearMask = "true";
|
|
2234
|
+
clear.textContent = "Clear mask";
|
|
2235
|
+
maskChip.append(label, clear);
|
|
2236
|
+
}
|
|
2237
|
+
inpaintUndo.disabled = !inpaintStrokes.length;
|
|
2238
|
+
inpaintRedo.disabled = !redoStrokes.length;
|
|
2239
|
+
inpaintClear.disabled = !painted;
|
|
2107
2240
|
}
|
|
2108
2241
|
|
|
2109
2242
|
function redrawInpaintMask() {
|
|
@@ -2161,6 +2294,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2161
2294
|
inpaintStrokes.push(currentInpaintStroke);
|
|
2162
2295
|
redoStrokes = [];
|
|
2163
2296
|
redrawInpaintMask();
|
|
2297
|
+
syncMaskUi();
|
|
2164
2298
|
}
|
|
2165
2299
|
|
|
2166
2300
|
function continueInpaintStroke(event) {
|
|
@@ -2451,22 +2585,17 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2451
2585
|
setStatus("The source image URL is invalid.", "error");
|
|
2452
2586
|
return;
|
|
2453
2587
|
}
|
|
2454
|
-
setSessionLocked(false);
|
|
2455
|
-
sourceImageUrl = url;
|
|
2456
2588
|
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");
|
|
2589
|
+
adoptLatestImage(url, sourceImageName);
|
|
2590
|
+
addTurn({
|
|
2591
|
+
role: "assistant",
|
|
2592
|
+
kind: "media",
|
|
2593
|
+
mediaKind: "image",
|
|
2594
|
+
url,
|
|
2595
|
+
note: origin === "generated" ? "generated" : "starting image"
|
|
2596
|
+
});
|
|
2597
|
+
recordSourceHistoryItem(url, origin || "url", sourceImageName);
|
|
2598
|
+
setStatus("Working image ready — describe an edit, or paint a mask on it first.", "success");
|
|
2470
2599
|
}
|
|
2471
2600
|
|
|
2472
2601
|
function clearInpaintUrlParams() {
|
|
@@ -2539,6 +2668,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2539
2668
|
currentInpaintStroke = null;
|
|
2540
2669
|
isInpaintPainting = false;
|
|
2541
2670
|
redrawInpaintMask();
|
|
2671
|
+
syncMaskUi();
|
|
2542
2672
|
}
|
|
2543
2673
|
|
|
2544
2674
|
function resetWorkspace() {
|
|
@@ -2546,7 +2676,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2546
2676
|
sourceImageUrl = "";
|
|
2547
2677
|
sourceImageName = "";
|
|
2548
2678
|
sourceName.textContent = "No image selected";
|
|
2549
|
-
emptyState.hidden = false;
|
|
2550
2679
|
canvasWrap.hidden = true;
|
|
2551
2680
|
outputPanel.hidden = true;
|
|
2552
2681
|
resultImage.removeAttribute("src");
|
|
@@ -2559,9 +2688,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2559
2688
|
pendingInpaintAttachmentScope = null;
|
|
2560
2689
|
activeInpaintColor = "#ff2d55";
|
|
2561
2690
|
inpaintColorPicker.value = activeInpaintColor;
|
|
2562
|
-
|
|
2691
|
+
chatTurns = [];
|
|
2692
|
+
activeTurnId = "";
|
|
2563
2693
|
resetMask();
|
|
2564
2694
|
renderInpaintControls();
|
|
2695
|
+
renderChat();
|
|
2565
2696
|
setStatus("", "");
|
|
2566
2697
|
}
|
|
2567
2698
|
|
|
@@ -2580,26 +2711,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2580
2711
|
const wasAutoPending = tracerAutoPending;
|
|
2581
2712
|
if (wasAutoPending) nameTracerFromSeed(fileName.replace(/\.[a-z0-9]+$/i, ""));
|
|
2582
2713
|
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;
|
|
2714
|
+
const uploaded = absoluteUrl(uploadedUrl);
|
|
2715
|
+
adoptLatestImage(uploaded, fileName);
|
|
2716
|
+
addTurn({ role: "assistant", kind: "media", mediaKind: "image", url: uploaded, note: "uploaded image" });
|
|
2597
2717
|
if (wasAutoPending) {
|
|
2598
2718
|
void loadInpaintHistory();
|
|
2599
2719
|
void loadTracerSessions();
|
|
2600
2720
|
}
|
|
2601
|
-
recordSourceHistoryItem(
|
|
2602
|
-
setStatus("
|
|
2721
|
+
recordSourceHistoryItem(uploaded, "upload", fileName);
|
|
2722
|
+
setStatus("Uploaded — describe an edit, or paint a mask on it first.", "success");
|
|
2603
2723
|
} catch (error) {
|
|
2604
2724
|
console.error(error);
|
|
2605
2725
|
setStatus(error instanceof Error ? error.message : "Unable to upload source image.", "error");
|
|
@@ -2641,48 +2761,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2641
2761
|
};
|
|
2642
2762
|
}
|
|
2643
2763
|
|
|
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
|
-
}
|
|
2764
|
+
// The right-hand session rail is DERIVED from the chat (see syncSessionAssets),
|
|
2765
|
+
// so the old push-based item store is gone. These two shims stay because the
|
|
2766
|
+
// legacy job-polling paths still call them; the chat is the source of truth.
|
|
2767
|
+
function upsertInpaintHistoryItem() {}
|
|
2768
|
+
function recordSourceHistoryItem() {}
|
|
2686
2769
|
|
|
2687
2770
|
// Mirror each completed inpaint output into /temp/inpaints/<tracer>/ so a
|
|
2688
2771
|
// session's outputs are browsable as files (grouped by the same tracer that
|
|
@@ -2713,16 +2796,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2713
2796
|
const url = absoluteUrl(outputUrl);
|
|
2714
2797
|
const jobId = String(payload?.job_id || payload?.jobId || "");
|
|
2715
2798
|
resultImage.src = url;
|
|
2716
|
-
|
|
2717
|
-
|
|
2799
|
+
// The finished edit becomes the newest image in the chat — and therefore
|
|
2800
|
+
// the thing the next message edits and the surface you paint on.
|
|
2801
|
+
resolveActiveTurn({ kind: "media", mediaKind: "image", url, jobId });
|
|
2802
|
+
adoptLatestImage(url);
|
|
2718
2803
|
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
2804
|
return url;
|
|
2727
2805
|
}
|
|
2728
2806
|
|
|
@@ -2736,12 +2814,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2736
2814
|
try {
|
|
2737
2815
|
const parsed = new URL(value, window.location.href);
|
|
2738
2816
|
const storageKey = parsed.searchParams.get("key") || decodeURIComponent(parsed.pathname).replace(/^\/+/, "");
|
|
2739
|
-
return !/\/temporary\/inpaints\/
|
|
2817
|
+
return !/\/temporary\/inpaints\/_(?:inputs|context)(?:\/|$)/i.test(storageKey)
|
|
2740
2818
|
&& !/\/temporary\/inpaint(?:-|\/|$)/i.test(storageKey)
|
|
2741
2819
|
&& !/\/temporary\/inpaint-masks(?:\/|$)/i.test(storageKey)
|
|
2742
2820
|
&& !/inpaint-mask-[^/]+\.png/i.test(storageKey);
|
|
2743
2821
|
} catch {
|
|
2744
|
-
return !/\/temporary\/inpaints\/
|
|
2822
|
+
return !/\/temporary\/inpaints\/_(?:inputs|context)(?:\/|$)/i.test(value)
|
|
2745
2823
|
&& !/\/temporary\/inpaint(?:-|\/|$)/i.test(value)
|
|
2746
2824
|
&& !/\/temporary\/inpaint-masks(?:\/|$)/i.test(value)
|
|
2747
2825
|
&& !/inpaint-mask-[^/]+\.png/i.test(value);
|
|
@@ -2749,16 +2827,21 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2749
2827
|
});
|
|
2750
2828
|
}
|
|
2751
2829
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2830
|
+
// Which chat turn an api-history entry represents. These four routes ARE the
|
|
2831
|
+
// conversation — everything else in the customer's run history is ignored here.
|
|
2832
|
+
function chatEntryKind(entry) {
|
|
2754
2833
|
const path = String(entry?.path || "").toLowerCase();
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2834
|
+
const templateId = String(entry?.linkedJob?.template_id || entry?.linkedJob?.templateId || "").toLowerCase();
|
|
2835
|
+
if (path.includes("/primitives/images/inpaint") || templateId === "primitive:image_inpaint") return "inpaint";
|
|
2836
|
+
if (path.includes("/primitives/images/edit") || templateId === "primitive:image_edit") return "edit";
|
|
2837
|
+
if (path.includes("/primitives/images/generate") || templateId === "primitive:image_generate") return "generate";
|
|
2838
|
+
if (path.includes("/primitives/videos/generate") || templateId === "primitive:video_generate") return "video";
|
|
2839
|
+
return "";
|
|
2758
2840
|
}
|
|
2759
2841
|
|
|
2760
|
-
function
|
|
2761
|
-
|
|
2842
|
+
function historyEntryVideoUrls(entry) {
|
|
2843
|
+
const urls = Array.isArray(entry?.outputMediaUrls) ? entry.outputMediaUrls : [];
|
|
2844
|
+
return urls.filter((url) => /\.(mp4|mov|webm|m4v)(\?|#|$)/i.test(String(url || "")));
|
|
2762
2845
|
}
|
|
2763
2846
|
|
|
2764
2847
|
function setHistoryEmpty(message) {
|
|
@@ -2769,14 +2852,46 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2769
2852
|
historyList.append(empty);
|
|
2770
2853
|
}
|
|
2771
2854
|
|
|
2855
|
+
// ---- right rail: every asset this session produced, derived from the chat ----
|
|
2856
|
+
function syncSessionAssets() {
|
|
2857
|
+
const items = [];
|
|
2858
|
+
for (let i = chatTurns.length - 1; i >= 0; i--) {
|
|
2859
|
+
const turn = chatTurns[i];
|
|
2860
|
+
if (turn.role !== "assistant") continue;
|
|
2861
|
+
if (turn.kind === "media" && turn.url) {
|
|
2862
|
+
items.push({
|
|
2863
|
+
key: turn.id,
|
|
2864
|
+
kind: "output",
|
|
2865
|
+
mediaKind: turn.mediaKind === "video" ? "video" : "image",
|
|
2866
|
+
url: turn.url,
|
|
2867
|
+
jobId: turn.jobId || "",
|
|
2868
|
+
title: turn.prompt || turn.note || (turn.mediaKind === "video" ? "Video" : "Image"),
|
|
2869
|
+
createdAt: turn.at
|
|
2870
|
+
});
|
|
2871
|
+
} else if (turn.kind === "pending") {
|
|
2872
|
+
items.push({
|
|
2873
|
+
key: turn.id,
|
|
2874
|
+
kind: "pending",
|
|
2875
|
+
jobId: turn.jobId || "",
|
|
2876
|
+
title: turn.prompt || "Working…",
|
|
2877
|
+
status: turn.label || "",
|
|
2878
|
+
createdAt: turn.at
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
inpaintHistoryItems = items;
|
|
2883
|
+
renderInpaintHistory();
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2772
2886
|
function renderInpaintHistory() {
|
|
2773
2887
|
historyList.innerHTML = "";
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2888
|
+
if (!inpaintHistoryItems.length) {
|
|
2889
|
+
setHistoryEmpty(resolveTracer()
|
|
2890
|
+
? "Nothing in this session yet. Send a message to create your first image."
|
|
2891
|
+
: "New session — outputs land here as you chat.");
|
|
2777
2892
|
return;
|
|
2778
2893
|
}
|
|
2779
|
-
for (const item of
|
|
2894
|
+
for (const item of inpaintHistoryItems) {
|
|
2780
2895
|
const row = document.createElement("div");
|
|
2781
2896
|
row.className = "custom-inpaint-history-item";
|
|
2782
2897
|
row.dataset.historyKey = item.key;
|
|
@@ -2785,158 +2900,83 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2785
2900
|
if (item.kind === "pending") {
|
|
2786
2901
|
thumbButton = document.createElement("div");
|
|
2787
2902
|
thumbButton.className = "custom-inpaint-history-generating";
|
|
2788
|
-
thumbButton.setAttribute("aria-label", "
|
|
2903
|
+
thumbButton.setAttribute("aria-label", "Generating");
|
|
2789
2904
|
thumbButton.setAttribute("role", "img");
|
|
2790
2905
|
} else {
|
|
2791
2906
|
thumbButton = document.createElement("button");
|
|
2792
2907
|
thumbButton.type = "button";
|
|
2793
2908
|
thumbButton.className = "custom-inpaint-history-thumb-button";
|
|
2794
2909
|
thumbButton.dataset.historyPreview = item.key;
|
|
2795
|
-
thumbButton.setAttribute("aria-label", "
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2910
|
+
thumbButton.setAttribute("aria-label", "Jump to this message");
|
|
2911
|
+
if (item.mediaKind === "video") {
|
|
2912
|
+
const badge = document.createElement("span");
|
|
2913
|
+
badge.className = "custom-inpaint-history-thumb custom-inpaint-history-videothumb";
|
|
2914
|
+
badge.textContent = "▶";
|
|
2915
|
+
thumbButton.append(badge);
|
|
2916
|
+
} else {
|
|
2917
|
+
const thumb = document.createElement("img");
|
|
2918
|
+
thumb.className = "custom-inpaint-history-thumb";
|
|
2919
|
+
thumb.src = item.url;
|
|
2920
|
+
thumb.alt = "";
|
|
2921
|
+
thumb.loading = "lazy";
|
|
2922
|
+
thumbButton.append(thumb);
|
|
2923
|
+
}
|
|
2802
2924
|
}
|
|
2803
2925
|
const meta = document.createElement("div");
|
|
2804
2926
|
meta.className = "custom-inpaint-history-meta";
|
|
2805
2927
|
const title = document.createElement("div");
|
|
2806
2928
|
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
|
-
}
|
|
2929
|
+
title.textContent = item.title;
|
|
2930
|
+
title.title = item.title;
|
|
2931
|
+
const subtitle = document.createElement("div");
|
|
2932
|
+
subtitle.className = "custom-inpaint-history-subtitle";
|
|
2933
|
+
subtitle.textContent = item.kind === "pending"
|
|
2934
|
+
? (item.status || "Working…")
|
|
2935
|
+
: (item.mediaKind === "video" ? "Video" : "Image") + (item.createdAt ? " · " + relTimeShort(item.createdAt) : "");
|
|
2936
|
+
meta.append(title, subtitle);
|
|
2828
2937
|
const actions = document.createElement("div");
|
|
2829
2938
|
actions.className = "custom-inpaint-history-actions";
|
|
2830
2939
|
if (item.kind === "pending") {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2940
|
+
if (item.jobId) {
|
|
2941
|
+
const check = document.createElement("button");
|
|
2942
|
+
check.type = "button";
|
|
2943
|
+
check.className = "custom-inpaint-history-check";
|
|
2944
|
+
check.dataset.historyCheck = item.jobId;
|
|
2945
|
+
check.textContent = "Check Status";
|
|
2946
|
+
actions.append(check);
|
|
2947
|
+
}
|
|
2837
2948
|
} else {
|
|
2838
2949
|
const download = document.createElement("a");
|
|
2839
2950
|
download.className = "custom-inpaint-history-icon";
|
|
2840
2951
|
download.href = item.url;
|
|
2841
2952
|
download.target = "_blank";
|
|
2842
2953
|
download.rel = "noopener noreferrer";
|
|
2843
|
-
download.download = imageDownloadName("
|
|
2844
|
-
download.setAttribute("aria-label", "Download
|
|
2954
|
+
download.download = imageDownloadName(item.mediaKind === "video" ? "vidfarm-video" : "vidfarm-image", item.createdAt);
|
|
2955
|
+
download.setAttribute("aria-label", "Download");
|
|
2845
2956
|
download.setAttribute("title", "Download");
|
|
2846
2957
|
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
2958
|
const copy = document.createElement("button");
|
|
2848
2959
|
copy.type = "button";
|
|
2849
2960
|
copy.className = "custom-inpaint-history-icon";
|
|
2850
2961
|
copy.dataset.historyCopy = item.key;
|
|
2851
|
-
copy.setAttribute("aria-label", "Copy
|
|
2962
|
+
copy.setAttribute("aria-label", "Copy the file URL");
|
|
2852
2963
|
copy.setAttribute("title", "Copy URL");
|
|
2853
2964
|
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
2965
|
const save = document.createElement("button");
|
|
2855
2966
|
save.type = "button";
|
|
2856
2967
|
save.className = "custom-inpaint-history-icon";
|
|
2857
2968
|
save.dataset.historySave = item.key;
|
|
2858
|
-
save.setAttribute("aria-label", "Save
|
|
2859
|
-
save.setAttribute("title", "Save to Files");
|
|
2969
|
+
save.setAttribute("aria-label", "Save to My Files");
|
|
2970
|
+
save.setAttribute("title", "Save to My Files");
|
|
2860
2971
|
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
2972
|
actions.append(download, copy, save);
|
|
2862
2973
|
}
|
|
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
2974
|
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);
|
|
2975
|
+
row.append(thumbButton, meta);
|
|
2892
2976
|
historyList.append(row);
|
|
2893
2977
|
}
|
|
2894
2978
|
}
|
|
2895
2979
|
|
|
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
2980
|
async function copyText(value) {
|
|
2941
2981
|
if (navigator.clipboard && window.isSecureContext) {
|
|
2942
2982
|
try {
|
|
@@ -2960,36 +3000,121 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2960
3000
|
return copied;
|
|
2961
3001
|
}
|
|
2962
3002
|
|
|
2963
|
-
|
|
3003
|
+
// ---- session restore -----------------------------------------------------
|
|
3004
|
+
// A session IS its chat. Every turn was a POST to one of the four primitive
|
|
3005
|
+
// routes, and the api-call history stores each request body — including the
|
|
3006
|
+
// the chat_message field we attach purely so the transcript rebuilds verbatim
|
|
3007
|
+
// (the API strips unknown envelope fields, so it never reaches a provider).
|
|
3008
|
+
function userTextFromEntry(entry, kind) {
|
|
3009
|
+
const body = (entry && entry.request && entry.request.body) || {};
|
|
3010
|
+
if (typeof body.chat_message === "string" && body.chat_message.trim()) return body.chat_message.trim();
|
|
3011
|
+
// Fallback for turns made before chat_message existed: recover what we can
|
|
3012
|
+
// from the prompt/instruction we composed.
|
|
3013
|
+
const payload = body.payload || {};
|
|
3014
|
+
const raw = String(payload.prompt || payload.instruction || "");
|
|
3015
|
+
if (!raw) return kind === "video" ? "(video)" : "(edit)";
|
|
3016
|
+
const latest = raw.split("LATEST INSTRUCTION: ").pop();
|
|
3017
|
+
if (latest && latest !== raw) return latest.trim();
|
|
3018
|
+
const edited = raw.split("Edit the SOURCE IMAGE as follows: ")[1];
|
|
3019
|
+
if (edited) return edited.split(" Keep everything else about the source image")[0].trim();
|
|
3020
|
+
const whole = raw.split("Whole image prompt: ")[1];
|
|
3021
|
+
if (whole) return whole.split(". Apply this only as guidance")[0].trim();
|
|
3022
|
+
return raw.length > 240 ? raw.slice(0, 240) + "…" : raw;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
function turnsFromHistoryEntries(entries) {
|
|
3026
|
+
const rows = (Array.isArray(entries) ? entries : [])
|
|
3027
|
+
.map((entry) => ({ entry, kind: chatEntryKind(entry) }))
|
|
3028
|
+
.filter((row) => row.kind)
|
|
3029
|
+
.sort((a, b) => Date.parse(a.entry.createdAt || 0) - Date.parse(b.entry.createdAt || 0));
|
|
3030
|
+
const turns = [];
|
|
3031
|
+
const seenJobs = new Set();
|
|
3032
|
+
for (const row of rows) {
|
|
3033
|
+
const entry = row.entry;
|
|
3034
|
+
const jobId = String(entry.jobId || (entry.linkedJob && entry.linkedJob.job_id) || "");
|
|
3035
|
+
if (jobId && seenJobs.has(jobId)) continue;
|
|
3036
|
+
if (jobId) seenJobs.add(jobId);
|
|
3037
|
+
const body = (entry.request && entry.request.body) || {};
|
|
3038
|
+
const text = userTextFromEntry(entry, row.kind);
|
|
3039
|
+
const at = String(entry.createdAt || new Date().toISOString());
|
|
3040
|
+
turns.push({
|
|
3041
|
+
id: nextTurnId(), at, role: "user", kind: "text", text,
|
|
3042
|
+
mode: row.kind === "video" ? "video" : "image",
|
|
3043
|
+
masked: row.kind === "inpaint" || body.chat_masked === true,
|
|
3044
|
+
isEdit: row.kind === "edit" || row.kind === "inpaint"
|
|
3045
|
+
});
|
|
3046
|
+
const status = String((entry.linkedJob && entry.linkedJob.status) || "");
|
|
3047
|
+
const urls = row.kind === "video" ? historyEntryVideoUrls(entry) : historyEntryUrls(entry);
|
|
3048
|
+
if (urls.length) {
|
|
3049
|
+
let url = "";
|
|
3050
|
+
try { url = absoluteUrl(urls[0]); } catch { url = String(urls[0]); }
|
|
3051
|
+
turns.push({
|
|
3052
|
+
id: nextTurnId(), at, role: "assistant", kind: "media",
|
|
3053
|
+
mediaKind: row.kind === "video" ? "video" : "image",
|
|
3054
|
+
url, jobId, prompt: text
|
|
3055
|
+
});
|
|
3056
|
+
} else if (status === "failed" || status === "cancelled") {
|
|
3057
|
+
turns.push({ id: nextTurnId(), at, role: "assistant", kind: "error", text: "That job " + status + "." });
|
|
3058
|
+
} else if (isPendingJobStatus(status)) {
|
|
3059
|
+
turns.push({
|
|
3060
|
+
id: nextTurnId(), at, role: "assistant", kind: "pending", jobId, prompt: text,
|
|
3061
|
+
label: "Still running (" + status + ") — use Check Status."
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
return turns;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// Rebuild the whole conversation for the active tracer from the server.
|
|
3069
|
+
async function restoreSession(options) {
|
|
2964
3070
|
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
3071
|
if (!tracer) {
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
renderInpaintHistory();
|
|
3072
|
+
// Brand-new unnamed session: nothing on the server yet.
|
|
3073
|
+
renderChat();
|
|
2971
3074
|
return;
|
|
2972
3075
|
}
|
|
3076
|
+
// Never stomp on a turn that is mid-flight locally.
|
|
3077
|
+
if (genBusy || activeTurnId) { syncSessionAssets(); return; }
|
|
2973
3078
|
historyRefresh.disabled = true;
|
|
2974
3079
|
try {
|
|
2975
3080
|
const endpoint = new URL("/job-runs/history", window.location.origin);
|
|
2976
|
-
endpoint.searchParams.set("limit", "
|
|
2977
|
-
endpoint.searchParams.set("template_id", "primitive:image_inpaint");
|
|
3081
|
+
endpoint.searchParams.set("limit", "250");
|
|
2978
3082
|
endpoint.searchParams.set("tracer", tracer);
|
|
2979
3083
|
const response = await fetch(endpoint, { credentials: "same-origin" });
|
|
2980
3084
|
const payload = await parseJsonResponse(response);
|
|
2981
|
-
if (!response.ok || (payload && payload.error)) throw new Error(payload.error || "Unable to load
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
3085
|
+
if (!response.ok || (payload && payload.error)) throw new Error(payload.error || "Unable to load this session.");
|
|
3086
|
+
const turns = turnsFromHistoryEntries(payload.entries);
|
|
3087
|
+
// A refresh must never LOSE messages: run history can lag a job by a moment,
|
|
3088
|
+
// and a fresh upload-only session has nothing on the server at all. Only a
|
|
3089
|
+
// deliberate session switch (replaceEmpty) may shrink the transcript.
|
|
3090
|
+
if (turns.length < chatTurns.length && !(options && options.replaceEmpty)) {
|
|
3091
|
+
renderChat();
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
chatTurns = turns;
|
|
3095
|
+
activeTurnId = "";
|
|
3096
|
+
// The newest IMAGE in the restored conversation becomes the paint surface
|
|
3097
|
+
// and the source of the next edit.
|
|
3098
|
+
let latest = "";
|
|
3099
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
3100
|
+
const turn = turns[i];
|
|
3101
|
+
if (turn.kind === "media" && turn.mediaKind === "image" && turn.url) { latest = turn.url; break; }
|
|
3102
|
+
}
|
|
3103
|
+
if (latest) adoptLatestImage(latest);
|
|
3104
|
+
else renderChat();
|
|
2985
3105
|
} catch (error) {
|
|
2986
3106
|
console.error(error);
|
|
2987
|
-
|
|
3107
|
+
setStatus(error instanceof Error ? error.message : "Unable to load this session.", "error");
|
|
2988
3108
|
} finally {
|
|
2989
3109
|
historyRefresh.disabled = false;
|
|
2990
3110
|
}
|
|
2991
3111
|
}
|
|
2992
3112
|
|
|
3113
|
+
// Legacy call sites (job polling, submit) refresh the session through this name.
|
|
3114
|
+
async function loadInpaintHistory() {
|
|
3115
|
+
await restoreSession();
|
|
3116
|
+
}
|
|
3117
|
+
|
|
2993
3118
|
async function checkInpaintJobStatus(jobId, button) {
|
|
2994
3119
|
if (!jobId) return null;
|
|
2995
3120
|
if (button instanceof HTMLButtonElement) {
|
|
@@ -3035,6 +3160,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3035
3160
|
|
|
3036
3161
|
async function pollInpaintJob(jobId) {
|
|
3037
3162
|
for (let attempt = 0; attempt < 90; attempt += 1) {
|
|
3163
|
+
throwIfStopped();
|
|
3038
3164
|
const response = await fetch("/api/v1/primitives/image_inpaint/jobs/" + encodeURIComponent(jobId), {
|
|
3039
3165
|
credentials: "same-origin"
|
|
3040
3166
|
});
|
|
@@ -3051,6 +3177,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3051
3177
|
return outputUrl;
|
|
3052
3178
|
}
|
|
3053
3179
|
upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
|
|
3180
|
+
updatePendingLabel(activeTurnId, "Applying the masked edit (" + (payload.status || "running") + ")\u2026");
|
|
3054
3181
|
setStatus("Image edit job " + jobId + " is " + (payload.status || "running") + ".", "");
|
|
3055
3182
|
await new Promise((resolve) => window.setTimeout(resolve, attempt < 8 ? 1500 : 3000));
|
|
3056
3183
|
}
|
|
@@ -3059,8 +3186,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3059
3186
|
return null;
|
|
3060
3187
|
}
|
|
3061
3188
|
|
|
3062
|
-
async function submitInpaintJob() {
|
|
3063
|
-
|
|
3189
|
+
async function submitInpaintJob(options) {
|
|
3190
|
+
const extraRefs = (options && Array.isArray(options.extraRefs)) ? options.extraRefs : [];
|
|
3191
|
+
// The user's own words, stored alongside the request so the chat can be
|
|
3192
|
+
// rebuilt later without un-parsing the composed instruction.
|
|
3193
|
+
const chatMessage = (options && options.chatMessage) ? String(options.chatMessage) : inpaintGlobalPrompt.value.trim();
|
|
3194
|
+
if (!sourceImageUrl) {
|
|
3064
3195
|
setStatus("Upload a source image first.", "error");
|
|
3065
3196
|
return;
|
|
3066
3197
|
}
|
|
@@ -3075,11 +3206,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3075
3206
|
setStatus("Add a replacement prompt for " + missingPromptColor.color.toUpperCase() + ".", "error");
|
|
3076
3207
|
return;
|
|
3077
3208
|
}
|
|
3078
|
-
const
|
|
3079
|
-
if (!
|
|
3209
|
+
const builtInstruction = buildInpaintInstruction();
|
|
3210
|
+
if (!builtInstruction) {
|
|
3080
3211
|
setStatus("Add a replacement prompt for at least one color.", "error");
|
|
3081
3212
|
return;
|
|
3082
3213
|
}
|
|
3214
|
+
const instruction = builtInstruction;
|
|
3083
3215
|
setSubmitLoading(true);
|
|
3084
3216
|
uploadButton.disabled = true;
|
|
3085
3217
|
setStatus("Uploading masks...", "");
|
|
@@ -3095,11 +3227,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3095
3227
|
headers: { "content-type": "application/json" },
|
|
3096
3228
|
body: JSON.stringify({
|
|
3097
3229
|
tracer,
|
|
3230
|
+
chat_message: chatMessage,
|
|
3231
|
+
chat_mode: "image",
|
|
3232
|
+
chat_masked: true,
|
|
3098
3233
|
payload: {
|
|
3099
3234
|
source_image_url: sourceImageUrl,
|
|
3100
3235
|
mask_url: absoluteUrl(maskUrl),
|
|
3101
3236
|
instruction,
|
|
3102
|
-
reference_attachments: inpaintAttachments.map((attachment) => attachment.url),
|
|
3237
|
+
reference_attachments: inpaintAttachments.map((attachment) => attachment.url).concat(extraRefs),
|
|
3103
3238
|
...(inpaintProviderSelect.value ? { provider: inpaintProviderSelect.value } : {}),
|
|
3104
3239
|
size: "auto",
|
|
3105
3240
|
...(getInpaintAspectRatio() ? { aspect_ratio: getInpaintAspectRatio() } : {}),
|
|
@@ -3110,13 +3245,19 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3110
3245
|
const payload = await parseJsonResponse(response);
|
|
3111
3246
|
if (!response.ok) throw new Error(payload.error || "Unable to create image edit job.");
|
|
3112
3247
|
setStatus("Image edit job " + (payload.job_id || "") + " started.", "success");
|
|
3248
|
+
activeJob = { kind: "image_inpaint", jobId: String(payload.job_id || "") };
|
|
3113
3249
|
upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
|
|
3114
3250
|
await pollInpaintJob(payload.job_id);
|
|
3251
|
+
if (activeTurnId) {
|
|
3252
|
+
updatePendingLabel(activeTurnId, "Still running \u2014 use Check Status in this session\u2019s history.");
|
|
3253
|
+
activeTurnId = "";
|
|
3254
|
+
}
|
|
3115
3255
|
await loadInpaintHistory();
|
|
3116
3256
|
void loadTracerSessions();
|
|
3117
3257
|
} catch (error) {
|
|
3118
3258
|
console.error(error);
|
|
3119
3259
|
setStatus(error instanceof Error ? error.message : "Unable to create image edit.", "error");
|
|
3260
|
+
throw error;
|
|
3120
3261
|
} finally {
|
|
3121
3262
|
setSubmitLoading(false);
|
|
3122
3263
|
// Respect the post-generate lock — don't re-enable source upload if the
|
|
@@ -3133,25 +3274,23 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3133
3274
|
historyRefresh.addEventListener("click", () => {
|
|
3134
3275
|
void loadInpaintHistory();
|
|
3135
3276
|
});
|
|
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
3277
|
historyList.addEventListener("click", (event) => {
|
|
3143
3278
|
const target = event.target;
|
|
3144
3279
|
if (!(target instanceof Element)) return;
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
event.stopPropagation();
|
|
3148
|
-
}
|
|
3280
|
+
// The rail mirrors the chat, so its thumbnail scrolls the feed to that turn
|
|
3281
|
+
// instead of opening a second, competing viewer.
|
|
3149
3282
|
const previewButton = target.closest("[data-history-preview]");
|
|
3150
3283
|
if (previewButton instanceof HTMLElement) {
|
|
3151
3284
|
const key = previewButton.dataset.historyPreview || "";
|
|
3285
|
+
const row = chatFeed.querySelector('[data-turn-id="' + CSS.escape(key) + '"]');
|
|
3286
|
+
if (row instanceof HTMLElement) {
|
|
3287
|
+
row.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
3288
|
+
row.classList.add("is-flash");
|
|
3289
|
+
window.setTimeout(() => row.classList.remove("is-flash"), 1200);
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3152
3292
|
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));
|
|
3293
|
+
if (item && item.url) openImagePreview(item.url, item.title, imageDownloadName("vidfarm-output", item.createdAt), item.mediaKind);
|
|
3155
3294
|
return;
|
|
3156
3295
|
}
|
|
3157
3296
|
const copyButton = target.closest("[data-history-copy]");
|
|
@@ -3169,40 +3308,12 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3169
3308
|
if (saveButton instanceof HTMLElement) {
|
|
3170
3309
|
const key = saveButton.dataset.historySave || "";
|
|
3171
3310
|
const item = inpaintHistoryItems.find((entry) => entry.key === key);
|
|
3172
|
-
if (item && item.url) saveMediaToFiles(item.url, imageDownloadName("
|
|
3311
|
+
if (item && item.url) saveMediaToFiles(item.url, imageDownloadName("vidfarm-output", item.createdAt));
|
|
3173
3312
|
return;
|
|
3174
3313
|
}
|
|
3175
3314
|
const checkButton = target.closest("[data-history-check]");
|
|
3176
3315
|
if (checkButton instanceof HTMLButtonElement) {
|
|
3177
3316
|
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
3317
|
}
|
|
3207
3318
|
});
|
|
3208
3319
|
resultOpenButton.addEventListener("click", () => {
|
|
@@ -3272,18 +3383,22 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3272
3383
|
inpaintGlobalPrompt.addEventListener("paste", (event) => {
|
|
3273
3384
|
handleAttachmentPaste(event, "global");
|
|
3274
3385
|
});
|
|
3275
|
-
|
|
3386
|
+
function undoStroke() {
|
|
3276
3387
|
const stroke = inpaintStrokes.pop();
|
|
3277
3388
|
if (stroke) redoStrokes.push(stroke);
|
|
3278
3389
|
redrawInpaintMask();
|
|
3390
|
+
syncMaskUi();
|
|
3279
3391
|
setStatus("", "");
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3392
|
+
}
|
|
3393
|
+
function redoStroke() {
|
|
3282
3394
|
const stroke = redoStrokes.pop();
|
|
3283
3395
|
if (stroke) inpaintStrokes.push(stroke);
|
|
3284
3396
|
redrawInpaintMask();
|
|
3397
|
+
syncMaskUi();
|
|
3285
3398
|
setStatus("", "");
|
|
3286
|
-
}
|
|
3399
|
+
}
|
|
3400
|
+
inpaintUndo.addEventListener("click", undoStroke);
|
|
3401
|
+
inpaintRedo.addEventListener("click", redoStroke);
|
|
3287
3402
|
inpaintClear.addEventListener("click", () => {
|
|
3288
3403
|
resetMask();
|
|
3289
3404
|
setStatus("", "");
|
|
@@ -3349,7 +3464,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3349
3464
|
hideBrushCursor();
|
|
3350
3465
|
});
|
|
3351
3466
|
inpaintSubmit.addEventListener("click", () => {
|
|
3352
|
-
|
|
3467
|
+
submitInpaintJob().catch(() => {});
|
|
3353
3468
|
});
|
|
3354
3469
|
function openSettingsModal() {
|
|
3355
3470
|
settingsPopover.hidden = false;
|
|
@@ -3366,6 +3481,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3366
3481
|
if (settingsPopover.hidden) openSettingsModal(); else closeSettingsModal();
|
|
3367
3482
|
});
|
|
3368
3483
|
if (settingsClose) settingsClose.addEventListener("click", () => closeSettingsModal());
|
|
3484
|
+
|
|
3485
|
+
// Copy chat ID — the session tracer IS this chat's thread id (it's what
|
|
3486
|
+
// /job-runs/history groups on, what ?tracer= reopens, and what the past-
|
|
3487
|
+
// sessions rail keys off). A still-unnamed session mints its id here so
|
|
3488
|
+
// there is always something concrete to copy/share.
|
|
3489
|
+
const copyChatButton = document.querySelector("[data-custom-inpaint-copy-chat]");
|
|
3490
|
+
if (copyChatButton instanceof HTMLButtonElement) {
|
|
3491
|
+
copyChatButton.addEventListener("click", () => {
|
|
3492
|
+
const tracer = resolveTracer() || getTracerValue();
|
|
3493
|
+
void copyText(tracer).then((copied) => {
|
|
3494
|
+
setStatus(copied ? "Chat ID copied — " + tracer : "Chat ID: " + tracer, copied ? "success" : "");
|
|
3495
|
+
});
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3369
3498
|
settingsPopover.addEventListener("click", (event) => {
|
|
3370
3499
|
if (event.target === settingsPopover) closeSettingsModal();
|
|
3371
3500
|
});
|
|
@@ -3384,16 +3513,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3384
3513
|
if (sessionsBackdrop) sessionsBackdrop.hidden = true;
|
|
3385
3514
|
if (settingsPopover.hidden) document.body.style.overflow = "";
|
|
3386
3515
|
}
|
|
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
3516
|
function relTimeShort(value) {
|
|
3398
3517
|
const then = Date.parse(value || 0);
|
|
3399
3518
|
if (!then) return "";
|
|
@@ -3454,13 +3573,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3454
3573
|
meta.className = "rk-ip-session-meta";
|
|
3455
3574
|
const title = document.createElement("div");
|
|
3456
3575
|
title.className = "rk-ip-session-title";
|
|
3457
|
-
title.textContent = isImage ? (group.name || "Standalone image") : group.tracer;
|
|
3576
|
+
title.textContent = isImage ? (group.name || "Standalone image") : (group.title || group.tracer);
|
|
3577
|
+
title.title = isImage ? "" : (group.title ? group.title + " (" + group.tracer + ")" : group.tracer);
|
|
3458
3578
|
const sub = document.createElement("div");
|
|
3459
3579
|
sub.className = "rk-ip-session-sub";
|
|
3460
3580
|
const when = relTimeShort(group.createdAt);
|
|
3461
3581
|
sub.textContent = isImage
|
|
3462
|
-
? "Standalone image — tap to
|
|
3463
|
-
: (group.count ? group.count + "
|
|
3582
|
+
? "Standalone image — tap to start a chat from it" + (when ? " · " + when : "")
|
|
3583
|
+
: (group.count ? group.count + " message" + (group.count === 1 ? "" : "s") : "session") + (when ? " · " + when : "");
|
|
3464
3584
|
meta.append(title, sub);
|
|
3465
3585
|
row.append(meta);
|
|
3466
3586
|
sessionsList.append(row);
|
|
@@ -3477,16 +3597,17 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3477
3597
|
const payload = await parseJsonResponse(response);
|
|
3478
3598
|
if (!response.ok || (payload && payload.error)) throw new Error((payload && payload.error) || "Unable to load sessions.");
|
|
3479
3599
|
const groups = new Map();
|
|
3480
|
-
// Past image gens
|
|
3481
|
-
//
|
|
3482
|
-
// a standalone image the user can load fresh into a new inpaint session.
|
|
3600
|
+
// Past one-off image gens with NO session tracer can't be reopened as a
|
|
3601
|
+
// conversation, so each is offered as a standalone image to start one from.
|
|
3483
3602
|
const loose = [];
|
|
3484
3603
|
const looseSeen = new Set();
|
|
3485
3604
|
for (const entry of Array.isArray(payload.entries) ? payload.entries : []) {
|
|
3486
|
-
|
|
3487
|
-
|
|
3605
|
+
const kind = chatEntryKind(entry);
|
|
3606
|
+
if (!kind) continue;
|
|
3607
|
+
const urls = kind === "video" ? historyEntryVideoUrls(entry) : historyEntryUrls(entry);
|
|
3488
3608
|
const tracer = String(entry.tracer || "").trim();
|
|
3489
3609
|
if (!tracer) {
|
|
3610
|
+
if (kind === "video") continue;
|
|
3490
3611
|
for (const raw of urls) {
|
|
3491
3612
|
let url = "";
|
|
3492
3613
|
try { url = absoluteUrl(raw); } catch { continue; }
|
|
@@ -3497,9 +3618,16 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3497
3618
|
continue;
|
|
3498
3619
|
}
|
|
3499
3620
|
let group = groups.get(tracer);
|
|
3500
|
-
if (!group) { group = { kind: "session", tracer, thumb: "", count: 0, createdAt: entry.createdAt || "" }; groups.set(tracer, group); }
|
|
3501
|
-
|
|
3502
|
-
|
|
3621
|
+
if (!group) { group = { kind: "session", tracer, thumb: "", title: "", count: 0, createdAt: entry.createdAt || "", firstAt: entry.createdAt || "" }; groups.set(tracer, group); }
|
|
3622
|
+
group.count += 1;
|
|
3623
|
+
// Newest image in the session is its cover; oldest message is its name.
|
|
3624
|
+
if (kind !== "video" && urls[0] && Date.parse(entry.createdAt || 0) >= Date.parse(group.createdAt || 0)) {
|
|
3625
|
+
try { group.thumb = absoluteUrl(urls[0]); } catch {}
|
|
3626
|
+
}
|
|
3627
|
+
if (!group.title || Date.parse(entry.createdAt || 0) <= Date.parse(group.firstAt || 0)) {
|
|
3628
|
+
const text = userTextFromEntry(entry, kind);
|
|
3629
|
+
if (text) { group.title = text.length > 60 ? text.slice(0, 60) + "…" : text; group.firstAt = entry.createdAt || group.firstAt; }
|
|
3630
|
+
}
|
|
3503
3631
|
if (Date.parse(entry.createdAt || 0) > Date.parse(group.createdAt || 0)) group.createdAt = entry.createdAt;
|
|
3504
3632
|
}
|
|
3505
3633
|
const list = Array.from(groups.values()).concat(loose)
|
|
@@ -3512,7 +3640,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3512
3640
|
sessionsLoading = false;
|
|
3513
3641
|
}
|
|
3514
3642
|
}
|
|
3515
|
-
function switchToTracer(tracer
|
|
3643
|
+
async function switchToTracer(tracer) {
|
|
3516
3644
|
const value = String(tracer || "").trim();
|
|
3517
3645
|
if (!value) return;
|
|
3518
3646
|
clearInpaintUrlParams();
|
|
@@ -3521,16 +3649,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3521
3649
|
tracerInput.value = value;
|
|
3522
3650
|
tracerAutoPending = false;
|
|
3523
3651
|
closeSessionsPanel();
|
|
3524
|
-
|
|
3652
|
+
setStatus("Opening " + value + "…", "");
|
|
3653
|
+
// Rebuild the whole conversation — messages, edits, outputs — and put the
|
|
3654
|
+
// newest image back under the brush.
|
|
3655
|
+
await restoreSession({ replaceEmpty: true });
|
|
3525
3656
|
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
|
-
}
|
|
3657
|
+
setStatus(chatTurns.length
|
|
3658
|
+
? "Session restored — keep chatting to pick up where you left off."
|
|
3659
|
+
: "Opened " + value + " — no messages in it yet.", chatTurns.length ? "success" : "");
|
|
3534
3660
|
}
|
|
3535
3661
|
// A past image gen with no session tracer: start a brand-new session and drop
|
|
3536
3662
|
// that image onto the editor as the source so it can be edited immediately.
|
|
@@ -3545,7 +3671,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3545
3671
|
closeSessionsPanel();
|
|
3546
3672
|
loadSourceUrl(value, "history");
|
|
3547
3673
|
void loadTracerSessions();
|
|
3548
|
-
setStatus("New session started from that image —
|
|
3674
|
+
setStatus("New session started from that image — describe an edit, or paint a mask first.", "success");
|
|
3549
3675
|
}
|
|
3550
3676
|
if (sessionsToggle) sessionsToggle.addEventListener("click", () => {
|
|
3551
3677
|
if (sessionsPanel && sessionsPanel.classList.contains("is-open")) closeSessionsPanel(); else openSessionsPanel();
|
|
@@ -3562,19 +3688,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3562
3688
|
}
|
|
3563
3689
|
const row = target.closest("[data-session-tracer]");
|
|
3564
3690
|
if (row instanceof HTMLElement && row.dataset.sessionTracer) {
|
|
3565
|
-
|
|
3566
|
-
switchToTracer(row.dataset.sessionTracer, thumb instanceof HTMLImageElement ? thumb.src : "");
|
|
3691
|
+
void switchToTracer(row.dataset.sessionTracer);
|
|
3567
3692
|
}
|
|
3568
3693
|
});
|
|
3569
3694
|
|
|
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
3695
|
window.addEventListener("resize", () => syncInpaintCanvasSize());
|
|
3579
3696
|
window.addEventListener("keydown", (event) => {
|
|
3580
3697
|
if (event.key !== "Escape") return;
|
|
@@ -3583,30 +3700,93 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3583
3700
|
if (sessionsPanel && sessionsPanel.classList.contains("is-open")) { closeSessionsPanel(); return; }
|
|
3584
3701
|
});
|
|
3585
3702
|
|
|
3586
|
-
// =====
|
|
3703
|
+
// ===== chat composer + feed ================================================
|
|
3587
3704
|
// Layered on top of the legacy inpaint logic; reuses its in-scope helpers
|
|
3588
|
-
// (setStatus,
|
|
3589
|
-
// absoluteUrl, extractOutputUrl, inpaintProviderSelect).
|
|
3590
|
-
//
|
|
3705
|
+
// (setStatus, uploadTemporaryBlob, getTracerValue, parseJsonResponse,
|
|
3706
|
+
// absoluteUrl, extractOutputUrl, inpaintProviderSelect, submitInpaintJob).
|
|
3707
|
+
// Every finished image becomes the newest image, which is both the paint
|
|
3708
|
+
// surface and the source the NEXT message edits.
|
|
3591
3709
|
var genPrompt = document.getElementById('rk-ip-genprompt');
|
|
3592
3710
|
var genClip = document.getElementById('rk-ip-genclip');
|
|
3593
3711
|
var genSend = document.getElementById('rk-ip-gensend');
|
|
3594
3712
|
var genRefsEl = document.getElementById('rk-ip-genrefs');
|
|
3595
3713
|
var genStatusEl = document.getElementById('rk-ip-genstatus');
|
|
3596
|
-
var genWrap =
|
|
3714
|
+
var genWrap = composerEl;
|
|
3597
3715
|
var GEN_SEND_HTML = genSend ? genSend.innerHTML : '';
|
|
3598
3716
|
var genRefs = [];
|
|
3599
3717
|
var genBusy = false;
|
|
3600
3718
|
|
|
3601
|
-
|
|
3602
|
-
function
|
|
3719
|
+
// One status line for the whole surface — the composer's.
|
|
3720
|
+
function setGenStatus(m, t){ setStatus(m, t); }
|
|
3721
|
+
function maskHasInstruction(){
|
|
3722
|
+
return inpaintStrokes.length > 0 && inpaintMaskColors.some(function(entry){ return String(entry.prompt || '').trim(); });
|
|
3723
|
+
}
|
|
3724
|
+
function canGenNew(){ return !genBusy && (Boolean((genPrompt && genPrompt.value || '').trim()) || maskHasInstruction()); }
|
|
3725
|
+
// While a turn is in flight the composer stays fully usable — you can keep
|
|
3726
|
+
// typing the next message, attach references, and scroll the history. The send
|
|
3727
|
+
// button turns into Stop, which cancels the running job.
|
|
3603
3728
|
function syncGenSend(){
|
|
3604
|
-
if(genPrompt) genPrompt.disabled = genBusy;
|
|
3605
|
-
if(genClip) genClip.disabled = genBusy;
|
|
3606
3729
|
if(!genSend) return;
|
|
3730
|
+
if(genBusy){
|
|
3731
|
+
genSend.disabled = false;
|
|
3732
|
+
genSend.dataset.mode = 'stop';
|
|
3733
|
+
genSend.setAttribute('aria-label', 'Stop generating');
|
|
3734
|
+
genSend.setAttribute('title', 'Stop generating');
|
|
3735
|
+
genSend.innerHTML = "<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>";
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
genSend.dataset.mode = 'send';
|
|
3739
|
+
genSend.setAttribute('aria-label', 'Send');
|
|
3740
|
+
genSend.setAttribute('title', 'Send');
|
|
3607
3741
|
genSend.disabled = !canGenNew();
|
|
3608
|
-
genSend.
|
|
3609
|
-
|
|
3742
|
+
genSend.innerHTML = GEN_SEND_HTML;
|
|
3743
|
+
}
|
|
3744
|
+
function setGenBusy(value){
|
|
3745
|
+
genBusy = Boolean(value);
|
|
3746
|
+
composerEl.classList.toggle('is-busy', genBusy);
|
|
3747
|
+
syncGenSend();
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
// ---- stop --------------------------------------------------------------
|
|
3751
|
+
// Stop aborts the client poll AND best-effort cancels the job server-side so a
|
|
3752
|
+
// long video render stops burning provider time.
|
|
3753
|
+
var activeJob = { kind: '', jobId: '' };
|
|
3754
|
+
var stopRequested = false;
|
|
3755
|
+
function stopError(){ var e = new Error('__vidfarm_stopped__'); e.stopped = true; return e; }
|
|
3756
|
+
function throwIfStopped(){ if(stopRequested) throw stopError(); }
|
|
3757
|
+
function stopActiveTurn(){
|
|
3758
|
+
if(!genBusy || stopRequested) return;
|
|
3759
|
+
stopRequested = true;
|
|
3760
|
+
setStatus('Stopping…', '');
|
|
3761
|
+
if(activeTurnId) updatePendingLabel(activeTurnId, 'Stopping…');
|
|
3762
|
+
if(activeJob.kind && activeJob.jobId){
|
|
3763
|
+
fetch('/api/v1/primitives/' + activeJob.kind + '/jobs/' + encodeURIComponent(activeJob.jobId) + '/cancel', {
|
|
3764
|
+
method: 'POST', credentials: 'same-origin'
|
|
3765
|
+
}).catch(function(){});
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
function composerPlaceholder(){
|
|
3769
|
+
if(!genPrompt) return;
|
|
3770
|
+
genPrompt.placeholder = modeSelect.value === 'video'
|
|
3771
|
+
? (sourceImageUrl ? 'Describe the video \u2014 the newest image is used as its opening frame\u2026' : 'Describe the video you want\u2026')
|
|
3772
|
+
: (sourceImageUrl ? (inpaintStrokes.length ? 'Describe the masked edit (whole-image guidance)\u2026' : 'Describe the next edit to the newest image\u2026') : 'Describe the image you want\u2026');
|
|
3773
|
+
}
|
|
3774
|
+
// Grows with the message up to 10 rows, then scrolls inside itself.
|
|
3775
|
+
var COMPOSER_MAX_ROWS = 10;
|
|
3776
|
+
function composerMaxHeight(){
|
|
3777
|
+
if(!genPrompt) return 0;
|
|
3778
|
+
var styles = window.getComputedStyle(genPrompt);
|
|
3779
|
+
var line = parseFloat(styles.lineHeight) || 20;
|
|
3780
|
+
var pad = (parseFloat(styles.paddingTop) || 0) + (parseFloat(styles.paddingBottom) || 0)
|
|
3781
|
+
+ (parseFloat(styles.borderTopWidth) || 0) + (parseFloat(styles.borderBottomWidth) || 0);
|
|
3782
|
+
return Math.round(line * COMPOSER_MAX_ROWS + pad);
|
|
3783
|
+
}
|
|
3784
|
+
function autoGrowComposer(){
|
|
3785
|
+
if(!genPrompt) return;
|
|
3786
|
+
var max = composerMaxHeight();
|
|
3787
|
+
genPrompt.style.height = 'auto';
|
|
3788
|
+
genPrompt.style.height = Math.min(max, Math.max(56, genPrompt.scrollHeight)) + 'px';
|
|
3789
|
+
genPrompt.style.overflowY = genPrompt.scrollHeight > max ? 'auto' : 'hidden';
|
|
3610
3790
|
}
|
|
3611
3791
|
function renderGenRefs(){
|
|
3612
3792
|
if(!genRefsEl) return;
|
|
@@ -3710,28 +3890,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3710
3890
|
}
|
|
3711
3891
|
function dtHasFiles(e){ var t = (e.dataTransfer && e.dataTransfer.types) || []; return Array.prototype.indexOf.call(t, 'Files') !== -1; }
|
|
3712
3892
|
|
|
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();
|
|
3733
|
-
}
|
|
3734
|
-
|
|
3735
3893
|
// "character card" / "storyboard" shorthand — spell out the full layout
|
|
3736
3894
|
// so the image model produces a reference sheet / numbered shot board
|
|
3737
3895
|
// instead of a single picture. Snippets are shared with the AI chat +
|
|
@@ -3765,36 +3923,404 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3765
3923
|
btn.addEventListener('click', function(){ applyGenPreset(btn.getAttribute('data-preset') || 'character-card'); });
|
|
3766
3924
|
});
|
|
3767
3925
|
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3926
|
+
// ---- chat state ----------------------------------------------------------
|
|
3927
|
+
// chatTurns/activeTurnId/renderChat/addTurn/adoptLatestImage are declared with
|
|
3928
|
+
// var + function declarations so the earlier legacy helpers (loadSourceUrl,
|
|
3929
|
+
// applyCompletedInpaintJob, resetWorkspace) can call them safely.
|
|
3930
|
+
var turnSeq = 0;
|
|
3931
|
+
function nextTurnId(){ turnSeq += 1; return 't' + turnSeq; }
|
|
3932
|
+
function addTurn(turn){
|
|
3933
|
+
var item = Object.assign({ id: nextTurnId(), at: new Date().toISOString(), role: 'assistant' }, turn);
|
|
3934
|
+
chatTurns.push(item);
|
|
3935
|
+
renderChat();
|
|
3936
|
+
return item;
|
|
3937
|
+
}
|
|
3938
|
+
function updateTurn(id, patch){
|
|
3939
|
+
if(!id) return;
|
|
3940
|
+
chatTurns = chatTurns.map(function(t){ return t.id === id ? Object.assign({}, t, patch) : t; });
|
|
3941
|
+
renderChat();
|
|
3942
|
+
}
|
|
3943
|
+
// Pending labels tick every poll; patch the node in place so a full re-render
|
|
3944
|
+
// never restarts a video that is playing further up the feed.
|
|
3945
|
+
function updatePendingLabel(id, label){
|
|
3946
|
+
if(!id) return;
|
|
3947
|
+
chatTurns = chatTurns.map(function(t){ return t.id === id ? Object.assign({}, t, { label: label }) : t; });
|
|
3948
|
+
var node = chatFeed.querySelector('[data-pending-id="' + id + '"]');
|
|
3949
|
+
if(node) node.textContent = label;
|
|
3950
|
+
}
|
|
3951
|
+
function resolveActiveTurn(patch){
|
|
3952
|
+
if(activeTurnId){ updateTurn(activeTurnId, patch); activeTurnId = ''; return; }
|
|
3953
|
+
addTurn(Object.assign({ role: 'assistant' }, patch));
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
// Make the given url the newest image: the paint surface AND the source of the next edit.
|
|
3957
|
+
function adoptLatestImage(url, name){
|
|
3958
|
+
if(!url) return;
|
|
3959
|
+
sourceImageUrl = url;
|
|
3960
|
+
sourceImageName = name || sourceNameFromUrl(url);
|
|
3961
|
+
sourceName.textContent = sourceImageName;
|
|
3962
|
+
canvasWrap.hidden = false;
|
|
3963
|
+
outputPanel.hidden = true;
|
|
3964
|
+
inpaintImage.onload = function(){ syncInpaintCanvasSize(); resetMask(); };
|
|
3965
|
+
inpaintImage.src = url;
|
|
3966
|
+
resetMask();
|
|
3967
|
+
renderChat();
|
|
3968
|
+
composerPlaceholder();
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
// ---- feed rendering ------------------------------------------------------
|
|
3972
|
+
function mediaActionsRow(turn){
|
|
3973
|
+
var row = document.createElement('div');
|
|
3974
|
+
row.className = 'rk-ip-media-actions';
|
|
3975
|
+
var full = document.createElement('button');
|
|
3976
|
+
full.type = 'button'; full.className = 'rk-ip-act'; full.dataset.ipOpen = turn.id;
|
|
3977
|
+
full.setAttribute('aria-label', 'Open fullscreen'); full.title = 'Fullscreen';
|
|
3978
|
+
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>";
|
|
3979
|
+
var download = document.createElement('a');
|
|
3980
|
+
download.className = 'rk-ip-act'; download.href = turn.url; download.target = '_blank'; download.rel = 'noopener noreferrer';
|
|
3981
|
+
download.setAttribute('download', imageDownloadName(turn.mediaKind === 'video' ? 'vidfarm-video' : 'vidfarm-image', turn.at));
|
|
3982
|
+
download.setAttribute('aria-label', 'Download this file'); download.title = 'Download';
|
|
3983
|
+
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>";
|
|
3984
|
+
var link = document.createElement('a');
|
|
3985
|
+
link.className = 'rk-ip-act'; link.href = turn.url; link.target = '_blank'; link.rel = 'noopener noreferrer';
|
|
3986
|
+
link.setAttribute('aria-label', 'Open the direct file URL'); link.title = 'Open direct file URL';
|
|
3987
|
+
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>";
|
|
3988
|
+
var copy = document.createElement('button');
|
|
3989
|
+
copy.type = 'button'; copy.className = 'rk-ip-act'; copy.dataset.ipCopy = turn.id;
|
|
3990
|
+
copy.setAttribute('aria-label', 'Copy the file URL'); copy.title = 'Copy URL';
|
|
3991
|
+
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>";
|
|
3992
|
+
var save = document.createElement('button');
|
|
3993
|
+
save.type = 'button'; save.className = 'rk-ip-act'; save.dataset.ipSave = turn.id;
|
|
3994
|
+
save.setAttribute('aria-label', 'Save to My Files'); save.title = 'Save to My Files';
|
|
3995
|
+
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>";
|
|
3996
|
+
row.append(full, download, link, copy, save);
|
|
3997
|
+
return row;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
function renderChat(){
|
|
4001
|
+
// Detach the paint surface first — innerHTML rebuild would otherwise drop it.
|
|
4002
|
+
if(paintStack.parentElement !== canvasHolder) canvasHolder.appendChild(paintStack);
|
|
4003
|
+
var wasNearBottom = (chatFeed.scrollHeight - chatFeed.scrollTop - chatFeed.clientHeight) < 120;
|
|
4004
|
+
chatFeed.innerHTML = '';
|
|
4005
|
+
if(!chatTurns.length){
|
|
4006
|
+
emptyState.hidden = false;
|
|
4007
|
+
chatFeed.appendChild(emptyState);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
emptyState.hidden = true;
|
|
4011
|
+
var latestPaintableId = '';
|
|
4012
|
+
for(var i = chatTurns.length - 1; i >= 0; i--){
|
|
4013
|
+
var candidate = chatTurns[i];
|
|
4014
|
+
if(candidate.kind === 'media' && candidate.mediaKind !== 'video' && candidate.url && candidate.url === sourceImageUrl){ latestPaintableId = candidate.id; break; }
|
|
4015
|
+
}
|
|
4016
|
+
chatTurns.forEach(function(turn){
|
|
4017
|
+
var row = document.createElement('div');
|
|
4018
|
+
row.className = 'rk-ip-turn';
|
|
4019
|
+
row.dataset.role = turn.role;
|
|
4020
|
+
row.dataset.kind = turn.kind;
|
|
4021
|
+
row.dataset.turnId = turn.id;
|
|
4022
|
+
if(turn.role === 'user'){
|
|
4023
|
+
if(turn.refs && turn.refs.length){
|
|
4024
|
+
var refs = document.createElement('div');
|
|
4025
|
+
refs.className = 'rk-ip-turn-refs';
|
|
4026
|
+
turn.refs.forEach(function(url){ var im = document.createElement('img'); im.src = url; im.alt = ''; im.loading = 'lazy'; refs.appendChild(im); });
|
|
4027
|
+
row.appendChild(refs);
|
|
4028
|
+
}
|
|
4029
|
+
var tag = document.createElement('span');
|
|
4030
|
+
tag.className = 'rk-ip-turn-tag';
|
|
4031
|
+
tag.textContent = turn.mode === 'video' ? 'Video' : (turn.masked ? 'Masked edit' : (turn.isEdit ? 'Edit' : 'New image'));
|
|
4032
|
+
row.appendChild(tag);
|
|
4033
|
+
var bubble = document.createElement('div');
|
|
4034
|
+
bubble.className = 'rk-ip-bubble';
|
|
4035
|
+
bubble.textContent = turn.text || '';
|
|
4036
|
+
row.appendChild(bubble);
|
|
4037
|
+
} else if(turn.kind === 'pending'){
|
|
4038
|
+
var pending = document.createElement('div');
|
|
4039
|
+
pending.className = 'rk-ip-pending';
|
|
4040
|
+
pending.dataset.pendingId = turn.id;
|
|
4041
|
+
pending.textContent = turn.label || 'Working\u2026';
|
|
4042
|
+
row.appendChild(pending);
|
|
4043
|
+
} else if(turn.kind === 'error' || turn.kind === 'stopped'){
|
|
4044
|
+
var err = document.createElement('div');
|
|
4045
|
+
err.className = 'rk-ip-bubble';
|
|
4046
|
+
err.textContent = turn.text || 'Something went wrong.';
|
|
4047
|
+
row.appendChild(err);
|
|
4048
|
+
} else if(turn.kind === 'media' && turn.url){
|
|
4049
|
+
var media = document.createElement('div');
|
|
4050
|
+
media.className = 'rk-ip-media';
|
|
4051
|
+
var isLatest = turn.id === latestPaintableId;
|
|
4052
|
+
var frame = document.createElement('div');
|
|
4053
|
+
frame.className = 'rk-ip-media-frame' + (isLatest ? ' is-latest' : '');
|
|
4054
|
+
if(isLatest){
|
|
4055
|
+
canvasWrap.hidden = false;
|
|
4056
|
+
frame.appendChild(paintStack);
|
|
4057
|
+
} else if(turn.mediaKind === 'video'){
|
|
4058
|
+
var video = document.createElement('video');
|
|
4059
|
+
video.src = turn.url; video.controls = true; video.playsInline = true; video.preload = 'metadata';
|
|
4060
|
+
frame.appendChild(video);
|
|
4061
|
+
} else {
|
|
4062
|
+
var img = document.createElement('img');
|
|
4063
|
+
img.src = turn.url; img.alt = turn.note || 'Generated image'; img.loading = 'lazy';
|
|
4064
|
+
frame.appendChild(img);
|
|
4065
|
+
}
|
|
4066
|
+
media.appendChild(frame);
|
|
4067
|
+
var actions = mediaActionsRow(turn);
|
|
4068
|
+
if(isLatest){
|
|
4069
|
+
var note = document.createElement('span');
|
|
4070
|
+
note.className = 'rk-ip-act-note';
|
|
4071
|
+
note.textContent = inpaintStrokes.length ? 'Newest \u00b7 masked' : 'Newest \u00b7 paint to mask a region';
|
|
4072
|
+
actions.appendChild(note);
|
|
4073
|
+
} else if(turn.note){
|
|
4074
|
+
var tagNote = document.createElement('span');
|
|
4075
|
+
tagNote.className = 'rk-ip-act-note';
|
|
4076
|
+
tagNote.textContent = turn.note;
|
|
4077
|
+
actions.appendChild(tagNote);
|
|
4078
|
+
}
|
|
4079
|
+
media.appendChild(actions);
|
|
4080
|
+
row.appendChild(media);
|
|
4081
|
+
} else {
|
|
4082
|
+
var text = document.createElement('div');
|
|
4083
|
+
text.className = 'rk-ip-bubble';
|
|
4084
|
+
text.textContent = turn.text || '';
|
|
4085
|
+
row.appendChild(text);
|
|
4086
|
+
}
|
|
4087
|
+
chatFeed.appendChild(row);
|
|
4088
|
+
});
|
|
4089
|
+
if(wasNearBottom) chatFeed.scrollTop = chatFeed.scrollHeight;
|
|
4090
|
+
updateBrushCursorSize();
|
|
4091
|
+
syncSessionAssets();
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
function turnById(id){ return chatTurns.filter(function(t){ return t.id === id; })[0] || null; }
|
|
4095
|
+
|
|
4096
|
+
chatFeed.addEventListener('click', function(event){
|
|
4097
|
+
var target = event.target;
|
|
4098
|
+
if(!(target instanceof Element)) return;
|
|
4099
|
+
var open = target.closest('[data-ip-open]');
|
|
4100
|
+
if(open instanceof HTMLElement){
|
|
4101
|
+
var openTurn = turnById(open.dataset.ipOpen || '');
|
|
4102
|
+
if(openTurn) openImagePreview(openTurn.url, openTurn.mediaKind === 'video' ? 'Generated video' : 'Generated image', imageDownloadName(openTurn.mediaKind === 'video' ? 'vidfarm-video' : 'vidfarm-image', openTurn.at), openTurn.mediaKind);
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
var copy = target.closest('[data-ip-copy]');
|
|
4106
|
+
if(copy instanceof HTMLElement){
|
|
4107
|
+
var copyTurn = turnById(copy.dataset.ipCopy || '');
|
|
4108
|
+
if(!copyTurn) return;
|
|
4109
|
+
void copyText(copyTurn.url).then(function(copied){
|
|
4110
|
+
copy.title = copied ? 'Copied' : 'Copy failed';
|
|
4111
|
+
setStatus(copied ? 'File URL copied.' : 'Copy failed.', copied ? 'success' : 'error');
|
|
4112
|
+
window.setTimeout(function(){ copy.title = 'Copy URL'; }, 1200);
|
|
4113
|
+
});
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
var save = target.closest('[data-ip-save]');
|
|
4117
|
+
if(save instanceof HTMLElement){
|
|
4118
|
+
var saveTurn = turnById(save.dataset.ipSave || '');
|
|
4119
|
+
if(saveTurn) saveMediaToFiles(saveTurn.url, imageDownloadName(saveTurn.mediaKind === 'video' ? 'vidfarm-video' : 'vidfarm-image', saveTurn.at));
|
|
4120
|
+
}
|
|
4121
|
+
});
|
|
4122
|
+
|
|
4123
|
+
maskChip.addEventListener('click', function(event){
|
|
4124
|
+
var target = event.target;
|
|
4125
|
+
if(target instanceof Element && target.closest('[data-ip-clear-mask]')){
|
|
4126
|
+
resetMask();
|
|
4127
|
+
renderChat();
|
|
4128
|
+
composerPlaceholder();
|
|
4129
|
+
}
|
|
4130
|
+
});
|
|
4131
|
+
|
|
4132
|
+
// ---- what each request carries -------------------------------------------
|
|
4133
|
+
// DELIBERATELY no conversation context. Each turn sends ONLY the user's latest
|
|
4134
|
+
// message and (for edits) the current image. Feeding back a transcript plus
|
|
4135
|
+
// earlier versions of the same picture made the models blend instructions
|
|
4136
|
+
// across turns and drift back toward superseded versions. The newest image
|
|
4137
|
+
// already encodes every edit before it, so it is the only history worth
|
|
4138
|
+
// sending. References the user attaches by hand still go through untouched.
|
|
4139
|
+
// ---- job polling ---------------------------------------------------------
|
|
4140
|
+
function extractVideoOutputUrl(payload){
|
|
4141
|
+
var result = (payload && payload.result) || {};
|
|
4142
|
+
var out = result.output || {};
|
|
4143
|
+
return (out.video && out.video.file_url) || out.primary_file_url || (result.video && result.video.file_url) || result.primary_file_url
|
|
4144
|
+
|| (Array.isArray(out.files) ? out.files[0] : '') || (Array.isArray(result.files) ? result.files[0] : '') || '';
|
|
4145
|
+
}
|
|
4146
|
+
function pollPrimitiveJob(kind, jobId, label, isVideo){
|
|
4147
|
+
var attempt = 0;
|
|
4148
|
+
var maxAttempts = isVideo ? 150 : 90;
|
|
4149
|
+
function step(){
|
|
4150
|
+
throwIfStopped();
|
|
4151
|
+
return fetch('/api/v1/primitives/' + kind + '/jobs/' + encodeURIComponent(jobId), { credentials: 'same-origin' }).then(function(r){
|
|
4152
|
+
return parseJsonResponse(r).then(function(payload){
|
|
4153
|
+
if(!r.ok) throw new Error(payload.error || ('Unable to poll ' + label + '.'));
|
|
4154
|
+
if(payload.status === 'failed' || payload.status === 'cancelled'){ throw new Error((payload.error && payload.error.message) || (label + ' ' + payload.status + '.')); }
|
|
4155
|
+
var out = isVideo ? extractVideoOutputUrl(payload) : extractOutputUrl(payload);
|
|
4156
|
+
if(payload.status === 'succeeded' || payload.status === 'complete' || out){
|
|
4157
|
+
if(!out) throw new Error(label + ' finished without an output URL.');
|
|
4158
|
+
return absoluteUrl(out);
|
|
4159
|
+
}
|
|
4160
|
+
var note = label + ' (' + (payload.status || 'running') + ')\u2026';
|
|
4161
|
+
updatePendingLabel(activeTurnId, note);
|
|
4162
|
+
setStatus(note, '');
|
|
4163
|
+
attempt++;
|
|
4164
|
+
if(attempt >= maxAttempts){ return ''; }
|
|
4165
|
+
return new Promise(function(res){ setTimeout(res, attempt < 8 ? 1500 : (isVideo ? 4000 : 3000)); }).then(step);
|
|
4166
|
+
});
|
|
4167
|
+
});
|
|
4168
|
+
}
|
|
4169
|
+
return step();
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
// ---- the four kinds of turn ---------------------------------------------
|
|
4173
|
+
function runGenerateTurn(text, refs){
|
|
4174
|
+
return Promise.resolve().then(function(){
|
|
4175
|
+
var pending = addTurn({ kind: 'pending', label: 'Generating image\u2026', prompt: text });
|
|
4176
|
+
activeTurnId = pending.id;
|
|
4177
|
+
var payload = { prompt: text, output_format: 'png' };
|
|
4178
|
+
if(refs.length) payload.prompt_attachments = refs;
|
|
4179
|
+
if(inpaintProviderSelect.value) payload.provider = inpaintProviderSelect.value;
|
|
4180
|
+
var tracer = tracerAutoPending ? nameTracerFromSeed(text) : getTracerValue();
|
|
4181
|
+
return fetch('/api/v1/primitives/images/generate', {
|
|
4182
|
+
method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' },
|
|
4183
|
+
body: JSON.stringify({ tracer: tracer, payload: payload, chat_message: text, chat_mode: 'image' })
|
|
4184
|
+
}).then(parseJsonResponse).then(function(j){
|
|
4185
|
+
if(!j || !j.job_id) throw new Error((j && j.error) || 'Unable to start image generation.');
|
|
4186
|
+
activeJob = { kind: 'image_generate', jobId: j.job_id };
|
|
4187
|
+
return pollPrimitiveJob('image_generate', j.job_id, 'Generating image', false).then(function(out){
|
|
4188
|
+
if(!out){ updatePendingLabel(activeTurnId, 'Still generating \u2014 it will appear in this session\u2019s history when it lands.'); activeTurnId = ''; return; }
|
|
4189
|
+
resolveActiveTurn({ kind: 'media', mediaKind: 'image', url: out, jobId: j.job_id });
|
|
4190
|
+
adoptLatestImage(out);
|
|
4191
|
+
recordSourceHistoryItem(out, 'generated', 'Generated image');
|
|
4192
|
+
setStatus('Image ready \u2014 keep chatting to refine it.', 'success');
|
|
4193
|
+
void loadTracerSessions();
|
|
4194
|
+
});
|
|
4195
|
+
});
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
function runEditTurn(text, refs){
|
|
4200
|
+
return Promise.resolve().then(function(){
|
|
4201
|
+
var pending = addTurn({ kind: 'pending', label: 'Editing the newest image\u2026', prompt: text });
|
|
4202
|
+
activeTurnId = pending.id;
|
|
4203
|
+
var instruction = 'Edit the SOURCE IMAGE as follows: ' + text
|
|
4204
|
+
+ ' Keep everything else about the source image \u2014 subject identity, composition, framing, style, and unrelated details \u2014 unchanged unless the instruction asks otherwise.';
|
|
4205
|
+
var payload = { source_image_url: sourceImageUrl, instruction: instruction, output_format: 'png' };
|
|
4206
|
+
if(refs.length) payload.reference_attachments = refs;
|
|
4207
|
+
if(inpaintProviderSelect.value) payload.provider = inpaintProviderSelect.value;
|
|
4208
|
+
var tracer = tracerAutoPending ? nameTracerFromSeed(text) : getTracerValue();
|
|
4209
|
+
return fetch('/api/v1/primitives/images/edit', {
|
|
4210
|
+
method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' },
|
|
4211
|
+
body: JSON.stringify({ tracer: tracer, payload: payload, chat_message: text, chat_mode: 'image' })
|
|
4212
|
+
}).then(parseJsonResponse).then(function(j){
|
|
4213
|
+
if(!j || !j.job_id) throw new Error((j && j.error) || 'Unable to start the edit.');
|
|
4214
|
+
activeJob = { kind: 'image_edit', jobId: j.job_id };
|
|
4215
|
+
return pollPrimitiveJob('image_edit', j.job_id, 'Editing image', false).then(function(out){
|
|
4216
|
+
if(!out){ updatePendingLabel(activeTurnId, 'Still editing \u2014 it will appear in this session\u2019s history when it lands.'); activeTurnId = ''; return; }
|
|
4217
|
+
resolveActiveTurn({ kind: 'media', mediaKind: 'image', url: out, jobId: j.job_id });
|
|
4218
|
+
adoptLatestImage(out);
|
|
4219
|
+
void persistInpaintOutputToTemp(out, j.job_id);
|
|
4220
|
+
setStatus('Edit applied \u2014 the newest image is now the one you are editing.', 'success');
|
|
4221
|
+
void loadTracerSessions();
|
|
4222
|
+
});
|
|
4223
|
+
});
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
function runMaskedTurn(text, refs){
|
|
4228
|
+
return Promise.resolve().then(function(){
|
|
4229
|
+
// The composer text is the legacy "whole image prompt"; per-color prompts
|
|
4230
|
+
// stay in the left panel and are merged by buildInpaintInstruction().
|
|
4231
|
+
inpaintGlobalPrompt.value = text;
|
|
4232
|
+
var pending = addTurn({ kind: 'pending', label: 'Applying the masked edit\u2026', prompt: text });
|
|
4233
|
+
activeTurnId = pending.id;
|
|
4234
|
+
return submitInpaintJob({ extraRefs: refs, chatMessage: text });
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
function runVideoTurn(text){
|
|
4239
|
+
return Promise.resolve().then(function(){
|
|
4240
|
+
var pending = addTurn({ kind: 'pending', label: 'Generating video \u2014 this can take a few minutes\u2026', prompt: text });
|
|
4241
|
+
activeTurnId = pending.id;
|
|
4242
|
+
var payload = { prompt: text, output_format: 'mp4' };
|
|
4243
|
+
// The newest image is the only visual reference — it is the opening frame.
|
|
4244
|
+
if(sourceImageUrl) payload.input_references = [sourceImageUrl];
|
|
4245
|
+
if(inpaintProviderSelect.value) payload.provider = inpaintProviderSelect.value;
|
|
4246
|
+
var tracer = tracerAutoPending ? nameTracerFromSeed(text) : getTracerValue();
|
|
4247
|
+
return fetch('/api/v1/primitives/videos/generate', {
|
|
4248
|
+
method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' },
|
|
4249
|
+
body: JSON.stringify({ tracer: tracer, payload: payload, chat_message: text, chat_mode: 'video' })
|
|
4250
|
+
}).then(parseJsonResponse).then(function(j){
|
|
4251
|
+
if(!j || !j.job_id) throw new Error((j && j.error) || 'Unable to start video generation.');
|
|
4252
|
+
activeJob = { kind: 'video_generate', jobId: j.job_id };
|
|
4253
|
+
return pollPrimitiveJob('video_generate', j.job_id, 'Generating video', true).then(function(out){
|
|
4254
|
+
if(!out){ updatePendingLabel(activeTurnId, 'Still rendering \u2014 check the Video tool\u2019s history shortly.'); activeTurnId = ''; return; }
|
|
4255
|
+
resolveActiveTurn({ kind: 'media', mediaKind: 'video', url: out, jobId: j.job_id, note: 'video' });
|
|
4256
|
+
setStatus('Video ready.', 'success');
|
|
4257
|
+
void loadTracerSessions();
|
|
4258
|
+
});
|
|
4259
|
+
});
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
// ---- send ----------------------------------------------------------------
|
|
4264
|
+
function sendChatMessage(){
|
|
3771
4265
|
if(genBusy) return;
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
var
|
|
3775
|
-
|
|
3776
|
-
if(
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
})
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
4266
|
+
var mode = modeSelect.value === 'video' ? 'video' : 'image';
|
|
4267
|
+
var raw = (genPrompt && genPrompt.value || '').trim();
|
|
4268
|
+
var text = mode === 'image' ? expandShorthand(raw) : raw;
|
|
4269
|
+
var masked = mode === 'image' && inpaintStrokes.length > 0;
|
|
4270
|
+
if(!text && !(masked && maskHasInstruction())){
|
|
4271
|
+
setStatus(mode === 'video' ? 'Describe the video you want.' : 'Type what you want to create or change.', 'error');
|
|
4272
|
+
if(genPrompt) genPrompt.focus();
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
if(mode === 'video' && !text){ setStatus('Describe the video you want.', 'error'); return; }
|
|
4276
|
+
var refs = genRefs.slice();
|
|
4277
|
+
addTurn({
|
|
4278
|
+
role: 'user', kind: 'text',
|
|
4279
|
+
text: text || 'Apply the painted mask instructions.',
|
|
4280
|
+
refs: refs, mode: mode, masked: masked,
|
|
4281
|
+
isEdit: mode === 'image' && Boolean(sourceImageUrl)
|
|
4282
|
+
});
|
|
4283
|
+
if(genPrompt){ genPrompt.value = ''; autoGrowComposer(); }
|
|
4284
|
+
genRefs = [];
|
|
4285
|
+
renderGenRefs();
|
|
4286
|
+
stopRequested = false;
|
|
4287
|
+
activeJob = { kind: '', jobId: '' };
|
|
4288
|
+
setGenBusy(true);
|
|
4289
|
+
setStatus(mode === 'video' ? 'Creating video\u2026' : 'Working\u2026', '');
|
|
4290
|
+
var work;
|
|
4291
|
+
if(mode === 'video') work = runVideoTurn(text);
|
|
4292
|
+
else if(!sourceImageUrl) work = runGenerateTurn(text, refs);
|
|
4293
|
+
else if(masked) work = runMaskedTurn(text, refs);
|
|
4294
|
+
else work = runEditTurn(text, refs);
|
|
4295
|
+
work.catch(function(err){
|
|
4296
|
+
if(err && (err.stopped || err.message === '__vidfarm_stopped__')){
|
|
4297
|
+
resolveActiveTurn({ kind: 'stopped', text: 'Stopped.' });
|
|
4298
|
+
setStatus('Stopped.', '');
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
var msg = (err && err.message) ? err.message : 'That request failed.';
|
|
4302
|
+
if(/401|403|sign in|unauth/i.test(msg)) msg = 'Sign in to generate media.';
|
|
4303
|
+
resolveActiveTurn({ kind: 'error', text: msg });
|
|
4304
|
+
setStatus(msg, 'error');
|
|
4305
|
+
}).then(function(){
|
|
4306
|
+
stopRequested = false;
|
|
4307
|
+
activeJob = { kind: '', jobId: '' };
|
|
4308
|
+
setGenBusy(false);
|
|
4309
|
+
composerPlaceholder();
|
|
4310
|
+
syncGenSend();
|
|
4311
|
+
void loadInpaintHistory();
|
|
4312
|
+
});
|
|
3793
4313
|
}
|
|
3794
4314
|
|
|
3795
4315
|
if(genPrompt){
|
|
3796
|
-
genPrompt.addEventListener('input', syncGenSend);
|
|
3797
|
-
genPrompt.addEventListener('keydown', function(e){
|
|
4316
|
+
genPrompt.addEventListener('input', function(){ syncGenSend(); autoGrowComposer(); });
|
|
4317
|
+
genPrompt.addEventListener('keydown', function(e){
|
|
4318
|
+
if(e.key !== 'Enter' || e.shiftKey) return;
|
|
4319
|
+
e.preventDefault();
|
|
4320
|
+
// Keep typing freely while a turn runs; Enter just won't fire a second one.
|
|
4321
|
+
if(genBusy){ setStatus('Still generating — press Stop to cancel, or wait and send then.', ''); return; }
|
|
4322
|
+
sendChatMessage();
|
|
4323
|
+
});
|
|
3798
4324
|
genPrompt.addEventListener('paste', function(e){ var imgs = imagesFromClipboard(e); if(imgs.length){ e.preventDefault(); addGenReferenceFiles(imgs); } });
|
|
3799
4325
|
}
|
|
3800
4326
|
if(genWrap){
|
|
@@ -3803,9 +4329,21 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3803
4329
|
genWrap.addEventListener('drop', function(e){ if(dtHasFiles(e)){ e.preventDefault(); e.stopPropagation(); genWrap.classList.remove('is-dragref'); addGenReferenceFiles(e.dataTransfer.files); } });
|
|
3804
4330
|
}
|
|
3805
4331
|
if(genClip) genClip.addEventListener('click', openGenFilePanel);
|
|
3806
|
-
if(genSend) genSend.addEventListener('click',
|
|
4332
|
+
if(genSend) genSend.addEventListener('click', function(){ if(genBusy) stopActiveTurn(); else sendChatMessage(); });
|
|
4333
|
+
modeSelect.addEventListener('change', function(){ composerPlaceholder(); syncGenSend(); });
|
|
4334
|
+
// ⌘Z / ⇧⌘Z anywhere outside a text field undoes and redoes the paint.
|
|
4335
|
+
window.addEventListener('keydown', function(e){
|
|
4336
|
+
if(!(e.metaKey || e.ctrlKey) || String(e.key).toLowerCase() !== 'z') return;
|
|
4337
|
+
var el = document.activeElement;
|
|
4338
|
+
if(el && (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT')) return;
|
|
4339
|
+
if(!inpaintStrokes.length && !redoStrokes.length) return;
|
|
4340
|
+
e.preventDefault();
|
|
4341
|
+
if(e.shiftKey) redoStroke(); else undoStroke();
|
|
4342
|
+
});
|
|
3807
4343
|
renderGenRefs();
|
|
3808
4344
|
syncGenSend();
|
|
4345
|
+
composerPlaceholder();
|
|
4346
|
+
renderChat();
|
|
3809
4347
|
|
|
3810
4348
|
// Drag-and-drop onto the upload dropzone (legacy only wired click).
|
|
3811
4349
|
var dropZone = document.querySelector('[data-custom-inpaint-upload]');
|
|
@@ -4365,6 +4903,34 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4365
4903
|
}
|
|
4366
4904
|
function fileIcon(kind) { return kind === 'pdf' ? '📕' : kind === 'json' ? '{ }' : kind === 'markdown' ? '📝' : kind === 'audio' ? '🎵' : kind === 'text' ? '📄' : '📄'; }
|
|
4367
4905
|
|
|
4906
|
+
// ── "edit this media" hand-off: dock chat → the matching creative tool ──
|
|
4907
|
+
// Images open /tools/image, videos /tools/video, each with the file preloaded as
|
|
4908
|
+
// that tool's working file (its chat then edits THAT media). Deliberately a fresh
|
|
4909
|
+
// tool session — no ?tracer — so the tool names its own chat from the first prompt.
|
|
4910
|
+
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>';
|
|
4911
|
+
function toolEditUrl(file) {
|
|
4912
|
+
if (!file || !file.viewUrl) return '';
|
|
4913
|
+
var k = fileKind(file.name, file.contentType);
|
|
4914
|
+
var name = file.name || '';
|
|
4915
|
+
if (k === 'image') return '/tools/image?source_image_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_image_name=' + encodeURIComponent(name) : '');
|
|
4916
|
+
if (k === 'video') return '/tools/video?source_video_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_video_name=' + encodeURIComponent(name) : '');
|
|
4917
|
+
return '';
|
|
4918
|
+
}
|
|
4919
|
+
function toolEditLink(file, className) {
|
|
4920
|
+
var href = toolEditUrl(file);
|
|
4921
|
+
if (!href) return null;
|
|
4922
|
+
var a = document.createElement('a');
|
|
4923
|
+
a.className = className;
|
|
4924
|
+
a.href = href;
|
|
4925
|
+
a.target = '_blank';
|
|
4926
|
+
a.rel = 'noopener noreferrer';
|
|
4927
|
+
a.title = 'Edit in the ' + (fileKind(file.name, file.contentType) === 'video' ? 'video' : 'image') + ' tool';
|
|
4928
|
+
a.setAttribute('aria-label', a.title);
|
|
4929
|
+
// The card itself opens the lightbox; the pencil must not also trigger it.
|
|
4930
|
+
a.addEventListener('click', function (e) { e.stopPropagation(); });
|
|
4931
|
+
return a;
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4368
4934
|
// Compact markdown → HTML for .md previews (headings, lists, quotes, rules,
|
|
4369
4935
|
// fenced + inline code, bold/italic/links). Escapes first, then injects tags
|
|
4370
4936
|
// (same order as linkify). Backticks are built via char codes to avoid
|
|
@@ -4425,7 +4991,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4425
4991
|
var dlBtn = document.createElement('a'); dlBtn.className = 'rk-aichat-preview-act'; dlBtn.target = '_blank'; dlBtn.rel = 'noopener noreferrer'; dlBtn.textContent = 'Download';
|
|
4426
4992
|
var linkBtn = document.createElement('button'); linkBtn.type = 'button'; linkBtn.className = 'rk-aichat-preview-act'; linkBtn.textContent = 'Copy link';
|
|
4427
4993
|
var xBtn = document.createElement('button'); xBtn.type = 'button'; xBtn.className = 'rk-aichat-modal-x'; xBtn.setAttribute('aria-label', 'Close'); xBtn.innerHTML = '×';
|
|
4428
|
-
|
|
4994
|
+
// Same hand-off as the card pencil, spelled out as a labelled action here.
|
|
4995
|
+
var editBtn = document.createElement('a'); editBtn.className = 'rk-aichat-preview-act'; editBtn.target = '_blank'; editBtn.rel = 'noopener noreferrer'; editBtn.textContent = 'Edit';
|
|
4996
|
+
head.appendChild(nameEl); head.appendChild(editBtn); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
|
|
4429
4997
|
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
4998
|
// Copy = file text (json/md/txt) or its URL for binaries. Copy link = the
|
|
4431
4999
|
// durable public S3 URL. Download opens in a new tab when the browser can't
|
|
@@ -4474,6 +5042,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4474
5042
|
var it = items[idx];
|
|
4475
5043
|
nameEl.textContent = it.name || 'file'; nameEl.title = it.name || '';
|
|
4476
5044
|
dlBtn.href = it.viewUrl; dlBtn.setAttribute('download', it.name || '');
|
|
5045
|
+
var editHref = toolEditUrl(it);
|
|
5046
|
+
editBtn.hidden = !editHref;
|
|
5047
|
+
if (editHref) { editBtn.href = editHref; editBtn.title = 'Edit in the ' + (fileKind(it.name, it.contentType) === 'video' ? 'video' : 'image') + ' tool'; }
|
|
4477
5048
|
linkBtn.style.display = it.publicUrl ? '' : 'none';
|
|
4478
5049
|
body.innerHTML = '';
|
|
4479
5050
|
var kind = fileKind(it.name, it.contentType);
|
|
@@ -4528,7 +5099,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4528
5099
|
var files = [], seen = {};
|
|
4529
5100
|
var arts = (job && job.artifacts) || [];
|
|
4530
5101
|
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
|
-
|
|
5102
|
+
// Name the direct result URL after the FILE, not the tracer — fileKind() reads the
|
|
5103
|
+
// extension, and a tracer-named entry ("my-poster") classifies as 'other', losing
|
|
5104
|
+
// the image/video thumbnail and the edit-in-tool hand-off.
|
|
5105
|
+
var mu = rkResultMediaUrl(job);
|
|
5106
|
+
if (mu && !seen[mu]) {
|
|
5107
|
+
seen[mu] = true;
|
|
5108
|
+
var seg = String(mu).split('?')[0].split('/').pop() || '';
|
|
5109
|
+
files.push({ name: (/.[a-z0-9]{2,5}$/i.test(seg) ? seg : (job.tracer || 'output')), contentType: '', viewUrl: mu, publicUrl: mu });
|
|
5110
|
+
}
|
|
4532
5111
|
var res = job && job.result;
|
|
4533
5112
|
if (res && typeof res === 'object' && !Array.isArray(res) && Object.keys(res).length) {
|
|
4534
5113
|
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 +5160,21 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
4581
5160
|
var grid = document.createElement('div'); grid.className = 'rk-aichat-files-grid';
|
|
4582
5161
|
list.forEach(function (f, ix) {
|
|
4583
5162
|
var k = fileKind(f.name, f.contentType);
|
|
4584
|
-
|
|
5163
|
+
// A div, not a button: media cards carry a nested "edit in tool" link, and
|
|
5164
|
+
// an anchor inside a button is invalid (and unclickable in some browsers).
|
|
5165
|
+
var card = document.createElement('div'); card.className = 'rk-aichat-file'; card.tabIndex = 0; card.setAttribute('role', 'button');
|
|
4585
5166
|
var thumb = document.createElement('div'); thumb.className = 'rk-aichat-file-thumb rk-aichat-file-thumb--' + k;
|
|
4586
5167
|
if (k === 'image') { var im = document.createElement('img'); im.src = f.viewUrl; im.alt = ''; im.loading = 'lazy'; thumb.appendChild(im); }
|
|
4587
5168
|
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
5169
|
else { var ic = document.createElement('span'); ic.className = 'rk-aichat-file-ic'; ic.textContent = fileIcon(k); thumb.appendChild(ic); }
|
|
5170
|
+
var edit = toolEditLink(f, 'rk-aichat-file-open');
|
|
5171
|
+
if (edit) { edit.innerHTML = PENCIL_SVG; thumb.appendChild(edit); }
|
|
4589
5172
|
var nm = document.createElement('div'); nm.className = 'rk-aichat-file-name'; nm.textContent = f.name || 'file'; nm.title = f.name || '';
|
|
4590
5173
|
card.appendChild(thumb); card.appendChild(nm);
|
|
4591
5174
|
card.addEventListener('click', function () { openFilePreview(list, ix); });
|
|
5175
|
+
card.addEventListener('keydown', function (e) {
|
|
5176
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFilePreview(list, ix); }
|
|
5177
|
+
});
|
|
4592
5178
|
grid.appendChild(card);
|
|
4593
5179
|
});
|
|
4594
5180
|
wrap.appendChild(grid); turn.appendChild(wrap); logBottom();
|