2026-07-03 00:34:03 +08:00
|
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import { useChat } from '../context/ChatProvider';
|
2026-06-15 22:09:38 -07:00
|
|
|
|
import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat';
|
2026-07-03 00:34:03 +08:00
|
|
|
|
import type { CapabilityMap, PortalUser, SessionSummary } from '../types';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import { useNetworkStatus } from '../hooks/useNetworkStatus';
|
|
|
|
|
|
import { getSessionDisplayName } from '../utils/sessions';
|
2026-07-03 23:21:50 +08:00
|
|
|
|
import { isH5OrWechatClient } from '../utils/wechat';
|
2026-06-15 22:09:38 -07:00
|
|
|
|
import { BalanceRing } from './BalanceRing';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import { HistorySidebar } from './HistorySidebar';
|
|
|
|
|
|
import { TKMindAvatar } from './TKMindAvatar';
|
|
|
|
|
|
import { ChatPanel } from './ChatPanel';
|
2026-06-30 09:30:51 +08:00
|
|
|
|
import { ChatLoadingSpinner } from './ChatLoadingSpinner';
|
2026-07-03 00:34:03 +08:00
|
|
|
|
import { ConversationPackagePanel } from './ConversationPackagePanel';
|
2026-06-15 15:47:19 -07:00
|
|
|
|
import { WechatBindPrompt } from './WechatBindPrompt';
|
|
|
|
|
|
import { WechatAccountButton } from './WechatAccountButton';
|
2026-06-15 22:09:38 -07:00
|
|
|
|
import { ChatHeaderMoreMenu } from './ChatHeaderMoreMenu';
|
2026-06-26 15:19:03 +08:00
|
|
|
|
import { NotificationCenter } from './NotificationCenter';
|
2026-06-15 15:04:43 -07:00
|
|
|
|
import type { MindSpaceSaveCategory } from '../types';
|
|
|
|
|
|
|
2026-07-03 00:34:03 +08:00
|
|
|
|
function RecentSessionRail({
|
|
|
|
|
|
sessions,
|
|
|
|
|
|
activeSessionId,
|
|
|
|
|
|
armedSessionId,
|
|
|
|
|
|
loading,
|
|
|
|
|
|
loadingMore,
|
|
|
|
|
|
hasMore,
|
|
|
|
|
|
chipWidth,
|
2026-07-03 23:21:50 +08:00
|
|
|
|
variant = 'inline',
|
2026-07-03 00:34:03 +08:00
|
|
|
|
onSelect,
|
|
|
|
|
|
onLoadMore,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
sessions: SessionSummary[];
|
|
|
|
|
|
activeSessionId?: string | null;
|
|
|
|
|
|
armedSessionId?: string | null;
|
|
|
|
|
|
loading: boolean;
|
|
|
|
|
|
loadingMore: boolean;
|
|
|
|
|
|
hasMore: boolean;
|
|
|
|
|
|
chipWidth?: number | null;
|
2026-07-03 23:21:50 +08:00
|
|
|
|
variant?: 'inline' | 'floating';
|
2026-07-03 00:34:03 +08:00
|
|
|
|
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<HTMLDivElement | null>(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;
|
|
|
|
|
|
|
2026-07-03 23:21:50 +08:00
|
|
|
|
const isFloating = variant === 'floating';
|
|
|
|
|
|
|
2026-07-03 00:34:03 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div
|
2026-07-03 23:21:50 +08:00
|
|
|
|
className={`header-session-rail${isFloating ? ' header-session-rail-floating' : ''}`}
|
2026-07-03 00:34:03 +08:00
|
|
|
|
aria-label="最近对话入口"
|
|
|
|
|
|
style={
|
2026-07-03 23:21:50 +08:00
|
|
|
|
!isFloating && chipWidth && chipWidth > 0
|
2026-07-03 00:34:03 +08:00
|
|
|
|
? ({ ['--header-session-chip-width' as string]: `${chipWidth}px` })
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="header-session-rail-arrow header-session-rail-arrow-left"
|
|
|
|
|
|
aria-label="查看更早的最近对话"
|
|
|
|
|
|
disabled={!canScrollLeft}
|
|
|
|
|
|
onClick={() => scrollByPage(-1)}
|
|
|
|
|
|
>
|
|
|
|
|
|
‹
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="header-session-rail-track"
|
|
|
|
|
|
ref={railRef}
|
|
|
|
|
|
>
|
|
|
|
|
|
{recentSessions.map((item) => {
|
|
|
|
|
|
const active = item.id === activeSessionId;
|
|
|
|
|
|
const armed = item.id === armedSessionId;
|
|
|
|
|
|
const displayName = getSessionDisplayName(item);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={item.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={`header-session-chip${active ? ' is-active' : ''}${armed ? ' is-armed' : ''}`}
|
|
|
|
|
|
aria-pressed={active || armed}
|
|
|
|
|
|
title={displayName}
|
|
|
|
|
|
onClick={() => onSelect(item)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>{compactLabel(displayName, 8)}</strong>
|
|
|
|
|
|
<span>{armed ? '再次点击' : item.message_count > 0 ? `${item.message_count}条` : '继续'}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
{(loading || loadingMore) && (
|
|
|
|
|
|
<div className="header-session-chip header-session-chip-loading" aria-hidden="true">
|
|
|
|
|
|
<strong>{loading ? '加载中…' : '继续加载…'}</strong>
|
|
|
|
|
|
<span>{loading ? '正在准备最近对话' : '滑到这里会自动接上更多'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="header-session-rail-arrow header-session-rail-arrow-right"
|
|
|
|
|
|
aria-label="查看更多最近对话"
|
|
|
|
|
|
disabled={!canScrollRight}
|
|
|
|
|
|
onClick={() => scrollByPage(1)}
|
|
|
|
|
|
>
|
|
|
|
|
|
›
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
export function ChatView({
|
|
|
|
|
|
user,
|
|
|
|
|
|
capabilities,
|
|
|
|
|
|
grantedSkills,
|
|
|
|
|
|
onLogout,
|
|
|
|
|
|
onOpenSpace,
|
|
|
|
|
|
onOpenPage,
|
|
|
|
|
|
onOpenAdmin,
|
2026-06-29 20:49:15 +08:00
|
|
|
|
onOpenFeedback,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
}: {
|
|
|
|
|
|
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;
|
2026-06-29 20:49:15 +08:00
|
|
|
|
onOpenFeedback?: () => void;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
}) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
session,
|
|
|
|
|
|
sessions,
|
|
|
|
|
|
sessionsLoading,
|
2026-06-29 20:49:15 +08:00
|
|
|
|
sessionsLoadingMore,
|
|
|
|
|
|
sessionsHasMore,
|
|
|
|
|
|
sessionSearchQuery,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
messages,
|
2026-06-29 20:49:15 +08:00
|
|
|
|
messageHistoryLoadingMore,
|
|
|
|
|
|
messageHistoryHasMore,
|
|
|
|
|
|
messageHistoryTotal,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
chatState,
|
|
|
|
|
|
error,
|
|
|
|
|
|
notice,
|
|
|
|
|
|
pendingTool,
|
|
|
|
|
|
submit,
|
|
|
|
|
|
stop,
|
|
|
|
|
|
approveTool,
|
|
|
|
|
|
newSession,
|
|
|
|
|
|
switchSession,
|
2026-06-15 22:09:38 -07:00
|
|
|
|
deleteSession,
|
2026-06-29 20:49:15 +08:00
|
|
|
|
loadMoreSessions,
|
|
|
|
|
|
loadOlderMessages,
|
|
|
|
|
|
setSessionSearchQuery,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
retryConnect,
|
|
|
|
|
|
dismissNotice,
|
|
|
|
|
|
onSidebarOpen,
|
|
|
|
|
|
balanceCents,
|
|
|
|
|
|
totalCreditCents,
|
2026-06-15 22:09:38 -07:00
|
|
|
|
tokensUsed,
|
2026-06-26 15:19:03 +08:00
|
|
|
|
subscription,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
openRecharge,
|
2026-06-26 15:19:03 +08:00
|
|
|
|
openSubscribe,
|
2026-06-19 23:06:43 +08:00
|
|
|
|
uploadChatImage,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
} = useChat();
|
|
|
|
|
|
const online = useNetworkStatus();
|
|
|
|
|
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
2026-07-03 00:34:03 +08:00
|
|
|
|
const [packagePanelSession, setPackagePanelSession] = useState<SessionSummary | null>(null);
|
|
|
|
|
|
const [armedSessionId, setArmedSessionId] = useState<string | null>(null);
|
|
|
|
|
|
const [desktopChipWidth, setDesktopChipWidth] = useState<number | null>(null);
|
|
|
|
|
|
const spaceButtonRef = useRef<HTMLButtonElement | null>(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]);
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (sidebarOpen) {
|
|
|
|
|
|
onSidebarOpen();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [sidebarOpen, onSidebarOpen]);
|
|
|
|
|
|
|
2026-06-27 21:57:21 +08:00
|
|
|
|
const showHomeWelcome = messages.length === 0;
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
const handleSelectSession = (sessionId: string) => {
|
|
|
|
|
|
void switchSession(sessionId);
|
|
|
|
|
|
setSidebarOpen(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-03 00:34:03 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
const handleNewSession = () => {
|
|
|
|
|
|
void newSession();
|
|
|
|
|
|
setSidebarOpen(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-30 09:30:51 +08:00
|
|
|
|
const isConnectingTitle = !session && chatState !== 'idle';
|
2026-07-03 00:34:03 +08:00
|
|
|
|
const pendingSessionTitle =
|
|
|
|
|
|
chatState === 'connecting'
|
|
|
|
|
|
? '正在创建新对话…'
|
|
|
|
|
|
: chatState === 'waiting'
|
|
|
|
|
|
? '请求已发出…'
|
|
|
|
|
|
: '连接中…';
|
2026-06-15 22:09:38 -07:00
|
|
|
|
const sessionTitle = session
|
|
|
|
|
|
? getSessionDisplayName(session)
|
2026-06-27 21:57:21 +08:00
|
|
|
|
: chatState === 'idle'
|
2026-06-15 22:09:38 -07:00
|
|
|
|
? '新对话'
|
2026-07-03 00:34:03 +08:00
|
|
|
|
: pendingSessionTitle;
|
2026-06-15 22:09:38 -07:00
|
|
|
|
|
|
|
|
|
|
const moreMenuItems = [
|
|
|
|
|
|
...(onOpenAdmin
|
|
|
|
|
|
? [{ id: 'admin', label: '管理', onClick: () => onOpenAdmin() }]
|
|
|
|
|
|
: []),
|
|
|
|
|
|
...(onLogout
|
|
|
|
|
|
? [{ id: 'logout', label: '登出', onClick: () => onLogout(), danger: true }]
|
|
|
|
|
|
: []),
|
|
|
|
|
|
];
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
2026-07-03 23:21:50 +08:00
|
|
|
|
const useFloatingSessionRail = useMemo(() => isH5OrWechatClient(), []);
|
|
|
|
|
|
|
|
|
|
|
|
const recentSessionRailProps = {
|
|
|
|
|
|
sessions,
|
|
|
|
|
|
activeSessionId: session?.id,
|
|
|
|
|
|
armedSessionId,
|
|
|
|
|
|
loading: sessionsLoading,
|
|
|
|
|
|
loadingMore: sessionsLoadingMore,
|
|
|
|
|
|
hasMore: sessionsHasMore,
|
|
|
|
|
|
onSelect: handleRecentSessionSelect,
|
|
|
|
|
|
onLoadMore: () => void loadMoreSessions(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
return (
|
2026-07-03 23:21:50 +08:00
|
|
|
|
<div className={`app-shell${useFloatingSessionRail ? ' app-shell-h5' : ''}`}>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
<HistorySidebar
|
|
|
|
|
|
open={sidebarOpen}
|
|
|
|
|
|
sessions={sessions}
|
|
|
|
|
|
activeSessionId={session?.id}
|
|
|
|
|
|
loading={sessionsLoading}
|
2026-06-29 20:49:15 +08:00
|
|
|
|
loadingMore={sessionsLoadingMore}
|
|
|
|
|
|
hasMore={sessionsHasMore}
|
|
|
|
|
|
searchQuery={sessionSearchQuery}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
onClose={() => setSidebarOpen(false)}
|
|
|
|
|
|
onSelect={handleSelectSession}
|
|
|
|
|
|
onNew={handleNewSession}
|
2026-06-15 22:09:38 -07:00
|
|
|
|
onDelete={(sessionId) => void deleteSession(sessionId)}
|
2026-06-29 20:49:15 +08:00
|
|
|
|
onLoadMore={() => void loadMoreSessions()}
|
|
|
|
|
|
onSearchChange={setSessionSearchQuery}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
/>
|
|
|
|
|
|
|
2026-06-27 21:57:21 +08:00
|
|
|
|
<div className={`app${showHomeWelcome ? ' app-home' : ''}`}>
|
2026-07-03 23:21:50 +08:00
|
|
|
|
<div className={useFloatingSessionRail ? 'header-shell header-shell-sticky' : undefined}>
|
|
|
|
|
|
<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>
|
2026-06-30 09:30:51 +08:00
|
|
|
|
</div>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
</div>
|
2026-07-03 23:21:50 +08:00
|
|
|
|
<div className="header-actions header-actions-desktop">
|
|
|
|
|
|
{!useFloatingSessionRail && (
|
|
|
|
|
|
<RecentSessionRail
|
|
|
|
|
|
{...recentSessionRailProps}
|
|
|
|
|
|
chipWidth={desktopChipWidth}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
{typeof balanceCents === 'number' && (
|
|
|
|
|
|
<BalanceRing
|
|
|
|
|
|
balanceCents={balanceCents}
|
|
|
|
|
|
totalCreditCents={totalCreditCents}
|
2026-06-15 22:09:38 -07:00
|
|
|
|
tokensUsed={tokensUsed}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
subscription={subscription}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
onRecharge={openRecharge}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
onSubscribe={openSubscribe}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
<NotificationCenter onOpenRecharge={() => openRecharge(false)} />
|
2026-07-03 23:21:50 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="header-icon-btn"
|
|
|
|
|
|
aria-label="新聊天"
|
|
|
|
|
|
title="新聊天"
|
|
|
|
|
|
onClick={() => void handleNewSession()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M12 5v14M5 12h14"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="1.75"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
{onOpenAdmin && (
|
|
|
|
|
|
<button type="button" className="ghost-btn" onClick={onOpenAdmin}>
|
|
|
|
|
|
管理
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{onOpenSpace && (
|
2026-07-03 00:34:03 +08:00
|
|
|
|
<button
|
|
|
|
|
|
ref={spaceButtonRef}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="ghost-btn"
|
|
|
|
|
|
onClick={() => onOpenSpace?.()}
|
|
|
|
|
|
>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
我的空间
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-06-15 15:47:19 -07:00
|
|
|
|
{user && <WechatAccountButton returnTo={window.location.pathname} />}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
{onLogout && (
|
|
|
|
|
|
<button type="button" className="ghost-btn logout-btn" onClick={onLogout}>
|
|
|
|
|
|
登出
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-06-15 22:09:38 -07:00
|
|
|
|
|
|
|
|
|
|
<div className="header-actions header-actions-mobile">
|
2026-07-03 23:21:50 +08:00
|
|
|
|
{!useFloatingSessionRail && (
|
|
|
|
|
|
<RecentSessionRail
|
|
|
|
|
|
{...recentSessionRailProps}
|
|
|
|
|
|
chipWidth={null}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-06-15 22:09:38 -07:00
|
|
|
|
{typeof balanceCents === 'number' && (
|
|
|
|
|
|
<BalanceRing
|
|
|
|
|
|
balanceCents={balanceCents}
|
|
|
|
|
|
totalCreditCents={totalCreditCents}
|
|
|
|
|
|
tokensUsed={tokensUsed}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
subscription={subscription}
|
2026-06-15 22:09:38 -07:00
|
|
|
|
onRecharge={openRecharge}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
onSubscribe={openSubscribe}
|
2026-06-15 22:09:38 -07:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
<NotificationCenter onOpenRecharge={() => openRecharge(false)} />
|
2026-07-03 23:21:50 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="header-icon-btn"
|
|
|
|
|
|
aria-label="新聊天"
|
|
|
|
|
|
title="新聊天"
|
|
|
|
|
|
onClick={() => void handleNewSession()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M12 5v14M5 12h14"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="1.75"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
2026-06-15 22:09:38 -07:00
|
|
|
|
{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>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
</header>
|
2026-07-03 23:21:50 +08:00
|
|
|
|
{useFloatingSessionRail && (
|
|
|
|
|
|
<RecentSessionRail
|
|
|
|
|
|
{...recentSessionRailProps}
|
|
|
|
|
|
variant="floating"
|
|
|
|
|
|
chipWidth={null}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
{!online && (
|
|
|
|
|
|
<div className="banner banner-offline">网络已断开,请检查连接后重试</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-06-15 15:47:19 -07:00
|
|
|
|
{user && <WechatBindPrompt returnTo={window.location.pathname} />}
|
|
|
|
|
|
|
2026-06-15 15:04:43 -07:00
|
|
|
|
{typeof balanceCents === 'number' && balanceCents <= 0 && (
|
|
|
|
|
|
<div className="banner banner-error">
|
|
|
|
|
|
<span>余额不足,请充值后继续使用</span>
|
2026-06-15 22:09:38 -07:00
|
|
|
|
<button type="button" className="banner-action" onClick={() => openRecharge(false)}>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
去充值
|
|
|
|
|
|
</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 && (
|
2026-06-15 22:09:38 -07:00
|
|
|
|
<div className={`banner${notice === INSUFFICIENT_BALANCE_NOTICE ? ' banner-warning' : ' banner-info'}`}>
|
2026-06-26 15:19:03 +08:00
|
|
|
|
<span style={{ whiteSpace: 'pre-line' }}>{notice}</span>
|
2026-06-15 22:09:38 -07:00
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
</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}
|
2026-06-29 20:49:15 +08:00
|
|
|
|
historyLoadingMore={messageHistoryLoadingMore}
|
|
|
|
|
|
historyHasMore={messageHistoryHasMore}
|
|
|
|
|
|
historyTotal={messageHistoryTotal}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
chatState={chatState}
|
|
|
|
|
|
pendingTool={pendingTool}
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
capabilities={capabilities}
|
|
|
|
|
|
grantedSkills={grantedSkills}
|
2026-07-02 23:46:05 +08:00
|
|
|
|
onSubmit={(text, imageUrls, previewImageUrls, options) =>
|
2026-07-04 14:48:45 +08:00
|
|
|
|
void submit(
|
|
|
|
|
|
text,
|
2026-07-04 14:57:13 +08:00
|
|
|
|
{ messageId: options?.messageId, forceDeepReasoning: options?.forceDeepReasoning },
|
2026-07-04 14:48:45 +08:00
|
|
|
|
imageUrls,
|
|
|
|
|
|
previewImageUrls,
|
|
|
|
|
|
)
|
2026-07-02 23:46:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
onUploadImage={(file, onProgress, options) =>
|
|
|
|
|
|
uploadChatImage(file, onProgress, { messageId: options?.messageId })
|
2026-06-28 12:18:26 +08:00
|
|
|
|
}
|
2026-06-29 20:49:15 +08:00
|
|
|
|
onLoadOlderMessages={loadOlderMessages}
|
2026-06-15 15:04:43 -07:00
|
|
|
|
onStop={stop}
|
|
|
|
|
|
onApproveTool={approveTool}
|
|
|
|
|
|
onPageSaved={(result) => {
|
|
|
|
|
|
if (result.kind === 'page' && result.pageId) {
|
|
|
|
|
|
onOpenPage?.(result.pageId);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
onOpenSpace?.({ categoryCode: result.categoryCode });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-07-03 00:34:03 +08:00
|
|
|
|
<ConversationPackagePanel
|
|
|
|
|
|
sessionId={packagePanelSession?.id ?? null}
|
|
|
|
|
|
sessionName={packagePanelSession ? getSessionDisplayName(packagePanelSession) : null}
|
|
|
|
|
|
open={Boolean(packagePanelSession)}
|
|
|
|
|
|
onClose={() => {
|
|
|
|
|
|
setPackagePanelSession(null);
|
|
|
|
|
|
setArmedSessionId(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-06-15 15:04:43 -07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|