Files
ai-terminal-assistant/packages/web/src/App.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

207 lines
6.9 KiB
TypeScript

/**
* App Component
*
* 响应式布局:支持桌面端和移动端
*/
import { useState, useEffect, useCallback } from 'react';
import {
Sidebar,
FileBrowser,
ConfigPanel,
CommandPanel,
MCPPanel,
HooksPanel,
AgentsPanel,
CheckpointPanel,
Toaster,
listSessions,
createSession,
type Session,
} from '@ai-assistant/ui';
import { ChatPage } from './pages/Chat';
export function App() {
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
const [isInitializing, setIsInitializing] = useState(true);
const [showFileBrowser, setShowFileBrowser] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [showCommands, setShowCommands] = useState(false);
const [showMCP, setShowMCP] = useState(false);
const [showHooks, setShowHooks] = useState(false);
const [showAgents, setShowAgents] = useState(false);
const [showCheckpoints, setShowCheckpoints] = useState(false);
const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null);
// 初始化:加载或创建会话
useEffect(() => {
async function init() {
try {
const { data: sessions } = await listSessions();
if (sessions.length > 0) {
// 选择最近的会话
setCurrentSessionId(sessions[0].id);
} else {
// 创建新会话
const { data: newSession } = await createSession();
setCurrentSessionId(newSession.id);
}
} catch (error) {
console.error('Failed to initialize:', error);
} finally {
setIsInitializing(false);
}
}
init();
}, []);
const handleSelectSession = (id: string) => {
setCurrentSessionId(id);
};
const handleCreateSession = (session: Session) => {
setCurrentSessionId(session.id);
};
// 会话不存在时自动创建新会话
const handleSessionNotFound = useCallback(async () => {
try {
const { data: newSession } = await createSession();
setCurrentSessionId(newSession.id);
} catch (error) {
console.error('Failed to create new session:', error);
}
}, []);
// 会话标题更新回调
const handleSessionUpdated = useCallback((sessionId: string, name: string) => {
setSessionTitleUpdate({ sessionId, name });
}, []);
if (isInitializing) {
return (
<div className="h-screen flex items-center justify-center bg-gray-900">
<div className="text-center">
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400">Initializing...</p>
</div>
</div>
);
}
return (
<div className="h-screen flex bg-gray-900">
<Sidebar
currentSessionId={currentSessionId}
onSelectSession={handleSelectSession}
onCreateSession={handleCreateSession}
responsive
sessionTitleUpdate={sessionTitleUpdate}
/>
{/* 主内容区域 */}
<div className="flex-1 flex min-w-0">
{/* 聊天区域 */}
<div className={`flex-1 min-w-0 ${showFileBrowser ? 'hidden md:block md:w-1/2' : 'w-full'}`}>
{currentSessionId ? (
<ChatPage
key={currentSessionId}
sessionId={currentSessionId}
onSessionNotFound={handleSessionNotFound}
onSessionUpdated={handleSessionUpdated}
responsive
showFileBrowser={showFileBrowser}
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
onOpenConfig={() => setShowConfig(true)}
onOpenCommands={() => setShowCommands(true)}
onOpenMCP={() => setShowMCP(true)}
onOpenHooks={() => setShowHooks(true)}
onOpenAgents={() => setShowAgents(true)}
onOpenCheckpoints={() => setShowCheckpoints(true)}
/>
) : (
<div className="flex-1 flex items-center justify-center h-full">
<p className="text-gray-400">Select or create a session</p>
</div>
)}
</div>
{/* 文件浏览器 - 桌面端侧边栏,移动端全屏覆盖 */}
{showFileBrowser && (
<>
{/* 移动端: 全屏覆盖 */}
<div className="fixed inset-0 z-50 bg-gray-900 md:hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-700">
<span className="text-lg font-semibold">Files</span>
<button
onClick={() => setShowFileBrowser(false)}
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="h-[calc(100%-56px)]">
<FileBrowser
onFileSelect={(path, _content) => {
console.log('Selected file:', path);
}}
/>
</div>
</div>
{/* 桌面端: 侧边栏 */}
<div className="hidden md:block w-1/2 border-l border-gray-700">
<FileBrowser
onFileSelect={(path, _content) => {
console.log('Selected file:', path);
}}
/>
</div>
</>
)}
</div>
{/* 配置面板 */}
{showConfig && <ConfigPanel onClose={() => setShowConfig(false)} responsive />}
{/* 命令面板 */}
{showCommands && <CommandPanel onClose={() => setShowCommands(false)} responsive />}
{/* MCP 面板 */}
{showMCP && <MCPPanel onClose={() => setShowMCP(false)} responsive />}
{/* Hooks 面板 */}
{showHooks && <HooksPanel onClose={() => setShowHooks(false)} responsive />}
{/* Agents 面板 */}
{showAgents && <AgentsPanel onClose={() => setShowAgents(false)} responsive />}
{/* Checkpoints 面板 */}
{showCheckpoints && <CheckpointPanel onClose={() => setShowCheckpoints(false)} responsive />}
{/* 移动端底部文件按钮 */}
<button
onClick={() => setShowFileBrowser(true)}
className="fixed bottom-20 right-4 z-30 p-3 rounded-full bg-gray-700 text-gray-300 hover:bg-gray-600 active:bg-gray-500 shadow-lg md:hidden"
title="Browse Files"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
</button>
{/* Toast 通知 */}
<Toaster />
</div>
);
}