返回 DeepSeek-Reasonix
MessageAttachments.tsx
根目录 / desktop / frontend / src / components / MessageAttachments.tsx
1 import { useCallback, useEffect, useState } from "react";
2 import { FileText, Folder, Image } from "lucide-react";
3 import { app } from "../lib/bridge";
4 import { sortDisplayAttachments, type DisplayAttachment } from "../lib/attachmentDisplay";
5 import { useT } from "../lib/i18n";
6 import { ImageViewer } from "./ImageViewer";
7 import { Tooltip } from "./Tooltip";
8
9 function loadPreview(tabId: string, attachment: DisplayAttachment): Promise<string> {
10 const { path } = attachment;
11 if (path.startsWith("draft:") && typeof app.ReadDraftImageForTab === "function") {
12 return app.ReadDraftImageForTab(tabId, path.slice("draft:".length));
13 }
14 if (path.startsWith("attachment:") && typeof app.ReadSessionAttachmentForTab === "function") {
15 const digest = path.slice("attachment:".length).slice(0, 64);
16 const mime = attachment.mime || (attachment.ext === "JPG" || attachment.ext === "JPEG" ? "image/jpeg" : `image/${attachment.ext.toLowerCase() || "png"}`);
17 return import("../lib/sessionAttachmentRead").then(module => module.readSessionAttachmentDataURL(
18 tabId,
19 digest,
20 mime,
21 (id, nextDigest, offset) => app.ReadSessionAttachmentForTab!(id, nextDigest, offset),
22 ));
23 }
24 return app.AttachmentDataURLForTab(tabId, path);
25 }
26
27 function attachmentIcon(kind: DisplayAttachment["kind"]) {
28 if (kind === "image") return <Image size={15} />;
29 if (kind === "folder") return <Folder size={15} />;
30 return <FileText size={15} />;
31 }
32
33 export function MessageAttachments({ attachments, tabId = "" }: {
34 attachments: DisplayAttachment[];
35 tabId?: string;
36 }) {
37 const t = useT();
38 const ordered = sortDisplayAttachments(attachments);
39 const [previews, setPreviews] = useState<Record<string, string>>({});
40 const [viewer, setViewer] = useState({ open: false, url: "", name: "" });
41 const openViewer = useCallback(async (attachment: DisplayAttachment) => {
42 let url = previews[attachment.path];
43 if (!url) {
44 try {
45 url = await loadPreview(tabId, attachment);
46 setPreviews(previous => previous[attachment.path] ? previous : { ...previous, [attachment.path]: url });
47 } catch { return; }
48 }
49 setViewer({ open: true, url, name: attachment.name });
50 }, [previews, tabId]);
51
52 const previewKey = ordered.filter(item => item.kind === "image" && item.source === "attachment").map(item => item.path).join("\n");
53 useEffect(() => {
54 let cancelled = false;
55 for (const path of previewKey ? previewKey.split("\n") : []) {
56 if (previews[path]) continue;
57 const attachment = ordered.find(item => item.path === path);
58 if (!attachment) continue;
59 loadPreview(tabId, attachment).then(url => {
60 if (!cancelled) setPreviews(previous => previous[path] ? previous : { ...previous, [path]: url });
61 }).catch(() => {});
62 }
63 return () => { cancelled = true; };
64 }, [previewKey, tabId]);
65
66 return (
67 <div className="msg-attachments" aria-label={t("msg.attachments")} data-transcript-selection-ignore>
68 {ordered.map((attachment, index) => {
69 const image = attachment.kind === "image";
70 const card = (
71 <div
72 className={`msg-attachment msg-attachment--${attachment.kind}`}
73 key={image ? undefined : `${attachment.path}:${index}`}
74 title={image ? undefined : attachment.path}
75 onClick={image ? () => openViewer(attachment) : undefined}
76 role={image ? "button" : undefined}
77 tabIndex={image ? 0 : undefined}
78 onKeyDown={image ? event => {
79 if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openViewer(attachment); }
80 } : undefined}
81 >
82 <span className={`msg-attachment__icon msg-attachment__icon--${attachment.kind}`} aria-hidden="true">
83 {image && previews[attachment.path] ? <img src={previews[attachment.path]} alt="" draggable={false} /> : attachmentIcon(attachment.kind)}
84 </span>
85 <span className="msg-attachment__main">
86 <span className="msg-attachment__name">{attachment.name}</span>
87 <span className="msg-attachment__meta">
88 {attachment.kind === "folder"
89 ? t("msg.folderReference")
90 : `${attachment.ext || t("msg.fileAttachment")} · ${attachment.source === "workspace" ? t("msg.workspaceReference") : image ? t("msg.imageAttachment") : t("msg.fileAttachment")}`}
91 </span>
92 </span>
93 </div>
94 );
95 return image ? <Tooltip key={`${attachment.path}:${index}`} label={t("imageViewer.clickToPreview")} block>{card}</Tooltip> : card;
96 })}
97 <ImageViewer open={viewer.open} imageUrl={viewer.url} imageName={viewer.name} onClose={() => setViewer(value => value.open ? { ...value, open: false } : value)} />
98 </div>
99 );
100 }
101
101 lines Plain Text