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:
Russell Ballestrini 2025-07-04 12:22:18 -04:00
parent 41818a202b
commit d7cc5531b8
2 changed files with 90 additions and 2 deletions

View file

@ -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

View file

@ -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 = `