@alia.onl/sdk 15.7.0 → 19.0.0
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/CHANGELOG.md +255 -12
- package/README.md +241 -155
- package/package.json +17 -5
- package/src/animations.ts +0 -1
- package/src/components/AliaAskProvider.tsx +127 -0
- package/src/components/AliaChatContent.tsx +604 -392
- package/src/components/AliaChatHeader.tsx +14 -38
- package/src/components/AliaChatScreen.tsx +56 -31
- package/src/components/AliaChatSheet.tsx +110 -36
- package/src/components/AliaComposer.tsx +1 -1
- package/src/components/AliaParticipants.tsx +202 -0
- package/src/components/AliaParticipantsHeader.tsx +60 -0
- package/src/components/AliaSharedConversation.tsx +86 -0
- package/src/components/AliaWelcomeMessage.tsx +18 -5
- package/src/components/Image.tsx +12 -0
- package/src/components/Markdown.tsx +498 -14
- package/src/components/PlanPreviewCard.tsx +7 -49
- package/src/components/ReplyMarkdown.tsx +359 -0
- package/src/components/RichBlocks.tsx +195 -0
- package/src/components/canvas/CanvasComponent.tsx +59 -0
- package/src/components/canvas/ChartRenderer.tsx +73 -0
- package/src/components/canvas/CodeRenderer.tsx +24 -0
- package/src/components/canvas/FormRenderer.tsx +101 -0
- package/src/components/canvas/MarkdownRenderer.tsx +14 -0
- package/src/components/canvas/TableRenderer.tsx +47 -0
- package/src/components/thought/FilesAndSources.tsx +120 -0
- package/src/components/thought/ThoughtPanel.tsx +417 -0
- package/src/components/thought/ToolStep.tsx +141 -0
- package/src/components/thread/AgentResultCard.tsx +125 -0
- package/src/components/thread/AgentTaskCard.tsx +127 -0
- package/src/components/thread/AliaThread.tsx +401 -0
- package/src/components/thread/MessageBlockBoundary.tsx +78 -0
- package/src/components/thread/NewConversationOffer.tsx +54 -0
- package/src/components/thread/ThreadRow.tsx +458 -0
- package/src/components/thread/ThreadSearch.tsx +129 -0
- package/src/components/thread/ToolResultCard.tsx +254 -0
- package/src/components/thread/TurnMenu.tsx +54 -0
- package/src/components/thread/TurnStatusLine.tsx +34 -0
- package/src/components/voice/AliaVoiceCall.tsx +21 -0
- package/src/components/voice/VoiceControls.tsx +2 -28
- package/src/components/voice/VoiceSession.tsx +39 -11
- package/src/csp.ts +50 -0
- package/src/hooks/useAliaActivity.ts +3 -3
- package/src/hooks/useAtBottom.ts +51 -0
- package/src/hooks/useAttachMenu.ts +94 -0
- package/src/hooks/useChatComposer.ts +299 -0
- package/src/hooks/useChatVoice.ts +129 -0
- package/src/hooks/useDictation.ts +2 -3
- package/src/hooks/useDocumentPicker.ts +3 -29
- package/src/hooks/useFailedTurnToast.ts +43 -0
- package/src/hooks/useMessageVote.ts +50 -0
- package/src/hooks/useOpenThought.ts +79 -0
- package/src/hooks/usePendingQuestions.ts +2 -2
- package/src/hooks/useReadAloud.ts +92 -0
- package/src/hooks/useShareHandler.ts +24 -0
- package/src/hooks/useSoundEffects.ts +1 -1
- package/src/hooks/useTTS.ts +2 -3
- package/src/hooks/useThoughtScope.ts +59 -0
- package/src/hooks/useThreadRows.ts +145 -0
- package/src/hooks/useThreadView.ts +68 -0
- package/src/hooks/useTimelineWindow.ts +256 -0
- package/src/hooks/useVoiceLauncher.ts +180 -0
- package/src/hooks/useVoiceRoom.ts +2 -6
- package/src/index.ts +43 -29
- package/src/lib/activity-store.ts +1 -1
- package/src/lib/add-menu.ts +7 -0
- package/src/lib/call-thread.ts +33 -0
- package/src/lib/character-runtime.ts +6 -4
- package/src/lib/citations.ts +79 -2
- package/src/lib/clipboard.ts +23 -0
- package/src/lib/content-utils.ts +10 -0
- package/src/lib/copy-en.ts +284 -0
- package/src/lib/copy.ts +32 -0
- package/src/lib/document-picker.ts +31 -0
- package/src/lib/flights.ts +22 -13
- package/src/lib/focus-return.ts +66 -0
- package/src/lib/keyboard.native.tsx +19 -0
- package/src/lib/keyboard.tsx +9 -0
- package/src/lib/labels.tsx +69 -17
- package/src/lib/origins.ts +15 -0
- package/src/lib/participants.ts +86 -0
- package/src/lib/power-level-options.ts +66 -0
- package/src/lib/questions.ts +2 -2
- package/src/lib/task-labels.ts +4 -4
- package/src/lib/tool-call-status.ts +1 -1
- package/src/lib/tool-icons.ts +36 -0
- package/src/lib/tool-renderers.tsx +48 -0
- package/src/lib/turn-state.ts +2 -2
- package/src/lib/voice-audio-engine.ts +14 -158
- package/src/lib/voice-call-request.ts +49 -0
- package/src/lib/voice-call.ts +76 -34
- package/src/lib/voice-controls-labels.ts +31 -0
- package/src/lib/voice-protocol.ts +32 -14
- package/src/lib/voice-worklets.ts +19 -0
- package/src/lib/work-log.ts +6 -6
- package/src/runtime/agents.ts +151 -0
- package/src/runtime/api.ts +123 -0
- package/src/runtime/attachment-utils.ts +96 -0
- package/src/runtime/composer-draft-store.ts +270 -0
- package/src/runtime/config.tsx +186 -0
- package/src/runtime/conversation-store.ts +49 -0
- package/src/runtime/conversations.ts +557 -0
- package/src/runtime/error-utils.ts +104 -0
- package/src/runtime/host-tools.ts +140 -0
- package/src/runtime/index.ts +113 -0
- package/src/runtime/message-history.ts +124 -0
- package/src/runtime/model.ts +39 -0
- package/src/runtime/oxy-files.ts +37 -0
- package/src/runtime/power-levels.ts +66 -0
- package/src/runtime/query-keys.ts +34 -0
- package/src/runtime/random-uuid.ts +25 -0
- package/src/runtime/routes.ts +51 -0
- package/src/runtime/sharing.ts +165 -0
- package/src/runtime/sse-frame-reader.ts +88 -0
- package/src/runtime/stream-events.ts +146 -0
- package/src/runtime/stream-outcome.ts +21 -0
- package/src/runtime/thought-store.ts +103 -0
- package/src/runtime/thread/agent-activity.ts +261 -0
- package/src/runtime/thread/automation-labels.ts +79 -0
- package/src/runtime/thread/conversation-export.ts +135 -0
- package/src/runtime/thread/message-days.ts +120 -0
- package/src/runtime/thread/message-processor.ts +93 -0
- package/src/runtime/thread/task-utils.ts +11 -0
- package/src/runtime/thread/thought-utils.ts +752 -0
- package/src/runtime/thread/thread-history.ts +139 -0
- package/src/runtime/thread/timeline.ts +202 -0
- package/src/runtime/thread/title-tags.ts +8 -0
- package/src/runtime/thread/tool-cards.ts +383 -0
- package/src/runtime/turn-failure.ts +108 -0
- package/src/runtime/types.ts +148 -0
- package/src/runtime/usage-limit-error.ts +44 -0
- package/src/runtime/use-agent-activity.ts +80 -0
- package/src/runtime/use-conversation-stream.ts +1427 -0
- package/src/runtime/use-conversation-thread.ts +556 -0
- package/src/runtime/use-start-conversation.ts +107 -0
- package/src/runtime/use-thread-history.ts +64 -0
- package/src/runtime/use-thread-search.ts +95 -0
- package/src/runtime/use-turn-edit.ts +99 -0
- package/src/tools.ts +2 -2
- package/src/types.ts +18 -47
- package/worklets/voice-processors.js +152 -0
- package/src/components/AliaChatMessageList.tsx +0 -532
- package/src/components/ThinkingBlock.tsx +0 -43
- package/src/hooks/useAliaChat.ts +0 -573
- package/src/lib/api-url.ts +0 -2
- package/src/lib/catalogue.ts +0 -236
- package/src/lib/chat-stream.ts +0 -655
- package/src/lib/chat-transport.ts +0 -98
- package/src/lib/delegation.ts +0 -34
- package/src/lib/work-log-copy.ts +0 -61
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Message,
|
|
3
|
+
TAIL,
|
|
4
|
+
TOP,
|
|
5
|
+
type TimelineAnchor,
|
|
6
|
+
type TimelinePosition,
|
|
7
|
+
collapseToTail,
|
|
8
|
+
pinAnchor,
|
|
9
|
+
resolveStart,
|
|
10
|
+
revealAbove,
|
|
11
|
+
sameTimelineShape,
|
|
12
|
+
timelinePositions,
|
|
13
|
+
} from '../runtime/model';
|
|
14
|
+
import type { AiChatThreadHandle } from '@oxy.so/bloom/ai-chat';
|
|
15
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState, type RefObject } from 'react';
|
|
16
|
+
import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The list, handed back as the SAME array for as long as only what its
|
|
20
|
+
* messages say has changed (`sameTimelineShape`).
|
|
21
|
+
*
|
|
22
|
+
* The array it returns may hold an older copy of the message being streamed
|
|
23
|
+
* into, so it is for deriving structure — ids, roles, stamps, speakers — and
|
|
24
|
+
* never for drawing text.
|
|
25
|
+
*/
|
|
26
|
+
export function useTimelineShape<
|
|
27
|
+
T extends Pick<Message, 'id' | 'role' | 'createdAt' | 'agentInfo'>,
|
|
28
|
+
>(list: readonly T[]): readonly T[] {
|
|
29
|
+
const shape = useRef(list);
|
|
30
|
+
if (!sameTimelineShape(shape.current, list)) shape.current = list;
|
|
31
|
+
return shape.current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Options {
|
|
35
|
+
/** Every row's id, in reading order. Keep its identity stable across tokens. */
|
|
36
|
+
ids: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* What a saved position belongs to — the conversation — or `null` for a view
|
|
39
|
+
* that is not remembered (a cursor jump's window of the past).
|
|
40
|
+
*/
|
|
41
|
+
scrollKey: string | null;
|
|
42
|
+
/** The conversation's messages have loaded; a restore waits for it. */
|
|
43
|
+
ready: boolean;
|
|
44
|
+
/** Index of a cursor jump's target row, or -1. It is always mounted. */
|
|
45
|
+
focusIndex: number;
|
|
46
|
+
/** A turn is streaming; the window is never folded under it. */
|
|
47
|
+
turnInFlight: boolean;
|
|
48
|
+
atBottomThreshold: number;
|
|
49
|
+
threadRef: RefObject<AiChatThreadHandle | null>;
|
|
50
|
+
/** Ask for the page of history above the first loaded row. */
|
|
51
|
+
onLoadHistory?: () => void;
|
|
52
|
+
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What is still to be done to the scroll once the rows it needs are laid out.
|
|
57
|
+
* A restore remembers the list it was set against: that list may still be the
|
|
58
|
+
* previous conversation's, and only a list that changed after it can say the
|
|
59
|
+
* saved row is really gone.
|
|
60
|
+
*/
|
|
61
|
+
type PendingScroll =
|
|
62
|
+
| { kind: 'end' }
|
|
63
|
+
| { kind: 'offset'; position: TimelinePosition; setAgainst: readonly string[] };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A restore is waiting for its conversation's rows: the row it was saved
|
|
67
|
+
* against is not on screen, and the list has not changed since it was set. A
|
|
68
|
+
* list that did change without bringing that row back means the row is gone
|
|
69
|
+
* (edited away, regenerated), and the restore falls back to the newest turn.
|
|
70
|
+
*/
|
|
71
|
+
function awaitingRows(pending: { current: PendingScroll | null }, ids: readonly string[]): boolean {
|
|
72
|
+
const todo = pending.current;
|
|
73
|
+
if (todo?.kind !== 'offset' || ids.includes(todo.position.probeId)) return false;
|
|
74
|
+
if (ids === todo.setAgainst) return true;
|
|
75
|
+
pending.current = { kind: 'end' };
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The state before the first key: nothing mounted yet, so nothing to remember. */
|
|
80
|
+
const MOUNTING = Symbol('mounting');
|
|
81
|
+
|
|
82
|
+
const nextFrame = (run: () => void): void => {
|
|
83
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(run);
|
|
84
|
+
else setTimeout(run, 0);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Which rows the thread mounts, and the scroll that goes with them: the window
|
|
89
|
+
* over a long history, and the position a conversation is returned to.
|
|
90
|
+
*
|
|
91
|
+
* ## The window
|
|
92
|
+
*
|
|
93
|
+
* The newest `INITIAL_ROWS` rows mount first. Near the top, Bloom's
|
|
94
|
+
* `onStartReached` reveals `PAGE_ROWS` more — or, with none left, asks for the
|
|
95
|
+
* thread's history — and `maintainStartPosition` holds the reader on the row
|
|
96
|
+
* they were reading while either lands above. The window folds back to its
|
|
97
|
+
* opening size only when the reader is at the bottom and nothing is streaming.
|
|
98
|
+
* See `runtime/thread/timeline.ts` for why it is this and not virtualization.
|
|
99
|
+
*
|
|
100
|
+
* ## The position
|
|
101
|
+
*
|
|
102
|
+
* Leaving a conversation — by navigating away or by switching the screen to
|
|
103
|
+
* another — saves the offset, whether the reader was at the bottom, and the
|
|
104
|
+
* window's anchor. Coming back restores the anchor first, so the same rows are
|
|
105
|
+
* mounted above the reader, and the offset once they have laid out. Coming
|
|
106
|
+
* back from a cursor jump does not restore: "back to latest" means the latest.
|
|
107
|
+
*/
|
|
108
|
+
export function useTimelineWindow({
|
|
109
|
+
ids,
|
|
110
|
+
scrollKey,
|
|
111
|
+
ready,
|
|
112
|
+
focusIndex,
|
|
113
|
+
turnInFlight,
|
|
114
|
+
atBottomThreshold,
|
|
115
|
+
threadRef,
|
|
116
|
+
onLoadHistory,
|
|
117
|
+
onScroll,
|
|
118
|
+
}: Options) {
|
|
119
|
+
const [state, setState] = useState<{ key: string | null; anchor: TimelineAnchor }>({
|
|
120
|
+
key: scrollKey,
|
|
121
|
+
anchor: TAIL,
|
|
122
|
+
});
|
|
123
|
+
// The render after a key change and before its effect: the new conversation
|
|
124
|
+
// starts from the newest rows until the effect says otherwise.
|
|
125
|
+
const anchor = state.key === scrollKey ? state.anchor : TAIL;
|
|
126
|
+
const start = useMemo(() => resolveStart(anchor, ids, focusIndex), [anchor, ids, focusIndex]);
|
|
127
|
+
|
|
128
|
+
const setAnchor = useCallback(
|
|
129
|
+
(next: TimelineAnchor) => setState((current) => ({ key: current.key, anchor: next })),
|
|
130
|
+
[],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
/** This render's facts, for callbacks and cleanups that outlive it. */
|
|
134
|
+
const latest = useRef({ ids, start, anchor, ready, turnInFlight, onLoadHistory });
|
|
135
|
+
useLayoutEffect(() => {
|
|
136
|
+
latest.current = { ids, start, anchor, ready, turnInFlight, onLoadHistory };
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/** The last position the thread reported for the current key. */
|
|
140
|
+
const lastOffset = useRef<number | null>(null);
|
|
141
|
+
const atBottom = useRef(true);
|
|
142
|
+
const pending = useRef<PendingScroll | null>(null);
|
|
143
|
+
const previousKey = useRef<string | null | typeof MOUNTING>(MOUNTING);
|
|
144
|
+
|
|
145
|
+
useLayoutEffect(() => {
|
|
146
|
+
const cameFrom = previousKey.current;
|
|
147
|
+
previousKey.current = scrollKey;
|
|
148
|
+
lastOffset.current = null;
|
|
149
|
+
atBottom.current = true;
|
|
150
|
+
// Returning from a jump is a request for the present, not for the past.
|
|
151
|
+
// A reader who left from the bottom comes back to the newest page, which
|
|
152
|
+
// is where they were; only a position mid-thread brings its rows back.
|
|
153
|
+
const read =
|
|
154
|
+
scrollKey !== null && cameFrom !== null ? timelinePositions.read(scrollKey) : undefined;
|
|
155
|
+
const saved = read?.atBottom === false ? read : undefined;
|
|
156
|
+
pending.current =
|
|
157
|
+
scrollKey === null
|
|
158
|
+
? null
|
|
159
|
+
: saved === undefined
|
|
160
|
+
? { kind: 'end' }
|
|
161
|
+
: { kind: 'offset', position: saved, setAgainst: ids };
|
|
162
|
+
setState({ key: scrollKey, anchor: saved?.anchor ?? TAIL });
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
if (scrollKey === null) return;
|
|
166
|
+
const { ids: rows, start: first, anchor: kept } = latest.current;
|
|
167
|
+
if (rows.length === 0) return;
|
|
168
|
+
timelinePositions.save(scrollKey, {
|
|
169
|
+
anchor: kept,
|
|
170
|
+
offset: lastOffset.current ?? 0,
|
|
171
|
+
atBottom: atBottom.current,
|
|
172
|
+
probeId: rows[first],
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
}, [scrollKey]);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Pin the opening window to its first row once the rows exist, so a turn
|
|
179
|
+
* appended at the bottom grows the window rather than sliding it — a slide
|
|
180
|
+
* would unmount the top row under a reader who is mid-thread. Re-pinned the
|
|
181
|
+
* same way when the pinned row has gone (the list was replaced) — except
|
|
182
|
+
* while a restore waits for its conversation's rows: the rows on screen are
|
|
183
|
+
* still the last conversation's, and the anchor being restored is not in
|
|
184
|
+
* them yet.
|
|
185
|
+
*/
|
|
186
|
+
useLayoutEffect(() => {
|
|
187
|
+
if (ids.length === 0 || awaitingRows(pending, ids)) return;
|
|
188
|
+
const stale = anchor.kind === 'tail' || (anchor.kind === 'row' && !ids.includes(anchor.id));
|
|
189
|
+
if (!stale) return;
|
|
190
|
+
setState((current) =>
|
|
191
|
+
current.key === scrollKey && current.anchor === anchor
|
|
192
|
+
? { key: current.key, anchor: pinAnchor(ids, start) }
|
|
193
|
+
: current,
|
|
194
|
+
);
|
|
195
|
+
}, [ids, anchor, start, scrollKey]);
|
|
196
|
+
|
|
197
|
+
const handleStartReached = useCallback(() => {
|
|
198
|
+
const { ids: rows, start: first, onLoadHistory: loadHistory } = latest.current;
|
|
199
|
+
const next = revealAbove(rows, first);
|
|
200
|
+
if (next !== null) {
|
|
201
|
+
setAnchor(next);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
setAnchor(TOP);
|
|
205
|
+
loadHistory?.();
|
|
206
|
+
}, [setAnchor]);
|
|
207
|
+
|
|
208
|
+
const handleScroll = useCallback(
|
|
209
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
210
|
+
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
|
211
|
+
lastOffset.current = contentOffset.y;
|
|
212
|
+
const wasAtBottom = atBottom.current;
|
|
213
|
+
atBottom.current =
|
|
214
|
+
contentSize.height - contentOffset.y - layoutMeasurement.height <= atBottomThreshold;
|
|
215
|
+
if (atBottom.current && !wasAtBottom && !latest.current.turnInFlight) {
|
|
216
|
+
const { ids: rows, start: first } = latest.current;
|
|
217
|
+
const folded = collapseToTail(rows, first);
|
|
218
|
+
if (folded !== null) {
|
|
219
|
+
pending.current = { kind: 'end' };
|
|
220
|
+
setAnchor(folded);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
onScroll?.(event);
|
|
224
|
+
},
|
|
225
|
+
[atBottomThreshold, onScroll, setAnchor],
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The rows have laid out: carry out whatever scroll was waiting for them.
|
|
230
|
+
* A frame later, so it lands after the thread's own reaction to the same
|
|
231
|
+
* layout (on web it follows to the end on its first one).
|
|
232
|
+
*/
|
|
233
|
+
const handleListLayout = useCallback(
|
|
234
|
+
(_event: LayoutChangeEvent) => {
|
|
235
|
+
const { ids: rows, ready: loaded } = latest.current;
|
|
236
|
+
if (pending.current === null || !loaded || rows.length === 0) return;
|
|
237
|
+
if (awaitingRows(pending, rows)) return;
|
|
238
|
+
const todo = pending.current;
|
|
239
|
+
pending.current = null;
|
|
240
|
+
nextFrame(() => {
|
|
241
|
+
if (todo.kind === 'end') threadRef.current?.scrollToEnd({ animated: false });
|
|
242
|
+
else threadRef.current?.scrollToOffset({ offset: todo.position.offset });
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
[threadRef],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
/** Index of the first mounted row. */
|
|
250
|
+
start,
|
|
251
|
+
/** For `AiChatThread`: absent when there is nothing above to reveal or load. */
|
|
252
|
+
onStartReached: start > 0 || onLoadHistory !== undefined ? handleStartReached : undefined,
|
|
253
|
+
onScroll: handleScroll,
|
|
254
|
+
onListLayout: handleListLayout,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host side of a voice call, shared by every surface that offers one.
|
|
3
|
+
*
|
|
4
|
+
* It owns whether a call is open, the state it reports back (for the ambient
|
|
5
|
+
* wave, the sound effects and the transcript), the gate before one opens (plan,
|
|
6
|
+
* credits, sign-in: the host answers, the SDK tells the person), and the end of
|
|
7
|
+
* a call that is no longer the screen's — another screen on show, another
|
|
8
|
+
* conversation, another account. `sessionProps` is everything `VoiceSession`
|
|
9
|
+
* needs; the sheet, `AliaChatScreen` and the Alia app all spread it.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here imports the call or its audio engine.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
|
+
import { toast } from '@oxy.so/bloom/toast';
|
|
16
|
+
import { useLabels, type VoiceBlockReason } from '../lib/labels';
|
|
17
|
+
import { isRequestFor, useVoiceCallRequests } from '../lib/voice-call-request';
|
|
18
|
+
import type { VoiceNotice, VoiceSessionProps, VoiceSessionState } from '../types';
|
|
19
|
+
|
|
20
|
+
export interface UseVoiceLauncherOptions {
|
|
21
|
+
/** Alia API base URL. */
|
|
22
|
+
apiUrl?: string;
|
|
23
|
+
/** The conversation open on this screen, which is where the call lives. */
|
|
24
|
+
conversationId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Called after the gate, when no conversation is open: creates (and shows) the one the call
|
|
27
|
+
* will live in and returns its id, or `undefined` when it could not be made (the call then
|
|
28
|
+
* does not open). Every call has a conversation, so its transcript has a home. The call opens
|
|
29
|
+
* on the screen that shows that conversation: this one, or the one the host navigates to.
|
|
30
|
+
*/
|
|
31
|
+
resolveConversation?: () => Promise<string | undefined>;
|
|
32
|
+
/** Whose call it would be. A live call ends when the account, or the open conversation, changes. */
|
|
33
|
+
accountId?: string | null;
|
|
34
|
+
/** The agent the call talks to; omitted, Alia. */
|
|
35
|
+
agentId?: string;
|
|
36
|
+
/** Language spoken (BCP-47); omitted, the platform's. */
|
|
37
|
+
locale?: string;
|
|
38
|
+
/** Which of the product's two voices answers. */
|
|
39
|
+
voice?: 'male' | 'female';
|
|
40
|
+
/** Alia stored messages of the call in the conversation: refresh the thread. */
|
|
41
|
+
onMessagesSaved?: VoiceSessionProps['onMessagesSaved'];
|
|
42
|
+
/**
|
|
43
|
+
* The gate before a call opens: why it may not (no account, no plan, no credits), or `null`.
|
|
44
|
+
* The person is told (`labels.voiceBlocked`); `onVoiceBlocked` adds what only the host can do.
|
|
45
|
+
*/
|
|
46
|
+
canStartVoice?: () => VoiceBlockReason | null;
|
|
47
|
+
/** The call was not opened for `reason`: sign the person in, open the plans… */
|
|
48
|
+
onVoiceBlocked?: (reason: VoiceBlockReason) => void;
|
|
49
|
+
/** Where the call's notices go; omitted, Bloom's toast. */
|
|
50
|
+
onNotice?: (notice: VoiceNotice) => void;
|
|
51
|
+
/** A call is over, however it ended: refresh credits, the thread… */
|
|
52
|
+
onCallEnded?: () => void;
|
|
53
|
+
/** Whether the screen holding the call is the one on show. A live call ends when it is not. */
|
|
54
|
+
isFocused?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface VoiceLauncher {
|
|
58
|
+
/** A call is open (or being opened). */
|
|
59
|
+
isActive: boolean;
|
|
60
|
+
/** What the call reports back; `null` until it is under way. */
|
|
61
|
+
state: VoiceSessionState | null;
|
|
62
|
+
/** Open a call, after the gate. Does nothing while one is open or the screen is not on show. */
|
|
63
|
+
start: () => Promise<void>;
|
|
64
|
+
/** End the call. */
|
|
65
|
+
end: () => void;
|
|
66
|
+
/** Spread onto `VoiceSession` (or `AliaVoiceCall`). */
|
|
67
|
+
sessionProps: VoiceSessionProps;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Bloom's toast, by kind: the default home of a call's notices. */
|
|
71
|
+
export function toastNotice({ kind, text }: VoiceNotice): void {
|
|
72
|
+
if (kind === 'error') toast.error(text);
|
|
73
|
+
else if (kind === 'warning') toast.warning(text);
|
|
74
|
+
else toast.info(text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function useVoiceLauncher(options: UseVoiceLauncherOptions = {}): VoiceLauncher {
|
|
78
|
+
const labels = useLabels();
|
|
79
|
+
const {
|
|
80
|
+
apiUrl,
|
|
81
|
+
conversationId,
|
|
82
|
+
resolveConversation,
|
|
83
|
+
accountId = null,
|
|
84
|
+
agentId,
|
|
85
|
+
locale,
|
|
86
|
+
voice,
|
|
87
|
+
onMessagesSaved,
|
|
88
|
+
canStartVoice,
|
|
89
|
+
onVoiceBlocked,
|
|
90
|
+
onNotice,
|
|
91
|
+
onCallEnded,
|
|
92
|
+
isFocused = true,
|
|
93
|
+
} = options;
|
|
94
|
+
const [isActive, setIsActive] = useState(false);
|
|
95
|
+
const [state, setState] = useState<VoiceSessionState | null>(null);
|
|
96
|
+
const latest = useRef({
|
|
97
|
+
canStartVoice,
|
|
98
|
+
onVoiceBlocked,
|
|
99
|
+
onNotice,
|
|
100
|
+
onCallEnded,
|
|
101
|
+
resolveConversation,
|
|
102
|
+
labels,
|
|
103
|
+
});
|
|
104
|
+
latest.current = {
|
|
105
|
+
canStartVoice,
|
|
106
|
+
onVoiceBlocked,
|
|
107
|
+
onNotice,
|
|
108
|
+
onCallEnded,
|
|
109
|
+
resolveConversation,
|
|
110
|
+
labels,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const notify = useCallback((notice: VoiceNotice) => {
|
|
114
|
+
(latest.current.onNotice ?? toastNotice)(notice);
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const end = useCallback(() => {
|
|
118
|
+
setIsActive(false);
|
|
119
|
+
setState(null);
|
|
120
|
+
latest.current.onCallEnded?.();
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const start = useCallback(async () => {
|
|
124
|
+
if (isActive || !isFocused) return;
|
|
125
|
+
const reason = latest.current.canStartVoice?.() ?? null;
|
|
126
|
+
if (reason !== null) {
|
|
127
|
+
notify({
|
|
128
|
+
kind: reason === 'credits-exhausted' ? 'error' : 'info',
|
|
129
|
+
text: latest.current.labels.voiceBlocked(reason),
|
|
130
|
+
});
|
|
131
|
+
latest.current.onVoiceBlocked?.(reason);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// The gate first: a call that may not open must not leave an empty conversation behind.
|
|
135
|
+
const home = conversationId ?? (await latest.current.resolveConversation?.());
|
|
136
|
+
if (home === undefined) return;
|
|
137
|
+
// The call opens where its conversation is shown (see `useVoiceCallRequests`): here, at once,
|
|
138
|
+
// when it already is; wherever the host takes the new conversation, when it was just made.
|
|
139
|
+
useVoiceCallRequests.getState().ask(home, accountId);
|
|
140
|
+
}, [isActive, isFocused, conversationId, accountId, notify]);
|
|
141
|
+
|
|
142
|
+
const request = useVoiceCallRequests((store) => store.request);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (isActive || !isFocused || !isRequestFor(request, conversationId, accountId)) return;
|
|
145
|
+
useVoiceCallRequests.getState().clear();
|
|
146
|
+
setIsActive(true);
|
|
147
|
+
}, [request, isActive, isFocused, conversationId, accountId]);
|
|
148
|
+
|
|
149
|
+
// A call belongs to one account and one conversation, on the screen that shows it: it ends when
|
|
150
|
+
// the screen goes out of focus, the account changes, or the screen moves to another conversation.
|
|
151
|
+
const openedIn = useRef<{ conversationId: string | undefined; accountId: string | null }>(
|
|
152
|
+
undefined,
|
|
153
|
+
);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!isActive) {
|
|
156
|
+
openedIn.current = undefined;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
openedIn.current ??= { conversationId, accountId };
|
|
160
|
+
const call = openedIn.current;
|
|
161
|
+
if (!isFocused || call.accountId !== accountId || call.conversationId !== conversationId) end();
|
|
162
|
+
}, [isActive, isFocused, accountId, conversationId, end]);
|
|
163
|
+
|
|
164
|
+
const sessionProps = useMemo<VoiceSessionProps>(
|
|
165
|
+
() => ({
|
|
166
|
+
...(apiUrl === undefined ? {} : { apiUrl }),
|
|
167
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
168
|
+
...(agentId === undefined ? {} : { agentId }),
|
|
169
|
+
...(locale === undefined ? {} : { locale }),
|
|
170
|
+
...(voice === undefined ? {} : { voice }),
|
|
171
|
+
...(onMessagesSaved === undefined ? {} : { onMessagesSaved }),
|
|
172
|
+
onNotice: notify,
|
|
173
|
+
onStateChange: setState,
|
|
174
|
+
onEnd: end,
|
|
175
|
+
}),
|
|
176
|
+
[apiUrl, conversationId, agentId, locale, voice, onMessagesSaved, notify, end],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return { isActive, state, start, end, sessionProps };
|
|
180
|
+
}
|
|
@@ -24,8 +24,7 @@ import type { VoiceAudioEngine } from '../lib/voice-audio-engine-types';
|
|
|
24
24
|
import { VoiceCall, type VoiceCallSnapshot, type VoiceSocketFactory } from '../lib/voice-call';
|
|
25
25
|
import { VoiceLevelChannel, type VoiceLevelSource } from '../lib/voice-levels';
|
|
26
26
|
import type { ProductVoice, VoiceCloseReason } from '../lib/voice-protocol';
|
|
27
|
-
|
|
28
|
-
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
27
|
+
import { ALIA_API_URL } from '../lib/origins';
|
|
29
28
|
|
|
30
29
|
export interface UseVoiceRoomOptions {
|
|
31
30
|
apiUrl?: string;
|
|
@@ -44,8 +43,6 @@ export interface UseVoiceRoomOptions {
|
|
|
44
43
|
lang?: string;
|
|
45
44
|
/** Alia stored messages of the call: refresh the thread. */
|
|
46
45
|
onMessagesSaved?: (conversationId: string, messageIds: readonly string[]) => void;
|
|
47
|
-
/** A tool filed an action for the person's OK; its card is in the conversation. */
|
|
48
|
-
onApprovalRequested?: (toolName: string) => void;
|
|
49
46
|
/** Test seams: the device's audio and the socket. */
|
|
50
47
|
createAudioEngine?: () => VoiceAudioEngine;
|
|
51
48
|
openSocket?: VoiceSocketFactory;
|
|
@@ -120,7 +117,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
120
117
|
if (live !== undefined && live !== 'ended' && live !== 'error') return;
|
|
121
118
|
const current = optionsRef.current;
|
|
122
119
|
const call = new VoiceCall({
|
|
123
|
-
apiUrl: current.apiUrl ||
|
|
120
|
+
apiUrl: current.apiUrl || ALIA_API_URL,
|
|
124
121
|
getToken,
|
|
125
122
|
conversationId: current.conversationId ?? newConversationId(),
|
|
126
123
|
...(current.agentId === undefined ? {} : { agentId: current.agentId }),
|
|
@@ -134,7 +131,6 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
134
131
|
},
|
|
135
132
|
onMessagesSaved: (conversationId, ids) =>
|
|
136
133
|
optionsRef.current.onMessagesSaved?.(conversationId, ids),
|
|
137
|
-
onApprovalRequested: (toolName) => optionsRef.current.onApprovalRequested?.(toolName),
|
|
138
134
|
});
|
|
139
135
|
callRef.current = call;
|
|
140
136
|
await call.start();
|
package/src/index.ts
CHANGED
|
@@ -3,17 +3,39 @@
|
|
|
3
3
|
// Reusable chat UI, voice, and streaming components for Oxy apps
|
|
4
4
|
|
|
5
5
|
// ── Main components ──
|
|
6
|
-
export
|
|
6
|
+
export { AliaChatContent } from './components/AliaChatContent';
|
|
7
|
+
export type {
|
|
8
|
+
AliaChatAgentRef,
|
|
9
|
+
AliaChatChrome,
|
|
10
|
+
AliaChatContentProps,
|
|
11
|
+
AliaChatState,
|
|
12
|
+
} from './components/AliaChatContent';
|
|
13
|
+
export { AliaParticipants, joinFlights, useParticipants } from './components/AliaParticipants';
|
|
14
|
+
export type { AliaParticipantsProps, UseParticipantsOptions } from './components/AliaParticipants';
|
|
15
|
+
export { AliaParticipantsHeader } from './components/AliaParticipantsHeader';
|
|
16
|
+
export type { AliaParticipantsHeaderProps } from './components/AliaParticipantsHeader';
|
|
17
|
+
export { ALIA_LEAD, leadOf } from './lib/participants';
|
|
18
|
+
export type { AliaChatParticipants, AliaLead, AliaParticipant } from './lib/participants';
|
|
19
|
+
export { ANCHORS, leadToProfile } from './lib/flights';
|
|
20
|
+
export { parseCatalogueTool, toolRendererKey } from './lib/tool-renderers';
|
|
21
|
+
export type { AliaToolRenderers, AliaToolResult } from './lib/tool-renderers';
|
|
22
|
+
export { AliaAskProvider, useAskAgent } from './components/AliaAskProvider';
|
|
23
|
+
export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './components/AliaAskProvider';
|
|
24
|
+
export { AliaSharedConversation } from './components/AliaSharedConversation';
|
|
25
|
+
export type { AliaSharedConversationProps } from './components/AliaSharedConversation';
|
|
26
|
+
export type { AliaAddMenu } from './lib/add-menu';
|
|
27
|
+
export type { AliaVoiceOptions } from './hooks/useChatVoice';
|
|
7
28
|
export { AliaChatSheet } from './components/AliaChatSheet';
|
|
8
29
|
export type { AliaChatSheetProps, AliaChatSheetRef } from './components/AliaChatSheet';
|
|
9
|
-
export type { AliaTurn } from './
|
|
30
|
+
export type { AliaTurn } from './hooks/useChatComposer';
|
|
10
31
|
export { AliaChatScreen } from './components/AliaChatScreen';
|
|
11
32
|
export type { AliaChatScreenProps } from './components/AliaChatScreen';
|
|
12
33
|
|
|
13
34
|
// ── Chat UI components ──
|
|
14
35
|
export { ALIA_CHARACTER_RUNTIME_URL } from './lib/character-runtime';
|
|
15
36
|
export { DEFAULT_LABELS } from './lib/labels';
|
|
16
|
-
export
|
|
37
|
+
export { AliaChatLabelsProvider, useLabels } from './lib/labels';
|
|
38
|
+
export type { AliaChatLabels, VoiceBlockReason } from './lib/labels';
|
|
17
39
|
export { VoiceModeIcon } from './components/VoiceModeIcon';
|
|
18
40
|
export { AmbientField, PARALLAX_DURATION, PARALLAX_EASE } from './components/AmbientField';
|
|
19
41
|
export type { AmbientFieldProps } from './components/AmbientField';
|
|
@@ -27,6 +49,16 @@ export { IdentityMark } from './components/IdentityMark';
|
|
|
27
49
|
export type { IdentityMarkProps, IdentityMarkState } from './components/IdentityMark';
|
|
28
50
|
export { AliaMarkdown } from './components/Markdown';
|
|
29
51
|
export type { AliaMarkdownProps, RenderCodeBlock } from './components/Markdown';
|
|
52
|
+
export { ReplyMarkdown } from './components/ReplyMarkdown';
|
|
53
|
+
export { AliaThread } from './components/thread/AliaThread';
|
|
54
|
+
export type { AliaThreadProps, NewConversationOfferProps } from './components/thread/AliaThread';
|
|
55
|
+
export { ThoughtPanel } from './components/thought/ThoughtPanel';
|
|
56
|
+
export type { ThoughtPanelProps } from './components/thought/ThoughtPanel';
|
|
57
|
+
export type { OutputActions } from './components/thought/FilesAndSources';
|
|
58
|
+
export { CanvasComponent } from './components/canvas/CanvasComponent';
|
|
59
|
+
export { restoreOpenerFocus } from './lib/focus-return';
|
|
60
|
+
export { buildPowerLevelOptions, POWER_LEVEL_ICONS } from './lib/power-level-options';
|
|
61
|
+
export type { DeviceModel } from './lib/power-level-options';
|
|
30
62
|
export { AliaWelcomeMessage } from './components/AliaWelcomeMessage';
|
|
31
63
|
export { PlanPreviewCard } from './components/PlanPreviewCard';
|
|
32
64
|
export { AskedQuestions, QuestionCard } from './components/QuestionCard';
|
|
@@ -49,11 +81,8 @@ export {
|
|
|
49
81
|
} from './lib/questions';
|
|
50
82
|
export type { AskedOption, AskedQuestion, QuestionAnswer } from './lib/questions';
|
|
51
83
|
|
|
52
|
-
// ──
|
|
53
|
-
export
|
|
54
|
-
export type { UseAliaChatOptions, UseAliaChatReturn } from './hooks/useAliaChat';
|
|
55
|
-
export { AliaChatStreamError } from './lib/chat-stream';
|
|
56
|
-
export type { AliaChatFailure } from './lib/chat-stream';
|
|
84
|
+
// ── Conversation runtime: the one data layer under the sheet, the screen and the Alia app ──
|
|
85
|
+
export * from './runtime';
|
|
57
86
|
|
|
58
87
|
// ── Read aloud and dictation (no call, no audio engine) ──
|
|
59
88
|
export { useTTS } from './hooks/useTTS';
|
|
@@ -67,36 +96,21 @@ export type { VoiceErrorCode } from './lib/speech-messages';
|
|
|
67
96
|
export { SpeechClipError } from './lib/speech-synthesis';
|
|
68
97
|
export type { SpeechClipErrorCode } from './lib/speech-synthesis';
|
|
69
98
|
|
|
70
|
-
// ── Model catalogue (GET /catalogue) ──
|
|
71
|
-
// Exported so a consumer building its own picker reads the real models the
|
|
72
|
-
// server offers; the SDK itself ships no model identifier.
|
|
73
|
-
export {
|
|
74
|
-
fetchCatalogue,
|
|
75
|
-
parseCatalogue,
|
|
76
|
-
resolveSelection,
|
|
77
|
-
resolveModelId,
|
|
78
|
-
clearCatalogueCache,
|
|
79
|
-
} from './lib/catalogue';
|
|
80
|
-
export type { Catalogue, CatalogueEntry, ModelSelection, ReasoningEffort } from './lib/catalogue';
|
|
81
|
-
|
|
82
99
|
// ── Types ──
|
|
83
100
|
export type {
|
|
84
|
-
ChatMessage,
|
|
85
|
-
ToolInvocation,
|
|
86
101
|
AliaAttachment,
|
|
87
102
|
AliaSuggestion,
|
|
88
103
|
ResearchProgress,
|
|
89
104
|
ResearchSource,
|
|
90
|
-
|
|
105
|
+
MessagePlan,
|
|
91
106
|
PlanStep,
|
|
92
107
|
} from './types';
|
|
93
108
|
|
|
94
|
-
// ── Voice
|
|
95
|
-
export type {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} from './types';
|
|
109
|
+
// ── Voice: part of the conversation screen; the host brings callbacks (see `AliaVoiceOptions`) ──
|
|
110
|
+
export type { VoiceNotice, VoiceSessionProps, VoiceSessionState } from './types';
|
|
111
|
+
export { AliaVoiceCall } from './components/voice/AliaVoiceCall';
|
|
112
|
+
export { useVoiceLauncher, toastNotice } from './hooks/useVoiceLauncher';
|
|
113
|
+
export type { UseVoiceLauncherOptions, VoiceLauncher } from './hooks/useVoiceLauncher';
|
|
100
114
|
|
|
101
115
|
// ── UI Primitives (NativeWind) ──
|
|
102
116
|
export { cn, formatFileSize } from './lib/utils';
|
|
@@ -6,7 +6,7 @@ type Sources = Readonly<Record<string, AliaAvatarState>>;
|
|
|
6
6
|
/**
|
|
7
7
|
* What Alia is doing, from every place that can know. Two sources feed it and neither owns it:
|
|
8
8
|
*
|
|
9
|
-
* - `local`: the turns running in THIS app, one entry per `
|
|
9
|
+
* - `local`: the turns running in THIS app, one entry per `useConversationThread` instance;
|
|
10
10
|
* - `remote`: runs the server reports for the person, one entry per agent session, so a button
|
|
11
11
|
* reacts to work that is not in any open chat.
|
|
12
12
|
*
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
|
|
2
|
+
|
|
3
|
+
/** The rows of the composer's add menu, and what pressing one does. */
|
|
4
|
+
export interface AliaAddMenu {
|
|
5
|
+
groups: readonly ComposerPanelAddMenuGroup[];
|
|
6
|
+
onSelect: (rowId: string) => void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the thread shows while a call is open: the text messages it held when
|
|
3
|
+
* the call started, then the call's messages.
|
|
4
|
+
*
|
|
5
|
+
* A call message the server has stored is shown AS STORED — the copy a reload
|
|
6
|
+
* or a resume shows, with its tools' arguments and results, which the call's
|
|
7
|
+
* socket never carries. Same id either way (the call names every message by the
|
|
8
|
+
* id it is stored under), so swapping one for the other never adds a message.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Message } from '../runtime/types';
|
|
12
|
+
import type { VoiceMessage } from '../types';
|
|
13
|
+
|
|
14
|
+
export function callThread(
|
|
15
|
+
before: readonly Message[],
|
|
16
|
+
call: readonly VoiceMessage[],
|
|
17
|
+
stored: readonly Message[] | undefined,
|
|
18
|
+
): Message[] {
|
|
19
|
+
const storedById = new Map((stored ?? []).map((message) => [message.id, message]));
|
|
20
|
+
return [...before, ...call.map((message) => storedById.get(message.id) ?? fromCall(message))];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fromCall(message: VoiceMessage): Message {
|
|
24
|
+
return {
|
|
25
|
+
id: message.id,
|
|
26
|
+
role: message.role,
|
|
27
|
+
content: message.content,
|
|
28
|
+
toolInvocations: message.toolInvocations,
|
|
29
|
+
createdAt: new Date(message.timestamp).toISOString(),
|
|
30
|
+
source: 'voice' as const,
|
|
31
|
+
speaker: message.speaker,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { ALIA_WEB_ORIGIN } from './origins';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Where the 3D character runtime (Alia's avatar) is served from when the host does not say.
|
|
3
5
|
*
|
|
4
6
|
* The runtime is ~214 MB of engine and data, so the chat loads it from Alia's own web origin
|
|
5
7
|
* instead of asking every app to host a copy. That origin serves it with CORS open
|
|
6
|
-
* (`packages/app/worker/index.js`), which a cross-origin module import needs;
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* (`packages/app/worker/index.js`), which a cross-origin module import needs; a host with a
|
|
9
|
+
* CSP allows it with `@alia.onl/sdk/csp`. A host that serves its own copy passes
|
|
10
|
+
* `characterRuntimeUrl`.
|
|
9
11
|
*/
|
|
10
|
-
export const ALIA_CHARACTER_RUNTIME_URL =
|
|
12
|
+
export const ALIA_CHARACTER_RUNTIME_URL = `${ALIA_WEB_ORIGIN}/bloom-character/runtime.mjs`;
|