Show conversation package folder in chat
This commit is contained in:
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user