@vanira/sdk-react-native 0.0.2
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 +239 -0
- package/package.json +53 -0
- package/src/__tests__/WebRTCClient.integration.test.ts +396 -0
- package/src/__tests__/adapters.test.ts +475 -0
- package/src/__tests__/httpResponse.test.ts +25 -0
- package/src/__tests__/mocks/react-native-incall-manager.ts +8 -0
- package/src/__tests__/mocks/react-native-permissions.ts +15 -0
- package/src/__tests__/mocks/react-native-webrtc.ts +6 -0
- package/src/__tests__/mocks/react-native.ts +28 -0
- package/src/__tests__/preset.test.ts +239 -0
- package/src/__tests__/resolveRuntimeConfig.test.ts +90 -0
- package/src/__tests__/storage.test.ts +211 -0
- package/src/__tests__/webrtcSignaling.test.ts +42 -0
- package/src/adapters/PeerConnectionAdapter.ts +101 -0
- package/src/adapters/browser/BrowserAudioAdapter.ts +43 -0
- package/src/adapters/browser/BrowserDataChannelAdapter.ts +69 -0
- package/src/adapters/browser/BrowserMediaAdapter.ts +15 -0
- package/src/adapters/browser/BrowserPeerAdapter.ts +14 -0
- package/src/adapters/browser/index.ts +4 -0
- package/src/adapters/interfaces.ts +84 -0
- package/src/adapters/react-native/RNAudioAdapter.ts +42 -0
- package/src/adapters/react-native/RNDataChannelAdapter.ts +79 -0
- package/src/adapters/react-native/RNMediaAdapter.ts +46 -0
- package/src/adapters/react-native/RNPeerAdapter.ts +28 -0
- package/src/adapters/react-native/callAudioRouting.ts +115 -0
- package/src/adapters/react-native/decodeUtf8.ts +72 -0
- package/src/adapters/react-native/index.ts +4 -0
- package/src/adapters/react-native/rnUploadFile.ts +76 -0
- package/src/adapters/storage/BrowserDualStorageAdapter.ts +71 -0
- package/src/adapters/storage/MemoryStorageAdapter.ts +50 -0
- package/src/adapters/storage/StorageAdapter.ts +21 -0
- package/src/adapters/storage/createSyncStorageAdapter.ts +40 -0
- package/src/adapters/storage/index.ts +7 -0
- package/src/api/services/ChatService.ts +304 -0
- package/src/api/services/ConfigService.ts +33 -0
- package/src/assets/icons.js +35 -0
- package/src/cdn.ts +68 -0
- package/src/core/CallSessionStore.ts +137 -0
- package/src/core/DraggableController.ts +83 -0
- package/src/core/SessionManager.ts +322 -0
- package/src/core/VaniraAI.ts +464 -0
- package/src/core/WebRTCClient.ts +1012 -0
- package/src/core/httpResponse.ts +22 -0
- package/src/core/iceServers.ts +18 -0
- package/src/core/toolCallNormalize.ts +80 -0
- package/src/core/voice-client.js +236 -0
- package/src/core/webrtcSignaling.ts +72 -0
- package/src/index.js +34 -0
- package/src/index.ts +6 -0
- package/src/platforms/browser.ts +67 -0
- package/src/platforms/react-native.ts +105 -0
- package/src/presets/BookingCalendarModal.tsx +457 -0
- package/src/presets/CameraModal.tsx +576 -0
- package/src/presets/DynamicFormModal.tsx +378 -0
- package/src/presets/NativePresetRenderer.tsx +350 -0
- package/src/presets/NavigateHandler.tsx +75 -0
- package/src/presets/PresetHost.tsx +155 -0
- package/src/presets/PresetShellModal.tsx +97 -0
- package/src/presets/UploadModal.tsx +321 -0
- package/src/presets/calendar/calendarUtils.ts +386 -0
- package/src/presets/call/CallSpeakerToggle.tsx +59 -0
- package/src/presets/call/callAudioRouting.ts +2 -0
- package/src/presets/call/useCallSpeaker.ts +31 -0
- package/src/presets/camera/cameraPermissions.ts +18 -0
- package/src/presets/camera/cameraStream.ts +19 -0
- package/src/presets/camera/cameraUtils.ts +21 -0
- package/src/presets/camera/useLivenessFlow.ts +95 -0
- package/src/presets/chalkboard/ChalkboardOverlay.tsx +156 -0
- package/src/presets/chalkboard/EraseTextHandler.tsx +95 -0
- package/src/presets/chalkboard/TypeTextHandler.tsx +107 -0
- package/src/presets/chalkboard/boardAbort.ts +36 -0
- package/src/presets/chalkboard/boardQueue.ts +620 -0
- package/src/presets/chalkboard/chalkboardSession.ts +75 -0
- package/src/presets/chalkboard/drawUtils.ts +123 -0
- package/src/presets/chalkboard/textUtils.ts +109 -0
- package/src/presets/clipRegion/ClipRegionModal.tsx +261 -0
- package/src/presets/clipRegion/clipRegionBridge.ts +19 -0
- package/src/presets/form/formValidation.ts +104 -0
- package/src/presets/form/parseFormFields.ts +171 -0
- package/src/presets/host/HostElementPresetHandler.tsx +155 -0
- package/src/presets/host/hostPresetBridge.ts +71 -0
- package/src/presets/index.ts +63 -0
- package/src/presets/liveScreen/CloseLiveScreenHandler.tsx +36 -0
- package/src/presets/liveScreen/LiveScreenCaptureHost.tsx +312 -0
- package/src/presets/liveScreen/LiveScreenHandler.tsx +25 -0
- package/src/presets/liveScreen/LiveScreenPipOverlay.tsx +6 -0
- package/src/presets/liveScreen/liveScreenSession.ts +73 -0
- package/src/presets/liveVision/CloseLiveVisionHandler.tsx +29 -0
- package/src/presets/liveVision/LiveVisionCameraHost.tsx +317 -0
- package/src/presets/liveVision/LiveVisionHandler.tsx +26 -0
- package/src/presets/liveVision/LiveVisionPipOverlay.tsx +7 -0
- package/src/presets/liveVision/liveVisionFrameLoop.ts +38 -0
- package/src/presets/liveVision/liveVisionSession.ts +75 -0
- package/src/presets/liveVision/liveVisionUpload.ts +62 -0
- package/src/presets/navigation/internalRouteRegistry.ts +25 -0
- package/src/presets/navigation/navigationBridge.ts +76 -0
- package/src/presets/navigation/navigationTypes.ts +12 -0
- package/src/presets/parseToolCall.ts +60 -0
- package/src/presets/presetClientAdapter.ts +29 -0
- package/src/presets/presetCompletion.ts +91 -0
- package/src/presets/presetEventHelpers.ts +45 -0
- package/src/presets/registry.ts +128 -0
- package/src/presets/streaming/mediaFrameUpload.ts +93 -0
- package/src/presets/types.ts +74 -0
- package/src/presets/upload/pickUploadFile.ts +256 -0
- package/src/presets/upload/uploadFormats.ts +163 -0
- package/src/presets/upload/uploadUtils.ts +68 -0
- package/src/react/PresetRenderer.tsx +144 -0
- package/src/react/index.ts +1 -0
- package/src/runtime/browserRuntime.ts +54 -0
- package/src/runtime/platform.ts +17 -0
- package/src/runtime/reactNativeRuntime.ts +68 -0
- package/src/runtime/resolveRuntimeConfig.ts +75 -0
- package/src/runtime/runtimeBundles.ts +74 -0
- package/src/runtime/types.ts +135 -0
- package/src/types/react-native-incall-manager.d.ts +17 -0
- package/src/types/react-native-webrtc.d.ts +47 -0
- package/src/types.ts +133 -0
- package/src/ui/VaniraWidget.ts +87 -0
- package/src/ui/abstraction/AbstractWidgetProvider.ts +18 -0
- package/src/ui/abstraction/interfaces.ts +12 -0
- package/src/ui/adapters/VaniraChatAdapter.ts +42 -0
- package/src/ui/components/AvatarView.ts +81 -0
- package/src/ui/components/ChatWindow.ts +263 -0
- package/src/ui/components/FloatingButton.ts +163 -0
- package/src/ui/components/FloatingWelcomeChips.ts +137 -0
- package/src/ui/components/Panel.ts +120 -0
- package/src/ui/components/VoiceOrb.ts +79 -0
- package/src/ui/components/VoiceOverlay.ts +497 -0
- package/src/ui/components/index.ts +7 -0
- package/src/ui/factory/WidgetFactory.ts +16 -0
- package/src/ui/icons_data.ts +2 -0
- package/src/ui/presets/WidgetPresetRenderer.ts +1802 -0
- package/src/ui/presets/types.ts +16 -0
- package/src/ui/providers/VaniraInternalProvider.ts +1066 -0
- package/src/ui/styles/index.ts +323 -0
- package/src/ui/styles/keyframes.ts +76 -0
- package/src/ui/styles/theme.ts +57 -0
- package/src/ui/styles/widget.css.ts +838 -0
- package/src/ui/utils.ts +37 -0
- package/src/ui/views/AbstractChatView.ts +93 -0
- package/src/ui/views/AbstractVoiceView.ts +57 -0
- package/src/ui/views/AvatarOnlyView.ts +78 -0
- package/src/ui/views/ChatAvatarView.ts +66 -0
- package/src/ui/views/ChatOnlyView.ts +28 -0
- package/src/ui/views/ChatVoiceView.ts +15 -0
- package/src/ui/views/VoiceOnlyView.ts +25 -0
- package/src/ui/views/index.ts +5 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whiteboard command queue — port of vanira-sdk WidgetPresetRenderer board queue.
|
|
3
|
+
* Serializes vanira_type_text / vanira_erase_text; handles interrupt + session reset.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {isBoardQueuedPreset as isBoardQueuedPresetId} from '../registry';
|
|
7
|
+
import type {PresetClient, PresetContext, PresetId} from '../types';
|
|
8
|
+
import {
|
|
9
|
+
buildShapesFromArgs,
|
|
10
|
+
shapesToBoardAnnotation,
|
|
11
|
+
} from './drawUtils';
|
|
12
|
+
import {
|
|
13
|
+
getChalkboardContent,
|
|
14
|
+
hideChalkboard,
|
|
15
|
+
setChalkboardContent,
|
|
16
|
+
setChalkboardTyping,
|
|
17
|
+
showChalkboard,
|
|
18
|
+
} from './chalkboardSession';
|
|
19
|
+
import {
|
|
20
|
+
eraseLastWords,
|
|
21
|
+
parseEraseTextArgs,
|
|
22
|
+
parseTypeTextArgs,
|
|
23
|
+
} from './textUtils';
|
|
24
|
+
|
|
25
|
+
export type ToolTerminalStatus = 'interrupted' | 'cancelled';
|
|
26
|
+
|
|
27
|
+
type OnComplete = (result: Record<string, unknown>) => void;
|
|
28
|
+
type OnCancel = (
|
|
29
|
+
reason?: string,
|
|
30
|
+
meta?: {status?: ToolTerminalStatus},
|
|
31
|
+
) => void;
|
|
32
|
+
|
|
33
|
+
type BoardQueuedPresetId = 'vanira_type_text' | 'vanira_erase_text' | 'vanira_draw';
|
|
34
|
+
|
|
35
|
+
type BoardQueueItem = {
|
|
36
|
+
ctx: PresetContext;
|
|
37
|
+
presetId: BoardQueuedPresetId;
|
|
38
|
+
onComplete: OnComplete;
|
|
39
|
+
onCancel: OnCancel;
|
|
40
|
+
aborted?: boolean;
|
|
41
|
+
settled?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type BoardWriteProgress = {
|
|
45
|
+
ctx: PresetContext;
|
|
46
|
+
mode: 'type' | 'erase' | 'draw';
|
|
47
|
+
textAtStart: string;
|
|
48
|
+
plannedUnits: number;
|
|
49
|
+
unitsDone: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let boardQueue: BoardQueueItem[] = [];
|
|
53
|
+
let boardQueueActive: BoardQueueItem | null = null;
|
|
54
|
+
let boardAnimTimer: ReturnType<typeof setTimeout> | null = null;
|
|
55
|
+
let activePresetGeneration = 0;
|
|
56
|
+
let settledBoardToolCallIds = new Set<string>();
|
|
57
|
+
let boardClient: PresetClient | null = null;
|
|
58
|
+
let activeBoardProgress: BoardWriteProgress | null = null;
|
|
59
|
+
|
|
60
|
+
function clearBoardAnimTimer(): void {
|
|
61
|
+
if (boardAnimTimer) {
|
|
62
|
+
clearTimeout(boardAnimTimer);
|
|
63
|
+
boardAnimTimer = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scheduleBoardAnim(fn: () => void, delayMs: number): void {
|
|
68
|
+
clearBoardAnimTimer();
|
|
69
|
+
boardAnimTimer = setTimeout(() => {
|
|
70
|
+
boardAnimTimer = null;
|
|
71
|
+
fn();
|
|
72
|
+
}, delayMs);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function syncBoardProgressUnits(unitsDone: number): void {
|
|
76
|
+
if (activeBoardProgress) {
|
|
77
|
+
activeBoardProgress.unitsDone = unitsDone;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clearActiveBoardProgress(): void {
|
|
82
|
+
activeBoardProgress = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function flushBoardContext(payload: Record<string, unknown>): void {
|
|
86
|
+
if (!boardClient?.sendContextUpdate) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
boardClient.sendContextUpdate(payload);
|
|
91
|
+
console.log('[BoardQueue] client_context_update (interrupt):', payload.message);
|
|
92
|
+
} catch {
|
|
93
|
+
/* silent */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildBoardContextPayload(
|
|
98
|
+
ctx: PresetContext,
|
|
99
|
+
textWritten: string,
|
|
100
|
+
interruptReason?: string,
|
|
101
|
+
): Record<string, unknown> {
|
|
102
|
+
const progress = activeBoardProgress;
|
|
103
|
+
const matchesProgress = progress?.ctx.toolCallId === ctx.toolCallId;
|
|
104
|
+
|
|
105
|
+
let writeProgressPct: number | undefined;
|
|
106
|
+
let unitsDone: number | undefined;
|
|
107
|
+
let unitsPlanned: number | undefined;
|
|
108
|
+
let partialThisCommand: string | undefined;
|
|
109
|
+
|
|
110
|
+
if (matchesProgress && progress) {
|
|
111
|
+
unitsDone = progress.unitsDone;
|
|
112
|
+
unitsPlanned = progress.plannedUnits;
|
|
113
|
+
writeProgressPct =
|
|
114
|
+
progress.plannedUnits > 0
|
|
115
|
+
? Math.min(
|
|
116
|
+
100,
|
|
117
|
+
Math.round((progress.unitsDone / progress.plannedUnits) * 100),
|
|
118
|
+
)
|
|
119
|
+
: 100;
|
|
120
|
+
|
|
121
|
+
partialThisCommand =
|
|
122
|
+
progress.mode === 'type'
|
|
123
|
+
? textWritten.slice(progress.textAtStart.length)
|
|
124
|
+
: textWritten;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const payload: Record<string, unknown> = {
|
|
128
|
+
ui_state: 'whiteboard_active',
|
|
129
|
+
tool_status: 'interrupted',
|
|
130
|
+
visible_preset: ctx.presetId,
|
|
131
|
+
tool_call_id: ctx.toolCallId,
|
|
132
|
+
interrupted: true,
|
|
133
|
+
interrupt_reason: interruptReason || 'Server interrupted',
|
|
134
|
+
whiteboard_content: textWritten,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (writeProgressPct !== undefined) {
|
|
138
|
+
payload.write_progress_pct = writeProgressPct;
|
|
139
|
+
payload.characters_written = unitsDone;
|
|
140
|
+
payload.characters_planned = unitsPlanned;
|
|
141
|
+
payload.partial_text_this_command = partialThisCommand ?? '';
|
|
142
|
+
|
|
143
|
+
const action = progress?.mode === 'erase' ? 'erase' : 'write';
|
|
144
|
+
const partialPreview =
|
|
145
|
+
(partialThisCommand ?? '').length > 120
|
|
146
|
+
? `${(partialThisCommand ?? '').slice(0, 120)}…`
|
|
147
|
+
: (partialThisCommand ?? '');
|
|
148
|
+
|
|
149
|
+
const continueHint =
|
|
150
|
+
progress?.mode === 'erase'
|
|
151
|
+
? 'The board already reflects this partial erase — continue from here; do not repeat earlier steps.'
|
|
152
|
+
: 'The board already contains this content — continue typing from here; do not repeat what is already on the board.';
|
|
153
|
+
|
|
154
|
+
payload.message =
|
|
155
|
+
`Whiteboard ${action} interrupted at ${writeProgressPct}% (${unitsDone}/${unitsPlanned} characters this command). ` +
|
|
156
|
+
`Partial text this turn: "${partialPreview}". ` +
|
|
157
|
+
continueHint;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return payload;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function sendBoardContextUpdate(
|
|
164
|
+
ctx: PresetContext,
|
|
165
|
+
textWritten: string,
|
|
166
|
+
interruptReason?: string,
|
|
167
|
+
): void {
|
|
168
|
+
flushBoardContext(
|
|
169
|
+
buildBoardContextPayload(ctx, textWritten, interruptReason),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getBoardProgressSnapshot(): {
|
|
174
|
+
write_progress_pct?: number;
|
|
175
|
+
characters_written?: number;
|
|
176
|
+
characters_planned?: number;
|
|
177
|
+
partial_text_this_command?: string;
|
|
178
|
+
} | null {
|
|
179
|
+
const p = activeBoardProgress;
|
|
180
|
+
if (!p || p.plannedUnits <= 0) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const textWritten = getChalkboardContent();
|
|
184
|
+
const partialThisCommand =
|
|
185
|
+
p.mode === 'type'
|
|
186
|
+
? textWritten.slice(p.textAtStart.length)
|
|
187
|
+
: textWritten;
|
|
188
|
+
return {
|
|
189
|
+
write_progress_pct: Math.min(
|
|
190
|
+
100,
|
|
191
|
+
Math.round((p.unitsDone / p.plannedUnits) * 100),
|
|
192
|
+
),
|
|
193
|
+
characters_written: p.unitsDone,
|
|
194
|
+
characters_planned: p.plannedUnits,
|
|
195
|
+
partial_text_this_command: partialThisCommand,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getBoardTerminalResult(
|
|
200
|
+
status: ToolTerminalStatus,
|
|
201
|
+
reason?: string,
|
|
202
|
+
presetId?: string,
|
|
203
|
+
): Record<string, unknown> {
|
|
204
|
+
const textWritten = getChalkboardContent();
|
|
205
|
+
const progress = getBoardProgressSnapshot();
|
|
206
|
+
return {
|
|
207
|
+
status,
|
|
208
|
+
reason: reason || (status === 'interrupted' ? 'Server interrupted' : 'User dismissed'),
|
|
209
|
+
...(presetId ? {preset_id: presetId} : {}),
|
|
210
|
+
text_written: textWritten,
|
|
211
|
+
whiteboard_content: textWritten,
|
|
212
|
+
...(progress || {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function closeBoardUi(clearContent = false): void {
|
|
217
|
+
if (clearContent) {
|
|
218
|
+
setChalkboardContent('');
|
|
219
|
+
}
|
|
220
|
+
hideChalkboard();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function settleBoardCancel(
|
|
224
|
+
item: BoardQueueItem,
|
|
225
|
+
reason: string,
|
|
226
|
+
status: ToolTerminalStatus = 'cancelled',
|
|
227
|
+
): void {
|
|
228
|
+
if (item.settled) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
item.aborted = true;
|
|
232
|
+
item.settled = true;
|
|
233
|
+
const id = item.ctx.toolCallId;
|
|
234
|
+
if (id) {
|
|
235
|
+
settledBoardToolCallIds.add(id);
|
|
236
|
+
}
|
|
237
|
+
const textWritten = getChalkboardContent();
|
|
238
|
+
if (status === 'interrupted') {
|
|
239
|
+
sendBoardContextUpdate(item.ctx, textWritten, reason);
|
|
240
|
+
}
|
|
241
|
+
item.onCancel(reason, {status});
|
|
242
|
+
clearActiveBoardProgress();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function settleBoardComplete(
|
|
246
|
+
item: BoardQueueItem,
|
|
247
|
+
result: Record<string, unknown>,
|
|
248
|
+
): void {
|
|
249
|
+
if (item.settled || item.aborted) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
item.settled = true;
|
|
253
|
+
const id = item.ctx.toolCallId;
|
|
254
|
+
if (id) {
|
|
255
|
+
settledBoardToolCallIds.add(id);
|
|
256
|
+
}
|
|
257
|
+
const textWritten = getChalkboardContent();
|
|
258
|
+
item.onComplete({
|
|
259
|
+
status: 'success',
|
|
260
|
+
preset_id: item.presetId,
|
|
261
|
+
tool_call_id: item.ctx.toolCallId,
|
|
262
|
+
text_written: textWritten,
|
|
263
|
+
whiteboard_content: textWritten,
|
|
264
|
+
...result,
|
|
265
|
+
});
|
|
266
|
+
clearActiveBoardProgress();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function runTypeText(
|
|
270
|
+
item: BoardQueueItem,
|
|
271
|
+
onComplete: (result?: Record<string, unknown>) => void,
|
|
272
|
+
onCancel: (reason?: string) => void,
|
|
273
|
+
): void {
|
|
274
|
+
const {ctx} = item;
|
|
275
|
+
const {text, delayMs} = parseTypeTextArgs(ctx);
|
|
276
|
+
|
|
277
|
+
console.log('[BoardQueue] type_text', {
|
|
278
|
+
toolCallId: ctx.toolCallId,
|
|
279
|
+
text,
|
|
280
|
+
delayMs,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
showChalkboard();
|
|
284
|
+
setChalkboardTyping(true);
|
|
285
|
+
|
|
286
|
+
const initialText = getChalkboardContent();
|
|
287
|
+
const textToAppend = initialText && text ? `\n${text}` : text;
|
|
288
|
+
|
|
289
|
+
if (!textToAppend) {
|
|
290
|
+
setChalkboardTyping(false);
|
|
291
|
+
onComplete();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const typingGeneration = activePresetGeneration;
|
|
296
|
+
|
|
297
|
+
activeBoardProgress = {
|
|
298
|
+
ctx,
|
|
299
|
+
mode: 'type',
|
|
300
|
+
textAtStart: initialText,
|
|
301
|
+
plannedUnits: textToAppend.length,
|
|
302
|
+
unitsDone: 0,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (delayMs <= 0) {
|
|
306
|
+
setChalkboardContent(initialText + textToAppend);
|
|
307
|
+
syncBoardProgressUnits(textToAppend.length);
|
|
308
|
+
setChalkboardTyping(false);
|
|
309
|
+
onComplete();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let currentText = initialText;
|
|
314
|
+
let index = 0;
|
|
315
|
+
|
|
316
|
+
const typeNextChar = () => {
|
|
317
|
+
if (typingGeneration !== activePresetGeneration) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (index < textToAppend.length) {
|
|
321
|
+
currentText += textToAppend[index];
|
|
322
|
+
setChalkboardContent(currentText);
|
|
323
|
+
index += 1;
|
|
324
|
+
syncBoardProgressUnits(index);
|
|
325
|
+
scheduleBoardAnim(typeNextChar, delayMs);
|
|
326
|
+
} else {
|
|
327
|
+
syncBoardProgressUnits(textToAppend.length);
|
|
328
|
+
setChalkboardTyping(false);
|
|
329
|
+
onComplete();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
typeNextChar();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function runDrawShapes(
|
|
337
|
+
item: BoardQueueItem,
|
|
338
|
+
onComplete: (result?: Record<string, unknown>) => void,
|
|
339
|
+
): void {
|
|
340
|
+
const {ctx} = item;
|
|
341
|
+
const merged = {...ctx.args, ...ctx.clientFields};
|
|
342
|
+
const shapes = buildShapesFromArgs(merged);
|
|
343
|
+
const annotation = shapesToBoardAnnotation(shapes);
|
|
344
|
+
|
|
345
|
+
console.log('[BoardQueue] draw_shapes', {
|
|
346
|
+
toolCallId: ctx.toolCallId,
|
|
347
|
+
shapes: shapes.map(s => s.summary),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
showChalkboard();
|
|
351
|
+
setChalkboardTyping(true);
|
|
352
|
+
|
|
353
|
+
const initialText = getChalkboardContent();
|
|
354
|
+
const textToAppend = annotation;
|
|
355
|
+
|
|
356
|
+
activeBoardProgress = {
|
|
357
|
+
ctx,
|
|
358
|
+
mode: 'draw',
|
|
359
|
+
textAtStart: initialText,
|
|
360
|
+
plannedUnits: textToAppend.length || 1,
|
|
361
|
+
unitsDone: 0,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
if (!textToAppend) {
|
|
365
|
+
setChalkboardTyping(false);
|
|
366
|
+
onComplete({
|
|
367
|
+
shapes_drawn: 0,
|
|
368
|
+
status: 'error',
|
|
369
|
+
message: 'No drawable shape parameters',
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
setChalkboardContent(initialText + textToAppend);
|
|
375
|
+
syncBoardProgressUnits(textToAppend.length);
|
|
376
|
+
setChalkboardTyping(false);
|
|
377
|
+
onComplete({
|
|
378
|
+
shapes_drawn: shapes.length,
|
|
379
|
+
shape_summaries: shapes.map(s => s.summary),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function runEraseText(
|
|
384
|
+
item: BoardQueueItem,
|
|
385
|
+
onComplete: (result?: Record<string, unknown>) => void,
|
|
386
|
+
onCancel: (reason?: string) => void,
|
|
387
|
+
): void {
|
|
388
|
+
const {ctx} = item;
|
|
389
|
+
const {mode, numWords, delayMs} = parseEraseTextArgs(ctx);
|
|
390
|
+
|
|
391
|
+
console.log('[BoardQueue] erase_text', {
|
|
392
|
+
toolCallId: ctx.toolCallId,
|
|
393
|
+
mode,
|
|
394
|
+
numWords,
|
|
395
|
+
delayMs,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const hadChalkboard =
|
|
399
|
+
getChalkboardContent().length > 0;
|
|
400
|
+
|
|
401
|
+
if (!hadChalkboard) {
|
|
402
|
+
setChalkboardTyping(false);
|
|
403
|
+
onComplete({status: 'error', message: 'Target element not found'});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
showChalkboard();
|
|
408
|
+
setChalkboardTyping(true);
|
|
409
|
+
|
|
410
|
+
const currentVal = getChalkboardContent();
|
|
411
|
+
const targetVal =
|
|
412
|
+
mode === 'words' ? eraseLastWords(currentVal, numWords) : '';
|
|
413
|
+
|
|
414
|
+
const eraseGeneration = activePresetGeneration;
|
|
415
|
+
const deletionsPlanned = Math.max(0, currentVal.length - targetVal.length);
|
|
416
|
+
|
|
417
|
+
activeBoardProgress = {
|
|
418
|
+
ctx,
|
|
419
|
+
mode: 'erase',
|
|
420
|
+
textAtStart: currentVal,
|
|
421
|
+
plannedUnits: deletionsPlanned,
|
|
422
|
+
unitsDone: delayMs <= 0 ? deletionsPlanned : 0,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
if (delayMs <= 0 || currentVal === targetVal) {
|
|
426
|
+
setChalkboardContent(targetVal);
|
|
427
|
+
setChalkboardTyping(false);
|
|
428
|
+
onComplete();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let working = currentVal;
|
|
433
|
+
|
|
434
|
+
const deleteNextChar = () => {
|
|
435
|
+
if (eraseGeneration !== activePresetGeneration) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (working.length > targetVal.length) {
|
|
439
|
+
working = working.slice(0, -1);
|
|
440
|
+
setChalkboardContent(working);
|
|
441
|
+
syncBoardProgressUnits(currentVal.length - working.length);
|
|
442
|
+
scheduleBoardAnim(deleteNextChar, Math.max(10, delayMs / 2));
|
|
443
|
+
} else {
|
|
444
|
+
setChalkboardTyping(false);
|
|
445
|
+
onComplete();
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
deleteNextChar();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function processBoardQueue(): void {
|
|
453
|
+
if (boardQueueActive || boardQueue.length === 0) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const item = boardQueue.shift()!;
|
|
458
|
+
boardQueueActive = item;
|
|
459
|
+
console.log(
|
|
460
|
+
`[BoardQueue] Running ${item.presetId} for tool_call_id=${item.ctx.toolCallId}`,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const finish = (
|
|
464
|
+
result: Record<string, unknown> | null,
|
|
465
|
+
cancelReason?: string,
|
|
466
|
+
) => {
|
|
467
|
+
if (boardQueueActive !== item) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (item.settled || item.aborted) {
|
|
471
|
+
boardQueueActive = null;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
boardQueueActive = null;
|
|
476
|
+
if (cancelReason !== undefined) {
|
|
477
|
+
settleBoardCancel(item, cancelReason);
|
|
478
|
+
} else {
|
|
479
|
+
settleBoardComplete(item, result || {});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
processBoardQueue();
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const wrappedComplete = (result: Record<string, unknown> = {}) =>
|
|
486
|
+
finish(result);
|
|
487
|
+
const wrappedCancel = (reason?: string) => {
|
|
488
|
+
activePresetGeneration++;
|
|
489
|
+
finish(null, reason || 'Cancelled');
|
|
490
|
+
if (!boardQueueActive && boardQueue.length === 0) {
|
|
491
|
+
closeBoardUi(true);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const onAnimComplete = (result: Record<string, unknown> = {status: 'success'}) =>
|
|
496
|
+
finish(result);
|
|
497
|
+
|
|
498
|
+
if (item.presetId === 'vanira_type_text') {
|
|
499
|
+
runTypeText(item, onAnimComplete, wrappedCancel);
|
|
500
|
+
} else if (item.presetId === 'vanira_draw') {
|
|
501
|
+
runDrawShapes(item, onAnimComplete);
|
|
502
|
+
} else {
|
|
503
|
+
runEraseText(item, onAnimComplete, wrappedCancel);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function isBoardQueuedPreset(
|
|
508
|
+
presetId: string | undefined,
|
|
509
|
+
): presetId is BoardQueuedPresetId {
|
|
510
|
+
return isBoardQueuedPresetId(presetId);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function enqueueBoardCommand(
|
|
514
|
+
item: {
|
|
515
|
+
ctx: PresetContext;
|
|
516
|
+
presetId: BoardQueuedPresetId;
|
|
517
|
+
onComplete: OnComplete;
|
|
518
|
+
onCancel: OnCancel;
|
|
519
|
+
},
|
|
520
|
+
client?: PresetClient | null,
|
|
521
|
+
): void {
|
|
522
|
+
if (client) {
|
|
523
|
+
boardClient = client;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const id = item.ctx.toolCallId;
|
|
527
|
+
if (id) {
|
|
528
|
+
if (settledBoardToolCallIds.has(id)) {
|
|
529
|
+
console.log(`[BoardQueue] Skipping duplicate settled tool_call_id=${id}`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (boardQueueActive?.ctx.toolCallId === id) {
|
|
533
|
+
console.log(`[BoardQueue] Skipping duplicate active tool_call_id=${id}`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (boardQueue.some(q => q.ctx.toolCallId === id)) {
|
|
537
|
+
console.log(`[BoardQueue] Skipping duplicate queued tool_call_id=${id}`);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
boardQueue.push(item);
|
|
543
|
+
console.log(
|
|
544
|
+
`[BoardQueue] Enqueued ${item.presetId} (${boardQueue.length} pending)`,
|
|
545
|
+
);
|
|
546
|
+
processBoardQueue();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function abortBoardTools(
|
|
550
|
+
reason = 'Interrupted',
|
|
551
|
+
toolCallId?: string,
|
|
552
|
+
status: ToolTerminalStatus = 'interrupted',
|
|
553
|
+
): void {
|
|
554
|
+
const matches = (item: BoardQueueItem) =>
|
|
555
|
+
!toolCallId || item.ctx.toolCallId === toolCallId;
|
|
556
|
+
|
|
557
|
+
const hasWork =
|
|
558
|
+
(boardQueueActive && matches(boardQueueActive)) ||
|
|
559
|
+
boardQueue.some(matches);
|
|
560
|
+
|
|
561
|
+
if (!hasWork) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
console.log(
|
|
566
|
+
`[BoardQueue] Aborting board tools: ${reason}${toolCallId ? ` (${toolCallId})` : ''}`,
|
|
567
|
+
);
|
|
568
|
+
clearBoardAnimTimer();
|
|
569
|
+
activePresetGeneration++;
|
|
570
|
+
|
|
571
|
+
if (boardQueueActive && matches(boardQueueActive)) {
|
|
572
|
+
const active = boardQueueActive;
|
|
573
|
+
boardQueueActive = null;
|
|
574
|
+
settleBoardCancel(active, reason, status);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
boardQueue = boardQueue.filter(item => {
|
|
578
|
+
if (!matches(item)) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
settleBoardCancel(item, reason, status);
|
|
582
|
+
return false;
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
setChalkboardTyping(false);
|
|
586
|
+
processBoardQueue();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** User closed chalkboard — cancel active + queued with cancelled status. */
|
|
590
|
+
export function dismissChalkboardUser(reason = 'User dismissed chalkboard'): void {
|
|
591
|
+
if (!boardQueueActive && boardQueue.length === 0) {
|
|
592
|
+
closeBoardUi(true);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
abortBoardTools(reason, undefined, 'cancelled');
|
|
596
|
+
if (!boardQueueActive && boardQueue.length === 0) {
|
|
597
|
+
closeBoardUi(true);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Full board teardown — close UI, drop queue, clear in-memory board state.
|
|
603
|
+
* Used on call end / new session (does not send client_tool_result).
|
|
604
|
+
*/
|
|
605
|
+
export function resetBoardSession(): void {
|
|
606
|
+
activePresetGeneration++;
|
|
607
|
+
clearBoardAnimTimer();
|
|
608
|
+
boardQueue = [];
|
|
609
|
+
boardQueueActive = null;
|
|
610
|
+
settledBoardToolCallIds.clear();
|
|
611
|
+
activeBoardProgress = null;
|
|
612
|
+
boardClient = null;
|
|
613
|
+
setChalkboardContent('');
|
|
614
|
+
closeBoardUi();
|
|
615
|
+
console.log('[BoardQueue] Session reset — board closed, queue cleared');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function hasActiveBoardWork(): boolean {
|
|
619
|
+
return !!boardQueueActive || boardQueue.length > 0;
|
|
620
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Module-level chalkboard state — mirrors WidgetPresetRenderer.chalkboardContent. */
|
|
2
|
+
|
|
3
|
+
export type ChalkboardSnapshot = {
|
|
4
|
+
visible: boolean;
|
|
5
|
+
content: string;
|
|
6
|
+
typing: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let content = '';
|
|
10
|
+
let visible = false;
|
|
11
|
+
let typing = false;
|
|
12
|
+
let cachedSnapshot: ChalkboardSnapshot = {
|
|
13
|
+
visible: false,
|
|
14
|
+
content: '',
|
|
15
|
+
typing: false,
|
|
16
|
+
};
|
|
17
|
+
const listeners = new Set<() => void>();
|
|
18
|
+
|
|
19
|
+
function refreshSnapshot(): ChalkboardSnapshot {
|
|
20
|
+
if (
|
|
21
|
+
cachedSnapshot.visible !== visible ||
|
|
22
|
+
cachedSnapshot.content !== content ||
|
|
23
|
+
cachedSnapshot.typing !== typing
|
|
24
|
+
) {
|
|
25
|
+
cachedSnapshot = {visible, content, typing};
|
|
26
|
+
}
|
|
27
|
+
return cachedSnapshot;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function notify(): void {
|
|
31
|
+
refreshSnapshot();
|
|
32
|
+
listeners.forEach(listener => listener());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function subscribeChalkboard(listener: () => void): () => void {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
return () => listeners.delete(listener);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getChalkboardSnapshot(): ChalkboardSnapshot {
|
|
41
|
+
return refreshSnapshot();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getChalkboardContent(): string {
|
|
45
|
+
return content;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function setChalkboardContent(next: string): void {
|
|
49
|
+
content = next;
|
|
50
|
+
notify();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isChalkboardVisible(): boolean {
|
|
54
|
+
return visible;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isChalkboardTyping(): boolean {
|
|
58
|
+
return typing;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function showChalkboard(): void {
|
|
62
|
+
visible = true;
|
|
63
|
+
notify();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function hideChalkboard(): void {
|
|
67
|
+
visible = false;
|
|
68
|
+
typing = false;
|
|
69
|
+
notify();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function setChalkboardTyping(active: boolean): void {
|
|
73
|
+
typing = active;
|
|
74
|
+
notify();
|
|
75
|
+
}
|