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:
John
2026-06-15 15:04:43 -07:00
commit 2e14873f2d
272 changed files with 64133 additions and 0 deletions
+233
View File
@@ -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>
);
}