diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 6cf082b..ce19275 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -4,7 +4,7 @@ import { openAvatarPicker } from '../utils/userAvatar'; import { CHAT_SKILL_OPTIONS, filterChatSkills } from '../utils/chatSkills'; import { getMessageSaveActions } from '../utils/messageSave'; import { getDisplayText } from '../utils/message'; -import { downloadChatMessageDocx } from '../api/client'; +import { ApiError, downloadChatMessageDocx, getMindSpaceConversationPackage } from '../api/client'; import { CHAT_IMAGE_UPLOAD_MAX_COUNT, CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES, @@ -17,7 +17,16 @@ import { ChatSharePreviewModal } from './ChatSharePreviewModal'; import { MessageList } from './MessageList'; import { PageSaveDialog } from './PageSaveDialog'; import { VoiceInputButton } from './VoiceInputButton'; -import type { CapabilityMap, ChatState, Message, PortalUser, Session, ToolConfirmation } from '../types'; +import type { + CapabilityMap, + ChatState, + Message, + MindSpaceConversationArtifact, + MindSpaceConversationPackage, + PortalUser, + Session, + ToolConfirmation, +} from '../types'; import type { MindSpaceSaveCategory } from '../types'; const CHAT_PLACEHOLDER_PROMPTS = [ @@ -106,6 +115,50 @@ function downloadBlob(blob: Blob, filename: string) { window.setTimeout(() => URL.revokeObjectURL(url), 60_000); } +function packageArtifactLabel(kind: MindSpaceConversationArtifact['kind']) { + switch (kind) { + case 'input_image': + return '输入图片'; + case 'input_file': + return '输入文件'; + case 'generated_image': + return '生成图片'; + case 'generated_file': + return '生成文件'; + case 'page': + return '页面草稿'; + case 'public_html': + return '公开页面'; + case 'long_image': + return '长图'; + case 'thumbnail': + return '缩略图'; + case 'docx': + return 'Word 文档'; + case 'pdf': + return 'PDF'; + default: + return '文件'; + } +} + +function formatPackageArtifactSize(sizeBytes?: number) { + if (!Number.isFinite(sizeBytes)) return null; + const bytes = Number(sizeBytes); + if (bytes < 1024) return `${bytes}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)}KB`; + return `${(bytes / 1024 / 1024).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)}MB`; +} + +function groupPackageArtifacts(artifacts: MindSpaceConversationArtifact[]) { + const groups = new Map(); + artifacts.forEach((artifact) => { + const label = packageArtifactLabel(artifact.kind); + groups.set(label, [...(groups.get(label) ?? []), artifact]); + }); + return Array.from(groups.entries()); +} + export function ChatPanel({ variant, user, @@ -159,6 +212,10 @@ export function ChatPanel({ const [uploadingImage, setUploadingImage] = useState(false); const [imageError, setImageError] = useState(null); const [voiceStopSignal, setVoiceStopSignal] = useState(0); + const [packagePanelOpen, setPackagePanelOpen] = useState(false); + const [packageLoading, setPackageLoading] = useState(false); + const [conversationPackage, setConversationPackage] = useState(null); + const [packageError, setPackageError] = useState(null); const [randomPrompt] = useState( () => CHAT_PLACEHOLDER_PROMPTS[Math.floor(Math.random() * CHAT_PLACEHOLDER_PROMPTS.length)], ); @@ -225,8 +282,35 @@ export function ChatPanel({ initializedSessionRef.current = null; pendingOlderLoadRef.current = null; nearBottomRef.current = true; + setPackagePanelOpen(false); + setPackageLoading(false); + setConversationPackage(null); + setPackageError(null); }, [session?.id]); + const loadConversationPackage = useCallback(async () => { + if (!session?.id) return; + setPackageLoading(true); + setPackageError(null); + try { + const data = await getMindSpaceConversationPackage(session.id); + setConversationPackage(data); + } catch (err) { + setConversationPackage(null); + if (err instanceof ApiError && err.status === 404) { + setPackageError('这个对话还没有形成文件夹内容'); + } else { + setPackageError(err instanceof Error ? err.message : '对话文件夹加载失败'); + } + } finally { + setPackageLoading(false); + } + }, [session?.id]); + + useEffect(() => { + if (packagePanelOpen) void loadConversationPackage(); + }, [loadConversationPackage, packagePanelOpen]); + useLayoutEffect(() => { const container = mainRef.current; if (!container) return; @@ -666,7 +750,92 @@ export function ChatPanel({ /> )} + {packagePanelOpen && !compact && ( +
setPackagePanelOpen(false)}> +
event.stopPropagation()} + > +
+
+

MindSpace 对话包

+

{conversationPackage?.title || session?.name || '本次对话文件夹'}

+
+ +
+
+ {conversationPackage?.artifacts.length ?? 0} 个项目 + {conversationPackage?.uri && {conversationPackage.uri}} +
+ {packageLoading &&
正在整理这个对话里的文件…
} + {!packageLoading && packageError && ( +
{packageError}
+ )} + {!packageLoading && !packageError && conversationPackage && conversationPackage.artifacts.length === 0 && ( +
+ 这个对话暂时没有图片、文件或页面。 +
+ )} + {!packageLoading && !packageError && conversationPackage && conversationPackage.artifacts.length > 0 && ( +
+ {groupPackageArtifacts(conversationPackage.artifacts).map(([label, artifacts]) => ( + + ))} +
+ )} +
+ +
+
+
+ )} +