@farming-labs/theme 0.2.3 → 0.2.5

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,6 +1,7 @@
1
1
  import { highlight } from "sugar-high";
2
2
 
3
3
  //#region src/ai-markdown.ts
4
+ const codeBlockTokenBoundary = String.fromCharCode(0);
4
5
  function buildCodeBlock(lang, code) {
5
6
  const highlighted = highlight(code.replace(/\n$/, "")).replace(/<\/span>\n<span/g, "</span><span");
6
7
  return `<div class="fd-ai-code-block"><div class="fd-ai-code-header">${lang ? `<div class="fd-ai-code-lang">${escapeHtml(lang)}</div>` : ""}<button class="fd-ai-code-copy" onclick="(function(btn){var code=btn.closest('.fd-ai-code-block').querySelector('code').textContent;navigator.clipboard.writeText(code).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy'},1500)})})(this)">Copy</button></div><pre><code>${highlighted}</code></pre></div>`;
@@ -51,13 +52,36 @@ function replaceFencedCodeBlocks(text, codeBlocks, tokenBoundary) {
51
52
  return output.join("\n");
52
53
  }
53
54
  function renderAIResponseMarkdown(text) {
54
- const codeBlockTokenBoundary = String.fromCharCode(0);
55
- const codeBlockTokenPattern = new RegExp(`${codeBlockTokenBoundary}CB(\\d+)${codeBlockTokenBoundary}`, "g");
56
55
  const codeBlocks = [];
57
- const lines = replaceFencedCodeBlocks(text, codeBlocks, codeBlockTokenBoundary).split("\n");
56
+ return renderMarkdownBlocks(replaceFencedCodeBlocks(text, codeBlocks, codeBlockTokenBoundary), codeBlocks);
57
+ }
58
+ function renderMarkdownBlocks(text, codeBlocks) {
59
+ const lines = text.split("\n");
58
60
  const output = [];
59
61
  let i = 0;
60
62
  while (i < lines.length) {
63
+ const trimmed = lines[i].trim();
64
+ if (!trimmed) {
65
+ i++;
66
+ continue;
67
+ }
68
+ const codeBlockIndex = getCodeBlockTokenIndex(trimmed);
69
+ if (codeBlockIndex !== null) {
70
+ output.push(codeBlocks[codeBlockIndex] ?? "");
71
+ i++;
72
+ continue;
73
+ }
74
+ if (isHorizontalRule(trimmed)) {
75
+ output.push("<hr class=\"fd-ai-hr\" />");
76
+ i++;
77
+ continue;
78
+ }
79
+ const heading = getHeading(trimmed);
80
+ if (heading) {
81
+ output.push(`<h${heading.level}>${renderInlineMarkdown(heading.text)}</h${heading.level}>`);
82
+ i++;
83
+ continue;
84
+ }
61
85
  if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
62
86
  const tableLines = [lines[i]];
63
87
  i++;
@@ -69,17 +93,82 @@ function renderAIResponseMarkdown(text) {
69
93
  output.push(renderTable(tableLines));
70
94
  continue;
71
95
  }
72
- output.push(lines[i]);
96
+ const unorderedItems = collectListItems(lines, i, "unordered");
97
+ if (unorderedItems) {
98
+ output.push(`<ul>${unorderedItems.items.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ul>`);
99
+ i = unorderedItems.nextIndex;
100
+ continue;
101
+ }
102
+ const orderedItems = collectListItems(lines, i, "ordered");
103
+ if (orderedItems) {
104
+ output.push(`<ol>${orderedItems.items.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ol>`);
105
+ i = orderedItems.nextIndex;
106
+ continue;
107
+ }
108
+ const paragraphLines = [];
109
+ while (i < lines.length && lines[i].trim() && !isMarkdownBlockStart(lines, i)) {
110
+ paragraphLines.push(lines[i].trim());
111
+ i++;
112
+ }
113
+ output.push(`<p>${paragraphLines.map((paragraphLine) => renderInlineMarkdown(paragraphLine)).join("<br>")}</p>`);
114
+ }
115
+ return output.join("");
116
+ }
117
+ function getCodeBlockTokenIndex(line) {
118
+ const prefix = `${codeBlockTokenBoundary}CB`;
119
+ if (!line.startsWith(prefix) || !line.endsWith(codeBlockTokenBoundary)) return null;
120
+ const rawIndex = line.slice(prefix.length, -1);
121
+ if (!/^\d+$/.test(rawIndex)) return null;
122
+ return Number(rawIndex);
123
+ }
124
+ function getHeading(line) {
125
+ const match = /^(#{1,4})\s+(.+)$/.exec(line);
126
+ if (!match) return null;
127
+ return {
128
+ level: match[1].length + 1,
129
+ text: match[2]
130
+ };
131
+ }
132
+ function collectListItems(lines, startIndex, type) {
133
+ const pattern = type === "ordered" ? /^(?: {0,3})\d+\.\s+(.+)$/ : /^(?: {0,3})[-*+]\s+(.+)$/;
134
+ const items = [];
135
+ let i = startIndex;
136
+ while (i < lines.length) {
137
+ const match = pattern.exec(lines[i]);
138
+ if (!match) break;
139
+ items.push(match[1]);
73
140
  i++;
74
141
  }
75
- let result = output.join("\n");
76
- result = result.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<a href=\"$2\">$1</a>").replace(/^### (.*$)/gm, "<h4>$1</h4>").replace(/^## (.*$)/gm, "<h3>$1</h3>").replace(/^# (.*$)/gm, "<h2>$1</h2>").replace(/^[-*] (.*$)/gm, "<div style=\"display:flex;gap:8px;padding:2px 0\"><span style=\"opacity:0.5\">•</span><span>$1</span></div>").replace(/^(\d+)\. (.*$)/gm, "<div style=\"display:flex;gap:8px;padding:2px 0\"><span style=\"opacity:0.5\">$1.</span><span>$2</span></div>").replace(/\n\n/g, "<div style=\"height:8px\"></div>").replace(/\n/g, "<br>");
77
- result = result.replace(codeBlockTokenPattern, (_m, idx) => codeBlocks[Number(idx)]);
78
- return result;
142
+ return items.length > 0 ? {
143
+ items,
144
+ nextIndex: i
145
+ } : null;
146
+ }
147
+ function isMarkdownBlockStart(lines, index) {
148
+ const line = lines[index];
149
+ const trimmed = line.trim();
150
+ return getCodeBlockTokenIndex(trimmed) !== null || isHorizontalRule(trimmed) || Boolean(getHeading(trimmed)) || isTableRow(line) && index + 1 < lines.length && isTableSeparator(lines[index + 1]) || Boolean(collectListItems(lines, index, "unordered")) || Boolean(collectListItems(lines, index, "ordered"));
151
+ }
152
+ function renderInlineMarkdown(text) {
153
+ const inlineCodeTokens = [];
154
+ let result = escapeHtml(text).replace(/`([^`]+)`/g, (_match, code) => {
155
+ inlineCodeTokens.push(`<code>${code}</code>`);
156
+ return `${codeBlockTokenBoundary}IC${inlineCodeTokens.length - 1}${codeBlockTokenBoundary}`;
157
+ });
158
+ result = result.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, href) => {
159
+ return `<a href="${escapeAttribute(href)}">${label}</a>`;
160
+ });
161
+ return result.replace(new RegExp(`${codeBlockTokenBoundary}IC(\\d+)${codeBlockTokenBoundary}`, "g"), (_match, idx) => inlineCodeTokens[Number(idx)] ?? "");
79
162
  }
80
163
  function escapeHtml(s) {
81
164
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
82
165
  }
166
+ function escapeAttribute(s) {
167
+ return escapeHtml(s).replace(/"/g, "&quot;");
168
+ }
169
+ function isHorizontalRule(line) {
170
+ return /^(?:-{3,}|\*{3,}|_{3,})$/.test(line);
171
+ }
83
172
  function isTableRow(line) {
84
173
  const trimmed = line.trim();
85
174
  return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
@@ -89,8 +178,8 @@ function isTableSeparator(line) {
89
178
  }
90
179
  function renderTable(rows) {
91
180
  const parseRow = (row) => row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
92
- return `<table>${`<thead><tr>${parseRow(rows[0]).map((c) => `<th>${c}</th>`).join("")}</tr></thead>`}<tbody>${rows.slice(1).map((row) => {
93
- return `<tr>${parseRow(row).map((c) => `<td>${c}</td>`).join("")}</tr>`;
181
+ return `<table>${`<thead><tr>${parseRow(rows[0]).map((c) => `<th>${renderInlineMarkdown(c)}</th>`).join("")}</tr></thead>`}<tbody>${rows.slice(1).map((row) => {
182
+ return `<tr>${parseRow(row).map((c) => `<td>${renderInlineMarkdown(c)}</td>`).join("")}</tr>`;
94
183
  }).join("")}</tbody></table>`;
95
184
  }
96
185
 
package/dist/docs-api.mjs CHANGED
@@ -429,6 +429,35 @@ async function parseAgentFeedbackData(request) {
429
429
  }
430
430
  };
431
431
  }
432
+ function buildAgentFeedbackAnalyticsProperties(data, options = {}) {
433
+ const payloadKeys = Object.keys(data.payload);
434
+ const context = data.context;
435
+ return {
436
+ ...options.requestProperties,
437
+ feedbackKind: "agent",
438
+ agentFeedbackContext: context,
439
+ contextPage: context?.page,
440
+ contextUrl: context?.url,
441
+ contextSlug: context?.slug,
442
+ contextLocale: context?.locale,
443
+ contextSource: context?.source,
444
+ payloadKeys,
445
+ payloadFieldCount: payloadKeys.length,
446
+ hasContext: Boolean(data.context),
447
+ hasPayload: payloadKeys.length > 0,
448
+ ...typeof options.handled === "boolean" ? { handled: options.handled } : {},
449
+ ...options.reason ? { reason: options.reason } : {},
450
+ ...options.error ? { error: options.error } : {}
451
+ };
452
+ }
453
+ function buildAgentFeedbackAnalyticsInput(data) {
454
+ return {
455
+ feedbackContext: data.context,
456
+ feedbackPayload: data.payload,
457
+ agentFeedbackContext: data.context,
458
+ agentFeedbackPayload: data.payload
459
+ };
460
+ }
432
461
  function validateAgentFeedbackPayload(value, schema, valuePath = "payload") {
433
462
  const schemaType = typeof schema.type === "string" ? schema.type : void 0;
434
463
  if (Array.isArray(schema.enum) && !schema.enum.some((entry) => Object.is(entry, value))) return `${valuePath} must be one of the configured enum values`;
@@ -2845,6 +2874,7 @@ function createDocsAPI(options) {
2845
2874
  path: url.pathname,
2846
2875
  properties: {
2847
2876
  ...requestAnalyticsProperties,
2877
+ feedbackKind: "agent",
2848
2878
  reason: "invalid_body"
2849
2879
  }
2850
2880
  });
@@ -2857,11 +2887,12 @@ function createDocsAPI(options) {
2857
2887
  source: "server",
2858
2888
  url: request.url,
2859
2889
  path: url.pathname,
2860
- properties: {
2861
- ...requestAnalyticsProperties,
2890
+ input: buildAgentFeedbackAnalyticsInput(parsed.data),
2891
+ properties: buildAgentFeedbackAnalyticsProperties(parsed.data, {
2892
+ requestProperties: requestAnalyticsProperties,
2862
2893
  reason: "invalid_payload",
2863
2894
  error: payloadError
2864
- }
2895
+ })
2865
2896
  });
2866
2897
  return Response.json({ error: payloadError }, { status: 400 });
2867
2898
  }
@@ -2871,12 +2902,11 @@ function createDocsAPI(options) {
2871
2902
  source: "server",
2872
2903
  url: request.url,
2873
2904
  path: url.pathname,
2874
- properties: {
2875
- ...requestAnalyticsProperties,
2876
- handled: false,
2877
- payloadKeys: Object.keys(parsed.data.payload),
2878
- hasContext: Boolean(parsed.data.context)
2879
- }
2905
+ input: buildAgentFeedbackAnalyticsInput(parsed.data),
2906
+ properties: buildAgentFeedbackAnalyticsProperties(parsed.data, {
2907
+ requestProperties: requestAnalyticsProperties,
2908
+ handled: false
2909
+ })
2880
2910
  });
2881
2911
  return Response.json({
2882
2912
  ok: true,
@@ -2889,12 +2919,11 @@ function createDocsAPI(options) {
2889
2919
  source: "server",
2890
2920
  url: request.url,
2891
2921
  path: url.pathname,
2892
- properties: {
2893
- ...requestAnalyticsProperties,
2894
- handled: true,
2895
- payloadKeys: Object.keys(parsed.data.payload),
2896
- hasContext: Boolean(parsed.data.context)
2897
- }
2922
+ input: buildAgentFeedbackAnalyticsInput(parsed.data),
2923
+ properties: buildAgentFeedbackAnalyticsProperties(parsed.data, {
2924
+ requestProperties: requestAnalyticsProperties,
2925
+ handled: true
2926
+ })
2898
2927
  });
2899
2928
  return Response.json({
2900
2929
  ok: true,
@@ -52,6 +52,22 @@ function buildFeedbackPayload(value, pathname, entry, comment, locale) {
52
52
  locale
53
53
  };
54
54
  }
55
+ function buildFeedbackAnalyticsProperties(data) {
56
+ return {
57
+ feedbackKind: "page",
58
+ value: data.value,
59
+ feedbackValue: data.value,
60
+ hasComment: Boolean(data.comment),
61
+ commentLength: data.comment?.length ?? 0,
62
+ title: data.title,
63
+ description: data.description,
64
+ url: data.url,
65
+ pathname: data.pathname,
66
+ path: data.path,
67
+ entry: data.entry,
68
+ slug: data.slug
69
+ };
70
+ }
55
71
  function ThumbUpIcon() {
56
72
  return /* @__PURE__ */ jsx("svg", {
57
73
  width: "14",
@@ -115,32 +131,40 @@ function DocsFeedback({ pathname, entry, locale, question = "How is this guide?"
115
131
  function handleSelect(value) {
116
132
  setSelected(value);
117
133
  if (status !== "idle") setStatus("idle");
118
- if (analytics) emitClientAnalyticsEvent({
119
- type: "feedback_select",
120
- locale,
121
- path: normalizedPathname,
122
- properties: {
123
- value,
124
- slug: resolveSlug(entry, normalizedPathname)
125
- }
126
- });
134
+ if (analytics) {
135
+ const slug = resolveSlug(entry, normalizedPathname);
136
+ emitClientAnalyticsEvent({
137
+ type: "feedback_select",
138
+ locale,
139
+ path: normalizedPathname,
140
+ input: { feedbackValue: value },
141
+ properties: {
142
+ feedbackKind: "page",
143
+ value,
144
+ feedbackValue: value,
145
+ entry,
146
+ pathname: normalizedPathname,
147
+ path: normalizedPathname,
148
+ slug
149
+ }
150
+ });
151
+ }
127
152
  }
128
153
  async function handleSubmit() {
129
154
  if (!selected || status === "submitting") return;
130
155
  setStatus("submitting");
156
+ const payload = buildFeedbackPayload(selected, normalizedPathname, entry, comment, locale);
131
157
  try {
132
- const payload = buildFeedbackPayload(selected, normalizedPathname, entry, comment, locale);
133
158
  await emitFeedback(payload, onFeedback);
134
159
  if (analytics) emitClientAnalyticsEvent({
135
160
  type: "feedback_submit",
136
161
  locale,
137
162
  path: normalizedPathname,
138
- properties: {
139
- value: payload.value,
140
- slug: payload.slug,
141
- hasComment: Boolean(payload.comment),
142
- commentLength: payload.comment?.length ?? 0
143
- }
163
+ input: {
164
+ feedbackValue: payload.value,
165
+ feedbackComment: payload.comment
166
+ },
167
+ properties: buildFeedbackAnalyticsProperties(payload)
144
168
  });
145
169
  setStatus("submitted");
146
170
  } catch {
@@ -148,12 +172,11 @@ function DocsFeedback({ pathname, entry, locale, question = "How is this guide?"
148
172
  type: "feedback_error",
149
173
  locale,
150
174
  path: normalizedPathname,
151
- properties: {
152
- value: selected,
153
- slug: resolveSlug(entry, normalizedPathname),
154
- hasComment: Boolean(comment.trim()),
155
- commentLength: comment.trim().length
156
- }
175
+ input: {
176
+ feedbackValue: payload.value,
177
+ feedbackComment: payload.comment
178
+ },
179
+ properties: buildFeedbackAnalyticsProperties(payload)
157
180
  });
158
181
  setStatus("error");
159
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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.2.3"
148
+ "@farming-labs/docs": "0.2.5"
149
149
  },
150
150
  "peerDependencies": {
151
151
  "@farming-labs/docs": ">=0.0.1",
package/styles/ai.css CHANGED
@@ -924,6 +924,35 @@
924
924
  color: inherit;
925
925
  }
926
926
 
927
+ .fd-ai-bubble-ai h5 {
928
+ font-size: 0.875rem;
929
+ font-weight: 600;
930
+ margin: 8px 0 4px;
931
+ line-height: 1.4;
932
+ color: inherit;
933
+ }
934
+
935
+ .fd-ai-bubble-ai ul,
936
+ .fd-ai-bubble-ai ol {
937
+ margin: 6px 0 8px;
938
+ padding-left: 1.25rem;
939
+ }
940
+
941
+ .fd-ai-bubble-ai li {
942
+ margin: 3px 0;
943
+ padding-left: 2px;
944
+ }
945
+
946
+ .fd-ai-bubble-ai li::marker {
947
+ color: var(--color-fd-muted-foreground, #71717a);
948
+ }
949
+
950
+ .fd-ai-bubble-ai .fd-ai-hr {
951
+ border: 0;
952
+ border-top: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
953
+ margin: 12px 0;
954
+ }
955
+
927
956
  .fd-ai-bubble-ai strong {
928
957
  font-weight: 600;
929
958
  color: var(--color-fd-foreground, #e4e4e7);
@@ -1251,6 +1280,35 @@
1251
1280
  color: var(--color-fd-foreground, #e4e4e7);
1252
1281
  }
1253
1282
 
1283
+ .fd-ai-fm-msg-content h5 {
1284
+ font-size: 0.9375rem;
1285
+ font-weight: 600;
1286
+ margin: 10px 0 4px;
1287
+ line-height: 1.4;
1288
+ color: var(--color-fd-foreground, #e4e4e7);
1289
+ }
1290
+
1291
+ .fd-ai-fm-msg-content ul,
1292
+ .fd-ai-fm-msg-content ol {
1293
+ margin: 8px 0 10px;
1294
+ padding-left: 1.35rem;
1295
+ }
1296
+
1297
+ .fd-ai-fm-msg-content li {
1298
+ margin: 4px 0;
1299
+ padding-left: 2px;
1300
+ }
1301
+
1302
+ .fd-ai-fm-msg-content li::marker {
1303
+ color: var(--color-fd-muted-foreground, #71717a);
1304
+ }
1305
+
1306
+ .fd-ai-fm-msg-content .fd-ai-hr {
1307
+ border: 0;
1308
+ border-top: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
1309
+ margin: 16px 0;
1310
+ }
1311
+
1254
1312
  .fd-ai-fm-msg-content strong {
1255
1313
  font-weight: 600;
1256
1314
  color: var(--color-fd-foreground, #e4e4e7);