Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- static/chat-engine.jsx +10 -45
- static/debug-panel.jsx +10 -34
- static/index.html +2 -3
- static/variant-editorial.jsx +87 -63
static/chat-engine.jsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
-
// chat-engine.jsx — Fetch chat logic.
|
| 2 |
-
// Response pipeline:
|
| 3 |
// Backends:
|
| 4 |
// "local" — keyword lookup + fallback only (works offline, no server)
|
| 5 |
-
// "
|
| 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
|
| 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 === "
|
| 122 |
try {
|
| 123 |
-
const data = await
|
| 124 |
const text = (data.reply || "").trim();
|
| 125 |
if (!text) return { text: pickFallback(), fact, source: "fallback" };
|
| 126 |
-
return { text, fact: data.fact || fact, source: "
|
| 127 |
} catch (err) {
|
| 128 |
-
console.warn("Fetch:
|
| 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
|
| 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,
|
| 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
|
| 3 |
-
// full keyword-lookup table
|
| 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:
|
| 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> </span>
|
| 95 |
{d.reply}
|
| 96 |
</div>
|
| 97 |
-
<div style={{ color: palette.dim, fontSize:
|
| 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"
|
| 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
|
| 159 |
-
<div>Responses
|
| 160 |
-
<div style={{ color: "#e8a87c" }}>
|
| 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> </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 |
-
//
|
| 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="
|
| 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:
|
| 8 |
-
sand:
|
| 9 |
-
bone:
|
| 10 |
};
|
| 11 |
|
| 12 |
-
function EditorialVariant({ backend,
|
| 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,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: "
|
| 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>
|
| 68 |
</div>
|
| 69 |
-
<div style={{ display: "flex", gap:
|
| 70 |
-
<button onClick={reset} style={
|
| 71 |
-
<button onClick={() => setShowDebug(s => !s)} style={
|
| 72 |
</div>
|
| 73 |
</header>
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 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: "
|
| 88 |
}}>
|
| 89 |
-
This is <em style={{ fontStyle: "italic" }}>Fetch</em>.
|
| 90 |
</h1>
|
| 91 |
<p style={{
|
| 92 |
-
fontFamily: fontPair.body, fontSize: 14.5, lineHeight: 1.
|
| 93 |
-
color: palette.
|
| 94 |
-
margin: "0 0
|
| 95 |
}}>
|
| 96 |
A friendly chatbot with a soft spot for dogs, powered by BlenderBot-400M-distill.
|
| 97 |
-
|
| 98 |
-
First reply may be slow — the model cold-starts on CPU.
|
| 99 |
</p>
|
| 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 |
</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.
|
| 161 |
-
fontSize:
|
| 162 |
}}
|
| 163 |
/>
|
| 164 |
<button type="submit" disabled={pending || !input.trim()} style={{
|
| 165 |
background: palette.ink, color: palette.paper,
|
| 166 |
-
border: "none", padding: "
|
| 167 |
borderRadius: 999, cursor: pending || !input.trim() ? "default" : "pointer",
|
| 168 |
-
fontFamily: fontPair.body, fontSize:
|
| 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:
|
| 243 |
-
padding: "
|
| 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
|
| 255 |
return {
|
| 256 |
-
background: "transparent",
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"/>
|