@farming-labs/theme 0.1.20 → 0.1.22

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}`);
@@ -608,6 +803,7 @@ function createDocsAPI(options) {
608
803
  const appDir = getNextAppDir(root);
609
804
  const contentDir = options?.contentDir ?? path.join(appDir, entry);
610
805
  const changelogConfig = resolveChangelogConfig(options?.changelog);
806
+ const agentFeedbackConfig = resolveAgentFeedbackConfig(options?.feedback);
611
807
  const i18n = resolveDocsI18n(options?.i18n ?? readI18nConfig(root));
612
808
  const aiConfig = options?.ai ?? readAIConfig(root);
613
809
  const searchConfig = options?.search;
@@ -731,6 +927,18 @@ function createDocsAPI(options) {
731
927
  async GET(request) {
732
928
  const ctx = resolveContextFromRequest(request);
733
929
  const url = new URL(request.url);
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
+ }
734
942
  const markdownRequest = resolveMarkdownRequest(entry, url);
735
943
  if (markdownRequest) {
736
944
  const document = await getMarkdownDocument(ctx, markdownRequest.requestedPath);
@@ -769,6 +977,26 @@ function createDocsAPI(options) {
769
977
  return new Response(JSON.stringify(results), { headers: { "Content-Type": "application/json" } });
770
978
  },
771
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
+ }
772
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 });
773
1001
  return handleAskAI(request, getIndexes(resolveContextFromRequest(request)), aiConfig);
774
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.20",
3
+ "version": "0.1.22",
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.20"
136
+ "@farming-labs/docs": "0.1.22"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "@farming-labs/docs": ">=0.0.1",