doccupine 0.0.51 → 0.0.52

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.
Files changed (78) hide show
  1. package/dist/index.js +2 -0
  2. package/dist/templates/app/api/mcp/route.d.ts +1 -1
  3. package/dist/templates/app/api/mcp/route.js +43 -8
  4. package/dist/templates/app/api/rag/route.d.ts +1 -1
  5. package/dist/templates/app/api/rag/route.js +27 -1
  6. package/dist/templates/app/api/theme/routes.d.ts +1 -1
  7. package/dist/templates/app/api/theme/routes.js +1 -1
  8. package/dist/templates/app/error.d.ts +1 -0
  9. package/dist/templates/app/error.js +45 -0
  10. package/dist/templates/app/layout.js +6 -3
  11. package/dist/templates/components/Chat.d.ts +1 -1
  12. package/dist/templates/components/Chat.js +5 -10
  13. package/dist/templates/components/Docs.d.ts +1 -1
  14. package/dist/templates/components/Docs.js +1 -1
  15. package/dist/templates/components/DocsSideBar.d.ts +1 -1
  16. package/dist/templates/components/DocsSideBar.js +1 -1
  17. package/dist/templates/components/MDXComponents.d.ts +1 -1
  18. package/dist/templates/components/MDXComponents.js +20 -10
  19. package/dist/templates/components/SideBar.d.ts +1 -1
  20. package/dist/templates/components/SideBar.js +3 -3
  21. package/dist/templates/components/layout/Accordion.d.ts +1 -1
  22. package/dist/templates/components/layout/Accordion.js +1 -1
  23. package/dist/templates/components/layout/ActionBar.d.ts +1 -1
  24. package/dist/templates/components/layout/ActionBar.js +1 -1
  25. package/dist/templates/components/layout/Button.d.ts +1 -1
  26. package/dist/templates/components/layout/Button.js +3 -2
  27. package/dist/templates/components/layout/Callout.d.ts +1 -1
  28. package/dist/templates/components/layout/Callout.js +1 -1
  29. package/dist/templates/components/layout/Card.d.ts +1 -1
  30. package/dist/templates/components/layout/Card.js +1 -1
  31. package/dist/templates/components/layout/Code.d.ts +1 -1
  32. package/dist/templates/components/layout/Code.js +1 -1
  33. package/dist/templates/components/layout/Columns.d.ts +1 -1
  34. package/dist/templates/components/layout/Columns.js +1 -1
  35. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  36. package/dist/templates/components/layout/DocsComponents.js +1 -1
  37. package/dist/templates/components/layout/DocsNavigation.d.ts +1 -1
  38. package/dist/templates/components/layout/DocsNavigation.js +2 -2
  39. package/dist/templates/components/layout/Field.d.ts +1 -1
  40. package/dist/templates/components/layout/Field.js +1 -1
  41. package/dist/templates/components/layout/Footer.d.ts +1 -1
  42. package/dist/templates/components/layout/Footer.js +1 -1
  43. package/dist/templates/components/layout/Header.d.ts +1 -1
  44. package/dist/templates/components/layout/Header.js +4 -4
  45. package/dist/templates/components/layout/Pictograms.d.ts +1 -1
  46. package/dist/templates/components/layout/Pictograms.js +1 -1
  47. package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
  48. package/dist/templates/components/layout/SharedStyles.js +1 -1
  49. package/dist/templates/components/layout/Steps.d.ts +1 -1
  50. package/dist/templates/components/layout/Steps.js +3 -3
  51. package/dist/templates/components/layout/Tabs.d.ts +1 -1
  52. package/dist/templates/components/layout/Tabs.js +1 -1
  53. package/dist/templates/components/layout/ThemeToggle.d.ts +1 -1
  54. package/dist/templates/components/layout/ThemeToggle.js +1 -1
  55. package/dist/templates/components/layout/Typography.d.ts +1 -1
  56. package/dist/templates/components/layout/Typography.js +1 -1
  57. package/dist/templates/components/layout/Update.d.ts +1 -1
  58. package/dist/templates/components/layout/Update.js +1 -1
  59. package/dist/templates/eslint.config.d.ts +1 -1
  60. package/dist/templates/eslint.config.js +27 -1
  61. package/dist/templates/mdx/model-context-protocol.mdx.d.ts +1 -1
  62. package/dist/templates/mdx/model-context-protocol.mdx.js +25 -0
  63. package/dist/templates/package.js +2 -2
  64. package/dist/templates/proxy.d.ts +1 -1
  65. package/dist/templates/proxy.js +18 -0
  66. package/dist/templates/services/llm/config.d.ts +1 -1
  67. package/dist/templates/services/llm/config.js +1 -1
  68. package/dist/templates/services/llm/factory.d.ts +1 -1
  69. package/dist/templates/services/llm/factory.js +1 -1
  70. package/dist/templates/services/llm/types.d.ts +1 -1
  71. package/dist/templates/services/llm/types.js +1 -1
  72. package/dist/templates/services/mcp/tools.d.ts +1 -1
  73. package/dist/templates/services/mcp/tools.js +10 -3
  74. package/dist/templates/utils/orderNavItems.d.ts +1 -1
  75. package/dist/templates/utils/orderNavItems.js +3 -1
  76. package/dist/templates/utils/rateLimit.d.ts +1 -0
  77. package/dist/templates/utils/rateLimit.js +44 -0
  78. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ import { llmIndexTemplate } from "./templates/services/llm/index.js";
61
61
  import { llmTypesTemplate } from "./templates/services/llm/types.js";
62
62
  import { styledDTemplate } from "./templates/types/styled.js";
63
63
  import { orderNavItemsTemplate } from "./templates/utils/orderNavItems.js";
64
+ import { rateLimitTemplate } from "./templates/utils/rateLimit.js";
64
65
  import { accordionMdxTemplate } from "./templates/mdx/accordion.mdx.js";
65
66
  import { aiAssistantMdxTemplate } from "./templates/mdx/ai-assistant.mdx.js";
66
67
  import { buttonsMdxTemplate } from "./templates/mdx/buttons.mdx.js";
@@ -253,6 +254,7 @@ class MDXToNextJSGenerator {
253
254
  "services/llm/types.ts": llmTypesTemplate,
254
255
  "types/styled.d.ts": styledDTemplate,
255
256
  "utils/orderNavItems.ts": orderNavItemsTemplate,
257
+ "utils/rateLimit.ts": rateLimitTemplate,
256
258
  "components/Chat.tsx": chatTemplate,
257
259
  "components/ClickOutside.ts": clickOutsideTemplate,
258
260
  "components/Docs.tsx": docsTemplate,
@@ -1 +1 @@
1
- export declare const mcpRoutesTemplate = "import { NextResponse } from \"next/server\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\nimport { createMCPServer } from \"@/services/mcp/server\";\nimport {\n searchDocs,\n ensureDocsIndex,\n getIndexStatus,\n DOCS_TOOLS,\n listDocs,\n getDoc,\n} from \"@/services/mcp\";\nimport type { MCPToolName } from \"@/services/mcp\";\n\n// Create a stateless transport for serverless environment\nfunction createTransport() {\n return new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // Stateless mode for serverless\n enableJsonResponse: true,\n });\n}\n\n// Handle MCP protocol requests\nasync function handleMCPRequest(req: Request) {\n const transport = createTransport();\n const server = createMCPServer();\n\n try {\n await server.connect(transport);\n const response = await transport.handleRequest(req);\n\n // Clean up after response is done\n response\n .clone()\n .body?.pipeTo(\n new WritableStream({\n close() {\n transport.close();\n server.close();\n },\n }),\n )\n .catch(() => {});\n\n return response;\n } catch (error) {\n console.error(\"MCP request error:\", error);\n return new Response(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32603,\n message: \"Internal server error\",\n },\n id: null,\n }),\n {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n },\n );\n }\n}\n\n// Handle REST API requests (original JSON format)\ninterface ToolCallRequest {\n tool: MCPToolName;\n params: Record<string, unknown>;\n}\n\nasync function handleRESTRequest(body: ToolCallRequest) {\n try {\n const { tool, params } = body;\n\n if (!tool) {\n return NextResponse.json(\n { error: \"Missing 'tool' parameter\" },\n { status: 400 },\n );\n }\n\n switch (tool) {\n case \"search_docs\": {\n const query = String(params?.query || \"\");\n const limit = typeof params?.limit === \"number\" ? params.limit : 6;\n await ensureDocsIndex();\n const results = await searchDocs(query, limit);\n return NextResponse.json({\n content: results.map(({ chunk, score }) => ({\n path: chunk.path,\n uri: chunk.uri,\n score: score.toFixed(3),\n text: chunk.text,\n })),\n });\n }\n\n case \"get_doc\": {\n const path = String(params?.path || \"\");\n const doc = await getDoc({ path });\n if (!doc) {\n return NextResponse.json(\n { error: \"Document not found\" },\n { status: 404 },\n );\n }\n return NextResponse.json({ content: doc });\n }\n\n case \"list_docs\": {\n const directory =\n typeof params?.directory === \"string\" ? params.directory : undefined;\n const docs = await listDocs({ directory });\n return NextResponse.json({\n content: docs.map((d) => ({\n name: d.name,\n path: d.path,\n uri: d.uri,\n })),\n });\n }\n\n default:\n return NextResponse.json(\n { error: `Unknown tool: ${tool}` },\n { status: 400 },\n );\n }\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : \"Unknown error\";\n return NextResponse.json({ error: message }, { status: 500 });\n }\n}\n\nexport async function POST(req: Request) {\n // Clone the request to read body twice if needed\n const clonedReq = req.clone();\n\n try {\n const body = await clonedReq.json();\n\n // Check if this is an MCP protocol request (has jsonrpc field)\n // or a REST API request (has tool field)\n if (\"jsonrpc\" in body) {\n // MCP protocol request - use the original request\n return handleMCPRequest(req);\n } else if (\"tool\" in body) {\n // REST API request\n return handleRESTRequest(body as ToolCallRequest);\n } else {\n return NextResponse.json(\n {\n error:\n \"Invalid request format. Expected 'jsonrpc' (MCP) or 'tool' (REST) field.\",\n },\n { status: 400 },\n );\n }\n } catch {\n return NextResponse.json({ error: \"Invalid JSON body\" }, { status: 400 });\n }\n}\n\nexport async function GET() {\n // GET always returns REST API format (tools list and index status)\n const status = getIndexStatus();\n return NextResponse.json({\n tools: DOCS_TOOLS,\n index: {\n ready: status.ready,\n chunkCount: status.chunkCount,\n },\n });\n}\n\nexport async function DELETE(req: Request) {\n // DELETE is only used by MCP protocol\n return handleMCPRequest(req);\n}\n";
1
+ export declare const mcpRoutesTemplate = "import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\nimport { createMCPServer } from \"@/services/mcp/server\";\nimport {\n searchDocs,\n ensureDocsIndex,\n getIndexStatus,\n DOCS_TOOLS,\n listDocs,\n getDoc,\n} from \"@/services/mcp\";\nimport type { MCPToolName } from \"@/services/mcp\";\n\nconst searchDocsSchema = z.object({\n query: z.string().min(1).max(2000),\n limit: z.number().int().min(1).max(50).optional(),\n});\n\nconst getDocSchema = z.object({\n path: z.string().min(1).max(500),\n});\n\nconst listDocsSchema = z.object({\n directory: z.string().max(500).optional(),\n});\n\n// Create a stateless transport for serverless environment\nfunction createTransport() {\n return new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // Stateless mode for serverless\n enableJsonResponse: true,\n });\n}\n\n// Handle MCP protocol requests\nasync function handleMCPRequest(req: Request) {\n const transport = createTransport();\n const server = createMCPServer();\n\n try {\n await server.connect(transport);\n const response = await transport.handleRequest(req);\n\n // Clean up after response is done\n response\n .clone()\n .body?.pipeTo(\n new WritableStream({\n close() {\n transport.close();\n server.close();\n },\n }),\n )\n .catch(() => {});\n\n return response;\n } catch (error) {\n console.error(\"MCP request error:\", error);\n return new Response(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32603,\n message: \"Internal server error\",\n },\n id: null,\n }),\n {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n },\n );\n }\n}\n\n// Handle REST API requests (original JSON format)\ninterface ToolCallRequest {\n tool: MCPToolName;\n params: Record<string, unknown>;\n}\n\nasync function handleRESTRequest(body: ToolCallRequest) {\n try {\n const { tool, params } = body;\n\n if (!tool) {\n return NextResponse.json(\n { error: \"Missing 'tool' parameter\" },\n { status: 400 },\n );\n }\n\n switch (tool) {\n case \"search_docs\": {\n const parsed = searchDocsSchema.safeParse(params);\n if (!parsed.success) {\n return NextResponse.json(\n { error: \"Invalid params\", details: parsed.error.issues },\n { status: 400 },\n );\n }\n await ensureDocsIndex();\n const results = await searchDocs(\n parsed.data.query,\n parsed.data.limit ?? 6,\n );\n return NextResponse.json({\n content: results.map(({ chunk, score }) => ({\n path: chunk.path,\n uri: chunk.uri,\n score: score.toFixed(3),\n text: chunk.text,\n })),\n });\n }\n\n case \"get_doc\": {\n const parsed = getDocSchema.safeParse(params);\n if (!parsed.success) {\n return NextResponse.json(\n { error: \"Invalid params\", details: parsed.error.issues },\n { status: 400 },\n );\n }\n const doc = await getDoc({ path: parsed.data.path });\n if (!doc) {\n return NextResponse.json(\n { error: \"Document not found\" },\n { status: 404 },\n );\n }\n return NextResponse.json({ content: doc });\n }\n\n case \"list_docs\": {\n const parsed = listDocsSchema.safeParse(params);\n if (!parsed.success) {\n return NextResponse.json(\n { error: \"Invalid params\", details: parsed.error.issues },\n { status: 400 },\n );\n }\n const docs = await listDocs({\n directory: parsed.data.directory,\n });\n return NextResponse.json({\n content: docs.map((d) => ({\n name: d.name,\n path: d.path,\n uri: d.uri,\n })),\n });\n }\n\n default:\n return NextResponse.json(\n { error: `Unknown tool: ${tool}` },\n { status: 400 },\n );\n }\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : \"Unknown error\";\n return NextResponse.json({ error: message }, { status: 500 });\n }\n}\n\nexport async function POST(req: Request) {\n // Clone the request to read body twice if needed\n const clonedReq = req.clone();\n\n try {\n const body = await clonedReq.json();\n\n // Check if this is an MCP protocol request (has jsonrpc field)\n // or a REST API request (has tool field)\n if (\"jsonrpc\" in body) {\n // MCP protocol request - use the original request\n return handleMCPRequest(req);\n } else if (\"tool\" in body) {\n // REST API request\n return handleRESTRequest(body as ToolCallRequest);\n } else {\n return NextResponse.json(\n {\n error:\n \"Invalid request format. Expected 'jsonrpc' (MCP) or 'tool' (REST) field.\",\n },\n { status: 400 },\n );\n }\n } catch {\n return NextResponse.json({ error: \"Invalid JSON body\" }, { status: 400 });\n }\n}\n\nexport async function GET() {\n // GET always returns REST API format (tools list and index status)\n const status = getIndexStatus();\n return NextResponse.json({\n tools: DOCS_TOOLS,\n index: {\n ready: status.ready,\n chunkCount: status.chunkCount,\n },\n });\n}\n\nexport async function DELETE(req: Request) {\n // DELETE is only used by MCP protocol\n return handleMCPRequest(req);\n}\n";
@@ -1,4 +1,5 @@
1
1
  export const mcpRoutesTemplate = `import { NextResponse } from "next/server";
2
+ import { z } from "zod";
2
3
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
4
  import { createMCPServer } from "@/services/mcp/server";
4
5
  import {
@@ -11,6 +12,19 @@ import {
11
12
  } from "@/services/mcp";
12
13
  import type { MCPToolName } from "@/services/mcp";
13
14
 
15
+ const searchDocsSchema = z.object({
16
+ query: z.string().min(1).max(2000),
17
+ limit: z.number().int().min(1).max(50).optional(),
18
+ });
19
+
20
+ const getDocSchema = z.object({
21
+ path: z.string().min(1).max(500),
22
+ });
23
+
24
+ const listDocsSchema = z.object({
25
+ directory: z.string().max(500).optional(),
26
+ });
27
+
14
28
  // Create a stateless transport for serverless environment
15
29
  function createTransport() {
16
30
  return new WebStandardStreamableHTTPServerTransport({
@@ -80,10 +94,18 @@ async function handleRESTRequest(body: ToolCallRequest) {
80
94
 
81
95
  switch (tool) {
82
96
  case "search_docs": {
83
- const query = String(params?.query || "");
84
- const limit = typeof params?.limit === "number" ? params.limit : 6;
97
+ const parsed = searchDocsSchema.safeParse(params);
98
+ if (!parsed.success) {
99
+ return NextResponse.json(
100
+ { error: "Invalid params", details: parsed.error.issues },
101
+ { status: 400 },
102
+ );
103
+ }
85
104
  await ensureDocsIndex();
86
- const results = await searchDocs(query, limit);
105
+ const results = await searchDocs(
106
+ parsed.data.query,
107
+ parsed.data.limit ?? 6,
108
+ );
87
109
  return NextResponse.json({
88
110
  content: results.map(({ chunk, score }) => ({
89
111
  path: chunk.path,
@@ -95,8 +117,14 @@ async function handleRESTRequest(body: ToolCallRequest) {
95
117
  }
96
118
 
97
119
  case "get_doc": {
98
- const path = String(params?.path || "");
99
- const doc = await getDoc({ path });
120
+ const parsed = getDocSchema.safeParse(params);
121
+ if (!parsed.success) {
122
+ return NextResponse.json(
123
+ { error: "Invalid params", details: parsed.error.issues },
124
+ { status: 400 },
125
+ );
126
+ }
127
+ const doc = await getDoc({ path: parsed.data.path });
100
128
  if (!doc) {
101
129
  return NextResponse.json(
102
130
  { error: "Document not found" },
@@ -107,9 +135,16 @@ async function handleRESTRequest(body: ToolCallRequest) {
107
135
  }
108
136
 
109
137
  case "list_docs": {
110
- const directory =
111
- typeof params?.directory === "string" ? params.directory : undefined;
112
- const docs = await listDocs({ directory });
138
+ const parsed = listDocsSchema.safeParse(params);
139
+ if (!parsed.success) {
140
+ return NextResponse.json(
141
+ { error: "Invalid params", details: parsed.error.issues },
142
+ { status: 400 },
143
+ );
144
+ }
145
+ const docs = await listDocs({
146
+ directory: parsed.data.directory,
147
+ });
113
148
  return NextResponse.json({
114
149
  content: docs.map((d) => ({
115
150
  name: d.name,
@@ -1 +1 @@
1
- export declare const ragRoutesTemplate = "import { NextResponse } from \"next/server\";\nimport path from \"node:path\";\nimport { getLLMConfig, createChatModel } from \"@/services/llm\";\nimport {\n searchDocs,\n ensureDocsIndex,\n getIndexStatus,\n} from \"@/services/mcp/server\";\nimport configData from \"@/config.json\";\n\nconst config = configData as Config;\n\ninterface Config {\n name?: string;\n description?: string;\n icon?: string;\n preview?: string;\n}\n\nconst PROJECT_ROOT = process.cwd();\n\nconst projectName = config.name || \"Doccupine\";\n\nconst systemContext = `You are AI Assistant, a documentation assistant for ${projectName}, Your name is ${projectName} AI Assistant.\n\n## Core Rules\n1. Answer ONLY from the provided context. Never fabricate information.\n2. If the answer isn't in the context, say so clearly and suggest relevant sections or pages the user might check.\n3. If the question is ambiguous, ask a brief clarifying question before answering.\n\n## Response Style\n- Be concise and direct. Lead with the answer, then provide details if needed.\n- Use code examples from the context when relevant.\n- Match the technical level of the user's question.\n\n## MDX/Code Formatting\nWhen including code blocks in your response:\n- Never nest fenced code blocks (triple backticks) inside other fenced code blocks.\n- If you need to show MDX source that itself contains code blocks, use indented code blocks or escape the inner backticks.\n- All output must be valid MDX that renders correctly.\n\n## Greetings & Small Talk\nIf the user sends a greeting or non-documentation question, respond briefly and ask how you can help with the documentation.`;\n\nexport async function POST(req: Request) {\n try {\n const { question, refresh } = await req.json();\n\n let config;\n try {\n config = getLLMConfig();\n } catch (error: unknown) {\n const message =\n error instanceof Error ? error.message : \"LLM configuration error\";\n return NextResponse.json({ error: message }, { status: 500 });\n }\n\n // Use MCP service to ensure docs are indexed\n await ensureDocsIndex(Boolean(refresh));\n\n // Use MCP search_docs tool to find relevant documentation\n const searchResults = await searchDocs(String(question || \"\"), 6);\n\n // Build context from search results\n const context = searchResults\n .map(\n ({ chunk, score }) =>\n `File: ${path.relative(PROJECT_ROOT, chunk.path)}\\nScore: ${score.toFixed(3)}\\n----\\n${chunk.text}`,\n )\n .join(\"\\n\\n================\\n\\n\");\n\n // Create chat model and stream response\n const llm = createChatModel(config);\n const prompt = [\n {\n role: \"system\" as const,\n content: systemContext as string,\n },\n {\n role: \"user\" as const,\n content: `Question: ${question}\\n\\nContext:\\n${context}`,\n },\n ];\n\n const stream = await llm.stream(prompt);\n\n // Build metadata from MCP search results\n const indexStatus = getIndexStatus();\n const metadata = {\n sources: searchResults.map(({ chunk, score }) => ({\n id: chunk.id,\n path: path.relative(PROJECT_ROOT, chunk.path),\n uri: chunk.uri,\n score,\n })),\n chunkCount: indexStatus.chunkCount,\n };\n\n const encoder = new TextEncoder();\n const readableStream = new ReadableStream({\n async start(controller) {\n try {\n controller.enqueue(\n encoder.encode(\n `data: ${JSON.stringify({ type: \"metadata\", data: metadata })}\\n\\n`,\n ),\n );\n\n for await (const chunk of stream) {\n const content = chunk?.content || \"\";\n if (content) {\n controller.enqueue(\n encoder.encode(\n `data: ${JSON.stringify({ type: \"content\", data: content })}\\n\\n`,\n ),\n );\n }\n }\n\n controller.enqueue(\n encoder.encode(`data: ${JSON.stringify({ type: \"done\" })}\\n\\n`),\n );\n controller.close();\n } catch (error: unknown) {\n const message =\n error instanceof Error ? error.message : \"Stream error\";\n controller.enqueue(\n encoder.encode(\n `data: ${JSON.stringify({ type: \"error\", data: message })}\\n\\n`,\n ),\n );\n controller.close();\n }\n },\n });\n\n return new Response(readableStream, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n });\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : \"Unknown error\";\n return NextResponse.json({ error: message }, { status: 500 });\n }\n}\n\nexport async function GET() {\n const status = getIndexStatus();\n return NextResponse.json({\n ready: status.ready,\n chunks: status.chunkCount,\n });\n}\n";
1
+ export declare const ragRoutesTemplate = "import { NextResponse } from \"next/server\";\nimport path from \"node:path\";\nimport { z } from \"zod\";\nimport { getLLMConfig, createChatModel } from \"@/services/llm\";\nimport {\n searchDocs,\n ensureDocsIndex,\n getIndexStatus,\n} from \"@/services/mcp/server\";\nimport { rateLimit } from \"@/utils/rateLimit\";\nimport configData from \"@/config.json\";\n\nconst config = configData as Config;\n\ninterface Config {\n name?: string;\n description?: string;\n icon?: string;\n preview?: string;\n}\n\nconst PROJECT_ROOT = process.cwd();\n\nconst ragSchema = z.object({\n question: z.string().min(1).max(2000),\n refresh: z.boolean().optional(),\n});\n\nconst projectName = config.name || \"Doccupine\";\n\nconst systemContext = `You are AI Assistant, a documentation assistant for ${projectName}, Your name is ${projectName} AI Assistant.\n\n## Core Rules\n1. Answer ONLY from the provided context. Never fabricate information.\n2. If the answer isn't in the context, say so clearly and suggest relevant sections or pages the user might check.\n3. If the question is ambiguous, ask a brief clarifying question before answering.\n\n## Response Style\n- Be concise and direct. Lead with the answer, then provide details if needed.\n- Use code examples from the context when relevant.\n- Match the technical level of the user's question.\n\n## MDX/Code Formatting\nWhen including code blocks in your response:\n- Never nest fenced code blocks (triple backticks) inside other fenced code blocks.\n- If you need to show MDX source that itself contains code blocks, use indented code blocks or escape the inner backticks.\n- All output must be valid MDX that renders correctly.\n\n## Greetings & Small Talk\nIf the user sends a greeting or non-documentation question, respond briefly and ask how you can help with the documentation.`;\n\nexport async function POST(req: Request) {\n // Rate limit by IP\n const ip =\n req.headers.get(\"x-forwarded-for\")?.split(\",\")[0]?.trim() ?? \"unknown\";\n const { allowed, retryAfter } = rateLimit(ip);\n if (!allowed) {\n return NextResponse.json(\n { error: \"Too many requests\" },\n { status: 429, headers: { \"Retry-After\": String(retryAfter) } },\n );\n }\n\n try {\n const body = await req.json();\n const parsed = ragSchema.safeParse(body);\n if (!parsed.success) {\n return NextResponse.json(\n { error: \"Invalid input\", details: parsed.error.issues },\n { status: 400 },\n );\n }\n const { question, refresh } = parsed.data;\n\n let config;\n try {\n config = getLLMConfig();\n } catch (error: unknown) {\n const message =\n error instanceof Error ? error.message : \"LLM configuration error\";\n return NextResponse.json({ error: message }, { status: 500 });\n }\n\n // Use MCP service to ensure docs are indexed\n await ensureDocsIndex(Boolean(refresh));\n\n // Use MCP search_docs tool to find relevant documentation\n const searchResults = await searchDocs(String(question || \"\"), 6);\n\n // Build context from search results\n const context = searchResults\n .map(\n ({ chunk, score }) =>\n `File: ${path.relative(PROJECT_ROOT, chunk.path)}\\nScore: ${score.toFixed(3)}\\n----\\n${chunk.text}`,\n )\n .join(\"\\n\\n================\\n\\n\");\n\n // Create chat model and stream response\n const llm = createChatModel(config);\n const prompt = [\n {\n role: \"system\" as const,\n content: systemContext as string,\n },\n {\n role: \"user\" as const,\n content: `Question: ${question}\\n\\nContext:\\n${context}`,\n },\n ];\n\n const stream = await llm.stream(prompt);\n\n // Build metadata from MCP search results\n const indexStatus = getIndexStatus();\n const metadata = {\n sources: searchResults.map(({ chunk, score }) => ({\n id: chunk.id,\n path: path.relative(PROJECT_ROOT, chunk.path),\n uri: chunk.uri,\n score,\n })),\n chunkCount: indexStatus.chunkCount,\n };\n\n const encoder = new TextEncoder();\n const readableStream = new ReadableStream({\n async start(controller) {\n try {\n controller.enqueue(\n encoder.encode(\n `data: ${JSON.stringify({ type: \"metadata\", data: metadata })}\\n\\n`,\n ),\n );\n\n for await (const chunk of stream) {\n const content = chunk?.content || \"\";\n if (content) {\n controller.enqueue(\n encoder.encode(\n `data: ${JSON.stringify({ type: \"content\", data: content })}\\n\\n`,\n ),\n );\n }\n }\n\n controller.enqueue(\n encoder.encode(`data: ${JSON.stringify({ type: \"done\" })}\\n\\n`),\n );\n controller.close();\n } catch (error: unknown) {\n const message =\n error instanceof Error ? error.message : \"Stream error\";\n controller.enqueue(\n encoder.encode(\n `data: ${JSON.stringify({ type: \"error\", data: message })}\\n\\n`,\n ),\n );\n controller.close();\n }\n },\n });\n\n return new Response(readableStream, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n });\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : \"Unknown error\";\n return NextResponse.json({ error: message }, { status: 500 });\n }\n}\n\nexport async function GET() {\n const status = getIndexStatus();\n return NextResponse.json({\n ready: status.ready,\n chunks: status.chunkCount,\n });\n}\n";
@@ -1,11 +1,13 @@
1
1
  export const ragRoutesTemplate = `import { NextResponse } from "next/server";
2
2
  import path from "node:path";
3
+ import { z } from "zod";
3
4
  import { getLLMConfig, createChatModel } from "@/services/llm";
4
5
  import {
5
6
  searchDocs,
6
7
  ensureDocsIndex,
7
8
  getIndexStatus,
8
9
  } from "@/services/mcp/server";
10
+ import { rateLimit } from "@/utils/rateLimit";
9
11
  import configData from "@/config.json";
10
12
 
11
13
  const config = configData as Config;
@@ -19,6 +21,11 @@ interface Config {
19
21
 
20
22
  const PROJECT_ROOT = process.cwd();
21
23
 
24
+ const ragSchema = z.object({
25
+ question: z.string().min(1).max(2000),
26
+ refresh: z.boolean().optional(),
27
+ });
28
+
22
29
  const projectName = config.name || "Doccupine";
23
30
 
24
31
  const systemContext = \`You are AI Assistant, a documentation assistant for \${projectName}, Your name is \${projectName} AI Assistant.
@@ -43,8 +50,27 @@ When including code blocks in your response:
43
50
  If the user sends a greeting or non-documentation question, respond briefly and ask how you can help with the documentation.\`;
44
51
 
45
52
  export async function POST(req: Request) {
53
+ // Rate limit by IP
54
+ const ip =
55
+ req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
56
+ const { allowed, retryAfter } = rateLimit(ip);
57
+ if (!allowed) {
58
+ return NextResponse.json(
59
+ { error: "Too many requests" },
60
+ { status: 429, headers: { "Retry-After": String(retryAfter) } },
61
+ );
62
+ }
63
+
46
64
  try {
47
- const { question, refresh } = await req.json();
65
+ const body = await req.json();
66
+ const parsed = ragSchema.safeParse(body);
67
+ if (!parsed.success) {
68
+ return NextResponse.json(
69
+ { error: "Invalid input", details: parsed.error.issues },
70
+ { status: 400 },
71
+ );
72
+ }
73
+ const { question, refresh } = parsed.data;
48
74
 
49
75
  let config;
50
76
  try {
@@ -1 +1 @@
1
- export declare const routesTemplate = "import { NextResponse } from \"next/server\";\nimport { cookies } from \"next/headers\";\n\nexport async function POST(request: Request) {\n try {\n const { theme } = await request.json();\n if (theme !== \"light\" && theme !== \"dark\") {\n return NextResponse.json(\n { ok: false, error: \"Invalid theme\" },\n { status: 400 },\n );\n }\n\n const cookieStore = await cookies();\n cookieStore.set(\"theme\", theme, {\n path: \"/\",\n maxAge: 60 * 60 * 24 * 365,\n sameSite: \"lax\",\n });\n\n return NextResponse.json({ ok: true });\n } catch (e) {\n return NextResponse.json(\n { ok: false, error: \"Bad Request\" },\n { status: 400 },\n );\n }\n}\n";
1
+ export declare const routesTemplate = "import { NextResponse } from \"next/server\";\nimport { cookies } from \"next/headers\";\n\nexport async function POST(request: Request) {\n try {\n const { theme } = await request.json();\n if (theme !== \"light\" && theme !== \"dark\") {\n return NextResponse.json(\n { ok: false, error: \"Invalid theme\" },\n { status: 400 },\n );\n }\n\n const cookieStore = await cookies();\n cookieStore.set(\"theme\", theme, {\n path: \"/\",\n maxAge: 60 * 60 * 24 * 365,\n sameSite: \"lax\",\n });\n\n return NextResponse.json({ ok: true });\n } catch {\n return NextResponse.json(\n { ok: false, error: \"Bad Request\" },\n { status: 400 },\n );\n }\n}\n";
@@ -19,7 +19,7 @@ export async function POST(request: Request) {
19
19
  });
20
20
 
21
21
  return NextResponse.json({ ok: true });
22
- } catch (e) {
22
+ } catch {
23
23
  return NextResponse.json(
24
24
  { ok: false, error: "Bad Request" },
25
25
  { status: 400 },
@@ -0,0 +1 @@
1
+ export declare const errorTemplate = "\"use client\";\n\nimport { useEffect } from \"react\";\n\nexport default function Error({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n useEffect(() => {\n console.error(\"Unhandled error:\", error);\n }, [error]);\n\n return (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"50vh\",\n padding: \"2rem\",\n textAlign: \"center\",\n }}\n >\n <h2 style={{ marginBottom: \"1rem\" }}>Something went wrong</h2>\n <button\n onClick={reset}\n style={{\n padding: \"0.5rem 1rem\",\n borderRadius: \"6px\",\n border: \"1px solid #ccc\",\n background: \"#fff\",\n cursor: \"pointer\",\n fontSize: \"1rem\",\n }}\n >\n Try again\n </button>\n </div>\n );\n}\n";
@@ -0,0 +1,45 @@
1
+ export const errorTemplate = `"use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ export default function Error({
6
+ error,
7
+ reset,
8
+ }: {
9
+ error: Error & { digest?: string };
10
+ reset: () => void;
11
+ }) {
12
+ useEffect(() => {
13
+ console.error("Unhandled error:", error);
14
+ }, [error]);
15
+
16
+ return (
17
+ <div
18
+ style={{
19
+ display: "flex",
20
+ flexDirection: "column",
21
+ alignItems: "center",
22
+ justifyContent: "center",
23
+ minHeight: "50vh",
24
+ padding: "2rem",
25
+ textAlign: "center",
26
+ }}
27
+ >
28
+ <h2 style={{ marginBottom: "1rem" }}>Something went wrong</h2>
29
+ <button
30
+ onClick={reset}
31
+ style={{
32
+ padding: "0.5rem 1rem",
33
+ borderRadius: "6px",
34
+ border: "1px solid #ccc",
35
+ background: "#fff",
36
+ cursor: "pointer",
37
+ fontSize: "1rem",
38
+ }}
39
+ >
40
+ Try again
41
+ </button>
42
+ </div>
43
+ );
44
+ }
45
+ `;
@@ -1,6 +1,6 @@
1
1
  export const layoutTemplate = (pages, fontConfig) => `import type { Metadata } from "next";
2
2
  ${fontConfig?.googleFont?.fontName?.length ? `import { ${fontConfig.googleFont.fontName} } from "next/font/google";` : fontConfig?.localFonts?.length || fontConfig?.localFonts?.src?.length ? 'import localFont from "next/font/local";' : 'import { Inter } from "next/font/google";'}
3
- import { StyledComponentsRegistry } from "cherry-styled-components/src/lib";
3
+ import { StyledComponentsRegistry } from "cherry-styled-components";
4
4
  import { theme, themeDark } from "@/app/theme";
5
5
  import { CherryThemeProvider } from "@/components/layout/CherryThemeProvider";
6
6
  import { Chat, ChtProvider } from "@/components/Chat";
@@ -9,7 +9,10 @@ import { Header } from "@/components/layout/Header";
9
9
  import { DocsWrapper } from "@/components/layout/DocsComponents";
10
10
  import { SideBar } from "@/components/SideBar";
11
11
  import { DocsNavigation } from "@/components/layout/DocsNavigation";
12
- import { transformPagesToGroupedStructure } from "@/utils/orderNavItems";
12
+ import {
13
+ transformPagesToGroupedStructure,
14
+ type PagesProps,
15
+ } from "@/utils/orderNavItems";
13
16
  import { StaticLinks } from "@/components/layout/StaticLinks";
14
17
  import navigation from "@/navigation.json";
15
18
 
@@ -52,7 +55,7 @@ export default async function RootLayout({
52
55
  },
53
56
  ];
54
57
 
55
- const pages: any = doccupinePages;
58
+ const pages: PagesProps[] = doccupinePages;
56
59
  const result = navigation.length
57
60
  ? navigation
58
61
  : transformPagesToGroupedStructure(pages);
@@ -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/src/lib\";\nimport { ArrowUp, LoaderPinwheel, 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 { mq, Theme } from \"@/app/theme\";\nimport { useMDXComponents } from \"@/components/MDXComponents\";\nimport { styledTable, stylesLists } from \"@/components/layout/SharedStyled\";\nimport links from \"@/links.json\";\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(100vh - 90px);\n overflow-y: scroll;\n overflow-x: hidden;\n z-index: 1000;\n width: 100%;\n padding: 0 20px;\n transition: all 0.3s ease;\n transform: translateX(0);\n background: ${({ theme }) => theme.colors.light};\n\n ${({ $isVisible }) =>\n !$isVisible &&\n css`\n transform: translateX(100%);\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\n ${mq(\"lg\")} {\n width: 420px;\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n }\n\n ${({ $isVisible }) =>\n $isVisible &&\n css`\n transform: translateX(0);\n `}\n\n & .loading {\n animation: ${loadingAnimation} 1s linear infinite;\n }\n`;\n\nconst StyledChatFixedForm = styled.form<{\n theme: Theme;\n $hide: boolean;\n $hasLinks: boolean;\n}>`\n transition: all 0.3s ease;\n position: fixed;\n bottom: 20px;\n left: 20px;\n width: calc(100% - 115px);\n z-index: 998;\n\n ${mq(\"lg\")} {\n left: 50%;\n transform: translateX(-50%) translateY(0);\n bottom: initial;\n position: absolute;\n top: 90px;\n width: calc(100% - 320px * 2 - 40px);\n opacity: 1;\n\n ${({ $hasLinks }) =>\n $hasLinks &&\n css`\n top: 164px;\n `}\n }\n\n ${({ $hide }) =>\n $hide &&\n css`\n transform: translateX(-100px);\n\n ${mq(\"lg\")} {\n opacity: 0;\n transform: translateX(-50%) translateY(-20px);\n }\n `}\n\n & .loading {\n animation: ${loadingAnimation} 1s linear infinite;\n }\n`;\n\nconst StyledChatFixedInner = styled.div`\n margin: auto;\n display: flex;\n gap: 10px;\n justify-content: center;\n align-items: center;\n\n ${mq(\"lg\")} {\n max-width: 640px;\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};A\n white-space: pre;\n }\n\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 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: 25px 20px;\n height: 73px;\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: 5px;\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 = { id: string; path: string; score: number };\ntype Answer = {\n text: string;\n answer?: boolean;\n mdx?: MDXRemoteSerializeResult;\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}\n\nfunction RainbowInput({\n id,\n value,\n onChange,\n placeholder,\n autoComplete,\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 id={id}\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n autoComplete={autoComplete}\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n />\n </StyledRainbowInputWrapper>\n );\n}\n\nfunction Chat() {\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 endRef = useRef<HTMLDivElement | null>(null);\n const mdxComponents = useMDXComponents({});\n const { isOpen, setIsOpen } = useContext(ChatContext);\n\n useEffect(() => {\n endRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"end\" });\n }, [answer]);\n\n useEffect(() => {\n if (answer?.length > 0) {\n const el = document.getElementById(\n \"chat-bottom-input\",\n ) as HTMLInputElement | null;\n el?.focus();\n }\n }, [answer]);\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 try {\n const res = await fetch(\"/api/rag\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ question: currentQuestion }),\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 let streamedContent = \"\";\n let metadata: any = null;\n\n if (!reader) {\n throw new Error(\"Failed to get response reader\");\n }\n\n // Add a placeholder for the streaming answer\n const streamingAnswerIndex = mergedQuestions.length;\n setAnswer([...mergedQuestions, { text: \"\", answer: true }]);\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const chunk = decoder.decode(value);\n const lines = chunk.split(\"\\n\");\n\n for (const line of lines) {\n if (line.startsWith(\"data: \")) {\n try {\n const data = JSON.parse(line.slice(6));\n\n if (data.type === \"metadata\") {\n metadata = data.data;\n } else if (data.type === \"content\") {\n streamedContent += data.data;\n\n setAnswer((prev) => {\n const newAnswers = [...prev];\n newAnswers[streamingAnswerIndex] = {\n text: streamedContent,\n answer: true,\n };\n return newAnswers;\n });\n } else if (data.type === \"error\") {\n throw new Error(data.data);\n } else if (data.type === \"done\") {\n // Finalize with MDX serialization\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: any) {\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 };\n return newAnswers;\n });\n }\n } catch (parseError) {\n console.error(\"Failed to parse SSE data:\", parseError);\n }\n }\n }\n }\n } catch (err: any) {\n setError(err.message || \"Unknown error\");\n } finally {\n setLoading(false);\n }\n }\n\n return (\n <>\n <StyledChatFixedForm\n onSubmit={ask}\n $hide={answer?.length > 0}\n $hasLinks={links.length > 0}\n >\n <StyledChatFixedInner>\n <RainbowInput\n value={question}\n onChange={(e) => setQuestion(e.target.value)}\n placeholder=\"Ask AI Assistant...\"\n autoComplete=\"off\"\n />\n <StyledRainbowButton\n type=\"submit\"\n disabled={loading}\n $hasContent={question.trim().length > 0}\n >\n {loading ? <LoaderPinwheel className=\"loading\" /> : <ArrowUp />}\n </StyledRainbowButton>\n </StyledChatFixedInner>\n </StyledChatFixedForm>\n\n <StyledChat $isVisible={isOpen}>\n <StyledChatTitle>\n <StyledChatTitleIconWrapper>\n <Sparkles />\n <h3>AI Assistant</h3>\n </StyledChatTitleIconWrapper>\n <StyledChatCloseButton\n onClick={() => {\n setAnswer([]);\n setIsOpen(false);\n }}\n >\n <X />\n </StyledChatCloseButton>\n </StyledChatTitle>\n {answer &&\n answer.map((a, i) => (\n <StyledAnswer key={i} $isAnswer={a.answer ?? false}>\n {a.answer && a.mdx ? (\n <MDXRemote {...a.mdx} components={mdxComponents} />\n ) : (\n a.text\n )}\n </StyledAnswer>\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 value={question}\n onChange={(e) => setQuestion(e.target.value)}\n placeholder=\"Ask AI Assistant...\"\n autoComplete=\"off\"\n />\n <StyledRainbowButton\n type=\"submit\"\n disabled={loading || question.trim() === \"\"}\n $hasContent={question.trim().length > 0}\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}>({\n isOpen: false,\n setIsOpen: () => {},\n isChatActive: false,\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\n return (\n <ChatContext.Provider\n value={{\n isOpen,\n setIsOpen,\n isChatActive,\n }}\n >\n {children}\n </ChatContext.Provider>\n );\n};\n\nexport { Chat, ChtProvider, ChatContext };\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, 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 { mq, Theme } from \"@/app/theme\";\nimport { useMDXComponents } from \"@/components/MDXComponents\";\nimport { styledTable, stylesLists } from \"@/components/layout/SharedStyled\";\nimport links from \"@/links.json\";\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(100vh - 90px);\n overflow-y: scroll;\n overflow-x: hidden;\n z-index: 1000;\n width: 100%;\n padding: 0 20px;\n transition: all 0.3s ease;\n transform: translateX(0);\n background: ${({ theme }) => theme.colors.light};\n\n ${({ $isVisible }) =>\n !$isVisible &&\n css`\n transform: translateX(100%);\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\n ${mq(\"lg\")} {\n width: 420px;\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n }\n\n ${({ $isVisible }) =>\n $isVisible &&\n css`\n transform: translateX(0);\n `}\n\n & .loading {\n animation: ${loadingAnimation} 1s linear infinite;\n }\n`;\n\nconst StyledChatFixedForm = styled.form<{\n theme: Theme;\n $hide: boolean;\n $hasLinks: boolean;\n}>`\n transition: all 0.3s ease;\n position: fixed;\n bottom: 20px;\n left: 20px;\n width: calc(100% - 115px);\n z-index: 998;\n\n ${mq(\"lg\")} {\n left: 50%;\n transform: translateX(-50%) translateY(0);\n bottom: initial;\n position: absolute;\n top: 90px;\n width: calc(100% - 320px * 2 - 40px);\n opacity: 1;\n\n ${({ $hasLinks }) =>\n $hasLinks &&\n css`\n top: 164px;\n `}\n }\n\n ${({ $hide }) =>\n $hide &&\n css`\n transform: translateX(-100px);\n\n ${mq(\"lg\")} {\n opacity: 0;\n transform: translateX(-50%) translateY(-20px);\n }\n `}\n\n & .loading {\n animation: ${loadingAnimation} 1s linear infinite;\n }\n`;\n\nconst StyledChatFixedInner = styled.div`\n margin: auto;\n display: flex;\n gap: 10px;\n justify-content: center;\n align-items: center;\n\n ${mq(\"lg\")} {\n max-width: 640px;\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};A\n white-space: pre;\n }\n\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 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: 25px 20px;\n height: 73px;\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: 5px;\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 Answer = {\n text: string;\n answer?: boolean;\n mdx?: MDXRemoteSerializeResult;\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}\n\nfunction RainbowInput({\n id,\n value,\n onChange,\n placeholder,\n autoComplete,\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 id={id}\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n autoComplete={autoComplete}\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n />\n </StyledRainbowInputWrapper>\n );\n}\n\nfunction Chat() {\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 endRef = useRef<HTMLDivElement | null>(null);\n const mdxComponents = useMDXComponents({});\n const { isOpen, setIsOpen } = useContext(ChatContext);\n\n useEffect(() => {\n endRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"end\" });\n }, [answer]);\n\n useEffect(() => {\n if (answer?.length > 0) {\n const el = document.getElementById(\n \"chat-bottom-input\",\n ) as HTMLInputElement | null;\n el?.focus();\n }\n }, [answer]);\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 try {\n const res = await fetch(\"/api/rag\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ question: currentQuestion }),\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 let streamedContent = \"\";\n if (!reader) {\n throw new Error(\"Failed to get response reader\");\n }\n\n // Add a placeholder for the streaming answer\n const streamingAnswerIndex = mergedQuestions.length;\n setAnswer([...mergedQuestions, { text: \"\", answer: true }]);\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const chunk = decoder.decode(value);\n const lines = chunk.split(\"\\n\");\n\n for (const line of lines) {\n if (line.startsWith(\"data: \")) {\n try {\n const data = JSON.parse(line.slice(6));\n\n if (data.type === \"content\") {\n streamedContent += data.data;\n\n setAnswer((prev) => {\n const newAnswers = [...prev];\n newAnswers[streamingAnswerIndex] = {\n text: streamedContent,\n answer: true,\n };\n return newAnswers;\n });\n } else if (data.type === \"error\") {\n throw new Error(data.data);\n } else if (data.type === \"done\") {\n // Finalize with MDX serialization\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 };\n return newAnswers;\n });\n }\n } catch (parseError) {\n console.error(\"Failed to parse SSE data:\", parseError);\n }\n }\n }\n }\n } catch (err: unknown) {\n setError(err instanceof Error ? err.message : \"Unknown error\");\n } finally {\n setLoading(false);\n }\n }\n\n return (\n <>\n <StyledChatFixedForm\n onSubmit={ask}\n $hide={answer?.length > 0}\n $hasLinks={links.length > 0}\n >\n <StyledChatFixedInner>\n <RainbowInput\n value={question}\n onChange={(e) => setQuestion(e.target.value)}\n placeholder=\"Ask AI Assistant...\"\n autoComplete=\"off\"\n />\n <StyledRainbowButton\n type=\"submit\"\n disabled={loading}\n $hasContent={question.trim().length > 0}\n >\n {loading ? <LoaderPinwheel className=\"loading\" /> : <ArrowUp />}\n </StyledRainbowButton>\n </StyledChatFixedInner>\n </StyledChatFixedForm>\n\n <StyledChat $isVisible={isOpen}>\n <StyledChatTitle>\n <StyledChatTitleIconWrapper>\n <Sparkles />\n <h3>AI Assistant</h3>\n </StyledChatTitleIconWrapper>\n <StyledChatCloseButton\n onClick={() => {\n setAnswer([]);\n setIsOpen(false);\n }}\n >\n <X />\n </StyledChatCloseButton>\n </StyledChatTitle>\n {answer &&\n answer.map((a, i) => (\n <StyledAnswer key={i} $isAnswer={a.answer ?? false}>\n {a.answer && a.mdx ? (\n <MDXRemote {...a.mdx} components={mdxComponents} />\n ) : (\n a.text\n )}\n </StyledAnswer>\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 value={question}\n onChange={(e) => setQuestion(e.target.value)}\n placeholder=\"Ask AI Assistant...\"\n autoComplete=\"off\"\n />\n <StyledRainbowButton\n type=\"submit\"\n disabled={loading || question.trim() === \"\"}\n $hasContent={question.trim().length > 0}\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}>({\n isOpen: false,\n setIsOpen: () => {},\n isChatActive: false,\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\n return (\n <ChatContext.Provider\n value={{\n isOpen,\n setIsOpen,\n isChatActive,\n }}\n >\n {children}\n </ChatContext.Provider>\n );\n};\n\nexport { Chat, ChtProvider, ChatContext };\n";
@@ -8,7 +8,7 @@ import React, {
8
8
  } from "react";
9
9
  import styled, { css, keyframes } from "styled-components";
10
10
  import { rgba } from "polished";
11
- import { Button } from "cherry-styled-components/src/lib";
11
+ import { Button } from "cherry-styled-components";
12
12
  import { ArrowUp, LoaderPinwheel, Sparkles, X } from "lucide-react";
13
13
  import remarkGfm from "remark-gfm";
14
14
  import rehypeHighlight from "rehype-highlight";
@@ -597,7 +597,6 @@ const StyledChatCloseButton = styled.button<{ theme: Theme }>\`
597
597
  }
598
598
  \`;
599
599
 
600
- type Source = { id: string; path: string; score: number };
601
600
  type Answer = {
602
601
  text: string;
603
602
  answer?: boolean;
@@ -736,8 +735,6 @@ function Chat() {
736
735
  const reader = res.body?.getReader();
737
736
  const decoder = new TextDecoder();
738
737
  let streamedContent = "";
739
- let metadata: any = null;
740
-
741
738
  if (!reader) {
742
739
  throw new Error("Failed to get response reader");
743
740
  }
@@ -758,9 +755,7 @@ function Chat() {
758
755
  try {
759
756
  const data = JSON.parse(line.slice(6));
760
757
 
761
- if (data.type === "metadata") {
762
- metadata = data.data;
763
- } else if (data.type === "content") {
758
+ if (data.type === "content") {
764
759
  streamedContent += data.data;
765
760
 
766
761
  setAnswer((prev) => {
@@ -786,7 +781,7 @@ function Chat() {
786
781
  development: false,
787
782
  },
788
783
  });
789
- } catch (mdxError: any) {
784
+ } catch (mdxError: unknown) {
790
785
  console.error("MDX serialization error:", mdxError);
791
786
  }
792
787
 
@@ -806,8 +801,8 @@ function Chat() {
806
801
  }
807
802
  }
808
803
  }
809
- } catch (err: any) {
810
- setError(err.message || "Unknown error");
804
+ } catch (err: unknown) {
805
+ setError(err instanceof Error ? err.message : "Unknown error");
811
806
  } finally {
812
807
  setLoading(false);
813
808
  }
@@ -1 +1 @@
1
- export declare const docsTemplate = "import { Flex } from \"cherry-styled-components/src/lib\";\nimport {\n DocsContainer,\n StyledMarkdownContainer,\n} from \"@/components/layout/DocsComponents\";\nimport { MDXRemote } from \"next-mdx-remote/rsc\";\nimport remarkGfm from \"remark-gfm\";\nimport { useMDXComponents } from \"@/components/MDXComponents\";\nimport { DocsSideBar } from \"@/components/DocsSideBar\";\nimport { ActionBar } from \"@/components/layout/ActionBar\";\n\ninterface DocsProps {\n content: string;\n}\n\ninterface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nfunction generateId(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .trim();\n}\n\nfunction extractHeadings(content: string): Heading[] {\n const contentWithoutCodeBlocks = content.replace(/```[\\s\\S]*?```/g, \"\");\n const headingRegex = /^(#{1,6})\\s+(.+)$/gm;\n const headings: Heading[] = [];\n let match;\n\n while ((match = headingRegex.exec(contentWithoutCodeBlocks)) !== null) {\n const level = match[1].length;\n const text = match[2].trim();\n const id = generateId(text);\n headings.push({ id, text, level });\n }\n\n return headings;\n}\n\nfunction Docs({ content }: DocsProps) {\n const headings = extractHeadings(content);\n const components = useMDXComponents({});\n\n return (\n <>\n <DocsContainer>\n <ActionBar content={content}>\n <Flex $gap={20}>\n <StyledMarkdownContainer>\n {content && (\n <MDXRemote\n source={content}\n options={{\n blockJS: false,\n mdxOptions: {\n remarkPlugins: [remarkGfm],\n },\n }}\n components={components}\n />\n )}\n </StyledMarkdownContainer>\n </Flex>\n </ActionBar>\n </DocsContainer>\n <DocsSideBar headings={headings} />\n </>\n );\n}\n\nexport { Docs };\n";
1
+ export declare const docsTemplate = "import { Flex } from \"cherry-styled-components\";\nimport {\n DocsContainer,\n StyledMarkdownContainer,\n} from \"@/components/layout/DocsComponents\";\nimport { MDXRemote } from \"next-mdx-remote/rsc\";\nimport remarkGfm from \"remark-gfm\";\nimport { useMDXComponents } from \"@/components/MDXComponents\";\nimport { DocsSideBar } from \"@/components/DocsSideBar\";\nimport { ActionBar } from \"@/components/layout/ActionBar\";\n\ninterface DocsProps {\n content: string;\n}\n\ninterface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nfunction generateId(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .trim();\n}\n\nfunction extractHeadings(content: string): Heading[] {\n const contentWithoutCodeBlocks = content.replace(/```[\\s\\S]*?```/g, \"\");\n const headingRegex = /^(#{1,6})\\s+(.+)$/gm;\n const headings: Heading[] = [];\n let match;\n\n while ((match = headingRegex.exec(contentWithoutCodeBlocks)) !== null) {\n const level = match[1].length;\n const text = match[2].trim();\n const id = generateId(text);\n headings.push({ id, text, level });\n }\n\n return headings;\n}\n\nfunction Docs({ content }: DocsProps) {\n const headings = extractHeadings(content);\n const components = useMDXComponents({});\n\n return (\n <>\n <DocsContainer>\n <ActionBar content={content}>\n <Flex $gap={20}>\n <StyledMarkdownContainer>\n {content && (\n <MDXRemote\n source={content}\n options={{\n blockJS: false,\n mdxOptions: {\n remarkPlugins: [remarkGfm],\n },\n }}\n components={components}\n />\n )}\n </StyledMarkdownContainer>\n </Flex>\n </ActionBar>\n </DocsContainer>\n <DocsSideBar headings={headings} />\n </>\n );\n}\n\nexport { Docs };\n";
@@ -1,4 +1,4 @@
1
- export const docsTemplate = `import { Flex } from "cherry-styled-components/src/lib";
1
+ export const docsTemplate = `import { Flex } from "cherry-styled-components";
2
2
  import {
3
3
  DocsContainer,
4
4
  StyledMarkdownContainer,
@@ -1 +1 @@
1
- export declare const docsSideBarTemplate = "\"use client\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Space } from \"cherry-styled-components/src/lib\";\nimport {\n StyledIndexSidebar,\n StyledIndexSidebarLink,\n StyledIndexSidebarLabel,\n} from \"@/components/layout/DocsComponents\";\n\nexport interface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nexport function DocsSideBar({ headings }: { headings: Heading[] }) {\n const [activeId, setActiveId] = useState<string>(\"\");\n\n const getScrollOffset = useCallback(() => {\n return document.getElementById(\"static-links\") ? 90 : 18;\n }, []);\n\n const handleScroll = useCallback(() => {\n if (headings.length === 0) return;\n\n const offset = getScrollOffset();\n\n const headingElements = headings\n .map((heading) => document.getElementById(heading.id))\n .filter(Boolean) as HTMLElement[];\n\n if (headingElements.length === 0) return;\n\n const windowHeight = window.innerHeight;\n\n const visibleHeadings = headingElements.filter((element) => {\n const rect = element.getBoundingClientRect();\n const elementTop = rect.top;\n const elementBottom = rect.bottom;\n return elementTop < windowHeight && elementBottom > -50;\n });\n\n if (visibleHeadings.length > 0) {\n let closestHeading = visibleHeadings[0];\n let closestDistance = Math.abs(\n closestHeading.getBoundingClientRect().top - offset,\n );\n for (const heading of visibleHeadings) {\n const distance = Math.abs(heading.getBoundingClientRect().top - offset);\n if (\n distance < closestDistance &&\n heading.getBoundingClientRect().top <= windowHeight * 0.3\n ) {\n closestDistance = distance;\n closestHeading = heading;\n }\n }\n setActiveId(closestHeading.id);\n return;\n }\n\n let currentActiveId = headings[0].id;\n for (const element of headingElements) {\n const rect = element.getBoundingClientRect();\n if (rect.top <= offset) {\n currentActiveId = element.id;\n } else {\n break;\n }\n }\n setActiveId(currentActiveId);\n }, [headings, getScrollOffset]);\n\n useEffect(() => {\n if (headings.length === 0) return;\n // Run initial scroll check on next frame to avoid synchronous setState in effect\n const rafId = requestAnimationFrame(handleScroll);\n let timeoutId: NodeJS.Timeout;\n const throttledHandleScroll = () => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(handleScroll, 50);\n };\n window.addEventListener(\"scroll\", throttledHandleScroll);\n window.addEventListener(\"resize\", handleScroll);\n return () => {\n window.removeEventListener(\"scroll\", throttledHandleScroll);\n window.removeEventListener(\"resize\", handleScroll);\n cancelAnimationFrame(rafId);\n clearTimeout(timeoutId);\n };\n }, [handleScroll, headings]);\n\n const handleHeadingClick = (headingId: string) => {\n const element = document.getElementById(headingId);\n if (element) {\n const offset = getScrollOffset();\n const elementPosition =\n element.getBoundingClientRect().top + window.scrollY;\n window.scrollTo({ top: elementPosition - offset, behavior: \"smooth\" });\n }\n };\n\n return (\n <StyledIndexSidebar>\n {headings?.length > 0 && (\n <>\n <StyledIndexSidebarLabel>On this page</StyledIndexSidebarLabel>\n <Space $size={20} />\n </>\n )}\n {headings.map((heading, index) => (\n <li\n key={index}\n style={{ paddingLeft: `${(heading.level - 1) * 16}px` }}\n >\n <StyledIndexSidebarLink\n href={`#${heading.id}`}\n onClick={(e) => {\n e.preventDefault();\n handleHeadingClick(heading.id);\n }}\n $isActive={activeId === heading.id}\n >\n {heading.text}\n </StyledIndexSidebarLink>\n </li>\n ))}\n </StyledIndexSidebar>\n );\n}\n";
1
+ export declare const docsSideBarTemplate = "\"use client\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Space } from \"cherry-styled-components\";\nimport {\n StyledIndexSidebar,\n StyledIndexSidebarLink,\n StyledIndexSidebarLabel,\n} from \"@/components/layout/DocsComponents\";\n\nexport interface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nexport function DocsSideBar({ headings }: { headings: Heading[] }) {\n const [activeId, setActiveId] = useState<string>(\"\");\n\n const getScrollOffset = useCallback(() => {\n return document.getElementById(\"static-links\") ? 90 : 18;\n }, []);\n\n const handleScroll = useCallback(() => {\n if (headings.length === 0) return;\n\n const offset = getScrollOffset();\n\n const headingElements = headings\n .map((heading) => document.getElementById(heading.id))\n .filter(Boolean) as HTMLElement[];\n\n if (headingElements.length === 0) return;\n\n const windowHeight = window.innerHeight;\n\n const visibleHeadings = headingElements.filter((element) => {\n const rect = element.getBoundingClientRect();\n const elementTop = rect.top;\n const elementBottom = rect.bottom;\n return elementTop < windowHeight && elementBottom > -50;\n });\n\n if (visibleHeadings.length > 0) {\n let closestHeading = visibleHeadings[0];\n let closestDistance = Math.abs(\n closestHeading.getBoundingClientRect().top - offset,\n );\n for (const heading of visibleHeadings) {\n const distance = Math.abs(heading.getBoundingClientRect().top - offset);\n if (\n distance < closestDistance &&\n heading.getBoundingClientRect().top <= windowHeight * 0.3\n ) {\n closestDistance = distance;\n closestHeading = heading;\n }\n }\n setActiveId(closestHeading.id);\n return;\n }\n\n let currentActiveId = headings[0].id;\n for (const element of headingElements) {\n const rect = element.getBoundingClientRect();\n if (rect.top <= offset) {\n currentActiveId = element.id;\n } else {\n break;\n }\n }\n setActiveId(currentActiveId);\n }, [headings, getScrollOffset]);\n\n useEffect(() => {\n if (headings.length === 0) return;\n // Run initial scroll check on next frame to avoid synchronous setState in effect\n const rafId = requestAnimationFrame(handleScroll);\n let timeoutId: NodeJS.Timeout;\n const throttledHandleScroll = () => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(handleScroll, 50);\n };\n window.addEventListener(\"scroll\", throttledHandleScroll);\n window.addEventListener(\"resize\", handleScroll);\n return () => {\n window.removeEventListener(\"scroll\", throttledHandleScroll);\n window.removeEventListener(\"resize\", handleScroll);\n cancelAnimationFrame(rafId);\n clearTimeout(timeoutId);\n };\n }, [handleScroll, headings]);\n\n const handleHeadingClick = (headingId: string) => {\n const element = document.getElementById(headingId);\n if (element) {\n const offset = getScrollOffset();\n const elementPosition =\n element.getBoundingClientRect().top + window.scrollY;\n window.scrollTo({ top: elementPosition - offset, behavior: \"smooth\" });\n }\n };\n\n return (\n <StyledIndexSidebar>\n {headings?.length > 0 && (\n <>\n <StyledIndexSidebarLabel>On this page</StyledIndexSidebarLabel>\n <Space $size={20} />\n </>\n )}\n {headings.map((heading, index) => (\n <li\n key={index}\n style={{ paddingLeft: `${(heading.level - 1) * 16}px` }}\n >\n <StyledIndexSidebarLink\n href={`#${heading.id}`}\n onClick={(e) => {\n e.preventDefault();\n handleHeadingClick(heading.id);\n }}\n $isActive={activeId === heading.id}\n >\n {heading.text}\n </StyledIndexSidebarLink>\n </li>\n ))}\n </StyledIndexSidebar>\n );\n}\n";
@@ -1,6 +1,6 @@
1
1
  export const docsSideBarTemplate = `"use client";
2
2
  import { useCallback, useEffect, useState } from "react";
3
- import { Space } from "cherry-styled-components/src/lib";
3
+ import { Space } from "cherry-styled-components";
4
4
  import {
5
5
  StyledIndexSidebar,
6
6
  StyledIndexSidebarLink,
@@ -1 +1 @@
1
- export declare const mdxComponentsTemplate = "import React from \"react\";\ntype MDXComponents = Record<string, React.ComponentType<any>>;\nimport { Space } from \"cherry-styled-components/src/lib\";\nimport { Code as CodeBlock } from \"@/components/layout/Code\";\nimport { Card } from \"@/components/layout/Card\";\nimport { Accordion } from \"@/components/layout/Accordion\";\nimport { Tabs, TabContent } from \"@/components/layout/Tabs\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { Columns } from \"@/components/layout/Columns\";\nimport { Field } from \"@/components/layout/Field\";\nimport { Update } from \"@/components/layout/Update\";\nimport { Steps, Step } from \"@/components/layout/Steps\";\nimport { Button } from \"@/components/layout/Button\";\nimport { DemoTheme } from \"@/components/layout/DemoTheme\";\n\nfunction extractAllTextFromChildren(children: React.ReactNode): string {\n if (children == null) return \"\";\n if (typeof children === \"string\") return children;\n if (typeof children === \"number\") return String(children);\n if (typeof children === \"boolean\") return \"\";\n if (Array.isArray(children))\n return children.map(extractAllTextFromChildren).join(\"\");\n if (React.isValidElement(children)) {\n const element = children as React.ReactElement<any>;\n return extractAllTextFromChildren(element.props.children);\n }\n return \"\";\n}\n\nfunction generateId(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .trim();\n}\n\n// Map <pre><code class=\"language-xyz\"> to our <Code /> component\nfunction Pre(props: any) {\n const child = React.Children.only(\n props.children,\n ) as React.ReactElement<any> | null;\n if (child && child.type === \"code\") {\n const className = child.props.className || \"\";\n const match = /language-(\\w+)/.exec(className);\n const language = match ? match[1] : undefined;\n const code = extractAllTextFromChildren(child.props.children).replace(\n /\\n$/,\n \"\",\n );\n if (language) {\n return (\n <CodeBlock className={className} code={code} language={language} />\n );\n }\n }\n return <pre {...props} />;\n}\n\nexport function useMDXComponents(components: MDXComponents): MDXComponents {\n return {\n // Headings with auto-generated ids for TOC and deep links\n h1: ({ children, ...props }: any) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h1 id={id} {...props}>\n {children}\n </h1>\n );\n },\n h2: ({ children, ...props }: any) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h2 id={id} {...props}>\n {children}\n </h2>\n );\n },\n h3: ({ children, ...props }: any) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h3 id={id} {...props}>\n {children}\n </h3>\n );\n },\n h4: ({ children, ...props }: any) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h4 id={id} {...props}>\n {children}\n </h4>\n );\n },\n h5: ({ children, ...props }: any) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h5 id={id} {...props}>\n {children}\n </h5>\n );\n },\n h6: ({ children, ...props }: any) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h6 id={id} {...props}>\n {children}\n </h6>\n );\n },\n\n // Code blocks\n pre: Pre,\n\n // Expose your custom components for MDX usage\n Card,\n Accordion,\n Tabs,\n TabContent,\n Callout,\n Icon,\n Columns,\n Field,\n Update,\n Steps,\n Step,\n Button,\n DemoTheme,\n Space,\n ...components,\n };\n}\n";
1
+ export declare const mdxComponentsTemplate = "import React from \"react\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype MDXComponents = Record<string, React.ComponentType<any>>;\nimport { Space } from \"cherry-styled-components\";\nimport { Code as CodeBlock } from \"@/components/layout/Code\";\nimport { Card } from \"@/components/layout/Card\";\nimport { Accordion } from \"@/components/layout/Accordion\";\nimport { Tabs, TabContent } from \"@/components/layout/Tabs\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { Columns } from \"@/components/layout/Columns\";\nimport { Field } from \"@/components/layout/Field\";\nimport { Update } from \"@/components/layout/Update\";\nimport { Steps, Step } from \"@/components/layout/Steps\";\nimport { Button } from \"@/components/layout/Button\";\nimport { DemoTheme } from \"@/components/layout/DemoTheme\";\n\ninterface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n children?: React.ReactNode;\n}\n\ninterface PreProps extends React.HTMLAttributes<HTMLPreElement> {\n children?: React.ReactNode;\n}\n\nfunction extractAllTextFromChildren(children: React.ReactNode): string {\n if (children == null) return \"\";\n if (typeof children === \"string\") return children;\n if (typeof children === \"number\") return String(children);\n if (typeof children === \"boolean\") return \"\";\n if (Array.isArray(children))\n return children.map(extractAllTextFromChildren).join(\"\");\n if (React.isValidElement(children)) {\n const element = children as React.ReactElement<{ children?: React.ReactNode }>;\n return extractAllTextFromChildren(element.props.children);\n }\n return \"\";\n}\n\nfunction generateId(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .trim();\n}\n\n// Map <pre><code class=\"language-xyz\"> to our <Code /> component\nfunction Pre(props: PreProps) {\n const child = React.Children.only(\n props.children,\n ) as React.ReactElement<{ className?: string; children?: React.ReactNode }> | null;\n if (child && child.type === \"code\") {\n const className = child.props.className || \"\";\n const match = /language-(\\w+)/.exec(className);\n const language = match ? match[1] : undefined;\n const code = extractAllTextFromChildren(child.props.children).replace(\n /\\n$/,\n \"\",\n );\n if (language) {\n return (\n <CodeBlock className={className} code={code} language={language} />\n );\n }\n }\n return <pre {...props} />;\n}\n\nexport function useMDXComponents(components: MDXComponents): MDXComponents {\n return {\n // Headings with auto-generated ids for TOC and deep links\n h1: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h1 id={id} {...props}>\n {children}\n </h1>\n );\n },\n h2: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h2 id={id} {...props}>\n {children}\n </h2>\n );\n },\n h3: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h3 id={id} {...props}>\n {children}\n </h3>\n );\n },\n h4: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h4 id={id} {...props}>\n {children}\n </h4>\n );\n },\n h5: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h5 id={id} {...props}>\n {children}\n </h5>\n );\n },\n h6: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h6 id={id} {...props}>\n {children}\n </h6>\n );\n },\n\n // Code blocks\n pre: Pre,\n\n // Expose your custom components for MDX usage\n Card,\n Accordion,\n Tabs,\n TabContent,\n Callout,\n Icon,\n Columns,\n Field,\n Update,\n Steps,\n Step,\n Button,\n DemoTheme,\n Space,\n ...components,\n };\n}\n";