@simmalugnt-se/payload-editor-assistant 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +29 -3
- package/dist/collections/approvals.js +3 -2
- package/dist/collections/audit.js +1 -1
- package/dist/components/ChangedFieldsHighlight.d.ts +8 -0
- package/dist/components/ChangedFieldsHighlight.js +50 -0
- package/dist/components/EditorAssistantAside.js +169 -63
- package/dist/components/EditorAssistantDocumentBridge.js +52 -1
- package/dist/components/EditorAssistantProvider.js +2 -1
- package/dist/components/assistant-context.d.ts +14 -0
- package/dist/components/assistant-context.js +4 -0
- package/dist/components/copy.d.ts +109 -0
- package/dist/components/copy.js +109 -0
- package/dist/components/selection-dom.d.ts +16 -0
- package/dist/components/selection-dom.js +104 -0
- package/dist/domain/propose.d.ts +7 -1
- package/dist/domain/propose.js +8 -3
- package/dist/endpoints/chat.js +2 -0
- package/dist/form/changed-paths.d.ts +6 -0
- package/dist/form/changed-paths.js +26 -0
- package/dist/form/diff.js +1 -22
- package/dist/form/empty.js +1 -19
- package/dist/form/lexical.d.ts +8 -0
- package/dist/form/lexical.js +24 -0
- package/dist/runtime/agent.d.ts +4 -1
- package/dist/runtime/agent.js +21 -8
- package/dist/runtime/context.d.ts +3 -0
- package/dist/runtime/context.js +10 -3
- package/dist/runtime/propose-loop.d.ts +9 -0
- package/dist/runtime/propose-loop.js +13 -1
- package/dist/runtime/selection.d.ts +28 -0
- package/dist/runtime/selection.js +87 -0
- package/dist/runtime/system-prompt.js +17 -1
- package/dist/runtime/tools.d.ts +7 -0
- package/dist/runtime/tools.js +2 -1
- package/dist/runtime/voice.js +2 -2
- package/dist/schema/fields.d.ts +1 -0
- package/dist/schema/fields.js +1 -0
- package/dist/styles/assistant.css +210 -181
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ export function AssistantStateProvider({ children, shortcuts, collections, }) {
|
|
|
11
11
|
const [formAdapter, setFormAdapter] = useState(null);
|
|
12
12
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
13
13
|
const [abortController, setAbortController] = useState(null);
|
|
14
|
+
const [selection, setSelection] = useState(null);
|
|
14
15
|
const stop = useCallback(() => {
|
|
15
16
|
abortController?.abort();
|
|
16
17
|
setAbortController(null);
|
|
@@ -33,11 +34,13 @@ export function AssistantStateProvider({ children, shortcuts, collections, }) {
|
|
|
33
34
|
bridge,
|
|
34
35
|
formAdapter,
|
|
35
36
|
pendingApproval,
|
|
37
|
+
selection,
|
|
36
38
|
setOpen,
|
|
37
39
|
setCollapsed,
|
|
38
40
|
setBridge,
|
|
39
41
|
setFormAdapter,
|
|
40
42
|
setPendingApproval,
|
|
43
|
+
setSelection,
|
|
41
44
|
setStatus,
|
|
42
45
|
setMessages,
|
|
43
46
|
resetConversation,
|
|
@@ -54,6 +57,7 @@ export function AssistantStateProvider({ children, shortcuts, collections, }) {
|
|
|
54
57
|
bridge,
|
|
55
58
|
formAdapter,
|
|
56
59
|
pendingApproval,
|
|
60
|
+
selection,
|
|
57
61
|
resetConversation,
|
|
58
62
|
stop,
|
|
59
63
|
abortController,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything the panel says itself, in Admin's language. The assistant's replies follow the language
|
|
3
|
+
* the editor chats in instead; those come from the model.
|
|
4
|
+
*/
|
|
5
|
+
export type UiLanguage = "sv" | "en";
|
|
6
|
+
export declare function uiLanguage(adminLanguage: string | undefined): UiLanguage;
|
|
7
|
+
export declare const COPY: {
|
|
8
|
+
readonly en: {
|
|
9
|
+
readonly assistant: "Assistant";
|
|
10
|
+
readonly newChat: "New chat";
|
|
11
|
+
readonly collapse: "Collapse";
|
|
12
|
+
readonly collapseLabel: "Collapse assistant";
|
|
13
|
+
readonly placeholder: "Message the assistant";
|
|
14
|
+
readonly send: "Send";
|
|
15
|
+
readonly stop: "Stop";
|
|
16
|
+
readonly working: "Working…";
|
|
17
|
+
readonly selected: "Selected";
|
|
18
|
+
readonly clearSelection: "Clear selection";
|
|
19
|
+
readonly approve: "Approve";
|
|
20
|
+
readonly reject: "Reject";
|
|
21
|
+
readonly keep: "Keep";
|
|
22
|
+
readonly undo: "Undo";
|
|
23
|
+
readonly undoAnyway: "Undo anyway";
|
|
24
|
+
readonly cancel: "Cancel";
|
|
25
|
+
readonly previewHint: "Shown in the form and the preview. Nothing is saved yet.";
|
|
26
|
+
readonly previewHintAutosave: "Shown in the form and the preview. The draft saves itself, but nothing is published.";
|
|
27
|
+
readonly undoLosesEdits: "You edited the form after the suggestion. Undo removes those edits too.";
|
|
28
|
+
readonly requestFailed: "I could not complete that request.";
|
|
29
|
+
readonly done: "Done.";
|
|
30
|
+
readonly proposalFailed: "The proposal could not be applied.";
|
|
31
|
+
readonly rejectedForm: "Rejected. The form was not changed.";
|
|
32
|
+
readonly rejectedDraft: "Rejected. No draft was created.";
|
|
33
|
+
readonly applied: "Applied to the unsaved form. Save in Payload when you are ready.";
|
|
34
|
+
readonly applyFailed: "The form could not be updated. It was left unchanged.";
|
|
35
|
+
readonly draftCreated: "The draft is created and open.";
|
|
36
|
+
readonly kept: "Kept. Save in Payload when you are ready.";
|
|
37
|
+
readonly keptAutosave: "Kept. The draft is saved; publish when you are ready.";
|
|
38
|
+
readonly undone: "Undone. The form is back as it was.";
|
|
39
|
+
readonly keepFailed: "The change could not be confirmed, so it was taken out of the form.";
|
|
40
|
+
readonly status: {
|
|
41
|
+
readonly unavailable: "The assistant is not available for your account.";
|
|
42
|
+
readonly unauthorized: "Your session has ended. Reload the page and sign in again.";
|
|
43
|
+
readonly rate_limited: "Too many messages at once. Wait a moment and try again.";
|
|
44
|
+
readonly timeout: "The assistant took too long to answer. Try again.";
|
|
45
|
+
readonly provider_error: "The AI service did not answer. The server log has the cause.";
|
|
46
|
+
readonly tool_denied: "That is not something the assistant may do here.";
|
|
47
|
+
readonly stale_context: "The form changed in the meantime. Try again.";
|
|
48
|
+
readonly aborted: "Stopped.";
|
|
49
|
+
};
|
|
50
|
+
readonly intro: {
|
|
51
|
+
readonly document: (name?: string) => string;
|
|
52
|
+
readonly notAllowed: "I can't work with this list, but I can help you find content, open a page, or start a new draft.";
|
|
53
|
+
readonly list: (plural: string, singular: string) => string;
|
|
54
|
+
readonly general: "I can help you find content, open a page, or start a new draft. What do you need?";
|
|
55
|
+
readonly documentFallback: "document";
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
readonly sv: {
|
|
59
|
+
readonly assistant: "Assistent";
|
|
60
|
+
readonly newChat: "Ny chatt";
|
|
61
|
+
readonly collapse: "Fäll ihop";
|
|
62
|
+
readonly collapseLabel: "Fäll ihop assistenten";
|
|
63
|
+
readonly placeholder: "Skriv till assistenten";
|
|
64
|
+
readonly send: "Skicka";
|
|
65
|
+
readonly stop: "Stoppa";
|
|
66
|
+
readonly working: "Arbetar…";
|
|
67
|
+
readonly selected: "Markerat";
|
|
68
|
+
readonly clearSelection: "Ta bort markering";
|
|
69
|
+
readonly approve: "Godkänn";
|
|
70
|
+
readonly reject: "Avvisa";
|
|
71
|
+
readonly keep: "Behåll";
|
|
72
|
+
readonly undo: "Ångra";
|
|
73
|
+
readonly undoAnyway: "Ångra ändå";
|
|
74
|
+
readonly cancel: "Avbryt";
|
|
75
|
+
readonly previewHint: "Syns i formuläret och förhandsvisningen. Inget är sparat än.";
|
|
76
|
+
readonly previewHintAutosave: "Syns i formuläret och förhandsvisningen. Utkastet sparas automatiskt, men inget publiceras.";
|
|
77
|
+
readonly undoLosesEdits: "Du har ändrat i formuläret efter förslaget. Ångra tar bort de ändringarna också.";
|
|
78
|
+
readonly requestFailed: "Jag kunde inte slutföra det.";
|
|
79
|
+
readonly done: "Klart.";
|
|
80
|
+
readonly proposalFailed: "Förslaget kunde inte genomföras.";
|
|
81
|
+
readonly rejectedForm: "Avvisat. Formuläret ändrades inte.";
|
|
82
|
+
readonly rejectedDraft: "Avvisat. Inget utkast skapades.";
|
|
83
|
+
readonly applied: "Ändringen är införd i formuläret men inte sparad. Spara i Payload när du är redo.";
|
|
84
|
+
readonly applyFailed: "Formuläret kunde inte uppdateras och lämnades orört.";
|
|
85
|
+
readonly draftCreated: "Utkastet är skapat och öppnat.";
|
|
86
|
+
readonly kept: "Behållet. Spara i Payload när du är redo.";
|
|
87
|
+
readonly keptAutosave: "Behållet. Utkastet är sparat; publicera när du är redo.";
|
|
88
|
+
readonly undone: "Ångrat. Formuläret är som innan.";
|
|
89
|
+
readonly keepFailed: "Ändringen kunde inte bekräftas, så den togs bort ur formuläret.";
|
|
90
|
+
readonly status: {
|
|
91
|
+
readonly unavailable: "Assistenten är inte tillgänglig för ditt konto.";
|
|
92
|
+
readonly unauthorized: "Din session har gått ut. Ladda om sidan och logga in igen.";
|
|
93
|
+
readonly rate_limited: "För många meddelanden på en gång. Vänta en stund och försök igen.";
|
|
94
|
+
readonly timeout: "Assistenten tog för lång tid på sig. Försök igen.";
|
|
95
|
+
readonly provider_error: "AI-tjänsten svarade inte. Orsaken finns i serverloggen.";
|
|
96
|
+
readonly tool_denied: "Det får assistenten inte göra här.";
|
|
97
|
+
readonly stale_context: "Formuläret ändrades under tiden. Försök igen.";
|
|
98
|
+
readonly aborted: "Stoppat.";
|
|
99
|
+
};
|
|
100
|
+
readonly intro: {
|
|
101
|
+
readonly document: (name?: string) => string;
|
|
102
|
+
readonly notAllowed: "Den här listan kan jag inte arbeta med, men jag kan hjälpa dig hitta innehåll, öppna en sida eller skapa ett utkast.";
|
|
103
|
+
readonly list: (plural: string, singular: string) => string;
|
|
104
|
+
readonly general: "Jag kan hjälpa dig hitta innehåll, öppna en sida eller skapa ett utkast. Vad behöver du?";
|
|
105
|
+
readonly documentFallback: "dokument";
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
export type Copy = (typeof COPY)[UiLanguage];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything the panel says itself, in Admin's language. The assistant's replies follow the language
|
|
3
|
+
* the editor chats in instead; those come from the model.
|
|
4
|
+
*/
|
|
5
|
+
export function uiLanguage(adminLanguage) {
|
|
6
|
+
return adminLanguage?.startsWith("sv") ? "sv" : "en";
|
|
7
|
+
}
|
|
8
|
+
export const COPY = {
|
|
9
|
+
en: {
|
|
10
|
+
assistant: "Assistant",
|
|
11
|
+
newChat: "New chat",
|
|
12
|
+
collapse: "Collapse",
|
|
13
|
+
collapseLabel: "Collapse assistant",
|
|
14
|
+
placeholder: "Message the assistant",
|
|
15
|
+
send: "Send",
|
|
16
|
+
stop: "Stop",
|
|
17
|
+
working: "Working…",
|
|
18
|
+
selected: "Selected",
|
|
19
|
+
clearSelection: "Clear selection",
|
|
20
|
+
approve: "Approve",
|
|
21
|
+
reject: "Reject",
|
|
22
|
+
keep: "Keep",
|
|
23
|
+
undo: "Undo",
|
|
24
|
+
undoAnyway: "Undo anyway",
|
|
25
|
+
cancel: "Cancel",
|
|
26
|
+
previewHint: "Shown in the form and the preview. Nothing is saved yet.",
|
|
27
|
+
previewHintAutosave: "Shown in the form and the preview. The draft saves itself, but nothing is published.",
|
|
28
|
+
undoLosesEdits: "You edited the form after the suggestion. Undo removes those edits too.",
|
|
29
|
+
requestFailed: "I could not complete that request.",
|
|
30
|
+
done: "Done.",
|
|
31
|
+
proposalFailed: "The proposal could not be applied.",
|
|
32
|
+
rejectedForm: "Rejected. The form was not changed.",
|
|
33
|
+
rejectedDraft: "Rejected. No draft was created.",
|
|
34
|
+
applied: "Applied to the unsaved form. Save in Payload when you are ready.",
|
|
35
|
+
applyFailed: "The form could not be updated. It was left unchanged.",
|
|
36
|
+
draftCreated: "The draft is created and open.",
|
|
37
|
+
kept: "Kept. Save in Payload when you are ready.",
|
|
38
|
+
keptAutosave: "Kept. The draft is saved; publish when you are ready.",
|
|
39
|
+
undone: "Undone. The form is back as it was.",
|
|
40
|
+
keepFailed: "The change could not be confirmed, so it was taken out of the form.",
|
|
41
|
+
status: {
|
|
42
|
+
unavailable: "The assistant is not available for your account.",
|
|
43
|
+
unauthorized: "Your session has ended. Reload the page and sign in again.",
|
|
44
|
+
rate_limited: "Too many messages at once. Wait a moment and try again.",
|
|
45
|
+
timeout: "The assistant took too long to answer. Try again.",
|
|
46
|
+
provider_error: "The AI service did not answer. The server log has the cause.",
|
|
47
|
+
tool_denied: "That is not something the assistant may do here.",
|
|
48
|
+
stale_context: "The form changed in the meantime. Try again.",
|
|
49
|
+
aborted: "Stopped.",
|
|
50
|
+
},
|
|
51
|
+
intro: {
|
|
52
|
+
document: (name) => `I can explain the fields on ${name ? `"${name}"` : "this document"}, suggest edits, or fill in empty fields. Click a field or block to work on just that.`,
|
|
53
|
+
notAllowed: "I can't work with this list, but I can help you find content, open a page, or start a new draft.",
|
|
54
|
+
list: (plural, singular) => `You are in ${plural}. I can find and open a ${singular}, or start a new draft for you.`,
|
|
55
|
+
general: "I can help you find content, open a page, or start a new draft. What do you need?",
|
|
56
|
+
documentFallback: "document",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
sv: {
|
|
60
|
+
assistant: "Assistent",
|
|
61
|
+
newChat: "Ny chatt",
|
|
62
|
+
collapse: "Fäll ihop",
|
|
63
|
+
collapseLabel: "Fäll ihop assistenten",
|
|
64
|
+
placeholder: "Skriv till assistenten",
|
|
65
|
+
send: "Skicka",
|
|
66
|
+
stop: "Stoppa",
|
|
67
|
+
working: "Arbetar…",
|
|
68
|
+
selected: "Markerat",
|
|
69
|
+
clearSelection: "Ta bort markering",
|
|
70
|
+
approve: "Godkänn",
|
|
71
|
+
reject: "Avvisa",
|
|
72
|
+
keep: "Behåll",
|
|
73
|
+
undo: "Ångra",
|
|
74
|
+
undoAnyway: "Ångra ändå",
|
|
75
|
+
cancel: "Avbryt",
|
|
76
|
+
previewHint: "Syns i formuläret och förhandsvisningen. Inget är sparat än.",
|
|
77
|
+
previewHintAutosave: "Syns i formuläret och förhandsvisningen. Utkastet sparas automatiskt, men inget publiceras.",
|
|
78
|
+
undoLosesEdits: "Du har ändrat i formuläret efter förslaget. Ångra tar bort de ändringarna också.",
|
|
79
|
+
requestFailed: "Jag kunde inte slutföra det.",
|
|
80
|
+
done: "Klart.",
|
|
81
|
+
proposalFailed: "Förslaget kunde inte genomföras.",
|
|
82
|
+
rejectedForm: "Avvisat. Formuläret ändrades inte.",
|
|
83
|
+
rejectedDraft: "Avvisat. Inget utkast skapades.",
|
|
84
|
+
applied: "Ändringen är införd i formuläret men inte sparad. Spara i Payload när du är redo.",
|
|
85
|
+
applyFailed: "Formuläret kunde inte uppdateras och lämnades orört.",
|
|
86
|
+
draftCreated: "Utkastet är skapat och öppnat.",
|
|
87
|
+
kept: "Behållet. Spara i Payload när du är redo.",
|
|
88
|
+
keptAutosave: "Behållet. Utkastet är sparat; publicera när du är redo.",
|
|
89
|
+
undone: "Ångrat. Formuläret är som innan.",
|
|
90
|
+
keepFailed: "Ändringen kunde inte bekräftas, så den togs bort ur formuläret.",
|
|
91
|
+
status: {
|
|
92
|
+
unavailable: "Assistenten är inte tillgänglig för ditt konto.",
|
|
93
|
+
unauthorized: "Din session har gått ut. Ladda om sidan och logga in igen.",
|
|
94
|
+
rate_limited: "För många meddelanden på en gång. Vänta en stund och försök igen.",
|
|
95
|
+
timeout: "Assistenten tog för lång tid på sig. Försök igen.",
|
|
96
|
+
provider_error: "AI-tjänsten svarade inte. Orsaken finns i serverloggen.",
|
|
97
|
+
tool_denied: "Det får assistenten inte göra här.",
|
|
98
|
+
stale_context: "Formuläret ändrades under tiden. Försök igen.",
|
|
99
|
+
aborted: "Stoppat.",
|
|
100
|
+
},
|
|
101
|
+
intro: {
|
|
102
|
+
document: (name) => `Jag kan förklara fälten på ${name ? `"${name}"` : "det här dokumentet"}, föreslå ändringar eller fylla i tomma fält. Klicka på ett fält eller block för att jobba med just det.`,
|
|
103
|
+
notAllowed: "Den här listan kan jag inte arbeta med, men jag kan hjälpa dig hitta innehåll, öppna en sida eller skapa ett utkast.",
|
|
104
|
+
list: (plural, singular) => `Du är i ${plural}. Jag kan hitta och öppna en ${singular}, eller skapa ett nytt utkast åt dig.`,
|
|
105
|
+
general: "Jag kan hjälpa dig hitta innehåll, öppna en sida eller skapa ett utkast. Vad behöver du?",
|
|
106
|
+
documentFallback: "dokument",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads what the editor clicked or focused in Payload's edit form from the DOM Payload renders.
|
|
3
|
+
* Inputs carry `field-…` ids (`field-layout__0__headline`), rich text wrappers `data-field-path`,
|
|
4
|
+
* and block and array rows, header included, `…-row-N` ids (`layout-0-items-row-1`).
|
|
5
|
+
*/
|
|
6
|
+
/** Visual editing announces preview clicks with this event: `detail: { path }`, `path: null` when cleared. */
|
|
7
|
+
export declare const VISUAL_EDITING_SELECT_EVENT = "visual-editing:select";
|
|
8
|
+
/** Visual editing keeps its preview outline when the editor clicks inside this attribute. */
|
|
9
|
+
export declare const KEEP_SELECTION_ATTR = "data-visual-editing-keep-selection";
|
|
10
|
+
export type DomSelection = {
|
|
11
|
+
path: string;
|
|
12
|
+
label: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function selectionFromElement(element: Element): DomSelection | undefined;
|
|
15
|
+
/** Label for a path announced by visual editing, read from the rendered form when it is there. */
|
|
16
|
+
export declare function selectionFromPath(path: string): DomSelection;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads what the editor clicked or focused in Payload's edit form from the DOM Payload renders.
|
|
3
|
+
* Inputs carry `field-…` ids (`field-layout__0__headline`), rich text wrappers `data-field-path`,
|
|
4
|
+
* and block and array rows, header included, `…-row-N` ids (`layout-0-items-row-1`).
|
|
5
|
+
*/
|
|
6
|
+
/** Visual editing announces preview clicks with this event: `detail: { path }`, `path: null` when cleared. */
|
|
7
|
+
export const VISUAL_EDITING_SELECT_EVENT = "visual-editing:select";
|
|
8
|
+
/** Visual editing keeps its preview outline when the editor clicks inside this attribute. */
|
|
9
|
+
export const KEEP_SELECTION_ATTR = "data-visual-editing-keep-selection";
|
|
10
|
+
export function selectionFromElement(element) {
|
|
11
|
+
const field = element.closest("[data-field-path], [id^='field-']");
|
|
12
|
+
const row = closestRow(element);
|
|
13
|
+
// The blocks field itself carries a `field-…` id, so a row header sits inside a "field";
|
|
14
|
+
// whichever is innermost is what was clicked.
|
|
15
|
+
if (row && (!field || field.contains(row.element))) {
|
|
16
|
+
return { path: row.path, label: rowTrail(row.element) ?? humanPath(row.path) };
|
|
17
|
+
}
|
|
18
|
+
if (!field) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const path = field.getAttribute("data-field-path") ?? pathFromFieldId(field.id);
|
|
22
|
+
return path ? { path, label: fieldLabel(field, path) } : undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Label for a path announced by visual editing, read from the rendered form when it is there. */
|
|
25
|
+
export function selectionFromPath(path) {
|
|
26
|
+
const field = document.getElementById(`field-${path.replace(/\./g, "__")}`) ??
|
|
27
|
+
document.querySelector(`[data-field-path="${CSS.escape(path)}"]`);
|
|
28
|
+
if (field) {
|
|
29
|
+
return { path, label: fieldLabel(field, path) };
|
|
30
|
+
}
|
|
31
|
+
const row = document.getElementById(rowId(path));
|
|
32
|
+
return { path, label: (row && rowTrail(row)) ?? humanPath(path) };
|
|
33
|
+
}
|
|
34
|
+
function closestRow(element) {
|
|
35
|
+
let row = element.closest("[id*='-row-']");
|
|
36
|
+
while (row) {
|
|
37
|
+
const path = pathFromRowId(row.id);
|
|
38
|
+
if (path) {
|
|
39
|
+
return { element: row, path };
|
|
40
|
+
}
|
|
41
|
+
row = row.parentElement?.closest("[id*='-row-']") ?? null;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function pathFromFieldId(id) {
|
|
46
|
+
return id.startsWith("field-") && id.length > "field-".length
|
|
47
|
+
? id.slice("field-".length).replace(/__/g, ".")
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
function pathFromRowId(id) {
|
|
51
|
+
const match = /^(.+)-row-(\d+)$/.exec(id);
|
|
52
|
+
return match ? `${match[1]?.split("-").join(".")}.${match[2]}` : undefined;
|
|
53
|
+
}
|
|
54
|
+
function rowId(path) {
|
|
55
|
+
const segments = path.split(".");
|
|
56
|
+
const index = segments.pop();
|
|
57
|
+
return `${segments.join("-")}-row-${index}`;
|
|
58
|
+
}
|
|
59
|
+
function fieldLabel(field, path) {
|
|
60
|
+
const own = (field.id && document.querySelector(`label[for="${CSS.escape(field.id)}"]`)) ||
|
|
61
|
+
field.closest(".field-type")?.querySelector(".field-label");
|
|
62
|
+
const name = text(own) ?? humanPath(path.split(".").at(-1) ?? path);
|
|
63
|
+
const row = closestRow(field);
|
|
64
|
+
const parent = row && rowTrail(row.element);
|
|
65
|
+
return parent ? `${parent} › ${name}` : name;
|
|
66
|
+
}
|
|
67
|
+
/** "Cards 03 › Item 01": the row's label after those of the rows it is nested in. */
|
|
68
|
+
function rowTrail(row) {
|
|
69
|
+
const own = rowLabel(row);
|
|
70
|
+
const outer = row.parentElement && closestRow(row.parentElement);
|
|
71
|
+
const parent = outer && rowTrail(outer.element);
|
|
72
|
+
return parent && own ? `${parent} › ${own}` : own;
|
|
73
|
+
}
|
|
74
|
+
/** Block rows show the block's label in a pill, array rows a row label, both with a number. */
|
|
75
|
+
function rowLabel(row) {
|
|
76
|
+
// `:scope >` keeps a nested row's header from answering for its parent.
|
|
77
|
+
const header = row.querySelector(":scope > .collapsible > .collapsible__toggle-wrap") ??
|
|
78
|
+
row.querySelector(".collapsible__toggle-wrap") ??
|
|
79
|
+
row;
|
|
80
|
+
const pill = text(header.querySelector(".blocks-field__block-pill"));
|
|
81
|
+
const number = text(header.querySelector(".blocks-field__block-number"));
|
|
82
|
+
if (pill) {
|
|
83
|
+
return number ? `${pill} ${number}` : pill;
|
|
84
|
+
}
|
|
85
|
+
return text(header.querySelector(".row-label"));
|
|
86
|
+
}
|
|
87
|
+
/** Visible label text without Payload's markers: the required asterisk and the locale ("— sv"). */
|
|
88
|
+
function text(element) {
|
|
89
|
+
if (!element) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const copy = element.cloneNode(true);
|
|
93
|
+
for (const marker of copy.querySelectorAll(".required, .localized")) {
|
|
94
|
+
marker.remove();
|
|
95
|
+
}
|
|
96
|
+
const value = copy.textContent?.replace(/\s+/g, " ").trim();
|
|
97
|
+
return value || undefined;
|
|
98
|
+
}
|
|
99
|
+
function humanPath(path) {
|
|
100
|
+
return path
|
|
101
|
+
.split(".")
|
|
102
|
+
.map((segment) => (/^\d+$/.test(segment) ? String(Number(segment) + 1) : segment))
|
|
103
|
+
.join(" › ");
|
|
104
|
+
}
|
package/dist/domain/propose.d.ts
CHANGED
|
@@ -6,11 +6,16 @@ export type ProposeInput = {
|
|
|
6
6
|
global?: string;
|
|
7
7
|
documentId?: string | number;
|
|
8
8
|
locale: string;
|
|
9
|
-
/**
|
|
9
|
+
/** Language of the panel's title and change list: Admin's, else the chat's. */
|
|
10
10
|
language?: "sv" | "en";
|
|
11
11
|
operations: unknown;
|
|
12
12
|
formData?: Record<string, unknown>;
|
|
13
13
|
};
|
|
14
|
+
/** The form as it would be with a proposal applied, so Admin can show it before it is kept. */
|
|
15
|
+
export type ProposalPreview = {
|
|
16
|
+
data: Record<string, unknown>;
|
|
17
|
+
changedPaths: string[];
|
|
18
|
+
};
|
|
14
19
|
export type ProposeResult = {
|
|
15
20
|
ok: true;
|
|
16
21
|
approvalId: string;
|
|
@@ -18,6 +23,7 @@ export type ProposeResult = {
|
|
|
18
23
|
expiresAt: string;
|
|
19
24
|
summary: string;
|
|
20
25
|
title: string;
|
|
26
|
+
preview?: ProposalPreview;
|
|
21
27
|
} | {
|
|
22
28
|
ok: false;
|
|
23
29
|
error: "tool_denied" | "stale_context";
|
package/dist/domain/propose.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { changedPaths } from "../form/changed-paths.js";
|
|
1
2
|
import { compileOperations } from "../form/compile.js";
|
|
2
3
|
import { humanDiff } from "../form/diff.js";
|
|
3
4
|
import { hashFormRevision } from "../form/hash.js";
|
|
@@ -10,7 +11,8 @@ import { writeAudit } from "./audit.js";
|
|
|
10
11
|
import { findContentById } from "./content.js";
|
|
11
12
|
import { localApi } from "./local-api.js";
|
|
12
13
|
import { verifyDocumentRelations } from "./relations.js";
|
|
13
|
-
|
|
14
|
+
/** Long enough to look at a change in the preview before keeping it. */
|
|
15
|
+
const TTL_MS = 30 * 60_000;
|
|
14
16
|
export async function proposeChange(req, options, input) {
|
|
15
17
|
const resolved = resolveAllowlist(options, input);
|
|
16
18
|
if (!resolved || !hasCapability(resolved.allowlist, input.capability)) {
|
|
@@ -108,13 +110,16 @@ export async function proposeChange(req, options, input) {
|
|
|
108
110
|
expiresAt,
|
|
109
111
|
title: approvalTitle(input.capability, input.language),
|
|
110
112
|
summary: diff.map((line) => line.text).join("\n"),
|
|
113
|
+
preview: input.capability === "form.propose"
|
|
114
|
+
? { data: finalized.data, changedPaths: changedPaths(typedOperations) }
|
|
115
|
+
: undefined,
|
|
111
116
|
};
|
|
112
117
|
}
|
|
113
118
|
function approvalTitle(capability, language) {
|
|
114
119
|
if (language === "sv") {
|
|
115
|
-
return capability === "form.propose" ? "
|
|
120
|
+
return capability === "form.propose" ? "Förslag i formuläret" : "Nytt utkast att godkänna";
|
|
116
121
|
}
|
|
117
|
-
return capability === "form.propose" ? "
|
|
122
|
+
return capability === "form.propose" ? "Suggested change" : "Pending draft create";
|
|
118
123
|
}
|
|
119
124
|
async function resolveCurrentForm(req, options, input) {
|
|
120
125
|
const snapshot = input.formData && typeof input.formData === "object" ? input.formData : {};
|
package/dist/endpoints/chat.js
CHANGED
|
@@ -55,7 +55,9 @@ export function createChatEndpoint(options) {
|
|
|
55
55
|
title: typeof body.bridge?.title === "string" ? body.bridge.title : undefined,
|
|
56
56
|
locale,
|
|
57
57
|
language,
|
|
58
|
+
uiLanguage: nonEmpty(body.uiLanguage)?.startsWith("sv") ? "sv" : "en",
|
|
58
59
|
view: sanitizeAdminView(body.view, options),
|
|
60
|
+
selection: typeof body.selection === "string" ? body.selection : undefined,
|
|
59
61
|
},
|
|
60
62
|
formData: body.formData,
|
|
61
63
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FormOperation } from "./operations.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Form paths a proposal touches, for highlighting them in the form: the field that is set, the row
|
|
4
|
+
* that is inserted, replaced or moved, and the list itself when a row is removed.
|
|
5
|
+
*/
|
|
6
|
+
export declare function changedPaths(operations: FormOperation[]): string[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form paths a proposal touches, for highlighting them in the form: the field that is set, the row
|
|
3
|
+
* that is inserted, replaced or moved, and the list itself when a row is removed.
|
|
4
|
+
*/
|
|
5
|
+
export function changedPaths(operations) {
|
|
6
|
+
const paths = operations.map((operation) => {
|
|
7
|
+
switch (operation.op) {
|
|
8
|
+
case "set":
|
|
9
|
+
case "unset":
|
|
10
|
+
case "array.remove":
|
|
11
|
+
case "blocks.remove":
|
|
12
|
+
return operation.path;
|
|
13
|
+
case "array.insert":
|
|
14
|
+
case "array.replace":
|
|
15
|
+
case "blocks.insert":
|
|
16
|
+
case "blocks.replace":
|
|
17
|
+
return `${operation.path}.${operation.index}`;
|
|
18
|
+
case "array.move":
|
|
19
|
+
case "blocks.move":
|
|
20
|
+
return `${operation.path}.${operation.to}`;
|
|
21
|
+
default:
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return [...new Set(paths.filter((path) => Boolean(path)))];
|
|
26
|
+
}
|
package/dist/form/diff.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isLexical, lexicalPlaintext } from "./lexical.js";
|
|
1
2
|
import { getAtPath, parseFieldPath } from "./path.js";
|
|
2
3
|
const COPY = {
|
|
3
4
|
en: {
|
|
@@ -128,28 +129,6 @@ function preview(value, copy, max = HEADER_PREVIEW) {
|
|
|
128
129
|
}
|
|
129
130
|
return truncate(JSON.stringify(value), max);
|
|
130
131
|
}
|
|
131
|
-
function isLexical(value) {
|
|
132
|
-
return Boolean(value && typeof value === "object" && "root" in value);
|
|
133
|
-
}
|
|
134
|
-
function lexicalPlaintext(value) {
|
|
135
|
-
const texts = [];
|
|
136
|
-
const visit = (node) => {
|
|
137
|
-
if (!node || typeof node !== "object") {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const record = node;
|
|
141
|
-
if (typeof record.text === "string") {
|
|
142
|
-
texts.push(record.text);
|
|
143
|
-
}
|
|
144
|
-
if (Array.isArray(record.children)) {
|
|
145
|
-
for (const child of record.children) {
|
|
146
|
-
visit(child);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
visit(value.root);
|
|
151
|
-
return texts.join(" ").trim();
|
|
152
|
-
}
|
|
153
132
|
function truncate(value, max) {
|
|
154
133
|
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
|
155
134
|
}
|
package/dist/form/empty.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isDeniedFieldName } from "../config/denied-fields.js";
|
|
2
|
+
import { lexicalPlaintext } from "./lexical.js";
|
|
2
3
|
import { getAtPath, parseFieldPath } from "./path.js";
|
|
3
4
|
const SKIP = new Set(["id", "slug", "blockType", "_status", "parent"]);
|
|
4
5
|
const MAX_PATHS = 12;
|
|
@@ -55,25 +56,6 @@ function walk(value, path, into) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
|
-
function lexicalPlaintext(value) {
|
|
59
|
-
const texts = [];
|
|
60
|
-
const visit = (node) => {
|
|
61
|
-
if (!node || typeof node !== "object") {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const record = node;
|
|
65
|
-
if (typeof record.text === "string") {
|
|
66
|
-
texts.push(record.text);
|
|
67
|
-
}
|
|
68
|
-
if (Array.isArray(record.children)) {
|
|
69
|
-
for (const child of record.children) {
|
|
70
|
-
visit(child);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
visit(value.root);
|
|
75
|
-
return texts.join(" ").trim();
|
|
76
|
-
}
|
|
77
59
|
function truncate(value) {
|
|
78
60
|
return value.length > 80 ? `${value.slice(0, 77)}…` : value;
|
|
79
61
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Lexical rich text as stored by Payload: `{ root: { children: [...] } }`. */
|
|
2
|
+
export declare function isLexical(value: unknown): value is {
|
|
3
|
+
root: unknown;
|
|
4
|
+
};
|
|
5
|
+
/** The text of every node, paragraphs joined by spaces. */
|
|
6
|
+
export declare function lexicalPlaintext(value: {
|
|
7
|
+
root: unknown;
|
|
8
|
+
}): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Lexical rich text as stored by Payload: `{ root: { children: [...] } }`. */
|
|
2
|
+
export function isLexical(value) {
|
|
3
|
+
return Boolean(value && typeof value === "object" && "root" in value);
|
|
4
|
+
}
|
|
5
|
+
/** The text of every node, paragraphs joined by spaces. */
|
|
6
|
+
export function lexicalPlaintext(value) {
|
|
7
|
+
const texts = [];
|
|
8
|
+
const visit = (node) => {
|
|
9
|
+
if (!node || typeof node !== "object") {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const record = node;
|
|
13
|
+
if (typeof record.text === "string") {
|
|
14
|
+
texts.push(record.text);
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(record.children)) {
|
|
17
|
+
for (const child of record.children) {
|
|
18
|
+
visit(child);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
visit(value.root);
|
|
23
|
+
return texts.join(" ").trim();
|
|
24
|
+
}
|
package/dist/runtime/agent.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PayloadRequest } from "payload";
|
|
2
2
|
import type { ValidatedEditorAssistantOptions } from "../config/validate.ts";
|
|
3
|
+
import type { ProposalPreview } from "../domain/propose.ts";
|
|
3
4
|
import { type ToolContext, type ToolOutcome } from "./tools.ts";
|
|
4
5
|
export type AgentChatResult = {
|
|
5
6
|
ok: true;
|
|
@@ -10,6 +11,8 @@ export type AgentChatResult = {
|
|
|
10
11
|
approvalId: string;
|
|
11
12
|
nonce: string;
|
|
12
13
|
capability: "form.propose" | "draft.create";
|
|
14
|
+
/** The form with the change applied, shown in Admin before the editor keeps it. */
|
|
15
|
+
preview?: ProposalPreview;
|
|
13
16
|
};
|
|
14
17
|
navigate?: {
|
|
15
18
|
href: string;
|
|
@@ -38,5 +41,5 @@ export declare function runAgentTurn(req: PayloadRequest, options: ValidatedEdit
|
|
|
38
41
|
export declare function collectResult(text: string, result: {
|
|
39
42
|
steps?: unknown;
|
|
40
43
|
toolResults?: unknown;
|
|
41
|
-
}, captured: ToolOutcome[], locale: string, expectedProposal?: boolean): AgentChatResult;
|
|
44
|
+
}, captured: ToolOutcome[], locale: string, expectedProposal?: boolean, previews?: Map<string, ProposalPreview>): AgentChatResult;
|
|
42
45
|
export {};
|