Notebook / index.html
SolarumAsteridion's picture
Update index.html
eef9e19 verified
raw
history blame
23 kB
<!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;
}
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>