diff --git a/ui/src/app/components/service-modal.tsx b/ui/src/app/components/service-modal.tsx index fcf0b32..a0c7a1c 100644 --- a/ui/src/app/components/service-modal.tsx +++ b/ui/src/app/components/service-modal.tsx @@ -20,9 +20,10 @@ import { Grid, GridItem, Link, + Badge, } from '@chakra-ui/react'; -import { FaMapMarkerAlt, FaPhone, FaGlobe, FaClock } from 'react-icons/fa'; -import { Service } from '../types/service'; +import { FaMapMarkerAlt, FaPhone, FaGlobe, FaClock, FaEnvelope } from 'react-icons/fa'; +import { Service, PhoneNumber, Address } from '../types/service'; interface ServiceModalProps { isOpen: boolean; @@ -38,13 +39,24 @@ const ServiceModal: React.FC = ({ isOpen, onClose, service }) const borderColor = useColorModeValue('gray.200', 'gray.600'); const linkColor = useColorModeValue('pink.600', 'pink.300'); - const formatServiceArea = (serviceArea: string | string[] | undefined): string => { - if (Array.isArray(serviceArea)) { - return serviceArea.join(', '); - } else if (typeof serviceArea === 'string') { - return serviceArea; + const formatAddress = (address: Address): string => { + const parts = [ + address.street1, + address.street2, + address.city, + address.province, + address.postal_code, + address.country, + ].filter(Boolean); + return parts.join(', '); + }; + + const formatPhoneNumber = (phone: PhoneNumber): string => { + let formatted = phone.number; + if (phone.extension) { + formatted += ` ext. ${phone.extension}`; } - return 'Not specified'; + return formatted; }; const renderHtml = (html: string) => { @@ -52,7 +64,6 @@ const ServiceModal: React.FC = ({ isOpen, onClose, service }) replace: (domNode: DOMNode) => { if (domNode instanceof Element && domNode.name === 'a' && domNode.attribs) { let href = domNode.attribs.href || ''; - // Ensure the URL has a protocol if (href && !href.startsWith('http://') && !href.startsWith('https://')) { href = `https://${href}`; } @@ -79,33 +90,42 @@ const ServiceModal: React.FC = ({ isOpen, onClose, service }) return parse(html, options); }; - const renderAdditionalInfo = () => { - const excludedKeys = ['id', 'ParentId', 'Score', 'Hours2', 'RecordOwner', 'UniqueIDPriorSystem', 'Latitude', 'Longitude', 'TaxonomyCodes', 'TaxonomyTerm', 'TaxonomyTerms', 'PublicName', 'Description', 'ServiceArea', 'PhoneNumbers', 'Website', 'Hours']; - const additionalInfo = Object.entries(service).filter(([key]) => !excludedKeys.includes(key)); + const renderMetadata = () => { + if (!service.metadata || Object.keys(service.metadata).length === 0) { + return null; + } return ( - {additionalInfo.map(([key, value]) => ( - - - - - {key} - - - - - {typeof value === 'string' ? value : JSON.stringify(value)} - + {Object.entries(service.metadata).map(([key, value]) => { + if (!value) return null; + + return ( + + + + + {key.charAt(0).toUpperCase() + key.slice(1)} + + + + + {Array.isArray(value) + ? value.join(', ') + : typeof value === 'object' + ? JSON.stringify(value) + : String(value)} + + - - - ))} + + ); + })} ); }; @@ -114,41 +134,72 @@ const ServiceModal: React.FC = ({ isOpen, onClose, service }) - {service.PublicName} + {service.name} - {service.Description && ( + {service.description && ( Description - {renderHtml(service.Description)} + {renderHtml(service.description)} )} - {service.ServiceArea && ( + + {service.address && ( - Service Area + Address - {formatServiceArea(service.ServiceArea)} + {formatAddress(service.address)} )} - {service.PhoneNumbers && service.PhoneNumbers.length > 0 && ( + + {service.phone_numbers && service.phone_numbers.length > 0 && ( - Phone + Contact Numbers - {service.PhoneNumbers[0].phone} + + {service.phone_numbers.map((phone, index) => ( + + + {formatPhoneNumber(phone)} + {phone.name && ` (${phone.name})`} + + {phone.type && ( + + {phone.type} + + )} + + ))} + )} - {service.Website && ( + + {service.email && ( + + + + + Email + + + + {service.email} + + + )} + + {service.metadata?.website && ( @@ -157,15 +208,18 @@ const ServiceModal: React.FC = ({ isOpen, onClose, service }) - {service.Website} + {service.metadata.website} )} - {service.Hours && ( + + {service.metadata?.hours && ( @@ -173,15 +227,21 @@ const ServiceModal: React.FC = ({ isOpen, onClose, service }) Hours - {service.Hours} + + {Array.isArray(service.metadata.hours) + ? service.metadata.hours.join(', ') + : service.metadata.hours} + )} + + Additional Information - {renderAdditionalInfo()} + {renderMetadata()} diff --git a/ui/src/app/recommendation/page.tsx b/ui/src/app/recommendation/page.tsx index 6f7580a..13edb00 100644 --- a/ui/src/app/recommendation/page.tsx +++ b/ui/src/app/recommendation/page.tsx @@ -2,14 +2,32 @@ import React, { useState, useEffect, useMemo } from 'react'; import { - Box, Container, Heading, Text, VStack, SimpleGrid, useColorModeValue, - Divider, Badge, Flex, Grid, GridItem, Skeleton, SkeletonText, SkeletonCircle + Box, + Container, + Heading, + Text, + VStack, + SimpleGrid, + useColorModeValue, + Divider, + Badge, + Flex, + Grid, + GridItem, + Skeleton, + SkeletonText, + SkeletonCircle, } from '@chakra-ui/react'; import ServiceCard from '../components/service-card'; import Header from '../components/header'; import Map, { computeViewState, TORONTO_COORDINATES } from '../components/map'; -import { Service, Location } from '../types/service'; -import { useRecommendationStore, Recommendation, Query, RecommendationStore } from '../stores/recommendation-store'; +import { Service, Location, Address } from '../types/service'; +import { + useRecommendationStore, + Recommendation, + Query, + RecommendationStore, +} from '../stores/recommendation-store'; import { useRouter } from 'next/navigation'; import AdditionalQuestions from '../components/additional-questions'; import EmergencyAlert from '../components/emergency-alert'; @@ -17,9 +35,15 @@ import OutOfScopeAlert from '../components/out-of-scope-alert'; import NoServicesFoundAlert from '../components/no-services-found-alert'; const RecommendationPage: React.FC = () => { - const recommendation = useRecommendationStore((state: RecommendationStore) => state.recommendation); - const setRecommendation = useRecommendationStore((state: RecommendationStore) => state.setRecommendation); - const originalQuery = useRecommendationStore((state: RecommendationStore) => state.query); + const recommendation = useRecommendationStore( + (state: RecommendationStore) => state.recommendation + ); + const setRecommendation = useRecommendationStore( + (state: RecommendationStore) => state.setRecommendation + ); + const originalQuery = useRecommendationStore( + (state: RecommendationStore) => state.query + ); const router = useRouter(); const [mapViewState, setMapViewState] = useState(TORONTO_COORDINATES); const [isLoading, setIsLoading] = useState(true); @@ -35,6 +59,18 @@ const RecommendationPage: React.FC = () => { const mapHeight = '400px'; const mapWidth = '100%'; + const formatAddress = (address: Address): string => { + const parts = [ + address.street1, + address.street2, + address.city, + address.province, + address.postal_code, + address.country, + ].filter(Boolean); + return parts.join(', '); + }; + useEffect(() => { if (!recommendation || !originalQuery) { router.replace('/'); @@ -50,7 +86,11 @@ const RecommendationPage: React.FC = () => { } try { - const response = await fetch(`/api/questions?query=${encodeURIComponent(originalQuery.query)}&recommendation=${encodeURIComponent(recommendation.message)}`); + const response = await fetch( + `/api/questions?query=${encodeURIComponent( + originalQuery.query + )}&recommendation=${encodeURIComponent(recommendation.message)}` + ); const data = await response.json(); setAdditionalQuestions(data.questions); setIsLoading(false); @@ -72,7 +112,7 @@ const RecommendationPage: React.FC = () => { query: originalQuery, recommendation: recommendation.message, questions: additionalQuestions, - answers: answers + answers: answers, }; const response = await fetch('/api/refine_recommendations', { @@ -89,7 +129,9 @@ const RecommendationPage: React.FC = () => { const refinedRecommendation: Recommendation = await response.json(); setRecommendation(refinedRecommendation); - updateMapViewState(refinedRecommendation.services); + if (refinedRecommendation.services) { + updateMapViewState(refinedRecommendation.services); + } } catch (error) { console.error('Error refining recommendations:', error); } finally { @@ -99,22 +141,15 @@ const RecommendationPage: React.FC = () => { const updateMapViewState = (services: Service[]) => { if (services && services.length > 0) { - const newMapLocations = services - .filter((service): service is Service & Required> => - typeof service.Latitude === 'number' && - typeof service.Longitude === 'number' && - !isNaN(service.Latitude) && - !isNaN(service.Longitude) - ) - .map(service => ({ - id: service.id, - name: service.PublicName, - latitude: service.Latitude, - longitude: service.Longitude, - description: service.Description || '', - address: service.Address || '', - phone: service.Phone || '', - })); + const newMapLocations = services.map((service) => ({ + id: service.id, + name: service.name, + latitude: service.latitude, + longitude: service.longitude, + description: service.description, + address: formatAddress(service.address), + phone: service.phone_numbers[0]?.number || '', + })); const newViewState = computeViewState(newMapLocations); setMapViewState(newViewState); @@ -124,22 +159,15 @@ const RecommendationPage: React.FC = () => { const mapLocations: Location[] = useMemo(() => { if (!recommendation?.services) return []; - return recommendation.services - .filter((service): service is Service & Required> => - typeof service.Latitude === 'number' && - typeof service.Longitude === 'number' && - !isNaN(service.Latitude) && - !isNaN(service.Longitude) - ) - .map(service => ({ - id: service.id, - name: service.PublicName, - latitude: service.Latitude, - longitude: service.Longitude, - description: service.Description || '', - address: service.Address || '', - phone: service.Phone || '', - })); + return recommendation.services.map((service) => ({ + id: service.id, + name: service.name, + latitude: service.latitude, + longitude: service.longitude, + description: service.description, + address: formatAddress(service.address), + phone: service.phone_numbers[0]?.number || '', + })); }, [recommendation]); useEffect(() => { @@ -152,15 +180,25 @@ const RecommendationPage: React.FC = () => { const renderRecommendationCard = (recommendation: Recommendation | null) => { if (!recommendation?.message) return null; - const [overviewWithLabel, ...reasoningParts] = recommendation.message.split('\n').filter(part => part.trim() !== ''); + const [overviewWithLabel, ...reasoningParts] = recommendation.message + .split('\n') + .filter((part) => part.trim() !== ''); const overview = overviewWithLabel.replace('Overview:', '').trim(); const reasoning = reasoningParts.join('\n').replace('Reasoning:', '').trim(); - const serviceName = recommendation.services[0]?.PublicName || 'Unknown Service'; + const serviceName = recommendation.services?.[0]?.name || 'Unknown Service'; const updatedOverview = `${serviceName}

${overview}`; return ( - + @@ -178,9 +216,11 @@ const RecommendationPage: React.FC = () => { }; const renderRecommendedServices = (services: Service[] | null) => { - const coloredServices = services?.map((service, index) => ({ + if (!services) return null; + + const coloredServices = services.map((service, index) => ({ ...service, - bgColor: index === 0 ? highlightColor : cardBgColor + bgColor: index === 0 ? highlightColor : cardBgColor, })); return ( @@ -191,10 +231,23 @@ const RecommendationPage: React.FC = () => { {isLoading ? ( Array.from({ length: 6 }).map((_, index) => ( - + - + @@ -204,8 +257,12 @@ const RecommendationPage: React.FC = () => { )) ) : ( - coloredServices?.map((service) => ( - + coloredServices.map((service) => ( + )) )} @@ -223,21 +280,41 @@ const RecommendationPage: React.FC = () => { } if (recommendation?.no_services_found) { - return ; + return ( + + ); } return ( <> - + {isLoading ? ( - + - + - + ) : (