@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.
- package/amplify/data/resource.js +25 -3
- package/amplify/data/resource.ts +25 -3
- package/amplify/data/schema.js +25 -3
- package/amplify/data/schema.ts +25 -3
- package/amplify/functions/category-action/handler.ts +51 -125
- package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
- package/components/conversation-thread.tsx +27 -12
- package/components/conversations-memory-source.ts +46 -0
- package/components/conversations-network-source.ts +40 -0
- package/components/conversations-page.tsx +14 -0
- package/components/conversations-source.ts +24 -0
- package/components/conversations-view.tsx +319 -0
- package/components/decision-control.tsx +307 -0
- package/components/needs-me-bodies.tsx +4 -3
- package/components/needs-me-feed.tsx +8 -14
- package/components/news-desk-taxonomy-client.ts +83 -14
- package/components/newsroom-organizer.css +24 -0
- package/components/notification-cards.tsx +5 -4
- package/components/reference-feed.tsx +60 -42
- package/components/reference-review-card.tsx +8 -134
- package/components/story-reporting-trail-client.ts +4 -3
- package/components/story-reporting-trail.tsx +8 -0
- package/components/topic-steering-workspace.tsx +40 -782
- package/components/use-reference-ratings.ts +11 -7
- package/lib/category-repository.ts +7 -0
- package/lib/conversation-feed.ts +96 -0
- package/lib/cyclotron-suggestions.ts +18 -36
- package/lib/local-preview-seed.ts +16 -27
- package/lib/reference-feed-keys.ts +69 -0
- package/lib/reference-policy.ts +83 -11
- package/lib/reference-review-details.ts +21 -57
- package/lib/relevance-decisions.ts +6 -2
- package/lib/story-reporting-trail.ts +28 -2
- package/package.json +9 -2
- package/routes.manifest.json +1 -1
- package/components/review-rating-grid.tsx +0 -190
|
@@ -9,7 +9,7 @@ import { cannotMoveReason, type DraftChoice } from "../lib/review-drafts";
|
|
|
9
9
|
import { answerWords, formatWhen, modeLabel, nextText, steeringText, subjectHref, subjectLabel, type ReviewRequest } from "../lib/review-requests";
|
|
10
10
|
import type { ReferencePreview } from "../lib/reference-preview";
|
|
11
11
|
import { FEED_SURFACE_PROPS, FeedImage, PILL, SourceMark } from "./reference-feed";
|
|
12
|
-
import {
|
|
12
|
+
import { DecisionControl } from "./decision-control";
|
|
13
13
|
|
|
14
14
|
/** What the editor typed with an answer. */
|
|
15
15
|
export type DecisionExtras = { why: string; notThisKind: boolean };
|
|
@@ -109,9 +109,9 @@ function DecisionFooter({ busy, decision, disabledReason, error, kind, lockedNot
|
|
|
109
109
|
{locked ? (
|
|
110
110
|
<>
|
|
111
111
|
<DecidedLine choice={decision.choice} notThisKind={decision.notThisKind} why={decision.why} />
|
|
112
|
-
<
|
|
112
|
+
<DecisionControl
|
|
113
113
|
answer={decision.choice} disabled disabledReason={null} feedback={{ phase: "saved" }} onChoose={() => undefined}
|
|
114
|
-
prediction={{ state: "none" }} showPrediction={false} surface="footer"
|
|
114
|
+
prediction={{ state: "none" }} showPrediction={false} showReason={false} surface="footer"
|
|
115
115
|
/>
|
|
116
116
|
<p className="text-xs text-muted-foreground" data-review-disabled-reason>{lockedNote}</p>
|
|
117
117
|
</>
|
|
@@ -132,7 +132,7 @@ function DecisionFooter({ busy, decision, disabledReason, error, kind, lockedNot
|
|
|
132
132
|
</button>
|
|
133
133
|
<span className="text-xs text-muted-foreground">Optional. Counts if you reject.</span>
|
|
134
134
|
</div>
|
|
135
|
-
<
|
|
135
|
+
<DecisionControl
|
|
136
136
|
answer={busy ? pending : null}
|
|
137
137
|
disabled={Boolean(disabledReason)}
|
|
138
138
|
disabledReason={disabledReason}
|
|
@@ -140,6 +140,7 @@ function DecisionFooter({ busy, decision, disabledReason, error, kind, lockedNot
|
|
|
140
140
|
onChoose={choose}
|
|
141
141
|
prediction={{ state: "none" }}
|
|
142
142
|
showPrediction={false}
|
|
143
|
+
showReason={false}
|
|
143
144
|
surface="footer"
|
|
144
145
|
/>
|
|
145
146
|
</>
|
|
@@ -20,7 +20,7 @@ import { referenceDisplayTitle } from "../lib/newsroom-references";
|
|
|
20
20
|
import type { RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
21
21
|
import { cn } from "../lib/utils";
|
|
22
22
|
import { predictionFromDecision, type ReviewChoice } from "./reference-review-card";
|
|
23
|
-
import {
|
|
23
|
+
import { DecisionControl } from "./decision-control";
|
|
24
24
|
import { CaptureBadge } from "./reference-capture-panel";
|
|
25
25
|
import { AutoAcceptedBadge } from "./auto-accepted-badge";
|
|
26
26
|
import { useRatingStore, type RatingController } from "./use-reference-ratings";
|
|
@@ -202,6 +202,58 @@ export function FeedImage({ host, lead, name, src }: { host: string | null; lead
|
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The standard Reference header: its picture, source, headline, dek and age. The feed card wraps it in the button that opens the
|
|
207
|
+
* Reference, and the Messages page shows it under a conversation as the thing the conversation is about. No decision controls.
|
|
208
|
+
*/
|
|
209
|
+
export function ReferenceCardHeader({ capture, lead = false, messageCount = 0, now, preview, reference, titleId }: {
|
|
210
|
+
capture?: CaptureStatus | undefined;
|
|
211
|
+
lead?: boolean;
|
|
212
|
+
messageCount?: number;
|
|
213
|
+
now: number;
|
|
214
|
+
preview: ReferencePreview | undefined;
|
|
215
|
+
reference: ReferenceRecord;
|
|
216
|
+
titleId?: string;
|
|
217
|
+
}) {
|
|
218
|
+
const host = referenceSourceHost(reference);
|
|
219
|
+
const site = preview?.siteName ?? host ?? "Unknown source";
|
|
220
|
+
const title = referenceDisplayTitle(reference);
|
|
221
|
+
const dek = referenceDek(reference, preview);
|
|
222
|
+
const showDek = dekAddsInformation(title, dek, preview?.imageUrl ?? null);
|
|
223
|
+
const time = referenceDisplayTime(reference);
|
|
224
|
+
const age = formatFeedAge(time, now);
|
|
225
|
+
return (
|
|
226
|
+
<>
|
|
227
|
+
<FeedImage host={host} lead={lead} name={site} src={preview?.imageUrl ?? null} />
|
|
228
|
+
<span className={cn("flex flex-1 flex-col gap-2.5", lead ? "p-5 @[36rem]:p-7" : "p-5")}>
|
|
229
|
+
<span className="flex items-center gap-2 text-xs font-medium text-muted-foreground" data-feed-source>
|
|
230
|
+
<SourceMark host={host} />
|
|
231
|
+
<span className="truncate">{site}</span>
|
|
232
|
+
</span>
|
|
233
|
+
<span className={cn("font-bold leading-[1.15] tracking-tight text-foreground", lead ? "line-clamp-3 text-2xl @[36rem]:text-3xl @[60rem]:text-4xl" : "line-clamp-3 text-xl")} data-feed-title id={titleId}>
|
|
234
|
+
{title}
|
|
235
|
+
</span>
|
|
236
|
+
{showDek ? <span className={cn("text-muted-foreground", lead ? "line-clamp-3 text-base" : "line-clamp-2 text-sm")} data-feed-dek>{dek}</span> : null}
|
|
237
|
+
<span className="mt-auto flex flex-wrap items-center gap-x-2 gap-y-1 pt-1 text-xs text-muted-foreground">
|
|
238
|
+
<span>
|
|
239
|
+
{age ? <time dateTime={new Date(time ?? now).toISOString()} title={new Date(time ?? now).toLocaleString()}>{age}</time> : null}
|
|
240
|
+
{age && host ? <span aria-hidden="true"> · </span> : null}
|
|
241
|
+
{host}
|
|
242
|
+
</span>
|
|
243
|
+
{capture ? <CaptureBadge status={capture} /> : null}
|
|
244
|
+
{messageCount > 0 ? (
|
|
245
|
+
<span aria-label={messageCountLabel(messageCount)} className="inline-flex items-center gap-1 tabular-nums" data-feed-message-count={messageCount} role="img">
|
|
246
|
+
<MessageSquareIcon aria-hidden="true" className="size-3.5" />
|
|
247
|
+
{messageCount}
|
|
248
|
+
</span>
|
|
249
|
+
) : null}
|
|
250
|
+
{isAutoAccepted(reference) ? <AutoAcceptedBadge actor={reference.curationStatusUpdatedBy} reason={reference.curationStatusReason} /> : null}
|
|
251
|
+
</span>
|
|
252
|
+
</span>
|
|
253
|
+
</>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
205
257
|
export function FeedCard({
|
|
206
258
|
capture,
|
|
207
259
|
messageCount = 0,
|
|
@@ -283,60 +335,26 @@ export function FeedCard({
|
|
|
283
335
|
onClick={onOpen}
|
|
284
336
|
type="button"
|
|
285
337
|
>
|
|
286
|
-
<
|
|
287
|
-
<span className={cn("flex flex-1 flex-col gap-2.5", lead ? "p-5 @[36rem]:p-7" : "p-5")}>
|
|
288
|
-
<span className="flex items-center gap-2 text-xs font-medium text-muted-foreground" data-feed-source>
|
|
289
|
-
<SourceMark host={host} />
|
|
290
|
-
<span className="truncate">{site}</span>
|
|
291
|
-
</span>
|
|
292
|
-
<span className={cn("font-bold leading-[1.15] tracking-tight text-foreground", lead ? "line-clamp-3 text-2xl @[36rem]:text-3xl @[60rem]:text-4xl" : "line-clamp-3 text-xl")} data-feed-title id={titleId}>
|
|
293
|
-
{title}
|
|
294
|
-
</span>
|
|
295
|
-
{showDek ? <span className={cn("text-muted-foreground", lead ? "line-clamp-3 text-base" : "line-clamp-2 text-sm")} data-feed-dek>{dek}</span> : null}
|
|
296
|
-
<span className="mt-auto flex flex-wrap items-center gap-x-2 gap-y-1 pt-1 text-xs text-muted-foreground">
|
|
297
|
-
<span>
|
|
298
|
-
{age ? <time dateTime={new Date(time ?? now).toISOString()} title={new Date(time ?? now).toLocaleString()}>{age}</time> : null}
|
|
299
|
-
{age && host ? <span aria-hidden="true"> · </span> : null}
|
|
300
|
-
{host}
|
|
301
|
-
</span>
|
|
302
|
-
{capture ? <CaptureBadge status={capture} /> : null}
|
|
303
|
-
{messageCount > 0 ? (
|
|
304
|
-
<span aria-label={messageCountLabel(messageCount)} className="inline-flex items-center gap-1 tabular-nums" data-feed-message-count={messageCount} role="img">
|
|
305
|
-
<MessageSquareIcon aria-hidden="true" className="size-3.5" />
|
|
306
|
-
{messageCount}
|
|
307
|
-
</span>
|
|
308
|
-
) : null}
|
|
309
|
-
{isAutoAccepted(reference) ? <AutoAcceptedBadge actor={reference.curationStatusUpdatedBy} reason={reference.curationStatusReason} /> : null}
|
|
310
|
-
</span>
|
|
311
|
-
</span>
|
|
338
|
+
<ReferenceCardHeader capture={capture} lead={lead} messageCount={messageCount} now={now} preview={preview} reference={reference} titleId={titleId} />
|
|
312
339
|
</button>
|
|
313
340
|
|
|
314
341
|
<div className="relative bg-[var(--feed-footer)] px-4 pb-4 pt-3" data-feed-actions>
|
|
315
|
-
<
|
|
342
|
+
<DecisionControl
|
|
316
343
|
answer={chosen}
|
|
317
344
|
decided={answer}
|
|
318
345
|
disabled={Boolean(disabledReason)}
|
|
319
346
|
disabledReason={disabledReason}
|
|
320
347
|
feedback={feedback}
|
|
348
|
+
needsReason={rating.askReason}
|
|
321
349
|
note={reasonLabel ? `Reason: ${reasonLabel}` : null}
|
|
322
350
|
onChoose={(choice) => { if (!disabledReason) store.choose(item.lineageId, choice); }}
|
|
351
|
+
onReasonChange={(text) => store.setNote(item.lineageId, text)}
|
|
352
|
+
onReasonCommit={() => { if (!disabledReason) store.commitNote(item.lineageId); }}
|
|
323
353
|
prediction={decision ? predictionFromDecision(decision) : predictionLoading ? { state: "loading" } : { state: "none" }}
|
|
354
|
+
reason={rating.note ?? ""}
|
|
355
|
+
reasonRequiredFor="reject"
|
|
324
356
|
statusOverlay
|
|
325
357
|
/>
|
|
326
|
-
{rating.askReason ? (
|
|
327
|
-
// Covers the footer rather than adding to it, so asking for a reason never pushes the cards below it down.
|
|
328
|
-
<div className="absolute inset-0 z-10 flex flex-col justify-center space-y-2 bg-[var(--feed-footer)] p-3" data-review-reasons>
|
|
329
|
-
<p className="text-xs font-medium">Reason for rejecting. Choose one to save your Reject.</p>
|
|
330
|
-
<div className="flex flex-wrap gap-1.5">
|
|
331
|
-
{REFERENCE_REJECTION_REASON_CODES.map((code) => (
|
|
332
|
-
<button className={cn(PILL, "min-h-8 bg-[var(--feed-control)] px-3 text-xs")} data-review-reason={code} key={code} onClick={() => store.setReason(item.lineageId, code)} type="button">
|
|
333
|
-
{REASON_LABELS[code]}
|
|
334
|
-
</button>
|
|
335
|
-
))}
|
|
336
|
-
<button className={cn(PILL, "min-h-8 px-3 text-xs text-muted-foreground")} onClick={() => store.cancelReason(item.lineageId)} type="button">Cancel</button>
|
|
337
|
-
</div>
|
|
338
|
-
</div>
|
|
339
|
-
) : null}
|
|
340
358
|
</div>
|
|
341
359
|
</article>
|
|
342
360
|
);
|
|
@@ -2,20 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ArchiveIcon,
|
|
5
|
-
CheckIcon,
|
|
6
5
|
ChevronLeftIcon,
|
|
7
6
|
ChevronRightIcon,
|
|
8
|
-
CircleCheckIcon,
|
|
9
|
-
CircleXIcon,
|
|
10
7
|
ExternalLinkIcon,
|
|
11
8
|
} from "lucide-react";
|
|
12
9
|
import { useEffect, useRef, useState, type ReactNode, type PointerEvent as ReactPointerEvent } from "react";
|
|
13
10
|
import { Button } from "./ui/button";
|
|
14
|
-
import { AutoGrowTextarea } from "./ui/auto-grow-textarea";
|
|
15
11
|
import { cn } from "../lib/utils";
|
|
16
|
-
import {
|
|
12
|
+
import { DecisionControl } from "./decision-control";
|
|
17
13
|
import { useRatingStore, type RatingController } from "./use-reference-ratings";
|
|
18
|
-
import {
|
|
14
|
+
import type { ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
19
15
|
|
|
20
16
|
export type ReviewChoice = "accept" | "reject";
|
|
21
17
|
|
|
@@ -31,96 +27,6 @@ export type ReviewCardDecision = {
|
|
|
31
27
|
reasonCode: ReferenceRejectionReasonCode | null;
|
|
32
28
|
};
|
|
33
29
|
|
|
34
|
-
const REASON_LABELS: Record<ReferenceRejectionReasonCode, string> = {
|
|
35
|
-
out_of_scope: "Out of scope",
|
|
36
|
-
policy_exclusion: "Policy exclusion",
|
|
37
|
-
duplicate: "Duplicate",
|
|
38
|
-
low_quality: "Low quality",
|
|
39
|
-
unavailable: "Unavailable",
|
|
40
|
-
provenance: "Provenance",
|
|
41
|
-
other: "Other",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const CHOICE_LABEL: Record<ReviewChoice, string> = { accept: "Accept", reject: "Reject" };
|
|
45
|
-
|
|
46
|
-
function percent(confidence: number | null): string | null {
|
|
47
|
-
return confidence === null ? null : `${Math.round(confidence * 100)}%`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The Cyclotron's read-only row: its Accept or Reject highlighted with its confidence, and a check or cross once a person has answered.
|
|
52
|
-
* Shared by the review card and by every list that shows a suggestion before a decision.
|
|
53
|
-
*/
|
|
54
|
-
export function CyclotronRow({ decided = null, prediction }: { decided?: ReviewChoice | null; prediction: CardPrediction }) {
|
|
55
|
-
const matched = decided && prediction.state === "ready" ? prediction.choice === decided : null;
|
|
56
|
-
return (
|
|
57
|
-
<div className="space-y-2" data-review-row="cyclotron">
|
|
58
|
-
<div className="flex items-center gap-3">
|
|
59
|
-
<span className="w-20 shrink-0 text-sm font-semibold">Cyclotron</span>
|
|
60
|
-
{prediction.state === "ready" ? (
|
|
61
|
-
<div className="flex flex-1 gap-2">
|
|
62
|
-
{(["accept", "reject"] as const).map((choice) => {
|
|
63
|
-
const predicted = prediction.state === "ready" && prediction.choice === choice;
|
|
64
|
-
const verdict = predicted && matched !== null ? (matched ? "match" : "miss") : null;
|
|
65
|
-
return (
|
|
66
|
-
<span
|
|
67
|
-
className={cn(
|
|
68
|
-
"inline-flex min-h-10 flex-1 items-center justify-center gap-1.5 rounded-md border px-3 text-sm",
|
|
69
|
-
predicted ? "border-foreground bg-muted font-semibold" : "border-dashed text-muted-foreground",
|
|
70
|
-
verdict === "match" && "border-green-700 bg-green-100 text-green-950",
|
|
71
|
-
verdict === "miss" && "border-red-700 bg-red-100 text-red-950",
|
|
72
|
-
)}
|
|
73
|
-
data-cyclotron-choice={choice}
|
|
74
|
-
data-predicted={predicted || undefined}
|
|
75
|
-
data-verdict={verdict ?? undefined}
|
|
76
|
-
key={choice}
|
|
77
|
-
>
|
|
78
|
-
{verdict === "match" ? <CircleCheckIcon aria-hidden="true" className="size-4" /> : null}
|
|
79
|
-
{verdict === "miss" ? <CircleXIcon aria-hidden="true" className="size-4" /> : null}
|
|
80
|
-
{CHOICE_LABEL[choice]}
|
|
81
|
-
{predicted && percent(prediction.confidence) ? <small className="text-xs font-normal">{percent(prediction.confidence)}</small> : null}
|
|
82
|
-
{verdict ? <span className="sr-only">{verdict === "match" ? ", matches your answer" : ", differs from your answer"}</span> : null}
|
|
83
|
-
</span>
|
|
84
|
-
);
|
|
85
|
-
})}
|
|
86
|
-
</div>
|
|
87
|
-
) : (
|
|
88
|
-
<p className="text-sm text-muted-foreground" data-cyclotron-state={prediction.state}>
|
|
89
|
-
{prediction.state === "loading" ? "Loading…" : prediction.state === "error" ? "Prediction unavailable" : "No prediction"}
|
|
90
|
-
</p>
|
|
91
|
-
)}
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** The two answers, large enough to tap: Accept and Reject, the chosen one filled and checked (never colour alone). */
|
|
98
|
-
export function ChoiceButtons({ chosen, disabled = false, onChoose }: { chosen: ReviewChoice | null; disabled?: boolean; onChoose: (choice: ReviewChoice) => void }) {
|
|
99
|
-
return (
|
|
100
|
-
<div className="flex flex-1 gap-2">
|
|
101
|
-
{(["accept", "reject"] as const).map((choice) => {
|
|
102
|
-
const on = chosen === choice;
|
|
103
|
-
return (
|
|
104
|
-
<Button
|
|
105
|
-
aria-disabled={disabled ? true : undefined}
|
|
106
|
-
aria-pressed={on}
|
|
107
|
-
className="min-h-14 flex-1 text-base"
|
|
108
|
-
data-review-choice={choice}
|
|
109
|
-
key={choice}
|
|
110
|
-
onClick={() => onChoose(choice)}
|
|
111
|
-
type="button"
|
|
112
|
-
variant={on ? (choice === "accept" ? "default" : "destructive") : "outline"}
|
|
113
|
-
>
|
|
114
|
-
{on ? <CheckIcon aria-hidden="true" className="size-5" /> : null}
|
|
115
|
-
{CHOICE_LABEL[choice]}
|
|
116
|
-
</Button>
|
|
117
|
-
);
|
|
118
|
-
})}
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** A reference's relevance decision in the card's own vocabulary. */
|
|
124
30
|
export function predictionFromDecision(decision: { label: string; positiveLabel: string | null; confidence: number | null }): CardPrediction {
|
|
125
31
|
return { state: "ready", choice: decision.label === (decision.positiveLabel ?? "include") ? "accept" : "reject", confidence: decision.confidence };
|
|
126
32
|
}
|
|
@@ -181,10 +87,8 @@ export function ReviewCard({
|
|
|
181
87
|
const store = ratings ?? local;
|
|
182
88
|
const item = store.get(itemId);
|
|
183
89
|
const note = item.note ?? initialNote;
|
|
184
|
-
const reason = item.reasonCode !== undefined ? item.reasonCode : initialReason;
|
|
185
90
|
const chosen = item.pending ?? decided;
|
|
186
91
|
const choose = (choice: ReviewChoice) => { if (!disabledReason) store.choose(itemId, choice); };
|
|
187
|
-
const chooseReason = (code: ReferenceRejectionReasonCode) => { if (!disabledReason) store.setReason(itemId, code); };
|
|
188
92
|
const commitNote = () => { if (!disabledReason) store.commitNote(itemId); };
|
|
189
93
|
|
|
190
94
|
// Desktop shortcuts: A accepts, R rejects. Never while typing.
|
|
@@ -222,53 +126,23 @@ export function ReviewCard({
|
|
|
222
126
|
) : null}
|
|
223
127
|
</header>
|
|
224
128
|
|
|
225
|
-
<
|
|
129
|
+
<DecisionControl
|
|
226
130
|
answer={chosen}
|
|
227
131
|
decided={decided}
|
|
228
132
|
disabled={Boolean(disabledReason)}
|
|
229
133
|
disabledReason={disabledReason}
|
|
230
134
|
feedback={item.feedback}
|
|
135
|
+
needsReason={item.askReason}
|
|
231
136
|
onChoose={choose}
|
|
137
|
+
onReasonChange={(text) => store.setNote(itemId, text)}
|
|
138
|
+
onReasonCommit={commitNote}
|
|
232
139
|
prediction={prediction}
|
|
140
|
+
reason={note}
|
|
141
|
+
reasonRequiredFor="reject"
|
|
233
142
|
size="large"
|
|
234
143
|
surface="card"
|
|
235
144
|
/>
|
|
236
145
|
|
|
237
|
-
<label className="block text-sm font-medium">
|
|
238
|
-
Why? (optional)
|
|
239
|
-
<AutoGrowTextarea
|
|
240
|
-
aria-label="Why? (optional)"
|
|
241
|
-
className="mt-1 block min-h-20 w-full rounded-[var(--feed-radius-lg,10px)] bg-[var(--feed-control)] p-3 text-base font-normal outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
242
|
-
onBlur={commitNote}
|
|
243
|
-
onChange={(event) => store.setNote(itemId, event.target.value)}
|
|
244
|
-
placeholder="Say what made this an easy yes or a clear no. Your note teaches the Cyclotron."
|
|
245
|
-
value={note}
|
|
246
|
-
/>
|
|
247
|
-
</label>
|
|
248
|
-
|
|
249
|
-
{chosen === "reject" ? (
|
|
250
|
-
<div className="space-y-2" data-review-reasons>
|
|
251
|
-
<p className="text-sm font-medium">Reason for rejecting</p>
|
|
252
|
-
<div className="flex flex-wrap gap-2">
|
|
253
|
-
{REFERENCE_REJECTION_REASON_CODES.map((code) => (
|
|
254
|
-
<button
|
|
255
|
-
aria-disabled={disabledReason ? true : undefined}
|
|
256
|
-
aria-pressed={reason === code}
|
|
257
|
-
className={cn("inline-flex min-h-10 items-center gap-1.5 rounded-[var(--feed-radius-pill,8px)] px-3 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring", reason === code ? "bg-[var(--primary)] font-semibold text-[var(--primary-foreground)]" : "bg-[var(--feed-control)]")}
|
|
258
|
-
data-review-reason={code}
|
|
259
|
-
key={code}
|
|
260
|
-
onClick={() => chooseReason(code)}
|
|
261
|
-
type="button"
|
|
262
|
-
>
|
|
263
|
-
{reason === code ? <CheckIcon aria-hidden="true" className="size-4" /> : null}
|
|
264
|
-
{REASON_LABELS[code]}
|
|
265
|
-
</button>
|
|
266
|
-
))}
|
|
267
|
-
</div>
|
|
268
|
-
{!reason && !note.trim() ? <p className="text-sm text-muted-foreground">Choose a reason, or say why above, to save your Reject.</p> : null}
|
|
269
|
-
</div>
|
|
270
|
-
) : null}
|
|
271
|
-
|
|
272
146
|
{children}
|
|
273
147
|
|
|
274
148
|
{onArchive ? (
|
|
@@ -4,7 +4,7 @@ import { generateClient } from "aws-amplify/data";
|
|
|
4
4
|
import type { Schema } from "../amplify/data/resource";
|
|
5
5
|
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
6
6
|
import { createLocalPreviewReportingTrail, createLocalPreviewStoriesForReference } from "../lib/local-preview-seed";
|
|
7
|
-
import { EMPTY_STORY_REPORTING_TRAIL, referencesByRole, siteNameOf, type ReferenceStory, type StoryReportingTrail, type TrailAssignment, type TrailAttachment, type TrailReference, type TrailThread, type TrailVersion } from "../lib/story-reporting-trail";
|
|
7
|
+
import { EMPTY_STORY_REPORTING_TRAIL, referencesByRole, siteNameOf, stageMovesOf, type ReferenceStory, type StoryReportingTrail, type TrailAssignment, type TrailAttachment, type TrailReference, type TrailThread, type TrailVersion } from "../lib/story-reporting-trail";
|
|
8
8
|
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
9
9
|
|
|
10
10
|
type GraphqlResponse = { data?: Record<string, { items?: Record<string, unknown>[] | null; nextToken?: string | null } | null> | null; errors?: { message?: string }[] | null };
|
|
@@ -89,7 +89,7 @@ export async function loadStoryReportingTrail(lineageId: string, itemId: string,
|
|
|
89
89
|
configureAmplifyClient();
|
|
90
90
|
const client = generateClient<Schema>() as unknown as GraphqlClient;
|
|
91
91
|
const [versionRows, assignmentRows, attachmentRows, relationRows] = await Promise.all([
|
|
92
|
-
listAll(client, "listItemsByLineageAndVersion", "id versionNumber versionState status stage headline title changeReason versionCreatedBy versionCreatedAt updatedAt", ", $lineageId: ID!", ", lineageId: $lineageId", { lineageId }).catch(() => []),
|
|
92
|
+
listAll(client, "listItemsByLineageAndVersion", "id versionNumber versionState status stage headline title changeReason versionCreatedBy versionCreatedAt updatedAt metadata", ", $lineageId: ID!", ", lineageId: $lineageId", { lineageId }).catch(() => []),
|
|
93
93
|
listAll(client, "listAssignmentsByItemLineageAndCreatedAt", "id title assignmentTypeKey status assigneeKey summary createdAt", ", $itemLineageId: ID!", ", itemLineageId: $itemLineageId", { itemLineageId: lineageId }).catch(() => []),
|
|
94
94
|
listAll(client, "listModelAttachmentsByOwnerRoleAndSortKey", "id role filename mediaType byteSize", ", $ownerId: ID!", ", ownerId: $ownerId", { ownerId: itemId }).catch(() => []),
|
|
95
95
|
listAll(client, "listSemanticRelations", "id predicate subjectKind subjectLineageId objectKind objectId objectLineageId metadata", ", $filter: ModelSemanticRelationFilterInput", ", filter: $filter", { filter: { or: [{ subjectLineageId: { eq: lineageId } }, { objectLineageId: { eq: lineageId } }] } }).catch(() => []),
|
|
@@ -124,5 +124,6 @@ export async function loadStoryReportingTrail(lineageId: string, itemId: string,
|
|
|
124
124
|
};
|
|
125
125
|
})));
|
|
126
126
|
const attachments: TrailAttachment[] = attachmentRows.map(row => ({ id: String(row.id), role: text(row.role), filename: text(row.filename), mediaType: text(row.mediaType), byteSize: typeof row.byteSize === "number" ? row.byteSize : null }));
|
|
127
|
-
|
|
127
|
+
const latestRow = [...versionRows].sort((left, right) => Number(right.versionNumber ?? 0) - Number(left.versionNumber ?? 0))[0];
|
|
128
|
+
return { ...EMPTY_STORY_REPORTING_TRAIL, versions, assignments, threads, references, attachments, stageMoves: stageMovesOf(latestRow?.metadata) };
|
|
128
129
|
}
|
|
@@ -91,6 +91,14 @@ export function StoryReportingTrailView({ citedKeys = [], demo = false, focusedR
|
|
|
91
91
|
</li>)}
|
|
92
92
|
</ol>
|
|
93
93
|
</TrailSection>
|
|
94
|
+
{trail.stageMoves.length > 0 && <TrailSection count={trail.stageMoves.length} title="Stage moves">
|
|
95
|
+
<ol className="story-trail-list" data-story-trail-stage-moves>
|
|
96
|
+
{[...trail.stageMoves].reverse().map(move => <li key={`${move.at}-${move.to}`}>
|
|
97
|
+
<div className="story-trail-row"><strong>{move.from ?? "start"} → {move.to}</strong>{move.by && <span className="story-trail-chip">{move.by}</span>}<span className="story-trail-meta">{formatTimestamp(move.at)}</span></div>
|
|
98
|
+
{move.reason && <p className="story-trail-meta">{move.reason}</p>}
|
|
99
|
+
</li>)}
|
|
100
|
+
</ol>
|
|
101
|
+
</TrailSection>}
|
|
94
102
|
<TrailSection count={trail.versions.length} title="Draft versions">
|
|
95
103
|
<ol className="story-trail-list">
|
|
96
104
|
{trail.versions.map((version, versionIndex) => <li key={version.id}>
|