Skip to content

Commit 93961c4

Browse files
committed
Refactor file preview with new FilePreview component
Introduce a reusable FilePreview component to unify file rendering and interactions. Replace existing file preview logic in ChatInput, ResponseMessage, and UserMessage with FilePreview. Add support for multiple file types (images, videos, audio, and others) with intelligent rendering based on contentType and fileName. Enhance user experience with consistent styles, hover effects, and delete functionality. Add new file type icons (PDF, Word, Excel, etc.) and a download button. Update README with detailed documentation for FilePreview. Standardize file handling with the FileDef interface.
1 parent f1074d4 commit 93961c4

15 files changed

Lines changed: 706 additions & 100 deletions

src/FE/components/Chat/ChatInput.tsx

Lines changed: 26 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import HomeContext from '@/contexts/home.context';
4747
import UploadButton from '../Button/UploadButton';
4848
import PasteUpload from '../PasteUpload/PasteUpload';
4949
import FilesPopover from '../Popover/FilesPopover';
50+
import FilePreview from '@/components/FilePreview/FilePreview';
5051
import PromptList from './PromptList';
5152
import VariableModal from './VariableModal';
5253

@@ -195,15 +196,16 @@ const ChatInput = ({
195196
toast.error(t('Please enter a message'));
196197
return;
197198
}
198-
const fileIds = contentFiles.map((f) => ({
199+
// 传递完整的 FileDef 对象,而不是只传 id
200+
const fileContents = contentFiles.map((f) => ({
199201
i: '',
200202
$type: MessageContentType.fileId as const,
201-
c: f.id,
203+
c: f, // 传递整个 FileDef 对象
202204
}));
203205
onSend({
204206
role: ChatRole.User,
205207
content: [
206-
...fileIds,
208+
...fileContents,
207209
{ i: '', $type: MessageContentType.text as const, c: contentText },
208210
],
209211
});
@@ -417,27 +419,16 @@ const ChatInput = ({
417419
<div className="absolute mb-1 bottom-full mx-auto flex w-full justify-start z-10">
418420
{!isFullWriting &&
419421
contentFiles.map((file, index) => (
420-
<div className="relative group shadow-sm" key={index}>
421-
<div className="mr-1 w-[4rem] h-[4rem] rounded overflow-hidden">
422-
<img
423-
src={getFileUrl(file)}
424-
alt=""
425-
className="w-full h-full object-cover shadow-sm"
426-
/>
427-
<button
428-
onClick={() => {
429-
setContentFiles((prev) => {
430-
return prev.filter((f) => f !== file);
431-
});
432-
}}
433-
className="absolute top-[-4px] right-[0px]"
434-
>
435-
<IconCircleX
436-
className="bg-background rounded-full text-black/50 dark:text-white/50"
437-
size={20}
438-
/>
439-
</button>
440-
</div>
422+
<div className="mr-2" key={index}>
423+
<FilePreview
424+
file={file}
425+
maxWidth={80}
426+
maxHeight={80}
427+
showDelete={true}
428+
onDelete={() => {
429+
setContentFiles((prev) => prev.filter((f) => f !== file));
430+
}}
431+
/>
441432
</div>
442433
))}
443434
</div>
@@ -655,30 +646,18 @@ const ChatInput = ({
655646

656647
{/* 全屏模式下的文件展示 */}
657648
{isFullWriting && contentFiles.length > 0 && (
658-
<div className="flex flex-row px-3 pb-2 gap-1">
649+
<div className="flex flex-row px-3 pb-2 gap-2">
659650
{contentFiles.map((file, index) => (
660-
<div className="relative group shadow-sm" key={index}>
661-
<div className="mr-1 w-[4rem] h-[4rem] rounded overflow-hidden">
662-
<img
663-
src={getFileUrl(file)}
664-
alt=""
665-
className="w-full h-full object-cover shadow-sm"
666-
/>
667-
<button
668-
onClick={() => {
669-
setContentFiles((prev) => {
670-
return prev.filter((f) => f !== file);
671-
});
672-
}}
673-
className="absolute top-[-4px] right-[0px]"
674-
>
675-
<IconCircleX
676-
className="bg-background rounded-full text-black/50 dark:text-white/50"
677-
size={20}
678-
/>
679-
</button>
680-
</div>
681-
</div>
651+
<FilePreview
652+
key={index}
653+
file={file}
654+
maxWidth={80}
655+
maxHeight={80}
656+
showDelete={true}
657+
onDelete={() => {
658+
setContentFiles((prev) => prev.filter((f) => f !== file));
659+
}}
660+
/>
682661
))}
683662
</div>
684663
)}

src/FE/components/ChatMessage/ResponseMessage.tsx

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { CodeBlock } from '@/components/Markdown/CodeBlock';
2121
import { MemoizedReactMarkdown } from '@/components/Markdown/MemoizedReactMarkdown';
2222
import ToolCallBlock from '@/components/Markdown/ToolCallBlock';
2323
import ImagePreview from '@/components/ImagePreview/ImagePreview';
24+
import FilePreview from '@/components/FilePreview/FilePreview';
2425

2526
import ChatError from '../ChatError/ChatError';
2627
import { IconCopy, IconDots, IconEdit } from '../Icons';
@@ -261,62 +262,75 @@ const ResponseMessage = (props: Props) => {
261262

262263
{/* Render content in original order */}
263264
{groupedContent.map((item, groupIndex) => {
264-
// 如果是图片数组,用容器包裹并横向排列
265+
// 如果是文件数组,用容器包裹并横向排列
265266
if (Array.isArray(item)) {
266267
return (
267-
<div key={`image-group-${groupIndex}`} className="flex flex-wrap gap-2">
268+
<div key={`file-group-${groupIndex}`} className="flex flex-wrap gap-2">
268269
{item.map((c, index) => {
269270
if (c.$type === MessageContentType.fileId) {
270-
const imageUrl = getFileUrl(c.c as FileDef);
271271
return (
272-
<img
273-
alt={t('Loading...')}
272+
<FilePreview
274273
key={'file-' + groupIndex + '-' + index}
275-
className="rounded-md cursor-pointer hover:opacity-90 transition-opacity"
276-
style={{ maxWidth: 300, maxHeight: 300 }}
277-
src={imageUrl}
278-
onClick={(e) => handleImageClick(imageUrl, allImageUrls, e)}
274+
file={c.c as FileDef}
275+
onImageClick={handleImageClick}
279276
/>
280277
);
281278
} else if (c.$type === MessageContentType.tempFileId) {
279+
// 临时文件显示加载效果
282280
const imageUrl = getFileUrl(c.c as FileDef);
283-
return (
284-
<div key={'temp-file-' + groupIndex + '-' + index} className="relative rounded-md overflow-hidden" style={{ maxWidth: 300, maxHeight: 300 }}>
285-
<img
286-
alt={t('Loading...')}
287-
className="w-full h-full object-cover rounded-md cursor-pointer hover:opacity-90 transition-opacity"
288-
src={imageUrl}
289-
onClick={(e) => handleImageClick(imageUrl, allImageUrls, e)}
290-
/>
291-
{/* 蓝色激光扫描效果 */}
292-
<div className="absolute inset-0 pointer-events-none">
293-
<div
294-
className="absolute w-full h-1 bg-gradient-to-r from-transparent via-blue-500 to-transparent shadow-[0_0_20px_rgba(59,130,246,0.8)]"
295-
style={{
296-
animation: 'scan 2s linear infinite',
297-
}}
281+
const fileDef = c.c as FileDef;
282+
const isImage = fileDef.contentType.startsWith('image/');
283+
284+
if (isImage) {
285+
return (
286+
<div key={'temp-file-' + groupIndex + '-' + index} className="relative rounded-md overflow-hidden" style={{ maxWidth: 300, maxHeight: 300 }}>
287+
<img
288+
alt={t('Loading...')}
289+
className="w-full h-full object-cover rounded-md cursor-pointer hover:opacity-90 transition-opacity"
290+
src={imageUrl}
291+
onClick={(e) => handleImageClick(imageUrl, allImageUrls, e)}
298292
/>
299-
</div>
300-
<style jsx>{`
301-
@keyframes scan {
302-
0% {
303-
top: -4px;
304-
opacity: 0;
305-
}
306-
10% {
307-
opacity: 1;
308-
}
309-
90% {
310-
opacity: 1;
293+
{/* 蓝色激光扫描效果 */}
294+
<div className="absolute inset-0 pointer-events-none">
295+
<div
296+
className="absolute w-full h-1 bg-gradient-to-r from-transparent via-blue-500 to-transparent shadow-[0_0_20px_rgba(59,130,246,0.8)]"
297+
style={{
298+
animation: 'scan 2s linear infinite',
299+
}}
300+
/>
301+
</div>
302+
<style jsx>{`
303+
@keyframes scan {
304+
0% {
305+
top: -4px;
306+
opacity: 0;
307+
}
308+
10% {
309+
opacity: 1;
310+
}
311+
90% {
312+
opacity: 1;
313+
}
314+
100% {
315+
top: 100%;
316+
opacity: 0;
317+
}
311318
}
312-
100% {
313-
top: 100%;
314-
opacity: 0;
315-
}
316-
}
317-
`}</style>
318-
</div>
319-
);
319+
`}</style>
320+
</div>
321+
);
322+
} else {
323+
// 非图片临时文件显示普通加载状态
324+
return (
325+
<div key={'temp-file-' + groupIndex + '-' + index} className="relative">
326+
<FilePreview
327+
file={fileDef}
328+
onImageClick={handleImageClick}
329+
className="opacity-60 animate-pulse"
330+
/>
331+
</div>
332+
);
333+
}
320334
}
321335
return null;
322336
})}

src/FE/components/ChatMessage/UserMessage.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { Button } from '@/components/ui/button';
1919
import { SendButton, useSendKeyHandler } from '@/components/ui/send-button';
2020
import ImagePreview from '@/components/ImagePreview/ImagePreview';
21+
import FilePreview from '@/components/FilePreview/FilePreview';
2122

2223
import { Textarea } from '../ui/textarea';
2324
import CopyAction from './CopyAction';
@@ -214,16 +215,12 @@ const UserMessage = (props: Props) => {
214215
<div className="flex flex-wrap justify-end text-right gap-2">
215216
{content
216217
.filter((x) => x.$type === MessageContentType.fileId)
217-
.map((img: any, index) => {
218-
const imageUrl = getFileUrl(img.c);
218+
.map((file: any, index) => {
219219
return (
220-
<img
221-
className="rounded-md not-prose cursor-pointer hover:opacity-90 transition-opacity"
222-
key={'user-img-' + index}
223-
style={{ maxWidth: 300, maxHeight: 300 }}
224-
src={imageUrl}
225-
alt=""
226-
onClick={(e) => handleImageClick(imageUrl, allImageUrls, e)}
220+
<FilePreview
221+
key={'user-file-' + index}
222+
file={file.c}
223+
onImageClick={handleImageClick}
227224
/>
228225
);
229226
})}

0 commit comments

Comments
 (0)