add outbound links to place suggestions

This commit is contained in:
alex wiesner
2026-03-14 12:05:55 +00:00
parent 1b004b9e65
commit 4b3f432640
2 changed files with 136 additions and 24 deletions

View File

@@ -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}
<div class="mt-2 space-y-2">
{#each deduplicateContextTools(uniqueToolResultsByCallId(msg.tool_results)) as result}
{#if hasPlaceResults(result)}
<div class="grid gap-2">
{#each getPlaceResults(result) as place}
<div class="card card-compact bg-base-200 p-3">
<h4 class="font-semibold">{place.name}</h4>
{#if place.address}
<p class="text-sm text-base-content/70">{place.address}</p>
{/if}
{#if place.rating}
<div class="flex items-center gap-1 text-sm">
<span></span>
<span>{place.rating}</span>
</div>
{/if}
{#if collectionId}
{@const isDuplicate = mergedLocationNames.has(
normalizeLocationName(place.name)
)}
{#if hasPlaceResults(result)}
<div class="grid gap-2">
{#each getPlaceResults(result) as place}
{@const placeLink = getPreferredPlaceLink(place)}
<div class="card card-compact bg-base-200 p-3">
<h4 class="font-semibold">{place.name}</h4>
{#if place.address}
<p class="text-sm text-base-content/70">{place.address}</p>
{/if}
{#if place.rating}
<div class="flex items-center gap-1 text-sm">
<span></span>
<span>{place.rating}</span>
</div>
{/if}
{#if placeLink}
<div class="mt-2">
<a
href={placeLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-ghost btn-xs"
>
{$t('adventures.external_link')}
</a>
</div>
{/if}
{#if collectionId}
{@const isDuplicate = mergedLocationNames.has(
normalizeLocationName(place.name)
)}
<button
class="btn btn-xs btn-primary btn-outline mt-2"
on:click={() => openDateSelector(place)}
@@ -1175,10 +1238,10 @@
{$t('add_to_itinerary')}
{/if}
</button>
{/if}
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
{:else if hasWebSearchResults(result)}
<div class="grid gap-2">
{#each getWebSearchResults(result) as item}

View File

@@ -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, unknown>): 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}
</div>
<div class="card-actions justify-end mt-3">
<div class="card-actions justify-between items-center mt-3">
{#if suggestion.link}
<a
href={suggestion.link}
target="_blank"
rel="noopener noreferrer"
class="btn btn-ghost btn-xs"
>
{$t('adventures.external_link')}
</a>
{/if}
<button
type="button"
class="btn btn-primary btn-sm"