'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import type { UseTranslationSubmitReturn, TranslationConfig, TranslationStatus, TranslationSubmitResponse, TranslationStatusResponse } from './types'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; const POLLING_INTERVAL_MS = 2000; const MAX_POLLING_FAILURES = 3; export function useTranslationSubmit(): UseTranslationSubmitReturn { const [jobId, setJobId] = useState(null); const [status, setStatus] = useState('idle'); const [progress, setProgress] = useState(0); const [currentStep, setCurrentStep] = useState(''); const [error, setError] = useState(null); const [estimatedRemaining, setEstimatedRemaining] = useState(null); const [fileName, setFileName] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [pollingFailures, setPollingFailures] = useState(0); const [isPolling, setIsPolling] = useState(false); const pollingIntervalRef = useRef(null); const isPollingRef = useRef(false); // Use a ref for failure count to avoid stale closure in the interval callback. // If we relied on state, the setInterval callback would always read the initial // value of pollingFailures (0) and never reach MAX_POLLING_FAILURES. const pollingFailuresRef = useRef(0); const stopPolling = useCallback(() => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } isPollingRef.current = false; setIsPolling(false); }, []); const pollProgress = useCallback(async (id: string) => { if (isPollingRef.current) return; isPollingRef.current = true; try { const token = localStorage.getItem('token'); const headers: Record = {}; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${API_BASE}/api/v1/translations/${id}`, { headers }); if (!response.ok) { if (response.status === 404) { stopPolling(); setStatus('failed'); setError('Translation job not found'); return; } throw new Error(`HTTP error! status: ${response.status}`); } const data: TranslationStatusResponse = await response.json(); const job = data.data; setStatus(job.status as TranslationStatus); setProgress(job.progress_percent || 0); setCurrentStep(job.current_step || ''); setEstimatedRemaining(data.meta.estimated_remaining_seconds ?? null); pollingFailuresRef.current = 0; setPollingFailures(0); if (job.file_name) { setFileName(job.file_name); } if (job.status === 'completed' || job.status === 'failed') { stopPolling(); if (job.status === 'failed') { setError(job.error_message || 'Translation failed'); } } } catch (err) { console.error('Polling error:', err); pollingFailuresRef.current += 1; setPollingFailures(pollingFailuresRef.current); if (pollingFailuresRef.current >= MAX_POLLING_FAILURES) { stopPolling(); setStatus('failed'); setError('Lost connection to translation service. Please check your internet connection and try again.'); } } finally { isPollingRef.current = false; } }, [stopPolling]); const startPolling = useCallback((id: string) => { stopPolling(); pollingFailuresRef.current = 0; setIsPolling(true); setPollingFailures(0); pollProgress(id); pollingIntervalRef.current = setInterval(() => { pollProgress(id); }, POLLING_INTERVAL_MS); }, [pollProgress, stopPolling]); const submitTranslation = useCallback(async (file: File, config: TranslationConfig) => { setIsSubmitting(true); setError(null); setProgress(0); setCurrentStep('Uploading file...'); setEstimatedRemaining(null); setStatus('processing'); // IMPORTANT: Set to 'processing' IMMEDIATELY so progress bar shows setFileName(file.name); setJobId(null); try { const formData = new FormData(); formData.append('file', file); formData.append('source_lang', config.sourceLang); formData.append('target_lang', config.targetLang); formData.append('mode', config.mode); // Provider is configured server-side by admin — only send the provider name. if (config.mode === 'llm' && config.provider) { formData.append('provider', config.provider); } const token = localStorage.getItem('token'); const headers: Record = {}; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${API_BASE}/api/v1/translate`, { method: 'POST', headers, body: formData, }); if (!response.ok) { let errorMessage = `Translation failed: ${response.status}`; try { const errorData = await response.json(); errorMessage = errorData.message || errorData.error || errorMessage; } catch { // Response not JSON, use default message } throw new Error(errorMessage); } const data: TranslationSubmitResponse = await response.json(); setJobId(data.data.id); setFileName(data.data.file_name || file.name); setProgress(data.data.progress_percent || 5); // Start with at least 5% setCurrentStep(data.data.current_step || 'Translating...'); startPolling(data.data.id); } catch (err) { setStatus('failed'); setError(err instanceof Error ? err.message : 'Translation failed'); setIsSubmitting(false); } // NOTE: Don't set isSubmitting(false) here - let polling handle the transition }, [startPolling]); const reset = useCallback(() => { stopPolling(); setJobId(null); setStatus('idle'); setProgress(0); setCurrentStep(''); setError(null); setEstimatedRemaining(null); setFileName(null); setIsSubmitting(false); setPollingFailures(0); }, [stopPolling]); useEffect(() => { return () => { stopPolling(); }; }, [stopPolling]); return { submitTranslation, jobId, status, progress, currentStep, error, estimatedRemaining, fileName, reset, isSubmitting, isPolling, pollingFailures, }; }