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 { CHAT_SKILL_OPTIONS, filterChatSkills } from '../utils/chatSkills';
|
||||||
import { getMessageSaveActions } from '../utils/messageSave';
|
import { getMessageSaveActions } from '../utils/messageSave';
|
||||||
import { getDisplayText } from '../utils/message';
|
import { getDisplayText } from '../utils/message';
|
||||||
import { downloadChatMessageDocx } from '../api/client';
|
import { ApiError, downloadChatMessageDocx, getMindSpaceConversationPackage } from '../api/client';
|
||||||
import {
|
import {
|
||||||
CHAT_IMAGE_UPLOAD_MAX_COUNT,
|
CHAT_IMAGE_UPLOAD_MAX_COUNT,
|
||||||
CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
|
CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
|
||||||
@@ -17,7 +17,16 @@ import { ChatSharePreviewModal } from './ChatSharePreviewModal';
|
|||||||
import { MessageList } from './MessageList';
|
import { MessageList } from './MessageList';
|
||||||
import { PageSaveDialog } from './PageSaveDialog';
|
import { PageSaveDialog } from './PageSaveDialog';
|
||||||
import { VoiceInputButton } from './VoiceInputButton';
|
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';
|
import type { MindSpaceSaveCategory } from '../types';
|
||||||
|
|
||||||
const CHAT_PLACEHOLDER_PROMPTS = [
|
const CHAT_PLACEHOLDER_PROMPTS = [
|
||||||
@@ -106,6 +115,50 @@ function downloadBlob(blob: Blob, filename: string) {
|
|||||||
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
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({
|
export function ChatPanel({
|
||||||
variant,
|
variant,
|
||||||
user,
|
user,
|
||||||
@@ -159,6 +212,10 @@ export function ChatPanel({
|
|||||||
const [uploadingImage, setUploadingImage] = useState(false);
|
const [uploadingImage, setUploadingImage] = useState(false);
|
||||||
const [imageError, setImageError] = useState<string | null>(null);
|
const [imageError, setImageError] = useState<string | null>(null);
|
||||||
const [voiceStopSignal, setVoiceStopSignal] = useState(0);
|
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(
|
const [randomPrompt] = useState(
|
||||||
() => CHAT_PLACEHOLDER_PROMPTS[Math.floor(Math.random() * CHAT_PLACEHOLDER_PROMPTS.length)],
|
() => CHAT_PLACEHOLDER_PROMPTS[Math.floor(Math.random() * CHAT_PLACEHOLDER_PROMPTS.length)],
|
||||||
);
|
);
|
||||||
@@ -225,8 +282,35 @@ export function ChatPanel({
|
|||||||
initializedSessionRef.current = null;
|
initializedSessionRef.current = null;
|
||||||
pendingOlderLoadRef.current = null;
|
pendingOlderLoadRef.current = null;
|
||||||
nearBottomRef.current = true;
|
nearBottomRef.current = true;
|
||||||
|
setPackagePanelOpen(false);
|
||||||
|
setPackageLoading(false);
|
||||||
|
setConversationPackage(null);
|
||||||
|
setPackageError(null);
|
||||||
}, [session?.id]);
|
}, [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(() => {
|
useLayoutEffect(() => {
|
||||||
const container = mainRef.current;
|
const container = mainRef.current;
|
||||||
if (!container) return;
|
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' : ''}`}>
|
<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 && (
|
{connectStatusText && (
|
||||||
<div className="chat-connect-status" role="status" aria-live="polite">
|
<div className="chat-connect-status" role="status" aria-live="polite">
|
||||||
<ChatLoadingSpinner />
|
<ChatLoadingSpinner />
|
||||||
|
|||||||
+194
@@ -2033,6 +2033,200 @@ body,
|
|||||||
background: transparent;
|
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 {
|
.chat-input-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user