Spaces:
Running
Running
Update index.html
Browse files- index.html +263 -164
index.html
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
|
| 2 |
<!DOCTYPE html>
|
| 3 |
<html lang="en">
|
| 4 |
<head>
|
|
@@ -55,6 +54,7 @@ window.MathJax = {
|
|
| 55 |
--code-border: #e6e0d2;
|
| 56 |
--inline-code-bg: #f2efe8;
|
| 57 |
}
|
|
|
|
| 58 |
body.dark {
|
| 59 |
--desk-bg: #2c2a27;
|
| 60 |
--desk-dot: #3a3733;
|
|
@@ -148,7 +148,7 @@ body{
|
|
| 148 |
#settingsBtn:hover{transform:rotate(20deg)scale(1.15)}
|
| 149 |
|
| 150 |
/* ───────── header ───────── */
|
| 151 |
-
.header{text-align:center;margin-bottom:34px;padding-bottom:18px;border-bottom:1px solid rgba(0,0,0,.05)}
|
| 152 |
h1{font-family:'Libre Baskerville',serif;margin:0;font-size:30px;letter-spacing:.5px}
|
| 153 |
.subtitle{font-family:'PT Mono',monospace;font-size:14px;color:#666;margin-top:6px;letter-spacing:1px}
|
| 154 |
|
|
@@ -273,30 +273,29 @@ th{font-weight:600}
|
|
| 273 |
background:var(--code-bg);
|
| 274 |
border:1px solid var(--code-border);
|
| 275 |
padding:12px;border-radius:6px;
|
| 276 |
-
margin-top: 8px;
|
| 277 |
}
|
| 278 |
|
| 279 |
-
/*
|
| 280 |
.copy-btn{
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
| 285 |
margin-left:8px;
|
| 286 |
-
|
| 287 |
color:var(--paper-text);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
}
|
| 289 |
-
.copy-btn:
|
| 290 |
-
|
| 291 |
-
}
|
| 292 |
-
.qa-block{
|
| 293 |
-
margin-bottom:1.5em;
|
| 294 |
-
}
|
| 295 |
-
.qa-header{
|
| 296 |
-
display:flex;
|
| 297 |
-
align-items:baseline;
|
| 298 |
-
margin-bottom:0.4em;
|
| 299 |
-
}
|
| 300 |
|
| 301 |
/* responsive & print */
|
| 302 |
@media(max-width:768px){
|
|
@@ -345,11 +344,11 @@ th{font-weight:600}
|
|
| 345 |
<label for="nebiusKey">Nebius API Key:</label>
|
| 346 |
<input type="password" id="nebiusKey" placeholder="Enter your Nebius API key" autocomplete="off">
|
| 347 |
<div class="api-hint">Used for OCR image processing</div>
|
| 348 |
-
|
| 349 |
<label for="cerebrasKey">Cerebras API Key:</label>
|
| 350 |
<input type="password" id="cerebrasKey" placeholder="Enter your Cerebras API key" autocomplete="off">
|
| 351 |
<div class="api-hint">Used for solving questions</div>
|
| 352 |
-
|
| 353 |
<div class="modal-buttons">
|
| 354 |
<button class="btn-cancel" onclick="closeSettings()">Cancel</button>
|
| 355 |
<button class="btn-save" onclick="saveSettings()">Save</button>
|
|
@@ -378,9 +377,9 @@ function processContent(text){
|
|
| 378 |
const keep=m=>(store.push(m),PL(idx++));
|
| 379 |
|
| 380 |
text = text
|
| 381 |
-
.replace(/\\\[[\s\S]*?\\\]/g, keep)
|
| 382 |
-
.replace(/\$\$[\s\S]*?\$\$/g, keep)
|
| 383 |
-
.replace(/\\\([\s\S]*?\\\)/g, keep)
|
| 384 |
.replace(/\$([^\$\n]+?)\$/g, keep); // $ ... $
|
| 385 |
|
| 386 |
let html = marked.parse(text);
|
|
@@ -388,9 +387,13 @@ function processContent(text){
|
|
| 388 |
content.innerHTML = html;
|
| 389 |
|
| 390 |
if(window.MathJax?.typesetPromise){
|
| 391 |
-
MathJax.typesetPromise([content])
|
| 392 |
-
.
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
}
|
| 395 |
|
| 396 |
/* ======= Image to Base64 converter ======= */
|
|
@@ -398,6 +401,7 @@ async function imageToBase64(file) {
|
|
| 398 |
return new Promise((resolve, reject) => {
|
| 399 |
const reader = new FileReader();
|
| 400 |
reader.onload = () => {
|
|
|
|
| 401 |
if (reader.result && reader.result.includes(',')) {
|
| 402 |
const base64 = reader.result.split(',')[1];
|
| 403 |
resolve(base64);
|
|
@@ -433,7 +437,7 @@ async function ocrImage(base64Image) {
|
|
| 433 |
messages: [
|
| 434 |
{
|
| 435 |
role: 'system',
|
| 436 |
-
content: '
|
| 437 |
},
|
| 438 |
{
|
| 439 |
role: 'user',
|
|
@@ -441,7 +445,7 @@ async function ocrImage(base64Image) {
|
|
| 441 |
{ type: 'text', text: 'Image:' },
|
| 442 |
{
|
| 443 |
type: 'image_url',
|
| 444 |
-
image_url: { url: `data:image/png;base64,${base64Image}` }
|
| 445 |
}
|
| 446 |
]
|
| 447 |
}
|
|
@@ -450,77 +454,120 @@ async function ocrImage(base64Image) {
|
|
| 450 |
});
|
| 451 |
|
| 452 |
if (!response.ok) {
|
| 453 |
-
const
|
| 454 |
-
throw new Error(`OCR API error: ${response.status}
|
| 455 |
}
|
| 456 |
|
| 457 |
const data = await response.json();
|
| 458 |
return data.choices[0].message.content;
|
| 459 |
-
} catch (
|
| 460 |
-
console.error('OCR
|
| 461 |
-
alert('Error during OCR: ' +
|
| 462 |
return null;
|
| 463 |
}
|
| 464 |
}
|
| 465 |
|
| 466 |
-
/* ======= UI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
function beginStreamingUI(question){
|
|
|
|
| 468 |
content.innerHTML = `
|
| 469 |
-
<div
|
| 470 |
-
<
|
|
|
|
|
|
|
| 471 |
<div class="mono-stream" id="qStream"></div>
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
<div class="mono-stream" id="aStream">(generating...)</div>
|
| 477 |
</div>`;
|
| 478 |
const qEl = document.getElementById('qStream');
|
| 479 |
const aEl = document.getElementById('aStream');
|
| 480 |
-
qEl.textContent = question;
|
| 481 |
-
aEl.textContent = '';
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
}
|
| 484 |
|
| 485 |
-
/* ======= Final render after streaming ======= */
|
| 486 |
function finalizeStreaming(question, fullAnswer){
|
| 487 |
-
|
| 488 |
-
const
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
</div>
|
| 494 |
-
<div class="qa-block">
|
| 495 |
-
<div class="qa-header"><strong>Answer</strong> <button class="copy-btn" data-copy-id="finalAnswer" title="Copy answer">📋</button></div>
|
| 496 |
-
<div class="qa-content" id="finalAnswer">${answerHTML}</div>
|
| 497 |
-
</div>`;
|
| 498 |
-
content.innerHTML = finalHTML;
|
| 499 |
-
|
| 500 |
-
if (window.MathJax?.typesetPromise) {
|
| 501 |
-
MathJax.typesetPromise([content]).then(hideProcessing)
|
| 502 |
-
.catch(e=>{console.error('MathJax error:',e);hideProcessing();});
|
| 503 |
-
} else {
|
| 504 |
-
hideProcessing();
|
| 505 |
-
}
|
| 506 |
}
|
| 507 |
|
| 508 |
-
/* =======
|
| 509 |
-
content.addEventListener('click', e => {
|
| 510 |
-
const btn = e.target.closest('.copy-btn');
|
| 511 |
-
if (!btn) return;
|
| 512 |
-
const targetId = btn.dataset.copyId;
|
| 513 |
-
const target = document.getElementById(targetId);
|
| 514 |
-
if (!target) return;
|
| 515 |
-
|
| 516 |
-
navigator.clipboard.writeText(target.innerText).then(() => {
|
| 517 |
-
const original = btn.textContent;
|
| 518 |
-
btn.textContent = '✅';
|
| 519 |
-
setTimeout(() => btn.textContent = original, 1200);
|
| 520 |
-
}).catch(err => console.error('Copy failed', err));
|
| 521 |
-
});
|
| 522 |
-
|
| 523 |
-
/* ======= Solve with Cerebras API (streaming) ======= */
|
| 524 |
async function solveQuestion(question) {
|
| 525 |
const cerebrasKey = localStorage.getItem('cerebras-api-key');
|
| 526 |
if (!cerebrasKey) {
|
|
@@ -529,7 +576,7 @@ async function solveQuestion(question) {
|
|
| 529 |
}
|
| 530 |
|
| 531 |
showProcessing('Solving the question...');
|
| 532 |
-
const ui = beginStreamingUI(question);
|
| 533 |
|
| 534 |
try {
|
| 535 |
const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
|
|
@@ -543,70 +590,83 @@ async function solveQuestion(question) {
|
|
| 543 |
model: 'gpt-oss-120b',
|
| 544 |
stream: true,
|
| 545 |
max_tokens: 65536,
|
| 546 |
-
temperature: 0.1,
|
| 547 |
-
reasoning_effort: 'medium',
|
|
|
|
| 548 |
messages: [
|
| 549 |
-
{ role: 'system', content: 'Solve this Question. Provide a clear, step
|
| 550 |
-
{ role: 'user',
|
| 551 |
]
|
| 552 |
})
|
| 553 |
});
|
| 554 |
|
| 555 |
if (!response.ok) {
|
| 556 |
-
const
|
| 557 |
-
throw new Error(`Cerebras API error: ${response.status}
|
| 558 |
}
|
| 559 |
|
| 560 |
const reader = response.body.getReader();
|
| 561 |
const decoder = new TextDecoder();
|
| 562 |
let fullAnswer = '';
|
| 563 |
-
let buffer = '';
|
| 564 |
-
let
|
| 565 |
-
const
|
| 566 |
|
| 567 |
-
const
|
|
|
|
| 568 |
ui.aEl.textContent = fullAnswer;
|
| 569 |
-
|
| 570 |
};
|
| 571 |
|
| 572 |
while (true) {
|
| 573 |
-
const {done, value} = await reader.read();
|
| 574 |
if (done) break;
|
| 575 |
|
| 576 |
-
buffer += decoder.decode(value, {stream:true});
|
|
|
|
| 577 |
const events = buffer.split('\n\n');
|
| 578 |
-
buffer = events.pop() || '';
|
| 579 |
|
| 580 |
-
for (const
|
| 581 |
-
|
|
|
|
| 582 |
if (!dataLine) continue;
|
| 583 |
-
|
| 584 |
-
|
|
|
|
| 585 |
|
| 586 |
try {
|
| 587 |
-
const
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
}
|
| 596 |
} catch (e) {
|
| 597 |
-
//
|
|
|
|
| 598 |
}
|
| 599 |
}
|
| 600 |
}
|
| 601 |
|
| 602 |
-
//
|
| 603 |
-
|
|
|
|
|
|
|
| 604 |
finalizeStreaming(question, fullAnswer);
|
| 605 |
return fullAnswer;
|
| 606 |
-
} catch (
|
| 607 |
-
console.error('
|
| 608 |
-
alert('Error
|
| 609 |
-
hideProcessing();
|
| 610 |
return null;
|
| 611 |
}
|
| 612 |
}
|
|
@@ -614,103 +674,142 @@ async function solveQuestion(question) {
|
|
| 614 |
/* ======= Process image pipeline ======= */
|
| 615 |
async function processImage(file) {
|
| 616 |
try {
|
|
|
|
| 617 |
const base64 = await imageToBase64(file);
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
await
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
hideProcessing();
|
| 625 |
}
|
| 626 |
}
|
| 627 |
|
| 628 |
-
/* =======
|
| 629 |
-
document.addEventListener('paste', async e => {
|
| 630 |
-
|
| 631 |
-
const
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
|
|
|
| 635 |
);
|
| 636 |
-
|
| 637 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
e.preventDefault();
|
| 639 |
-
|
|
|
|
| 640 |
const items = Array.from(e.clipboardData.items);
|
| 641 |
-
const
|
| 642 |
-
|
| 643 |
-
if (
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
} else {
|
|
|
|
| 648 |
const txt = e.clipboardData.getData('text/plain');
|
| 649 |
if (txt.trim()) processContent(txt);
|
| 650 |
}
|
| 651 |
});
|
| 652 |
|
| 653 |
-
/* ======= Settings modal
|
| 654 |
-
const settingsBtn
|
| 655 |
const settingsModal = document.getElementById('settingsModal');
|
| 656 |
-
const nebiusKeyInput
|
| 657 |
const cerebrasKeyInput = document.getElementById('cerebrasKey');
|
| 658 |
|
| 659 |
settingsBtn.addEventListener('click', () => {
|
| 660 |
-
|
|
|
|
| 661 |
cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || '';
|
| 662 |
settingsModal.classList.add('show');
|
| 663 |
});
|
| 664 |
|
| 665 |
-
function closeSettings(){
|
|
|
|
|
|
|
| 666 |
|
| 667 |
-
function saveSettings(){
|
| 668 |
-
const
|
| 669 |
-
const
|
| 670 |
-
|
| 671 |
-
if (
|
|
|
|
|
|
|
| 672 |
closeSettings();
|
| 673 |
alert('API keys saved successfully!');
|
| 674 |
}
|
| 675 |
|
| 676 |
-
|
| 677 |
-
settingsModal.addEventListener('click', e => {
|
| 678 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
|
| 680 |
-
/* placeholder click
|
| 681 |
content.addEventListener('click',()=>{
|
| 682 |
-
const ph
|
| 683 |
-
if
|
| 684 |
ph.style.transform='scale(.97)';
|
| 685 |
ph.style.transition='transform .12s';
|
| 686 |
setTimeout(()=>ph.style.transform='scale(1)',120);
|
| 687 |
}
|
| 688 |
});
|
| 689 |
|
| 690 |
-
/* fade
|
| 691 |
document.addEventListener('DOMContentLoaded',()=>{
|
| 692 |
const sheet=document.querySelector('.container');
|
| 693 |
sheet.style.opacity='0';
|
| 694 |
-
setTimeout(()=>{sheet.style.transition='opacity .6s ease';sheet.style.opacity='1'
|
| 695 |
});
|
| 696 |
|
| 697 |
-
/* theme toggler */
|
| 698 |
-
const
|
| 699 |
-
const prefersDark
|
| 700 |
-
const savedTheme
|
| 701 |
|
| 702 |
initTheme();
|
| 703 |
-
|
| 704 |
document.body.classList.toggle('dark');
|
| 705 |
updateIcon();
|
| 706 |
-
localStorage.setItem('note-theme',
|
| 707 |
});
|
| 708 |
function initTheme(){
|
| 709 |
-
if
|
| 710 |
-
|
|
|
|
|
|
|
|
|
|
| 711 |
updateIcon();
|
| 712 |
}
|
| 713 |
-
function updateIcon(){
|
|
|
|
|
|
|
| 714 |
</script>
|
| 715 |
</body>
|
| 716 |
</html>
|
|
|
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
|
|
|
| 54 |
--code-border: #e6e0d2;
|
| 55 |
--inline-code-bg: #f2efe8;
|
| 56 |
}
|
| 57 |
+
|
| 58 |
body.dark {
|
| 59 |
--desk-bg: #2c2a27;
|
| 60 |
--desk-dot: #3a3733;
|
|
|
|
| 148 |
#settingsBtn:hover{transform:rotate(20deg)scale(1.15)}
|
| 149 |
|
| 150 |
/* ───────── header ───────── */
|
| 151 |
+
.header{text-align:center;margin-bottom:34px;padding-bottom:18px;border-bottom:1px solid rgba(0,0,0,.05')}
|
| 152 |
h1{font-family:'Libre Baskerville',serif;margin:0;font-size:30px;letter-spacing:.5px}
|
| 153 |
.subtitle{font-family:'PT Mono',monospace;font-size:14px;color:#666;margin-top:6px;letter-spacing:1px}
|
| 154 |
|
|
|
|
| 273 |
background:var(--code-bg);
|
| 274 |
border:1px solid var(--code-border);
|
| 275 |
padding:12px;border-radius:6px;
|
| 276 |
+
margin-top: 8px; /* Add some spacing */
|
| 277 |
}
|
| 278 |
|
| 279 |
+
/* Copy button styles */
|
| 280 |
.copy-btn{
|
| 281 |
+
display:inline-flex;
|
| 282 |
+
align-items:center;
|
| 283 |
+
justify-content:center;
|
| 284 |
+
gap:6px;
|
| 285 |
+
font-family:'PT Mono',monospace;
|
| 286 |
+
font-size:13px;
|
| 287 |
+
padding:6px 8px;
|
| 288 |
margin-left:8px;
|
| 289 |
+
background: #efefef;
|
| 290 |
color:var(--paper-text);
|
| 291 |
+
border:1px solid var(--code-border);
|
| 292 |
+
border-radius:6px;
|
| 293 |
+
cursor:pointer;
|
| 294 |
+
user-select:none;
|
| 295 |
}
|
| 296 |
+
.copy-btn.small{ padding:4px 6px; font-size:12px; border-radius:5px; }
|
| 297 |
+
.copy-btn:active{ transform: translateY(1px); }
|
| 298 |
+
body.dark .copy-btn{ background:#444; color:var(--paper-text); border-color:var(--code-border); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
/* responsive & print */
|
| 301 |
@media(max-width:768px){
|
|
|
|
| 344 |
<label for="nebiusKey">Nebius API Key:</label>
|
| 345 |
<input type="password" id="nebiusKey" placeholder="Enter your Nebius API key" autocomplete="off">
|
| 346 |
<div class="api-hint">Used for OCR image processing</div>
|
| 347 |
+
|
| 348 |
<label for="cerebrasKey">Cerebras API Key:</label>
|
| 349 |
<input type="password" id="cerebrasKey" placeholder="Enter your Cerebras API key" autocomplete="off">
|
| 350 |
<div class="api-hint">Used for solving questions</div>
|
| 351 |
+
|
| 352 |
<div class="modal-buttons">
|
| 353 |
<button class="btn-cancel" onclick="closeSettings()">Cancel</button>
|
| 354 |
<button class="btn-save" onclick="saveSettings()">Save</button>
|
|
|
|
| 377 |
const keep=m=>(store.push(m),PL(idx++));
|
| 378 |
|
| 379 |
text = text
|
| 380 |
+
.replace(/\\\[[\s\S]*?\\\]/g, keep) // \[ ... \]
|
| 381 |
+
.replace(/\$\$[\s\S]*?\$\$/g, keep) // $$ ... $$
|
| 382 |
+
.replace(/\\\([\s\S]*?\\\)/g, keep) // \( ... \)
|
| 383 |
.replace(/\$([^\$\n]+?)\$/g, keep); // $ ... $
|
| 384 |
|
| 385 |
let html = marked.parse(text);
|
|
|
|
| 387 |
content.innerHTML = html;
|
| 388 |
|
| 389 |
if(window.MathJax?.typesetPromise){
|
| 390 |
+
return MathJax.typesetPromise([content])
|
| 391 |
+
.then(()=>{ hideProcessing(); })
|
| 392 |
+
.catch(e=>{ console.error('MathJax error:',e); hideProcessing(); });
|
| 393 |
+
}else{
|
| 394 |
+
hideProcessing();
|
| 395 |
+
return Promise.resolve();
|
| 396 |
+
}
|
| 397 |
}
|
| 398 |
|
| 399 |
/* ======= Image to Base64 converter ======= */
|
|
|
|
| 401 |
return new Promise((resolve, reject) => {
|
| 402 |
const reader = new FileReader();
|
| 403 |
reader.onload = () => {
|
| 404 |
+
// Ensure it's a valid data URL and extract base64 part
|
| 405 |
if (reader.result && reader.result.includes(',')) {
|
| 406 |
const base64 = reader.result.split(',')[1];
|
| 407 |
resolve(base64);
|
|
|
|
| 437 |
messages: [
|
| 438 |
{
|
| 439 |
role: 'system',
|
| 440 |
+
content: 'You are an OCR assistant. Extract the question as plain text and preserve math using LaTeX delimiters ($...$ or $$...$$) only. Under no circumstances output LaTeX list environments or list commands such as \\begin{itemize}, \\end{itemize}, \\begin{enumerate}, \\end{enumerate}, or \\item. If the original image uses bullets or numbered lists, convert them into plain text lines labeled (a), (b), (c) or 1., 2., etc., without using LaTeX list environments. Do not attempt to solve the question. Output only the question text with required math markup and no additional commentary or headings.'
|
| 441 |
},
|
| 442 |
{
|
| 443 |
role: 'user',
|
|
|
|
| 445 |
{ type: 'text', text: 'Image:' },
|
| 446 |
{
|
| 447 |
type: 'image_url',
|
| 448 |
+
image_url: { url: `data:image/png;base64,${base64Image}` } // Assuming PNG, adjust if needed
|
| 449 |
}
|
| 450 |
]
|
| 451 |
}
|
|
|
|
| 454 |
});
|
| 455 |
|
| 456 |
if (!response.ok) {
|
| 457 |
+
const errorText = await response.text();
|
| 458 |
+
throw new Error(`OCR API error: ${response.status} - ${errorText}`);
|
| 459 |
}
|
| 460 |
|
| 461 |
const data = await response.json();
|
| 462 |
return data.choices[0].message.content;
|
| 463 |
+
} catch (error) {
|
| 464 |
+
console.error('OCR Error:', error);
|
| 465 |
+
alert('Error during OCR: ' + error.message);
|
| 466 |
return null;
|
| 467 |
}
|
| 468 |
}
|
| 469 |
|
| 470 |
+
/* ======= UI Helpers for Streaming + Copy ======= */
|
| 471 |
+
|
| 472 |
+
/* Copy helper with fallback and small "Copied!" feedback */
|
| 473 |
+
async function copyTextToClipboard(text, btn) {
|
| 474 |
+
if (!btn) { // make a dummy if none supplied
|
| 475 |
+
btn = document.createElement('button');
|
| 476 |
+
}
|
| 477 |
+
const origLabel = btn.textContent;
|
| 478 |
+
try {
|
| 479 |
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
| 480 |
+
await navigator.clipboard.writeText(text);
|
| 481 |
+
} else {
|
| 482 |
+
const ta = document.createElement('textarea');
|
| 483 |
+
ta.value = text;
|
| 484 |
+
ta.setAttribute('readonly', '');
|
| 485 |
+
ta.style.position = 'absolute';
|
| 486 |
+
ta.style.left = '-9999px';
|
| 487 |
+
document.body.appendChild(ta);
|
| 488 |
+
ta.select();
|
| 489 |
+
document.execCommand('copy');
|
| 490 |
+
document.body.removeChild(ta);
|
| 491 |
+
}
|
| 492 |
+
btn.textContent = 'Copied!';
|
| 493 |
+
setTimeout(()=>{ btn.textContent = origLabel; }, 1100);
|
| 494 |
+
} catch (e) {
|
| 495 |
+
console.error('Copy failed', e);
|
| 496 |
+
alert('Copy failed: ' + e.message);
|
| 497 |
+
}
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
function beginStreamingUI(question){
|
| 501 |
+
// Show a lightweight, non-MathJax view while the model streams
|
| 502 |
content.innerHTML = `
|
| 503 |
+
<div>
|
| 504 |
+
<p><strong>Question</strong>:
|
| 505 |
+
<button class="copy-btn small" id="copyQBtn" title="Copy question">📋</button>
|
| 506 |
+
</p>
|
| 507 |
<div class="mono-stream" id="qStream"></div>
|
| 508 |
+
<hr style="opacity:.35; margin: 20px 0;">
|
| 509 |
+
<p><strong>Answer</strong>:
|
| 510 |
+
<button class="copy-btn small" id="copyABtn" title="Copy answer">📋</button>
|
| 511 |
+
</p>
|
| 512 |
<div class="mono-stream" id="aStream">(generating...)</div>
|
| 513 |
</div>`;
|
| 514 |
const qEl = document.getElementById('qStream');
|
| 515 |
const aEl = document.getElementById('aStream');
|
| 516 |
+
qEl.textContent = question; // plain text now; pretty render later
|
| 517 |
+
aEl.textContent = ''; // clear "(generating...)"
|
| 518 |
+
|
| 519 |
+
// attach copy handlers
|
| 520 |
+
const qBtn = document.getElementById('copyQBtn');
|
| 521 |
+
const aBtn = document.getElementById('copyABtn');
|
| 522 |
+
if (qBtn) qBtn.addEventListener('click', ()=> copyTextToClipboard(question, qBtn));
|
| 523 |
+
if (aBtn) aBtn.addEventListener('click', ()=> copyTextToClipboard(aEl.textContent, aBtn));
|
| 524 |
+
|
| 525 |
+
return { qEl, aEl, qBtn, aBtn };
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
/* After final render, add copy buttons next to the rendered Question/Answer
|
| 529 |
+
(copies the original plain-text question/answer passed in) */
|
| 530 |
+
function addCopyButtonsToRenderedContent(question, answer){
|
| 531 |
+
// Find paragraphs containing <strong>Question</strong> or <strong>Answer</strong>
|
| 532 |
+
const pEls = content.querySelectorAll('p');
|
| 533 |
+
pEls.forEach(p => {
|
| 534 |
+
const st = p.querySelector('strong');
|
| 535 |
+
if(!st) return;
|
| 536 |
+
const label = st.textContent.replace(':','').trim().toLowerCase();
|
| 537 |
+
if(label.startsWith('question')){
|
| 538 |
+
// remove existing copy if present
|
| 539 |
+
const existing = p.querySelector('.copy-btn');
|
| 540 |
+
if(existing) existing.remove();
|
| 541 |
+
const btn = document.createElement('button');
|
| 542 |
+
btn.className = 'copy-btn small';
|
| 543 |
+
btn.title = 'Copy question text';
|
| 544 |
+
btn.innerHTML = '📋';
|
| 545 |
+
btn.addEventListener('click', ()=> copyTextToClipboard(question, btn));
|
| 546 |
+
// insert right after the strong label
|
| 547 |
+
st.after(btn);
|
| 548 |
+
} else if(label.startsWith('answer')){
|
| 549 |
+
const existing = p.querySelector('.copy-btn');
|
| 550 |
+
if(existing) existing.remove();
|
| 551 |
+
const btn = document.createElement('button');
|
| 552 |
+
btn.className = 'copy-btn small';
|
| 553 |
+
btn.title = 'Copy answer text';
|
| 554 |
+
btn.innerHTML = '📋';
|
| 555 |
+
btn.addEventListener('click', ()=> copyTextToClipboard(answer, btn));
|
| 556 |
+
st.after(btn);
|
| 557 |
+
}
|
| 558 |
+
});
|
| 559 |
}
|
| 560 |
|
|
|
|
| 561 |
function finalizeStreaming(question, fullAnswer){
|
| 562 |
+
// One single heavy render (Markdown + MathJax) at the end
|
| 563 |
+
const formatted = `**Question**: ${question}\n\n**Answer**: ${fullAnswer}`;
|
| 564 |
+
processContent(formatted).then(()=> {
|
| 565 |
+
// After render + MathJax finished, add copy buttons that copy the *raw* texts
|
| 566 |
+
addCopyButtonsToRenderedContent(question, fullAnswer);
|
| 567 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
}
|
| 569 |
|
| 570 |
+
/* ======= Solve with Cerebras API (Streaming Optimization) ======= */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
async function solveQuestion(question) {
|
| 572 |
const cerebrasKey = localStorage.getItem('cerebras-api-key');
|
| 573 |
if (!cerebrasKey) {
|
|
|
|
| 576 |
}
|
| 577 |
|
| 578 |
showProcessing('Solving the question...');
|
| 579 |
+
const ui = beginStreamingUI(question); // Prepare the lightweight streaming UI
|
| 580 |
|
| 581 |
try {
|
| 582 |
const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
|
|
|
|
| 590 |
model: 'gpt-oss-120b',
|
| 591 |
stream: true,
|
| 592 |
max_tokens: 65536,
|
| 593 |
+
temperature: 0.1, // Set temperature to 0.1
|
| 594 |
+
reasoning_effort: 'medium', // Set reasoning_effort to 'medium'
|
| 595 |
+
// top_p: 1, // Removed as per user's request
|
| 596 |
messages: [
|
| 597 |
+
{ role: 'system', content: 'Solve this Question. Provide a clear, step-by-step solution.' },
|
| 598 |
+
{ role: 'user', content: question }
|
| 599 |
]
|
| 600 |
})
|
| 601 |
});
|
| 602 |
|
| 603 |
if (!response.ok) {
|
| 604 |
+
const errorText = await response.text();
|
| 605 |
+
throw new Error(`Cerebras API error: ${response.status} - ${errorText}`);
|
| 606 |
}
|
| 607 |
|
| 608 |
const reader = response.body.getReader();
|
| 609 |
const decoder = new TextDecoder();
|
| 610 |
let fullAnswer = '';
|
| 611 |
+
let buffer = ''; // buffer for partial SSE frames
|
| 612 |
+
let lastFlushTime = 0;
|
| 613 |
+
const flushThrottle = 120; // milliseconds to wait between DOM updates
|
| 614 |
|
| 615 |
+
const flushUI = () => {
|
| 616 |
+
// Update the lightweight streaming area without MathJax
|
| 617 |
ui.aEl.textContent = fullAnswer;
|
| 618 |
+
lastFlushTime = performance.now();
|
| 619 |
};
|
| 620 |
|
| 621 |
while (true) {
|
| 622 |
+
const { done, value } = await reader.read();
|
| 623 |
if (done) break;
|
| 624 |
|
| 625 |
+
buffer += decoder.decode(value, { stream: true });
|
| 626 |
+
// SSE events are typically separated by '\n\n'
|
| 627 |
const events = buffer.split('\n\n');
|
| 628 |
+
buffer = events.pop() || ''; // Keep any incomplete event for the next chunk
|
| 629 |
|
| 630 |
+
for (const evt of events) {
|
| 631 |
+
// Find the 'data:' line, which contains the JSON payload
|
| 632 |
+
const dataLine = evt.split('\n').find(line => line.trim().startsWith('data: '));
|
| 633 |
if (!dataLine) continue;
|
| 634 |
+
|
| 635 |
+
const data = dataLine.slice(6).trim(); // Remove 'data: ' prefix
|
| 636 |
+
if (data === '[DONE]') continue; // End of stream marker
|
| 637 |
|
| 638 |
try {
|
| 639 |
+
const parsed = JSON.parse(data);
|
| 640 |
+
// Extract content, being flexible with potential API response structures
|
| 641 |
+
const deltaContent = parsed.choices?.[0]?.delta?.content
|
| 642 |
+
?? parsed.choices?.[0]?.message?.content
|
| 643 |
+
?? parsed.choices?.[0]?.text // Some APIs might use 'text'
|
| 644 |
+
?? '';
|
| 645 |
+
|
| 646 |
+
if (deltaContent) {
|
| 647 |
+
fullAnswer += deltaContent;
|
| 648 |
+
// Throttle DOM updates to prevent excessive rendering and jank
|
| 649 |
+
if (performance.now() - lastFlushTime > flushThrottle) {
|
| 650 |
+
flushUI();
|
| 651 |
+
}
|
| 652 |
}
|
| 653 |
} catch (e) {
|
| 654 |
+
// Ignore errors parsing JSON chunks if it's just partial data
|
| 655 |
+
console.error('Error parsing stream chunk data:', e, 'Chunk:', data);
|
| 656 |
}
|
| 657 |
}
|
| 658 |
}
|
| 659 |
|
| 660 |
+
// Final flush to ensure all streamed content is displayed in the lightweight view
|
| 661 |
+
flushUI();
|
| 662 |
+
|
| 663 |
+
// Once streaming is complete, perform the final, heavier render with Markdown and MathJax
|
| 664 |
finalizeStreaming(question, fullAnswer);
|
| 665 |
return fullAnswer;
|
| 666 |
+
} catch (error) {
|
| 667 |
+
console.error('Solving Error:', error);
|
| 668 |
+
alert('Error during solving: ' + error.message);
|
| 669 |
+
hideProcessing(); // Ensure the processing indicator is hidden on error
|
| 670 |
return null;
|
| 671 |
}
|
| 672 |
}
|
|
|
|
| 674 |
/* ======= Process image pipeline ======= */
|
| 675 |
async function processImage(file) {
|
| 676 |
try {
|
| 677 |
+
// Convert image to base64
|
| 678 |
const base64 = await imageToBase64(file);
|
| 679 |
+
|
| 680 |
+
// OCR the image
|
| 681 |
+
const ocrText = await ocrImage(base64);
|
| 682 |
+
if (!ocrText) {
|
| 683 |
+
hideProcessing();
|
| 684 |
+
return;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
// Solve the question
|
| 688 |
+
const answer = await solveQuestion(ocrText);
|
| 689 |
+
// The solveQuestion function now handles hiding the processing indicator
|
| 690 |
+
// unless an error occurred, in which case it was hidden earlier.
|
| 691 |
+
|
| 692 |
+
} catch (error) {
|
| 693 |
+
console.error('Image processing error:', error);
|
| 694 |
+
alert('Error processing image: ' + error.message);
|
| 695 |
hideProcessing();
|
| 696 |
}
|
| 697 |
}
|
| 698 |
|
| 699 |
+
/* ======= FIXED paste listener - allows normal paste in input fields ======= */
|
| 700 |
+
document.addEventListener('paste', async (e) => {
|
| 701 |
+
// Check if we're pasting into an input, textarea, or contenteditable element
|
| 702 |
+
const activeElement = document.activeElement;
|
| 703 |
+
const isInputField = activeElement && (
|
| 704 |
+
activeElement.tagName === 'INPUT' ||
|
| 705 |
+
activeElement.tagName === 'TEXTAREA' ||
|
| 706 |
+
activeElement.isContentEditable === true // Use isContentEditable for modern check
|
| 707 |
);
|
| 708 |
+
|
| 709 |
+
// If pasting into an input field, let the browser handle it normally
|
| 710 |
+
if (isInputField) {
|
| 711 |
+
return; // Don't prevent default, let normal paste happen
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
// Otherwise, handle custom paste logic
|
| 715 |
e.preventDefault();
|
| 716 |
+
|
| 717 |
+
// Check for image files first
|
| 718 |
const items = Array.from(e.clipboardData.items);
|
| 719 |
+
const imageItem = items.find(item => item.type.startsWith('image/'));
|
| 720 |
+
|
| 721 |
+
if (imageItem) {
|
| 722 |
+
// Handle image paste
|
| 723 |
+
const file = imageItem.getAsFile();
|
| 724 |
+
if (file) {
|
| 725 |
+
await processImage(file);
|
| 726 |
+
} else {
|
| 727 |
+
alert("Could not get image file from clipboard.");
|
| 728 |
+
}
|
| 729 |
} else {
|
| 730 |
+
// Handle text paste (existing functionality)
|
| 731 |
const txt = e.clipboardData.getData('text/plain');
|
| 732 |
if (txt.trim()) processContent(txt);
|
| 733 |
}
|
| 734 |
});
|
| 735 |
|
| 736 |
+
/* ======= Settings modal functions ======= */
|
| 737 |
+
const settingsBtn = document.getElementById('settingsBtn');
|
| 738 |
const settingsModal = document.getElementById('settingsModal');
|
| 739 |
+
const nebiusKeyInput = document.getElementById('nebiusKey');
|
| 740 |
const cerebrasKeyInput = document.getElementById('cerebrasKey');
|
| 741 |
|
| 742 |
settingsBtn.addEventListener('click', () => {
|
| 743 |
+
// Load existing keys
|
| 744 |
+
nebiusKeyInput.value = localStorage.getItem('nebius-api-key') || '';
|
| 745 |
cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || '';
|
| 746 |
settingsModal.classList.add('show');
|
| 747 |
});
|
| 748 |
|
| 749 |
+
function closeSettings() {
|
| 750 |
+
settingsModal.classList.remove('show');
|
| 751 |
+
}
|
| 752 |
|
| 753 |
+
function saveSettings() {
|
| 754 |
+
const nebiusKey = nebiusKeyInput.value.trim();
|
| 755 |
+
const cerebrasKey = cerebrasKeyInput.value.trim();
|
| 756 |
+
|
| 757 |
+
if (nebiusKey) localStorage.setItem('nebius-api-key', nebiusKey);
|
| 758 |
+
if (cerebrasKey) localStorage.setItem('cerebras-api-key', cerebrasKey);
|
| 759 |
+
|
| 760 |
closeSettings();
|
| 761 |
alert('API keys saved successfully!');
|
| 762 |
}
|
| 763 |
|
| 764 |
+
// Close modal on escape or background click
|
| 765 |
+
settingsModal.addEventListener('click', (e) => {
|
| 766 |
+
if (e.target === settingsModal) closeSettings();
|
| 767 |
+
});
|
| 768 |
+
document.addEventListener('keydown', (e) => {
|
| 769 |
+
if (e.key === 'Escape' && settingsModal.classList.contains('show')) {
|
| 770 |
+
closeSettings();
|
| 771 |
+
}
|
| 772 |
+
});
|
| 773 |
|
| 774 |
+
/* small "bounce" on placeholder click */
|
| 775 |
content.addEventListener('click',()=>{
|
| 776 |
+
const ph=content.querySelector('.placeholder');
|
| 777 |
+
if(ph){
|
| 778 |
ph.style.transform='scale(.97)';
|
| 779 |
ph.style.transition='transform .12s';
|
| 780 |
setTimeout(()=>ph.style.transform='scale(1)',120);
|
| 781 |
}
|
| 782 |
});
|
| 783 |
|
| 784 |
+
/* smooth fade in */
|
| 785 |
document.addEventListener('DOMContentLoaded',()=>{
|
| 786 |
const sheet=document.querySelector('.container');
|
| 787 |
sheet.style.opacity='0';
|
| 788 |
+
setTimeout(()=>{sheet.style.transition='opacity .6s ease';sheet.style.opacity='1'},80);
|
| 789 |
});
|
| 790 |
|
| 791 |
+
/* ======= theme toggler ======= */
|
| 792 |
+
const btn = document.getElementById('themeToggle');
|
| 793 |
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
| 794 |
+
const savedTheme = localStorage.getItem('note-theme');
|
| 795 |
|
| 796 |
initTheme();
|
| 797 |
+
btn.addEventListener('click',()=>{
|
| 798 |
document.body.classList.toggle('dark');
|
| 799 |
updateIcon();
|
| 800 |
+
localStorage.setItem('note-theme',document.body.classList.contains('dark')?'dark':'light');
|
| 801 |
});
|
| 802 |
function initTheme(){
|
| 803 |
+
if(savedTheme){
|
| 804 |
+
document.body.classList.toggle('dark',savedTheme==='dark');
|
| 805 |
+
}else if(prefersDark.matches){
|
| 806 |
+
document.body.classList.add('dark');
|
| 807 |
+
}
|
| 808 |
updateIcon();
|
| 809 |
}
|
| 810 |
+
function updateIcon(){
|
| 811 |
+
btn.textContent=document.body.classList.contains('dark')?'☀️':'🌙';
|
| 812 |
+
}
|
| 813 |
</script>
|
| 814 |
</body>
|
| 815 |
</html>
|