diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index df8b512..7dd4244 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { CommandConsole } from './components/CommandConsole'; import { LiveView } from './components/LiveView'; import { useMission } from './hooks/useMission'; @@ -11,7 +11,7 @@ function App() { const { mission, latestScreenshot, error } = useMission(currentMissionId); - const handleSubmitMission = async (prompt: string) => { + const handleSubmitMission = useCallback(async (prompt: string) => { setIsSubmitting(true); try { const response = await api.createMission(prompt); @@ -22,7 +22,7 @@ function App() { } finally { setIsSubmitting(false); } - }; + }, []); return (
diff --git a/frontend/src/components/CommandConsole.tsx b/frontend/src/components/CommandConsole.tsx index 07a3dbc..dc5e6b8 100644 --- a/frontend/src/components/CommandConsole.tsx +++ b/frontend/src/components/CommandConsole.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useCallback, useMemo, memo } from 'react'; import { Mission, MissionStepType, MissionStatus } from '../types/mission'; import './CommandConsole.css'; @@ -8,22 +8,25 @@ interface CommandConsoleProps { isSubmitting: boolean; } -export function CommandConsole({ +export const CommandConsole = memo(function CommandConsole({ mission, onSubmitMission, isSubmitting, }: CommandConsoleProps) { const [prompt, setPrompt] = useState(''); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (prompt.trim() && !isSubmitting) { - onSubmitMission(prompt.trim()); - setPrompt(''); - } - }; + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (prompt.trim() && !isSubmitting) { + onSubmitMission(prompt.trim()); + setPrompt(''); + } + }, + [prompt, isSubmitting, onSubmitMission] + ); - const getStepIcon = (type: MissionStepType): string => { + const getStepIcon = useCallback((type: MissionStepType): string => { switch (type) { case MissionStepType.OBSERVATION: return '👁️'; @@ -36,9 +39,9 @@ export function CommandConsole({ default: return '•'; } - }; + }, []); - const getStatusBadge = (status: MissionStatus) => { + const getStatusBadge = useCallback((status: MissionStatus) => { const badges = { [MissionStatus.PENDING]: { label: 'PENDING', className: 'pending' }, [MissionStatus.RUNNING]: { label: 'RUNNING', className: 'running' }, @@ -53,12 +56,22 @@ export function CommandConsole({ const badge = badges[status] || { label: status, className: '' }; return {badge.label}; - }; + }, []); - const formatTimestamp = (timestamp: string): string => { + const formatTimestamp = useCallback((timestamp: string): string => { const date = new Date(timestamp); return date.toLocaleTimeString(); - }; + }, []); + + // Memoize sorted/processed steps to avoid recalculation on every render + const sortedSteps = useMemo(() => { + if (!mission || !mission.steps) return []; + return [...mission.steps].sort((a, b) => { + const aTime = new Date(a.timestamp).getTime(); + const bTime = new Date(b.timestamp).getTime(); + return aTime - bTime; + }); + }, [mission?.steps]); return (
@@ -100,8 +113,8 @@ export function CommandConsole({

Mission Timeline

- {mission && mission.steps.length > 0 ? ( - mission.steps.map((step) => ( + {sortedSteps.length > 0 ? ( + sortedSteps.map((step) => (
{getStepIcon(step.type)}
@@ -126,4 +139,4 @@ export function CommandConsole({
); -} +}); diff --git a/frontend/src/components/LiveView.tsx b/frontend/src/components/LiveView.tsx index 018eb20..1c1f622 100644 --- a/frontend/src/components/LiveView.tsx +++ b/frontend/src/components/LiveView.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import { Mission } from '../types/mission'; import './LiveView.css'; @@ -6,7 +7,7 @@ interface LiveViewProps { latestScreenshot: string | null; } -export function LiveView({ mission, latestScreenshot }: LiveViewProps) { +export const LiveView = memo(function LiveView({ mission, latestScreenshot }: LiveViewProps) { return (
@@ -79,4 +80,4 @@ export function LiveView({ mission, latestScreenshot }: LiveViewProps) { )}
); -} +});