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>
153 lines
4.3 KiB
JavaScript
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;
|