@simmalugnt-se/payload-editor-assistant 0.13.1 → 0.14.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 +12 -0
- package/README.md +1 -1
- package/dist/components/BatchPanel.d.ts +1 -1
- package/dist/components/BatchPanel.js +30 -4
- package/dist/components/CompletedResultCard.d.ts +7 -0
- package/dist/components/CompletedResultCard.js +10 -0
- package/dist/components/EditorAssistantAside.js +51 -34
- package/dist/components/EditorAssistantDocumentBridge.js +19 -17
- package/dist/components/assistant-context.d.ts +11 -3
- package/dist/components/assistant-context.js +4 -4
- package/dist/components/conversation-store.js +21 -0
- package/dist/components/copy.d.ts +12 -0
- package/dist/components/copy.js +12 -0
- package/dist/domain/decide.d.ts +1 -0
- package/dist/domain/decide.js +7 -1
- package/dist/runtime/system-prompt.js +1 -1
- package/dist/styles/assistant.css +36 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
- A work-list link (`?ask=`) opens the panel with the prompt as a task to start or cancel, like a
|
|
6
|
+
batch, instead of putting it in the message box. The task belongs to the document it opened.
|
|
7
|
+
|
|
8
|
+
## 0.13.2
|
|
9
|
+
|
|
10
|
+
- Keep compact result cards in the conversation after accepted draft creations, form changes and
|
|
11
|
+
saved batch fixes. Cards link to the document and retain changed fields and locale after reload.
|
|
12
|
+
- Chat-only image descriptions follow the language of the editor's question even when Payload is
|
|
13
|
+
displaying another content locale. Values submitted to CMS fields still follow the content locale.
|
|
14
|
+
|
|
3
15
|
## 0.13.1
|
|
4
16
|
|
|
5
17
|
- The chat composer starts at three lines and grows with typed or pasted text up to eight lines
|
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ with a `visual-editing:select` event. Without it the selection follows the form
|
|
|
177
177
|
`@simmalugnt-se/payload-content-health` lists content issues and offers "Fix with the assistant" in
|
|
178
178
|
the collections the assistant works in. The link opens the document with the field selected (a
|
|
179
179
|
`content-health:select` event) and `?ask=<prompt>` in the address: the assistant opens with the
|
|
180
|
-
prompt
|
|
180
|
+
prompt as a task, and the editor starts it. A link alone never makes the assistant act.
|
|
181
181
|
|
|
182
182
|
The plugin publishes its allowlisted collections as `config.admin.custom.editorAssistant.collections`
|
|
183
183
|
so the work list knows where to offer it; the packages do not import each other.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BatchRequest } from "./assistant-context.tsx";
|
|
2
2
|
/**
|
|
3
3
|
* Many documents at once, from a work list link: the assistant suggests a change for each, one
|
|
4
4
|
* request after the other, and the editor saves the ones they want. Nothing is written before that.
|
|
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
6
6
|
import { formatBatchTargets } from "../batch/targets.js";
|
|
7
7
|
import { ENDPOINT_PREFIX } from "../package-name.js";
|
|
8
8
|
import { assistantRequest } from "./assistant-api.js";
|
|
9
|
+
import { useAssistantState } from "./assistant-context.js";
|
|
9
10
|
import { cardAfterApply, cardAfterPropose, changedEdits, initialCards, savePlan, waitingKeys, } from "./batch-run.js";
|
|
10
11
|
import { BATCH_COPY, uiLanguage } from "./copy.js";
|
|
11
12
|
import { ProposalReview } from "./ProposalReview.js";
|
|
@@ -14,6 +15,7 @@ import { ProposalReview } from "./ProposalReview.js";
|
|
|
14
15
|
* request after the other, and the editor saves the ones they want. Nothing is written before that.
|
|
15
16
|
*/
|
|
16
17
|
export function BatchPanel({ batch, onClose }) {
|
|
18
|
+
const { setMessages } = useAssistantState();
|
|
17
19
|
const { config } = useConfig();
|
|
18
20
|
const { i18n } = useTranslation();
|
|
19
21
|
const router = useRouter();
|
|
@@ -82,13 +84,14 @@ export function BatchPanel({ batch, onClose }) {
|
|
|
82
84
|
abortRef.current?.abort();
|
|
83
85
|
};
|
|
84
86
|
const apply = useCallback(async (card, action) => {
|
|
85
|
-
|
|
87
|
+
const proposal = card.proposal;
|
|
88
|
+
if (!proposal)
|
|
86
89
|
return;
|
|
87
90
|
update(card.key, (current) => ({ ...current, status: "saving" }));
|
|
88
91
|
const answer = await assistantRequest(apiBase, "/batch/apply", {
|
|
89
92
|
action,
|
|
90
|
-
approvalId:
|
|
91
|
-
nonce:
|
|
93
|
+
approvalId: proposal.approvalId,
|
|
94
|
+
nonce: proposal.nonce,
|
|
92
95
|
batchId: batch.id,
|
|
93
96
|
...(action === "apply" ? { edits: changedEdits(card) } : {}),
|
|
94
97
|
}).catch(() => ({
|
|
@@ -98,7 +101,30 @@ export function BatchPanel({ batch, onClose }) {
|
|
|
98
101
|
status: 0,
|
|
99
102
|
}));
|
|
100
103
|
update(card.key, (current) => cardAfterApply(current, answer.ok ? answer.data : { ok: false, error: answer.error, message: answer.message }));
|
|
101
|
-
|
|
104
|
+
if (action === "apply" && answer.ok && answer.data.ok && answer.data.status !== "rejected") {
|
|
105
|
+
const href = `${config.routes.admin}/collections/${encodeURIComponent(card.target.collection)}/${encodeURIComponent(card.target.id)}${card.target.locale ? `?locale=${encodeURIComponent(card.target.locale)}` : ""}`;
|
|
106
|
+
setMessages((current) => [
|
|
107
|
+
...current,
|
|
108
|
+
{
|
|
109
|
+
id: crypto.randomUUID(),
|
|
110
|
+
role: "assistant",
|
|
111
|
+
text: `${copy.status[answer.data.status]}: ${card.title}`,
|
|
112
|
+
result: {
|
|
113
|
+
kind: "saved",
|
|
114
|
+
title: card.title.slice(0, 200),
|
|
115
|
+
href,
|
|
116
|
+
locale: card.target.locale,
|
|
117
|
+
fields: [
|
|
118
|
+
...new Set(proposal.lines
|
|
119
|
+
.map((line) => line.label)
|
|
120
|
+
.filter((label) => Boolean(label))
|
|
121
|
+
.map((label) => label.slice(0, 80))),
|
|
122
|
+
].slice(0, 4),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
}, [apiBase, batch.id, config.routes.admin, copy, setMessages, update]);
|
|
102
128
|
const save = async () => {
|
|
103
129
|
setPhase("saving");
|
|
104
130
|
for (const step of savePlan(cardsRef.current)) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AssistantMessage } from "./assistant-context.tsx";
|
|
2
|
+
import type { Copy } from "./copy.ts";
|
|
3
|
+
/** A finished action stays visible in the conversation after its approval panel closes. */
|
|
4
|
+
export declare function CompletedResultCard({ result, copy, }: {
|
|
5
|
+
result: NonNullable<AssistantMessage["result"]>;
|
|
6
|
+
copy: Copy;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** A finished action stays visible in the conversation after its approval panel closes. */
|
|
3
|
+
export function CompletedResultCard({ result, copy, }) {
|
|
4
|
+
const status = result.kind === "draft"
|
|
5
|
+
? copy.completedDraft
|
|
6
|
+
: result.kind === "saved"
|
|
7
|
+
? copy.completedSavedChange
|
|
8
|
+
: copy.completedChange;
|
|
9
|
+
return (_jsxs("div", { className: "ea-result", children: [_jsxs("span", { className: "ea-result__status", children: [status, result.locale ? ` · ${result.locale.toUpperCase()}` : ""] }), result.href ? (_jsx("a", { className: "ea-result__title", href: result.href, children: result.title })) : (_jsx("strong", { className: "ea-result__title", children: result.title })), result.fields?.length ? (_jsx("span", { className: "ea-result__fields", children: result.fields.join(" · ") })) : null] }));
|
|
10
|
+
}
|
|
@@ -11,6 +11,7 @@ import { useAssistantState, } from "./assistant-context.js";
|
|
|
11
11
|
import { BatchPanel } from "./BatchPanel.js";
|
|
12
12
|
import { releaseTakenBatches } from "./batch-link.js";
|
|
13
13
|
import { ChatImages, useChatImages } from "./ChatImages.js";
|
|
14
|
+
import { CompletedResultCard } from "./CompletedResultCard.js";
|
|
14
15
|
import { COPY, translateAction, uiLanguage } from "./copy.js";
|
|
15
16
|
import { ProposalReview } from "./ProposalReview.js";
|
|
16
17
|
import { isFormProposal, proposalSteps, refineProposal, replaceLatestStep, } from "./proposal-chain.js";
|
|
@@ -18,7 +19,7 @@ import { ReplyText } from "./ReplyText.js";
|
|
|
18
19
|
import { KEEP_SELECTION_ATTR } from "./selection-dom.js";
|
|
19
20
|
import { useTranslationOffer } from "./translation-offer.js";
|
|
20
21
|
export function EditorAssistantAside() {
|
|
21
|
-
const { open, setOpen, status, messages, shortcuts, collections, bridge, formAdapter, pendingApproval, selection, setSelection,
|
|
22
|
+
const { open, setOpen, status, messages, shortcuts, collections, bridge, formAdapter, pendingApproval, selection, setSelection, task, setTask, resetConversation, setStatus, setMessages, setPendingApproval, stop, setAbortController, batch, setBatch, } = useAssistantState();
|
|
22
23
|
const { config, getEntityConfig } = useConfig();
|
|
23
24
|
const router = useRouter();
|
|
24
25
|
const locale = useLocale();
|
|
@@ -32,13 +33,6 @@ export function EditorAssistantAside() {
|
|
|
32
33
|
const visibleShortcuts = shortcuts.filter((shortcut) => shortcutMatches(shortcut, view));
|
|
33
34
|
const [draft, setDraft] = useState("");
|
|
34
35
|
const attachments = useChatImages(`${pathname}:${locale.code}`, status === "streaming" || Boolean(batch), copy);
|
|
35
|
-
// A ready prompt replaces the draft; it is only sent when the editor sends it.
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
if (prefill !== null) {
|
|
38
|
-
setDraft(prefill);
|
|
39
|
-
setPrefill(null);
|
|
40
|
-
}
|
|
41
|
-
}, [prefill, setPrefill]);
|
|
42
36
|
const threadRef = useRef(null);
|
|
43
37
|
const composerRef = useRef(null);
|
|
44
38
|
// Grow on typing/prefill and remeasure when the panel reopens or its width changes.
|
|
@@ -109,9 +103,38 @@ export function EditorAssistantAside() {
|
|
|
109
103
|
document.removeEventListener("change", onEdit, true);
|
|
110
104
|
};
|
|
111
105
|
}, [previewing]);
|
|
112
|
-
const say = (text) => {
|
|
113
|
-
setMessages((current) => [
|
|
106
|
+
const say = (text, result) => {
|
|
107
|
+
setMessages((current) => [
|
|
108
|
+
...current,
|
|
109
|
+
{
|
|
110
|
+
id: crypto.randomUUID(),
|
|
111
|
+
role: "assistant",
|
|
112
|
+
text: result ? `${text} ${result.title}` : text,
|
|
113
|
+
...(result ? { result } : {}),
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
114
116
|
};
|
|
117
|
+
const changeResult = (pending) => ({
|
|
118
|
+
kind: "change",
|
|
119
|
+
title: (bridge?.title ||
|
|
120
|
+
bridge?.collectionSlug ||
|
|
121
|
+
bridge?.globalSlug ||
|
|
122
|
+
copy.completedChange).slice(0, 200),
|
|
123
|
+
...(bridge?.collectionSlug && bridge.documentId
|
|
124
|
+
? {
|
|
125
|
+
href: `${config.routes.admin}/collections/${encodeURIComponent(bridge.collectionSlug)}/${encodeURIComponent(String(bridge.documentId))}`,
|
|
126
|
+
}
|
|
127
|
+
: bridge?.globalSlug
|
|
128
|
+
? { href: `${config.routes.admin}/globals/${encodeURIComponent(bridge.globalSlug)}` }
|
|
129
|
+
: {}),
|
|
130
|
+
locale: bridge?.locale ?? locale.code,
|
|
131
|
+
fields: [
|
|
132
|
+
...new Set((pending.lines ?? [])
|
|
133
|
+
.map((line) => line.label)
|
|
134
|
+
.filter((label) => Boolean(label))
|
|
135
|
+
.map((label) => label.slice(0, 80))),
|
|
136
|
+
].slice(0, 4),
|
|
137
|
+
});
|
|
115
138
|
/**
|
|
116
139
|
* Puts a form proposal into the form so the form and Live Preview show it before it is kept.
|
|
117
140
|
* `basis` is the form the proposal was made for. With a suggestion already in the form, the new
|
|
@@ -169,7 +192,7 @@ export function EditorAssistantAside() {
|
|
|
169
192
|
* Keeps a previewed change. The server checks each step against the form it was made for and
|
|
170
193
|
* logs it, oldest first; the form already shows the change, so nothing is applied again.
|
|
171
194
|
*/
|
|
172
|
-
const keepPreview = async (pending
|
|
195
|
+
const keepPreview = async (pending) => {
|
|
173
196
|
const applied = pending.applied;
|
|
174
197
|
if (!applied) {
|
|
175
198
|
return false;
|
|
@@ -186,9 +209,7 @@ export function EditorAssistantAside() {
|
|
|
186
209
|
return false;
|
|
187
210
|
}
|
|
188
211
|
}
|
|
189
|
-
|
|
190
|
-
say(autosave ? talk.keptAutosave : talk.kept);
|
|
191
|
-
}
|
|
212
|
+
say(autosave ? talk.keptAutosave : talk.kept, changeResult(pending));
|
|
192
213
|
return true;
|
|
193
214
|
};
|
|
194
215
|
const undoPreview = async (pending, force = false) => {
|
|
@@ -314,7 +335,7 @@ export function EditorAssistantAside() {
|
|
|
314
335
|
// Leaving the form or starting a draft ends the open suggestion: keep it first, as before.
|
|
315
336
|
const chain = shown ?? open;
|
|
316
337
|
if (chain?.applied && (next || (navigate && "href" in navigate))) {
|
|
317
|
-
if (!(await keepPreview(chain
|
|
338
|
+
if (!(await keepPreview(chain))) {
|
|
318
339
|
return;
|
|
319
340
|
}
|
|
320
341
|
}
|
|
@@ -422,14 +443,7 @@ export function EditorAssistantAside() {
|
|
|
422
443
|
try {
|
|
423
444
|
await formAdapter.reset(result.data.patchedData);
|
|
424
445
|
formAdapter.setModified(true);
|
|
425
|
-
|
|
426
|
-
...current,
|
|
427
|
-
{
|
|
428
|
-
id: crypto.randomUUID(),
|
|
429
|
-
role: "assistant",
|
|
430
|
-
text: talk.applied,
|
|
431
|
-
},
|
|
432
|
-
]);
|
|
446
|
+
say(talk.applied, changeResult(pendingApproval));
|
|
433
447
|
}
|
|
434
448
|
catch {
|
|
435
449
|
setStatus("stale_context");
|
|
@@ -447,15 +461,15 @@ export function EditorAssistantAside() {
|
|
|
447
461
|
if (result.data.capability === "draft.create" &&
|
|
448
462
|
result.data.collection &&
|
|
449
463
|
result.data.documentId) {
|
|
450
|
-
const href = `${config.routes.admin}/collections/${result.data.collection}/${result.data.documentId}`;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
464
|
+
const href = `${config.routes.admin}/collections/${encodeURIComponent(result.data.collection)}/${encodeURIComponent(result.data.documentId)}`;
|
|
465
|
+
say(talk.draftCreated, {
|
|
466
|
+
kind: "draft",
|
|
467
|
+
title: (result.data.title ||
|
|
468
|
+
pendingApproval.lines?.find((line) => line.path === "title")?.edit?.value ||
|
|
469
|
+
result.data.collection).slice(0, 200),
|
|
470
|
+
href,
|
|
471
|
+
locale: bridge?.locale ?? locale.code,
|
|
472
|
+
});
|
|
459
473
|
router.push(href);
|
|
460
474
|
}
|
|
461
475
|
}
|
|
@@ -483,9 +497,12 @@ export function EditorAssistantAside() {
|
|
|
483
497
|
}, size: "medium", children: copy.newChat })) : null, _jsx(Button, { "aria-label": copy.collapseLabel, buttonStyle: "icon-label", className: "ea-aside__collapse", icon: _jsx(ChevronIcon, { direction: "right" }), margin: false, onClick: () => setOpen(false), tooltip: copy.collapse })] }), batch ? (_jsx("div", { className: "ea-aside__body", children: _jsx(BatchPanel, { batch: batch, onClose: () => {
|
|
484
498
|
releaseTakenBatches();
|
|
485
499
|
setBatch(null);
|
|
486
|
-
} }, batch.id) })) : (_jsxs("div", { className: "ea-aside__body", children: [_jsxs("div", { className: "ea-messages", ref: threadRef, onScroll: onThreadScroll, children: [messages.length === 0 ? (_jsx("div", { className: "ea-msg ea-msg--assistant", children: introText(view, bridge, collections, config, copy) })) : null, messages.map((message) => (_jsxs("div", { className: `ea-msg ea-msg--${message.role}`, children: [message.role === "assistant" ? _jsx(ReplyText, { text: message.text }) : message.text, _jsx(ChatImages, { images: message.images, copy: copy }), message.candidates?.length ? (_jsx("div", { className: "ea-actions", children: message.candidates.map((candidate) => (_jsx(Button, { buttonStyle: "pill", margin: false, onClick: () => {
|
|
500
|
+
} }, batch.id) })) : (_jsxs("div", { className: "ea-aside__body", children: [_jsxs("div", { className: "ea-messages", ref: threadRef, onScroll: onThreadScroll, children: [messages.length === 0 ? (_jsx("div", { className: "ea-msg ea-msg--assistant", children: introText(view, bridge, collections, config, copy) })) : null, messages.map((message) => (_jsxs("div", { className: `ea-msg ea-msg--${message.role}`, children: [message.result ? (_jsx(CompletedResultCard, { result: message.result, copy: copy })) : message.role === "assistant" ? (_jsx(ReplyText, { text: message.text })) : (message.text), _jsx(ChatImages, { images: message.images, copy: copy }), message.candidates?.length ? (_jsx("div", { className: "ea-actions", children: message.candidates.map((candidate) => (_jsx(Button, { buttonStyle: "pill", margin: false, onClick: () => {
|
|
487
501
|
router.push(candidate.href);
|
|
488
|
-
}, size: "small", children: candidate.label }, candidate.href))) })) : null] }, message.id))), status === "streaming" ? (_jsxs("div", { className: "ea-msg ea-msg--working", children: [_jsxs("span", { className: "ea-dots", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }), _jsx("span", { children: step ? talk.steps[step] : talk.working })] })) : null] }), pendingApproval ? (_jsxs("div", { className: "ea-approval", children: [
|
|
502
|
+
}, size: "small", children: candidate.label }, candidate.href))) })) : null] }, message.id))), status === "streaming" ? (_jsxs("div", { className: "ea-msg ea-msg--working", children: [_jsxs("span", { className: "ea-dots", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }), _jsx("span", { children: step ? talk.steps[step] : talk.working })] })) : null] }), task && !pendingApproval && status !== "streaming" ? (_jsxs("div", { className: "ea-approval ea-task", children: [_jsxs("p", { className: "ea-task__text", children: [_jsxs("strong", { children: [copy.task, ":"] }), " ", task] }), _jsx("p", { className: "ea-approval__hint", children: copy.taskHint }), _jsxs("div", { className: "ea-actions", children: [_jsx(Button, { buttonStyle: "primary", disabled: attachments.preparing, margin: false, onClick: () => {
|
|
503
|
+
setTask(null);
|
|
504
|
+
void send(task);
|
|
505
|
+
}, size: "medium", children: copy.start }), _jsx(Button, { buttonStyle: "secondary", margin: false, onClick: () => setTask(null), size: "medium", children: copy.cancel })] })] })) : null, pendingApproval ? (_jsxs("div", { className: "ea-approval", children: [_jsx("h3", { children: pendingApproval.title }), pendingApproval.applied ? (_jsxs("p", { className: "ea-approval__hint", children: [autosave ? copy.previewHintAutosave : copy.previewHint, pendingApproval.earlier?.length ? ` ${copy.refinedHint}` : ""] })) : null, pendingApproval.lines?.length ? (_jsx("div", { className: "ea-approval__review", children: _jsx(ProposalReview, { lines: pendingApproval.lines, language: uiLanguage(i18n.language) }) })) : (_jsx("ul", { className: "ea-approval__diff", children: pendingApproval.summary.split("\n").map((line, index) => line.startsWith(" ") ? (
|
|
489
506
|
// biome-ignore lint/suspicious/noArrayIndexKey: lines are static for one approval
|
|
490
507
|
_jsx("li", { className: "ea-approval__sub", children: line.trim() }, index)) : (
|
|
491
508
|
// biome-ignore lint/suspicious/noArrayIndexKey: lines are static for one approval
|
|
@@ -6,14 +6,14 @@ import { ASK_PARAM, CONTENT_HEALTH_SELECT_EVENT, selectionFromElement, selection
|
|
|
6
6
|
/** Collapsed rows render their labels only once visual editing has opened them. */
|
|
7
7
|
const RELABEL_MS = [150, 500, 1200];
|
|
8
8
|
export function EditorAssistantDocumentBridge() {
|
|
9
|
-
const { setBridge, setFormAdapter, setOpen,
|
|
9
|
+
const { setBridge, setFormAdapter, setOpen, setSelection, setTask } = useAssistantState();
|
|
10
10
|
const documentInfo = useDocumentInfo();
|
|
11
11
|
const locale = useLocale();
|
|
12
12
|
const modified = useFormModified();
|
|
13
13
|
const form = useDocumentForm();
|
|
14
14
|
const formRef = useRef(form);
|
|
15
15
|
// Kept across effect re-runs: the parameter is gone from the address after the first read.
|
|
16
|
-
const asked = useRef(
|
|
16
|
+
const asked = useRef(undefined);
|
|
17
17
|
useEffect(() => {
|
|
18
18
|
formRef.current = form;
|
|
19
19
|
});
|
|
@@ -27,25 +27,27 @@ export function EditorAssistantDocumentBridge() {
|
|
|
27
27
|
setFormAdapter(null);
|
|
28
28
|
};
|
|
29
29
|
}, [setFormAdapter]);
|
|
30
|
-
// A work list link (`?ask=`) opens the panel with a ready
|
|
31
|
-
// link alone never makes the assistant act. The parameter is removed once read
|
|
30
|
+
// A work list link (`?ask=`) opens the panel with a ready task. The editor starts it, so a
|
|
31
|
+
// link alone never makes the assistant act. The parameter is removed once read, and the task
|
|
32
|
+
// belongs to this document: it goes when the editor leaves.
|
|
32
33
|
useEffect(() => {
|
|
33
|
-
if (asked.current) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if (asked.current === undefined) {
|
|
35
|
+
const url = new URL(window.location.href);
|
|
36
|
+
const prompt = url.searchParams.get(ASK_PARAM)?.trim().slice(0, 2000);
|
|
37
|
+
asked.current = prompt || null;
|
|
38
|
+
if (prompt !== undefined) {
|
|
39
|
+
url.searchParams.delete(ASK_PARAM);
|
|
40
|
+
window.history.replaceState(null, "", url);
|
|
41
|
+
}
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (prompt) {
|
|
45
|
-
setPrefill(prompt);
|
|
43
|
+
if (asked.current) {
|
|
44
|
+
setTask(asked.current);
|
|
46
45
|
setOpen(true);
|
|
47
46
|
}
|
|
48
|
-
|
|
47
|
+
return () => {
|
|
48
|
+
setTask(null);
|
|
49
|
+
};
|
|
50
|
+
}, [setOpen, setTask]);
|
|
49
51
|
// "This" in the editor's message is what they last clicked or focused: a field, a block or a row,
|
|
50
52
|
// in the form, (through visual editing) in the preview, or brought into view by content health. Clicks elsewhere in Admin clear it;
|
|
51
53
|
// clicks in the assistant itself keep it, so the editor can click, then type.
|
|
@@ -62,6 +62,14 @@ export type AssistantMessage = {
|
|
|
62
62
|
id: string;
|
|
63
63
|
role: "user" | "assistant" | "system";
|
|
64
64
|
text: string;
|
|
65
|
+
/** A compact, persistent receipt for a change the editor accepted. */
|
|
66
|
+
result?: {
|
|
67
|
+
kind: "draft" | "change" | "saved";
|
|
68
|
+
title: string;
|
|
69
|
+
href?: string;
|
|
70
|
+
locale?: string;
|
|
71
|
+
fields?: string[];
|
|
72
|
+
};
|
|
65
73
|
candidates?: Array<{
|
|
66
74
|
href: string;
|
|
67
75
|
label: string;
|
|
@@ -81,8 +89,8 @@ type AssistantContextValue = {
|
|
|
81
89
|
pendingApproval: PendingApproval | null;
|
|
82
90
|
/** What the editor last clicked or focused in the open form. */
|
|
83
91
|
selection: DomSelection | null;
|
|
84
|
-
/** A
|
|
85
|
-
|
|
92
|
+
/** A ready task from a work list link: the panel shows it until the editor starts or cancels it. */
|
|
93
|
+
task: string | null;
|
|
86
94
|
batch: BatchRequest | null;
|
|
87
95
|
setBatch: (batch: BatchRequest | null) => void;
|
|
88
96
|
/** `remember: false` when the panel makes way on its own (see nav-room.ts), not the editor. */
|
|
@@ -93,7 +101,7 @@ type AssistantContextValue = {
|
|
|
93
101
|
setFormAdapter: (adapter: FormAdapter | null) => void;
|
|
94
102
|
setPendingApproval: (pending: PendingApproval | null) => void;
|
|
95
103
|
setSelection: (selection: DomSelection | null) => void;
|
|
96
|
-
|
|
104
|
+
setTask: (task: string | null) => void;
|
|
97
105
|
setStatus: (status: AssistantChromeStatus) => void;
|
|
98
106
|
setMessages: (updater: (current: AssistantMessage[]) => AssistantMessage[]) => void;
|
|
99
107
|
resetConversation: () => void;
|
|
@@ -12,7 +12,7 @@ export function AssistantStateProvider({ children, shortcuts, collections, userI
|
|
|
12
12
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
13
13
|
const [abortController, setAbortController] = useState(null);
|
|
14
14
|
const [selection, setSelection] = useState(null);
|
|
15
|
-
const [
|
|
15
|
+
const [task, setTask] = useState(null);
|
|
16
16
|
const [batch, setBatch] = useState(null);
|
|
17
17
|
// Read after mount, so the server render (closed) and the first client render agree.
|
|
18
18
|
useEffect(() => {
|
|
@@ -76,7 +76,7 @@ export function AssistantStateProvider({ children, shortcuts, collections, userI
|
|
|
76
76
|
formAdapter,
|
|
77
77
|
pendingApproval,
|
|
78
78
|
selection,
|
|
79
|
-
|
|
79
|
+
task,
|
|
80
80
|
batch,
|
|
81
81
|
setBatch,
|
|
82
82
|
setOpen,
|
|
@@ -84,7 +84,7 @@ export function AssistantStateProvider({ children, shortcuts, collections, userI
|
|
|
84
84
|
setFormAdapter,
|
|
85
85
|
setPendingApproval,
|
|
86
86
|
setSelection,
|
|
87
|
-
|
|
87
|
+
setTask,
|
|
88
88
|
setStatus,
|
|
89
89
|
setMessages,
|
|
90
90
|
resetConversation,
|
|
@@ -101,7 +101,7 @@ export function AssistantStateProvider({ children, shortcuts, collections, userI
|
|
|
101
101
|
formAdapter,
|
|
102
102
|
pendingApproval,
|
|
103
103
|
selection,
|
|
104
|
-
|
|
104
|
+
task,
|
|
105
105
|
batch,
|
|
106
106
|
setOpen,
|
|
107
107
|
resetConversation,
|
|
@@ -103,6 +103,9 @@ export function parseStored(raw, now) {
|
|
|
103
103
|
candidates: Array.isArray(message.candidates)
|
|
104
104
|
? message.candidates.filter(isCandidate)
|
|
105
105
|
: undefined,
|
|
106
|
+
...(message.role === "assistant" && isCompletedResult(message.result)
|
|
107
|
+
? { result: message.result }
|
|
108
|
+
: {}),
|
|
106
109
|
})))
|
|
107
110
|
: [];
|
|
108
111
|
}
|
|
@@ -120,6 +123,24 @@ function isCandidate(value) {
|
|
|
120
123
|
const candidate = value;
|
|
121
124
|
return typeof candidate?.href === "string" && typeof candidate.label === "string";
|
|
122
125
|
}
|
|
126
|
+
function isCompletedResult(value) {
|
|
127
|
+
const result = value;
|
|
128
|
+
return Boolean(result &&
|
|
129
|
+
(result.kind === "draft" || result.kind === "change" || result.kind === "saved") &&
|
|
130
|
+
typeof result.title === "string" &&
|
|
131
|
+
result.title.length > 0 &&
|
|
132
|
+
result.title.length <= 200 &&
|
|
133
|
+
(result.href === undefined ||
|
|
134
|
+
(typeof result.href === "string" &&
|
|
135
|
+
/^\/[a-zA-Z0-9/_%-]+(?:\?locale=[a-zA-Z-]{2,12})?$/.test(result.href) &&
|
|
136
|
+
!result.href.startsWith("//"))) &&
|
|
137
|
+
(result.locale === undefined ||
|
|
138
|
+
(typeof result.locale === "string" && /^[a-zA-Z-]{2,12}$/.test(result.locale))) &&
|
|
139
|
+
(result.fields === undefined ||
|
|
140
|
+
(Array.isArray(result.fields) &&
|
|
141
|
+
result.fields.length <= 4 &&
|
|
142
|
+
result.fields.every((field) => typeof field === "string" && field.length <= 80))));
|
|
143
|
+
}
|
|
123
144
|
function isStoredImage(value) {
|
|
124
145
|
if (isChatImage(value))
|
|
125
146
|
return true;
|
|
@@ -40,6 +40,9 @@ export declare const COPY: {
|
|
|
40
40
|
readonly undo: "Undo";
|
|
41
41
|
readonly undoAnyway: "Undo anyway";
|
|
42
42
|
readonly cancel: "Cancel";
|
|
43
|
+
readonly task: "Task";
|
|
44
|
+
readonly taskHint: "The assistant suggests a change in the form, which you keep or undo. Nothing is saved yet.";
|
|
45
|
+
readonly start: "Start";
|
|
43
46
|
readonly previewHint: "Shown in the form and the preview. Nothing is saved yet.";
|
|
44
47
|
readonly previewHintAutosave: "Shown in the form and the preview. The draft saves itself, but nothing is published.";
|
|
45
48
|
readonly undoLosesEdits: "You edited the form after the suggestion. Undo removes those edits too.";
|
|
@@ -52,6 +55,9 @@ export declare const COPY: {
|
|
|
52
55
|
readonly applied: "Applied to the unsaved form. Save in Payload when you are ready.";
|
|
53
56
|
readonly applyFailed: "The form could not be updated. It was left unchanged.";
|
|
54
57
|
readonly draftCreated: "The draft is created and open.";
|
|
58
|
+
readonly completedDraft: "Draft created";
|
|
59
|
+
readonly completedChange: "Change kept in the form";
|
|
60
|
+
readonly completedSavedChange: "Change saved";
|
|
55
61
|
readonly kept: "Kept. Save in Payload when you are ready.";
|
|
56
62
|
readonly keptAutosave: "Kept. The draft is saved; publish when you are ready.";
|
|
57
63
|
readonly undone: "Undone. The form is back as it was.";
|
|
@@ -110,6 +116,9 @@ export declare const COPY: {
|
|
|
110
116
|
readonly undo: "Ångra";
|
|
111
117
|
readonly undoAnyway: "Ångra ändå";
|
|
112
118
|
readonly cancel: "Avbryt";
|
|
119
|
+
readonly task: "Uppgift";
|
|
120
|
+
readonly taskHint: "Assistenten föreslår en ändring i formuläret som du behåller eller ångrar. Inget sparas än.";
|
|
121
|
+
readonly start: "Starta";
|
|
113
122
|
readonly previewHint: "Syns i formuläret och förhandsvisningen. Inget är sparat än.";
|
|
114
123
|
readonly previewHintAutosave: "Syns i formuläret och förhandsvisningen. Utkastet sparas automatiskt, men inget publiceras.";
|
|
115
124
|
readonly undoLosesEdits: "Du har ändrat i formuläret efter förslaget. Ångra tar bort de ändringarna också.";
|
|
@@ -122,6 +131,9 @@ export declare const COPY: {
|
|
|
122
131
|
readonly applied: "Ändringen är införd i formuläret men inte sparad. Spara i Payload när du är redo.";
|
|
123
132
|
readonly applyFailed: "Formuläret kunde inte uppdateras och lämnades orört.";
|
|
124
133
|
readonly draftCreated: "Utkastet är skapat och öppnat.";
|
|
134
|
+
readonly completedDraft: "Utkast skapat";
|
|
135
|
+
readonly completedChange: "Ändring behållen i formuläret";
|
|
136
|
+
readonly completedSavedChange: "Ändring sparad";
|
|
125
137
|
readonly kept: "Behållet. Spara i Payload när du är redo.";
|
|
126
138
|
readonly keptAutosave: "Behållet. Utkastet är sparat; publicera när du är redo.";
|
|
127
139
|
readonly undone: "Ångrat. Formuläret är som innan.";
|
package/dist/components/copy.js
CHANGED
|
@@ -41,6 +41,9 @@ export const COPY = {
|
|
|
41
41
|
undo: "Undo",
|
|
42
42
|
undoAnyway: "Undo anyway",
|
|
43
43
|
cancel: "Cancel",
|
|
44
|
+
task: "Task",
|
|
45
|
+
taskHint: "The assistant suggests a change in the form, which you keep or undo. Nothing is saved yet.",
|
|
46
|
+
start: "Start",
|
|
44
47
|
previewHint: "Shown in the form and the preview. Nothing is saved yet.",
|
|
45
48
|
previewHintAutosave: "Shown in the form and the preview. The draft saves itself, but nothing is published.",
|
|
46
49
|
undoLosesEdits: "You edited the form after the suggestion. Undo removes those edits too.",
|
|
@@ -53,6 +56,9 @@ export const COPY = {
|
|
|
53
56
|
applied: "Applied to the unsaved form. Save in Payload when you are ready.",
|
|
54
57
|
applyFailed: "The form could not be updated. It was left unchanged.",
|
|
55
58
|
draftCreated: "The draft is created and open.",
|
|
59
|
+
completedDraft: "Draft created",
|
|
60
|
+
completedChange: "Change kept in the form",
|
|
61
|
+
completedSavedChange: "Change saved",
|
|
56
62
|
kept: "Kept. Save in Payload when you are ready.",
|
|
57
63
|
keptAutosave: "Kept. The draft is saved; publish when you are ready.",
|
|
58
64
|
undone: "Undone. The form is back as it was.",
|
|
@@ -111,6 +117,9 @@ export const COPY = {
|
|
|
111
117
|
undo: "Ångra",
|
|
112
118
|
undoAnyway: "Ångra ändå",
|
|
113
119
|
cancel: "Avbryt",
|
|
120
|
+
task: "Uppgift",
|
|
121
|
+
taskHint: "Assistenten föreslår en ändring i formuläret som du behåller eller ångrar. Inget sparas än.",
|
|
122
|
+
start: "Starta",
|
|
114
123
|
previewHint: "Syns i formuläret och förhandsvisningen. Inget är sparat än.",
|
|
115
124
|
previewHintAutosave: "Syns i formuläret och förhandsvisningen. Utkastet sparas automatiskt, men inget publiceras.",
|
|
116
125
|
undoLosesEdits: "Du har ändrat i formuläret efter förslaget. Ångra tar bort de ändringarna också.",
|
|
@@ -123,6 +132,9 @@ export const COPY = {
|
|
|
123
132
|
applied: "Ändringen är införd i formuläret men inte sparad. Spara i Payload när du är redo.",
|
|
124
133
|
applyFailed: "Formuläret kunde inte uppdateras och lämnades orört.",
|
|
125
134
|
draftCreated: "Utkastet är skapat och öppnat.",
|
|
135
|
+
completedDraft: "Utkast skapat",
|
|
136
|
+
completedChange: "Ändring behållen i formuläret",
|
|
137
|
+
completedSavedChange: "Ändring sparad",
|
|
126
138
|
kept: "Behållet. Spara i Payload när du är redo.",
|
|
127
139
|
keptAutosave: "Behållet. Utkastet är sparat; publicera när du är redo.",
|
|
128
140
|
undone: "Ångrat. Formuläret är som innan.",
|
package/dist/domain/decide.d.ts
CHANGED
package/dist/domain/decide.js
CHANGED
|
@@ -5,7 +5,7 @@ import { relationValueError } from "../form/relation-shape.js";
|
|
|
5
5
|
import { loadRichText } from "../form/rich-text.js";
|
|
6
6
|
import { finalizeCompiled, openRichText } from "../form/shape.js";
|
|
7
7
|
import { DEFAULT_APPROVAL_SLUG } from "../package-name.js";
|
|
8
|
-
import { entityFields } from "../schema/discover.js";
|
|
8
|
+
import { entityFields, findCollectionConfig } from "../schema/discover.js";
|
|
9
9
|
import { hasCapability, resolveAllowlist } from "./allowlist.js";
|
|
10
10
|
import { writeAudit } from "./audit.js";
|
|
11
11
|
import { localApi } from "./local-api.js";
|
|
@@ -141,6 +141,7 @@ export async function decideChange(req, options, input) {
|
|
|
141
141
|
capability: "draft.create",
|
|
142
142
|
documentId: String(created.id),
|
|
143
143
|
collection: record.targetCollection,
|
|
144
|
+
title: createdTitle(req, record.targetCollection, created),
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
await consume(req, record.id, "executed");
|
|
@@ -159,6 +160,11 @@ export async function decideChange(req, options, input) {
|
|
|
159
160
|
});
|
|
160
161
|
return { ok: true, capability: "form.propose", patchedData: finalized.data };
|
|
161
162
|
}
|
|
163
|
+
function createdTitle(req, collection, created) {
|
|
164
|
+
const field = findCollectionConfig(req, collection)?.admin?.useAsTitle ?? "title";
|
|
165
|
+
const value = created?.[field];
|
|
166
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, 200) : undefined;
|
|
167
|
+
}
|
|
162
168
|
function identityMismatch(record, input) {
|
|
163
169
|
return ((input.collection ?? "") !== (record.targetCollection ?? "") ||
|
|
164
170
|
(input.global ?? "") !== (record.targetGlobal ?? "") ||
|
|
@@ -108,7 +108,7 @@ function thisTurnBlock(bridge, turnContext) {
|
|
|
108
108
|
];
|
|
109
109
|
if (locale) {
|
|
110
110
|
const content = languageName(locale);
|
|
111
|
-
lines.push(`Write every field value in ${content}; that is the language version being edited. Do not point out which language the content is in; the panel does that.`);
|
|
111
|
+
lines.push(`Write every field value submitted to form.propose or draft.create in ${content}; that is the language version being edited. A suggested alt text given only in chat is not a field value: write it in the editor's chat language, even if the Payload locale is different. Do not point out which language the edited content is in; the panel does that.`);
|
|
112
112
|
}
|
|
113
113
|
const selection = turnContext?.selection;
|
|
114
114
|
if (selection) {
|
|
@@ -128,6 +128,37 @@ html[data-theme="dark"] .ea-aside {
|
|
|
128
128
|
align-self: flex-start;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
.ea-result {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
gap: 0.15em;
|
|
135
|
+
max-width: 100%;
|
|
136
|
+
padding: calc(var(--base) * 0.45) calc(var(--base) * 0.6);
|
|
137
|
+
border: 1px solid var(--theme-elevation-150);
|
|
138
|
+
border-inline-start: 3px solid var(--ea-accent);
|
|
139
|
+
border-radius: var(--style-radius-m);
|
|
140
|
+
background: var(--theme-elevation-50);
|
|
141
|
+
font-size: 13px;
|
|
142
|
+
white-space: normal;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ea-result__status,
|
|
146
|
+
.ea-result__fields {
|
|
147
|
+
color: var(--theme-elevation-500);
|
|
148
|
+
font-size: 11px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.ea-result__title {
|
|
152
|
+
color: var(--theme-text);
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
overflow-wrap: anywhere;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
a.ea-result__title {
|
|
158
|
+
text-decoration: underline;
|
|
159
|
+
text-underline-offset: 0.15em;
|
|
160
|
+
}
|
|
161
|
+
|
|
131
162
|
/* A reply's Markdown: paragraphs and lists spaced like the thread, a line break kept inside one. */
|
|
132
163
|
.ea-reply__p,
|
|
133
164
|
.ea-reply__list {
|
|
@@ -232,6 +263,11 @@ html[data-theme="dark"] .ea-aside {
|
|
|
232
263
|
line-height: 1.4;
|
|
233
264
|
}
|
|
234
265
|
|
|
266
|
+
.ea-task__text {
|
|
267
|
+
margin: 0 0 calc(var(--base) * 0.4);
|
|
268
|
+
line-height: 1.5;
|
|
269
|
+
}
|
|
270
|
+
|
|
235
271
|
.ea-approval__diff {
|
|
236
272
|
max-height: 40vh;
|
|
237
273
|
overflow-y: auto;
|