@humaan/payload-plugin-ai-assistant 0.0.0-stage → 0.1.1

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.
Files changed (190) hide show
  1. package/README.md +277 -2
  2. package/dist/admin/AiAssistantControl.d.ts +11 -0
  3. package/dist/admin/AiAssistantControl.js +31 -0
  4. package/dist/admin/AiAssistantControl.js.map +1 -0
  5. package/dist/admin/AiAssistantPanel.d.ts +10 -0
  6. package/dist/admin/AiAssistantPanel.js +208 -0
  7. package/dist/admin/AiAssistantPanel.js.map +1 -0
  8. package/dist/admin/AiAssistantPanel.module.scss +689 -0
  9. package/dist/admin/AiSourceSwitch.d.ts +13 -0
  10. package/dist/admin/AiSourceSwitch.js +14 -0
  11. package/dist/admin/AiSourceSwitch.js.map +1 -0
  12. package/dist/admin/BudgetField.d.ts +8 -0
  13. package/dist/admin/BudgetField.js +145 -0
  14. package/dist/admin/BudgetField.js.map +1 -0
  15. package/dist/admin/BudgetField.module.scss +47 -0
  16. package/dist/admin/Conversation.d.ts +12 -0
  17. package/dist/admin/Conversation.js +469 -0
  18. package/dist/admin/Conversation.js.map +1 -0
  19. package/dist/admin/Markdown.d.ts +9 -0
  20. package/dist/admin/Markdown.js +63 -0
  21. package/dist/admin/Markdown.js.map +1 -0
  22. package/dist/admin/PageContextBridge.d.ts +5 -0
  23. package/dist/admin/PageContextBridge.js +72 -0
  24. package/dist/admin/PageContextBridge.js.map +1 -0
  25. package/dist/admin/Pill.d.ts +11 -0
  26. package/dist/admin/Pill.js +27 -0
  27. package/dist/admin/Pill.js.map +1 -0
  28. package/dist/admin/PlanCard.d.ts +14 -0
  29. package/dist/admin/PlanCard.js +465 -0
  30. package/dist/admin/PlanCard.js.map +1 -0
  31. package/dist/admin/SettingsAlerts.d.ts +7 -0
  32. package/dist/admin/SettingsAlerts.js +37 -0
  33. package/dist/admin/SettingsAlerts.js.map +1 -0
  34. package/dist/admin/UsageSummary.d.ts +11 -0
  35. package/dist/admin/UsageSummary.js +129 -0
  36. package/dist/admin/UsageSummary.js.map +1 -0
  37. package/dist/admin/attachmentUpload.d.ts +25 -0
  38. package/dist/admin/attachmentUpload.js +76 -0
  39. package/dist/admin/attachmentUpload.js.map +1 -0
  40. package/dist/admin/openDocument.d.ts +13 -0
  41. package/dist/admin/openDocument.js +34 -0
  42. package/dist/admin/openDocument.js.map +1 -0
  43. package/dist/admin/percent.d.ts +10 -0
  44. package/dist/admin/percent.js +21 -0
  45. package/dist/admin/percent.js.map +1 -0
  46. package/dist/admin/threads.d.ts +21 -0
  47. package/dist/admin/threads.js +92 -0
  48. package/dist/admin/threads.js.map +1 -0
  49. package/dist/admin/toolSteps.d.ts +12 -0
  50. package/dist/admin/toolSteps.js +57 -0
  51. package/dist/admin/toolSteps.js.map +1 -0
  52. package/dist/admin/transport.d.ts +24 -0
  53. package/dist/admin/transport.js +75 -0
  54. package/dist/admin/transport.js.map +1 -0
  55. package/dist/admin/useAttachments.d.ts +32 -0
  56. package/dist/admin/useAttachments.js +91 -0
  57. package/dist/admin/useAttachments.js.map +1 -0
  58. package/dist/admin/useEntityLabel.d.ts +3 -0
  59. package/dist/admin/useEntityLabel.js +23 -0
  60. package/dist/admin/useEntityLabel.js.map +1 -0
  61. package/dist/attachments/collection.d.ts +15 -0
  62. package/dist/attachments/collection.js +90 -0
  63. package/dist/attachments/collection.js.map +1 -0
  64. package/dist/attachments/conversation.d.ts +25 -0
  65. package/dist/attachments/conversation.js +105 -0
  66. package/dist/attachments/conversation.js.map +1 -0
  67. package/dist/attachments/rules.d.ts +9 -0
  68. package/dist/attachments/rules.js +17 -0
  69. package/dist/attachments/rules.js.map +1 -0
  70. package/dist/attachments/store.d.ts +27 -0
  71. package/dist/attachments/store.js +187 -0
  72. package/dist/attachments/store.js.map +1 -0
  73. package/dist/billing.d.ts +60 -0
  74. package/dist/billing.js +116 -0
  75. package/dist/billing.js.map +1 -0
  76. package/dist/chat/history.d.ts +37 -0
  77. package/dist/chat/history.js +165 -0
  78. package/dist/chat/history.js.map +1 -0
  79. package/dist/chat/messages.d.ts +5 -0
  80. package/dist/chat/messages.js +3 -0
  81. package/dist/chat/messages.js.map +1 -0
  82. package/dist/chat/threads.d.ts +33 -0
  83. package/dist/chat/threads.js +77 -0
  84. package/dist/chat/threads.js.map +1 -0
  85. package/dist/chat/tools.d.ts +76 -0
  86. package/dist/chat/tools.js +596 -0
  87. package/dist/chat/tools.js.map +1 -0
  88. package/dist/chat/usage.d.ts +66 -0
  89. package/dist/chat/usage.js +219 -0
  90. package/dist/chat/usage.js.map +1 -0
  91. package/dist/collectionAccess.d.ts +6 -0
  92. package/dist/collectionAccess.js +32 -0
  93. package/dist/collectionAccess.js.map +1 -0
  94. package/dist/constants.d.ts +64 -0
  95. package/dist/constants.js +51 -0
  96. package/dist/constants.js.map +1 -0
  97. package/dist/endpoints/access.d.ts +4 -0
  98. package/dist/endpoints/access.js +28 -0
  99. package/dist/endpoints/access.js.map +1 -0
  100. package/dist/endpoints/allowance.d.ts +22 -0
  101. package/dist/endpoints/allowance.js +37 -0
  102. package/dist/endpoints/allowance.js.map +1 -0
  103. package/dist/endpoints/applyEndpoint.d.ts +12 -0
  104. package/dist/endpoints/applyEndpoint.js +170 -0
  105. package/dist/endpoints/applyEndpoint.js.map +1 -0
  106. package/dist/endpoints/budget.d.ts +30 -0
  107. package/dist/endpoints/budget.js +153 -0
  108. package/dist/endpoints/budget.js.map +1 -0
  109. package/dist/endpoints/chatEndpoint.d.ts +12 -0
  110. package/dist/endpoints/chatEndpoint.js +295 -0
  111. package/dist/endpoints/chatEndpoint.js.map +1 -0
  112. package/dist/endpoints/form.d.ts +23 -0
  113. package/dist/endpoints/form.js +95 -0
  114. package/dist/endpoints/form.js.map +1 -0
  115. package/dist/endpoints/usageEndpoint.d.ts +13 -0
  116. package/dist/endpoints/usageEndpoint.js +70 -0
  117. package/dist/endpoints/usageEndpoint.js.map +1 -0
  118. package/dist/index.d.ts +4 -0
  119. package/dist/index.js +5 -0
  120. package/dist/index.js.map +1 -0
  121. package/dist/lexical/editorConfig.d.ts +4 -0
  122. package/dist/lexical/editorConfig.js +19 -0
  123. package/dist/lexical/editorConfig.js.map +1 -0
  124. package/dist/lexical/lexicalToMarkdown.d.ts +10 -0
  125. package/dist/lexical/lexicalToMarkdown.js +10 -0
  126. package/dist/lexical/lexicalToMarkdown.js.map +1 -0
  127. package/dist/lexical/markdownToLexical.d.ts +7 -0
  128. package/dist/lexical/markdownToLexical.js +10 -0
  129. package/dist/lexical/markdownToLexical.js.map +1 -0
  130. package/dist/plan/allowed.d.ts +7 -0
  131. package/dist/plan/allowed.js +19 -0
  132. package/dist/plan/allowed.js.map +1 -0
  133. package/dist/plan/applyPlan.d.ts +16 -0
  134. package/dist/plan/applyPlan.js +353 -0
  135. package/dist/plan/applyPlan.js.map +1 -0
  136. package/dist/plan/catalogue.d.ts +37 -0
  137. package/dist/plan/catalogue.js +157 -0
  138. package/dist/plan/catalogue.js.map +1 -0
  139. package/dist/plan/diff.d.ts +13 -0
  140. package/dist/plan/diff.js +87 -0
  141. package/dist/plan/diff.js.map +1 -0
  142. package/dist/plan/parsePlan.d.ts +27 -0
  143. package/dist/plan/parsePlan.js +322 -0
  144. package/dist/plan/parsePlan.js.map +1 -0
  145. package/dist/plan/prompt.d.ts +1 -0
  146. package/dist/plan/prompt.js +45 -0
  147. package/dist/plan/prompt.js.map +1 -0
  148. package/dist/plan/stored.d.ts +53 -0
  149. package/dist/plan/stored.js +3 -0
  150. package/dist/plan/stored.js.map +1 -0
  151. package/dist/plan/tokens.d.ts +8 -0
  152. package/dist/plan/tokens.js +79 -0
  153. package/dist/plan/tokens.js.map +1 -0
  154. package/dist/plan/view.d.ts +4 -0
  155. package/dist/plan/view.js +43 -0
  156. package/dist/plan/view.js.map +1 -0
  157. package/dist/plugin.d.ts +3 -0
  158. package/dist/plugin.js +464 -0
  159. package/dist/plugin.js.map +1 -0
  160. package/dist/read/outline.d.ts +7 -0
  161. package/dist/read/outline.js +70 -0
  162. package/dist/read/outline.js.map +1 -0
  163. package/dist/read/readTarget.d.ts +4 -0
  164. package/dist/read/readTarget.js +13 -0
  165. package/dist/read/readTarget.js.map +1 -0
  166. package/dist/read/readable.d.ts +8 -0
  167. package/dist/read/readable.js +131 -0
  168. package/dist/read/readable.js.map +1 -0
  169. package/dist/runtime.d.ts +15 -0
  170. package/dist/runtime.js +14 -0
  171. package/dist/runtime.js.map +1 -0
  172. package/dist/schema/fields.d.ts +113 -0
  173. package/dist/schema/fields.js +1036 -0
  174. package/dist/schema/fields.js.map +1 -0
  175. package/dist/schema/listingFields.d.ts +9 -0
  176. package/dist/schema/listingFields.js +95 -0
  177. package/dist/schema/listingFields.js.map +1 -0
  178. package/dist/schema/slugify.d.ts +2 -0
  179. package/dist/schema/slugify.js +5 -0
  180. package/dist/schema/slugify.js.map +1 -0
  181. package/dist/settings.d.ts +10 -0
  182. package/dist/settings.js +54 -0
  183. package/dist/settings.js.map +1 -0
  184. package/dist/types.d.ts +198 -0
  185. package/dist/types.js +6 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/uploads/routeAsset.d.ts +27 -0
  188. package/dist/uploads/routeAsset.js +127 -0
  189. package/dist/uploads/routeAsset.js.map +1 -0
  190. package/package.json +122 -3
@@ -0,0 +1,295 @@
1
+ import { createGateway } from '@ai-sdk/gateway';
2
+ import { convertToModelMessages, createIdGenerator, stepCountIs, streamText } from 'ai';
3
+ import { addAttachments, conversationFiles, modelFileParts } from '../attachments/conversation.js';
4
+ import { findAttachments } from '../attachments/store.js';
5
+ import { activeBilling, redactKey } from '../billing.js';
6
+ import { pruneToolResults, threadTitle, withTurnContext } from '../chat/history.js';
7
+ import { findThread, saveThread } from '../chat/threads.js';
8
+ import { assistantTools, readDocument } from '../chat/tools.js';
9
+ import { recordSpend, spendCheck, spentToday } from '../chat/usage.js';
10
+ import { chatEndpointPath, defaultModelId, keepToolResultsForTurns, maxContextCharacters, maxModelSteps, maxSourceCharacters } from '../constants.js';
11
+ import { planTargets } from '../plan/catalogue.js';
12
+ import { systemPrompt } from '../plan/prompt.js';
13
+ import { outline } from '../read/outline.js';
14
+ import { readableTarget } from '../read/readTarget.js';
15
+ import { allowAssistant } from './access.js';
16
+ import { accountAllowance, budgetExceededMessage, dailyCapUsd } from './allowance.js';
17
+ import { errorMessage, readChatForm } from './form.js';
18
+ const generateMessageId = createIdGenerator({
19
+ prefix: 'msg',
20
+ size: 16
21
+ });
22
+ /**
23
+ * One turn of a conversation. The drawer sends the new message; the history comes from the stored thread,
24
+ * so a client cannot rewrite what the assistant said earlier. The reply streams back as UI message parts.
25
+ */ export function chatEndpoint(options, fallback, siteKey) {
26
+ return {
27
+ path: chatEndpointPath,
28
+ method: 'post',
29
+ handler: async (req)=>{
30
+ const settings = await allowAssistant(req, fallback);
31
+ if (settings instanceof Response) {
32
+ return settings;
33
+ }
34
+ const timeZone = options.timeZone ?? 'UTC';
35
+ const model = options.model ?? defaultModelId;
36
+ const billing = activeBilling(settings.billing, siteKey);
37
+ // Still read on the own key: a failed key falls back to Vercel's credit, which the budget caps.
38
+ const allowance = await accountAllowance(req);
39
+ const spend = spendCheck({
40
+ spentUsd: await spentToday(req, timeZone),
41
+ // The cap is a share of the Vercel budget, so it only applies when Vercel pays.
42
+ capUsd: billing.billing === 'vercel' ? dailyCapUsd(settings.dailySpendCapPercent, allowance) : undefined
43
+ });
44
+ if (spend.blocked) {
45
+ return Response.json({
46
+ message: "You have used today's assistant allowance. It resets at midnight. An admin can raise the cap in the Humaan AI settings."
47
+ }, {
48
+ status: 429
49
+ });
50
+ }
51
+ const form = await readChatForm(req);
52
+ if (form instanceof Response) {
53
+ return form;
54
+ }
55
+ const incoming = parseIncomingMessage(form.message);
56
+ if (!incoming) {
57
+ return Response.json({
58
+ message: 'The message could not be read.'
59
+ }, {
60
+ status: 400
61
+ });
62
+ }
63
+ const thread = await findThread(req, form.chatId);
64
+ const knownFiles = thread?.files ?? [];
65
+ const match = addAttachments(knownFiles, form.attachmentIds, await findAttachments(req, form.attachmentIds));
66
+ if (!match.ok) {
67
+ return Response.json({
68
+ message: match.message
69
+ }, {
70
+ status: 400
71
+ });
72
+ }
73
+ if (!incoming.text && match.added.length === 0) {
74
+ return Response.json({
75
+ message: 'Add a message or a file.'
76
+ }, {
77
+ status: 400
78
+ });
79
+ }
80
+ const context = parsePageContext(req, form.context);
81
+ const attachments = match.added.map((file, offset)=>({
82
+ index: knownFiles.length + offset,
83
+ name: file.name,
84
+ type: file.type
85
+ }));
86
+ const userMessage = {
87
+ id: incoming.id,
88
+ role: 'user',
89
+ parts: incoming.text ? [
90
+ {
91
+ type: 'text',
92
+ text: incoming.text
93
+ }
94
+ ] : [],
95
+ metadata: {
96
+ ...attachments.length > 0 ? {
97
+ attachments
98
+ } : {},
99
+ ...context ? {
100
+ context
101
+ } : {}
102
+ }
103
+ };
104
+ const history = [
105
+ ...thread?.messages ?? [],
106
+ userMessage
107
+ ];
108
+ const { files } = match;
109
+ const title = thread?.title || threadTitle(incoming.text || attachments.map((file)=>file.name).join(', '));
110
+ // Saved before the model runs, so the editor's message survives a failed reply.
111
+ const threadId = await saveThread(req, {
112
+ id: thread?.id,
113
+ chatId: form.chatId,
114
+ title,
115
+ messages: history,
116
+ files
117
+ });
118
+ const available = conversationFiles(req.payload.config, files);
119
+ const open = context ? await readOpenDocument(req, context, form.contextData) : undefined;
120
+ let planned = false;
121
+ const tools = assistantTools({
122
+ req,
123
+ chatId: form.chatId,
124
+ collectionRules: options.collectionRules,
125
+ files: available,
126
+ onPlan: ()=>{
127
+ planned = true;
128
+ },
129
+ openDocument: open
130
+ });
131
+ const modelMessages = await convertToModelMessages(withTurnContext(pruneToolResults(history, keepToolResultsForTurns), {
132
+ files: available,
133
+ fileParts: await modelFileParts(req, available),
134
+ openDocument: open ? outlineText(open.document) : undefined
135
+ }), {
136
+ tools,
137
+ ignoreIncompleteToolCalls: true
138
+ });
139
+ // The gateway authenticates with the deployment's OIDC token. On the own key, the credentials are sent with
140
+ // each request instead of being set in the Vercel dashboard, which would apply to every project on the team.
141
+ const gateway = createGateway();
142
+ const ownKey = billing.billing === 'own-key' ? billing.key : undefined;
143
+ // Counted when the run ends, including a run the editor stopped, so the cap sees every reply.
144
+ const record = async (steps)=>{
145
+ try {
146
+ await recordSpend(req, timeZone, gateway, steps, billing.billing);
147
+ } catch (error) {
148
+ req.payload.logger.error({
149
+ err: error,
150
+ chatId: form.chatId
151
+ }, 'Humaan AI could not record spend');
152
+ }
153
+ };
154
+ const result = streamText({
155
+ model: gateway(model),
156
+ system: systemPrompt(options.siteRules),
157
+ messages: modelMessages,
158
+ tools,
159
+ stopWhen: [
160
+ stepCountIs(maxModelSteps),
161
+ ()=>planned
162
+ ],
163
+ maxOutputTokens: 16_000,
164
+ maxRetries: 1,
165
+ abortSignal: req.signal,
166
+ onFinish: ({ steps })=>record(steps),
167
+ onAbort: ({ steps })=>record(steps),
168
+ providerOptions: {
169
+ // Each step resends the conversation so far. Caching makes the repeated part cheap.
170
+ gateway: {
171
+ caching: 'auto',
172
+ // Providers with credentials are tried first, so the model runs where the site's own key can pay.
173
+ ...ownKey ? {
174
+ byok: ownKey.credentials,
175
+ order: ownKey.providers
176
+ } : {}
177
+ },
178
+ anthropic: {
179
+ thinking: {
180
+ type: 'adaptive'
181
+ },
182
+ effort: 'low'
183
+ }
184
+ }
185
+ });
186
+ // Keeps the run going to its end if the browser goes away, so onFinish still saves the reply.
187
+ void result.consumeStream();
188
+ return result.toUIMessageStreamResponse({
189
+ originalMessages: history,
190
+ generateMessageId,
191
+ sendReasoning: false,
192
+ messageMetadata: ({ part })=>part.type === 'finish' ? {
193
+ usage: {
194
+ inputTokens: part.totalUsage.inputTokens,
195
+ outputTokens: part.totalUsage.outputTokens,
196
+ totalTokens: part.totalUsage.totalTokens,
197
+ cachedInputTokens: part.totalUsage.inputTokenDetails.cacheReadTokens
198
+ }
199
+ } : undefined,
200
+ onFinish: async ({ messages, responseMessage })=>{
201
+ if (responseMessage.parts.length === 0) {
202
+ return;
203
+ }
204
+ try {
205
+ await saveThread(req, {
206
+ id: threadId,
207
+ chatId: form.chatId,
208
+ title,
209
+ messages,
210
+ files
211
+ });
212
+ } catch (error) {
213
+ req.payload.logger.error({
214
+ err: error,
215
+ chatId: form.chatId
216
+ }, 'Humaan AI could not save the conversation');
217
+ }
218
+ },
219
+ onError: (error)=>{
220
+ // A provider's error can echo the credentials it was given.
221
+ const message = redactKey(errorMessage(error), ownKey);
222
+ req.payload.logger.error({
223
+ message,
224
+ chatId: form.chatId
225
+ }, 'Humaan AI reply failed');
226
+ // The gateway's own budget message names dollar amounts, which editors do not see.
227
+ return budgetExceededMessage(message, allowance, timeZone) ?? message;
228
+ }
229
+ });
230
+ }
231
+ };
232
+ }
233
+ function parseIncomingMessage(value) {
234
+ if (!isRecord(value) || typeof value.id !== 'string' || !/^[\w-]{1,64}$/.test(value.id)) {
235
+ return undefined;
236
+ }
237
+ if (value.role !== 'user' || !Array.isArray(value.parts)) {
238
+ return undefined;
239
+ }
240
+ const text = value.parts.flatMap((part)=>isRecord(part) && part.type === 'text' && typeof part.text === 'string' ? [
241
+ part.text
242
+ ] : []).join('\n').trim();
243
+ if (text.length > maxSourceCharacters) {
244
+ return undefined;
245
+ }
246
+ return {
247
+ id: value.id,
248
+ text
249
+ };
250
+ }
251
+ /** Only targets the assistant may read. A collection document with no id is one being created. The label is shown to the model, so it is cut short. */ function parsePageContext(req, value) {
252
+ if (!isRecord(value)) {
253
+ return undefined;
254
+ }
255
+ const kind = value.kind === 'global' ? 'global' : value.kind === 'collection' ? 'collection' : undefined;
256
+ const slug = typeof value.slug === 'string' ? value.slug : '';
257
+ if (!kind || !planTargets(req.payload.config).some((target)=>target.kind === kind && target.slug === slug)) {
258
+ return undefined;
259
+ }
260
+ const id = typeof value.id === 'string' && /^[\w-]{1,64}$/.test(value.id) ? value.id : undefined;
261
+ const label = typeof value.label === 'string' && value.label.trim() ? value.label.trim().slice(0, 200) : slug;
262
+ return {
263
+ kind,
264
+ slug,
265
+ ...id ? {
266
+ id
267
+ } : {},
268
+ label
269
+ };
270
+ }
271
+ /**
272
+ * The open document as the model reads it. Form values from the drawer include unsaved changes;
273
+ * without them, the saved document is read with the editor's access.
274
+ */ async function readOpenDocument(req, context, formData) {
275
+ if (isRecord(formData)) {
276
+ return {
277
+ context,
278
+ document: await readableTarget(req.payload.config, context.kind, context.slug, formData)
279
+ };
280
+ }
281
+ const read = await readDocument(req, context.kind, context.slug, context.id);
282
+ return 'error' in read ? undefined : {
283
+ context,
284
+ document: read.document
285
+ };
286
+ }
287
+ /** Sent with every message, so it is an outline. readDocument with a path returns any part in full. */ function outlineText(document) {
288
+ const json = JSON.stringify(outline(document));
289
+ return json.length > maxContextCharacters ? `${json.slice(0, maxContextCharacters)} [cut: ${json.length - maxContextCharacters} more characters]` : json;
290
+ }
291
+ function isRecord(value) {
292
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
293
+ }
294
+
295
+ //# sourceMappingURL=chatEndpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/chatEndpoint.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { createGateway } from '@ai-sdk/gateway'\nimport { convertToModelMessages, createIdGenerator, stepCountIs, streamText } from 'ai'\n\nimport type { OwnKey } from '../billing.js'\nimport type { AssistantUIMessage } from '../chat/messages.js'\nimport type { OpenDocument } from '../chat/tools.js'\nimport type { StepSpend } from '../chat/usage.js'\nimport type { IncomingFile } from '../plan/parsePlan.js'\nimport type {\n AiAssistantPluginOptions,\n AssistantSettings,\n AttachmentView,\n PageContext,\n} from '../types.js'\n\nimport { addAttachments, conversationFiles, modelFileParts } from '../attachments/conversation.js'\nimport { findAttachments } from '../attachments/store.js'\nimport { activeBilling, redactKey } from '../billing.js'\nimport { pruneToolResults, threadTitle, withTurnContext } from '../chat/history.js'\nimport { findThread, saveThread } from '../chat/threads.js'\nimport { assistantTools, readDocument } from '../chat/tools.js'\nimport { recordSpend, spendCheck, spentToday } from '../chat/usage.js'\nimport {\n chatEndpointPath,\n defaultModelId,\n keepToolResultsForTurns,\n maxContextCharacters,\n maxModelSteps,\n maxSourceCharacters,\n} from '../constants.js'\nimport { planTargets } from '../plan/catalogue.js'\nimport { systemPrompt } from '../plan/prompt.js'\nimport { outline } from '../read/outline.js'\nimport { readableTarget } from '../read/readTarget.js'\nimport { allowAssistant } from './access.js'\nimport { accountAllowance, budgetExceededMessage, dailyCapUsd } from './allowance.js'\nimport { errorMessage, readChatForm } from './form.js'\n\nconst generateMessageId = createIdGenerator({ prefix: 'msg', size: 16 })\n\n/**\n * One turn of a conversation. The drawer sends the new message; the history comes from the stored thread,\n * so a client cannot rewrite what the assistant said earlier. The reply streams back as UI message parts.\n */\nexport function chatEndpoint(\n options: AiAssistantPluginOptions,\n fallback: AssistantSettings,\n siteKey: OwnKey | undefined,\n) {\n return {\n path: chatEndpointPath,\n method: 'post' as const,\n handler: async (req: PayloadRequest) => {\n const settings = await allowAssistant(req, fallback)\n if (settings instanceof Response) {\n return settings\n }\n\n const timeZone = options.timeZone ?? 'UTC'\n const model = options.model ?? defaultModelId\n const billing = activeBilling(settings.billing, siteKey)\n // Still read on the own key: a failed key falls back to Vercel's credit, which the budget caps.\n const allowance = await accountAllowance(req)\n const spend = spendCheck({\n spentUsd: await spentToday(req, timeZone),\n // The cap is a share of the Vercel budget, so it only applies when Vercel pays.\n capUsd:\n billing.billing === 'vercel'\n ? dailyCapUsd(settings.dailySpendCapPercent, allowance)\n : undefined,\n })\n if (spend.blocked) {\n return Response.json(\n {\n message:\n \"You have used today's assistant allowance. It resets at midnight. An admin can raise the cap in the Humaan AI settings.\",\n },\n { status: 429 },\n )\n }\n\n const form = await readChatForm(req)\n if (form instanceof Response) {\n return form\n }\n const incoming = parseIncomingMessage(form.message)\n if (!incoming) {\n return Response.json({ message: 'The message could not be read.' }, { status: 400 })\n }\n\n const thread = await findThread(req, form.chatId)\n const knownFiles = thread?.files ?? []\n const match = addAttachments(\n knownFiles,\n form.attachmentIds,\n await findAttachments(req, form.attachmentIds),\n )\n if (!match.ok) {\n return Response.json({ message: match.message }, { status: 400 })\n }\n if (!incoming.text && match.added.length === 0) {\n return Response.json({ message: 'Add a message or a file.' }, { status: 400 })\n }\n\n const context = parsePageContext(req, form.context)\n const attachments: AttachmentView[] = match.added.map((file, offset) => ({\n index: knownFiles.length + offset,\n name: file.name,\n type: file.type,\n }))\n const userMessage: AssistantUIMessage = {\n id: incoming.id,\n role: 'user',\n parts: incoming.text ? [{ type: 'text', text: incoming.text }] : [],\n metadata: {\n ...(attachments.length > 0 ? { attachments } : {}),\n ...(context ? { context } : {}),\n },\n }\n const history = [...(thread?.messages ?? []), userMessage]\n const { files } = match\n const title =\n thread?.title ||\n threadTitle(incoming.text || attachments.map((file) => file.name).join(', '))\n\n // Saved before the model runs, so the editor's message survives a failed reply.\n const threadId = await saveThread(req, {\n id: thread?.id,\n chatId: form.chatId,\n title,\n messages: history,\n files,\n })\n\n const available: IncomingFile[] = conversationFiles(req.payload.config, files)\n const open = context ? await readOpenDocument(req, context, form.contextData) : undefined\n let planned = false\n const tools = assistantTools({\n req,\n chatId: form.chatId,\n collectionRules: options.collectionRules,\n files: available,\n onPlan: () => {\n planned = true\n },\n openDocument: open,\n })\n\n const modelMessages = await convertToModelMessages(\n withTurnContext(pruneToolResults(history, keepToolResultsForTurns), {\n files: available,\n fileParts: await modelFileParts(req, available),\n openDocument: open ? outlineText(open.document) : undefined,\n }),\n { tools, ignoreIncompleteToolCalls: true },\n )\n\n // The gateway authenticates with the deployment's OIDC token. On the own key, the credentials are sent with\n // each request instead of being set in the Vercel dashboard, which would apply to every project on the team.\n const gateway = createGateway()\n const ownKey = billing.billing === 'own-key' ? billing.key : undefined\n // Counted when the run ends, including a run the editor stopped, so the cap sees every reply.\n const record = async (steps: readonly StepSpend[]) => {\n try {\n await recordSpend(req, timeZone, gateway, steps, billing.billing)\n } catch (error) {\n req.payload.logger.error(\n { err: error, chatId: form.chatId },\n 'Humaan AI could not record spend',\n )\n }\n }\n const result = streamText({\n model: gateway(model),\n system: systemPrompt(options.siteRules),\n messages: modelMessages,\n tools,\n stopWhen: [stepCountIs(maxModelSteps), () => planned],\n maxOutputTokens: 16_000,\n maxRetries: 1,\n abortSignal: req.signal,\n onFinish: ({ steps }) => record(steps as readonly StepSpend[]),\n onAbort: ({ steps }) => record(steps as readonly StepSpend[]),\n providerOptions: {\n // Each step resends the conversation so far. Caching makes the repeated part cheap.\n gateway: {\n caching: 'auto',\n // Providers with credentials are tried first, so the model runs where the site's own key can pay.\n ...(ownKey ? { byok: ownKey.credentials, order: ownKey.providers } : {}),\n },\n anthropic: {\n thinking: { type: 'adaptive' },\n effort: 'low',\n },\n },\n })\n // Keeps the run going to its end if the browser goes away, so onFinish still saves the reply.\n void result.consumeStream()\n\n return result.toUIMessageStreamResponse<AssistantUIMessage>({\n originalMessages: history,\n generateMessageId,\n sendReasoning: false,\n messageMetadata: ({ part }) =>\n part.type === 'finish'\n ? {\n usage: {\n inputTokens: part.totalUsage.inputTokens,\n outputTokens: part.totalUsage.outputTokens,\n totalTokens: part.totalUsage.totalTokens,\n cachedInputTokens: part.totalUsage.inputTokenDetails.cacheReadTokens,\n },\n }\n : undefined,\n onFinish: async ({ messages, responseMessage }) => {\n if (responseMessage.parts.length === 0) {\n return\n }\n try {\n await saveThread(req, { id: threadId, chatId: form.chatId, title, messages, files })\n } catch (error) {\n req.payload.logger.error(\n { err: error, chatId: form.chatId },\n 'Humaan AI could not save the conversation',\n )\n }\n },\n onError: (error) => {\n // A provider's error can echo the credentials it was given.\n const message = redactKey(errorMessage(error), ownKey)\n req.payload.logger.error({ message, chatId: form.chatId }, 'Humaan AI reply failed')\n // The gateway's own budget message names dollar amounts, which editors do not see.\n return budgetExceededMessage(message, allowance, timeZone) ?? message\n },\n })\n },\n }\n}\n\nfunction parseIncomingMessage(value: unknown): { id: string; text: string } | undefined {\n if (!isRecord(value) || typeof value.id !== 'string' || !/^[\\w-]{1,64}$/.test(value.id)) {\n return undefined\n }\n if (value.role !== 'user' || !Array.isArray(value.parts)) {\n return undefined\n }\n const text = value.parts\n .flatMap((part) =>\n isRecord(part) && part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],\n )\n .join('\\n')\n .trim()\n if (text.length > maxSourceCharacters) {\n return undefined\n }\n return { id: value.id, text }\n}\n\n/** Only targets the assistant may read. A collection document with no id is one being created. The label is shown to the model, so it is cut short. */\nfunction parsePageContext(req: PayloadRequest, value: unknown): PageContext | undefined {\n if (!isRecord(value)) {\n return undefined\n }\n const kind =\n value.kind === 'global' ? 'global' : value.kind === 'collection' ? 'collection' : undefined\n const slug = typeof value.slug === 'string' ? value.slug : ''\n if (\n !kind ||\n !planTargets(req.payload.config).some((target) => target.kind === kind && target.slug === slug)\n ) {\n return undefined\n }\n const id = typeof value.id === 'string' && /^[\\w-]{1,64}$/.test(value.id) ? value.id : undefined\n const label =\n typeof value.label === 'string' && value.label.trim() ? value.label.trim().slice(0, 200) : slug\n return { kind, slug, ...(id ? { id } : {}), label }\n}\n\n/**\n * The open document as the model reads it. Form values from the drawer include unsaved changes;\n * without them, the saved document is read with the editor's access.\n */\nasync function readOpenDocument(\n req: PayloadRequest,\n context: PageContext,\n formData: unknown,\n): Promise<OpenDocument | undefined> {\n if (isRecord(formData)) {\n return {\n context,\n document: await readableTarget(req.payload.config, context.kind, context.slug, formData),\n }\n }\n const read = await readDocument(req, context.kind, context.slug, context.id)\n return 'error' in read ? undefined : { context, document: read.document }\n}\n\n/** Sent with every message, so it is an outline. readDocument with a path returns any part in full. */\nfunction outlineText(document: unknown): string {\n const json = JSON.stringify(outline(document))\n return json.length > maxContextCharacters\n ? `${json.slice(0, maxContextCharacters)} [cut: ${json.length - maxContextCharacters} more characters]`\n : json\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n"],"names":["createGateway","convertToModelMessages","createIdGenerator","stepCountIs","streamText","addAttachments","conversationFiles","modelFileParts","findAttachments","activeBilling","redactKey","pruneToolResults","threadTitle","withTurnContext","findThread","saveThread","assistantTools","readDocument","recordSpend","spendCheck","spentToday","chatEndpointPath","defaultModelId","keepToolResultsForTurns","maxContextCharacters","maxModelSteps","maxSourceCharacters","planTargets","systemPrompt","outline","readableTarget","allowAssistant","accountAllowance","budgetExceededMessage","dailyCapUsd","errorMessage","readChatForm","generateMessageId","prefix","size","chatEndpoint","options","fallback","siteKey","path","method","handler","req","settings","Response","timeZone","model","billing","allowance","spend","spentUsd","capUsd","dailySpendCapPercent","undefined","blocked","json","message","status","form","incoming","parseIncomingMessage","thread","chatId","knownFiles","files","match","attachmentIds","ok","text","added","length","context","parsePageContext","attachments","map","file","offset","index","name","type","userMessage","id","role","parts","metadata","history","messages","title","join","threadId","available","payload","config","open","readOpenDocument","contextData","planned","tools","collectionRules","onPlan","openDocument","modelMessages","fileParts","outlineText","document","ignoreIncompleteToolCalls","gateway","ownKey","key","record","steps","error","logger","err","result","system","siteRules","stopWhen","maxOutputTokens","maxRetries","abortSignal","signal","onFinish","onAbort","providerOptions","caching","byok","credentials","order","providers","anthropic","thinking","effort","consumeStream","toUIMessageStreamResponse","originalMessages","sendReasoning","messageMetadata","part","usage","inputTokens","totalUsage","outputTokens","totalTokens","cachedInputTokens","inputTokenDetails","cacheReadTokens","responseMessage","onError","value","isRecord","test","Array","isArray","flatMap","trim","kind","slug","some","target","label","slice","formData","read","JSON","stringify","Boolean"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ export interface ChatForm {
3
+ /**
4
+ * Attachments added since the last message, by id. The files are already in storage; the panel uploaded
5
+ * them directly, so no file passes through this request.
6
+ */
7
+ attachmentIds: string[];
8
+ chatId: string;
9
+ /** The document open in the admin, if any. */
10
+ context: unknown;
11
+ /** The open document's current form values, including unsaved changes. */
12
+ contextData: unknown;
13
+ /** The new user message, as the panel built it. Parsed and checked by the chat endpoint. */
14
+ message: unknown;
15
+ }
16
+ /** A chat request carries text and ids only, so it stays well under hosting request limits such as Vercel's. */
17
+ export declare function readChatForm(req: PayloadRequest): Promise<ChatForm | Response>;
18
+ /** Proceed sends only the plan's id. The files it saves are read from the conversation's attachments. */
19
+ export declare function readPlanRequest(req: PayloadRequest): Promise<{
20
+ planId: string;
21
+ } | Response>;
22
+ /** The error's own message. Where a request carried the site's own provider key, redact it with `redactKey`. */
23
+ export declare function errorMessage(error: unknown): string;
@@ -0,0 +1,95 @@
1
+ import { maxContextUploadCharacters, maxFiles, maxSourceCharacters } from '../constants.js';
2
+ /** A chat request carries text and ids only, so it stays well under hosting request limits such as Vercel's. */ export async function readChatForm(req) {
3
+ const form = await readFormData(req);
4
+ if (form instanceof Response) {
5
+ return form;
6
+ }
7
+ const chatId = textField(form, 'chatId');
8
+ if (!/^[\w-]{1,64}$/.test(chatId)) {
9
+ return Response.json({
10
+ message: 'Missing the conversation.'
11
+ }, {
12
+ status: 400
13
+ });
14
+ }
15
+ const rawMessage = textField(form, 'message');
16
+ const rawContextData = textField(form, 'contextData');
17
+ if (rawMessage.length > maxSourceCharacters * 2 || rawContextData.length > maxContextUploadCharacters) {
18
+ return Response.json({
19
+ message: 'The message is too long.'
20
+ }, {
21
+ status: 400
22
+ });
23
+ }
24
+ const attachmentIds = parseJson(textField(form, 'attachmentIds'));
25
+ if (attachmentIds !== undefined && (!Array.isArray(attachmentIds) || attachmentIds.length > maxFiles || !attachmentIds.every((id)=>typeof id === 'string' && /^[\w-]{1,64}$/.test(id)))) {
26
+ return Response.json({
27
+ message: 'The attachments could not be read.'
28
+ }, {
29
+ status: 400
30
+ });
31
+ }
32
+ return {
33
+ chatId,
34
+ message: parseJson(rawMessage),
35
+ context: parseJson(textField(form, 'context')),
36
+ contextData: parseJson(rawContextData),
37
+ attachmentIds: attachmentIds ?? []
38
+ };
39
+ }
40
+ /** Proceed sends only the plan's id. The files it saves are read from the conversation's attachments. */ export async function readPlanRequest(req) {
41
+ let body;
42
+ try {
43
+ body = await req.json?.();
44
+ } catch {
45
+ body = undefined;
46
+ }
47
+ const planId = body && typeof body === 'object' && 'planId' in body && typeof body.planId === 'string' ? body.planId : '';
48
+ if (!planId) {
49
+ return Response.json({
50
+ message: 'Missing the plan.'
51
+ }, {
52
+ status: 400
53
+ });
54
+ }
55
+ return {
56
+ planId
57
+ };
58
+ }
59
+ /** The error's own message. Where a request carried the site's own provider key, redact it with `redactKey`. */ export function errorMessage(error) {
60
+ return error instanceof Error && error.message ? error.message : 'The assistant could not reply.';
61
+ }
62
+ function textField(form, name) {
63
+ const value = form.get(name);
64
+ return typeof value === 'string' ? value : '';
65
+ }
66
+ async function readFormData(req) {
67
+ if (typeof req.formData !== 'function') {
68
+ return Response.json({
69
+ message: 'Send the request as form data.'
70
+ }, {
71
+ status: 400
72
+ });
73
+ }
74
+ try {
75
+ return await req.formData();
76
+ } catch {
77
+ return Response.json({
78
+ message: 'Send the request as form data.'
79
+ }, {
80
+ status: 400
81
+ });
82
+ }
83
+ }
84
+ function parseJson(value) {
85
+ if (!value) {
86
+ return undefined;
87
+ }
88
+ try {
89
+ return JSON.parse(value);
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ }
94
+
95
+ //# sourceMappingURL=form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/form.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { maxContextUploadCharacters, maxFiles, maxSourceCharacters } from '../constants.js'\n\nexport interface ChatForm {\n /**\n * Attachments added since the last message, by id. The files are already in storage; the panel uploaded\n * them directly, so no file passes through this request.\n */\n attachmentIds: string[]\n chatId: string\n /** The document open in the admin, if any. */\n context: unknown\n /** The open document's current form values, including unsaved changes. */\n contextData: unknown\n /** The new user message, as the panel built it. Parsed and checked by the chat endpoint. */\n message: unknown\n}\n\n/** A chat request carries text and ids only, so it stays well under hosting request limits such as Vercel's. */\nexport async function readChatForm(req: PayloadRequest): Promise<ChatForm | Response> {\n const form = await readFormData(req)\n if (form instanceof Response) {\n return form\n }\n\n const chatId = textField(form, 'chatId')\n if (!/^[\\w-]{1,64}$/.test(chatId)) {\n return Response.json({ message: 'Missing the conversation.' }, { status: 400 })\n }\n\n const rawMessage = textField(form, 'message')\n const rawContextData = textField(form, 'contextData')\n if (\n rawMessage.length > maxSourceCharacters * 2 ||\n rawContextData.length > maxContextUploadCharacters\n ) {\n return Response.json({ message: 'The message is too long.' }, { status: 400 })\n }\n\n const attachmentIds = parseJson(textField(form, 'attachmentIds'))\n if (\n attachmentIds !== undefined &&\n (!Array.isArray(attachmentIds) ||\n attachmentIds.length > maxFiles ||\n !attachmentIds.every((id) => typeof id === 'string' && /^[\\w-]{1,64}$/.test(id)))\n ) {\n return Response.json({ message: 'The attachments could not be read.' }, { status: 400 })\n }\n\n return {\n chatId,\n message: parseJson(rawMessage),\n context: parseJson(textField(form, 'context')),\n contextData: parseJson(rawContextData),\n attachmentIds: (attachmentIds as string[] | undefined) ?? [],\n }\n}\n\n/** Proceed sends only the plan's id. The files it saves are read from the conversation's attachments. */\nexport async function readPlanRequest(req: PayloadRequest): Promise<{ planId: string } | Response> {\n let body: unknown\n try {\n body = await req.json?.()\n } catch {\n body = undefined\n }\n const planId =\n body && typeof body === 'object' && 'planId' in body && typeof body.planId === 'string'\n ? body.planId\n : ''\n if (!planId) {\n return Response.json({ message: 'Missing the plan.' }, { status: 400 })\n }\n return { planId }\n}\n\n/** The error's own message. Where a request carried the site's own provider key, redact it with `redactKey`. */\nexport function errorMessage(error: unknown): string {\n return error instanceof Error && error.message ? error.message : 'The assistant could not reply.'\n}\n\nfunction textField(form: FormData, name: string): string {\n const value = form.get(name)\n return typeof value === 'string' ? value : ''\n}\n\nasync function readFormData(req: PayloadRequest): Promise<FormData | Response> {\n if (typeof req.formData !== 'function') {\n return Response.json({ message: 'Send the request as form data.' }, { status: 400 })\n }\n try {\n return await req.formData()\n } catch {\n return Response.json({ message: 'Send the request as form data.' }, { status: 400 })\n }\n}\n\nfunction parseJson(value: string): unknown {\n if (!value) {\n return undefined\n }\n try {\n return JSON.parse(value)\n } catch {\n return undefined\n }\n}\n"],"names":["maxContextUploadCharacters","maxFiles","maxSourceCharacters","readChatForm","req","form","readFormData","Response","chatId","textField","test","json","message","status","rawMessage","rawContextData","length","attachmentIds","parseJson","undefined","Array","isArray","every","id","context","contextData","readPlanRequest","body","planId","errorMessage","error","Error","name","value","get","formData","JSON","parse"],"mappings":"AAEA,SAASA,0BAA0B,EAAEC,QAAQ,EAAEC,mBAAmB,QAAQ,kBAAiB;AAiB3F,8GAA8G,GAC9G,OAAO,eAAeC,aAAaC,GAAmB;IACpD,MAAMC,OAAO,MAAMC,aAAaF;IAChC,IAAIC,gBAAgBE,UAAU;QAC5B,OAAOF;IACT;IAEA,MAAMG,SAASC,UAAUJ,MAAM;IAC/B,IAAI,CAAC,gBAAgBK,IAAI,CAACF,SAAS;QACjC,OAAOD,SAASI,IAAI,CAAC;YAAEC,SAAS;QAA4B,GAAG;YAAEC,QAAQ;QAAI;IAC/E;IAEA,MAAMC,aAAaL,UAAUJ,MAAM;IACnC,MAAMU,iBAAiBN,UAAUJ,MAAM;IACvC,IACES,WAAWE,MAAM,GAAGd,sBAAsB,KAC1Ca,eAAeC,MAAM,GAAGhB,4BACxB;QACA,OAAOO,SAASI,IAAI,CAAC;YAAEC,SAAS;QAA2B,GAAG;YAAEC,QAAQ;QAAI;IAC9E;IAEA,MAAMI,gBAAgBC,UAAUT,UAAUJ,MAAM;IAChD,IACEY,kBAAkBE,aACjB,CAAA,CAACC,MAAMC,OAAO,CAACJ,kBACdA,cAAcD,MAAM,GAAGf,YACvB,CAACgB,cAAcK,KAAK,CAAC,CAACC,KAAO,OAAOA,OAAO,YAAY,gBAAgBb,IAAI,CAACa,IAAG,GACjF;QACA,OAAOhB,SAASI,IAAI,CAAC;YAAEC,SAAS;QAAqC,GAAG;YAAEC,QAAQ;QAAI;IACxF;IAEA,OAAO;QACLL;QACAI,SAASM,UAAUJ;QACnBU,SAASN,UAAUT,UAAUJ,MAAM;QACnCoB,aAAaP,UAAUH;QACvBE,eAAe,AAACA,iBAA0C,EAAE;IAC9D;AACF;AAEA,uGAAuG,GACvG,OAAO,eAAeS,gBAAgBtB,GAAmB;IACvD,IAAIuB;IACJ,IAAI;QACFA,OAAO,MAAMvB,IAAIO,IAAI;IACvB,EAAE,OAAM;QACNgB,OAAOR;IACT;IACA,MAAMS,SACJD,QAAQ,OAAOA,SAAS,YAAY,YAAYA,QAAQ,OAAOA,KAAKC,MAAM,KAAK,WAC3ED,KAAKC,MAAM,GACX;IACN,IAAI,CAACA,QAAQ;QACX,OAAOrB,SAASI,IAAI,CAAC;YAAEC,SAAS;QAAoB,GAAG;YAAEC,QAAQ;QAAI;IACvE;IACA,OAAO;QAAEe;IAAO;AAClB;AAEA,8GAA8G,GAC9G,OAAO,SAASC,aAAaC,KAAc;IACzC,OAAOA,iBAAiBC,SAASD,MAAMlB,OAAO,GAAGkB,MAAMlB,OAAO,GAAG;AACnE;AAEA,SAASH,UAAUJ,IAAc,EAAE2B,IAAY;IAC7C,MAAMC,QAAQ5B,KAAK6B,GAAG,CAACF;IACvB,OAAO,OAAOC,UAAU,WAAWA,QAAQ;AAC7C;AAEA,eAAe3B,aAAaF,GAAmB;IAC7C,IAAI,OAAOA,IAAI+B,QAAQ,KAAK,YAAY;QACtC,OAAO5B,SAASI,IAAI,CAAC;YAAEC,SAAS;QAAiC,GAAG;YAAEC,QAAQ;QAAI;IACpF;IACA,IAAI;QACF,OAAO,MAAMT,IAAI+B,QAAQ;IAC3B,EAAE,OAAM;QACN,OAAO5B,SAASI,IAAI,CAAC;YAAEC,SAAS;QAAiC,GAAG;YAAEC,QAAQ;QAAI;IACpF;AACF;AAEA,SAASK,UAAUe,KAAa;IAC9B,IAAI,CAACA,OAAO;QACV,OAAOd;IACT;IACA,IAAI;QACF,OAAOiB,KAAKC,KAAK,CAACJ;IACpB,EAAE,OAAM;QACN,OAAOd;IACT;AACF"}
@@ -0,0 +1,13 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { OwnKey } from '../billing.js';
3
+ import type { AiAssistantPluginOptions, AssistantSettings } from '../types.js';
4
+ /**
5
+ * On Vercel billing: the editor's own spend today against the daily cap and, for admins, the project's remaining
6
+ * budget, which is the whole project's, so editors do not see it. On the own key: which provider pays and, for
7
+ * admins, everyone's spend on the key today and any calls that fell back to Vercel's credit.
8
+ */
9
+ export declare function usageEndpoint(fallback: AssistantSettings, options: AiAssistantPluginOptions, siteKey: OwnKey | undefined): {
10
+ path: string;
11
+ method: "get";
12
+ handler: (req: PayloadRequest) => Promise<Response>;
13
+ };
@@ -0,0 +1,70 @@
1
+ import { sweepExpiredAttachments } from '../attachments/store.js';
2
+ import { activeBilling } from '../billing.js';
3
+ import { dayIn, spendCheck, spentToday, usageTotals } from '../chat/usage.js';
4
+ import { usageEndpointPath } from '../constants.js';
5
+ import { isAdminRole } from '../settings.js';
6
+ import { allowAssistant } from './access.js';
7
+ import { accountAllowance, budgetView, dailyCapUsd } from './allowance.js';
8
+ /**
9
+ * On Vercel billing: the editor's own spend today against the daily cap and, for admins, the project's remaining
10
+ * budget, which is the whole project's, so editors do not see it. On the own key: which provider pays and, for
11
+ * admins, everyone's spend on the key today and any calls that fell back to Vercel's credit.
12
+ */ export function usageEndpoint(fallback, options, siteKey) {
13
+ return {
14
+ path: usageEndpointPath,
15
+ method: 'get',
16
+ handler: async (req)=>{
17
+ const settings = await allowAssistant(req, fallback);
18
+ if (settings instanceof Response) {
19
+ return settings;
20
+ }
21
+ // Opening the panel is a convenient, regular moment to clear expired attachments, a few at a time,
22
+ // so no scheduled job is needed. A failure here must not stop the panel loading.
23
+ await sweepExpiredAttachments(req).catch((error)=>req.payload.logger.error({
24
+ err: error
25
+ }, 'Humaan AI could not clear expired attachments'));
26
+ const timeZone = options.timeZone ?? 'UTC';
27
+ const billing = activeBilling(settings.billing, siteKey);
28
+ if (billing.billing === 'own-key') {
29
+ const ownKey = {
30
+ provider: billing.key.label
31
+ };
32
+ if (isAdminRole(req.user)) {
33
+ const day = dayIn(timeZone);
34
+ const totals = await usageTotals(req, day, day);
35
+ ownKey.todayUsd = totals.ownKeyUsd;
36
+ ownKey.fallbackSteps = totals.fallbackSteps;
37
+ }
38
+ const view = {
39
+ ownKey
40
+ };
41
+ return Response.json(view, {
42
+ headers: {
43
+ 'Cache-Control': 'private, no-store'
44
+ }
45
+ });
46
+ }
47
+ const allowance = await accountAllowance(req);
48
+ const today = spendCheck({
49
+ spentUsd: await spentToday(req, timeZone),
50
+ capUsd: dailyCapUsd(settings.dailySpendCapPercent, allowance)
51
+ });
52
+ // Only shares of the Vercel budget leave the server, never its dollar amounts.
53
+ const view = today.capUsd === undefined ? {} : {
54
+ today: {
55
+ usedShare: today.spentUsd / today.capUsd
56
+ }
57
+ };
58
+ if (isAdminRole(req.user) && typeof allowance !== 'string') {
59
+ view.budget = budgetView(allowance);
60
+ }
61
+ return Response.json(view, {
62
+ headers: {
63
+ 'Cache-Control': 'private, no-store'
64
+ }
65
+ });
66
+ }
67
+ };
68
+ }
69
+
70
+ //# sourceMappingURL=usageEndpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/usageEndpoint.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport type { OwnKey } from '../billing.js'\nimport type {\n AiAssistantPluginOptions,\n AssistantSettings,\n OwnKeyView,\n UsageView,\n} from '../types.js'\n\nimport { sweepExpiredAttachments } from '../attachments/store.js'\nimport { activeBilling } from '../billing.js'\nimport { dayIn, spendCheck, spentToday, usageTotals } from '../chat/usage.js'\nimport { usageEndpointPath } from '../constants.js'\nimport { isAdminRole } from '../settings.js'\nimport { allowAssistant } from './access.js'\nimport { accountAllowance, budgetView, dailyCapUsd } from './allowance.js'\n\n/**\n * On Vercel billing: the editor's own spend today against the daily cap and, for admins, the project's remaining\n * budget, which is the whole project's, so editors do not see it. On the own key: which provider pays and, for\n * admins, everyone's spend on the key today and any calls that fell back to Vercel's credit.\n */\nexport function usageEndpoint(\n fallback: AssistantSettings,\n options: AiAssistantPluginOptions,\n siteKey: OwnKey | undefined,\n) {\n return {\n path: usageEndpointPath,\n method: 'get' as const,\n handler: async (req: PayloadRequest) => {\n const settings = await allowAssistant(req, fallback)\n if (settings instanceof Response) {\n return settings\n }\n // Opening the panel is a convenient, regular moment to clear expired attachments, a few at a time,\n // so no scheduled job is needed. A failure here must not stop the panel loading.\n await sweepExpiredAttachments(req).catch((error) =>\n req.payload.logger.error({ err: error }, 'Humaan AI could not clear expired attachments'),\n )\n\n const timeZone = options.timeZone ?? 'UTC'\n const billing = activeBilling(settings.billing, siteKey)\n if (billing.billing === 'own-key') {\n const ownKey: OwnKeyView = { provider: billing.key.label }\n if (isAdminRole(req.user)) {\n const day = dayIn(timeZone)\n const totals = await usageTotals(req, day, day)\n ownKey.todayUsd = totals.ownKeyUsd\n ownKey.fallbackSteps = totals.fallbackSteps\n }\n const view: UsageView = { ownKey }\n return Response.json(view, { headers: { 'Cache-Control': 'private, no-store' } })\n }\n\n const allowance = await accountAllowance(req)\n const today = spendCheck({\n spentUsd: await spentToday(req, timeZone),\n capUsd: dailyCapUsd(settings.dailySpendCapPercent, allowance),\n })\n // Only shares of the Vercel budget leave the server, never its dollar amounts.\n const view: UsageView =\n today.capUsd === undefined ? {} : { today: { usedShare: today.spentUsd / today.capUsd } }\n if (isAdminRole(req.user) && typeof allowance !== 'string') {\n view.budget = budgetView(allowance)\n }\n return Response.json(view, { headers: { 'Cache-Control': 'private, no-store' } })\n },\n }\n}\n"],"names":["sweepExpiredAttachments","activeBilling","dayIn","spendCheck","spentToday","usageTotals","usageEndpointPath","isAdminRole","allowAssistant","accountAllowance","budgetView","dailyCapUsd","usageEndpoint","fallback","options","siteKey","path","method","handler","req","settings","Response","catch","error","payload","logger","err","timeZone","billing","ownKey","provider","key","label","user","day","totals","todayUsd","ownKeyUsd","fallbackSteps","view","json","headers","allowance","today","spentUsd","capUsd","dailySpendCapPercent","undefined","usedShare","budget"],"mappings":"AAUA,SAASA,uBAAuB,QAAQ,0BAAyB;AACjE,SAASC,aAAa,QAAQ,gBAAe;AAC7C,SAASC,KAAK,EAAEC,UAAU,EAAEC,UAAU,EAAEC,WAAW,QAAQ,mBAAkB;AAC7E,SAASC,iBAAiB,QAAQ,kBAAiB;AACnD,SAASC,WAAW,QAAQ,iBAAgB;AAC5C,SAASC,cAAc,QAAQ,cAAa;AAC5C,SAASC,gBAAgB,EAAEC,UAAU,EAAEC,WAAW,QAAQ,iBAAgB;AAE1E;;;;CAIC,GACD,OAAO,SAASC,cACdC,QAA2B,EAC3BC,OAAiC,EACjCC,OAA2B;IAE3B,OAAO;QACLC,MAAMV;QACNW,QAAQ;QACRC,SAAS,OAAOC;YACd,MAAMC,WAAW,MAAMZ,eAAeW,KAAKN;YAC3C,IAAIO,oBAAoBC,UAAU;gBAChC,OAAOD;YACT;YACA,mGAAmG;YACnG,iFAAiF;YACjF,MAAMpB,wBAAwBmB,KAAKG,KAAK,CAAC,CAACC,QACxCJ,IAAIK,OAAO,CAACC,MAAM,CAACF,KAAK,CAAC;oBAAEG,KAAKH;gBAAM,GAAG;YAG3C,MAAMI,WAAWb,QAAQa,QAAQ,IAAI;YACrC,MAAMC,UAAU3B,cAAcmB,SAASQ,OAAO,EAAEb;YAChD,IAAIa,QAAQA,OAAO,KAAK,WAAW;gBACjC,MAAMC,SAAqB;oBAAEC,UAAUF,QAAQG,GAAG,CAACC,KAAK;gBAAC;gBACzD,IAAIzB,YAAYY,IAAIc,IAAI,GAAG;oBACzB,MAAMC,MAAMhC,MAAMyB;oBAClB,MAAMQ,SAAS,MAAM9B,YAAYc,KAAKe,KAAKA;oBAC3CL,OAAOO,QAAQ,GAAGD,OAAOE,SAAS;oBAClCR,OAAOS,aAAa,GAAGH,OAAOG,aAAa;gBAC7C;gBACA,MAAMC,OAAkB;oBAAEV;gBAAO;gBACjC,OAAOR,SAASmB,IAAI,CAACD,MAAM;oBAAEE,SAAS;wBAAE,iBAAiB;oBAAoB;gBAAE;YACjF;YAEA,MAAMC,YAAY,MAAMjC,iBAAiBU;YACzC,MAAMwB,QAAQxC,WAAW;gBACvByC,UAAU,MAAMxC,WAAWe,KAAKQ;gBAChCkB,QAAQlC,YAAYS,SAAS0B,oBAAoB,EAAEJ;YACrD;YACA,+EAA+E;YAC/E,MAAMH,OACJI,MAAME,MAAM,KAAKE,YAAY,CAAC,IAAI;gBAAEJ,OAAO;oBAAEK,WAAWL,MAAMC,QAAQ,GAAGD,MAAME,MAAM;gBAAC;YAAE;YAC1F,IAAItC,YAAYY,IAAIc,IAAI,KAAK,OAAOS,cAAc,UAAU;gBAC1DH,KAAKU,MAAM,GAAGvC,WAAWgC;YAC3B;YACA,OAAOrB,SAASmB,IAAI,CAACD,MAAM;gBAAEE,SAAS;oBAAE,iBAAiB;gBAAoB;YAAE;QACjF;IACF;AACF"}
@@ -0,0 +1,4 @@
1
+ export { aiAssistantAttachmentsCollection } from './attachments/collection.js';
2
+ export { aiAssistantAttachmentSlug } from './constants.js';
3
+ export { aiAssistantPlugin } from './plugin.js';
4
+ export type { AiAssistantPluginOptions, AssistantChange, AssistantPlanView, AssistantSettings, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { aiAssistantAttachmentsCollection } from './attachments/collection.js';
2
+ export { aiAssistantAttachmentSlug } from './constants.js';
3
+ export { aiAssistantPlugin } from './plugin.js';
4
+
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { aiAssistantAttachmentsCollection } from './attachments/collection.js'\nexport { aiAssistantAttachmentSlug } from './constants.js'\nexport { aiAssistantPlugin } from './plugin.js'\nexport type {\n AiAssistantPluginOptions,\n AssistantChange,\n AssistantPlanView,\n AssistantSettings,\n} from './types.js'\n"],"names":["aiAssistantAttachmentsCollection","aiAssistantAttachmentSlug","aiAssistantPlugin"],"mappings":"AAAA,SAASA,gCAAgC,QAAQ,8BAA6B;AAC9E,SAASC,yBAAyB,QAAQ,iBAAgB;AAC1D,SAASC,iBAAiB,QAAQ,cAAa"}
@@ -0,0 +1,4 @@
1
+ import type { SanitizedServerEditorConfig } from '@payloadcms/richtext-lexical';
2
+ import type { RichTextField, SanitizedConfig } from 'payload';
3
+ /** The sanitized lexical editor config for a rich text field. Cached per field. */
4
+ export declare function editorConfigFor(config: SanitizedConfig, field: RichTextField): Promise<SanitizedServerEditorConfig>;
@@ -0,0 +1,19 @@
1
+ import { editorConfigFactory } from '@payloadcms/richtext-lexical';
2
+ const cache = new WeakMap();
3
+ /** The sanitized lexical editor config for a rich text field. Cached per field. */ export function editorConfigFor(config, field) {
4
+ const cached = cache.get(field);
5
+ if (cached) {
6
+ return cached;
7
+ }
8
+ const editor = field.editor;
9
+ const next = typeof editor === 'object' && editor !== null && 'editorConfig' in editor ? Promise.resolve(editorConfigFactory.fromField({
10
+ field: field
11
+ })) : editorConfigFactory.fromUnsanitizedField({
12
+ config,
13
+ field: field
14
+ });
15
+ cache.set(field, next);
16
+ return next;
17
+ }
18
+
19
+ //# sourceMappingURL=editorConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lexical/editorConfig.ts"],"sourcesContent":["import type { SanitizedServerEditorConfig } from '@payloadcms/richtext-lexical'\nimport type { RichTextField, SanitizedConfig } from 'payload'\n\nimport { editorConfigFactory } from '@payloadcms/richtext-lexical'\n\nconst cache = new WeakMap<RichTextField, Promise<SanitizedServerEditorConfig>>()\n\n/** The sanitized lexical editor config for a rich text field. Cached per field. */\nexport function editorConfigFor(\n config: SanitizedConfig,\n field: RichTextField,\n): Promise<SanitizedServerEditorConfig> {\n const cached = cache.get(field)\n if (cached) {\n return cached\n }\n\n const editor = field.editor\n const next =\n typeof editor === 'object' && editor !== null && 'editorConfig' in editor\n ? Promise.resolve(\n editorConfigFactory.fromField({\n field: field as { editor: { editorConfig: never } } & RichTextField,\n }),\n )\n : editorConfigFactory.fromUnsanitizedField({\n config,\n field: field as { editor: NonNullable<RichTextField['editor']> } & RichTextField,\n })\n cache.set(field, next)\n return next\n}\n"],"names":["editorConfigFactory","cache","WeakMap","editorConfigFor","config","field","cached","get","editor","next","Promise","resolve","fromField","fromUnsanitizedField","set"],"mappings":"AAGA,SAASA,mBAAmB,QAAQ,+BAA8B;AAElE,MAAMC,QAAQ,IAAIC;AAElB,iFAAiF,GACjF,OAAO,SAASC,gBACdC,MAAuB,EACvBC,KAAoB;IAEpB,MAAMC,SAASL,MAAMM,GAAG,CAACF;IACzB,IAAIC,QAAQ;QACV,OAAOA;IACT;IAEA,MAAME,SAASH,MAAMG,MAAM;IAC3B,MAAMC,OACJ,OAAOD,WAAW,YAAYA,WAAW,QAAQ,kBAAkBA,SAC/DE,QAAQC,OAAO,CACbX,oBAAoBY,SAAS,CAAC;QAC5BP,OAAOA;IACT,MAEFL,oBAAoBa,oBAAoB,CAAC;QACvCT;QACAC,OAAOA;IACT;IACNJ,MAAMa,GAAG,CAACT,OAAOI;IACjB,OAAOA;AACT"}
@@ -0,0 +1,10 @@
1
+ import type { RichTextField, SanitizedConfig } from 'payload';
2
+ import { convertLexicalToMarkdown } from '@payloadcms/richtext-lexical';
3
+ type LexicalState = Parameters<typeof convertLexicalToMarkdown>[0]['data'];
4
+ /** Turns stored lexical JSON back into markdown, so the assistant reads the words rather than the node tree. */
5
+ export declare function lexicalToMarkdown({ config, field, data, }: {
6
+ config: SanitizedConfig;
7
+ data: LexicalState;
8
+ field: RichTextField;
9
+ }): Promise<string>;
10
+ export {};