@anthusai/papyrus 5.58.0 → 5.59.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/message-actions.js +68 -0
- package/amplify/data/message-actions.ts +70 -0
- package/amplify/data/resolvers/delete-own-message.js +30 -0
- package/amplify/data/resolvers/edit-message.js +33 -0
- package/amplify/data/resolvers/moderate-message.js +32 -0
- package/amplify/data/resolvers/post-message.js +68 -0
- package/amplify/data/resolvers/rename-thread.js +28 -0
- package/amplify/data/resource.js +8 -5
- package/amplify/data/resource.ts +8 -5
- package/amplify/data/schema.js +14 -7
- package/amplify/data/schema.ts +14 -7
- package/components/conversation-context.tsx +29 -2
- package/components/conversation-provider.tsx +5 -1
- package/components/conversation-thread.tsx +133 -19
- package/components/conversations-memory-source.ts +43 -6
- package/components/conversations-network-source.ts +13 -1
- package/components/conversations-source.ts +15 -3
- package/components/conversations-view.tsx +437 -76
- package/components/list-detail-layout.tsx +44 -0
- package/components/needs-me-bodies.tsx +14 -14
- package/components/needs-me-feed.tsx +8 -18
- package/components/news-desk-taxonomy-client.ts +142 -57
- package/components/newsroom-article-editor.tsx +10 -4
- package/components/newsroom-organizer.css +10 -1
- package/components/newsroom-references-view.tsx +41 -100
- package/components/notification-cards.tsx +4 -4
- package/components/notification-center.tsx +5 -7
- package/components/proposals-view.tsx +4 -4
- package/components/reference-conversation.tsx +5 -0
- package/components/reference-feed.tsx +69 -85
- package/components/reference-review-card.tsx +1 -1
- package/components/references-page-loading.tsx +12 -6
- package/components/topic-steering-workspace.tsx +4 -4
- package/components/use-conversation.ts +36 -2
- package/components/use-feed-preferences.ts +33 -0
- package/lib/category-repository.ts +7 -38
- package/lib/console-chat-client.ts +8 -178
- package/lib/conversation.ts +73 -2
- package/lib/needs-me-items.ts +4 -4
- package/lib/needs-me-ranking.ts +9 -22
- package/lib/newsroom-board.ts +10 -1
- package/lib/newsroom-demo-dashboard.ts +0 -7
- package/lib/newsroom-organizer-nav.ts +2 -2
- package/lib/post-message.ts +54 -0
- package/lib/proposals-admin.ts +2 -2
- package/lib/reference-feed-ranking.ts +77 -160
- package/lib/review-drafts.ts +2 -2
- package/lib/review-fixture.ts +1 -1
- package/lib/review-requests.ts +4 -4
- package/lib/semantic-graph.ts +1 -1
- package/package.json +4 -1
- package/routes.manifest.json +1 -1
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { InfoIcon, Loader2Icon, MessageSquareIcon } from "lucide-react";
|
|
4
|
-
import { useEffect, useId, useRef, useState, type CSSProperties
|
|
3
|
+
import { CheckCheckIcon, CircleDashedIcon, ClockIcon, InfoIcon, Loader2Icon, MessageSquareIcon, SparklesIcon } from "lucide-react";
|
|
4
|
+
import { useEffect, useId, useRef, useState, type CSSProperties } from "react";
|
|
5
5
|
import type { ReferenceRecord } from "../lib/category-repository";
|
|
6
6
|
import { REFERENCE_REJECTION_REASON_CODES, isAutoAccepted, normalizeReferenceCurationStatus, type ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
7
7
|
import {
|
|
8
8
|
referenceDisplayTime,
|
|
9
|
-
groupFeedItems,
|
|
10
|
-
EARLIER_AFTER_TAUS,
|
|
11
|
-
MINIMUM_FRESHNESS_EXAMPLES,
|
|
12
|
-
type FeedGroupKey,
|
|
13
9
|
type FeedItem,
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
type ReferenceFeedTabKey,
|
|
10
|
+
type FeedShow,
|
|
11
|
+
type FeedSort,
|
|
17
12
|
} from "../lib/reference-feed-ranking";
|
|
18
13
|
import { dekAddsInformation, isCardStyleImage, referenceDek, referenceSourceHost, formatFeedAge, type ReferencePreview } from "../lib/reference-preview";
|
|
19
14
|
import { referenceDisplayTitle } from "../lib/newsroom-references";
|
|
@@ -47,8 +42,14 @@ export const REASON_LABELS: Record<ReferenceRejectionReasonCode, string> = {
|
|
|
47
42
|
other: "Other",
|
|
48
43
|
};
|
|
49
44
|
|
|
50
|
-
const
|
|
51
|
-
|
|
45
|
+
const SORTS: Array<{ key: FeedSort; label: string; Icon: typeof SparklesIcon }> = [
|
|
46
|
+
{ key: "best", label: "Best bets", Icon: SparklesIcon },
|
|
47
|
+
{ key: "newest", label: "Newest", Icon: ClockIcon },
|
|
48
|
+
];
|
|
49
|
+
const SHOWS: Array<{ key: FeedShow; label: string; Icon: typeof SparklesIcon }> = [
|
|
50
|
+
{ key: "unrated", label: "Unrated only", Icon: CircleDashedIcon },
|
|
51
|
+
{ key: "all", label: "Include rated", Icon: CheckCheckIcon },
|
|
52
|
+
];
|
|
52
53
|
|
|
53
54
|
export const PILL = "inline-flex min-h-9 items-center justify-center gap-1.5 rounded-full px-3.5 text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring";
|
|
54
55
|
|
|
@@ -63,16 +64,20 @@ export type ReferenceFeedProps = {
|
|
|
63
64
|
onOpen: (lineageId: string) => void;
|
|
64
65
|
onRate: (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null) => Promise<void> | void;
|
|
65
66
|
onVisitRated: (lineageId: string) => void;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
onSortChange: (sort: FeedSort) => void;
|
|
68
|
+
onShowChange: (show: FeedShow) => void;
|
|
69
|
+
/** The visible list, already ordered by the chosen sort and filtered by the chosen Show. */
|
|
70
|
+
items: FeedItem[];
|
|
71
|
+
/** How many References already have an answer, for the empty text. */
|
|
72
|
+
ratedCount: number;
|
|
73
|
+
sort: FeedSort;
|
|
74
|
+
show: FeedShow;
|
|
69
75
|
predictionLoading?: boolean;
|
|
70
76
|
previews: ReadonlyMap<string, ReferencePreview>;
|
|
71
77
|
selectedLineageId?: string;
|
|
72
78
|
/** Capture status per Reference lineage, for the small badge on each card. Optional. */
|
|
73
79
|
captures?: ReadonlyMap<string, CaptureStatus>;
|
|
74
|
-
/** The
|
|
75
|
-
groups?: Array<{ key: FeedGroupKey; items: FeedItem[] }>;
|
|
80
|
+
/** The large lead card, if any. */
|
|
76
81
|
leadId?: string;
|
|
77
82
|
/** Scrolls the grid to a card, deliberately (Previous / Next). A new nonce is a new request; selecting alone never scrolls. */
|
|
78
83
|
scrollTarget?: { id: string; nonce: number } | null;
|
|
@@ -80,7 +85,6 @@ export type ReferenceFeedProps = {
|
|
|
80
85
|
ratings?: RatingController;
|
|
81
86
|
/** True when the cards are a saved copy that is being refreshed. */
|
|
82
87
|
stale?: boolean;
|
|
83
|
-
tab: ReferenceFeedTabKey;
|
|
84
88
|
visitRated: ReadonlySet<string>;
|
|
85
89
|
};
|
|
86
90
|
|
|
@@ -360,13 +364,8 @@ export function FeedCard({
|
|
|
360
364
|
);
|
|
361
365
|
}
|
|
362
366
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return `${days < 10 ? days.toFixed(1).replace(/\.0$/, "") : Math.round(days)} days`;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** The plain-words recipe, with this site's learned numbers in it. */
|
|
369
|
-
function RankingPopover({ freshness }: { freshness: FeedFreshness }) {
|
|
367
|
+
/** The plain-words recipe. */
|
|
368
|
+
function RankingPopover() {
|
|
370
369
|
const [open, setOpen] = useState(false);
|
|
371
370
|
const panelId = useId();
|
|
372
371
|
const root = useRef<HTMLDivElement | null>(null);
|
|
@@ -405,15 +404,10 @@ function RankingPopover({ freshness }: { freshness: FeedFreshness }) {
|
|
|
405
404
|
>
|
|
406
405
|
<p className="text-base font-bold">A feed run by algorithms you control</p>
|
|
407
406
|
<ol className="list-decimal space-y-2.5 pl-5">
|
|
408
|
-
<li><strong>Best bets
|
|
409
|
-
<li><strong>
|
|
410
|
-
<li><strong>
|
|
411
|
-
|
|
412
|
-
: <>Age does not change the order yet. The pace is learned from the items you accept, and so far there are {freshness.examples} of the {MINIMUM_FRESHNESS_EXAMPLES} needed.</>}</li>
|
|
413
|
-
<li><strong>Earlier.</strong> {freshness.tauDays !== null
|
|
414
|
-
? <>Unrated items older than {plainDays(freshness.tauDays * EARLIER_AFTER_TAUS)} move to Earlier at the bottom. You can still rate them. Nothing is deleted.</>
|
|
415
|
-
: <>Once the pace is learned, unrated items past three times that pace move to Earlier at the bottom. Nothing is deleted.</>}</li>
|
|
416
|
-
<li><strong>Your answers stay put.</strong> A card you rate stays where it is for this visit. Next time it is under Rated.</li>
|
|
407
|
+
<li><strong>Best bets</strong> puts the items the Cyclotron is most sure you will Accept first. It goes by confidence alone: an Accept at 90% ranks above an Accept at 60%, and the ones it expects you to Reject most surely come last. Items with no prediction sit in the middle. Age does not change this order.</li>
|
|
408
|
+
<li><strong>Newest</strong> puts the items that arrived most recently first.</li>
|
|
409
|
+
<li><strong>Unrated only</strong> shows items nobody has answered yet. <strong>Include rated</strong> adds the ones you have already accepted or rejected to the same list, showing your answer, in the same sort. Nothing is hidden because it is old.</li>
|
|
410
|
+
<li><strong>Your answers stay put.</strong> A card you rate stays where it is for this visit, and new items wait behind a button until you ask for them.</li>
|
|
417
411
|
</ol>
|
|
418
412
|
</div>
|
|
419
413
|
) : null}
|
|
@@ -421,10 +415,6 @@ function RankingPopover({ freshness }: { freshness: FeedFreshness }) {
|
|
|
421
415
|
);
|
|
422
416
|
}
|
|
423
417
|
|
|
424
|
-
export function SectionHeading({ children, id }: { children: ReactNode; id: string }) {
|
|
425
|
-
return <h3 className="mb-5 mt-10 text-3xl font-bold tracking-tight text-primary first:mt-2 sm:mt-12 sm:text-4xl" id={id}>{children}</h3>;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
418
|
export function ReferenceFeed({
|
|
429
419
|
disabledReason = null,
|
|
430
420
|
loading = false,
|
|
@@ -432,22 +422,23 @@ export function ReferenceFeed({
|
|
|
432
422
|
now,
|
|
433
423
|
onOpen,
|
|
434
424
|
onRate,
|
|
435
|
-
|
|
425
|
+
onShowChange,
|
|
426
|
+
onSortChange,
|
|
436
427
|
onVisitRated,
|
|
437
|
-
|
|
428
|
+
items,
|
|
429
|
+
ratedCount,
|
|
438
430
|
predictionLoading = false,
|
|
439
431
|
previews,
|
|
440
432
|
selectedLineageId = "",
|
|
441
433
|
captures,
|
|
442
|
-
|
|
443
|
-
leadId: stableLeadId,
|
|
434
|
+
leadId,
|
|
444
435
|
ratings,
|
|
445
436
|
scrollTarget = null,
|
|
437
|
+
show,
|
|
438
|
+
sort,
|
|
446
439
|
stale = false,
|
|
447
|
-
tab,
|
|
448
440
|
visitRated,
|
|
449
441
|
}: ReferenceFeedProps) {
|
|
450
|
-
const counts: Record<ReferenceFeedTabKey, number> = { feed: partition.feed.length, rated: partition.rated.length, earlier: partition.earlier.length };
|
|
451
442
|
|
|
452
443
|
const renderCard = (item: FeedItem, lead: boolean) => (
|
|
453
444
|
<FeedCard
|
|
@@ -479,28 +470,39 @@ export function ReferenceFeed({
|
|
|
479
470
|
document.querySelector(`[data-feed-card="${CSS.escape(scrollTarget.id)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
480
471
|
}, [scrollTarget]);
|
|
481
472
|
|
|
482
|
-
const groups = stableGroups ?? groupFeedItems(partition.feed);
|
|
483
|
-
const headings = groups.length > 1;
|
|
484
|
-
const leadId = stableGroups ? stableLeadId : groups[0]?.key !== "least" ? groups[0]?.items[0]?.lineageId : undefined;
|
|
485
473
|
|
|
486
474
|
return (
|
|
487
|
-
<div className="@container space-y-5" data-reference-feed data-feed-
|
|
488
|
-
<div className="flex items-center justify-between gap-2">
|
|
489
|
-
<div
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
475
|
+
<div className="@container space-y-5" data-reference-feed data-feed-sort={sort} data-feed-show={show} {...FEED_SURFACE_PROPS}>
|
|
476
|
+
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
|
477
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
|
478
|
+
<div aria-label="Sort" className="inline-flex rounded-full bg-[var(--feed-card)] p-1" data-feed-control="sort" role="group">
|
|
479
|
+
{SORTS.map(({ key, label, Icon }) => (
|
|
480
|
+
<button
|
|
481
|
+
aria-pressed={sort === key}
|
|
482
|
+
className={cn(PILL, "px-3.5", sort === key ? "bg-[var(--feed-control)] text-foreground" : "text-muted-foreground hover:text-foreground")}
|
|
483
|
+
data-feed-sort-button={key}
|
|
484
|
+
key={key}
|
|
485
|
+
onClick={() => onSortChange(key)}
|
|
486
|
+
type="button"
|
|
487
|
+
>
|
|
488
|
+
<Icon aria-hidden="true" className="size-4" />{label}
|
|
489
|
+
</button>
|
|
490
|
+
))}
|
|
491
|
+
</div>
|
|
492
|
+
<div aria-label="Show" className="inline-flex rounded-full bg-[var(--feed-card)] p-1" data-feed-control="show" role="group">
|
|
493
|
+
{SHOWS.map(({ key, label, Icon }) => (
|
|
494
|
+
<button
|
|
495
|
+
aria-pressed={show === key}
|
|
496
|
+
className={cn(PILL, "px-3.5", show === key ? "bg-[var(--feed-control)] text-foreground" : "text-muted-foreground hover:text-foreground")}
|
|
497
|
+
data-feed-show-button={key}
|
|
498
|
+
key={key}
|
|
499
|
+
onClick={() => onShowChange(key)}
|
|
500
|
+
type="button"
|
|
501
|
+
>
|
|
502
|
+
<Icon aria-hidden="true" className="size-4" />{label}
|
|
503
|
+
</button>
|
|
504
|
+
))}
|
|
505
|
+
</div>
|
|
504
506
|
</div>
|
|
505
507
|
<div className="flex items-center gap-2">
|
|
506
508
|
{stale ? (
|
|
@@ -508,33 +510,15 @@ export function ReferenceFeed({
|
|
|
508
510
|
<Loader2Icon aria-hidden="true" className="size-3.5 animate-spin" />Showing your last view while it updates
|
|
509
511
|
</span>
|
|
510
512
|
) : null}
|
|
511
|
-
<RankingPopover
|
|
513
|
+
<RankingPopover />
|
|
512
514
|
</div>
|
|
513
515
|
</div>
|
|
514
516
|
{stale ? <p className="text-xs text-muted-foreground sm:hidden" role="status"><Loader2Icon aria-hidden="true" className="mr-1 inline size-3.5 animate-spin" />Showing your last view while it updates</p> : null}
|
|
515
517
|
|
|
516
518
|
{loading ? <ReferenceFeedSkeleton /> : (
|
|
517
|
-
|
|
518
|
-
{
|
|
519
|
-
|
|
520
|
-
{groups.length ? groups.map((group) => (
|
|
521
|
-
<section aria-labelledby={headings ? `feed-group-${group.key}` : undefined} data-feed-group={group.key} key={group.key}>
|
|
522
|
-
{headings ? <SectionHeading id={`feed-group-${group.key}`}>{GROUP_HEADINGS[group.key]}</SectionHeading> : null}
|
|
523
|
-
<div className={FEED_GRID} data-feed-grid>{group.items.map((item) => renderCard(item, item.lineageId === leadId))}</div>
|
|
524
|
-
</section>
|
|
525
|
-
)) : empty(counts.rated || counts.earlier ? "You are all caught up. Check Earlier and Rated." : "No references yet.")}
|
|
526
|
-
{partition.earlier.length ? (
|
|
527
|
-
<section aria-labelledby="feed-earlier-heading" data-feed-earlier>
|
|
528
|
-
<SectionHeading id="feed-earlier-heading">Earlier</SectionHeading>
|
|
529
|
-
<p className="-mt-3 mb-5 text-sm text-muted-foreground">Unrated and past their freshness. Still yours to rate.</p>
|
|
530
|
-
<div className={FEED_GRID} data-feed-grid>{partition.earlier.map((item) => renderCard(item, false))}</div>
|
|
531
|
-
</section>
|
|
532
|
-
) : null}
|
|
533
|
-
</div>
|
|
534
|
-
) : null}
|
|
535
|
-
{tab === "rated" ? (partition.rated.length ? <div className={FEED_GRID} data-feed-grid>{partition.rated.map((item) => renderCard(item, false))}</div> : empty("Nothing rated yet. Your Accepts and Rejects collect here, newest first.")) : null}
|
|
536
|
-
{tab === "earlier" ? (partition.earlier.length ? <div className={FEED_GRID} data-feed-grid>{partition.earlier.map((item) => renderCard(item, false))}</div> : empty("Nothing has aged out. Unrated items that fall past their freshness collect here.")) : null}
|
|
537
|
-
</div>
|
|
519
|
+
items.length ? (
|
|
520
|
+
<div className={FEED_GRID} data-feed-grid>{items.map((item) => renderCard(item, item.lineageId === leadId))}</div>
|
|
521
|
+
) : empty(ratedCount && show === "unrated" ? "You are all caught up. Choose Include rated to see your answers." : "No references yet.")
|
|
538
522
|
)}
|
|
539
523
|
</div>
|
|
540
524
|
);
|
|
@@ -177,7 +177,7 @@ export function ReviewQueueBar({
|
|
|
177
177
|
return (
|
|
178
178
|
<nav
|
|
179
179
|
aria-label="Review queue"
|
|
180
|
-
className={cn("sticky bottom-0 z-10 flex shrink-0 items-center gap-2 bg-[var(--feed-card)] px-3 py-2", anchored && "
|
|
180
|
+
className={cn("sticky bottom-0 z-10 flex shrink-0 items-center gap-2 bg-[var(--feed-card)] px-3 py-2", anchored && "lg:fixed lg:bottom-0 lg:right-0 lg:w-[calc(24rem+1rem)] lg:pr-7 xl:w-[calc(28rem+1rem)]")}
|
|
181
181
|
data-review-queue-anchored={anchored || undefined}
|
|
182
182
|
data-review-queue-bar
|
|
183
183
|
>
|
|
@@ -4,9 +4,10 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
4
4
|
import { LoaderIcon } from "lucide-react";
|
|
5
5
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
6
6
|
import { loadFeedSnapshot, type FeedSnapshot } from "../lib/reference-feed-cache";
|
|
7
|
-
import {
|
|
7
|
+
import { buildFeedItems, feedLeadId, visibleFeedItems } from "../lib/reference-feed-ranking";
|
|
8
8
|
import { selectCanonicalReferenceRecords } from "../lib/newsroom-references";
|
|
9
9
|
import { ReferenceFeed, ReferenceFeedSkeleton } from "./reference-feed";
|
|
10
|
+
import { useFeedPreferences } from "./use-feed-preferences";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* What the References page shows while the session and the first reads are still resolving: the real page header at once,
|
|
@@ -16,7 +17,7 @@ import { ReferenceFeed, ReferenceFeedSkeleton } from "./reference-feed";
|
|
|
16
17
|
export function ReferencesPageLoading() {
|
|
17
18
|
const [snapshot, setSnapshot] = useState<FeedSnapshot | null>(null);
|
|
18
19
|
const [checked, setChecked] = useState(false);
|
|
19
|
-
const
|
|
20
|
+
const { sort, setSort, show, setShow } = useFeedPreferences();
|
|
20
21
|
const [note, setNote] = useState<string | null>(null);
|
|
21
22
|
const [now] = useState(() => Date.now());
|
|
22
23
|
useEffect(() => {
|
|
@@ -26,8 +27,9 @@ export function ReferencesPageLoading() {
|
|
|
26
27
|
const references = useMemo(() => selectCanonicalReferenceRecords(snapshot?.references ?? []), [snapshot]);
|
|
27
28
|
const decisions = useMemo(() => new Map(snapshot?.decisions ?? []), [snapshot]);
|
|
28
29
|
const previews = useMemo(() => new Map(snapshot?.previews ?? []), [snapshot]);
|
|
29
|
-
const partition = useMemo(() => partitionReferenceFeed(references, decisions, { now }), [references, decisions, now]);
|
|
30
30
|
const noVisitRated = useMemo(() => new Set<string>(), []);
|
|
31
|
+
const allItems = useMemo(() => buildFeedItems(references, decisions), [references, decisions]);
|
|
32
|
+
const items = useMemo(() => visibleFeedItems(allItems, sort, show, noVisitRated), [allItems, sort, show, noVisitRated]);
|
|
31
33
|
|
|
32
34
|
return (
|
|
33
35
|
<div className="space-y-4" data-references-page-loading>
|
|
@@ -42,12 +44,16 @@ export function ReferencesPageLoading() {
|
|
|
42
44
|
now={now}
|
|
43
45
|
onOpen={() => setNote("Opening a reference is available in a moment, once your sign-in is confirmed.")}
|
|
44
46
|
onRate={() => undefined}
|
|
45
|
-
|
|
47
|
+
onShowChange={setShow}
|
|
48
|
+
onSortChange={setSort}
|
|
46
49
|
onVisitRated={() => undefined}
|
|
47
|
-
|
|
50
|
+
items={items}
|
|
51
|
+
leadId={feedLeadId(items)}
|
|
52
|
+
ratedCount={allItems.filter((item) => item.rated).length}
|
|
48
53
|
previews={previews}
|
|
54
|
+
show={show}
|
|
55
|
+
sort={sort}
|
|
49
56
|
stale
|
|
50
|
-
tab={tab}
|
|
51
57
|
visitRated={noVisitRated}
|
|
52
58
|
/>
|
|
53
59
|
) : (
|
|
@@ -4378,8 +4378,8 @@ function AdministrationDeskView({
|
|
|
4378
4378
|
data-active={administrationPanel === "proposals" || undefined}
|
|
4379
4379
|
onClick={() => onPanelChange("proposals")}
|
|
4380
4380
|
>
|
|
4381
|
-
<strong>
|
|
4382
|
-
<span>
|
|
4381
|
+
<strong>Proposed Stories</strong>
|
|
4382
|
+
<span>Proposed Stories and changes that AI proposes</span>
|
|
4383
4383
|
</Link>
|
|
4384
4384
|
{readerPluginEnabled() ? (
|
|
4385
4385
|
<Link
|
|
@@ -15967,8 +15967,8 @@ function CategorySetPanel({
|
|
|
15967
15967
|
<dd>{referenceAttachments.length}</dd>
|
|
15968
15968
|
</div>
|
|
15969
15969
|
<div>
|
|
15970
|
-
<dt>
|
|
15971
|
-
<dd>{
|
|
15970
|
+
<dt>Monitor Deliveries</dt>
|
|
15971
|
+
<dd>{references.filter((reference) => Boolean(reference.discoveredByMonitorId)).length}</dd>
|
|
15972
15972
|
</div>
|
|
15973
15973
|
<div>
|
|
15974
15974
|
<dt>Review Links</dt>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
-
import type
|
|
4
|
+
import { NO_VIEWER, type ConversationAnchor, type ConversationMessage, type ConversationViewer } from "../lib/conversation";
|
|
5
5
|
import { useConversationStore } from "./conversation-context";
|
|
6
6
|
|
|
7
7
|
export type ConversationState = "loading" | "ready" | "error";
|
|
@@ -64,7 +64,41 @@ export function useConversation(anchor: ConversationAnchor) {
|
|
|
64
64
|
}
|
|
65
65
|
}, [store]);
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
const [viewer, setViewer] = useState<ConversationViewer>(NO_VIEWER);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!store) return;
|
|
70
|
+
let active = true;
|
|
71
|
+
store.viewer().then((loaded) => { if (active) setViewer(loaded); }).catch(() => undefined);
|
|
72
|
+
return () => { active = false; };
|
|
73
|
+
}, [store]);
|
|
74
|
+
|
|
75
|
+
// Display names come from the authors' profiles: one request for every author not yet asked about.
|
|
76
|
+
const [names, setNames] = useState<ReadonlyMap<string, string>>(() => new Map());
|
|
77
|
+
const asked = useRef(new Set<string>());
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!store) return;
|
|
80
|
+
const missing = [...new Set(messages.map((message) => message.authorUserProfileId).filter((id): id is string => Boolean(id) && !asked.current.has(id as string)))];
|
|
81
|
+
if (!missing.length) return;
|
|
82
|
+
missing.forEach((id) => asked.current.add(id));
|
|
83
|
+
let active = true;
|
|
84
|
+
store.authors(missing).then((loaded) => { if (active && loaded.size) setNames((current) => new Map([...current, ...loaded])); }).catch(() => undefined);
|
|
85
|
+
return () => { active = false; };
|
|
86
|
+
}, [messages, store]);
|
|
87
|
+
|
|
88
|
+
const edit = useCallback(async (messageId: string, body: string) => {
|
|
89
|
+
if (!store) throw new Error("Messages are not available here.");
|
|
90
|
+
const saved = await store.edit(messageId, body);
|
|
91
|
+
setMessages((current) => current.map((message) => (message.id === messageId ? { ...message, ...saved } : message)));
|
|
92
|
+
}, [store]);
|
|
93
|
+
const remove = useCallback(async (messageId: string) => {
|
|
94
|
+
if (!store) throw new Error("Messages are not available here.");
|
|
95
|
+
const target = messages.find((message) => message.id === messageId);
|
|
96
|
+
const moderate = viewer.isAdmin && target?.authorSub !== viewer.sub;
|
|
97
|
+
const saved = await store.remove(messageId, moderate);
|
|
98
|
+
setMessages((current) => current.map((message) => (message.id === messageId ? { ...message, ...saved } : message)));
|
|
99
|
+
}, [store, messages, viewer]);
|
|
100
|
+
|
|
101
|
+
return { available: Boolean(store), messages, state, error, retry, post, edit, remove, viewer, names };
|
|
68
102
|
}
|
|
69
103
|
|
|
70
104
|
/** Message counts per Reference lineage, for the feed cards. A failed read just shows no counts. */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { DEFAULT_FEED_SHOW, DEFAULT_FEED_SORT, type FeedShow, type FeedSort } from "../lib/reference-feed-ranking";
|
|
5
|
+
|
|
6
|
+
const SORT_KEY = "papyrus:reference-feed:sort";
|
|
7
|
+
const SHOW_KEY = "papyrus:reference-feed:show";
|
|
8
|
+
|
|
9
|
+
function read<T extends string>(key: string, allowed: readonly T[], fallback: T): T {
|
|
10
|
+
try {
|
|
11
|
+
const stored = window.localStorage.getItem(key);
|
|
12
|
+
return allowed.includes(stored as T) ? (stored as T) : fallback;
|
|
13
|
+
} catch {
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function write(key: string, value: string) {
|
|
19
|
+
try { window.localStorage.setItem(key, value); } catch { /* The choice still works for this visit without storage. */ }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The viewer's Sort and Show choices, remembered in this browser. The page renders with the defaults, then applies what was saved. */
|
|
23
|
+
export function useFeedPreferences() {
|
|
24
|
+
const [sort, setSortState] = useState<FeedSort>(DEFAULT_FEED_SORT);
|
|
25
|
+
const [show, setShowState] = useState<FeedShow>(DEFAULT_FEED_SHOW);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setSortState(read<FeedSort>(SORT_KEY, ["best", "newest"], DEFAULT_FEED_SORT));
|
|
28
|
+
setShowState(read<FeedShow>(SHOW_KEY, ["unrated", "all"], DEFAULT_FEED_SHOW));
|
|
29
|
+
}, []);
|
|
30
|
+
const setSort = useCallback((next: FeedSort) => { setSortState(next); write(SORT_KEY, next); }, []);
|
|
31
|
+
const setShow = useCallback((next: FeedShow) => { setShowState(next); write(SHOW_KEY, next); }, []);
|
|
32
|
+
return { sort, setSort, show, setShow };
|
|
33
|
+
}
|
|
@@ -289,6 +289,10 @@ export type ReferenceRecord = {
|
|
|
289
289
|
curationDecisionRelationId?: string | null;
|
|
290
290
|
newsroomFeedKey?: string | null;
|
|
291
291
|
reviewedFeedKey?: string | null;
|
|
292
|
+
/** The monitor that delivered this Reference (set at intake; the Reference is the record, no Message). */
|
|
293
|
+
discoveredByMonitorId?: string | null;
|
|
294
|
+
/** What the monitor's source said about it (a Hub model's structured fields, say). */
|
|
295
|
+
discoveryDetails?: unknown;
|
|
292
296
|
metadata?: unknown;
|
|
293
297
|
updatedAt?: string | null;
|
|
294
298
|
};
|
|
@@ -419,6 +423,9 @@ export type MessageRecord = {
|
|
|
419
423
|
authorSub?: string | null;
|
|
420
424
|
authorUserProfileId?: string | null;
|
|
421
425
|
authorLabel?: string | null;
|
|
426
|
+
editedAt?: string | null;
|
|
427
|
+
deletedAt?: string | null;
|
|
428
|
+
deletedBySub?: string | null;
|
|
422
429
|
threadId?: string | null;
|
|
423
430
|
parentMessageId?: string | null;
|
|
424
431
|
sequenceNumber?: number | null;
|
|
@@ -1478,18 +1485,6 @@ export function createDemoCategorySteeringDashboard(
|
|
|
1478
1485
|
},
|
|
1479
1486
|
],
|
|
1480
1487
|
messages: [
|
|
1481
|
-
{
|
|
1482
|
-
id: "message-demo-history-002-rationale",
|
|
1483
|
-
messageKind: "ingestion_rationale",
|
|
1484
|
-
messageDomain: "commentary",
|
|
1485
|
-
status: "active",
|
|
1486
|
-
body: "Imported as a useful holdout against modern scaling coverage.",
|
|
1487
|
-
summary: "Holdout ingestion rationale",
|
|
1488
|
-
source: "biblicus-import",
|
|
1489
|
-
importRunId: "knowledge-import-demo-projection",
|
|
1490
|
-
createdAt: importedAt,
|
|
1491
|
-
updatedAt: importedAt,
|
|
1492
|
-
},
|
|
1493
1488
|
{
|
|
1494
1489
|
id: reportingPacketMessageId,
|
|
1495
1490
|
messageKind: "reporting_context_packet",
|
|
@@ -1833,32 +1828,6 @@ export function createDemoCategorySteeringDashboard(
|
|
|
1833
1828
|
importRunId: "knowledge-import-demo-projection",
|
|
1834
1829
|
importedAt,
|
|
1835
1830
|
},
|
|
1836
|
-
{
|
|
1837
|
-
id: "semantic-relation-demo-message-history-002-rationale",
|
|
1838
|
-
relationState: "current",
|
|
1839
|
-
predicate: "comment",
|
|
1840
|
-
relationTypeId: "semantic-relation-type-comment",
|
|
1841
|
-
relationTypeKey: "comment",
|
|
1842
|
-
relationDomain: "commentary",
|
|
1843
|
-
subjectKind: "message",
|
|
1844
|
-
subjectId: "message-demo-history-002-rationale",
|
|
1845
|
-
subjectLineageId: "message-demo-history-002-rationale",
|
|
1846
|
-
subjectVersionNumber: 1,
|
|
1847
|
-
objectKind: "reference",
|
|
1848
|
-
objectId: referenceHistoryTwoId,
|
|
1849
|
-
objectLineageId: referenceHistoryTwoLineageId,
|
|
1850
|
-
objectVersionNumber: 1,
|
|
1851
|
-
subjectStateKey: "message#message-demo-history-002-rationale#current",
|
|
1852
|
-
objectStateKey: `reference#${referenceHistoryTwoLineageId}#current`,
|
|
1853
|
-
objectSubjectStateKey: `reference#${referenceHistoryTwoLineageId}#current#message`,
|
|
1854
|
-
predicateObjectStateKey: `comment#reference#${referenceHistoryTwoLineageId}#current`,
|
|
1855
|
-
subjectVersionKey: "message#message-demo-history-002-rationale",
|
|
1856
|
-
objectVersionKey: `reference#${referenceHistoryTwoId}`,
|
|
1857
|
-
rank: 1,
|
|
1858
|
-
reviewRecommended: false,
|
|
1859
|
-
importRunId: "knowledge-import-demo-projection",
|
|
1860
|
-
importedAt,
|
|
1861
|
-
},
|
|
1862
1831
|
],
|
|
1863
1832
|
loadError: null,
|
|
1864
1833
|
};
|