Files
CodePilot/src/components/chat/FileAttachmentDisplay.tsx
T
7418 00d3fef5a4 feat: refactor image generation to bypass model dependency, fix UI issues
Image Generation Refactor:
- MessageInput: add onImageGenerate callback, remove skill fetch dependency
- ChatView: implement handleImageGenerate that creates image-gen-request
  message for ImageGenConfirmation UI, then user selects params and generates
- New API route POST /api/chat/messages for DB-only message persistence
- image-gen-completed event now only persists notice to DB, no longer triggers model
- Reference images passed via module-level Map (imageGenRefStore) to avoid
  DB bloat from base64 data; user message DB content stores metadata only

Bug Fixes:
- Fix useCallback dependency missing imageGen causing stale closure reads
- Fix React Hooks order violation in MessageItem (early return before hooks)
- Fix localStorage cache collision across sessions (key now includes sessionId)
- Fix empty src attribute in FileAttachmentDisplay for DB-restored messages
- Add JSON parse fallback for malformed image-gen-request blocks

Gallery Reference Images:
- /api/media/generate saves reference images to disk, stores paths in metadata
- /api/media/gallery extracts referenceImages from metadata in mapRow
- GalleryDetail displays reference image thumbnails
- ImageGenCard supports referenceImages prop for chat display

UI Changes:
- ImageGenToggle: replace paint brush icon with text label (en: Image, zh: 生图)
- Model selector button: remove provider name display to save space
- ImageGenConfirmation: support referenceImagesData prop for base64 preview

i18n:
- Add imageGen.toggleLabel key (en: Image, zh: 生图)
2026-02-26 21:07:48 +08:00

83 lines
2.3 KiB
TypeScript

'use client';
import { useState, useCallback } from 'react';
import type { FileAttachment } from '@/types';
import { isImageFile } from '@/types';
import { ImageThumbnail } from './ImageThumbnail';
import { FileCard } from './FileCard';
import { ImageLightbox } from './ImageLightbox';
interface FileAttachmentDisplayProps {
files: FileAttachment[];
}
/**
* Build a display URL for a file attachment.
* - If base64 `data` is available (optimistic / in-memory): use data URI
* - If `filePath` is available (reloaded from DB): use the uploads API
*/
function fileUrl(f: FileAttachment): string {
if (f.data) return `data:${f.type};base64,${f.data}`;
if (f.filePath) return `/api/uploads?path=${encodeURIComponent(f.filePath)}`;
return '';
}
export function FileAttachmentDisplay({ files }: FileAttachmentDisplayProps) {
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
const imageFiles = files.filter((f) => isImageFile(f.type) && fileUrl(f));
const otherFiles = files.filter((f) => !isImageFile(f.type) || !fileUrl(f));
const lightboxImages = imageFiles.map((f) => ({
src: fileUrl(f),
alt: f.name,
}));
const handlePreview = useCallback((index: number) => {
setLightboxIndex(index);
setLightboxOpen(true);
}, []);
if (files.length === 0) return null;
const imageGridCols =
imageFiles.length === 1
? 'grid-cols-1 max-w-xs'
: imageFiles.length === 2
? 'grid-cols-2 max-w-sm'
: 'grid-cols-3 max-w-md';
return (
<div className="space-y-2 mb-2">
{imageFiles.length > 0 && (
<div className={`grid gap-2 ${imageGridCols}`}>
{imageFiles.map((file, i) => (
<ImageThumbnail
key={file.id}
src={fileUrl(file)}
alt={file.name}
onClick={() => handlePreview(i)}
/>
))}
</div>
)}
{otherFiles.length > 0 && (
<div className="space-y-1.5">
{otherFiles.map((file) => (
<FileCard key={file.id} name={file.name} size={file.size} />
))}
</div>
)}
<ImageLightbox
images={lightboxImages}
initialIndex={lightboxIndex}
open={lightboxOpen}
onOpenChange={setLightboxOpen}
/>
</div>
);
}