Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 64 additions & 56 deletions frontend/src/components/evaluation/FullTechniquesProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,69 +95,77 @@ export const FullTechniquesProgress: React.FC<FullTechniquesProgressProps> = ({
))}
</div>

{(state.currentStage === 'deep_synthesis' || state.currentStage === 'quality_gate' || state.currentStage === 'complete') && (
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-40 flex items-center justify-center p-4 animate-in fade-in duration-500">
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full text-center border border-[#722F37]/20 relative overflow-hidden">

<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-[#722F37] via-[#DAA520] to-[#722F37]" />

{state.currentStage === 'complete' ? (
<div className="space-y-6">
<div className="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-10 h-10 text-emerald-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-[#722F37] mb-2">Analysis Complete</h2>
<p className="text-gray-600">Your vintage report is ready.</p>
</div>
{state.totalScore !== undefined && (
<div className="py-4">
<span className={`text-5xl font-bold ${getScoreColor(state.totalScore)}`}>
{state.totalScore}
</span>
<span className="text-gray-400 text-xl">/100</span>
</div>
)}
<button
onClick={() => router.push(`/evaluate/${evaluationId}/result`)}
className="w-full py-3 bg-[#722F37] text-white rounded-lg font-semibold hover:bg-[#5A252C] transition-colors"
>
View Report
</button>
<div
className={`fixed inset-0 bg-black/20 backdrop-blur-sm z-40 flex items-center justify-center p-4 transition-opacity duration-500 ${
(state.currentStage === 'deep_synthesis' || state.currentStage === 'quality_gate' || state.currentStage === 'complete')
? 'opacity-100'
: 'opacity-0 pointer-events-none'
}`}
style={{ visibility: (state.currentStage === 'deep_synthesis' || state.currentStage === 'quality_gate' || state.currentStage === 'complete') ? 'visible' : 'hidden' }}
Comment on lines +99 to +104

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The condition to check the current stage is repeated and a bit long. For better readability and maintainability, you can use an array and the includes method. This also makes it easier to add more stages in the future.

Suggested change
className={`fixed inset-0 bg-black/20 backdrop-blur-sm z-40 flex items-center justify-center p-4 transition-opacity duration-500 ${
(state.currentStage === 'deep_synthesis' || state.currentStage === 'quality_gate' || state.currentStage === 'complete')
? 'opacity-100'
: 'opacity-0 pointer-events-none'
}`}
style={{ visibility: (state.currentStage === 'deep_synthesis' || state.currentStage === 'quality_gate' || state.currentStage === 'complete') ? 'visible' : 'hidden' }}
className={`fixed inset-0 bg-black/20 backdrop-blur-sm z-40 flex items-center justify-center p-4 transition-opacity duration-500 ${
['deep_synthesis', 'quality_gate', 'complete'].includes(state.currentStage)
? 'opacity-100'
: 'opacity-0 pointer-events-none'
}`}
style={{ visibility: ['deep_synthesis', 'quality_gate', 'complete'].includes(state.currentStage) ? 'visible' : 'hidden' }}

>
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full text-center border border-[#722F37]/20 relative overflow-hidden">

<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-[#722F37] via-[#DAA520] to-[#722F37]" />

<div className={state.currentStage === 'complete' ? 'block' : 'hidden'}>
<div className="space-y-6">
<div className="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-10 h-10 text-emerald-600" />
</div>
) : (
<div className="space-y-6">
<div className="w-20 h-20 bg-[#FAF4E8] rounded-full flex items-center justify-center mx-auto mb-4 relative">
<Sparkles className="w-10 h-10 text-[#DAA520] animate-pulse" />
<div className="absolute inset-0 border-4 border-[#DAA520]/30 rounded-full border-t-[#DAA520] animate-spin" />
</div>
<div>
<h2 className="text-2xl font-bold text-[#722F37] mb-2">
{state.currentStage === 'deep_synthesis' ? 'Deep Synthesis' : 'Quality Gate'}
</h2>
<p className="text-gray-600">
{state.currentStage === 'deep_synthesis'
? 'Connecting patterns across categories...'
: 'Finalizing scores and recommendations...'}
</p>
</div>
<div>
<h2 className="text-2xl font-bold text-[#722F37] mb-2">Analysis Complete</h2>
<p className="text-gray-600">Your vintage report is ready.</p>
</div>
)}
<div className={`py-4 ${state.totalScore !== undefined ? 'block' : 'hidden'}`}>
<span className={`text-5xl font-bold ${getScoreColor(state.totalScore ?? 0)}`}>
{state.totalScore ?? 0}
</span>
<span className="text-gray-400 text-xl">/100</span>
</div>
<button
onClick={() => router.push(`/evaluate/${evaluationId}/result`)}
className="w-full py-3 bg-[#722F37] text-white rounded-lg font-semibold hover:bg-[#5A252C] transition-colors"
>
View Report
</button>
</div>
</div>

<div className={state.currentStage !== 'complete' ? 'block' : 'hidden'}>
<div className="space-y-6">
<div className="w-20 h-20 bg-[#FAF4E8] rounded-full flex items-center justify-center mx-auto mb-4 relative">
<Sparkles className="w-10 h-10 text-[#DAA520] animate-pulse" />
<div className="absolute inset-0 border-4 border-[#DAA520]/30 rounded-full border-t-[#DAA520] animate-spin" />
</div>
<div>
<h2 className="text-2xl font-bold text-[#722F37] mb-2">
{state.currentStage === 'deep_synthesis' ? 'Deep Synthesis' : 'Quality Gate'}
</h2>
<p className="text-gray-600">
{state.currentStage === 'deep_synthesis'
? 'Connecting patterns across categories...'
: 'Finalizing scores and recommendations...'}
</p>
</div>
</div>
</div>
</div>
)}
</div>

{state.error && (
<div className="fixed bottom-4 right-4 max-w-md bg-white border-l-4 border-red-500 shadow-lg rounded-r-lg p-4 animate-in slide-in-from-right">
<div className="flex items-start gap-3">
<AlertTriangle className="text-red-500 flex-shrink-0" />
<div>
<h3 className="font-bold text-gray-900">Error</h3>
<p className="text-sm text-gray-600">{state.error}</p>
</div>
<div
className={`fixed bottom-4 right-4 max-w-md bg-white border-l-4 border-red-500 shadow-lg rounded-r-lg p-4 transition-all duration-300 ${
state.error ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full pointer-events-none'
}`}
style={{ visibility: state.error ? 'visible' : 'hidden' }}
>
<div className="flex items-start gap-3">
<AlertTriangle className="text-red-500 flex-shrink-0" />
<div>
<h3 className="font-bold text-gray-900">Error</h3>
<p className="text-sm text-gray-600">{state.error || ''}</p>
</div>
</div>
)}
</div>

</main>
</div>
Expand Down
13 changes: 7 additions & 6 deletions frontend/src/components/evaluation/ProgressTopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ export const ProgressTopBar: React.FC<ProgressTopBarProps> = ({
</div>

<div className="flex items-center gap-4 text-sm">
{enrichmentMessage && (
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-700 text-xs font-medium">
<Loader2 size={12} className="animate-spin" />
<span className="truncate max-w-[150px]">{enrichmentMessage}</span>
</div>
)}
<div
className={`flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-700 text-xs font-medium transition-opacity duration-300 ${enrichmentMessage ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
style={{ visibility: enrichmentMessage ? 'visible' : 'hidden' }}
>
<Loader2 size={12} className="animate-spin" />
<span className="truncate max-w-[150px]">{enrichmentMessage || 'Loading...'}</span>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback text 'Loading...' will never be visible because the parent div is hidden when enrichmentMessage is falsy. This makes the fallback redundant. You can safely remove it to make the code cleaner.

Suggested change
<span className="truncate max-w-[150px]">{enrichmentMessage || 'Loading...'}</span>
<span className="truncate max-w-[150px]">{enrichmentMessage}</span>

</div>
<div className="hidden md:block text-gray-600 font-medium">
<span className="text-[#722F37] font-bold">{completedTechniques}</span>
<span className="text-gray-400 mx-1">/</span>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/hooks/useEvaluationStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,22 @@ export const useEvaluationStream = (evaluationId: string): UseEvaluationStreamRe
case 'error':
break;

// Full techniques mode events - handled by useFullTechniquesStream
// Silently ignore here to prevent console warnings
case 'technique_start':
case 'technique_complete':
case 'technique_error':
case 'category_start':
case 'category_complete':
case 'deep_synthesis_start':
case 'deep_synthesis_complete':
case 'quality_gate_complete':
case 'enrichment_start':
case 'enrichment_complete':
case 'enrichment_error':
case 'metrics_update':
break;

default:
console.warn('Unknown event type:', eventType);
}
Expand Down
37 changes: 36 additions & 1 deletion frontend/src/hooks/useFullTechniquesStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ const INITIAL_CATEGORIES: Record<string, CategoryStatus> = Object.entries(CATEGO
{}
);

const ESTIMATED_TOTAL_SECONDS = 600;

const initialState: FullTechniquesStreamState = {
connectionStatus: 'connecting',
retryCount: 0,
Expand All @@ -103,6 +105,8 @@ const initialState: FullTechniquesStreamState = {
isComplete: false,
tokensUsed: 0,
costUsd: 0,
startedAt: new Date().toISOString(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

initialState is a constant defined at the module level, so new Date().toISOString() is executed only once when the module is first loaded. This results in a stale startedAt timestamp for subsequent evaluations, leading to incorrect ETA calculations.

To fix this, startedAt should be initialized when the state is actually created or reset. A common React pattern is to use the lazy initialization function of useReducer.

Since a code suggestion cannot span multiple parts of the file, here is an example of how you could implement this:

// 1. Create a function to generate the initial state
const createInitialState = (): FullTechniquesStreamState => ({
  // ... other initial state properties from your existing initialState object
  startedAt: new Date().toISOString(),
  etaSeconds: ESTIMATED_TOTAL_SECONDS,
});

// 2. Use this function for lazy initialization in the hook
const [state, dispatch] = useReducer(reducer, undefined, createInitialState);

// 3. Use the function in your reducer's RESET case
case 'RESET':
  return createInitialState();

etaSeconds: ESTIMATED_TOTAL_SECONDS,
enrichmentPhase: 'idle',
enrichmentMessage: null,
enrichmentStatus: {
Expand All @@ -116,7 +120,8 @@ type Action =
| { type: 'CONNECTION_CHANGE'; status: FullTechniquesStreamState['connectionStatus']; retryCount?: number }
| { type: 'EVENT_RECEIVED'; event: SSEEvent }
| { type: 'ERROR'; error: string }
| { type: 'RESET' };
| { type: 'RESET' }
| { type: 'UPDATE_ETA'; elapsedSeconds: number };

function reducer(state: FullTechniquesStreamState, action: Action): FullTechniquesStreamState {
switch (action.type) {
Expand Down Expand Up @@ -334,6 +339,23 @@ function reducer(state: FullTechniquesStreamState, action: Action): FullTechniqu
return newState;
}

case 'UPDATE_ETA': {
if (state.isComplete || state.completedTechniques === 0) {
return state;
}

const { elapsedSeconds } = action;
const completionRatio = state.completedTechniques / state.totalTechniques;

if (completionRatio > 0 && completionRatio < 1) {
const estimatedTotalTime = elapsedSeconds / completionRatio;
const remainingSeconds = Math.max(0, Math.ceil(estimatedTotalTime - elapsedSeconds));
return { ...state, etaSeconds: remainingSeconds };
}

return state;
}

default:
return state;
}
Expand Down Expand Up @@ -427,5 +449,18 @@ export function useFullTechniquesStream(evaluationId: string | null) {
};
}, [evaluationId, state.isComplete, token]);

useEffect(() => {
if (!evaluationId || state.isComplete) return;

const startTime = state.startedAt ? new Date(state.startedAt).getTime() : Date.now();

const interval = setInterval(() => {
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
dispatch({ type: 'UPDATE_ETA', elapsedSeconds });
}, 5000);

return () => clearInterval(interval);
}, [evaluationId, state.isComplete, state.startedAt]);

return state;
}