@anthusai/papyrus 5.57.0 → 5.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/data/message-actions.js +68 -0
- package/amplify/data/message-actions.ts +70 -0
- package/amplify/data/resolvers/delete-own-message.js +30 -0
- package/amplify/data/resolvers/edit-message.js +33 -0
- package/amplify/data/resolvers/moderate-message.js +32 -0
- package/amplify/data/resolvers/post-message.js +68 -0
- package/amplify/data/resolvers/rename-thread.js +28 -0
- package/amplify/data/resource.js +28 -3
- package/amplify/data/resource.ts +28 -3
- package/amplify/data/schema.js +34 -5
- package/amplify/data/schema.ts +34 -5
- package/amplify/functions/category-action/handler.ts +51 -125
- package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
- package/components/conversation-context.tsx +29 -2
- package/components/conversation-provider.tsx +5 -1
- package/components/conversation-thread.tsx +154 -25
- package/components/conversations-memory-source.ts +83 -0
- package/components/conversations-network-source.ts +52 -0
- package/components/conversations-page.tsx +14 -0
- package/components/conversations-source.ts +36 -0
- package/components/conversations-view.tsx +680 -0
- package/components/decision-control.tsx +307 -0
- package/components/list-detail-layout.tsx +44 -0
- package/components/needs-me-bodies.tsx +18 -17
- package/components/needs-me-feed.tsx +16 -32
- package/components/news-desk-taxonomy-client.ts +216 -62
- package/components/newsroom-article-editor.tsx +10 -4
- package/components/newsroom-organizer.css +34 -1
- package/components/newsroom-references-view.tsx +41 -100
- package/components/notification-cards.tsx +9 -8
- package/components/notification-center.tsx +5 -7
- package/components/proposals-view.tsx +4 -4
- package/components/reference-conversation.tsx +5 -0
- package/components/reference-feed.tsx +129 -127
- package/components/reference-review-card.tsx +9 -135
- package/components/references-page-loading.tsx +12 -6
- package/components/story-reporting-trail-client.ts +4 -3
- package/components/story-reporting-trail.tsx +8 -0
- package/components/topic-steering-workspace.tsx +44 -786
- package/components/use-conversation.ts +36 -2
- package/components/use-feed-preferences.ts +33 -0
- package/components/use-reference-ratings.ts +11 -7
- package/lib/category-repository.ts +14 -38
- package/lib/console-chat-client.ts +8 -178
- package/lib/conversation-feed.ts +96 -0
- package/lib/conversation.ts +73 -2
- package/lib/cyclotron-suggestions.ts +18 -36
- package/lib/local-preview-seed.ts +16 -27
- package/lib/needs-me-items.ts +4 -4
- package/lib/needs-me-ranking.ts +9 -22
- package/lib/newsroom-board.ts +10 -1
- package/lib/newsroom-demo-dashboard.ts +0 -7
- package/lib/newsroom-organizer-nav.ts +2 -2
- package/lib/post-message.ts +54 -0
- package/lib/proposals-admin.ts +2 -2
- package/lib/reference-feed-keys.ts +69 -0
- package/lib/reference-feed-ranking.ts +77 -160
- package/lib/reference-policy.ts +83 -11
- package/lib/reference-review-details.ts +21 -57
- package/lib/relevance-decisions.ts +6 -2
- package/lib/review-drafts.ts +2 -2
- package/lib/review-fixture.ts +1 -1
- package/lib/review-requests.ts +4 -4
- package/lib/semantic-graph.ts +1 -1
- package/lib/story-reporting-trail.ts +28 -2
- package/package.json +12 -2
- package/routes.manifest.json +1 -1
- package/components/review-rating-grid.tsx +0 -190
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReferenceRecord } from "./category-repository";
|
|
2
2
|
import type { RelevanceCuration, RelevanceDecisionView } from "./relevance-decisions";
|
|
3
3
|
import { safeId } from "./relevance-status";
|
|
4
4
|
|
|
@@ -149,22 +149,12 @@ function parseMetadata(value: unknown): Record<string, unknown> {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
* Reference lineage ids whose latest
|
|
153
|
-
*
|
|
154
|
-
* does a reference a person decided before a later non-human change.
|
|
152
|
+
* Reference lineage ids whose latest vote was made by a signed-in person
|
|
153
|
+
* (`curationActorKind` is `human`). Agent, Cyclotron and rule votes never appear
|
|
154
|
+
* here, and neither does a reference a person decided before a later non-human change.
|
|
155
155
|
*/
|
|
156
|
-
export function referencesWithHumanLatestCuration(
|
|
157
|
-
|
|
158
|
-
for (const message of messages) {
|
|
159
|
-
if (message.messageKind !== "reference_curation") continue;
|
|
160
|
-
const metadata = parseMetadata(message.metadata);
|
|
161
|
-
const lineageId = typeof metadata.referenceLineageId === "string" ? metadata.referenceLineageId : "";
|
|
162
|
-
if (!lineageId) continue;
|
|
163
|
-
const seen = latestByReference.get(lineageId);
|
|
164
|
-
if (seen && seen.createdAt >= message.createdAt) continue;
|
|
165
|
-
latestByReference.set(lineageId, { createdAt: message.createdAt, human: metadata.actorKind === "human" });
|
|
166
|
-
}
|
|
167
|
-
return new Set([...latestByReference].filter(([, entry]) => entry.human).map(([lineageId]) => lineageId));
|
|
156
|
+
export function referencesWithHumanLatestCuration(references: readonly ReferenceRecord[]): Set<string> {
|
|
157
|
+
return new Set(references.filter((reference) => reference.curationActorKind === "human").map((reference) => reference.lineageId ?? reference.id));
|
|
168
158
|
}
|
|
169
159
|
|
|
170
160
|
export type SuggestionChoice = "agree" | "overrule";
|
|
@@ -233,30 +223,22 @@ export function delegationShortId(delegationId: string): string {
|
|
|
233
223
|
}
|
|
234
224
|
|
|
235
225
|
/**
|
|
236
|
-
* Delegated curations by reference lineage, from each
|
|
237
|
-
*
|
|
226
|
+
* Delegated curations by reference lineage, from each Reference's latest vote only.
|
|
227
|
+
* The person is who set the status (`curationStatusUpdatedBy`); the agent is the delegation's relayedBy.
|
|
238
228
|
*/
|
|
239
|
-
export function delegatedCurationsByReference(
|
|
240
|
-
const latest = new Map<string, MessageRecord>();
|
|
241
|
-
for (const message of messages) {
|
|
242
|
-
if (message.messageKind !== "reference_curation") continue;
|
|
243
|
-
const lineageId = parseMetadata(message.metadata).referenceLineageId;
|
|
244
|
-
if (typeof lineageId !== "string" || !lineageId) continue;
|
|
245
|
-
const seen = latest.get(lineageId);
|
|
246
|
-
if (!seen || seen.createdAt < message.createdAt) latest.set(lineageId, message);
|
|
247
|
-
}
|
|
229
|
+
export function delegatedCurationsByReference(references: readonly ReferenceRecord[]): Map<string, DelegatedCuration> {
|
|
248
230
|
const delegated = new Map<string, DelegatedCuration>();
|
|
249
|
-
for (const
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
const delegationId = typeof
|
|
253
|
-
delegated.set(lineageId, {
|
|
254
|
-
agent: typeof
|
|
255
|
-
person:
|
|
256
|
-
instruction: typeof
|
|
231
|
+
for (const reference of references) {
|
|
232
|
+
const delegation = parseMetadata(reference.curationDelegation);
|
|
233
|
+
if (!Object.keys(delegation).length) continue;
|
|
234
|
+
const delegationId = typeof delegation.delegationId === "string" ? delegation.delegationId : "";
|
|
235
|
+
delegated.set(reference.lineageId ?? reference.id, {
|
|
236
|
+
agent: typeof delegation.relayedBy === "string" && delegation.relayedBy ? delegation.relayedBy : "an agent",
|
|
237
|
+
person: reference.curationStatusUpdatedBy?.trim() || null,
|
|
238
|
+
instruction: typeof delegation.instruction === "string" ? delegation.instruction : "",
|
|
257
239
|
delegationId,
|
|
258
240
|
delegationShortId: delegationShortId(delegationId),
|
|
259
|
-
countsAsTrainingLabel:
|
|
241
|
+
countsAsTrainingLabel: reference.curationActorKind === "human",
|
|
260
242
|
});
|
|
261
243
|
}
|
|
262
244
|
return delegated;
|
|
@@ -210,15 +210,16 @@ const PREVIEW_CYCLOTRON_ID = "papyrus-relevance";
|
|
|
210
210
|
/**
|
|
211
211
|
* Fixture data for reviewing the cyclotron suggestion UI without a backend:
|
|
212
212
|
* suggestions with provenance on the first references, a mix of human and
|
|
213
|
-
* non-human
|
|
213
|
+
* non-human votes recorded on the References, and a live-agreement payload.
|
|
214
214
|
*/
|
|
215
215
|
export function createLocalPreviewCyclotronFixtures(references: ReferenceRecord[]): {
|
|
216
216
|
relations: SemanticRelationRecord[];
|
|
217
|
-
|
|
217
|
+
/** The first few References with a latest vote recorded on them, by Reference id. */
|
|
218
|
+
votedReferences: Map<string, ReferenceRecord>;
|
|
218
219
|
shadowAgreement: ShadowAgreement;
|
|
219
220
|
} {
|
|
220
221
|
const relations: SemanticRelationRecord[] = [];
|
|
221
|
-
const
|
|
222
|
+
const votedReferences = new Map<string, ReferenceRecord>();
|
|
222
223
|
references.slice(0, PREVIEW_SUGGESTION_PLANS.length).forEach((reference, index) => {
|
|
223
224
|
const plan = PREVIEW_SUGGESTION_PLANS[index];
|
|
224
225
|
const lineageId = reference.lineageId ?? reference.id;
|
|
@@ -264,35 +265,23 @@ export function createLocalPreviewCyclotronFixtures(references: ReferenceRecord[
|
|
|
264
265
|
}),
|
|
265
266
|
} as SemanticRelationRecord);
|
|
266
267
|
if (plan.curation !== "none") {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
metadata: JSON.stringify({
|
|
279
|
-
action: reference.curationStatus === "rejected" ? "reject" : "accept",
|
|
280
|
-
curationStatus: reference.curationStatus,
|
|
281
|
-
referenceLineageId: lineageId,
|
|
282
|
-
actorKind: plan.curation === "service" ? "service" : "human",
|
|
283
|
-
...(plan.curation === "delegated" ? {
|
|
284
|
-
attestation: "delegated",
|
|
285
|
-
relayedBy: "newsroom-reporter",
|
|
286
|
-
delegationId: "curation-delegation-9b1f6c24-5d0e-4a7b-8c31-2e6f0a1d7b45",
|
|
287
|
-
instruction: "Reject that one, it duplicates the filing from Tuesday.",
|
|
288
|
-
} : {}),
|
|
289
|
-
}),
|
|
268
|
+
// A Reference keeps its latest vote on itself (no Message): the details sit in its curation* fields.
|
|
269
|
+
votedReferences.set(reference.id, {
|
|
270
|
+
...reference,
|
|
271
|
+
curationStatusUpdatedAt: isoTimestampHoursBeforeBase(30 - index),
|
|
272
|
+
curationStatusUpdatedBy: plan.curation === "delegated" ? "Maya Okafor" : reference.curationStatusUpdatedBy,
|
|
273
|
+
curationActorKind: plan.curation === "service" ? "agent" : "human",
|
|
274
|
+
curationDelegation: plan.curation === "delegated" ? {
|
|
275
|
+
delegationId: "curation-delegation-9b1f6c24",
|
|
276
|
+
relayedBy: "newsroom-reporter",
|
|
277
|
+
instruction: "Reject that one, it duplicates the filing from Tuesday.",
|
|
278
|
+
} : null,
|
|
290
279
|
});
|
|
291
280
|
}
|
|
292
281
|
});
|
|
293
282
|
return {
|
|
294
283
|
relations,
|
|
295
|
-
|
|
284
|
+
votedReferences,
|
|
296
285
|
shadowAgreement: {
|
|
297
286
|
schema: SHADOW_AGREEMENT_SCHEMA,
|
|
298
287
|
comparisons: 23,
|
package/lib/needs-me-items.ts
CHANGED
|
@@ -127,9 +127,9 @@ export function draftItem(item: DraftItem, bySource: ReadonlyMap<string, Referen
|
|
|
127
127
|
type: "draft",
|
|
128
128
|
id: item.key,
|
|
129
129
|
title: view?.headline ?? link?.label ?? "Draft",
|
|
130
|
-
dek: view ? view.dek : "Loading the
|
|
130
|
+
dek: view ? view.dek : "Loading the Proposed Story…",
|
|
131
131
|
image: picture.imageUrl,
|
|
132
|
-
source: picture.siteName ?? picture.host ?? (view ? `AI ${view.kindLabel.toLowerCase()}` : "
|
|
132
|
+
source: picture.siteName ?? picture.host ?? (view ? `AI ${view.kindLabel.toLowerCase()}` : "Proposed Story"),
|
|
133
133
|
host: picture.host,
|
|
134
134
|
createdAt: timeOf(request.raisedAt),
|
|
135
135
|
importance: 1,
|
|
@@ -273,10 +273,10 @@ function plural(count: number, one: string, many = `${one}s`): string {
|
|
|
273
273
|
return `${count} ${count === 1 ? one : many}`;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
/** "3
|
|
276
|
+
/** "3 Proposed Stories and 12 references waiting for you"; "All caught up" when nothing waits. */
|
|
277
277
|
export function waitingTextFor(counts: NeedsMeCounts): string {
|
|
278
278
|
const parts = [
|
|
279
|
-
counts.drafts ? plural(counts.drafts, "
|
|
279
|
+
counts.drafts ? plural(counts.drafts, "Proposed Story", "Proposed Stories") : null,
|
|
280
280
|
counts.references ? plural(counts.references, "reference") : null,
|
|
281
281
|
counts.stories ? plural(counts.stories, "story review") : null,
|
|
282
282
|
counts.email ? plural(counts.email, "held email", "held emails") : null,
|
package/lib/needs-me-ranking.ts
CHANGED
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
* What they share is one question: where would a person's answer change the most? In plain words, as the page says them:
|
|
6
6
|
* 1. Hard gates first. Nothing continues until a person decides one, so it leads, oldest first.
|
|
7
7
|
* 2. Then the interim "value of your input" score:
|
|
8
|
-
* value = uncertainty x importance
|
|
8
|
+
* value = uncertainty x importance
|
|
9
9
|
* - uncertainty is 1 - |2P - 1|, where P is the Cyclotron's probability that you would Accept. A coin toss (P = 0.5) is 1, a
|
|
10
10
|
* certainty is 0. An item with no prediction is 0.5: we do not know how unsure we are.
|
|
11
11
|
* - importance is the item's own priority (a relevance score, or a story or section priority). It is 1 until a source gives one.
|
|
12
|
-
* - freshness is exp(-age / tau), the weight the References feed already uses; tau is learned from how old the References you
|
|
13
|
-
* accepted were when you accepted them (reference-feed-ranking.ts). With too few examples there is no decay.
|
|
14
12
|
* 3. Types are interleaved, never more than two of one type in a row while another type is waiting, so a long run of References
|
|
15
13
|
* does not bury the drafts and emails behind it.
|
|
16
14
|
* 4. Confident items do not ask for you. An item whose uncertainty is below the threshold goes to "Decided automatically", where it
|
|
@@ -25,7 +23,7 @@ import type { ReferenceRecord } from "./category-repository";
|
|
|
25
23
|
import { referenceLineageId } from "./newsroom-references";
|
|
26
24
|
import { normalizeReferenceCurationStatus } from "./reference-policy";
|
|
27
25
|
import type { RelevanceDecisionView } from "./relevance-decisions";
|
|
28
|
-
import { acceptProbability
|
|
26
|
+
import { acceptProbability } from "./reference-feed-ranking";
|
|
29
27
|
|
|
30
28
|
export type NeedsMeType = "reference" | "draft" | "stage_review" | "held_email" | "incomplete_capture" | "concept_proposal" | "flag";
|
|
31
29
|
|
|
@@ -35,7 +33,7 @@ export const NEEDS_ME_TYPES: readonly NeedsMeType[] = ["reference", "draft", "st
|
|
|
35
33
|
export type NeedsMeFilter = "all" | "references" | "drafts" | "stories" | "email" | "captures" | "other";
|
|
36
34
|
export const NEEDS_ME_FILTERS: readonly NeedsMeFilter[] = ["all", "references", "drafts", "stories", "email", "captures", "other"];
|
|
37
35
|
export const NEEDS_ME_FILTER_LABELS: Record<NeedsMeFilter, string> = {
|
|
38
|
-
all: "All", references: "References", drafts: "
|
|
36
|
+
all: "All", references: "References", drafts: "Proposed Stories", stories: "Stories", email: "Email", captures: "Captures", other: "Other",
|
|
39
37
|
};
|
|
40
38
|
|
|
41
39
|
const FILTER_OF: Record<NeedsMeType, Exclude<NeedsMeFilter, "all">> = {
|
|
@@ -52,7 +50,7 @@ export function matchesFilter(item: Pick<NeedsMeItem, "type">, filter: NeedsMeFi
|
|
|
52
50
|
|
|
53
51
|
/** The words for a type, used on the card and in the detail pane. */
|
|
54
52
|
export const NEEDS_ME_TYPE_LABELS: Record<NeedsMeType, string> = {
|
|
55
|
-
reference: "Reference", draft: "
|
|
53
|
+
reference: "Reference", draft: "Proposed Story", stage_review: "Story review", held_email: "Held email", incomplete_capture: "Capture check", concept_proposal: "Concept proposal", flag: "For your information",
|
|
56
54
|
};
|
|
57
55
|
|
|
58
56
|
export type NeedsMePrediction = {
|
|
@@ -109,33 +107,23 @@ export function predictionOf(decision: RelevanceDecisionView | undefined): Needs
|
|
|
109
107
|
return { label: decision.label === (decision.positiveLabel ?? "include") ? "accept" : "reject", pAccept, confidence: decision.confidence };
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
const DAY_MILLISECONDS = 86_400_000;
|
|
113
|
-
|
|
114
|
-
/** exp(-age / tau) at `now`; 1 without a learned tau or an unknown arrival. The References feed's own weight. */
|
|
115
|
-
export function freshnessAt(createdAt: number | null, freshness: FeedFreshness, now: number): number {
|
|
116
|
-
if (freshness.tauDays === null || createdAt === null) return 1;
|
|
117
|
-
return Math.exp(-Math.max(now - createdAt, 0) / DAY_MILLISECONDS / freshness.tauDays);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export type AttentionContext = { now: number; freshness: FeedFreshness };
|
|
121
|
-
|
|
122
110
|
/**
|
|
123
111
|
* The value of this editor's input on one item. Higher is earlier. The interim model below is a formula; a Cyclotron "attention"
|
|
124
112
|
* decision implements the same interface and is passed to `rankNeedsMe` in its place.
|
|
125
113
|
*/
|
|
126
114
|
export interface AttentionModel {
|
|
127
115
|
readonly id: string;
|
|
128
|
-
value(item: NeedsMeItem
|
|
116
|
+
value(item: NeedsMeItem): number;
|
|
129
117
|
}
|
|
130
118
|
|
|
131
119
|
export const interimAttention: AttentionModel = {
|
|
132
120
|
id: "interim-value-of-input",
|
|
133
|
-
value: (item
|
|
121
|
+
value: (item) => item.uncertainty * item.importance,
|
|
134
122
|
};
|
|
135
123
|
|
|
136
124
|
// --- confident items ----------------------------------------------------------------------------------------------------
|
|
137
125
|
|
|
138
|
-
export const MINIMUM_CONFIRMED_EXAMPLES =
|
|
126
|
+
export const MINIMUM_CONFIRMED_EXAMPLES = 10;
|
|
139
127
|
|
|
140
128
|
export type ConfidenceThreshold = {
|
|
141
129
|
/** Null with fewer than MINIMUM_CONFIRMED_EXAMPLES confirmed predictions: nothing is guessed, nothing is skipped. */
|
|
@@ -217,11 +205,10 @@ function newestFirst(left: NeedsMeItem, right: NeedsMeItem): number {
|
|
|
217
205
|
|
|
218
206
|
export function rankNeedsMe<P>(
|
|
219
207
|
items: readonly NeedsMeItem<P>[],
|
|
220
|
-
options: {
|
|
208
|
+
options: { threshold: ConfidenceThreshold; attention?: AttentionModel },
|
|
221
209
|
): NeedsMeRanking<P> {
|
|
222
210
|
const attention = options.attention ?? interimAttention;
|
|
223
|
-
const
|
|
224
|
-
const scores = new Map(items.map((item) => [item.id, attention.value(item, context)]));
|
|
211
|
+
const scores = new Map(items.map((item) => [item.id, attention.value(item)]));
|
|
225
212
|
const score = (item: NeedsMeItem) => scores.get(item.id) ?? 0;
|
|
226
213
|
const gates = items.filter((item) => item.hardGate).sort((left, right) => (left.createdAt ?? 0) - (right.createdAt ?? 0) || left.id.localeCompare(right.id));
|
|
227
214
|
const rest = items.filter((item) => !item.hardGate);
|
package/lib/newsroom-board.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KanbanConfig, KanbanIssue } from "@kanbus/ui";
|
|
1
|
+
import type { KanbanConfig, KanbanIssue, KanbanSortFieldRule } from "@kanbus/ui";
|
|
2
2
|
import type { AssignmentRecord } from "./category-repository";
|
|
3
3
|
import {
|
|
4
4
|
ASSIGNMENT_PHASE_STAGE_KEYS,
|
|
@@ -118,11 +118,19 @@ export function assignmentColumnOrder(statusesPresentOnBoard: readonly string[])
|
|
|
118
118
|
return [...ASSIGNMENT_STATUSES_BEFORE_OTHERS, ...otherStatuses, ...ASSIGNMENT_STATUSES_AFTER_OTHERS];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Kanbus's default column order is oldest-created first, and our content items carry no `created_at`, so without this every
|
|
123
|
+
* column listed its cards by id. Newest first by `updated_at`: it is the order the stage pages load in, and the only time a
|
|
124
|
+
* paged content item has (each save is a new version, so `createdAt` is the last edit, not the story's start).
|
|
125
|
+
*/
|
|
126
|
+
export const NEWEST_FIRST: KanbanSortFieldRule[] = [{ field: "updated_at", direction: "desc" }];
|
|
127
|
+
|
|
121
128
|
function boardConfigForType(type: string, columns: readonly { key: string; name: string; collapsed?: boolean }[]): KanbanConfig {
|
|
122
129
|
const statuses = columns.map(column => ({ ...column, category: "native" }));
|
|
123
130
|
return {
|
|
124
131
|
hierarchy: [], types: [type], statuses,
|
|
125
132
|
categories: [{ name: "native" }], priorities: {}, type_colors: {},
|
|
133
|
+
sort_order: Object.fromEntries(statuses.map(status => [status.key, [...NEWEST_FIRST]])),
|
|
126
134
|
workflows: { [type]: Object.fromEntries(statuses.map(status => [status.key, []])) },
|
|
127
135
|
};
|
|
128
136
|
}
|
|
@@ -165,6 +173,7 @@ export function deskBoardConfig(issues: readonly NativeBoardIssue[]): KanbanConf
|
|
|
165
173
|
statuses: [...storiesConfig.statuses, ...assignmentsConfig.statuses],
|
|
166
174
|
categories: storiesConfig.categories,
|
|
167
175
|
workflows: { ...storiesConfig.workflows, ...assignmentsConfig.workflows },
|
|
176
|
+
sort_order: { ...assignmentsConfig.sort_order, ...storiesConfig.sort_order },
|
|
168
177
|
};
|
|
169
178
|
}
|
|
170
179
|
|
|
@@ -161,13 +161,6 @@ export function applyPilobolusDemoDashboardContent(
|
|
|
161
161
|
}));
|
|
162
162
|
|
|
163
163
|
localized.messages = localized.messages.map((message) => {
|
|
164
|
-
if (message.messageKind === "ingestion_rationale") {
|
|
165
|
-
return {
|
|
166
|
-
...message,
|
|
167
|
-
body: "Imported as a useful holdout against generic platform demo intake.",
|
|
168
|
-
summary: "Pilobolus intake rationale",
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
164
|
if (message.messageKind === "reporting_context_packet") {
|
|
172
165
|
const metadata = message.metadata && typeof message.metadata === "object" && !Array.isArray(message.metadata)
|
|
173
166
|
? message.metadata as Record<string, unknown>
|
|
@@ -8,10 +8,10 @@ export type OrganizerDestination = {
|
|
|
8
8
|
};
|
|
9
9
|
export const ORGANIZER_NAV_GROUPS: readonly { title: string; items: readonly OrganizerDestination[] }[] = [
|
|
10
10
|
{ title: "Editorial work", items: [
|
|
11
|
-
{ key: "overview", label: "Needs me", description: "See what needs you:
|
|
11
|
+
{ key: "overview", label: "Needs me", description: "See what needs you: Proposed Stories, references and reviews.", path: "/newsroom", primary: true },
|
|
12
12
|
{ key: "articles", label: getContentItemName().plural, description: `Follow ${contentItemPluralLowercase} across their stages and open one to write or revise it.`, path: `/newsroom/${contentItemBoardPathSegment()}`, alsoActiveOnPaths: ["/newsroom/articles"], primary: true },
|
|
13
13
|
{ key: "assignments", label: "Assignments", description: "Follow research and reporting work across its statuses and claim it.", path: "/newsroom/assignments" },
|
|
14
|
-
{ key: "drafts", label: "
|
|
14
|
+
{ key: "drafts", label: "Proposed Stories", description: "Read the Proposed Stories that AI suggests, then accept or reject them.", path: "/newsroom/administration/proposals" },
|
|
15
15
|
{ key: "messages", label: "Messages", description: "Read and reply to newsroom conversations.", path: "/newsroom/messages", primary: true },
|
|
16
16
|
] },
|
|
17
17
|
{ title: "Research & knowledge", items: [
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages are created through the `postMessage` mutation, never through `createMessage`: the backend stamps the author from the
|
|
3
|
+
* signed-in identity, so a browser cannot name one. This is the one place that turns a Message record into that mutation and its
|
|
4
|
+
* arguments, shared by the conversation, forum and console chat writers. Authorship and timestamps are left out on purpose.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ARGUMENT_TYPES = {
|
|
8
|
+
id: "ID!",
|
|
9
|
+
threadId: "ID!",
|
|
10
|
+
content: "String",
|
|
11
|
+
messageKind: "String!",
|
|
12
|
+
messageDomain: "String!",
|
|
13
|
+
summary: "String",
|
|
14
|
+
parentMessageId: "ID",
|
|
15
|
+
sequenceNumber: "Int",
|
|
16
|
+
messageType: "String",
|
|
17
|
+
role: "String",
|
|
18
|
+
source: "String",
|
|
19
|
+
authorLabel: "String",
|
|
20
|
+
semanticLayer: "String",
|
|
21
|
+
searchVisibility: "String",
|
|
22
|
+
responseTarget: "String",
|
|
23
|
+
responseStatus: "String",
|
|
24
|
+
responseOwner: "String",
|
|
25
|
+
responseStartedAt: "AWSDateTime",
|
|
26
|
+
responseCompletedAt: "AWSDateTime",
|
|
27
|
+
metadata: "AWSJSON",
|
|
28
|
+
newsroomFeedKey: "String",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export type PostMessageArguments = Partial<Record<keyof typeof ARGUMENT_TYPES, string | number | null>>;
|
|
32
|
+
|
|
33
|
+
type MessageLike = Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
/** The arguments of `postMessage` from a Message record. Anything else on the record (authorSub, authorUserProfileId, createdAt, updatedAt) is dropped. */
|
|
36
|
+
export function postMessageArguments(record: MessageLike): PostMessageArguments {
|
|
37
|
+
const args: PostMessageArguments = {};
|
|
38
|
+
for (const key of Object.keys(ARGUMENT_TYPES) as Array<keyof typeof ARGUMENT_TYPES>) {
|
|
39
|
+
let value = record[key === "content" ? "content" : key];
|
|
40
|
+
if (key === "content") value = record.content ?? record.body;
|
|
41
|
+
if (value === undefined || value === null) continue;
|
|
42
|
+
if (key === "metadata") value = typeof value === "string" ? value : JSON.stringify(value);
|
|
43
|
+
args[key] = value as string | number;
|
|
44
|
+
}
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `mutation PostMessage($id: ID!, …) { postMessage(id: $id, …) { <selection> } }` declaring exactly the arguments given. */
|
|
49
|
+
export function postMessageMutation(args: PostMessageArguments, selection: string): string {
|
|
50
|
+
const keys = Object.keys(args) as Array<keyof typeof ARGUMENT_TYPES>;
|
|
51
|
+
const declared = keys.map((key) => `$${key}: ${ARGUMENT_TYPES[key]}`).join(", ");
|
|
52
|
+
const passed = keys.map((key) => `${key}: $${key}`).join(", ");
|
|
53
|
+
return `mutation PostMessage(${declared}) { postMessage(${passed}) { ${selection} } }`;
|
|
54
|
+
}
|
package/lib/proposals-admin.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type ProposalFilter = "open" | "draft" | "in_steering" | "editor_review"
|
|
|
22
22
|
export type ProposalMove = { label: string; path?: string[]; edit?: "pin" | "unpin" | "hold" | "release"; state?: string };
|
|
23
23
|
|
|
24
24
|
const STATE_LABELS: Record<string, string> = {
|
|
25
|
-
draft: "
|
|
25
|
+
draft: "Proposed",
|
|
26
26
|
in_steering: "In steering",
|
|
27
27
|
editor_review: "Editor review",
|
|
28
28
|
approved: "Approved",
|
|
@@ -31,7 +31,7 @@ const STATE_LABELS: Record<string, string> = {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const KIND_LABELS: Record<string, string> = {
|
|
34
|
-
story_draft: "Story
|
|
34
|
+
story_draft: "Proposed Story",
|
|
35
35
|
new_concept: "New concept",
|
|
36
36
|
rename: "Rename",
|
|
37
37
|
merge: "Merge",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keys the References page reads through (docs/reference-feed-indexes.md). The Python twin is
|
|
3
|
+
* src/papyrus_content/reference_feed_keys.py; scripts/fixtures/reference-feed-key-vectors.json holds the vectors both test.
|
|
4
|
+
*
|
|
5
|
+
* `feedStateKey` is "<corpusId>#unrated" (pending) or "<corpusId>#rated" (accepted or rejected); a Reference that is not the current
|
|
6
|
+
* version, or is archived, has none and is in neither feed. `predictionSortKey` is the Cyclotron's ORIGINAL P(accept), then arrival, then
|
|
7
|
+
* id; a Reference nobody predicted sorts as 0.5. A vote changes `feedStateKey` and nothing else here.
|
|
8
|
+
*/
|
|
9
|
+
import { normalizeReferenceCurationStatus } from "./reference-policy";
|
|
10
|
+
|
|
11
|
+
export const UNDECIDED_PROBABILITY = 0.5;
|
|
12
|
+
const EPOCH = "1970-01-01T00:00:00.000Z";
|
|
13
|
+
const ARRIVAL_FIELDS = ["createdAt", "importedAt", "retrievedAt", "sourcePublishedAt", "versionCreatedAt"] as const;
|
|
14
|
+
|
|
15
|
+
export type ReferenceFeedKeyInput = {
|
|
16
|
+
id: string;
|
|
17
|
+
corpusId?: string | null;
|
|
18
|
+
curationStatus?: string | null;
|
|
19
|
+
versionState?: string | null;
|
|
20
|
+
predictedAcceptProbability?: number | null;
|
|
21
|
+
createdAt?: string | null;
|
|
22
|
+
importedAt?: string | null;
|
|
23
|
+
retrievedAt?: string | null;
|
|
24
|
+
sourcePublishedAt?: string | null;
|
|
25
|
+
versionCreatedAt?: string | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ReferenceFeedKeys = {
|
|
29
|
+
feedStateKey: string | null;
|
|
30
|
+
feedArrivalAt: string;
|
|
31
|
+
predictionSortKey: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function instant(value: string | null | undefined): string | null {
|
|
35
|
+
if (!value) return null;
|
|
36
|
+
const parsed = Date.parse(value);
|
|
37
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function referenceFeedArrivalAt(reference: ReferenceFeedKeyInput): string {
|
|
41
|
+
for (const field of ARRIVAL_FIELDS) {
|
|
42
|
+
const found = instant(reference[field]);
|
|
43
|
+
if (found) return found;
|
|
44
|
+
}
|
|
45
|
+
return EPOCH;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function referenceFeedStateKey(reference: ReferenceFeedKeyInput): string | null {
|
|
49
|
+
if (reference.versionState && reference.versionState !== "current") return null;
|
|
50
|
+
if (!reference.corpusId) return null;
|
|
51
|
+
const status = normalizeReferenceCurationStatus(reference.curationStatus);
|
|
52
|
+
if (status === "archived") return null;
|
|
53
|
+
return `${reference.corpusId}#${status === "pending" ? "unrated" : "rated"}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function predictionSortKeyFor(probability: number | null | undefined, arrival: string, referenceId: string): string {
|
|
57
|
+
const chosen = probability === null || probability === undefined ? UNDECIDED_PROBABILITY : Math.min(1, Math.max(0, probability));
|
|
58
|
+
return `${chosen.toFixed(6)}#${arrival}#${referenceId}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The three feed keys for a Reference as it stands. `probability` overrides the one stored on it. */
|
|
62
|
+
export function referenceFeedKeys(reference: ReferenceFeedKeyInput, probability?: number | null): ReferenceFeedKeys {
|
|
63
|
+
const arrival = referenceFeedArrivalAt(reference);
|
|
64
|
+
return {
|
|
65
|
+
feedStateKey: referenceFeedStateKey(reference),
|
|
66
|
+
feedArrivalAt: arrival,
|
|
67
|
+
predictionSortKey: predictionSortKeyFor(probability ?? reference.predictedAcceptProbability, arrival, reference.id),
|
|
68
|
+
};
|
|
69
|
+
}
|