@theoremjs/react 0.0.0-stage → 0.3.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/README.md +772 -2
- package/client/attachment-hover-preview.d.ts +11 -0
- package/client/attachment-hover-preview.js +36 -0
- package/client/audio-level.d.ts +5 -0
- package/client/audio-level.js +24 -0
- package/client/composer-attachments.d.ts +30 -0
- package/client/composer-attachments.js +33 -0
- package/client/composer-drawer.d.ts +19 -0
- package/client/composer-drawer.js +15 -0
- package/client/composer-hints.d.ts +25 -0
- package/client/composer-hints.js +20 -0
- package/client/composer-primary.d.ts +16 -0
- package/client/composer-primary.js +25 -0
- package/client/decision-images.d.ts +76 -0
- package/client/decision-images.js +139 -0
- package/client/decision-transport.d.ts +70 -0
- package/client/decision-transport.js +175 -0
- package/client/decode-composer-draft.d.ts +11 -0
- package/client/decode-composer-draft.js +12 -0
- package/client/embedding-input.d.ts +84 -0
- package/client/embedding-input.js +194 -0
- package/client/embedding-space.d.ts +82 -0
- package/client/embedding-space.js +389 -0
- package/client/embedding-transport.d.ts +40 -0
- package/client/embedding-transport.js +71 -0
- package/client/encode-composer-draft.d.ts +9 -0
- package/client/encode-composer-draft.js +34 -0
- package/client/encode-files.d.ts +18 -0
- package/client/encode-files.js +54 -0
- package/client/failure.d.ts +29 -0
- package/client/failure.js +34 -0
- package/client/generation-selection.d.ts +15 -0
- package/client/generation-selection.js +24 -0
- package/client/host-call.d.ts +51 -0
- package/client/host-call.js +47 -0
- package/client/host-transport.d.ts +70 -0
- package/client/host-transport.js +75 -0
- package/client/image-output.d.ts +6 -0
- package/client/image-output.js +13 -0
- package/client/index.d.ts +28 -0
- package/client/index.js +22 -0
- package/client/ink-waveform.d.ts +17 -0
- package/client/ink-waveform.js +68 -0
- package/client/live/apply-live-turn-tool-event.d.ts +14 -0
- package/client/live/apply-live-turn-tool-event.js +39 -0
- package/client/live/live-captions.d.ts +53 -0
- package/client/live/live-captions.js +174 -0
- package/client/live/live-mic-forward.d.ts +30 -0
- package/client/live/live-mic-forward.js +46 -0
- package/client/live/live-page-tool.d.ts +34 -0
- package/client/live/live-page-tool.js +11 -0
- package/client/live/live-state.d.ts +13 -0
- package/client/live/live-state.js +22 -0
- package/client/live/live-tool.d.ts +11 -0
- package/client/live/live-tool.js +1 -0
- package/client/live/live-video.d.ts +10 -0
- package/client/live/live-video.js +71 -0
- package/client/live/run-live-tool-call.d.ts +22 -0
- package/client/live/run-live-tool-call.js +70 -0
- package/client/live/use-live-caption-log.d.ts +17 -0
- package/client/live/use-live-caption-log.js +51 -0
- package/client/live-client.d.ts +189 -0
- package/client/live-client.js +829 -0
- package/client/live-errors.d.ts +2 -0
- package/client/live-errors.js +9 -0
- package/client/live-messages.d.ts +148 -0
- package/client/live-messages.js +100 -0
- package/client/mic-capture.d.ts +4 -0
- package/client/mic-capture.js +6 -0
- package/client/oauth-popup.d.ts +26 -0
- package/client/oauth-popup.js +30 -0
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/pcm-downsample.d.ts +4 -0
- package/client/pcm-downsample.js +45 -0
- package/client/run-commit.d.ts +34 -0
- package/client/run-commit.js +49 -0
- package/client/run-session.d.ts +79 -0
- package/client/run-session.js +207 -0
- package/client/schema-fields.d.ts +63 -0
- package/client/schema-fields.js +199 -0
- package/client/shaped-data.d.ts +118 -0
- package/client/shaped-data.js +343 -0
- package/client/source-citations.d.ts +13 -0
- package/client/source-citations.js +56 -0
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/state-fields.d.ts +16 -0
- package/client/state-fields.js +44 -0
- package/client/tool-result.d.ts +78 -0
- package/client/tool-result.js +293 -0
- package/client/tool-resume.d.ts +50 -0
- package/client/tool-resume.js +14 -0
- package/client/trace-feed.d.ts +13 -0
- package/client/trace-feed.js +18 -0
- package/client/trace-story.d.ts +163 -0
- package/client/trace-story.js +440 -0
- package/client/trace-view.d.ts +130 -0
- package/client/trace-view.js +333 -0
- package/client/transcript-groups.d.ts +140 -0
- package/client/transcript-groups.js +217 -0
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transcript-scroll.d.ts +7 -0
- package/client/transcript-scroll.js +6 -0
- package/client/transport.d.ts +264 -0
- package/client/transport.js +330 -0
- package/client/turn-client.d.ts +44 -0
- package/client/turn-client.js +88 -0
- package/client/voice-label.d.ts +2 -0
- package/client/voice-label.js +15 -0
- package/client/voice-recorder.d.ts +27 -0
- package/client/voice-recorder.js +162 -0
- package/client/wire-line.d.ts +56 -0
- package/client/wire-line.js +90 -0
- package/client/worklets/mic-capture.js +16 -0
- package/components/InkWaveform.d.ts +21 -0
- package/components/InkWaveform.js +131 -0
- package/components/live/use-live-runner-controls.d.ts +47 -0
- package/components/live/use-live-runner-controls.js +96 -0
- package/components/live/use-live-runner-model.d.ts +49 -0
- package/components/live/use-live-runner-model.js +190 -0
- package/components/live/use-live-runner-ui.d.ts +67 -0
- package/components/live/use-live-runner-ui.js +167 -0
- package/components/live/use-live-session-client.d.ts +41 -0
- package/components/live/use-live-session-client.js +126 -0
- package/components/use-composer-voice.d.ts +28 -0
- package/components/use-composer-voice.js +90 -0
- package/components/use-editor-selection.d.ts +3 -0
- package/components/use-editor-selection.js +19 -0
- package/components/use-voice-playback.d.ts +17 -0
- package/components/use-voice-playback.js +68 -0
- package/hooks/use-described.d.ts +12 -0
- package/hooks/use-described.js +25 -0
- package/hooks/use-embedding-index.d.ts +27 -0
- package/hooks/use-embedding-index.js +43 -0
- package/hooks/use-one-call.d.ts +31 -0
- package/hooks/use-one-call.js +76 -0
- package/hooks/use-page-tool-warnings.d.ts +6 -0
- package/hooks/use-page-tool-warnings.js +25 -0
- package/hooks/use-second-ticker.d.ts +2 -0
- package/hooks/use-second-ticker.js +13 -0
- package/hooks/use-theorem-chat-actions.d.ts +79 -0
- package/hooks/use-theorem-chat-actions.js +287 -0
- package/hooks/use-theorem-chat-state.d.ts +66 -0
- package/hooks/use-theorem-chat-state.js +122 -0
- package/hooks/use-theorem-chat.d.ts +97 -0
- package/hooks/use-theorem-chat.js +528 -0
- package/hooks/use-theorem-decision.d.ts +18 -0
- package/hooks/use-theorem-decision.js +12 -0
- package/hooks/use-theorem-embedding.d.ts +18 -0
- package/hooks/use-theorem-embedding.js +12 -0
- package/hooks/use-theorem-host.d.ts +45 -0
- package/hooks/use-theorem-host.js +135 -0
- package/hooks/use-theorem-interface.d.ts +22 -0
- package/hooks/use-theorem-interface.js +15 -0
- package/hooks/use-trace-records.d.ts +4 -0
- package/hooks/use-trace-records.js +7 -0
- package/index.d.ts +17 -0
- package/index.js +17 -0
- package/live.d.ts +9 -0
- package/live.js +10 -0
- package/package.json +74 -4
- package/server/checkpoint-ownership.d.ts +6 -0
- package/server/checkpoint-ownership.js +42 -0
- package/server/conversation-store.d.ts +92 -0
- package/server/conversation-store.js +131 -0
- package/server/conversation.d.ts +193 -0
- package/server/conversation.js +552 -0
- package/server/credential-store.d.ts +24 -0
- package/server/credential-store.js +14 -0
- package/server/decision-handler.d.ts +42 -0
- package/server/decision-handler.js +46 -0
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +152 -0
- package/server/handler.js +771 -0
- package/server/json-response.d.ts +2 -0
- package/server/json-response.js +7 -0
- package/server/mod.d.ts +25 -0
- package/server/mod.js +23 -0
- package/server/page-dispatch.d.ts +22 -0
- package/server/page-dispatch.js +127 -0
- package/server/request-body.d.ts +2 -0
- package/server/request-body.js +42 -0
- package/server/request-check.d.ts +21 -0
- package/server/request-check.js +49 -0
- package/server/session-authority.d.ts +8 -0
- package/server/session-authority.js +78 -0
- package/server/session-boundary.d.ts +26 -0
- package/server/session-boundary.js +64 -0
- package/server/session-identity.d.ts +16 -0
- package/server/session-identity.js +41 -0
- package/server/session-limits.d.ts +12 -0
- package/server/session-limits.js +59 -0
- package/server/session-store.d.ts +123 -0
- package/server/session-store.js +264 -0
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.d.ts +35 -0
- package/server/steer-inbox.js +52 -0
- package/server/tool-operations.d.ts +6 -0
- package/server/tool-operations.js +132 -0
- package/server/turn-input.d.ts +10 -0
- package/server/turn-input.js +29 -0
- package/styles/ink-controls.css +134 -0
- package/ui/CardWall.d.ts +10 -0
- package/ui/CardWall.js +61 -0
- package/ui/ChatComposerBar.d.ts +62 -0
- package/ui/ChatComposerBar.js +306 -0
- package/ui/ChatTranscript.d.ts +43 -0
- package/ui/ChatTranscript.js +527 -0
- package/ui/ConsoleFrame.d.ts +104 -0
- package/ui/ConsoleFrame.js +146 -0
- package/ui/DecisionAnswers.d.ts +53 -0
- package/ui/DecisionAnswers.js +233 -0
- package/ui/DecisionImages.d.ts +16 -0
- package/ui/DecisionImages.js +46 -0
- package/ui/EmbeddingDocuments.d.ts +78 -0
- package/ui/EmbeddingDocuments.js +278 -0
- package/ui/EmbeddingQuery.d.ts +27 -0
- package/ui/EmbeddingQuery.js +95 -0
- package/ui/EmbeddingResults.d.ts +41 -0
- package/ui/EmbeddingResults.js +165 -0
- package/ui/EmbeddingSpace.d.ts +26 -0
- package/ui/EmbeddingSpace.js +106 -0
- package/ui/LiveCaptionsPanel.d.ts +22 -0
- package/ui/LiveCaptionsPanel.js +27 -0
- package/ui/LiveRunner.d.ts +30 -0
- package/ui/LiveRunner.js +180 -0
- package/ui/PaneState.d.ts +33 -0
- package/ui/PaneState.js +50 -0
- package/ui/SchemaFields.d.ts +10 -0
- package/ui/SchemaFields.js +150 -0
- package/ui/ShapedData.d.ts +11 -0
- package/ui/ShapedData.js +196 -0
- package/ui/SidePanel.d.ts +126 -0
- package/ui/SidePanel.js +123 -0
- package/ui/StateFields.d.ts +44 -0
- package/ui/StateFields.js +139 -0
- package/ui/TheoremChat.d.ts +122 -0
- package/ui/TheoremChat.js +134 -0
- package/ui/TheoremDecision.d.ts +30 -0
- package/ui/TheoremDecision.js +130 -0
- package/ui/TheoremEmbedding.d.ts +27 -0
- package/ui/TheoremEmbedding.js +122 -0
- package/ui/TheoremHost.d.ts +55 -0
- package/ui/TheoremHost.js +240 -0
- package/ui/ToolGateCard.d.ts +32 -0
- package/ui/ToolGateCard.js +107 -0
- package/ui/ToolResult.d.ts +10 -0
- package/ui/ToolResult.js +62 -0
- package/ui/ToolResultCharts.d.ts +5 -0
- package/ui/ToolResultCharts.js +103 -0
- package/ui/TraceCharts.d.ts +13 -0
- package/ui/TraceCharts.js +266 -0
- package/ui/TraceGuardrails.d.ts +11 -0
- package/ui/TraceGuardrails.js +74 -0
- package/ui/TraceInspector.d.ts +16 -0
- package/ui/TraceInspector.js +206 -0
- package/ui/TraceInspectorPanel.d.ts +46 -0
- package/ui/TraceInspectorPanel.js +95 -0
- package/ui/TraceOverview.d.ts +38 -0
- package/ui/TraceOverview.js +126 -0
- package/ui/TraceSpanDetail.d.ts +10 -0
- package/ui/TraceSpanDetail.js +190 -0
- package/ui/TraceStory.d.ts +20 -0
- package/ui/TraceStory.js +108 -0
- package/ui/TraceValues.d.ts +36 -0
- package/ui/TraceValues.js +123 -0
- package/ui/TraceWaterfall.d.ts +13 -0
- package/ui/TraceWaterfall.js +135 -0
- package/ui/VoiceNote.d.ts +12 -0
- package/ui/VoiceNote.js +28 -0
- package/ui/approval-heading.d.ts +4 -0
- package/ui/approval-heading.js +9 -0
- package/ui/arrive.d.ts +18 -0
- package/ui/arrive.js +46 -0
- package/ui/built/theme.css +792 -0
- package/ui/built/theme.css.d.ts +9 -0
- package/ui/built/theorem.d.ts +13 -0
- package/ui/built/theorem.js +660 -0
- package/ui/built/theorem.variants.d.ts +22 -0
- package/ui/code-language.d.ts +12 -0
- package/ui/code-language.js +50 -0
- package/ui/disclosure-motion.d.ts +6 -0
- package/ui/disclosure-motion.js +207 -0
- package/ui/embedding-space-gl.d.ts +37 -0
- package/ui/embedding-space-gl.js +663 -0
- package/ui/embedding-space-view.d.ts +8 -0
- package/ui/embedding-space-view.js +8 -0
- package/ui/glide.d.ts +7 -0
- package/ui/glide.js +44 -0
- package/ui/icons.d.ts +4 -0
- package/ui/icons.js +38 -0
- package/ui/index.d.ts +36 -0
- package/ui/index.js +39 -0
- package/ui/labels-provider.d.ts +16 -0
- package/ui/labels-provider.js +64 -0
- package/ui/labels.d.ts +1442 -0
- package/ui/labels.js +1443 -0
- package/ui/markdown-plugins.d.ts +9 -0
- package/ui/markdown-plugins.js +13 -0
- package/ui/mcp-icon.d.ts +6 -0
- package/ui/mcp-icon.js +8 -0
- package/ui/row-keys.d.ts +19 -0
- package/ui/row-keys.js +33 -0
- package/ui/theme.d.ts +10 -0
- package/ui/theme.js +13 -0
- package/ui/theorem-theme.d.ts +10 -0
- package/ui/theorem-theme.js +105 -0
- package/ui/trace-palette.d.ts +10 -0
- package/ui/trace-palette.js +10 -0
- package/ui/trace-patterns.d.ts +40 -0
- package/ui/trace-patterns.js +57 -0
- package/ui/transcript-copy-text.d.ts +4 -0
- package/ui/transcript-copy-text.js +40 -0
- package/ui/wall-columns.d.ts +8 -0
- package/ui/wall-columns.js +19 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { AspectRatio } from '@astryxdesign/core/AspectRatio';
|
|
3
|
+
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, ChatSystemMessage, ChatToolCalls, } from '@astryxdesign/core/Chat';
|
|
4
|
+
import { Citation } from '@astryxdesign/core/Citation';
|
|
5
|
+
import { ClickableCard } from '@astryxdesign/core/ClickableCard';
|
|
6
|
+
import { Collapsible } from '@astryxdesign/core/Collapsible';
|
|
7
|
+
import { HStack } from '@astryxdesign/core/HStack';
|
|
8
|
+
import { useStreamingText } from '@astryxdesign/core/hooks';
|
|
9
|
+
import { Icon } from '@astryxdesign/core/Icon';
|
|
10
|
+
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
11
|
+
import { useLocale } from '@astryxdesign/core/i18n';
|
|
12
|
+
import { useLightbox } from '@astryxdesign/core/Lightbox';
|
|
13
|
+
import { Markdown } from '@astryxdesign/core/Markdown';
|
|
14
|
+
import { Skeleton } from '@astryxdesign/core/Skeleton';
|
|
15
|
+
import { Text } from '@astryxdesign/core/Text';
|
|
16
|
+
import { Thumbnail } from '@astryxdesign/core/Thumbnail';
|
|
17
|
+
import { Timestamp } from '@astryxdesign/core/Timestamp';
|
|
18
|
+
import { Token } from '@astryxdesign/core/Token';
|
|
19
|
+
import { Tooltip as HoverTip } from '@astryxdesign/core/Tooltip';
|
|
20
|
+
import { VStack } from '@astryxdesign/core/VStack';
|
|
21
|
+
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
|
22
|
+
import { stopText } from '@theoremjs/agents/interface';
|
|
23
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
24
|
+
import { citationsFromBlock } from "../client/source-citations.js";
|
|
25
|
+
import { assistantTurnCopyText, assistantTurnTiming, composeAssistantTurn, groupTranscriptBlocks, pendingPromptOf, promptReplyKey, promptTime, replyKey, toolCallLabel, workStatus, } from "../client/transcript-groups.js";
|
|
26
|
+
import { useSecondTicker } from "../hooks/use-second-ticker.js";
|
|
27
|
+
import { useDisclosureMotion } from "./disclosure-motion.js";
|
|
28
|
+
import { usageLine, workDuration, workStatusLabel } from "./labels.js";
|
|
29
|
+
import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
|
|
30
|
+
import { useMarkdownPlugins } from "./markdown-plugins.js";
|
|
31
|
+
import { keyedByContent } from "./row-keys.js";
|
|
32
|
+
import { ShapedData } from "./ShapedData.js";
|
|
33
|
+
import { ApprovalCard, AuthChallengeCard } from "./ToolGateCard.js";
|
|
34
|
+
import { transcriptBlockCopyText } from "./transcript-copy-text.js";
|
|
35
|
+
import { VoiceNote } from "./VoiceNote.js";
|
|
36
|
+
/** Whether the transcript shows usage (`ChatTranscriptProps.usage`). */
|
|
37
|
+
const ShowUsage = createContext(false);
|
|
38
|
+
/** Largest a generated image renders in the transcript. */
|
|
39
|
+
const GENERATED_IMAGE_MAX_WIDTH = 512;
|
|
40
|
+
/** First-seen time per group key, so timestamps don't jump while streaming. */
|
|
41
|
+
function useFirstSeen(keys) {
|
|
42
|
+
const times = useRef(new Map());
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
if (!times.current.has(key))
|
|
46
|
+
times.current.set(key, now);
|
|
47
|
+
}
|
|
48
|
+
return (key) => times.current.get(key) ?? now;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Each turn's span, keyed by the user group that started it. A turn stops at
|
|
52
|
+
* a gate and streams again under the same prompt once it's answered; the wait
|
|
53
|
+
* in between counts as paused, not worked.
|
|
54
|
+
*/
|
|
55
|
+
function useTurnSpans(streaming, lastUserKey) {
|
|
56
|
+
const spans = useRef(new Map());
|
|
57
|
+
// why: The first send mounts the transcript already streaming; that's the start.
|
|
58
|
+
const wasStreaming = useRef(false);
|
|
59
|
+
if (lastUserKey && streaming !== wasStreaming.current) {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const span = spans.current.get(lastUserKey);
|
|
62
|
+
if (streaming) {
|
|
63
|
+
const pausedMs = span?.endedAt === undefined ? 0 : span.pausedMs + now - span.endedAt;
|
|
64
|
+
spans.current.set(lastUserKey, { pausedMs });
|
|
65
|
+
}
|
|
66
|
+
else if (span) {
|
|
67
|
+
spans.current.set(lastUserKey, { ...span, endedAt: now });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
wasStreaming.current = streaming;
|
|
71
|
+
return spans.current;
|
|
72
|
+
}
|
|
73
|
+
function CopyButton({ text }) {
|
|
74
|
+
const t = useLabels();
|
|
75
|
+
const [copied, setCopied] = useState(false);
|
|
76
|
+
if (!text.trim())
|
|
77
|
+
return null;
|
|
78
|
+
const label = t(copied ? '@theorem.transcript.copied' : '@theorem.transcript.copy');
|
|
79
|
+
return (_jsx(IconButton, { label: label, tooltip: label, size: "sm", variant: "ghost", icon: _jsx(Icon, { icon: copied ? IconCheck : IconCopy, size: "xsm", color: "secondary" }), onClick: () => {
|
|
80
|
+
void navigator.clipboard.writeText(text).then(() => {
|
|
81
|
+
setCopied(true);
|
|
82
|
+
globalThis.setTimeout(() => setCopied(false), 1500);
|
|
83
|
+
});
|
|
84
|
+
} }));
|
|
85
|
+
}
|
|
86
|
+
const MINUTE_MS = 60_000;
|
|
87
|
+
/**
|
|
88
|
+
* Astryx's relative time, counted in minutes: "now" for the first minute, then
|
|
89
|
+
* "1m ago", "5m ago", … (its own formatter shows "15s ago" in between).
|
|
90
|
+
*/
|
|
91
|
+
function MessageTime({ at }) {
|
|
92
|
+
const t = useLabels();
|
|
93
|
+
const [isFresh, setIsFresh] = useState(() => Date.now() - at < MINUTE_MS);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!isFresh)
|
|
96
|
+
return;
|
|
97
|
+
const timer = globalThis.setTimeout(() => setIsFresh(false), Math.max(0, at + MINUTE_MS - Date.now()));
|
|
98
|
+
return () => globalThis.clearTimeout(timer);
|
|
99
|
+
}, [isFresh, at]);
|
|
100
|
+
if (isFresh) {
|
|
101
|
+
return (_jsx(Text, { type: "supporting", color: "secondary", children: t('@theorem.transcript.now') }));
|
|
102
|
+
}
|
|
103
|
+
return _jsx(Timestamp, { value: at, format: "relative_short", isLive: true });
|
|
104
|
+
}
|
|
105
|
+
/** What a reply or a called agent used, in the viewer's locale. */
|
|
106
|
+
function Usage({ tokens }) {
|
|
107
|
+
return usageLine(useLabels(), useLocale(), tokens);
|
|
108
|
+
}
|
|
109
|
+
/** Why a turn stopped short, drawn as the delivery statuses are. */
|
|
110
|
+
function Stopped({ label }) {
|
|
111
|
+
return (_jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(Icon, { icon: "stop", size: "xsm", color: "inherit" }), _jsx("span", { children: label })] }));
|
|
112
|
+
}
|
|
113
|
+
/** A failed message's status, drawn as Astryx draws its error status; the reason shows on hover. */
|
|
114
|
+
function Failed({ reason }) {
|
|
115
|
+
const label = useLabels()('@theorem.transcript.failed');
|
|
116
|
+
return (_jsx(HoverTip, { content: reason, children: _jsxs(HStack, { gap: 1, vAlign: "center", style: { color: 'var(--color-error)' }, children: [_jsx(Icon, { icon: "error", size: "xsm", color: "inherit" }), _jsx("span", { children: label })] }) }));
|
|
117
|
+
}
|
|
118
|
+
/** A message's copy button and time, the button on the outside: right for the person, left for the agent. */
|
|
119
|
+
function CopyAndTime(props) {
|
|
120
|
+
const copy = _jsx(CopyButton, { text: props.copyText });
|
|
121
|
+
const time = _jsx(MessageTime, { at: props.at });
|
|
122
|
+
return (_jsxs(HStack, { gap: 1, vAlign: "center", children: [props.sender === 'user' ? time : copy, props.sender === 'user' ? copy : time] }));
|
|
123
|
+
}
|
|
124
|
+
/** A message's copy button (outermost), time and status; a failure says why on hovering its status. */
|
|
125
|
+
function MessageChrome(props) {
|
|
126
|
+
const usage = props.usage ? _jsx(Usage, { tokens: props.usage }) : undefined;
|
|
127
|
+
const aside = props.error !== undefined ? (_jsx(Failed, { reason: props.error })) : props.stopped !== undefined ? (_jsx(Stopped, { label: props.stopped })) : (usage);
|
|
128
|
+
return (_jsx(ChatMessageMetadata
|
|
129
|
+
// why: The row's first slot is the one nearest the margin.
|
|
130
|
+
, {
|
|
131
|
+
// why: The row's first slot is the one nearest the margin.
|
|
132
|
+
timestamp: _jsx(CopyAndTime, { sender: props.sender, at: props.at, copyText: props.copyText }),
|
|
133
|
+
// why: Astryx's statuses are a closed set, so these ride the footer.
|
|
134
|
+
footer: aside, status: props.error !== undefined || props.stopped !== undefined ? undefined : props.status }));
|
|
135
|
+
}
|
|
136
|
+
function dataUrl(mimeType, data) {
|
|
137
|
+
return data === undefined ? undefined : `data:${mimeType};base64,${data}`;
|
|
138
|
+
}
|
|
139
|
+
function ImageAttachment({ src, name }) {
|
|
140
|
+
const lightbox = useLightbox({ media: { src, alt: name, caption: name }, hasZoom: true });
|
|
141
|
+
return (_jsxs(_Fragment, { children: [_jsx(Thumbnail, { src: src, alt: name, label: name, onClick: () => lightbox.open() }), lightbox.element] }));
|
|
142
|
+
}
|
|
143
|
+
function UserBlock({ block, metadata }) {
|
|
144
|
+
if (block.kind === 'user-text')
|
|
145
|
+
return _jsx(ChatMessageBubble, { metadata: metadata, children: block.text });
|
|
146
|
+
if (block.kind !== 'user-attachment' && block.kind !== 'user-voice')
|
|
147
|
+
return null;
|
|
148
|
+
return _jsx(UserFile, { block: block });
|
|
149
|
+
}
|
|
150
|
+
/** An attached image or voice note inline; any other file as a Token. */
|
|
151
|
+
function UserFile({ block }) {
|
|
152
|
+
const src = dataUrl(block.mimeType, block.data);
|
|
153
|
+
if (src && block.mimeType.startsWith('image/')) {
|
|
154
|
+
return _jsx(ImageAttachment, { src: src, name: block.name });
|
|
155
|
+
}
|
|
156
|
+
if (src && block.mimeType.startsWith('audio/')) {
|
|
157
|
+
return _jsx(VoiceNote, { src: src, mimeType: block.mimeType });
|
|
158
|
+
}
|
|
159
|
+
return _jsx(Token, { label: block.name, description: block.mimeType });
|
|
160
|
+
}
|
|
161
|
+
function mediaSrc(block) {
|
|
162
|
+
return block.url ?? dataUrl(block.mimeType, block.data);
|
|
163
|
+
}
|
|
164
|
+
function lightboxMedia(t, src, mimeType) {
|
|
165
|
+
if (mimeType.startsWith('image/'))
|
|
166
|
+
return { src, alt: t('@theorem.transcript.generated_image'), type: 'image' };
|
|
167
|
+
if (mimeType.startsWith('video/'))
|
|
168
|
+
return { src, alt: t('@theorem.transcript.generated_video'), type: 'video' };
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
/** Image or video media: shown as Thumbnails in a gallery row, not as its own body row. */
|
|
172
|
+
function galleryItemOf(t, block) {
|
|
173
|
+
if (block.kind !== 'media')
|
|
174
|
+
return undefined;
|
|
175
|
+
const src = mediaSrc(block);
|
|
176
|
+
const media = src ? lightboxMedia(t, src, block.mimeType) : undefined;
|
|
177
|
+
return media ? { media, preview: block.previewUrl ?? media.src } : undefined;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Consecutive images and videos as one wrapping row of Astryx Thumbnails that
|
|
181
|
+
* share a Lightbox gallery (Astryx's `useLightbox({ media: [...] })` pattern).
|
|
182
|
+
*/
|
|
183
|
+
function MediaGallery({ items }) {
|
|
184
|
+
const lightbox = useLightbox({ media: items.map((item) => item.media), hasZoom: true });
|
|
185
|
+
return (_jsxs(HStack, { gap: 2, wrap: "wrap", children: [items.map(({ media: item, preview }, i) => (_jsx(Thumbnail, { src: item.type === 'image' ? preview : undefined, alt: item.alt, label: item.alt, onClick: () => lightbox.open(i) }, item.src))), lightbox.element] }));
|
|
186
|
+
}
|
|
187
|
+
/** One generated image, framed to its natural ratio once loaded (the pinned ratio until then). */
|
|
188
|
+
function GeneratedImage(props) {
|
|
189
|
+
const t = useLabels();
|
|
190
|
+
const [natural, setNatural] = useState();
|
|
191
|
+
return (_jsx(ClickableCard, { label: t('@theorem.transcript.open_generated_image'), onClick: props.onOpen, padding: 0, width: "100%", maxWidth: GENERATED_IMAGE_MAX_WIDTH, children: _jsx(AspectRatio, { ratio: natural ?? props.ratio ?? 1, fit: "cover", children: _jsx("img", { src: props.item.preview, alt: props.item.media.alt, onLoad: (event) => {
|
|
192
|
+
const { naturalWidth, naturalHeight } = event.currentTarget;
|
|
193
|
+
if (naturalWidth > 0 && naturalHeight > 0)
|
|
194
|
+
setNatural(naturalWidth / naturalHeight);
|
|
195
|
+
} }) }) }));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Image-profile gallery: images are the answer, so they show large at their
|
|
199
|
+
* own ratio. Anything else (video) keeps the square Thumbnail.
|
|
200
|
+
*/
|
|
201
|
+
function GeneratedGallery({ items, ratio }) {
|
|
202
|
+
const lightbox = useLightbox({ media: items.map((item) => item.media), hasZoom: true });
|
|
203
|
+
return (_jsxs(VStack, { gap: 2, width: "100%", children: [items.map((item, i) => item.media.type === 'image' ? (_jsx(GeneratedImage, { item: item, ratio: ratio, onOpen: () => lightbox.open(i) }, item.media.src)) : (_jsx(Thumbnail, { alt: item.media.alt, label: item.media.alt, onClick: () => lightbox.open(i) }, item.media.src))), lightbox.element] }));
|
|
204
|
+
}
|
|
205
|
+
/** An image takes seconds: a slow, smooth breath rather than the Skeleton's quick stepped blink. */
|
|
206
|
+
const GENERATING_PULSE = {
|
|
207
|
+
animationDuration: '1.8s',
|
|
208
|
+
animationTimingFunction: 'ease-in-out',
|
|
209
|
+
};
|
|
210
|
+
/** Placeholder shaped like the image being generated. */
|
|
211
|
+
function GeneratingImage({ ratio }) {
|
|
212
|
+
const t = useLabels();
|
|
213
|
+
return (_jsx(VStack, { width: "100%", maxWidth: GENERATED_IMAGE_MAX_WIDTH, children: _jsx(AspectRatio, { ratio: ratio ?? 1, children: _jsx(Skeleton, { width: "100%", height: "100%", radius: 3, style: GENERATING_PULSE, "aria-label": t('@theorem.transcript.generating_image') }) }) }));
|
|
214
|
+
}
|
|
215
|
+
function MediaBlock({ block }) {
|
|
216
|
+
const src = mediaSrc(block);
|
|
217
|
+
if (src && block.mimeType.startsWith('audio/')) {
|
|
218
|
+
return _jsx(VoiceNote, { src: src, mimeType: block.mimeType });
|
|
219
|
+
}
|
|
220
|
+
return _jsx(Text, { color: "secondary", children: block.mimeType });
|
|
221
|
+
}
|
|
222
|
+
/** Runs of images/videos become one gallery row; everything else is a row of its own. */
|
|
223
|
+
function bodyRows(t, body) {
|
|
224
|
+
const rows = [];
|
|
225
|
+
for (const block of body) {
|
|
226
|
+
const item = galleryItemOf(t, block);
|
|
227
|
+
const last = rows.at(-1);
|
|
228
|
+
if (item && last?.kind === 'gallery')
|
|
229
|
+
last.items.push(item);
|
|
230
|
+
else if (item)
|
|
231
|
+
rows.push({ kind: 'gallery', items: [item] });
|
|
232
|
+
else
|
|
233
|
+
rows.push({ kind: 'block', block });
|
|
234
|
+
}
|
|
235
|
+
return rows;
|
|
236
|
+
}
|
|
237
|
+
function Sources({ block }) {
|
|
238
|
+
const t = useLabels();
|
|
239
|
+
const citations = citationsFromBlock(block);
|
|
240
|
+
if (citations.length === 0)
|
|
241
|
+
return null;
|
|
242
|
+
return (_jsx(HStack, { gap: 1, wrap: "wrap", "aria-label": t('@theorem.transcript.sources'), children: citations.map((citation, i) => (_jsx(Citation, { source: { title: citation.title, url: citation.href, src: citation.icon }, number: i + 1 }, citation.key))) }));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Streamed text (the reply, or the latest thought). Guardrails release text in batches (the last one when
|
|
246
|
+
* the stream closes), and Astryx's reveal snaps to the full text once
|
|
247
|
+
* `isStreaming` goes false — so keep the reveal on until Astryx's own
|
|
248
|
+
* `useStreamingText` has caught up with everything received.
|
|
249
|
+
*/
|
|
250
|
+
function StreamedMarkdown({ text, streaming, density, components, }) {
|
|
251
|
+
const [revealing, setRevealing] = useState(streaming);
|
|
252
|
+
if (streaming && !revealing)
|
|
253
|
+
setRevealing(true);
|
|
254
|
+
const plugins = useMarkdownPlugins();
|
|
255
|
+
const shown = useStreamingText(text, revealing);
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (!streaming && shown.length >= text.length)
|
|
258
|
+
setRevealing(false);
|
|
259
|
+
}, [streaming, shown, text]);
|
|
260
|
+
return (_jsx(Markdown, { isStreaming: revealing, density: density, components: components, plugins: plugins, children: text }));
|
|
261
|
+
}
|
|
262
|
+
function BodyBlock({ block, streaming }) {
|
|
263
|
+
if (block.kind === 'text')
|
|
264
|
+
return _jsx(StreamedMarkdown, { text: block.text, streaming: streaming });
|
|
265
|
+
if (block.kind === 'tool')
|
|
266
|
+
return _jsx(ToolCall, { tool: block.tool });
|
|
267
|
+
return _jsx(ResultBlock, { block: block });
|
|
268
|
+
}
|
|
269
|
+
/** Non-streaming answer rows: errors, sources, structured output, and media. */
|
|
270
|
+
function ResultBlock({ block }) {
|
|
271
|
+
switch (block.kind) {
|
|
272
|
+
case 'error':
|
|
273
|
+
return null;
|
|
274
|
+
case 'citation':
|
|
275
|
+
return _jsx(Sources, { block: block });
|
|
276
|
+
case 'structured':
|
|
277
|
+
return _jsx(ShapedData, { value: block.value });
|
|
278
|
+
case 'media':
|
|
279
|
+
return _jsx(MediaBlock, { block: block });
|
|
280
|
+
default:
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// why: Astryx sets a call's name in the code font; labels are sentences, so the row takes the body
|
|
285
|
+
// font and the detail restores the code font kept on the wrapper.
|
|
286
|
+
const CODE_FONT_KEPT = {
|
|
287
|
+
display: 'contents',
|
|
288
|
+
'--theorem-font-code': 'var(--font-family-code)',
|
|
289
|
+
};
|
|
290
|
+
const CODE_FONT_AS_BODY = { '--font-family-code': 'var(--font-family-body)' };
|
|
291
|
+
const CODE_FONT_RESTORED = { '--font-family-code': 'var(--theorem-font-code)' };
|
|
292
|
+
function ToolCalls({ calls }) {
|
|
293
|
+
return (_jsx("div", { style: CODE_FONT_KEPT, children: _jsx(ChatToolCalls, { calls: calls, style: CODE_FONT_AS_BODY }) }));
|
|
294
|
+
}
|
|
295
|
+
/** A call's detail: what it ran with, then what came back. */
|
|
296
|
+
function toolDetail(t, tool, result) {
|
|
297
|
+
const input = tool.edited ? (_jsx(ShapedData, { value: tool.edited.to, title: t('@theorem.transcript.tool_input_edited') })) : (_jsx(ShapedData, { value: tool.arguments, title: t('@theorem.transcript.tool_input') }));
|
|
298
|
+
return (_jsxs(VStack, { gap: 2, style: CODE_FONT_RESTORED, children: [_jsx(Text, { type: "code", size: "sm", color: "secondary", children: tool.name }), input, result] }));
|
|
299
|
+
}
|
|
300
|
+
/** How long a finished call ran, when both ends were seen. */
|
|
301
|
+
function toolDuration(t, tool) {
|
|
302
|
+
if (tool.startedAt === undefined || tool.endedAt === undefined)
|
|
303
|
+
return {};
|
|
304
|
+
return { duration: workDuration(t, tool.endedAt - tool.startedAt) };
|
|
305
|
+
}
|
|
306
|
+
/** What the agent an agent tool ran used, after the call's name. Only agent tools report it. */
|
|
307
|
+
function toolUsageStats(state, showUsage) {
|
|
308
|
+
return showUsage && state.tokens ? { stats: _jsx(Usage, { tokens: state.tokens }) } : {};
|
|
309
|
+
}
|
|
310
|
+
function toolCallItem(t, id, tool, showUsage) {
|
|
311
|
+
const base = { key: id, name: toolCallLabel(tool) };
|
|
312
|
+
const { state } = tool;
|
|
313
|
+
switch (state?.phase) {
|
|
314
|
+
case 'error':
|
|
315
|
+
return {
|
|
316
|
+
...base,
|
|
317
|
+
status: 'error',
|
|
318
|
+
target: state.failure.message,
|
|
319
|
+
...toolUsageStats(state, showUsage),
|
|
320
|
+
errorMessage: state.failure.message,
|
|
321
|
+
resultDetail: toolDetail(t, tool, _jsx(ShapedData, { value: state.failure, title: t('@theorem.transcript.tool_error') })),
|
|
322
|
+
};
|
|
323
|
+
case 'complete':
|
|
324
|
+
return {
|
|
325
|
+
...base,
|
|
326
|
+
status: 'complete',
|
|
327
|
+
...toolDuration(t, tool),
|
|
328
|
+
...toolUsageStats(state, showUsage),
|
|
329
|
+
resultDetail: toolDetail(t, tool, state.output === undefined ? undefined : (_jsx(ShapedData, { value: state.output, title: t('@theorem.transcript.tool_output') }))),
|
|
330
|
+
};
|
|
331
|
+
case 'running':
|
|
332
|
+
return { ...base, status: 'running', resultDetail: toolDetail(t, tool) };
|
|
333
|
+
default:
|
|
334
|
+
return { ...base, status: 'pending', resultDetail: toolDetail(t, tool) };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function ToolCall({ tool }) {
|
|
338
|
+
const t = useLabels();
|
|
339
|
+
const showUsage = useContext(ShowUsage);
|
|
340
|
+
return _jsx(ToolCalls, { calls: [toolCallItem(t, tool.name, tool, showUsage)] });
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Thinking reads as quieter text than the reply. Astryx has no reasoning
|
|
344
|
+
* component, and Markdown draws its own Text, so restyle through its
|
|
345
|
+
* documented `components` seam.
|
|
346
|
+
*/
|
|
347
|
+
const THOUGHT_MARKDOWN = {
|
|
348
|
+
paragraph: ({ children }) => (_jsx(Text, { size: "sm", color: "secondary", display: "block", as: "p", children: children })),
|
|
349
|
+
heading: ({ children }) => (_jsx(Text, { size: "sm", color: "secondary", weight: "semibold", display: "block", as: "p", children: children })),
|
|
350
|
+
};
|
|
351
|
+
function TraceList({ items, streaming }) {
|
|
352
|
+
const t = useLabels();
|
|
353
|
+
const showUsage = useContext(ShowUsage);
|
|
354
|
+
const rows = [];
|
|
355
|
+
let tools = [];
|
|
356
|
+
const flush = () => {
|
|
357
|
+
if (tools.length === 0)
|
|
358
|
+
return;
|
|
359
|
+
rows.push(_jsx(ToolCalls, { calls: tools }, tools[0]?.key));
|
|
360
|
+
tools = [];
|
|
361
|
+
};
|
|
362
|
+
for (const [i, item] of items.entries()) {
|
|
363
|
+
if (item.kind === 'tool') {
|
|
364
|
+
tools.push(toolCallItem(t, item.id, item.block.tool, showUsage));
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
flush();
|
|
368
|
+
rows.push(_jsx(StreamedMarkdown, { text: item.text, streaming: streaming && i === items.length - 1, density: "compact", components: THOUGHT_MARKDOWN }, item.id));
|
|
369
|
+
}
|
|
370
|
+
flush();
|
|
371
|
+
return _jsx(VStack, { gap: 2, children: rows });
|
|
372
|
+
}
|
|
373
|
+
/** The gate a person decides. The page answers a page gate, so it has none. */
|
|
374
|
+
function personGate(tool) {
|
|
375
|
+
if (tool.state?.phase !== 'gate' || tool.state.gate.kind === 'page')
|
|
376
|
+
return null;
|
|
377
|
+
return tool.state.gate;
|
|
378
|
+
}
|
|
379
|
+
/** The answer on its way for this tool's gate, if the one being sent is this tool's. */
|
|
380
|
+
function answerSent(answering, tool) {
|
|
381
|
+
const ours = answering?.callId === tool.callId && answering?.operationId === tool.operationId;
|
|
382
|
+
return ours ? answering.action : null;
|
|
383
|
+
}
|
|
384
|
+
function GateCard({ block, handle, handlers, }) {
|
|
385
|
+
const { tool } = block;
|
|
386
|
+
const gate = personGate(tool);
|
|
387
|
+
if (!gate)
|
|
388
|
+
return null;
|
|
389
|
+
const index = handlers.indexOf(block);
|
|
390
|
+
const answer = answerSent(handlers.answering, tool);
|
|
391
|
+
if (gate.kind === 'auth') {
|
|
392
|
+
return (_jsx(AuthChallengeCard, { gate: gate, toolName: tool.name, submitted: answer === 'auth', onAuthenticated: (secret) => handlers.onAuthenticated?.(index, secret) }));
|
|
393
|
+
}
|
|
394
|
+
return (_jsx(ApprovalCard, { gate: gate, toolName: tool.name, agent: handle, input: tool.arguments, decided: answer === 'auth' ? null : answer, onDecision: (action) => handlers.onToolDecision?.(index, action) }));
|
|
395
|
+
}
|
|
396
|
+
/** Live elapsed time while the turn streams; its final duration once it ends. */
|
|
397
|
+
/** Live: counted from the start. Stopped: the work its blocks record. */
|
|
398
|
+
function useTurnElapsed(streaming, startedAt, workedMs) {
|
|
399
|
+
const now = useSecondTicker(streaming && startedAt !== undefined);
|
|
400
|
+
if (!streaming)
|
|
401
|
+
return workedMs;
|
|
402
|
+
return startedAt !== undefined ? now - startedAt : undefined;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The turn's work status; with a trace, a Collapsible over its thinking and
|
|
406
|
+
* tool calls, open while the turn runs so they can be followed, folded away
|
|
407
|
+
* when it ends.
|
|
408
|
+
*/
|
|
409
|
+
function TurnStatus(props) {
|
|
410
|
+
const { status, streaming } = props;
|
|
411
|
+
const [traceOpen, setTraceOpen] = useState(streaming);
|
|
412
|
+
const [wasStreaming, setWasStreaming] = useState(streaming);
|
|
413
|
+
if (wasStreaming !== streaming) {
|
|
414
|
+
setWasStreaming(streaming);
|
|
415
|
+
setTraceOpen(streaming);
|
|
416
|
+
}
|
|
417
|
+
if (!status)
|
|
418
|
+
return null;
|
|
419
|
+
const label = (_jsx(Text, { size: "sm", color: "secondary", children: status }));
|
|
420
|
+
if (!props.hasTrace)
|
|
421
|
+
return label;
|
|
422
|
+
return (_jsx(Collapsible, { trigger: label, isOpen: traceOpen, onOpenChange: setTraceOpen, children: _jsx(TraceList, { items: props.trace, streaming: streaming }) }));
|
|
423
|
+
}
|
|
424
|
+
function BodyRowView({ row, streaming, imageOutput, }) {
|
|
425
|
+
if (row.kind === 'block')
|
|
426
|
+
return _jsx(BodyBlock, { block: row.block, streaming: streaming });
|
|
427
|
+
if (imageOutput)
|
|
428
|
+
return _jsx(GeneratedGallery, { items: row.items, ratio: imageOutput.ratio });
|
|
429
|
+
return _jsx(MediaGallery, { items: row.items });
|
|
430
|
+
}
|
|
431
|
+
/** An image profile's placeholder until the turn's first image arrives. */
|
|
432
|
+
function PendingImage(props) {
|
|
433
|
+
if (!props.imageOutput || !props.streaming || props.rows.some((row) => row.kind === 'gallery'))
|
|
434
|
+
return null;
|
|
435
|
+
return _jsx(GeneratingImage, { ratio: props.imageOutput.ratio });
|
|
436
|
+
}
|
|
437
|
+
function AssistantTurn(props) {
|
|
438
|
+
const t = useLabels();
|
|
439
|
+
const showUsage = useContext(ShowUsage);
|
|
440
|
+
const elapsedMs = useTurnElapsed(props.streaming, props.startedAt, props.workedMs);
|
|
441
|
+
const { trace, gatedTools, body, hasTrace } = composeAssistantTurn(props.blocks);
|
|
442
|
+
const rows = bodyRows(t, body);
|
|
443
|
+
const status = workStatusLabel(t, workStatus({ streaming: props.streaming, hasTrace, elapsedMs }));
|
|
444
|
+
const copyText = assistantTurnCopyText(body.length > 0 ? body : props.blocks);
|
|
445
|
+
return (_jsx(ChatMessage, { sender: "assistant", name: props.handle, metadata: props.streaming ? undefined : (_jsx(MessageChrome, { sender: "assistant", at: props.at, copyText: copyText, error: props.error, stopped: props.stopped, usage: showUsage ? props.usage : undefined })), children: _jsxs(VStack, { gap: 3, width: "100%", children: [_jsx(TurnStatus, { status: status, trace: trace, hasTrace: hasTrace, streaming: props.streaming }), gatedTools.map((block) => (_jsx(GateCard, { block: block, handle: props.handle, handlers: props.handlers }, block.id))), keyedByContent(rows, (row) => (row.kind === 'gallery' ? 'gallery' : row.block.kind)).map(({ item: row, index: i, key }) => (_jsx(BodyRowView
|
|
446
|
+
// why: Not the block id: the committed turn re-mints ids, and a
|
|
447
|
+
// remount would cut the text reveal short.
|
|
448
|
+
, { row: row, streaming: props.streaming && i === rows.length - 1, imageOutput: props.imageOutput }, key))), _jsx(PendingImage, { imageOutput: props.imageOutput, streaming: props.streaming, rows: rows })] }) }));
|
|
449
|
+
}
|
|
450
|
+
function UserTurn(props) {
|
|
451
|
+
const t = useLabels();
|
|
452
|
+
const copyText = props.blocks
|
|
453
|
+
.map((block) => transcriptBlockCopyText(t, block))
|
|
454
|
+
.filter(Boolean)
|
|
455
|
+
.join('\n\n');
|
|
456
|
+
const chrome = (_jsx(MessageChrome, { sender: "user", at: props.at, copyText: copyText, status: props.status, stopped: props.stopped, error: props.error }));
|
|
457
|
+
// why: Astryx: metadata goes on the last bubble, or on the message when the last
|
|
458
|
+
// content is unbubbled (an attachment or voice note).
|
|
459
|
+
const last = props.blocks.at(-1);
|
|
460
|
+
const lastIsBubble = last?.kind === 'user-text';
|
|
461
|
+
return (_jsx(ChatMessage, { sender: "user", metadata: lastIsBubble ? undefined : chrome, children: props.blocks.map((block) => (_jsx(UserBlock, { block: block, metadata: block === last && lastIsBubble ? chrome : undefined }, block.id))) }));
|
|
462
|
+
}
|
|
463
|
+
/** Why a turn failed: its error block's message. */
|
|
464
|
+
function failureOf(group) {
|
|
465
|
+
return group?.blocks.findLast((block) => block.kind === 'error')?.message;
|
|
466
|
+
}
|
|
467
|
+
/** A reply that is nothing but its failure: the turn failed before the model answered. */
|
|
468
|
+
function isBareFailure(group) {
|
|
469
|
+
return group?.kind === 'assistant' && group.blocks.every((block) => block.kind === 'error');
|
|
470
|
+
}
|
|
471
|
+
/** Theorem transcript blocks rendered as Astryx chat messages. */
|
|
472
|
+
export function ChatTranscript(props) {
|
|
473
|
+
return (_jsx(TheoremLabelsProvider, { children: _jsx(ShowUsage.Provider, { value: props.usage === true, children: _jsx(ChatTranscriptBody, { ...props }) }) }));
|
|
474
|
+
}
|
|
475
|
+
function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToolDecision, answering = null, onAuthenticated, emptyState, dividers, imageOutput, labels, }) {
|
|
476
|
+
const groups = useMemo(() => groupTranscriptBlocks(blocks, dividers && new Set(Object.keys(dividers))), [blocks, dividers]);
|
|
477
|
+
const timeOf = useFirstSeen(groups.map((group, index) => (group.kind === 'user' ? group.key : replyKey(groups, index))));
|
|
478
|
+
const spans = useTurnSpans(streaming, groups.findLast((group) => group.kind === 'user')?.key);
|
|
479
|
+
const pendingPrompt = streaming ? pendingPromptOf(groups) : undefined;
|
|
480
|
+
const listRef = useRef(null);
|
|
481
|
+
useDisclosureMotion(listRef);
|
|
482
|
+
const handlers = {
|
|
483
|
+
indexOf: (block) => blocks.findIndex((entry) => entry.id === block.id),
|
|
484
|
+
answering,
|
|
485
|
+
onToolDecision,
|
|
486
|
+
onAuthenticated,
|
|
487
|
+
};
|
|
488
|
+
const turn = { handle, handlers, imageOutput };
|
|
489
|
+
const lastUser = groups.findLastIndex((group) => group.kind === 'user');
|
|
490
|
+
const userTurn = (group, index) => {
|
|
491
|
+
const next = groups[index + 1];
|
|
492
|
+
const error = isBareFailure(next) ? failureOf(next) : undefined;
|
|
493
|
+
const status = index === lastUser ? (delivery ?? undefined) : undefined;
|
|
494
|
+
return (_jsx(UserTurn, { blocks: group.blocks, at: promptTime(groups, index, timeOf), status: status, stopped: stopText(group.stop, group.blocks, labels), error: error }, group.key));
|
|
495
|
+
};
|
|
496
|
+
const turnOf = (group, index) => {
|
|
497
|
+
if (group.kind === 'user')
|
|
498
|
+
return userTurn(group, index);
|
|
499
|
+
if (isBareFailure(group) && groups[index - 1]?.kind === 'user')
|
|
500
|
+
return [];
|
|
501
|
+
const { key, live, endedAt, ...timing } = assistantTurnTiming({
|
|
502
|
+
groups,
|
|
503
|
+
index,
|
|
504
|
+
streaming,
|
|
505
|
+
timeOf,
|
|
506
|
+
spans,
|
|
507
|
+
});
|
|
508
|
+
// why: A reply is dated when it last stopped: a reply that just finished reads "now".
|
|
509
|
+
const at = endedAt ?? timeOf(key);
|
|
510
|
+
return (_jsx(AssistantTurn, { ...turn, ...timing, blocks: group.blocks, streaming: live, at: at, error: failureOf(group), stopped: stopText(group.stop, group.blocks, labels), usage: group.usage }, key));
|
|
511
|
+
};
|
|
512
|
+
const turns = groups.flatMap((group, index) => {
|
|
513
|
+
const label = dividers?.[group.key];
|
|
514
|
+
if (label === undefined)
|
|
515
|
+
return turnOf(group, index);
|
|
516
|
+
return [
|
|
517
|
+
_jsx(ChatSystemMessage, { variant: "divider", children: label }, `divider:${group.key}`),
|
|
518
|
+
turnOf(group, index),
|
|
519
|
+
].flat();
|
|
520
|
+
});
|
|
521
|
+
// why: It sits in the same keyed list as the streamed reply, so the reply stays
|
|
522
|
+
// one message; a remount would read to the layout as a new message.
|
|
523
|
+
if (pendingPrompt) {
|
|
524
|
+
turns.push(_jsx(AssistantTurn, { ...turn, blocks: [], streaming: true, at: timeOf(pendingPrompt.key), startedAt: timeOf(pendingPrompt.key) }, promptReplyKey(pendingPrompt)));
|
|
525
|
+
}
|
|
526
|
+
return (_jsx(ChatMessageList, { ref: listRef, isStreaming: streaming, emptyState: emptyState, children: turns }));
|
|
527
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { DefinedTheme } from '@astryxdesign/core/theme';
|
|
2
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
3
|
+
import type { HttpOptions } from '../client/transport.ts';
|
|
4
|
+
import type { TheoremLabels } from './labels.ts';
|
|
5
|
+
import { type TraceInspector } from './TraceInspectorPanel.tsx';
|
|
6
|
+
/** The props a one-call console page shares: its look, its wording, its trace, and its layout. */
|
|
7
|
+
export type ConsolePageProps = {
|
|
8
|
+
/** Extra fetch options for the default HTTP transport (auth headers, custom fetch). */
|
|
9
|
+
http?: HttpOptions;
|
|
10
|
+
theme?: DefinedTheme;
|
|
11
|
+
mode?: 'system' | 'light' | 'dark';
|
|
12
|
+
/** Replacement lines by locale, for any `@theorem.*` or `@astryx.*` key. */
|
|
13
|
+
labels?: TheoremLabels;
|
|
14
|
+
/** Widest the column may grow, as a CSS length. Default `960px`. */
|
|
15
|
+
maxWidth?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Drive the trace from the host's own control. The built-in toggle hides.
|
|
18
|
+
* The open trace takes the result's place, unless this page is inside
|
|
19
|
+
* `TracePlacement value="panel"`, where it docks. Omit to keep the toggle.
|
|
20
|
+
* Needs a profile that records traces.
|
|
21
|
+
*/
|
|
22
|
+
trace?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The page is already a raised panel. The console sits in it instead of
|
|
25
|
+
* painting the ground it uses on a flat page.
|
|
26
|
+
*/
|
|
27
|
+
flush?: boolean;
|
|
28
|
+
/** The request on the left and the result on the right. */
|
|
29
|
+
columns?: boolean;
|
|
30
|
+
className?: string;
|
|
31
|
+
style?: CSSProperties;
|
|
32
|
+
};
|
|
33
|
+
export type ConsoleFrameProps = {
|
|
34
|
+
inspector: TraceInspector;
|
|
35
|
+
maxWidth: string;
|
|
36
|
+
/**
|
|
37
|
+
* The page is already a raised panel. The console sits in that panel instead
|
|
38
|
+
* of painting the ground it uses when the page behind it is flat.
|
|
39
|
+
*/
|
|
40
|
+
flush?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The request and the response sit side by side. The request is a start
|
|
43
|
+
* panel at two fifths of that pair, so its handle is on the trailing edge
|
|
44
|
+
* and the response fills the other three fifths. Below the shell's drawer
|
|
45
|
+
* breakpoint they stack, request first. Two children: the request, then the
|
|
46
|
+
* response. One child keeps the single column.
|
|
47
|
+
*/
|
|
48
|
+
columns?: boolean;
|
|
49
|
+
/** The request column's name, read by its scroll region and its resize handle. */
|
|
50
|
+
requestLabel?: string;
|
|
51
|
+
className?: string;
|
|
52
|
+
style?: CSSProperties;
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* A console's page, for decisions and hosts alike: the trace's toggle in the
|
|
57
|
+
* header, and one raised panel, flush with the host's, scrolling inside.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ConsoleFrame({ inspector, maxWidth, flush, columns, requestLabel, className, style, children, }: ConsoleFrameProps): import("react").JSX.Element;
|
|
60
|
+
/** One card of a response with several: its request and result sit in it without cards of their own. */
|
|
61
|
+
export declare function ConsoleSection({ children }: {
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
}): import("react").JSX.Element;
|
|
64
|
+
/** The response. Beside the request it scrolls in the panel; a centred state fills the card, as an empty trace does. */
|
|
65
|
+
export declare function ResponseColumn({ label, fill, children, }: {
|
|
66
|
+
label: string;
|
|
67
|
+
fill?: boolean;
|
|
68
|
+
children: ReactNode;
|
|
69
|
+
}): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
70
|
+
export type ConsoleView = 'fields' | 'json';
|
|
71
|
+
export type RequestCardProps = {
|
|
72
|
+
title: string;
|
|
73
|
+
/** The view shown: fields only while the request parses. */
|
|
74
|
+
view: ConsoleView;
|
|
75
|
+
hasFields: boolean;
|
|
76
|
+
onView: (next: ConsoleView) => void;
|
|
77
|
+
/** Runs on ⌘/Ctrl+Enter anywhere in the card. */
|
|
78
|
+
onRun: () => void;
|
|
79
|
+
children: ReactNode;
|
|
80
|
+
};
|
|
81
|
+
/** The request's card: its heading with the Fields/JSON switch, then the request and its run bar. */
|
|
82
|
+
export declare function RequestCard({ title, view, hasFields, onView, onRun, children }: RequestCardProps): import("react").JSX.Element;
|
|
83
|
+
/** The request as JSON text, marked while it doesn't parse. */
|
|
84
|
+
export declare function JsonRequest({ label, text, isInvalid, onChange, }: {
|
|
85
|
+
label: string;
|
|
86
|
+
text: string;
|
|
87
|
+
isInvalid: boolean;
|
|
88
|
+
onChange: (next: string) => void;
|
|
89
|
+
}): import("react").JSX.Element;
|
|
90
|
+
export type RunBarProps = {
|
|
91
|
+
/** The shortcut, or what the request still lacks. */
|
|
92
|
+
note: string | null;
|
|
93
|
+
isReady: boolean;
|
|
94
|
+
isRunning: boolean;
|
|
95
|
+
runLabel: string;
|
|
96
|
+
runningLabel: string;
|
|
97
|
+
stopLabel: string;
|
|
98
|
+
onRun: () => void;
|
|
99
|
+
onStop: () => void;
|
|
100
|
+
};
|
|
101
|
+
/** A note on the request, Stop while it runs, and Run. */
|
|
102
|
+
export declare function RunBar({ note, isReady, isRunning, runLabel, runningLabel, stopLabel, onRun, onStop, }: RunBarProps): import("react").JSX.Element;
|
|
103
|
+
/** A size in bytes as the reader's locale writes it: bytes under a kilobyte, else kilobytes. */
|
|
104
|
+
export declare function useBytes(): (bytes: number) => string;
|