generated from nhcarrigan/template
852a4d6661
## Summary - Adds Tauri clipboard-manager plugin to read images from native clipboard - Falls back to native clipboard when WebView clipboard API returns empty (fixes screenshot paste) - Allows sending messages with just attachments (no text required) - Logs attached files to output with 📎 emoji ## Test plan - [ ] Build and run the app natively on Windows - [ ] Copy a screenshot (Win+Shift+S) and paste in the chat input - [ ] Verify the screenshot appears as an attachment preview - [ ] Send the attachment and verify Claude receives the file path - [ ] Test sending a message with only an attachment (no text) - [ ] Verify the 📎 log line shows the attached filename **Note:** Paste will not work in WSLg dev environment due to clipboard isolation - needs native Windows build to test. ✨ This PR was created with help from Hikari~ 🌸 Co-authored-by: Hikari <hikari@nhcarrigan.com> Reviewed-on: #67 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
210 lines
4.7 KiB
Svelte
210 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import type { Attachment } from "$lib/types/messages";
|
|
|
|
interface Props {
|
|
attachments: Attachment[];
|
|
onRemove: (id: string) => void;
|
|
}
|
|
|
|
let { attachments, onRemove }: Props = $props();
|
|
|
|
function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
function getFileIcon(type: Attachment["type"]): string {
|
|
switch (type) {
|
|
case "image":
|
|
return "🖼️";
|
|
case "document":
|
|
return "📄";
|
|
default:
|
|
return "📎";
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if attachments.length > 0}
|
|
<div class="attachment-preview-container">
|
|
<div class="attachment-header">
|
|
<span class="attachment-count"
|
|
>{attachments.length} attachment{attachments.length !== 1 ? "s" : ""}</span
|
|
>
|
|
</div>
|
|
<div class="attachment-list">
|
|
{#each attachments as attachment (attachment.id)}
|
|
<div class="attachment-item" class:is-image={attachment.type === "image"}>
|
|
{#if attachment.type === "image" && attachment.previewUrl}
|
|
<div class="image-preview">
|
|
<img src={attachment.previewUrl} alt={attachment.filename} />
|
|
</div>
|
|
{:else}
|
|
<div class="file-icon">
|
|
{getFileIcon(attachment.type)}
|
|
</div>
|
|
{/if}
|
|
<div class="attachment-info">
|
|
<span class="attachment-filename" title={attachment.filename}>
|
|
{attachment.filename}
|
|
</span>
|
|
<span class="attachment-size">
|
|
{formatFileSize(attachment.size)}
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="remove-button"
|
|
onclick={() => onRemove(attachment.id)}
|
|
title="Remove attachment"
|
|
>
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.attachment-preview-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 8px;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.attachment-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.attachment-count {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.attachment-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.attachment-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 8px;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
max-width: 200px;
|
|
position: relative;
|
|
}
|
|
|
|
.attachment-item.is-image {
|
|
flex-direction: column;
|
|
padding: 4px;
|
|
max-width: 120px;
|
|
}
|
|
|
|
.image-preview {
|
|
width: 100%;
|
|
max-width: 110px;
|
|
max-height: 80px;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
background: var(--bg-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.image-preview img {
|
|
max-width: 100%;
|
|
max-height: 80px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.file-icon {
|
|
font-size: 24px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.attachment-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
overflow: hidden;
|
|
flex: 1;
|
|
}
|
|
|
|
.is-image .attachment-info {
|
|
width: 100%;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.attachment-filename {
|
|
font-size: 12px;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.attachment-size {
|
|
font-size: 10px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.remove-button {
|
|
position: absolute;
|
|
top: -6px;
|
|
right: -6px;
|
|
width: 20px;
|
|
height: 20px;
|
|
padding: 0;
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 50%;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
transition:
|
|
opacity 0.2s,
|
|
background 0.2s,
|
|
color 0.2s;
|
|
}
|
|
|
|
.attachment-item:hover .remove-button {
|
|
opacity: 1;
|
|
}
|
|
|
|
.remove-button:hover {
|
|
background: var(--error-color, #ef4444);
|
|
border-color: var(--error-color, #ef4444);
|
|
color: white;
|
|
}
|
|
</style>
|