Files
ai-terminal-assistant/packages/web/src/pages/Chat.tsx
T
kurihada cb554c65b4 feat(checkpoint): 添加 Checkpoint 可视化管理功能
Core 层增强:
- 添加 safety.ts: 7点安全检查机制
- 添加 session-tracker.ts: 会话级检查点跟踪
- 添加 lock.ts: 并发控制文件锁
- 添加 lfs.ts: Git LFS 大文件支持
- 添加 path-validator.ts: 路径验证
- 添加 commit-message.ts: 智能提交消息生成
- 增强 manager.ts: 支持三种恢复模式、unrevert 撤销回滚

Server 层:
- 添加 checkpoints.ts: 16个 REST API 端点
  - GET/POST /checkpoints: 列表/创建检查点
  - GET/DELETE /checkpoints/🆔 获取/删除检查点
  - GET /checkpoints/:id/diff: 获取差异
  - POST /checkpoints/:id/restore: 恢复到检查点
  - POST /checkpoints/unrevert: 撤销回滚
  - GET /checkpoints/:id/safety-check: 安全检查

UI 层:
- 添加 CheckpointPanel.tsx: 检查点列表面板
- 添加 CheckpointDiffViewer.tsx: 差异查看器
- 添加 RestoreDialog.tsx: 恢复确认对话框
- 添加 16 个 API 客户端函数
- 添加完整的 TypeScript 类型定义

Web/Desktop 集成:
- 添加 History 按钮到工具栏
- 集成 CheckpointPanel 组件
2025-12-12 22:52:27 +08:00

270 lines
8.7 KiB
TypeScript

/**
* Chat Page
*/
import { useEffect, useRef } from 'react';
import { WifiOff, MessageSquare, Settings, FolderOpen, Terminal, Plug, Zap, Bot, History } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import {
useChat,
ChatMessage,
StreamingMessage,
TypingIndicator,
ChatInput,
} from '@ai-assistant/ui';
interface ChatPageProps {
sessionId: string;
onSessionNotFound?: () => void;
onSessionUpdated?: (sessionId: string, name: string) => void;
responsive?: boolean;
// 工具栏按钮
showFileBrowser?: boolean;
onToggleFileBrowser?: () => void;
onOpenConfig?: () => void;
onOpenCommands?: () => void;
onOpenMCP?: () => void;
onOpenHooks?: () => void;
onOpenAgents?: () => void;
onOpenCheckpoints?: () => void;
}
export function ChatPage({
sessionId,
onSessionNotFound,
onSessionUpdated,
responsive = false,
showFileBrowser,
onToggleFileBrowser,
onOpenConfig,
onOpenCommands,
onOpenMCP,
onOpenHooks,
onOpenAgents,
onOpenCheckpoints,
}: ChatPageProps) {
const {
messages,
isConnected,
isLoading,
streamingContent,
sendMessage,
cancelProcessing,
} = useChat({
sessionId,
onError: (error) => {
console.error('Chat error:', error);
},
onSessionNotFound,
onSessionUpdated,
});
const messagesEndRef = useRef<HTMLDivElement>(null);
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, streamingContent]);
// 空状态组件
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 bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
Start a conversation
</h2>
<p className="text-gray-400 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-gray-800 hover:bg-gray-700 rounded-full text-sm text-gray-300 transition-colors"
>
"{suggestion}"
</motion.button>
))}
</div>
</motion.div>
);
// 连接状态指示器
const ConnectionStatus = () => (
<div className="flex items-center gap-1.5 text-sm">
{isConnected ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center gap-1.5"
>
<span className="relative flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" />
</span>
<span className="text-green-400 hidden sm:inline">Connected</span>
</motion.div>
) : (
<div className="flex items-center gap-1.5 text-red-400">
<WifiOff size={16} />
<span className="hidden sm:inline">Disconnected</span>
</div>
)}
</div>
);
return (
<div className="flex-1 flex flex-col h-screen">
{/* Header */}
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-gray-700 bg-gray-800">
<h1 className="text-lg font-medium">Chat</h1>
<div className="flex items-center gap-3">
{/* 连接状态 */}
<ConnectionStatus />
{/* 工具栏按钮 */}
{(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints) && (
<div className="flex items-center gap-1.5 border-l border-gray-600 pl-3">
{/* Checkpoints 按钮 */}
{onOpenCheckpoints && (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onOpenCheckpoints}
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
title="Checkpoints"
>
<History 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-gray-400 hover:text-gray-200 hover:bg-gray-700 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-gray-400 hover:text-gray-200 hover:bg-gray-700 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-gray-400 hover:text-gray-200 hover:bg-gray-700 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-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
title="Commands"
>
<Terminal size={20} />
</motion.button>
)}
{/* 配置按钮 */}
{onOpenConfig && (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onOpenConfig}
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
title="Settings"
>
<Settings size={20} />
</motion.button>
)}
{/* 文件浏览器按钮 - 仅桌面端显示 */}
{onToggleFileBrowser && (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onToggleFileBrowser}
className={`hidden md:block p-1.5 rounded-lg transition-colors ${
showFileBrowser
? 'text-blue-400 bg-blue-500/20'
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
}`}
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
>
<FolderOpen 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} />
))}
</AnimatePresence>
{streamingContent && <StreamingMessage content={streamingContent} />}
{isLoading && !streamingContent && <TypingIndicator />}
<div ref={messagesEndRef} />
</div>
</div>
{/* Input */}
<ChatInput
onSend={sendMessage}
onCancel={cancelProcessing}
isLoading={isLoading}
disabled={!isConnected}
responsive={responsive}
/>
</div>
);
}