Turn into Itinerary working

This commit is contained in:
adeliptr 2025-06-06 20:25:46 +09:00
parent 1f384c909a
commit 422c39e3e1
4 changed files with 308 additions and 105 deletions

View File

@ -0,0 +1,64 @@
<script lang="ts">
import Button from './Button.svelte';
export let showPopup = false;
export let onConfirm: () => void;
export let onCancel: () => void;
</script>
{#if showPopup}
<div class="popup-overlay">
<div class="popup">
<h2>Turn into Itinerary</h2>
<p>Do you want to assign the places in Places To Visit into your Itinerary?</p>
<div class="buttons">
<Button text="Yes" type="single" onClick={onConfirm} />
<Button text="No" 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>

View File

@ -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<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;
}
}

View File

@ -18,14 +18,17 @@
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 { getPlaceRecommendations } from '$lib/services/openai'; import { getPlaceRecommendations, distributePlacesToItinerary } from '../../../services/openai';
import RecommendationPopup from '$lib/components/RecommendationPopup.svelte'; import RecommendationPopup from '$lib/components/RecommendationPopup.svelte';
import LoadingOverlay from '$lib/components/LoadingOverlay.svelte'; import LoadingOverlay from '$lib/components/LoadingOverlay.svelte';
import TurnIntoItineraryPopup from '$lib/components/TurnIntoItineraryPopup.svelte';
let tripData: any = null; let tripData: any = null;
let tripDates: string[] = []; let tripDates: string[] = [];
let tid: string; let tid: string;
let countryCode = 'tw'; // country code to restrict the autocomplete search let countryCode = 'tw'; // country code to restrict the autocomplete search
let showTurnIntoItineraryPopup = false;
let isDistributingPlaces = false;
// the place data structure saved in the database // the place data structure saved in the database
interface Place { interface Place {
@ -460,8 +463,48 @@
} }
} }
function handleTurnIntoItinerary() { async function handleTurnIntoItinerary() {
console.log(`please turn this into itinerary`); 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[]) { async function handlePlacePlanned(date: string, places: Place[]) {
@ -588,9 +631,15 @@
onCancel={handleRecommendationCancel} onCancel={handleRecommendationCancel}
/> />
<TurnIntoItineraryPopup
showPopup={showTurnIntoItineraryPopup}
onConfirm={handleConfirmTurnIntoItinerary}
onCancel={handleCancelTurnIntoItinerary}
/>
<LoadingOverlay <LoadingOverlay
show={isGeneratingRecommendations} show={isGeneratingRecommendations || isDistributingPlaces}
message="Generating Recommended Places" message={isDistributingPlaces ? "Distributing Places into Itinerary" : "Generating Recommended Places"}
/> />
</main> </main>

190
src/services/openai.ts Normal file
View File

@ -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<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;
}
}
export async function distributePlacesToItinerary(
places: Array<{ name: string; desc?: string; image?: string; geometry?: { lat: number; lng: number } }>,
dates: string[],
destination: string,
existingPlacesPlanned: Record<string, { placesPlanned: any[] }> = {}
): Promise<Record<string, { placesPlanned: any[] }>> {
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 24 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<string, { placesPlanned: string[] }>;
// Start with the existing places planned
const result: Record<string, { placesPlanned: any[] }> = { ...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;
}
}