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,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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user