feat(ui): 添加 IDE 组件(文件浏览器 + 代码编辑器)
- 新增 CodeEditor 组件,基于 CodeMirror 实现多标签代码编辑 - 新增 FileExplorer 组件,支持文件树展开/折叠和文件选择 - 新增 IDE 组件,整合文件浏览器和代码编辑器 - 新增 SessionPanel 组件,用于会话管理 - 添加文件写入 API(PUT /api/files/write) - 优化布局:IDE 始终显示,移除文件切换按钮 - 工作目录路径显示在文件浏览器标题栏,支持悬浮显示完整路径
This commit is contained in:
+25
-81
@@ -6,8 +6,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Sidebar,
|
||||
FileBrowser,
|
||||
IDE,
|
||||
CommandPanel,
|
||||
MCPPanel,
|
||||
HooksPanel,
|
||||
@@ -16,11 +15,11 @@ import {
|
||||
ProvidersPanel,
|
||||
LSPPanel,
|
||||
DiagnosticsPanel,
|
||||
SessionPanel,
|
||||
Toaster,
|
||||
ThemeProvider,
|
||||
listSessions,
|
||||
createSession,
|
||||
getWorkingDirectory,
|
||||
type Session,
|
||||
} from '@ai-assistant/ui';
|
||||
import { ChatPage } from './pages/Chat';
|
||||
@@ -28,7 +27,6 @@ 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 [showCommands, setShowCommands] = useState(false);
|
||||
const [showMCP, setShowMCP] = useState(false);
|
||||
const [showHooks, setShowHooks] = useState(false);
|
||||
@@ -37,28 +35,18 @@ export function App() {
|
||||
const [showProviders, setShowProviders] = useState(false);
|
||||
const [showLSP, setShowLSP] = useState(false);
|
||||
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
||||
const [showSessions, setShowSessions] = useState(false);
|
||||
const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null);
|
||||
const [workingDirectory, setWorkingDirectory] = useState<string>('');
|
||||
|
||||
// 初始化:加载会话和工作目录
|
||||
// 初始化:加载会话
|
||||
useEffect(() => {
|
||||
const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions';
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
// 并行获取会话和工作目录
|
||||
const [sessionsResult, workdirResult] = await Promise.all([
|
||||
listSessions(),
|
||||
getWorkingDirectory().catch(() => null),
|
||||
]);
|
||||
|
||||
const sessionsResult = await listSessions();
|
||||
const { data: sessions } = sessionsResult;
|
||||
|
||||
// 设置工作目录
|
||||
if (workdirResult?.data?.workingDirectory) {
|
||||
setWorkingDirectory(workdirResult.data.workingDirectory);
|
||||
}
|
||||
|
||||
if (sessions.length > 0) {
|
||||
// 有会话,选择最近的
|
||||
setCurrentSessionId(sessions[0].id);
|
||||
@@ -124,18 +112,15 @@ export function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<div className="h-screen flex bg-surface-base">
|
||||
<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'}`}>
|
||||
{/* 左侧:IDE(文件浏览器 + 代码编辑器) */}
|
||||
<div className="hidden md:flex flex-col border-r border-line w-[50%] lg:w-[60%]">
|
||||
<IDE />
|
||||
</div>
|
||||
|
||||
{/* 右侧:聊天区域 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{currentSessionId ? (
|
||||
<ChatPage
|
||||
key={currentSessionId}
|
||||
@@ -143,8 +128,6 @@ export function App() {
|
||||
onSessionNotFound={handleSessionNotFound}
|
||||
onSessionUpdated={handleSessionUpdated}
|
||||
responsive
|
||||
showFileBrowser={showFileBrowser}
|
||||
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
|
||||
onOpenCommands={() => setShowCommands(true)}
|
||||
onOpenMCP={() => setShowMCP(true)}
|
||||
onOpenHooks={() => setShowHooks(true)}
|
||||
@@ -153,7 +136,7 @@ export function App() {
|
||||
onOpenProviders={() => setShowProviders(true)}
|
||||
onOpenLSP={() => setShowLSP(true)}
|
||||
onOpenDiagnostics={() => setShowDiagnostics(true)}
|
||||
workingDirectory={workingDirectory}
|
||||
onOpenSessions={() => setShowSessions(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center h-full">
|
||||
@@ -162,41 +145,6 @@ export function App() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 文件浏览器 - 桌面端侧边栏,移动端全屏覆盖 */}
|
||||
{showFileBrowser && (
|
||||
<>
|
||||
{/* 移动端: 全屏覆盖 */}
|
||||
<div className="fixed inset-0 z-50 bg-surface-base md:hidden">
|
||||
<div className="flex items-center justify-between p-3 border-b border-line">
|
||||
<span className="text-lg font-semibold text-fg">Files</span>
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(false)}
|
||||
className="p-2 rounded-lg bg-surface-muted text-fg-muted hover:bg-surface-emphasis"
|
||||
>
|
||||
<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-line">
|
||||
<FileBrowser
|
||||
onFileSelect={(path, _content) => {
|
||||
console.log('Selected file:', path);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 命令面板 */}
|
||||
@@ -241,21 +189,17 @@ export function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 移动端底部文件按钮 */}
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(true)}
|
||||
className="fixed bottom-20 right-4 z-30 p-3 rounded-full bg-surface-muted text-fg-muted hover:bg-surface-emphasis active:bg-surface-emphasis 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>
|
||||
{/* Sessions 面板 */}
|
||||
{showSessions && (
|
||||
<SessionPanel
|
||||
onClose={() => setShowSessions(false)}
|
||||
currentSessionId={currentSessionId}
|
||||
onSelectSession={handleSelectSession}
|
||||
onCreateSession={handleCreateSession}
|
||||
sessionTitleUpdate={sessionTitleUpdate}
|
||||
responsive
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Toast 通知 */}
|
||||
<Toaster />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { WifiOff, MessageSquare, FolderOpen, Terminal, Plug, Zap, Bot, History, Server, Folder } from 'lucide-react';
|
||||
import { WifiOff, MessageSquare, Terminal, Plug, Zap, Bot, History, Server, MessagesSquare } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
@@ -23,8 +23,6 @@ interface ChatPageProps {
|
||||
onSessionUpdated?: (sessionId: string, name: string) => void;
|
||||
responsive?: boolean;
|
||||
// 工具栏按钮
|
||||
showFileBrowser?: boolean;
|
||||
onToggleFileBrowser?: () => void;
|
||||
onOpenCommands?: () => void;
|
||||
onOpenMCP?: () => void;
|
||||
onOpenHooks?: () => void;
|
||||
@@ -33,8 +31,7 @@ interface ChatPageProps {
|
||||
onOpenProviders?: () => void;
|
||||
onOpenLSP?: () => void;
|
||||
onOpenDiagnostics?: () => void;
|
||||
// Working Directory
|
||||
workingDirectory?: string;
|
||||
onOpenSessions?: () => void;
|
||||
}
|
||||
|
||||
export function ChatPage({
|
||||
@@ -42,8 +39,6 @@ export function ChatPage({
|
||||
onSessionNotFound,
|
||||
onSessionUpdated,
|
||||
responsive = false,
|
||||
showFileBrowser,
|
||||
onToggleFileBrowser,
|
||||
onOpenCommands,
|
||||
onOpenMCP,
|
||||
onOpenHooks,
|
||||
@@ -52,7 +47,7 @@ export function ChatPage({
|
||||
onOpenProviders,
|
||||
onOpenLSP,
|
||||
onOpenDiagnostics,
|
||||
workingDirectory,
|
||||
onOpenSessions,
|
||||
}: ChatPageProps) {
|
||||
const {
|
||||
messages,
|
||||
@@ -162,18 +157,7 @@ export function ChatPage({
|
||||
<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-line bg-surface-subtle">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<h1 className="text-lg font-medium text-fg flex-shrink-0">Chat</h1>
|
||||
{/* Working Directory */}
|
||||
{workingDirectory && (
|
||||
<div className="flex items-center gap-1.5 text-sm text-fg-muted min-w-0">
|
||||
<Folder size={14} className="flex-shrink-0 text-fg-subtle" />
|
||||
<span className="truncate font-mono text-xs" title={workingDirectory}>
|
||||
{workingDirectory}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-lg font-medium text-fg">Chat</h1>
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
{/* 上下文使用情况 - 紧凑模式 */}
|
||||
{sessionId && (
|
||||
@@ -189,7 +173,7 @@ export function ChatPage({
|
||||
<ConnectionStatus />
|
||||
|
||||
{/* 工具栏按钮 */}
|
||||
{(onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders || onOpenLSP || onOpenDiagnostics) && (
|
||||
{(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) && (
|
||||
@@ -278,20 +262,16 @@ export function ChatPage({
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* 文件浏览器按钮 - 仅桌面端显示 */}
|
||||
{onToggleFileBrowser && (
|
||||
{/* Sessions 按钮 */}
|
||||
{onOpenSessions && (
|
||||
<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-fg-muted hover:text-fg-secondary hover:bg-surface-muted'
|
||||
}`}
|
||||
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
|
||||
onClick={onOpenSessions}
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Sessions"
|
||||
>
|
||||
<FolderOpen size={20} />
|
||||
<MessagesSquare size={20} />
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user