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 />
+194
View File
@@ -2033,6 +2033,200 @@ body,
background: transparent;
}
.conversation-package-strip {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid rgba(96, 118, 153, 0.2);
border-radius: 14px;
color: #6e7b8f;
background:
linear-gradient(135deg, rgba(244, 248, 255, 0.9), rgba(232, 239, 248, 0.82)),
rgba(255, 255, 255, 0.86);
font-size: 12px;
}
.conversation-package-strip button {
border: 0;
border-radius: 999px;
padding: 6px 10px;
color: #f2f7ff;
background: linear-gradient(135deg, #203347, #0f1c2a);
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.conversation-package-strip button:hover {
filter: brightness(1.08);
}
.conversation-package-backdrop {
position: fixed;
inset: 0;
z-index: 80;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 18px;
background: rgba(7, 12, 19, 0.46);
backdrop-filter: blur(10px);
}
.conversation-package-panel {
width: min(680px, 100%);
max-height: min(76vh, 720px);
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
border: 1px solid rgba(138, 159, 185, 0.28);
border-radius: 24px;
color: #dbe7f7;
background:
radial-gradient(circle at 12% 0%, rgba(67, 113, 154, 0.28), transparent 32%),
linear-gradient(160deg, rgba(15, 26, 40, 0.98), rgba(7, 14, 24, 0.98));
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.36);
}
.conversation-package-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.conversation-package-head p {
margin: 0 0 4px;
color: #91a4ba;
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.conversation-package-head h2 {
margin: 0;
color: #f4f8ff;
font-size: 20px;
line-height: 1.25;
}
.conversation-package-close {
border: 1px solid rgba(168, 187, 210, 0.2);
border-radius: 999px;
padding: 7px 12px;
color: #dce8f8;
background: rgba(255, 255, 255, 0.06);
cursor: pointer;
}
.conversation-package-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
color: #93a8bf;
font-size: 12px;
}
.conversation-package-meta code {
max-width: 100%;
padding: 4px 8px;
overflow: hidden;
text-overflow: ellipsis;
border-radius: 8px;
color: #bcd0e7;
background: rgba(255, 255, 255, 0.06);
}
.conversation-package-state {
padding: 28px 12px;
border: 1px dashed rgba(158, 178, 202, 0.25);
border-radius: 16px;
color: #d3e1f2;
text-align: center;
}
.conversation-package-state-muted {
color: #95a9be;
}
.conversation-package-groups {
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
overflow: auto;
padding-right: 2px;
}
.conversation-package-group h3 {
margin: 0 0 8px;
color: #aebfce;
font-size: 13px;
}
.conversation-package-items {
display: grid;
gap: 8px;
}
.conversation-package-item {
display: grid;
grid-template-columns: auto 1fr;
gap: 3px 10px;
padding: 10px;
border: 1px solid rgba(155, 175, 199, 0.16);
border-radius: 14px;
color: inherit;
text-decoration: none;
background: rgba(255, 255, 255, 0.055);
}
.conversation-package-item:hover {
border-color: rgba(181, 202, 226, 0.34);
background: rgba(255, 255, 255, 0.085);
}
.conversation-package-item[aria-disabled='true'] {
cursor: default;
}
.conversation-package-item-kind {
grid-row: span 2;
align-self: center;
padding: 5px 8px;
border-radius: 999px;
color: #ffd59d;
background: rgba(255, 189, 113, 0.12);
font-size: 12px;
font-weight: 700;
}
.conversation-package-item strong {
min-width: 0;
overflow: hidden;
color: #f1f7ff;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-package-item span:last-child {
min-width: 0;
overflow: hidden;
color: #91a6bc;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-package-actions {
display: flex;
justify-content: flex-end;
}
.chat-input-row {
display: flex;
align-items: center;