157 lines
4.8 KiB
TypeScript
157 lines
4.8 KiB
TypeScript
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<File | null>
|
|
}
|
|
|
|
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(),
|
|
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,
|
|
}
|
|
}
|