topic-analysis / frontend /src /hooks /useAnalysis.tsx
alexchilton
Initial deployment: Sentiment & Topic Analysis Dashboard
6242ddb
import { useState, useCallback, useContext, createContext, type ReactNode } from 'react';
import type { AnalysisResult, FilterParams, JobStatus } from '../types';
import { api } from '../services/api';
interface AnalysisState {
jobs: JobStatus[];
currentResult: AnalysisResult | null;
activeJobId: string | null;
loading: boolean;
error: string | null;
uploadFile: (file: File, source?: string) => Promise<JobStatus>;
loadJobs: () => Promise<void>;
loadResult: (jobId: string) => Promise<AnalysisResult | undefined>;
selectJob: (jobId: string) => void;
pollJobStatus: (jobId: string, onUpdate?: (status: JobStatus) => void) => Promise<void>;
exportResults: (jobId: string, format: 'csv' | 'json' | 'pdf', filters?: FilterParams) => Promise<void>;
setError: (error: string | null) => void;
}
const AnalysisContext = createContext<AnalysisState | null>(null);
export function AnalysisProvider({ children }: { children: ReactNode }) {
const [jobs, setJobs] = useState<JobStatus[]>([]);
const [currentResult, setCurrentResult] = useState<AnalysisResult | null>(null);
const [activeJobId, setActiveJobId] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const uploadFile = useCallback(async (file: File, source?: string) => {
setLoading(true);
setError(null);
try {
const useChunked = file.size > 10 * 1024 * 1024;
const status = useChunked ? await api.uploadChunked(file) : await api.uploadFile(file, source);
setJobs((prev) => [status, ...prev]);
return status;
} catch (err) {
const msg = err instanceof Error ? err.message : 'Upload failed';
setError(msg);
throw err;
} finally {
setLoading(false);
}
}, []);
const loadJobs = useCallback(async () => {
try {
const data = await api.getJobs();
setJobs(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load jobs');
}
}, []);
const loadResult = useCallback(async (jobId: string) => {
setLoading(true);
setError(null);
try {
const result = await api.getJobResult(jobId);
setCurrentResult(result);
setActiveJobId(jobId);
return result;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load results');
throw err;
} finally {
setLoading(false);
}
}, []);
const selectJob = useCallback((jobId: string) => {
setActiveJobId(jobId);
loadResult(jobId);
}, [loadResult]);
const pollJobStatus = useCallback(
async (jobId: string, onUpdate?: (status: JobStatus) => void) => {
const poll = async () => {
try {
const status = await api.getJobStatus(jobId);
onUpdate?.(status);
if (status.status === 'completed') {
await loadResult(jobId);
return;
}
if (status.status === 'failed') {
setError('Analysis failed');
return;
}
setTimeout(poll, 2000);
} catch {
setTimeout(poll, 5000);
}
};
poll();
},
[loadResult],
);
const exportResults = useCallback(async (jobId: string, format: 'csv' | 'json' | 'pdf', filters?: FilterParams) => {
try {
const blob = await api.exportResults(jobId, format, filters);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `analysis_${jobId}.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
setError(err instanceof Error ? err.message : 'Export failed');
}
}, []);
return (
<AnalysisContext.Provider
value={{
jobs,
currentResult,
activeJobId,
loading,
error,
uploadFile,
loadJobs,
loadResult,
selectJob,
pollJobStatus,
exportResults,
setError,
}}
>
{children}
</AnalysisContext.Provider>
);
}
export function useAnalysis(): AnalysisState {
const context = useContext(AnalysisContext);
if (!context) {
throw new Error('useAnalysis must be used within an AnalysisProvider');
}
return context;
}