Notebook / index.html
SolarumAsteridion's picture
Update index.html
3c9e002 verified
<!DOCTYPE html>
<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;
/* copy button theme vars */
--copy-bg: var(--code-bg);
--copy-border: var(--code-border);
--copy-color: var(--paper-text);
--copy-hover-bg: var(--blockquote-bg);
--copy-hover-border: var(--blockquote-bar);
--copy-success-bg: #4a5f4a;
--copy-success-color:#fff;
}
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;
/* dark theme copy button vars */
--copy-bg: var(--code-bg);
--copy-border: var(--code-border);
--copy-color: var(--paper-text);
--copy-hover-bg: #4a443d; /* Darker gray for hover */
--copy-hover-border:#6a604e; /* Darker border for hover */
--copy-success-bg: #4a5f4a; /* Consistent success color */
--copy-success-color:#fff;
}
/* ─────────────────────────────────────────
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-flex; /* Use flex for alignment */
align-items: center;
gap: 6px; /* Space between icon and text */
margin-left: 10px;
padding: 4px 10px;
background: var(--copy-bg);
border: 1px solid var(--copy-border);
color: var(--copy-color);
border-radius: 6px;
font-family: 'PT Mono', monospace;
font-size: 12px;
cursor: pointer;
transition: all 0.18s ease; /* Smoother transition */
user-select: none;
}
.copy-btn:hover {
background: var(--copy-hover-bg);
border-color: var(--copy-hover-border);
transform: translateY(-1px); /* Subtle lift effect */
}
.copy-btn.copied {
background: var(--copy-success-bg);
border-color: var(--copy-success-bg);
color: var(--copy-success-color);
}
/* header layout (updated to use span for title, avoid <p> spacing issues) */
.section-header {
display: flex;
align-items: center;
gap: 10px; /* Space between title and button */
margin: 6px 0 8px; /* Adjust vertical spacing */
}
.section-title {
font-family:'Libre Baskerville',serif;
font-weight:700;
}
.section-title::after {
content: " :"; /* Add colon after the title */
opacity:.85;
}
/* Optional: container for rendered blocks in final view */
.rendered {
margin-bottom: 20px;
}
/* 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');
}, 1500); /* Shorter timeout for feedback */
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
});
}
/* ======= Markdown + LaTeX renderer ======= */
function renderMdLatex(text) {
const store = [];
const PL = i => `%%LATEX_${i}%%`; /* Placeholder for LaTeX */
let idx = 0;
const keep = m => (store.push(m), PL(idx++)); /* Function to store and replace with placeholder */
// Protect LaTeX blocks before Markdown parsing
text = (text || '')
.replace(/\\\[[\s\S]*?\\\]/g, keep) // \[ ... \]
.replace(/\$\$[\s\S]*?\$\$/g, keep) // $$ ... $$
.replace(/\\\([\s\S]*?\\\)/g, keep) // \( ... \)
.replace(/\$([^\$\n]+?)\$/g, keep); // $ ... $ (inline math)
let html = marked.parse(text || ''); // Parse Markdown
// Re-insert LaTeX blocks
store.forEach((latex, i) => { html = html.replaceAll(PL(i), latex); });
return html;
}
/* ======= processContent uses the new renderer ======= */
function processContent(text){
showProcessing();
content.innerHTML = renderMdLatex(text); /* Render with Markdown and LaTeX */
if (window.MathJax?.typesetPromise) {
MathJax.typesetPromise([content]).then(hideProcessing)
.catch(e => { console.error('MathJax error:', e); hideProcessing(); });
} else {
hideProcessing(); /* Hide if MathJax is not available */
}
}
/* ======= Clean leading labels to avoid duplication ======= */
function normalizeSection(s) {
if (!s) return '';
let out = s.trim();
// Remove common leading labels like "Question:", "Answer:", etc.
// This regex is more robust and handles various casings and separators.
out = out.replace(/^(?:\s*[-*_]*\s*)*(?:Question|Q|Problem|Prompt|Answer|Solution|Ans)\s*[:\-]?\s*/i, '');
// Additional specific cleanups for common markdown bolding patterns
out = out.replace(/^\s*\*{0,2}Answer\*{0,2}\s*:\s*/i, '');
out = out.replace(/^\s*\*{0,2}Solution\*{0,2}\s*:\s*/i, '');
out = out.replace(/^\s*\*{0,2}Ans\*{0,2}\s*:\s*/i, '');
out = out.replace(/^\s*\*{0,2}Q\*{0,2}\s*:\s*/i, '');
out = out.replace(/^\s*\*{0,2}Question\*{0,2}\s*:\s*/i, '');
out = out.replace(/^\s*\*{0,2}Problem\*{0,2}\s*:\s*/i, '');
return out.trim(); /* Return cleaned string */
}
/* ======= UI Helpers for Streaming (updated headers & copy buttons) ======= */
let currentQuestion = '';
let currentAnswer = '';
function beginStreamingUI(question){
currentQuestion = normalizeSection(question); /* Normalize question text */
currentAnswer = ''; /* Reset answer when starting a new question */
/* Set up the initial streaming UI with copy buttons */
content.innerHTML = `
<div>
<div class="section-header">
<span class="section-title">Question</span>
<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">
<span class="section-title">Answer</span>
<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 = currentQuestion; /* Display the normalized question */
aEl.textContent = ''; /* Clear "(generating...)" initially */
return { qEl, aEl }; /* Return elements for updating */
}
function finalizeStreaming(question, fullAnswer){
currentQuestion = normalizeSection(question); /* Normalize question text */
currentAnswer = normalizeSection(fullAnswer); /* Normalize answer text */
/* Build the final HTML structure with proper rendering and copy buttons */
content.innerHTML = `
<div class="section-header">
<span class="section-title">Question</span>
<button class="copy-btn" onclick="copyToClipboard(currentQuestion, this)">📋 Copy</button>
</div>
<div id="qRender" class="rendered"></div>
<hr style="opacity:.35; margin: 20px 0;">
<div class="section-header">
<span class="section-title">Answer</span>
<button class="copy-btn" onclick="copyToClipboard(currentAnswer, this)">📋 Copy</button>
</div>
<div id="aRender" class="rendered"></div>
`;
/* Render Markdown+LaTeX into the respective blocks */
const qRender = document.getElementById('qRender');
const aRender = document.getElementById('aRender');
qRender.innerHTML = renderMdLatex(currentQuestion); /* Render question */
aRender.innerHTML = renderMdLatex(currentAnswer); /* Render answer */
/* Apply MathJax typesetting to the rendered blocks */
if (window.MathJax?.typesetPromise) {
MathJax.typesetPromise([qRender, aRender]).then(hideProcessing) /* Hide processing indicator on success */
.catch(e => { console.error('MathJax error:', e); hideProcessing(); }); /* Hide on error */
} else {
hideProcessing(); /* Hide if MathJax is not available */
}
}
/* ======= 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', /* Model for OCR */
messages: [
{
role: 'system',
/* Enhanced prompt to prevent list environments and ensure raw LaTeX output */
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.'
},
{
role: 'user',
content: [
{ type: 'text', text: 'Image:' },
{ type: 'image_url', image_url: { url: `data:image/png;base64,${base64Image}` } } /* Image data */
]
}
]
})
});
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; /* Return extracted text */
} catch (error) {
console.error('OCR Error:', error);
alert('Error during OCR: ' + error.message);
return null;
}
}
/* ======= 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 UI for streaming */
try {
const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream', /* Request server-sent events */
'Authorization': `Bearer ${cerebrasKey}`
},
body: JSON.stringify({
model: 'gpt-oss-120b',
stream: true,
max_tokens: 65536,
temperature: 0.1, /* Lower temperature for more deterministic answers */
reasoning_effort: 'medium', /* Medium reasoning effort */
messages: [
{ role: 'system', content: 'Solve this Question. Provide a clear, step-by-step solution.' },
{ role: 'user', content: question } /* User's 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 Server-Sent Events (SSE) frames */
let lastFlushTime = 0;
const flushThrottle = 120; /* Milliseconds to wait between DOM updates to prevent jank */
/* Helper to update the UI during streaming */
const flushUI = () => {
ui.aEl.textContent = fullAnswer; /* Update the answer display */
currentAnswer = fullAnswer; /* Update global variable for copy button */
lastFlushTime = performance.now(); /* Record time of last update */
};
while (true) {
const { done, value } = await reader.read();
if (done) break; /* Exit loop if stream is done */
buffer += decoder.decode(value, { stream: true }); /* Append decoded data */
const events = buffer.split('\n\n'); /* Split buffer by SSE frame delimiter */
buffer = events.pop() || ''; /* Keep any incomplete event for the next chunk */
for (const evt of events) {
const dataLine = evt.split('\n').find(line => line.trim().startsWith('data: ')); /* Find data line */
if (!dataLine) continue;
const data = dataLine.slice(6).trim(); /* Extract JSON data */
if (data === '[DONE]') continue; /* Ignore end-of-stream marker */
try {
const parsed = JSON.parse(data);
/* Extract content delta from potential response structures */
const deltaContent = parsed.choices?.[0]?.delta?.content
?? parsed.choices?.[0]?.message?.content
?? parsed.choices?.[0]?.text /* Fallback for other potential fields */
?? '';
if (deltaContent) {
fullAnswer += deltaContent; /* Append new content */
/* Throttle DOM updates to keep UI responsive */
if (performance.now() - lastFlushTime > flushThrottle) {
flushUI();
}
}
} catch (e) {
/* Log errors parsing chunks, but continue streaming */
console.error('Error parsing stream chunk data:', e, 'Chunk:', data);
}
}
}
flushUI(); /* Final flush to display any remaining content */
/* After streaming, perform the final heavy render with Markdown and MathJax */
finalizeStreaming(question, fullAnswer);
return fullAnswer; /* Return the complete answer */
} catch (error) {
console.error('Solving Error:', error);
alert('Error during solving: ' + error.message);
hideProcessing(); /* Ensure processing indicator is hidden on error */
return null;
}
}
/* ======= Process image pipeline ======= */
async function processImage(file) {
try {
/* Convert image file to base64 string */
const base64 = await imageToBase64(file);
/* OCR the image to get question text */
const ocrText = await ocrImage(base64);
if (!ocrText) {
/* Error handled within ocrImage, which calls hideProcessing */
return;
}
/* Solve the question using the extracted text */
const answer = await solveQuestion(ocrText);
/* solveQuestion handles hiding the processing indicator */
} catch (error) {
console.error('Image processing error:', error);
alert('Error processing image: ' + error.message);
hideProcessing(); /* Ensure processing indicator is hidden on error */
}
}
/* ======= 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); /* Reject on reader error */
reader.readAsDataURL(file); /* Read file as Data URL */
});
}
/* ======= FIXED paste listener - allows normal paste in input fields ======= */
document.addEventListener('paste', async (e) => {
/* Check if the paste event is happening inside an input, textarea, or contenteditable element */
const activeElement = document.activeElement;
const isInputField = activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.isContentEditable === true /* Modern check for editable content */
);
/* If pasting into an input field, let the browser handle it normally */
if (isInputField) {
return; /* Do not intercept, allow default paste behavior */
}
/* Otherwise, handle custom paste logic for the content area */
e.preventDefault(); /* Prevent default paste behavior */
/* Check for image files first in the clipboard data */
const items = Array.from(e.clipboardData.items);
const imageItem = items.find(item => item.type.startsWith('image/'));
if (imageItem) {
/* Handle image paste: convert to base64, OCR, and solve */
const file = imageItem.getAsFile();
if (file) {
await processImage(file);
} else {
alert("Could not get image file from clipboard.");
}
} else {
/* Handle text paste: process it directly */
const txt = e.clipboardData.getData('text/plain');
if (txt.trim()) {
processContent(txt); /* Use the processContent function for direct text pastes */
}
}
});
/* ======= 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 API keys from localStorage when modal is opened */
nebiusKeyInput.value = localStorage.getItem('nebius-api-key') || '';
cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || '';
settingsModal.classList.add('show'); /* Show the modal */
});
function closeSettings() {
settingsModal.classList.remove('show'); /* Hide the modal */
}
function saveSettings() {
const nebiusKey = nebiusKeyInput.value.trim();
const cerebrasKey = cerebrasKeyInput.value.trim();
/* Save keys to localStorage if they are provided */
if (nebiusKey) localStorage.setItem('nebius-api-key', nebiusKey);
if (cerebrasKey) localStorage.setItem('cerebras-api-key', cerebrasKey);
closeSettings(); /* Close the modal */
alert('API keys saved successfully!'); /* Confirmation */
}
/* Close modal on escape key press or background click */
settingsModal.addEventListener('click', (e) => {
if (e.target === settingsModal) closeSettings(); /* Close if background clicked */
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && settingsModal.classList.contains('show')) {
closeSettings(); /* Close if Escape key pressed and modal is shown */
}
});
/* 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 animation for the container */
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(); /* Initialize theme on load */
btn.addEventListener('click',()=>{
document.body.classList.toggle('dark'); /* Toggle dark class on body */
updateIcon(); /* Update theme toggle icon */
localStorage.setItem('note-theme',document.body.classList.contains('dark')?'dark':'light'); /* Save theme preference */
});
function initTheme(){
/* Set initial theme based on saved preference or system preference */
if(savedTheme){
document.body.classList.toggle('dark',savedTheme==='dark');
}else if(prefersDark.matches){
document.body.classList.add('dark');
}
updateIcon(); /* Set initial icon */
}
function updateIcon(){
/* Update the moon/sun icon based on current theme */
btn.textContent=document.body.classList.contains('dark')?'☀️':'🌙';
}
</script>
</body>
</html>