@anthusai/papyrus 1.0.0-next.64 → 1.0.0-next.66
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/schema.js +0 -2
- package/amplify/data/schema.ts +0 -2
- package/amplify/functions/email-submission-processor/handler.py +3 -1
- package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
- package/amplify/functions/shared/email-submission.ts +109 -1
- package/app/tailwind.css +0 -6
- package/components/topic-steering-workspace.tsx +0 -2
- package/package.json +1 -8
- package/routes.manifest.json +1 -1
- package/components/ai-elements/message.tsx +0 -360
package/amplify/data/schema.js
CHANGED
|
@@ -815,7 +815,6 @@ const baseDefinitions = {
|
|
|
815
815
|
])
|
|
816
816
|
.authorization((allow) => [
|
|
817
817
|
allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
|
|
818
|
-
allow.authenticated("identityPool").to(authoringOperations),
|
|
819
818
|
]),
|
|
820
819
|
AssignmentEvent: modelWithoutSubscriptions({
|
|
821
820
|
id: a.id().required(),
|
|
@@ -1395,7 +1394,6 @@ const baseDefinitions = {
|
|
|
1395
1394
|
])
|
|
1396
1395
|
.authorization((allow) => [
|
|
1397
1396
|
allow.groups(categoryWriteGroups).to(["read", "create", "update"]),
|
|
1398
|
-
allow.authenticated("identityPool").to(authoringOperations),
|
|
1399
1397
|
]),
|
|
1400
1398
|
ModelAttachment: modelWithoutSubscriptions({
|
|
1401
1399
|
id: a.id().required(),
|
package/amplify/data/schema.ts
CHANGED
|
@@ -886,7 +886,6 @@ const baseDefinitions = {
|
|
|
886
886
|
])
|
|
887
887
|
.authorization((allow) => [
|
|
888
888
|
allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
|
|
889
|
-
allow.authenticated("identityPool").to(authoringOperations),
|
|
890
889
|
]),
|
|
891
890
|
|
|
892
891
|
AssignmentEvent: modelWithoutSubscriptions({
|
|
@@ -1486,7 +1485,6 @@ const baseDefinitions = {
|
|
|
1486
1485
|
])
|
|
1487
1486
|
.authorization((allow) => [
|
|
1488
1487
|
allow.groups(categoryWriteGroups).to(["read", "create", "update"]),
|
|
1489
|
-
allow.authenticated("identityPool").to(authoringOperations),
|
|
1490
1488
|
]),
|
|
1491
1489
|
|
|
1492
1490
|
ModelAttachment: modelWithoutSubscriptions({
|
|
@@ -30,8 +30,10 @@ def handler(event: dict[str, Any], _context: Any = None) -> dict[str, Any]:
|
|
|
30
30
|
event.get("corpusKey")
|
|
31
31
|
or event.get("corpus_key")
|
|
32
32
|
or os.environ.get("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY")
|
|
33
|
-
or "
|
|
33
|
+
or ""
|
|
34
34
|
).strip()
|
|
35
|
+
if not corpus_key:
|
|
36
|
+
raise ValueError("corpusKey or PAPYRUS_INBOUND_EMAIL_CORPUS_KEY is required.")
|
|
35
37
|
|
|
36
38
|
from papyrus_content.graphql_authoring import PapyrusGraphQLAuthoringClient
|
|
37
39
|
from papyrus_newsroom.email_submissions import process_email_submission_message
|
|
@@ -2,6 +2,13 @@ import { InvokeCommand, LambdaClient } from "@aws-sdk/client-lambda";
|
|
|
2
2
|
import { GetObjectCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3";
|
|
3
3
|
import {
|
|
4
4
|
buildEmailSubmissionMessageInput,
|
|
5
|
+
isInboundSenderAuthenticated,
|
|
6
|
+
readInboundSenderAuthenticationFromRawMime,
|
|
7
|
+
readInboundScanVerdictsFromSesReceipt,
|
|
8
|
+
REJECTION_KIND_UNAUTHENTICATED_SENDER,
|
|
9
|
+
UNAUTHENTICATED_SENDER_RESPONSE_ERROR,
|
|
10
|
+
UNVERIFIED_SENDER_AUTHENTICATION,
|
|
11
|
+
type InboundSenderAuthentication,
|
|
5
12
|
classifyInboundEmailIntake,
|
|
6
13
|
countInboundAttachments,
|
|
7
14
|
extractDirectCitations,
|
|
@@ -49,6 +56,7 @@ type InboundPayload = {
|
|
|
49
56
|
attachmentCount: number;
|
|
50
57
|
userComposedText: string;
|
|
51
58
|
parentSubmissionMessageId: string | null;
|
|
59
|
+
senderAuthentication: InboundSenderAuthentication;
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
type ExistingMessage = {
|
|
@@ -123,7 +131,8 @@ async function processInboundSubmission(inbound: InboundPayload): Promise<Record
|
|
|
123
131
|
const resolvedMessageId = messageId ?? `message-email-submission-${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
|
|
124
132
|
const dataClient = await getLambdaDataClient();
|
|
125
133
|
const profileId = await lookupRegisteredUserProfileId(dataClient, inbound.senderEmail);
|
|
126
|
-
const
|
|
134
|
+
const senderAuthenticated = isInboundSenderAuthenticated(inbound.senderAuthentication, inbound.senderEmail);
|
|
135
|
+
const authorized = Boolean(profileId) && senderAuthenticated;
|
|
127
136
|
const citations = authorized
|
|
128
137
|
? (inbound.citations.length > 0
|
|
129
138
|
? inbound.citations
|
|
@@ -143,7 +152,9 @@ async function processInboundSubmission(inbound: InboundPayload): Promise<Record
|
|
|
143
152
|
|
|
144
153
|
let status = authorized ? "received" : "rejected";
|
|
145
154
|
let responseStatus = authorized ? "PENDING" : "REJECTED";
|
|
146
|
-
let responseError: string | null = authorized
|
|
155
|
+
let responseError: string | null = authorized
|
|
156
|
+
? null
|
|
157
|
+
: (senderAuthenticated ? UNREGISTERED_SENDER_RESPONSE_ERROR : UNAUTHENTICATED_SENDER_RESPONSE_ERROR);
|
|
147
158
|
|
|
148
159
|
if (authorized && looksLikeResearchAssignmentRequest(inbound.bodyText, citations.length)) {
|
|
149
160
|
status = "rejected";
|
|
@@ -173,7 +184,8 @@ async function processInboundSubmission(inbound: InboundPayload): Promise<Record
|
|
|
173
184
|
s3Bucket: inbound.s3Bucket,
|
|
174
185
|
s3Key: inbound.s3Key,
|
|
175
186
|
authorized,
|
|
176
|
-
|
|
187
|
+
rejectionKind: senderAuthenticated ? undefined : REJECTION_KIND_UNAUTHENTICATED_SENDER,
|
|
188
|
+
authorUserProfileId: authorized ? profileId : null,
|
|
177
189
|
authorLabel: inbound.senderEmail || "unknown-sender",
|
|
178
190
|
citations,
|
|
179
191
|
status,
|
|
@@ -205,7 +217,7 @@ async function processInboundSubmission(inbound: InboundPayload): Promise<Record
|
|
|
205
217
|
|
|
206
218
|
if (authorized && responseStatus === "PENDING" && PROCESSOR_FUNCTION_NAME) {
|
|
207
219
|
await invokeEmailProcessor(resolvedMessageId);
|
|
208
|
-
} else if (responseStatus === "REJECTED" && PROCESSOR_FUNCTION_NAME) {
|
|
220
|
+
} else if (responseStatus === "REJECTED" && senderAuthenticated && PROCESSOR_FUNCTION_NAME) {
|
|
209
221
|
await invokeEmailProcessor(resolvedMessageId, { sendFeedbackOnly: true });
|
|
210
222
|
}
|
|
211
223
|
|
|
@@ -390,6 +402,7 @@ async function resolveFromSesMail(
|
|
|
390
402
|
senderEmail: loaded.senderEmail || senderEmail,
|
|
391
403
|
recipients: loaded.recipients.length > 0 ? loaded.recipients : recipients,
|
|
392
404
|
subject: loaded.subject !== "(no subject)" ? loaded.subject : subject,
|
|
405
|
+
senderAuthentication: mergeScanVerdicts(loaded.senderAuthentication, readInboundScanVerdictsFromSesReceipt(receipt)),
|
|
393
406
|
};
|
|
394
407
|
}
|
|
395
408
|
|
|
@@ -407,6 +420,7 @@ async function resolveFromSesMail(
|
|
|
407
420
|
attachmentCount: 0,
|
|
408
421
|
userComposedText: "",
|
|
409
422
|
parentSubmissionMessageId: null,
|
|
423
|
+
senderAuthentication: { ...UNVERIFIED_SENDER_AUTHENTICATION, ...readInboundScanVerdictsFromSesReceipt(receipt) },
|
|
410
424
|
});
|
|
411
425
|
}
|
|
412
426
|
|
|
@@ -433,6 +447,7 @@ async function loadInboundFromS3Object(
|
|
|
433
447
|
attachmentCount: countInboundAttachments(rawBytes),
|
|
434
448
|
userComposedText: extractUserComposedReplyText(intake.bodyText),
|
|
435
449
|
parentSubmissionMessageId: null,
|
|
450
|
+
senderAuthentication: readInboundSenderAuthenticationFromRawMime(rawBytes),
|
|
436
451
|
});
|
|
437
452
|
}
|
|
438
453
|
|
|
@@ -446,6 +461,19 @@ function enrichInboundPayload(payload: InboundPayload): InboundPayload {
|
|
|
446
461
|
};
|
|
447
462
|
}
|
|
448
463
|
|
|
464
|
+
function mergeScanVerdicts(
|
|
465
|
+
authentication: InboundSenderAuthentication,
|
|
466
|
+
receiptVerdicts: Pick<InboundSenderAuthentication, "spamVerdict" | "virusVerdict">,
|
|
467
|
+
): InboundSenderAuthentication {
|
|
468
|
+
const worst = (fromMime: string, fromReceipt: string) =>
|
|
469
|
+
fromMime === "fail" || fromReceipt === "fail" ? "fail" : fromMime !== "unknown" ? fromMime : fromReceipt;
|
|
470
|
+
return {
|
|
471
|
+
...authentication,
|
|
472
|
+
spamVerdict: worst(authentication.spamVerdict, receiptVerdicts.spamVerdict),
|
|
473
|
+
virusVerdict: worst(authentication.virusVerdict, receiptVerdicts.virusVerdict),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
449
477
|
function sesMessageIdFromObjectKey(key: string): string | null {
|
|
450
478
|
const basename = key.split("/").pop() ?? "";
|
|
451
479
|
const match = basename.match(/^[A-Za-z0-9_-]+$/);
|
|
@@ -11,6 +11,113 @@ export const REJECTION_KIND_UNREGISTERED_SENDER = "unregistered_sender";
|
|
|
11
11
|
export const UNREGISTERED_SENDER_RESPONSE_ERROR =
|
|
12
12
|
"This submission was not accepted because only registered Papyrus users may send reference submissions by email.";
|
|
13
13
|
|
|
14
|
+
export const UNAUTHENTICATED_SENDER_RESPONSE_ERROR =
|
|
15
|
+
"This submission was not accepted because the sender could not be authenticated (SPF or DKIM must pass).";
|
|
16
|
+
export const REJECTION_KIND_UNAUTHENTICATED_SENDER = "unauthenticated_sender";
|
|
17
|
+
|
|
18
|
+
export type InboundSenderAuthentication = {
|
|
19
|
+
spfPassDomains: string[];
|
|
20
|
+
dkimPassDomains: string[];
|
|
21
|
+
spamVerdict: string;
|
|
22
|
+
virusVerdict: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const TRUSTED_AUTHENTICATION_RESULTS_SERVER_ID = "amazonses.com";
|
|
26
|
+
|
|
27
|
+
export const UNVERIFIED_SENDER_AUTHENTICATION: InboundSenderAuthentication = {
|
|
28
|
+
spfPassDomains: [],
|
|
29
|
+
dkimPassDomains: [],
|
|
30
|
+
spamVerdict: "unknown",
|
|
31
|
+
virusVerdict: "unknown",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function domainOfAddress(address: string): string {
|
|
35
|
+
return normalizeEmailAddress(address).split("@")[1] ?? "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function domainsAreAligned(authenticatedDomain: string, fromDomain: string): boolean {
|
|
39
|
+
if (!authenticatedDomain || !fromDomain) return false;
|
|
40
|
+
return authenticatedDomain === fromDomain
|
|
41
|
+
|| fromDomain.endsWith(`.${authenticatedDomain}`)
|
|
42
|
+
|| authenticatedDomain.endsWith(`.${fromDomain}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isInboundSenderAuthenticated(
|
|
46
|
+
authentication: InboundSenderAuthentication,
|
|
47
|
+
fromAddress: string,
|
|
48
|
+
): boolean {
|
|
49
|
+
if (authentication.spamVerdict === "fail" || authentication.virusVerdict === "fail") return false;
|
|
50
|
+
const fromDomain = domainOfAddress(fromAddress);
|
|
51
|
+
return [...authentication.spfPassDomains, ...authentication.dkimPassDomains]
|
|
52
|
+
.some((domain) => domainsAreAligned(domain, fromDomain));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readInboundScanVerdictsFromSesReceipt(
|
|
56
|
+
receipt: Record<string, unknown> | undefined,
|
|
57
|
+
): Pick<InboundSenderAuthentication, "spamVerdict" | "virusVerdict"> {
|
|
58
|
+
const verdictStatus = (key: string): string => {
|
|
59
|
+
const verdict = receipt?.[key];
|
|
60
|
+
const status = verdict && typeof verdict === "object" ? (verdict as { status?: unknown }).status : null;
|
|
61
|
+
return typeof status === "string" ? status.trim().toLowerCase() : "unknown";
|
|
62
|
+
};
|
|
63
|
+
return { spamVerdict: verdictStatus("spamVerdict"), virusVerdict: verdictStatus("virusVerdict") };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readHeaderBlock(rawBytes: Uint8Array): string[] {
|
|
67
|
+
const raw = Buffer.from(rawBytes).toString("utf8");
|
|
68
|
+
const headerBlockEnd = raw.search(/\r?\n\r?\n/);
|
|
69
|
+
const headerBlock = headerBlockEnd === -1 ? raw : raw.slice(0, headerBlockEnd);
|
|
70
|
+
return headerBlock.replace(/\r?\n[ \t]+/g, " ").split(/\r?\n/);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function firstHeaderValue(headerLines: string[], name: string): string | null {
|
|
74
|
+
const prefix = new RegExp(`^${name}:\\s*`, "i");
|
|
75
|
+
const line = headerLines.find((entry) => prefix.test(entry));
|
|
76
|
+
return line ? line.replace(prefix, "") : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readInboundSenderAuthenticationFromRawMime(rawBytes: Uint8Array): InboundSenderAuthentication {
|
|
80
|
+
const headerLines = readHeaderBlock(rawBytes);
|
|
81
|
+
const verdictHeader = (name: string): string =>
|
|
82
|
+
(firstHeaderValue(headerLines, name) ?? "unknown").trim().toLowerCase();
|
|
83
|
+
const scanVerdicts = {
|
|
84
|
+
spamVerdict: verdictHeader("X-SES-Spam-Verdict"),
|
|
85
|
+
virusVerdict: verdictHeader("X-SES-Virus-Verdict"),
|
|
86
|
+
};
|
|
87
|
+
const topmostResults = firstHeaderValue(headerLines, "Authentication-Results");
|
|
88
|
+
if (!topmostResults) return { ...UNVERIFIED_SENDER_AUTHENTICATION, ...scanVerdicts };
|
|
89
|
+
const [serverId, ...parts] = topmostResults.replace(/\([^)]*\)/g, "").split(";").map((part) => part.trim());
|
|
90
|
+
if (serverId.toLowerCase() !== TRUSTED_AUTHENTICATION_RESULTS_SERVER_ID) {
|
|
91
|
+
return { ...UNVERIFIED_SENDER_AUTHENTICATION, ...scanVerdicts };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const spfPassDomains: string[] = [];
|
|
95
|
+
const dkimPassDomains: string[] = [];
|
|
96
|
+
let spfPassed = false;
|
|
97
|
+
let envelopeFromDomain = "";
|
|
98
|
+
let heloDomain = "";
|
|
99
|
+
for (const part of parts) {
|
|
100
|
+
const result = /^(spf|dkim)\s*=\s*([a-z]+)/i.exec(part);
|
|
101
|
+
if (result) {
|
|
102
|
+
const passed = result[2].toLowerCase() === "pass";
|
|
103
|
+
if (result[1].toLowerCase() === "spf") spfPassed = spfPassed || passed;
|
|
104
|
+
else if (passed) {
|
|
105
|
+
const signingDomain = /header\.(?:i|d)\s*=\s*@?([^\s;]+)/i.exec(part)?.[1];
|
|
106
|
+
if (signingDomain) dkimPassDomains.push(signingDomain.toLowerCase().replace(/^.*@/, ""));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const envelopeFrom = /^(?:envelope-from|smtp\.mailfrom)\s*=\s*(\S+)/i.exec(part)?.[1];
|
|
110
|
+
if (envelopeFrom) envelopeFromDomain = domainOfAddress(envelopeFrom.replace(/^<|>$/g, ""));
|
|
111
|
+
const helo = /^(?:helo|smtp\.helo)\s*=\s*(\S+)/i.exec(part)?.[1];
|
|
112
|
+
if (helo) heloDomain = helo.toLowerCase();
|
|
113
|
+
}
|
|
114
|
+
if (spfPassed) {
|
|
115
|
+
const spfDomain = envelopeFromDomain || heloDomain;
|
|
116
|
+
if (spfDomain) spfPassDomains.push(spfDomain);
|
|
117
|
+
}
|
|
118
|
+
return { spfPassDomains, dkimPassDomains, ...scanVerdicts };
|
|
119
|
+
}
|
|
120
|
+
|
|
14
121
|
const URL_PATTERN = /https?:\/\/[^\s<>"')\]]+/gi;
|
|
15
122
|
const DOI_PATTERN = /\b10\.\d{4,9}\/[-._;()/:A-Z0-9]+/gi;
|
|
16
123
|
const RESEARCH_ASSIGNMENT_PHRASES = [
|
|
@@ -337,6 +444,7 @@ export function buildEmailSubmissionMessageInput(input: {
|
|
|
337
444
|
s3Bucket: string | null;
|
|
338
445
|
s3Key: string | null;
|
|
339
446
|
authorized: boolean;
|
|
447
|
+
rejectionKind?: string;
|
|
340
448
|
authorUserProfileId: string | null;
|
|
341
449
|
authorLabel: string;
|
|
342
450
|
citations: Array<Record<string, unknown>>;
|
|
@@ -378,7 +486,7 @@ export function buildEmailSubmissionMessageInput(input: {
|
|
|
378
486
|
...(input.authorized
|
|
379
487
|
? {}
|
|
380
488
|
: {
|
|
381
|
-
rejectionKind: REJECTION_KIND_UNREGISTERED_SENDER,
|
|
489
|
+
rejectionKind: input.rejectionKind ?? REJECTION_KIND_UNREGISTERED_SENDER,
|
|
382
490
|
}),
|
|
383
491
|
directCitationCount: input.citations.length,
|
|
384
492
|
directCitations: input.citations,
|
package/app/tailwind.css
CHANGED
|
@@ -3,14 +3,8 @@
|
|
|
3
3
|
@source "../components/**/*.{ts,tsx}";
|
|
4
4
|
@source "../app/**/*.{ts,tsx}";
|
|
5
5
|
@source "../lib/**/*.{ts,tsx}";
|
|
6
|
-
@source "../../../streamdown/dist/*.js";
|
|
7
|
-
@source "../../../@streamdown/code/dist/*.js";
|
|
8
|
-
@source "../../../@streamdown/cjk/dist/*.js";
|
|
9
|
-
@source "../../../@streamdown/math/dist/*.js";
|
|
10
|
-
@source "../../../@streamdown/mermaid/dist/*.js";
|
|
11
6
|
@import "tw-animate-css";
|
|
12
7
|
@import "shadcn/tailwind.css";
|
|
13
|
-
@import "streamdown/styles.css";
|
|
14
8
|
|
|
15
9
|
@custom-variant dark (&:is(.dark *));
|
|
16
10
|
|
|
@@ -263,7 +263,6 @@ type TopicProposalEditState = {
|
|
|
263
263
|
};
|
|
264
264
|
type AnalysisCommandPlanEntry = {
|
|
265
265
|
label: string;
|
|
266
|
-
cwd: string;
|
|
267
266
|
executable: string;
|
|
268
267
|
args: string[];
|
|
269
268
|
metadata?: Record<string, unknown>;
|
|
@@ -13607,7 +13606,6 @@ function buildAnalysisDestructivePreview({
|
|
|
13607
13606
|
function analysisCommand(label: string, args: string[], mode: AnalysisReindexMode): AnalysisCommandPlanEntry {
|
|
13608
13607
|
return {
|
|
13609
13608
|
label,
|
|
13610
|
-
cwd: "/Users/ryan/Projects/Biblicus",
|
|
13611
13609
|
executable: "uv",
|
|
13612
13610
|
args: ["run", "--extra", "topic-modeling", "biblicus", ...args],
|
|
13613
13611
|
metadata: { mode },
|
package/package.json
CHANGED
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
"default": "./infra/index.js"
|
|
66
66
|
},
|
|
67
67
|
"./components/ai-elements/conversation": "./components/ai-elements/conversation.tsx",
|
|
68
|
-
"./components/ai-elements/message": "./components/ai-elements/message.tsx",
|
|
69
68
|
"./components/ai-elements/model-selector": "./components/ai-elements/model-selector.tsx",
|
|
70
69
|
"./components/ai-elements/prompt-input": "./components/ai-elements/prompt-input.tsx",
|
|
71
70
|
"./components/ai-elements/shimmer": "./components/ai-elements/shimmer.tsx",
|
|
@@ -340,7 +339,7 @@
|
|
|
340
339
|
"optional": true
|
|
341
340
|
}
|
|
342
341
|
},
|
|
343
|
-
"version": "1.0.0-next.
|
|
342
|
+
"version": "1.0.0-next.66",
|
|
344
343
|
"dependencies": {
|
|
345
344
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
346
345
|
"@aws-crypto/sha256-js": "5.2.0",
|
|
@@ -357,10 +356,6 @@
|
|
|
357
356
|
"@radix-ui/colors": "3.0.0",
|
|
358
357
|
"@smithy/protocol-http": "5.4.1",
|
|
359
358
|
"@smithy/signature-v4": "5.7.4",
|
|
360
|
-
"@streamdown/cjk": "^1.0.3",
|
|
361
|
-
"@streamdown/code": "^1.1.1",
|
|
362
|
-
"@streamdown/math": "^1.0.2",
|
|
363
|
-
"@streamdown/mermaid": "^1.0.2",
|
|
364
359
|
"@tailwindcss/postcss": "^4.3.0",
|
|
365
360
|
"ai": "^6.0.191",
|
|
366
361
|
"anthus-vultus": "github:AnthusAI/Vultus#af98d57",
|
|
@@ -373,12 +368,10 @@
|
|
|
373
368
|
"esbuild": "^0.28.2",
|
|
374
369
|
"gray-matter": "^4.0.3",
|
|
375
370
|
"gsap": "^3.15.0",
|
|
376
|
-
"katex": "^0.17.0",
|
|
377
371
|
"lucide-react": "^1.16.0",
|
|
378
372
|
"motion": "^12.40.0",
|
|
379
373
|
"nanoid": "^5.1.11",
|
|
380
374
|
"shadcn": "^4.8.0",
|
|
381
|
-
"streamdown": "^2.5.0",
|
|
382
375
|
"tailwind-merge": "^3.6.0",
|
|
383
376
|
"tailwindcss": "^4.3.0",
|
|
384
377
|
"tsx": "^4.23.15",
|
package/routes.manifest.json
CHANGED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Button } from "../ui/button";
|
|
4
|
-
import {
|
|
5
|
-
ButtonGroup,
|
|
6
|
-
ButtonGroupText,
|
|
7
|
-
} from "../ui/button-group";
|
|
8
|
-
import {
|
|
9
|
-
Tooltip,
|
|
10
|
-
TooltipContent,
|
|
11
|
-
TooltipProvider,
|
|
12
|
-
TooltipTrigger,
|
|
13
|
-
} from "../ui/tooltip";
|
|
14
|
-
import { cn } from "../../lib/utils";
|
|
15
|
-
import { cjk } from "@streamdown/cjk";
|
|
16
|
-
import { code } from "@streamdown/code";
|
|
17
|
-
import { math } from "@streamdown/math";
|
|
18
|
-
import { mermaid } from "@streamdown/mermaid";
|
|
19
|
-
import type { UIMessage } from "ai";
|
|
20
|
-
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
21
|
-
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
|
22
|
-
import {
|
|
23
|
-
createContext,
|
|
24
|
-
memo,
|
|
25
|
-
useCallback,
|
|
26
|
-
useContext,
|
|
27
|
-
useEffect,
|
|
28
|
-
useMemo,
|
|
29
|
-
useState,
|
|
30
|
-
} from "react";
|
|
31
|
-
import { Streamdown } from "streamdown";
|
|
32
|
-
|
|
33
|
-
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
|
34
|
-
from: UIMessage["role"];
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const Message = ({ className, from, ...props }: MessageProps) => (
|
|
38
|
-
<div
|
|
39
|
-
className={cn(
|
|
40
|
-
"group flex w-full max-w-[95%] flex-col gap-2",
|
|
41
|
-
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
|
|
42
|
-
className
|
|
43
|
-
)}
|
|
44
|
-
{...props}
|
|
45
|
-
/>
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
|
49
|
-
|
|
50
|
-
export const MessageContent = ({
|
|
51
|
-
children,
|
|
52
|
-
className,
|
|
53
|
-
...props
|
|
54
|
-
}: MessageContentProps) => (
|
|
55
|
-
<div
|
|
56
|
-
className={cn(
|
|
57
|
-
"is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
|
|
58
|
-
"group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground",
|
|
59
|
-
"group-[.is-assistant]:text-foreground",
|
|
60
|
-
className
|
|
61
|
-
)}
|
|
62
|
-
{...props}
|
|
63
|
-
>
|
|
64
|
-
{children}
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
export type MessageActionsProps = ComponentProps<"div">;
|
|
69
|
-
|
|
70
|
-
export const MessageActions = ({
|
|
71
|
-
className,
|
|
72
|
-
children,
|
|
73
|
-
...props
|
|
74
|
-
}: MessageActionsProps) => (
|
|
75
|
-
<div className={cn("flex items-center gap-1", className)} {...props}>
|
|
76
|
-
{children}
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
export type MessageActionProps = ComponentProps<typeof Button> & {
|
|
81
|
-
tooltip?: string;
|
|
82
|
-
label?: string;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export const MessageAction = ({
|
|
86
|
-
tooltip,
|
|
87
|
-
children,
|
|
88
|
-
label,
|
|
89
|
-
variant = "ghost",
|
|
90
|
-
size = "icon-sm",
|
|
91
|
-
...props
|
|
92
|
-
}: MessageActionProps) => {
|
|
93
|
-
const button = (
|
|
94
|
-
<Button size={size} type="button" variant={variant} {...props}>
|
|
95
|
-
{children}
|
|
96
|
-
<span className="sr-only">{label || tooltip}</span>
|
|
97
|
-
</Button>
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
if (tooltip) {
|
|
101
|
-
return (
|
|
102
|
-
<TooltipProvider>
|
|
103
|
-
<Tooltip>
|
|
104
|
-
<TooltipTrigger>{button}</TooltipTrigger>
|
|
105
|
-
<TooltipContent>
|
|
106
|
-
<p>{tooltip}</p>
|
|
107
|
-
</TooltipContent>
|
|
108
|
-
</Tooltip>
|
|
109
|
-
</TooltipProvider>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return button;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
interface MessageBranchContextType {
|
|
117
|
-
currentBranch: number;
|
|
118
|
-
totalBranches: number;
|
|
119
|
-
goToPrevious: () => void;
|
|
120
|
-
goToNext: () => void;
|
|
121
|
-
branches: ReactElement[];
|
|
122
|
-
setBranches: (branches: ReactElement[]) => void;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const MessageBranchContext = createContext<MessageBranchContextType | null>(
|
|
126
|
-
null
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const useMessageBranch = () => {
|
|
130
|
-
const context = useContext(MessageBranchContext);
|
|
131
|
-
|
|
132
|
-
if (!context) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
"MessageBranch components must be used within MessageBranch"
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return context;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
|
|
142
|
-
defaultBranch?: number;
|
|
143
|
-
onBranchChange?: (branchIndex: number) => void;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export const MessageBranch = ({
|
|
147
|
-
defaultBranch = 0,
|
|
148
|
-
onBranchChange,
|
|
149
|
-
className,
|
|
150
|
-
...props
|
|
151
|
-
}: MessageBranchProps) => {
|
|
152
|
-
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
|
153
|
-
const [branches, setBranches] = useState<ReactElement[]>([]);
|
|
154
|
-
|
|
155
|
-
const handleBranchChange = useCallback(
|
|
156
|
-
(newBranch: number) => {
|
|
157
|
-
setCurrentBranch(newBranch);
|
|
158
|
-
onBranchChange?.(newBranch);
|
|
159
|
-
},
|
|
160
|
-
[onBranchChange]
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
const goToPrevious = useCallback(() => {
|
|
164
|
-
const newBranch =
|
|
165
|
-
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
|
166
|
-
handleBranchChange(newBranch);
|
|
167
|
-
}, [currentBranch, branches.length, handleBranchChange]);
|
|
168
|
-
|
|
169
|
-
const goToNext = useCallback(() => {
|
|
170
|
-
const newBranch =
|
|
171
|
-
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
|
172
|
-
handleBranchChange(newBranch);
|
|
173
|
-
}, [currentBranch, branches.length, handleBranchChange]);
|
|
174
|
-
|
|
175
|
-
const contextValue = useMemo<MessageBranchContextType>(
|
|
176
|
-
() => ({
|
|
177
|
-
branches,
|
|
178
|
-
currentBranch,
|
|
179
|
-
goToNext,
|
|
180
|
-
goToPrevious,
|
|
181
|
-
setBranches,
|
|
182
|
-
totalBranches: branches.length,
|
|
183
|
-
}),
|
|
184
|
-
[branches, currentBranch, goToNext, goToPrevious]
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<MessageBranchContext.Provider value={contextValue}>
|
|
189
|
-
<div
|
|
190
|
-
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
|
|
191
|
-
{...props}
|
|
192
|
-
/>
|
|
193
|
-
</MessageBranchContext.Provider>
|
|
194
|
-
);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
|
|
198
|
-
|
|
199
|
-
export const MessageBranchContent = ({
|
|
200
|
-
children,
|
|
201
|
-
...props
|
|
202
|
-
}: MessageBranchContentProps) => {
|
|
203
|
-
const { currentBranch, setBranches, branches } = useMessageBranch();
|
|
204
|
-
const childrenArray = useMemo(
|
|
205
|
-
() => (Array.isArray(children) ? children : [children]),
|
|
206
|
-
[children]
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
// Use useEffect to update branches when they change
|
|
210
|
-
useEffect(() => {
|
|
211
|
-
if (branches.length !== childrenArray.length) {
|
|
212
|
-
setBranches(childrenArray);
|
|
213
|
-
}
|
|
214
|
-
}, [childrenArray, branches, setBranches]);
|
|
215
|
-
|
|
216
|
-
return childrenArray.map((branch, index) => (
|
|
217
|
-
<div
|
|
218
|
-
className={cn(
|
|
219
|
-
"grid gap-2 overflow-hidden [&>div]:pb-0",
|
|
220
|
-
index === currentBranch ? "block" : "hidden"
|
|
221
|
-
)}
|
|
222
|
-
key={branch.key}
|
|
223
|
-
{...props}
|
|
224
|
-
>
|
|
225
|
-
{branch}
|
|
226
|
-
</div>
|
|
227
|
-
));
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
|
|
231
|
-
|
|
232
|
-
export const MessageBranchSelector = ({
|
|
233
|
-
className,
|
|
234
|
-
...props
|
|
235
|
-
}: MessageBranchSelectorProps) => {
|
|
236
|
-
const { totalBranches } = useMessageBranch();
|
|
237
|
-
|
|
238
|
-
// Don't render if there's only one branch
|
|
239
|
-
if (totalBranches <= 1) {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return (
|
|
244
|
-
<ButtonGroup
|
|
245
|
-
className={cn(
|
|
246
|
-
"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
|
|
247
|
-
className
|
|
248
|
-
)}
|
|
249
|
-
orientation="horizontal"
|
|
250
|
-
{...props}
|
|
251
|
-
/>
|
|
252
|
-
);
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
|
|
256
|
-
|
|
257
|
-
export const MessageBranchPrevious = ({
|
|
258
|
-
children,
|
|
259
|
-
...props
|
|
260
|
-
}: MessageBranchPreviousProps) => {
|
|
261
|
-
const { goToPrevious, totalBranches } = useMessageBranch();
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<Button
|
|
265
|
-
aria-label="Previous branch"
|
|
266
|
-
disabled={totalBranches <= 1}
|
|
267
|
-
onClick={goToPrevious}
|
|
268
|
-
size="icon-sm"
|
|
269
|
-
type="button"
|
|
270
|
-
variant="ghost"
|
|
271
|
-
{...props}
|
|
272
|
-
>
|
|
273
|
-
{children ?? <ChevronLeftIcon size={14} />}
|
|
274
|
-
</Button>
|
|
275
|
-
);
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
export type MessageBranchNextProps = ComponentProps<typeof Button>;
|
|
279
|
-
|
|
280
|
-
export const MessageBranchNext = ({
|
|
281
|
-
children,
|
|
282
|
-
...props
|
|
283
|
-
}: MessageBranchNextProps) => {
|
|
284
|
-
const { goToNext, totalBranches } = useMessageBranch();
|
|
285
|
-
|
|
286
|
-
return (
|
|
287
|
-
<Button
|
|
288
|
-
aria-label="Next branch"
|
|
289
|
-
disabled={totalBranches <= 1}
|
|
290
|
-
onClick={goToNext}
|
|
291
|
-
size="icon-sm"
|
|
292
|
-
type="button"
|
|
293
|
-
variant="ghost"
|
|
294
|
-
{...props}
|
|
295
|
-
>
|
|
296
|
-
{children ?? <ChevronRightIcon size={14} />}
|
|
297
|
-
</Button>
|
|
298
|
-
);
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
|
302
|
-
|
|
303
|
-
export const MessageBranchPage = ({
|
|
304
|
-
className,
|
|
305
|
-
...props
|
|
306
|
-
}: MessageBranchPageProps) => {
|
|
307
|
-
const { currentBranch, totalBranches } = useMessageBranch();
|
|
308
|
-
|
|
309
|
-
return (
|
|
310
|
-
<ButtonGroupText
|
|
311
|
-
className={cn(
|
|
312
|
-
"border-none bg-transparent text-muted-foreground shadow-none",
|
|
313
|
-
className
|
|
314
|
-
)}
|
|
315
|
-
{...props}
|
|
316
|
-
>
|
|
317
|
-
{currentBranch + 1} of {totalBranches}
|
|
318
|
-
</ButtonGroupText>
|
|
319
|
-
);
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
|
323
|
-
|
|
324
|
-
const streamdownPlugins = { cjk, code, math, mermaid };
|
|
325
|
-
|
|
326
|
-
export const MessageResponse = memo(
|
|
327
|
-
({ className, ...props }: MessageResponseProps) => (
|
|
328
|
-
<Streamdown
|
|
329
|
-
className={cn(
|
|
330
|
-
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
|
331
|
-
className
|
|
332
|
-
)}
|
|
333
|
-
plugins={streamdownPlugins}
|
|
334
|
-
{...props}
|
|
335
|
-
/>
|
|
336
|
-
),
|
|
337
|
-
(prevProps, nextProps) =>
|
|
338
|
-
prevProps.children === nextProps.children &&
|
|
339
|
-
nextProps.isAnimating === prevProps.isAnimating
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
MessageResponse.displayName = "MessageResponse";
|
|
343
|
-
|
|
344
|
-
export type MessageToolbarProps = ComponentProps<"div">;
|
|
345
|
-
|
|
346
|
-
export const MessageToolbar = ({
|
|
347
|
-
className,
|
|
348
|
-
children,
|
|
349
|
-
...props
|
|
350
|
-
}: MessageToolbarProps) => (
|
|
351
|
-
<div
|
|
352
|
-
className={cn(
|
|
353
|
-
"mt-4 flex w-full items-center justify-between gap-4",
|
|
354
|
-
className
|
|
355
|
-
)}
|
|
356
|
-
{...props}
|
|
357
|
-
>
|
|
358
|
-
{children}
|
|
359
|
-
</div>
|
|
360
|
-
);
|