Frontend Interaction
This document describes how the frontend interacts with the accommodation search system through the conversational AI interface.
Search Flow
API Endpoints
Create Trip Search
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:
| Type | Color | Description |
|---|---|---|
POSITIVE | Green | Meets or exceeds preferences |
NEUTRAL | Gray | Informational |
NEGATIVE | Red | Doesn't meet preferences |
POLICY_VIOLATION | Red with warning | Violates 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;
}
}
}
Related Documentation
- Accommodation Overview — System overview
- Orchestrator — Search coordination