add copy btn to code blocks and tables

3clyp50 committed Jan 27, 2026 at 18:07 UTC d189156a1658726752d41118275b9c44c33a888b
3 files changed +83 -13
webui/components/messages/action-buttons/simple-action-buttons.css
+17 -5
@@ -14,7 +14,8 @@
14 pointer-events: none;
15 }
16
17 -.step-action-buttons .action-button {
17 +.step-action-buttons .action-button,
18 +.action-button {
19 display: flex;
20 align-items: center;
21 justify-content: center;
@@ -24,7 +25,8 @@
25 cursor: pointer;
26 }
27
27 -.step-action-buttons .action-button .material-symbols-outlined {
28 +.step-action-buttons .action-button .material-symbols-outlined,
29 +.action-button .material-symbols-outlined {
30 font-size: 0.85rem;
31 line-height: 1;
32 color: var(--color-message-text);
@@ -32,18 +34,21 @@
34 transition: color 0.15s ease;
35 }
36
35 -.step-action-buttons .action-button:hover .material-symbols-outlined {
37 +.step-action-buttons .action-button:hover .material-symbols-outlined,
38 +.action-button:hover .material-symbols-outlined {
39 color: var(--color-text);
40 }
41
42 /* Success state */
40 -.step-action-buttons .action-button.success .material-symbols-outlined {
43 +.step-action-buttons .action-button.success .material-symbols-outlined,
44 +.action-button.success .material-symbols-outlined {
45 color: #4CAF50;
46 font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
47 }
48
49 /* Error state */
46 -.step-action-buttons .action-button.error .material-symbols-outlined {
50 +.step-action-buttons .action-button.error .material-symbols-outlined,
51 +.action-button.error .material-symbols-outlined {
52 color: var(--color-accent);
53 font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
54 }
@@ -65,6 +70,13 @@
70 pointer-events: auto;
71 }
72
73 +/* Code blocks and tables: show copy button on hover */
74 +.device-pointer .code-block-wrapper:hover .step-action-buttons,
75 +.device-pointer .message-markdown-table-wrap:hover .step-action-buttons {
76 + opacity: 1;
77 + pointer-events: auto;
78 +}
79 +
80 /* ===========================================
81 Touch devices - Always visible
82 =========================================== */
webui/css/messages.css
+17
@@ -732,6 +732,23 @@
732 border-radius: 0.3em;
733 }
734
735 +/* Code block and table copy buttons */
736 +.code-block-wrapper,
737 +.message-markdown-table-wrap {
738 + position: relative;
739 +}
740 +
741 +.overlay-actions {
742 + position: absolute;
743 + top: 0;
744 + right: 0;
745 + background: var(--color-panel);
746 + border-radius: var(--border-radius-sm);
747 + z-index: 1;
748 + padding: 2px !important;
749 + gap: 0 !important;
750 +}
751 +
752 .msg-content hr {
753 border: 0;
754 border-top: 1px solid var(--color-border);
webui/js/messages.js
+49 -8
@@ -1883,16 +1883,57 @@ function convertPathsToLinks(str) {
1883 .join("");
1884 }
1885
1886 +// markdown render helpers //
1887 +
1888 +// wraps an element with a container div
1889 +const wrapElement = (el, className) => {
1890 + const wrapper = document.createElement("div");
1891 + wrapper.className = className;
1892 + el.parentNode.insertBefore(wrapper, el);
1893 + wrapper.appendChild(el);
1894 + return wrapper;
1895 +};
1896 +
1897 +// data extractors
1898 +const extractTableTSV = (table) =>
1899 + [...table.rows]
1900 + .map((row) =>
1901 + [...row.cells]
1902 + .map((cell) => cell.textContent.replace(/\t/g, " ").replace(/\n/g, " "))
1903 + .join("\t"),
1904 + )
1905 + .join("\n");
1906 +
1907 function adjustMarkdownRender(element) {
1908 // find all tables in the element
1888 - const elements = element.querySelectorAll("table");
1889 -
1890 - // wrap each with a div with class message-markdown-table-wrap
1891 - elements.forEach((el) => {
1892 - const wrapper = document.createElement("div");
1893 - wrapper.className = "message-markdown-table-wrap";
1894 - el.parentNode.insertBefore(wrapper, el);
1895 - wrapper.appendChild(el);
1909 + const tables = element.querySelectorAll("table");
1910 + tables.forEach((el) => {
1911 + if (el.parentNode.classList.contains("message-markdown-table-wrap"))
1912 + return;
1913 + const wrapper = wrapElement(el, "message-markdown-table-wrap");
1914 +
1915 + // create copy button directly
1916 + const copyBtn = createActionButton("copy", "", () =>
1917 + copyToClipboard(extractTableTSV(el))
1918 + );
1919 + copyBtn.classList.add("step-action-buttons", "overlay-actions");
1920 + wrapper.appendChild(copyBtn);
1921 + });
1922 +
1923 + // find all code blocks
1924 + // we select the code element to ensure valid targets, then wrap the parent pre
1925 + const codeElements = element.querySelectorAll("pre > code");
1926 + codeElements.forEach((code) => {
1927 + const pre = code.parentNode;
1928 + if (pre.parentNode.classList.contains("code-block-wrapper")) return;
1929 + const wrapper = wrapElement(pre, "code-block-wrapper");
1930 +
1931 + // create copy button directly
1932 + const copyBtn = createActionButton("copy", "", () =>
1933 + copyToClipboard(code.textContent)
1934 + );
1935 + copyBtn.classList.add("step-action-buttons", "overlay-actions");
1936 + wrapper.appendChild(copyBtn);
1937 });
1938
1939 // find all images