diff --git a/docs/content/en/4.ai/1.assistant.md b/docs/content/en/4.ai/1.assistant.md index 68c2177d9..80fac5fef 100644 --- a/docs/content/en/4.ai/1.assistant.md +++ b/docs/content/en/4.ai/1.assistant.md @@ -27,59 +27,25 @@ By default, the assistant connects to your documentation's built-in MCP server a ## Quick Start -### 1. Install dependencies - -::code-group - -```bash [npm] -npm install ai @ai-sdk/vue @ai-sdk/gateway @ai-sdk/mcp @comark/nuxt -``` - -```bash [pnpm] -pnpm add ai @ai-sdk/vue @ai-sdk/gateway @ai-sdk/mcp @comark/nuxt -``` - -```bash [yarn] -yarn add ai @ai-sdk/vue @ai-sdk/gateway @ai-sdk/mcp @comark/nuxt -``` - +::note{to="#custom-ai-provider"} +This quick start uses Vercel AI Gateway. To use another provider (Mistral, OpenAI, Cloudflare AI Gateway, or anything else supported by the AI SDK), see **Custom AI provider**. :: -### 2. Set up AI Gateway authentication +### 1. Set up AI Gateway authentication -Pick **one** of this method: +Pick **one** of these methods: -**API key** — Create a key in [Vercel AI Gateway](https://vercel.com/~/ai/api-keys) and add it to your environment: +**API key**: create a key in [Vercel AI Gateway](https://vercel.com/~/ai/api-keys) and add it to your environment: ```bash [.env] AI_GATEWAY_API_KEY=your-api-key ``` -**OIDC (only on Vercel)** — `VERCEL_OIDC_TOKEN` is injected automatically. Nothing to add in the production. For local dev, run `vercel env pull` on a [linked project](https://vercel.com/docs/cli/link). - -### 3. Deploy - -Deploy your site — the assistant is available as soon as authentication is configured. +**OIDC (only on Vercel)**: `VERCEL_OIDC_TOKEN` is injected automatically, so there is nothing to add in production. For local dev, run `vercel env pull` on a [linked project](https://vercel.com/docs/cli/link). -## Using the Assistant +### 2. Deploy -Users can interact with the assistant in multiple ways: - -### Floating Input - -On documentation pages, a floating input appears at the bottom of the screen. Users can type their questions directly and press Enter to get answers. - -::tip -Use the keyboard shortcut :kbd{value="meta"} :kbd{value="I"} to focus the floating input. -:: - -### Explain with AI - -Each documentation page includes an **Explain with AI** button in the table of contents sidebar. Clicking this button opens the assistant with the current page as context, asking it to explain the content. - -### Slideover Chat - -When a conversation starts, a slideover panel opens on the right side of the screen. This panel displays the conversation history and allows users to continue asking questions. +Deploy your site, the assistant is available as soon as authentication is configured. ## Configuration @@ -111,7 +77,7 @@ export default defineAppConfig({ }) ``` -### FAQ Questions +### Questions Display suggested questions when the chat is empty. This helps users discover what they can ask. @@ -175,7 +141,7 @@ export default defineAppConfig({ }) ``` -## Keyboard Shortcuts +### Keyboard Shortcuts Configure the keyboard shortcut for focusing the floating input: @@ -196,7 +162,7 @@ The shortcut format uses underscores to separate keys. Common examples: - `meta_k` - Cmd+K (Mac) / Ctrl+K (Windows) - `ctrl_shift_p` - Ctrl+Shift+P -## Custom Icons +### Icons Customize the icons used by the assistant: @@ -216,20 +182,9 @@ export default defineAppConfig({ Icons use the [Iconify](https://iconify.design/) format (e.g., `i-lucide-sparkles`, `i-heroicons-sparkles`). -## Internationalization - -All UI texts are automatically translated based on the user's locale. Docus includes built-in translations for English and French. +### Features -The following texts are translated: - -- Slideover title and placeholder -- Tooltip texts -- Button labels ("Clear chat", "Close", "Explain with AI") -- Status messages ("Thinking...", "Chat is cleared on refresh") - -## Disable Features - -### Disable the Floating Input +#### Disable the Floating Input Hide the floating input at the bottom of documentation pages: @@ -241,7 +196,7 @@ export default defineAppConfig({ }) ``` -### Disable "Explain with AI" +#### Disable "Explain with AI" Hide the "Explain with AI" button in the documentation sidebar: @@ -253,32 +208,15 @@ export default defineAppConfig({ }) ``` -### Disable the Assistant Entirely - -The assistant is disabled when no authentication is available. To explicitly disable it, remove `AI_GATEWAY_API_KEY` from your environment: - -```bash [.env] -# AI_GATEWAY_API_KEY=your-api-key -``` - -On Vercel with OIDC, remove the auto-injected system environment variable from your project settings. - -## Advanced Configuration +#### Disable the Assistant Entirely -Configure advanced options in `nuxt.config.ts` under `docus.assistant`. +Set `enabled` to `false` to disable the assistant, even when AI Gateway credentials are available: ```ts [nuxt.config.ts] export default defineNuxtConfig({ docus: { assistant: { - // AI model (uses AI SDK Gateway format) - model: 'google/gemini-3-flash', - - // MCP server (path or URL) - mcpServer: '/mcp', - - // API endpoint path - apiPath: '/__docus__/assistant' + enabled: false } } }) @@ -322,7 +260,7 @@ export default defineNuxtConfig({ This is useful when you want the assistant to answer questions from a different documentation source, or when connecting to a centralized knowledge base. -### Custom AI Model +### Model The assistant uses `google/gemini-3-flash` by default. You can change this to any model supported by the AI SDK Gateway: @@ -336,7 +274,7 @@ export default defineNuxtConfig({ }) ``` -### Site Name in Responses +### Site Name The assistant automatically uses your site name in its responses. Configure the site name in `nuxt.config.ts`: @@ -350,6 +288,157 @@ export default defineNuxtConfig({ This makes the assistant respond as "the My Documentation assistant" and speak with authority about your specific product. +## Custom provider + +The `model` option above resolves models through Vercel AI Gateway, so it requires `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN`. To use another provider (Mistral, OpenAI, Cloudflare AI Gateway, or anything else supported by the [AI SDK](https://ai-sdk.dev/)), enable the assistant explicitly and provide your own endpoint. + +::steps +### Enable the assistant and pick a path + +Set `enabled: true` so the assistant no longer depends on AI Gateway credentials, and point `apiPath` at the route you're about to create: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + docus: { + assistant: { + enabled: true, + apiPath: '/api/assistant' + } + } +}) +``` + +Your own server route always takes precedence: when you define a route at `apiPath`, Docus steps aside and doesn't register its built-in endpoint there. + +### Install a provider + +Install the AI SDK provider package you need, for example Mistral: + + :::code-group + ```bash [npm] + npm install @ai-sdk/mistral + ``` + + ```bash [pnpm] + pnpm add @ai-sdk/mistral + ``` + + ```bash [yarn] + yarn add @ai-sdk/mistral + ``` + ::: + +### Implement the endpoint + +```ts [server/api/assistant.ts] +import { streamText, convertToModelMessages } from 'ai' +import { createMistral } from '@ai-sdk/mistral' + +const mistral = createMistral() + +export default defineEventHandler(async (event) => { + const { messages } = await readBody(event) + + return createAssistantResponse(streamText({ + ...await getAssistantDefaultOptions(event), + model: mistral('mistral-large-latest'), + messages: await convertToModelMessages(messages) + })) +}) +``` + + :::tip + Because you own the `streamText` call, provider specific constraints are solved where they belong. + ::: + + :::warning + Spread the defaults **first**. Options you set after the spread win (before are overwritten). + ::: + +| Util | Role | +| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`getAssistantDefaultOptions(event)`](#getassistantdefaultoptions) | Every `streamText` option the built-in endpoint uses: MCP tools, abort on disconnect, client cleanup, the documentation prompt, and the step and token limits. | +| [`getAssistantSystemPrompt(event)`](#getassistantsystemprompt) | The default documentation-tuned prompt on its own, for when you want to extend it. | +| [`createAssistantResponse(result)`](#createassistantresponse) | Wraps the result in the response format the assistant UI expects. | + +#### `getAssistantDefaultOptions` + +Returns real `streamText` options, so you can see and override every one of them: + +| Option | Default | +| ------------------------ | ----------------------------------------------------------------------------------------- | +| `tools` | The MCP tools from `docus.assistant.mcpServer` | +| `abortSignal` | Aborts generation when the client disconnects | +| `onEnd` / `onAbort` | Closes the MCP client | +| `onError` | Logs the provider error server-side, then closes the MCP client | +| `instructions` | `getAssistantSystemPrompt(event)` | +| `maxOutputTokens` | `8000` | +| `maxRetries` | `2` | +| `stopWhen` | `isStepCount(10)` | +| `prepareStep` | Disables tools on the last step so the model answers instead of stopping mid tool-calling | +| `experimental_transform` | `smoothStream()` | + +`model` and `messages` are not included, and neither are provider specific options like `providerOptions` or `temperature`, since they don't port across providers. + +Override by setting the option after the spread: + +```ts [server/api/assistant.ts] +return createAssistantResponse(streamText({ + ...await getAssistantDefaultOptions(event), + model: mistral('mistral-large-latest'), + // Wins over the default 8000 + maxOutputTokens: 4000, + messages: await convertToModelMessages(messages) +})) +``` + +To add behaviour to a callback rather than replace it, keep a reference and call through to it, so MCP cleanup still runs: + +```ts [server/api/assistant.ts] +const defaults = await getAssistantDefaultOptions(event) + +return createAssistantResponse(streamText({ + ...defaults, + model: mistral('mistral-large-latest'), + messages: await convertToModelMessages(messages), + onError: (payload) => { + myErrorReporter(payload.error) + // Still closes the MCP client + defaults.onError(payload) + } +})) +``` + + :::warning + `onEnd`, `onAbort` and `onError` close the MCP client. Replacing one without calling through to the original leaks a connection per request. + ::: + +#### `getAssistantSystemPrompt` + +`getAssistantDefaultOptions` already sets this prompt as `instructions`, so you only need this util to extend it. It returns a plain string, so concatenate: + +```ts [server/api/assistant.ts] +const defaults = await getAssistantDefaultOptions(event) + +return createAssistantResponse(streamText({ + ...defaults, + model: mistral('mistral-large-latest'), + messages: await convertToModelMessages(messages), + instructions: `${defaults.instructions} + +**Extra instructions:** +- Always mention the minimum supported version +- Never speculate about the roadmap` +})) +``` + +Set `instructions` to your own string to replace the default entirely. + +#### `createAssistantResponse` + +Wraps a `streamText` result in the response format the assistant UI expects, so your route follows future stream format changes without being edited. +:: + ## Programmatic Access Use the `useAssistant` composable to control the assistant programmatically: @@ -371,13 +460,13 @@ function askQuestion() { ``` -### Composable API +## Composable API -| Property | Type | Description | -| -------------------------------- | ---------------------- | --------------------------------------------------------------------------------------- | -| `isEnabled` | `ComputedRef` | Whether the assistant is enabled (`AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN` at build) | -| `isOpen` | `Ref` | Whether the slideover is open | -| `open(message?, clearPrevious?)` | `Function` | Open the assistant, optionally with a message | -| `close()` | `Function` | Close the assistant slideover | -| `toggle()` | `Function` | Toggle the assistant open/closed | -| `clearMessages()` | `Function` | Clear the conversation history | +| Property | Type | Description | +| -------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `isEnabled` | `ComputedRef` | Whether the assistant is enabled (`docus.assistant.enabled`, or `AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN` at build) | +| `isOpen` | `Ref` | Whether the slideover is open | +| `open(message?, clearPrevious?)` | `Function` | Open the assistant, optionally with a message | +| `close()` | `Function` | Close the assistant slideover | +| `toggle()` | `Function` | Toggle the assistant open/closed | +| `clearMessages()` | `Function` | Clear the conversation history | diff --git a/docs/content/fr/4.ai/1.assistant.md b/docs/content/fr/4.ai/1.assistant.md index d008c4f39..eda3c4f86 100644 --- a/docs/content/fr/4.ai/1.assistant.md +++ b/docs/content/fr/4.ai/1.assistant.md @@ -27,39 +27,25 @@ Par défaut, l'assistant se connecte au serveur MCP intégré de votre documenta ## Démarrage rapide -### 1. Installer les dépendances - -::code-group - -```bash [npm] -npm install ai @ai-sdk/vue @ai-sdk/gateway @ai-sdk/mcp @comark/nuxt -``` - -```bash [pnpm] -pnpm add ai @ai-sdk/vue @ai-sdk/gateway @ai-sdk/mcp @comark/nuxt -``` - -```bash [yarn] -yarn add ai @ai-sdk/vue @ai-sdk/gateway @ai-sdk/mcp @comark/nuxt -``` - +::note{to="#fournisseur-ia-personnalisé"} +Ce démarrage rapide utilise Vercel AI Gateway. Pour utiliser un autre fournisseur (Mistral, OpenAI, Cloudflare AI Gateway, ou tout autre fournisseur supporté par l'AI SDK), consultez **Fournisseur IA personnalisé**. :: -### 2. Configurer l'authentification AI Gateway +### 1. Configurer l'authentification AI Gateway Choisissez **une** de ces méthodes : -**Clé API** — Créez une clé dans [Vercel AI Gateway](https://vercel.com/~/ai/api-keys) et ajoutez-la à votre environnement : +**Clé API** : créez une clé dans [Vercel AI Gateway](https://vercel.com/~/ai/api-keys) et ajoutez-la à votre environnement : ```bash [.env] AI_GATEWAY_API_KEY=votre-cle-api ``` -**OIDC (uniquement sur Vercel)** — `VERCEL_OIDC_TOKEN` est injecté automatiquement. Rien à ajouter en production. En local, lancez `vercel env pull` sur un [projet lié](https://vercel.com/docs/cli/link). +**OIDC (uniquement sur Vercel)** : `VERCEL_OIDC_TOKEN` est injecté automatiquement, il n'y a donc rien à ajouter en production. En local, lancez `vercel env pull` sur un [projet lié](https://vercel.com/docs/cli/link). -### 3. Déployer +### 2. Déployer -Déployez votre site — l'assistant est disponible dès que l'authentification est configurée. +Déployez votre site, l'assistant est disponible dès que l'authentification est configurée. ## Utiliser l'Assistant @@ -255,7 +241,19 @@ export default defineAppConfig({ ### Désactiver l'assistant entièrement -L'assistant est désactivé quand aucune authentification n'est disponible. Pour le désactiver explicitement, supprimez `AI_GATEWAY_API_KEY` de votre environnement : +Passez `enabled` à `false` pour désactiver l'assistant, même quand des identifiants AI Gateway sont disponibles : + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + docus: { + assistant: { + enabled: false + } + } +}) +``` + +L'assistant est aussi désactivé quand aucune authentification n'est disponible : supprimer `AI_GATEWAY_API_KEY` de votre environnement a donc le même effet : ```bash [.env] # AI_GATEWAY_API_KEY=votre-cle-api @@ -271,6 +269,10 @@ Configurez les options avancées dans `nuxt.config.ts` sous `docus.assistant`. export default defineNuxtConfig({ docus: { assistant: { + // Force l'activation ou la désactivation de l'assistant + // Par défaut, détection automatique via les identifiants AI Gateway + enabled: true, + // Modèle IA (utilise le format AI SDK Gateway) model: 'google/gemini-3-flash', @@ -336,6 +338,163 @@ export default defineNuxtConfig({ }) ``` +### Fournisseur IA personnalisé + +L'option `model` ci-dessus résout les modèles via Vercel AI Gateway, elle nécessite donc `AI_GATEWAY_API_KEY` ou `VERCEL_OIDC_TOKEN`. Pour utiliser un autre fournisseur (Mistral, OpenAI, Cloudflare AI Gateway, ou tout autre fournisseur supporté par l'[AI SDK](https://ai-sdk.dev/)), activez explicitement l'assistant et fournissez votre propre endpoint. + +#### 1. Activer l'assistant et choisir un chemin + +Passez `enabled` à `true` pour que l'assistant ne dépende plus des identifiants AI Gateway, et faites pointer `apiPath` vers la route que vous allez créer : + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + docus: { + assistant: { + enabled: true, + apiPath: '/api/assistant' + } + } +}) +``` + +Votre route serveur est toujours prioritaire : quand vous définissez une route sur `apiPath`, Docus s'efface et n'enregistre pas son endpoint intégré à cet emplacement. + +#### 2. Installer un fournisseur + +Installez le paquet du fournisseur AI SDK dont vous avez besoin, par exemple Mistral : + +::code-group +```bash [npm] +npm install @ai-sdk/mistral +``` + +```bash [pnpm] +pnpm add @ai-sdk/mistral +``` + +```bash [yarn] +yarn add @ai-sdk/mistral +``` +:: + +#### 3. Implémenter l'endpoint + +```ts [server/api/assistant.ts] +import { streamText, convertToModelMessages } from 'ai' +import { createMistral } from '@ai-sdk/mistral' + +const mistral = createMistral() + +export default defineEventHandler(async (event) => { + const { messages } = await readBody(event) + + return createAssistantResponse(streamText({ + ...await getAssistantDefaultOptions(event), + model: mistral('mistral-large-latest'), + messages: await convertToModelMessages(messages) + })) +}) +``` + +L'intégration s'arrête là : la recherche dans la documentation, le streaming et la gestion de l'annulation continuent de fonctionner, et tous les paramètres du modèle sont à vous. + +::tip +Parce que vous possédez l'appel à `streamText`, les contraintes spécifiques aux fournisseurs se règlent là où elles doivent l'être. +:: + +::warning +Étalez les valeurs par défaut **en premier**. Les options définies après le spread gagnent, celles définies avant sont écrasées. +:: + +Docus n'expose délibérément pas ces éléments sous forme d'options : les paramètres de modèle ne sont pas portables, donc toute liste serait incomplète pour un fournisseur ou un autre. Ce qu'il expose, c'est la plomberie que vous ne devriez pas avoir à copier-coller, sous forme d'utilitaires serveur auto-importés : + +| Utilitaire | Rôle | +| ---------- | ---- | +| [`getAssistantDefaultOptions(event)`](#getassistantdefaultoptions) | Toutes les options `streamText` utilisées par l'endpoint intégré : outils MCP, annulation à la déconnexion, fermeture du client, prompt de documentation, limites d'étapes et de tokens. | +| [`getAssistantSystemPrompt(event)`](#getassistantsystemprompt) | Le prompt par défaut conçu pour la documentation, seul, pour quand vous voulez l'étendre. | +| [`createAssistantResponse(result)`](#createassistantresponse) | Emballe le résultat dans le format de réponse attendu par l'interface de l'assistant. | + +##### getAssistantDefaultOptions + +Retourne de vraies options `streamText` : vous voyez et pouvez surcharger chacune d'elles. + +| Option | Valeur par défaut | +| ------ | ----------------- | +| `tools` | Les outils MCP de `docus.assistant.mcpServer` | +| `abortSignal` | Annule la génération quand le client se déconnecte | +| `onEnd` / `onAbort` | Ferme le client MCP | +| `onError` | Log l'erreur du fournisseur côté serveur, puis ferme le client MCP | +| `instructions` | `getAssistantSystemPrompt(event)` | +| `maxOutputTokens` | `8000` | +| `maxRetries` | `2` | +| `stopWhen` | `isStepCount(10)` | +| `prepareStep` | Désactive les outils à la dernière étape pour que le modèle réponde au lieu de s'arrêter en plein appel d'outil | +| `experimental_transform` | `smoothStream()` | + +`model` et `messages` ne sont pas inclus, pas plus que les options spécifiques au fournisseur comme `providerOptions` ou `temperature`, puisqu'elles ne sont pas portables. + +Surchargez en définissant l'option après le spread : + +```ts [server/api/assistant.ts] +return createAssistantResponse(streamText({ + ...await getAssistantDefaultOptions(event), + model: mistral('mistral-large-latest'), + // Gagne sur le 8000 par défaut + maxOutputTokens: 4000, + messages: await convertToModelMessages(messages) +})) +``` + +Pour ajouter du comportement à un callback plutôt que le remplacer, gardez une référence et rappelez-le, afin que la fermeture MCP ait toujours lieu : + +```ts [server/api/assistant.ts] +const defaults = await getAssistantDefaultOptions(event) + +return createAssistantResponse(streamText({ + ...defaults, + model: mistral('mistral-large-latest'), + messages: await convertToModelMessages(messages), + onError: (payload) => { + monRapporteurDErreurs(payload.error) + // Ferme quand même le client MCP + defaults.onError(payload) + } +})) +``` + +::warning +`onEnd`, `onAbort` et `onError` ferment le client MCP. Remplacer l'un d'eux sans rappeler l'original fait fuiter une connexion par requête. +:: + +##### getAssistantSystemPrompt + +`getAssistantDefaultOptions` définit déjà ce prompt comme `instructions` : cet utilitaire ne sert donc qu'à l'étendre. Il retourne une simple chaîne, donc concaténez : + +```ts [server/api/assistant.ts] +const defaults = await getAssistantDefaultOptions(event) + +return createAssistantResponse(streamText({ + ...defaults, + model: mistral('mistral-large-latest'), + messages: await convertToModelMessages(messages), + instructions: `${defaults.instructions} + +**Instructions supplémentaires :** +- Toujours mentionner la version minimale supportée +- Ne jamais spéculer sur la roadmap` +})) +``` + +Définissez `instructions` avec votre propre chaîne pour remplacer le prompt entièrement. + +##### createAssistantResponse + +Emballe un résultat de `streamText` dans le format de réponse attendu par l'interface de l'assistant, pour que votre route suive les futurs changements de format sans être modifiée. + +::warning +L'interface de l'assistant communique avec votre route via le `DefaultChatTransport` de l'AI SDK : elle doit donc accepter un `POST` avec un body `{ messages }` de `UIMessage` de l'AI SDK. Construire la réponse vous-même fonctionne, mais vous en assumez alors le format de stream. +:: + ### Nom du site dans les réponses L'assistant utilise automatiquement le nom de votre site dans ses réponses. Configurez le nom du site dans `nuxt.config.ts` : @@ -375,7 +534,7 @@ function askQuestion() { | Propriété | Type | Description | | -------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------- | -| `isEnabled` | `ComputedRef` | Si l'assistant est activé (`AI_GATEWAY_API_KEY` ou `VERCEL_OIDC_TOKEN` au build) | +| `isEnabled` | `ComputedRef` | Si l'assistant est activé (`docus.assistant.enabled`, ou `AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN` au build) | | `isOpen` | `Ref` | Si le panneau est ouvert | | `open(message?, clearPrevious?)` | `Function` | Ouvrir l'assistant, optionnellement avec un message | | `close()` | `Function` | Fermer le panneau de l'assistant | diff --git a/layer/modules/assistant/README.md b/layer/modules/assistant/README.md index 183b7eb18..8fbda17b6 100644 --- a/layer/modules/assistant/README.md +++ b/layer/modules/assistant/README.md @@ -156,6 +156,7 @@ clearMessages() | Option | Type | Default | Description | |--------|------|---------|-------------| +| `enabled` | `boolean` | auto-detected | Force enable or disable the assistant. Defaults to `true` when `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN` is available at build time | | `apiPath` | `string` | `/__docus__/assistant` | API endpoint path for the chat | | `mcpServer` | `string` | `/mcp` | MCP server path or full URL (e.g., `https://docs.example.com/mcp` for external servers) | | `model` | `string` | `google/gemini-3-flash` | AI model identifier for AI SDK Gateway | @@ -208,14 +209,53 @@ Composable for syntax highlighting code blocks with Shiki. - Nuxt 4.x - Nuxt UI 3.x (for `USlideover`, `UButton`, `UTextarea`, `UChatMessages`, etc.) - An MCP server running (path configurable via `mcpServer`) -- `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN` at build time +- `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN` at build time, unless you set `enabled: true` and provide your own endpoint ## Customization -### System Prompt +### Custom provider or system prompt -To customize the AI's behavior, edit the system prompt in: -`runtime/server/api/search.ts` +The endpoint is not configurable by design. To use another provider or different model parameters, set `enabled: true`, point `apiPath` at your own route, and own the `streamText` call: + +```ts +// server/api/assistant.ts +import { streamText, convertToModelMessages } from 'ai' +import { createMistral } from '@ai-sdk/mistral' + +const mistral = createMistral() + +export default defineEventHandler(async (event) => { + const { messages } = await readBody(event) + + return createAssistantResponse(streamText({ + // Spread first, so your options below win + ...await getAssistantDefaultOptions(event), + model: mistral('mistral-large-latest'), + maxOutputTokens: 4000, + messages: await convertToModelMessages(messages), + })) +}) +``` + +Auto-imported server utils, all defined in `runtime/server/utils/assistant.ts`: + +| Util | Role | +|------|------| +| `getAssistantDefaultOptions(event)` | Every `streamText` option the built-in endpoint uses: MCP tools, abort signal, client cleanup, `instructions`, `maxOutputTokens`, `maxRetries`, `stopWhen`, `prepareStep`, `smoothStream`. Spread it first | +| `getAssistantSystemPrompt(event)` | The documentation prompt on its own, for extending it by concatenation | +| `createAssistantResponse(result)` | Wraps a `streamText` result in the stream format the UI expects | + +`model`, `messages` and provider specific options (`providerOptions`, `temperature`) are excluded, since they don't port across providers. + +The built-in endpoint lives in `runtime/server/api/assistant.ts` and is written with these same three utils, so it doubles as the reference implementation to copy. + +A working override on a non-Gateway provider lives in `playground/server/api/assistant.ts` (Mistral). Run it with: + +```bash +MISTRAL_API_KEY=... pnpm playground:dev +``` + +A server route you define at `apiPath` always takes precedence over the built-in endpoint. ### Styling diff --git a/layer/modules/assistant/index.ts b/layer/modules/assistant/index.ts index beaa76709..ff43e0ad4 100644 --- a/layer/modules/assistant/index.ts +++ b/layer/modules/assistant/index.ts @@ -1,7 +1,21 @@ -import { addComponent, addImports, addServerHandler, createResolver, defineNuxtModule, logger } from '@nuxt/kit' +import { addComponent, addImports, addServerHandler, addServerImports, createResolver, defineNuxtModule, logger } from '@nuxt/kit' import { defu } from 'defu' export interface AssistantModuleOptions { + /** + * Enable the assistant. + * + * When left undefined, the assistant is enabled if `AI_GATEWAY_API_KEY` or + * `VERCEL_OIDC_TOKEN` is available at build time. + * + * Set it to `true` to use a custom AI SDK provider: + * - Docus skips registering its own endpoint, + * - You provide a route at `apiPath` built with `createAssistantResponse`. + * + * Set it to `false` to disable the assistant. + * @default undefined + */ + enabled?: boolean /** * API endpoint path for the assistant * @default '/__docus__/assistant' @@ -21,9 +35,15 @@ export interface AssistantModuleOptions { model?: string } +/** Subset of the Nitro instance used to hand the endpoint over to a user route. */ +interface NitroBuildContext { + scannedHandlers: Array<{ route?: string }> + options: { handlers: Array<{ route?: string, handler?: string }> } +} + const log = logger.withTag('docus') -const defaults: Required = { +const defaults: Required> = { apiPath: '/__docus__/assistant', mcpServer: '/mcp', model: 'google/gemini-3-flash', @@ -45,10 +65,17 @@ export default defineNuxtModule({ process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN ) + // An explicit `enabled` value always wins, so a custom AI SDK provider can + // be used without AI Gateway credentials. + const isEnabled = options.enabled ?? hasAiGatewayAuth + // Docus only owns the endpoint when it can authenticate to the AI Gateway. + // Otherwise the user brings their own route at `apiPath`. + const hasDefaultHandler = isEnabled && hasAiGatewayAuth + const { resolve } = createResolver(import.meta.url) nuxt.options.runtimeConfig.public.assistant = { - enabled: hasAiGatewayAuth, + enabled: isEnabled, apiPath: options.apiPath, } @@ -68,7 +95,7 @@ export default defineNuxtModule({ components.forEach(name => addComponent({ name, - filePath: hasAiGatewayAuth + filePath: isEnabled ? resolve(`./runtime/components/${name}.vue`) : resolve('./runtime/components/AssistantChatDisabled.vue'), }), @@ -79,22 +106,64 @@ export default defineNuxtModule({ filePath: resolve('./runtime/components/AssistantComark'), }) - if (!hasAiGatewayAuth) { - nuxt.hook('modules:done', () => { - log.warn('AI assistant disabled: neither `AI_GATEWAY_API_KEY` nor `VERCEL_OIDC_TOKEN` found') - }) - return - } - nuxt.options.runtimeConfig.assistant = { mcpServer: options.mcpServer, model: options.model, } + // Exposed even when disabled so overriding the endpoint stays type-safe. + addServerImports([ + 'getAssistantDefaultOptions', + 'getAssistantSystemPrompt', + 'createAssistantResponse', + ].map(name => ({ + name, + from: resolve('./runtime/server/utils/assistant'), + }))) + + if (!isEnabled) { + if (options.enabled === undefined) { + nuxt.hook('modules:done', () => { + log.warn('AI assistant disabled: neither `AI_GATEWAY_API_KEY` nor `VERCEL_OIDC_TOKEN` found') + }) + } + return + } + + if (!hasDefaultHandler) { + nuxt.hook('modules:done', () => { + log.info(`AI assistant enabled without AI Gateway credentials: provide a server route at \`${options.apiPath}\` using \`getAssistantDefaultOptions\``) + }) + return + } + const routePath = options.apiPath!.replace(/^\//, '') - addServerHandler({ - route: `/${routePath}`, - handler: resolve('./runtime/server/api/search'), + const route = `/${routePath}` + const handler = resolve('./runtime/server/api/assistant') + + addServerHandler({ route, handler }) + + // A server route defined by the user at the same path would otherwise be + // silently shadowed by the handler above, so drop ours when it exists. + // `nitro:build:before` is declared by `@nuxt/nitro-server`, which Docus does + // not depend on directly, hence the local typing. + const hookNitroBuild = nuxt.hook as unknown as ( + name: 'nitro:build:before', + callback: (nitro: NitroBuildContext) => void, + ) => void + + hookNitroBuild('nitro:build:before', (nitro) => { + if (!nitro.scannedHandlers.some(scanned => scanned.route === route)) { + return + } + + const index = nitro.options.handlers.findIndex(h => h.route === route && h.handler === handler) + if (index === -1) { + return + } + + nitro.options.handlers.splice(index, 1) + log.info(`AI assistant using your \`${route}\` server route instead of the built-in endpoint`) }) }, }) diff --git a/layer/modules/assistant/runtime/server/api/assistant.ts b/layer/modules/assistant/runtime/server/api/assistant.ts new file mode 100644 index 000000000..8eb8bc20d --- /dev/null +++ b/layer/modules/assistant/runtime/server/api/assistant.ts @@ -0,0 +1,20 @@ +import { streamText, convertToModelMessages } from 'ai' + +/** + * Built-in assistant endpoint, resolving its model through the Vercel AI Gateway. + * + * This is also the reference implementation: to use another provider, copy it + * into your own route at `docus.assistant.apiPath` and swap the `model`. + */ +export default defineEventHandler(async (event) => { + const { messages } = await readBody(event) + const config = useRuntimeConfig() + + return createAssistantResponse(streamText({ + ...await getAssistantDefaultOptions(event), + model: config.assistant.model, + // Gateway specific, so it stays out of the shared defaults. + providerOptions: { gateway: { caching: 'auto' } }, + messages: await convertToModelMessages(messages), + })) +}) diff --git a/layer/modules/assistant/runtime/server/api/search.ts b/layer/modules/assistant/runtime/server/utils/assistant.ts similarity index 65% rename from layer/modules/assistant/runtime/server/api/search.ts rename to layer/modules/assistant/runtime/server/utils/assistant.ts index 5d4133348..514c61591 100644 --- a/layer/modules/assistant/runtime/server/api/search.ts +++ b/layer/modules/assistant/runtime/server/utils/assistant.ts @@ -1,10 +1,30 @@ -import { streamText, convertToModelMessages, isStepCount, smoothStream, toUIMessageStream, createUIMessageStreamResponse } from 'ai' -import type { ToolSet } from 'ai' +import { toUIMessageStream, createUIMessageStreamResponse, isStepCount, smoothStream } from 'ai' +import type { streamText, ToolSet } from 'ai' import { createMCPClient } from '@ai-sdk/mcp' import type { H3Event } from 'h3' +type StreamTextOptions = Parameters[0] + +/** Max model/tool steps before the assistant is forced to produce a final answer. */ const MAX_STEPS = 10 +export interface AssistantDefaultOptions { + /** MCP tools exposed by the configured documentation server. */ + tools: ToolSet + /** Aborts generation when the client disconnects. */ + abortSignal: AbortSignal + onEnd: () => void + onAbort: () => void + onError: (payload: { error: unknown }) => void + /** The documentation-tuned prompt, built from the site name. */ + instructions: string + maxOutputTokens: number + maxRetries: number + stopWhen: StreamTextOptions['stopWhen'] + prepareStep: StreamTextOptions['prepareStep'] + experimental_transform: StreamTextOptions['experimental_transform'] +} + function createLocalFetch(event: H3Event): typeof fetch { const origin = getRequestURL(event).origin @@ -22,50 +42,11 @@ function createLocalFetch(event: H3Event): typeof fetch { } } -function getSystemPrompt(siteName: string) { - return `You are the documentation assistant for ${siteName}. Help users navigate and understand the project documentation. - -**Your identity:** -- You are an assistant helping users with ${siteName} documentation -- NEVER use first person ("I", "me", "my") - always refer to the project by name: "${siteName} provides...", "${siteName} supports...", "The project offers..." -- Be confident and knowledgeable about the project -- Speak as a helpful guide, not as the documentation itself - -**Tool usage (CRITICAL):** -- You have tools: list-pages (discover pages) and get-page (read a page) -- If a page title clearly matches the question, read it directly without listing first -- ALWAYS respond with text after using tools - never end with just tool calls - -**Guidelines:** -- If you can't find something, say "There is no documentation on that yet" or "${siteName} doesn't cover that topic yet" -- Be concise, helpful, and direct -- Guide users like a friendly expert would - -**Links and exploration:** -- Tool results include a \`url\` for each page — prefer markdown links \`[label](url)\` so users can open the doc in one click -- When it helps, add extra links (related pages, "read more", side topics) — make the answer easy to dig into, not a wall of text -- Stick to URLs from tool results (\`url\` / \`path\`) so links stay valid - -**FORMATTING RULES (CRITICAL):** -- NEVER use markdown headings (#, ##, ###, etc.) -- Use **bold text** for emphasis and section labels -- Start responses with content directly, never with a heading -- Use bullet points for lists -- Keep code examples focused and minimal - -**Response style:** -- Conversational but professional -- "Here's how you can do that:" instead of "The documentation shows:" -- "${siteName} supports TypeScript out of the box" instead of "I support TypeScript" -- Provide actionable guidance, not just information dumps` -} - -export default defineEventHandler(async (event) => { - const { messages } = await readBody(event) +/** + * Every `streamText` option the built-in assistant endpoint uses. + */ +export async function getAssistantDefaultOptions(event: H3Event): Promise { const config = useRuntimeConfig() - const siteConfig = getSiteConfig(event) - - const siteName = siteConfig.name || 'Documentation' const mcpServer = config.assistant.mcpServer const isExternalUrl = mcpServer.startsWith('http://') || mcpServer.startsWith('https://') @@ -88,6 +69,8 @@ export default defineEventHandler(async (event) => { } } else { + // The internal MCP route is not always reachable over the network in + // production, so route the request through the event instead. transport = { type: 'http', url: `${getRequestURL(event).origin}${baseURL}${mcpServer}`, @@ -96,36 +79,78 @@ export default defineEventHandler(async (event) => { } const httpClient = await createMCPClient({ transport }) - const mcpTools = await httpClient.tools() + const tools = await httpClient.tools() as ToolSet - const closeMcp = () => event.waitUntil(httpClient.close()) + const close = () => event.waitUntil(httpClient.close()) - const result = streamText({ - model: config.assistant.model, + return { + tools, + abortSignal: abortController.signal, + onEnd: close, + onAbort: close, + onError: ({ error }) => { + console.error('[docus] assistant error:', error) + close() + }, + instructions: getAssistantSystemPrompt(event), maxOutputTokens: 8000, maxRetries: 2, - abortSignal: abortController.signal, stopWhen: isStepCount(MAX_STEPS), - // On the last allowed step, disable tools so the model is forced to - // produce a final text answer instead of stopping mid tool-calling. prepareStep: ({ stepNumber }) => { return stepNumber >= MAX_STEPS - 1 ? { toolChoice: 'none' } : {} }, - providerOptions: { - gateway: { - caching: 'auto', - }, - }, - instructions: getSystemPrompt(siteName), - messages: await convertToModelMessages(messages), - tools: mcpTools as ToolSet, experimental_transform: smoothStream(), - onEnd: closeMcp, - onAbort: closeMcp, - onError: closeMcp, - }) + } +} + +/** + * Build the default documentation assistant prompt, tuned for the current site. + */ +export function getAssistantSystemPrompt(event: H3Event): string { + const siteName = getSiteConfig(event).name || 'Documentation' + + return `You are the documentation assistant for ${siteName}. Help users navigate and understand the project documentation. + +**Your identity:** +- You are an assistant helping users with ${siteName} documentation +- NEVER use first person ("I", "me", "my") - always refer to the project by name: "${siteName} provides...", "${siteName} supports...", "The project offers..." +- Be confident and knowledgeable about the project +- Speak as a helpful guide, not as the documentation itself +**Tool usage (CRITICAL):** +- You have tools: list-pages (discover pages) and get-page (read a page) +- If a page title clearly matches the question, read it directly without listing first +- ALWAYS respond with text after using tools - never end with just tool calls + +**Guidelines:** +- If you can't find something, say "There is no documentation on that yet" or "${siteName} doesn't cover that topic yet" +- Be concise, helpful, and direct +- Guide users like a friendly expert would + +**Links and exploration:** +- Tool results include a \`url\` for each page — prefer markdown links \`[label](url)\` so users can open the doc in one click +- When it helps, add extra links (related pages, "read more", side topics) — make the answer easy to dig into, not a wall of text +- Stick to URLs from tool results (\`url\` / \`path\`) so links stay valid + +**FORMATTING RULES (CRITICAL):** +- NEVER use markdown headings (#, ##, ###, etc.) +- Use **bold text** for emphasis and section labels +- Start responses with content directly, never with a heading +- Use bullet points for lists +- Keep code examples focused and minimal + +**Response style:** +- Conversational but professional +- "Here's how you can do that:" instead of "The documentation shows:" +- "${siteName} supports TypeScript out of the box" instead of "I support TypeScript" +- Provide actionable guidance, not just information dumps` +} + +/** + * Wrap a `streamText` result in the response format the assistant UI expects. + */ +export function createAssistantResponse(result: ReturnType): Response { return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }), }) -}) +} diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 782c45510..262ea296e 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,4 +1,11 @@ export default defineNuxtConfig({ + docus: { + assistant: { + enabled: Boolean(process.env.MISTRAL_API_KEY), + apiPath: '/api/assistant', + }, + }, + // Explicitly disable i18n for playground testing (enabled by .nuxtrc) i18n: false, }) diff --git a/playground/package.json b/playground/package.json index 0f76c43e6..39530f8c1 100644 --- a/playground/package.json +++ b/playground/package.json @@ -5,8 +5,10 @@ "build": "nuxt build --extends docus" }, "dependencies": { - "docus": "latest", + "docus": "workspace:*", + "@ai-sdk/mistral": "^4.0.33", + "ai": "^7.0.77", "better-sqlite3": "^12.5.0", "nuxt": "^4.3.1" } -} \ No newline at end of file +} diff --git a/playground/server/api/assistant.ts b/playground/server/api/assistant.ts new file mode 100644 index 000000000..cbff59f7b --- /dev/null +++ b/playground/server/api/assistant.ts @@ -0,0 +1,32 @@ +import { streamText, convertToModelMessages } from 'ai' +import { createMistral } from '@ai-sdk/mistral' + +/** + * Custom assistant endpoint, rebuilt on Mistral instead of the Vercel AI Gateway. + */ + +// Reads MISTRAL_API_KEY from the environment. +const mistral = createMistral() + +export default defineEventHandler(async (event) => { + const { messages } = await readBody(event) + const defaults = await getAssistantDefaultOptions(event) + + return createAssistantResponse(streamText({ + ...defaults, + model: mistral('mistral-small-latest'), + + // Mistral rejects the 8000 the built-in endpoint uses. + maxOutputTokens: 4000, + temperature: 0.3, + + // Extend the default prompt instead of replacing it. + instructions: `${defaults.instructions} + +**Playground rules:** +- Mention that answers come from the Docus playground running on Mistral +- Keep answers under five bullet points`, + + messages: await convertToModelMessages(messages), + })) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8de8f8715..e846850b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -224,6 +224,24 @@ importers: specifier: ^3.25.2 version: 3.25.2(zod@4.4.3) + playground: + dependencies: + '@ai-sdk/mistral': + specifier: ^4.0.33 + version: 4.0.33(zod@4.4.3) + ai: + specifier: ^7.0.29 + version: 7.0.29(zod@4.4.3) + better-sqlite3: + specifier: ^12.5.0 + version: 12.11.1 + docus: + specifier: workspace:* + version: link:../layer + nuxt: + specifier: ^4.3.1 + version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.144.0)(@parcel/watcher@2.5.6)(@types/node@26.2.0)(@vue/compiler-sfc@3.5.41)(better-sqlite3@12.11.1)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.4)(rollup@4.62.2))(rollup@4.62.2)(srvx@0.12.7)(supports-color@10.2.2)(terser@5.49.0)(tsx@4.23.12)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + packages: '@ai-sdk/gateway@4.0.21': @@ -238,6 +256,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/mistral@4.0.33': + resolution: {integrity: sha512-+LmYNzmqclv1FWErfKghJqVsPY/VUbHwZzNpbx0wfxeYpcR2ioQwtukv3PWoCLa2jSiJoK8HDK9P/Aaw6GexiQ==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@5.0.10': resolution: {integrity: sha512-uPyec0+85dwxZYXtb8qe8gCjhjDfxP4LCDo/uRQS/iG+FIgYbHPRhr/ys281udG90bTaE18+5cxWraYaf8oHCw==} engines: {node: '>=22'} @@ -250,6 +274,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@5.0.30': + resolution: {integrity: sha512-9TyxUXolql77ntHIeygLqt7PO1O0HZPB73cJhLG2OsPecSBRIZhNXLxT1T7rlL6Zpm1eDoLMFrMV99kAJ28/Sg==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider@4.0.3': resolution: {integrity: sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==} engines: {node: '>=22'} @@ -258,6 +288,10 @@ packages: resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==} engines: {node: '>=22'} + '@ai-sdk/provider@4.0.8': + resolution: {integrity: sha512-aWO7iwhFUGf347tCwNGggggfmZigaSu7TF739IZSrWWABUp7zkb4Cr3fMqvBe5EIS7ABJJu3Cadn0g/zs1G0QQ==} + engines: {node: '>=22'} + '@ai-sdk/vue@4.0.29': resolution: {integrity: sha512-oowEVPi1ESTx69qy3hP8r/ZKsXjumDhnXZGdppy0pNTBAWyG88MEH70X0QzS2nem3+g1/Wzj4XbyHLrfD0lO+Q==} engines: {node: '>=22'} @@ -9508,6 +9542,12 @@ snapshots: pkce-challenge: 5.0.1 zod: 4.4.3 + '@ai-sdk/mistral@4.0.33(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.8 + '@ai-sdk/provider-utils': 5.0.30(zod@4.4.3) + zod: 4.4.3 + '@ai-sdk/provider-utils@5.0.10(zod@4.4.3)': dependencies: '@ai-sdk/provider': 4.0.3 @@ -9525,6 +9565,15 @@ snapshots: undici: 7.29.0 zod: 4.4.3 + '@ai-sdk/provider-utils@5.0.30(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.8 + '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 + eventsource-parser: 3.1.0 + undici: 7.29.0 + zod: 4.4.3 + '@ai-sdk/provider@4.0.3': dependencies: json-schema: 0.4.0 @@ -9533,6 +9582,10 @@ snapshots: dependencies: json-schema: 0.4.0 + '@ai-sdk/provider@4.0.8': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/vue@4.0.29(vue@3.5.41(typescript@6.0.3))(zod@4.4.3)': dependencies: '@ai-sdk/provider-utils': 5.0.10(zod@4.4.3) @@ -12048,6 +12101,91 @@ snapshots: - webpack - xml2js + '@nuxt/nitro-server@4.5.2(2a6e92daab8b39c44a647d8d6ca2b962)': + dependencies: + '@nuxt/devalue': 2.0.2 + '@nuxt/kit': 4.5.2(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))) + '@unhead/vue': 3.3.2(@oxc-project/types@0.144.0)(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@vue/shared': 3.5.41 + consola: 3.4.2 + defu: 6.1.7 + destr: 2.0.5 + devalue: 5.9.0 + errx: 0.1.2 + escape-string-regexp: 5.0.0 + exsolve: 1.1.1 + h3: 1.15.11 + impound: 1.1.7(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) + klona: 2.0.6 + mocked-exports: 0.1.1 + nitropack: 2.13.4(better-sqlite3@12.11.1)(oxc-parser@0.143.0)(rolldown@1.2.4)(srvx@0.12.7)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) + nostics: 1.2.0 + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.144.0)(@parcel/watcher@2.5.6)(@types/node@26.2.0)(@vue/compiler-sfc@3.5.41)(better-sqlite3@12.11.1)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.4)(rollup@4.62.2))(rollup@4.62.2)(srvx@0.12.7)(supports-color@10.2.2)(terser@5.49.0)(tsx@4.23.12)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nypm: 0.6.9 + ohash: 2.0.12 + pathe: 2.0.3 + rou3: 0.9.2 + std-env: 4.2.0 + ufo: 1.6.4 + unctx: 3.0.0(magic-string@1.2.0)(oxc-parser@0.143.0)(rolldown@1.2.4)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))) + unstorage: 1.17.5(db0@0.3.4(better-sqlite3@12.11.1))(ioredis@5.11.1(supports-color@10.2.2)) + vue: 3.5.41(typescript@6.0.3) + vue-bundle-renderer: 2.3.1 + vue-devtools-stub: 0.1.0 + optionalDependencies: + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@farmfe/core' + - '@libsql/client' + - '@netlify/blobs' + - '@oxc-project/types' + - '@planetscale/database' + - '@rspack/core' + - '@unhead/cli' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools-kit' + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - bun-types-no-globals + - db0 + - drizzle-orm + - encoding + - esbuild + - idb-keyval + - ioredis + - lightningcss + - magic-string + - magicast + - mysql2 + - oxc-parser + - react-native-b4a + - rolldown + - rollup + - sqlite3 + - srvx + - supports-color + - typescript + - unloader + - unplugin + - uploadthing + - vite + - webpack + - xml2js + '@nuxt/nitro-server@4.5.2(4f17f64dd2fbdc1804285c06ff55d677)': dependencies: '@nuxt/devalue': 2.0.2 @@ -12555,6 +12693,75 @@ snapshots: - webpack - yaml + '@nuxt/vite-builder@4.5.2(61ace4baf0817637bede22ce20555bb9)': + dependencies: + '@nuxt/kit': 4.5.2(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))) + '@vitejs/plugin-vue': 6.0.8(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@vitejs/plugin-vue-jsx': 5.1.6(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + autoprefixer: 10.5.4(postcss@8.5.26) + consola: 3.4.2 + cssnano: 8.0.2(postcss@8.5.26) + defu: 6.1.7 + escape-string-regexp: 5.0.0 + exsolve: 1.1.1 + generic-names: 4.0.0 + get-port-please: 3.2.0 + jiti: 2.7.0 + js-tokens: 10.0.0 + knitwork: 1.3.0 + mlly: 1.8.2 + mocked-exports: 0.1.1 + nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.144.0)(@parcel/watcher@2.5.6)(@types/node@26.2.0)(@vue/compiler-sfc@3.5.41)(better-sqlite3@12.11.1)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.4)(rollup@4.62.2))(rollup@4.62.2)(srvx@0.12.7)(supports-color@10.2.2)(terser@5.49.0)(tsx@4.23.12)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0) + nypm: 0.6.9 + pathe: 2.0.3 + pkg-types: 2.3.1 + postcss: 8.5.26 + rolldown-string: 0.3.1(rolldown@1.2.4) + seroval: 1.6.2 + std-env: 4.2.0 + ufo: 1.6.4 + unenv: 2.0.0-rc.24 + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0) + vite-node: 6.0.0(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0) + vite-plugin-checker: 0.14.5(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(meow@13.2.0)(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3)) + vue: 3.5.41(typescript@6.0.3) + vue-bundle-renderer: 2.3.1 + optionalDependencies: + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + rolldown: 1.2.4 + rollup-plugin-visualizer: 7.0.1(rolldown@1.2.4)(rollup@4.62.2) + transitivePeerDependencies: + - '@biomejs/biome' + - '@farmfe/core' + - '@rspack/core' + - '@types/node' + - '@vitejs/devtools' + - bun-types-no-globals + - esbuild + - eslint + - less + - magic-string + - magicast + - meow + - optionator + - oxc-parser + - oxlint + - rollup + - sass + - sass-embedded + - stylelint + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - unloader + - vue-tsc + - webpack + - yaml + '@nuxt/vite-builder@4.5.2(7674a6ff7fb0f48ae65476afeeeaac2b)': dependencies: '@nuxt/kit': 4.5.2(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))) @@ -18738,6 +18945,148 @@ snapshots: - xml2js - yaml + nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.144.0)(@parcel/watcher@2.5.6)(@types/node@26.2.0)(@vue/compiler-sfc@3.5.41)(better-sqlite3@12.11.1)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.4)(rollup@4.62.2))(rollup@4.62.2)(srvx@0.12.7)(supports-color@10.2.2)(terser@5.49.0)(tsx@4.23.12)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0): + dependencies: + '@dxup/nuxt': 0.5.8(esbuild@0.28.1)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) + '@nuxt/cli': 3.37.0(@nuxt/schema@4.5.2)(cac@6.7.14)(magicast@0.5.4)(supports-color@10.2.2) + '@nuxt/devtools': 3.4.1(db0@0.3.4(better-sqlite3@12.11.1))(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.0)(oxc-parser@0.143.0)(rolldown@1.2.4)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@nuxt/kit': 4.5.2(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))) + '@nuxt/nitro-server': 4.5.2(2a6e92daab8b39c44a647d8d6ca2b962) + '@nuxt/schema': 4.5.2 + '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.5.2(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)))) + '@nuxt/vite-builder': 4.5.2(61ace4baf0817637bede22ce20555bb9) + '@unhead/vue': 3.3.2(@oxc-project/types@0.144.0)(esbuild@0.28.1)(lightningcss@1.33.0)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@vue/shared': 3.5.41 + chokidar: 5.0.0 + compatx: 0.2.0 + consola: 3.4.2 + cookie-es: 3.1.1 + defu: 6.1.7 + devalue: 5.9.0 + errx: 0.1.2 + escape-string-regexp: 5.0.0 + exsolve: 1.1.1 + fnv1a-64: 0.1.2 + hookable: 6.1.1 + ignore: 7.0.6 + impound: 1.1.7(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) + jiti: 2.7.0 + klona: 2.0.6 + knitwork: 1.3.0 + magic-string: 1.2.0 + mlly: 1.8.2 + nanotar: 0.3.0 + nostics: 1.2.0 + nypm: 0.6.9 + object-identity: 0.2.3 + ofetch: 1.5.1 + ohash: 2.0.11 + on-change: 6.0.2 + oxc-walker: 1.1.1(@oxc-project/types@0.144.0)(oxc-parser@0.143.0)(rolldown@1.2.4) + pathe: 2.0.3 + perfect-debounce: 2.1.0 + picomatch: 4.0.5 + pkg-types: 2.3.1 + rolldown: 1.2.4 + rolldown-string: 0.3.1(rolldown@1.2.4) + rou3: 0.9.2 + scule: 1.3.0 + std-env: 4.2.0 + tinyglobby: 0.2.17 + ufo: 1.6.4 + ultrahtml: 1.7.0 + uncrypto: 0.1.3 + unctx: 3.0.0(magic-string@1.2.0)(oxc-parser@0.143.0)(rolldown@1.2.4)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))) + undici: 8.10.0 + unhead: 3.3.2(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) + unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) + unrouting: 0.2.3 + untyped: 2.0.0 + verkit: 0.3.2 + vue: 3.5.41(typescript@6.0.3) + vue-component-type-helpers: 3.3.10 + vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + optionalDependencies: + '@parcel/watcher': 2.5.6 + '@types/node': 26.2.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@babel/plugin-proposal-decorators' + - '@babel/plugin-syntax-jsx' + - '@babel/plugin-syntax-typescript' + - '@biomejs/biome' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@farmfe/core' + - '@libsql/client' + - '@netlify/blobs' + - '@oxc-project/types' + - '@pinia/colada' + - '@planetscale/database' + - '@rollup/plugin-babel' + - '@rspack/core' + - '@unhead/cli' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools' + - '@vitejs/devtools-kit' + - '@vue/compiler-sfc' + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - bufferutil + - bun-types-no-globals + - cac + - commander + - db0 + - drizzle-orm + - encoding + - esbuild + - eslint + - idb-keyval + - ioredis + - less + - lightningcss + - magicast + - meow + - mysql2 + - optionator + - oxc-parser + - oxlint + - pinia + - react-native-b4a + - rollup + - rollup-plugin-visualizer + - sass + - sass-embedded + - sqlite3 + - srvx + - stylelint + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - unloader + - uploadthing + - utf-8-validate + - vite + - vue-tsc + - webpack + - xml2js + - yaml + nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.144.0)(@parcel/watcher@2.5.6)(@types/node@26.2.0)(@vue/compiler-sfc@3.5.41)(better-sqlite3@12.11.1)(cac@7.0.0)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(optionator@0.9.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.4)(rollup@4.62.2))(rollup@4.62.2)(srvx@0.12.7)(supports-color@10.2.2)(terser@5.49.0)(tsx@4.23.12)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.0): dependencies: '@dxup/nuxt': 0.5.8(esbuild@0.28.1)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.12)(yaml@2.9.0)) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7043b704c..028e615ef 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - cli - docs - layer + - playground overrides: h3: 1.15.11