Spaces:
Running
Running
| // chat-engine.jsx — shared chat logic for all Fetch variants. | |
| // Response pipeline: intent matching → keyword lookup → fallbacks. | |
| // Two backends: "claude" (window.claude.complete) and "local" (intent | |
| // matching + keyword lookup + fallbacks — the path HF Static Spaces take). | |
| 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 INTENTS = [ | |
| { | |
| label: "greeting", | |
| patterns: [/\b(hi|hello|hey)\b/i, /good (morning|afternoon|evening)/i], | |
| response: "Hi! I'm Fetch Pup. Ask me something about dogs, or try the suggestions below.", | |
| }, | |
| { | |
| label: "small talk", | |
| patterns: [/how are you/i, /how('s| is) it going/i, /what'?s up/i], | |
| response: "Tail's wagging, thanks for asking! Got a dog question for me?", | |
| }, | |
| { | |
| label: "capability", | |
| patterns: [/what can you do/i, /what do you (know|do)/i, /\bhelp\b/i], | |
| response: "I know a bunch of dog facts. Ask a question or try the suggestions — I'll do my best.", | |
| }, | |
| { | |
| label: "identity", | |
| patterns: [/who are you/i, /what are you/i], | |
| response: "I'm Fetch Pup — a static chatbot demo. No model running, just intent matching and keyword lookup. But I know my dogs!", | |
| }, | |
| ]; | |
| function matchIntent(query) { | |
| const q = query || ""; | |
| for (const intent of INTENTS) { | |
| if (intent.patterns.some(p => p.test(q))) return intent; | |
| } | |
| return null; | |
| } | |
| 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 domesticated?", | |
| "Do all dogs bark?", | |
| "Tell me about greyhounds", | |
| "How much exercise does my dog 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", | |
| ]; | |
| 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)]; | |
| } | |
| // Build a BlenderBot-style prompt so the "claude" backend produces responses | |
| // in the same voice the Python reference targeted. | |
| function buildPrompt(history, userInput, fact, personality) { | |
| const lastFour = history.slice(-4); | |
| const lines = lastFour.map(m => `${m.role === "user" ? "User" : "Bot"}: ${m.content}`); | |
| if (fact) lines.unshift(fact.fact); | |
| lines.push(`User: ${userInput}`); | |
| const toneByLevel = { | |
| subtle: "Keep the dog references light; answer the question plainly.", | |
| medium: "Be warm and playful, with a clear dog-lover streak.", | |
| loud: "Go full dog mode: enthusiastic, tail-wagging, lots of 'woof' energy.", | |
| }; | |
| const tone = toneByLevel[personality] || toneByLevel.subtle; | |
| return ( | |
| "You are Fetch, a friendly dog-loving chatbot. " | |
| + "Answer in 1–2 short sentences. " | |
| + tone + " " | |
| + "If a fact is provided above the conversation, weave it naturally into your reply.\n\n" | |
| + lines.join("\n") | |
| + "\nBot:" | |
| ); | |
| } | |
| async function generateReply({ userInput, history, backend, personality }) { | |
| const intent = matchIntent(userInput); | |
| if (intent) return { text: intent.response, fact: null, source: "intent", label: intent.label }; | |
| const fact = retrieveFact(userInput); | |
| if (backend === "claude" && typeof window !== "undefined" && window.claude?.complete) { | |
| try { | |
| const prompt = buildPrompt(history, userInput, fact, personality); | |
| const raw = await window.claude.complete(prompt); | |
| const text = (raw || "").trim().replace(/^Bot:\s*/i, "").split("\nUser:")[0].trim(); | |
| if (!text || text.split(/\s+/).length <= 2) { | |
| return { text: pickFallback(), fact, source: "fallback" }; | |
| } | |
| return { text, fact, source: "model" }; | |
| } catch (err) { | |
| console.warn("Fetch: claude backend failed, falling back.", err); | |
| // fall through to local path | |
| } | |
| } | |
| // Local / offline / HF-static path: keyword fact wins, else fallback. | |
| if (fact) return { text: fact.fact, fact, source: "fact" }; | |
| return { text: pickFallback(), fact: null, source: "fallback" }; | |
| } | |
| // Hook: owns conversation state + send/clear + rotating suggestions. | |
| function useFetchChat({ backend = "claude", personality = "subtle" } = {}) { | |
| 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() }; | |
| // Snapshot history BEFORE adding the new user message — that's what the | |
| // prompt builder should see as prior context. | |
| 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, personality }); | |
| } catch (err) { | |
| console.error("Fetch: generateReply failed.", err); | |
| result = { text: "Something went wrong. Please try again.", fact: null, source: "error" }; | |
| } | |
| const { text, fact, source, label } = result; | |
| const ms = Math.round(performance.now() - t0); | |
| setMessages(prev => prev.map(m => | |
| m.id === placeholderId | |
| ? { ...m, content: text, pending: false, fact, ts: Date.now() } | |
| : m | |
| )); | |
| setDebug(prev => [ | |
| { t: Date.now(), user: userInput, reply: text, fact, source, label, ms, backend }, | |
| ...prev, | |
| ].slice(0, 20)); | |
| setPending(false); | |
| }, [backend, personality, 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 }; | |
| } | |
| // Suggestion carousel — replaces 1 or 2 at random per tick, no duplicates. | |
| 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, | |
| FETCH_INTENTS: INTENTS, | |
| }); | |