SolarumAsteridion commited on
Commit
fb65023
·
verified ·
1 Parent(s): eef9e19

Update index.html

Browse files
Files changed (1) hide show
  1. 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
- /* ───────── 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 */
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]).then(hideProcessing)
392
- .catch(e=>{console.error('MathJax error:',e);hideProcessing()});
393
- }else{hideProcessing()}
 
 
 
 
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: '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,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 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,7 +576,7 @@ async function solveQuestion(question) {
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', {
@@ -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, stepbystep 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,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
- 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
- /* fadein 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>
 
 
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>