Spaces:
Running
Running
File size: 10,236 Bytes
0f3c25b 259107b 0f3c25b 4163d36 0f3c25b 4163d36 0f3c25b 259107b b819eec 259107b b819eec 259107b b819eec 259107b b819eec 259107b b819eec 259107b 0f3c25b 4163d36 0f3c25b 259107b b819eec 259107b 0f3c25b b819eec 0f3c25b b819eec 0f3c25b b819eec 0f3c25b b819eec 0f3c25b b819eec 0f3c25b 506b1dc 259107b 0f3c25b 506b1dc 0f3c25b 259107b 0f3c25b b819eec 0f3c25b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | // 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,
});
|