@anthusai/papyrus 0.0.0 → 1.0.0-next.2

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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,664 @@
1
+ "use client";
2
+
3
+ import {
4
+ ChevronLeftIcon,
5
+ ChevronRightIcon,
6
+ } from "lucide-react";
7
+ import { usePathname } from "next/navigation";
8
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
9
+ import { Badge } from "./ui/badge";
10
+ import { Button } from "./ui/button";
11
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
12
+ import { ScrollArea } from "./ui/scroll-area";
13
+ import { Separator } from "./ui/separator";
14
+ import {
15
+ Sheet,
16
+ SheetContent,
17
+ SheetHeader,
18
+ SheetTitle,
19
+ } from "./ui/sheet";
20
+ import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
21
+ import type { AssignmentRecord } from "../lib/category-repository";
22
+ import type { AssignmentAction, AssignmentStatusFilter } from "../lib/newsroom-assignments";
23
+ import {
24
+ assignmentListSubtitle,
25
+ assignmentStatusBadgeVariant,
26
+ assignmentStatusLabel,
27
+ compareAssignments,
28
+ filterAssignmentsByStatus,
29
+ formatAssignmentListDate,
30
+ formatAssignmentTypeLabel,
31
+ selectedAssignmentById,
32
+ } from "../lib/newsroom-assignments";
33
+ import {
34
+ effectiveAssignmentsIndexFilters,
35
+ parseAssignmentIdFromNewsroomPathname,
36
+ readAssignmentsIndexFilters,
37
+ syncBrowserNewsroomIndexUrl,
38
+ } from "../lib/newsroom-index-filters";
39
+ import { getNewsroomNavHref } from "../lib/newsroom-nav";
40
+ import { newsroomListRowClassName, NEWSROOM_SIDEBAR_NAV_ACTIVE } from "../lib/newsroom-list-selection";
41
+ import { cn } from "../lib/utils";
42
+
43
+ export type AssignmentDeskViewMode = "queue" | "budget";
44
+
45
+ export type ReportingPacketReviewDecision = "select" | "merge" | "brief" | "hold" | "kill";
46
+
47
+ export type ReportingPacketDecisionSummary = {
48
+ decision: ReportingPacketReviewDecision;
49
+ copywritingAssignmentId?: string | null;
50
+ copywritingStatus?: string | null;
51
+ draftItemId?: string | null;
52
+ eventId: string;
53
+ note?: string | null;
54
+ targetItemId?: string | null;
55
+ };
56
+
57
+ type NewsroomAssignmentsViewProps = {
58
+ assignments: AssignmentRecord[];
59
+ budgetPanel?: ReactNode;
60
+ demo?: boolean;
61
+ disabled?: boolean;
62
+ footerLabel?: string | null;
63
+ hasMore?: boolean;
64
+ initialAssignmentId?: string | null;
65
+ initialView?: AssignmentDeskViewMode;
66
+ isLoadingMore?: boolean;
67
+ onAction: (assignment: AssignmentRecord, action: AssignmentAction, note?: string) => void;
68
+ onLoadMore?: () => void;
69
+ onReviewReportingPacket?: (
70
+ assignment: AssignmentRecord,
71
+ decision: ReportingPacketReviewDecision,
72
+ note?: string,
73
+ targetItemId?: string,
74
+ ) => void;
75
+ renderAssignmentDetailExtras?: (assignment: AssignmentRecord) => ReactNode;
76
+ reportingDecisionForAssignment?: (assignmentId: string) => ReportingPacketDecisionSummary | null;
77
+ reportingPacketSummaryForAssignment?: (assignment: AssignmentRecord) => string | null;
78
+ };
79
+
80
+ const STATUS_FILTERS: Array<{ key: AssignmentStatusFilter; label: string }> = [
81
+ { key: "all", label: "All" },
82
+ { key: "open", label: "Open" },
83
+ { key: "claimed", label: "Claimed" },
84
+ { key: "completed", label: "Completed" },
85
+ { key: "canceled", label: "Canceled" },
86
+ ];
87
+
88
+ function buildAssignmentDetailPath(assignmentId: string, demo?: boolean): string {
89
+ return getNewsroomNavHref(`/newsroom/assignments/${encodeURIComponent(assignmentId)}`, demo);
90
+ }
91
+
92
+ function AssignmentDetailPanel({
93
+ assignment,
94
+ disabled,
95
+ note,
96
+ onAction,
97
+ onClose,
98
+ onNoteChange,
99
+ onReviewReportingPacket,
100
+ onReportingMergeTargetChange,
101
+ reportingDecision,
102
+ reportingMergeTargetItemId,
103
+ reportingPacketSummary,
104
+ detailExtras,
105
+ }: {
106
+ assignment: AssignmentRecord;
107
+ disabled?: boolean;
108
+ note: string;
109
+ onAction: (action: AssignmentAction) => void;
110
+ onClose?: () => void;
111
+ onNoteChange: (note: string) => void;
112
+ onReviewReportingPacket?: (decision: ReportingPacketReviewDecision) => void;
113
+ onReportingMergeTargetChange: (value: string) => void;
114
+ reportingDecision: ReportingPacketDecisionSummary | null;
115
+ reportingMergeTargetItemId: string;
116
+ reportingPacketSummary: string | null;
117
+ detailExtras?: ReactNode;
118
+ }) {
119
+ const terminal = assignment.status === "completed" || assignment.status === "canceled";
120
+ const isReporting = assignment.assignmentTypeKey === "reporting.edition-candidate";
121
+
122
+ return (
123
+ <div
124
+ className="flex h-full flex-col font-sans"
125
+ data-news-desk-assignment-detail={assignment.id}
126
+ >
127
+ <article
128
+ className={`news-desk-assignment-row${terminal ? " news-desk-assignment-row--terminal" : ""}`}
129
+ data-assignment-candidate={assignment.id}
130
+ data-assignment-id={assignment.id}
131
+ data-assignment-status={assignment.status}
132
+ data-reporting-decision={reportingDecision?.decision ?? ""}
133
+ >
134
+ <div className="space-y-4 p-4">
135
+ <div className="space-y-2">
136
+ <div className="flex flex-wrap items-center gap-2">
137
+ <Badge variant={assignmentStatusBadgeVariant(assignment.status)}>
138
+ {assignmentStatusLabel(assignment.status)}
139
+ </Badge>
140
+ <Badge variant="outline">{formatAssignmentTypeLabel(assignment.assignmentTypeKey)}</Badge>
141
+ {assignment.sectionKey ? (
142
+ <Badge variant="outline">{assignment.sectionKey}</Badge>
143
+ ) : null}
144
+ </div>
145
+ <p className="text-xs text-muted-foreground">{assignment.assignmentTypeKey}</p>
146
+ <h2 className="text-lg font-semibold leading-snug text-foreground">{assignment.title}</h2>
147
+ {assignment.summary ? (
148
+ <p className="text-sm text-muted-foreground">{assignment.summary}</p>
149
+ ) : null}
150
+ </div>
151
+
152
+ <div className="flex flex-wrap gap-2" data-news-desk-assignment-actions>
153
+ {assignment.status === "open" ? (
154
+ <Button
155
+ data-news-desk-assignment-claim
156
+ disabled={disabled}
157
+ onClick={() => onAction("claim")}
158
+ size="sm"
159
+ type="button"
160
+ >
161
+ Claim
162
+ </Button>
163
+ ) : null}
164
+ {assignment.status === "claimed" ? (
165
+ <Button
166
+ data-news-desk-assignment-release
167
+ disabled={disabled}
168
+ onClick={() => onAction("release")}
169
+ size="sm"
170
+ type="button"
171
+ variant="outline"
172
+ >
173
+ Release
174
+ </Button>
175
+ ) : null}
176
+ {!terminal ? (
177
+ <>
178
+ <Button
179
+ data-news-desk-assignment-complete
180
+ disabled={disabled}
181
+ onClick={() => onAction("complete")}
182
+ size="sm"
183
+ type="button"
184
+ >
185
+ Complete
186
+ </Button>
187
+ <Button
188
+ data-news-desk-assignment-cancel
189
+ disabled={disabled}
190
+ onClick={() => onAction("cancel")}
191
+ size="sm"
192
+ type="button"
193
+ variant="destructive"
194
+ >
195
+ Cancel
196
+ </Button>
197
+ </>
198
+ ) : (
199
+ <Button
200
+ data-news-desk-assignment-reopen
201
+ disabled={disabled}
202
+ onClick={() => onAction("reopen")}
203
+ size="sm"
204
+ type="button"
205
+ variant="outline"
206
+ >
207
+ Reopen
208
+ </Button>
209
+ )}
210
+ </div>
211
+
212
+ {isReporting && reportingPacketSummary ? (
213
+ <div className="space-y-2 rounded-xl border border-border bg-muted/20 p-3" data-reporting-packet-review>
214
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Reporting packet</p>
215
+ <p className="text-sm text-foreground">{reportingPacketSummary}</p>
216
+ {onReviewReportingPacket ? (
217
+ <div className="flex flex-wrap gap-2">
218
+ <Button disabled={disabled} onClick={() => onReviewReportingPacket("select")} size="sm" type="button">
219
+ Select Packet
220
+ </Button>
221
+ <Button disabled={disabled} onClick={() => onReviewReportingPacket("brief")} size="sm" type="button" variant="outline">
222
+ Make Brief
223
+ </Button>
224
+ <Button disabled={disabled} onClick={() => onReviewReportingPacket("hold")} size="sm" type="button" variant="outline">
225
+ Hold Packet
226
+ </Button>
227
+ <Button disabled={disabled} onClick={() => onReviewReportingPacket("kill")} size="sm" type="button" variant="destructive">
228
+ Kill Packet
229
+ </Button>
230
+ <label className="flex w-full flex-col gap-1 text-xs text-muted-foreground">
231
+ <span>Merge target Item ID</span>
232
+ <input
233
+ className="rounded-md border border-border bg-background px-2 py-1.5 text-sm text-foreground"
234
+ data-reporting-merge-target={assignment.id}
235
+ disabled={disabled}
236
+ onChange={(event) => onReportingMergeTargetChange(event.target.value)}
237
+ placeholder="Required only for Merge Packet"
238
+ value={reportingMergeTargetItemId}
239
+ />
240
+ </label>
241
+ <Button
242
+ disabled={disabled || !reportingMergeTargetItemId.trim()}
243
+ onClick={() => onReviewReportingPacket("merge")}
244
+ size="sm"
245
+ type="button"
246
+ variant="outline"
247
+ >
248
+ Merge Packet
249
+ </Button>
250
+ </div>
251
+ ) : null}
252
+ {reportingDecision ? (
253
+ <p className="text-xs text-muted-foreground">
254
+ Decision: {reportingDecision.decision}
255
+ {reportingDecision.copywritingAssignmentId ? (
256
+ <span data-reporting-copywriting-assignment={reportingDecision.copywritingAssignmentId}>
257
+ {" "}· copywriting {reportingDecision.copywritingAssignmentId}
258
+ </span>
259
+ ) : null}
260
+ {reportingDecision.draftItemId ? (
261
+ <span data-reporting-draft-item={reportingDecision.draftItemId}>
262
+ {" "}· draft {reportingDecision.draftItemId}
263
+ </span>
264
+ ) : null}
265
+ </p>
266
+ ) : null}
267
+ </div>
268
+ ) : null}
269
+
270
+ <Separator />
271
+
272
+ <dl className="grid gap-3 text-sm">
273
+ <div>
274
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Queue</dt>
275
+ <dd className="mt-1 text-foreground">{assignment.queueKey}</dd>
276
+ </div>
277
+ <div className="grid grid-cols-2 gap-3">
278
+ <div>
279
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Assignee</dt>
280
+ <dd className="mt-1 text-foreground">{assignment.assigneeKey ?? assignment.assigneeType ?? "Unassigned"}</dd>
281
+ </div>
282
+ <div>
283
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Updated</dt>
284
+ <dd className="mt-1 text-foreground">{formatAssignmentListDate(assignment)}</dd>
285
+ </div>
286
+ </div>
287
+ </dl>
288
+
289
+ <label className="block space-y-1.5 text-sm">
290
+ <span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Note</span>
291
+ <textarea
292
+ className="min-h-16 w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
293
+ data-assignment-reason={assignment.id}
294
+ disabled={disabled}
295
+ onChange={(event) => onNoteChange(event.target.value)}
296
+ rows={2}
297
+ value={note}
298
+ />
299
+ </label>
300
+
301
+ {detailExtras}
302
+ </div>
303
+ </article>
304
+
305
+ {onClose ? (
306
+ <div className="border-t border-border p-4">
307
+ <Button className="w-full" onClick={onClose} type="button" variant="secondary">
308
+ Close
309
+ </Button>
310
+ </div>
311
+ ) : null}
312
+ </div>
313
+ );
314
+ }
315
+
316
+ export function NewsroomAssignmentsView({
317
+ assignments,
318
+ budgetPanel,
319
+ demo = false,
320
+ disabled = false,
321
+ footerLabel,
322
+ hasMore = false,
323
+ initialAssignmentId = null,
324
+ initialView = "queue",
325
+ isLoadingMore = false,
326
+ onAction,
327
+ onLoadMore,
328
+ onReviewReportingPacket,
329
+ renderAssignmentDetailExtras,
330
+ reportingDecisionForAssignment,
331
+ reportingPacketSummaryForAssignment,
332
+ }: NewsroomAssignmentsViewProps) {
333
+ const pathname = usePathname();
334
+ const pathnameAssignmentId = useMemo(
335
+ () => parseAssignmentIdFromNewsroomPathname(pathname),
336
+ [pathname],
337
+ );
338
+ const routeAssignmentId = initialAssignmentId ?? pathnameAssignmentId ?? "";
339
+
340
+ const [viewMode, setViewMode] = useState<AssignmentDeskViewMode>(initialView);
341
+ const [statusFilter, setStatusFilter] = useState<AssignmentStatusFilter>(() => {
342
+ if (typeof window === "undefined") return "all";
343
+ const filters = readAssignmentsIndexFilters(new URLSearchParams(window.location.search));
344
+ const status = filters.status?.trim();
345
+ if (status === "open" || status === "claimed" || status === "completed" || status === "canceled") {
346
+ return status;
347
+ }
348
+ return "all";
349
+ });
350
+ const [selectedAssignmentId, setSelectedAssignmentId] = useState(routeAssignmentId);
351
+ const [mobileDetailOpen, setMobileDetailOpen] = useState(Boolean(routeAssignmentId));
352
+ const [actionNote, setActionNote] = useState("");
353
+ const [reportingMergeTargetItemId, setReportingMergeTargetItemId] = useState("");
354
+
355
+ const sortedAssignments = useMemo(
356
+ () => [...assignments].sort(compareAssignments),
357
+ [assignments],
358
+ );
359
+ const statusCounts = useMemo(() => {
360
+ const counts: Record<AssignmentStatusFilter, number> = {
361
+ all: sortedAssignments.length,
362
+ open: 0,
363
+ claimed: 0,
364
+ completed: 0,
365
+ canceled: 0,
366
+ };
367
+ for (const assignment of sortedAssignments) {
368
+ const status = assignment.status as AssignmentStatusFilter;
369
+ if (status in counts && status !== "all") counts[status] += 1;
370
+ }
371
+ return counts;
372
+ }, [sortedAssignments]);
373
+
374
+ const filteredAssignments = useMemo(
375
+ () => filterAssignmentsByStatus(sortedAssignments, statusFilter),
376
+ [sortedAssignments, statusFilter],
377
+ );
378
+
379
+ const selectedAssignment = useMemo(
380
+ () => selectedAssignmentById(filteredAssignments, selectedAssignmentId)
381
+ ?? selectedAssignmentById(sortedAssignments, selectedAssignmentId),
382
+ [filteredAssignments, selectedAssignmentId, sortedAssignments],
383
+ );
384
+
385
+ const selectedIndex = selectedAssignment
386
+ ? filteredAssignments.findIndex((entry) => entry.id === selectedAssignment.id)
387
+ : -1;
388
+ const previousAssignment = selectedIndex > 0 ? filteredAssignments[selectedIndex - 1] : null;
389
+ const nextAssignment = selectedIndex >= 0 && selectedIndex < filteredAssignments.length - 1
390
+ ? filteredAssignments[selectedIndex + 1]
391
+ : null;
392
+
393
+ const syncStatusUrl = useCallback((nextStatus: AssignmentStatusFilter, nextView: AssignmentDeskViewMode) => {
394
+ if (demo) return;
395
+ syncBrowserNewsroomIndexUrl("assignments", effectiveAssignmentsIndexFilters({
396
+ status: nextStatus === "all" ? "" : nextStatus,
397
+ type: "",
398
+ view: nextView === "budget" ? "budget" : "queue",
399
+ }), { replace: true });
400
+ }, [demo]);
401
+
402
+ useEffect(() => {
403
+ if (!routeAssignmentId) return;
404
+ setSelectedAssignmentId(routeAssignmentId);
405
+ setMobileDetailOpen(true);
406
+ }, [routeAssignmentId]);
407
+
408
+ useEffect(() => {
409
+ setActionNote("");
410
+ setReportingMergeTargetItemId("");
411
+ }, [selectedAssignment?.id, selectedAssignment?.status]);
412
+
413
+ const selectAssignment = (assignmentId: string) => {
414
+ setSelectedAssignmentId(assignmentId);
415
+ setMobileDetailOpen(true);
416
+ if (typeof window !== "undefined") {
417
+ const nextPath = buildAssignmentDetailPath(assignmentId, demo);
418
+ if (`${window.location.pathname}${window.location.search}` !== nextPath) {
419
+ window.history.pushState(null, "", nextPath);
420
+ }
421
+ }
422
+ };
423
+
424
+ const closeDetail = () => {
425
+ setMobileDetailOpen(false);
426
+ setSelectedAssignmentId("");
427
+ if (typeof window !== "undefined") {
428
+ const indexPath = getNewsroomNavHref("/newsroom/assignments", demo);
429
+ window.history.replaceState(null, "", indexPath);
430
+ }
431
+ };
432
+
433
+ const runAction = (action: AssignmentAction) => {
434
+ if (!selectedAssignment) return;
435
+ onAction(selectedAssignment, action, actionNote);
436
+ setActionNote("");
437
+ };
438
+
439
+ const runReportingReview = (decision: ReportingPacketReviewDecision) => {
440
+ if (!selectedAssignment || !onReviewReportingPacket) return;
441
+ onReviewReportingPacket(selectedAssignment, decision, actionNote, reportingMergeTargetItemId);
442
+ setActionNote("");
443
+ };
444
+
445
+ const selectViewMode = (nextView: AssignmentDeskViewMode) => {
446
+ setViewMode(nextView);
447
+ syncStatusUrl(statusFilter, nextView);
448
+ };
449
+
450
+ const emptyLabel = sortedAssignments.length
451
+ ? "No assignments match this filter."
452
+ : "No assignments in the queue yet.";
453
+
454
+ const reportingDecision = selectedAssignment && reportingDecisionForAssignment
455
+ ? reportingDecisionForAssignment(selectedAssignment.id)
456
+ : null;
457
+ const reportingPacketSummary = selectedAssignment && reportingPacketSummaryForAssignment
458
+ ? reportingPacketSummaryForAssignment(selectedAssignment)
459
+ : null;
460
+
461
+ return (
462
+ <div
463
+ className="flex min-h-0 flex-1 flex-col gap-4 font-sans md:flex-row"
464
+ data-detail-open={selectedAssignment || routeAssignmentId ? "true" : "false"}
465
+ data-news-desk-assignments
466
+ data-news-desk-section="assignments"
467
+ >
468
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col gap-4">
469
+ <div className="flex flex-wrap items-start justify-between gap-3">
470
+ <div className="space-y-1">
471
+ <h2 className="text-lg font-semibold tracking-tight">Assignments</h2>
472
+ <p className="text-sm text-muted-foreground">
473
+ Claim, complete, and review newsroom work from the private queue.
474
+ </p>
475
+ </div>
476
+ <div className="flex gap-1 rounded-lg border border-border bg-muted/30 p-1" role="group" aria-label="Assignment view">
477
+ <Button
478
+ className={viewMode === "queue" ? NEWSROOM_SIDEBAR_NAV_ACTIVE : undefined}
479
+ data-active={viewMode === "queue" || undefined}
480
+ onClick={() => selectViewMode("queue")}
481
+ size="sm"
482
+ type="button"
483
+ variant="ghost"
484
+ >
485
+ Queue
486
+ </Button>
487
+ <Button
488
+ className={viewMode === "budget" ? NEWSROOM_SIDEBAR_NAV_ACTIVE : undefined}
489
+ data-active={viewMode === "budget" || undefined}
490
+ onClick={() => selectViewMode("budget")}
491
+ size="sm"
492
+ type="button"
493
+ variant="ghost"
494
+ >
495
+ Story Budget
496
+ </Button>
497
+ </div>
498
+ </div>
499
+
500
+ {viewMode === "budget" ? (
501
+ <div className="min-h-0 flex-1">{budgetPanel}</div>
502
+ ) : (
503
+ <>
504
+ <Tabs
505
+ defaultValue="all"
506
+ onValueChange={(value) => {
507
+ const next = value as AssignmentStatusFilter;
508
+ setStatusFilter(next);
509
+ syncStatusUrl(next, viewMode);
510
+ }}
511
+ value={statusFilter}
512
+ >
513
+ <TabsList className="h-auto w-full flex-wrap justify-start gap-1 bg-muted/40 p-1">
514
+ {STATUS_FILTERS.map((filter) => (
515
+ <TabsTrigger className="text-xs sm:text-sm" key={filter.key} value={filter.key}>
516
+ {filter.label}
517
+ <Badge className="ml-1.5 min-w-5 px-1.5" variant="secondary">
518
+ {statusCounts[filter.key]}
519
+ </Badge>
520
+ </TabsTrigger>
521
+ ))}
522
+ </TabsList>
523
+ </Tabs>
524
+
525
+ <ScrollArea className="min-h-0 flex-1 md:max-h-[calc(100dvh-16rem)]">
526
+ <div className="flex flex-col gap-2 pb-4" data-newsroom-card-grid>
527
+ {filteredAssignments.length ? filteredAssignments.map((assignment) => {
528
+ const active = selectedAssignmentId === assignment.id;
529
+ return (
530
+ <button
531
+ aria-current={active ? "true" : undefined}
532
+ className={newsroomListRowClassName(active)}
533
+ data-newsroom-list-row
534
+ data-selected={active || undefined}
535
+ data-assignment-candidate={assignment.id}
536
+ data-assignment-status={assignment.status}
537
+ data-newsroom-card
538
+ data-newsroom-card-id={assignment.id}
539
+ key={assignment.id}
540
+ onClick={() => selectAssignment(assignment.id)}
541
+ type="button"
542
+ >
543
+ <div className="flex items-start justify-between gap-3">
544
+ <div className="min-w-0 flex-1 space-y-1">
545
+ <p className="truncate text-sm font-medium text-foreground">{assignment.title}</p>
546
+ <p className="truncate text-xs text-muted-foreground">{assignmentListSubtitle(assignment)}</p>
547
+ </div>
548
+ <Badge variant={assignmentStatusBadgeVariant(assignment.status)}>
549
+ {assignmentStatusLabel(assignment.status)}
550
+ </Badge>
551
+ </div>
552
+ <p className="mt-2 text-xs text-muted-foreground">{formatAssignmentListDate(assignment)}</p>
553
+ </button>
554
+ );
555
+ }) : (
556
+ <Card>
557
+ <CardHeader>
558
+ <CardTitle className="text-base">No assignments</CardTitle>
559
+ <CardDescription>{emptyLabel}</CardDescription>
560
+ </CardHeader>
561
+ </Card>
562
+ )}
563
+ </div>
564
+ {footerLabel ? (
565
+ <p className="px-1 pb-2 text-xs text-muted-foreground">{footerLabel}</p>
566
+ ) : null}
567
+ {hasMore && onLoadMore ? (
568
+ <div className="pb-4">
569
+ <Button disabled={isLoadingMore} onClick={onLoadMore} type="button" variant="outline">
570
+ {isLoadingMore ? "Loading…" : "Load more"}
571
+ </Button>
572
+ </div>
573
+ ) : null}
574
+ </ScrollArea>
575
+ </>
576
+ )}
577
+ </div>
578
+
579
+ {viewMode === "queue" ? (
580
+ <div
581
+ className="hidden min-h-0 w-full shrink-0 flex-col rounded-xl border border-border bg-card md:flex md:w-[min(100%,24rem)] lg:w-[min(100%,28rem)]"
582
+ data-detail-open={selectedAssignment ? "true" : "false"}
583
+ data-newsroom-list-detail-shell
584
+ >
585
+ <div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
586
+ <span className="text-sm font-medium text-foreground">Detail</span>
587
+ <div className="newsroom-list-detail-shell__detail-toolbar-trailing flex items-center gap-1">
588
+ <Button
589
+ aria-label="Previous"
590
+ disabled={!previousAssignment}
591
+ onClick={() => previousAssignment && selectAssignment(previousAssignment.id)}
592
+ size="icon-sm"
593
+ type="button"
594
+ variant="ghost"
595
+ >
596
+ <ChevronLeftIcon className="size-4" />
597
+ </Button>
598
+ <Button
599
+ aria-label="Next"
600
+ disabled={!nextAssignment}
601
+ onClick={() => nextAssignment && selectAssignment(nextAssignment.id)}
602
+ size="icon-sm"
603
+ type="button"
604
+ variant="ghost"
605
+ >
606
+ <ChevronRightIcon className="size-4" />
607
+ </Button>
608
+ </div>
609
+ </div>
610
+ <ScrollArea className="min-h-0 flex-1">
611
+ {selectedAssignment ? (
612
+ <AssignmentDetailPanel
613
+ assignment={selectedAssignment}
614
+ detailExtras={renderAssignmentDetailExtras?.(selectedAssignment)}
615
+ disabled={disabled}
616
+ note={actionNote}
617
+ onAction={runAction}
618
+ onNoteChange={setActionNote}
619
+ onReportingMergeTargetChange={setReportingMergeTargetItemId}
620
+ onReviewReportingPacket={onReviewReportingPacket ? runReportingReview : undefined}
621
+ reportingDecision={reportingDecision}
622
+ reportingMergeTargetItemId={reportingMergeTargetItemId}
623
+ reportingPacketSummary={reportingPacketSummary}
624
+ />
625
+ ) : (
626
+ <CardContent className="p-4 text-sm text-muted-foreground">
627
+ Select an assignment to inspect work details.
628
+ </CardContent>
629
+ )}
630
+ </ScrollArea>
631
+ </div>
632
+ ) : null}
633
+
634
+ <Sheet onOpenChange={(open) => {
635
+ if (!open) closeDetail();
636
+ else setMobileDetailOpen(true);
637
+ }} open={mobileDetailOpen && Boolean(selectedAssignment) && viewMode === "queue"}>
638
+ <SheetContent className="gap-0 p-0 md:hidden" side="bottom">
639
+ <SheetHeader className="border-b border-border px-4 py-3">
640
+ <SheetTitle className="text-left text-base">Assignment</SheetTitle>
641
+ </SheetHeader>
642
+ {selectedAssignment ? (
643
+ <ScrollArea className="max-h-[75dvh]">
644
+ <AssignmentDetailPanel
645
+ assignment={selectedAssignment}
646
+ detailExtras={renderAssignmentDetailExtras?.(selectedAssignment)}
647
+ disabled={disabled}
648
+ note={actionNote}
649
+ onAction={runAction}
650
+ onClose={closeDetail}
651
+ onNoteChange={setActionNote}
652
+ onReportingMergeTargetChange={setReportingMergeTargetItemId}
653
+ onReviewReportingPacket={onReviewReportingPacket ? runReportingReview : undefined}
654
+ reportingDecision={reportingDecision}
655
+ reportingMergeTargetItemId={reportingMergeTargetItemId}
656
+ reportingPacketSummary={reportingPacketSummary}
657
+ />
658
+ </ScrollArea>
659
+ ) : null}
660
+ </SheetContent>
661
+ </Sheet>
662
+ </div>
663
+ );
664
+ }