From 4b3f4326406e041bddcbcaae78ead6338e3e6aa4 Mon Sep 17 00:00:00 2001 From: alex wiesner Date: Sat, 14 Mar 2026 12:05:55 +0000 Subject: [PATCH] add outbound links to place suggestions --- .../src/lib/components/AITravelChat.svelte | 107 ++++++++++++++---- .../ItinerarySuggestionModal.svelte | 53 ++++++++- 2 files changed, 136 insertions(+), 24 deletions(-) diff --git a/frontend/src/lib/components/AITravelChat.svelte b/frontend/src/lib/components/AITravelChat.svelte index f532262b..4a44cd26 100644 --- a/frontend/src/lib/components/AITravelChat.svelte +++ b/frontend/src/lib/components/AITravelChat.svelte @@ -24,6 +24,18 @@ rating?: number; latitude?: number | string; longitude?: number | string; + link?: string | null; + preferred_link?: string | null; + url?: string | null; + website?: string | null; + map_link?: string | null; + links?: { + link?: string | null; + preferred?: string | null; + official?: string | null; + website?: string | null; + map?: string | null; + } | null; }; type Conversation = { @@ -661,6 +673,43 @@ return null; } + function normalizeHttpUrl(value: unknown): string | null { + if (typeof value !== 'string') { + return null; + } + + const candidate = value.trim(); + if (!candidate) { + return null; + } + + try { + const parsed = new URL(candidate); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return null; + } + + return parsed.toString(); + } catch { + return null; + } + } + + function getPreferredPlaceLink(place: PlaceResult): string | null { + return ( + normalizeHttpUrl(place.link) || + normalizeHttpUrl(place.preferred_link) || + normalizeHttpUrl(place.url) || + normalizeHttpUrl(place.website) || + normalizeHttpUrl(place.map_link) || + normalizeHttpUrl(place.links?.link) || + normalizeHttpUrl(place.links?.preferred) || + normalizeHttpUrl(place.links?.official) || + normalizeHttpUrl(place.links?.website) || + normalizeHttpUrl(place.links?.map) + ); + } + function hasPlaceCoordinates(place: PlaceResult): boolean { return parseCoordinate(place.latitude) !== null && parseCoordinate(place.longitude) !== null; } @@ -737,6 +786,7 @@ location: place.address || place.name, latitude, longitude, + link: getPreferredPlaceLink(place), collections: [collectionId], is_public: false }) @@ -1146,24 +1196,37 @@ {#if msg.role === 'assistant' && msg.tool_results}
{#each deduplicateContextTools(uniqueToolResultsByCallId(msg.tool_results)) as result} - {#if hasPlaceResults(result)} -
- {#each getPlaceResults(result) as place} -
-

{place.name}

- {#if place.address} -

{place.address}

- {/if} - {#if place.rating} -
- - {place.rating} -
- {/if} - {#if collectionId} - {@const isDuplicate = mergedLocationNames.has( - normalizeLocationName(place.name) - )} + {#if hasPlaceResults(result)} +
+ {#each getPlaceResults(result) as place} + {@const placeLink = getPreferredPlaceLink(place)} +
+

{place.name}

+ {#if place.address} +

{place.address}

+ {/if} + {#if place.rating} +
+ + {place.rating} +
+ {/if} + {#if placeLink} + + {/if} + {#if collectionId} + {@const isDuplicate = mergedLocationNames.has( + normalizeLocationName(place.name) + )} - {/if} -
- {/each} -
+ {/if} +
+ {/each} +
{:else if hasWebSearchResults(result)}
{#each getWebSearchResults(result) as item} diff --git a/frontend/src/lib/components/collections/ItinerarySuggestionModal.svelte b/frontend/src/lib/components/collections/ItinerarySuggestionModal.svelte index 7b078d4d..06ea3908 100644 --- a/frontend/src/lib/components/collections/ItinerarySuggestionModal.svelte +++ b/frontend/src/lib/components/collections/ItinerarySuggestionModal.svelte @@ -20,6 +20,7 @@ price_level?: string | null; latitude?: number | string | null; longitude?: number | string | null; + link?: string | null; }; const dispatch = createEventDispatcher(); @@ -166,6 +167,40 @@ return null; } + function normalizeHttpUrl(value: unknown): string | null { + if (typeof value !== 'string') return null; + const candidate = value.trim(); + if (!candidate) return null; + + try { + const parsed = new URL(candidate); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return null; + } + return parsed.toString(); + } catch { + return null; + } + } + + function getPreferredSuggestionLink(item: Record): string | null { + const links = asRecord(item.links); + return ( + normalizeHttpUrl(item.link) || + normalizeHttpUrl(item.preferred_link) || + normalizeHttpUrl(item.url) || + normalizeHttpUrl(item.website) || + normalizeHttpUrl(item.map_link) || + (links + ? normalizeHttpUrl(links.link) || + normalizeHttpUrl(links.preferred) || + normalizeHttpUrl(links.official) || + normalizeHttpUrl(links.website) || + normalizeHttpUrl(links.map) + : null) + ); + } + function normalizeSuggestionItem(value: unknown): SuggestionItem | null { const item = asRecord(value); if (!item) return null; @@ -191,6 +226,7 @@ const rating = normalizeRating(item.rating ?? item.score); const latitude = normalizeCoordinate(item.latitude ?? item.lat); const longitude = normalizeCoordinate(item.longitude ?? item.lon ?? item.lng); + const link = getPreferredSuggestionLink(item); const finalName = name || location; if (!finalName) return null; @@ -204,7 +240,8 @@ rating, price_level: priceLevel || null, latitude, - longitude + longitude, + link }; } @@ -225,6 +262,7 @@ const rating = normalizeRating(suggestion.rating); const latitude = normalizeCoordinate(suggestion.latitude); const longitude = normalizeCoordinate(suggestion.longitude); + const link = normalizeHttpUrl(suggestion.link); return { name, @@ -233,6 +271,7 @@ rating, latitude, longitude, + link, collections: [collection.id], is_public: Boolean(collection?.is_public) }; @@ -508,7 +547,17 @@ {/if}
-
+
+ {#if suggestion.link} + + ↗ {$t('adventures.external_link')} + + {/if}