@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 +37 -19
- package/components/live/{use-live-runner-model.d.ts → use-live-call.d.ts} +17 -5
- package/components/live/{use-live-runner-model.js → use-live-call.js} +17 -8
- package/live.d.ts +2 -0
- package/live.js +2 -0
- package/package.json +3 -3
- package/ui/LiveCallTile.d.ts +24 -0
- package/ui/LiveCallTile.js +49 -0
- package/ui/LiveRunner.d.ts +10 -6
- package/ui/LiveRunner.js +40 -63
- package/ui/LiveVideoPreview.d.ts +10 -0
- package/ui/LiveVideoPreview.js +33 -0
- package/ui/index.d.ts +1 -1
- package/ui/index.js +1 -1
- package/ui/labels.d.ts +8 -0
- package/ui/labels.js +8 -0
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
|
|
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
|
|
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
|
-
|
|
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
|
|
382
|
+
Three more values for `useLiveCall` come from the page. The profile declares each one; the page only supplies the value.
|
|
365
383
|
|
|
366
|
-
|
|
|
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
|
|
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
|
-
|
|
378
|
-
iface
|
|
379
|
-
connection
|
|
380
|
-
options
|
|
381
|
-
context
|
|
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,
|
|
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
|
-
-
|
|
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,
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
64
|
-
"@theoremjs/web": "^0.3.4-next.
|
|
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
|
+
}
|
package/ui/LiveRunner.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { DefinedTheme } from '@astryxdesign/core/theme';
|
|
2
|
-
import type {
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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,
|
|
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({
|
|
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,
|
|
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 (!
|
|
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, {
|
|
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, {
|
|
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({
|
|
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:
|
|
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({
|
|
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:
|
|
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({
|
|
98
|
+
function LiveControls({ call, captionsToggle, onMinimize, }) {
|
|
103
99
|
const t = useLabels();
|
|
104
|
-
const inactive = !
|
|
100
|
+
const inactive = !call.sessionActive;
|
|
105
101
|
const controls = [];
|
|
106
|
-
if (
|
|
107
|
-
controls.push(_jsx(ToggleButton, { label: t('@theorem.live.microphone'), tooltip: t(
|
|
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 (
|
|
110
|
-
controls.push(_jsx(
|
|
111
|
-
|
|
112
|
-
|
|
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"),
|
|
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({
|
|
129
|
-
return (_jsx(LiveCaptionsPanel, { handle:
|
|
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 {
|
|
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 {
|
|
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.',
|