Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Content Carousel</title> | |
| <!-- Load Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Configure Tailwind for custom classes and Inter font --> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['Inter', 'sans-serif'], | |
| }, | |
| colors: { | |
| 'primary-dark': '#1e293b', | |
| 'secondary-dark': '#0f172a', | |
| 'next-start': '#3b82f6', // Blue-500 | |
| 'next-end': '#1d4ed8', // Blue-700 | |
| 'prev-start': '#10b981', // Emerald-500 | |
| 'prev-end': '#059669', // Emerald-700 | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* Custom styles for the button effect */ | |
| .btn-base { | |
| transition: all 0.3s ease; | |
| transform: translateY(0); | |
| } | |
| .btn-base:hover { | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| transform: translateY(-2px); | |
| } | |
| .btn-base:active { | |
| transform: translateY(0); | |
| box-shadow: none; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-secondary-dark text-white font-sans min-h-screen flex flex-col items-center justify-center p-4"> | |
| <div class="w-full max-w-lg bg-primary-dark p-8 md:p-12 rounded-2xl shadow-2xl space-y-8"> | |
| <h1 class="text-4xl font-extrabold text-center text-gray-100 mb-6"> | |
| Content Carousel | |
| </h1> | |
| <p class="text-center text-gray-400 mb-8"> | |
| Move the content by clicking Next or Previous. | |
| </p> | |
| <!-- Button Container --> | |
| <div class="flex flex-col sm:flex-row gap-6 justify-center"> | |
| <!-- Previous Button --> | |
| <button id="prev-btn" data-endpoint="/prev" | |
| class="btn-base flex-1 flex items-center justify-center px-8 py-4 text-lg font-semibold rounded-xl | |
| bg-gradient-to-r from-prev-start to-prev-end hover:from-prev-end hover:to-prev-start | |
| shadow-lg shadow-emerald-700/50"> | |
| <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7"></path></svg> | |
| Previous Slide | |
| </button> | |
| <!-- Next Button --> | |
| <button id="next-btn" data-endpoint="/next" | |
| class="btn-base flex-1 flex items-center justify-center px-8 py-4 text-lg font-semibold rounded-xl | |
| bg-gradient-to-r from-next-start to-next-end hover:from-next-end hover:to-next-start | |
| shadow-lg shadow-blue-700/50"> | |
| Next Slide | |
| <svg class="w-5 h-5 ml-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"></path></svg> | |
| </button> | |
| </div> | |
| <!-- Response Display Area --> | |
| <div id="response-display" class="pt-8 text-center min-h-[4rem] text-lg font-medium text-gray-400"> | |
| <!-- Messages will appear here --> | |
| </div> | |
| </div> | |
| <script> | |
| const BASE_URL = 'https://32d5c4f4e919.ngrok-free.app'; | |
| const prevBtn = document.getElementById('prev-btn'); | |
| const nextBtn = document.getElementById('next-btn'); | |
| const responseDisplay = document.getElementById('response-display'); | |
| /** | |
| * Updates the UI display with the given message and style. | |
| * @param {string} message - The text to display. | |
| * @param {string} type - 'success' or 'error'. | |
| */ | |
| function updateDisplay(message, type) { | |
| responseDisplay.textContent = message; | |
| responseDisplay.classList.remove('text-green-400', 'text-red-400', 'text-gray-400'); | |
| if (type === 'success') { | |
| responseDisplay.classList.add('text-green-400'); | |
| } else if (type === 'error') { | |
| responseDisplay.classList.add('text-red-400'); | |
| } else { | |
| responseDisplay.classList.add('text-gray-400'); | |
| } | |
| } | |
| /** | |
| * Handles the API call with exponential backoff for resilience. | |
| */ | |
| async function safeFetch(fullUrl, retries = 3, delay = 1000) { | |
| for (let i = 0; i < retries; i++) { | |
| try { | |
| const response = await fetch(fullUrl); | |
| const contentType = response.headers.get("content-type"); | |
| if (!response.ok) { | |
| const responseText = await response.text(); | |
| throw new Error(`HTTP error ${response.status}: ${responseText.substring(0, 100)}...`); | |
| } | |
| if (!contentType || !contentType.includes("application/json")) { | |
| const responseText = await response.text(); | |
| throw new Error(`Content-Type error! Expected 'application/json' but received: ${contentType || 'None'}. Server body starts with: ${responseText.substring(0, 50)}...`); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error(`Attempt ${i + 1} failed for ${fullUrl}:`, error.message); | |
| if (i < retries - 1) { | |
| await new Promise(resolve => setTimeout(resolve, delay)); | |
| delay *= 2; | |
| } else { | |
| throw new Error(`Failed to complete request to ${fullUrl} after ${retries} attempts. Reason: ${error.message}`); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Main function to handle button click and send request. | |
| */ | |
| async function sendRequest(endpoint) { | |
| const fullUrl = `${BASE_URL}${endpoint}`; | |
| // Disable buttons and show loading state | |
| prevBtn.disabled = true; | |
| nextBtn.disabled = true; | |
| updateDisplay("Connecting to backend...", 'loading'); | |
| console.log(`Sending request to: ${fullUrl}`); | |
| try { | |
| const result = await safeFetch(fullUrl); | |
| // Extract and display the "Statement" from your JSON response | |
| updateDisplay(result.Statement || "Success! JSON received.", 'success'); | |
| console.log(`SUCCESS: Response from ${endpoint}`, result); | |
| } catch (error) { | |
| // Display the detailed error on the screen | |
| updateDisplay(`Connection Error: ${error.message}`, 'error'); | |
| console.error(`ERROR: Failed request to ${endpoint}`, error.message); | |
| } finally { | |
| // Enable buttons regardless of success or failure | |
| prevBtn.disabled = false; | |
| nextBtn.disabled = false; | |
| } | |
| } | |
| // Event listeners for the buttons | |
| prevBtn.addEventListener('click', () => sendRequest(prevBtn.dataset.endpoint)); | |
| nextBtn.addEventListener('click', () => sendRequest(nextBtn.dataset.endpoint)); | |
| // Set initial message | |
| updateDisplay("Ready. Please ensure your FastAPI server is running.", 'loading'); | |
| </script> | |
| </body> | |
| </html> | |