@seoengine.ai/next-llm-ready 1.0.0
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/LICENSE +21 -0
- package/README.md +858 -0
- package/dist/api/index.cjs +624 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.d.cts +295 -0
- package/dist/api/index.d.ts +295 -0
- package/dist/api/index.js +613 -0
- package/dist/api/index.js.map +1 -0
- package/dist/hooks/index.cjs +619 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.cts +257 -0
- package/dist/hooks/index.d.ts +257 -0
- package/dist/hooks/index.js +611 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.cjs +1609 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +850 -0
- package/dist/index.d.ts +850 -0
- package/dist/index.js +1576 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.cjs +398 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +134 -0
- package/dist/server/index.d.ts +134 -0
- package/dist/server/index.js +390 -0
- package/dist/server/index.js.map +1 -0
- package/dist/styles.css +855 -0
- package/package.json +118 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1576 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
|
|
5
|
+
// src/components/CopyButton.tsx
|
|
6
|
+
|
|
7
|
+
// src/utils/clipboard.ts
|
|
8
|
+
async function copyToClipboard(text) {
|
|
9
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
10
|
+
try {
|
|
11
|
+
await navigator.clipboard.writeText(text);
|
|
12
|
+
return true;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.warn("Clipboard API failed, using fallback:", err);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return fallbackCopyToClipboard(text);
|
|
18
|
+
}
|
|
19
|
+
function fallbackCopyToClipboard(text) {
|
|
20
|
+
const textarea = document.createElement("textarea");
|
|
21
|
+
textarea.value = text;
|
|
22
|
+
textarea.style.position = "fixed";
|
|
23
|
+
textarea.style.top = "-9999px";
|
|
24
|
+
textarea.style.left = "-9999px";
|
|
25
|
+
textarea.style.opacity = "0";
|
|
26
|
+
textarea.style.pointerEvents = "none";
|
|
27
|
+
textarea.style.fontSize = "16px";
|
|
28
|
+
document.body.appendChild(textarea);
|
|
29
|
+
textarea.focus();
|
|
30
|
+
textarea.select();
|
|
31
|
+
try {
|
|
32
|
+
const successful = document.execCommand("copy");
|
|
33
|
+
document.body.removeChild(textarea);
|
|
34
|
+
return successful;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error("Fallback copy failed:", err);
|
|
37
|
+
document.body.removeChild(textarea);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/utils/download.ts
|
|
43
|
+
var defaultOptions = {
|
|
44
|
+
filename: "content",
|
|
45
|
+
extension: "md",
|
|
46
|
+
includeMetadata: false
|
|
47
|
+
};
|
|
48
|
+
function downloadAsFile(content, options = {}) {
|
|
49
|
+
const opts = { ...defaultOptions, ...options };
|
|
50
|
+
const filename = sanitizeFilename(opts.filename || "content");
|
|
51
|
+
const fullFilename = `${filename}.${opts.extension}`;
|
|
52
|
+
const mimeType = opts.extension === "md" ? "text/markdown" : "text/plain";
|
|
53
|
+
const blob = new Blob([content], { type: `${mimeType};charset=utf-8` });
|
|
54
|
+
const url = URL.createObjectURL(blob);
|
|
55
|
+
const link = document.createElement("a");
|
|
56
|
+
link.href = url;
|
|
57
|
+
link.download = fullFilename;
|
|
58
|
+
document.body.appendChild(link);
|
|
59
|
+
link.click();
|
|
60
|
+
document.body.removeChild(link);
|
|
61
|
+
URL.revokeObjectURL(url);
|
|
62
|
+
}
|
|
63
|
+
function sanitizeFilename(name) {
|
|
64
|
+
return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").substring(0, 100);
|
|
65
|
+
}
|
|
66
|
+
function generateFilename(title) {
|
|
67
|
+
const base = sanitizeFilename(title);
|
|
68
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
69
|
+
return `${base}-${date}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/utils/html-to-markdown.ts
|
|
73
|
+
var defaultOptions2 = {
|
|
74
|
+
preserveLineBreaks: true,
|
|
75
|
+
convertImages: true,
|
|
76
|
+
convertLinks: true,
|
|
77
|
+
stripScripts: true,
|
|
78
|
+
customHandlers: {}
|
|
79
|
+
};
|
|
80
|
+
function htmlToMarkdown(html, options = {}) {
|
|
81
|
+
const opts = { ...defaultOptions2, ...options };
|
|
82
|
+
if (typeof window === "undefined") {
|
|
83
|
+
return serverSideConvert(html, opts);
|
|
84
|
+
}
|
|
85
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
86
|
+
return convertNode(doc.body, opts);
|
|
87
|
+
}
|
|
88
|
+
function serverSideConvert(html, opts) {
|
|
89
|
+
let markdown = html;
|
|
90
|
+
if (opts.stripScripts) {
|
|
91
|
+
markdown = markdown.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
|
92
|
+
markdown = markdown.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "");
|
|
93
|
+
}
|
|
94
|
+
markdown = markdown.replace(/<h1[^>]*>(.*?)<\/h1>/gi, "\n# $1\n");
|
|
95
|
+
markdown = markdown.replace(/<h2[^>]*>(.*?)<\/h2>/gi, "\n## $1\n");
|
|
96
|
+
markdown = markdown.replace(/<h3[^>]*>(.*?)<\/h3>/gi, "\n### $1\n");
|
|
97
|
+
markdown = markdown.replace(/<h4[^>]*>(.*?)<\/h4>/gi, "\n#### $1\n");
|
|
98
|
+
markdown = markdown.replace(/<h5[^>]*>(.*?)<\/h5>/gi, "\n##### $1\n");
|
|
99
|
+
markdown = markdown.replace(/<h6[^>]*>(.*?)<\/h6>/gi, "\n###### $1\n");
|
|
100
|
+
markdown = markdown.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gi, "**$2**");
|
|
101
|
+
markdown = markdown.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gi, "*$2*");
|
|
102
|
+
if (opts.convertLinks) {
|
|
103
|
+
markdown = markdown.replace(/<a[^>]*href=["']([^"']*)["'][^>]*>(.*?)<\/a>/gi, "[$2]($1)");
|
|
104
|
+
}
|
|
105
|
+
if (opts.convertImages) {
|
|
106
|
+
markdown = markdown.replace(
|
|
107
|
+
/<img[^>]*src=["']([^"']*)["'][^>]*alt=["']([^"']*)["'][^>]*\/?>/gi,
|
|
108
|
+
""
|
|
109
|
+
);
|
|
110
|
+
markdown = markdown.replace(
|
|
111
|
+
/<img[^>]*alt=["']([^"']*)["'][^>]*src=["']([^"']*)["'][^>]*\/?>/gi,
|
|
112
|
+
""
|
|
113
|
+
);
|
|
114
|
+
markdown = markdown.replace(/<img[^>]*src=["']([^"']*)["'][^>]*\/?>/gi, "");
|
|
115
|
+
}
|
|
116
|
+
markdown = markdown.replace(/<ul[^>]*>/gi, "\n");
|
|
117
|
+
markdown = markdown.replace(/<\/ul>/gi, "\n");
|
|
118
|
+
markdown = markdown.replace(/<ol[^>]*>/gi, "\n");
|
|
119
|
+
markdown = markdown.replace(/<\/ol>/gi, "\n");
|
|
120
|
+
markdown = markdown.replace(/<li[^>]*>(.*?)<\/li>/gi, "- $1\n");
|
|
121
|
+
markdown = markdown.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gis, (_, content) => {
|
|
122
|
+
return "\n> " + content.trim().replace(/\n/g, "\n> ") + "\n";
|
|
123
|
+
});
|
|
124
|
+
markdown = markdown.replace(/<pre[^>]*><code[^>]*>(.*?)<\/code><\/pre>/gis, "\n```\n$1\n```\n");
|
|
125
|
+
markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
|
|
126
|
+
markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gis, "\n$1\n");
|
|
127
|
+
markdown = markdown.replace(/<br\s*\/?>/gi, "\n");
|
|
128
|
+
markdown = markdown.replace(/<hr[^>]*\/?>/gi, "\n---\n");
|
|
129
|
+
markdown = markdown.replace(/<[^>]+>/g, "");
|
|
130
|
+
markdown = decodeHTMLEntities(markdown);
|
|
131
|
+
markdown = markdown.replace(/\n{3,}/g, "\n\n");
|
|
132
|
+
markdown = markdown.trim();
|
|
133
|
+
return markdown;
|
|
134
|
+
}
|
|
135
|
+
function convertNode(node, opts) {
|
|
136
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
137
|
+
return node.textContent || "";
|
|
138
|
+
}
|
|
139
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
const element = node;
|
|
143
|
+
const tagName = element.tagName.toLowerCase();
|
|
144
|
+
if (opts.customHandlers?.[tagName]) {
|
|
145
|
+
return opts.customHandlers[tagName](element);
|
|
146
|
+
}
|
|
147
|
+
if (opts.stripScripts && (tagName === "script" || tagName === "style")) {
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
const childContent = Array.from(element.childNodes).map((child) => convertNode(child, opts)).join("");
|
|
151
|
+
switch (tagName) {
|
|
152
|
+
case "h1":
|
|
153
|
+
return `
|
|
154
|
+
# ${childContent.trim()}
|
|
155
|
+
`;
|
|
156
|
+
case "h2":
|
|
157
|
+
return `
|
|
158
|
+
## ${childContent.trim()}
|
|
159
|
+
`;
|
|
160
|
+
case "h3":
|
|
161
|
+
return `
|
|
162
|
+
### ${childContent.trim()}
|
|
163
|
+
`;
|
|
164
|
+
case "h4":
|
|
165
|
+
return `
|
|
166
|
+
#### ${childContent.trim()}
|
|
167
|
+
`;
|
|
168
|
+
case "h5":
|
|
169
|
+
return `
|
|
170
|
+
##### ${childContent.trim()}
|
|
171
|
+
`;
|
|
172
|
+
case "h6":
|
|
173
|
+
return `
|
|
174
|
+
###### ${childContent.trim()}
|
|
175
|
+
`;
|
|
176
|
+
case "p":
|
|
177
|
+
return `
|
|
178
|
+
${childContent.trim()}
|
|
179
|
+
`;
|
|
180
|
+
case "br":
|
|
181
|
+
return opts.preserveLineBreaks ? "\n" : " ";
|
|
182
|
+
case "strong":
|
|
183
|
+
case "b":
|
|
184
|
+
return `**${childContent}**`;
|
|
185
|
+
case "em":
|
|
186
|
+
case "i":
|
|
187
|
+
return `*${childContent}*`;
|
|
188
|
+
case "u":
|
|
189
|
+
return `_${childContent}_`;
|
|
190
|
+
case "s":
|
|
191
|
+
case "strike":
|
|
192
|
+
case "del":
|
|
193
|
+
return `~~${childContent}~~`;
|
|
194
|
+
case "a":
|
|
195
|
+
if (opts.convertLinks) {
|
|
196
|
+
const href = element.getAttribute("href") || "";
|
|
197
|
+
return `[${childContent}](${href})`;
|
|
198
|
+
}
|
|
199
|
+
return childContent;
|
|
200
|
+
case "img":
|
|
201
|
+
if (opts.convertImages) {
|
|
202
|
+
const src = element.getAttribute("src") || "";
|
|
203
|
+
const alt = element.getAttribute("alt") || "";
|
|
204
|
+
return ``;
|
|
205
|
+
}
|
|
206
|
+
return "";
|
|
207
|
+
case "ul":
|
|
208
|
+
return `
|
|
209
|
+
${childContent}
|
|
210
|
+
`;
|
|
211
|
+
case "ol":
|
|
212
|
+
return `
|
|
213
|
+
${childContent}
|
|
214
|
+
`;
|
|
215
|
+
case "li":
|
|
216
|
+
return `- ${childContent.trim()}
|
|
217
|
+
`;
|
|
218
|
+
case "blockquote":
|
|
219
|
+
return `
|
|
220
|
+
> ${childContent.trim().replace(/\n/g, "\n> ")}
|
|
221
|
+
`;
|
|
222
|
+
case "pre":
|
|
223
|
+
const codeElement = element.querySelector("code");
|
|
224
|
+
const lang = codeElement?.className.match(/language-(\w+)/)?.[1] || "";
|
|
225
|
+
const code = codeElement?.textContent || childContent;
|
|
226
|
+
return `
|
|
227
|
+
\`\`\`${lang}
|
|
228
|
+
${code.trim()}
|
|
229
|
+
\`\`\`
|
|
230
|
+
`;
|
|
231
|
+
case "code":
|
|
232
|
+
if (element.parentElement?.tagName.toLowerCase() === "pre") {
|
|
233
|
+
return childContent;
|
|
234
|
+
}
|
|
235
|
+
return `\`${childContent}\``;
|
|
236
|
+
case "hr":
|
|
237
|
+
return "\n---\n";
|
|
238
|
+
case "table":
|
|
239
|
+
return convertTable(element);
|
|
240
|
+
case "div":
|
|
241
|
+
case "section":
|
|
242
|
+
case "article":
|
|
243
|
+
case "main":
|
|
244
|
+
case "aside":
|
|
245
|
+
case "header":
|
|
246
|
+
case "footer":
|
|
247
|
+
case "nav":
|
|
248
|
+
return childContent;
|
|
249
|
+
default:
|
|
250
|
+
return childContent;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function convertTable(table) {
|
|
254
|
+
const rows = table.querySelectorAll("tr");
|
|
255
|
+
if (rows.length === 0) return "";
|
|
256
|
+
let markdown = "\n";
|
|
257
|
+
let headerProcessed = false;
|
|
258
|
+
rows.forEach((row, index) => {
|
|
259
|
+
const cells = row.querySelectorAll("th, td");
|
|
260
|
+
const rowContent = Array.from(cells).map((cell) => cell.textContent?.trim() || "").join(" | ");
|
|
261
|
+
markdown += `| ${rowContent} |
|
|
262
|
+
`;
|
|
263
|
+
if (!headerProcessed && (row.querySelector("th") || index === 0)) {
|
|
264
|
+
const separator = Array.from(cells).map(() => "---").join(" | ");
|
|
265
|
+
markdown += `| ${separator} |
|
|
266
|
+
`;
|
|
267
|
+
headerProcessed = true;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
return markdown + "\n";
|
|
271
|
+
}
|
|
272
|
+
function decodeHTMLEntities(text) {
|
|
273
|
+
const entities = {
|
|
274
|
+
"&": "&",
|
|
275
|
+
"<": "<",
|
|
276
|
+
">": ">",
|
|
277
|
+
""": '"',
|
|
278
|
+
"'": "'",
|
|
279
|
+
"'": "'",
|
|
280
|
+
" ": " ",
|
|
281
|
+
"—": "\u2014",
|
|
282
|
+
"–": "\u2013",
|
|
283
|
+
"…": "\u2026",
|
|
284
|
+
"©": "\xA9",
|
|
285
|
+
"®": "\xAE",
|
|
286
|
+
"™": "\u2122"
|
|
287
|
+
};
|
|
288
|
+
let result = text;
|
|
289
|
+
for (const [entity, char] of Object.entries(entities)) {
|
|
290
|
+
result = result.replace(new RegExp(entity, "g"), char);
|
|
291
|
+
}
|
|
292
|
+
result = result.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)));
|
|
293
|
+
result = result.replace(
|
|
294
|
+
/&#x([0-9a-f]+);/gi,
|
|
295
|
+
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
296
|
+
);
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
function countWords(text) {
|
|
300
|
+
return text.replace(/[^\w\s]/g, "").split(/\s+/).filter((word) => word.length > 0).length;
|
|
301
|
+
}
|
|
302
|
+
function calculateReadingTime(text, wordsPerMinute = 200) {
|
|
303
|
+
const words = countWords(text);
|
|
304
|
+
return Math.ceil(words / wordsPerMinute);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/hooks/use-llm-copy.ts
|
|
308
|
+
function generateMarkdownFromContent(content) {
|
|
309
|
+
if (typeof content === "string") {
|
|
310
|
+
return content;
|
|
311
|
+
}
|
|
312
|
+
const parts = [];
|
|
313
|
+
if (content.promptPrefix?.trim()) {
|
|
314
|
+
parts.push(content.promptPrefix.trim());
|
|
315
|
+
parts.push("");
|
|
316
|
+
}
|
|
317
|
+
parts.push(`# ${content.title}`);
|
|
318
|
+
parts.push("");
|
|
319
|
+
if (content.excerpt) {
|
|
320
|
+
parts.push(`> ${content.excerpt}`);
|
|
321
|
+
parts.push("");
|
|
322
|
+
}
|
|
323
|
+
parts.push("---");
|
|
324
|
+
parts.push(`- **Source**: ${content.url}`);
|
|
325
|
+
if (content.date) {
|
|
326
|
+
parts.push(`- **Date**: ${content.date}`);
|
|
327
|
+
}
|
|
328
|
+
if (content.author) {
|
|
329
|
+
parts.push(`- **Author**: ${content.author}`);
|
|
330
|
+
}
|
|
331
|
+
if (content.categories?.length) {
|
|
332
|
+
parts.push(`- **Categories**: ${content.categories.join(", ")}`);
|
|
333
|
+
}
|
|
334
|
+
if (content.tags?.length) {
|
|
335
|
+
parts.push(`- **Tags**: ${content.tags.join(", ")}`);
|
|
336
|
+
}
|
|
337
|
+
parts.push("---");
|
|
338
|
+
parts.push("");
|
|
339
|
+
if (content.content) {
|
|
340
|
+
const contentMarkdown = content.content.includes("<") && content.content.includes(">") ? htmlToMarkdown(content.content) : content.content;
|
|
341
|
+
parts.push(contentMarkdown);
|
|
342
|
+
}
|
|
343
|
+
return parts.join("\n").trim();
|
|
344
|
+
}
|
|
345
|
+
function useLLMCopy(options) {
|
|
346
|
+
const [isCopying, setIsCopying] = useState(false);
|
|
347
|
+
const [isSuccess, setIsSuccess] = useState(false);
|
|
348
|
+
const [error, setError] = useState(null);
|
|
349
|
+
const markdown = useMemo(() => {
|
|
350
|
+
return generateMarkdownFromContent(options.content);
|
|
351
|
+
}, [options.content]);
|
|
352
|
+
const trackEvent = useCallback(
|
|
353
|
+
(action) => {
|
|
354
|
+
if (options.onAnalytics) {
|
|
355
|
+
const contentId = typeof options.content === "string" ? void 0 : options.content.url;
|
|
356
|
+
options.onAnalytics({
|
|
357
|
+
action,
|
|
358
|
+
contentId,
|
|
359
|
+
url: typeof window !== "undefined" ? window.location.href : void 0,
|
|
360
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
[options]
|
|
365
|
+
);
|
|
366
|
+
const copy = useCallback(async () => {
|
|
367
|
+
setIsCopying(true);
|
|
368
|
+
setError(null);
|
|
369
|
+
try {
|
|
370
|
+
const success = await copyToClipboard(markdown);
|
|
371
|
+
if (success) {
|
|
372
|
+
setIsSuccess(true);
|
|
373
|
+
trackEvent("copy");
|
|
374
|
+
options.onSuccess?.("copy");
|
|
375
|
+
setTimeout(() => setIsSuccess(false), 2e3);
|
|
376
|
+
return true;
|
|
377
|
+
} else {
|
|
378
|
+
throw new Error("Failed to copy to clipboard");
|
|
379
|
+
}
|
|
380
|
+
} catch (err) {
|
|
381
|
+
const error2 = err instanceof Error ? err : new Error("Copy failed");
|
|
382
|
+
setError(error2);
|
|
383
|
+
options.onError?.(error2);
|
|
384
|
+
return false;
|
|
385
|
+
} finally {
|
|
386
|
+
setIsCopying(false);
|
|
387
|
+
}
|
|
388
|
+
}, [markdown, options, trackEvent]);
|
|
389
|
+
const view = useCallback(() => {
|
|
390
|
+
trackEvent("view");
|
|
391
|
+
options.onSuccess?.("view");
|
|
392
|
+
return markdown;
|
|
393
|
+
}, [markdown, options, trackEvent]);
|
|
394
|
+
const download = useCallback(
|
|
395
|
+
(filename) => {
|
|
396
|
+
const title = typeof options.content === "string" ? "content" : options.content.title;
|
|
397
|
+
downloadAsFile(markdown, {
|
|
398
|
+
filename: filename || generateFilename(title),
|
|
399
|
+
extension: "md"
|
|
400
|
+
});
|
|
401
|
+
trackEvent("download");
|
|
402
|
+
options.onSuccess?.("download");
|
|
403
|
+
},
|
|
404
|
+
[markdown, options, trackEvent]
|
|
405
|
+
);
|
|
406
|
+
return {
|
|
407
|
+
copy,
|
|
408
|
+
view,
|
|
409
|
+
download,
|
|
410
|
+
markdown,
|
|
411
|
+
isCopying,
|
|
412
|
+
isSuccess,
|
|
413
|
+
error
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function CopyButton({
|
|
417
|
+
content,
|
|
418
|
+
text = "Copy",
|
|
419
|
+
position = "inline",
|
|
420
|
+
keyboardShortcut = true,
|
|
421
|
+
className = "",
|
|
422
|
+
toastMessage = "Copied!",
|
|
423
|
+
toastDuration = 2e3,
|
|
424
|
+
onCopy,
|
|
425
|
+
onError,
|
|
426
|
+
onAnalytics,
|
|
427
|
+
style,
|
|
428
|
+
disabled = false
|
|
429
|
+
}) {
|
|
430
|
+
const [showToast, setShowToast] = useState(false);
|
|
431
|
+
const { copy, isCopying, isSuccess, error } = useLLMCopy({
|
|
432
|
+
content,
|
|
433
|
+
onSuccess: (action) => {
|
|
434
|
+
setShowToast(true);
|
|
435
|
+
setTimeout(() => setShowToast(false), toastDuration);
|
|
436
|
+
onCopy?.(action);
|
|
437
|
+
},
|
|
438
|
+
onError,
|
|
439
|
+
onAnalytics
|
|
440
|
+
});
|
|
441
|
+
React.useEffect(() => {
|
|
442
|
+
if (!keyboardShortcut) return;
|
|
443
|
+
const handleKeyDown = (e) => {
|
|
444
|
+
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
|
445
|
+
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
446
|
+
if (modKey && e.shiftKey && e.key.toLowerCase() === "c") {
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
copy();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
452
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
453
|
+
}, [keyboardShortcut, copy]);
|
|
454
|
+
const buttonClasses = [
|
|
455
|
+
"llm-ready-copy-btn",
|
|
456
|
+
`llm-ready-position-${position}`,
|
|
457
|
+
isCopying && "llm-ready-copying",
|
|
458
|
+
isSuccess && "llm-ready-success",
|
|
459
|
+
error && "llm-ready-error",
|
|
460
|
+
className
|
|
461
|
+
].filter(Boolean).join(" ");
|
|
462
|
+
return /* @__PURE__ */ jsxs("div", { className: "llm-ready-container", style, children: [
|
|
463
|
+
/* @__PURE__ */ jsxs(
|
|
464
|
+
"button",
|
|
465
|
+
{
|
|
466
|
+
type: "button",
|
|
467
|
+
className: buttonClasses,
|
|
468
|
+
onClick: copy,
|
|
469
|
+
disabled: disabled || isCopying,
|
|
470
|
+
"aria-label": text,
|
|
471
|
+
title: keyboardShortcut ? `${text} (${navigator.platform.includes("Mac") ? "\u2318" : "Ctrl"}+Shift+C)` : text,
|
|
472
|
+
children: [
|
|
473
|
+
/* @__PURE__ */ jsx(
|
|
474
|
+
"svg",
|
|
475
|
+
{
|
|
476
|
+
width: "16",
|
|
477
|
+
height: "16",
|
|
478
|
+
viewBox: "0 0 24 24",
|
|
479
|
+
fill: "none",
|
|
480
|
+
stroke: "currentColor",
|
|
481
|
+
strokeWidth: "2",
|
|
482
|
+
strokeLinecap: "round",
|
|
483
|
+
strokeLinejoin: "round",
|
|
484
|
+
className: "llm-ready-icon",
|
|
485
|
+
children: isSuccess ? /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
486
|
+
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
|
|
487
|
+
/* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
488
|
+
] })
|
|
489
|
+
}
|
|
490
|
+
),
|
|
491
|
+
/* @__PURE__ */ jsx("span", { className: "llm-ready-btn-text", children: isSuccess ? toastMessage : text })
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
),
|
|
495
|
+
showToast && /* @__PURE__ */ jsx("div", { className: "llm-ready-toast llm-ready-toast--visible", role: "status", "aria-live": "polite", children: toastMessage })
|
|
496
|
+
] });
|
|
497
|
+
}
|
|
498
|
+
var defaultMenuItems = [
|
|
499
|
+
{
|
|
500
|
+
id: "copy",
|
|
501
|
+
label: "Copy to clipboard",
|
|
502
|
+
action: "copy",
|
|
503
|
+
icon: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
504
|
+
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
|
|
505
|
+
/* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
506
|
+
] }),
|
|
507
|
+
shortcut: "\u2318+Shift+C"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: "view",
|
|
511
|
+
label: "View markdown",
|
|
512
|
+
action: "view",
|
|
513
|
+
icon: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
514
|
+
/* @__PURE__ */ jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
|
|
515
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
|
|
516
|
+
] })
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: "download",
|
|
520
|
+
label: "Download .md file",
|
|
521
|
+
action: "download",
|
|
522
|
+
icon: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
523
|
+
/* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
|
|
524
|
+
/* @__PURE__ */ jsx("polyline", { points: "7 10 12 15 17 10" }),
|
|
525
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
|
|
526
|
+
] })
|
|
527
|
+
}
|
|
528
|
+
];
|
|
529
|
+
function CopyDropdown({
|
|
530
|
+
content,
|
|
531
|
+
text = "Copy",
|
|
532
|
+
menuItems = defaultMenuItems,
|
|
533
|
+
position = "inline",
|
|
534
|
+
keyboardShortcut = true,
|
|
535
|
+
className = "",
|
|
536
|
+
toastMessage = "Copied!",
|
|
537
|
+
toastDuration = 2e3,
|
|
538
|
+
onCopy,
|
|
539
|
+
onError,
|
|
540
|
+
onAnalytics,
|
|
541
|
+
style,
|
|
542
|
+
disabled = false
|
|
543
|
+
}) {
|
|
544
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
545
|
+
const [showToast, setShowToast] = useState(false);
|
|
546
|
+
const [showModal, setShowModal] = useState(false);
|
|
547
|
+
const containerRef = useRef(null);
|
|
548
|
+
const dropdownRef = useRef(null);
|
|
549
|
+
const { copy, view, download, markdown, isCopying, isSuccess } = useLLMCopy({
|
|
550
|
+
content,
|
|
551
|
+
onSuccess: (action) => {
|
|
552
|
+
if (action === "copy") {
|
|
553
|
+
setShowToast(true);
|
|
554
|
+
setTimeout(() => setShowToast(false), toastDuration);
|
|
555
|
+
}
|
|
556
|
+
onCopy?.(action);
|
|
557
|
+
},
|
|
558
|
+
onError,
|
|
559
|
+
onAnalytics
|
|
560
|
+
});
|
|
561
|
+
useEffect(() => {
|
|
562
|
+
const handleClickOutside = (e) => {
|
|
563
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
564
|
+
setIsOpen(false);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
document.addEventListener("click", handleClickOutside);
|
|
568
|
+
return () => document.removeEventListener("click", handleClickOutside);
|
|
569
|
+
}, []);
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
if (!keyboardShortcut) return;
|
|
572
|
+
const handleKeyDown = (e) => {
|
|
573
|
+
if (e.key === "Escape" && isOpen) {
|
|
574
|
+
setIsOpen(false);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
|
578
|
+
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
579
|
+
if (modKey && e.shiftKey && e.key.toLowerCase() === "c") {
|
|
580
|
+
e.preventDefault();
|
|
581
|
+
copy();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
585
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
586
|
+
}, [keyboardShortcut, copy, isOpen]);
|
|
587
|
+
const handleItemClick = useCallback(
|
|
588
|
+
(item) => {
|
|
589
|
+
setIsOpen(false);
|
|
590
|
+
if (typeof item.action === "function") {
|
|
591
|
+
item.action();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
switch (item.action) {
|
|
595
|
+
case "copy":
|
|
596
|
+
copy();
|
|
597
|
+
break;
|
|
598
|
+
case "view":
|
|
599
|
+
view();
|
|
600
|
+
setShowModal(true);
|
|
601
|
+
break;
|
|
602
|
+
case "download":
|
|
603
|
+
download();
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
[copy, view, download]
|
|
608
|
+
);
|
|
609
|
+
const handleDropdownKeyDown = useCallback(
|
|
610
|
+
(e) => {
|
|
611
|
+
if (!isOpen) return;
|
|
612
|
+
const items = dropdownRef.current?.querySelectorAll("button");
|
|
613
|
+
if (!items) return;
|
|
614
|
+
const currentIndex = Array.from(items).indexOf(document.activeElement);
|
|
615
|
+
switch (e.key) {
|
|
616
|
+
case "ArrowDown":
|
|
617
|
+
e.preventDefault();
|
|
618
|
+
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
619
|
+
items[nextIndex]?.focus();
|
|
620
|
+
break;
|
|
621
|
+
case "ArrowUp":
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
624
|
+
items[prevIndex]?.focus();
|
|
625
|
+
break;
|
|
626
|
+
case "Escape":
|
|
627
|
+
setIsOpen(false);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
[isOpen]
|
|
632
|
+
);
|
|
633
|
+
const containerClasses = [
|
|
634
|
+
"llm-ready-container",
|
|
635
|
+
`llm-ready-position-${position}`,
|
|
636
|
+
className
|
|
637
|
+
].filter(Boolean).join(" ");
|
|
638
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
639
|
+
/* @__PURE__ */ jsxs("div", { ref: containerRef, className: containerClasses, style, children: [
|
|
640
|
+
/* @__PURE__ */ jsxs("div", { className: "llm-ready-btn-group", children: [
|
|
641
|
+
/* @__PURE__ */ jsxs(
|
|
642
|
+
"button",
|
|
643
|
+
{
|
|
644
|
+
type: "button",
|
|
645
|
+
className: `llm-ready-copy-btn ${isCopying ? "llm-ready-copying" : ""} ${isSuccess ? "llm-ready-success" : ""}`,
|
|
646
|
+
onClick: copy,
|
|
647
|
+
disabled: disabled || isCopying,
|
|
648
|
+
"aria-label": text,
|
|
649
|
+
children: [
|
|
650
|
+
/* @__PURE__ */ jsx(
|
|
651
|
+
"svg",
|
|
652
|
+
{
|
|
653
|
+
width: "16",
|
|
654
|
+
height: "16",
|
|
655
|
+
viewBox: "0 0 24 24",
|
|
656
|
+
fill: "none",
|
|
657
|
+
stroke: "currentColor",
|
|
658
|
+
strokeWidth: "2",
|
|
659
|
+
className: "llm-ready-icon",
|
|
660
|
+
children: isSuccess ? /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
661
|
+
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
|
|
662
|
+
/* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
663
|
+
] })
|
|
664
|
+
}
|
|
665
|
+
),
|
|
666
|
+
/* @__PURE__ */ jsx("span", { className: "llm-ready-btn-text", children: isSuccess ? toastMessage : text })
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
/* @__PURE__ */ jsx(
|
|
671
|
+
"button",
|
|
672
|
+
{
|
|
673
|
+
type: "button",
|
|
674
|
+
className: "llm-ready-dropdown-btn",
|
|
675
|
+
onClick: () => setIsOpen(!isOpen),
|
|
676
|
+
"aria-expanded": isOpen,
|
|
677
|
+
"aria-haspopup": "menu",
|
|
678
|
+
"aria-label": "More options",
|
|
679
|
+
disabled,
|
|
680
|
+
children: /* @__PURE__ */ jsx(
|
|
681
|
+
"svg",
|
|
682
|
+
{
|
|
683
|
+
width: "16",
|
|
684
|
+
height: "16",
|
|
685
|
+
viewBox: "0 0 24 24",
|
|
686
|
+
fill: "none",
|
|
687
|
+
stroke: "currentColor",
|
|
688
|
+
strokeWidth: "2",
|
|
689
|
+
children: /* @__PURE__ */ jsx("polyline", { points: "6 9 12 15 18 9" })
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
] }),
|
|
695
|
+
/* @__PURE__ */ jsx(
|
|
696
|
+
"div",
|
|
697
|
+
{
|
|
698
|
+
ref: dropdownRef,
|
|
699
|
+
className: `llm-ready-dropdown ${isOpen ? "llm-ready-dropdown--open" : ""}`,
|
|
700
|
+
role: "menu",
|
|
701
|
+
"aria-hidden": !isOpen,
|
|
702
|
+
onKeyDown: handleDropdownKeyDown,
|
|
703
|
+
children: menuItems.map((item) => /* @__PURE__ */ jsxs(
|
|
704
|
+
"button",
|
|
705
|
+
{
|
|
706
|
+
type: "button",
|
|
707
|
+
className: "llm-ready-dropdown-item",
|
|
708
|
+
role: "menuitem",
|
|
709
|
+
onClick: () => handleItemClick(item),
|
|
710
|
+
tabIndex: isOpen ? 0 : -1,
|
|
711
|
+
children: [
|
|
712
|
+
item.icon && /* @__PURE__ */ jsx("span", { className: "llm-ready-dropdown-icon", children: item.icon }),
|
|
713
|
+
/* @__PURE__ */ jsx("span", { className: "llm-ready-dropdown-label", children: item.label }),
|
|
714
|
+
item.shortcut && /* @__PURE__ */ jsx("span", { className: "llm-ready-dropdown-shortcut", children: item.shortcut })
|
|
715
|
+
]
|
|
716
|
+
},
|
|
717
|
+
item.id
|
|
718
|
+
))
|
|
719
|
+
}
|
|
720
|
+
),
|
|
721
|
+
showToast && /* @__PURE__ */ jsx("div", { className: "llm-ready-toast llm-ready-toast--visible", role: "status", children: toastMessage })
|
|
722
|
+
] }),
|
|
723
|
+
showModal && /* @__PURE__ */ jsx(
|
|
724
|
+
MarkdownModal,
|
|
725
|
+
{
|
|
726
|
+
content: markdown,
|
|
727
|
+
onClose: () => setShowModal(false),
|
|
728
|
+
onCopy: copy,
|
|
729
|
+
onDownload: download
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
] });
|
|
733
|
+
}
|
|
734
|
+
function MarkdownModal({
|
|
735
|
+
content,
|
|
736
|
+
onClose,
|
|
737
|
+
onCopy,
|
|
738
|
+
onDownload
|
|
739
|
+
}) {
|
|
740
|
+
const modalRef = useRef(null);
|
|
741
|
+
useEffect(() => {
|
|
742
|
+
const handleKeyDown = (e) => {
|
|
743
|
+
if (e.key === "Escape") onClose();
|
|
744
|
+
};
|
|
745
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
746
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
747
|
+
}, [onClose]);
|
|
748
|
+
useEffect(() => {
|
|
749
|
+
const modal = modalRef.current;
|
|
750
|
+
if (!modal) return;
|
|
751
|
+
const focusable = modal.querySelectorAll(
|
|
752
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
753
|
+
);
|
|
754
|
+
const first = focusable[0];
|
|
755
|
+
const last = focusable[focusable.length - 1];
|
|
756
|
+
first?.focus();
|
|
757
|
+
const handleTab = (e) => {
|
|
758
|
+
if (e.key !== "Tab") return;
|
|
759
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
760
|
+
e.preventDefault();
|
|
761
|
+
last?.focus();
|
|
762
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
763
|
+
e.preventDefault();
|
|
764
|
+
first?.focus();
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
modal.addEventListener("keydown", handleTab);
|
|
768
|
+
return () => modal.removeEventListener("keydown", handleTab);
|
|
769
|
+
}, []);
|
|
770
|
+
return /* @__PURE__ */ jsxs("div", { className: "llm-ready-modal", role: "dialog", "aria-modal": "true", "aria-label": "Markdown content", children: [
|
|
771
|
+
/* @__PURE__ */ jsx("div", { className: "llm-ready-modal-backdrop", onClick: onClose }),
|
|
772
|
+
/* @__PURE__ */ jsxs("div", { ref: modalRef, className: "llm-ready-modal-content", children: [
|
|
773
|
+
/* @__PURE__ */ jsxs("div", { className: "llm-ready-modal-header", children: [
|
|
774
|
+
/* @__PURE__ */ jsx("h2", { className: "llm-ready-modal-title", children: "Markdown Content" }),
|
|
775
|
+
/* @__PURE__ */ jsx(
|
|
776
|
+
"button",
|
|
777
|
+
{
|
|
778
|
+
type: "button",
|
|
779
|
+
className: "llm-ready-modal-close",
|
|
780
|
+
onClick: onClose,
|
|
781
|
+
"aria-label": "Close modal",
|
|
782
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
783
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
784
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
785
|
+
] })
|
|
786
|
+
}
|
|
787
|
+
)
|
|
788
|
+
] }),
|
|
789
|
+
/* @__PURE__ */ jsx("div", { className: "llm-ready-modal-body", children: /* @__PURE__ */ jsx("pre", { className: "llm-ready-markdown-content", children: content }) }),
|
|
790
|
+
/* @__PURE__ */ jsxs("div", { className: "llm-ready-modal-footer", children: [
|
|
791
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "llm-ready-btn-secondary", onClick: onClose, children: "Close" }),
|
|
792
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "llm-ready-btn-secondary", onClick: onDownload, children: "Download" }),
|
|
793
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "llm-ready-btn-primary", onClick: onCopy, children: "Copy" })
|
|
794
|
+
] })
|
|
795
|
+
] })
|
|
796
|
+
] });
|
|
797
|
+
}
|
|
798
|
+
function useTOC(options) {
|
|
799
|
+
const {
|
|
800
|
+
contentRef,
|
|
801
|
+
levels = ["h2", "h3", "h4"],
|
|
802
|
+
rootMargin = "-100px 0px -80% 0px",
|
|
803
|
+
threshold = 0
|
|
804
|
+
} = options;
|
|
805
|
+
const [headings, setHeadings] = useState([]);
|
|
806
|
+
const [activeId, setActiveId] = useState(null);
|
|
807
|
+
useEffect(() => {
|
|
808
|
+
if (!contentRef?.current) return;
|
|
809
|
+
const container = contentRef.current;
|
|
810
|
+
const selector = levels.join(", ");
|
|
811
|
+
const elements = container.querySelectorAll(selector);
|
|
812
|
+
const extracted = [];
|
|
813
|
+
let index = 0;
|
|
814
|
+
elements.forEach((el) => {
|
|
815
|
+
const tagName = el.tagName.toLowerCase();
|
|
816
|
+
const level = parseInt(tagName.charAt(1), 10);
|
|
817
|
+
const text = el.textContent?.trim() || "";
|
|
818
|
+
let id = el.id;
|
|
819
|
+
if (!id) {
|
|
820
|
+
id = generateHeadingId(text, index);
|
|
821
|
+
el.id = id;
|
|
822
|
+
}
|
|
823
|
+
extracted.push({ id, text, level });
|
|
824
|
+
index++;
|
|
825
|
+
});
|
|
826
|
+
setHeadings(extracted);
|
|
827
|
+
}, [contentRef, levels]);
|
|
828
|
+
useEffect(() => {
|
|
829
|
+
if (!contentRef?.current || headings.length === 0) return;
|
|
830
|
+
const container = contentRef.current;
|
|
831
|
+
const observerOptions = {
|
|
832
|
+
root: null,
|
|
833
|
+
rootMargin,
|
|
834
|
+
threshold
|
|
835
|
+
};
|
|
836
|
+
const observer = new IntersectionObserver((entries) => {
|
|
837
|
+
entries.forEach((entry) => {
|
|
838
|
+
if (entry.isIntersecting) {
|
|
839
|
+
setActiveId(entry.target.id);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
}, observerOptions);
|
|
843
|
+
headings.forEach((heading) => {
|
|
844
|
+
const element = container.querySelector(`#${CSS.escape(heading.id)}`);
|
|
845
|
+
if (element) {
|
|
846
|
+
observer.observe(element);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
return () => observer.disconnect();
|
|
850
|
+
}, [contentRef, headings, rootMargin, threshold]);
|
|
851
|
+
const scrollTo = useCallback((id) => {
|
|
852
|
+
const element = document.getElementById(id);
|
|
853
|
+
if (element) {
|
|
854
|
+
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
855
|
+
setActiveId(id);
|
|
856
|
+
if (typeof window !== "undefined") {
|
|
857
|
+
window.history.pushState(null, "", `#${id}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}, []);
|
|
861
|
+
return { headings, activeId, scrollTo };
|
|
862
|
+
}
|
|
863
|
+
function generateHeadingId(text, index) {
|
|
864
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").substring(0, 50);
|
|
865
|
+
return slug || `heading-${index}`;
|
|
866
|
+
}
|
|
867
|
+
function buildNestedTOC(headings) {
|
|
868
|
+
if (headings.length === 0) return [];
|
|
869
|
+
const result = [];
|
|
870
|
+
const stack = [];
|
|
871
|
+
for (const heading of headings) {
|
|
872
|
+
const item = { ...heading, children: [] };
|
|
873
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
|
|
874
|
+
stack.pop();
|
|
875
|
+
}
|
|
876
|
+
if (stack.length === 0) {
|
|
877
|
+
result.push(item);
|
|
878
|
+
} else {
|
|
879
|
+
const parent = stack[stack.length - 1].heading;
|
|
880
|
+
if (!parent.children) parent.children = [];
|
|
881
|
+
parent.children.push(item);
|
|
882
|
+
}
|
|
883
|
+
stack.push({ heading: item, level: heading.level });
|
|
884
|
+
}
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
function useTOCKeyboard(headings, activeId, scrollTo) {
|
|
888
|
+
useEffect(() => {
|
|
889
|
+
const handleKeyDown = (e) => {
|
|
890
|
+
if (headings.length === 0) return;
|
|
891
|
+
const currentIndex = headings.findIndex((h) => h.id === activeId);
|
|
892
|
+
if (e.key === "ArrowDown" && e.altKey) {
|
|
893
|
+
e.preventDefault();
|
|
894
|
+
const nextIndex = Math.min(currentIndex + 1, headings.length - 1);
|
|
895
|
+
scrollTo(headings[nextIndex].id);
|
|
896
|
+
} else if (e.key === "ArrowUp" && e.altKey) {
|
|
897
|
+
e.preventDefault();
|
|
898
|
+
const prevIndex = Math.max(currentIndex - 1, 0);
|
|
899
|
+
scrollTo(headings[prevIndex].id);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
903
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
904
|
+
}, [headings, activeId, scrollTo]);
|
|
905
|
+
}
|
|
906
|
+
function TOC({
|
|
907
|
+
content,
|
|
908
|
+
contentRef,
|
|
909
|
+
headings: providedHeadings,
|
|
910
|
+
position = "right",
|
|
911
|
+
levels = ["h2", "h3", "h4"],
|
|
912
|
+
title = "On This Page",
|
|
913
|
+
sticky = true,
|
|
914
|
+
stickyOffset = 80,
|
|
915
|
+
className = "",
|
|
916
|
+
smoothScroll = true,
|
|
917
|
+
highlightActive = true,
|
|
918
|
+
collapsible = true,
|
|
919
|
+
defaultCollapsed = false,
|
|
920
|
+
style
|
|
921
|
+
}) {
|
|
922
|
+
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
|
923
|
+
const { headings: dynamicHeadings, activeId, scrollTo } = useTOC({
|
|
924
|
+
contentRef: contentRef || { current: null },
|
|
925
|
+
levels
|
|
926
|
+
});
|
|
927
|
+
const headings = providedHeadings || dynamicHeadings;
|
|
928
|
+
useTOCKeyboard(headings, activeId, scrollTo);
|
|
929
|
+
const handleClick = useCallback(
|
|
930
|
+
(e, id) => {
|
|
931
|
+
e.preventDefault();
|
|
932
|
+
if (smoothScroll) {
|
|
933
|
+
scrollTo(id);
|
|
934
|
+
} else {
|
|
935
|
+
const element = document.getElementById(id);
|
|
936
|
+
if (element) {
|
|
937
|
+
element.scrollIntoView();
|
|
938
|
+
window.history.pushState(null, "", `#${id}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
[scrollTo, smoothScroll]
|
|
943
|
+
);
|
|
944
|
+
if (headings.length === 0) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
const tocClasses = [
|
|
948
|
+
"llm-ready-toc",
|
|
949
|
+
`llm-ready-toc-${position}`,
|
|
950
|
+
sticky && "llm-ready-toc-sticky",
|
|
951
|
+
isCollapsed && "llm-ready-toc-collapsed",
|
|
952
|
+
className
|
|
953
|
+
].filter(Boolean).join(" ");
|
|
954
|
+
const tocStyle = {
|
|
955
|
+
...style,
|
|
956
|
+
...sticky && { top: stickyOffset }
|
|
957
|
+
};
|
|
958
|
+
return /* @__PURE__ */ jsxs("nav", { className: tocClasses, style: tocStyle, "aria-label": "Table of contents", children: [
|
|
959
|
+
/* @__PURE__ */ jsxs("div", { className: "llm-ready-toc-header", children: [
|
|
960
|
+
/* @__PURE__ */ jsx("h3", { className: "llm-ready-toc-title", children: title }),
|
|
961
|
+
collapsible && /* @__PURE__ */ jsx(
|
|
962
|
+
"button",
|
|
963
|
+
{
|
|
964
|
+
type: "button",
|
|
965
|
+
className: "llm-ready-toc-toggle",
|
|
966
|
+
onClick: () => setIsCollapsed(!isCollapsed),
|
|
967
|
+
"aria-expanded": !isCollapsed,
|
|
968
|
+
"aria-label": isCollapsed ? "Expand table of contents" : "Collapse table of contents",
|
|
969
|
+
children: /* @__PURE__ */ jsx(
|
|
970
|
+
"svg",
|
|
971
|
+
{
|
|
972
|
+
width: "16",
|
|
973
|
+
height: "16",
|
|
974
|
+
viewBox: "0 0 24 24",
|
|
975
|
+
fill: "none",
|
|
976
|
+
stroke: "currentColor",
|
|
977
|
+
strokeWidth: "2",
|
|
978
|
+
style: { transform: isCollapsed ? "rotate(180deg)" : "none" },
|
|
979
|
+
children: /* @__PURE__ */ jsx("polyline", { points: "6 9 12 15 18 9" })
|
|
980
|
+
}
|
|
981
|
+
)
|
|
982
|
+
}
|
|
983
|
+
)
|
|
984
|
+
] }),
|
|
985
|
+
!isCollapsed && /* @__PURE__ */ jsx("ul", { className: "llm-ready-toc-list", children: headings.map((heading) => /* @__PURE__ */ jsx(
|
|
986
|
+
TOCItem,
|
|
987
|
+
{
|
|
988
|
+
heading,
|
|
989
|
+
activeId,
|
|
990
|
+
highlightActive,
|
|
991
|
+
onClick: handleClick
|
|
992
|
+
},
|
|
993
|
+
heading.id
|
|
994
|
+
)) })
|
|
995
|
+
] });
|
|
996
|
+
}
|
|
997
|
+
function TOCItem({
|
|
998
|
+
heading,
|
|
999
|
+
activeId,
|
|
1000
|
+
highlightActive,
|
|
1001
|
+
onClick
|
|
1002
|
+
}) {
|
|
1003
|
+
const isActive = highlightActive && heading.id === activeId;
|
|
1004
|
+
const itemClasses = [
|
|
1005
|
+
"llm-ready-toc-item",
|
|
1006
|
+
`llm-ready-toc-level-${heading.level}`,
|
|
1007
|
+
isActive && "llm-ready-toc-active"
|
|
1008
|
+
].filter(Boolean).join(" ");
|
|
1009
|
+
return /* @__PURE__ */ jsxs("li", { className: itemClasses, children: [
|
|
1010
|
+
/* @__PURE__ */ jsx(
|
|
1011
|
+
"a",
|
|
1012
|
+
{
|
|
1013
|
+
href: `#${heading.id}`,
|
|
1014
|
+
className: "llm-ready-toc-link",
|
|
1015
|
+
onClick: (e) => onClick(e, heading.id),
|
|
1016
|
+
"aria-current": isActive ? "location" : void 0,
|
|
1017
|
+
children: heading.text
|
|
1018
|
+
}
|
|
1019
|
+
),
|
|
1020
|
+
heading.children && heading.children.length > 0 && /* @__PURE__ */ jsx("ul", { className: "llm-ready-toc-nested", children: heading.children.map((child) => /* @__PURE__ */ jsx(
|
|
1021
|
+
TOCItem,
|
|
1022
|
+
{
|
|
1023
|
+
heading: child,
|
|
1024
|
+
activeId,
|
|
1025
|
+
highlightActive,
|
|
1026
|
+
onClick
|
|
1027
|
+
},
|
|
1028
|
+
child.id
|
|
1029
|
+
)) })
|
|
1030
|
+
] });
|
|
1031
|
+
}
|
|
1032
|
+
function TOCMobile(props) {
|
|
1033
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1034
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1035
|
+
/* @__PURE__ */ jsxs(
|
|
1036
|
+
"button",
|
|
1037
|
+
{
|
|
1038
|
+
type: "button",
|
|
1039
|
+
className: "llm-ready-toc-mobile-toggle",
|
|
1040
|
+
onClick: () => setIsOpen(true),
|
|
1041
|
+
"aria-label": "Open table of contents",
|
|
1042
|
+
children: [
|
|
1043
|
+
/* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
1044
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" }),
|
|
1045
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6" }),
|
|
1046
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18" })
|
|
1047
|
+
] }),
|
|
1048
|
+
/* @__PURE__ */ jsx("span", { children: "Contents" })
|
|
1049
|
+
]
|
|
1050
|
+
}
|
|
1051
|
+
),
|
|
1052
|
+
isOpen && /* @__PURE__ */ jsxs("div", { className: "llm-ready-toc-mobile-panel", children: [
|
|
1053
|
+
/* @__PURE__ */ jsx("div", { className: "llm-ready-toc-mobile-backdrop", onClick: () => setIsOpen(false) }),
|
|
1054
|
+
/* @__PURE__ */ jsxs("div", { className: "llm-ready-toc-mobile-content", children: [
|
|
1055
|
+
/* @__PURE__ */ jsxs("div", { className: "llm-ready-toc-mobile-header", children: [
|
|
1056
|
+
/* @__PURE__ */ jsx("h3", { children: "Contents" }),
|
|
1057
|
+
/* @__PURE__ */ jsx(
|
|
1058
|
+
"button",
|
|
1059
|
+
{
|
|
1060
|
+
type: "button",
|
|
1061
|
+
className: "llm-ready-toc-mobile-close",
|
|
1062
|
+
onClick: () => setIsOpen(false),
|
|
1063
|
+
"aria-label": "Close",
|
|
1064
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
1065
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
1066
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
1067
|
+
] })
|
|
1068
|
+
}
|
|
1069
|
+
)
|
|
1070
|
+
] }),
|
|
1071
|
+
/* @__PURE__ */ jsx(TOC, { ...props, sticky: false, collapsible: false })
|
|
1072
|
+
] })
|
|
1073
|
+
] })
|
|
1074
|
+
] });
|
|
1075
|
+
}
|
|
1076
|
+
function LLMBadge({
|
|
1077
|
+
text = "AI Ready",
|
|
1078
|
+
showTooltip = true,
|
|
1079
|
+
tooltipContent = "This content is optimized for AI assistants like ChatGPT and Claude",
|
|
1080
|
+
size = "md",
|
|
1081
|
+
className = ""
|
|
1082
|
+
}) {
|
|
1083
|
+
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
|
|
1084
|
+
const badgeClasses = [
|
|
1085
|
+
"llm-ready-badge",
|
|
1086
|
+
`llm-ready-badge-${size}`,
|
|
1087
|
+
className
|
|
1088
|
+
].filter(Boolean).join(" ");
|
|
1089
|
+
return /* @__PURE__ */ jsxs(
|
|
1090
|
+
"div",
|
|
1091
|
+
{
|
|
1092
|
+
className: badgeClasses,
|
|
1093
|
+
onMouseEnter: () => showTooltip && setIsTooltipVisible(true),
|
|
1094
|
+
onMouseLeave: () => setIsTooltipVisible(false),
|
|
1095
|
+
onFocus: () => showTooltip && setIsTooltipVisible(true),
|
|
1096
|
+
onBlur: () => setIsTooltipVisible(false),
|
|
1097
|
+
tabIndex: showTooltip ? 0 : -1,
|
|
1098
|
+
role: showTooltip ? "tooltip" : void 0,
|
|
1099
|
+
"aria-describedby": showTooltip ? "llm-badge-tooltip" : void 0,
|
|
1100
|
+
children: [
|
|
1101
|
+
/* @__PURE__ */ jsxs(
|
|
1102
|
+
"svg",
|
|
1103
|
+
{
|
|
1104
|
+
width: size === "sm" ? 12 : size === "lg" ? 18 : 14,
|
|
1105
|
+
height: size === "sm" ? 12 : size === "lg" ? 18 : 14,
|
|
1106
|
+
viewBox: "0 0 24 24",
|
|
1107
|
+
fill: "none",
|
|
1108
|
+
stroke: "currentColor",
|
|
1109
|
+
strokeWidth: "2",
|
|
1110
|
+
className: "llm-ready-badge-icon",
|
|
1111
|
+
children: [
|
|
1112
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
|
|
1113
|
+
/* @__PURE__ */ jsx("path", { d: "M12 2v4m0 12v4M2 12h4m12 0h4" }),
|
|
1114
|
+
/* @__PURE__ */ jsx("path", { d: "m4.93 4.93 2.83 2.83m8.48 8.48 2.83 2.83m0-14.14-2.83 2.83m-8.48 8.48-2.83 2.83" })
|
|
1115
|
+
]
|
|
1116
|
+
}
|
|
1117
|
+
),
|
|
1118
|
+
/* @__PURE__ */ jsx("span", { className: "llm-ready-badge-text", children: text }),
|
|
1119
|
+
showTooltip && isTooltipVisible && /* @__PURE__ */ jsx("div", { id: "llm-badge-tooltip", className: "llm-ready-badge-tooltip", role: "tooltip", children: tooltipContent })
|
|
1120
|
+
]
|
|
1121
|
+
}
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
function useAnalytics(options = {}) {
|
|
1125
|
+
const {
|
|
1126
|
+
endpoint = "/api/llm-analytics",
|
|
1127
|
+
includeUrl = true,
|
|
1128
|
+
includeTimestamp = true,
|
|
1129
|
+
metadata = {}
|
|
1130
|
+
} = options;
|
|
1131
|
+
const pendingRef = useRef(/* @__PURE__ */ new Set());
|
|
1132
|
+
const track = useCallback(
|
|
1133
|
+
async (event) => {
|
|
1134
|
+
const key = `${event.action}-${event.contentId || ""}-${Date.now()}`;
|
|
1135
|
+
if (pendingRef.current.has(key)) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
pendingRef.current.add(key);
|
|
1139
|
+
try {
|
|
1140
|
+
const payload = {
|
|
1141
|
+
...event,
|
|
1142
|
+
...includeUrl && { url: event.url || window.location.href },
|
|
1143
|
+
...includeTimestamp && { timestamp: event.timestamp || (/* @__PURE__ */ new Date()).toISOString() },
|
|
1144
|
+
metadata: { ...metadata, ...event.metadata }
|
|
1145
|
+
};
|
|
1146
|
+
if (navigator.sendBeacon) {
|
|
1147
|
+
const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
|
|
1148
|
+
navigator.sendBeacon(endpoint, blob);
|
|
1149
|
+
} else {
|
|
1150
|
+
await fetch(endpoint, {
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
headers: { "Content-Type": "application/json" },
|
|
1153
|
+
body: JSON.stringify(payload),
|
|
1154
|
+
keepalive: true
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
console.error("Analytics tracking failed:", error);
|
|
1159
|
+
} finally {
|
|
1160
|
+
pendingRef.current.delete(key);
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
[endpoint, includeUrl, includeTimestamp, metadata]
|
|
1164
|
+
);
|
|
1165
|
+
const trackCopy = useCallback(() => {
|
|
1166
|
+
return track({ action: "copy" });
|
|
1167
|
+
}, [track]);
|
|
1168
|
+
const trackView = useCallback(() => {
|
|
1169
|
+
return track({ action: "view" });
|
|
1170
|
+
}, [track]);
|
|
1171
|
+
const trackDownload = useCallback(() => {
|
|
1172
|
+
return track({ action: "download" });
|
|
1173
|
+
}, [track]);
|
|
1174
|
+
return { track, trackCopy, trackView, trackDownload };
|
|
1175
|
+
}
|
|
1176
|
+
function createAnalyticsTracker(endpoint) {
|
|
1177
|
+
return async function trackEvent(action, contentId, metadata) {
|
|
1178
|
+
const payload = {
|
|
1179
|
+
action,
|
|
1180
|
+
contentId,
|
|
1181
|
+
url: typeof window !== "undefined" ? window.location.href : void 0,
|
|
1182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1183
|
+
metadata
|
|
1184
|
+
};
|
|
1185
|
+
try {
|
|
1186
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
1187
|
+
const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
|
|
1188
|
+
navigator.sendBeacon(endpoint, blob);
|
|
1189
|
+
} else if (typeof fetch !== "undefined") {
|
|
1190
|
+
await fetch(endpoint, {
|
|
1191
|
+
method: "POST",
|
|
1192
|
+
headers: { "Content-Type": "application/json" },
|
|
1193
|
+
body: JSON.stringify(payload),
|
|
1194
|
+
keepalive: true
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.error("Analytics tracking failed:", error);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/server/generate-markdown.ts
|
|
1204
|
+
function generateMarkdown(content) {
|
|
1205
|
+
const parts = [];
|
|
1206
|
+
if (content.promptPrefix?.trim()) {
|
|
1207
|
+
parts.push(content.promptPrefix.trim());
|
|
1208
|
+
parts.push("");
|
|
1209
|
+
}
|
|
1210
|
+
parts.push(`# ${content.title}`);
|
|
1211
|
+
parts.push("");
|
|
1212
|
+
if (content.excerpt) {
|
|
1213
|
+
parts.push(`> ${content.excerpt}`);
|
|
1214
|
+
parts.push("");
|
|
1215
|
+
}
|
|
1216
|
+
parts.push("---");
|
|
1217
|
+
parts.push(`- **Source**: ${content.url}`);
|
|
1218
|
+
if (content.date) {
|
|
1219
|
+
parts.push(`- **Date**: ${content.date}`);
|
|
1220
|
+
}
|
|
1221
|
+
if (content.modifiedDate) {
|
|
1222
|
+
parts.push(`- **Modified**: ${content.modifiedDate}`);
|
|
1223
|
+
}
|
|
1224
|
+
if (content.author) {
|
|
1225
|
+
parts.push(`- **Author**: ${content.author}`);
|
|
1226
|
+
}
|
|
1227
|
+
if (content.categories?.length) {
|
|
1228
|
+
parts.push(`- **Categories**: ${content.categories.join(", ")}`);
|
|
1229
|
+
}
|
|
1230
|
+
if (content.tags?.length) {
|
|
1231
|
+
parts.push(`- **Tags**: ${content.tags.join(", ")}`);
|
|
1232
|
+
}
|
|
1233
|
+
if (content.readingTime) {
|
|
1234
|
+
parts.push(`- **Reading Time**: ${content.readingTime} min`);
|
|
1235
|
+
}
|
|
1236
|
+
parts.push("---");
|
|
1237
|
+
parts.push("");
|
|
1238
|
+
if (content.content) {
|
|
1239
|
+
const contentMarkdown = isHTML(content.content) ? htmlToMarkdown(content.content) : content.content;
|
|
1240
|
+
parts.push(contentMarkdown);
|
|
1241
|
+
}
|
|
1242
|
+
const markdown = parts.join("\n").trim();
|
|
1243
|
+
const wordCount = countWords(markdown);
|
|
1244
|
+
const readingTime = calculateReadingTime(markdown);
|
|
1245
|
+
const headings = extractMarkdownHeadings(markdown);
|
|
1246
|
+
return {
|
|
1247
|
+
markdown,
|
|
1248
|
+
wordCount,
|
|
1249
|
+
readingTime,
|
|
1250
|
+
headings
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function generateMarkdownString(content) {
|
|
1254
|
+
return generateMarkdown(content).markdown;
|
|
1255
|
+
}
|
|
1256
|
+
function isHTML(str) {
|
|
1257
|
+
return /<[a-z][\s\S]*>/i.test(str);
|
|
1258
|
+
}
|
|
1259
|
+
function extractMarkdownHeadings(markdown) {
|
|
1260
|
+
const headings = [];
|
|
1261
|
+
const lines = markdown.split("\n");
|
|
1262
|
+
let index = 0;
|
|
1263
|
+
for (const line of lines) {
|
|
1264
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1265
|
+
if (match) {
|
|
1266
|
+
const level = match[1].length;
|
|
1267
|
+
const text = match[2].trim();
|
|
1268
|
+
const id = generateSlug(text, index);
|
|
1269
|
+
headings.push({ id, text, level });
|
|
1270
|
+
index++;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return headings;
|
|
1274
|
+
}
|
|
1275
|
+
function generateSlug(text, index) {
|
|
1276
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
1277
|
+
return slug || `heading-${index}`;
|
|
1278
|
+
}
|
|
1279
|
+
function contentToPlainText(content) {
|
|
1280
|
+
const markdown = generateMarkdownString(content);
|
|
1281
|
+
return markdown.replace(/^#+\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/^[-*+]\s+/gm, "").replace(/^>\s+/gm, "").replace(/^---$/gm, "").replace(/\n{2,}/g, "\n\n").trim();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/server/generate-llms-txt.ts
|
|
1285
|
+
function generateLLMsTxt(config) {
|
|
1286
|
+
const parts = [];
|
|
1287
|
+
parts.push(`# ${config.siteName}`);
|
|
1288
|
+
parts.push("");
|
|
1289
|
+
if (config.siteDescription) {
|
|
1290
|
+
parts.push(`> ${config.siteDescription}`);
|
|
1291
|
+
parts.push("");
|
|
1292
|
+
}
|
|
1293
|
+
if (config.headerText) {
|
|
1294
|
+
parts.push(config.headerText);
|
|
1295
|
+
} else {
|
|
1296
|
+
parts.push("This file provides LLM-friendly access to the content on this website.");
|
|
1297
|
+
}
|
|
1298
|
+
parts.push("");
|
|
1299
|
+
parts.push("---");
|
|
1300
|
+
parts.push(`- **Site URL**: ${config.siteUrl}`);
|
|
1301
|
+
parts.push("- **Format**: Append `?llm=1` to any page URL to get markdown");
|
|
1302
|
+
parts.push("---");
|
|
1303
|
+
parts.push("");
|
|
1304
|
+
if (config.content.length > 0) {
|
|
1305
|
+
parts.push("## Available Content");
|
|
1306
|
+
parts.push("");
|
|
1307
|
+
for (const item of config.content) {
|
|
1308
|
+
const llmUrl = appendQueryParam(item.url, "llm", "1");
|
|
1309
|
+
parts.push(`### [${item.title}](${llmUrl})`);
|
|
1310
|
+
if (item.type) {
|
|
1311
|
+
parts.push(`- **Type**: ${item.type}`);
|
|
1312
|
+
}
|
|
1313
|
+
if (item.date) {
|
|
1314
|
+
parts.push(`- **Date**: ${item.date}`);
|
|
1315
|
+
}
|
|
1316
|
+
if (item.description) {
|
|
1317
|
+
parts.push(`- **Description**: ${item.description}`);
|
|
1318
|
+
}
|
|
1319
|
+
parts.push(`- **Markdown URL**: ${llmUrl}`);
|
|
1320
|
+
parts.push("");
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
parts.push("---");
|
|
1324
|
+
parts.push("");
|
|
1325
|
+
if (config.footerText) {
|
|
1326
|
+
parts.push(config.footerText);
|
|
1327
|
+
} else {
|
|
1328
|
+
parts.push("*Generated by [next-llm-ready](https://seoengine.ai) - Make your content AI-ready*");
|
|
1329
|
+
}
|
|
1330
|
+
return parts.join("\n");
|
|
1331
|
+
}
|
|
1332
|
+
function appendQueryParam(url, key, value) {
|
|
1333
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
1334
|
+
return `${url}${separator}${key}=${value}`;
|
|
1335
|
+
}
|
|
1336
|
+
function createLLMsTxtHandler(options) {
|
|
1337
|
+
return async function handler(request) {
|
|
1338
|
+
try {
|
|
1339
|
+
const siteConfig = await options.getSiteConfig();
|
|
1340
|
+
const content = await options.getContent();
|
|
1341
|
+
const config = {
|
|
1342
|
+
siteName: siteConfig.siteName,
|
|
1343
|
+
siteDescription: siteConfig.siteDescription,
|
|
1344
|
+
siteUrl: siteConfig.siteUrl,
|
|
1345
|
+
content,
|
|
1346
|
+
headerText: options.headerText,
|
|
1347
|
+
footerText: options.footerText
|
|
1348
|
+
};
|
|
1349
|
+
const llmsTxt = generateLLMsTxt(config);
|
|
1350
|
+
return new NextResponse(llmsTxt, {
|
|
1351
|
+
status: 200,
|
|
1352
|
+
headers: {
|
|
1353
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1354
|
+
"Cache-Control": options.cacheControl || "public, max-age=3600",
|
|
1355
|
+
"X-Robots-Tag": "noindex"
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
console.error("Error generating llms.txt:", error);
|
|
1360
|
+
return new NextResponse("Error generating llms.txt", { status: 500 });
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function createLLMsTxtPageHandler(options) {
|
|
1365
|
+
return async function handler(req, res) {
|
|
1366
|
+
if (req.method !== "GET") {
|
|
1367
|
+
res.status(405).end("Method Not Allowed");
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
try {
|
|
1371
|
+
const siteConfig = await options.getSiteConfig();
|
|
1372
|
+
const content = await options.getContent();
|
|
1373
|
+
const config = {
|
|
1374
|
+
siteName: siteConfig.siteName,
|
|
1375
|
+
siteDescription: siteConfig.siteDescription,
|
|
1376
|
+
siteUrl: siteConfig.siteUrl,
|
|
1377
|
+
content,
|
|
1378
|
+
headerText: options.headerText,
|
|
1379
|
+
footerText: options.footerText
|
|
1380
|
+
};
|
|
1381
|
+
const llmsTxt = generateLLMsTxt(config);
|
|
1382
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
1383
|
+
res.setHeader("Cache-Control", options.cacheControl || "public, max-age=3600");
|
|
1384
|
+
res.setHeader("X-Robots-Tag", "noindex");
|
|
1385
|
+
res.status(200).end(llmsTxt);
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
console.error("Error generating llms.txt:", error);
|
|
1388
|
+
res.status(500).end("Error generating llms.txt");
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function withLLMParam(options) {
|
|
1393
|
+
return async function middleware(request) {
|
|
1394
|
+
const url = new URL(request.url);
|
|
1395
|
+
const llmParam = url.searchParams.get("llm");
|
|
1396
|
+
if (llmParam !== "1") {
|
|
1397
|
+
return void 0;
|
|
1398
|
+
}
|
|
1399
|
+
try {
|
|
1400
|
+
const pathname = url.pathname;
|
|
1401
|
+
const content = await options.getContent(pathname);
|
|
1402
|
+
if (!content) {
|
|
1403
|
+
return new NextResponse("Content not found", { status: 404 });
|
|
1404
|
+
}
|
|
1405
|
+
const markdown = generateMarkdownString(content);
|
|
1406
|
+
return new NextResponse(markdown, {
|
|
1407
|
+
status: 200,
|
|
1408
|
+
headers: {
|
|
1409
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
1410
|
+
"Cache-Control": options.cacheControl || "public, max-age=3600",
|
|
1411
|
+
"X-Robots-Tag": "noindex",
|
|
1412
|
+
...options.cors && {
|
|
1413
|
+
"Access-Control-Allow-Origin": "*",
|
|
1414
|
+
"Access-Control-Allow-Methods": "GET"
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
console.error("Error generating markdown:", error);
|
|
1420
|
+
return new NextResponse("Error generating markdown", { status: 500 });
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function createMarkdownHandler(options) {
|
|
1425
|
+
return async function handler(request, { params }) {
|
|
1426
|
+
try {
|
|
1427
|
+
const slug = params?.slug || "";
|
|
1428
|
+
const content = await options.getContent(slug);
|
|
1429
|
+
if (!content) {
|
|
1430
|
+
return new NextResponse("Content not found", {
|
|
1431
|
+
status: 404,
|
|
1432
|
+
headers: { "Content-Type": "text/plain" }
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
const markdown = generateMarkdownString(content);
|
|
1436
|
+
return new NextResponse(markdown, {
|
|
1437
|
+
status: 200,
|
|
1438
|
+
headers: {
|
|
1439
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
1440
|
+
"Cache-Control": options.cacheControl || "public, max-age=3600",
|
|
1441
|
+
"X-Robots-Tag": "noindex",
|
|
1442
|
+
...options.cors && {
|
|
1443
|
+
"Access-Control-Allow-Origin": "*",
|
|
1444
|
+
"Access-Control-Allow-Methods": "GET"
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
console.error("Error generating markdown:", error);
|
|
1450
|
+
return new NextResponse("Error generating markdown", { status: 500 });
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
function hasLLMParam(request) {
|
|
1455
|
+
const url = new URL(request.url);
|
|
1456
|
+
return url.searchParams.get("llm") === "1";
|
|
1457
|
+
}
|
|
1458
|
+
function createInMemoryStorage() {
|
|
1459
|
+
const events = [];
|
|
1460
|
+
return {
|
|
1461
|
+
save: async (event) => {
|
|
1462
|
+
events.push(event);
|
|
1463
|
+
},
|
|
1464
|
+
getAll: async () => [...events],
|
|
1465
|
+
clear: async () => {
|
|
1466
|
+
events.length = 0;
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function createAnalyticsHandler(options) {
|
|
1471
|
+
const requestCounts = /* @__PURE__ */ new Map();
|
|
1472
|
+
return async function handler(request) {
|
|
1473
|
+
if (request.method === "OPTIONS") {
|
|
1474
|
+
return new NextResponse(null, {
|
|
1475
|
+
status: 204,
|
|
1476
|
+
headers: {
|
|
1477
|
+
"Access-Control-Allow-Origin": "*",
|
|
1478
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
1479
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
if (request.method !== "POST") {
|
|
1484
|
+
return new NextResponse("Method Not Allowed", { status: 405 });
|
|
1485
|
+
}
|
|
1486
|
+
if (options.rateLimit) {
|
|
1487
|
+
const clientIp = request.headers.get("x-forwarded-for") || "unknown";
|
|
1488
|
+
const now = Date.now();
|
|
1489
|
+
const windowMs = 6e4;
|
|
1490
|
+
const clientData = requestCounts.get(clientIp) || { count: 0, resetAt: now + windowMs };
|
|
1491
|
+
if (now > clientData.resetAt) {
|
|
1492
|
+
clientData.count = 0;
|
|
1493
|
+
clientData.resetAt = now + windowMs;
|
|
1494
|
+
}
|
|
1495
|
+
clientData.count++;
|
|
1496
|
+
requestCounts.set(clientIp, clientData);
|
|
1497
|
+
if (clientData.count > options.rateLimit) {
|
|
1498
|
+
return new NextResponse("Rate limit exceeded", {
|
|
1499
|
+
status: 429,
|
|
1500
|
+
headers: {
|
|
1501
|
+
"Retry-After": Math.ceil((clientData.resetAt - now) / 1e3).toString()
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
try {
|
|
1507
|
+
const body = await request.json();
|
|
1508
|
+
const event = {
|
|
1509
|
+
action: body.action || "copy",
|
|
1510
|
+
contentId: body.contentId,
|
|
1511
|
+
url: body.url || request.headers.get("referer"),
|
|
1512
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1513
|
+
metadata: body.metadata
|
|
1514
|
+
};
|
|
1515
|
+
await options.storage.save(event);
|
|
1516
|
+
const headers = {
|
|
1517
|
+
"Content-Type": "application/json"
|
|
1518
|
+
};
|
|
1519
|
+
if (options.cors) {
|
|
1520
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
1521
|
+
}
|
|
1522
|
+
return NextResponse.json({ success: true, event }, { headers });
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
console.error("Analytics tracking error:", error);
|
|
1525
|
+
return NextResponse.json({ success: false, error: "Failed to track event" }, { status: 500 });
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function createAnalyticsPageHandler(options) {
|
|
1530
|
+
return async function handler(req, res) {
|
|
1531
|
+
if (req.method !== "POST") {
|
|
1532
|
+
res.status(405).end("Method Not Allowed");
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
const body = req.body;
|
|
1537
|
+
const event = {
|
|
1538
|
+
action: body?.action || "copy",
|
|
1539
|
+
contentId: body?.contentId,
|
|
1540
|
+
url: body?.url || req.headers?.referer,
|
|
1541
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1542
|
+
metadata: body?.metadata
|
|
1543
|
+
};
|
|
1544
|
+
await options.storage.save(event);
|
|
1545
|
+
if (options.cors) {
|
|
1546
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1547
|
+
}
|
|
1548
|
+
res.status(200).json({ success: true, event });
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
console.error("Analytics tracking error:", error);
|
|
1551
|
+
res.status(500).json({ success: false, error: "Failed to track event" });
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
async function aggregateEvents(storage) {
|
|
1556
|
+
const events = await storage.getAll();
|
|
1557
|
+
const counts = {
|
|
1558
|
+
copy: 0,
|
|
1559
|
+
view: 0,
|
|
1560
|
+
download: 0
|
|
1561
|
+
};
|
|
1562
|
+
for (const event of events) {
|
|
1563
|
+
if (event.action in counts) {
|
|
1564
|
+
counts[event.action]++;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return counts;
|
|
1568
|
+
}
|
|
1569
|
+
async function getEventsForContent(storage, contentId) {
|
|
1570
|
+
const events = await storage.getAll();
|
|
1571
|
+
return events.filter((event) => event.contentId === contentId);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
export { CopyButton, CopyDropdown, LLMBadge, TOC, TOCMobile, aggregateEvents, buildNestedTOC, contentToPlainText, copyToClipboard, createAnalyticsHandler, createAnalyticsPageHandler, createAnalyticsTracker, createInMemoryStorage, createLLMsTxtHandler, createLLMsTxtPageHandler, createMarkdownHandler, downloadAsFile, generateLLMsTxt, generateMarkdown, generateMarkdownString, getEventsForContent, hasLLMParam, htmlToMarkdown, useAnalytics, useLLMCopy, useTOC, useTOCKeyboard, withLLMParam };
|
|
1575
|
+
//# sourceMappingURL=index.js.map
|
|
1576
|
+
//# sourceMappingURL=index.js.map
|