mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
CLI: Support other React.js frameworks for AI chat
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
Support other React.js frameworks for AI chat
|
||||
@@ -18,6 +18,14 @@ export const registry: Registry = {
|
||||
if (filePath.startsWith('lib/source/')) return false;
|
||||
},
|
||||
onResolve(ref) {
|
||||
if (ref.type === 'unknown-specifier' && ref.specifier === 'hast') {
|
||||
return {
|
||||
type: 'dependency',
|
||||
dep: '@types/hast',
|
||||
specifier: 'hast',
|
||||
};
|
||||
}
|
||||
|
||||
if (ref.type === 'file') {
|
||||
const file = path.relative(baseDir, ref.file);
|
||||
|
||||
|
||||
@@ -386,7 +386,7 @@ const markdownUrl = `${page.url}.mdx`
|
||||
|
||||
You can install the AI chat dialog using Fumadocs CLI.
|
||||
|
||||
<Tabs items={['OpenRouter', 'Inkeep AI']}>
|
||||
<Tabs items={['AI SDK', 'Inkeep AI']}>
|
||||
<Tab>
|
||||
|
||||
```npm
|
||||
@@ -395,6 +395,12 @@ npx @fumadocs/cli add ai/openrouter
|
||||
|
||||
It's automatically configured for [OpenRouter](https://openrouter.ai) using Vercel AI SDK, with a `/search` tool for AI.
|
||||
|
||||
You can use other models by updating the `/api/chat` route.
|
||||
|
||||
> Fumadocs doesn't provide the AI model, it's up to you.
|
||||
>
|
||||
> Your AI model can use the `llms-full.txt` file generated above, or more diversified sources of information when combined with 3rd party solutions.
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
|
||||
@@ -413,43 +419,37 @@ INKEEP_API_KEY="..."
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Add the component & trigger to root layout (or anywhere you prefer):
|
||||
Add the component & trigger to docs layout:
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { AISearch, AISearchPanel, AISearchTrigger } from '@/components/ai/search';
|
||||
import { MessageCircleIcon } from 'lucide-react';
|
||||
// or import your own button styles
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{/* [!code ++:17] */}
|
||||
<AISearch>
|
||||
<AISearchPanel />
|
||||
<AISearchTrigger
|
||||
position="float"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'secondary',
|
||||
className: 'text-fd-muted-foreground rounded-2xl',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<MessageCircleIcon className="size-4.5" />
|
||||
Ask AI
|
||||
</AISearchTrigger>
|
||||
</AISearch>
|
||||
<DocsLayout>
|
||||
{/* [!code ++:17] */}
|
||||
<AISearch>
|
||||
<AISearchPanel />
|
||||
<AISearchTrigger
|
||||
position="float"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'secondary',
|
||||
className: 'text-fd-muted-foreground rounded-2xl',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<MessageCircleIcon className="size-4.5" />
|
||||
Ask AI
|
||||
</AISearchTrigger>
|
||||
</AISearch>
|
||||
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
To use your own AI models, update the configurations in `useChat` and `/api/chat` route.
|
||||
|
||||
Note that Fumadocs doesn't provide the AI model, it's up to you.
|
||||
Your AI model can use the `llms-full.txt` file generated above, or more diversified sources of information when combined with 3rd party solutions.
|
||||
|
||||
@@ -2,10 +2,9 @@ import path from 'node:path';
|
||||
import MagicString from 'magic-string';
|
||||
import { Visitor } from 'oxc-parser';
|
||||
import type {
|
||||
ArrowFunctionExpression,
|
||||
Argument,
|
||||
CallExpression,
|
||||
Expression,
|
||||
Function as AstFunction,
|
||||
ImportDeclaration,
|
||||
ObjectExpression,
|
||||
ParamPattern,
|
||||
@@ -165,7 +164,7 @@ interface HandlerInfo {
|
||||
bodyText: string;
|
||||
}
|
||||
|
||||
function parseHandlerFromAst(s: MagicString, expr: Expression): HandlerInfo {
|
||||
function parseHandlerFromAst(s: MagicString, expr: Argument): HandlerInfo {
|
||||
const isFn = expr.type === 'FunctionExpression';
|
||||
const isArrow = expr.type === 'ArrowFunctionExpression';
|
||||
if (!isFn && !isArrow) {
|
||||
@@ -173,7 +172,7 @@ function parseHandlerFromAst(s: MagicString, expr: Expression): HandlerInfo {
|
||||
'route-handler.build: second argument to $routeHandler must be a function or async arrow function',
|
||||
);
|
||||
}
|
||||
const fn = expr as ArrowFunctionExpression | (AstFunction & { type: 'FunctionExpression' });
|
||||
const fn = expr;
|
||||
if (!fn.async) {
|
||||
throw new Error('route-handler.build: route handler must be async');
|
||||
}
|
||||
@@ -440,8 +439,8 @@ export function buildRouteHandlerFile(
|
||||
throw new Error('route-handler.build: $routeHandler must be called with (info, handler)');
|
||||
}
|
||||
|
||||
const arg0 = call.arguments[0] as Expression;
|
||||
const arg1 = call.arguments[1] as Expression;
|
||||
const arg0 = call.arguments[0];
|
||||
const arg1 = call.arguments[1];
|
||||
if (arg0.type !== 'ObjectExpression') {
|
||||
throw new Error(
|
||||
'route-handler.build: first argument to $routeHandler must be an object literal',
|
||||
|
||||
@@ -252,9 +252,9 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (options.aiChat) {
|
||||
const { nextUseAi } = await import('./plugins/next-use-ai');
|
||||
const { ai } = await import('./plugins/ai');
|
||||
|
||||
plugins.push(nextUseAi(options.aiChat));
|
||||
plugins.push(ai(options.aiChat));
|
||||
}
|
||||
|
||||
await create({
|
||||
|
||||
+34
-17
@@ -7,23 +7,21 @@ import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { SyntaxKind } from 'ts-morph';
|
||||
|
||||
export function nextUseAi(provider: 'openrouter' | 'inkeep'): TemplatePlugin {
|
||||
export function ai(provider: 'openrouter' | 'inkeep'): TemplatePlugin {
|
||||
return {
|
||||
async afterWrite() {
|
||||
if (this.template.value === '+next+fuma-docs-mdx') {
|
||||
const config = await getDefaultConfig();
|
||||
await install(
|
||||
`ai/${provider}`,
|
||||
new ComponentInstaller(new HttpRegistryClient('https://fumadocs.dev/registry', config), {
|
||||
cwd: this.dest,
|
||||
}),
|
||||
);
|
||||
await addAIChat(this);
|
||||
await fs.writeFile(
|
||||
path.join(this.dest, '.env.local'),
|
||||
provider === 'openrouter' ? 'OPENROUTER_API_KEY=' : 'INKEEP_API_KEY=',
|
||||
);
|
||||
}
|
||||
const config = await getDefaultConfig();
|
||||
await install(
|
||||
`ai/${provider}`,
|
||||
new ComponentInstaller(new HttpRegistryClient('https://fumadocs.dev/registry', config), {
|
||||
cwd: this.dest,
|
||||
}),
|
||||
);
|
||||
await addAIChat(this);
|
||||
await fs.writeFile(
|
||||
path.join(this.dest, '.env.local'),
|
||||
provider === 'openrouter' ? 'OPENROUTER_API_KEY=' : 'INKEEP_API_KEY=',
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -47,8 +45,27 @@ async function install(target: string, installer: ComponentInstaller) {
|
||||
await installer.onEnd();
|
||||
}
|
||||
|
||||
async function addAIChat({ appDir }: TemplatePluginContext) {
|
||||
const file = await createSourceFile(path.join(appDir, 'app/docs/layout.tsx'));
|
||||
async function addAIChat({ template, appDir }: TemplatePluginContext) {
|
||||
let filePath: string;
|
||||
switch (template.value) {
|
||||
case '+next+fuma-docs-mdx':
|
||||
case '+next+fuma-docs-mdx+static':
|
||||
filePath = path.join(appDir, 'app/docs/layout.tsx');
|
||||
break;
|
||||
case 'waku':
|
||||
filePath = path.join(appDir, 'pages/docs/_layout.tsx');
|
||||
break;
|
||||
case 'react-router':
|
||||
case 'react-router-spa':
|
||||
filePath = path.join(appDir, 'routes/docs.tsx');
|
||||
break;
|
||||
case 'tanstack-start':
|
||||
case 'tanstack-start-spa':
|
||||
filePath = path.join(appDir, 'routes/docs/$.tsx');
|
||||
break;
|
||||
}
|
||||
|
||||
const file = await createSourceFile(filePath);
|
||||
const elements = file.getDescendantsOfKind(SyntaxKind.JsxElement);
|
||||
const code = `<AISearch>
|
||||
<AISearchPanel />
|
||||
Reference in New Issue
Block a user