Skip to content

Commit 4b1ad6e

Browse files
authored
Merge pull request #149 from better-stack-ai/feat/ai-chat-phase2-sweep
feat(ai-chat): complete phase 2 client sweep
2 parents 9f18db6 + e4acadc commit 4b1ad6e

31 files changed

Lines changed: 1997 additions & 630 deletions

docs/content/docs/i18n.mdx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,77 @@ An app-provided `localization` override wins byte-for-byte; otherwise the string
113113
| `blog.search.placeholder` | Type to search... |
114114
| `blog.search.searching` | Searching... |
115115

116+
## AI Chat
117+
118+
Legacy `AiChatLocalization` values override these catalog entries when both are configured.
119+
120+
| Key | Default |
121+
| --- | --- |
122+
| `aiChat.a11y.assistantMessage` | AI response |
123+
| `aiChat.a11y.clearChat` | Clear chat |
124+
| `aiChat.a11y.closeChat` | Close chat |
125+
| `aiChat.a11y.closeSidebar` | Close sidebar |
126+
| `aiChat.a11y.conversationActions` | Conversation actions |
127+
| `aiChat.a11y.openChat` | Open chat |
128+
| `aiChat.a11y.openMenu` | Open menu |
129+
| `aiChat.a11y.openSidebar` | Open sidebar |
130+
| `aiChat.a11y.title` | AI Chat |
131+
| `aiChat.a11y.userMessage` | Your message |
132+
| `aiChat.chat.emptyState` | Start a conversation... |
133+
| `aiChat.chat.error` | Something went wrong. Please try again. |
134+
| `aiChat.chat.loading` | Thinking... |
135+
| `aiChat.chat.placeholder` | Type a message... |
136+
| `aiChat.chat.send` | Send |
137+
| `aiChat.conversation.delete` | Delete |
138+
| `aiChat.conversation.deleteCancel` | Cancel |
139+
| `aiChat.conversation.deleteConfirmButton` | Delete |
140+
| `aiChat.conversation.deleteConfirmDescription` | Are you sure you want to delete this conversation? This action cannot be undone. |
141+
| `aiChat.conversation.deleteConfirmTitle` | Delete conversation |
142+
| `aiChat.conversation.rename` | Rename |
143+
| `aiChat.conversation.renameCancel` | Cancel |
144+
| `aiChat.conversation.renameDescription` | Enter a new title for this conversation. |
145+
| `aiChat.conversation.renamePlaceholder` | Enter conversation name |
146+
| `aiChat.conversation.renameSave` | Save |
147+
| `aiChat.conversation.titleRequired` | Title is required |
148+
| `aiChat.errors.genericMessage` | An error occurred while loading the chat. Please try again. |
149+
| `aiChat.errors.genericTitle` | Something went wrong |
150+
| `aiChat.errors.missingConversation` | Conversation is required |
151+
| `aiChat.errors.notFoundDescription` | The conversation you're looking for doesn't exist or has been deleted. |
152+
| `aiChat.errors.notFoundTitle` | Chat not found |
153+
| `aiChat.files.attach` | Attach file |
154+
| `aiChat.files.fallbackName` | File |
155+
| `aiChat.files.remove` | Remove file |
156+
| `aiChat.files.tooLarge` | File must be less than 10MB |
157+
| `aiChat.files.uploadFailure` | Failed to attach file |
158+
| `aiChat.files.uploadSuccess` | File attached |
159+
| `aiChat.images.attachedAlt` | Attached image \{\{count\}\} |
160+
| `aiChat.images.generatedAlt` | Image \{\{count\}\} |
161+
| `aiChat.messages.cancel` | Cancel |
162+
| `aiChat.messages.copied` | Copied! |
163+
| `aiChat.messages.copy` | Copy message |
164+
| `aiChat.messages.edit` | Edit message |
165+
| `aiChat.messages.retry` | Retry |
166+
| `aiChat.messages.save` | Save |
167+
| `aiChat.sidebar.empty` | No conversations yet |
168+
| `aiChat.sidebar.newChat` | New chat |
169+
| `aiChat.time.daysAgo` | \{\{count\}\} days ago |
170+
| `aiChat.time.hoursAgo` | \{\{count\}\} hours ago |
171+
| `aiChat.time.justNow` | Just now |
172+
| `aiChat.time.minutesAgo` | \{\{count\}\} minutes ago |
173+
| `aiChat.time.yesterday` | Yesterday |
174+
| `aiChat.toasts.deleteFailure` | Failed to delete conversation |
175+
| `aiChat.toasts.deleteSuccess` | Conversation deleted |
176+
| `aiChat.toasts.renameFailure` | Failed to rename conversation |
177+
| `aiChat.toasts.renameSuccess` | Conversation renamed |
178+
| `aiChat.tools.executionFailed` | Tool execution failed |
179+
| `aiChat.tools.handlerMissing` | No client-side handler registered for tool "\{\{toolName\}\}". The page context may have changed while the response was streaming. |
180+
| `aiChat.tools.id` | ID: \{\{id\}\} |
181+
| `aiChat.tools.input` | Input |
182+
| `aiChat.tools.output` | Output |
183+
| `aiChat.tools.status.complete` | Complete |
184+
| `aiChat.tools.status.error` | Error |
185+
| `aiChat.tools.status.executing` | Executing... |
186+
| `aiChat.tools.status.pending` | Pending |
187+
| `aiChat.tools.status.running` | Running... |
188+
116189
Other plugins adopt the same convention as their phase-2 sweeps land.

docs/content/docs/plugins/ai-chat.mdx

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,11 @@ The AI Chat plugin supports two distinct modes:
347347
The AI Chat plugin provides the following API endpoints (mounted at your configured `apiBasePath`):
348348

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

356356
### Page Routes
357357

@@ -397,7 +397,30 @@ aiChatClientPlugin({
397397

398398
### Adding Authorization
399399

400-
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.
400+
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:
401+
402+
| Resource | Action | UI covered |
403+
| --- | --- | --- |
404+
| `ai-chat:conversation` | `read` | `/chat`, `/chat/:id`, and conversation history |
405+
| `ai-chat:conversation` | `create` | New chat and the first persisted send |
406+
| `ai-chat:conversation` | `update` | Continue, retry, edit, and rename; receives `{ id }` when available |
407+
| `ai-chat:conversation` | `delete` | Delete; receives `{ id }` |
408+
409+
```tsx
410+
<StackProvider
411+
auth={{
412+
getIdentity: () => session?.user ?? null,
413+
can: ({ resource, action, params }) =>
414+
authorizeConversation(resource, action, params?.id),
415+
loginPath: "/sign-in",
416+
}}
417+
// ...
418+
>
419+
{children}
420+
</StackProvider>
421+
```
422+
423+
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.
401424

402425
## API Reference
403426

@@ -717,6 +740,7 @@ import {
717740
useSuspenseConversation,
718741
useCreateConversation,
719742
useRenameConversation,
743+
useRenameConversationForm,
720744
useDeleteConversation,
721745
} from "@btst/stack/plugins/ai-chat/client/hooks"
722746
```
@@ -737,6 +761,12 @@ import {
737761

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

764+
### UseRenameConversationFormOptions
765+
766+
<AutoTypeTable path="../packages/stack/src/plugins/ai-chat/client/hooks/chat-hooks.tsx" name="UseRenameConversationFormOptions" />
767+
768+
`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.
769+
740770
**Example usage:**
741771

742772
```tsx
@@ -745,6 +775,7 @@ import {
745775
useConversation,
746776
useCreateConversation,
747777
useRenameConversation,
778+
useRenameConversationForm,
748779
useDeleteConversation,
749780
} from "@btst/stack/plugins/ai-chat/client/hooks"
750781

@@ -777,6 +808,35 @@ function ConversationsList() {
777808
}
778809
```
779810

811+
For a custom rename dialog, prefer the form lifecycle over calling the raw mutation directly:
812+
813+
```tsx
814+
const renameForm = useRenameConversationForm({
815+
conversation,
816+
onSuccess: () => setOpen(false),
817+
})
818+
819+
await renameForm.submit({ title })
820+
821+
return renameForm.fieldErrors.title ? (
822+
<p role="alert">{renameForm.fieldErrors.title}</p>
823+
) : null
824+
```
825+
826+
### Query keys and resource declaration
827+
828+
The server-safe query-key entry point exposes both the factory and the underlying declaration:
829+
830+
```ts
831+
import {
832+
aiChatResources,
833+
createAiChatQueryKeys,
834+
type AiChatQueryKeys,
835+
} from "@btst/stack/plugins/ai-chat/query-keys"
836+
```
837+
838+
The stable keys remain `['conversations', 'list', 'all']` and `['conversations', 'detail', id]`, so existing dehydrated caches and manual invalidations continue to match.
839+
780840
## Model & Tools Configuration
781841

782842
### Using Different Models
@@ -988,7 +1048,7 @@ overrides={{
9881048
```
9891049

9901050
<Callout type="info">
991-
In public mode, the sidebar is hidden, conversation history is not saved to the database, and only the `/chat` route is available.
1051+
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.
9921052
</Callout>
9931053

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

1039-
## Localization
1099+
## Localization and notifications
10401100

1041-
Customize UI strings by providing a `localization` override:
1101+
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:
10421102

10431103
```tsx
10441104
overrides={{
@@ -1059,6 +1119,20 @@ overrides={{
10591119

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

1122+
Rename, delete, and file-upload feedback uses the shared `notify` provider. Field validation and streaming errors remain inline:
1123+
1124+
```tsx
1125+
<StackProvider
1126+
notify={{
1127+
success: (message) => myToast.success(message),
1128+
error: (message) => myToast.error(message),
1129+
}}
1130+
// ...
1131+
>
1132+
{children}
1133+
</StackProvider>
1134+
```
1135+
10621136
## Server-side Data Access
10631137

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

0 commit comments

Comments
 (0)