// chat-engine.jsx — Fetch Good Boi chat logic. // Response pipeline: identity intent → keyword lookup → docker model / fallbacks. // Backends: // "local" — keyword lookup + fallback only (works offline, no server) // "docker" — POST /api/chat to the FastAPI backend serving BlenderBot const KNOWLEDGE_BASE = [ ["domesticate", "Dogs have been domesticated for at least 15,000 years, making them the oldest domesticated animal."], ["skills", "Dogs can understand up to 250 words and gestures, and can count up to 5."], ["border collie","Border Collies are considered the most intelligent dog breed, capable of learning a new command in under 5 seconds."], ["greyhound", "Greyhounds can reach speeds of up to 45 mph, yet are famously calm and lazy indoors."], ["exercise", "Dogs need mental stimulation as much as physical exercise. A bored dog is often a destructive dog."], ["dental", "Regular dental care is one of the most overlooked aspects of dog health. Most dogs show signs of dental disease by age 3."], ["bark", "The Basenji is the only dog breed that doesn't bark... it yodels."], ["smell", "Dogs have a sense of smell estimated to be 10,000 to 100,000 times more acute than humans."], ["dream", "Dogs dream just like humans do. Research suggests they replay their day while sleeping, and smaller breeds tend to dream more frequently than larger ones."], ["woof", "Did someone say woof? You speak my language."], ["good boi", "That would be me, obviously."], ["walk", "Did anybody say walk? I'm in."], ["nose", "A dog's nose print is unique, like a human fingerprint — no two are the same."], ["sleep", "Dogs sleep 12–14 hours a day on average. Puppies and seniors can sleep up to 18 hours."], ["sweat", "Dogs have sweat glands in their paws — one of the few places they actually sweat."], ["puppy", "Puppies are born blind, deaf, and toothless. Their eyes and ears open at around 2–3 weeks old."], ["hear", "Dogs can hear sounds four times farther away than humans and detect frequencies up to 65,000 Hz."], ["color", "Dogs aren't fully colorblind — they can see blue and yellow, but not red or green."], ["tail", "A dog wagging its tail to the right signals positive feelings; to the left, anxiety or stress."], ["golden", "Golden Retrievers have such a soft bite they can carry a raw egg without cracking it."], ["poodle", "Poodles were originally bred as water retrievers — the iconic clip was functional, not decorative."], ["husky", "Huskies are among the closest living genetic relatives of the ancient grey wolf."], ]; const FALLBACKS = [ "I don't know, but I'd bet a dog would!", "Good question! I need more dog facts for that one.", "I'm not sure about that, but dogs are great!", ]; const SUGGESTIONS = [ "Tell me about border collies", "How good is a dog's sense of smell?", "Do dogs dream?", "Why is dental care important?", "How long have dogs been around?", "Do all dogs bark?", "Tell me about greyhounds", "How much exercise do dogs need?", "Are dog nose prints unique?", "How much do dogs sleep?", "Do dogs sweat?", "What can a newborn puppy see?", "How well do dogs hear?", "Are dogs colorblind?", "What does tail wagging mean?", "Tell me about golden retrievers", "Tell me about poodles", "Tell me about the husky breed", ]; // Identity intent — BlenderBot has no knowledge of its own name or persona, // so intercept name/identity questions before they reach the model. const IDENTITY_INTENT = { patterns: [/what('?s| is) your name/i, /who are you/i, /what are you/i, /introduce yourself/i], response: "I'm Fetch — a dog-loving chatbot powered by BlenderBot-400M-distill. Ask me something about dogs!", }; function matchIdentity(query) { return IDENTITY_INTENT.patterns.some(p => p.test(query || "")); } const INITIAL_GREETING = "Hi there! Ask me something simple, ideally about dogs."; function retrieveFact(query) { const q = (query || "").toLowerCase(); for (const [keyword, fact] of KNOWLEDGE_BASE) { if (q.includes(keyword)) return { keyword, fact }; } return null; } function pickFallback() { return FALLBACKS[Math.floor(Math.random() * FALLBACKS.length)]; } async function callDockerBackend({ userInput, history }) { const trimmed = history.slice(-4).map(m => ({ role: m.role, content: m.content })); const res = await fetch("api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: userInput, history: trimmed }), }); if (!res.ok) throw new Error(`Backend ${res.status}`); return await res.json(); } async function generateReply({ userInput, history, backend }) { if (matchIdentity(userInput)) { return { text: IDENTITY_INTENT.response, fact: null, source: "intent" }; } const fact = retrieveFact(userInput); if (backend === "docker") { try { const data = await callDockerBackend({ userInput, history }); const text = (data.reply || "").trim(); if (!text) return { text: pickFallback(), fact, source: "fallback" }; return { text, fact: data.fact || fact, source: "BlenderBot" }; } catch (err) { console.warn("Fetch: docker backend failed, using local fallback.", err); } } if (fact) return { text: fact.fact, fact, source: "fact" }; return { text: pickFallback(), fact: null, source: "fallback" }; } function useFetchChat({ backend = "local" } = {}) { const [messages, setMessages] = React.useState(() => [ { id: "m0", role: "assistant", content: INITIAL_GREETING, ts: Date.now() }, ]); const [pending, setPending] = React.useState(false); const [debug, setDebug] = React.useState([]); const seq = React.useRef(1); const nextId = () => `m${seq.current++}`; const send = React.useCallback(async (rawInput) => { const userInput = (rawInput || "").trim(); if (!userInput || pending) return; const userMsg = { id: nextId(), role: "user", content: userInput, ts: Date.now() }; const placeholderId = nextId(); const placeholder = { id: placeholderId, role: "assistant", content: "…", pending: true, ts: Date.now() }; let priorHistory = []; setMessages(prev => { priorHistory = prev; return [...prev, userMsg, placeholder]; }); setPending(true); const t0 = performance.now(); let result; try { result = await generateReply({ userInput, history: priorHistory, backend }); } catch (err) { console.error("Fetch: generateReply failed.", err); result = { text: "Something went wrong. Please try again.", fact: null, source: "error" }; } const { text, fact, source } = result; const ms = Math.round(performance.now() - t0); setMessages(prev => prev.map(m => m.id === placeholderId ? { ...m, content: text, pending: false, fact, source, ts: Date.now() } : m )); setDebug(prev => [ { t: Date.now(), user: userInput, reply: text, fact, source, ms, backend }, ...prev, ].slice(0, 20)); setPending(false); }, [backend, pending]); const reset = React.useCallback(() => { seq.current = 1; setMessages([{ id: "m0", role: "assistant", content: INITIAL_GREETING, ts: Date.now() }]); setDebug([]); }, []); return { messages, pending, debug, send, reset }; } function useRotatingSuggestions({ visible = 3, intervalMs = 6000 } = {}) { const [current, setCurrent] = React.useState(() => [...SUGGESTIONS].sort(() => Math.random() - 0.5).slice(0, visible) ); React.useEffect(() => { const id = setInterval(() => { setCurrent(prev => { const available = SUGGESTIONS.filter(s => !prev.includes(s)); const count = Math.random() < 0.5 ? 1 : 2; const incoming = [...available].sort(() => Math.random() - 0.5).slice(0, count); const slots = [...Array(visible).keys()].sort(() => Math.random() - 0.5).slice(0, count); const next = [...prev]; slots.forEach((slot, i) => { next[slot] = incoming[i]; }); return next; }); }, intervalMs); return () => clearInterval(id); }, [visible, intervalMs]); return current; } Object.assign(window, { useFetchChat, useRotatingSuggestions, retrieveFact, FETCH_KNOWLEDGE_BASE: KNOWLEDGE_BASE, FETCH_SUGGESTIONS: SUGGESTIONS, });