Spaces:
Running
Running
| import { useCallback, useRef, useEffect, useState } from 'react'; | |
| import { | |
| Box, | |
| Drawer, | |
| IconButton, | |
| Avatar, | |
| Menu, | |
| MenuItem, | |
| Typography, | |
| CircularProgress, | |
| } from '@mui/material'; | |
| import MenuIcon from '@mui/icons-material/Menu'; | |
| import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; | |
| import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; | |
| import AccountCircleIcon from '@mui/icons-material/AccountCircle'; | |
| import SettingsIcon from '@mui/icons-material/Settings'; | |
| import LogoutIcon from '@mui/icons-material/Logout'; | |
| import { useSessionStore } from '@/store/sessionStore'; | |
| import { useAgentStore } from '@/store/agentStore'; | |
| import { useLayoutStore } from '@/store/layoutStore'; | |
| import { useAuthStore } from '@/store/authStore'; | |
| import { useAgentEvents } from '@/hooks/useAgentEvents'; | |
| import SessionSidebar from '@/components/SessionSidebar/SessionSidebar'; | |
| import CodePanel from '@/components/CodePanel/CodePanel'; | |
| import ChatInput from '@/components/Chat/ChatInput'; | |
| import MessageList from '@/components/Chat/MessageList'; | |
| import { WelcomeScreen } from '@/components/Welcome'; | |
| import { SetupPrompt } from '@/components/Onboarding'; | |
| import { SettingsModal } from '@/components/Settings'; | |
| import type { Message } from '@/types/agent'; | |
| const API_BASE = import.meta.env.DEV ? 'http://127.0.0.1:7860' : ''; | |
| const DRAWER_WIDTH = 260; | |
| export default function AppLayout() { | |
| const { activeSessionId, sessions, isLoading: sessionsLoading, isLoaded: sessionsLoaded, loadSessions, createSession } = useSessionStore(); | |
| const { isConnected, isProcessing, messages, addMessage, clearMessages, setPlan, setPanelContent } = useAgentStore(); | |
| const { | |
| isLeftSidebarOpen, | |
| isRightPanelOpen, | |
| rightPanelWidth, | |
| setRightPanelWidth, | |
| toggleLeftSidebar, | |
| toggleRightPanel | |
| } = useLayoutStore(); | |
| const { user, isLoading: authLoading, getAuthHeaders, isAuthenticated, logout } = useAuthStore(); | |
| const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null); | |
| const [showSettings, setShowSettings] = useState(false); | |
| const [isCreatingSession, setIsCreatingSession] = useState(false); | |
| const isResizing = useRef(false); | |
| const startResizing = useCallback((e: React.MouseEvent) => { | |
| e.preventDefault(); | |
| isResizing.current = true; | |
| document.addEventListener('mousemove', handleMouseMove); | |
| document.addEventListener('mouseup', stopResizing); | |
| document.body.style.cursor = 'col-resize'; | |
| }, []); | |
| const stopResizing = useCallback(() => { | |
| isResizing.current = false; | |
| document.removeEventListener('mousemove', handleMouseMove); | |
| document.removeEventListener('mouseup', stopResizing); | |
| document.body.style.cursor = 'default'; | |
| }, []); | |
| const handleMouseMove = useCallback((e: MouseEvent) => { | |
| if (!isResizing.current) return; | |
| const newWidth = window.innerWidth - e.clientX; | |
| const maxWidth = window.innerWidth * 0.8; | |
| const minWidth = 300; | |
| if (newWidth > minWidth && newWidth < maxWidth) { | |
| setRightPanelWidth(newWidth); | |
| } | |
| }, [setRightPanelWidth]); | |
| useEffect(() => { | |
| return () => { | |
| document.removeEventListener('mousemove', handleMouseMove); | |
| document.removeEventListener('mouseup', stopResizing); | |
| }; | |
| }, [handleMouseMove, stopResizing]); | |
| // Load sessions when authenticated (only once) | |
| // Note: user is in deps to re-run when auth state changes (isAuthenticated is a stable function ref) | |
| useEffect(() => { | |
| if (isAuthenticated() && !sessionsLoading && !sessionsLoaded) { | |
| loadSessions(); | |
| } | |
| }, [user, sessionsLoading, sessionsLoaded, loadSessions, isAuthenticated]); | |
| useAgentEvents({ | |
| sessionId: activeSessionId, | |
| onReady: () => console.log('Agent ready'), | |
| onError: (error) => console.error('Agent error:', error), | |
| }); | |
| const handleSendMessage = useCallback( | |
| async (text: string) => { | |
| if (!activeSessionId || !text.trim()) return; | |
| // Bypass Anthropic key check as we use HF token | |
| const userMsg: Message = { | |
| id: `user_${Date.now()}`, | |
| role: 'user', | |
| content: text.trim(), | |
| timestamp: new Date().toISOString(), | |
| }; | |
| addMessage(userMsg); | |
| try { | |
| await fetch(`${API_BASE}/api/submit`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...getAuthHeaders(), | |
| }, | |
| body: JSON.stringify({ | |
| session_id: activeSessionId, | |
| text: text.trim(), | |
| }), | |
| }); | |
| } catch (e) { | |
| console.error('Send failed:', e); | |
| } | |
| }, | |
| [activeSessionId, addMessage, getAuthHeaders] | |
| ); | |
| const handleLogout = async () => { | |
| setUserMenuAnchor(null); | |
| await logout(); | |
| }; | |
| const handleOpenSettings = () => { | |
| setUserMenuAnchor(null); | |
| setShowSettings(true); | |
| }; | |
| const handleStartSession = useCallback(async () => { | |
| setIsCreatingSession(true); | |
| try { | |
| const sessionId = await createSession(); | |
| if (sessionId) { | |
| clearMessages(); | |
| setPlan([]); | |
| setPanelContent(null); | |
| } | |
| } catch (e) { | |
| console.error('Failed to create session:', e); | |
| } finally { | |
| setIsCreatingSession(false); | |
| } | |
| }, [createSession, clearMessages, setPlan, setPanelContent]); | |
| // Auto-create session when authenticated but no sessions (only after sessions have been loaded) | |
| const noSessionsAtAll = sessions.length === 0 && !activeSessionId; | |
| useEffect(() => { | |
| if (isAuthenticated() && sessionsLoaded && noSessionsAtAll && !isCreatingSession) { | |
| handleStartSession(); | |
| } | |
| }, [isAuthenticated, sessionsLoaded, noSessionsAtAll, isCreatingSession, handleStartSession]); | |
| // Show loading spinner while auth is loading | |
| if (authLoading) { | |
| return ( | |
| <Box | |
| sx={{ | |
| width: '100%', | |
| height: '100%', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| background: 'var(--bg)', | |
| }} | |
| > | |
| <CircularProgress /> | |
| </Box> | |
| ); | |
| } | |
| // Show welcome screen for unauthenticated users | |
| if (!isAuthenticated()) { | |
| return <WelcomeScreen />; | |
| } | |
| // Show loading while sessions are being fetched | |
| if (sessionsLoading) { | |
| return ( | |
| <Box | |
| sx={{ | |
| width: '100%', | |
| height: '100%', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| background: 'var(--bg)', | |
| }} | |
| > | |
| <CircularProgress /> | |
| </Box> | |
| ); | |
| } | |
| // Bypassed setup prompt logic as we use HF token | |
| const needsApiKey = false; | |
| if (needsApiKey) { | |
| return ( | |
| <Box sx={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}> | |
| {/* Minimal header for setup screens */} | |
| <Box | |
| sx={{ | |
| height: '60px', | |
| px: 2, | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'space-between', | |
| borderBottom: 1, | |
| borderColor: 'divider', | |
| bgcolor: 'background.default', | |
| }} | |
| > | |
| <img | |
| src="/hf-logo-white.png" | |
| alt="Hugging Face" | |
| style={{ height: '32px', objectFit: 'contain' }} | |
| /> | |
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | |
| <IconButton onClick={handleOpenSettings} size="small"> | |
| <SettingsIcon /> | |
| </IconButton> | |
| <IconButton | |
| onClick={(e) => setUserMenuAnchor(e.currentTarget)} | |
| size="small" | |
| > | |
| {user?.picture ? ( | |
| <Avatar src={user.picture} sx={{ width: 32, height: 32 }} /> | |
| ) : ( | |
| <AccountCircleIcon /> | |
| )} | |
| </IconButton> | |
| <Menu | |
| anchorEl={userMenuAnchor} | |
| open={Boolean(userMenuAnchor)} | |
| onClose={() => setUserMenuAnchor(null)} | |
| > | |
| <MenuItem disabled> | |
| <Typography variant="body2">{user?.name || user?.username}</Typography> | |
| </MenuItem> | |
| <MenuItem onClick={handleOpenSettings}> | |
| <SettingsIcon sx={{ mr: 1 }} fontSize="small" /> | |
| Settings | |
| </MenuItem> | |
| <MenuItem onClick={handleLogout}> | |
| <LogoutIcon sx={{ mr: 1 }} fontSize="small" /> | |
| Logout | |
| </MenuItem> | |
| </Menu> | |
| </Box> | |
| </Box> | |
| {/* Setup content */} | |
| <Box sx={{ flex: 1 }}> | |
| <SetupPrompt onOpenSettings={handleOpenSettings} /> | |
| </Box> | |
| <SettingsModal open={showSettings} onClose={() => setShowSettings(false)} /> | |
| </Box> | |
| ); | |
| } | |
| // Show loading while auto-creating first session | |
| if (isCreatingSession) { | |
| return ( | |
| <Box | |
| sx={{ | |
| width: '100%', | |
| height: '100%', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| background: 'var(--bg)', | |
| }} | |
| > | |
| <CircularProgress /> | |
| </Box> | |
| ); | |
| } | |
| // Full app layout for authenticated users with active session | |
| return ( | |
| <Box sx={{ display: 'flex', width: '100%', height: '100%' }}> | |
| {/* Left Sidebar Drawer */} | |
| <Box | |
| component="nav" | |
| sx={{ | |
| width: { md: isLeftSidebarOpen ? DRAWER_WIDTH : 0 }, | |
| flexShrink: { md: 0 }, | |
| transition: isResizing.current ? 'none' : 'width 0.2s', | |
| overflow: 'hidden', | |
| }} | |
| > | |
| <Drawer | |
| variant="persistent" | |
| sx={{ | |
| display: { xs: 'none', md: 'block' }, | |
| '& .MuiDrawer-paper': { | |
| boxSizing: 'border-box', | |
| width: DRAWER_WIDTH, | |
| borderRight: '1px solid', | |
| borderColor: 'divider', | |
| top: 0, | |
| height: '100%', | |
| bgcolor: 'var(--panel)', | |
| }, | |
| }} | |
| open={isLeftSidebarOpen} | |
| > | |
| <SessionSidebar /> | |
| </Drawer> | |
| </Box> | |
| {/* Main Content Area */} | |
| <Box | |
| sx={{ | |
| flexGrow: 1, | |
| height: '100%', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| transition: isResizing.current ? 'none' : 'width 0.2s', | |
| position: 'relative', | |
| overflow: 'hidden', | |
| }} | |
| > | |
| {/* Top Header Bar */} | |
| <Box | |
| sx={{ | |
| height: '60px', | |
| px: 1, | |
| display: 'flex', | |
| alignItems: 'center', | |
| borderBottom: 1, | |
| borderColor: 'divider', | |
| bgcolor: 'background.default', | |
| zIndex: 1200, | |
| }} | |
| > | |
| <IconButton onClick={toggleLeftSidebar} size="small"> | |
| {isLeftSidebarOpen ? <ChevronLeftIcon /> : <MenuIcon />} | |
| </IconButton> | |
| <Box sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}> | |
| <img | |
| src="/hf-logo-white.png" | |
| alt="Hugging Face" | |
| style={{ height: '40px', objectFit: 'contain' }} | |
| /> | |
| </Box> | |
| {/* User Section */} | |
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | |
| <IconButton | |
| onClick={(e) => setUserMenuAnchor(e.currentTarget)} | |
| size="small" | |
| > | |
| {user?.picture ? ( | |
| <Avatar src={user.picture} sx={{ width: 32, height: 32 }} /> | |
| ) : ( | |
| <AccountCircleIcon /> | |
| )} | |
| </IconButton> | |
| <Menu | |
| anchorEl={userMenuAnchor} | |
| open={Boolean(userMenuAnchor)} | |
| onClose={() => setUserMenuAnchor(null)} | |
| > | |
| <MenuItem disabled> | |
| <Typography variant="body2">{user?.name || user?.username}</Typography> | |
| </MenuItem> | |
| <MenuItem onClick={handleOpenSettings}> | |
| <SettingsIcon sx={{ mr: 1 }} fontSize="small" /> | |
| Settings | |
| </MenuItem> | |
| <MenuItem onClick={handleLogout}> | |
| <LogoutIcon sx={{ mr: 1 }} fontSize="small" /> | |
| Logout | |
| </MenuItem> | |
| </Menu> | |
| <IconButton | |
| onClick={toggleRightPanel} | |
| size="small" | |
| sx={{ visibility: isRightPanelOpen ? 'hidden' : 'visible' }} | |
| > | |
| <MenuIcon /> | |
| </IconButton> | |
| </Box> | |
| </Box> | |
| {/* Chat Area */} | |
| <Box | |
| component="main" | |
| className="chat-pane" | |
| sx={{ | |
| flexGrow: 1, | |
| display: 'flex', | |
| flexDirection: 'column', | |
| overflow: 'hidden', | |
| background: 'linear-gradient(180deg, var(--bg), var(--panel))', | |
| padding: '24px', | |
| }} | |
| > | |
| <MessageList messages={messages} isProcessing={isProcessing} /> | |
| <ChatInput onSend={handleSendMessage} disabled={isProcessing || !isConnected} /> | |
| </Box> | |
| </Box> | |
| {/* Resize Handle */} | |
| {isRightPanelOpen && ( | |
| <Box | |
| onMouseDown={startResizing} | |
| sx={{ | |
| width: '4px', | |
| cursor: 'col-resize', | |
| bgcolor: 'divider', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| transition: 'background-color 0.2s', | |
| zIndex: 1300, | |
| overflow: 'hidden', | |
| '&:hover': { | |
| bgcolor: 'primary.main', | |
| }, | |
| }} | |
| > | |
| <DragIndicatorIcon | |
| sx={{ | |
| fontSize: '0.8rem', | |
| color: 'text.secondary', | |
| pointerEvents: 'none', | |
| }} | |
| /> | |
| </Box> | |
| )} | |
| {/* Right Panel Drawer */} | |
| <Box | |
| component="nav" | |
| sx={{ | |
| width: { md: isRightPanelOpen ? rightPanelWidth : 0 }, | |
| flexShrink: { md: 0 }, | |
| transition: isResizing.current ? 'none' : 'width 0.2s', | |
| overflow: 'hidden', | |
| }} | |
| > | |
| <Drawer | |
| anchor="right" | |
| variant="persistent" | |
| sx={{ | |
| display: { xs: 'none', md: 'block' }, | |
| '& .MuiDrawer-paper': { | |
| boxSizing: 'border-box', | |
| width: rightPanelWidth, | |
| borderLeft: 'none', | |
| top: 0, | |
| height: '100%', | |
| bgcolor: 'var(--panel)', | |
| }, | |
| }} | |
| open={isRightPanelOpen} | |
| > | |
| <CodePanel /> | |
| </Drawer> | |
| </Box> | |
| {/* Settings Modal */} | |
| <SettingsModal open={showSettings} onClose={() => setShowSettings(false)} /> | |
| </Box> | |
| ); | |
| } | |