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
+186
View File
@@ -0,0 +1,186 @@
import { useEffect, useRef, useState } from 'react';
import { useNetworkStatus } from '../hooks/useNetworkStatus';
import { openAvatarPicker } from '../utils/userAvatar';
import { buildPublishSkillPrompt } from '../utils/publishSkill';
import { AvatarPicker } from './AvatarPicker';
import { MessageList } from './MessageList';
import { PageSaveDialog } from './PageSaveDialog';
import { VoiceInputButton } from './VoiceInputButton';
import type { CapabilityMap, ChatState, Message, PortalUser, Session, ToolConfirmation } from '../types';
import type { MindSpaceSaveCategory } from '../types';
export function ChatPanel({
variant,
user,
messages,
chatState,
pendingTool,
session,
capabilities,
grantedSkills,
onSubmit,
onStop,
onApproveTool,
onPageSaved,
}: {
variant: 'full' | 'compact';
user?: PortalUser | null;
messages: Message[];
chatState: ChatState;
pendingTool: ToolConfirmation | null;
session: Session | null;
capabilities?: CapabilityMap;
grantedSkills?: string[];
onSubmit: (text: string) => void | Promise<void>;
onStop: () => void | Promise<void>;
onApproveTool: (allow: boolean) => void | Promise<void>;
onPageSaved?: (result: {
kind: 'page' | 'category';
pageId?: string;
categoryCode?: MindSpaceSaveCategory;
}) => void;
}) {
const online = useNetworkStatus();
const [input, setInput] = useState('');
const [pageSource, setPageSource] = useState<Message | null>(null);
const [voiceNotice, setVoiceNotice] = useState<string | null>(null);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, chatState, pendingTool]);
const busy = chatState === 'streaming' || chatState === 'loading';
const offlineBlocked = !online;
const publishSkillName = user?.publishSkillName ?? 'static-page-publish';
const hasPublishSkill = grantedSkills?.includes(publishSkillName) ?? false;
const canPublish = Boolean(capabilities?.static_publish) || hasPublishSkill;
const compact = variant === 'compact';
const handleSubmit = async () => {
const text = input;
if (!text.trim()) return;
setInput('');
await onSubmit(text);
};
const placeholder = offlineBlocked
? '网络断开,恢复连接后可继续输入'
: compact
? '在这里继续和 Agent 对话…'
: '输入任务,例如:列出当前目录文件';
const handleVoiceSend = async (text: string) => {
setVoiceNotice(null);
await onSubmit(text);
};
const voiceDisabled = busy || !!pendingTool || chatState === 'error' || offlineBlocked;
return (
<>
<main className={compact ? 'space-chat-panel-body' : 'main'}>
<MessageList
messages={messages}
streaming={chatState === 'streaming'}
onAvatarClick={compact ? undefined : openAvatarPicker}
onSaveAsPage={(message) => setPageSource(message)}
publishUsername={user?.username}
compact={compact}
/>
{!compact && (
<div className="user-avatar-input-host" aria-hidden="true">
<AvatarPicker variant="compact" />
</div>
)}
<div ref={bottomRef} />
</main>
{pendingTool && (
<div className={`tool-confirm${compact ? ' tool-confirm-compact' : ''}`}>
<div className="tool-confirm-text">
允许执行工具 <strong>{pendingTool.toolName}</strong>?
{pendingTool.prompt && <p>{pendingTool.prompt}</p>}
</div>
<div className="tool-confirm-actions">
<button type="button" onClick={() => void onApproveTool(true)}>
允许
</button>
<button type="button" className="danger" onClick={() => void onApproveTool(false)}>
拒绝
</button>
</div>
</div>
)}
{pageSource?.id && session?.id && (
<PageSaveDialog
sessionId={session.id}
messageId={pageSource.id}
compact={compact}
onClose={() => setPageSource(null)}
onSaved={(result) => {
setPageSource(null);
onPageSaved?.(result);
}}
/>
)}
<footer className={compact ? 'space-chat-panel-footer' : 'footer'}>
{voiceNotice && (
<div className="voice-notice" role="status">
{voiceNotice}
</div>
)}
<div className="chat-input-row">
<VoiceInputButton
disabled={voiceDisabled}
onSend={(text) => void handleVoiceSend(text)}
onError={setVoiceNotice}
/>
<textarea
className="input"
rows={1}
placeholder={placeholder}
value={input}
disabled={voiceDisabled}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
void handleSubmit();
}
}}
/>
{!compact && canPublish && (
<button
type="button"
className="ghost-btn chat-input-action"
disabled={voiceDisabled}
title={`加载 ${publishSkillName} 技能并生成 HTML`}
onClick={() => {
setInput(buildPublishSkillPrompt(publishSkillName));
}}
>
静态页面
</button>
)}
{chatState === 'streaming' ? (
<button type="button" className="danger-btn chat-input-action" onClick={() => void onStop()}>
停止
</button>
) : (
<button
type="button"
className="send-btn chat-input-action"
disabled={!input.trim() || voiceDisabled}
onClick={() => void handleSubmit()}
>
发送
</button>
)}
</div>
</footer>
</>
);
}