Skip to content

Commit 73a541f

Browse files
authored
Merge pull request #272 from Two-Weeks-Team/fix/full-techniques-sse-and-eta
fix(frontend): resolve SSE warnings, add ETA, and prevent DOM errors in full_techniques mode
2 parents c7c7782 + 89bbbb9 commit 73a541f

4 files changed

Lines changed: 123 additions & 63 deletions

File tree

frontend/src/components/evaluation/FullTechniquesProgress.tsx

Lines changed: 64 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -95,69 +95,77 @@ export const FullTechniquesProgress: React.FC<FullTechniquesProgressProps> = ({
9595
))}
9696
</div>
9797

98-
{(state.currentStage === 'deep_synthesis' || state.currentStage === 'quality_gate' || state.currentStage === 'complete') && (
99-
<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">
100-
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full text-center border border-[#722F37]/20 relative overflow-hidden">
101-
102-
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-[#722F37] via-[#DAA520] to-[#722F37]" />
103-
104-
{state.currentStage === 'complete' ? (
105-
<div className="space-y-6">
106-
<div className="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4">
107-
<CheckCircle2 className="w-10 h-10 text-emerald-600" />
108-
</div>
109-
<div>
110-
<h2 className="text-2xl font-bold text-[#722F37] mb-2">Analysis Complete</h2>
111-
<p className="text-gray-600">Your vintage report is ready.</p>
112-
</div>
113-
{state.totalScore !== undefined && (
114-
<div className="py-4">
115-
<span className={`text-5xl font-bold ${getScoreColor(state.totalScore)}`}>
116-
{state.totalScore}
117-
</span>
118-
<span className="text-gray-400 text-xl">/100</span>
119-
</div>
120-
)}
121-
<button
122-
onClick={() => router.push(`/evaluate/${evaluationId}/result`)}
123-
className="w-full py-3 bg-[#722F37] text-white rounded-lg font-semibold hover:bg-[#5A252C] transition-colors"
124-
>
125-
View Report
126-
</button>
98+
<div
99+
className={`fixed inset-0 bg-black/20 backdrop-blur-sm z-40 flex items-center justify-center p-4 transition-opacity duration-500 ${
100+
(state.currentStage === 'deep_synthesis' || state.currentStage === 'quality_gate' || state.currentStage === 'complete')
101+
? 'opacity-100'
102+
: 'opacity-0 pointer-events-none'
103+
}`}
104+
style={{ visibility: (state.currentStage === 'deep_synthesis' || state.currentStage === 'quality_gate' || state.currentStage === 'complete') ? 'visible' : 'hidden' }}
105+
>
106+
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full text-center border border-[#722F37]/20 relative overflow-hidden">
107+
108+
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-[#722F37] via-[#DAA520] to-[#722F37]" />
109+
110+
<div className={state.currentStage === 'complete' ? 'block' : 'hidden'}>
111+
<div className="space-y-6">
112+
<div className="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4">
113+
<CheckCircle2 className="w-10 h-10 text-emerald-600" />
127114
</div>
128-
) : (
129-
<div className="space-y-6">
130-
<div className="w-20 h-20 bg-[#FAF4E8] rounded-full flex items-center justify-center mx-auto mb-4 relative">
131-
<Sparkles className="w-10 h-10 text-[#DAA520] animate-pulse" />
132-
<div className="absolute inset-0 border-4 border-[#DAA520]/30 rounded-full border-t-[#DAA520] animate-spin" />
133-
</div>
134-
<div>
135-
<h2 className="text-2xl font-bold text-[#722F37] mb-2">
136-
{state.currentStage === 'deep_synthesis' ? 'Deep Synthesis' : 'Quality Gate'}
137-
</h2>
138-
<p className="text-gray-600">
139-
{state.currentStage === 'deep_synthesis'
140-
? 'Connecting patterns across categories...'
141-
: 'Finalizing scores and recommendations...'}
142-
</p>
143-
</div>
115+
<div>
116+
<h2 className="text-2xl font-bold text-[#722F37] mb-2">Analysis Complete</h2>
117+
<p className="text-gray-600">Your vintage report is ready.</p>
144118
</div>
145-
)}
119+
<div className={`py-4 ${state.totalScore !== undefined ? 'block' : 'hidden'}`}>
120+
<span className={`text-5xl font-bold ${getScoreColor(state.totalScore ?? 0)}`}>
121+
{state.totalScore ?? 0}
122+
</span>
123+
<span className="text-gray-400 text-xl">/100</span>
124+
</div>
125+
<button
126+
onClick={() => router.push(`/evaluate/${evaluationId}/result`)}
127+
className="w-full py-3 bg-[#722F37] text-white rounded-lg font-semibold hover:bg-[#5A252C] transition-colors"
128+
>
129+
View Report
130+
</button>
131+
</div>
132+
</div>
133+
134+
<div className={state.currentStage !== 'complete' ? 'block' : 'hidden'}>
135+
<div className="space-y-6">
136+
<div className="w-20 h-20 bg-[#FAF4E8] rounded-full flex items-center justify-center mx-auto mb-4 relative">
137+
<Sparkles className="w-10 h-10 text-[#DAA520] animate-pulse" />
138+
<div className="absolute inset-0 border-4 border-[#DAA520]/30 rounded-full border-t-[#DAA520] animate-spin" />
139+
</div>
140+
<div>
141+
<h2 className="text-2xl font-bold text-[#722F37] mb-2">
142+
{state.currentStage === 'deep_synthesis' ? 'Deep Synthesis' : 'Quality Gate'}
143+
</h2>
144+
<p className="text-gray-600">
145+
{state.currentStage === 'deep_synthesis'
146+
? 'Connecting patterns across categories...'
147+
: 'Finalizing scores and recommendations...'}
148+
</p>
149+
</div>
150+
</div>
146151
</div>
147152
</div>
148-
)}
153+
</div>
149154

150-
{state.error && (
151-
<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">
152-
<div className="flex items-start gap-3">
153-
<AlertTriangle className="text-red-500 flex-shrink-0" />
154-
<div>
155-
<h3 className="font-bold text-gray-900">Error</h3>
156-
<p className="text-sm text-gray-600">{state.error}</p>
157-
</div>
155+
<div
156+
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 ${
157+
state.error ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full pointer-events-none'
158+
}`}
159+
style={{ visibility: state.error ? 'visible' : 'hidden' }}
160+
>
161+
<div className="flex items-start gap-3">
162+
<AlertTriangle className="text-red-500 flex-shrink-0" />
163+
<div>
164+
<h3 className="font-bold text-gray-900">Error</h3>
165+
<p className="text-sm text-gray-600">{state.error || ''}</p>
158166
</div>
159167
</div>
160-
)}
168+
</div>
161169

162170
</main>
163171
</div>

frontend/src/components/evaluation/ProgressTopBar.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,13 @@ export const ProgressTopBar: React.FC<ProgressTopBarProps> = ({
6161
</div>
6262

6363
<div className="flex items-center gap-4 text-sm">
64-
{enrichmentMessage && (
65-
<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">
66-
<Loader2 size={12} className="animate-spin" />
67-
<span className="truncate max-w-[150px]">{enrichmentMessage}</span>
68-
</div>
69-
)}
64+
<div
65+
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'}`}
66+
style={{ visibility: enrichmentMessage ? 'visible' : 'hidden' }}
67+
>
68+
<Loader2 size={12} className="animate-spin" />
69+
<span className="truncate max-w-[150px]">{enrichmentMessage || 'Loading...'}</span>
70+
</div>
7071
<div className="hidden md:block text-gray-600 font-medium">
7172
<span className="text-[#722F37] font-bold">{completedTechniques}</span>
7273
<span className="text-gray-400 mx-1">/</span>

frontend/src/hooks/useEvaluationStream.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,22 @@ export const useEvaluationStream = (evaluationId: string): UseEvaluationStreamRe
155155
case 'error':
156156
break;
157157

158+
// Full techniques mode events - handled by useFullTechniquesStream
159+
// Silently ignore here to prevent console warnings
160+
case 'technique_start':
161+
case 'technique_complete':
162+
case 'technique_error':
163+
case 'category_start':
164+
case 'category_complete':
165+
case 'deep_synthesis_start':
166+
case 'deep_synthesis_complete':
167+
case 'quality_gate_complete':
168+
case 'enrichment_start':
169+
case 'enrichment_complete':
170+
case 'enrichment_error':
171+
case 'metrics_update':
172+
break;
173+
158174
default:
159175
console.warn('Unknown event type:', eventType);
160176
}

frontend/src/hooks/useFullTechniquesStream.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ const INITIAL_CATEGORIES: Record<string, CategoryStatus> = Object.entries(CATEGO
8989
{}
9090
);
9191

92+
const ESTIMATED_TOTAL_SECONDS = 600;
93+
9294
const initialState: FullTechniquesStreamState = {
9395
connectionStatus: 'connecting',
9496
retryCount: 0,
@@ -103,6 +105,8 @@ const initialState: FullTechniquesStreamState = {
103105
isComplete: false,
104106
tokensUsed: 0,
105107
costUsd: 0,
108+
startedAt: new Date().toISOString(),
109+
etaSeconds: ESTIMATED_TOTAL_SECONDS,
106110
enrichmentPhase: 'idle',
107111
enrichmentMessage: null,
108112
enrichmentStatus: {
@@ -116,7 +120,8 @@ type Action =
116120
| { type: 'CONNECTION_CHANGE'; status: FullTechniquesStreamState['connectionStatus']; retryCount?: number }
117121
| { type: 'EVENT_RECEIVED'; event: SSEEvent }
118122
| { type: 'ERROR'; error: string }
119-
| { type: 'RESET' };
123+
| { type: 'RESET' }
124+
| { type: 'UPDATE_ETA'; elapsedSeconds: number };
120125

121126
function reducer(state: FullTechniquesStreamState, action: Action): FullTechniquesStreamState {
122127
switch (action.type) {
@@ -334,6 +339,23 @@ function reducer(state: FullTechniquesStreamState, action: Action): FullTechniqu
334339
return newState;
335340
}
336341

342+
case 'UPDATE_ETA': {
343+
if (state.isComplete || state.completedTechniques === 0) {
344+
return state;
345+
}
346+
347+
const { elapsedSeconds } = action;
348+
const completionRatio = state.completedTechniques / state.totalTechniques;
349+
350+
if (completionRatio > 0 && completionRatio < 1) {
351+
const estimatedTotalTime = elapsedSeconds / completionRatio;
352+
const remainingSeconds = Math.max(0, Math.ceil(estimatedTotalTime - elapsedSeconds));
353+
return { ...state, etaSeconds: remainingSeconds };
354+
}
355+
356+
return state;
357+
}
358+
337359
default:
338360
return state;
339361
}
@@ -427,5 +449,18 @@ export function useFullTechniquesStream(evaluationId: string | null) {
427449
};
428450
}, [evaluationId, state.isComplete, token]);
429451

452+
useEffect(() => {
453+
if (!evaluationId || state.isComplete) return;
454+
455+
const startTime = state.startedAt ? new Date(state.startedAt).getTime() : Date.now();
456+
457+
const interval = setInterval(() => {
458+
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
459+
dispatch({ type: 'UPDATE_ETA', elapsedSeconds });
460+
}, 5000);
461+
462+
return () => clearInterval(interval);
463+
}, [evaluationId, state.isComplete, state.startedAt]);
464+
430465
return state;
431466
}

0 commit comments

Comments
 (0)