@theoremjs/react 0.3.4-next.827.gdc3858d → 0.3.4-next.837.g097599a

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` | `LiveRunner`, the voice and video call 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 |
@@ -344,51 +344,69 @@ For your own UI, use `useTheoremEmbedding` for one request, and `embed(inputs, {
344
344
 
345
345
  A `live` profile runs a voice or video call. The call goes through a WebSocket relay that you host with `runSession` from `@theoremjs/agents`.
346
346
 
347
- `LiveRunner` needs two props:
347
+ `useLiveCall` holds the call, and `LiveRunner` draws it. `useLiveCall` needs two values:
348
348
 
349
349
  - `iface`: the interface of the profile. `interfaceFromProfile` (from `@theoremjs/agents/interface`) builds it.
350
- - `connection`: a function that tells the runner what to connect to.
350
+ - `connection`: a function that tells the call what to connect to.
351
351
 
352
352
  ```tsx
353
- import { LiveRunner } from '@theoremjs/react/live';
353
+ import { LiveRunner, useLiveCall } from '@theoremjs/react/live';
354
354
 
355
- <LiveRunner iface={iface} connection={() => ({ profile: 'support.voice' })} />
355
+ const call = useLiveCall({ iface, connection: () => ({ profile: 'support.voice' }) });
356
+
357
+ <LiveRunner call={call} />
356
358
  ```
357
359
 
358
360
  This code opens `wss://<your host>/api/live/relay?profile=<id>`. To send your own message to the relay, add `openMessage`. To use your own socket, add `createSocket`.
359
361
 
360
362
  `LiveRunner` also accepts `theme`, `mode`, `labels`, and `trace`. Tool gates in a call appear in a dialog over the call.
361
363
 
364
+ ### A call across pages
365
+
366
+ The call lasts as long as the component that calls `useLiveCall`. Call it above your routes, and the call goes on while the person moves to another page. `LiveRunner` can unmount and mount again on the same call. The call ends when the component that holds it unmounts.
367
+
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
+
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
+
362
380
  ### The page's part in a call
363
381
 
364
- Three more props give the call values from the page. The profile declares each one; the page only supplies the value.
382
+ Three more values for `useLiveCall` come from the page. The profile declares each one; the page only supplies the value.
365
383
 
366
- | Prop | The profile declares | What the page gives |
384
+ | Value | The profile declares | What the page gives |
367
385
  | --- | --- | --- |
368
386
  | `options` | `inputs.options` | The value chosen for each option. It is read when the call starts. A value that changes during a call takes the call up again with it when the profile has resumption on; otherwise the next call has it. |
369
387
  | `context` | `inputs.context` with `from: ['client']` | Any JSON the agent should know, such as the page the person is on. |
370
388
  | `pageTools` | A function tool with `answeredBy: 'page'` | A function for each page tool, by name. It returns `{ output }`. |
371
389
 
372
- `stated` is a fourth prop, for a page you trust as yourself. It says the `path` and the `options` from `identity.options` that choose the call's prompt lines, and the server reads it only with `pageStatesTurn` set. A change during a call takes the call up again, as an option does. `TheoremChat` takes the same prop, on each turn.
390
+ `stated` is a fourth value, for a page you trust as yourself. It says the `path` and the `options` from `identity.options` that choose the call's prompt lines, and the server reads it only with `pageStatesTurn` set. A change during a call takes the call up again, as an option does. `TheoremChat` takes the same prop, on each turn.
373
391
 
374
392
  `model` and `effort` pick the call's model when the profile sets `allowModelSelect`. They are read when the call starts.
375
393
 
376
394
  ```tsx
377
- <LiveRunner
378
- iface={iface}
379
- connection={() => ({ profile: 'support.voice' })}
380
- options={{ language: 'fr' }}
381
- context={{ page: '/pricing', cart }}
382
- pageTools={{
395
+ const call = useLiveCall({
396
+ iface,
397
+ connection: () => ({ profile: 'support.voice' }),
398
+ options: { language: 'fr' },
399
+ context: { page: '/pricing', cart },
400
+ pageTools: {
383
401
  highlight: (args) => ({ output: { success: highlight(String(args.target)) } }),
384
- }}
385
- />
402
+ },
403
+ });
386
404
  ```
387
405
 
388
- - `context` goes with the start of the call. When its value changes, the runner sends it again. The agent reads it as background and does not reply to it.
406
+ - `context` goes with the start of the call. When its value changes, `useLiveCall` sends it again. The agent reads it as background and does not reply to it.
389
407
  - A page tool's output schema checks what the page returned. Every other tool runs on the relay, with its gates.
390
408
  - `TheoremChat` and `useTheoremChat` take the same `pageTools`. When a chat agent calls a page tool, the reply waits for the page's answer and then goes on. The person is shown no prompt.
391
- - The runner writes a console warning when the profile has a page tool with no function in `pageTools`, and when `pageTools` has a function for a tool that is not a page tool.
409
+ - `useLiveCall` writes a console warning when the profile has a page tool with no function in `pageTools`, and when `pageTools` has a function for a tool that is not a page tool.
392
410
 
393
411
  The live client checks a tool call with its relay before it invokes a page callback.
394
412
  It waits for permission and sign-in decisions first. A claimed dispatch grant supplies the final checked arguments.
@@ -417,7 +435,7 @@ A failed call does not reconnect. Without `live.sessionResumption`, a provider-e
417
435
 
418
436
  The relay is server code. To authenticate the conversation and resume a call in your relay, see [Your relay](https://github.com/masudl-hub/theoremai/blob/main/web/README.md#your-relay).
419
437
 
420
- To build your own call view, use `useLiveRunnerModel(iface, connection, { options, context, pageTools })` from `@theoremjs/react/ui`. It returns the state of the call, its captions, and its controls. `LiveCaptionsPanel` draws the captions.
438
+ To build your own call view, draw the call that `useLiveCall` returns. It holds the state of the call, its captions, and its controls. `LiveCaptionsPanel` from `@theoremjs/react/ui` draws the captions.
421
439
 
422
440
  ### A custom browser transport
423
441
 
@@ -1,16 +1,28 @@
1
1
  import type { LiveProfileInterface } from '@theoremjs/agents/interface';
2
2
  import type { LiveCallOptions, LiveConnection } from '@theoremjs/web/client';
3
3
  import { type LiveCaptionState, type LiveCaptionTurn } from '../../client/live/live-captions.ts';
4
+ /** What `useLiveCall` takes: the profile, what the call opens, and the host's part in the call. */
5
+ export type LiveCallInput = {
6
+ iface: LiveProfileInterface;
7
+ /**
8
+ * What the call opens: a host with a fixed profile returns its id; the studio
9
+ * returns its draft as the relay's open message.
10
+ */
11
+ connection: () => LiveConnection | Promise<LiveConnection>;
12
+ } & LiveCallOptions;
13
+ /** One live call: its state, captions, and controls. `LiveRunner` draws it. */
14
+ export type LiveCall = ReturnType<typeof useLiveCall>;
4
15
  /**
5
- * Owns LiveRunner state, session client, and stage callbacks.
6
- * `connection` resolves what the call opens: a host with a fixed profile returns
7
- * its id; the studio returns its draft as the relay's open message. `options`
8
- * are the host's part in the call: its options, its stated turn, the page's context, and its page tools.
16
+ * Owns a live call: its session client, state, and controls. The call lasts as
17
+ * long as the component that calls this hook, so a host that calls it above its
18
+ * routes keeps the call while the person moves between pages.
9
19
  */
10
- export declare function useLiveRunnerModel(iface: LiveProfileInterface, connection: () => LiveConnection | Promise<LiveConnection>, options?: LiveCallOptions): {
20
+ export declare function useLiveCall({ iface, connection, ...options }: LiveCallInput): {
21
+ iface: LiveProfileInterface;
11
22
  handle: string;
12
23
  traces: import("@theoremjs/web/client").TraceFeed;
13
24
  callStarted: boolean;
25
+ connectedAt: number | null;
14
26
  pastCalls: LiveCaptionTurn[][];
15
27
  captions: LiveCaptionState;
16
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,15 +55,14 @@ 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
- * Owns LiveRunner state, session client, and stage callbacks.
54
- * `connection` resolves what the call opens: a host with a fixed profile returns
55
- * its id; the studio returns its draft as the relay's open message. `options`
56
- * are the host's part in the call: its options, its stated turn, the page's context, and its page tools.
61
+ * Owns a live call: its session client, state, and controls. The call lasts as
62
+ * long as the component that calls this hook, so a host that calls it above its
63
+ * routes keeps the call while the person moves between pages.
57
64
  */
58
- export function useLiveRunnerModel(iface, connection, options = {}) {
65
+ export function useLiveCall({ iface, connection, ...options }) {
59
66
  const ui = useLiveRunnerUiState(iface.labels);
60
67
  const gate = useLiveRunnerGate({
61
68
  setCaptions: ui.setCaptions,
@@ -74,7 +81,7 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
74
81
  isMuted: ui.isMuted,
75
82
  voiceEnabled: voiceAvailable,
76
83
  }), [ui.activeTool, ui.connectPhase, ui.isMuted, ui.status, voiceAvailable]);
77
- // why: One feed per runner: every session it opens adds its records.
84
+ // why: One feed per call: every session it opens adds its records.
78
85
  const traces = useMemo(createTraceFeed, []);
79
86
  const { clientRef, ensureClient, clearClient } = useLiveSessionClient({
80
87
  options: options.options,
@@ -140,7 +147,7 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
140
147
  setVideoFacingMode: ui.setVideoFacingMode,
141
148
  setIsVideoOn: ui.setIsVideoOn,
142
149
  });
143
- 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);
144
151
  // why: The page's context is compared by value, so a host may build it inline on each render.
145
152
  const contextJson = JSON.stringify(options.context);
146
153
  useEffect(() => {
@@ -158,9 +165,11 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
158
165
  }, [clientRef, statedJson]);
159
166
  usePageToolWarnings(iface.tools.page, options.pageTools);
160
167
  return {
168
+ iface,
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
@@ -4,5 +4,7 @@
4
4
  * @module
5
5
  */
6
6
  export type { LiveCallOptions, LiveConnection, PageTool, PageToolAnswer, PageTools, } from '@theoremjs/web/client';
7
+ export { type LiveCall, type LiveCallInput, useLiveCall, } from './components/live/use-live-call.ts';
8
+ export { LiveCallTile, type LiveCallTileProps } from './ui/LiveCallTile.tsx';
7
9
  export type { LiveRunnerProps } from './ui/LiveRunner.tsx';
8
10
  export { LiveRunner } from './ui/LiveRunner.tsx';
package/live.js CHANGED
@@ -7,4 +7,6 @@
7
7
  import '@astryxdesign/core/reset.css';
8
8
  import '@astryxdesign/core/astryx.css';
9
9
  import './ui/built/theme.css';
10
+ export { useLiveCall, } from "./components/live/use-live-call.js";
11
+ export { LiveCallTile } from "./ui/LiveCallTile.js";
10
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.827.gdc3858d",
3
+ "version": "0.3.4-next.837.g097599a",
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.827.gdc3858d",
64
- "@theoremjs/web": "^0.3.4-next.827.gdc3858d"
63
+ "@theoremjs/agents": "^0.3.4-next.837.g097599a",
64
+ "@theoremjs/web": "^0.3.4-next.837.g097599a"
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
+ }
@@ -1,11 +1,9 @@
1
1
  import type { DefinedTheme } from '@astryxdesign/core/theme';
2
- import type { LiveProfileInterface } from '@theoremjs/agents/interface';
3
- import type { LiveCallOptions, LiveConnection } from '@theoremjs/web/client';
2
+ import type { LiveCall } from '../components/live/use-live-call.ts';
4
3
  import { type TheoremLabels } from './labels.ts';
5
4
  export type LiveRunnerProps = {
6
- iface: LiveProfileInterface;
7
- /** What the call opens: a profile the host registered, or an open message for the relay. */
8
- connection: () => LiveConnection | Promise<LiveConnection>;
5
+ /** The call to draw, from `useLiveCall`. The call outlives the runner: it ends when its owner unmounts. */
6
+ call: LiveCall;
9
7
  /** Astryx theme, as on `TheoremChat`. Omit to inherit the host's `<Theme>` or use `theoremTheme`. */
10
8
  theme?: DefinedTheme;
11
9
  mode?: 'system' | 'light' | 'dark';
@@ -18,7 +16,13 @@ export type LiveRunnerProps = {
18
16
  * Needs a profile that records traces.
19
17
  */
20
18
  trace?: boolean;
21
- } & LiveCallOptions;
19
+ /**
20
+ * Called when the person asks to minimize the call. The host decides what
21
+ * that means, such as going to another page; the call goes on. Omit to hide
22
+ * the control.
23
+ */
24
+ onMinimize?: () => void;
25
+ };
22
26
  /**
23
27
  * Live (voice / video) runner on Astryx. Before a call: a landing like the
24
28
  * chat's, offering a voice or video call. In a call: the agent tag and state
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';
@@ -13,11 +12,11 @@ import { Text } from '@astryxdesign/core/Text';
13
12
  import { ToggleButton } from '@astryxdesign/core/ToggleButton';
14
13
  import { Toolbar } from '@astryxdesign/core/Toolbar';
15
14
  import { VStack } from '@astryxdesign/core/VStack';
16
- import { IconCameraRotate, IconMessage, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneOff, IconVideo, IconVideoOff, } from '@tabler/icons-react';
17
- import { Fragment, useEffect, useLayoutEffect, useRef, } from 'react';
15
+ import { IconArrowsDiagonalMinimize2, IconCameraRotate, IconMessage, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneOff, IconVideo, IconVideoOff, } from '@tabler/icons-react';
16
+ import { Fragment, useLayoutEffect, useRef } from 'react';
18
17
  import { InkWaveform } from "../components/InkWaveform.js";
19
- import { useLiveRunnerModel } from "../components/live/use-live-runner-model.js";
20
18
  import { LiveCaptionsPanel } from "./LiveCaptionsPanel.js";
19
+ import { LiveVideoPreview } from "./LiveVideoPreview.js";
21
20
  import { liveStateLabel } from "./labels.js";
22
21
  import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
23
22
  import { PANEL_EDGE, SidePanel, SidePanelHeader, SidePanelToggle, useSidePanel, } from "./SidePanel.js";
@@ -38,12 +37,11 @@ const WAVE_HEIGHT = 320;
38
37
  export function LiveRunner({ theme, mode, labels, ...props }) {
39
38
  return (_jsx(TheoremThemeProvider, { theme: theme, mode: mode, children: _jsx(TheoremLabelsProvider, { labels: labels, children: _jsx(LiveRunnerBody, { ...props }) }) }));
40
39
  }
41
- function LiveRunnerBody({ iface, connection, trace, ...options }) {
40
+ function LiveRunnerBody({ call, trace, onMinimize, }) {
42
41
  const t = useLabels();
43
- const model = useLiveRunnerModel(iface, connection, options);
44
42
  // why: Both panels size against the whole live layout, so a third means the same for each.
45
43
  const layoutRef = useRef(null);
46
- const inspector = useTraceInspector(iface, model.traces, trace);
44
+ const inspector = useTraceInspector(call.iface, call.traces, trace);
47
45
  const captions = useSidePanel(layoutRef, true);
48
46
  const captionLabels = {
49
47
  name: t('@theorem.panel.captions.name'),
@@ -52,29 +50,27 @@ function LiveRunnerBody({ iface, connection, trace, ...options }) {
52
50
  resize: t('@theorem.panel.captions.resize'),
53
51
  };
54
52
  const captionsToggle = (_jsx(SidePanelToggle, { labels: captionLabels, icon: _jsx(Icon, { icon: IconMessage }), panelId: captions.id, open: captions.open, onToggle: captions.toggle }));
55
- if (!model.callStarted) {
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 }) }) }) }));
53
+ if (!call.callStarted) {
54
+ 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, { call: call }) }) }) }));
57
55
  }
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%", padding: PANEL_EDGE, children: _jsx(LiveStage, { model: model, captionsToggle: captionsToggle }) }) }) }) }) }) }), _jsx(LiveToolGateDialog, { prompt: model.gatePrompt, agent: t('@theorem.agent.handle', { handle: model.handle }), onResolve: model.resolveGateDecision })] }));
56
+ 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, { call: call }) }), content: _jsx(LayoutContent, { padding: 0, children: _jsx(VStack, { height: "100%", padding: PANEL_EDGE, children: _jsx(LiveStage, { call: call, captionsToggle: captionsToggle, onMinimize: onMinimize }) }) }) }) }) }) }), _jsx(LiveToolGateDialog, { prompt: call.gatePrompt, agent: t('@theorem.agent.handle', { handle: call.handle }), onResolve: call.resolveGateDecision })] }));
59
57
  }
60
58
  /**
61
59
  * Before a call: the chat landing's greeting, with call buttons where the
62
60
  * composer would be. Video starts camera and mic together.
63
61
  */
64
- function LiveLanding({ model }) {
62
+ function LiveLanding({ call }) {
65
63
  const t = useLabels();
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] })] }) }) }));
64
+ 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: call.handle }) }), _jsx(Text, { type: "display-2", as: "h1", children: t('@theorem.live.greeting') })] }), _jsxs(HStack, { gap: 2, wrap: "wrap", children: [_jsx(Button, { label: t(call.voiceAvailable ? '@theorem.live.start_voice_call' : '@theorem.live.start_call'), variant: "primary", icon: _jsx(Icon, { icon: IconPhone }), onClick: () => call.startCall({ video: false }) }), call.videoAvailable ? (_jsx(Button, { label: t('@theorem.live.start_video_call'), variant: "secondary", icon: _jsx(Icon, { icon: IconVideo }), onClick: () => call.startCall({ video: true }) })) : null] })] }) }) }));
69
65
  }
70
66
  /**
71
67
  * The agent tag and state over the resting waveform, then the call controls,
72
68
  * all in one column the waveform's width.
73
69
  */
74
- function LiveStage({ model, captionsToggle }) {
70
+ function LiveStage({ call, captionsToggle, onMinimize, }) {
75
71
  const t = useLabels();
76
72
  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", 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 })] }) }));
73
+ 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: call.handle }) }), _jsx(Text, { size: "sm", color: "secondary", children: liveStateLabel(t, call.liveState, call.activeTool) })] }), call.failure ? _jsx(Banner, { status: "error", role: "alert", title: call.failure.error }) : null, call.sessionEnded ? (_jsx(Banner, { status: "info", role: "status", title: call.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: call.status, levelsRef: call.levelsRef, toolActive: call.toolActive, variant: "hero" }) }) }) }), _jsx(LiveControls, { call: call, captionsToggle: captionsToggle, onMinimize: onMinimize })] }) }));
78
74
  }
79
75
  /**
80
76
  * Eases the call view in from the landing: Astryx has no enter-transition
@@ -99,63 +95,44 @@ function useEnterMotion() {
99
95
  * Mic, camera, captions, and call controls, centred under the stage. Icons show
100
96
  * what a click does, as End call does: a slash means "turn this off".
101
97
  */
102
- function LiveControls({ model, captionsToggle }) {
98
+ function LiveControls({ call, captionsToggle, onMinimize, }) {
103
99
  const t = useLabels();
104
- const inactive = !model.sessionActive;
100
+ const inactive = !call.sessionActive;
105
101
  const controls = [];
106
- if (model.voiceAvailable) {
107
- controls.push(_jsx(ToggleButton, { label: t('@theorem.live.microphone'), tooltip: t(model.isMuted ? '@theorem.live.unmute' : '@theorem.live.mute'), isIconOnly: true, isPressed: !model.isMuted, isDisabled: inactive, icon: _jsx(Icon, { icon: IconMicrophone }), pressedIcon: _jsx(Icon, { icon: IconMicrophoneOff }), onPressedChange: model.handleToggleMic }, "mic"));
102
+ if (call.voiceAvailable) {
103
+ controls.push(_jsx(ToggleButton, { label: t('@theorem.live.microphone'), tooltip: t(call.isMuted ? '@theorem.live.unmute' : '@theorem.live.mute'), isIconOnly: true, isPressed: !call.isMuted, isDisabled: inactive, icon: _jsx(Icon, { icon: IconMicrophone }), pressedIcon: _jsx(Icon, { icon: IconMicrophoneOff }), onPressedChange: call.handleToggleMic }, "mic"));
108
104
  }
109
- if (model.videoAvailable) {
110
- controls.push(_jsx(ToggleButton, { label: t('@theorem.live.camera'), tooltip: t(model.isVideoOn ? '@theorem.live.camera_off' : '@theorem.live.camera_on'), isIconOnly: true, isPressed: model.isVideoOn, isDisabled: inactive, icon: _jsx(Icon, { icon: IconVideo }), pressedIcon: _jsx(Icon, { icon: IconVideoOff }), onPressedChange: () => {
111
- void model.handleToggleVideo();
112
- } }, "video"));
113
- if (model.isVideoOn) {
114
- controls.push(_jsx(IconButton, { label: t('@theorem.live.flip_camera'), tooltip: t('@theorem.live.flip_camera'), variant: "ghost", isDisabled: inactive, icon: _jsx(Icon, { icon: IconCameraRotate }), onClick: () => {
115
- void model.handleFlipCamera();
116
- } }, "flip"));
117
- }
105
+ if (call.videoAvailable)
106
+ controls.push(_jsx(LiveVideoControls, { call: call }, "video"));
107
+ if (onMinimize) {
108
+ controls.push(_jsx(IconButton, { label: t('@theorem.live.minimize'), tooltip: t('@theorem.live.minimize'), variant: "ghost", icon: _jsx(Icon, { icon: IconArrowsDiagonalMinimize2 }), onClick: onMinimize }, "minimize"));
118
109
  }
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
- void model.handleRestart();
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")));
110
+ controls.push(_jsx(Fragment, { children: captionsToggle }, "captions"), _jsx(LiveCallButton, { call: call }, "call"));
122
111
  return (_jsx(Toolbar, { label: t('@theorem.live.controls'), dividers: ['top'], centerContent: _jsx(HStack, { gap: 2, vAlign: "center", children: controls }) }));
123
112
  }
113
+ /** The camera toggle, and the flip control while the camera is on. */
114
+ function LiveVideoControls({ call }) {
115
+ const t = useLabels();
116
+ const inactive = !call.sessionActive;
117
+ return (_jsxs(_Fragment, { children: [_jsx(ToggleButton, { label: t('@theorem.live.camera'), tooltip: t(call.isVideoOn ? '@theorem.live.camera_off' : '@theorem.live.camera_on'), isIconOnly: true, isPressed: call.isVideoOn, isDisabled: inactive, icon: _jsx(Icon, { icon: IconVideo }), pressedIcon: _jsx(Icon, { icon: IconVideoOff }), onPressedChange: () => {
118
+ void call.handleToggleVideo();
119
+ } }), call.isVideoOn ? (_jsx(IconButton, { label: t('@theorem.live.flip_camera'), tooltip: t('@theorem.live.flip_camera'), variant: "ghost", isDisabled: inactive, icon: _jsx(Icon, { icon: IconCameraRotate }), onClick: () => {
120
+ void call.handleFlipCamera();
121
+ } })) : null] }));
122
+ }
123
+ /** End call while one runs; Start call once it can start again. */
124
+ function LiveCallButton({ call }) {
125
+ const t = useLabels();
126
+ return call.canRestart ? (_jsx(IconButton, { label: t('@theorem.live.start_call'), tooltip: t('@theorem.live.start_call'), variant: "primary", icon: _jsx(Icon, { icon: IconPhone }), onClick: () => {
127
+ void call.handleRestart();
128
+ } })) : (_jsx(IconButton, { label: t('@theorem.live.end_call'), tooltip: t('@theorem.live.end_call'), variant: "destructive", isDisabled: !call.sessionActive, icon: _jsx(Icon, { icon: IconPhoneOff }), onClick: call.handleEnd }));
129
+ }
124
130
  /**
125
131
  * Captions drawn by the chat's own transcript, with the text composer in its
126
132
  * dock when the profile takes text.
127
133
  */
128
- function LiveCaptions({ model }) {
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 }));
130
- }
131
- /**
132
- * The camera preview, at the top of the captions panel. The capture element comes
133
- * from the session client, so it is mounted imperatively; it sits one level
134
- * below AspectRatio, so it carries its own fill and mirror styles.
135
- */
136
- function LiveVideoPreview({ video, facingMode, }) {
137
- const t = useLabels();
138
- const hostRef = useRef(null);
139
- useEffect(() => {
140
- const host = hostRef.current;
141
- if (!host)
142
- return;
143
- Object.assign(video.style, {
144
- width: '100%',
145
- height: '100%',
146
- objectFit: 'cover',
147
- display: 'block',
148
- });
149
- host.replaceChildren(video);
150
- return () => {
151
- if (video.parentElement === host)
152
- host.removeChild(video);
153
- };
154
- }, [video]);
155
- useEffect(() => {
156
- video.style.transform = facingMode === 'user' ? 'scaleX(-1)' : '';
157
- }, [video, facingMode]);
158
- return (_jsx(AspectRatio, { ratio: 4 / 3, fit: "cover", children: _jsx("div", { ref: hostRef, "aria-label": t('@theorem.live.camera_preview'), role: "img" }) }));
134
+ function LiveCaptions({ call }) {
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 }));
159
136
  }
160
137
  /** Room around the gate card for its shadow. */
161
138
  const 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/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export type { LiveCallOptions, PageTool, PageToolAnswer, PageTools, } from '@the
12
12
  export type { InkWaveStatus } from '../client/ink-waveform.ts';
13
13
  export { useLiveCaptionLog } from '../client/live/use-live-caption-log.ts';
14
14
  export { InkWaveform, type InkWaveformProps, type InkWaveLevels, } from '../components/InkWaveform.tsx';
15
- export { useLiveRunnerModel } from '../components/live/use-live-runner-model.ts';
15
+ export { type LiveCall, type LiveCallInput, useLiveCall, } from '../components/live/use-live-call.ts';
16
16
  export { theoremTheme } from './built/theorem.js';
17
17
  export { ChatComposerBar, type ChatComposerBarProps, EFFORT_ICON, generationOptions, type LiveComposerBarProps, } from './ChatComposerBar.tsx';
18
18
  export { ChatTranscript, type ChatTranscriptProps } from './ChatTranscript.tsx';
package/ui/index.js CHANGED
@@ -15,7 +15,7 @@ import '@astryxdesign/core/astryx.css';
15
15
  import './built/theme.css';
16
16
  export { useLiveCaptionLog } from "../client/live/use-live-caption-log.js";
17
17
  export { InkWaveform, } from "../components/InkWaveform.js";
18
- export { useLiveRunnerModel } from "../components/live/use-live-runner-model.js";
18
+ export { useLiveCall, } from "../components/live/use-live-call.js";
19
19
  export { theoremTheme } from './built/theorem.js';
20
20
  export { ChatComposerBar, EFFORT_ICON, generationOptions, } from "./ChatComposerBar.js";
21
21
  export { ChatTranscript } from "./ChatTranscript.js";
package/ui/labels.d.ts CHANGED
@@ -1364,6 +1364,14 @@ 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
+ };
1371
+ readonly '@theorem.live.minimize': {
1372
+ readonly defaultMessage: "Minimize call";
1373
+ readonly description: "Asks the host to show the call small; the call goes on.";
1374
+ };
1367
1375
  readonly '@theorem.live.new_session': {
1368
1376
  readonly defaultMessage: "New session";
1369
1377
  readonly description: "Captions divider between calls.";
package/ui/labels.js CHANGED
@@ -1291,6 +1291,14 @@ 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
+ },
1298
+ '@theorem.live.minimize': {
1299
+ defaultMessage: 'Minimize call',
1300
+ description: 'Asks the host to show the call small; the call goes on.',
1301
+ },
1294
1302
  '@theorem.live.new_session': {
1295
1303
  defaultMessage: 'New session',
1296
1304
  description: 'Captions divider between calls.',