@farming-labs/theme 0.1.71 → 0.1.73
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/dist/ai-search-dialog.d.mts +9 -3
- package/dist/ai-search-dialog.mjs +340 -30
- package/dist/docs-ai-features.d.mts +3 -1
- package/dist/docs-ai-features.mjs +15 -9
- package/dist/docs-api.d.mts +8 -1
- package/dist/docs-api.mjs +347 -31
- package/dist/docs-client-hooks.d.mts +7 -1
- package/dist/docs-client-hooks.mjs +3 -1
- package/dist/docs-layout.mjs +3 -1
- package/package.json +2 -2
- package/styles/ai.css +42 -0
|
@@ -17,7 +17,8 @@ declare function DocsSearchDialog({
|
|
|
17
17
|
loadingComponentHtml,
|
|
18
18
|
models,
|
|
19
19
|
defaultModelId,
|
|
20
|
-
analytics
|
|
20
|
+
analytics,
|
|
21
|
+
feedbackEnabled
|
|
21
22
|
}: {
|
|
22
23
|
open: boolean;
|
|
23
24
|
onOpenChange: (open: boolean) => void;
|
|
@@ -29,6 +30,7 @@ declare function DocsSearchDialog({
|
|
|
29
30
|
models?: AIModelOption[];
|
|
30
31
|
defaultModelId?: string;
|
|
31
32
|
analytics?: boolean;
|
|
33
|
+
feedbackEnabled?: boolean;
|
|
32
34
|
}): react.ReactPortal | null;
|
|
33
35
|
type FloatingPosition = "bottom-right" | "bottom-left" | "bottom-center";
|
|
34
36
|
type FloatingStyle = "panel" | "modal" | "popover" | "full-modal";
|
|
@@ -43,7 +45,8 @@ declare function FloatingAIChat({
|
|
|
43
45
|
loadingComponentHtml,
|
|
44
46
|
models,
|
|
45
47
|
defaultModelId,
|
|
46
|
-
analytics
|
|
48
|
+
analytics,
|
|
49
|
+
feedbackEnabled
|
|
47
50
|
}: {
|
|
48
51
|
api?: string;
|
|
49
52
|
position?: FloatingPosition;
|
|
@@ -56,6 +59,7 @@ declare function FloatingAIChat({
|
|
|
56
59
|
models?: AIModelOption[];
|
|
57
60
|
defaultModelId?: string;
|
|
58
61
|
analytics?: boolean;
|
|
62
|
+
feedbackEnabled?: boolean;
|
|
59
63
|
}): react_jsx_runtime0.JSX.Element | null;
|
|
60
64
|
declare function AIModalDialog({
|
|
61
65
|
open,
|
|
@@ -67,7 +71,8 @@ declare function AIModalDialog({
|
|
|
67
71
|
loadingComponentHtml,
|
|
68
72
|
models,
|
|
69
73
|
defaultModelId,
|
|
70
|
-
analytics
|
|
74
|
+
analytics,
|
|
75
|
+
feedbackEnabled
|
|
71
76
|
}: {
|
|
72
77
|
open: boolean;
|
|
73
78
|
onOpenChange: (open: boolean) => void;
|
|
@@ -79,6 +84,7 @@ declare function AIModalDialog({
|
|
|
79
84
|
models?: AIModelOption[];
|
|
80
85
|
defaultModelId?: string;
|
|
81
86
|
analytics?: boolean;
|
|
87
|
+
feedbackEnabled?: boolean;
|
|
82
88
|
}): react.ReactPortal | null;
|
|
83
89
|
//#endregion
|
|
84
90
|
export { AIModalDialog, DocsSearchDialog, FloatingAIChat };
|
|
@@ -18,6 +18,118 @@ import { highlight } from "sugar-high";
|
|
|
18
18
|
* - `mode="search"` (default): AI tab inside the Cmd+K search dialog
|
|
19
19
|
* - `mode="floating"`: Standalone floating chat widget with configurable position
|
|
20
20
|
*/
|
|
21
|
+
let aiMessageId = 0;
|
|
22
|
+
function createAIMessageId() {
|
|
23
|
+
aiMessageId += 1;
|
|
24
|
+
return `ai_${Date.now().toString(36)}_${aiMessageId.toString(36)}`;
|
|
25
|
+
}
|
|
26
|
+
function getLastUserQuestion(messages, assistantIndex) {
|
|
27
|
+
for (let i = assistantIndex - 1; i >= 0; i -= 1) {
|
|
28
|
+
const message = messages[i];
|
|
29
|
+
if (message?.role === "user") return message.content;
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
function buildActionPayload(options) {
|
|
34
|
+
const location = typeof window !== "undefined" ? {
|
|
35
|
+
url: window.location.href,
|
|
36
|
+
path: window.location.pathname
|
|
37
|
+
} : {};
|
|
38
|
+
return {
|
|
39
|
+
type: options.type,
|
|
40
|
+
value: options.type === "like" || options.type === "dislike" ? options.type : void 0,
|
|
41
|
+
question: getLastUserQuestion(options.messages, options.index),
|
|
42
|
+
answer: options.message.content,
|
|
43
|
+
messageId: options.message.id,
|
|
44
|
+
messageIndex: options.index,
|
|
45
|
+
model: options.message.model,
|
|
46
|
+
surface: options.surface,
|
|
47
|
+
messages: options.messages.slice(0, options.index + 1).map((message) => ({
|
|
48
|
+
role: message.role,
|
|
49
|
+
content: message.content
|
|
50
|
+
})),
|
|
51
|
+
copied: options.copied,
|
|
52
|
+
...location
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function toFeedbackPayload(data) {
|
|
56
|
+
if (data.type !== "like" && data.type !== "dislike") return null;
|
|
57
|
+
return {
|
|
58
|
+
value: data.type,
|
|
59
|
+
question: data.question,
|
|
60
|
+
answer: data.answer,
|
|
61
|
+
messageId: data.messageId,
|
|
62
|
+
messageIndex: data.messageIndex,
|
|
63
|
+
model: data.model,
|
|
64
|
+
surface: data.surface,
|
|
65
|
+
url: data.url,
|
|
66
|
+
path: data.path,
|
|
67
|
+
messages: data.messages
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function emitAskAIAction(data) {
|
|
71
|
+
if (typeof window === "undefined") return;
|
|
72
|
+
try {
|
|
73
|
+
const result = window.__fdOnAIActions__?.(data);
|
|
74
|
+
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
75
|
+
} catch {}
|
|
76
|
+
try {
|
|
77
|
+
window.dispatchEvent(new CustomEvent("fd:ai-action", { detail: data }));
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
function emitAskAIFeedback(data, analytics) {
|
|
81
|
+
if (analytics) emitClientAnalyticsEvent({
|
|
82
|
+
type: "ai_feedback",
|
|
83
|
+
input: {
|
|
84
|
+
question: data.question,
|
|
85
|
+
feedbackValue: data.value
|
|
86
|
+
},
|
|
87
|
+
properties: {
|
|
88
|
+
value: data.value,
|
|
89
|
+
surface: data.surface,
|
|
90
|
+
model: data.model,
|
|
91
|
+
questionLength: data.question.length,
|
|
92
|
+
answerLength: data.answer.length,
|
|
93
|
+
messageIndex: data.messageIndex
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (typeof window === "undefined") return;
|
|
97
|
+
try {
|
|
98
|
+
const result = window.__fdOnAIFeedback__?.(data);
|
|
99
|
+
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
100
|
+
} catch {}
|
|
101
|
+
try {
|
|
102
|
+
window.dispatchEvent(new CustomEvent("fd:ai-feedback", { detail: data }));
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
function fallbackCopyText(text) {
|
|
106
|
+
if (typeof document === "undefined") return false;
|
|
107
|
+
const textarea = document.createElement("textarea");
|
|
108
|
+
textarea.value = text;
|
|
109
|
+
textarea.setAttribute("readonly", "");
|
|
110
|
+
textarea.style.position = "fixed";
|
|
111
|
+
textarea.style.top = "-9999px";
|
|
112
|
+
textarea.style.opacity = "0";
|
|
113
|
+
document.body.appendChild(textarea);
|
|
114
|
+
textarea.select();
|
|
115
|
+
try {
|
|
116
|
+
document.execCommand("copy");
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
} finally {
|
|
121
|
+
document.body.removeChild(textarea);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function copyTextToClipboard(text) {
|
|
125
|
+
if (!text) return false;
|
|
126
|
+
try {
|
|
127
|
+
await navigator.clipboard.writeText(text);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return fallbackCopyText(text);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
21
133
|
function buildCodeBlock(lang, code) {
|
|
22
134
|
const highlighted = highlight(code.replace(/\n$/, "")).replace(/<\/span>\n<span/g, "</span><span");
|
|
23
135
|
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>`;
|
|
@@ -146,6 +258,65 @@ function XIcon() {
|
|
|
146
258
|
children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
|
|
147
259
|
});
|
|
148
260
|
}
|
|
261
|
+
function ThumbsUpIcon() {
|
|
262
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
263
|
+
width: "14",
|
|
264
|
+
height: "14",
|
|
265
|
+
viewBox: "0 0 24 24",
|
|
266
|
+
fill: "none",
|
|
267
|
+
stroke: "currentColor",
|
|
268
|
+
strokeWidth: "2",
|
|
269
|
+
strokeLinecap: "round",
|
|
270
|
+
strokeLinejoin: "round",
|
|
271
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M7 10v12" }), /* @__PURE__ */ jsx("path", { d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" })]
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function ThumbsDownIcon() {
|
|
275
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
276
|
+
width: "14",
|
|
277
|
+
height: "14",
|
|
278
|
+
viewBox: "0 0 24 24",
|
|
279
|
+
fill: "none",
|
|
280
|
+
stroke: "currentColor",
|
|
281
|
+
strokeWidth: "2",
|
|
282
|
+
strokeLinecap: "round",
|
|
283
|
+
strokeLinejoin: "round",
|
|
284
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M17 14V2" }), /* @__PURE__ */ jsx("path", { d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" })]
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function CopyIcon() {
|
|
288
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
289
|
+
width: "14",
|
|
290
|
+
height: "14",
|
|
291
|
+
viewBox: "0 0 24 24",
|
|
292
|
+
fill: "none",
|
|
293
|
+
stroke: "currentColor",
|
|
294
|
+
strokeWidth: "2",
|
|
295
|
+
strokeLinecap: "round",
|
|
296
|
+
strokeLinejoin: "round",
|
|
297
|
+
children: [/* @__PURE__ */ jsx("rect", {
|
|
298
|
+
x: "9",
|
|
299
|
+
y: "9",
|
|
300
|
+
width: "13",
|
|
301
|
+
height: "13",
|
|
302
|
+
rx: "2",
|
|
303
|
+
ry: "2"
|
|
304
|
+
}), /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })]
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function CheckIcon() {
|
|
308
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
309
|
+
width: "14",
|
|
310
|
+
height: "14",
|
|
311
|
+
viewBox: "0 0 24 24",
|
|
312
|
+
fill: "none",
|
|
313
|
+
stroke: "currentColor",
|
|
314
|
+
strokeWidth: "2.4",
|
|
315
|
+
strokeLinecap: "round",
|
|
316
|
+
strokeLinejoin: "round",
|
|
317
|
+
children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
|
|
318
|
+
});
|
|
319
|
+
}
|
|
149
320
|
function LoaderIndicator({ variant = "shimmer-dots" }) {
|
|
150
321
|
const text = "Thinking";
|
|
151
322
|
switch (variant) {
|
|
@@ -346,7 +517,51 @@ function ModelSelector({ models, selectedId, onChange, disabled }) {
|
|
|
346
517
|
})]
|
|
347
518
|
});
|
|
348
519
|
}
|
|
349
|
-
function
|
|
520
|
+
function AIFeedbackControls({ value, onCopy, onSelect }) {
|
|
521
|
+
const [copied, setCopied] = useState(false);
|
|
522
|
+
const handleCopy = useCallback(async () => {
|
|
523
|
+
if (!await onCopy()) return;
|
|
524
|
+
setCopied(true);
|
|
525
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
526
|
+
}, [onCopy]);
|
|
527
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
528
|
+
className: "fd-ai-feedback",
|
|
529
|
+
role: "group",
|
|
530
|
+
"aria-label": "Rate this Ask AI response",
|
|
531
|
+
children: [
|
|
532
|
+
/* @__PURE__ */ jsx("button", {
|
|
533
|
+
type: "button",
|
|
534
|
+
className: "fd-ai-feedback-btn",
|
|
535
|
+
"data-copied": copied ? "true" : void 0,
|
|
536
|
+
"aria-label": copied ? "Copied response" : "Copy response",
|
|
537
|
+
title: copied ? "Copied" : "Copy response",
|
|
538
|
+
onClick: handleCopy,
|
|
539
|
+
children: copied ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(CopyIcon, {})
|
|
540
|
+
}),
|
|
541
|
+
/* @__PURE__ */ jsx("button", {
|
|
542
|
+
type: "button",
|
|
543
|
+
className: "fd-ai-feedback-btn",
|
|
544
|
+
"data-active": value === "like",
|
|
545
|
+
"aria-pressed": value === "like",
|
|
546
|
+
"aria-label": "Helpful",
|
|
547
|
+
title: "Helpful",
|
|
548
|
+
onClick: () => onSelect("like"),
|
|
549
|
+
children: /* @__PURE__ */ jsx(ThumbsUpIcon, {})
|
|
550
|
+
}),
|
|
551
|
+
/* @__PURE__ */ jsx("button", {
|
|
552
|
+
type: "button",
|
|
553
|
+
className: "fd-ai-feedback-btn",
|
|
554
|
+
"data-active": value === "dislike",
|
|
555
|
+
"aria-pressed": value === "dislike",
|
|
556
|
+
"aria-label": "Not helpful",
|
|
557
|
+
title: "Not helpful",
|
|
558
|
+
onClick: () => onSelect("dislike"),
|
|
559
|
+
children: /* @__PURE__ */ jsx(ThumbsDownIcon, {})
|
|
560
|
+
})
|
|
561
|
+
]
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled = true, surface = "chat" }) {
|
|
350
565
|
const label = aiLabel || "AI";
|
|
351
566
|
const aiInputRef = useRef(null);
|
|
352
567
|
const messagesEndRef = useRef(null);
|
|
@@ -368,12 +583,15 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
368
583
|
content: question
|
|
369
584
|
};
|
|
370
585
|
const newMessages = [...messages, userMessage];
|
|
586
|
+
const assistantMessage = {
|
|
587
|
+
role: "assistant",
|
|
588
|
+
content: "",
|
|
589
|
+
id: createAIMessageId(),
|
|
590
|
+
model: effectiveModelId
|
|
591
|
+
};
|
|
371
592
|
setAiInput("");
|
|
372
593
|
setIsStreaming(true);
|
|
373
|
-
setMessages([...newMessages,
|
|
374
|
-
role: "assistant",
|
|
375
|
-
content: ""
|
|
376
|
-
}]);
|
|
594
|
+
setMessages([...newMessages, assistantMessage]);
|
|
377
595
|
const startedAt = Date.now();
|
|
378
596
|
if (analytics) emitClientAnalyticsEvent({
|
|
379
597
|
type: "ai_question",
|
|
@@ -402,8 +620,9 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
402
620
|
errMsg = (await res.json()).error || errMsg;
|
|
403
621
|
} catch {}
|
|
404
622
|
setMessages([...newMessages, {
|
|
405
|
-
|
|
406
|
-
content: errMsg
|
|
623
|
+
...assistantMessage,
|
|
624
|
+
content: errMsg,
|
|
625
|
+
isError: true
|
|
407
626
|
}]);
|
|
408
627
|
setIsStreaming(false);
|
|
409
628
|
if (analytics) emitClientAnalyticsEvent({
|
|
@@ -435,7 +654,7 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
435
654
|
if (content) {
|
|
436
655
|
assistantContent += content;
|
|
437
656
|
setMessages([...newMessages, {
|
|
438
|
-
|
|
657
|
+
...assistantMessage,
|
|
439
658
|
content: assistantContent
|
|
440
659
|
}]);
|
|
441
660
|
}
|
|
@@ -443,7 +662,7 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
443
662
|
}
|
|
444
663
|
}
|
|
445
664
|
if (assistantContent) setMessages([...newMessages, {
|
|
446
|
-
|
|
665
|
+
...assistantMessage,
|
|
447
666
|
content: assistantContent
|
|
448
667
|
}]);
|
|
449
668
|
if (analytics) emitClientAnalyticsEvent({
|
|
@@ -458,8 +677,9 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
458
677
|
});
|
|
459
678
|
} catch {
|
|
460
679
|
setMessages([...newMessages, {
|
|
461
|
-
|
|
462
|
-
content: "Failed to connect. Please try again."
|
|
680
|
+
...assistantMessage,
|
|
681
|
+
content: "Failed to connect. Please try again.",
|
|
682
|
+
isError: true
|
|
463
683
|
}]);
|
|
464
684
|
if (analytics) emitClientAnalyticsEvent({
|
|
465
685
|
type: "ai_error",
|
|
@@ -492,6 +712,42 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
492
712
|
}
|
|
493
713
|
};
|
|
494
714
|
const canSend = !!(aiInput.trim() && !isStreaming);
|
|
715
|
+
const handleFeedback = useCallback((message, index, value) => {
|
|
716
|
+
if (message.feedback === value) return;
|
|
717
|
+
const updatedMessage = {
|
|
718
|
+
...message,
|
|
719
|
+
feedback: value
|
|
720
|
+
};
|
|
721
|
+
const updatedMessages = messages.map((item, itemIndex) => itemIndex === index ? updatedMessage : item);
|
|
722
|
+
setMessages(updatedMessages);
|
|
723
|
+
const actionPayload = buildActionPayload({
|
|
724
|
+
type: value,
|
|
725
|
+
message: updatedMessage,
|
|
726
|
+
messages: updatedMessages,
|
|
727
|
+
index,
|
|
728
|
+
surface
|
|
729
|
+
});
|
|
730
|
+
emitAskAIAction(actionPayload);
|
|
731
|
+
const feedbackPayload = toFeedbackPayload(actionPayload);
|
|
732
|
+
if (feedbackPayload) emitAskAIFeedback(feedbackPayload, analytics);
|
|
733
|
+
}, [
|
|
734
|
+
analytics,
|
|
735
|
+
messages,
|
|
736
|
+
setMessages,
|
|
737
|
+
surface
|
|
738
|
+
]);
|
|
739
|
+
const handleCopyMessage = useCallback(async (message, index) => {
|
|
740
|
+
const copied = await copyTextToClipboard(message.content);
|
|
741
|
+
emitAskAIAction(buildActionPayload({
|
|
742
|
+
type: "copy",
|
|
743
|
+
message,
|
|
744
|
+
messages,
|
|
745
|
+
index,
|
|
746
|
+
surface,
|
|
747
|
+
copied
|
|
748
|
+
}));
|
|
749
|
+
return copied;
|
|
750
|
+
}, [messages, surface]);
|
|
495
751
|
return /* @__PURE__ */ jsxs("div", {
|
|
496
752
|
style: {
|
|
497
753
|
display: "flex",
|
|
@@ -536,10 +792,14 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
536
792
|
children: msg.content
|
|
537
793
|
}) : /* @__PURE__ */ jsx("div", {
|
|
538
794
|
className: "fd-ai-bubble-ai",
|
|
539
|
-
children: msg.content ? /* @__PURE__ */ jsx("div", {
|
|
795
|
+
children: msg.content ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
540
796
|
className: isStreaming && i === messages.length - 1 ? "fd-ai-streaming" : void 0,
|
|
541
797
|
dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) }
|
|
542
|
-
})
|
|
798
|
+
}), feedbackEnabled && !msg.isError && !isStreaming && /* @__PURE__ */ jsx(AIFeedbackControls, {
|
|
799
|
+
value: msg.feedback,
|
|
800
|
+
onCopy: () => handleCopyMessage(msg, i),
|
|
801
|
+
onSelect: (value) => handleFeedback(msg, i, value)
|
|
802
|
+
})] }) : loadingComponentHtml ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: loadingComponentHtml } }) : /* @__PURE__ */ jsx(LoaderIndicator, {
|
|
543
803
|
variant: loaderVariant,
|
|
544
804
|
label
|
|
545
805
|
})
|
|
@@ -600,7 +860,7 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
600
860
|
})]
|
|
601
861
|
});
|
|
602
862
|
}
|
|
603
|
-
function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false }) {
|
|
863
|
+
function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
604
864
|
const [tab, setTab] = useState("search");
|
|
605
865
|
const [searchQuery, setSearchQuery] = useState("");
|
|
606
866
|
const [searchResults, setSearchResults] = useState([]);
|
|
@@ -849,6 +1109,7 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
|
|
|
849
1109
|
models,
|
|
850
1110
|
defaultModelId: effectiveModelId,
|
|
851
1111
|
analytics,
|
|
1112
|
+
feedbackEnabled,
|
|
852
1113
|
surface: "ai-dialog"
|
|
853
1114
|
})
|
|
854
1115
|
]
|
|
@@ -923,7 +1184,7 @@ function getContainerStyles(style, position) {
|
|
|
923
1184
|
function getAnimation(style) {
|
|
924
1185
|
return style === "modal" ? "fd-ai-float-center-in 200ms ease-out" : "fd-ai-float-in 200ms ease-out";
|
|
925
1186
|
}
|
|
926
|
-
function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false }) {
|
|
1187
|
+
function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
927
1188
|
const [mounted, setMounted] = useState(false);
|
|
928
1189
|
const [isOpen, setIsOpen] = useState(false);
|
|
929
1190
|
const [messages, setMessages] = useState([]);
|
|
@@ -980,7 +1241,8 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
|
|
|
980
1241
|
position,
|
|
981
1242
|
models,
|
|
982
1243
|
defaultModelId,
|
|
983
|
-
analytics
|
|
1244
|
+
analytics,
|
|
1245
|
+
feedbackEnabled
|
|
984
1246
|
});
|
|
985
1247
|
const btnPosition = BTN_POSITIONS[position] || BTN_POSITIONS["bottom-right"];
|
|
986
1248
|
const isModal = floatingStyle === "modal";
|
|
@@ -1028,7 +1290,10 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
|
|
|
1028
1290
|
aiLabel,
|
|
1029
1291
|
loaderVariant,
|
|
1030
1292
|
loadingComponentHtml,
|
|
1293
|
+
models,
|
|
1294
|
+
defaultModelId,
|
|
1031
1295
|
analytics,
|
|
1296
|
+
feedbackEnabled,
|
|
1032
1297
|
surface: "floating"
|
|
1033
1298
|
})]
|
|
1034
1299
|
}),
|
|
@@ -1064,7 +1329,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
|
|
|
1064
1329
|
}))
|
|
1065
1330
|
] }), document.body);
|
|
1066
1331
|
}
|
|
1067
|
-
function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position, models, defaultModelId, analytics }) {
|
|
1332
|
+
function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position, models, defaultModelId, analytics, feedbackEnabled = true }) {
|
|
1068
1333
|
const label = aiLabel || "AI";
|
|
1069
1334
|
const inputRef = useRef(null);
|
|
1070
1335
|
const listRef = useRef(null);
|
|
@@ -1090,12 +1355,15 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1090
1355
|
content: question
|
|
1091
1356
|
};
|
|
1092
1357
|
const newMessages = [...messages, userMessage];
|
|
1358
|
+
const assistantMessage = {
|
|
1359
|
+
role: "assistant",
|
|
1360
|
+
content: "",
|
|
1361
|
+
id: createAIMessageId(),
|
|
1362
|
+
model: effectiveModelId
|
|
1363
|
+
};
|
|
1093
1364
|
setAiInput("");
|
|
1094
1365
|
setIsStreaming(true);
|
|
1095
|
-
setMessages([...newMessages,
|
|
1096
|
-
role: "assistant",
|
|
1097
|
-
content: ""
|
|
1098
|
-
}]);
|
|
1366
|
+
setMessages([...newMessages, assistantMessage]);
|
|
1099
1367
|
const startedAt = Date.now();
|
|
1100
1368
|
if (analytics) emitClientAnalyticsEvent({
|
|
1101
1369
|
type: "ai_question",
|
|
@@ -1124,8 +1392,9 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1124
1392
|
errMsg = (await res.json()).error || errMsg;
|
|
1125
1393
|
} catch {}
|
|
1126
1394
|
setMessages([...newMessages, {
|
|
1127
|
-
|
|
1128
|
-
content: errMsg
|
|
1395
|
+
...assistantMessage,
|
|
1396
|
+
content: errMsg,
|
|
1397
|
+
isError: true
|
|
1129
1398
|
}]);
|
|
1130
1399
|
setIsStreaming(false);
|
|
1131
1400
|
if (analytics) emitClientAnalyticsEvent({
|
|
@@ -1157,7 +1426,7 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1157
1426
|
if (content) {
|
|
1158
1427
|
assistantContent += content;
|
|
1159
1428
|
setMessages([...newMessages, {
|
|
1160
|
-
|
|
1429
|
+
...assistantMessage,
|
|
1161
1430
|
content: assistantContent
|
|
1162
1431
|
}]);
|
|
1163
1432
|
}
|
|
@@ -1165,7 +1434,7 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1165
1434
|
}
|
|
1166
1435
|
}
|
|
1167
1436
|
if (assistantContent) setMessages([...newMessages, {
|
|
1168
|
-
|
|
1437
|
+
...assistantMessage,
|
|
1169
1438
|
content: assistantContent
|
|
1170
1439
|
}]);
|
|
1171
1440
|
if (analytics) emitClientAnalyticsEvent({
|
|
@@ -1180,8 +1449,9 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1180
1449
|
});
|
|
1181
1450
|
} catch {
|
|
1182
1451
|
setMessages([...newMessages, {
|
|
1183
|
-
|
|
1184
|
-
content: "Failed to connect. Please try again."
|
|
1452
|
+
...assistantMessage,
|
|
1453
|
+
content: "Failed to connect. Please try again.",
|
|
1454
|
+
isError: true
|
|
1185
1455
|
}]);
|
|
1186
1456
|
if (analytics) emitClientAnalyticsEvent({
|
|
1187
1457
|
type: "ai_error",
|
|
@@ -1205,6 +1475,41 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1205
1475
|
]);
|
|
1206
1476
|
const canSend = !!(aiInput.trim() && !isStreaming);
|
|
1207
1477
|
const showSuggestions = messages.length === 0 && !isStreaming;
|
|
1478
|
+
const handleFeedback = useCallback((message, index, value) => {
|
|
1479
|
+
if (message.feedback === value) return;
|
|
1480
|
+
const updatedMessage = {
|
|
1481
|
+
...message,
|
|
1482
|
+
feedback: value
|
|
1483
|
+
};
|
|
1484
|
+
const updatedMessages = messages.map((item, itemIndex) => itemIndex === index ? updatedMessage : item);
|
|
1485
|
+
setMessages(updatedMessages);
|
|
1486
|
+
const actionPayload = buildActionPayload({
|
|
1487
|
+
type: value,
|
|
1488
|
+
message: updatedMessage,
|
|
1489
|
+
messages: updatedMessages,
|
|
1490
|
+
index,
|
|
1491
|
+
surface: "full-modal"
|
|
1492
|
+
});
|
|
1493
|
+
emitAskAIAction(actionPayload);
|
|
1494
|
+
const feedbackPayload = toFeedbackPayload(actionPayload);
|
|
1495
|
+
if (feedbackPayload) emitAskAIFeedback(feedbackPayload, analytics);
|
|
1496
|
+
}, [
|
|
1497
|
+
analytics,
|
|
1498
|
+
messages,
|
|
1499
|
+
setMessages
|
|
1500
|
+
]);
|
|
1501
|
+
const handleCopyMessage = useCallback(async (message, index) => {
|
|
1502
|
+
const copied = await copyTextToClipboard(message.content);
|
|
1503
|
+
emitAskAIAction(buildActionPayload({
|
|
1504
|
+
type: "copy",
|
|
1505
|
+
message,
|
|
1506
|
+
messages,
|
|
1507
|
+
index,
|
|
1508
|
+
surface: "full-modal",
|
|
1509
|
+
copied
|
|
1510
|
+
}));
|
|
1511
|
+
return copied;
|
|
1512
|
+
}, [messages]);
|
|
1208
1513
|
const handleKeyDown = (e) => {
|
|
1209
1514
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
1210
1515
|
e.preventDefault();
|
|
@@ -1237,10 +1542,14 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1237
1542
|
children: msg.role === "user" ? "you" : label
|
|
1238
1543
|
}), /* @__PURE__ */ jsx("div", {
|
|
1239
1544
|
className: "fd-ai-fm-msg-content",
|
|
1240
|
-
children: msg.content ? /* @__PURE__ */ jsx("div", {
|
|
1545
|
+
children: msg.content ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
1241
1546
|
className: isStreaming && i === messages.length - 1 ? "fd-ai-streaming" : void 0,
|
|
1242
1547
|
dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) }
|
|
1243
|
-
})
|
|
1548
|
+
}), msg.role === "assistant" && feedbackEnabled && !msg.isError && !isStreaming && /* @__PURE__ */ jsx(AIFeedbackControls, {
|
|
1549
|
+
value: msg.feedback,
|
|
1550
|
+
onCopy: () => handleCopyMessage(msg, i),
|
|
1551
|
+
onSelect: (value) => handleFeedback(msg, i, value)
|
|
1552
|
+
})] }) : loadingComponentHtml ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: loadingComponentHtml } }) : /* @__PURE__ */ jsx(LoaderIndicator, {
|
|
1244
1553
|
variant: loaderVariant,
|
|
1245
1554
|
label
|
|
1246
1555
|
})
|
|
@@ -1368,7 +1677,7 @@ function TrashIcon() {
|
|
|
1368
1677
|
]
|
|
1369
1678
|
});
|
|
1370
1679
|
}
|
|
1371
|
-
function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false }) {
|
|
1680
|
+
function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
1372
1681
|
const [messages, setMessages] = useState([]);
|
|
1373
1682
|
const [aiInput, setAiInput] = useState("");
|
|
1374
1683
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
@@ -1450,6 +1759,7 @@ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestio
|
|
|
1450
1759
|
models,
|
|
1451
1760
|
defaultModelId,
|
|
1452
1761
|
analytics,
|
|
1762
|
+
feedbackEnabled,
|
|
1453
1763
|
surface: "modal"
|
|
1454
1764
|
}),
|
|
1455
1765
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -18,6 +18,7 @@ interface DocsAIFeaturesProps {
|
|
|
18
18
|
}[];
|
|
19
19
|
defaultModelId?: string;
|
|
20
20
|
analytics?: boolean;
|
|
21
|
+
feedbackEnabled?: boolean;
|
|
21
22
|
}
|
|
22
23
|
declare function DocsAIFeatures({
|
|
23
24
|
mode,
|
|
@@ -32,7 +33,8 @@ declare function DocsAIFeatures({
|
|
|
32
33
|
loadingComponentHtml,
|
|
33
34
|
models,
|
|
34
35
|
defaultModelId,
|
|
35
|
-
analytics
|
|
36
|
+
analytics,
|
|
37
|
+
feedbackEnabled
|
|
36
38
|
}: DocsAIFeaturesProps): react_jsx_runtime0.JSX.Element;
|
|
37
39
|
//#endregion
|
|
38
40
|
export { DocsAIFeatures };
|
|
@@ -22,7 +22,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
22
22
|
* This component is rendered inside the docs layout so the user's root layout
|
|
23
23
|
* never needs to be modified — AI features work purely from `docs.config.ts`.
|
|
24
24
|
*/
|
|
25
|
-
function DocsAIFeatures({ mode, api = "/api/docs", locale, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false }) {
|
|
25
|
+
function DocsAIFeatures({ mode, api = "/api/docs", locale, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
26
26
|
const localizedApi = withLangInUrl(api, resolveClientLocale(useWindowSearchParams(), locale));
|
|
27
27
|
if (mode === "search") return /* @__PURE__ */ jsx(SearchModeAI, {
|
|
28
28
|
api: localizedApi,
|
|
@@ -32,7 +32,8 @@ function DocsAIFeatures({ mode, api = "/api/docs", locale, position = "bottom-ri
|
|
|
32
32
|
loadingComponentHtml,
|
|
33
33
|
models,
|
|
34
34
|
defaultModelId,
|
|
35
|
-
analytics
|
|
35
|
+
analytics,
|
|
36
|
+
feedbackEnabled
|
|
36
37
|
});
|
|
37
38
|
if (mode === "sidebar-icon") return /* @__PURE__ */ jsx(SidebarIconModeAI, {
|
|
38
39
|
api: localizedApi,
|
|
@@ -42,7 +43,8 @@ function DocsAIFeatures({ mode, api = "/api/docs", locale, position = "bottom-ri
|
|
|
42
43
|
loadingComponentHtml,
|
|
43
44
|
models,
|
|
44
45
|
defaultModelId,
|
|
45
|
-
analytics
|
|
46
|
+
analytics,
|
|
47
|
+
feedbackEnabled
|
|
46
48
|
});
|
|
47
49
|
return /* @__PURE__ */ jsx(FloatingAIChat, {
|
|
48
50
|
api: localizedApi,
|
|
@@ -55,10 +57,11 @@ function DocsAIFeatures({ mode, api = "/api/docs", locale, position = "bottom-ri
|
|
|
55
57
|
loadingComponentHtml,
|
|
56
58
|
models,
|
|
57
59
|
defaultModelId,
|
|
58
|
-
analytics
|
|
60
|
+
analytics,
|
|
61
|
+
feedbackEnabled
|
|
59
62
|
});
|
|
60
63
|
}
|
|
61
|
-
function SearchModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics }) {
|
|
64
|
+
function SearchModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled }) {
|
|
62
65
|
const [open, setOpen] = useState(false);
|
|
63
66
|
useEffect(() => {
|
|
64
67
|
function handler(e) {
|
|
@@ -111,10 +114,11 @@ function SearchModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loading
|
|
|
111
114
|
loadingComponentHtml,
|
|
112
115
|
models,
|
|
113
116
|
defaultModelId,
|
|
114
|
-
analytics
|
|
117
|
+
analytics,
|
|
118
|
+
feedbackEnabled
|
|
115
119
|
});
|
|
116
120
|
}
|
|
117
|
-
function SidebarIconModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics }) {
|
|
121
|
+
function SidebarIconModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled }) {
|
|
118
122
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
119
123
|
const [aiOpen, setAiOpen] = useState(false);
|
|
120
124
|
useEffect(() => {
|
|
@@ -167,7 +171,8 @@ function SidebarIconModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, lo
|
|
|
167
171
|
loadingComponentHtml,
|
|
168
172
|
models,
|
|
169
173
|
defaultModelId,
|
|
170
|
-
analytics
|
|
174
|
+
analytics,
|
|
175
|
+
feedbackEnabled
|
|
171
176
|
}), /* @__PURE__ */ jsx(AIModalDialog, {
|
|
172
177
|
open: aiOpen,
|
|
173
178
|
onOpenChange: setAiOpen,
|
|
@@ -178,7 +183,8 @@ function SidebarIconModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, lo
|
|
|
178
183
|
loadingComponentHtml,
|
|
179
184
|
models,
|
|
180
185
|
defaultModelId,
|
|
181
|
-
analytics
|
|
186
|
+
analytics,
|
|
187
|
+
feedbackEnabled
|
|
182
188
|
})] });
|
|
183
189
|
}
|
|
184
190
|
|
package/dist/docs-api.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChangelogConfig, DocsAnalyticsConfig, DocsI18nConfig, DocsMcpConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
|
|
1
|
+
import { ChangelogConfig, DocsAnalyticsConfig, DocsI18nConfig, DocsMcpConfig, DocsObservabilityConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
|
|
2
2
|
|
|
3
3
|
//#region src/docs-api.d.ts
|
|
4
4
|
interface AIProviderConfig {
|
|
@@ -19,6 +19,10 @@ interface AIOptions {
|
|
|
19
19
|
model?: string | AIModelConfig;
|
|
20
20
|
providers?: Record<string, AIProviderConfig>;
|
|
21
21
|
systemPrompt?: string;
|
|
22
|
+
/** Package name the AI should use in import examples. */
|
|
23
|
+
packageName?: string;
|
|
24
|
+
/** Public docs URL the AI should use for absolute links. */
|
|
25
|
+
docsUrl?: string;
|
|
22
26
|
/** Default baseUrl when no per-model provider is configured. */
|
|
23
27
|
baseUrl?: string;
|
|
24
28
|
/** Default apiKey when no per-model provider is configured. */
|
|
@@ -43,6 +47,8 @@ interface DocsAPIOptions {
|
|
|
43
47
|
search?: boolean | DocsSearchConfig;
|
|
44
48
|
/** Analytics configuration */
|
|
45
49
|
analytics?: boolean | DocsAnalyticsConfig;
|
|
50
|
+
/** Observability configuration for logs, traces, and metrics callbacks. */
|
|
51
|
+
observability?: boolean | DocsObservabilityConfig;
|
|
46
52
|
/** Feedback configuration */
|
|
47
53
|
feedback?: boolean | FeedbackConfig;
|
|
48
54
|
/** MCP configuration used for the agent discovery spec. */
|
|
@@ -59,6 +65,7 @@ interface DocsMCPAPIOptions {
|
|
|
59
65
|
mcp?: boolean | DocsMcpConfig;
|
|
60
66
|
search?: boolean | DocsSearchConfig;
|
|
61
67
|
analytics?: boolean | DocsAnalyticsConfig;
|
|
68
|
+
observability?: boolean | DocsObservabilityConfig;
|
|
62
69
|
}
|
|
63
70
|
/**
|
|
64
71
|
* Create a unified docs API route handler.
|
package/dist/docs-api.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { getNextAppDir } from "./get-app-dir.mjs";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import matter from "gray-matter";
|
|
6
|
-
import { emitDocsAnalyticsEvent, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig } from "@farming-labs/docs";
|
|
6
|
+
import { buildDocsAskAIContext, createDocsAgentTraceContext, createDocsAgentTraceId, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, formatDocsAskAIPackageHints, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig } from "@farming-labs/docs";
|
|
7
7
|
import { createDocsMcpHttpHandler, createFilesystemDocsMcpSource, resolveDocsMcpConfig } from "@farming-labs/docs/server";
|
|
8
8
|
|
|
9
9
|
//#region src/docs-api.ts
|
|
@@ -414,13 +414,17 @@ function readAIConfig(root) {
|
|
|
414
414
|
const apiKeyMatch = content.match(/ai\s*:\s*\{[^}]*apiKey\s*:\s*process\.env\.(\w+)/s);
|
|
415
415
|
const maxResultsMatch = content.match(/ai\s*:\s*\{[^}]*maxResults\s*:\s*(\d+)/s);
|
|
416
416
|
const systemPromptMatch = content.match(/ai\s*:\s*\{[^}]*systemPrompt\s*:\s*["'`]([^"'`]+)["'`]/s);
|
|
417
|
+
const packageNameMatch = content.match(/ai\s*:\s*\{[^}]*packageName\s*:\s*["'`]([^"'`]+)["'`]/s);
|
|
418
|
+
const docsUrlMatch = content.match(/ai\s*:\s*\{[^}]*docsUrl\s*:\s*["'`]([^"'`]+)["'`]/s);
|
|
417
419
|
return {
|
|
418
420
|
enabled: true,
|
|
419
421
|
model: modelMatch?.[1],
|
|
420
422
|
baseUrl: baseUrlMatch?.[1],
|
|
421
423
|
apiKey: apiKeyMatch?.[1] ? process.env[apiKeyMatch[1]] : void 0,
|
|
422
424
|
maxResults: maxResultsMatch ? parseInt(maxResultsMatch[1], 10) : void 0,
|
|
423
|
-
systemPrompt: systemPromptMatch?.[1]
|
|
425
|
+
systemPrompt: systemPromptMatch?.[1],
|
|
426
|
+
packageName: packageNameMatch?.[1],
|
|
427
|
+
docsUrl: docsUrlMatch?.[1]
|
|
424
428
|
};
|
|
425
429
|
} catch {}
|
|
426
430
|
}
|
|
@@ -936,7 +940,21 @@ function truncateSkillDescription(value) {
|
|
|
936
940
|
function toYamlString(value) {
|
|
937
941
|
return JSON.stringify(value);
|
|
938
942
|
}
|
|
939
|
-
|
|
943
|
+
function buildDefaultSystemPrompt(aiConfig) {
|
|
944
|
+
const lines = [
|
|
945
|
+
"You are a helpful documentation assistant.",
|
|
946
|
+
"Answer only from the provided documentation context.",
|
|
947
|
+
"Prefer exact code/config snippets from the context when the question asks how to implement something.",
|
|
948
|
+
"Cite the relevant documentation URL when you use a source.",
|
|
949
|
+
"Use only URLs exactly as they appear in the context; do not invent placeholder domains.",
|
|
950
|
+
"Never use placeholder package names or imports such as \"your-auth-library\", \"your-package\", \"your-sdk\", \"replace-me\", or \"example-library\". If the exact package or import is not in the context, do not include an import snippet.",
|
|
951
|
+
"Be concise and accurate. If the answer is not in the context, say so honestly.",
|
|
952
|
+
"Use markdown formatting for code examples and links."
|
|
953
|
+
];
|
|
954
|
+
if (aiConfig.packageName) lines.push(`When showing import examples, use "${aiConfig.packageName}" as the package name and prefer exact imports copied from the documentation context.`);
|
|
955
|
+
if (aiConfig.docsUrl) lines.push(`When linking to documentation pages, use "${aiConfig.docsUrl}" as the base URL (e.g. ${aiConfig.docsUrl}/docs/get-started).`);
|
|
956
|
+
return lines.join(" ");
|
|
957
|
+
}
|
|
940
958
|
function resolveModelAndProvider(aiConfig, requestedModelId) {
|
|
941
959
|
const raw = aiConfig.model;
|
|
942
960
|
const modelList = typeof raw === "object" && raw?.models || [];
|
|
@@ -956,9 +974,74 @@ function resolveModelAndProvider(aiConfig, requestedModelId) {
|
|
|
956
974
|
apiKey
|
|
957
975
|
};
|
|
958
976
|
}
|
|
959
|
-
|
|
977
|
+
function safeUrlOrigin(value) {
|
|
978
|
+
try {
|
|
979
|
+
return new URL(value).origin;
|
|
980
|
+
} catch {
|
|
981
|
+
return value;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async function handleAskAI(request, indexes, aiConfig, search, analytics, observability, analyticsContext = {}) {
|
|
960
985
|
const url = new URL(request.url);
|
|
961
986
|
const requestStartedAt = Date.now();
|
|
987
|
+
const trace = createDocsAgentTraceContext("ask-ai");
|
|
988
|
+
const runSpanId = createDocsAgentTraceId("span");
|
|
989
|
+
const traceBase = {
|
|
990
|
+
source: "server",
|
|
991
|
+
traceId: trace.traceId,
|
|
992
|
+
url: request.url,
|
|
993
|
+
path: url.pathname,
|
|
994
|
+
locale: analyticsContext.locale
|
|
995
|
+
};
|
|
996
|
+
async function emitTrace(event) {
|
|
997
|
+
await emitDocsAgentTraceEvent(observability, {
|
|
998
|
+
...traceBase,
|
|
999
|
+
...event
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
async function emitRunError(reason, outputPreview = {}) {
|
|
1003
|
+
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1004
|
+
const elapsed = Math.max(0, Date.now() - requestStartedAt);
|
|
1005
|
+
const common = {
|
|
1006
|
+
name: "ask-ai",
|
|
1007
|
+
startedAt: trace.startedAt,
|
|
1008
|
+
endedAt,
|
|
1009
|
+
durationMs: elapsed,
|
|
1010
|
+
status: "error",
|
|
1011
|
+
outputPreview: {
|
|
1012
|
+
reason,
|
|
1013
|
+
...outputPreview
|
|
1014
|
+
},
|
|
1015
|
+
metadata: { reason }
|
|
1016
|
+
};
|
|
1017
|
+
await emitTrace({
|
|
1018
|
+
...common,
|
|
1019
|
+
type: "error",
|
|
1020
|
+
parentSpanId: runSpanId
|
|
1021
|
+
});
|
|
1022
|
+
await emitTrace({
|
|
1023
|
+
...common,
|
|
1024
|
+
type: "run.error",
|
|
1025
|
+
spanId: runSpanId
|
|
1026
|
+
});
|
|
1027
|
+
await emitTrace({
|
|
1028
|
+
...common,
|
|
1029
|
+
type: "run.end",
|
|
1030
|
+
spanId: runSpanId
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
await emitTrace({
|
|
1034
|
+
type: "run.start",
|
|
1035
|
+
name: "ask-ai",
|
|
1036
|
+
spanId: runSpanId,
|
|
1037
|
+
startedAt: trace.startedAt,
|
|
1038
|
+
durationMs: 0,
|
|
1039
|
+
status: "started",
|
|
1040
|
+
inputPreview: {
|
|
1041
|
+
method: request.method,
|
|
1042
|
+
path: url.pathname
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
962
1045
|
let body;
|
|
963
1046
|
try {
|
|
964
1047
|
body = await request.json();
|
|
@@ -974,6 +1057,7 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
974
1057
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
975
1058
|
}
|
|
976
1059
|
});
|
|
1060
|
+
await emitRunError("invalid_json", { status: 400 });
|
|
977
1061
|
return Response.json({ error: "Invalid JSON body. Expected { messages: [...] }" }, { status: 400 });
|
|
978
1062
|
}
|
|
979
1063
|
const messages = body.messages;
|
|
@@ -989,6 +1073,7 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
989
1073
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
990
1074
|
}
|
|
991
1075
|
});
|
|
1076
|
+
await emitRunError("missing_messages", { status: 400 });
|
|
992
1077
|
return Response.json({ error: "messages array is required and must not be empty." }, { status: 400 });
|
|
993
1078
|
}
|
|
994
1079
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
@@ -1005,27 +1090,101 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1005
1090
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1006
1091
|
}
|
|
1007
1092
|
});
|
|
1093
|
+
await emitRunError("missing_user_message", {
|
|
1094
|
+
status: 400,
|
|
1095
|
+
messageCount: messages.length
|
|
1096
|
+
});
|
|
1008
1097
|
return Response.json({ error: "At least one user message is required." }, { status: 400 });
|
|
1009
1098
|
}
|
|
1010
1099
|
const maxResults = aiConfig.maxResults ?? 5;
|
|
1011
1100
|
const query = lastUserMessage.content;
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1101
|
+
await emitTrace({
|
|
1102
|
+
type: "user.input",
|
|
1103
|
+
name: "ask-ai",
|
|
1104
|
+
parentSpanId: runSpanId,
|
|
1105
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1106
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1107
|
+
durationMs: 0,
|
|
1108
|
+
status: "success",
|
|
1109
|
+
inputPreview: {
|
|
1110
|
+
messageCount: messages.length,
|
|
1111
|
+
questionLength: query.length,
|
|
1112
|
+
requestedModel: typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
const retrievalStartedAt = Date.now();
|
|
1116
|
+
const retrievalStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1117
|
+
const retrievalSpanId = createDocsAgentTraceId("span");
|
|
1118
|
+
await emitTrace({
|
|
1119
|
+
type: "retrieval.query",
|
|
1120
|
+
name: "docs-index",
|
|
1121
|
+
spanId: retrievalSpanId,
|
|
1122
|
+
parentSpanId: runSpanId,
|
|
1123
|
+
startedAt: retrievalStartedAtIso,
|
|
1124
|
+
status: "started",
|
|
1125
|
+
inputPreview: {
|
|
1126
|
+
queryLength: query.length,
|
|
1127
|
+
maxResults,
|
|
1128
|
+
indexSize: indexes.length
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
const retrieval = await buildDocsAskAIContext({
|
|
1132
|
+
pages: indexes,
|
|
1133
|
+
query,
|
|
1134
|
+
search,
|
|
1135
|
+
locale: analyticsContext.locale,
|
|
1136
|
+
pathname: url.searchParams.get("pathname") ?? void 0,
|
|
1137
|
+
siteTitle: "Documentation",
|
|
1138
|
+
baseUrl: url.origin,
|
|
1139
|
+
limit: maxResults
|
|
1140
|
+
});
|
|
1141
|
+
const scored = retrieval.results;
|
|
1142
|
+
await emitTrace({
|
|
1143
|
+
type: "retrieval.result",
|
|
1144
|
+
name: "docs-index",
|
|
1145
|
+
parentSpanId: retrievalSpanId,
|
|
1146
|
+
startedAt: retrievalStartedAtIso,
|
|
1147
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1148
|
+
durationMs: Math.max(0, Date.now() - retrievalStartedAt),
|
|
1149
|
+
status: "success",
|
|
1150
|
+
outputPreview: {
|
|
1151
|
+
resultCount: scored.length,
|
|
1152
|
+
urls: scored.slice(0, 5).map((doc) => doc.url)
|
|
1153
|
+
},
|
|
1154
|
+
metadata: {
|
|
1155
|
+
maxResults,
|
|
1156
|
+
indexSize: indexes.length
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
const promptStartedAt = Date.now();
|
|
1160
|
+
const promptStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1161
|
+
const promptSpanId = createDocsAgentTraceId("span");
|
|
1162
|
+
const context = retrieval.context;
|
|
1163
|
+
const fullSystemPrompt = [aiConfig.systemPrompt ?? buildDefaultSystemPrompt(aiConfig), formatDocsAskAIPackageHints(retrieval.packageHints, aiConfig.packageName)].filter(Boolean).join("\n\n");
|
|
1164
|
+
const systemMessage = {
|
|
1026
1165
|
role: "system",
|
|
1027
|
-
content: context ? `${
|
|
1028
|
-
}
|
|
1166
|
+
content: context ? `${fullSystemPrompt}\n\n---\n\nDocumentation context:\n\n${context}` : fullSystemPrompt
|
|
1167
|
+
};
|
|
1168
|
+
const llmMessages = [systemMessage, ...messages.filter((m) => m.role !== "system")];
|
|
1169
|
+
await emitTrace({
|
|
1170
|
+
type: "prompt.build",
|
|
1171
|
+
name: "ask-ai.prompt",
|
|
1172
|
+
spanId: promptSpanId,
|
|
1173
|
+
parentSpanId: runSpanId,
|
|
1174
|
+
startedAt: promptStartedAtIso,
|
|
1175
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1176
|
+
durationMs: Math.max(0, Date.now() - promptStartedAt),
|
|
1177
|
+
status: "success",
|
|
1178
|
+
inputPreview: {
|
|
1179
|
+
messageCount: messages.length,
|
|
1180
|
+
retrievedCount: scored.length
|
|
1181
|
+
},
|
|
1182
|
+
outputPreview: {
|
|
1183
|
+
llmMessageCount: llmMessages.length,
|
|
1184
|
+
contextChars: context.length,
|
|
1185
|
+
systemMessageChars: systemMessage.content.length
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1029
1188
|
const resolved = resolveModelAndProvider(aiConfig, typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0);
|
|
1030
1189
|
if (!resolved.apiKey) {
|
|
1031
1190
|
await emitDocsAnalyticsEvent(analytics, {
|
|
@@ -1044,6 +1203,13 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1044
1203
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1045
1204
|
}
|
|
1046
1205
|
});
|
|
1206
|
+
await emitRunError("missing_api_key", {
|
|
1207
|
+
status: 500,
|
|
1208
|
+
messageCount: messages.length,
|
|
1209
|
+
questionLength: query.length,
|
|
1210
|
+
retrievedCount: scored.length,
|
|
1211
|
+
model: resolved.model
|
|
1212
|
+
});
|
|
1047
1213
|
return Response.json({ error: `AI is enabled but no API key was found. Either set apiKey in your docs.config ai section, configure a provider, or add OPENAI_API_KEY to your .env.local file.` }, { status: 500 });
|
|
1048
1214
|
}
|
|
1049
1215
|
await emitDocsAnalyticsEvent(analytics, {
|
|
@@ -1060,20 +1226,100 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1060
1226
|
model: resolved.model
|
|
1061
1227
|
}
|
|
1062
1228
|
});
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1229
|
+
const modelStartedAt = Date.now();
|
|
1230
|
+
const modelStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1231
|
+
const modelSpanId = createDocsAgentTraceId("span");
|
|
1232
|
+
const providerOrigin = safeUrlOrigin(resolved.baseUrl);
|
|
1233
|
+
await emitTrace({
|
|
1234
|
+
type: "model.call",
|
|
1235
|
+
name: resolved.model,
|
|
1236
|
+
spanId: modelSpanId,
|
|
1237
|
+
parentSpanId: runSpanId,
|
|
1238
|
+
startedAt: modelStartedAtIso,
|
|
1239
|
+
status: "started",
|
|
1240
|
+
inputPreview: {
|
|
1241
|
+
messageCount: llmMessages.length,
|
|
1071
1242
|
stream: true,
|
|
1072
|
-
|
|
1073
|
-
}
|
|
1243
|
+
providerOrigin
|
|
1244
|
+
},
|
|
1245
|
+
metadata: { model: resolved.model }
|
|
1074
1246
|
});
|
|
1247
|
+
let llmResponse;
|
|
1248
|
+
try {
|
|
1249
|
+
llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
|
|
1250
|
+
method: "POST",
|
|
1251
|
+
headers: {
|
|
1252
|
+
"Content-Type": "application/json",
|
|
1253
|
+
Authorization: `Bearer ${resolved.apiKey}`
|
|
1254
|
+
},
|
|
1255
|
+
body: JSON.stringify({
|
|
1256
|
+
model: resolved.model,
|
|
1257
|
+
stream: true,
|
|
1258
|
+
messages: llmMessages
|
|
1259
|
+
})
|
|
1260
|
+
});
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
const elapsed = Math.max(0, Date.now() - modelStartedAt);
|
|
1263
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1264
|
+
await emitTrace({
|
|
1265
|
+
type: "model.error",
|
|
1266
|
+
name: resolved.model,
|
|
1267
|
+
parentSpanId: modelSpanId,
|
|
1268
|
+
startedAt: modelStartedAtIso,
|
|
1269
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1270
|
+
durationMs: elapsed,
|
|
1271
|
+
status: "error",
|
|
1272
|
+
outputPreview: { message },
|
|
1273
|
+
metadata: {
|
|
1274
|
+
model: resolved.model,
|
|
1275
|
+
providerOrigin
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1279
|
+
type: "api_ai_error",
|
|
1280
|
+
source: "server",
|
|
1281
|
+
url: request.url,
|
|
1282
|
+
path: url.pathname,
|
|
1283
|
+
locale: analyticsContext.locale,
|
|
1284
|
+
input: { question: query },
|
|
1285
|
+
properties: {
|
|
1286
|
+
reason: "llm_fetch_error",
|
|
1287
|
+
messageCount: messages.length,
|
|
1288
|
+
questionLength: query.length,
|
|
1289
|
+
retrievedCount: scored.length,
|
|
1290
|
+
model: resolved.model,
|
|
1291
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
await emitRunError("llm_fetch_error", {
|
|
1295
|
+
status: 502,
|
|
1296
|
+
messageCount: messages.length,
|
|
1297
|
+
questionLength: query.length,
|
|
1298
|
+
retrievedCount: scored.length,
|
|
1299
|
+
model: resolved.model
|
|
1300
|
+
});
|
|
1301
|
+
return Response.json({ error: "LLM API request failed." }, { status: 502 });
|
|
1302
|
+
}
|
|
1075
1303
|
if (!llmResponse.ok) {
|
|
1076
1304
|
const errText = await llmResponse.text().catch(() => "Unknown error");
|
|
1305
|
+
const elapsed = Math.max(0, Date.now() - modelStartedAt);
|
|
1306
|
+
await emitTrace({
|
|
1307
|
+
type: "model.error",
|
|
1308
|
+
name: resolved.model,
|
|
1309
|
+
parentSpanId: modelSpanId,
|
|
1310
|
+
startedAt: modelStartedAtIso,
|
|
1311
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1312
|
+
durationMs: elapsed,
|
|
1313
|
+
status: "error",
|
|
1314
|
+
outputPreview: {
|
|
1315
|
+
status: llmResponse.status,
|
|
1316
|
+
errorChars: errText.length
|
|
1317
|
+
},
|
|
1318
|
+
metadata: {
|
|
1319
|
+
model: resolved.model,
|
|
1320
|
+
providerOrigin
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1077
1323
|
await emitDocsAnalyticsEvent(analytics, {
|
|
1078
1324
|
type: "api_ai_error",
|
|
1079
1325
|
source: "server",
|
|
@@ -1091,6 +1337,14 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1091
1337
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1092
1338
|
}
|
|
1093
1339
|
});
|
|
1340
|
+
await emitRunError("llm_error", {
|
|
1341
|
+
status: 502,
|
|
1342
|
+
modelStatus: llmResponse.status,
|
|
1343
|
+
messageCount: messages.length,
|
|
1344
|
+
questionLength: query.length,
|
|
1345
|
+
retrievedCount: scored.length,
|
|
1346
|
+
model: resolved.model
|
|
1347
|
+
});
|
|
1094
1348
|
return Response.json({ error: `LLM API error (${llmResponse.status}): ${errText}` }, { status: 502 });
|
|
1095
1349
|
}
|
|
1096
1350
|
await emitDocsAnalyticsEvent(analytics, {
|
|
@@ -1108,6 +1362,66 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1108
1362
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1109
1363
|
}
|
|
1110
1364
|
});
|
|
1365
|
+
const responseEndedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1366
|
+
const modelDurationMs = Math.max(0, Date.now() - modelStartedAt);
|
|
1367
|
+
await emitTrace({
|
|
1368
|
+
type: "model.response",
|
|
1369
|
+
name: resolved.model,
|
|
1370
|
+
parentSpanId: modelSpanId,
|
|
1371
|
+
startedAt: modelStartedAtIso,
|
|
1372
|
+
endedAt: responseEndedAt,
|
|
1373
|
+
durationMs: modelDurationMs,
|
|
1374
|
+
status: "success",
|
|
1375
|
+
outputPreview: {
|
|
1376
|
+
status: llmResponse.status,
|
|
1377
|
+
stream: true,
|
|
1378
|
+
contentType: llmResponse.headers.get("content-type") ?? void 0
|
|
1379
|
+
},
|
|
1380
|
+
metadata: {
|
|
1381
|
+
model: resolved.model,
|
|
1382
|
+
providerOrigin
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
await emitTrace({
|
|
1386
|
+
type: "model.stream",
|
|
1387
|
+
name: resolved.model,
|
|
1388
|
+
parentSpanId: modelSpanId,
|
|
1389
|
+
startedAt: modelStartedAtIso,
|
|
1390
|
+
endedAt: responseEndedAt,
|
|
1391
|
+
durationMs: modelDurationMs,
|
|
1392
|
+
status: "success",
|
|
1393
|
+
outputPreview: { stream: true },
|
|
1394
|
+
metadata: { model: resolved.model }
|
|
1395
|
+
});
|
|
1396
|
+
const runDurationMs = Math.max(0, Date.now() - requestStartedAt);
|
|
1397
|
+
await emitTrace({
|
|
1398
|
+
type: "agent.final",
|
|
1399
|
+
name: "ask-ai",
|
|
1400
|
+
parentSpanId: runSpanId,
|
|
1401
|
+
startedAt: trace.startedAt,
|
|
1402
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1403
|
+
durationMs: runDurationMs,
|
|
1404
|
+
status: "success",
|
|
1405
|
+
outputPreview: {
|
|
1406
|
+
stream: true,
|
|
1407
|
+
retrievedCount: scored.length
|
|
1408
|
+
},
|
|
1409
|
+
metadata: { model: resolved.model }
|
|
1410
|
+
});
|
|
1411
|
+
await emitTrace({
|
|
1412
|
+
type: "run.end",
|
|
1413
|
+
name: "ask-ai",
|
|
1414
|
+
spanId: runSpanId,
|
|
1415
|
+
startedAt: trace.startedAt,
|
|
1416
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1417
|
+
durationMs: runDurationMs,
|
|
1418
|
+
status: "success",
|
|
1419
|
+
outputPreview: {
|
|
1420
|
+
stream: true,
|
|
1421
|
+
retrievedCount: scored.length
|
|
1422
|
+
},
|
|
1423
|
+
metadata: { model: resolved.model }
|
|
1424
|
+
});
|
|
1111
1425
|
return new Response(llmResponse.body, { headers: {
|
|
1112
1426
|
"Content-Type": "text/event-stream",
|
|
1113
1427
|
"Cache-Control": "no-cache",
|
|
@@ -1188,6 +1502,7 @@ function createDocsAPI(options) {
|
|
|
1188
1502
|
const root = options?.rootDir ?? process.cwd();
|
|
1189
1503
|
const entry = options?.entry ?? readEntry(root);
|
|
1190
1504
|
const analytics = options?.analytics;
|
|
1505
|
+
const observability = options?.observability;
|
|
1191
1506
|
const appDir = getNextAppDir(root);
|
|
1192
1507
|
const contentDir = options?.contentDir ?? path.join(appDir, entry);
|
|
1193
1508
|
const changelogConfig = resolveChangelogConfig(options?.changelog);
|
|
@@ -1586,7 +1901,7 @@ function createDocsAPI(options) {
|
|
|
1586
1901
|
return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
|
|
1587
1902
|
}
|
|
1588
1903
|
const ctx = resolveContextFromRequest(request);
|
|
1589
|
-
return handleAskAI(request, getIndexes(ctx), aiConfig, analytics, { locale: ctx.locale });
|
|
1904
|
+
return handleAskAI(request, getIndexes(ctx), aiConfig, resolveSearchRequestConfig(searchConfig, request.url), analytics, observability, { locale: ctx.locale });
|
|
1590
1905
|
}
|
|
1591
1906
|
};
|
|
1592
1907
|
}
|
|
@@ -1616,6 +1931,7 @@ function createDocsMCPAPI(options = {}) {
|
|
|
1616
1931
|
mcp: options.mcp ?? readMcpConfig(rootDir),
|
|
1617
1932
|
search: options.search,
|
|
1618
1933
|
analytics: options.analytics,
|
|
1934
|
+
observability: options.observability,
|
|
1619
1935
|
defaultName: navTitle
|
|
1620
1936
|
});
|
|
1621
1937
|
return {
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { CodeBlockCopyData, DocsAnalyticsConfig, DocsFeedbackData } from "@farming-labs/docs";
|
|
1
|
+
import { CodeBlockCopyData, DocsAnalyticsConfig, DocsAskAIActionData, DocsAskAIFeedbackData, DocsFeedbackData } from "@farming-labs/docs";
|
|
2
2
|
|
|
3
3
|
//#region src/docs-client-hooks.d.ts
|
|
4
4
|
type CopyHandler = (data: CodeBlockCopyData) => void;
|
|
5
5
|
type FeedbackHandler = (data: DocsFeedbackData) => void | Promise<void>;
|
|
6
|
+
type AIActionHandler = (data: DocsAskAIActionData) => void | Promise<void>;
|
|
7
|
+
type AIFeedbackHandler = (data: DocsAskAIFeedbackData) => void | Promise<void>;
|
|
6
8
|
declare function DocsClientHooks({
|
|
7
9
|
onCopyClick,
|
|
8
10
|
onFeedback,
|
|
11
|
+
onAIActions,
|
|
12
|
+
onAIFeedback,
|
|
9
13
|
analytics
|
|
10
14
|
}: {
|
|
11
15
|
onCopyClick?: CopyHandler;
|
|
12
16
|
onFeedback?: FeedbackHandler;
|
|
17
|
+
onAIActions?: AIActionHandler;
|
|
18
|
+
onAIFeedback?: AIFeedbackHandler;
|
|
13
19
|
analytics?: boolean | DocsAnalyticsConfig;
|
|
14
20
|
}): null;
|
|
15
21
|
//#endregion
|
|
@@ -65,9 +65,11 @@ function useCodeCopyAnalytics(analytics) {
|
|
|
65
65
|
return () => document.removeEventListener("click", handleClick, true);
|
|
66
66
|
}, [analytics]);
|
|
67
67
|
}
|
|
68
|
-
function DocsClientHooks({ onCopyClick, onFeedback, analytics }) {
|
|
68
|
+
function DocsClientHooks({ onCopyClick, onFeedback, onAIActions, onAIFeedback, analytics }) {
|
|
69
69
|
useWindowHook("__fdOnCopyClick__", onCopyClick);
|
|
70
70
|
useWindowHook("__fdOnFeedback__", onFeedback);
|
|
71
|
+
useWindowHook("__fdOnAIActions__", onAIActions);
|
|
72
|
+
useWindowHook("__fdOnAIFeedback__", onAIFeedback);
|
|
71
73
|
useAnalyticsHook(analytics);
|
|
72
74
|
useCodeCopyAnalytics(analytics);
|
|
73
75
|
return null;
|
package/dist/docs-layout.mjs
CHANGED
|
@@ -622,6 +622,7 @@ function createDocsLayout(config, options) {
|
|
|
622
622
|
const aiSuggestedQuestions = aiConfig?.suggestedQuestions;
|
|
623
623
|
const aiLabel = aiConfig?.aiLabel;
|
|
624
624
|
const aiLoaderVariant = aiConfig?.loader;
|
|
625
|
+
const aiFeedbackEnabled = aiConfig?.feedback === false || typeof aiConfig?.feedback === "object" && aiConfig.feedback.enabled === false ? false : true;
|
|
625
626
|
const aiLoadingComponentHtml = typeof aiConfig?.loadingComponent === "function" ? serializeIcon(aiConfig.loadingComponent({ name: aiLabel || "AI" })) : void 0;
|
|
626
627
|
const rawModelConfig = aiConfig?.model;
|
|
627
628
|
let aiModels = aiConfig?.models;
|
|
@@ -707,7 +708,8 @@ function createDocsLayout(config, options) {
|
|
|
707
708
|
loadingComponentHtml: aiLoadingComponentHtml,
|
|
708
709
|
models: aiModels,
|
|
709
710
|
defaultModelId: aiDefaultModelId,
|
|
710
|
-
analytics: analyticsEnabled
|
|
711
|
+
analytics: analyticsEnabled,
|
|
712
|
+
feedbackEnabled: aiFeedbackEnabled
|
|
711
713
|
})
|
|
712
714
|
}),
|
|
713
715
|
/* @__PURE__ */ jsx(Suspense, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farming-labs/theme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73",
|
|
4
4
|
"description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"tsdown": "^0.20.3",
|
|
140
140
|
"typescript": "^5.9.3",
|
|
141
141
|
"vitest": "^3.2.4",
|
|
142
|
-
"@farming-labs/docs": "0.1.
|
|
142
|
+
"@farming-labs/docs": "0.1.73"
|
|
143
143
|
},
|
|
144
144
|
"peerDependencies": {
|
|
145
145
|
"@farming-labs/docs": ">=0.0.1",
|
package/styles/ai.css
CHANGED
|
@@ -337,6 +337,48 @@
|
|
|
337
337
|
animation: fd-ai-msg-in 300ms ease-out;
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
.fd-ai-feedback {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 4px;
|
|
344
|
+
margin-top: 10px;
|
|
345
|
+
padding-top: 8px;
|
|
346
|
+
border-top: 1px solid color-mix(in srgb, var(--color-fd-border, #1f1f2e) 70%, transparent);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.fd-ai-feedback-btn {
|
|
350
|
+
display: inline-flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
width: 26px;
|
|
354
|
+
height: 26px;
|
|
355
|
+
border: 1px solid transparent;
|
|
356
|
+
border-radius: var(--radius, 6px);
|
|
357
|
+
background: transparent;
|
|
358
|
+
color: var(--color-fd-muted-foreground, #71717a);
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
transition:
|
|
361
|
+
background 150ms,
|
|
362
|
+
border-color 150ms,
|
|
363
|
+
color 150ms;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.fd-ai-feedback-btn:hover {
|
|
367
|
+
color: var(--color-fd-foreground, #e4e4e7);
|
|
368
|
+
background: var(--color-fd-accent, rgba(255, 255, 255, 0.06));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.fd-ai-feedback-btn[data-active="true"] {
|
|
372
|
+
color: var(--color-fd-primary, #6366f1);
|
|
373
|
+
border-color: color-mix(in srgb, var(--color-fd-primary, #6366f1) 40%, transparent);
|
|
374
|
+
background: color-mix(in srgb, var(--color-fd-primary, #6366f1) 10%, transparent);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.fd-ai-feedback-btn[data-copied="true"] {
|
|
378
|
+
color: var(--color-fd-primary, #6366f1);
|
|
379
|
+
background: color-mix(in srgb, var(--color-fd-primary, #6366f1) 10%, transparent);
|
|
380
|
+
}
|
|
381
|
+
|
|
340
382
|
@keyframes fd-ai-msg-in {
|
|
341
383
|
from { opacity: 0; transform: translateY(6px); }
|
|
342
384
|
to { opacity: 1; transform: translateY(0); }
|