Tienda

Carrito de compra
CARRITO DE COMPRAS3
equipo femoral acostado peso integrado st falcon fitness maquinas de gym maquinas para hacer ejercicio gym en casa
Maquina para femoral acostado con peso integrado$37,900.00
-
+
jalon de espalda y remo peso integrado st falcon fitness maquinas de gym maquinas para hacer ejercicio gym en casa
Equipo jalon de espalda y remo con peso integrado$37,900.00
-
+
extension abdominal y espalda peso integrado falcon fitness maquinas de gym maquinas para hacer ejercicio gym en casa
Extension abdominal y espalda de peso integrado$37,900.00
-
+
Total
$118,200.00
3
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)}`); } }; }); })();