improve AI

This commit is contained in:
Andras Schmelczer 2026-03-15 11:09:01 +00:00
parent daf830c5ed
commit b3a7ab40c8
7 changed files with 118 additions and 17 deletions

View file

@ -1,22 +1,36 @@
import { useState, useCallback, useRef } from 'react';
import type { FeatureFilters } from '../types';
import type { TransportMode } from './useTravelTime';
import { apiUrl, authHeaders, logNonAbortError } from '../lib/api';
export interface AiTravelTimeFilter {
mode: TransportMode;
slug: string;
label: string;
min?: number;
max?: number;
}
interface AiFiltersResult {
filters: FeatureFilters;
travelTimeFilters: AiTravelTimeFilter[];
notes: string;
}
export type AiFilterErrorType = 'auth' | 'verification' | 'limit' | 'error';
interface UseAiFiltersResult {
fetchAiFilters: (query: string) => Promise<AiFiltersResult | null>;
loading: boolean;
error: string | null;
errorType: AiFilterErrorType | null;
notes: string | null;
}
export function useAiFilters(): UseAiFiltersResult {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [errorType, setErrorType] = useState<AiFilterErrorType | null>(null);
const [notes, setNotes] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
@ -27,6 +41,7 @@ export function useAiFilters(): UseAiFiltersResult {
setLoading(true);
setError(null);
setErrorType(null);
setNotes(null);
try {
@ -43,12 +58,36 @@ export function useAiFilters(): UseAiFiltersResult {
if (!response.ok) {
const text = await response.text();
throw new Error(text || `HTTP ${response.status}`);
if (response.status === 401) {
setErrorType('auth');
setError(text || 'Login required');
} else if (response.status === 403) {
setErrorType('verification');
setError(text || 'Email verification required');
} else if (response.status === 429) {
setErrorType('limit');
setError(text || 'Weekly usage limit reached');
} else {
setErrorType('error');
setError(text || `HTTP ${response.status}`);
}
setLoading(false);
return null;
}
const json = await response.json();
const travelTimeFilters: AiTravelTimeFilter[] = (json.travel_time_filters || []).map(
(tt: { mode: string; slug: string; label: string; min?: number; max?: number }) => ({
mode: tt.mode as TransportMode,
slug: tt.slug,
label: tt.label,
min: tt.min,
max: tt.max,
})
);
const result: AiFiltersResult = {
filters: json.filters as FeatureFilters,
travelTimeFilters,
notes: json.notes || '',
};
setNotes(result.notes || null);
@ -58,11 +97,12 @@ export function useAiFilters(): UseAiFiltersResult {
if (controller.signal.aborted) return null;
logNonAbortError('ai-filters', err);
const message = err instanceof Error ? err.message : 'Failed to generate filters';
setErrorType('error');
setError(message);
setLoading(false);
return null;
}
}, []);
return { fetchAiFilters, loading, error, notes };
return { fetchAiFilters, loading, error, errorType, notes };
}

View file

@ -246,7 +246,9 @@ export function useMapData({
const data = (viewFeature && dragFeatureRef.current === viewFeature ? dragHexData : null) ?? rawData;
const effectivePostcodeData = (viewFeature && dragFeatureRef.current === viewFeature ? dragPostcodeData : null) ?? postcodeData;
// Compute p5/p95 from visible data for the viewed feature
// Compute p5/p95 from committed data for the viewed feature.
// Always uses rawData/postcodeData (not drag preview data) so the color
// scale stays stable while dragging a filter slider.
const dataRange = useMemo((): [number, number] | null => {
if (!viewFeature) return null;
@ -260,8 +262,8 @@ export function useMapData({
const vals: number[] = [];
if (usePostcodeView) {
if (effectivePostcodeData.length === 0) return null;
for (const feat of effectivePostcodeData) {
if (postcodeData.length === 0) return null;
for (const feat of postcodeData) {
if (bounds) {
const [lng, lat] = feat.properties.centroid as [number, number];
if (lat < bounds.south || lat > bounds.north || lng < bounds.west || lng > bounds.east)
@ -271,8 +273,8 @@ export function useMapData({
if (typeof val === 'number' && !isNaN(val)) vals.push(val);
}
} else {
if (data.length === 0) return null;
for (const item of data) {
if (rawData.length === 0) return null;
for (const item of rawData) {
if (bounds) {
const { lat, lon } = item;
if (lat < bounds.south || lat > bounds.north || lon < bounds.west || lon > bounds.east)
@ -289,7 +291,7 @@ export function useMapData({
percentile(vals, COLOR_RANGE_LOW_PERCENTILE),
percentile(vals, COLOR_RANGE_HIGH_PERCENTILE),
];
}, [viewFeature, data, effectivePostcodeData, usePostcodeView, features, bounds]);
}, [viewFeature, rawData, postcodeData, usePostcodeView, features, bounds]);
// Color range for the legend and hex coloring
const colorRange = useMemo((): [number, number] | null => {

View file

@ -83,6 +83,10 @@ export function useTravelTime(initial?: TravelTimeInitial) {
[]
);
const handleSetEntries = useCallback((newEntries: TravelTimeEntry[]) => {
setEntries(newEntries);
}, []);
/** Entries that have a destination selected (slug is set) */
const activeEntries = useMemo(
() => entries.filter((e) => e.slug !== ''),
@ -95,6 +99,7 @@ export function useTravelTime(initial?: TravelTimeInitial) {
handleAddEntry,
handleRemoveEntry,
handleSetDestination,
handleSetEntries,
handleTimeRangeChange,
handleToggleBest,
};