← back to Exo
handle thinking outputs nicely, format latex beautifully
cfdaaef8e6d2f1f135d0f1d6a3b7b71b9198051f · 2025-01-24 17:49:25 +0000 · Alex Cheema
Files touched
M exo/tinychat/index.cssM exo/tinychat/index.htmlM exo/tinychat/index.js
Diff
commit cfdaaef8e6d2f1f135d0f1d6a3b7b71b9198051f
Author: Alex Cheema <alexcheema123@gmail.com>
Date: Fri Jan 24 17:49:25 2025 +0000
handle thinking outputs nicely, format latex beautifully
---
exo/tinychat/index.css | 36 +++++++++++
exo/tinychat/index.html | 165 ++++++++++++++++++++++++++++++++----------------
exo/tinychat/index.js | 14 ++--
3 files changed, 153 insertions(+), 62 deletions(-)
diff --git a/exo/tinychat/index.css b/exo/tinychat/index.css
index b82bae26..2dd3d087 100644
--- a/exo/tinychat/index.css
+++ b/exo/tinychat/index.css
@@ -742,4 +742,40 @@ main {
.peer-connection i {
font-size: 0.8em;
color: #666;
+}
+
+.thinking-block {
+ background-color: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ margin: 8px 0;
+ overflow: hidden;
+}
+
+.thinking-header {
+ background-color: rgba(255, 255, 255, 0.1);
+ padding: 8px 12px;
+ font-size: 0.9em;
+ color: #a0a0a0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.thinking-content {
+ padding: 12px;
+ white-space: pre-wrap;
+}
+
+@keyframes thinking-spin {
+ to { transform: rotate(360deg); }
+}
+
+.thinking-header.thinking::before {
+ content: '';
+ width: 12px;
+ height: 12px;
+ border: 2px solid #a0a0a0;
+ border-top-color: transparent;
+ border-radius: 50%;
+ animation: thinking-spin 1s linear infinite;
}
\ No newline at end of file
diff --git a/exo/tinychat/index.html b/exo/tinychat/index.html
index 013d0d63..7ef552e9 100644
--- a/exo/tinychat/index.html
+++ b/exo/tinychat/index.html
@@ -22,6 +22,7 @@
<link href="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css" rel="stylesheet"/>
<link href="/index.css" rel="stylesheet"/>
<link href="/common.css" rel="stylesheet"/>
+<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
</head>
<body>
<main x-data="state" x-init="console.log(endpoint)">
@@ -190,67 +191,87 @@
<i class="fas fa-arrow-left"></i>
Back to Chats
</button>
-<div class="messages" x-init="
- $watch('cstate', value => {
- $el.innerHTML = '';
- value.messages.forEach(({ role, content }) => {
- const div = document.createElement('div');
- div.className = `message message-role-${role}`;
- try {
- if (content.includes('![Generated Image]')) {
- const imageUrl = content.match(/\((.*?)\)/)[1];
- const img = document.createElement('img');
- img.src = imageUrl;
- img.alt = 'Generated Image';
- img.onclick = async () => {
- try {
- const response = await fetch(img.src);
- const blob = await response.blob();
- const file = new File([blob], 'image.png', { type: 'image/png' });
- handleImageUpload({ target: { files: [file] } });
- } catch (error) {
- console.error('Error fetching image:', error);
- }
- };
- div.appendChild(img);
- } else {
- div.innerHTML = DOMPurify.sanitize(marked.parse(content));
- }
- } catch (e) {
- console.log(content);
- console.error(e);
+<div class="messages"
+ x-init="
+ $watch('cstate', (value) => {
+ $el.innerHTML = '';
+
+ value.messages.forEach((msg) => {
+ const div = document.createElement('div');
+ div.className = `message message-role-${msg.role}`;
+
+ try {
+ // If there's an embedded generated image
+ if (msg.content.includes('![Generated Image]')) {
+ const imageUrlMatch = msg.content.match(/\((.*?)\)/);
+ if (imageUrlMatch) {
+ const imageUrl = imageUrlMatch[1];
+ const img = document.createElement('img');
+ img.src = imageUrl;
+ img.alt = 'Generated Image';
+
+ img.onclick = async () => {
+ try {
+ const response = await fetch(img.src);
+ const blob = await response.blob();
+ const file = new File([blob], 'image.png', { type: 'image/png' });
+ handleImageUpload({ target: { files: [file] } });
+ } catch (error) {
+ console.error('Error fetching image:', error);
+ }
+ };
+ div.appendChild(img);
+ } else {
+ // fallback if markdown is malformed
+ div.textContent = msg.content;
+ }
+ } else {
+ // Otherwise, transform message text (including streamed think blocks).
+ div.innerHTML = transformMessageContent(msg);
+ // Render math after content is inserted
+ MathJax.typesetPromise([div]);
}
+ } catch (e) {
+ console.error('Error rendering message:', e);
+ div.textContent = msg.content; // fallback
+ }
+
+ // Add a clipboard button to code blocks
+ const codeBlocks = div.querySelectorAll('.hljs');
+ codeBlocks.forEach((codeBlock) => {
+ const button = document.createElement('button');
+ button.className = 'clipboard-button';
+ button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
- // add a clipboard button to all code blocks
- const codeBlocks = div.querySelectorAll('.hljs');
- codeBlocks.forEach(codeBlock => {
- const button = document.createElement('button');
- button.className = 'clipboard-button';
- button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
- button.onclick = () => {
- // navigator.clipboard.writeText(codeBlock.textContent);
- const range = document.createRange();
- range.setStartBefore(codeBlock);
- range.setEndAfter(codeBlock);
- window.getSelection()?.removeAllRanges();
- window.getSelection()?.addRange(range);
- document.execCommand('copy');
- window.getSelection()?.removeAllRanges();
+ button.onclick = () => {
+ const range = document.createRange();
+ range.setStartBefore(codeBlock);
+ range.setEndAfter(codeBlock);
+ window.getSelection()?.removeAllRanges();
+ window.getSelection()?.addRange(range);
+ document.execCommand('copy');
+ window.getSelection()?.removeAllRanges();
- button.innerHTML = '<i class=\'fas fa-check\'></i>';
- setTimeout(() => button.innerHTML = '<i class=\'fas fa-clipboard\'></i>', 1000);
- };
- codeBlock.appendChild(button);
- });
+ button.innerHTML = '<i class=\'fas fa-check\'></i>';
+ setTimeout(() => {
+ button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
+ }, 1000);
+ };
- $el.appendChild(div);
+ codeBlock.appendChild(button);
});
- $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
+ $el.appendChild(div);
});
- " x-intersect="
+
+ // Scroll to bottom after rendering
$el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
- " x-ref="messages" x-show="home === 2" x-transition="">
+ });
+ "
+ x-ref="messages"
+ x-show="home === 2"
+ x-transition=""
+>
</div>
<!-- Download Progress Section -->
@@ -353,4 +374,42 @@
</div>
</div>
</main>
+
+<script>
+ /**
+ * Transform a single message's content into HTML, preserving <think> blocks.
+ * Ensure LaTeX expressions are properly delimited for MathJax.
+ */
+ function transformMessageContent(message) {
+ let text = message.content;
+ console.log('Processing message content:', text);
+
+ // First replace think blocks
+ text = text.replace(
+ /<think>([\s\S]*?)(?:<\/think>|$)/g,
+ (match, body) => {
+ console.log('Found think block with content:', body);
+ const isComplete = match.includes('</think>');
+ const spinnerClass = isComplete ? '' : ' thinking';
+ const parsedBody = DOMPurify.sanitize(marked.parse(body));
+ return `
+<div class='thinking-block'>
+ <div class='thinking-header${spinnerClass}'>Thinking...</div>
+ <div class='thinking-content'>${parsedBody}</div>
+</div>`;
+ }
+ );
+
+ // Add backslashes to parentheses and brackets for LaTeX
+ text = text
+ .replace(/\((?=\s*[\d\\])/g, '\\(') // Add backslash before opening parentheses
+ .replace(/\)(?!\w)/g, '\\)') // Add backslash before closing parentheses
+ .replace(/\[(?=\s*[\d\\])/g, '\\[') // Add backslash before opening brackets
+ .replace(/\](?!\w)/g, '\\]') // Add backslash before closing brackets
+ .replace(/\[[\s\n]*\\boxed/g, '\\[\\boxed') // Ensure boxed expressions are properly delimited
+ .replace(/\\!/g, '\\\\!'); // Preserve LaTeX spacing commands
+
+ return DOMPurify.sanitize(marked.parse(text));
+ }
+</script>
</body>
diff --git a/exo/tinychat/index.js b/exo/tinychat/index.js
index 5aa6c4a9..5b0a76d0 100644
--- a/exo/tinychat/index.js
+++ b/exo/tinychat/index.js
@@ -393,8 +393,6 @@ document.addEventListener("alpine:init", () => {
},
async *openaiChatCompletion(model, messages) {
- // stream response
- console.log("model", model)
const response = await fetch(`${this.endpoint}/chat/completions`, {
method: "POST",
headers: {
@@ -417,19 +415,17 @@ document.addEventListener("alpine:init", () => {
const reader = response.body.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream()).getReader();
+
while (true) {
const { done, value } = await reader.read();
- if (done) {
- break;
- }
+ if (done) break;
+
if (value.type === "event") {
const json = JSON.parse(value.data);
if (json.choices) {
const choice = json.choices[0];
- if (choice.finish_reason === "stop") {
- break;
- }
- yield choice.delta.content;
+ if (choice.finish_reason === "stop") break;
+ if (choice.delta.content) yield choice.delta.content;
}
}
}
← d8ffa59d add deepseek v1, v3 and all the distills
·
back to Exo
·
we have a lot of models so group them nicely 59174bdc →