generated from nhcarrigan/template
feat: add copy buttons to user and assistant messages (#78)
Closes #74 Reviewed-on: #78 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #78.
This commit is contained in:
@@ -155,6 +155,30 @@
|
|||||||
terminalElement.removeEventListener("copy", handleCopy);
|
terminalElement.removeEventListener("copy", handleCopy);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Copy message content to clipboard
|
||||||
|
async function copyMessage(content: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
// Optionally capture to clipboard history
|
||||||
|
await clipboardStore.captureClipboard(content, null, "Message from chat");
|
||||||
|
|
||||||
|
// Visual feedback could be added here if needed
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy message:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State for showing "Copied!" feedback
|
||||||
|
let copiedMessageId: string | null = null;
|
||||||
|
|
||||||
|
async function handleCopyMessage(messageId: string, content: string) {
|
||||||
|
await copyMessage(content);
|
||||||
|
copiedMessageId = messageId;
|
||||||
|
setTimeout(() => {
|
||||||
|
copiedMessageId = null;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -185,7 +209,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each lines as line (line.id)}
|
{#each lines as line (line.id)}
|
||||||
<div class="terminal-line mb-2 {getLineClass(line.type)}">
|
<div class="terminal-line mb-2 {getLineClass(line.type)} relative group">
|
||||||
<span class="terminal-timestamp text-xs mr-2">{formatTime(line.timestamp)}</span>
|
<span class="terminal-timestamp text-xs mr-2">{formatTime(line.timestamp)}</span>
|
||||||
{#if getLinePrefix(line.type)}
|
{#if getLinePrefix(line.type)}
|
||||||
<span class="terminal-prefix mr-2">{getLinePrefix(line.type)}</span>
|
<span class="terminal-prefix mr-2">{getLinePrefix(line.type)}</span>
|
||||||
@@ -194,10 +218,32 @@
|
|||||||
<span class="terminal-tool-name mr-2">[{line.toolName}]</span>
|
<span class="terminal-tool-name mr-2">[{line.toolName}]</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if line.type === "assistant" || line.type === "user"}
|
{#if line.type === "assistant" || line.type === "user"}
|
||||||
<Markdown
|
<div class="message-content-wrapper">
|
||||||
content={maskPaths(line.content, hidePaths)}
|
<Markdown
|
||||||
searchQuery={currentSearchQuery}
|
content={maskPaths(line.content, hidePaths)}
|
||||||
/>
|
searchQuery={currentSearchQuery}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="copy-message-btn opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onclick={() => handleCopyMessage(line.id, line.content)}
|
||||||
|
title="Copy message"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="copy-text">{copiedMessageId === line.id ? "Copied!" : "Copy"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<HighlightedText
|
<HighlightedText
|
||||||
content={maskPaths(line.content, hidePaths)}
|
content={maskPaths(line.content, hidePaths)}
|
||||||
@@ -267,4 +313,45 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Message content wrapper for positioning */
|
||||||
|
.message-content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy button styling */
|
||||||
|
.copy-message-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-message-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-hover, var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-message-btn svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure relative positioning for parent */
|
||||||
|
.terminal-line {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user