mirror of
https://github.com/exo-explore/exo.git
synced 2025-10-23 02:57:14 +03:00
handle thinking outputs nicely, format latex beautifully
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 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();
|
||||
// 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>';
|
||||
|
||||
button.innerHTML = '<i class=\'fas fa-check\'></i>';
|
||||
setTimeout(() => button.innerHTML = '<i class=\'fas fa-clipboard\'></i>', 1000);
|
||||
};
|
||||
codeBlock.appendChild(button);
|
||||
});
|
||||
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();
|
||||
|
||||
$el.appendChild(div);
|
||||
button.innerHTML = '<i class=\'fas fa-check\'></i>';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user