@simmalugnt-se/payload-editor-assistant 0.13.0 → 0.13.2

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.2
4
+
5
+ - Keep compact result cards in the conversation after accepted draft creations, form changes and
6
+ saved batch fixes. Cards link to the document and retain changed fields and locale after reload.
7
+ - Chat-only image descriptions follow the language of the editor's question even when Payload is
8
+ displaying another content locale. Values submitted to CMS fields still follow the content locale.
9
+
10
+ ## 0.13.1
11
+
12
+ - The chat composer starts at three lines and grows with typed or pasted text up to eight lines
13
+ (or 30% of the viewport), then scrolls. It also resizes after prefilled prompts and panel reopening.
14
+
3
15
  ## 0.13.0
4
16
 
5
17
  - Attach reference images to chat with file selection, drag-and-drop or clipboard paste. Show
@@ -1,4 +1,4 @@
1
- import type { BatchRequest } from "./assistant-context.tsx";
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
- if (!card.proposal)
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: card.proposal.approvalId,
91
- nonce: card.proposal.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
- }, [apiBase, batch.id, update]);
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";
@@ -40,6 +41,28 @@ export function EditorAssistantAside() {
40
41
  }
41
42
  }, [prefill, setPrefill]);
42
43
  const threadRef = useRef(null);
44
+ const composerRef = useRef(null);
45
+ // Grow on typing/prefill and remeasure when the panel reopens or its width changes.
46
+ // biome-ignore lint/correctness/useExhaustiveDependencies: draft changes and returning from a batch require remeasuring the DOM.
47
+ useLayoutEffect(() => {
48
+ const textarea = composerRef.current;
49
+ if (!textarea || !open)
50
+ return;
51
+ const resize = () => {
52
+ textarea.style.height = "auto";
53
+ textarea.style.height = `${textarea.scrollHeight}px`;
54
+ };
55
+ resize();
56
+ let width = textarea.clientWidth;
57
+ const observer = new ResizeObserver(() => {
58
+ if (textarea.clientWidth === width)
59
+ return;
60
+ width = textarea.clientWidth;
61
+ resize();
62
+ });
63
+ observer.observe(textarea);
64
+ return () => observer.disconnect();
65
+ }, [draft, open, batch]);
43
66
  /** Whether the editor typed in the form while a preview was in it; undo would discard that. */
44
67
  const editedSinceRef = useRef(false);
45
68
  const [confirmUndo, setConfirmUndo] = useState(false);
@@ -87,9 +110,38 @@ export function EditorAssistantAside() {
87
110
  document.removeEventListener("change", onEdit, true);
88
111
  };
89
112
  }, [previewing]);
90
- const say = (text) => {
91
- setMessages((current) => [...current, { id: crypto.randomUUID(), role: "assistant", text }]);
113
+ const say = (text, result) => {
114
+ setMessages((current) => [
115
+ ...current,
116
+ {
117
+ id: crypto.randomUUID(),
118
+ role: "assistant",
119
+ text: result ? `${text} ${result.title}` : text,
120
+ ...(result ? { result } : {}),
121
+ },
122
+ ]);
92
123
  };
124
+ const changeResult = (pending) => ({
125
+ kind: "change",
126
+ title: (bridge?.title ||
127
+ bridge?.collectionSlug ||
128
+ bridge?.globalSlug ||
129
+ copy.completedChange).slice(0, 200),
130
+ ...(bridge?.collectionSlug && bridge.documentId
131
+ ? {
132
+ href: `${config.routes.admin}/collections/${encodeURIComponent(bridge.collectionSlug)}/${encodeURIComponent(String(bridge.documentId))}`,
133
+ }
134
+ : bridge?.globalSlug
135
+ ? { href: `${config.routes.admin}/globals/${encodeURIComponent(bridge.globalSlug)}` }
136
+ : {}),
137
+ locale: bridge?.locale ?? locale.code,
138
+ fields: [
139
+ ...new Set((pending.lines ?? [])
140
+ .map((line) => line.label)
141
+ .filter((label) => Boolean(label))
142
+ .map((label) => label.slice(0, 80))),
143
+ ].slice(0, 4),
144
+ });
93
145
  /**
94
146
  * Puts a form proposal into the form so the form and Live Preview show it before it is kept.
95
147
  * `basis` is the form the proposal was made for. With a suggestion already in the form, the new
@@ -147,7 +199,7 @@ export function EditorAssistantAside() {
147
199
  * Keeps a previewed change. The server checks each step against the form it was made for and
148
200
  * logs it, oldest first; the form already shows the change, so nothing is applied again.
149
201
  */
150
- const keepPreview = async (pending, quiet = false) => {
202
+ const keepPreview = async (pending) => {
151
203
  const applied = pending.applied;
152
204
  if (!applied) {
153
205
  return false;
@@ -164,9 +216,7 @@ export function EditorAssistantAside() {
164
216
  return false;
165
217
  }
166
218
  }
167
- if (!quiet) {
168
- say(autosave ? talk.keptAutosave : talk.kept);
169
- }
219
+ say(autosave ? talk.keptAutosave : talk.kept, changeResult(pending));
170
220
  return true;
171
221
  };
172
222
  const undoPreview = async (pending, force = false) => {
@@ -292,7 +342,7 @@ export function EditorAssistantAside() {
292
342
  // Leaving the form or starting a draft ends the open suggestion: keep it first, as before.
293
343
  const chain = shown ?? open;
294
344
  if (chain?.applied && (next || (navigate && "href" in navigate))) {
295
- if (!(await keepPreview(chain, true))) {
345
+ if (!(await keepPreview(chain))) {
296
346
  return;
297
347
  }
298
348
  }
@@ -400,14 +450,7 @@ export function EditorAssistantAside() {
400
450
  try {
401
451
  await formAdapter.reset(result.data.patchedData);
402
452
  formAdapter.setModified(true);
403
- setMessages((current) => [
404
- ...current,
405
- {
406
- id: crypto.randomUUID(),
407
- role: "assistant",
408
- text: talk.applied,
409
- },
410
- ]);
453
+ say(talk.applied, changeResult(pendingApproval));
411
454
  }
412
455
  catch {
413
456
  setStatus("stale_context");
@@ -425,15 +468,15 @@ export function EditorAssistantAside() {
425
468
  if (result.data.capability === "draft.create" &&
426
469
  result.data.collection &&
427
470
  result.data.documentId) {
428
- const href = `${config.routes.admin}/collections/${result.data.collection}/${result.data.documentId}`;
429
- setMessages((current) => [
430
- ...current,
431
- {
432
- id: crypto.randomUUID(),
433
- role: "assistant",
434
- text: talk.draftCreated,
435
- },
436
- ]);
471
+ const href = `${config.routes.admin}/collections/${encodeURIComponent(result.data.collection)}/${encodeURIComponent(result.data.documentId)}`;
472
+ say(talk.draftCreated, {
473
+ kind: "draft",
474
+ title: (result.data.title ||
475
+ pendingApproval.lines?.find((line) => line.path === "title")?.edit?.value ||
476
+ result.data.collection).slice(0, 200),
477
+ href,
478
+ locale: bridge?.locale ?? locale.code,
479
+ });
437
480
  router.push(href);
438
481
  }
439
482
  }
@@ -461,7 +504,7 @@ export function EditorAssistantAside() {
461
504
  }, 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: () => {
462
505
  releaseTakenBatches();
463
506
  setBatch(null);
464
- } }, 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: () => {
507
+ } }, 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: () => {
465
508
  router.push(candidate.href);
466
509
  }, 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: [_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(" ") ? (
467
510
  // biome-ignore lint/suspicious/noArrayIndexKey: lines are static for one approval
@@ -471,7 +514,7 @@ export function EditorAssistantAside() {
471
514
  void send(translate.prompt);
472
515
  }, size: "small", children: translate.label })) : null, visibleShortcuts.map((shortcut) => (_jsx(Button, { buttonStyle: "pill", disabled: status === "streaming", margin: false, onClick: () => {
473
516
  void send(shortcut.prompt);
474
- }, size: "small", children: shortcut.label }, shortcut.id)))] })) : null, selection && bridge ? (_jsxs("div", { className: "ea-selection", children: [_jsxs("span", { className: "ea-selection__label", children: [copy.selected, ": ", _jsx("strong", { children: selection.label })] }), _jsx("button", { "aria-label": copy.clearSelection, className: "ea-selection__clear", type: "button", onClick: () => setSelection(null), children: _jsx(XIcon, {}) })] })) : null, _jsxs("div", { className: "ea-composer__box", children: [attachments.input, attachments.preview, _jsx("textarea", { disabled: status === "streaming", placeholder: copy.placeholder, rows: 2, value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: onKeyDown, onPaste: attachments.onPaste }), _jsxs("div", { className: "ea-composer__bar", children: [_jsx(Button, { buttonStyle: "subtle", type: "button", margin: false, size: "small", disabled: status === "streaming" || attachments.preparing, onClick: attachments.choose, children: copy.attachImage }), status === "streaming" ? (
517
+ }, size: "small", children: shortcut.label }, shortcut.id)))] })) : null, selection && bridge ? (_jsxs("div", { className: "ea-selection", children: [_jsxs("span", { className: "ea-selection__label", children: [copy.selected, ": ", _jsx("strong", { children: selection.label })] }), _jsx("button", { "aria-label": copy.clearSelection, className: "ea-selection__clear", type: "button", onClick: () => setSelection(null), children: _jsx(XIcon, {}) })] })) : null, _jsxs("div", { className: "ea-composer__box", children: [attachments.input, attachments.preview, _jsx("textarea", { ref: composerRef, disabled: status === "streaming", placeholder: copy.placeholder, rows: 3, value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: onKeyDown, onPaste: attachments.onPaste }), _jsxs("div", { className: "ea-composer__bar", children: [_jsx(Button, { buttonStyle: "subtle", type: "button", margin: false, size: "small", disabled: status === "streaming" || attachments.preparing, onClick: attachments.choose, children: copy.attachImage }), status === "streaming" ? (
475
518
  // While the assistant works, Send becomes Stop, where the editor's eye already is.
476
519
  _jsx(Button, { "aria-label": copy.stop, buttonStyle: "secondary", className: "ea-composer__stop", icon: _jsx("span", { className: "ea-stop-icon", "aria-hidden": "true" }), iconPosition: "left", margin: false, onClick: () => stop(), size: "medium", type: "button", children: copy.stop })) : (_jsx(Button, { buttonStyle: "primary", disabled: attachments.preparing ||
477
520
  (draft.trim() === "" && attachments.images.length === 0), margin: false, size: "medium", type: "submit", children: copy.send }))] })] })] })] }))] }));
@@ -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;
@@ -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;
@@ -52,6 +52,9 @@ export declare const COPY: {
52
52
  readonly applied: "Applied to the unsaved form. Save in Payload when you are ready.";
53
53
  readonly applyFailed: "The form could not be updated. It was left unchanged.";
54
54
  readonly draftCreated: "The draft is created and open.";
55
+ readonly completedDraft: "Draft created";
56
+ readonly completedChange: "Change kept in the form";
57
+ readonly completedSavedChange: "Change saved";
55
58
  readonly kept: "Kept. Save in Payload when you are ready.";
56
59
  readonly keptAutosave: "Kept. The draft is saved; publish when you are ready.";
57
60
  readonly undone: "Undone. The form is back as it was.";
@@ -122,6 +125,9 @@ export declare const COPY: {
122
125
  readonly applied: "Ändringen är införd i formuläret men inte sparad. Spara i Payload när du är redo.";
123
126
  readonly applyFailed: "Formuläret kunde inte uppdateras och lämnades orört.";
124
127
  readonly draftCreated: "Utkastet är skapat och öppnat.";
128
+ readonly completedDraft: "Utkast skapat";
129
+ readonly completedChange: "Ändring behållen i formuläret";
130
+ readonly completedSavedChange: "Ändring sparad";
125
131
  readonly kept: "Behållet. Spara i Payload när du är redo.";
126
132
  readonly keptAutosave: "Behållet. Utkastet är sparat; publicera när du är redo.";
127
133
  readonly undone: "Ångrat. Formuläret är som innan.";
@@ -53,6 +53,9 @@ export const COPY = {
53
53
  applied: "Applied to the unsaved form. Save in Payload when you are ready.",
54
54
  applyFailed: "The form could not be updated. It was left unchanged.",
55
55
  draftCreated: "The draft is created and open.",
56
+ completedDraft: "Draft created",
57
+ completedChange: "Change kept in the form",
58
+ completedSavedChange: "Change saved",
56
59
  kept: "Kept. Save in Payload when you are ready.",
57
60
  keptAutosave: "Kept. The draft is saved; publish when you are ready.",
58
61
  undone: "Undone. The form is back as it was.",
@@ -123,6 +126,9 @@ export const COPY = {
123
126
  applied: "Ändringen är införd i formuläret men inte sparad. Spara i Payload när du är redo.",
124
127
  applyFailed: "Formuläret kunde inte uppdateras och lämnades orört.",
125
128
  draftCreated: "Utkastet är skapat och öppnat.",
129
+ completedDraft: "Utkast skapat",
130
+ completedChange: "Ändring behållen i formuläret",
131
+ completedSavedChange: "Ändring sparad",
126
132
  kept: "Behållet. Spara i Payload när du är redo.",
127
133
  keptAutosave: "Behållet. Utkastet är sparat; publicera när du är redo.",
128
134
  undone: "Ångrat. Formuläret är som innan.",
@@ -19,6 +19,7 @@ export type DecideResult = {
19
19
  capability: "draft.create";
20
20
  documentId: string;
21
21
  collection: string;
22
+ title?: string;
22
23
  } | {
23
24
  ok: true;
24
25
  rejected: true;
@@ -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 {
@@ -358,7 +389,9 @@ html[data-theme="dark"] .ea-aside {
358
389
  .ea-composer textarea {
359
390
  width: 100%;
360
391
  box-sizing: border-box;
361
- min-height: 40px;
392
+ min-height: 4.5em;
393
+ max-height: min(12em, 30vh);
394
+ overflow-y: auto;
362
395
  padding: 0;
363
396
  border: 0;
364
397
  background: transparent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simmalugnt-se/payload-editor-assistant",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Context-aware editor assistant plugin for Payload Admin",
5
5
  "keywords": [
6
6
  "payload",