@theoremjs/react 0.3.4-next.831.g934609e → 0.3.4-next.841.g5f7aa38

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 CHANGED
@@ -147,7 +147,7 @@ The profile, the keys, and the system prompt stay on the server. The browser rec
147
147
  | --- | --- |
148
148
  | `@theoremjs/react` | Hooks, transports, and client logic |
149
149
  | `@theoremjs/react/ui` | `TheoremChat`, `TheoremHost`, `TheoremDecision`, `TheoremEmbedding`, and their parts |
150
- | `@theoremjs/react/live` | `useLiveCall` and `LiveRunner`, the voice and video call and its UI |
150
+ | `@theoremjs/react/live` | `useLiveCall`, `LiveRunner` and `LiveCallTile`, the voice and video call and its UI |
151
151
  | `@theoremjs/react/server` | `@theoremjs/web/server`: the handlers and the stores they use |
152
152
  | `@theoremjs/react/ui/theme` | `theoremTheme`, the base for your own Astryx theme |
153
153
  | `@theoremjs/react/ui/icons` | The icon set of the theme |
@@ -367,6 +367,16 @@ The call lasts as long as the component that calls `useLiveCall`. Call it above
367
367
 
368
368
  Give `LiveRunner` an `onMinimize` to show a minimize control in the call. Theorem calls it and does not navigate: your code decides where to go, and the call goes on.
369
369
 
370
+ `LiveCallTile` draws the same call small: the camera preview, or the ink waveform when the camera is off, then the call time, mute, end, and expand. It fills the box you give it, so your code places it and sizes it. A press on the tile or its expand control calls `onExpand`.
371
+
372
+ ```tsx
373
+ import { LiveCallTile } from '@theoremjs/react/live';
374
+
375
+ <LiveCallTile call={call} onExpand={() => navigate('/call')} />
376
+ ```
377
+
378
+ Show `LiveRunner` or `LiveCallTile` for a call, not both: the camera preview is one element, and it shows in the one that mounted last.
379
+
370
380
  ### The page's part in a call
371
381
 
372
382
  Three more values for `useLiveCall` come from the page. The profile declares each one; the page only supplies the value.
@@ -22,6 +22,7 @@ export declare function useLiveCall({ iface, connection, ...options }: LiveCallI
22
22
  handle: string;
23
23
  traces: import("@theoremjs/web/client").TraceFeed;
24
24
  callStarted: boolean;
25
+ connectedAt: number | null;
25
26
  pastCalls: LiveCaptionTurn[][];
26
27
  captions: LiveCaptionState;
27
28
  failure: import("@theoremjs/web/client").ClientFailure | null;
@@ -14,6 +14,8 @@ import { useLiveSessionClient } from "./use-live-session-client.js";
14
14
  function useLiveCallLifecycle(controls, captionsRef, sessionActive, videoAvailable) {
15
15
  const [callStarted, setCallStarted] = useState(false);
16
16
  const [ended, setEnded] = useState(false);
17
+ /** When this call first connected; a reconnect keeps it, a new call clears it. */
18
+ const [connectedAt, setConnectedAt] = useState(null);
17
19
  const videoOnConnectRef = useRef(false);
18
20
  const startCall = useCallback((options) => {
19
21
  videoOnConnectRef.current = options.video && videoAvailable;
@@ -21,6 +23,10 @@ function useLiveCallLifecycle(controls, captionsRef, sessionActive, videoAvailab
21
23
  setEnded(false);
22
24
  void controls.startSession();
23
25
  }, [controls, videoAvailable]);
26
+ useEffect(() => {
27
+ if (sessionActive)
28
+ setConnectedAt((at) => at ?? Date.now());
29
+ }, [sessionActive]);
24
30
  // why: Camera capture needs a live session, so a video call turns it on once connected.
25
31
  useEffect(() => {
26
32
  if (!sessionActive || !videoOnConnectRef.current)
@@ -32,12 +38,14 @@ function useLiveCallLifecycle(controls, captionsRef, sessionActive, videoAvailab
32
38
  videoOnConnectRef.current = false;
33
39
  controls.teardownSession();
34
40
  setEnded(true);
41
+ setConnectedAt(null);
35
42
  }, [controls]);
36
43
  const [pastCalls, setPastCalls] = useState([]);
37
44
  const handleRestart = useCallback(async () => {
38
45
  const previous = captionsRef.current.turns;
39
46
  setPastCalls((calls) => stashLiveCaptionCall(calls, previous));
40
47
  setEnded(false);
48
+ setConnectedAt(null);
41
49
  await controls.handleRestart();
42
50
  }, [controls, captionsRef]);
43
51
  const teardownSessionRef = useRef(controls.teardownSession);
@@ -47,7 +55,7 @@ function useLiveCallLifecycle(controls, captionsRef, sessionActive, videoAvailab
47
55
  teardownSessionRef.current();
48
56
  };
49
57
  }, []);
50
- return { callStarted, ended, pastCalls, startCall, handleEnd, handleRestart };
58
+ return { callStarted, connectedAt, ended, pastCalls, startCall, handleEnd, handleRestart };
51
59
  }
52
60
  /**
53
61
  * Owns a live call: its session client, state, and controls. The call lasts as
@@ -139,7 +147,7 @@ export function useLiveCall({ iface, connection, ...options }) {
139
147
  setVideoFacingMode: ui.setVideoFacingMode,
140
148
  setIsVideoOn: ui.setIsVideoOn,
141
149
  });
142
- const { callStarted, ended, pastCalls, startCall, handleEnd, handleRestart } = useLiveCallLifecycle(controls, ui.captionsRef, ui.sessionActive, videoAvailable);
150
+ const { callStarted, connectedAt, ended, pastCalls, startCall, handleEnd, handleRestart } = useLiveCallLifecycle(controls, ui.captionsRef, ui.sessionActive, videoAvailable);
143
151
  // why: The page's context is compared by value, so a host may build it inline on each render.
144
152
  const contextJson = JSON.stringify(options.context);
145
153
  useEffect(() => {
@@ -161,6 +169,7 @@ export function useLiveCall({ iface, connection, ...options }) {
161
169
  handle: iface.identity.handle,
162
170
  traces,
163
171
  callStarted,
172
+ connectedAt,
164
173
  pastCalls,
165
174
  captions: ui.captions,
166
175
  failure: ui.failure,
package/live.d.ts CHANGED
@@ -5,5 +5,6 @@
5
5
  */
6
6
  export type { LiveCallOptions, LiveConnection, PageTool, PageToolAnswer, PageTools, } from '@theoremjs/web/client';
7
7
  export { type LiveCall, type LiveCallInput, useLiveCall, } from './components/live/use-live-call.ts';
8
+ export { LiveCallTile, type LiveCallTileProps } from './ui/LiveCallTile.tsx';
8
9
  export type { LiveRunnerProps } from './ui/LiveRunner.tsx';
9
10
  export { LiveRunner } from './ui/LiveRunner.tsx';
package/live.js CHANGED
@@ -8,4 +8,5 @@ import '@astryxdesign/core/reset.css';
8
8
  import '@astryxdesign/core/astryx.css';
9
9
  import './ui/built/theme.css';
10
10
  export { useLiveCall, } from "./components/live/use-live-call.js";
11
+ export { LiveCallTile } from "./ui/LiveCallTile.js";
11
12
  export { LiveRunner } from "./ui/LiveRunner.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/react",
3
- "version": "0.3.4-next.831.g934609e",
3
+ "version": "0.3.4-next.841.g5f7aa38",
4
4
  "description": "React bindings for Theorem: headless hooks + transport, an Astryx chat UI, and a Web-standard server handler.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,8 +60,8 @@
60
60
  "@stylexjs/stylex": "0.19.0",
61
61
  "react": "^19.0.0",
62
62
  "react-dom": "^19.0.0",
63
- "@theoremjs/agents": "^0.3.4-next.831.g934609e",
64
- "@theoremjs/web": "^0.3.4-next.831.g934609e"
63
+ "@theoremjs/agents": "^0.3.4-next.841.g5f7aa38",
64
+ "@theoremjs/web": "^0.3.4-next.841.g5f7aa38"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@astryxdesign/core": {
@@ -0,0 +1,24 @@
1
+ import type { DefinedTheme } from '@astryxdesign/core/theme';
2
+ import type { LiveCall } from '../components/live/use-live-call.ts';
3
+ import { type TheoremLabels } from './labels.ts';
4
+ export type LiveCallTileProps = {
5
+ /** The call to draw, from `useLiveCall`: the same call `LiveRunner` draws. */
6
+ call: LiveCall;
7
+ /**
8
+ * Called when the person asks to open the call full size, from the tile or
9
+ * its expand control. The host decides what that means, such as going back
10
+ * to the call's page.
11
+ */
12
+ onExpand: () => void;
13
+ /** Astryx theme, as on `LiveRunner`. */
14
+ theme?: DefinedTheme;
15
+ mode?: 'system' | 'light' | 'dark';
16
+ /** Replacement lines by locale, as on `LiveRunner`. */
17
+ labels?: TheoremLabels;
18
+ };
19
+ /**
20
+ * A small view of a running call: the camera preview, or the ink waveform
21
+ * when the camera is off, then the call time, mute, end, and expand. It fills
22
+ * the box the host gives it; the host places it.
23
+ */
24
+ export declare function LiveCallTile({ theme, mode, labels, ...props }: LiveCallTileProps): import("react").JSX.Element;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { AspectRatio } from '@astryxdesign/core/AspectRatio';
3
+ import { ClickableCard } from '@astryxdesign/core/ClickableCard';
4
+ import { HStack } from '@astryxdesign/core/HStack';
5
+ import { Icon } from '@astryxdesign/core/Icon';
6
+ import { IconButton } from '@astryxdesign/core/IconButton';
7
+ import { StackItem } from '@astryxdesign/core/Stack';
8
+ import { Text } from '@astryxdesign/core/Text';
9
+ import { ToggleButton } from '@astryxdesign/core/ToggleButton';
10
+ import { VStack } from '@astryxdesign/core/VStack';
11
+ import { IconArrowsDiagonal, IconMicrophone, IconMicrophoneOff, IconPhoneOff, } from '@tabler/icons-react';
12
+ import { InkWaveform } from "../components/InkWaveform.js";
13
+ import { useSecondTicker } from "../hooks/use-second-ticker.js";
14
+ import { LiveVideoPreview } from "./LiveVideoPreview.js";
15
+ import { liveDuration, liveStateLabel } from "./labels.js";
16
+ import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
17
+ import { TheoremThemeProvider } from "./theme.js";
18
+ /**
19
+ * A small view of a running call: the camera preview, or the ink waveform
20
+ * when the camera is off, then the call time, mute, end, and expand. It fills
21
+ * the box the host gives it; the host places it.
22
+ */
23
+ export function LiveCallTile({ theme, mode, labels, ...props }) {
24
+ return (_jsx(TheoremThemeProvider, { theme: theme, mode: mode, children: _jsx(TheoremLabelsProvider, { labels: labels, children: _jsx(LiveCallTileBody, { ...props }) }) }));
25
+ }
26
+ function LiveCallTileBody({ call, onExpand, }) {
27
+ const t = useLabels();
28
+ return (_jsx(ClickableCard, { label: t('@theorem.live.expand'), onClick: onExpand, padding: 0, elevation: "high", width: "100%", children: _jsxs(VStack, { children: [_jsx(TileMedia, { call: call }), _jsxs(HStack, { gap: 1, vAlign: "center", padding: 2, children: [_jsx(StackItem, { size: "fill", children: _jsx(TileTime, { call: call }) }), _jsx(TileControls, { call: call, onExpand: onExpand })] })] }) }));
29
+ }
30
+ /** The camera preview while the camera is on; otherwise the ink waveform. */
31
+ function TileMedia({ call }) {
32
+ if (call.isVideoOn && call.videoPreview) {
33
+ return _jsx(LiveVideoPreview, { video: call.videoPreview, facingMode: call.videoFacingMode });
34
+ }
35
+ return (_jsx(AspectRatio, { ratio: 4 / 3, children: _jsx(VStack, { height: "100%", vAlign: "center", padding: 4, "aria-hidden": "true", children: _jsx(InkWaveform, { status: call.status, levelsRef: call.levelsRef, toolActive: call.toolActive, variant: "pill" }) }) }));
36
+ }
37
+ /** How long the call has run, or its state before it connects and after it ends. */
38
+ function TileTime({ call }) {
39
+ const t = useLabels();
40
+ const since = call.sessionActive ? call.connectedAt : null;
41
+ const now = useSecondTicker(since !== null);
42
+ return (_jsx(Text, { type: "supporting", hasTabularNumbers: true, maxLines: 1, children: since === null
43
+ ? liveStateLabel(t, call.liveState, call.activeTool)
44
+ : liveDuration(t, now - since) }));
45
+ }
46
+ function TileControls({ call, onExpand }) {
47
+ const t = useLabels();
48
+ return (_jsxs(_Fragment, { children: [call.voiceAvailable ? (_jsx(ToggleButton, { label: t('@theorem.live.microphone'), tooltip: t(call.isMuted ? '@theorem.live.unmute' : '@theorem.live.mute'), isIconOnly: true, size: "sm", isPressed: !call.isMuted, isDisabled: !call.sessionActive, icon: _jsx(Icon, { icon: IconMicrophone }), pressedIcon: _jsx(Icon, { icon: IconMicrophoneOff }), onPressedChange: call.handleToggleMic })) : null, _jsx(IconButton, { label: t('@theorem.live.end_call'), tooltip: t('@theorem.live.end_call'), variant: "destructive", size: "sm", isDisabled: !call.sessionActive, icon: _jsx(Icon, { icon: IconPhoneOff }), onClick: call.handleEnd }), _jsx(IconButton, { label: t('@theorem.live.expand'), tooltip: t('@theorem.live.expand'), variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconArrowsDiagonal }), onClick: onExpand })] }));
49
+ }
package/ui/LiveRunner.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { AspectRatio } from '@astryxdesign/core/AspectRatio';
3
2
  import { Banner } from '@astryxdesign/core/Banner';
4
3
  import { Button } from '@astryxdesign/core/Button';
5
4
  import { Center } from '@astryxdesign/core/Center';
@@ -14,9 +13,10 @@ import { ToggleButton } from '@astryxdesign/core/ToggleButton';
14
13
  import { Toolbar } from '@astryxdesign/core/Toolbar';
15
14
  import { VStack } from '@astryxdesign/core/VStack';
16
15
  import { IconArrowsDiagonalMinimize2, IconCameraRotate, IconMessage, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneOff, IconVideo, IconVideoOff, } from '@tabler/icons-react';
17
- import { Fragment, useEffect, useLayoutEffect, useRef, } from 'react';
16
+ import { Fragment, useLayoutEffect, useRef } from 'react';
18
17
  import { InkWaveform } from "../components/InkWaveform.js";
19
18
  import { LiveCaptionsPanel } from "./LiveCaptionsPanel.js";
19
+ import { LiveVideoPreview } from "./LiveVideoPreview.js";
20
20
  import { liveStateLabel } from "./labels.js";
21
21
  import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
22
22
  import { PANEL_EDGE, SidePanel, SidePanelHeader, SidePanelToggle, useSidePanel, } from "./SidePanel.js";
@@ -134,35 +134,6 @@ function LiveCallButton({ call }) {
134
134
  function LiveCaptions({ call }) {
135
135
  return (_jsx(LiveCaptionsPanel, { handle: call.handle, captions: call.captions, pastCalls: call.pastCalls, draftText: call.textDraft, onDraftTextChange: call.setTextDraft, onSubmit: call.handleSendText, isDisabled: !call.sessionActive, showComposer: call.textAvailable, leading: call.videoPreview ? (_jsx(LiveVideoPreview, { video: call.videoPreview, facingMode: call.videoFacingMode })) : null }));
136
136
  }
137
- /**
138
- * The camera preview, at the top of the captions panel. The capture element comes
139
- * from the session client, so it is mounted imperatively; it sits one level
140
- * below AspectRatio, so it carries its own fill and mirror styles.
141
- */
142
- function LiveVideoPreview({ video, facingMode, }) {
143
- const t = useLabels();
144
- const hostRef = useRef(null);
145
- useEffect(() => {
146
- const host = hostRef.current;
147
- if (!host)
148
- return;
149
- Object.assign(video.style, {
150
- width: '100%',
151
- height: '100%',
152
- objectFit: 'cover',
153
- display: 'block',
154
- });
155
- host.replaceChildren(video);
156
- return () => {
157
- if (video.parentElement === host)
158
- host.removeChild(video);
159
- };
160
- }, [video]);
161
- useEffect(() => {
162
- video.style.transform = facingMode === 'user' ? 'scaleX(-1)' : '';
163
- }, [video, facingMode]);
164
- return (_jsx(AspectRatio, { ratio: 4 / 3, fit: "cover", children: _jsx("div", { ref: hostRef, "aria-label": t('@theorem.live.camera_preview'), role: "img" }) }));
165
- }
166
137
  /** Room around the gate card for its shadow. */
167
138
  const GATE_DIALOG_PADDING = 4;
168
139
  const GATE_DIALOG_WIDTH = GATE_CARD_WIDTH + 2 * GATE_DIALOG_PADDING * 4;
@@ -0,0 +1,10 @@
1
+ import type { LiveFacingMode } from '../client/live/live-video.ts';
2
+ /**
3
+ * The camera preview: atop the call's captions, or in its tile. The capture element comes
4
+ * from the session client, so it is mounted imperatively; it sits one level
5
+ * below AspectRatio, so it carries its own fill and mirror styles.
6
+ */
7
+ export declare function LiveVideoPreview({ video, facingMode, }: {
8
+ video: HTMLVideoElement;
9
+ facingMode: LiveFacingMode;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { AspectRatio } from '@astryxdesign/core/AspectRatio';
3
+ import { useEffect, useRef } from 'react';
4
+ import { useLabels } from "./labels-provider.js";
5
+ /**
6
+ * The camera preview: atop the call's captions, or in its tile. The capture element comes
7
+ * from the session client, so it is mounted imperatively; it sits one level
8
+ * below AspectRatio, so it carries its own fill and mirror styles.
9
+ */
10
+ export function LiveVideoPreview({ video, facingMode, }) {
11
+ const t = useLabels();
12
+ const hostRef = useRef(null);
13
+ useEffect(() => {
14
+ const host = hostRef.current;
15
+ if (!host)
16
+ return;
17
+ Object.assign(video.style, {
18
+ width: '100%',
19
+ height: '100%',
20
+ objectFit: 'cover',
21
+ display: 'block',
22
+ });
23
+ host.replaceChildren(video);
24
+ return () => {
25
+ if (video.parentElement === host)
26
+ host.removeChild(video);
27
+ };
28
+ }, [video]);
29
+ useEffect(() => {
30
+ video.style.transform = facingMode === 'user' ? 'scaleX(-1)' : '';
31
+ }, [video, facingMode]);
32
+ return (_jsx(AspectRatio, { ratio: 4 / 3, fit: "cover", children: _jsx("div", { ref: hostRef, "aria-label": t('@theorem.live.camera_preview'), role: "img" }) }));
33
+ }
package/ui/labels.d.ts CHANGED
@@ -1364,6 +1364,10 @@ export declare const THEOREM_UI_CATALOG: {
1364
1364
  readonly defaultMessage: "End call";
1365
1365
  readonly description: "Hangs up.";
1366
1366
  };
1367
+ readonly '@theorem.live.expand': {
1368
+ readonly defaultMessage: "Open call";
1369
+ readonly description: "Asks the host to show a minimized call full size.";
1370
+ };
1367
1371
  readonly '@theorem.live.minimize': {
1368
1372
  readonly defaultMessage: "Minimize call";
1369
1373
  readonly description: "Asks the host to show the call small; the call goes on.";
package/ui/labels.js CHANGED
@@ -1291,6 +1291,10 @@ export const THEOREM_UI_CATALOG = {
1291
1291
  description: 'Switches front and back camera.',
1292
1292
  },
1293
1293
  '@theorem.live.end_call': { defaultMessage: 'End call', description: 'Hangs up.' },
1294
+ '@theorem.live.expand': {
1295
+ defaultMessage: 'Open call',
1296
+ description: 'Asks the host to show a minimized call full size.',
1297
+ },
1294
1298
  '@theorem.live.minimize': {
1295
1299
  defaultMessage: 'Minimize call',
1296
1300
  description: 'Asks the host to show the call small; the call goes on.',