import { Component, input, ChangeDetectionStrategy, ViewEncapsulation, computed, effect, inject, ElementRef, AfterViewInit, ViewChild, } from "@angular/core"; import { CommonModule } from "@angular/common"; import { Marked } from "marked"; import hljs from "highlight.js"; import * as katex from "katex"; import { completePartialMarkdown } from "@copilotkit/core"; import { LucideAngularModule } from "lucide-angular"; import { copyToClipboard } from "@copilotkit/shared"; import { injectChatLabels } from "../../chat-config"; @Component({ standalone: true, selector: "copilot-chat-assistant-message-renderer", imports: [CommonModule, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
`, styles: [ ` copilot-chat-assistant-message-renderer { display: block; width: 100%; } /* Inline code styling */ copilot-chat-assistant-message-renderer code:not(pre code) { padding: 2.5px 4.8px; background-color: rgb(236, 236, 236); border-radius: 0.25rem; font-size: 0.875rem; font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; font-weight: 500; color: #000000; } .dark copilot-chat-assistant-message-renderer code:not(pre code) { background-color: #171717; /* same as code blocks */ color: rgb(248, 250, 252); /* text-foreground in dark mode */ } /* Code block container */ copilot-chat-assistant-message-renderer .code-block-container { position: relative; margin: 0.25rem 0; background-color: rgb(249, 249, 249); border-radius: 1rem; } .dark copilot-chat-assistant-message-renderer .code-block-container { background-color: #171717; } copilot-chat-assistant-message-renderer .code-block-header { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem 0.75rem 1rem; font-size: 0.75rem; background-color: transparent; } copilot-chat-assistant-message-renderer .code-block-language { font-weight: 400; color: rgba(115, 115, 115, 1); } .dark copilot-chat-assistant-message-renderer .code-block-language { color: white; } copilot-chat-assistant-message-renderer .code-block-copy-button { display: flex; align-items: center; gap: 0.125rem; padding: 0 0.5rem; font-size: 0.75rem; color: rgba(115, 115, 115, 1); cursor: pointer; background: none; border: none; transition: opacity 0.2s; } .dark copilot-chat-assistant-message-renderer .code-block-copy-button { color: white; } copilot-chat-assistant-message-renderer .code-block-copy-button:hover { opacity: 0.8; } copilot-chat-assistant-message-renderer .code-block-copy-button svg { width: 10px; height: 10px; } copilot-chat-assistant-message-renderer .code-block-copy-button span { font-size: 11px; } copilot-chat-assistant-message-renderer pre { margin: 0; padding: 0 1rem 1rem 1rem; overflow-x: auto; background-color: transparent; border-radius: 1rem; } .dark copilot-chat-assistant-message-renderer pre { background-color: transparent; } copilot-chat-assistant-message-renderer pre code { background-color: transparent; padding: 0; font-size: 0.875rem; font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; } /* Highlight.js theme adjustments */ copilot-chat-assistant-message-renderer .hljs { background: transparent; color: rgb(56, 58, 66); } .dark copilot-chat-assistant-message-renderer .hljs { background: transparent; color: #abb2bf; } /* Math equations */ copilot-chat-assistant-message-renderer .katex-display { overflow-x: auto; overflow-y: hidden; padding: 1rem 0; } `, ], }) export class CopilotChatAssistantMessageRenderer implements AfterViewInit { readonly content = input${highlighted}
<\/pre>/g, (match) => {
const index = codeBlocks.length;
codeBlocks.push(match);
return `${placeholder}${index}___`;
});
// Also protect inline code
const inlineCode: string[] = [];
const inlinePlaceholder = "___INLINE_CODE_PLACEHOLDER_";
html = html.replace(/[\s\S]*?<\/code>/g, (match) => {
const index = inlineCode.length;
inlineCode.push(match);
return `${inlinePlaceholder}${index}___`;
});
// Process display math $$ ... $$
html = html.replace(/\$\$([\s\S]*?)\$\$/g, (match, equation) => {
try {
return katex.renderToString(equation, {
displayMode: true,
throwOnError: false,
});
} catch {
return match;
}
});
// Process inline math $ ... $
html = html.replace(/\$([^$]+)\$/g, (match, equation) => {
try {
return katex.renderToString(equation, {
displayMode: false,
throwOnError: false,
});
} catch {
return match;
}
});
// Restore code blocks
codeBlocks.forEach((block, index) => {
html = html.replace(`${placeholder}${index}___`, block);
});
// Restore inline code
inlineCode.forEach((code, index) => {
html = html.replace(`${inlinePlaceholder}${index}___`, code);
});
return html;
}
private renderMathEquations(): void {
if (!this.markdownContainer) return;
const container = this.markdownContainer.nativeElement;
// Find all math placeholders and render them
const mathElements = container.querySelectorAll(".math-placeholder");
mathElements.forEach((element) => {
const equation = element.getAttribute("data-equation");
const displayMode = element.getAttribute("data-display") === "true";
if (equation) {
try {
katex.render(equation, element as HTMLElement, {
displayMode,
throwOnError: false,
});
} catch (error) {
console.error("Failed to render math equation:", error);
}
}
});
}
handleClick(event: MouseEvent): void {
const target = event.target as HTMLElement;
// Check if clicked on copy button or its children
const copyButton = target.closest(
".code-block-copy-button",
) as HTMLButtonElement;
if (copyButton) {
event.preventDefault();
const blockId = copyButton.getAttribute("data-code-block-id");
if (blockId) {
// Get the raw code from our map instead of from DOM
const code = this.codeBlocksMap.get(blockId);
if (code) {
this.copyCodeBlock(blockId, code);
}
}
}
}
private copyCodeBlock(blockId: string, code: string): void {
copyToClipboard(code).then((success) => {
if (!success) return;
// Update the button in the DOM
const button = this.elementRef.nativeElement.querySelector(
`[data-code-block-id="${blockId}"]`,
);
if (button) {
const originalHTML = button.innerHTML;
button.innerHTML = `
${this.labels.assistantMessageToolbarCopyCodeCopiedLabel}
`;
button.setAttribute(
"aria-label",
`${this.labels.assistantMessageToolbarCopyCodeCopiedLabel} code`,
);
// Reset after 2 seconds
setTimeout(() => {
button.innerHTML = originalHTML;
button.setAttribute(
"aria-label",
`${this.labels.assistantMessageToolbarCopyCodeLabel} code`,
);
}, 2000);
}
});
}
private generateBlockId(code: string): string {
// Simple hash function for generating unique IDs
let hash = 0;
for (let i = 0; i < code.length; i++) {
const char = code.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return `code-block-${hash}`;
}
private escapeHtml(text: string): string {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
}