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 />
|
||||
|
||||
+194
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user