@anthusai/papyrus 5.58.0 → 5.60.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 (53) hide show
  1. package/amplify/data/message-actions.js +72 -0
  2. package/amplify/data/message-actions.ts +74 -0
  3. package/amplify/data/resolvers/delete-own-message.js +30 -0
  4. package/amplify/data/resolvers/edit-message.js +33 -0
  5. package/amplify/data/resolvers/moderate-message.js +32 -0
  6. package/amplify/data/resolvers/post-message.js +68 -0
  7. package/amplify/data/resolvers/rename-thread.js +28 -0
  8. package/amplify/data/resource.js +8 -5
  9. package/amplify/data/resource.ts +8 -5
  10. package/amplify/data/schema.js +14 -7
  11. package/amplify/data/schema.ts +14 -7
  12. package/components/conversation-context.tsx +29 -2
  13. package/components/conversation-provider.tsx +5 -1
  14. package/components/conversation-thread.tsx +133 -19
  15. package/components/conversations-memory-source.ts +43 -6
  16. package/components/conversations-network-source.ts +13 -1
  17. package/components/conversations-source.ts +15 -3
  18. package/components/conversations-view.tsx +437 -76
  19. package/components/list-detail-layout.tsx +44 -0
  20. package/components/needs-me-bodies.tsx +14 -14
  21. package/components/needs-me-feed.tsx +8 -18
  22. package/components/news-desk-taxonomy-client.ts +142 -57
  23. package/components/newsroom-article-editor.tsx +10 -4
  24. package/components/newsroom-organizer.css +10 -1
  25. package/components/newsroom-references-view.tsx +41 -100
  26. package/components/notification-cards.tsx +4 -4
  27. package/components/notification-center.tsx +5 -7
  28. package/components/proposals-view.tsx +4 -4
  29. package/components/reference-conversation.tsx +5 -0
  30. package/components/reference-feed.tsx +69 -85
  31. package/components/reference-review-card.tsx +1 -1
  32. package/components/references-page-loading.tsx +12 -6
  33. package/components/topic-steering-workspace.tsx +8 -24
  34. package/components/use-conversation.ts +36 -2
  35. package/components/use-feed-preferences.ts +33 -0
  36. package/lib/category-repository.ts +7 -38
  37. package/lib/console-chat-client.ts +8 -178
  38. package/lib/conversation.ts +73 -2
  39. package/lib/needs-me-items.ts +4 -4
  40. package/lib/needs-me-ranking.ts +9 -22
  41. package/lib/newsroom-board.ts +10 -1
  42. package/lib/newsroom-demo-dashboard.ts +0 -7
  43. package/lib/newsroom-organizer-nav.ts +2 -2
  44. package/lib/post-message.ts +54 -0
  45. package/lib/proposals-admin.ts +2 -2
  46. package/lib/reference-feed-ranking.ts +77 -160
  47. package/lib/reference-review-response.ts +19 -0
  48. package/lib/review-drafts.ts +2 -2
  49. package/lib/review-fixture.ts +1 -1
  50. package/lib/review-requests.ts +4 -4
  51. package/lib/semantic-graph.ts +1 -1
  52. package/package.json +5 -1
  53. package/routes.manifest.json +1 -1
@@ -54,7 +54,16 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
54
54
  .organizer-board-body{position:relative;display:grid;grid-template-columns:minmax(0,1fr);flex:1;min-width:0;min-height:0;gap:16px}
55
55
  .organizer-board-body[data-detail-open=true]{grid-template-columns:minmax(0,1fr) clamp(320px,34vw,1000px)}
56
56
  .organizer-board-body>.kb-issue-collection{height:100%;min-height:0;overflow:auto}
57
- .organizer-board-body>.kb-issue-detail{overflow:auto;min-height:0}
57
+ /* The detail view is a solid surface from its first frame: the theme's panel token (opaque), with the card token as a fallback if
58
+ a theme has not defined it yet, and never any opacity animation, so the board never shows through it while it loads. */
59
+ .organizer-board-body>.kb-issue-detail{overflow:auto;min-height:0;position:relative;z-index:2;opacity:1;background-color:var(--kb-theme-panel,var(--card,Canvas));isolation:isolate}
60
+ /* One stable shape while an article loads and after: the preview card (and its skeleton) reserve the same height, so the page below it does not move. */
61
+ .newsroom-embedded-article :is([data-newsroom-article-preview],[data-skeleton-preview]){min-height:min(28rem,55dvh)}
62
+ /* The loading skeleton is the loaded editor's size block for block: the toolbar wraps onto more rows as the panel narrows (a container query,
63
+ because the panel's width, not the window's, decides), and the three fields stack when it is narrow. */
64
+ [data-skeleton-toolbar]{height:44px}[data-skeleton-fields]{height:76px}[data-skeleton-preview]{height:min(28rem,55dvh)}
65
+ @container (max-width:719px){[data-skeleton-toolbar]{height:80px}}
66
+ @container (max-width:399px){[data-skeleton-toolbar]{height:116px}[data-skeleton-fields]{height:208px}}
58
67
  .organizer-board-body .kb-issue-detail a:not(.organizer-header-button){display:block;min-height:44px;text-decoration:underline;padding:12px 0}
59
68
  .organizer-menu{height:min(85dvh,760px);max-height:85dvh;display:flex;flex-direction:column;padding:16px;border-radius:24px 24px 0 0;gap:20px}
60
69
  .organizer-menu .organizer-destinations{overflow-y:auto}.organizer-menu .organizer-destinations section{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.organizer-menu .organizer-destinations h3{grid-column:1/-1;font-size:12px;text-transform:none;margin:0 0 4px}
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
+ import { DETAIL_PANE_WIDTH, LIST_HIDDEN_WHEN_DETAIL_OPEN } from "./list-detail-layout";
3
4
  import type { PrimingPanelProps } from "cyclotron/components/priming-panel";
4
5
  import { ReferenceAddEntry } from "./reference-add-entry";
5
6
  import { ReferenceCurationControls, useReferenceImport } from "./reference-curation-controls";
6
7
 
7
- import { ExternalLinkIcon, LayoutGridIcon, ListIcon, PlusIcon } from "lucide-react";
8
+ import { ArrowLeftIcon, ExternalLinkIcon, LayoutGridIcon, ListIcon, PlusIcon } from "lucide-react";
8
9
  import { usePathname } from "next/navigation";
9
10
  import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
10
11
  import { Badge } from "./ui/badge";
@@ -13,10 +14,6 @@ import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card";
13
14
  import { ScrollArea } from "./ui/scroll-area";
14
15
  import { Separator } from "./ui/separator";
15
16
  import {
16
- Sheet,
17
- SheetContent,
18
- SheetHeader,
19
- SheetTitle,
20
17
  } from "./ui/sheet";
21
18
  import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
22
19
  import type { ReferenceAttachmentRecord, ReferenceRecord } from "../lib/category-repository";
@@ -45,21 +42,19 @@ import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
45
42
  import { ReferenceSourcePreview } from "./reference-source-preview";
46
43
  import { ReferenceStories } from "./reference-stories";
47
44
  import { useReferencePreviewAttachments } from "./use-reference-preview-attachments";
48
- import { useMediaQuery } from "./use-media-query";
49
45
  import { FEED_SURFACE_PROPS, FEED_SURFACE_STYLE, ReferenceFeed } from "./reference-feed";
46
+ import { useFeedPreferences } from "./use-feed-preferences";
50
47
  import { ReferenceCapturePanel } from "./reference-capture-panel";
51
48
  import { useReferenceCapture, type CorrectCaptureAction } from "./reference-capture-client";
52
49
  import { useRatingStore, type RatingController } from "./use-reference-ratings";
53
50
  import { ReferenceConversation } from "./reference-conversation";
54
51
  import { useReferenceMessageCounts } from "./use-conversation";
55
52
  import { captureStatusesByLineage } from "../lib/reference-capture";
56
- import { applyFeedOrderSnapshot, partitionReferenceFeed, takeFeedOrderSnapshot, type FeedOrderSnapshot, type ReferenceFeedTabKey } from "../lib/reference-feed-ranking";
53
+ import { applyFeedOrderSnapshot, buildFeedItems, feedLeadId, takeFeedOrderSnapshot, visibleFeedItems, type FeedOrderSnapshot } from "../lib/reference-feed-ranking";
57
54
  import { referenceDek, referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
58
55
  import { isLocalPreviewEnabled } from "../lib/local-preview";
59
56
  import { chooseSyntheticRejects, withSyntheticRejects } from "../lib/local-preview-synthetic-rejects";
60
57
  import { cn } from "../lib/utils";
61
- import { getKanbusTheme, SITE_BRAND } from "../lib/site-brand";
62
- import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
63
58
  import {
64
59
  ReviewCard,
65
60
  predictionFromDecision,
@@ -405,12 +400,8 @@ export function NewsroomReferencesView({
405
400
  savedCopy = null,
406
401
  }: NewsroomReferencesViewProps) {
407
402
  const pathname = usePathname();
408
- // The portaled mobile sheet sits outside the newsroom root, so it carries the theme attributes itself (same as useNewsroomThemeAttributes).
409
- const themeMode = useResolvedPapyrusTheme();
410
- const themeAttributes = { "data-kanbus-theme": getKanbusTheme(SITE_BRAND), "data-color-mode": themeMode };
411
403
  const sessionImport = useReferenceImport(disabled);
412
404
  const [importedCount, setImportedCount] = useState(0);
413
- const isMobileDetail = useMediaQuery("(max-width: 767px)");
414
405
  const pathnameLineageId = useMemo(
415
406
  () => parseReferenceLineageIdFromNewsroomPathname(pathname),
416
407
  [pathname],
@@ -449,20 +440,13 @@ export function NewsroomReferencesView({
449
440
  }, [referenceAttachments, savedCopy, showingSavedCopy]);
450
441
  const [view, setView] = useState<ReferenceView>("feed");
451
442
  const [addOpen, setAddOpen] = useState(false);
452
- const [feedTab, setFeedTab] = useState<ReferenceFeedTabKey>("feed");
443
+ const { sort: feedSort, setSort: setFeedSort, show: feedShow, setShow: setFeedShow } = useFeedPreferences();
453
444
  const { counts: messageCounts, setCount: setMessageCount } = useReferenceMessageCounts(!loading);
454
445
  const [visitRated, setVisitRated] = useState<ReadonlySet<string>>(() => new Set());
455
446
  const [mountedAt] = useState(() => Date.now());
456
447
  const markVisitRated = useCallback((lineageId: string) => {
457
448
  setVisitRated((current) => (current.has(lineageId) ? current : new Set(current).add(lineageId)));
458
449
  }, []);
459
- // Preview only: ?feedTau=<days> stands in for the learned freshness period, to review the Earlier section on a young corpus.
460
- const tauDaysOverride = useMemo(() => {
461
- if (typeof window === "undefined" || !isLocalPreviewEnabled()) return null;
462
- const requested = Number(new URLSearchParams(window.location.search).get("feedTau"));
463
- return Number.isFinite(requested) && requested > 0 ? requested : null;
464
- }, []);
465
-
466
450
  // Preview only: ?feedState=loading holds the page in its loading state, to review the card skeletons.
467
451
  const previewSkeleton = useMemo(
468
452
  () => typeof window !== "undefined" && isLocalPreviewEnabled() && new URLSearchParams(window.location.search).get("feedState") === "loading",
@@ -470,24 +454,24 @@ export function NewsroomReferencesView({
470
454
  );
471
455
 
472
456
  const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
473
- const livePartition = useMemo(
474
- () => partitionReferenceFeed(canonicalReferences, decisions, { now: mountedAt, visitRated, tauDaysOverride }),
475
- [canonicalReferences, decisions, mountedAt, tauDaysOverride, visitRated],
476
- );
457
+ const liveItems = useMemo(() => buildFeedItems(canonicalReferences, decisions), [canonicalReferences, decisions]);
477
458
  // The feed's order is taken ONCE per visit, when the feed has loaded (References and predictions), and then left alone. Selecting an
478
459
  // item, rating one, a background refresh or a cache revalidation changes cards' content, never their places. Items that arrive later
479
- // wait behind a "N new items: show" pill. (The Rated tab is a live list: it is the one place a new answer is meant to appear.)
460
+ // wait behind a "N new items: show" pill. The same holds for both sorts and for Include rated.
480
461
  const orderSnapshot = useRef<FeedOrderSnapshot | null>(null);
481
462
  const [, bumpOrder] = useState(0);
482
463
  const orderReady = !loading && !showingSavedCopy && !previewSkeleton && predictionState !== "loading" && canonicalReferences.length > 0;
483
- if (!orderSnapshot.current && orderReady) orderSnapshot.current = takeFeedOrderSnapshot(livePartition);
484
- const stable = orderSnapshot.current ? applyFeedOrderSnapshot(orderSnapshot.current, livePartition) : null;
485
- const partition = stable?.partition ?? livePartition;
486
- const showNewItems = () => { orderSnapshot.current = takeFeedOrderSnapshot(livePartition); bumpOrder((count) => count + 1); };
464
+ if (!orderSnapshot.current && orderReady) orderSnapshot.current = takeFeedOrderSnapshot(liveItems);
465
+ const stable = orderSnapshot.current ? applyFeedOrderSnapshot(orderSnapshot.current, liveItems, feedSort, feedShow) : null;
466
+ const liveVisible = useMemo(() => visibleFeedItems(liveItems, feedSort, feedShow, visitRated), [liveItems, feedSort, feedShow, visitRated]);
467
+ const feedItems = stable?.items ?? liveVisible;
468
+ const feedLead = stable ? stable.leadId : feedLeadId(feedItems);
469
+ const ratedCount = useMemo(() => liveItems.filter((item) => item.rated).length, [liveItems]);
470
+ const showNewItems = () => { orderSnapshot.current = takeFeedOrderSnapshot(liveItems); bumpOrder((count) => count + 1); };
487
471
  const [scrollTarget, setScrollTarget] = useState<{ id: string; nonce: number } | null>(null);
488
472
  const feedOrderIds = useMemo(
489
- () => (feedTab === "feed" ? [...partition.feed, ...partition.earlier] : feedTab === "rated" ? partition.rated : partition.earlier).map((item) => item.lineageId),
490
- [feedTab, partition],
473
+ () => feedItems.map((item) => item.lineageId),
474
+ [feedItems],
491
475
  );
492
476
  const statusCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
493
477
 
@@ -498,7 +482,6 @@ export function NewsroomReferencesView({
498
482
  );
499
483
  const filters = decisions?.size ? [NEEDS_REVIEW_FILTER, ...STATUS_FILTERS] : STATUS_FILTERS;
500
484
  const [selectedLineageId, setSelectedLineageId] = useState(routeLineageId);
501
- const [mobileDetailOpen, setMobileDetailOpen] = useState(Boolean(routeLineageId) && isMobileDetail);
502
485
 
503
486
  const filteredReferences = useMemo(
504
487
  () => statusFilter === "needs_review"
@@ -551,18 +534,11 @@ export function NewsroomReferencesView({
551
534
 
552
535
  useEffect(() => {
553
536
  setSelectedLineageId(routeLineageId);
554
- if (isMobileDetail) setMobileDetailOpen(Boolean(routeLineageId));
555
- }, [isMobileDetail, routeLineageId]);
556
-
557
- useEffect(() => {
558
- if (isMobileDetail) return;
559
- setMobileDetailOpen(false);
560
- }, [isMobileDetail]);
537
+ }, [routeLineageId]);
561
538
 
562
539
  const selectReference = (lineageId: string) => {
563
540
  setCaughtUp(false);
564
541
  setSelectedLineageId(lineageId);
565
- if (isMobileDetail) setMobileDetailOpen(true);
566
542
  if (typeof window !== "undefined") {
567
543
  const nextPath = buildReferenceDetailPath(lineageId, demo);
568
544
  if (`${window.location.pathname}${window.location.search}` !== nextPath) {
@@ -587,7 +563,6 @@ export function NewsroomReferencesView({
587
563
 
588
564
  const closeDetail = () => {
589
565
  setCaughtUp(false);
590
- setMobileDetailOpen(false);
591
566
  setSelectedLineageId("");
592
567
  if (typeof window !== "undefined") {
593
568
  const indexPath = getNewsroomNavHref("/newsroom/references", demo);
@@ -667,17 +642,20 @@ export function NewsroomReferencesView({
667
642
  ? "No references match this filter."
668
643
  : "No private references imported yet.";
669
644
 
670
- // The grid must not move when a detail opens, so in the feed the detail pane's place is reserved all the time (from the md breakpoint
671
- // up): opening it changes nothing, and the grid's column count follows the width it has beside that place, never the selection. Below
672
- // md the detail is a sheet over the grid. There is no overlay mode: an overlay would hide the cards Previous / Next scrolls to.
645
+ // Two layouts, chosen by the viewport, never by the selection. From the `lg` breakpoint (1024px, an iPad in landscape) the detail is a
646
+ // pane to the right of the feed, and the grid's column count follows the width it has beside that pane. Below it (phones and a tablet in
647
+ // portrait) there is one column: the feed, or, once a reference is open, its detail in the feed's place at the full width with a Back
648
+ // control. There is no overlay: an overlay would hide the cards Previous / Next scrolls to.
649
+ const detailOpen = Boolean(selectedReference) || caughtUp;
673
650
 
674
651
  return (
675
652
  <div
676
- className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[100dvh] md:flex-row"
653
+ className="flex min-h-0 flex-1 flex-col gap-4 lg:max-h-[100dvh] lg:flex-row"
654
+ data-reference-layout={detailOpen ? "detail" : "feed"}
677
655
  data-news-desk-section="references"
678
656
  data-detail-open={selectedReference || routeLineageId ? "true" : "false"}
679
657
  >
680
- <div className="flex min-h-0 min-w-0 flex-1 flex-col gap-4 md:overflow-hidden">
658
+ <div className={cn("flex min-h-0 min-w-0 flex-1 flex-col gap-4 lg:flex lg:overflow-hidden", detailOpen && LIST_HIDDEN_WHEN_DETAIL_OPEN)} data-reference-feed-column>
681
659
  <div className="flex shrink-0 items-center justify-between gap-3" {...FEED_SURFACE_PROPS}>
682
660
  <div className="min-w-0 space-y-0.5">
683
661
  <h2 className="text-2xl font-bold tracking-tight">References</h2>
@@ -761,7 +739,7 @@ export function NewsroomReferencesView({
761
739
 
762
740
  {view === "feed" ? (
763
741
  <div className="relative flex min-h-0 flex-1 flex-col" {...FEED_SURFACE_PROPS}>
764
- {stable && stable.newCount > 0 && feedTab === "feed" ? (
742
+ {stable && stable.newCount > 0 ? (
765
743
  // Floats over the top of the grid, outside the scrolling content: announcing new items never moves a card.
766
744
  <div className="pointer-events-none absolute inset-x-0 top-2 z-20 flex justify-center">
767
745
  <button
@@ -784,19 +762,21 @@ export function NewsroomReferencesView({
784
762
  now={mountedAt}
785
763
  onOpen={selectReference}
786
764
  onRate={rateFromFeed}
787
- onTabChange={(next) => { setFeedTab(next); setCaughtUp(false); }}
765
+ onShowChange={(next) => { setFeedShow(next); setCaughtUp(false); }}
766
+ onSortChange={(next) => { setFeedSort(next); setCaughtUp(false); }}
788
767
  onVisitRated={markVisitRated}
789
- partition={partition}
768
+ items={feedItems}
769
+ ratedCount={ratedCount}
790
770
  predictionLoading={predictionState === "loading"}
791
771
  previews={previews}
792
772
  captures={showCaptures ? captureStatuses : undefined}
793
- groups={stable?.groups}
794
- leadId={stable?.leadId}
773
+ leadId={feedLead}
795
774
  scrollTarget={scrollTarget}
796
775
  ratings={ratings}
797
776
  selectedLineageId={selectedLineageId}
798
777
  stale={showingSavedCopy || (loading && references.length > 0)}
799
- tab={feedTab}
778
+ show={feedShow}
779
+ sort={feedSort}
800
780
  visitRated={visitRated}
801
781
  />
802
782
  </div>
@@ -886,9 +866,10 @@ export function NewsroomReferencesView({
886
866
 
887
867
  <div
888
868
  className={cn(
889
- "hidden min-h-0 w-full shrink-0 flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)] md:w-[min(100%,24rem)] md:pb-16 lg:w-[min(100%,28rem)]",
890
- // The pane appears once a reference is selected (or when the queue is done); until then the feed has the whole width.
891
- (selectedReference || caughtUp) && "md:flex",
869
+ "hidden min-h-0 w-full shrink-0 flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)] lg:pb-16",
870
+ DETAIL_PANE_WIDTH,
871
+ // The detail appears once a reference is selected (or when the queue is done); until then the feed has the whole width.
872
+ detailOpen && "flex",
892
873
  )}
893
874
  data-newsroom-list-detail-shell
894
875
  data-news-desk-section="references"
@@ -897,6 +878,10 @@ export function NewsroomReferencesView({
897
878
  {...FEED_SURFACE_PROPS}
898
879
  >
899
880
  <div className="flex shrink-0 items-center justify-between gap-2 px-4 pt-3 pb-1">
881
+ <Button className="-ml-2 lg:hidden" data-reference-back onClick={closeDetail} size="sm" type="button" variant="ghost">
882
+ <ArrowLeftIcon aria-hidden="true" />
883
+ References
884
+ </Button>
900
885
  <span className="text-sm font-medium text-foreground">Detail</span>
901
886
  </div>
902
887
  <div className="flex min-h-0 flex-1 flex-col" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}>
@@ -931,50 +916,6 @@ export function NewsroomReferencesView({
931
916
  {queueBar}
932
917
  </div>
933
918
 
934
- {isMobileDetail ? (
935
- <Sheet
936
- onOpenChange={(open) => {
937
- if (!open) closeDetail();
938
- else setMobileDetailOpen(true);
939
- }}
940
- open={(mobileDetailOpen && Boolean(selectedReference)) || caughtUp}
941
- >
942
- <SheetContent className="gap-0 rounded-t-[length:var(--feed-radius-xl)] bg-[var(--feed-card)] p-0" data-feed-flat="" data-feed-surface="" side="bottom" {...themeAttributes}>
943
- <SheetHeader className="border-b border-border px-4 py-3">
944
- <SheetTitle className="text-left text-base">Reference</SheetTitle>
945
- </SheetHeader>
946
- {selectedReference ? (
947
- <div className="min-w-0" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
948
- <ReferenceDetailPanel
949
- key={referenceLineageId(selectedReference)}
950
- captureTeaserLine={teaserLine}
951
- correctCapture={onCorrectCapture}
952
- ratings={ratings}
953
- sectionChoices={sectionChoices}
954
- predictionState={predictionState}
955
- reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
956
- reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
957
- countsAsTrainingLabel={humanCuratedReferenceLineageIds?.has(referenceLineageId(selectedReference))}
958
- delegated={delegatedCurations?.get(referenceLineageId(selectedReference))}
959
- decision={decisions?.get(referenceLineageId(selectedReference))}
960
- demo={demo}
961
- disabled={disabled}
962
- onClose={closeDetail}
963
- onMessageCount={setMessageCount}
964
- onRelevanceReview={onRelevanceReview ? (curation) => onRelevanceReview(selectedReference, curation) : undefined}
965
- onReview={runReview}
966
- sectionAgreement={sectionAgreement}
967
- sectionSuggestion={sectionSuggestions?.get(referenceLineageId(selectedReference))}
968
- sections={sections}
969
- previewAttachments={previewAttachments}
970
- reference={selectedReference}
971
- />
972
- </ScrollArea></div>
973
- ) : caughtUp ? caughtUpPanel : null}
974
- {queueBar}
975
- </SheetContent>
976
- </Sheet>
977
- ) : null}
978
919
  </div>
979
920
  );
980
921
  }
@@ -167,7 +167,7 @@ export function DraftCard({ busy, demo: _demo, error, item, onDecide, previews,
167
167
  <article aria-busy="true" className={CARD} data-draft-state="loading" data-review-draft={draftId} role="status" {...FEED_SURFACE_PROPS}>
168
168
  <div className="aspect-[16/10] animate-pulse bg-[var(--feed-control)]" />
169
169
  <div className="space-y-3 p-5">
170
- <p className="text-xs text-muted-foreground">{link?.label ?? "Draft"}: loading the draft…</p>
170
+ <p className="text-xs text-muted-foreground">{link?.label ?? "Proposed Story"}: loading the Proposed Story…</p>
171
171
  <div className="h-5 w-11/12 animate-pulse rounded-full bg-[var(--feed-control)]" />
172
172
  <div className="h-5 w-2/3 animate-pulse rounded-full bg-[var(--feed-control)]" />
173
173
  </div>
@@ -216,13 +216,13 @@ export function DraftCard({ busy, demo: _demo, error, item, onDecide, previews,
216
216
  </li>
217
217
  );
218
218
  })}
219
- {view.moreSources ? <li className="self-center text-xs text-muted-foreground">and {view.moreSources} more in the full draft</li> : null}
219
+ {view.moreSources ? <li className="self-center text-xs text-muted-foreground">and {view.moreSources} more in the full Proposed Story</li> : null}
220
220
  </ul>
221
221
  ) : null}
222
222
  <p className="mt-auto flex flex-wrap items-center justify-between gap-x-3 gap-y-1 pt-1 text-xs text-muted-foreground">
223
223
  <span data-draft-caption>{requestCaption(request)}</span>
224
224
  <Link className="inline-flex items-center gap-1 font-medium text-primary underline-offset-4 hover:underline" data-draft-open href={view.href}>
225
- Open full draft<ExternalLinkIcon aria-hidden="true" className="size-3.5 shrink-0" />
225
+ Open full Proposed Story<ExternalLinkIcon aria-hidden="true" className="size-3.5 shrink-0" />
226
226
  </Link>
227
227
  </p>
228
228
  </div>
@@ -232,7 +232,7 @@ export function DraftCard({ busy, demo: _demo, error, item, onDecide, previews,
232
232
  disabledReason={hold ?? session}
233
233
  error={error}
234
234
  kind="story"
235
- lockedNote={<>To change it, <Link className="text-primary underline underline-offset-4" href={view.href}>open the draft in Proposals</Link>.</>}
235
+ lockedNote={<>To change it, <Link className="text-primary underline underline-offset-4" href={view.href}>open the Proposed Story in Proposals</Link>.</>}
236
236
  onDecide={(choice, extras) => onDecide(draftId, choice, extras)}
237
237
  placeholder="Why? Optional. Your note teaches the AI."
238
238
  />
@@ -9,7 +9,6 @@ import { acceptPath, batchAnswer, draftDecisionOf, draftReasonUpdate, draftView,
9
9
  import { previewsBySourceAddress, recentDecisions } from "../lib/notification-feed";
10
10
  import { answerReasonUpdate, answerUpdate, applyReviewUpdate, cyclotronAutoDecided, draftIdsOf, isDraftReview, sectionReviews, type AnswerInput, type DraftDecision, type ReviewRequest } from "../lib/review-requests";
11
11
  import type { RelevanceDecisionView } from "../lib/relevance-decisions";
12
- import { learnFreshness } from "../lib/reference-feed-ranking";
13
12
  import { learnConfidenceThreshold, shapingThisWeek } from "../lib/needs-me-ranking";
14
13
  import { buildNeedsMeItems } from "../lib/needs-me-items";
15
14
  import { captureStatusesByLineage } from "../lib/reference-capture";
@@ -290,9 +289,9 @@ export function NotificationCenter({
290
289
  resolving.current.add(request.id);
291
290
  try {
292
291
  const view = drafts[draftId];
293
- if (!view) throw new Error("This draft has not loaded yet.");
292
+ if (!view) throw new Error("This Proposed Story has not loaded yet.");
294
293
  const path = choice === "accept" ? acceptPath(view.state) : rejectPath(view.state);
295
- if (!path) throw new Error(`This draft is ${stateLabel(view.state, false).toLowerCase()} and cannot be ${choice === "accept" ? "accepted" : "rejected"} from here. Open it in Proposals.`);
294
+ if (!path) throw new Error(`This Proposed Story is ${stateLabel(view.state, false).toLowerCase()} and cannot be ${choice === "accept" ? "accepted" : "rejected"} from here. Open it in Proposals.`);
296
295
  const receipt = await api.transitionProposalPath(draftId, path);
297
296
  if (receipt.status !== "ok") throw new Error(proposalReceiptMessage(receipt));
298
297
  const decided: DraftView = { ...view, state: choice === "accept" ? "approved" : "declined", decided: choice };
@@ -307,7 +306,7 @@ export function NotificationCenter({
307
306
  setRequests((current) => current.map((entry) => (entry.id === saved.id ? saved : entry)));
308
307
  if (closing) await markRead(request.id);
309
308
  } catch (failure) {
310
- throw new Error(`The draft was ${choice === "accept" ? "accepted" : "rejected"} in Proposals, but this request could not record it. ${failure instanceof Error ? failure.message : ""}`.trim());
309
+ throw new Error(`The Proposed Story was ${choice === "accept" ? "accepted" : "rejected"} in Proposals, but this request could not record it. ${failure instanceof Error ? failure.message : ""}`.trim());
311
310
  }
312
311
  } catch (failure) {
313
312
  if (!demo) void loadDraftsFor([request]);
@@ -375,7 +374,7 @@ export function NotificationCenter({
375
374
  const request = latest.current.requests.find((entry) => entry.id === payload.item.request.id) ?? payload.item.request;
376
375
  const view = latest.current.drafts[payload.item.draftId];
377
376
  const decision = draftDecisionOf(request.links.find((link) => link.id === payload.item.draftId), view, { by: actorLabel, at: now.toISOString() });
378
- if (!decision) throw new Error("This draft has not been decided yet.");
377
+ if (!decision) throw new Error("This Proposed Story has not been decided yet.");
379
378
  store(await api.saveReviewReason(request, draftReasonUpdate(request, payload.item.draftId, { ...decision, title: decision.title ?? view?.headline, why }, actorLabel, now)));
380
379
  } else if (payload.kind === "request") {
381
380
  const request = latest.current.requests.find((entry) => entry.id === payload.request.id) ?? payload.request;
@@ -417,7 +416,6 @@ export function NotificationCenter({
417
416
  }),
418
417
  [shownReferences, decisions, previews, captures, visitRated, requests, drafts, missingDrafts, draftsState, stay, bySource, storyTitles, storyExcerpts, deletedStories, emails, emailsDecided],
419
418
  );
420
- const freshness = useMemo(() => learnFreshness(shownReferences, visitRated), [shownReferences, visitRated]);
421
419
  const threshold = useMemo(() => learnConfidenceThreshold(shownReferences, decisions, visitRated), [shownReferences, decisions, visitRated]);
422
420
  const recent = useMemo(() => recentDecisions(requests, references, demo), [requests, references, demo]);
423
421
  const shaping = useMemo(
@@ -435,7 +433,7 @@ export function NotificationCenter({
435
433
 
436
434
  return (
437
435
  <NeedsMeView
438
- below={below} captureTeaserLine={teaserLine} demo={demo} draftsFailed={draftsState === "failed"} entries={entries} error={error} freshness={freshness} legacyAuto={legacyAuto}
436
+ below={below} captureTeaserLine={teaserLine} demo={demo} draftsFailed={draftsState === "failed"} entries={entries} error={error} legacyAuto={legacyAuto}
439
437
  loadEmail={api.loadEmail} loadState={preview === "loading" ? "loading" : loadState} missingDraftCount={missingCount} now={now} onCorrectCapture={onCorrectCapture} onDecide={decide} onEditReason={editReason}
440
438
  onRetryDrafts={() => void loadDraftsFor(requests)} predictionLoading={predictionLoading} recent={recent} referenceAttachments={referenceAttachments}
441
439
  referenceDisabledReason={referenceDisabledReason} referencesLoading={referencesLoading} settled={settled} shaping={shaping} stale={stale} threshold={threshold}
@@ -14,7 +14,7 @@ import {
14
14
 
15
15
  const FILTERS: Array<{ key: ProposalFilter; label: string }> = [
16
16
  { key: "open", label: "Open" },
17
- { key: "draft", label: "Drafts" },
17
+ { key: "draft", label: "Proposed" },
18
18
  { key: "in_steering", label: "In steering" },
19
19
  { key: "editor_review", label: "Editor review" },
20
20
  { key: "decided", label: "Decided" },
@@ -42,8 +42,8 @@ export function ProposalsView(props: ProposalsViewProps) {
42
42
  const shown = filterProposals(sorted, props.filter);
43
43
  return (
44
44
  <section aria-labelledby="newsroom-proposals-title" data-news-desk-admin-panel="proposals">
45
- <h3 id="newsroom-proposals-title">Proposals</h3>
46
- <p>Story drafts and other changes that AI proposes. Editors pin, hold and decide them. Only editors and admins can see this.</p>
45
+ <h3 id="newsroom-proposals-title">Proposed Stories</h3>
46
+ <p>Proposed Stories and other changes that AI proposes. Editors pin, hold and decide them. Only editors and admins can see this.</p>
47
47
  <p role="tablist" aria-label="Proposal views">
48
48
  {FILTERS.filter((entry) => props.readers || entry.key !== "in_steering").map((entry) => (
49
49
  <span key={entry.key}>
@@ -73,7 +73,7 @@ export function ProposalsView(props: ProposalsViewProps) {
73
73
  {row.summary ? <p>{row.summary}</p> : null}
74
74
  {row.body ? (
75
75
  <details open={props.focusId === row.id || undefined}>
76
- <summary>Read the draft</summary>
76
+ <summary>Read the Proposed Story</summary>
77
77
  <pre style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{row.body}</pre>
78
78
  </details>
79
79
  ) : null}
@@ -22,6 +22,7 @@ export function ReferenceConversation({ disabledReason = null, lineageId, onCoun
22
22
  const timer = window.setInterval(() => setNow(Date.now()), 60000);
23
23
  return () => window.clearInterval(timer);
24
24
  }, []);
25
+ // A deleted Message stays in the conversation, so it still counts.
25
26
  const settled = conversation.messages.filter((message) => !message.pending).length;
26
27
  useEffect(() => {
27
28
  if (conversation.state === "ready") onCount?.(lineageId, settled);
@@ -30,13 +31,17 @@ export function ReferenceConversation({ disabledReason = null, lineageId, onCoun
30
31
  return (
31
32
  <div data-reference-conversation={lineageId}>
32
33
  <ConversationThread
34
+ authorNames={conversation.names}
33
35
  disabledReason={disabledReason}
34
36
  error={conversation.error}
35
37
  messages={conversation.messages}
36
38
  now={now}
39
+ onDelete={conversation.remove}
40
+ onEdit={conversation.edit}
37
41
  onPost={conversation.post}
38
42
  onRetry={conversation.retry}
39
43
  state={conversation.state}
44
+ viewer={conversation.viewer}
40
45
  />
41
46
  </div>
42
47
  );