diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 8803303..d7b8813 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/modules/calendar/__pycache__/service.cpython-312.pyc b/backend/modules/calendar/__pycache__/service.cpython-312.pyc index 01ad4b5..4b3a1bc 100644 Binary files a/backend/modules/calendar/__pycache__/service.cpython-312.pyc and b/backend/modules/calendar/__pycache__/service.cpython-312.pyc differ diff --git a/backend/modules/calendar/service.py b/backend/modules/calendar/service.py index 6b00f2b..5b2c27f 100644 --- a/backend/modules/calendar/service.py +++ b/backend/modules/calendar/service.py @@ -20,29 +20,57 @@ def create_calendar_event(db: Session, user_id: int, event_data: CalendarEventCr return event def get_calendar_events(db: Session, user_id: int, start: datetime | None, end: datetime | None): + """ + Retrieves calendar events for a user, optionally filtered by a date range. + + Args: + db: The database session. + user_id: The ID of the user whose events are to be retrieved. + start: The start datetime of the filter range (inclusive). + end: The end datetime of the filter range (exclusive). + + Returns: + A list of CalendarEvent objects matching the criteria, ordered by start time. + """ + print(f"Getting calendar events for user {user_id} in range [{start}, {end})") query = db.query(CalendarEvent).filter(CalendarEvent.user_id == user_id) - # If start and end dates are provided, filter for events overlapping the range. - # An event overlaps if: event_start < query_end AND (event_end IS NULL OR event_end > query_start) + # If start and end dates are provided, filter for events overlapping the range [start, end). if start and end: + # An event overlaps the range [start, end) if: + # 1. It has a duration (end is not None) AND its interval [event.start, event.end) + # intersects with [start, end). Intersection occurs if: + # event.start < end AND event.end > start + # 2. It's a point event (end is None) AND its start time falls within the range: + # start <= event.start < end query = query.filter( - CalendarEvent.start < end, # Event starts before the query window ends or_( - CalendarEvent.end == None, # Event has no end date (considered single point in time at start) - CalendarEvent.end > start # Event ends after the query window starts + # Case 1: Event has duration and overlaps + (CalendarEvent.end != None) & (CalendarEvent.start < end) & (CalendarEvent.end > start), + # Case 2: Event is a point event within the range + (CalendarEvent.end == None) & (CalendarEvent.start >= start) & (CalendarEvent.start < end) ) ) # If only start is provided, filter events starting on or after start elif start: + # Includes events with duration starting >= start + # Includes point events occurring >= start query = query.filter(CalendarEvent.start >= start) - # If only end is provided, filter events ending on or before end (or starting before end if no end date) + # If only end is provided, filter events ending before end, or point events occurring before end elif end: + # Includes events with duration ending <= end (or starting before end if end is None) + # Includes point events occurring < end query = query.filter( or_( - CalendarEvent.end <= end, - (CalendarEvent.end == None and CalendarEvent.start < end) + # Event ends before the specified end time + (CalendarEvent.end != None) & (CalendarEvent.end <= end), + # Point event occurs before the specified end time + (CalendarEvent.end == None) & (CalendarEvent.start < end) ) ) + # Alternative interpretation for "ending before end": include events that *start* before end + # query = query.filter(CalendarEvent.start < end) + return query.order_by(CalendarEvent.start).all() # Order by start time diff --git a/backend/modules/nlp/__pycache__/api.cpython-312.pyc b/backend/modules/nlp/__pycache__/api.cpython-312.pyc index 78a6a0a..f7b10bd 100644 Binary files a/backend/modules/nlp/__pycache__/api.cpython-312.pyc and b/backend/modules/nlp/__pycache__/api.cpython-312.pyc differ diff --git a/backend/modules/nlp/__pycache__/schemas.cpython-312.pyc b/backend/modules/nlp/__pycache__/schemas.cpython-312.pyc index 5b4a3e8..4cecfc6 100644 Binary files a/backend/modules/nlp/__pycache__/schemas.cpython-312.pyc and b/backend/modules/nlp/__pycache__/schemas.cpython-312.pyc differ diff --git a/backend/modules/nlp/api.py b/backend/modules/nlp/api.py index 34fbb43..0800ab9 100644 --- a/backend/modules/nlp/api.py +++ b/backend/modules/nlp/api.py @@ -1,74 +1,103 @@ # modules/nlp/api.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session +from typing import List from core.database import get_db from modules.auth.dependencies import get_current_user from modules.auth.models import User from modules.nlp.service import process_request, ask_ai -from modules.nlp.schemas import ProcessCommandRequest +# Import the response schema +from modules.nlp.schemas import ProcessCommandRequest, ProcessCommandResponse from modules.calendar.service import create_calendar_event, get_calendar_events, update_calendar_event, delete_calendar_event +# Import the CalendarEvent *model* for type hinting +from modules.calendar.models import CalendarEvent +# Import the CalendarEvent Pydantic schemas for data validation from modules.calendar.schemas import CalendarEventCreate, CalendarEventUpdate router = APIRouter(prefix="/nlp", tags=["nlp"]) -@router.post("/process-command") +# Helper to format calendar events (expects list of CalendarEvent models) +def format_calendar_events(events: List[CalendarEvent]) -> List[str]: + if not events: + return ["You have no events matching that criteria."] + formatted = ["Here are the events:"] + for event in events: + # Access attributes directly from the model instance + start_str = event.start.strftime("%Y-%m-%d %H:%M") if event.start else "No start time" + end_str = event.end.strftime("%H:%M") if event.end else "" + title = event.title or "Untitled Event" + formatted.append(f"- {title} ({start_str}{' - ' + end_str if end_str else ''})") + return formatted + +# Update the response model for the endpoint +@router.post("/process-command", response_model=ProcessCommandResponse) def process_command(request_data: ProcessCommandRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): """ - Process the user command, execute the action, and return a user-friendly response. + Process the user command, execute the action, and return user-friendly responses. """ - user_input = request_data.user_input + user_input = request_data.user_input command_data = process_request(user_input) intent = command_data["intent"] params = command_data["params"] response_text = command_data["response_text"] + responses = [response_text] # Start with the initial response + if intent == "error": raise HTTPException(status_code=400, detail=response_text) - - if intent == "clarification_needed": - return {"response": response_text} - - if intent == "unknown": - return {"response": response_text} + + if intent == "clarification_needed" or intent == "unknown": + return ProcessCommandResponse(responses=responses) try: match intent: case "ask_ai": ai_answer = ask_ai(**params) - return {"response": ai_answer} - + responses.append(ai_answer) + return ProcessCommandResponse(responses=responses) + case "get_calendar_events": - result = get_calendar_events(db, current_user.id, **params) - return {"response": response_text, "details": result} - + # get_calendar_events returns List[CalendarEvent models] + events: List[CalendarEvent] = get_calendar_events(db, current_user.id, **params) + responses.extend(format_calendar_events(events)) + return ProcessCommandResponse(responses=responses) + case "add_calendar_event": - event = CalendarEventCreate(**params) - result = create_calendar_event(db, current_user.id, event) - return {"response": response_text, "details": result} + # Validate input with Pydantic schema + event_data = CalendarEventCreate(**params) + created_event = create_calendar_event(db, current_user.id, event_data) + start_str = created_event.start.strftime("%Y-%m-%d %H:%M") if created_event.start else "No start time" + title = created_event.title or "Untitled Event" + responses.append(f"Added: {title} starting at {start_str}.") + return ProcessCommandResponse(responses=responses) case "update_calendar_event": event_id = params.pop('event_id', None) if event_id is None: raise HTTPException(status_code=400, detail="Event ID is required for update.") + # Validate input with Pydantic schema event_data = CalendarEventUpdate(**params) - result = update_calendar_event(db, current_user.id, event_id, event_data=event_data) - return {"response": response_text, "details": result} - + updated_event = update_calendar_event(db, current_user.id, event_id, event_data=event_data) + title = updated_event.title or "Untitled Event" + responses.append(f"Updated event ID {updated_event.id}: {title}.") + return ProcessCommandResponse(responses=responses) + case "delete_calendar_event": event_id = params.get('event_id') if event_id is None: raise HTTPException(status_code=400, detail="Event ID is required for delete.") - result = delete_calendar_event(db, current_user.id, event_id) - return {"response": response_text, "details": {"deleted": True, "event_id": event_id}} - + delete_calendar_event(db, current_user.id, event_id) + responses.append(f"Deleted event ID {event_id}.") + return ProcessCommandResponse(responses=responses) + case _: print(f"Warning: Unhandled intent '{intent}' reached api.py match statement.") - raise HTTPException(status_code=500, detail="An unexpected error occurred processing the command.") + return ProcessCommandResponse(responses=responses) except HTTPException as http_exc: raise http_exc except Exception as e: print(f"Error executing intent '{intent}': {e}") - raise HTTPException(status_code=500, detail="Sorry, I encountered an error while trying to perform that action.") \ No newline at end of file + return ProcessCommandResponse(responses=["Sorry, I encountered an error while trying to perform that action."]) \ No newline at end of file diff --git a/backend/modules/nlp/schemas.py b/backend/modules/nlp/schemas.py index f0de734..48b1870 100644 --- a/backend/modules/nlp/schemas.py +++ b/backend/modules/nlp/schemas.py @@ -1,5 +1,11 @@ # modules/nlp/schemas.py from pydantic import BaseModel +from typing import List class ProcessCommandRequest(BaseModel): user_input: str + +class ProcessCommandResponse(BaseModel): + responses: List[str] + # Optional: Keep details if needed for specific frontend logic beyond display + # details: dict | None = None diff --git a/interfaces/nativeapp/App.tsx b/interfaces/nativeapp/App.tsx index e62406a..151bfe8 100644 --- a/interfaces/nativeapp/App.tsx +++ b/interfaces/nativeapp/App.tsx @@ -1,32 +1,77 @@ // App.tsx -import React from 'react'; -import { Platform } from 'react-native'; +import React, { useCallback } from 'react'; // Removed useEffect, useState as they are implicitly used by useFonts +import { Platform, View } from 'react-native'; import { Provider as PaperProvider } from 'react-native-paper'; -import { NavigationContainer } from '@react-navigation/native'; // Always used +import { NavigationContainer, DarkTheme as NavigationDarkTheme } from '@react-navigation/native'; // Import NavigationDarkTheme import { SafeAreaProvider } from 'react-native-safe-area-context'; import { StatusBar } from 'expo-status-bar'; +import * as SplashScreen from 'expo-splash-screen'; +import { useFonts } from 'expo-font'; import { AuthProvider } from './src/contexts/AuthContext'; import RootNavigator from './src/navigation/RootNavigator'; -import theme from './src/constants/theme'; -// Import the combined theme -import { CombinedDarkTheme } from './src/navigation/WebAppLayout'; // Adjust import path if needed +import theme from './src/constants/theme'; // This is the Paper theme +// Removed CombinedDarkTheme import as we'll use NavigationDarkTheme directly for NavigationContainer + +// Keep the splash screen visible while we fetch resourcesDone, please go ahead with the changes. +SplashScreen.preventAutoHideAsync(); + +// Create a navigation theme based on the Paper theme colors but without the incompatible fonts object +const navigationTheme = { + ...NavigationDarkTheme, // Use React Navigation's dark theme as a base + colors: { + ...NavigationDarkTheme.colors, + primary: theme.colors.primary, + background: theme.colors.background, + card: theme.colors.surface, // Map Paper surface to Navigation card + text: theme.colors.text, + border: theme.colors.surface, // Use surface for border or another appropriate color + notification: theme.colors.primary, // Example mapping + }, +}; export default function App() { + const [fontsLoaded, fontError] = useFonts({ + 'Inter-Regular': require('./src/assets/fonts/Inter-Regular.ttf'), + 'Inter-Bold': require('./src/assets/fonts/Inter-Bold.ttf'), + 'Inter-Medium': require('./src/assets/fonts/Inter-Medium.ttf'), + 'Inter-Light': require('./src/assets/fonts/Inter-Light.ttf'), + 'Inter-Thin': require('./src/assets/fonts/Inter-Thin.ttf'), + // Add other weights/styles if you have them + }); + + const onLayoutRootView = useCallback(async () => { + if (fontsLoaded || fontError) { + // Log font loading status + if (fontError) { + console.error("Font loading error:", fontError); + } + await SplashScreen.hideAsync(); + } + }, [fontsLoaded, fontError]); + + if (!fontsLoaded && !fontError) { + return null; // Return null or a loading indicator while fonts are loading + } + + // If fonts are loaded (or there was an error), render the app return ( - - - - {/* NavigationContainer wraps RootNavigator for ALL platforms */} - - - - - - - + + + + {/* PaperProvider uses the full theme with custom fonts */} + + {/* NavigationContainer uses the simplified navigationTheme */} + + + + + + + + ); } \ No newline at end of file diff --git a/interfaces/nativeapp/app.json b/interfaces/nativeapp/app.json index 6531458..957c7ea 100644 --- a/interfaces/nativeapp/app.json +++ b/interfaces/nativeapp/app.json @@ -25,7 +25,8 @@ "favicon": "./assets/favicon.png" }, "plugins": [ - "expo-secure-store" + "expo-secure-store", + "expo-font" ] } } diff --git a/interfaces/nativeapp/package-lock.json b/interfaces/nativeapp/package-lock.json index 40cc055..412bb1a 100644 --- a/interfaces/nativeapp/package-lock.json +++ b/interfaces/nativeapp/package-lock.json @@ -18,7 +18,9 @@ "axios": "^1.8.4", "date-fns": "^4.1.0", "expo": "~52.0.46", + "expo-font": "~13.0.4", "expo-secure-store": "~14.0.1", + "expo-splash-screen": "~0.29.24", "expo-status-bar": "~2.0.1", "react": "18.3.1", "react-dom": "18.3.1", @@ -5405,6 +5407,17 @@ "expo": "*" } }, + "node_modules/expo-splash-screen": { + "version": "0.29.24", + "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.29.24.tgz", + "integrity": "sha512-k2rdjbb3Qeg4g104Sdz6+qXXYba8QgiuZRSxHX8IpsSYiiTU48BmCCGy12sN+O1B+sD1/+WPL4duCa1Fy6+Y4g==", + "dependencies": { + "@expo/prebuild-config": "~8.2.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-status-bar": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.0.1.tgz", diff --git a/interfaces/nativeapp/package.json b/interfaces/nativeapp/package.json index 44a4896..0dbf64c 100644 --- a/interfaces/nativeapp/package.json +++ b/interfaces/nativeapp/package.json @@ -31,7 +31,9 @@ "react-native-screens": "~4.4.0", "react-native-vector-icons": "^10.2.0", "react-native-web": "~0.19.13", - "@react-native-community/datetimepicker": "8.2.0" + "@react-native-community/datetimepicker": "8.2.0", + "expo-font": "~13.0.4", + "expo-splash-screen": "~0.29.24" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/interfaces/nativeapp/src/assets/fonts/Inter-Bold.ttf b/interfaces/nativeapp/src/assets/fonts/Inter-Bold.ttf new file mode 100644 index 0000000..46b3583 Binary files /dev/null and b/interfaces/nativeapp/src/assets/fonts/Inter-Bold.ttf differ diff --git a/interfaces/nativeapp/src/assets/fonts/Inter-Light.ttf b/interfaces/nativeapp/src/assets/fonts/Inter-Light.ttf new file mode 100644 index 0000000..1a2a6f2 Binary files /dev/null and b/interfaces/nativeapp/src/assets/fonts/Inter-Light.ttf differ diff --git a/interfaces/nativeapp/src/assets/fonts/Inter-Medium.ttf b/interfaces/nativeapp/src/assets/fonts/Inter-Medium.ttf new file mode 100644 index 0000000..5c88739 Binary files /dev/null and b/interfaces/nativeapp/src/assets/fonts/Inter-Medium.ttf differ diff --git a/interfaces/nativeapp/src/assets/fonts/Inter-Regular.ttf b/interfaces/nativeapp/src/assets/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..6b088a7 Binary files /dev/null and b/interfaces/nativeapp/src/assets/fonts/Inter-Regular.ttf differ diff --git a/interfaces/nativeapp/src/assets/fonts/Inter-Thin.ttf b/interfaces/nativeapp/src/assets/fonts/Inter-Thin.ttf new file mode 100644 index 0000000..3505b35 Binary files /dev/null and b/interfaces/nativeapp/src/assets/fonts/Inter-Thin.ttf differ diff --git a/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx b/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx index 9d33bcf..94745ab 100644 --- a/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx +++ b/interfaces/nativeapp/src/components/calendar/CalendarDayCell.tsx @@ -24,9 +24,11 @@ const CalendarDayCell: React.FC = ({ date, events, isCurre width: width, height: height, borderWidth: 0.5, + borderTopWidth: 0, borderColor: theme.colors.outlineVariant, padding: 2, - backgroundColor: isCurrentMonth ? theme.colors.surface : theme.colors.surfaceDisabled, // Dim non-month days + paddingTop: 0, + backgroundColor: theme.colors.background, overflow: 'hidden', // Prevent events overflowing cell boundaries }, dateNumberContainer: { @@ -34,8 +36,9 @@ const CalendarDayCell: React.FC = ({ date, events, isCurre marginBottom: 2, }, dateNumber: { - fontSize: 10, + fontSize: 12, fontWeight: today ? 'bold' : 'normal', + marginTop: 8, color: today ? theme.colors.primary : (isCurrentMonth ? theme.colors.onSurface : theme.colors.onSurfaceDisabled), }, eventsContainer: { diff --git a/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx b/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx index f679a62..9e792d8 100644 --- a/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx +++ b/interfaces/nativeapp/src/components/calendar/CalendarHeader.tsx @@ -2,19 +2,23 @@ import React from 'react'; import { View, StyleSheet } from 'react-native'; import { Text, IconButton, useTheme } from 'react-native-paper'; +import ViewSwitcher from './ViewSwitcher'; +import { CalendarViewMode } from './CustomCalendarView'; interface CalendarHeaderProps { currentRangeText: string; onPrev: () => void; onNext: () => void; + currentView: CalendarViewMode; + onViewChange: (view: CalendarViewMode) => void; } -const CalendarHeader: React.FC = ({ currentRangeText, onPrev, onNext }) => { +const CalendarHeader: React.FC = ({ currentRangeText, onPrev, onNext, currentView, onViewChange }) => { const theme = useTheme(); const styles = StyleSheet.create({ container: { flexDirection: 'row', - justifyContent: 'space-between', + justifyContent: 'flex-start', alignItems: 'center', paddingVertical: 8, paddingHorizontal: 12, @@ -37,13 +41,16 @@ const CalendarHeader: React.FC = ({ currentRangeText, onPre size={24} iconColor={theme.colors.primary} // Use theme color /> - {currentRangeText} + {/* Placeholder for alignment */} + {currentRangeText} + {/* Spacer to push ViewSwitcher to the right */} + ); }; diff --git a/interfaces/nativeapp/src/components/calendar/CustomCalendarView.tsx b/interfaces/nativeapp/src/components/calendar/CustomCalendarView.tsx index 08ee8ff..1226931 100644 --- a/interfaces/nativeapp/src/components/calendar/CustomCalendarView.tsx +++ b/interfaces/nativeapp/src/components/calendar/CustomCalendarView.tsx @@ -7,6 +7,8 @@ import { addWeeks, subWeeks, addDays, subDays, eachDayOfInterval, format, getMonth, getYear, isSameMonth, parseISO, isValid, isSameDay, startOfDay, endOfDay } from 'date-fns'; +// Import useFocusEffect +import { useFocusEffect } from '@react-navigation/native'; import CalendarHeader from './CalendarHeader'; import ViewSwitcher from './ViewSwitcher'; @@ -69,9 +71,17 @@ const CustomCalendarView = () => { } }, [startDate, endDate]); // Depend on calculated start/end dates - useEffect(() => { - fetchEvents(); - }, [fetchEvents]); // Re-run fetchEvents when it changes (due to date changes) + // Use useFocusEffect to fetch events when the screen is focused + useFocusEffect( + useCallback(() => { + console.log('[CustomCalendar] Screen focused, fetching events.'); + fetchEvents(); + // Optional: Return a cleanup function if needed, though not necessary for just fetching + return () => { + console.log('[CustomCalendar] Screen unfocused.'); + }; + }, [fetchEvents]) // Re-run effect if fetchEvents changes (due to date range change) + ); // Navigation handlers const handlePrev = useCallback(() => { @@ -147,9 +157,9 @@ const CustomCalendarView = () => { currentRangeText={displayRangeText} onPrev={handlePrev} onNext={handleNext} + currentView={viewMode} + onViewChange={setViewMode} /> - - {isLoading ? ( diff --git a/interfaces/nativeapp/src/components/calendar/EventItem.tsx b/interfaces/nativeapp/src/components/calendar/EventItem.tsx index 1899181..541446a 100644 --- a/interfaces/nativeapp/src/components/calendar/EventItem.tsx +++ b/interfaces/nativeapp/src/components/calendar/EventItem.tsx @@ -43,7 +43,7 @@ const EventItem: React.FC = ({ event, showTime = true }) => { }, text: { color: theme.colors.onPrimary, // Ensure text is readable on the background color - fontSize: 10, + fontSize: 12, fontWeight: '500', }, timeText: { diff --git a/interfaces/nativeapp/src/components/calendar/MonthView.tsx b/interfaces/nativeapp/src/components/calendar/MonthView.tsx index e72c161..0982810 100644 --- a/interfaces/nativeapp/src/components/calendar/MonthView.tsx +++ b/interfaces/nativeapp/src/components/calendar/MonthView.tsx @@ -45,7 +45,7 @@ const MonthView: React.FC = ({ startDate, eventsByDate }) => { } }); - const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + const weekDays = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']; const styles = StyleSheet.create({ container: { flex: 1 }, @@ -56,17 +56,20 @@ const MonthView: React.FC = ({ startDate, eventsByDate }) => { dayHeaderRow: { flexDirection: 'row', paddingVertical: 5, - borderBottomWidth: 1, - borderBottomColor: theme.colors.outlineVariant, - backgroundColor: theme.colors.surfaceVariant, // Slightly different background for header + paddingBottom: 0, + backgroundColor: theme.colors.background, }, dayHeaderCell: { flex: 1, alignItems: 'center', justifyContent: 'center', + borderLeftWidth: 0.5, + borderRightWidth: 0.5, + borderColor: theme.colors.outlineVariant, + backgroundColor: theme.colors.background, }, dayHeaderText: { - fontSize: 10, + fontSize: 11, fontWeight: 'bold', color: theme.colors.onSurfaceVariant, }, diff --git a/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx b/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx index aacbbc1..d4e7393 100644 --- a/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx +++ b/interfaces/nativeapp/src/components/calendar/ViewSwitcher.tsx @@ -16,7 +16,6 @@ const ViewSwitcher: React.FC = ({ currentView, onViewChange } paddingVertical: 8, paddingHorizontal: 16, backgroundColor: theme.colors.surface, // Match background - borderBottomWidth: 1, borderBottomColor: theme.colors.outlineVariant, }, }); @@ -27,12 +26,11 @@ const ViewSwitcher: React.FC = ({ currentView, onViewChange } value={currentView} onValueChange={(value) => onViewChange(value as CalendarViewMode)} // Cast value buttons={[ - { value: 'month', label: 'Month' }, - { value: 'week', label: 'Week' }, - { value: '3day', label: '3-Day' }, + { value: 'month', label: 'M', checkedColor: theme.colors.onPrimary }, + { value: 'week', label: 'W', checkedColor: theme.colors.onPrimary }, + { value: '3day', label: '3', checkedColor: theme.colors.onPrimary }, ]} - // Optional: Add density for smaller buttons - // density="medium" + density="high" /> ); diff --git a/interfaces/nativeapp/src/constants/colors.ts b/interfaces/nativeapp/src/constants/colors.ts index 5242bcc..ccdabde 100644 --- a/interfaces/nativeapp/src/constants/colors.ts +++ b/interfaces/nativeapp/src/constants/colors.ts @@ -1,11 +1,12 @@ // src/constants/colors.ts export const colors = { - background: '#0a0a0a', // Dark blue-teal background + background: '#131314', // Dark blue-teal background primary: '#4DB6AC', // Main teal color for text, icons, active elements secondary: '#64FFDA', // Bright cyan accent for highlights, important actions - surface: '#252525', // Slightly lighter background for cards/modals (optional) + surface: '#1b1b1b', // Slightly lighter background for cards/modals (optional) text: '#FFFFFF', // White text for high contrast on dark background textSecondary: '#B0BEC5', // Lighter gray for less important text error: '#FF5252', // Standard error color disabled: '#78909C', // Color for disabled elements + outline: '#333537', // Outline color for borders }; \ No newline at end of file diff --git a/interfaces/nativeapp/src/constants/theme.ts b/interfaces/nativeapp/src/constants/theme.ts index 754e417..354978e 100644 --- a/interfaces/nativeapp/src/constants/theme.ts +++ b/interfaces/nativeapp/src/constants/theme.ts @@ -1,44 +1,77 @@ // src/constants/theme.ts import { MD3DarkTheme as DefaultTheme, configureFonts } from 'react-native-paper'; +import { Platform } from 'react-native'; import { colors } from './colors'; -// const fontConfig = { -// default: { -// regular: { -// fontFamily: 'Inter, sans-serif', -// fontWeight: 'normal', -// fontSize: 14, -// lineHeight: 20, -// letterSpacing: 0.25, -// }, -// medium: { -// fontFamily: 'Inter, sans-serif', -// fontWeight: '500', -// fontSize: 16, -// lineHeight: 24, -// letterSpacing: 0.15, -// }, -// light: { -// fontFamily: 'Inter, sans-serif', -// fontWeight: '300', -// fontSize: 12, -// lineHeight: 16, -// letterSpacing: 0.4, -// }, -// thin: { -// fontFamily: 'Inter, sans-serif', -// fontWeight: '100', -// fontSize: 10, -// lineHeight: 14, -// letterSpacing: 0.5, -// }, -// }, -// }; -// const fonts = configureFonts({ config: fontConfig }); +// Define the font configuration using the names loaded in App.tsx +const fontConfig = { + // Default configuration for all platforms + regular: { + fontFamily: 'Inter-Regular', + fontWeight: 'normal' as 'normal', // Type assertion needed + }, + medium: { + fontFamily: 'Inter-Medium', + fontWeight: '500' as '500', // Type assertion needed + }, + light: { + fontFamily: 'Inter-Light', + fontWeight: '300' as '300', // Type assertion needed + }, + thin: { + fontFamily: 'Inter-Thin', + fontWeight: '100' as '100', // Type assertion needed + }, + // Add bold if needed and loaded + bold: { + fontFamily: 'Inter-Bold', + fontWeight: 'bold' as 'bold', // Type assertion needed + }, + + // Define specific font variants used by Paper components + // These map to the keys above + displayLarge: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 57, lineHeight: 64, letterSpacing: -0.25 }, + displayMedium: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 45, lineHeight: 52, letterSpacing: 0 }, + displaySmall: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 36, lineHeight: 44, letterSpacing: 0 }, + + headlineLarge: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 32, lineHeight: 40, letterSpacing: 0 }, + headlineMedium: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 28, lineHeight: 36, letterSpacing: 0 }, + headlineSmall: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 24, lineHeight: 32, letterSpacing: 0 }, + + titleLarge: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 22, lineHeight: 28, letterSpacing: 0 }, + titleMedium: { fontFamily: 'Inter-Medium', fontWeight: '500' as '500', fontSize: 16, lineHeight: 24, letterSpacing: 0.15 }, + titleSmall: { fontFamily: 'Inter-Medium', fontWeight: '500' as '500', fontSize: 14, lineHeight: 20, letterSpacing: 0.1 }, + + labelLarge: { fontFamily: 'Inter-Medium', fontWeight: '500' as '500', fontSize: 14, lineHeight: 20, letterSpacing: 0.1 }, + labelMedium: { fontFamily: 'Inter-Medium', fontWeight: '500' as '500', fontSize: 12, lineHeight: 16, letterSpacing: 0.5 }, + labelSmall: { fontFamily: 'Inter-Medium', fontWeight: '500' as '500', fontSize: 11, lineHeight: 16, letterSpacing: 0.5 }, + + bodyLarge: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 16, lineHeight: 24, letterSpacing: 0.5 }, + bodyMedium: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 14, lineHeight: 20, letterSpacing: 0.25 }, + bodySmall: { fontFamily: 'Inter-Regular', fontWeight: '400' as '400', fontSize: 12, lineHeight: 16, letterSpacing: 0.4 }, + + // Default is used as a fallback + default: { + fontFamily: 'Inter-Regular', + fontWeight: 'normal' as 'normal', + }, +}; + + +// Configure fonts for React Native Paper V5 (MD3) +// The structure differs slightly from V4 +const fonts = configureFonts({ + config: fontConfig, + // You might need specific web/ios/android configs if fonts differ + // web: fontConfig, + // ios: fontConfig, + // android: fontConfig, +}); + const theme = { ...DefaultTheme, // Use MD3 dark theme as a base - // fonts: fonts, + fonts: fonts, // Apply the configured fonts colors: { ...DefaultTheme.colors, // Keep default colors unless overridden primary: colors.primary, @@ -46,6 +79,8 @@ const theme = { secondary: colors.secondary, tertiary: colors.secondary, // Assign accent to tertiary as well if needed background: colors.background, + outline: colors.outline, + outlineVariant: colors.outline, surface: colors.surface || colors.background, // Use surface or fallback to background text: colors.text, onPrimary: colors.background, // Text color on primary background diff --git a/interfaces/nativeapp/src/screens/ChatScreen.tsx b/interfaces/nativeapp/src/screens/ChatScreen.tsx index 1b108f2..4a939ae 100644 --- a/interfaces/nativeapp/src/screens/ChatScreen.tsx +++ b/interfaces/nativeapp/src/screens/ChatScreen.tsx @@ -13,6 +13,11 @@ interface Message { timestamp: Date; } +// Define the expected structure for the API response +interface NlpResponse { + responses: string[]; // Expecting an array of response strings +} + const ChatScreen = () => { const theme = useTheme(); const [messages, setMessages] = useState([]); @@ -32,6 +37,7 @@ const ChatScreen = () => { timestamp: new Date(), }; + // Add user message optimistically setMessages(prevMessages => [...prevMessages, userMessage]); setInputText(''); setIsLoading(true); @@ -39,20 +45,37 @@ const ChatScreen = () => { // Scroll to bottom after sending user message setTimeout(() => flatListRef.current?.scrollToEnd({ animated: true }), 100); - // --- Call Backend API --- + // --- Call Backend API --- try { console.log(`[ChatScreen] Sending to /nlp/process-command: ${trimmedText}`); - const response = await apiClient.post<{ response: string }>('/nlp/process-command', { user_input: trimmedText }); + // Expect the backend to return an object with a 'responses' array + const response = await apiClient.post('/nlp/process-command', { user_input: trimmedText }); console.log("[ChatScreen] Received response:", response.data); - const aiResponse: Message = { - id: Date.now().toString() + '-ai', - // Assuming the backend returns the response text in a 'response' field - text: response.data.response || "Sorry, I didn't get a valid response.", - sender: 'ai', - timestamp: new Date(), - }; - setMessages(prevMessages => [...prevMessages, aiResponse]); + const aiResponses: Message[] = []; + if (response.data && Array.isArray(response.data.responses) && response.data.responses.length > 0) { + response.data.responses.forEach((responseText, index) => { + aiResponses.push({ + id: `${Date.now()}-ai-${index}`, // Ensure unique IDs + text: responseText || "...", // Handle potential empty strings + sender: 'ai', + timestamp: new Date(), + }); + }); + } else { + // Handle cases where the response format is unexpected or empty + console.warn("[ChatScreen] Received invalid or empty responses array:", response.data); + aiResponses.push({ + id: Date.now().toString() + '-ai-fallback', + text: "Sorry, I didn't get a valid response.", + sender: 'ai', + timestamp: new Date(), + }); + } + + // Add all AI responses to the state + setMessages(prevMessages => [...prevMessages, ...aiResponses]); + } catch (error: any) { console.error("Failed to get AI response:", error.response?.data || error.message || error); const errorResponse: Message = { @@ -64,7 +87,7 @@ const ChatScreen = () => { setMessages(prevMessages => [...prevMessages, errorResponse]); } finally { setIsLoading(false); - // Scroll to bottom after receiving AI message + // Scroll to bottom after receiving AI message(s) setTimeout(() => flatListRef.current?.scrollToEnd({ animated: true }), 100); } // --- End API Call ---