Spaces:
Running
Running
Update index.html
Browse files- index.html +167 -165
index.html
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
|
@@ -54,7 +55,6 @@ window.MathJax = {
|
|
| 54 |
--code-border: #e6e0d2;
|
| 55 |
--inline-code-bg: #f2efe8;
|
| 56 |
}
|
| 57 |
-
|
| 58 |
body.dark {
|
| 59 |
--desk-bg: #2c2a27;
|
| 60 |
--desk-dot: #3a3733;
|
|
@@ -273,7 +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 |
/* responsive & print */
|
|
@@ -323,11 +345,11 @@ th{font-weight:600}
|
|
| 323 |
<label for="nebiusKey">Nebius API Key:</label>
|
| 324 |
<input type="password" id="nebiusKey" placeholder="Enter your Nebius API key" autocomplete="off">
|
| 325 |
<div class="api-hint">Used for OCR image processing</div>
|
| 326 |
-
|
| 327 |
<label for="cerebrasKey">Cerebras API Key:</label>
|
| 328 |
<input type="password" id="cerebrasKey" placeholder="Enter your Cerebras API key" autocomplete="off">
|
| 329 |
<div class="api-hint">Used for solving questions</div>
|
| 330 |
-
|
| 331 |
<div class="modal-buttons">
|
| 332 |
<button class="btn-cancel" onclick="closeSettings()">Cancel</button>
|
| 333 |
<button class="btn-save" onclick="saveSettings()">Save</button>
|
|
@@ -356,9 +378,9 @@ function processContent(text){
|
|
| 356 |
const keep=m=>(store.push(m),PL(idx++));
|
| 357 |
|
| 358 |
text = text
|
| 359 |
-
.replace(/\\\[[\s\S]*?\\\]/g, keep)
|
| 360 |
-
.replace(/\$\$[\s\S]*?\$\$/g, keep)
|
| 361 |
-
.replace(/\\\([\s\S]*?\\\)/g, keep)
|
| 362 |
.replace(/\$([^\$\n]+?)\$/g, keep); // $ ... $
|
| 363 |
|
| 364 |
let html = marked.parse(text);
|
|
@@ -376,7 +398,6 @@ async function imageToBase64(file) {
|
|
| 376 |
return new Promise((resolve, reject) => {
|
| 377 |
const reader = new FileReader();
|
| 378 |
reader.onload = () => {
|
| 379 |
-
// Ensure it's a valid data URL and extract base64 part
|
| 380 |
if (reader.result && reader.result.includes(',')) {
|
| 381 |
const base64 = reader.result.split(',')[1];
|
| 382 |
resolve(base64);
|
|
@@ -412,7 +433,7 @@ async function ocrImage(base64Image) {
|
|
| 412 |
messages: [
|
| 413 |
{
|
| 414 |
role: 'system',
|
| 415 |
-
content: 'GIVE AS TEXT WITH LATEX like $this$ or $$this$$. DO NOT SOLVE THE QUESTION. DO NOT OUTPUT ANYTHING BUT THE FORMAT OF THE QUESTION.'
|
| 416 |
},
|
| 417 |
{
|
| 418 |
role: 'user',
|
|
@@ -420,7 +441,7 @@ async function ocrImage(base64Image) {
|
|
| 420 |
{ type: 'text', text: 'Image:' },
|
| 421 |
{
|
| 422 |
type: 'image_url',
|
| 423 |
-
image_url: { url: `data:image/png;base64,${base64Image}` }
|
| 424 |
}
|
| 425 |
]
|
| 426 |
}
|
|
@@ -429,44 +450,77 @@ async function ocrImage(base64Image) {
|
|
| 429 |
});
|
| 430 |
|
| 431 |
if (!response.ok) {
|
| 432 |
-
const
|
| 433 |
-
throw new Error(`OCR API error: ${response.status}
|
| 434 |
}
|
| 435 |
|
| 436 |
const data = await response.json();
|
| 437 |
return data.choices[0].message.content;
|
| 438 |
-
} catch (
|
| 439 |
-
console.error('OCR
|
| 440 |
-
alert('Error during OCR: ' +
|
| 441 |
return null;
|
| 442 |
}
|
| 443 |
}
|
| 444 |
|
| 445 |
-
/* ======= UI
|
| 446 |
function beginStreamingUI(question){
|
| 447 |
-
// Show a lightweight, non-MathJax view while the model streams
|
| 448 |
content.innerHTML = `
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
|
|
|
| 452 |
<hr style="opacity:.35; margin: 20px 0;">
|
| 453 |
-
<
|
| 454 |
-
|
| 455 |
-
|
|
|
|
| 456 |
const qEl = document.getElementById('qStream');
|
| 457 |
const aEl = document.getElementById('aStream');
|
| 458 |
-
qEl.textContent = question;
|
| 459 |
-
aEl.textContent = '';
|
| 460 |
return { qEl, aEl };
|
| 461 |
}
|
| 462 |
|
|
|
|
| 463 |
function finalizeStreaming(question, fullAnswer){
|
| 464 |
-
|
| 465 |
-
const
|
| 466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
}
|
| 468 |
|
| 469 |
-
/* =======
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
async function solveQuestion(question) {
|
| 471 |
const cerebrasKey = localStorage.getItem('cerebras-api-key');
|
| 472 |
if (!cerebrasKey) {
|
|
@@ -475,97 +529,84 @@ async function solveQuestion(question) {
|
|
| 475 |
}
|
| 476 |
|
| 477 |
showProcessing('Solving the question...');
|
| 478 |
-
const ui = beginStreamingUI(question);
|
| 479 |
|
| 480 |
try {
|
| 481 |
const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
|
| 482 |
method: 'POST',
|
| 483 |
headers: {
|
| 484 |
'Content-Type': 'application/json',
|
| 485 |
-
'Accept': 'text/event-stream',
|
| 486 |
'Authorization': `Bearer ${cerebrasKey}`
|
| 487 |
},
|
| 488 |
body: JSON.stringify({
|
| 489 |
model: 'gpt-oss-120b',
|
| 490 |
stream: true,
|
| 491 |
max_tokens: 65536,
|
| 492 |
-
temperature: 0.1,
|
| 493 |
-
reasoning_effort: 'medium',
|
| 494 |
-
// top_p: 1, // Removed as per user's request
|
| 495 |
messages: [
|
| 496 |
-
{ role: 'system', content: 'Solve this Question. Provide a clear, step
|
| 497 |
-
{ role: 'user',
|
| 498 |
]
|
| 499 |
})
|
| 500 |
});
|
| 501 |
|
| 502 |
if (!response.ok) {
|
| 503 |
-
const
|
| 504 |
-
throw new Error(`Cerebras API error: ${response.status}
|
| 505 |
}
|
| 506 |
|
| 507 |
const reader = response.body.getReader();
|
| 508 |
const decoder = new TextDecoder();
|
| 509 |
let fullAnswer = '';
|
| 510 |
-
let buffer = '';
|
| 511 |
-
let
|
| 512 |
-
const
|
| 513 |
|
| 514 |
-
const
|
| 515 |
-
// Update the lightweight streaming area without MathJax
|
| 516 |
ui.aEl.textContent = fullAnswer;
|
| 517 |
-
|
| 518 |
};
|
| 519 |
|
| 520 |
while (true) {
|
| 521 |
-
const {
|
| 522 |
if (done) break;
|
| 523 |
|
| 524 |
-
buffer += decoder.decode(value, {
|
| 525 |
-
// SSE events are typically separated by '\n\n'
|
| 526 |
const events = buffer.split('\n\n');
|
| 527 |
-
buffer = events.pop() || '';
|
| 528 |
|
| 529 |
-
for (const
|
| 530 |
-
|
| 531 |
-
const dataLine = evt.split('\n').find(line => line.trim().startsWith('data: '));
|
| 532 |
if (!dataLine) continue;
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
if (data === '[DONE]') continue; // End of stream marker
|
| 536 |
|
| 537 |
try {
|
| 538 |
-
const
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
fullAnswer += deltaContent;
|
| 547 |
-
// Throttle DOM updates to prevent excessive rendering and jank
|
| 548 |
-
if (performance.now() - lastFlushTime > flushThrottle) {
|
| 549 |
-
flushUI();
|
| 550 |
-
}
|
| 551 |
}
|
| 552 |
} catch (e) {
|
| 553 |
-
//
|
| 554 |
-
console.error('Error parsing stream chunk data:', e, 'Chunk:', data);
|
| 555 |
}
|
| 556 |
}
|
| 557 |
}
|
| 558 |
|
| 559 |
-
//
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
// Once streaming is complete, perform the final, heavier render with Markdown and MathJax
|
| 563 |
finalizeStreaming(question, fullAnswer);
|
| 564 |
return fullAnswer;
|
| 565 |
-
} catch (
|
| 566 |
-
console.error('
|
| 567 |
-
alert('Error
|
| 568 |
-
hideProcessing();
|
| 569 |
return null;
|
| 570 |
}
|
| 571 |
}
|
|
@@ -573,142 +614,103 @@ async function solveQuestion(question) {
|
|
| 573 |
/* ======= Process image pipeline ======= */
|
| 574 |
async function processImage(file) {
|
| 575 |
try {
|
| 576 |
-
// Convert image to base64
|
| 577 |
const base64 = await imageToBase64(file);
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
}
|
| 585 |
-
|
| 586 |
-
// Solve the question
|
| 587 |
-
const answer = await solveQuestion(ocrText);
|
| 588 |
-
// The solveQuestion function now handles hiding the processing indicator
|
| 589 |
-
// unless an error occurred, in which case it was hidden earlier.
|
| 590 |
-
|
| 591 |
-
} catch (error) {
|
| 592 |
-
console.error('Image processing error:', error);
|
| 593 |
-
alert('Error processing image: ' + error.message);
|
| 594 |
hideProcessing();
|
| 595 |
}
|
| 596 |
}
|
| 597 |
|
| 598 |
-
/* =======
|
| 599 |
-
document.addEventListener('paste', async
|
| 600 |
-
|
| 601 |
-
const
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
activeElement.isContentEditable === true // Use isContentEditable for modern check
|
| 606 |
);
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
if (isInputField) {
|
| 610 |
-
return; // Don't prevent default, let normal paste happen
|
| 611 |
-
}
|
| 612 |
-
|
| 613 |
-
// Otherwise, handle custom paste logic
|
| 614 |
e.preventDefault();
|
| 615 |
-
|
| 616 |
-
// Check for image files first
|
| 617 |
const items = Array.from(e.clipboardData.items);
|
| 618 |
-
const
|
| 619 |
-
|
| 620 |
-
if (
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
await processImage(file);
|
| 625 |
-
} else {
|
| 626 |
-
alert("Could not get image file from clipboard.");
|
| 627 |
-
}
|
| 628 |
} else {
|
| 629 |
-
// Handle text paste (existing functionality)
|
| 630 |
const txt = e.clipboardData.getData('text/plain');
|
| 631 |
if (txt.trim()) processContent(txt);
|
| 632 |
}
|
| 633 |
});
|
| 634 |
|
| 635 |
-
/* ======= Settings modal
|
| 636 |
-
const settingsBtn
|
| 637 |
const settingsModal = document.getElementById('settingsModal');
|
| 638 |
-
const nebiusKeyInput
|
| 639 |
const cerebrasKeyInput = document.getElementById('cerebrasKey');
|
| 640 |
|
| 641 |
settingsBtn.addEventListener('click', () => {
|
| 642 |
-
|
| 643 |
-
nebiusKeyInput.value = localStorage.getItem('nebius-api-key') || '';
|
| 644 |
cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || '';
|
| 645 |
settingsModal.classList.add('show');
|
| 646 |
});
|
| 647 |
|
| 648 |
-
function closeSettings()
|
| 649 |
-
settingsModal.classList.remove('show');
|
| 650 |
-
}
|
| 651 |
|
| 652 |
-
function saveSettings()
|
| 653 |
-
const
|
| 654 |
-
const
|
| 655 |
-
|
| 656 |
-
if (
|
| 657 |
-
if (cerebrasKey) localStorage.setItem('cerebras-api-key', cerebrasKey);
|
| 658 |
-
|
| 659 |
closeSettings();
|
| 660 |
alert('API keys saved successfully!');
|
| 661 |
}
|
| 662 |
|
| 663 |
-
|
| 664 |
-
settingsModal.addEventListener('click', (e)
|
| 665 |
-
|
| 666 |
-
});
|
| 667 |
-
document.addEventListener('keydown', (e) => {
|
| 668 |
-
if (e.key === 'Escape' && settingsModal.classList.contains('show')) {
|
| 669 |
-
closeSettings();
|
| 670 |
-
}
|
| 671 |
-
});
|
| 672 |
|
| 673 |
-
/*
|
| 674 |
content.addEventListener('click',()=>{
|
| 675 |
-
const ph=content.querySelector('.placeholder');
|
| 676 |
-
if(ph){
|
| 677 |
ph.style.transform='scale(.97)';
|
| 678 |
ph.style.transition='transform .12s';
|
| 679 |
setTimeout(()=>ph.style.transform='scale(1)',120);
|
| 680 |
}
|
| 681 |
});
|
| 682 |
|
| 683 |
-
/*
|
| 684 |
document.addEventListener('DOMContentLoaded',()=>{
|
| 685 |
const sheet=document.querySelector('.container');
|
| 686 |
sheet.style.opacity='0';
|
| 687 |
-
setTimeout(()=>{sheet.style.transition='opacity .6s ease';sheet.style.opacity='1'},80);
|
| 688 |
});
|
| 689 |
|
| 690 |
-
/*
|
| 691 |
-
const
|
| 692 |
-
const prefersDark
|
| 693 |
-
const savedTheme
|
| 694 |
|
| 695 |
initTheme();
|
| 696 |
-
|
| 697 |
document.body.classList.toggle('dark');
|
| 698 |
updateIcon();
|
| 699 |
-
localStorage.setItem('note-theme',document.body.classList.contains('dark')?'dark':'light');
|
| 700 |
});
|
| 701 |
function initTheme(){
|
| 702 |
-
if(savedTheme)
|
| 703 |
-
|
| 704 |
-
}else if(prefersDark.matches){
|
| 705 |
-
document.body.classList.add('dark');
|
| 706 |
-
}
|
| 707 |
updateIcon();
|
| 708 |
}
|
| 709 |
-
function updateIcon(){
|
| 710 |
-
btn.textContent=document.body.classList.contains('dark')?'☀️':'🌙';
|
| 711 |
-
}
|
| 712 |
</script>
|
| 713 |
</body>
|
| 714 |
</html>
|
|
|
|
| 1 |
+
|
| 2 |
<!DOCTYPE html>
|
| 3 |
<html lang="en">
|
| 4 |
<head>
|
|
|
|
| 55 |
--code-border: #e6e0d2;
|
| 56 |
--inline-code-bg: #f2efe8;
|
| 57 |
}
|
|
|
|
| 58 |
body.dark {
|
| 59 |
--desk-bg: #2c2a27;
|
| 60 |
--desk-dot: #3a3733;
|
|
|
|
| 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 |
+
/* ───────── copy button & QA block styling ───────── */
|
| 280 |
+
.copy-btn{
|
| 281 |
+
background:none;
|
| 282 |
+
border:none;
|
| 283 |
+
cursor:pointer;
|
| 284 |
+
font-size:0.9em;
|
| 285 |
+
margin-left:8px;
|
| 286 |
+
vertical-align:middle;
|
| 287 |
+
color:var(--paper-text);
|
| 288 |
+
}
|
| 289 |
+
.copy-btn:hover{
|
| 290 |
+
transform:scale(1.1);
|
| 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 */
|
|
|
|
| 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 |
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);
|
|
|
|
| 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 |
messages: [
|
| 434 |
{
|
| 435 |
role: 'system',
|
| 436 |
+
content: 'GIVE AS TEXT WITH LATEX like $this$ or $$this$$. DO NOT USE ITEMIZE. DO NOT SOLVE THE QUESTION. DO NOT OUTPUT ANYTHING BUT THE FORMAT OF THE QUESTION.'
|
| 437 |
},
|
| 438 |
{
|
| 439 |
role: 'user',
|
|
|
|
| 441 |
{ type: 'text', text: 'Image:' },
|
| 442 |
{
|
| 443 |
type: 'image_url',
|
| 444 |
+
image_url: { url: `data:image/png;base64,${base64Image}` }
|
| 445 |
}
|
| 446 |
]
|
| 447 |
}
|
|
|
|
| 450 |
});
|
| 451 |
|
| 452 |
if (!response.ok) {
|
| 453 |
+
const err = await response.text();
|
| 454 |
+
throw new Error(`OCR API error: ${response.status} – ${err}`);
|
| 455 |
}
|
| 456 |
|
| 457 |
const data = await response.json();
|
| 458 |
return data.choices[0].message.content;
|
| 459 |
+
} catch (e) {
|
| 460 |
+
console.error('OCR error:', e);
|
| 461 |
+
alert('Error during OCR: ' + e.message);
|
| 462 |
return null;
|
| 463 |
}
|
| 464 |
}
|
| 465 |
|
| 466 |
+
/* ======= UI helpers for streaming ======= */
|
| 467 |
function beginStreamingUI(question){
|
|
|
|
| 468 |
content.innerHTML = `
|
| 469 |
+
<div class="qa-block">
|
| 470 |
+
<div class="qa-header"><strong>Question</strong> <button class="copy-btn" data-copy-id="qStream" title="Copy question">📋</button></div>
|
| 471 |
+
<div class="mono-stream" id="qStream"></div>
|
| 472 |
+
</div>
|
| 473 |
<hr style="opacity:.35; margin: 20px 0;">
|
| 474 |
+
<div class="qa-block">
|
| 475 |
+
<div class="qa-header"><strong>Answer</strong> <button class="copy-btn" data-copy-id="aStream" title="Copy answer">📋</button></div>
|
| 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 |
return { qEl, aEl };
|
| 483 |
}
|
| 484 |
|
| 485 |
+
/* ======= Final render after streaming ======= */
|
| 486 |
function finalizeStreaming(question, fullAnswer){
|
| 487 |
+
const questionHTML = marked.parse(question);
|
| 488 |
+
const answerHTML = marked.parse(fullAnswer);
|
| 489 |
+
const finalHTML = `
|
| 490 |
+
<div class="qa-block">
|
| 491 |
+
<div class="qa-header"><strong>Question</strong> <button class="copy-btn" data-copy-id="finalQuestion" title="Copy question">📋</button></div>
|
| 492 |
+
<div class="qa-content" id="finalQuestion">${questionHTML}</div>
|
| 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 |
+
/* ======= Copy‑button handler (delegated) ======= */
|
| 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 |
}
|
| 530 |
|
| 531 |
showProcessing('Solving the question...');
|
| 532 |
+
const ui = beginStreamingUI(question); // lightweight view while streaming
|
| 533 |
|
| 534 |
try {
|
| 535 |
const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
|
| 536 |
method: 'POST',
|
| 537 |
headers: {
|
| 538 |
'Content-Type': 'application/json',
|
| 539 |
+
'Accept': 'text/event-stream',
|
| 540 |
'Authorization': `Bearer ${cerebrasKey}`
|
| 541 |
},
|
| 542 |
body: JSON.stringify({
|
| 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‑by‑step solution.' },
|
| 550 |
+
{ role: 'user', content: question }
|
| 551 |
]
|
| 552 |
})
|
| 553 |
});
|
| 554 |
|
| 555 |
if (!response.ok) {
|
| 556 |
+
const err = await response.text();
|
| 557 |
+
throw new Error(`Cerebras API error: ${response.status} – ${err}`);
|
| 558 |
}
|
| 559 |
|
| 560 |
const reader = response.body.getReader();
|
| 561 |
const decoder = new TextDecoder();
|
| 562 |
let fullAnswer = '';
|
| 563 |
+
let buffer = '';
|
| 564 |
+
let lastFlush = 0;
|
| 565 |
+
const FLUSH_MS = 120; // throttle UI updates
|
| 566 |
|
| 567 |
+
const flush = () => {
|
|
|
|
| 568 |
ui.aEl.textContent = fullAnswer;
|
| 569 |
+
lastFlush = performance.now();
|
| 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 ev of events) {
|
| 581 |
+
const dataLine = ev.split('\n').find(l => l.startsWith('data: '));
|
|
|
|
| 582 |
if (!dataLine) continue;
|
| 583 |
+
const data = dataLine.slice(6).trim();
|
| 584 |
+
if (data === '[DONE]') continue;
|
|
|
|
| 585 |
|
| 586 |
try {
|
| 587 |
+
const json = JSON.parse(data);
|
| 588 |
+
const delta = json.choices?.[0]?.delta?.content
|
| 589 |
+
?? json.choices?.[0]?.message?.content
|
| 590 |
+
?? json.choices?.[0]?.text
|
| 591 |
+
?? '';
|
| 592 |
+
if (delta) {
|
| 593 |
+
fullAnswer += delta;
|
| 594 |
+
if (performance.now() - lastFlush > FLUSH_MS) flush();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
}
|
| 596 |
} catch (e) {
|
| 597 |
+
// ignore malformed chunks
|
|
|
|
| 598 |
}
|
| 599 |
}
|
| 600 |
}
|
| 601 |
|
| 602 |
+
// final UI update before heavy render
|
| 603 |
+
flush();
|
|
|
|
|
|
|
| 604 |
finalizeStreaming(question, fullAnswer);
|
| 605 |
return fullAnswer;
|
| 606 |
+
} catch (e) {
|
| 607 |
+
console.error('Solve error:', e);
|
| 608 |
+
alert('Error while solving: ' + e.message);
|
| 609 |
+
hideProcessing();
|
| 610 |
return null;
|
| 611 |
}
|
| 612 |
}
|
|
|
|
| 614 |
/* ======= Process image pipeline ======= */
|
| 615 |
async function processImage(file) {
|
| 616 |
try {
|
|
|
|
| 617 |
const base64 = await imageToBase64(file);
|
| 618 |
+
const ocr = await ocrImage(base64);
|
| 619 |
+
if (!ocr) { hideProcessing(); return; }
|
| 620 |
+
await solveQuestion(ocr);
|
| 621 |
+
} catch (e) {
|
| 622 |
+
console.error('Image pipeline error:', e);
|
| 623 |
+
alert('Error processing image: ' + e.message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
hideProcessing();
|
| 625 |
}
|
| 626 |
}
|
| 627 |
|
| 628 |
+
/* ======= Paste listener (keeps normal input fields functional) ======= */
|
| 629 |
+
document.addEventListener('paste', async e => {
|
| 630 |
+
const active = document.activeElement;
|
| 631 |
+
const isInput = active && (
|
| 632 |
+
active.tagName === 'INPUT' ||
|
| 633 |
+
active.tagName === 'TEXTAREA' ||
|
| 634 |
+
active.isContentEditable
|
|
|
|
| 635 |
);
|
| 636 |
+
if (isInput) return; // let the browser handle normal paste
|
| 637 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
e.preventDefault();
|
| 639 |
+
|
|
|
|
| 640 |
const items = Array.from(e.clipboardData.items);
|
| 641 |
+
const imgItem = items.find(i => i.type.startsWith('image/'));
|
| 642 |
+
|
| 643 |
+
if (imgItem) {
|
| 644 |
+
const file = imgItem.getAsFile();
|
| 645 |
+
if (file) await processImage(file);
|
| 646 |
+
else alert('Could not retrieve image from clipboard.');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
} else {
|
|
|
|
| 648 |
const txt = e.clipboardData.getData('text/plain');
|
| 649 |
if (txt.trim()) processContent(txt);
|
| 650 |
}
|
| 651 |
});
|
| 652 |
|
| 653 |
+
/* ======= Settings modal handling ======= */
|
| 654 |
+
const settingsBtn = document.getElementById('settingsBtn');
|
| 655 |
const settingsModal = document.getElementById('settingsModal');
|
| 656 |
+
const nebiusKeyInput = document.getElementById('nebiusKey');
|
| 657 |
const cerebrasKeyInput = document.getElementById('cerebrasKey');
|
| 658 |
|
| 659 |
settingsBtn.addEventListener('click', () => {
|
| 660 |
+
nebiusKeyInput.value = localStorage.getItem('nebius-api-key') || '';
|
|
|
|
| 661 |
cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || '';
|
| 662 |
settingsModal.classList.add('show');
|
| 663 |
});
|
| 664 |
|
| 665 |
+
function closeSettings(){ settingsModal.classList.remove('show'); }
|
|
|
|
|
|
|
| 666 |
|
| 667 |
+
function saveSettings(){
|
| 668 |
+
const nb = nebiusKeyInput.value.trim();
|
| 669 |
+
const cb = cerebrasKeyInput.value.trim();
|
| 670 |
+
if (nb) localStorage.setItem('nebius-api-key', nb);
|
| 671 |
+
if (cb) localStorage.setItem('cerebras-api-key', cb);
|
|
|
|
|
|
|
| 672 |
closeSettings();
|
| 673 |
alert('API keys saved successfully!');
|
| 674 |
}
|
| 675 |
|
| 676 |
+
/* close modal on background click or Escape */
|
| 677 |
+
settingsModal.addEventListener('click', e => { if (e.target===settingsModal) closeSettings(); });
|
| 678 |
+
document.addEventListener('keydown', e => { if (e.key==='Escape' && settingsModal.classList.contains('show')) closeSettings(); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
|
| 680 |
+
/* placeholder click animation */
|
| 681 |
content.addEventListener('click',()=>{
|
| 682 |
+
const ph = content.querySelector('.placeholder');
|
| 683 |
+
if (ph){
|
| 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‑in on load */
|
| 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';},80);
|
| 695 |
});
|
| 696 |
|
| 697 |
+
/* theme toggler */
|
| 698 |
+
const themeBtn = document.getElementById('themeToggle');
|
| 699 |
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
| 700 |
+
const savedTheme = localStorage.getItem('note-theme');
|
| 701 |
|
| 702 |
initTheme();
|
| 703 |
+
themeBtn.addEventListener('click',()=>{
|
| 704 |
document.body.classList.toggle('dark');
|
| 705 |
updateIcon();
|
| 706 |
+
localStorage.setItem('note-theme', document.body.classList.contains('dark') ? 'dark' : 'light');
|
| 707 |
});
|
| 708 |
function initTheme(){
|
| 709 |
+
if (savedTheme) document.body.classList.toggle('dark', savedTheme==='dark');
|
| 710 |
+
else if (prefersDark.matches) document.body.classList.add('dark');
|
|
|
|
|
|
|
|
|
|
| 711 |
updateIcon();
|
| 712 |
}
|
| 713 |
+
function updateIcon(){ themeBtn.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙'; }
|
|
|
|
|
|
|
| 714 |
</script>
|
| 715 |
</body>
|
| 716 |
</html>
|