Files
memind/src/components/MindSpaceSpaceChat.tsx
T
john 14a00774d9 feat(h5): web 联网能力、实时查询路由与 session Finish 对齐
- 新增 web 能力并挂载 platform/web(web_search/fetch_url)
- 实时查询强制 web skill,router fallback 与 await session Finish
- Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 16:06:26 +08:00

134 lines
4.1 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react';
import { useChat } from '../context/ChatProvider';
import { buildSelectedAssetGreeting } from '../utils/mindspaceChatContext';
import { shouldShowChatMessage } from '../utils/message';
import type { Message, MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types';
import { SpaceChatFab } from './SpaceChatFab';
import { SpaceChatPanel } from './SpaceChatPanel';
const SELECTED_ASSET_GREETING_ID = 'mindspace-selected-asset-greeting';
function buildSelectedAssetGreetingMessage(
asset: NonNullable<MindSpaceChatContext['selectedAssets']>[number],
): Message {
const text = buildSelectedAssetGreeting(asset);
return {
id: SELECTED_ASSET_GREETING_ID,
role: 'assistant',
created: Math.floor(Date.now() / 1000),
content: [{ type: 'text', text }],
metadata: {
userVisible: true,
agentVisible: false,
displayText: text,
},
};
}
export function MindSpaceSpaceChat({
context,
user,
onOpenFullChat,
onPageSaved,
open: controlledOpen,
onOpenChange,
variant = 'default',
chatBridge,
hideOpenFullChat = false,
title,
}: {
context: MindSpaceChatContext;
user: PortalUser;
onOpenFullChat: () => void;
onPageSaved?: (result: {
kind: 'page' | 'category';
pageId?: string;
categoryCode?: MindSpaceSaveCategory;
}) => void;
open?: boolean;
onOpenChange?: (open: boolean) => void;
variant?: 'default' | 'overlay';
chatBridge?: import('../hooks/usePageEditSubChat').PageEditSubChatBridge;
hideOpenFullChat?: boolean;
title?: string;
}) {
const { chatState, pendingTool, newSession, switchSession, session, messages } = useChat();
const [internalOpen, setInternalOpen] = useState(false);
const open = controlledOpen ?? internalOpen;
const setOpen = onOpenChange ?? setInternalOpen;
const wasOpenRef = useRef(false);
const restoreSessionIdRef = useRef<string | null>(null);
const messagesRef = useRef(messages);
messagesRef.current = messages;
useEffect(() => {
if (chatBridge) {
wasOpenRef.current = open;
return;
}
const wasOpen = wasOpenRef.current;
if (open && !wasOpen) {
restoreSessionIdRef.current = session?.id ?? null;
newSession();
} else if (!open && wasOpen) {
if (messagesRef.current.length === 0 && restoreSessionIdRef.current) {
switchSession(restoreSessionIdRef.current);
}
restoreSessionIdRef.current = null;
}
wasOpenRef.current = open;
}, [chatBridge, newSession, open, session?.id, switchSession]);
useEffect(() => {
if (chatBridge) return;
return () => {
if (messagesRef.current.length === 0 && restoreSessionIdRef.current) {
switchSession(restoreSessionIdRef.current);
}
};
}, [chatBridge, switchSession]);
const prefillMessages = useMemo(() => {
if (chatBridge || !open) return [];
if (messages.some((message) => message.role === 'user' && shouldShowChatMessage(message))) return [];
const selected = context.selectedAssets ?? [];
if (selected.length !== 1) return [];
return [buildSelectedAssetGreetingMessage(selected[0])];
}, [chatBridge, context.selectedAssets, messages, open]);
const wrapperClass =
variant === 'overlay' ? 'space-chat-stack space-chat-stack-overlay' : 'space-chat-stack';
return (
<div className={wrapperClass}>
{open ? (
<button
type="button"
className="space-chat-backdrop"
aria-label="收起对话"
onClick={() => setOpen(false)}
/>
) : null}
<SpaceChatFab
open={open}
streaming={chatState === 'waiting' || chatState === 'streaming'}
pendingTool={Boolean(pendingTool)}
onClick={() => setOpen(!open)}
/>
<SpaceChatPanel
open={open}
context={context}
user={user}
onClose={() => setOpen(false)}
onOpenFullChat={onOpenFullChat}
onPageSaved={onPageSaved}
chatBridge={chatBridge}
hideOpenFullChat={hideOpenFullChat}
title={title}
prefillMessages={prefillMessages}
/>
</div>
);
}