@anthusai/papyrus 5.57.0 → 5.58.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 (36) hide show
  1. package/amplify/data/resource.js +25 -3
  2. package/amplify/data/resource.ts +25 -3
  3. package/amplify/data/schema.js +25 -3
  4. package/amplify/data/schema.ts +25 -3
  5. package/amplify/functions/category-action/handler.ts +51 -125
  6. package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
  7. package/components/conversation-thread.tsx +27 -12
  8. package/components/conversations-memory-source.ts +46 -0
  9. package/components/conversations-network-source.ts +40 -0
  10. package/components/conversations-page.tsx +14 -0
  11. package/components/conversations-source.ts +24 -0
  12. package/components/conversations-view.tsx +319 -0
  13. package/components/decision-control.tsx +307 -0
  14. package/components/needs-me-bodies.tsx +4 -3
  15. package/components/needs-me-feed.tsx +8 -14
  16. package/components/news-desk-taxonomy-client.ts +83 -14
  17. package/components/newsroom-organizer.css +24 -0
  18. package/components/notification-cards.tsx +5 -4
  19. package/components/reference-feed.tsx +60 -42
  20. package/components/reference-review-card.tsx +8 -134
  21. package/components/story-reporting-trail-client.ts +4 -3
  22. package/components/story-reporting-trail.tsx +8 -0
  23. package/components/topic-steering-workspace.tsx +40 -782
  24. package/components/use-reference-ratings.ts +11 -7
  25. package/lib/category-repository.ts +7 -0
  26. package/lib/conversation-feed.ts +96 -0
  27. package/lib/cyclotron-suggestions.ts +18 -36
  28. package/lib/local-preview-seed.ts +16 -27
  29. package/lib/reference-feed-keys.ts +69 -0
  30. package/lib/reference-policy.ts +83 -11
  31. package/lib/reference-review-details.ts +21 -57
  32. package/lib/relevance-decisions.ts +6 -2
  33. package/lib/story-reporting-trail.ts +28 -2
  34. package/package.json +9 -2
  35. package/routes.manifest.json +1 -1
  36. package/components/review-rating-grid.tsx +0 -190
@@ -17,6 +17,15 @@ export type TrailVersion = {
17
17
  updatedAt: string | null;
18
18
  };
19
19
 
20
+ /** One stage move of the story, read from the latest Item version's `metadata.stageMoves` (not from Messages). */
21
+ export type TrailStageMove = {
22
+ at: string;
23
+ from: string | null;
24
+ to: string;
25
+ by: string | null;
26
+ reason: string | null;
27
+ };
28
+
20
29
  export type TrailAssignmentEvent = {
21
30
  id: string;
22
31
  eventType: string;
@@ -89,12 +98,29 @@ export type StoryReportingTrail = {
89
98
  threads: TrailThread[];
90
99
  references: TrailReference[];
91
100
  attachments: TrailAttachment[];
101
+ stageMoves: TrailStageMove[];
92
102
  };
93
103
 
94
- export const EMPTY_STORY_REPORTING_TRAIL: StoryReportingTrail = { versions: [], assignments: [], threads: [], references: [], attachments: [] };
104
+ export const EMPTY_STORY_REPORTING_TRAIL: StoryReportingTrail = { versions: [], assignments: [], threads: [], references: [], attachments: [], stageMoves: [] };
105
+
106
+ /** The stage moves recorded on an Item version's metadata, oldest first. Anything that is not a well-formed move is skipped. */
107
+ export function stageMovesOf(metadata: unknown): TrailStageMove[] {
108
+ let parsed: unknown = metadata;
109
+ if (typeof metadata === "string") {
110
+ try { parsed = JSON.parse(metadata); } catch { return []; }
111
+ }
112
+ const moves = typeof parsed === "object" && parsed ? (parsed as Record<string, unknown>).stageMoves : null;
113
+ if (!Array.isArray(moves)) return [];
114
+ return moves.flatMap(entry => {
115
+ if (typeof entry !== "object" || !entry) return [];
116
+ const move = entry as Record<string, unknown>;
117
+ if (typeof move.at !== "string" || typeof move.to !== "string") return [];
118
+ return [{ at: move.at, from: typeof move.from === "string" ? move.from : null, to: move.to, by: typeof move.by === "string" ? move.by : null, reason: typeof move.reason === "string" ? move.reason : null }];
119
+ });
120
+ }
95
121
 
96
122
  export function storyReportingTrailIsEmpty(trail: StoryReportingTrail): boolean {
97
- return trail.assignments.length === 0 && trail.threads.length === 0 && trail.references.length === 0 && trail.attachments.length === 0 && trail.versions.length <= 1;
123
+ return trail.assignments.length === 0 && trail.threads.length === 0 && trail.stageMoves.length === 0 && trail.references.length === 0 && trail.attachments.length === 0 && trail.versions.length <= 1;
98
124
  }
99
125
 
100
126
  export type StoryFeedEntry =
package/package.json CHANGED
@@ -83,7 +83,13 @@
83
83
  "./components/conversation-context": "./components/conversation-context.tsx",
84
84
  "./components/conversation-provider": "./components/conversation-provider.tsx",
85
85
  "./components/conversation-thread": "./components/conversation-thread.tsx",
86
+ "./components/conversations-memory-source": "./components/conversations-memory-source.ts",
87
+ "./components/conversations-network-source": "./components/conversations-network-source.ts",
88
+ "./components/conversations-page": "./components/conversations-page.tsx",
89
+ "./components/conversations-source": "./components/conversations-source.ts",
90
+ "./components/conversations-view": "./components/conversations-view.tsx",
86
91
  "./components/curation-delegation-client": "./components/curation-delegation-client.ts",
92
+ "./components/decision-control": "./components/decision-control.tsx",
87
93
  "./components/dev-sandbox-editor-sign-in": "./components/dev-sandbox-editor-sign-in.tsx",
88
94
  "./components/editorial-checks-admin-panel": "./components/editorial-checks-admin-panel.tsx",
89
95
  "./components/editorial-finding-card": "./components/editorial-finding-card.tsx",
@@ -164,7 +170,6 @@
164
170
  "./components/reference-stories": "./components/reference-stories.tsx",
165
171
  "./components/references-page-loading": "./components/references-page-loading.tsx",
166
172
  "./components/references-prefetch": "./components/references-prefetch.ts",
167
- "./components/review-rating-grid": "./components/review-rating-grid.tsx",
168
173
  "./components/settings-page": "./components/settings-page.tsx",
169
174
  "./components/signup-entries-panel": "./components/signup-entries-panel.tsx",
170
175
  "./components/staging-banner": "./components/staging-banner.tsx",
@@ -234,6 +239,7 @@
234
239
  "./lib/content-repository": "./lib/content-repository.ts",
235
240
  "./lib/content-source-context": "./lib/content-source-context.ts",
236
241
  "./lib/content-types": "./lib/content-types.ts",
242
+ "./lib/conversation-feed": "./lib/conversation-feed.ts",
237
243
  "./lib/conversation": "./lib/conversation.ts",
238
244
  "./lib/curation-client": "./lib/curation-client.ts",
239
245
  "./lib/curation-delegation-helpers": "./lib/curation-delegation-helpers.ts",
@@ -348,6 +354,7 @@
348
354
  "./lib/reference-capture": "./lib/reference-capture.ts",
349
355
  "./lib/reference-display": "./lib/reference-display.ts",
350
356
  "./lib/reference-feed-cache": "./lib/reference-feed-cache.ts",
357
+ "./lib/reference-feed-keys": "./lib/reference-feed-keys.ts",
351
358
  "./lib/reference-feed-ranking": "./lib/reference-feed-ranking.ts",
352
359
  "./lib/reference-policy": "./lib/reference-policy.ts",
353
360
  "./lib/reference-preview": "./lib/reference-preview.ts",
@@ -510,7 +517,7 @@
510
517
  "optional": true
511
518
  }
512
519
  },
513
- "version": "5.57.0",
520
+ "version": "5.58.0",
514
521
  "dependencies": {
515
522
  "@aws-amplify/adapter-nextjs": "^1.7.3",
516
523
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.57.0",
2
+ "version": "5.58.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",
@@ -1,190 +0,0 @@
1
- "use client";
2
-
3
- import { CheckIcon, CircleCheckIcon, CircleXIcon, Loader2Icon, XIcon } from "lucide-react";
4
- import { UNRECORDED_CONFIDENCE } from "../lib/reference-feed-ranking";
5
- import { cn } from "../lib/utils";
6
- import type { CardPrediction, ReviewChoice } from "./reference-review-card";
7
-
8
- /**
9
- * The rating area of a card: one grid, three columns (label | Accept | Reject), and the two rows share its columns exactly.
10
- * Cyclotron [ Accept 96% ] [ Reject ] read only; the predicted side is highlighted with its confidence
11
- * [====== confidence bar, centred at 50% ======]
12
- * You [ Accept ] [ Reject ] the real buttons, each directly under its matching cell
13
- * Feedback is never colour alone: before you answer the predicted cell is neutral-accent with a check or cross; after, it turns
14
- * green with a CircleCheck (Match) or red with a CircleX (Miss), and the confidence bar tints the same way. Your chosen button is
15
- * filled. Flat tones and rounded shapes only, no borders.
16
- * The grid sizes itself to its own width (a container query), not the window's: on a narrow card the label column shrinks to its
17
- * word, the padding tightens, and the confidence wraps under "Accept" instead of spilling out of its cell.
18
- */
19
-
20
- export type RatingFeedback = { phase: "idle" | "saving" | "saved" | "error"; message?: string };
21
-
22
- /** P(accept) from a prediction: its confidence is in the side it chose; an unrecorded confidence is a mild lean. */
23
- export function predictionAcceptProbability(prediction: Extract<CardPrediction, { state: "ready" }>): number {
24
- const confidence = prediction.confidence ?? UNRECORDED_CONFIDENCE;
25
- return prediction.choice === "accept" ? confidence : 1 - confidence;
26
- }
27
-
28
- /** How far, in percent of the whole bar, the fill reaches from the centre: |P(accept) - 0.5| * 2, over the two halves. */
29
- export function confidenceFillPercent(probability: number): number {
30
- return Math.min(100, Math.abs(probability - 0.5) * 2 * 100);
31
- }
32
-
33
- const WORD: Record<ReviewChoice, string> = { accept: "Accept", reject: "Reject" };
34
-
35
- const TONE = {
36
- match: "bg-emerald-600/20 text-emerald-900 dark:bg-emerald-400/20 dark:text-emerald-100",
37
- miss: "bg-red-600/20 text-red-900 dark:bg-red-400/20 dark:text-red-100",
38
- } as const;
39
- const FILL = { match: "bg-emerald-600 dark:bg-emerald-400", miss: "bg-red-600 dark:bg-red-400", neutral: "bg-[var(--primary)]" } as const;
40
-
41
- export function RatingGrid({
42
- answer,
43
- decided = answer,
44
- disabled = false,
45
- disabledReason = null,
46
- feedback = { phase: "idle" },
47
- note = null,
48
- onChoose,
49
- prediction,
50
- showPrediction = true,
51
- size = "compact",
52
- statusOverlay = false,
53
- surface = "footer",
54
- }: {
55
- /** The side the buttons show as chosen (it may be a pending choice that is still saving). */
56
- answer: ReviewChoice | null;
57
- /** The saved human answer, which the Match / Miss verdict compares against. */
58
- decided?: ReviewChoice | null;
59
- disabled?: boolean;
60
- disabledReason?: string | null;
61
- feedback?: RatingFeedback;
62
- /** A short line shown in the status row beside the Saved tick (for example the reason). It never adds a row. */
63
- note?: string | null;
64
- onChoose: (choice: ReviewChoice) => void;
65
- prediction: CardPrediction;
66
- /** False for a card with nothing to predict (an AI draft): the Cyclotron row and the "You" label are left out and only the two buttons remain. */
67
- showPrediction?: boolean;
68
- size?: "compact" | "large";
69
- /** Lay the Saved / note line over the bottom padding of the nearest positioned ancestor, so it adds no height under the buttons. */
70
- statusOverlay?: boolean;
71
- /** The tone the grid sits on, so the bar's centre notch matches it. */
72
- surface?: "footer" | "card";
73
- }) {
74
- const ready = prediction.state === "ready" ? prediction : null;
75
- const verdict = ready && decided ? (ready.choice === decided ? "match" : "miss") : null;
76
- const probability = ready ? predictionAcceptProbability(ready) : 0.5;
77
- const half = confidenceFillPercent(probability) / 2; // percent of the full bar width, on one side of the centre
78
- const leansAccept = probability >= 0.5;
79
- const confidenceText = ready && ready.confidence !== null ? `${Math.round(ready.confidence * 100)}%` : null;
80
- const cell = size === "large" ? "min-h-14 text-base" : "min-h-12 text-sm";
81
- const label = size === "large" ? "text-sm" : "text-xs";
82
-
83
- return (
84
- <div
85
- className={cn(
86
- "@container/rating grid gap-x-2 gap-y-2",
87
- showPrediction ? "grid-cols-[auto_minmax(0,1fr)_minmax(0,1fr)] @max-[17rem]/rating:gap-x-1.5" : "grid-cols-2",
88
- )}
89
- data-rating-grid
90
- >
91
- {showPrediction ? <div className="col-span-3 grid grid-cols-subgrid items-stretch gap-y-2" data-review-row="cyclotron">
92
- <span className={cn("self-center font-semibold", label)}>Cyclotron</span>
93
- {(["accept", "reject"] as const).map((choice) => {
94
- const predicted = ready?.choice === choice;
95
- const Icon = predicted ? (verdict === "match" ? CircleCheckIcon : verdict === "miss" ? CircleXIcon : choice === "accept" ? CheckIcon : XIcon) : null;
96
- return (
97
- <span
98
- className={cn(
99
- "flex min-w-0 flex-col items-center justify-center rounded-[16px] px-2 py-1.5 text-center @max-[17rem]/rating:px-1",
100
- cell,
101
- !predicted && "bg-[var(--feed-control)]/45 text-muted-foreground",
102
- predicted && !verdict && "bg-[var(--feed-control)] font-semibold text-[var(--primary)]",
103
- predicted && verdict && cn("font-semibold", TONE[verdict]),
104
- )}
105
- data-cyclotron-choice={choice}
106
- data-predicted={predicted || undefined}
107
- data-verdict={predicted && verdict ? verdict : undefined}
108
- key={choice}
109
- >
110
- {/* Wraps rather than overflows: when the cell is narrow, the confidence moves onto its own line. */}
111
- <span className="flex max-w-full flex-wrap items-center justify-center gap-x-1.5 leading-tight">
112
- <span className="inline-flex items-center gap-1">
113
- {Icon ? <Icon aria-hidden="true" className="size-4 shrink-0" /> : null}
114
- {WORD[choice]}
115
- </span>
116
- {/* The confidence and the verdict share one line, so a vote never makes the cell (or the card) taller. */}
117
- {predicted && (confidenceText || verdict) ? (
118
- <small className="inline-flex items-center gap-1 whitespace-nowrap text-xs">
119
- {confidenceText ? <span className="font-normal opacity-80" data-cyclotron-confidence>{confidenceText}</span> : null}
120
- {confidenceText && verdict ? <span aria-hidden="true" className="opacity-60">·</span> : null}
121
- {verdict ? <span className="font-medium" data-verdict-word>{verdict === "match" ? "Match" : "Miss"}</span> : null}
122
- </small>
123
- ) : null}
124
- </span>
125
- {predicted && verdict ? <span className="sr-only">{verdict === "match" ? ", matches your answer" : ", differs from your answer"}</span> : null}
126
- </span>
127
- );
128
- })}
129
- <div className="col-span-2 col-start-2 flex flex-col justify-center" data-confidence>
130
- <div
131
- aria-label={ready ? `Cyclotron confidence: leans ${leansAccept ? "Accept" : "Reject"}, ${Math.round(confidenceFillPercent(probability))} percent of the way` : "No confidence"}
132
- className="relative h-2.5 w-full overflow-hidden rounded-full bg-[var(--feed-control)]"
133
- data-confidence-bar
134
- data-confidence-direction={ready ? (leansAccept ? "accept" : "reject") : "none"}
135
- role="img"
136
- >
137
- {ready ? (
138
- <span
139
- className={cn("absolute inset-y-0 rounded-full", FILL[verdict ?? "neutral"])}
140
- data-confidence-fill
141
- data-fill-percent={Math.round(half * 2)}
142
- style={{ left: leansAccept ? `${50 - half}%` : "50%", width: `${half}%` }}
143
- />
144
- ) : null}
145
- <span aria-hidden="true" className={cn("absolute inset-y-0 left-1/2 w-1 -translate-x-1/2", surface === "footer" ? "bg-[var(--feed-footer)]" : "bg-[var(--feed-card)]")} />
146
- </div>
147
- {/* Screen-reader status only: a visible line under the bar would push it off the middle between the Cyclotron and You rows. */}
148
- <small className="sr-only" data-cyclotron-state={prediction.state === "ready" ? undefined : prediction.state}>
149
- {prediction.state === "ready" ? "" : prediction.state === "loading" ? "Loading…" : prediction.state === "error" ? "Prediction unavailable" : "No prediction"}
150
- </small>
151
- </div>
152
- </div> : null}
153
-
154
- <div className={cn("grid grid-cols-subgrid items-stretch", showPrediction ? "col-span-3" : "col-span-2")} data-review-row="you">
155
- {showPrediction ? <span className={cn("self-center font-semibold", label)}>You</span> : null}
156
- {(["accept", "reject"] as const).map((choice) => {
157
- const on = answer === choice;
158
- const Icon = choice === "accept" ? CheckIcon : XIcon;
159
- return (
160
- <button
161
- aria-disabled={disabled ? true : undefined}
162
- aria-pressed={on}
163
- className={cn(
164
- "inline-flex min-w-0 items-center justify-center gap-1.5 rounded-[16px] px-2 font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring @max-[17rem]/rating:gap-1 @max-[17rem]/rating:px-1",
165
- cell,
166
- on ? "bg-[var(--primary)] font-semibold text-[var(--primary-foreground)]" : "bg-[var(--feed-control)] text-foreground hover:bg-[var(--feed-control)]/70",
167
- disabled && !on && "opacity-60",
168
- )}
169
- data-review-choice={choice}
170
- key={choice}
171
- onClick={() => onChoose(choice)}
172
- type="button"
173
- >
174
- <Icon aria-hidden="true" className={cn("size-4", !on && "opacity-60")} />
175
- {WORD[choice]}
176
- </button>
177
- );
178
- })}
179
- </div>
180
-
181
- <div className={cn("flex flex-wrap items-center gap-x-3 text-xs text-muted-foreground", statusOverlay ? "absolute inset-x-4 bottom-0 h-4 overflow-hidden leading-4" : cn("min-h-5", showPrediction ? "col-span-3" : "col-span-2"))} data-feed-saved role="status">
182
- {note ? <span data-feed-reason>{note}</span> : null}
183
- {feedback.phase === "saving" ? <span className="inline-flex items-center gap-1"><Loader2Icon aria-hidden="true" className="size-3.5 animate-spin" />Saving…</span> : null}
184
- {feedback.phase === "saved" ? <span className="inline-flex items-center gap-1" data-review-saved><CheckIcon aria-hidden="true" className="size-3.5" />Saved</span> : null}
185
- </div>
186
- {disabledReason ? <p className={cn("text-xs text-muted-foreground", showPrediction ? "col-span-3" : "col-span-2")} data-review-disabled-reason>{disabledReason}</p> : null}
187
- {feedback.phase === "error" ? <p className={cn("text-xs text-destructive", showPrediction ? "col-span-3" : "col-span-2")} role="alert">{feedback.message}</p> : null}
188
- </div>
189
- );
190
- }