Buckets:
| import { onMount } from "solid-js" | |
| import { makeEventListener } from "@solid-primitives/event-listener" | |
| import { showToast } from "@/utils/toast" | |
| import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" | |
| import { useLanguage } from "@/context/language" | |
| import { uuid } from "@/utils/uuid" | |
| import { getCursorPosition } from "./editor-dom" | |
| import { attachmentMime } from "./files" | |
| import { normalizePaste, pasteMode } from "./paste" | |
| function dataUrl(file: File, mime: string) { | |
| return new Promise<string>((resolve) => { | |
| const reader = new FileReader() | |
| reader.addEventListener("error", () => resolve("")) | |
| reader.addEventListener("load", () => { | |
| const value = typeof reader.result === "string" ? reader.result : "" | |
| const idx = value.indexOf(",") | |
| if (idx === -1) { | |
| resolve(value) | |
| return | |
| } | |
| resolve(`data:${mime};base64,${value.slice(idx + 1)}`) | |
| }) | |
| reader.readAsDataURL(file) | |
| }) | |
| } | |
| type PromptAttachmentsInput = { | |
| editor: () => HTMLDivElement | undefined | |
| isDialogActive: () => boolean | |
| setDraggingType: (type: "image" | "@mention" | null) => void | |
| focusEditor: () => void | |
| addPart: (part: ContentPart) => boolean | |
| readClipboardImage?: () => Promise<File | null> | |
| } | |
| export function createPromptAttachments(input: PromptAttachmentsInput) { | |
| const prompt = usePrompt() | |
| const language = useLanguage() | |
| const warn = () => { | |
| showToast({ | |
| title: language.t("prompt.toast.pasteUnsupported.title"), | |
| description: language.t("prompt.toast.pasteUnsupported.description"), | |
| }) | |
| } | |
| const add = async (file: File, toast = true) => { | |
| const mime = await attachmentMime(file) | |
| if (!mime) { | |
| if (toast) warn() | |
| return false | |
| } | |
| const editor = input.editor() | |
| if (!editor) return false | |
| const url = await dataUrl(file, mime) | |
| if (!url) return false | |
| const attachment: ImageAttachmentPart = { | |
| type: "image", | |
| id: uuid(), | |
| filename: file.name, | |
| mime, | |
| dataUrl: url, | |
| } | |
| const cursor = prompt.cursor() ?? getCursorPosition(editor) | |
| prompt.set([...prompt.current(), attachment], cursor) | |
| return true | |
| } | |
| const addAttachment = (file: File) => add(file) | |
| const addAttachments = async (files: File[], toast = true) => { | |
| let found = false | |
| for (const file of files) { | |
| const ok = await add(file, false) | |
| if (ok) found = true | |
| } | |
| if (!found && files.length > 0 && toast) warn() | |
| return found | |
| } | |
| const removeAttachment = (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) => { | |
| const clipboardData = event.clipboardData | |
| if (!clipboardData) return | |
| event.preventDefault() | |
| event.stopPropagation() | |
| const files = Array.from(clipboardData.items).flatMap((item) => { | |
| if (item.kind !== "file") return [] | |
| const file = item.getAsFile() | |
| return file ? [file] : [] | |
| }) | |
| if (files.length > 0) { | |
| await addAttachments(files) | |
| 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 addAttachment(file) | |
| return | |
| } | |
| } | |
| if (!plainText) return | |
| const text = normalizePaste(plainText) | |
| const put = () => { | |
| if (input.addPart({ type: "text", content: text, start: 0, end: 0 })) return true | |
| input.focusEditor() | |
| return input.addPart({ type: "text", content: text, start: 0, end: 0 }) | |
| } | |
| if (pasteMode(text) === "manual") { | |
| put() | |
| return | |
| } | |
| const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text) | |
| if (inserted) return | |
| put() | |
| } | |
| 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 | |
| await addAttachments(Array.from(dropped)) | |
| } | |
| onMount(() => { | |
| makeEventListener(document, "dragover", handleGlobalDragOver) | |
| makeEventListener(document, "dragleave", handleGlobalDragLeave) | |
| makeEventListener(document, "drop", handleGlobalDrop) | |
| }) | |
| return { | |
| addAttachment, | |
| addAttachments, | |
| removeAttachment, | |
| handlePaste, | |
| } | |
| } | |
Xet Storage Details
- Size:
- 5.55 kB
- Xet hash:
- 4cbb8086021cfa828f7db8b5b1cb9933dbf0ea3578cd27a20cf5739300a96024
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.