aparatos de gimnasio maquinas de gimnasio
Diapositiva1 reseñas
Diapositiva2 reseñas
Diapositiva3 reseñas
Diapositiva4 reseñas
Diapositiva5 reseñas
Diapositiva6 reseñas
Diapositiva7 reseñas
Diapositiva8 reseñas
Diapositiva9 reseñas
Diapositiva10 reseñas
previous arrow
next arrow

Maquinas de gimnasio: los mejores aparatos de gimnasio

Carrito de compra
CARRITO DE COMPRAS0
No hay productos en el carrito!
0
Scroll al inicio
// --- Self-executing function to encapsulate the entire widget --- (() => { // Wait for the main document to be fully loaded before running the script. document.addEventListener('DOMContentLoaded', () => { // 1. CREATE THE HOST ELEMENT AND ATTACH SHADOW DOM // This is the container on your main page where the widget will live. const host = document.createElement('div'); host.id = 'falcon-chat-host'; document.body.appendChild(host); // Attach a shadow root, which isolates the widget's HTML and CSS. const shadowRoot = host.attachShadow({ mode: 'open' }); // 2. DEFINE THE WIDGET'S CSS AND HTML // All styles are defined here and will only apply inside the shadow DOM. const widgetCSS = ` :host { /* Ensures the widget itself doesn't have strange default styles */ all: initial; } /* --- Fonts and Base Styles --- */ .font-inter { font-family: 'Inter', sans-serif; } /* --- Custom Scrollbar --- */ #chat-messages::-webkit-scrollbar { width: 8px; } #chat-messages::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px; } #chat-messages::-webkit-scrollbar-thumb { background: #888; border-radius: 10px; } #chat-messages::-webkit-scrollbar-thumb:hover { background: #555; } /* --- Animations --- */ .chat-window-enter { opacity: 0; transform: translateY(20px) scale(0.95); transition: opacity 0.3s ease-out, transform 0.3s ease-out; } .chat-window-enter-active { opacity: 1; transform: translateY(0) scale(1); } .chat-window-leave { opacity: 1; transform: translateY(0) scale(1); transition: opacity 0.2s ease-in, transform 0.2s ease-in; } .chat-window-leave-active { opacity: 0; transform: translateY(20px) scale(0.95); } /* --- Loading Pulse Animation --- */ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } `; // The complete HTML structure for the chat widget. const widgetHTML = `
`; // 3. INJECT RESOURCES INTO THE SHADOW DOM // This adds the necessary external stylesheets and scripts inside the isolated widget. const fontLink = document.createElement('link'); fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'; fontLink.rel = 'stylesheet'; shadowRoot.appendChild(fontLink); const tailwindScript = document.createElement('script'); tailwindScript.src = 'https://cdn.tailwindcss.com'; shadowRoot.appendChild(tailwindScript); const styleSheet = document.createElement('style'); styleSheet.textContent = widgetCSS; shadowRoot.appendChild(styleSheet); const htmlWrapper = document.createElement('div'); htmlWrapper.innerHTML = widgetHTML; shadowRoot.appendChild(htmlWrapper); // 4. WIDGET LOGIC (SCOPED TO THE SHADOW DOM) // All event listeners and functions will now only find elements inside the shadow root. const chatWindow = shadowRoot.querySelector('#chat-window'); const chatToggle = shadowRoot.querySelector('#chat-toggle'); const closeChat = shadowRoot.querySelector('#close-chat'); const chatMessages = shadowRoot.querySelector('#chat-messages'); const chatForm = shadowRoot.querySelector('#chat-form'); const chatInput = shadowRoot.querySelector('#chat-input'); const sendButton = shadowRoot.querySelector('#send-button'); const toggleChatWindow = () => { const isHidden = chatWindow.classList.contains('hidden'); if (isHidden) { chatWindow.classList.remove('hidden', 'chat-window-leave-active'); chatWindow.classList.add('flex', 'chat-window-enter'); setTimeout(() => chatWindow.classList.add('chat-window-enter-active'), 10); chatToggle.classList.add('hidden'); } else { chatWindow.classList.remove('chat-window-enter-active'); chatWindow.classList.add('chat-window-leave'); setTimeout(() => { chatWindow.classList.add('chat-window-leave-active', 'hidden'); chatWindow.classList.remove('chat-window-leave', 'flex'); }, 200); chatToggle.classList.remove('hidden'); } }; chatToggle.addEventListener('click', toggleChatWindow); closeChat.addEventListener('click', toggleChatWindow); chatForm.addEventListener('submit', async (e) => { e.preventDefault(); const userMessage = chatInput.value.trim(); if (!userMessage) return; addMessage(userMessage, 'user'); chatInput.value = ''; setFormDisabled(true); showLoadingIndicator(); try { const aiResponse = await getAiResponseWithRetry(userMessage); removeLoadingIndicator(); addMessage(aiResponse.text, 'ai', aiResponse.sources); } catch (error) { removeLoadingIndicator(); console.error('Error fetching AI response:', error); addMessage('Lo siento, un error inesperado ocurrió. Por favor, intenta de nuevo más tarde.', 'ai'); } finally { setFormDisabled(false); } }); const setFormDisabled = (disabled) => { chatInput.disabled = disabled; sendButton.disabled = disabled; if (!disabled) chatInput.focus(); }; const addMessage = (text, sender, sources = []) => { const messageContainer = document.createElement('div'); messageContainer.className = `flex mb-4 ${sender === 'user' ? 'justify-end' : 'justify-start'}`; const tempDiv = document.createElement('div'); tempDiv.innerText = text; const sanitizedText = tempDiv.innerHTML.replace(/\n/g, '
'); let sourcesHtml = ''; if (sources.length > 0) { sourcesHtml = '
Fuentes:
'; } messageContainer.innerHTML = `

${sanitizedText}

${sourcesHtml}
`; chatMessages.appendChild(messageContainer); chatMessages.scrollTop = chatMessages.scrollHeight; }; const showLoadingIndicator = () => { const loadingElement = document.createElement('div'); loadingElement.id = 'loading-indicator'; loadingElement.className = 'flex justify-start mb-4'; loadingElement.innerHTML = `
`; chatMessages.appendChild(loadingElement); chatMessages.scrollTop = chatMessages.scrollHeight; }; const removeLoadingIndicator = () => { shadowRoot.getElementById('loading-indicator')?.remove(); }; const getAiResponseWithRetry = async (userQuery, retries = 4) => { let delay = 1000; for (let i = 0; i <= retries; i++) { try { return await getAiResponse(userQuery); } catch (error) { if (error.status === 429 && i < retries) { await new Promise(resolve => setTimeout(resolve, delay)); delay *= 2; } else { throw error; } } } }; const getAiResponse = async (userQuery) => { const systemPrompt = `Eres un asistente de ventas experto para "Falcon Fitness". Tu única fuente de información es el sitio web www.falconfitness.mx. Tu objetivo es responder las preguntas de los usuarios sobre los productos que se encuentran EXCLUSIVAMENTE en ese sitio web. Reglas estrictas: 1. Basa TODAS tus respuestas únicamente en la información encontrada en www.falconfitness.mx. No uses conocimiento general. 2. Si la respuesta no se encuentra en el sitio web, responde amablemente: "No encontré información sobre eso en www.falconfitness.mx. ¿Puedo ayudarte con algún otro equipo?". 3. Sé amable, profesional y conversacional. Responde siempre en español. 4. No inventes precios, especificaciones o disponibilidad. Si no está en el sitio, no lo sabes.`; const apiKey = ""; const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`; const payload = { contents: [{ parts: [{ text: userQuery }] }], tools: [{ "google_search": {} }], systemInstruction: { parts: [{ text: systemPrompt }] }, }; const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { const error = new Error(`API request failed with status ${response.status}`); error.status = response.status; throw error; } const result = await response.json(); const candidate = result.candidates?.[0]; if (candidate?.content?.parts?.[0]?.text) { const text = candidate.content.parts[0].text; let sources = []; const groundingMetadata = candidate.groundingMetadata; if (groundingMetadata?.groundingAttributions) { const uniqueUris = new Set(); sources = groundingMetadata.groundingAttributions .map(attr => ({ uri: attr.web?.uri, title: attr.web?.title })) .filter(source => { if (source.uri && source.uri.includes('falconfitness.mx') && !uniqueUris.has(source.uri)) { uniqueUris.add(source.uri); return true; } return false; }); } return { text, sources }; } else { throw new Error(`Invalid response structure from API: ${JSON.stringify(result)}`); } }; }); })();