@theoremjs/react 0.3.0 → 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/README.md +728 -202
- package/client/composer-hints.js +1 -1
- package/client/decision-images.d.ts +76 -0
- package/client/decision-images.js +139 -0
- package/client/decision-transport.d.ts +26 -4
- package/client/decision-transport.js +79 -6
- 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/failure.d.ts +7 -10
- package/client/failure.js +9 -9
- package/client/host-call.d.ts +1 -1
- package/client/host-call.js +7 -2
- package/client/host-transport.d.ts +2 -0
- package/client/host-transport.js +3 -2
- package/client/index.d.ts +19 -11
- package/client/index.js +14 -10
- package/client/ink-waveform.d.ts +1 -1
- package/client/ink-waveform.js +1 -2
- package/client/live/apply-live-turn-tool-event.js +1 -1
- package/client/live/live-captions.d.ts +30 -0
- package/client/live/live-captions.js +105 -1
- package/client/live/live-mic-forward.d.ts +1 -0
- package/client/live/live-mic-forward.js +11 -2
- 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 +4 -2
- package/client/live/live-state.js +2 -0
- package/client/live/live-video.js +1 -1
- package/client/live/run-live-tool-call.d.ts +7 -2
- package/client/live/run-live-tool-call.js +48 -10
- 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 +52 -4
- package/client/live-client.js +250 -59
- package/client/live-messages.d.ts +51 -13
- package/client/live-messages.js +42 -9
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/run-commit.d.ts +16 -43
- package/client/run-commit.js +26 -101
- package/client/run-session.d.ts +40 -41
- package/client/run-session.js +104 -112
- package/client/schema-fields.js +27 -6
- package/client/shaped-data.js +37 -8
- package/client/source-citations.js +1 -1
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/state-fields.js +1 -1
- package/client/tool-result.js +30 -12
- package/client/tool-resume.d.ts +18 -1
- package/client/tool-resume.js +4 -1
- package/client/trace-feed.d.ts +1 -1
- package/client/trace-story.d.ts +2 -2
- package/client/trace-story.js +43 -19
- package/client/trace-view.d.ts +2 -2
- package/client/trace-view.js +56 -16
- package/client/transcript-groups.d.ts +28 -2
- package/client/transcript-groups.js +63 -13
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transport.d.ts +114 -26
- package/client/transport.js +106 -32
- package/client/turn-client.d.ts +17 -8
- package/client/turn-client.js +26 -31
- package/client/voice-recorder.d.ts +1 -1
- package/client/voice-recorder.js +2 -5
- package/client/wire-line.js +5 -5
- package/client/worklets/mic-capture.js +9 -9
- package/components/InkWaveform.d.ts +12 -1
- package/components/InkWaveform.js +65 -42
- package/components/live/use-live-runner-controls.d.ts +3 -4
- package/components/live/use-live-runner-controls.js +6 -10
- package/components/live/use-live-runner-model.d.ts +10 -6
- package/components/live/use-live-runner-model.js +29 -14
- package/components/live/use-live-runner-ui.d.ts +10 -8
- package/components/live/use-live-runner-ui.js +19 -11
- package/components/live/use-live-session-client.d.ts +4 -4
- package/components/live/use-live-session-client.js +11 -8
- package/components/use-composer-voice.d.ts +4 -4
- package/components/use-composer-voice.js +8 -8
- package/components/use-editor-selection.js +4 -1
- package/components/use-voice-playback.d.ts +2 -1
- package/components/use-voice-playback.js +4 -4
- package/hooks/use-described.d.ts +5 -1
- package/hooks/use-described.js +11 -9
- 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 +25 -21
- package/hooks/use-theorem-chat-actions.js +67 -45
- package/hooks/use-theorem-chat-state.d.ts +6 -6
- package/hooks/use-theorem-chat-state.js +13 -9
- package/hooks/use-theorem-chat.d.ts +48 -14
- package/hooks/use-theorem-chat.js +329 -141
- package/hooks/use-theorem-decision.d.ts +5 -10
- package/hooks/use-theorem-decision.js +4 -67
- package/hooks/use-theorem-embedding.d.ts +18 -0
- package/hooks/use-theorem-embedding.js +12 -0
- package/hooks/use-theorem-host.d.ts +5 -4
- package/hooks/use-theorem-host.js +26 -8
- package/hooks/use-theorem-interface.d.ts +6 -3
- package/hooks/use-theorem-interface.js +12 -22
- package/index.d.ts +6 -3
- package/index.js +5 -2
- package/live.d.ts +1 -0
- package/live.js +1 -1
- package/package.json +6 -5
- 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/decision-handler.d.ts +12 -4
- package/server/decision-handler.js +29 -38
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +58 -20
- package/server/handler.js +441 -238
- package/server/mod.d.ts +15 -6
- package/server/mod.js +13 -5
- 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 +6 -1
- package/server/request-check.js +27 -12
- 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 +69 -20
- package/server/session-store.js +220 -5
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.js +1 -1
- 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 +78 -78
- package/ui/CardWall.d.ts +10 -0
- package/ui/CardWall.js +61 -0
- package/ui/ChatComposerBar.d.ts +28 -5
- package/ui/ChatComposerBar.js +85 -30
- package/ui/ChatTranscript.d.ts +14 -1
- package/ui/ChatTranscript.js +110 -57
- package/ui/ConsoleFrame.d.ts +61 -4
- package/ui/ConsoleFrame.js +109 -9
- package/ui/DecisionAnswers.d.ts +40 -2
- package/ui/DecisionAnswers.js +166 -35
- 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 +6 -4
- package/ui/LiveRunner.js +42 -55
- package/ui/PaneState.d.ts +33 -0
- package/ui/PaneState.js +50 -0
- package/ui/SchemaFields.d.ts +1 -1
- package/ui/SchemaFields.js +54 -26
- package/ui/ShapedData.js +73 -28
- package/ui/SidePanel.d.ts +74 -5
- package/ui/SidePanel.js +82 -9
- package/ui/StateFields.d.ts +10 -2
- package/ui/StateFields.js +56 -13
- package/ui/TheoremChat.d.ts +65 -14
- package/ui/TheoremChat.js +66 -78
- package/ui/TheoremDecision.d.ts +12 -20
- package/ui/TheoremDecision.js +78 -35
- package/ui/TheoremEmbedding.d.ts +27 -0
- package/ui/TheoremEmbedding.js +122 -0
- package/ui/TheoremHost.d.ts +22 -5
- package/ui/TheoremHost.js +127 -37
- package/ui/ToolGateCard.d.ts +5 -1
- package/ui/ToolGateCard.js +30 -23
- package/ui/ToolResult.js +12 -10
- package/ui/ToolResultCharts.js +29 -12
- package/ui/TraceCharts.d.ts +2 -2
- package/ui/TraceCharts.js +48 -17
- package/ui/TraceGuardrails.d.ts +3 -2
- package/ui/TraceGuardrails.js +24 -6
- package/ui/TraceInspector.d.ts +11 -1
- package/ui/TraceInspector.js +45 -28
- package/ui/TraceInspectorPanel.d.ts +31 -9
- package/ui/TraceInspectorPanel.js +77 -39
- package/ui/TraceOverview.d.ts +7 -3
- package/ui/TraceOverview.js +44 -14
- package/ui/TraceSpanDetail.js +24 -22
- package/ui/TraceStory.js +9 -8
- package/ui/TraceValues.js +19 -9
- package/ui/TraceWaterfall.js +12 -9
- package/ui/VoiceNote.js +4 -4
- 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 +86 -68
- package/ui/built/theme.css.d.ts +9 -0
- package/ui/built/theorem.d.ts +3 -3
- package/ui/built/theorem.js +34 -5
- package/ui/built/theorem.variants.d.ts +8 -2
- package/ui/code-language.d.ts +12 -0
- package/ui/code-language.js +50 -0
- package/ui/disclosure-motion.js +53 -9
- 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.js +2 -1
- package/ui/index.d.ts +20 -9
- package/ui/index.js +20 -10
- package/ui/labels-provider.js +5 -2
- package/ui/labels.d.ts +374 -85
- package/ui/labels.js +1028 -188
- 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 +1 -1
- package/ui/theme.js +2 -2
- package/ui/theorem-theme.js +32 -10
- package/ui/trace-palette.d.ts +2 -16
- package/ui/trace-palette.js +2 -16
- package/ui/trace-patterns.d.ts +11 -3
- package/ui/trace-patterns.js +16 -3
- package/ui/transcript-copy-text.js +1 -0
- package/ui/wall-columns.d.ts +8 -0
- package/ui/wall-columns.js +19 -0
- package/client/walk-away.d.ts +0 -34
- package/client/walk-away.js +0 -48
- package/server/walk-away.d.ts +0 -22
- package/server/walk-away.js +0 -41
package/ui/ChatTranscript.js
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { AspectRatio } from '@astryxdesign/core/AspectRatio';
|
|
3
|
-
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, ChatToolCalls, } from '@astryxdesign/core/Chat';
|
|
3
|
+
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, ChatSystemMessage, ChatToolCalls, } from '@astryxdesign/core/Chat';
|
|
4
4
|
import { Citation } from '@astryxdesign/core/Citation';
|
|
5
5
|
import { ClickableCard } from '@astryxdesign/core/ClickableCard';
|
|
6
6
|
import { Collapsible } from '@astryxdesign/core/Collapsible';
|
|
7
7
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
8
|
+
import { useStreamingText } from '@astryxdesign/core/hooks';
|
|
8
9
|
import { Icon } from '@astryxdesign/core/Icon';
|
|
9
10
|
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
11
|
+
import { useLocale } from '@astryxdesign/core/i18n';
|
|
10
12
|
import { useLightbox } from '@astryxdesign/core/Lightbox';
|
|
11
|
-
import { useStreamingText } from '@astryxdesign/core/hooks';
|
|
12
13
|
import { Markdown } from '@astryxdesign/core/Markdown';
|
|
13
14
|
import { Skeleton } from '@astryxdesign/core/Skeleton';
|
|
14
15
|
import { Text } from '@astryxdesign/core/Text';
|
|
15
16
|
import { Thumbnail } from '@astryxdesign/core/Thumbnail';
|
|
16
17
|
import { Timestamp } from '@astryxdesign/core/Timestamp';
|
|
17
18
|
import { Token } from '@astryxdesign/core/Token';
|
|
19
|
+
import { Tooltip as HoverTip } from '@astryxdesign/core/Tooltip';
|
|
18
20
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
19
21
|
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
|
20
|
-
import {
|
|
22
|
+
import { stopText } from '@theoremjs/agents/interface';
|
|
23
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
21
24
|
import { citationsFromBlock } from "../client/source-citations.js";
|
|
22
|
-
import { assistantTurnCopyText, assistantTurnTiming, composeAssistantTurn, groupTranscriptBlocks, pendingPromptOf, promptReplyKey, replyKey, toolCallLabel, workStatus, } from "../client/transcript-groups.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";
|
|
23
27
|
import { useDisclosureMotion } from "./disclosure-motion.js";
|
|
24
|
-
import { workDuration, workStatusLabel } from "./labels.js";
|
|
28
|
+
import { usageLine, workDuration, workStatusLabel } from "./labels.js";
|
|
25
29
|
import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
|
|
30
|
+
import { useMarkdownPlugins } from "./markdown-plugins.js";
|
|
31
|
+
import { keyedByContent } from "./row-keys.js";
|
|
26
32
|
import { ShapedData } from "./ShapedData.js";
|
|
27
|
-
import { transcriptBlockCopyText } from "./transcript-copy-text.js";
|
|
28
33
|
import { ApprovalCard, AuthChallengeCard } from "./ToolGateCard.js";
|
|
34
|
+
import { transcriptBlockCopyText } from "./transcript-copy-text.js";
|
|
29
35
|
import { VoiceNote } from "./VoiceNote.js";
|
|
36
|
+
/** Whether the transcript shows usage (`ChatTranscriptProps.usage`). */
|
|
37
|
+
const ShowUsage = createContext(false);
|
|
30
38
|
/** Largest a generated image renders in the transcript. */
|
|
31
39
|
const GENERATED_IMAGE_MAX_WIDTH = 512;
|
|
32
40
|
/** First-seen time per group key, so timestamps don't jump while streaming. */
|
|
@@ -39,18 +47,6 @@ function useFirstSeen(keys) {
|
|
|
39
47
|
}
|
|
40
48
|
return (key) => times.current.get(key) ?? now;
|
|
41
49
|
}
|
|
42
|
-
/** `Date.now()`, refreshed every second while `isActive`. */
|
|
43
|
-
function useSecondTicker(isActive) {
|
|
44
|
-
const [now, setNow] = useState(() => Date.now());
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (!isActive)
|
|
47
|
-
return;
|
|
48
|
-
setNow(Date.now());
|
|
49
|
-
const timer = globalThis.setInterval(() => setNow(Date.now()), 1_000);
|
|
50
|
-
return () => globalThis.clearInterval(timer);
|
|
51
|
-
}, [isActive]);
|
|
52
|
-
return now;
|
|
53
|
-
}
|
|
54
50
|
/**
|
|
55
51
|
* Each turn's span, keyed by the user group that started it. A turn stops at
|
|
56
52
|
* a gate and streams again under the same prompt once it's answered; the wait
|
|
@@ -58,7 +54,7 @@ function useSecondTicker(isActive) {
|
|
|
58
54
|
*/
|
|
59
55
|
function useTurnSpans(streaming, lastUserKey) {
|
|
60
56
|
const spans = useRef(new Map());
|
|
61
|
-
// The first send mounts the transcript already streaming; that's the start.
|
|
57
|
+
// why: The first send mounts the transcript already streaming; that's the start.
|
|
62
58
|
const wasStreaming = useRef(false);
|
|
63
59
|
if (lastUserKey && streaming !== wasStreaming.current) {
|
|
64
60
|
const now = Date.now();
|
|
@@ -106,13 +102,36 @@ function MessageTime({ at }) {
|
|
|
106
102
|
}
|
|
107
103
|
return _jsx(Timestamp, { value: at, format: "relative_short", isLive: true });
|
|
108
104
|
}
|
|
109
|
-
/**
|
|
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. */
|
|
110
125
|
function MessageChrome(props) {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 }));
|
|
116
135
|
}
|
|
117
136
|
function dataUrl(mimeType, data) {
|
|
118
137
|
return data === undefined ? undefined : `data:${mimeType};base64,${data}`;
|
|
@@ -184,7 +203,10 @@ function GeneratedGallery({ items, ratio }) {
|
|
|
184
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] }));
|
|
185
204
|
}
|
|
186
205
|
/** An image takes seconds: a slow, smooth breath rather than the Skeleton's quick stepped blink. */
|
|
187
|
-
const GENERATING_PULSE = {
|
|
206
|
+
const GENERATING_PULSE = {
|
|
207
|
+
animationDuration: '1.8s',
|
|
208
|
+
animationTimingFunction: 'ease-in-out',
|
|
209
|
+
};
|
|
188
210
|
/** Placeholder shaped like the image being generated. */
|
|
189
211
|
function GeneratingImage({ ratio }) {
|
|
190
212
|
const t = useLabels();
|
|
@@ -229,12 +251,13 @@ function StreamedMarkdown({ text, streaming, density, components, }) {
|
|
|
229
251
|
const [revealing, setRevealing] = useState(streaming);
|
|
230
252
|
if (streaming && !revealing)
|
|
231
253
|
setRevealing(true);
|
|
254
|
+
const plugins = useMarkdownPlugins();
|
|
232
255
|
const shown = useStreamingText(text, revealing);
|
|
233
256
|
useEffect(() => {
|
|
234
257
|
if (!streaming && shown.length >= text.length)
|
|
235
258
|
setRevealing(false);
|
|
236
259
|
}, [streaming, shown, text]);
|
|
237
|
-
return (_jsx(Markdown, { isStreaming: revealing, density: density, components: components, children: text }));
|
|
260
|
+
return (_jsx(Markdown, { isStreaming: revealing, density: density, components: components, plugins: plugins, children: text }));
|
|
238
261
|
}
|
|
239
262
|
function BodyBlock({ block, streaming }) {
|
|
240
263
|
if (block.kind === 'text')
|
|
@@ -243,10 +266,9 @@ function BodyBlock({ block, streaming }) {
|
|
|
243
266
|
return _jsx(ToolCall, { tool: block.tool });
|
|
244
267
|
return _jsx(ResultBlock, { block: block });
|
|
245
268
|
}
|
|
246
|
-
/** Non-streaming answer rows: errors, sources, structured output and media. */
|
|
269
|
+
/** Non-streaming answer rows: errors, sources, structured output, and media. */
|
|
247
270
|
function ResultBlock({ block }) {
|
|
248
271
|
switch (block.kind) {
|
|
249
|
-
// A failure shows on the message's metadata line (MessageChrome).
|
|
250
272
|
case 'error':
|
|
251
273
|
return null;
|
|
252
274
|
case 'citation':
|
|
@@ -259,7 +281,7 @@ function ResultBlock({ block }) {
|
|
|
259
281
|
return null;
|
|
260
282
|
}
|
|
261
283
|
}
|
|
262
|
-
// Astryx sets a call's name in the code font; labels are sentences, so the row takes the body
|
|
284
|
+
// why: Astryx sets a call's name in the code font; labels are sentences, so the row takes the body
|
|
263
285
|
// font and the detail restores the code font kept on the wrapper.
|
|
264
286
|
const CODE_FONT_KEPT = {
|
|
265
287
|
display: 'contents',
|
|
@@ -281,16 +303,20 @@ function toolDuration(t, tool) {
|
|
|
281
303
|
return {};
|
|
282
304
|
return { duration: workDuration(t, tool.endedAt - tool.startedAt) };
|
|
283
305
|
}
|
|
284
|
-
|
|
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) {
|
|
285
311
|
const base = { key: id, name: toolCallLabel(tool) };
|
|
286
312
|
const { state } = tool;
|
|
287
313
|
switch (state?.phase) {
|
|
288
314
|
case 'error':
|
|
289
|
-
// What failed, as the tool reported it; the whole failure is in the detail.
|
|
290
315
|
return {
|
|
291
316
|
...base,
|
|
292
317
|
status: 'error',
|
|
293
318
|
target: state.failure.message,
|
|
319
|
+
...toolUsageStats(state, showUsage),
|
|
294
320
|
errorMessage: state.failure.message,
|
|
295
321
|
resultDetail: toolDetail(t, tool, _jsx(ShapedData, { value: state.failure, title: t('@theorem.transcript.tool_error') })),
|
|
296
322
|
};
|
|
@@ -299,6 +325,7 @@ function toolCallItem(t, id, tool) {
|
|
|
299
325
|
...base,
|
|
300
326
|
status: 'complete',
|
|
301
327
|
...toolDuration(t, tool),
|
|
328
|
+
...toolUsageStats(state, showUsage),
|
|
302
329
|
resultDetail: toolDetail(t, tool, state.output === undefined ? undefined : (_jsx(ShapedData, { value: state.output, title: t('@theorem.transcript.tool_output') }))),
|
|
303
330
|
};
|
|
304
331
|
case 'running':
|
|
@@ -309,7 +336,8 @@ function toolCallItem(t, id, tool) {
|
|
|
309
336
|
}
|
|
310
337
|
function ToolCall({ tool }) {
|
|
311
338
|
const t = useLabels();
|
|
312
|
-
|
|
339
|
+
const showUsage = useContext(ShowUsage);
|
|
340
|
+
return _jsx(ToolCalls, { calls: [toolCallItem(t, tool.name, tool, showUsage)] });
|
|
313
341
|
}
|
|
314
342
|
/**
|
|
315
343
|
* Thinking reads as quieter text than the reply. Astryx has no reasoning
|
|
@@ -322,6 +350,7 @@ const THOUGHT_MARKDOWN = {
|
|
|
322
350
|
};
|
|
323
351
|
function TraceList({ items, streaming }) {
|
|
324
352
|
const t = useLabels();
|
|
353
|
+
const showUsage = useContext(ShowUsage);
|
|
325
354
|
const rows = [];
|
|
326
355
|
let tools = [];
|
|
327
356
|
const flush = () => {
|
|
@@ -332,7 +361,7 @@ function TraceList({ items, streaming }) {
|
|
|
332
361
|
};
|
|
333
362
|
for (const [i, item] of items.entries()) {
|
|
334
363
|
if (item.kind === 'tool') {
|
|
335
|
-
tools.push(toolCallItem(t, item.id, item.block.tool));
|
|
364
|
+
tools.push(toolCallItem(t, item.id, item.block.tool, showUsage));
|
|
336
365
|
continue;
|
|
337
366
|
}
|
|
338
367
|
flush();
|
|
@@ -341,17 +370,28 @@ function TraceList({ items, streaming }) {
|
|
|
341
370
|
flush();
|
|
342
371
|
return _jsx(VStack, { gap: 2, children: rows });
|
|
343
372
|
}
|
|
344
|
-
|
|
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, }) {
|
|
345
385
|
const { tool } = block;
|
|
346
|
-
|
|
386
|
+
const gate = personGate(tool);
|
|
387
|
+
if (!gate)
|
|
347
388
|
return null;
|
|
348
|
-
const { gate } = tool.state;
|
|
349
389
|
const index = handlers.indexOf(block);
|
|
350
|
-
const answer = handlers.answering
|
|
390
|
+
const answer = answerSent(handlers.answering, tool);
|
|
351
391
|
if (gate.kind === 'auth') {
|
|
352
392
|
return (_jsx(AuthChallengeCard, { gate: gate, toolName: tool.name, submitted: answer === 'auth', onAuthenticated: (secret) => handlers.onAuthenticated?.(index, secret) }));
|
|
353
393
|
}
|
|
354
|
-
return (_jsx(ApprovalCard, { gate: gate, toolName: tool.name, input: tool.arguments, decided: answer === 'auth' ? null : answer, onDecision: (action) => handlers.onToolDecision?.(index, action) }));
|
|
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) }));
|
|
355
395
|
}
|
|
356
396
|
/** Live elapsed time while the turn streams; its final duration once it ends. */
|
|
357
397
|
/** Live: counted from the start. Stopped: the work its blocks record. */
|
|
@@ -381,7 +421,7 @@ function TurnStatus(props) {
|
|
|
381
421
|
return label;
|
|
382
422
|
return (_jsx(Collapsible, { trigger: label, isOpen: traceOpen, onOpenChange: setTraceOpen, children: _jsx(TraceList, { items: props.trace, streaming: streaming }) }));
|
|
383
423
|
}
|
|
384
|
-
function BodyRowView({ row, streaming, imageOutput }) {
|
|
424
|
+
function BodyRowView({ row, streaming, imageOutput, }) {
|
|
385
425
|
if (row.kind === 'block')
|
|
386
426
|
return _jsx(BodyBlock, { block: row.block, streaming: streaming });
|
|
387
427
|
if (imageOutput)
|
|
@@ -396,15 +436,16 @@ function PendingImage(props) {
|
|
|
396
436
|
}
|
|
397
437
|
function AssistantTurn(props) {
|
|
398
438
|
const t = useLabels();
|
|
439
|
+
const showUsage = useContext(ShowUsage);
|
|
399
440
|
const elapsedMs = useTurnElapsed(props.streaming, props.startedAt, props.workedMs);
|
|
400
441
|
const { trace, gatedTools, body, hasTrace } = composeAssistantTurn(props.blocks);
|
|
401
442
|
const rows = bodyRows(t, body);
|
|
402
443
|
const status = workStatusLabel(t, workStatus({ streaming: props.streaming, hasTrace, elapsedMs }));
|
|
403
444
|
const copyText = assistantTurnCopyText(body.length > 0 ? body : props.blocks);
|
|
404
|
-
return (_jsx(ChatMessage, { sender: "assistant", name: props.handle, metadata: props.streaming ? undefined : _jsx(MessageChrome, { at: props.at, copyText: copyText, error: props.error }), 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, handlers: props.handlers }, block.id))), rows.map((row, i) => (_jsx(BodyRowView
|
|
405
|
-
//
|
|
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
|
|
406
447
|
// remount would cut the text reveal short.
|
|
407
|
-
, { row: row, streaming: props.streaming && i === rows.length - 1, imageOutput: props.imageOutput },
|
|
448
|
+
, { row: row, streaming: props.streaming && i === rows.length - 1, imageOutput: props.imageOutput }, key))), _jsx(PendingImage, { imageOutput: props.imageOutput, streaming: props.streaming, rows: rows })] }) }));
|
|
408
449
|
}
|
|
409
450
|
function UserTurn(props) {
|
|
410
451
|
const t = useLabels();
|
|
@@ -412,8 +453,8 @@ function UserTurn(props) {
|
|
|
412
453
|
.map((block) => transcriptBlockCopyText(t, block))
|
|
413
454
|
.filter(Boolean)
|
|
414
455
|
.join('\n\n');
|
|
415
|
-
const chrome = _jsx(MessageChrome, { at: props.at, copyText: copyText, status: props.status, error: props.error });
|
|
416
|
-
// Astryx: metadata goes on the last bubble, or on the message when the last
|
|
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
|
|
417
458
|
// content is unbubbled (an attachment or voice note).
|
|
418
459
|
const last = props.blocks.at(-1);
|
|
419
460
|
const lastIsBubble = last?.kind === 'user-text';
|
|
@@ -429,15 +470,14 @@ function isBareFailure(group) {
|
|
|
429
470
|
}
|
|
430
471
|
/** Theorem transcript blocks rendered as Astryx chat messages. */
|
|
431
472
|
export function ChatTranscript(props) {
|
|
432
|
-
return (_jsx(TheoremLabelsProvider, { children: _jsx(ChatTranscriptBody, { ...props }) }));
|
|
473
|
+
return (_jsx(TheoremLabelsProvider, { children: _jsx(ShowUsage.Provider, { value: props.usage === true, children: _jsx(ChatTranscriptBody, { ...props }) }) }));
|
|
433
474
|
}
|
|
434
|
-
function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToolDecision, answering = null, onAuthenticated, emptyState, imageOutput, }) {
|
|
435
|
-
const groups = useMemo(() => groupTranscriptBlocks(blocks), [blocks]);
|
|
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]);
|
|
436
477
|
const timeOf = useFirstSeen(groups.map((group, index) => (group.kind === 'user' ? group.key : replyKey(groups, index))));
|
|
437
478
|
const spans = useTurnSpans(streaming, groups.findLast((group) => group.kind === 'user')?.key);
|
|
438
479
|
const pendingPrompt = streaming ? pendingPromptOf(groups) : undefined;
|
|
439
480
|
const listRef = useRef(null);
|
|
440
|
-
// Disclosures ease open and shut in place; opening one never moves the transcript.
|
|
441
481
|
useDisclosureMotion(listRef);
|
|
442
482
|
const handlers = {
|
|
443
483
|
indexOf: (block) => blocks.findIndex((entry) => entry.id === block.id),
|
|
@@ -448,24 +488,37 @@ function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToo
|
|
|
448
488
|
const turn = { handle, handlers, imageOutput };
|
|
449
489
|
const lastUser = groups.findLastIndex((group) => group.kind === 'user');
|
|
450
490
|
const userTurn = (group, index) => {
|
|
451
|
-
// A turn that failed before any reply shows it on the message itself.
|
|
452
491
|
const next = groups[index + 1];
|
|
453
492
|
const error = isBareFailure(next) ? failureOf(next) : undefined;
|
|
454
493
|
const status = index === lastUser ? (delivery ?? undefined) : undefined;
|
|
455
|
-
return _jsx(UserTurn, { blocks: group.blocks, at: timeOf
|
|
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));
|
|
456
495
|
};
|
|
457
|
-
const
|
|
496
|
+
const turnOf = (group, index) => {
|
|
458
497
|
if (group.kind === 'user')
|
|
459
498
|
return userTurn(group, index);
|
|
460
499
|
if (isBareFailure(group) && groups[index - 1]?.kind === 'user')
|
|
461
500
|
return [];
|
|
462
|
-
const { key, live, endedAt, ...timing } = assistantTurnTiming({
|
|
463
|
-
|
|
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".
|
|
464
509
|
const at = endedAt ?? timeOf(key);
|
|
465
|
-
return (_jsx(AssistantTurn, { ...turn, ...timing, blocks: group.blocks, streaming: live, at: at, error: failureOf(group) }, 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();
|
|
466
520
|
});
|
|
467
|
-
//
|
|
468
|
-
// It sits in the same keyed list as the streamed reply, so the reply stays
|
|
521
|
+
// why: It sits in the same keyed list as the streamed reply, so the reply stays
|
|
469
522
|
// one message; a remount would read to the layout as a new message.
|
|
470
523
|
if (pendingPrompt) {
|
|
471
524
|
turns.push(_jsx(AssistantTurn, { ...turn, blocks: [], streaming: true, at: timeOf(pendingPrompt.key), startedAt: timeOf(pendingPrompt.key) }, promptReplyKey(pendingPrompt)));
|
package/ui/ConsoleFrame.d.ts
CHANGED
|
@@ -1,8 +1,53 @@
|
|
|
1
|
-
import type {
|
|
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';
|
|
2
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
|
+
};
|
|
3
33
|
export type ConsoleFrameProps = {
|
|
4
34
|
inspector: TraceInspector;
|
|
5
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;
|
|
6
51
|
className?: string;
|
|
7
52
|
style?: CSSProperties;
|
|
8
53
|
children: ReactNode;
|
|
@@ -11,7 +56,17 @@ export type ConsoleFrameProps = {
|
|
|
11
56
|
* A console's page, for decisions and hosts alike: the trace's toggle in the
|
|
12
57
|
* header, and one raised panel, flush with the host's, scrolling inside.
|
|
13
58
|
*/
|
|
14
|
-
export declare function ConsoleFrame({ inspector, maxWidth, className, style, children }: ConsoleFrameProps): import("react").JSX.Element;
|
|
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;
|
|
15
70
|
export type ConsoleView = 'fields' | 'json';
|
|
16
71
|
export type RequestCardProps = {
|
|
17
72
|
title: string;
|
|
@@ -26,7 +81,7 @@ export type RequestCardProps = {
|
|
|
26
81
|
/** The request's card: its heading with the Fields/JSON switch, then the request and its run bar. */
|
|
27
82
|
export declare function RequestCard({ title, view, hasFields, onView, onRun, children }: RequestCardProps): import("react").JSX.Element;
|
|
28
83
|
/** The request as JSON text, marked while it doesn't parse. */
|
|
29
|
-
export declare function JsonRequest({ label, text, isInvalid, onChange }: {
|
|
84
|
+
export declare function JsonRequest({ label, text, isInvalid, onChange, }: {
|
|
30
85
|
label: string;
|
|
31
86
|
text: string;
|
|
32
87
|
isInvalid: boolean;
|
|
@@ -44,4 +99,6 @@ export type RunBarProps = {
|
|
|
44
99
|
onStop: () => void;
|
|
45
100
|
};
|
|
46
101
|
/** A note on the request, Stop while it runs, and Run. */
|
|
47
|
-
export declare function RunBar({ note, isReady, isRunning, runLabel, runningLabel, stopLabel, onRun, onStop }: RunBarProps): import("react").JSX.Element;
|
|
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;
|
package/ui/ConsoleFrame.js
CHANGED
|
@@ -3,20 +3,102 @@ import { Button } from '@astryxdesign/core/Button';
|
|
|
3
3
|
import { Card } from '@astryxdesign/core/Card';
|
|
4
4
|
import { Center } from '@astryxdesign/core/Center';
|
|
5
5
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
6
|
+
import { useLocale } from '@astryxdesign/core/i18n';
|
|
6
7
|
import { Layout, LayoutContent } from '@astryxdesign/core/Layout';
|
|
8
|
+
import { useResizable } from '@astryxdesign/core/Resizable';
|
|
7
9
|
import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
|
|
8
10
|
import { Text } from '@astryxdesign/core/Text';
|
|
9
11
|
import { TextArea } from '@astryxdesign/core/TextArea';
|
|
10
12
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
13
|
+
import { Children, createContext, use, useMemo, useRef, useSyncExternalStore, } from 'react';
|
|
11
14
|
import { useLabels } from "./labels-provider.js";
|
|
12
|
-
import { RAISED, SidePanelHeader } from "./SidePanel.js";
|
|
15
|
+
import { PanelBody, PanelScroll, RAISED, SidePanel, SidePanelHeader } from "./SidePanel.js";
|
|
16
|
+
import { textRows } from "./StateFields.js";
|
|
13
17
|
import { WithTrace } from "./TraceInspectorPanel.js";
|
|
18
|
+
import { CONSOLE_CARD } from "./trace-patterns.js";
|
|
19
|
+
/** Cards on a flush console. The flat page uses the surface; a raised page uses the card. */
|
|
20
|
+
const FLUSH_CARD = {
|
|
21
|
+
'--theorem-console-card': 'var(--color-background-muted)',
|
|
22
|
+
};
|
|
23
|
+
/** Below the app shell's drawer breakpoint, where the two columns stack. */
|
|
24
|
+
const NARROW = '(width < 768px)';
|
|
25
|
+
/** Side by side, the panel card is the frame, so the request and response bring no card of their own. */
|
|
26
|
+
const ConsoleChrome = createContext(true);
|
|
27
|
+
function useNarrow() {
|
|
28
|
+
return useSyncExternalStore((change) => {
|
|
29
|
+
const query = globalThis.matchMedia(NARROW);
|
|
30
|
+
query.addEventListener('change', change);
|
|
31
|
+
return () => {
|
|
32
|
+
query.removeEventListener('change', change);
|
|
33
|
+
};
|
|
34
|
+
}, () => globalThis.matchMedia(NARROW).matches, () => false);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One column: the request, then the response under it, in the page's own card.
|
|
38
|
+
* `wide` is a pair stacked on a narrow page, which takes the page's width.
|
|
39
|
+
*/
|
|
40
|
+
function ConsoleSingle({ flush, wide, maxWidth, children, }) {
|
|
41
|
+
return (_jsx(VStack, { height: "100%", paddingInline: 3, style: flush ? FLUSH_CARD : undefined, children: _jsx(Card, { variant: "transparent", height: "100%", padding: 0, style: flush ? { overflowY: 'auto' } : { ...RAISED, overflowY: 'auto' }, children: _jsx(Center, { axis: "horizontal", width: "100%", minHeight: "100%", children: _jsx(VStack, { width: "100%", maxWidth: wide ? undefined : maxWidth, gap: 3, padding: 4, children: children }) }) }) }));
|
|
42
|
+
}
|
|
14
43
|
/**
|
|
15
44
|
* A console's page, for decisions and hosts alike: the trace's toggle in the
|
|
16
45
|
* header, and one raised panel, flush with the host's, scrolling inside.
|
|
17
46
|
*/
|
|
18
|
-
export function ConsoleFrame({ inspector, maxWidth, className, style, children }) {
|
|
19
|
-
|
|
47
|
+
export function ConsoleFrame({ inspector, maxWidth, flush = false, columns = false, requestLabel, className, style, children, }) {
|
|
48
|
+
const layoutRef = useRef(null);
|
|
49
|
+
return (_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", className: className, style: style, header: inspector.toggle ? _jsx(SidePanelHeader, { children: inspector.toggle }) : undefined, content: _jsx(LayoutContent, { padding: 0, isScrollable: false, children: _jsx(ConsoleBody, { layoutRef: layoutRef, flush: flush, columns: columns, maxWidth: maxWidth, requestLabel: requestLabel, children: children }) }) }) }));
|
|
50
|
+
}
|
|
51
|
+
/** The console's panels: a request and response side by side when there is room, else one column. */
|
|
52
|
+
function ConsoleBody({ layoutRef, flush, columns, maxWidth, requestLabel, children, }) {
|
|
53
|
+
const narrow = useNarrow();
|
|
54
|
+
// why: Children.count counts a null slot; toArray drops it, which is the child that is actually drawn.
|
|
55
|
+
const drawn = Children.toArray(children).length;
|
|
56
|
+
const paired = columns && drawn > 1;
|
|
57
|
+
if (paired && !narrow) {
|
|
58
|
+
return (_jsx(ConsoleColumns, { requestLabel: requestLabel, basisWidth: layoutRef.current?.clientWidth ?? 0, children: children }));
|
|
59
|
+
}
|
|
60
|
+
return (_jsx(ConsoleSingle, { flush: flush, wide: paired, maxWidth: maxWidth, children: children }));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The request is the sized panel and the response fills what it leaves, so
|
|
64
|
+
* one handle keeps the pair at two fifths / three fifths. The request sizes
|
|
65
|
+
* against this inner layout, not the page, so a trace docked outside the pair
|
|
66
|
+
* cannot take the request's share.
|
|
67
|
+
*/
|
|
68
|
+
function ConsoleColumns({ requestLabel = 'Request', basisWidth, children, }) {
|
|
69
|
+
const t = useLabels();
|
|
70
|
+
const pairRef = useRef(null);
|
|
71
|
+
// why: a percentage default resolves against a 1200px stand-in until the pair
|
|
72
|
+
// is measured, and the panel's width transition then eases between the two,
|
|
73
|
+
// reflowing the request. This layout is already on screen, so the first
|
|
74
|
+
// width is two fifths of it. 70% leaves the response a readable share, and a
|
|
75
|
+
// percentage maximum follows the pair when the trace opens.
|
|
76
|
+
const requestPanel = useResizable({
|
|
77
|
+
defaultSize: basisWidth > 0 ? Math.round(basisWidth * 0.4) : '40%',
|
|
78
|
+
minSize: 280,
|
|
79
|
+
maxSize: '70%',
|
|
80
|
+
containerRef: pairRef,
|
|
81
|
+
});
|
|
82
|
+
const [request, response] = Children.toArray(children);
|
|
83
|
+
const labels = {
|
|
84
|
+
name: requestLabel,
|
|
85
|
+
show: requestLabel,
|
|
86
|
+
hide: requestLabel,
|
|
87
|
+
resize: t('@theorem.panel.resize', { name: requestLabel }),
|
|
88
|
+
};
|
|
89
|
+
return (_jsx(ConsoleChrome, { value: false, children: _jsx(Layout, { ref: pairRef, height: "fill", start: _jsx(SidePanel, { side: "start", className: "theorem-console-request", labels: labels, resizable: requestPanel, open: true, children: _jsx(PanelScroll, { label: requestLabel, children: request }) }), content: _jsx(LayoutContent, { padding: 0, isScrollable: false, children: _jsx(PanelBody, { inset: "beside", className: "theorem-console-response", children: response }) }) }) }));
|
|
90
|
+
}
|
|
91
|
+
/** One card of a response with several: its request and result sit in it without cards of their own. */
|
|
92
|
+
export function ConsoleSection({ children }) {
|
|
93
|
+
return (_jsx(Card, { variant: "muted", padding: 0, className: "theorem-console-card", style: CONSOLE_CARD, children: _jsx(ConsoleChrome, { value: false, children: children }) }));
|
|
94
|
+
}
|
|
95
|
+
/** The response. Beside the request it scrolls in the panel; a centred state fills the card, as an empty trace does. */
|
|
96
|
+
export function ResponseColumn({ label, fill = false, children, }) {
|
|
97
|
+
const framed = use(ConsoleChrome);
|
|
98
|
+
if (!framed) {
|
|
99
|
+
return fill ? (children) : (_jsx(PanelScroll, { label: label, padding: 4, children: children }));
|
|
100
|
+
}
|
|
101
|
+
return (_jsx(Card, { variant: "muted", padding: fill ? 0 : 4, className: "theorem-console-card", style: CONSOLE_CARD, children: fill ? (children) : (_jsx(VStack, { height: "100%", gap: 3, children: children })) }));
|
|
20
102
|
}
|
|
21
103
|
/** ⌘/Ctrl+Enter runs the console's request. */
|
|
22
104
|
function runShortcut(run) {
|
|
@@ -30,17 +112,35 @@ function runShortcut(run) {
|
|
|
30
112
|
/** The request's card: its heading with the Fields/JSON switch, then the request and its run bar. */
|
|
31
113
|
export function RequestCard({ title, view, hasFields, onView, onRun, children }) {
|
|
32
114
|
const t = useLabels();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
115
|
+
const framed = use(ConsoleChrome);
|
|
116
|
+
const body = (_jsxs(VStack, { gap: 3, padding: framed ? undefined : 4, onKeyDown: runShortcut(onRun), children: [_jsxs(HStack, { gap: 2, hAlign: "between", vAlign: "center", children: [_jsx(Text, { type: "supporting", color: "secondary", as: "h3", children: title }), _jsxs(SegmentedControl, { label: t('@theorem.decision.view'), size: "sm", value: view, onChange: (next) => {
|
|
117
|
+
onView(next === 'json' ? 'json' : 'fields');
|
|
118
|
+
}, children: [_jsx(SegmentedControlItem, { value: "fields", label: t('@theorem.decision.fields'), isDisabled: !hasFields }), _jsx(SegmentedControlItem, { value: "json", label: t('@theorem.data.json') })] })] }), children] }));
|
|
119
|
+
if (!framed)
|
|
120
|
+
return body;
|
|
121
|
+
return (_jsx(Card, { variant: "muted", padding: 4, className: "theorem-console-card", style: CONSOLE_CARD, children: body }));
|
|
36
122
|
}
|
|
37
123
|
/** The field reads as code: the body font is swapped for the code font inside it. */
|
|
38
124
|
const CODE_FONT = { '--font-family-body': 'var(--font-family-code)' };
|
|
39
125
|
/** The request as JSON text, marked while it doesn't parse. */
|
|
40
|
-
export function JsonRequest({ label, text, isInvalid, onChange }) {
|
|
41
|
-
return (_jsx(TextArea, { label: label, isLabelHidden: true, value: text, onChange: onChange, rows: 8, hasSpellCheck: false, status: isInvalid ? { type: 'error' } : undefined,
|
|
126
|
+
export function JsonRequest({ label, text, isInvalid, onChange, }) {
|
|
127
|
+
return (_jsx(TextArea, { label: label, isLabelHidden: true, value: text, onChange: onChange, rows: textRows(text, 8, 24), hasSpellCheck: false, status: isInvalid ? { type: 'error' } : undefined, controlStyle: CODE_FONT }));
|
|
42
128
|
}
|
|
43
129
|
/** A note on the request, Stop while it runs, and Run. */
|
|
44
|
-
export function RunBar({ note, isReady, isRunning, runLabel, runningLabel, stopLabel, onRun, onStop }) {
|
|
130
|
+
export function RunBar({ note, isReady, isRunning, runLabel, runningLabel, stopLabel, onRun, onStop, }) {
|
|
45
131
|
return (_jsxs(HStack, { gap: 3, hAlign: "between", vAlign: "center", children: [_jsx(Text, { type: "supporting", color: "secondary", style: isReady ? undefined : { color: 'var(--color-error)' }, hasTabularNumbers: true, children: note }), _jsxs(HStack, { gap: 2, vAlign: "center", children: [isRunning ? _jsx(Button, { label: stopLabel, variant: "ghost", onClick: onStop }) : null, _jsx(Button, { label: isRunning ? runningLabel : runLabel, variant: "primary", isLoading: isRunning, isDisabled: !isReady, onClick: onRun })] })] }));
|
|
46
132
|
}
|
|
133
|
+
/** A size in bytes as the reader's locale writes it: bytes under a kilobyte, else kilobytes. */
|
|
134
|
+
export function useBytes() {
|
|
135
|
+
const locale = useLocale();
|
|
136
|
+
return useMemo(() => {
|
|
137
|
+
const b = new Intl.NumberFormat(locale, { style: 'unit', unit: 'byte', unitDisplay: 'narrow' });
|
|
138
|
+
const kb = new Intl.NumberFormat(locale, {
|
|
139
|
+
style: 'unit',
|
|
140
|
+
unit: 'kilobyte',
|
|
141
|
+
unitDisplay: 'narrow',
|
|
142
|
+
maximumFractionDigits: 1,
|
|
143
|
+
});
|
|
144
|
+
return (bytes) => (bytes < 1024 ? b.format(bytes) : kb.format(bytes / 1024));
|
|
145
|
+
}, [locale]);
|
|
146
|
+
}
|