@theoremjs/react 0.3.4-next.755.g88665bd → 0.3.4-next.759.g46e0e27
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 +4 -4
- package/components/live/use-live-runner-model.d.ts +1 -1
- package/components/live/use-live-runner-model.js +7 -7
- package/components/live/use-live-session-client.js +1 -1
- package/hooks/use-theorem-chat.d.ts +3 -3
- package/hooks/use-theorem-chat.js +2 -2
- package/package.json +3 -3
- package/ui/ChatComposerBar.d.ts +8 -8
- package/ui/ChatComposerBar.js +7 -7
- package/ui/TheoremChat.d.ts +4 -4
- package/ui/TheoremChat.js +4 -4
package/README.md
CHANGED
|
@@ -365,11 +365,11 @@ Three more props give the call values from the page. The profile declares each o
|
|
|
365
365
|
|
|
366
366
|
| Prop | The profile declares | What the page gives |
|
|
367
367
|
| --- | --- | --- |
|
|
368
|
-
| `
|
|
368
|
+
| `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
369
|
| `context` | `inputs.context` with `from: ['client']` | Any JSON the agent should know, such as the page the person is on. |
|
|
370
370
|
| `pageTools` | A function tool with `answeredBy: 'page'` | A function for each page tool, by name. It returns `{ output }`. |
|
|
371
371
|
|
|
372
|
-
`stated` is a fourth prop, for a page you trust as yourself. It says the `path` and `
|
|
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.
|
|
373
373
|
|
|
374
374
|
`model` and `effort` pick the call's model when the profile sets `allowModelSelect`. They are read when the call starts.
|
|
375
375
|
|
|
@@ -377,7 +377,7 @@ Three more props give the call values from the page. The profile declares each o
|
|
|
377
377
|
<LiveRunner
|
|
378
378
|
iface={iface}
|
|
379
379
|
connection={() => ({ profile: 'support.voice' })}
|
|
380
|
-
|
|
380
|
+
options={{ language: 'fr' }}
|
|
381
381
|
context={{ page: '/pricing', cart }}
|
|
382
382
|
pageTools={{
|
|
383
383
|
highlight: (args) => ({ output: { success: highlight(String(args.target)) } }),
|
|
@@ -417,7 +417,7 @@ A failed call does not reconnect. Without `live.sessionResumption`, a provider-e
|
|
|
417
417
|
|
|
418
418
|
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
419
|
|
|
420
|
-
To build your own call view, use `useLiveRunnerModel(iface, connection, {
|
|
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.
|
|
421
421
|
|
|
422
422
|
### A custom browser transport
|
|
423
423
|
|
|
@@ -5,7 +5,7 @@ import { type LiveCaptionState, type LiveCaptionTurn } from '../../client/live/l
|
|
|
5
5
|
* Owns LiveRunner state, session client, and stage callbacks.
|
|
6
6
|
* `connection` resolves what the call opens: a host with a fixed profile returns
|
|
7
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
|
|
8
|
+
* are the host's part in the call: its options, its stated turn, the page's context, and its page tools.
|
|
9
9
|
*/
|
|
10
10
|
export declare function useLiveRunnerModel(iface: LiveProfileInterface, connection: () => LiveConnection | Promise<LiveConnection>, options?: LiveCallOptions): {
|
|
11
11
|
handle: string;
|
|
@@ -53,7 +53,7 @@ function useLiveCallLifecycle(controls, captionsRef, sessionActive, videoAvailab
|
|
|
53
53
|
* Owns LiveRunner state, session client, and stage callbacks.
|
|
54
54
|
* `connection` resolves what the call opens: a host with a fixed profile returns
|
|
55
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
|
|
56
|
+
* are the host's part in the call: its options, its stated turn, the page's context, and its page tools.
|
|
57
57
|
*/
|
|
58
58
|
export function useLiveRunnerModel(iface, connection, options = {}) {
|
|
59
59
|
const ui = useLiveRunnerUiState(iface.labels);
|
|
@@ -77,7 +77,7 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
|
|
|
77
77
|
// why: One feed per runner: every session it opens adds its records.
|
|
78
78
|
const traces = useMemo(createTraceFeed, []);
|
|
79
79
|
const { clientRef, ensureClient, clearClient } = useLiveSessionClient({
|
|
80
|
-
|
|
80
|
+
options: options.options,
|
|
81
81
|
stated: options.stated,
|
|
82
82
|
model: options.model,
|
|
83
83
|
effort: options.effort,
|
|
@@ -146,12 +146,12 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
|
|
|
146
146
|
useEffect(() => {
|
|
147
147
|
clientRef.current?.setContext(contextJson === undefined ? undefined : JSON.parse(contextJson));
|
|
148
148
|
}, [clientRef, contextJson]);
|
|
149
|
-
// why:
|
|
150
|
-
const
|
|
149
|
+
// why: Options are compared by value too; a changed value takes an open call up again with it.
|
|
150
|
+
const optionsJson = JSON.stringify(options.options);
|
|
151
151
|
useEffect(() => {
|
|
152
|
-
clientRef.current?.
|
|
153
|
-
}, [clientRef,
|
|
154
|
-
// why: As
|
|
152
|
+
clientRef.current?.setOptions(optionsJson === undefined ? undefined : JSON.parse(optionsJson));
|
|
153
|
+
}, [clientRef, optionsJson]);
|
|
154
|
+
// why: As options: a changed pick takes an open call up again with the lines it chooses.
|
|
155
155
|
const statedJson = JSON.stringify(options.stated);
|
|
156
156
|
useEffect(() => {
|
|
157
157
|
clientRef.current?.setStated(statedJson === undefined ? undefined : JSON.parse(statedJson));
|
|
@@ -8,9 +8,9 @@ export type UseTheoremChatOptions = ChatOptions;
|
|
|
8
8
|
* `@theoremjs/web/client`, which works with no framework.
|
|
9
9
|
*/
|
|
10
10
|
export declare function useTheoremChat(options: UseTheoremChatOptions): {
|
|
11
|
-
|
|
11
|
+
optionChoices: {
|
|
12
12
|
onChange: (name: string, value: string) => void;
|
|
13
|
-
declared:
|
|
13
|
+
declared: import("../../../mod").ProfileOptions;
|
|
14
14
|
values: Record<string, string>;
|
|
15
15
|
} | undefined;
|
|
16
16
|
setDraftText: (value: import("@theoremjs/web/client").ChatUpdate<string>) => void;
|
|
@@ -50,5 +50,5 @@ export declare function useTheoremChat(options: UseTheoremChatOptions): {
|
|
|
50
50
|
session: import("../../../src/interface/session").InterfaceTurnSession;
|
|
51
51
|
delivery: import("@theoremjs/web/client").MessageDelivery | null;
|
|
52
52
|
streaming: boolean;
|
|
53
|
-
|
|
53
|
+
options: import("@theoremjs/web/client").ChatOptionPicks | undefined;
|
|
54
54
|
};
|
|
@@ -12,13 +12,13 @@ export function useTheoremChat(options) {
|
|
|
12
12
|
const [chat] = useState(() => createChatController(options));
|
|
13
13
|
useOptionsEffect(() => chat.update(options));
|
|
14
14
|
const state = useSyncExternalStore(chat.subscribe, chat.getState, chat.getState);
|
|
15
|
-
const
|
|
15
|
+
const optionChoices = useMemo(() => (state.options ? { ...state.options, onChange: chat.pickOption } : undefined), [chat, state.options]);
|
|
16
16
|
// why: The transcript draws each gate by its index; the chat answers the one gate that waits.
|
|
17
17
|
const handleAuthenticated = useCallback((_index, secret) => chat.authenticate(secret), [chat]);
|
|
18
18
|
const handleToolDecision = useCallback((_index, action) => chat.decide(action), [chat]);
|
|
19
19
|
return {
|
|
20
20
|
...state,
|
|
21
|
-
|
|
21
|
+
optionChoices,
|
|
22
22
|
setDraftText: chat.setDraftText,
|
|
23
23
|
setPendingFiles: chat.setPendingFiles,
|
|
24
24
|
setPendingVoice: chat.setPendingVoice,
|
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.759.g46e0e27",
|
|
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.759.g46e0e27",
|
|
64
|
+
"@theoremjs/web": "^0.3.4-next.759.g46e0e27"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@astryxdesign/core": {
|
package/ui/ChatComposerBar.d.ts
CHANGED
|
@@ -2,19 +2,19 @@ import { type ChatComposerInputHandle } from '@astryxdesign/core/Chat';
|
|
|
2
2
|
import { type AttachmentValidationIssue, type ComposerMenuAction, type ComposerPendingMessage, type ComposerProfileInterface, type ComposerRunPhase, type LiveProfileInterface } from '@theoremjs/agents/interface';
|
|
3
3
|
import type { ClientFailure } from '@theoremjs/web/client';
|
|
4
4
|
import { type ReactNode } from 'react';
|
|
5
|
-
/**
|
|
5
|
+
/** An option the profile declares: its allowed values, the one chosen, and how to choose another. */
|
|
6
6
|
/** The effort selector's icon. The studio's Agents list shows the same one. */
|
|
7
7
|
export declare const EFFORT_ICON: import("react").ForwardRefExoticComponent<import("@tabler/icons-react").IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
8
|
-
export type
|
|
8
|
+
export type OptionChoiceView = {
|
|
9
9
|
value: string;
|
|
10
10
|
description?: string;
|
|
11
11
|
};
|
|
12
|
-
export type
|
|
12
|
+
export type OptionDeclView = {
|
|
13
13
|
label?: string;
|
|
14
|
-
choices: readonly
|
|
14
|
+
choices: readonly OptionChoiceView[];
|
|
15
15
|
};
|
|
16
|
-
export type
|
|
17
|
-
declared: Readonly<Record<string,
|
|
16
|
+
export type OptionChoices = {
|
|
17
|
+
declared: Readonly<Record<string, OptionDeclView>>;
|
|
18
18
|
values: Readonly<Record<string, string>>;
|
|
19
19
|
onChange: (name: string, value: string) => void;
|
|
20
20
|
};
|
|
@@ -47,8 +47,8 @@ export type ChatComposerBarProps = {
|
|
|
47
47
|
}) => void;
|
|
48
48
|
/** Stands in for the model and effort selectors, for a bar whose choices are not one chat's. */
|
|
49
49
|
generationSelect?: ReactNode;
|
|
50
|
-
/** The profile's `inputs.
|
|
51
|
-
|
|
50
|
+
/** The profile's `inputs.options` with the value chosen for each; one selector per option, beside the model's. */
|
|
51
|
+
optionChoices?: OptionChoices;
|
|
52
52
|
onPendingMove: (id: string, direction: 'up' | 'down') => void;
|
|
53
53
|
onPendingRemove: (id: string) => void;
|
|
54
54
|
onPendingQueue: (id: string) => void;
|
package/ui/ChatComposerBar.js
CHANGED
|
@@ -24,7 +24,7 @@ import { composerDrawerLabel } from "./labels.js";
|
|
|
24
24
|
import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
|
|
25
25
|
import { VoiceNote } from "./VoiceNote.js";
|
|
26
26
|
const NO_FOCUS_RING = { '--focus-outline-width': '0px' };
|
|
27
|
-
/**
|
|
27
|
+
/** An option the profile declares: its allowed values, the one chosen, and how to choose another. */
|
|
28
28
|
/** The effort selector's icon. The studio's Agents list shows the same one. */
|
|
29
29
|
export const EFFORT_ICON = IconBrain;
|
|
30
30
|
const isImage = (file) => file.type.startsWith('image/');
|
|
@@ -89,7 +89,7 @@ function EffortSelector(props) {
|
|
|
89
89
|
return null;
|
|
90
90
|
return (_jsx(Selector, { label: props.label, isLabelHidden: true, size: "sm", variant: "ghost", startIcon: _jsx(EFFORT_ICON, { size: 14 }), placement: "above", isDisabled: props.isDisabled, value: props.value ?? '', options: props.efforts.map((e) => ({ value: e.alias, label: e.alias, description: e.level })), onChange: props.onChange }));
|
|
91
91
|
}
|
|
92
|
-
function
|
|
92
|
+
function OptionSelectors(props) {
|
|
93
93
|
if (!props.choices)
|
|
94
94
|
return null;
|
|
95
95
|
const { declared, values, onChange } = props.choices;
|
|
@@ -108,15 +108,15 @@ function SlotSelectors(props) {
|
|
|
108
108
|
}
|
|
109
109
|
function GenerationSelect(props) {
|
|
110
110
|
const { models, efforts } = generationOptions(props.iface, props.selectedModel);
|
|
111
|
-
const
|
|
111
|
+
const optionCount = Object.keys(props.optionChoices?.declared ?? {}).length;
|
|
112
112
|
if (props.control)
|
|
113
|
-
return (_jsxs(HStack, { gap: 1, children: [props.control, _jsx(
|
|
114
|
-
if (models.length + efforts.length +
|
|
113
|
+
return (_jsxs(HStack, { gap: 1, children: [props.control, _jsx(OptionSelectors, { choices: props.optionChoices, isDisabled: props.isDisabled })] }));
|
|
114
|
+
if (models.length + efforts.length + optionCount === 0)
|
|
115
115
|
return null;
|
|
116
116
|
return (_jsxs(HStack, { gap: 1, children: [_jsx(ModelSelector, { label: labelText('composer.model', {}, props.iface.labels), models: models, value: props.selectedModel, isDisabled: props.isDisabled, onChange: (modelId) => props.onChange?.({ modelId }) }), _jsx(EffortSelector, { label: labelText('composer.effort', {}, props.iface.labels), efforts: efforts, value: props.selectedEffort, isDisabled: props.isDisabled, onChange: (effort) => {
|
|
117
117
|
if (props.selectedModel)
|
|
118
118
|
props.onChange?.({ modelId: props.selectedModel, effort });
|
|
119
|
-
} }), _jsx(
|
|
119
|
+
} }), _jsx(OptionSelectors, { choices: props.optionChoices, isDisabled: props.isDisabled })] }));
|
|
120
120
|
}
|
|
121
121
|
function composerStatus(args) {
|
|
122
122
|
if (args.failure)
|
|
@@ -330,7 +330,7 @@ function ChatComposerBarBody(props) {
|
|
|
330
330
|
: (props.placeholder ??
|
|
331
331
|
t('@theorem.composer.placeholder', { handle: iface.identity.handle })),
|
|
332
332
|
// why: A profile that takes no text has no text field (false, not null: null gets Astryx's default one).
|
|
333
|
-
input: inputs.text && (_jsx(ComposerTextInput, { editorRef: editorRef, handleRef: props.inputRef, onStash: canStash ? () => props.onMenuAction('stash') : undefined })), drawer: composerDrawer(props, stagedFiles, () => voice.discardRecordingOrVoice()), headerActions: inputs.attachments ? (_jsx(AttachFilesButton, { accept: inputs.attachments.acceptAttr, onFiles: stageFiles })) : undefined, headerContext: hint ? _jsx(ComposerHint, { hint: hint, onAction: () => props.onMenuAction('stash') }) : undefined, footerActions: _jsx(GenerationSelect, { iface: iface,
|
|
333
|
+
input: inputs.text && (_jsx(ComposerTextInput, { editorRef: editorRef, handleRef: props.inputRef, onStash: canStash ? () => props.onMenuAction('stash') : undefined })), drawer: composerDrawer(props, stagedFiles, () => voice.discardRecordingOrVoice()), headerActions: inputs.attachments ? (_jsx(AttachFilesButton, { accept: inputs.attachments.acceptAttr, onFiles: stageFiles })) : undefined, headerContext: hint ? _jsx(ComposerHint, { hint: hint, onAction: () => props.onMenuAction('stash') }) : undefined, footerActions: _jsx(GenerationSelect, { iface: iface, optionChoices: props.optionChoices, selectedModel: props.selectedModel, selectedEffort: props.selectedEffort, isDisabled: phase === 'streaming', onChange: props.onGenerationChange, control: props.generationSelect }),
|
|
334
334
|
// why: Astryx: sendActions render to the left of the send button, at size="md".
|
|
335
335
|
sendActions: _jsxs(_Fragment, { children: [_jsx(SendMenu, { actions: menuActions, onAction: props.onMenuAction }), _jsx(ComposerRecordButton, { hasVoice: Boolean(inputs.voice), voice: voice })] }), sendButton: _jsx(ChatSendButton, { isStopShown: primary === 'stop', isDisabled: primaryDisabled, onSend: runPrimary, onStop: props.onStop }), status: composerStatus({
|
|
336
336
|
failure: props.failure,
|
package/ui/TheoremChat.d.ts
CHANGED
|
@@ -28,13 +28,13 @@ export type TheoremChatProps = {
|
|
|
28
28
|
*/
|
|
29
29
|
detectCodeLanguage?: boolean;
|
|
30
30
|
/**
|
|
31
|
-
* The value chosen for each of the profile's `inputs.
|
|
31
|
+
* The value chosen for each of the profile's `inputs.options`, sent with every turn. An option you
|
|
32
32
|
* name is fixed to that value; one you leave out gets a selector in the composer bar, beside
|
|
33
33
|
* the model's, starting at its first allowed value.
|
|
34
34
|
*/
|
|
35
|
-
|
|
36
|
-
/** Told each time the visitor chooses
|
|
37
|
-
|
|
35
|
+
options?: Record<string, string>;
|
|
36
|
+
/** Told each time the visitor chooses an option in the composer, with every option's value. */
|
|
37
|
+
onOptionsChange?: (options: Record<string, string>) => void;
|
|
38
38
|
/** Which prompt lines each turn gets, for a host that lets its page choose (`pageStatesTurn`). */
|
|
39
39
|
stated?: StatedTurn;
|
|
40
40
|
/**
|
package/ui/TheoremChat.js
CHANGED
|
@@ -41,7 +41,7 @@ function ChatComposerForChat({ chat, iface, placeholder, inputRef, }) {
|
|
|
41
41
|
chat.setIssues([]);
|
|
42
42
|
}, onVoiceClear: () => chat.setPendingVoice([]), onSubmit: () => {
|
|
43
43
|
void chat.handleSubmit();
|
|
44
|
-
}, onStop: chat.handleStop, onMenuAction: chat.handleMenuAction, onGenerationChange: chat.handleGenerationChange,
|
|
44
|
+
}, onStop: chat.handleStop, onMenuAction: chat.handleMenuAction, onGenerationChange: chat.handleGenerationChange, optionChoices: chat.optionChoices, onPendingMove: chat.handlePendingMove, onPendingRemove: (id) => {
|
|
45
45
|
chat.setPendingMessages((prev) => removeComposerPendingMessage(prev, id));
|
|
46
46
|
}, onPendingQueue: chat.handlePendingQueue, onPendingRestore: (id) => {
|
|
47
47
|
void chat.handlePendingRestore(id);
|
|
@@ -51,7 +51,7 @@ function ChatComposerForChat({ chat, iface, placeholder, inputRef, }) {
|
|
|
51
51
|
void chat.handleSendNow(message);
|
|
52
52
|
} }));
|
|
53
53
|
}
|
|
54
|
-
function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth = DEFAULT_CHAT_MAX_WIDTH, scrollRef, trace, usage, className, style, conversation, conversationView, initialText, initialFiles, initialVoice, initialPending, onConversationChange, chatRef, composer: hasComposer = true,
|
|
54
|
+
function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth = DEFAULT_CHAT_MAX_WIDTH, scrollRef, trace, usage, className, style, conversation, conversationView, initialText, initialFiles, initialVoice, initialPending, onConversationChange, chatRef, composer: hasComposer = true, options, onOptionsChange, stated, context, pageTools, onTurnEvent, }) {
|
|
55
55
|
const t = useLabels();
|
|
56
56
|
const chat = useTheoremChat({
|
|
57
57
|
transport,
|
|
@@ -63,8 +63,8 @@ function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth
|
|
|
63
63
|
initialVoice,
|
|
64
64
|
initialPending,
|
|
65
65
|
onConversationChange,
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
options,
|
|
67
|
+
onOptionsChange,
|
|
68
68
|
stated,
|
|
69
69
|
context,
|
|
70
70
|
pageTools,
|