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 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}
|
||||
/>
|
||||
|
||||
<TurnIntoItineraryPopup
|
||||
showPopup={showTurnIntoItineraryPopup}
|
||||
onConfirm={handleConfirmTurnIntoItinerary}
|
||||
onCancel={handleCancelTurnIntoItinerary}
|
||||
/>
|
||||
|
||||
<LoadingOverlay
|
||||
show={isGeneratingRecommendations}
|
||||
message="Generating Recommended Places"
|
||||
show={isGeneratingRecommendations || isDistributingPlaces}
|
||||
message={isDistributingPlaces ? "Distributing Places into Itinerary" : "Generating Recommended Places"}
|
||||
/>
|
||||
</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