@humaan/payload-plugin-ai-assistant 0.0.0-stage → 0.1.0
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.
- package/README.md +277 -2
- package/dist/admin/AiAssistantControl.d.ts +11 -0
- package/dist/admin/AiAssistantControl.js +31 -0
- package/dist/admin/AiAssistantControl.js.map +1 -0
- package/dist/admin/AiAssistantPanel.d.ts +10 -0
- package/dist/admin/AiAssistantPanel.js +208 -0
- package/dist/admin/AiAssistantPanel.js.map +1 -0
- package/dist/admin/AiAssistantPanel.module.scss +689 -0
- package/dist/admin/AiSourceSwitch.d.ts +13 -0
- package/dist/admin/AiSourceSwitch.js +14 -0
- package/dist/admin/AiSourceSwitch.js.map +1 -0
- package/dist/admin/BudgetField.d.ts +8 -0
- package/dist/admin/BudgetField.js +145 -0
- package/dist/admin/BudgetField.js.map +1 -0
- package/dist/admin/BudgetField.module.scss +47 -0
- package/dist/admin/Conversation.d.ts +12 -0
- package/dist/admin/Conversation.js +469 -0
- package/dist/admin/Conversation.js.map +1 -0
- package/dist/admin/Markdown.d.ts +9 -0
- package/dist/admin/Markdown.js +63 -0
- package/dist/admin/Markdown.js.map +1 -0
- package/dist/admin/PageContextBridge.d.ts +5 -0
- package/dist/admin/PageContextBridge.js +72 -0
- package/dist/admin/PageContextBridge.js.map +1 -0
- package/dist/admin/Pill.d.ts +11 -0
- package/dist/admin/Pill.js +27 -0
- package/dist/admin/Pill.js.map +1 -0
- package/dist/admin/PlanCard.d.ts +14 -0
- package/dist/admin/PlanCard.js +465 -0
- package/dist/admin/PlanCard.js.map +1 -0
- package/dist/admin/SettingsAlerts.d.ts +7 -0
- package/dist/admin/SettingsAlerts.js +37 -0
- package/dist/admin/SettingsAlerts.js.map +1 -0
- package/dist/admin/UsageSummary.d.ts +11 -0
- package/dist/admin/UsageSummary.js +129 -0
- package/dist/admin/UsageSummary.js.map +1 -0
- package/dist/admin/attachmentUpload.d.ts +25 -0
- package/dist/admin/attachmentUpload.js +76 -0
- package/dist/admin/attachmentUpload.js.map +1 -0
- package/dist/admin/openDocument.d.ts +13 -0
- package/dist/admin/openDocument.js +34 -0
- package/dist/admin/openDocument.js.map +1 -0
- package/dist/admin/percent.d.ts +10 -0
- package/dist/admin/percent.js +21 -0
- package/dist/admin/percent.js.map +1 -0
- package/dist/admin/threads.d.ts +21 -0
- package/dist/admin/threads.js +92 -0
- package/dist/admin/threads.js.map +1 -0
- package/dist/admin/toolSteps.d.ts +12 -0
- package/dist/admin/toolSteps.js +57 -0
- package/dist/admin/toolSteps.js.map +1 -0
- package/dist/admin/transport.d.ts +24 -0
- package/dist/admin/transport.js +75 -0
- package/dist/admin/transport.js.map +1 -0
- package/dist/admin/useAttachments.d.ts +32 -0
- package/dist/admin/useAttachments.js +91 -0
- package/dist/admin/useAttachments.js.map +1 -0
- package/dist/admin/useEntityLabel.d.ts +3 -0
- package/dist/admin/useEntityLabel.js +23 -0
- package/dist/admin/useEntityLabel.js.map +1 -0
- package/dist/attachments/collection.d.ts +15 -0
- package/dist/attachments/collection.js +90 -0
- package/dist/attachments/collection.js.map +1 -0
- package/dist/attachments/conversation.d.ts +25 -0
- package/dist/attachments/conversation.js +105 -0
- package/dist/attachments/conversation.js.map +1 -0
- package/dist/attachments/rules.d.ts +9 -0
- package/dist/attachments/rules.js +17 -0
- package/dist/attachments/rules.js.map +1 -0
- package/dist/attachments/store.d.ts +27 -0
- package/dist/attachments/store.js +187 -0
- package/dist/attachments/store.js.map +1 -0
- package/dist/billing.d.ts +60 -0
- package/dist/billing.js +116 -0
- package/dist/billing.js.map +1 -0
- package/dist/chat/history.d.ts +37 -0
- package/dist/chat/history.js +165 -0
- package/dist/chat/history.js.map +1 -0
- package/dist/chat/messages.d.ts +5 -0
- package/dist/chat/messages.js +3 -0
- package/dist/chat/messages.js.map +1 -0
- package/dist/chat/threads.d.ts +33 -0
- package/dist/chat/threads.js +77 -0
- package/dist/chat/threads.js.map +1 -0
- package/dist/chat/tools.d.ts +76 -0
- package/dist/chat/tools.js +596 -0
- package/dist/chat/tools.js.map +1 -0
- package/dist/chat/usage.d.ts +66 -0
- package/dist/chat/usage.js +219 -0
- package/dist/chat/usage.js.map +1 -0
- package/dist/collectionAccess.d.ts +6 -0
- package/dist/collectionAccess.js +32 -0
- package/dist/collectionAccess.js.map +1 -0
- package/dist/constants.d.ts +64 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/endpoints/access.d.ts +4 -0
- package/dist/endpoints/access.js +28 -0
- package/dist/endpoints/access.js.map +1 -0
- package/dist/endpoints/allowance.d.ts +22 -0
- package/dist/endpoints/allowance.js +37 -0
- package/dist/endpoints/allowance.js.map +1 -0
- package/dist/endpoints/applyEndpoint.d.ts +12 -0
- package/dist/endpoints/applyEndpoint.js +170 -0
- package/dist/endpoints/applyEndpoint.js.map +1 -0
- package/dist/endpoints/budget.d.ts +30 -0
- package/dist/endpoints/budget.js +153 -0
- package/dist/endpoints/budget.js.map +1 -0
- package/dist/endpoints/chatEndpoint.d.ts +12 -0
- package/dist/endpoints/chatEndpoint.js +295 -0
- package/dist/endpoints/chatEndpoint.js.map +1 -0
- package/dist/endpoints/form.d.ts +23 -0
- package/dist/endpoints/form.js +95 -0
- package/dist/endpoints/form.js.map +1 -0
- package/dist/endpoints/usageEndpoint.d.ts +13 -0
- package/dist/endpoints/usageEndpoint.js +70 -0
- package/dist/endpoints/usageEndpoint.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lexical/editorConfig.d.ts +4 -0
- package/dist/lexical/editorConfig.js +19 -0
- package/dist/lexical/editorConfig.js.map +1 -0
- package/dist/lexical/lexicalToMarkdown.d.ts +10 -0
- package/dist/lexical/lexicalToMarkdown.js +10 -0
- package/dist/lexical/lexicalToMarkdown.js.map +1 -0
- package/dist/lexical/markdownToLexical.d.ts +7 -0
- package/dist/lexical/markdownToLexical.js +10 -0
- package/dist/lexical/markdownToLexical.js.map +1 -0
- package/dist/plan/allowed.d.ts +7 -0
- package/dist/plan/allowed.js +19 -0
- package/dist/plan/allowed.js.map +1 -0
- package/dist/plan/applyPlan.d.ts +16 -0
- package/dist/plan/applyPlan.js +353 -0
- package/dist/plan/applyPlan.js.map +1 -0
- package/dist/plan/catalogue.d.ts +37 -0
- package/dist/plan/catalogue.js +157 -0
- package/dist/plan/catalogue.js.map +1 -0
- package/dist/plan/diff.d.ts +13 -0
- package/dist/plan/diff.js +87 -0
- package/dist/plan/diff.js.map +1 -0
- package/dist/plan/parsePlan.d.ts +27 -0
- package/dist/plan/parsePlan.js +322 -0
- package/dist/plan/parsePlan.js.map +1 -0
- package/dist/plan/prompt.d.ts +1 -0
- package/dist/plan/prompt.js +45 -0
- package/dist/plan/prompt.js.map +1 -0
- package/dist/plan/stored.d.ts +53 -0
- package/dist/plan/stored.js +3 -0
- package/dist/plan/stored.js.map +1 -0
- package/dist/plan/tokens.d.ts +8 -0
- package/dist/plan/tokens.js +79 -0
- package/dist/plan/tokens.js.map +1 -0
- package/dist/plan/view.d.ts +4 -0
- package/dist/plan/view.js +43 -0
- package/dist/plan/view.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +464 -0
- package/dist/plugin.js.map +1 -0
- package/dist/read/outline.d.ts +7 -0
- package/dist/read/outline.js +70 -0
- package/dist/read/outline.js.map +1 -0
- package/dist/read/readTarget.d.ts +4 -0
- package/dist/read/readTarget.js +13 -0
- package/dist/read/readTarget.js.map +1 -0
- package/dist/read/readable.d.ts +8 -0
- package/dist/read/readable.js +131 -0
- package/dist/read/readable.js.map +1 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +14 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema/fields.d.ts +113 -0
- package/dist/schema/fields.js +1036 -0
- package/dist/schema/fields.js.map +1 -0
- package/dist/schema/listingFields.d.ts +9 -0
- package/dist/schema/listingFields.js +95 -0
- package/dist/schema/listingFields.js.map +1 -0
- package/dist/schema/slugify.d.ts +2 -0
- package/dist/schema/slugify.js +5 -0
- package/dist/schema/slugify.js.map +1 -0
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +54 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.d.ts +198 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/uploads/routeAsset.d.ts +27 -0
- package/dist/uploads/routeAsset.js +127 -0
- package/dist/uploads/routeAsset.js.map +1 -0
- package/package.json +122 -3
package/README.md
CHANGED
|
@@ -1,3 +1,278 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @humaan/payload-plugin-ai-assistant
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Adds Humaan AI to the Payload admin: a side panel where editors chat with an assistant that reads the site's content, answers questions, and proposes changes as plans. Nothing is saved until the editor reviews the plan and chooses Proceed.
|
|
4
|
+
|
|
5
|
+
Models run through [Vercel AI Gateway](https://vercel.com/docs/ai-gateway), authenticated with the deployment's [OIDC token](https://vercel.com/docs/ai-gateway/authentication-and-byok/oidc), so spend is billed to the Vercel project. The site must run on Vercel. Requires Payload 3.90 or later.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @humaan/payload-plugin-ai-assistant
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The admin styles are Sass modules, so the project needs `sass` installed.
|
|
14
|
+
|
|
15
|
+
Add the plugin **last** in `plugins`, so it sees collections other plugins add:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { aiAssistantPlugin } from '@humaan/payload-plugin-ai-assistant'
|
|
19
|
+
|
|
20
|
+
export default buildConfig({
|
|
21
|
+
plugins: [
|
|
22
|
+
// …other plugins first
|
|
23
|
+
aiAssistantPlugin({
|
|
24
|
+
roles: [
|
|
25
|
+
{ label: 'Admin', value: 'admin' },
|
|
26
|
+
{ label: 'Editor', value: 'editor' },
|
|
27
|
+
],
|
|
28
|
+
siteRules: 'Write in Australian English. Use the Oxford comma.',
|
|
29
|
+
timeZone: 'Australia/Perth',
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then:
|
|
36
|
+
|
|
37
|
+
1. Register the attachments collection, if editors should attach files (see [Attachments](#attachments)).
|
|
38
|
+
2. Run `pnpm generate:importmap && pnpm generate:types`.
|
|
39
|
+
3. Set up Vercel (below).
|
|
40
|
+
|
|
41
|
+
## Options
|
|
42
|
+
|
|
43
|
+
| Option | Default | Purpose |
|
|
44
|
+
| ------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `roles` | required | User roles an admin can allow to run the assistant. Users need a `role` field. |
|
|
46
|
+
| `siteRules` | required | Editorial rules editors' prompts cannot override. |
|
|
47
|
+
| `model` | `"anthropic/claude-sonnet-5.5"` | AI Gateway model id. Needs vision and tool use. |
|
|
48
|
+
| `deniedCollections` / `deniedGlobals` | `[]` | Can be read but not changed. Unknown slugs throw at startup. |
|
|
49
|
+
| `byok` | none | The site's own provider credentials. See [Own provider key](#own-provider-key). |
|
|
50
|
+
| `timeZone` | `"UTC"` | When the daily cap resets, and how dates are shown. |
|
|
51
|
+
| `adminGroup` | `"Settings"` | Admin nav group for the settings. |
|
|
52
|
+
| `collectionRules` | none | Guidance for a whole collection or global. Prefer field guidance (see [Guiding the assistant](#guiding-the-assistant)). |
|
|
53
|
+
| `enabled` | `true` | `false` keeps the schema but adds no panel or endpoints. |
|
|
54
|
+
|
|
55
|
+
## Vercel setup
|
|
56
|
+
|
|
57
|
+
| Needs | Where from |
|
|
58
|
+
| -------------------------- | --------------------------------------------------------------------------- |
|
|
59
|
+
| `VERCEL_OIDC_TOKEN` | Issued to each deployment. Locally, pulled with the Vercel CLI. |
|
|
60
|
+
| A project budget | **AI Gateway → Budgets → Projects** in Vercel. |
|
|
61
|
+
| `VERCEL_BUDGET_READ_TOKEN` | A read-only Vercel access token. Used for the budget bar and the daily cap. |
|
|
62
|
+
|
|
63
|
+
### Budgets
|
|
64
|
+
|
|
65
|
+
Each site is held to its own **project budget**. Set:
|
|
66
|
+
|
|
67
|
+
- a **project default budget** as a catch-all for every project without one;
|
|
68
|
+
- a **custom budget** on each client project, monthly, with spend alerts at 50%, 75%, and 100%;
|
|
69
|
+
- optionally, a **team budget** across all clients.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
vercel ai-gateway budgets set project --default --limit 20 --refresh-period monthly
|
|
73
|
+
vercel ai-gateway budgets set project <project-name> --limit 100 --refresh-period monthly
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Budgets reset at the start of each period in UTC. A project's admins can change its budget, so keep client users off the Vercel team.
|
|
77
|
+
|
|
78
|
+
### Budget-reading token
|
|
79
|
+
|
|
80
|
+
1. Invite a shared service account (such as `support+vercel@humaan.com.au`) to the team with the **Pro Viewer** role. It's free and read-only.
|
|
81
|
+
2. Signed in as that account, create a token at [vercel.com/account/tokens](https://vercel.com/account/tokens), scoped to the team with **All Projects**, with an expiry.
|
|
82
|
+
3. Add it as a team **shared environment variable**, `VERCEL_BUDGET_READ_TOKEN`, type **Secret**, for Production and Preview, linked to each project. Redeploy.
|
|
83
|
+
|
|
84
|
+
To rotate: create a new token, update the variable, redeploy, then delete the old token.
|
|
85
|
+
|
|
86
|
+
### Check it
|
|
87
|
+
|
|
88
|
+
**Settings → Humaan AI** shows the share of the budget used and the reset date, or why it can't be read:
|
|
89
|
+
|
|
90
|
+
| Message | Fix |
|
|
91
|
+
| ----------------------------------------- | ----------------------------------------------------------------- |
|
|
92
|
+
| No Vercel OIDC token | Locally, see [Local development](#local-development). |
|
|
93
|
+
| Set `VERCEL_BUDGET_READ_TOKEN` | Link the variable to this project and environment, then redeploy. |
|
|
94
|
+
| Vercel refused `VERCEL_BUDGET_READ_TOKEN` | The token expired, was revoked, or is scoped to another team. |
|
|
95
|
+
| No AI Gateway budget | Add a custom budget to the project. |
|
|
96
|
+
|
|
97
|
+
### Local development
|
|
98
|
+
|
|
99
|
+
Use a current Vercel CLI (`brew install --cask vercel`; version 41 can't log in).
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
vercel login
|
|
103
|
+
vercel link # the existing project; don't create one
|
|
104
|
+
vercel env pull .env.vercel.local # copy VERCEL_OIDC_TOKEN into .env.local, then delete the file
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Add `VERCEL_BUDGET_READ_TOKEN` to `.env.local` by hand: Secret values can't be pulled. Without it the assistant still runs, with no budget bar or daily cap.
|
|
108
|
+
|
|
109
|
+
## Own provider key
|
|
110
|
+
|
|
111
|
+
A client can pay for the model with their own provider account. The site still calls the gateway, but sends the client's credentials with each request ([request-scoped BYOK](https://vercel.com/docs/ai-gateway/authentication-and-byok/byok#request-scoped-byok)), so the provider bills the client with no markup.
|
|
112
|
+
|
|
113
|
+
Pass credentials in the `byok` option, in the gateway's shape, reading environment variables you choose:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
byok: {
|
|
117
|
+
anthropic: { apiKey: process.env.AI_ASSISTANT_ANTHROPIC_API_KEY },
|
|
118
|
+
// bedrock: { accessKeyId: process.env.…, secretAccessKey: process.env.…, region: "ap-southeast-2" },
|
|
119
|
+
// openai: [{ apiKey: process.env.… }, { apiKey: process.env.… }], // tried in order
|
|
120
|
+
},
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- Any [gateway provider](https://vercel.com/docs/ai-gateway/authentication-and-byok/byok#credential-structure-by-provider) works, as long as it serves the `model`.
|
|
124
|
+
- A credential is used only when every value is set, so the option can stay in the config everywhere and turns on where its variables are populated. A malformed option throws at startup.
|
|
125
|
+
- Set the variables on that client's project as **Secret**, then redeploy. Don't add them on Vercel's team **Bring Your Own Key** page, which applies them to every project.
|
|
126
|
+
- Choose it in **Settings → Humaan AI → AI Source**.
|
|
127
|
+
|
|
128
|
+
On the own key:
|
|
129
|
+
|
|
130
|
+
- **No budget or daily cap applies.** The client's provider account is the limit.
|
|
131
|
+
- **Fallbacks bill Humaan.** If the credentials fail, the gateway retries on Vercel's credentials, which can't be turned off. Keep a small project budget on every site. Fallbacks are flagged at the top of the settings screen and in the panel.
|
|
132
|
+
- **Admins see spend in US dollars** at the gateway's list price, this month and today. The provider's invoice is the record.
|
|
133
|
+
- **Credentials stay on the server.** They are only sent to the gateway, and removed from error messages.
|
|
134
|
+
|
|
135
|
+
## Settings
|
|
136
|
+
|
|
137
|
+
The **Humaan AI** global, admin only:
|
|
138
|
+
|
|
139
|
+
| Setting | What it does |
|
|
140
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
141
|
+
| Alerts | Warnings at the top: own key misconfigured, or calls that fell back to Humaan AI's credit this month. |
|
|
142
|
+
| AI Source | **Humaan AI** or **Own provider key**. Only shown when `byok` credentials are complete. |
|
|
143
|
+
| Humaan AI Budget | Read-only. Share of the budget used and the reset date, or, on the own key, spend on it. Follows AI Source before saving. |
|
|
144
|
+
| Who can run it | Roles from the `roles` option. |
|
|
145
|
+
| Daily spend cap per editor | Share of the budget each editor can spend per day. Default `5`, `0` for none. Humaan AI only. |
|
|
146
|
+
|
|
147
|
+
The panel shows editors their share of today's cap, and admins the share of the budget left, flagged low at 80% used. When the budget runs out, editors see the reset date rather than dollar amounts.
|
|
148
|
+
|
|
149
|
+
## Database
|
|
150
|
+
|
|
151
|
+
The plugin adds these to the database. All collections are hidden from the admin nav.
|
|
152
|
+
|
|
153
|
+
| Slug | Type | Holds |
|
|
154
|
+
| -------------------------- | ----------------- | --------------------------------------------------------------------------------------------- |
|
|
155
|
+
| `ai-assistant` | Global | AI Source, allowed roles, daily cap. |
|
|
156
|
+
| `ai-assistant-threads` | Collection | Each editor's conversations: messages and attached file ids. |
|
|
157
|
+
| `ai-assistant-plans` | Collection | Proposed changes awaiting Proceed or Cancel, with status. |
|
|
158
|
+
| `ai-assistant-usage` | Collection | Spend per editor per day: Humaan AI cost, own-key cost, fallbacks, tokens, replies. |
|
|
159
|
+
| `ai-assistant-attachments` | Upload collection | Files editors attach. **The project registers it** (see below). Deleted 30 days after upload. |
|
|
160
|
+
|
|
161
|
+
Each document belongs to one user. Users read and delete their own; admins can also read everyone's usage. Only the plugin's endpoints write them.
|
|
162
|
+
|
|
163
|
+
## Attachments
|
|
164
|
+
|
|
165
|
+
Editors can attach images, PDFs, and other files. Register the collection in the project, so your storage adapter handles it:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import {
|
|
169
|
+
aiAssistantAttachmentSlug,
|
|
170
|
+
aiAssistantAttachmentsCollection,
|
|
171
|
+
} from '@humaan/payload-plugin-ai-assistant'
|
|
172
|
+
|
|
173
|
+
export default buildConfig({
|
|
174
|
+
collections: [...collections, aiAssistantAttachmentsCollection()],
|
|
175
|
+
plugins: [
|
|
176
|
+
s3Storage({ collections: { [aiAssistantAttachmentSlug]: true }, clientUploads: true }),
|
|
177
|
+
// …Humaan AI last
|
|
178
|
+
],
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- Works with any storage adapter. Without the collection, the panel works without attachments.
|
|
183
|
+
- Turn on `clientUploads`: Vercel limits request bodies to 4.5 MB.
|
|
184
|
+
- Keep the files private (for example `{ private: true }` with ImageKit).
|
|
185
|
+
- Limits: 8 files per conversation, images up to 6 MB, other files up to 20 MB. Programs and scripts are refused.
|
|
186
|
+
|
|
187
|
+
## Guiding the assistant
|
|
188
|
+
|
|
189
|
+
The assistant learns each collection from the schema. Add guidance:
|
|
190
|
+
|
|
191
|
+
- `admin.description` on a field, when it helps editors too;
|
|
192
|
+
- `custom: { aiAssistant: "…" }` on a field, block, collection, or global, when only the assistant needs it;
|
|
193
|
+
- `siteRules` for rules that apply everywhere.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
{ name: "pageType", type: "select", custom: { aiAssistant: "New pages are content pages. Choose home only when asked." } }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Security
|
|
200
|
+
|
|
201
|
+
- Endpoints need a logged-in user with an allowed role.
|
|
202
|
+
- Every read and write runs with that user's access (`overrideAccess: false`), so the assistant can only do what the editor could.
|
|
203
|
+
- The model only proposes. Proceed applies the stored plan after checking its owner, its status, and the denied lists.
|
|
204
|
+
- Always denied: auth collections, `payload-*` collections, and the plugin's own. Publish status can't be set.
|
|
205
|
+
- Content the assistant reads could contain instructions aimed at it. The editor's review of each plan, showing current and proposed values, is the safeguard.
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
The `dev` folder holds a Payload app for working on the plugin. To run it:
|
|
210
|
+
|
|
211
|
+
1. Copy `dev/.env.example` to `dev/.env`. Set `PAYLOAD_SECRET` to any random string, and point `DATABASE_URL` at a local MongoDB. The example value, `mongodb://127.0.0.1/payload-plugin-ai-assistant`, works with MongoDB's default port.
|
|
212
|
+
2. To chat with the assistant, add `VERCEL_OIDC_TOKEN` to `dev/.env` (see [Local development](#local-development)). Without it the admin still loads, but the panel can't reach a model.
|
|
213
|
+
3. Run `pnpm dev` and open `http://localhost:3000/admin`. Add `--port 3001` if another app already uses port 3000.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
pnpm generate:importmap
|
|
217
|
+
pnpm generate:types
|
|
218
|
+
pnpm lint
|
|
219
|
+
pnpm test:int
|
|
220
|
+
pnpm test:e2e
|
|
221
|
+
pnpm build
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`pnpm test:int` runs the unit tests in `src/**/*.test.ts` with Node's test runner. `src/plugin.test.ts` checks the plugin against Payload's plugin contract.
|
|
225
|
+
|
|
226
|
+
`pnpm test:e2e` runs the Playwright tests. Install their browser once with `pnpm exec playwright install chromium`. The tests start the dev app with an in-memory database on port 3000. If something is already running on port 3000, they use it instead, so stop any other app there first.
|
|
227
|
+
|
|
228
|
+
## Releasing
|
|
229
|
+
|
|
230
|
+
Features land on `main` by pull request as usual. To cut a release:
|
|
231
|
+
|
|
232
|
+
1. **Actions → Version → Run workflow**, and pick a bump type (`patch`, `minor`, `major`,
|
|
233
|
+
`prerelease`, or `custom` with an explicit version).
|
|
234
|
+
|
|
235
|
+
It bumps `package.json`, regenerates `CHANGELOG.md`, and opens a `chore(release): vX.Y.Z`
|
|
236
|
+
pull request.
|
|
237
|
+
|
|
238
|
+
2. Nothing else to do. `humaan-bot` opens the pull request, its checks start automatically, and
|
|
239
|
+
GitHub merges it with the repository's normal squash strategy once all lifecycle gates pass.
|
|
240
|
+
Failed checks leave the pull request open and unmerged.
|
|
241
|
+
|
|
242
|
+
3. CI runs on `main`, then the `Release` workflow publishes to npm, tags
|
|
243
|
+
`vX.Y.Z`, and creates the GitHub release.
|
|
244
|
+
|
|
245
|
+
The version bump cannot be pushed to `main` by CI — the branch ruleset requires status checks a
|
|
246
|
+
direct push can never satisfy — so it goes through the protected pull request path like any other
|
|
247
|
+
change. See [ADR 0001](docs/adr/0001-release-by-pull-request.md) for the reasoning.
|
|
248
|
+
|
|
249
|
+
### If a publish fails
|
|
250
|
+
|
|
251
|
+
Re-run **Actions → Release → Run workflow** with `confirm` set to `publish`. Nothing on `main`
|
|
252
|
+
changed, so the same unpublished version is picked up, and the tag is only pushed after a
|
|
253
|
+
successful publish — a failure never leaves an orphan tag behind.
|
|
254
|
+
|
|
255
|
+
Do **not** cut a new version to work around a failed publish. `Version` will refuse to bump while
|
|
256
|
+
the version on `main` is missing from npm, because that would skip a version number.
|
|
257
|
+
|
|
258
|
+
Ticking `dry_run` on `Release` reports the version and dist-tag it would use and publishes
|
|
259
|
+
nothing.
|
|
260
|
+
|
|
261
|
+
### Bumping locally
|
|
262
|
+
|
|
263
|
+
`Version` runs the same script you can run yourself, if you would rather prepare the release
|
|
264
|
+
branch by hand:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pnpm release:bump patch
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
It rewrites `package.json`, regenerates `CHANGELOG.md`, and prints the branch and commit steps.
|
|
271
|
+
The pull request title must be exactly `chore(release): vX.Y.Z` — `validate` requires a
|
|
272
|
+
conventional commit, and `Release` keys off that prefix to decide whether a merge is a release.
|
|
273
|
+
|
|
274
|
+
### Prerelease versions
|
|
275
|
+
|
|
276
|
+
A version with a prerelease identifier publishes under a matching dist-tag instead of `latest`:
|
|
277
|
+
`alpha`, `beta`, `rc`, and `next` map to themselves, anything else to `next`. The `prerelease`
|
|
278
|
+
bump type uses the `beta` identifier.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ServerProps } from 'payload';
|
|
2
|
+
import type { AssistantSettings } from '../types.js';
|
|
3
|
+
type AiAssistantControlProps = {
|
|
4
|
+
fallbackSettings?: AssistantSettings;
|
|
5
|
+
} & ServerProps;
|
|
6
|
+
/**
|
|
7
|
+
* Header control, rendered beside the account link.
|
|
8
|
+
* Settings are read on the server and only the panel's own props reach the client.
|
|
9
|
+
*/
|
|
10
|
+
export declare function AiAssistantControl({ payload, user, fallbackSettings, }: AiAssistantControlProps): Promise<import("react").JSX.Element | null>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { aiAssistantAttachmentSlug, aiAssistantGlobalSlug, defaultDailySpendCapPercent } from '../constants.js';
|
|
3
|
+
import { canUseAssistant, parseSettings } from '../settings.js';
|
|
4
|
+
import { AiAssistantPanel } from './AiAssistantPanel.js';
|
|
5
|
+
/**
|
|
6
|
+
* Header control, rendered beside the account link.
|
|
7
|
+
* Settings are read on the server and only the panel's own props reach the client.
|
|
8
|
+
*/ export async function AiAssistantControl({ payload, user, fallbackSettings }) {
|
|
9
|
+
const stored = await payload.findGlobal({
|
|
10
|
+
slug: aiAssistantGlobalSlug,
|
|
11
|
+
depth: 0,
|
|
12
|
+
overrideAccess: true
|
|
13
|
+
});
|
|
14
|
+
const fallback = fallbackSettings ?? {
|
|
15
|
+
billing: 'vercel',
|
|
16
|
+
allowedRoles: [
|
|
17
|
+
'admin'
|
|
18
|
+
],
|
|
19
|
+
dailySpendCapPercent: defaultDailySpendCapPercent
|
|
20
|
+
};
|
|
21
|
+
const settings = parseSettings(stored, fallback);
|
|
22
|
+
if (!canUseAssistant(user, settings)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
// Attaching files needs the project to register the attachments collection with its storage.
|
|
26
|
+
return /*#__PURE__*/ _jsx(AiAssistantPanel, {
|
|
27
|
+
attachmentsEnabled: Boolean(payload.collections[aiAssistantAttachmentSlug])
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=AiAssistantControl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/AiAssistantControl.tsx"],"sourcesContent":["import type { GlobalSlug, ServerProps } from 'payload'\n\nimport type { AssistantSettings } from '../types.js'\n\nimport {\n aiAssistantAttachmentSlug,\n aiAssistantGlobalSlug,\n defaultDailySpendCapPercent,\n} from '../constants.js'\nimport { canUseAssistant, parseSettings } from '../settings.js'\nimport { AiAssistantPanel } from './AiAssistantPanel.js'\n\ntype AiAssistantControlProps = {\n fallbackSettings?: AssistantSettings\n} & ServerProps\n\n/**\n * Header control, rendered beside the account link.\n * Settings are read on the server and only the panel's own props reach the client.\n */\nexport async function AiAssistantControl({\n payload,\n user,\n fallbackSettings,\n}: AiAssistantControlProps) {\n const stored = await payload.findGlobal({\n slug: aiAssistantGlobalSlug as GlobalSlug,\n depth: 0,\n overrideAccess: true,\n })\n const fallback: AssistantSettings = fallbackSettings ?? {\n billing: 'vercel',\n allowedRoles: ['admin'],\n dailySpendCapPercent: defaultDailySpendCapPercent,\n }\n const settings = parseSettings(stored, fallback)\n if (!canUseAssistant(user, settings)) {\n return null\n }\n\n // Attaching files needs the project to register the attachments collection with its storage.\n return (\n <AiAssistantPanel\n attachmentsEnabled={Boolean(payload.collections[aiAssistantAttachmentSlug as never])}\n />\n )\n}\n"],"names":["aiAssistantAttachmentSlug","aiAssistantGlobalSlug","defaultDailySpendCapPercent","canUseAssistant","parseSettings","AiAssistantPanel","AiAssistantControl","payload","user","fallbackSettings","stored","findGlobal","slug","depth","overrideAccess","fallback","billing","allowedRoles","dailySpendCapPercent","settings","attachmentsEnabled","Boolean","collections"],"mappings":";AAIA,SACEA,yBAAyB,EACzBC,qBAAqB,EACrBC,2BAA2B,QACtB,kBAAiB;AACxB,SAASC,eAAe,EAAEC,aAAa,QAAQ,iBAAgB;AAC/D,SAASC,gBAAgB,QAAQ,wBAAuB;AAMxD;;;CAGC,GACD,OAAO,eAAeC,mBAAmB,EACvCC,OAAO,EACPC,IAAI,EACJC,gBAAgB,EACQ;IACxB,MAAMC,SAAS,MAAMH,QAAQI,UAAU,CAAC;QACtCC,MAAMX;QACNY,OAAO;QACPC,gBAAgB;IAClB;IACA,MAAMC,WAA8BN,oBAAoB;QACtDO,SAAS;QACTC,cAAc;YAAC;SAAQ;QACvBC,sBAAsBhB;IACxB;IACA,MAAMiB,WAAWf,cAAcM,QAAQK;IACvC,IAAI,CAACZ,gBAAgBK,MAAMW,WAAW;QACpC,OAAO;IACT;IAEA,6FAA6F;IAC7F,qBACE,KAACd;QACCe,oBAAoBC,QAAQd,QAAQe,WAAW,CAACtB,0BAAmC;;AAGzF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A side panel, not a modal, so the page being edited stays visible and usable beside the conversation.
|
|
3
|
+
* It stays mounted once opened, so closing it keeps the conversation.
|
|
4
|
+
*/
|
|
5
|
+
interface AiAssistantPanelProps {
|
|
6
|
+
/** Whether the project registered the attachments collection. */
|
|
7
|
+
attachmentsEnabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function AiAssistantPanel({ attachmentsEnabled }: AiAssistantPanelProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button, useConfig } from '@payloadcms/ui';
|
|
4
|
+
import { generateId } from 'ai';
|
|
5
|
+
import { useEffect, useId, useState } from 'react';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
7
|
+
import styles from './AiAssistantPanel.module.scss';
|
|
8
|
+
import { Conversation } from './Conversation.js';
|
|
9
|
+
import { listThreads, loadThread, threadOptionLabel } from './threads.js';
|
|
10
|
+
import { UsageSummary } from './UsageSummary.js';
|
|
11
|
+
export function AiAssistantPanel({ attachmentsEnabled }) {
|
|
12
|
+
const { config } = useConfig();
|
|
13
|
+
const apiRoute = config.routes?.api ?? '/api';
|
|
14
|
+
const toggleId = useId();
|
|
15
|
+
const panelId = useId();
|
|
16
|
+
const titleId = useId();
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
// The portal needs document.body, so the panel renders only after the first open, which is client only.
|
|
19
|
+
const [opened, setOpened] = useState(false);
|
|
20
|
+
const show = ()=>{
|
|
21
|
+
setOpened(true);
|
|
22
|
+
setOpen(true);
|
|
23
|
+
requestAnimationFrame(()=>document.getElementById(panelId)?.querySelector('textarea')?.focus());
|
|
24
|
+
};
|
|
25
|
+
const close = ()=>{
|
|
26
|
+
setOpen(false);
|
|
27
|
+
document.getElementById(toggleId)?.focus();
|
|
28
|
+
};
|
|
29
|
+
// Escape closes the panel while focus is inside it, and hands focus back to the button that opened it.
|
|
30
|
+
useEffect(()=>{
|
|
31
|
+
if (!open) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const onKeyDown = (event)=>{
|
|
35
|
+
if (event.key !== 'Escape' || event.defaultPrevented) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!(event.target instanceof Node) || !document.getElementById(panelId)?.contains(event.target)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
setOpen(false);
|
|
43
|
+
document.getElementById(toggleId)?.focus();
|
|
44
|
+
};
|
|
45
|
+
document.addEventListener('keydown', onKeyDown);
|
|
46
|
+
return ()=>document.removeEventListener('keydown', onKeyDown);
|
|
47
|
+
}, [
|
|
48
|
+
open,
|
|
49
|
+
panelId,
|
|
50
|
+
toggleId
|
|
51
|
+
]);
|
|
52
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
53
|
+
children: [
|
|
54
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
55
|
+
buttonStyle: "secondary",
|
|
56
|
+
extraButtonProps: {
|
|
57
|
+
'aria-expanded': open,
|
|
58
|
+
'aria-controls': panelId
|
|
59
|
+
},
|
|
60
|
+
id: toggleId,
|
|
61
|
+
margin: false,
|
|
62
|
+
onClick: ()=>open ? close() : show(),
|
|
63
|
+
size: "small",
|
|
64
|
+
type: "button",
|
|
65
|
+
children: "Humaan AI"
|
|
66
|
+
}),
|
|
67
|
+
opened ? /*#__PURE__*/ createPortal(/*#__PURE__*/ _jsxs("aside", {
|
|
68
|
+
"aria-labelledby": titleId,
|
|
69
|
+
className: styles.panel,
|
|
70
|
+
hidden: !open,
|
|
71
|
+
id: panelId,
|
|
72
|
+
children: [
|
|
73
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
74
|
+
className: styles.panel__header,
|
|
75
|
+
children: [
|
|
76
|
+
/*#__PURE__*/ _jsx("h2", {
|
|
77
|
+
className: styles.panel__title,
|
|
78
|
+
id: titleId,
|
|
79
|
+
children: "Humaan AI"
|
|
80
|
+
}),
|
|
81
|
+
/*#__PURE__*/ _jsx(UsageSummary, {
|
|
82
|
+
apiRoute: apiRoute,
|
|
83
|
+
open: open
|
|
84
|
+
}),
|
|
85
|
+
/*#__PURE__*/ _jsxs("button", {
|
|
86
|
+
className: styles.panel__iconButton,
|
|
87
|
+
onClick: close,
|
|
88
|
+
type: "button",
|
|
89
|
+
children: [
|
|
90
|
+
/*#__PURE__*/ _jsx("span", {
|
|
91
|
+
"aria-hidden": "true",
|
|
92
|
+
children: "×"
|
|
93
|
+
}),
|
|
94
|
+
/*#__PURE__*/ _jsx("span", {
|
|
95
|
+
className: styles.assistant__sr,
|
|
96
|
+
children: "Close Humaan AI"
|
|
97
|
+
})
|
|
98
|
+
]
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
}),
|
|
102
|
+
/*#__PURE__*/ _jsx(AssistantThreads, {
|
|
103
|
+
apiRoute: apiRoute,
|
|
104
|
+
attachmentsEnabled: attachmentsEnabled
|
|
105
|
+
})
|
|
106
|
+
]
|
|
107
|
+
}), document.body) : null
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function AssistantThreads({ apiRoute, attachmentsEnabled }) {
|
|
112
|
+
const pickerId = useId();
|
|
113
|
+
const [session, setSession] = useState(newSession);
|
|
114
|
+
const [threads, setThreads] = useState([]);
|
|
115
|
+
const [threadsVersion, setThreadsVersion] = useState(0);
|
|
116
|
+
const [loadError, setLoadError] = useState('');
|
|
117
|
+
useEffect(()=>{
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
listThreads(apiRoute, controller.signal).then(setThreads).catch(()=>undefined);
|
|
120
|
+
return ()=>controller.abort();
|
|
121
|
+
}, [
|
|
122
|
+
apiRoute,
|
|
123
|
+
threadsVersion
|
|
124
|
+
]);
|
|
125
|
+
const openThread = async (chatId)=>{
|
|
126
|
+
setLoadError('');
|
|
127
|
+
if (!chatId) {
|
|
128
|
+
setSession(newSession());
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
setSession(await loadThread(apiRoute, chatId));
|
|
133
|
+
} catch (caught) {
|
|
134
|
+
setLoadError(caught instanceof Error ? caught.message : 'That conversation could not be opened.');
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const listed = threads.some((thread)=>thread.chatId === session.chatId);
|
|
138
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
139
|
+
className: styles.assistant,
|
|
140
|
+
children: [
|
|
141
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
142
|
+
className: styles.assistant__bar,
|
|
143
|
+
children: [
|
|
144
|
+
/*#__PURE__*/ _jsx("label", {
|
|
145
|
+
className: styles.assistant__sr,
|
|
146
|
+
htmlFor: pickerId,
|
|
147
|
+
children: "Conversation"
|
|
148
|
+
}),
|
|
149
|
+
/*#__PURE__*/ _jsx("div", {
|
|
150
|
+
className: styles.assistant__selectWrap,
|
|
151
|
+
children: /*#__PURE__*/ _jsxs("select", {
|
|
152
|
+
className: styles.assistant__select,
|
|
153
|
+
id: pickerId,
|
|
154
|
+
onChange: (event)=>void openThread(event.target.value),
|
|
155
|
+
value: listed ? session.chatId : '',
|
|
156
|
+
children: [
|
|
157
|
+
/*#__PURE__*/ _jsx("option", {
|
|
158
|
+
value: "",
|
|
159
|
+
children: "New conversation"
|
|
160
|
+
}),
|
|
161
|
+
threads.map((thread)=>/*#__PURE__*/ _jsx("option", {
|
|
162
|
+
value: thread.chatId,
|
|
163
|
+
children: threadOptionLabel(thread)
|
|
164
|
+
}, thread.chatId))
|
|
165
|
+
]
|
|
166
|
+
})
|
|
167
|
+
}),
|
|
168
|
+
/*#__PURE__*/ _jsxs("button", {
|
|
169
|
+
className: styles.assistant__barButton,
|
|
170
|
+
onClick: ()=>void openThread(''),
|
|
171
|
+
type: "button",
|
|
172
|
+
children: [
|
|
173
|
+
/*#__PURE__*/ _jsx("span", {
|
|
174
|
+
"aria-hidden": "true",
|
|
175
|
+
children: "+"
|
|
176
|
+
}),
|
|
177
|
+
" New",
|
|
178
|
+
/*#__PURE__*/ _jsx("span", {
|
|
179
|
+
className: styles.assistant__sr,
|
|
180
|
+
children: " conversation"
|
|
181
|
+
})
|
|
182
|
+
]
|
|
183
|
+
})
|
|
184
|
+
]
|
|
185
|
+
}),
|
|
186
|
+
loadError ? /*#__PURE__*/ _jsx("p", {
|
|
187
|
+
className: styles.assistant__error,
|
|
188
|
+
role: "alert",
|
|
189
|
+
children: loadError
|
|
190
|
+
}) : null,
|
|
191
|
+
/*#__PURE__*/ _jsx(Conversation, {
|
|
192
|
+
apiRoute: apiRoute,
|
|
193
|
+
attachmentsEnabled: attachmentsEnabled,
|
|
194
|
+
onReply: ()=>setThreadsVersion((version)=>version + 1),
|
|
195
|
+
session: session
|
|
196
|
+
}, session.chatId)
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function newSession() {
|
|
201
|
+
return {
|
|
202
|
+
chatId: generateId(),
|
|
203
|
+
messages: [],
|
|
204
|
+
attachments: []
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
//# sourceMappingURL=AiAssistantPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/AiAssistantPanel.tsx"],"sourcesContent":["'use client'\n\nimport { Button, useConfig } from '@payloadcms/ui'\nimport { generateId } from 'ai'\nimport { useEffect, useId, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\nimport type { LoadedThread, ThreadSummary } from './threads.js'\n\nimport styles from './AiAssistantPanel.module.scss'\nimport { Conversation } from './Conversation.js'\nimport { listThreads, loadThread, threadOptionLabel } from './threads.js'\nimport { UsageSummary } from './UsageSummary.js'\n\n/**\n * A side panel, not a modal, so the page being edited stays visible and usable beside the conversation.\n * It stays mounted once opened, so closing it keeps the conversation.\n */\ninterface AiAssistantPanelProps {\n /** Whether the project registered the attachments collection. */\n attachmentsEnabled: boolean\n}\n\nexport function AiAssistantPanel({ attachmentsEnabled }: AiAssistantPanelProps) {\n const { config } = useConfig()\n const apiRoute = config.routes?.api ?? '/api'\n const toggleId = useId()\n const panelId = useId()\n const titleId = useId()\n const [open, setOpen] = useState(false)\n // The portal needs document.body, so the panel renders only after the first open, which is client only.\n const [opened, setOpened] = useState(false)\n\n const show = () => {\n setOpened(true)\n setOpen(true)\n requestAnimationFrame(() =>\n document.getElementById(panelId)?.querySelector('textarea')?.focus(),\n )\n }\n\n const close = () => {\n setOpen(false)\n document.getElementById(toggleId)?.focus()\n }\n\n // Escape closes the panel while focus is inside it, and hands focus back to the button that opened it.\n useEffect(() => {\n if (!open) {\n return\n }\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || event.defaultPrevented) {\n return\n }\n if (\n !(event.target instanceof Node) ||\n !document.getElementById(panelId)?.contains(event.target)\n ) {\n return\n }\n event.preventDefault()\n setOpen(false)\n document.getElementById(toggleId)?.focus()\n }\n document.addEventListener('keydown', onKeyDown)\n return () => document.removeEventListener('keydown', onKeyDown)\n }, [open, panelId, toggleId])\n\n return (\n <>\n <Button\n buttonStyle=\"secondary\"\n extraButtonProps={{ 'aria-expanded': open, 'aria-controls': panelId }}\n id={toggleId}\n margin={false}\n onClick={() => (open ? close() : show())}\n size=\"small\"\n type=\"button\"\n >\n Humaan AI\n </Button>\n {opened\n ? createPortal(\n <aside aria-labelledby={titleId} className={styles.panel} hidden={!open} id={panelId}>\n <div className={styles.panel__header}>\n <h2 className={styles.panel__title} id={titleId}>\n Humaan AI\n </h2>\n <UsageSummary apiRoute={apiRoute} open={open} />\n <button className={styles.panel__iconButton} onClick={close} type=\"button\">\n <span aria-hidden=\"true\">×</span>\n <span className={styles.assistant__sr}>Close Humaan AI</span>\n </button>\n </div>\n <AssistantThreads apiRoute={apiRoute} attachmentsEnabled={attachmentsEnabled} />\n </aside>,\n document.body,\n )\n : null}\n </>\n )\n}\n\nfunction AssistantThreads({\n apiRoute,\n attachmentsEnabled,\n}: {\n apiRoute: string\n attachmentsEnabled: boolean\n}) {\n const pickerId = useId()\n const [session, setSession] = useState<LoadedThread>(newSession)\n const [threads, setThreads] = useState<ThreadSummary[]>([])\n const [threadsVersion, setThreadsVersion] = useState(0)\n const [loadError, setLoadError] = useState('')\n\n useEffect(() => {\n const controller = new AbortController()\n listThreads(apiRoute, controller.signal)\n .then(setThreads)\n .catch(() => undefined)\n return () => controller.abort()\n }, [apiRoute, threadsVersion])\n\n const openThread = async (chatId: string) => {\n setLoadError('')\n if (!chatId) {\n setSession(newSession())\n return\n }\n try {\n setSession(await loadThread(apiRoute, chatId))\n } catch (caught) {\n setLoadError(\n caught instanceof Error ? caught.message : 'That conversation could not be opened.',\n )\n }\n }\n\n const listed = threads.some((thread) => thread.chatId === session.chatId)\n\n return (\n <div className={styles.assistant}>\n <div className={styles.assistant__bar}>\n <label className={styles.assistant__sr} htmlFor={pickerId}>\n Conversation\n </label>\n <div className={styles.assistant__selectWrap}>\n <select\n className={styles.assistant__select}\n id={pickerId}\n onChange={(event) => void openThread(event.target.value)}\n value={listed ? session.chatId : ''}\n >\n <option value=\"\">New conversation</option>\n {threads.map((thread) => (\n <option key={thread.chatId} value={thread.chatId}>\n {threadOptionLabel(thread)}\n </option>\n ))}\n </select>\n </div>\n <button\n className={styles.assistant__barButton}\n onClick={() => void openThread('')}\n type=\"button\"\n >\n <span aria-hidden=\"true\">+</span> New\n <span className={styles.assistant__sr}> conversation</span>\n </button>\n </div>\n {loadError ? (\n <p className={styles.assistant__error} role=\"alert\">\n {loadError}\n </p>\n ) : null}\n <Conversation\n apiRoute={apiRoute}\n attachmentsEnabled={attachmentsEnabled}\n key={session.chatId}\n onReply={() => setThreadsVersion((version) => version + 1)}\n session={session}\n />\n </div>\n )\n}\n\nfunction newSession(): LoadedThread {\n return { chatId: generateId(), messages: [], attachments: [] }\n}\n"],"names":["Button","useConfig","generateId","useEffect","useId","useState","createPortal","styles","Conversation","listThreads","loadThread","threadOptionLabel","UsageSummary","AiAssistantPanel","attachmentsEnabled","config","apiRoute","routes","api","toggleId","panelId","titleId","open","setOpen","opened","setOpened","show","requestAnimationFrame","document","getElementById","querySelector","focus","close","onKeyDown","event","key","defaultPrevented","target","Node","contains","preventDefault","addEventListener","removeEventListener","buttonStyle","extraButtonProps","id","margin","onClick","size","type","aside","aria-labelledby","className","panel","hidden","div","panel__header","h2","panel__title","button","panel__iconButton","span","aria-hidden","assistant__sr","AssistantThreads","body","pickerId","session","setSession","newSession","threads","setThreads","threadsVersion","setThreadsVersion","loadError","setLoadError","controller","AbortController","signal","then","catch","undefined","abort","openThread","chatId","caught","Error","message","listed","some","thread","assistant","assistant__bar","label","htmlFor","assistant__selectWrap","select","assistant__select","onChange","value","option","map","assistant__barButton","p","assistant__error","role","onReply","version","messages","attachments"],"mappings":"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"}
|