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; | |
| } | |
| /* ───────── copy button & QA block styling ───────── */ | |
| .copy-btn{ | |
| background:none; | |
| border:none; | |
| cursor:pointer; | |
| font-size:0.9em; | |
| margin-left:8px; | |
| vertical-align:middle; | |
| color:var(--paper-text); | |
| } | |
| .copy-btn:hover{ | |
| transform:scale(1.1); | |
| } | |
| .qa-block{ | |
| margin-bottom:1.5em; | |
| } | |
| .qa-header{ | |
| display:flex; | |
| align-items:baseline; | |
| margin-bottom:0.4em; | |
| } | |
| /* 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{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); | |
| } | |
| /* ======= 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 = () => { | |
| 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$$. DO NOT USE ITEMIZE. DO NOT SOLVE THE QUESTION. DO NOT OUTPUT ANYTHING BUT THE FORMAT OF THE QUESTION.' | |
| }, | |
| { | |
| role: 'user', | |
| content: [ | |
| { type: 'text', text: 'Image:' }, | |
| { | |
| type: 'image_url', | |
| image_url: { url: `data:image/png;base64,${base64Image}` } | |
| } | |
| ] | |
| } | |
| ] | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const err = await response.text(); | |
| throw new Error(`OCR API error: ${response.status} – ${err}`); | |
| } | |
| const data = await response.json(); | |
| return data.choices[0].message.content; | |
| } catch (e) { | |
| console.error('OCR error:', e); | |
| alert('Error during OCR: ' + e.message); | |
| return null; | |
| } | |
| } | |
| /* ======= UI helpers for streaming ======= */ | |
| function beginStreamingUI(question){ | |
| content.innerHTML = ` | |
| <div class="qa-block"> | |
| <div class="qa-header"><strong>Question</strong> <button class="copy-btn" data-copy-id="qStream" title="Copy question">📋</button></div> | |
| <div class="mono-stream" id="qStream"></div> | |
| </div> | |
| <hr style="opacity:.35; margin: 20px 0;"> | |
| <div class="qa-block"> | |
| <div class="qa-header"><strong>Answer</strong> <button class="copy-btn" data-copy-id="aStream" title="Copy answer">📋</button></div> | |
| <div class="mono-stream" id="aStream">(generating...)</div> | |
| </div>`; | |
| const qEl = document.getElementById('qStream'); | |
| const aEl = document.getElementById('aStream'); | |
| qEl.textContent = question; | |
| aEl.textContent = ''; | |
| return { qEl, aEl }; | |
| } | |
| /* ======= Final render after streaming ======= */ | |
| function finalizeStreaming(question, fullAnswer){ | |
| const questionHTML = marked.parse(question); | |
| const answerHTML = marked.parse(fullAnswer); | |
| const finalHTML = ` | |
| <div class="qa-block"> | |
| <div class="qa-header"><strong>Question</strong> <button class="copy-btn" data-copy-id="finalQuestion" title="Copy question">📋</button></div> | |
| <div class="qa-content" id="finalQuestion">${questionHTML}</div> | |
| </div> | |
| <div class="qa-block"> | |
| <div class="qa-header"><strong>Answer</strong> <button class="copy-btn" data-copy-id="finalAnswer" title="Copy answer">📋</button></div> | |
| <div class="qa-content" id="finalAnswer">${answerHTML}</div> | |
| </div>`; | |
| content.innerHTML = finalHTML; | |
| if (window.MathJax?.typesetPromise) { | |
| MathJax.typesetPromise([content]).then(hideProcessing) | |
| .catch(e=>{console.error('MathJax error:',e);hideProcessing();}); | |
| } else { | |
| hideProcessing(); | |
| } | |
| } | |
| /* ======= Copy‑button handler (delegated) ======= */ | |
| content.addEventListener('click', e => { | |
| const btn = e.target.closest('.copy-btn'); | |
| if (!btn) return; | |
| const targetId = btn.dataset.copyId; | |
| const target = document.getElementById(targetId); | |
| if (!target) return; | |
| navigator.clipboard.writeText(target.innerText).then(() => { | |
| const original = btn.textContent; | |
| btn.textContent = '✅'; | |
| setTimeout(() => btn.textContent = original, 1200); | |
| }).catch(err => console.error('Copy failed', err)); | |
| }); | |
| /* ======= Solve with Cerebras API (streaming) ======= */ | |
| 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); // lightweight view while streaming | |
| 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, | |
| reasoning_effort: 'medium', | |
| messages: [ | |
| { role: 'system', content: 'Solve this Question. Provide a clear, step‑by‑step solution.' }, | |
| { role: 'user', content: question } | |
| ] | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const err = await response.text(); | |
| throw new Error(`Cerebras API error: ${response.status} – ${err}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullAnswer = ''; | |
| let buffer = ''; | |
| let lastFlush = 0; | |
| const FLUSH_MS = 120; // throttle UI updates | |
| const flush = () => { | |
| ui.aEl.textContent = fullAnswer; | |
| lastFlush = performance.now(); | |
| }; | |
| while (true) { | |
| const {done, value} = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, {stream:true}); | |
| const events = buffer.split('\n\n'); | |
| buffer = events.pop() || ''; | |
| for (const ev of events) { | |
| const dataLine = ev.split('\n').find(l => l.startsWith('data: ')); | |
| if (!dataLine) continue; | |
| const data = dataLine.slice(6).trim(); | |
| if (data === '[DONE]') continue; | |
| try { | |
| const json = JSON.parse(data); | |
| const delta = json.choices?.[0]?.delta?.content | |
| ?? json.choices?.[0]?.message?.content | |
| ?? json.choices?.[0]?.text | |
| ?? ''; | |
| if (delta) { | |
| fullAnswer += delta; | |
| if (performance.now() - lastFlush > FLUSH_MS) flush(); | |
| } | |
| } catch (e) { | |
| // ignore malformed chunks | |
| } | |
| } | |
| } | |
| // final UI update before heavy render | |
| flush(); | |
| finalizeStreaming(question, fullAnswer); | |
| return fullAnswer; | |
| } catch (e) { | |
| console.error('Solve error:', e); | |
| alert('Error while solving: ' + e.message); | |
| hideProcessing(); | |
| return null; | |
| } | |
| } | |
| /* ======= Process image pipeline ======= */ | |
| async function processImage(file) { | |
| try { | |
| const base64 = await imageToBase64(file); | |
| const ocr = await ocrImage(base64); | |
| if (!ocr) { hideProcessing(); return; } | |
| await solveQuestion(ocr); | |
| } catch (e) { | |
| console.error('Image pipeline error:', e); | |
| alert('Error processing image: ' + e.message); | |
| hideProcessing(); | |
| } | |
| } | |
| /* ======= Paste listener (keeps normal input fields functional) ======= */ | |
| document.addEventListener('paste', async e => { | |
| const active = document.activeElement; | |
| const isInput = active && ( | |
| active.tagName === 'INPUT' || | |
| active.tagName === 'TEXTAREA' || | |
| active.isContentEditable | |
| ); | |
| if (isInput) return; // let the browser handle normal paste | |
| e.preventDefault(); | |
| const items = Array.from(e.clipboardData.items); | |
| const imgItem = items.find(i => i.type.startsWith('image/')); | |
| if (imgItem) { | |
| const file = imgItem.getAsFile(); | |
| if (file) await processImage(file); | |
| else alert('Could not retrieve image from clipboard.'); | |
| } else { | |
| const txt = e.clipboardData.getData('text/plain'); | |
| if (txt.trim()) processContent(txt); | |
| } | |
| }); | |
| /* ======= Settings modal handling ======= */ | |
| const settingsBtn = document.getElementById('settingsBtn'); | |
| const settingsModal = document.getElementById('settingsModal'); | |
| const nebiusKeyInput = document.getElementById('nebiusKey'); | |
| const cerebrasKeyInput = document.getElementById('cerebrasKey'); | |
| settingsBtn.addEventListener('click', () => { | |
| 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 nb = nebiusKeyInput.value.trim(); | |
| const cb = cerebrasKeyInput.value.trim(); | |
| if (nb) localStorage.setItem('nebius-api-key', nb); | |
| if (cb) localStorage.setItem('cerebras-api-key', cb); | |
| closeSettings(); | |
| alert('API keys saved successfully!'); | |
| } | |
| /* close modal on background click or Escape */ | |
| settingsModal.addEventListener('click', e => { if (e.target===settingsModal) closeSettings(); }); | |
| document.addEventListener('keydown', e => { if (e.key==='Escape' && settingsModal.classList.contains('show')) closeSettings(); }); | |
| /* placeholder click animation */ | |
| 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); | |
| } | |
| }); | |
| /* fade‑in on load */ | |
| 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 themeBtn = document.getElementById('themeToggle'); | |
| const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); | |
| const savedTheme = localStorage.getItem('note-theme'); | |
| initTheme(); | |
| themeBtn.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(){ themeBtn.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙'; } | |
| </script> | |
| </body> | |
| </html> |