@theoremjs/react 0.3.4-next.373.gab46e23 → 0.3.4-next.377.g4dbec64
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 +2 -0
- package/hooks/use-page-tool-warnings.d.ts +1 -4
- package/hooks/use-page-tool-warnings.js +3 -14
- package/hooks/use-theorem-chat.d.ts +35 -80
- package/hooks/use-theorem-chat.js +30 -517
- package/package.json +3 -3
- package/ui/ChatComposerBar.js +1 -3
- package/ui/labels.d.ts +1 -2
- package/client/composer-drawer.d.ts +0 -19
- package/client/composer-drawer.js +0 -15
- package/client/composer-hints.d.ts +0 -25
- package/client/composer-hints.js +0 -20
- package/client/composer-primary.d.ts +0 -16
- package/client/composer-primary.js +0 -25
- package/hooks/use-theorem-chat-actions.d.ts +0 -77
- package/hooks/use-theorem-chat-actions.js +0 -285
- package/hooks/use-theorem-chat-state.d.ts +0 -65
- package/hooks/use-theorem-chat-state.js +0 -122
package/client/composer-hints.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Composer hints — ephemeral guidance about the current composer interaction
|
|
3
|
-
* (not pending-message state). Mirrors seance's `stash-selected-draft` hint.
|
|
4
|
-
*/
|
|
5
|
-
/** Stash shortcut in Astryx `Kbd` notation (`mod` is ⌘ on macOS, Ctrl elsewhere). */
|
|
6
|
-
export const STASH_SHORTCUT = 'mod+shift+s';
|
|
7
|
-
/** Whether a keydown is the stash shortcut (⌘⇧S / Ctrl+Shift+S). */
|
|
8
|
-
export function isStashShortcut(event) {
|
|
9
|
-
return (event.code === 'KeyS' && event.shiftKey && !event.altKey && (event.metaKey || event.ctrlKey));
|
|
10
|
-
}
|
|
11
|
-
/** The stash hint — shown while the whole draft is selected in the focused input and it can be stashed. */
|
|
12
|
-
export function resolveComposerHint(args) {
|
|
13
|
-
const draft = args.draftText.trim();
|
|
14
|
-
if (!args.canStash || !draft || args.selectedText.trim() !== draft)
|
|
15
|
-
return null;
|
|
16
|
-
return {
|
|
17
|
-
id: 'stash-selected-draft',
|
|
18
|
-
shortcut: STASH_SHORTCUT,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { type ComposerMenuAction, type ComposerPrimaryAction, type ComposerProfileInterface, type ComposerRunPhase } from '@theoremjs/agents/interface';
|
|
2
|
-
export type ComposerActionState = {
|
|
3
|
-
primary: ComposerPrimaryAction;
|
|
4
|
-
menuActions: ComposerMenuAction[];
|
|
5
|
-
/** Send is off while recording, with nothing to act on, or with an empty draft to send or queue. */
|
|
6
|
-
primaryDisabled: boolean;
|
|
7
|
-
};
|
|
8
|
-
/** The composer's primary button and send menu for the current draft and run phase. */
|
|
9
|
-
export declare function composerActionState(args: {
|
|
10
|
-
iface: ComposerProfileInterface;
|
|
11
|
-
phase: ComposerRunPhase;
|
|
12
|
-
draftText: string;
|
|
13
|
-
pendingFiles: readonly File[];
|
|
14
|
-
pendingVoice: readonly File[];
|
|
15
|
-
recording: boolean;
|
|
16
|
-
}): ComposerActionState;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { resolveComposerMenuActions, resolveComposerPrimary, userDraftHasPayload, } from '@theoremjs/agents/interface';
|
|
2
|
-
function fileSpecs(files) {
|
|
3
|
-
return files.map((file) => ({
|
|
4
|
-
name: file.name,
|
|
5
|
-
mimeType: file.type || 'application/octet-stream',
|
|
6
|
-
sizeBytes: file.size,
|
|
7
|
-
}));
|
|
8
|
-
}
|
|
9
|
-
/** The composer's primary button and send menu for the current draft and run phase. */
|
|
10
|
-
export function composerActionState(args) {
|
|
11
|
-
const allowSteering = 'allowSteering' in args.iface && Boolean(args.iface.allowSteering);
|
|
12
|
-
const hasPayload = userDraftHasPayload({
|
|
13
|
-
...(args.draftText.trim() ? { text: args.draftText } : {}),
|
|
14
|
-
...(args.pendingFiles.length ? { attachments: fileSpecs(args.pendingFiles) } : {}),
|
|
15
|
-
...(args.pendingVoice.length ? { voice: fileSpecs(args.pendingVoice) } : {}),
|
|
16
|
-
});
|
|
17
|
-
const context = { phase: args.phase, hasPayload, allowSteering };
|
|
18
|
-
const primary = resolveComposerPrimary(context);
|
|
19
|
-
const needsPayload = primary === 'send' || primary === 'queue';
|
|
20
|
-
return {
|
|
21
|
-
primary,
|
|
22
|
-
menuActions: resolveComposerMenuActions(context),
|
|
23
|
-
primaryDisabled: args.recording || primary === 'none' || (needsPayload && !hasPayload),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { AttachmentValidationIssue, ComposerMenuAction, ComposerPendingMessage, ComposerProfileInterface, ComposerRunPhase, InterfaceTurnSession, TranscriptBlock } from '@theoremjs/agents/interface';
|
|
2
|
-
import type { RunOutcome, TheoremTransport } from '@theoremjs/web/client';
|
|
3
|
-
import { type AnsweringGate, type ClientFailure, type StreamView, type ToolDecisionAction, type ToolGateResolution } from '@theoremjs/web/client';
|
|
4
|
-
import { type MutableRefObject } from 'react';
|
|
5
|
-
import type { MessageDelivery } from './use-theorem-chat-state.ts';
|
|
6
|
-
export type RunTurnStream = (run: (view: StreamView) => Promise<RunOutcome>, options?: {
|
|
7
|
-
/** The run sends a message: one that never posts goes back in the composer with its failure. */
|
|
8
|
-
message?: {
|
|
9
|
-
restore(): Promise<void>;
|
|
10
|
-
};
|
|
11
|
-
}) => Promise<void>;
|
|
12
|
-
export type TheoremChatActionArgs = {
|
|
13
|
-
iface: ComposerProfileInterface | null;
|
|
14
|
-
transport: TheoremTransport;
|
|
15
|
-
phase: ComposerRunPhase;
|
|
16
|
-
gated: boolean;
|
|
17
|
-
draftText: string;
|
|
18
|
-
pendingFiles: File[];
|
|
19
|
-
pendingVoice: File[];
|
|
20
|
-
/** The composer as it is now, for a run that ends after it changed. */
|
|
21
|
-
composerRef: MutableRefObject<{
|
|
22
|
-
draftText: string;
|
|
23
|
-
pendingFiles: File[];
|
|
24
|
-
pendingVoice: File[];
|
|
25
|
-
}>;
|
|
26
|
-
/** The ref of the conversation the chat continues; none before its first message posts. */
|
|
27
|
-
conversationRef: MutableRefObject<string | undefined>;
|
|
28
|
-
clearComposer: () => void;
|
|
29
|
-
runTurnStream: RunTurnStream;
|
|
30
|
-
sessionRef: MutableRefObject<InterfaceTurnSession>;
|
|
31
|
-
blocksRef: MutableRefObject<TranscriptBlock[]>;
|
|
32
|
-
abortRef: MutableRefObject<AbortController | null>;
|
|
33
|
-
turnIdRef: MutableRefObject<string | null>;
|
|
34
|
-
busyRef: MutableRefObject<boolean>;
|
|
35
|
-
runPromiseRef: MutableRefObject<Promise<void> | null>;
|
|
36
|
-
allowQueueDrainRef: MutableRefObject<boolean>;
|
|
37
|
-
setBlocks: (value: TranscriptBlock[] | ((prev: TranscriptBlock[]) => TranscriptBlock[])) => void;
|
|
38
|
-
setStreamBlocks: (value: TranscriptBlock[]) => void;
|
|
39
|
-
setSession: (value: InterfaceTurnSession) => void;
|
|
40
|
-
setChatStarted: (value: boolean) => void;
|
|
41
|
-
setStreaming: (value: boolean) => void;
|
|
42
|
-
setPendingMessages: (value: ComposerPendingMessage[] | ((prev: ComposerPendingMessage[]) => ComposerPendingMessage[])) => void;
|
|
43
|
-
setDraftText: (value: string) => void;
|
|
44
|
-
setPendingFiles: (value: File[]) => void;
|
|
45
|
-
setPendingVoice: (value: File[]) => void;
|
|
46
|
-
setIssues: (value: AttachmentValidationIssue[]) => void;
|
|
47
|
-
setFailure: (value: ClientFailure | null) => void;
|
|
48
|
-
setAnswering: (value: AnsweringGate | null) => void;
|
|
49
|
-
setDelivery: (value: {
|
|
50
|
-
status: MessageDelivery;
|
|
51
|
-
} | null) => void;
|
|
52
|
-
pendingRef: MutableRefObject<ComposerPendingMessage[]>;
|
|
53
|
-
};
|
|
54
|
-
type ComposerFields = {
|
|
55
|
-
text: string;
|
|
56
|
-
files: File[];
|
|
57
|
-
voice: File[];
|
|
58
|
-
};
|
|
59
|
-
export declare function useTheoremChatActions(args: TheoremChatActionArgs): {
|
|
60
|
-
startTurnFromFields: (fields: ComposerFields) => Promise<void>;
|
|
61
|
-
startTurnFromDraft: (draft: ComposerPendingMessage["draft"], options?: {
|
|
62
|
-
/** Send while the reply waits on gates: the message walks away from them. */
|
|
63
|
-
walkAway?: boolean;
|
|
64
|
-
/** Once the message posts: it leaves wherever it was waiting to be sent. */
|
|
65
|
-
onPosted?: () => void;
|
|
66
|
-
}) => Promise<void>;
|
|
67
|
-
handleStop: () => void;
|
|
68
|
-
handleSubmit: () => Promise<void>;
|
|
69
|
-
handleSendNow: (draftSource?: ComposerPendingMessage) => Promise<void>;
|
|
70
|
-
handleMenuAction: (action: ComposerMenuAction) => void;
|
|
71
|
-
handleToolDecision: (_index: number, action: ToolDecisionAction) => Promise<void>;
|
|
72
|
-
handleAuthenticated: (_index: number, secret?: string) => Promise<void>;
|
|
73
|
-
resumeGatedTool: (resolution: ToolGateResolution) => Promise<void>;
|
|
74
|
-
handlePendingRestore: (id: string) => Promise<void>;
|
|
75
|
-
enqueuePending: (kind: "queue" | "steer" | "stash") => Promise<void>;
|
|
76
|
-
};
|
|
77
|
-
export {};
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { TheoremError } from '@theoremjs/agents';
|
|
2
|
-
import { convertSteersToFrontQueued, createComposerPendingMessage, orderComposerPendingMessages, removeComposerPendingMessage, userDraftHasPayload, userDraftToSteerInject, } from '@theoremjs/agents/interface';
|
|
3
|
-
import { clientFailure, composerFieldsFromDraft, encodeComposerDraft, issuedOperationId, resumeInterfaceTool, streamInterfaceDraftTurn, streamInterfaceTurn, } from '@theoremjs/web/client';
|
|
4
|
-
import { useCallback } from 'react';
|
|
5
|
-
function composerFieldsPayload(text, pendingFiles, pendingVoice) {
|
|
6
|
-
const meta = (files) => files.map((f) => ({
|
|
7
|
-
name: f.name,
|
|
8
|
-
mimeType: f.type || 'application/octet-stream',
|
|
9
|
-
sizeBytes: f.size,
|
|
10
|
-
}));
|
|
11
|
-
return {
|
|
12
|
-
...(text.trim() ? { text } : {}),
|
|
13
|
-
...(pendingFiles.length ? { attachments: meta(pendingFiles) } : {}),
|
|
14
|
-
...(pendingVoice.length ? { voice: meta(pendingVoice) } : {}),
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function newTurnId() {
|
|
18
|
-
return typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
19
|
-
? crypto.randomUUID()
|
|
20
|
-
: `turn-${Date.now()}`;
|
|
21
|
-
}
|
|
22
|
-
function beginAbortableTurn(args) {
|
|
23
|
-
if (!args.iface)
|
|
24
|
-
return null;
|
|
25
|
-
const controller = new AbortController();
|
|
26
|
-
args.abortRef.current = controller;
|
|
27
|
-
const turnId = newTurnId();
|
|
28
|
-
args.turnIdRef.current = turnId;
|
|
29
|
-
return {
|
|
30
|
-
composer: args.iface,
|
|
31
|
-
turnId,
|
|
32
|
-
signal: controller.signal,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
/** The run's scope: the conversation the chat continues, and the run's own turn. */
|
|
36
|
-
function scopeOf(args, turnId) {
|
|
37
|
-
return { conversation: args.conversationRef.current, turnId };
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Puts a message back in the composer. What the composer holds now is
|
|
41
|
-
* stashed first, so nothing the user typed is lost.
|
|
42
|
-
*/
|
|
43
|
-
function useRestoreToComposer(args) {
|
|
44
|
-
return useCallback(async (fields) => {
|
|
45
|
-
const { draftText, pendingFiles, pendingVoice } = args.composerRef.current;
|
|
46
|
-
if (userDraftHasPayload(composerFieldsPayload(draftText, pendingFiles, pendingVoice))) {
|
|
47
|
-
const draft = await encodeComposerDraft({ text: draftText, pendingFiles, pendingVoice });
|
|
48
|
-
const stash = createComposerPendingMessage({ kind: 'stash', draft });
|
|
49
|
-
args.setPendingMessages((prev) => orderComposerPendingMessages([...prev, stash]));
|
|
50
|
-
}
|
|
51
|
-
args.setDraftText(fields.text);
|
|
52
|
-
args.setPendingFiles(fields.files);
|
|
53
|
-
args.setPendingVoice(fields.voice);
|
|
54
|
-
args.setIssues([]);
|
|
55
|
-
}, [args]);
|
|
56
|
-
}
|
|
57
|
-
/** Starts a turn from the composer's fields, or from an encoded draft (queued or send-now). */
|
|
58
|
-
function useTurnStarters(args, restoreToComposer) {
|
|
59
|
-
const startTurnFromFields = useCallback(async (fields) => {
|
|
60
|
-
const started = beginAbortableTurn(args);
|
|
61
|
-
if (!started)
|
|
62
|
-
return;
|
|
63
|
-
await args.runTurnStream((view) => streamInterfaceTurn({
|
|
64
|
-
iface: started.composer,
|
|
65
|
-
transport: args.transport,
|
|
66
|
-
session: args.sessionRef.current,
|
|
67
|
-
text: fields.text,
|
|
68
|
-
pendingFiles: fields.files,
|
|
69
|
-
pendingVoice: fields.voice,
|
|
70
|
-
signal: started.signal,
|
|
71
|
-
scope: scopeOf(args, started.turnId),
|
|
72
|
-
view,
|
|
73
|
-
transcript: args.blocksRef.current,
|
|
74
|
-
onUserBlocks: (userBlocks) => {
|
|
75
|
-
args.setBlocks((prev) => [...prev, ...userBlocks]);
|
|
76
|
-
args.setDelivery({ status: 'sending' });
|
|
77
|
-
args.setChatStarted(true);
|
|
78
|
-
args.setStreaming(true);
|
|
79
|
-
args.clearComposer();
|
|
80
|
-
},
|
|
81
|
-
}), { message: { restore: () => restoreToComposer(fields) } });
|
|
82
|
-
}, [args, restoreToComposer]);
|
|
83
|
-
const startTurnFromDraft = useCallback(async (draft, options = {}) => {
|
|
84
|
-
const started = beginAbortableTurn(args);
|
|
85
|
-
if (!started)
|
|
86
|
-
return;
|
|
87
|
-
await args.runTurnStream((view) => streamInterfaceDraftTurn({
|
|
88
|
-
iface: started.composer,
|
|
89
|
-
transport: args.transport,
|
|
90
|
-
session: args.sessionRef.current,
|
|
91
|
-
draft,
|
|
92
|
-
signal: started.signal,
|
|
93
|
-
scope: scopeOf(args, started.turnId),
|
|
94
|
-
view,
|
|
95
|
-
transcript: args.blocksRef.current,
|
|
96
|
-
walkAway: options.walkAway === true,
|
|
97
|
-
onUserBlocks: (posted) => {
|
|
98
|
-
args.setBlocks((prev) => [...prev, ...posted]);
|
|
99
|
-
args.setStreamBlocks([]);
|
|
100
|
-
args.setDelivery({ status: 'sending' });
|
|
101
|
-
args.setChatStarted(true);
|
|
102
|
-
args.setStreaming(true);
|
|
103
|
-
options.onPosted?.();
|
|
104
|
-
},
|
|
105
|
-
}), { message: { restore: () => restoreToComposer(composerFieldsFromDraft(draft)) } });
|
|
106
|
-
}, [args, restoreToComposer]);
|
|
107
|
-
return { startTurnFromFields, startTurnFromDraft };
|
|
108
|
-
}
|
|
109
|
-
/** A steer with no running turn: the user reads `session.turn_ended`; queued steers move to the front. */
|
|
110
|
-
function steerAfterTurnEnded(args, messageId) {
|
|
111
|
-
args.setFailure(clientFailure(
|
|
112
|
-
// labels-exempt: internal diagnostic; the user reads session.turn_ended
|
|
113
|
-
new TheoremError('request', 'steer: no active turn', { copy: { key: 'session.turn_ended' } }), args.iface?.labels));
|
|
114
|
-
args.setPendingMessages((prev) => orderComposerPendingMessages(convertSteersToFrontQueued(removeComposerPendingMessage(prev, messageId))));
|
|
115
|
-
}
|
|
116
|
-
/** Queue / steer / stash the composer draft, and restore a pending message into the composer. */
|
|
117
|
-
function usePendingActions(args, restoreToComposer) {
|
|
118
|
-
const enqueuePending = useCallback(async (kind) => {
|
|
119
|
-
if (!userDraftHasPayload(composerFieldsPayload(args.draftText, args.pendingFiles, args.pendingVoice))) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
args.setIssues([]);
|
|
123
|
-
try {
|
|
124
|
-
const draft = await encodeComposerDraft({
|
|
125
|
-
text: args.draftText,
|
|
126
|
-
pendingFiles: args.pendingFiles,
|
|
127
|
-
pendingVoice: args.pendingVoice,
|
|
128
|
-
});
|
|
129
|
-
const message = createComposerPendingMessage({ kind, draft });
|
|
130
|
-
args.setPendingMessages((prev) => orderComposerPendingMessages([...prev, message]));
|
|
131
|
-
args.clearComposer();
|
|
132
|
-
if (kind !== 'steer')
|
|
133
|
-
return;
|
|
134
|
-
const turnId = args.turnIdRef.current;
|
|
135
|
-
if (!turnId) {
|
|
136
|
-
steerAfterTurnEnded(args, message.id);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const inject = userDraftToSteerInject(draft);
|
|
140
|
-
if (inject.length === 0)
|
|
141
|
-
return;
|
|
142
|
-
await args.transport.steer({ turnId, id: message.id, inject });
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
args.setFailure(clientFailure(err, args.iface?.labels));
|
|
146
|
-
}
|
|
147
|
-
}, [args]);
|
|
148
|
-
const handlePendingRestore = useCallback(async (id) => {
|
|
149
|
-
const message = args.pendingRef.current.find((m) => m.id === id);
|
|
150
|
-
if (!message)
|
|
151
|
-
return;
|
|
152
|
-
try {
|
|
153
|
-
await restoreToComposer(composerFieldsFromDraft(message.draft));
|
|
154
|
-
args.setPendingMessages((prev) => removeComposerPendingMessage(prev, id));
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
args.setFailure(clientFailure(err, args.iface?.labels));
|
|
158
|
-
}
|
|
159
|
-
}, [args, restoreToComposer]);
|
|
160
|
-
return { enqueuePending, handlePendingRestore };
|
|
161
|
-
}
|
|
162
|
-
/** Answer the gate the session waits on: a decision, or a sign-in. */
|
|
163
|
-
function useGateActions(args) {
|
|
164
|
-
const resumeGatedTool = useCallback(async (resolution) => {
|
|
165
|
-
const composer = args.iface;
|
|
166
|
-
if (!composer)
|
|
167
|
-
return;
|
|
168
|
-
// why: Set once the run starts (not while another runs), and cleared once it has ended.
|
|
169
|
-
let started = false;
|
|
170
|
-
try {
|
|
171
|
-
await args.runTurnStream((view) => {
|
|
172
|
-
started = true;
|
|
173
|
-
const session = args.sessionRef.current;
|
|
174
|
-
// why: A person's answer shows on its gate while it settles; the page's has no gate to show on.
|
|
175
|
-
if (session.gatedTool && resolution.action !== 'page')
|
|
176
|
-
args.setAnswering({
|
|
177
|
-
callId: session.gatedTool.callId,
|
|
178
|
-
operationId: issuedOperationId(session.gatedTool),
|
|
179
|
-
action: resolution.action,
|
|
180
|
-
});
|
|
181
|
-
return resumeInterfaceTool({
|
|
182
|
-
iface: composer,
|
|
183
|
-
transport: args.transport,
|
|
184
|
-
session,
|
|
185
|
-
scope: scopeOf(args),
|
|
186
|
-
resolution,
|
|
187
|
-
view,
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
finally {
|
|
192
|
-
if (started)
|
|
193
|
-
args.setAnswering(null);
|
|
194
|
-
}
|
|
195
|
-
}, [args]);
|
|
196
|
-
const handleToolDecision = useCallback(async (_index, action) => {
|
|
197
|
-
await resumeGatedTool({ action });
|
|
198
|
-
}, [resumeGatedTool]);
|
|
199
|
-
const handleAuthenticated = useCallback(async (_index, secret) => {
|
|
200
|
-
await resumeGatedTool({ action: 'auth', ...(secret === undefined ? {} : { secret }) });
|
|
201
|
-
}, [resumeGatedTool]);
|
|
202
|
-
return { handleToolDecision, handleAuthenticated, resumeGatedTool };
|
|
203
|
-
}
|
|
204
|
-
export function useTheoremChatActions(args) {
|
|
205
|
-
const restoreToComposer = useRestoreToComposer(args);
|
|
206
|
-
const { startTurnFromFields, startTurnFromDraft } = useTurnStarters(args, restoreToComposer);
|
|
207
|
-
const { enqueuePending, handlePendingRestore } = usePendingActions(args, restoreToComposer);
|
|
208
|
-
const { handleToolDecision, handleAuthenticated, resumeGatedTool } = useGateActions(args);
|
|
209
|
-
const handleStop = useCallback(() => {
|
|
210
|
-
args.abortRef.current?.abort();
|
|
211
|
-
}, [args]);
|
|
212
|
-
const handleSubmit = useCallback(async () => {
|
|
213
|
-
if (args.phase === 'streaming' || args.phase === 'gated') {
|
|
214
|
-
await enqueuePending('queue');
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (args.gated)
|
|
218
|
-
return;
|
|
219
|
-
args.setIssues([]);
|
|
220
|
-
await startTurnFromFields({
|
|
221
|
-
text: args.draftText,
|
|
222
|
-
files: [...args.pendingFiles],
|
|
223
|
-
voice: [...args.pendingVoice],
|
|
224
|
-
});
|
|
225
|
-
}, [args, enqueuePending, startTurnFromFields]);
|
|
226
|
-
/** The queued message's draft, or the composer's when it holds something to send. */
|
|
227
|
-
const draftToSend = useCallback(async (draftSource) => {
|
|
228
|
-
if (draftSource)
|
|
229
|
-
return draftSource.draft;
|
|
230
|
-
const draft = await encodeComposerDraft({
|
|
231
|
-
text: args.draftText,
|
|
232
|
-
pendingFiles: args.pendingFiles,
|
|
233
|
-
pendingVoice: args.pendingVoice,
|
|
234
|
-
});
|
|
235
|
-
return userDraftHasPayload(draft) ? draft : undefined;
|
|
236
|
-
}, [args]);
|
|
237
|
-
/** The sent message leaves the queue, or the composer it came from. */
|
|
238
|
-
const releaseDraft = useCallback((draftSource) => {
|
|
239
|
-
if (!draftSource) {
|
|
240
|
-
args.clearComposer();
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
const pendingId = draftSource.id;
|
|
244
|
-
args.setPendingMessages((prev) => removeComposerPendingMessage(prev, pendingId));
|
|
245
|
-
}, [args]);
|
|
246
|
-
const handleSendNow = useCallback(async (draftSource) => {
|
|
247
|
-
if (!args.iface)
|
|
248
|
-
return;
|
|
249
|
-
const draft = await draftToSend(draftSource);
|
|
250
|
-
if (!draft)
|
|
251
|
-
return;
|
|
252
|
-
if (args.busyRef.current) {
|
|
253
|
-
args.abortRef.current?.abort();
|
|
254
|
-
await args.runPromiseRef.current;
|
|
255
|
-
}
|
|
256
|
-
args.setPendingMessages((prev) => convertSteersToFrontQueued(prev));
|
|
257
|
-
args.allowQueueDrainRef.current = false;
|
|
258
|
-
// why: Sent while the reply waits on gates, the message walks away from them in its own request.
|
|
259
|
-
await startTurnFromDraft(draft, {
|
|
260
|
-
walkAway: args.sessionRef.current.gatedTool !== null,
|
|
261
|
-
onPosted: () => releaseDraft(draftSource),
|
|
262
|
-
});
|
|
263
|
-
}, [args, draftToSend, releaseDraft, startTurnFromDraft]);
|
|
264
|
-
const handleMenuAction = useCallback((action) => {
|
|
265
|
-
if (action === 'queue' || action === 'steer' || action === 'stash') {
|
|
266
|
-
void enqueuePending(action);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
if (action === 'send_now')
|
|
270
|
-
void handleSendNow();
|
|
271
|
-
}, [enqueuePending, handleSendNow]);
|
|
272
|
-
return {
|
|
273
|
-
startTurnFromFields,
|
|
274
|
-
startTurnFromDraft,
|
|
275
|
-
handleStop,
|
|
276
|
-
handleSubmit,
|
|
277
|
-
handleSendNow,
|
|
278
|
-
handleMenuAction,
|
|
279
|
-
handleToolDecision,
|
|
280
|
-
handleAuthenticated,
|
|
281
|
-
resumeGatedTool,
|
|
282
|
-
handlePendingRestore,
|
|
283
|
-
enqueuePending,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { type AttachmentValidationIssue, type ComposerPendingMessage, type InterfaceTurnSession, type TranscriptBlock } from '@theoremjs/agents/interface';
|
|
2
|
-
import type { AnsweringGate, ClientFailure } from '@theoremjs/web/client';
|
|
3
|
-
type SetSession = (value: InterfaceTurnSession | ((prev: InterfaceTurnSession) => InterfaceTurnSession)) => void;
|
|
4
|
-
export type { SetSession };
|
|
5
|
-
/**
|
|
6
|
-
* How far the latest message has got: posted (`sending`), taken by the server
|
|
7
|
-
* (`sent`), reached the model (`delivered`), answered (`read`).
|
|
8
|
-
*/
|
|
9
|
-
export type MessageDelivery = 'sending' | 'sent' | 'delivered' | 'read';
|
|
10
|
-
/** Composer / transcript / session state behind {@link useTheoremChat}. */
|
|
11
|
-
export declare function useTheoremChatState(initialText?: string): {
|
|
12
|
-
blocks: TranscriptBlock[];
|
|
13
|
-
setBlocks: (value: TranscriptBlock[] | ((prev: TranscriptBlock[]) => TranscriptBlock[])) => void;
|
|
14
|
-
streamBlocks: TranscriptBlock[];
|
|
15
|
-
setStreamBlocks: import("react").Dispatch<import("react").SetStateAction<TranscriptBlock[]>>;
|
|
16
|
-
streamBlocksRef: import("react").RefObject<TranscriptBlock[]>;
|
|
17
|
-
draftText: string;
|
|
18
|
-
setDraftText: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
19
|
-
pendingFiles: File[];
|
|
20
|
-
setPendingFiles: import("react").Dispatch<import("react").SetStateAction<File[]>>;
|
|
21
|
-
pendingVoice: File[];
|
|
22
|
-
setPendingVoice: import("react").Dispatch<import("react").SetStateAction<File[]>>;
|
|
23
|
-
pendingMessages: ComposerPendingMessage[];
|
|
24
|
-
setPendingMessages: import("react").Dispatch<import("react").SetStateAction<ComposerPendingMessage[]>>;
|
|
25
|
-
issues: AttachmentValidationIssue[];
|
|
26
|
-
setIssues: import("react").Dispatch<import("react").SetStateAction<AttachmentValidationIssue[]>>;
|
|
27
|
-
failure: ClientFailure | null;
|
|
28
|
-
setFailure: import("react").Dispatch<import("react").SetStateAction<ClientFailure | null>>;
|
|
29
|
-
answering: AnsweringGate | null;
|
|
30
|
-
setAnswering: import("react").Dispatch<import("react").SetStateAction<AnsweringGate | null>>;
|
|
31
|
-
busy: boolean;
|
|
32
|
-
setBusy: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
33
|
-
chatStarted: boolean;
|
|
34
|
-
setChatStarted: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
35
|
-
streaming: boolean;
|
|
36
|
-
setStreaming: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
37
|
-
session: InterfaceTurnSession;
|
|
38
|
-
setSession: import("react").Dispatch<import("react").SetStateAction<InterfaceTurnSession>>;
|
|
39
|
-
delivery: {
|
|
40
|
-
status: MessageDelivery;
|
|
41
|
-
} | null;
|
|
42
|
-
setDelivery: (next: {
|
|
43
|
-
status: MessageDelivery;
|
|
44
|
-
} | null) => void;
|
|
45
|
-
deliveryRef: import("react").RefObject<{
|
|
46
|
-
status: MessageDelivery;
|
|
47
|
-
} | null>;
|
|
48
|
-
composerRef: import("react").RefObject<{
|
|
49
|
-
draftText: string;
|
|
50
|
-
pendingFiles: File[];
|
|
51
|
-
pendingVoice: File[];
|
|
52
|
-
}>;
|
|
53
|
-
blocksRef: import("react").RefObject<TranscriptBlock[]>;
|
|
54
|
-
busyRef: import("react").RefObject<boolean>;
|
|
55
|
-
abortRef: import("react").RefObject<AbortController | null>;
|
|
56
|
-
turnIdRef: import("react").RefObject<string | null>;
|
|
57
|
-
pendingRef: import("react").RefObject<ComposerPendingMessage[]>;
|
|
58
|
-
sessionRef: import("react").RefObject<InterfaceTurnSession>;
|
|
59
|
-
drainLockRef: import("react").RefObject<boolean>;
|
|
60
|
-
allowQueueDrainRef: import("react").RefObject<boolean>;
|
|
61
|
-
runPromiseRef: import("react").RefObject<Promise<void> | null>;
|
|
62
|
-
cancelPendingStreamFrame: () => void;
|
|
63
|
-
scheduleStreamBlocks: (partial: TranscriptBlock[]) => void;
|
|
64
|
-
clearComposer: () => void;
|
|
65
|
-
};
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { emptyInterfaceTurnSession, } from '@theoremjs/agents/interface';
|
|
2
|
-
import { useCallback, useRef, useState } from 'react';
|
|
3
|
-
/** Coalesces streamed partial blocks to one state update per animation frame. */
|
|
4
|
-
function useStreamFrames(setStreamBlocks) {
|
|
5
|
-
const streamRafRef = useRef(null);
|
|
6
|
-
const pendingStreamRef = useRef(null);
|
|
7
|
-
const cancelPendingStreamFrame = useCallback(() => {
|
|
8
|
-
if (streamRafRef.current != null) {
|
|
9
|
-
cancelAnimationFrame(streamRafRef.current);
|
|
10
|
-
streamRafRef.current = null;
|
|
11
|
-
}
|
|
12
|
-
pendingStreamRef.current = null;
|
|
13
|
-
}, []);
|
|
14
|
-
const scheduleStreamBlocks = useCallback((partial) => {
|
|
15
|
-
pendingStreamRef.current = partial;
|
|
16
|
-
if (streamRafRef.current != null)
|
|
17
|
-
return;
|
|
18
|
-
streamRafRef.current = requestAnimationFrame(() => {
|
|
19
|
-
streamRafRef.current = null;
|
|
20
|
-
const next = pendingStreamRef.current;
|
|
21
|
-
pendingStreamRef.current = null;
|
|
22
|
-
if (next)
|
|
23
|
-
setStreamBlocks(next);
|
|
24
|
-
});
|
|
25
|
-
}, [setStreamBlocks]);
|
|
26
|
-
return { cancelPendingStreamFrame, scheduleStreamBlocks };
|
|
27
|
-
}
|
|
28
|
-
/** Composer / transcript / session state behind {@link useTheoremChat}. */
|
|
29
|
-
export function useTheoremChatState(initialText = '') {
|
|
30
|
-
const [blocks, setBlocksState] = useState([]);
|
|
31
|
-
const [streamBlocks, setStreamBlocks] = useState([]);
|
|
32
|
-
const [draftText, setDraftText] = useState(initialText);
|
|
33
|
-
const [pendingFiles, setPendingFiles] = useState([]);
|
|
34
|
-
const [pendingVoice, setPendingVoice] = useState([]);
|
|
35
|
-
const [pendingMessages, setPendingMessages] = useState([]);
|
|
36
|
-
const [issues, setIssues] = useState([]);
|
|
37
|
-
const [failure, setFailure] = useState(null);
|
|
38
|
-
const [answering, setAnswering] = useState(null);
|
|
39
|
-
const [busy, setBusy] = useState(false);
|
|
40
|
-
const [chatStarted, setChatStarted] = useState(false);
|
|
41
|
-
const [streaming, setStreaming] = useState(false);
|
|
42
|
-
const [session, setSession] = useState(emptyInterfaceTurnSession);
|
|
43
|
-
// why: One object per posted message, so a run can tell whether it posted one.
|
|
44
|
-
const [delivery, setDeliveryState] = useState(null);
|
|
45
|
-
const blocksRef = useRef(blocks);
|
|
46
|
-
// why: Set as it is written, so a caller awaiting a turn reads the transcript the turn left.
|
|
47
|
-
const setBlocks = useCallback((value) => {
|
|
48
|
-
blocksRef.current = typeof value === 'function' ? value(blocksRef.current) : value;
|
|
49
|
-
setBlocksState(blocksRef.current);
|
|
50
|
-
}, []);
|
|
51
|
-
const streamBlocksRef = useRef(streamBlocks);
|
|
52
|
-
streamBlocksRef.current = streamBlocks;
|
|
53
|
-
const busyRef = useRef(false);
|
|
54
|
-
const abortRef = useRef(null);
|
|
55
|
-
const turnIdRef = useRef(null);
|
|
56
|
-
const pendingRef = useRef(pendingMessages);
|
|
57
|
-
pendingRef.current = pendingMessages;
|
|
58
|
-
const sessionRef = useRef(session);
|
|
59
|
-
sessionRef.current = session;
|
|
60
|
-
const drainLockRef = useRef(false);
|
|
61
|
-
const allowQueueDrainRef = useRef(false);
|
|
62
|
-
const runPromiseRef = useRef(null);
|
|
63
|
-
const deliveryRef = useRef(delivery);
|
|
64
|
-
// why: A run's end reads the composer as it is then, not as it was when the run began.
|
|
65
|
-
const composerRef = useRef({ draftText, pendingFiles, pendingVoice });
|
|
66
|
-
composerRef.current = { draftText, pendingFiles, pendingVoice };
|
|
67
|
-
const setDelivery = useCallback((next) => {
|
|
68
|
-
deliveryRef.current = next;
|
|
69
|
-
setDeliveryState(next);
|
|
70
|
-
}, []);
|
|
71
|
-
const { cancelPendingStreamFrame, scheduleStreamBlocks } = useStreamFrames(setStreamBlocks);
|
|
72
|
-
const clearComposer = useCallback(() => {
|
|
73
|
-
setDraftText('');
|
|
74
|
-
setPendingFiles([]);
|
|
75
|
-
setPendingVoice([]);
|
|
76
|
-
}, []);
|
|
77
|
-
return {
|
|
78
|
-
blocks,
|
|
79
|
-
setBlocks,
|
|
80
|
-
streamBlocks,
|
|
81
|
-
setStreamBlocks,
|
|
82
|
-
streamBlocksRef,
|
|
83
|
-
draftText,
|
|
84
|
-
setDraftText,
|
|
85
|
-
pendingFiles,
|
|
86
|
-
setPendingFiles,
|
|
87
|
-
pendingVoice,
|
|
88
|
-
setPendingVoice,
|
|
89
|
-
pendingMessages,
|
|
90
|
-
setPendingMessages,
|
|
91
|
-
issues,
|
|
92
|
-
setIssues,
|
|
93
|
-
failure,
|
|
94
|
-
setFailure,
|
|
95
|
-
answering,
|
|
96
|
-
setAnswering,
|
|
97
|
-
busy,
|
|
98
|
-
setBusy,
|
|
99
|
-
chatStarted,
|
|
100
|
-
setChatStarted,
|
|
101
|
-
streaming,
|
|
102
|
-
setStreaming,
|
|
103
|
-
session,
|
|
104
|
-
setSession,
|
|
105
|
-
delivery,
|
|
106
|
-
setDelivery,
|
|
107
|
-
deliveryRef,
|
|
108
|
-
composerRef,
|
|
109
|
-
blocksRef,
|
|
110
|
-
busyRef,
|
|
111
|
-
abortRef,
|
|
112
|
-
turnIdRef,
|
|
113
|
-
pendingRef,
|
|
114
|
-
sessionRef,
|
|
115
|
-
drainLockRef,
|
|
116
|
-
allowQueueDrainRef,
|
|
117
|
-
runPromiseRef,
|
|
118
|
-
cancelPendingStreamFrame,
|
|
119
|
-
scheduleStreamBlocks,
|
|
120
|
-
clearComposer,
|
|
121
|
-
};
|
|
122
|
-
}
|