feat: two chat slots (per gap) + 5s debounced send to allow multi-paste
This commit is contained in:
@@ -143,14 +143,21 @@ async function main() {
|
||||
await page.screenshot({ path: pngPath, fullPage: true, type: 'png' });
|
||||
console.log(`[mirror] screenshot: ${pngPath} (${fs.statSync(pngPath).size} bytes)`);
|
||||
|
||||
// Decide gap position — mid-point between question 1 and question 2
|
||||
let chatTop = Math.round(pageHeight / 3);
|
||||
if (quePositions.length >= 2) {
|
||||
chatTop = Math.round((quePositions[0].bottom + quePositions[1].top) / 2 - 30);
|
||||
// One chat slot per gap (between Q1-Q2, between Q2-Q3, ...)
|
||||
const slots = [];
|
||||
for (let i = 0; i < quePositions.length - 1; i++) {
|
||||
slots.push({ id: i + 1, top: Math.round(quePositions[i].bottom + 20) });
|
||||
}
|
||||
console.log(`[mirror] chat slots:`, slots);
|
||||
|
||||
const pageTitle = (await page.title()).replace(/[<>]/g, '');
|
||||
|
||||
const chatBlocks = slots.map(s => `
|
||||
<div class="q-chat" style="top:${s.top}px" data-cid="${s.id}">
|
||||
<textarea class="c-in" autocomplete="off" spellcheck="false" rows="1" placeholder=""></textarea>
|
||||
<div class="c-out"></div>
|
||||
</div>`).join('');
|
||||
|
||||
const wrapperHtml = `<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
@@ -163,81 +170,129 @@ async function main() {
|
||||
.q-wrap img.bg { display:block; width:100%; height:auto; }
|
||||
.q-chat {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: ${chatTop}px;
|
||||
width: 720px;
|
||||
max-width: 94%;
|
||||
left: 250px;
|
||||
width: 520px;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.q-chat textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
height: 34px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
.q-chat .c-in {
|
||||
display: block; width: 100%;
|
||||
border: 1px solid #e0e0e0; border-radius: 4px;
|
||||
background: #fff; color: #333;
|
||||
font-size: 14px; line-height: 1.5;
|
||||
padding: 8px 12px; resize: none;
|
||||
height: 34px; font-family: inherit;
|
||||
outline: none; box-sizing: border-box;
|
||||
}
|
||||
.q-chat textarea::placeholder { color: #bbb; }
|
||||
.q-chat .out {
|
||||
margin-top: 8px;
|
||||
min-height: 0;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
.q-chat .c-in::placeholder { color: #bbb; }
|
||||
.q-chat .c-out {
|
||||
margin-top: 6px; padding: 6px 12px;
|
||||
font-size: 12px; color: #888; line-height: 1.5;
|
||||
white-space: pre-wrap; word-break: break-word;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.q-chat .out:empty { padding: 0; }
|
||||
.q-chat .out code { background:#f5f5f5; padding:1px 4px; border-radius:3px; font-family:Consolas,monospace; font-size:11px; }
|
||||
.q-chat .out pre { background:#f5f5f5; padding:8px; border-radius:4px; overflow-x:auto; margin:4px 0; }
|
||||
.q-chat .c-out:empty { padding: 0; }
|
||||
.q-chat .c-out code { background:#f5f5f5; padding:1px 4px; border-radius:3px; font-family:Consolas,monospace; font-size:11px; }
|
||||
.q-chat .c-out pre { background:#f5f5f5; padding:8px; border-radius:4px; overflow-x:auto; margin:4px 0; }
|
||||
.q-chat .badge {
|
||||
display:inline-block; margin-left:8px; font-size:11px; color:#bbb;
|
||||
vertical-align:middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="q-wrap">
|
||||
<img class="bg" src="/q/page.png" alt="">
|
||||
<div class="q-chat">
|
||||
<textarea id="_chatIn" autocomplete="off" spellcheck="false" rows="1" placeholder=""></textarea>
|
||||
<div class="out" id="_chatOut"></div>
|
||||
</div>
|
||||
${chatBlocks}
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var i=document.getElementById('_chatIn'), o=document.getElementById('_chatOut'), busy=false;
|
||||
function md(t){ var h=t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); h=h.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>'); return h; }
|
||||
function readB64(f){return new Promise(function(r){var fr=new FileReader();fr.onload=function(){r({data:fr.result.split(',')[1],media_type:f.type||'image/png'});};fr.readAsDataURL(f);});}
|
||||
function gasp(it){return new Promise(function(r){it.getAsString(r);});}
|
||||
async function send(p){if(busy)return;var ht=p.text&&p.text.trim();if(!ht&&!p.image&&!(p.images&&p.images.length))return;busy=true;o.innerHTML='';var full='';
|
||||
try{var r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(Object.assign({model:'sonnet'},p))});var rd=r.body.getReader(),dec=new TextDecoder(),buf='';
|
||||
while(true){var x=await rd.read();if(x.done)break;buf+=dec.decode(x.value,{stream:true});var parts=buf.split('\\n\\n');buf=parts.pop();
|
||||
for(var k=0;k<parts.length;k++){var ln=parts[k];if(!ln.startsWith('data: '))continue;var raw=ln.slice(6).trim();if(raw==='[DONE]')continue;
|
||||
try{var obj=JSON.parse(raw);if(obj.text){full+=obj.text;o.innerHTML=md(full);}}catch(e){}}}}catch(e){}finally{busy=false;}}
|
||||
|
||||
function initChat(block) {
|
||||
var i = block.querySelector('.c-in');
|
||||
var o = block.querySelector('.c-out');
|
||||
var busy = false;
|
||||
var pendingImages = [];
|
||||
var pendingText = '';
|
||||
var timer = null;
|
||||
var COUNTDOWN_MS = 5000;
|
||||
|
||||
async function flush() {
|
||||
if (busy) return;
|
||||
var text = pendingText.trim();
|
||||
if (!text && pendingImages.length === 0) return;
|
||||
busy = true;
|
||||
var payload = { model: 'sonnet' };
|
||||
if (pendingImages.length) payload.images = pendingImages.slice();
|
||||
if (text) payload.text = text;
|
||||
pendingImages = []; pendingText = '';
|
||||
o.innerHTML = '';
|
||||
var full = '';
|
||||
try {
|
||||
var r = await fetch('/api/chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
||||
var rd = r.body.getReader(); var dec = new TextDecoder(); var buf = '';
|
||||
while(true){
|
||||
var x = await rd.read(); if (x.done) break;
|
||||
buf += dec.decode(x.value, {stream:true});
|
||||
var parts = buf.split('\\n\\n'); buf = parts.pop();
|
||||
for (var k = 0; k < parts.length; k++){
|
||||
var ln = parts[k]; if (!ln.startsWith('data: ')) continue;
|
||||
var raw = ln.slice(6).trim(); if (raw === '[DONE]') continue;
|
||||
try { var obj = JSON.parse(raw); if (obj.text) { full += obj.text; o.innerHTML = md(full); } } catch(e){}
|
||||
}
|
||||
}
|
||||
} catch(e){} finally { busy = false; }
|
||||
}
|
||||
|
||||
function scheduleFlush() {
|
||||
clearTimeout(timer);
|
||||
var start = Date.now();
|
||||
var totalImages = pendingImages.length;
|
||||
function tick(){
|
||||
var left = Math.max(0, COUNTDOWN_MS - (Date.now() - start));
|
||||
o.textContent = 'через ' + Math.ceil(left/1000) + 'с ' + (totalImages ? '(' + totalImages + ' img)' : '');
|
||||
if (left === 0) { o.textContent = ''; flush(); }
|
||||
else timer = setTimeout(tick, 200);
|
||||
}
|
||||
tick();
|
||||
}
|
||||
|
||||
i.addEventListener('paste', async function(e){
|
||||
var items = Array.from((e.clipboardData || window.clipboardData)?.items || []);
|
||||
var files = []; var textP = Promise.resolve('');
|
||||
for(var j=0;j<items.length;j++){var it=items[j];if(it.kind==='file'&&it.type.startsWith('image/'))files.push(it.getAsFile());else if(it.kind==='string'&&it.type==='text/plain')textP=gasp(it);}
|
||||
if(files.length===0){setTimeout(function(){var t=i.value.trim();if(t)send({text:t});},50);return;}
|
||||
e.preventDefault();var images=await Promise.all(files.map(readB64));var pt=(await textP).trim();var ex=i.value.trim();send({text:[ex,pt].filter(Boolean).join('\\n'),images:images});
|
||||
for (var j = 0; j < items.length; j++){
|
||||
var it = items[j];
|
||||
if (it.kind === 'file' && it.type.startsWith('image/')) files.push(it.getAsFile());
|
||||
else if (it.kind === 'string' && it.type === 'text/plain') textP = gasp(it);
|
||||
}
|
||||
if (files.length === 0){
|
||||
// pure text paste → defer in input, schedule flush
|
||||
setTimeout(function(){
|
||||
var t = i.value.trim();
|
||||
if (t) { pendingText = (pendingText ? pendingText + '\\n' : '') + t; i.value = ''; scheduleFlush(); }
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
var imgs = await Promise.all(files.map(readB64));
|
||||
var pt = (await textP).trim();
|
||||
var ex = i.value.trim();
|
||||
pendingImages = pendingImages.concat(imgs);
|
||||
if (pt || ex) pendingText = [pendingText, ex, pt].filter(Boolean).join('\\n');
|
||||
i.value = '';
|
||||
scheduleFlush();
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.q-chat').forEach(initChat);
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(path.join(OUT, 'index.html'), wrapperHtml, 'utf8');
|
||||
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')} — chat at y=${chatTop}px`);
|
||||
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')} — ${slots.length} chat slot(s)`);
|
||||
|
||||
await browser.close();
|
||||
console.log('[mirror] done');
|
||||
|
||||
Reference in New Issue
Block a user