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,15 +1,27 @@
import { memo, useState, useCallback } from 'react';
import { SpinnerIcon } from '../ui/icons/SpinnerIcon';
import { SparklesIcon } from '../ui/icons/SparklesIcon';
import type { AiFilterErrorType } from '../../hooks/useAiFilters';
interface AiFilterInputProps {
loading: boolean;
error: string | null;
errorType: AiFilterErrorType | null;
notes: string | null;
onSubmit: (query: string) => void;
isLoggedIn: boolean;
onLoginRequired: () => void;
}
export default memo(function AiFilterInput({ loading, error, notes, onSubmit }: AiFilterInputProps) {
export default memo(function AiFilterInput({
loading,
error,
errorType,
notes,
onSubmit,
isLoggedIn,
onLoginRequired,
}: AiFilterInputProps) {
const [query, setQuery] = useState('');
const handleSubmit = useCallback(
@ -17,9 +29,13 @@ export default memo(function AiFilterInput({ loading, error, notes, onSubmit }:
e.preventDefault();
const trimmed = query.trim();
if (!trimmed || loading) return;
if (!isLoggedIn) {
onLoginRequired();
return;
}
onSubmit(trimmed);
},
[query, loading, onSubmit]
[query, loading, isLoggedIn, onLoginRequired, onSubmit]
);
const hasContent = query.trim().length > 0;
@ -52,7 +68,17 @@ export default memo(function AiFilterInput({ loading, error, notes, onSubmit }:
</button>
)}
</form>
{error && (
{error && errorType === 'verification' && (
<p className="mt-1.5 text-xs text-amber-600 dark:text-amber-400">
Please verify your email address to use AI-powered search. Check your inbox for a verification link.
</p>
)}
{error && errorType === 'limit' && (
<p className="mt-1.5 text-xs text-amber-600 dark:text-amber-400">
You've reached the weekly AI usage limit. It will reset automatically next week.
</p>
)}
{error && errorType === 'error' && (
<p className="mt-1 text-xs text-red-600 dark:text-red-400 truncate" title={error}>
{error}
</p>

View file

@ -15,6 +15,7 @@ import { FeatureInfoPopup } from '../ui/FeatureInfoPopup';
import { FeatureActions } from '../ui/FeatureIcons';
import { FeatureLabel } from '../ui/FeatureLabel';
import AiFilterInput from './AiFilterInput';
import type { AiFilterErrorType } from '../../hooks/useAiFilters';
import FeatureBrowser from './FeatureBrowser';
import { TravelTimeCard } from './TravelTimeCard';
import {
@ -89,8 +90,11 @@ interface FiltersProps {
onTravelTimeToggleBest: (index: number) => void;
aiFilterLoading: boolean;
aiFilterError: string | null;
aiFilterErrorType: AiFilterErrorType | null;
aiFilterNotes: string | null;
onAiFilterSubmit: (query: string) => void;
isLoggedIn: boolean;
onLoginRequired: () => void;
isLicensed: boolean;
onUpgradeClick?: () => void;
onResetTutorial?: () => void;
@ -121,8 +125,11 @@ export default memo(function Filters({
onTravelTimeToggleBest,
aiFilterLoading,
aiFilterError,
aiFilterErrorType,
aiFilterNotes,
onAiFilterSubmit,
isLoggedIn,
onLoginRequired,
isLicensed,
onUpgradeClick,
onResetTutorial,
@ -278,7 +285,7 @@ export default memo(function Filters({
</div>
<div ref={scrollRef} className="md:flex-1 md:overflow-y-auto">
<AiFilterInput loading={aiFilterLoading} error={aiFilterError} notes={aiFilterNotes} onSubmit={onAiFilterSubmit} />
<AiFilterInput loading={aiFilterLoading} error={aiFilterError} errorType={aiFilterErrorType} notes={aiFilterNotes} onSubmit={onAiFilterSubmit} isLoggedIn={isLoggedIn} onLoginRequired={onLoginRequired} />
<div className="px-3 pb-2 space-y-2">
<div className="flex rounded-lg bg-warm-100 dark:bg-warm-800 p-0.5">
{(['historical', 'buy', 'rent'] as const).map((type) => {

View file

@ -143,16 +143,29 @@ export default function MapPage({
});
const aiFilters = useAiFilters();
const travelTime = useTravelTime(initialTravelTime);
const handleAiFilterSubmit = useCallback(
async (query: string) => {
const result = await aiFilters.fetchAiFilters(query);
if (result) handleSetFilters(result.filters);
if (!result) return;
handleSetFilters(result.filters);
// Apply travel time filters from AI
if (result.travelTimeFilters.length > 0) {
const newEntries = result.travelTimeFilters.map((tt) => ({
mode: tt.mode,
slug: tt.slug,
label: tt.label,
timeRange: [tt.min ?? 0, tt.max ?? 120] as [number, number],
useBest: false,
}));
travelTime.handleSetEntries(newEntries);
}
},
[aiFilters.fetchAiFilters, handleSetFilters]
[aiFilters.fetchAiFilters, handleSetFilters, travelTime.handleSetEntries]
);
const travelTime = useTravelTime(initialTravelTime);
const handleTravelTimeSetDestination = useCallback(
(index: number, slug: string, label: string) => {
travelTime.handleSetDestination(index, slug, label);
@ -499,8 +512,11 @@ export default function MapPage({
onTravelTimeToggleBest={travelTime.handleToggleBest}
aiFilterLoading={aiFilters.loading}
aiFilterError={aiFilters.error}
aiFilterErrorType={aiFilters.errorType}
aiFilterNotes={aiFilters.notes}
onAiFilterSubmit={handleAiFilterSubmit}
isLoggedIn={!!user}
onLoginRequired={onRegisterClick ?? (() => {})}
isLicensed={user?.subscription === 'licensed'}
onUpgradeClick={() => onNavigateTo('pricing')}
onResetTutorial={tutorial.resetTutorial}

View file

@ -3,7 +3,12 @@ const H = 1.15; // digit slot height in em
function Digit({ char, delay, active }: { char: string; delay: number; active: boolean }) {
const idx = DIGITS.indexOf(char);
if (idx === -1) return <span>{char}</span>;
if (idx === -1)
return (
<span className="inline-block" style={{ height: `${H}em`, lineHeight: `${H}em` }}>
{char}
</span>
);
const offset = active ? -idx * H : 0;