182 lines
5.5 KiB
TypeScript
182 lines
5.5 KiB
TypeScript
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, Message } from '../types';
|
||
import { formatContextChip } from '../utils/mindspaceChatContext';
|
||
import { shouldShowChatMessage } from '../utils/message';
|
||
import { ChatPanel } from './ChatPanel';
|
||
import { TKMindAvatar } from './TKMindAvatar';
|
||
|
||
export function SpaceChatPanel({
|
||
open,
|
||
context,
|
||
user,
|
||
onClose,
|
||
onOpenFullChat,
|
||
onPageSaved,
|
||
chatBridge,
|
||
hideOpenFullChat = false,
|
||
title = 'TKMind',
|
||
prefillMessages = [],
|
||
}: {
|
||
open: boolean;
|
||
context: MindSpaceChatContext;
|
||
user: PortalUser;
|
||
onClose: () => void;
|
||
onOpenFullChat: () => void;
|
||
onPageSaved?: (result: {
|
||
kind: 'page' | 'category';
|
||
pageId?: string;
|
||
categoryCode?: MindSpaceSaveCategory;
|
||
}) => void;
|
||
chatBridge?: PageEditSubChatBridge;
|
||
hideOpenFullChat?: boolean;
|
||
title?: string;
|
||
prefillMessages?: Message[];
|
||
}) {
|
||
const mainChat = useChat();
|
||
const chat = chatBridge ?? mainChat;
|
||
const {
|
||
session,
|
||
messages,
|
||
chatState,
|
||
pendingTool,
|
||
submit,
|
||
stop,
|
||
approveTool,
|
||
error,
|
||
notice,
|
||
dismissNotice,
|
||
openRecharge,
|
||
uploadChatImage,
|
||
uploadChatAttachment,
|
||
retryConnect,
|
||
} = chat;
|
||
const { capabilities, grantedSkills } = mainChat;
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const handleKeyDown = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') onClose();
|
||
};
|
||
window.addEventListener('keydown', handleKeyDown);
|
||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||
}, [open, onClose]);
|
||
|
||
const displayMessages = useMemo(() => {
|
||
if (!prefillMessages.length) return messages;
|
||
if (messages.some((message) => message.role === 'user' && shouldShowChatMessage(message))) 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);
|
||
|
||
return (
|
||
<div className="space-chat-panel" role="dialog" aria-modal="false" aria-label="Agent 对话">
|
||
<header className="space-chat-panel-header">
|
||
<div className="space-chat-panel-brand">
|
||
<TKMindAvatar size="sm" />
|
||
<div>
|
||
<strong>{title}</strong>
|
||
<span className="space-chat-context-chip" title={context.route}>
|
||
{contextLabel}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="space-chat-panel-actions">
|
||
{!hideOpenFullChat ? (
|
||
<button type="button" className="ghost-btn" onClick={onOpenFullChat}>
|
||
打开完整聊天
|
||
</button>
|
||
) : null}
|
||
<button type="button" className="space-chat-panel-close" onClick={onClose} aria-label="关闭">
|
||
×
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
{notice && (
|
||
<div className="space-chat-panel-error space-chat-panel-notice">
|
||
<span>{notice}</span>
|
||
{notice === INSUFFICIENT_BALANCE_NOTICE ? (
|
||
<>
|
||
<button type="button" onClick={() => openRecharge(false)}>
|
||
去充值
|
||
</button>
|
||
<button type="button" onClick={dismissNotice}>
|
||
知道了
|
||
</button>
|
||
</>
|
||
) : (
|
||
<button type="button" onClick={dismissNotice}>
|
||
知道了
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{error && (
|
||
<div className="space-chat-panel-error">
|
||
<span>{error}</span>
|
||
<button type="button" onClick={() => void retryConnect()}>
|
||
重试
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
<ChatPanel
|
||
variant="compact"
|
||
user={user}
|
||
messages={displayMessages}
|
||
chatState={chatState}
|
||
pendingTool={pendingTool}
|
||
session={session}
|
||
capabilities={capabilities ?? undefined}
|
||
grantedSkills={grantedSkills}
|
||
onSubmit={(text, imageUrls, previewImageUrls, options) =>
|
||
chatBridge
|
||
? void submit(
|
||
text,
|
||
{
|
||
...context,
|
||
messageId: options?.messageId,
|
||
forceDeepReasoning: options?.forceDeepReasoning,
|
||
pgRequired: options?.pgRequired,
|
||
fileAttachments: options?.fileAttachments,
|
||
},
|
||
imageUrls,
|
||
previewImageUrls,
|
||
)
|
||
: void submit(
|
||
text,
|
||
{
|
||
mindspaceContext: context,
|
||
messageId: options?.messageId,
|
||
forceDeepReasoning: options?.forceDeepReasoning,
|
||
pgRequired: options?.pgRequired,
|
||
fileAttachments: options?.fileAttachments,
|
||
},
|
||
imageUrls,
|
||
previewImageUrls,
|
||
)
|
||
}
|
||
onUploadImage={(file, onProgress, options) =>
|
||
uploadChatImage(file, onProgress, { messageId: options?.messageId })
|
||
}
|
||
onUploadFile={(file, onProgress, options) =>
|
||
uploadChatAttachment(file, onProgress, { messageId: options?.messageId })
|
||
}
|
||
onStop={stop}
|
||
onApproveTool={approveTool}
|
||
onPageSaved={onPageSaved}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|