@simmalugnt-se/payload-editor-assistant 0.9.1 → 0.11.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +40 -0
  3. package/dist/batch/targets.d.ts +25 -0
  4. package/dist/batch/targets.js +47 -0
  5. package/dist/capabilities.d.ts +4 -4
  6. package/dist/capabilities.js +3 -0
  7. package/dist/components/BatchPanel.d.ts +9 -0
  8. package/dist/components/BatchPanel.js +161 -0
  9. package/dist/components/EditorAssistantAside.js +4 -3
  10. package/dist/components/EditorAssistantProvider.js +2 -0
  11. package/dist/components/assistant-context.d.ts +11 -0
  12. package/dist/components/assistant-context.js +4 -0
  13. package/dist/components/batch-link.d.ts +5 -0
  14. package/dist/components/batch-link.js +35 -0
  15. package/dist/components/batch-run.d.ts +65 -0
  16. package/dist/components/batch-run.js +67 -0
  17. package/dist/components/copy.d.ts +82 -0
  18. package/dist/components/copy.js +81 -0
  19. package/dist/config/validate.js +3 -0
  20. package/dist/domain/audit.d.ts +2 -0
  21. package/dist/domain/audit.js +1 -0
  22. package/dist/domain/batch.d.ts +74 -0
  23. package/dist/domain/batch.js +342 -0
  24. package/dist/domain/content.d.ts +16 -0
  25. package/dist/domain/content.js +14 -5
  26. package/dist/domain/decide.js +4 -0
  27. package/dist/domain/guard.d.ts +3 -1
  28. package/dist/domain/guard.js +2 -2
  29. package/dist/domain/propose.d.ts +27 -1
  30. package/dist/domain/propose.js +105 -9
  31. package/dist/endpoints/batch.d.ts +8 -0
  32. package/dist/endpoints/batch.js +102 -0
  33. package/dist/form/diff.js +15 -2
  34. package/dist/form/relation-shape.d.ts +9 -0
  35. package/dist/form/relation-shape.js +74 -0
  36. package/dist/package-name.d.ts +3 -2
  37. package/dist/package-name.js +3 -2
  38. package/dist/plugin.js +12 -2
  39. package/dist/runtime/agent.d.ts +4 -2
  40. package/dist/runtime/agent.js +5 -3
  41. package/dist/runtime/context.d.ts +4 -0
  42. package/dist/runtime/context.js +9 -1
  43. package/dist/runtime/system-prompt.js +14 -3
  44. package/dist/runtime/tools.d.ts +15 -2
  45. package/dist/runtime/tools.js +20 -3
  46. package/dist/runtime/voice.d.ts +2 -2
  47. package/dist/runtime/voice.js +7 -3
  48. package/dist/schema/fields.d.ts +3 -0
  49. package/dist/schema/fields.js +2 -0
  50. package/dist/styles/assistant.css +203 -0
  51. package/package.json +1 -1
@@ -1,6 +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
+ import { type ProposalLine, type ProposalPreview } from "../domain/propose.ts";
4
4
  import type { AdminView } from "../view.ts";
5
5
  import type { ChatLanguage } from "./language.ts";
6
6
  export type ToolBridge = {
@@ -22,6 +22,15 @@ export type ToolBridge = {
22
22
  openProposal?: {
23
23
  changedPaths: string[];
24
24
  };
25
+ /**
26
+ * One document of a batch from the work list: the saved document stands in for the form, and a
27
+ * proposal is a `content.update` that may reach only `allowedPaths`. Set by the server, never the
28
+ * client.
29
+ */
30
+ batch?: {
31
+ allowedPaths: string[];
32
+ requestId?: string;
33
+ };
25
34
  };
26
35
  export type ToolContext = {
27
36
  req: PayloadRequest;
@@ -42,8 +51,10 @@ export type ToolSuccess = {
42
51
  summary: string;
43
52
  approvalId: string;
44
53
  nonce: string;
45
- capability: "form.propose" | "draft.create";
54
+ capability: "form.propose" | "draft.create" | "content.update";
46
55
  };
56
+ /** The change list; a batch shows it per document. */
57
+ lines?: ProposalLine[];
47
58
  navigate?: {
48
59
  href: string;
49
60
  label: string;
@@ -71,6 +82,8 @@ export type ToolFailure = {
71
82
  export type ToolOutcome = ToolSuccess | ToolFailure;
72
83
  export declare const TOOL_JSON_SCHEMAS: Record<string, Record<string, unknown>>;
73
84
  export declare const TOOL_DESCRIPTIONS: Record<string, string>;
85
+ /** The tools the model is given for this turn. */
86
+ export declare function toolNamesFor(bridge: ToolBridge): string[];
74
87
  /** Provider APIs reject dots in tool names (`^[a-zA-Z0-9_-]+$`). Capability IDs stay dotted. */
75
88
  export declare function toProviderToolName(capability: string): string;
76
89
  export declare function fromProviderToolName(name: string): string;
@@ -80,6 +80,12 @@ export const TOOL_DESCRIPTIONS = {
80
80
  "admin.navigate": "Navigate Admin to an allowlisted collection, global, or unique document match.",
81
81
  "draft.create": 'Stage creation of one draft document. Requires editor approval. Never publishes. The document starts empty: set top-level fields with {"op":"set","path":"title","value":"Start"}, and add each layout block with {"op":"blocks.insert","path":"layout","index":0,"blockType":"hero","data":{"headline":"Välkommen"}} using only block types and fields from the quoted context. Rich text fields take Markdown (headings, lists, **bold**, [links](https://…)).',
82
82
  };
83
+ /** Tools a batch turn has no use for: it stays on its one saved document. */
84
+ const NOT_IN_BATCH = new Set(["admin.navigate", "draft.create"]);
85
+ /** The tools the model is given for this turn. */
86
+ export function toolNamesFor(bridge) {
87
+ return Object.keys(TOOL_JSON_SCHEMAS).filter((name) => !(bridge.batch && NOT_IN_BATCH.has(name)));
88
+ }
83
89
  /** Provider APIs reject dots in tool names (`^[a-zA-Z0-9_-]+$`). Capability IDs stay dotted. */
84
90
  export function toProviderToolName(capability) {
85
91
  return capability.replaceAll(".", "_");
@@ -194,29 +200,40 @@ export async function executeTool(name, input, ctx) {
194
200
  }
195
201
  case "form.propose":
196
202
  case "draft.create": {
203
+ if (ctx.bridge.batch && name === "draft.create") {
204
+ return { ok: false, capability: name, error: "tool_denied", message: "Not in a batch." };
205
+ }
206
+ const batch = ctx.bridge.batch;
207
+ const capability = batch ? "content.update" : name;
197
208
  const proposed = await proposeChange(ctx.req, ctx.options, {
198
- capability: name,
209
+ capability,
210
+ allowedPaths: batch?.allowedPaths,
211
+ requestId: batch?.requestId,
199
212
  collection: name === "draft.create" ? asString(input.collection) : ctx.bridge.collection,
200
213
  global: name === "form.propose" ? ctx.bridge.global : undefined,
201
214
  documentId: name === "form.propose" ? ctx.bridge.documentId : undefined,
202
215
  locale: ctx.bridge.locale,
203
216
  language: ctx.bridge.uiLanguage ?? ctx.bridge.language,
204
217
  operations: input.operations ?? input.patches ?? input.changes ?? input.ops,
205
- formData: name === "form.propose" ? ctx.formData : undefined,
218
+ formData: name === "form.propose" && !batch ? ctx.formData : undefined,
206
219
  });
207
220
  if (!proposed.ok) {
208
221
  return { ok: false, capability: name, error: proposed.error, message: proposed.message };
209
222
  }
223
+ // Models count characters badly; the count lets them correct a text that misses a length.
224
+ const counted = Object.keys(proposed.characters).length > 0;
210
225
  return {
211
226
  ok: true,
212
227
  capability: name,
228
+ ...(counted ? { data: { characters: proposed.characters } } : {}),
213
229
  pendingApproval: {
214
230
  title: proposed.title,
215
231
  summary: proposed.summary,
216
232
  approvalId: proposed.approvalId,
217
233
  nonce: proposed.nonce,
218
- capability: name,
234
+ capability,
219
235
  },
236
+ ...(batch ? { lines: proposed.lines } : {}),
220
237
  preview: proposed.preview,
221
238
  };
222
239
  }
@@ -2,13 +2,13 @@ export declare function editorFacingText(input: {
2
2
  text: string;
3
3
  locale: string;
4
4
  pendingApproval?: {
5
- capability: "form.propose" | "draft.create";
5
+ capability: "form.propose" | "draft.create" | "content.update";
6
6
  };
7
7
  expectedProposal?: boolean;
8
8
  }): string;
9
9
  export declare function stripInternalSpeech(text: string, locale: string): string;
10
10
  export declare function withoutOverclaims(text: string): string;
11
- export declare function approvalCopy(locale: string, capability: "form.propose" | "draft.create"): string;
11
+ export declare function approvalCopy(locale: string, capability: "form.propose" | "draft.create" | "content.update"): string;
12
12
  export declare function couldNotStageCopy(locale: string): string;
13
13
  /** Tells the editor the content is written in another language than the one they chat in. */
14
14
  export declare function contentLanguageNote(chat: string, contentLocale: string): string | undefined;
@@ -3,9 +3,10 @@ const INTERNAL = /\b(form\.propose|form_propose|draft\.create|draft_create|schem
3
3
  const ENGLISH_META = /\b(despite|therefore|however|the tool|I should|I've|has been issued|appropriate turn|proposed values)\b/i;
4
4
  export function editorFacingText(input) {
5
5
  if (input.pendingApproval) {
6
- // A form change is in the form now, so the model's own words about it can stand, minus any
7
- // claim the panel contradicts. A draft is not created until approved; that keeps fixed copy.
8
- const own = input.pendingApproval.capability === "form.propose"
6
+ // A form change is in the form now, and a batch change shown beside its document, so the
7
+ // model's own words about it can stand, minus any claim the panel contradicts. A draft is not
8
+ // created until approved; that keeps fixed copy.
9
+ const own = input.pendingApproval.capability !== "draft.create"
9
10
  ? withoutOverclaims(stripInternalSpeech(input.text, input.locale))
10
11
  : "";
11
12
  return own || approvalCopy(input.locale, input.pendingApproval.capability);
@@ -39,6 +40,9 @@ export function withoutOverclaims(text) {
39
40
  return kept.length <= MAX_OWN_TEXT ? kept : "";
40
41
  }
41
42
  export function approvalCopy(locale, capability) {
43
+ if (capability === "content.update") {
44
+ return isSwedish(locale) ? "Här är ett förslag." : "Here is a suggestion.";
45
+ }
42
46
  if (isSwedish(locale)) {
43
47
  return capability === "draft.create"
44
48
  ? "Utkastet ligger i panelen. Godkänn det så skapas det."
@@ -7,6 +7,9 @@ export type FieldNode = {
7
7
  localized?: boolean;
8
8
  relationTo?: string[];
9
9
  hasMany?: boolean;
10
+ /** How many a `hasMany` field takes. */
11
+ minRows?: number;
12
+ maxRows?: number;
10
13
  options?: string[];
11
14
  label?: string;
12
15
  description?: string;
@@ -55,6 +55,8 @@ function walkField(field, parentPath, allowlist, denyFields) {
55
55
  required: field.required,
56
56
  localized: field.localized,
57
57
  hasMany: field.hasMany,
58
+ ...(field.hasMany && typeof field.minRows === "number" ? { minRows: field.minRows } : {}),
59
+ ...(field.hasMany && typeof field.maxRows === "number" ? { maxRows: field.maxRows } : {}),
58
60
  relationTo: normalizeRelationTo(field.relationTo),
59
61
  options: optionValues(field.options),
60
62
  label: asLabel(field.label) ?? field.name,
@@ -432,3 +432,206 @@ html[data-theme="dark"] .ea-aside {
432
432
  html[data-theme="dark"] .ea-trigger--open .ea-trigger__icon {
433
433
  color: #60a5fa;
434
434
  }
435
+
436
+ /* Batch: many documents from the work list, one card each. The head and buttons stay put and the
437
+ cards scroll between them. */
438
+ .ea-batch {
439
+ display: flex;
440
+ flex-direction: column;
441
+ flex: 1;
442
+ min-height: 0;
443
+ }
444
+
445
+ .ea-batch__head {
446
+ flex-shrink: 0;
447
+ padding: calc(var(--base) * 0.6) var(--base) calc(var(--base) * 0.4);
448
+ font-size: 13px;
449
+ line-height: 1.5;
450
+ }
451
+
452
+ .ea-batch__head h3 {
453
+ margin: 0 0 calc(var(--base) * 0.3);
454
+ font-size: 14px;
455
+ font-weight: 600;
456
+ }
457
+
458
+ .ea-batch__head p {
459
+ margin: 0 0 calc(var(--base) * 0.3);
460
+ color: var(--theme-elevation-700);
461
+ }
462
+
463
+ .ea-batch__task span {
464
+ font-weight: 600;
465
+ color: var(--theme-text);
466
+ }
467
+
468
+ .ea-batch__cards {
469
+ flex: 1;
470
+ min-height: 0;
471
+ overflow-y: auto;
472
+ margin: 0;
473
+ padding: 0 var(--base);
474
+ list-style: none;
475
+ }
476
+
477
+ .ea-batch__card {
478
+ margin-bottom: calc(var(--base) * 0.5);
479
+ padding: calc(var(--base) * 0.5) calc(var(--base) * 0.6);
480
+ border: 1px solid var(--theme-elevation-150);
481
+ border-radius: var(--style-radius-m);
482
+ background: var(--theme-elevation-50);
483
+ font-size: 13px;
484
+ line-height: 1.45;
485
+ }
486
+
487
+ .ea-batch__card--proposed {
488
+ border-inline-start: 3px solid var(--ea-accent);
489
+ }
490
+
491
+ .ea-batch__card--saved,
492
+ .ea-batch__card--draft {
493
+ border-inline-start: 3px solid var(--theme-success-500, #16a34a);
494
+ }
495
+
496
+ .ea-batch__card--failed,
497
+ .ea-batch__card--stale,
498
+ .ea-batch__card--locked {
499
+ border-inline-start: 3px solid var(--theme-warning-500, #d97706);
500
+ }
501
+
502
+ .ea-batch__card--unavailable,
503
+ .ea-batch__card--rejected,
504
+ .ea-batch__card--nothing {
505
+ opacity: 0.75;
506
+ }
507
+
508
+ .ea-batch__card-head {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: calc(var(--base) * 0.4);
512
+ min-width: 0;
513
+ }
514
+
515
+ .ea-batch__card-head input[type="checkbox"] {
516
+ flex-shrink: 0;
517
+ margin: 0;
518
+ accent-color: var(--ea-accent);
519
+ }
520
+
521
+ .ea-batch__thumb {
522
+ flex-shrink: 0;
523
+ width: 32px;
524
+ height: 32px;
525
+ object-fit: cover;
526
+ border-radius: var(--style-radius-s);
527
+ background: var(--theme-elevation-100);
528
+ }
529
+
530
+ .ea-batch__title {
531
+ flex: 1;
532
+ min-width: 0;
533
+ overflow: hidden;
534
+ font-weight: 600;
535
+ text-overflow: ellipsis;
536
+ white-space: nowrap;
537
+ }
538
+
539
+ .ea-batch__pill {
540
+ margin-inline-start: 0.4em;
541
+ padding: 0 0.4em;
542
+ border-radius: var(--style-radius-s);
543
+ background: var(--theme-elevation-100);
544
+ font-size: 11px;
545
+ font-weight: 400;
546
+ }
547
+
548
+ .ea-batch__status {
549
+ flex-shrink: 0;
550
+ color: var(--theme-elevation-600);
551
+ font-size: 12px;
552
+ }
553
+
554
+ .ea-batch__lines {
555
+ margin: calc(var(--base) * 0.4) 0 0;
556
+ padding-left: 1.1em;
557
+ }
558
+
559
+ .ea-batch__edit {
560
+ display: flex;
561
+ flex-direction: column;
562
+ gap: 2px;
563
+ margin-inline-start: -1.1em;
564
+ list-style: none;
565
+ }
566
+
567
+ .ea-batch__label {
568
+ font-weight: 600;
569
+ }
570
+
571
+ .ea-batch__before {
572
+ color: var(--theme-elevation-600);
573
+ overflow-wrap: anywhere;
574
+ }
575
+
576
+ .ea-batch__edit textarea {
577
+ box-sizing: border-box;
578
+ width: 100%;
579
+ padding: calc(var(--base) * 0.3) calc(var(--base) * 0.4);
580
+ border: 1px solid var(--theme-elevation-200);
581
+ border-radius: var(--style-radius-s);
582
+ background: var(--theme-input-bg, var(--theme-elevation-0));
583
+ color: var(--theme-text);
584
+ font: inherit;
585
+ resize: vertical;
586
+ /* Grows with the text where the browser can; two rows otherwise. */
587
+ field-sizing: content;
588
+ min-height: calc(2lh + calc(var(--base) * 0.6));
589
+ }
590
+
591
+ .ea-batch__count {
592
+ align-self: flex-end;
593
+ color: var(--theme-elevation-500);
594
+ font-size: 11px;
595
+ }
596
+
597
+ .ea-batch__text,
598
+ .ea-batch__hint {
599
+ margin: calc(var(--base) * 0.3) 0 0;
600
+ color: var(--theme-elevation-700);
601
+ }
602
+
603
+ .ea-batch__hint {
604
+ font-size: 12px;
605
+ }
606
+
607
+ .ea-batch__retry {
608
+ display: flex;
609
+ gap: calc(var(--base) * 0.3);
610
+ margin-top: calc(var(--base) * 0.4);
611
+ }
612
+
613
+ .ea-batch__retry input {
614
+ flex: 1;
615
+ min-width: 0;
616
+ padding: calc(var(--base) * 0.2) calc(var(--base) * 0.4);
617
+ border: 1px solid var(--theme-elevation-200);
618
+ border-radius: var(--style-radius-s);
619
+ background: var(--theme-input-bg, var(--theme-elevation-0));
620
+ color: var(--theme-text);
621
+ font: inherit;
622
+ }
623
+
624
+ .ea-batch__card-actions {
625
+ display: flex;
626
+ align-items: center;
627
+ gap: calc(var(--base) * 0.6);
628
+ margin-top: calc(var(--base) * 0.3);
629
+ font-size: 12px;
630
+ }
631
+
632
+ .ea-batch__actions {
633
+ flex-shrink: 0;
634
+ margin: 0;
635
+ padding: calc(var(--base) * 0.5) var(--base) calc(var(--base) * 0.8);
636
+ border-top: 1px solid var(--theme-elevation-100);
637
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simmalugnt-se/payload-editor-assistant",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Context-aware editor assistant plugin for Payload Admin",
5
5
  "keywords": [
6
6
  "payload",