@farming-labs/theme 0.1.59 → 0.1.62
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 +253 -34
- package/dist/client-analytics.mjs +23 -0
- package/dist/docs-ai-features.d.mts +3 -1
- package/dist/docs-ai-features.mjs +49 -14
- package/dist/docs-api.d.mts +4 -1
- package/dist/docs-api.mjs +352 -59
- package/dist/docs-client-hooks.d.mts +4 -2
- package/dist/docs-client-hooks.mjs +52 -1
- package/dist/docs-command-search.d.mts +3 -1
- package/dist/docs-command-search.mjs +67 -15
- package/dist/docs-feedback.d.mts +3 -1
- package/dist/docs-feedback.mjs +35 -2
- package/dist/docs-layout.mjs +7 -3
- package/dist/docs-page-client.d.mts +2 -0
- package/dist/docs-page-client.mjs +34 -9
- package/dist/page-actions.d.mts +3 -1
- package/dist/page-actions.mjs +39 -7
- package/dist/tanstack-layout.mjs +7 -3
- package/package.json +2 -2
- package/styles/base.css +25 -1
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 { normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolveSearchRequestConfig } from "@farming-labs/docs";
|
|
6
|
+
import { emitDocsAnalyticsEvent, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, 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
|
|
@@ -805,15 +805,30 @@ function acceptsMarkdown(request) {
|
|
|
805
805
|
});
|
|
806
806
|
}
|
|
807
807
|
function resolveMarkdownRequest(entry, url, request) {
|
|
808
|
-
if (url.searchParams.get("format")?.trim() === "markdown") return {
|
|
808
|
+
if (url.searchParams.get("format")?.trim() === "markdown") return {
|
|
809
|
+
requestedPath: url.searchParams.get("path")?.trim() ?? "",
|
|
810
|
+
delivery: "api_format"
|
|
811
|
+
};
|
|
809
812
|
const pathname = normalizeUrlPath(url.pathname);
|
|
810
813
|
const normalizedEntry = `/${normalizePathSegment(entry)}`;
|
|
811
|
-
if (pathname === `${normalizedEntry}.md`) return {
|
|
814
|
+
if (pathname === `${normalizedEntry}.md`) return {
|
|
815
|
+
requestedPath: "",
|
|
816
|
+
delivery: "md_route"
|
|
817
|
+
};
|
|
812
818
|
const slugPrefix = `${normalizedEntry}/`;
|
|
813
|
-
if (pathname.startsWith(slugPrefix) && pathname.endsWith(".md")) return {
|
|
819
|
+
if (pathname.startsWith(slugPrefix) && pathname.endsWith(".md")) return {
|
|
820
|
+
requestedPath: pathname.slice(slugPrefix.length, -3),
|
|
821
|
+
delivery: "md_route"
|
|
822
|
+
};
|
|
814
823
|
if (acceptsMarkdown(request)) {
|
|
815
|
-
if (pathname === normalizedEntry) return {
|
|
816
|
-
|
|
824
|
+
if (pathname === normalizedEntry) return {
|
|
825
|
+
requestedPath: "",
|
|
826
|
+
delivery: "accept_header"
|
|
827
|
+
};
|
|
828
|
+
if (pathname.startsWith(slugPrefix)) return {
|
|
829
|
+
requestedPath: pathname.slice(slugPrefix.length),
|
|
830
|
+
delivery: "accept_header"
|
|
831
|
+
};
|
|
817
832
|
}
|
|
818
833
|
return null;
|
|
819
834
|
}
|
|
@@ -900,20 +915,60 @@ function resolveModelAndProvider(aiConfig, requestedModelId) {
|
|
|
900
915
|
apiKey
|
|
901
916
|
};
|
|
902
917
|
}
|
|
903
|
-
async function handleAskAI(request, indexes, aiConfig) {
|
|
918
|
+
async function handleAskAI(request, indexes, aiConfig, analytics, analyticsContext = {}) {
|
|
919
|
+
const url = new URL(request.url);
|
|
920
|
+
const requestStartedAt = Date.now();
|
|
904
921
|
let body;
|
|
905
922
|
try {
|
|
906
923
|
body = await request.json();
|
|
907
924
|
} catch {
|
|
925
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
926
|
+
type: "api_ai_error",
|
|
927
|
+
source: "server",
|
|
928
|
+
url: request.url,
|
|
929
|
+
path: url.pathname,
|
|
930
|
+
locale: analyticsContext.locale,
|
|
931
|
+
properties: {
|
|
932
|
+
reason: "invalid_json",
|
|
933
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
934
|
+
}
|
|
935
|
+
});
|
|
908
936
|
return Response.json({ error: "Invalid JSON body. Expected { messages: [...] }" }, { status: 400 });
|
|
909
937
|
}
|
|
910
938
|
const messages = body.messages;
|
|
911
|
-
if (!Array.isArray(messages) || messages.length === 0)
|
|
939
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
940
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
941
|
+
type: "api_ai_error",
|
|
942
|
+
source: "server",
|
|
943
|
+
url: request.url,
|
|
944
|
+
path: url.pathname,
|
|
945
|
+
locale: analyticsContext.locale,
|
|
946
|
+
properties: {
|
|
947
|
+
reason: "missing_messages",
|
|
948
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
return Response.json({ error: "messages array is required and must not be empty." }, { status: 400 });
|
|
952
|
+
}
|
|
912
953
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
913
|
-
if (!lastUserMessage)
|
|
954
|
+
if (!lastUserMessage) {
|
|
955
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
956
|
+
type: "api_ai_error",
|
|
957
|
+
source: "server",
|
|
958
|
+
url: request.url,
|
|
959
|
+
path: url.pathname,
|
|
960
|
+
locale: analyticsContext.locale,
|
|
961
|
+
properties: {
|
|
962
|
+
reason: "missing_user_message",
|
|
963
|
+
messageCount: messages.length,
|
|
964
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
return Response.json({ error: "At least one user message is required." }, { status: 400 });
|
|
968
|
+
}
|
|
914
969
|
const maxResults = aiConfig.maxResults ?? 5;
|
|
915
970
|
const query = lastUserMessage.content;
|
|
916
|
-
const
|
|
971
|
+
const scored = indexes.map((doc) => {
|
|
917
972
|
const q = query.toLowerCase();
|
|
918
973
|
const titleMatch = doc.title.toLowerCase().includes(q) ? 10 : 0;
|
|
919
974
|
const contentMatch = q.split(/\s+/).reduce((score, word) => {
|
|
@@ -923,14 +978,47 @@ async function handleAskAI(request, indexes, aiConfig) {
|
|
|
923
978
|
...doc,
|
|
924
979
|
score: titleMatch + contentMatch
|
|
925
980
|
};
|
|
926
|
-
}).filter((d) => d.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults)
|
|
981
|
+
}).filter((d) => d.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults);
|
|
982
|
+
const context = scored.map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`).join("\n\n---\n\n");
|
|
927
983
|
const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
928
984
|
const llmMessages = [{
|
|
929
985
|
role: "system",
|
|
930
986
|
content: context ? `${systemPrompt}\n\n---\n\nDocumentation context:\n\n${context}` : systemPrompt
|
|
931
987
|
}, ...messages.filter((m) => m.role !== "system")];
|
|
932
988
|
const resolved = resolveModelAndProvider(aiConfig, typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0);
|
|
933
|
-
if (!resolved.apiKey)
|
|
989
|
+
if (!resolved.apiKey) {
|
|
990
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
991
|
+
type: "api_ai_error",
|
|
992
|
+
source: "server",
|
|
993
|
+
url: request.url,
|
|
994
|
+
path: url.pathname,
|
|
995
|
+
locale: analyticsContext.locale,
|
|
996
|
+
input: { question: query },
|
|
997
|
+
properties: {
|
|
998
|
+
reason: "missing_api_key",
|
|
999
|
+
messageCount: messages.length,
|
|
1000
|
+
questionLength: query.length,
|
|
1001
|
+
retrievedCount: scored.length,
|
|
1002
|
+
model: resolved.model,
|
|
1003
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
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 });
|
|
1007
|
+
}
|
|
1008
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1009
|
+
type: "api_ai_request",
|
|
1010
|
+
source: "server",
|
|
1011
|
+
url: request.url,
|
|
1012
|
+
path: url.pathname,
|
|
1013
|
+
locale: analyticsContext.locale,
|
|
1014
|
+
input: { question: query },
|
|
1015
|
+
properties: {
|
|
1016
|
+
messageCount: messages.length,
|
|
1017
|
+
questionLength: query.length,
|
|
1018
|
+
retrievedCount: scored.length,
|
|
1019
|
+
model: resolved.model
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
934
1022
|
const llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
|
|
935
1023
|
method: "POST",
|
|
936
1024
|
headers: {
|
|
@@ -945,8 +1033,40 @@ async function handleAskAI(request, indexes, aiConfig) {
|
|
|
945
1033
|
});
|
|
946
1034
|
if (!llmResponse.ok) {
|
|
947
1035
|
const errText = await llmResponse.text().catch(() => "Unknown error");
|
|
1036
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1037
|
+
type: "api_ai_error",
|
|
1038
|
+
source: "server",
|
|
1039
|
+
url: request.url,
|
|
1040
|
+
path: url.pathname,
|
|
1041
|
+
locale: analyticsContext.locale,
|
|
1042
|
+
input: { question: query },
|
|
1043
|
+
properties: {
|
|
1044
|
+
reason: "llm_error",
|
|
1045
|
+
status: llmResponse.status,
|
|
1046
|
+
messageCount: messages.length,
|
|
1047
|
+
questionLength: query.length,
|
|
1048
|
+
retrievedCount: scored.length,
|
|
1049
|
+
model: resolved.model,
|
|
1050
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
948
1053
|
return Response.json({ error: `LLM API error (${llmResponse.status}): ${errText}` }, { status: 502 });
|
|
949
1054
|
}
|
|
1055
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1056
|
+
type: "api_ai_response",
|
|
1057
|
+
source: "server",
|
|
1058
|
+
url: request.url,
|
|
1059
|
+
path: url.pathname,
|
|
1060
|
+
locale: analyticsContext.locale,
|
|
1061
|
+
input: { question: query },
|
|
1062
|
+
properties: {
|
|
1063
|
+
messageCount: messages.length,
|
|
1064
|
+
questionLength: query.length,
|
|
1065
|
+
retrievedCount: scored.length,
|
|
1066
|
+
model: resolved.model,
|
|
1067
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
950
1070
|
return new Response(llmResponse.body, { headers: {
|
|
951
1071
|
"Content-Type": "text/event-stream",
|
|
952
1072
|
"Cache-Control": "no-cache",
|
|
@@ -1026,6 +1146,7 @@ function generateLlmsTxt(indexes, options) {
|
|
|
1026
1146
|
function createDocsAPI(options) {
|
|
1027
1147
|
const root = options?.rootDir ?? process.cwd();
|
|
1028
1148
|
const entry = options?.entry ?? readEntry(root);
|
|
1149
|
+
const analytics = options?.analytics;
|
|
1029
1150
|
const appDir = getNextAppDir(root);
|
|
1030
1151
|
const contentDir = options?.contentDir ?? path.join(appDir, entry);
|
|
1031
1152
|
const changelogConfig = resolveChangelogConfig(options?.changelog);
|
|
@@ -1154,51 +1275,131 @@ function createDocsAPI(options) {
|
|
|
1154
1275
|
async GET(request) {
|
|
1155
1276
|
const ctx = resolveContextFromRequest(request);
|
|
1156
1277
|
const url = new URL(request.url);
|
|
1157
|
-
if (resolveAgentSpecRequest(url))
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1278
|
+
if (resolveAgentSpecRequest(url)) {
|
|
1279
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1280
|
+
type: "agent_spec_request",
|
|
1281
|
+
source: "server",
|
|
1282
|
+
url: request.url,
|
|
1283
|
+
path: url.pathname,
|
|
1284
|
+
locale: ctx.locale,
|
|
1285
|
+
properties: { method: "GET" }
|
|
1286
|
+
});
|
|
1287
|
+
return Response.json(buildAgentSpec({
|
|
1288
|
+
origin: url.origin,
|
|
1289
|
+
entry,
|
|
1290
|
+
i18n,
|
|
1291
|
+
search: searchConfig,
|
|
1292
|
+
mcp: mcpConfig,
|
|
1293
|
+
feedback: agentFeedbackConfig,
|
|
1294
|
+
llms: llmsConfig
|
|
1295
|
+
}), { headers: {
|
|
1296
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
1297
|
+
"X-Robots-Tag": "noindex"
|
|
1298
|
+
} });
|
|
1299
|
+
}
|
|
1169
1300
|
const agentFeedbackRequest = resolveAgentFeedbackRequest(url, agentFeedbackConfig);
|
|
1170
1301
|
if (agentFeedbackRequest) {
|
|
1171
1302
|
if (agentFeedbackRequest.kind === "submit") return Response.json({ error: "Method Not Allowed" }, {
|
|
1172
1303
|
status: 405,
|
|
1173
1304
|
headers: { Allow: "POST" }
|
|
1174
1305
|
});
|
|
1306
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1307
|
+
type: "agent_feedback_schema",
|
|
1308
|
+
source: "server",
|
|
1309
|
+
url: request.url,
|
|
1310
|
+
path: url.pathname,
|
|
1311
|
+
locale: ctx.locale,
|
|
1312
|
+
properties: { method: "GET" }
|
|
1313
|
+
});
|
|
1175
1314
|
return new Response(JSON.stringify(agentFeedbackConfig.schema, null, 2), { headers: {
|
|
1176
1315
|
"Content-Type": "application/schema+json; charset=utf-8",
|
|
1177
1316
|
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
1178
1317
|
"X-Robots-Tag": "noindex"
|
|
1179
1318
|
} });
|
|
1180
1319
|
}
|
|
1181
|
-
if (resolveSkillRequest(url))
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1320
|
+
if (resolveSkillRequest(url)) {
|
|
1321
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1322
|
+
type: "skill_request",
|
|
1323
|
+
source: "server",
|
|
1324
|
+
url: request.url,
|
|
1325
|
+
path: url.pathname,
|
|
1326
|
+
locale: ctx.locale,
|
|
1327
|
+
properties: { method: "GET" }
|
|
1328
|
+
});
|
|
1329
|
+
return new Response(readRootSkillDocument(root) ?? renderSkillDocument({
|
|
1330
|
+
origin: url.origin,
|
|
1331
|
+
entry,
|
|
1332
|
+
i18n,
|
|
1333
|
+
search: searchConfig,
|
|
1334
|
+
mcp: mcpConfig,
|
|
1335
|
+
feedback: agentFeedbackConfig,
|
|
1336
|
+
llms: llmsConfig
|
|
1337
|
+
}), { headers: {
|
|
1338
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
1339
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600",
|
|
1340
|
+
"X-Robots-Tag": "noindex"
|
|
1341
|
+
} });
|
|
1342
|
+
}
|
|
1194
1343
|
const markdownRequest = resolveMarkdownRequest(entry, url, request);
|
|
1195
1344
|
if (markdownRequest) {
|
|
1196
1345
|
const document = await getMarkdownDocument(ctx, markdownRequest.requestedPath);
|
|
1197
|
-
if (!document)
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1346
|
+
if (!document) {
|
|
1347
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1348
|
+
type: "agent_read",
|
|
1349
|
+
source: "server",
|
|
1350
|
+
url: request.url,
|
|
1351
|
+
path: url.pathname,
|
|
1352
|
+
locale: ctx.locale,
|
|
1353
|
+
properties: {
|
|
1354
|
+
requestedPath: markdownRequest.requestedPath,
|
|
1355
|
+
delivery: markdownRequest.delivery,
|
|
1356
|
+
found: false
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1360
|
+
type: "markdown_request",
|
|
1361
|
+
source: "server",
|
|
1362
|
+
url: request.url,
|
|
1363
|
+
path: url.pathname,
|
|
1364
|
+
locale: ctx.locale,
|
|
1365
|
+
properties: {
|
|
1366
|
+
requestedPath: markdownRequest.requestedPath,
|
|
1367
|
+
delivery: markdownRequest.delivery,
|
|
1368
|
+
found: false
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
return new Response("Not Found", {
|
|
1372
|
+
status: 404,
|
|
1373
|
+
headers: {
|
|
1374
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1375
|
+
"X-Robots-Tag": "noindex"
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1380
|
+
type: "agent_read",
|
|
1381
|
+
source: "server",
|
|
1382
|
+
url: request.url,
|
|
1383
|
+
path: url.pathname,
|
|
1384
|
+
locale: ctx.locale,
|
|
1385
|
+
properties: {
|
|
1386
|
+
requestedPath: markdownRequest.requestedPath,
|
|
1387
|
+
delivery: markdownRequest.delivery,
|
|
1388
|
+
found: true,
|
|
1389
|
+
contentLength: document.length
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1393
|
+
type: "markdown_request",
|
|
1394
|
+
source: "server",
|
|
1395
|
+
url: request.url,
|
|
1396
|
+
path: url.pathname,
|
|
1397
|
+
locale: ctx.locale,
|
|
1398
|
+
properties: {
|
|
1399
|
+
requestedPath: markdownRequest.requestedPath,
|
|
1400
|
+
delivery: markdownRequest.delivery,
|
|
1401
|
+
found: true,
|
|
1402
|
+
contentLength: document.length
|
|
1202
1403
|
}
|
|
1203
1404
|
});
|
|
1204
1405
|
return new Response(document, { headers: {
|
|
@@ -1208,16 +1409,37 @@ function createDocsAPI(options) {
|
|
|
1208
1409
|
} });
|
|
1209
1410
|
}
|
|
1210
1411
|
const llmsFormat = resolveLlmsTxtFormat(url);
|
|
1211
|
-
if (llmsFormat === "llms")
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1412
|
+
if (llmsFormat === "llms") {
|
|
1413
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1414
|
+
type: "llms_request",
|
|
1415
|
+
source: "server",
|
|
1416
|
+
url: request.url,
|
|
1417
|
+
path: url.pathname,
|
|
1418
|
+
locale: ctx.locale,
|
|
1419
|
+
properties: { format: "llms" }
|
|
1420
|
+
});
|
|
1421
|
+
return new Response(getLlmsContent(ctx).llmsTxt, { headers: {
|
|
1422
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1423
|
+
"Cache-Control": "public, max-age=3600"
|
|
1424
|
+
} });
|
|
1425
|
+
}
|
|
1426
|
+
if (llmsFormat === "llms-full") {
|
|
1427
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1428
|
+
type: "llms_request",
|
|
1429
|
+
source: "server",
|
|
1430
|
+
url: request.url,
|
|
1431
|
+
path: url.pathname,
|
|
1432
|
+
locale: ctx.locale,
|
|
1433
|
+
properties: { format: "llms-full" }
|
|
1434
|
+
});
|
|
1435
|
+
return new Response(getLlmsContent(ctx).llmsFullTxt, { headers: {
|
|
1436
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1437
|
+
"Cache-Control": "public, max-age=3600"
|
|
1438
|
+
} });
|
|
1439
|
+
}
|
|
1219
1440
|
const query = url.searchParams.get("query")?.trim();
|
|
1220
1441
|
if (!query) return new Response(JSON.stringify([]), { headers: { "Content-Type": "application/json" } });
|
|
1442
|
+
const searchStartedAt = Date.now();
|
|
1221
1443
|
const results = await performDocsSearch({
|
|
1222
1444
|
pages: getIndexes(ctx),
|
|
1223
1445
|
query,
|
|
@@ -1226,31 +1448,101 @@ function createDocsAPI(options) {
|
|
|
1226
1448
|
pathname: url.searchParams.get("pathname") ?? void 0,
|
|
1227
1449
|
siteTitle: llmsConfig.siteTitle ?? "Documentation"
|
|
1228
1450
|
});
|
|
1451
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1452
|
+
type: "api_search",
|
|
1453
|
+
source: "server",
|
|
1454
|
+
url: request.url,
|
|
1455
|
+
path: url.pathname,
|
|
1456
|
+
locale: ctx.locale,
|
|
1457
|
+
input: { query },
|
|
1458
|
+
properties: {
|
|
1459
|
+
queryLength: query.length,
|
|
1460
|
+
resultCount: results.length,
|
|
1461
|
+
pathname: url.searchParams.get("pathname") ?? void 0,
|
|
1462
|
+
durationMs: Math.max(0, Date.now() - searchStartedAt)
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1229
1465
|
return new Response(JSON.stringify(results), { headers: { "Content-Type": "application/json" } });
|
|
1230
1466
|
},
|
|
1231
1467
|
async POST(request) {
|
|
1232
|
-
const
|
|
1468
|
+
const url = new URL(request.url);
|
|
1469
|
+
const agentFeedbackRequest = resolveAgentFeedbackRequest(url, agentFeedbackConfig);
|
|
1233
1470
|
if (agentFeedbackRequest) {
|
|
1234
1471
|
if (agentFeedbackRequest.kind === "schema") return Response.json({ error: "Method Not Allowed" }, {
|
|
1235
1472
|
status: 405,
|
|
1236
1473
|
headers: { Allow: "GET" }
|
|
1237
1474
|
});
|
|
1238
1475
|
const parsed = await parseAgentFeedbackData(request);
|
|
1239
|
-
if (!parsed.ok)
|
|
1476
|
+
if (!parsed.ok) {
|
|
1477
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1478
|
+
type: "agent_feedback_error",
|
|
1479
|
+
source: "server",
|
|
1480
|
+
url: request.url,
|
|
1481
|
+
path: url.pathname,
|
|
1482
|
+
properties: { reason: "invalid_body" }
|
|
1483
|
+
});
|
|
1484
|
+
return parsed.response;
|
|
1485
|
+
}
|
|
1240
1486
|
const payloadError = validateAgentFeedbackPayload(parsed.data.payload, agentFeedbackConfig.payloadSchema);
|
|
1241
|
-
if (payloadError)
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1487
|
+
if (payloadError) {
|
|
1488
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1489
|
+
type: "agent_feedback_error",
|
|
1490
|
+
source: "server",
|
|
1491
|
+
url: request.url,
|
|
1492
|
+
path: url.pathname,
|
|
1493
|
+
properties: {
|
|
1494
|
+
reason: "invalid_payload",
|
|
1495
|
+
error: payloadError
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
return Response.json({ error: payloadError }, { status: 400 });
|
|
1499
|
+
}
|
|
1500
|
+
if (!agentFeedbackConfig.onFeedback) {
|
|
1501
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1502
|
+
type: "agent_feedback_submit",
|
|
1503
|
+
source: "server",
|
|
1504
|
+
url: request.url,
|
|
1505
|
+
path: url.pathname,
|
|
1506
|
+
properties: {
|
|
1507
|
+
handled: false,
|
|
1508
|
+
payloadKeys: Object.keys(parsed.data.payload),
|
|
1509
|
+
hasContext: Boolean(parsed.data.context)
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
return Response.json({
|
|
1513
|
+
ok: true,
|
|
1514
|
+
handled: false
|
|
1515
|
+
}, { status: 202 });
|
|
1516
|
+
}
|
|
1246
1517
|
await agentFeedbackConfig.onFeedback(parsed.data);
|
|
1518
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1519
|
+
type: "agent_feedback_submit",
|
|
1520
|
+
source: "server",
|
|
1521
|
+
url: request.url,
|
|
1522
|
+
path: url.pathname,
|
|
1523
|
+
properties: {
|
|
1524
|
+
handled: true,
|
|
1525
|
+
payloadKeys: Object.keys(parsed.data.payload),
|
|
1526
|
+
hasContext: Boolean(parsed.data.context)
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1247
1529
|
return Response.json({
|
|
1248
1530
|
ok: true,
|
|
1249
1531
|
handled: true
|
|
1250
1532
|
}, { status: 201 });
|
|
1251
1533
|
}
|
|
1252
|
-
if (!aiConfig.enabled)
|
|
1253
|
-
|
|
1534
|
+
if (!aiConfig.enabled) {
|
|
1535
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1536
|
+
type: "api_ai_error",
|
|
1537
|
+
source: "server",
|
|
1538
|
+
url: request.url,
|
|
1539
|
+
path: url.pathname,
|
|
1540
|
+
properties: { reason: "disabled" }
|
|
1541
|
+
});
|
|
1542
|
+
return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
|
|
1543
|
+
}
|
|
1544
|
+
const ctx = resolveContextFromRequest(request);
|
|
1545
|
+
return handleAskAI(request, getIndexes(ctx), aiConfig, analytics, { locale: ctx.locale });
|
|
1254
1546
|
}
|
|
1255
1547
|
};
|
|
1256
1548
|
}
|
|
@@ -1279,6 +1571,7 @@ function createDocsMCPAPI(options = {}) {
|
|
|
1279
1571
|
}),
|
|
1280
1572
|
mcp: options.mcp ?? readMcpConfig(rootDir),
|
|
1281
1573
|
search: options.search,
|
|
1574
|
+
analytics: options.analytics,
|
|
1282
1575
|
defaultName: navTitle
|
|
1283
1576
|
});
|
|
1284
1577
|
return {
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { CodeBlockCopyData, DocsFeedbackData } from "@farming-labs/docs";
|
|
1
|
+
import { CodeBlockCopyData, DocsAnalyticsConfig, 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
6
|
declare function DocsClientHooks({
|
|
7
7
|
onCopyClick,
|
|
8
|
-
onFeedback
|
|
8
|
+
onFeedback,
|
|
9
|
+
analytics
|
|
9
10
|
}: {
|
|
10
11
|
onCopyClick?: CopyHandler;
|
|
11
12
|
onFeedback?: FeedbackHandler;
|
|
13
|
+
analytics?: boolean | DocsAnalyticsConfig;
|
|
12
14
|
}): null;
|
|
13
15
|
//#endregion
|
|
14
16
|
export { DocsClientHooks };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
|
|
3
4
|
import { useEffect } from "react";
|
|
5
|
+
import { emitDocsAnalyticsEvent } from "@farming-labs/docs";
|
|
4
6
|
|
|
5
7
|
//#region src/docs-client-hooks.tsx
|
|
6
8
|
function useWindowHook(key, handler) {
|
|
@@ -16,9 +18,58 @@ function useWindowHook(key, handler) {
|
|
|
16
18
|
};
|
|
17
19
|
}, [handler, key]);
|
|
18
20
|
}
|
|
19
|
-
function
|
|
21
|
+
function useAnalyticsHook(analytics) {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (typeof window === "undefined") return;
|
|
24
|
+
if (!analytics) return;
|
|
25
|
+
const target = window;
|
|
26
|
+
const handler = (event) => {
|
|
27
|
+
emitDocsAnalyticsEvent(analytics, event);
|
|
28
|
+
};
|
|
29
|
+
const previous = target.__fdAnalytics__;
|
|
30
|
+
target.__fdAnalytics__ = handler;
|
|
31
|
+
const queued = target.__fdAnalyticsQueue__ ?? [];
|
|
32
|
+
delete target.__fdAnalyticsQueue__;
|
|
33
|
+
for (const event of queued) handler(event);
|
|
34
|
+
return () => {
|
|
35
|
+
if (target.__fdAnalytics__ === handler) if (typeof previous === "function") target.__fdAnalytics__ = previous;
|
|
36
|
+
else delete target.__fdAnalytics__;
|
|
37
|
+
};
|
|
38
|
+
}, [analytics]);
|
|
39
|
+
}
|
|
40
|
+
function useCodeCopyAnalytics(analytics) {
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (typeof window === "undefined") return;
|
|
43
|
+
if (!analytics) return;
|
|
44
|
+
const handleClick = (event) => {
|
|
45
|
+
const target = event.target;
|
|
46
|
+
if (!target.closest?.("button")) return;
|
|
47
|
+
const figure = target.closest("figure");
|
|
48
|
+
if (!figure) return;
|
|
49
|
+
const code = figure.querySelector("pre code");
|
|
50
|
+
if (!code) return;
|
|
51
|
+
const content = code.textContent ?? "";
|
|
52
|
+
const language = code.getAttribute("data-language") ?? figure.getAttribute("data-language") ?? void 0;
|
|
53
|
+
const title = figure.querySelector("[data-title]")?.textContent?.trim() ?? figure.querySelector(".fd-codeblock-title-text")?.textContent?.trim() ?? void 0;
|
|
54
|
+
emitClientAnalyticsEvent({
|
|
55
|
+
type: "code_block_copy",
|
|
56
|
+
input: { content },
|
|
57
|
+
properties: {
|
|
58
|
+
title,
|
|
59
|
+
language,
|
|
60
|
+
contentLength: content.length
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
document.addEventListener("click", handleClick, true);
|
|
65
|
+
return () => document.removeEventListener("click", handleClick, true);
|
|
66
|
+
}, [analytics]);
|
|
67
|
+
}
|
|
68
|
+
function DocsClientHooks({ onCopyClick, onFeedback, analytics }) {
|
|
20
69
|
useWindowHook("__fdOnCopyClick__", onCopyClick);
|
|
21
70
|
useWindowHook("__fdOnFeedback__", onFeedback);
|
|
71
|
+
useAnalyticsHook(analytics);
|
|
72
|
+
useCodeCopyAnalytics(analytics);
|
|
22
73
|
return null;
|
|
23
74
|
}
|
|
24
75
|
|
|
@@ -9,10 +9,12 @@ import * as React$1 from "react";
|
|
|
9
9
|
*/
|
|
10
10
|
declare function DocsCommandSearch({
|
|
11
11
|
api,
|
|
12
|
-
locale
|
|
12
|
+
locale,
|
|
13
|
+
analytics
|
|
13
14
|
}: {
|
|
14
15
|
api?: string;
|
|
15
16
|
locale?: string;
|
|
17
|
+
analytics?: boolean;
|
|
16
18
|
}): React$1.ReactPortal | null;
|
|
17
19
|
//#endregion
|
|
18
20
|
export { DocsCommandSearch };
|