@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
|
@@ -6,7 +6,6 @@ import dynamic from "next/dynamic";
|
|
|
6
6
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
7
7
|
import { CheckIcon, UserIcon } from "lucide-react";
|
|
8
8
|
import { Md5 } from "@smithy/md5-js";
|
|
9
|
-
import { gsap } from "gsap";
|
|
10
9
|
import {
|
|
11
10
|
createConsoleMessage,
|
|
12
11
|
createConsoleThread,
|
|
@@ -35,10 +34,9 @@ import {
|
|
|
35
34
|
subscribeReaderSettingsChanges,
|
|
36
35
|
type ReaderMotionSetting,
|
|
37
36
|
} from "./reader-settings";
|
|
38
|
-
import {
|
|
37
|
+
import { PapyrusConsoleConversation, type ConsoleConversationMessage } from "./papyrus-console-conversation";
|
|
38
|
+
import { PapyrusPrivateChatPanel } from "./papyrus-private-chat-panel";
|
|
39
39
|
import { ModelSelector } from "./ai-elements/model-selector";
|
|
40
|
-
import { PromptInput, PromptInputBody, PromptInputFooter, type PromptInputMessage, PromptInputSubmit, PromptInputTextarea, PromptInputTools } from "./ai-elements/prompt-input";
|
|
41
|
-
import { Shimmer } from "./ai-elements/shimmer";
|
|
42
40
|
import { Suggestion, Suggestions } from "./ai-elements/suggestion";
|
|
43
41
|
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "./ai-elements/tool";
|
|
44
42
|
import { CONNECTION_STATE_CHANGE, ConnectionState } from "aws-amplify/data";
|
|
@@ -54,7 +52,6 @@ import {
|
|
|
54
52
|
useRef,
|
|
55
53
|
useState,
|
|
56
54
|
} from "react";
|
|
57
|
-
import { useStickToBottomContext } from "use-stick-to-bottom";
|
|
58
55
|
|
|
59
56
|
const CONSOLE_STARTER_SUGGESTIONS = [
|
|
60
57
|
"Start a research assignment",
|
|
@@ -108,6 +105,7 @@ const CONSOLE_MODEL_OPTIONS = [
|
|
|
108
105
|
|
|
109
106
|
type PapyrusConsoleShellProps = {
|
|
110
107
|
children: ReactNode;
|
|
108
|
+
privateChatEnabled?: boolean;
|
|
111
109
|
};
|
|
112
110
|
|
|
113
111
|
type PapyrusConsoleContextValue = {
|
|
@@ -148,7 +146,7 @@ type ConsoleAuthorIdentity = {
|
|
|
148
146
|
|
|
149
147
|
const PapyrusConsoleContext = createContext<PapyrusConsoleContextValue | null>(null);
|
|
150
148
|
|
|
151
|
-
export function PapyrusConsoleShell({ children }: PapyrusConsoleShellProps) {
|
|
149
|
+
export function PapyrusConsoleShell({ children, privateChatEnabled = false }: PapyrusConsoleShellProps) {
|
|
152
150
|
const pathname = usePathname();
|
|
153
151
|
const [session, setSession] = useState<ReaderSessionSnapshot | null>(null);
|
|
154
152
|
const [open, setOpen] = useState(false);
|
|
@@ -367,7 +365,9 @@ export function PapyrusConsoleShell({ children }: PapyrusConsoleShellProps) {
|
|
|
367
365
|
<aside className={open ? "papyrus-console papyrus-console--open" : "papyrus-console"} aria-label="Papyrus console">
|
|
368
366
|
{open ? (
|
|
369
367
|
canUseConsole
|
|
370
|
-
?
|
|
368
|
+
? privateChatEnabled
|
|
369
|
+
? <PapyrusPrivateChatPanel onClose={toggleOpen} />
|
|
370
|
+
: <ConsolePanel actorEmail={session?.auth.status === "signedIn" ? (session.auth.email ?? null) : null} actorLabel={session?.auth.label ?? "Papyrus editor"} onClose={toggleOpen} />
|
|
371
371
|
: <ConsoleAccessPanel onClose={toggleOpen} session={session} />
|
|
372
372
|
) : null}
|
|
373
373
|
</aside>
|
|
@@ -421,7 +421,6 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
|
|
|
421
421
|
const [thread, setThread] = useState<ConsoleChatThread | null>(null);
|
|
422
422
|
const [threads, setThreads] = useState<ConsoleChatThread[]>([]);
|
|
423
423
|
const [messages, setMessages] = useState<ConsoleChatMessage[]>([]);
|
|
424
|
-
const [draft, setDraft] = useState("");
|
|
425
424
|
const [loading, setLoading] = useState(false);
|
|
426
425
|
const [sending, setSending] = useState(false);
|
|
427
426
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -863,13 +862,6 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
|
|
|
863
862
|
}
|
|
864
863
|
}
|
|
865
864
|
|
|
866
|
-
async function sendMessage(message: PromptInputMessage) {
|
|
867
|
-
const content = message.text.trim();
|
|
868
|
-
if (!content) return;
|
|
869
|
-
setDraft("");
|
|
870
|
-
await submitMessage(content);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
865
|
function handleSuggestionClick(suggestion: string) {
|
|
874
866
|
if (sending) return;
|
|
875
867
|
void submitMessage(suggestion);
|
|
@@ -1045,18 +1037,18 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
|
|
|
1045
1037
|
}
|
|
1046
1038
|
return latestAgentEntryId;
|
|
1047
1039
|
}, [isSupersededRunningAssistantMessage, timelineEntries]);
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
return [
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
(
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
}, [
|
|
1040
|
+
const presentationEntries = useMemo(() => new Map(timelineEntries.map((entry) => [entry.id, entry])), [timelineEntries]);
|
|
1041
|
+
const conversationMessages = useMemo<ConsoleConversationMessage[]>(() => timelineEntries.flatMap<ConsoleConversationMessage>((entry) => {
|
|
1042
|
+
if (entry.kind === "tool") return [{ id: entry.id, role: "TOOL", text: "", createdAt: entry.tool.firstCreatedAt }];
|
|
1043
|
+
const message = entry.message;
|
|
1044
|
+
const content = (message.content ?? "").trim();
|
|
1045
|
+
if (isSupersededRunningAssistantMessage(message, content)) return [];
|
|
1046
|
+
const awaiting = message.pendingPlaceholder || (message.role === "ASSISTANT" && message.responseStatus === "RUNNING");
|
|
1047
|
+
return [{ id: message.id, role: message.role,
|
|
1048
|
+
text: content || (awaiting ? "" : sanitizeConsoleSummary((message.summary ?? "").trim())),
|
|
1049
|
+
createdAt: message.createdAt, pending: awaiting, failed: message.responseStatus === "FAILED",
|
|
1050
|
+
error: message.responseError, context: message.messageKind === CONSOLE_WEB_CONTEXT_MESSAGE_KIND }];
|
|
1051
|
+
}), [isSupersededRunningAssistantMessage, timelineEntries]);
|
|
1060
1052
|
|
|
1061
1053
|
return (
|
|
1062
1054
|
<div className="papyrus-console__panel">
|
|
@@ -1089,141 +1081,49 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
|
|
|
1089
1081
|
</button>
|
|
1090
1082
|
) : null}
|
|
1091
1083
|
</div>
|
|
1092
|
-
<
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
? resolveAssistantAvatarPresentation(message, motionSetting === "low", pending)
|
|
1136
|
-
: null;
|
|
1137
|
-
const userAvatarHash = message.role === "USER" ? readConsoleAuthorAvatarHash(message) : null;
|
|
1138
|
-
const userAuthorEmail = message.role === "USER" ? readConsoleAuthorEmail(message) : null;
|
|
1139
|
-
const shouldShowThinking = (
|
|
1140
|
-
message.pendingPlaceholder
|
|
1141
|
-
|| (message.role === "ASSISTANT" && message.responseStatus === "RUNNING" && !content && !isSupersededRunningAssistant)
|
|
1142
|
-
);
|
|
1143
|
-
if (isSupersededRunningAssistant) return null;
|
|
1144
|
-
|
|
1145
|
-
return (
|
|
1146
|
-
<ConsoleChatMessageRow
|
|
1147
|
-
key={message.id}
|
|
1148
|
-
role={isWebContextMessage ? "CONTEXT" : message.role}
|
|
1149
|
-
>
|
|
1150
|
-
<div className={message.role === "ASSISTANT" ? "papyrus-console-message__markdown" : undefined}>
|
|
1151
|
-
{message.role === "USER" ? <ConsoleUserAvatar authorEmail={userAuthorEmail} avatarHash={userAvatarHash} /> : null}
|
|
1152
|
-
{isWebContextMessage ? (
|
|
1153
|
-
<p className="papyrus-console-message__context-label">Page context</p>
|
|
1154
|
-
) : null}
|
|
1155
|
-
{shouldShowThinking ? (
|
|
1156
|
-
<Shimmer className="papyrus-console__thinking-shimmer">Thinking...</Shimmer>
|
|
1157
|
-
) : <ConsoleChatMessageContent role={message.role} text={content || sanitizedSummary} />}
|
|
1158
|
-
</div>
|
|
1159
|
-
{message.responseStatus === "FAILED" ? <span className="papyrus-console-message__error">{message.responseError ?? "Responder failed"}</span> : null}
|
|
1160
|
-
{assistantAvatar ? (
|
|
1161
|
-
<span className="papyrus-console-message__assistant-avatar" data-tone={assistantAvatar.tone}>
|
|
1162
|
-
<VultusBotAvatar
|
|
1163
|
-
ariaLabel={assistantAvatar.ariaLabel}
|
|
1164
|
-
lightColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_LIGHT_COLOR : ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR}
|
|
1165
|
-
neutralIdleMode={assistantAvatar.neutralIdleMode}
|
|
1166
|
-
shadowColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_SHADOW_COLOR : ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR}
|
|
1167
|
-
size={ASSISTANT_AVATAR_SIZE_PX}
|
|
1168
|
-
state={assistantAvatar.state}
|
|
1169
|
-
transitionDurationSeconds={0.35}
|
|
1170
|
-
/>
|
|
1171
|
-
</span>
|
|
1172
|
-
) : null}
|
|
1173
|
-
</ConsoleChatMessageRow>
|
|
1174
|
-
);
|
|
1175
|
-
})}
|
|
1176
|
-
</ConversationContent>
|
|
1177
|
-
<ConversationScrollButton
|
|
1178
|
-
aria-label="Scroll to latest message"
|
|
1179
|
-
className="papyrus-console__scroll-bottom"
|
|
1180
|
-
/>
|
|
1181
|
-
</Conversation>
|
|
1182
|
-
{!loading && !sortedMessages.length ? (
|
|
1183
|
-
<div className="papyrus-console__suggestions papyrus-console-ui">
|
|
1184
|
-
<Suggestions>
|
|
1185
|
-
{CONSOLE_STARTER_SUGGESTIONS.map((suggestion) => (
|
|
1186
|
-
<Suggestion
|
|
1187
|
-
disabled={sending}
|
|
1188
|
-
key={suggestion}
|
|
1189
|
-
onClick={handleSuggestionClick}
|
|
1190
|
-
suggestion={suggestion}
|
|
1191
|
-
/>
|
|
1192
|
-
))}
|
|
1193
|
-
</Suggestions>
|
|
1194
|
-
</div>
|
|
1195
|
-
) : null}
|
|
1196
|
-
{error ? <p className="papyrus-console__error">{error}</p> : null}
|
|
1197
|
-
<PromptInput className="papyrus-console__composer" onSubmit={(message) => void sendMessage(message)}>
|
|
1198
|
-
<PromptInputBody>
|
|
1199
|
-
<PromptInputTextarea
|
|
1200
|
-
aria-label="Console message"
|
|
1201
|
-
disabled={sending}
|
|
1202
|
-
onChange={(event) => setDraft(event.target.value)}
|
|
1203
|
-
placeholder="Write a message..."
|
|
1204
|
-
rows={3}
|
|
1205
|
-
value={draft}
|
|
1206
|
-
/>
|
|
1207
|
-
</PromptInputBody>
|
|
1208
|
-
<PromptInputFooter>
|
|
1209
|
-
<PromptInputTools>
|
|
1210
|
-
<ModelSelector
|
|
1211
|
-
aria-label="Console model"
|
|
1212
|
-
disabled={sending}
|
|
1213
|
-
onValueChange={handleModelChange}
|
|
1214
|
-
options={CONSOLE_MODEL_OPTIONS.map((option) => ({
|
|
1215
|
-
value: option.value,
|
|
1216
|
-
label: option.label,
|
|
1217
|
-
}))}
|
|
1218
|
-
value={selectedModel}
|
|
1219
|
-
/>
|
|
1220
|
-
</PromptInputTools>
|
|
1221
|
-
<PromptInputSubmit
|
|
1222
|
-
disabled={!draft.trim() || sending}
|
|
1223
|
-
status={pending ? "streaming" : "ready"}
|
|
1224
|
-
/>
|
|
1225
|
-
</PromptInputFooter>
|
|
1226
|
-
</PromptInput>
|
|
1084
|
+
<PapyrusConsoleConversation
|
|
1085
|
+
messages={conversationMessages}
|
|
1086
|
+
isSending={sending}
|
|
1087
|
+
isRunning={pending}
|
|
1088
|
+
onSendMessage={submitMessage}
|
|
1089
|
+
error={error}
|
|
1090
|
+
emptyState={loading ? <p className="papyrus-console__empty">Loading conversation…</p> : (
|
|
1091
|
+
<div className="papyrus-console__suggestions papyrus-console-ui">
|
|
1092
|
+
<Suggestions>{CONSOLE_STARTER_SUGGESTIONS.map((suggestion) => (
|
|
1093
|
+
<Suggestion disabled={sending} key={suggestion} onClick={handleSuggestionClick} suggestion={suggestion} />
|
|
1094
|
+
))}</Suggestions>
|
|
1095
|
+
</div>
|
|
1096
|
+
)}
|
|
1097
|
+
composerActions={<div className="papyrus-console-ui"><ModelSelector
|
|
1098
|
+
aria-label="Console model" disabled={sending} onValueChange={handleModelChange}
|
|
1099
|
+
options={CONSOLE_MODEL_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
|
|
1100
|
+
value={selectedModel}
|
|
1101
|
+
/></div>}
|
|
1102
|
+
presentation={{
|
|
1103
|
+
header: (id) => {
|
|
1104
|
+
const entry = presentationEntries.get(id);
|
|
1105
|
+
if (entry?.kind !== "message" || entry.message.role !== "USER") return null;
|
|
1106
|
+
return <ConsoleUserAvatar authorEmail={readConsoleAuthorEmail(entry.message)} avatarHash={readConsoleAuthorAvatarHash(entry.message)} />;
|
|
1107
|
+
},
|
|
1108
|
+
body: (id, body) => {
|
|
1109
|
+
const entry = presentationEntries.get(id);
|
|
1110
|
+
return entry?.kind === "tool" ? <ConsoleToolMessageContent tool={entry.tool} /> : body;
|
|
1111
|
+
},
|
|
1112
|
+
footer: (id) => {
|
|
1113
|
+
const entry = presentationEntries.get(id);
|
|
1114
|
+
if (!entry || id !== latestVisibleAvatarAnchorId) return null;
|
|
1115
|
+
const avatar = resolveAssistantAvatarPresentation(entry.kind === "tool" ? entry.tool : entry.message, motionSetting === "low", pending);
|
|
1116
|
+
if (!avatar) return null;
|
|
1117
|
+
return <span className="papyrus-console-message__assistant-avatar" data-tone={avatar.tone}>
|
|
1118
|
+
<VultusBotAvatar ariaLabel={avatar.ariaLabel}
|
|
1119
|
+
lightColor={avatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_LIGHT_COLOR : ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR}
|
|
1120
|
+
shadowColor={avatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_SHADOW_COLOR : ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR}
|
|
1121
|
+
neutralIdleMode={avatar.neutralIdleMode} size={ASSISTANT_AVATAR_SIZE_PX}
|
|
1122
|
+
state={avatar.state} transitionDurationSeconds={0.35} />
|
|
1123
|
+
</span>;
|
|
1124
|
+
},
|
|
1125
|
+
}}
|
|
1126
|
+
/>
|
|
1227
1127
|
</div>
|
|
1228
1128
|
);
|
|
1229
1129
|
}
|
|
@@ -1516,30 +1416,6 @@ function ConsoleToolMessageContent({ tool }: { tool: ConsoleToolTimelineEntry })
|
|
|
1516
1416
|
);
|
|
1517
1417
|
}
|
|
1518
1418
|
|
|
1519
|
-
function ConsoleToolTimelineRow({ children }: { children: ReactNode }) {
|
|
1520
|
-
const rowRef = useRef<HTMLElement | null>(null);
|
|
1521
|
-
|
|
1522
|
-
useEffect(() => {
|
|
1523
|
-
const row = rowRef.current;
|
|
1524
|
-
if (!row || prefersReducedMotion()) return;
|
|
1525
|
-
const tween = gsap.fromTo(
|
|
1526
|
-
row,
|
|
1527
|
-
{ autoAlpha: 0, y: 8 },
|
|
1528
|
-
{ autoAlpha: 1, y: 0, duration: 0.16, ease: "power2.out", overwrite: "auto" },
|
|
1529
|
-
);
|
|
1530
|
-
return () => {
|
|
1531
|
-
tween.kill();
|
|
1532
|
-
gsap.killTweensOf(row);
|
|
1533
|
-
};
|
|
1534
|
-
}, []);
|
|
1535
|
-
|
|
1536
|
-
return (
|
|
1537
|
-
<article className="papyrus-console-tool-row" ref={rowRef}>
|
|
1538
|
-
{children}
|
|
1539
|
-
</article>
|
|
1540
|
-
);
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
1419
|
function formatToolOutputText(value: unknown): string {
|
|
1544
1420
|
if (typeof value === "string") return value;
|
|
1545
1421
|
if (value === undefined || value === null) return "";
|
|
@@ -1550,240 +1426,6 @@ function formatToolOutputText(value: unknown): string {
|
|
|
1550
1426
|
}
|
|
1551
1427
|
}
|
|
1552
1428
|
|
|
1553
|
-
function ConsoleChatMessageContent({ role, text }: { role: string | null | undefined; text: string }) {
|
|
1554
|
-
const blocks = useMemo(() => parseConsoleMarkdownBlocks(text), [text]);
|
|
1555
|
-
if (role !== "ASSISTANT") return text;
|
|
1556
|
-
return (
|
|
1557
|
-
<>
|
|
1558
|
-
{blocks.map((block, index) => renderConsoleMarkdownBlock(block, index))}
|
|
1559
|
-
</>
|
|
1560
|
-
);
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
type ConsoleMarkdownBlock =
|
|
1564
|
-
| { type: "code"; text: string }
|
|
1565
|
-
| { type: "heading"; depth: number; text: string }
|
|
1566
|
-
| { type: "list"; ordered: boolean; items: string[] }
|
|
1567
|
-
| { type: "paragraph"; text: string };
|
|
1568
|
-
|
|
1569
|
-
function parseConsoleMarkdownBlocks(text: string): ConsoleMarkdownBlock[] {
|
|
1570
|
-
const blocks: ConsoleMarkdownBlock[] = [];
|
|
1571
|
-
const lines = text.split(/\r?\n/);
|
|
1572
|
-
let paragraph: string[] = [];
|
|
1573
|
-
let list: string[] = [];
|
|
1574
|
-
let listOrdered = false;
|
|
1575
|
-
let code: string[] = [];
|
|
1576
|
-
let inCode = false;
|
|
1577
|
-
|
|
1578
|
-
const flushParagraph = () => {
|
|
1579
|
-
if (!paragraph.length) return;
|
|
1580
|
-
blocks.push({ type: "paragraph", text: paragraph.join(" ").trim() });
|
|
1581
|
-
paragraph = [];
|
|
1582
|
-
};
|
|
1583
|
-
const flushList = () => {
|
|
1584
|
-
if (!list.length) return;
|
|
1585
|
-
blocks.push({ type: "list", ordered: listOrdered, items: list });
|
|
1586
|
-
list = [];
|
|
1587
|
-
};
|
|
1588
|
-
|
|
1589
|
-
for (const line of lines) {
|
|
1590
|
-
if (line.trim().startsWith("```")) {
|
|
1591
|
-
if (inCode) {
|
|
1592
|
-
blocks.push({ type: "code", text: code.join("\n") });
|
|
1593
|
-
code = [];
|
|
1594
|
-
inCode = false;
|
|
1595
|
-
} else {
|
|
1596
|
-
flushParagraph();
|
|
1597
|
-
flushList();
|
|
1598
|
-
inCode = true;
|
|
1599
|
-
}
|
|
1600
|
-
continue;
|
|
1601
|
-
}
|
|
1602
|
-
if (inCode) {
|
|
1603
|
-
code.push(line);
|
|
1604
|
-
continue;
|
|
1605
|
-
}
|
|
1606
|
-
const trimmed = line.trim();
|
|
1607
|
-
if (!trimmed) {
|
|
1608
|
-
flushParagraph();
|
|
1609
|
-
flushList();
|
|
1610
|
-
continue;
|
|
1611
|
-
}
|
|
1612
|
-
const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed);
|
|
1613
|
-
if (heading) {
|
|
1614
|
-
flushParagraph();
|
|
1615
|
-
flushList();
|
|
1616
|
-
blocks.push({ type: "heading", depth: heading[1].length, text: heading[2].trim() });
|
|
1617
|
-
continue;
|
|
1618
|
-
}
|
|
1619
|
-
const bullet = /^([-*+])\s+(.+)$/.exec(trimmed);
|
|
1620
|
-
if (bullet) {
|
|
1621
|
-
flushParagraph();
|
|
1622
|
-
if (!list.length) listOrdered = false;
|
|
1623
|
-
list.push(bullet[2].trim());
|
|
1624
|
-
continue;
|
|
1625
|
-
}
|
|
1626
|
-
const ordered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
|
|
1627
|
-
if (ordered) {
|
|
1628
|
-
flushParagraph();
|
|
1629
|
-
if (!list.length) listOrdered = true;
|
|
1630
|
-
list.push(ordered[1].trim());
|
|
1631
|
-
continue;
|
|
1632
|
-
}
|
|
1633
|
-
flushList();
|
|
1634
|
-
paragraph.push(trimmed);
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
flushParagraph();
|
|
1638
|
-
flushList();
|
|
1639
|
-
if (code.length) blocks.push({ type: "code", text: code.join("\n") });
|
|
1640
|
-
return blocks;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
function renderConsoleMarkdownBlock(block: ConsoleMarkdownBlock, index: number): ReactNode {
|
|
1644
|
-
const key = `${block.type}-${index}`;
|
|
1645
|
-
if (block.type === "code") return <pre className="papyrus-console-message__code" key={key}>{block.text}</pre>;
|
|
1646
|
-
if (block.type === "list") {
|
|
1647
|
-
if (block.ordered) {
|
|
1648
|
-
return (
|
|
1649
|
-
<ol key={key}>
|
|
1650
|
-
{block.items.map((item, itemIndex) => <li key={`${key}-${itemIndex}`}>{renderConsoleInlineMarkdown(item)}</li>)}
|
|
1651
|
-
</ol>
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
return (
|
|
1655
|
-
<ul key={key}>
|
|
1656
|
-
{block.items.map((item, itemIndex) => <li key={`${key}-${itemIndex}`}>{renderConsoleInlineMarkdown(item)}</li>)}
|
|
1657
|
-
</ul>
|
|
1658
|
-
);
|
|
1659
|
-
}
|
|
1660
|
-
if (block.type === "heading") {
|
|
1661
|
-
if (block.depth <= 1) return <h3 key={key}>{renderConsoleInlineMarkdown(block.text)}</h3>;
|
|
1662
|
-
if (block.depth === 2) return <h4 key={key}>{renderConsoleInlineMarkdown(block.text)}</h4>;
|
|
1663
|
-
return <h5 key={key}>{renderConsoleInlineMarkdown(block.text)}</h5>;
|
|
1664
|
-
}
|
|
1665
|
-
return <p key={key}>{renderConsoleInlineMarkdown(block.text)}</p>;
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
function renderConsoleInlineMarkdown(text: string): ReactNode[] {
|
|
1669
|
-
const nodes: ReactNode[] = [];
|
|
1670
|
-
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\))/g;
|
|
1671
|
-
let lastIndex = 0;
|
|
1672
|
-
let match: RegExpExecArray | null;
|
|
1673
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
1674
|
-
if (match.index > lastIndex) nodes.push(text.slice(lastIndex, match.index));
|
|
1675
|
-
const token = match[0];
|
|
1676
|
-
if (token.startsWith("`")) {
|
|
1677
|
-
nodes.push(<code key={`${match.index}-code`}>{token.slice(1, -1)}</code>);
|
|
1678
|
-
} else if (token.startsWith("**")) {
|
|
1679
|
-
nodes.push(<strong key={`${match.index}-strong`}>{token.slice(2, -2)}</strong>);
|
|
1680
|
-
} else {
|
|
1681
|
-
const linkMatch = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
1682
|
-
if (linkMatch) {
|
|
1683
|
-
nodes.push(
|
|
1684
|
-
<a href={linkMatch[2]} key={`${match.index}-link`} rel="noreferrer noopener" target="_blank">
|
|
1685
|
-
{linkMatch[1]}
|
|
1686
|
-
</a>,
|
|
1687
|
-
);
|
|
1688
|
-
} else {
|
|
1689
|
-
nodes.push(token);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
lastIndex = match.index + token.length;
|
|
1693
|
-
}
|
|
1694
|
-
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
1695
|
-
return nodes;
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
function ConsoleChatMessageRow({
|
|
1699
|
-
role,
|
|
1700
|
-
children,
|
|
1701
|
-
}: {
|
|
1702
|
-
role: string | null | undefined;
|
|
1703
|
-
children: ReactNode;
|
|
1704
|
-
}) {
|
|
1705
|
-
const rowRef = useRef<HTMLElement | null>(null);
|
|
1706
|
-
|
|
1707
|
-
useEffect(() => {
|
|
1708
|
-
const row = rowRef.current;
|
|
1709
|
-
if (!row || prefersReducedMotion()) return;
|
|
1710
|
-
const tween = gsap.fromTo(
|
|
1711
|
-
row,
|
|
1712
|
-
{ autoAlpha: 0, y: 8 },
|
|
1713
|
-
{ autoAlpha: 1, y: 0, duration: 0.16, ease: "power2.out", overwrite: "auto" },
|
|
1714
|
-
);
|
|
1715
|
-
return () => {
|
|
1716
|
-
tween.kill();
|
|
1717
|
-
gsap.killTweensOf(row);
|
|
1718
|
-
};
|
|
1719
|
-
}, []);
|
|
1720
|
-
|
|
1721
|
-
return (
|
|
1722
|
-
<article
|
|
1723
|
-
className={
|
|
1724
|
-
role === "USER"
|
|
1725
|
-
? "papyrus-console-message papyrus-console-message--user"
|
|
1726
|
-
: role === "CONTEXT"
|
|
1727
|
-
? "papyrus-console-message papyrus-console-message--context"
|
|
1728
|
-
: "papyrus-console-message papyrus-console-message--assistant"
|
|
1729
|
-
}
|
|
1730
|
-
ref={rowRef}
|
|
1731
|
-
>
|
|
1732
|
-
{children}
|
|
1733
|
-
</article>
|
|
1734
|
-
);
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
function ConsoleStreamingAutoStick({ active, streamSignal }: { active: boolean; streamSignal: string | null }) {
|
|
1738
|
-
const { scrollRef, scrollToBottom } = useStickToBottomContext();
|
|
1739
|
-
const previousSignalRef = useRef<string | null>(null);
|
|
1740
|
-
|
|
1741
|
-
const snapToBottom = useCallback(() => {
|
|
1742
|
-
void scrollToBottom({ animation: "instant" });
|
|
1743
|
-
const scrollElement = scrollRef.current;
|
|
1744
|
-
if (!scrollElement) return;
|
|
1745
|
-
scrollElement.scrollTop = Math.max(0, scrollElement.scrollHeight - scrollElement.clientHeight);
|
|
1746
|
-
}, [scrollRef, scrollToBottom]);
|
|
1747
|
-
|
|
1748
|
-
useEffect(() => {
|
|
1749
|
-
if (!active || !streamSignal) return;
|
|
1750
|
-
if (previousSignalRef.current === streamSignal) return;
|
|
1751
|
-
previousSignalRef.current = streamSignal;
|
|
1752
|
-
|
|
1753
|
-
snapToBottom();
|
|
1754
|
-
const frame = window.requestAnimationFrame(() => {
|
|
1755
|
-
snapToBottom();
|
|
1756
|
-
});
|
|
1757
|
-
return () => {
|
|
1758
|
-
window.cancelAnimationFrame(frame);
|
|
1759
|
-
};
|
|
1760
|
-
}, [active, snapToBottom, streamSignal]);
|
|
1761
|
-
|
|
1762
|
-
useEffect(() => {
|
|
1763
|
-
if (!active) return;
|
|
1764
|
-
let frame = 0;
|
|
1765
|
-
const pin = () => {
|
|
1766
|
-
snapToBottom();
|
|
1767
|
-
frame = window.requestAnimationFrame(pin);
|
|
1768
|
-
};
|
|
1769
|
-
pin();
|
|
1770
|
-
return () => {
|
|
1771
|
-
window.cancelAnimationFrame(frame);
|
|
1772
|
-
};
|
|
1773
|
-
}, [active, snapToBottom]);
|
|
1774
|
-
|
|
1775
|
-
useEffect(() => {
|
|
1776
|
-
if (!active) previousSignalRef.current = null;
|
|
1777
|
-
}, [active]);
|
|
1778
|
-
|
|
1779
|
-
return null;
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
function prefersReducedMotion(): boolean {
|
|
1783
|
-
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
1784
|
-
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
1429
|
function formatConsoleThreadOption(thread: ConsoleChatThread): string {
|
|
1788
1430
|
const stamp = thread.lastMessageAt ?? thread.updatedAt ?? thread.createdAt;
|
|
1789
1431
|
const date = stamp ? new Date(stamp) : null;
|