@anthusai/papyrus 5.39.0 → 5.40.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/functions/monitoring-view/schema.js +6 -0
- package/amplify/functions/monitoring-view/schema.ts +6 -0
- package/app/api/newsroom/chat/[...path]/route.ts +17 -0
- package/app/layout.tsx +5 -1
- package/components/monitoring-overview-view.tsx +48 -2
- package/components/monitoring-panel.tsx +11 -3
- package/components/papyrus-console-conversation.css +14 -0
- package/components/papyrus-console-conversation.tsx +55 -0
- package/components/papyrus-console-shell.tsx +62 -420
- package/components/papyrus-private-chat-panel.tsx +134 -0
- package/infra/amplify-app-shell.js +2 -0
- package/infra/amplify-app-shell.ts +2 -0
- package/infra/site-config.js +4 -0
- package/infra/site-config.ts +4 -0
- package/lib/monitoring-client.ts +20 -15
- package/lib/monitoring-overview.ts +13 -0
- package/lib/papyrus-chat-admission.ts +65 -0
- package/lib/papyrus-hosted-chat.ts +217 -0
- package/lib/papyrus-private-chat-client.ts +81 -0
- package/lib/prospectus-session.ts +15 -0
- package/lib/prospectus-sign-in-routes.ts +2 -12
- package/lib/shared-chat-admission.ts +182 -0
- package/lib/site-origin-value.ts +50 -0
- package/lib/site-origin.ts +3 -62
- package/package.json +10 -1
- package/routes.manifest.json +13 -1
- package/src/with-papyrus.mjs +1 -1
|
@@ -14,4 +14,10 @@ export const monitoringViewSchemaDefinitions = {
|
|
|
14
14
|
.returns(a.json())
|
|
15
15
|
.authorization((allow) => [allow.groups(editors)])
|
|
16
16
|
.handler(functionHandler("monitoringView", monitoringView)),
|
|
17
|
+
monitoringEmailSubmission: a
|
|
18
|
+
.query()
|
|
19
|
+
.arguments({ receiptId: a.string().required() })
|
|
20
|
+
.returns(a.json())
|
|
21
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
22
|
+
.handler(functionHandler("monitoringView", monitoringView)),
|
|
17
23
|
};
|
|
@@ -16,4 +16,10 @@ export const monitoringViewSchemaDefinitions = {
|
|
|
16
16
|
.returns(a.json())
|
|
17
17
|
.authorization((allow) => [allow.groups(editors)])
|
|
18
18
|
.handler(functionHandler("monitoringView", monitoringView)),
|
|
19
|
+
monitoringEmailSubmission: a
|
|
20
|
+
.query()
|
|
21
|
+
.arguments({ receiptId: a.string().required() })
|
|
22
|
+
.returns(a.json())
|
|
23
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
24
|
+
.handler(functionHandler("monitoringView", monitoringView)),
|
|
19
25
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handlePapyrusHostedChatRequest } from "../../../../../lib/papyrus-hosted-chat";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
type RouteContext = { params: Promise<{ path: string[] }> };
|
|
8
|
+
|
|
9
|
+
export async function GET(request: NextRequest, context: RouteContext) {
|
|
10
|
+
const { path } = await context.params;
|
|
11
|
+
return handlePapyrusHostedChatRequest(request, path);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function POST(request: NextRequest, context: RouteContext) {
|
|
15
|
+
const { path } = await context.params;
|
|
16
|
+
return handlePapyrusHostedChatRequest(request, path);
|
|
17
|
+
}
|
package/app/layout.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "chatticus/chat.css";
|
|
2
|
+
import "../components/papyrus-console-conversation.css";
|
|
1
3
|
import type { Metadata } from "next";
|
|
2
4
|
import { Playfair_Display, Plus_Jakarta_Sans } from "next/font/google";
|
|
3
5
|
import Script from "next/script";
|
|
@@ -152,7 +154,9 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
|
|
|
152
154
|
{showStagingBanner() ? <StagingBanner /> : null}
|
|
153
155
|
<AmplifyClientProvider>
|
|
154
156
|
<TooltipProvider>
|
|
155
|
-
<PapyrusConsoleShell
|
|
157
|
+
<PapyrusConsoleShell privateChatEnabled={process.env.PAPYRUS_PRIVATE_CHAT_ENABLED === "true"}>
|
|
158
|
+
{children}
|
|
159
|
+
</PapyrusConsoleShell>
|
|
156
160
|
</TooltipProvider>
|
|
157
161
|
</AmplifyClientProvider>
|
|
158
162
|
</body>
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
routingLabel,
|
|
10
10
|
usageText,
|
|
11
11
|
type MonitoringItem,
|
|
12
|
+
type MonitoringEmailDetails,
|
|
13
|
+
type MonitoringEmailSummary,
|
|
12
14
|
type MonitoringOverview,
|
|
13
15
|
type MonitoringRelevance,
|
|
14
16
|
type MonitoringResult,
|
|
@@ -88,7 +90,45 @@ function QueueItem({ item, ownId, demo }: { item: MonitoringItem; ownId: string
|
|
|
88
90
|
);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
function
|
|
93
|
+
function EmailSubmissions({ items, detail, onOpen }: {
|
|
94
|
+
items: MonitoringEmailSummary[]; detail: MonitoringEmailDetails | null; onOpen: (receiptId: string) => void;
|
|
95
|
+
}) {
|
|
96
|
+
return (
|
|
97
|
+
<article className="news-desk-doctrine-card" data-monitoring-section="email-submissions">
|
|
98
|
+
<div className="news-desk-doctrine-card__header"><div><h3>Email submissions</h3><span>Untrusted source material held privately for editor review</span></div></div>
|
|
99
|
+
{items.length ? <ul className="news-desk-monitoring__list">
|
|
100
|
+
{items.map((item) => <li key={item.receiptId} data-email-submission={item.receiptId}>
|
|
101
|
+
<strong>{item.subject || "(no subject)"}</strong>
|
|
102
|
+
<span>{formatUtc(item.observedAt)} · {item.linkCount} candidate links · {item.processedCount} queued</span>
|
|
103
|
+
{item.reviewRequired ? <span>Authentication warnings: private review required; nothing was routed automatically.</span> : null}
|
|
104
|
+
{item.unresolvedLinkCount ? <span>{item.unresolvedLinkCount} tracking links remain unresolved and were not opened.</span> : null}
|
|
105
|
+
<span>Envelope sender: {item.envelopeSender || "unknown"} · MIME From (unverified): {item.headerFrom || "unknown"}</span>
|
|
106
|
+
<span>SES signals — SPF {item.verdicts.spf}, DKIM {item.verdicts.dkim}, DMARC {item.verdicts.dmarc}; spam {item.verdicts.spam}, virus {item.verdicts.virus}</span>
|
|
107
|
+
{item.warnings.length ? <span>Warnings: {item.warnings.join(", ")}</span> : null}
|
|
108
|
+
{item.errorCode ? <span>State: {item.status} ({item.errorCode})</span> : <span>State: {item.status}</span>}
|
|
109
|
+
<button type="button" onClick={() => onOpen(item.receiptId)}>Read email and review candidate links</button>
|
|
110
|
+
</li>)}
|
|
111
|
+
</ul> : <p>No email submissions are available for this publication.</p>}
|
|
112
|
+
{detail && "submission" in detail ? (
|
|
113
|
+
<section aria-label="Email submission details" data-email-submission-details={detail.submission.receiptId}>
|
|
114
|
+
<h4>{detail.submission.subject || "Submitted email"}</h4>
|
|
115
|
+
<p>Original content is shown as inert text. Sender and message content are untrusted; no instructions are executed.</p>
|
|
116
|
+
<pre style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{detail.safeBodyText}</pre>
|
|
117
|
+
<h5>Candidate references ({detail.candidateLinks.length})</h5>
|
|
118
|
+
<ul>{detail.candidateLinks.map((link, index) => <li key={`${link.url}-${index}`}>
|
|
119
|
+
{link.resolutionStatus === "unresolved" ? <span>{link.title || link.url} (unresolved; not opened)</span>
|
|
120
|
+
: <a href={link.url} target="_blank" rel="noreferrer noopener">{link.title || link.url}</a>}
|
|
121
|
+
</li>)}</ul>
|
|
122
|
+
</section>
|
|
123
|
+
) : detail && "error" in detail ? <p role="alert">You cannot read this submission.</p>
|
|
124
|
+
: detail && "state" in detail ? <p role="alert">Submission details are unavailable.</p> : null}
|
|
125
|
+
</article>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function Ready({ data, demo, emailDetail, onEmailOpen }: {
|
|
130
|
+
data: MonitoringOverview; demo: boolean; emailDetail: MonitoringEmailDetails | null; onEmailOpen: (id: string) => void;
|
|
131
|
+
}) {
|
|
92
132
|
const ceilings = data.usage?.ceilings ?? null;
|
|
93
133
|
const router = data.router;
|
|
94
134
|
return (
|
|
@@ -97,6 +137,8 @@ function Ready({ data, demo }: { data: MonitoringOverview; demo: boolean }) {
|
|
|
97
137
|
<p className="news-desk-delegation-note" data-monitoring-notice>This publication is not one of the router's outputs, so no relevance is shown for it here.</p>
|
|
98
138
|
) : null}
|
|
99
139
|
|
|
140
|
+
<EmailSubmissions items={data.emailSubmissions ?? []} detail={emailDetail} onOpen={onEmailOpen} />
|
|
141
|
+
|
|
100
142
|
<article className="news-desk-doctrine-card" data-monitoring-section="router">
|
|
101
143
|
<div className="news-desk-doctrine-card__header"><div><h3>Router</h3><span>Sends collected items to the shared Cyclotron and delivers relevant ones to References</span></div></div>
|
|
102
144
|
{router ? (
|
|
@@ -192,12 +234,16 @@ export function MonitoringOverviewView({
|
|
|
192
234
|
mode,
|
|
193
235
|
onModeChange,
|
|
194
236
|
demo = false,
|
|
237
|
+
emailDetail = null,
|
|
238
|
+
onEmailOpen = () => {},
|
|
195
239
|
}: {
|
|
196
240
|
result: MonitoringResult | null;
|
|
197
241
|
canViewAll: boolean;
|
|
198
242
|
mode: MonitoringMode;
|
|
199
243
|
onModeChange: (mode: MonitoringMode) => void;
|
|
200
244
|
demo?: boolean;
|
|
245
|
+
emailDetail?: MonitoringEmailDetails | null;
|
|
246
|
+
onEmailOpen?: (receiptId: string) => void;
|
|
201
247
|
}) {
|
|
202
248
|
let body: React.ReactNode;
|
|
203
249
|
if (result === null) {
|
|
@@ -213,7 +259,7 @@ export function MonitoringOverviewView({
|
|
|
213
259
|
} else if (result.state === "empty") {
|
|
214
260
|
body = <Message kind="empty" title="Nothing has been collected yet">No feed is configured and no run has been recorded. Items appear here after the collector's first run.</Message>;
|
|
215
261
|
} else {
|
|
216
|
-
body = <Ready data={result} demo={demo} />;
|
|
262
|
+
body = <Ready data={result} demo={demo} emailDetail={emailDetail} onEmailOpen={onEmailOpen} />;
|
|
217
263
|
}
|
|
218
264
|
const scopeLine = result && "scope" in result
|
|
219
265
|
? (result.scope.mode === "all" ? "Showing the whole shared pipeline, every publication." : `Showing what is relevant to ${result.scope.publicationId ?? "this publication"}.`)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { loadMonitoringOverview } from "../lib/monitoring-client";
|
|
4
|
+
import { loadMonitoringEmailSubmission, loadMonitoringOverview } from "../lib/monitoring-client";
|
|
5
5
|
import { FIXTURE_ALL_PIPELINE_OVERVIEW, FIXTURE_MONITORING_OVERVIEW } from "../lib/monitoring-overview-fixture";
|
|
6
|
-
import type { MonitoringResult } from "../lib/monitoring-overview";
|
|
6
|
+
import type { MonitoringEmailDetails, MonitoringResult } from "../lib/monitoring-overview";
|
|
7
7
|
import { MonitoringOverviewView, type MonitoringMode } from "./monitoring-overview-view";
|
|
8
8
|
|
|
9
9
|
const REFRESH_MILLISECONDS = 60_000;
|
|
@@ -12,6 +12,7 @@ const REFRESH_MILLISECONDS = 60_000;
|
|
|
12
12
|
export function MonitoringPanel({ isAdmin, isDemo }: { isAdmin: boolean; isDemo: boolean }) {
|
|
13
13
|
const [mode, setMode] = useState<MonitoringMode>("publication");
|
|
14
14
|
const [result, setResult] = useState<MonitoringResult | null>(null);
|
|
15
|
+
const [emailDetail, setEmailDetail] = useState<MonitoringEmailDetails | null>(null);
|
|
15
16
|
|
|
16
17
|
useEffect(() => {
|
|
17
18
|
if (isDemo) {
|
|
@@ -36,5 +37,12 @@ export function MonitoringPanel({ isAdmin, isDemo }: { isAdmin: boolean; isDemo:
|
|
|
36
37
|
};
|
|
37
38
|
}, [mode, isDemo]);
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
const openEmail = async (receiptId: string) => {
|
|
41
|
+
setEmailDetail({ state: "unavailable" });
|
|
42
|
+
const next = await loadMonitoringEmailSubmission(receiptId);
|
|
43
|
+
setEmailDetail(next);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return <MonitoringOverviewView result={result} canViewAll={isAdmin} mode={mode} onModeChange={setMode} demo={isDemo}
|
|
47
|
+
emailDetail={emailDetail} onEmailOpen={openEmail} />;
|
|
40
48
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.papyrus-console-conversation {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex: 1;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
--chatticus-chat-surface: rgb(var(--paper-rgb));
|
|
7
|
+
--chatticus-chat-raised: color-mix(in srgb, rgb(var(--paper-rgb)) 94%, rgb(var(--ink-rgb)));
|
|
8
|
+
--chatticus-chat-foreground: var(--ink);
|
|
9
|
+
--chatticus-chat-accent: var(--ink);
|
|
10
|
+
--chatticus-chat-error: var(--ink);
|
|
11
|
+
--chatticus-chat-action: var(--ink);
|
|
12
|
+
--chatticus-chat-action-text: rgb(var(--paper-rgb));
|
|
13
|
+
}
|
|
14
|
+
.papyrus-console-conversation .papyrus-console__suggestions { padding: 12px 0; }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, type ReactNode } from "react";
|
|
4
|
+
import { ChatRuntime, type ChatMessage, type MessagePresentation } from "chatticus/chat-react";
|
|
5
|
+
|
|
6
|
+
/** Editorial console data adapted to the shared presentation without exposing GraphQL or auth. */
|
|
7
|
+
export type ConsoleConversationMessage = {
|
|
8
|
+
id: string;
|
|
9
|
+
role: string | null | undefined;
|
|
10
|
+
text: string;
|
|
11
|
+
createdAt?: string;
|
|
12
|
+
pending?: boolean;
|
|
13
|
+
failed?: boolean;
|
|
14
|
+
error?: string | null;
|
|
15
|
+
context?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Normalize host records; Papyrus keeps thread ownership, persistence and response state. */
|
|
19
|
+
export function consoleConversationMessages(messages: readonly ConsoleConversationMessage[]): ChatMessage[] {
|
|
20
|
+
return messages.map((message) => ({
|
|
21
|
+
id: message.id,
|
|
22
|
+
role: message.role === "USER" ? "user" : "assistant",
|
|
23
|
+
content: [{ type: "text", text: message.failed ? message.error || "Responder failed" : message.text }],
|
|
24
|
+
createdAt: message.createdAt ? new Date(message.createdAt) : undefined,
|
|
25
|
+
status: message.failed ? { type: "incomplete", reason: "error" } : message.pending ? { type: "running" } : undefined,
|
|
26
|
+
metadata: { custom: {
|
|
27
|
+
authorLabel: message.context ? "Page context" : undefined,
|
|
28
|
+
failed: message.failed,
|
|
29
|
+
isStreaming: message.pending,
|
|
30
|
+
isStreamingShell: message.pending && !message.text,
|
|
31
|
+
createdAt: message.createdAt,
|
|
32
|
+
} },
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Controlled newsroom adapter for the reusable Chatticus transcript and composer. */
|
|
37
|
+
export function PapyrusConsoleConversation({ messages, isSending, isRunning, onSendMessage, presentation, composerActions, emptyState, error }: {
|
|
38
|
+
messages: readonly ConsoleConversationMessage[];
|
|
39
|
+
isSending: boolean;
|
|
40
|
+
isRunning: boolean;
|
|
41
|
+
onSendMessage: (text: string) => Promise<void>;
|
|
42
|
+
presentation?: MessagePresentation;
|
|
43
|
+
composerActions?: ReactNode;
|
|
44
|
+
emptyState?: ReactNode;
|
|
45
|
+
error?: string | null;
|
|
46
|
+
}) {
|
|
47
|
+
const sharedMessages = useMemo(() => consoleConversationMessages(messages), [messages]);
|
|
48
|
+
return <div className="papyrus-console-conversation" data-reply-running={isRunning || undefined}>
|
|
49
|
+
{/* Papyrus routes can accept another editor message while the current reply runs.
|
|
50
|
+
The pending assistant message carries its own running status for display. */}
|
|
51
|
+
<ChatRuntime messages={sharedMessages} isRunning={false} isSendDisabled={isSending}
|
|
52
|
+
onSendMessage={onSendMessage} presentation={presentation}
|
|
53
|
+
threadProps={{ composerPlaceholder: "Write a message...", composerActions, emptyState, streamError: error }} />
|
|
54
|
+
</div>;
|
|
55
|
+
}
|