Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 71 additions & 10 deletions webviewUi/src/components/botMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import DOMPurify from "dompurify";
import React from "react";
import { BotIcon } from "./botIcon";
import { DownloadIcon } from "./downloadIcon";
import { downloadAsMarkdown } from "../utils/downloadMarkdown";
import { IParseURL, parseUrl } from "../utils/parseUrl";
import UrlCardList from "./urlCardList";

Expand All @@ -20,16 +19,78 @@ export const BotMessage: React.FC<CodeBlockProps> = ({ language, content }) => {
parsedUrls = parseUrl(sanitizedContent);
}

const handleDownload = () => {
console.log("Download button clicked");
console.log("Content length:", sanitizedContent.length);
console.log("Content preview:", sanitizedContent.substring(0, 100));
const handleCopyMarkdown = async () => {
try {
// Use the original content directly since it's already in markdown format
const markdownContent = content || sanitizedContent;

const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, "-");
const filename = `codebuddy-response-${timestamp}`;
console.log("Filename:", filename);
// If content is HTML, we need to convert it back to markdown
// But if it's already markdown, use it as-is
let contentToCopy = markdownContent;

downloadAsMarkdown(sanitizedContent, filename);
// Check if the content contains HTML tags (indicating it's been processed)
if (markdownContent.includes("<") && markdownContent.includes(">")) {
// Create a temporary div to extract text content while preserving line breaks
const tempDiv = document.createElement("div");
tempDiv.innerHTML = markdownContent;

// Get the text content and preserve markdown-like structure
contentToCopy = tempDiv.textContent || tempDiv.innerText || "";

// Clean up the text to maintain markdown formatting
contentToCopy = contentToCopy
.replace(/\n\s*\n\s*\n/g, "\n\n") // Remove excessive line breaks
.replace(/(^\s+)|(\s+$)/g, "") // Trim whitespace
.trim();
} else {
// Content is already in markdown format, use it directly
contentToCopy = markdownContent.trim();
}

await navigator.clipboard.writeText(contentToCopy);
console.log("Markdown content copied to clipboard successfully");
} catch (error) {
console.error("Failed to copy markdown to clipboard:", error);

// Fallback method
try {
const markdownContent = content || sanitizedContent;
let contentToCopy = markdownContent;

if (markdownContent.includes("<") && markdownContent.includes(">")) {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = markdownContent;
contentToCopy = tempDiv.textContent || tempDiv.innerText || "";
contentToCopy = contentToCopy
.replace(/\n\s*\n\s*\n/g, "\n\n")
.replace(/(^\s+)|(\s+$)/g, "")
.trim();
} else {
contentToCopy = markdownContent.trim();
}

// Create a temporary textarea for fallback copy
const textarea = document.createElement("textarea");
textarea.value = contentToCopy;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();

// Use the modern approach with fallback
try {
document.execCommand("copy");
} catch (execError) {
console.error("execCommand failed:", execError);
}

document.body.removeChild(textarea);

console.log("Markdown content copied to clipboard using fallback method");
} catch (fallbackError) {
console.error("Both clipboard methods failed:", fallbackError);
}
}
};

return (
Expand All @@ -45,7 +106,7 @@ export const BotMessage: React.FC<CodeBlockProps> = ({ language, content }) => {
<div className="code-header">
<span className="language-label">{language}</span>
<div className="header-buttons">
<DownloadIcon onClick={handleDownload} />
<DownloadIcon onClick={handleCopyMarkdown} />
</div>
</div>
{parsedUrls.length > 0 ? (
Expand Down
75 changes: 49 additions & 26 deletions webviewUi/src/components/downloadIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,73 @@
import React, { useState } from "react";

interface DownloadIconProps {
onClick: () => void;
onClick: () => void | Promise<void>;
className?: string;
title?: string;
}

export const DownloadIcon: React.FC<DownloadIconProps> = ({
onClick,
className = "",
title = "Download as Markdown",
}) => {
const [isDownloading, setIsDownloading] = useState(false);
export const DownloadIcon: React.FC<DownloadIconProps> = ({ onClick, className = "", title = "Copy as Markdown" }) => {
const [isProcessing, setIsProcessing] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);

const handleClick = async () => {
setIsDownloading(true);
setIsProcessing(true);
setShowSuccess(false);

try {
await onClick();
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 2000);
} catch (error) {
console.error("Copy operation failed:", error);
} finally {
setTimeout(() => setIsDownloading(false), 1000);
setIsProcessing(false);
}
};

const getButtonText = () => {
if (showSuccess) return "Copied!";
if (isProcessing) return "...";
return "MD";
};

return (
<button
onClick={handleClick}
className={`download-button ${className} ${isDownloading ? "downloading" : ""}`}
className={`download-button ${className} ${isProcessing ? "processing" : ""} ${showSuccess ? "success" : ""}`}
title={title}
aria-label={title}
disabled={isDownloading}
disabled={isProcessing}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<span className="download-text">{isDownloading ? "..." : "MD"}</span>
{showSuccess ? (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20,6 9,17 4,12" />
</svg>
) : (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
)}
<span className="download-text">{getButtonText()}</span>
</button>
);
};
35 changes: 35 additions & 0 deletions webviewUi/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,41 @@ hr {
color: white;
}

/* Individual code block wrapper and header styles */
.code-block-wrapper {
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
margin: 16px 0;
font-family: "JetBrains Mono", SF Mono, "Geist Mono", "Fira Code", "Fira Mono", "Menlo", "Consolas", "DejaVu Sans Mono", monospace;
}

.individual-code-header {
background-color: var(--vscode-editor-background);
border-bottom: 1px solid var(--vscode-panel-border);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}

.individual-code-header .copy-button {
margin-left: 0;
font-size: 11px;
padding: 3px 6px;
border-color: #7aa2f7;
}

.individual-code-header .copy-button:hover {
background: #7aa2f7;
border-color: #7aa2f7;
color: white;
}

.code-block-wrapper pre {
margin: 0;
border-radius: 0 0 6px 6px;
background-color: var(--vscode-editor-background);
}

.download-button {
border: 1px solid #28a745;
background: transparent;
Expand Down
61 changes: 39 additions & 22 deletions webviewUi/src/utils/highlightCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@ export const highlightCodeBlocks = (hljsApi: HLJSApi, messages: any) => {
if (!hljsApi || messages?.length <= 0) return;
document.querySelectorAll("pre code:not(.hljs-done)").forEach((block) => {
let language = null;
const languageClass = Array.from(block.classList).find((className) =>
className.startsWith("language-"),
);
const languageClass = Array.from(block.classList).find((className) => className.startsWith("language-"));
if (languageClass) {
language = languageClass.substring("language-".length);
}

try {
const decodedCode = decodeHtml(block.textContent ?? "");
const detectedLanguage =
language ?? hljsApi.highlightAuto(decodedCode).language;
const detectedLanguage = language ?? hljsApi.highlightAuto(decodedCode).language;
if (detectedLanguage != undefined) {
const highlightedCode = hljsApi.highlight(decodedCode, {
language: detectedLanguage,
Expand All @@ -48,23 +45,43 @@ export const highlightCodeBlocks = (hljsApi: HLJSApi, messages: any) => {
}
});

// Find the closest code-block parent and add the button to the header buttons container
const codeBlockParent = block.closest(".code-block");
if (codeBlockParent) {
const headerButtons =
codeBlockParent.querySelector(".header-buttons");
if (headerButtons) {
headerButtons.appendChild(copyButton);
} else {
const codeHeader = codeBlockParent.querySelector(".code-header");
if (codeHeader) {
codeHeader.appendChild(copyButton);
} else {
codeBlockParent.appendChild(copyButton);
}
}
} else {
block.parentNode?.insertBefore(copyButton, block);
// Create a wrapper for the code block with its own copy button
const preElement = block.closest("pre");
if (preElement && !preElement.querySelector(".code-block-wrapper")) {
// Create a wrapper div for this specific code block
const wrapper = document.createElement("div");
wrapper.classList.add("code-block-wrapper");
wrapper.style.position = "relative";
wrapper.style.marginBottom = "1rem";

// Create a header for this code block
const codeHeader = document.createElement("div");
codeHeader.classList.add("individual-code-header");
codeHeader.style.display = "flex";
codeHeader.style.justifyContent = "space-between";
codeHeader.style.alignItems = "center";
codeHeader.style.padding = "0.5rem 1rem";
codeHeader.style.backgroundColor = "var(--vscode-editor-background)";
codeHeader.style.borderBottom = "1px solid var(--vscode-panel-border)";
codeHeader.style.fontSize = "0.875rem";

// Add language label
const languageLabel = document.createElement("span");
languageLabel.textContent = detectedLanguage || "code";
languageLabel.style.color = "var(--vscode-editor-foreground)";
languageLabel.style.opacity = "0.8";

// Add copy button to the header
copyButton.style.position = "static";
copyButton.style.margin = "0";

codeHeader.appendChild(languageLabel);
codeHeader.appendChild(copyButton);

// Wrap the pre element
preElement.parentNode?.insertBefore(wrapper, preElement);
wrapper.appendChild(codeHeader);
wrapper.appendChild(preElement);
}
}
} catch (error) {
Expand Down