Files
claude-web-ui/frontend/src/App.jsx
Nikolas Syring e5d17bfad3 perf: Major performance overhaul with virtual scrolling and context splitting
Phase 1 - Virtual Scrolling:
- Add @tanstack/react-virtual for efficient message list rendering
- Only render visible messages instead of entire history
- Fix auto-scroll using native scrollTop instead of unreliable virtualizer

Phase 2 - Context Optimization:
- Split monolithic SessionContext into 4 specialized contexts
- MessagesContext, SessionsContext, SettingsContext, UIContext
- Prevents unnecessary re-renders across unrelated components

Phase 3 - Compression & Cleanup:
- Enable Brotli compression (~23% smaller than gzip)
- Switch to fholzer/nginx-brotli:v1.28.0 image
- Add automatic upload cleanup for idle sessions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 17:28:03 +01:00

153 lines
4.3 KiB
JavaScript

import { useState, useCallback, useEffect } from 'react';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { HostProvider } from './contexts/HostContext';
import { SettingsProvider } from './contexts/SettingsContext';
import { UIProvider, useUI } from './contexts/UIContext';
import { MessagesProvider } from './contexts/MessagesContext';
import { SessionsProvider, useSessions } from './contexts/SessionsContext';
import { Sidebar } from './components/Sidebar';
import { TabBar } from './components/TabBar';
import { ChatPanel } from './components/ChatPanel';
import { SplitLayout } from './components/SplitLayout';
import { LoginPage } from './components/LoginPage';
import { Menu, Loader2 } from 'lucide-react';
function AppContent() {
const { sessions, createSession } = useSessions();
const { tabOrder, splitSessions, focusedSessionId } = useUI();
const [sidebarOpen, setSidebarOpen] = useState(true);
// Create initial session if none exists
useEffect(() => {
if (tabOrder.length === 0) {
createSession('neko', '/home/sumdex/projects');
}
}, [tabOrder.length, createSession]);
const handleToggleSidebar = useCallback(() => {
setSidebarOpen(prev => !prev);
}, []);
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e) => {
// Ctrl+T: New tab
if (e.ctrlKey && e.key === 't') {
e.preventDefault();
createSession();
}
// Ctrl+B: Toggle sidebar
if (e.ctrlKey && e.key === 'b') {
e.preventDefault();
setSidebarOpen(prev => !prev);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [createSession]);
// Render panel content for a session
const renderPanel = useCallback((sessionId) => {
return <ChatPanel sessionId={sessionId} />;
}, []);
return (
<div className="flex h-screen bg-dark-950">
{/* Sidebar */}
<Sidebar open={sidebarOpen} onToggle={handleToggleSidebar} />
{/* Main Content */}
<div className="flex-1 flex flex-col min-w-0">
{/* Header with TabBar */}
<div className="flex items-center bg-dark-900 border-b border-dark-800">
{/* Sidebar toggle for mobile */}
<button
onClick={handleToggleSidebar}
className="p-3 hover:bg-dark-800 lg:hidden"
>
<Menu className="w-5 h-5" />
</button>
{/* Tab Bar */}
<div className="flex-1 min-w-0">
<TabBar />
</div>
</div>
{/* Content Area */}
<div className="flex-1 min-h-0 overflow-hidden">
{splitSessions.length > 0 ? (
// Split view mode
<SplitLayout
splitSessions={splitSessions}
renderPanel={renderPanel}
/>
) : focusedSessionId ? (
// Single panel mode
<ChatPanel sessionId={focusedSessionId} />
) : (
// No session
<div className="flex items-center justify-center h-full text-dark-500">
<div className="text-center">
<p className="text-lg mb-2">No sessions open</p>
<p className="text-sm">Click the + button to create a new session</p>
</div>
</div>
)}
</div>
</div>
</div>
);
}
// Loading screen while checking auth
function LoadingScreen() {
return (
<div className="min-h-screen bg-dark-950 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-8 h-8 text-orange-500 animate-spin mx-auto mb-4" />
<p className="text-dark-400">Loading...</p>
</div>
</div>
);
}
// Auth wrapper - shows login or main app
function AuthenticatedApp() {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <LoadingScreen />;
}
if (!isAuthenticated) {
return <LoginPage />;
}
return (
<HostProvider>
<SettingsProvider>
<UIProvider>
<MessagesProvider>
<SessionsProvider>
<AppContent />
</SessionsProvider>
</MessagesProvider>
</UIProvider>
</SettingsProvider>
</HostProvider>
);
}
function App() {
return (
<AuthProvider>
<AuthenticatedApp />
</AuthProvider>
);
}
export default App;