Skip to main content

Frontend Interaction

This document describes how the frontend interacts with the accommodation search system through the conversational AI interface.

Search Flow

API Endpoints

POST /api/trip-searches
Content-Type: application/json

{
"trip_step_id": "uuid",
"name": "Recommended",
"category": "ACCOMMODATION"
}

Get Accommodation Options

GET /api/hotels/rubric/{rubric_id}

Returns ranked accommodation options with insights:

{
"options": [
{
"id": "uuid",
"rank": 1,
"total_score": 92.5,
"accommodation": {
"name": "Hilton Paris Opera",
"address": "108 Rue Saint-Lazare",
"star_rating": 4.5,
"guest_rating": 8.7,
"distance_km": 1.2
},
"product": {
"name": "Deluxe King Room",
"price_per_night": 245.00,
"currency": "EUR",
"is_refundable": true
},
"insights": [
{
"type": "POSITIVE",
"message": "Within your budget"
},
{
"type": "POSITIVE",
"message": "Has gym facility"
}
]
}
]
}

Frontend Components

Search Form

interface AccommodationSearchFormProps {
tripStepId: string;
onSubmit: (filters: AccommodationFilters) => Promise<void>;
isLoading: boolean;
}

const AccommodationSearchForm: React.FC<AccommodationSearchFormProps> = ({
tripStepId,
onSubmit,
isLoading
}) => {
// Form with location, dates, guests, amenities
};

Results Display

interface AccommodationResultsProps {
options: AccommodationOption[];
onSelect: (option: AccommodationOption) => void;
}

const AccommodationResults: React.FC<AccommodationResultsProps> = ({
options,
onSelect
}) => {
return (
<div className="grid gap-4">
{options.map((option) => (
<AccommodationCard
key={option.id}
option={option}
onClick={() => onSelect(option)}
/>
))}
</div>
);
};

Accommodation Card

const AccommodationCard: React.FC<{ option: AccommodationOption }> = ({ option }) => {
return (
<div className="flex gap-4 p-4 border rounded-lg">
<img
src={option.accommodation.image_url}
alt={option.accommodation.name}
className="w-48 h-32 object-cover rounded"
/>
<div className="flex-1">
<h3 className="font-semibold">{option.accommodation.name}</h3>
<div className="flex items-center gap-2 text-sm text-gray-600">
<StarRating rating={option.accommodation.star_rating} />
<span></span>
<span>{option.accommodation.distance_km} km from center</span>
</div>
<div className="mt-2">
<span className="text-lg font-bold">
{option.product.currency} {option.product.price_per_night}
</span>
<span className="text-sm text-gray-500"> / night</span>
</div>
<InsightBadges insights={option.insights} />
</div>
</div>
);
};

Insight Display

Insights are color-coded by type:

TypeColorDescription
POSITIVEGreenMeets or exceeds preferences
NEUTRALGrayInformational
NEGATIVERedDoesn't meet preferences
POLICY_VIOLATIONRed with warningViolates company policy

State Management

const AccommodationSearchContext = createContext<AccommodationSearchState | null>(null);

interface AccommodationSearchState {
tripSearch: TripSearch | null;
options: AccommodationOption[];
isSearching: boolean;
selectedOption: AccommodationOption | null;
filters: AccommodationFilters;

updateFilters: (filters: Partial<AccommodationFilters>) => void;
executeSearch: () => Promise<void>;
selectOption: (option: AccommodationOption) => void;
}

Filter Options

Available filters for accommodation search:

interface AccommodationFilters {
// Required
location: string;
checkin_date: string;
checkout_date: string;

// Optional
guests?: number;
rooms?: number;
min_price?: number;
max_price?: number;
min_rating?: number;
star_rating?: number[];
amenities?: string[];
property_type?: string[];
}

Error Handling

try {
await executeSearch();
} catch (error) {
if (error instanceof APIError) {
switch (error.status) {
case 422:
setFieldErrors(error.details);
break;
case 429:
showToast('Rate limited. Please try again.');
break;
case 404:
showToast('No accommodations found for your criteria.');
break;
}
}
}