feature: copy text button, nudge & fix: various styles

- Copy button for all messages - Nudge button front-end - Fixed various non-styled light mode elements to do -> css cleanup and whisper loading

Alessandro committed Dec 3, 2024 at 04:03 UTC fedf2d4bdc6357f9e50e76b9202e06081c66db5e
3 files changed +219 -31
webui/index.css
+127 -25
@@ -540,6 +540,7 @@ pre {
540 .message-user {
541 background-color: #4a4a4a;
542 border-bottom-right-radius: var(--spacing-xs);
543 + min-width: 195px;
544 text-align: end;
545 }
546
@@ -590,6 +591,7 @@ font-size: var(--font-size-small)
591 color: #e0e0e0;
592 }
593 .message-agent-response {
594 + min-width: 255px;
595 background-color: #1f3c1e;
596 color: #e0e0e0;
597 }
@@ -641,7 +643,7 @@ font-size: var(--font-size-small)
643 .msg-kvps th,
644 .msg-kvps td {
645 border-bottom: 1px solid rgba(255, 255, 255, 0.1);
644 - align-content: baseline;
646 + align-content: center;
647 padding: 0.25rem;
648 text-align: left;
649 }
@@ -877,7 +879,7 @@ font-size: var(--font-size-small)
879 }
880
881 .attachment-item:hover {
880 - background: var(--color-secondary-dark);
882 + background: var(--color-secondary);
883 }
884
885 .attachment-item.file-type {
@@ -885,7 +887,7 @@ font-size: var(--font-size-small)
887 }
888
889 .attachment-item:hover {
888 - background: var(--color-secondary-dark);
890 + background: var(--color-secondary);
891 }
892
893 .attachment-preview {
@@ -982,10 +984,6 @@ font-size: var(--font-size-small)
984 text-transform: uppercase;
985 }
986
985 -.file-preview:hover {
986 - background: var(--color-secondary-dark);
987 -}
988 -
987 .remove-attachment {
988 position: absolute;
989 top: -6px;
@@ -1013,21 +1011,6 @@ font-size: var(--font-size-small)
1011 transform: scale(0.9);
1012 }
1013
1016 -/* File type specific styles
1017 -.extension[data-type="image"] {
1018 - background: #4CAF50;
1019 -}
1020 -
1021 -.extension[data-type="code"] {
1022 - background: #2196F3;
1023 -}
1024 -
1025 -.extension[data-type="document"] {
1026 - background: #FF9800;
1027 -}
1028 -
1029 -*/
1030 -
1014 /* Error handling */
1015 .image-error {
1016 border: 1px solid var(--color-error);
@@ -1075,7 +1058,7 @@ font-size: var(--font-size-small)
1058 border-radius: 5px;
1059 color: var(--color-text);
1060 font-family: "Rubik", Arial, Helvetica, sans-serif;
1078 - font-size: 0.7rem;
1061 + font-size: 0.6rem;
1062 padding: 6px var(--spacing-sm);
1063 cursor: pointer;
1064 opacity: 0.8;
@@ -1326,6 +1309,11 @@ input:checked + .slider:before {
1309 --color-input-focus: var(--color-input-focus-light);
1310 }
1311
1312 +.light-mode .msg-kvps th,
1313 +.light-mode .msg-kvps td {
1314 + border-bottom: 1px solid var(--color-border);
1315 +}
1316 +
1317 .light-mode .message-default {
1318 background-color: #ffffff;
1319 color: #1a242f;
@@ -1434,6 +1422,12 @@ input:checked + .slider:before {
1422 filter: invert(100%) grayscale(100%);
1423 }
1424
1425 +.light-mode .extension {
1426 + background: var(--color-primary);
1427 + color: var(--color-background);
1428 + opacity: 0.7;
1429 +}
1430 +
1431 /* Utility Classes */
1432 .kvps-key {
1433 font-weight: 500;
@@ -1563,7 +1557,7 @@ input:checked + .slider:before {
1557 }
1558 }
1559
1566 -@media (max-width: 480px) {
1560 +@media (max-width: 510px) {
1561 .text-buttons-row {
1562 display: table;
1563 gap: 0.1rem !important;
@@ -1649,18 +1643,59 @@ input:checked + .slider:before {
1643 }
1644
1645 .text-buttons-row {
1652 - width: 100%;
1646 + width: 90%;
1647 display: flex;
1648 padding-top: var(--spacing-xs);
1649 gap: var(--spacing-xs);
1650 white-space: pre-wrap;
1651 }
1652
1653 + .text-button {
1654 + font-size: 0.6rem;
1655 + }
1656 +
1657 .text-button svg {
1658 width: 18px;
1659 height: 18px;
1660 flex-shrink: 0; /* prevents SVG from shrinking */
1661 }
1662 + .copy-button {
1663 + display: none !important;
1664 + }
1665 +
1666 + .msg-content span,
1667 + .kvps-val,
1668 + .message-text {
1669 + cursor: pointer;
1670 + position: relative;
1671 + }
1672 +
1673 + .msg-thoughts span::after,
1674 + .msg-content span::after,
1675 + .kvps-val::after,
1676 + .message-text::after {
1677 + content: 'Copied!';
1678 + position: absolute;
1679 + opacity: 0;
1680 + font-family: "Rubik", Arial, Helvetica, sans-serif;
1681 + font-size: 0.7rem;
1682 + text-wrap: nowrap;
1683 + padding: 6px var(--spacing-sm);
1684 + transition: opacity var(--transition-speed) ease-in-out;
1685 + }
1686 +
1687 + .msg-thoughts span.copied::after,
1688 + .msg-content span.copied::after,
1689 + .kvps-val.copied::after,
1690 + .message-text.copied::after {
1691 + opacity: 1;
1692 + right: 0px;
1693 + top: 0px;
1694 + background-color: var(--color-background);
1695 + border: none;
1696 + border-radius: 5px;
1697 + color: inherit;
1698 + }
1699 }
1700
1701 @media (min-width: 768px) {
@@ -1703,4 +1738,71 @@ input:checked + .slider:before {
1738 -webkit-text-size-adjust: none;
1739 text-size-adjust: none;
1740 }
1741 +}
1742 +
1743 +/* Copy button styles */
1744 +.copy-button {
1745 + position: absolute;
1746 + right: 0;
1747 + top: var(--spacing-sm);
1748 + background: none;
1749 + border: none;
1750 + padding: var(--spacing-xs) var(--spacing-sm);
1751 + padding-right: 0;
1752 + cursor: pointer;
1753 + text-decoration: underline;
1754 + text-wrap: nowrap;
1755 + opacity: 0;
1756 + transition: opacity var(--transition-speed) ease-in-out;
1757 + color: inherit;
1758 + font-size: 12px;
1759 + font-family: "Rubik", Arial, Helvetica, sans-serif;
1760 +}
1761 +
1762 +.copy-button:hover {
1763 + opacity: 0.8 !important;
1764 +}
1765 +
1766 +.msg-content:hover .copy-button,
1767 +.kvps-row:hover .copy-button,
1768 +.message-text:hover .copy-button {
1769 + opacity: 0.6;
1770 +}
1771 +
1772 +.copy-button.copied {
1773 + font-family: "Rubik", Arial, Helvetica, sans-serif !important;
1774 + opacity: 1 !important;
1775 +}
1776 +
1777 +.msg-thoughts .copy-button {
1778 + top: -12px !important;
1779 +}
1780 +
1781 +.message-user .copy-button {
1782 + top: -15px !important;
1783 + left: -13px !important;
1784 + right: 99% !important;
1785 +}
1786 +
1787 +.message-agent-response .copy-button {
1788 + top: -22px !important;
1789 + right: 0 !important;
1790 + padding-right: 0px !important;
1791 +}
1792 +
1793 +.message-tool .copy-button {
1794 + top: -12px !important;
1795 + right: 0 !important;
1796 +}
1797 +
1798 +.msg-output .copy-button {
1799 + top: -6px !important;
1800 + right: 0 !important;
1801 +}
1802 +
1803 +/* Make message containers relative for absolute positioning of copy buttons */
1804 +.msg-content,
1805 +.kvps-row,
1806 +.message-text {
1807 + position: relative;
1808 }
\ No newline at end of file
webui/index.html
+8
@@ -388,6 +388,14 @@
388 <p>Context</p>
389 </button>
390
391 + <button class="text-button" id="nudges_window" @click="NUDGE_LOGIC()">
392 + <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 58" fill="currentColor">
393 + <path d="m11.97,16.32c-.46,0-.91-.25-1.15-.68-.9-1.63-1.36-3.34-1.36-5.1C9.45,4.73,14.18,0,20,0s10.55,4.73,10.55,10.55c0,.87-.13,1.75-.41,2.76-.19.7-.9,1.13-1.62.93-.7-.19-1.12-.92-.93-1.62.21-.79.31-1.44.31-2.07,0-4.36-3.55-7.91-7.91-7.91s-7.91,3.55-7.91,7.91c0,1.3.35,2.59,1.03,3.82.36.64.13,1.44-.51,1.79-.21.11-.42.17-.64.17Z" stroke-width="0.5" stroke="currentColor"/>
394 + <path d="m34.5,58h-6.18c-3.17,0-6.15-1.23-8.39-3.47L1.16,35.75c-1.54-1.54-1.54-4.05,0-5.59,2.4-2.4,6.27-2.68,8.99-.64l4.58,3.44V10.55c0-2.91,2.36-5.27,5.27-5.27s5.27,2.36,5.27,5.27v8.62c.78-.45,1.68-.71,2.64-.71,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.91,0,5.27,2.36,5.27,5.27v14.5c0,8-6.51,14.5-14.5,14.5ZM6.03,30.79c-1.1,0-2.19.42-3.01,1.23-.51.51-.51,1.35,0,1.86l18.77,18.78c1.74,1.74,4.06,2.7,6.53,2.7h6.18c6.54,0,11.86-5.32,11.86-11.86v-14.5c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v1.32c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-3.95c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v3.95c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-6.59c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v6.59c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32V10.55c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v25.05c0,.5-.28.95-.73,1.18s-.98.18-1.38-.12l-6.69-5.02c-.75-.56-1.65-.84-2.54-.84Z" stroke-width="0.5" stroke="currentColor"/>
395 + </svg>
396 + <p>Nudge</p>
397 + </button>
398 +
399 </div>
400 </div>
401 </div>
webui/js/messages.js
+84 -6
@@ -1,3 +1,46 @@
1 +// copy button
2 +
3 +function createCopyButton() {
4 + const button = document.createElement('button');
5 + button.className = 'copy-button';
6 + button.textContent = 'Copy';
7 +
8 + button.addEventListener('click', async function(e) {
9 + e.stopPropagation();
10 + const container = this.closest('.msg-content, .kvps-row, .message-text');
11 + let textToCopy;
12 +
13 + if (container.classList.contains('kvps-row')) {
14 + textToCopy = container.querySelector('.kvps-val').textContent;
15 + } else if (container.classList.contains('message-text')) {
16 + textToCopy = container.textContent.replace('copy', '');
17 + } else {
18 + textToCopy = container.querySelector('span').textContent;
19 + }
20 +
21 + try {
22 + await navigator.clipboard.writeText(textToCopy);
23 + const originalText = button.textContent;
24 + button.classList.add('copied');
25 + button.textContent = 'Copied!';
26 + setTimeout(() => {
27 + button.classList.remove('copied');
28 + button.textContent = originalText;
29 + }, 2000);
30 + } catch (err) {
31 + console.error('Failed to copy text:', err);
32 + }
33 + });
34 +
35 + return button;
36 +}
37 +
38 +function addCopyButtonToElement(element) {
39 + if (!element.querySelector('.copy-button')) {
40 + element.appendChild(createCopyButton());
41 + }
42 +}
43 +
44 export function getHandler(type) {
45 switch (type) {
46 case 'user':
@@ -49,7 +92,14 @@ export function _drawMessage(messageContainer, heading, content, temp, followUp,
92
93 const spanElement = document.createElement('span');
94 spanElement.innerHTML = content;
95 +
96 + // Add click handler for small screens
97 + spanElement.addEventListener('click', () => {
98 + copyText(spanElement.textContent, spanElement);
99 + });
100 +
101 preElement.appendChild(spanElement);
102 + addCopyButtonToElement(preElement);
103 messageDiv.appendChild(preElement);
104
105 // Render LaTeX math within the span
@@ -114,6 +164,13 @@ export function drawMessageUser(messageContainer, id, type, heading, content, te
164 const textDiv = document.createElement('div');
165 textDiv.classList.add('message-text');
166 textDiv.textContent = content;
167 +
168 + // Add click handler
169 + textDiv.addEventListener('click', () => {
170 + copyText(content, textDiv);
171 + });
172 +
173 + addCopyButtonToElement(textDiv);
174 messageDiv.appendChild(textDiv);
175 }
176
@@ -229,17 +286,20 @@ function drawKvps(container, kvps) {
286 const pre = document.createElement('pre');
287 pre.classList.add('kvps-val');
288
232 - // if value is array, join it with new line
289 if (Array.isArray(value)) value = value.join('\n');
290
291 if (row.classList.contains('msg-thoughts')) {
236 - // Wrap content in a <span> to allow HTML parsing
292 const span = document.createElement('span');
238 - span.innerHTML = value; // Use innerHTML to enable KaTeX parsing
293 + span.innerHTML = value;
294 pre.appendChild(span);
295 td.appendChild(pre);
296 + addCopyButtonToElement(row);
297 +
298 + // Add click handler
299 + span.addEventListener('click', () => {
300 + copyText(span.textContent, span);
301 + });
302
242 - // Render LaTeX math within the span
303 if (window.renderMathInElement) {
304 renderMathInElement(span, {
305 delimiters: [
@@ -248,13 +308,19 @@ function drawKvps(container, kvps) {
308 { left: "$", right: "$", display: false },
309 { left: "\$$", right: "\$$", display: false }
310 ],
251 - throwOnError: false // Prevent KaTeX from throwing errors
311 + throwOnError: false
312 });
313 }
314 } else {
255 - // For non-thought rows, use textContent as before
315 pre.textContent = value;
316 +
317 + // Add click handler
318 + pre.addEventListener('click', () => {
319 + copyText(value, pre);
320 + });
321 +
322 td.appendChild(pre);
323 + addCopyButtonToElement(row);
324 }
325 }
326 container.appendChild(table);
@@ -282,3 +348,15 @@ function convertImageTags(content) {
348
349 return updatedContent;
350 }
351 +
352 +async function copyText(text, element) {
353 + try {
354 + await navigator.clipboard.writeText(text);
355 + element.classList.add('copied');
356 + setTimeout(() => {
357 + element.classList.remove('copied');
358 + }, 2000);
359 + } catch (err) {
360 + console.error('Failed to copy text:', err);
361 + }
362 +}