Skip to content

Enhance UI, add Campus Map, and integrate Neo4j features#6

Merged
Copilot merged 14 commits into
ai-researchfrom
main
Apr 18, 2026
Merged

Enhance UI, add Campus Map, and integrate Neo4j features#6
Copilot merged 14 commits into
ai-researchfrom
main

Conversation

@rodas-yg

Copy link
Copy Markdown
Member

No description provided.

rodas-yg and others added 13 commits April 4, 2026 16:29
…affold

- Notification model + check_favorites_and_notify Celery task (4AM daily)
- vision/log-meal endpoint via Google Gemini Vision (langchain)
- Gym/protein boost in item_scorer, profile passed through meal planner
- Neo4j connection settings + neo4j_models.py client skeleton
- New URL route for vision endpoint
- Fix fetchWaitTimes to call real API with mock fallback
- Fix isOpen() with proper time comparison against operating_hours
- Add isClosingSoon() badge for halls closing within 30 min
- Add skeleton loading states for hall grid and menu items
- Add toast notifications for favorites (no emojis)
- Remove duplicate nav header from DashboardView
- Add greeting and formatted date header to Dashboard
- Add refresh button with spin animation for wait times
- Add isLoading/planGenerated props to MealTimeline with distinct empty states
- Strip card wrapper from WaitTimeWidget (owned by Dashboard now)
- Rebuild Login page with proper Tailwind classes (was using undefined CSS classes like .card, .input, .btn-primary causing broken UI)
- Improve login error messages: distinguish 401 wrong credentials vs network down vs server error
- Add loading spinner to sign-in button
- Strip backdrop-blur/bg-white/5 glassmorphism from App.vue sidebar, DashboardView, DiningHallsView, MealTimeline
- Replace with clean slate-800/slate-700 solid cards
- Center all page content with max-w-* mx-auto
- Sidebar: cleaner bg-slate-950, no blur, tighter sizing
- DiningHallsView: simplified hall cards, cleaner menu item rows
- Delete neo4j_models.py
- Remove Neo4j settings from settings.py
- Remove Neo4j service from docker-compose.yml
- Remove neo4j from requirements.txt
- Strip Neo4j from .env.example and README
- CampusMap.vue: interactive Mapbox GL map with dining hall markers, user geolocation, and walking route on click
- CampusMapView.vue: full page view with hall picker grid
- Route /map added to router, nav link added to sidebar
- VITE_MAPBOX_TOKEN env var for frontend, MAPBOX_ACCESS_TOKEN in backend .env.example
- Updated README and docker-compose with Mapbox setup
- Add useTheme composable with localStorage persistence
- Enable Tailwind darkMode: 'class'
- Add sun/moon toggle button in sidebar
- Full light mode support across App, DiningHalls, Dashboard, MealTimeline, WaitTimeWidget
- Replace dynamic area dropdown with fixed North/Central/West Campus options
- Group halls by region with divider headers when All Areas is selected
- Flat grid shown when specific region is filtered
Copilot AI review requested due to automatic review settings April 16, 2026 03:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes the frontend UI (including dark mode), adds a Campus Map experience backed by Mapbox, and introduces new backend capabilities (Gemini Vision meal logging + daily favorite-meal notifications + a small personalization tweak in scoring).

Changes:

  • Frontend UI refresh across core views (login, dining halls, dashboard) + dark-mode toggle and Tailwind dark-mode configuration.
  • New Campus Map route/view/component using Mapbox GL + environment/config plumbing for Mapbox tokens.
  • Backend additions: Vision endpoint/service for image-based meal logging; Celery Beat task + Notification model for favorite-meal alerts; meal scoring updated to incorporate profile context.

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
frontend/tailwind.config.js Enables class-based dark mode.
frontend/src/views/Login.vue Login UI redesign + more granular error messaging.
frontend/src/views/DiningHallsView.vue New layout/filtering/cards/skeletons + favorites toast + updated “open” logic.
frontend/src/views/DashboardView.vue Dashboard redesign + greeting/date + manual wait-time refresh.
frontend/src/views/CampusMapView.vue New Campus Map page that embeds the Mapbox map component.
frontend/src/stores/mainStore.js Switches wait-time fetching to real API with fallback demo data.
frontend/src/router/index.js Adds /map route and restructures routes (removes login route/guard).
frontend/src/composables/useTheme.js Theme state + toggling via dark class.
frontend/src/components/WaitTimeWidget.vue Styling updates + empty-state message.
frontend/src/components/MealTimeline.vue Adds loading skeleton + clearer empty states.
frontend/src/components/CampusMap.vue New Mapbox GL map w/ hall markers and walking directions.
frontend/src/api/index.js Axios client simplified; auth interceptor logic removed.
frontend/src/App.vue New sidebar styling + theme toggle + Campus Map nav link.
frontend/package.json Adds mapbox-gl dependency.
frontend/package-lock.json Locks transitive deps for Mapbox GL.
frontend/.env.example Adds VITE_MAPBOX_TOKEN example.
docker-compose.yml Adds env wiring for Google API key + Mapbox token into services.
backend/requirements.txt Adds langchain-core dependency.
backend/bigredmacro/settings.py Adds Celery Beat crontab schedule for favorite notifications.
backend/api/views.py Adds authenticated Vision meal logging endpoint.
backend/api/urls.py Routes Vision endpoint.
backend/api/tasks.py Adds daily “check favorites and notify” task.
backend/api/services/vision_service.py Gemini Vision integration via LangChain.
backend/api/services/meal_planner.py Passes profile into scoring path.
backend/api/models.py Adds Notification document model.
backend/api/ml/item_scorer.py Adds “gym/high-protein” scoring boost based on profile.
backend/.env.example Documents SECRET_KEY, GOOGLE_API_KEY, MAPBOX_ACCESS_TOKEN.
README.md Overhauls setup instructions and documents new env vars/flows.
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/views/DiningHallsView.vue Outdated
Comment on lines +247 to +266
function hallsByRegion(regionKey) {
return halls.value.filter(hall => {
// Status match
const area = (hall.campus_area || '').toLowerCase()
const key = regionKey.toLowerCase()
if (!area.includes(key)) return false
const open = isOpen(hall)
if (filterStatus.value === 'open' && !open) return false
if (filterStatus.value === 'closed' && open) return false

// Area match
if (filterArea.value !== 'all' && hall.campus_area !== filterArea.value) return false

return true
})
})
}

const filteredHalls = computed(() =>
halls.value.filter(hall => {
const open = isOpen(hall)
if (filterStatus.value === 'open' && !open) return false
if (filterStatus.value === 'closed' && open) return false
const area = (hall.campus_area || '').toLowerCase()
const selected = filterArea.value.toLowerCase()
if (filterArea.value !== 'all' && !area.includes(selected)) return false

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hallsByRegion() / filteredHalls rely on hall.campus_area, but the backend DiningHallSerializer currently does not include campus_area in the /dining-halls/ payload. That means area.includes(...) will evaluate against an empty string and the grouped view (and any non-"all" area filter) will show zero halls. Either include campus_area in the backend serializer/endpoint response, or adjust the frontend to use a field that is actually returned.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/api/index.js
Comment on lines 1 to 6
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'

const api = axios.create({
baseURL: '/api',
headers: { 'Content-Type': 'application/json' },
})

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The axios client no longer attaches the JWT access token (and no longer auto-refreshes on 401). Since the backend is configured to use JWTAuthentication by default, all authenticated API calls (e.g., /dining-halls/, /profile/, /wait-times/) will start failing with 401. Restore an Authorization header mechanism (e.g., interceptor reading the Pinia auth store/localStorage) and, if needed, the refresh-on-401 retry logic, or explicitly pass auth headers in every API wrapper.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +148
const popup = new mapboxgl.Popup({ offset: 20, closeButton: false })
.setHTML(`
<div style="font-family: system-ui; padding: 4px 0;">
<strong style="color: #1e293b;">${hall.name}</strong>
<p style="margin: 4px 0 0; color: #64748b; font-size: 12px;">${hall.campus_area || ''} Campus</p>
</div>
`)

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Mapbox popup is built with setHTML(...) and interpolates hall.name / hall.campus_area directly into an HTML string. If any of those values ever come from an untrusted source, this becomes an XSS vector. Prefer popup.setText(...) for plain text, or build DOM nodes and use setDOMContent(...) (ensuring textContent is used for dynamic values).

Suggested change
const popup = new mapboxgl.Popup({ offset: 20, closeButton: false })
.setHTML(`
<div style="font-family: system-ui; padding: 4px 0;">
<strong style="color: #1e293b;">${hall.name}</strong>
<p style="margin: 4px 0 0; color: #64748b; font-size: 12px;">${hall.campus_area || ''} Campus</p>
</div>
`)
const popupContent = document.createElement('div')
popupContent.style.fontFamily = 'system-ui'
popupContent.style.padding = '4px 0'
const popupTitle = document.createElement('strong')
popupTitle.style.color = '#1e293b'
popupTitle.textContent = hall.name || ''
const popupSubtitle = document.createElement('p')
popupSubtitle.style.margin = '4px 0 0'
popupSubtitle.style.color = '#64748b'
popupSubtitle.style.fontSize = '12px'
popupSubtitle.textContent = `${hall.campus_area || ''} Campus`
popupContent.appendChild(popupTitle)
popupContent.appendChild(popupSubtitle)
const popup = new mapboxgl.Popup({ offset: 20, closeButton: false })
.setDOMContent(popupContent)

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +15
<!-- Map -->
<CampusMap
:halls="halls"
:access-token="mapboxToken"
map-height="500px"
@hall-click="onHallClick"
/>

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapboxToken falls back to an empty string, but CampusMap requires accessToken and initMap() unconditionally uses it. When the token isn't configured, Mapbox GL typically throws at runtime and the view becomes unusable. Add a guard UI (e.g., render a setup message instead of the map when the token is missing) or make the prop optional and no-op init when blank.

Copilot uses AI. Check for mistakes.
Comment thread backend/api/views.py
Comment on lines +280 to +287
b64_image = request.data.get("image")
if not b64_image:
return Response({"detail": "No image provided."}, status=status.HTTP_400_BAD_REQUEST)

description = identify_food_from_image(b64_image)
if "Error" in description or "Unknown" in description:
return Response({"detail": description}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log_meal_from_image treats any Vision failure (including missing API key) as a 500 and returns the raw error/description string back to the client. This both makes client handling harder (config errors vs. bad input vs. upstream issues) and can leak internal exception details. Consider: (1) validating/limiting the input image payload, (2) returning 400 for invalid input, 503 (or 502) for upstream Vision failures, and (3) returning a generic error message while logging the detailed exception server-side.

Copilot uses AI. Check for mistakes.
Comment on lines +284 to 309
function parseTimeToday(timeStr) {
const [hours, minutes] = timeStr.split(':').map(Number)
const d = new Date()
d.setHours(hours, minutes, 0, 0)
return d
}

function getTodayName() {
return new Date().toLocaleDateString('en-US', { weekday: 'long' })
}

function isOpen(hall) {
// A crude demo checker
return !!hall.operating_hours && Object.keys(hall.operating_hours).length > 0;
if (!hall.operating_hours || typeof hall.operating_hours !== 'object') return false
const todayHours = hall.operating_hours[getTodayName()]
if (!todayHours?.open || !todayHours?.close) return false
const now = new Date()
return now >= parseTimeToday(todayHours.open) && now < parseTimeToday(todayHours.close)
}

function isClosingSoon(hall) {
if (!hall.operating_hours || typeof hall.operating_hours !== 'object') return false
const todayHours = hall.operating_hours[getTodayName()]
if (!todayHours?.close) return false
const diffMs = parseTimeToday(todayHours.close) - new Date()
return diffMs > 0 && diffMs <= 30 * 60 * 1000
}

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new open/closing-soon logic assumes hall.operating_hours is keyed by weekday name and contains {open, close} fields. In the backend model/serializer, operating_hours is a dict keyed by meal period (e.g., "lunch": "11:00am–2:00pm"), so this will always return false (and parseTimeToday will produce NaN for strings like "11:00am"). Adjust the frontend to match the actual operating_hours format (parse the "start–end" strings per period, and consider a hall open if any period window contains now).

Copilot uses AI. Check for mistakes.
const regions = [
{ key: 'North', label: 'North Campus' },
{ key: 'Central', label: 'Central Campus' },
{ key: 'West', label: 'West Campus' },

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When "All Areas" is selected, halls are displayed only for regions listed in regions. The list currently omits "East", but the backend recognizes East campus locations; any East halls will silently disappear from the grouped view (and also can't be selected in the area dropdown). Add an East entry (or derive regions dynamically from the fetched data).

Suggested change
{ key: 'West', label: 'West Campus' },
{ key: 'West', label: 'West Campus' },
{ key: 'East', label: 'East Campus' },

Copilot uses AI. Check for mistakes.
Comment on lines +235 to +236
const toast = ref({ visible: false, message: '' })
let toastTimer = null

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toastTimer is a module-scoped timeout that is never cleared on component unmount. If the user navigates away quickly, the timer can still fire and mutate reactive state after unmount. Clear the timeout in an onUnmounted hook (and set toastTimer = null) to avoid lingering timers.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +27
api_key = os.getenv("GOOGLE_API_KEY")
if not api_key:
return "Unknown Food (API Key missing)"

llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", google_api_key=api_key)

msg = HumanMessage(
content=[
{"type": "text", "text": "What food item is in this image? Provide a brief, concise description outlining the main components (e.g., 'Grilled chicken salad with tomatoes'). Only reply with the description, no conversational text."},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}},
]
)

try:
response = llm.invoke([msg])
return response.content.strip()
except Exception as e:
return f"Error identifying food: {str(e)}"

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

identify_food_from_image returns exception text directly in the function result ("Error identifying food: ..."), which is later forwarded to clients. This can expose implementation details and makes error handling rely on brittle substring checks. Prefer raising a typed exception (or returning a structured result) so the API layer can map failures to appropriate HTTP statuses and return a safe, consistent error payload.

Copilot uses AI. Check for mistakes.
]
try {
const res = await getWaitTimes()
waitTimes.value = res.data

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchWaitTimes() assigns res.data directly to waitTimes, but the backend /wait-times/ response uses fields like dining_hall_name and estimated_wait_minutes (see WaitTimeSerializer). WaitTimeWidget.vue expects { name, wait_time }, so once the real API call succeeds the widget will display undefined/break (e.g., hall.name.charAt(0)). Map the API response into the UI shape, or update the widget to use the backend field names consistently.

Suggested change
waitTimes.value = res.data
const waitTimeData = Array.isArray(res.data) ? res.data : []
waitTimes.value = waitTimeData.map((hall) => ({
id: hall.id,
name: hall.name ?? hall.dining_hall_name,
wait_time: hall.wait_time ?? hall.estimated_wait_minutes,
capacity: hall.capacity
}))

Copilot uses AI. Check for mistakes.
Copilot AI merged commit bcb2233 into ai-research Apr 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants