Files
ai-terminal-assistant/packages/web/src/pages/Chat.tsx
T
kurihada ddbd56a0ac feat(ui): 添加工具栏溢出菜单优化响应式布局
- 新增 DropdownMenu 基础组件(基于 Radix UI)
- 新增 ToolbarOverflowMenu 组件(齿轮图标设置菜单)
- 将 Header 次要按钮收入溢出菜单
- 保留 Diagnostics 和 Sessions 按钮始终可见
- 溢出菜单放置在最右侧
2025-12-17 17:52:20 +08:00

242 lines
7.3 KiB
TypeScript

/**
* Chat Page
*/
import { useEffect, useRef } from 'react';
import { MessageSquare, Terminal, Plug, Zap, Bot, History, Server, MessagesSquare } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { toast } from 'sonner';
import {
useChat,
ChatMessage,
TypingIndicator,
ChatInput,
PermissionDialog,
ContextUsage,
SubagentProgress,
DiagnosticsIndicator,
ToolbarOverflowMenu,
} from '@ai-assistant/ui';
interface ChatPageProps {
sessionId: string;
onSessionNotFound?: () => void;
onSessionUpdated?: (sessionId: string, name: string) => void;
responsive?: boolean;
// 工具栏按钮
onOpenCommands?: () => void;
onOpenMCP?: () => void;
onOpenHooks?: () => void;
onOpenAgents?: () => void;
onOpenCheckpoints?: () => void;
onOpenProviders?: () => void;
onOpenLSP?: () => void;
onOpenDiagnostics?: () => void;
onOpenSessions?: () => void;
}
export function ChatPage({
sessionId,
onSessionNotFound,
onSessionUpdated,
responsive = false,
onOpenCommands,
onOpenMCP,
onOpenHooks,
onOpenAgents,
onOpenCheckpoints,
onOpenProviders,
onOpenLSP,
onOpenDiagnostics,
onOpenSessions,
}: ChatPageProps) {
const {
messages,
isConnected,
isLoading,
streamingMessage,
sendMessage,
cancelProcessing,
permissionRequest,
allowPermission,
denyPermission,
agentMode,
autoApprove,
setAgentMode,
setAutoApprove,
currentAgent,
currentSubagent,
answerQuestion,
} = useChat({
sessionId,
onError: (error) => {
console.error('Chat error:', error);
},
onSessionNotFound,
onSessionUpdated,
onConfigError: (error) => {
toast.error(error.message, {
duration: 10000,
action: onOpenProviders
? {
label: '去配置',
onClick: onOpenProviders,
}
: undefined,
});
},
});
const messagesEndRef = useRef<HTMLDivElement>(null);
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, streamingMessage]);
// 空状态组件
const EmptyState = () => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="text-center py-20"
>
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-gradient-to-br from-primary-500/20 to-primary-600/20 flex items-center justify-center">
<MessageSquare size={32} className="text-primary-400" />
</div>
<h2 className="text-2xl font-semibold mb-2 text-fg">
Start a conversation
</h2>
<p className="text-fg-muted mb-6 max-w-md mx-auto">
Ask me anything about coding, debugging, or software development.
</p>
<div className="flex flex-wrap justify-center gap-2">
{['Help me debug this code', 'Explain this function', 'Write a test'].map((suggestion) => (
<motion.button
key={suggestion}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => sendMessage(suggestion)}
className="px-3 py-1.5 bg-surface-subtle hover:bg-surface-muted rounded-full text-sm text-fg-secondary transition-colors"
>
"{suggestion}"
</motion.button>
))}
</div>
</motion.div>
);
return (
<div className="flex-1 flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-line bg-surface-subtle">
<h1 className="text-lg font-medium text-fg">Chat</h1>
<div className="flex items-center gap-3 flex-shrink-0">
{/* 上下文使用情况 - 紧凑模式 */}
{sessionId && (
<ContextUsage
sessionId={sessionId}
compact
showCompressButton
refreshInterval={30000}
/>
)}
{/* 工具栏按钮 */}
{(onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders || onOpenLSP || onOpenDiagnostics || onOpenSessions) && (
<div className="flex items-center gap-1.5 border-l border-line-muted pl-3">
{/* LSP 诊断指示器 */}
{(onOpenLSP || onOpenDiagnostics) && (
<DiagnosticsIndicator
onClickDiagnostics={onOpenDiagnostics}
onClickLSP={onOpenLSP}
refreshInterval={30000}
/>
)}
{/* Sessions 按钮 */}
{onOpenSessions && (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onOpenSessions}
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
title="Sessions"
>
<MessagesSquare size={20} />
</motion.button>
)}
{/* 设置菜单 - 齿轮图标,放在最右侧 */}
<ToolbarOverflowMenu
items={[
{ icon: History, label: 'Checkpoints', onClick: onOpenCheckpoints },
{ icon: Server, label: 'Model Providers', onClick: onOpenProviders },
{ icon: Bot, label: 'Agent Presets', onClick: onOpenAgents },
{ icon: Zap, label: 'Hooks', onClick: onOpenHooks },
{ icon: Plug, label: 'MCP Servers', onClick: onOpenMCP },
{ icon: Terminal, label: 'Commands', onClick: onOpenCommands },
]}
/>
</div>
)}
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-4xl mx-auto space-y-4">
{messages.length === 0 && !isLoading && <EmptyState />}
<AnimatePresence mode="popLayout">
{messages.map((message) => (
<ChatMessage key={message.id} message={message} onAnswerQuestion={answerQuestion} />
))}
</AnimatePresence>
{/* 流式消息 - 复用 ChatMessage 组件 */}
{streamingMessage && (
<ChatMessage message={streamingMessage} isStreaming onAnswerQuestion={answerQuestion} />
)}
{/* 子 Agent 进度显示 */}
{currentSubagent && (
<SubagentProgress subagent={currentSubagent} />
)}
{isLoading && !streamingMessage && !currentSubagent && <TypingIndicator agentName={currentAgent} />}
<div ref={messagesEndRef} />
</div>
</div>
{/* Input */}
<ChatInput
onSend={sendMessage}
onCancel={cancelProcessing}
isLoading={isLoading}
disabled={!isConnected}
responsive={responsive}
agentMode={agentMode}
onAgentModeChange={setAgentMode}
autoApprove={autoApprove}
onAutoApproveChange={setAutoApprove}
/>
{/* Permission Dialog */}
{permissionRequest && (
<PermissionDialog
request={permissionRequest}
onAllow={(requestId, remember) => allowPermission(requestId, remember)}
onDeny={(requestId, remember) => denyPermission(requestId, remember)}
responsive={responsive}
/>
)}
</div>
);
}