@anthusai/papyrus 0.0.0 → 1.0.0-next.1
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/topic-steering-workspace.tsx +18063 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +89 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-nav.ts +42 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +47 -0
|
@@ -0,0 +1,1912 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { BotAvatarState } from "anthus-vultus";
|
|
4
|
+
import dynamic from "next/dynamic";
|
|
5
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
6
|
+
import { CheckIcon, UserIcon } from "lucide-react";
|
|
7
|
+
import { Md5 } from "@smithy/md5-js";
|
|
8
|
+
import { gsap } from "gsap";
|
|
9
|
+
import {
|
|
10
|
+
createConsoleMessage,
|
|
11
|
+
createConsoleThread,
|
|
12
|
+
listConsoleMessagesByThread,
|
|
13
|
+
listConsoleThreads,
|
|
14
|
+
subscribeConsoleMessages,
|
|
15
|
+
updateConsoleMessage,
|
|
16
|
+
updateConsoleThread,
|
|
17
|
+
type ConsoleChatMessage,
|
|
18
|
+
type ConsoleChatThread,
|
|
19
|
+
} from "../lib/console-chat-client";
|
|
20
|
+
import { buildNewsroomKnowledgeQueryInput, type NewsroomKnowledgeQueryTarget } from "../lib/newsroom-knowledge-query-request";
|
|
21
|
+
import {
|
|
22
|
+
CONSOLE_WEB_CONTEXT_MESSAGE_KIND,
|
|
23
|
+
formatConsoleWebContextMessage,
|
|
24
|
+
readConsoleWebUiFromMessageMetadata,
|
|
25
|
+
webUiContextKey,
|
|
26
|
+
type ConsoleWebContextEvent,
|
|
27
|
+
} from "../lib/console-web-context";
|
|
28
|
+
import { buildWebUiContext, extractNavigationIntent, type PapyrusWebUiContext } from "../lib/papyrus-web-locations";
|
|
29
|
+
import { runNewsroomKnowledgeQuery } from "./news-desk-taxonomy-client";
|
|
30
|
+
import { loadReaderSessionSnapshot, type ReaderSessionSnapshot } from "./reader-auth-state";
|
|
31
|
+
import {
|
|
32
|
+
readLocalReaderSettings,
|
|
33
|
+
resolveReaderSettings,
|
|
34
|
+
subscribeReaderSettingsChanges,
|
|
35
|
+
type ReaderMotionSetting,
|
|
36
|
+
} from "./reader-settings";
|
|
37
|
+
import { Conversation, ConversationContent, ConversationScrollButton } from "./ai-elements/conversation";
|
|
38
|
+
import { ModelSelector } from "./ai-elements/model-selector";
|
|
39
|
+
import { PromptInput, PromptInputBody, PromptInputFooter, type PromptInputMessage, PromptInputSubmit, PromptInputTextarea, PromptInputTools } from "./ai-elements/prompt-input";
|
|
40
|
+
import { Shimmer } from "./ai-elements/shimmer";
|
|
41
|
+
import { Suggestion, Suggestions } from "./ai-elements/suggestion";
|
|
42
|
+
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "./ai-elements/tool";
|
|
43
|
+
import { CONNECTION_STATE_CHANGE, ConnectionState } from "aws-amplify/data";
|
|
44
|
+
import { Hub } from "aws-amplify/utils";
|
|
45
|
+
import {
|
|
46
|
+
type ComponentType,
|
|
47
|
+
createContext,
|
|
48
|
+
type ReactNode,
|
|
49
|
+
useCallback,
|
|
50
|
+
useContext,
|
|
51
|
+
useEffect,
|
|
52
|
+
useMemo,
|
|
53
|
+
useRef,
|
|
54
|
+
useState,
|
|
55
|
+
} from "react";
|
|
56
|
+
import { useStickToBottomContext } from "use-stick-to-bottom";
|
|
57
|
+
|
|
58
|
+
const CONSOLE_STARTER_SUGGESTIONS = [
|
|
59
|
+
"Start a research assignment",
|
|
60
|
+
"Start a reporting assignment",
|
|
61
|
+
"Discuss a reference",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
const CONSOLE_THREAD_ANCHOR_KEY = "site#papyrus";
|
|
65
|
+
const CONSOLE_NEWSROOM_FEED_KEY = "consoleChat";
|
|
66
|
+
const CONSOLE_MESSAGE_KIND = "console_chat_turn";
|
|
67
|
+
const CONSOLE_TOOL_CALL_KIND = "console_tool_call";
|
|
68
|
+
const CONSOLE_TOOL_RESULT_KIND = "console_tool_result";
|
|
69
|
+
const CONSOLE_MESSAGE_DOMAIN = "conversation";
|
|
70
|
+
const CONSOLE_SEMANTIC_LAYER = "chat_detail";
|
|
71
|
+
const CONSOLE_SEARCH_VISIBILITY = "explicit";
|
|
72
|
+
const CONSOLE_RESPONSE_TARGET = process.env.NEXT_PUBLIC_PAPYRUS_CONSOLE_RESPONSE_TARGET?.trim() || "cloud";
|
|
73
|
+
const DEFAULT_CONSOLE_MODEL = process.env.NEXT_PUBLIC_PAPYRUS_CONSOLE_MODEL?.trim() || "gpt-5.4-mini";
|
|
74
|
+
const CONSOLE_SELECT_THREAD_EVENT = "papyrus:console-select-thread";
|
|
75
|
+
const ASSISTANT_AVATAR_SIZE_PX = 32;
|
|
76
|
+
const ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR = "rgb(var(--ink-solid-rgb) / var(--ink-display-alpha))";
|
|
77
|
+
const ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR = "rgb(var(--paper-rgb) / 1)";
|
|
78
|
+
const ASSISTANT_AVATAR_ERROR_SHADOW_COLOR = ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR;
|
|
79
|
+
const ASSISTANT_AVATAR_ERROR_LIGHT_COLOR = ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR;
|
|
80
|
+
type VultusBotAvatarProps = {
|
|
81
|
+
ariaLabel?: string;
|
|
82
|
+
lightColor?: string;
|
|
83
|
+
neutralIdleMode?: "bored-random" | "static";
|
|
84
|
+
shadowColor?: string;
|
|
85
|
+
size?: number;
|
|
86
|
+
state?: BotAvatarState;
|
|
87
|
+
transitionDurationSeconds?: number;
|
|
88
|
+
};
|
|
89
|
+
const VULTUS_BOT_AVATAR_FALLBACK: ComponentType<VultusBotAvatarProps> = () => null;
|
|
90
|
+
const VultusBotAvatar = dynamic<VultusBotAvatarProps>(
|
|
91
|
+
() => (
|
|
92
|
+
import("anthus-vultus")
|
|
93
|
+
.then((module) => module.BotAvatar as ComponentType<VultusBotAvatarProps>)
|
|
94
|
+
.catch((error) => {
|
|
95
|
+
console.error("[PapyrusConsole] Failed to load anthus-vultus BotAvatar", error);
|
|
96
|
+
return VULTUS_BOT_AVATAR_FALLBACK;
|
|
97
|
+
})
|
|
98
|
+
),
|
|
99
|
+
{ ssr: false },
|
|
100
|
+
);
|
|
101
|
+
const CONSOLE_MODEL_OPTIONS = [
|
|
102
|
+
{ value: "gpt-5.5", label: "GPT 5.5", provider: "openai" },
|
|
103
|
+
{ value: "gpt-5.4", label: "GPT 5.4", provider: "openai" },
|
|
104
|
+
{ value: "gpt-5.4-mini", label: "GPT 5.4 Mini", provider: "openai" },
|
|
105
|
+
{ value: "gpt-5.4-nano", label: "GPT 5.4 Nano", provider: "openai" },
|
|
106
|
+
] as const;
|
|
107
|
+
|
|
108
|
+
type PapyrusConsoleShellProps = {
|
|
109
|
+
children: ReactNode;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type PapyrusConsoleContextValue = {
|
|
113
|
+
open: boolean;
|
|
114
|
+
openingReferenceChat: boolean;
|
|
115
|
+
openConsole: () => void;
|
|
116
|
+
shouldOfferConsole: boolean;
|
|
117
|
+
startReferenceChat: (target: NewsroomKnowledgeQueryTarget) => Promise<void>;
|
|
118
|
+
toggleOpen: () => void;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type DisplayConsoleMessage = ConsoleChatMessage & {
|
|
122
|
+
pendingPlaceholder?: boolean;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type ConsoleToolTimelineEntry = {
|
|
126
|
+
id: string;
|
|
127
|
+
firstSequence: number;
|
|
128
|
+
firstCreatedAt: string;
|
|
129
|
+
messageKind: string;
|
|
130
|
+
role: "TOOL";
|
|
131
|
+
toolCallId: string;
|
|
132
|
+
toolName: string;
|
|
133
|
+
callMessage?: DisplayConsoleMessage;
|
|
134
|
+
resultMessage?: DisplayConsoleMessage;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type ConsoleTimelineEntry =
|
|
138
|
+
| { kind: "message"; id: string; message: DisplayConsoleMessage }
|
|
139
|
+
| { kind: "tool"; id: string; tool: ConsoleToolTimelineEntry };
|
|
140
|
+
|
|
141
|
+
type AssistantAvatarTone = "default" | "error";
|
|
142
|
+
type ConsoleAuthorIdentity = {
|
|
143
|
+
avatarHash: string | null;
|
|
144
|
+
email: string | null;
|
|
145
|
+
userProfileId: string | null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const PapyrusConsoleContext = createContext<PapyrusConsoleContextValue | null>(null);
|
|
149
|
+
|
|
150
|
+
export function PapyrusConsoleShell({ children }: PapyrusConsoleShellProps) {
|
|
151
|
+
const pathname = usePathname();
|
|
152
|
+
const [session, setSession] = useState<ReaderSessionSnapshot | null>(null);
|
|
153
|
+
const [open, setOpen] = useState(false);
|
|
154
|
+
const [openingReferenceChat, setOpeningReferenceChat] = useState(false);
|
|
155
|
+
const isNewsroomPath = pathname === "/newsroom" || pathname.startsWith("/newsroom/");
|
|
156
|
+
const canUseConsole = Boolean(session?.hasSession && session.groups.some((group) => group === "editor" || group === "admin"));
|
|
157
|
+
const shouldOfferConsole = isNewsroomPath && canUseConsole;
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
let active = true;
|
|
161
|
+
void loadReaderSessionSnapshot()
|
|
162
|
+
.then((snapshot) => {
|
|
163
|
+
if (active) setSession(snapshot);
|
|
164
|
+
})
|
|
165
|
+
.catch(() => {
|
|
166
|
+
if (active) setSession({ auth: { status: "signedOut", label: "Signed out" }, groups: [], hasSession: false });
|
|
167
|
+
});
|
|
168
|
+
try {
|
|
169
|
+
setOpen(window.localStorage.getItem("papyrus:console-open") === "true");
|
|
170
|
+
} catch {
|
|
171
|
+
setOpen(false);
|
|
172
|
+
}
|
|
173
|
+
return () => {
|
|
174
|
+
active = false;
|
|
175
|
+
};
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
const toggleOpen = useCallback(() => {
|
|
179
|
+
setOpen((current) => {
|
|
180
|
+
const next = !current;
|
|
181
|
+
try {
|
|
182
|
+
window.localStorage.setItem("papyrus:console-open", String(next));
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore storage failures.
|
|
185
|
+
}
|
|
186
|
+
return next;
|
|
187
|
+
});
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
const openConsole = useCallback(() => {
|
|
191
|
+
setOpen(true);
|
|
192
|
+
try {
|
|
193
|
+
window.localStorage.setItem("papyrus:console-open", "true");
|
|
194
|
+
} catch {
|
|
195
|
+
// Ignore storage failures.
|
|
196
|
+
}
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
const startReferenceChat = useCallback(async (target: NewsroomKnowledgeQueryTarget) => {
|
|
200
|
+
if (!canUseConsole) {
|
|
201
|
+
throw new Error("Chat is only available to editor/admin sessions.");
|
|
202
|
+
}
|
|
203
|
+
if (openingReferenceChat) return;
|
|
204
|
+
setOpeningReferenceChat(true);
|
|
205
|
+
try {
|
|
206
|
+
openConsole();
|
|
207
|
+
const now = new Date().toISOString();
|
|
208
|
+
const title = target.title?.trim() || "Reference";
|
|
209
|
+
const summary = `Reference chat for ${title}`.slice(0, 180);
|
|
210
|
+
const lineageId = target.anchor.lineageId ?? target.anchor.id;
|
|
211
|
+
const thread = await createConsoleThread({
|
|
212
|
+
id: `message-thread-console-${crypto.randomUUID()}`,
|
|
213
|
+
threadKind: "console",
|
|
214
|
+
status: "active",
|
|
215
|
+
title: "Reference Chat",
|
|
216
|
+
summary,
|
|
217
|
+
primaryAnchorKind: "site",
|
|
218
|
+
primaryAnchorId: "papyrus",
|
|
219
|
+
primaryAnchorLineageId: "papyrus",
|
|
220
|
+
primaryAnchorKey: CONSOLE_THREAD_ANCHOR_KEY,
|
|
221
|
+
createdByLabel: session?.auth.label ?? "Papyrus editor",
|
|
222
|
+
messageCount: 0,
|
|
223
|
+
metadata: toAwsJson({
|
|
224
|
+
console: {
|
|
225
|
+
cache: "lambda-tmp-jit",
|
|
226
|
+
model: DEFAULT_CONSOLE_MODEL,
|
|
227
|
+
rawChatSearchVisibility: CONSOLE_SEARCH_VISIBILITY,
|
|
228
|
+
},
|
|
229
|
+
bootstrap: {
|
|
230
|
+
mode: "reference_chat",
|
|
231
|
+
anchor: target.anchor,
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
235
|
+
createdAt: now,
|
|
236
|
+
updatedAt: now,
|
|
237
|
+
});
|
|
238
|
+
if (typeof window !== "undefined") {
|
|
239
|
+
window.dispatchEvent(new CustomEvent(CONSOLE_SELECT_THREAD_EVENT, { detail: { threadId: thread.id } }));
|
|
240
|
+
}
|
|
241
|
+
const pendingMessage = await createConsoleMessage({
|
|
242
|
+
id: `message-console-assistant-${crypto.randomUUID()}`,
|
|
243
|
+
threadId: thread.id,
|
|
244
|
+
parentMessageId: null,
|
|
245
|
+
sequenceNumber: 1,
|
|
246
|
+
role: "ASSISTANT",
|
|
247
|
+
messageKind: CONSOLE_MESSAGE_KIND,
|
|
248
|
+
messageType: "MESSAGE",
|
|
249
|
+
content: "",
|
|
250
|
+
summary: "Thinking...",
|
|
251
|
+
messageDomain: CONSOLE_MESSAGE_DOMAIN,
|
|
252
|
+
status: "active",
|
|
253
|
+
source: "papyrus-console",
|
|
254
|
+
authorLabel: "Papyrus",
|
|
255
|
+
semanticLayer: CONSOLE_SEMANTIC_LAYER,
|
|
256
|
+
searchVisibility: CONSOLE_SEARCH_VISIBILITY,
|
|
257
|
+
responseTarget: CONSOLE_RESPONSE_TARGET,
|
|
258
|
+
responseStatus: "RUNNING",
|
|
259
|
+
responseStartedAt: now,
|
|
260
|
+
metadata: toAwsJson({
|
|
261
|
+
model: DEFAULT_CONSOLE_MODEL,
|
|
262
|
+
bootstrap: "reference-chat",
|
|
263
|
+
anchor: target.anchor,
|
|
264
|
+
}),
|
|
265
|
+
createdAt: now,
|
|
266
|
+
updatedAt: now,
|
|
267
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
268
|
+
});
|
|
269
|
+
await updateConsoleThread({
|
|
270
|
+
id: thread.id,
|
|
271
|
+
messageCount: 1,
|
|
272
|
+
lastMessageId: pendingMessage.id,
|
|
273
|
+
lastMessageAt: now,
|
|
274
|
+
activeResponseMessageId: pendingMessage.id,
|
|
275
|
+
updatedAt: now,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const tokenBudget = 1600;
|
|
279
|
+
const request = buildNewsroomKnowledgeQueryInput(target, "", tokenBudget);
|
|
280
|
+
const completedAt = new Date().toISOString();
|
|
281
|
+
let markdown: string;
|
|
282
|
+
try {
|
|
283
|
+
const result = await withTimeout(
|
|
284
|
+
runNewsroomKnowledgeQuery(request),
|
|
285
|
+
45_000,
|
|
286
|
+
"Reference context preload timed out.",
|
|
287
|
+
);
|
|
288
|
+
markdown = result.context?.text?.trim() ?? "";
|
|
289
|
+
if (!markdown) throw new Error("knowledgeQuery returned no markdown context.");
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
292
|
+
markdown = `Unable to preload reference context.\n\n${message}`;
|
|
293
|
+
}
|
|
294
|
+
let completedMessage: ConsoleChatMessage;
|
|
295
|
+
try {
|
|
296
|
+
completedMessage = await updateConsoleMessage({
|
|
297
|
+
id: pendingMessage.id,
|
|
298
|
+
content: markdown,
|
|
299
|
+
summary: truncateConsoleSummary(markdown),
|
|
300
|
+
responseStatus: "COMPLETED",
|
|
301
|
+
responseCompletedAt: completedAt,
|
|
302
|
+
updatedAt: completedAt,
|
|
303
|
+
});
|
|
304
|
+
} catch {
|
|
305
|
+
completedMessage = await createConsoleMessage({
|
|
306
|
+
id: `message-console-assistant-${crypto.randomUUID()}`,
|
|
307
|
+
threadId: thread.id,
|
|
308
|
+
parentMessageId: pendingMessage.id,
|
|
309
|
+
sequenceNumber: 2,
|
|
310
|
+
role: "ASSISTANT",
|
|
311
|
+
messageKind: CONSOLE_MESSAGE_KIND,
|
|
312
|
+
messageType: "MESSAGE",
|
|
313
|
+
content: markdown,
|
|
314
|
+
summary: truncateConsoleSummary(markdown),
|
|
315
|
+
messageDomain: CONSOLE_MESSAGE_DOMAIN,
|
|
316
|
+
status: "active",
|
|
317
|
+
source: "papyrus-console",
|
|
318
|
+
authorLabel: "Papyrus",
|
|
319
|
+
semanticLayer: CONSOLE_SEMANTIC_LAYER,
|
|
320
|
+
searchVisibility: CONSOLE_SEARCH_VISIBILITY,
|
|
321
|
+
responseTarget: CONSOLE_RESPONSE_TARGET,
|
|
322
|
+
responseStatus: "COMPLETED",
|
|
323
|
+
responseCompletedAt: completedAt,
|
|
324
|
+
metadata: toAwsJson({
|
|
325
|
+
model: DEFAULT_CONSOLE_MODEL,
|
|
326
|
+
bootstrap: "reference-chat",
|
|
327
|
+
anchor: target.anchor,
|
|
328
|
+
}),
|
|
329
|
+
createdAt: completedAt,
|
|
330
|
+
updatedAt: completedAt,
|
|
331
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
await updateConsoleThread({
|
|
335
|
+
id: thread.id,
|
|
336
|
+
messageCount: 1,
|
|
337
|
+
lastMessageId: completedMessage.id,
|
|
338
|
+
lastMessageAt: completedAt,
|
|
339
|
+
activeResponseMessageId: null,
|
|
340
|
+
updatedAt: completedAt,
|
|
341
|
+
});
|
|
342
|
+
if (typeof window !== "undefined") {
|
|
343
|
+
window.dispatchEvent(new CustomEvent(CONSOLE_SELECT_THREAD_EVENT, { detail: { threadId: thread.id } }));
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
setOpeningReferenceChat(false);
|
|
347
|
+
}
|
|
348
|
+
}, [canUseConsole, openConsole, openingReferenceChat, session?.auth.label]);
|
|
349
|
+
|
|
350
|
+
const consoleContext = useMemo<PapyrusConsoleContextValue>(() => ({
|
|
351
|
+
open,
|
|
352
|
+
openingReferenceChat,
|
|
353
|
+
openConsole,
|
|
354
|
+
shouldOfferConsole,
|
|
355
|
+
startReferenceChat,
|
|
356
|
+
toggleOpen,
|
|
357
|
+
}), [open, openConsole, openingReferenceChat, shouldOfferConsole, startReferenceChat, toggleOpen]);
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<PapyrusConsoleContext.Provider value={consoleContext}>
|
|
361
|
+
<div className={canUseConsole && open ? "papyrus-console-page papyrus-console-page--open" : "papyrus-console-page"}>
|
|
362
|
+
{children}
|
|
363
|
+
</div>
|
|
364
|
+
{shouldOfferConsole ? (
|
|
365
|
+
<aside className={open ? "papyrus-console papyrus-console--open" : "papyrus-console"} aria-label="Papyrus console">
|
|
366
|
+
{open ? (
|
|
367
|
+
canUseConsole
|
|
368
|
+
? <ConsolePanel actorEmail={session?.auth.status === "signedIn" ? (session.auth.email ?? null) : null} actorLabel={session?.auth.label ?? "Papyrus editor"} onClose={toggleOpen} />
|
|
369
|
+
: <ConsoleAccessPanel onClose={toggleOpen} session={session} />
|
|
370
|
+
) : null}
|
|
371
|
+
</aside>
|
|
372
|
+
) : null}
|
|
373
|
+
</PapyrusConsoleContext.Provider>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function NewsroomConsoleProgressToggle() {
|
|
378
|
+
const consoleContext = usePapyrusConsole();
|
|
379
|
+
if (!consoleContext?.shouldOfferConsole || consoleContext.open) return null;
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
className="edition-progress__button edition-progress__button--next edition-progress__button--console"
|
|
385
|
+
aria-expanded={false}
|
|
386
|
+
aria-label="Open console"
|
|
387
|
+
onClick={consoleContext.toggleOpen}
|
|
388
|
+
title="Open console"
|
|
389
|
+
>
|
|
390
|
+
<PapyrusConsoleChatIcon />
|
|
391
|
+
</button>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function usePapyrusConsole(): PapyrusConsoleContextValue | null {
|
|
396
|
+
return useContext(PapyrusConsoleContext);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function ConsoleAccessPanel({ onClose, session }: { onClose: () => void; session: ReaderSessionSnapshot | null }) {
|
|
400
|
+
return (
|
|
401
|
+
<div className="papyrus-console__panel papyrus-console__panel--access">
|
|
402
|
+
<ConsolePanelHeader onClose={onClose} />
|
|
403
|
+
<div className="papyrus-console__empty">
|
|
404
|
+
<strong>Checking Newsroom credentials.</strong>
|
|
405
|
+
<span>
|
|
406
|
+
The Newsroom is visible, but this console is waiting for the client session groups.
|
|
407
|
+
Refresh after the current deployment finishes if this does not resolve.
|
|
408
|
+
</span>
|
|
409
|
+
<span>Groups: {session?.groups.length ? session.groups.join(", ") : "none loaded"}</span>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string | null; actorLabel: string; onClose: () => void }) {
|
|
416
|
+
const router = useRouter();
|
|
417
|
+
const pathname = usePathname();
|
|
418
|
+
const searchParams = useSearchParams();
|
|
419
|
+
const [thread, setThread] = useState<ConsoleChatThread | null>(null);
|
|
420
|
+
const [threads, setThreads] = useState<ConsoleChatThread[]>([]);
|
|
421
|
+
const [messages, setMessages] = useState<ConsoleChatMessage[]>([]);
|
|
422
|
+
const [draft, setDraft] = useState("");
|
|
423
|
+
const [loading, setLoading] = useState(false);
|
|
424
|
+
const [sending, setSending] = useState(false);
|
|
425
|
+
const [error, setError] = useState<string | null>(null);
|
|
426
|
+
const [selectedModel, setSelectedModel] = useState(DEFAULT_CONSOLE_MODEL);
|
|
427
|
+
const [motionSetting, setMotionSetting] = useState<ReaderMotionSetting>("standard");
|
|
428
|
+
const [authorIdentity, setAuthorIdentity] = useState<ConsoleAuthorIdentity>({ avatarHash: null, email: null, userProfileId: null });
|
|
429
|
+
const [fallbackAvatarHash, setFallbackAvatarHash] = useState<string | null>(null);
|
|
430
|
+
const hasLoadedRef = useRef(false);
|
|
431
|
+
const activeThreadIdRef = useRef<string | null>(null);
|
|
432
|
+
const threadRef = useRef<ConsoleChatThread | null>(null);
|
|
433
|
+
const handledNavigationMessageIdsRef = useRef<Set<string>>(new Set());
|
|
434
|
+
|
|
435
|
+
const currentWebPath = useMemo(() => {
|
|
436
|
+
const path = pathname ?? "/newsroom";
|
|
437
|
+
const query = searchParams.toString();
|
|
438
|
+
return query ? `${path}?${query}` : path;
|
|
439
|
+
}, [pathname, searchParams]);
|
|
440
|
+
|
|
441
|
+
const currentWebUi = useMemo(() => buildWebUiContext(currentWebPath), [currentWebPath]);
|
|
442
|
+
const currentWebUiKey = useMemo(() => webUiContextKey(currentWebUi), [currentWebUi]);
|
|
443
|
+
const recordingContextRef = useRef<string | null>(null);
|
|
444
|
+
const [hydratedThreadId, setHydratedThreadId] = useState<string | null>(null);
|
|
445
|
+
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
for (const message of messages) {
|
|
448
|
+
if (message.messageKind !== CONSOLE_TOOL_RESULT_KIND) continue;
|
|
449
|
+
if (handledNavigationMessageIdsRef.current.has(message.id)) continue;
|
|
450
|
+
const metadata = readAwsJsonObject(message.metadata);
|
|
451
|
+
const navigation = extractNavigationIntent(readNestedValue(metadata, ["toolResultJson"]));
|
|
452
|
+
if (!navigation) continue;
|
|
453
|
+
handledNavigationMessageIdsRef.current.add(message.id);
|
|
454
|
+
if (currentWebPath !== navigation.webPath) {
|
|
455
|
+
router.push(navigation.webPath);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}, [currentWebPath, messages, router]);
|
|
459
|
+
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
setMotionSetting(readLocalReaderSettings().motion);
|
|
462
|
+
return subscribeReaderSettingsChanges((settings) => {
|
|
463
|
+
setMotionSetting(settings.motion);
|
|
464
|
+
});
|
|
465
|
+
}, []);
|
|
466
|
+
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
let active = true;
|
|
469
|
+
void resolveReaderSettings()
|
|
470
|
+
.then((resolution) => {
|
|
471
|
+
if (!active) return;
|
|
472
|
+
setAuthorIdentity({
|
|
473
|
+
avatarHash: resolution.avatarHash ?? null,
|
|
474
|
+
email: resolution.email ?? actorEmail ?? null,
|
|
475
|
+
userProfileId: resolution.userProfileId ?? null,
|
|
476
|
+
});
|
|
477
|
+
})
|
|
478
|
+
.catch(() => {
|
|
479
|
+
if (!active) return;
|
|
480
|
+
setAuthorIdentity({ avatarHash: null, email: actorEmail ?? null, userProfileId: null });
|
|
481
|
+
});
|
|
482
|
+
return () => {
|
|
483
|
+
active = false;
|
|
484
|
+
};
|
|
485
|
+
}, [actorEmail]);
|
|
486
|
+
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
let active = true;
|
|
489
|
+
if (!actorEmail) {
|
|
490
|
+
setFallbackAvatarHash(null);
|
|
491
|
+
return () => {
|
|
492
|
+
active = false;
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
void computeAvatarHashFromEmail(actorEmail)
|
|
496
|
+
.then((hash) => {
|
|
497
|
+
if (active) setFallbackAvatarHash(hash);
|
|
498
|
+
})
|
|
499
|
+
.catch(() => {
|
|
500
|
+
if (active) setFallbackAvatarHash(null);
|
|
501
|
+
});
|
|
502
|
+
return () => {
|
|
503
|
+
active = false;
|
|
504
|
+
};
|
|
505
|
+
}, [actorEmail]);
|
|
506
|
+
|
|
507
|
+
const effectiveAuthorIdentity = useMemo<ConsoleAuthorIdentity>(() => ({
|
|
508
|
+
avatarHash: authorIdentity.avatarHash ?? fallbackAvatarHash,
|
|
509
|
+
email: authorIdentity.email ?? actorEmail ?? null,
|
|
510
|
+
userProfileId: authorIdentity.userProfileId,
|
|
511
|
+
}), [actorEmail, authorIdentity.avatarHash, authorIdentity.email, authorIdentity.userProfileId, fallbackAvatarHash]);
|
|
512
|
+
|
|
513
|
+
const sortedMessages = useMemo(() => (
|
|
514
|
+
[...messages].sort((left, right) => (left.sequenceNumber ?? 0) - (right.sequenceNumber ?? 0) || left.createdAt.localeCompare(right.createdAt))
|
|
515
|
+
), [messages]);
|
|
516
|
+
|
|
517
|
+
const mergeMessage = useCallback((message: ConsoleChatMessage) => {
|
|
518
|
+
setMessages((current) => upsertConsoleMessage(current, message));
|
|
519
|
+
}, []);
|
|
520
|
+
|
|
521
|
+
const appendConsoleWebContextMessage = useCallback(async (
|
|
522
|
+
activeThread: ConsoleChatThread,
|
|
523
|
+
webUi: PapyrusWebUiContext,
|
|
524
|
+
event: ConsoleWebContextEvent,
|
|
525
|
+
) => {
|
|
526
|
+
const { visibleContent, agentInstructions, summary } = formatConsoleWebContextMessage(webUi, event);
|
|
527
|
+
const sequenceNumber = (activeThread.messageCount ?? 0) + 1;
|
|
528
|
+
const now = new Date().toISOString();
|
|
529
|
+
const messageMetadata = {
|
|
530
|
+
console: {
|
|
531
|
+
webUi,
|
|
532
|
+
contextEvent: event,
|
|
533
|
+
agentInstructions,
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
const message: ConsoleChatMessage = {
|
|
537
|
+
id: `message-console-context-${crypto.randomUUID()}`,
|
|
538
|
+
threadId: activeThread.id,
|
|
539
|
+
parentMessageId: activeThread.lastMessageId ?? null,
|
|
540
|
+
sequenceNumber,
|
|
541
|
+
role: "SYSTEM",
|
|
542
|
+
messageKind: CONSOLE_WEB_CONTEXT_MESSAGE_KIND,
|
|
543
|
+
messageType: "MESSAGE",
|
|
544
|
+
content: visibleContent,
|
|
545
|
+
summary,
|
|
546
|
+
responseStatus: "COMPLETED",
|
|
547
|
+
responseCompletedAt: now,
|
|
548
|
+
metadata: messageMetadata,
|
|
549
|
+
createdAt: now,
|
|
550
|
+
};
|
|
551
|
+
mergeMessage(message);
|
|
552
|
+
const persisted = await createConsoleMessage({
|
|
553
|
+
...message,
|
|
554
|
+
messageDomain: CONSOLE_MESSAGE_DOMAIN,
|
|
555
|
+
status: "active",
|
|
556
|
+
source: "papyrus-console",
|
|
557
|
+
authorLabel: actorLabel,
|
|
558
|
+
semanticLayer: CONSOLE_SEMANTIC_LAYER,
|
|
559
|
+
searchVisibility: CONSOLE_SEARCH_VISIBILITY,
|
|
560
|
+
responseTarget: CONSOLE_RESPONSE_TARGET,
|
|
561
|
+
metadata: toAwsJson(messageMetadata),
|
|
562
|
+
updatedAt: now,
|
|
563
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
564
|
+
});
|
|
565
|
+
mergeMessage({ ...message, ...persisted });
|
|
566
|
+
await updateConsoleThread({
|
|
567
|
+
id: activeThread.id,
|
|
568
|
+
messageCount: sequenceNumber,
|
|
569
|
+
lastMessageId: persisted.id,
|
|
570
|
+
lastMessageAt: now,
|
|
571
|
+
updatedAt: now,
|
|
572
|
+
});
|
|
573
|
+
setThread((current) => current?.id === activeThread.id ? {
|
|
574
|
+
...current,
|
|
575
|
+
messageCount: sequenceNumber,
|
|
576
|
+
lastMessageId: persisted.id,
|
|
577
|
+
lastMessageAt: now,
|
|
578
|
+
updatedAt: now,
|
|
579
|
+
} : current);
|
|
580
|
+
recordingContextRef.current = webUiContextKey(webUi);
|
|
581
|
+
return persisted;
|
|
582
|
+
}, [actorLabel, mergeMessage]);
|
|
583
|
+
|
|
584
|
+
const recordWebContextIfNeeded = useCallback(async (
|
|
585
|
+
activeThread: ConsoleChatThread,
|
|
586
|
+
webUi: PapyrusWebUiContext,
|
|
587
|
+
event: ConsoleWebContextEvent,
|
|
588
|
+
) => {
|
|
589
|
+
const nextKey = webUiContextKey(webUi);
|
|
590
|
+
if (!nextKey || recordingContextRef.current === nextKey) return;
|
|
591
|
+
const lastContextMessage = [...sortedMessages].reverse().find((entry) => entry.messageKind === CONSOLE_WEB_CONTEXT_MESSAGE_KIND);
|
|
592
|
+
const lastRecorded = lastContextMessage
|
|
593
|
+
? readConsoleWebUiFromMessageMetadata(readAwsJsonObject(lastContextMessage.metadata))
|
|
594
|
+
: null;
|
|
595
|
+
if (lastRecorded && webUiContextKey(lastRecorded) === nextKey) {
|
|
596
|
+
recordingContextRef.current = nextKey;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
await appendConsoleWebContextMessage(activeThread, webUi, event);
|
|
600
|
+
}, [appendConsoleWebContextMessage, sortedMessages]);
|
|
601
|
+
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
threadRef.current = thread;
|
|
604
|
+
}, [thread]);
|
|
605
|
+
|
|
606
|
+
useEffect(() => {
|
|
607
|
+
recordingContextRef.current = null;
|
|
608
|
+
}, [thread?.id]);
|
|
609
|
+
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
if (!thread?.id || hydratedThreadId !== thread.id || !currentWebUi.papyrusLocationUri) return;
|
|
612
|
+
const hasPriorContext = sortedMessages.some((entry) => entry.messageKind === CONSOLE_WEB_CONTEXT_MESSAGE_KIND);
|
|
613
|
+
void recordWebContextIfNeeded(
|
|
614
|
+
thread,
|
|
615
|
+
currentWebUi,
|
|
616
|
+
hasPriorContext ? "navigation" : "session_start",
|
|
617
|
+
).catch((error) => {
|
|
618
|
+
console.error("[PapyrusConsole] Unable to record web UI context", error);
|
|
619
|
+
});
|
|
620
|
+
}, [currentWebUi, currentWebUiKey, hydratedThreadId, recordWebContextIfNeeded, sortedMessages, thread]);
|
|
621
|
+
|
|
622
|
+
const loadMessages = useCallback(async (threadId: string) => {
|
|
623
|
+
setHydratedThreadId(null);
|
|
624
|
+
const nextMessages = await listConsoleMessagesByThread(threadId);
|
|
625
|
+
setMessages(nextMessages);
|
|
626
|
+
setHydratedThreadId(threadId);
|
|
627
|
+
}, []);
|
|
628
|
+
|
|
629
|
+
const loadThreads = useCallback(async () => {
|
|
630
|
+
return listConsoleThreads(CONSOLE_THREAD_ANCHOR_KEY, 25);
|
|
631
|
+
}, []);
|
|
632
|
+
|
|
633
|
+
const resolveActiveThread = useCallback((
|
|
634
|
+
recentThreads: ConsoleChatThread[],
|
|
635
|
+
currentThread: ConsoleChatThread | null,
|
|
636
|
+
): ConsoleChatThread | null => {
|
|
637
|
+
const preferredId = activeThreadIdRef.current ?? currentThread?.id ?? null;
|
|
638
|
+
if (preferredId) {
|
|
639
|
+
const match = recentThreads.find((entry) => entry.id === preferredId);
|
|
640
|
+
if (match) return match;
|
|
641
|
+
if (currentThread?.id === preferredId) return currentThread;
|
|
642
|
+
}
|
|
643
|
+
const fallback = recentThreads[0] ?? null;
|
|
644
|
+
if (fallback) activeThreadIdRef.current = fallback.id;
|
|
645
|
+
return fallback;
|
|
646
|
+
}, []);
|
|
647
|
+
|
|
648
|
+
const loadThread = useCallback(async () => {
|
|
649
|
+
const initialLoad = !hasLoadedRef.current;
|
|
650
|
+
if (initialLoad) setLoading(true);
|
|
651
|
+
setError(null);
|
|
652
|
+
try {
|
|
653
|
+
const recentThreads = await loadThreads();
|
|
654
|
+
const nextThread = resolveActiveThread(recentThreads, threadRef.current);
|
|
655
|
+
setThreads(recentThreads);
|
|
656
|
+
setThread(nextThread);
|
|
657
|
+
setSelectedModel(resolveThreadModel(nextThread));
|
|
658
|
+
if (nextThread) {
|
|
659
|
+
await loadMessages(nextThread.id);
|
|
660
|
+
} else {
|
|
661
|
+
setMessages([]);
|
|
662
|
+
}
|
|
663
|
+
} catch (nextError) {
|
|
664
|
+
const normalizedError = formatUnknownConsoleError(nextError);
|
|
665
|
+
console.error("[PapyrusConsole] Unable to load console thread", normalizedError, nextError);
|
|
666
|
+
if (initialLoad) setError(normalizedError);
|
|
667
|
+
} finally {
|
|
668
|
+
hasLoadedRef.current = true;
|
|
669
|
+
if (initialLoad) setLoading(false);
|
|
670
|
+
}
|
|
671
|
+
}, [loadMessages, loadThreads, resolveActiveThread]);
|
|
672
|
+
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
void loadThread();
|
|
675
|
+
}, [loadThread]);
|
|
676
|
+
|
|
677
|
+
useEffect(() => {
|
|
678
|
+
if (!thread?.id) return;
|
|
679
|
+
const activeThreadId = thread.id;
|
|
680
|
+
const unsubscribe = subscribeConsoleMessages(
|
|
681
|
+
activeThreadId,
|
|
682
|
+
(message) => {
|
|
683
|
+
if (message.threadId && message.threadId !== activeThreadIdRef.current) return;
|
|
684
|
+
mergeMessage(message);
|
|
685
|
+
},
|
|
686
|
+
(nextError) => {
|
|
687
|
+
console.warn("[PapyrusConsole] Console message subscription failed", nextError);
|
|
688
|
+
},
|
|
689
|
+
);
|
|
690
|
+
return unsubscribe;
|
|
691
|
+
}, [mergeMessage, thread?.id]);
|
|
692
|
+
|
|
693
|
+
useEffect(() => {
|
|
694
|
+
if (typeof window === "undefined") return;
|
|
695
|
+
const onSelectThread = (event: Event) => {
|
|
696
|
+
const detail = (event as CustomEvent<{ threadId?: string }>).detail;
|
|
697
|
+
const threadId = detail?.threadId?.trim();
|
|
698
|
+
if (!threadId) return;
|
|
699
|
+
activeThreadIdRef.current = threadId;
|
|
700
|
+
const currentMatch = threads.find((entry) => entry.id === threadId) ?? null;
|
|
701
|
+
if (currentMatch) {
|
|
702
|
+
setThread(currentMatch);
|
|
703
|
+
setSelectedModel(resolveThreadModel(currentMatch));
|
|
704
|
+
void loadMessages(threadId);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
void loadThread();
|
|
708
|
+
};
|
|
709
|
+
window.addEventListener(CONSOLE_SELECT_THREAD_EVENT, onSelectThread as EventListener);
|
|
710
|
+
return () => {
|
|
711
|
+
window.removeEventListener(CONSOLE_SELECT_THREAD_EVENT, onSelectThread as EventListener);
|
|
712
|
+
};
|
|
713
|
+
}, [loadMessages, loadThread, threads]);
|
|
714
|
+
|
|
715
|
+
useEffect(() => {
|
|
716
|
+
let previousState: ConnectionState | null = null;
|
|
717
|
+
const unsubscribe = Hub.listen("api", ({ payload }) => {
|
|
718
|
+
if (payload.event !== CONNECTION_STATE_CHANGE) return;
|
|
719
|
+
const data = payload.data as { connectionState?: ConnectionState } | undefined;
|
|
720
|
+
const nextState = data?.connectionState;
|
|
721
|
+
if (previousState && previousState !== ConnectionState.Connected && nextState === ConnectionState.Connected) {
|
|
722
|
+
const activeThreadId = activeThreadIdRef.current;
|
|
723
|
+
if (activeThreadId) void loadMessages(activeThreadId);
|
|
724
|
+
}
|
|
725
|
+
previousState = nextState ?? previousState;
|
|
726
|
+
});
|
|
727
|
+
return () => unsubscribe();
|
|
728
|
+
}, [loadMessages]);
|
|
729
|
+
|
|
730
|
+
const selectThread = useCallback((threadId: string) => {
|
|
731
|
+
activeThreadIdRef.current = threadId;
|
|
732
|
+
const nextThread = threads.find((entry) => entry.id === threadId) ?? null;
|
|
733
|
+
setThread(nextThread);
|
|
734
|
+
setSelectedModel(resolveThreadModel(nextThread));
|
|
735
|
+
setMessages([]);
|
|
736
|
+
setHydratedThreadId(null);
|
|
737
|
+
if (nextThread) void loadMessages(nextThread.id);
|
|
738
|
+
}, [loadMessages, threads]);
|
|
739
|
+
|
|
740
|
+
async function createNewThread(): Promise<ConsoleChatThread> {
|
|
741
|
+
const now = new Date().toISOString();
|
|
742
|
+
const nextThread: ConsoleChatThread = {
|
|
743
|
+
id: `message-thread-console-${crypto.randomUUID()}`,
|
|
744
|
+
threadKind: "console",
|
|
745
|
+
status: "active",
|
|
746
|
+
title: "Papyrus Console",
|
|
747
|
+
summary: "General editor console thread for Papyrus.",
|
|
748
|
+
primaryAnchorKind: "site",
|
|
749
|
+
primaryAnchorId: "papyrus",
|
|
750
|
+
primaryAnchorLineageId: "papyrus",
|
|
751
|
+
primaryAnchorKey: CONSOLE_THREAD_ANCHOR_KEY,
|
|
752
|
+
createdByLabel: actorLabel,
|
|
753
|
+
messageCount: 0,
|
|
754
|
+
metadata: toAwsJson({
|
|
755
|
+
console: {
|
|
756
|
+
cache: "lambda-tmp-jit",
|
|
757
|
+
model: selectedModel,
|
|
758
|
+
rawChatSearchVisibility: CONSOLE_SEARCH_VISIBILITY,
|
|
759
|
+
},
|
|
760
|
+
}),
|
|
761
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
762
|
+
createdAt: now,
|
|
763
|
+
updatedAt: now,
|
|
764
|
+
};
|
|
765
|
+
activeThreadIdRef.current = nextThread.id;
|
|
766
|
+
setThread(nextThread);
|
|
767
|
+
setThreads((current) => [nextThread, ...current]);
|
|
768
|
+
setMessages([]);
|
|
769
|
+
setHydratedThreadId(null);
|
|
770
|
+
const created = await createConsoleThread(nextThread);
|
|
771
|
+
activeThreadIdRef.current = created.id;
|
|
772
|
+
setThread(created);
|
|
773
|
+
setSelectedModel(resolveThreadModel(created));
|
|
774
|
+
setThreads((current) => [created, ...current.filter((entry) => entry.id !== created.id)]);
|
|
775
|
+
setHydratedThreadId(created.id);
|
|
776
|
+
if (currentWebUi.papyrusLocationUri) {
|
|
777
|
+
await recordWebContextIfNeeded(created, currentWebUi, "session_start");
|
|
778
|
+
}
|
|
779
|
+
return created;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function ensureThread(): Promise<ConsoleChatThread> {
|
|
783
|
+
return thread ?? createNewThread();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function submitMessage(content: string) {
|
|
787
|
+
if (!content || sending) return;
|
|
788
|
+
setSending(true);
|
|
789
|
+
setError(null);
|
|
790
|
+
try {
|
|
791
|
+
const activeThread = await ensureThread();
|
|
792
|
+
const previousMessage = sortedMessages[sortedMessages.length - 1] ?? null;
|
|
793
|
+
const previousSequenceNumber = previousMessage?.sequenceNumber ?? activeThread.messageCount ?? 0;
|
|
794
|
+
const sequenceNumber = previousSequenceNumber + 1;
|
|
795
|
+
const now = new Date().toISOString();
|
|
796
|
+
const messageMetadata = {
|
|
797
|
+
model: selectedModel,
|
|
798
|
+
previousSequenceNumber,
|
|
799
|
+
previousMessageId: previousMessage?.id ?? null,
|
|
800
|
+
previousContextDigest: activeThread.contextDigest ?? null,
|
|
801
|
+
cacheHint: "lambda-tmp-jit",
|
|
802
|
+
console: {
|
|
803
|
+
author: {
|
|
804
|
+
avatarHash: effectiveAuthorIdentity.avatarHash,
|
|
805
|
+
email: effectiveAuthorIdentity.email,
|
|
806
|
+
userProfileId: effectiveAuthorIdentity.userProfileId,
|
|
807
|
+
},
|
|
808
|
+
webUi: currentWebUi,
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
const message: ConsoleChatMessage = {
|
|
812
|
+
id: `message-console-user-${crypto.randomUUID()}`,
|
|
813
|
+
threadId: activeThread.id,
|
|
814
|
+
parentMessageId: previousMessage?.id ?? null,
|
|
815
|
+
sequenceNumber,
|
|
816
|
+
role: "USER",
|
|
817
|
+
messageKind: CONSOLE_MESSAGE_KIND,
|
|
818
|
+
messageType: "MESSAGE",
|
|
819
|
+
content,
|
|
820
|
+
summary: truncateConsoleSummary(content),
|
|
821
|
+
responseStatus: "PENDING",
|
|
822
|
+
authorUserProfileId: effectiveAuthorIdentity.userProfileId,
|
|
823
|
+
metadata: messageMetadata,
|
|
824
|
+
createdAt: now,
|
|
825
|
+
};
|
|
826
|
+
mergeMessage(message);
|
|
827
|
+
const persistedMessage = await createConsoleMessage({
|
|
828
|
+
...message,
|
|
829
|
+
messageDomain: CONSOLE_MESSAGE_DOMAIN,
|
|
830
|
+
status: "active",
|
|
831
|
+
source: "papyrus-console",
|
|
832
|
+
authorLabel: actorLabel,
|
|
833
|
+
authorUserProfileId: effectiveAuthorIdentity.userProfileId,
|
|
834
|
+
semanticLayer: CONSOLE_SEMANTIC_LAYER,
|
|
835
|
+
searchVisibility: CONSOLE_SEARCH_VISIBILITY,
|
|
836
|
+
responseTarget: CONSOLE_RESPONSE_TARGET,
|
|
837
|
+
metadata: toAwsJson(messageMetadata),
|
|
838
|
+
updatedAt: now,
|
|
839
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
840
|
+
});
|
|
841
|
+
mergeMessage({ ...message, ...persistedMessage });
|
|
842
|
+
await updateConsoleThread({
|
|
843
|
+
id: activeThread.id,
|
|
844
|
+
messageCount: sequenceNumber,
|
|
845
|
+
lastMessageId: message.id,
|
|
846
|
+
lastMessageAt: now,
|
|
847
|
+
updatedAt: now,
|
|
848
|
+
});
|
|
849
|
+
setThread((current) => current ? {
|
|
850
|
+
...current,
|
|
851
|
+
messageCount: sequenceNumber,
|
|
852
|
+
lastMessageId: message.id,
|
|
853
|
+
lastMessageAt: now,
|
|
854
|
+
updatedAt: now,
|
|
855
|
+
} : current);
|
|
856
|
+
} catch (nextError) {
|
|
857
|
+
console.error("[PapyrusConsole] Unable to send console message", nextError);
|
|
858
|
+
setError(nextError instanceof Error ? nextError.message : "Unable to send console message.");
|
|
859
|
+
} finally {
|
|
860
|
+
setSending(false);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function sendMessage(message: PromptInputMessage) {
|
|
865
|
+
const content = message.text.trim();
|
|
866
|
+
if (!content) return;
|
|
867
|
+
setDraft("");
|
|
868
|
+
await submitMessage(content);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function handleSuggestionClick(suggestion: string) {
|
|
872
|
+
if (sending) return;
|
|
873
|
+
void submitMessage(suggestion);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const handleModelChange = useCallback((nextModel: string) => {
|
|
877
|
+
setSelectedModel(nextModel);
|
|
878
|
+
const activeThread = threadRef.current;
|
|
879
|
+
if (!activeThread) return;
|
|
880
|
+
const metadata = readAwsJsonObject(activeThread.metadata) ?? {};
|
|
881
|
+
const nextMetadata = {
|
|
882
|
+
...metadata,
|
|
883
|
+
console: {
|
|
884
|
+
...(readNestedObject(metadata, "console") ?? {}),
|
|
885
|
+
model: nextModel,
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
const now = new Date().toISOString();
|
|
889
|
+
void updateConsoleThread({
|
|
890
|
+
id: activeThread.id,
|
|
891
|
+
metadata: toAwsJson(nextMetadata),
|
|
892
|
+
updatedAt: now,
|
|
893
|
+
})
|
|
894
|
+
.then((updated) => {
|
|
895
|
+
setThread(updated);
|
|
896
|
+
setThreads((current) => current.map((entry) => (
|
|
897
|
+
entry.id === updated.id ? updated : entry
|
|
898
|
+
)));
|
|
899
|
+
})
|
|
900
|
+
.catch((nextError) => {
|
|
901
|
+
console.error("[PapyrusConsole] Unable to update console thread model", nextError);
|
|
902
|
+
setError(nextError instanceof Error ? nextError.message : "Unable to update console model.");
|
|
903
|
+
});
|
|
904
|
+
}, []);
|
|
905
|
+
|
|
906
|
+
const latestAwaitingAssistantUserMessage = [...sortedMessages]
|
|
907
|
+
.reverse()
|
|
908
|
+
.find((message) => (
|
|
909
|
+
message.role === "USER"
|
|
910
|
+
&& isAwaitingAssistantStatus(message.responseStatus)
|
|
911
|
+
&& !sortedMessages.some((candidate) => (
|
|
912
|
+
candidate.role === "ASSISTANT"
|
|
913
|
+
&& isResponderMessageForPendingUser(candidate, message)
|
|
914
|
+
))
|
|
915
|
+
));
|
|
916
|
+
const pendingUserResponse = Boolean(latestAwaitingAssistantUserMessage);
|
|
917
|
+
const pendingAssistantResponse = sortedMessages.some((message, index) => {
|
|
918
|
+
if (message.role !== "ASSISTANT" || message.responseStatus !== "RUNNING") return false;
|
|
919
|
+
return !sortedMessages.slice(index + 1).some((candidate) => (
|
|
920
|
+
candidate.role === "ASSISTANT"
|
|
921
|
+
&& candidate.threadId === message.threadId
|
|
922
|
+
&& candidate.responseStatus === "COMPLETED"
|
|
923
|
+
&& (candidate.parentMessageId === message.id || (candidate.sequenceNumber ?? 0) > (message.sequenceNumber ?? 0))
|
|
924
|
+
));
|
|
925
|
+
});
|
|
926
|
+
const pending = pendingUserResponse || pendingAssistantResponse;
|
|
927
|
+
useEffect(() => {
|
|
928
|
+
if (!thread?.id || !pending) return;
|
|
929
|
+
let cancelled = false;
|
|
930
|
+
let timer: number | null = null;
|
|
931
|
+
|
|
932
|
+
const refresh = async () => {
|
|
933
|
+
if (cancelled) return;
|
|
934
|
+
try {
|
|
935
|
+
await loadMessages(thread.id);
|
|
936
|
+
} catch (nextError) {
|
|
937
|
+
console.warn("[PapyrusConsole] Pending message refresh failed", nextError);
|
|
938
|
+
} finally {
|
|
939
|
+
if (!cancelled) timer = window.setTimeout(refresh, 1200);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
timer = window.setTimeout(refresh, 1200);
|
|
944
|
+
return () => {
|
|
945
|
+
cancelled = true;
|
|
946
|
+
if (timer !== null) window.clearTimeout(timer);
|
|
947
|
+
};
|
|
948
|
+
}, [loadMessages, pending, thread?.id]);
|
|
949
|
+
|
|
950
|
+
const displayMessages = useMemo<DisplayConsoleMessage[]>(() => {
|
|
951
|
+
if (!latestAwaitingAssistantUserMessage) return sortedMessages;
|
|
952
|
+
return [
|
|
953
|
+
...sortedMessages,
|
|
954
|
+
{
|
|
955
|
+
id: `pending-assistant-placeholder-${latestAwaitingAssistantUserMessage.id}`,
|
|
956
|
+
threadId: latestAwaitingAssistantUserMessage.threadId,
|
|
957
|
+
parentMessageId: latestAwaitingAssistantUserMessage.id,
|
|
958
|
+
sequenceNumber: (latestAwaitingAssistantUserMessage.sequenceNumber ?? sortedMessages.length) + 1,
|
|
959
|
+
role: "ASSISTANT",
|
|
960
|
+
messageKind: CONSOLE_MESSAGE_KIND,
|
|
961
|
+
messageType: "MESSAGE",
|
|
962
|
+
content: "",
|
|
963
|
+
summary: "Thinking...",
|
|
964
|
+
responseStatus: "RUNNING",
|
|
965
|
+
createdAt: latestAwaitingAssistantUserMessage.createdAt,
|
|
966
|
+
pendingPlaceholder: true,
|
|
967
|
+
},
|
|
968
|
+
];
|
|
969
|
+
}, [latestAwaitingAssistantUserMessage, sortedMessages]);
|
|
970
|
+
const timelineEntries = useMemo<ConsoleTimelineEntry[]>(() => {
|
|
971
|
+
const groupedTools = new Map<string, ConsoleToolTimelineEntry>();
|
|
972
|
+
const entries: ConsoleTimelineEntry[] = [];
|
|
973
|
+
|
|
974
|
+
for (const message of displayMessages) {
|
|
975
|
+
const isToolMessage = message.messageKind === CONSOLE_TOOL_CALL_KIND || message.messageKind === CONSOLE_TOOL_RESULT_KIND;
|
|
976
|
+
if (!isToolMessage) {
|
|
977
|
+
entries.push({ kind: "message", id: message.id, message });
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const metadata = readAwsJsonObject(message.metadata);
|
|
981
|
+
const toolCallId = readNestedString(metadata, ["toolCallId"]);
|
|
982
|
+
const toolName = readNestedString(metadata, ["toolName"]);
|
|
983
|
+
if (!toolCallId) {
|
|
984
|
+
entries.push({ kind: "message", id: message.id, message });
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const existing = groupedTools.get(toolCallId);
|
|
989
|
+
if (!existing) {
|
|
990
|
+
const firstSequence = typeof message.sequenceNumber === "number" ? message.sequenceNumber : Number.MAX_SAFE_INTEGER;
|
|
991
|
+
const next: ConsoleToolTimelineEntry = {
|
|
992
|
+
id: `tool-timeline-${toolCallId}`,
|
|
993
|
+
firstSequence,
|
|
994
|
+
firstCreatedAt: message.createdAt,
|
|
995
|
+
messageKind: message.messageKind ?? CONSOLE_TOOL_CALL_KIND,
|
|
996
|
+
role: "TOOL",
|
|
997
|
+
toolCallId,
|
|
998
|
+
toolName: toolName ?? "execute_tactus",
|
|
999
|
+
callMessage: message.messageKind === CONSOLE_TOOL_CALL_KIND ? message : undefined,
|
|
1000
|
+
resultMessage: message.messageKind === CONSOLE_TOOL_RESULT_KIND ? message : undefined,
|
|
1001
|
+
};
|
|
1002
|
+
groupedTools.set(toolCallId, next);
|
|
1003
|
+
entries.push({ kind: "tool", id: next.id, tool: next });
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (!existing.toolName && toolName) existing.toolName = toolName;
|
|
1008
|
+
if (message.messageKind === CONSOLE_TOOL_CALL_KIND) existing.callMessage = message;
|
|
1009
|
+
if (message.messageKind === CONSOLE_TOOL_RESULT_KIND) existing.resultMessage = message;
|
|
1010
|
+
const sequence = typeof message.sequenceNumber === "number" ? message.sequenceNumber : Number.MAX_SAFE_INTEGER;
|
|
1011
|
+
if (sequence < existing.firstSequence || (sequence === existing.firstSequence && message.createdAt < existing.firstCreatedAt)) {
|
|
1012
|
+
existing.firstSequence = sequence;
|
|
1013
|
+
existing.firstCreatedAt = message.createdAt;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return entries;
|
|
1018
|
+
}, [displayMessages]);
|
|
1019
|
+
const isSupersededRunningAssistantMessage = useCallback((
|
|
1020
|
+
message: DisplayConsoleMessage,
|
|
1021
|
+
_trimmedContent: string,
|
|
1022
|
+
) => (
|
|
1023
|
+
message.role === "ASSISTANT"
|
|
1024
|
+
&& message.responseStatus === "RUNNING"
|
|
1025
|
+
&& sortedMessages.some((candidate) => (
|
|
1026
|
+
candidate.role === "ASSISTANT"
|
|
1027
|
+
&& candidate.threadId === message.threadId
|
|
1028
|
+
&& candidate.responseStatus === "COMPLETED"
|
|
1029
|
+
&& (candidate.parentMessageId === message.id || (candidate.sequenceNumber ?? 0) > (message.sequenceNumber ?? 0))
|
|
1030
|
+
))
|
|
1031
|
+
), [sortedMessages]);
|
|
1032
|
+
const latestVisibleAvatarAnchorId = useMemo(() => {
|
|
1033
|
+
let latestAgentEntryId: string | null = null;
|
|
1034
|
+
for (const entry of timelineEntries) {
|
|
1035
|
+
if (entry.kind === "tool") {
|
|
1036
|
+
latestAgentEntryId = entry.id;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const content = (entry.message.content ?? "").trim();
|
|
1040
|
+
if (isSupersededRunningAssistantMessage(entry.message, content)) continue;
|
|
1041
|
+
const isAgentRole = entry.message.role === "ASSISTANT" || entry.message.role === "TOOL";
|
|
1042
|
+
if (isAgentRole) latestAgentEntryId = entry.id;
|
|
1043
|
+
}
|
|
1044
|
+
return latestAgentEntryId;
|
|
1045
|
+
}, [isSupersededRunningAssistantMessage, timelineEntries]);
|
|
1046
|
+
const streamingAutoStickSignal = useMemo(() => {
|
|
1047
|
+
const activeStreamingAssistant = [...displayMessages]
|
|
1048
|
+
.reverse()
|
|
1049
|
+
.find((message) => message.role === "ASSISTANT" && message.responseStatus === "RUNNING");
|
|
1050
|
+
if (!activeStreamingAssistant) return null;
|
|
1051
|
+
return [
|
|
1052
|
+
activeStreamingAssistant.id,
|
|
1053
|
+
activeStreamingAssistant.pendingPlaceholder ? "placeholder" : "message",
|
|
1054
|
+
(activeStreamingAssistant.content ?? "").length,
|
|
1055
|
+
activeStreamingAssistant.updatedAt ?? activeStreamingAssistant.createdAt,
|
|
1056
|
+
].join(":");
|
|
1057
|
+
}, [displayMessages]);
|
|
1058
|
+
|
|
1059
|
+
return (
|
|
1060
|
+
<div className="papyrus-console__panel">
|
|
1061
|
+
<ConsolePanelHeader onClose={onClose} />
|
|
1062
|
+
<div className="papyrus-console__sessions">
|
|
1063
|
+
<select
|
|
1064
|
+
aria-label="Console chat session"
|
|
1065
|
+
className="papyrus-console__session-select"
|
|
1066
|
+
onChange={(event) => selectThread(event.target.value)}
|
|
1067
|
+
value={thread?.id ?? ""}
|
|
1068
|
+
>
|
|
1069
|
+
{!thread ? <option value="">No session selected</option> : null}
|
|
1070
|
+
{threads.map((entry) => (
|
|
1071
|
+
<option key={entry.id} value={entry.id}>
|
|
1072
|
+
{formatConsoleThreadOption(entry)}
|
|
1073
|
+
</option>
|
|
1074
|
+
))}
|
|
1075
|
+
</select>
|
|
1076
|
+
{sortedMessages.length > 0 ? (
|
|
1077
|
+
<button
|
|
1078
|
+
className="papyrus-console__new-session"
|
|
1079
|
+
disabled={sending}
|
|
1080
|
+
onClick={() => void createNewThread().catch((nextError) => {
|
|
1081
|
+
console.error("[PapyrusConsole] Unable to create console thread", nextError);
|
|
1082
|
+
setError(nextError instanceof Error ? nextError.message : "Unable to create console thread.");
|
|
1083
|
+
})}
|
|
1084
|
+
type="button"
|
|
1085
|
+
>
|
|
1086
|
+
New chat
|
|
1087
|
+
</button>
|
|
1088
|
+
) : null}
|
|
1089
|
+
</div>
|
|
1090
|
+
<Conversation className="papyrus-console__conversation" resize={pending ? "instant" : "smooth"}>
|
|
1091
|
+
<ConsoleStreamingAutoStick active={pending} streamSignal={streamingAutoStickSignal} />
|
|
1092
|
+
<ConversationContent
|
|
1093
|
+
aria-live="polite"
|
|
1094
|
+
className="papyrus-console__body"
|
|
1095
|
+
scrollClassName="papyrus-console__scrollport"
|
|
1096
|
+
role="log"
|
|
1097
|
+
>
|
|
1098
|
+
{loading && !sortedMessages.length ? <p className="papyrus-console__empty">Loading conversation…</p> : null}
|
|
1099
|
+
{timelineEntries.map((entry) => {
|
|
1100
|
+
if (entry.kind === "tool") {
|
|
1101
|
+
const showAssistantAvatar = entry.id === latestVisibleAvatarAnchorId;
|
|
1102
|
+
const assistantAvatar = showAssistantAvatar
|
|
1103
|
+
? resolveAssistantAvatarPresentation(entry.tool, motionSetting === "low", pending)
|
|
1104
|
+
: null;
|
|
1105
|
+
return (
|
|
1106
|
+
<ConsoleToolTimelineRow key={entry.id}>
|
|
1107
|
+
<ConsoleToolMessageContent tool={entry.tool} />
|
|
1108
|
+
{assistantAvatar ? (
|
|
1109
|
+
<span className="papyrus-console-message__assistant-avatar" data-tone={assistantAvatar.tone}>
|
|
1110
|
+
<VultusBotAvatar
|
|
1111
|
+
ariaLabel={assistantAvatar.ariaLabel}
|
|
1112
|
+
lightColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_LIGHT_COLOR : ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR}
|
|
1113
|
+
neutralIdleMode={assistantAvatar.neutralIdleMode}
|
|
1114
|
+
shadowColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_SHADOW_COLOR : ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR}
|
|
1115
|
+
size={ASSISTANT_AVATAR_SIZE_PX}
|
|
1116
|
+
state={assistantAvatar.state}
|
|
1117
|
+
transitionDurationSeconds={0.35}
|
|
1118
|
+
/>
|
|
1119
|
+
</span>
|
|
1120
|
+
) : null}
|
|
1121
|
+
</ConsoleToolTimelineRow>
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const message = entry.message;
|
|
1126
|
+
const isWebContextMessage = message.messageKind === CONSOLE_WEB_CONTEXT_MESSAGE_KIND;
|
|
1127
|
+
const content = (message.content ?? "").trim();
|
|
1128
|
+
const summary = (message.summary ?? "").trim();
|
|
1129
|
+
const sanitizedSummary = sanitizeConsoleSummary(summary);
|
|
1130
|
+
const isSupersededRunningAssistant = isSupersededRunningAssistantMessage(message, content);
|
|
1131
|
+
const showAssistantAvatar = entry.id === latestVisibleAvatarAnchorId;
|
|
1132
|
+
const assistantAvatar = showAssistantAvatar
|
|
1133
|
+
? resolveAssistantAvatarPresentation(message, motionSetting === "low", pending)
|
|
1134
|
+
: null;
|
|
1135
|
+
const userAvatarHash = message.role === "USER" ? readConsoleAuthorAvatarHash(message) : null;
|
|
1136
|
+
const userAuthorEmail = message.role === "USER" ? readConsoleAuthorEmail(message) : null;
|
|
1137
|
+
const shouldShowThinking = (
|
|
1138
|
+
message.pendingPlaceholder
|
|
1139
|
+
|| (message.role === "ASSISTANT" && message.responseStatus === "RUNNING" && !content && !isSupersededRunningAssistant)
|
|
1140
|
+
);
|
|
1141
|
+
if (isSupersededRunningAssistant) return null;
|
|
1142
|
+
|
|
1143
|
+
return (
|
|
1144
|
+
<ConsoleChatMessageRow
|
|
1145
|
+
key={message.id}
|
|
1146
|
+
role={isWebContextMessage ? "CONTEXT" : message.role}
|
|
1147
|
+
>
|
|
1148
|
+
<div className={message.role === "ASSISTANT" ? "papyrus-console-message__markdown" : undefined}>
|
|
1149
|
+
{message.role === "USER" ? <ConsoleUserAvatar authorEmail={userAuthorEmail} avatarHash={userAvatarHash} /> : null}
|
|
1150
|
+
{isWebContextMessage ? (
|
|
1151
|
+
<p className="papyrus-console-message__context-label">Page context</p>
|
|
1152
|
+
) : null}
|
|
1153
|
+
{shouldShowThinking ? (
|
|
1154
|
+
<Shimmer className="papyrus-console__thinking-shimmer">Thinking...</Shimmer>
|
|
1155
|
+
) : <ConsoleChatMessageContent role={message.role} text={content || sanitizedSummary} />}
|
|
1156
|
+
</div>
|
|
1157
|
+
{message.responseStatus === "FAILED" ? <span className="papyrus-console-message__error">{message.responseError ?? "Responder failed"}</span> : null}
|
|
1158
|
+
{assistantAvatar ? (
|
|
1159
|
+
<span className="papyrus-console-message__assistant-avatar" data-tone={assistantAvatar.tone}>
|
|
1160
|
+
<VultusBotAvatar
|
|
1161
|
+
ariaLabel={assistantAvatar.ariaLabel}
|
|
1162
|
+
lightColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_LIGHT_COLOR : ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR}
|
|
1163
|
+
neutralIdleMode={assistantAvatar.neutralIdleMode}
|
|
1164
|
+
shadowColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_SHADOW_COLOR : ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR}
|
|
1165
|
+
size={ASSISTANT_AVATAR_SIZE_PX}
|
|
1166
|
+
state={assistantAvatar.state}
|
|
1167
|
+
transitionDurationSeconds={0.35}
|
|
1168
|
+
/>
|
|
1169
|
+
</span>
|
|
1170
|
+
) : null}
|
|
1171
|
+
</ConsoleChatMessageRow>
|
|
1172
|
+
);
|
|
1173
|
+
})}
|
|
1174
|
+
</ConversationContent>
|
|
1175
|
+
<ConversationScrollButton
|
|
1176
|
+
aria-label="Scroll to latest message"
|
|
1177
|
+
className="papyrus-console__scroll-bottom"
|
|
1178
|
+
/>
|
|
1179
|
+
</Conversation>
|
|
1180
|
+
{!loading && !sortedMessages.length ? (
|
|
1181
|
+
<div className="papyrus-console__suggestions papyrus-console-ui">
|
|
1182
|
+
<Suggestions>
|
|
1183
|
+
{CONSOLE_STARTER_SUGGESTIONS.map((suggestion) => (
|
|
1184
|
+
<Suggestion
|
|
1185
|
+
disabled={sending}
|
|
1186
|
+
key={suggestion}
|
|
1187
|
+
onClick={handleSuggestionClick}
|
|
1188
|
+
suggestion={suggestion}
|
|
1189
|
+
/>
|
|
1190
|
+
))}
|
|
1191
|
+
</Suggestions>
|
|
1192
|
+
</div>
|
|
1193
|
+
) : null}
|
|
1194
|
+
{error ? <p className="papyrus-console__error">{error}</p> : null}
|
|
1195
|
+
<PromptInput className="papyrus-console__composer" onSubmit={(message) => void sendMessage(message)}>
|
|
1196
|
+
<PromptInputBody>
|
|
1197
|
+
<PromptInputTextarea
|
|
1198
|
+
aria-label="Console message"
|
|
1199
|
+
disabled={sending}
|
|
1200
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
1201
|
+
placeholder="Write a message..."
|
|
1202
|
+
rows={3}
|
|
1203
|
+
value={draft}
|
|
1204
|
+
/>
|
|
1205
|
+
</PromptInputBody>
|
|
1206
|
+
<PromptInputFooter>
|
|
1207
|
+
<PromptInputTools>
|
|
1208
|
+
<ModelSelector
|
|
1209
|
+
aria-label="Console model"
|
|
1210
|
+
disabled={sending}
|
|
1211
|
+
onValueChange={handleModelChange}
|
|
1212
|
+
options={CONSOLE_MODEL_OPTIONS.map((option) => ({
|
|
1213
|
+
value: option.value,
|
|
1214
|
+
label: option.label,
|
|
1215
|
+
}))}
|
|
1216
|
+
value={selectedModel}
|
|
1217
|
+
/>
|
|
1218
|
+
</PromptInputTools>
|
|
1219
|
+
<PromptInputSubmit
|
|
1220
|
+
disabled={!draft.trim() || sending}
|
|
1221
|
+
status={pending ? "streaming" : "ready"}
|
|
1222
|
+
/>
|
|
1223
|
+
</PromptInputFooter>
|
|
1224
|
+
</PromptInput>
|
|
1225
|
+
</div>
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function truncateConsoleSummary(value: string): string {
|
|
1230
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
1231
|
+
return normalized.length > 180 ? `${normalized.slice(0, 179)}…` : normalized;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
|
1235
|
+
return new Promise<T>((resolve, reject) => {
|
|
1236
|
+
const timeout = window.setTimeout(() => {
|
|
1237
|
+
reject(new Error(message));
|
|
1238
|
+
}, timeoutMs);
|
|
1239
|
+
promise.then(
|
|
1240
|
+
(value) => {
|
|
1241
|
+
window.clearTimeout(timeout);
|
|
1242
|
+
resolve(value);
|
|
1243
|
+
},
|
|
1244
|
+
(error) => {
|
|
1245
|
+
window.clearTimeout(timeout);
|
|
1246
|
+
reject(error);
|
|
1247
|
+
},
|
|
1248
|
+
);
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function upsertConsoleMessage(messages: ConsoleChatMessage[], message: ConsoleChatMessage): ConsoleChatMessage[] {
|
|
1253
|
+
const index = messages.findIndex((entry) => entry.id === message.id);
|
|
1254
|
+
const next = index >= 0
|
|
1255
|
+
? messages.map((entry, currentIndex) => (
|
|
1256
|
+
currentIndex === index
|
|
1257
|
+
? mergeConsoleMessage(entry, message)
|
|
1258
|
+
: entry
|
|
1259
|
+
))
|
|
1260
|
+
: [...messages, message];
|
|
1261
|
+
return next.sort((left, right) => (left.sequenceNumber ?? 0) - (right.sequenceNumber ?? 0) || left.createdAt.localeCompare(right.createdAt));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function mergeConsoleMessage(previous: ConsoleChatMessage, incoming: ConsoleChatMessage): ConsoleChatMessage {
|
|
1265
|
+
const merged = { ...previous } as Record<string, unknown>;
|
|
1266
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
1267
|
+
if (value !== undefined && value !== null) merged[key] = value;
|
|
1268
|
+
}
|
|
1269
|
+
return merged as ConsoleChatMessage;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function isConsoleMessageAfter(message: ConsoleChatMessage, anchor: ConsoleChatMessage): boolean {
|
|
1273
|
+
if (message.id === anchor.id) return false;
|
|
1274
|
+
if (typeof message.sequenceNumber === "number" && typeof anchor.sequenceNumber === "number") {
|
|
1275
|
+
return message.sequenceNumber > anchor.sequenceNumber;
|
|
1276
|
+
}
|
|
1277
|
+
return message.createdAt >= anchor.createdAt;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function isResponderMessageForPendingUser(message: ConsoleChatMessage, pendingUser: ConsoleChatMessage): boolean {
|
|
1281
|
+
if (message.id === pendingUser.id) return false;
|
|
1282
|
+
if (message.parentMessageId && message.parentMessageId === pendingUser.id) return true;
|
|
1283
|
+
if (isConsoleMessageAfter(message, pendingUser)) return true;
|
|
1284
|
+
if (
|
|
1285
|
+
typeof message.sequenceNumber === "number"
|
|
1286
|
+
&& typeof pendingUser.sequenceNumber === "number"
|
|
1287
|
+
&& message.sequenceNumber === pendingUser.sequenceNumber
|
|
1288
|
+
) {
|
|
1289
|
+
return message.createdAt >= pendingUser.createdAt;
|
|
1290
|
+
}
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function isAwaitingAssistantStatus(status: string | null | undefined): boolean {
|
|
1295
|
+
return status === "PENDING" || status === "RUNNING";
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function sanitizeConsoleSummary(summary: string): string {
|
|
1299
|
+
const lowered = summary.trim().toLowerCase();
|
|
1300
|
+
if (!lowered) return "";
|
|
1301
|
+
if (lowered === "thinking...") return "";
|
|
1302
|
+
if (lowered.endsWith("responding.")) return "";
|
|
1303
|
+
return summary;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function readConsoleAuthorAvatarHash(message: ConsoleChatMessage): string | null {
|
|
1307
|
+
const metadata = readAwsJsonObject(message.metadata);
|
|
1308
|
+
return (
|
|
1309
|
+
readNestedString(metadata, ["console", "author", "avatarHash"])
|
|
1310
|
+
?? readNestedString(metadata, ["author", "avatarHash"])
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function readConsoleAuthorEmail(message: ConsoleChatMessage): string | null {
|
|
1315
|
+
const metadata = readAwsJsonObject(message.metadata);
|
|
1316
|
+
return (
|
|
1317
|
+
readNestedString(metadata, ["console", "author", "email"])
|
|
1318
|
+
?? readNestedString(metadata, ["author", "email"])
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function gravatarUrlFromHash(hash: string, size: number): string {
|
|
1323
|
+
const normalized = hash.trim().toLowerCase();
|
|
1324
|
+
return `https://www.gravatar.com/avatar/${encodeURIComponent(normalized)}?s=${size}&d=404`;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function computeAvatarHashFromEmail(email: string): Promise<string | null> {
|
|
1328
|
+
const normalized = email.trim().toLowerCase();
|
|
1329
|
+
if (!normalized) return null;
|
|
1330
|
+
return computeMd5Hex(normalized);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
async function computeMd5Hex(value: string): Promise<string> {
|
|
1334
|
+
const md5 = new Md5();
|
|
1335
|
+
md5.update(new TextEncoder().encode(value));
|
|
1336
|
+
const digest = await md5.digest();
|
|
1337
|
+
return Array.from(digest)
|
|
1338
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
1339
|
+
.join("");
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function ConsoleUserAvatar({ authorEmail, avatarHash }: { authorEmail: string | null; avatarHash: string | null }) {
|
|
1343
|
+
const [imageFailed, setImageFailed] = useState(false);
|
|
1344
|
+
useEffect(() => {
|
|
1345
|
+
setImageFailed(false);
|
|
1346
|
+
}, [avatarHash]);
|
|
1347
|
+
const imageUrl = avatarHash ? gravatarUrlFromHash(avatarHash, 64) : null;
|
|
1348
|
+
const showImage = Boolean(imageUrl && !imageFailed);
|
|
1349
|
+
const tooltip = authorEmail ?? "Email unavailable";
|
|
1350
|
+
|
|
1351
|
+
return (
|
|
1352
|
+
<span
|
|
1353
|
+
aria-label={`Message author email: ${tooltip}`}
|
|
1354
|
+
className="papyrus-console-message__user-avatar"
|
|
1355
|
+
role="img"
|
|
1356
|
+
title={tooltip}
|
|
1357
|
+
>
|
|
1358
|
+
{showImage ? (
|
|
1359
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
1360
|
+
<img
|
|
1361
|
+
alt=""
|
|
1362
|
+
className="papyrus-console-message__user-avatar-image"
|
|
1363
|
+
onError={() => setImageFailed(true)}
|
|
1364
|
+
src={imageUrl ?? undefined}
|
|
1365
|
+
/>
|
|
1366
|
+
) : (
|
|
1367
|
+
<UserIcon className="papyrus-console-message__user-avatar-icon" size={14} strokeWidth={2} />
|
|
1368
|
+
)}
|
|
1369
|
+
</span>
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function resolveAssistantAvatarPresentation(
|
|
1374
|
+
entry: DisplayConsoleMessage | ConsoleToolTimelineEntry,
|
|
1375
|
+
forceLowMotion: boolean,
|
|
1376
|
+
hasActiveResponse: boolean,
|
|
1377
|
+
): { ariaLabel: string; neutralIdleMode?: "bored-random" | "static"; state: BotAvatarState; tone: AssistantAvatarTone } {
|
|
1378
|
+
const failedState = resolveConsoleEntryFailedState(entry);
|
|
1379
|
+
if (forceLowMotion) {
|
|
1380
|
+
return {
|
|
1381
|
+
ariaLabel: "Assistant avatar in low-motion mode",
|
|
1382
|
+
neutralIdleMode: "static",
|
|
1383
|
+
state: "neutral",
|
|
1384
|
+
tone: failedState ? "error" : "default",
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (failedState) {
|
|
1389
|
+
return {
|
|
1390
|
+
ariaLabel: "Assistant avatar in error state",
|
|
1391
|
+
neutralIdleMode: "static",
|
|
1392
|
+
state: "neutral",
|
|
1393
|
+
tone: "error",
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
if (isToolTimelineEntry(entry) && hasActiveResponse) {
|
|
1397
|
+
const toolHasResult = isToolTimelineResultAvailable(entry);
|
|
1398
|
+
if (toolHasResult) {
|
|
1399
|
+
return {
|
|
1400
|
+
ariaLabel: "Assistant avatar while handling tool results",
|
|
1401
|
+
state: "toolResponse",
|
|
1402
|
+
tone: "default",
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
return {
|
|
1406
|
+
ariaLabel: "Assistant avatar while calling tools",
|
|
1407
|
+
state: "toolCalling",
|
|
1408
|
+
tone: "default",
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
if (isToolTimelineEntry(entry)) return {
|
|
1412
|
+
ariaLabel: "Assistant avatar in neutral state",
|
|
1413
|
+
state: "neutral",
|
|
1414
|
+
tone: "default",
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
const trimmedContent = (entry.content ?? "").trim();
|
|
1418
|
+
if (entry.responseStatus === "RUNNING" && !hasActiveResponse) {
|
|
1419
|
+
return {
|
|
1420
|
+
ariaLabel: "Assistant avatar in neutral state",
|
|
1421
|
+
state: "neutral",
|
|
1422
|
+
tone: "default",
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
if (entry.pendingPlaceholder || (entry.responseStatus === "RUNNING" && !trimmedContent)) {
|
|
1426
|
+
return {
|
|
1427
|
+
ariaLabel: "Assistant avatar while thinking",
|
|
1428
|
+
state: "thinking",
|
|
1429
|
+
tone: "default",
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
if (entry.responseStatus === "RUNNING" && trimmedContent) {
|
|
1433
|
+
return {
|
|
1434
|
+
ariaLabel: "Assistant avatar while speaking",
|
|
1435
|
+
state: "speakingOpen",
|
|
1436
|
+
tone: "default",
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
ariaLabel: "Assistant avatar in neutral state",
|
|
1441
|
+
state: "neutral",
|
|
1442
|
+
tone: "default",
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function resolveConsoleEntryFailedState(entry: DisplayConsoleMessage | ConsoleToolTimelineEntry): boolean {
|
|
1447
|
+
if (!isToolTimelineEntry(entry)) return entry.responseStatus === "FAILED";
|
|
1448
|
+
if (!entry.resultMessage) return false;
|
|
1449
|
+
const metadata = readAwsJsonObject(entry.resultMessage.metadata);
|
|
1450
|
+
const toolResult = readNestedValue(metadata, ["toolResultJson"]);
|
|
1451
|
+
if (toolResult && typeof toolResult === "object" && !Array.isArray(toolResult)) {
|
|
1452
|
+
const toolResultObject = toolResult as Record<string, unknown>;
|
|
1453
|
+
if (toolResultObject.ok === false) return true;
|
|
1454
|
+
if (toolResultObject.error) return true;
|
|
1455
|
+
}
|
|
1456
|
+
const markdown = entry.resultMessage.content?.toLowerCase() ?? "";
|
|
1457
|
+
return /(^|\n)-\s*ok:\s*false\b/.test(markdown);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function isToolTimelineEntry(entry: DisplayConsoleMessage | ConsoleToolTimelineEntry): entry is ConsoleToolTimelineEntry {
|
|
1461
|
+
return typeof (entry as ConsoleToolTimelineEntry).toolCallId === "string";
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function isToolTimelineResultAvailable(entry: ConsoleToolTimelineEntry): boolean {
|
|
1465
|
+
return Boolean(entry.resultMessage);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function ConsoleToolMessageContent({ tool }: { tool: ConsoleToolTimelineEntry }) {
|
|
1469
|
+
const callMessage = tool.callMessage;
|
|
1470
|
+
const resultMessage = tool.resultMessage;
|
|
1471
|
+
const callMetadata = readAwsJsonObject(callMessage?.metadata);
|
|
1472
|
+
const resultMetadata = readAwsJsonObject(resultMessage?.metadata);
|
|
1473
|
+
const toolState: "input-available" | "output-available" | "output-error" = resultMessage
|
|
1474
|
+
? (resolveConsoleEntryFailedState(tool) ? "output-error" : "output-available")
|
|
1475
|
+
: "input-available";
|
|
1476
|
+
const toolInput = (
|
|
1477
|
+
readNestedValue(callMetadata, ["arguments"])
|
|
1478
|
+
?? callMessage?.content
|
|
1479
|
+
?? null
|
|
1480
|
+
);
|
|
1481
|
+
const toolResultOutput = resultMessage
|
|
1482
|
+
? (
|
|
1483
|
+
readNestedValue(resultMetadata, ["toolResultJson"])
|
|
1484
|
+
?? resultMessage.content
|
|
1485
|
+
?? null
|
|
1486
|
+
)
|
|
1487
|
+
: null;
|
|
1488
|
+
const toolErrorText = toolState === "output-error" && resultMessage
|
|
1489
|
+
? (
|
|
1490
|
+
readNestedString(resultMetadata, ["toolResultJson", "error", "message"])
|
|
1491
|
+
?? readNestedString(resultMetadata, ["toolResultJson", "error"])
|
|
1492
|
+
?? resultMessage.content
|
|
1493
|
+
?? "Tool error"
|
|
1494
|
+
)
|
|
1495
|
+
: undefined;
|
|
1496
|
+
|
|
1497
|
+
return (
|
|
1498
|
+
<div className="papyrus-console-tool-row__content">
|
|
1499
|
+
<Tool>
|
|
1500
|
+
<ToolHeader
|
|
1501
|
+
state={toolState}
|
|
1502
|
+
toolName={tool.toolName}
|
|
1503
|
+
type={`tool-${tool.toolName}`}
|
|
1504
|
+
/>
|
|
1505
|
+
<ToolContent>
|
|
1506
|
+
<ToolInput input={toolInput} />
|
|
1507
|
+
<ToolOutput
|
|
1508
|
+
errorText={toolErrorText}
|
|
1509
|
+
output={toolState === "output-error" ? undefined : formatToolOutputText(toolResultOutput)}
|
|
1510
|
+
/>
|
|
1511
|
+
</ToolContent>
|
|
1512
|
+
</Tool>
|
|
1513
|
+
</div>
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function ConsoleToolTimelineRow({ children }: { children: ReactNode }) {
|
|
1518
|
+
const rowRef = useRef<HTMLElement | null>(null);
|
|
1519
|
+
|
|
1520
|
+
useEffect(() => {
|
|
1521
|
+
const row = rowRef.current;
|
|
1522
|
+
if (!row || prefersReducedMotion()) return;
|
|
1523
|
+
const tween = gsap.fromTo(
|
|
1524
|
+
row,
|
|
1525
|
+
{ autoAlpha: 0, y: 8 },
|
|
1526
|
+
{ autoAlpha: 1, y: 0, duration: 0.16, ease: "power2.out", overwrite: "auto" },
|
|
1527
|
+
);
|
|
1528
|
+
return () => {
|
|
1529
|
+
tween.kill();
|
|
1530
|
+
gsap.killTweensOf(row);
|
|
1531
|
+
};
|
|
1532
|
+
}, []);
|
|
1533
|
+
|
|
1534
|
+
return (
|
|
1535
|
+
<article className="papyrus-console-tool-row" ref={rowRef}>
|
|
1536
|
+
{children}
|
|
1537
|
+
</article>
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function formatToolOutputText(value: unknown): string {
|
|
1542
|
+
if (typeof value === "string") return value;
|
|
1543
|
+
if (value === undefined || value === null) return "";
|
|
1544
|
+
try {
|
|
1545
|
+
return JSON.stringify(value, null, 2);
|
|
1546
|
+
} catch {
|
|
1547
|
+
return String(value);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function ConsoleChatMessageContent({ role, text }: { role: string | null | undefined; text: string }) {
|
|
1552
|
+
const blocks = useMemo(() => parseConsoleMarkdownBlocks(text), [text]);
|
|
1553
|
+
if (role !== "ASSISTANT") return text;
|
|
1554
|
+
return (
|
|
1555
|
+
<>
|
|
1556
|
+
{blocks.map((block, index) => renderConsoleMarkdownBlock(block, index))}
|
|
1557
|
+
</>
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
type ConsoleMarkdownBlock =
|
|
1562
|
+
| { type: "code"; text: string }
|
|
1563
|
+
| { type: "heading"; depth: number; text: string }
|
|
1564
|
+
| { type: "list"; ordered: boolean; items: string[] }
|
|
1565
|
+
| { type: "paragraph"; text: string };
|
|
1566
|
+
|
|
1567
|
+
function parseConsoleMarkdownBlocks(text: string): ConsoleMarkdownBlock[] {
|
|
1568
|
+
const blocks: ConsoleMarkdownBlock[] = [];
|
|
1569
|
+
const lines = text.split(/\r?\n/);
|
|
1570
|
+
let paragraph: string[] = [];
|
|
1571
|
+
let list: string[] = [];
|
|
1572
|
+
let listOrdered = false;
|
|
1573
|
+
let code: string[] = [];
|
|
1574
|
+
let inCode = false;
|
|
1575
|
+
|
|
1576
|
+
const flushParagraph = () => {
|
|
1577
|
+
if (!paragraph.length) return;
|
|
1578
|
+
blocks.push({ type: "paragraph", text: paragraph.join(" ").trim() });
|
|
1579
|
+
paragraph = [];
|
|
1580
|
+
};
|
|
1581
|
+
const flushList = () => {
|
|
1582
|
+
if (!list.length) return;
|
|
1583
|
+
blocks.push({ type: "list", ordered: listOrdered, items: list });
|
|
1584
|
+
list = [];
|
|
1585
|
+
};
|
|
1586
|
+
|
|
1587
|
+
for (const line of lines) {
|
|
1588
|
+
if (line.trim().startsWith("```")) {
|
|
1589
|
+
if (inCode) {
|
|
1590
|
+
blocks.push({ type: "code", text: code.join("\n") });
|
|
1591
|
+
code = [];
|
|
1592
|
+
inCode = false;
|
|
1593
|
+
} else {
|
|
1594
|
+
flushParagraph();
|
|
1595
|
+
flushList();
|
|
1596
|
+
inCode = true;
|
|
1597
|
+
}
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
if (inCode) {
|
|
1601
|
+
code.push(line);
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
const trimmed = line.trim();
|
|
1605
|
+
if (!trimmed) {
|
|
1606
|
+
flushParagraph();
|
|
1607
|
+
flushList();
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed);
|
|
1611
|
+
if (heading) {
|
|
1612
|
+
flushParagraph();
|
|
1613
|
+
flushList();
|
|
1614
|
+
blocks.push({ type: "heading", depth: heading[1].length, text: heading[2].trim() });
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
const bullet = /^([-*+])\s+(.+)$/.exec(trimmed);
|
|
1618
|
+
if (bullet) {
|
|
1619
|
+
flushParagraph();
|
|
1620
|
+
if (!list.length) listOrdered = false;
|
|
1621
|
+
list.push(bullet[2].trim());
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
const ordered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
|
|
1625
|
+
if (ordered) {
|
|
1626
|
+
flushParagraph();
|
|
1627
|
+
if (!list.length) listOrdered = true;
|
|
1628
|
+
list.push(ordered[1].trim());
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
flushList();
|
|
1632
|
+
paragraph.push(trimmed);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
flushParagraph();
|
|
1636
|
+
flushList();
|
|
1637
|
+
if (code.length) blocks.push({ type: "code", text: code.join("\n") });
|
|
1638
|
+
return blocks;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function renderConsoleMarkdownBlock(block: ConsoleMarkdownBlock, index: number): ReactNode {
|
|
1642
|
+
const key = `${block.type}-${index}`;
|
|
1643
|
+
if (block.type === "code") return <pre className="papyrus-console-message__code" key={key}>{block.text}</pre>;
|
|
1644
|
+
if (block.type === "list") {
|
|
1645
|
+
if (block.ordered) {
|
|
1646
|
+
return (
|
|
1647
|
+
<ol key={key}>
|
|
1648
|
+
{block.items.map((item, itemIndex) => <li key={`${key}-${itemIndex}`}>{renderConsoleInlineMarkdown(item)}</li>)}
|
|
1649
|
+
</ol>
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
return (
|
|
1653
|
+
<ul key={key}>
|
|
1654
|
+
{block.items.map((item, itemIndex) => <li key={`${key}-${itemIndex}`}>{renderConsoleInlineMarkdown(item)}</li>)}
|
|
1655
|
+
</ul>
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
if (block.type === "heading") {
|
|
1659
|
+
if (block.depth <= 1) return <h3 key={key}>{renderConsoleInlineMarkdown(block.text)}</h3>;
|
|
1660
|
+
if (block.depth === 2) return <h4 key={key}>{renderConsoleInlineMarkdown(block.text)}</h4>;
|
|
1661
|
+
return <h5 key={key}>{renderConsoleInlineMarkdown(block.text)}</h5>;
|
|
1662
|
+
}
|
|
1663
|
+
return <p key={key}>{renderConsoleInlineMarkdown(block.text)}</p>;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function renderConsoleInlineMarkdown(text: string): ReactNode[] {
|
|
1667
|
+
const nodes: ReactNode[] = [];
|
|
1668
|
+
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\))/g;
|
|
1669
|
+
let lastIndex = 0;
|
|
1670
|
+
let match: RegExpExecArray | null;
|
|
1671
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
1672
|
+
if (match.index > lastIndex) nodes.push(text.slice(lastIndex, match.index));
|
|
1673
|
+
const token = match[0];
|
|
1674
|
+
if (token.startsWith("`")) {
|
|
1675
|
+
nodes.push(<code key={`${match.index}-code`}>{token.slice(1, -1)}</code>);
|
|
1676
|
+
} else if (token.startsWith("**")) {
|
|
1677
|
+
nodes.push(<strong key={`${match.index}-strong`}>{token.slice(2, -2)}</strong>);
|
|
1678
|
+
} else {
|
|
1679
|
+
const linkMatch = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
|
|
1680
|
+
if (linkMatch) {
|
|
1681
|
+
nodes.push(
|
|
1682
|
+
<a href={linkMatch[2]} key={`${match.index}-link`} rel="noreferrer noopener" target="_blank">
|
|
1683
|
+
{linkMatch[1]}
|
|
1684
|
+
</a>,
|
|
1685
|
+
);
|
|
1686
|
+
} else {
|
|
1687
|
+
nodes.push(token);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
lastIndex = match.index + token.length;
|
|
1691
|
+
}
|
|
1692
|
+
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
|
|
1693
|
+
return nodes;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function ConsoleChatMessageRow({
|
|
1697
|
+
role,
|
|
1698
|
+
children,
|
|
1699
|
+
}: {
|
|
1700
|
+
role: string | null | undefined;
|
|
1701
|
+
children: ReactNode;
|
|
1702
|
+
}) {
|
|
1703
|
+
const rowRef = useRef<HTMLElement | null>(null);
|
|
1704
|
+
|
|
1705
|
+
useEffect(() => {
|
|
1706
|
+
const row = rowRef.current;
|
|
1707
|
+
if (!row || prefersReducedMotion()) return;
|
|
1708
|
+
const tween = gsap.fromTo(
|
|
1709
|
+
row,
|
|
1710
|
+
{ autoAlpha: 0, y: 8 },
|
|
1711
|
+
{ autoAlpha: 1, y: 0, duration: 0.16, ease: "power2.out", overwrite: "auto" },
|
|
1712
|
+
);
|
|
1713
|
+
return () => {
|
|
1714
|
+
tween.kill();
|
|
1715
|
+
gsap.killTweensOf(row);
|
|
1716
|
+
};
|
|
1717
|
+
}, []);
|
|
1718
|
+
|
|
1719
|
+
return (
|
|
1720
|
+
<article
|
|
1721
|
+
className={
|
|
1722
|
+
role === "USER"
|
|
1723
|
+
? "papyrus-console-message papyrus-console-message--user"
|
|
1724
|
+
: role === "CONTEXT"
|
|
1725
|
+
? "papyrus-console-message papyrus-console-message--context"
|
|
1726
|
+
: "papyrus-console-message papyrus-console-message--assistant"
|
|
1727
|
+
}
|
|
1728
|
+
ref={rowRef}
|
|
1729
|
+
>
|
|
1730
|
+
{children}
|
|
1731
|
+
</article>
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function ConsoleStreamingAutoStick({ active, streamSignal }: { active: boolean; streamSignal: string | null }) {
|
|
1736
|
+
const { scrollRef, scrollToBottom } = useStickToBottomContext();
|
|
1737
|
+
const previousSignalRef = useRef<string | null>(null);
|
|
1738
|
+
|
|
1739
|
+
const snapToBottom = useCallback(() => {
|
|
1740
|
+
void scrollToBottom({ animation: "instant" });
|
|
1741
|
+
const scrollElement = scrollRef.current;
|
|
1742
|
+
if (!scrollElement) return;
|
|
1743
|
+
scrollElement.scrollTop = Math.max(0, scrollElement.scrollHeight - scrollElement.clientHeight);
|
|
1744
|
+
}, [scrollRef, scrollToBottom]);
|
|
1745
|
+
|
|
1746
|
+
useEffect(() => {
|
|
1747
|
+
if (!active || !streamSignal) return;
|
|
1748
|
+
if (previousSignalRef.current === streamSignal) return;
|
|
1749
|
+
previousSignalRef.current = streamSignal;
|
|
1750
|
+
|
|
1751
|
+
snapToBottom();
|
|
1752
|
+
const frame = window.requestAnimationFrame(() => {
|
|
1753
|
+
snapToBottom();
|
|
1754
|
+
});
|
|
1755
|
+
return () => {
|
|
1756
|
+
window.cancelAnimationFrame(frame);
|
|
1757
|
+
};
|
|
1758
|
+
}, [active, snapToBottom, streamSignal]);
|
|
1759
|
+
|
|
1760
|
+
useEffect(() => {
|
|
1761
|
+
if (!active) return;
|
|
1762
|
+
let frame = 0;
|
|
1763
|
+
const pin = () => {
|
|
1764
|
+
snapToBottom();
|
|
1765
|
+
frame = window.requestAnimationFrame(pin);
|
|
1766
|
+
};
|
|
1767
|
+
pin();
|
|
1768
|
+
return () => {
|
|
1769
|
+
window.cancelAnimationFrame(frame);
|
|
1770
|
+
};
|
|
1771
|
+
}, [active, snapToBottom]);
|
|
1772
|
+
|
|
1773
|
+
useEffect(() => {
|
|
1774
|
+
if (!active) previousSignalRef.current = null;
|
|
1775
|
+
}, [active]);
|
|
1776
|
+
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function prefersReducedMotion(): boolean {
|
|
1781
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
1782
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function formatConsoleThreadOption(thread: ConsoleChatThread): string {
|
|
1786
|
+
const stamp = thread.lastMessageAt ?? thread.updatedAt ?? thread.createdAt;
|
|
1787
|
+
const date = stamp ? new Date(stamp) : null;
|
|
1788
|
+
const suffix = date && Number.isFinite(date.getTime())
|
|
1789
|
+
? ` — ${date.toLocaleDateString(undefined, { month: "short", day: "numeric" })}`
|
|
1790
|
+
: "";
|
|
1791
|
+
return `${thread.title || "Papyrus Console"}${suffix}`;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function resolveThreadModel(thread: ConsoleChatThread | null): string {
|
|
1795
|
+
const metadata = readAwsJsonObject(thread?.metadata);
|
|
1796
|
+
const model =
|
|
1797
|
+
readNestedString(metadata, ["console", "model"])
|
|
1798
|
+
?? readNestedString(metadata, ["model"]);
|
|
1799
|
+
return isSupportedConsoleModel(model) ? model : DEFAULT_CONSOLE_MODEL;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function isSupportedConsoleModel(model: unknown): model is string {
|
|
1803
|
+
return typeof model === "string" && CONSOLE_MODEL_OPTIONS.some((option) => option.value === model);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function readNestedObject(value: Record<string, unknown> | null, key: string): Record<string, unknown> | null {
|
|
1807
|
+
if (!value) return null;
|
|
1808
|
+
const candidate = value[key];
|
|
1809
|
+
return candidate && typeof candidate === "object" && !Array.isArray(candidate)
|
|
1810
|
+
? candidate as Record<string, unknown>
|
|
1811
|
+
: null;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function readNestedString(value: Record<string, unknown> | null, path: string[]): string | null {
|
|
1815
|
+
let cursor: unknown = value;
|
|
1816
|
+
for (const key of path) {
|
|
1817
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) return null;
|
|
1818
|
+
cursor = (cursor as Record<string, unknown>)[key];
|
|
1819
|
+
}
|
|
1820
|
+
return typeof cursor === "string" && cursor.trim() ? cursor.trim() : null;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function readNestedValue(value: Record<string, unknown> | null, path: string[]): unknown {
|
|
1824
|
+
let cursor: unknown = value;
|
|
1825
|
+
for (const key of path) {
|
|
1826
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) return null;
|
|
1827
|
+
cursor = (cursor as Record<string, unknown>)[key];
|
|
1828
|
+
}
|
|
1829
|
+
return cursor ?? null;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function readAwsJsonObject(value: unknown): Record<string, unknown> | null {
|
|
1833
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
1834
|
+
if (typeof value === "string") {
|
|
1835
|
+
try {
|
|
1836
|
+
const parsed = JSON.parse(value) as unknown;
|
|
1837
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
|
|
1838
|
+
} catch {
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function ConsolePanelHeader({ onClose }: { onClose: () => void }) {
|
|
1846
|
+
return (
|
|
1847
|
+
<header className="papyrus-console__header">
|
|
1848
|
+
<p className="papyrus-console__header-label">Chat</p>
|
|
1849
|
+
<button className="papyrus-console__close" onClick={onClose} type="button" aria-label="Close console">
|
|
1850
|
+
<LucideXIcon />
|
|
1851
|
+
</button>
|
|
1852
|
+
</header>
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function LucideXIcon() {
|
|
1857
|
+
return (
|
|
1858
|
+
<svg
|
|
1859
|
+
aria-hidden="true"
|
|
1860
|
+
className="papyrus-console__close-icon news-desk-search-mark__icon"
|
|
1861
|
+
fill="none"
|
|
1862
|
+
focusable="false"
|
|
1863
|
+
height="18"
|
|
1864
|
+
stroke="currentColor"
|
|
1865
|
+
strokeLinecap="round"
|
|
1866
|
+
strokeLinejoin="round"
|
|
1867
|
+
strokeWidth="2"
|
|
1868
|
+
viewBox="0 0 24 24"
|
|
1869
|
+
width="18"
|
|
1870
|
+
>
|
|
1871
|
+
<path d="M18 6 6 18" />
|
|
1872
|
+
<path d="m6 6 12 12" />
|
|
1873
|
+
</svg>
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
export function PapyrusConsoleChatIcon() {
|
|
1878
|
+
return (
|
|
1879
|
+
<svg
|
|
1880
|
+
aria-hidden="true"
|
|
1881
|
+
className="papyrus-console__icon news-desk-search-mark__icon"
|
|
1882
|
+
fill="none"
|
|
1883
|
+
focusable="false"
|
|
1884
|
+
height="18"
|
|
1885
|
+
stroke="currentColor"
|
|
1886
|
+
strokeLinecap="round"
|
|
1887
|
+
strokeLinejoin="round"
|
|
1888
|
+
strokeWidth="2"
|
|
1889
|
+
viewBox="0 0 24 24"
|
|
1890
|
+
width="18"
|
|
1891
|
+
>
|
|
1892
|
+
<path d="M14 9a2 2 0 0 1-2 2H6.5L3 14V5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2Z" />
|
|
1893
|
+
<path d="M18 9h1a2 2 0 0 1 2 2v9l-3.5-3H12a2 2 0 0 1-2-2v-1" />
|
|
1894
|
+
</svg>
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function toAwsJson(value: unknown): string {
|
|
1899
|
+
return JSON.stringify(value ?? {});
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function formatUnknownConsoleError(error: unknown): string {
|
|
1903
|
+
if (error instanceof Error && error.message.trim()) return error.message;
|
|
1904
|
+
if (typeof error === "string" && error.trim()) return error;
|
|
1905
|
+
try {
|
|
1906
|
+
const text = JSON.stringify(error);
|
|
1907
|
+
if (text && text !== "{}") return text;
|
|
1908
|
+
} catch {
|
|
1909
|
+
// Ignore serialization failures.
|
|
1910
|
+
}
|
|
1911
|
+
return "Unable to load console thread.";
|
|
1912
|
+
}
|