- Hero: remove full-width mobile submit, add inline "Go" button next to locate
- Prediction cards: tighter mobile padding (px-3 py-3)
- Search filters: right-aligned toolbar, remove "X stations found" count and map toggle
- Map: initialize view immediately to avoid tile wiggle, skip recenter on fresh init
- Station list: hidden by default, toggled via "Stations {count}" pill above map
- Typography: hide desktop h1 on mobile, scale down section headings and spacing
- Footer: remove uppercase styling from headings and copyright line
- Filter popover: auto-close on fuel/radius/sort/brand selection
fix(llm): retry submit_overlay when events_cited is missing, extend Fuel Finder timeout with retries
- LlmOverlayService: add `minItems: 1` to events_cited schema, detect missing citations
in submit response, inject tool_result error and retry once with explicit prompt
- Log full raw_result context when no verified citations, capturing direction/confidence/reasoning
- FuelPriceService: add 3×1s retry with 60s timeout to batch price requests (was 30s no retry)
- Tests: cover successful retry recovery and rejection when retry also omits citations
94 lines
3.5 KiB
Vue
94 lines
3.5 KiB
Vue
<template>
|
|
<div
|
|
v-if="prediction"
|
|
class="p-2 sm:p-5 bg-white rounded-2xl border border-zinc-300"
|
|
>
|
|
<div class="grid gap-4 lg:grid-cols-2 lg:gap-5">
|
|
<div class="space-y-1.5">
|
|
<p class="font-mono text-[10px] uppercase tracking-widest text-zinc-500 mb-1 truncate">Price Prediction</p>
|
|
<h3 class="text-sm font-semibold text-zinc-800 leading-snug">{{ actionLabel }}</h3>
|
|
<p class="text-sm text-zinc-500 leading-snug">{{ prediction.reasoning }}</p>
|
|
<p class="text-sm text-zinc-500 leading-snug">
|
|
<span>Avg {{ prediction.current_avg }}p</span>
|
|
<span class="text-zinc-400"> · </span>
|
|
<span>Confidence {{ prediction.confidence_label }}</span>
|
|
<template v-if="prediction.predicted_change_pence">
|
|
<span class="text-zinc-400"> · </span>
|
|
<span>{{ prediction.predicted_change_pence > 0 ? '+' : '' }}{{ prediction.predicted_change_pence.toFixed(1) }}p expected</span>
|
|
</template>
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="weeklyHeadline || todayContext"
|
|
class="space-y-1.5 pt-3 border-t border-zinc-200 lg:pt-0 lg:border-t-0 lg:border-l lg:pl-5"
|
|
>
|
|
<p class="font-mono text-[10px] uppercase tracking-widest text-zinc-500 mb-1 truncate">Last 7 days</p>
|
|
<p v-if="weeklyHeadline" class="text-sm font-semibold text-zinc-800 leading-snug">{{ weeklyHeadline }}</p>
|
|
<p v-if="todayContext" class="text-sm text-zinc-500 leading-snug">{{ todayContext }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
|
|
const props = defineProps({
|
|
prediction: { type: Object, default: null },
|
|
})
|
|
|
|
const actionLabel = computed(() => {
|
|
if (!props.prediction) return ''
|
|
return {
|
|
fill_now: 'Fill up now',
|
|
wait: 'Wait — prices falling',
|
|
no_signal: 'No clear signal',
|
|
}[props.prediction.action] ?? 'Check local prices'
|
|
})
|
|
|
|
const weekly = computed(() => props.prediction?.weekly_summary ?? null)
|
|
|
|
function formatPence(value) {
|
|
if (value === null || value === undefined) return null
|
|
return Number(value).toFixed(1) + 'p'
|
|
}
|
|
|
|
function formatDateShort(iso) {
|
|
if (!iso) return ''
|
|
const d = new Date(iso)
|
|
if (Number.isNaN(d.getTime())) return iso
|
|
return d.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric' })
|
|
}
|
|
|
|
const weeklyHeadline = computed(() => {
|
|
const w = weekly.value
|
|
if (!w || !w.cheapest_day || !w.priciest_day || w.last_7_days_change_pence === null) {
|
|
return null
|
|
}
|
|
const change = w.last_7_days_change_pence
|
|
const lead = change > 0.05
|
|
? `Avg rose ${change.toFixed(1)}p`
|
|
: change < -0.05
|
|
? `Avg fell ${Math.abs(change).toFixed(1)}p`
|
|
: 'Avg held steady'
|
|
return `${lead} — cheapest ${formatDateShort(w.cheapest_day.date)} (${formatPence(w.cheapest_day.avg)}), priciest ${formatDateShort(w.priciest_day.date)} (${formatPence(w.priciest_day.avg)}).`
|
|
})
|
|
|
|
const todayContext = computed(() => {
|
|
const w = weekly.value
|
|
if (!w) return null
|
|
|
|
const today = formatPence(w.today_avg)
|
|
const tomorrow = formatPence(w.tomorrow_estimated_avg)
|
|
|
|
if (today && tomorrow) {
|
|
return `Today ${today}; tomorrow ≈ ${tomorrow}.`
|
|
}
|
|
if (today) {
|
|
return `Today ${today}.`
|
|
}
|
|
return null
|
|
})
|
|
</script>
|