Add image clipboard support with paste detection and thumbnails

This commit is contained in:
Shantur Rathore
2025-10-24 13:14:54 +01:00
parent 3c73a6bc4a
commit 640dbec2fb

View File

@@ -35,6 +35,7 @@ export default function PromptInput(props: PromptInputProps) {
const [isDragging, setIsDragging] = createSignal(false) const [isDragging, setIsDragging] = createSignal(false)
const [ignoredAtPositions, setIgnoredAtPositions] = createSignal<Set<number>>(new Set()) const [ignoredAtPositions, setIgnoredAtPositions] = createSignal<Set<number>>(new Set())
const [pasteCount, setPasteCount] = createSignal(0) const [pasteCount, setPasteCount] = createSignal(0)
const [imageCount, setImageCount] = createSignal(0)
let textareaRef: HTMLTextAreaElement | undefined let textareaRef: HTMLTextAreaElement | undefined
let containerRef: HTMLDivElement | undefined let containerRef: HTMLDivElement | undefined
@@ -74,7 +75,44 @@ export default function PromptInput(props: PromptInputProps) {
} }
} }
function handlePaste(e: ClipboardEvent) { async function handlePaste(e: ClipboardEvent) {
const items = e.clipboardData?.items
if (!items) return
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.type.startsWith("image/")) {
e.preventDefault()
const blob = item.getAsFile()
if (!blob) continue
const count = imageCount() + 1
setImageCount(count)
const reader = new FileReader()
reader.onload = () => {
const base64Data = (reader.result as string).split(",")[1]
const display = `[Image #${count}]`
const filename = `image-${count}.png`
const attachment = createFileAttachment(
filename,
filename,
"image/png",
new TextEncoder().encode(base64Data),
props.instanceFolder,
)
attachment.url = `data:image/png;base64,${base64Data}`
addAttachment(props.instanceId, props.sessionId, attachment)
}
reader.readAsDataURL(blob)
return
}
}
const pastedText = e.clipboardData?.getData("text/plain") const pastedText = e.clipboardData?.getData("text/plain")
if (!pastedText) return if (!pastedText) return
@@ -307,6 +345,7 @@ export default function PromptInput(props: PromptInputProps) {
clearAttachments(props.instanceId, props.sessionId) clearAttachments(props.instanceId, props.sessionId)
setIgnoredAtPositions(new Set<number>()) setIgnoredAtPositions(new Set<number>())
setPasteCount(0) setPasteCount(0)
setImageCount(0)
if (textareaRef) { if (textareaRef) {
textareaRef.style.height = "auto" textareaRef.style.height = "auto"
@@ -537,8 +576,13 @@ export default function PromptInput(props: PromptInputProps) {
<Show when={attachments().length > 0}> <Show when={attachments().length > 0}>
<div class="flex flex-wrap gap-1.5 border-b border-gray-200 pb-2 dark:border-gray-700"> <div class="flex flex-wrap gap-1.5 border-b border-gray-200 pb-2 dark:border-gray-700">
<For each={attachments()}> <For each={attachments()}>
{(attachment) => ( {(attachment) => {
const isImage = attachment.mediaType.startsWith("image/")
return (
<div class="inline-flex items-center gap-1.5 rounded-md bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-500/10 dark:text-blue-400 dark:ring-blue-500/20"> <div class="inline-flex items-center gap-1.5 rounded-md bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-500/10 dark:text-blue-400 dark:ring-blue-500/20">
<Show
when={isImage}
fallback={
<Show <Show
when={attachment.source.type === "text"} when={attachment.source.type === "text"}
fallback={ fallback={
@@ -575,6 +619,10 @@ export default function PromptInput(props: PromptInputProps) {
/> />
</svg> </svg>
</Show> </Show>
}
>
<img src={attachment.url} alt={attachment.filename} class="h-5 w-5 rounded object-cover" />
</Show>
<span>{attachment.source.type === "text" ? attachment.display : attachment.filename}</span> <span>{attachment.source.type === "text" ? attachment.display : attachment.filename}</span>
<button <button
onClick={() => handleRemoveAttachment(attachment.id)} onClick={() => handleRemoveAttachment(attachment.id)}
@@ -591,7 +639,8 @@ export default function PromptInput(props: PromptInputProps) {
</svg> </svg>
</button> </button>
</div> </div>
)} )
}}
</For> </For>
</div> </div>
</Show> </Show>