Add LLM summary
This commit is contained in:
parent
f5e6894c0f
commit
9e71ed77df
3 changed files with 275 additions and 9 deletions
108
frontend/src/hooks/useAreaSummary.ts
Normal file
108
frontend/src/hooks/useAreaSummary.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { FeatureMeta, FeatureFilters, HexagonStatsResponse } from '../types';
|
||||
import { apiUrl, authHeaders, logNonAbortError } from '../lib/api';
|
||||
|
||||
interface UseAreaSummaryOptions {
|
||||
stats: HexagonStatsResponse | null;
|
||||
hexagonId: string | null;
|
||||
isPostcode: boolean;
|
||||
filters: FeatureFilters;
|
||||
features: FeatureMeta[];
|
||||
}
|
||||
|
||||
interface UseAreaSummaryResult {
|
||||
summary: string;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
retry: () => void;
|
||||
}
|
||||
|
||||
const FORBIDDEN_FEATURES = ['% White', '% Black', '% Asian', '% Mixed', '% Other'];
|
||||
|
||||
export function useAreaSummary({
|
||||
stats,
|
||||
hexagonId,
|
||||
isPostcode,
|
||||
filters,
|
||||
features,
|
||||
}: UseAreaSummaryOptions): UseAreaSummaryResult {
|
||||
const [summary, setSummary] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const fetchSummary = useCallback(async () => {
|
||||
if (!stats || !hexagonId) return;
|
||||
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
setSummary('');
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const filterDescriptions: string[] = [];
|
||||
for (const [name, value] of Object.entries(filters)) {
|
||||
const meta = features.find((f) => f.name === name);
|
||||
if (meta?.type === 'enum') {
|
||||
filterDescriptions.push(`${name}: ${(value as string[]).join(', ')}`);
|
||||
} else {
|
||||
const [min, max] = value as [number, number];
|
||||
filterDescriptions.push(`${name}: ${min}–${max}`);
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
count: stats.count,
|
||||
location: hexagonId,
|
||||
is_postcode: isPostcode,
|
||||
filters: filterDescriptions,
|
||||
numeric_stats: stats.numeric_features.filter(f => !FORBIDDEN_FEATURES.includes(f.name)).map((f) => ({
|
||||
name: f.name,
|
||||
mean: f.mean,
|
||||
})),
|
||||
enum_stats: stats.enum_features.map((f) => ({
|
||||
name: f.name,
|
||||
counts: f.counts,
|
||||
})),
|
||||
};
|
||||
|
||||
const url = apiUrl('area-summary');
|
||||
const response = await fetch(url, authHeaders({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
}));
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(text || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
setSummary(json.summary || '');
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
if (controller.signal.aborted) return;
|
||||
logNonAbortError('area-summary', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to generate summary');
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stats, hexagonId, isPostcode, filters, features]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSummary();
|
||||
return () => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [stats, hexagonId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const retry = useCallback(() => {
|
||||
fetchSummary();
|
||||
}, [fetchSummary]);
|
||||
|
||||
return { summary, loading, error, retry };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue