Add major feature improvements and developer experience enhancements

New Features:
 File Content Syncing - Code changes now persist to file tree (App.tsx)
  - Added handleCodeChange() to update file content in real-time
  - Syncs changes to both files state and openFiles tabs
  - Templates now properly update active file content

 Keyboard Shortcuts System (use-keyboard-shortcuts.ts)
  - Cmd/Ctrl+S - Save file notification
  - Cmd/Ctrl+P - Quick file search (placeholder)
  - Cmd/Ctrl+K - Command palette (placeholder)
  - Cmd/Ctrl+N - New project modal
  - Cmd/Ctrl+/ - Find in editor hint
  - Cross-platform support (Mac/Windows/Linux)
  - Integrated with PostHog analytics

 Enhanced Error Boundary (ErrorBoundary.tsx)
  - Better error UI with stack traces
  - Sentry integration for error reporting
  - Reload and retry options
  - User-friendly error messages
  - Replaced react-error-boundary with custom implementation

 Loading States Infrastructure (loading-spinner.tsx)
  - Reusable LoadingSpinner component (sm/md/lg sizes)
  - LoadingOverlay for full-screen loading
  - Accessible with ARIA labels
  - Ready for async operation improvements

Developer Experience:
- All keyboard shortcuts tracked via PostHog
- Better error debugging with component stack traces
- Auto-save functionality foundation

This commit significantly improves core functionality and sets foundation for:
- File search modal
- Command palette
- Enhanced async operation handling
This commit is contained in:
Claude 2026-01-17 21:53:28 +00:00
parent 281faf1395
commit 1b1466f4ec
No known key found for this signature in database
5 changed files with 280 additions and 5 deletions

View file

@ -12,6 +12,7 @@ import { NewProjectModal, ProjectConfig } from './components/NewProjectModal';
import { ConsolePanel } from './components/ConsolePanel'; import { ConsolePanel } from './components/ConsolePanel';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable';
import { useIsMobile } from './hooks/use-mobile'; import { useIsMobile } from './hooks/use-mobile';
import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { EducationPanel } from './components/EducationPanel'; import { EducationPanel } from './components/EducationPanel';
@ -40,6 +41,63 @@ function App() {
initSentry(); initSentry();
}, []); }, []);
// Keyboard shortcuts
useKeyboardShortcuts([
{
key: 's',
meta: true, // Cmd on Mac
ctrl: true, // Ctrl on Windows/Linux
handler: () => {
toast.success('File saved automatically!');
captureEvent('keyboard_shortcut', { action: 'save' });
},
description: 'Save file',
},
{
key: 'p',
meta: true,
ctrl: true,
handler: () => {
// TODO: Implement file search modal
toast.info('File search coming soon! (Cmd/Ctrl+P)');
captureEvent('keyboard_shortcut', { action: 'file_search' });
},
description: 'Quick file search',
},
{
key: 'k',
meta: true,
ctrl: true,
handler: () => {
// TODO: Implement command palette
toast.info('Command palette coming soon! (Cmd/Ctrl+K)');
captureEvent('keyboard_shortcut', { action: 'command_palette' });
},
description: 'Command palette',
},
{
key: 'n',
meta: true,
ctrl: true,
handler: () => {
setShowNewProject(true);
captureEvent('keyboard_shortcut', { action: 'new_project' });
},
description: 'New project',
},
{
key: '/',
meta: true,
ctrl: true,
handler: () => {
// Monaco editor has built-in Cmd/Ctrl+F for find
toast.info('Use Cmd/Ctrl+F in the editor to find text');
captureEvent('keyboard_shortcut', { action: 'find' });
},
description: 'Find in editor',
},
]);
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => { const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
setUser(user); setUser(user);
localStorage.setItem('aethex-user', JSON.stringify(user)); localStorage.setItem('aethex-user', JSON.stringify(user));
@ -89,6 +147,40 @@ end)`,
const handleTemplateSelect = (templateCode: string) => { const handleTemplateSelect = (templateCode: string) => {
setCode(templateCode); setCode(templateCode);
setCurrentCode(templateCode); setCurrentCode(templateCode);
// Update active file content
if (activeFileId) {
handleCodeChange(templateCode);
}
};
const handleCodeChange = (newCode: string) => {
setCurrentCode(newCode);
setCode(newCode);
// Update the file content in the files tree
if (activeFileId) {
setFiles((prev) => {
const updateFileContent = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === activeFileId) {
return { ...node, content: newCode };
}
if (node.children) {
return { ...node, children: updateFileContent(node.children) };
}
return node;
});
};
return updateFileContent(prev || []);
});
// Also update in openFiles to keep tabs in sync
setOpenFiles((prev) =>
(prev || []).map((file) =>
file.id === activeFileId ? { ...file, content: newCode } : file
)
);
}
}; };
const handleFileSelect = (file: FileNode) => { const handleFileSelect = (file: FileNode) => {
@ -249,7 +341,7 @@ end)`,
onFileClose={handleFileClose} onFileClose={handleFileClose}
/> />
<div className="flex-1"> <div className="flex-1">
<CodeEditor onCodeChange={setCurrentCode} /> <CodeEditor onCodeChange={handleCodeChange} />
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="ai" className="flex-1 m-0"> <TabsContent value="ai" className="flex-1 m-0">

View file

@ -0,0 +1,107 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { AlertTriangle } from '@phosphor-icons/react';
import { captureError } from '../lib/sentry';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
errorInfo: null,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error, errorInfo: null };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
this.setState({
error,
errorInfo,
});
// Report to Sentry
if (typeof captureError === 'function') {
captureError(error, { extra: { errorInfo } });
}
}
private handleReset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
window.location.reload();
};
private handleResetWithoutReload = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="h-screen w-screen flex items-center justify-center bg-background p-4">
<Card className="max-w-2xl w-full p-8">
<div className="flex items-center gap-4 mb-6">
<AlertTriangle className="text-destructive" size={48} weight="fill" />
<div>
<h1 className="text-2xl font-bold">Something went wrong</h1>
<p className="text-muted-foreground mt-1">
An unexpected error occurred in the application
</p>
</div>
</div>
{this.state.error && (
<div className="bg-muted p-4 rounded-lg mb-6 overflow-auto max-h-64">
<p className="font-mono text-sm text-destructive font-semibold mb-2">
{this.state.error.toString()}
</p>
{this.state.errorInfo && (
<pre className="font-mono text-xs text-muted-foreground whitespace-pre-wrap">
{this.state.errorInfo.componentStack}
</pre>
)}
</div>
)}
<div className="flex gap-4">
<Button onClick={this.handleReset} className="flex-1">
Reload Application
</Button>
<Button
onClick={this.handleResetWithoutReload}
variant="outline"
className="flex-1"
>
Try Again
</Button>
</div>
<p className="text-xs text-muted-foreground mt-4 text-center">
If this problem persists, please report it to the development team
</p>
</Card>
</div>
);
}
return this.props.children;
}
}

View file

@ -0,0 +1,39 @@
import { cn } from '@/lib/utils';
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
const sizeClasses = {
sm: 'h-4 w-4 border-2',
md: 'h-8 w-8 border-2',
lg: 'h-12 w-12 border-3',
};
return (
<div
className={cn(
'animate-spin rounded-full border-accent border-t-transparent',
sizeClasses[size],
className
)}
role="status"
aria-label="Loading"
>
<span className="sr-only">Loading...</span>
</div>
);
}
export function LoadingOverlay({ message = 'Loading...' }: { message?: string }) {
return (
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50">
<div className="flex flex-col items-center gap-4">
<LoadingSpinner size="lg" />
<p className="text-sm text-muted-foreground">{message}</p>
</div>
</div>
);
}

View file

@ -0,0 +1,38 @@
import { useEffect } from 'react';
export interface KeyboardShortcut {
key: string;
ctrl?: boolean;
meta?: boolean;
shift?: boolean;
alt?: boolean;
handler: (e: KeyboardEvent) => void;
description?: string;
}
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const handleKeyDown = (e: KeyboardEvent) => {
for (const shortcut of shortcuts) {
const keyMatches = e.key.toLowerCase() === shortcut.key.toLowerCase();
const ctrlMatches = shortcut.ctrl === undefined || e.ctrlKey === shortcut.ctrl;
const metaMatches = shortcut.meta === undefined || e.metaKey === shortcut.meta;
const shiftMatches = shortcut.shift === undefined || e.shiftKey === shortcut.shift;
const altMatches = shortcut.alt === undefined || e.altKey === shortcut.alt;
if (keyMatches && ctrlMatches && metaMatches && shiftMatches && altMatches) {
e.preventDefault();
shortcut.handler(e);
break;
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [shortcuts]);
}

View file

@ -1,16 +1,15 @@
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { ErrorBoundary } from "react-error-boundary";
import "@github/spark/spark" import "@github/spark/spark"
import App from './App' import App from './App'
import { ErrorFallback } from './ErrorFallback' import { ErrorBoundary } from './components/ErrorBoundary'
import "./main.css" import "./main.css"
import "./styles/theme.css" import "./styles/theme.css"
import "./index.css" import "./index.css"
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary>
<App /> <App />
</ErrorBoundary> </ErrorBoundary>
) )