jekyll-theme-zer0 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +8 -4
- data/_data/backlog.yml +135 -2
- data/_data/features.yml +31 -10
- data/_data/hub.yml +68 -0
- data/_data/hub_index.yml +203 -0
- data/_data/navigation/hub.yml +110 -0
- data/_data/navigation/main.yml +7 -0
- data/_includes/analytics/google-analytics.html +14 -6
- data/_includes/analytics/google-tag-manager-head.html +13 -5
- data/_includes/components/ai-chat.html +143 -346
- data/_includes/core/head.html +4 -2
- data/_layouts/home.html +17 -4
- data/assets/js/ai-chat.js +853 -0
- data/scripts/content-review.rb +22 -4
- data/scripts/lib/hub.rb +208 -0
- data/scripts/provision-org-sites.rb +252 -0
- data/scripts/provision-org-sites.sh +23 -0
- data/scripts/sync-hub-metadata.rb +184 -0
- data/scripts/sync-hub-metadata.sh +22 -0
- metadata +11 -2
|
@@ -1,41 +1,28 @@
|
|
|
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
|
-
Security:
|
|
28
|
-
- API key is injected at build time, not stored in source
|
|
29
|
-
- IMPORTANT: Client-side API keys are visible in page source.
|
|
30
|
-
For production, route requests through a server-side proxy
|
|
31
|
-
by changing the 'endpoint' config to your proxy URL.
|
|
32
|
-
|
|
33
|
-
Accessibility:
|
|
34
|
-
- ARIA labels for screen readers
|
|
35
|
-
- Keyboard navigation support (Escape to close)
|
|
36
|
-
- Focus management when opening/closing the chat panel
|
|
37
|
-
===================================================================
|
|
38
|
-
-->
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
Component: ai-chat
|
|
3
|
+
Path: _includes/components/ai-chat.html
|
|
4
|
+
Purpose: Floating AI chat assistant powered by the Claude Messages
|
|
5
|
+
API, grounded in the current page's content, with optional
|
|
6
|
+
GitHub actions (create issues / propose page-improvement
|
|
7
|
+
pull requests) exposed to the model via Claude tool use.
|
|
8
|
+
Params: none — configured via site.ai_chat in _config.yml
|
|
9
|
+
Depends on: Bootstrap 5 + Bootstrap Icons, assets/js/ai-chat.js,
|
|
10
|
+
design tokens (_sass/tokens/_spacing.scss, _layers.scss),
|
|
11
|
+
site.ai_chat configuration
|
|
12
|
+
Notes:
|
|
13
|
+
- Renders nothing unless ai_chat.enabled AND a usable auth path
|
|
14
|
+
exists: proxy mode with a deployed proxy (proxy_ready: true,
|
|
15
|
+
recommended) or direct mode with a non-empty api_key.
|
|
16
|
+
- Proxy mode keeps the Anthropic key server-side (see
|
|
17
|
+
templates/deploy/chat-proxy/). Direct mode sends the key from the
|
|
18
|
+
browser (visible in page source) — local development only.
|
|
19
|
+
- GitHub actions: github.mode "url" opens pre-filled github.com
|
|
20
|
+
forms (no token anywhere); "proxy" creates issues/PRs through the
|
|
21
|
+
proxy's GitHub endpoints with a server-side token. Creation always
|
|
22
|
+
requires an in-chat confirmation by the user.
|
|
23
|
+
- Accessibility: ARIA dialog semantics, Escape to close, focus
|
|
24
|
+
management, aria-live message log, reduced-motion support.
|
|
25
|
+
{%- endcomment -%}
|
|
39
26
|
|
|
40
27
|
{% assign ai_auth_mode = site.ai_chat.auth_mode | default: 'proxy' %}
|
|
41
28
|
{% comment %}
|
|
@@ -55,6 +42,26 @@
|
|
|
55
42
|
{% endif %}
|
|
56
43
|
{% if ai_render %}
|
|
57
44
|
|
|
45
|
+
{% comment %}
|
|
46
|
+
GitHub actions are on by default in "url" mode (pre-filled github.com
|
|
47
|
+
forms need no token). `default:` can't preserve an explicit false, so
|
|
48
|
+
compute the flag with an if-tag.
|
|
49
|
+
{% endcomment %}
|
|
50
|
+
{% assign gh_enabled = true %}
|
|
51
|
+
{% if site.ai_chat.github.enabled == false %}
|
|
52
|
+
{% assign gh_enabled = false %}
|
|
53
|
+
{% endif %}
|
|
54
|
+
|
|
55
|
+
{% comment %}
|
|
56
|
+
local_edit lets the assistant write the current page's source file via the
|
|
57
|
+
dev proxy. It is a development-only capability — enabled in _config_dev.yml,
|
|
58
|
+
off in production. Compute with an if-tag (default false).
|
|
59
|
+
{% endcomment %}
|
|
60
|
+
{% assign local_edit_flag = false %}
|
|
61
|
+
{% if site.ai_chat.local_edit %}
|
|
62
|
+
{% assign local_edit_flag = true %}
|
|
63
|
+
{% endif %}
|
|
64
|
+
|
|
58
65
|
<!-- AI Chat Toggle Button -->
|
|
59
66
|
<button id="aiChatToggle"
|
|
60
67
|
class="ai-chat-toggle btn btn-primary rounded-circle shadow-lg"
|
|
@@ -62,8 +69,8 @@
|
|
|
62
69
|
aria-label="Open AI chat assistant"
|
|
63
70
|
aria-expanded="false"
|
|
64
71
|
aria-controls="aiChatPanel">
|
|
65
|
-
<i class="{{ site.ai_chat.icon | default: 'bi-robot' }} ai-chat-icon-open" aria-hidden="true"></i>
|
|
66
|
-
<i class="bi-x-lg ai-chat-icon-close d-none" aria-hidden="true"></i>
|
|
72
|
+
<i class="bi {{ site.ai_chat.icon | default: 'bi-robot' }} ai-chat-icon-open" aria-hidden="true"></i>
|
|
73
|
+
<i class="bi bi-x-lg ai-chat-icon-close d-none" aria-hidden="true"></i>
|
|
67
74
|
</button>
|
|
68
75
|
|
|
69
76
|
<!-- AI Chat Panel -->
|
|
@@ -76,14 +83,14 @@
|
|
|
76
83
|
<!-- Chat Header -->
|
|
77
84
|
<div class="ai-chat-header d-flex align-items-center justify-content-between p-3">
|
|
78
85
|
<div class="d-flex align-items-center gap-2">
|
|
79
|
-
<i class="{{ site.ai_chat.icon | default: 'bi-robot' }} fs-5" aria-hidden="true"></i>
|
|
86
|
+
<i class="bi {{ site.ai_chat.icon | default: 'bi-robot' }} fs-5" aria-hidden="true"></i>
|
|
80
87
|
<h6 id="aiChatTitle" class="mb-0 fw-semibold">Zer0 Assistant</h6>
|
|
81
88
|
</div>
|
|
82
89
|
<button type="button"
|
|
83
90
|
class="btn btn-sm btn-outline-light border-0"
|
|
84
91
|
id="aiChatClose"
|
|
85
92
|
aria-label="Close chat">
|
|
86
|
-
<i class="bi-x-lg" aria-hidden="true"></i>
|
|
93
|
+
<i class="bi bi-x-lg" aria-hidden="true"></i>
|
|
87
94
|
</button>
|
|
88
95
|
</div>
|
|
89
96
|
|
|
@@ -92,6 +99,29 @@
|
|
|
92
99
|
<!-- Welcome message inserted by JS -->
|
|
93
100
|
</div>
|
|
94
101
|
|
|
102
|
+
{% if gh_enabled %}
|
|
103
|
+
<!-- Quick actions (GitHub-backed flows) -->
|
|
104
|
+
<div class="ai-chat-chips px-3 pb-2 d-flex gap-2 flex-wrap">
|
|
105
|
+
<button type="button"
|
|
106
|
+
class="ai-chat-chip btn btn-sm btn-outline-secondary rounded-pill"
|
|
107
|
+
data-prompt="I'd like to report a problem with this page.">
|
|
108
|
+
<i class="bi bi-bug" aria-hidden="true"></i> Report an issue
|
|
109
|
+
</button>
|
|
110
|
+
<button type="button"
|
|
111
|
+
class="ai-chat-chip btn btn-sm btn-outline-secondary rounded-pill"
|
|
112
|
+
data-prompt="I have a suggestion to improve this page's content or design.">
|
|
113
|
+
<i class="bi bi-lightbulb" aria-hidden="true"></i> Suggest an improvement
|
|
114
|
+
</button>
|
|
115
|
+
{% if local_edit_flag %}
|
|
116
|
+
<button type="button"
|
|
117
|
+
class="ai-chat-chip btn btn-sm btn-outline-secondary rounded-pill"
|
|
118
|
+
data-prompt="Edit this page directly: ">
|
|
119
|
+
<i class="bi bi-pencil-square" aria-hidden="true"></i> Edit this page
|
|
120
|
+
</button>
|
|
121
|
+
{% endif %}
|
|
122
|
+
</div>
|
|
123
|
+
{% endif %}
|
|
124
|
+
|
|
95
125
|
<!-- Chat Input -->
|
|
96
126
|
<div class="ai-chat-input p-3">
|
|
97
127
|
<form id="aiChatForm" class="d-flex gap-2" autocomplete="off">
|
|
@@ -106,7 +136,7 @@
|
|
|
106
136
|
class="btn btn-primary btn-sm"
|
|
107
137
|
id="aiChatSend"
|
|
108
138
|
aria-label="Send message">
|
|
109
|
-
<i class="bi-send" aria-hidden="true"></i>
|
|
139
|
+
<i class="bi bi-send" aria-hidden="true"></i>
|
|
110
140
|
</button>
|
|
111
141
|
</form>
|
|
112
142
|
<p class="ai-chat-disclaimer text-muted mt-1 mb-0">
|
|
@@ -121,12 +151,14 @@
|
|
|
121
151
|
"page_title": {{ page.title | default: site.title | jsonify }},
|
|
122
152
|
"page_description": {{ page.description | default: page.excerpt | strip_html | default: "" | jsonify }},
|
|
123
153
|
"page_url": {{ page.url | absolute_url | jsonify }},
|
|
154
|
+
"page_path": {{ page.path | default: "" | jsonify }},
|
|
124
155
|
"page_categories": {{ page.categories | default: "" | jsonify }},
|
|
125
156
|
"page_tags": {{ page.tags | default: "" | jsonify }},
|
|
126
157
|
"page_layout": {{ page.layout | default: "" | jsonify }},
|
|
127
158
|
"page_date": {{ page.date | date: "%Y-%m-%d" | default: "" | jsonify }},
|
|
128
159
|
"site_title": {{ site.title | jsonify }},
|
|
129
|
-
"site_description": {{ site.description | strip_html | strip | truncate: 200 | jsonify }}
|
|
160
|
+
"site_description": {{ site.description | strip_html | strip | truncate: 200 | jsonify }},
|
|
161
|
+
"repository": {{ site.repository | join: "" | jsonify }}
|
|
130
162
|
}
|
|
131
163
|
</script>
|
|
132
164
|
|
|
@@ -138,308 +170,49 @@
|
|
|
138
170
|
{% assign context_length = site.ai_chat.context_max_length | default: 2000 %}
|
|
139
171
|
<script id="aiChatPageContent" type="text/plain">{{ page_text | truncate: context_length | replace: '</script>', '<\/script>' }}</script>
|
|
140
172
|
|
|
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
|
-
set(target, prop, value) {
|
|
169
|
-
const result = Reflect.set(target, prop, value);
|
|
170
|
-
|
|
171
|
-
// When the array grows (numeric index or length change), enforce the cap
|
|
172
|
-
if (prop === 'length' || !Number.isNaN(Number(prop))) {
|
|
173
|
-
const excess = target.length - MAX_MESSAGES;
|
|
174
|
-
if (excess > 0) {
|
|
175
|
-
// Remove oldest messages, keep only the most recent MAX_MESSAGES
|
|
176
|
-
target.splice(0, excess);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return result;
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
// DOM Elements
|
|
184
|
-
const toggle = document.getElementById('aiChatToggle');
|
|
185
|
-
const panel = document.getElementById('aiChatPanel');
|
|
186
|
-
const messagesContainer = document.getElementById('aiChatMessages');
|
|
187
|
-
const form = document.getElementById('aiChatForm');
|
|
188
|
-
const input = document.getElementById('aiChatInput');
|
|
189
|
-
const closeBtn = document.getElementById('aiChatClose');
|
|
190
|
-
const iconOpen = toggle.querySelector('.ai-chat-icon-open');
|
|
191
|
-
const iconClose = toggle.querySelector('.ai-chat-icon-close');
|
|
192
|
-
|
|
193
|
-
// Build page context for system message
|
|
194
|
-
function getPageContext() {
|
|
195
|
-
let context = '';
|
|
196
|
-
try {
|
|
197
|
-
const meta = JSON.parse(document.getElementById('aiChatPageContext').textContent);
|
|
198
|
-
const pageContent = document.getElementById('aiChatPageContent').textContent.trim();
|
|
199
|
-
|
|
200
|
-
context = 'Current page context:\n';
|
|
201
|
-
if (meta.page_title) context += '- Title: ' + meta.page_title + '\n';
|
|
202
|
-
if (meta.page_description) context += '- Description: ' + meta.page_description + '\n';
|
|
203
|
-
if (meta.page_url) context += '- URL: ' + meta.page_url + '\n';
|
|
204
|
-
if (meta.page_categories && meta.page_categories.length) {
|
|
205
|
-
context += '- Categories: ' + (Array.isArray(meta.page_categories) ? meta.page_categories.join(', ') : meta.page_categories) + '\n';
|
|
206
|
-
}
|
|
207
|
-
if (meta.page_tags && meta.page_tags.length) {
|
|
208
|
-
context += '- Tags: ' + (Array.isArray(meta.page_tags) ? meta.page_tags.join(', ') : meta.page_tags) + '\n';
|
|
209
|
-
}
|
|
210
|
-
if (meta.page_date) context += '- Date: ' + meta.page_date + '\n';
|
|
211
|
-
if (meta.site_title) context += '- Site: ' + meta.site_title + '\n';
|
|
212
|
-
if (pageContent) context += '\nPage content:\n' + pageContent + '\n';
|
|
213
|
-
} catch (e) {
|
|
214
|
-
console.warn('AI Chat: Could not parse page context', e);
|
|
215
|
-
}
|
|
216
|
-
return context;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Build system message with page context
|
|
220
|
-
function getSystemMessage() {
|
|
221
|
-
const pageContext = getPageContext();
|
|
222
|
-
let systemContent = CONFIG.systemPrompt;
|
|
223
|
-
if (CONFIG.strictContext) {
|
|
224
|
-
systemContent += '\n\nGrounding rules:\n'
|
|
225
|
-
+ '- Answer ONLY using the provided page context.\n'
|
|
226
|
-
+ '- If the answer is not in the page context, reply exactly with: "' + CONFIG.outOfScopeMessage + '"\n'
|
|
227
|
-
+ '- Do not invent facts, links, or features.\n';
|
|
228
|
-
}
|
|
229
|
-
if (pageContext) {
|
|
230
|
-
systemContent += '\n\n' + pageContext;
|
|
231
|
-
}
|
|
232
|
-
return { role: 'system', content: systemContent };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function escapeHtml(text) {
|
|
236
|
-
return String(text)
|
|
237
|
-
.replace(/&/g, '&')
|
|
238
|
-
.replace(/</g, '<')
|
|
239
|
-
.replace(/>/g, '>')
|
|
240
|
-
.replace(/\"/g, '"')
|
|
241
|
-
.replace(/'/g, ''');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function renderAssistantMarkdown(raw) {
|
|
245
|
-
// Escape first, then allow a small markdown subset safely.
|
|
246
|
-
let safe = escapeHtml(raw || '');
|
|
247
|
-
|
|
248
|
-
safe = safe
|
|
249
|
-
.replace(/^###\s+(.+)$/gm, '<strong>$1</strong>')
|
|
250
|
-
.replace(/^##\s+(.+)$/gm, '<strong>$1</strong>')
|
|
251
|
-
.replace(/^#\s+(.+)$/gm, '<strong>$1</strong>')
|
|
252
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
253
|
-
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
254
|
-
.replace(/\[(.+?)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
|
255
|
-
.replace(/^\s*[-*]\s+(.+)$/gm, '• $1')
|
|
256
|
-
.replace(/\n/g, '<br>');
|
|
257
|
-
|
|
258
|
-
return safe;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Create a message element
|
|
262
|
-
function createMessageEl(role, content) {
|
|
263
|
-
const wrapper = document.createElement('div');
|
|
264
|
-
wrapper.className = 'ai-chat-message ai-chat-message--' + role + ' mb-2';
|
|
265
|
-
|
|
266
|
-
const bubble = document.createElement('div');
|
|
267
|
-
bubble.className = 'ai-chat-bubble p-2 rounded-3 small';
|
|
268
|
-
if (role === 'assistant') {
|
|
269
|
-
bubble.innerHTML = renderAssistantMarkdown(content);
|
|
270
|
-
} else {
|
|
271
|
-
bubble.textContent = content;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
wrapper.appendChild(bubble);
|
|
275
|
-
return wrapper;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Create a loading indicator
|
|
279
|
-
function createLoadingEl() {
|
|
280
|
-
const wrapper = document.createElement('div');
|
|
281
|
-
wrapper.className = 'ai-chat-message ai-chat-message--assistant mb-2';
|
|
282
|
-
wrapper.id = 'aiChatLoading';
|
|
283
|
-
|
|
284
|
-
const bubble = document.createElement('div');
|
|
285
|
-
bubble.className = 'ai-chat-bubble p-2 rounded-3 small';
|
|
286
|
-
bubble.innerHTML = '<span class="ai-chat-typing"><span>.</span><span>.</span><span>.</span></span>';
|
|
287
|
-
|
|
288
|
-
wrapper.appendChild(bubble);
|
|
289
|
-
return wrapper;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Append message to chat
|
|
293
|
-
function appendMessage(role, content) {
|
|
294
|
-
const el = createMessageEl(role, content);
|
|
295
|
-
messagesContainer.appendChild(el);
|
|
296
|
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
297
|
-
return el;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Remove loading indicator
|
|
301
|
-
function removeLoading() {
|
|
302
|
-
const loading = document.getElementById('aiChatLoading');
|
|
303
|
-
if (loading) loading.remove();
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Send message to AI
|
|
307
|
-
async function sendMessage(userMessage) {
|
|
308
|
-
if (isLoading || !userMessage.trim()) return;
|
|
309
|
-
|
|
310
|
-
// Enforce cooldown between sends
|
|
311
|
-
const now = Date.now();
|
|
312
|
-
if (now - lastSendTime < SEND_COOLDOWN_MS) return;
|
|
313
|
-
lastSendTime = now;
|
|
314
|
-
|
|
315
|
-
// Add user message to UI and history
|
|
316
|
-
appendMessage('user', userMessage);
|
|
317
|
-
messages.push({ role: 'user', content: userMessage });
|
|
318
|
-
|
|
319
|
-
// Show loading
|
|
320
|
-
isLoading = true;
|
|
321
|
-
input.disabled = true;
|
|
322
|
-
messagesContainer.appendChild(createLoadingEl());
|
|
323
|
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
const response = await fetch(CONFIG.endpoint, {
|
|
327
|
-
method: 'POST',
|
|
328
|
-
headers: (function() {
|
|
329
|
-
const requestHeaders = {
|
|
330
|
-
'Content-Type': 'application/json'
|
|
331
|
-
};
|
|
332
|
-
if (CONFIG.authMode === 'direct' && CONFIG.apiKey) {
|
|
333
|
-
requestHeaders.Authorization = 'Bearer ' + CONFIG.apiKey;
|
|
334
|
-
}
|
|
335
|
-
return requestHeaders;
|
|
336
|
-
})(),
|
|
337
|
-
body: JSON.stringify({
|
|
338
|
-
model: CONFIG.model,
|
|
339
|
-
messages: [getSystemMessage(), ...messages],
|
|
340
|
-
max_tokens: CONFIG.maxTokens,
|
|
341
|
-
temperature: CONFIG.temperature
|
|
342
|
-
})
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
removeLoading();
|
|
346
|
-
|
|
347
|
-
if (!response.ok) {
|
|
348
|
-
const errorData = await response.json().catch(function() { return {}; });
|
|
349
|
-
var statusMessages = {
|
|
350
|
-
401: 'Authentication failed. The API key may be invalid or missing.',
|
|
351
|
-
429: 'Rate limit exceeded. Please wait a moment and try again.',
|
|
352
|
-
500: 'The AI service is temporarily unavailable. Please try again later.',
|
|
353
|
-
503: 'The AI service is temporarily unavailable. Please try again later.'
|
|
354
|
-
};
|
|
355
|
-
throw new Error(errorData.error?.message || statusMessages[response.status] || 'API request failed (' + response.status + ')');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const data = await response.json();
|
|
359
|
-
let assistantMessage = data.choices?.[0]?.message?.content || 'Sorry, I could not generate a response.';
|
|
360
|
-
assistantMessage = String(assistantMessage).trim() || CONFIG.outOfScopeMessage;
|
|
361
|
-
|
|
362
|
-
// Add assistant message to UI and history
|
|
363
|
-
appendMessage('assistant', assistantMessage);
|
|
364
|
-
messages.push({ role: 'assistant', content: assistantMessage });
|
|
365
|
-
|
|
366
|
-
} catch (error) {
|
|
367
|
-
removeLoading();
|
|
368
|
-
appendMessage('assistant', 'Sorry, something went wrong: ' + error.message);
|
|
369
|
-
console.error('AI Chat error:', error);
|
|
370
|
-
} finally {
|
|
371
|
-
isLoading = false;
|
|
372
|
-
input.disabled = false;
|
|
373
|
-
input.focus();
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Toggle chat panel
|
|
378
|
-
function toggleChat() {
|
|
379
|
-
isOpen = !isOpen;
|
|
380
|
-
panel.classList.toggle('ai-chat-panel--open', isOpen);
|
|
381
|
-
panel.setAttribute('aria-hidden', !isOpen);
|
|
382
|
-
toggle.setAttribute('aria-expanded', isOpen);
|
|
383
|
-
iconOpen.classList.toggle('d-none', isOpen);
|
|
384
|
-
iconClose.classList.toggle('d-none', !isOpen);
|
|
385
|
-
|
|
386
|
-
if (isOpen) {
|
|
387
|
-
// Focus input when opening (short delay for CSS transition to start)
|
|
388
|
-
setTimeout(function() { input.focus(); }, 50);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Initialize
|
|
393
|
-
function init() {
|
|
394
|
-
// Show welcome message
|
|
395
|
-
appendMessage('assistant', CONFIG.welcomeMessage);
|
|
396
|
-
|
|
397
|
-
// Event: toggle button
|
|
398
|
-
toggle.addEventListener('click', toggleChat);
|
|
399
|
-
|
|
400
|
-
// Event: close button
|
|
401
|
-
closeBtn.addEventListener('click', toggleChat);
|
|
402
|
-
|
|
403
|
-
// Event: form submit
|
|
404
|
-
form.addEventListener('submit', function(e) {
|
|
405
|
-
e.preventDefault();
|
|
406
|
-
const msg = input.value.trim();
|
|
407
|
-
if (msg) {
|
|
408
|
-
input.value = '';
|
|
409
|
-
sendMessage(msg);
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// Event: Escape key closes chat
|
|
414
|
-
document.addEventListener('keydown', function(e) {
|
|
415
|
-
if (e.key === 'Escape' && isOpen) {
|
|
416
|
-
toggleChat();
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Event: click outside closes chat
|
|
421
|
-
document.addEventListener('click', function(e) {
|
|
422
|
-
if (isOpen && !panel.contains(e.target) && !toggle.contains(e.target)) {
|
|
423
|
-
toggleChat();
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Start when DOM is ready
|
|
429
|
-
if (document.readyState === 'loading') {
|
|
430
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
431
|
-
} else {
|
|
432
|
-
init();
|
|
173
|
+
{% comment %}
|
|
174
|
+
Widget configuration consumed by assets/js/ai-chat.js. The Anthropic
|
|
175
|
+
key appears here only in direct mode (local development); proxy mode
|
|
176
|
+
leaves it empty and the proxy injects credentials server-side.
|
|
177
|
+
{% endcomment %}
|
|
178
|
+
<script id="aiChatConfig" type="application/json">
|
|
179
|
+
{
|
|
180
|
+
"endpoint": {{ site.ai_chat.endpoint | default: "/api/chat" | jsonify }},
|
|
181
|
+
"authMode": {{ ai_auth_mode | jsonify }},
|
|
182
|
+
"apiKey": {{ site.ai_chat.api_key | jsonify }},
|
|
183
|
+
"anthropicVersion": {{ site.ai_chat.anthropic_version | default: "2023-06-01" | jsonify }},
|
|
184
|
+
"model": {{ site.ai_chat.model | default: "claude-opus-4-8" | jsonify }},
|
|
185
|
+
"maxTokens": {{ site.ai_chat.max_tokens | default: 1024 }},
|
|
186
|
+
"strictContext": {{ site.ai_chat.strict_context | default: true | jsonify }},
|
|
187
|
+
"outOfScopeMessage": {{ site.ai_chat.out_of_scope_message | default: "I can only answer from the content on this page." | jsonify }},
|
|
188
|
+
"systemPrompt": {{ site.ai_chat.system_prompt | default: "You are a helpful assistant." | jsonify }},
|
|
189
|
+
"welcomeMessage": {{ site.ai_chat.welcome_message | default: "Hi! Ask me anything about this page." | jsonify }},
|
|
190
|
+
"localEdit": {{ local_edit_flag }},
|
|
191
|
+
"localEditEndpoint": {{ site.ai_chat.local_edit_endpoint | default: "/api/page" | jsonify }},
|
|
192
|
+
"github": {
|
|
193
|
+
"enabled": {{ gh_enabled }},
|
|
194
|
+
"mode": {{ site.ai_chat.github.mode | default: "url" | jsonify }},
|
|
195
|
+
"endpoint": {{ site.ai_chat.github.endpoint | default: "/api/github" | jsonify }},
|
|
196
|
+
"repository": {{ site.repository | join: "" | jsonify }},
|
|
197
|
+
"baseBranch": {{ site.ai_chat.github.base_branch | default: "main" | jsonify }},
|
|
198
|
+
"defaultLabels": {% if site.ai_chat.github.default_labels %}{{ site.ai_chat.github.default_labels | jsonify }}{% else %}[]{% endif %},
|
|
199
|
+
"prBranchPrefix": {{ site.ai_chat.github.pr_branch_prefix | default: "chat/" | jsonify }}
|
|
433
200
|
}
|
|
434
|
-
}
|
|
201
|
+
}
|
|
435
202
|
</script>
|
|
436
203
|
|
|
204
|
+
<script defer src="{{ '/assets/js/ai-chat.js' | relative_url }}"></script>
|
|
205
|
+
|
|
437
206
|
<style>
|
|
438
207
|
/* AI Chat Toggle Button */
|
|
439
208
|
/* FAB stack slot above back-to-top, below the TOC FAB — driven by the
|
|
440
209
|
design tokens (_sass/tokens/_spacing.scss, _layers.scss) so all
|
|
441
210
|
floating buttons share one offset/stacking system. */
|
|
442
|
-
|
|
211
|
+
/* ID selectors (like #backToTopBtn) are required for the fixed-position
|
|
212
|
+
elements: body children otherwise match the higher-specificity
|
|
213
|
+
`.zer0-bg-body > :not(...)` elevation rule in _sass/theme/_backgrounds.scss,
|
|
214
|
+
which forces position: relative. */
|
|
215
|
+
#aiChatToggle {
|
|
443
216
|
position: fixed;
|
|
444
217
|
bottom: calc(var(--zer0-space-fab-offset, 1rem) + var(--zer0-space-fab-size, 3.5rem) + var(--zer0-space-fab-gap, 0.75rem));
|
|
445
218
|
right: var(--zer0-space-fab-offset, 1rem);
|
|
@@ -453,18 +226,18 @@
|
|
|
453
226
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
454
227
|
}
|
|
455
228
|
|
|
456
|
-
|
|
229
|
+
#aiChatToggle:hover {
|
|
457
230
|
transform: scale(1.1);
|
|
458
231
|
}
|
|
459
232
|
|
|
460
233
|
/* AI Chat Panel */
|
|
461
|
-
|
|
234
|
+
#aiChatPanel {
|
|
462
235
|
position: fixed;
|
|
463
236
|
bottom: calc(var(--zer0-space-fab-offset, 1rem) + 2 * (var(--zer0-space-fab-size, 3.5rem) + var(--zer0-space-fab-gap, 0.75rem)));
|
|
464
237
|
right: var(--zer0-space-fab-offset, 1rem);
|
|
465
238
|
z-index: var(--zer0-layer-fab-chat, 1052);
|
|
466
239
|
width: 360px;
|
|
467
|
-
max-height:
|
|
240
|
+
max-height: 540px;
|
|
468
241
|
display: flex;
|
|
469
242
|
flex-direction: column;
|
|
470
243
|
background: var(--bs-body-bg, #212529);
|
|
@@ -475,7 +248,7 @@
|
|
|
475
248
|
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease;
|
|
476
249
|
}
|
|
477
250
|
|
|
478
|
-
.ai-chat-panel--open {
|
|
251
|
+
#aiChatPanel.ai-chat-panel--open {
|
|
479
252
|
opacity: 1;
|
|
480
253
|
visibility: visible;
|
|
481
254
|
transform: translateY(0) scale(1);
|
|
@@ -518,6 +291,30 @@
|
|
|
518
291
|
word-wrap: break-word;
|
|
519
292
|
}
|
|
520
293
|
|
|
294
|
+
/* Quick-action chips */
|
|
295
|
+
.ai-chat-chips {
|
|
296
|
+
flex-shrink: 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.ai-chat-chips .ai-chat-chip {
|
|
300
|
+
font-size: 0.75rem;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Action cards (tool confirmations + created issue/PR links) */
|
|
304
|
+
.ai-chat-action-card {
|
|
305
|
+
background: var(--bs-tertiary-bg, #2b3035);
|
|
306
|
+
border: 1px solid var(--bs-border-color, #495057);
|
|
307
|
+
color: var(--bs-body-color, #dee2e6);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.ai-chat-action-card--resolved {
|
|
311
|
+
opacity: 0.7;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.ai-chat-action-card .ai-chat-action-meta {
|
|
315
|
+
overflow-wrap: anywhere;
|
|
316
|
+
}
|
|
317
|
+
|
|
521
318
|
/* Chat Input */
|
|
522
319
|
.ai-chat-input {
|
|
523
320
|
border-top: 1px solid var(--bs-border-color, #495057);
|
|
@@ -552,14 +349,14 @@
|
|
|
552
349
|
|
|
553
350
|
/* Mobile Responsive */
|
|
554
351
|
@media (max-width: 576px) {
|
|
555
|
-
|
|
352
|
+
#aiChatPanel {
|
|
556
353
|
width: calc(100vw - 2rem);
|
|
557
354
|
right: 1rem;
|
|
558
355
|
bottom: 8rem;
|
|
559
356
|
max-height: 60vh;
|
|
560
357
|
}
|
|
561
358
|
|
|
562
|
-
|
|
359
|
+
#aiChatToggle {
|
|
563
360
|
bottom: 4.5rem;
|
|
564
361
|
right: 1rem;
|
|
565
362
|
}
|
|
@@ -567,8 +364,8 @@
|
|
|
567
364
|
|
|
568
365
|
/* Respect reduced motion */
|
|
569
366
|
@media (prefers-reduced-motion: reduce) {
|
|
570
|
-
|
|
571
|
-
|
|
367
|
+
#aiChatPanel,
|
|
368
|
+
#aiChatToggle,
|
|
572
369
|
.ai-chat-typing span {
|
|
573
370
|
transition: none !important;
|
|
574
371
|
animation: none !important;
|
data/_includes/core/head.html
CHANGED
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
<!-- ANALYTICS AND TRACKING -->
|
|
31
31
|
<!-- ================================ -->
|
|
32
32
|
<!-- Google Tag Manager - Must be in head section for proper tracking -->
|
|
33
|
-
|
|
33
|
+
<!-- Only load in production builds (JEKYLL_ENV=production); the include also guards against dev hostnames -->
|
|
34
|
+
{% if jekyll.environment == "production" %}{% include analytics/google-tag-manager-head.html %}{% endif %}
|
|
34
35
|
|
|
35
36
|
<!-- ================================ -->
|
|
36
37
|
<!-- JAVASCRIPT LIBRARIES -->
|
|
@@ -118,7 +119,8 @@ window.MathJax = {
|
|
|
118
119
|
<!-- ANALYTICS INTEGRATION -->
|
|
119
120
|
<!-- ========================== -->
|
|
120
121
|
<!-- Google Analytics tracking - Configured in _config.yml -->
|
|
121
|
-
|
|
122
|
+
<!-- Only load in production AND when an ID is configured; the include also guards against dev hostnames -->
|
|
123
|
+
{% if jekyll.environment == "production" and site.google_analytics %}{% include analytics/google-analytics.html %}{% endif %}
|
|
122
124
|
|
|
123
125
|
|
|
124
126
|
<!-- ================================ -->
|
data/_layouts/home.html
CHANGED
|
@@ -28,6 +28,13 @@ layout: root
|
|
|
28
28
|
- Set layout: home in page frontmatter
|
|
29
29
|
- Optionally include title: "Page Title" for display
|
|
30
30
|
- Content can include hero sections, feature highlights, etc.
|
|
31
|
+
|
|
32
|
+
Front matter options:
|
|
33
|
+
- title: displayed as <h1> (and used for SEO) when present
|
|
34
|
+
- hide_title:true keep `title` for SEO but suppress the visible <h1>
|
|
35
|
+
(useful when a custom hero provides its own heading)
|
|
36
|
+
- rss_subscribe:false hide the RSS subscribe link on landing/showcase homepages
|
|
37
|
+
Both default to the original behavior, so existing homepages are unaffected.
|
|
31
38
|
|
|
32
39
|
Design Philosophy:
|
|
33
40
|
- Minimal structure allows for maximum content flexibility
|
|
@@ -43,9 +50,12 @@ layout: root
|
|
|
43
50
|
<!-- ========================== -->
|
|
44
51
|
<!-- OPTIONAL PAGE TITLE -->
|
|
45
52
|
<!-- ========================== -->
|
|
46
|
-
<!-- Display title only if specified in page frontmatter -->
|
|
47
|
-
<!-- Usage: Add 'title: "Homepage Title"' to page frontmatter -->
|
|
48
|
-
|
|
53
|
+
<!-- Display title only if specified in page frontmatter. -->
|
|
54
|
+
<!-- Usage: Add 'title: "Homepage Title"' to page frontmatter. -->
|
|
55
|
+
<!-- Landing/showcase pages can keep `title` for SEO but suppress the -->
|
|
56
|
+
<!-- visible heading (e.g. when a custom hero supplies its own <h1>) -->
|
|
57
|
+
<!-- with `hide_title: true`. -->
|
|
58
|
+
{%- if page.title and page.hide_title != true -%}
|
|
49
59
|
<h1 class="page-heading">{{ page.title }}</h1>
|
|
50
60
|
{%- endif -%}
|
|
51
61
|
|
|
@@ -59,8 +69,11 @@ layout: root
|
|
|
59
69
|
<!-- ========================== -->
|
|
60
70
|
<!-- RSS SUBSCRIPTION LINK -->
|
|
61
71
|
<!-- ========================== -->
|
|
62
|
-
<!-- Provides easy access to RSS feed for blog updates -->
|
|
72
|
+
<!-- Provides easy access to RSS feed for blog updates. -->
|
|
73
|
+
<!-- Opt out on landing/showcase homepages with `rss_subscribe: false`. -->
|
|
74
|
+
{%- unless page.rss_subscribe == false -%}
|
|
63
75
|
<p class="rss-subscribe">
|
|
64
76
|
subscribe <a href="{{ "/feed.xml" | relative_url }}">via RSS</a>
|
|
65
77
|
</p>
|
|
78
|
+
{%- endunless -%}
|
|
66
79
|
</div>
|