@farming-labs/theme 0.1.145 → 0.2.1
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 +21 -0
- package/dist/ai-search-dialog.mjs +221 -77
- package/dist/docs-ai-features.d.mts +6 -0
- package/dist/docs-ai-features.mjs +21 -3
- package/dist/docs-api.d.mts +4 -0
- package/dist/docs-api.mjs +421 -2
- package/dist/docs-cloud-ai-client.mjs +44 -0
- package/dist/docs-layout.mjs +6 -1
- package/dist/tanstack-layout.mjs +6 -1
- package/dist/threadline/index.mjs +1 -1
- package/package.json +2 -2
- package/styles/ai.css +16 -4
|
@@ -6,11 +6,17 @@ type AIModelOption = {
|
|
|
6
6
|
id: string;
|
|
7
7
|
label: string;
|
|
8
8
|
};
|
|
9
|
+
type AIRequestMode = "openai-chat" | "docs-cloud";
|
|
10
|
+
type AIRequestHeaders = Record<string, string>;
|
|
11
|
+
type AIRequestStream = boolean;
|
|
9
12
|
type LoaderVariant = "shimmer-dots" | "circular" | "dots" | "typing" | "wave" | "bars" | "pulse" | "pulse-dot" | "terminal" | "text-blink" | "text-shimmer" | "loading-dots";
|
|
10
13
|
declare function DocsSearchDialog({
|
|
11
14
|
open,
|
|
12
15
|
onOpenChange,
|
|
13
16
|
api,
|
|
17
|
+
requestMode,
|
|
18
|
+
requestHeaders,
|
|
19
|
+
requestStream,
|
|
14
20
|
suggestedQuestions,
|
|
15
21
|
aiLabel,
|
|
16
22
|
loaderVariant,
|
|
@@ -23,6 +29,9 @@ declare function DocsSearchDialog({
|
|
|
23
29
|
open: boolean;
|
|
24
30
|
onOpenChange: (open: boolean) => void;
|
|
25
31
|
api?: string;
|
|
32
|
+
requestMode?: AIRequestMode;
|
|
33
|
+
requestHeaders?: AIRequestHeaders;
|
|
34
|
+
requestStream?: AIRequestStream;
|
|
26
35
|
suggestedQuestions?: string[];
|
|
27
36
|
aiLabel?: string;
|
|
28
37
|
loaderVariant?: LoaderVariant;
|
|
@@ -36,6 +45,9 @@ type FloatingPosition = "bottom-right" | "bottom-left" | "bottom-center";
|
|
|
36
45
|
type FloatingStyle = "panel" | "modal" | "popover" | "full-modal";
|
|
37
46
|
declare function FloatingAIChat({
|
|
38
47
|
api,
|
|
48
|
+
requestMode,
|
|
49
|
+
requestHeaders,
|
|
50
|
+
requestStream,
|
|
39
51
|
position,
|
|
40
52
|
floatingStyle,
|
|
41
53
|
triggerComponentHtml,
|
|
@@ -49,6 +61,9 @@ declare function FloatingAIChat({
|
|
|
49
61
|
feedbackEnabled
|
|
50
62
|
}: {
|
|
51
63
|
api?: string;
|
|
64
|
+
requestMode?: AIRequestMode;
|
|
65
|
+
requestHeaders?: AIRequestHeaders;
|
|
66
|
+
requestStream?: AIRequestStream;
|
|
52
67
|
position?: FloatingPosition;
|
|
53
68
|
floatingStyle?: FloatingStyle;
|
|
54
69
|
triggerComponentHtml?: string;
|
|
@@ -65,6 +80,9 @@ declare function AIModalDialog({
|
|
|
65
80
|
open,
|
|
66
81
|
onOpenChange,
|
|
67
82
|
api,
|
|
83
|
+
requestMode,
|
|
84
|
+
requestHeaders,
|
|
85
|
+
requestStream,
|
|
68
86
|
suggestedQuestions,
|
|
69
87
|
aiLabel,
|
|
70
88
|
loaderVariant,
|
|
@@ -77,6 +95,9 @@ declare function AIModalDialog({
|
|
|
77
95
|
open: boolean;
|
|
78
96
|
onOpenChange: (open: boolean) => void;
|
|
79
97
|
api?: string;
|
|
98
|
+
requestMode?: AIRequestMode;
|
|
99
|
+
requestHeaders?: AIRequestHeaders;
|
|
100
|
+
requestStream?: AIRequestStream;
|
|
80
101
|
suggestedQuestions?: string[];
|
|
81
102
|
aiLabel?: string;
|
|
82
103
|
loaderVariant?: LoaderVariant;
|
|
@@ -23,6 +23,158 @@ function createAIMessageId() {
|
|
|
23
23
|
aiMessageId += 1;
|
|
24
24
|
return `ai_${Date.now().toString(36)}_${aiMessageId.toString(36)}`;
|
|
25
25
|
}
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
28
|
+
}
|
|
29
|
+
function buildAIRequestHeaders(requestHeaders, requestStream = true) {
|
|
30
|
+
return {
|
|
31
|
+
Accept: requestStream ? "text/event-stream, application/json" : "application/json",
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
...requestHeaders
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildAIRequestBody(options) {
|
|
37
|
+
const messages = options.messages.map((message) => ({
|
|
38
|
+
role: message.role,
|
|
39
|
+
content: message.content
|
|
40
|
+
}));
|
|
41
|
+
if (options.requestMode === "docs-cloud") return JSON.stringify({
|
|
42
|
+
question: options.question,
|
|
43
|
+
messages,
|
|
44
|
+
answerMode: "auto",
|
|
45
|
+
answerStyle: "public",
|
|
46
|
+
modelPreference: options.model,
|
|
47
|
+
stream: options.requestStream !== false
|
|
48
|
+
});
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
messages,
|
|
51
|
+
model: options.model,
|
|
52
|
+
stream: options.requestStream !== false
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function getOpenAICompatibleDeltaContent(value) {
|
|
56
|
+
if (!isPlainObject(value)) return void 0;
|
|
57
|
+
const firstChoice = Array.isArray(value.choices) ? value.choices[0] : void 0;
|
|
58
|
+
if (!isPlainObject(firstChoice)) return void 0;
|
|
59
|
+
const delta = firstChoice.delta;
|
|
60
|
+
if (!isPlainObject(delta)) return void 0;
|
|
61
|
+
return typeof delta.content === "string" ? delta.content : void 0;
|
|
62
|
+
}
|
|
63
|
+
function getDocsCloudAnswerContent(value) {
|
|
64
|
+
if (typeof value === "string") return value;
|
|
65
|
+
if (!isPlainObject(value)) return void 0;
|
|
66
|
+
for (const key of [
|
|
67
|
+
"answer",
|
|
68
|
+
"content",
|
|
69
|
+
"text",
|
|
70
|
+
"delta"
|
|
71
|
+
]) {
|
|
72
|
+
const content = value[key];
|
|
73
|
+
if (typeof content === "string") return content;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function readAIResponseContent(response, onContent) {
|
|
77
|
+
if ((response.headers.get("content-type")?.toLowerCase() ?? "").includes("application/json")) {
|
|
78
|
+
const answer = getDocsCloudAnswerContent(await response.json().catch(() => null)) ?? "";
|
|
79
|
+
if (answer) onContent(answer);
|
|
80
|
+
return answer;
|
|
81
|
+
}
|
|
82
|
+
if (!response.body) return "";
|
|
83
|
+
const reader = response.body.getReader();
|
|
84
|
+
const decoder = new TextDecoder();
|
|
85
|
+
let buffer = "";
|
|
86
|
+
let assistantContent = "";
|
|
87
|
+
while (true) {
|
|
88
|
+
const { done, value } = await reader.read();
|
|
89
|
+
if (done) break;
|
|
90
|
+
buffer += decoder.decode(value, { stream: true });
|
|
91
|
+
const lines = buffer.split("\n");
|
|
92
|
+
buffer = lines.pop() || "";
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (!line.startsWith("data: ")) continue;
|
|
95
|
+
const data = line.slice(6).trim();
|
|
96
|
+
if (!data || data === "[DONE]") continue;
|
|
97
|
+
try {
|
|
98
|
+
const json = JSON.parse(data);
|
|
99
|
+
const content = getOpenAICompatibleDeltaContent(json) ?? getDocsCloudAnswerContent(json);
|
|
100
|
+
if (content) {
|
|
101
|
+
assistantContent += content;
|
|
102
|
+
onContent(assistantContent);
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return assistantContent;
|
|
108
|
+
}
|
|
109
|
+
function createSmoothAIStreamRenderer(onRender) {
|
|
110
|
+
let targetContent = "";
|
|
111
|
+
let visibleContent = "";
|
|
112
|
+
let frameId = null;
|
|
113
|
+
let lastFrameAt = 0;
|
|
114
|
+
let finishing = false;
|
|
115
|
+
let cancelled = false;
|
|
116
|
+
let resolveFinish;
|
|
117
|
+
const scheduleFrame = () => {
|
|
118
|
+
if (frameId !== null || cancelled) return;
|
|
119
|
+
if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
|
|
120
|
+
frameId = window.requestAnimationFrame(renderFrame);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
frameId = setTimeout(() => renderFrame(Date.now()), 16);
|
|
124
|
+
};
|
|
125
|
+
const completeIfReady = () => {
|
|
126
|
+
if (!finishing || visibleContent !== targetContent || !resolveFinish) return;
|
|
127
|
+
const resolve = resolveFinish;
|
|
128
|
+
resolveFinish = void 0;
|
|
129
|
+
resolve(visibleContent);
|
|
130
|
+
};
|
|
131
|
+
const nextRevealCount = (gap, elapsedMs) => {
|
|
132
|
+
if (gap <= 0) return 0;
|
|
133
|
+
let charsPerSecond = 900;
|
|
134
|
+
if (gap > 2400) charsPerSecond = finishing ? 3600 : 3e3;
|
|
135
|
+
else if (gap > 1e3) charsPerSecond = finishing ? 2400 : 2e3;
|
|
136
|
+
else if (gap > 220) charsPerSecond = finishing ? 1600 : 1300;
|
|
137
|
+
return Math.max(1, Math.min(gap, Math.ceil(charsPerSecond * elapsedMs / 1e3)));
|
|
138
|
+
};
|
|
139
|
+
function renderFrame(timestamp) {
|
|
140
|
+
frameId = null;
|
|
141
|
+
if (cancelled) return;
|
|
142
|
+
const elapsedMs = lastFrameAt ? Math.min(48, Math.max(8, timestamp - lastFrameAt)) : 16;
|
|
143
|
+
lastFrameAt = timestamp;
|
|
144
|
+
const gap = targetContent.length - visibleContent.length;
|
|
145
|
+
if (gap > 0) {
|
|
146
|
+
const nextLength = visibleContent.length + nextRevealCount(gap, elapsedMs);
|
|
147
|
+
visibleContent = targetContent.slice(0, nextLength);
|
|
148
|
+
onRender(visibleContent);
|
|
149
|
+
}
|
|
150
|
+
if (visibleContent.length < targetContent.length) {
|
|
151
|
+
scheduleFrame();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
completeIfReady();
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
push(content) {
|
|
158
|
+
if (cancelled) return;
|
|
159
|
+
targetContent = content;
|
|
160
|
+
scheduleFrame();
|
|
161
|
+
},
|
|
162
|
+
finish() {
|
|
163
|
+
finishing = true;
|
|
164
|
+
if (visibleContent === targetContent) return Promise.resolve(visibleContent);
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
resolveFinish = resolve;
|
|
167
|
+
scheduleFrame();
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
cancel() {
|
|
171
|
+
cancelled = true;
|
|
172
|
+
if (frameId !== null) if (typeof window !== "undefined" && typeof window.cancelAnimationFrame === "function") window.cancelAnimationFrame(frameId);
|
|
173
|
+
else clearTimeout(frameId);
|
|
174
|
+
frameId = null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
26
178
|
function getLastUserQuestion(messages, assistantIndex) {
|
|
27
179
|
for (let i = assistantIndex - 1; i >= 0; i -= 1) {
|
|
28
180
|
const message = messages[i];
|
|
@@ -561,7 +713,7 @@ function AIFeedbackControls({ value, onCopy, onSelect }) {
|
|
|
561
713
|
]
|
|
562
714
|
});
|
|
563
715
|
}
|
|
564
|
-
function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled = true, surface = "chat" }) {
|
|
716
|
+
function AIChat({ api, requestMode, requestHeaders, requestStream = true, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled = true, surface = "chat" }) {
|
|
565
717
|
const label = aiLabel || "AI";
|
|
566
718
|
const aiInputRef = useRef(null);
|
|
567
719
|
const messagesEndRef = useRef(null);
|
|
@@ -571,8 +723,8 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
571
723
|
});
|
|
572
724
|
const effectiveModelId = selectedModel || (Array.isArray(models) && models.length > 0 ? models[0].id : void 0);
|
|
573
725
|
useEffect(() => {
|
|
574
|
-
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
575
|
-
}, [messages]);
|
|
726
|
+
messagesEndRef.current?.scrollIntoView({ behavior: isStreaming ? "auto" : "smooth" });
|
|
727
|
+
}, [isStreaming, messages]);
|
|
576
728
|
useEffect(() => {
|
|
577
729
|
aiInputRef.current?.focus();
|
|
578
730
|
}, []);
|
|
@@ -602,15 +754,16 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
602
754
|
model: effectiveModelId
|
|
603
755
|
}
|
|
604
756
|
});
|
|
757
|
+
let streamRenderer;
|
|
605
758
|
try {
|
|
606
759
|
const res = await fetch(api, {
|
|
607
760
|
method: "POST",
|
|
608
|
-
headers:
|
|
609
|
-
body:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
761
|
+
headers: buildAIRequestHeaders(requestHeaders, requestStream),
|
|
762
|
+
body: buildAIRequestBody({
|
|
763
|
+
requestMode,
|
|
764
|
+
requestStream,
|
|
765
|
+
question,
|
|
766
|
+
messages: newMessages,
|
|
614
767
|
model: effectiveModelId
|
|
615
768
|
})
|
|
616
769
|
});
|
|
@@ -636,46 +789,32 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
636
789
|
});
|
|
637
790
|
return;
|
|
638
791
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const data = line.slice(6).trim();
|
|
651
|
-
if (data === "[DONE]") continue;
|
|
652
|
-
try {
|
|
653
|
-
const content = JSON.parse(data).choices?.[0]?.delta?.content;
|
|
654
|
-
if (content) {
|
|
655
|
-
assistantContent += content;
|
|
656
|
-
setMessages([...newMessages, {
|
|
657
|
-
...assistantMessage,
|
|
658
|
-
content: assistantContent
|
|
659
|
-
}]);
|
|
660
|
-
}
|
|
661
|
-
} catch {}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
if (assistantContent) setMessages([...newMessages, {
|
|
792
|
+
streamRenderer = createSmoothAIStreamRenderer((content) => {
|
|
793
|
+
setMessages([...newMessages, {
|
|
794
|
+
...assistantMessage,
|
|
795
|
+
content
|
|
796
|
+
}]);
|
|
797
|
+
});
|
|
798
|
+
const assistantContent = await readAIResponseContent(res, (content) => {
|
|
799
|
+
streamRenderer?.push(content);
|
|
800
|
+
});
|
|
801
|
+
const finalContent = await streamRenderer.finish() || assistantContent;
|
|
802
|
+
if (finalContent) setMessages([...newMessages, {
|
|
665
803
|
...assistantMessage,
|
|
666
|
-
content:
|
|
804
|
+
content: finalContent
|
|
667
805
|
}]);
|
|
668
806
|
if (analytics) emitClientAnalyticsEvent({
|
|
669
807
|
type: "ai_response",
|
|
670
808
|
properties: {
|
|
671
809
|
surface,
|
|
672
810
|
questionLength: question.length,
|
|
673
|
-
responseLength:
|
|
811
|
+
responseLength: finalContent.length,
|
|
674
812
|
durationMs: Math.max(0, Date.now() - startedAt),
|
|
675
813
|
model: effectiveModelId
|
|
676
814
|
}
|
|
677
815
|
});
|
|
678
816
|
} catch {
|
|
817
|
+
streamRenderer?.cancel();
|
|
679
818
|
setMessages([...newMessages, {
|
|
680
819
|
...assistantMessage,
|
|
681
820
|
content: "Failed to connect. Please try again.",
|
|
@@ -694,6 +833,9 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
694
833
|
}, [
|
|
695
834
|
messages,
|
|
696
835
|
api,
|
|
836
|
+
requestMode,
|
|
837
|
+
requestHeaders,
|
|
838
|
+
requestStream,
|
|
697
839
|
isStreaming,
|
|
698
840
|
setMessages,
|
|
699
841
|
setAiInput,
|
|
@@ -860,7 +1002,7 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
|
|
|
860
1002
|
})]
|
|
861
1003
|
});
|
|
862
1004
|
}
|
|
863
|
-
function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
1005
|
+
function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", requestMode, requestHeaders, requestStream, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
864
1006
|
const [tab, setTab] = useState("search");
|
|
865
1007
|
const [searchQuery, setSearchQuery] = useState("");
|
|
866
1008
|
const [searchResults, setSearchResults] = useState([]);
|
|
@@ -1096,6 +1238,9 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
|
|
|
1096
1238
|
}),
|
|
1097
1239
|
tab === "ai" && /* @__PURE__ */ jsx(AIChat, {
|
|
1098
1240
|
api,
|
|
1241
|
+
requestMode,
|
|
1242
|
+
requestHeaders,
|
|
1243
|
+
requestStream,
|
|
1099
1244
|
messages,
|
|
1100
1245
|
setMessages,
|
|
1101
1246
|
aiInput,
|
|
@@ -1184,7 +1329,7 @@ function getContainerStyles(style, position) {
|
|
|
1184
1329
|
function getAnimation(style) {
|
|
1185
1330
|
return style === "modal" ? "fd-ai-float-center-in 200ms ease-out" : "fd-ai-float-in 200ms ease-out";
|
|
1186
1331
|
}
|
|
1187
|
-
function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
1332
|
+
function FloatingAIChat({ api = "/api/docs", requestMode, requestHeaders, requestStream, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
1188
1333
|
const [mounted, setMounted] = useState(false);
|
|
1189
1334
|
const [isOpen, setIsOpen] = useState(false);
|
|
1190
1335
|
const [messages, setMessages] = useState([]);
|
|
@@ -1224,6 +1369,9 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
|
|
|
1224
1369
|
if (!mounted) return null;
|
|
1225
1370
|
if (floatingStyle === "full-modal") return /* @__PURE__ */ jsx(FullModalAIChat, {
|
|
1226
1371
|
api,
|
|
1372
|
+
requestMode,
|
|
1373
|
+
requestHeaders,
|
|
1374
|
+
requestStream,
|
|
1227
1375
|
isOpen,
|
|
1228
1376
|
setIsOpen,
|
|
1229
1377
|
closeAI: closeFloatingAI,
|
|
@@ -1280,6 +1428,9 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
|
|
|
1280
1428
|
]
|
|
1281
1429
|
}), /* @__PURE__ */ jsx(AIChat, {
|
|
1282
1430
|
api,
|
|
1431
|
+
requestMode,
|
|
1432
|
+
requestHeaders,
|
|
1433
|
+
requestStream,
|
|
1283
1434
|
messages,
|
|
1284
1435
|
setMessages,
|
|
1285
1436
|
aiInput,
|
|
@@ -1329,7 +1480,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
|
|
|
1329
1480
|
}))
|
|
1330
1481
|
] }), document.body);
|
|
1331
1482
|
}
|
|
1332
|
-
function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position, models, defaultModelId, analytics, feedbackEnabled = true }) {
|
|
1483
|
+
function FullModalAIChat({ api, requestMode, requestHeaders, requestStream, isOpen, setIsOpen, closeAI, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position, models, defaultModelId, analytics, feedbackEnabled = true }) {
|
|
1333
1484
|
const label = aiLabel || "AI";
|
|
1334
1485
|
const inputRef = useRef(null);
|
|
1335
1486
|
const listRef = useRef(null);
|
|
@@ -1345,9 +1496,9 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1345
1496
|
useEffect(() => {
|
|
1346
1497
|
if (listRef.current) listRef.current.scrollTo({
|
|
1347
1498
|
top: listRef.current.scrollHeight,
|
|
1348
|
-
behavior: "smooth"
|
|
1499
|
+
behavior: isStreaming ? "auto" : "smooth"
|
|
1349
1500
|
});
|
|
1350
|
-
}, [messages]);
|
|
1501
|
+
}, [isStreaming, messages]);
|
|
1351
1502
|
const submitQuestion = useCallback(async (question) => {
|
|
1352
1503
|
if (!question.trim() || isStreaming) return;
|
|
1353
1504
|
const userMessage = {
|
|
@@ -1374,15 +1525,16 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1374
1525
|
model: effectiveModelId
|
|
1375
1526
|
}
|
|
1376
1527
|
});
|
|
1528
|
+
let streamRenderer;
|
|
1377
1529
|
try {
|
|
1378
1530
|
const res = await fetch(api, {
|
|
1379
1531
|
method: "POST",
|
|
1380
|
-
headers:
|
|
1381
|
-
body:
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1532
|
+
headers: buildAIRequestHeaders(requestHeaders, requestStream),
|
|
1533
|
+
body: buildAIRequestBody({
|
|
1534
|
+
requestMode,
|
|
1535
|
+
requestStream,
|
|
1536
|
+
question,
|
|
1537
|
+
messages: newMessages,
|
|
1386
1538
|
model: effectiveModelId
|
|
1387
1539
|
})
|
|
1388
1540
|
});
|
|
@@ -1408,46 +1560,32 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1408
1560
|
});
|
|
1409
1561
|
return;
|
|
1410
1562
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const data = line.slice(6).trim();
|
|
1423
|
-
if (data === "[DONE]") continue;
|
|
1424
|
-
try {
|
|
1425
|
-
const content = JSON.parse(data).choices?.[0]?.delta?.content;
|
|
1426
|
-
if (content) {
|
|
1427
|
-
assistantContent += content;
|
|
1428
|
-
setMessages([...newMessages, {
|
|
1429
|
-
...assistantMessage,
|
|
1430
|
-
content: assistantContent
|
|
1431
|
-
}]);
|
|
1432
|
-
}
|
|
1433
|
-
} catch {}
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
if (assistantContent) setMessages([...newMessages, {
|
|
1563
|
+
streamRenderer = createSmoothAIStreamRenderer((content) => {
|
|
1564
|
+
setMessages([...newMessages, {
|
|
1565
|
+
...assistantMessage,
|
|
1566
|
+
content
|
|
1567
|
+
}]);
|
|
1568
|
+
});
|
|
1569
|
+
const assistantContent = await readAIResponseContent(res, (content) => {
|
|
1570
|
+
streamRenderer?.push(content);
|
|
1571
|
+
});
|
|
1572
|
+
const finalContent = await streamRenderer.finish() || assistantContent;
|
|
1573
|
+
if (finalContent) setMessages([...newMessages, {
|
|
1437
1574
|
...assistantMessage,
|
|
1438
|
-
content:
|
|
1575
|
+
content: finalContent
|
|
1439
1576
|
}]);
|
|
1440
1577
|
if (analytics) emitClientAnalyticsEvent({
|
|
1441
1578
|
type: "ai_response",
|
|
1442
1579
|
properties: {
|
|
1443
1580
|
surface: "full-modal",
|
|
1444
1581
|
questionLength: question.length,
|
|
1445
|
-
responseLength:
|
|
1582
|
+
responseLength: finalContent.length,
|
|
1446
1583
|
durationMs: Math.max(0, Date.now() - startedAt),
|
|
1447
1584
|
model: effectiveModelId
|
|
1448
1585
|
}
|
|
1449
1586
|
});
|
|
1450
1587
|
} catch {
|
|
1588
|
+
streamRenderer?.cancel();
|
|
1451
1589
|
setMessages([...newMessages, {
|
|
1452
1590
|
...assistantMessage,
|
|
1453
1591
|
content: "Failed to connect. Please try again.",
|
|
@@ -1466,6 +1604,9 @@ function FullModalAIChat({ api, isOpen, setIsOpen, closeAI, messages, setMessage
|
|
|
1466
1604
|
}, [
|
|
1467
1605
|
messages,
|
|
1468
1606
|
api,
|
|
1607
|
+
requestMode,
|
|
1608
|
+
requestHeaders,
|
|
1609
|
+
requestStream,
|
|
1469
1610
|
isStreaming,
|
|
1470
1611
|
setMessages,
|
|
1471
1612
|
setAiInput,
|
|
@@ -1677,7 +1818,7 @@ function TrashIcon() {
|
|
|
1677
1818
|
]
|
|
1678
1819
|
});
|
|
1679
1820
|
}
|
|
1680
|
-
function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
1821
|
+
function AIModalDialog({ open, onOpenChange, api = "/api/docs", requestMode, requestHeaders, requestStream, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics = false, feedbackEnabled = true }) {
|
|
1681
1822
|
const [messages, setMessages] = useState([]);
|
|
1682
1823
|
const [aiInput, setAiInput] = useState("");
|
|
1683
1824
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
@@ -1746,6 +1887,9 @@ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestio
|
|
|
1746
1887
|
}),
|
|
1747
1888
|
/* @__PURE__ */ jsx(AIChat, {
|
|
1748
1889
|
api,
|
|
1890
|
+
requestMode,
|
|
1891
|
+
requestHeaders,
|
|
1892
|
+
requestStream,
|
|
1749
1893
|
messages,
|
|
1750
1894
|
setMessages,
|
|
1751
1895
|
aiInput,
|
|
@@ -4,6 +4,9 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
4
4
|
interface DocsAIFeaturesProps {
|
|
5
5
|
mode: "search" | "floating" | "sidebar-icon";
|
|
6
6
|
api?: string;
|
|
7
|
+
requestMode?: "openai-chat" | "docs-cloud";
|
|
8
|
+
requestHeaders?: Record<string, string>;
|
|
9
|
+
requestStream?: boolean;
|
|
7
10
|
locale?: string;
|
|
8
11
|
position?: "bottom-right" | "bottom-left" | "bottom-center";
|
|
9
12
|
floatingStyle?: "panel" | "modal" | "popover" | "full-modal";
|
|
@@ -23,6 +26,9 @@ interface DocsAIFeaturesProps {
|
|
|
23
26
|
declare function DocsAIFeatures({
|
|
24
27
|
mode,
|
|
25
28
|
api,
|
|
29
|
+
requestMode,
|
|
30
|
+
requestHeaders,
|
|
31
|
+
requestStream,
|
|
26
32
|
locale,
|
|
27
33
|
position,
|
|
28
34
|
floatingStyle,
|
|
@@ -22,10 +22,13 @@ 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, feedbackEnabled = true }) {
|
|
25
|
+
function DocsAIFeatures({ mode, api = "/api/docs", requestMode, requestHeaders, requestStream, 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,
|
|
29
|
+
requestMode,
|
|
30
|
+
requestHeaders,
|
|
31
|
+
requestStream,
|
|
29
32
|
suggestedQuestions,
|
|
30
33
|
aiLabel,
|
|
31
34
|
loaderVariant,
|
|
@@ -37,6 +40,9 @@ function DocsAIFeatures({ mode, api = "/api/docs", locale, position = "bottom-ri
|
|
|
37
40
|
});
|
|
38
41
|
if (mode === "sidebar-icon") return /* @__PURE__ */ jsx(SidebarIconModeAI, {
|
|
39
42
|
api: localizedApi,
|
|
43
|
+
requestMode,
|
|
44
|
+
requestHeaders,
|
|
45
|
+
requestStream,
|
|
40
46
|
suggestedQuestions,
|
|
41
47
|
aiLabel,
|
|
42
48
|
loaderVariant,
|
|
@@ -48,6 +54,9 @@ function DocsAIFeatures({ mode, api = "/api/docs", locale, position = "bottom-ri
|
|
|
48
54
|
});
|
|
49
55
|
return /* @__PURE__ */ jsx(FloatingAIChat, {
|
|
50
56
|
api: localizedApi,
|
|
57
|
+
requestMode,
|
|
58
|
+
requestHeaders,
|
|
59
|
+
requestStream,
|
|
51
60
|
position,
|
|
52
61
|
floatingStyle,
|
|
53
62
|
triggerComponentHtml,
|
|
@@ -61,7 +70,7 @@ function DocsAIFeatures({ mode, api = "/api/docs", locale, position = "bottom-ri
|
|
|
61
70
|
feedbackEnabled
|
|
62
71
|
});
|
|
63
72
|
}
|
|
64
|
-
function SearchModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled }) {
|
|
73
|
+
function SearchModeAI({ api, requestMode, requestHeaders, requestStream, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled }) {
|
|
65
74
|
const [open, setOpen] = useState(false);
|
|
66
75
|
useEffect(() => {
|
|
67
76
|
function handler(e) {
|
|
@@ -108,6 +117,9 @@ function SearchModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loading
|
|
|
108
117
|
open,
|
|
109
118
|
onOpenChange: setOpen,
|
|
110
119
|
api,
|
|
120
|
+
requestMode,
|
|
121
|
+
requestHeaders,
|
|
122
|
+
requestStream,
|
|
111
123
|
suggestedQuestions,
|
|
112
124
|
aiLabel,
|
|
113
125
|
loaderVariant,
|
|
@@ -118,7 +130,7 @@ function SearchModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loading
|
|
|
118
130
|
feedbackEnabled
|
|
119
131
|
});
|
|
120
132
|
}
|
|
121
|
-
function SidebarIconModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled }) {
|
|
133
|
+
function SidebarIconModeAI({ api, requestMode, requestHeaders, requestStream, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, models, defaultModelId, analytics, feedbackEnabled }) {
|
|
122
134
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
123
135
|
const [aiOpen, setAiOpen] = useState(false);
|
|
124
136
|
useEffect(() => {
|
|
@@ -165,6 +177,9 @@ function SidebarIconModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, lo
|
|
|
165
177
|
open: searchOpen,
|
|
166
178
|
onOpenChange: setSearchOpen,
|
|
167
179
|
api,
|
|
180
|
+
requestMode,
|
|
181
|
+
requestHeaders,
|
|
182
|
+
requestStream,
|
|
168
183
|
suggestedQuestions,
|
|
169
184
|
aiLabel,
|
|
170
185
|
loaderVariant,
|
|
@@ -177,6 +192,9 @@ function SidebarIconModeAI({ api, suggestedQuestions, aiLabel, loaderVariant, lo
|
|
|
177
192
|
open: aiOpen,
|
|
178
193
|
onOpenChange: setAiOpen,
|
|
179
194
|
api,
|
|
195
|
+
requestMode,
|
|
196
|
+
requestHeaders,
|
|
197
|
+
requestStream,
|
|
180
198
|
suggestedQuestions,
|
|
181
199
|
aiLabel,
|
|
182
200
|
loaderVariant,
|
package/dist/docs-api.d.mts
CHANGED
|
@@ -16,6 +16,7 @@ interface AIModelConfig {
|
|
|
16
16
|
}
|
|
17
17
|
interface AIOptions {
|
|
18
18
|
enabled?: boolean;
|
|
19
|
+
provider?: string;
|
|
19
20
|
model?: string | AIModelConfig;
|
|
20
21
|
providers?: Record<string, AIProviderConfig>;
|
|
21
22
|
systemPrompt?: string;
|
|
@@ -29,6 +30,7 @@ interface AIOptions {
|
|
|
29
30
|
apiKey?: string;
|
|
30
31
|
maxResults?: number;
|
|
31
32
|
useMcp?: boolean | DocsAskAIMcpConfig;
|
|
33
|
+
stream?: boolean;
|
|
32
34
|
}
|
|
33
35
|
interface DocsAPIOptions {
|
|
34
36
|
rootDir?: string;
|
|
@@ -44,6 +46,8 @@ interface DocsAPIOptions {
|
|
|
44
46
|
language?: string;
|
|
45
47
|
/** AI chat configuration */
|
|
46
48
|
ai?: AIOptions;
|
|
49
|
+
/** Hosted Docs Cloud configuration. */
|
|
50
|
+
cloud?: DocsConfig["cloud"];
|
|
47
51
|
/** i18n config (optional) */
|
|
48
52
|
i18n?: DocsI18nConfig;
|
|
49
53
|
/** Search configuration */
|
package/dist/docs-api.mjs
CHANGED
|
@@ -35,6 +35,8 @@ const FILE_EXTS = [
|
|
|
35
35
|
"js"
|
|
36
36
|
];
|
|
37
37
|
const DEFAULT_DOCS_API_ROUTE = "/api/docs";
|
|
38
|
+
const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://docs-app.farming-labs.dev";
|
|
39
|
+
const DEFAULT_DOCS_CLOUD_API_KEY_ENV = "DOCS_CLOUD_API_KEY";
|
|
38
40
|
const DEFAULT_AGENT_SPEC_ROUTE = "/api/docs/agent/spec";
|
|
39
41
|
const DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE = "/.well-known/agent";
|
|
40
42
|
const DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE = "/.well-known/agent.json";
|
|
@@ -513,21 +515,25 @@ function readAIConfig(root) {
|
|
|
513
515
|
if (!content.includes("ai:") && !content.includes("ai :")) return {};
|
|
514
516
|
const enabledMatch = content.match(/ai\s*:\s*\{[^}]*enabled\s*:\s*(true|false)/s);
|
|
515
517
|
if (enabledMatch && enabledMatch[1] === "false") return {};
|
|
518
|
+
const providerMatch = content.match(/ai\s*:\s*\{[^}]*provider\s*:\s*["']([^"']+)["']/s);
|
|
516
519
|
const modelMatch = content.match(/ai\s*:\s*\{[^}]*model\s*:\s*["']([^"']+)["']/s);
|
|
517
520
|
const baseUrlMatch = content.match(/ai\s*:\s*\{[^}]*baseUrl\s*:\s*["']([^"']+)["']/s);
|
|
518
521
|
const apiKeyMatch = content.match(/ai\s*:\s*\{[^}]*apiKey\s*:\s*process\.env\.(\w+)/s);
|
|
519
522
|
const maxResultsMatch = content.match(/ai\s*:\s*\{[^}]*maxResults\s*:\s*(\d+)/s);
|
|
520
523
|
const useMcpMatch = content.match(/ai\s*:\s*\{[^}]*useMcp\s*:\s*(true|false)/s);
|
|
524
|
+
const streamMatch = content.match(/ai\s*:\s*\{[^}]*stream\s*:\s*(true|false)/s);
|
|
521
525
|
const systemPromptMatch = content.match(/ai\s*:\s*\{[^}]*systemPrompt\s*:\s*["'`]([^"'`]+)["'`]/s);
|
|
522
526
|
const packageNameMatch = content.match(/ai\s*:\s*\{[^}]*packageName\s*:\s*["'`]([^"'`]+)["'`]/s);
|
|
523
527
|
const docsUrlMatch = content.match(/ai\s*:\s*\{[^}]*docsUrl\s*:\s*["'`]([^"'`]+)["'`]/s);
|
|
524
528
|
return {
|
|
525
529
|
enabled: true,
|
|
530
|
+
provider: providerMatch?.[1],
|
|
526
531
|
model: modelMatch?.[1],
|
|
527
532
|
baseUrl: baseUrlMatch?.[1],
|
|
528
533
|
apiKey: apiKeyMatch?.[1] ? process.env[apiKeyMatch[1]] : void 0,
|
|
529
534
|
maxResults: maxResultsMatch ? parseInt(maxResultsMatch[1], 10) : void 0,
|
|
530
535
|
useMcp: useMcpMatch ? useMcpMatch[1] === "true" : void 0,
|
|
536
|
+
stream: streamMatch ? streamMatch[1] === "true" : void 0,
|
|
531
537
|
systemPrompt: systemPromptMatch?.[1],
|
|
532
538
|
packageName: packageNameMatch?.[1],
|
|
533
539
|
docsUrl: docsUrlMatch?.[1]
|
|
@@ -536,6 +542,22 @@ function readAIConfig(root) {
|
|
|
536
542
|
}
|
|
537
543
|
return {};
|
|
538
544
|
}
|
|
545
|
+
function readCloudConfig(root) {
|
|
546
|
+
for (const ext of FILE_EXTS) {
|
|
547
|
+
const configPath = path.join(root, `docs.config.${ext}`);
|
|
548
|
+
if (!fs.existsSync(configPath)) continue;
|
|
549
|
+
try {
|
|
550
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
551
|
+
const sanitized = stripCommentsAndStrings(content);
|
|
552
|
+
const configObject = extractRootConfigObject(content, sanitized);
|
|
553
|
+
const cloudBlock = extractObjectLiteral(configObject?.content ?? content, configObject?.sanitized ?? sanitized, "cloud");
|
|
554
|
+
if (!cloudBlock) continue;
|
|
555
|
+
const apiKeyBlock = extractObjectLiteral(cloudBlock, stripCommentsAndStrings(cloudBlock), "apiKey");
|
|
556
|
+
const apiKeyEnv = apiKeyBlock ? readStringFromBlock(apiKeyBlock, "env") : void 0;
|
|
557
|
+
return apiKeyEnv ? { apiKey: { env: apiKeyEnv } } : {};
|
|
558
|
+
} catch {}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
539
561
|
function readMcpConfig(root) {
|
|
540
562
|
for (const ext of FILE_EXTS) {
|
|
541
563
|
const configPath = path.join(root, `docs.config.${ext}`);
|
|
@@ -1294,6 +1316,121 @@ function resolveModelAndProvider(aiConfig, requestedModelId) {
|
|
|
1294
1316
|
apiKey
|
|
1295
1317
|
};
|
|
1296
1318
|
}
|
|
1319
|
+
function readRuntimeEnv(name) {
|
|
1320
|
+
const value = process.env[name]?.trim();
|
|
1321
|
+
return value ? value : void 0;
|
|
1322
|
+
}
|
|
1323
|
+
function resolveDocsCloudApiBaseUrl() {
|
|
1324
|
+
return (readRuntimeEnv("DOCS_CLOUD_API_URL") ?? readRuntimeEnv("NEXT_PUBLIC_DOCS_CLOUD_URL") ?? DEFAULT_DOCS_CLOUD_API_BASE_URL).replace(/\/+$/, "");
|
|
1325
|
+
}
|
|
1326
|
+
function resolveDocsCloudProjectId() {
|
|
1327
|
+
return readRuntimeEnv("NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID") ?? readRuntimeEnv("DOCS_CLOUD_PROJECT_ID");
|
|
1328
|
+
}
|
|
1329
|
+
function resolveDocsCloudApiKey(cloudConfig) {
|
|
1330
|
+
const envName = cloudConfig?.apiKey?.env?.trim() || DEFAULT_DOCS_CLOUD_API_KEY_ENV;
|
|
1331
|
+
return {
|
|
1332
|
+
envName,
|
|
1333
|
+
apiKey: readRuntimeEnv(envName)
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function createOpenAICompatibleSseChunk(content) {
|
|
1337
|
+
return `data: ${JSON.stringify({ choices: [{ delta: { content } }] })}\n\n`;
|
|
1338
|
+
}
|
|
1339
|
+
function createOpenAICompatibleSseResponse(content) {
|
|
1340
|
+
const encoder = new TextEncoder();
|
|
1341
|
+
const stream = new ReadableStream({ start(controller) {
|
|
1342
|
+
if (content) controller.enqueue(encoder.encode(createOpenAICompatibleSseChunk(content)));
|
|
1343
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
1344
|
+
controller.close();
|
|
1345
|
+
} });
|
|
1346
|
+
return new Response(stream, { headers: {
|
|
1347
|
+
"Content-Type": "text/event-stream",
|
|
1348
|
+
"Cache-Control": "no-cache",
|
|
1349
|
+
Connection: "keep-alive"
|
|
1350
|
+
} });
|
|
1351
|
+
}
|
|
1352
|
+
function getOpenAICompatibleDeltaContent(value) {
|
|
1353
|
+
if (!isPlainObject(value)) return void 0;
|
|
1354
|
+
const firstChoice = Array.isArray(value.choices) ? value.choices[0] : void 0;
|
|
1355
|
+
if (!isPlainObject(firstChoice)) return void 0;
|
|
1356
|
+
const delta = firstChoice.delta;
|
|
1357
|
+
if (!isPlainObject(delta)) return void 0;
|
|
1358
|
+
return typeof delta.content === "string" ? delta.content : void 0;
|
|
1359
|
+
}
|
|
1360
|
+
function getDocsCloudStreamContent(value) {
|
|
1361
|
+
if (typeof value === "string") return value;
|
|
1362
|
+
if (!isPlainObject(value)) return void 0;
|
|
1363
|
+
for (const key of [
|
|
1364
|
+
"content",
|
|
1365
|
+
"text",
|
|
1366
|
+
"answer",
|
|
1367
|
+
"delta"
|
|
1368
|
+
]) {
|
|
1369
|
+
const content = value[key];
|
|
1370
|
+
if (typeof content === "string") return content;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function createDocsCloudSseProxyResponse(body) {
|
|
1374
|
+
if (!body) return createOpenAICompatibleSseResponse("");
|
|
1375
|
+
const decoder = new TextDecoder();
|
|
1376
|
+
const encoder = new TextEncoder();
|
|
1377
|
+
const stream = new ReadableStream({ async start(controller) {
|
|
1378
|
+
const reader = body.getReader();
|
|
1379
|
+
let buffer = "";
|
|
1380
|
+
let doneSent = false;
|
|
1381
|
+
const enqueue = (chunk) => {
|
|
1382
|
+
controller.enqueue(encoder.encode(chunk));
|
|
1383
|
+
};
|
|
1384
|
+
const flushEvent = (event) => {
|
|
1385
|
+
const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter(Boolean).join("\n").trim();
|
|
1386
|
+
if (!data) return;
|
|
1387
|
+
if (data === "[DONE]") {
|
|
1388
|
+
doneSent = true;
|
|
1389
|
+
enqueue("data: [DONE]\n\n");
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
try {
|
|
1393
|
+
const parsed = JSON.parse(data);
|
|
1394
|
+
if (getOpenAICompatibleDeltaContent(parsed) !== void 0) {
|
|
1395
|
+
enqueue(`data: ${data}\n\n`);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const content = getDocsCloudStreamContent(parsed);
|
|
1399
|
+
if (content) enqueue(createOpenAICompatibleSseChunk(content));
|
|
1400
|
+
} catch {
|
|
1401
|
+
enqueue(createOpenAICompatibleSseChunk(data));
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
try {
|
|
1405
|
+
while (true) {
|
|
1406
|
+
const { value, done } = await reader.read();
|
|
1407
|
+
if (done) break;
|
|
1408
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1409
|
+
const events = buffer.split(/\r?\n\r?\n/);
|
|
1410
|
+
buffer = events.pop() ?? "";
|
|
1411
|
+
for (const event of events) flushEvent(event);
|
|
1412
|
+
}
|
|
1413
|
+
buffer += decoder.decode();
|
|
1414
|
+
if (buffer.trim()) flushEvent(buffer);
|
|
1415
|
+
if (!doneSent) enqueue("data: [DONE]\n\n");
|
|
1416
|
+
controller.close();
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
controller.error(error);
|
|
1419
|
+
}
|
|
1420
|
+
} });
|
|
1421
|
+
return new Response(stream, { headers: {
|
|
1422
|
+
"Content-Type": "text/event-stream",
|
|
1423
|
+
"Cache-Control": "no-cache",
|
|
1424
|
+
Connection: "keep-alive"
|
|
1425
|
+
} });
|
|
1426
|
+
}
|
|
1427
|
+
function getDocsCloudAnswerFromPayload(payload) {
|
|
1428
|
+
if (!isPlainObject(payload)) return "";
|
|
1429
|
+
const answer = payload.answer;
|
|
1430
|
+
if (typeof answer === "string") return answer;
|
|
1431
|
+
const text = payload.text;
|
|
1432
|
+
return typeof text === "string" ? text : "";
|
|
1433
|
+
}
|
|
1297
1434
|
function safeUrlOrigin(value) {
|
|
1298
1435
|
try {
|
|
1299
1436
|
return new URL(value).origin;
|
|
@@ -1305,7 +1442,7 @@ function getRequestAnalyticsProperties(request) {
|
|
|
1305
1442
|
const userAgent = request.headers.get("user-agent")?.trim();
|
|
1306
1443
|
return userAgent ? { userAgent } : {};
|
|
1307
1444
|
}
|
|
1308
|
-
async function handleAskAI(request, indexes, aiConfig, search, analytics, observability, analyticsContext = {}) {
|
|
1445
|
+
async function handleAskAI(request, indexes, aiConfig, cloudConfig, search, analytics, observability, analyticsContext = {}) {
|
|
1309
1446
|
const url = new URL(request.url);
|
|
1310
1447
|
const requestAnalyticsProperties = getRequestAnalyticsProperties(request);
|
|
1311
1448
|
const requestStartedAt = Date.now();
|
|
@@ -1387,6 +1524,7 @@ async function handleAskAI(request, indexes, aiConfig, search, analytics, observ
|
|
|
1387
1524
|
return Response.json({ error: "Invalid JSON body. Expected { messages: [...] }" }, { status: 400 });
|
|
1388
1525
|
}
|
|
1389
1526
|
const messages = body.messages;
|
|
1527
|
+
const shouldStreamResponse = body.stream !== false && (request.headers.get("accept")?.toLowerCase().includes("text/event-stream") ?? true);
|
|
1390
1528
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1391
1529
|
await emitDocsAnalyticsEvent(analytics, {
|
|
1392
1530
|
type: "api_ai_error",
|
|
@@ -1440,6 +1578,286 @@ async function handleAskAI(request, indexes, aiConfig, search, analytics, observ
|
|
|
1440
1578
|
requestedModel: typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0
|
|
1441
1579
|
}
|
|
1442
1580
|
});
|
|
1581
|
+
if (aiConfig.provider === "docs-cloud") {
|
|
1582
|
+
const requestedModel = typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0;
|
|
1583
|
+
const model = requestedModel ?? "docs-cloud";
|
|
1584
|
+
const projectId = resolveDocsCloudProjectId();
|
|
1585
|
+
const { envName, apiKey } = resolveDocsCloudApiKey(cloudConfig);
|
|
1586
|
+
const cloudApiBaseUrl = resolveDocsCloudApiBaseUrl();
|
|
1587
|
+
const cloudAskUrl = projectId ? `${cloudApiBaseUrl}/v1/projects/${encodeURIComponent(projectId)}/knowledge/ask` : void 0;
|
|
1588
|
+
if (!projectId || !apiKey || !cloudAskUrl) {
|
|
1589
|
+
const reason = !projectId ? "missing_docs_cloud_project_id" : "missing_docs_cloud_api_key";
|
|
1590
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1591
|
+
type: "api_ai_error",
|
|
1592
|
+
source: "server",
|
|
1593
|
+
url: request.url,
|
|
1594
|
+
path: url.pathname,
|
|
1595
|
+
locale: analyticsContext.locale,
|
|
1596
|
+
input: { question: query },
|
|
1597
|
+
properties: {
|
|
1598
|
+
...requestAnalyticsProperties,
|
|
1599
|
+
reason,
|
|
1600
|
+
provider: "docs-cloud",
|
|
1601
|
+
messageCount: messages.length,
|
|
1602
|
+
questionLength: query.length,
|
|
1603
|
+
model,
|
|
1604
|
+
envName: !apiKey ? envName : void 0,
|
|
1605
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
await emitRunError(reason, {
|
|
1609
|
+
status: 500,
|
|
1610
|
+
provider: "docs-cloud",
|
|
1611
|
+
messageCount: messages.length,
|
|
1612
|
+
questionLength: query.length,
|
|
1613
|
+
model,
|
|
1614
|
+
envName: !apiKey ? envName : void 0
|
|
1615
|
+
});
|
|
1616
|
+
return Response.json({ error: !projectId ? "AI provider docs-cloud requires NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID or DOCS_CLOUD_PROJECT_ID." : `AI provider docs-cloud requires ${envName} to be set on the server.` }, { status: 500 });
|
|
1617
|
+
}
|
|
1618
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1619
|
+
type: "api_ai_request",
|
|
1620
|
+
source: "server",
|
|
1621
|
+
url: request.url,
|
|
1622
|
+
path: url.pathname,
|
|
1623
|
+
locale: analyticsContext.locale,
|
|
1624
|
+
input: { question: query },
|
|
1625
|
+
properties: {
|
|
1626
|
+
...requestAnalyticsProperties,
|
|
1627
|
+
provider: "docs-cloud",
|
|
1628
|
+
messageCount: messages.length,
|
|
1629
|
+
questionLength: query.length,
|
|
1630
|
+
model
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
const modelStartedAt = Date.now();
|
|
1634
|
+
const modelStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1635
|
+
const modelSpanId = createDocsAgentTraceId("span");
|
|
1636
|
+
const providerOrigin = safeUrlOrigin(cloudApiBaseUrl);
|
|
1637
|
+
await emitTrace({
|
|
1638
|
+
type: "model.call",
|
|
1639
|
+
name: model,
|
|
1640
|
+
spanId: modelSpanId,
|
|
1641
|
+
parentSpanId: runSpanId,
|
|
1642
|
+
startedAt: modelStartedAtIso,
|
|
1643
|
+
status: "started",
|
|
1644
|
+
inputPreview: {
|
|
1645
|
+
messageCount: messages.length,
|
|
1646
|
+
stream: shouldStreamResponse,
|
|
1647
|
+
providerOrigin
|
|
1648
|
+
},
|
|
1649
|
+
metadata: {
|
|
1650
|
+
model,
|
|
1651
|
+
provider: "docs-cloud"
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
let cloudResponse;
|
|
1655
|
+
try {
|
|
1656
|
+
cloudResponse = await fetch(cloudAskUrl, {
|
|
1657
|
+
method: "POST",
|
|
1658
|
+
headers: {
|
|
1659
|
+
Accept: shouldStreamResponse ? "text/event-stream, application/json" : "application/json",
|
|
1660
|
+
"Content-Type": "application/json",
|
|
1661
|
+
Authorization: `Bearer ${apiKey}`
|
|
1662
|
+
},
|
|
1663
|
+
body: JSON.stringify({
|
|
1664
|
+
question: query,
|
|
1665
|
+
answerMode: "auto",
|
|
1666
|
+
answerStyle: "public",
|
|
1667
|
+
modelPreference: requestedModel,
|
|
1668
|
+
stream: shouldStreamResponse
|
|
1669
|
+
})
|
|
1670
|
+
});
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
const elapsed = Math.max(0, Date.now() - modelStartedAt);
|
|
1673
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1674
|
+
await emitTrace({
|
|
1675
|
+
type: "model.error",
|
|
1676
|
+
name: model,
|
|
1677
|
+
parentSpanId: modelSpanId,
|
|
1678
|
+
startedAt: modelStartedAtIso,
|
|
1679
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1680
|
+
durationMs: elapsed,
|
|
1681
|
+
status: "error",
|
|
1682
|
+
outputPreview: { message },
|
|
1683
|
+
metadata: {
|
|
1684
|
+
model,
|
|
1685
|
+
provider: "docs-cloud",
|
|
1686
|
+
providerOrigin
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1690
|
+
type: "api_ai_error",
|
|
1691
|
+
source: "server",
|
|
1692
|
+
url: request.url,
|
|
1693
|
+
path: url.pathname,
|
|
1694
|
+
locale: analyticsContext.locale,
|
|
1695
|
+
input: { question: query },
|
|
1696
|
+
properties: {
|
|
1697
|
+
...requestAnalyticsProperties,
|
|
1698
|
+
reason: "docs_cloud_fetch_error",
|
|
1699
|
+
provider: "docs-cloud",
|
|
1700
|
+
messageCount: messages.length,
|
|
1701
|
+
questionLength: query.length,
|
|
1702
|
+
model,
|
|
1703
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
await emitRunError("docs_cloud_fetch_error", {
|
|
1707
|
+
status: 502,
|
|
1708
|
+
provider: "docs-cloud",
|
|
1709
|
+
messageCount: messages.length,
|
|
1710
|
+
questionLength: query.length,
|
|
1711
|
+
model
|
|
1712
|
+
});
|
|
1713
|
+
return Response.json({ error: "Docs Cloud Ask AI request failed." }, { status: 502 });
|
|
1714
|
+
}
|
|
1715
|
+
if (!cloudResponse.ok) {
|
|
1716
|
+
const errText = await cloudResponse.text().catch(() => "Unknown error");
|
|
1717
|
+
const elapsed = Math.max(0, Date.now() - modelStartedAt);
|
|
1718
|
+
await emitTrace({
|
|
1719
|
+
type: "model.error",
|
|
1720
|
+
name: model,
|
|
1721
|
+
parentSpanId: modelSpanId,
|
|
1722
|
+
startedAt: modelStartedAtIso,
|
|
1723
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1724
|
+
durationMs: elapsed,
|
|
1725
|
+
status: "error",
|
|
1726
|
+
outputPreview: {
|
|
1727
|
+
status: cloudResponse.status,
|
|
1728
|
+
errorChars: errText.length
|
|
1729
|
+
},
|
|
1730
|
+
metadata: {
|
|
1731
|
+
model,
|
|
1732
|
+
provider: "docs-cloud",
|
|
1733
|
+
providerOrigin
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1737
|
+
type: "api_ai_error",
|
|
1738
|
+
source: "server",
|
|
1739
|
+
url: request.url,
|
|
1740
|
+
path: url.pathname,
|
|
1741
|
+
locale: analyticsContext.locale,
|
|
1742
|
+
input: { question: query },
|
|
1743
|
+
properties: {
|
|
1744
|
+
...requestAnalyticsProperties,
|
|
1745
|
+
reason: "docs_cloud_error",
|
|
1746
|
+
provider: "docs-cloud",
|
|
1747
|
+
status: cloudResponse.status,
|
|
1748
|
+
messageCount: messages.length,
|
|
1749
|
+
questionLength: query.length,
|
|
1750
|
+
model,
|
|
1751
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
await emitRunError("docs_cloud_error", {
|
|
1755
|
+
status: 502,
|
|
1756
|
+
modelStatus: cloudResponse.status,
|
|
1757
|
+
provider: "docs-cloud",
|
|
1758
|
+
messageCount: messages.length,
|
|
1759
|
+
questionLength: query.length,
|
|
1760
|
+
model
|
|
1761
|
+
});
|
|
1762
|
+
return Response.json({ error: `Docs Cloud Ask AI error (${cloudResponse.status}).` }, { status: 502 });
|
|
1763
|
+
}
|
|
1764
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1765
|
+
type: "api_ai_response",
|
|
1766
|
+
source: "server",
|
|
1767
|
+
url: request.url,
|
|
1768
|
+
path: url.pathname,
|
|
1769
|
+
locale: analyticsContext.locale,
|
|
1770
|
+
input: { question: query },
|
|
1771
|
+
properties: {
|
|
1772
|
+
...requestAnalyticsProperties,
|
|
1773
|
+
provider: "docs-cloud",
|
|
1774
|
+
messageCount: messages.length,
|
|
1775
|
+
questionLength: query.length,
|
|
1776
|
+
model,
|
|
1777
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
const responseEndedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1781
|
+
const modelDurationMs = Math.max(0, Date.now() - modelStartedAt);
|
|
1782
|
+
const contentType = cloudResponse.headers.get("content-type") ?? void 0;
|
|
1783
|
+
await emitTrace({
|
|
1784
|
+
type: "model.response",
|
|
1785
|
+
name: model,
|
|
1786
|
+
parentSpanId: modelSpanId,
|
|
1787
|
+
startedAt: modelStartedAtIso,
|
|
1788
|
+
endedAt: responseEndedAt,
|
|
1789
|
+
durationMs: modelDurationMs,
|
|
1790
|
+
status: "success",
|
|
1791
|
+
outputPreview: {
|
|
1792
|
+
status: cloudResponse.status,
|
|
1793
|
+
stream: shouldStreamResponse,
|
|
1794
|
+
contentType
|
|
1795
|
+
},
|
|
1796
|
+
metadata: {
|
|
1797
|
+
model,
|
|
1798
|
+
provider: "docs-cloud",
|
|
1799
|
+
providerOrigin
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
await emitTrace({
|
|
1803
|
+
type: "model.stream",
|
|
1804
|
+
name: model,
|
|
1805
|
+
parentSpanId: modelSpanId,
|
|
1806
|
+
startedAt: modelStartedAtIso,
|
|
1807
|
+
endedAt: responseEndedAt,
|
|
1808
|
+
durationMs: modelDurationMs,
|
|
1809
|
+
status: "success",
|
|
1810
|
+
outputPreview: { stream: shouldStreamResponse },
|
|
1811
|
+
metadata: {
|
|
1812
|
+
model,
|
|
1813
|
+
provider: "docs-cloud"
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
const runDurationMs = Math.max(0, Date.now() - requestStartedAt);
|
|
1817
|
+
await emitTrace({
|
|
1818
|
+
type: "agent.final",
|
|
1819
|
+
name: "ask-ai",
|
|
1820
|
+
parentSpanId: runSpanId,
|
|
1821
|
+
startedAt: trace.startedAt,
|
|
1822
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1823
|
+
durationMs: runDurationMs,
|
|
1824
|
+
status: "success",
|
|
1825
|
+
outputPreview: {
|
|
1826
|
+
stream: shouldStreamResponse,
|
|
1827
|
+
provider: "docs-cloud"
|
|
1828
|
+
},
|
|
1829
|
+
metadata: {
|
|
1830
|
+
model,
|
|
1831
|
+
provider: "docs-cloud"
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
await emitTrace({
|
|
1835
|
+
type: "run.end",
|
|
1836
|
+
name: "ask-ai",
|
|
1837
|
+
spanId: runSpanId,
|
|
1838
|
+
startedAt: trace.startedAt,
|
|
1839
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1840
|
+
durationMs: runDurationMs,
|
|
1841
|
+
status: "success",
|
|
1842
|
+
outputPreview: {
|
|
1843
|
+
stream: shouldStreamResponse,
|
|
1844
|
+
provider: "docs-cloud"
|
|
1845
|
+
},
|
|
1846
|
+
metadata: {
|
|
1847
|
+
model,
|
|
1848
|
+
provider: "docs-cloud"
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
if (shouldStreamResponse && contentType?.includes("text/event-stream")) return createDocsCloudSseProxyResponse(cloudResponse.body);
|
|
1852
|
+
const cloudBody = await cloudResponse.text();
|
|
1853
|
+
let answer = cloudBody;
|
|
1854
|
+
let cloudPayload;
|
|
1855
|
+
try {
|
|
1856
|
+
cloudPayload = JSON.parse(cloudBody);
|
|
1857
|
+
answer = getDocsCloudAnswerFromPayload(cloudPayload);
|
|
1858
|
+
} catch {}
|
|
1859
|
+
return shouldStreamResponse ? createOpenAICompatibleSseResponse(answer) : Response.json(isPlainObject(cloudPayload) ? cloudPayload : { answer });
|
|
1860
|
+
}
|
|
1443
1861
|
const retrievalStartedAt = Date.now();
|
|
1444
1862
|
const retrievalStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1445
1863
|
const retrievalSpanId = createDocsAgentTraceId("span");
|
|
@@ -1923,6 +2341,7 @@ function createDocsAPI(options) {
|
|
|
1923
2341
|
const agentFeedbackConfig = resolveAgentFeedbackConfig(options?.feedback);
|
|
1924
2342
|
const i18n = resolveDocsI18n(options?.i18n ?? readI18nConfig(root));
|
|
1925
2343
|
const aiConfig = options?.ai ?? readAIConfig(root);
|
|
2344
|
+
const cloudConfig = options?.cloud ?? readCloudConfig(root);
|
|
1926
2345
|
const searchConfig = options?.search;
|
|
1927
2346
|
const llmsConfig = resolveLlmsTxtConfig(options?.llmsTxt, readLlmsTxtConfig(root));
|
|
1928
2347
|
const sitemapConfig = options?.sitemap ?? readSitemapConfig(root);
|
|
@@ -2496,7 +2915,7 @@ function createDocsAPI(options) {
|
|
|
2496
2915
|
return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
|
|
2497
2916
|
}
|
|
2498
2917
|
const ctx = resolveContextFromRequest(request);
|
|
2499
|
-
return handleAskAI(request, getIndexes(ctx), aiConfig, resolveAskAISearchRequestConfig({
|
|
2918
|
+
return handleAskAI(request, getIndexes(ctx), aiConfig, cloudConfig, resolveAskAISearchRequestConfig({
|
|
2500
2919
|
search: searchConfig,
|
|
2501
2920
|
useMcp: aiConfig.useMcp,
|
|
2502
2921
|
mcpEndpoint: mcpConfig.route,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/docs-cloud-ai-client.ts
|
|
2
|
+
const DEFAULT_DOCS_API_ROUTE = "/api/docs";
|
|
3
|
+
const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://docs-app.farming-labs.dev";
|
|
4
|
+
const DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV = "NEXT_PUBLIC_DOCS_CLOUD_API_KEY";
|
|
5
|
+
function readRuntimeEnv(name) {
|
|
6
|
+
const value = process.env[name]?.trim();
|
|
7
|
+
return value ? value : void 0;
|
|
8
|
+
}
|
|
9
|
+
function resolveDocsCloudApiBaseUrl() {
|
|
10
|
+
return (readRuntimeEnv("NEXT_PUBLIC_DOCS_CLOUD_URL") ?? readRuntimeEnv("DOCS_CLOUD_API_URL") ?? DEFAULT_DOCS_CLOUD_API_BASE_URL).replace(/\/+$/, "");
|
|
11
|
+
}
|
|
12
|
+
function resolveDocsCloudProjectId() {
|
|
13
|
+
return readRuntimeEnv("NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID") ?? readRuntimeEnv("DOCS_CLOUD_PROJECT_ID");
|
|
14
|
+
}
|
|
15
|
+
function resolvePublicDocsCloudApiKey(config) {
|
|
16
|
+
const configuredEnv = config.cloud?.apiKey?.env?.trim();
|
|
17
|
+
if (!configuredEnv) return readRuntimeEnv(DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV);
|
|
18
|
+
if (!configuredEnv.startsWith("NEXT_PUBLIC_")) return void 0;
|
|
19
|
+
return readRuntimeEnv(configuredEnv);
|
|
20
|
+
}
|
|
21
|
+
function resolveDocsCloudAIStream(config) {
|
|
22
|
+
const aiConfig = config.ai;
|
|
23
|
+
const stream = aiConfig?.stream ?? aiConfig?.streaming;
|
|
24
|
+
return typeof stream === "boolean" ? stream : true;
|
|
25
|
+
}
|
|
26
|
+
function resolveDocsCloudAIClientRequest(config, fallbackApi = DEFAULT_DOCS_API_ROUTE) {
|
|
27
|
+
if (config.ai?.provider !== "docs-cloud") return { api: fallbackApi };
|
|
28
|
+
const projectId = resolveDocsCloudProjectId();
|
|
29
|
+
const apiKey = resolvePublicDocsCloudApiKey(config);
|
|
30
|
+
const requestStream = resolveDocsCloudAIStream(config);
|
|
31
|
+
if (!projectId || !apiKey) return {
|
|
32
|
+
api: fallbackApi,
|
|
33
|
+
requestStream
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
api: `${resolveDocsCloudApiBaseUrl()}/v1/projects/${encodeURIComponent(projectId)}/knowledge/ask`,
|
|
37
|
+
requestMode: "docs-cloud",
|
|
38
|
+
requestStream,
|
|
39
|
+
requestHeaders: { Authorization: `Bearer ${apiKey}` }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { resolveDocsCloudAIClientRequest };
|
package/dist/docs-layout.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { serializeIcon } from "./serialize-icon.mjs";
|
|
|
2
2
|
import { withLangInUrl } from "./i18n.mjs";
|
|
3
3
|
import { DocsPageClient } from "./docs-page-client.mjs";
|
|
4
4
|
import { DocsAIFeatures } from "./docs-ai-features.mjs";
|
|
5
|
+
import { resolveDocsCloudAIClientRequest } from "./docs-cloud-ai-client.mjs";
|
|
5
6
|
import { DocsCommandSearch } from "./docs-command-search.mjs";
|
|
6
7
|
import { resolveOpenDocsProviders } from "./open-docs-providers.mjs";
|
|
7
8
|
import { resolvePageReadingTime, resolveReadingTimeOptions } from "./reading-time.mjs";
|
|
@@ -632,6 +633,7 @@ function createDocsLayout(config, options) {
|
|
|
632
633
|
const i18n = resolveDocsI18nConfig(getDocsI18n(config));
|
|
633
634
|
const activeLocale = localeContext.locale ?? i18n?.defaultLocale;
|
|
634
635
|
const docsApiUrl = withLangInUrl("/api/docs", activeLocale);
|
|
636
|
+
const aiClientRequest = resolveDocsCloudAIClientRequest(config, docsApiUrl);
|
|
635
637
|
const changelogConfig = resolveChangelogConfig(config.changelog);
|
|
636
638
|
const changelogBasePath = changelogConfig.enabled ? publicDocsRoute(localeContext, [changelogConfig.path]) : void 0;
|
|
637
639
|
const navTitle = config.nav?.title ?? "Docs";
|
|
@@ -758,7 +760,10 @@ function createDocsLayout(config, options) {
|
|
|
758
760
|
fallback: null,
|
|
759
761
|
children: /* @__PURE__ */ jsx(DocsAIFeatures, {
|
|
760
762
|
mode: aiMode,
|
|
761
|
-
api:
|
|
763
|
+
api: aiClientRequest.api,
|
|
764
|
+
requestMode: aiClientRequest.requestMode,
|
|
765
|
+
requestHeaders: aiClientRequest.requestHeaders,
|
|
766
|
+
requestStream: aiClientRequest.requestStream,
|
|
762
767
|
locale: activeLocale,
|
|
763
768
|
position: aiPosition,
|
|
764
769
|
floatingStyle: aiFloatingStyle,
|
package/dist/tanstack-layout.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { withLangInUrl } from "./i18n.mjs";
|
|
|
2
2
|
import { escapeJsonLdForScript } from "./json-ld.mjs";
|
|
3
3
|
import { DocsPageClient } from "./docs-page-client.mjs";
|
|
4
4
|
import { DocsAIFeatures } from "./docs-ai-features.mjs";
|
|
5
|
+
import { resolveDocsCloudAIClientRequest } from "./docs-cloud-ai-client.mjs";
|
|
5
6
|
import { DocsCommandSearch } from "./docs-command-search.mjs";
|
|
6
7
|
import { resolveOpenDocsProviders } from "./open-docs-providers.mjs";
|
|
7
8
|
import { resolveReadingTimeOptions } from "./reading-time.mjs";
|
|
@@ -220,6 +221,7 @@ function TanstackDocsLayout({ config, tree, locale, description, readingTime, la
|
|
|
220
221
|
const tocStyle = tocConfig?.style;
|
|
221
222
|
const analyticsEnabled = resolveDocsAnalyticsConfig(config.analytics).enabled;
|
|
222
223
|
const docsApiUrl = withLangInUrl("/api/docs", locale);
|
|
224
|
+
const aiClientRequest = resolveDocsCloudAIClientRequest(config, docsApiUrl);
|
|
223
225
|
const navTitle = config.nav?.title ?? "Docs";
|
|
224
226
|
const navUrl = withLangInUrl(config.nav?.url ?? `/${config.entry ?? "docs"}`, locale);
|
|
225
227
|
const themeSwitch = resolveThemeSwitch(config.themeToggle);
|
|
@@ -327,7 +329,10 @@ function TanstackDocsLayout({ config, tree, locale, description, readingTime, la
|
|
|
327
329
|
fallback: null,
|
|
328
330
|
children: /* @__PURE__ */ jsx(DocsAIFeatures, {
|
|
329
331
|
mode: aiMode,
|
|
330
|
-
api:
|
|
332
|
+
api: aiClientRequest.api,
|
|
333
|
+
requestMode: aiClientRequest.requestMode,
|
|
334
|
+
requestHeaders: aiClientRequest.requestHeaders,
|
|
335
|
+
requestStream: aiClientRequest.requestStream,
|
|
331
336
|
locale,
|
|
332
337
|
position: aiPosition,
|
|
333
338
|
floatingStyle: aiFloatingStyle,
|
|
@@ -140,7 +140,7 @@ const threadlinePageActions = {
|
|
|
140
140
|
name: "T3 Chat",
|
|
141
141
|
urlTemplate: "https://t3.chat/new?q={prompt}",
|
|
142
142
|
promptUrlTemplate: "https://t3.chat/new?q={prompt}",
|
|
143
|
-
icon: `<svg width="16" height="16" viewBox="0 0 512 512" fill="none" aria-hidden="true"><
|
|
143
|
+
icon: `<svg width="16" height="16" viewBox="0 0 512 512" fill="none" aria-hidden="true"><path fill="var(--color-fd-foreground, currentColor)" d="M115.3 407.6c-4.7 2.7-10.4-1.1-9.7-6.5l11.7-87.8C100.9 292.3 92 267.9 92 242c0-80.7 83.7-146.2 187-146.2S466 161.3 466 242 382.3 388.2 279 388.2c-28.2 0-55-4.9-78.9-13.6l-84.8 33Z"/></svg>`
|
|
144
144
|
}
|
|
145
145
|
]
|
|
146
146
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farming-labs/theme",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"tsdown": "^0.20.3",
|
|
146
146
|
"typescript": "^5.9.3",
|
|
147
147
|
"vitest": "^4.1.8",
|
|
148
|
-
"@farming-labs/docs": "0.1
|
|
148
|
+
"@farming-labs/docs": "0.2.1"
|
|
149
149
|
},
|
|
150
150
|
"peerDependencies": {
|
|
151
151
|
"@farming-labs/docs": ">=0.0.1",
|
package/styles/ai.css
CHANGED
|
@@ -777,20 +777,32 @@
|
|
|
777
777
|
|
|
778
778
|
/* ─── Streaming cursor ───────────────────────────────────────────── */
|
|
779
779
|
|
|
780
|
+
.fd-ai-streaming {
|
|
781
|
+
text-rendering: optimizeLegibility;
|
|
782
|
+
}
|
|
783
|
+
|
|
780
784
|
.fd-ai-streaming::after {
|
|
781
785
|
content: "";
|
|
782
786
|
display: inline-block;
|
|
783
787
|
width: 2px;
|
|
784
|
-
height:
|
|
788
|
+
height: 0.95em;
|
|
785
789
|
background: var(--color-fd-primary, #6366f1);
|
|
786
|
-
|
|
790
|
+
border-radius: 999px;
|
|
791
|
+
margin-left: 3px;
|
|
787
792
|
vertical-align: text-bottom;
|
|
788
|
-
animation: fd-ai-cursor-blink
|
|
793
|
+
animation: fd-ai-cursor-blink 900ms ease-in-out infinite;
|
|
794
|
+
box-shadow: 0 0 10px color-mix(in srgb, var(--color-fd-primary, #6366f1) 35%, transparent);
|
|
789
795
|
}
|
|
790
796
|
|
|
791
797
|
@keyframes fd-ai-cursor-blink {
|
|
792
798
|
0%, 100% { opacity: 1; }
|
|
793
|
-
50% { opacity: 0; }
|
|
799
|
+
50% { opacity: 0.25; }
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
@media (prefers-reduced-motion: reduce) {
|
|
803
|
+
.fd-ai-streaming::after {
|
|
804
|
+
animation: none;
|
|
805
|
+
}
|
|
794
806
|
}
|
|
795
807
|
|
|
796
808
|
/* ─── Floating trigger button ────────────────────────────────────── */
|