Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/content/docs/i18n.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,77 @@ An app-provided `localization` override wins byte-for-byte; otherwise the string
| `blog.search.placeholder` | Type to search... |
| `blog.search.searching` | Searching... |

## AI Chat

Legacy `AiChatLocalization` values override these catalog entries when both are configured.

| Key | Default |
| --- | --- |
| `aiChat.a11y.assistantMessage` | AI response |
| `aiChat.a11y.clearChat` | Clear chat |
| `aiChat.a11y.closeChat` | Close chat |
| `aiChat.a11y.closeSidebar` | Close sidebar |
| `aiChat.a11y.conversationActions` | Conversation actions |
| `aiChat.a11y.openChat` | Open chat |
| `aiChat.a11y.openMenu` | Open menu |
| `aiChat.a11y.openSidebar` | Open sidebar |
| `aiChat.a11y.title` | AI Chat |
| `aiChat.a11y.userMessage` | Your message |
| `aiChat.chat.emptyState` | Start a conversation... |
| `aiChat.chat.error` | Something went wrong. Please try again. |
| `aiChat.chat.loading` | Thinking... |
| `aiChat.chat.placeholder` | Type a message... |
| `aiChat.chat.send` | Send |
| `aiChat.conversation.delete` | Delete |
| `aiChat.conversation.deleteCancel` | Cancel |
| `aiChat.conversation.deleteConfirmButton` | Delete |
| `aiChat.conversation.deleteConfirmDescription` | Are you sure you want to delete this conversation? This action cannot be undone. |
| `aiChat.conversation.deleteConfirmTitle` | Delete conversation |
| `aiChat.conversation.rename` | Rename |
| `aiChat.conversation.renameCancel` | Cancel |
| `aiChat.conversation.renameDescription` | Enter a new title for this conversation. |
| `aiChat.conversation.renamePlaceholder` | Enter conversation name |
| `aiChat.conversation.renameSave` | Save |
| `aiChat.conversation.titleRequired` | Title is required |
| `aiChat.errors.genericMessage` | An error occurred while loading the chat. Please try again. |
| `aiChat.errors.genericTitle` | Something went wrong |
| `aiChat.errors.missingConversation` | Conversation is required |
| `aiChat.errors.notFoundDescription` | The conversation you're looking for doesn't exist or has been deleted. |
| `aiChat.errors.notFoundTitle` | Chat not found |
| `aiChat.files.attach` | Attach file |
| `aiChat.files.fallbackName` | File |
| `aiChat.files.remove` | Remove file |
| `aiChat.files.tooLarge` | File must be less than 10MB |
| `aiChat.files.uploadFailure` | Failed to attach file |
| `aiChat.files.uploadSuccess` | File attached |
| `aiChat.images.attachedAlt` | Attached image \{\{count\}\} |
| `aiChat.images.generatedAlt` | Image \{\{count\}\} |
| `aiChat.messages.cancel` | Cancel |
| `aiChat.messages.copied` | Copied! |
| `aiChat.messages.copy` | Copy message |
| `aiChat.messages.edit` | Edit message |
| `aiChat.messages.retry` | Retry |
| `aiChat.messages.save` | Save |
| `aiChat.sidebar.empty` | No conversations yet |
| `aiChat.sidebar.newChat` | New chat |
| `aiChat.time.daysAgo` | \{\{count\}\} days ago |
| `aiChat.time.hoursAgo` | \{\{count\}\} hours ago |
| `aiChat.time.justNow` | Just now |
| `aiChat.time.minutesAgo` | \{\{count\}\} minutes ago |
| `aiChat.time.yesterday` | Yesterday |
| `aiChat.toasts.deleteFailure` | Failed to delete conversation |
| `aiChat.toasts.deleteSuccess` | Conversation deleted |
| `aiChat.toasts.renameFailure` | Failed to rename conversation |
| `aiChat.toasts.renameSuccess` | Conversation renamed |
| `aiChat.tools.executionFailed` | Tool execution failed |
| `aiChat.tools.handlerMissing` | No client-side handler registered for tool "\{\{toolName\}\}". The page context may have changed while the response was streaming. |
| `aiChat.tools.id` | ID: \{\{id\}\} |
| `aiChat.tools.input` | Input |
| `aiChat.tools.output` | Output |
| `aiChat.tools.status.complete` | Complete |
| `aiChat.tools.status.error` | Error |
| `aiChat.tools.status.executing` | Executing... |
| `aiChat.tools.status.pending` | Pending |
| `aiChat.tools.status.running` | Running... |

Other plugins adopt the same convention as their phase-2 sweeps land.
92 changes: 83 additions & 9 deletions docs/content/docs/plugins/ai-chat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,11 @@ The AI Chat plugin supports two distinct modes:
The AI Chat plugin provides the following API endpoints (mounted at your configured `apiBasePath`):

- **POST** `/chat` - Send a message and receive streaming response
- **GET** `/conversations` - List all conversations (authenticated mode only)
- **GET** `/conversations/:id` - Get a conversation with messages
- **POST** `/conversations` - Create a new conversation
- **PUT** `/conversations/:id` - Update (rename) a conversation
- **DELETE** `/conversations/:id` - Delete a conversation
- **GET** `/chat/conversations` - List all conversations (authenticated mode only)
- **GET** `/chat/conversations/:id` - Get a conversation with messages
- **POST** `/chat/conversations` - Create a new conversation
- **PUT** `/chat/conversations/:id` - Rename a conversation; the title is trimmed and must not be empty
- **DELETE** `/chat/conversations/:id` - Delete a conversation

### Page Routes

Expand Down Expand Up @@ -397,7 +397,30 @@ aiChatClientPlugin({

### Adding Authorization

To add authorization rules and customize behavior, you can use the lifecycle hooks defined in the API Reference section below. These hooks allow you to control access to API endpoints, add logging, and customize the plugin's behavior to fit your application's needs.
Use the `auth` provider on `StackProvider` for client-side route and control visibility, and backend lifecycle hooks for authoritative API authorization. Authenticated mode uses this permission map:

| Resource | Action | UI covered |
| --- | --- | --- |
| `ai-chat:conversation` | `read` | `/chat`, `/chat/:id`, and conversation history |
| `ai-chat:conversation` | `create` | New chat and the first persisted send |
| `ai-chat:conversation` | `update` | Continue, retry, edit, and rename; receives `{ id }` when available |
| `ai-chat:conversation` | `delete` | Delete; receives `{ id }` |

```tsx
<StackProvider
auth={{
getIdentity: () => session?.user ?? null,
can: ({ resource, action, params }) =>
authorizeConversation(resource, action, params?.id),
loginPath: "/sign-in",
}}
// ...
>
{children}
</StackProvider>
```

Without an auth provider, permission checks remain permissive for backward compatibility. Public mode intentionally bypasses conversation permission gates because it is stateless; protect the public streaming endpoint with backend rate limits or `onBeforeChat` as needed.

## API Reference

Expand Down Expand Up @@ -717,6 +740,7 @@ import {
useSuspenseConversation,
useCreateConversation,
useRenameConversation,
useRenameConversationForm,
useDeleteConversation,
} from "@btst/stack/plugins/ai-chat/client/hooks"
```
Expand All @@ -737,6 +761,12 @@ import {

<AutoTypeTable path="../packages/stack/src/plugins/ai-chat/client/hooks/chat-hooks.tsx" name="UseConversationResult" />

### UseRenameConversationFormOptions

<AutoTypeTable path="../packages/stack/src/plugins/ai-chat/client/hooks/chat-hooks.tsx" name="UseRenameConversationFormOptions" />

`useRenameConversationForm()` trims the submitted title, maps server validation issues to `fieldErrors.title`, sends success and non-field failures through the `StackProvider` `notify` provider, and preserves the conversation detail cache while refreshing the list.

**Example usage:**

```tsx
Expand All @@ -745,6 +775,7 @@ import {
useConversation,
useCreateConversation,
useRenameConversation,
useRenameConversationForm,
useDeleteConversation,
} from "@btst/stack/plugins/ai-chat/client/hooks"

Expand Down Expand Up @@ -777,6 +808,35 @@ function ConversationsList() {
}
```

For a custom rename dialog, prefer the form lifecycle over calling the raw mutation directly:

```tsx
const renameForm = useRenameConversationForm({
conversation,
onSuccess: () => setOpen(false),
})

await renameForm.submit({ title })

return renameForm.fieldErrors.title ? (
<p role="alert">{renameForm.fieldErrors.title}</p>
) : null
```

### Query keys and resource declaration

The server-safe query-key entry point exposes both the factory and the underlying declaration:

```ts
import {
aiChatResources,
createAiChatQueryKeys,
type AiChatQueryKeys,
} from "@btst/stack/plugins/ai-chat/query-keys"
```

The stable keys remain `['conversations', 'list', 'all']` and `['conversations', 'detail', id]`, so existing dehydrated caches and manual invalidations continue to match.

## Model & Tools Configuration

### Using Different Models
Expand Down Expand Up @@ -988,7 +1048,7 @@ overrides={{
```

<Callout type="info">
In public mode, the sidebar is hidden, conversation history is not saved to the database, and only the `/chat` route is available.
In public mode, the sidebar is hidden, conversation history is not saved to the database, only the `/chat` route is available, and client conversation permission gates are bypassed.
</Callout>

### Local Storage Persistence
Expand Down Expand Up @@ -1036,9 +1096,9 @@ This pattern enables:
- **IndexedDB** - Larger storage for long conversations
- **External state management** - Redux, Zustand, etc.

## Localization
## Localization and notifications

Customize UI strings by providing a `localization` override:
All rendered AI Chat copy is routed through the `StackProvider` `i18n` provider with `aiChat.<area>.<name>` keys. The legacy `localization` override remains supported and takes precedence when both are configured:

```tsx
overrides={{
Expand All @@ -1059,6 +1119,20 @@ overrides={{

<AutoTypeTable path="../packages/stack/src/plugins/ai-chat/client/localization/index.ts" name="AiChatLocalization" />

Rename, delete, and file-upload feedback uses the shared `notify` provider. Field validation and streaming errors remain inline:

```tsx
<StackProvider
notify={{
success: (message) => myToast.success(message),
error: (message) => myToast.error(message),
}}
// ...
>
{children}
</StackProvider>
```

## Server-side Data Access

The AI Chat plugin exposes standalone getter functions for server-side use cases, giving you direct access to conversation history without going through HTTP.
Expand Down
Loading