@farming-labs/astro-theme 0.0.2-beta.15
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.
- package/package.json +75 -0
- package/src/components/DocsContent.astro +39 -0
- package/src/components/DocsLayout.astro +321 -0
- package/src/components/DocsPage.astro +178 -0
- package/src/components/FloatingAIChat.astro +468 -0
- package/src/components/SearchDialog.astro +315 -0
- package/src/components/ThemeToggle.astro +46 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +10 -0
- package/src/lib/renderMarkdown.js +110 -0
- package/src/themes/darksharp.d.ts +4 -0
- package/src/themes/darksharp.js +42 -0
- package/src/themes/default.d.ts +4 -0
- package/src/themes/default.js +42 -0
- package/src/themes/pixel-border.d.ts +4 -0
- package/src/themes/pixel-border.js +38 -0
- package/styles/darksharp-bundle.css +6 -0
- package/styles/darksharp.css +206 -0
- package/styles/docs.css +2176 -0
- package/styles/pixel-border-bundle.css +6 -0
- package/styles/pixel-border.css +606 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
---
|
|
2
|
+
const {
|
|
3
|
+
api = "/api/docs",
|
|
4
|
+
suggestedQuestions = [],
|
|
5
|
+
aiLabel = "AI",
|
|
6
|
+
position = "bottom-right",
|
|
7
|
+
floatingStyle = "panel",
|
|
8
|
+
} = Astro.props;
|
|
9
|
+
|
|
10
|
+
const isFullModal = floatingStyle === "full-modal";
|
|
11
|
+
|
|
12
|
+
const BTN_POSITIONS: Record<string, string> = {
|
|
13
|
+
"bottom-right": "bottom:24px;right:24px",
|
|
14
|
+
"bottom-left": "bottom:24px;left:24px",
|
|
15
|
+
"bottom-center": "bottom:24px;left:50%;transform:translateX(-50%)",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const PANEL_POSITIONS: Record<string, string> = {
|
|
19
|
+
"bottom-right": "bottom:80px;right:24px",
|
|
20
|
+
"bottom-left": "bottom:80px;left:24px",
|
|
21
|
+
"bottom-center": "bottom:80px;left:50%;transform:translateX(-50%)",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getContainerStyle(style: string, pos: string) {
|
|
25
|
+
switch (style) {
|
|
26
|
+
case "modal":
|
|
27
|
+
return "top:50%;left:50%;transform:translate(-50%,-50%);width:min(680px,calc(100vw - 32px));height:min(560px,calc(100vh - 64px))";
|
|
28
|
+
case "popover":
|
|
29
|
+
return `${PANEL_POSITIONS[pos] || PANEL_POSITIONS["bottom-right"]};width:min(360px,calc(100vw - 48px));height:min(400px,calc(100vh - 120px))`;
|
|
30
|
+
case "panel":
|
|
31
|
+
default:
|
|
32
|
+
return `${PANEL_POSITIONS[pos] || PANEL_POSITIONS["bottom-right"]};width:min(400px,calc(100vw - 48px));height:min(500px,calc(100vh - 120px))`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const btnStyle = BTN_POSITIONS[position] || BTN_POSITIONS["bottom-right"];
|
|
37
|
+
const containerStyle = getContainerStyle(floatingStyle, position);
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<div id="fd-float-config" data-api={api} data-ai-label={aiLabel} data-full-modal={isFullModal ? "true" : "false"} style="display:none"></div>
|
|
41
|
+
|
|
42
|
+
{isFullModal ? (
|
|
43
|
+
<Fragment>
|
|
44
|
+
<!-- Full-modal overlay (hidden by default) -->
|
|
45
|
+
<div class="fd-ai-fm-overlay" id="fd-float-overlay" style="display:none">
|
|
46
|
+
<div class="fd-ai-fm-topbar">
|
|
47
|
+
<button id="fd-float-close-top" class="fd-ai-fm-close-btn" aria-label="Close">
|
|
48
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
49
|
+
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
|
|
50
|
+
</svg>
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="fd-ai-fm-messages" id="fd-float-messages"></div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Bottom input bar -->
|
|
57
|
+
<div class="fd-ai-fm-input-bar fd-ai-fm-input-bar--closed" id="fd-float-bar" style={btnStyle}>
|
|
58
|
+
<button id="fd-float-trigger" class="fd-ai-fm-trigger-btn" aria-label={`Ask ${aiLabel}`}>
|
|
59
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
60
|
+
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
|
|
61
|
+
<path d="M20 3v4"/><path d="M22 5h-4"/>
|
|
62
|
+
</svg>
|
|
63
|
+
<span>Ask {aiLabel}</span>
|
|
64
|
+
</button>
|
|
65
|
+
|
|
66
|
+
<div id="fd-float-input-container" class="fd-ai-fm-input-container" style="display:none">
|
|
67
|
+
<div class="fd-ai-fm-input-wrap">
|
|
68
|
+
<textarea id="fd-float-input" class="fd-ai-fm-input" placeholder={`Ask ${aiLabel}`} rows="1"></textarea>
|
|
69
|
+
<button id="fd-float-send" class="fd-ai-fm-send-btn" aria-label="Send" disabled>
|
|
70
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
71
|
+
<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{suggestedQuestions.length > 0 && (
|
|
77
|
+
<div class="fd-ai-fm-suggestions-area" id="fd-float-suggestions">
|
|
78
|
+
<div class="fd-ai-fm-suggestions-label">Try asking:</div>
|
|
79
|
+
<div class="fd-ai-fm-suggestions">
|
|
80
|
+
{suggestedQuestions.map((q: string) => (
|
|
81
|
+
<button class="fd-ai-fm-suggestion" data-question={q}>{q}</button>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
<div class="fd-ai-fm-footer-bar">
|
|
88
|
+
<div class="fd-ai-fm-footer-hint" id="fd-float-footer-hint">
|
|
89
|
+
AI can be inaccurate, please verify the information.
|
|
90
|
+
</div>
|
|
91
|
+
<button class="fd-ai-fm-clear-btn" id="fd-float-clear" style="display:none">
|
|
92
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
93
|
+
<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
|
94
|
+
</svg>
|
|
95
|
+
<span>Clear</span>
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</Fragment>
|
|
101
|
+
) : (
|
|
102
|
+
<Fragment>
|
|
103
|
+
<!-- Panel/Modal/Popover overlay -->
|
|
104
|
+
{(floatingStyle === "modal") && (
|
|
105
|
+
<div class="fd-ai-overlay" id="fd-float-modal-bg" style="display:none"></div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
<!-- Panel/modal/popover container -->
|
|
109
|
+
<div class="fd-ai-dialog" id="fd-float-dialog" style={`display:none;${containerStyle};animation:fd-ai-float-in 200ms ease-out`}>
|
|
110
|
+
<div class="fd-ai-header">
|
|
111
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
112
|
+
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
|
|
113
|
+
</svg>
|
|
114
|
+
<span class="fd-ai-header-title">Ask {aiLabel}</span>
|
|
115
|
+
{floatingStyle === "modal" && <kbd class="fd-ai-esc">ESC</kbd>}
|
|
116
|
+
<button id="fd-float-close" class="fd-ai-close-btn" aria-label="Close">
|
|
117
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
118
|
+
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
|
|
119
|
+
</svg>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div style="display:flex;flex-direction:column;flex:1;min-height:0">
|
|
124
|
+
<div class="fd-ai-messages" id="fd-float-messages">
|
|
125
|
+
<div class="fd-ai-empty">
|
|
126
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
127
|
+
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
|
|
128
|
+
<path d="M20 3v4"/><path d="M22 5h-4"/>
|
|
129
|
+
</svg>
|
|
130
|
+
<div class="fd-ai-empty-title">Ask anything about the docs</div>
|
|
131
|
+
<div class="fd-ai-empty-desc">{aiLabel} will search through the documentation and answer your question.</div>
|
|
132
|
+
{suggestedQuestions.length > 0 && (
|
|
133
|
+
<div class="fd-ai-suggestions" id="fd-float-suggestions">
|
|
134
|
+
{suggestedQuestions.map((q: string) => (
|
|
135
|
+
<button class="fd-ai-suggestion" data-question={q}>
|
|
136
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
137
|
+
<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>
|
|
138
|
+
</svg>
|
|
139
|
+
<span style="flex:1">{q}</span>
|
|
140
|
+
</button>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="fd-ai-chat-footer">
|
|
148
|
+
<div id="fd-float-clear-wrap" style="display:none;justify-content:flex-end;padding-bottom:8px">
|
|
149
|
+
<button id="fd-float-clear" class="fd-ai-clear-btn">Clear chat</button>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="fd-ai-input-wrap">
|
|
152
|
+
<input id="fd-float-input" type="text" placeholder="Ask a question..." class="fd-ai-input" />
|
|
153
|
+
<button id="fd-float-send" class="fd-ai-send-btn" aria-label="Send">
|
|
154
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
155
|
+
<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>
|
|
156
|
+
</svg>
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Floating trigger button -->
|
|
164
|
+
<button id="fd-float-btn" class="fd-ai-floating-btn" style={btnStyle} aria-label={`Ask ${aiLabel}`}>
|
|
165
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
166
|
+
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
|
|
167
|
+
<path d="M20 3v4"/><path d="M22 5h-4"/>
|
|
168
|
+
</svg>
|
|
169
|
+
</button>
|
|
170
|
+
</Fragment>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<script>
|
|
174
|
+
import { highlight } from 'sugar-high';
|
|
175
|
+
|
|
176
|
+
function escapeHtml(s: string): string {
|
|
177
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildCodeBlock(lang: string, code: string): string {
|
|
181
|
+
const trimmed = code.replace(/\n$/, '');
|
|
182
|
+
const highlighted = highlight(trimmed).replace(/<\/span>\n<span/g, '</span><span');
|
|
183
|
+
const langLabel = lang ? `<div class="fd-ai-code-lang">${escapeHtml(lang)}</div>` : '';
|
|
184
|
+
const copyBtn = `<button class="fd-ai-code-copy" onclick="(function(btn){var code=btn.closest('.fd-ai-code-block').querySelector('code').textContent;navigator.clipboard.writeText(code).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy'},1500)})})(this)">Copy</button>`;
|
|
185
|
+
return `<div class="fd-ai-code-block"><div class="fd-ai-code-header">${langLabel}${copyBtn}</div><pre><code>${highlighted}</code></pre></div>`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isTableRow(line: string): boolean {
|
|
189
|
+
const t = line.trim();
|
|
190
|
+
return t.startsWith('|') && t.endsWith('|') && t.includes('|');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isTableSeparator(line: string): boolean {
|
|
194
|
+
return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderTable(rows: string[]): string {
|
|
198
|
+
const parseRow = (row: string) =>
|
|
199
|
+
row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim());
|
|
200
|
+
const headerCells = parseRow(rows[0]);
|
|
201
|
+
const thead = `<thead><tr>${headerCells.map(c => `<th>${c}</th>`).join('')}</tr></thead>`;
|
|
202
|
+
const bodyRows = rows.slice(1).map(row => {
|
|
203
|
+
const cells = parseRow(row);
|
|
204
|
+
return `<tr>${cells.map(c => `<td>${c}</td>`).join('')}</tr>`;
|
|
205
|
+
}).join('');
|
|
206
|
+
return `<table>${thead}<tbody>${bodyRows}</tbody></table>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function renderMarkdown(text: string): string {
|
|
210
|
+
if (!text) return '';
|
|
211
|
+
const codeBlocks: string[] = [];
|
|
212
|
+
let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code) => {
|
|
213
|
+
codeBlocks.push(buildCodeBlock(lang, code));
|
|
214
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
215
|
+
});
|
|
216
|
+
processed = processed.replace(/```(\w*)\n([\s\S]*)$/, (_m, lang, code) => {
|
|
217
|
+
codeBlocks.push(buildCodeBlock(lang, code));
|
|
218
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
219
|
+
});
|
|
220
|
+
const lines = processed.split('\n');
|
|
221
|
+
const output: string[] = [];
|
|
222
|
+
let i = 0;
|
|
223
|
+
while (i < lines.length) {
|
|
224
|
+
if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
|
225
|
+
const tableLines = [lines[i]];
|
|
226
|
+
i += 2;
|
|
227
|
+
while (i < lines.length && isTableRow(lines[i])) { tableLines.push(lines[i]); i++; }
|
|
228
|
+
output.push(renderTable(tableLines));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
output.push(lines[i]);
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
let result = output.join('\n');
|
|
235
|
+
result = result
|
|
236
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
237
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
238
|
+
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
|
|
239
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
240
|
+
.replace(/^### (.*$)/gm, '<h4>$1</h4>')
|
|
241
|
+
.replace(/^## (.*$)/gm, '<h3>$1</h3>')
|
|
242
|
+
.replace(/^# (.*$)/gm, '<h2>$1</h2>')
|
|
243
|
+
.replace(/^[-*] (.*$)/gm, '<div style="display:flex;gap:8px;padding:2px 0"><span style="opacity:0.5">\u2022</span><span>$1</span></div>')
|
|
244
|
+
.replace(/^(\d+)\. (.*$)/gm, '<div style="display:flex;gap:8px;padding:2px 0"><span style="opacity:0.5">$1.</span><span>$2</span></div>')
|
|
245
|
+
.replace(/\n\n/g, '<div style="height:8px"></div>')
|
|
246
|
+
.replace(/\n/g, '<br>');
|
|
247
|
+
result = result.replace(/\x00CB(\d+)\x00/g, (_m, idx) => codeBlocks[Number(idx)]);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const THINKING_HTML_FM = `<div class="fd-ai-fm-thinking"><span class="fd-ai-fm-thinking-dot"></span><span class="fd-ai-fm-thinking-dot"></span><span class="fd-ai-fm-thinking-dot"></span></div>`;
|
|
252
|
+
const THINKING_HTML = `<span class="fd-ai-loading"><span class="fd-ai-loading-text">Thinking</span><span class="fd-ai-loading-dots"><span class="fd-ai-loading-dot"></span><span class="fd-ai-loading-dot"></span><span class="fd-ai-loading-dot"></span></span></span>`;
|
|
253
|
+
|
|
254
|
+
function initFloatingAI() {
|
|
255
|
+
const cfgEl = document.getElementById('fd-float-config');
|
|
256
|
+
if (!cfgEl) return;
|
|
257
|
+
const api = cfgEl.dataset.api || '/api/docs';
|
|
258
|
+
const aiLabel = cfgEl.dataset.aiLabel || 'AI';
|
|
259
|
+
const isFullModal = cfgEl.dataset.fullModal === 'true';
|
|
260
|
+
|
|
261
|
+
let messages: Array<{ role: string; content: string }> = [];
|
|
262
|
+
let isStreaming = false;
|
|
263
|
+
let isOpen = false;
|
|
264
|
+
let renderScheduled = false;
|
|
265
|
+
|
|
266
|
+
const overlay = document.getElementById('fd-float-overlay');
|
|
267
|
+
const bar = document.getElementById('fd-float-bar');
|
|
268
|
+
const trigger = document.getElementById('fd-float-trigger');
|
|
269
|
+
const closeTop = document.getElementById('fd-float-close-top');
|
|
270
|
+
const inputContainer = document.getElementById('fd-float-input-container');
|
|
271
|
+
const input = document.getElementById('fd-float-input') as HTMLInputElement | HTMLTextAreaElement | null;
|
|
272
|
+
const sendBtn = document.getElementById('fd-float-send') as HTMLButtonElement | null;
|
|
273
|
+
const messagesEl = document.getElementById('fd-float-messages');
|
|
274
|
+
const suggestionsEl = document.getElementById('fd-float-suggestions');
|
|
275
|
+
const clearBtn = document.getElementById('fd-float-clear');
|
|
276
|
+
const footerHint = document.getElementById('fd-float-footer-hint');
|
|
277
|
+
const floatBtn = document.getElementById('fd-float-btn');
|
|
278
|
+
const floatDialog = document.getElementById('fd-float-dialog');
|
|
279
|
+
const floatClose = document.getElementById('fd-float-close');
|
|
280
|
+
const modalBg = document.getElementById('fd-float-modal-bg');
|
|
281
|
+
const clearWrap = document.getElementById('fd-float-clear-wrap');
|
|
282
|
+
|
|
283
|
+
function openChat() {
|
|
284
|
+
isOpen = true;
|
|
285
|
+
if (isFullModal) {
|
|
286
|
+
if (overlay) overlay.style.display = '';
|
|
287
|
+
if (bar) { bar.classList.remove('fd-ai-fm-input-bar--closed'); bar.classList.add('fd-ai-fm-input-bar--open'); bar.removeAttribute('style'); }
|
|
288
|
+
if (trigger) trigger.style.display = 'none';
|
|
289
|
+
if (inputContainer) inputContainer.style.display = '';
|
|
290
|
+
document.body.style.overflow = 'hidden';
|
|
291
|
+
setTimeout(() => input?.focus(), 100);
|
|
292
|
+
} else {
|
|
293
|
+
if (floatDialog) floatDialog.style.display = '';
|
|
294
|
+
if (floatBtn) floatBtn.style.display = 'none';
|
|
295
|
+
if (modalBg) modalBg.style.display = '';
|
|
296
|
+
setTimeout(() => input?.focus(), 100);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function closeChat() {
|
|
301
|
+
isOpen = false;
|
|
302
|
+
if (isFullModal) {
|
|
303
|
+
if (overlay) overlay.style.display = 'none';
|
|
304
|
+
if (bar) { bar.classList.remove('fd-ai-fm-input-bar--open'); bar.classList.add('fd-ai-fm-input-bar--closed'); bar.style.cssText = bar.dataset.btnStyle || ''; }
|
|
305
|
+
if (trigger) trigger.style.display = '';
|
|
306
|
+
if (inputContainer) inputContainer.style.display = 'none';
|
|
307
|
+
document.body.style.overflow = '';
|
|
308
|
+
} else {
|
|
309
|
+
if (floatDialog) floatDialog.style.display = 'none';
|
|
310
|
+
if (floatBtn) floatBtn.style.display = '';
|
|
311
|
+
if (modalBg) modalBg.style.display = 'none';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function scheduleRender() {
|
|
316
|
+
if (renderScheduled) return;
|
|
317
|
+
renderScheduled = true;
|
|
318
|
+
requestAnimationFrame(() => {
|
|
319
|
+
renderScheduled = false;
|
|
320
|
+
doRender();
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function doRender() {
|
|
325
|
+
if (!messagesEl) return;
|
|
326
|
+
if (messages.length === 0) {
|
|
327
|
+
if (isFullModal) {
|
|
328
|
+
messagesEl.innerHTML = '';
|
|
329
|
+
} else {
|
|
330
|
+
messagesEl.innerHTML = `<div class="fd-ai-empty"><div class="fd-ai-empty-title">Ask anything about the docs</div><div class="fd-ai-empty-desc">${aiLabel} will search and answer your question.</div></div>`;
|
|
331
|
+
}
|
|
332
|
+
if (suggestionsEl) suggestionsEl.style.display = '';
|
|
333
|
+
if (clearBtn) clearBtn.style.display = 'none';
|
|
334
|
+
if (footerHint) footerHint.style.display = '';
|
|
335
|
+
if (clearWrap) clearWrap.style.display = 'none';
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (suggestionsEl) suggestionsEl.style.display = 'none';
|
|
339
|
+
if (clearBtn) clearBtn.style.display = '';
|
|
340
|
+
if (footerHint) footerHint.style.display = 'none';
|
|
341
|
+
if (clearWrap) clearWrap.style.display = 'flex';
|
|
342
|
+
|
|
343
|
+
const thinkingHtml = isFullModal ? THINKING_HTML_FM : THINKING_HTML;
|
|
344
|
+
messagesEl.innerHTML = messages.map(m => {
|
|
345
|
+
const label = m.role === 'user' ? 'You' : aiLabel;
|
|
346
|
+
const renderedContent = m.role === 'user'
|
|
347
|
+
? escapeHtml(m.content)
|
|
348
|
+
: (m.content ? renderMarkdown(m.content) : thinkingHtml);
|
|
349
|
+
const bubbleClass = isFullModal
|
|
350
|
+
? 'fd-ai-fm-msg-content'
|
|
351
|
+
: (m.role === 'user' ? 'fd-ai-bubble-user' : 'fd-ai-bubble-ai');
|
|
352
|
+
return `<div class="${isFullModal ? 'fd-ai-fm-msg' : 'fd-ai-msg'}" data-role="${m.role}"><div class="${isFullModal ? 'fd-ai-fm-msg-label' : 'fd-ai-msg-label'}" data-role="${m.role}">${label}</div><div class="${bubbleClass}">${renderedContent}</div></div>`;
|
|
353
|
+
}).join('');
|
|
354
|
+
|
|
355
|
+
if (isFullModal) {
|
|
356
|
+
messagesEl.scrollTo({ top: messagesEl.scrollHeight, behavior: 'smooth' });
|
|
357
|
+
} else {
|
|
358
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function submitQuestion(question: string) {
|
|
363
|
+
if (!question.trim() || isStreaming) return;
|
|
364
|
+
isStreaming = true;
|
|
365
|
+
messages.push({ role: 'user', content: question });
|
|
366
|
+
messages.push({ role: 'assistant', content: '' });
|
|
367
|
+
if (input) input.value = '';
|
|
368
|
+
doRender();
|
|
369
|
+
updateSendBtn();
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const res = await fetch(api, {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
375
|
+
body: JSON.stringify({ messages: messages.filter(m => m.content).map(m => ({ role: m.role, content: m.content })) }),
|
|
376
|
+
});
|
|
377
|
+
if (!res.ok) {
|
|
378
|
+
let errMsg = 'Something went wrong.';
|
|
379
|
+
try { const err = await res.json(); errMsg = err.error || errMsg; } catch {}
|
|
380
|
+
messages[messages.length - 1].content = errMsg;
|
|
381
|
+
doRender();
|
|
382
|
+
isStreaming = false;
|
|
383
|
+
updateSendBtn();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const reader = res.body!.getReader();
|
|
387
|
+
const decoder = new TextDecoder();
|
|
388
|
+
let buffer = '';
|
|
389
|
+
let assistantContent = '';
|
|
390
|
+
while (true) {
|
|
391
|
+
const { done, value } = await reader.read();
|
|
392
|
+
if (done) break;
|
|
393
|
+
buffer += decoder.decode(value, { stream: true });
|
|
394
|
+
const lines = buffer.split('\n');
|
|
395
|
+
buffer = lines.pop() || '';
|
|
396
|
+
for (const line of lines) {
|
|
397
|
+
if (line.startsWith('data: ')) {
|
|
398
|
+
const data = line.slice(6).trim();
|
|
399
|
+
if (data === '[DONE]') continue;
|
|
400
|
+
try {
|
|
401
|
+
const json = JSON.parse(data);
|
|
402
|
+
const content = json.choices?.[0]?.delta?.content;
|
|
403
|
+
if (content) {
|
|
404
|
+
assistantContent += content;
|
|
405
|
+
messages[messages.length - 1].content = assistantContent;
|
|
406
|
+
scheduleRender();
|
|
407
|
+
}
|
|
408
|
+
} catch {}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
messages[messages.length - 1].content = messages[messages.length - 1].content || 'Failed to connect.';
|
|
414
|
+
doRender();
|
|
415
|
+
}
|
|
416
|
+
isStreaming = false;
|
|
417
|
+
doRender();
|
|
418
|
+
updateSendBtn();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function updateSendBtn() {
|
|
422
|
+
if (sendBtn) {
|
|
423
|
+
const val = input ? input.value.trim() : '';
|
|
424
|
+
sendBtn.disabled = !val || isStreaming;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
trigger?.addEventListener('click', openChat);
|
|
429
|
+
floatBtn?.addEventListener('click', openChat);
|
|
430
|
+
closeTop?.addEventListener('click', closeChat);
|
|
431
|
+
floatClose?.addEventListener('click', closeChat);
|
|
432
|
+
modalBg?.addEventListener('click', closeChat);
|
|
433
|
+
overlay?.addEventListener('click', (e) => { if (e.target === overlay) closeChat(); });
|
|
434
|
+
|
|
435
|
+
if (bar) bar.dataset.btnStyle = bar.style.cssText;
|
|
436
|
+
|
|
437
|
+
sendBtn?.addEventListener('click', () => {
|
|
438
|
+
if (input?.value) submitQuestion(input.value);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
input?.addEventListener('keydown', (e) => {
|
|
442
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
if (input.value.trim()) submitQuestion(input.value);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
input?.addEventListener('input', updateSendBtn);
|
|
449
|
+
|
|
450
|
+
clearBtn?.addEventListener('click', () => {
|
|
451
|
+
if (!isStreaming) { messages = []; doRender(); }
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
document.querySelectorAll('[data-question]').forEach(btn => {
|
|
455
|
+
btn.addEventListener('click', () => {
|
|
456
|
+
const q = btn.getAttribute('data-question');
|
|
457
|
+
if (q) submitQuestion(q);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
document.addEventListener('keydown', (e) => {
|
|
462
|
+
if (e.key === 'Escape' && isOpen) closeChat();
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
initFloatingAI();
|
|
467
|
+
document.addEventListener('astro:after-swap', initFloatingAI);
|
|
468
|
+
</script>
|