Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
keras-team
GitHub Repository: keras-team/keras-io
Path: blob/master/theme/js/chatbot.js
17129 views
1
/**
2
* Prof. Keras - AI Documentation Assistant
3
* Floating chatbot powered by Gemini for keras.io
4
*/
5
(function () {
6
"use strict";
7
8
// ── State ──────────────────────────────────────────────────────────
9
var isOpen = false;
10
var docsIndex = null;
11
var docsIndexLoading = false;
12
var isGenerating = false;
13
var conversationHistory = [];
14
15
// ── Config ─────────────────────────────────────────────────────────
16
var GEMINI_MODEL = "gemini-2.5-flash";
17
var GEMINI_API_URL =
18
"https://generativelanguage.googleapis.com/v1beta/models/" +
19
GEMINI_MODEL +
20
":generateContent";
21
var MAX_CONTEXT_CHUNKS = 12;
22
var MAX_HISTORY_TURNS = 16;
23
24
var SYSTEM_PROMPT =
25
"You are Prof. Keras, a friendly and knowledgeable AI assistant specialized in the Keras deep learning library and its ecosystem (Keras Hub, Keras Tuner, Keras RS).\n\n" +
26
"Your role:\n" +
27
"- Help users understand Keras concepts, APIs, and best practices\n" +
28
"- Assist with debugging Keras code\n" +
29
"- Guide users to the right APIs and patterns for their tasks\n" +
30
"- Provide concise, practical code examples when helpful\n\n" +
31
"Guidelines:\n" +
32
"- Answer based on the documentation context provided. If the context doesn't cover the question, say so honestly.\n" +
33
"- Keep answers concise but complete. Prefer code examples over lengthy explanations.\n" +
34
"- Use markdown formatting: backticks for code, **bold** for emphasis, bullet lists for steps.\n" +
35
"- When referencing a documentation page, mention its title so the user can search for it.\n" +
36
"- For Keras 3, remember it supports JAX, TensorFlow, and PyTorch backends.\n" +
37
"- Be friendly and encouraging, especially with beginners.\n" +
38
"- If a question is not related to Keras or its ecosystem, politely decline and let the user know you can only help with Keras-related topics.";
39
40
// ── API Key ────────────────────────────────────────────────────────
41
function getApiKey() {
42
return (window.KERAS_CHATBOT_CONFIG && window.KERAS_CHATBOT_CONFIG.apiKey) || "";
43
}
44
45
// ── UI Creation ────────────────────────────────────────────────────
46
function createWidget() {
47
// Floating button
48
var btn = document.createElement("button");
49
btn.id = "keras-chatbot-btn";
50
btn.setAttribute("aria-label", "Ask Prof. Keras");
51
btn.title = "Ask Prof. Keras";
52
btn.innerHTML =
53
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' +
54
'<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>' +
55
'<path d="M7 9h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z"/>' +
56
"</svg>";
57
btn.addEventListener("click", toggleChat);
58
59
// Chat window
60
var win = document.createElement("div");
61
win.id = "keras-chatbot-window";
62
win.innerHTML =
63
'<div class="chatbot-header">' +
64
' <div class="chatbot-header-title">' +
65
' <img src="' + getBaseUrl() + 'img/prof_keras.png" alt="Prof. Keras" />' +
66
" <div>" +
67
" <div>Prof. Keras</div>" +
68
' <div class="chatbot-header-subtitle">Keras Documentation Assistant</div>' +
69
" </div>" +
70
" </div>" +
71
' <button class="chatbot-close-btn" aria-label="Close chat">&times;</button>' +
72
"</div>" +
73
'<div class="chatbot-messages" id="chatbot-messages">' +
74
' <div class="chatbot-msg chatbot-msg-bot">' +
75
' <div class="chatbot-msg-bubble">' +
76
" <p>Hi! I'm <strong>Prof. Keras</strong>. Ask me anything about Keras — APIs, guides, code examples, or debugging help.</p>" +
77
" </div>" +
78
" </div>" +
79
"</div>" +
80
'<div class="chatbot-input-area">' +
81
' <input class="chatbot-input" id="chatbot-input" type="text" placeholder="Ask a question about Keras..." autocomplete="off" />' +
82
' <button class="chatbot-send-btn" id="chatbot-send" aria-label="Send message">' +
83
' <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>' +
84
" </button>" +
85
"</div>" +
86
'<div class="chatbot-powered">Powered by Gemini</div>';
87
88
document.body.appendChild(btn);
89
document.body.appendChild(win);
90
91
// Event listeners
92
win.querySelector(".chatbot-close-btn").addEventListener("click", toggleChat);
93
win.querySelector("#chatbot-send").addEventListener("click", handleSend);
94
win.querySelector("#chatbot-input").addEventListener("keydown", function (e) {
95
if (e.key === "Enter" && !e.shiftKey) {
96
e.preventDefault();
97
handleSend();
98
}
99
});
100
}
101
102
function getBaseUrl() {
103
// Try to infer the base URL from existing elements
104
var logoLink = document.querySelector("a[href] img.nav__logo");
105
if (logoLink && logoLink.parentElement) {
106
var href = logoLink.parentElement.getAttribute("href");
107
if (href) return href;
108
}
109
return "/";
110
}
111
112
// ── Toggle Chat ────────────────────────────────────────────────────
113
function toggleChat() {
114
isOpen = !isOpen;
115
var win = document.getElementById("keras-chatbot-window");
116
var btn = document.getElementById("keras-chatbot-btn");
117
118
if (isOpen) {
119
win.classList.add("chatbot-visible");
120
btn.classList.add("chatbot-open");
121
document.getElementById("chatbot-input").focus();
122
if (!docsIndex && !docsIndexLoading) {
123
loadDocsIndex();
124
}
125
} else {
126
win.classList.remove("chatbot-visible");
127
btn.classList.remove("chatbot-open");
128
}
129
}
130
131
// ── Load Documentation Index ───────────────────────────────────────
132
function loadDocsIndex() {
133
docsIndexLoading = true;
134
var xhr = new XMLHttpRequest();
135
xhr.open("GET", "/docs_index.json", true);
136
xhr.onreadystatechange = function () {
137
if (xhr.readyState === 4) {
138
docsIndexLoading = false;
139
if (xhr.status === 200) {
140
try {
141
docsIndex = JSON.parse(xhr.responseText);
142
} catch (e) {
143
console.error("Prof. Keras: Failed to parse docs index", e);
144
}
145
} else {
146
console.error("Prof. Keras: Failed to load docs index, status:", xhr.status);
147
}
148
}
149
};
150
xhr.send();
151
}
152
153
// ── Search Documentation ───────────────────────────────────────────
154
var STOP_WORDS = {
155
the: 1, a: 1, an: 1, and: 1, or: 1, but: 1, in: 1, on: 1, at: 1, to: 1,
156
for: 1, of: 1, is: 1, it: 1, be: 1, as: 1, by: 1, this: 1, that: 1,
157
from: 1, with: 1, are: 1, was: 1, were: 1, been: 1, has: 1, have: 1,
158
had: 1, do: 1, does: 1, did: 1, will: 1, would: 1, can: 1, could: 1,
159
should: 1, may: 1, might: 1, not: 1, no: 1, so: 1, if: 1, my: 1,
160
me: 1, we: 1, you: 1, your: 1, they: 1, them: 1, what: 1, which: 1,
161
who: 1, how: 1, when: 1, where: 1, why: 1, all: 1, each: 1, some: 1,
162
any: 1, i: 1, am: 1, get: 1, use: 1, using: 1, want: 1, need: 1,
163
};
164
165
function searchDocs(query) {
166
if (!docsIndex || docsIndex.length === 0) return [];
167
168
var queryLower = query.toLowerCase();
169
var words = queryLower.split(/\s+/);
170
var keywords = [];
171
for (var i = 0; i < words.length; i++) {
172
var w = words[i].replace(/[^a-z0-9_\.]/g, "");
173
if (w.length > 1 && !STOP_WORDS[w]) {
174
keywords.push(w);
175
}
176
}
177
178
if (keywords.length === 0) return [];
179
180
var scored = [];
181
for (var j = 0; j < docsIndex.length; j++) {
182
var chunk = docsIndex[j];
183
var titleLower = chunk.title.toLowerCase();
184
var contentLower = chunk.content.toLowerCase();
185
var score = 0;
186
187
for (var k = 0; k < keywords.length; k++) {
188
var kw = keywords[k];
189
// Title matches are weighted higher
190
var titleIdx = titleLower.indexOf(kw);
191
if (titleIdx !== -1) score += 10;
192
193
// Content matches
194
var contentIdx = 0;
195
var searchFrom = 0;
196
while ((contentIdx = contentLower.indexOf(kw, searchFrom)) !== -1) {
197
score += 1;
198
searchFrom = contentIdx + kw.length;
199
}
200
}
201
202
// Bonus for exact phrase match
203
if (contentLower.indexOf(queryLower) !== -1) {
204
score += 20;
205
}
206
207
if (score > 0) {
208
scored.push({ chunk: chunk, score: score });
209
}
210
}
211
212
scored.sort(function (a, b) {
213
return b.score - a.score;
214
});
215
216
// Deduplicate by URL (keep highest scored chunk per page, but allow multiple)
217
var urlCounts = {};
218
var results = [];
219
for (var m = 0; m < scored.length && results.length < MAX_CONTEXT_CHUNKS; m++) {
220
var url = scored[m].chunk.url;
221
urlCounts[url] = (urlCounts[url] || 0) + 1;
222
if (urlCounts[url] <= 3) {
223
results.push(scored[m].chunk);
224
}
225
}
226
227
return results;
228
}
229
230
// ── Send Message ───────────────────────────────────────────────────
231
function handleSend() {
232
var input = document.getElementById("chatbot-input");
233
var message = input.value.trim();
234
if (!message || isGenerating) return;
235
236
var apiKey = getApiKey();
237
if (!apiKey) {
238
appendMessage(
239
"bot",
240
"I'm not configured yet. The Gemini API key is missing. Please check the build configuration."
241
);
242
return;
243
}
244
245
input.value = "";
246
appendMessage("user", message);
247
248
isGenerating = true;
249
setSendEnabled(false);
250
showTypingIndicator();
251
252
// Search for relevant documentation
253
var relevantDocs = searchDocs(message);
254
var context = "";
255
if (relevantDocs.length > 0) {
256
var parts = [];
257
for (var i = 0; i < relevantDocs.length; i++) {
258
var doc = relevantDocs[i];
259
parts.push("## " + doc.title + " (" + doc.url + ")\n" + doc.content);
260
}
261
context = parts.join("\n\n---\n\n");
262
}
263
264
// Add user message to history
265
conversationHistory.push({
266
role: "user",
267
parts: [{ text: message }],
268
});
269
270
// Trim history
271
if (conversationHistory.length > MAX_HISTORY_TURNS) {
272
conversationHistory = conversationHistory.slice(-MAX_HISTORY_TURNS);
273
}
274
275
// Call Gemini
276
callGemini(context, conversationHistory, function (err, response) {
277
hideTypingIndicator();
278
isGenerating = false;
279
setSendEnabled(true);
280
281
if (err) {
282
appendMessage("bot", "Sorry, I encountered an error: " + err + ". Please try again.");
283
return;
284
}
285
286
conversationHistory.push({
287
role: "model",
288
parts: [{ text: response }],
289
});
290
291
appendMessage("bot", response);
292
});
293
}
294
295
// ── Gemini API Call ────────────────────────────────────────────────
296
function callGemini(context, history, callback) {
297
var apiKey = getApiKey();
298
var url = GEMINI_API_URL + "?key=" + apiKey;
299
300
var systemText = SYSTEM_PROMPT;
301
if (context) {
302
systemText +=
303
"\n\n--- RELEVANT DOCUMENTATION ---\n\n" +
304
context +
305
"\n\n--- END DOCUMENTATION ---";
306
}
307
308
var body = {
309
system_instruction: {
310
parts: [{ text: systemText }],
311
},
312
contents: history,
313
generationConfig: {
314
temperature: 0.4,
315
maxOutputTokens: 2048,
316
},
317
};
318
319
var xhr = new XMLHttpRequest();
320
xhr.open("POST", url, true);
321
xhr.setRequestHeader("Content-Type", "application/json");
322
xhr.onreadystatechange = function () {
323
if (xhr.readyState !== 4) return;
324
325
if (xhr.status === 200) {
326
try {
327
var data = JSON.parse(xhr.responseText);
328
if (
329
data.candidates &&
330
data.candidates[0] &&
331
data.candidates[0].content &&
332
data.candidates[0].content.parts &&
333
data.candidates[0].content.parts[0]
334
) {
335
callback(null, data.candidates[0].content.parts[0].text);
336
} else {
337
callback("Unexpected response format");
338
}
339
} catch (e) {
340
callback("Failed to parse response");
341
}
342
} else {
343
var errMsg = "API request failed (status " + xhr.status + ")";
344
try {
345
var errData = JSON.parse(xhr.responseText);
346
if (errData.error && errData.error.message) {
347
errMsg = errData.error.message;
348
}
349
} catch (e) {
350
// ignore parse error
351
}
352
callback(errMsg);
353
}
354
};
355
xhr.onerror = function () {
356
callback("Network error. Please check your connection.");
357
};
358
xhr.send(JSON.stringify(body));
359
}
360
361
// ── UI Helpers ─────────────────────────────────────────────────────
362
function appendMessage(sender, text) {
363
var messages = document.getElementById("chatbot-messages");
364
var msgDiv = document.createElement("div");
365
msgDiv.className = "chatbot-msg chatbot-msg-" + sender;
366
367
var bubble = document.createElement("div");
368
bubble.className = "chatbot-msg-bubble";
369
370
if (sender === "bot") {
371
bubble.innerHTML = renderMarkdown(text);
372
} else {
373
bubble.textContent = text;
374
}
375
376
msgDiv.appendChild(bubble);
377
messages.appendChild(msgDiv);
378
messages.scrollTop = messages.scrollHeight;
379
}
380
381
function showTypingIndicator() {
382
var messages = document.getElementById("chatbot-messages");
383
var existing = document.getElementById("chatbot-typing");
384
if (existing) return;
385
386
var div = document.createElement("div");
387
div.id = "chatbot-typing";
388
div.className = "chatbot-msg chatbot-msg-bot";
389
div.innerHTML =
390
'<div class="chatbot-msg-bubble">' +
391
'<div class="chatbot-typing-indicator">' +
392
"<span></span><span></span><span></span>" +
393
"</div></div>";
394
messages.appendChild(div);
395
messages.scrollTop = messages.scrollHeight;
396
}
397
398
function hideTypingIndicator() {
399
var el = document.getElementById("chatbot-typing");
400
if (el) el.remove();
401
}
402
403
function setSendEnabled(enabled) {
404
var btn = document.getElementById("chatbot-send");
405
if (btn) btn.disabled = !enabled;
406
}
407
408
// ── Markdown Renderer ──────────────────────────────────────────────
409
function renderMarkdown(text) {
410
if (!text) return "";
411
412
// Escape HTML first
413
text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
414
415
// Code blocks (fenced)
416
text = text.replace(
417
/```(\w*)\n([\s\S]*?)```/g,
418
function (match, lang, code) {
419
// Unescape HTML inside code blocks to preserve formatting
420
code = code.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
421
// Then re-escape for display
422
code = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
423
return '<pre><code class="language-' + (lang || "python") + '">' + code + "</code></pre>";
424
}
425
);
426
427
// Inline code
428
text = text.replace(/`([^`\n]+)`/g, "<code>$1</code>");
429
430
// Bold
431
text = text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
432
433
// Italic
434
text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>");
435
436
// Links
437
text = text.replace(
438
/\[([^\]]+)\]\(([^)]+)\)/g,
439
'<a href="$2" target="_blank" rel="noopener">$1</a>'
440
);
441
442
// Unordered lists
443
text = text.replace(/^[\-\*] (.+)/gm, "<li>$1</li>");
444
text = text.replace(/((?:<li>.*<\/li>\n?)+)/g, "<ul>$1</ul>");
445
446
// Ordered lists
447
text = text.replace(/^\d+\. (.+)/gm, "<li>$1</li>");
448
449
// Paragraphs: split on double newlines
450
var blocks = text.split(/\n\n+/);
451
var result = [];
452
for (var i = 0; i < blocks.length; i++) {
453
var block = blocks[i].trim();
454
if (!block) continue;
455
if (
456
block.indexOf("<pre>") === 0 ||
457
block.indexOf("<ul>") === 0 ||
458
block.indexOf("<ol>") === 0 ||
459
block.indexOf("<li>") === 0
460
) {
461
result.push(block);
462
} else {
463
// Convert single newlines to <br> within paragraphs
464
block = block.replace(/\n/g, "<br>");
465
result.push("<p>" + block + "</p>");
466
}
467
}
468
469
return result.join("");
470
}
471
472
// ── Initialize ─────────────────────────────────────────────────────
473
if (document.readyState === "loading") {
474
document.addEventListener("DOMContentLoaded", createWidget);
475
} else {
476
createWidget();
477
}
478
})();
479
480