feat(mindspace): 悬浮聊天勾选资料联动、UI 与交互优化

勾选单个文件时 Agent 获知上下文并主动问候;打开悬浮窗使用新会话。
统一弹窗浅色主题、固定高度与输入区布局,支持点击外部收起。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 02:17:36 +08:00
parent 2c91689692
commit 71e536eceb
10 changed files with 609 additions and 47 deletions
+1 -1
View File
@@ -297,7 +297,7 @@ export function ChatPanel({
: chatState === 'waiting'
? '消息已收到,正在连接后台…'
: compact
? '在这里继续和 Agent 对话…'
? '随时召唤你的小助手吧'
: randomPrompt;
const mergeVoiceText = (spoken: string) => {
+73 -3
View File
@@ -1,9 +1,29 @@
import { useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useChat } from '../context/ChatProvider';
import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types';
import { buildSelectedAssetGreeting } from '../utils/mindspaceChatContext';
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,
@@ -31,15 +51,64 @@ export function MindSpaceSpaceChat({
hideOpenFullChat?: boolean;
title?: string;
}) {
const { chatState, pendingTool } = useChat();
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')) 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 === 'streaming'}
@@ -56,6 +125,7 @@ export function MindSpaceSpaceChat({
chatBridge={chatBridge}
hideOpenFullChat={hideOpenFullChat}
title={title}
prefillMessages={prefillMessages}
/>
</div>
);
+10 -2
View File
@@ -667,6 +667,14 @@ export function MindSpaceView({
return () => document.removeEventListener('pointerdown', handlePointerDown);
}, [openAssetMenuId]);
const selectedAssetsForChat = useMemo(
() =>
selectedAssetIds
.map((id) => assets.find((asset) => asset.id === id))
.filter((asset): asset is MindSpaceAsset => Boolean(asset)),
[selectedAssetIds, assets],
);
const mindspaceChatContext = useMemo(() => {
if (!space) return null;
return buildMindSpaceChatContext({
@@ -677,7 +685,7 @@ export function MindSpaceView({
pages,
pageLive: pageLiveContext,
assets,
focusedAsset: agentAsset,
selectedAssets: selectedAssetsForChat,
route: `${location.pathname}${location.search}`,
agentSessionId: session?.id ?? null,
h5ApiBase,
@@ -690,7 +698,7 @@ export function MindSpaceView({
pages,
pageLiveContext,
assets,
agentAsset,
selectedAssetsForChat,
location.pathname,
location.search,
session?.id,
+14 -4
View File
@@ -1,8 +1,8 @@
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { useChat } from '../context/ChatProvider';
import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat';
import type { PageEditSubChatBridge } from '../hooks/usePageEditSubChat';
import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types';
import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser, Message } from '../types';
import { formatContextChip } from '../utils/mindspaceChatContext';
import { ChatPanel } from './ChatPanel';
import { TKMindAvatar } from './TKMindAvatar';
@@ -17,6 +17,7 @@ export function SpaceChatPanel({
chatBridge,
hideOpenFullChat = false,
title = 'TKMind',
prefillMessages = [],
}: {
open: boolean;
context: MindSpaceChatContext;
@@ -31,6 +32,7 @@ export function SpaceChatPanel({
chatBridge?: PageEditSubChatBridge;
hideOpenFullChat?: boolean;
title?: string;
prefillMessages?: Message[];
}) {
const mainChat = useChat();
const chat = chatBridge ?? mainChat;
@@ -59,6 +61,15 @@ export function SpaceChatPanel({
return () => window.removeEventListener('keydown', handleKeyDown);
}, [open, onClose]);
const displayMessages = useMemo(() => {
if (!prefillMessages.length) return messages;
if (messages.some((message) => message.role === 'user')) return messages;
const extras = prefillMessages.filter(
(item) => !messages.some((message) => message.id && message.id === item.id),
);
return extras.length ? [...messages, ...extras] : messages;
}, [messages, prefillMessages]);
if (!open) return null;
const contextLabel = formatContextChip(context);
@@ -119,7 +130,7 @@ export function SpaceChatPanel({
<ChatPanel
variant="compact"
user={user}
messages={messages}
messages={displayMessages}
chatState={chatState}
pendingTool={pendingTool}
session={session}
@@ -132,7 +143,6 @@ export function SpaceChatPanel({
onStop={stop}
onApproveTool={approveTool}
onPageSaved={onPageSaved}
onClose={onClose}
/>
</div>
);