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

Update index.html

Browse files
Files changed (1) hide show
  1. 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; /* Add some spacing */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}` } // Assuming PNG, adjust if needed
424
  }
425
  ]
426
  }
@@ -429,44 +450,77 @@ async function ocrImage(base64Image) {
429
  });
430
 
431
  if (!response.ok) {
432
- const errorText = await response.text();
433
- throw new Error(`OCR API error: ${response.status} - ${errorText}`);
434
  }
435
 
436
  const data = await response.json();
437
  return data.choices[0].message.content;
438
- } catch (error) {
439
- console.error('OCR Error:', error);
440
- alert('Error during OCR: ' + error.message);
441
  return null;
442
  }
443
  }
444
 
445
- /* ======= UI Helpers for Streaming ======= */
446
  function beginStreamingUI(question){
447
- // Show a lightweight, non-MathJax view while the model streams
448
  content.innerHTML = `
449
- <div>
450
- <p><strong>Question</strong>:</p>
451
- <div class="mono-stream" id="qStream"></div>
 
452
  <hr style="opacity:.35; margin: 20px 0;">
453
- <p><strong>Answer</strong>:</p>
454
- <div class="mono-stream" id="aStream">(generating...)</div>
455
- </div>`;
 
456
  const qEl = document.getElementById('qStream');
457
  const aEl = document.getElementById('aStream');
458
- qEl.textContent = question; // plain text now; pretty render later
459
- aEl.textContent = ''; // clear "(generating...)"
460
  return { qEl, aEl };
461
  }
462
 
 
463
  function finalizeStreaming(question, fullAnswer){
464
- // One single heavy render (Markdown + MathJax) at the end
465
- const formatted = `**Question**: ${question}\n\n**Answer**: ${fullAnswer}`;
466
- processContent(formatted); // processContent calls hideProcessing after MathJax
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  }
468
 
469
- /* ======= Solve with Cerebras API (Streaming Optimization) ======= */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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); // Prepare the lightweight streaming UI
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', // Specify that we expect a 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, // Set temperature to 0.1
493
- reasoning_effort: 'medium', // Set reasoning_effort to 'medium'
494
- // top_p: 1, // Removed as per user's request
495
  messages: [
496
- { role: 'system', content: 'Solve this Question. Provide a clear, step-by-step solution.' },
497
- { role: 'user', content: question }
498
  ]
499
  })
500
  });
501
 
502
  if (!response.ok) {
503
- const errorText = await response.text();
504
- throw new Error(`Cerebras API error: ${response.status} - ${errorText}`);
505
  }
506
 
507
  const reader = response.body.getReader();
508
  const decoder = new TextDecoder();
509
  let fullAnswer = '';
510
- let buffer = ''; // buffer for partial SSE frames
511
- let lastFlushTime = 0;
512
- const flushThrottle = 120; // milliseconds to wait between DOM updates
513
 
514
- const flushUI = () => {
515
- // Update the lightweight streaming area without MathJax
516
  ui.aEl.textContent = fullAnswer;
517
- lastFlushTime = performance.now();
518
  };
519
 
520
  while (true) {
521
- const { done, value } = await reader.read();
522
  if (done) break;
523
 
524
- buffer += decoder.decode(value, { stream: true });
525
- // SSE events are typically separated by '\n\n'
526
  const events = buffer.split('\n\n');
527
- buffer = events.pop() || ''; // Keep any incomplete event for the next chunk
528
 
529
- for (const evt of events) {
530
- // Find the 'data:' line, which contains the JSON payload
531
- const dataLine = evt.split('\n').find(line => line.trim().startsWith('data: '));
532
  if (!dataLine) continue;
533
-
534
- const data = dataLine.slice(6).trim(); // Remove 'data: ' prefix
535
- if (data === '[DONE]') continue; // End of stream marker
536
 
537
  try {
538
- const parsed = JSON.parse(data);
539
- // Extract content, being flexible with potential API response structures
540
- const deltaContent = parsed.choices?.[0]?.delta?.content
541
- ?? parsed.choices?.[0]?.message?.content
542
- ?? parsed.choices?.[0]?.text // Some APIs might use 'text'
543
- ?? '';
544
-
545
- if (deltaContent) {
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
- // Ignore errors parsing JSON chunks if it's just partial data
554
- console.error('Error parsing stream chunk data:', e, 'Chunk:', data);
555
  }
556
  }
557
  }
558
 
559
- // Final flush to ensure all streamed content is displayed in the lightweight view
560
- flushUI();
561
-
562
- // Once streaming is complete, perform the final, heavier render with Markdown and MathJax
563
  finalizeStreaming(question, fullAnswer);
564
  return fullAnswer;
565
- } catch (error) {
566
- console.error('Solving Error:', error);
567
- alert('Error during solving: ' + error.message);
568
- hideProcessing(); // Ensure the processing indicator is hidden on error
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
- // OCR the image
580
- const ocrText = await ocrImage(base64);
581
- if (!ocrText) {
582
- hideProcessing();
583
- return;
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
- /* ======= FIXED paste listener - allows normal paste in input fields ======= */
599
- document.addEventListener('paste', async (e) => {
600
- // Check if we're pasting into an input, textarea, or contenteditable element
601
- const activeElement = document.activeElement;
602
- const isInputField = activeElement && (
603
- activeElement.tagName === 'INPUT' ||
604
- activeElement.tagName === 'TEXTAREA' ||
605
- activeElement.isContentEditable === true // Use isContentEditable for modern check
606
  );
607
-
608
- // If pasting into an input field, let the browser handle it normally
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 imageItem = items.find(item => item.type.startsWith('image/'));
619
-
620
- if (imageItem) {
621
- // Handle image paste
622
- const file = imageItem.getAsFile();
623
- if (file) {
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 functions ======= */
636
- const settingsBtn = document.getElementById('settingsBtn');
637
  const settingsModal = document.getElementById('settingsModal');
638
- const nebiusKeyInput = document.getElementById('nebiusKey');
639
  const cerebrasKeyInput = document.getElementById('cerebrasKey');
640
 
641
  settingsBtn.addEventListener('click', () => {
642
- // Load existing keys
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 nebiusKey = nebiusKeyInput.value.trim();
654
- const cerebrasKey = cerebrasKeyInput.value.trim();
655
-
656
- if (nebiusKey) localStorage.setItem('nebius-api-key', nebiusKey);
657
- if (cerebrasKey) localStorage.setItem('cerebras-api-key', cerebrasKey);
658
-
659
  closeSettings();
660
  alert('API keys saved successfully!');
661
  }
662
 
663
- // Close modal on escape or background click
664
- settingsModal.addEventListener('click', (e) => {
665
- if (e.target === settingsModal) closeSettings();
666
- });
667
- document.addEventListener('keydown', (e) => {
668
- if (e.key === 'Escape' && settingsModal.classList.contains('show')) {
669
- closeSettings();
670
- }
671
- });
672
 
673
- /* small "bounce" on placeholder click */
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
- /* smooth fade in */
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
- /* ======= theme toggler ======= */
691
- const btn = document.getElementById('themeToggle');
692
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
693
- const savedTheme = localStorage.getItem('note-theme');
694
 
695
  initTheme();
696
- btn.addEventListener('click',()=>{
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
- document.body.classList.toggle('dark',savedTheme==='dark');
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, 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
  /* ======= 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>