@mevdragon/vidfarm-devcli 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/editor-capabilities/SKILL.md +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +2 -0
- package/SKILL.director.md +174 -16
- package/SKILL.platform.md +7 -3
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/dist/src/app.js +1513 -145
- package/dist/src/cli.js +1365 -105
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clips.js +3 -0
- package/dist/src/devcli/composition-edit.js +396 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +208 -1
- package/dist/src/editor-chat.js +33 -17
- package/dist/src/frontend/file-directory.js +271 -20
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +6 -3
- package/dist/src/page-shell.js +1 -1
- package/dist/src/primitive-registry.js +409 -4
- package/dist/src/reskin/chat-page.js +103 -15
- package/dist/src/reskin/discover-page.js +247 -10
- package/dist/src/reskin/document.js +147 -10
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -452
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +324 -82
- package/dist/src/reskin/theme.js +36 -11
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/hunt.js +2 -0
- package/dist/src/services/clip-records.js +28 -0
- package/dist/src/services/file-directory.js +6 -3
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +24 -2
- package/package.json +1 -1
- package/public/assets/file-directory-app.js +2 -2
- package/public/assets/homepage-client-app.js +1 -1
- package/public/assets/page-runtime-client-app.js +2 -2
- package/public/assets/placeholders/scene-placeholder.png +0 -0
|
@@ -145,6 +145,30 @@ function viewerHtml() {
|
|
|
145
145
|
</div>
|
|
146
146
|
</div>`;
|
|
147
147
|
}
|
|
148
|
+
// The fullscreen TikTok-style viewer for the SWIPE deck — a sibling of the feed
|
|
149
|
+
// viewer above but purpose-built for swiping: slides carry live composition
|
|
150
|
+
// iframes (not video/img), and a bottom action bar reject/approves the ACTIVE
|
|
151
|
+
// card, driving the same POST /discover/swipe/:id/{approve,reject} the deck uses.
|
|
152
|
+
// The client script (swipe block) fills [data-sv-track] with slides on open.
|
|
153
|
+
function swipeViewerHtml() {
|
|
154
|
+
return `
|
|
155
|
+
<div class="rk-discover-viewer rk-swipe-viewer" data-rk-swipe-viewer hidden role="dialog" aria-modal="true" aria-label="Swipe preview">
|
|
156
|
+
<div class="rk-discover-viewer-scrim" data-sv-close></div>
|
|
157
|
+
<div class="rk-discover-viewer-track rk-swipe-viewer-track" data-sv-track></div>
|
|
158
|
+
<div class="rk-discover-viewer-ui">
|
|
159
|
+
<button type="button" class="rk-discover-viewer-btn" data-sv-mute aria-label="Toggle sound"><span class="rk-dvm-on">${SOUND_ON_ICON}</span><span class="rk-dvm-off">${SOUND_OFF_ICON}</span></button>
|
|
160
|
+
<button type="button" class="rk-discover-viewer-btn" data-sv-close aria-label="Close viewer">${CROSS_ICON}</button>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="rk-discover-viewer-nav">
|
|
163
|
+
<button type="button" class="rk-discover-viewer-btn" data-sv-prev aria-label="Previous">${CHEV_UP_ICON}</button>
|
|
164
|
+
<button type="button" class="rk-discover-viewer-btn" data-sv-next aria-label="Next">${CHEV_DOWN_ICON}</button>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="rk-swipe-viewer-actions" data-sv-actions>
|
|
167
|
+
<button type="button" class="rk-discover-act rk-discover-act-skip" data-sv-reject aria-label="Reject">${CROSS_ICON}</button>
|
|
168
|
+
<button type="button" class="rk-discover-act rk-discover-act-fork" data-sv-approve aria-label="Approve">${CHECK_ICON}</button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>`;
|
|
171
|
+
}
|
|
148
172
|
// The "Add New" modal (feed mode only) — paste a social video URL or upload a
|
|
149
173
|
// video, adding it to the caller's private Discover feed via the same real
|
|
150
174
|
// endpoints the old React SPA used (POST /discover/templates + the upload
|
|
@@ -237,8 +261,8 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
237
261
|
</div>
|
|
238
262
|
<button type="button" class="rk-discover-act rk-discover-act-fork" data-swipe-fork data-swipe-act hidden aria-label="Fork">${CHECK_ICON}</button>
|
|
239
263
|
</div>
|
|
240
|
-
<p class="rk-discover-act-hint" data-actions-hint hidden>Swipe to customize into an ad for your brand.</p>
|
|
241
|
-
</div
|
|
264
|
+
<p class="rk-discover-act-hint" data-actions-hint hidden>Tap the card to preview full-screen. Swipe to customize into an ad for your brand.</p>
|
|
265
|
+
</div>${swipeViewerHtml()}`;
|
|
242
266
|
}
|
|
243
267
|
// ── CATEGORIES ──────────────────────────────────────────────────────────
|
|
244
268
|
if (mode === "categories") {
|
|
@@ -537,6 +561,19 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
537
561
|
.rk-discover-act-fork:hover{background:var(--rk-gold-700);box-shadow:0 8px 22px -6px rgba(252,185,0,.6)}
|
|
538
562
|
.rk-discover-act-hint{font-size:12.5px;font-weight:600;color:var(--rk-text-faint);text-align:center;letter-spacing:.01em}
|
|
539
563
|
.rk-discover-act-hint[hidden]{display:none}
|
|
564
|
+
/* tapping the front card opens the fullscreen swipe viewer */
|
|
565
|
+
.rk-discover-deck .rk-discover-swipe{cursor:pointer}
|
|
566
|
+
/* ── swipe: fullscreen TikTok viewer — reuses .rk-discover-viewer* shell; slides
|
|
567
|
+
hold live composition iframes (.rk-discover-swipe-frame) and a bottom bar
|
|
568
|
+
reject/approves the active card. */
|
|
569
|
+
.rk-swipe-viewer-stage{background:#000}
|
|
570
|
+
.rk-swipe-viewer-slide .rk-discover-swipe-frame{border-radius:inherit}
|
|
571
|
+
.rk-swipe-viewer .rk-discover-viewer-info{padding-bottom:112px}
|
|
572
|
+
.rk-swipe-viewer-actions{position:absolute;left:0;right:0;bottom:max(24px,env(safe-area-inset-bottom,0px));z-index:6;
|
|
573
|
+
display:flex;gap:40px;justify-content:center;align-items:center;pointer-events:none}
|
|
574
|
+
.rk-swipe-viewer-actions .rk-discover-act{pointer-events:auto;width:64px;height:64px}
|
|
575
|
+
.rk-swipe-viewer-actions .rk-discover-act[hidden]{display:none}
|
|
576
|
+
@media(max-width:560px){.rk-swipe-viewer-actions{gap:30px}}
|
|
540
577
|
|
|
541
578
|
/* ── categories: browse-by-format cards ── */
|
|
542
579
|
.rk-discover-cats{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:20px}
|
|
@@ -759,7 +796,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
759
796
|
? '<span class="rk-btn rk-btn-ghost rk-btn-sm rk-btn-block rk-discover-fork rk-discover-fork-disabled" aria-disabled="true">'+(t.status==='failed'?'Download failed':'Preparing…')+'</span>'
|
|
760
797
|
: remix
|
|
761
798
|
? '<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork" data-remix="'+attr(inspirationIdOf(t))+'">'+EDIT_ICON+' Remix</button>'
|
|
762
|
-
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork" href="'+attr(editorUrl(tid))+'">'+EDIT_ICON+' Open Editor</a>';
|
|
799
|
+
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork" href="'+attr(editorUrl(tid))+'" target="_blank" rel="noopener">'+EDIT_ICON+' Open Editor</a>';
|
|
763
800
|
return '<article class="rk-discover-card rk-card-hover">'
|
|
764
801
|
+ poster
|
|
765
802
|
+ '<div class="rk-discover-body">'
|
|
@@ -846,7 +883,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
846
883
|
+(desc?'<div class="rk-discover-viewer-desc">'+esc(desc)+'</div>':'')
|
|
847
884
|
+(remix
|
|
848
885
|
? '<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-discover-viewer-fork" data-remix="'+attr(inspirationIdOf(t))+'">'+EDIT_ICON+' Remix</button>'
|
|
849
|
-
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-discover-viewer-fork" href="'+attr(editorUrl(tid))+'">'+EDIT_ICON+' Open Editor</a>')
|
|
886
|
+
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-discover-viewer-fork" href="'+attr(editorUrl(tid))+'" target="_blank" rel="noopener">'+EDIT_ICON+' Open Editor</a>')
|
|
850
887
|
+'</div>'
|
|
851
888
|
+'</figure>'
|
|
852
889
|
+'</section>';
|
|
@@ -1236,6 +1273,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
1236
1273
|
var soundBtn=swipeRoot.querySelector('[data-swipe-sound]');
|
|
1237
1274
|
var sw={target:5,customizing:0,productSet:false,busy:false,refilling:false,loaded:false,soundOn:false,known:{}};
|
|
1238
1275
|
var nodes=[]; // live card DOM nodes, top-of-deck first
|
|
1276
|
+
var deckExpanded=false; // true while the fullscreen swipe viewer owns the previews
|
|
1239
1277
|
|
|
1240
1278
|
// ── audio: the same-origin preview iframes poll these window flags (see
|
|
1241
1279
|
// injectSwipePreviewFit). Only the TOP card's id is named audible so the
|
|
@@ -1243,6 +1281,9 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
1243
1281
|
// always start muted (the only autoplay browsers permit); the toggle is the
|
|
1244
1282
|
// user gesture that unlocks sound.
|
|
1245
1283
|
function syncSound(){
|
|
1284
|
+
// While the fullscreen viewer is open it owns the audio flags (it names the
|
|
1285
|
+
// active slide's card audible); the deck must not clobber them.
|
|
1286
|
+
if(deckExpanded) return;
|
|
1246
1287
|
try{
|
|
1247
1288
|
window.__VF_SWIPE_SOUND__ = sw.soundOn===true;
|
|
1248
1289
|
window.__VF_SWIPE_SOUND_TOP__ = nodes.length? String(nodes[0]._card.id): null;
|
|
@@ -1308,12 +1349,16 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
1308
1349
|
el.style.zIndex=String(20-i);
|
|
1309
1350
|
el.style.transform='translateY('+(i*12)+'px) scale('+(1-i*0.045).toFixed(3)+')';
|
|
1310
1351
|
el.style.opacity=i===0?'1':(i===1?'0.92':'0.82');
|
|
1311
|
-
|
|
1352
|
+
// The viewer owns the previews while expanded — don't build deck iframes
|
|
1353
|
+
// (that would double the audio + CPU); resume rebuilds them on close.
|
|
1354
|
+
if(i<=1 && !deckExpanded) upgradeToIframe(el);
|
|
1312
1355
|
});
|
|
1313
1356
|
var has=nodes.length>0;
|
|
1314
1357
|
actionBtns.forEach(function(b){ b.hidden=!has; });
|
|
1315
1358
|
if(hint) hint.hidden=!has;
|
|
1316
1359
|
syncSound(); updateSoundBtn();
|
|
1360
|
+
// Keep the open viewer's slide list in step with refills/removals.
|
|
1361
|
+
if(deckExpanded && swViewer) swViewer.sync();
|
|
1317
1362
|
if(!has) swCaughtUp();
|
|
1318
1363
|
}
|
|
1319
1364
|
function swCaughtUp(){
|
|
@@ -1323,6 +1368,25 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
1323
1368
|
swShowState('<div class="rk-discover-swipe-state-title">You\\u2019re all caught up</div><div>No more templates to customize right now. Check back as new ones are added.</div>');
|
|
1324
1369
|
}
|
|
1325
1370
|
}
|
|
1371
|
+
// POST approve/reject for a card (shared by the deck buttons + the viewer).
|
|
1372
|
+
function postDecision(card,dir){
|
|
1373
|
+
var path=swipeBase+'/'+encodeURIComponent(String(card.id))+'/'+(dir==='fork'?'approve':'reject');
|
|
1374
|
+
return fetch(withAccount(path),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
1375
|
+
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
1376
|
+
.catch(function(){ return {}; });
|
|
1377
|
+
}
|
|
1378
|
+
// Drop a card from the live deck (array + its DOM node) by id. The viewer can
|
|
1379
|
+
// consume a card that isn't the top one, so removal is id-based, not shift().
|
|
1380
|
+
function removeNodeByCard(card){
|
|
1381
|
+
var id=card&&card.id!=null?String(card.id):null; if(id==null) return;
|
|
1382
|
+
for(var i=0;i<nodes.length;i++){
|
|
1383
|
+
if(String(nodes[i]._card.id)===id){
|
|
1384
|
+
var el=nodes[i]; nodes.splice(i,1);
|
|
1385
|
+
if(el&&el.parentNode) el.parentNode.removeChild(el);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1326
1390
|
function decide(dir){
|
|
1327
1391
|
if(sw.busy||!nodes.length) return;
|
|
1328
1392
|
sw.busy=true;
|
|
@@ -1333,11 +1397,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
1333
1397
|
el.style.transform='translateX('+(dir==='fork'?460:-460)+'px) rotate('+(dir==='fork'?15:-15)+'deg)';
|
|
1334
1398
|
el.style.opacity='0';
|
|
1335
1399
|
window.setTimeout(function(){ if(el&&el.parentNode) el.parentNode.removeChild(el); },380);
|
|
1336
|
-
|
|
1337
|
-
fetch(withAccount(path),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
1338
|
-
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
1339
|
-
.catch(function(){ /* swallowed — the card already left the deck */ })
|
|
1340
|
-
.then(function(){ sw.busy=false; restack(); refill(); });
|
|
1400
|
+
postDecision(card,dir).then(function(){ sw.busy=false; restack(); refill(); });
|
|
1341
1401
|
}
|
|
1342
1402
|
function refill(){
|
|
1343
1403
|
if(sw.refilling||!sw.productSet||!sw.loaded) return;
|
|
@@ -1420,6 +1480,183 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
1420
1480
|
swShowState('<div class="rk-discover-swipe-state-title">Couldn\\u2019t load your deck</div><div>'+esc(friendlyError(err))+'</div>','error');
|
|
1421
1481
|
});
|
|
1422
1482
|
}
|
|
1483
|
+
// ── fullscreen swipe viewer — a TikTok-style vertical scroll over the CURRENT
|
|
1484
|
+
// deck. Its bottom bar reject/approves the ACTIVE slide (same postDecision +
|
|
1485
|
+
// refill the deck buttons use). While open it OWNS the previews: the deck's
|
|
1486
|
+
// iframes are torn down (parkDeck) so audio/CPU never double, and it drives
|
|
1487
|
+
// __VF_SWIPE_SOUND_* for the active slide's card. Closing rebuilds the deck.
|
|
1488
|
+
var swViewer=(function(){
|
|
1489
|
+
var el=root.querySelector('[data-rk-swipe-viewer]');
|
|
1490
|
+
if(!el) return null;
|
|
1491
|
+
var track=el.querySelector('[data-sv-track]');
|
|
1492
|
+
var actionsEl=el.querySelector('[data-sv-actions]');
|
|
1493
|
+
var rejectBtn=el.querySelector('[data-sv-reject]');
|
|
1494
|
+
var approveBtn=el.querySelector('[data-sv-approve]');
|
|
1495
|
+
var open=false, active=-1, sound=false, io=null, busy=false, activeCardId=null;
|
|
1496
|
+
|
|
1497
|
+
function updateMuteUi(){ el.classList.toggle('is-muted',!sound); }
|
|
1498
|
+
function syncSVSound(){
|
|
1499
|
+
try{ window.__VF_SWIPE_SOUND__ = sound===true; window.__VF_SWIPE_SOUND_TOP__ = activeCardId || null; }catch(e){}
|
|
1500
|
+
updateMuteUi();
|
|
1501
|
+
}
|
|
1502
|
+
function updateActionState(){ if(actionsEl) actionsEl.style.display=track.children.length?'':'none'; }
|
|
1503
|
+
// Detach every deck iframe so only the viewer plays audio/video.
|
|
1504
|
+
function parkDeck(){
|
|
1505
|
+
nodes.forEach(function(n){
|
|
1506
|
+
var f=n.querySelector('.rk-discover-swipe-frame');
|
|
1507
|
+
if(f&&f.parentNode) f.parentNode.removeChild(f);
|
|
1508
|
+
n._iframed=false;
|
|
1509
|
+
var fb=n.querySelector('.rk-discover-swipe-fallback');
|
|
1510
|
+
if(fb){ fb.style.opacity=''; fb.style.pointerEvents=''; }
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
function hydrate(slide){
|
|
1514
|
+
if(!slide||slide._iframed) return;
|
|
1515
|
+
var url=slide.getAttribute('data-preview-url'); if(!url) return;
|
|
1516
|
+
var fb=slide.querySelector('.rk-discover-swipe-fallback');
|
|
1517
|
+
var frame=document.createElement('iframe');
|
|
1518
|
+
frame.className='rk-discover-swipe-frame';
|
|
1519
|
+
frame.setAttribute('src',url);
|
|
1520
|
+
frame.setAttribute('title', slide.getAttribute('data-title')||'Preview');
|
|
1521
|
+
frame.setAttribute('loading','lazy');
|
|
1522
|
+
frame.setAttribute('scrolling','no');
|
|
1523
|
+
frame.setAttribute('allow','autoplay');
|
|
1524
|
+
function reveal(){ if(fb){ fb.style.opacity='0'; fb.style.pointerEvents='none'; } }
|
|
1525
|
+
frame.addEventListener('load',function(){ window.setTimeout(reveal,120); });
|
|
1526
|
+
window.setTimeout(reveal,2600);
|
|
1527
|
+
var stage=slide.querySelector('.rk-swipe-viewer-stage');
|
|
1528
|
+
if(stage) stage.insertBefore(frame, stage.firstChild); else slide.insertBefore(frame, slide.firstChild);
|
|
1529
|
+
slide._iframed=true;
|
|
1530
|
+
}
|
|
1531
|
+
function setActive(idx){
|
|
1532
|
+
active=idx;
|
|
1533
|
+
var kids=track.children;
|
|
1534
|
+
for(var i=0;i<kids.length;i++){ if(Math.abs(i-idx)<=1) hydrate(kids[i]); }
|
|
1535
|
+
var s=kids[idx];
|
|
1536
|
+
activeCardId=s? s.getAttribute('data-card-id'): null;
|
|
1537
|
+
syncSVSound();
|
|
1538
|
+
}
|
|
1539
|
+
function appendSlide(card){
|
|
1540
|
+
var slide=document.createElement('section');
|
|
1541
|
+
slide.className='rk-discover-viewer-slide rk-swipe-viewer-slide';
|
|
1542
|
+
slide.setAttribute('data-card-id', String(card.id));
|
|
1543
|
+
var url=card.preview_url||card.previewUrl||'';
|
|
1544
|
+
if(url) slide.setAttribute('data-preview-url', url);
|
|
1545
|
+
var titleTxt=String(card.title||card.source_title||card.template_id||'Template');
|
|
1546
|
+
slide.setAttribute('data-title', titleTxt);
|
|
1547
|
+
var title=esc(titleTxt);
|
|
1548
|
+
var caption=card.caption?('<div class="rk-discover-viewer-desc">'+esc(card.caption)+'</div>'):'';
|
|
1549
|
+
slide.innerHTML='<figure class="rk-discover-viewer-stage rk-swipe-viewer-stage">'
|
|
1550
|
+
+'<div class="rk-discover-swipe-fallback"><span>'+title+'</span></div>'
|
|
1551
|
+
+'<div class="rk-discover-viewer-shade"></div>'
|
|
1552
|
+
+'<div class="rk-discover-viewer-info"><div class="rk-discover-viewer-title">'+title+'</div>'+caption+'</div>'
|
|
1553
|
+
+'</figure>';
|
|
1554
|
+
track.appendChild(slide);
|
|
1555
|
+
if(open&&io) io.observe(slide);
|
|
1556
|
+
return slide;
|
|
1557
|
+
}
|
|
1558
|
+
// Keep the open track in step with the live deck: append a slide for any
|
|
1559
|
+
// node that doesn't have one yet (refills). Removals go through decide().
|
|
1560
|
+
function sync(){
|
|
1561
|
+
if(!open) return;
|
|
1562
|
+
var have={};
|
|
1563
|
+
for(var i=0;i<track.children.length;i++){ have[track.children[i].getAttribute('data-card-id')]=true; }
|
|
1564
|
+
nodes.forEach(function(n){ if(!have[String(n._card.id)]) appendSlide(n._card); });
|
|
1565
|
+
updateActionState();
|
|
1566
|
+
}
|
|
1567
|
+
function step(d){
|
|
1568
|
+
if(!open) return;
|
|
1569
|
+
var next=Math.max(0,Math.min(track.children.length-1,active+d));
|
|
1570
|
+
if(next===active) return;
|
|
1571
|
+
track.scrollTo({top:next*track.clientHeight,behavior:'smooth'});
|
|
1572
|
+
}
|
|
1573
|
+
function decideActive(dir){
|
|
1574
|
+
if(busy||active<0) return;
|
|
1575
|
+
var slide=track.children[active]; if(!slide) return;
|
|
1576
|
+
var id=slide.getAttribute('data-card-id'); var card=null;
|
|
1577
|
+
for(var i=0;i<nodes.length;i++){ if(String(nodes[i]._card.id)===String(id)){ card=nodes[i]._card; break; } }
|
|
1578
|
+
if(!card) return;
|
|
1579
|
+
busy=true; var wasIdx=active;
|
|
1580
|
+
var stage=slide.querySelector('.rk-swipe-viewer-stage');
|
|
1581
|
+
if(stage){
|
|
1582
|
+
stage.style.transition='transform .34s ease, opacity .34s ease';
|
|
1583
|
+
stage.style.transform='translateX('+(dir==='fork'?'62%':'-62%')+') rotate('+(dir==='fork'?'9deg':'-9deg')+')';
|
|
1584
|
+
stage.style.opacity='0';
|
|
1585
|
+
}
|
|
1586
|
+
postDecision(card,dir).then(function(){ refill(); });
|
|
1587
|
+
removeNodeByCard(card);
|
|
1588
|
+
window.setTimeout(function(){
|
|
1589
|
+
if(slide&&slide.parentNode) slide.parentNode.removeChild(slide);
|
|
1590
|
+
busy=false; updateActionState();
|
|
1591
|
+
if(!track.children.length){ close(); return; }
|
|
1592
|
+
var nextIdx=Math.min(wasIdx, track.children.length-1);
|
|
1593
|
+
track.scrollTop=nextIdx*track.clientHeight;
|
|
1594
|
+
setActive(nextIdx);
|
|
1595
|
+
},340);
|
|
1596
|
+
}
|
|
1597
|
+
function openAt(startId){
|
|
1598
|
+
if(!nodes.length||open) return;
|
|
1599
|
+
deckExpanded=true; parkDeck();
|
|
1600
|
+
track.innerHTML=''; sound=sw.soundOn===true; open=true;
|
|
1601
|
+
el.hidden=false; document.documentElement.classList.add('rk-noscroll');
|
|
1602
|
+
if(io) io.disconnect();
|
|
1603
|
+
io=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){
|
|
1604
|
+
entries.forEach(function(en){
|
|
1605
|
+
if(!en.isIntersecting) return;
|
|
1606
|
+
var i=[].indexOf.call(track.children,en.target);
|
|
1607
|
+
if(i>=0&&i!==active) setActive(i);
|
|
1608
|
+
});
|
|
1609
|
+
},{root:track,threshold:0.6}):null;
|
|
1610
|
+
nodes.forEach(function(n){ appendSlide(n._card); });
|
|
1611
|
+
updateActionState();
|
|
1612
|
+
var startIdx=0;
|
|
1613
|
+
for(var i=0;i<nodes.length;i++){ if(String(nodes[i]._card.id)===String(startId)){ startIdx=i; break; } }
|
|
1614
|
+
track.scrollTop=startIdx*track.clientHeight;
|
|
1615
|
+
setActive(startIdx);
|
|
1616
|
+
}
|
|
1617
|
+
function close(){
|
|
1618
|
+
if(!open) return;
|
|
1619
|
+
open=false; active=-1; activeCardId=null; busy=false;
|
|
1620
|
+
if(io){ io.disconnect(); io=null; }
|
|
1621
|
+
track.innerHTML=''; el.hidden=true;
|
|
1622
|
+
document.documentElement.classList.remove('rk-noscroll');
|
|
1623
|
+
deckExpanded=false; sw.soundOn=sound; // carry the sound pref back to the deck
|
|
1624
|
+
restack(); updateSoundBtn();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
el.querySelectorAll('[data-sv-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
1628
|
+
var muteBtn=el.querySelector('[data-sv-mute]');
|
|
1629
|
+
if(muteBtn) muteBtn.addEventListener('click',function(){ sound=!sound; syncSVSound(); });
|
|
1630
|
+
var prevBtn=el.querySelector('[data-sv-prev]'); if(prevBtn) prevBtn.addEventListener('click',function(){ step(-1); });
|
|
1631
|
+
var nextBtn=el.querySelector('[data-sv-next]'); if(nextBtn) nextBtn.addEventListener('click',function(){ step(1); });
|
|
1632
|
+
if(rejectBtn) rejectBtn.addEventListener('click',function(){ decideActive('skip'); });
|
|
1633
|
+
if(approveBtn) approveBtn.addEventListener('click',function(){ decideActive('fork'); });
|
|
1634
|
+
// tap the empty gutter (not the phone frame) closes the viewer
|
|
1635
|
+
track.addEventListener('click',function(e){
|
|
1636
|
+
var t=e.target;
|
|
1637
|
+
if(t&&t.classList&&(t.classList.contains('rk-discover-viewer-slide')||t===track)) close();
|
|
1638
|
+
});
|
|
1639
|
+
document.addEventListener('keydown',function(e){
|
|
1640
|
+
if(!open) return;
|
|
1641
|
+
if(e.key==='Escape'){ e.preventDefault(); close(); }
|
|
1642
|
+
else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); step(1); }
|
|
1643
|
+
else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); step(-1); }
|
|
1644
|
+
else if(e.key==='ArrowRight'){ e.preventDefault(); decideActive('fork'); }
|
|
1645
|
+
else if(e.key==='ArrowLeft'){ e.preventDefault(); decideActive('skip'); }
|
|
1646
|
+
});
|
|
1647
|
+
window.addEventListener('resize',function(){ if(open&&active>=0) track.scrollTop=active*track.clientHeight; });
|
|
1648
|
+
|
|
1649
|
+
return { openAt:openAt, close:close, sync:sync, isOpen:function(){ return open; } };
|
|
1650
|
+
})();
|
|
1651
|
+
|
|
1652
|
+
// Tap the front card → open the fullscreen viewer at that card.
|
|
1653
|
+
if(deck) deck.addEventListener('click',function(e){
|
|
1654
|
+
if(sw.busy||(swViewer&&swViewer.isOpen())||!nodes.length) return;
|
|
1655
|
+
var el=(e.target&&e.target.closest)?e.target.closest('.rk-discover-swipe'):null;
|
|
1656
|
+
if(!el||!el._card||el!==nodes[0]) return; // only the front card is interactive
|
|
1657
|
+
if(swViewer) swViewer.openAt(el._card.id);
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1423
1660
|
if(skipBtn) skipBtn.addEventListener('click',function(){ decide('skip'); });
|
|
1424
1661
|
if(forkBtn) forkBtn.addEventListener('click',function(){ decide('fork'); });
|
|
1425
1662
|
loadStack();
|
|
@@ -37,7 +37,7 @@ export function rkBrandTitle(raw) {
|
|
|
37
37
|
* Other pages stay reachable from the /reskin gallery (brand link). */
|
|
38
38
|
const SIDEBAR_NAV = [
|
|
39
39
|
{ slug: "discover", label: "Discover" },
|
|
40
|
-
{ slug: "chat", label: "
|
|
40
|
+
{ slug: "chat", label: "Create" },
|
|
41
41
|
{ slug: "library", label: "Library" },
|
|
42
42
|
{ slug: "calendar", label: "Calendar" },
|
|
43
43
|
{ slug: "help", label: "Guides" },
|
|
@@ -115,7 +115,7 @@ export function renderChatDock(activeSlug) {
|
|
|
115
115
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>
|
|
116
116
|
</button>
|
|
117
117
|
</div>
|
|
118
|
-
<div class="rk-aichat-files-mount" id="rkAichatFilesMount" data-rk-directory data-mode="attach"></div>
|
|
118
|
+
<div class="rk-aichat-files-mount" id="rkAichatFilesMount" data-rk-directory data-mode="attach" data-primary-click="preview"></div>
|
|
119
119
|
</div>
|
|
120
120
|
<div class="rk-aichat-history" id="rkAichatHistory" aria-label="Chat history">
|
|
121
121
|
<div class="rk-aichat-hist-head">Chat history</div>
|
|
@@ -127,6 +127,9 @@ export function renderChatDock(activeSlug) {
|
|
|
127
127
|
<button class="rk-aichat-tool" id="rkHistoryToggle" type="button" title="Chat history" aria-label="Toggle chat history" aria-pressed="false">
|
|
128
128
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l3 2"/></svg>
|
|
129
129
|
</button>
|
|
130
|
+
<button class="rk-aichat-tool" id="rkCopyThread" type="button" title="Copy chat ID" aria-label="Copy chat ID">
|
|
131
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
132
|
+
</button>
|
|
130
133
|
<button class="rk-aichat-tool" id="rkNewChat" type="button" title="New chat" aria-label="Start a new chat">
|
|
131
134
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>
|
|
132
135
|
</button>
|
|
@@ -306,7 +309,35 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
306
309
|
var ENDPOINT = '/api/v1/editor-chat', API_KEY = null, TEMPLATE = null, CACHED = null;
|
|
307
310
|
var THREADS_URL = '/api/v1/editor-chat/threads', TEMPLATE_ID = 'chat-brainstorm';
|
|
308
311
|
var convo = []; // [{role,text}] running conversation
|
|
309
|
-
|
|
312
|
+
// Minted up-front so the "Copy chat ID" tool always has something to copy
|
|
313
|
+
// (even before the first message) and so the id we copy is the SAME one the
|
|
314
|
+
// first send persists under. Reset to a fresh id on New chat, replaced by the
|
|
315
|
+
// real id when a saved thread is opened.
|
|
316
|
+
var threadId = genId('thread');
|
|
317
|
+
|
|
318
|
+
// Cross-page hand-off: /chat (or a future AI-driven page nav) can pass
|
|
319
|
+
// ?rk_chat=<threadId> to continue that EXACT conversation in this dock. Read it
|
|
320
|
+
// once, strip it from the URL (so reload / back-button doesn't re-trigger), and
|
|
321
|
+
// replay the thread as soon as boot is ready (see consumeHandoff, called from
|
|
322
|
+
// loadBoot's ready path). Also opens the panel below so the jump is seamless.
|
|
323
|
+
var handoffThread = null;
|
|
324
|
+
try {
|
|
325
|
+
var _hq = new URLSearchParams(window.location.search);
|
|
326
|
+
handoffThread = _hq.get('rk_chat') || null;
|
|
327
|
+
if (handoffThread) {
|
|
328
|
+
_hq.delete('rk_chat');
|
|
329
|
+
var _hqs = _hq.toString();
|
|
330
|
+
window.history.replaceState(window.history.state, '', window.location.pathname + (_hqs ? '?' + _hqs : '') + window.location.hash);
|
|
331
|
+
}
|
|
332
|
+
} catch (e) { handoffThread = null; }
|
|
333
|
+
// Replay a handed-off thread once the dock is authed + booted. openThread needs
|
|
334
|
+
// API_KEY (set by loadBoot), so this only fires from the boot-ready path.
|
|
335
|
+
function consumeHandoff() {
|
|
336
|
+
if (!handoffThread || BOOT_STATE !== 'ready') return;
|
|
337
|
+
var id = handoffThread; handoffThread = null;
|
|
338
|
+
setLeftMode(''); // show the conversation, not the Files/History drawer
|
|
339
|
+
openThread(id);
|
|
340
|
+
}
|
|
310
341
|
var busy = false;
|
|
311
342
|
var pendingAbort = null; // AbortController for the in-flight reply (Stop button)
|
|
312
343
|
|
|
@@ -385,6 +416,7 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
385
416
|
THREADS_URL = b.threadsUrl || THREADS_URL;
|
|
386
417
|
BOOT_STATE = 'ready';
|
|
387
418
|
loadThreads();
|
|
419
|
+
consumeHandoff();
|
|
388
420
|
})
|
|
389
421
|
.catch(function () { BOOT_STATE = 'error'; });
|
|
390
422
|
}
|
|
@@ -759,10 +791,27 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
759
791
|
return pump();
|
|
760
792
|
}
|
|
761
793
|
|
|
794
|
+
// Turn this turn's attachments into a text line the model can quote back as
|
|
795
|
+
// REST payload URLs (prompt_attachments / source_image_url / media_url), in
|
|
796
|
+
// addition to the binary file content parts. Mirrors the /editor React dock
|
|
797
|
+
// (buildAttachmentTransportText). Double-backslash-n = a browser-side newline
|
|
798
|
+
// (we live inside a script template literal, so a bare newline escape breaks it).
|
|
799
|
+
function buildAttachmentUrlText(atts) {
|
|
800
|
+
if (!atts || !atts.length) return '';
|
|
801
|
+
var lines = atts.map(function (a) { return '- ' + (a.fileName || 'file') + ' (' + (a.contentType || '') + '): ' + a.viewUrl; });
|
|
802
|
+
return '\\n\\nAttached file URLs for REST payloads:\\n' + lines.join('\\n') + '\\nUse these exact URLs as prompt_attachments for image generation, source_image_url for image edits, media_url for video slides, or reference attachment URLs when calling template routes.';
|
|
803
|
+
}
|
|
804
|
+
|
|
762
805
|
function sendMessage(text) {
|
|
763
806
|
if (busy) return;
|
|
764
807
|
text = (text || '').trim();
|
|
765
808
|
var picked = (typeof selected !== 'undefined' ? selected : []).slice();
|
|
809
|
+
// A pasted file may still be uploading to /temp — wait for it rather than
|
|
810
|
+
// silently dropping the attachment. Flash the in-flight chips as a hint.
|
|
811
|
+
if (picked.some(function (f) { return f && f.pending; })) {
|
|
812
|
+
picked.forEach(function (f) { if (f.pending) flashChip(f.id); });
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
766
815
|
var atts = picked.filter(function (f) { return f && f.viewUrl; }).map(function (f) {
|
|
767
816
|
return { id: String(f.id), fileName: f.name || 'file', contentType: f.contentType || 'application/octet-stream', viewUrl: f.viewUrl };
|
|
768
817
|
});
|
|
@@ -788,10 +837,22 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
788
837
|
// Attach a fresh <editor_context> to the current (last) user turn only, so the
|
|
789
838
|
// model sees the composition state without bloating persisted history.
|
|
790
839
|
var ctxBlock = editorContextBlock();
|
|
840
|
+
// Attachments (pasted files OR files picked from the directory explorer) must
|
|
841
|
+
// ride in the model messages as file content parts + a URL text line — the
|
|
842
|
+
// backend only feeds the model messages[].content, NOT user_message.attachments
|
|
843
|
+
// (which is persistence-only). Without this the AI never saw attached files.
|
|
844
|
+
var attUrlText = atts.length ? buildAttachmentUrlText(atts) : '';
|
|
791
845
|
var outMessages = convo.map(function (m, i) {
|
|
846
|
+
var isLast = i === convo.length - 1;
|
|
847
|
+
var isLastUser = isLast && m.role === 'user';
|
|
792
848
|
var t = m.text;
|
|
793
|
-
if (
|
|
794
|
-
|
|
849
|
+
if (isLastUser && attUrlText) t = t + attUrlText;
|
|
850
|
+
if (ctxBlock && isLastUser) t = t + ctxBlock;
|
|
851
|
+
var content = [{ type: 'text', text: t }];
|
|
852
|
+
if (isLastUser && atts.length) {
|
|
853
|
+
atts.forEach(function (a) { content.push({ type: 'file', data: a.viewUrl, mediaType: a.contentType || 'application/octet-stream' }); });
|
|
854
|
+
}
|
|
855
|
+
return { role: m.role, content: content };
|
|
795
856
|
});
|
|
796
857
|
var body = {
|
|
797
858
|
messages: outMessages,
|
|
@@ -859,7 +920,7 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
859
920
|
}
|
|
860
921
|
|
|
861
922
|
function resetConversation() {
|
|
862
|
-
convo = []; threadId =
|
|
923
|
+
convo = []; threadId = genId('thread');
|
|
863
924
|
if (log) { log.innerHTML = ''; bubble('is-ai', GREETING); }
|
|
864
925
|
if (typeof clearChips === 'function') clearChips();
|
|
865
926
|
}
|
|
@@ -1047,6 +1108,16 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
1047
1108
|
var historyToggle = document.getElementById('rkHistoryToggle');
|
|
1048
1109
|
var newChatBtn = document.getElementById('rkNewChat');
|
|
1049
1110
|
if (newChatBtn) newChatBtn.addEventListener('click', function () { if (!busy) newChat(); });
|
|
1111
|
+
// Copy the current chat thread id (handy for support / linking a conversation).
|
|
1112
|
+
// threadId is minted up-front, so this always has a real id to copy.
|
|
1113
|
+
var copyThreadBtn = document.getElementById('rkCopyThread');
|
|
1114
|
+
if (copyThreadBtn) copyThreadBtn.addEventListener('click', function () {
|
|
1115
|
+
var prevTitle = copyThreadBtn.title;
|
|
1116
|
+
copyText(String(threadId));
|
|
1117
|
+
copyThreadBtn.title = 'Copied chat ID';
|
|
1118
|
+
copyThreadBtn.classList.add('is-on');
|
|
1119
|
+
setTimeout(function () { copyThreadBtn.title = prevTitle; copyThreadBtn.classList.remove('is-on'); }, 1400);
|
|
1120
|
+
});
|
|
1050
1121
|
var histBody = document.getElementById('rkAichatHistBody');
|
|
1051
1122
|
var selected = [];
|
|
1052
1123
|
window.__rkDirectoryHost = {
|
|
@@ -1087,14 +1158,15 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
1087
1158
|
chipsEl.innerHTML = '';
|
|
1088
1159
|
selected.forEach(function (f, i) {
|
|
1089
1160
|
var chip = document.createElement('span');
|
|
1090
|
-
chip.className = 'rk-aichat-chip';
|
|
1161
|
+
chip.className = 'rk-aichat-chip' + (f.pending ? ' is-pending' : '');
|
|
1091
1162
|
chip.setAttribute('data-id', f.id);
|
|
1163
|
+
if (f.pending) { var sp = document.createElement('span'); sp.className = 'rk-aichat-chip-spin'; sp.setAttribute('aria-hidden', 'true'); chip.appendChild(sp); }
|
|
1092
1164
|
var label = document.createElement('button');
|
|
1093
1165
|
label.type = 'button';
|
|
1094
1166
|
label.className = 'rk-aichat-chip-label';
|
|
1095
|
-
label.title = 'Preview ' + f.name;
|
|
1167
|
+
label.title = f.pending ? 'Uploading ' + f.name + '…' : 'Preview ' + f.name;
|
|
1096
1168
|
label.textContent = f.name;
|
|
1097
|
-
if (f.viewUrl) label.addEventListener('click', function () { openFilePreview(selected,
|
|
1169
|
+
if (f.viewUrl && !f.pending) label.addEventListener('click', function () { openFilePreview(selected.filter(function (s) { return !s.pending; }), selected.filter(function (s) { return !s.pending; }).indexOf(f)); });
|
|
1098
1170
|
var x = document.createElement('button');
|
|
1099
1171
|
x.type = 'button';
|
|
1100
1172
|
x.setAttribute('aria-label', 'Remove ' + f.name);
|
|
@@ -1275,6 +1347,66 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
1275
1347
|
if (composerAttach) {
|
|
1276
1348
|
composerAttach.addEventListener('click', function () { setLeftMode(leftMode() === 'files' ? '' : 'files'); });
|
|
1277
1349
|
}
|
|
1350
|
+
|
|
1351
|
+
// ─── paste-to-attach: drop a copied image/file straight into the composer ───
|
|
1352
|
+
// Clipboard files (screenshot, copied image, "Copy image") get auto-uploaded
|
|
1353
|
+
// to the user's /temp folder, then attached as a chip just like a picked file.
|
|
1354
|
+
// We show a spinner chip immediately and swap it for the real file on success.
|
|
1355
|
+
function extNameFor(blob, idx) {
|
|
1356
|
+
var ct = (blob && blob.type) || '';
|
|
1357
|
+
var base = ct.indexOf('image/') === 0 ? 'pasted-image' : ct.indexOf('video/') === 0 ? 'pasted-video' : ct.indexOf('audio/') === 0 ? 'pasted-audio' : 'pasted-file';
|
|
1358
|
+
var ext = ct && ct.indexOf('/') > -1 ? ct.split('/')[1].split('+')[0].split(';')[0] : 'bin';
|
|
1359
|
+
if (ext === 'jpeg') ext = 'jpg';
|
|
1360
|
+
var stamp = Date.now().toString(36) + (idx ? '-' + idx : '');
|
|
1361
|
+
return base + '-' + stamp + '.' + ext;
|
|
1362
|
+
}
|
|
1363
|
+
function uploadPastedBlob(blob, fileName, pendingId) {
|
|
1364
|
+
var fd = new FormData();
|
|
1365
|
+
fd.append('file', blob, fileName);
|
|
1366
|
+
var headers = {}; if (API_KEY) headers['vidfarm-api-key'] = API_KEY;
|
|
1367
|
+
fetch(window.location.origin + '/api/v1/user/me/temporary-files/upload', {
|
|
1368
|
+
method: 'POST', credentials: 'same-origin', headers: headers, body: fd
|
|
1369
|
+
}).then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { if (!r.ok) throw new Error((j && j.error) || ('upload failed (' + r.status + ')')); return j; }); })
|
|
1370
|
+
.then(function (j) {
|
|
1371
|
+
var f = j && j.file;
|
|
1372
|
+
if (!f || !f.viewUrl) throw new Error('upload returned no file');
|
|
1373
|
+
var idx = selected.map(function (s) { return s.id; }).indexOf(pendingId);
|
|
1374
|
+
var item = { id: String(f.id), name: f.fileName || fileName, contentType: f.contentType || (blob && blob.type) || 'application/octet-stream', viewUrl: f.viewUrl };
|
|
1375
|
+
if (idx > -1) selected[idx] = item; else selected.push(item);
|
|
1376
|
+
renderChips();
|
|
1377
|
+
})
|
|
1378
|
+
.catch(function () {
|
|
1379
|
+
// A failed paste-upload just drops its chip — keep the chat log clean.
|
|
1380
|
+
selected = selected.filter(function (s) { return s.id !== pendingId; });
|
|
1381
|
+
renderChips();
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
function handlePasteFiles(e) {
|
|
1385
|
+
// Files-only drawer (on /chat) has no composer of its own — let that page's
|
|
1386
|
+
// composer own paste; here we only handle the dock's real chat composer.
|
|
1387
|
+
if (panel.classList.contains('is-files-only')) return;
|
|
1388
|
+
var dt = e.clipboardData || (window.clipboardData);
|
|
1389
|
+
if (!dt) return;
|
|
1390
|
+
var blobs = [];
|
|
1391
|
+
var items = dt.items;
|
|
1392
|
+
if (items && items.length) {
|
|
1393
|
+
for (var i = 0; i < items.length; i++) {
|
|
1394
|
+
if (items[i] && items[i].kind === 'file') { var b = items[i].getAsFile(); if (b) blobs.push(b); }
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (!blobs.length && dt.files && dt.files.length) { for (var k = 0; k < dt.files.length; k++) blobs.push(dt.files[k]); }
|
|
1398
|
+
if (!blobs.length) return; // plain-text paste → let the textarea handle it
|
|
1399
|
+
e.preventDefault();
|
|
1400
|
+
blobs.forEach(function (blob, idx) {
|
|
1401
|
+
var name = (blob && blob.name) || extNameFor(blob, idx);
|
|
1402
|
+
var pendingId = 'paste-' + Date.now().toString(36) + '-' + idx + '-' + Math.random().toString(36).slice(2, 6);
|
|
1403
|
+
selected.push({ id: pendingId, name: name, contentType: (blob && blob.type) || 'application/octet-stream', pending: true });
|
|
1404
|
+
renderChips();
|
|
1405
|
+
uploadPastedBlob(blob, name, pendingId);
|
|
1406
|
+
});
|
|
1407
|
+
if (input) input.focus();
|
|
1408
|
+
}
|
|
1409
|
+
if (input) input.addEventListener('paste', handlePasteFiles);
|
|
1278
1410
|
if (historyToggle) {
|
|
1279
1411
|
historyToggle.addEventListener('click', function () { setLeftMode(leftMode() === 'history' ? '' : 'history'); });
|
|
1280
1412
|
}
|
|
@@ -1307,6 +1439,11 @@ export const RESKIN_CHROME_SCRIPT = `(() => {
|
|
|
1307
1439
|
else if (reopen === '1') openPanel();
|
|
1308
1440
|
} catch (e) {}
|
|
1309
1441
|
|
|
1442
|
+
// A handed-off thread (?rk_chat=…) always opens the dock so the conversation
|
|
1443
|
+
// reappears immediately (openPanel → loadBoot → consumeHandoff replays it).
|
|
1444
|
+
// The wide editor dock force-opens itself just below, so skip it here.
|
|
1445
|
+
if (handoffThread && !isChatPage && !editorDockWide) openPanel();
|
|
1446
|
+
|
|
1310
1447
|
// Editor dock: force the panel open and pinned as a left column. Reuses the
|
|
1311
1448
|
// normal openPanel path so the live /chat-dock/boot fetch + threads + files
|
|
1312
1449
|
// all wire up exactly as on the reskin pages.
|
|
@@ -1442,7 +1579,7 @@ ${renderChatDock(active)}
|
|
|
1442
1579
|
${RESKIN_FONT_LINKS}
|
|
1443
1580
|
<style>${RESKIN_CSS}${pageCss}</style>
|
|
1444
1581
|
</head>
|
|
1445
|
-
<body class="rk-root${chrome === "full" ? " rk-has-sidebar" : ""}">
|
|
1582
|
+
<body class="rk-root${chrome === "full" ? " rk-has-sidebar" : ""}${input.account?.email?.trim() ? " rk-signed-in" : ""}">
|
|
1446
1583
|
${shell}${script}
|
|
1447
1584
|
</body>
|
|
1448
1585
|
</html>`;
|