Notebook / index.html
SolarumAsteridion's picture
Update index.html
814e789 verified
raw
history blame
23.4 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; /* Add some spacing */
}
/* 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 = () => {
// 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$$. 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}` } // 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 ======= */
function beginStreamingUI(question){
// Show a lightweight, non-MathJax view while the model streams
content.innerHTML = `
<div>
<p><strong>Question</strong>:</p>
<div class="mono-stream" id="qStream"></div>
<hr style="opacity:.35; margin: 20px 0;">
<p><strong>Answer</strong>:</p>
<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){
// One single heavy render (Markdown + MathJax) at the end
const formatted = `**Question**: ${question}\n\n**Answer**: ${fullAnswer}`;
processContent(formatted); // processContent calls hideProcessing after MathJax
}
/* ======= 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;
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>