Skip to content

Commit 0abae4e

Browse files
made componenets mobile responsive
1 parent aa63eaf commit 0abae4e

6 files changed

Lines changed: 434 additions & 50 deletions

File tree

frontend/src/components/layout/AppFooter.vue

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const currentYear = new Date().getFullYear()
7979
align-items: center;
8080
gap: var(--spacing-md);
8181
margin-bottom: var(--spacing-md);
82+
flex-wrap: wrap;
8283
}
8384
8485
.footer-logo-icon {
@@ -95,6 +96,13 @@ const currentYear = new Date().getFullYear()
9596
color: white;
9697
font-size: var(--font-size-lg);
9798
margin-bottom: 0;
99+
word-break: break-word;
100+
}
101+
102+
@media (max-width: 768px) {
103+
.footer-section h3 {
104+
font-size: var(--font-size-base);
105+
}
98106
}
99107
100108
.footer-section h4 {
@@ -143,6 +151,7 @@ const currentYear = new Date().getFullYear()
143151
color: rgba(255, 255, 255, 0.9);
144152
font-size: var(--font-size-sm);
145153
margin: var(--spacing-xs) 0;
154+
word-break: break-word;
146155
}
147156
148157
.footer-tagline {
@@ -155,9 +164,29 @@ const currentYear = new Date().getFullYear()
155164
.footer-text a, .hf-link {
156165
color: #ffffff;
157166
text-decoration: underline;
167+
word-break: break-all;
158168
}
159169
160170
.footer-text a:hover, .hf-link:hover {
161171
opacity: 0.9;
162172
}
173+
174+
@media (max-width: 768px) {
175+
.footer-container {
176+
padding: var(--spacing-lg) var(--spacing-md);
177+
}
178+
179+
.footer-section {
180+
text-align: center;
181+
}
182+
183+
.footer-logo {
184+
justify-content: center;
185+
}
186+
187+
.footer-links {
188+
text-align: left;
189+
display: inline-block;
190+
}
191+
}
163192
</style>

frontend/src/components/results/FoodRecommendations.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@ const items = ref([])
1313
const loading = ref(false)
1414
const error = ref(null)
1515
16+
// If store already has recommendations (e.g., fetched automatically after prediction), use them
17+
if (store.recommendations && store.recommendations.length) {
18+
items.value = store.recommendations
19+
}
20+
21+
// keep items in sync if the store recommendations update later
22+
watch(() => store.recommendations, (val) => {
23+
if (val && val.length) items.value = val
24+
})
25+
1626
async function load() {
1727
try {
1828
loading.value = true
1929
error.value = null
2030
const res = await store.fetchRecommendations(props.inputData, 6)
21-
items.value = res || []
31+
// fetchRecommendations returns an array (and stores into store.recommendations)
32+
items.value = res || store.recommendations || []
2233
} catch (err) {
2334
error.value = err.message || String(err)
2435
} finally {

frontend/src/store/predictionStore.js

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ export const usePredictionStore = defineStore('prediction', () => {
2525
currentInput.value = inputData
2626
currentPrediction.value = result
2727

28+
// Automatically fetch food recommendations based on the input data
29+
try {
30+
// fetchRecommendations will populate `recommendations` in this store
31+
await fetchRecommendations(inputData, 6)
32+
// Attach recommendations to the prediction result for easy access in the UI
33+
currentPrediction.value.recommendedFoods = recommendations.value || []
34+
} catch (recErr) {
35+
// Non-fatal: log and continue
36+
console.warn('Failed to fetch recommendations:', recErr)
37+
}
38+
2839
// Add to history (limit to last 10)
2940
predictionHistory.value.unshift({
3041
input: inputData,
@@ -88,14 +99,116 @@ export const usePredictionStore = defineStore('prediction', () => {
8899

89100
const vector = keys.map((k) => (inputData && inputData[k] != null ? Number(inputData[k]) : 0))
90101

91-
const result = await apiService.getRecommendations({ vector, top_k })
92-
if (result && result.success) {
93-
recommendations.value = result.items || []
102+
// Try ensemble-based recommendations first
103+
let recResult = null
104+
try {
105+
recResult = await apiService.getRecommendations({ vector, top_k: Math.max(top_k, 20) })
106+
} catch (e) {
107+
// Continue to fallback logic below
108+
recResult = null
109+
}
110+
111+
// Fetch local foods to enrich metadata and enable region/price filtering
112+
let localFoods = []
113+
try {
114+
const lf = await apiService.getLocalFoods()
115+
if (Array.isArray(lf)) localFoods = lf
116+
} catch (e) {
117+
// Non-fatal; we can proceed without local enrichment
118+
console.warn('Could not fetch local foods for enrichment:', e)
119+
}
120+
121+
const regionMap = ['central', 'western', 'eastern', 'northern']
122+
const preferredRegion = (inputData && typeof inputData.region_encoded === 'number') ? regionMap[inputData.region_encoded] : null
123+
const budget = inputData && (inputData.estimated_cost_ugx || inputData.budget)
124+
125+
// Helper to match recommended item to a local food entry
126+
const buildLocalMap = (arr) => {
127+
const byName = {}
128+
const byId = {}
129+
arr.forEach((f) => {
130+
if (!f) return
131+
const name = (f.name || f.title || '').toString().toLowerCase()
132+
if (name) byName[name] = f
133+
if (f.id != null) byId[String(f.id)] = f
134+
})
135+
return { byName, byId }
136+
}
137+
138+
const { byName, byId } = buildLocalMap(localFoods)
139+
140+
const enrichItem = (item) => {
141+
const meta = (item && item.meta) || {}
142+
// try match by meta name or id
143+
const nameKey = (meta.name || meta.title || item.id || '').toString().toLowerCase()
144+
const localMatch = byId[String(item.id)] || byName[nameKey]
145+
const mergedMeta = Object.assign({}, meta, (localMatch || {}))
146+
return Object.assign({}, item, { meta: mergedMeta })
147+
}
148+
149+
let items = []
150+
if (recResult && recResult.success && Array.isArray(recResult.items) && recResult.items.length) {
151+
// Enrich recommendations
152+
items = recResult.items.map(enrichItem)
153+
154+
// Filter out unavailable items if we have available flags
155+
const hasAvailability = items.some((it) => it.meta && typeof it.meta.available !== 'undefined')
156+
if (hasAvailability) {
157+
const filtered = items.filter((it) => it.meta && it.meta.available !== false)
158+
if (filtered.length) items = filtered
159+
}
160+
161+
// Sort: prefer same region, then by score desc, then by price asc
162+
items.sort((a, b) => {
163+
const aRegion = (a.meta && (a.meta.region || a.meta.region_name || a.meta.region_str) || '').toString().toLowerCase()
164+
const bRegion = (b.meta && (b.meta.region || b.meta.region_name || b.meta.region_str) || '').toString().toLowerCase()
165+
const aSame = preferredRegion && aRegion === preferredRegion ? 1 : 0
166+
const bSame = preferredRegion && bRegion === preferredRegion ? 1 : 0
167+
if (bSame !== aSame) return bSame - aSame
168+
const scoreDiff = (b.score || 0) - (a.score || 0)
169+
if (scoreDiff !== 0) return scoreDiff
170+
const aPrice = (a.meta && (a.meta.pricePerKg || a.meta.price_per_kg || a.meta.price)) || Number.MAX_SAFE_INTEGER
171+
const bPrice = (b.meta && (b.meta.pricePerKg || b.meta.price_per_kg || b.meta.price)) || Number.MAX_SAFE_INTEGER
172+
return aPrice - bPrice
173+
})
174+
175+
// If budget provided, prefer items with price <= budget (heuristic)
176+
if (budget) {
177+
const affordable = items.filter((it) => {
178+
const price = (it.meta && (it.meta.pricePerKg || it.meta.price_per_kg || it.meta.price)) || null
179+
if (price == null) return true
180+
// Treat budget as daily budget in UGX; fall back to allowing items cheaper than budget*2
181+
return Number(price) <= Number(budget) * 2
182+
})
183+
if (affordable.length) items = affordable
184+
}
185+
186+
// Limit to requested top_k
187+
items = items.slice(0, top_k)
188+
recommendations.value = items
94189
return recommendations.value
95-
} else {
96-
recError.value = (result && result.error) || 'Recommendation failed'
97-
return []
98190
}
191+
192+
// Fallback: choose local foods by region/availability/price when no embedding results
193+
if (localFoods && localFoods.length) {
194+
let picks = [...localFoods]
195+
// filter by availability if present
196+
if (picks.some((f) => typeof f.available !== 'undefined')) {
197+
picks = picks.filter((f) => f.available !== false)
198+
}
199+
if (preferredRegion) {
200+
const inRegion = picks.filter((f) => (f.region || '').toString().toLowerCase() === preferredRegion)
201+
if (inRegion.length) picks = inRegion
202+
}
203+
// sort by price asc if present
204+
picks.sort((a, b) => ( (a.pricePerKg || Number.MAX_SAFE_INTEGER) - (b.pricePerKg || Number.MAX_SAFE_INTEGER) ))
205+
picks = picks.slice(0, top_k).map((f) => ({ id: f.name || f.id, score: 0, meta: f }))
206+
recommendations.value = picks
207+
return recommendations.value
208+
}
209+
210+
recError.value = 'No recommendations available'
211+
return []
99212
} catch (err) {
100213
recError.value = err.message || String(err)
101214
throw err

frontend/src/views/ChatView.vue

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -515,43 +515,59 @@ onMounted(() => {
515515
.voice-features-grid {
516516
display: grid;
517517
grid-template-columns: 1fr;
518-
gap: var(--spacing-lg);
518+
gap: var(--spacing-md);
519519
}
520520
521521
@media (min-width: 768px) {
522522
.voice-features-grid {
523523
grid-template-columns: repeat(3, 1fr);
524+
gap: var(--spacing-lg);
524525
}
525526
}
526527
527528
.feature-item {
528529
display: flex;
529-
gap: var(--spacing-md);
530+
gap: var(--spacing-sm);
530531
align-items: flex-start;
531532
background: white;
532-
padding: var(--spacing-lg);
533+
padding: var(--spacing-md);
533534
border-radius: var(--radius-md);
534535
box-shadow: var(--shadow-sm);
535536
transition: transform 0.2s, box-shadow 0.2s;
536537
}
537538
539+
@media (min-width: 768px) {
540+
.feature-item {
541+
gap: var(--spacing-md);
542+
padding: var(--spacing-lg);
543+
}
544+
}
545+
538546
.feature-item:hover {
539547
transform: translateY(-4px);
540548
box-shadow: var(--shadow-md);
541549
}
542550
543551
.feature-icon {
544-
width: 60px;
545-
height: 60px;
552+
width: 48px;
553+
height: 48px;
546554
border-radius: 50%;
547555
display: flex;
548556
align-items: center;
549557
justify-content: center;
550558
flex-shrink: 0;
551-
font-size: 1.75rem;
559+
font-size: 1.5rem;
552560
color: white;
553561
}
554562
563+
@media (min-width: 768px) {
564+
.feature-icon {
565+
width: 60px;
566+
height: 60px;
567+
font-size: 1.75rem;
568+
}
569+
}
570+
555571
.feature-icon.microphone {
556572
background: linear-gradient(135deg, #d90000 0%, #ff4444 100%);
557573
animation: pulse-mic 2s infinite;
@@ -568,14 +584,27 @@ onMounted(() => {
568584
.feature-content h4 {
569585
margin: 0 0 var(--spacing-xs) 0;
570586
color: var(--text-color);
571-
font-size: var(--font-size-lg);
587+
font-size: var(--font-size-base);
588+
}
589+
590+
@media (min-width: 768px) {
591+
.feature-content h4 {
592+
font-size: var(--font-size-lg);
593+
}
572594
}
573595
574596
.feature-content p {
575597
margin: 0;
576598
color: var(--text-secondary);
577-
font-size: var(--font-size-sm);
578-
line-height: 1.5;
599+
font-size: 0.8rem;
600+
line-height: 1.4;
601+
}
602+
603+
@media (min-width: 768px) {
604+
.feature-content p {
605+
font-size: var(--font-size-sm);
606+
line-height: 1.5;
607+
}
579608
}
580609
581610
@keyframes pulse-icon {
@@ -622,16 +651,5 @@ onMounted(() => {
622651
font-size: var(--font-size-lg);
623652
}
624653
625-
.feature-item {
626-
flex-direction: column;
627-
text-align: center;
628-
align-items: center;
629-
}
630-
631-
.feature-icon {
632-
width: 50px;
633-
height: 50px;
634-
font-size: 1.5rem;
635-
}
636654
}
637655
</style>

0 commit comments

Comments
 (0)