feat(ui): mobile-first redesign — compact hero, inline submit button, map-first with collapsible list
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

- 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
This commit is contained in:
Ovidiu U
2026-05-14 13:23:52 +01:00
parent 11a3b433ff
commit 97e27fc057
10 changed files with 302 additions and 87 deletions

View File

@@ -5,12 +5,12 @@
<div class="hero-gradient">
<!-- Hero -->
<section id="hero" class="relative pt-24 md:pt-36 pb-4 md:pb-8 px-6 overflow-hidden">
<section id="hero" class="relative pt-24 md:pt-36 pb-2 md:pb-8 px-3 overflow-hidden">
<div class="max-w-7xl mx-auto grid lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">
<div class="space-y-6 md:space-y-8">
<div class="space-y-3 md:space-y-8">
<LiveTicker :latest-price-at="liveStats.latestPriceAt" :station-count="liveStats.stationCount" />
<h1 class="font-serif text-zinc-900 text-[40px] leading-[0.98] md:text-6xl lg:text-[88px] lg:leading-[0.95] max-w-[560px]">
<h1 class="hidden lg:block font-serif text-zinc-900 text-[40px] leading-[0.98] md:text-6xl lg:text-[88px] lg:leading-[0.95] max-w-140">
Know <span class="text-accent">exactly</span> when to fill up.
</h1>
@@ -33,8 +33,8 @@
</section>
<!-- Search Results -->
<section v-if="searchAttempted" id="searchAttempted" class="px-6">
<div class="max-w-7xl mx-auto space-y-6">
<section v-if="searchAttempted" id="searchAttempted" class="px-3">
<div class="max-w-7xl mx-auto space-y-3">
<!-- Prediction box (sits above filter results) -->
<PredictionCard
@@ -48,10 +48,7 @@
v-model:brand-filter="brandFilter"
:brands="availableBrands"
:initial="searchInitial"
:map-open="mapOpen"
:station-count="filteredStations.length"
@search="onSearch"
@toggle-map="mapOpen = !mapOpen"
/>
<!-- Loading -->
@@ -76,7 +73,6 @@
</div>
<template v-else>
<LeafletMap
:is-open="mapOpen"
:origin="searchOrigin"
:radius-miles="radiusMiles"
:selected-station-id="selectedStationId"
@@ -111,11 +107,28 @@
</template>
</LeafletMap>
<UpsellBanner :station-count="liveStats.stationCount" />
<StationList
:current-sort="sort"
:origin="searchOrigin"
:stations="filteredStations"
/>
<button
:aria-expanded="listOpen"
:class="listOpen ? '' : 'mb-6'"
aria-controls="station-list-panel"
class="pill w-full justify-center"
type="button"
@click="listOpen = !listOpen"
>
<iconify-icon :icon="listOpen ? 'lucide:chevron-up' : 'lucide:list'" class="text-sm opacity-70"></iconify-icon>
<span class="text-sm font-medium">
{{ listOpen ? 'Hide list' : `Stations ${filteredStations.length}` }}
</span>
</button>
<div v-if="listOpen" id="station-list-panel">
<StationList
:current-sort="sort"
:origin="searchOrigin"
:stations="filteredStations"
/>
</div>
</template>
</template>
@@ -124,41 +137,41 @@
</div>
<!-- How It Works -->
<section id="how-it-works" class="py-12 md:py-24 px-6 bg-zinc-50">
<section id="how-it-works" class="py-4 md:py-24 px-3 bg-zinc-50">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16 space-y-4">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800">Smart Savings in 3 Steps</h2>
<p class="text-zinc-500 text-lg max-w-2xl mx-auto">Stop guessing when to fill up. Our engine analyzes thousands of data points daily to save you money.</p>
<div class="text-center mb-4 md:mb-8 space-y-2 md:space-y-4">
<h2 class="text-xl md:text-5xl font-black font-display text-zinc-800">Smart Savings in 3 Steps</h2>
<p class="text-zinc-500 text-sm md:text-lg max-w-2xl mx-auto">Stop guessing when to fill up. Our engine analyzes thousands of data points daily to save you money.</p>
</div>
<div class="grid md:grid-cols-3 gap-12">
<div class="text-center space-y-4">
<div class="text-center space-y-2 md:space-y-4">
<div class="w-16 h-16 bg-accent/10 text-accent rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:search"></iconify-icon>
</div>
<h3 class="text-2xl font-bold font-display">1. Search</h3>
<p class="text-zinc-500">Enter your postcode or location to find every forecourt within a 520 mile radius instantly.</p>
<h3 class="text-xl md:text-5xl font-bold font-display">1. Search</h3>
<p class="text-zinc-500 text-sm md:text-lg">Enter your postcode or location to find every forecourt within a 520 mile radius instantly.</p>
</div>
<div class="text-center space-y-4">
<div class="text-center space-y-2 md:space-y-4">
<div class="w-16 h-16 bg-accent/10 text-accent rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:trending-up"></iconify-icon>
</div>
<h3 class="text-2xl font-bold font-display">2. Get Advice</h3>
<p class="text-zinc-500">Our AI compares local prices against national wholesale trends to give you a Fill Up / Wait recommendation.</p>
<h3 class="text-sm md:text-lg font-bold font-display">2. Get Advice</h3>
<p class="text-zinc-500 text-sm md:text-lg">Our AI compares local prices against national wholesale trends to give you a Fill Up / Wait recommendation.</p>
</div>
<div class="text-center space-y-4">
<div class="text-center space-y-2 md:space-y-4">
<div class="w-16 h-16 bg-accent/10 text-accent rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:wallet"></iconify-icon>
</div>
<h3 class="text-2xl font-bold font-display">3. Fill Up Smart</h3>
<p class="text-zinc-500">Navigate to the cheapest station and fill up with confidence knowing you've secured the best price.</p>
<h3 class="text-sm md:text-lg font-bold font-display">3. Fill Up Smart</h3>
<p class="text-zinc-500 text-sm md:text-lg">Navigate to the cheapest station and fill up with confidence knowing you've secured the best price.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section id="features" class="py-12 md:py-24 px-6">
<section id="features" class="py-4 md:py-24 px-6">
<div class="max-w-7xl mx-auto">
<div class="grid lg:grid-cols-2 gap-20 items-center">
<div class="order-2 lg:order-1">
@@ -187,8 +200,8 @@
</div>
<div class="order-1 lg:order-2 space-y-8">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800">The ultimate fuel companion.</h2>
<p class="text-lg text-zinc-500">Whether you're a daily commuter, a delivery professional, or just planning a weekend road trip, FuelAlert gives you the edge at the pump.</p>
<h2 class="text-xl md:text-5xl font-black font-display text-zinc-800">The ultimate fuel companion.</h2>
<p class="text-sm md:text-lg text-zinc-500">Whether you're a daily commuter, a delivery professional, or just planning a weekend road trip, FuelAlert gives you the edge at the pump.</p>
<ul class="space-y-4">
<li class="flex items-center gap-3 font-bold">
<iconify-icon class="text-accent" icon="lucide:check-circle-2"></iconify-icon>
@@ -213,7 +226,7 @@
</section>
<!-- Pricing -->
<section id="pricing" class="py-12 md:py-24 px-6 bg-zinc-50">
<section id="pricing" class="py-4 md:py-24 px-6 bg-zinc-50">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800 mb-4">Pricing for every driver</h2>
@@ -367,7 +380,7 @@
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Product</h5>
<h5 class="font-black text-xs text-zinc-800 tracking-widest">Product</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#pricing">Pricing</a></li>
<li><a class="hover:text-accent transition-colors" href="#features">Features</a></li>
@@ -377,7 +390,7 @@
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Resources</h5>
<h5 class="font-black text-xs text-zinc-800 tracking-widest">Resources</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#">Market Insights</a></li>
<li><a class="hover:text-accent transition-colors" href="#">How We Track</a></li>
@@ -387,7 +400,7 @@
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Legal</h5>
<h5 class="font-black text-xs text-zinc-800 tracking-widest">Legal</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#">Privacy Policy</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Terms of Service</a></li>
@@ -396,7 +409,7 @@
</div>
</div>
<div class="max-w-7xl mx-auto pt-8 border-t border-zinc-300 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] font-bold uppercase tracking-widest text-zinc-500">
<div class="max-w-7xl mx-auto pt-8 border-t border-zinc-300 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] tracking-widest text-zinc-500">
<p>© 2024 FuelAlert UK Limited. All Rights Reserved.</p>
<p>Data provided by official UK retail price transparency schemes.</p>
<p>Postcode data from <a class="underline hover:text-accent" href="https://geoportal.statistics.gov.uk/datasets/ons::onspd-online-latest-centroids-1/about" rel="noopener" target="_blank">ONS Postcode Directory</a>: contains OS data © Crown copyright &amp; database right, Royal Mail data © Royal Mail copyright &amp; database right, and National Statistics data © Crown copyright &amp; database right.</p>
@@ -501,23 +514,23 @@ const searchAttempted = ref(false)
const radiusMiles = ref(10)
const brandFilter = ref('')
const MAP_STORAGE_KEY = 'fuel-price:map-open'
const LIST_STORAGE_KEY = 'fuelalert:list-open'
function readSavedMapOpen() {
function readSavedListOpen() {
try {
const v = localStorage.getItem(MAP_STORAGE_KEY)
if (v === null) return true
const v = localStorage.getItem(LIST_STORAGE_KEY)
if (v === null) return false
return v === '1'
} catch {
return true
return false
}
}
const mapOpen = ref(readSavedMapOpen())
const listOpen = ref(readSavedListOpen())
watch(mapOpen, (v) => {
watch(listOpen, (v) => {
try {
localStorage.setItem(MAP_STORAGE_KEY, v ? '1' : '0')
localStorage.setItem(LIST_STORAGE_KEY, v ? '1' : '0')
} catch {
// ignore quota / privacy-mode errors
}