Recommend Places by LLM
This commit is contained in:
parent
13f93623a5
commit
1f384c909a
22
package-lock.json
generated
22
package-lock.json
generated
|
@ -11,6 +11,7 @@
|
||||||
"@googlemaps/js-api-loader": "^1.16.8",
|
"@googlemaps/js-api-loader": "^1.16.8",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"firebase": "^11.9.0",
|
"firebase": "^11.9.0",
|
||||||
|
"openai": "^5.1.1",
|
||||||
"topojson-client": "^3.1.0",
|
"topojson-client": "^3.1.0",
|
||||||
"topojson-server": "^3.0.1"
|
"topojson-server": "^3.0.1"
|
||||||
},
|
},
|
||||||
|
@ -3108,6 +3109,27 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openai": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/openai/-/openai-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-lgIdLqvpLpz8xPUKcEIV6ml+by74mbSBz8zv/AHHebtLn/WdpH4kdXT3/Q5uUKDHg3vHV/z9+G9wZINRX6rkDg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"openai": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ws": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"@googlemaps/js-api-loader": "^1.16.8",
|
"@googlemaps/js-api-loader": "^1.16.8",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"firebase": "^11.9.0",
|
"firebase": "^11.9.0",
|
||||||
|
"openai": "^5.1.1",
|
||||||
"topojson-client": "^3.1.0",
|
"topojson-client": "^3.1.0",
|
||||||
"topojson-server": "^3.0.1"
|
"topojson-server": "^3.0.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,18 @@
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full-gray {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--gray-50);
|
||||||
|
color: var(--gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-gray:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
color: var(--gray-600);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
.blue {
|
.blue {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
background-color: var(--planner-400);
|
background-color: var(--planner-400);
|
||||||
|
|
58
src/lib/components/LoadingOverlay.svelte
Normal file
58
src/lib/components/LoadingOverlay.svelte
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<script>
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
export let show = false;
|
||||||
|
export let message = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<div class="overlay" transition:fade={{ duration: 200 }}>
|
||||||
|
<div class="content" transition:fade={{ duration: 150, delay: 50 }}>
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>{message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid var(--white);
|
||||||
|
border-top: 3px solid transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,15 +2,12 @@
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import TripCard from './TripCard.svelte';
|
import TripCard from './TripCard.svelte';
|
||||||
import { ref, onValue } from 'firebase/database';
|
|
||||||
import { db } from '../../firebase';
|
|
||||||
|
|
||||||
export let showPanel = false;
|
export let showPanel = false;
|
||||||
export let destination = '';
|
export let destination = '';
|
||||||
|
export let pastTrips: any[] = [];
|
||||||
export let onClose = () => {};
|
export let onClose = () => {};
|
||||||
|
|
||||||
let pastTrips: any[] = [];
|
|
||||||
let loading = true;
|
|
||||||
let tripsContainer: HTMLElement;
|
let tripsContainer: HTMLElement;
|
||||||
let showLeftButton = false;
|
let showLeftButton = false;
|
||||||
let showRightButton = true;
|
let showRightButton = true;
|
||||||
|
@ -43,35 +40,6 @@
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (showPanel && destination) {
|
|
||||||
// Fetch past trips for this destination
|
|
||||||
const tripsRef = ref(db, 'trips');
|
|
||||||
onValue(tripsRef, (snapshot) => {
|
|
||||||
const allTrips: any[] = [];
|
|
||||||
snapshot.forEach((childSnapshot) => {
|
|
||||||
const trip = {
|
|
||||||
tid: childSnapshot.key,
|
|
||||||
...childSnapshot.val()
|
|
||||||
};
|
|
||||||
allTrips.push(trip);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get today's date at midnight for comparison
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
// Filter past trips for this destination
|
|
||||||
pastTrips = allTrips
|
|
||||||
.filter(trip => {
|
|
||||||
const endDate = new Date(trip.endDate);
|
|
||||||
return endDate < today && trip.destination.name === destination;
|
|
||||||
})
|
|
||||||
.sort((a, b) => new Date(b.endDate).getTime() - new Date(a.endDate).getTime()); // Most recent first
|
|
||||||
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showPanel}
|
{#if showPanel}
|
||||||
|
@ -87,9 +55,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#if loading}
|
{#if pastTrips.length === 0}
|
||||||
<div class="message">Loading...</div>
|
|
||||||
{:else if pastTrips.length === 0}
|
|
||||||
<div class="message">This is your first trip to {destination}!</div>
|
<div class="message">This is your first trip to {destination}!</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="trips-scroll-container">
|
<div class="trips-scroll-container">
|
||||||
|
|
67
src/lib/components/RecommendationPopup.svelte
Normal file
67
src/lib/components/RecommendationPopup.svelte
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
|
export let showPopup = false;
|
||||||
|
export let destination = '';
|
||||||
|
export let onSelect: (type: 'new' | 'old' | 'mix') => void;
|
||||||
|
export let onCancel: () => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showPopup}
|
||||||
|
<div class="popup-overlay">
|
||||||
|
<div class="popup">
|
||||||
|
<h2>Get Recommendations</h2>
|
||||||
|
<p>You've been to {destination} before. What type of recommendations would you like?</p>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<Button text="New Places" type="single" onClick={() => onSelect('new')} />
|
||||||
|
<Button text="Places I've Been to" type="single" onClick={() => onSelect('old')} />
|
||||||
|
<Button text="Mix of Both" type="single" onClick={() => onSelect('mix')} />
|
||||||
|
<Button text="Cancel" type="full-gray" onClick={onCancel} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popup-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1rem 0 1.5rem;
|
||||||
|
color: var(--gray-600);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
100
src/lib/services/openai.ts
Normal file
100
src/lib/services/openai.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
const OPENAI_API_KEY = import.meta.env.VITE_OPENAI_API_KEY;
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: OPENAI_API_KEY,
|
||||||
|
dangerouslyAllowBrowser: true // might be dangerous
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PlaceRecommendation {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlaceRecommendations(
|
||||||
|
destination: string,
|
||||||
|
currentPlaces: string[],
|
||||||
|
pastPlaces: string[] = [],
|
||||||
|
recommendationType: 'new' | 'old' | 'mix'
|
||||||
|
): Promise<PlaceRecommendation[]> {
|
||||||
|
let prompt = `You are a travel planner expert. I need recommendations for places to visit in ${destination}.
|
||||||
|
|
||||||
|
Current itinerary includes: ${currentPlaces.join(', ')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (pastPlaces.length > 0) {
|
||||||
|
prompt += `From my previous trip, I visited: ${pastPlaces.join(', ')}`;
|
||||||
|
|
||||||
|
// Adjust recommendation type based on user preference
|
||||||
|
let recommendationGuidance = '';
|
||||||
|
switch (recommendationType) {
|
||||||
|
case 'new':
|
||||||
|
recommendationGuidance = 'Please recommend 5 new places I haven\'t visited before.';
|
||||||
|
break;
|
||||||
|
case 'old':
|
||||||
|
recommendationGuidance = 'Please recommend 5 places from my previous visits that are worth revisiting. If there are less than 5 places, recommend all of them.';
|
||||||
|
break;
|
||||||
|
case 'mix':
|
||||||
|
recommendationGuidance = 'Please recommend a mix of 5 places, including both new locations and some worth revisiting from my previous trip.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prompt += recommendationGuidance;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
prompt += `This is my first time going to ${destination}. Please recommend 5 places I should visit.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += `
|
||||||
|
|
||||||
|
Please return your answer as a JSON object with a key "recommendations", containing an array of 5 objects. Each object should have two keys: "name" and "desc".
|
||||||
|
|
||||||
|
Example format:
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
{
|
||||||
|
"name": "Place Name 1",
|
||||||
|
"desc": "Brief description of why to visit Place Name 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Place Name 2",
|
||||||
|
"desc": "Brief description of why to visit Place Name 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log(`prompt: ${prompt}`);
|
||||||
|
try {
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
model: 'gpt-4.1-mini',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: 'You are a knowledgeable travel expert providing place recommendations in JSON format.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}
|
||||||
|
],
|
||||||
|
temperature: 0.7,
|
||||||
|
response_format: { type: "json_object" }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
const content = response.choices[0]?.message?.content;
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('No recommendations received');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`content: ${content}`);
|
||||||
|
|
||||||
|
const parsedContent = JSON.parse(content);
|
||||||
|
return parsedContent.recommendations || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting recommendations:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,8 +7,10 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Loader } from '@googlemaps/js-api-loader';
|
import { Loader } from '@googlemaps/js-api-loader';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { ref, onValue, update } from 'firebase/database';
|
import { ref, onValue, update, get } from 'firebase/database';
|
||||||
import { db } from '../../../firebase';
|
import { db } from '../../../firebase';
|
||||||
|
import { countryMappings } from '$lib/constants/CountryMappings';
|
||||||
|
import { Colors } from '$lib/constants/Colors';
|
||||||
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
||||||
import BottomBar from '$lib/components/BottomBar.svelte';
|
import BottomBar from '$lib/components/BottomBar.svelte';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
@ -16,8 +18,9 @@
|
||||||
import AddPlaces from '$lib/components/AddPlaces.svelte';
|
import AddPlaces from '$lib/components/AddPlaces.svelte';
|
||||||
import PlaceCard from '$lib/components/PlaceCard.svelte';
|
import PlaceCard from '$lib/components/PlaceCard.svelte';
|
||||||
import PastTripsPanel from '$lib/components/PastTripsPanel.svelte';
|
import PastTripsPanel from '$lib/components/PastTripsPanel.svelte';
|
||||||
import { countryMappings } from '$lib/constants/CountryMappings';
|
import { getPlaceRecommendations } from '$lib/services/openai';
|
||||||
import { Colors } from '$lib/constants/Colors';
|
import RecommendationPopup from '$lib/components/RecommendationPopup.svelte';
|
||||||
|
import LoadingOverlay from '$lib/components/LoadingOverlay.svelte';
|
||||||
|
|
||||||
let tripData: any = null;
|
let tripData: any = null;
|
||||||
let tripDates: string[] = [];
|
let tripDates: string[] = [];
|
||||||
|
@ -57,6 +60,73 @@
|
||||||
let map: google.maps.Map | null = null;
|
let map: google.maps.Map | null = null;
|
||||||
let markers: google.maps.marker.AdvancedMarkerElement[] = [];
|
let markers: google.maps.marker.AdvancedMarkerElement[] = [];
|
||||||
let showPastTrips = false;
|
let showPastTrips = false;
|
||||||
|
let showRecommendationPopup = false;
|
||||||
|
let isGeneratingRecommendations = false;
|
||||||
|
|
||||||
|
let pastTripsData: any[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are any past trips to the same destination
|
||||||
|
* and fetch their places data
|
||||||
|
*/
|
||||||
|
async function checkPastTrips(destinationName: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const tripsRef = ref(db, 'trips');
|
||||||
|
const snapshot = await get(tripsRef);
|
||||||
|
if (!snapshot.exists()) return false;
|
||||||
|
|
||||||
|
// Get today's date at midnight for comparison
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Filter past trips for this destination
|
||||||
|
pastTripsData = Object.entries(snapshot.val())
|
||||||
|
.map(([tripId, data]) => ({
|
||||||
|
tid: tripId,
|
||||||
|
...data as any
|
||||||
|
}))
|
||||||
|
.filter(trip => {
|
||||||
|
const endDate = new Date(trip.endDate);
|
||||||
|
return trip.tid !== tid && // not current trip
|
||||||
|
endDate < today && // trip is in the past
|
||||||
|
trip.destination.name === destinationName;
|
||||||
|
})
|
||||||
|
.sort((a, b) => new Date(b.endDate).getTime() - new Date(a.endDate).getTime()); // Most recent first
|
||||||
|
|
||||||
|
return pastTripsData.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking past trips:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all places from past trips to this destination
|
||||||
|
*/
|
||||||
|
function getPastPlaces(): string[] {
|
||||||
|
const places = new Set<string>();
|
||||||
|
|
||||||
|
pastTripsData.forEach(trip => {
|
||||||
|
// Add places from placesToVisit
|
||||||
|
if (trip.placesToVisit) {
|
||||||
|
trip.placesToVisit.forEach((place: any) => places.add(place.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add places from itineraryDate
|
||||||
|
if (trip.itineraryDate) {
|
||||||
|
Object.values(trip.itineraryDate).forEach((dateData: any) => {
|
||||||
|
dateData.placesPlanned?.forEach((place: any) => places.add(place.name));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(places);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When tripData is loaded, check for past trips
|
||||||
|
$: if (tripData?.destination?.name) {
|
||||||
|
checkPastTrips(tripData.destination.name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ISO 3166-1 alpha-2 country code from /constants/CountryMappings.ts
|
* Get the ISO 3166-1 alpha-2 country code from /constants/CountryMappings.ts
|
||||||
|
@ -288,16 +358,106 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
async function handleRecommendPlaces() {
|
||||||
console.log('cancel update');
|
if (!tripData?.destination?.name) return;
|
||||||
|
console.log("recommend places");
|
||||||
|
|
||||||
|
// Check for past trips
|
||||||
|
const hasPastTrips = await checkPastTrips(tripData.destination.name);
|
||||||
|
|
||||||
|
if (hasPastTrips) {
|
||||||
|
console.log("has past trips");
|
||||||
|
showRecommendationPopup = true;
|
||||||
|
} else {
|
||||||
|
// If no past trips, proceed with getting new recommendations
|
||||||
|
await getRecommendations('new');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSave() {
|
async function handleRecommendationSelect(type: 'new' | 'old' | 'mix') {
|
||||||
console.log('save update');
|
showRecommendationPopup = false;
|
||||||
|
await getRecommendations(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRecommendPlaces() {
|
function handleRecommendationCancel() {
|
||||||
console.log(`will give recommendation using OpenAI`);
|
showRecommendationPopup = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRecommendations(recommendationType: 'new' | 'old' | 'mix') {
|
||||||
|
isGeneratingRecommendations = true;
|
||||||
|
try {
|
||||||
|
// Get current places from both placesToVisit and placesPlanned
|
||||||
|
const currentPlaces = [
|
||||||
|
...placesToVisit.map(p => p.name),
|
||||||
|
...Object.values(placesPlanned).flatMap(date =>
|
||||||
|
date.placesPlanned?.map(p => p.name) || []
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get past places from previous trips
|
||||||
|
const pastPlaces = getPastPlaces();
|
||||||
|
|
||||||
|
// Get recommendations from OpenAI
|
||||||
|
const recommendations = await getPlaceRecommendations(
|
||||||
|
tripData.destination.name,
|
||||||
|
currentPlaces,
|
||||||
|
pastPlaces,
|
||||||
|
recommendationType
|
||||||
|
);
|
||||||
|
|
||||||
|
// For each recommendation, search Google Places API for details
|
||||||
|
const placesService = new google.maps.places.PlacesService(map!);
|
||||||
|
|
||||||
|
for (const rec of recommendations) {
|
||||||
|
const request = {
|
||||||
|
query: `${rec.name} ${tripData.destination.name}`,
|
||||||
|
fields: ['name', 'formatted_address', 'photos', 'place_id', 'geometry']
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await new Promise<google.maps.places.PlaceResult[]>((resolve, reject) => {
|
||||||
|
placesService.textSearch(request, (results, status) => {
|
||||||
|
if (status === google.maps.places.PlacesServiceStatus.OK && results) {
|
||||||
|
resolve(results);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Places API error: ${status}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
const place = results[0];
|
||||||
|
const photoUrl = place.photos?.[0]?.getUrl();
|
||||||
|
|
||||||
|
const newPlace = {
|
||||||
|
name: place.name || rec.name,
|
||||||
|
desc: place.formatted_address || '',
|
||||||
|
image: photoUrl || '/placeholder.jpeg',
|
||||||
|
geometry: place.geometry?.location ? {
|
||||||
|
lat: place.geometry.location.lat(),
|
||||||
|
lng: place.geometry.location.lng()
|
||||||
|
} : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to placesToVisit
|
||||||
|
placesToVisit = [...placesToVisit, newPlace];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching place details for ${rec.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the database with new places
|
||||||
|
await update(ref(db, `trips/${tid}`), {
|
||||||
|
placesToVisit
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting recommendations:', error);
|
||||||
|
alert('Failed to get recommendations. Please try again.');
|
||||||
|
} finally {
|
||||||
|
isGeneratingRecommendations = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTurnIntoItinerary() {
|
function handleTurnIntoItinerary() {
|
||||||
|
@ -407,12 +567,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="button-group">
|
|
||||||
<Button text="Cancel" type="gray" onClick={handleCancel}/>
|
|
||||||
<!-- later edit this so button turns blue only when there is changes to the plan -->
|
|
||||||
<Button text="Save" type="blue" onClick={handleSave} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -420,11 +574,24 @@
|
||||||
<div class="map-container" bind:this={mapContainer}></div>
|
<div class="map-container" bind:this={mapContainer}></div>
|
||||||
<BottomBar title="Past Trips" desc="Click to view all past trips to {tripData?.destination?.name}" onClick={handlePastTrip} />
|
<BottomBar title="Past Trips" desc="Click to view all past trips to {tripData?.destination?.name}" onClick={handlePastTrip} />
|
||||||
<PastTripsPanel
|
<PastTripsPanel
|
||||||
showPanel={showPastTrips}
|
showPanel={showPastTrips}
|
||||||
destination={tripData?.destination?.name || ''}
|
destination={tripData?.destination?.name || ''}
|
||||||
onClose={() => showPastTrips = false}
|
pastTrips={pastTripsData}
|
||||||
|
onClose={() => showPastTrips = false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<RecommendationPopup
|
||||||
|
showPopup={showRecommendationPopup}
|
||||||
|
destination={tripData?.destination?.name || ''}
|
||||||
|
onSelect={handleRecommendationSelect}
|
||||||
|
onCancel={handleRecommendationCancel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingOverlay
|
||||||
|
show={isGeneratingRecommendations}
|
||||||
|
message="Generating Recommended Places"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -572,15 +739,4 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
|
||||||
position: sticky;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background-color: var(--white);
|
|
||||||
padding: 1.5rem 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
Loading…
Reference in New Issue
Block a user