feat: chat in gap between Q1 and Q2, output directly under input

This commit is contained in:
aedes
2026-04-22 02:53:58 +03:00
parent a070cfcc1f
commit 8590ab0556

View File

@@ -121,37 +121,117 @@ async function main() {
console.log(`[mirror] networkidle reached. extra settle…`);
await page.waitForTimeout(4000);
// Pixel-perfect full-page screenshot
// Measure question block positions (document coords, CSS pixels)
const quePositions = await page.$$eval('.que', els =>
els.map(el => {
const r = el.getBoundingClientRect();
return { top: r.top + window.scrollY, bottom: r.top + window.scrollY + r.height };
})
);
const pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
const pageWidth = await page.evaluate(() => document.documentElement.scrollWidth);
console.log(`[mirror] page ${pageWidth}x${pageHeight} CSS px | questions:`, quePositions);
fs.mkdirSync(OUT, { recursive: true });
const pngPath = path.join(OUT, 'page.png');
await page.screenshot({ path: pngPath, fullPage: true, type: 'png' });
const pngSize = fs.statSync(pngPath).size;
console.log(`[mirror] screenshot: ${pngPath} (${pngSize} bytes)`);
console.log(`[mirror] screenshot: ${pngPath} (${fs.statSync(pngPath).size} bytes)`);
// Compute actual page width for the wrapper (so image scales to its captured size)
const pageWidth = await page.evaluate(() => document.documentElement.scrollWidth);
// 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);
}
const pageTitle = (await page.title()).replace(/[<>]/g, '');
const wrapperHtml = `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=${pageWidth},initial-scale=1">
<title>${await page.title()}</title>
<title>${pageTitle}</title>
<style>
html, body { margin:0; padding:0; background:#fff; }
body { padding-bottom:55px; }
.q-page { display:block; width:100%; max-width:${pageWidth}px; margin:0 auto; }
.q-page img { display:block; width:100%; height:auto; }
html, body { margin:0; padding:0; background:#fff; font-family: Arial, sans-serif; }
.q-wrap { position: relative; width: 100%; max-width: ${pageWidth}px; margin: 0 auto; }
.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%;
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 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;
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; }
</style>
</head>
<body>
<div class="q-page"><img src="/q/page.png" alt=""></div>
${CHAT_OVERLAY}
<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>
</div>
<script>
(function(){
var i=document.getElementById('_chatIn'), o=document.getElementById('_chatOut'), busy=false;
function md(t){ var h=t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); 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;}}
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});
});
})();
</script>
</body>
</html>`;
fs.writeFileSync(path.join(OUT, 'index.html'), wrapperHtml, 'utf8');
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')}`);
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')} — chat at y=${chatTop}px`);
await browser.close();
console.log('[mirror] done');