@farming-labs/theme 0.1.19 → 0.1.21

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.
@@ -1,4 +1,4 @@
1
- import { ChangelogConfig, DocsI18nConfig, DocsMcpConfig, DocsSearchConfig, OrderingItem } from "@farming-labs/docs";
1
+ import { ChangelogConfig, DocsI18nConfig, DocsMcpConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
2
2
 
3
3
  //#region src/docs-api.d.ts
4
4
  interface AIProviderConfig {
@@ -41,6 +41,8 @@ interface DocsAPIOptions {
41
41
  i18n?: DocsI18nConfig;
42
42
  /** Search configuration */
43
43
  search?: boolean | DocsSearchConfig;
44
+ /** Feedback configuration */
45
+ feedback?: boolean | FeedbackConfig;
44
46
  }
45
47
  interface DocsMCPAPIOptions {
46
48
  rootDir?: string;
package/dist/docs-api.mjs CHANGED
@@ -32,6 +32,201 @@ const FILE_EXTS = [
32
32
  "jsx",
33
33
  "js"
34
34
  ];
35
+ const DEFAULT_AGENT_FEEDBACK_ROUTE = "/api/docs/agent/feedback";
36
+ const DEFAULT_AGENT_FEEDBACK_PAYLOAD_SCHEMA = {
37
+ type: "object",
38
+ additionalProperties: false,
39
+ properties: {
40
+ task: {
41
+ type: "string",
42
+ description: "Short description of what the agent was trying to do."
43
+ },
44
+ understanding: {
45
+ type: "string",
46
+ description: "How well the docs supported the task, e.g. \"partial\" or \"clear\"."
47
+ },
48
+ outcome: {
49
+ type: "string",
50
+ description: "What happened after reading the docs, e.g. \"implemented\" or \"blocked\"."
51
+ },
52
+ confidence: {
53
+ type: "number",
54
+ minimum: 0,
55
+ maximum: 1,
56
+ description: "Confidence score from 0 to 1."
57
+ },
58
+ neededCodeReading: {
59
+ type: "boolean",
60
+ description: "Whether the agent still needed to inspect repository code."
61
+ },
62
+ missingContext: {
63
+ type: "array",
64
+ items: { type: "string" },
65
+ description: "Important details the docs did not provide clearly enough."
66
+ },
67
+ docIssues: {
68
+ type: "array",
69
+ items: { type: "string" },
70
+ description: "Specific documentation problems encountered during the task."
71
+ },
72
+ suggestedImprovement: {
73
+ type: "string",
74
+ description: "Concrete suggestion for improving the docs page or examples."
75
+ }
76
+ },
77
+ required: ["task", "outcome"]
78
+ };
79
+ function normalizeAgentFeedbackRoute(route, fallback = DEFAULT_AGENT_FEEDBACK_ROUTE) {
80
+ if (!route || route.trim().length === 0) return fallback;
81
+ const normalized = `/${route}`.replace(/\/+/g, "/");
82
+ return normalized !== "/" ? normalized.replace(/\/+$/, "") : fallback;
83
+ }
84
+ function buildAgentFeedbackSchema(payloadSchema) {
85
+ return {
86
+ type: "object",
87
+ additionalProperties: false,
88
+ properties: {
89
+ context: {
90
+ type: "object",
91
+ additionalProperties: false,
92
+ properties: {
93
+ page: { type: "string" },
94
+ url: { type: "string" },
95
+ slug: { type: "string" },
96
+ locale: { type: "string" },
97
+ source: { type: "string" }
98
+ }
99
+ },
100
+ payload: payloadSchema
101
+ },
102
+ required: ["payload"]
103
+ };
104
+ }
105
+ function resolveAgentFeedbackConfig(feedback) {
106
+ const route = normalizeAgentFeedbackRoute();
107
+ const disabled = {
108
+ enabled: false,
109
+ route,
110
+ schemaRoute: `${route}/schema`,
111
+ payloadSchema: DEFAULT_AGENT_FEEDBACK_PAYLOAD_SCHEMA,
112
+ schema: buildAgentFeedbackSchema(DEFAULT_AGENT_FEEDBACK_PAYLOAD_SCHEMA)
113
+ };
114
+ if (!feedback || typeof feedback !== "object") return disabled;
115
+ const agent = feedback.agent;
116
+ if (!agent) return disabled;
117
+ if (agent === true) return {
118
+ enabled: true,
119
+ route,
120
+ schemaRoute: `${route}/schema`,
121
+ payloadSchema: DEFAULT_AGENT_FEEDBACK_PAYLOAD_SCHEMA,
122
+ schema: buildAgentFeedbackSchema(DEFAULT_AGENT_FEEDBACK_PAYLOAD_SCHEMA)
123
+ };
124
+ const resolvedRoute = normalizeAgentFeedbackRoute(agent.route, route);
125
+ const resolvedSchemaRoute = normalizeAgentFeedbackRoute(agent.schemaRoute, `${resolvedRoute}/schema`);
126
+ const payloadSchema = agent.schema ?? DEFAULT_AGENT_FEEDBACK_PAYLOAD_SCHEMA;
127
+ return {
128
+ enabled: agent.enabled !== false,
129
+ route: resolvedRoute,
130
+ schemaRoute: resolvedSchemaRoute,
131
+ payloadSchema,
132
+ schema: buildAgentFeedbackSchema(payloadSchema),
133
+ onFeedback: agent.onFeedback
134
+ };
135
+ }
136
+ function resolveAgentFeedbackRequest(url, feedback) {
137
+ if (!feedback.enabled) return null;
138
+ const feedbackMode = url.searchParams.get("feedback")?.trim();
139
+ const schemaMode = url.searchParams.get("schema")?.trim();
140
+ if (feedbackMode === "agent") return { kind: schemaMode === "1" || schemaMode === "true" ? "schema" : "submit" };
141
+ const pathname = normalizeUrlPath(url.pathname);
142
+ if (pathname === feedback.schemaRoute) return { kind: "schema" };
143
+ if (pathname === feedback.route) return { kind: "submit" };
144
+ return null;
145
+ }
146
+ function isPlainObject(value) {
147
+ return typeof value === "object" && value !== null && !Array.isArray(value);
148
+ }
149
+ function normalizeAgentFeedbackContext(value) {
150
+ if (!isPlainObject(value)) return void 0;
151
+ const context = {};
152
+ if (typeof value.page === "string") context.page = value.page;
153
+ if (typeof value.url === "string") context.url = value.url;
154
+ if (typeof value.slug === "string") context.slug = value.slug;
155
+ if (typeof value.locale === "string") context.locale = value.locale;
156
+ if (typeof value.source === "string") context.source = value.source;
157
+ return Object.keys(context).length > 0 ? context : void 0;
158
+ }
159
+ async function parseAgentFeedbackData(request) {
160
+ let body;
161
+ try {
162
+ body = await request.json();
163
+ } catch {
164
+ return {
165
+ ok: false,
166
+ response: Response.json({ error: "Agent feedback body must be valid JSON" }, { status: 400 })
167
+ };
168
+ }
169
+ if (!isPlainObject(body)) return {
170
+ ok: false,
171
+ response: Response.json({ error: "Agent feedback body must be an object" }, { status: 400 })
172
+ };
173
+ if (!isPlainObject(body.payload)) return {
174
+ ok: false,
175
+ response: Response.json({ error: "Agent feedback body must include a payload object" }, { status: 400 })
176
+ };
177
+ return {
178
+ ok: true,
179
+ data: {
180
+ context: normalizeAgentFeedbackContext(body.context),
181
+ payload: body.payload
182
+ }
183
+ };
184
+ }
185
+ function validateAgentFeedbackPayload(value, schema, valuePath = "payload") {
186
+ const schemaType = typeof schema.type === "string" ? schema.type : void 0;
187
+ if (Array.isArray(schema.enum) && !schema.enum.some((entry) => Object.is(entry, value))) return `${valuePath} must be one of the configured enum values`;
188
+ if (schemaType === "object" || !schemaType && (schema.properties || schema.required)) {
189
+ if (!isPlainObject(value)) return `${valuePath} must be an object`;
190
+ const properties = isPlainObject(schema.properties) ? schema.properties : {};
191
+ const required = Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === "string") : [];
192
+ for (const key of required) if (!(key in value)) return `${valuePath}.${key} is required`;
193
+ if (schema.additionalProperties === false) {
194
+ for (const key of Object.keys(value)) if (!(key in properties)) return `${valuePath}.${key} is not allowed`;
195
+ }
196
+ for (const [key, propertySchema] of Object.entries(properties)) {
197
+ if (!(key in value)) continue;
198
+ if (!isPlainObject(propertySchema)) continue;
199
+ const error = validateAgentFeedbackPayload(value[key], propertySchema, `${valuePath}.${key}`);
200
+ if (error) return error;
201
+ }
202
+ return;
203
+ }
204
+ if (schemaType === "array") {
205
+ if (!Array.isArray(value)) return `${valuePath} must be an array`;
206
+ if (!isPlainObject(schema.items)) return void 0;
207
+ for (const [index, item] of value.entries()) {
208
+ const error = validateAgentFeedbackPayload(item, schema.items, `${valuePath}[${index}]`);
209
+ if (error) return error;
210
+ }
211
+ return;
212
+ }
213
+ if (schemaType === "string") {
214
+ if (typeof value !== "string") return `${valuePath} must be a string`;
215
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) return `${valuePath} must be at least ${schema.minLength} characters`;
216
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) return `${valuePath} must be at most ${schema.maxLength} characters`;
217
+ return;
218
+ }
219
+ if (schemaType === "number") {
220
+ if (typeof value !== "number" || !Number.isFinite(value)) return `${valuePath} must be a number`;
221
+ if (typeof schema.minimum === "number" && value < schema.minimum) return `${valuePath} must be >= ${schema.minimum}`;
222
+ if (typeof schema.maximum === "number" && value > schema.maximum) return `${valuePath} must be <= ${schema.maximum}`;
223
+ return;
224
+ }
225
+ if (schemaType === "boolean") {
226
+ if (typeof value !== "boolean") return `${valuePath} must be a boolean`;
227
+ return;
228
+ }
229
+ }
35
230
  function readEntry(root) {
36
231
  for (const ext of FILE_EXTS) {
37
232
  const configPath = path.join(root, `docs.config.${ext}`);
@@ -444,6 +639,15 @@ function findDocsMcpPage(entry, pages, requestedPath) {
444
639
  for (const page of pages) if (normalizePathSegment(page.slug) === normalizedSlug) return page;
445
640
  return null;
446
641
  }
642
+ function resolveMarkdownRequest(entry, url) {
643
+ if (url.searchParams.get("format")?.trim() === "markdown") return { requestedPath: url.searchParams.get("path")?.trim() ?? "" };
644
+ const pathname = normalizeUrlPath(url.pathname);
645
+ const normalizedEntry = `/${normalizePathSegment(entry)}`;
646
+ if (pathname === `${normalizedEntry}.md`) return { requestedPath: "" };
647
+ const slugPrefix = `${normalizedEntry}/`;
648
+ if (pathname.startsWith(slugPrefix) && pathname.endsWith(".md")) return { requestedPath: pathname.slice(slugPrefix.length, -3) };
649
+ return null;
650
+ }
447
651
  function renderMarkdownDocument(page) {
448
652
  if ("agentRawContent" in page && page.agentRawContent !== void 0) return page.agentRawContent;
449
653
  const lines = [`# ${page.title}`, `URL: ${page.url}`];
@@ -599,6 +803,7 @@ function createDocsAPI(options) {
599
803
  const appDir = getNextAppDir(root);
600
804
  const contentDir = options?.contentDir ?? path.join(appDir, entry);
601
805
  const changelogConfig = resolveChangelogConfig(options?.changelog);
806
+ const agentFeedbackConfig = resolveAgentFeedbackConfig(options?.feedback);
602
807
  const i18n = resolveDocsI18n(options?.i18n ?? readI18nConfig(root));
603
808
  const aiConfig = options?.ai ?? readAIConfig(root);
604
809
  const searchConfig = options?.search;
@@ -722,9 +927,21 @@ function createDocsAPI(options) {
722
927
  async GET(request) {
723
928
  const ctx = resolveContextFromRequest(request);
724
929
  const url = new URL(request.url);
725
- const format = url.searchParams.get("format");
726
- if (format === "markdown") {
727
- const document = await getMarkdownDocument(ctx, url.searchParams.get("path")?.trim() ?? "");
930
+ const agentFeedbackRequest = resolveAgentFeedbackRequest(url, agentFeedbackConfig);
931
+ if (agentFeedbackRequest) {
932
+ if (agentFeedbackRequest.kind === "submit") return Response.json({ error: "Method Not Allowed" }, {
933
+ status: 405,
934
+ headers: { Allow: "POST" }
935
+ });
936
+ return new Response(JSON.stringify(agentFeedbackConfig.schema, null, 2), { headers: {
937
+ "Content-Type": "application/schema+json; charset=utf-8",
938
+ "Cache-Control": "public, max-age=0, s-maxage=3600",
939
+ "X-Robots-Tag": "noindex"
940
+ } });
941
+ }
942
+ const markdownRequest = resolveMarkdownRequest(entry, url);
943
+ if (markdownRequest) {
944
+ const document = await getMarkdownDocument(ctx, markdownRequest.requestedPath);
728
945
  if (!document) return new Response("Not Found", {
729
946
  status: 404,
730
947
  headers: {
@@ -738,6 +955,7 @@ function createDocsAPI(options) {
738
955
  "X-Robots-Tag": "noindex"
739
956
  } });
740
957
  }
958
+ const format = url.searchParams.get("format");
741
959
  if (format === "llms") return new Response(getLlmsContent(ctx).llmsTxt, { headers: {
742
960
  "Content-Type": "text/plain; charset=utf-8",
743
961
  "Cache-Control": "public, max-age=3600"
@@ -759,6 +977,26 @@ function createDocsAPI(options) {
759
977
  return new Response(JSON.stringify(results), { headers: { "Content-Type": "application/json" } });
760
978
  },
761
979
  async POST(request) {
980
+ const agentFeedbackRequest = resolveAgentFeedbackRequest(new URL(request.url), agentFeedbackConfig);
981
+ if (agentFeedbackRequest) {
982
+ if (agentFeedbackRequest.kind === "schema") return Response.json({ error: "Method Not Allowed" }, {
983
+ status: 405,
984
+ headers: { Allow: "GET" }
985
+ });
986
+ const parsed = await parseAgentFeedbackData(request);
987
+ if (!parsed.ok) return parsed.response;
988
+ const payloadError = validateAgentFeedbackPayload(parsed.data.payload, agentFeedbackConfig.payloadSchema);
989
+ if (payloadError) return Response.json({ error: payloadError }, { status: 400 });
990
+ if (!agentFeedbackConfig.onFeedback) return Response.json({
991
+ ok: true,
992
+ handled: false
993
+ }, { status: 202 });
994
+ await agentFeedbackConfig.onFeedback(parsed.data);
995
+ return Response.json({
996
+ ok: true,
997
+ handled: true
998
+ }, { status: 201 });
999
+ }
762
1000
  if (!aiConfig.enabled) return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
763
1001
  return handleAskAI(request, getIndexes(resolveContextFromRequest(request)), aiConfig);
764
1002
  }
@@ -716,8 +716,9 @@ function resolveFeedbackConfig(feedback) {
716
716
  ...defaults,
717
717
  enabled: true
718
718
  };
719
+ const hasHumanFeedbackConfig = feedback.enabled !== void 0 || feedback.question !== void 0 || feedback.placeholder !== void 0 || feedback.positiveLabel !== void 0 || feedback.negativeLabel !== void 0 || feedback.submitLabel !== void 0 || feedback.onFeedback !== void 0;
719
720
  return {
720
- enabled: feedback.enabled !== false,
721
+ enabled: feedback.enabled === true || feedback.enabled !== false && hasHumanFeedbackConfig,
721
722
  question: feedback.question ?? defaults.question,
722
723
  placeholder: feedback.placeholder ?? defaults.placeholder,
723
724
  positiveLabel: feedback.positiveLabel ?? defaults.positiveLabel,
@@ -192,8 +192,9 @@ function resolveFeedbackConfig(feedback) {
192
192
  ...defaults,
193
193
  enabled: true
194
194
  };
195
+ const hasHumanFeedbackConfig = feedback.enabled !== void 0 || feedback.question !== void 0 || feedback.placeholder !== void 0 || feedback.positiveLabel !== void 0 || feedback.negativeLabel !== void 0 || feedback.submitLabel !== void 0 || feedback.onFeedback !== void 0;
195
196
  return {
196
- enabled: feedback.enabled !== false,
197
+ enabled: feedback.enabled === true || feedback.enabled !== false && hasHumanFeedbackConfig,
197
198
  question: feedback.question ?? defaults.question,
198
199
  placeholder: feedback.placeholder ?? defaults.placeholder,
199
200
  positiveLabel: feedback.positiveLabel ?? defaults.positiveLabel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -133,7 +133,7 @@
133
133
  "tsdown": "^0.20.3",
134
134
  "typescript": "^5.9.3",
135
135
  "vitest": "^3.2.4",
136
- "@farming-labs/docs": "0.1.19"
136
+ "@farming-labs/docs": "0.1.21"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "@farming-labs/docs": ">=0.0.1",