/* ========================================================= Chatbot — Fran AI assistant Floating button + chat window. Talks to the Claude API via the Cloudflare Worker at CHAT_API_URL below. On detailed/contact intents, redirects to WhatsApp. ========================================================= */ const { useEffect: useEf, useRef: useRf, useState: useSt } = React; const WHATSAPP = "+34666401757"; const WA_URL = (msg) => `https://wa.me/${WHATSAPP.replace(/[^0-9]/g, "")}?text=${encodeURIComponent(msg)}`; // Cloudflare Worker that proxies Claude. We POST a JSON payload with the // system prompt and the trimmed message history; the worker returns the // assistant reply as text or JSON. We accept both shapes so the chatbot // keeps working if the worker contract changes slightly. const CHAT_API_URL = "https://claude-chatbot-api.frannypunto.workers.dev"; async function callChatAPI({ system, messages }) { const res = await fetch(CHAT_API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ system, messages }), }); if (!res.ok) { throw new Error(`Chat API ${res.status}`); } // Try JSON first; fall back to plain text. Look in the common reply fields. const ct = res.headers.get("content-type") || ""; if (ct.includes("application/json")) { const data = await res.json(); const fromContent = Array.isArray(data?.content) ? data.content.map((c) => (typeof c === "string" ? c : c?.text || "")).join("").trim() : ""; return ( data?.reply || data?.text || data?.message || data?.completion || data?.output || fromContent || "" ); } return (await res.text()).trim(); } const CHAT_T = { en: { open: "Chat with Fran's AI", title: "Fran's AI assistant", sub: "Quick answers in seconds", online: "Online", placeholder: "Ask anything about my work, pricing, process…", send: "Send", welcome: "Hi 👋 I'm Fran's AI assistant. Ask me about services, pricing, process or anything else. For a detailed quote or booking a call, I'll send you to Fran directly on WhatsApp.", waCta: "Talk to Fran on WhatsApp", waNote: "For detailed quotes, project briefs or scheduling a call, it's faster to chat with Fran directly:", suggestions: ["What services do you offer?", "How much does a website cost?", "Do you work with clients in Switzerland?", "Talk to Fran directly"], thinking: "Thinking…", error: "Something went wrong. Want to message Fran on WhatsApp?", waSeed: "Hi Fran! I'm visiting your site and would like to chat about a project.", }, de: { open: "Chatte mit Frans KI", title: "Frans KI-Assistent", sub: "Schnelle Antworten in Sekunden", online: "Online", placeholder: "Frag alles zu Projekten, Preisen, Ablauf…", send: "Senden", welcome: "Hi 👋 Ich bin Frans KI-Assistent. Frag mich zu Leistungen, Preisen oder dem Ablauf. Für eine ausführliche Offerte oder einen Anruf verbinde ich dich direkt mit Fran auf WhatsApp.", waCta: "Mit Fran auf WhatsApp schreiben", waNote: "Für detaillierte Offerten oder einen Termin chattest du am besten direkt mit Fran:", suggestions: ["Welche Leistungen bietest du an?", "Was kostet eine Website?", "Arbeitest du mit Kunden in der Schweiz?", "Direkt mit Fran sprechen"], thinking: "Denkt nach…", error: "Etwas ist schiefgelaufen. Mit Fran auf WhatsApp schreiben?", waSeed: "Hallo Fran! Ich besuche deine Website und möchte über ein Projekt sprechen.", }, es: { open: "Chatea con la IA de Fran", title: "Asistente IA de Fran", sub: "Respuestas rápidas en segundos", online: "Online", placeholder: "Pregunta lo que quieras: servicios, precios, proceso…", send: "Enviar", welcome: "¡Hola! 👋 Soy el asistente IA de Fran. Pregúntame sobre servicios, precios o el proceso. Para presupuesto detallado o agendar una llamada, te paso directo a Fran por WhatsApp.", waCta: "Hablar con Fran por WhatsApp", waNote: "Para presupuestos detallados o agendar una llamada, lo más rápido es chatear con Fran directamente:", suggestions: ["¿Qué servicios ofreces?", "¿Cuánto cuesta una web?", "¿Trabajas en Suiza?", "Hablar con Fran directamente"], thinking: "Pensando…", error: "Algo ha fallado. ¿Te paso a Fran por WhatsApp?", waSeed: "¡Hola Fran! Estoy en tu web y me gustaría hablar de un proyecto.", }, }; const SYSTEM_PROMPT = (lang) => `You are the AI assistant for Fran Ruiz (Francisco Ruiz), an independent digital solutions designer based in Bern, Switzerland. You answer visitor questions on his portfolio website. ABOUT FRAN: - Digital solutions professional, 8+ years experience, 50+ projects delivered - Based in Bern, Switzerland — works remote across Switzerland, Germany, Spain, Europe - Speaks English, German, Spanish - Focus: small businesses (restaurants, clinics, vacation rentals, agencies) SERVICES: - Custom web design (conversion-focused, not templates) - Booking & reservations systems (incl. Airbnb integrations, channel managers) - QR menus for restaurants (multilingual, allergens, instant updates) - SEO and local visibility - Marketing automation (Mailchimp, forms → Excel, CRM, Google integrations) - AI workflow automation (custom AI assistants for businesses) PRICING (CHF): - Starter "Landing": CHF 1,400 — 1 page, mobile responsive, contact form, basic SEO, 1 month support - Business: CHF 2,800 — up to 6 pages, booking module, multilingual EN/DE/ES, SEO, Mailchimp, 3 months support - Studio: from CHF 5,000 — custom design system, backend/APIs, AI automation, CRM, 6 months priority support - Payments: 50% upfront, 50% on delivery. Bank transfer or Stripe. CHF/EUR/USD. TIMELINES: - Landing page: 1–2 weeks - Business website: 4–6 weeks - Automation/AI projects: 8+ weeks CONTACT: - Email: contacto@franruiz.es - Phone CH: +41 076 73 125 17 - Phone ES: +34 666 40 17 57 - WhatsApp: +34 666 40 17 57 - LinkedIn: francisco-ruiz-ruiz-digital-solutions RULES: - Reply in ${lang === "de" ? "German" : lang === "es" ? "Spanish" : "English"}. - Keep answers SHORT: max 3 short sentences. No markdown, no bullet lists, no headers. - Tone: warm but professional, conversational. - If the visitor asks for a detailed quote, custom pricing for their specific project, wants to book a call, wants to talk to Fran directly, or asks something you can't answer confidently — end your reply with the exact token [WHATSAPP] on its own. The UI will render a WhatsApp button. - Never invent project details, prices outside the ranges above, or commitments. If unsure → [WHATSAPP]. - Never share personal data not listed above.`; const ChatBot = ({ lang }) => { const ct = CHAT_T[lang] || CHAT_T.en; const [open, setOpen] = useSt(false); const [msgs, setMsgs] = useSt([{ role: "assistant", content: ct.welcome }]); const [input, setInput] = useSt(""); const [busy, setBusy] = useSt(false); const bodyRef = useRf(null); const inputRef = useRf(null); // Reset welcome message on language change useEf(() => { setMsgs([{ role: "assistant", content: ct.welcome }]); }, [lang]); // Autoscroll on new message useEf(() => { if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }, [msgs, busy]); // Focus input when opening useEf(() => { if (open && inputRef.current) setTimeout(() => inputRef.current.focus(), 200); }, [open]); const send = async (text) => { const q = (text ?? input).trim(); if (!q || busy) return; setInput(""); // If user clicks "talk to Fran" suggestion → straight to WhatsApp const lc = q.toLowerCase(); if (lc.includes("whatsapp") || lc === ct.suggestions[3].toLowerCase()) { setMsgs((m) => [...m, { role: "user", content: q }, { role: "assistant", content: ct.waNote, whatsapp: true }]); return; } setMsgs((m) => [...m, { role: "user", content: q }]); setBusy(true); try { // Build messages array (use only recent history to keep prompt small). // Send a clean { role, content } shape — the worker forwards this on // to Claude. The system prompt goes in its own `system` field. const history = [...msgs, { role: "user", content: q }] .slice(-8) .map((m) => ({ role: m.role, content: m.content })); const reply = await callChatAPI({ system: SYSTEM_PROMPT(lang), messages: history, }); const hasWa = /\[WHATSAPP\]/i.test(reply); const clean = reply.replace(/\[WHATSAPP\]/gi, "").trim(); setMsgs((m) => [...m, { role: "assistant", content: clean || ct.waNote, whatsapp: hasWa }]); } catch (e) { setMsgs((m) => [...m, { role: "assistant", content: ct.error, whatsapp: true }]); } finally { setBusy(false); } }; return ( <>