@alia.onl/sdk 10.1.1 → 11.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 +53 -0
- package/README.md +24 -0
- package/package.json +4 -6
- package/src/components/AliaAvatar.tsx +66 -0
- package/src/components/AliaChatContent.tsx +235 -112
- package/src/components/AliaChatHeader.tsx +118 -0
- package/src/components/AliaChatMessageList.tsx +171 -260
- package/src/components/AliaChatScreen.tsx +23 -24
- package/src/components/AliaChatSheet.tsx +70 -316
- package/src/components/AliaWelcomeMessage.tsx +47 -50
- package/src/components/AmbientField.tsx +577 -0
- package/src/components/IdentityMark.tsx +10 -2
- package/src/components/Markdown.tsx +116 -40
- package/src/components/PlanPreviewCard.tsx +71 -55
- package/src/components/ThinkingBlock.tsx +47 -0
- package/src/components/VoiceModeIcon.tsx +10 -0
- package/src/components/voice/VoiceControls.tsx +22 -73
- package/src/components/voice/VoiceSession.tsx +2 -1
- package/src/hooks/useAliaChat.ts +22 -15
- package/src/hooks/useAmbientWave.ts +35 -5
- package/src/hooks/useSoundEffects.ts +14 -11
- package/src/hooks/useSpeechToText.ts +9 -2
- package/src/hooks/useTTS.ts +163 -156
- package/src/hooks/useVoiceRoom.ts +228 -170
- package/src/index.ts +18 -6
- package/src/lib/catalogue.ts +17 -4
- package/src/lib/chat-stream.ts +25 -7
- package/src/lib/chat-transport.ts +2 -1
- package/src/lib/citations.ts +63 -0
- package/src/lib/content-utils.ts +3 -2
- package/src/lib/image-data-url.ts +15 -0
- package/src/lib/keyboard.tsx +1 -1
- package/src/lib/labels.tsx +81 -0
- package/src/lib/speech-recognition.native.ts +15 -4
- package/src/lib/speech-recognition.ts +27 -7
- package/src/lib/task-labels.ts +67 -0
- package/src/lib/tool-call-status.ts +28 -0
- package/src/lib/tool-registry.ts +14 -14
- package/src/lib/voice-text.ts +12 -6
- package/src/lib/voice-turn.ts +4 -1
- package/src/lib/work-log-copy.ts +61 -0
- package/src/lib/work-log.ts +378 -0
- package/src/tools.ts +11 -0
- package/src/types.ts +5 -2
- package/src/voice.ts +12 -4
- package/src/components/Reasoning.tsx +0 -152
- package/src/components/ResearchProgressCard.tsx +0 -159
- package/src/components/ThinkingIndicator.tsx +0 -98
- package/src/components/ui/button.tsx +0 -104
- package/src/components/ui/chat-text-input.tsx +0 -184
- package/src/components/ui/dropdown-menu.tsx +0 -2
- package/src/components/ui/dropdown-menu.web.tsx +0 -383
- package/src/components/ui/prompt-input/actions.tsx +0 -28
- package/src/components/ui/prompt-input/add-menu.tsx +0 -83
- package/src/components/ui/prompt-input/attachments.tsx +0 -181
- package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
- package/src/components/ui/prompt-input/context.tsx +0 -71
- package/src/components/ui/prompt-input/mic-button.tsx +0 -53
- package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
- package/src/components/ui/prompt-input/submit-button.tsx +0 -50
- package/src/components/ui/prompt-input/textarea.tsx +0 -80
- package/src/components/ui/text.tsx +0 -29
- package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
- package/src/components/voice/VoiceOverlay.tsx +0 -75
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RiBookOpenLine,
|
|
3
|
+
RiFileTextLine,
|
|
4
|
+
RiGlobalLine,
|
|
5
|
+
RiSearchLine,
|
|
6
|
+
RiTerminalBoxLine,
|
|
7
|
+
RiToolsLine,
|
|
8
|
+
} from '@oxy.so/bloom/icons';
|
|
9
|
+
import type { TaskListChip, TaskListStep, TaskListTask } from '@oxy.so/bloom/task-list';
|
|
10
|
+
import type { WebSearchBrand, WebSearchSource, WebSearchStep } from '@oxy.so/bloom/web-search';
|
|
11
|
+
import type { ToolInvocation } from '../types';
|
|
12
|
+
import { extractCitationSources } from './citations';
|
|
13
|
+
import { getToolDoneLabel, getToolPillLabel } from './task-labels';
|
|
14
|
+
import { toolCallStatus } from './tool-call-status';
|
|
15
|
+
import { englishWorkLog, type Translate } from './work-log-copy';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A turn's tool calls, as the two Bloom logs draw them.
|
|
19
|
+
*
|
|
20
|
+
* Where the turn WENT (searches, page visits, research) is a `WebSearch`
|
|
21
|
+
* trail; what it DID with every other tool is a `TaskList`. Calls that return
|
|
22
|
+
* a card (weather, market…) are neither — the caller filters them out first.
|
|
23
|
+
*
|
|
24
|
+
* Both logs count UNITS and are driven by `revealed`, never by their own
|
|
25
|
+
* ticker: a unit counts as revealed only when the event that produced it has
|
|
26
|
+
* arrived. A call whose arguments are still streaming (`partial-call`) is a
|
|
27
|
+
* step that is known but not yet revealed, so the log shimmers on it and shows
|
|
28
|
+
* its Working tail; a step's sources are a unit that exists only once the
|
|
29
|
+
* result carried them.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** What a host can swap: the words, and where a site's icon is served from. */
|
|
33
|
+
export interface WorkLogOptions {
|
|
34
|
+
/** Translator for the log's words (see `work-log-copy.ts`). Default: English. */
|
|
35
|
+
t?: Translate;
|
|
36
|
+
/** The URL of a site's cached icon, or nothing for Bloom's quiet dot. */
|
|
37
|
+
faviconUrl?: (url: string) => string | null | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The result of a call, when it is an object (the tools return objects). */
|
|
41
|
+
function resultOf(inv: Pick<ToolInvocation, 'result'>): Record<string, unknown> | undefined {
|
|
42
|
+
const result = inv.result;
|
|
43
|
+
return result !== null && typeof result === 'object' && !Array.isArray(result)
|
|
44
|
+
? (result as Record<string, unknown>)
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The research the live stream reports before the answer is persisted. */
|
|
49
|
+
export interface LiveResearch {
|
|
50
|
+
currentQuery?: string;
|
|
51
|
+
sourcesFound?: number;
|
|
52
|
+
sources?: Array<{ id?: unknown; url?: unknown; title?: unknown }> | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Tools whose calls are a place the turn went, not a task it did. */
|
|
56
|
+
const WEB_TOOLS = new Set(['webSearch', 'browse', 'webScraper', 'deepResearch']);
|
|
57
|
+
|
|
58
|
+
export function isWebInvocation(inv: Pick<ToolInvocation, 'toolName'>): boolean {
|
|
59
|
+
return WEB_TOOLS.has(inv.toolName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function str(value: unknown): string | undefined {
|
|
63
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function domainOf(url: string): string {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(url).hostname;
|
|
69
|
+
} catch {
|
|
70
|
+
return url;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The site marks Bloom draws, by registrable domain. Anything else is a quiet dot. */
|
|
75
|
+
const BRANDS: Array<[string, WebSearchBrand]> = [
|
|
76
|
+
['github.com', 'github'],
|
|
77
|
+
['gitlab.com', 'gitlab'],
|
|
78
|
+
['bitbucket.org', 'bitbucket'],
|
|
79
|
+
['x.com', 'x'],
|
|
80
|
+
['twitter.com', 'x'],
|
|
81
|
+
['reddit.com', 'reddit'],
|
|
82
|
+
['facebook.com', 'facebook'],
|
|
83
|
+
['instagram.com', 'instagram'],
|
|
84
|
+
['linkedin.com', 'linkedin'],
|
|
85
|
+
['tiktok.com', 'tiktok'],
|
|
86
|
+
['spotify.com', 'spotify'],
|
|
87
|
+
['twitch.tv', 'twitch'],
|
|
88
|
+
['notion.so', 'notion'],
|
|
89
|
+
['notion.site', 'notion'],
|
|
90
|
+
['figma.com', 'figma'],
|
|
91
|
+
['apple.com', 'apple'],
|
|
92
|
+
['microsoft.com', 'microsoft'],
|
|
93
|
+
['google.com', 'google'],
|
|
94
|
+
['discord.com', 'discord'],
|
|
95
|
+
['slack.com', 'slack'],
|
|
96
|
+
['dropbox.com', 'dropbox'],
|
|
97
|
+
['telegram.org', 'telegram'],
|
|
98
|
+
['t.me', 'telegram'],
|
|
99
|
+
['whatsapp.com', 'whatsapp'],
|
|
100
|
+
['amazon.com', 'amazon'],
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
function brandOf(domain: string): WebSearchBrand | undefined {
|
|
104
|
+
const host = domain.toLowerCase();
|
|
105
|
+
return BRANDS.find(([site]) => host === site || host.endsWith(`.${site}`))?.[1];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One source row. Its favicon is Clarity's copy of the site's icon: the one a
|
|
110
|
+
* search result carries, or the URL Clarity serves it at for anything else
|
|
111
|
+
* (a page Alia read). Bloom draws a brand mark over it, and the dot when
|
|
112
|
+
* Clarity has not fetched that site yet.
|
|
113
|
+
*/
|
|
114
|
+
function source(
|
|
115
|
+
url: string,
|
|
116
|
+
options: WorkLogOptions,
|
|
117
|
+
title?: string,
|
|
118
|
+
faviconUrl?: string,
|
|
119
|
+
): WebSearchSource {
|
|
120
|
+
const domain = domainOf(url);
|
|
121
|
+
const brand = brandOf(domain);
|
|
122
|
+
const favicon = faviconUrl ?? options.faviconUrl?.(url);
|
|
123
|
+
return {
|
|
124
|
+
title: title ?? domain,
|
|
125
|
+
domain,
|
|
126
|
+
href: url,
|
|
127
|
+
...(brand ? { brand } : {}),
|
|
128
|
+
...(favicon ? { faviconUrl: favicon } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function uniqueSources(list: WebSearchSource[]): WebSearchSource[] {
|
|
133
|
+
const seen = new Set<string>();
|
|
134
|
+
return list.filter((s) => {
|
|
135
|
+
const key = s.href ?? s.title;
|
|
136
|
+
if (seen.has(key)) return false;
|
|
137
|
+
seen.add(key);
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function succeeded(inv: ToolInvocation): boolean {
|
|
143
|
+
return toolCallStatus(inv, true) === 'done';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isSearch(inv: ToolInvocation): boolean {
|
|
147
|
+
if (inv.toolName === 'webSearch') return true;
|
|
148
|
+
const action = str(resultOf(inv)?.action) ?? str(inv.args?.action);
|
|
149
|
+
return inv.toolName === 'browse' && action === 'search';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One step, and whether the event that produced it has arrived. */
|
|
153
|
+
interface MappedStep {
|
|
154
|
+
step: WebSearchStep;
|
|
155
|
+
arrived: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function webStep(
|
|
159
|
+
inv: ToolInvocation,
|
|
160
|
+
live: LiveResearch | undefined,
|
|
161
|
+
options: WorkLogOptions,
|
|
162
|
+
): MappedStep {
|
|
163
|
+
const t = options.t ?? englishWorkLog;
|
|
164
|
+
const arrived = inv.state !== 'partial-call';
|
|
165
|
+
const ok = succeeded(inv);
|
|
166
|
+
const res = resultOf(inv);
|
|
167
|
+
|
|
168
|
+
if (inv.toolName === 'deepResearch') {
|
|
169
|
+
const cited = extractCitationSources([inv], live?.sources);
|
|
170
|
+
const searches = typeof res?.totalSearches === 'number' ? res.totalSearches : 0;
|
|
171
|
+
return {
|
|
172
|
+
arrived,
|
|
173
|
+
step: {
|
|
174
|
+
label: t('chat.bloom.researched'),
|
|
175
|
+
query: str(inv.args?.query),
|
|
176
|
+
icon: RiBookOpenLine,
|
|
177
|
+
...(searches > 0 ? { meta: t('chat.bloom.searchCount', { count: searches }) } : {}),
|
|
178
|
+
...(cited.length > 0 ? { sources: cited.map((c) => source(c.url, options, c.title)) } : {}),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (isSearch(inv)) {
|
|
184
|
+
const results: unknown[] = ok && Array.isArray(res?.results) ? res.results : [];
|
|
185
|
+
const sources = uniqueSources(
|
|
186
|
+
results.flatMap((r) => {
|
|
187
|
+
const url = str((r as { url?: unknown })?.url);
|
|
188
|
+
return url
|
|
189
|
+
? [
|
|
190
|
+
source(
|
|
191
|
+
url,
|
|
192
|
+
options,
|
|
193
|
+
str((r as { title?: unknown }).title),
|
|
194
|
+
str((r as { faviconUrl?: unknown }).faviconUrl),
|
|
195
|
+
),
|
|
196
|
+
]
|
|
197
|
+
: [];
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
const count = typeof res?.count === 'number' ? res.count : results.length;
|
|
201
|
+
return {
|
|
202
|
+
arrived,
|
|
203
|
+
step: {
|
|
204
|
+
label: t('chat.bloom.searchedWeb'),
|
|
205
|
+
query: str(inv.args?.query),
|
|
206
|
+
icon: RiSearchLine,
|
|
207
|
+
// Say what came back, including nothing: a search that found nothing or
|
|
208
|
+
// failed must not look like one whose sources are missing.
|
|
209
|
+
...(inv.state !== 'result'
|
|
210
|
+
? {}
|
|
211
|
+
: !ok
|
|
212
|
+
? { meta: t('chat.bloom.searchFailed') }
|
|
213
|
+
: count === 0
|
|
214
|
+
? { meta: t('chat.bloom.noResults') }
|
|
215
|
+
: { meta: t('chat.bloom.resultCount', { count }) }),
|
|
216
|
+
...(sources.length > 0 ? { sources } : {}),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A page visit: `webScraper`, or `browse` reading a URL.
|
|
222
|
+
const url = str(res?.url) ?? str(inv.args?.url);
|
|
223
|
+
return {
|
|
224
|
+
arrived,
|
|
225
|
+
step: {
|
|
226
|
+
label: t('chat.bloom.visited'),
|
|
227
|
+
query: url ? domainOf(url) : undefined,
|
|
228
|
+
icon: RiGlobalLine,
|
|
229
|
+
...(ok && url && inv.state === 'result'
|
|
230
|
+
? { sources: [source(url, options, str(res?.title))] }
|
|
231
|
+
: {}),
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** How many leading units have arrived. Units are revealed in order, so the first missing one stops the count. */
|
|
237
|
+
function arrivedUnits(mapped: MappedStep[]): number {
|
|
238
|
+
let revealed = 0;
|
|
239
|
+
for (const { step, arrived } of mapped) {
|
|
240
|
+
if (!arrived) break;
|
|
241
|
+
revealed += step.sources?.length ? 2 : 1;
|
|
242
|
+
}
|
|
243
|
+
return revealed;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface WebSearchLog {
|
|
247
|
+
steps: WebSearchStep[];
|
|
248
|
+
revealed: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The `WebSearch` trail of a turn: one step per search, page visit or
|
|
253
|
+
* research run, with the sources each surfaced.
|
|
254
|
+
*
|
|
255
|
+
* `research` is the live `researchProgress` of a research answer. Before the
|
|
256
|
+
* answer is saved there is no `deepResearch` invocation yet, so the progress
|
|
257
|
+
* is the step; once it is saved the invocation is, and the live sources only
|
|
258
|
+
* fill in what the persisted ones lack.
|
|
259
|
+
*/
|
|
260
|
+
export function webSearchLog(
|
|
261
|
+
invocations: readonly ToolInvocation[] | undefined,
|
|
262
|
+
research: LiveResearch | undefined,
|
|
263
|
+
options: WorkLogOptions = {},
|
|
264
|
+
): WebSearchLog {
|
|
265
|
+
const t = options.t ?? englishWorkLog;
|
|
266
|
+
const web = (invocations ?? []).filter(isWebInvocation);
|
|
267
|
+
const mapped = web.map((inv) => webStep(inv, research, options));
|
|
268
|
+
|
|
269
|
+
const hasResearchCall = web.some((inv) => inv.toolName === 'deepResearch');
|
|
270
|
+
if (research && !hasResearchCall) {
|
|
271
|
+
const cited = extractCitationSources(undefined, research.sources);
|
|
272
|
+
const found = typeof research.sourcesFound === 'number' ? research.sourcesFound : cited.length;
|
|
273
|
+
mapped.push({
|
|
274
|
+
arrived: true,
|
|
275
|
+
step: {
|
|
276
|
+
label: t('chat.bloom.researching'),
|
|
277
|
+
query: str(research.currentQuery),
|
|
278
|
+
icon: RiBookOpenLine,
|
|
279
|
+
...(found > 0 ? { meta: t('chat.bloom.sourceCount', { count: found }) } : {}),
|
|
280
|
+
...(cited.length > 0 ? { sources: cited.map((c) => source(c.url, options, c.title)) } : {}),
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { steps: mapped.map((m) => m.step), revealed: arrivedUnits(mapped) };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Argument keys worth a chip, in the order they are shown. */
|
|
289
|
+
const CHIP_KEYS = [
|
|
290
|
+
'filename',
|
|
291
|
+
'path',
|
|
292
|
+
'file',
|
|
293
|
+
'command',
|
|
294
|
+
'url',
|
|
295
|
+
'query',
|
|
296
|
+
'name',
|
|
297
|
+
'title',
|
|
298
|
+
'subject',
|
|
299
|
+
'to',
|
|
300
|
+
'handle',
|
|
301
|
+
];
|
|
302
|
+
const CHIP_MAX = 60;
|
|
303
|
+
|
|
304
|
+
function argChips(args: Record<string, unknown> | undefined): TaskListChip[] {
|
|
305
|
+
if (!args) return [];
|
|
306
|
+
const chips: TaskListChip[] = [];
|
|
307
|
+
for (const key of CHIP_KEYS) {
|
|
308
|
+
const value = str(args[key]);
|
|
309
|
+
if (value && value.length <= CHIP_MAX) chips.push({ label: value });
|
|
310
|
+
if (chips.length === 3) break;
|
|
311
|
+
}
|
|
312
|
+
return chips;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const TASK_ICONS: Record<string, NonNullable<TaskListTask['icon']>> = {
|
|
316
|
+
generateFile: RiFileTextLine,
|
|
317
|
+
fileEdit: RiFileTextLine,
|
|
318
|
+
shellExec: RiTerminalBoxLine,
|
|
319
|
+
codeInterpreter: RiTerminalBoxLine,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export interface TaskListLog {
|
|
323
|
+
tasks: TaskListTask[];
|
|
324
|
+
revealed: number;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The `TaskList` of a turn: one task per call that is neither a web step nor
|
|
329
|
+
* a card, titled from `task-utils.ts`.
|
|
330
|
+
*
|
|
331
|
+
* Steps come from what the call really carried: its arguments as chips once
|
|
332
|
+
* the call is made (the file name for `generateFile`), and how it ended once
|
|
333
|
+
* the result is in — an error, or a call the turn left behind (`live` false
|
|
334
|
+
* with no result). A successful call adds no "Done" row: the swap from the
|
|
335
|
+
* running title to the finished one already says it.
|
|
336
|
+
*/
|
|
337
|
+
export function taskListLog(
|
|
338
|
+
invocations: readonly ToolInvocation[] | undefined,
|
|
339
|
+
live: boolean,
|
|
340
|
+
options: WorkLogOptions = {},
|
|
341
|
+
): TaskListLog {
|
|
342
|
+
const t = options.t ?? englishWorkLog;
|
|
343
|
+
let revealed = 0;
|
|
344
|
+
let open = true;
|
|
345
|
+
const tasks = (invocations ?? [])
|
|
346
|
+
.filter((inv) => !isWebInvocation(inv))
|
|
347
|
+
.map((inv): TaskListTask => {
|
|
348
|
+
const status = toolCallStatus(inv, live);
|
|
349
|
+
const steps: TaskListStep[] = [];
|
|
350
|
+
|
|
351
|
+
if (inv.toolName === 'generateFile') {
|
|
352
|
+
const name = str(resultOf(inv)?.filename) ?? str(inv.args?.filename);
|
|
353
|
+
if (name)
|
|
354
|
+
steps.push({
|
|
355
|
+
label: t(status === 'done' ? 'chat.bloom.createdFile' : 'chat.bloom.writingFile'),
|
|
356
|
+
chips: [{ label: name }],
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
const chips = argChips(inv.args);
|
|
360
|
+
if (chips.length > 0) steps.push({ label: t('chat.bloom.taskInput'), chips });
|
|
361
|
+
}
|
|
362
|
+
if (status === 'error') steps.push({ label: t('chat.bloom.taskFailed') });
|
|
363
|
+
if (status === 'interrupted') steps.push({ label: t('chat.bloom.taskStopped') });
|
|
364
|
+
|
|
365
|
+
// Header and argument steps land with the call; the outcome with the result.
|
|
366
|
+
const arrived = inv.state !== 'partial-call';
|
|
367
|
+
if (open && arrived) revealed += 1 + steps.length;
|
|
368
|
+
else open = false;
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
title: getToolDoneLabel(inv.toolName, t),
|
|
372
|
+
runningTitle: getToolPillLabel(inv.toolName, t),
|
|
373
|
+
icon: TASK_ICONS[inv.toolName] ?? RiToolsLine,
|
|
374
|
+
steps,
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
return { tasks, revealed };
|
|
378
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @alia.onl/sdk/tools — what a turn's tool calls mean, without the UI.
|
|
2
|
+
// Pure functions: the status of a call, the numbered sources of a research answer, the
|
|
3
|
+
// words for a tool. They load no React Native, so a host's models and tests can use them.
|
|
4
|
+
|
|
5
|
+
export { extractCitationSources } from './lib/citations';
|
|
6
|
+
export type { CitationSource } from './lib/citations';
|
|
7
|
+
export { getToolDoneLabel, getToolPillLabel } from './lib/task-labels';
|
|
8
|
+
export { toolCallStatus } from './lib/tool-call-status';
|
|
9
|
+
export type { ToolCallStatus } from './lib/tool-call-status';
|
|
10
|
+
export { englishWorkLog } from './lib/work-log-copy';
|
|
11
|
+
export type { Translate } from './lib/work-log-copy';
|
package/src/types.ts
CHANGED
|
@@ -115,8 +115,11 @@ export interface VoiceSessionProps {
|
|
|
115
115
|
apiUrl?: string;
|
|
116
116
|
/** Called on every room, agent, or transcript change. */
|
|
117
117
|
onStateChange: (state: VoiceSessionState) => void;
|
|
118
|
-
/**
|
|
119
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The call is over — the user ended it, it errored, or the room dropped. When it
|
|
120
|
+
* failed, say why, so the shell can tell the person instead of silently going back.
|
|
121
|
+
*/
|
|
122
|
+
onEnd: (error?: string) => void;
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
/**
|
package/src/voice.ts
CHANGED
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
// ── Voice components ──
|
|
7
7
|
export { VoiceSession } from './components/voice/VoiceSession';
|
|
8
|
-
export { AudioWaveVisualizer } from './components/voice/AudioWaveVisualizer';
|
|
9
|
-
export { VoiceOverlay } from './components/voice/VoiceOverlay';
|
|
10
8
|
export { VoiceControls } from './components/voice/VoiceControls';
|
|
11
9
|
export { VOICE_CONTROLS_LABELS } from './components/voice/VoiceControls';
|
|
12
10
|
export type { VoiceControlsLabels, VoiceControlsProps } from './components/voice/VoiceControls';
|
|
@@ -15,14 +13,24 @@ export type { VoiceControlsLabels, VoiceControlsProps } from './components/voice
|
|
|
15
13
|
export { useVoiceRoom } from './hooks/useVoiceRoom';
|
|
16
14
|
export type { UseVoiceRoomOptions } from './hooks/useVoiceRoom';
|
|
17
15
|
export { createAliaVoiceTurnSender } from './lib/voice-turn';
|
|
18
|
-
export type {
|
|
16
|
+
export type {
|
|
17
|
+
VoiceTurn,
|
|
18
|
+
VoiceTurnMessage,
|
|
19
|
+
VoiceTurnSender,
|
|
20
|
+
AliaVoiceTurnSenderOptions,
|
|
21
|
+
} from './lib/voice-turn';
|
|
19
22
|
export type { VoiceLevelSource } from './lib/voice-levels';
|
|
20
23
|
export { VOICE_ERROR_MESSAGES } from './lib/speech-messages';
|
|
21
24
|
export type { VoiceErrorCode } from './lib/speech-messages';
|
|
22
25
|
export { useAudioLevelMonitor } from './hooks/useAudioLevelMonitor';
|
|
23
26
|
export { useAudioLevels } from './hooks/useAudioLevels';
|
|
24
27
|
export { useAmbientWave } from './hooks/useAmbientWave';
|
|
25
|
-
export type {
|
|
28
|
+
export type {
|
|
29
|
+
UseAmbientWaveOptions,
|
|
30
|
+
UseAmbientWaveResult,
|
|
31
|
+
AmbientWaveVoice,
|
|
32
|
+
AmbientWaveMode,
|
|
33
|
+
} from './hooks/useAmbientWave';
|
|
26
34
|
export { useSoundEffects, useVoiceSoundEffects } from './hooks/useSoundEffects';
|
|
27
35
|
export type { SoundName, SoundSources } from './hooks/useSoundEffects';
|
|
28
36
|
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { View, Text, Pressable, StyleSheet } from 'react-native';
|
|
3
|
-
import Animated, {
|
|
4
|
-
useAnimatedStyle,
|
|
5
|
-
useSharedValue,
|
|
6
|
-
withRepeat,
|
|
7
|
-
withTiming,
|
|
8
|
-
withSequence,
|
|
9
|
-
cancelAnimation,
|
|
10
|
-
} from 'react-native-reanimated';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// Context
|
|
14
|
-
type ReasoningContextType = {
|
|
15
|
-
isStreaming: boolean;
|
|
16
|
-
isOpen: boolean;
|
|
17
|
-
setIsOpen: (open: boolean) => void;
|
|
18
|
-
duration: number | undefined;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const ReasoningContext = createContext<ReasoningContextType | null>(null);
|
|
22
|
-
|
|
23
|
-
function useReasoning() {
|
|
24
|
-
const context = useContext(ReasoningContext);
|
|
25
|
-
if (!context) throw new Error('useReasoning must be used within a Reasoning component');
|
|
26
|
-
return context;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Props
|
|
30
|
-
interface ReasoningProps {
|
|
31
|
-
isStreaming?: boolean;
|
|
32
|
-
defaultOpen?: boolean;
|
|
33
|
-
duration?: number;
|
|
34
|
-
children: React.ReactNode;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ReasoningTriggerProps {
|
|
38
|
-
onPress?: () => void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface ReasoningContentProps {
|
|
42
|
-
children: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Reasoning wrapper — manages open/close state with auto-open during streaming
|
|
46
|
-
export function Reasoning({ isStreaming = false, defaultOpen = true, duration: externalDuration, children }: ReasoningProps) {
|
|
47
|
-
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
48
|
-
const [duration, setDuration] = useState<number | undefined>(externalDuration);
|
|
49
|
-
const startTimeRef = useRef<number | null>(null);
|
|
50
|
-
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (isStreaming) {
|
|
54
|
-
setIsOpen(true);
|
|
55
|
-
startTimeRef.current = Date.now();
|
|
56
|
-
intervalRef.current = setInterval(() => {
|
|
57
|
-
if (startTimeRef.current) {
|
|
58
|
-
setDuration(Math.floor((Date.now() - startTimeRef.current) / 1000));
|
|
59
|
-
}
|
|
60
|
-
}, 1000);
|
|
61
|
-
} else {
|
|
62
|
-
if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; }
|
|
63
|
-
if (startTimeRef.current !== null) {
|
|
64
|
-
const t = setTimeout(() => setIsOpen(false), 500);
|
|
65
|
-
return () => clearTimeout(t);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
|
|
69
|
-
}, [isStreaming]);
|
|
70
|
-
|
|
71
|
-
useEffect(() => { if (externalDuration !== undefined) setDuration(externalDuration); }, [externalDuration]);
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<ReasoningContext.Provider value={{ isStreaming, isOpen, setIsOpen, duration }}>
|
|
75
|
-
<View>{children}</View>
|
|
76
|
-
</ReasoningContext.Provider>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Trigger — shows "Thinking for Xs..." label with pulsing icon
|
|
81
|
-
export function ReasoningTrigger({ onPress }: ReasoningTriggerProps) {
|
|
82
|
-
const { isStreaming, isOpen, setIsOpen, duration } = useReasoning();
|
|
83
|
-
const pulseOpacity = useSharedValue(1);
|
|
84
|
-
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
if (isStreaming) {
|
|
87
|
-
pulseOpacity.value = withRepeat(withSequence(withTiming(0.4, { duration: 800 }), withTiming(1, { duration: 800 })), -1, false);
|
|
88
|
-
} else {
|
|
89
|
-
cancelAnimation(pulseOpacity);
|
|
90
|
-
pulseOpacity.value = 1;
|
|
91
|
-
}
|
|
92
|
-
}, [isStreaming]);
|
|
93
|
-
|
|
94
|
-
const pulseStyle = useAnimatedStyle(() => ({ opacity: pulseOpacity.value }));
|
|
95
|
-
|
|
96
|
-
const message = isStreaming
|
|
97
|
-
? (duration ? `Thinking for ${duration}s...` : 'Thinking...')
|
|
98
|
-
: (duration ? `Thought for ${duration} seconds` : 'Reasoning');
|
|
99
|
-
|
|
100
|
-
const handlePress = onPress || (() => setIsOpen(!isOpen));
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<Pressable onPress={handlePress} style={styles.trigger}>
|
|
104
|
-
<Animated.View style={isStreaming ? pulseStyle : undefined}>
|
|
105
|
-
<Text style={{ color: '#a855f7', fontSize: 14 }}>{'\u2726'}</Text>
|
|
106
|
-
</Animated.View>
|
|
107
|
-
<Text style={[styles.triggerText, { color: '#a855f7' }]}>{message}</Text>
|
|
108
|
-
<Text style={{ color: '#a855f7', fontSize: 14 }}>{onPress ? '\u203A' : (isOpen ? '\u25BE' : '\u25B8')}</Text>
|
|
109
|
-
</Pressable>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Content — renders thinking text (collapsible)
|
|
114
|
-
export function ReasoningContent({ children }: ReasoningContentProps) {
|
|
115
|
-
const { isOpen, isStreaming } = useReasoning();
|
|
116
|
-
|
|
117
|
-
if (!isOpen) return null;
|
|
118
|
-
|
|
119
|
-
// Lazy import AliaMarkdown to avoid circular dependency
|
|
120
|
-
const { AliaMarkdown } = require('./Markdown');
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<View style={[styles.contentWrapper, { backgroundColor: '#a855f710', borderColor: '#a855f730' }]}>
|
|
124
|
-
<View style={{ opacity: isStreaming ? 0.8 : 1 }}>
|
|
125
|
-
<AliaMarkdown content={children} />
|
|
126
|
-
</View>
|
|
127
|
-
</View>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const styles = StyleSheet.create({
|
|
132
|
-
trigger: {
|
|
133
|
-
flexDirection: 'row',
|
|
134
|
-
alignItems: 'center',
|
|
135
|
-
gap: 8,
|
|
136
|
-
paddingVertical: 8,
|
|
137
|
-
paddingHorizontal: 12,
|
|
138
|
-
borderRadius: 8,
|
|
139
|
-
},
|
|
140
|
-
triggerText: {
|
|
141
|
-
fontSize: 14,
|
|
142
|
-
fontWeight: '500',
|
|
143
|
-
flex: 1,
|
|
144
|
-
},
|
|
145
|
-
contentWrapper: {
|
|
146
|
-
borderRadius: 8,
|
|
147
|
-
padding: 12,
|
|
148
|
-
borderWidth: 1,
|
|
149
|
-
marginHorizontal: 12,
|
|
150
|
-
marginBottom: 12,
|
|
151
|
-
},
|
|
152
|
-
});
|