good changes
This commit is contained in:
parent
160283f1a1
commit
c997ea46a5
26 changed files with 991 additions and 288 deletions
|
|
@ -19,6 +19,8 @@ export interface AiFiltersResult {
|
|||
summary: string;
|
||||
/** The listing mode used (historical/buy/rent) */
|
||||
listingType: string;
|
||||
/** Number of properties matching the proposed filters (excludes travel time) */
|
||||
matchCount: number;
|
||||
}
|
||||
|
||||
export type AiFilterErrorType = 'auth' | 'limit' | 'error';
|
||||
|
|
@ -43,7 +45,11 @@ interface UseAiFiltersResult {
|
|||
}
|
||||
|
||||
/** Build a human-readable summary of the AI result. */
|
||||
function buildSummary(filters: FeatureFilters, travelTimeFilters: AiTravelTimeFilter[]): string {
|
||||
function buildSummary(
|
||||
filters: FeatureFilters,
|
||||
travelTimeFilters: AiTravelTimeFilter[],
|
||||
matchCount: number
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const [name, value] of Object.entries(filters)) {
|
||||
|
|
@ -63,7 +69,8 @@ function buildSummary(filters: FeatureFilters, travelTimeFilters: AiTravelTimeFi
|
|||
}
|
||||
|
||||
if (parts.length === 0) return 'No filters set';
|
||||
return `Set ${parts.length} filter${parts.length > 1 ? 's' : ''}: ${parts.join(', ')}`;
|
||||
const countStr = matchCount.toLocaleString();
|
||||
return `${countStr} properties match · Set ${parts.length} filter${parts.length > 1 ? 's' : ''}: ${parts.join(', ')}`;
|
||||
}
|
||||
|
||||
export function useAiFilters(): UseAiFiltersResult {
|
||||
|
|
@ -137,13 +144,15 @@ export function useAiFilters(): UseAiFiltersResult {
|
|||
})
|
||||
);
|
||||
const filters = json.filters as FeatureFilters;
|
||||
const summaryText = buildSummary(filters, travelTimeFilters);
|
||||
const matchCount: number = json.match_count ?? 0;
|
||||
const summaryText = buildSummary(filters, travelTimeFilters, matchCount);
|
||||
const result: AiFiltersResult = {
|
||||
filters,
|
||||
travelTimeFilters,
|
||||
notes: json.notes || '',
|
||||
summary: summaryText,
|
||||
listingType: json.listing_type || 'historical',
|
||||
matchCount,
|
||||
};
|
||||
setNotes(result.notes || null);
|
||||
setSummary(summaryText);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useCollapsibleGroups(): [
|
||||
Set<string>,
|
||||
/**
|
||||
* Manages collapsible group state.
|
||||
* @param defaultCollapsed When true, groups start collapsed (tracks expanded groups).
|
||||
* When false (default), groups start expanded (tracks collapsed groups).
|
||||
*/
|
||||
export function useCollapsibleGroups(defaultCollapsed = false): [
|
||||
(name: string) => boolean,
|
||||
(name: string) => void,
|
||||
(name: string) => void,
|
||||
] {
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
||||
const [toggled, setToggled] = useState<Set<string>>(new Set());
|
||||
|
||||
const isExpanded = useCallback(
|
||||
(name: string) => (defaultCollapsed ? toggled.has(name) : !toggled.has(name)),
|
||||
[toggled, defaultCollapsed]
|
||||
);
|
||||
|
||||
const toggle = useCallback((name: string) => {
|
||||
setCollapsed((prev) => {
|
||||
setToggled((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(name)) next.delete(name);
|
||||
else next.add(name);
|
||||
|
|
@ -16,14 +26,24 @@ export function useCollapsibleGroups(): [
|
|||
});
|
||||
}, []);
|
||||
|
||||
const expand = useCallback((name: string) => {
|
||||
setCollapsed((prev) => {
|
||||
if (!prev.has(name)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(name);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
const expand = useCallback(
|
||||
(name: string) => {
|
||||
setToggled((prev) => {
|
||||
if (defaultCollapsed) {
|
||||
if (prev.has(name)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(name);
|
||||
return next;
|
||||
} else {
|
||||
if (!prev.has(name)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(name);
|
||||
return next;
|
||||
}
|
||||
});
|
||||
},
|
||||
[defaultCollapsed]
|
||||
);
|
||||
|
||||
return [collapsed, toggle, expand];
|
||||
return [isExpanded, toggle, expand];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ export function useDeckLayers({
|
|||
ttVal as number,
|
||||
ttVal as number,
|
||||
clr,
|
||||
null,
|
||||
fr,
|
||||
0,
|
||||
densityGradientRef.current,
|
||||
dark,
|
||||
|
|
@ -422,7 +422,7 @@ export function useDeckLayers({
|
|||
ttVal as number,
|
||||
ttVal as number,
|
||||
clr,
|
||||
null,
|
||||
fr,
|
||||
0,
|
||||
densityGradientRef.current,
|
||||
dark,
|
||||
|
|
|
|||
|
|
@ -119,11 +119,14 @@ export function useMapData({
|
|||
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
|
||||
const isTravelTimeDrag = activeFeature.startsWith('tt_');
|
||||
const dragTravelParam = isTravelTimeDrag ? buildTravelParam(activeFeature) : travelParam;
|
||||
// Travel time fields are computed from the travel param, not regular feature columns.
|
||||
// Sending a tt_* name as fields would cause a 400 (unknown field). Use empty string instead.
|
||||
const fieldsParam = isTravelTimeDrag ? '' : activeFeature;
|
||||
|
||||
if (usePostcodeView) {
|
||||
const params = new URLSearchParams({ bounds: boundsStr });
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', activeFeature);
|
||||
params.set('fields', fieldsParam);
|
||||
if (dragTravelParam) params.set('travel', dragTravelParam);
|
||||
|
||||
fetch(apiUrl('postcodes', params), authHeaders({ signal: dragAbortRef.current.signal }))
|
||||
|
|
@ -140,7 +143,7 @@ export function useMapData({
|
|||
bounds: boundsStr,
|
||||
});
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', activeFeature);
|
||||
params.set('fields', fieldsParam);
|
||||
if (dragTravelParam) params.set('travel', dragTravelParam);
|
||||
|
||||
fetch(apiUrl('hexagons', params), authHeaders({ signal: dragAbortRef.current.signal }))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue