@officexapp/vidfarm-devcli 0.21.27 → 0.21.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1185,7 +1185,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1185
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}
1186
1186
  .rk-inpaint-workbench.is-locked .rk-inpaint-generate svg{display:none}
1187
1187
 
1188
- /* center: chat feed + composer */
1188
+
1189
+ /* ---- shared chat surface (tools/image + tools/video) ---------------------- */
1189
1190
  .rk-ip-chat{display:grid;grid-template-rows:minmax(0,1fr) auto;height:min(76vh,860px);min-height:520px;overflow:hidden}
1190
1191
  .rk-ip-feed{min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:18px 18px 6px;display:flex;flex-direction:column;gap:14px}
1191
1192
  .rk-ip-canvasholder{display:none}
@@ -1211,43 +1212,24 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1211
1212
  .rk-ip-turn.is-flash .rk-ip-bubble,.rk-ip-turn.is-flash .rk-ip-media-frame{box-shadow:0 0 0 3px var(--rk-gold-600)}
1212
1213
  .rk-ip-turn[data-kind="stopped"] .rk-ip-bubble{background:var(--rk-n-100);border-color:var(--rk-border-strong);color:var(--rk-text-muted);font-weight:700}
1213
1214
  .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}
1215
- .rk-inpaint-empty[hidden]{display:none}
1216
- .rk-inpaint-empty-ic{color:var(--rk-n-400)}
1217
- .rk-inpaint-empty-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink)}
1218
- .rk-inpaint-empty-sub{font-size:13px}
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);
1220
- --inpaint-brush-cursor-size:70px;--inpaint-brush-cursor-x:0px;--inpaint-brush-cursor-y:0px}
1221
- .editor-inpaint-canvas-wrap[hidden]{display:none}
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}
1235
- .editor-inpaint-canvas-wrap canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.86;cursor:none;touch-action:none}
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);
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;
1238
- transform:translate(var(--inpaint-brush-cursor-x),var(--inpaint-brush-cursor-y)) translate(-50%,-50%)}
1239
- .editor-inpaint-brush-cursor[data-visible="true"]{opacity:1}
1240
1215
 
1241
- /* chat composer */
1216
+ /* ---- composer ------------------------------------------------------------ */
1242
1217
  .rk-ip-composer{flex:none;margin:0;padding:12px 18px 16px;border-top:1px solid var(--rk-border);background:var(--rk-surface);display:grid;gap:8px;
1243
1218
  border-bottom-left-radius:var(--rk-r-3xl);border-bottom-right-radius:var(--rk-r-3xl)}
1244
1219
  .rk-ip-composer.is-dragref{background:var(--rk-gold-tint)}
1245
1220
  .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)}
1221
+ /* The grip is anchored to this wrapper, not the textarea, so it never scrolls
1222
+ away with the textarea's own overflow once the message passes 10 rows. */
1223
+ .rk-ip-comp-field{position:relative;display:block}
1224
+ .rk-ip-comp-input{display:block;width:100%;border:1.5px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);background:#fff;outline:0;resize:none;font:inherit;font-size:14px;line-height:1.45;
1225
+ color:var(--rk-ink);min-height:56px;overflow-y:hidden;padding:10px 12px;transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
1248
1226
  .rk-ip-comp-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
1249
1227
  .rk-ip-comp-input::placeholder{color:var(--rk-text-faint)}
1250
1228
  .rk-ip-comp-input:disabled{cursor:not-allowed;color:var(--rk-text-muted);background:var(--rk-n-50)}
1229
+ .rk-ip-comp-grip{position:absolute;right:5px;bottom:5px;width:16px;height:16px;display:grid;place-items:center;color:var(--rk-n-400);cursor:ns-resize;touch-action:none;
1230
+ border-radius:4px;opacity:.75;transition:color var(--rk-dur) var(--rk-ease),opacity var(--rk-dur) var(--rk-ease)}
1231
+ .rk-ip-comp-grip:hover,.rk-ip-comp-field.is-resizing .rk-ip-comp-grip{color:var(--rk-gold-700);opacity:1}
1232
+ .rk-ip-comp-field.is-resizing{user-select:none}
1251
1233
  .rk-ip-comp-bar{display:flex;align-items:center;gap:8px}
1252
1234
  .rk-ip-comp-modeselect{appearance:none;-webkit-appearance:none;border:1px solid var(--rk-border-strong);border-radius:999px;background:var(--rk-n-50);color:var(--rk-ink);
1253
1235
  font:inherit;font-size:12px;font-weight:800;padding:7px 26px 7px 12px;cursor:pointer;outline:0;
@@ -1261,10 +1243,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1261
1243
  font-size:11.5px;font-weight:700;color:var(--rk-gold-700);justify-self:start;max-width:100%}
1262
1244
  .rk-ip-comp-mask[hidden]{display:none}
1263
1245
  .rk-ip-comp-mask button{border:0;background:transparent;color:inherit;font:inherit;font-size:11.5px;font-weight:800;text-decoration:underline;cursor:pointer;padding:0}
1264
- .rk-ip-gen-clip:disabled{opacity:.5;cursor:not-allowed}
1265
- .rk-ip-gen-presets{display:flex;flex-wrap:wrap;gap:6px}
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)}
1267
- .rk-ip-preset:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
1246
+
1247
+ /* ---- attachment chips ---------------------------------------------------- */
1268
1248
  .rk-ip-gen-refs{display:flex;flex-wrap:wrap;gap:6px}
1269
1249
  .rk-ip-gen-refs[hidden]{display:none}
1270
1250
  .rk-ip-gen-ref{display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-50);font-size:11.5px;color:var(--rk-n-700);max-width:180px}
@@ -1272,25 +1252,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1272
1252
  .rk-ip-gen-ref span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1273
1253
  .rk-ip-gen-ref button{flex:none;border:0;background:transparent;cursor:pointer;color:var(--rk-text-faint);font-size:15px;line-height:1;padding:0 2px}
1274
1254
  .rk-ip-gen-ref button:hover{color:var(--rk-ink)}
1275
- .rk-ip-gen-bar{display:flex;align-items:center;gap:8px}
1276
- .rk-ip-gen-hint{flex:1;font-size:11.5px;color:var(--rk-text-faint);line-height:1.35}
1255
+ .rk-ip-gen-ref.is-pending{opacity:.6}
1277
1256
  .rk-ip-gen-clip,.rk-ip-gen-send{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),opacity var(--rk-dur) var(--rk-ease)}
1278
1257
  .rk-ip-gen-clip{border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
1279
1258
  .rk-ip-gen-clip:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
1259
+ .rk-ip-gen-clip:disabled{opacity:.5;cursor:not-allowed}
1280
1260
  .rk-ip-gen-send{border:0;background:var(--rk-gold-500,var(--rk-gold-600));color:var(--rk-ink)}
1281
1261
  .rk-ip-gen-send:hover:not(:disabled){background:var(--rk-gold-600)}
1282
1262
  .rk-ip-gen-send:disabled{opacity:.5;cursor:default}
1283
1263
  .rk-ip-gen-send[data-mode="stop"]{background:var(--rk-ink);color:#fff}
1284
- .rk-ip-gen-send[data-mode="stop"]:hover{background:#000}
1285
- .rk-ip-gen-send.is-busy{background:var(--rk-gold-tint);pointer-events:none}
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}
1287
1264
  .rk-ip-gen-status{font-size:12px;font-weight:600;min-height:16px;color:var(--rk-text-muted);text-align:left;padding-left:2px}
1265
+ .rk-ip-gen-status[hidden]{display:none}
1288
1266
  .rk-ip-gen-status[data-tone="error"]{color:#b42318}
1289
1267
  .rk-ip-gen-status[data-tone="success"]{color:#1f7a44}
1268
+ .rk-ip-gen-presets{display:flex;flex-wrap:wrap;gap:6px}
1269
+ .rk-ip-preset{flex:none;border:1px dashed var(--rk-border-strong,var(--rk-border));background:var(--rk-n-50);color:var(--rk-n-700);border-radius:999px;padding:4px 10px;font-size:11.5px;font-weight:600;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
1270
+ .rk-ip-preset:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
1290
1271
 
1291
- /* file-directory pop panel (reference picker) */
1272
+ /* ---- attach pop panel (shared file explorer) ----------------------------- */
1292
1273
  .rk-ip-filemodal{position:fixed;inset:0;z-index:60;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px;animation:rk-ip-fade .16s var(--rk-ease)}
1293
- @keyframes rk-ip-fade{from{opacity:0}to{opacity:1}}
1294
1274
  .rk-ip-filemodal-panel{width:min(560px,100%);max-height:min(80vh,680px);display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
1295
1275
  .rk-ip-filemodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
1296
1276
  .rk-ip-filemodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
@@ -1301,6 +1281,37 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1301
1281
  .rk-ip-filemodal-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted);cursor:pointer;padding:0 4px}
1302
1282
  .rk-ip-filemodal-close:hover{color:var(--rk-ink)}
1303
1283
  .rk-ip-filemodal-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}
1284
+ @keyframes rk-ip-spin{to{transform:rotate(360deg)}}
1285
+ @keyframes rk-ip-fade{from{opacity:0}to{opacity:1}}
1286
+ @media(max-width:820px){.rk-ip-chat{height:auto;min-height:0}.rk-ip-feed{max-height:62vh}}
1287
+
1288
+ .rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:34px 20px;margin:auto 0}
1289
+ .rk-inpaint-empty[hidden]{display:none}
1290
+ .rk-inpaint-empty-ic{color:var(--rk-n-400)}
1291
+ .rk-inpaint-empty-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink)}
1292
+ .rk-inpaint-empty-sub{font-size:13px}
1293
+ .editor-inpaint-canvas-wrap{position:relative;display:inline-block;line-height:0;max-width:100%;border-radius:var(--rk-r-2xl);overflow:hidden;box-shadow:var(--rk-shadow-md);
1294
+ --inpaint-brush-cursor-size:70px;--inpaint-brush-cursor-x:0px;--inpaint-brush-cursor-y:0px}
1295
+ .editor-inpaint-canvas-wrap[hidden]{display:none}
1296
+ .editor-inpaint-canvas-wrap img{display:block;max-width:100%;max-height:min(52vh,520px);width:auto;height:auto;object-fit:contain;user-select:none}
1297
+ /* paint toolbar — sits directly under the image it paints, never over it */
1298
+ /* align-items:center keeps the canvas wrap at the image's intrinsic width — stretching
1299
+ it would stretch the absolutely-positioned mask canvas and skew stroke coordinates. */
1300
+ .rk-ip-paintstack{display:inline-flex;flex-direction:column;align-items:center;max-width:100%}
1301
+ .rk-ip-painttools{width:100%;box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:6px;padding:6px 8px;border-top:1px solid var(--rk-border);background:var(--rk-n-50);line-height:1;flex-wrap:wrap}
1302
+ .rk-ip-painttools-brush{display:flex;align-items:center;gap:6px}
1303
+ .rk-ip-painttools-label{font-size:11px;font-weight:800;color:var(--rk-text-muted);white-space:nowrap}
1304
+ .rk-ip-painttools .rk-inpaint-range{width:86px}
1305
+ .rk-ip-painttool{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-700);cursor:pointer}
1306
+ .rk-ip-painttool:hover:not(:disabled){background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
1307
+ .rk-ip-painttool:disabled{opacity:.45;cursor:default}
1308
+ .rk-ip-painttool svg{width:13px;height:13px}
1309
+ .editor-inpaint-canvas-wrap canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.86;cursor:none;touch-action:none}
1310
+ .editor-inpaint-brush-cursor{position:absolute;z-index:2;left:0;top:0;width:var(--inpaint-brush-cursor-size);height:var(--inpaint-brush-cursor-size);
1311
+ border:2px solid rgba(255,255,255,.95);border-radius:50%;box-shadow:0 0 0 1px rgba(28,25,23,.7),0 0 18px rgba(28,25,23,.25);opacity:0;pointer-events:none;
1312
+ transform:translate(var(--inpaint-brush-cursor-x),var(--inpaint-brush-cursor-y)) translate(-50%,-50%)}
1313
+ .editor-inpaint-brush-cursor[data-visible="true"]{opacity:1}
1314
+
1304
1315
 
1305
1316
  /* right: result + history */
1306
1317
  .rk-inpaint-output{padding:18px;display:grid;gap:18px;align-content:start}
@@ -1457,7 +1468,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1457
1468
  <div class="rk-inpaint-head-text">
1458
1469
  <div class="rk-ip-tabs" role="tablist" aria-label="Editor mode"><a class="rk-ip-tab is-active" href="/tools/image" role="tab" aria-selected="true"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.5"/><path d="m4 17 4.5-4.5 3 3L16 11l4 4"/></svg><span>Image</span></a><a class="rk-ip-tab" href="/tools/video" role="tab" aria-selected="false"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="14" height="14" rx="2"/><path d="m17 9 4-2v10l-4-2"/></svg><span>Video</span></a><a class="rk-ip-tab" href="/tools/clipper" role="tab" aria-selected="false"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M8.1 8.1 20 18M8.1 15.9 20 6"/></svg><span>Clipper</span></a></div>
1459
1470
  <h1 class="rk-h1">Create Image</h1>
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>
1471
+ <p class="rk-sub">Chat to create an image, then keep chatting to refine it — every message edits the newest image, and only that image, so instructions stay sharp. Paint a mask on it to change only that region. To animate an image, take it to the Video tool.</p>
1461
1472
  </div>
1462
1473
  <div class="rk-inpaint-toolbar">
1463
1474
  <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-custom-inpaint-start-new><svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4v12"/><path d="M4 10h12"/></svg> Start New</button>
@@ -1516,7 +1527,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1516
1527
  <div class="rk-inpaint-empty" data-custom-inpaint-empty>
1517
1528
  <span class="rk-inpaint-empty-ic" aria-hidden="true"><svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l1.9 5.1 5.1 1.9-5.1 1.9L12 16.5l-1.9-5.1L5 9.5l5.1-1.9z"/></svg></span>
1518
1529
  <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>
1530
+ <p class="rk-inpaint-empty-sub">Then keep chatting to refine it — every message edits the newest image. Paint a mask on it first to edit just that region.</p>
1520
1531
  <div class="rk-ip-gen-presets" id="rk-ip-genpresets">
1521
1532
  <button type="button" class="rk-ip-preset" data-preset="character-card" title="Fill the prompt with a character reference sheet (multi-angle, expressions, poses)">+ Character card</button>
1522
1533
  <button type="button" class="rk-ip-preset" data-preset="storyboard" title="Fill the prompt with a numbered storyboard (sequential shot panels)">+ Storyboard</button>
@@ -1548,16 +1559,14 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1548
1559
  <div class="rk-ip-composer" data-ip-composer>
1549
1560
  <div class="rk-ip-comp-mask" data-ip-comp-mask hidden></div>
1550
1561
  <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>
1562
+ <div class="rk-ip-comp-field">
1563
+ <textarea id="rk-ip-genprompt" class="rk-ip-comp-input" rows="2" placeholder="Describe the image you want…"></textarea>
1564
+ <span class="rk-ip-comp-grip" data-ip-grip role="separator" aria-label="Drag to resize the message box" title="Drag to resize"><svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M11 4 4 11"/><path d="M11 8.5 8.5 11"/></svg></span>
1565
+ </div>
1552
1566
  <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>
1567
+
1559
1568
  <button type="button" class="rk-ip-gen-clip" id="rk-ip-genclip" aria-label="Attach reference images from your files" title="Attach reference images"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"/><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"/></svg></button>
1560
- <p class="custom-inpaint-status rk-ip-comp-status" data-custom-inpaint-status data-tone="" aria-live="polite"></p>
1569
+ <p class="rk-ip-comp-status custom-inpaint-status" data-ip-status data-custom-inpaint-status data-tone="" aria-live="polite"></p>
1561
1570
  <button type="button" class="rk-ip-gen-send" id="rk-ip-gensend" aria-label="Send"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg></button>
1562
1571
  </div>
1563
1572
  <p class="rk-ip-gen-status" id="rk-ip-genstatus" data-tone="" aria-live="polite" hidden></p>
@@ -1718,6 +1727,282 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1718
1727
  </aside>
1719
1728
  </div>
1720
1729
  <script>
1730
+
1731
+ (function(){
1732
+ var IMAGE_RE = /\.(png|jpe?g|gif|webp|avif)$/i;
1733
+ var SEND_ICON = "<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5\"/><path d=\"m5 12 7-7 7 7\"/></svg>";
1734
+ var STOP_ICON = "<svg width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"5\" y=\"5\" width=\"10\" height=\"10\" rx=\"2\"/></svg>";
1735
+ var MAX_ROWS = 10;
1736
+
1737
+ function isImageFile(f){ return /^image\//i.test((f && f.type) || '') || IMAGE_RE.test((f && f.name) || ''); }
1738
+ function imagesFromClipboard(e){
1739
+ var items = (e.clipboardData && e.clipboardData.items) || [], out = [];
1740
+ for(var i = 0; i < items.length; i++){
1741
+ if(items[i].kind === 'file'){ var f = items[i].getAsFile(); if(f && /^image\//i.test(f.type || '')) out.push(f); }
1742
+ }
1743
+ return out;
1744
+ }
1745
+ function dtHasFiles(e){
1746
+ var t = (e.dataTransfer && e.dataTransfer.types) || [];
1747
+ return Array.prototype.indexOf.call(t, 'Files') !== -1;
1748
+ }
1749
+ function plural(n, word){ return n + ' ' + word + (n > 1 ? 's' : ''); }
1750
+
1751
+ window.__rkChatComposer = function(cfg){
1752
+ cfg = cfg || {};
1753
+ var root = cfg.root, ta = cfg.textarea, refsEl = cfg.refsEl;
1754
+ var sendBtn = cfg.sendBtn, clipBtn = cfg.clipBtn;
1755
+ var uploadBlob = cfg.uploadBlob || function(){ return Promise.reject(new Error('Uploads are unavailable.')); };
1756
+ var status = cfg.onStatus || function(){};
1757
+ var canSendExtra = cfg.canSend || function(){ return false; };
1758
+ var refs = [];
1759
+ var busy = false;
1760
+ // Once the user drags the grip, their height wins: auto-grow stops fighting
1761
+ // them. Sending clears it so the next message starts compact again.
1762
+ var manualHeight = 0;
1763
+
1764
+ // ---- height ------------------------------------------------------------
1765
+ function maxHeight(){
1766
+ if(!ta) return 0;
1767
+ var s = window.getComputedStyle(ta);
1768
+ var line = parseFloat(s.lineHeight) || 20;
1769
+ var pad = (parseFloat(s.paddingTop) || 0) + (parseFloat(s.paddingBottom) || 0)
1770
+ + (parseFloat(s.borderTopWidth) || 0) + (parseFloat(s.borderBottomWidth) || 0);
1771
+ return Math.round(line * MAX_ROWS + pad);
1772
+ }
1773
+ function applyHeight(px, cap){
1774
+ ta.style.height = px + 'px';
1775
+ ta.style.overflowY = ta.scrollHeight > px + 1 ? 'auto' : (cap ? 'auto' : 'hidden');
1776
+ }
1777
+ function autoGrow(){
1778
+ if(!ta) return;
1779
+ if(manualHeight){ applyHeight(manualHeight, true); return; }
1780
+ var max = maxHeight();
1781
+ ta.style.height = 'auto';
1782
+ applyHeight(Math.min(max, Math.max(56, ta.scrollHeight)), ta.scrollHeight > max);
1783
+ }
1784
+ function resetHeight(){ manualHeight = 0; if(ta){ ta.style.height = 'auto'; } autoGrow(); }
1785
+
1786
+ // Corner grip: pointer-drag the textarea taller/shorter. Bounded so it can
1787
+ // never collapse under one row or grow past the viewport.
1788
+ var field = ta && ta.parentElement;
1789
+ var grip = root && root.querySelector('[data-ip-grip]');
1790
+ if(grip && ta){
1791
+ grip.addEventListener('pointerdown', function(e){
1792
+ e.preventDefault();
1793
+ var startY = e.clientY, startH = ta.getBoundingClientRect().height;
1794
+ var maxPx = Math.max(120, Math.round(window.innerHeight * 0.6));
1795
+ try{ grip.setPointerCapture(e.pointerId); }catch(_){}
1796
+ if(field) field.classList.add('is-resizing');
1797
+ function move(ev){
1798
+ var next = Math.min(maxPx, Math.max(56, Math.round(startH + (ev.clientY - startY))));
1799
+ manualHeight = next;
1800
+ applyHeight(next, true);
1801
+ }
1802
+ function up(ev){
1803
+ try{ grip.releasePointerCapture(e.pointerId); }catch(_){}
1804
+ if(field) field.classList.remove('is-resizing');
1805
+ grip.removeEventListener('pointermove', move);
1806
+ grip.removeEventListener('pointerup', up);
1807
+ grip.removeEventListener('pointercancel', up);
1808
+ // A click with no travel resets to auto-grow — an escape hatch out of
1809
+ // a height you dragged and no longer want.
1810
+ if(Math.abs(ev.clientY - startY) < 3) resetHeight();
1811
+ }
1812
+ grip.addEventListener('pointermove', move);
1813
+ grip.addEventListener('pointerup', up);
1814
+ grip.addEventListener('pointercancel', up);
1815
+ });
1816
+ grip.addEventListener('dblclick', function(){ resetHeight(); });
1817
+ }
1818
+
1819
+ // ---- send / stop button ------------------------------------------------
1820
+ function syncSend(){
1821
+ if(!sendBtn) return;
1822
+ if(busy){
1823
+ sendBtn.disabled = false;
1824
+ sendBtn.dataset.mode = 'stop';
1825
+ sendBtn.setAttribute('aria-label', 'Stop generating');
1826
+ sendBtn.setAttribute('title', 'Stop generating');
1827
+ sendBtn.innerHTML = STOP_ICON;
1828
+ return;
1829
+ }
1830
+ sendBtn.dataset.mode = 'send';
1831
+ sendBtn.setAttribute('aria-label', 'Send');
1832
+ sendBtn.setAttribute('title', 'Send');
1833
+ sendBtn.disabled = !(Boolean((ta && ta.value || '').trim()) || canSendExtra());
1834
+ sendBtn.innerHTML = SEND_ICON;
1835
+ }
1836
+ function setBusy(v){
1837
+ busy = Boolean(v);
1838
+ if(root) root.classList.toggle('is-busy', busy);
1839
+ syncSend();
1840
+ }
1841
+
1842
+ // ---- reference chips ---------------------------------------------------
1843
+ function renderRefs(){
1844
+ if(!refsEl) return;
1845
+ refsEl.hidden = refs.length === 0;
1846
+ refsEl.innerHTML = '';
1847
+ refs.forEach(function(url, idx){
1848
+ var chip = document.createElement('span'); chip.className = 'rk-ip-gen-ref';
1849
+ var im = document.createElement('img'); im.src = url; im.alt = ''; im.loading = 'lazy'; chip.appendChild(im);
1850
+ var lbl = document.createElement('span');
1851
+ var nm = url.split('/').pop() || 'reference';
1852
+ lbl.textContent = decodeURIComponent(nm).slice(0, 28);
1853
+ chip.appendChild(lbl);
1854
+ var x = document.createElement('button'); x.type = 'button';
1855
+ x.setAttribute('aria-label', 'Remove reference'); x.innerHTML = '&times;';
1856
+ x.addEventListener('click', function(){ refs.splice(idx, 1); renderRefs(); });
1857
+ chip.appendChild(x);
1858
+ refsEl.appendChild(chip);
1859
+ });
1860
+ }
1861
+
1862
+ // Paste or drop image files straight into the composer -> temp upload -> ref.
1863
+ function addFiles(fileList){
1864
+ var files = Array.prototype.slice.call(fileList || []).filter(isImageFile);
1865
+ if(!files.length) return Promise.resolve(0);
1866
+ status('Uploading ' + plural(files.length, 'reference') + '\u2026', '');
1867
+ var added = 0;
1868
+ return files.reduce(function(chain, f){
1869
+ return chain.then(function(){
1870
+ return uploadBlob(f, f.name || 'pasted-image.png', f.type || 'image/png').then(function(url){
1871
+ if(url && refs.indexOf(url) === -1){ refs.push(url); added++; renderRefs(); }
1872
+ });
1873
+ });
1874
+ }, Promise.resolve()).then(function(){
1875
+ if(added) status(plural(added, 'reference') + ' attached.', 'success');
1876
+ return added;
1877
+ }).catch(function(err){
1878
+ status((err && err.message) ? err.message : 'Upload failed.', 'error');
1879
+ return added;
1880
+ });
1881
+ }
1882
+
1883
+ // ---- attach pop panel (shared file-directory explorer) -----------------
1884
+ var modal = null;
1885
+ function onEsc(e){ if(e.key === 'Escape') closePanel(); }
1886
+ function closePanel(){
1887
+ if(!modal) return;
1888
+ try{ modal._handle && modal._handle.destroy && modal._handle.destroy(); }catch(_){}
1889
+ modal.remove(); modal = null;
1890
+ document.removeEventListener('keydown', onEsc, true);
1891
+ }
1892
+ function openPanel(){
1893
+ if(modal) return;
1894
+ var overlay = document.createElement('div'); overlay.className = 'rk-ip-filemodal';
1895
+ var panel = document.createElement('div'); panel.className = 'rk-ip-filemodal-panel';
1896
+ var head = document.createElement('div'); head.className = 'rk-ip-filemodal-head';
1897
+ var title = document.createElement('span'); title.className = 'rk-ip-filemodal-title';
1898
+ title.textContent = cfg.attachTitle || 'Attach reference images';
1899
+ var actions = document.createElement('div'); actions.className = 'rk-ip-filemodal-actions';
1900
+ var uploadBtn = document.createElement('button'); uploadBtn.type = 'button';
1901
+ uploadBtn.className = 'rk-ip-filemodal-upload'; uploadBtn.textContent = '\u2191 Upload from computer';
1902
+ var uploadInput = document.createElement('input'); uploadInput.type = 'file';
1903
+ uploadInput.accept = 'image/png,image/jpeg,image/webp,image/gif,image/avif';
1904
+ uploadInput.multiple = true; uploadInput.hidden = true;
1905
+ var close = document.createElement('button'); close.type = 'button';
1906
+ close.className = 'rk-ip-filemodal-close'; close.setAttribute('aria-label', 'Close'); close.innerHTML = '&times;';
1907
+ actions.appendChild(uploadBtn); actions.appendChild(close);
1908
+ head.appendChild(title); head.appendChild(actions); head.appendChild(uploadInput);
1909
+ uploadBtn.addEventListener('click', function(){ if(!uploadBtn.disabled) uploadInput.click(); });
1910
+ uploadInput.addEventListener('change', function(){
1911
+ var files = Array.prototype.slice.call(uploadInput.files || []).filter(isImageFile);
1912
+ uploadInput.value = '';
1913
+ if(!files.length){ status('Pick an image file to upload as a reference.', 'error'); return; }
1914
+ var original = uploadBtn.textContent;
1915
+ uploadBtn.disabled = true; uploadBtn.textContent = 'Uploading\u2026';
1916
+ addFiles(files).then(function(added){
1917
+ uploadBtn.disabled = false; uploadBtn.textContent = original;
1918
+ if(added) closePanel();
1919
+ else status('Those files were already attached.', '');
1920
+ });
1921
+ });
1922
+ var mountEl = document.createElement('div'); mountEl.className = 'rk-ip-filemodal-mount';
1923
+ panel.appendChild(head); panel.appendChild(mountEl); overlay.appendChild(panel);
1924
+ document.body.appendChild(overlay); modal = overlay;
1925
+ close.addEventListener('click', closePanel);
1926
+ overlay.addEventListener('click', function(e){ if(e.target === overlay) closePanel(); });
1927
+ document.addEventListener('keydown', onEsc, true);
1928
+ if(window.__vidfarmDirectory && window.__vidfarmDirectory.mount){
1929
+ modal._handle = window.__vidfarmDirectory.mount(mountEl, {
1930
+ mode: 'attach', showMultiSelect: true, primaryClick: 'attach',
1931
+ onAttach: function(items){
1932
+ var added = 0;
1933
+ (items || []).forEach(function(it){
1934
+ var ct = it.contentType || '';
1935
+ var isImg = ct.indexOf('image/') === 0 || /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(it.name || '');
1936
+ if(it.viewUrl && isImg && refs.indexOf(it.viewUrl) === -1){ refs.push(it.viewUrl); added++; }
1937
+ });
1938
+ renderRefs();
1939
+ if(added){ status(plural(added, 'reference') + ' attached.', 'success'); closePanel(); }
1940
+ else status('Pick an image file to use as a reference.', 'error');
1941
+ }
1942
+ });
1943
+ } else {
1944
+ mountEl.innerHTML = '<p style="padding:16px;color:var(--rk-text-muted);font-size:13px">File browser is unavailable right now.</p>';
1945
+ }
1946
+ }
1947
+
1948
+ // ---- wiring ------------------------------------------------------------
1949
+ if(ta){
1950
+ ta.addEventListener('input', function(){ autoGrow(); syncSend(); });
1951
+ ta.addEventListener('keydown', function(e){
1952
+ if(e.key !== 'Enter' || e.shiftKey) return;
1953
+ e.preventDefault();
1954
+ // Enter is ignored mid-turn so an impatient double-send cannot queue a
1955
+ // second job; the draft stays put.
1956
+ if(busy) return;
1957
+ if(cfg.onSend) cfg.onSend();
1958
+ });
1959
+ ta.addEventListener('paste', function(e){
1960
+ var imgs = imagesFromClipboard(e);
1961
+ if(imgs.length){ e.preventDefault(); addFiles(imgs); }
1962
+ });
1963
+ }
1964
+ if(clipBtn) clipBtn.addEventListener('click', openPanel);
1965
+ if(sendBtn) sendBtn.addEventListener('click', function(){
1966
+ if(busy){ if(cfg.onStop) cfg.onStop(); return; }
1967
+ if(cfg.onSend) cfg.onSend();
1968
+ });
1969
+ if(root){
1970
+ ['dragenter', 'dragover'].forEach(function(ev){
1971
+ root.addEventListener(ev, function(e){
1972
+ if(dtHasFiles(e)){ e.preventDefault(); e.stopPropagation(); root.classList.add('is-dragref'); }
1973
+ });
1974
+ });
1975
+ ['dragleave', 'dragend'].forEach(function(ev){
1976
+ root.addEventListener(ev, function(){ root.classList.remove('is-dragref'); });
1977
+ });
1978
+ root.addEventListener('drop', function(e){
1979
+ if(!dtHasFiles(e)) return;
1980
+ e.preventDefault(); e.stopPropagation();
1981
+ root.classList.remove('is-dragref');
1982
+ addFiles(e.dataTransfer.files);
1983
+ });
1984
+ }
1985
+
1986
+ autoGrow();
1987
+ syncSend();
1988
+
1989
+ return {
1990
+ refs: function(){ return refs.slice(); },
1991
+ setRefs: function(list){ refs = (list || []).slice(); renderRefs(); },
1992
+ clearRefs: function(){ refs = []; renderRefs(); },
1993
+ addFiles: addFiles,
1994
+ renderRefs: renderRefs,
1995
+ autoGrow: autoGrow,
1996
+ resetHeight: resetHeight,
1997
+ setBusy: setBusy,
1998
+ isBusy: function(){ return busy; },
1999
+ syncSend: syncSend,
2000
+ closePanel: closePanel,
2001
+ setPlaceholder: function(s){ if(ta) ta.placeholder = s; }
2002
+ };
2003
+ };
2004
+ })();
2005
+
1721
2006
  (function(){
1722
2007
  const uploadButton = document.querySelector("[data-custom-inpaint-upload]");
1723
2008
  const startNewButton = document.querySelector("[data-custom-inpaint-start-new]");
@@ -1771,7 +2056,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1771
2056
  const chatFeed = document.querySelector("[data-ip-feed]");
1772
2057
  const canvasHolder = document.querySelector("[data-ip-canvas-holder]");
1773
2058
  const composerEl = document.querySelector("[data-ip-composer]");
1774
- const modeSelect = document.querySelector("[data-ip-mode]");
1775
2059
  const maskBlock = document.querySelector("[data-ip-mask-block]");
1776
2060
  const maskChip = document.querySelector("[data-ip-comp-mask]");
1777
2061
  const paintStack = document.querySelector("[data-ip-paint-stack]");
@@ -1818,7 +2102,6 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
1818
2102
  || !(chatFeed instanceof HTMLElement)
1819
2103
  || !(canvasHolder instanceof HTMLElement)
1820
2104
  || !(composerEl instanceof HTMLElement)
1821
- || !(modeSelect instanceof HTMLSelectElement)
1822
2105
  || !(maskBlock instanceof HTMLElement)
1823
2106
  || !(maskChip instanceof HTMLElement)
1824
2107
  || !(paintStack instanceof HTMLElement)) {
@@ -3074,7 +3357,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3074
3357
  return;
3075
3358
  }
3076
3359
  // Never stomp on a turn that is mid-flight locally.
3077
- if (genBusy || activeTurnId) { syncSessionAssets(); return; }
3360
+ if (isGenBusy() || activeTurnId) { syncSessionAssets(); return; }
3078
3361
  historyRefresh.disabled = true;
3079
3362
  try {
3080
3363
  const endpoint = new URL("/job-runs/history", window.location.origin);
@@ -3711,41 +3994,36 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3711
3994
  var genSend = document.getElementById('rk-ip-gensend');
3712
3995
  var genRefsEl = document.getElementById('rk-ip-genrefs');
3713
3996
  var genStatusEl = document.getElementById('rk-ip-genstatus');
3714
- var genWrap = composerEl;
3715
- var GEN_SEND_HTML = genSend ? genSend.innerHTML : '';
3716
- var genRefs = [];
3717
- var genBusy = false;
3997
+ // The composer itself — auto-grow, the drag-to-resize grip, reference chips,
3998
+ // paste/drop, and the attach pop panel — is the shared implementation from
3999
+ // chat-composer.ts, byte-identical to the one on /tools/video.
4000
+ var composer = window.__rkChatComposer({
4001
+ root: composerEl,
4002
+ textarea: genPrompt,
4003
+ refsEl: genRefsEl,
4004
+ sendBtn: genSend,
4005
+ clipBtn: genClip,
4006
+ attachTitle: "Attach reference images",
4007
+ uploadBlob: function(f, name, type){ return uploadTemporaryBlob(f, name, type); },
4008
+ onStatus: function(m, t){ setStatus(m, t); },
4009
+ canSend: function(){ return maskHasInstruction(); },
4010
+ onSend: function(){ sendChatMessage(); },
4011
+ onStop: function(){ stopActiveTurn(); }
4012
+ });
4013
+ // Aliases so the rest of the page keeps its original call sites.
4014
+ function isGenBusy(){ return Boolean(composer && composer.isBusy()); }
4015
+ function renderGenRefs(){ composer.renderRefs(); }
4016
+ function autoGrowComposer(){ composer.autoGrow(); }
4017
+ function addGenReferenceFiles(fl){ return composer.addFiles(fl); }
4018
+ function closeGenFilePanel(){ composer.closePanel(); }
4019
+ function syncGenSend(){ composer.syncSend(); }
4020
+ function setGenBusy(v){ composer.setBusy(v); }
3718
4021
 
3719
4022
  // One status line for the whole surface — the composer's.
3720
4023
  function setGenStatus(m, t){ setStatus(m, t); }
3721
4024
  function maskHasInstruction(){
3722
4025
  return inpaintStrokes.length > 0 && inpaintMaskColors.some(function(entry){ return String(entry.prompt || '').trim(); });
3723
4026
  }
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.
3728
- function syncGenSend(){
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');
3741
- genSend.disabled = !canGenNew();
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
4027
 
3750
4028
  // ---- stop --------------------------------------------------------------
3751
4029
  // Stop aborts the client poll AND best-effort cancels the job server-side so a
@@ -3755,7 +4033,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3755
4033
  function stopError(){ var e = new Error('__vidfarm_stopped__'); e.stopped = true; return e; }
3756
4034
  function throwIfStopped(){ if(stopRequested) throw stopError(); }
3757
4035
  function stopActiveTurn(){
3758
- if(!genBusy || stopRequested) return;
4036
+ if(!isGenBusy() || stopRequested) return;
3759
4037
  stopRequested = true;
3760
4038
  setStatus('Stopping…', '');
3761
4039
  if(activeTurnId) updatePendingLabel(activeTurnId, 'Stopping…');
@@ -3767,129 +4045,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3767
4045
  }
3768
4046
  function composerPlaceholder(){
3769
4047
  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';
3790
- }
3791
- function renderGenRefs(){
3792
- if(!genRefsEl) return;
3793
- genRefsEl.hidden = genRefs.length === 0;
3794
- genRefsEl.innerHTML = '';
3795
- genRefs.forEach(function(url, idx){
3796
- var chip = document.createElement('span'); chip.className = 'rk-ip-gen-ref';
3797
- var im = document.createElement('img'); im.src = url; im.alt = ''; im.loading = 'lazy'; chip.appendChild(im);
3798
- var lbl = document.createElement('span'); var nm = url.split('/').pop() || 'reference'; lbl.textContent = decodeURIComponent(nm).slice(0, 28); chip.appendChild(lbl);
3799
- var x = document.createElement('button'); x.type = 'button'; x.setAttribute('aria-label', 'Remove reference'); x.innerHTML = '&times;';
3800
- x.addEventListener('click', function(){ genRefs.splice(idx, 1); renderGenRefs(); }); chip.appendChild(x);
3801
- genRefsEl.appendChild(chip);
3802
- });
4048
+ genPrompt.placeholder = sourceImageUrl
4049
+ ? (inpaintStrokes.length ? 'Describe the masked edit (whole-image guidance)\u2026' : 'Describe the next edit to the newest image\u2026')
4050
+ : 'Describe the image you want\u2026';
3803
4051
  }
3804
4052
 
3805
- // Paperclip -> shared file-directory explorer (pop panel) to attach image refs.
3806
- var genFileModal = null;
3807
- function closeGenFilePanel(){ if(genFileModal){ try{ genFileModal._handle && genFileModal._handle.destroy && genFileModal._handle.destroy(); }catch(_){} genFileModal.remove(); genFileModal = null; document.removeEventListener('keydown', onGenFileEsc, true); } }
3808
- function onGenFileEsc(e){ if(e.key === 'Escape') closeGenFilePanel(); }
3809
- function openGenFilePanel(){
3810
- if(genFileModal) return;
3811
- var overlay = document.createElement('div'); overlay.className = 'rk-ip-filemodal';
3812
- var panel = document.createElement('div'); panel.className = 'rk-ip-filemodal-panel';
3813
- var head = document.createElement('div'); head.className = 'rk-ip-filemodal-head';
3814
- var title = document.createElement('span'); title.className = 'rk-ip-filemodal-title'; title.textContent = 'Attach reference images';
3815
- var headActions = document.createElement('div'); headActions.className = 'rk-ip-filemodal-actions';
3816
- var uploadBtn = document.createElement('button'); uploadBtn.type = 'button'; uploadBtn.className = 'rk-ip-filemodal-upload'; uploadBtn.textContent = '\u2191 Upload from computer';
3817
- var uploadInput = document.createElement('input'); uploadInput.type = 'file'; uploadInput.accept = 'image/png,image/jpeg,image/webp,image/gif,image/avif'; uploadInput.multiple = true; uploadInput.hidden = true;
3818
- var close = document.createElement('button'); close.type = 'button'; close.className = 'rk-ip-filemodal-close'; close.setAttribute('aria-label', 'Close'); close.innerHTML = '&times;';
3819
- headActions.appendChild(uploadBtn); headActions.appendChild(close);
3820
- head.appendChild(title); head.appendChild(headActions); head.appendChild(uploadInput);
3821
- uploadBtn.addEventListener('click', function(){ if(!uploadBtn.disabled) uploadInput.click(); });
3822
- uploadInput.addEventListener('change', function(){
3823
- var files = Array.prototype.slice.call(uploadInput.files || []).filter(function(f){ return /^image\//i.test(f.type || '') || /\.(png|jpe?g|gif|webp|avif)$/i.test(f.name || ''); });
3824
- uploadInput.value = '';
3825
- if(!files.length){ setGenStatus('Pick an image file to upload as a reference.', 'error'); return; }
3826
- uploadBtn.disabled = true; var original = uploadBtn.textContent; uploadBtn.textContent = 'Uploading\u2026';
3827
- setGenStatus('Uploading ' + files.length + ' reference' + (files.length > 1 ? 's' : '') + '\u2026', '');
3828
- var added = 0;
3829
- files.reduce(function(chain, f){
3830
- return chain.then(function(){
3831
- return uploadTemporaryBlob(f, f.name || 'reference.png', f.type || 'image/png').then(function(url){
3832
- if(url && genRefs.indexOf(url) === -1){ genRefs.push(url); added++; renderGenRefs(); }
3833
- });
3834
- });
3835
- }, Promise.resolve()).then(function(){
3836
- uploadBtn.disabled = false; uploadBtn.textContent = original;
3837
- if(added){ setGenStatus(added + ' reference' + (added > 1 ? 's' : '') + ' uploaded.', 'success'); closeGenFilePanel(); }
3838
- else setGenStatus('Those files were already attached.', '');
3839
- }).catch(function(err){
3840
- uploadBtn.disabled = false; uploadBtn.textContent = original;
3841
- setGenStatus((err && err.message) ? err.message : 'Upload failed.', 'error');
3842
- });
3843
- });
3844
- var mountEl = document.createElement('div'); mountEl.className = 'rk-ip-filemodal-mount';
3845
- panel.appendChild(head); panel.appendChild(mountEl); overlay.appendChild(panel);
3846
- document.body.appendChild(overlay); genFileModal = overlay;
3847
- close.addEventListener('click', closeGenFilePanel);
3848
- overlay.addEventListener('click', function(e){ if(e.target === overlay) closeGenFilePanel(); });
3849
- document.addEventListener('keydown', onGenFileEsc, true);
3850
- if(window.__vidfarmDirectory && window.__vidfarmDirectory.mount){
3851
- genFileModal._handle = window.__vidfarmDirectory.mount(mountEl, {
3852
- mode: 'attach', showMultiSelect: true, primaryClick: 'attach',
3853
- onAttach: function(items){
3854
- var added = 0;
3855
- (items || []).forEach(function(it){
3856
- var ct = (it.contentType || ''); var isImg = ct.indexOf('image/') === 0 || /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(it.name || '');
3857
- if(it.viewUrl && isImg && genRefs.indexOf(it.viewUrl) === -1){ genRefs.push(it.viewUrl); added++; }
3858
- });
3859
- renderGenRefs();
3860
- if(added){ setGenStatus(added + ' reference' + (added > 1 ? 's' : '') + ' attached.', 'success'); closeGenFilePanel(); }
3861
- else setGenStatus('Pick an image file to use as a reference.', 'error');
3862
- }
3863
- });
3864
- } else {
3865
- mountEl.innerHTML = '<p style="padding:16px;color:var(--rk-text-muted);font-size:13px">File browser is unavailable right now.</p>';
3866
- }
3867
- }
3868
-
3869
- // Paste or drop image files straight into the composer -> temp upload -> reference.
3870
- function addGenReferenceFiles(fileList){
3871
- var files = Array.prototype.slice.call(fileList || []).filter(function(f){ return /^image\//i.test(f.type || '') || /\.(png|jpe?g|gif|webp|avif)$/i.test(f.name || ''); });
3872
- if(!files.length) return Promise.resolve(0);
3873
- setGenStatus('Uploading ' + files.length + ' reference' + (files.length > 1 ? 's' : '') + '\u2026', '');
3874
- var added = 0;
3875
- return files.reduce(function(chain, f){
3876
- return chain.then(function(){
3877
- return uploadTemporaryBlob(f, f.name || 'pasted-image.png', f.type || 'image/png').then(function(url){
3878
- if(url && genRefs.indexOf(url) === -1){ genRefs.push(url); added++; renderGenRefs(); }
3879
- });
3880
- });
3881
- }, Promise.resolve()).then(function(){
3882
- if(added) setGenStatus(added + ' reference' + (added > 1 ? 's' : '') + ' attached.', 'success');
3883
- return added;
3884
- }).catch(function(err){ setGenStatus((err && err.message) ? err.message : 'Upload failed.', 'error'); return added; });
3885
- }
3886
- function imagesFromClipboard(e){
3887
- var items = (e.clipboardData && e.clipboardData.items) || []; var out = [];
3888
- for(var i = 0; i < items.length; i++){ if(items[i].kind === 'file'){ var f = items[i].getAsFile(); if(f && /^image\//i.test(f.type || '')) out.push(f); } }
3889
- return out;
3890
- }
3891
- function dtHasFiles(e){ var t = (e.dataTransfer && e.dataTransfer.types) || []; return Array.prototype.indexOf.call(t, 'Files') !== -1; }
3892
-
3893
4053
  // "character card" / "storyboard" shorthand — spell out the full layout
3894
4054
  // so the image model produces a reference sheet / numbered shot board
3895
4055
  // instead of a single picture. Snippets are shared with the AI chat +
@@ -4235,61 +4395,31 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4235
4395
  });
4236
4396
  }
4237
4397
 
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
4398
  // ---- send ----------------------------------------------------------------
4264
4399
  function sendChatMessage(){
4265
- if(genBusy) return;
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;
4400
+ if(isGenBusy()) return;
4401
+ var text = expandShorthand((genPrompt && genPrompt.value || '').trim());
4402
+ var masked = inpaintStrokes.length > 0;
4270
4403
  if(!text && !(masked && maskHasInstruction())){
4271
- setStatus(mode === 'video' ? 'Describe the video you want.' : 'Type what you want to create or change.', 'error');
4404
+ setStatus('Type what you want to create or change.', 'error');
4272
4405
  if(genPrompt) genPrompt.focus();
4273
4406
  return;
4274
4407
  }
4275
- if(mode === 'video' && !text){ setStatus('Describe the video you want.', 'error'); return; }
4276
- var refs = genRefs.slice();
4408
+ var refs = composer.refs();
4277
4409
  addTurn({
4278
4410
  role: 'user', kind: 'text',
4279
4411
  text: text || 'Apply the painted mask instructions.',
4280
- refs: refs, mode: mode, masked: masked,
4281
- isEdit: mode === 'image' && Boolean(sourceImageUrl)
4412
+ refs: refs, mode: 'image', masked: masked,
4413
+ isEdit: Boolean(sourceImageUrl)
4282
4414
  });
4283
- if(genPrompt){ genPrompt.value = ''; autoGrowComposer(); }
4284
- genRefs = [];
4285
- renderGenRefs();
4415
+ if(genPrompt){ genPrompt.value = ''; composer.resetHeight(); }
4416
+ composer.clearRefs();
4286
4417
  stopRequested = false;
4287
4418
  activeJob = { kind: '', jobId: '' };
4288
4419
  setGenBusy(true);
4289
- setStatus(mode === 'video' ? 'Creating video\u2026' : 'Working\u2026', '');
4420
+ setStatus('Working\u2026', '');
4290
4421
  var work;
4291
- if(mode === 'video') work = runVideoTurn(text);
4292
- else if(!sourceImageUrl) work = runGenerateTurn(text, refs);
4422
+ if(!sourceImageUrl) work = runGenerateTurn(text, refs);
4293
4423
  else if(masked) work = runMaskedTurn(text, refs);
4294
4424
  else work = runEditTurn(text, refs);
4295
4425
  work.catch(function(err){
@@ -4312,25 +4442,8 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4312
4442
  });
4313
4443
  }
4314
4444
 
4315
- if(genPrompt){
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
- });
4324
- genPrompt.addEventListener('paste', function(e){ var imgs = imagesFromClipboard(e); if(imgs.length){ e.preventDefault(); addGenReferenceFiles(imgs); } });
4325
- }
4326
- if(genWrap){
4327
- ['dragenter', 'dragover'].forEach(function(ev){ genWrap.addEventListener(ev, function(e){ if(dtHasFiles(e)){ e.preventDefault(); e.stopPropagation(); genWrap.classList.add('is-dragref'); } }); });
4328
- ['dragleave', 'dragend'].forEach(function(ev){ genWrap.addEventListener(ev, function(){ genWrap.classList.remove('is-dragref'); }); });
4329
- genWrap.addEventListener('drop', function(e){ if(dtHasFiles(e)){ e.preventDefault(); e.stopPropagation(); genWrap.classList.remove('is-dragref'); addGenReferenceFiles(e.dataTransfer.files); } });
4330
- }
4331
- if(genClip) genClip.addEventListener('click', openGenFilePanel);
4332
- if(genSend) genSend.addEventListener('click', function(){ if(genBusy) stopActiveTurn(); else sendChatMessage(); });
4333
- modeSelect.addEventListener('change', function(){ composerPlaceholder(); syncGenSend(); });
4445
+ // input / Enter-to-send / paste / drag-drop / paperclip / send-stop are all
4446
+ // wired by the shared composer runtime above.
4334
4447
  // ⌘Z / ⇧⌘Z anywhere outside a text field undoes and redoes the paint.
4335
4448
  window.addEventListener('keydown', function(e){
4336
4449
  if(!(e.metaKey || e.ctrlKey) || String(e.key).toLowerCase() !== 'z') return;