feat: chat in gap between Q1 and Q2, output directly under input
This commit is contained in:
@@ -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,'&').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;}}
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user