SolarumAsteridion commited on
Commit
3c9e002
·
verified ·
1 Parent(s): b52d177

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +252 -169
index.html CHANGED
@@ -53,6 +53,15 @@ window.MathJax = {
53
  --code-bg: #f4f2ec;
54
  --code-border: #e6e0d2;
55
  --inline-code-bg: #f2efe8;
 
 
 
 
 
 
 
 
 
56
  }
57
 
58
  body.dark {
@@ -72,6 +81,15 @@ body.dark {
72
  --code-bg: #3a3530;
73
  --code-border: #514b42;
74
  --inline-code-bg: #4a443d;
 
 
 
 
 
 
 
 
 
75
  }
76
 
77
  /* ─────────────────────────────────────────
@@ -278,35 +296,51 @@ th{font-weight:600}
278
 
279
  /* ───────── copy button styles ───────── */
280
  .copy-btn {
281
- display: inline-block;
 
 
282
  margin-left: 10px;
283
- padding: 4px 8px;
284
- background: var(--code-bg);
285
- border: 1px solid var(--code-border);
286
- border-radius: 4px;
 
287
  font-family: 'PT Mono', monospace;
288
  font-size: 12px;
289
  cursor: pointer;
290
- transition: all 0.2s;
291
  user-select: none;
292
  }
293
  .copy-btn:hover {
294
- background: var(--blockquote-bg);
295
- transform: translateY(-1px);
 
296
  }
297
  .copy-btn.copied {
298
- background: #4a5f4a;
299
- color: #fff;
300
- border-color: #4a5f4a;
301
  }
 
 
302
  .section-header {
303
  display: flex;
304
  align-items: center;
305
- gap: 8px;
306
- margin-bottom: 0;
307
  }
308
- .section-header strong {
309
- margin: 0;
 
 
 
 
 
 
 
 
 
 
310
  }
311
 
312
  /* responsive & print */
@@ -390,52 +424,134 @@ function copyToClipboard(text, button) {
390
  setTimeout(() => {
391
  button.textContent = originalText;
392
  button.classList.remove('copied');
393
- }, 2000);
394
  }).catch(err => {
395
  console.error('Failed to copy:', err);
396
  alert('Failed to copy to clipboard');
397
  });
398
  }
399
 
400
- /* ======= markdown + latex pipeline ======= */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  function processContent(text){
402
  showProcessing();
 
 
 
 
 
 
 
 
 
403
 
404
- const store=[], PL=i=>`%%LATEX_${i}%%`; let idx=0;
405
- const keep=m=>(store.push(m),PL(idx++));
 
 
406
 
407
- text = text
408
- .replace(/\\\[[\s\S]*?\\\]/g, keep) // \[ ... \]
409
- .replace(/\$\$[\s\S]*?\$\$/g, keep) // $$ ... $$
410
- .replace(/\\\([\s\S]*?\\\)/g, keep) // \( ... \)
411
- .replace(/\$([^\$\n]+?)\$/g, keep); // $ ... $
412
 
413
- let html = marked.parse(text);
414
- store.forEach((latex,i)=>{html=html.replaceAll(PL(i),latex)});
415
- content.innerHTML = html;
 
 
 
 
416
 
417
- if(window.MathJax?.typesetPromise){
418
- MathJax.typesetPromise([content]).then(hideProcessing)
419
- .catch(e=>{console.error('MathJax error:',e);hideProcessing()});
420
- }else{hideProcessing()}
421
  }
422
 
423
- /* ======= Image to Base64 converter ======= */
424
- async function imageToBase64(file) {
425
- return new Promise((resolve, reject) => {
426
- const reader = new FileReader();
427
- reader.onload = () => {
428
- // Ensure it's a valid data URL and extract base64 part
429
- if (reader.result && reader.result.includes(',')) {
430
- const base64 = reader.result.split(',')[1];
431
- resolve(base64);
432
- } else {
433
- reject(new Error("Failed to read file as Data URL."));
434
- }
435
- };
436
- reader.onerror = () => reject(reader.error);
437
- reader.readAsDataURL(file);
438
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  }
440
 
441
  /* ======= OCR with Nebius API ======= */
@@ -457,20 +573,18 @@ async function ocrImage(base64Image) {
457
  'Authorization': `Bearer ${nebiusKey}`
458
  },
459
  body: JSON.stringify({
460
- model: 'google/gemma-3-27b-it',
461
  messages: [
462
  {
463
  role: 'system',
464
- content: 'GIVE AS TEXT WITH LATEX like $this$ or $$this$$. NEVER USE \\itemize, \\begin{itemize}, \\end{itemize}, \\item or any list environments. Use plain text with numbers or letters for lists. DO NOT SOLVE THE QUESTION. DO NOT OUTPUT ANYTHING BUT THE EXACT FORMAT OF THE QUESTION AS IT APPEARS IN THE IMAGE.'
 
465
  },
466
  {
467
  role: 'user',
468
  content: [
469
  { type: 'text', text: 'Image:' },
470
- {
471
- type: 'image_url',
472
- image_url: { url: `data:image/png;base64,${base64Image}` } // Assuming PNG, adjust if needed
473
- }
474
  ]
475
  }
476
  ]
@@ -483,7 +597,7 @@ async function ocrImage(base64Image) {
483
  }
484
 
485
  const data = await response.json();
486
- return data.choices[0].message.content;
487
  } catch (error) {
488
  console.error('OCR Error:', error);
489
  alert('Error during OCR: ' + error.message);
@@ -491,58 +605,6 @@ async function ocrImage(base64Image) {
491
  }
492
  }
493
 
494
- /* ======= UI Helpers for Streaming ======= */
495
- let currentQuestion = '';
496
- let currentAnswer = '';
497
-
498
- function beginStreamingUI(question){
499
- currentQuestion = question;
500
- currentAnswer = '';
501
-
502
- // Show a lightweight, non-MathJax view while the model streams
503
- content.innerHTML = `
504
- <div>
505
- <div class="section-header">
506
- <p><strong>Question</strong>:</p>
507
- <button class="copy-btn" onclick="copyToClipboard(currentQuestion, this)">📋 Copy</button>
508
- </div>
509
- <div class="mono-stream" id="qStream"></div>
510
- <hr style="opacity:.35; margin: 20px 0;">
511
- <div class="section-header">
512
- <p><strong>Answer</strong>:</p>
513
- <button class="copy-btn" onclick="copyToClipboard(currentAnswer, this)">📋 Copy</button>
514
- </div>
515
- <div class="mono-stream" id="aStream">(generating...)</div>
516
- </div>`;
517
- const qEl = document.getElementById('qStream');
518
- const aEl = document.getElementById('aStream');
519
- qEl.textContent = question; // plain text now; pretty render later
520
- aEl.textContent = ''; // clear "(generating...)"
521
- return { qEl, aEl };
522
- }
523
-
524
- function finalizeStreaming(question, fullAnswer){
525
- currentQuestion = question;
526
- currentAnswer = fullAnswer;
527
-
528
- // Create HTML with copy buttons
529
- const htmlContent = `
530
- <div class="section-header">
531
- <strong>Question</strong>:
532
- <button class="copy-btn" onclick="copyToClipboard(currentQuestion, this)">📋 Copy</button>
533
- </div>
534
- <div style="margin-bottom: 20px;">${question}</div>
535
-
536
- <div class="section-header">
537
- <strong>Answer</strong>:
538
- <button class="copy-btn" onclick="copyToClipboard(currentAnswer, this)">📋 Copy</button>
539
- </div>
540
- <div>${fullAnswer}</div>`;
541
-
542
- // Process the content with Markdown and MathJax
543
- processContent(htmlContent);
544
- }
545
-
546
  /* ======= Solve with Cerebras API (Streaming Optimization) ======= */
547
  async function solveQuestion(question) {
548
  const cerebrasKey = localStorage.getItem('cerebras-api-key');
@@ -552,26 +614,25 @@ async function solveQuestion(question) {
552
  }
553
 
554
  showProcessing('Solving the question...');
555
- const ui = beginStreamingUI(question); // Prepare the lightweight streaming UI
556
 
557
  try {
558
  const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
559
  method: 'POST',
560
  headers: {
561
  'Content-Type': 'application/json',
562
- 'Accept': 'text/event-stream',
563
  'Authorization': `Bearer ${cerebrasKey}`
564
  },
565
  body: JSON.stringify({
566
  model: 'gpt-oss-120b',
567
  stream: true,
568
  max_tokens: 65536,
569
- temperature: 0.1, // Set temperature to 0.1
570
- reasoning_effort: 'medium', // Set reasoning_effort to 'medium'
571
- // top_p: 1, // Removed as per user's request
572
  messages: [
573
  { role: 'system', content: 'Solve this Question. Provide a clear, step-by-step solution.' },
574
- { role: 'user', content: question }
575
  ]
576
  })
577
  });
@@ -584,66 +645,63 @@ async function solveQuestion(question) {
584
  const reader = response.body.getReader();
585
  const decoder = new TextDecoder();
586
  let fullAnswer = '';
587
- let buffer = ''; // buffer for partial SSE frames
588
  let lastFlushTime = 0;
589
- const flushThrottle = 120; // milliseconds to wait between DOM updates
590
 
 
591
  const flushUI = () => {
592
- // Update the lightweight streaming area without MathJax
593
- ui.aEl.textContent = fullAnswer;
594
- currentAnswer = fullAnswer; // Update global variable for copy button
595
- lastFlushTime = performance.now();
596
  };
597
 
598
  while (true) {
599
  const { done, value } = await reader.read();
600
- if (done) break;
601
 
602
- buffer += decoder.decode(value, { stream: true });
603
- // SSE events are typically separated by '\n\n'
604
- const events = buffer.split('\n\n');
605
- buffer = events.pop() || ''; // Keep any incomplete event for the next chunk
606
 
607
  for (const evt of events) {
608
- // Find the 'data:' line, which contains the JSON payload
609
- const dataLine = evt.split('\n').find(line => line.trim().startsWith('data: '));
610
  if (!dataLine) continue;
611
 
612
- const data = dataLine.slice(6).trim(); // Remove 'data: ' prefix
613
- if (data === '[DONE]') continue; // End of stream marker
614
 
615
  try {
616
  const parsed = JSON.parse(data);
617
- // Extract content, being flexible with potential API response structures
618
  const deltaContent = parsed.choices?.[0]?.delta?.content
619
  ?? parsed.choices?.[0]?.message?.content
620
- ?? parsed.choices?.[0]?.text // Some APIs might use 'text'
621
  ?? '';
622
 
623
  if (deltaContent) {
624
- fullAnswer += deltaContent;
625
- // Throttle DOM updates to prevent excessive rendering and jank
626
  if (performance.now() - lastFlushTime > flushThrottle) {
627
  flushUI();
628
  }
629
  }
630
  } catch (e) {
631
- // Ignore errors parsing JSON chunks if it's just partial data
632
  console.error('Error parsing stream chunk data:', e, 'Chunk:', data);
633
  }
634
  }
635
  }
636
 
637
- // Final flush to ensure all streamed content is displayed in the lightweight view
638
- flushUI();
639
 
640
- // Once streaming is complete, perform the final, heavier render with Markdown and MathJax
641
  finalizeStreaming(question, fullAnswer);
642
- return fullAnswer;
643
  } catch (error) {
644
  console.error('Solving Error:', error);
645
  alert('Error during solving: ' + error.message);
646
- hideProcessing(); // Ensure the processing indicator is hidden on error
647
  return null;
648
  }
649
  }
@@ -651,52 +709,70 @@ async function solveQuestion(question) {
651
  /* ======= Process image pipeline ======= */
652
  async function processImage(file) {
653
  try {
654
- // Convert image to base64
655
  const base64 = await imageToBase64(file);
656
 
657
- // OCR the image
658
  const ocrText = await ocrImage(base64);
659
  if (!ocrText) {
660
- hideProcessing();
661
  return;
662
  }
663
 
664
- // Solve the question
665
  const answer = await solveQuestion(ocrText);
666
- // The solveQuestion function now handles hiding the processing indicator
667
- // unless an error occurred, in which case it was hidden earlier.
668
 
669
  } catch (error) {
670
  console.error('Image processing error:', error);
671
  alert('Error processing image: ' + error.message);
672
- hideProcessing();
673
  }
674
  }
675
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  /* ======= FIXED paste listener - allows normal paste in input fields ======= */
677
  document.addEventListener('paste', async (e) => {
678
- // Check if we're pasting into an input, textarea, or contenteditable element
679
  const activeElement = document.activeElement;
680
  const isInputField = activeElement && (
681
  activeElement.tagName === 'INPUT' ||
682
  activeElement.tagName === 'TEXTAREA' ||
683
- activeElement.isContentEditable === true // Use isContentEditable for modern check
684
  );
685
 
686
- // If pasting into an input field, let the browser handle it normally
687
  if (isInputField) {
688
- return; // Don't prevent default, let normal paste happen
689
  }
690
 
691
- // Otherwise, handle custom paste logic
692
- e.preventDefault();
693
 
694
- // Check for image files first
695
  const items = Array.from(e.clipboardData.items);
696
  const imageItem = items.find(item => item.type.startsWith('image/'));
697
 
698
  if (imageItem) {
699
- // Handle image paste
700
  const file = imageItem.getAsFile();
701
  if (file) {
702
  await processImage(file);
@@ -704,9 +780,11 @@ document.addEventListener('paste', async (e) => {
704
  alert("Could not get image file from clipboard.");
705
  }
706
  } else {
707
- // Handle text paste (existing functionality)
708
  const txt = e.clipboardData.getData('text/plain');
709
- if (txt.trim()) processContent(txt);
 
 
710
  }
711
  });
712
 
@@ -717,34 +795,35 @@ const nebiusKeyInput = document.getElementById('nebiusKey');
717
  const cerebrasKeyInput = document.getElementById('cerebrasKey');
718
 
719
  settingsBtn.addEventListener('click', () => {
720
- // Load existing keys
721
  nebiusKeyInput.value = localStorage.getItem('nebius-api-key') || '';
722
  cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || '';
723
- settingsModal.classList.add('show');
724
  });
725
 
726
  function closeSettings() {
727
- settingsModal.classList.remove('show');
728
  }
729
 
730
  function saveSettings() {
731
  const nebiusKey = nebiusKeyInput.value.trim();
732
  const cerebrasKey = cerebrasKeyInput.value.trim();
733
 
 
734
  if (nebiusKey) localStorage.setItem('nebius-api-key', nebiusKey);
735
  if (cerebrasKey) localStorage.setItem('cerebras-api-key', cerebrasKey);
736
 
737
- closeSettings();
738
- alert('API keys saved successfully!');
739
  }
740
 
741
- // Close modal on escape or background click
742
  settingsModal.addEventListener('click', (e) => {
743
- if (e.target === settingsModal) closeSettings();
744
  });
745
  document.addEventListener('keydown', (e) => {
746
  if (e.key === 'Escape' && settingsModal.classList.contains('show')) {
747
- closeSettings();
748
  }
749
  });
750
 
@@ -758,7 +837,7 @@ content.addEventListener('click',()=>{
758
  }
759
  });
760
 
761
- /* smooth fade in */
762
  document.addEventListener('DOMContentLoaded',()=>{
763
  const sheet=document.querySelector('.container');
764
  sheet.style.opacity='0';
@@ -770,23 +849,27 @@ const btn = document.getElementById('themeToggle');
770
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
771
  const savedTheme = localStorage.getItem('note-theme');
772
 
773
- initTheme();
774
  btn.addEventListener('click',()=>{
775
- document.body.classList.toggle('dark');
776
- updateIcon();
777
- localStorage.setItem('note-theme',document.body.classList.contains('dark')?'dark':'light');
778
  });
 
779
  function initTheme(){
 
780
  if(savedTheme){
781
  document.body.classList.toggle('dark',savedTheme==='dark');
782
  }else if(prefersDark.matches){
783
  document.body.classList.add('dark');
784
  }
785
- updateIcon();
786
  }
 
787
  function updateIcon(){
 
788
  btn.textContent=document.body.classList.contains('dark')?'☀️':'🌙';
789
  }
790
  </script>
791
  </body>
792
- </html>
 
53
  --code-bg: #f4f2ec;
54
  --code-border: #e6e0d2;
55
  --inline-code-bg: #f2efe8;
56
+
57
+ /* copy button theme vars */
58
+ --copy-bg: var(--code-bg);
59
+ --copy-border: var(--code-border);
60
+ --copy-color: var(--paper-text);
61
+ --copy-hover-bg: var(--blockquote-bg);
62
+ --copy-hover-border: var(--blockquote-bar);
63
+ --copy-success-bg: #4a5f4a;
64
+ --copy-success-color:#fff;
65
  }
66
 
67
  body.dark {
 
81
  --code-bg: #3a3530;
82
  --code-border: #514b42;
83
  --inline-code-bg: #4a443d;
84
+
85
+ /* dark theme copy button vars */
86
+ --copy-bg: var(--code-bg);
87
+ --copy-border: var(--code-border);
88
+ --copy-color: var(--paper-text);
89
+ --copy-hover-bg: #4a443d; /* Darker gray for hover */
90
+ --copy-hover-border:#6a604e; /* Darker border for hover */
91
+ --copy-success-bg: #4a5f4a; /* Consistent success color */
92
+ --copy-success-color:#fff;
93
  }
94
 
95
  /* ─────────────────────────────────────────
 
296
 
297
  /* ───────── copy button styles ───────── */
298
  .copy-btn {
299
+ display: inline-flex; /* Use flex for alignment */
300
+ align-items: center;
301
+ gap: 6px; /* Space between icon and text */
302
  margin-left: 10px;
303
+ padding: 4px 10px;
304
+ background: var(--copy-bg);
305
+ border: 1px solid var(--copy-border);
306
+ color: var(--copy-color);
307
+ border-radius: 6px;
308
  font-family: 'PT Mono', monospace;
309
  font-size: 12px;
310
  cursor: pointer;
311
+ transition: all 0.18s ease; /* Smoother transition */
312
  user-select: none;
313
  }
314
  .copy-btn:hover {
315
+ background: var(--copy-hover-bg);
316
+ border-color: var(--copy-hover-border);
317
+ transform: translateY(-1px); /* Subtle lift effect */
318
  }
319
  .copy-btn.copied {
320
+ background: var(--copy-success-bg);
321
+ border-color: var(--copy-success-bg);
322
+ color: var(--copy-success-color);
323
  }
324
+
325
+ /* header layout (updated to use span for title, avoid <p> spacing issues) */
326
  .section-header {
327
  display: flex;
328
  align-items: center;
329
+ gap: 10px; /* Space between title and button */
330
+ margin: 6px 0 8px; /* Adjust vertical spacing */
331
  }
332
+ .section-title {
333
+ font-family:'Libre Baskerville',serif;
334
+ font-weight:700;
335
+ }
336
+ .section-title::after {
337
+ content: " :"; /* Add colon after the title */
338
+ opacity:.85;
339
+ }
340
+
341
+ /* Optional: container for rendered blocks in final view */
342
+ .rendered {
343
+ margin-bottom: 20px;
344
  }
345
 
346
  /* responsive & print */
 
424
  setTimeout(() => {
425
  button.textContent = originalText;
426
  button.classList.remove('copied');
427
+ }, 1500); /* Shorter timeout for feedback */
428
  }).catch(err => {
429
  console.error('Failed to copy:', err);
430
  alert('Failed to copy to clipboard');
431
  });
432
  }
433
 
434
+ /* ======= Markdown + LaTeX renderer ======= */
435
+ function renderMdLatex(text) {
436
+ const store = [];
437
+ const PL = i => `%%LATEX_${i}%%`; /* Placeholder for LaTeX */
438
+ let idx = 0;
439
+ const keep = m => (store.push(m), PL(idx++)); /* Function to store and replace with placeholder */
440
+
441
+ // Protect LaTeX blocks before Markdown parsing
442
+ text = (text || '')
443
+ .replace(/\\\[[\s\S]*?\\\]/g, keep) // \[ ... \]
444
+ .replace(/\$\$[\s\S]*?\$\$/g, keep) // $$ ... $$
445
+ .replace(/\\\([\s\S]*?\\\)/g, keep) // \( ... \)
446
+ .replace(/\$([^\$\n]+?)\$/g, keep); // $ ... $ (inline math)
447
+
448
+ let html = marked.parse(text || ''); // Parse Markdown
449
+
450
+ // Re-insert LaTeX blocks
451
+ store.forEach((latex, i) => { html = html.replaceAll(PL(i), latex); });
452
+ return html;
453
+ }
454
+
455
+ /* ======= processContent uses the new renderer ======= */
456
  function processContent(text){
457
  showProcessing();
458
+ content.innerHTML = renderMdLatex(text); /* Render with Markdown and LaTeX */
459
+
460
+ if (window.MathJax?.typesetPromise) {
461
+ MathJax.typesetPromise([content]).then(hideProcessing)
462
+ .catch(e => { console.error('MathJax error:', e); hideProcessing(); });
463
+ } else {
464
+ hideProcessing(); /* Hide if MathJax is not available */
465
+ }
466
+ }
467
 
468
+ /* ======= Clean leading labels to avoid duplication ======= */
469
+ function normalizeSection(s) {
470
+ if (!s) return '';
471
+ let out = s.trim();
472
 
473
+ // Remove common leading labels like "Question:", "Answer:", etc.
474
+ // This regex is more robust and handles various casings and separators.
475
+ out = out.replace(/^(?:\s*[-*_]*\s*)*(?:Question|Q|Problem|Prompt|Answer|Solution|Ans)\s*[:\-]?\s*/i, '');
 
 
476
 
477
+ // Additional specific cleanups for common markdown bolding patterns
478
+ out = out.replace(/^\s*\*{0,2}Answer\*{0,2}\s*:\s*/i, '');
479
+ out = out.replace(/^\s*\*{0,2}Solution\*{0,2}\s*:\s*/i, '');
480
+ out = out.replace(/^\s*\*{0,2}Ans\*{0,2}\s*:\s*/i, '');
481
+ out = out.replace(/^\s*\*{0,2}Q\*{0,2}\s*:\s*/i, '');
482
+ out = out.replace(/^\s*\*{0,2}Question\*{0,2}\s*:\s*/i, '');
483
+ out = out.replace(/^\s*\*{0,2}Problem\*{0,2}\s*:\s*/i, '');
484
 
485
+ return out.trim(); /* Return cleaned string */
 
 
 
486
  }
487
 
488
+ /* ======= UI Helpers for Streaming (updated headers & copy buttons) ======= */
489
+ let currentQuestion = '';
490
+ let currentAnswer = '';
491
+
492
+ function beginStreamingUI(question){
493
+ currentQuestion = normalizeSection(question); /* Normalize question text */
494
+ currentAnswer = ''; /* Reset answer when starting a new question */
495
+
496
+ /* Set up the initial streaming UI with copy buttons */
497
+ content.innerHTML = `
498
+ <div>
499
+ <div class="section-header">
500
+ <span class="section-title">Question</span>
501
+ <button class="copy-btn" onclick="copyToClipboard(currentQuestion, this)">📋 Copy</button>
502
+ </div>
503
+ <div class="mono-stream" id="qStream"></div>
504
+
505
+ <hr style="opacity:.35; margin: 20px 0;">
506
+
507
+ <div class="section-header">
508
+ <span class="section-title">Answer</span>
509
+ <button class="copy-btn" onclick="copyToClipboard(currentAnswer, this)">📋 Copy</button>
510
+ </div>
511
+ <div class="mono-stream" id="aStream">(generating...)</div>
512
+ </div>`;
513
+
514
+ const qEl = document.getElementById('qStream');
515
+ const aEl = document.getElementById('aStream');
516
+ qEl.textContent = currentQuestion; /* Display the normalized question */
517
+ aEl.textContent = ''; /* Clear "(generating...)" initially */
518
+ return { qEl, aEl }; /* Return elements for updating */
519
+ }
520
+
521
+ function finalizeStreaming(question, fullAnswer){
522
+ currentQuestion = normalizeSection(question); /* Normalize question text */
523
+ currentAnswer = normalizeSection(fullAnswer); /* Normalize answer text */
524
+
525
+ /* Build the final HTML structure with proper rendering and copy buttons */
526
+ content.innerHTML = `
527
+ <div class="section-header">
528
+ <span class="section-title">Question</span>
529
+ <button class="copy-btn" onclick="copyToClipboard(currentQuestion, this)">📋 Copy</button>
530
+ </div>
531
+ <div id="qRender" class="rendered"></div>
532
+
533
+ <hr style="opacity:.35; margin: 20px 0;">
534
+
535
+ <div class="section-header">
536
+ <span class="section-title">Answer</span>
537
+ <button class="copy-btn" onclick="copyToClipboard(currentAnswer, this)">📋 Copy</button>
538
+ </div>
539
+ <div id="aRender" class="rendered"></div>
540
+ `;
541
+
542
+ /* Render Markdown+LaTeX into the respective blocks */
543
+ const qRender = document.getElementById('qRender');
544
+ const aRender = document.getElementById('aRender');
545
+ qRender.innerHTML = renderMdLatex(currentQuestion); /* Render question */
546
+ aRender.innerHTML = renderMdLatex(currentAnswer); /* Render answer */
547
+
548
+ /* Apply MathJax typesetting to the rendered blocks */
549
+ if (window.MathJax?.typesetPromise) {
550
+ MathJax.typesetPromise([qRender, aRender]).then(hideProcessing) /* Hide processing indicator on success */
551
+ .catch(e => { console.error('MathJax error:', e); hideProcessing(); }); /* Hide on error */
552
+ } else {
553
+ hideProcessing(); /* Hide if MathJax is not available */
554
+ }
555
  }
556
 
557
  /* ======= OCR with Nebius API ======= */
 
573
  'Authorization': `Bearer ${nebiusKey}`
574
  },
575
  body: JSON.stringify({
576
+ model: 'google/gemma-3-27b-it', /* Model for OCR */
577
  messages: [
578
  {
579
  role: 'system',
580
+ /* Enhanced prompt to prevent list environments and ensure raw LaTeX output */
581
+ content: 'OUTPUT ONLY the question text as plain text with LaTeX like $...$ or $$...$$. Do NOT SOLVE. NEVER use LaTeX list environments: \\itemize, \\enumerate, \\description, \\items or \\item. No bullet or numbered lists in LaTeX. If you need a list, write plain lines prefixed with "1) ", "a) " etc as text.'
582
  },
583
  {
584
  role: 'user',
585
  content: [
586
  { type: 'text', text: 'Image:' },
587
+ { type: 'image_url', image_url: { url: `data:image/png;base64,${base64Image}` } } /* Image data */
 
 
 
588
  ]
589
  }
590
  ]
 
597
  }
598
 
599
  const data = await response.json();
600
+ return data.choices[0].message.content; /* Return extracted text */
601
  } catch (error) {
602
  console.error('OCR Error:', error);
603
  alert('Error during OCR: ' + error.message);
 
605
  }
606
  }
607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  /* ======= Solve with Cerebras API (Streaming Optimization) ======= */
609
  async function solveQuestion(question) {
610
  const cerebrasKey = localStorage.getItem('cerebras-api-key');
 
614
  }
615
 
616
  showProcessing('Solving the question...');
617
+ const ui = beginStreamingUI(question); /* Prepare the UI for streaming */
618
 
619
  try {
620
  const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
621
  method: 'POST',
622
  headers: {
623
  'Content-Type': 'application/json',
624
+ 'Accept': 'text/event-stream', /* Request server-sent events */
625
  'Authorization': `Bearer ${cerebrasKey}`
626
  },
627
  body: JSON.stringify({
628
  model: 'gpt-oss-120b',
629
  stream: true,
630
  max_tokens: 65536,
631
+ temperature: 0.1, /* Lower temperature for more deterministic answers */
632
+ reasoning_effort: 'medium', /* Medium reasoning effort */
 
633
  messages: [
634
  { role: 'system', content: 'Solve this Question. Provide a clear, step-by-step solution.' },
635
+ { role: 'user', content: question } /* User's question */
636
  ]
637
  })
638
  });
 
645
  const reader = response.body.getReader();
646
  const decoder = new TextDecoder();
647
  let fullAnswer = '';
648
+ let buffer = ''; /* Buffer for partial Server-Sent Events (SSE) frames */
649
  let lastFlushTime = 0;
650
+ const flushThrottle = 120; /* Milliseconds to wait between DOM updates to prevent jank */
651
 
652
+ /* Helper to update the UI during streaming */
653
  const flushUI = () => {
654
+ ui.aEl.textContent = fullAnswer; /* Update the answer display */
655
+ currentAnswer = fullAnswer; /* Update global variable for copy button */
656
+ lastFlushTime = performance.now(); /* Record time of last update */
 
657
  };
658
 
659
  while (true) {
660
  const { done, value } = await reader.read();
661
+ if (done) break; /* Exit loop if stream is done */
662
 
663
+ buffer += decoder.decode(value, { stream: true }); /* Append decoded data */
664
+ const events = buffer.split('\n\n'); /* Split buffer by SSE frame delimiter */
665
+ buffer = events.pop() || ''; /* Keep any incomplete event for the next chunk */
 
666
 
667
  for (const evt of events) {
668
+ const dataLine = evt.split('\n').find(line => line.trim().startsWith('data: ')); /* Find data line */
 
669
  if (!dataLine) continue;
670
 
671
+ const data = dataLine.slice(6).trim(); /* Extract JSON data */
672
+ if (data === '[DONE]') continue; /* Ignore end-of-stream marker */
673
 
674
  try {
675
  const parsed = JSON.parse(data);
676
+ /* Extract content delta from potential response structures */
677
  const deltaContent = parsed.choices?.[0]?.delta?.content
678
  ?? parsed.choices?.[0]?.message?.content
679
+ ?? parsed.choices?.[0]?.text /* Fallback for other potential fields */
680
  ?? '';
681
 
682
  if (deltaContent) {
683
+ fullAnswer += deltaContent; /* Append new content */
684
+ /* Throttle DOM updates to keep UI responsive */
685
  if (performance.now() - lastFlushTime > flushThrottle) {
686
  flushUI();
687
  }
688
  }
689
  } catch (e) {
690
+ /* Log errors parsing chunks, but continue streaming */
691
  console.error('Error parsing stream chunk data:', e, 'Chunk:', data);
692
  }
693
  }
694
  }
695
 
696
+ flushUI(); /* Final flush to display any remaining content */
 
697
 
698
+ /* After streaming, perform the final heavy render with Markdown and MathJax */
699
  finalizeStreaming(question, fullAnswer);
700
+ return fullAnswer; /* Return the complete answer */
701
  } catch (error) {
702
  console.error('Solving Error:', error);
703
  alert('Error during solving: ' + error.message);
704
+ hideProcessing(); /* Ensure processing indicator is hidden on error */
705
  return null;
706
  }
707
  }
 
709
  /* ======= Process image pipeline ======= */
710
  async function processImage(file) {
711
  try {
712
+ /* Convert image file to base64 string */
713
  const base64 = await imageToBase64(file);
714
 
715
+ /* OCR the image to get question text */
716
  const ocrText = await ocrImage(base64);
717
  if (!ocrText) {
718
+ /* Error handled within ocrImage, which calls hideProcessing */
719
  return;
720
  }
721
 
722
+ /* Solve the question using the extracted text */
723
  const answer = await solveQuestion(ocrText);
724
+ /* solveQuestion handles hiding the processing indicator */
 
725
 
726
  } catch (error) {
727
  console.error('Image processing error:', error);
728
  alert('Error processing image: ' + error.message);
729
+ hideProcessing(); /* Ensure processing indicator is hidden on error */
730
  }
731
  }
732
 
733
+ /* ======= Image to Base64 converter ======= */
734
+ async function imageToBase64(file) {
735
+ return new Promise((resolve, reject) => {
736
+ const reader = new FileReader();
737
+ reader.onload = () => {
738
+ /* Ensure it's a valid data URL and extract base64 part */
739
+ if (reader.result && reader.result.includes(',')) {
740
+ const base64 = reader.result.split(',')[1];
741
+ resolve(base64);
742
+ } else {
743
+ reject(new Error("Failed to read file as Data URL."));
744
+ }
745
+ };
746
+ reader.onerror = () => reject(reader.error); /* Reject on reader error */
747
+ reader.readAsDataURL(file); /* Read file as Data URL */
748
+ });
749
+ }
750
+
751
+
752
  /* ======= FIXED paste listener - allows normal paste in input fields ======= */
753
  document.addEventListener('paste', async (e) => {
754
+ /* Check if the paste event is happening inside an input, textarea, or contenteditable element */
755
  const activeElement = document.activeElement;
756
  const isInputField = activeElement && (
757
  activeElement.tagName === 'INPUT' ||
758
  activeElement.tagName === 'TEXTAREA' ||
759
+ activeElement.isContentEditable === true /* Modern check for editable content */
760
  );
761
 
762
+ /* If pasting into an input field, let the browser handle it normally */
763
  if (isInputField) {
764
+ return; /* Do not intercept, allow default paste behavior */
765
  }
766
 
767
+ /* Otherwise, handle custom paste logic for the content area */
768
+ e.preventDefault(); /* Prevent default paste behavior */
769
 
770
+ /* Check for image files first in the clipboard data */
771
  const items = Array.from(e.clipboardData.items);
772
  const imageItem = items.find(item => item.type.startsWith('image/'));
773
 
774
  if (imageItem) {
775
+ /* Handle image paste: convert to base64, OCR, and solve */
776
  const file = imageItem.getAsFile();
777
  if (file) {
778
  await processImage(file);
 
780
  alert("Could not get image file from clipboard.");
781
  }
782
  } else {
783
+ /* Handle text paste: process it directly */
784
  const txt = e.clipboardData.getData('text/plain');
785
+ if (txt.trim()) {
786
+ processContent(txt); /* Use the processContent function for direct text pastes */
787
+ }
788
  }
789
  });
790
 
 
795
  const cerebrasKeyInput = document.getElementById('cerebrasKey');
796
 
797
  settingsBtn.addEventListener('click', () => {
798
+ /* Load existing API keys from localStorage when modal is opened */
799
  nebiusKeyInput.value = localStorage.getItem('nebius-api-key') || '';
800
  cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || '';
801
+ settingsModal.classList.add('show'); /* Show the modal */
802
  });
803
 
804
  function closeSettings() {
805
+ settingsModal.classList.remove('show'); /* Hide the modal */
806
  }
807
 
808
  function saveSettings() {
809
  const nebiusKey = nebiusKeyInput.value.trim();
810
  const cerebrasKey = cerebrasKeyInput.value.trim();
811
 
812
+ /* Save keys to localStorage if they are provided */
813
  if (nebiusKey) localStorage.setItem('nebius-api-key', nebiusKey);
814
  if (cerebrasKey) localStorage.setItem('cerebras-api-key', cerebrasKey);
815
 
816
+ closeSettings(); /* Close the modal */
817
+ alert('API keys saved successfully!'); /* Confirmation */
818
  }
819
 
820
+ /* Close modal on escape key press or background click */
821
  settingsModal.addEventListener('click', (e) => {
822
+ if (e.target === settingsModal) closeSettings(); /* Close if background clicked */
823
  });
824
  document.addEventListener('keydown', (e) => {
825
  if (e.key === 'Escape' && settingsModal.classList.contains('show')) {
826
+ closeSettings(); /* Close if Escape key pressed and modal is shown */
827
  }
828
  });
829
 
 
837
  }
838
  });
839
 
840
+ /* smooth fade in animation for the container */
841
  document.addEventListener('DOMContentLoaded',()=>{
842
  const sheet=document.querySelector('.container');
843
  sheet.style.opacity='0';
 
849
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
850
  const savedTheme = localStorage.getItem('note-theme');
851
 
852
+ initTheme(); /* Initialize theme on load */
853
  btn.addEventListener('click',()=>{
854
+ document.body.classList.toggle('dark'); /* Toggle dark class on body */
855
+ updateIcon(); /* Update theme toggle icon */
856
+ localStorage.setItem('note-theme',document.body.classList.contains('dark')?'dark':'light'); /* Save theme preference */
857
  });
858
+
859
  function initTheme(){
860
+ /* Set initial theme based on saved preference or system preference */
861
  if(savedTheme){
862
  document.body.classList.toggle('dark',savedTheme==='dark');
863
  }else if(prefersDark.matches){
864
  document.body.classList.add('dark');
865
  }
866
+ updateIcon(); /* Set initial icon */
867
  }
868
+
869
  function updateIcon(){
870
+ /* Update the moon/sun icon based on current theme */
871
  btn.textContent=document.body.classList.contains('dark')?'☀️':'🌙';
872
  }
873
  </script>
874
  </body>
875
+ </html>