@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from '@astryxdesign/core/Text';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { createSpaceRenderer } from "./embedding-space-gl.js";
|
|
5
|
+
/** Times the GPU may drop the space before it is listed instead. */
|
|
6
|
+
const LOSSES = 2;
|
|
7
|
+
/** Pixels a press moves before it counts as turning the sphere, not pointing. */
|
|
8
|
+
const DRAG_PX = 3;
|
|
9
|
+
function isStill() {
|
|
10
|
+
return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
11
|
+
}
|
|
12
|
+
function SpaceCanvas({ space, query, matches, hovered, onHover, nameOf, label, onUnavailable, onLost, }) {
|
|
13
|
+
const canvasRef = useRef(null);
|
|
14
|
+
const rendererRef = useRef(null);
|
|
15
|
+
const press = useRef(null);
|
|
16
|
+
const failed = useRef(onUnavailable);
|
|
17
|
+
failed.current = onUnavailable;
|
|
18
|
+
const lost = useRef(onLost);
|
|
19
|
+
lost.current = onLost;
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const canvas = canvasRef.current;
|
|
22
|
+
if (!canvas)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
rendererRef.current = createSpaceRenderer(canvas, { still: isStill() });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
failed.current();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const onContextLost = (event) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
lost.current();
|
|
34
|
+
};
|
|
35
|
+
canvas.addEventListener('webglcontextlost', onContextLost);
|
|
36
|
+
return () => {
|
|
37
|
+
canvas.removeEventListener('webglcontextlost', onContextLost);
|
|
38
|
+
rendererRef.current?.destroy();
|
|
39
|
+
rendererRef.current = null;
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
rendererRef.current?.setSpace(space);
|
|
44
|
+
}, [space]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
rendererRef.current?.setFocus({ query, matches, hovered });
|
|
47
|
+
}, [query, matches, hovered]);
|
|
48
|
+
const at = (event) => {
|
|
49
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
50
|
+
return { x: event.clientX - box.left, y: event.clientY - box.top };
|
|
51
|
+
};
|
|
52
|
+
return (_jsxs("div", { style: { position: 'relative', width: '100%', aspectRatio: '1 / 1', maxHeight: 460 }, children: [_jsx("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: {
|
|
53
|
+
display: 'block',
|
|
54
|
+
width: '100%',
|
|
55
|
+
height: '100%',
|
|
56
|
+
cursor: hovered === null ? 'grab' : 'pointer',
|
|
57
|
+
// why: a vertical swipe still scrolls the page; only a sideways one turns the sphere.
|
|
58
|
+
touchAction: 'pan-y',
|
|
59
|
+
}, onPointerDown: (event) => {
|
|
60
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
61
|
+
press.current = { ...at(event), moved: false };
|
|
62
|
+
}, onPointerMove: (event) => {
|
|
63
|
+
const point = at(event);
|
|
64
|
+
const held = press.current;
|
|
65
|
+
if (held) {
|
|
66
|
+
const dx = point.x - held.x;
|
|
67
|
+
const dy = point.y - held.y;
|
|
68
|
+
if (!held.moved && Math.hypot(dx, dy) < DRAG_PX)
|
|
69
|
+
return;
|
|
70
|
+
held.moved = true;
|
|
71
|
+
rendererRef.current?.drag(dx, dy);
|
|
72
|
+
press.current = { ...point, moved: true };
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
onHover(rendererRef.current?.pick(point.x, point.y) ?? null);
|
|
76
|
+
}, onPointerUp: () => {
|
|
77
|
+
press.current = null;
|
|
78
|
+
rendererRef.current?.release();
|
|
79
|
+
}, onPointerCancel: () => {
|
|
80
|
+
press.current = null;
|
|
81
|
+
rendererRef.current?.release();
|
|
82
|
+
}, onPointerLeave: () => {
|
|
83
|
+
if (!press.current)
|
|
84
|
+
onHover(null);
|
|
85
|
+
} }), hovered !== null ? (_jsx("div", { style: { position: 'absolute', left: 0, right: 0, bottom: 0, pointerEvents: 'none' }, children: _jsx(Text, { type: "supporting", maxLines: 1, children: nameOf(hovered) }) })) : null] }));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The index drawn as a space: each document a dot in a ball, placed by the
|
|
89
|
+
* three directions the vectors differ most along, with a circle round the rim
|
|
90
|
+
* in each plane two of them span. Each document is joined to its nearest
|
|
91
|
+
* neighbour by cosine similarity when that pair stands out from the index's
|
|
92
|
+
* typical pair; documents the lines connect share a colour, and a query's
|
|
93
|
+
* white dot has a line to each match that stands out the same way. Drag to turn
|
|
94
|
+
* it; point at a dot to name its document.
|
|
95
|
+
*/
|
|
96
|
+
export function EmbeddingSpaceView(props) {
|
|
97
|
+
// why: a lost GPU context does not come back on the same canvas; a new one asks again.
|
|
98
|
+
const [generation, setGeneration] = useState(0);
|
|
99
|
+
const onLost = () => {
|
|
100
|
+
if (generation >= LOSSES)
|
|
101
|
+
props.onUnavailable();
|
|
102
|
+
else
|
|
103
|
+
setGeneration(generation + 1);
|
|
104
|
+
};
|
|
105
|
+
return _jsx(SpaceCanvas, { ...props, onLost: onLost }, generation);
|
|
106
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type LiveCaptionState, type LiveCaptionTurn } from '../client/live/live-captions.ts';
|
|
3
|
+
export type LiveCaptionsPanelProps = {
|
|
4
|
+
/** Profile handle. The transcript shows it as the agent's name. */
|
|
5
|
+
handle: string;
|
|
6
|
+
captions: LiveCaptionState;
|
|
7
|
+
pastCalls: readonly (readonly LiveCaptionTurn[])[];
|
|
8
|
+
draftText: string;
|
|
9
|
+
onDraftTextChange: (text: string) => void;
|
|
10
|
+
onSubmit: (text: string) => void;
|
|
11
|
+
/** Off while no call is connected. */
|
|
12
|
+
isDisabled: boolean;
|
|
13
|
+
/** Omit the text field when the profile takes no text. */
|
|
14
|
+
showComposer?: boolean;
|
|
15
|
+
/** Drawn above the transcript. The live runner puts the camera here. */
|
|
16
|
+
leading?: ReactNode;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The live call's captions: the chat transcript, an empty state, and the text
|
|
20
|
+
* composer. The same panel the live runner docks on the right.
|
|
21
|
+
*/
|
|
22
|
+
export declare function LiveCaptionsPanel(props: LiveCaptionsPanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
|
3
|
+
import { Icon } from '@astryxdesign/core/Icon';
|
|
4
|
+
import { StackItem } from '@astryxdesign/core/Stack';
|
|
5
|
+
import { VStack } from '@astryxdesign/core/VStack';
|
|
6
|
+
import { IconSubtitles } from '@tabler/icons-react';
|
|
7
|
+
import { liveCaptionStreaming, liveCaptionTranscript, } from "../client/live/live-captions.js";
|
|
8
|
+
import { ChatComposerBar } from "./ChatComposerBar.js";
|
|
9
|
+
import { ChatTranscript } from "./ChatTranscript.js";
|
|
10
|
+
import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
|
|
11
|
+
import { PanelScroll } from "./SidePanel.js";
|
|
12
|
+
/**
|
|
13
|
+
* The live call's captions: the chat transcript, an empty state, and the text
|
|
14
|
+
* composer. The same panel the live runner docks on the right.
|
|
15
|
+
*/
|
|
16
|
+
export function LiveCaptionsPanel(props) {
|
|
17
|
+
return (_jsx(TheoremLabelsProvider, { children: _jsx(LiveCaptionsBody, { ...props }) }));
|
|
18
|
+
}
|
|
19
|
+
function LiveCaptionsBody({ handle, captions, pastCalls, draftText, onDraftTextChange, onSubmit, isDisabled, showComposer = true, leading, }) {
|
|
20
|
+
const t = useLabels();
|
|
21
|
+
const agentName = t('@theorem.agent.handle', { handle });
|
|
22
|
+
const { blocks, callStarts } = liveCaptionTranscript(pastCalls, captions);
|
|
23
|
+
const newSession = t('@theorem.live.new_session');
|
|
24
|
+
const dividers = Object.fromEntries(callStarts.map((id) => [id, newSession]));
|
|
25
|
+
const composer = showComposer ? (_jsx(ChatComposerBar, { handle: handle, draftText: draftText, onDraftTextChange: onDraftTextChange, onSubmit: onSubmit, isDisabled: isDisabled })) : null;
|
|
26
|
+
return (_jsxs(VStack, { height: "100%", children: [leading, _jsx(StackItem, { size: "fill", children: blocks.length > 0 ? (_jsx(PanelScroll, { label: t('@theorem.panel.captions.name'), children: _jsx(ChatTranscript, { blocks: blocks, dividers: dividers, handle: agentName, streaming: liveCaptionStreaming(captions) }) })) : (_jsx(VStack, { height: "100%", vAlign: "center", padding: 4, children: _jsx(EmptyState, { icon: _jsx(Icon, { icon: IconSubtitles, size: "lg", color: "secondary" }), title: t('@theorem.panel.captions.empty.title'), description: t('@theorem.panel.captions.empty.description'), isCompact: true }) })) }), composer] }));
|
|
27
|
+
}
|
package/ui/LiveRunner.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DefinedTheme } from '@astryxdesign/core/theme';
|
|
2
2
|
import type { LiveProfileInterface } from '@theoremjs/agents/interface';
|
|
3
|
+
import type { LiveCallOptions } from '../client/live/live-page-tool.ts';
|
|
3
4
|
import type { LiveConnection } from '../client/live-client.ts';
|
|
4
5
|
import { type TheoremLabels } from './labels.ts';
|
|
5
6
|
export type LiveRunnerProps = {
|
|
@@ -12,12 +13,13 @@ export type LiveRunnerProps = {
|
|
|
12
13
|
/** Replacement lines by locale, as on `TheoremChat`. */
|
|
13
14
|
labels?: TheoremLabels;
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Drive the trace from the host's own control. The built-in toggle hides.
|
|
17
|
+
* The open trace takes the call's place, unless this page is inside
|
|
18
|
+
* `TracePlacement value="panel"`, where it docks. Omit to keep the toggle.
|
|
19
|
+
* Needs a profile that records traces.
|
|
18
20
|
*/
|
|
19
21
|
trace?: boolean;
|
|
20
|
-
};
|
|
22
|
+
} & LiveCallOptions;
|
|
21
23
|
/**
|
|
22
24
|
* Live (voice / video) runner on Astryx. Before a call: a landing like the
|
|
23
25
|
* chat's, offering a voice or video call. In a call: the agent tag and state
|
package/ui/LiveRunner.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AspectRatio } from '@astryxdesign/core/AspectRatio';
|
|
3
3
|
import { Banner } from '@astryxdesign/core/Banner';
|
|
4
4
|
import { Button } from '@astryxdesign/core/Button';
|
|
5
5
|
import { Center } from '@astryxdesign/core/Center';
|
|
6
|
-
import { ChatComposer, ChatComposerInput, ChatLayout, ChatMessage, ChatMessageBubble, ChatMessageList, ChatSystemMessage, ChatSendButton, } from '@astryxdesign/core/Chat';
|
|
7
6
|
import { Dialog } from '@astryxdesign/core/Dialog';
|
|
8
|
-
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
|
9
7
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
10
8
|
import { Icon } from '@astryxdesign/core/Icon';
|
|
11
9
|
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
@@ -15,18 +13,18 @@ import { Text } from '@astryxdesign/core/Text';
|
|
|
15
13
|
import { ToggleButton } from '@astryxdesign/core/ToggleButton';
|
|
16
14
|
import { Toolbar } from '@astryxdesign/core/Toolbar';
|
|
17
15
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
18
|
-
import { IconCameraRotate, IconMessage, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneOff,
|
|
19
|
-
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
16
|
+
import { IconCameraRotate, IconMessage, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneOff, IconVideo, IconVideoOff, } from '@tabler/icons-react';
|
|
17
|
+
import { Fragment, useEffect, useLayoutEffect, useRef, } from 'react';
|
|
20
18
|
import { InkWaveform } from "../components/InkWaveform.js";
|
|
21
19
|
import { useLiveRunnerModel } from "../components/live/use-live-runner-model.js";
|
|
22
|
-
import {
|
|
20
|
+
import { LiveCaptionsPanel } from "./LiveCaptionsPanel.js";
|
|
23
21
|
import { liveStateLabel } from "./labels.js";
|
|
24
22
|
import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
|
|
25
|
-
import { SidePanel, SidePanelHeader, SidePanelToggle, useSidePanel } from "./SidePanel.js";
|
|
26
|
-
import { ApprovalCard, AuthChallengeCard } from "./ToolGateCard.js";
|
|
23
|
+
import { PANEL_BOTTOM, PANEL_EDGE, SidePanel, SidePanelHeader, SidePanelToggle, useSidePanel, } from "./SidePanel.js";
|
|
27
24
|
import { DEFAULT_CHAT_MAX_WIDTH } from "./TheoremChat.js";
|
|
28
|
-
import {
|
|
25
|
+
import { ApprovalCard, AuthChallengeCard, GATE_CARD_WIDTH } from "./ToolGateCard.js";
|
|
29
26
|
import { useTraceInspector, WithTrace } from "./TraceInspectorPanel.js";
|
|
27
|
+
import { TheoremThemeProvider } from "./theme.js";
|
|
30
28
|
/** The waveform's box: it rests on the stage floor rather than filling the stage. */
|
|
31
29
|
const WAVE_MAX_WIDTH = 720;
|
|
32
30
|
const WAVE_HEIGHT = 320;
|
|
@@ -40,10 +38,10 @@ const WAVE_HEIGHT = 320;
|
|
|
40
38
|
export function LiveRunner({ theme, mode, labels, ...props }) {
|
|
41
39
|
return (_jsx(TheoremThemeProvider, { theme: theme, mode: mode, children: _jsx(TheoremLabelsProvider, { labels: labels, children: _jsx(LiveRunnerBody, { ...props }) }) }));
|
|
42
40
|
}
|
|
43
|
-
function LiveRunnerBody({ iface, connection, trace }) {
|
|
41
|
+
function LiveRunnerBody({ iface, connection, trace, slots, context, pageTools, }) {
|
|
44
42
|
const t = useLabels();
|
|
45
|
-
const model = useLiveRunnerModel(iface, connection);
|
|
46
|
-
// Both panels size against the whole live layout, so a third means the same for each.
|
|
43
|
+
const model = useLiveRunnerModel(iface, connection, { slots, context, pageTools });
|
|
44
|
+
// why: Both panels size against the whole live layout, so a third means the same for each.
|
|
47
45
|
const layoutRef = useRef(null);
|
|
48
46
|
const inspector = useTraceInspector(iface, model.traces, trace);
|
|
49
47
|
const captions = useSidePanel(layoutRef, true);
|
|
@@ -57,7 +55,7 @@ function LiveRunnerBody({ iface, connection, trace }) {
|
|
|
57
55
|
if (!model.callStarted) {
|
|
58
56
|
return (_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", header: _jsx(SidePanelHeader, { children: inspector.toggle }), content: _jsx(LayoutContent, { padding: 0, children: _jsx(LiveLanding, { model: model }) }) }) }));
|
|
59
57
|
}
|
|
60
|
-
return (_jsxs(_Fragment, { children: [_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", header:
|
|
58
|
+
return (_jsxs(_Fragment, { children: [_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", header: _jsx(SidePanelHeader, { children: inspector.toggle }), content: _jsx(LayoutContent, { padding: 0, children: _jsx(Layout, { height: "fill", end: _jsx(SidePanel, { id: captions.id, labels: captionLabels, resizable: captions.resizable, open: captions.open, children: _jsx(LiveCaptions, { model: model }) }), content: _jsx(LayoutContent, { padding: 0, children: _jsx(VStack, { height: "100%", paddingInline: PANEL_EDGE, paddingBlockEnd: PANEL_BOTTOM, children: _jsx(LiveStage, { model: model, captionsToggle: captionsToggle }) }) }) }) }) }) }), _jsx(LiveToolGateDialog, { prompt: model.gatePrompt, agent: t('@theorem.agent.handle', { handle: model.handle }), onResolve: model.resolveGateDecision })] }));
|
|
61
59
|
}
|
|
62
60
|
/**
|
|
63
61
|
* Before a call: the chat landing's greeting, with call buttons where the
|
|
@@ -65,16 +63,18 @@ function LiveRunnerBody({ iface, connection, trace }) {
|
|
|
65
63
|
*/
|
|
66
64
|
function LiveLanding({ model }) {
|
|
67
65
|
const t = useLabels();
|
|
68
|
-
return (_jsx(VStack, { minHeight: "100%", vAlign: "center", gap: 8, paddingInline: 3, children: _jsx(Center, { axis: "horizontal", width: "100%", children: _jsxs(VStack, { width: "100%", maxWidth: DEFAULT_CHAT_MAX_WIDTH, gap: 8, children: [_jsxs(VStack, { gap: 1, children: [_jsx(Text, { type: "large", as: "h2", children: t('@theorem.agent.handle', { handle: model.handle }) }), _jsx(Text, { type: "display-2", as: "h1", children: t('@theorem.live.greeting') })] }), _jsxs(HStack, { gap: 2, wrap: "wrap", children: [_jsx(Button, { label: t(model.voiceAvailable
|
|
66
|
+
return (_jsx(VStack, { minHeight: "100%", vAlign: "center", gap: 8, paddingInline: 3, children: _jsx(Center, { axis: "horizontal", width: "100%", children: _jsxs(VStack, { width: "100%", maxWidth: DEFAULT_CHAT_MAX_WIDTH, gap: 8, children: [_jsxs(VStack, { gap: 1, children: [_jsx(Text, { type: "large", as: "h2", children: t('@theorem.agent.handle', { handle: model.handle }) }), _jsx(Text, { type: "display-2", as: "h1", children: t('@theorem.live.greeting') })] }), _jsxs(HStack, { gap: 2, wrap: "wrap", children: [_jsx(Button, { label: t(model.voiceAvailable
|
|
67
|
+
? '@theorem.live.start_voice_call'
|
|
68
|
+
: '@theorem.live.start_call'), variant: "primary", icon: _jsx(Icon, { icon: IconPhone }), onClick: () => model.startCall({ video: false }) }), model.videoAvailable ? (_jsx(Button, { label: t('@theorem.live.start_video_call'), variant: "secondary", icon: _jsx(Icon, { icon: IconVideo }), onClick: () => model.startCall({ video: true }) })) : null] })] }) }) }));
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* The agent tag and state over the resting waveform, then the call controls,
|
|
72
72
|
* all in one column the waveform's width.
|
|
73
73
|
*/
|
|
74
|
-
function LiveStage({ model }) {
|
|
74
|
+
function LiveStage({ model, captionsToggle }) {
|
|
75
75
|
const t = useLabels();
|
|
76
76
|
const ref = useEnterMotion();
|
|
77
|
-
return (_jsx(VStack, { ref: ref, height: "100%", hAlign: "center", children: _jsxs(VStack, { width: "100%", maxWidth: WAVE_MAX_WIDTH, height: "100%", gap: 3, children: [_jsxs(VStack, { gap: 0.5, children: [_jsx(Text, { type: "label", color: "secondary", children: t('@theorem.agent.handle', { handle: model.handle }) }), _jsx(Text, { size: "sm", color: "secondary", children: liveStateLabel(t, model.liveState, model.activeTool) })] }), model.failure ? _jsx(Banner, { status: "error", title: model.failure.error }) : null, model.sessionEnded ? _jsx(Banner, { status: "info", title: model.sessionEnded }) : null, _jsx(StackItem, { size: "fill", children: _jsx(VStack, { height: "100%", vAlign: "end", children: _jsx(VStack, { width: "100%", height: WAVE_HEIGHT, style: { color: 'var(--color-icon-primary)' }, "aria-hidden": "true", children: _jsx(InkWaveform, { status: model.status,
|
|
77
|
+
return (_jsx(VStack, { ref: ref, height: "100%", hAlign: "center", children: _jsxs(VStack, { width: "100%", maxWidth: WAVE_MAX_WIDTH, height: "100%", gap: 3, children: [_jsxs(VStack, { gap: 0.5, children: [_jsx(Text, { type: "label", color: "secondary", children: t('@theorem.agent.handle', { handle: model.handle }) }), _jsx(Text, { size: "sm", color: "secondary", children: liveStateLabel(t, model.liveState, model.activeTool) })] }), model.failure ? _jsx(Banner, { status: "error", role: "alert", title: model.failure.error }) : null, model.sessionEnded ? (_jsx(Banner, { status: "info", role: "status", title: model.sessionEnded })) : null, _jsx(StackItem, { size: "fill", children: _jsx(VStack, { height: "100%", vAlign: "end", children: _jsx(VStack, { width: "100%", height: WAVE_HEIGHT, style: { color: 'var(--color-icon-primary)' }, "aria-hidden": "true", children: _jsx(InkWaveform, { status: model.status, levelsRef: model.levelsRef, toolActive: model.toolActive, variant: "hero" }) }) }) }), _jsx(LiveControls, { model: model, captionsToggle: captionsToggle })] }) }));
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* Eases the call view in from the landing: Astryx has no enter-transition
|
|
@@ -96,10 +96,10 @@ function useEnterMotion() {
|
|
|
96
96
|
return ref;
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
* Mic, camera,
|
|
99
|
+
* Mic, camera, captions, and call controls, centred under the stage. Icons show
|
|
100
100
|
* what a click does, as End call does: a slash means "turn this off".
|
|
101
101
|
*/
|
|
102
|
-
function LiveControls({ model }) {
|
|
102
|
+
function LiveControls({ model, captionsToggle }) {
|
|
103
103
|
const t = useLabels();
|
|
104
104
|
const inactive = !model.sessionActive;
|
|
105
105
|
const controls = [];
|
|
@@ -116,57 +116,36 @@ function LiveControls({ model }) {
|
|
|
116
116
|
} }, "flip"));
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
controls.push(model.canRestart ? (_jsx(IconButton, { label: t('@theorem.live.start_call'), tooltip: t('@theorem.live.start_call'), variant: "primary", icon: _jsx(Icon, { icon: IconPhone }), onClick: () => {
|
|
119
|
+
controls.push(_jsx(Fragment, { children: captionsToggle }, "captions"), model.canRestart ? (_jsx(IconButton, { label: t('@theorem.live.start_call'), tooltip: t('@theorem.live.start_call'), variant: "primary", icon: _jsx(Icon, { icon: IconPhone }), onClick: () => {
|
|
120
120
|
void model.handleRestart();
|
|
121
121
|
} }, "call")) : (_jsx(IconButton, { label: t('@theorem.live.end_call'), tooltip: t('@theorem.live.end_call'), variant: "destructive", isDisabled: inactive, icon: _jsx(Icon, { icon: IconPhoneOff }), onClick: model.handleEnd }, "call")));
|
|
122
122
|
return (_jsx(Toolbar, { label: t('@theorem.live.controls'), dividers: ['top'], centerContent: _jsx(HStack, { gap: 2, vAlign: "center", children: controls }) }));
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
|
-
* Captions
|
|
126
|
-
*
|
|
125
|
+
* Captions drawn by the chat's own transcript, with the text composer in its
|
|
126
|
+
* dock when the profile takes text.
|
|
127
127
|
*/
|
|
128
128
|
function LiveCaptions({ model }) {
|
|
129
|
-
|
|
130
|
-
const { interimAgent } = model.captions;
|
|
131
|
-
const agentName = t('@theorem.agent.handle', { handle: model.handle });
|
|
132
|
-
const composer = model.textAvailable ? (_jsx(ChatComposer, { style: NO_FOCUS_RING, value: model.textDraft, onChange: model.setTextDraft, onSubmit: model.handleSendText, isDisabled: !model.sessionActive, placeholder: t('@theorem.composer.placeholder', { handle: model.handle }), input: _jsx(ChatComposerInput, {}), sendButton: _jsx(ChatSendButton, {}) })) : null;
|
|
133
|
-
const messages = [
|
|
134
|
-
...model.pastCalls.flatMap((turns, call) => [
|
|
135
|
-
...turns.map((turn) => captionMessage(`${String(call)}:${turn.id}`, turn.role, turn.text, agentName)),
|
|
136
|
-
_jsx(ChatSystemMessage, { variant: "divider", children: t('@theorem.live.new_session') }, `session-${String(call + 1)}`),
|
|
137
|
-
]),
|
|
138
|
-
...captionMessages(model.captions, agentName),
|
|
139
|
-
];
|
|
140
|
-
// The empty state goes in ChatLayout's slot, which centres it; the list's
|
|
141
|
-
// own slot sits under its bottom-align spacer. ChatLayout flexes to fill the
|
|
142
|
-
// stack below any video preview.
|
|
143
|
-
return (_jsxs(VStack, { height: "100%", children: [model.videoPreview ? (_jsx(LiveVideoPreview, { video: model.videoPreview, facingMode: model.videoFacingMode })) : null, _jsx(ChatLayout, { composer: composer, emptyState: _jsx(EmptyState, { icon: _jsx(Icon, { icon: IconSubtitles, size: "lg", color: "secondary" }), title: t('@theorem.panel.captions.empty.title'), description: t('@theorem.panel.captions.empty.description'), isCompact: true }), children: messages.length > 0 ? (_jsx(ChatMessageList, { isStreaming: interimAgent !== '', children: messages })) : null })] }));
|
|
144
|
-
}
|
|
145
|
-
/** The current call's committed turns, then the lines still being heard. */
|
|
146
|
-
function captionMessages({ turns, interimUser, interimAgent }, agentName) {
|
|
147
|
-
const messages = turns.map((turn) => captionMessage(turn.id, turn.role, turn.text, agentName));
|
|
148
|
-
if (interimUser)
|
|
149
|
-
messages.push(captionMessage('interim-user', 'user', interimUser, agentName));
|
|
150
|
-
if (interimAgent)
|
|
151
|
-
messages.push(captionMessage('interim-agent', 'agent', interimAgent, agentName));
|
|
152
|
-
return messages;
|
|
153
|
-
}
|
|
154
|
-
function captionMessage(key, role, text, agentName) {
|
|
155
|
-
return role === 'user' ? (_jsx(ChatMessage, { sender: "user", children: _jsx(ChatMessageBubble, { children: text }) }, key)) : (_jsx(ChatMessage, { sender: "assistant", name: agentName, children: _jsx(Text, { children: text }) }, key));
|
|
129
|
+
return (_jsx(LiveCaptionsPanel, { handle: model.handle, captions: model.captions, pastCalls: model.pastCalls, draftText: model.textDraft, onDraftTextChange: model.setTextDraft, onSubmit: model.handleSendText, isDisabled: !model.sessionActive, showComposer: model.textAvailable, leading: model.videoPreview ? (_jsx(LiveVideoPreview, { video: model.videoPreview, facingMode: model.videoFacingMode })) : null }));
|
|
156
130
|
}
|
|
157
131
|
/**
|
|
158
132
|
* The camera preview, at the top of the captions panel. The capture element comes
|
|
159
133
|
* from the session client, so it is mounted imperatively; it sits one level
|
|
160
134
|
* below AspectRatio, so it carries its own fill and mirror styles.
|
|
161
135
|
*/
|
|
162
|
-
function LiveVideoPreview({ video, facingMode }) {
|
|
136
|
+
function LiveVideoPreview({ video, facingMode, }) {
|
|
163
137
|
const t = useLabels();
|
|
164
138
|
const hostRef = useRef(null);
|
|
165
139
|
useEffect(() => {
|
|
166
140
|
const host = hostRef.current;
|
|
167
141
|
if (!host)
|
|
168
142
|
return;
|
|
169
|
-
Object.assign(video.style, {
|
|
143
|
+
Object.assign(video.style, {
|
|
144
|
+
width: '100%',
|
|
145
|
+
height: '100%',
|
|
146
|
+
objectFit: 'cover',
|
|
147
|
+
display: 'block',
|
|
148
|
+
});
|
|
170
149
|
host.replaceChildren(video);
|
|
171
150
|
return () => {
|
|
172
151
|
if (video.parentElement === host)
|
|
@@ -178,16 +157,24 @@ function LiveVideoPreview({ video, facingMode }) {
|
|
|
178
157
|
}, [video, facingMode]);
|
|
179
158
|
return (_jsx(AspectRatio, { ratio: 4 / 3, fit: "cover", children: _jsx("div", { ref: hostRef, "aria-label": t('@theorem.live.camera_preview'), role: "img" }) }));
|
|
180
159
|
}
|
|
181
|
-
/**
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
160
|
+
/** Room around the gate card for its shadow. */
|
|
161
|
+
const GATE_DIALOG_PADDING = 4;
|
|
162
|
+
const GATE_DIALOG_WIDTH = GATE_CARD_WIDTH + 2 * GATE_DIALOG_PADDING * 4;
|
|
163
|
+
/** The dialog draws no surface of its own: the card is the one chat shows, alone. */
|
|
164
|
+
const GATE_DIALOG_BARE = { background: 'transparent', boxShadow: 'none' };
|
|
165
|
+
/**
|
|
166
|
+
* Tool approvals and credential prompts, as a dialog that must be answered. It shows the card
|
|
167
|
+
* chat shows inline and nothing else, and closes on the answer.
|
|
168
|
+
*/
|
|
169
|
+
function LiveToolGateDialog({ prompt, agent, onResolve, }) {
|
|
170
|
+
return (_jsx(Dialog, { isOpen: prompt !== null, purpose: "required", width: GATE_DIALOG_WIDTH, padding: GATE_DIALOG_PADDING, style: GATE_DIALOG_BARE,
|
|
171
|
+
// why: "required" disables dismissal; should one slip through, it denies.
|
|
185
172
|
onOpenChange: (open) => {
|
|
186
173
|
if (!open)
|
|
187
174
|
onResolve({ action: 'deny' });
|
|
188
175
|
}, children: prompt ? (prompt.gate.kind === 'auth' ? (_jsx(AuthChallengeCard, { gate: prompt.gate, toolName: prompt.gate.tool, onAuthenticated: (secret) => {
|
|
189
176
|
onResolve({ action: 'auth', secret });
|
|
190
|
-
} })) : (_jsx(ApprovalCard, { gate: prompt.gate, toolName: prompt.gate.tool, input: prompt.input, onDecision: (action) => {
|
|
177
|
+
} })) : (_jsx(ApprovalCard, { gate: prompt.gate, toolName: prompt.gate.tool, agent: agent, input: prompt.input, onDecision: (action) => {
|
|
191
178
|
onResolve({ action });
|
|
192
179
|
} }))) : null }));
|
|
193
180
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* What a pane says when it has nothing of its own to show: centred both ways
|
|
4
|
+
* in the room it is given. Loading, a failure, and an empty pane are this one
|
|
5
|
+
* component, so they sit in the same place.
|
|
6
|
+
*/
|
|
7
|
+
export declare function PaneState({ icon, title, description, actions, children, }: {
|
|
8
|
+
/** Every pane state has one: it is what the eye finds first. */
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
/** The buttons that lead out of the state. */
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
/** What goes under the words: the detail of a failure, or the way out of it. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* A pane that is waiting: the spinner where an empty pane's icon would be,
|
|
19
|
+
* once the wait has gone on a moment; blank and busy before then. Given when
|
|
20
|
+
* the work began, it counts the seconds under the label.
|
|
21
|
+
*/
|
|
22
|
+
export declare function PaneLoading({ label, since }: {
|
|
23
|
+
label: string;
|
|
24
|
+
since?: number | null;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
26
|
+
/** A pane that could not show what it is for, and why. */
|
|
27
|
+
export declare function PaneFailure({ title, description, actions, children, }: {
|
|
28
|
+
title: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
/** The buttons that lead out of the failure. */
|
|
31
|
+
actions?: ReactNode;
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}): import("react").JSX.Element;
|
package/ui/PaneState.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
|
3
|
+
import { Icon } from '@astryxdesign/core/Icon';
|
|
4
|
+
import { Spinner } from '@astryxdesign/core/Spinner';
|
|
5
|
+
import { VStack } from '@astryxdesign/core/VStack';
|
|
6
|
+
import { IconAlertTriangle } from '@tabler/icons-react';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { useSecondTicker } from "../hooks/use-second-ticker.js";
|
|
9
|
+
import { liveDuration } from "./labels.js";
|
|
10
|
+
import { useLabels } from "./labels-provider.js";
|
|
11
|
+
/**
|
|
12
|
+
* What a pane says when it has nothing of its own to show: centred both ways
|
|
13
|
+
* in the room it is given. Loading, a failure, and an empty pane are this one
|
|
14
|
+
* component, so they sit in the same place.
|
|
15
|
+
*/
|
|
16
|
+
export function PaneState({ icon, title, description, actions, children, }) {
|
|
17
|
+
return (_jsxs(VStack, { height: "100%", vAlign: "center", hAlign: "center", gap: 3, padding: 4, children: [_jsx(EmptyState, { icon: icon, title: title, description: description, actions: actions, isCompact: true }), children] }));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A pane that is waiting: the spinner where an empty pane's icon would be,
|
|
21
|
+
* once the wait has gone on a moment; blank and busy before then. Given when
|
|
22
|
+
* the work began, it counts the seconds under the label.
|
|
23
|
+
*/
|
|
24
|
+
export function PaneLoading({ label, since }) {
|
|
25
|
+
const t = useLabels();
|
|
26
|
+
const now = useSecondTicker(since != null);
|
|
27
|
+
const long = useLongWait();
|
|
28
|
+
if (!long)
|
|
29
|
+
return _jsx("div", { "aria-busy": "true", style: { height: '100%' } });
|
|
30
|
+
return (_jsx(PaneState, { icon: _jsx(Spinner, { size: "lg" }), title: label, description: since == null ? undefined : liveDuration(t, now - since) }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* How long a wait stays blank before its spinner shows. Most waits (a profile
|
|
34
|
+
* describing itself, a switch to another agent) end sooner, and the pane
|
|
35
|
+
* changes straight to what it waited for instead of flashing a spinner.
|
|
36
|
+
*/
|
|
37
|
+
const QUIET_WAIT_MS = 300;
|
|
38
|
+
/** Whether a wait has gone on long enough to say so. */
|
|
39
|
+
function useLongWait() {
|
|
40
|
+
const [long, setLong] = useState(false);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const timer = globalThis.setTimeout(() => setLong(true), QUIET_WAIT_MS);
|
|
43
|
+
return () => globalThis.clearTimeout(timer);
|
|
44
|
+
}, []);
|
|
45
|
+
return long;
|
|
46
|
+
}
|
|
47
|
+
/** A pane that could not show what it is for, and why. */
|
|
48
|
+
export function PaneFailure({ title, description, actions, children, }) {
|
|
49
|
+
return (_jsx(PaneState, { icon: _jsx(Icon, { icon: IconAlertTriangle, size: "lg", color: "secondary" }), title: title, description: description, actions: actions, children: children }));
|
|
50
|
+
}
|
package/ui/SchemaFields.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ import { type Edit as StateEdit } from './StateFields.tsx';
|
|
|
5
5
|
* input its type asks for, labelled by its title or key and described by its
|
|
6
6
|
* description. Optional fields left empty aren't sent.
|
|
7
7
|
*/
|
|
8
|
-
export declare function SchemaFields({ schema, value, onChange }: StateEdit<Record<string, unknown>> & {
|
|
8
|
+
export declare function SchemaFields({ schema, value, onChange, }: StateEdit<Record<string, unknown>> & {
|
|
9
9
|
schema: JsonSchema;
|
|
10
10
|
}): import("react").JSX.Element;
|