@farming-labs/theme 0.0.2-beta.4

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.
Files changed (41) hide show
  1. package/dist/_virtual/_rolldown/runtime.mjs +7 -0
  2. package/dist/ai-search-dialog.d.mts +37 -0
  3. package/dist/ai-search-dialog.mjs +937 -0
  4. package/dist/darksharp/index.d.mts +97 -0
  5. package/dist/darksharp/index.mjs +111 -0
  6. package/dist/default/index.d.mts +97 -0
  7. package/dist/default/index.mjs +110 -0
  8. package/dist/docs-ai-features.d.mts +23 -0
  9. package/dist/docs-ai-features.mjs +81 -0
  10. package/dist/docs-api.d.mts +68 -0
  11. package/dist/docs-api.mjs +204 -0
  12. package/dist/docs-layout.d.mts +33 -0
  13. package/dist/docs-layout.mjs +331 -0
  14. package/dist/docs-page-client.d.mts +46 -0
  15. package/dist/docs-page-client.mjs +128 -0
  16. package/dist/index.d.mts +11 -0
  17. package/dist/index.mjs +12 -0
  18. package/dist/mdx.d.mts +38 -0
  19. package/dist/mdx.mjs +27 -0
  20. package/dist/page-actions.d.mts +21 -0
  21. package/dist/page-actions.mjs +155 -0
  22. package/dist/pixel-border/index.d.mts +87 -0
  23. package/dist/pixel-border/index.mjs +95 -0
  24. package/dist/provider.d.mts +14 -0
  25. package/dist/provider.mjs +29 -0
  26. package/dist/search.d.mts +34 -0
  27. package/dist/search.mjs +36 -0
  28. package/dist/serialize-icon.d.mts +4 -0
  29. package/dist/serialize-icon.mjs +16 -0
  30. package/dist/theme.d.mts +2 -0
  31. package/dist/theme.mjs +3 -0
  32. package/package.json +90 -0
  33. package/styles/ai.css +894 -0
  34. package/styles/base.css +298 -0
  35. package/styles/darksharp.css +433 -0
  36. package/styles/default.css +88 -0
  37. package/styles/fumadocs.css +2 -0
  38. package/styles/pixel-border.css +671 -0
  39. package/styles/presets/base.css +14 -0
  40. package/styles/presets/black.css +14 -0
  41. package/styles/presets/neutral.css +14 -0
@@ -0,0 +1,937 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { highlight } from "sugar-high";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+
8
+ //#region src/ai-search-dialog.tsx
9
+ /**
10
+ * Enhanced search dialog with "Ask AI" tab + floating chat widget.
11
+ *
12
+ * All visual styles live in CSS (`ai.css` + theme overrides) using `fd-ai-*`
13
+ * class names. Each theme (default, darksharp, pixel-border) provides its
14
+ * own variant so the AI UI matches the rest of the docs.
15
+ *
16
+ * Two modes:
17
+ * - `mode="search"` (default): AI tab inside the Cmd+K search dialog
18
+ * - `mode="floating"`: Standalone floating chat widget with configurable position
19
+ */
20
+ function buildCodeBlock(lang, code) {
21
+ const highlighted = highlight(code.replace(/\n$/, "")).replace(/<\/span>\n<span/g, "</span><span");
22
+ return `<div class="fd-ai-code-block"><div class="fd-ai-code-header">${lang ? `<div class="fd-ai-code-lang">${escapeHtml(lang)}</div>` : ""}<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></div><pre><code>${highlighted}</code></pre></div>`;
23
+ }
24
+ function renderMarkdown(text) {
25
+ const codeBlocks = [];
26
+ let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
27
+ codeBlocks.push(buildCodeBlock(lang, code));
28
+ return `\x00CB${codeBlocks.length - 1}\x00`;
29
+ });
30
+ processed = processed.replace(/```(\w*)\n([\s\S]*)$/, (_match, lang, code) => {
31
+ codeBlocks.push(buildCodeBlock(lang, code));
32
+ return `\x00CB${codeBlocks.length - 1}\x00`;
33
+ });
34
+ const lines = processed.split("\n");
35
+ const output = [];
36
+ let i = 0;
37
+ while (i < lines.length) {
38
+ if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
39
+ const tableLines = [lines[i]];
40
+ i++;
41
+ i++;
42
+ while (i < lines.length && isTableRow(lines[i])) {
43
+ tableLines.push(lines[i]);
44
+ i++;
45
+ }
46
+ output.push(renderTable(tableLines));
47
+ continue;
48
+ }
49
+ output.push(lines[i]);
50
+ i++;
51
+ }
52
+ let result = output.join("\n");
53
+ result = result.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<a href=\"$2\">$1</a>").replace(/^### (.*$)/gm, "<h4>$1</h4>").replace(/^## (.*$)/gm, "<h3>$1</h3>").replace(/^# (.*$)/gm, "<h2>$1</h2>").replace(/^[-*] (.*$)/gm, "<div style=\"display:flex;gap:8px;padding:2px 0\"><span style=\"opacity:0.5\">•</span><span>$1</span></div>").replace(/^(\d+)\. (.*$)/gm, "<div style=\"display:flex;gap:8px;padding:2px 0\"><span style=\"opacity:0.5\">$1.</span><span>$2</span></div>").replace(/\n\n/g, "<div style=\"height:8px\"></div>").replace(/\n/g, "<br>");
54
+ result = result.replace(/\x00CB(\d+)\x00/g, (_m, idx) => codeBlocks[Number(idx)]);
55
+ return result;
56
+ }
57
+ function escapeHtml(s) {
58
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
59
+ }
60
+ function isTableRow(line) {
61
+ const trimmed = line.trim();
62
+ return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
63
+ }
64
+ function isTableSeparator(line) {
65
+ return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
66
+ }
67
+ function renderTable(rows) {
68
+ const parseRow = (row) => row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
69
+ return `<table>${`<thead><tr>${parseRow(rows[0]).map((c) => `<th>${c}</th>`).join("")}</tr></thead>`}<tbody>${rows.slice(1).map((row) => {
70
+ return `<tr>${parseRow(row).map((c) => `<td>${c}</td>`).join("")}</tr>`;
71
+ }).join("")}</tbody></table>`;
72
+ }
73
+ function SearchIcon() {
74
+ return /* @__PURE__ */ jsxs("svg", {
75
+ width: "16",
76
+ height: "16",
77
+ viewBox: "0 0 24 24",
78
+ fill: "none",
79
+ stroke: "currentColor",
80
+ strokeWidth: "2",
81
+ strokeLinecap: "round",
82
+ strokeLinejoin: "round",
83
+ children: [/* @__PURE__ */ jsx("circle", {
84
+ cx: "11",
85
+ cy: "11",
86
+ r: "8"
87
+ }), /* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })]
88
+ });
89
+ }
90
+ function SparklesIcon({ size = 16 }) {
91
+ return /* @__PURE__ */ jsxs("svg", {
92
+ width: size,
93
+ height: size,
94
+ viewBox: "0 0 24 24",
95
+ fill: "none",
96
+ stroke: "currentColor",
97
+ strokeWidth: "2",
98
+ strokeLinecap: "round",
99
+ strokeLinejoin: "round",
100
+ children: [
101
+ /* @__PURE__ */ jsx("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" }),
102
+ /* @__PURE__ */ jsx("path", { d: "M20 3v4" }),
103
+ /* @__PURE__ */ jsx("path", { d: "M22 5h-4" })
104
+ ]
105
+ });
106
+ }
107
+ function FileIcon() {
108
+ return /* @__PURE__ */ jsxs("svg", {
109
+ width: "14",
110
+ height: "14",
111
+ viewBox: "0 0 24 24",
112
+ fill: "none",
113
+ stroke: "currentColor",
114
+ strokeWidth: "2",
115
+ strokeLinecap: "round",
116
+ strokeLinejoin: "round",
117
+ children: [/* @__PURE__ */ jsx("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }), /* @__PURE__ */ jsx("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })]
118
+ });
119
+ }
120
+ function ArrowUpIcon() {
121
+ return /* @__PURE__ */ jsxs("svg", {
122
+ width: "16",
123
+ height: "16",
124
+ viewBox: "0 0 24 24",
125
+ fill: "none",
126
+ stroke: "currentColor",
127
+ strokeWidth: "2",
128
+ strokeLinecap: "round",
129
+ strokeLinejoin: "round",
130
+ children: [/* @__PURE__ */ jsx("path", { d: "m5 12 7-7 7 7" }), /* @__PURE__ */ jsx("path", { d: "M12 19V5" })]
131
+ });
132
+ }
133
+ function XIcon() {
134
+ return /* @__PURE__ */ jsxs("svg", {
135
+ width: "16",
136
+ height: "16",
137
+ viewBox: "0 0 24 24",
138
+ fill: "none",
139
+ stroke: "currentColor",
140
+ strokeWidth: "2",
141
+ strokeLinecap: "round",
142
+ strokeLinejoin: "round",
143
+ children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
144
+ });
145
+ }
146
+ function DefaultLoadingIndicator({ label }) {
147
+ return /* @__PURE__ */ jsxs("span", {
148
+ className: "fd-ai-loading",
149
+ children: [/* @__PURE__ */ jsxs("span", {
150
+ className: "fd-ai-loading-text",
151
+ children: [label, " is thinking"]
152
+ }), /* @__PURE__ */ jsxs("span", {
153
+ className: "fd-ai-loading-dots",
154
+ children: [
155
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" }),
156
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" }),
157
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" })
158
+ ]
159
+ })]
160
+ });
161
+ }
162
+ function LoadingDots() {
163
+ return /* @__PURE__ */ jsxs("span", {
164
+ className: "fd-ai-loading-dots",
165
+ children: [
166
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" }),
167
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" }),
168
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" })
169
+ ]
170
+ });
171
+ }
172
+ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loadingComponentHtml }) {
173
+ const label = aiLabel || "AI";
174
+ const aiInputRef = useRef(null);
175
+ const messagesEndRef = useRef(null);
176
+ useEffect(() => {
177
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
178
+ }, [messages]);
179
+ useEffect(() => {
180
+ aiInputRef.current?.focus();
181
+ }, []);
182
+ const submitQuestion = useCallback(async (question) => {
183
+ if (!question.trim() || isStreaming) return;
184
+ const userMessage = {
185
+ role: "user",
186
+ content: question
187
+ };
188
+ const newMessages = [...messages, userMessage];
189
+ setAiInput("");
190
+ setIsStreaming(true);
191
+ setMessages([...newMessages, {
192
+ role: "assistant",
193
+ content: ""
194
+ }]);
195
+ try {
196
+ const res = await fetch(api, {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify({ messages: newMessages.map((m) => ({
200
+ role: m.role,
201
+ content: m.content
202
+ })) })
203
+ });
204
+ if (!res.ok) {
205
+ let errMsg = "Something went wrong.";
206
+ try {
207
+ errMsg = (await res.json()).error || errMsg;
208
+ } catch {}
209
+ setMessages([...newMessages, {
210
+ role: "assistant",
211
+ content: errMsg
212
+ }]);
213
+ setIsStreaming(false);
214
+ return;
215
+ }
216
+ const reader = res.body.getReader();
217
+ const decoder = new TextDecoder();
218
+ let buffer = "";
219
+ let assistantContent = "";
220
+ while (true) {
221
+ const { done, value } = await reader.read();
222
+ if (done) break;
223
+ buffer += decoder.decode(value, { stream: true });
224
+ const lines = buffer.split("\n");
225
+ buffer = lines.pop() || "";
226
+ for (const line of lines) if (line.startsWith("data: ")) {
227
+ const data = line.slice(6).trim();
228
+ if (data === "[DONE]") continue;
229
+ try {
230
+ const content = JSON.parse(data).choices?.[0]?.delta?.content;
231
+ if (content) {
232
+ assistantContent += content;
233
+ setMessages([...newMessages, {
234
+ role: "assistant",
235
+ content: assistantContent
236
+ }]);
237
+ }
238
+ } catch {}
239
+ }
240
+ }
241
+ if (assistantContent) setMessages([...newMessages, {
242
+ role: "assistant",
243
+ content: assistantContent
244
+ }]);
245
+ } catch {
246
+ setMessages([...newMessages, {
247
+ role: "assistant",
248
+ content: "Failed to connect. Please try again."
249
+ }]);
250
+ }
251
+ setIsStreaming(false);
252
+ }, [
253
+ messages,
254
+ api,
255
+ isStreaming,
256
+ setMessages,
257
+ setAiInput,
258
+ setIsStreaming
259
+ ]);
260
+ const handleAskAI = useCallback(async () => {
261
+ await submitQuestion(aiInput);
262
+ }, [aiInput, submitQuestion]);
263
+ const handleAIKeyDown = (e) => {
264
+ if (e.key === "Enter" && !e.shiftKey) {
265
+ e.preventDefault();
266
+ handleAskAI();
267
+ }
268
+ };
269
+ const canSend = !!(aiInput.trim() && !isStreaming);
270
+ return /* @__PURE__ */ jsxs("div", {
271
+ style: {
272
+ display: "flex",
273
+ flexDirection: "column",
274
+ flex: 1,
275
+ minHeight: 0
276
+ },
277
+ children: [/* @__PURE__ */ jsxs("div", {
278
+ className: "fd-ai-messages",
279
+ children: [messages.length === 0 ? /* @__PURE__ */ jsxs("div", {
280
+ className: "fd-ai-empty",
281
+ children: [
282
+ /* @__PURE__ */ jsx(SparklesIcon, { size: 20 }),
283
+ /* @__PURE__ */ jsx("div", {
284
+ className: "fd-ai-empty-title",
285
+ children: "Ask anything about the docs"
286
+ }),
287
+ /* @__PURE__ */ jsxs("div", {
288
+ className: "fd-ai-empty-desc",
289
+ children: [label, " will search through the documentation and answer your question with relevant context."]
290
+ }),
291
+ suggestedQuestions && suggestedQuestions.length > 0 && /* @__PURE__ */ jsx("div", {
292
+ className: "fd-ai-suggestions",
293
+ children: suggestedQuestions.map((q, i) => /* @__PURE__ */ jsxs("button", {
294
+ onClick: () => submitQuestion(q),
295
+ className: "fd-ai-suggestion",
296
+ children: [/* @__PURE__ */ jsx(ArrowUpIcon, {}), /* @__PURE__ */ jsx("span", {
297
+ style: { flex: 1 },
298
+ children: q
299
+ })]
300
+ }, i))
301
+ })
302
+ ]
303
+ }) : messages.map((msg, i) => /* @__PURE__ */ jsxs("div", {
304
+ className: "fd-ai-msg",
305
+ "data-role": msg.role,
306
+ children: [/* @__PURE__ */ jsx("div", {
307
+ className: "fd-ai-msg-label",
308
+ children: msg.role === "user" ? "You" : label
309
+ }), msg.role === "user" ? /* @__PURE__ */ jsx("div", {
310
+ className: "fd-ai-bubble-user",
311
+ children: msg.content
312
+ }) : /* @__PURE__ */ jsx("div", {
313
+ className: "fd-ai-bubble-ai",
314
+ children: msg.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) } }) : loadingComponentHtml ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: loadingComponentHtml } }) : /* @__PURE__ */ jsx(DefaultLoadingIndicator, { label })
315
+ })]
316
+ }, i)), /* @__PURE__ */ jsx("div", { ref: messagesEndRef })]
317
+ }), /* @__PURE__ */ jsxs("div", {
318
+ className: "fd-ai-chat-footer",
319
+ children: [messages.length > 0 && /* @__PURE__ */ jsx("div", {
320
+ style: {
321
+ display: "flex",
322
+ justifyContent: "flex-end",
323
+ paddingBottom: 8
324
+ },
325
+ children: /* @__PURE__ */ jsx("button", {
326
+ onClick: () => {
327
+ setMessages([]);
328
+ setAiInput("");
329
+ },
330
+ className: "fd-ai-clear-btn",
331
+ children: "Clear chat"
332
+ })
333
+ }), /* @__PURE__ */ jsxs("div", {
334
+ className: "fd-ai-input-wrap",
335
+ children: [/* @__PURE__ */ jsx("input", {
336
+ ref: aiInputRef,
337
+ type: "text",
338
+ placeholder: "Ask a question...",
339
+ value: aiInput,
340
+ onChange: (e) => setAiInput(e.target.value),
341
+ onKeyDown: handleAIKeyDown,
342
+ disabled: isStreaming,
343
+ className: "fd-ai-input",
344
+ style: { opacity: isStreaming ? .5 : 1 }
345
+ }), /* @__PURE__ */ jsx("button", {
346
+ onClick: handleAskAI,
347
+ disabled: !canSend,
348
+ className: "fd-ai-send-btn",
349
+ "data-active": canSend,
350
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
351
+ })]
352
+ })]
353
+ })]
354
+ });
355
+ }
356
+ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loadingComponentHtml }) {
357
+ const [tab, setTab] = useState("search");
358
+ const [searchQuery, setSearchQuery] = useState("");
359
+ const [searchResults, setSearchResults] = useState([]);
360
+ const [isSearching, setIsSearching] = useState(false);
361
+ const [activeIndex, setActiveIndex] = useState(0);
362
+ const searchInputRef = useRef(null);
363
+ const [messages, setMessages] = useState([]);
364
+ const [aiInput, setAiInput] = useState("");
365
+ const [isStreaming, setIsStreaming] = useState(false);
366
+ useEffect(() => {
367
+ if (open) {
368
+ setSearchQuery("");
369
+ setSearchResults([]);
370
+ setActiveIndex(0);
371
+ setTimeout(() => {
372
+ if (tab === "search") searchInputRef.current?.focus();
373
+ }, 50);
374
+ }
375
+ }, [open, tab]);
376
+ useEffect(() => {
377
+ if (!open) return;
378
+ const handler = (e) => {
379
+ if (e.key === "Escape") onOpenChange(false);
380
+ };
381
+ document.addEventListener("keydown", handler);
382
+ return () => document.removeEventListener("keydown", handler);
383
+ }, [open, onOpenChange]);
384
+ useEffect(() => {
385
+ if (open) document.body.style.overflow = "hidden";
386
+ else document.body.style.overflow = "";
387
+ return () => {
388
+ document.body.style.overflow = "";
389
+ };
390
+ }, [open]);
391
+ useEffect(() => {
392
+ if (!searchQuery.trim() || tab !== "search") {
393
+ setSearchResults([]);
394
+ setActiveIndex(0);
395
+ return;
396
+ }
397
+ setIsSearching(true);
398
+ const timer = setTimeout(async () => {
399
+ try {
400
+ const res = await fetch(`${api}?query=${encodeURIComponent(searchQuery)}`);
401
+ if (res.ok) {
402
+ setSearchResults(await res.json());
403
+ setActiveIndex(0);
404
+ }
405
+ } catch {}
406
+ setIsSearching(false);
407
+ }, 150);
408
+ return () => clearTimeout(timer);
409
+ }, [
410
+ searchQuery,
411
+ api,
412
+ tab
413
+ ]);
414
+ const handleSearchKeyDown = (e) => {
415
+ if (e.key === "ArrowDown") {
416
+ e.preventDefault();
417
+ setActiveIndex((i) => Math.min(i + 1, searchResults.length - 1));
418
+ } else if (e.key === "ArrowUp") {
419
+ e.preventDefault();
420
+ setActiveIndex((i) => Math.max(i - 1, 0));
421
+ } else if (e.key === "Enter" && searchResults[activeIndex]) {
422
+ e.preventDefault();
423
+ onOpenChange(false);
424
+ window.location.href = searchResults[activeIndex].url;
425
+ }
426
+ };
427
+ if (!open) return null;
428
+ const aiName = aiLabel || "AI";
429
+ return createPortal(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
430
+ onClick: () => onOpenChange(false),
431
+ className: "fd-ai-overlay"
432
+ }), /* @__PURE__ */ jsxs("div", {
433
+ role: "dialog",
434
+ "aria-modal": "true",
435
+ onClick: (e) => e.stopPropagation(),
436
+ className: "fd-ai-dialog",
437
+ style: {
438
+ left: "50%",
439
+ top: "50%",
440
+ transform: "translate(-50%, -50%)",
441
+ width: "min(680px, calc(100vw - 32px))",
442
+ maxHeight: "min(560px, calc(100vh - 64px))",
443
+ animation: "fd-ai-slide-up 200ms ease-out"
444
+ },
445
+ children: [
446
+ /* @__PURE__ */ jsxs("div", {
447
+ className: "fd-ai-tab-bar",
448
+ children: [
449
+ /* @__PURE__ */ jsxs("button", {
450
+ onClick: () => setTab("search"),
451
+ className: "fd-ai-tab",
452
+ "data-active": tab === "search",
453
+ children: [/* @__PURE__ */ jsx(SearchIcon, {}), " Search"]
454
+ }),
455
+ /* @__PURE__ */ jsxs("button", {
456
+ onClick: () => setTab("ai"),
457
+ className: "fd-ai-tab",
458
+ "data-active": tab === "ai",
459
+ children: [
460
+ /* @__PURE__ */ jsx(SparklesIcon, {}),
461
+ " Ask ",
462
+ aiName
463
+ ]
464
+ }),
465
+ /* @__PURE__ */ jsx("div", {
466
+ style: {
467
+ marginLeft: "auto",
468
+ display: "flex",
469
+ gap: 4,
470
+ alignItems: "center"
471
+ },
472
+ children: /* @__PURE__ */ jsx("kbd", {
473
+ className: "fd-ai-esc",
474
+ children: "ESC"
475
+ })
476
+ })
477
+ ]
478
+ }),
479
+ tab === "search" && /* @__PURE__ */ jsxs("div", {
480
+ style: {
481
+ display: "flex",
482
+ flexDirection: "column",
483
+ flex: 1,
484
+ minHeight: 0
485
+ },
486
+ children: [/* @__PURE__ */ jsxs("div", {
487
+ className: "fd-ai-search-wrap",
488
+ children: [
489
+ /* @__PURE__ */ jsx(SearchIcon, {}),
490
+ /* @__PURE__ */ jsx("input", {
491
+ ref: searchInputRef,
492
+ type: "text",
493
+ placeholder: "Search documentation...",
494
+ value: searchQuery,
495
+ onChange: (e) => setSearchQuery(e.target.value),
496
+ onKeyDown: handleSearchKeyDown,
497
+ className: "fd-ai-input"
498
+ }),
499
+ isSearching && /* @__PURE__ */ jsx(LoadingDots, {})
500
+ ]
501
+ }), /* @__PURE__ */ jsx("div", {
502
+ className: "fd-ai-results",
503
+ children: searchResults.length > 0 ? searchResults.map((result, i) => /* @__PURE__ */ jsxs("button", {
504
+ onClick: () => {
505
+ onOpenChange(false);
506
+ window.location.href = result.url;
507
+ },
508
+ onMouseEnter: () => setActiveIndex(i),
509
+ className: "fd-ai-result",
510
+ "data-active": i === activeIndex,
511
+ children: [/* @__PURE__ */ jsx(FileIcon, {}), /* @__PURE__ */ jsx("span", {
512
+ dangerouslySetInnerHTML: { __html: result.content },
513
+ style: { flex: 1 }
514
+ })]
515
+ }, result.id)) : /* @__PURE__ */ jsx("div", {
516
+ className: "fd-ai-result-empty",
517
+ children: searchQuery.trim() ? isSearching ? "Searching..." : `No results found. Try the Ask ${aiName} tab.` : "Type to search the docs"
518
+ })
519
+ })]
520
+ }),
521
+ tab === "ai" && /* @__PURE__ */ jsx(AIChat, {
522
+ api,
523
+ messages,
524
+ setMessages,
525
+ aiInput,
526
+ setAiInput,
527
+ isStreaming,
528
+ setIsStreaming,
529
+ suggestedQuestions,
530
+ aiLabel,
531
+ loadingComponentHtml
532
+ })
533
+ ]
534
+ })] }), document.body);
535
+ }
536
+ const BTN_POSITIONS = {
537
+ "bottom-right": {
538
+ bottom: 24,
539
+ right: 24
540
+ },
541
+ "bottom-left": {
542
+ bottom: 24,
543
+ left: 24
544
+ },
545
+ "bottom-center": {
546
+ bottom: 24,
547
+ left: "50%",
548
+ transform: "translateX(-50%)"
549
+ }
550
+ };
551
+ const PANEL_POSITIONS = {
552
+ "bottom-right": {
553
+ bottom: 80,
554
+ right: 24
555
+ },
556
+ "bottom-left": {
557
+ bottom: 80,
558
+ left: 24
559
+ },
560
+ "bottom-center": {
561
+ bottom: 80,
562
+ left: "50%",
563
+ transform: "translateX(-50%)"
564
+ }
565
+ };
566
+ const POPOVER_POSITIONS = {
567
+ "bottom-right": {
568
+ bottom: 80,
569
+ right: 24
570
+ },
571
+ "bottom-left": {
572
+ bottom: 80,
573
+ left: 24
574
+ },
575
+ "bottom-center": {
576
+ bottom: 80,
577
+ left: "50%",
578
+ transform: "translateX(-50%)"
579
+ }
580
+ };
581
+ function getContainerStyles(style, position) {
582
+ switch (style) {
583
+ case "modal": return {
584
+ top: "50%",
585
+ left: "50%",
586
+ transform: "translate(-50%, -50%)",
587
+ width: "min(680px, calc(100vw - 32px))",
588
+ height: "min(560px, calc(100vh - 64px))"
589
+ };
590
+ case "popover": return {
591
+ ...POPOVER_POSITIONS[position] || POPOVER_POSITIONS["bottom-right"],
592
+ width: "min(360px, calc(100vw - 48px))",
593
+ height: "min(400px, calc(100vh - 120px))"
594
+ };
595
+ default: return {
596
+ ...PANEL_POSITIONS[position] || PANEL_POSITIONS["bottom-right"],
597
+ width: "min(400px, calc(100vw - 48px))",
598
+ height: "min(500px, calc(100vh - 120px))"
599
+ };
600
+ }
601
+ }
602
+ function getAnimation(style) {
603
+ return style === "modal" ? "fd-ai-float-center-in 200ms ease-out" : "fd-ai-float-in 200ms ease-out";
604
+ }
605
+ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loadingComponentHtml }) {
606
+ const [mounted, setMounted] = useState(false);
607
+ const [isOpen, setIsOpen] = useState(false);
608
+ const [messages, setMessages] = useState([]);
609
+ const [aiInput, setAiInput] = useState("");
610
+ const [isStreaming, setIsStreaming] = useState(false);
611
+ useEffect(() => {
612
+ setMounted(true);
613
+ }, []);
614
+ useEffect(() => {
615
+ if (isOpen) {
616
+ const handler = (e) => {
617
+ if (e.key === "Escape") setIsOpen(false);
618
+ };
619
+ document.addEventListener("keydown", handler);
620
+ return () => document.removeEventListener("keydown", handler);
621
+ }
622
+ }, [isOpen]);
623
+ useEffect(() => {
624
+ if (isOpen && (floatingStyle === "modal" || floatingStyle === "full-modal")) document.body.style.overflow = "hidden";
625
+ else document.body.style.overflow = "";
626
+ return () => {
627
+ document.body.style.overflow = "";
628
+ };
629
+ }, [isOpen, floatingStyle]);
630
+ if (!mounted) return null;
631
+ if (floatingStyle === "full-modal") return /* @__PURE__ */ jsx(FullModalAIChat, {
632
+ api,
633
+ isOpen,
634
+ setIsOpen,
635
+ messages,
636
+ setMessages,
637
+ aiInput,
638
+ setAiInput,
639
+ isStreaming,
640
+ setIsStreaming,
641
+ suggestedQuestions,
642
+ aiLabel,
643
+ loadingComponentHtml,
644
+ triggerComponentHtml,
645
+ position
646
+ });
647
+ const btnPosition = BTN_POSITIONS[position] || BTN_POSITIONS["bottom-right"];
648
+ const isModal = floatingStyle === "modal";
649
+ const containerStyles = getContainerStyles(floatingStyle, position);
650
+ const aiName = aiLabel || "AI";
651
+ return createPortal(/* @__PURE__ */ jsxs(Fragment, { children: [
652
+ isOpen && isModal && /* @__PURE__ */ jsx("div", {
653
+ onClick: () => setIsOpen(false),
654
+ className: "fd-ai-overlay"
655
+ }),
656
+ isOpen && /* @__PURE__ */ jsxs("div", {
657
+ onClick: (e) => e.stopPropagation(),
658
+ className: "fd-ai-dialog",
659
+ style: {
660
+ ...containerStyles,
661
+ animation: getAnimation(floatingStyle)
662
+ },
663
+ children: [/* @__PURE__ */ jsxs("div", {
664
+ className: "fd-ai-header",
665
+ children: [
666
+ /* @__PURE__ */ jsx(SparklesIcon, { size: 16 }),
667
+ /* @__PURE__ */ jsxs("span", {
668
+ className: "fd-ai-header-title",
669
+ children: ["Ask ", aiName]
670
+ }),
671
+ isModal && /* @__PURE__ */ jsx("kbd", {
672
+ className: "fd-ai-esc",
673
+ children: "ESC"
674
+ }),
675
+ /* @__PURE__ */ jsx("button", {
676
+ onClick: () => setIsOpen(false),
677
+ className: "fd-ai-close-btn",
678
+ children: /* @__PURE__ */ jsx(XIcon, {})
679
+ })
680
+ ]
681
+ }), /* @__PURE__ */ jsx(AIChat, {
682
+ api,
683
+ messages,
684
+ setMessages,
685
+ aiInput,
686
+ setAiInput,
687
+ isStreaming,
688
+ setIsStreaming,
689
+ suggestedQuestions,
690
+ aiLabel,
691
+ loadingComponentHtml
692
+ })]
693
+ }),
694
+ !isOpen && (triggerComponentHtml ? /* @__PURE__ */ jsx("div", {
695
+ onClick: () => setIsOpen(true),
696
+ className: "fd-ai-floating-trigger",
697
+ style: btnPosition,
698
+ dangerouslySetInnerHTML: { __html: triggerComponentHtml }
699
+ }) : /* @__PURE__ */ jsx("button", {
700
+ onClick: () => setIsOpen(true),
701
+ "aria-label": `Ask ${aiName}`,
702
+ className: "fd-ai-floating-btn",
703
+ style: btnPosition,
704
+ children: /* @__PURE__ */ jsx(SparklesIcon, { size: 22 })
705
+ }))
706
+ ] }), document.body);
707
+ }
708
+ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loadingComponentHtml, triggerComponentHtml, position }) {
709
+ const label = aiLabel || "AI";
710
+ const inputRef = useRef(null);
711
+ const listRef = useRef(null);
712
+ const btnPosition = BTN_POSITIONS[position] || BTN_POSITIONS["bottom-right"];
713
+ useEffect(() => {
714
+ if (isOpen) setTimeout(() => inputRef.current?.focus(), 100);
715
+ }, [isOpen]);
716
+ useEffect(() => {
717
+ if (listRef.current) listRef.current.scrollTo({
718
+ top: listRef.current.scrollHeight,
719
+ behavior: "smooth"
720
+ });
721
+ }, [messages]);
722
+ const submitQuestion = useCallback(async (question) => {
723
+ if (!question.trim() || isStreaming) return;
724
+ const userMessage = {
725
+ role: "user",
726
+ content: question
727
+ };
728
+ const newMessages = [...messages, userMessage];
729
+ setAiInput("");
730
+ setIsStreaming(true);
731
+ setMessages([...newMessages, {
732
+ role: "assistant",
733
+ content: ""
734
+ }]);
735
+ try {
736
+ const res = await fetch(api, {
737
+ method: "POST",
738
+ headers: { "Content-Type": "application/json" },
739
+ body: JSON.stringify({ messages: newMessages.map((m) => ({
740
+ role: m.role,
741
+ content: m.content
742
+ })) })
743
+ });
744
+ if (!res.ok) {
745
+ let errMsg = "Something went wrong.";
746
+ try {
747
+ errMsg = (await res.json()).error || errMsg;
748
+ } catch {}
749
+ setMessages([...newMessages, {
750
+ role: "assistant",
751
+ content: errMsg
752
+ }]);
753
+ setIsStreaming(false);
754
+ return;
755
+ }
756
+ const reader = res.body.getReader();
757
+ const decoder = new TextDecoder();
758
+ let buffer = "";
759
+ let assistantContent = "";
760
+ while (true) {
761
+ const { done, value } = await reader.read();
762
+ if (done) break;
763
+ buffer += decoder.decode(value, { stream: true });
764
+ const lines = buffer.split("\n");
765
+ buffer = lines.pop() || "";
766
+ for (const line of lines) if (line.startsWith("data: ")) {
767
+ const data = line.slice(6).trim();
768
+ if (data === "[DONE]") continue;
769
+ try {
770
+ const content = JSON.parse(data).choices?.[0]?.delta?.content;
771
+ if (content) {
772
+ assistantContent += content;
773
+ setMessages([...newMessages, {
774
+ role: "assistant",
775
+ content: assistantContent
776
+ }]);
777
+ }
778
+ } catch {}
779
+ }
780
+ }
781
+ if (assistantContent) setMessages([...newMessages, {
782
+ role: "assistant",
783
+ content: assistantContent
784
+ }]);
785
+ } catch {
786
+ setMessages([...newMessages, {
787
+ role: "assistant",
788
+ content: "Failed to connect. Please try again."
789
+ }]);
790
+ }
791
+ setIsStreaming(false);
792
+ }, [
793
+ messages,
794
+ api,
795
+ isStreaming,
796
+ setMessages,
797
+ setAiInput,
798
+ setIsStreaming
799
+ ]);
800
+ const canSend = !!(aiInput.trim() && !isStreaming);
801
+ const showSuggestions = messages.length === 0 && !isStreaming;
802
+ const handleKeyDown = (e) => {
803
+ if (e.key === "Enter" && !e.shiftKey) {
804
+ e.preventDefault();
805
+ if (canSend) submitQuestion(aiInput);
806
+ }
807
+ };
808
+ return createPortal(/* @__PURE__ */ jsxs(Fragment, { children: [isOpen && /* @__PURE__ */ jsxs("div", {
809
+ className: "fd-ai-fm-overlay",
810
+ onClick: (e) => {
811
+ if (e.target === e.currentTarget) setIsOpen(false);
812
+ },
813
+ children: [/* @__PURE__ */ jsx("div", {
814
+ className: "fd-ai-fm-topbar",
815
+ children: /* @__PURE__ */ jsx("button", {
816
+ onClick: () => setIsOpen(false),
817
+ className: "fd-ai-fm-close-btn",
818
+ children: /* @__PURE__ */ jsx(XIcon, {})
819
+ })
820
+ }), /* @__PURE__ */ jsx("div", {
821
+ ref: listRef,
822
+ className: "fd-ai-fm-messages",
823
+ children: /* @__PURE__ */ jsx("div", {
824
+ className: "fd-ai-fm-messages-inner",
825
+ children: messages.map((msg, i) => /* @__PURE__ */ jsxs("div", {
826
+ className: "fd-ai-fm-msg",
827
+ "data-role": msg.role,
828
+ children: [/* @__PURE__ */ jsx("div", {
829
+ className: "fd-ai-fm-msg-label",
830
+ "data-role": msg.role,
831
+ children: msg.role === "user" ? "you" : label
832
+ }), /* @__PURE__ */ jsx("div", {
833
+ className: "fd-ai-fm-msg-content",
834
+ children: msg.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) } }) : loadingComponentHtml ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: loadingComponentHtml } }) : /* @__PURE__ */ jsxs("div", {
835
+ className: "fd-ai-fm-thinking",
836
+ children: [
837
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-fm-thinking-dot" }),
838
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-fm-thinking-dot" }),
839
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-fm-thinking-dot" })
840
+ ]
841
+ })
842
+ })]
843
+ }, i))
844
+ })
845
+ })]
846
+ }), /* @__PURE__ */ jsx("div", {
847
+ className: `fd-ai-fm-input-bar ${isOpen ? "fd-ai-fm-input-bar--open" : "fd-ai-fm-input-bar--closed"}`,
848
+ style: isOpen ? void 0 : btnPosition,
849
+ children: !isOpen ? triggerComponentHtml ? /* @__PURE__ */ jsx("div", {
850
+ onClick: () => setIsOpen(true),
851
+ dangerouslySetInnerHTML: { __html: triggerComponentHtml }
852
+ }) : /* @__PURE__ */ jsxs("button", {
853
+ onClick: () => setIsOpen(true),
854
+ className: "fd-ai-fm-trigger-btn",
855
+ "aria-label": `Ask ${label}`,
856
+ children: [/* @__PURE__ */ jsx(SparklesIcon, { size: 16 }), /* @__PURE__ */ jsxs("span", { children: ["Ask ", label] })]
857
+ }) : /* @__PURE__ */ jsxs("div", {
858
+ className: "fd-ai-fm-input-container",
859
+ children: [
860
+ /* @__PURE__ */ jsxs("div", {
861
+ className: "fd-ai-fm-input-wrap",
862
+ children: [/* @__PURE__ */ jsx("textarea", {
863
+ ref: inputRef,
864
+ className: "fd-ai-fm-input",
865
+ placeholder: isStreaming ? "answering..." : `Ask ${label}`,
866
+ value: aiInput,
867
+ onChange: (e) => setAiInput(e.target.value),
868
+ onKeyDown: handleKeyDown,
869
+ disabled: isStreaming,
870
+ rows: 1
871
+ }), isStreaming ? /* @__PURE__ */ jsx("button", {
872
+ className: "fd-ai-fm-send-btn",
873
+ onClick: () => setIsStreaming(false),
874
+ children: /* @__PURE__ */ jsx(LoadingDots, {})
875
+ }) : /* @__PURE__ */ jsx("button", {
876
+ className: "fd-ai-fm-send-btn",
877
+ "data-active": canSend,
878
+ disabled: !canSend,
879
+ onClick: () => submitQuestion(aiInput),
880
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
881
+ })]
882
+ }),
883
+ showSuggestions && suggestedQuestions && suggestedQuestions.length > 0 && /* @__PURE__ */ jsxs("div", {
884
+ className: "fd-ai-fm-suggestions-area",
885
+ children: [/* @__PURE__ */ jsx("div", {
886
+ className: "fd-ai-fm-suggestions-label",
887
+ children: "Try asking:"
888
+ }), /* @__PURE__ */ jsx("div", {
889
+ className: "fd-ai-fm-suggestions",
890
+ children: suggestedQuestions.map((q, i) => /* @__PURE__ */ jsx("button", {
891
+ onClick: () => submitQuestion(q),
892
+ className: "fd-ai-fm-suggestion",
893
+ children: q
894
+ }, i))
895
+ })]
896
+ }),
897
+ /* @__PURE__ */ jsx("div", {
898
+ className: "fd-ai-fm-footer-bar",
899
+ children: messages.length > 0 ? /* @__PURE__ */ jsxs("button", {
900
+ className: "fd-ai-fm-clear-btn",
901
+ onClick: () => {
902
+ if (!isStreaming) {
903
+ setMessages([]);
904
+ setAiInput("");
905
+ }
906
+ },
907
+ "aria-disabled": isStreaming,
908
+ children: [/* @__PURE__ */ jsx(TrashIcon, {}), /* @__PURE__ */ jsx("span", { children: "Clear" })]
909
+ }) : /* @__PURE__ */ jsx("div", {
910
+ className: "fd-ai-fm-footer-hint",
911
+ children: "AI can be inaccurate, please verify the information."
912
+ })
913
+ })
914
+ ]
915
+ })
916
+ })] }), document.body);
917
+ }
918
+ function TrashIcon() {
919
+ return /* @__PURE__ */ jsxs("svg", {
920
+ width: "12",
921
+ height: "12",
922
+ viewBox: "0 0 24 24",
923
+ fill: "none",
924
+ stroke: "currentColor",
925
+ strokeWidth: "2",
926
+ strokeLinecap: "round",
927
+ strokeLinejoin: "round",
928
+ children: [
929
+ /* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
930
+ /* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
931
+ /* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
932
+ ]
933
+ });
934
+ }
935
+
936
+ //#endregion
937
+ export { DocsSearchDialog, FloatingAIChat };