babe65719b
- ContextUsage: 紧凑模式下始终显示压缩按钮 - StatusBar: 连接状态移至中间位置,显示绿点动画和文字 - StatusBar: 添加内部连接状态检测(通过 health API) - Chat: 移除 Header 中的连接状态指示器
307 lines
9.6 KiB
TypeScript
307 lines
9.6 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,
|
|
} 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}
|
|
/>
|
|
)}
|
|
|
|
{/* Checkpoints 按钮 */}
|
|
{onOpenCheckpoints && (
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={onOpenCheckpoints}
|
|
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
|
title="Checkpoints"
|
|
>
|
|
<History size={20} />
|
|
</motion.button>
|
|
)}
|
|
|
|
{/* Providers 按钮 */}
|
|
{onOpenProviders && (
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={onOpenProviders}
|
|
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
|
title="Model Providers"
|
|
>
|
|
<Server size={20} />
|
|
</motion.button>
|
|
)}
|
|
|
|
{/* Agents 按钮 */}
|
|
{onOpenAgents && (
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={onOpenAgents}
|
|
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
|
title="Agent Presets"
|
|
>
|
|
<Bot size={20} />
|
|
</motion.button>
|
|
)}
|
|
|
|
{/* Hooks 按钮 */}
|
|
{onOpenHooks && (
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={onOpenHooks}
|
|
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
|
title="Hooks"
|
|
>
|
|
<Zap size={20} />
|
|
</motion.button>
|
|
)}
|
|
|
|
{/* MCP 按钮 */}
|
|
{onOpenMCP && (
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={onOpenMCP}
|
|
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
|
title="MCP Servers"
|
|
>
|
|
<Plug size={20} />
|
|
</motion.button>
|
|
)}
|
|
|
|
{/* 命令按钮 */}
|
|
{onOpenCommands && (
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={onOpenCommands}
|
|
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
|
title="Commands"
|
|
>
|
|
<Terminal size={20} />
|
|
</motion.button>
|
|
)}
|
|
|
|
{/* 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>
|
|
)}
|
|
</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>
|
|
);
|
|
}
|