doccupine 0.0.68 → 0.0.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +104 -1
- package/dist/lib/layout.d.ts +1 -1
- package/dist/lib/layout.js +46 -38
- package/dist/lib/structures.js +6 -6
- package/dist/lib/types.d.ts +8 -0
- package/dist/templates/components/Chat.d.ts +1 -1
- package/dist/templates/components/Chat.js +9 -1
- package/dist/templates/components/PostHogProvider.d.ts +1 -0
- package/dist/templates/components/PostHogProvider.js +72 -0
- package/dist/templates/lib/posthog.d.ts +1 -0
- package/dist/templates/lib/posthog.js +36 -0
- package/dist/templates/mdx/analytics.mdx.d.ts +1 -0
- package/dist/templates/mdx/analytics.mdx.js +112 -0
- package/dist/templates/mdx/deployment-and-hosting.mdx.d.ts +1 -1
- package/dist/templates/mdx/deployment-and-hosting.mdx.js +1 -1
- package/dist/templates/mdx/media-and-assets.mdx.d.ts +1 -1
- package/dist/templates/mdx/media-and-assets.mdx.js +2 -1
- package/dist/templates/mdx/platform/file-editor.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/file-editor.mdx.js +7 -1
- package/dist/templates/mdx/platform/navigation-settings.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/navigation-settings.mdx.js +56 -26
- package/dist/templates/mdx/platform/sections-settings.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/sections-settings.mdx.js +12 -7
- package/dist/templates/next.config.d.ts +2 -1
- package/dist/templates/next.config.js +33 -1
- package/dist/templates/package.js +2 -0
- package/dist/templates/proxy.d.ts +2 -1
- package/dist/templates/proxy.js +65 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import { layoutTemplate } from "./lib/layout.js";
|
|
|
11
11
|
import { ConfigManager } from "./lib/config-manager.js";
|
|
12
12
|
import { findAvailablePort, generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
|
|
13
13
|
import { generateMetadataBlock, generateRuntimeOnlyMetadataBlock, } from "./lib/metadata.js";
|
|
14
|
+
import { nextConfigTemplate } from "./templates/next.config.js";
|
|
15
|
+
import { proxyTemplate } from "./templates/proxy.js";
|
|
14
16
|
export { generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
|
|
15
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
18
|
const __dirname = path.dirname(__filename);
|
|
@@ -24,6 +26,7 @@ class MDXToNextJSGenerator {
|
|
|
24
26
|
configWatcher = null;
|
|
25
27
|
fontWatcher = null;
|
|
26
28
|
publicWatcher = null;
|
|
29
|
+
analyticsWatcher = null;
|
|
27
30
|
configFiles = [
|
|
28
31
|
"theme.json",
|
|
29
32
|
"navigation.json",
|
|
@@ -32,6 +35,8 @@ class MDXToNextJSGenerator {
|
|
|
32
35
|
"sections.json",
|
|
33
36
|
];
|
|
34
37
|
fontConfigFile = "fonts.json";
|
|
38
|
+
analyticsConfigFile = "analytics.json";
|
|
39
|
+
analyticsConfig = null;
|
|
35
40
|
sectionsConfig = null;
|
|
36
41
|
/** Guards against recursive reprocessing when maybeUpdateSections() triggers processAllMDXFiles() */
|
|
37
42
|
isReprocessing = false;
|
|
@@ -45,13 +50,18 @@ class MDXToNextJSGenerator {
|
|
|
45
50
|
await fs.ensureDir(this.watchDir);
|
|
46
51
|
await fs.ensureDir(this.outputDir);
|
|
47
52
|
this.sectionsConfig = await this.resolveSections();
|
|
53
|
+
this.analyticsConfig = await this.loadAnalyticsConfig();
|
|
48
54
|
if (this.sectionsConfig) {
|
|
49
55
|
console.log(chalk.blue(`📑 Found ${this.sectionsConfig.length} section(s): ${this.sectionsConfig.map((s) => s.label).join(", ")}`));
|
|
50
56
|
}
|
|
57
|
+
if (this.analyticsConfig) {
|
|
58
|
+
console.log(chalk.blue(`📊 Analytics enabled: ${this.analyticsConfig.provider}`));
|
|
59
|
+
}
|
|
51
60
|
await this.createNextJSStructure();
|
|
52
61
|
await this.createStartingDocs();
|
|
53
62
|
await this.copyCustomConfigFiles();
|
|
54
63
|
await this.copyFontConfig();
|
|
64
|
+
await this.copyAnalyticsConfig();
|
|
55
65
|
await this.copyPublicFiles();
|
|
56
66
|
await this.processAllMDXFiles();
|
|
57
67
|
await this.generateSectionIndexPages();
|
|
@@ -63,6 +73,9 @@ class MDXToNextJSGenerator {
|
|
|
63
73
|
async createNextJSStructure() {
|
|
64
74
|
const structure = {
|
|
65
75
|
...appStructure,
|
|
76
|
+
"next.config.ts": nextConfigTemplate(this.analyticsConfig),
|
|
77
|
+
"proxy.ts": proxyTemplate(this.analyticsConfig),
|
|
78
|
+
"analytics.json": `{}\n`,
|
|
66
79
|
"config.json": `{}\n`,
|
|
67
80
|
"links.json": `[]\n`,
|
|
68
81
|
"navigation.json": `[]\n`,
|
|
@@ -127,6 +140,34 @@ class MDXToNextJSGenerator {
|
|
|
127
140
|
}
|
|
128
141
|
return null;
|
|
129
142
|
}
|
|
143
|
+
async loadAnalyticsConfig() {
|
|
144
|
+
const analyticsPath = path.join(this.rootDir, this.analyticsConfigFile);
|
|
145
|
+
try {
|
|
146
|
+
if (await fs.pathExists(analyticsPath)) {
|
|
147
|
+
const content = await fs.readFile(analyticsPath, "utf8");
|
|
148
|
+
const parsed = JSON.parse(content);
|
|
149
|
+
if (parsed?.provider === "posthog" && parsed.posthog?.key) {
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.warn(chalk.yellow(`⚠️ Error reading ${this.analyticsConfigFile}`), error);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
async copyAnalyticsConfig() {
|
|
160
|
+
console.log(chalk.blue(`🔍 Checking for analytics configuration...`));
|
|
161
|
+
const sourcePath = path.join(this.rootDir, this.analyticsConfigFile);
|
|
162
|
+
const destPath = path.join(this.outputDir, this.analyticsConfigFile);
|
|
163
|
+
if (await fs.pathExists(sourcePath)) {
|
|
164
|
+
await fs.copy(sourcePath, destPath);
|
|
165
|
+
console.log(chalk.green(` ✓ Copied ${this.analyticsConfigFile} to Next.js app`));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log(chalk.gray(` ✗ ${this.analyticsConfigFile} not found, skipping`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
130
171
|
async loadSectionsConfig() {
|
|
131
172
|
const sectionsPath = path.join(this.rootDir, "sections.json");
|
|
132
173
|
try {
|
|
@@ -337,6 +378,44 @@ class MDXToNextJSGenerator {
|
|
|
337
378
|
console.error(chalk.red(`❌ Error removing font configuration:`), error);
|
|
338
379
|
}
|
|
339
380
|
}
|
|
381
|
+
async handleAnalyticsConfigChange() {
|
|
382
|
+
console.log(chalk.cyan(`📊 Analytics configuration changed`));
|
|
383
|
+
const sourcePath = path.join(this.rootDir, this.analyticsConfigFile);
|
|
384
|
+
const destPath = path.join(this.outputDir, this.analyticsConfigFile);
|
|
385
|
+
try {
|
|
386
|
+
await fs.copy(sourcePath, destPath);
|
|
387
|
+
console.log(chalk.green(`📋 Updated ${this.analyticsConfigFile} in Next.js app`));
|
|
388
|
+
this.analyticsConfig = await this.loadAnalyticsConfig();
|
|
389
|
+
// Regenerate dynamic templates that depend on analytics config
|
|
390
|
+
await fs.writeFile(path.join(this.outputDir, "next.config.ts"), nextConfigTemplate(this.analyticsConfig), "utf8");
|
|
391
|
+
await fs.writeFile(path.join(this.outputDir, "proxy.ts"), proxyTemplate(this.analyticsConfig), "utf8");
|
|
392
|
+
await this.updateRootLayout();
|
|
393
|
+
console.log(chalk.green(`✅ Analytics configuration updated`));
|
|
394
|
+
if (this.analyticsConfig) {
|
|
395
|
+
console.log(chalk.yellow(`⚠️ Next.js dev server restart may be required for analytics proxy changes`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.error(chalk.red(`❌ Error updating analytics configuration:`), error);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async handleAnalyticsConfigDelete() {
|
|
403
|
+
console.log(chalk.red(`🗑️ Analytics configuration deleted`));
|
|
404
|
+
const destPath = path.join(this.outputDir, this.analyticsConfigFile);
|
|
405
|
+
try {
|
|
406
|
+
// Write empty analytics.json so runtime imports don't break
|
|
407
|
+
await fs.writeFile(destPath, `{}\n`, "utf8");
|
|
408
|
+
this.analyticsConfig = null;
|
|
409
|
+
// Regenerate dynamic templates without analytics
|
|
410
|
+
await fs.writeFile(path.join(this.outputDir, "next.config.ts"), nextConfigTemplate(null), "utf8");
|
|
411
|
+
await fs.writeFile(path.join(this.outputDir, "proxy.ts"), proxyTemplate(null), "utf8");
|
|
412
|
+
await this.updateRootLayout();
|
|
413
|
+
console.log(chalk.green(`✅ Analytics removed from Next.js app`));
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error(chalk.red(`❌ Error removing analytics configuration:`), error);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
340
419
|
async copyPublicFiles() {
|
|
341
420
|
const publicDir = path.join(this.rootDir, "public");
|
|
342
421
|
const destDir = path.join(this.outputDir, "public");
|
|
@@ -452,6 +531,25 @@ class MDXToNextJSGenerator {
|
|
|
452
531
|
.on("error", (error) => {
|
|
453
532
|
console.error(chalk.red("❌ Font watcher error:"), error);
|
|
454
533
|
});
|
|
534
|
+
const analyticsPath = path.join(this.rootDir, this.analyticsConfigFile);
|
|
535
|
+
this.analyticsWatcher = chokidar.watch(analyticsPath, {
|
|
536
|
+
persistent: true,
|
|
537
|
+
ignoreInitial: true,
|
|
538
|
+
});
|
|
539
|
+
this.analyticsWatcher
|
|
540
|
+
.on("add", () => {
|
|
541
|
+
console.log(chalk.cyan(`📊 Analytics configuration added`));
|
|
542
|
+
this.handleAnalyticsConfigChange();
|
|
543
|
+
})
|
|
544
|
+
.on("change", () => {
|
|
545
|
+
this.handleAnalyticsConfigChange();
|
|
546
|
+
})
|
|
547
|
+
.on("unlink", () => {
|
|
548
|
+
this.handleAnalyticsConfigDelete();
|
|
549
|
+
})
|
|
550
|
+
.on("error", (error) => {
|
|
551
|
+
console.error(chalk.red("❌ Analytics watcher error:"), error);
|
|
552
|
+
});
|
|
455
553
|
const publicDir = path.join(this.rootDir, "public");
|
|
456
554
|
if (await fs.pathExists(publicDir)) {
|
|
457
555
|
this.publicWatcher = chokidar.watch(publicDir, {
|
|
@@ -582,7 +680,8 @@ class MDXToNextJSGenerator {
|
|
|
582
680
|
async generateRootLayout() {
|
|
583
681
|
const pages = await this.buildAllPagesMeta();
|
|
584
682
|
const fontConfig = await this.loadFontConfig();
|
|
585
|
-
|
|
683
|
+
const analyticsEnabled = this.analyticsConfig !== null;
|
|
684
|
+
return layoutTemplate(pages, fontConfig, this.sectionsConfig, analyticsEnabled);
|
|
586
685
|
}
|
|
587
686
|
async generateSectionIndexPages() {
|
|
588
687
|
if (!this.sectionsConfig || this.sectionsConfig.length === 0)
|
|
@@ -732,6 +831,10 @@ export default function Page() {
|
|
|
732
831
|
await this.fontWatcher.close();
|
|
733
832
|
console.log(chalk.yellow("👋 Stopped watching for font config changes"));
|
|
734
833
|
}
|
|
834
|
+
if (this.analyticsWatcher) {
|
|
835
|
+
await this.analyticsWatcher.close();
|
|
836
|
+
console.log(chalk.yellow("👋 Stopped watching for analytics config changes"));
|
|
837
|
+
}
|
|
735
838
|
if (this.publicWatcher) {
|
|
736
839
|
await this.publicWatcher.close();
|
|
737
840
|
console.log(chalk.yellow("👋 Stopped watching for public directory changes"));
|
package/dist/lib/layout.d.ts
CHANGED
|
@@ -26,5 +26,5 @@ interface FontConfig {
|
|
|
26
26
|
src?: LocalFontSrc[];
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
export declare const layoutTemplate: (pages: PageData[], fontConfig: FontConfig | null, sectionsConfig?: SectionConfig[] | null) => string;
|
|
29
|
+
export declare const layoutTemplate: (pages: PageData[], fontConfig: FontConfig | null, sectionsConfig?: SectionConfig[] | null, analyticsEnabled?: boolean) => string;
|
|
30
30
|
export {};
|
package/dist/lib/layout.js
CHANGED
|
@@ -35,8 +35,16 @@ function getLocalFontSrc(fc) {
|
|
|
35
35
|
return `"${fc.localFonts}"`;
|
|
36
36
|
return JSON.stringify(fc.localFonts?.src, null, 2).replace(/"([^"]+)":/g, "$1:");
|
|
37
37
|
}
|
|
38
|
-
export const layoutTemplate = (pages, fontConfig, sectionsConfig = null) => {
|
|
38
|
+
export const layoutTemplate = (pages, fontConfig, sectionsConfig = null, analyticsEnabled = false) => {
|
|
39
39
|
const hasSections = sectionsConfig !== null && sectionsConfig.length > 0;
|
|
40
|
+
// Extra indent when PostHogProvider wraps the inner content
|
|
41
|
+
const a = analyticsEnabled ? " " : "";
|
|
42
|
+
// ChtProvider line wraps at wider indent
|
|
43
|
+
const chtOpen = analyticsEnabled
|
|
44
|
+
? `<ChtProvider
|
|
45
|
+
${a} isChatActive={process.env.LLM_PROVIDER ? true : false}
|
|
46
|
+
${a} >`
|
|
47
|
+
: `<ChtProvider isChatActive={process.env.LLM_PROVIDER ? true : false}>`;
|
|
40
48
|
return `import type { Metadata } from "next";
|
|
41
49
|
${isGoogleFont(fontConfig) ? `import { ${fontConfig.googleFont.fontName} } from "next/font/google";` : isLocalFont(fontConfig) ? 'import localFont from "next/font/local";' : 'import { Inter } from "next/font/google";'}
|
|
42
50
|
import dynamic from "next/dynamic";
|
|
@@ -70,7 +78,7 @@ ${hasSections
|
|
|
70
78
|
? `import { SectionBar } from "@/components/layout/SectionBar";
|
|
71
79
|
import { SectionNavProvider } from "@/components/SectionNavProvider";
|
|
72
80
|
`
|
|
73
|
-
: ""}const Chat = dynamic(() => import("@/components/Chat").then((mod) => mod.Chat));
|
|
81
|
+
: ""}${analyticsEnabled ? `import { PostHogProvider } from "@/components/PostHogProvider";\n` : ""}const Chat = dynamic(() => import("@/components/Chat").then((mod) => mod.Chat));
|
|
74
82
|
|
|
75
83
|
${isGoogleFont(fontConfig)
|
|
76
84
|
? `const font = ${fontConfig.googleFont.fontName}({ ${[fontConfig.googleFont.subsets?.length ? `subsets: ${JSON.stringify(fontConfig.googleFont.subsets)}` : "", fontConfig.googleFont.weight?.length ? `weight: ${Array.isArray(fontConfig.googleFont.weight) ? JSON.stringify(fontConfig.googleFont.weight) : `"${fontConfig.googleFont.weight}"`}` : ""].filter(Boolean).join(", ")} });`
|
|
@@ -123,24 +131,24 @@ ${hasSections
|
|
|
123
131
|
</head>
|
|
124
132
|
<body className={font.className}>
|
|
125
133
|
<StyledComponentsRegistry>
|
|
126
|
-
<CherryThemeProvider theme={theme} themeDark={themeDark}>
|
|
127
|
-
|
|
128
|
-
<Header>
|
|
129
|
-
<SectionBar sections={doccupineSections} />
|
|
130
|
-
</Header>
|
|
131
|
-
{process.env.LLM_PROVIDER && <Chat />}
|
|
132
|
-
<DocsWrapper>
|
|
133
|
-
<SectionNavProvider
|
|
134
|
-
sections={doccupineSections}
|
|
135
|
-
allPages={pages}
|
|
136
|
-
hideBranding={hideBranding}
|
|
137
|
-
>
|
|
138
|
-
{children}
|
|
139
|
-
</SectionNavProvider>
|
|
140
|
-
</DocsWrapper>
|
|
141
|
-
</ChtProvider>
|
|
142
|
-
</CherryThemeProvider>
|
|
143
|
-
</StyledComponentsRegistry>
|
|
134
|
+
${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme} themeDark={themeDark}>
|
|
135
|
+
${a} ${chtOpen}
|
|
136
|
+
${a} <Header>
|
|
137
|
+
${a} <SectionBar sections={doccupineSections} />
|
|
138
|
+
${a} </Header>
|
|
139
|
+
${a} {process.env.LLM_PROVIDER && <Chat />}
|
|
140
|
+
${a} <DocsWrapper>
|
|
141
|
+
${a} <SectionNavProvider
|
|
142
|
+
${a} sections={doccupineSections}
|
|
143
|
+
${a} allPages={pages}
|
|
144
|
+
${a} hideBranding={hideBranding}
|
|
145
|
+
${a} >
|
|
146
|
+
${a} {children}
|
|
147
|
+
${a} </SectionNavProvider>
|
|
148
|
+
${a} </DocsWrapper>
|
|
149
|
+
${a} </ChtProvider>
|
|
150
|
+
${a} </CherryThemeProvider>
|
|
151
|
+
${analyticsEnabled ? " </PostHogProvider>\n" : ""} </StyledComponentsRegistry>
|
|
144
152
|
</body>
|
|
145
153
|
</html>
|
|
146
154
|
);
|
|
@@ -182,24 +190,24 @@ ${hasSections
|
|
|
182
190
|
</head>
|
|
183
191
|
<body className={font.className}>
|
|
184
192
|
<StyledComponentsRegistry>
|
|
185
|
-
<CherryThemeProvider theme={theme} themeDark={themeDark}>
|
|
186
|
-
|
|
187
|
-
<Header />
|
|
188
|
-
{process.env.LLM_PROVIDER && <Chat />}
|
|
189
|
-
<SectionBarProvider hasSectionBar={false}>
|
|
190
|
-
<DocsWrapper>
|
|
191
|
-
<SideBar result={result.length ? result : defaultResults} />
|
|
192
|
-
{children}
|
|
193
|
-
<DocsNavigation
|
|
194
|
-
result={result.length ? result : defaultResults}
|
|
195
|
-
/>
|
|
196
|
-
<StaticLinks />
|
|
197
|
-
<Footer hideBranding={hideBranding} />
|
|
198
|
-
</DocsWrapper>
|
|
199
|
-
</SectionBarProvider>
|
|
200
|
-
</ChtProvider>
|
|
201
|
-
</CherryThemeProvider>
|
|
202
|
-
</StyledComponentsRegistry>
|
|
193
|
+
${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme} themeDark={themeDark}>
|
|
194
|
+
${a} ${chtOpen}
|
|
195
|
+
${a} <Header />
|
|
196
|
+
${a} {process.env.LLM_PROVIDER && <Chat />}
|
|
197
|
+
${a} <SectionBarProvider hasSectionBar={false}>
|
|
198
|
+
${a} <DocsWrapper>
|
|
199
|
+
${a} <SideBar result={result.length ? result : defaultResults} />
|
|
200
|
+
${a} {children}
|
|
201
|
+
${a} <DocsNavigation
|
|
202
|
+
${a} result={result.length ? result : defaultResults}
|
|
203
|
+
${a} />
|
|
204
|
+
${a} <StaticLinks />
|
|
205
|
+
${a} <Footer hideBranding={hideBranding} />
|
|
206
|
+
${a} </DocsWrapper>
|
|
207
|
+
${a} </SectionBarProvider>
|
|
208
|
+
${a} </ChtProvider>
|
|
209
|
+
${a} </CherryThemeProvider>
|
|
210
|
+
${analyticsEnabled ? " </PostHogProvider>\n" : ""} </StyledComponentsRegistry>
|
|
203
211
|
</body>
|
|
204
212
|
</html>
|
|
205
213
|
);
|
package/dist/lib/structures.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { envExampleTemplate } from "../templates/env.example.js";
|
|
2
2
|
import { gitignoreTemplate } from "../templates/gitignore.js";
|
|
3
3
|
import { eslintConfigTemplate } from "../templates/eslint.config.js";
|
|
4
|
-
import { nextConfigTemplate } from "../templates/next.config.js";
|
|
5
4
|
import { packageJsonTemplate } from "../templates/package.js";
|
|
6
5
|
import { prettierrcTemplate } from "../templates/prettierrc.js";
|
|
7
6
|
import { prettierignoreTemplate } from "../templates/prettierignore.js";
|
|
8
|
-
import { proxyTemplate } from "../templates/proxy.js";
|
|
9
7
|
import { tsconfigTemplate } from "../templates/tsconfig.js";
|
|
10
8
|
import { mcpRoutesTemplate } from "../templates/app/api/mcp/route.js";
|
|
11
9
|
import { ragRoutesTemplate } from "../templates/app/api/rag/route.js";
|
|
@@ -19,6 +17,7 @@ import { docsTemplate } from "../templates/components/Docs.js";
|
|
|
19
17
|
import { docsSideBarTemplate } from "../templates/components/DocsSideBar.js";
|
|
20
18
|
import { mdxComponentsTemplate } from "../templates/components/MDXComponents.js";
|
|
21
19
|
import { sectionNavProviderTemplate } from "../templates/components/SectionNavProvider.js";
|
|
20
|
+
import { postHogProviderTemplate } from "../templates/components/PostHogProvider.js";
|
|
22
21
|
import { sideBarTemplate } from "../templates/components/SideBar.js";
|
|
23
22
|
import { sectionBarTemplate } from "../templates/components/layout/SectionBar.js";
|
|
24
23
|
import { accordionTemplate } from "../templates/components/layout/Accordion.js";
|
|
@@ -54,6 +53,7 @@ import { llmConfigTemplate } from "../templates/services/llm/config.js";
|
|
|
54
53
|
import { llmFactoryTemplate } from "../templates/services/llm/factory.js";
|
|
55
54
|
import { llmIndexTemplate } from "../templates/services/llm/index.js";
|
|
56
55
|
import { llmTypesTemplate } from "../templates/services/llm/types.js";
|
|
56
|
+
import { posthogServerTemplate } from "../templates/lib/posthog.js";
|
|
57
57
|
import { styledDTemplate } from "../templates/types/styled.js";
|
|
58
58
|
import { orderNavItemsTemplate } from "../templates/utils/orderNavItems.js";
|
|
59
59
|
import { rateLimitTemplate } from "../templates/utils/rateLimit.js";
|
|
@@ -61,6 +61,7 @@ import { brandingTemplate } from "../templates/utils/branding.js";
|
|
|
61
61
|
import { configTemplate } from "../templates/utils/config.js";
|
|
62
62
|
import { accordionMdxTemplate } from "../templates/mdx/accordion.mdx.js";
|
|
63
63
|
import { aiAssistantMdxTemplate } from "../templates/mdx/ai-assistant.mdx.js";
|
|
64
|
+
import { analyticsMdxTemplate } from "../templates/mdx/analytics.mdx.js";
|
|
64
65
|
import { buttonsMdxTemplate } from "../templates/mdx/buttons.mdx.js";
|
|
65
66
|
import { calloutsMdxTemplate } from "../templates/mdx/callouts.mdx.js";
|
|
66
67
|
import { cardsMdxTemplate } from "../templates/mdx/cards.mdx.js";
|
|
@@ -93,7 +94,6 @@ import { platformCreatingAProjectMdxTemplate } from "../templates/mdx/platform/c
|
|
|
93
94
|
import { platformSiteSettingsMdxTemplate } from "../templates/mdx/platform/site-settings.mdx.js";
|
|
94
95
|
import { platformThemeSettingsMdxTemplate } from "../templates/mdx/platform/theme-settings.mdx.js";
|
|
95
96
|
import { platformNavigationSettingsMdxTemplate } from "../templates/mdx/platform/navigation-settings.mdx.js";
|
|
96
|
-
import { platformSectionsSettingsMdxTemplate } from "../templates/mdx/platform/sections-settings.mdx.js";
|
|
97
97
|
import { platformFontsSettingsMdxTemplate } from "../templates/mdx/platform/fonts-settings.mdx.js";
|
|
98
98
|
import { platformExternalLinksMdxTemplate } from "../templates/mdx/platform/external-links.mdx.js";
|
|
99
99
|
import { platformAiAssistantMdxTemplate } from "../templates/mdx/platform/ai-assistant.mdx.js";
|
|
@@ -108,9 +108,7 @@ export const appStructure = {
|
|
|
108
108
|
".prettierrc": prettierrcTemplate,
|
|
109
109
|
".prettierignore": prettierignoreTemplate,
|
|
110
110
|
"eslint.config.mjs": eslintConfigTemplate,
|
|
111
|
-
"next.config.ts": nextConfigTemplate,
|
|
112
111
|
"package.json": packageJsonTemplate,
|
|
113
|
-
"proxy.ts": proxyTemplate,
|
|
114
112
|
"tsconfig.json": tsconfigTemplate,
|
|
115
113
|
"app/not-found.tsx": notFoundTemplate,
|
|
116
114
|
"app/theme.ts": themeTemplate,
|
|
@@ -126,6 +124,7 @@ export const appStructure = {
|
|
|
126
124
|
"services/llm/index.ts": llmIndexTemplate,
|
|
127
125
|
"services/llm/types.ts": llmTypesTemplate,
|
|
128
126
|
"types/styled.d.ts": styledDTemplate,
|
|
127
|
+
"lib/posthog.ts": posthogServerTemplate,
|
|
129
128
|
"utils/branding.ts": brandingTemplate,
|
|
130
129
|
"utils/orderNavItems.ts": orderNavItemsTemplate,
|
|
131
130
|
"utils/rateLimit.ts": rateLimitTemplate,
|
|
@@ -137,6 +136,7 @@ export const appStructure = {
|
|
|
137
136
|
"components/DocsSideBar.tsx": docsSideBarTemplate,
|
|
138
137
|
"components/MDXComponents.tsx": mdxComponentsTemplate,
|
|
139
138
|
"components/SectionNavProvider.tsx": sectionNavProviderTemplate,
|
|
139
|
+
"components/PostHogProvider.tsx": postHogProviderTemplate,
|
|
140
140
|
"components/SideBar.tsx": sideBarTemplate,
|
|
141
141
|
"components/layout/Accordion.tsx": accordionTemplate,
|
|
142
142
|
"components/layout/ActionBar.tsx": actionBarTemplate,
|
|
@@ -168,6 +168,7 @@ export const appStructure = {
|
|
|
168
168
|
export const startingDocsStructure = {
|
|
169
169
|
"accordion.mdx": accordionMdxTemplate,
|
|
170
170
|
"ai-assistant.mdx": aiAssistantMdxTemplate,
|
|
171
|
+
"analytics.mdx": analyticsMdxTemplate,
|
|
171
172
|
"buttons.mdx": buttonsMdxTemplate,
|
|
172
173
|
"callouts.mdx": calloutsMdxTemplate,
|
|
173
174
|
"cards.mdx": cardsMdxTemplate,
|
|
@@ -200,7 +201,6 @@ export const startingDocsStructure = {
|
|
|
200
201
|
"platform/site-settings.mdx": platformSiteSettingsMdxTemplate,
|
|
201
202
|
"platform/theme-settings.mdx": platformThemeSettingsMdxTemplate,
|
|
202
203
|
"platform/navigation-settings.mdx": platformNavigationSettingsMdxTemplate,
|
|
203
|
-
"platform/sections-settings.mdx": platformSectionsSettingsMdxTemplate,
|
|
204
204
|
"platform/fonts-settings.mdx": platformFontsSettingsMdxTemplate,
|
|
205
205
|
"platform/external-links.mdx": platformExternalLinksMdxTemplate,
|
|
206
206
|
"platform/ai-assistant.mdx": platformAiAssistantMdxTemplate,
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -28,3 +28,11 @@ export interface DoccupineConfig {
|
|
|
28
28
|
export interface FontConfig {
|
|
29
29
|
[key: string]: any;
|
|
30
30
|
}
|
|
31
|
+
export interface PostHogConfig {
|
|
32
|
+
key: string;
|
|
33
|
+
host?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface AnalyticsConfig {
|
|
36
|
+
provider: "posthog";
|
|
37
|
+
posthog: PostHogConfig;
|
|
38
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const chatTemplate = "\"use client\";\nimport React, {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport styled, { css, keyframes } from \"styled-components\";\nimport { rgba } from \"polished\";\nimport { Button } from \"cherry-styled-components\";\nimport { ArrowUp, LoaderPinwheel, RotateCcw, Sparkles, X } from \"lucide-react\";\nimport remarkGfm from \"remark-gfm\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport { MDXRemote, MDXRemoteSerializeResult } from \"next-mdx-remote\";\nimport { serialize } from \"next-mdx-remote/serialize\";\nimport Link from \"next/link\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { useLockBodyScroll } from \"@/components/LockBodyScroll\";\nimport { useMDXComponents as getMDXComponents } from \"@/components/MDXComponents\";\nimport {\n styledAnchor,\n styledTable,\n stylesLists,\n StyledSmallButton,\n interactiveStyles,\n} from \"@/components/layout/SharedStyled\";\n\nconst mdxComponents = getMDXComponents({});\n\nconst styledText = css<{ theme: Theme }>`\n font-size: ${({ theme }) => theme.fontSizes.text.xs};\n line-height: ${({ theme }) => theme.lineHeights.text.xs};\n\n ${mq(\"lg\")} {\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n line-height: ${({ theme }) => theme.lineHeights.small.lg};\n }\n`;\n\nconst StyledChat = styled.div<{ theme: Theme; $isVisible: boolean }>`\n margin: 0;\n position: fixed;\n top: 0;\n right: 0;\n width: 100%;\n height: calc(100dvh - 90px);\n overflow-y: scroll;\n overflow-x: hidden;\n z-index: 1000;\n padding: 0 20px;\n transition: all 0.3s ease;\n transform: translateX(0);\n background: ${({ theme }) => theme.colors.light};\n -webkit-overflow-scrolling: touch;\n opacity: 1;\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n ${({ $isVisible }) =>\n !$isVisible &&\n css`\n transform: translateX(100%);\n opacity: 0;\n `}\n\n ${mq(\"lg\")} {\n width: 420px;\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n }\n`;\n\nconst loadingAnimation = keyframes`\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n`;\n\nconst rotateGradient = keyframes`\n 0% {\n --gradient-angle: 0deg;\n }\n 100% {\n --gradient-angle: 360deg;\n }\n`;\n\nconst pulseGlow = keyframes`\n 0%, 100% {\n opacity: 0.5;\n filter: blur(16px);\n }\n 50% {\n opacity: 1;\n filter: blur(22px);\n }\n`;\n\nconst sparkleFloat = keyframes`\n 0%, 100% {\n opacity: 0;\n transform: translateY(0) scale(0);\n }\n 50% {\n opacity: 0.9;\n transform: translateY(-20px) scale(1);\n }\n`;\n\nconst shimmer = keyframes`\n 0% {\n background-position: 0% center;\n }\n 50% {\n background-position: 100% center;\n }\n 100% {\n background-position: 0% center;\n }\n`;\n\nconst StyledRainbowInputWrapper = styled.div<{\n theme: Theme;\n $isActive: boolean;\n}>`\n @property --gradient-angle {\n syntax: \"<angle>\";\n initial-value: 0deg;\n inherits: false;\n }\n\n position: relative;\n flex: 1;\n\n &::before {\n content: \"\";\n position: absolute;\n inset: -2px;\n border-radius: 14px;\n background: conic-gradient(\n from var(--gradient-angle),\n #cc5555,\n #d9a745,\n #3ab0cc,\n #cc7fc2,\n #4380cc,\n #4c1fa3,\n #cc5555\n );\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n animation: ${rotateGradient} 3s linear infinite;\n z-index: 0;\n }\n\n &::after {\n content: \"\";\n position: absolute;\n inset: -10px;\n border-radius: 20px;\n background: conic-gradient(\n from var(--gradient-angle),\n ${rgba(\"#ff6b6b\", 0.4)},\n ${rgba(\"#feca57\", 0.4)},\n ${rgba(\"#48dbfb\", 0.4)},\n ${rgba(\"#ff9ff3\", 0.4)},\n ${rgba(\"#54a0ff\", 0.4)},\n ${rgba(\"#5f27cd\", 0.4)},\n ${rgba(\"#ff6b6b\", 0.4)}\n );\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n animation:\n ${rotateGradient} 3s linear infinite,\n ${pulseGlow} 2s ease-in-out infinite;\n z-index: -1;\n pointer-events: none;\n }\n\n &:hover::before,\n &:focus-within::before {\n opacity: 1;\n }\n\n &:hover::after,\n &:focus-within::after {\n opacity: 1;\n }\n\n ${({ $isActive }) =>\n $isActive &&\n css`\n &::before {\n opacity: 1;\n }\n &::after {\n opacity: 1;\n }\n `}\n`;\n\nconst StyledSparkleContainer = styled.div<{ $isActive: boolean }>`\n position: absolute;\n inset: -30px;\n pointer-events: none;\n overflow: hidden;\n border-radius: 30px;\n z-index: -2;\n opacity: 0;\n transition: opacity 0.4s ease;\n\n ${({ $isActive }) =>\n $isActive &&\n css`\n opacity: 1;\n `}\n`;\n\nconst StyledSparkle = styled.div<{\n $color: string;\n $left: number;\n $top: number;\n $delay: number;\n}>`\n position: absolute;\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: ${({ $color }) => $color};\n box-shadow: 0 0 6px ${({ $color }) => $color};\n left: ${({ $left }) => $left}%;\n top: ${({ $top }) => $top}%;\n animation: ${sparkleFloat} 2s ease-in-out infinite;\n animation-delay: ${({ $delay }) => $delay}s;\n`;\n\nconst StyledRainbowInput = styled.input<{ theme: Theme }>`\n position: relative;\n z-index: 1;\n width: 100%;\n background: ${({ theme }) => theme.colors.light};\n border: 1px solid ${({ theme }) => theme.colors.grayLight};\n border-radius: 12px;\n padding: 14px 18px;\n font-size: ${({ theme }) => theme.fontSizes.text.lg};\n font-family: inherit;\n color: ${({ theme }) => theme.colors.dark};\n outline: none;\n transition:\n border-color 0.3s ease,\n box-shadow 0.3s ease;\n\n ${mq(\"lg\")} {\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n }\n\n &::placeholder {\n color: ${({ theme }) => rgba(theme.colors.dark, 0.4)};\n transition: color 0.3s ease;\n }\n\n &:focus::placeholder {\n color: ${({ theme }) => rgba(theme.colors.dark, 0.6)};\n }\n\n &:focus {\n border-color: transparent;\n }\n`;\n\nconst StyledRainbowButton = styled(Button)<{\n theme: Theme;\n $hasContent: boolean;\n}>`\n padding-top: 10px;\n padding-bottom: 10px;\n position: relative;\n overflow: hidden;\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;\n color: ${({ theme }) =>\n theme.isDark ? theme.colors.dark : theme.colors.light};\n\n &::before {\n content: \"\";\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 135deg,\n #ff6b6b,\n #feca57,\n #48dbfb,\n #ff9ff3,\n #54a0ff\n );\n background-size: 300% 300%;\n opacity: 0;\n transition: opacity 0.3s ease;\n z-index: 0;\n animation: ${shimmer} 3s linear infinite;\n width: 200%;\n }\n\n ${({ $hasContent }) =>\n $hasContent &&\n css`\n &::before {\n opacity: 1;\n }\n `}\n\n &:hover::before {\n opacity: 1;\n }\n\n &:hover {\n transform: scale(1.05);\n }\n\n &:active {\n transform: scale(0.95);\n }\n\n & svg {\n position: relative;\n z-index: 1;\n transition: transform 0.3s ease;\n }\n\n &:disabled,\n &:disabled:hover {\n background: ${({ theme }) => theme.colors.primaryDark};\n transform: none;\n box-shadow: none;\n\n &::before {\n opacity: 0;\n }\n }\n`;\n\nconst StyledChatForm = styled.form<{ theme: Theme; $isVisible: boolean }>`\n display: flex;\n gap: 10px;\n justify-content: center;\n align-items: center;\n background: ${({ theme }) => theme.colors.light};\n padding: 20px;\n position: fixed;\n bottom: 0;\n right: 0;\n z-index: 1000;\n width: 100%;\n border-top: solid 1px ${({ theme }) => theme.colors.grayLight};\n transition: all 0.3s ease;\n transform: translateX(100%);\n opacity: 0;\n\n ${mq(\"lg\")} {\n width: 420px;\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n }\n\n ${({ $isVisible }) =>\n $isVisible &&\n css`\n opacity: 1;\n transform: translateX(0);\n `}\n\n & .loading {\n animation: ${loadingAnimation} 1s linear infinite;\n }\n`;\n\nconst StyledGlowSmallButton = styled(StyledSmallButton)<{\n theme: Theme;\n $hasContent: boolean;\n}>`\n @property --gradient-angle {\n syntax: \"<angle>\";\n initial-value: 0deg;\n inherits: false;\n }\n\n position: relative;\n isolation: isolate;\n margin-right: 0;\n background: ${({ theme }) => theme.colors.light};\n padding: 0;\n\n &::before {\n content: \"\";\n inset: -2px;\n border-radius: 8px;\n background: conic-gradient(\n from var(--gradient-angle),\n #cc5555,\n #d9a745,\n #3ab0cc,\n #cc7fc2,\n #4380cc,\n #4c1fa3,\n #cc5555\n );\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n animation: ${rotateGradient} 3s linear infinite;\n z-index: -1;\n position: absolute;\n top: -2px;\n left: -2px;\n width: calc(100% + 4px);\n height: calc(100% + 4px);\n }\n\n &::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n border-radius: 14px;\n background: conic-gradient(\n from var(--gradient-angle),\n ${rgba(\"#ff6b6b\", 0.4)},\n ${rgba(\"#feca57\", 0.4)},\n ${rgba(\"#48dbfb\", 0.4)},\n ${rgba(\"#ff9ff3\", 0.4)},\n ${rgba(\"#54a0ff\", 0.4)},\n ${rgba(\"#5f27cd\", 0.4)},\n ${rgba(\"#ff6b6b\", 0.4)}\n );\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n animation:\n ${rotateGradient} 3s linear infinite,\n ${pulseGlow} 2s ease-in-out infinite;\n z-index: -2;\n pointer-events: none;\n }\n\n &:hover::before,\n &:hover::after {\n opacity: 1;\n }\n\n & span {\n padding: 6px 8px;\n display: flex;\n background: ${({ theme }) => theme.colors.light};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n gap: 6px;\n }\n\n ${({ $hasContent }) =>\n $hasContent &&\n css`\n &::before {\n opacity: 1;\n }\n &::after {\n opacity: 1;\n }\n `}\n`;\n\nconst StyledError = styled.div<{ theme: Theme }>`\n overflow-x: auto;\n background: ${({ theme }) => theme.colors.error};\n color: ${({ theme }) =>\n theme.isDark ? theme.colors.dark : theme.colors.light};\n padding: 10px;\n border-radius: 8px;\n margin: 20px 0;\n width: 100%;\n ${styledText};\n`;\n\nconst loadingDotAnimation = keyframes`\n 0% {\n opacity: 0;\n }\n 50% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n }\n`;\n\nconst StyledLoading = styled.div<{ theme: Theme }>`\n overflow-x: auto;\n margin: 20px 0;\n width: 100%;\n font-weight: 600;\n ${styledText};\n color: ${({ theme }) => theme.colors.dark};\n\n & span {\n &:nth-child(1) {\n animation: ${loadingDotAnimation} 1s ease infinite;\n }\n &:nth-child(2) {\n animation: ${loadingDotAnimation} 1s ease infinite;\n animation-delay: 0.2s;\n }\n &:nth-child(3) {\n animation: ${loadingDotAnimation} 1s ease infinite;\n animation-delay: 0.4s;\n }\n }\n`;\n\nconst StyledAnswer = styled.div<{ theme: Theme; $isAnswer: boolean }>`\n overflow-x: auto;\n background: ${({ theme }) => theme.colors.primary};\n color: ${({ theme }) =>\n theme.isDark ? theme.colors.dark : theme.colors.light};\n padding: 10px;\n border-radius: 8px;\n margin: 20px 0;\n width: 100%;\n ${styledText};\n\n & p {\n ${styledText};\n }\n\n ${({ $isAnswer }) =>\n $isAnswer &&\n css`\n background: transparent;\n color: ${({ theme }) => theme.colors.dark};\n padding: 0;\n `}\n\n & code:not([class]) {\n background: ${({ theme }) => rgba(theme.colors.primaryLight, 0.2)};\n color: ${({ theme }) => theme.colors.dark};\n padding: 2px 4px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n white-space: pre;\n }\n\n ${styledAnchor};\n ${stylesLists};\n ${styledTable};\n\n & pre,\n & .hljs {\n margin: 10px 0;\n }\n\n & .code-wrapper pre {\n margin: 0;\n ${styledText};\n }\n\n & > *:first-child {\n margin-top: 0;\n }\n\n & > *:last-child {\n margin-bottom: 0;\n\n & > *:last-child {\n margin-bottom: 0;\n }\n }\n\n & ul,\n & ol {\n & li {\n ${styledText};\n }\n }\n\n & ol {\n & > li {\n padding-left: 20px;\n\n &::before {\n position: absolute;\n top: 0;\n left: 0;\n }\n }\n }\n\n & img,\n & video,\n & iframe {\n max-width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n margin: 10px 0;\n display: block;\n }\n\n & h1,\n & h2,\n & h3,\n & h4,\n & h5,\n & h6 {\n margin: 10px 0;\n padding: 0;\n }\n`;\n\nconst StyledSources = styled.div`\n display: flex;\n gap: 16px;\n flex-wrap: wrap;\n margin: -5px 0 20px;\n`;\n\nconst StyledSourceLink = styled(Link)<{ theme: Theme }>`\n position: relative;\n text-decoration: none;\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n line-height: 1;\n color: ${({ theme }) => theme.colors.primary};\n display: flex;\n gap: 6px;\n transition: all 0.3s ease;\n font-weight: 600;\n white-space: nowrap;\n min-width: fit-content;\n background: ${({ theme }) => rgba(theme.colors.primaryLight, 0.1)};\n padding: 6px 8px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n ${interactiveStyles};\n\n & * {\n margin: auto 0;\n }\n\n &:hover {\n color: ${({ theme }) =>\n theme.isDark ? theme.colors.primaryLight : theme.colors.primaryDark};\n }\n`;\n\nconst StyledChatTitle = styled.div<{ theme: Theme }>`\n display: flex;\n flex-wrap: nowrap;\n justify-content: space-between;\n position: sticky;\n margin: 0 -20px;\n padding: 16px 20px;\n height: 62px;\n top: 0;\n background: ${({ theme }) => theme.colors.light};\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n z-index: 1000;\n`;\n\nconst StyledChatTitleIconWrapper = styled.span<{ theme: Theme }>`\n display: flex;\n align-items: center;\n gap: 12px;\n color: ${({ theme }) => theme.colors.dark};\n`;\n\nconst StyledChatCloseButton = styled.button<{ theme: Theme }>`\n background: transparent;\n border: none;\n cursor: pointer;\n padding: 0;\n margin: 0;\n color: ${({ theme }) => theme.colors.primary};\n\n &:hover {\n color: ${({ theme }) => theme.colors.primaryDark};\n transform: scale(1.05);\n }\n\n &:active {\n transform: scale(0.95);\n }\n`;\n\ntype Source = {\n id: string;\n path: string;\n uri: string;\n score: number;\n};\n\ntype Answer = {\n text: string;\n answer?: boolean;\n mdx?: MDXRemoteSerializeResult;\n sources?: Source[];\n};\n\nconst SPARKLE_COLORS = [\n \"#ff6b6b\",\n \"#feca57\",\n \"#48dbfb\",\n \"#ff9ff3\",\n \"#54a0ff\",\n \"#5f27cd\",\n];\n\n// Deterministic sparkle positions to avoid hydration mismatch\nconst SPARKLE_POSITIONS = [\n { left: 8, top: 35 },\n { left: 17, top: 55 },\n { left: 26, top: 28 },\n { left: 35, top: 68 },\n { left: 44, top: 42 },\n { left: 53, top: 75 },\n { left: 62, top: 32 },\n { left: 71, top: 58 },\n { left: 80, top: 45 },\n { left: 89, top: 65 },\n];\n\ninterface RainbowInputProps {\n id?: string;\n value: string;\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n placeholder?: string;\n autoComplete?: string;\n \"aria-label\"?: string;\n inputRef?: React.Ref<HTMLInputElement>;\n}\n\nfunction RainbowInput({\n id,\n value,\n onChange,\n placeholder,\n autoComplete,\n \"aria-label\": ariaLabel,\n inputRef,\n}: RainbowInputProps) {\n const [isFocused, setIsFocused] = useState(false);\n const [isHovered, setIsHovered] = useState(false);\n const isActive = isFocused || isHovered;\n\n const sparkles = SPARKLE_POSITIONS.map((pos, i) => ({\n color: SPARKLE_COLORS[i % SPARKLE_COLORS.length],\n left: pos.left,\n top: pos.top,\n delay: i * 0.12,\n }));\n\n return (\n <StyledRainbowInputWrapper\n $isActive={isActive}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <StyledSparkleContainer $isActive={isActive}>\n {sparkles.map((sparkle, i) => (\n <StyledSparkle\n key={i}\n $color={sparkle.color}\n $left={sparkle.left}\n $top={sparkle.top}\n $delay={sparkle.delay}\n />\n ))}\n </StyledSparkleContainer>\n <StyledRainbowInput\n ref={inputRef}\n id={id}\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n autoComplete={autoComplete}\n aria-label={ariaLabel}\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n />\n </StyledRainbowInputWrapper>\n );\n}\n\nfunction ChatButtonCTA() {\n const { setIsOpen, isOpen, answer, setAnswer, chatInputRef } =\n useContext(ChatContext);\n\n return (\n <StyledGlowSmallButton\n onClick={() => {\n const next = !isOpen;\n setIsOpen(next);\n if (next) {\n if (answer.length === 0) {\n setAnswer([\n { text: \"Hey there, how can I assist you?\", answer: true },\n ]);\n }\n setTimeout(() => {\n chatInputRef.current?.focus();\n }, 350);\n }\n }}\n aria-label=\"Ask AI Assistant\"\n $hasContent={isOpen}\n type=\"button\"\n >\n <span>\n <Sparkles size={16} />\n Ask AI\n </span>\n </StyledGlowSmallButton>\n );\n}\n\nfunction Chat() {\n const {\n isOpen,\n question,\n setQuestion,\n loading,\n error,\n answer,\n ask,\n closeChat,\n resetChat,\n chatInputRef,\n } = useContext(ChatContext);\n const endRef = useRef<HTMLDivElement | null>(null);\n\n useLockBodyScroll(isOpen);\n\n useEffect(() => {\n endRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"end\" });\n }, [answer]);\n\n useEffect(() => {\n if (answer?.length > 0) {\n chatInputRef.current?.focus();\n }\n }, [answer, chatInputRef]);\n\n return (\n <>\n <StyledChat $isVisible={isOpen}>\n <StyledChatTitle>\n <StyledChatTitleIconWrapper>\n <Sparkles />\n <h3>AI Assistant</h3>\n </StyledChatTitleIconWrapper>\n <StyledChatTitleIconWrapper>\n <StyledChatCloseButton\n onClick={resetChat}\n aria-label=\"Reset chat history\"\n title=\"Reset chat history\"\n >\n <RotateCcw size={18} />\n </StyledChatCloseButton>\n <StyledChatCloseButton\n onClick={closeChat}\n aria-label=\"Close chat\"\n title=\"Close chat\"\n >\n <X />\n </StyledChatCloseButton>\n </StyledChatTitleIconWrapper>\n </StyledChatTitle>\n {answer &&\n answer.map((a, i) => (\n <React.Fragment key={i}>\n <StyledAnswer $isAnswer={a.answer ?? false}>\n {a.answer && a.mdx ? (\n <MDXRemote {...a.mdx} components={mdxComponents} />\n ) : (\n a.text\n )}\n </StyledAnswer>\n {a.answer && a.sources && a.sources.length > 0 && (\n <StyledSources>\n {a.sources.map((src) => {\n const slug = src.uri\n .replace(\"docs://\", \"\")\n .replace(/^\\/+/, \"\");\n const href = slug ? `/${slug}/` : \"/\";\n const label = slug\n ? slug\n .split(\"/\")\n .pop()!\n .replace(/-/g, \" \")\n .replace(/\\b\\w/g, (c: string) => c.toUpperCase())\n : \"Home\";\n return (\n <StyledSourceLink key={src.id} href={href}>\n {label}\n </StyledSourceLink>\n );\n })}\n </StyledSources>\n )}\n </React.Fragment>\n ))}\n {loading && (\n <StyledLoading>\n Answering<span>.</span>\n <span>.</span>\n <span>.</span>\n </StyledLoading>\n )}\n {error && (\n <StyledError>\n <strong>Error:</strong> {error}\n </StyledError>\n )}\n <div ref={endRef} />\n </StyledChat>\n\n <StyledChatForm onSubmit={ask} $isVisible={isOpen}>\n <RainbowInput\n id=\"chat-bottom-input\"\n inputRef={chatInputRef}\n value={question}\n onChange={(e) => setQuestion(e.target.value)}\n placeholder=\"Ask AI Assistant...\"\n autoComplete=\"off\"\n aria-label=\"Ask a follow-up question\"\n />\n <StyledRainbowButton\n type=\"submit\"\n disabled={loading || question.trim() === \"\"}\n $hasContent={question.trim().length > 0}\n aria-label={loading ? \"Loading response\" : \"Submit question\"}\n >\n {loading ? <LoaderPinwheel className=\"loading\" /> : <ArrowUp />}\n </StyledRainbowButton>\n </StyledChatForm>\n </>\n );\n}\n\nconst ChatContext = createContext<{\n isOpen: boolean;\n setIsOpen: (isOpen: boolean) => void;\n isChatActive: boolean;\n question: string;\n setQuestion: (q: string) => void;\n loading: boolean;\n error: string | null;\n answer: Answer[];\n setAnswer: (answers: Answer[]) => void;\n ask: (e: React.FormEvent) => void;\n closeChat: () => void;\n resetChat: () => void;\n chatInputRef: React.RefObject<HTMLInputElement | null>;\n}>({\n isOpen: false,\n setIsOpen: () => {},\n isChatActive: false,\n question: \"\",\n setQuestion: () => {},\n loading: false,\n error: null,\n answer: [],\n setAnswer: () => {},\n ask: () => {},\n closeChat: () => {},\n resetChat: () => {},\n chatInputRef: { current: null },\n});\n\ninterface ChatContextProviderProps {\n children: React.ReactNode;\n isChatActive: boolean;\n}\n\nconst ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {\n const [isOpen, setIsOpen] = useState(false);\n const [question, setQuestion] = useState(\"\");\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [answer, setAnswer] = useState<Answer[]>([]);\n const abortRef = useRef<AbortController | null>(null);\n const chatInputRef = useRef<HTMLInputElement | null>(null);\n\n async function ask(e: React.FormEvent) {\n e.preventDefault();\n if (loading || question.trim() === \"\") return;\n const currentQuestion = question;\n setQuestion(\"\");\n setIsOpen(true);\n setLoading(true);\n setError(null);\n\n const mergedQuestions =\n answer.length > 0\n ? [...answer, { text: currentQuestion, answer: false }]\n : [{ text: currentQuestion, answer: false }];\n\n setAnswer(mergedQuestions);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n try {\n const history = answer\n .filter((a) => a.text.trim() !== \"\")\n .map((a) => ({\n role: a.answer ? (\"assistant\" as const) : (\"user\" as const),\n content: a.text,\n }));\n\n const res = await fetch(\"/api/rag\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ question: currentQuestion, history }),\n signal: controller.signal,\n });\n\n if (!res.ok) {\n const errorData = await res.json();\n throw new Error(errorData.error || \"Request failed\");\n }\n\n const reader = res.body?.getReader();\n const decoder = new TextDecoder();\n const contentParts: string[] = [];\n let sources: Source[] = [];\n if (!reader) {\n throw new Error(\"Failed to get response reader\");\n }\n\n const streamingAnswerIndex = mergedQuestions.length;\n setAnswer([...mergedQuestions, { text: \"\", answer: true }]);\n\n let buffer = \"\";\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const parts = buffer.split(\"\\n\");\n buffer = parts.pop() ?? \"\";\n\n for (const line of parts) {\n if (line.startsWith(\"data: \")) {\n try {\n const data = JSON.parse(line.slice(6));\n\n if (data.type === \"metadata\") {\n const allSources: Source[] = data.data?.sources ?? [];\n const seen = new Set<string>();\n sources = allSources.filter((s: Source) => {\n if (s.score < 0.4 || seen.has(s.uri)) return false;\n seen.add(s.uri);\n return true;\n });\n } else if (data.type === \"content\") {\n contentParts.push(data.data);\n const streamedContent = contentParts.join(\"\");\n\n setAnswer((prev) => {\n const newAnswers = [...prev];\n newAnswers[streamingAnswerIndex] = {\n text: streamedContent,\n answer: true,\n sources,\n };\n return newAnswers;\n });\n } else if (data.type === \"error\") {\n throw new Error(data.data);\n } else if (data.type === \"done\") {\n const streamedContent = contentParts.join(\"\");\n let mdxSource: MDXRemoteSerializeResult | null = null;\n try {\n mdxSource = await serialize(streamedContent, {\n parseFrontmatter: false,\n mdxOptions: {\n remarkPlugins: [remarkGfm],\n rehypePlugins: [rehypeHighlight],\n format: \"md\",\n development: false,\n },\n });\n } catch (mdxError: unknown) {\n console.error(\"MDX serialization error:\", mdxError);\n }\n\n setAnswer((prev) => {\n const newAnswers = [...prev];\n newAnswers[streamingAnswerIndex] = {\n text: streamedContent,\n answer: true,\n mdx: mdxSource || undefined,\n sources,\n };\n return newAnswers;\n });\n }\n } catch (parseError) {\n if (\n parseError instanceof Error &&\n parseError.message !== \"Unknown error\"\n ) {\n console.error(\"Failed to parse SSE data:\", parseError);\n }\n }\n }\n }\n }\n } catch (err: unknown) {\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setError(err instanceof Error ? err.message : \"Unknown error\");\n } finally {\n abortRef.current = null;\n setLoading(false);\n }\n }\n\n function closeChat() {\n setIsOpen(false);\n }\n\n function resetChat() {\n abortRef.current?.abort();\n setLoading(false);\n setError(null);\n setAnswer([{ text: \"Hey there, how can I assist you?\", answer: true }]);\n }\n\n return (\n <ChatContext.Provider\n value={{\n isOpen,\n setIsOpen,\n isChatActive,\n question,\n setQuestion,\n loading,\n error,\n answer,\n setAnswer,\n ask,\n closeChat,\n resetChat,\n chatInputRef,\n }}\n >\n {children}\n </ChatContext.Provider>\n );\n};\n\nexport { Chat, ChtProvider, ChatContext, ChatButtonCTA };\n";
|
|
1
|
+
export declare const chatTemplate = "\"use client\";\nimport React, {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport styled, { css, keyframes } from \"styled-components\";\nimport { rgba } from \"polished\";\nimport { Button } from \"cherry-styled-components\";\nimport { ArrowUp, LoaderPinwheel, RotateCcw, Sparkles, X } from \"lucide-react\";\nimport remarkGfm from \"remark-gfm\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport { MDXRemote, MDXRemoteSerializeResult } from \"next-mdx-remote\";\nimport { serialize } from \"next-mdx-remote/serialize\";\nimport Link from \"next/link\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { useLockBodyScroll } from \"@/components/LockBodyScroll\";\nimport { useMDXComponents as getMDXComponents } from \"@/components/MDXComponents\";\nimport {\n styledAnchor,\n styledTable,\n stylesLists,\n StyledSmallButton,\n interactiveStyles,\n} from \"@/components/layout/SharedStyled\";\n\nconst mdxComponents = getMDXComponents({});\n\nconst styledText = css<{ theme: Theme }>`\n font-size: ${({ theme }) => theme.fontSizes.text.xs};\n line-height: ${({ theme }) => theme.lineHeights.text.xs};\n\n ${mq(\"lg\")} {\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n line-height: ${({ theme }) => theme.lineHeights.small.lg};\n }\n`;\n\nconst StyledChat = styled.div<{ theme: Theme; $isVisible: boolean }>`\n margin: 0;\n position: fixed;\n top: 0;\n right: 0;\n width: 100%;\n height: calc(100dvh - 90px);\n overflow-y: scroll;\n overflow-x: hidden;\n z-index: 1000;\n padding: 0 20px;\n transition: all 0.3s ease;\n transform: translateX(0);\n background: ${({ theme }) => theme.colors.light};\n -webkit-overflow-scrolling: touch;\n opacity: 1;\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n ${({ $isVisible }) =>\n !$isVisible &&\n css`\n transform: translateX(100%);\n opacity: 0;\n `}\n\n ${mq(\"lg\")} {\n width: 420px;\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n }\n`;\n\nconst loadingAnimation = keyframes`\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n`;\n\nconst rotateGradient = keyframes`\n 0% {\n --gradient-angle: 0deg;\n }\n 100% {\n --gradient-angle: 360deg;\n }\n`;\n\nconst pulseGlow = keyframes`\n 0%, 100% {\n opacity: 0.5;\n filter: blur(16px);\n }\n 50% {\n opacity: 1;\n filter: blur(22px);\n }\n`;\n\nconst sparkleFloat = keyframes`\n 0%, 100% {\n opacity: 0;\n transform: translateY(0) scale(0);\n }\n 50% {\n opacity: 0.9;\n transform: translateY(-20px) scale(1);\n }\n`;\n\nconst shimmer = keyframes`\n 0% {\n background-position: 0% center;\n }\n 50% {\n background-position: 100% center;\n }\n 100% {\n background-position: 0% center;\n }\n`;\n\nconst StyledRainbowInputWrapper = styled.div<{\n theme: Theme;\n $isActive: boolean;\n}>`\n @property --gradient-angle {\n syntax: \"<angle>\";\n initial-value: 0deg;\n inherits: false;\n }\n\n position: relative;\n flex: 1;\n\n &::before {\n content: \"\";\n position: absolute;\n inset: -2px;\n border-radius: 14px;\n background: conic-gradient(\n from var(--gradient-angle),\n #cc5555,\n #d9a745,\n #3ab0cc,\n #cc7fc2,\n #4380cc,\n #4c1fa3,\n #cc5555\n );\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n animation: ${rotateGradient} 3s linear infinite;\n z-index: 0;\n }\n\n &::after {\n content: \"\";\n position: absolute;\n inset: -10px;\n border-radius: 20px;\n background: conic-gradient(\n from var(--gradient-angle),\n ${rgba(\"#ff6b6b\", 0.4)},\n ${rgba(\"#feca57\", 0.4)},\n ${rgba(\"#48dbfb\", 0.4)},\n ${rgba(\"#ff9ff3\", 0.4)},\n ${rgba(\"#54a0ff\", 0.4)},\n ${rgba(\"#5f27cd\", 0.4)},\n ${rgba(\"#ff6b6b\", 0.4)}\n );\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n animation:\n ${rotateGradient} 3s linear infinite,\n ${pulseGlow} 2s ease-in-out infinite;\n z-index: -1;\n pointer-events: none;\n }\n\n &:hover::before,\n &:focus-within::before {\n opacity: 1;\n }\n\n &:hover::after,\n &:focus-within::after {\n opacity: 1;\n }\n\n ${({ $isActive }) =>\n $isActive &&\n css`\n &::before {\n opacity: 1;\n }\n &::after {\n opacity: 1;\n }\n `}\n`;\n\nconst StyledSparkleContainer = styled.div<{ $isActive: boolean }>`\n position: absolute;\n inset: -30px;\n pointer-events: none;\n overflow: hidden;\n border-radius: 30px;\n z-index: -2;\n opacity: 0;\n transition: opacity 0.4s ease;\n\n ${({ $isActive }) =>\n $isActive &&\n css`\n opacity: 1;\n `}\n`;\n\nconst StyledSparkle = styled.div<{\n $color: string;\n $left: number;\n $top: number;\n $delay: number;\n}>`\n position: absolute;\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: ${({ $color }) => $color};\n box-shadow: 0 0 6px ${({ $color }) => $color};\n left: ${({ $left }) => $left}%;\n top: ${({ $top }) => $top}%;\n animation: ${sparkleFloat} 2s ease-in-out infinite;\n animation-delay: ${({ $delay }) => $delay}s;\n`;\n\nconst StyledRainbowInput = styled.input<{ theme: Theme }>`\n position: relative;\n z-index: 1;\n width: 100%;\n background: ${({ theme }) => theme.colors.light};\n border: 1px solid ${({ theme }) => theme.colors.grayLight};\n border-radius: 12px;\n padding: 14px 18px;\n font-size: ${({ theme }) => theme.fontSizes.text.lg};\n font-family: inherit;\n color: ${({ theme }) => theme.colors.dark};\n outline: none;\n transition:\n border-color 0.3s ease,\n box-shadow 0.3s ease;\n\n ${mq(\"lg\")} {\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n }\n\n &::placeholder {\n color: ${({ theme }) => rgba(theme.colors.dark, 0.4)};\n transition: color 0.3s ease;\n }\n\n &:focus::placeholder {\n color: ${({ theme }) => rgba(theme.colors.dark, 0.6)};\n }\n\n &:focus {\n border-color: transparent;\n }\n`;\n\nconst StyledRainbowButton = styled(Button)<{\n theme: Theme;\n $hasContent: boolean;\n}>`\n padding-top: 10px;\n padding-bottom: 10px;\n position: relative;\n overflow: hidden;\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;\n color: ${({ theme }) =>\n theme.isDark ? theme.colors.dark : theme.colors.light};\n\n &::before {\n content: \"\";\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 135deg,\n #ff6b6b,\n #feca57,\n #48dbfb,\n #ff9ff3,\n #54a0ff\n );\n background-size: 300% 300%;\n opacity: 0;\n transition: opacity 0.3s ease;\n z-index: 0;\n animation: ${shimmer} 3s linear infinite;\n width: 200%;\n }\n\n ${({ $hasContent }) =>\n $hasContent &&\n css`\n &::before {\n opacity: 1;\n }\n `}\n\n &:hover::before {\n opacity: 1;\n }\n\n &:hover {\n transform: scale(1.05);\n }\n\n &:active {\n transform: scale(0.95);\n }\n\n & svg {\n position: relative;\n z-index: 1;\n transition: transform 0.3s ease;\n }\n\n &:disabled,\n &:disabled:hover {\n background: ${({ theme }) => theme.colors.primaryDark};\n transform: none;\n box-shadow: none;\n\n &::before {\n opacity: 0;\n }\n }\n`;\n\nconst StyledChatForm = styled.form<{ theme: Theme; $isVisible: boolean }>`\n display: flex;\n gap: 10px;\n justify-content: center;\n align-items: center;\n background: ${({ theme }) => theme.colors.light};\n padding: 20px;\n position: fixed;\n bottom: 0;\n right: 0;\n z-index: 1000;\n width: 100%;\n border-top: solid 1px ${({ theme }) => theme.colors.grayLight};\n transition: all 0.3s ease;\n transform: translateX(100%);\n opacity: 0;\n\n ${mq(\"lg\")} {\n width: 420px;\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n }\n\n ${({ $isVisible }) =>\n $isVisible &&\n css`\n opacity: 1;\n transform: translateX(0);\n `}\n\n & .loading {\n animation: ${loadingAnimation} 1s linear infinite;\n }\n`;\n\nconst StyledGlowSmallButton = styled(StyledSmallButton)<{\n theme: Theme;\n $hasContent: boolean;\n}>`\n @property --gradient-angle {\n syntax: \"<angle>\";\n initial-value: 0deg;\n inherits: false;\n }\n\n position: relative;\n isolation: isolate;\n margin-right: 0;\n background: ${({ theme }) => theme.colors.light};\n padding: 0;\n\n &::before {\n content: \"\";\n inset: -2px;\n border-radius: 8px;\n background: conic-gradient(\n from var(--gradient-angle),\n #cc5555,\n #d9a745,\n #3ab0cc,\n #cc7fc2,\n #4380cc,\n #4c1fa3,\n #cc5555\n );\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n animation: ${rotateGradient} 3s linear infinite;\n z-index: -1;\n position: absolute;\n top: -2px;\n left: -2px;\n width: calc(100% + 4px);\n height: calc(100% + 4px);\n }\n\n &::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n border-radius: 14px;\n background: conic-gradient(\n from var(--gradient-angle),\n ${rgba(\"#ff6b6b\", 0.4)},\n ${rgba(\"#feca57\", 0.4)},\n ${rgba(\"#48dbfb\", 0.4)},\n ${rgba(\"#ff9ff3\", 0.4)},\n ${rgba(\"#54a0ff\", 0.4)},\n ${rgba(\"#5f27cd\", 0.4)},\n ${rgba(\"#ff6b6b\", 0.4)}\n );\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n animation:\n ${rotateGradient} 3s linear infinite,\n ${pulseGlow} 2s ease-in-out infinite;\n z-index: -2;\n pointer-events: none;\n }\n\n &:hover::before,\n &:hover::after {\n opacity: 1;\n }\n\n & span {\n padding: 6px 8px;\n display: flex;\n background: ${({ theme }) => theme.colors.light};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n gap: 6px;\n }\n\n ${({ $hasContent }) =>\n $hasContent &&\n css`\n &::before {\n opacity: 1;\n }\n &::after {\n opacity: 1;\n }\n `}\n`;\n\nconst StyledError = styled.div<{ theme: Theme }>`\n overflow-x: auto;\n background: ${({ theme }) => theme.colors.error};\n color: ${({ theme }) =>\n theme.isDark ? theme.colors.dark : theme.colors.light};\n padding: 10px;\n border-radius: 8px;\n margin: 20px 0;\n width: 100%;\n ${styledText};\n`;\n\nconst loadingDotAnimation = keyframes`\n 0% {\n opacity: 0;\n }\n 50% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n }\n`;\n\nconst StyledLoading = styled.div<{ theme: Theme }>`\n overflow-x: auto;\n margin: 20px 0;\n width: 100%;\n font-weight: 600;\n ${styledText};\n color: ${({ theme }) => theme.colors.dark};\n\n & span {\n &:nth-child(1) {\n animation: ${loadingDotAnimation} 1s ease infinite;\n }\n &:nth-child(2) {\n animation: ${loadingDotAnimation} 1s ease infinite;\n animation-delay: 0.2s;\n }\n &:nth-child(3) {\n animation: ${loadingDotAnimation} 1s ease infinite;\n animation-delay: 0.4s;\n }\n }\n`;\n\nconst StyledAnswer = styled.div<{ theme: Theme; $isAnswer: boolean }>`\n overflow-x: auto;\n background: ${({ theme }) => theme.colors.primary};\n color: ${({ theme }) =>\n theme.isDark ? theme.colors.dark : theme.colors.light};\n padding: 10px;\n border-radius: 8px;\n margin: 20px 0;\n width: 100%;\n ${styledText};\n\n & p {\n ${styledText};\n }\n\n ${({ $isAnswer }) =>\n $isAnswer &&\n css`\n background: transparent;\n color: ${({ theme }) => theme.colors.dark};\n padding: 0;\n `}\n\n & code:not([class]) {\n background: ${({ theme }) => rgba(theme.colors.primaryLight, 0.2)};\n color: ${({ theme }) => theme.colors.dark};\n padding: 2px 4px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n white-space: pre;\n }\n\n ${styledAnchor};\n ${stylesLists};\n ${styledTable};\n\n & pre,\n & .hljs {\n margin: 10px 0;\n }\n\n & .code-wrapper pre {\n margin: 0;\n ${styledText};\n }\n\n & > *:first-child {\n margin-top: 0;\n }\n\n & > *:last-child {\n margin-bottom: 0;\n\n & > *:last-child {\n margin-bottom: 0;\n }\n }\n\n & ul,\n & ol {\n & li {\n ${styledText};\n }\n }\n\n & ol {\n & > li {\n padding-left: 20px;\n\n &::before {\n position: absolute;\n top: 0;\n left: 0;\n }\n }\n }\n\n & img,\n & video,\n & iframe {\n max-width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n margin: 10px 0;\n display: block;\n }\n\n & h1,\n & h2,\n & h3,\n & h4,\n & h5,\n & h6 {\n margin: 10px 0;\n padding: 0;\n }\n`;\n\nconst StyledSources = styled.div`\n display: flex;\n gap: 16px;\n flex-wrap: wrap;\n margin: -5px 0 20px;\n`;\n\nconst StyledSourceLink = styled(Link)<{ theme: Theme }>`\n position: relative;\n text-decoration: none;\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n line-height: 1;\n color: ${({ theme }) => theme.colors.primary};\n display: flex;\n gap: 6px;\n transition: all 0.3s ease;\n font-weight: 600;\n white-space: nowrap;\n min-width: fit-content;\n background: ${({ theme }) => rgba(theme.colors.primaryLight, 0.1)};\n padding: 6px 8px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n ${interactiveStyles};\n\n & * {\n margin: auto 0;\n }\n\n &:hover {\n color: ${({ theme }) =>\n theme.isDark ? theme.colors.primaryLight : theme.colors.primaryDark};\n }\n`;\n\nconst StyledChatTitle = styled.div<{ theme: Theme }>`\n display: flex;\n flex-wrap: nowrap;\n justify-content: space-between;\n position: sticky;\n margin: 0 -20px;\n padding: 16px 20px;\n height: 62px;\n top: 0;\n background: ${({ theme }) => theme.colors.light};\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n z-index: 1000;\n`;\n\nconst StyledChatTitleIconWrapper = styled.span<{ theme: Theme }>`\n display: flex;\n align-items: center;\n gap: 12px;\n color: ${({ theme }) => theme.colors.dark};\n`;\n\nconst StyledChatCloseButton = styled.button<{ theme: Theme }>`\n background: transparent;\n border: none;\n cursor: pointer;\n padding: 0;\n margin: 0;\n color: ${({ theme }) => theme.colors.primary};\n\n &:hover {\n color: ${({ theme }) => theme.colors.primaryDark};\n transform: scale(1.05);\n }\n\n &:active {\n transform: scale(0.95);\n }\n`;\n\ntype Source = {\n id: string;\n path: string;\n uri: string;\n score: number;\n};\n\ntype Answer = {\n text: string;\n answer?: boolean;\n mdx?: MDXRemoteSerializeResult;\n sources?: Source[];\n};\n\nconst SPARKLE_COLORS = [\n \"#ff6b6b\",\n \"#feca57\",\n \"#48dbfb\",\n \"#ff9ff3\",\n \"#54a0ff\",\n \"#5f27cd\",\n];\n\n// Deterministic sparkle positions to avoid hydration mismatch\nconst SPARKLE_POSITIONS = [\n { left: 8, top: 35 },\n { left: 17, top: 55 },\n { left: 26, top: 28 },\n { left: 35, top: 68 },\n { left: 44, top: 42 },\n { left: 53, top: 75 },\n { left: 62, top: 32 },\n { left: 71, top: 58 },\n { left: 80, top: 45 },\n { left: 89, top: 65 },\n];\n\ninterface RainbowInputProps {\n id?: string;\n value: string;\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n placeholder?: string;\n autoComplete?: string;\n \"aria-label\"?: string;\n inputRef?: React.Ref<HTMLInputElement>;\n}\n\nfunction RainbowInput({\n id,\n value,\n onChange,\n placeholder,\n autoComplete,\n \"aria-label\": ariaLabel,\n inputRef,\n}: RainbowInputProps) {\n const [isFocused, setIsFocused] = useState(false);\n const [isHovered, setIsHovered] = useState(false);\n const isActive = isFocused || isHovered;\n\n const sparkles = SPARKLE_POSITIONS.map((pos, i) => ({\n color: SPARKLE_COLORS[i % SPARKLE_COLORS.length],\n left: pos.left,\n top: pos.top,\n delay: i * 0.12,\n }));\n\n return (\n <StyledRainbowInputWrapper\n $isActive={isActive}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <StyledSparkleContainer $isActive={isActive}>\n {sparkles.map((sparkle, i) => (\n <StyledSparkle\n key={i}\n $color={sparkle.color}\n $left={sparkle.left}\n $top={sparkle.top}\n $delay={sparkle.delay}\n />\n ))}\n </StyledSparkleContainer>\n <StyledRainbowInput\n ref={inputRef}\n id={id}\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n autoComplete={autoComplete}\n aria-label={ariaLabel}\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n />\n </StyledRainbowInputWrapper>\n );\n}\n\nfunction ChatButtonCTA() {\n const { setIsOpen, isOpen, answer, setAnswer, chatInputRef } =\n useContext(ChatContext);\n\n return (\n <StyledGlowSmallButton\n onClick={() => {\n const next = !isOpen;\n setIsOpen(next);\n if (next) {\n if (answer.length === 0) {\n setAnswer([\n { text: \"Hey there, how can I assist you?\", answer: true },\n ]);\n }\n setTimeout(() => {\n chatInputRef.current?.focus();\n }, 350);\n }\n }}\n aria-label=\"Ask AI Assistant\"\n $hasContent={isOpen}\n type=\"button\"\n >\n <span>\n <Sparkles size={16} />\n Ask AI\n </span>\n </StyledGlowSmallButton>\n );\n}\n\nfunction Chat() {\n const {\n isOpen,\n question,\n setQuestion,\n loading,\n error,\n answer,\n ask,\n closeChat,\n resetChat,\n chatInputRef,\n } = useContext(ChatContext);\n const endRef = useRef<HTMLDivElement | null>(null);\n\n useLockBodyScroll(isOpen);\n\n useEffect(() => {\n endRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"end\" });\n }, [answer]);\n\n useEffect(() => {\n if (answer?.length > 0) {\n chatInputRef.current?.focus();\n }\n }, [answer, chatInputRef]);\n\n return (\n <>\n <StyledChat $isVisible={isOpen}>\n <StyledChatTitle>\n <StyledChatTitleIconWrapper>\n <Sparkles />\n <h3>AI Assistant</h3>\n </StyledChatTitleIconWrapper>\n <StyledChatTitleIconWrapper>\n <StyledChatCloseButton\n onClick={resetChat}\n aria-label=\"Reset chat history\"\n title=\"Reset chat history\"\n >\n <RotateCcw size={18} />\n </StyledChatCloseButton>\n <StyledChatCloseButton\n onClick={closeChat}\n aria-label=\"Close chat\"\n title=\"Close chat\"\n >\n <X />\n </StyledChatCloseButton>\n </StyledChatTitleIconWrapper>\n </StyledChatTitle>\n {answer &&\n answer.map((a, i) => (\n <React.Fragment key={i}>\n <StyledAnswer $isAnswer={a.answer ?? false}>\n {a.answer && a.mdx ? (\n <MDXRemote {...a.mdx} components={mdxComponents} />\n ) : (\n a.text\n )}\n </StyledAnswer>\n {a.answer && a.sources && a.sources.length > 0 && (\n <StyledSources>\n {a.sources.map((src) => {\n const slug = src.uri\n .replace(\"docs://\", \"\")\n .replace(/^\\/+/, \"\");\n const href = slug ? `/${slug}/` : \"/\";\n const label = slug\n ? slug\n .split(\"/\")\n .pop()!\n .replace(/-/g, \" \")\n .replace(/\\b\\w/g, (c: string) => c.toUpperCase())\n : \"Home\";\n return (\n <StyledSourceLink\n key={src.id}\n href={href}\n onClick={() => {\n if (window.innerWidth <= 992) {\n closeChat();\n }\n }}\n >\n {label}\n </StyledSourceLink>\n );\n })}\n </StyledSources>\n )}\n </React.Fragment>\n ))}\n {loading && (\n <StyledLoading>\n Answering<span>.</span>\n <span>.</span>\n <span>.</span>\n </StyledLoading>\n )}\n {error && (\n <StyledError>\n <strong>Error:</strong> {error}\n </StyledError>\n )}\n <div ref={endRef} />\n </StyledChat>\n\n <StyledChatForm onSubmit={ask} $isVisible={isOpen}>\n <RainbowInput\n id=\"chat-bottom-input\"\n inputRef={chatInputRef}\n value={question}\n onChange={(e) => setQuestion(e.target.value)}\n placeholder=\"Ask AI Assistant...\"\n autoComplete=\"off\"\n aria-label=\"Ask a follow-up question\"\n />\n <StyledRainbowButton\n type=\"submit\"\n disabled={loading || question.trim() === \"\"}\n $hasContent={question.trim().length > 0}\n aria-label={loading ? \"Loading response\" : \"Submit question\"}\n >\n {loading ? <LoaderPinwheel className=\"loading\" /> : <ArrowUp />}\n </StyledRainbowButton>\n </StyledChatForm>\n </>\n );\n}\n\nconst ChatContext = createContext<{\n isOpen: boolean;\n setIsOpen: (isOpen: boolean) => void;\n isChatActive: boolean;\n question: string;\n setQuestion: (q: string) => void;\n loading: boolean;\n error: string | null;\n answer: Answer[];\n setAnswer: (answers: Answer[]) => void;\n ask: (e: React.FormEvent) => void;\n closeChat: () => void;\n resetChat: () => void;\n chatInputRef: React.RefObject<HTMLInputElement | null>;\n}>({\n isOpen: false,\n setIsOpen: () => {},\n isChatActive: false,\n question: \"\",\n setQuestion: () => {},\n loading: false,\n error: null,\n answer: [],\n setAnswer: () => {},\n ask: () => {},\n closeChat: () => {},\n resetChat: () => {},\n chatInputRef: { current: null },\n});\n\ninterface ChatContextProviderProps {\n children: React.ReactNode;\n isChatActive: boolean;\n}\n\nconst ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {\n const [isOpen, setIsOpen] = useState(false);\n const [question, setQuestion] = useState(\"\");\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [answer, setAnswer] = useState<Answer[]>([]);\n const abortRef = useRef<AbortController | null>(null);\n const chatInputRef = useRef<HTMLInputElement | null>(null);\n\n async function ask(e: React.FormEvent) {\n e.preventDefault();\n if (loading || question.trim() === \"\") return;\n const currentQuestion = question;\n setQuestion(\"\");\n setIsOpen(true);\n setLoading(true);\n setError(null);\n\n const mergedQuestions =\n answer.length > 0\n ? [...answer, { text: currentQuestion, answer: false }]\n : [{ text: currentQuestion, answer: false }];\n\n setAnswer(mergedQuestions);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n try {\n const history = answer\n .filter((a) => a.text.trim() !== \"\")\n .map((a) => ({\n role: a.answer ? (\"assistant\" as const) : (\"user\" as const),\n content: a.text,\n }));\n\n const res = await fetch(\"/api/rag\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ question: currentQuestion, history }),\n signal: controller.signal,\n });\n\n if (!res.ok) {\n const errorData = await res.json();\n throw new Error(errorData.error || \"Request failed\");\n }\n\n const reader = res.body?.getReader();\n const decoder = new TextDecoder();\n const contentParts: string[] = [];\n let sources: Source[] = [];\n if (!reader) {\n throw new Error(\"Failed to get response reader\");\n }\n\n const streamingAnswerIndex = mergedQuestions.length;\n setAnswer([...mergedQuestions, { text: \"\", answer: true }]);\n\n let buffer = \"\";\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const parts = buffer.split(\"\\n\");\n buffer = parts.pop() ?? \"\";\n\n for (const line of parts) {\n if (line.startsWith(\"data: \")) {\n try {\n const data = JSON.parse(line.slice(6));\n\n if (data.type === \"metadata\") {\n const allSources: Source[] = data.data?.sources ?? [];\n const seen = new Set<string>();\n sources = allSources.filter((s: Source) => {\n if (s.score < 0.4 || seen.has(s.uri)) return false;\n seen.add(s.uri);\n return true;\n });\n } else if (data.type === \"content\") {\n contentParts.push(data.data);\n const streamedContent = contentParts.join(\"\");\n\n setAnswer((prev) => {\n const newAnswers = [...prev];\n newAnswers[streamingAnswerIndex] = {\n text: streamedContent,\n answer: true,\n sources,\n };\n return newAnswers;\n });\n } else if (data.type === \"error\") {\n throw new Error(data.data);\n } else if (data.type === \"done\") {\n const streamedContent = contentParts.join(\"\");\n let mdxSource: MDXRemoteSerializeResult | null = null;\n try {\n mdxSource = await serialize(streamedContent, {\n parseFrontmatter: false,\n mdxOptions: {\n remarkPlugins: [remarkGfm],\n rehypePlugins: [rehypeHighlight],\n format: \"md\",\n development: false,\n },\n });\n } catch (mdxError: unknown) {\n console.error(\"MDX serialization error:\", mdxError);\n }\n\n setAnswer((prev) => {\n const newAnswers = [...prev];\n newAnswers[streamingAnswerIndex] = {\n text: streamedContent,\n answer: true,\n mdx: mdxSource || undefined,\n sources,\n };\n return newAnswers;\n });\n }\n } catch (parseError) {\n if (\n parseError instanceof Error &&\n parseError.message !== \"Unknown error\"\n ) {\n console.error(\"Failed to parse SSE data:\", parseError);\n }\n }\n }\n }\n }\n } catch (err: unknown) {\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setError(err instanceof Error ? err.message : \"Unknown error\");\n } finally {\n abortRef.current = null;\n setLoading(false);\n }\n }\n\n function closeChat() {\n setIsOpen(false);\n }\n\n function resetChat() {\n abortRef.current?.abort();\n setLoading(false);\n setError(null);\n setAnswer([{ text: \"Hey there, how can I assist you?\", answer: true }]);\n }\n\n return (\n <ChatContext.Provider\n value={{\n isOpen,\n setIsOpen,\n isChatActive,\n question,\n setQuestion,\n loading,\n error,\n answer,\n setAnswer,\n ask,\n closeChat,\n resetChat,\n chatInputRef,\n }}\n >\n {children}\n </ChatContext.Provider>\n );\n};\n\nexport { Chat, ChtProvider, ChatContext, ChatButtonCTA };\n";
|
|
@@ -892,7 +892,15 @@ function Chat() {
|
|
|
892
892
|
.replace(/\\b\\w/g, (c: string) => c.toUpperCase())
|
|
893
893
|
: "Home";
|
|
894
894
|
return (
|
|
895
|
-
<StyledSourceLink
|
|
895
|
+
<StyledSourceLink
|
|
896
|
+
key={src.id}
|
|
897
|
+
href={href}
|
|
898
|
+
onClick={() => {
|
|
899
|
+
if (window.innerWidth <= 992) {
|
|
900
|
+
closeChat();
|
|
901
|
+
}
|
|
902
|
+
}}
|
|
903
|
+
>
|
|
896
904
|
{label}
|
|
897
905
|
</StyledSourceLink>
|
|
898
906
|
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const postHogProviderTemplate = "\"use client\";\n\nimport posthog from \"posthog-js\";\nimport { PostHogProvider as PHProvider } from \"posthog-js/react\";\nimport { Suspense, useEffect, useRef, useState } from \"react\";\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport rawAnalyticsConfig from \"@/analytics.json\";\n\ninterface AnalyticsConfig {\n provider?: string;\n posthog?: {\n key?: string;\n host?: string;\n };\n}\n\nconst analyticsConfig = rawAnalyticsConfig as AnalyticsConfig;\n\nconst posthogKey =\n analyticsConfig?.provider === \"posthog\" ? analyticsConfig.posthog?.key : null;\n\nfunction PostHogInit({ onReady }: { onReady: () => void }) {\n const initRef = useRef(false);\n\n useEffect(() => {\n if (initRef.current || !posthogKey) return;\n initRef.current = true;\n\n posthog.init(posthogKey, {\n api_host: \"/ingest\",\n ui_host: analyticsConfig.posthog?.host || \"https://us.posthog.com\",\n capture_pageview: false,\n capture_pageleave: true,\n loaded: onReady,\n });\n }, [onReady]);\n\n return null;\n}\n\nfunction PostHogPageviewTracker() {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n useEffect(() => {\n if (pathname) {\n const url = searchParams?.size\n ? `${pathname}?${searchParams.toString()}`\n : pathname;\n posthog.capture(\"$pageview\", { $current_url: url });\n }\n }, [pathname, searchParams]);\n\n return null;\n}\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n const [ready, setReady] = useState(false);\n\n if (!posthogKey) {\n return <>{children}</>;\n }\n\n return (\n <PHProvider client={posthog}>\n <PostHogInit onReady={() => setReady(true)} />\n <Suspense fallback={null}>{ready && <PostHogPageviewTracker />}</Suspense>\n {children}\n </PHProvider>\n );\n}\n";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const postHogProviderTemplate = `"use client";
|
|
2
|
+
|
|
3
|
+
import posthog from "posthog-js";
|
|
4
|
+
import { PostHogProvider as PHProvider } from "posthog-js/react";
|
|
5
|
+
import { Suspense, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { usePathname, useSearchParams } from "next/navigation";
|
|
7
|
+
import rawAnalyticsConfig from "@/analytics.json";
|
|
8
|
+
|
|
9
|
+
interface AnalyticsConfig {
|
|
10
|
+
provider?: string;
|
|
11
|
+
posthog?: {
|
|
12
|
+
key?: string;
|
|
13
|
+
host?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const analyticsConfig = rawAnalyticsConfig as AnalyticsConfig;
|
|
18
|
+
|
|
19
|
+
const posthogKey =
|
|
20
|
+
analyticsConfig?.provider === "posthog" ? analyticsConfig.posthog?.key : null;
|
|
21
|
+
|
|
22
|
+
function PostHogInit({ onReady }: { onReady: () => void }) {
|
|
23
|
+
const initRef = useRef(false);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (initRef.current || !posthogKey) return;
|
|
27
|
+
initRef.current = true;
|
|
28
|
+
|
|
29
|
+
posthog.init(posthogKey, {
|
|
30
|
+
api_host: "/ingest",
|
|
31
|
+
ui_host: analyticsConfig.posthog?.host || "https://us.posthog.com",
|
|
32
|
+
capture_pageview: false,
|
|
33
|
+
capture_pageleave: true,
|
|
34
|
+
loaded: onReady,
|
|
35
|
+
});
|
|
36
|
+
}, [onReady]);
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function PostHogPageviewTracker() {
|
|
42
|
+
const pathname = usePathname();
|
|
43
|
+
const searchParams = useSearchParams();
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (pathname) {
|
|
47
|
+
const url = searchParams?.size
|
|
48
|
+
? \`\${pathname}?\${searchParams.toString()}\`
|
|
49
|
+
: pathname;
|
|
50
|
+
posthog.capture("$pageview", { $current_url: url });
|
|
51
|
+
}
|
|
52
|
+
}, [pathname, searchParams]);
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|
58
|
+
const [ready, setReady] = useState(false);
|
|
59
|
+
|
|
60
|
+
if (!posthogKey) {
|
|
61
|
+
return <>{children}</>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<PHProvider client={posthog}>
|
|
66
|
+
<PostHogInit onReady={() => setReady(true)} />
|
|
67
|
+
<Suspense fallback={null}>{ready && <PostHogPageviewTracker />}</Suspense>
|
|
68
|
+
{children}
|
|
69
|
+
</PHProvider>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
`;
|