@anthusai/papyrus 5.43.0-next.41 → 5.43.0-next.43

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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The Messages page: which MessageThreads are conversations, which section each belongs to, and how several newest-first streams
3
+ * (one per thread kind) merge into one page of conversations ordered by last activity. Pure helpers, shared by the browser and the tests.
4
+ */
5
+ import { GENERAL_CONVERSATION_KIND, REFERENCE_CONVERSATION_KIND, STORY_CONVERSATION_KIND } from "./conversation";
6
+
7
+ export type ConversationSection = "all" | "references" | "stories" | "chats" | "other";
8
+ export const CONVERSATION_SECTIONS: ReadonlyArray<{ key: ConversationSection; label: string }> = [
9
+ { key: "all", label: "All" },
10
+ { key: "references", label: "References" },
11
+ { key: "stories", label: "Stories" },
12
+ { key: "chats", label: "Chats" },
13
+ { key: "other", label: "Other" },
14
+ ];
15
+
16
+ export const CHAT_THREAD_KIND = "console";
17
+ export const FORUM_THREAD_KINDS = ["edition_forum", "section_forum"] as const;
18
+
19
+ /** The thread kinds that are conversations, by section. Machine records (provenance, curation, ingestion rationale) are in none of them. */
20
+ const KINDS_BY_SECTION: Record<Exclude<ConversationSection, "all">, ReadonlyArray<string>> = {
21
+ references: [REFERENCE_CONVERSATION_KIND],
22
+ stories: [STORY_CONVERSATION_KIND],
23
+ chats: [CHAT_THREAD_KIND],
24
+ other: [GENERAL_CONVERSATION_KIND, ...FORUM_THREAD_KINDS],
25
+ };
26
+
27
+ export function threadKindsForSection(section: ConversationSection): string[] {
28
+ if (section === "all") return Object.values(KINDS_BY_SECTION).flat();
29
+ return [...KINDS_BY_SECTION[section]];
30
+ }
31
+
32
+ /** The section a thread kind is listed under, or null for a kind that is not a conversation. */
33
+ export function sectionOfThreadKind(kind: string): Exclude<ConversationSection, "all"> | null {
34
+ for (const [section, kinds] of Object.entries(KINDS_BY_SECTION)) if (kinds.includes(kind)) return section as Exclude<ConversationSection, "all">;
35
+ return null;
36
+ }
37
+
38
+ export const SECTION_EMPTY_COPY: Record<ConversationSection, string> = {
39
+ all: "No conversations yet. Write a message above, or on a Reference, to start one.",
40
+ references: "No Reference has a message yet. Open a Reference and write down what it made you think.",
41
+ stories: "No Story has a message yet.",
42
+ chats: "No chats yet.",
43
+ other: "No other conversations yet. Write a message above to start one.",
44
+ };
45
+
46
+ export type PagedThreads<Thread> = { items: Thread[]; nextToken: string | null };
47
+
48
+ type Streamable = { id: string; updatedAt: string };
49
+
50
+ /**
51
+ * One page at a time out of several newest-first streams, merged by `updatedAt` (the order of every thread index). A stream is only
52
+ * asked for another page when its buffered threads have all been shown, so the order across streams is exact, never approximate.
53
+ */
54
+ export function createMergedThreadStream<Thread extends Streamable>(
55
+ kinds: ReadonlyArray<string>,
56
+ loadPage: (kind: string, nextToken: string | null) => Promise<PagedThreads<Thread>>,
57
+ ) {
58
+ const lanes = kinds.map((kind) => ({ kind, buffer: [] as Thread[], nextToken: null as string | null, started: false, done: false }));
59
+ const seen = new Set<string>();
60
+
61
+ async function fill() {
62
+ await Promise.all(lanes.filter((lane) => !lane.done && lane.buffer.length === 0).map(async (lane) => {
63
+ const page = await loadPage(lane.kind, lane.started ? lane.nextToken : null);
64
+ lane.started = true;
65
+ lane.nextToken = page.nextToken;
66
+ lane.done = !page.nextToken;
67
+ lane.buffer = [...page.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
68
+ }));
69
+ }
70
+
71
+ return {
72
+ /** The next `count` conversations, newest first, and whether more remain. */
73
+ async next(count: number): Promise<{ items: Thread[]; hasMore: boolean }> {
74
+ const items: Thread[] = [];
75
+ while (items.length < count) {
76
+ await fill();
77
+ const live = lanes.filter((lane) => lane.buffer.length > 0);
78
+ if (!live.length) break;
79
+ // A lane that is not finished and has an empty buffer after a fill returned an empty page with more behind it: ask again.
80
+ if (lanes.some((lane) => !lane.done && lane.buffer.length === 0)) continue;
81
+ const best = live.reduce((winner, lane) => (lane.buffer[0].updatedAt.localeCompare(winner.buffer[0].updatedAt) > 0 ? lane : winner));
82
+ const thread = best.buffer.shift()!;
83
+ if (seen.has(thread.id)) continue;
84
+ seen.add(thread.id);
85
+ items.push(thread);
86
+ }
87
+ return { items, hasMore: lanes.some((lane) => lane.buffer.length > 0 || !lane.done) };
88
+ },
89
+ };
90
+ }
91
+
92
+ /** The newest `limit` messages, oldest first, and how many are held back behind "Show all". */
93
+ export function latestMessages<Message>(messages: ReadonlyArray<Message>, limit: number, showAll: boolean): { shown: Message[]; hidden: number } {
94
+ if (showAll || messages.length <= limit) return { shown: [...messages], hidden: 0 };
95
+ return { shown: messages.slice(messages.length - limit), hidden: messages.length - limit };
96
+ }
package/package.json CHANGED
@@ -83,6 +83,11 @@
83
83
  "./components/conversation-context": "./components/conversation-context.tsx",
84
84
  "./components/conversation-provider": "./components/conversation-provider.tsx",
85
85
  "./components/conversation-thread": "./components/conversation-thread.tsx",
86
+ "./components/conversations-memory-source": "./components/conversations-memory-source.ts",
87
+ "./components/conversations-network-source": "./components/conversations-network-source.ts",
88
+ "./components/conversations-page": "./components/conversations-page.tsx",
89
+ "./components/conversations-source": "./components/conversations-source.ts",
90
+ "./components/conversations-view": "./components/conversations-view.tsx",
86
91
  "./components/curation-delegation-client": "./components/curation-delegation-client.ts",
87
92
  "./components/decision-control": "./components/decision-control.tsx",
88
93
  "./components/dev-sandbox-editor-sign-in": "./components/dev-sandbox-editor-sign-in.tsx",
@@ -234,6 +239,7 @@
234
239
  "./lib/content-repository": "./lib/content-repository.ts",
235
240
  "./lib/content-source-context": "./lib/content-source-context.ts",
236
241
  "./lib/content-types": "./lib/content-types.ts",
242
+ "./lib/conversation-feed": "./lib/conversation-feed.ts",
237
243
  "./lib/conversation": "./lib/conversation.ts",
238
244
  "./lib/curation-client": "./lib/curation-client.ts",
239
245
  "./lib/curation-delegation-helpers": "./lib/curation-delegation-helpers.ts",
@@ -510,7 +516,7 @@
510
516
  "optional": true
511
517
  }
512
518
  },
513
- "version": "5.43.0-next.41",
519
+ "version": "5.43.0-next.43",
514
520
  "dependencies": {
515
521
  "@aws-amplify/adapter-nextjs": "^1.7.3",
516
522
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.43.0-next.41",
2
+ "version": "5.43.0-next.43",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",