Turn into Itinerary working
This commit is contained in:
parent
1f384c909a
commit
422c39e3e1
64
src/lib/components/TurnIntoItineraryPopup.svelte
Normal file
64
src/lib/components/TurnIntoItineraryPopup.svelte
Normal 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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
190
src/services/openai.ts
Normal 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 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<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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user