@anthusai/papyrus 5.43.0-next.11 → 5.43.0-next.13
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.
|
@@ -357,6 +357,8 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
357
357
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-[20px]"] { border-radius: 20px !important; }
|
|
358
358
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-2xl"],
|
|
359
359
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-2xl"] { border-radius: 16px !important; }
|
|
360
|
+
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-[16px]"],
|
|
361
|
+
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-[16px]"] { border-radius: 16px !important; }
|
|
360
362
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-3xl"],
|
|
361
363
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-3xl"] { border-radius: 24px !important; }
|
|
362
364
|
|
|
@@ -391,3 +393,5 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
391
393
|
}
|
|
392
394
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-cyclotron-choice] { border-radius: 9999px !important; }
|
|
393
395
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-review-queue-bar] { background: var(--feed-footer) !important; }
|
|
396
|
+
/* The rating grid keeps its own rounded rectangles in the borderless panes (not the pill shape the flat rules give other buttons). */
|
|
397
|
+
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-rating-grid] :is(button, [data-cyclotron-choice]) { border-radius: 16px !important; }
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
CheckIcon,
|
|
5
|
-
CircleCheckIcon,
|
|
6
|
-
CircleXIcon,
|
|
7
|
-
InfoIcon,
|
|
8
|
-
Loader2Icon,
|
|
9
|
-
MinusIcon,
|
|
10
|
-
XIcon,
|
|
11
|
-
} from "lucide-react";
|
|
3
|
+
import { InfoIcon, Loader2Icon } from "lucide-react";
|
|
12
4
|
import { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
|
13
5
|
import type { ReferenceRecord } from "../lib/category-repository";
|
|
14
6
|
import { REFERENCE_REJECTION_REASON_CODES, normalizeReferenceCurationStatus, type ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
@@ -28,6 +20,7 @@ import { referenceDisplayTitle } from "../lib/newsroom-references";
|
|
|
28
20
|
import type { RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
29
21
|
import { cn } from "../lib/utils";
|
|
30
22
|
import { predictionFromDecision, type ReviewChoice } from "./reference-review-card";
|
|
23
|
+
import { RatingGrid } from "./review-rating-grid";
|
|
31
24
|
|
|
32
25
|
/**
|
|
33
26
|
* Flat surfaces, no borders: the page tone, a card tone one step away from it, a footer tone, and a control tone for chips and
|
|
@@ -182,46 +175,6 @@ function FeedImage({ host, lead, name, src }: { host: string | null; lead: boole
|
|
|
182
175
|
);
|
|
183
176
|
}
|
|
184
177
|
|
|
185
|
-
function percent(confidence: number | null): string | null {
|
|
186
|
-
return confidence === null ? null : `${Math.round(confidence * 100)}%`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** The Cyclotron's read-only answer as a soft pill: Accept or Reject with its confidence, then Match or Miss once a person has answered. */
|
|
190
|
-
function PredictionChip({ answer, decision, loading }: { answer: ReviewChoice | null; decision: RelevanceDecisionView | undefined; loading: boolean }) {
|
|
191
|
-
const base = "inline-flex min-h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium";
|
|
192
|
-
if (!decision) {
|
|
193
|
-
return (
|
|
194
|
-
<span className={cn(base, "bg-[var(--feed-control)] text-muted-foreground")} data-cyclotron-state={loading ? "loading" : "none"}>
|
|
195
|
-
{loading ? <Loader2Icon aria-hidden="true" className="size-3.5 animate-spin" /> : <MinusIcon aria-hidden="true" className="size-3.5" />}
|
|
196
|
-
{loading ? "Predicting…" : "No prediction"}
|
|
197
|
-
</span>
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
const prediction = predictionFromDecision(decision);
|
|
201
|
-
if (prediction.state !== "ready") return null;
|
|
202
|
-
const matched = answer ? prediction.choice === answer : null;
|
|
203
|
-
const verdict = matched === null ? null : matched ? "match" : "miss";
|
|
204
|
-
const Icon = verdict === "match" ? CircleCheckIcon : verdict === "miss" ? CircleXIcon : prediction.choice === "accept" ? CheckIcon : XIcon;
|
|
205
|
-
return (
|
|
206
|
-
<span
|
|
207
|
-
className={cn(
|
|
208
|
-
base,
|
|
209
|
-
verdict === null && "bg-[var(--feed-control)] text-foreground",
|
|
210
|
-
verdict === "match" && "bg-primary/15 text-primary",
|
|
211
|
-
verdict === "miss" && "bg-destructive/15 text-destructive",
|
|
212
|
-
)}
|
|
213
|
-
data-cyclotron-choice={prediction.choice}
|
|
214
|
-
data-predicted
|
|
215
|
-
data-verdict={verdict ?? undefined}
|
|
216
|
-
>
|
|
217
|
-
<Icon aria-hidden="true" className="size-4" />
|
|
218
|
-
<span>Cyclotron · {prediction.choice === "accept" ? "Accept" : "Reject"}{percent(prediction.confidence) ? ` ${percent(prediction.confidence)}` : ""}</span>
|
|
219
|
-
{verdict ? <span data-verdict-word>· {verdict === "match" ? "Match" : "Miss"}</span> : null}
|
|
220
|
-
{verdict ? <span className="sr-only">{verdict === "match" ? ", matches your answer" : ", differs from your answer"}</span> : null}
|
|
221
|
-
</span>
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
178
|
type RateState = { phase: "idle" | "saving" | "saved" | "error" | "reason"; message?: string };
|
|
226
179
|
|
|
227
180
|
function FeedCard({
|
|
@@ -324,34 +277,14 @@ function FeedCard({
|
|
|
324
277
|
</button>
|
|
325
278
|
|
|
326
279
|
<div className="space-y-2.5 bg-[var(--feed-footer)] p-3" data-feed-actions>
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
<span className="sr-only">You</span>
|
|
336
|
-
{(["accept", "reject"] as const).map((choice) => {
|
|
337
|
-
const on = answer === choice;
|
|
338
|
-
return (
|
|
339
|
-
<button
|
|
340
|
-
aria-disabled={disabledReason ? true : undefined}
|
|
341
|
-
aria-pressed={on}
|
|
342
|
-
className={cn(PILL, "min-h-10 flex-1", on ? "bg-[var(--primary)] text-[var(--primary-foreground)]" : "bg-[var(--feed-control)] text-foreground hover:bg-[var(--feed-control)]/70", disabledReason && "opacity-60")}
|
|
343
|
-
data-review-choice={choice}
|
|
344
|
-
key={choice}
|
|
345
|
-
onClick={() => choose(choice)}
|
|
346
|
-
type="button"
|
|
347
|
-
>
|
|
348
|
-
{choice === "accept" ? <CheckIcon aria-hidden="true" className={cn("size-4", !on && "opacity-60")} /> : <XIcon aria-hidden="true" className={cn("size-4", !on && "opacity-60")} />}
|
|
349
|
-
{choice === "accept" ? "Accept" : "Reject"}
|
|
350
|
-
</button>
|
|
351
|
-
);
|
|
352
|
-
})}
|
|
353
|
-
</div>
|
|
354
|
-
{disabledReason ? <p className="text-xs text-muted-foreground" data-review-disabled-reason>{disabledReason}</p> : null}
|
|
280
|
+
<RatingGrid
|
|
281
|
+
answer={answer}
|
|
282
|
+
disabled={Boolean(disabledReason)}
|
|
283
|
+
disabledReason={disabledReason}
|
|
284
|
+
feedback={rate.phase === "saving" ? { phase: "saving" } : rate.phase === "error" ? { phase: "error", message: rate.message } : rate.phase === "saved" || (visitRated && rate.phase === "idle" && answer) ? { phase: "saved" } : { phase: "idle" }}
|
|
285
|
+
onChoose={choose}
|
|
286
|
+
prediction={decision ? predictionFromDecision(decision) : predictionLoading ? { state: "loading" } : { state: "none" }}
|
|
287
|
+
/>
|
|
355
288
|
{rate.phase === "reason" ? (
|
|
356
289
|
<div className="space-y-2" data-review-reasons>
|
|
357
290
|
<p className="text-xs font-medium">Reason for rejecting. Choose one to save your Reject.</p>
|
|
@@ -365,7 +298,6 @@ function FeedCard({
|
|
|
365
298
|
</div>
|
|
366
299
|
</div>
|
|
367
300
|
) : null}
|
|
368
|
-
{rate.phase === "error" ? <p className="text-xs text-destructive" role="alert">{rate.message}</p> : null}
|
|
369
301
|
</div>
|
|
370
302
|
</article>
|
|
371
303
|
);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { useEffect, useRef, useState, type ReactNode, type PointerEvent as ReactPointerEvent } from "react";
|
|
13
13
|
import { Button } from "./ui/button";
|
|
14
14
|
import { cn } from "../lib/utils";
|
|
15
|
+
import { RatingGrid } from "./review-rating-grid";
|
|
15
16
|
import { REFERENCE_REJECTION_REASON_CODES, type ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
16
17
|
|
|
17
18
|
export type ReviewChoice = "accept" | "reject";
|
|
@@ -247,21 +248,17 @@ export function ReviewCard({
|
|
|
247
248
|
{summary ? <p className="text-sm text-muted-foreground">{summary}</p> : null}
|
|
248
249
|
</header>
|
|
249
250
|
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
</div>
|
|
262
|
-
{disabledReason ? <p className="text-sm text-muted-foreground" data-review-disabled-reason>{disabledReason}</p> : null}
|
|
263
|
-
{error ? <p className="text-sm text-destructive" role="alert">{error}</p> : null}
|
|
264
|
-
</div>
|
|
251
|
+
<RatingGrid
|
|
252
|
+
answer={chosen}
|
|
253
|
+
decided={decided}
|
|
254
|
+
disabled={Boolean(disabledReason)}
|
|
255
|
+
disabledReason={disabledReason}
|
|
256
|
+
feedback={saving ? { phase: "saving" } : error ? { phase: "error", message: error } : saved ? { phase: "saved" } : { phase: "idle" }}
|
|
257
|
+
onChoose={choose}
|
|
258
|
+
prediction={prediction}
|
|
259
|
+
size="large"
|
|
260
|
+
surface="card"
|
|
261
|
+
/>
|
|
265
262
|
|
|
266
263
|
<label className="block text-sm font-medium">
|
|
267
264
|
Why? (optional)
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type RatingFeedback = { phase: "idle" | "saving" | "saved" | "error"; message?: string };
|
|
19
|
+
|
|
20
|
+
/** P(accept) from a prediction: its confidence is in the side it chose; an unrecorded confidence is a mild lean. */
|
|
21
|
+
export function predictionAcceptProbability(prediction: Extract<CardPrediction, { state: "ready" }>): number {
|
|
22
|
+
const confidence = prediction.confidence ?? UNRECORDED_CONFIDENCE;
|
|
23
|
+
return prediction.choice === "accept" ? confidence : 1 - confidence;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** How far, in percent of the whole bar, the fill reaches from the centre: |P(accept) - 0.5| * 2, over the two halves. */
|
|
27
|
+
export function confidenceFillPercent(probability: number): number {
|
|
28
|
+
return Math.min(100, Math.abs(probability - 0.5) * 2 * 100);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const WORD: Record<ReviewChoice, string> = { accept: "Accept", reject: "Reject" };
|
|
32
|
+
|
|
33
|
+
const TONE = {
|
|
34
|
+
match: "bg-emerald-600/20 text-emerald-900 dark:bg-emerald-400/20 dark:text-emerald-100",
|
|
35
|
+
miss: "bg-red-600/20 text-red-900 dark:bg-red-400/20 dark:text-red-100",
|
|
36
|
+
} as const;
|
|
37
|
+
const FILL = { match: "bg-emerald-600 dark:bg-emerald-400", miss: "bg-red-600 dark:bg-red-400", neutral: "bg-[var(--primary)]" } as const;
|
|
38
|
+
|
|
39
|
+
export function RatingGrid({
|
|
40
|
+
answer,
|
|
41
|
+
decided = answer,
|
|
42
|
+
disabled = false,
|
|
43
|
+
disabledReason = null,
|
|
44
|
+
feedback = { phase: "idle" },
|
|
45
|
+
onChoose,
|
|
46
|
+
prediction,
|
|
47
|
+
size = "compact",
|
|
48
|
+
surface = "footer",
|
|
49
|
+
}: {
|
|
50
|
+
/** The side the buttons show as chosen (it may be a pending choice that is still saving). */
|
|
51
|
+
answer: ReviewChoice | null;
|
|
52
|
+
/** The saved human answer, which the Match / Miss verdict compares against. */
|
|
53
|
+
decided?: ReviewChoice | null;
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
disabledReason?: string | null;
|
|
56
|
+
feedback?: RatingFeedback;
|
|
57
|
+
onChoose: (choice: ReviewChoice) => void;
|
|
58
|
+
prediction: CardPrediction;
|
|
59
|
+
size?: "compact" | "large";
|
|
60
|
+
/** The tone the grid sits on, so the bar's centre notch matches it. */
|
|
61
|
+
surface?: "footer" | "card";
|
|
62
|
+
}) {
|
|
63
|
+
const ready = prediction.state === "ready" ? prediction : null;
|
|
64
|
+
const verdict = ready && decided ? (ready.choice === decided ? "match" : "miss") : null;
|
|
65
|
+
const probability = ready ? predictionAcceptProbability(ready) : 0.5;
|
|
66
|
+
const half = confidenceFillPercent(probability) / 2; // percent of the full bar width, on one side of the centre
|
|
67
|
+
const leansAccept = probability >= 0.5;
|
|
68
|
+
const confidenceText = ready && ready.confidence !== null ? `${Math.round(ready.confidence * 100)}%` : null;
|
|
69
|
+
const cell = size === "large" ? "min-h-14 text-base" : "min-h-12 text-sm";
|
|
70
|
+
const label = size === "large" ? "text-sm" : "text-xs";
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className={cn("grid grid-cols-[4.5rem_minmax(0,1fr)_minmax(0,1fr)] gap-x-2 gap-y-2.5", size === "large" && "grid-cols-[5rem_minmax(0,1fr)_minmax(0,1fr)]")} data-rating-grid>
|
|
74
|
+
<div className="col-span-3 grid grid-cols-subgrid items-stretch gap-y-2" data-review-row="cyclotron">
|
|
75
|
+
<span className={cn("self-center font-semibold", label)}>Cyclotron</span>
|
|
76
|
+
{(["accept", "reject"] as const).map((choice) => {
|
|
77
|
+
const predicted = ready?.choice === choice;
|
|
78
|
+
const Icon = predicted ? (verdict === "match" ? CircleCheckIcon : verdict === "miss" ? CircleXIcon : choice === "accept" ? CheckIcon : XIcon) : null;
|
|
79
|
+
return (
|
|
80
|
+
<span
|
|
81
|
+
className={cn(
|
|
82
|
+
"flex flex-col items-center justify-center rounded-[16px] px-2 py-1.5 text-center",
|
|
83
|
+
cell,
|
|
84
|
+
!predicted && "bg-[var(--feed-control)]/45 text-muted-foreground",
|
|
85
|
+
predicted && !verdict && "bg-[var(--feed-control)] font-semibold text-[var(--primary)]",
|
|
86
|
+
predicted && verdict && cn("font-semibold", TONE[verdict]),
|
|
87
|
+
)}
|
|
88
|
+
data-cyclotron-choice={choice}
|
|
89
|
+
data-predicted={predicted || undefined}
|
|
90
|
+
data-verdict={predicted && verdict ? verdict : undefined}
|
|
91
|
+
key={choice}
|
|
92
|
+
>
|
|
93
|
+
<span className="inline-flex items-center gap-1.5">
|
|
94
|
+
{Icon ? <Icon aria-hidden="true" className="size-4 shrink-0" /> : null}
|
|
95
|
+
{WORD[choice]}
|
|
96
|
+
{predicted && confidenceText ? <small className="text-xs font-normal opacity-80">{confidenceText}</small> : null}
|
|
97
|
+
</span>
|
|
98
|
+
{predicted && verdict ? <small className="text-xs font-medium" data-verdict-word>{verdict === "match" ? "Match" : "Miss"}</small> : null}
|
|
99
|
+
{predicted && verdict ? <span className="sr-only">{verdict === "match" ? ", matches your answer" : ", differs from your answer"}</span> : null}
|
|
100
|
+
</span>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
<div className="col-span-2 col-start-2 flex flex-col gap-1" data-confidence>
|
|
104
|
+
<div
|
|
105
|
+
aria-label={ready ? `Cyclotron confidence: leans ${leansAccept ? "Accept" : "Reject"}, ${Math.round(confidenceFillPercent(probability))} percent of the way` : "No confidence"}
|
|
106
|
+
className="relative h-2.5 w-full overflow-hidden rounded-full bg-[var(--feed-control)]"
|
|
107
|
+
data-confidence-bar
|
|
108
|
+
data-confidence-direction={ready ? (leansAccept ? "accept" : "reject") : "none"}
|
|
109
|
+
role="img"
|
|
110
|
+
>
|
|
111
|
+
{ready ? (
|
|
112
|
+
<span
|
|
113
|
+
className={cn("absolute inset-y-0 rounded-full", FILL[verdict ?? "neutral"])}
|
|
114
|
+
data-confidence-fill
|
|
115
|
+
data-fill-percent={Math.round(half * 2)}
|
|
116
|
+
style={{ left: leansAccept ? `${50 - half}%` : "50%", width: `${half}%` }}
|
|
117
|
+
/>
|
|
118
|
+
) : null}
|
|
119
|
+
<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)]")} />
|
|
120
|
+
</div>
|
|
121
|
+
{prediction.state === "ready" ? null : (
|
|
122
|
+
<small className="text-center text-xs text-muted-foreground" data-cyclotron-state={prediction.state}>
|
|
123
|
+
{prediction.state === "loading" ? "Loading…" : prediction.state === "error" ? "Prediction unavailable" : "No prediction"}
|
|
124
|
+
</small>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div className="col-span-3 grid grid-cols-subgrid items-stretch" data-review-row="you">
|
|
130
|
+
<span className={cn("self-center font-semibold", label)}>You</span>
|
|
131
|
+
{(["accept", "reject"] as const).map((choice) => {
|
|
132
|
+
const on = answer === choice;
|
|
133
|
+
const Icon = choice === "accept" ? CheckIcon : XIcon;
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
aria-disabled={disabled ? true : undefined}
|
|
137
|
+
aria-pressed={on}
|
|
138
|
+
className={cn(
|
|
139
|
+
"inline-flex 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",
|
|
140
|
+
cell,
|
|
141
|
+
on ? "bg-[var(--primary)] font-semibold text-[var(--primary-foreground)]" : "bg-[var(--feed-control)] text-foreground hover:bg-[var(--feed-control)]/70",
|
|
142
|
+
disabled && "opacity-60",
|
|
143
|
+
)}
|
|
144
|
+
data-review-choice={choice}
|
|
145
|
+
key={choice}
|
|
146
|
+
onClick={() => onChoose(choice)}
|
|
147
|
+
type="button"
|
|
148
|
+
>
|
|
149
|
+
<Icon aria-hidden="true" className={cn("size-4", !on && "opacity-60")} />
|
|
150
|
+
{WORD[choice]}
|
|
151
|
+
</button>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="col-span-3 min-h-5 text-xs text-muted-foreground" data-feed-saved role="status">
|
|
157
|
+
{feedback.phase === "saving" ? <span className="inline-flex items-center gap-1"><Loader2Icon aria-hidden="true" className="size-3.5 animate-spin" />Saving…</span> : null}
|
|
158
|
+
{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}
|
|
159
|
+
</div>
|
|
160
|
+
{disabledReason ? <p className="col-span-3 text-xs text-muted-foreground" data-review-disabled-reason>{disabledReason}</p> : null}
|
|
161
|
+
{feedback.phase === "error" ? <p className="col-span-3 text-xs text-destructive" role="alert">{feedback.message}</p> : null}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
package/package.json
CHANGED
|
@@ -149,6 +149,7 @@
|
|
|
149
149
|
"./components/reference-source-preview": "./components/reference-source-preview.tsx",
|
|
150
150
|
"./components/references-page-loading": "./components/references-page-loading.tsx",
|
|
151
151
|
"./components/references-prefetch": "./components/references-prefetch.ts",
|
|
152
|
+
"./components/review-rating-grid": "./components/review-rating-grid.tsx",
|
|
152
153
|
"./components/settings-page": "./components/settings-page.tsx",
|
|
153
154
|
"./components/signup-entries-panel": "./components/signup-entries-panel.tsx",
|
|
154
155
|
"./components/staging-banner": "./components/staging-banner.tsx",
|
|
@@ -479,7 +480,7 @@
|
|
|
479
480
|
"optional": true
|
|
480
481
|
}
|
|
481
482
|
},
|
|
482
|
-
"version": "5.43.0-next.
|
|
483
|
+
"version": "5.43.0-next.13",
|
|
483
484
|
"dependencies": {
|
|
484
485
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
485
486
|
"@aws-crypto/sha256-js": "5.2.0",
|
package/routes.manifest.json
CHANGED