Show conversation package folder in chat

This commit is contained in:
john
2026-07-02 21:36:33 +08:00
parent ca88716d68
commit 85936fff05
2 changed files with 365 additions and 2 deletions
+171 -2
View File
@@ -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<string, MindSpaceConversationArtifact[]>();
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<string | null>(null);
const [voiceStopSignal, setVoiceStopSignal] = useState(0);
const [packagePanelOpen, setPackagePanelOpen] = useState(false);
const [packageLoading, setPackageLoading] = useState(false);
const [conversationPackage, setConversationPackage] = useState<MindSpaceConversationPackage | null>(null);
const [packageError, setPackageError] = useState<string | null>(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 && (
<div className="conversation-package-backdrop" role="presentation" onClick={() => setPackagePanelOpen(false)}>
<section
className="conversation-package-panel"
role="dialog"
aria-modal="true"
aria-label="本次对话文件夹"
onClick={(event) => event.stopPropagation()}
>
<div className="conversation-package-head">
<div>
<p>MindSpace </p>
<h2>{conversationPackage?.title || session?.name || '本次对话文件夹'}</h2>
</div>
<button type="button" className="conversation-package-close" onClick={() => setPackagePanelOpen(false)}>
</button>
</div>
<div className="conversation-package-meta">
<span>{conversationPackage?.artifacts.length ?? 0} </span>
{conversationPackage?.uri && <code>{conversationPackage.uri}</code>}
</div>
{packageLoading && <div className="conversation-package-state"></div>}
{!packageLoading && packageError && (
<div className="conversation-package-state conversation-package-state-muted">{packageError}</div>
)}
{!packageLoading && !packageError && conversationPackage && conversationPackage.artifacts.length === 0 && (
<div className="conversation-package-state conversation-package-state-muted">
</div>
)}
{!packageLoading && !packageError && conversationPackage && conversationPackage.artifacts.length > 0 && (
<div className="conversation-package-groups">
{groupPackageArtifacts(conversationPackage.artifacts).map(([label, artifacts]) => (
<div className="conversation-package-group" key={label}>
<h3>{label}</h3>
<div className="conversation-package-items">
{artifacts.map((artifact) => {
const sizeLabel = formatPackageArtifactSize(artifact.sizeBytes);
return (
<a
key={artifact.artifactId}
className="conversation-package-item"
href={artifact.canonicalUrl || undefined}
target="_blank"
rel="noreferrer"
aria-disabled={!artifact.canonicalUrl}
onClick={(event) => {
if (!artifact.canonicalUrl) event.preventDefault();
}}
>
<span className="conversation-package-item-kind">{packageArtifactLabel(artifact.kind)}</span>
<strong>{artifact.displayName || artifact.canonicalUrl || artifact.artifactId}</strong>
<span>
{[
artifact.mimeType,
sizeLabel,
artifact.messageId ? `消息 ${artifact.messageId}` : null,
].filter(Boolean).join(' · ') || '已记录到对话包'}
</span>
</a>
);
})}
</div>
</div>
))}
</div>
)}
<div className="conversation-package-actions">
<button type="button" className="ghost-btn" onClick={() => void loadConversationPackage()}>
</button>
</div>
</section>
</div>
)}
<footer className={compact ? 'space-chat-panel-footer' : `footer${showHomeWelcome ? ' footer-home' : ''}`}>
{!compact && session?.id && (
<div className="conversation-package-strip">
<button type="button" onClick={() => setPackagePanelOpen(true)}>
</button>
<span></span>
</div>
)}
{connectStatusText && (
<div className="chat-connect-status" role="status" aria-live="polite">
<ChatLoadingSpinner />