diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index 589b0b62d..9c1c44669 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -263,6 +263,43 @@ cursor: not-allowed; } +/* Drag-over visual feedback */ +.chat-compose--drag-over { + outline: 2px dashed var(--accent); + outline-offset: -2px; + background: rgba(var(--accent-rgb), 0.05); + border-radius: 12px; +} + +/* File attach button */ +.chat-compose__attach { + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + cursor: pointer; + flex-shrink: 0; + border-radius: 8px; + border: 1px solid var(--border); + background: transparent; + color: var(--muted); + transition: all 150ms ease-out; +} + +.chat-compose__attach:hover { + background: var(--panel); + color: var(--text); + border-color: var(--border-strong); +} + +.chat-compose__attach svg { + width: 18px; + height: 18px; + stroke: currentColor; + fill: none; + stroke-width: 1.5px; +} + .chat-compose__actions { flex-shrink: 0; display: flex; diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 970adaad0..0c145f0af 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -93,4 +93,24 @@ describe("chat view", () => { expect(onNewSession).toHaveBeenCalledTimes(1); expect(container.textContent).not.toContain("Stop"); }); + + it("renders file input for image attachments", () => { + const container = document.createElement("div"); + render( + renderChat( + createProps({ + onAttachmentsChange: vi.fn(), + }), + ), + container, + ); + + const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement | null; + expect(fileInput).not.toBeNull(); + expect(fileInput?.accept).toBe("image/*"); + expect(fileInput?.multiple).toBe(true); + + const attachLabel = container.querySelector('label[for="chat-file-input"]'); + expect(attachLabel).not.toBeNull(); + }); }); diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index f5fb6e80b..d2302a360 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -108,10 +108,29 @@ function generateAttachmentId(): string { return `att-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } -function handlePaste( - e: ClipboardEvent, - props: ChatProps, -) { +function processFiles(files: FileList, props: ChatProps) { + if (!props.onAttachmentsChange) return; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (!file.type.startsWith("image/")) continue; + + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = reader.result as string; + const newAttachment: ChatAttachment = { + id: generateAttachmentId(), + dataUrl, + mimeType: file.type, + }; + const current = props.attachments ?? []; + props.onAttachmentsChange?.([...current, newAttachment]); + }; + reader.readAsDataURL(file); + } +} + +function handlePaste(e: ClipboardEvent, props: ChatProps) { const items = e.clipboardData?.items; if (!items || !props.onAttachmentsChange) return; @@ -146,6 +165,34 @@ function handlePaste( } } +function handleDragOver(e: DragEvent) { + e.preventDefault(); + e.stopPropagation(); + (e.currentTarget as HTMLElement).classList.add("chat-compose--drag-over"); +} + +function handleDragLeave(e: DragEvent) { + e.preventDefault(); + e.stopPropagation(); + (e.currentTarget as HTMLElement).classList.remove("chat-compose--drag-over"); +} + +function handleDrop(e: DragEvent, props: ChatProps) { + e.preventDefault(); + e.stopPropagation(); + (e.currentTarget as HTMLElement).classList.remove("chat-compose--drag-over"); + + const files = e.dataTransfer?.files; + if (!files || !props.onAttachmentsChange) return; + processFiles(files, props); +} + +function handleFileSelect(e: Event, props: ChatProps) { + const input = e.target as HTMLInputElement; + if (input.files) processFiles(input.files, props); + input.value = ""; // Reset so same file can be selected again +} + function renderAttachmentPreview(props: ChatProps) { const attachments = props.attachments ?? []; if (attachments.length === 0) return nothing; @@ -327,7 +374,12 @@ export function renderChat(props: ChatProps) { ` : nothing} -