722b18326f
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。 Co-authored-by: Cursor <cursoragent@cursor.com>
316 lines
10 KiB
TypeScript
316 lines
10 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { useChat } from '../context/ChatProvider';
|
||
import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat';
|
||
import type { CapabilityMap, PortalUser } from '../types';
|
||
import { useNetworkStatus } from '../hooks/useNetworkStatus';
|
||
import { getSessionDisplayName } from '../utils/sessions';
|
||
import { BalanceRing } from './BalanceRing';
|
||
import { HistorySidebar } from './HistorySidebar';
|
||
import { TKMindAvatar } from './TKMindAvatar';
|
||
import { ChatPanel } from './ChatPanel';
|
||
import { ChatLoadingSpinner } from './ChatLoadingSpinner';
|
||
import { WechatBindPrompt } from './WechatBindPrompt';
|
||
import { WechatAccountButton } from './WechatAccountButton';
|
||
import { ChatHeaderMoreMenu } from './ChatHeaderMoreMenu';
|
||
import { NotificationCenter } from './NotificationCenter';
|
||
import type { MindSpaceSaveCategory } from '../types';
|
||
|
||
export function ChatView({
|
||
user,
|
||
capabilities,
|
||
grantedSkills,
|
||
onLogout,
|
||
onOpenSpace,
|
||
onOpenPage,
|
||
onOpenAdmin,
|
||
onOpenFeedback,
|
||
}: {
|
||
user?: PortalUser | null;
|
||
capabilities?: CapabilityMap;
|
||
grantedSkills?: string[];
|
||
onUserUpdate?: (user: PortalUser) => void;
|
||
onLogout?: () => void;
|
||
onOpenSpace?: (target?: { categoryCode?: MindSpaceSaveCategory; pageId?: string }) => void;
|
||
onOpenPage?: (pageId: string) => void;
|
||
onOpenAdmin?: () => void;
|
||
onOpenFeedback?: () => void;
|
||
}) {
|
||
const {
|
||
session,
|
||
sessions,
|
||
sessionsLoading,
|
||
sessionsLoadingMore,
|
||
sessionsHasMore,
|
||
sessionSearchQuery,
|
||
messages,
|
||
messageHistoryLoadingMore,
|
||
messageHistoryHasMore,
|
||
messageHistoryTotal,
|
||
chatState,
|
||
error,
|
||
notice,
|
||
pendingTool,
|
||
submit,
|
||
stop,
|
||
approveTool,
|
||
newSession,
|
||
switchSession,
|
||
deleteSession,
|
||
loadMoreSessions,
|
||
loadOlderMessages,
|
||
setSessionSearchQuery,
|
||
retryConnect,
|
||
dismissNotice,
|
||
onSidebarOpen,
|
||
balanceCents,
|
||
totalCreditCents,
|
||
tokensUsed,
|
||
subscription,
|
||
openRecharge,
|
||
openSubscribe,
|
||
uploadChatImage,
|
||
} = useChat();
|
||
const online = useNetworkStatus();
|
||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (sidebarOpen) {
|
||
onSidebarOpen();
|
||
}
|
||
}, [sidebarOpen, onSidebarOpen]);
|
||
|
||
const showHomeWelcome = messages.length === 0;
|
||
|
||
const handleSelectSession = (sessionId: string) => {
|
||
void switchSession(sessionId);
|
||
setSidebarOpen(false);
|
||
};
|
||
|
||
const handleNewSession = () => {
|
||
void newSession();
|
||
setSidebarOpen(false);
|
||
};
|
||
|
||
const isConnectingTitle = !session && chatState !== 'idle';
|
||
const sessionTitle = session
|
||
? getSessionDisplayName(session)
|
||
: chatState === 'idle'
|
||
? '新对话'
|
||
: '连接中…';
|
||
|
||
const moreMenuItems = [
|
||
{
|
||
id: 'new-session',
|
||
label: '新会话',
|
||
onClick: () => void handleNewSession(),
|
||
},
|
||
...(onOpenFeedback
|
||
? [{ id: 'feedback', label: '反馈与建议', onClick: () => onOpenFeedback() }]
|
||
: []),
|
||
...(onOpenAdmin
|
||
? [{ id: 'admin', label: '管理', onClick: () => onOpenAdmin() }]
|
||
: []),
|
||
...(onLogout
|
||
? [{ id: 'logout', label: '登出', onClick: () => onLogout(), danger: true }]
|
||
: []),
|
||
];
|
||
|
||
return (
|
||
<div className="app-shell">
|
||
<HistorySidebar
|
||
open={sidebarOpen}
|
||
sessions={sessions}
|
||
activeSessionId={session?.id}
|
||
loading={sessionsLoading}
|
||
loadingMore={sessionsLoadingMore}
|
||
hasMore={sessionsHasMore}
|
||
searchQuery={sessionSearchQuery}
|
||
onClose={() => setSidebarOpen(false)}
|
||
onSelect={handleSelectSession}
|
||
onNew={handleNewSession}
|
||
onDelete={(sessionId) => void deleteSession(sessionId)}
|
||
onLoadMore={() => void loadMoreSessions()}
|
||
onSearchChange={setSessionSearchQuery}
|
||
/>
|
||
|
||
<div className={`app${showHomeWelcome ? ' app-home' : ''}`}>
|
||
<header className={`header${showHomeWelcome ? ' header-home' : ''}`}>
|
||
<div className="header-left">
|
||
<button
|
||
type="button"
|
||
className="header-menu-btn"
|
||
aria-label="展开历史对话"
|
||
onClick={() => setSidebarOpen(true)}
|
||
>
|
||
☰
|
||
</button>
|
||
<TKMindAvatar size="sm" className="header-brand-avatar" />
|
||
<div>
|
||
<div className="header-title">{user?.displayName ?? 'TKMind'}</div>
|
||
<div className="header-sub">
|
||
{isConnectingTitle ? <ChatLoadingSpinner /> : null}
|
||
<span>{sessionTitle}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="header-actions header-actions-desktop">
|
||
{typeof balanceCents === 'number' && (
|
||
<BalanceRing
|
||
balanceCents={balanceCents}
|
||
totalCreditCents={totalCreditCents}
|
||
tokensUsed={tokensUsed}
|
||
subscription={subscription}
|
||
onRecharge={openRecharge}
|
||
onSubscribe={openSubscribe}
|
||
/>
|
||
)}
|
||
<NotificationCenter onOpenRecharge={() => openRecharge(false)} />
|
||
{onOpenAdmin && (
|
||
<button type="button" className="ghost-btn" onClick={onOpenAdmin}>
|
||
管理
|
||
</button>
|
||
)}
|
||
{onOpenSpace && (
|
||
<button type="button" className="ghost-btn" onClick={() => onOpenSpace?.()}>
|
||
我的空间
|
||
</button>
|
||
)}
|
||
{onOpenFeedback && (
|
||
<button type="button" className="ghost-btn" onClick={() => onOpenFeedback()}>
|
||
反馈
|
||
</button>
|
||
)}
|
||
<button type="button" className="ghost-btn" onClick={() => void handleNewSession()}>
|
||
新会话
|
||
</button>
|
||
{user && <WechatAccountButton returnTo={window.location.pathname} />}
|
||
{onLogout && (
|
||
<button type="button" className="ghost-btn logout-btn" onClick={onLogout}>
|
||
登出
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="header-actions header-actions-mobile">
|
||
{typeof balanceCents === 'number' && (
|
||
<BalanceRing
|
||
balanceCents={balanceCents}
|
||
totalCreditCents={totalCreditCents}
|
||
tokensUsed={tokensUsed}
|
||
subscription={subscription}
|
||
onRecharge={openRecharge}
|
||
onSubscribe={openSubscribe}
|
||
/>
|
||
)}
|
||
<NotificationCenter onOpenRecharge={() => openRecharge(false)} />
|
||
{onOpenSpace && (
|
||
<button
|
||
type="button"
|
||
className="header-icon-btn"
|
||
aria-label="我的空间"
|
||
title="我的空间"
|
||
onClick={() => onOpenSpace?.()}
|
||
>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||
<path
|
||
d="M4 8.5V19a1 1 0 001 1h14a1 1 0 001-1V8.5M4 8.5l8-4.5 8 4.5M4 8.5h16M9 21v-6h6v6"
|
||
stroke="currentColor"
|
||
strokeWidth="1.75"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
)}
|
||
<ChatHeaderMoreMenu
|
||
items={moreMenuItems}
|
||
returnTo={window.location.pathname}
|
||
showWechat={Boolean(user)}
|
||
/>
|
||
</div>
|
||
</header>
|
||
|
||
{!online && (
|
||
<div className="banner banner-offline">网络已断开,请检查连接后重试</div>
|
||
)}
|
||
|
||
{user && <WechatBindPrompt returnTo={window.location.pathname} />}
|
||
|
||
{typeof balanceCents === 'number' && balanceCents <= 0 && (
|
||
<div className="banner banner-error">
|
||
<span>余额不足,请充值后继续使用</span>
|
||
<button type="button" className="banner-action" onClick={() => openRecharge(false)}>
|
||
去充值
|
||
</button>
|
||
</div>
|
||
)}
|
||
{typeof balanceCents === 'number' && balanceCents > 0 && balanceCents <= 100 && (
|
||
<div className="banner banner-warning">
|
||
<span>余额较低(¥{(balanceCents / 100).toFixed(2)}),建议提前充值</span>
|
||
<button type="button" className="banner-action" onClick={() => openRecharge(false)}>
|
||
去充值
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{notice && (
|
||
<div className={`banner${notice === INSUFFICIENT_BALANCE_NOTICE ? ' banner-warning' : ' banner-info'}`}>
|
||
<span style={{ whiteSpace: 'pre-line' }}>{notice}</span>
|
||
{notice === INSUFFICIENT_BALANCE_NOTICE ? (
|
||
<>
|
||
<button type="button" className="banner-action" onClick={() => openRecharge(false)}>
|
||
去充值
|
||
</button>
|
||
<button type="button" className="banner-dismiss" onClick={dismissNotice}>
|
||
知道了
|
||
</button>
|
||
</>
|
||
) : (
|
||
<button type="button" className="banner-dismiss" onClick={dismissNotice}>
|
||
知道了
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{error && (
|
||
<div className="banner banner-error">
|
||
<span>{error}</span>
|
||
<button type="button" className="banner-action" onClick={() => void retryConnect()}>
|
||
重试
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
<ChatPanel
|
||
variant="full"
|
||
user={user}
|
||
messages={messages}
|
||
historyLoadingMore={messageHistoryLoadingMore}
|
||
historyHasMore={messageHistoryHasMore}
|
||
historyTotal={messageHistoryTotal}
|
||
chatState={chatState}
|
||
pendingTool={pendingTool}
|
||
session={session}
|
||
capabilities={capabilities}
|
||
grantedSkills={grantedSkills}
|
||
onSubmit={(text, imageUrls, previewImageUrls) =>
|
||
void submit(text, undefined, imageUrls, previewImageUrls)
|
||
}
|
||
onUploadImage={uploadChatImage}
|
||
onLoadOlderMessages={loadOlderMessages}
|
||
onStop={stop}
|
||
onApproveTool={approveTool}
|
||
onPageSaved={(result) => {
|
||
if (result.kind === 'page' && result.pageId) {
|
||
onOpenPage?.(result.pageId);
|
||
return;
|
||
}
|
||
onOpenSpace?.({ categoryCode: result.categoryCode });
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|