SolarumAsteridion commited on
Commit
0c283d2
·
verified ·
1 Parent(s): c744306

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +332 -20
index.html CHANGED
@@ -75,7 +75,7 @@ body.dark {
75
  }
76
 
77
  /* ─────────────────────────────────────────
78
- GLOBAL DESK BACKGROUND
79
  ───────────────────────────────────────── */
80
  html,body{height:100%}
81
  body{
@@ -127,24 +127,25 @@ body{
127
  pointer-events:none;
128
  }
129
 
130
- /* dark-mode gradient uses the *same* var so stays consistent */
131
-
132
  /* ───────── theme toggle ───────── */
133
  #themeToggle{
134
  position:absolute;top:12px;right:14px;
135
  font-size:20px;background:none;border:none;cursor:pointer;
136
  transition:transform .25s;
137
  user-select:none;
 
138
  }
 
139
 
140
- /* put this anywhere after the existing #themeToggle rule */
141
- #themeToggle{
142
- position:absolute; /* you already have this */
143
- z-index:10; /* NEW – lift it above the curl */
 
 
 
144
  }
145
-
146
-
147
- #themeToggle:hover{transform:rotate(20deg)scale(1.15)}
148
 
149
  /* ───────── header ───────── */
150
  .header{text-align:center;margin-bottom:34px;padding-bottom:18px;border-bottom:1px solid rgba(0,0,0,.05)}
@@ -199,6 +200,65 @@ th{font-weight:600}
199
  }
200
  .processing.show{opacity:.9}
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  /* responsive & print */
203
  @media(max-width:768px){
204
  .container{margin:20px 16px;padding:28px}
@@ -208,7 +268,7 @@ th{font-weight:600}
208
  @media print{
209
  body{background:#fff}
210
  .container{box-shadow:none;border:none}
211
- .header,.processing,.instructions,#themeToggle{display:none}
212
  }
213
  </style>
214
  </head>
@@ -217,6 +277,8 @@ th{font-weight:600}
217
  <div class="container">
218
  <!-- theme icon -->
219
  <button id="themeToggle" title="Toggle dark / light">🌙</button>
 
 
220
 
221
  <div class="header">
222
  <h1>LaTeX Notepad</h1>
@@ -225,24 +287,47 @@ th{font-weight:600}
225
 
226
  <div id="content">
227
  <div class="placeholder">
228
- Press <kbd>Ctrl</kbd>+<kbd>V</kbd> (or <kbd>⌘</kbd>+<kbd>V</kbd>) to paste and render Markdown / LaTeX
 
229
  </div>
230
  </div>
231
 
232
  <div class="instructions" style="text-align:center;font-family:'PT Mono',monospace;font-size:14px;color:#666;font-style:italic;margin-top:22px;">
233
- Tip: you can paste raw Markdown or TeXit will be rendered instantly ✨
234
  </div>
235
  </div>
236
 
237
  <div class="processing">Processing…</div>
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  <script>
240
  /* ======= processing badge helpers ======= */
241
  const content = document.getElementById('content');
242
  const processingNode = document.querySelector('.processing');
243
 
244
- function showProcessing(){processingNode.classList.add('show')}
245
- function hideProcessing(){setTimeout(()=>processingNode.classList.remove('show'),300)}
 
 
 
 
 
246
 
247
  /* ======= markdown + latex pipeline ======= */
248
  function processContent(text){
@@ -267,14 +352,241 @@ function processContent(text){
267
  }else{hideProcessing()}
268
  }
269
 
270
- /* ======= paste listener ======= */
271
- document.addEventListener('paste',e=>{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  e.preventDefault();
273
- const txt=e.clipboardData.getData('text/plain');
274
- if(txt.trim())processContent(txt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  });
276
 
277
- /* small bounce on placeholder click */
278
  content.addEventListener('click',()=>{
279
  const ph=content.querySelector('.placeholder');
280
  if(ph){
 
75
  }
76
 
77
  /* ─────────────────────────────────────────
78
+ GLOBAL "DESK" BACKGROUND
79
  ───────────────────────────────────────── */
80
  html,body{height:100%}
81
  body{
 
127
  pointer-events:none;
128
  }
129
 
 
 
130
  /* ───────── theme toggle ───────── */
131
  #themeToggle{
132
  position:absolute;top:12px;right:14px;
133
  font-size:20px;background:none;border:none;cursor:pointer;
134
  transition:transform .25s;
135
  user-select:none;
136
+ z-index:10;
137
  }
138
+ #themeToggle:hover{transform:rotate(20deg)scale(1.15)}
139
 
140
+ /* ───────── settings button ───────── */
141
+ #settingsBtn{
142
+ position:absolute;top:12px;right:50px;
143
+ font-size:20px;background:none;border:none;cursor:pointer;
144
+ transition:transform .25s;
145
+ user-select:none;
146
+ z-index:10;
147
  }
148
+ #settingsBtn:hover{transform:rotate(20deg)scale(1.15)}
 
 
149
 
150
  /* ───────── header ───────── */
151
  .header{text-align:center;margin-bottom:34px;padding-bottom:18px;border-bottom:1px solid rgba(0,0,0,.05)}
 
200
  }
201
  .processing.show{opacity:.9}
202
 
203
+ /* ───────── settings modal ───────── */
204
+ .modal{
205
+ display:none;
206
+ position:fixed;top:0;left:0;width:100%;height:100%;
207
+ background:rgba(0,0,0,0.5);z-index:3000;
208
+ }
209
+ .modal.show{display:flex;align-items:center;justify-content:center}
210
+ .modal-content{
211
+ background:var(--paper-bg);
212
+ color:var(--paper-text);
213
+ padding:30px;
214
+ border-radius:8px;
215
+ max-width:500px;
216
+ width:90%;
217
+ box-shadow:0 20px 60px rgba(0,0,0,0.3);
218
+ }
219
+ .modal h2{margin-top:0;font-family:'Libre Baskerville',serif}
220
+ .modal label{
221
+ display:block;
222
+ margin-top:15px;
223
+ font-weight:600;
224
+ font-size:14px;
225
+ font-family:'PT Mono',monospace;
226
+ }
227
+ .modal input{
228
+ width:100%;
229
+ padding:8px;
230
+ margin-top:5px;
231
+ border:1px solid var(--code-border);
232
+ border-radius:4px;
233
+ background:var(--code-bg);
234
+ color:var(--paper-text);
235
+ font-family:'PT Mono',monospace;
236
+ font-size:13px;
237
+ }
238
+ .modal-buttons{
239
+ margin-top:20px;
240
+ display:flex;
241
+ gap:10px;
242
+ justify-content:flex-end;
243
+ }
244
+ .modal button{
245
+ padding:8px 16px;
246
+ border:none;
247
+ border-radius:4px;
248
+ cursor:pointer;
249
+ font-family:'PT Mono',monospace;
250
+ font-size:14px;
251
+ }
252
+ .btn-save{
253
+ background:#4a5f4a;
254
+ color:#fff;
255
+ }
256
+ .btn-cancel{
257
+ background:var(--code-bg);
258
+ color:var(--paper-text);
259
+ border:1px solid var(--code-border);
260
+ }
261
+
262
  /* responsive & print */
263
  @media(max-width:768px){
264
  .container{margin:20px 16px;padding:28px}
 
268
  @media print{
269
  body{background:#fff}
270
  .container{box-shadow:none;border:none}
271
+ .header,.processing,.instructions,#themeToggle,#settingsBtn{display:none}
272
  }
273
  </style>
274
  </head>
 
277
  <div class="container">
278
  <!-- theme icon -->
279
  <button id="themeToggle" title="Toggle dark / light">🌙</button>
280
+ <!-- settings icon -->
281
+ <button id="settingsBtn" title="API Settings">⚙️</button>
282
 
283
  <div class="header">
284
  <h1>LaTeX Notepad</h1>
 
287
 
288
  <div id="content">
289
  <div class="placeholder">
290
+ Press <kbd>Ctrl</kbd>+<kbd>V</kbd> (or <kbd>⌘</kbd>+<kbd>V</kbd>) to paste and render Markdown / LaTeX<br>
291
+ <small style="font-size:14px;opacity:0.8">You can also paste images to OCR and solve them!</small>
292
  </div>
293
  </div>
294
 
295
  <div class="instructions" style="text-align:center;font-family:'PT Mono',monospace;font-size:14px;color:#666;font-style:italic;margin-top:22px;">
296
+ Tip: you can paste raw Markdown, TeX, or imagesthey will be processed instantly ✨
297
  </div>
298
  </div>
299
 
300
  <div class="processing">Processing…</div>
301
 
302
+ <!-- Settings Modal -->
303
+ <div id="settingsModal" class="modal">
304
+ <div class="modal-content">
305
+ <h2>API Settings</h2>
306
+ <label for="nebiusKey">Nebius API Key:</label>
307
+ <input type="password" id="nebiusKey" placeholder="Enter your Nebius API key">
308
+
309
+ <label for="cerebrasKey">Cerebras API Key:</label>
310
+ <input type="password" id="cerebrasKey" placeholder="Enter your Cerebras API key">
311
+
312
+ <div class="modal-buttons">
313
+ <button class="btn-cancel" onclick="closeSettings()">Cancel</button>
314
+ <button class="btn-save" onclick="saveSettings()">Save</button>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
  <script>
320
  /* ======= processing badge helpers ======= */
321
  const content = document.getElementById('content');
322
  const processingNode = document.querySelector('.processing');
323
 
324
+ function showProcessing(text = 'Processing…'){
325
+ processingNode.textContent = text;
326
+ processingNode.classList.add('show');
327
+ }
328
+ function hideProcessing(){
329
+ setTimeout(()=>processingNode.classList.remove('show'),300);
330
+ }
331
 
332
  /* ======= markdown + latex pipeline ======= */
333
  function processContent(text){
 
352
  }else{hideProcessing()}
353
  }
354
 
355
+ /* ======= Image to Base64 converter ======= */
356
+ async function imageToBase64(file) {
357
+ return new Promise((resolve, reject) => {
358
+ const reader = new FileReader();
359
+ reader.onload = () => {
360
+ const base64 = reader.result.split(',')[1];
361
+ resolve(base64);
362
+ };
363
+ reader.onerror = reject;
364
+ reader.readAsDataURL(file);
365
+ });
366
+ }
367
+
368
+ /* ======= OCR with Nebius API ======= */
369
+ async function ocrImage(base64Image) {
370
+ const nebiusKey = localStorage.getItem('nebius-api-key');
371
+ if (!nebiusKey) {
372
+ alert('Please set your Nebius API key in settings (⚙️)');
373
+ return null;
374
+ }
375
+
376
+ showProcessing('Extracting text from image...');
377
+
378
+ try {
379
+ const response = await fetch('https://api.studio.nebius.com/v1/chat/completions', {
380
+ method: 'POST',
381
+ headers: {
382
+ 'Content-Type': 'application/json',
383
+ 'Accept': '*/*',
384
+ 'Authorization': `Bearer ${nebiusKey}`
385
+ },
386
+ body: JSON.stringify({
387
+ model: 'mistralai/Mistral-Small-3.1-24B-Instruct-2503',
388
+ messages: [
389
+ {
390
+ role: 'system',
391
+ content: 'GIVE AS TEXT WITH LATEX like $this$ or $$this$$. DO NOT SOLVE THE QUESTION. DO NOT OUTPUT ANYTHING BUT THE FORMAT OF THE QUESTION.'
392
+ },
393
+ {
394
+ role: 'user',
395
+ content: [
396
+ { type: 'text', text: 'Image:' },
397
+ {
398
+ type: 'image_url',
399
+ image_url: { url: `data:image/png;base64,${base64Image}` }
400
+ }
401
+ ]
402
+ }
403
+ ]
404
+ })
405
+ });
406
+
407
+ if (!response.ok) {
408
+ throw new Error(`OCR API error: ${response.status}`);
409
+ }
410
+
411
+ const data = await response.json();
412
+ return data.choices[0].message.content;
413
+ } catch (error) {
414
+ console.error('OCR Error:', error);
415
+ alert('Error during OCR: ' + error.message);
416
+ return null;
417
+ }
418
+ }
419
+
420
+ /* ======= Solve with Cerebras API (Streaming) ======= */
421
+ async function solveQuestion(question) {
422
+ const cerebrasKey = localStorage.getItem('cerebras-api-key');
423
+ if (!cerebrasKey) {
424
+ alert('Please set your Cerebras API key in settings (⚙️)');
425
+ return null;
426
+ }
427
+
428
+ showProcessing('Solving the question...');
429
+
430
+ try {
431
+ const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
432
+ method: 'POST',
433
+ headers: {
434
+ 'Content-Type': 'application/json',
435
+ 'Authorization': `Bearer ${cerebrasKey}`
436
+ },
437
+ body: JSON.stringify({
438
+ model: 'gpt-oss-120b',
439
+ stream: true,
440
+ max_tokens: 65536,
441
+ temperature: 0.23,
442
+ top_p: 1,
443
+ reasoning_effort: 'high',
444
+ messages: [
445
+ {
446
+ role: 'system',
447
+ content: 'Solve this Question. Provide a clear, step-by-step solution.'
448
+ },
449
+ {
450
+ role: 'user',
451
+ content: question
452
+ }
453
+ ]
454
+ })
455
+ });
456
+
457
+ if (!response.ok) {
458
+ throw new Error(`Cerebras API error: ${response.status}`);
459
+ }
460
+
461
+ // Process streaming response
462
+ const reader = response.body.getReader();
463
+ const decoder = new TextDecoder();
464
+ let fullAnswer = '';
465
+
466
+ while (true) {
467
+ const { done, value } = await reader.read();
468
+ if (done) break;
469
+
470
+ const chunk = decoder.decode(value);
471
+ const lines = chunk.split('\n').filter(line => line.trim() !== '');
472
+
473
+ for (const line of lines) {
474
+ if (line.startsWith('data: ')) {
475
+ const data = line.slice(6);
476
+ if (data === '[DONE]') continue;
477
+
478
+ try {
479
+ const parsed = JSON.parse(data);
480
+ const content = parsed.choices[0]?.delta?.content;
481
+ if (content) {
482
+ fullAnswer += content;
483
+ // Update display in real-time
484
+ const formatted = `**Question**: ${question}\n\n**Answer**: ${fullAnswer}`;
485
+ processContent(formatted);
486
+ }
487
+ } catch (e) {
488
+ console.error('Error parsing stream chunk:', e);
489
+ }
490
+ }
491
+ }
492
+ }
493
+
494
+ return fullAnswer;
495
+ } catch (error) {
496
+ console.error('Solving Error:', error);
497
+ alert('Error during solving: ' + error.message);
498
+ return null;
499
+ }
500
+ }
501
+
502
+ /* ======= Process image pipeline ======= */
503
+ async function processImage(file) {
504
+ try {
505
+ // Convert image to base64
506
+ const base64 = await imageToBase64(file);
507
+
508
+ // OCR the image
509
+ const ocrText = await ocrImage(base64);
510
+ if (!ocrText) {
511
+ hideProcessing();
512
+ return;
513
+ }
514
+
515
+ // Solve the question
516
+ const answer = await solveQuestion(ocrText);
517
+ if (!answer) {
518
+ hideProcessing();
519
+ return;
520
+ }
521
+
522
+ // Display the result
523
+ const formatted = `**Question**: ${ocrText}\n\n**Answer**: ${answer}`;
524
+ processContent(formatted);
525
+ } catch (error) {
526
+ console.error('Image processing error:', error);
527
+ alert('Error processing image: ' + error.message);
528
+ hideProcessing();
529
+ }
530
+ }
531
+
532
+ /* ======= paste listener (updated for images) ======= */
533
+ document.addEventListener('paste', async (e) => {
534
  e.preventDefault();
535
+
536
+ // Check for image files
537
+ const items = Array.from(e.clipboardData.items);
538
+ const imageItem = items.find(item => item.type.startsWith('image/'));
539
+
540
+ if (imageItem) {
541
+ // Handle image paste
542
+ const file = imageItem.getAsFile();
543
+ await processImage(file);
544
+ } else {
545
+ // Handle text paste (existing functionality)
546
+ const txt = e.clipboardData.getData('text/plain');
547
+ if (txt.trim()) processContent(txt);
548
+ }
549
+ });
550
+
551
+ /* ======= Settings modal functions ======= */
552
+ const settingsBtn = document.getElementById('settingsBtn');
553
+ const settingsModal = document.getElementById('settingsModal');
554
+ const nebiusKeyInput = document.getElementById('nebiusKey');
555
+ const cerebrasKeyInput = document.getElementById('cerebrasKey');
556
+
557
+ settingsBtn.addEventListener('click', () => {
558
+ // Load existing keys
559
+ nebiusKeyInput.value = localStorage.getItem('nebius-api-key') || '';
560
+ cerebrasKeyInput.value = localStorage.getItem('cerebras-api-key') || '';
561
+ settingsModal.classList.add('show');
562
+ });
563
+
564
+ function closeSettings() {
565
+ settingsModal.classList.remove('show');
566
+ }
567
+
568
+ function saveSettings() {
569
+ const nebiusKey = nebiusKeyInput.value.trim();
570
+ const cerebrasKey = cerebrasKeyInput.value.trim();
571
+
572
+ if (nebiusKey) localStorage.setItem('nebius-api-key', nebiusKey);
573
+ if (cerebrasKey) localStorage.setItem('cerebras-api-key', cerebrasKey);
574
+
575
+ closeSettings();
576
+ alert('API keys saved successfully!');
577
+ }
578
+
579
+ // Close modal on escape or background click
580
+ settingsModal.addEventListener('click', (e) => {
581
+ if (e.target === settingsModal) closeSettings();
582
+ });
583
+ document.addEventListener('keydown', (e) => {
584
+ if (e.key === 'Escape' && settingsModal.classList.contains('show')) {
585
+ closeSettings();
586
+ }
587
  });
588
 
589
+ /* small "bounce" on placeholder click */
590
  content.addEventListener('click',()=>{
591
  const ph=content.querySelector('.placeholder');
592
  if(ph){