mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 05:43:55 +00:00
fix(app): support text attachments (#17335)
This commit is contained in:
parent
05cb3c87ca
commit
843f188aaa
@ -38,7 +38,8 @@ import { usePlatform } from "@/context/platform"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||
import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
|
||||
import { createPromptAttachments } from "./prompt-input/attachments"
|
||||
import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
|
||||
import {
|
||||
canNavigateHistoryAtCursor,
|
||||
navigatePromptHistory,
|
||||
@ -1007,7 +1008,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const { addImageAttachment, removeImageAttachment, handlePaste } = createPromptAttachments({
|
||||
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
|
||||
editor: () => editorRef,
|
||||
isFocused,
|
||||
isDialogActive: () => !!dialog.active,
|
||||
@ -1247,7 +1248,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onOpen={(attachment) =>
|
||||
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
|
||||
}
|
||||
onRemove={removeImageAttachment}
|
||||
onRemove={removeAttachment}
|
||||
removeLabel={language.t("prompt.attachment.remove")}
|
||||
/>
|
||||
<div
|
||||
@ -1311,7 +1312,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
class="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.currentTarget.files?.[0]
|
||||
if (file) addImageAttachment(file)
|
||||
if (file) void addAttachment(file)
|
||||
e.currentTarget.value = ""
|
||||
}}
|
||||
/>
|
||||
|
||||
24
packages/app/src/components/prompt-input/attachments.test.ts
Normal file
24
packages/app/src/components/prompt-input/attachments.test.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { attachmentMime } from "./files"
|
||||
|
||||
describe("attachmentMime", () => {
|
||||
test("keeps PDFs when the browser reports the mime", async () => {
|
||||
const file = new File(["%PDF-1.7"], "guide.pdf", { type: "application/pdf" })
|
||||
expect(await attachmentMime(file)).toBe("application/pdf")
|
||||
})
|
||||
|
||||
test("normalizes structured text types to text/plain", async () => {
|
||||
const file = new File(['{"ok":true}\n'], "data.json", { type: "application/json" })
|
||||
expect(await attachmentMime(file)).toBe("text/plain")
|
||||
})
|
||||
|
||||
test("accepts text files even with a misleading browser mime", async () => {
|
||||
const file = new File(["export const x = 1\n"], "main.ts", { type: "video/mp2t" })
|
||||
expect(await attachmentMime(file)).toBe("text/plain")
|
||||
})
|
||||
|
||||
test("rejects binary files", async () => {
|
||||
const file = new File([Uint8Array.of(0, 255, 1, 2)], "blob.bin", { type: "application/octet-stream" })
|
||||
expect(await attachmentMime(file)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@ -4,12 +4,27 @@ import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { uuid } from "@/utils/uuid"
|
||||
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"]
|
||||
import { attachmentMime } from "./files"
|
||||
const LARGE_PASTE_CHARS = 8000
|
||||
const LARGE_PASTE_BREAKS = 120
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
function largePaste(text: string) {
|
||||
if (text.length >= LARGE_PASTE_CHARS) return true
|
||||
let breaks = 0
|
||||
@ -35,28 +50,41 @@ 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: uuid(),
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
dataUrl,
|
||||
}
|
||||
const cursorPosition = prompt.cursor() ?? getCursorPosition(editor)
|
||||
prompt.set([...prompt.current(), attachment], cursorPosition)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
const warn = () => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.pasteUnsupported.title"),
|
||||
description: language.t("prompt.toast.pasteUnsupported.description"),
|
||||
})
|
||||
}
|
||||
|
||||
const removeImageAttachment = (id: string) => {
|
||||
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 removeAttachment = (id: string) => {
|
||||
const current = prompt.current()
|
||||
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
||||
prompt.set(next, prompt.cursor())
|
||||
@ -72,21 +100,16 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
|
||||
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"),
|
||||
})
|
||||
let found = false
|
||||
for (const item of fileItems) {
|
||||
const file = item.getAsFile()
|
||||
if (!file) continue
|
||||
const ok = await add(file, false)
|
||||
if (ok) found = true
|
||||
}
|
||||
if (!found) warn()
|
||||
return
|
||||
}
|
||||
|
||||
@ -96,7 +119,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
if (input.readClipboardImage && !plainText) {
|
||||
const file = await input.readClipboardImage()
|
||||
if (file) {
|
||||
await addImageAttachment(file)
|
||||
await addAttachment(file)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -153,11 +176,12 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
const dropped = event.dataTransfer?.files
|
||||
if (!dropped) return
|
||||
|
||||
let found = false
|
||||
for (const file of Array.from(dropped)) {
|
||||
if (ACCEPTED_FILE_TYPES.includes(file.type)) {
|
||||
await addImageAttachment(file)
|
||||
}
|
||||
const ok = await add(file, false)
|
||||
if (ok) found = true
|
||||
}
|
||||
if (!found && dropped.length > 0) warn()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@ -173,8 +197,8 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
})
|
||||
|
||||
return {
|
||||
addImageAttachment,
|
||||
removeImageAttachment,
|
||||
addAttachment,
|
||||
removeAttachment,
|
||||
handlePaste,
|
||||
}
|
||||
}
|
||||
|
||||
119
packages/app/src/components/prompt-input/files.ts
Normal file
119
packages/app/src/components/prompt-input/files.ts
Normal file
@ -0,0 +1,119 @@
|
||||
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||
|
||||
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
|
||||
const IMAGE_EXTS = new Map([
|
||||
["gif", "image/gif"],
|
||||
["jpeg", "image/jpeg"],
|
||||
["jpg", "image/jpeg"],
|
||||
["png", "image/png"],
|
||||
["webp", "image/webp"],
|
||||
])
|
||||
const TEXT_MIMES = new Set([
|
||||
"application/json",
|
||||
"application/ld+json",
|
||||
"application/toml",
|
||||
"application/x-toml",
|
||||
"application/x-yaml",
|
||||
"application/xml",
|
||||
"application/yaml",
|
||||
])
|
||||
|
||||
export const ACCEPTED_FILE_TYPES = [
|
||||
...ACCEPTED_IMAGE_TYPES,
|
||||
"application/pdf",
|
||||
"text/*",
|
||||
"application/json",
|
||||
"application/ld+json",
|
||||
"application/toml",
|
||||
"application/x-toml",
|
||||
"application/x-yaml",
|
||||
"application/xml",
|
||||
"application/yaml",
|
||||
".c",
|
||||
".cc",
|
||||
".cjs",
|
||||
".conf",
|
||||
".cpp",
|
||||
".css",
|
||||
".csv",
|
||||
".cts",
|
||||
".env",
|
||||
".go",
|
||||
".gql",
|
||||
".graphql",
|
||||
".h",
|
||||
".hh",
|
||||
".hpp",
|
||||
".htm",
|
||||
".html",
|
||||
".ini",
|
||||
".java",
|
||||
".js",
|
||||
".json",
|
||||
".jsx",
|
||||
".log",
|
||||
".md",
|
||||
".mdx",
|
||||
".mjs",
|
||||
".mts",
|
||||
".py",
|
||||
".rb",
|
||||
".rs",
|
||||
".sass",
|
||||
".scss",
|
||||
".sh",
|
||||
".sql",
|
||||
".toml",
|
||||
".ts",
|
||||
".tsx",
|
||||
".txt",
|
||||
".xml",
|
||||
".yaml",
|
||||
".yml",
|
||||
".zsh",
|
||||
]
|
||||
|
||||
const SAMPLE = 4096
|
||||
|
||||
function kind(type: string) {
|
||||
return type.split(";", 1)[0]?.trim().toLowerCase() ?? ""
|
||||
}
|
||||
|
||||
function ext(name: string) {
|
||||
const idx = name.lastIndexOf(".")
|
||||
if (idx === -1) return ""
|
||||
return name.slice(idx + 1).toLowerCase()
|
||||
}
|
||||
|
||||
function textMime(type: string) {
|
||||
if (!type) return false
|
||||
if (type.startsWith("text/")) return true
|
||||
if (TEXT_MIMES.has(type)) return true
|
||||
if (type.endsWith("+json")) return true
|
||||
return type.endsWith("+xml")
|
||||
}
|
||||
|
||||
function textBytes(bytes: Uint8Array) {
|
||||
if (bytes.length === 0) return true
|
||||
let count = 0
|
||||
for (const byte of bytes) {
|
||||
if (byte === 0) return false
|
||||
if (byte < 9 || (byte > 13 && byte < 32)) count += 1
|
||||
}
|
||||
return count / bytes.length <= 0.3
|
||||
}
|
||||
|
||||
export async function attachmentMime(file: File) {
|
||||
const type = kind(file.type)
|
||||
if (IMAGE_MIMES.has(type)) return type
|
||||
if (type === "application/pdf") return type
|
||||
|
||||
const suffix = ext(file.name)
|
||||
const fallback = IMAGE_EXTS.get(suffix) ?? (suffix === "pdf" ? "application/pdf" : undefined)
|
||||
if ((!type || type === "application/octet-stream") && fallback) return fallback
|
||||
|
||||
if (textMime(type)) return "text/plain"
|
||||
const bytes = new Uint8Array(await file.slice(0, SAMPLE).arrayBuffer())
|
||||
if (!textBytes(bytes)) return
|
||||
return "text/plain"
|
||||
}
|
||||
@ -244,7 +244,7 @@ export const dict = {
|
||||
"prompt.example.25": "كيف تعمل متغيرات البيئة هنا؟",
|
||||
"prompt.popover.emptyResults": "لا توجد نتائج مطابقة",
|
||||
"prompt.popover.emptyCommands": "لا توجد أوامر مطابقة",
|
||||
"prompt.dropzone.label": "أفلت الصور أو ملفات PDF هنا",
|
||||
"prompt.dropzone.label": "أفلت الصور أو ملفات PDF أو الملفات النصية هنا",
|
||||
"prompt.dropzone.file.label": "أفلت لإشارة @ للملف",
|
||||
"prompt.slash.badge.custom": "مخصص",
|
||||
"prompt.slash.badge.skill": "مهارة",
|
||||
@ -257,8 +257,8 @@ export const dict = {
|
||||
"prompt.attachment.remove": "إزالة المرفق",
|
||||
"prompt.action.send": "إرسال",
|
||||
"prompt.action.stop": "توقف",
|
||||
"prompt.toast.pasteUnsupported.title": "لصق غير مدعوم",
|
||||
"prompt.toast.pasteUnsupported.description": "يمكن لصق الصور أو ملفات PDF فقط هنا.",
|
||||
"prompt.toast.pasteUnsupported.title": "مرفق غير مدعوم",
|
||||
"prompt.toast.pasteUnsupported.description": "يمكن إرفاق الصور أو ملفات PDF أو الملفات النصية فقط هنا.",
|
||||
"prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً",
|
||||
"prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "فشل إنشاء شجرة العمل",
|
||||
|
||||
@ -244,7 +244,7 @@ export const dict = {
|
||||
"prompt.example.25": "Como funcionam as variáveis de ambiente aqui?",
|
||||
"prompt.popover.emptyResults": "Nenhum resultado correspondente",
|
||||
"prompt.popover.emptyCommands": "Nenhum comando correspondente",
|
||||
"prompt.dropzone.label": "Solte imagens ou PDFs aqui",
|
||||
"prompt.dropzone.label": "Arraste imagens, PDFs ou arquivos de texto aqui",
|
||||
"prompt.dropzone.file.label": "Solte para @mencionar arquivo",
|
||||
"prompt.slash.badge.custom": "personalizado",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
@ -257,8 +257,8 @@ export const dict = {
|
||||
"prompt.attachment.remove": "Remover anexo",
|
||||
"prompt.action.send": "Enviar",
|
||||
"prompt.action.stop": "Parar",
|
||||
"prompt.toast.pasteUnsupported.title": "Colagem não suportada",
|
||||
"prompt.toast.pasteUnsupported.description": "Somente imagens ou PDFs podem ser colados aqui.",
|
||||
"prompt.toast.pasteUnsupported.title": "Anexo não suportado",
|
||||
"prompt.toast.pasteUnsupported.description": "Apenas imagens, PDFs ou arquivos de texto podem ser anexados aqui.",
|
||||
"prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo",
|
||||
"prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Falha ao criar worktree",
|
||||
|
||||
@ -264,7 +264,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "Nema rezultata",
|
||||
"prompt.popover.emptyCommands": "Nema komandi",
|
||||
"prompt.dropzone.label": "Spusti slike ili PDF-ove ovdje",
|
||||
"prompt.dropzone.label": "Ovdje prevucite slike, PDF-ove ili tekstualne datoteke",
|
||||
"prompt.dropzone.file.label": "Spusti za @spominjanje datoteke",
|
||||
"prompt.slash.badge.custom": "prilagođeno",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
@ -278,8 +278,8 @@ export const dict = {
|
||||
"prompt.action.send": "Pošalji",
|
||||
"prompt.action.stop": "Zaustavi",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Nepodržano lijepljenje",
|
||||
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu zalijepiti samo slike ili PDF-ovi.",
|
||||
"prompt.toast.pasteUnsupported.title": "Nepodržan prilog",
|
||||
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.",
|
||||
"prompt.toast.modelAgentRequired.title": "Odaberi agenta i model",
|
||||
"prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Neuspješno kreiranje worktree-a",
|
||||
|
||||
@ -262,7 +262,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "Ingen matchende resultater",
|
||||
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
|
||||
"prompt.dropzone.label": "Slip billeder eller PDF'er her",
|
||||
"prompt.dropzone.label": "Slip billeder, PDF'er eller tekstfiler her",
|
||||
"prompt.dropzone.file.label": "Slip for at @nævne fil",
|
||||
"prompt.slash.badge.custom": "brugerdefineret",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
@ -276,8 +276,8 @@ export const dict = {
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stop",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Ikke understøttet indsæt",
|
||||
"prompt.toast.pasteUnsupported.description": "Kun billeder eller PDF'er kan indsættes her.",
|
||||
"prompt.toast.pasteUnsupported.title": "Ikke understøttet vedhæftning",
|
||||
"prompt.toast.pasteUnsupported.description": "Kun billeder, PDF'er eller tekstfiler kan vedhæftes her.",
|
||||
"prompt.toast.modelAgentRequired.title": "Vælg en agent og model",
|
||||
"prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke oprette worktree",
|
||||
|
||||
@ -249,7 +249,7 @@ export const dict = {
|
||||
"prompt.example.25": "Wie funktionieren Umgebungsvariablen hier?",
|
||||
"prompt.popover.emptyResults": "Keine passenden Ergebnisse",
|
||||
"prompt.popover.emptyCommands": "Keine passenden Befehle",
|
||||
"prompt.dropzone.label": "Bilder oder PDFs hier ablegen",
|
||||
"prompt.dropzone.label": "Bilder, PDFs oder Textdateien hier ablegen",
|
||||
"prompt.dropzone.file.label": "Ablegen zum @Erwähnen der Datei",
|
||||
"prompt.slash.badge.custom": "benutzerdefiniert",
|
||||
"prompt.slash.badge.skill": "Skill",
|
||||
@ -262,8 +262,8 @@ export const dict = {
|
||||
"prompt.attachment.remove": "Anhang entfernen",
|
||||
"prompt.action.send": "Senden",
|
||||
"prompt.action.stop": "Stopp",
|
||||
"prompt.toast.pasteUnsupported.title": "Nicht unterstütztes Einfügen",
|
||||
"prompt.toast.pasteUnsupported.description": "Hier können nur Bilder oder PDFs eingefügt werden.",
|
||||
"prompt.toast.pasteUnsupported.title": "Nicht unterstützter Anhang",
|
||||
"prompt.toast.pasteUnsupported.description": "Hier können nur Bilder, PDFs oder Textdateien angehängt werden.",
|
||||
"prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell",
|
||||
"prompt.toast.modelAgentRequired.description":
|
||||
"Wählen Sie einen Agenten und ein Modell, bevor Sie eine Eingabe senden.",
|
||||
|
||||
@ -264,7 +264,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "No matching results",
|
||||
"prompt.popover.emptyCommands": "No matching commands",
|
||||
"prompt.dropzone.label": "Drop images or PDFs here",
|
||||
"prompt.dropzone.label": "Drop images, PDFs, or text files here",
|
||||
"prompt.dropzone.file.label": "Drop to @mention file",
|
||||
"prompt.slash.badge.custom": "custom",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
@ -278,8 +278,8 @@ export const dict = {
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stop",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Unsupported paste",
|
||||
"prompt.toast.pasteUnsupported.description": "Only images or PDFs can be pasted here.",
|
||||
"prompt.toast.pasteUnsupported.title": "Unsupported attachment",
|
||||
"prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.",
|
||||
"prompt.toast.modelAgentRequired.title": "Select an agent and model",
|
||||
"prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Failed to create worktree",
|
||||
|
||||
@ -263,7 +263,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "Sin resultados coincidentes",
|
||||
"prompt.popover.emptyCommands": "Sin comandos coincidentes",
|
||||
"prompt.dropzone.label": "Suelta imágenes o PDFs aquí",
|
||||
"prompt.dropzone.label": "Suelta imágenes, PDFs o archivos de texto aquí",
|
||||
"prompt.dropzone.file.label": "Suelta para @mencionar archivo",
|
||||
"prompt.slash.badge.custom": "personalizado",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
@ -277,8 +277,8 @@ export const dict = {
|
||||
"prompt.action.send": "Enviar",
|
||||
"prompt.action.stop": "Detener",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Pegado no soportado",
|
||||
"prompt.toast.pasteUnsupported.description": "Solo se pueden pegar imágenes o PDFs aquí.",
|
||||
"prompt.toast.pasteUnsupported.title": "Adjunto no compatible",
|
||||
"prompt.toast.pasteUnsupported.description": "Solo se pueden adjuntar imágenes, PDFs o archivos de texto aquí.",
|
||||
"prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo",
|
||||
"prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Fallo al crear el árbol de trabajo",
|
||||
|
||||
@ -244,7 +244,7 @@ export const dict = {
|
||||
"prompt.example.25": "Comment fonctionnent les variables d'environnement ici ?",
|
||||
"prompt.popover.emptyResults": "Aucun résultat correspondant",
|
||||
"prompt.popover.emptyCommands": "Aucune commande correspondante",
|
||||
"prompt.dropzone.label": "Déposez des images ou des PDF ici",
|
||||
"prompt.dropzone.label": "Déposez des images, des PDF ou des fichiers texte ici",
|
||||
"prompt.dropzone.file.label": "Déposez pour @mentionner le fichier",
|
||||
"prompt.slash.badge.custom": "personnalisé",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
@ -257,8 +257,9 @@ export const dict = {
|
||||
"prompt.attachment.remove": "Supprimer la pièce jointe",
|
||||
"prompt.action.send": "Envoyer",
|
||||
"prompt.action.stop": "Arrêter",
|
||||
"prompt.toast.pasteUnsupported.title": "Collage non supporté",
|
||||
"prompt.toast.pasteUnsupported.description": "Seules les images ou les PDF peuvent être collés ici.",
|
||||
"prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge",
|
||||
"prompt.toast.pasteUnsupported.description":
|
||||
"Seules les images, les PDF ou les fichiers texte peuvent être joints ici.",
|
||||
"prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle",
|
||||
"prompt.toast.modelAgentRequired.description": "Choisissez un agent et un modèle avant d'envoyer un message.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Échec de la création de l'arbre de travail",
|
||||
|
||||
@ -243,7 +243,7 @@ export const dict = {
|
||||
"prompt.example.25": "ここでは環境変数はどう機能しますか?",
|
||||
"prompt.popover.emptyResults": "一致する結果がありません",
|
||||
"prompt.popover.emptyCommands": "一致するコマンドがありません",
|
||||
"prompt.dropzone.label": "画像またはPDFをここにドロップ",
|
||||
"prompt.dropzone.label": "画像、PDF、またはテキストファイルをここにドロップしてください",
|
||||
"prompt.dropzone.file.label": "ドロップして@メンションファイルを追加",
|
||||
"prompt.slash.badge.custom": "カスタム",
|
||||
"prompt.slash.badge.skill": "スキル",
|
||||
@ -256,8 +256,8 @@ export const dict = {
|
||||
"prompt.attachment.remove": "添付ファイルを削除",
|
||||
"prompt.action.send": "送信",
|
||||
"prompt.action.stop": "停止",
|
||||
"prompt.toast.pasteUnsupported.title": "サポートされていない貼り付け",
|
||||
"prompt.toast.pasteUnsupported.description": "ここでは画像またはPDFのみ貼り付け可能です。",
|
||||
"prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル",
|
||||
"prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。",
|
||||
"prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択",
|
||||
"prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。",
|
||||
"prompt.toast.worktreeCreateFailed.title": "ワークツリーの作成に失敗しました",
|
||||
|
||||
@ -247,7 +247,7 @@ export const dict = {
|
||||
"prompt.example.25": "여기서 환경 변수는 어떻게 작동하나요?",
|
||||
"prompt.popover.emptyResults": "일치하는 결과 없음",
|
||||
"prompt.popover.emptyCommands": "일치하는 명령어 없음",
|
||||
"prompt.dropzone.label": "이미지나 PDF를 여기에 드롭하세요",
|
||||
"prompt.dropzone.label": "이미지, PDF 또는 텍스트 파일을 이곳에 드롭하세요",
|
||||
"prompt.dropzone.file.label": "드롭하여 파일 @멘션 추가",
|
||||
"prompt.slash.badge.custom": "사용자 지정",
|
||||
"prompt.slash.badge.skill": "스킬",
|
||||
@ -260,8 +260,8 @@ export const dict = {
|
||||
"prompt.attachment.remove": "첨부 파일 제거",
|
||||
"prompt.action.send": "전송",
|
||||
"prompt.action.stop": "중지",
|
||||
"prompt.toast.pasteUnsupported.title": "지원되지 않는 붙여넣기",
|
||||
"prompt.toast.pasteUnsupported.description": "이미지나 PDF만 붙여넣을 수 있습니다.",
|
||||
"prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일",
|
||||
"prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.",
|
||||
"prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택",
|
||||
"prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "작업 트리 생성 실패",
|
||||
|
||||
@ -266,7 +266,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "Ingen matchende resultater",
|
||||
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
|
||||
"prompt.dropzone.label": "Slipp bilder eller PDF-er her",
|
||||
"prompt.dropzone.label": "Slipp bilder, PDF-er eller tekstfiler her",
|
||||
"prompt.dropzone.file.label": "Slipp for å @nevne fil",
|
||||
"prompt.slash.badge.custom": "egendefinert",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
@ -280,8 +280,8 @@ export const dict = {
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stopp",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Liming ikke støttet",
|
||||
"prompt.toast.pasteUnsupported.description": "Kun bilder eller PDF-er kan limes inn her.",
|
||||
"prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg",
|
||||
"prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.",
|
||||
"prompt.toast.modelAgentRequired.title": "Velg en agent og modell",
|
||||
"prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke opprette worktree",
|
||||
|
||||
@ -245,7 +245,7 @@ export const dict = {
|
||||
"prompt.example.25": "Jak działają tutaj zmienne środowiskowe?",
|
||||
"prompt.popover.emptyResults": "Brak pasujących wyników",
|
||||
"prompt.popover.emptyCommands": "Brak pasujących poleceń",
|
||||
"prompt.dropzone.label": "Upuść obrazy lub pliki PDF tutaj",
|
||||
"prompt.dropzone.label": "Upuść tutaj obrazy, pliki PDF lub pliki tekstowe",
|
||||
"prompt.dropzone.file.label": "Upuść, aby @wspomnieć plik",
|
||||
"prompt.slash.badge.custom": "własne",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
@ -258,8 +258,8 @@ export const dict = {
|
||||
"prompt.attachment.remove": "Usuń załącznik",
|
||||
"prompt.action.send": "Wyślij",
|
||||
"prompt.action.stop": "Zatrzymaj",
|
||||
"prompt.toast.pasteUnsupported.title": "Nieobsługiwane wklejanie",
|
||||
"prompt.toast.pasteUnsupported.description": "Tylko obrazy lub pliki PDF mogą być tutaj wklejane.",
|
||||
"prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik",
|
||||
"prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.",
|
||||
"prompt.toast.modelAgentRequired.title": "Wybierz agenta i model",
|
||||
"prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Nie udało się utworzyć drzewa roboczego",
|
||||
|
||||
@ -263,7 +263,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "Нет совпадений",
|
||||
"prompt.popover.emptyCommands": "Нет совпадающих команд",
|
||||
"prompt.dropzone.label": "Перетащите изображения или PDF сюда",
|
||||
"prompt.dropzone.label": "Перетащите сюда изображения, PDF или текстовые файлы",
|
||||
"prompt.dropzone.file.label": "Отпустите для @упоминания файла",
|
||||
"prompt.slash.badge.custom": "своё",
|
||||
"prompt.slash.badge.skill": "навык",
|
||||
@ -277,8 +277,8 @@ export const dict = {
|
||||
"prompt.action.send": "Отправить",
|
||||
"prompt.action.stop": "Остановить",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Неподдерживаемая вставка",
|
||||
"prompt.toast.pasteUnsupported.description": "Сюда можно вставлять только изображения или PDF.",
|
||||
"prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение",
|
||||
"prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.",
|
||||
"prompt.toast.modelAgentRequired.title": "Выберите агента и модель",
|
||||
"prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Не удалось создать worktree",
|
||||
|
||||
@ -263,7 +263,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน",
|
||||
"prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน",
|
||||
"prompt.dropzone.label": "วางรูปภาพหรือ PDF ที่นี่",
|
||||
"prompt.dropzone.label": "ลากรูปภาพ, PDF หรือไฟล์ข้อความมาวางที่นี่",
|
||||
"prompt.dropzone.file.label": "วางเพื่อ @กล่าวถึงไฟล์",
|
||||
"prompt.slash.badge.custom": "กำหนดเอง",
|
||||
"prompt.slash.badge.skill": "ทักษะ",
|
||||
@ -277,8 +277,8 @@ export const dict = {
|
||||
"prompt.action.send": "ส่ง",
|
||||
"prompt.action.stop": "หยุด",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "การวางไม่รองรับ",
|
||||
"prompt.toast.pasteUnsupported.description": "สามารถวางรูปภาพหรือ PDF เท่านั้น",
|
||||
"prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ",
|
||||
"prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ, PDF หรือไฟล์ข้อความเท่านั้น",
|
||||
"prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล",
|
||||
"prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์",
|
||||
"prompt.toast.worktreeCreateFailed.title": "ไม่สามารถสร้าง worktree",
|
||||
|
||||
@ -268,7 +268,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "Eşleşen sonuç yok",
|
||||
"prompt.popover.emptyCommands": "Eşleşen komut yok",
|
||||
"prompt.dropzone.label": "Görsel veya PDF'leri buraya bırakın",
|
||||
"prompt.dropzone.label": "Resimleri, PDF'leri veya metin dosyalarını buraya bırakın",
|
||||
"prompt.dropzone.file.label": "@bahsetmek için dosyayı bırakın",
|
||||
"prompt.slash.badge.custom": "özel",
|
||||
"prompt.slash.badge.skill": "beceri",
|
||||
@ -282,8 +282,8 @@ export const dict = {
|
||||
"prompt.action.send": "Gönder",
|
||||
"prompt.action.stop": "Durdur",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Desteklenmeyen yapıştırma",
|
||||
"prompt.toast.pasteUnsupported.description": "Buraya sadece görsel veya PDF yapıştırılabilir.",
|
||||
"prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek",
|
||||
"prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.",
|
||||
"prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin",
|
||||
"prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Çalışma ağacı oluşturulamadı",
|
||||
|
||||
@ -283,7 +283,7 @@ export const dict = {
|
||||
"prompt.example.25": "这里的环境变量是怎么工作的?",
|
||||
"prompt.popover.emptyResults": "没有匹配的结果",
|
||||
"prompt.popover.emptyCommands": "没有匹配的命令",
|
||||
"prompt.dropzone.label": "将图片或 PDF 拖到这里",
|
||||
"prompt.dropzone.label": "将图片、PDF 或文本文件拖放到此处",
|
||||
"prompt.dropzone.file.label": "拖放以 @提及文件",
|
||||
"prompt.slash.badge.custom": "自定义",
|
||||
"prompt.slash.badge.skill": "技能",
|
||||
@ -296,8 +296,8 @@ export const dict = {
|
||||
"prompt.attachment.remove": "移除附件",
|
||||
"prompt.action.send": "发送",
|
||||
"prompt.action.stop": "停止",
|
||||
"prompt.toast.pasteUnsupported.title": "不支持的粘贴",
|
||||
"prompt.toast.pasteUnsupported.description": "这里只能粘贴图片或 PDF 文件。",
|
||||
"prompt.toast.pasteUnsupported.title": "不支持的附件",
|
||||
"prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。",
|
||||
"prompt.toast.modelAgentRequired.title": "请选择智能体和模型",
|
||||
"prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。",
|
||||
"prompt.toast.worktreeCreateFailed.title": "创建工作树失败",
|
||||
|
||||
@ -263,7 +263,7 @@ export const dict = {
|
||||
|
||||
"prompt.popover.emptyResults": "沒有符合的結果",
|
||||
"prompt.popover.emptyCommands": "沒有符合的命令",
|
||||
"prompt.dropzone.label": "將圖片或 PDF 拖到這裡",
|
||||
"prompt.dropzone.label": "將圖片、PDF 或文字檔案拖放到此處",
|
||||
"prompt.dropzone.file.label": "拖放以 @提及檔案",
|
||||
"prompt.slash.badge.custom": "自訂",
|
||||
"prompt.slash.badge.skill": "技能",
|
||||
@ -277,8 +277,8 @@ export const dict = {
|
||||
"prompt.action.send": "傳送",
|
||||
"prompt.action.stop": "停止",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "不支援的貼上",
|
||||
"prompt.toast.pasteUnsupported.description": "這裡只能貼上圖片或 PDF 檔案。",
|
||||
"prompt.toast.pasteUnsupported.title": "不支援的附件",
|
||||
"prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。",
|
||||
"prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型",
|
||||
"prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。",
|
||||
"prompt.toast.worktreeCreateFailed.title": "建立工作樹失敗",
|
||||
|
||||
@ -47,6 +47,7 @@ import { LLM } from "./llm"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { Truncate } from "@/tool/truncation"
|
||||
import { decodeDataUrl } from "@/util/data-url"
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
@ -1079,7 +1080,7 @@ export namespace SessionPrompt {
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
synthetic: true,
|
||||
text: Buffer.from(part.url, "base64url").toString(),
|
||||
text: decodeDataUrl(part.url),
|
||||
},
|
||||
{
|
||||
...part,
|
||||
|
||||
9
packages/opencode/src/util/data-url.ts
Normal file
9
packages/opencode/src/util/data-url.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function decodeDataUrl(url: string) {
|
||||
const idx = url.indexOf(",")
|
||||
if (idx === -1) return ""
|
||||
|
||||
const head = url.slice(0, idx)
|
||||
const body = url.slice(idx + 1)
|
||||
if (head.includes(";base64")) return Buffer.from(body, "base64").toString("utf8")
|
||||
return decodeURIComponent(body)
|
||||
}
|
||||
14
packages/opencode/test/util/data-url.test.ts
Normal file
14
packages/opencode/test/util/data-url.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { decodeDataUrl } from "../../src/util/data-url"
|
||||
|
||||
describe("decodeDataUrl", () => {
|
||||
test("decodes base64 data URLs", () => {
|
||||
const body = '{\n "ok": true\n}\n'
|
||||
const url = `data:text/plain;base64,${Buffer.from(body).toString("base64")}`
|
||||
expect(decodeDataUrl(url)).toBe(body)
|
||||
})
|
||||
|
||||
test("decodes plain data URLs", () => {
|
||||
expect(decodeDataUrl("data:text/plain,hello%20world")).toBe("hello world")
|
||||
})
|
||||
})
|
||||
55
packages/ui/src/components/message-file.test.ts
Normal file
55
packages/ui/src/components/message-file.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { FilePart } from "@opencode-ai/sdk/v2"
|
||||
import { attached, inline, kind } from "./message-file"
|
||||
|
||||
function file(part: Partial<FilePart> = {}): FilePart {
|
||||
return {
|
||||
id: "part_1",
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_1",
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: "file:///repo/README.txt",
|
||||
filename: "README.txt",
|
||||
...part,
|
||||
}
|
||||
}
|
||||
|
||||
describe("message-file", () => {
|
||||
test("treats data URLs as attachments", () => {
|
||||
expect(attached(file({ url: "data:text/plain;base64,SGVsbG8=" }))).toBe(true)
|
||||
expect(attached(file())).toBe(false)
|
||||
})
|
||||
|
||||
test("treats only non-attachment source ranges as inline references", () => {
|
||||
expect(
|
||||
inline(
|
||||
file({
|
||||
source: {
|
||||
type: "file",
|
||||
path: "/repo/README.txt",
|
||||
text: { value: "@README.txt", start: 0, end: 11 },
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
inline(
|
||||
file({
|
||||
url: "data:text/plain;base64,SGVsbG8=",
|
||||
source: {
|
||||
type: "file",
|
||||
path: "/repo/README.txt",
|
||||
text: { value: "@README.txt", start: 0, end: 11 },
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test("separates image and file attachment kinds", () => {
|
||||
expect(kind(file({ mime: "image/png" }))).toBe("image")
|
||||
expect(kind(file({ mime: "application/pdf" }))).toBe("file")
|
||||
})
|
||||
})
|
||||
14
packages/ui/src/components/message-file.ts
Normal file
14
packages/ui/src/components/message-file.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { FilePart } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export function attached(part: FilePart) {
|
||||
return part.url.startsWith("data:")
|
||||
}
|
||||
|
||||
export function inline(part: FilePart) {
|
||||
if (attached(part)) return false
|
||||
return part.source?.text?.start !== undefined && part.source?.text?.end !== undefined
|
||||
}
|
||||
|
||||
export function kind(part: FilePart) {
|
||||
return part.mime.startsWith("image/") ? "image" : "file"
|
||||
}
|
||||
@ -38,10 +38,12 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--surface-weak);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
cursor: default;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
opacity 0.3s ease;
|
||||
@ -50,14 +52,19 @@
|
||||
border-color: var(--border-strong-base);
|
||||
}
|
||||
|
||||
&[data-clickable] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-type="image"] {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
&[data-type="file"] {
|
||||
width: 48px;
|
||||
width: min(220px, 100%);
|
||||
height: 48px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +88,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="user-message-attachment-file"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
[data-component="file-icon"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="user-message-attachment-name"] {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-base);
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
|
||||
[data-slot="user-message-body"] {
|
||||
width: fit-content;
|
||||
max-width: min(82%, 64ch);
|
||||
|
||||
@ -54,6 +54,7 @@ import { AnimatedCountList } from "./tool-count-summary"
|
||||
import { ToolStatusTitle } from "./tool-status-title"
|
||||
import { animate } from "motion"
|
||||
import { useLocation } from "@solidjs/router"
|
||||
import { attached, inline, kind } from "./message-file"
|
||||
|
||||
function ShellSubmessage(props: { text: string; animate?: boolean }) {
|
||||
let widthRef: HTMLSpanElement | undefined
|
||||
@ -901,19 +902,9 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
||||
|
||||
const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? [])
|
||||
|
||||
const attachments = createMemo(() =>
|
||||
files()?.filter((f) => {
|
||||
const mime = f.mime
|
||||
return mime.startsWith("image/") || mime === "application/pdf"
|
||||
}),
|
||||
)
|
||||
const attachments = createMemo(() => files().filter(attached))
|
||||
|
||||
const inlineFiles = createMemo(() =>
|
||||
files().filter((f) => {
|
||||
const mime = f.mime
|
||||
return !mime.startsWith("image/") && mime !== "application/pdf" && f.source?.text?.start !== undefined
|
||||
}),
|
||||
)
|
||||
const inlineFiles = createMemo(() => files().filter(inline))
|
||||
|
||||
const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? [])
|
||||
|
||||
@ -973,32 +964,34 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
||||
<Show when={attachments().length > 0}>
|
||||
<div data-slot="user-message-attachments">
|
||||
<For each={attachments()}>
|
||||
{(file) => (
|
||||
<div
|
||||
data-slot="user-message-attachment"
|
||||
data-type={file.mime.startsWith("image/") ? "image" : "file"}
|
||||
onClick={() => {
|
||||
if (file.mime.startsWith("image/") && file.url) {
|
||||
openImagePreview(file.url, file.filename)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={file.mime.startsWith("image/") && file.url}
|
||||
fallback={
|
||||
<div data-slot="user-message-attachment-icon">
|
||||
<Icon name="folder" />
|
||||
</div>
|
||||
}
|
||||
{(file) => {
|
||||
const type = kind(file)
|
||||
const name = file.filename ?? i18n.t("ui.message.attachment.alt")
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="user-message-attachment"
|
||||
data-type={type}
|
||||
data-clickable={type === "image" ? "true" : undefined}
|
||||
title={type === "file" ? name : undefined}
|
||||
onClick={() => {
|
||||
if (type === "image") openImagePreview(file.url, name)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
data-slot="user-message-attachment-image"
|
||||
src={file.url}
|
||||
alt={file.filename ?? i18n.t("ui.message.attachment.alt")}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
<Show
|
||||
when={type === "image"}
|
||||
fallback={
|
||||
<div data-slot="user-message-attachment-file">
|
||||
<FileIcon node={{ path: name, type: "file" }} />
|
||||
<span data-slot="user-message-attachment-name">{name}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user