From 5ad904b26542f02aaaf44d123a1ab170211e547d Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 2 Dec 2025 15:20:58 +0000 Subject: [PATCH] Auto commit: 2025-12-02T15:20:58.075Z --- api/index.php | 169 +++++++++++++-------- assets/css/custom.css | 54 +++++++ assets/js/main.js | 342 ++++++++++++++++++++++++------------------ db/migrate.php | 21 +++ index.php | 20 +-- saved.php | 177 ++++++++++++++++++++++ 6 files changed, 560 insertions(+), 223 deletions(-) create mode 100644 db/migrate.php create mode 100644 saved.php diff --git a/api/index.php b/api/index.php index 37f644a..bb3a603 100644 --- a/api/index.php +++ b/api/index.php @@ -3,80 +3,129 @@ ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); -session_start(); - header('Content-Type: application/json'); require_once __DIR__ . '/../ai/LocalAIApi.php'; require_once __DIR__ . '/../includes/pexels.php'; +require_once __DIR__ . '/../db/config.php'; -$requestBody = file_get_contents('php://input'); -$data = json_decode($requestBody, true); -$query = $data['query'] ?? ''; -$persona = $data['persona'] ?? 'travel_agent'; +$action = $_GET['action'] ?? 'get_suggestion'; -if (empty($query)) { - echo json_encode(['error' => 'Query is empty']); - exit; +switch ($action) { + case 'get_suggestion': + handle_get_suggestion(); + break; + case 'save_suggestion': + handle_save_suggestion(); + break; + default: + echo json_encode(['error' => 'Invalid action']); + break; } -$conversation = $_SESSION['conversation'] ?? []; -$conversation[] = ['role' => 'user', 'content' => $query]; +function handle_get_suggestion() { + $requestBody = file_get_contents('php://input'); + $data = json_decode($requestBody, true); -$persona_prompts = [ - 'travel_agent' => 'You are a helpful travel agent specializing in Poland.', - 'historian' => 'You are a historian specializing in Polish history. Your responses should be informative, detailed, and focus on the historical significance of places and events.', - 'foodie' => 'You are a food critic and blogger with a passion for Polish cuisine. Your responses should be enthusiastic, descriptive, and focus on food, restaurants, and culinary experiences.', - 'adventurer' => 'You are an adventure travel guide who loves exploring the wild side of Poland. Your responses should be exciting, energetic, and focus on outdoor activities, hiking, and unique experiences.' -]; + $query = $data['query'] ?? ''; + $persona = $data['persona'] ?? 'travel_agent'; + $conversation = $data['conversation'] ?? []; -$system_prompt = $persona_prompts[$persona] . ' The user is looking for: ' . $query . '. Provide a travel suggestion with a title, a short description, a 3-day itinerary, and the latitude and longitude of the location. Your response must be in JSON format, like this: {"title": "...", "description": "...", "itinerary": [{"day": 1, "activities": ["...", "..."]}, {"day": 2, "activities": ["...", "..."]}, {"day": 3, "activities": ["...", "..."]}], "location": {"lat": ..., "lng": ...}}'; + if (empty($query)) { + echo json_encode(['error' => 'Query is empty']); + exit; + } -$messages = array_merge([['role' => 'system', 'content' => $system_prompt]], $conversation); + $conversation[] = ['role' => 'user', 'content' => $query]; -$resp = LocalAIApi::createResponse( - [ - 'input' => $messages - ] -); + $persona_prompts = [ + 'travel_agent' => 'You are a helpful travel agent specializing in Poland.', + 'historian' => 'You are a historian specializing in Polish history. Your responses should be informative, detailed, and focus on the historical significance of places and events.', + 'foodie' => 'You are a food critic and blogger with a passion for Polish cuisine. Your responses should be enthusiastic, descriptive, and focus on food, restaurants, and culinary experiences.', + 'adventurer' => 'You are an adventure travel guide who loves exploring the wild side of Poland. Your responses should be exciting, energetic, and focus on outdoor activities, hiking, and unique experiences.' + ]; -if (!empty($resp['success'])) { - $text = LocalAIApi::extractText($resp); - $aiResponse = json_decode($text, true); + $system_prompt = $persona_prompts[$persona] . ' Provide a travel suggestion with a title, a short description, a 3-day itinerary, and the latitude and longitude of the location. Your response must be in JSON format, like this: {"title": "...", "description": "...", "itinerary": [{"day": 1, "activities": ["...", "..."]}, {"day": 2, "activities": ["...", "..."]}, {"day": 3, "activities": ["...", "..."]}], "location": {"lat": ..., "lng": ...}}. The user is on a conversation, so answer the last query based on the context of the conversation.'; - if (json_last_error() !== JSON_ERROR_NONE) { - // If the response is not a valid JSON, we try to extract the title and description manually - // This is a fallback mechanism - $title = "AI Generated Response"; - $description = $text; - $itinerary = []; + $messages = array_merge([['role' => 'system', 'content' => $system_prompt]], $conversation); + + $resp = LocalAIApi::createResponse( + [ + 'input' => $messages + ] + ); + + if (!empty($resp['success'])) { + $text = LocalAIApi::extractText($resp); + $aiResponse = json_decode($text, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + preg_match('/"title":\s*"(.*?)"/', $text, $title_matches); + preg_match('/"description":\s*"(.*?)"/', $text, $description_matches); + $title = $title_matches[1] ?? 'AI Generated Response'; + $description = $description_matches[1] ?? $text; + $itinerary = []; + $location = null; + } else { + $title = $aiResponse['title'] ?? 'AI Generated Response'; + $description = $aiResponse['description'] ?? 'No description available.'; + $itinerary = $aiResponse['itinerary'] ?? []; + $location = $aiResponse['location'] ?? null; + } + + $pexelsUrl = 'https://api.pexels.com/v1/search?query=' . urlencode($title) . '&orientation=landscape&per_page=1&page=1'; + $pexelsData = pexels_get($pexelsUrl); + + $imageUrl = 'https://images.pexels.com/photos/1699030/pexels-photo-1699030.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'; + if ($pexelsData && !empty($pexelsData['photos'])) { + $photo = $pexelsData['photos'][0]; + $imageUrl = $photo['src']['large2x'] ?? ($photo['src']['large'] ?? $photo['src']['original']); + } + + echo json_encode([ + 'title' => $title, + 'description' => $description, + 'itinerary' => $itinerary, + 'location' => $location, + 'image' => $imageUrl + ]); } else { - $title = $aiResponse['title'] ?? 'AI Generated Response'; - $description = $aiResponse['description'] ?? 'No description available.'; - $itinerary = $aiResponse['itinerary'] ?? []; - $location = $aiResponse['location'] ?? null; + error_log('AI error: ' . ($resp['error'] ?? 'unknown')); + echo json_encode(['error' => 'Failed to get AI response']); + } +} + +function handle_save_suggestion() { + $requestBody = file_get_contents('php://input'); + $data = json_decode($requestBody, true); + + $title = $data['title'] ?? null; + $description = $data['description'] ?? null; + $itinerary = $data['itinerary'] ?? null; + $location = $data['location'] ?? null; + $image_url = $data['image'] ?? null; + + if (empty($title)) { + echo json_encode(['error' => 'Title is required to save a suggestion.']); + exit; + } + + try { + $pdo = db(); + $stmt = $pdo->prepare(" + INSERT INTO saved_suggestions (title, description, itinerary, location, image_url) + VALUES (:title, :description, :itinerary, :location, :image_url) + "); + $stmt->execute([ + ':title' => $title, + ':description' => $description, + ':itinerary' => json_encode($itinerary), + ':location' => json_encode($location), + ':image_url' => $image_url + ]); + echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]); + } catch (PDOException $e) { + error_log('DB error: ' . $e->getMessage()); + echo json_encode(['error' => 'Failed to save suggestion to the database.']); } - - $conversation[] = ['role' => 'assistant', 'content' => $text]; - $_SESSION['conversation'] = $conversation; - - $pexelsUrl = 'https://api.pexels.com/v1/search?query=' . urlencode($title) . '&orientation=landscape&per_page=1&page=1'; - $pexelsData = pexels_get($pexelsUrl); - - $imageUrl = 'https://images.pexels.com/photos/1699030/pexels-photo-1699030.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'; // Default image - if ($pexelsData && !empty($pexelsData['photos'])) { - $photo = $pexelsData['photos'][0]; - $imageUrl = $photo['src']['large2x'] ?? ($photo['src']['large'] ?? $photo['src']['original']); - } - - echo json_encode([ - 'title' => $title, - 'description' => $description, - 'itinerary' => $itinerary, - 'location' => $location, - 'image' => $imageUrl - ]); -} else { - error_log('AI error: ' . ($resp['error'] ?? 'unknown')); - echo json_encode(['error' => 'Failed to get AI response']); } diff --git a/assets/css/custom.css b/assets/css/custom.css index 9ec8986..465c43b 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -68,3 +68,57 @@ body { .spinner-border { color: #DB2777 !important; } + +#chat-container { + max-height: 600px; + overflow-y: auto; + padding: 1rem; + background-color: #fff; + border-radius: 1rem; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +.message-wrapper { + display: flex; + margin-bottom: 1rem; +} + +.user-wrapper { + justify-content: flex-end; +} + +.assistant-wrapper { + justify-content: flex-start; +} + +.message-bubble { + max-width: 80%; + padding: 1rem; + border-radius: 1rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.user-bubble { + background-color: #DB2777; + color: white; + border-top-right-radius: 0; +} + +.assistant-bubble { + background-color: #f1f0f0; + border-top-left-radius: 0; +} + +.assistant-bubble .card-body { + padding: 0; +} + +.assistant-bubble img { + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; + max-width: 100%; +} + +.itinerary-section { + margin-top: 1rem; +} diff --git a/assets/js/main.js b/assets/js/main.js index bab2a2b..16221cf 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,23 +1,18 @@ -document.getElementById('suggestion-form').addEventListener('submit', function(e) { - e.preventDefault(); - const query = document.getElementById('query').value; - const persona = document.getElementById('persona').value; - const suggestionCard = document.getElementById('suggestion-card'); - const loading = document.getElementById('loading'); + +document.addEventListener('DOMContentLoaded', () => { + const suggestionForm = document.getElementById('suggestion-form'); + const queryInput = document.getElementById('query'); + const personaSelect = document.getElementById('persona'); + const loadingIndicator = document.getElementById('loading'); const errorMessage = document.getElementById('error-message'); const errorText = document.getElementById('error-text'); - const suggestionImage = document.getElementById('suggestion-image'); - const suggestionTitle = document.getElementById('suggestion-title'); - const suggestionText = document.getElementById('suggestion-text'); + const chatContainer = document.getElementById('chat-container'); + const luckyButton = document.getElementById('lucky-button'); const funFact = document.getElementById('fun-fact'); - const historySection = document.getElementById('history-section'); - const historyList = document.getElementById('history-list'); - const itinerarySection = document.getElementById('itinerary-section'); - const itineraryList = document.getElementById('itinerary-list'); - const mapElement = document.getElementById('map'); - const suggestionHistory = []; + let conversation = []; let map; + let funFactInterval; const funFacts = [ "Poland is home to the world's largest castle, Malbork Castle.", @@ -30,156 +25,213 @@ document.getElementById('suggestion-form').addEventListener('submit', function(e "Poland's Białowieża Forest is one of the last and largest remaining parts of the immense primeval forest that once stretched across the European Plain." ]; - let funFactInterval; + suggestionForm.addEventListener('submit', function(e) { + e.preventDefault(); + const query = queryInput.value.trim(); + if (!query) return; - suggestionCard.style.display = 'none'; - errorMessage.style.display = 'none'; - loading.style.display = 'block'; - mapElement.style.display = 'none'; + const persona = personaSelect.value; + const userMessage = { role: 'user', content: query }; + conversation.push(userMessage); + + appendMessage('user', { content: query }); + queryInput.value = ''; + + fetchSuggestions(persona); + }); + + luckyButton.addEventListener('click', function() { + const queries = [ + "a hidden gem in Warsaw", + "the best pierogi in Krakow", + "a beautiful beach on the Baltic coast", + "a challenging hike in the Tatra Mountains", + "a quirky museum in Gdansk" + ]; + const randomQuery = queries[Math.floor(Math.random() * queries.length)]; + queryInput.value = randomQuery; + suggestionForm.dispatchEvent(new Event('submit')); + }); function showRandomFunFact() { const randomIndex = Math.floor(Math.random() * funFacts.length); funFact.textContent = funFacts[randomIndex]; } - showRandomFunFact(); - funFactInterval = setInterval(showRandomFunFact, 3000); + function startLoading() { + loadingIndicator.style.display = 'block'; + showRandomFunFact(); + funFactInterval = setInterval(showRandomFunFact, 3000); + } + + function stopLoading() { + loadingIndicator.style.display = 'none'; + clearInterval(funFactInterval); + } function showError(message) { errorText.textContent = message; errorMessage.style.display = 'block'; - loading.style.display = 'none'; - clearInterval(funFactInterval); } - function updateHistory() { - historyList.innerHTML = ''; - suggestionHistory.slice(0, 5).forEach(item => { - const a = document.createElement('a'); - a.href = '#'; - a.className = 'list-group-item list-group-item-action'; - a.textContent = item.title; - a.onclick = (e) => { - e.preventDefault(); - suggestionImage.src = item.image; - suggestionTitle.textContent = item.title; - suggestionText.textContent = item.description; - - if (item.itinerary) { - itineraryList.innerHTML = ''; - item.itinerary.forEach(day => { - const li = document.createElement('li'); - li.className = 'list-group-item'; - li.innerHTML = `Day ${day.day}: ${day.activities.join(', ')}`; - itineraryList.appendChild(li); - }); - itinerarySection.style.display = 'block'; - } else { - itinerarySection.style.display = 'none'; - } - - if (item.location && item.location.lat && item.location.lng) { - mapElement.style.display = 'block'; - if (!map) { - map = L.map('map').setView([item.location.lat, item.location.lng], 13); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(map); - } else { - map.setView([item.location.lat, item.location.lng], 13); - } - L.marker([item.location.lat, item.location.lng]).addTo(map) - .bindPopup(item.title) - .openPopup(); - } else { - mapElement.style.display = 'none'; - } - - suggestionCard.style.display = 'block'; - }; - historyList.appendChild(a); - }); - historySection.style.display = suggestionHistory.length > 0 ? 'block' : 'none'; + function hideError() { + errorMessage.style.display = 'none'; } - fetch('api/index.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ query: query, persona: persona }) - }) - .then(response => response.json()) - .then(data => { - clearInterval(funFactInterval); - if (data.error) { - showError(data.error); - return; - } + function appendMessage(role, data) { + const messageWrapper = document.createElement('div'); + messageWrapper.classList.add('message-wrapper', `${role}-wrapper`); - suggestionImage.src = data.image; - suggestionTitle.textContent = data.title; - suggestionText.textContent = data.description; + const messageBubble = document.createElement('div'); + messageBubble.classList.add('message-bubble', `${role}-bubble`); - if (data.itinerary) { - itineraryList.innerHTML = ''; - data.itinerary.forEach(day => { - const li = document.createElement('li'); - li.className = 'list-group-item'; - li.innerHTML = `Day ${day.day}: ${day.activities.join(', ')}`; - itineraryList.appendChild(li); - }); - itinerarySection.style.display = 'block'; + if (role === 'user') { + messageBubble.textContent = data.content; } else { - itinerarySection.style.display = 'none'; - } - - if (data.location && data.location.lat && data.location.lng) { - mapElement.style.display = 'block'; - if (!map) { - map = L.map('map').setView([data.location.lat, data.location.lng], 13); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(map); - } else { - map.setView([data.location.lat, data.location.lng], 13); + if (data.image) { + const img = document.createElement('img'); + img.src = data.image; + img.classList.add('card-img-top'); + messageBubble.appendChild(img); } - L.marker([data.location.lat, data.location.lng]).addTo(map) - .bindPopup(data.title) - .openPopup(); - } else { - mapElement.style.display = 'none'; + + const cardBody = document.createElement('div'); + cardBody.classList.add('card-body'); + + if (data.title) { + const title = document.createElement('h5'); + title.classList.add('card-title'); + title.textContent = data.title; + cardBody.appendChild(title); + } + + if (data.description) { + const description = document.createElement('p'); + description.classList.add('card-text'); + description.innerHTML = data.description; // Using innerHTML to render line breaks + cardBody.appendChild(description); + } + + if (data.itinerary && data.itinerary.length > 0) { + const itinerarySection = document.createElement('div'); + itinerarySection.classList.add('itinerary-section'); + const itineraryTitle = document.createElement('h6'); + itineraryTitle.textContent = 'Itinerary'; + itinerarySection.appendChild(itineraryTitle); + + const itineraryList = document.createElement('ul'); + itineraryList.classList.add('list-group'); + data.itinerary.forEach(day => { + const li = document.createElement('li'); + li.classList.add('list-group-item'); + li.innerHTML = `Day ${day.day}: ${day.activities.join(', ')}`; + itineraryList.appendChild(li); + }); + itinerarySection.appendChild(itineraryList); + cardBody.appendChild(itinerarySection); + } + if (data.location && data.location.lat && data.location.lng) { + const mapId = `map-${Date.now()}`; + const mapDiv = document.createElement('div'); + mapDiv.id = mapId; + mapDiv.style.height = '300px'; + mapDiv.classList.add('mt-3'); + cardBody.appendChild(mapDiv); + + // Use a timeout to ensure the div is in the DOM before initializing the map + setTimeout(() => { + const map = L.map(mapId).setView([data.location.lat, data.location.lng], 13); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(map); + L.marker([data.location.lat, data.location.lng]).addTo(map) + .bindPopup(data.title || 'Location') + .openPopup(); + }, 100); + } + + + messageBubble.appendChild(cardBody); + + const saveButton = document.createElement('button'); + saveButton.textContent = 'Save'; + saveButton.classList.add('btn', 'btn-sm', 'btn-outline-primary', 'mt-2'); + saveButton.addEventListener('click', () => saveSuggestion(data)); + cardBody.appendChild(saveButton); + } - suggestionHistory.unshift(data); - updateHistory(); + messageWrapper.appendChild(messageBubble); + chatContainer.appendChild(messageWrapper); + chatContainer.scrollTop = chatContainer.scrollHeight; + } - loading.style.display = 'none'; - suggestionCard.style.display = 'block'; - }) - .catch(error => { - console.error('Error:', error); - showError('An error occurred while fetching the suggestion.'); - }); + function saveSuggestion(suggestionData) { + fetch('api/index.php?action=save_suggestion', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(suggestionData) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Suggestion saved!'); + } else { + showError(data.error || 'Could not save suggestion.'); + } + }) + .catch(error => { + console.error('Error:', error); + showError('An error occurred while saving the suggestion.'); + }); + } + + function fetchSuggestions(persona) { + startLoading(); + hideError(); + + fetch('api/index.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query: conversation[conversation.length - 1].content, + persona: persona, + conversation: conversation.slice(0, -1) // Send previous conversation + }) + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + stopLoading(); + if (data.error) { + showError(data.error); + // Do not add error to conversation history + return; + } + + const assistantMessage = { role: 'assistant', content: data }; + conversation.push(assistantMessage); + appendMessage('assistant', data); + }) + .catch(error => { + console.error('Error:', error); + stopLoading(); + showError('An error occurred while fetching the suggestion. Please try again.'); + }); + } + + const errorAlert = document.getElementById('error-message'); + if (errorAlert) { + errorAlert.querySelector('.btn-close').addEventListener('click', function () { + hideError(); + }); + } }); - -document.getElementById('lucky-button').addEventListener('click', function() { - const queries = [ - "a hidden gem in Warsaw", - "the best pierogi in Krakow", - "a beautiful beach on the Baltic coast", - "a challenging hike in the Tatra Mountains", - "a quirky museum in Gdansk" - ]; - const randomQuery = queries[Math.floor(Math.random() * queries.length)]; - document.getElementById('query').value = randomQuery; - document.getElementById('suggestion-form').dispatchEvent(new Event('submit')); -}); - -// Optional: Add event listener to close the error message -const errorAlert = document.getElementById('error-message'); -if (errorAlert) { - errorAlert.querySelector('.btn-close').addEventListener('click', function () { - errorAlert.style.display = 'none'; - }); -} \ No newline at end of file diff --git a/db/migrate.php b/db/migrate.php new file mode 100644 index 0000000..fbb15f8 --- /dev/null +++ b/db/migrate.php @@ -0,0 +1,21 @@ +exec(" + CREATE TABLE IF NOT EXISTS saved_suggestions ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + itinerary JSON, + location JSON, + image_url VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + "); + echo "Migration successful!\n"; +} catch (PDOException $e) { + die("Migration failed: " . $e->getMessage() . "\n"); +} + diff --git a/index.php b/index.php index f09f5cb..11db58b 100644 --- a/index.php +++ b/index.php @@ -11,6 +11,7 @@

Your Next Adventure Awaits

Discover Poland with AI.

+ View Saved Suggestions
@@ -54,24 +55,7 @@

Thinking...

-
- Suggestion Image -
-
-

- -
-
- - - - +
diff --git a/saved.php b/saved.php new file mode 100644 index 0000000..fea9841 --- /dev/null +++ b/saved.php @@ -0,0 +1,177 @@ +prepare("SELECT * FROM saved_suggestions WHERE id = ?"); + $stmt->execute([$_GET['id']]); + $single_suggestion = $stmt->fetch(); +} + +$stmt = $pdo->query("SELECT * FROM saved_suggestions ORDER BY created_at DESC"); +$suggestions = $stmt->fetchAll(); + +?> + + + + + + Saved Suggestions + + + + + + +
+
+

+

+ Home +
+
+ +
+
+ +
+
+ <?php echo htmlspecialchars($single_suggestion['title']); ?> +
+
+

+ + +
+
Itinerary
+
    + +
  • + Day : +
  • + +
+
+ + + +
+ +
+
+
+ +
+

You haven't saved any suggestions yet.

+ Find a new adventure +
+ + $suggestion): ?> +
+
+ <?php echo htmlspecialchars($suggestion['title']); ?> +
+
+

+ + +
+
Itinerary
+
    + +
  • + Day : +
  • + +
+
+ + + +
+ + + +
+
+
+ + +
+
+ + + + + +