@theoremjs/react 0.3.4-next.827.gdc3858d → 0.3.4-next.831.g934609e
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 +27 -19
- package/components/live/{use-live-runner-model.d.ts → use-live-call.d.ts} +16 -5
- package/components/live/{use-live-runner-model.js → use-live-call.js} +6 -6
- package/live.d.ts +1 -0
- package/live.js +1 -0
- package/package.json +3 -3
- package/ui/LiveRunner.d.ts +10 -6
- package/ui/LiveRunner.js +38 -32
- package/ui/index.d.ts +1 -1
- package/ui/index.js +1 -1
- package/ui/labels.d.ts +4 -0
- package/ui/labels.js +4 -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` and `LiveRunner`, 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,59 @@ 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
|
+
|
|
362
370
|
### The page's part in a call
|
|
363
371
|
|
|
364
|
-
Three more
|
|
372
|
+
Three more values for `useLiveCall` come from the page. The profile declares each one; the page only supplies the value.
|
|
365
373
|
|
|
366
|
-
|
|
|
374
|
+
| Value | The profile declares | What the page gives |
|
|
367
375
|
| --- | --- | --- |
|
|
368
376
|
| `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
377
|
| `context` | `inputs.context` with `from: ['client']` | Any JSON the agent should know, such as the page the person is on. |
|
|
370
378
|
| `pageTools` | A function tool with `answeredBy: 'page'` | A function for each page tool, by name. It returns `{ output }`. |
|
|
371
379
|
|
|
372
|
-
`stated` is a fourth
|
|
380
|
+
`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
381
|
|
|
374
382
|
`model` and `effort` pick the call's model when the profile sets `allowModelSelect`. They are read when the call starts.
|
|
375
383
|
|
|
376
384
|
```tsx
|
|
377
|
-
|
|
378
|
-
iface
|
|
379
|
-
connection
|
|
380
|
-
options
|
|
381
|
-
context
|
|
382
|
-
pageTools
|
|
385
|
+
const call = useLiveCall({
|
|
386
|
+
iface,
|
|
387
|
+
connection: () => ({ profile: 'support.voice' }),
|
|
388
|
+
options: { language: 'fr' },
|
|
389
|
+
context: { page: '/pricing', cart },
|
|
390
|
+
pageTools: {
|
|
383
391
|
highlight: (args) => ({ output: { success: highlight(String(args.target)) } }),
|
|
384
|
-
}
|
|
385
|
-
|
|
392
|
+
},
|
|
393
|
+
});
|
|
386
394
|
```
|
|
387
395
|
|
|
388
|
-
- `context` goes with the start of the call. When its value changes,
|
|
396
|
+
- `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
397
|
- A page tool's output schema checks what the page returned. Every other tool runs on the relay, with its gates.
|
|
390
398
|
- `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
|
-
-
|
|
399
|
+
- `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
400
|
|
|
393
401
|
The live client checks a tool call with its relay before it invokes a page callback.
|
|
394
402
|
It waits for permission and sign-in decisions first. A claimed dispatch grant supplies the final checked arguments.
|
|
@@ -417,7 +425,7 @@ A failed call does not reconnect. Without `live.sessionResumption`, a provider-e
|
|
|
417
425
|
|
|
418
426
|
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
427
|
|
|
420
|
-
To build your own call view,
|
|
428
|
+
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
429
|
|
|
422
430
|
### A custom browser transport
|
|
423
431
|
|
|
@@ -1,13 +1,24 @@
|
|
|
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;
|
|
@@ -50,12 +50,11 @@ function useLiveCallLifecycle(controls, captionsRef, sessionActive, videoAvailab
|
|
|
50
50
|
return { callStarted, ended, pastCalls, startCall, handleEnd, handleRestart };
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
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.
|
|
53
|
+
* Owns a live call: its session client, state, and controls. The call lasts as
|
|
54
|
+
* long as the component that calls this hook, so a host that calls it above its
|
|
55
|
+
* routes keeps the call while the person moves between pages.
|
|
57
56
|
*/
|
|
58
|
-
export function
|
|
57
|
+
export function useLiveCall({ iface, connection, ...options }) {
|
|
59
58
|
const ui = useLiveRunnerUiState(iface.labels);
|
|
60
59
|
const gate = useLiveRunnerGate({
|
|
61
60
|
setCaptions: ui.setCaptions,
|
|
@@ -74,7 +73,7 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
|
|
|
74
73
|
isMuted: ui.isMuted,
|
|
75
74
|
voiceEnabled: voiceAvailable,
|
|
76
75
|
}), [ui.activeTool, ui.connectPhase, ui.isMuted, ui.status, voiceAvailable]);
|
|
77
|
-
// why: One feed per
|
|
76
|
+
// why: One feed per call: every session it opens adds its records.
|
|
78
77
|
const traces = useMemo(createTraceFeed, []);
|
|
79
78
|
const { clientRef, ensureClient, clearClient } = useLiveSessionClient({
|
|
80
79
|
options: options.options,
|
|
@@ -158,6 +157,7 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
|
|
|
158
157
|
}, [clientRef, statedJson]);
|
|
159
158
|
usePageToolWarnings(iface.tools.page, options.pageTools);
|
|
160
159
|
return {
|
|
160
|
+
iface,
|
|
161
161
|
handle: iface.identity.handle,
|
|
162
162
|
traces,
|
|
163
163
|
callStarted,
|
package/live.d.ts
CHANGED
|
@@ -4,5 +4,6 @@
|
|
|
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';
|
|
7
8
|
export type { LiveRunnerProps } from './ui/LiveRunner.tsx';
|
|
8
9
|
export { LiveRunner } from './ui/LiveRunner.tsx';
|
package/live.js
CHANGED
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.831.g934609e",
|
|
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.831.g934609e",
|
|
64
|
+
"@theoremjs/web": "^0.3.4-next.831.g934609e"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@astryxdesign/core": {
|
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
|
@@ -13,10 +13,9 @@ import { Text } from '@astryxdesign/core/Text';
|
|
|
13
13
|
import { ToggleButton } from '@astryxdesign/core/ToggleButton';
|
|
14
14
|
import { Toolbar } from '@astryxdesign/core/Toolbar';
|
|
15
15
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
16
|
-
import { IconCameraRotate, IconMessage, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneOff, IconVideo, IconVideoOff, } from '@tabler/icons-react';
|
|
16
|
+
import { IconArrowsDiagonalMinimize2, IconCameraRotate, IconMessage, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneOff, IconVideo, IconVideoOff, } from '@tabler/icons-react';
|
|
17
17
|
import { Fragment, useEffect, useLayoutEffect, useRef, } from 'react';
|
|
18
18
|
import { InkWaveform } from "../components/InkWaveform.js";
|
|
19
|
-
import { useLiveRunnerModel } from "../components/live/use-live-runner-model.js";
|
|
20
19
|
import { LiveCaptionsPanel } from "./LiveCaptionsPanel.js";
|
|
21
20
|
import { liveStateLabel } from "./labels.js";
|
|
22
21
|
import { TheoremLabelsProvider, useLabels } from "./labels-provider.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,34 +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:
|
|
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 }));
|
|
130
136
|
}
|
|
131
137
|
/**
|
|
132
138
|
* The camera preview, at the top of the captions panel. The capture element comes
|
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,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.minimize': {
|
|
1368
|
+
readonly defaultMessage: "Minimize call";
|
|
1369
|
+
readonly description: "Asks the host to show the call small; the call goes on.";
|
|
1370
|
+
};
|
|
1367
1371
|
readonly '@theorem.live.new_session': {
|
|
1368
1372
|
readonly defaultMessage: "New session";
|
|
1369
1373
|
readonly description: "Captions divider between calls.";
|
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.minimize': {
|
|
1295
|
+
defaultMessage: 'Minimize call',
|
|
1296
|
+
description: 'Asks the host to show the call small; the call goes on.',
|
|
1297
|
+
},
|
|
1294
1298
|
'@theorem.live.new_session': {
|
|
1295
1299
|
defaultMessage: 'New session',
|
|
1296
1300
|
description: 'Captions divider between calls.',
|