diff --git a/frontend/src/lib/components/AITravelChat.svelte b/frontend/src/lib/components/AITravelChat.svelte index fbe6d0c5..323beba9 100644 --- a/frontend/src/lib/components/AITravelChat.svelte +++ b/frontend/src/lib/components/AITravelChat.svelte @@ -56,6 +56,7 @@ export let startDate: string | undefined = undefined; export let endDate: string | undefined = undefined; export let destination: string | undefined = undefined; + export let collectionLocations: Location[] = []; let conversations: Conversation[] = []; let activeConversation: Conversation | null = null; @@ -662,7 +663,39 @@ return parseCoordinate(place.latitude) !== null && parseCoordinate(place.longitude) !== null; } + function normalizeLocationName(value: unknown): string { + if (typeof value !== 'string') { + return ''; + } + + return value.trim().toLowerCase(); + } + + let sessionDuplicateCollectionId: string | undefined = collectionId; + let sessionAddedLocationNames = new Set(); + + $: if (sessionDuplicateCollectionId !== collectionId) { + sessionDuplicateCollectionId = collectionId; + sessionAddedLocationNames = new Set(); + } + + $: existingLocationNames = new Set( + (collectionLocations || []) + .map((location) => normalizeLocationName(location?.name)) + .filter((name) => name.length > 0) + ); + + $: mergedLocationNames = new Set([...existingLocationNames, ...sessionAddedLocationNames]); + + function placeAlreadyInCollection(place: PlaceResult): boolean { + return mergedLocationNames.has(normalizeLocationName(place.name)); + } + function openDateSelector(place: PlaceResult) { + if (placeAlreadyInCollection(place)) { + return; + } + selectedPlaceToAdd = place; selectedDate = startDate || ''; showDateSelector = true; @@ -679,6 +712,10 @@ return; } + if (placeAlreadyInCollection(place)) { + return; + } + const latitude = parseCoordinate(place.latitude); const longitude = parseCoordinate(place.longitude); if (latitude === null || longitude === null) { @@ -729,6 +766,10 @@ } const itineraryItem = await itineraryResponse.json(); + const normalizedPlaceName = normalizeLocationName(place.name); + if (normalizedPlaceName) { + sessionAddedLocationNames = new Set([...sessionAddedLocationNames, normalizedPlaceName]); + } dispatch('itemAdded', { location, itineraryItem, date }); addToast('success', $t('added_successfully')); @@ -797,12 +838,92 @@ }; } + if (result.name === 'move_itinerary_item') { + const item = asRecord(payload?.itinerary_item); + const date = typeof item?.date === 'string' ? item.date : 'the selected day'; + return { + icon: 'โ†•๏ธ', + text: `Moved itinerary item to ${date}.` + }; + } + + if (result.name === 'remove_itinerary_item') { + return { + icon: '๐Ÿ—‘๏ธ', + text: 'Removed item from itinerary.' + }; + } + + if (result.name === 'update_location_details') { + const location = asRecord(payload?.location); + const locationName = typeof location?.name === 'string' ? location.name : 'location'; + return { + icon: '๐Ÿ“', + text: `Updated details for ${locationName}.` + }; + } + + if (result.name === 'add_lodging') { + const lodging = asRecord(payload?.lodging); + const lodgingName = typeof lodging?.name === 'string' ? lodging.name : 'lodging'; + return { + icon: '๐Ÿจ', + text: `Added lodging: ${lodgingName}.` + }; + } + + if (result.name === 'update_lodging') { + const lodging = asRecord(payload?.lodging); + const lodgingName = typeof lodging?.name === 'string' ? lodging.name : 'lodging'; + return { + icon: '๐Ÿจ', + text: `Updated lodging: ${lodgingName}.` + }; + } + + if (result.name === 'remove_lodging') { + return { + icon: '๐Ÿงน', + text: 'Removed lodging from trip.' + }; + } + + if (result.name === 'add_transportation') { + const transportation = asRecord(payload?.transportation); + const name = + typeof transportation?.name === 'string' ? transportation.name : 'transportation item'; + return { + icon: '๐ŸšŒ', + text: `Added transportation: ${name}.` + }; + } + + if (result.name === 'update_transportation') { + const transportation = asRecord(payload?.transportation); + const name = + typeof transportation?.name === 'string' ? transportation.name : 'transportation item'; + return { + icon: '๐ŸšŒ', + text: `Updated transportation: ${name}.` + }; + } + + if (result.name === 'remove_transportation') { + return { + icon: '๐Ÿงน', + text: 'Removed transportation from trip.' + }; + } + if (result.name === 'get_weather') { const entries = Array.isArray(payload?.results) ? payload.results : []; const availableCount = entries.filter((entry) => asRecord(entry)?.available === true).length; + const estimatedCount = entries.filter( + (entry) => asRecord(entry)?.is_estimate === true + ).length; return { icon: '๐ŸŒค๏ธ', - text: `Checked weather for ${entries.length} date${entries.length === 1 ? '' : 's'} (${availableCount} available).` + text: `Checked weather for ${entries.length} date${entries.length === 1 ? '' : 's'} (${availableCount} available, ${estimatedCount} estimated).` }; } @@ -820,10 +941,12 @@ class:shadow-xl={!embedded} class:border={embedded} class:border-base-300={embedded} + class:h-full={panelMode} + class:min-h-0={panelMode} > -
+
-
+
{:else} -
+
{#if messages.length === 0 && !activeConversation}
๐ŸŒ
@@ -1022,12 +1145,19 @@
{/if} {#if collectionId} + {@const isDuplicate = mergedLocationNames.has( + normalizeLocationName(place.name) + )} {/if}
diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 300c14cc..50645eb6 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -1648,7 +1648,9 @@ {#if canModifyCollection}
-
+

{$t('chat.travel_assistant')}

-
+