From 4943e4825493f2d31d8093889a9e19ec3e485996 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Wed, 13 Aug 2025 00:23:37 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=A1=B9=E7=9B=AE=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ClaudeCodeSession.tsx | 721 +++++++++--------- src/components/FileExplorerPanelEnhanced.tsx | 46 +- src/components/FloatingPromptInput.tsx | 20 +- src/components/GitPanelEnhanced.tsx | 22 +- src/components/layout/ChatView.tsx | 50 ++ src/components/layout/FlexLayoutContainer.tsx | 144 ++++ src/components/layout/MainContentArea.tsx | 25 + src/components/layout/SidePanel.tsx | 49 ++ src/hooks/useLayoutManager.ts | 55 ++ 9 files changed, 712 insertions(+), 420 deletions(-) create mode 100644 src/components/layout/ChatView.tsx create mode 100644 src/components/layout/FlexLayoutContainer.tsx create mode 100644 src/components/layout/MainContentArea.tsx create mode 100644 src/components/layout/SidePanel.tsx diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index 7738ffe..09204c9 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useMemo } from "react"; +import React, { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { ArrowLeft, @@ -14,7 +14,9 @@ import { Hash, Command, PanelLeftOpen, - PanelRightOpen + PanelRightOpen, + ArrowUp, + ArrowDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -41,7 +43,13 @@ import { FileEditorEnhanced } from "./FileEditorEnhanced"; import type { ClaudeStreamMessage } from "./AgentExecution"; import { useVirtualizer } from "@tanstack/react-virtual"; import { useTrackEvent, useComponentMetrics, useWorkflowTracking, useLayoutManager } from "@/hooks"; -import { GridLayoutContainer, ResponsivePanel } from "@/components/ui/grid-layout"; +// import { GridLayoutContainer, ResponsivePanel } from "@/components/ui/grid-layout"; + +// 新增布局组件导入 +import { FlexLayoutContainer } from "@/components/layout/FlexLayoutContainer"; +import { MainContentArea } from "@/components/layout/MainContentArea"; +import { SidePanel } from "@/components/layout/SidePanel"; +import { ChatView } from "@/components/layout/ChatView"; interface ClaudeCodeSessionProps { /** @@ -94,8 +102,11 @@ export const ClaudeCodeSession: React.FC = ({ toggleTimeline, setPanelWidth, setSplitPosition: setLayoutSplitPosition, - getGridTemplateColumns, - getResponsiveClasses + getResponsiveClasses, + openFileEditor, + closeFileEditor, + openPreview: openLayoutPreview, + closePreview: closeLayoutPreview } = layoutManager; const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || ""); @@ -118,17 +129,30 @@ export const ClaudeCodeSession: React.FC = ({ // Queued prompts state const [queuedPrompts, setQueuedPrompts] = useState>([]); - // New state for preview feature - const [showPreview, setShowPreview] = useState(false); - const [previewUrl, setPreviewUrl] = useState(""); + // 使用布局管理器的预览功能 + const handleOpenPreview = useCallback((url: string) => { + openLayoutPreview(url); + setShowPreviewPrompt(false); + }, [openLayoutPreview]); + + const handleClosePreview = useCallback(() => { + closeLayoutPreview(); + setIsPreviewMaximized(false); + }, [closeLayoutPreview]); + + // 添加临时状态用于预览提示 const [showPreviewPrompt, setShowPreviewPrompt] = useState(false); const [isPreviewMaximized, setIsPreviewMaximized] = useState(false); + const [showScrollButtons, setShowScrollButtons] = useState(false); + const [isAtTop, setIsAtTop] = useState(true); + const [isAtBottom, setIsAtBottom] = useState(true); // Add collapsed state for queued prompts const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false); // File editor state - const [editingFile, setEditingFile] = useState(null); + // 移除重复的状态,使用 layout 中的状态 + // const [editingFile, setEditingFile] = useState(null); // 移除,使用 layout.editingFile const parentRef = useRef(null); const unlistenRefs = useRef([]); @@ -287,13 +311,30 @@ export const ClaudeCodeSession: React.FC = ({ useEffect(() => { onStreamingChange?.(isLoading, claudeSessionId); }, [isLoading, claudeSessionId, onStreamingChange]); + + // 滚动到顶部 + const scrollToTop = useCallback(() => { + if (parentRef.current) { + parentRef.current.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, []); + + // 滚动到底部 + const scrollToBottom = useCallback(() => { + if (parentRef.current) { + parentRef.current.scrollTo({ top: parentRef.current.scrollHeight, behavior: 'smooth' }); + } + }, []); // Auto-scroll to bottom when new messages arrive useEffect(() => { if (displayableMessages.length > 0) { - rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'smooth' }); + // 使用setTimeout确保DOM更新后再滚动 + setTimeout(() => { + scrollToBottom(); + }, 100); } - }, [displayableMessages.length, rowVirtualizer]); + }, [displayableMessages.length, scrollToBottom]); // Calculate total tokens from messages useEffect(() => { @@ -1067,32 +1108,51 @@ export const ClaudeCodeSession: React.FC = ({ } }; - // Handle URL detection from terminal output + // 处理URL检测 const handleLinkDetected = (url: string) => { - if (!showPreview && !showPreviewPrompt) { - setPreviewUrl(url); + if (!layout.previewUrl && !showPreviewPrompt) { + openLayoutPreview(url); setShowPreviewPrompt(true); } }; - - const handleClosePreview = () => { - setShowPreview(false); - setIsPreviewMaximized(false); - // Keep the previewUrl so it can be restored when reopening - }; - - const handlePreviewUrlChange = (url: string) => { - console.log('[ClaudeCodeSession] Preview URL changed to:', url); - setPreviewUrl(url); - }; - + + // 监听滚动位置 + useEffect(() => { + const scrollContainer = parentRef.current; + if (!scrollContainer) return; + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = scrollContainer; + setIsAtTop(scrollTop < 10); + setIsAtBottom(scrollTop + clientHeight >= scrollHeight - 10); + setShowScrollButtons(scrollHeight > clientHeight); + }; + + handleScroll(); // 初始检查 + scrollContainer.addEventListener('scroll', handleScroll); + + // 监听内容变化 + const observer = new ResizeObserver(handleScroll); + observer.observe(scrollContainer); + + return () => { + scrollContainer.removeEventListener('scroll', handleScroll); + observer.disconnect(); + }; + }, []); + const handleTogglePreviewMaximize = () => { setIsPreviewMaximized(!isPreviewMaximized); - // Reset split position when toggling maximize + // 重置分割位置 if (isPreviewMaximized) { setLayoutSplitPosition(50); } }; + + const handlePreviewUrlChange = (url: string) => { + console.log('[ClaudeCodeSession] Preview URL changed to:', url); + openLayoutPreview(url); + }; // Cleanup event listeners and track mount state useEffect(() => { @@ -1150,20 +1210,23 @@ export const ClaudeCodeSession: React.FC = ({ const messagesList = (
- {rowVirtualizer.getVirtualItems().map((virtualItem) => { + {displayableMessages.length === 0 ? ( +
+ +

开始对话或等待消息加载...

+
+ ) : ( + rowVirtualizer.getVirtualItems().map((virtualItem) => { const message = displayableMessages[virtualItem.index]; return ( = ({ animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3 }} - className="absolute inset-x-4 pb-4" + className="absolute inset-x-4 pb-3" style={{ top: virtualItem.start, }} @@ -1186,7 +1249,8 @@ export const ClaudeCodeSession: React.FC = ({ /> ); - })} + }) + )}
@@ -1195,7 +1259,7 @@ export const ClaudeCodeSession: React.FC = ({
@@ -1206,11 +1270,62 @@ export const ClaudeCodeSession: React.FC = ({ {error} )} + + {/* 滚动按钮 */} + + {showScrollButtons && ( + + {!isAtTop && ( + + + + + + +

滚动到顶部

+
+
+
+ )} + {!isAtBottom && ( + + + + + + +

滚动到底部

+
+
+
+ )} +
+ )} +
); @@ -1246,7 +1361,7 @@ export const ClaudeCodeSession: React.FC = ({ ); // If preview is maximized, render only the WebviewPreview in full screen - if (showPreview && isPreviewMaximized) { + if (layout.activeView === 'preview' && layout.previewUrl && isPreviewMaximized) { return ( = ({ transition={{ duration: 0.2 }} > = ({
+ {/* Token计数器 */} + {totalTokens > 0 && ( +
+ + {totalTokens.toLocaleString()} + tokens +
+ )} + {/* File Explorer Toggle */} {projectPath && ( @@ -1460,319 +1584,194 @@ export const ClaudeCodeSession: React.FC = ({
- {/* Main Content Area with Grid Layout */} - - {/* File Explorer Panel */} - {layout.showFileExplorer && ( - setPanelWidth('fileExplorer', width)} - minWidth={200} - maxWidth={500} - > - { - floatingPromptRef.current?.addImage(path); - }} - onFileOpen={(path) => { - setEditingFile(path); - }} - onToggle={toggleFileExplorer} - /> - - )} - - {/* Main Content */} -
- {showPreview ? ( - // Split pane layout when preview is active -
- -
- {projectPathInput} - {messagesList} -
- {/* Floating Input for preview mode */} -
- -
-
- } - right={ - - } - initialSplit={layout.splitPosition} - onSplitChange={(position) => { - setLayoutSplitPosition(position); - }} - minLeftWidth={400} - minRightWidth={400} - className="h-full" - /> -
- ) : editingFile ? ( - // File Editor layout with enhanced features -
- setEditingFile(null)} - className="flex-1" - /> -
- ) : ( - // Original layout when no preview or editor -
- {/* Main content area with messages */} -
- {projectPathInput} - {messagesList} - - {isLoading && messages.length === 0 && ( -
-
-
- - {session ? "Loading session history..." : "Initializing Claude Code..."} - -
-
- )} -
- - {/* Floating elements container - same width as main content */} -
- - {/* Queued Prompts Display */} - - {queuedPrompts.length > 0 && ( - -
-
-
-
- Queued Prompts ({queuedPrompts.length}) -
- -
- {!queuedPromptsCollapsed && queuedPrompts.map((queuedPrompt, index) => ( - -
-
- #{index + 1} - - {queuedPrompt.model === "opus" ? "Opus" : "Sonnet"} - -
-

{queuedPrompt.prompt}

+ mainContentId="main-content" + panels={[ + // 文件浏览器面板 + { + id: 'file-explorer', + position: 'left', + visible: layout.showFileExplorer, + defaultWidth: layout.fileExplorerWidth, + minWidth: 200, + maxWidth: 500, + resizable: !breakpoints.isMobile, + content: ( + { + floatingPromptRef.current?.addImage(path); + }} + onFileOpen={(path) => { + openFileEditor(path); + }} + onToggle={toggleFileExplorer} + /> + ) + }, + // 主内容区域 + { + id: 'main-content', + position: 'center', + visible: true, + content: ( + + {layout.activeView === 'editor' && layout.editingFile ? ( + // 文件编辑器视图 + + ) : layout.activeView === 'preview' && layout.previewUrl ? ( + // 预览视图 + +
- -
- ))} + } + /> + } + right={ + + } + initialSplit={layout.splitPosition} + onSplitChange={(position) => { + setLayoutSplitPosition(position); + }} + minLeftWidth={400} + minRightWidth={400} + className="h-full" + /> + ) : ( + // 默认聊天视图 + +
-
-
+ } + floatingElements={ + <> + {/* 排队提示显示 */} + + {queuedPrompts.length > 0 && ( + +
+
+
+ Queued Prompts ({queuedPrompts.length}) +
+ +
+ {!queuedPromptsCollapsed && queuedPrompts.map((queuedPrompt, index) => ( + +
+
+ #{index + 1} + + {queuedPrompt.model === "opus" ? "Opus" : "Sonnet"} + +
+

{queuedPrompt.prompt}

+
+ +
+ ))} +
+
+ )} +
+ + } + /> )} -
- - {/* Navigation Arrows */} - {displayableMessages.length > 5 && ( - -
- -
- -
- - )} - - {/* Floating Prompt Input - Now properly aligned with main content */} -
- -
- - {/* Token Counter */} - {totalTokens > 0 && ( -
-
-
- -
- - {totalTokens.toLocaleString()} - tokens -
-
-
-
-
- )} - -
-
- )} -
- - {/* Git Panel */} - {layout.showGitPanel && ( - setPanelWidth('gitPanel', width)} - minWidth={200} - maxWidth={500} - > - - - )} - - {/* Timeline Panel - Only on desktop */} - {layout.showTimeline && effectiveSession && !breakpoints.isMobile && ( - setPanelWidth('timeline', width)} - minWidth={320} - maxWidth={600} - className="border-l" - > -
-
-

{t('app.sessionTimeline')}

- -
-
+ + ) + }, + // Git 面板 + { + id: 'git-panel', + position: 'right', + visible: layout.showGitPanel, + defaultWidth: layout.gitPanelWidth, + minWidth: 200, + maxWidth: 500, + resizable: !breakpoints.isMobile, + content: ( + + ) + }, + // 时间线面板(仅桌面端) + ...(layout.showTimeline && effectiveSession && !breakpoints.isMobile ? [{ + id: 'timeline', + position: 'right' as const, + visible: true, + defaultWidth: layout.timelineWidth, + minWidth: 320, + maxWidth: 600, + resizable: true, + content: ( + = ({ onCheckpointCreated={handleCheckpointCreated} refreshVersion={timelineVersion} /> -
-
-
- )} - + + ) + }] : []) + ]} + onPanelResize={(panelId, width) => { + if (panelId === 'file-explorer') { + setPanelWidth('fileExplorer', width); + } else if (panelId === 'git-panel') { + setPanelWidth('gitPanel', width); + } else if (panelId === 'timeline') { + setPanelWidth('timeline', width); + } + }} + savedWidths={{ + 'file-explorer': layout.fileExplorerWidth, + 'git-panel': layout.gitPanelWidth, + 'timeline': layout.timelineWidth, + }} + />
{/* Fork Dialog */} diff --git a/src/components/FileExplorerPanelEnhanced.tsx b/src/components/FileExplorerPanelEnhanced.tsx index d65ff23..42b4be1 100644 --- a/src/components/FileExplorerPanelEnhanced.tsx +++ b/src/components/FileExplorerPanelEnhanced.tsx @@ -667,39 +667,17 @@ export const FileExplorerPanelEnhanced: React.FC ); }; + // 如果不可见,返回null + if (!isVisible) return null; + return ( - - {isVisible && ( - - {/* 拖拽手柄 */} -
setIsResizing(true)} - > -
- -
-
- - {/* Header */} -
-
- -

{t("app.fileExplorer")}

-
+
+ {/* Header */} +
+
+ +

{t("app.fileExplorer")}

+
{/* 展开/收起按钮 */} @@ -819,9 +797,7 @@ export const FileExplorerPanelEnhanced: React.FC
)} - - )} - +
); }; diff --git a/src/components/FloatingPromptInput.tsx b/src/components/FloatingPromptInput.tsx index afd53a3..9a4b286 100644 --- a/src/components/FloatingPromptInput.tsx +++ b/src/components/FloatingPromptInput.tsx @@ -872,7 +872,7 @@ const FloatingPromptInputInner = ( {/* Fixed Position Input Bar */}
-
+
{/* Image previews */} {embeddedImages.length > 0 && ( )} -
+
{/* Model Picker */} {selectedModelData.icon} {selectedModelData.name} @@ -949,7 +949,7 @@ const FloatingPromptInputInner = ( variant="outline" size="default" disabled={disabled} - className="gap-2 h-10" + className="gap-2 h-8" > setIsExpanded(true)} disabled={disabled} - className="absolute right-1 bottom-1 h-8 w-8" + className="absolute right-1 bottom-0 h-6 w-6" > @@ -1057,7 +1057,7 @@ const FloatingPromptInputInner = ( disabled={isLoading ? false : (!prompt.trim() || disabled)} variant={isLoading ? "destructive" : "default"} size="default" - className="min-w-[60px] h-10" + className="min-w-[56px] h-8" > {isLoading ? ( <> @@ -1069,10 +1069,6 @@ const FloatingPromptInputInner = ( )}
- -
- {t('input.pressEnterToSend')}{projectPath?.trim() && t('input.withFileAndCommandSupport')} -
diff --git a/src/components/GitPanelEnhanced.tsx b/src/components/GitPanelEnhanced.tsx index 2904466..5d0c327 100644 --- a/src/components/GitPanelEnhanced.tsx +++ b/src/components/GitPanelEnhanced.tsx @@ -639,30 +639,14 @@ export const GitPanelEnhanced: React.FC = ({ <> {isVisible && ( - - {/* 拖拽手柄 */} -
setIsResizing(true)} - > -
- -
-
- {/* Header */}
@@ -823,7 +807,7 @@ export const GitPanelEnhanced: React.FC = ({ )} - +
)} diff --git a/src/components/layout/ChatView.tsx b/src/components/layout/ChatView.tsx new file mode 100644 index 0000000..d368bfd --- /dev/null +++ b/src/components/layout/ChatView.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface ChatViewProps { + projectPathInput?: React.ReactNode; + messagesList: React.ReactNode; + floatingInput?: React.ReactNode; + floatingElements?: React.ReactNode; + className?: string; +} + +export const ChatView: React.FC = ({ + projectPathInput, + messagesList, + floatingInput, + floatingElements, + className +}) => { + return ( +
+ {/* 项目路径输入(如果提供) */} + {projectPathInput && ( +
+ {projectPathInput} +
+ )} + + {/* 消息列表区域 - 占据大部分空间 */} +
+ {messagesList} +
+ + {/* 浮动输入框 - 最小化高度 */} + {floatingInput && ( +
+ {floatingInput} +
+ )} + + {/* 其他浮动元素(如队列提示、Token计数器等) */} + {floatingElements && ( +
+
+ {floatingElements} +
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/layout/FlexLayoutContainer.tsx b/src/components/layout/FlexLayoutContainer.tsx new file mode 100644 index 0000000..6507b88 --- /dev/null +++ b/src/components/layout/FlexLayoutContainer.tsx @@ -0,0 +1,144 @@ +import React, { ReactNode, useState, useCallback, useEffect } from 'react'; +import { cn } from '@/lib/utils'; + +export interface LayoutPanel { + id: string; + content: ReactNode; + defaultWidth?: number; + minWidth?: number; + maxWidth?: number; + resizable?: boolean; + visible?: boolean; + position?: 'left' | 'center' | 'right'; + className?: string; +} + +interface FlexLayoutContainerProps { + panels: LayoutPanel[]; + className?: string; + mainContentId: string; + onPanelResize?: (panelId: string, width: number) => void; + savedWidths?: Record; +} + +export const FlexLayoutContainer: React.FC = ({ + panels, + className, + mainContentId, + onPanelResize, + savedWidths = {} +}) => { + const [panelWidths, setPanelWidths] = useState>({}); + const [isDragging, setIsDragging] = useState(null); + const [dragStartX, setDragStartX] = useState(0); + const [dragStartWidth, setDragStartWidth] = useState(0); + + // 初始化面板宽度 + useEffect(() => { + const initialWidths: Record = {}; + panels.forEach(panel => { + if (panel.visible !== false && panel.id !== mainContentId) { + initialWidths[panel.id] = savedWidths[panel.id] || panel.defaultWidth || 280; + } + }); + setPanelWidths(initialWidths); + }, [panels, mainContentId, savedWidths]); + + // 处理拖拽开始 + const handleDragStart = useCallback((e: React.MouseEvent, panelId: string) => { + e.preventDefault(); + setIsDragging(panelId); + setDragStartX(e.clientX); + setDragStartWidth(panelWidths[panelId] || 280); + }, [panelWidths]); + + // 处理拖拽移动 + useEffect(() => { + if (!isDragging) return; + + const handleMouseMove = (e: MouseEvent) => { + const panel = panels.find(p => p.id === isDragging); + if (!panel) return; + + const delta = panel.position === 'left' + ? e.clientX - dragStartX + : dragStartX - e.clientX; + + const newWidth = Math.max( + panel.minWidth || 200, + Math.min(panel.maxWidth || 600, dragStartWidth + delta) + ); + + setPanelWidths(prev => ({ + ...prev, + [isDragging]: newWidth + })); + + if (onPanelResize) { + onPanelResize(isDragging, newWidth); + } + }; + + const handleMouseUp = () => { + setIsDragging(null); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, dragStartX, dragStartWidth, panels, onPanelResize]); + + // 渲染面板 + const renderPanel = (panel: LayoutPanel) => { + if (panel.visible === false) return null; + + const isMain = panel.id === mainContentId; + const width = isMain ? 'flex-1' : `${panelWidths[panel.id] || panel.defaultWidth || 280}px`; + + return ( +
+ {panel.content} + + {/* 调整手柄 */} + {!isMain && panel.resizable !== false && ( +
handleDragStart(e, panel.id)} + > +
+
+ )} +
+ ); + }; + + // 按位置排序面板 + const sortedPanels = [...panels].sort((a, b) => { + const positionOrder = { left: 0, center: 1, right: 2 }; + const aPos = a.position || (a.id === mainContentId ? 'center' : 'left'); + const bPos = b.position || (b.id === mainContentId ? 'center' : 'left'); + return positionOrder[aPos] - positionOrder[bPos]; + }); + + return ( +
+ {sortedPanels.map(renderPanel)} +
+ ); +}; \ No newline at end of file diff --git a/src/components/layout/MainContentArea.tsx b/src/components/layout/MainContentArea.tsx new file mode 100644 index 0000000..1139e67 --- /dev/null +++ b/src/components/layout/MainContentArea.tsx @@ -0,0 +1,25 @@ +import React, { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; + +interface MainContentAreaProps { + children: ReactNode; + className?: string; + isEditing?: boolean; +} + +export const MainContentArea: React.FC = ({ + children, + className, + isEditing = false +}) => { + return ( +
+ {children} +
+ ); +}; \ No newline at end of file diff --git a/src/components/layout/SidePanel.tsx b/src/components/layout/SidePanel.tsx new file mode 100644 index 0000000..0fd6c74 --- /dev/null +++ b/src/components/layout/SidePanel.tsx @@ -0,0 +1,49 @@ +import React, { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { X } from 'lucide-react'; + +interface SidePanelProps { + children: ReactNode; + title?: string; + onClose?: () => void; + className?: string; + position?: 'left' | 'right'; +} + +export const SidePanel: React.FC = ({ + children, + title, + onClose, + className, + position = 'left' +}) => { + return ( +
+ {title && ( +
+

{title}

+ {onClose && ( + + )} +
+ )} +
+ {children} +
+
+ ); +}; \ No newline at end of file diff --git a/src/hooks/useLayoutManager.ts b/src/hooks/useLayoutManager.ts index 698b80e..856ee0d 100644 --- a/src/hooks/useLayoutManager.ts +++ b/src/hooks/useLayoutManager.ts @@ -9,6 +9,9 @@ interface LayoutState { showTimeline: boolean; splitPosition: number; isCompactMode: boolean; + activeView: 'chat' | 'editor' | 'preview'; // 新增:当前活动视图 + editingFile: string | null; // 新增:正在编辑的文件 + previewUrl: string | null; // 新增:预览URL } interface LayoutBreakpoints { @@ -29,6 +32,9 @@ const DEFAULT_LAYOUT: LayoutState = { showTimeline: false, splitPosition: 50, isCompactMode: false, + activeView: 'chat', // 默认显示聊天视图 + editingFile: null, + previewUrl: null, }; const STORAGE_KEY = 'claudia_layout_preferences'; @@ -250,6 +256,49 @@ export function useLayoutManager(projectPath?: string) { return classes.join(' '); }, [breakpoints, layout.isCompactMode]); + // 打开文件编辑器 + const openFileEditor = useCallback((filePath: string) => { + saveLayout({ + activeView: 'editor', + editingFile: filePath, + previewUrl: null, // 关闭预览 + }); + }, [saveLayout]); + + // 关闭文件编辑器 + const closeFileEditor = useCallback(() => { + saveLayout({ + activeView: 'chat', + editingFile: null, + }); + }, [saveLayout]); + + // 打开预览 + const openPreview = useCallback((url: string) => { + saveLayout({ + activeView: 'preview', + previewUrl: url, + editingFile: null, // 关闭编辑器 + }); + }, [saveLayout]); + + // 关闭预览 + const closePreview = useCallback(() => { + saveLayout({ + activeView: 'chat', + previewUrl: null, + }); + }, [saveLayout]); + + // 切换到聊天视图 + const switchToChatView = useCallback(() => { + saveLayout({ + activeView: 'chat', + editingFile: null, + previewUrl: null, + }); + }, [saveLayout]); + return { layout, breakpoints, @@ -264,5 +313,11 @@ export function useLayoutManager(projectPath?: string) { getGridTemplateColumns, getResponsiveClasses, saveLayout, + // 新增的方法 + openFileEditor, + closeFileEditor, + openPreview, + closePreview, + switchToChatView, }; } \ No newline at end of file