ElisaTrippetti commited on
Commit
a24b67f
·
verified ·
1 Parent(s): 9e891be

Upload 8 files

Browse files
static/chat-engine.jsx CHANGED
@@ -1,8 +1,8 @@
1
- // chat-engine.jsx — Fetch chat logic.
2
- // Response pipeline: intent matching → keyword lookup → fallbacks / gradio model.
3
  // Backends:
4
  // "local" — keyword lookup + fallback only (works offline, no server)
5
- // "gradio" — POST /api/chat to the FastAPI backend serving BlenderBot
6
 
7
  const KNOWLEDGE_BASE = [
8
  ["domesticate", "Dogs have been domesticated for at least 15,000 years, making them the oldest domesticated animal."],
@@ -35,37 +35,6 @@ const FALLBACKS = [
35
  "I'm not sure about that, but dogs are great!",
36
  ];
37
 
38
- const INTENTS = [
39
- {
40
- label: "greeting",
41
- patterns: [/\b(hi|hello|hey)\b/i, /good (morning|afternoon|evening)/i],
42
- response: "Hi! I'm Fetch Pup. Ask me something about dogs, or try the suggestions below.",
43
- },
44
- {
45
- label: "small talk",
46
- patterns: [/how are you/i, /how('s| is) it going/i, /what'?s up/i],
47
- response: "Tail's wagging, thanks for asking! Got a dog question for me?",
48
- },
49
- {
50
- label: "capability",
51
- patterns: [/what can you do/i, /what do you (know|do)/i, /\bhelp\b/i],
52
- response: "I know a bunch of dog facts. Ask a question or try the suggestions — I'll do my best.",
53
- },
54
- {
55
- label: "identity",
56
- patterns: [/who are you/i, /what are you/i],
57
- response: "I'm Fetch Pup — a dog-loving chatbot backed by BlenderBot-400M-distill. I also know my keyword facts!",
58
- },
59
- ];
60
-
61
- function matchIntent(query) {
62
- const q = query || "";
63
- for (const intent of INTENTS) {
64
- if (intent.patterns.some(p => p.test(q))) return intent;
65
- }
66
- return null;
67
- }
68
-
69
  const SUGGESTIONS = [
70
  "Tell me about border collies",
71
  "How good is a dog's sense of smell?",
@@ -101,7 +70,7 @@ function pickFallback() {
101
  return FALLBACKS[Math.floor(Math.random() * FALLBACKS.length)];
102
  }
103
 
104
- async function callGradioBackend({ userInput, history }) {
105
  const trimmed = history.slice(-4).map(m => ({ role: m.role, content: m.content }));
106
  const res = await fetch("api/chat", {
107
  method: "POST",
@@ -113,19 +82,16 @@ async function callGradioBackend({ userInput, history }) {
113
  }
114
 
115
  async function generateReply({ userInput, history, backend }) {
116
- const intent = matchIntent(userInput);
117
- if (intent) return { text: intent.response, fact: null, source: "intent", label: intent.label };
118
-
119
  const fact = retrieveFact(userInput);
120
 
121
- if (backend === "gradio") {
122
  try {
123
- const data = await callGradioBackend({ userInput, history });
124
  const text = (data.reply || "").trim();
125
  if (!text) return { text: pickFallback(), fact, source: "fallback" };
126
- return { text, fact: data.fact || fact, source: "gradio" };
127
  } catch (err) {
128
- console.warn("Fetch: gradio backend failed, using local fallback.", err);
129
  }
130
  }
131
 
@@ -162,7 +128,7 @@ function useFetchChat({ backend = "local" } = {}) {
162
  console.error("Fetch: generateReply failed.", err);
163
  result = { text: "Something went wrong. Please try again.", fact: null, source: "error" };
164
  }
165
- const { text, fact, source, label } = result;
166
  const ms = Math.round(performance.now() - t0);
167
 
168
  setMessages(prev => prev.map(m =>
@@ -171,7 +137,7 @@ function useFetchChat({ backend = "local" } = {}) {
171
  : m
172
  ));
173
  setDebug(prev => [
174
- { t: Date.now(), user: userInput, reply: text, fact, source, label, ms, backend },
175
  ...prev,
176
  ].slice(0, 20));
177
  setPending(false);
@@ -213,5 +179,4 @@ Object.assign(window, {
213
  retrieveFact,
214
  FETCH_KNOWLEDGE_BASE: KNOWLEDGE_BASE,
215
  FETCH_SUGGESTIONS: SUGGESTIONS,
216
- FETCH_INTENTS: INTENTS,
217
  });
 
1
+ // chat-engine.jsx — Fetch Good Boi chat logic.
2
+ // Response pipeline: keyword lookup → docker model / fallbacks.
3
  // Backends:
4
  // "local" — keyword lookup + fallback only (works offline, no server)
5
+ // "docker" — POST /api/chat to the FastAPI backend serving BlenderBot
6
 
7
  const KNOWLEDGE_BASE = [
8
  ["domesticate", "Dogs have been domesticated for at least 15,000 years, making them the oldest domesticated animal."],
 
35
  "I'm not sure about that, but dogs are great!",
36
  ];
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  const SUGGESTIONS = [
39
  "Tell me about border collies",
40
  "How good is a dog's sense of smell?",
 
70
  return FALLBACKS[Math.floor(Math.random() * FALLBACKS.length)];
71
  }
72
 
73
+ async function callDockerBackend({ userInput, history }) {
74
  const trimmed = history.slice(-4).map(m => ({ role: m.role, content: m.content }));
75
  const res = await fetch("api/chat", {
76
  method: "POST",
 
82
  }
83
 
84
  async function generateReply({ userInput, history, backend }) {
 
 
 
85
  const fact = retrieveFact(userInput);
86
 
87
+ if (backend === "docker") {
88
  try {
89
+ const data = await callDockerBackend({ userInput, history });
90
  const text = (data.reply || "").trim();
91
  if (!text) return { text: pickFallback(), fact, source: "fallback" };
92
+ return { text, fact: data.fact || fact, source: "docker" };
93
  } catch (err) {
94
+ console.warn("Fetch: docker backend failed, using local fallback.", err);
95
  }
96
  }
97
 
 
128
  console.error("Fetch: generateReply failed.", err);
129
  result = { text: "Something went wrong. Please try again.", fact: null, source: "error" };
130
  }
131
+ const { text, fact, source } = result;
132
  const ms = Math.round(performance.now() - t0);
133
 
134
  setMessages(prev => prev.map(m =>
 
137
  : m
138
  ));
139
  setDebug(prev => [
140
+ { t: Date.now(), user: userInput, reply: text, fact, source, ms, backend },
141
  ...prev,
142
  ].slice(0, 20));
143
  setPending(false);
 
179
  retrieveFact,
180
  FETCH_KNOWLEDGE_BASE: KNOWLEDGE_BASE,
181
  FETCH_SUGGESTIONS: SUGGESTIONS,
 
182
  });
static/debug-panel.jsx CHANGED
@@ -1,6 +1,6 @@
1
  // debug-panel.jsx — floating developer panel shared across all variants.
2
- // Surfaces: recent exchanges, fact-match status, backend, latency, and the
3
- // full keyword-lookup table from the Python reference.
4
 
5
  function DebugPanel({ debug, onClose, palette, font }) {
6
  const [tab, setTab] = React.useState("turns");
@@ -25,7 +25,7 @@ function DebugPanel({ debug, onClose, palette, font }) {
25
  <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
26
  <span style={{ color: palette.accent }}>●</span>
27
  <span style={{ letterSpacing: 0.8, textTransform: "uppercase", fontSize: 11 }}>
28
- fetch / debug
29
  </span>
30
  </div>
31
  <button onClick={onClose} aria-label="Close debug panel" style={{
@@ -41,7 +41,6 @@ function DebugPanel({ debug, onClose, palette, font }) {
41
  }}>
42
  {[
43
  { id: "turns", label: "turns" },
44
- { id: "intents", label: "intents" },
45
  { id: "keywords", label: "keywords" },
46
  { id: "about", label: "about" },
47
  ].map(t => (
@@ -58,7 +57,6 @@ function DebugPanel({ debug, onClose, palette, font }) {
58
 
59
  <div style={{ flex: 1, overflowY: "auto", padding: 12 }}>
60
  {tab === "turns" && <DebugLog debug={debug} palette={palette} />}
61
- {tab === "intents" && <DebugIntents palette={palette} />}
62
  {tab === "keywords" && <DebugKB palette={palette} />}
63
  {tab === "about" && <DebugAbout palette={palette} />}
64
  </div>
@@ -70,7 +68,7 @@ function DebugLog({ debug, palette }) {
70
  if (debug.length === 0) {
71
  return (
72
  <div style={{ color: palette.dim, fontStyle: "italic", padding: "8px 4px" }}>
73
- No turns yet. Ask Fetch something.
74
  </div>
75
  );
76
  }
@@ -82,7 +80,7 @@ function DebugLog({ debug, palette }) {
82
  borderRadius: 6,
83
  padding: 8,
84
  }}>
85
- <div style={{ color: palette.dim, fontSize: 11, display: "flex", justifyContent: "space-between", marginBottom: 4 }}>
86
  <span>{new Date(d.t).toLocaleTimeString()}</span>
87
  <span>{d.backend} · {d.ms}ms</span>
88
  </div>
@@ -94,16 +92,14 @@ function DebugLog({ debug, palette }) {
94
  <span style={{ color: palette.accent }}>reply&gt; </span>
95
  {d.reply}
96
  </div>
97
- <div style={{ color: palette.dim, fontSize: 11 }}>
98
  {"source: "}
99
  <span style={{
100
  color: d.source === "error" ? "#ff6b6b"
101
  : d.source === "fallback" ? palette.dim
102
  : palette.accent,
103
  }}>
104
- {d.source === "fact" ? `fact · ${d.fact?.keyword}`
105
- : d.source === "intent" ? `intent · ${d.label}`
106
- : d.source || "unknown"}
107
  </span>
108
  </div>
109
  </div>
@@ -132,32 +128,12 @@ function DebugKB({ palette }) {
132
  );
133
  }
134
 
135
- function DebugIntents({ palette }) {
136
- return (
137
- <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
138
- <div style={{ color: palette.dim, marginBottom: 4 }}>
139
- {FETCH_INTENTS.length} intents · first match wins
140
- </div>
141
- {FETCH_INTENTS.map(intent => (
142
- <div key={intent.label} style={{
143
- border: `1px solid ${palette.muted}`,
144
- borderRadius: 6,
145
- padding: 8,
146
- }}>
147
- <div style={{ color: palette.accent, marginBottom: 3 }}>{intent.label}</div>
148
- <div style={{ color: palette.fg, lineHeight: 1.4 }}>{intent.response}</div>
149
- </div>
150
- ))}
151
- </div>
152
- );
153
- }
154
-
155
  function DebugAbout({ palette }) {
156
  return (
157
  <div style={{ lineHeight: 1.6, color: palette.fg, display: "flex", flexDirection: "column", gap: 10 }}>
158
- <div>Fetch Pup is a dog-loving chatbot backed by BlenderBot-400M-distill running on CPU.</div>
159
- <div>Responses go through an intent matcher first, then a keyword lookup table — if neither matches, the query is sent to the model. Fallbacks cover anything the model returns too short or empty. If the backend fails, the browser falls back to keyword lookup automatically.</div>
160
- <div style={{ color: "#e8a87c" }}>First reply may be slow BlenderBot cold-starts on HuggingFace Spaces. Subsequent replies are faster.</div>
161
  <div style={{ color: palette.dim }}>Designed with Claude Design · Built with Claude Code</div>
162
  </div>
163
  );
 
1
  // debug-panel.jsx — floating developer panel shared across all variants.
2
+ // Surfaces: recent turns, source tracking, backend, latency, and the
3
+ // full keyword-lookup table.
4
 
5
  function DebugPanel({ debug, onClose, palette, font }) {
6
  const [tab, setTab] = React.useState("turns");
 
25
  <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
26
  <span style={{ color: palette.accent }}>●</span>
27
  <span style={{ letterSpacing: 0.8, textTransform: "uppercase", fontSize: 11 }}>
28
+ fetch good boi / debug
29
  </span>
30
  </div>
31
  <button onClick={onClose} aria-label="Close debug panel" style={{
 
41
  }}>
42
  {[
43
  { id: "turns", label: "turns" },
 
44
  { id: "keywords", label: "keywords" },
45
  { id: "about", label: "about" },
46
  ].map(t => (
 
57
 
58
  <div style={{ flex: 1, overflowY: "auto", padding: 12 }}>
59
  {tab === "turns" && <DebugLog debug={debug} palette={palette} />}
 
60
  {tab === "keywords" && <DebugKB palette={palette} />}
61
  {tab === "about" && <DebugAbout palette={palette} />}
62
  </div>
 
68
  if (debug.length === 0) {
69
  return (
70
  <div style={{ color: palette.dim, fontStyle: "italic", padding: "8px 4px" }}>
71
+ No turns yet. Ask Fetch Good Boi something.
72
  </div>
73
  );
74
  }
 
80
  borderRadius: 6,
81
  padding: 8,
82
  }}>
83
+ <div style={{ color: palette.dim, fontSize: 12, display: "flex", justifyContent: "space-between", marginBottom: 4 }}>
84
  <span>{new Date(d.t).toLocaleTimeString()}</span>
85
  <span>{d.backend} · {d.ms}ms</span>
86
  </div>
 
92
  <span style={{ color: palette.accent }}>reply&gt; </span>
93
  {d.reply}
94
  </div>
95
+ <div style={{ color: palette.dim, fontSize: 12 }}>
96
  {"source: "}
97
  <span style={{
98
  color: d.source === "error" ? "#ff6b6b"
99
  : d.source === "fallback" ? palette.dim
100
  : palette.accent,
101
  }}>
102
+ {d.source === "fact" ? `fact · ${d.fact?.keyword}` : d.source || "unknown"}
 
 
103
  </span>
104
  </div>
105
  </div>
 
128
  );
129
  }
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  function DebugAbout({ palette }) {
132
  return (
133
  <div style={{ lineHeight: 1.6, color: palette.fg, display: "flex", flexDirection: "column", gap: 10 }}>
134
+ <div>Fetch Good Boi is a dog-loving chatbot backed by BlenderBot-400M-distill running on CPU.</div>
135
+ <div>Responses use a keyword lookup layer — if a fact matches the query, it's prepended to the prompt. BlenderBot then generates a reply within a four-turn context window. Fallbacks cover anything the model returns too short or empty. If the backend fails, the browser falls back to keyword lookup automatically.</div>
136
+ <div style={{ color: "#e8a87c" }}>Fetch Good Boi runs on CPU, so responses may take a few seconds.</div>
137
  <div style={{ color: palette.dim }}>Designed with Claude Design · Built with Claude Code</div>
138
  </div>
139
  );
static/index.html CHANGED
@@ -39,7 +39,7 @@
39
  <script type="text/babel" src="variant-editorial.jsx"></script>
40
 
41
  <script type="text/babel" data-presets="react">
42
- // Gradio build: editorial variant + BlenderBot backend via /api/chat.
43
  // If the backend fails (cold start, timeout) the chat engine falls back
44
  // to the local keyword lookup automatically.
45
  const FONT_PAIR = {
@@ -51,8 +51,7 @@ const FONT_PAIR = {
51
  function FetchApp() {
52
  return (
53
  <EditorialVariant
54
- backend="gradio"
55
- personality="subtle"
56
  density="comfortable"
57
  accent="bone"
58
  fontPair={FONT_PAIR}
 
39
  <script type="text/babel" src="variant-editorial.jsx"></script>
40
 
41
  <script type="text/babel" data-presets="react">
42
+ // Docker build: editorial variant + BlenderBot backend via /api/chat.
43
  // If the backend fails (cold start, timeout) the chat engine falls back
44
  // to the local keyword lookup automatically.
45
  const FONT_PAIR = {
 
51
  function FetchApp() {
52
  return (
53
  <EditorialVariant
54
+ backend="docker"
 
55
  density="comfortable"
56
  accent="bone"
57
  fontPair={FONT_PAIR}
static/variant-editorial.jsx CHANGED
@@ -1,16 +1,16 @@
1
- // variant-editorial.jsx — Fetch as an editorial reading experience.
2
  // Centered narrow column, big Instrument Serif title, messages read as
3
  // paragraphs with slim rules between turns. Single paw glyph as the one
4
- // dog cue.
5
 
6
  const editorialPalettes = {
7
- cream: { bg: "#f8f5ef", paper: "#ffffff", ink: "#1a1714", muted: "#8b857a", rule: "#e6e0d4", accent: "#8a4b2a" },
8
- sand: { bg: "#ece7dc", paper: "#f8f5ef", ink: "#1a1714", muted: "#8b857a", rule: "#d8cfbc", accent: "#4a3a1a" },
9
- bone: { bg: "#eeeae3", paper: "#ffffff", ink: "#1a1714", muted: "#8b857a", rule: "#dcd4c4", accent: "#2a5c4a" },
10
  };
11
 
12
- function EditorialVariant({ backend, personality, density, accent, fontPair, bubble }) {
13
- const { messages, pending, debug, send, reset } = useFetchChat({ backend, personality });
14
  const suggestions = useRotatingSuggestions({ visible: 3 });
15
  const [input, setInput] = React.useState("");
16
  const [showDebug, setShowDebug] = React.useState(false);
@@ -59,71 +59,84 @@ function EditorialVariant({ backend, personality, density, accent, fontPair, bub
59
  }}>
60
  <header style={{
61
  display: "flex", alignItems: "center", justifyContent: "space-between",
62
- padding: "18px 32px",
63
  color: palette.muted, fontSize: 12, letterSpacing: 1.2, textTransform: "uppercase",
 
64
  }}>
65
  <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
66
  <PawGlyph size={14} color={palette.accent} />
67
- <span>fetch pup · a dog-loving chatbot</span>
68
  </div>
69
- <div style={{ display: "flex", gap: 18, textTransform: "none", letterSpacing: 0 }}>
70
- <button onClick={reset} style={editLink(palette)}>Clear</button>
71
- <button onClick={() => setShowDebug(s => !s)} style={editLink(palette)}>Debug</button>
72
  </div>
73
  </header>
74
 
75
- <div ref={scrollRef} style={{
76
- flex: 1, overflowY: "auto",
77
  display: "flex", justifyContent: "center",
 
 
 
 
78
  }}>
79
- <div style={{
80
- width: "min(620px, 88%)",
81
- padding: "24px 0 40px",
82
- }}>
83
  <h1 style={{
84
  fontFamily: fontPair.display, fontWeight: 400,
85
  fontSize: "clamp(44px, 6vw, 68px)",
86
  lineHeight: 1.02, letterSpacing: -0.5,
87
- margin: "16px 0 12px",
88
  }}>
89
- This is <em style={{ fontStyle: "italic" }}>Fetch</em>.
90
  </h1>
91
  <p style={{
92
- fontFamily: fontPair.body, fontSize: 14.5, lineHeight: 1.6,
93
- color: palette.muted, maxWidth: 520,
94
- margin: "0 0 28px",
95
  }}>
96
  A friendly chatbot with a soft spot for dogs, powered by BlenderBot-400M-distill.
97
- Intent matching, four-turn memory, a keyword lookup, a handful of fallbacks.
98
- First reply may be slow — the model cold-starts on CPU.
99
  </p>
100
-
101
- <div style={{
102
- borderTop: `1px solid ${palette.rule}`,
103
- paddingTop: 8,
104
  }}>
105
- {messages.filter(m => !m.pending).map((m, i) => (
106
- <EditorialTurn
107
- key={m.id}
108
- msg={m}
109
- palette={palette}
110
- fontPair={fontPair}
111
- fontSize={fontSize}
112
- gap={gap}
113
- isFirst={i === 0}
114
- radii={radii}
115
- />
116
- ))}
117
- {pending && (
118
- <div style={{
119
- color: palette.muted, fontStyle: "italic",
120
- padding: `${gap / 2}px 0`, fontFamily: fontPair.display,
121
- fontSize: fontSize + 1,
122
- }}>
123
- Fetching an answer<EllipsisAnim />
124
- </div>
125
- )}
126
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
127
  </div>
128
  </div>
129
 
@@ -132,6 +145,7 @@ function EditorialVariant({ backend, personality, density, accent, fontPair, bub
132
  background: palette.paper,
133
  padding: "16px 0 22px",
134
  display: "flex", justifyContent: "center",
 
135
  }}>
136
  <form onSubmit={submit} style={{
137
  width: "min(620px, 88%)",
@@ -152,21 +166,22 @@ function EditorialVariant({ backend, personality, density, accent, fontPair, bub
152
  <input
153
  value={input}
154
  onChange={e => setInput(e.target.value)}
155
- placeholder="Ask Fetch a question"
156
  autoFocus
157
  disabled={pending}
158
  style={{
159
  flex: 1, background: "transparent", border: "none", outline: "none",
160
- color: palette.ink, fontFamily: fontPair.display,
161
- fontSize: 22, fontStyle: "italic",
162
  }}
163
  />
164
  <button type="submit" disabled={pending || !input.trim()} style={{
165
  background: palette.ink, color: palette.paper,
166
- border: "none", padding: "8px 18px",
167
  borderRadius: 999, cursor: pending || !input.trim() ? "default" : "pointer",
168
- fontFamily: fontPair.body, fontSize: 13, letterSpacing: 0.5,
169
  opacity: pending || !input.trim() ? 0.4 : 1,
 
170
  }}>Ask →</button>
171
  </div>
172
  </form>
@@ -219,7 +234,6 @@ function EditorialTurn({ msg, palette, fontPair, fontSize, gap, isFirst, radii }
219
  fontSize: isUser ? fontSize : fontSize + 3,
220
  lineHeight: 1.5,
221
  color: palette.ink,
222
- fontStyle: isUser ? "normal" : "normal",
223
  }}>
224
  {msg.pending ? <span style={{ color: palette.muted }}>…</span> : msg.content}
225
  </div>
@@ -239,8 +253,8 @@ function EditorialSuggestions({ suggestions, onPick, palette, fontPair, pending
239
  style={{
240
  border: `1px solid ${palette.rule}`,
241
  background: "transparent", color: palette.ink,
242
- fontFamily: fontPair.body, fontSize: 12,
243
- padding: "5px 12px", borderRadius: 999,
244
  cursor: pending ? "default" : "pointer",
245
  opacity: pending ? 0.5 : 1,
246
  animation: `fetch-fadein .4s ease ${i * 60}ms both`,
@@ -251,15 +265,25 @@ function EditorialSuggestions({ suggestions, onPick, palette, fontPair, pending
251
  );
252
  }
253
 
254
- function editLink(p) {
255
  return {
256
- background: "transparent", border: "none", padding: 0,
257
- color: p.muted, fontSize: 12, cursor: "pointer", fontFamily: "inherit",
 
 
 
 
 
 
 
 
 
 
 
258
  };
259
  }
260
 
261
  function PawGlyph({ size = 14, color = "#8a4b2a" }) {
262
- // Simple paw mark — 4 toes + pad. Kept small + unambiguous.
263
  return (
264
  <svg width={size} height={size} viewBox="0 0 20 20" fill={color} aria-hidden="true">
265
  <circle cx="5" cy="6" r="2"/>
 
1
+ // variant-editorial.jsx — Fetch Good Boi as an editorial reading experience.
2
  // Centered narrow column, big Instrument Serif title, messages read as
3
  // paragraphs with slim rules between turns. Single paw glyph as the one
4
+ // dog cue. H1 + intro stay sticky; only the conversation scrolls.
5
 
6
  const editorialPalettes = {
7
+ cream: { bg: "#f8f5ef", paper: "#ffffff", ink: "#1a1714", secondary: "#4a4540", muted: "#8b857a", rule: "#e6e0d4", accent: "#8a4b2a" },
8
+ sand: { bg: "#ece7dc", paper: "#f8f5ef", ink: "#1a1714", secondary: "#4a4540", muted: "#8b857a", rule: "#d8cfbc", accent: "#4a3a1a" },
9
+ bone: { bg: "#eeeae3", paper: "#ffffff", ink: "#1a1714", secondary: "#4a4540", muted: "#8b857a", rule: "#dcd4c4", accent: "#2a5c4a" },
10
  };
11
 
12
+ function EditorialVariant({ backend, density, accent, fontPair, bubble }) {
13
+ const { messages, pending, debug, send, reset } = useFetchChat({ backend });
14
  const suggestions = useRotatingSuggestions({ visible: 3 });
15
  const [input, setInput] = React.useState("");
16
  const [showDebug, setShowDebug] = React.useState(false);
 
59
  }}>
60
  <header style={{
61
  display: "flex", alignItems: "center", justifyContent: "space-between",
62
+ padding: "14px 32px",
63
  color: palette.muted, fontSize: 12, letterSpacing: 1.2, textTransform: "uppercase",
64
+ flexShrink: 0,
65
  }}>
66
  <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
67
  <PawGlyph size={14} color={palette.accent} />
68
+ <span>Fetch Good Boi · a dog-loving BlenderBot</span>
69
  </div>
70
+ <div style={{ display: "flex", gap: 8, textTransform: "none", letterSpacing: 0 }}>
71
+ <button onClick={reset} style={editBtn(palette)} aria-label="Clear conversation">Clear</button>
72
+ <button onClick={() => setShowDebug(s => !s)} style={editBtn(palette)} aria-label="Toggle debug panel">Debug</button>
73
  </div>
74
  </header>
75
 
76
+ {/* Sticky intro — stays visible as conversation scrolls */}
77
+ <div style={{
78
  display: "flex", justifyContent: "center",
79
+ flexShrink: 0,
80
+ padding: "20px 0 0",
81
+ background: palette.bg,
82
+ borderBottom: `1px solid ${palette.rule}`,
83
  }}>
84
+ <div style={{ width: "min(620px, 88%)", paddingBottom: 20 }}>
 
 
 
85
  <h1 style={{
86
  fontFamily: fontPair.display, fontWeight: 400,
87
  fontSize: "clamp(44px, 6vw, 68px)",
88
  lineHeight: 1.02, letterSpacing: -0.5,
89
+ margin: "0 0 12px",
90
  }}>
91
+ This is <em style={{ fontStyle: "italic" }}>Fetch Good Boi</em>.
92
  </h1>
93
  <p style={{
94
+ fontFamily: fontPair.body, fontSize: 14.5, lineHeight: 1.65,
95
+ color: palette.secondary, maxWidth: 520, letterSpacing: "0.015em",
96
+ margin: "0 0 8px",
97
  }}>
98
  A friendly chatbot with a soft spot for dogs, powered by BlenderBot-400M-distill.
99
+ Four-turn context window · A small keyword lookup layer · A handful of fallbacks
 
100
  </p>
101
+ <p style={{
102
+ fontFamily: fontPair.body, fontSize: 13, lineHeight: 1.5,
103
+ color: palette.muted, maxWidth: 520,
104
+ margin: 0, fontStyle: "italic",
105
  }}>
106
+ Fetch Good Boi runs on CPU, so responses may take a few seconds.
107
+ </p>
108
+ </div>
109
+ </div>
110
+
111
+ <div ref={scrollRef} style={{
112
+ flex: 1, overflowY: "auto",
113
+ display: "flex", justifyContent: "center",
114
+ }}>
115
+ <div style={{
116
+ width: "min(620px, 88%)",
117
+ padding: "16px 0 40px",
118
+ }}>
119
+ {messages.filter(m => !m.pending).map((m, i) => (
120
+ <EditorialTurn
121
+ key={m.id}
122
+ msg={m}
123
+ palette={palette}
124
+ fontPair={fontPair}
125
+ fontSize={fontSize}
126
+ gap={gap}
127
+ isFirst={i === 0}
128
+ radii={radii}
129
+ />
130
+ ))}
131
+ {pending && (
132
+ <div style={{
133
+ color: palette.muted, fontStyle: "italic",
134
+ padding: `${gap / 2}px 0`, fontFamily: fontPair.display,
135
+ fontSize: fontSize + 1,
136
+ }}>
137
+ Fetching an answer<EllipsisAnim />
138
+ </div>
139
+ )}
140
  </div>
141
  </div>
142
 
 
145
  background: palette.paper,
146
  padding: "16px 0 22px",
147
  display: "flex", justifyContent: "center",
148
+ flexShrink: 0,
149
  }}>
150
  <form onSubmit={submit} style={{
151
  width: "min(620px, 88%)",
 
166
  <input
167
  value={input}
168
  onChange={e => setInput(e.target.value)}
169
+ placeholder="Ask Fetch Good Boi a question"
170
  autoFocus
171
  disabled={pending}
172
  style={{
173
  flex: 1, background: "transparent", border: "none", outline: "none",
174
+ color: palette.ink, fontFamily: fontPair.body,
175
+ fontSize: 20,
176
  }}
177
  />
178
  <button type="submit" disabled={pending || !input.trim()} style={{
179
  background: palette.ink, color: palette.paper,
180
+ border: "none", padding: "10px 20px",
181
  borderRadius: 999, cursor: pending || !input.trim() ? "default" : "pointer",
182
+ fontFamily: fontPair.body, fontSize: 14, letterSpacing: 0.5,
183
  opacity: pending || !input.trim() ? 0.4 : 1,
184
+ minHeight: 40,
185
  }}>Ask →</button>
186
  </div>
187
  </form>
 
234
  fontSize: isUser ? fontSize : fontSize + 3,
235
  lineHeight: 1.5,
236
  color: palette.ink,
 
237
  }}>
238
  {msg.pending ? <span style={{ color: palette.muted }}>…</span> : msg.content}
239
  </div>
 
253
  style={{
254
  border: `1px solid ${palette.rule}`,
255
  background: "transparent", color: palette.ink,
256
+ fontFamily: fontPair.body, fontSize: 13,
257
+ padding: "7px 14px", borderRadius: 999,
258
  cursor: pending ? "default" : "pointer",
259
  opacity: pending ? 0.5 : 1,
260
  animation: `fetch-fadein .4s ease ${i * 60}ms both`,
 
265
  );
266
  }
267
 
268
+ function editBtn(p) {
269
  return {
270
+ background: "transparent",
271
+ border: `1px solid ${p.muted}`,
272
+ padding: "6px 10px",
273
+ minHeight: 32,
274
+ color: p.secondary,
275
+ fontSize: 12,
276
+ cursor: "pointer",
277
+ fontFamily: "inherit",
278
+ borderRadius: 4,
279
+ letterSpacing: 0.8,
280
+ textTransform: "uppercase",
281
+ display: "inline-flex",
282
+ alignItems: "center",
283
  };
284
  }
285
 
286
  function PawGlyph({ size = 14, color = "#8a4b2a" }) {
 
287
  return (
288
  <svg width={size} height={size} viewBox="0 0 20 20" fill={color} aria-hidden="true">
289
  <circle cx="5" cy="6" r="2"/>