import { useEffect, useMemo, useRef, useState } from 'react'; import { useChat } from '../context/ChatProvider'; import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat'; import type { CapabilityMap, PortalUser, SessionSummary } from '../types'; import { useNetworkStatus } from '../hooks/useNetworkStatus'; import { getSessionDisplayName } from '../utils/sessions'; import { isH5OrWechatClient } from '../utils/wechat'; import { BalanceRing } from './BalanceRing'; import { HistorySidebar } from './HistorySidebar'; import { TKMindAvatar } from './TKMindAvatar'; import { ChatPanel } from './ChatPanel'; import { ChatLoadingSpinner } from './ChatLoadingSpinner'; import { ConversationPackagePanel } from './ConversationPackagePanel'; import { WechatBindPrompt } from './WechatBindPrompt'; import { WechatAccountButton } from './WechatAccountButton'; import { ChatHeaderMoreMenu } from './ChatHeaderMoreMenu'; import { NotificationCenter } from './NotificationCenter'; import type { MindSpaceSaveCategory } from '../types'; function RecentSessionRail({ sessions, activeSessionId, armedSessionId, loading, loadingMore, hasMore, chipWidth, variant = 'inline', onSelect, onLoadMore, }: { sessions: SessionSummary[]; activeSessionId?: string | null; armedSessionId?: string | null; loading: boolean; loadingMore: boolean; hasMore: boolean; chipWidth?: number | null; variant?: 'inline' | 'floating'; onSelect: (session: SessionSummary) => void; onLoadMore: () => void; }) { const compactLabel = (value: string, maxChars = 8) => { const trimmed = String(value ?? '').trim(); if (!trimmed) return '未命名'; return trimmed.length > maxChars ? `${trimmed.slice(0, maxChars)}...` : trimmed; }; const railRef = useRef(null); const recentSessions = useMemo(() => sessions, [sessions]); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); useEffect(() => { const node = railRef.current; if (!node) return; const updateScrollState = () => { const maxScrollLeft = Math.max(0, node.scrollWidth - node.clientWidth); setCanScrollLeft(node.scrollLeft > 8); setCanScrollRight(node.scrollLeft < maxScrollLeft - 8); }; updateScrollState(); node.addEventListener('scroll', updateScrollState, { passive: true }); window.addEventListener('resize', updateScrollState); return () => { node.removeEventListener('scroll', updateScrollState); window.removeEventListener('resize', updateScrollState); }; }, [recentSessions.length]); useEffect(() => { const node = railRef.current; if (!node) return; const maybeLoadMore = () => { if (!hasMore || loadingMore) return; const remaining = node.scrollWidth - (node.scrollLeft + node.clientWidth); if (remaining <= 96) onLoadMore(); }; maybeLoadMore(); node.addEventListener('scroll', maybeLoadMore, { passive: true }); return () => node.removeEventListener('scroll', maybeLoadMore); }, [hasMore, loadingMore, onLoadMore, recentSessions.length]); const scrollByPage = (direction: -1 | 1) => { const node = railRef.current; if (!node) return; const amount = Math.max(180, Math.round(node.clientWidth * 0.82)) * direction; node.scrollBy({ left: amount, behavior: 'smooth' }); }; if (!loading && recentSessions.length === 0) return null; const isFloating = variant === 'floating'; return (
0 ? ({ ['--header-session-chip-width' as string]: `${chipWidth}px` }) : undefined } >
{recentSessions.map((item) => { const active = item.id === activeSessionId; const armed = item.id === armedSessionId; const displayName = getSessionDisplayName(item); return ( ); })} {(loading || loadingMore) && ( )}
); } 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); const [packagePanelSession, setPackagePanelSession] = useState(null); const [armedSessionId, setArmedSessionId] = useState(null); const [desktopChipWidth, setDesktopChipWidth] = useState(null); const spaceButtonRef = useRef(null); useEffect(() => { const node = spaceButtonRef.current; if (!node) return; const updateWidth = () => { const nextWidth = Math.round(node.getBoundingClientRect().width); if (nextWidth > 0) setDesktopChipWidth(nextWidth); }; updateWidth(); const resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(updateWidth) : null; resizeObserver?.observe(node); window.addEventListener('resize', updateWidth); return () => { resizeObserver?.disconnect(); window.removeEventListener('resize', updateWidth); }; }, [onOpenSpace]); useEffect(() => { if (sidebarOpen) { onSidebarOpen(); } }, [sidebarOpen, onSidebarOpen]); const showHomeWelcome = messages.length === 0; const handleSelectSession = (sessionId: string) => { void switchSession(sessionId); setSidebarOpen(false); }; const handleOpenConversationPackage = (sessionSummary: SessionSummary) => { setPackagePanelSession(sessionSummary); }; const handleRecentSessionSelect = (sessionSummary: SessionSummary) => { const isSameSession = session?.id === sessionSummary.id; const isArmed = armedSessionId === sessionSummary.id; if (isSameSession && isArmed) { handleOpenConversationPackage(sessionSummary); setArmedSessionId(null); return; } setPackagePanelSession(null); setArmedSessionId(sessionSummary.id); if (!isSameSession) { handleSelectSession(sessionSummary.id); } }; const handleNewSession = () => { void newSession(); setSidebarOpen(false); }; const isConnectingTitle = !session && chatState !== 'idle'; const pendingSessionTitle = chatState === 'connecting' ? '正在创建新对话…' : chatState === 'waiting' ? '请求已发出…' : '连接中…'; const sessionTitle = session ? getSessionDisplayName(session) : chatState === 'idle' ? '新对话' : pendingSessionTitle; const moreMenuItems = [ ...(onOpenAdmin ? [{ id: 'admin', label: '管理', onClick: () => onOpenAdmin() }] : []), ...(onLogout ? [{ id: 'logout', label: '登出', onClick: () => onLogout(), danger: true }] : []), ]; const useFloatingSessionRail = useMemo(() => isH5OrWechatClient(), []); const recentSessionRailProps = { sessions, activeSessionId: session?.id, armedSessionId, loading: sessionsLoading, loadingMore: sessionsLoadingMore, hasMore: sessionsHasMore, onSelect: handleRecentSessionSelect, onLoadMore: () => void loadMoreSessions(), }; return (
setSidebarOpen(false)} onSelect={handleSelectSession} onNew={handleNewSession} onDelete={(sessionId) => void deleteSession(sessionId)} onLoadMore={() => void loadMoreSessions()} onSearchChange={setSessionSearchQuery} />
{user?.displayName ?? 'TKMind'}
{isConnectingTitle ? : null} {sessionTitle}
{!useFloatingSessionRail && ( )} {typeof balanceCents === 'number' && ( )} openRecharge(false)} /> {onOpenAdmin && ( )} {onOpenSpace && ( )} {user && } {onLogout && ( )}
{!useFloatingSessionRail && ( )} {typeof balanceCents === 'number' && ( )} openRecharge(false)} /> {onOpenSpace && ( )}
{useFloatingSessionRail && ( )}
{!online && (
网络已断开,请检查连接后重试
)} {user && } {typeof balanceCents === 'number' && balanceCents <= 0 && (
余额不足,请充值后继续使用
)} {typeof balanceCents === 'number' && balanceCents > 0 && balanceCents <= 100 && (
余额较低(¥{(balanceCents / 100).toFixed(2)}),建议提前充值
)} {notice && (
{notice} {notice === INSUFFICIENT_BALANCE_NOTICE ? ( <> ) : ( )}
)} {error && (
{error}
)} void submit( text, { messageId: options?.messageId, forceDeepReasoning: options?.forceDeepReasoning }, imageUrls, previewImageUrls, ) } onUploadImage={(file, onProgress, options) => uploadChatImage(file, onProgress, { messageId: options?.messageId }) } onLoadOlderMessages={loadOlderMessages} onStop={stop} onApproveTool={approveTool} onPageSaved={(result) => { if (result.kind === 'page' && result.pageId) { onOpenPage?.(result.pageId); return; } onOpenSpace?.({ categoryCode: result.categoryCode }); }} /> { setPackagePanelSession(null); setArmedSessionId(null); }} />
); }