fix(chat): improve OpenCode Zen integration and error handling
- Fetch models dynamically from OpenCode Zen API (36 models vs 5 hardcoded) - Add function calling support check before using tools - Add retry logic (num_retries=2) for transient failures - Improve logging for debugging API calls and errors - Update system prompt for multi-stop itinerary context - Clean up unused imports in frontend components - Remove deleted views.py (moved to views/__init__.py)
This commit is contained in:
@@ -52,8 +52,7 @@
|
||||
class="link link-primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/Alex-Wiesner/voyage"
|
||||
>documentation</a
|
||||
href="https://github.com/Alex-Wiesner/voyage">documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -348,12 +348,12 @@
|
||||
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
|
||||
{activeView === 'collections'
|
||||
? $t('collection.no_collections_yet')
|
||||
: $t('collection.no_archived_collections')}
|
||||
: $t('collection.no_archived_collections')}
|
||||
</h3>
|
||||
<p class="text-base-content/50 text-center max-w-md">
|
||||
{activeView === 'collections'
|
||||
? $t('collection.create_first')
|
||||
: $t('collection.archived_appear_here')}
|
||||
: $t('collection.archived_appear_here')}
|
||||
</p>
|
||||
{#if activeView === 'collections'}
|
||||
<button
|
||||
@@ -426,137 +426,137 @@
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div class="card bg-base-200/50 p-4 mb-4">
|
||||
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||
<Filter class="w-5 h-5" />
|
||||
{$t('adventures.status_filter')}
|
||||
</h3>
|
||||
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||
<Filter class="w-5 h-5" />
|
||||
{$t('adventures.status_filter')}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === ''}
|
||||
on:change={() => updateStatusFilter('')}
|
||||
/>
|
||||
<span class="label-text">{$t('adventures.all')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === 'folder'}
|
||||
on:change={() => updateStatusFilter('folder')}
|
||||
/>
|
||||
<span class="label-text">📁 {$t('adventures.folder')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === 'upcoming'}
|
||||
on:change={() => updateStatusFilter('upcoming')}
|
||||
/>
|
||||
<span class="label-text">🚀 {$t('adventures.upcoming')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === 'in_progress'}
|
||||
on:change={() => updateStatusFilter('in_progress')}
|
||||
/>
|
||||
<span class="label-text">🎯 {$t('adventures.in_progress')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === 'completed'}
|
||||
on:change={() => updateStatusFilter('completed')}
|
||||
/>
|
||||
<span class="label-text">✓ {$t('adventures.completed')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === ''}
|
||||
on:change={() => updateStatusFilter('')}
|
||||
/>
|
||||
<span class="label-text">{$t('adventures.all')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === 'folder'}
|
||||
on:change={() => updateStatusFilter('folder')}
|
||||
/>
|
||||
<span class="label-text">📁 {$t('adventures.folder')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === 'upcoming'}
|
||||
on:change={() => updateStatusFilter('upcoming')}
|
||||
/>
|
||||
<span class="label-text">🚀 {$t('adventures.upcoming')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === 'in_progress'}
|
||||
on:change={() => updateStatusFilter('in_progress')}
|
||||
/>
|
||||
<span class="label-text">🎯 {$t('adventures.in_progress')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="status_filter"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={statusFilter === 'completed'}
|
||||
on:change={() => updateStatusFilter('completed')}
|
||||
/>
|
||||
<span class="label-text">✓ {$t('adventures.completed')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort Form - Updated to use URL navigation -->
|
||||
<div class="card bg-base-200/50 p-4">
|
||||
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||
<Sort class="w-5 h-5" />
|
||||
{$t(`adventures.sort`)}
|
||||
</h3>
|
||||
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||
<Sort class="w-5 h-5" />
|
||||
{$t(`adventures.sort`)}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">{$t(`adventures.order_direction`)}</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<button
|
||||
class="join-item btn btn-sm flex-1 {orderDirection === 'asc'
|
||||
? 'btn-active'
|
||||
: ''}"
|
||||
on:click={() => updateSort(orderBy, 'asc')}
|
||||
>
|
||||
{$t(`adventures.ascending`)}
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm flex-1 {orderDirection === 'desc'
|
||||
? 'btn-active'
|
||||
: ''}"
|
||||
on:click={() => updateSort(orderBy, 'desc')}
|
||||
>
|
||||
{$t(`adventures.descending`)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">{$t('adventures.order_by')}</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="order_by_radio"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={orderBy === 'updated_at'}
|
||||
on:change={() => updateSort('updated_at', orderDirection)}
|
||||
/>
|
||||
<span class="label-text">{$t('adventures.updated')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="order_by_radio"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={orderBy === 'start_date'}
|
||||
on:change={() => updateSort('start_date', orderDirection)}
|
||||
/>
|
||||
<span class="label-text">{$t('adventures.start_date')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="order_by_radio"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={orderBy === 'name'}
|
||||
on:change={() => updateSort('name', orderDirection)}
|
||||
/>
|
||||
<span class="label-text">{$t('adventures.name')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">{$t(`adventures.order_direction`)}</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<button
|
||||
class="join-item btn btn-sm flex-1 {orderDirection === 'asc'
|
||||
? 'btn-active'
|
||||
: ''}"
|
||||
on:click={() => updateSort(orderBy, 'asc')}
|
||||
>
|
||||
{$t(`adventures.ascending`)}
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm flex-1 {orderDirection === 'desc'
|
||||
? 'btn-active'
|
||||
: ''}"
|
||||
on:click={() => updateSort(orderBy, 'desc')}
|
||||
>
|
||||
{$t(`adventures.descending`)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">{$t('adventures.order_by')}</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="order_by_radio"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={orderBy === 'updated_at'}
|
||||
on:change={() => updateSort('updated_at', orderDirection)}
|
||||
/>
|
||||
<span class="label-text">{$t('adventures.updated')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="order_by_radio"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={orderBy === 'start_date'}
|
||||
on:change={() => updateSort('start_date', orderDirection)}
|
||||
/>
|
||||
<span class="label-text">{$t('adventures.start_date')}</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="order_by_radio"
|
||||
class="radio radio-primary radio-sm"
|
||||
checked={orderBy === 'name'}
|
||||
on:change={() => updateSort('name', orderDirection)}
|
||||
/>
|
||||
<span class="label-text">{$t('adventures.name')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
import FolderMultiple from '~icons/mdi/folder-multiple';
|
||||
import FormatListBulleted from '~icons/mdi/format-list-bulleted';
|
||||
import Timeline from '~icons/mdi/timeline';
|
||||
import Map from '~icons/mdi/map';
|
||||
import MapIcon from '~icons/mdi/map';
|
||||
import Lightbulb from '~icons/mdi/lightbulb';
|
||||
import ChartBar from '~icons/mdi/chart-bar';
|
||||
import Plus from '~icons/mdi/plus';
|
||||
@@ -261,20 +261,43 @@
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const firstLocation = current.locations.find((loc) =>
|
||||
Boolean(loc.city?.name || loc.country?.name || loc.location || loc.name)
|
||||
);
|
||||
if (!firstLocation) {
|
||||
const maxStops = 4;
|
||||
const stops: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const loc of current.locations) {
|
||||
const cityName = loc.city?.name?.trim();
|
||||
const countryName = loc.country?.name?.trim();
|
||||
|
||||
if (cityName || countryName) {
|
||||
const label =
|
||||
cityName && countryName ? `${cityName}, ${countryName}` : cityName || countryName;
|
||||
if (!label) continue;
|
||||
const key = `geo:${(cityName || '').toLowerCase()}|${(countryName || '').toLowerCase()}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
stops.push(label);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fallbackName = (loc.location || loc.name || '').trim();
|
||||
if (!fallbackName) continue;
|
||||
const key = `name:${fallbackName.toLowerCase()}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
stops.push(fallbackName);
|
||||
}
|
||||
|
||||
if (stops.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cityName = firstLocation.city?.name;
|
||||
const countryName = firstLocation.country?.name;
|
||||
if (cityName && countryName) {
|
||||
return `${cityName}, ${countryName}`;
|
||||
const summarizedStops = stops.slice(0, maxStops).join('; ');
|
||||
if (stops.length > maxStops) {
|
||||
return `${summarizedStops}; +${stops.length - maxStops} more`;
|
||||
}
|
||||
|
||||
return cityName || countryName || firstLocation.location || firstLocation.name || undefined;
|
||||
return summarizedStops;
|
||||
}
|
||||
|
||||
$: collectionDestination = deriveCollectionDestination(collection);
|
||||
@@ -1138,7 +1161,7 @@
|
||||
class:btn-active={currentView === 'map'}
|
||||
on:click={() => switchView('map')}
|
||||
>
|
||||
<Map class="w-5 h-5 sm:mr-2" aria-hidden="true" />
|
||||
<MapIcon class="w-5 h-5 sm:mr-2" aria-hidden="true" />
|
||||
<span class="hidden sm:inline">{$t('navbar.map')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -511,24 +511,25 @@
|
||||
<div class="text-sm">
|
||||
{#if visit.timezone}
|
||||
<strong>{$t('adventures.start')}:</strong>
|
||||
{DateTime.fromISO(visit.start_date, { zone: 'utc' })
|
||||
.setZone(visit.timezone)
|
||||
.toLocaleString(DateTime.DATETIME_MED, { locale: 'en-GB' })}<br />
|
||||
<strong>{$t('adventures.end')}:</strong>
|
||||
{DateTime.fromISO(visit.end_date, { zone: 'utc' })
|
||||
.setZone(visit.timezone)
|
||||
.toLocaleString(DateTime.DATETIME_MED, { locale: 'en-GB' })}
|
||||
{:else}
|
||||
<strong>Start:</strong>
|
||||
{DateTime.fromISO(visit.start_date).toLocaleString(
|
||||
DateTime.DATETIME_MED,
|
||||
{ locale: 'en-GB' }
|
||||
)}<br />
|
||||
<strong>End:</strong>
|
||||
{DateTime.fromISO(visit.end_date).toLocaleString(
|
||||
DateTime.DATETIME_MED,
|
||||
{ locale: 'en-GB' }
|
||||
)}
|
||||
{DateTime.fromISO(visit.start_date, { zone: 'utc' })
|
||||
.setZone(visit.timezone)
|
||||
.toLocaleString(DateTime.DATETIME_MED, { locale: 'en-GB' })}<br
|
||||
/>
|
||||
<strong>{$t('adventures.end')}:</strong>
|
||||
{DateTime.fromISO(visit.end_date, { zone: 'utc' })
|
||||
.setZone(visit.timezone)
|
||||
.toLocaleString(DateTime.DATETIME_MED, { locale: 'en-GB' })}
|
||||
{:else}
|
||||
<strong>Start:</strong>
|
||||
{DateTime.fromISO(visit.start_date).toLocaleString(
|
||||
DateTime.DATETIME_MED,
|
||||
{ locale: 'en-GB' }
|
||||
)}<br />
|
||||
<strong>End:</strong>
|
||||
{DateTime.fromISO(visit.end_date).toLocaleString(
|
||||
DateTime.DATETIME_MED,
|
||||
{ locale: 'en-GB' }
|
||||
)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -362,20 +362,20 @@
|
||||
zoom={13}
|
||||
>
|
||||
<DefaultMarker lngLat={[lodging.longitude, lodging.latitude]}>
|
||||
<Popup openOn="click" offset={[0, -10]}>
|
||||
<div class="p-2">
|
||||
<div class="text-lg font-bold text-black mb-1">{lodging.name}</div>
|
||||
<p class="font-semibold text-black text-sm mb-2">
|
||||
{$t(`lodging.${lodging.type}`)}
|
||||
{getLodgingIcon(lodging.type)}
|
||||
</p>
|
||||
{#if lodging.location}
|
||||
<div class="text-xs text-black">
|
||||
📍 {lodging.location}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Popup>
|
||||
<Popup openOn="click" offset={[0, -10]}>
|
||||
<div class="p-2">
|
||||
<div class="text-lg font-bold text-black mb-1">{lodging.name}</div>
|
||||
<p class="font-semibold text-black text-sm mb-2">
|
||||
{$t(`lodging.${lodging.type}`)}
|
||||
{getLodgingIcon(lodging.type)}
|
||||
</p>
|
||||
{#if lodging.location}
|
||||
<div class="text-xs text-black">
|
||||
📍 {lodging.location}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Popup>
|
||||
</DefaultMarker>
|
||||
</MapLibre>
|
||||
</div>
|
||||
|
||||
@@ -746,20 +746,20 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
Visits: {hoveredLocation.visits?.length ?? 0}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
Media: {hoveredLocation.images?.length ?? 0}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
Files: {hoveredLocation.attachments?.length ?? 0}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
Trails: {hoveredLocation.trails?.length ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
Visits: {hoveredLocation.visits?.length ?? 0}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
Media: {hoveredLocation.images?.length ?? 0}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
Files: {hoveredLocation.attachments?.length ?? 0}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
Trails: {hoveredLocation.trails?.length ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hoveredLocation.visits && hoveredLocation.visits.length > 0}
|
||||
<div class="text-xs text-base-content/70">
|
||||
|
||||
@@ -333,12 +333,12 @@
|
||||
<div class="flex items-center justify-center gap-2 text-base-content/60">
|
||||
<Calendar class="w-5 h-5" />
|
||||
<span class="text-lg">
|
||||
{$t('profile.member_since')}
|
||||
{new Date(user.date_joined).toLocaleDateString('en-GB', {
|
||||
timeZone: 'UTC',
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
})}
|
||||
{$t('profile.member_since')}
|
||||
{new Date(user.date_joined).toLocaleDateString('en-GB', {
|
||||
timeZone: 'UTC',
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -726,14 +726,14 @@
|
||||
<p class="text-sm">{$t('worldtravel.no_country_data_available_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="link link-primary mt-4 inline-block"
|
||||
href="https://github.com/Alex-Wiesner/voyage/blob/main/documentation/docs/configuration/updating.md#updating-region-data"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{$t('settings.documentation_link')}
|
||||
</a>
|
||||
<a
|
||||
class="link link-primary mt-4 inline-block"
|
||||
href="https://github.com/Alex-Wiesner/voyage/blob/main/documentation/docs/configuration/updating.md#updating-region-data"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{$t('settings.documentation_link')}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user