Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useChat } from '../context/ChatProvider';
|
||||
import type { CapabilityMap, PortalUser } from '../types';
|
||||
import { useNetworkStatus } from '../hooks/useNetworkStatus';
|
||||
import { getSessionDisplayName } from '../utils/sessions';
|
||||
import { openAvatarPicker } from '../utils/userAvatar';
|
||||
import { HistorySidebar } from './HistorySidebar';
|
||||
import { TKMindAvatar } from './TKMindAvatar';
|
||||
import { ChatPanel } from './ChatPanel';
|
||||
import { BalanceRing } from './BalanceRing';
|
||||
import type { MindSpaceSaveCategory } from '../types';
|
||||
|
||||
export function ChatView({
|
||||
user,
|
||||
capabilities,
|
||||
grantedSkills,
|
||||
onLogout,
|
||||
onOpenSpace,
|
||||
onOpenPage,
|
||||
onOpenAdmin,
|
||||
}: {
|
||||
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;
|
||||
}) {
|
||||
const {
|
||||
session,
|
||||
sessions,
|
||||
sessionsLoading,
|
||||
messages,
|
||||
chatState,
|
||||
error,
|
||||
notice,
|
||||
pendingTool,
|
||||
memoryLoading,
|
||||
submit,
|
||||
stop,
|
||||
approveTool,
|
||||
newSession,
|
||||
rememberCurrentContext,
|
||||
refreshProjectMemory,
|
||||
switchSession,
|
||||
retryConnect,
|
||||
dismissNotice,
|
||||
onSidebarOpen,
|
||||
workingDir,
|
||||
balanceCents,
|
||||
totalCreditCents,
|
||||
openRecharge,
|
||||
} = useChat();
|
||||
const online = useNetworkStatus();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebarOpen) {
|
||||
onSidebarOpen();
|
||||
}
|
||||
}, [sidebarOpen, onSidebarOpen]);
|
||||
|
||||
const busy = chatState === 'streaming' || chatState === 'loading';
|
||||
|
||||
const handleSelectSession = (sessionId: string) => {
|
||||
void switchSession(sessionId);
|
||||
setSidebarOpen(false);
|
||||
};
|
||||
|
||||
const handleNewSession = () => {
|
||||
void newSession();
|
||||
setSidebarOpen(false);
|
||||
};
|
||||
|
||||
const sessionTitle = session ? getSessionDisplayName(session) : '连接中…';
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<HistorySidebar
|
||||
open={sidebarOpen}
|
||||
sessions={sessions}
|
||||
activeSessionId={session?.id}
|
||||
loading={sessionsLoading}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
onSelect={handleSelectSession}
|
||||
onNew={handleNewSession}
|
||||
/>
|
||||
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<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">{sessionTitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
{typeof balanceCents === 'number' && (
|
||||
<BalanceRing
|
||||
balanceCents={balanceCents}
|
||||
totalCreditCents={totalCreditCents}
|
||||
onRecharge={openRecharge}
|
||||
/>
|
||||
)}
|
||||
{onOpenAdmin && (
|
||||
<button type="button" className="ghost-btn" onClick={onOpenAdmin}>
|
||||
管理
|
||||
</button>
|
||||
)}
|
||||
{onOpenSpace && (
|
||||
<button type="button" className="ghost-btn" onClick={() => onOpenSpace?.()}>
|
||||
我的空间
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || memoryLoading || messages.length === 0}
|
||||
onClick={() => void rememberCurrentContext()}
|
||||
title="把最近几条对话保存到 harness 项目知识库"
|
||||
>
|
||||
记住本轮
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || memoryLoading}
|
||||
onClick={() => void refreshProjectMemory()}
|
||||
title="从 harness 和项目文件重新加载长期记忆"
|
||||
>
|
||||
{memoryLoading ? '同步中…' : '刷新记忆'}
|
||||
</button>
|
||||
<button type="button" className="ghost-btn" onClick={() => void handleNewSession()}>
|
||||
新会话
|
||||
</button>
|
||||
{onLogout && (
|
||||
<button type="button" className="ghost-btn logout-btn" onClick={onLogout}>
|
||||
登出
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!online && (
|
||||
<div className="banner banner-offline">网络已断开,请检查连接后重试</div>
|
||||
)}
|
||||
|
||||
{typeof balanceCents === 'number' && balanceCents <= 0 && (
|
||||
<div className="banner banner-error">
|
||||
<span>余额不足,请充值后继续使用</span>
|
||||
<button type="button" className="banner-action" onClick={() => openRecharge(true)}>
|
||||
去充值
|
||||
</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>
|
||||
)}
|
||||
|
||||
{user?.publishUrl && (
|
||||
<div className="banner banner-info publish-banner">
|
||||
<span>
|
||||
你的发布目录:
|
||||
<a href={user.publishUrl} target="_blank" rel="noreferrer">
|
||||
{user.publishUrl}
|
||||
</a>
|
||||
{Boolean(capabilities?.static_publish) || (grantedSkills?.includes(user.publishSkillName ?? 'static-page-publish') ?? false)
|
||||
? '(已开通静态页面技能,生成后可直接访问)'
|
||||
: '(需管理员在「技能配置」中勾选 static-page-publish)'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notice && (
|
||||
<div className="banner banner-info">
|
||||
<span>{notice}</span>
|
||||
<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}
|
||||
chatState={chatState}
|
||||
pendingTool={pendingTool}
|
||||
session={session}
|
||||
capabilities={capabilities}
|
||||
grantedSkills={grantedSkills}
|
||||
onSubmit={submit}
|
||||
onStop={stop}
|
||||
onApproveTool={approveTool}
|
||||
onPageSaved={(result) => {
|
||||
if (result.kind === 'page' && result.pageId) {
|
||||
onOpenPage?.(result.pageId);
|
||||
return;
|
||||
}
|
||||
onOpenSpace?.({ categoryCode: result.categoryCode });
|
||||
}}
|
||||
/>
|
||||
|
||||
{workingDir && <div className="meta">工作目录:{workingDir}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user