Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>LaTeX Notepad</title> | |
| <!-- ──────── MathJax ──────── --> | |
| <script> | |
| window.MathJax = { | |
| tex: { | |
| inlineMath: [['$', '$'], ['\\(', '\\)']], | |
| displayMath: [['$$', '$$'], ['\\[', '\\]']], | |
| processEscapes: true, | |
| processEnvironments: true, | |
| }, | |
| options: { skipHtmlTags: ['script','noscript','style','textarea','pre'] } | |
| }; | |
| </script> | |
| <script | |
| id="MathJax-script" | |
| async | |
| src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" | |
| ></script> | |
| <!-- marked.js for Markdown --> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <!-- Google fonts --> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Crimson+Text:wght@400;600&family=Libre+Baskerville:wght@400;700&family=PT+Mono&display=swap" | |
| rel="stylesheet" | |
| /> | |
| <style> | |
| /* ───────────────────────────────────────── | |
| COLOR SYSTEM (light / dark via variables) | |
| ───────────────────────────────────────── */ | |
| :root { | |
| --desk-bg: #fbf9f5; | |
| --desk-dot: #e2dccd; | |
| --paper-bg: #fffefa; | |
| --paper-text: #222; | |
| --shadow: rgba(0,0,0,.35); | |
| --line: rgba(201,190,170,.18); | |
| --perforation: #d0c6b7; | |
| --tbl-border: #d7cebf; | |
| --blockquote-bg: #fbf8f1; | |
| --blockquote-bar: #ccbfae; | |
| --code-bg: #f4f2ec; | |
| --code-border: #e6e0d2; | |
| --inline-code-bg: #f2efe8; | |
| } | |
| body.dark { | |
| --desk-bg: #2c2a27; | |
| --desk-dot: #3a3733; | |
| --paper-bg: #302e2b; | |
| --paper-text: #e9e7e2; | |
| --shadow: rgba(0,0,0,.55); | |
| --line: rgba(110,103,94,.28); | |
| --perforation: #6d6456; | |
| --tbl-border: #555048; | |
| --blockquote-bg: #38332e; | |
| --blockquote-bar: #6a604e; | |
| --code-bg: #3a3530; | |
| --code-border: #514b42; | |
| --inline-code-bg: #4a443d; | |
| } | |
| /* ───────────────────────────────────────── | |
| GLOBAL "DESK" BACKGROUND | |
| ───────────────────────────────────────── */ | |
| html,body{height:100%} | |
| body{ | |
| margin:0; | |
| background: var(--desk-bg); | |
| background-image: radial-gradient(var(--desk-dot) 1px,transparent 1px); | |
| background-size:14px 14px; | |
| font-family:'Crimson Text','Times New Roman',serif; | |
| color:var(--paper-text); | |
| line-height:1.8; | |
| -webkit-font-smoothing:antialiased; | |
| } | |
| /* ───────────────────────────────────────── | |
| PAPER SHEET | |
| ───────────────────────────────────────── */ | |
| .container{ | |
| max-width:840px; | |
| margin:40px auto; | |
| padding:40px 60px 60px; | |
| background:var(--paper-bg); | |
| color:var(--paper-text); | |
| border:1px solid rgba(0,0,0,.05); | |
| border-radius:12px 12px 10px 10px; | |
| position:relative; | |
| box-shadow:0 18px 40px -22px var(--shadow), | |
| inset 0 2px 6px rgba(0,0,0,.06); | |
| background-size:160px 160px,100% 100%; | |
| } | |
| /* perforation holes */ | |
| .container::before{ | |
| content:''; | |
| position:absolute;top:26px;bottom:26px;left:30px;width:9px; | |
| background-image:radial-gradient(circle var(--perforation) 0%,var(--perforation) 2px,transparent 3px); | |
| background-size:9px 28px; | |
| background-repeat:repeat-y; | |
| pointer-events:none; | |
| } | |
| /* curled corner */ | |
| .container::after{ | |
| content:'';position:absolute;top:0;right:0;width:110px;height:110px; | |
| background: | |
| linear-gradient(135deg,rgba(0,0,0,.08) 0%,rgba(0,0,0,0) 42%), | |
| linear-gradient(135deg,var(--paper-bg) 0%,var(--paper-bg) 50%,rgba(255,255,255,0) 51%); | |
| background-size:100% 100%; | |
| border-bottom-left-radius:12px; | |
| transform:translate(1px,-1px); | |
| pointer-events:none; | |
| } | |
| /* ───────── theme toggle ───────── */ | |
| #themeToggle{ | |
| position:absolute;top:12px;right:14px; | |
| font-size:20px;background:none;border:none;cursor:pointer; | |
| transition:transform .25s; | |
| user-select:none; | |
| z-index:10; | |
| } | |
| #themeToggle:hover{transform:rotate(20deg)scale(1.15)} | |
| /* ───────── settings button ───────── */ | |
| #settingsBtn{ | |
| position:absolute;top:12px;right:50px; | |
| font-size:20px;background:none;border:none;cursor:pointer; | |
| transition:transform .25s; | |
| user-select:none; | |
| z-index:10; | |
| } | |
| #settingsBtn:hover{transform:rotate(20deg)scale(1.15)} | |
| /* ───────── header ───────── */ | |
| .header{text-align:center;margin-bottom:34px;padding-bottom:18px;border-bottom:1px solid rgba(0,0,0,.05)} | |
| h1{font-family:'Libre Baskerville',serif;margin:0;font-size:30px;letter-spacing:.5px} | |
| .subtitle{font-family:'PT Mono',monospace;font-size:14px;color:#666;margin-top:6px;letter-spacing:1px} | |
| /* ───────── content area ───────── */ | |
| #content{ | |
| min-height:520px;font-size:18px;position:relative; | |
| padding:10px 0 10px 26px;overflow-wrap:break-word;hyphens:auto; | |
| } | |
| #content::before{ | |
| content:'';position:absolute;inset:0; | |
| background:repeating-linear-gradient( | |
| 0deg, | |
| transparent,transparent 2.65em, | |
| var(--line) 2.65em,var(--line) 2.7em); | |
| pointer-events:none;z-index:1; | |
| } | |
| #content *{position:relative;z-index:2} | |
| .placeholder{color:#888;font-style:italic;text-align:center;padding:110px 20px;user-select:none} | |
| /* ───────── markdown tweaks ───────── */ | |
| blockquote{ | |
| border-left:4px solid var(--blockquote-bar); | |
| margin:20px 0;padding:15px 26px; | |
| background:var(--blockquote-bg);font-style:italic | |
| } | |
| code{font-family:'PT Mono',monospace;background:var(--inline-code-bg); | |
| padding:2px 6px;border-radius:3px;font-size:.9em} | |
| pre{background:var(--code-bg);padding:16px 20px;border:1px solid var(--code-border); | |
| border-radius:6px;overflow-x:auto;font-family:'PT Mono',monospace} | |
| /* lists */ | |
| ol{counter-reset:item;padding-left:0;list-style:none} | |
| ol>li{counter-increment:item;margin:.5em 0 .5em 2em} | |
| ol>li::before{content:counter(item)')';display:inline-block;width:1.5em;margin-left:-2em;text-align:right;font-weight:600} | |
| ol ol>li::before{content:counter(item,lower-alpha)')'} | |
| /* ───────── TABLES ───────── */ | |
| table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums;margin:1.2em 0} | |
| thead tr{border-bottom:1px solid var(--tbl-border)} | |
| tbody tr:not(:last-child){border-bottom:1px solid var(--tbl-border)} | |
| th,td{padding:.55em .8em;text-align:right} | |
| th{font-weight:600} | |
| /* ───────── processing badge ───────── */ | |
| .processing{ | |
| position:fixed;top:20px;right:20px;background:#333;color:#fff; | |
| padding:10px 20px;border-radius:6px;font-family:'PT Mono',monospace; | |
| font-size:14px;opacity:0;transition:opacity .25s;z-index:2000 | |
| } | |
| .processing.show{opacity:.9} | |
| /* ───────── settings modal ───────── */ | |
| .modal{ | |
| display:none; | |
| position:fixed;top:0;left:0;width:100%;height:100%; | |
| background:rgba(0,0,0,0.5);z-index:3000; | |
| } | |
| .modal.show{display:flex;align-items:center;justify-content:center} | |
| .modal-content{ | |
| background:var(--paper-bg); | |
| color:var(--paper-text); | |
| padding:30px; | |
| border-radius:8px; | |
| max-width:500px; | |
| width:90%; | |
| box-shadow:0 20px 60px rgba(0,0,0,0.3); | |
| } | |
| .modal h2{margin-top:0;font-family:'Libre Baskerville',serif} | |
| .modal label{ | |
| display:block; | |
| margin-top:15px; | |
| font-weight:600; | |
| font-size:14px; | |
| font-family:'PT Mono',monospace; | |
| } | |
| .modal input{ | |
| width:100%; | |
| padding:8px; | |
| margin-top:5px; | |
| border:1px solid var(--code-border); | |
| border-radius:4px; | |
| background:var(--code-bg); | |
| color:var(--paper-text); | |
| font-family:'PT Mono',monospace; | |
| font-size:13px; | |
| box-sizing: border-box; | |
| } | |
| .modal-buttons{ | |
| margin-top:20px; | |
| display:flex; | |
| gap:10px; | |
| justify-content:flex-end; | |
| } | |
| .modal button{ | |
| padding:8px 16px; | |
| border:none; | |
| border-radius:4px; | |
| cursor:pointer; | |
| font-family:'PT Mono',monospace; | |
| font-size:14px; | |
| } | |
| .btn-save{ | |
| background:#4a5f4a; | |
| color:#fff; | |
| } | |
| .btn-cancel{ | |
| background:var(--code-bg); | |
| color:var(--paper-text); | |
| border:1px solid var(--code-border); | |
| } | |
| .api-hint{ | |
| font-size:11px; | |
| color:#888; | |
| margin-top:3px; | |
| font-style:italic; | |
| } | |
| /* Styles for the lightweight streaming area */ | |
| .mono-stream{ | |
| font-family:'PT Mono',monospace; | |
| white-space:pre-wrap; | |
| background:var(--code-bg); | |
| border:1px solid var(--code-border); | |
| padding:12px;border-radius:6px; | |
| margin-top: 8px; /* Add some spacing */ | |
| } | |
| /* ───────── copy button styles ───────── */ | |
| .copy-btn { | |
| display: inline-block; | |
| margin-left: 10px; | |
| padding: 4px 8px; | |
| background: var(--code-bg); | |
| border: 1px solid var(--code-border); | |
| border-radius: 4px; | |
| font-family: 'PT Mono', monospace; | |
| font-size: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| user-select: none; | |
| } | |
| .copy-btn:hover { | |
| background: var(--blockquote-bg); | |
| transform: translateY(-1px); | |
| } | |
| .copy-btn.copied { | |
| background: #4a5f4a; | |
| color: #fff; | |
| border-color: #4a5f4a; | |
| } | |
| .section-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 0; | |
| } | |
| .section-header strong { | |
| margin: 0; | |
| } | |
| /* responsive & print */ | |
| @media(max-width:768px){ | |
| .container{margin:20px 16px;padding:28px} | |
| #content{font-size:16px} | |
| h1{font-size:24px} | |
| } | |
| @media print{ | |
| body{background:#fff} | |
| .container{box-shadow:none;border:none} | |
| .header,.processing,.instructions,#themeToggle,#settingsBtn,.copy-btn{display:none} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- theme icon --> | |
| <button id="themeToggle" title="Toggle dark / light">🌙</button> | |
| <!-- settings icon --> | |
| <button id="settingsBtn" title="API Settings">⚙️</button> | |
| <div class="header"> | |
| <h1>LaTeX Notepad</h1> | |
| <div class="subtitle">press ctrl+v anywhere to render</div> | |
| </div> | |
| <div id="content"> | |
| <div class="placeholder"> | |
| Press <kbd>Ctrl</kbd>+<kbd>V</kbd> (or <kbd>⌘</kbd>+<kbd>V</kbd>) to paste and render Markdown / LaTeX<br> | |
| <small style="font-size:14px;opacity:0.8">You can also paste images to OCR and solve them!</small> | |
| </div> | |
| </div> | |
| <div class="instructions" style="text-align:center;font-family:'PT Mono',monospace;font-size:14px;color:#666;font-style:italic;margin-top:22px;"> | |
| Tip: you can paste raw Markdown, TeX, or images – they will be processed instantly ✨ | |
| </div> | |
| </div> | |
| <div class="processing">Processing…</div> | |
| <!-- Settings Modal --> | |
| <div id="settingsModal" class="modal"> | |
| <div class="modal-content"> | |
| <h2>API Settings</h2> | |
| <label for="nebiusKey">Nebius API Key:</label> | |
| <input type="password" id="nebiusKey" placeholder="Enter your Nebius API key" autocomplete="off"> | |
| <div class="api-hint">Used for OCR image processing</div> | |
| <label for="cerebrasKey">Cerebras API Key:</label> | |
| <input type="password" id="cerebrasKey" placeholder="Enter your Cerebras API key" autocomplete="off"> | |
| <div class="api-hint">Used for solving questions</div> | |
| <div class="modal-buttons"> | |
| <button class="btn-cancel" onclick="closeSettings()">Cancel</button> | |
| <button class="btn-save" onclick="saveSettings()">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /* ======= processing badge helpers ======= */ | |
| const content = document.getElementById('content'); | |
| const processingNode = document.querySelector('.processing'); | |
| function showProcessing(text = 'Processing…'){ | |
| processingNode.textContent = text; | |
| processingNode.classList.add('show'); | |
| } | |
| function hideProcessing(){ | |
| setTimeout(()=>processingNode.classList.remove('show'),300); | |
| } | |
| /* ======= Copy to clipboard helper ======= */ | |
| function copyToClipboard(text, button) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| const originalText = button.textContent; | |
| button.textContent = '✓ Copied'; | |
| button.classList.add('copied'); | |
| setTimeout(() => { | |
| button.textContent = originalText; | |
| button.classList.remove('copied'); | |
| }, 2000); | |
| }).catch(err => { | |
| console.error('Failed to copy:', err); | |
| alert('Failed to copy to clipboard'); | |
| }); | |
| } | |
| /* ======= markdown + latex pipeline ======= */ | |
| function processContent(text){ | |
| showProcessing(); | |
| const store=[], PL=i=>`%%LATEX_${i}%%`; let idx=0; | |
| const keep=m=>(store.push(m),PL(idx++)); | |
| text = text | |
| .replace(/\\\[[\s\S]*?\\\]/g, keep) // \[ ... \] | |
| .replace(/\$\$[\s\S]*?\$\$/g, keep) // $$ ... $$ | |
| .replace(/\\\([\s\S]*?\\\)/g, keep) // \( ... \) | |
| .replace(/\$([^\$\n]+?)\$/g, keep); // $ ... $ | |
| let html = marked.parse(text); | |
| store.forEach((latex,i)=>{html=html.replaceAll(PL(i),latex)}); | |
| content.innerHTML = html; | |
| if(window.MathJax?.typesetPromise){ | |
| MathJax.typesetPromise([content]).then(hideProcessing) | |
| .catch(e=>{console.error('MathJax error:',e);hideProcessing()}); | |
| }else{hideProcessing()} | |
| } | |
| /* ======= Image to Base64 converter ======= */ | |
| async function imageToBase64(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| // Ensure it's a valid data URL and extract base64 part | |
| if (reader.result && reader.result.includes(',')) { | |
| const base64 = reader.result.split(',')[1]; | |
| resolve(base64); | |
| } else { | |
| reject(new Error("Failed to read file as Data URL.")); | |
| } | |
| }; | |
| reader.onerror = () => reject(reader.error); | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| /* ======= OCR with Nebius API ======= */ | |
| async function ocrImage(base64Image) { | |
| const nebiusKey = localStorage.getItem('nebius-api-key'); | |
| if (!nebiusKey) { | |
| alert('Please set your Nebius API key in settings (⚙️)'); | |
| return null; | |
| } | |
| showProcessing('Extracting text from image...'); | |
| try { | |
| const response = await fetch('https://api.studio.nebius.com/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Accept': '*/*', | |
| 'Authorization': `Bearer ${nebiusKey}` | |
| }, | |
| body: JSON.stringify({ | |
| model: 'google/gemma-3-27b-it', | |
| messages: [ | |
| { | |
| role: 'system', | |
| 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.' | |
| }, | |
| { | |
| role: 'user', | |
| content: [ | |
| { type: 'text', text: 'Image:' }, | |
| { | |
| type: 'image_url', | |
| image_url: { url: `data:image/png;base64,${base64Image}` } // Assuming PNG, adjust if needed | |
| } | |
| ] | |
| } | |
| ] | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`OCR API error: ${response.status} - ${errorText}`); | |
| } | |
| const data = await response.json(); | |
| return data.choices[0].message.content; | |
| } catch (error) { | |
| console.error('OCR Error:', error); | |
| alert('Error during OCR: ' + error.message); | |
| return null; | |
| } | |
| } | |
| /* ======= UI Helpers for Streaming ======= */ | |
| let currentQuestion = ''; | |
| let currentAnswer = ''; | |
| function beginStreamingUI(question){ | |
| currentQuestion = question; | |
| currentAnswer = ''; | |
| // Show a lightweight, non-MathJax view while the model streams | |
| content.innerHTML = ` | |
| <div> | |
| <div class="section-header"> | |
| <p><strong>Question</strong>:</p> | |
| <button class="copy-btn" onclick="copyToClipboard(currentQuestion, this)">📋 Copy</button> | |
| </div> | |
| <div class="mono-stream" id="qStream"></div> | |
| <hr style="opacity:.35; margin: 20px 0;"> | |
| <div class="section-header"> | |
| <p><strong>Answer</strong>:</p> | |
| <button class="copy-btn" onclick="copyToClipboard(currentAnswer, this)">📋 Copy</button> | |
| </div> | |
| <div class="mono-stream" id="aStream">(generating...)</div> | |
| </div>`; | |
| const qEl = document.getElementById('qStream'); | |
| const aEl = document.getElementById('aStream'); | |
| qEl.textContent = question; // plain text now; pretty render later | |
| aEl.textContent = ''; // clear "(generating...)" | |
| return { qEl, aEl }; | |
| } | |
| function finalizeStreaming(question, fullAnswer){ | |
| currentQuestion = question; | |
| currentAnswer = fullAnswer; | |
| // Create HTML with copy buttons | |
| const htmlContent = ` | |
| <div class="section-header"> | |
| <strong>Question</strong>: | |
| <button class="copy-btn" onclick="copyToClipboard(currentQuestion, this)">📋 Copy</button> | |
| </div> | |
| <div style="margin-bottom: 20px;">${question}</div> | |
| <div class="section-header"> | |
| <strong>Answer</strong>: | |
| <button class="copy-btn" onclick="copyToClipboard(currentAnswer, this)">📋 Copy</button> | |
| </div> | |
| <div>${fullAnswer}</div>`; | |
| // Process the content with Markdown and MathJax | |
| processContent(htmlContent); | |
| } | |
| /* ======= Solve with Cerebras API (Streaming Optimization) ======= */ | |
| async function solveQuestion(question) { | |
| const cerebrasKey = localStorage.getItem('cerebras-api-key'); | |
| if (!cerebrasKey) { | |
| alert('Please set your Cerebras API key in settings (⚙️)'); | |
| return null; | |
| } | |
| showProcessing('Solving the question...'); | |
| const ui = beginStreamingUI(question); // Prepare the lightweight streaming UI | |
| try { | |
| const response = await fetch('https://api.cerebras.ai/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Accept': 'text/event-stream', | |
| 'Authorization': `Bearer ${cerebrasKey}` | |
| }, | |
| body: JSON.stringify({ | |
| model: 'gpt-oss-120b', | |
| stream: true, | |
| max_tokens: 65536, | |
| temperature: 0.1, // Set temperature to 0.1 | |
| reasoning_effort: 'medium', // Set reasoning_effort to 'medium' | |
| // top_p: 1, // Removed as per user's request | |
| messages: [ | |
| { role: 'system', content: 'Solve this Question. Provide a clear, step-by-step solution.' }, | |
| { role: 'user', content: question } | |
| ] | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`Cerebras API error: ${response.status} - ${errorText}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullAnswer = ''; | |
| let buffer = ''; // buffer for partial SSE frames | |
| let lastFlushTime = 0; | |
| const flushThrottle = 120; // milliseconds to wait between DOM updates | |
| const flushUI = () => { | |
| // Update the lightweight streaming area without MathJax | |
| ui.aEl.textContent = fullAnswer; | |
| currentAnswer = fullAnswer; // Update global variable for copy button | |
| lastFlushTime = performance.now(); | |
| }; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| // SSE events are typically separated by '\n\n' | |
| const events = buffer.split('\n\n'); | |
| buffer = events.pop() || ''; // Keep any incomplete event for the next chunk | |
| for (const evt of events) { | |
| // Find the 'data:' line, which contains the JSON payload | |
| const dataLine = evt.split('\n').find(line => line.trim().startsWith('data: ')); | |
| if (!dataLine) continue; | |
| const data = dataLine.slice(6).trim(); // Remove 'data: ' prefix | |
| if (data === '[DONE]') continue; // End of stream marker | |
| try { | |
| const parsed = JSON.parse(data); | |
| // Extract content, being flexible with potential API response structures | |
| const deltaContent = parsed.choices?.[0]?.delta?.content | |
| ?? parsed.choices?.[0]?.message?.content | |
| ?? parsed.choices?.[0]?.text // Some APIs might use 'text' | |
| ?? ''; | |
| if (deltaContent) { | |
| fullAnswer += deltaContent; | |
| // Throttle DOM updates to prevent excessive rendering and jank | |
| if (performance.now() - lastFlushTime > flushThrottle) { | |
| flushUI(); | |
| } | |
| } | |
| } catch (e) { | |
| // Ignore errors parsing JSON chunks if it's just partial data | |
| console.error('Error parsing stream chunk data:', e, 'Chunk:', data); | |
| } | |
| } | |
| } | |
| // Final flush to ensure all streamed content is displayed in the lightweight view | |
| flushUI(); | |
| // Once streaming is complete, perform the final, heavier render with Markdown and MathJax | |
| finalizeStreaming(question, fullAnswer); | |
| return fullAnswer; | |
| } catch (error) { | |
| console.error('Solving Error:', error); | |
| alert('Error during solving: ' + error.message); | |
| hideProcessing(); // Ensure the processing indicator is hidden on error | |
| return null; | |
| } | |
| } | |
| /* ======= Process image pipeline ======= */ | |
| async function processImage(file) { | |
| try { | |
| // Convert image to base64 | |
| const base64 = await imageToBase64(file); | |
| // OCR the image | |
| const ocrText = await ocrImage(base64); | |
| if (!ocrText) { | |
| hideProcessing(); | |
| return; | |
| } | |
| // Solve the question | |
| const answer = await solveQuestion(ocrText); | |
| // The solveQuestion function now handles hiding the processing indicator | |
| // unless an error occurred, in which case it was hidden earlier. | |
| } catch (error) { | |
| console.error('Image processing error:', error); | |
| alert('Error processing image: ' + error.message); | |
| hideProcessing(); | |
| } | |
| } | |
| /* ======= FIXED paste listener - allows normal paste in input fields ======= */ | |
| document.addEventListener('paste', async (e) => { | |
| // Check if we're pasting into an input, textarea, or contenteditable element | |
| const activeElement = document.activeElement; | |
| const isInputField = activeElement && ( | |
| activeElement.tagName === 'INPUT' || | |
| activeElement.tagName === 'TEXTAREA' || | |
| activeElement.isContentEditable === true // Use isContentEditable for modern check | |
| ); | |
| // If pasting into an input field, let the browser handle it normally | |
| if (isInputField) { | |
| return; // Don't prevent default, let normal paste happen | |
| } | |
| // Otherwise, handle custom paste logic | |
| e.preventDefault(); | |
| // Check for image files first | |
| const items = Array.from(e.clipboardData.items); | |
| const imageItem = items.find(item => item.type.startsWith('image/')); | |
| if (imageItem) { | |
| // Handle image paste | |
| const file = imageItem.getAsFile(); | |
| if (file) { | |
| await processImage(file); | |
| } else { | |
| alert("Could not get image file from clipboard."); | |
| } | |
| } else { | |
| // Handle text paste (existing functionality) | |
| const txt = e.clipboardData.getData('text/plain'); | |
| if (txt.trim()) processContent(txt); | |
| } | |
| }); | |
| /* ======= Settings modal functions ======= */ | |
| const settingsBtn = document.getElementById('settingsBtn'); | |
| const settingsModal = document.getElementById('settingsModal'); | |
| const nebiusKeyInput = document.getElementById('nebiusKey'); | |
| const cerebrasKeyInput = document.getElementById('cerebrasKey'); | |
| settingsBtn.addEventListener('click', () => { | |
| // Load existing keys | |
| nebiusKeyInput.value = localStorage.getItem('nebius-api-key') || ''; | |
| cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || ''; | |
| settingsModal.classList.add('show'); | |
| }); | |
| function closeSettings() { | |
| settingsModal.classList.remove('show'); | |
| } | |
| function saveSettings() { | |
| const nebiusKey = nebiusKeyInput.value.trim(); | |
| const cerebrasKey = cerebrasKeyInput.value.trim(); | |
| if (nebiusKey) localStorage.setItem('nebius-api-key', nebiusKey); | |
| if (cerebrasKey) localStorage.setItem('cerebras-api-key', cerebrasKey); | |
| closeSettings(); | |
| alert('API keys saved successfully!'); | |
| } | |
| // Close modal on escape or background click | |
| settingsModal.addEventListener('click', (e) => { | |
| if (e.target === settingsModal) closeSettings(); | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape' && settingsModal.classList.contains('show')) { | |
| closeSettings(); | |
| } | |
| }); | |
| /* small "bounce" on placeholder click */ | |
| content.addEventListener('click',()=>{ | |
| const ph=content.querySelector('.placeholder'); | |
| if(ph){ | |
| ph.style.transform='scale(.97)'; | |
| ph.style.transition='transform .12s'; | |
| setTimeout(()=>ph.style.transform='scale(1)',120); | |
| } | |
| }); | |
| /* smooth fade in */ | |
| document.addEventListener('DOMContentLoaded',()=>{ | |
| const sheet=document.querySelector('.container'); | |
| sheet.style.opacity='0'; | |
| setTimeout(()=>{sheet.style.transition='opacity .6s ease';sheet.style.opacity='1'},80); | |
| }); | |
| /* ======= theme toggler ======= */ | |
| const btn = document.getElementById('themeToggle'); | |
| const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); | |
| const savedTheme = localStorage.getItem('note-theme'); | |
| initTheme(); | |
| btn.addEventListener('click',()=>{ | |
| document.body.classList.toggle('dark'); | |
| updateIcon(); | |
| localStorage.setItem('note-theme',document.body.classList.contains('dark')?'dark':'light'); | |
| }); | |
| function initTheme(){ | |
| if(savedTheme){ | |
| document.body.classList.toggle('dark',savedTheme==='dark'); | |
| }else if(prefersDark.matches){ | |
| document.body.classList.add('dark'); | |
| } | |
| updateIcon(); | |
| } | |
| function updateIcon(){ | |
| btn.textContent=document.body.classList.contains('dark')?'☀️':'🌙'; | |
| } | |
| </script> | |
| </body> | |
| </html> |