From 422c39e3e118b661f7cc98f49c6941b055cee702 Mon Sep 17 00:00:00 2001 From: adeliptr Date: Fri, 6 Jun 2025 20:25:46 +0900 Subject: [PATCH] Turn into Itinerary working --- .../components/TurnIntoItineraryPopup.svelte | 64 ++++++ src/lib/services/openai.ts | 100 --------- src/routes/itinerary/[tid]/+page.svelte | 59 +++++- src/services/openai.ts | 190 ++++++++++++++++++ 4 files changed, 308 insertions(+), 105 deletions(-) create mode 100644 src/lib/components/TurnIntoItineraryPopup.svelte delete mode 100644 src/lib/services/openai.ts create mode 100644 src/services/openai.ts diff --git a/src/lib/components/TurnIntoItineraryPopup.svelte b/src/lib/components/TurnIntoItineraryPopup.svelte new file mode 100644 index 0000000..ed19792 --- /dev/null +++ b/src/lib/components/TurnIntoItineraryPopup.svelte @@ -0,0 +1,64 @@ + + +{#if showPopup} + +{/if} + + \ No newline at end of file diff --git a/src/lib/services/openai.ts b/src/lib/services/openai.ts deleted file mode 100644 index 5586f67..0000000 --- a/src/lib/services/openai.ts +++ /dev/null @@ -1,100 +0,0 @@ -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 { - 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; - } -} \ No newline at end of file diff --git a/src/routes/itinerary/[tid]/+page.svelte b/src/routes/itinerary/[tid]/+page.svelte index 1c61629..8e0d9f0 100644 --- a/src/routes/itinerary/[tid]/+page.svelte +++ b/src/routes/itinerary/[tid]/+page.svelte @@ -18,14 +18,17 @@ import AddPlaces from '$lib/components/AddPlaces.svelte'; import PlaceCard from '$lib/components/PlaceCard.svelte'; import PastTripsPanel from '$lib/components/PastTripsPanel.svelte'; - import { getPlaceRecommendations } from '$lib/services/openai'; + import { getPlaceRecommendations, distributePlacesToItinerary } from '../../../services/openai'; import RecommendationPopup from '$lib/components/RecommendationPopup.svelte'; import LoadingOverlay from '$lib/components/LoadingOverlay.svelte'; + import TurnIntoItineraryPopup from '$lib/components/TurnIntoItineraryPopup.svelte'; let tripData: any = null; let tripDates: string[] = []; let tid: string; let countryCode = 'tw'; // country code to restrict the autocomplete search + let showTurnIntoItineraryPopup = false; + let isDistributingPlaces = false; // the place data structure saved in the database interface Place { @@ -460,8 +463,48 @@ } } - function handleTurnIntoItinerary() { - console.log(`please turn this into itinerary`); + async function handleTurnIntoItinerary() { + showTurnIntoItineraryPopup = true; + } + + async function handleConfirmTurnIntoItinerary() { + showTurnIntoItineraryPopup = false; + isDistributingPlaces = true; + + try { + // Convert dates to DD-MM-YYYY format + const formattedDates = tripDates.map(date => convertDateFormat(date)); + + // Distribute places using OpenAI, passing the existing places + const distribution = await distributePlacesToItinerary( + placesToVisit, + formattedDates, + tripData.destination.name, + placesPlanned + ); + + // Update the database with the new distribution + await update(ref(db, `trips/${tid}/itineraryDate`), distribution); + + // Update local state + placesPlanned = distribution; + + // Clear places to visit + await update(ref(db, `trips/${tid}`), { + placesToVisit: [] + }); + placesToVisit = []; + + } catch (error) { + console.error('Error turning into itinerary:', error); + alert('Failed to distribute places. Please try again.'); + } finally { + isDistributingPlaces = false; + } + } + + function handleCancelTurnIntoItinerary() { + showTurnIntoItineraryPopup = false; } async function handlePlacePlanned(date: string, places: Place[]) { @@ -588,9 +631,15 @@ onCancel={handleRecommendationCancel} /> + + diff --git a/src/services/openai.ts b/src/services/openai.ts new file mode 100644 index 0000000..aea82fe --- /dev/null +++ b/src/services/openai.ts @@ -0,0 +1,190 @@ +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 { + 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; + } +} + +export async function distributePlacesToItinerary( + places: Array<{ name: string; desc?: string; image?: string; geometry?: { lat: number; lng: number } }>, + dates: string[], + destination: string, + existingPlacesPlanned: Record = {} +): Promise> { + if (!OPENAI_API_KEY) { + throw new Error('OpenAI API key is missing'); + } + + const placesData = places.map(place => ({ + name: place.name, + location: place.geometry ? `${place.geometry.lat},${place.geometry.lng}` : 'unknown' + })); + + const prompt = `You are a travel planner helping to distribute ${places.length} places across ${dates.length} days in ${destination}. +Here are the places with their coordinates: ${JSON.stringify(placesData)}. +The available dates are: ${dates.join(', ')}. + +Please distribute these places into a daily itinerary following these rules: +1. Group places that are geographically close to each other on the same day to minimize travel time +2. Consider a realistic number of places per day (typically 2–4 places) +3. If there are more days than needed, it's okay to leave some days empty +4. Return the result as a JSON object where: + - Keys are dates in DD-MM-YYYY format + - Values are objects with a 'placesPlanned' array containing the place names (just the names as strings) + - Each place should be assigned to exactly one day + +The response should be ONLY the JSON object, nothing else.`; + console.log(`prompt: ${prompt}`); + + try { + const response = await openai.chat.completions.create({ + model: 'gpt-4.1-mini', + messages: [ + { + role: 'system', + content: 'You are a helpful travel itinerary planner.' + }, + { + role: 'user', + content: prompt + } + ], + temperature: 0.7, + response_format: { type: "json_object" } + }); + + const content = response.choices[0]?.message?.content; + if (!content) { + throw new Error('No itinerary received from OpenAI.'); + } + + const distribution = JSON.parse(content) as Record; + + // Start with the existing places planned + const result: Record = { ...existingPlacesPlanned }; + + // Add new places while preserving all their properties + for (const [date, dayData] of Object.entries(distribution)) { + // Initialize the date's places array with existing places or empty array + result[date] = { + placesPlanned: [...(result[date]?.placesPlanned || [])] + }; + + // Add the new places with all their properties + const newPlaces = dayData.placesPlanned.map((placeName: string) => { + const originalPlace = places.find(p => p.name === placeName); + if (!originalPlace) return { name: placeName }; + + // Keep all original properties + return { + name: originalPlace.name, + desc: originalPlace.desc, + image: originalPlace.image, + geometry: originalPlace.geometry + }; + }); + + // Append new places to existing ones + result[date].placesPlanned = [...result[date].placesPlanned, ...newPlaces]; + } + + return result; + } catch (error) { + console.error('Error distributing places:', error); + throw error; + } +} \ No newline at end of file