@anthusai/papyrus 0.0.0 → 1.0.0-next.10
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2069 -0
- package/amplify/data/resource.ts +2185 -0
- package/amplify/data/schema.js +1882 -0
- package/amplify/data/schema.ts +1990 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/content-actions/handler.py +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +130 -0
- package/amplify/functions/shared/python-bundle.ts +162 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +36 -0
- package/amplify/site-backend.js +615 -0
- package/amplify/site-backend.ts +739 -0
- package/amplify/storage/resource.js +21 -0
- package/amplify/storage/resource.ts +22 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10345 -0
- package/app/layout.tsx +142 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/papyrus-theme.css +11 -0
- package/app/robots.ts +11 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +20 -0
- package/bin/papyrus-infra.mjs +95 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +156 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +116 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +321 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +18005 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +234 -0
- package/infra/amplify-app-shell.ts +284 -0
- package/infra/build-specs.js +85 -0
- package/infra/build-specs.ts +131 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +15 -0
- package/infra/site-config.js +186 -0
- package/infra/site-config.ts +235 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +90 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +43 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/empty-theme.css +1 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +1140 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +44 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +22 -0
- package/lib/site-brand.ts +194 -0
- package/lib/site-env.ts +52 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +103 -0
- package/package.json +318 -1
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +169 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +470 -0
- package/routes.manifest.json +208 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +60 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export type NewsroomIndexTab = "references" | "messages" | "assignments" | "insights";
|
|
2
|
+
|
|
3
|
+
export type ReferenceIndexOrder = "published" | "imported";
|
|
4
|
+
|
|
5
|
+
export type ReferencesIndexFilters = { status: string; processing: string; order: ReferenceIndexOrder };
|
|
6
|
+
export type MessagesIndexFilters = { kind: string; domain: string };
|
|
7
|
+
export type AssignmentsIndexFilters = { status: string; type: string; view: string };
|
|
8
|
+
export type InsightsIndexFilters = { domain: string };
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_REFERENCES_INDEX_FILTERS: ReferencesIndexFilters = {
|
|
11
|
+
status: "exclude-pending",
|
|
12
|
+
processing: "",
|
|
13
|
+
order: "published",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_MESSAGES_INDEX_FILTERS: MessagesIndexFilters = {
|
|
17
|
+
kind: "",
|
|
18
|
+
domain: "",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_ASSIGNMENTS_INDEX_FILTERS: AssignmentsIndexFilters = {
|
|
22
|
+
status: "",
|
|
23
|
+
type: "",
|
|
24
|
+
view: "queue",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_INSIGHTS_INDEX_FILTERS: InsightsIndexFilters = {
|
|
28
|
+
domain: "",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function referencesStatusFromUrl(value: string): string {
|
|
32
|
+
return value === "exclude-pending" ? "__exclude_pending" : value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function referencesStatusToUrl(value: string): string {
|
|
36
|
+
return value === "__exclude_pending" ? "exclude-pending" : value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeReferenceIndexOrder(value: string | null | undefined): ReferenceIndexOrder {
|
|
40
|
+
return value?.trim() === "imported" ? "imported" : "published";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function effectiveReferencesIndexFilters(partial?: Partial<ReferencesIndexFilters>): ReferencesIndexFilters {
|
|
44
|
+
const order = normalizeReferenceIndexOrder(partial?.order);
|
|
45
|
+
const explicitStatus = partial?.status?.trim();
|
|
46
|
+
let status = explicitStatus || DEFAULT_REFERENCES_INDEX_FILTERS.status;
|
|
47
|
+
// Import-date sort is for intake triage; the default "reviewed only" filter hides
|
|
48
|
+
// pending references created by inbound email and other intake paths.
|
|
49
|
+
if (order === "imported" && !explicitStatus) {
|
|
50
|
+
status = "";
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
status,
|
|
54
|
+
processing: partial?.processing?.trim() ?? DEFAULT_REFERENCES_INDEX_FILTERS.processing,
|
|
55
|
+
order,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function effectiveMessagesIndexFilters(partial?: Partial<MessagesIndexFilters>): MessagesIndexFilters {
|
|
60
|
+
return {
|
|
61
|
+
kind: partial?.kind?.trim() ?? DEFAULT_MESSAGES_INDEX_FILTERS.kind,
|
|
62
|
+
domain: partial?.domain?.trim() ?? DEFAULT_MESSAGES_INDEX_FILTERS.domain,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function effectiveAssignmentsIndexFilters(partial?: Partial<AssignmentsIndexFilters>): AssignmentsIndexFilters {
|
|
67
|
+
return {
|
|
68
|
+
status: partial?.status?.trim() ?? DEFAULT_ASSIGNMENTS_INDEX_FILTERS.status,
|
|
69
|
+
type: partial?.type?.trim() ?? DEFAULT_ASSIGNMENTS_INDEX_FILTERS.type,
|
|
70
|
+
view: partial?.view?.trim() || DEFAULT_ASSIGNMENTS_INDEX_FILTERS.view,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function effectiveInsightsIndexFilters(partial?: Partial<InsightsIndexFilters>): InsightsIndexFilters {
|
|
75
|
+
return {
|
|
76
|
+
domain: partial?.domain?.trim() ?? DEFAULT_INSIGHTS_INDEX_FILTERS.domain,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function readReferencesIndexFilters(searchParams: URLSearchParams): ReferencesIndexFilters {
|
|
81
|
+
const statusParam = searchParams.get("status")?.trim() ?? "";
|
|
82
|
+
const orderParam = searchParams.get("order")?.trim();
|
|
83
|
+
return effectiveReferencesIndexFilters({
|
|
84
|
+
status: statusParam ? referencesStatusFromUrl(statusParam) : undefined,
|
|
85
|
+
processing: searchParams.get("processing")?.trim() ?? undefined,
|
|
86
|
+
order: orderParam ? normalizeReferenceIndexOrder(orderParam) : undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function readMessagesIndexFilters(searchParams: URLSearchParams): MessagesIndexFilters {
|
|
91
|
+
return effectiveMessagesIndexFilters({
|
|
92
|
+
kind: searchParams.get("kind")?.trim() ?? undefined,
|
|
93
|
+
domain: searchParams.get("domain")?.trim() ?? undefined,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function readAssignmentsIndexFilters(searchParams: URLSearchParams): AssignmentsIndexFilters {
|
|
98
|
+
return effectiveAssignmentsIndexFilters({
|
|
99
|
+
status: searchParams.get("status")?.trim() ?? undefined,
|
|
100
|
+
type: searchParams.get("type")?.trim() ?? undefined,
|
|
101
|
+
view: searchParams.get("view")?.trim() ?? undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readInsightsIndexFilters(searchParams: URLSearchParams): InsightsIndexFilters {
|
|
106
|
+
return effectiveInsightsIndexFilters({
|
|
107
|
+
domain: searchParams.get("domain")?.trim() ?? undefined,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildReferencesIndexQuery(filters: ReferencesIndexFilters): string {
|
|
112
|
+
const params = new URLSearchParams();
|
|
113
|
+
if (filters.status && filters.status !== DEFAULT_REFERENCES_INDEX_FILTERS.status) {
|
|
114
|
+
params.set("status", referencesStatusToUrl(filters.status));
|
|
115
|
+
}
|
|
116
|
+
if (filters.processing.trim()) params.set("processing", filters.processing.trim());
|
|
117
|
+
if (filters.order === "imported") params.set("order", "imported");
|
|
118
|
+
const query = params.toString();
|
|
119
|
+
return query ? `?${query}` : "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function buildMessagesIndexQuery(filters: MessagesIndexFilters): string {
|
|
123
|
+
const params = new URLSearchParams();
|
|
124
|
+
if (filters.kind.trim()) params.set("kind", filters.kind.trim());
|
|
125
|
+
if (filters.domain.trim()) params.set("domain", filters.domain.trim());
|
|
126
|
+
const query = params.toString();
|
|
127
|
+
return query ? `?${query}` : "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function buildAssignmentsIndexQuery(filters: AssignmentsIndexFilters): string {
|
|
131
|
+
const params = new URLSearchParams();
|
|
132
|
+
if (filters.status.trim()) params.set("status", filters.status.trim());
|
|
133
|
+
if (filters.type.trim()) params.set("type", filters.type.trim());
|
|
134
|
+
if (filters.view.trim() === "budget") params.set("view", "budget");
|
|
135
|
+
const query = params.toString();
|
|
136
|
+
return query ? `?${query}` : "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildInsightsIndexQuery(filters: InsightsIndexFilters): string {
|
|
140
|
+
const params = new URLSearchParams();
|
|
141
|
+
if (filters.domain.trim()) params.set("domain", filters.domain.trim());
|
|
142
|
+
const query = params.toString();
|
|
143
|
+
return query ? `?${query}` : "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function buildNewsroomIndexWebPath(
|
|
147
|
+
tab: NewsroomIndexTab,
|
|
148
|
+
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters | InsightsIndexFilters,
|
|
149
|
+
): string {
|
|
150
|
+
if (tab === "references") return `/newsroom/references${buildReferencesIndexQuery(filters as ReferencesIndexFilters)}`;
|
|
151
|
+
if (tab === "messages") return `/newsroom/messages${buildMessagesIndexQuery(filters as MessagesIndexFilters)}`;
|
|
152
|
+
if (tab === "insights") return `/newsroom/insights${buildInsightsIndexQuery(filters as InsightsIndexFilters)}`;
|
|
153
|
+
return `/newsroom/assignments${buildAssignmentsIndexQuery(filters as AssignmentsIndexFilters)}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function syncBrowserNewsroomIndexUrl(
|
|
157
|
+
tab: NewsroomIndexTab,
|
|
158
|
+
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters | InsightsIndexFilters,
|
|
159
|
+
options?: { replace?: boolean },
|
|
160
|
+
) {
|
|
161
|
+
if (typeof window === "undefined") return;
|
|
162
|
+
const nextPath = buildNewsroomIndexWebPath(tab, filters);
|
|
163
|
+
const current = `${window.location.pathname}${window.location.search}`;
|
|
164
|
+
if (current === nextPath) return;
|
|
165
|
+
if (options?.replace) window.history.replaceState(null, "", nextPath);
|
|
166
|
+
else window.history.pushState(null, "", nextPath);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Lineage id from `/newsroom/references/<id>` (not index-only URLs). */
|
|
170
|
+
export function parseReferenceLineageIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
171
|
+
if (!pathname?.startsWith("/newsroom/references/")) return null;
|
|
172
|
+
const segment = pathname.slice("/newsroom/references/".length).split("/")[0]?.trim() ?? "";
|
|
173
|
+
if (!segment) return null;
|
|
174
|
+
try {
|
|
175
|
+
return decodeURIComponent(segment);
|
|
176
|
+
} catch {
|
|
177
|
+
return segment;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Assignment id from `/newsroom/assignments/<id>` (not index-only URLs). */
|
|
182
|
+
export function parseAssignmentIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
183
|
+
if (!pathname?.startsWith("/newsroom/assignments/")) return null;
|
|
184
|
+
const segment = pathname.slice("/newsroom/assignments/".length).split("/")[0]?.trim() ?? "";
|
|
185
|
+
if (!segment) return null;
|
|
186
|
+
try {
|
|
187
|
+
return decodeURIComponent(segment);
|
|
188
|
+
} catch {
|
|
189
|
+
return segment;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function parseInsightThreadIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
194
|
+
if (!pathname?.startsWith("/newsroom/insights/")) return null;
|
|
195
|
+
const segment = pathname.slice("/newsroom/insights/".length).split("/")[0]?.trim() ?? "";
|
|
196
|
+
if (!segment) return null;
|
|
197
|
+
try {
|
|
198
|
+
return decodeURIComponent(segment);
|
|
199
|
+
} catch {
|
|
200
|
+
return segment;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type NewsroomKnowledgeQueryAnchor = {
|
|
2
|
+
kind: "assignment" | "category" | "categorySet" | "item" | "message" | "newsroomSection" | "reference" | "semanticNode";
|
|
3
|
+
id: string;
|
|
4
|
+
lineageId?: string | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type NewsroomKnowledgeQueryTarget = {
|
|
8
|
+
anchor: NewsroomKnowledgeQueryAnchor;
|
|
9
|
+
title: string;
|
|
10
|
+
subtitle?: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type BuildKnowledgeQueryInputOptions = {
|
|
14
|
+
profile?: "researcher" | "reporter" | "editor" | "reviewer" | "chat";
|
|
15
|
+
semanticSearch?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function buildNewsroomKnowledgeQueryInput(
|
|
19
|
+
target: NewsroomKnowledgeQueryTarget | null,
|
|
20
|
+
semanticText: string,
|
|
21
|
+
maxTokens: number,
|
|
22
|
+
options: BuildKnowledgeQueryInputOptions = {},
|
|
23
|
+
): Record<string, unknown> {
|
|
24
|
+
const request: Record<string, unknown> = {
|
|
25
|
+
semanticQuery: semanticText.trim(),
|
|
26
|
+
scope: {
|
|
27
|
+
depth: 1,
|
|
28
|
+
topK: target ? 18 : 24,
|
|
29
|
+
relatedRecordLimit: target ? 8 : 10,
|
|
30
|
+
semanticSearch: options.semanticSearch ?? true,
|
|
31
|
+
},
|
|
32
|
+
profile: options.profile ?? "editor",
|
|
33
|
+
ranking: {
|
|
34
|
+
profile: "balanced",
|
|
35
|
+
diversity: target ? "balanced" : "broad",
|
|
36
|
+
},
|
|
37
|
+
output: {
|
|
38
|
+
format: "markdown",
|
|
39
|
+
maxTokens,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
if (target) {
|
|
43
|
+
const anchor: Record<string, unknown> = {
|
|
44
|
+
kind: target.anchor.kind,
|
|
45
|
+
id: target.anchor.id,
|
|
46
|
+
};
|
|
47
|
+
if (target.anchor.lineageId) anchor.lineageId = target.anchor.lineageId;
|
|
48
|
+
request.anchors = [anchor];
|
|
49
|
+
}
|
|
50
|
+
return request;
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cn } from "./utils";
|
|
2
|
+
|
|
3
|
+
/** List-row selection: dark copy on moss/paper tint in light mode; light copy on wash in dark mode. Never invert to primary-foreground. */
|
|
4
|
+
export const NEWSROOM_LIST_ROW_BASE =
|
|
5
|
+
"w-full rounded-xl border bg-card p-4 text-left text-foreground transition-colors";
|
|
6
|
+
|
|
7
|
+
export const NEWSROOM_LIST_ROW_INACTIVE =
|
|
8
|
+
"border-border hover:border-primary/30 hover:bg-primary/5";
|
|
9
|
+
|
|
10
|
+
export const NEWSROOM_LIST_ROW_ACTIVE =
|
|
11
|
+
"border-primary/40 bg-primary/10 text-foreground ring-1 ring-primary/15";
|
|
12
|
+
|
|
13
|
+
export const NEWSROOM_INBOX_ROW_BASE =
|
|
14
|
+
"flex items-start gap-3 rounded-xl border bg-card p-3 text-foreground transition-colors";
|
|
15
|
+
|
|
16
|
+
export const NEWSROOM_INBOX_ROW_INACTIVE =
|
|
17
|
+
"border-border hover:border-primary/30 hover:bg-primary/5";
|
|
18
|
+
|
|
19
|
+
export const NEWSROOM_INBOX_ROW_ACTIVE =
|
|
20
|
+
"border-primary/40 bg-primary/10 text-foreground ring-1 ring-primary/15";
|
|
21
|
+
|
|
22
|
+
export const NEWSROOM_SIDEBAR_NAV_ACTIVE =
|
|
23
|
+
"bg-primary/10 text-foreground shadow-none hover:bg-primary/15";
|
|
24
|
+
|
|
25
|
+
export const NEWSROOM_SIDEBAR_NAV_INACTIVE =
|
|
26
|
+
"text-muted-foreground hover:bg-muted/50 hover:text-foreground";
|
|
27
|
+
|
|
28
|
+
export function newsroomListRowClassName(active: boolean, className?: string): string {
|
|
29
|
+
return cn(
|
|
30
|
+
NEWSROOM_LIST_ROW_BASE,
|
|
31
|
+
active ? NEWSROOM_LIST_ROW_ACTIVE : NEWSROOM_LIST_ROW_INACTIVE,
|
|
32
|
+
className,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function newsroomInboxRowClassName(active: boolean, className?: string): string {
|
|
37
|
+
return cn(
|
|
38
|
+
NEWSROOM_INBOX_ROW_BASE,
|
|
39
|
+
active ? NEWSROOM_INBOX_ROW_ACTIVE : NEWSROOM_INBOX_ROW_INACTIVE,
|
|
40
|
+
className,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { generateClient } from "aws-amplify/data";
|
|
2
|
+
import { getUrl, uploadData } from "aws-amplify/storage";
|
|
3
|
+
import type { Schema } from "../amplify/data/resource";
|
|
4
|
+
import type { MediaAssetFields, MediaAssetRecord, NewsroomMediaBackend } from "./newsroom-media";
|
|
5
|
+
|
|
6
|
+
type MediaListPage = {
|
|
7
|
+
data?: (Record<string, unknown> | null)[] | null;
|
|
8
|
+
nextToken?: string | null;
|
|
9
|
+
errors?: { message?: string }[] | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MediaDataClient = {
|
|
13
|
+
models: {
|
|
14
|
+
MediaAsset: {
|
|
15
|
+
create: (
|
|
16
|
+
input: MediaAssetFields,
|
|
17
|
+
options: { authMode: "userPool" },
|
|
18
|
+
) => Promise<{ errors?: { message?: string }[] | null }>;
|
|
19
|
+
listMediaAssetsByItemAndSortKey: (
|
|
20
|
+
input: { itemId: string },
|
|
21
|
+
options: { authMode: "userPool"; nextToken?: string | null; limit?: number },
|
|
22
|
+
) => Promise<MediaListPage>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MediaStorageApi = {
|
|
28
|
+
upload: (input: { path: string; data: Blob; contentType: string }) => Promise<void>;
|
|
29
|
+
signedUrl: (path: string) => Promise<string>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function failOnErrors(errors: { message?: string }[] | null | undefined): void {
|
|
33
|
+
if (errors && errors.length > 0) {
|
|
34
|
+
throw new Error(errors.map((error) => error.message ?? "GraphQL error").join("; "));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function srcPathFromMetadata(metadata: unknown): string | null {
|
|
39
|
+
let parsed = metadata;
|
|
40
|
+
if (typeof metadata === "string") {
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(metadata);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
48
|
+
const srcPath = (parsed as Record<string, unknown>).srcPath;
|
|
49
|
+
return typeof srcPath === "string" ? srcPath : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createMediaBackend(client: () => MediaDataClient, storage: MediaStorageApi): NewsroomMediaBackend {
|
|
53
|
+
return {
|
|
54
|
+
uploadImage: ({ storagePath, data, contentType }) =>
|
|
55
|
+
storage.upload({ path: storagePath, data, contentType }),
|
|
56
|
+
async createMediaAsset(fields) {
|
|
57
|
+
const response = await client().models.MediaAsset.create(fields, { authMode: "userPool" });
|
|
58
|
+
failOnErrors(response.errors);
|
|
59
|
+
},
|
|
60
|
+
async listMediaAssets(itemId) {
|
|
61
|
+
const records: MediaAssetRecord[] = [];
|
|
62
|
+
let nextToken: string | null | undefined = null;
|
|
63
|
+
do {
|
|
64
|
+
const page: MediaListPage = await client().models.MediaAsset.listMediaAssetsByItemAndSortKey(
|
|
65
|
+
{ itemId },
|
|
66
|
+
{ authMode: "userPool", nextToken, limit: 500 },
|
|
67
|
+
);
|
|
68
|
+
failOnErrors(page.errors);
|
|
69
|
+
for (const row of page.data ?? []) {
|
|
70
|
+
if (!row) continue;
|
|
71
|
+
records.push({
|
|
72
|
+
id: String(row.id),
|
|
73
|
+
sortKey: String(row.sortKey),
|
|
74
|
+
storagePath: typeof row.storagePath === "string" ? row.storagePath : null,
|
|
75
|
+
alt: typeof row.alt === "string" ? row.alt : "",
|
|
76
|
+
srcPath: srcPathFromMetadata(row.metadata),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
nextToken = page.nextToken;
|
|
80
|
+
} while (nextToken);
|
|
81
|
+
return records;
|
|
82
|
+
},
|
|
83
|
+
imageUrl: (storagePath) => storage.signedUrl(storagePath),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const amplifyStorage: MediaStorageApi = {
|
|
88
|
+
async upload({ path, data, contentType }) {
|
|
89
|
+
await uploadData({ path, data, options: { contentType } }).result;
|
|
90
|
+
},
|
|
91
|
+
async signedUrl(path) {
|
|
92
|
+
const { url } = await getUrl({ path });
|
|
93
|
+
return url.toString();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function createLiveMediaBackend(): NewsroomMediaBackend {
|
|
98
|
+
let client: MediaDataClient | null = null;
|
|
99
|
+
return createMediaBackend(() => {
|
|
100
|
+
if (!client) client = generateClient<Schema>() as unknown as MediaDataClient;
|
|
101
|
+
return client;
|
|
102
|
+
}, amplifyStorage);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createDemoMediaBackend(): NewsroomMediaBackend {
|
|
106
|
+
const objects = new Map<string, Blob>();
|
|
107
|
+
const rows = new Map<string, MediaAssetFields[]>();
|
|
108
|
+
return {
|
|
109
|
+
async uploadImage({ storagePath, data }) {
|
|
110
|
+
objects.set(storagePath, data);
|
|
111
|
+
},
|
|
112
|
+
async createMediaAsset(fields) {
|
|
113
|
+
rows.set(fields.itemId, [...(rows.get(fields.itemId) ?? []), fields]);
|
|
114
|
+
},
|
|
115
|
+
async listMediaAssets(itemId) {
|
|
116
|
+
return (rows.get(itemId) ?? []).map((fields) => ({
|
|
117
|
+
id: fields.id,
|
|
118
|
+
sortKey: fields.sortKey,
|
|
119
|
+
storagePath: fields.storagePath,
|
|
120
|
+
alt: fields.alt,
|
|
121
|
+
srcPath: srcPathFromMetadata(fields.metadata),
|
|
122
|
+
}));
|
|
123
|
+
},
|
|
124
|
+
async imageUrl(storagePath) {
|
|
125
|
+
const object = objects.get(storagePath);
|
|
126
|
+
if (!object) throw new Error(`No demo object at ${storagePath}.`);
|
|
127
|
+
return URL.createObjectURL(object);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let liveBackend: NewsroomMediaBackend | null = null;
|
|
133
|
+
let demoBackend: NewsroomMediaBackend | null = null;
|
|
134
|
+
|
|
135
|
+
export function resolveMediaBackend(demo: boolean): NewsroomMediaBackend {
|
|
136
|
+
if (demo) {
|
|
137
|
+
demoBackend ??= createDemoMediaBackend();
|
|
138
|
+
return demoBackend;
|
|
139
|
+
}
|
|
140
|
+
liveBackend ??= createLiveMediaBackend();
|
|
141
|
+
return liveBackend;
|
|
142
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
2
|
+
|
|
3
|
+
export const IMAGE_CONTENT_TYPES: Record<string, string> = {
|
|
4
|
+
png: "image/png",
|
|
5
|
+
jpg: "image/jpeg",
|
|
6
|
+
jpeg: "image/jpeg",
|
|
7
|
+
gif: "image/gif",
|
|
8
|
+
webp: "image/webp",
|
|
9
|
+
avif: "image/avif",
|
|
10
|
+
svg: "image/svg+xml",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ImageFileLike = { name: string; size: number };
|
|
14
|
+
|
|
15
|
+
function extensionOf(filename: string): string {
|
|
16
|
+
const dot = filename.lastIndexOf(".");
|
|
17
|
+
return dot === -1 ? "" : filename.slice(dot + 1).toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function contentTypeForFilename(filename: string): string | null {
|
|
21
|
+
return IMAGE_CONTENT_TYPES[extensionOf(filename)] ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isSvgFilename(filename: string): boolean {
|
|
25
|
+
return extensionOf(filename) === "svg";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeSegment(value: string): string {
|
|
29
|
+
return value
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
32
|
+
.replace(/-{2,}/g, "-")
|
|
33
|
+
.replace(/^[-.]+|-+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function srcPathFor(slug: string, filename: string, existingSrcPaths: Iterable<string> = []): string {
|
|
37
|
+
const taken = new Set(existingSrcPaths);
|
|
38
|
+
const extension = sanitizeSegment(extensionOf(filename));
|
|
39
|
+
const dot = filename.lastIndexOf(".");
|
|
40
|
+
const rawStem = dot === -1 ? filename : filename.slice(0, dot);
|
|
41
|
+
const stem = sanitizeSegment(rawStem) || "image";
|
|
42
|
+
const directory = sanitizeSegment(slug) || "article";
|
|
43
|
+
const suffix = extension ? `.${extension}` : "";
|
|
44
|
+
let candidate = `assets/${directory}/${stem}${suffix}`;
|
|
45
|
+
for (let counter = 2; taken.has(candidate); counter += 1) {
|
|
46
|
+
candidate = `assets/${directory}/${stem}-${counter}${suffix}`;
|
|
47
|
+
}
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function sha256Hex(data: string | ArrayBuffer): Promise<string> {
|
|
52
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
53
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
54
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function mediaIdFor(itemId: string, srcPath: string): Promise<string> {
|
|
58
|
+
return `media-${itemId}-${(await sha256Hex(srcPath)).slice(0, 10)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateImageFile(file: ImageFileLike): string | null {
|
|
62
|
+
if (contentTypeForFilename(file.name) === null) {
|
|
63
|
+
return "Choose a PNG, JPEG, GIF, WebP, AVIF or SVG image.";
|
|
64
|
+
}
|
|
65
|
+
if (file.size <= 0) return "That file is empty.";
|
|
66
|
+
if (file.size > MAX_IMAGE_BYTES) return "Images can be at most 10 MB.";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function altAttributeValue(alt: string): string {
|
|
71
|
+
return alt.replace(/[\r\n]+/g, " ").replace(/"/g, "'").replace(/[{}]/g, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function imageDirective(srcPath: string, alt: string): string {
|
|
75
|
+
return `::image{src="${srcPath}" alt="${altAttributeValue(alt)}" layout="inline"}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type MediaAssetFields = {
|
|
79
|
+
id: string;
|
|
80
|
+
itemId: string;
|
|
81
|
+
type: "image";
|
|
82
|
+
role: "body";
|
|
83
|
+
sortKey: string;
|
|
84
|
+
storagePath: string;
|
|
85
|
+
alt: string;
|
|
86
|
+
width?: number;
|
|
87
|
+
height?: number;
|
|
88
|
+
metadata: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function mediaSortKey(position: number, srcPath: string): string {
|
|
92
|
+
return `${String(position).padStart(3, "0")}#${srcPath}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type SelectionInsertion = { body: string; altSelectionStart: number };
|
|
96
|
+
|
|
97
|
+
function countEdgeNewlines(text: string, fromEnd: boolean): number {
|
|
98
|
+
let count = 0;
|
|
99
|
+
while (count < 2 && (fromEnd ? text[text.length - 1 - count] : text[count]) === "\n") count += 1;
|
|
100
|
+
return count;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function insertDirectiveAtSelection(
|
|
104
|
+
body: string,
|
|
105
|
+
selectionStart: number,
|
|
106
|
+
selectionEnd: number,
|
|
107
|
+
directive: string,
|
|
108
|
+
): SelectionInsertion {
|
|
109
|
+
const before = body.slice(0, selectionStart);
|
|
110
|
+
const after = body.slice(selectionEnd);
|
|
111
|
+
const prefix = before === "" ? "" : "\n".repeat(2 - countEdgeNewlines(before, true));
|
|
112
|
+
const suffix = after === "" ? "\n" : "\n".repeat(2 - countEdgeNewlines(after, false));
|
|
113
|
+
const directiveStart = before.length + prefix.length;
|
|
114
|
+
return {
|
|
115
|
+
body: `${before}${prefix}${directive}${suffix}${after}`,
|
|
116
|
+
altSelectionStart: directiveStart + directive.indexOf('alt="') + 'alt="'.length,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type MediaAssetRecord = {
|
|
121
|
+
id: string;
|
|
122
|
+
sortKey: string;
|
|
123
|
+
storagePath: string | null;
|
|
124
|
+
alt: string;
|
|
125
|
+
srcPath: string | null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type NewsroomMediaBackend = {
|
|
129
|
+
uploadImage: (input: { storagePath: string; data: Blob; contentType: string }) => Promise<void>;
|
|
130
|
+
createMediaAsset: (fields: MediaAssetFields) => Promise<void>;
|
|
131
|
+
listMediaAssets: (itemId: string) => Promise<MediaAssetRecord[]>;
|
|
132
|
+
imageUrl: (storagePath: string) => Promise<string>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type ImageDimensionReader = (file: Blob, filename: string) => Promise<{ width: number; height: number } | null>;
|
|
136
|
+
|
|
137
|
+
export type UploadedImage = { srcPath: string; directive: string; imageUrl: string };
|
|
138
|
+
|
|
139
|
+
export async function uploadArticleImage(
|
|
140
|
+
backend: NewsroomMediaBackend,
|
|
141
|
+
readDimensions: ImageDimensionReader,
|
|
142
|
+
input: { itemId: string; slug: string; file: File },
|
|
143
|
+
): Promise<UploadedImage> {
|
|
144
|
+
const problem = validateImageFile(input.file);
|
|
145
|
+
if (problem) throw new Error(problem);
|
|
146
|
+
const contentType = contentTypeForFilename(input.file.name) as string;
|
|
147
|
+
const existing = await backend.listMediaAssets(input.itemId);
|
|
148
|
+
const srcPath = srcPathFor(
|
|
149
|
+
input.slug,
|
|
150
|
+
input.file.name,
|
|
151
|
+
existing.map((asset) => asset.srcPath).filter((path): path is string => path !== null),
|
|
152
|
+
);
|
|
153
|
+
const bytes = await input.file.arrayBuffer();
|
|
154
|
+
const sha256 = await sha256Hex(bytes);
|
|
155
|
+
const dimensions = await readDimensions(input.file, input.file.name);
|
|
156
|
+
const storagePath = `media/${srcPath}`;
|
|
157
|
+
await backend.uploadImage({ storagePath, data: input.file, contentType });
|
|
158
|
+
await backend.createMediaAsset({
|
|
159
|
+
id: await mediaIdFor(input.itemId, srcPath),
|
|
160
|
+
itemId: input.itemId,
|
|
161
|
+
type: "image",
|
|
162
|
+
role: "body",
|
|
163
|
+
sortKey: mediaSortKey(existing.length + 1, srcPath),
|
|
164
|
+
storagePath,
|
|
165
|
+
alt: input.file.name,
|
|
166
|
+
...(dimensions ?? {}),
|
|
167
|
+
metadata: JSON.stringify({ srcPath, sha256, bytes: bytes.byteLength, contentType }),
|
|
168
|
+
});
|
|
169
|
+
return { srcPath, directive: imageDirective(srcPath, ""), imageUrl: await backend.imageUrl(storagePath) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function readImageDimensions(file: Blob, filename: string): Promise<{ width: number; height: number } | null> {
|
|
173
|
+
if (isSvgFilename(filename)) return null;
|
|
174
|
+
try {
|
|
175
|
+
const bitmap = await createImageBitmap(file);
|
|
176
|
+
const size = { width: bitmap.width, height: bitmap.height };
|
|
177
|
+
bitmap.close();
|
|
178
|
+
return size;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { newsroomHref, toPublicNewsroomPath } from "./newsroom-base-path";
|
|
2
|
+
|
|
3
|
+
export type NewsroomNavTabId =
|
|
4
|
+
| "overview"
|
|
5
|
+
| "articles"
|
|
6
|
+
| "messages"
|
|
7
|
+
| "insights"
|
|
8
|
+
| "assignments"
|
|
9
|
+
| "references"
|
|
10
|
+
| "topics"
|
|
11
|
+
| "concepts"
|
|
12
|
+
| "administration"
|
|
13
|
+
| "search"
|
|
14
|
+
| "desks";
|
|
15
|
+
|
|
16
|
+
export type NewsroomNavItem = {
|
|
17
|
+
id: NewsroomNavTabId;
|
|
18
|
+
label: string;
|
|
19
|
+
detail: string;
|
|
20
|
+
href: string;
|
|
21
|
+
mobilePrimary?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const NEWSROOM_OPS_NAV: NewsroomNavItem[] = [
|
|
25
|
+
{ id: "overview", label: "Overview", detail: "Desk home", href: newsroomHref(), mobilePrimary: true },
|
|
26
|
+
{ id: "articles", label: "Articles", detail: "Drafts & published", href: newsroomHref("articles"), mobilePrimary: true },
|
|
27
|
+
{ id: "assignments", label: "Assignments", detail: "Work queue", href: newsroomHref("assignments"), mobilePrimary: true },
|
|
28
|
+
{ id: "references", label: "References", detail: "Knowledge base", href: newsroomHref("references"), mobilePrimary: true },
|
|
29
|
+
{ id: "messages", label: "Messages", detail: "Forum", href: newsroomHref("messages") },
|
|
30
|
+
{ id: "insights", label: "Insights", detail: "Research threads", href: newsroomHref("insights") },
|
|
31
|
+
{ id: "topics", label: "Topics", detail: "Taxonomy", href: newsroomHref("topics"), mobilePrimary: true },
|
|
32
|
+
{ id: "concepts", label: "Concepts", detail: "Ontology", href: newsroomHref("concepts") },
|
|
33
|
+
{ id: "administration", label: "Administration", detail: "Users & policies", href: newsroomHref("administration") },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const NEWSROOM_MOBILE_PRIMARY_NAV = NEWSROOM_OPS_NAV.filter((item) => item.mobilePrimary);
|
|
37
|
+
|
|
38
|
+
export function getNewsroomNavHref(href: string, demo?: boolean): string {
|
|
39
|
+
const path = toPublicNewsroomPath(href);
|
|
40
|
+
if (!demo) return path;
|
|
41
|
+
const url = new URL(path, "http://localhost");
|
|
42
|
+
url.searchParams.set("demo", "1");
|
|
43
|
+
return `${url.pathname}${url.search}`;
|
|
44
|
+
}
|