@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.
- package/dist/components/AIActionProposalList.d.ts +2 -1
- package/dist/components/AIActionProposalList.js +74 -6
- package/dist/components/AIActionProposalList.module.css +49 -5
- package/dist/components/AIInput.js +125 -10
- package/dist/components/AIInput.module.css +27 -6
- package/dist/components/DiffDialog.d.ts +12 -0
- package/dist/components/DiffDialog.js +310 -0
- package/dist/components/DiffDialog.module.css +249 -0
- package/dist/components/Icons.d.ts +4 -0
- package/dist/components/Icons.js +87 -0
- package/dist/components/RecentChangesList.d.ts +18 -0
- package/dist/components/RecentChangesList.js +66 -0
- package/dist/components/RecentChangesList.module.css +135 -0
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +1 -0
- package/dist/endpoints/aiApplyActionEndpointHandler.js +200 -4
- package/dist/endpoints/aiChatEndpointHandler.js +85 -24
- package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +7 -0
- package/dist/endpoints/aiProposalDiffEndpointHandler.js +138 -0
- package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +6 -0
- package/dist/endpoints/aiRecentChangesEndpointHandler.js +35 -0
- package/dist/index.js +111 -0
- package/dist/payload/schemaContext.d.ts +44 -1
- package/dist/payload/schemaContext.js +100 -12
- package/package.json +1 -1
|
@@ -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 {
|
|
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,
|
|
8
|
-
import {
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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 {};
|