feat: improve streaming completion detection and add syntax highlighting with copy buttons
- Detect when model streaming is complete in all streaming locations - Add syntax highlighting (hljs) to code blocks after streaming finishes - Add copy buttons to code blocks after highlighting is applied - Prevent duplicate copy buttons with CSS class detection - Optimize performance by doing syntax highlighting and copy buttons only once per message - Apply improvements to both modal streaming and standalone chat - Add copy buttons to intro message generation as well 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
41818a202b
commit
d7cc5531b8
2 changed files with 90 additions and 2 deletions
62
src/chat.js
62
src/chat.js
|
|
@ -6,6 +6,65 @@ import { getSelectedModel, getSelectedModelEndpoint } from "./models.js";
|
|||
import { initializeChatHistory, saveConversationHistory } from "./storage.js";
|
||||
import { generateTitleForTTS, speakText } from "./tts.js";
|
||||
|
||||
// Helper function to add copy buttons to code blocks
|
||||
function addCodeBlockCopyButtons(element) {
|
||||
const codeBlocks = element.querySelectorAll("pre code");
|
||||
codeBlocks.forEach((codeBlock) => {
|
||||
const pre = codeBlock.parentElement;
|
||||
if (pre.tagName.toLowerCase() === "pre") {
|
||||
// Check if copy button already exists to avoid duplicates
|
||||
if (pre.querySelector('.code-copy-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make the pre element relative for positioning
|
||||
pre.style.position = "relative";
|
||||
|
||||
// Create copy button
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "code-copy-btn";
|
||||
copyBtn.textContent = "📋";
|
||||
copyBtn.title = "Copy code";
|
||||
copyBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
copyBtn.onmouseenter = () => {
|
||||
copyBtn.style.opacity = "1";
|
||||
};
|
||||
copyBtn.onmouseleave = () => {
|
||||
copyBtn.style.opacity = "0.8";
|
||||
};
|
||||
|
||||
copyBtn.onclick = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeBlock.textContent);
|
||||
copyBtn.textContent = "✓";
|
||||
setTimeout(() => {
|
||||
copyBtn.textContent = "📋";
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy code:", error);
|
||||
}
|
||||
};
|
||||
|
||||
pre.appendChild(copyBtn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize chat history
|
||||
export let chatHistory = initializeChatHistory();
|
||||
|
||||
|
|
@ -91,6 +150,9 @@ export async function handleUserInput() {
|
|||
});
|
||||
}
|
||||
|
||||
// Streaming is complete - add copy buttons to code blocks
|
||||
addCodeBlockCopyButtons(responseContent);
|
||||
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
|
||||
// Add the response to chat history
|
||||
|
|
|
|||
|
|
@ -85,11 +85,17 @@ function addCodeBlockCopyButtons(element) {
|
|||
codeBlocks.forEach((codeBlock) => {
|
||||
const pre = codeBlock.parentElement;
|
||||
if (pre.tagName.toLowerCase() === "pre") {
|
||||
// Check if copy button already exists to avoid duplicates
|
||||
if (pre.querySelector('.code-copy-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make the pre element relative for positioning
|
||||
pre.style.position = "relative";
|
||||
|
||||
// Create copy button
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "code-copy-btn";
|
||||
copyBtn.textContent = "📋";
|
||||
copyBtn.title = "Copy code";
|
||||
copyBtn.style.cssText = `
|
||||
|
|
@ -1404,9 +1410,19 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
for await (const chunk of sendMessage(message)) {
|
||||
response += chunk;
|
||||
aiMsg.innerHTML = marked.parse(response);
|
||||
addCodeBlockCopyButtons(aiMsg);
|
||||
}
|
||||
|
||||
// Streaming is complete - now add final processing
|
||||
aiMsg.innerHTML = marked.parse(response);
|
||||
|
||||
// Add syntax highlighting to all code blocks
|
||||
aiMsg.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
// Add copy buttons after highlighting
|
||||
addCodeBlockCopyButtons(aiMsg);
|
||||
|
||||
// Add the AI response to chat history manually since sendMessage generator doesn't do it
|
||||
const { getChatHistory, updateChatHistory } = await import("./chat.js");
|
||||
const currentHistory = await getChatHistory();
|
||||
|
|
@ -2346,9 +2362,19 @@ ${getUIText("systemPromptInstructions")} ${languageName}.`;
|
|||
for await (const chunk of sendMessageWithCustomHistory(introHistory)) {
|
||||
response += chunk;
|
||||
introMsg.innerHTML = marked.parse(response);
|
||||
addCodeBlockCopyButtons(introMsg);
|
||||
}
|
||||
|
||||
// Streaming is complete - now add final processing
|
||||
introMsg.innerHTML = marked.parse(response);
|
||||
|
||||
// Add syntax highlighting to all code blocks
|
||||
introMsg.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
// Add copy buttons after highlighting
|
||||
addCodeBlockCopyButtons(introMsg);
|
||||
|
||||
// SECOND: Set up conversation system prompt for follow-up messages
|
||||
const conversationContextAppend = `
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue