feat(client): add a copy button to read-only text

This commit is contained in:
Elian Doran 2025-05-26 15:17:10 +03:00
parent 4752db6bc5
commit 02e2b5d4ad
No known key found for this signature in database
7 changed files with 46 additions and 16 deletions

View File

@ -9,7 +9,7 @@ import treeService from "./tree.js";
import FNote from "../entities/fnote.js"; import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js"; import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js"; import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js"; import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import renderDoc from "./doc_renderer.js"; import renderDoc from "./doc_renderer.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
@ -106,7 +106,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
await linkService.loadReferenceLinkTitle($(el)); await linkService.loadReferenceLinkTitle($(el));
} }
await applySyntaxHighlight($renderedContent); await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote) { } else if (note instanceof FNote) {
await renderChildrenList($renderedContent, note); await renderChildrenList($renderedContent, note);
} }

View File

@ -1,6 +1,6 @@
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import { getCurrentLanguage } from "./i18n.js"; import { getCurrentLanguage } from "./i18n.js";
import { applySyntaxHighlight } from "./syntax_highlight.js"; import { formatCodeBlocks } from "./syntax_highlight.js";
export default function renderDoc(note: FNote) { export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => { return new Promise<JQuery<HTMLElement>>((resolve) => {
@ -41,7 +41,7 @@ function processContent(url: string, $content: JQuery<HTMLElement>) {
$img.attr("src", dir + "/" + $img.attr("src")); $img.attr("src", dir + "/" + $img.attr("src"));
}); });
applySyntaxHighlight($content); formatCodeBlocks($content);
} }
function getUrl(docNameValue: string, language: string) { function getUrl(docNameValue: string, language: string) {

View File

@ -6,16 +6,16 @@ let highlightingLoaded = false;
/** /**
* Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks. * Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks.
* Additionally, adds a "Copy to clipboard" button.
* *
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them. * @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
*/ */
export async function applySyntaxHighlight($container: JQuery<HTMLElement>) { export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
if (!isSyntaxHighlightEnabled()) { const syntaxHighlightingEnabled = isSyntaxHighlightEnabled();
return; if (syntaxHighlightingEnabled) {
await ensureMimeTypesForHighlighting();
} }
await ensureMimeTypesForHighlighting();
const codeBlocks = $container.find("pre code"); const codeBlocks = $container.find("pre code");
for (const codeBlock of codeBlocks) { for (const codeBlock of codeBlocks) {
const normalizedMimeType = extractLanguageFromClassList(codeBlock); const normalizedMimeType = extractLanguageFromClassList(codeBlock);
@ -23,10 +23,20 @@ export async function applySyntaxHighlight($container: JQuery<HTMLElement>) {
continue; continue;
} }
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); applyCopyToClipboardButton($(codeBlock));
if (syntaxHighlightingEnabled) {
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
}
} }
} }
export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
const $copyButton = $("<button>")
.addClass("bx component btn tn-tool-button bx-copy copy-button");
$codeBlock.parent().append($copyButton);
}
/** /**
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js. * Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
*/ */

View File

@ -529,6 +529,26 @@ pre:not(.hljs) {
font-size: 100%; font-size: 100%;
} }
pre {
position: relative;
}
pre > button.copy-button {
position: absolute;
top: 1em;
right: 1em;
opacity: 0.8;
}
pre > button.copy-button:hover {
color: inherit !important;
opacity: 1;
}
pre > button.copy-button:active {
background-color: unset !important;
}
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }

View File

@ -12,7 +12,7 @@ import { createChatSession, checkSessionExists, setupStreamingResponse, getDirec
import { extractInChatToolSteps } from "./message_processor.js"; import { extractInChatToolSteps } from "./message_processor.js";
import { validateEmbeddingProviders } from "./validation.js"; import { validateEmbeddingProviders } from "./validation.js";
import type { MessageData, ToolExecutionStep, ChatData } from "./types.js"; import type { MessageData, ToolExecutionStep, ChatData } from "./types.js";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
import "../../stylesheets/llm_chat.css"; import "../../stylesheets/llm_chat.css";
@ -925,7 +925,7 @@ export default class LlmChatPanel extends BasicWidget {
// Apply syntax highlighting if this is the final update // Apply syntax highlighting if this is the final update
if (isDone) { if (isDone) {
applySyntaxHighlight($(assistantMessageEl as HTMLElement)); formatCodeBlocks($(assistantMessageEl as HTMLElement));
// Update message in the data model for storage // Update message in the data model for storage
// Find the last assistant message to update, or add a new one if none exists // Find the last assistant message to update, or add a new one if none exists

View File

@ -2,7 +2,7 @@
* Utility functions for LLM Chat * Utility functions for LLM Chat
*/ */
import { marked } from "marked"; import { marked } from "marked";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
/** /**
* Format markdown content for display * Format markdown content for display
@ -62,7 +62,7 @@ export function escapeHtml(text: string): string {
* Apply syntax highlighting to content * Apply syntax highlighting to content
*/ */
export function applyHighlighting(element: HTMLElement): void { export function applyHighlighting(element: HTMLElement): void {
applySyntaxHighlight($(element)); formatCodeBlocks($(element));
} }
/** /**

View File

@ -1,5 +1,5 @@
import AbstractTextTypeWidget from "./abstract_text_type_widget.js"; import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { CommandListenerData, EventData } from "../../components/app_context.js"; import type { CommandListenerData, EventData } from "../../components/app_context.js";
import { getLocaleById } from "../../services/i18n.js"; import { getLocaleById } from "../../services/i18n.js";
@ -125,7 +125,7 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
} }
await this.#applyInlineMermaid(); await this.#applyInlineMermaid();
await applySyntaxHighlight(this.$content); await formatCodeBlocks(this.$content);
} }
async #applyInlineMermaid() { async #applyInlineMermaid() {