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' });
|
await page.screenshot({ path: pngPath, fullPage: true, type: 'png' });
|
||||||
console.log(`[mirror] screenshot: ${pngPath} (${fs.statSync(pngPath).size} bytes)`);
|
console.log(`[mirror] screenshot: ${pngPath} (${fs.statSync(pngPath).size} bytes)`);
|
||||||
|
|
||||||
// Decide gap position — mid-point between question 1 and question 2
|
// One chat slot per gap (between Q1-Q2, between Q2-Q3, ...)
|
||||||
let chatTop = Math.round(pageHeight / 3);
|
const slots = [];
|
||||||
if (quePositions.length >= 2) {
|
for (let i = 0; i < quePositions.length - 1; i++) {
|
||||||
chatTop = Math.round((quePositions[0].bottom + quePositions[1].top) / 2 - 30);
|
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 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>
|
const wrapperHtml = `<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
@@ -163,81 +170,129 @@ async function main() {
|
|||||||
.q-wrap img.bg { display:block; width:100%; height:auto; }
|
.q-wrap img.bg { display:block; width:100%; height:auto; }
|
||||||
.q-chat {
|
.q-chat {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 250px;
|
||||||
transform: translateX(-50%);
|
width: 520px;
|
||||||
top: ${chatTop}px;
|
|
||||||
width: 720px;
|
|
||||||
max-width: 94%;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
.q-chat textarea {
|
.q-chat .c-in {
|
||||||
display: block;
|
display: block; width: 100%;
|
||||||
width: 100%;
|
border: 1px solid #e0e0e0; border-radius: 4px;
|
||||||
border: 1px solid #e0e0e0;
|
background: #fff; color: #333;
|
||||||
border-radius: 4px;
|
font-size: 14px; line-height: 1.5;
|
||||||
background: #fff;
|
padding: 8px 12px; resize: none;
|
||||||
color: #333;
|
height: 34px; font-family: inherit;
|
||||||
font-size: 14px;
|
outline: none; box-sizing: border-box;
|
||||||
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 .c-in::placeholder { color: #bbb; }
|
||||||
.q-chat .out {
|
.q-chat .c-out {
|
||||||
margin-top: 8px;
|
margin-top: 6px; padding: 6px 12px;
|
||||||
min-height: 0;
|
font-size: 12px; color: #888; line-height: 1.5;
|
||||||
padding: 6px 12px;
|
white-space: pre-wrap; word-break: break-word;
|
||||||
font-size: 12px;
|
|
||||||
color: #888;
|
|
||||||
line-height: 1.5;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
.q-chat .out:empty { padding: 0; }
|
.q-chat .c-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 .c-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 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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="q-wrap">
|
<div class="q-wrap">
|
||||||
<img class="bg" src="/q/page.png" alt="">
|
<img class="bg" src="/q/page.png" alt="">
|
||||||
<div class="q-chat">
|
${chatBlocks}
|
||||||
<textarea id="_chatIn" autocomplete="off" spellcheck="false" rows="1" placeholder=""></textarea>
|
|
||||||
<div class="out" id="_chatOut"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<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 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 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);});}
|
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='';
|
function initChat(block) {
|
||||||
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();
|
var i = block.querySelector('.c-in');
|
||||||
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;
|
var o = block.querySelector('.c-out');
|
||||||
try{var obj=JSON.parse(raw);if(obj.text){full+=obj.text;o.innerHTML=md(full);}}catch(e){}}}}catch(e){}finally{busy=false;}}
|
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){
|
i.addEventListener('paste', async function(e){
|
||||||
var items = Array.from((e.clipboardData || window.clipboardData)?.items || []);
|
var items = Array.from((e.clipboardData || window.clipboardData)?.items || []);
|
||||||
var files = []; var textP = Promise.resolve('');
|
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);}
|
for (var j = 0; j < items.length; j++){
|
||||||
if(files.length===0){setTimeout(function(){var t=i.value.trim();if(t)send({text:t});},50);return;}
|
var it = items[j];
|
||||||
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});
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
||||||
fs.writeFileSync(path.join(OUT, 'index.html'), wrapperHtml, 'utf8');
|
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();
|
await browser.close();
|
||||||
console.log('[mirror] done');
|
console.log('[mirror] done');
|
||||||
|
|||||||
Reference in New Issue
Block a user