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…`);
|
console.log(`[mirror] networkidle reached. extra settle…`);
|
||||||
await page.waitForTimeout(4000);
|
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 });
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
const pngPath = path.join(OUT, 'page.png');
|
const pngPath = path.join(OUT, 'page.png');
|
||||||
await page.screenshot({ path: pngPath, fullPage: true, type: 'png' });
|
await page.screenshot({ path: pngPath, fullPage: true, type: 'png' });
|
||||||
const pngSize = fs.statSync(pngPath).size;
|
console.log(`[mirror] screenshot: ${pngPath} (${fs.statSync(pngPath).size} bytes)`);
|
||||||
console.log(`[mirror] screenshot: ${pngPath} (${pngSize} bytes)`);
|
|
||||||
|
|
||||||
// Compute actual page width for the wrapper (so image scales to its captured size)
|
// Decide gap position — mid-point between question 1 and question 2
|
||||||
const pageWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
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>
|
const wrapperHtml = `<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=${pageWidth},initial-scale=1">
|
<meta name="viewport" content="width=${pageWidth},initial-scale=1">
|
||||||
<title>${await page.title()}</title>
|
<title>${pageTitle}</title>
|
||||||
<style>
|
<style>
|
||||||
html, body { margin:0; padding:0; background:#fff; }
|
html, body { margin:0; padding:0; background:#fff; font-family: Arial, sans-serif; }
|
||||||
body { padding-bottom:55px; }
|
.q-wrap { position: relative; width: 100%; max-width: ${pageWidth}px; margin: 0 auto; }
|
||||||
.q-page { display:block; width:100%; max-width:${pageWidth}px; margin:0 auto; }
|
.q-wrap img.bg { display:block; width:100%; height:auto; }
|
||||||
.q-page img { 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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="q-page"><img src="/q/page.png" alt=""></div>
|
<div class="q-wrap">
|
||||||
${CHAT_OVERLAY}
|
<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>
|
</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')}`);
|
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')} — chat at y=${chatTop}px`);
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
console.log('[mirror] done');
|
console.log('[mirror] done');
|
||||||
|
|||||||
Reference in New Issue
Block a user