import { onCleanup, onMount } from "solid-js" import { showToast } from "@opencode-ai/ui/toast" import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" import { useLanguage } from "@/context/language" import { getCursorPosition } from "./editor-dom" export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"] type PromptAttachmentsInput = { editor: () => HTMLDivElement | undefined isFocused: () => boolean isDialogActive: () => boolean setDraggingType: (type: "image" | "@mention" | null) => void focusEditor: () => void addPart: (part: ContentPart) => void readClipboardImage?: () => Promise } export function createPromptAttachments(input: PromptAttachmentsInput) { const prompt = usePrompt() const language = useLanguage() const addImageAttachment = async (file: File) => { if (!ACCEPTED_FILE_TYPES.includes(file.type)) return const reader = new FileReader() reader.onload = () => { const editor = input.editor() if (!editor) return const dataUrl = reader.result as string const attachment: ImageAttachmentPart = { type: "image", id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2), filename: file.name, mime: file.type, dataUrl, } const cursorPosition = prompt.cursor() ?? getCursorPosition(editor) prompt.set([...prompt.current(), attachment], cursorPosition) } reader.readAsDataURL(file) } const removeImageAttachment = (id: string) => { const current = prompt.current() const next = current.filter((part) => part.type !== "image" || part.id !== id) prompt.set(next, prompt.cursor()) } const handlePaste = async (event: ClipboardEvent) => { if (!input.isFocused()) return const clipboardData = event.clipboardData if (!clipboardData) return event.preventDefault() event.stopPropagation() const items = Array.from(clipboardData.items) const fileItems = items.filter((item) => item.kind === "file") const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) if (imageItems.length > 0) { for (const item of imageItems) { const file = item.getAsFile() if (file) await addImageAttachment(file) } return } if (fileItems.length > 0) { showToast({ title: language.t("prompt.toast.pasteUnsupported.title"), description: language.t("prompt.toast.pasteUnsupported.description"), }) return } const plainText = clipboardData.getData("text/plain") ?? "" // Desktop: Browser clipboard has no images and no text, try platform's native clipboard for images if (input.readClipboardImage && !plainText) { const file = await input.readClipboardImage() if (file) { await addImageAttachment(file) return } } if (!plainText) return input.addPart({ type: "text", content: plainText, start: 0, end: 0 }) } const handleGlobalDragOver = (event: DragEvent) => { if (input.isDialogActive()) return event.preventDefault() const hasFiles = event.dataTransfer?.types.includes("Files") const hasText = event.dataTransfer?.types.includes("text/plain") if (hasFiles) { input.setDraggingType("image") } else if (hasText) { input.setDraggingType("@mention") } } const handleGlobalDragLeave = (event: DragEvent) => { if (input.isDialogActive()) return if (!event.relatedTarget) { input.setDraggingType(null) } } const handleGlobalDrop = async (event: DragEvent) => { if (input.isDialogActive()) return event.preventDefault() input.setDraggingType(null) const plainText = event.dataTransfer?.getData("text/plain") const filePrefix = "file:" if (plainText?.startsWith(filePrefix)) { const filePath = plainText.slice(filePrefix.length) input.focusEditor() input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 }) return } const dropped = event.dataTransfer?.files if (!dropped) return for (const file of Array.from(dropped)) { if (ACCEPTED_FILE_TYPES.includes(file.type)) { await addImageAttachment(file) } } } onMount(() => { document.addEventListener("dragover", handleGlobalDragOver) document.addEventListener("dragleave", handleGlobalDragLeave) document.addEventListener("drop", handleGlobalDrop) }) onCleanup(() => { document.removeEventListener("dragover", handleGlobalDragOver) document.removeEventListener("dragleave", handleGlobalDragLeave) document.removeEventListener("drop", handleGlobalDrop) }) return { addImageAttachment, removeImageAttachment, handlePaste, } }