@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
|
@@ -49,8 +49,11 @@ import type {
|
|
|
49
49
|
import { relationTypeKey, semanticStateKey, semanticVersionKey } from "../lib/semantic-graph";
|
|
50
50
|
import {
|
|
51
51
|
CONVERSATION_FEED_KEY,
|
|
52
|
+
GENERAL_CONVERSATION_KIND,
|
|
52
53
|
REFERENCE_CONVERSATION_KIND,
|
|
53
54
|
conversationAnchorKey,
|
|
55
|
+
conversationPreviewLine,
|
|
56
|
+
threadMetadataWithLastAuthor,
|
|
54
57
|
conversationMessageId,
|
|
55
58
|
conversationThreadId,
|
|
56
59
|
conversationThreadKind,
|
|
@@ -58,12 +61,15 @@ import {
|
|
|
58
61
|
findOrCreateThread,
|
|
59
62
|
type ConversationAnchor,
|
|
60
63
|
type ConversationMessage,
|
|
64
|
+
type ConversationViewer,
|
|
61
65
|
} from "../lib/conversation";
|
|
66
|
+
import { postMessageArguments, postMessageMutation } from "../lib/post-message";
|
|
62
67
|
import { legacyStageKeysToLoad, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
|
|
63
68
|
import { getContentItemStageDefinitions } from "../lib/site-brand";
|
|
64
69
|
import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
|
|
65
70
|
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
66
71
|
import { isUnauthenticatedError, loadReaderSessionSnapshot, type ReaderAuthSnapshot } from "./reader-auth-state";
|
|
72
|
+
import { resolveReaderSettings } from "./reader-settings";
|
|
67
73
|
|
|
68
74
|
const USER_POOL_AUTH_MODE = "userPool";
|
|
69
75
|
const USER_POOL_LIST_LIMIT = 500;
|
|
@@ -77,7 +83,7 @@ const REFERENCE_STATUS_KEYS = ["pending", "accepted", "rejected", "archived"] as
|
|
|
77
83
|
const NEWSROOM_MESSAGE_FEED_QUERY = `
|
|
78
84
|
query ListMessagesByNewsroomFeedAndCreatedAt($newsroomFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelMessageFilterInput) {
|
|
79
85
|
listMessagesByNewsroomFeedAndCreatedAt(newsroomFeedKey: $newsroomFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
|
|
80
|
-
items { id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey }
|
|
86
|
+
items { id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel editedAt deletedAt deletedBySub threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey }
|
|
81
87
|
nextToken
|
|
82
88
|
}
|
|
83
89
|
}
|
|
@@ -136,7 +142,7 @@ const CREATE_SEMANTIC_RELATION_MUTATION = `
|
|
|
136
142
|
const LIST_MESSAGES_BY_THREAD_AND_SEQUENCE_QUERY = `
|
|
137
143
|
query ListMessagesByThreadAndSequence($threadId: ID!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
|
|
138
144
|
listMessagesByThreadAndSequence(threadId: $threadId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
|
|
139
|
-
items { id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey }
|
|
145
|
+
items { id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel editedAt deletedAt deletedBySub threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey }
|
|
140
146
|
nextToken
|
|
141
147
|
}
|
|
142
148
|
}
|
|
@@ -145,7 +151,7 @@ const LIST_MESSAGES_BY_THREAD_AND_SEQUENCE_QUERY = `
|
|
|
145
151
|
const LIST_MESSAGES_BY_KIND_AND_CREATED_AT_QUERY = `
|
|
146
152
|
query ListMessagesByKindAndCreatedAt($messageKind: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
|
|
147
153
|
listMessagesByKindAndCreatedAt(messageKind: $messageKind, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
|
|
148
|
-
items { id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey }
|
|
154
|
+
items { id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel editedAt deletedAt deletedBySub threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey }
|
|
149
155
|
nextToken
|
|
150
156
|
}
|
|
151
157
|
}
|
|
@@ -154,7 +160,7 @@ const LIST_MESSAGES_BY_KIND_AND_CREATED_AT_QUERY = `
|
|
|
154
160
|
const GET_MESSAGE_QUERY = `
|
|
155
161
|
query GetMessage($id: ID!) {
|
|
156
162
|
getMessage(id: $id) {
|
|
157
|
-
id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey
|
|
163
|
+
id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel editedAt deletedAt deletedBySub threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey
|
|
158
164
|
}
|
|
159
165
|
}
|
|
160
166
|
`;
|
|
@@ -219,40 +225,7 @@ const UPDATE_MESSAGE_THREAD_MUTATION = `
|
|
|
219
225
|
}
|
|
220
226
|
`;
|
|
221
227
|
|
|
222
|
-
const
|
|
223
|
-
mutation CreateMessage($input: CreateMessageInput!) {
|
|
224
|
-
createMessage(input: $input) {
|
|
225
|
-
id
|
|
226
|
-
messageKind
|
|
227
|
-
messageDomain
|
|
228
|
-
status
|
|
229
|
-
summary
|
|
230
|
-
source
|
|
231
|
-
importRunId
|
|
232
|
-
authorSub
|
|
233
|
-
authorUserProfileId
|
|
234
|
-
authorLabel
|
|
235
|
-
threadId
|
|
236
|
-
parentMessageId
|
|
237
|
-
sequenceNumber
|
|
238
|
-
role
|
|
239
|
-
messageType
|
|
240
|
-
content
|
|
241
|
-
semanticLayer
|
|
242
|
-
searchVisibility
|
|
243
|
-
responseTarget
|
|
244
|
-
responseStatus
|
|
245
|
-
responseOwner
|
|
246
|
-
responseStartedAt
|
|
247
|
-
responseCompletedAt
|
|
248
|
-
responseError
|
|
249
|
-
metadata
|
|
250
|
-
createdAt
|
|
251
|
-
updatedAt
|
|
252
|
-
newsroomFeedKey
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
`;
|
|
228
|
+
const POST_MESSAGE_SELECTION = "id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel editedAt deletedAt deletedBySub threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey";
|
|
256
229
|
|
|
257
230
|
const UPDATE_MESSAGE_MUTATION = `
|
|
258
231
|
mutation UpdateMessage($input: UpdateMessageInput!) {
|
|
@@ -267,6 +240,9 @@ const UPDATE_MESSAGE_MUTATION = `
|
|
|
267
240
|
authorSub
|
|
268
241
|
authorUserProfileId
|
|
269
242
|
authorLabel
|
|
243
|
+
editedAt
|
|
244
|
+
deletedAt
|
|
245
|
+
deletedBySub
|
|
270
246
|
threadId
|
|
271
247
|
parentMessageId
|
|
272
248
|
sequenceNumber
|
|
@@ -301,7 +277,7 @@ const NEWSROOM_ASSIGNMENT_FEED_QUERY = `
|
|
|
301
277
|
const NEWSROOM_REFERENCE_FEED_QUERY = `
|
|
302
278
|
query ListReferencesByNewsroomFeedAndCreatedAt($newsroomFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelReferenceFilterInput) {
|
|
303
279
|
listReferencesByNewsroomFeedAndCreatedAt(newsroomFeedKey: $newsroomFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
|
|
304
|
-
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
|
|
280
|
+
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey discoveredByMonitorId updatedAt }
|
|
305
281
|
nextToken
|
|
306
282
|
}
|
|
307
283
|
}
|
|
@@ -310,7 +286,7 @@ const NEWSROOM_REFERENCE_FEED_QUERY = `
|
|
|
310
286
|
const NEWSROOM_REFERENCE_IMPORTED_FEED_QUERY = `
|
|
311
287
|
query ListReferencesByNewsroomFeedAndImportedAt($newsroomFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelReferenceFilterInput) {
|
|
312
288
|
listReferencesByNewsroomFeedAndImportedAt(newsroomFeedKey: $newsroomFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
|
|
313
|
-
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
|
|
289
|
+
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey discoveredByMonitorId updatedAt }
|
|
314
290
|
nextToken
|
|
315
291
|
}
|
|
316
292
|
}
|
|
@@ -319,7 +295,7 @@ const NEWSROOM_REFERENCE_IMPORTED_FEED_QUERY = `
|
|
|
319
295
|
const LIST_REFERENCES_BY_CURATION_STATUS_AND_UPDATED_AT_QUERY = `
|
|
320
296
|
query ListReferencesByCurationStatusAndUpdatedAt($curationStatus: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelReferenceFilterInput) {
|
|
321
297
|
listReferencesByCurationStatusAndUpdatedAt(curationStatus: $curationStatus, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
|
|
322
|
-
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
|
|
298
|
+
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey discoveredByMonitorId updatedAt }
|
|
323
299
|
nextToken
|
|
324
300
|
}
|
|
325
301
|
}
|
|
@@ -328,7 +304,7 @@ const LIST_REFERENCES_BY_CURATION_STATUS_AND_UPDATED_AT_QUERY = `
|
|
|
328
304
|
const LIST_REFERENCES_BY_REVIEWED_FEED_AND_UPDATED_AT_QUERY = `
|
|
329
305
|
query ListReferencesByReviewedFeedAndUpdatedAt($reviewedFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelReferenceFilterInput) {
|
|
330
306
|
listReferencesByReviewedFeedAndUpdatedAt(reviewedFeedKey: $reviewedFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
|
|
331
|
-
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
|
|
307
|
+
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey discoveredByMonitorId updatedAt }
|
|
332
308
|
nextToken
|
|
333
309
|
}
|
|
334
310
|
}
|
|
@@ -337,7 +313,7 @@ const LIST_REFERENCES_BY_REVIEWED_FEED_AND_UPDATED_AT_QUERY = `
|
|
|
337
313
|
const LIST_REFERENCES_FALLBACK_QUERY = `
|
|
338
314
|
query ListReferencesFallback($limit: Int, $nextToken: String) {
|
|
339
315
|
listReferences(limit: $limit, nextToken: $nextToken) {
|
|
340
|
-
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
|
|
316
|
+
items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey discoveredByMonitorId updatedAt }
|
|
341
317
|
nextToken
|
|
342
318
|
}
|
|
343
319
|
}
|
|
@@ -470,7 +446,7 @@ const MODEL_ATTACHMENTS_BY_OWNER_QUERY = `
|
|
|
470
446
|
|
|
471
447
|
/** Matches Reference model fields in amplify/data/resource.ts (no metadata on Reference). */
|
|
472
448
|
const REFERENCE_RECORD_GRAPHQL_FIELDS =
|
|
473
|
-
"id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt";
|
|
449
|
+
"id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey discoveredByMonitorId updatedAt";
|
|
474
450
|
|
|
475
451
|
const LIST_REFERENCES_BY_LINEAGE_AND_VERSION_QUERY = `
|
|
476
452
|
query ListReferencesByLineageAndVersion($lineageId: ID!, $limit: Int, $nextToken: String) {
|
|
@@ -1422,7 +1398,7 @@ export async function createSectionForumThreadRecord(input: {
|
|
|
1422
1398
|
* Appends one Message to a thread: the next sequence number, the Message, then the thread's count and last Message. The Message id
|
|
1423
1399
|
* derives from the sequence number, so a create that loses a race for the number collides and retries with the next one.
|
|
1424
1400
|
*/
|
|
1425
|
-
async function appendMessageToThread(thread: MessageThreadRecord, build: (sequenceNumber: number, now: string) => MessageRecord): Promise<{ thread: MessageThreadRecord; message: MessageRecord }> {
|
|
1401
|
+
async function appendMessageToThread(thread: MessageThreadRecord, build: (sequenceNumber: number, now: string) => MessageRecord, threadFields: { summary?: string; metadata?: string } = {}): Promise<{ thread: MessageThreadRecord; message: MessageRecord }> {
|
|
1426
1402
|
const threadId = thread.id;
|
|
1427
1403
|
let now = new Date().toISOString();
|
|
1428
1404
|
let built: MessageRecord | null = null;
|
|
@@ -1430,11 +1406,15 @@ async function appendMessageToThread(thread: MessageThreadRecord, build: (sequen
|
|
|
1430
1406
|
list: () => listMessagesByThreadId(threadId, 1000),
|
|
1431
1407
|
create: (sequenceNumber) => {
|
|
1432
1408
|
now = new Date().toISOString();
|
|
1433
|
-
|
|
1434
|
-
|
|
1409
|
+
// `createdAt` is part of the composite sort key of messagesByResponseTargetStatusCreatedAt, so AppSync refuses a Message that
|
|
1410
|
+
// sets it without `responseStatus`. A person's message waits on no responder: COMPLETED, as the Python message contract writes.
|
|
1411
|
+
built = { responseStatus: "COMPLETED", ...build(sequenceNumber, now) };
|
|
1412
|
+
// The backend stamps the author from the signed-in identity (postMessage); nothing here names one.
|
|
1413
|
+
const args = postMessageArguments(built as unknown as Record<string, unknown>);
|
|
1414
|
+
return runGraphql<{ postMessage?: MessageRecord | null }>(postMessageMutation(args, POST_MESSAGE_SELECTION), args);
|
|
1435
1415
|
},
|
|
1436
1416
|
});
|
|
1437
|
-
const createdMessage = normalizeMessageRecord(created.
|
|
1417
|
+
const createdMessage = normalizeMessageRecord(created.postMessage ?? built!);
|
|
1438
1418
|
const updated = await runGraphql<{ updateMessageThread?: MessageThreadRecord | null }>(
|
|
1439
1419
|
UPDATE_MESSAGE_THREAD_MUTATION,
|
|
1440
1420
|
{
|
|
@@ -1444,11 +1424,12 @@ async function appendMessageToThread(thread: MessageThreadRecord, build: (sequen
|
|
|
1444
1424
|
lastMessageId: createdMessage.id,
|
|
1445
1425
|
lastMessageAt: createdMessage.createdAt,
|
|
1446
1426
|
updatedAt: now,
|
|
1427
|
+
...threadFields,
|
|
1447
1428
|
},
|
|
1448
1429
|
},
|
|
1449
1430
|
);
|
|
1450
1431
|
return {
|
|
1451
|
-
thread: normalizeThreadRecord(updated.updateMessageThread ?? { ...thread, updatedAt: now }),
|
|
1432
|
+
thread: normalizeThreadRecord(updated.updateMessageThread ?? { ...thread, ...threadFields, updatedAt: now }),
|
|
1452
1433
|
message: createdMessage,
|
|
1453
1434
|
};
|
|
1454
1435
|
}
|
|
@@ -1490,7 +1471,7 @@ export async function appendForumThreadMessageRecord(input: {
|
|
|
1490
1471
|
}));
|
|
1491
1472
|
}
|
|
1492
1473
|
|
|
1493
|
-
type ConversationAuthor = { sub: string | null; label: string };
|
|
1474
|
+
type ConversationAuthor = { sub: string | null; label: string; userProfileId: string | null; isAdmin: boolean };
|
|
1494
1475
|
|
|
1495
1476
|
async function currentConversationAuthor(): Promise<ConversationAuthor> {
|
|
1496
1477
|
const snapshot = await loadReaderSessionSnapshot();
|
|
@@ -1502,7 +1483,50 @@ async function currentConversationAuthor(): Promise<ConversationAuthor> {
|
|
|
1502
1483
|
} catch {
|
|
1503
1484
|
sub = null;
|
|
1504
1485
|
}
|
|
1505
|
-
|
|
1486
|
+
const profile = await currentUserProfile();
|
|
1487
|
+
// The author's name travels on the message (other editors cannot read profiles): their display name, else the session's label.
|
|
1488
|
+
const sessionLabel = snapshot.auth.status === "signedIn" ? snapshot.auth.label : "editor";
|
|
1489
|
+
return { sub, label: profile.displayName || sessionLabel, userProfileId: profile.id, isAdmin: snapshot.groups.some((group) => group.trim().toLowerCase() === "admin") };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
let userProfileLookup: Promise<{ id: string | null; displayName: string | null }> | null = null;
|
|
1493
|
+
/** The signed-in person's profile id and display name (their own profile is readable by them), read once per page load. A failed read is not kept, and does not stop a post. */
|
|
1494
|
+
function currentUserProfile(): Promise<{ id: string | null; displayName: string | null }> {
|
|
1495
|
+
userProfileLookup ??= (async () => {
|
|
1496
|
+
const resolution = await resolveReaderSettings();
|
|
1497
|
+
const id = resolution.userProfileId ?? null;
|
|
1498
|
+
if (!id) return { id: null, displayName: null };
|
|
1499
|
+
const names = await loadUserProfileNames([id]);
|
|
1500
|
+
return { id, displayName: names.get(id) ?? null };
|
|
1501
|
+
})().catch(() => { userProfileLookup = null; return { id: null, displayName: null }; });
|
|
1502
|
+
return userProfileLookup;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/** Who is looking at a conversation: their sub, and whether they are an admin. Decided from the session, never from a Message. */
|
|
1506
|
+
export async function loadConversationViewer(): Promise<ConversationViewer> {
|
|
1507
|
+
const author = await currentConversationAuthor();
|
|
1508
|
+
return { sub: author.sub, isAdmin: author.isAdmin };
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/** Display names by profile id, in one request. A profile this person may not read is left out and its Messages keep the saved label. */
|
|
1512
|
+
export async function loadUserProfileNames(profileIds: string[]): Promise<Map<string, string>> {
|
|
1513
|
+
const ids = [...new Set(profileIds.filter(Boolean))];
|
|
1514
|
+
const names = new Map<string, string>();
|
|
1515
|
+
if (!ids.length) return names;
|
|
1516
|
+
const query = `query UserProfileNames { ${ids.map((id, index) => `p${index}: getUserProfile(id: ${JSON.stringify(id)}) { id displayName }`).join(" ")} }`;
|
|
1517
|
+
configureAmplifyClient();
|
|
1518
|
+
const client = generateClient<Schema>() as unknown as { graphql: (options: Record<string, unknown>) => Promise<{ data?: Record<string, { id?: string; displayName?: string | null } | null> | null }> };
|
|
1519
|
+
let data: Record<string, { id?: string; displayName?: string | null } | null> | null | undefined;
|
|
1520
|
+
try {
|
|
1521
|
+
data = (await client.graphql({ query, authMode: USER_POOL_AUTH_MODE })).data;
|
|
1522
|
+
} catch (failure) {
|
|
1523
|
+
// Some of the profiles are refused: the rest of the answer is still good.
|
|
1524
|
+
data = (failure as { data?: typeof data } | null)?.data;
|
|
1525
|
+
}
|
|
1526
|
+
for (const profile of Object.values(data ?? {})) {
|
|
1527
|
+
if (profile?.id && profile.displayName?.trim()) names.set(profile.id, profile.displayName.trim());
|
|
1528
|
+
}
|
|
1529
|
+
return names;
|
|
1506
1530
|
}
|
|
1507
1531
|
|
|
1508
1532
|
async function findConversationThread(anchorKey: string): Promise<MessageThreadRecord | null> {
|
|
@@ -1536,6 +1560,7 @@ export async function ensureConversationThread(anchor: ConversationAnchor, autho
|
|
|
1536
1560
|
primaryAnchorLineageId: lineageId,
|
|
1537
1561
|
primaryAnchorKey: anchorKey,
|
|
1538
1562
|
createdBySub: author?.sub ?? null,
|
|
1563
|
+
createdByUserProfileId: author?.userProfileId ?? null,
|
|
1539
1564
|
createdByLabel: author?.label ?? "editor",
|
|
1540
1565
|
messageCount: 0,
|
|
1541
1566
|
lastMessageId: null,
|
|
@@ -1556,9 +1581,14 @@ function toConversationMessage(record: MessageRecord): ConversationMessage {
|
|
|
1556
1581
|
id: record.id,
|
|
1557
1582
|
parentMessageId: record.parentMessageId ?? null,
|
|
1558
1583
|
authorLabel: record.authorLabel ?? "",
|
|
1559
|
-
|
|
1584
|
+
authorSub: record.authorSub ?? null,
|
|
1585
|
+
authorUserProfileId: record.authorUserProfileId ?? null,
|
|
1586
|
+
content: record.status === "deleted" ? "" : record.content ?? record.body ?? "",
|
|
1560
1587
|
createdAt: record.createdAt,
|
|
1561
1588
|
sequenceNumber: Number(record.sequenceNumber ?? 0),
|
|
1589
|
+
editedAt: record.editedAt ?? null,
|
|
1590
|
+
deleted: record.status === "deleted",
|
|
1591
|
+
deletedAt: record.deletedAt ?? null,
|
|
1562
1592
|
};
|
|
1563
1593
|
}
|
|
1564
1594
|
|
|
@@ -1569,7 +1599,7 @@ export async function loadConversationMessages(anchor: ConversationAnchor): Prom
|
|
|
1569
1599
|
const thread = await findConversationThread(anchorKey);
|
|
1570
1600
|
if (!thread) return [];
|
|
1571
1601
|
const records = await listMessagesByThreadId(thread.id, 500);
|
|
1572
|
-
return records.
|
|
1602
|
+
return records.map(toConversationMessage);
|
|
1573
1603
|
}
|
|
1574
1604
|
|
|
1575
1605
|
/** How many Messages each Reference has, from the Reference conversation threads alone (no query per card). Keyed by lineage id. */
|
|
@@ -1591,11 +1621,13 @@ export async function appendConversationMessage(input: {
|
|
|
1591
1621
|
anchor: ConversationAnchor;
|
|
1592
1622
|
body: string;
|
|
1593
1623
|
parentMessageId?: string | null;
|
|
1624
|
+
/** The conversation to write into, when the caller already holds it (the Messages page). Without it the anchor finds or creates it. */
|
|
1625
|
+
thread?: MessageThreadRecord;
|
|
1594
1626
|
}): Promise<{ message: ConversationMessage; thread: MessageThreadRecord; relationError: string | null }> {
|
|
1595
1627
|
const content = String(input.body || "").trim();
|
|
1596
1628
|
if (!content) throw new Error("Write something to post.");
|
|
1597
1629
|
const author = await currentConversationAuthor();
|
|
1598
|
-
const thread = await ensureConversationThread(input.anchor, author);
|
|
1630
|
+
const thread = input.thread ?? await ensureConversationThread(input.anchor, author);
|
|
1599
1631
|
const result = await appendMessageToThread(thread, (sequenceNumber, now) => ({
|
|
1600
1632
|
id: conversationMessageId(thread.id, sequenceNumber),
|
|
1601
1633
|
messageKind: "comment",
|
|
@@ -1603,7 +1635,6 @@ export async function appendConversationMessage(input: {
|
|
|
1603
1635
|
status: "active",
|
|
1604
1636
|
summary: content.slice(0, 120),
|
|
1605
1637
|
source: "newsroom.conversation",
|
|
1606
|
-
authorSub: author.sub,
|
|
1607
1638
|
authorLabel: author.label,
|
|
1608
1639
|
threadId: thread.id,
|
|
1609
1640
|
parentMessageId: input.parentMessageId?.trim() || null,
|
|
@@ -1615,7 +1646,7 @@ export async function appendConversationMessage(input: {
|
|
|
1615
1646
|
createdAt: now,
|
|
1616
1647
|
updatedAt: now,
|
|
1617
1648
|
newsroomFeedKey: CONVERSATION_FEED_KEY,
|
|
1618
|
-
}));
|
|
1649
|
+
}), { summary: conversationPreviewLine(content), metadata: threadMetadataWithLastAuthor(thread.metadata, author.label) });
|
|
1619
1650
|
let relationError: string | null = null;
|
|
1620
1651
|
if (input.anchor.kind !== "none") {
|
|
1621
1652
|
try {
|
|
@@ -1627,6 +1658,124 @@ export async function appendConversationMessage(input: {
|
|
|
1627
1658
|
return { message: toConversationMessage(result.message), thread: result.thread, relationError };
|
|
1628
1659
|
}
|
|
1629
1660
|
|
|
1661
|
+
/** One page of conversation threads of a kind, newest activity first. */
|
|
1662
|
+
export async function loadConversationThreadPage(threadKind: string, nextToken: string | null, limit = 20): Promise<{ items: MessageThreadRecord[]; nextToken: string | null }> {
|
|
1663
|
+
type ThreadConnectionResponse = { listMessageThreadsByKindAndUpdatedAt?: { items?: Array<MessageThreadRecord | null> | null; nextToken?: string | null } | null };
|
|
1664
|
+
const response = await runGraphql<ThreadConnectionResponse>(LIST_MESSAGE_THREADS_BY_KIND_AND_UPDATED_AT_QUERY, { threadKind, sortDirection: "DESC", limit, nextToken });
|
|
1665
|
+
const connection = response.listMessageThreadsByKindAndUpdatedAt;
|
|
1666
|
+
const items = ((connection?.items ?? []).filter(Boolean) as MessageThreadRecord[]).map(normalizeThreadRecord).filter((thread) => thread.status !== "deleted");
|
|
1667
|
+
return { items, nextToken: connection?.nextToken ?? null };
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/** Every Message in a conversation thread, oldest first. */
|
|
1671
|
+
export async function loadConversationThreadMessages(threadId: string): Promise<ConversationMessage[]> {
|
|
1672
|
+
const records = await listMessagesByThreadId(threadId, 500);
|
|
1673
|
+
return records.map(toConversationMessage);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/** Starts a conversation about nothing but itself: its own thread, and its first Message. */
|
|
1677
|
+
export async function startUnattachedConversation(title: string, body: string): Promise<{ message: ConversationMessage; thread: MessageThreadRecord }> {
|
|
1678
|
+
const content = String(body || "").trim();
|
|
1679
|
+
const heading = String(title || "").trim();
|
|
1680
|
+
if (!heading) throw new Error("Give the conversation a title.");
|
|
1681
|
+
if (!content) throw new Error("Write something to post.");
|
|
1682
|
+
const author = await currentConversationAuthor();
|
|
1683
|
+
const now = new Date().toISOString();
|
|
1684
|
+
const id = `message-thread-conversation-new-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1685
|
+
const thread: MessageThreadRecord = {
|
|
1686
|
+
id,
|
|
1687
|
+
threadKind: GENERAL_CONVERSATION_KIND,
|
|
1688
|
+
status: "active",
|
|
1689
|
+
title: heading.slice(0, 120),
|
|
1690
|
+
createdBySub: author.sub,
|
|
1691
|
+
createdByUserProfileId: author.userProfileId,
|
|
1692
|
+
createdByLabel: author.label,
|
|
1693
|
+
messageCount: 0,
|
|
1694
|
+
lastMessageId: null,
|
|
1695
|
+
lastMessageAt: null,
|
|
1696
|
+
metadata: JSON.stringify({ anchorKind: "none" }),
|
|
1697
|
+
createdAt: now,
|
|
1698
|
+
updatedAt: now,
|
|
1699
|
+
newsroomFeedKey: CONVERSATION_FEED_KEY,
|
|
1700
|
+
};
|
|
1701
|
+
const created = await runGraphql<{ createMessageThread?: MessageThreadRecord | null }>(CREATE_MESSAGE_THREAD_MUTATION, { input: thread });
|
|
1702
|
+
const saved = normalizeThreadRecord(created.createMessageThread ?? thread);
|
|
1703
|
+
const result = await appendConversationMessage({ anchor: { kind: "none" }, body: content, thread: saved });
|
|
1704
|
+
return { message: result.message, thread: result.thread };
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const GET_MESSAGE_THREAD_QUERY = `
|
|
1708
|
+
query GetMessageThread($id: ID!) {
|
|
1709
|
+
getMessageThread(id: $id) {
|
|
1710
|
+
id threadKind status title summary primaryAnchorKind primaryAnchorId primaryAnchorLineageId primaryAnchorKey createdBySub createdByUserProfileId createdByLabel messageCount lastMessageId lastMessageAt contextDigest activeResponseMessageId responseLockOwner responseLockExpiresAt metadata createdAt updatedAt newsroomFeedKey
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
`;
|
|
1714
|
+
|
|
1715
|
+
const MESSAGE_ACTION_SELECTION = "id messageKind messageDomain status summary source importRunId authorSub authorUserProfileId authorLabel editedAt deletedAt deletedBySub threadId parentMessageId sequenceNumber role messageType content semanticLayer searchVisibility responseTarget responseStatus responseOwner responseStartedAt responseCompletedAt responseError metadata createdAt updatedAt newsroomFeedKey";
|
|
1716
|
+
|
|
1717
|
+
/** The author changes the text of their own Message. The backend (editMessage) checks the stored author against the signed-in user and records when. */
|
|
1718
|
+
export async function editConversationMessage(messageId: string, body: string): Promise<ConversationMessage> {
|
|
1719
|
+
const content = String(body || "").trim();
|
|
1720
|
+
if (!content) throw new Error("A message cannot be empty.");
|
|
1721
|
+
const updated = await runGraphql<{ editMessage?: MessageRecord | null }>(
|
|
1722
|
+
`mutation EditMessage($id: ID!, $content: String!, $summary: String) { editMessage(id: $id, content: $content, summary: $summary) { ${MESSAGE_ACTION_SELECTION} } }`,
|
|
1723
|
+
{ id: messageId, content, summary: content.slice(0, 120) },
|
|
1724
|
+
);
|
|
1725
|
+
if (!updated.editMessage) throw new Error("The message was not saved.");
|
|
1726
|
+
return toConversationMessage(normalizeMessageRecord(updated.editMessage));
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* Deletes a Message softly: it stays (so its replies keep their place) marked deleted, with when and by whom. Nothing is removed.
|
|
1731
|
+
* An author deletes their own (deleteOwnMessage); an admin deleting someone else's moderates it (moderateMessage). The backend decides.
|
|
1732
|
+
*/
|
|
1733
|
+
export async function deleteConversationMessage(messageId: string, moderate = false): Promise<ConversationMessage> {
|
|
1734
|
+
const field = moderate ? "moderateMessage" : "deleteOwnMessage";
|
|
1735
|
+
const updated = await runGraphql<Record<string, MessageRecord | null | undefined>>(
|
|
1736
|
+
`mutation DeleteMessage($id: ID!) { ${field}(id: $id) { ${MESSAGE_ACTION_SELECTION} } }`,
|
|
1737
|
+
{ id: messageId },
|
|
1738
|
+
);
|
|
1739
|
+
const record = updated[field];
|
|
1740
|
+
if (!record) throw new Error("The message was not deleted.");
|
|
1741
|
+
return toConversationMessage(normalizeMessageRecord(record));
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/** One conversation thread by id (a linked address), or null when there is none or it was removed. */
|
|
1745
|
+
export async function loadConversationThread(threadId: string): Promise<MessageThreadRecord | null> {
|
|
1746
|
+
const response = await runGraphql<{ getMessageThread?: MessageThreadRecord | null }>(GET_MESSAGE_THREAD_QUERY, { id: threadId });
|
|
1747
|
+
const thread = response.getMessageThread ? normalizeThreadRecord(response.getMessageThread) : null;
|
|
1748
|
+
return thread && thread.status !== "deleted" ? thread : null;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/** Gives a conversation a new title. Nothing else about the thread changes. */
|
|
1752
|
+
export async function renameConversationThread(thread: MessageThreadRecord, title: string): Promise<MessageThreadRecord> {
|
|
1753
|
+
const next = String(title || "").trim();
|
|
1754
|
+
if (!next) throw new Error("A title cannot be empty.");
|
|
1755
|
+
// renameThread (the backend): only the person who started the conversation, or an admin.
|
|
1756
|
+
const updated = await runGraphql<{ renameThread?: MessageThreadRecord | null }>(
|
|
1757
|
+
`mutation RenameThread($id: ID!, $title: String!) { renameThread(id: $id, title: $title) { id threadKind status title summary primaryAnchorKind primaryAnchorId primaryAnchorLineageId primaryAnchorKey createdBySub createdByUserProfileId createdByLabel messageCount lastMessageId lastMessageAt contextDigest activeResponseMessageId responseLockOwner responseLockExpiresAt metadata createdAt updatedAt newsroomFeedKey } }`,
|
|
1758
|
+
{ id: thread.id, title: next.slice(0, 120) },
|
|
1759
|
+
);
|
|
1760
|
+
return normalizeThreadRecord(updated.renameThread ?? { ...thread, title: next.slice(0, 120) });
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/** The title and editorial stage of each Story, by lineage id. A Story that cannot be read has no entry. */
|
|
1764
|
+
export async function loadStoryHeaders(lineageIds: string[]): Promise<Map<string, { title: string; stage: string | null }>> {
|
|
1765
|
+
const headers = new Map<string, { title: string; stage: string | null }>();
|
|
1766
|
+
await Promise.all([...new Set(lineageIds)].map(async (lineageId) => {
|
|
1767
|
+
try {
|
|
1768
|
+
type Response = { listItemsByLineageAndVersion?: { items?: Array<{ versionNumber?: number | null; headline?: string | null; title?: string | null; stage?: string | null } | null> | null } | null };
|
|
1769
|
+
const response = await runGraphql<Response>(`query StoryHeader($lineageId: ID!) { listItemsByLineageAndVersion(lineageId: $lineageId, limit: 50) { items { versionNumber headline title stage } } }`, { lineageId });
|
|
1770
|
+
const items = (response.listItemsByLineageAndVersion?.items ?? []).filter(Boolean) as Array<{ versionNumber?: number | null; headline?: string | null; title?: string | null; stage?: string | null }>;
|
|
1771
|
+
const newest = items.sort((left, right) => (right.versionNumber ?? 0) - (left.versionNumber ?? 0))[0];
|
|
1772
|
+
const title = newest?.headline || newest?.title;
|
|
1773
|
+
if (title) headers.set(lineageId, { title, stage: newest?.stage ?? null });
|
|
1774
|
+
} catch { /* a Story that cannot be read keeps the title the thread was started with */ }
|
|
1775
|
+
}));
|
|
1776
|
+
return headers;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1630
1779
|
async function createCommentRelation(message: MessageRecord, anchor: ConversationAnchor): Promise<void> {
|
|
1631
1780
|
if (anchor.kind === "none") return;
|
|
1632
1781
|
const lineageId = anchor.lineageId.trim();
|
|
@@ -1843,10 +1992,17 @@ async function runGraphql<T extends Record<string, unknown>>(
|
|
|
1843
1992
|
const client = generateClient<Schema>() as unknown as {
|
|
1844
1993
|
graphql: (options: Record<string, unknown>) => Promise<{ data?: T | null; errors?: unknown[] | null }>;
|
|
1845
1994
|
};
|
|
1995
|
+
// The Amplify client rejects with the raw `{ data, errors }` response when the API returns errors; turn that into an
|
|
1996
|
+
// Error carrying the API's own words, so a refusal is said in words instead of as an unknown failure.
|
|
1846
1997
|
const response = await client.graphql({
|
|
1847
1998
|
query,
|
|
1848
1999
|
variables,
|
|
1849
2000
|
authMode: USER_POOL_AUTH_MODE,
|
|
2001
|
+
}).catch((failure: unknown) => {
|
|
2002
|
+
if (failure && typeof failure === "object" && Array.isArray((failure as { errors?: unknown }).errors)) {
|
|
2003
|
+
assertNoGraphQLErrors((failure as { errors: unknown[] }).errors);
|
|
2004
|
+
}
|
|
2005
|
+
throw failure;
|
|
1850
2006
|
});
|
|
1851
2007
|
assertNoGraphQLErrors(response.errors);
|
|
1852
2008
|
return (response.data ?? {}) as T;
|
|
@@ -2082,17 +2238,15 @@ export async function loadEditorReferenceReviewDetails(lineageId: string) {
|
|
|
2082
2238
|
configureAmplifyClient();
|
|
2083
2239
|
const testMock = getTestEditorNewsroomMock();
|
|
2084
2240
|
return loadReferenceReviewDetails(lineageId, {
|
|
2085
|
-
relations: async (
|
|
2241
|
+
relations: async () => {
|
|
2086
2242
|
if (testMock?.semanticRelations) return testMock.semanticRelations;
|
|
2087
2243
|
return loadSemanticRelationsByState({
|
|
2088
|
-
field:
|
|
2089
|
-
keyName:
|
|
2244
|
+
field: "listSemanticRelationsBySubjectState",
|
|
2245
|
+
keyName: "subjectStateKey",
|
|
2090
2246
|
keyValue: `reference#${lineageId}#current`,
|
|
2091
|
-
query:
|
|
2247
|
+
query: LIST_SEMANTIC_RELATIONS_BY_SUBJECT_STATE_QUERY,
|
|
2092
2248
|
});
|
|
2093
2249
|
},
|
|
2094
|
-
message: async (id) => testMock?.messages ? testMock.messages.find((message) => message.id === id) ?? null : getMessageRecordById(id),
|
|
2095
|
-
payloads: (id) => loadModelPayloadsForOwner("message", id, ["metadata"]),
|
|
2096
2250
|
});
|
|
2097
2251
|
}
|
|
2098
2252
|
|
|
@@ -513,10 +513,15 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
|
|
|
513
513
|
}
|
|
514
514
|
if (!editor) {
|
|
515
515
|
return (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
<div className="
|
|
516
|
+
// The same blocks, in the same order and heights, as the loaded editor (toolbar, then fields, tabs and the preview card),
|
|
517
|
+
// so nothing moves when the article arrives. The heights live in newsroom-organizer.css (data-skeleton-*).
|
|
518
|
+
<div aria-label="Loading article" aria-busy="true" className="space-y-4" data-newsroom-article-skeleton role="status">
|
|
519
|
+
<div className="rounded-md bg-muted" data-skeleton-toolbar />
|
|
520
|
+
<div className="grid gap-6">
|
|
521
|
+
<div className="rounded-md bg-muted" data-skeleton-fields />
|
|
522
|
+
<div className="h-10 w-72 max-w-full rounded-md bg-muted" data-skeleton-tabs />
|
|
523
|
+
<div className="rounded-xl bg-muted" data-skeleton-preview />
|
|
524
|
+
</div>
|
|
520
525
|
</div>
|
|
521
526
|
);
|
|
522
527
|
}
|
|
@@ -568,6 +573,7 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
|
|
|
568
573
|
<Badge data-newsroom-article-version variant="outline">{`Version ${editor.versionNumber}`}</Badge>
|
|
569
574
|
) : null}
|
|
570
575
|
<Badge
|
|
576
|
+
className="min-w-[8.5rem] justify-center"
|
|
571
577
|
data-newsroom-article-validity={validation.status}
|
|
572
578
|
variant={validation.status === "valid" ? "secondary" : invalid ? "outline" : "muted"}
|
|
573
579
|
>
|
|
@@ -54,7 +54,16 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
54
54
|
.organizer-board-body{position:relative;display:grid;grid-template-columns:minmax(0,1fr);flex:1;min-width:0;min-height:0;gap:16px}
|
|
55
55
|
.organizer-board-body[data-detail-open=true]{grid-template-columns:minmax(0,1fr) clamp(320px,34vw,1000px)}
|
|
56
56
|
.organizer-board-body>.kb-issue-collection{height:100%;min-height:0;overflow:auto}
|
|
57
|
-
|
|
57
|
+
/* The detail view is a solid surface from its first frame: the theme's panel token (opaque), with the card token as a fallback if
|
|
58
|
+
a theme has not defined it yet, and never any opacity animation, so the board never shows through it while it loads. */
|
|
59
|
+
.organizer-board-body>.kb-issue-detail{overflow:auto;min-height:0;position:relative;z-index:2;opacity:1;background-color:var(--kb-theme-panel,var(--card,Canvas));isolation:isolate}
|
|
60
|
+
/* One stable shape while an article loads and after: the preview card (and its skeleton) reserve the same height, so the page below it does not move. */
|
|
61
|
+
.newsroom-embedded-article :is([data-newsroom-article-preview],[data-skeleton-preview]){min-height:min(28rem,55dvh)}
|
|
62
|
+
/* The loading skeleton is the loaded editor's size block for block: the toolbar wraps onto more rows as the panel narrows (a container query,
|
|
63
|
+
because the panel's width, not the window's, decides), and the three fields stack when it is narrow. */
|
|
64
|
+
[data-skeleton-toolbar]{height:44px}[data-skeleton-fields]{height:76px}[data-skeleton-preview]{height:min(28rem,55dvh)}
|
|
65
|
+
@container (max-width:719px){[data-skeleton-toolbar]{height:80px}}
|
|
66
|
+
@container (max-width:399px){[data-skeleton-toolbar]{height:116px}[data-skeleton-fields]{height:208px}}
|
|
58
67
|
.organizer-board-body .kb-issue-detail a:not(.organizer-header-button){display:block;min-height:44px;text-decoration:underline;padding:12px 0}
|
|
59
68
|
.organizer-menu{height:min(85dvh,760px);max-height:85dvh;display:flex;flex-direction:column;padding:16px;border-radius:24px 24px 0 0;gap:20px}
|
|
60
69
|
.organizer-menu .organizer-destinations{overflow-y:auto}.organizer-menu .organizer-destinations section{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.organizer-menu .organizer-destinations h3{grid-column:1/-1;font-size:12px;text-transform:none;margin:0 0 4px}
|
|
@@ -401,11 +410,29 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
401
410
|
--feed-radius-lg: 10px;
|
|
402
411
|
--feed-radius-md: 8px;
|
|
403
412
|
--feed-radius-pill: 8px;
|
|
413
|
+
/* Verdict tones for the rating grid. Hue is never the only signal: the cell also carries a CircleCheck or
|
|
414
|
+
CircleX and the word Match or Miss. Named here, in the feed layer, because the component must not hold
|
|
415
|
+
literal palette values and because Tailwind's dark: variant (.dark *) never fires in this app. */
|
|
416
|
+
--feed-verdict-match-tint: #05966933;
|
|
417
|
+
--feed-verdict-match-ink: #064e3b;
|
|
418
|
+
--feed-verdict-match-fill: #059669;
|
|
419
|
+
--feed-verdict-miss-tint: #dc262633;
|
|
420
|
+
--feed-verdict-miss-ink: #7f1d1d;
|
|
421
|
+
--feed-verdict-miss-fill: #dc2626;
|
|
422
|
+
/* The quiet step for an unpredicted cell and a switched-off button. */
|
|
423
|
+
--feed-quiet: 0.6;
|
|
404
424
|
}
|
|
405
425
|
[data-kanbus-theme][data-color-mode="dark"] [data-feed-surface], [data-kanbus-theme][data-color-mode="dark"][data-feed-surface] {
|
|
406
426
|
--feed-card: color-mix(in oklab, var(--kb-theme-paper) 94%, var(--kb-theme-ink));
|
|
407
427
|
--feed-footer: color-mix(in oklab, var(--kb-theme-paper) 90%, var(--kb-theme-ink));
|
|
408
428
|
--feed-control: color-mix(in oklab, var(--kb-theme-paper) 80%, var(--kb-theme-ink));
|
|
429
|
+
/* Dark verdicts lift toward the tint rather than down into it, so the ink reads on a dark card. */
|
|
430
|
+
--feed-verdict-match-tint: #34d39933;
|
|
431
|
+
--feed-verdict-match-ink: #d1fae5;
|
|
432
|
+
--feed-verdict-match-fill: #34d399;
|
|
433
|
+
--feed-verdict-miss-tint: #f8717133;
|
|
434
|
+
--feed-verdict-miss-ink: #fee2e2;
|
|
435
|
+
--feed-verdict-miss-fill: #f87171;
|
|
409
436
|
}
|
|
410
437
|
|
|
411
438
|
/* Borderless detail pane, Add panel and mobile sheet ([data-feed-flat]): no border, rule or box anywhere inside. Regions are told apart
|
|
@@ -426,6 +453,12 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
426
453
|
}
|
|
427
454
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-cyclotron-choice] { border-radius: var(--feed-radius-pill) !important; }
|
|
428
455
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-review-queue-bar] { background: var(--feed-footer) !important; }
|
|
456
|
+
/* The decision control keeps one corner across its segments and its reason field, and its predicted segment
|
|
457
|
+
keeps its tones apart: the flat rules above give every unpressed button the same control fill, which would
|
|
458
|
+
leave the predicted side and an untouched side looking identical. */
|
|
459
|
+
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-decision-control] :is(button, textarea) { border-radius: var(--feed-radius-md) !important; }
|
|
460
|
+
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-decision-control] [data-review-choice][data-state="idle"] { background: color-mix(in oklab, var(--feed-control) 45%, transparent) !important; }
|
|
461
|
+
|
|
429
462
|
/* The rating grid keeps its own rounded rectangles in the borderless panes (not the pill shape the flat rules give other buttons). */
|
|
430
463
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-rating-grid] :is(button, [data-cyclotron-choice]) { border-radius: var(--feed-radius-md) !important; }
|
|
431
464
|
|