Spaces:
Running
Running
Update index.html
Browse files- index.html +111 -49
index.html
CHANGED
|
@@ -266,6 +266,16 @@ th{font-weight:600}
|
|
| 266 |
font-style:italic;
|
| 267 |
}
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
/* responsive & print */
|
| 270 |
@media(max-width:768px){
|
| 271 |
.container{margin:20px 16px;padding:28px}
|
|
@@ -366,10 +376,15 @@ async function imageToBase64(file) {
|
|
| 366 |
return new Promise((resolve, reject) => {
|
| 367 |
const reader = new FileReader();
|
| 368 |
reader.onload = () => {
|
| 369 |
-
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
};
|
| 372 |
-
reader.onerror = reject;
|
| 373 |
reader.readAsDataURL(file);
|
| 374 |
});
|
| 375 |
}
|
|
@@ -403,9 +418,9 @@ async function ocrImage(base64Image) {
|
|
| 403 |
role: 'user',
|
| 404 |
content: [
|
| 405 |
{ type: 'text', text: 'Image:' },
|
| 406 |
-
{
|
| 407 |
-
type: 'image_url',
|
| 408 |
-
image_url: { url: `data:image/png;base64,${base64Image}` }
|
| 409 |
}
|
| 410 |
]
|
| 411 |
}
|
|
@@ -414,7 +429,8 @@ async function ocrImage(base64Image) {
|
|
| 414 |
});
|
| 415 |
|
| 416 |
if (!response.ok) {
|
| 417 |
-
|
|
|
|
| 418 |
}
|
| 419 |
|
| 420 |
const data = await response.json();
|
|
@@ -426,7 +442,31 @@ async function ocrImage(base64Image) {
|
|
| 426 |
}
|
| 427 |
}
|
| 428 |
|
| 429 |
-
/* =======
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
async function solveQuestion(question) {
|
| 431 |
const cerebrasKey = localStorage.getItem('cerebras-api-key');
|
| 432 |
if (!cerebrasKey) {
|
|
@@ -435,74 +475,97 @@ async function solveQuestion(question) {
|
|
| 435 |
}
|
| 436 |
|
| 437 |
showProcessing('Solving the question...');
|
|
|
|
| 438 |
|
| 439 |
try {
|
| 440 |
const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
|
| 441 |
method: 'POST',
|
| 442 |
headers: {
|
| 443 |
'Content-Type': 'application/json',
|
|
|
|
| 444 |
'Authorization': `Bearer ${cerebrasKey}`
|
| 445 |
},
|
| 446 |
body: JSON.stringify({
|
| 447 |
model: 'gpt-oss-120b',
|
| 448 |
stream: true,
|
| 449 |
max_tokens: 65536,
|
| 450 |
-
temperature: 0.1,
|
| 451 |
-
reasoning_effort: 'medium',
|
|
|
|
| 452 |
messages: [
|
| 453 |
-
{
|
| 454 |
-
|
| 455 |
-
content: 'Solve this Question. Provide a clear, step-by-step solution with LaTeX.'
|
| 456 |
-
},
|
| 457 |
-
{
|
| 458 |
-
role: 'user',
|
| 459 |
-
content: question
|
| 460 |
-
}
|
| 461 |
]
|
| 462 |
})
|
| 463 |
});
|
| 464 |
|
| 465 |
if (!response.ok) {
|
| 466 |
-
|
|
|
|
| 467 |
}
|
| 468 |
|
| 469 |
-
|
| 470 |
-
const reader = response.body.getReader();
|
| 471 |
const decoder = new TextDecoder();
|
| 472 |
let fullAnswer = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
while (true) {
|
| 475 |
const { done, value } = await reader.read();
|
| 476 |
if (done) break;
|
| 477 |
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
}
|
| 495 |
-
} catch (e) {
|
| 496 |
-
console.error('Error parsing stream chunk:', e);
|
| 497 |
}
|
|
|
|
|
|
|
|
|
|
| 498 |
}
|
| 499 |
}
|
| 500 |
}
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
return fullAnswer;
|
| 503 |
} catch (error) {
|
| 504 |
console.error('Solving Error:', error);
|
| 505 |
alert('Error during solving: ' + error.message);
|
|
|
|
| 506 |
return null;
|
| 507 |
}
|
| 508 |
}
|
|
@@ -522,14 +585,9 @@ async function processImage(file) {
|
|
| 522 |
|
| 523 |
// Solve the question
|
| 524 |
const answer = await solveQuestion(ocrText);
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
return;
|
| 528 |
-
}
|
| 529 |
|
| 530 |
-
// Display the result
|
| 531 |
-
const formatted = `**Question**: ${ocrText}\n\n**Answer**: ${answer}`;
|
| 532 |
-
processContent(formatted);
|
| 533 |
} catch (error) {
|
| 534 |
console.error('Image processing error:', error);
|
| 535 |
alert('Error processing image: ' + error.message);
|
|
@@ -544,7 +602,7 @@ document.addEventListener('paste', async (e) => {
|
|
| 544 |
const isInputField = activeElement && (
|
| 545 |
activeElement.tagName === 'INPUT' ||
|
| 546 |
activeElement.tagName === 'TEXTAREA' ||
|
| 547 |
-
activeElement.
|
| 548 |
);
|
| 549 |
|
| 550 |
// If pasting into an input field, let the browser handle it normally
|
|
@@ -555,14 +613,18 @@ document.addEventListener('paste', async (e) => {
|
|
| 555 |
// Otherwise, handle custom paste logic
|
| 556 |
e.preventDefault();
|
| 557 |
|
| 558 |
-
// Check for image files
|
| 559 |
const items = Array.from(e.clipboardData.items);
|
| 560 |
const imageItem = items.find(item => item.type.startsWith('image/'));
|
| 561 |
|
| 562 |
if (imageItem) {
|
| 563 |
// Handle image paste
|
| 564 |
const file = imageItem.getAsFile();
|
| 565 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
} else {
|
| 567 |
// Handle text paste (existing functionality)
|
| 568 |
const txt = e.clipboardData.getData('text/plain');
|
|
|
|
| 266 |
font-style:italic;
|
| 267 |
}
|
| 268 |
|
| 269 |
+
/* Styles for the lightweight streaming area */
|
| 270 |
+
.mono-stream{
|
| 271 |
+
font-family:'PT Mono',monospace;
|
| 272 |
+
white-space:pre-wrap;
|
| 273 |
+
background:var(--code-bg);
|
| 274 |
+
border:1px solid var(--code-border);
|
| 275 |
+
padding:12px;border-radius:6px;
|
| 276 |
+
margin-top: 8px; /* Add some spacing */
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
/* responsive & print */
|
| 280 |
@media(max-width:768px){
|
| 281 |
.container{margin:20px 16px;padding:28px}
|
|
|
|
| 376 |
return new Promise((resolve, reject) => {
|
| 377 |
const reader = new FileReader();
|
| 378 |
reader.onload = () => {
|
| 379 |
+
// Ensure it's a valid data URL and extract base64 part
|
| 380 |
+
if (reader.result && reader.result.includes(',')) {
|
| 381 |
+
const base64 = reader.result.split(',')[1];
|
| 382 |
+
resolve(base64);
|
| 383 |
+
} else {
|
| 384 |
+
reject(new Error("Failed to read file as Data URL."));
|
| 385 |
+
}
|
| 386 |
};
|
| 387 |
+
reader.onerror = () => reject(reader.error);
|
| 388 |
reader.readAsDataURL(file);
|
| 389 |
});
|
| 390 |
}
|
|
|
|
| 418 |
role: 'user',
|
| 419 |
content: [
|
| 420 |
{ type: 'text', text: 'Image:' },
|
| 421 |
+
{
|
| 422 |
+
type: 'image_url',
|
| 423 |
+
image_url: { url: `data:image/png;base64,${base64Image}` } // Assuming PNG, adjust if needed
|
| 424 |
}
|
| 425 |
]
|
| 426 |
}
|
|
|
|
| 429 |
});
|
| 430 |
|
| 431 |
if (!response.ok) {
|
| 432 |
+
const errorText = await response.text();
|
| 433 |
+
throw new Error(`OCR API error: ${response.status} - ${errorText}`);
|
| 434 |
}
|
| 435 |
|
| 436 |
const data = await response.json();
|
|
|
|
| 442 |
}
|
| 443 |
}
|
| 444 |
|
| 445 |
+
/* ======= UI Helpers for Streaming ======= */
|
| 446 |
+
function beginStreamingUI(question){
|
| 447 |
+
// Show a lightweight, non-MathJax view while the model streams
|
| 448 |
+
content.innerHTML = `
|
| 449 |
+
<div>
|
| 450 |
+
<p><strong>Question</strong>:</p>
|
| 451 |
+
<div class="mono-stream" id="qStream"></div>
|
| 452 |
+
<hr style="opacity:.35; margin: 20px 0;">
|
| 453 |
+
<p><strong>Answer</strong>:</p>
|
| 454 |
+
<div class="mono-stream" id="aStream">(generating...)</div>
|
| 455 |
+
</div>`;
|
| 456 |
+
const qEl = document.getElementById('qStream');
|
| 457 |
+
const aEl = document.getElementById('aStream');
|
| 458 |
+
qEl.textContent = question; // plain text now; pretty render later
|
| 459 |
+
aEl.textContent = ''; // clear "(generating...)"
|
| 460 |
+
return { qEl, aEl };
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
function finalizeStreaming(question, fullAnswer){
|
| 464 |
+
// One single heavy render (Markdown + MathJax) at the end
|
| 465 |
+
const formatted = `**Question**: ${question}\n\n**Answer**: ${fullAnswer}`;
|
| 466 |
+
processContent(formatted); // processContent calls hideProcessing after MathJax
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
/* ======= Solve with Cerebras API (Streaming Optimization) ======= */
|
| 470 |
async function solveQuestion(question) {
|
| 471 |
const cerebrasKey = localStorage.getItem('cerebras-api-key');
|
| 472 |
if (!cerebrasKey) {
|
|
|
|
| 475 |
}
|
| 476 |
|
| 477 |
showProcessing('Solving the question...');
|
| 478 |
+
const ui = beginStreamingUI(question); // Prepare the lightweight streaming UI
|
| 479 |
|
| 480 |
try {
|
| 481 |
const response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
|
| 482 |
method: 'POST',
|
| 483 |
headers: {
|
| 484 |
'Content-Type': 'application/json',
|
| 485 |
+
'Accept': 'text/event-stream', // Specify that we expect a stream
|
| 486 |
'Authorization': `Bearer ${cerebrasKey}`
|
| 487 |
},
|
| 488 |
body: JSON.stringify({
|
| 489 |
model: 'gpt-oss-120b',
|
| 490 |
stream: true,
|
| 491 |
max_tokens: 65536,
|
| 492 |
+
temperature: 0.1, // Set temperature to 0.1
|
| 493 |
+
reasoning_effort: 'medium', // Set reasoning_effort to 'medium'
|
| 494 |
+
// top_p: 1, // Removed as per user's request
|
| 495 |
messages: [
|
| 496 |
+
{ role: 'system', content: 'Solve this Question. Provide a clear, step-by-step solution.' },
|
| 497 |
+
{ role: 'user', content: question }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
]
|
| 499 |
})
|
| 500 |
});
|
| 501 |
|
| 502 |
if (!response.ok) {
|
| 503 |
+
const errorText = await response.text();
|
| 504 |
+
throw new Error(`Cerebras API error: ${response.status} - ${errorText}`);
|
| 505 |
}
|
| 506 |
|
| 507 |
+
const reader = response.body.getReader();
|
|
|
|
| 508 |
const decoder = new TextDecoder();
|
| 509 |
let fullAnswer = '';
|
| 510 |
+
let buffer = ''; // buffer for partial SSE frames
|
| 511 |
+
let lastFlushTime = 0;
|
| 512 |
+
const flushThrottle = 120; // milliseconds to wait between DOM updates
|
| 513 |
+
|
| 514 |
+
const flushUI = () => {
|
| 515 |
+
// Update the lightweight streaming area without MathJax
|
| 516 |
+
ui.aEl.textContent = fullAnswer;
|
| 517 |
+
lastFlushTime = performance.now();
|
| 518 |
+
};
|
| 519 |
|
| 520 |
while (true) {
|
| 521 |
const { done, value } = await reader.read();
|
| 522 |
if (done) break;
|
| 523 |
|
| 524 |
+
buffer += decoder.decode(value, { stream: true });
|
| 525 |
+
// SSE events are typically separated by '\n\n'
|
| 526 |
+
const events = buffer.split('\n\n');
|
| 527 |
+
buffer = events.pop() || ''; // Keep any incomplete event for the next chunk
|
| 528 |
+
|
| 529 |
+
for (const evt of events) {
|
| 530 |
+
// Find the 'data:' line, which contains the JSON payload
|
| 531 |
+
const dataLine = evt.split('\n').find(line => line.trim().startsWith('data: '));
|
| 532 |
+
if (!dataLine) continue;
|
| 533 |
+
|
| 534 |
+
const data = dataLine.slice(6).trim(); // Remove 'data: ' prefix
|
| 535 |
+
if (data === '[DONE]') continue; // End of stream marker
|
| 536 |
+
|
| 537 |
+
try {
|
| 538 |
+
const parsed = JSON.parse(data);
|
| 539 |
+
// Extract content, being flexible with potential API response structures
|
| 540 |
+
const deltaContent = parsed.choices?.[0]?.delta?.content
|
| 541 |
+
?? parsed.choices?.[0]?.message?.content
|
| 542 |
+
?? parsed.choices?.[0]?.text // Some APIs might use 'text'
|
| 543 |
+
?? '';
|
| 544 |
+
|
| 545 |
+
if (deltaContent) {
|
| 546 |
+
fullAnswer += deltaContent;
|
| 547 |
+
// Throttle DOM updates to prevent excessive rendering and jank
|
| 548 |
+
if (performance.now() - lastFlushTime > flushThrottle) {
|
| 549 |
+
flushUI();
|
| 550 |
}
|
|
|
|
|
|
|
| 551 |
}
|
| 552 |
+
} catch (e) {
|
| 553 |
+
// Ignore errors parsing JSON chunks if it's just partial data
|
| 554 |
+
console.error('Error parsing stream chunk data:', e, 'Chunk:', data);
|
| 555 |
}
|
| 556 |
}
|
| 557 |
}
|
| 558 |
|
| 559 |
+
// Final flush to ensure all streamed content is displayed in the lightweight view
|
| 560 |
+
flushUI();
|
| 561 |
+
|
| 562 |
+
// Once streaming is complete, perform the final, heavier render with Markdown and MathJax
|
| 563 |
+
finalizeStreaming(question, fullAnswer);
|
| 564 |
return fullAnswer;
|
| 565 |
} catch (error) {
|
| 566 |
console.error('Solving Error:', error);
|
| 567 |
alert('Error during solving: ' + error.message);
|
| 568 |
+
hideProcessing(); // Ensure the processing indicator is hidden on error
|
| 569 |
return null;
|
| 570 |
}
|
| 571 |
}
|
|
|
|
| 585 |
|
| 586 |
// Solve the question
|
| 587 |
const answer = await solveQuestion(ocrText);
|
| 588 |
+
// The solveQuestion function now handles hiding the processing indicator
|
| 589 |
+
// unless an error occurred, in which case it was hidden earlier.
|
|
|
|
|
|
|
| 590 |
|
|
|
|
|
|
|
|
|
|
| 591 |
} catch (error) {
|
| 592 |
console.error('Image processing error:', error);
|
| 593 |
alert('Error processing image: ' + error.message);
|
|
|
|
| 602 |
const isInputField = activeElement && (
|
| 603 |
activeElement.tagName === 'INPUT' ||
|
| 604 |
activeElement.tagName === 'TEXTAREA' ||
|
| 605 |
+
activeElement.isContentEditable === true // Use isContentEditable for modern check
|
| 606 |
);
|
| 607 |
|
| 608 |
// If pasting into an input field, let the browser handle it normally
|
|
|
|
| 613 |
// Otherwise, handle custom paste logic
|
| 614 |
e.preventDefault();
|
| 615 |
|
| 616 |
+
// Check for image files first
|
| 617 |
const items = Array.from(e.clipboardData.items);
|
| 618 |
const imageItem = items.find(item => item.type.startsWith('image/'));
|
| 619 |
|
| 620 |
if (imageItem) {
|
| 621 |
// Handle image paste
|
| 622 |
const file = imageItem.getAsFile();
|
| 623 |
+
if (file) {
|
| 624 |
+
await processImage(file);
|
| 625 |
+
} else {
|
| 626 |
+
alert("Could not get image file from clipboard.");
|
| 627 |
+
}
|
| 628 |
} else {
|
| 629 |
// Handle text paste (existing functionality)
|
| 630 |
const txt = e.clipboardData.getData('text/plain');
|