@mvriu5/payload-ai 0.5.8 → 0.5.9

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.
@@ -0,0 +1,66 @@
1
+ import { useState } from "react";
2
+ import { DiffDialog } from "./DiffDialog.js";
3
+ import { FileDiff } from "./Icons.js";
4
+ import styles from "./RecentChangesList.module.css";
5
+ import { ExternalLinkIcon } from "@payloadcms/ui";
6
+ const getChangeProposal = (change)=>({
7
+ action: change.action || "update",
8
+ collection: change.collection || undefined,
9
+ id: change.documentID || undefined,
10
+ label: change.title,
11
+ slug: change.slug || undefined
12
+ });
13
+ export const RecentChangesList = ({ changes })=>{
14
+ const [activeDiff, setActiveDiff] = useState(null);
15
+ return /*#__PURE__*/ React.createElement("aside", {
16
+ className: styles.recentChanges
17
+ }, /*#__PURE__*/ React.createElement("div", {
18
+ className: styles.header
19
+ }, /*#__PURE__*/ React.createElement("h3", {
20
+ className: styles.title
21
+ }, "Recent changes")), /*#__PURE__*/ React.createElement("div", {
22
+ className: styles.list
23
+ }, changes.length === 0 ? /*#__PURE__*/ React.createElement("div", {
24
+ className: styles.empty
25
+ }, "No changes yet.") : changes.map((change, index)=>/*#__PURE__*/ React.createElement("div", {
26
+ className: styles.item,
27
+ key: `${change.title}-${index}`
28
+ }, /*#__PURE__*/ React.createElement("div", {
29
+ className: styles.titleRow
30
+ }, /*#__PURE__*/ React.createElement("div", {
31
+ className: styles.itemTitle
32
+ }, change.title), change.url ? /*#__PURE__*/ React.createElement("a", {
33
+ className: styles.ghostButton,
34
+ href: change.url,
35
+ rel: "noreferrer noopener",
36
+ target: "_blank"
37
+ }, /*#__PURE__*/ React.createElement(ExternalLinkIcon, null)) : null), /*#__PURE__*/ React.createElement("div", {
38
+ className: styles.stats
39
+ }, /*#__PURE__*/ React.createElement("code", {
40
+ className: styles.additions
41
+ }, "+", change.additions), /*#__PURE__*/ React.createElement("code", {
42
+ className: styles.removals
43
+ }, "-", change.removals)), /*#__PURE__*/ React.createElement("button", {
44
+ className: styles.button,
45
+ onClick: ()=>{
46
+ if (change.before === undefined || change.after === undefined) {
47
+ return;
48
+ }
49
+ setActiveDiff({
50
+ diff: {
51
+ after: change.after,
52
+ before: change.before
53
+ },
54
+ proposal: getChangeProposal(change)
55
+ });
56
+ },
57
+ type: "button"
58
+ }, /*#__PURE__*/ React.createElement(FileDiff, {
59
+ height: 16,
60
+ width: 16
61
+ }), "Review")))), activeDiff ? /*#__PURE__*/ React.createElement(DiffDialog, {
62
+ diff: activeDiff.diff,
63
+ onClose: ()=>setActiveDiff(null),
64
+ proposal: activeDiff.proposal
65
+ }) : null);
66
+ };
@@ -0,0 +1,135 @@
1
+ .recentChanges {
2
+ border: 1px solid var(--theme-elevation-150);
3
+ border-radius: 8px;
4
+ display: flex;
5
+ flex-direction: column;
6
+ min-height: 0;
7
+ padding: 16px;
8
+ }
9
+
10
+ .header {
11
+ flex: 0 0 auto;
12
+ margin-bottom: 10px;
13
+ }
14
+
15
+ .title {
16
+ font-size: 14px;
17
+ line-height: 1.3;
18
+ margin: 0;
19
+ }
20
+
21
+ .list {
22
+ display: flex;
23
+ flex: 1 1 auto;
24
+ flex-direction: column;
25
+ gap: 8px;
26
+ max-height: 260px;
27
+ min-height: 0;
28
+ overflow: auto;
29
+ padding-right: 2px;
30
+ }
31
+
32
+ .empty {
33
+ color: var(--theme-elevation-500);
34
+ font-size: 12px;
35
+ line-height: 1.4;
36
+ }
37
+
38
+ .item {
39
+ align-items: center;
40
+ border: 1px solid var(--theme-elevation-100);
41
+ border-radius: 4px;
42
+ display: grid;
43
+ gap: 8px;
44
+ grid-template-columns: minmax(0, 1fr) auto auto;
45
+ padding: 10px;
46
+ }
47
+
48
+ .titleRow {
49
+ align-items: center;
50
+ display: flex;
51
+ gap: 8px;
52
+ min-width: 0;
53
+ }
54
+
55
+ .itemTitle {
56
+ color: var(--theme-text);
57
+ flex: 0 1 auto;
58
+ font-size: 12px;
59
+ font-weight: 600;
60
+ line-height: 1.4;
61
+ min-width: 0;
62
+ overflow: hidden;
63
+ text-overflow: ellipsis;
64
+ white-space: nowrap;
65
+ }
66
+
67
+ .stats {
68
+ display: flex;
69
+ gap: 6px;
70
+ justify-content: flex-end;
71
+ }
72
+
73
+ .additions,
74
+ .removals {
75
+ border-radius: 3px;
76
+ font-size: 11px;
77
+ line-height: 18px;
78
+ padding: 0 5px;
79
+ }
80
+
81
+ .additions {
82
+ background: rgba(39, 132, 90, 0.16);
83
+ color: #27845a;
84
+ }
85
+
86
+ .removals {
87
+ background: rgba(160, 26, 26, 0.16);
88
+ color: #a01a1a;
89
+ }
90
+
91
+ .button {
92
+ align-items: center;
93
+ background: var(--theme-elevation-0);
94
+ border: 1px solid var(--theme-elevation-150);
95
+ border-radius: 4px;
96
+ color: var(--theme-text);
97
+ cursor: pointer;
98
+ display: inline-flex;
99
+ font-size: 12px;
100
+ gap: 6px;
101
+ justify-content: center;
102
+ line-height: 24px;
103
+ min-height: 24px;
104
+ padding: 0 10px;
105
+ text-align: center;
106
+ text-decoration: none;
107
+ }
108
+
109
+ .button:disabled {
110
+ color: var(--theme-elevation-400);
111
+ cursor: not-allowed;
112
+ }
113
+
114
+ .button svg {
115
+ color: var(--theme-elevation-500);
116
+ flex: 0 0 auto;
117
+ }
118
+
119
+ .ghostButton {
120
+ color: var(--theme-elevation-500);
121
+ flex: 0 0 auto;
122
+ font-size: 12px;
123
+ line-height: 18px;
124
+ text-decoration: none;
125
+ }
126
+
127
+ .ghostButton:hover {
128
+ text-decoration: underline;
129
+ }
130
+
131
+ @media (max-width: 768px) {
132
+ .list {
133
+ max-height: 220px;
134
+ }
135
+ }
@@ -1,6 +1,7 @@
1
1
  import type { PayloadHandler } from "payload";
2
2
  import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
3
3
  type AIApplyActionEndpointOptions = {
4
+ changeLogCollection?: string;
4
5
  collections?: ResolvedAICollectionPermissionMap;
5
6
  };
6
7
  export declare const createAIApplyActionEndpointHandler: (options?: AIApplyActionEndpointOptions) => PayloadHandler;
@@ -1,5 +1,5 @@
1
1
  import { verifyAIActionProposal } from "../ai/proposals.js";
2
- import { containsSensitiveData } from "../ai/sensitiveData.js";
2
+ import { containsSensitiveData, redactSensitiveData } from "../ai/sensitiveData.js";
3
3
  import { getCollectionFields, isAuthCollection, normalizeAuthData, normalizeDataForFields } from "../payload/normalizeData.js";
4
4
  import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
5
5
  const getProposalMeta = (proposal)=>{
@@ -16,9 +16,6 @@ const getAppliedDocReference = (doc)=>{
16
16
  id: doc.id
17
17
  };
18
18
  };
19
- const isKnownCollection = (req, collection)=>{
20
- return req.payload.config.collections.some((item)=>item.slug === collection);
21
- };
22
19
  const isAllowedCollection = (req, collection, collections, action = "read")=>{
23
20
  return isCollectionActionAllowed({
24
21
  action,
@@ -33,6 +30,151 @@ const isKnownGlobal = (req, slug)=>{
33
30
  const isRecord = (value)=>{
34
31
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
35
32
  };
33
+ const mergeData = (current, next)=>{
34
+ return Object.fromEntries(Object.entries({
35
+ ...current,
36
+ ...next
37
+ }).map(([key, value])=>{
38
+ const currentValue = current[key];
39
+ if (isRecord(currentValue) && isRecord(value)) {
40
+ return [
41
+ key,
42
+ mergeData(currentValue, value)
43
+ ];
44
+ }
45
+ return [
46
+ key,
47
+ value
48
+ ];
49
+ }));
50
+ };
51
+ const getJSONLineKey = (line)=>{
52
+ const match = /^(\s*)"([^"]+)":/.exec(line);
53
+ return match ? `${match[1]}${match[2]}` : null;
54
+ };
55
+ const countDiffChanges = ({ after, before })=>{
56
+ const beforeLines = JSON.stringify(before, null, 2).split("\n");
57
+ const afterLines = JSON.stringify(after, null, 2).split("\n");
58
+ const dp = Array.from({
59
+ length: beforeLines.length + 1
60
+ }, ()=>Array(afterLines.length + 1).fill(0));
61
+ let additions = 0;
62
+ let removals = 0;
63
+ for(let i = beforeLines.length - 1; i >= 0; i -= 1){
64
+ for(let j = afterLines.length - 1; j >= 0; j -= 1){
65
+ dp[i][j] = beforeLines[i] === afterLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
66
+ }
67
+ }
68
+ let beforeIndex = 0;
69
+ let afterIndex = 0;
70
+ while(beforeIndex < beforeLines.length && afterIndex < afterLines.length){
71
+ if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
72
+ beforeIndex += 1;
73
+ afterIndex += 1;
74
+ continue;
75
+ }
76
+ const beforeKey = getJSONLineKey(beforeLines[beforeIndex]);
77
+ const afterKey = getJSONLineKey(afterLines[afterIndex]);
78
+ if (beforeKey && beforeKey === afterKey) {
79
+ removals += 1;
80
+ additions += 1;
81
+ beforeIndex += 1;
82
+ afterIndex += 1;
83
+ continue;
84
+ }
85
+ if (dp[beforeIndex + 1][afterIndex] >= dp[beforeIndex][afterIndex + 1]) {
86
+ removals += 1;
87
+ beforeIndex += 1;
88
+ } else {
89
+ additions += 1;
90
+ afterIndex += 1;
91
+ }
92
+ }
93
+ removals += beforeLines.length - beforeIndex;
94
+ additions += afterLines.length - afterIndex;
95
+ return {
96
+ additions,
97
+ removals
98
+ };
99
+ };
100
+ const getTargetURL = ({ documentID, proposal, req })=>{
101
+ const adminRoute = req.payload.config.routes.admin || "/admin";
102
+ if (proposal.action === "updateGlobal") {
103
+ return `${adminRoute}/globals/${proposal.slug}`;
104
+ }
105
+ if (proposal.action === "delete") {
106
+ return null;
107
+ }
108
+ const id = proposal.action === "create" ? documentID : proposal.id;
109
+ if (!id) return null;
110
+ return `${adminRoute}/collections/${proposal.collection}/${id}`;
111
+ };
112
+ const getUserID = (req)=>{
113
+ const user = req.user;
114
+ return user?.id === undefined ? undefined : String(user.id);
115
+ };
116
+ const logAIChange = async ({ changeLogCollection, target, proposal, req })=>{
117
+ if (!changeLogCollection) return null;
118
+ const before = redactSensitiveData(target.before);
119
+ const after = redactSensitiveData(target.after);
120
+ const { additions, removals } = countDiffChanges({
121
+ after,
122
+ before
123
+ });
124
+ const proposalForLog = {
125
+ ...proposal
126
+ };
127
+ delete proposalForLog._aiSignature;
128
+ try {
129
+ await req.payload.create({
130
+ collection: changeLogCollection,
131
+ data: {
132
+ action: proposal.action,
133
+ additions,
134
+ after,
135
+ before,
136
+ collection: "collection" in proposal ? proposal.collection : undefined,
137
+ documentID: target.documentID === undefined ? undefined : String(target.documentID),
138
+ proposal: redactSensitiveData(proposalForLog),
139
+ removals,
140
+ slug: "slug" in proposal ? proposal.slug : undefined,
141
+ targetType: proposal.action === "updateGlobal" ? "global" : "collection",
142
+ targetURL: getTargetURL({
143
+ documentID: target.documentID,
144
+ proposal,
145
+ req
146
+ }),
147
+ title: proposal.label,
148
+ userID: getUserID(req)
149
+ },
150
+ overrideAccess: true,
151
+ req
152
+ });
153
+ return {
154
+ action: proposal.action,
155
+ additions,
156
+ after,
157
+ before,
158
+ collection: "collection" in proposal ? proposal.collection : null,
159
+ documentID: target.documentID === undefined ? null : String(target.documentID),
160
+ removals,
161
+ slug: "slug" in proposal ? proposal.slug : null,
162
+ title: proposal.label,
163
+ url: getTargetURL({
164
+ documentID: target.documentID,
165
+ proposal,
166
+ req
167
+ })
168
+ };
169
+ } catch (err) {
170
+ req.payload.logger.error({
171
+ err,
172
+ msg: "AI change log entry could not be written",
173
+ proposal: getProposalMeta(proposal)
174
+ });
175
+ return null;
176
+ }
177
+ };
36
178
  const isActionProposal = (proposal)=>{
37
179
  if (!isRecord(proposal) || typeof proposal.label !== "string") return false;
38
180
  if (proposal.action === "create") {
@@ -87,13 +229,28 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
87
229
  });
88
230
  const globalConfig = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
89
231
  normalized = normalizeDataForFields(globalConfig?.fields || [], proposal.data);
232
+ const before = await req.payload.findGlobal({
233
+ depth: 2,
234
+ req,
235
+ slug: proposal.slug
236
+ });
90
237
  const doc = await req.payload.updateGlobal({
91
238
  data: normalized.data,
92
239
  overrideAccess: false,
93
240
  req,
94
241
  slug: proposal.slug
95
242
  });
243
+ const change = await logAIChange({
244
+ changeLogCollection: options.changeLogCollection,
245
+ proposal,
246
+ req,
247
+ target: {
248
+ after: mergeData(before, normalized.data),
249
+ before
250
+ }
251
+ });
96
252
  return Response.json({
253
+ change,
97
254
  doc: getAppliedDocReference(doc),
98
255
  status: "applied"
99
256
  });
@@ -110,7 +267,18 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
110
267
  overrideAccess: false,
111
268
  req
112
269
  });
270
+ const change = await logAIChange({
271
+ changeLogCollection: options.changeLogCollection,
272
+ proposal,
273
+ req,
274
+ target: {
275
+ after: {},
276
+ before: doc,
277
+ documentID: proposal.id
278
+ }
279
+ });
113
280
  return Response.json({
281
+ change,
114
282
  doc: getAppliedDocReference(doc),
115
283
  status: "applied"
116
284
  });
@@ -138,11 +306,28 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
138
306
  overrideAccess: false,
139
307
  req
140
308
  });
309
+ const change = await logAIChange({
310
+ changeLogCollection: options.changeLogCollection,
311
+ proposal,
312
+ req,
313
+ target: {
314
+ after: doc,
315
+ before: {},
316
+ documentID: doc.id
317
+ }
318
+ });
141
319
  return Response.json({
320
+ change,
142
321
  doc: getAppliedDocReference(doc),
143
322
  status: "applied"
144
323
  });
145
324
  }
325
+ const before = await req.payload.findByID({
326
+ collection: proposal.collection,
327
+ depth: 2,
328
+ id: proposal.id,
329
+ req
330
+ });
146
331
  const doc = await req.payload.update({
147
332
  collection: proposal.collection,
148
333
  data: normalized.data,
@@ -150,7 +335,18 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
150
335
  overrideAccess: false,
151
336
  req
152
337
  });
338
+ const change = await logAIChange({
339
+ changeLogCollection: options.changeLogCollection,
340
+ proposal,
341
+ req,
342
+ target: {
343
+ after: mergeData(before, normalized.data),
344
+ before,
345
+ documentID: proposal.id
346
+ }
347
+ });
153
348
  return Response.json({
349
+ change,
154
350
  doc: getAppliedDocReference(doc),
155
351
  status: "applied"
156
352
  });
@@ -1,11 +1,18 @@
1
- import { generateText, stepCountIs } from "ai";
1
+ import { streamText, stepCountIs } from "ai";
2
2
  import { z } from "zod";
3
3
  import { signAIActionProposal } from "../ai/proposals.js";
4
4
  import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
5
5
  import { isAIProvider } from "../ai/providerOptions.js";
6
6
  import { containsSensitiveData } from "../ai/sensitiveData.js";
7
- import { buildPromptWithMentionContext, collectBlocks, describeField, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
8
- import { getCollectionPermissions, isCollectionActionAllowed } from "../payload/collectionPermissions.js";
7
+ import { buildPromptWithMentionContext, collectBlocks, describeCollectionLikeConfig, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
8
+ import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
9
+ const maxProposalLabelLength = 90;
10
+ const getSafeProposalLabel = (label)=>{
11
+ const firstLine = label.replace(/[*_`>#-]/g, "").split("\n").map((line)=>line.trim()).find(Boolean);
12
+ if (!firstLine) return "Review proposed CMS change";
13
+ if (firstLine.length <= maxProposalLabelLength) return firstLine;
14
+ return `${firstLine.slice(0, maxProposalLabelLength - 3).trim()}...`;
15
+ };
9
16
  export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
10
17
  if (!req.user) return Response.json({
11
18
  error: "Unauthorized"
@@ -91,6 +98,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
91
98
  const mentionContext = await getMentionContext({
92
99
  blockContexts,
93
100
  collectionSlugs,
101
+ collections: options.collections,
94
102
  globalSlugs,
95
103
  mentions: body?.mentions,
96
104
  req
@@ -128,14 +136,10 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
128
136
  description: "List all Payload CMS collections available in this app.",
129
137
  inputSchema: z.object({}),
130
138
  execute: async ()=>{
131
- return allowedCollections.map((collection)=>({
132
- fields: collection.fields.map(describeField),
133
- label: collection.labels?.plural || collection.slug,
134
- permissions: getCollectionPermissions({
135
- permissions: options.collections,
136
- slug: collection.slug
137
- }),
138
- slug: collection.slug
139
+ return allowedCollections.map((collection)=>describeCollectionLikeConfig({
140
+ config: collection,
141
+ permissions: options.collections,
142
+ type: "collection"
139
143
  }));
140
144
  }
141
145
  },
@@ -161,10 +165,9 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
161
165
  description: "List all Payload CMS globals available in this app.",
162
166
  inputSchema: z.object({}),
163
167
  execute: async ()=>{
164
- return req.payload.config.globals?.map((global)=>({
165
- fields: global.fields.map(describeField),
166
- label: global.label || global.slug,
167
- slug: global.slug
168
+ return req.payload.config.globals?.map((global)=>describeCollectionLikeConfig({
169
+ config: global,
170
+ type: "global"
168
171
  })) || [];
169
172
  }
170
173
  },
@@ -182,7 +185,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
182
185
  action: "create",
183
186
  collection,
184
187
  data,
185
- label
188
+ label: getSafeProposalLabel(label)
186
189
  };
187
190
  return addSignedProposal(proposal);
188
191
  }
@@ -201,7 +204,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
201
204
  action: "delete",
202
205
  collection,
203
206
  id,
204
- label
207
+ label: getSafeProposalLabel(label)
205
208
  };
206
209
  return addSignedProposal(proposal);
207
210
  }
@@ -222,7 +225,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
222
225
  collection,
223
226
  data,
224
227
  id,
225
- label
228
+ label: getSafeProposalLabel(label)
226
229
  };
227
230
  return addSignedProposal(proposal);
228
231
  }
@@ -242,7 +245,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
242
245
  const proposal = {
243
246
  action: "updateGlobal",
244
247
  data,
245
- label,
248
+ label: getSafeProposalLabel(label),
246
249
  slug
247
250
  };
248
251
  return addSignedProposal(proposal);
@@ -280,7 +283,11 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
280
283
  }
281
284
  }
282
285
  };
283
- const result = await generateText({
286
+ const encoder = new TextEncoder();
287
+ const sendEvent = (controller, event, data)=>{
288
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
289
+ };
290
+ const result = streamText({
284
291
  maxOutputTokens: 700,
285
292
  model: getModel({
286
293
  apiKey: providerConfig.apiKey,
@@ -298,13 +305,67 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
298
305
  "When mention context is provided, use it as the selected CMS scope and prefer it over guessing collection names.",
299
306
  "Never claim that a write has been applied unless the user confirms an action proposal in the UI.",
300
307
  "For create, update, and delete requests, call the proposal tools instead of directly changing data.",
301
- "Keep the visible response under 80 words. Put concrete changes in proposal tool calls instead of long prose."
308
+ "Keep the visible response under 40 words and describe only what kind of change was proposed.",
309
+ "Write visible responses as plain text only. Do not use Markdown formatting, bold markers (**), headings, bullets, tables, or code fences.",
310
+ "Do not include proposed field values, full replacement text, quoted content, markdown sections, code blocks, or headings like 'New content'/'Neuer Inhalt' in the visible response.",
311
+ "Put all concrete field values and replacement content only in proposal tool data. Proposal labels must be short action summaries and must not contain proposed content."
302
312
  ].join("\n"),
303
313
  tools
304
314
  });
305
- return Response.json({
306
- proposals,
307
- text: result.text
315
+ const stream = new ReadableStream({
316
+ start: async (controller)=>{
317
+ let didSendTerminalEvent = false;
318
+ try {
319
+ for await (const part of result.fullStream){
320
+ if (part.type === "text-delta") {
321
+ sendEvent(controller, "text", {
322
+ delta: part.text
323
+ });
324
+ continue;
325
+ }
326
+ if (part.type === "error") {
327
+ didSendTerminalEvent = true;
328
+ sendEvent(controller, "error", {
329
+ error: "AI request failed."
330
+ });
331
+ break;
332
+ }
333
+ if (part.type === "finish") {
334
+ sendEvent(controller, "proposals", {
335
+ proposals
336
+ });
337
+ sendEvent(controller, "done", {});
338
+ didSendTerminalEvent = true;
339
+ }
340
+ }
341
+ if (!didSendTerminalEvent) {
342
+ sendEvent(controller, "proposals", {
343
+ proposals
344
+ });
345
+ sendEvent(controller, "done", {});
346
+ }
347
+ } catch (err) {
348
+ req.payload.logger.error({
349
+ debug,
350
+ err,
351
+ msg: "AI chat stream failed"
352
+ });
353
+ if (!didSendTerminalEvent) {
354
+ sendEvent(controller, "error", {
355
+ error: "AI request failed."
356
+ });
357
+ }
358
+ } finally{
359
+ controller.close();
360
+ }
361
+ }
362
+ });
363
+ return new Response(stream, {
364
+ headers: {
365
+ "Cache-Control": "no-cache, no-transform",
366
+ Connection: "keep-alive",
367
+ "Content-Type": "text/event-stream; charset=utf-8"
368
+ }
308
369
  });
309
370
  } catch (err) {
310
371
  req.payload.logger.error({
@@ -0,0 +1,7 @@
1
+ import type { PayloadHandler } from "payload";
2
+ import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
3
+ type AIProposalDiffEndpointOptions = {
4
+ collections?: ResolvedAICollectionPermissionMap;
5
+ };
6
+ export declare const createAIProposalDiffEndpointHandler: (options?: AIProposalDiffEndpointOptions) => PayloadHandler;
7
+ export {};