@alia.onl/sdk 10.1.1 → 11.1.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +24 -0
  3. package/package.json +4 -6
  4. package/src/components/AliaAvatar.tsx +66 -0
  5. package/src/components/AliaChatContent.tsx +236 -112
  6. package/src/components/AliaChatHeader.tsx +118 -0
  7. package/src/components/AliaChatMessageList.tsx +197 -260
  8. package/src/components/AliaChatScreen.tsx +23 -24
  9. package/src/components/AliaChatSheet.tsx +70 -316
  10. package/src/components/AliaWelcomeMessage.tsx +47 -50
  11. package/src/components/AmbientField.tsx +577 -0
  12. package/src/components/IdentityMark.tsx +10 -2
  13. package/src/components/Markdown.tsx +116 -40
  14. package/src/components/PlanPreviewCard.tsx +71 -55
  15. package/src/components/QuestionCard.tsx +62 -0
  16. package/src/components/ThinkingBlock.tsx +47 -0
  17. package/src/components/VoiceModeIcon.tsx +10 -0
  18. package/src/components/voice/VoiceControls.tsx +22 -73
  19. package/src/components/voice/VoiceSession.tsx +2 -1
  20. package/src/hooks/useAliaChat.ts +22 -15
  21. package/src/hooks/useAmbientWave.ts +35 -5
  22. package/src/hooks/useSoundEffects.ts +14 -11
  23. package/src/hooks/useSpeechToText.ts +9 -2
  24. package/src/hooks/useTTS.ts +163 -156
  25. package/src/hooks/useVoiceRoom.ts +228 -170
  26. package/src/index.ts +26 -6
  27. package/src/lib/catalogue.ts +17 -4
  28. package/src/lib/chat-stream.ts +25 -7
  29. package/src/lib/chat-transport.ts +2 -1
  30. package/src/lib/citations.ts +63 -0
  31. package/src/lib/content-utils.ts +3 -2
  32. package/src/lib/image-data-url.ts +15 -0
  33. package/src/lib/keyboard.tsx +1 -1
  34. package/src/lib/labels.tsx +81 -0
  35. package/src/lib/questions.ts +78 -0
  36. package/src/lib/speech-recognition.native.ts +15 -4
  37. package/src/lib/speech-recognition.ts +27 -7
  38. package/src/lib/task-labels.ts +67 -0
  39. package/src/lib/tool-call-status.ts +28 -0
  40. package/src/lib/tool-registry.ts +14 -14
  41. package/src/lib/voice-text.ts +12 -6
  42. package/src/lib/voice-turn.ts +4 -1
  43. package/src/lib/work-log-copy.ts +61 -0
  44. package/src/lib/work-log.ts +378 -0
  45. package/src/tools.ts +18 -0
  46. package/src/types.ts +5 -2
  47. package/src/voice.ts +12 -4
  48. package/src/components/Reasoning.tsx +0 -152
  49. package/src/components/ResearchProgressCard.tsx +0 -159
  50. package/src/components/ThinkingIndicator.tsx +0 -98
  51. package/src/components/ui/button.tsx +0 -104
  52. package/src/components/ui/chat-text-input.tsx +0 -184
  53. package/src/components/ui/dropdown-menu.tsx +0 -2
  54. package/src/components/ui/dropdown-menu.web.tsx +0 -383
  55. package/src/components/ui/prompt-input/actions.tsx +0 -28
  56. package/src/components/ui/prompt-input/add-menu.tsx +0 -83
  57. package/src/components/ui/prompt-input/attachments.tsx +0 -181
  58. package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
  59. package/src/components/ui/prompt-input/context.tsx +0 -71
  60. package/src/components/ui/prompt-input/mic-button.tsx +0 -53
  61. package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
  62. package/src/components/ui/prompt-input/submit-button.tsx +0 -50
  63. package/src/components/ui/prompt-input/textarea.tsx +0 -80
  64. package/src/components/ui/text.tsx +0 -29
  65. package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
  66. package/src/components/voice/VoiceOverlay.tsx +0 -75
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Bottom voice control bar that replaces the composer when voice mode is active.
3
- * Contains status text and mute/end buttons.
3
+ * Contains status text and Bloom's mute/end call controls.
4
4
  */
5
5
 
6
- import { View, Text, Pressable, ActivityIndicator, StyleSheet } from 'react-native';
7
- import Mic from 'lucide-react-native/icons/mic';
8
- import MicOff from 'lucide-react-native/icons/mic-off';
9
- import PhoneOff from 'lucide-react-native/icons/phone-off';
6
+ import { CallControls } from '@oxy.so/bloom/call-ui';
7
+ import { Loading } from '@oxy.so/bloom/loading';
8
+ import { Text } from '@oxy.so/bloom/typography';
9
+ import { View } from 'react-native';
10
10
  import type { RoomState, AgentState } from '../../types';
11
11
 
12
12
  /** Every word the bar shows or announces. An app in another language passes its own. */
@@ -40,8 +40,6 @@ export interface VoiceControlsProps {
40
40
  isMuted: boolean;
41
41
  onToggleMute: () => void;
42
42
  onEnd: () => void;
43
- /** Override theme primary color (resolved hex/hsl value, not CSS var) */
44
- primaryColor?: string;
45
43
  /** Overrides for any of {@link VOICE_CONTROLS_LABELS}. */
46
44
  labels?: Partial<VoiceControlsLabels>;
47
45
  }
@@ -68,84 +66,35 @@ export function VoiceControls({
68
66
  isMuted,
69
67
  onToggleMute,
70
68
  onEnd,
71
- primaryColor,
72
69
  labels: labelOverrides,
73
70
  }: VoiceControlsProps) {
74
71
  const labels = { ...VOICE_CONTROLS_LABELS, ...labelOverrides };
75
72
  const statusText = getStatusText(roomState, agentState, isMuted, labels);
76
73
 
77
74
  return (
78
- <View style={styles.container}>
75
+ <View className="items-center gap-4 pb-6 pt-4">
79
76
  {statusText ? (
80
- <Text className="text-lg font-medium mb-4 text-foreground" accessibilityLiveRegion="polite" aria-live="polite">
77
+ <Text
78
+ className="text-lg font-medium text-text-primary"
79
+ accessibilityLiveRegion="polite"
80
+ aria-live="polite"
81
+ >
81
82
  {statusText}
82
83
  </Text>
83
84
  ) : null}
84
85
 
85
- {roomState === 'connected' && (
86
- <View style={styles.buttonRow}>
87
- <View style={styles.buttonWrapper}>
88
- <Pressable
89
- onPress={onToggleMute}
90
- accessibilityRole="button"
91
- accessibilityLabel={labels.mute}
92
- accessibilityState={{ selected: isMuted }}
93
- aria-pressed={isMuted}
94
- className={isMuted ? undefined : 'bg-muted'}
95
- style={[styles.button, isMuted ? { backgroundColor: '#ef4444' } : undefined]}
96
- >
97
- {isMuted ? (
98
- <MicOff size={24} color="white" />
99
- ) : (
100
- <Mic size={24} color="white" />
101
- )}
102
- </Pressable>
103
- <Text className="text-xs text-muted-foreground">
104
- {isMuted ? labels.unmute : labels.mute}
105
- </Text>
106
- </View>
107
-
108
- <View style={styles.buttonWrapper}>
109
- <Pressable
110
- onPress={onEnd}
111
- accessibilityRole="button"
112
- accessibilityLabel={labels.end}
113
- style={[styles.button, { backgroundColor: '#ef4444' }]}
114
- >
115
- <PhoneOff size={24} color="white" />
116
- </Pressable>
117
- <Text className="text-xs text-muted-foreground">{labels.end}</Text>
118
- </View>
119
- </View>
120
- )}
86
+ {roomState === 'connected' ? (
87
+ <CallControls
88
+ muted={isMuted}
89
+ onMutedChange={onToggleMute}
90
+ onEndCall={onEnd}
91
+ // TODO(bloom): `CallControls` draws its captions for a dark call stage, so on the
92
+ // chat's light surface they vanish. The buttons keep their accessible names.
93
+ labels={{ mute: labels.mute, unmute: labels.unmute, endCall: labels.end }}
94
+ />
95
+ ) : null}
121
96
 
122
- {roomState === 'connecting' && (
123
- <ActivityIndicator size="large" className="text-muted-foreground" />
124
- )}
97
+ {roomState === 'connecting' ? <Loading accessibilityLabel={labels.connecting} /> : null}
125
98
  </View>
126
99
  );
127
100
  }
128
-
129
- const styles = StyleSheet.create({
130
- container: {
131
- alignItems: 'center',
132
- paddingBottom: 24,
133
- paddingTop: 16,
134
- },
135
- buttonRow: {
136
- flexDirection: 'row',
137
- alignItems: 'center',
138
- gap: 32,
139
- },
140
- buttonWrapper: {
141
- alignItems: 'center',
142
- gap: 8,
143
- },
144
- button: {
145
- width: 56,
146
- height: 56,
147
- borderRadius: 28,
148
- alignItems: 'center',
149
- justifyContent: 'center',
150
- },
151
- });
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { useEffect, useRef } from 'react';
13
13
  import { useVoiceRoom } from '../../hooks/useVoiceRoom';
14
+ import { VOICE_ERROR_MESSAGES } from '../../lib/speech-messages';
14
15
  import { useAudioLevelMonitor } from '../../hooks/useAudioLevelMonitor';
15
16
  import { useAudioLevels } from '../../hooks/useAudioLevels';
16
17
  import { VoiceControls } from './VoiceControls';
@@ -57,7 +58,7 @@ export function VoiceSession({ apiUrl, onStateChange, onEnd }: VoiceSessionProps
57
58
  useEffect(() => {
58
59
  if (roomState === 'connected') reachedRoomRef.current = true;
59
60
  if (error || roomState === 'error') {
60
- onEnd();
61
+ onEnd(error ?? VOICE_ERROR_MESSAGES['turn-failed']);
61
62
  return;
62
63
  }
63
64
  if (roomState === 'disconnected' && reachedRoomRef.current) onEnd();
@@ -1,7 +1,7 @@
1
1
  import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { useOxy } from '@oxy.so/services/ui/client';
3
3
  import type { ChatMessage, ToolInvocation } from '../types';
4
- import { getTextFromContent } from '../lib/content-utils';
4
+ import { getTextFromContent, type MessageContent } from '../lib/content-utils';
5
5
  import { resolveModelId } from '../lib/catalogue';
6
6
  import { streamAliaChat } from '../lib/chat-transport';
7
7
  import type { AliaChatStreamEvent } from '../lib/chat-stream';
@@ -45,10 +45,15 @@ export interface UseAliaChatOptions {
45
45
  accessToken?: string;
46
46
  }
47
47
 
48
+ export interface SendOptions {
49
+ images?: ReadonlyArray<string>;
50
+ }
51
+
48
52
  export interface UseAliaChatReturn {
49
53
  messages: ChatMessage[];
50
54
  setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>;
51
- send: (text: string) => void;
55
+ /** `images` are `data:` or `https:` URLs sent with the turn as `image_url` parts. */
56
+ send: (text: string, options?: SendOptions) => void;
52
57
  isStreaming: boolean;
53
58
  stop: () => void;
54
59
  clear: () => void;
@@ -112,12 +117,7 @@ function waitWithAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>
112
117
  * handler during its recorded compatibility window.
113
118
  */
114
119
  export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn {
115
- const {
116
- apiUrl = API_URL,
117
- model,
118
- clientContext,
119
- accessToken: accessTokenProp,
120
- } = options;
120
+ const { apiUrl = API_URL, model, clientContext, accessToken: accessTokenProp } = options;
121
121
  const requestUrl = '/v1/chat/completions';
122
122
 
123
123
  const { oxyServices } = useOxy();
@@ -231,9 +231,10 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
231
231
  }, [cancelPendingTimer]);
232
232
 
233
233
  const send = useCallback(
234
- async (text: string) => {
234
+ async (text: string, options?: SendOptions) => {
235
235
  const trimmed = text.trim();
236
- if (!trimmed) return;
236
+ const images = options?.images ?? [];
237
+ if (!trimmed && images.length === 0) return;
237
238
 
238
239
  const activeToken = oxyServices.session.accessToken;
239
240
  if (accessTokenProp !== undefined && accessTokenProp !== activeToken) {
@@ -253,10 +254,17 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
253
254
  flushPendingUpdates(previousPending);
254
255
  }
255
256
 
257
+ const userContent: MessageContent =
258
+ images.length > 0
259
+ ? [
260
+ ...(trimmed ? [{ type: 'text', text: trimmed }] : []),
261
+ ...images.map((url) => ({ type: 'image_url', image_url: { url } })),
262
+ ]
263
+ : trimmed;
256
264
  const userMessage: ChatMessage = {
257
265
  id: generateId(),
258
266
  role: 'user',
259
- content: trimmed,
267
+ content: userContent,
260
268
  createdAt: Date.now(),
261
269
  };
262
270
  const assistantMessage: ChatMessage = {
@@ -281,7 +289,7 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
281
289
  setError(null);
282
290
  setIsStreaming(true);
283
291
 
284
- const apiMessages: Array<{ role: string; content: string }> = [];
292
+ const apiMessages: Array<{ role: string; content: MessageContent }> = [];
285
293
  if (clientContext) apiMessages.push({ role: 'system', content: clientContext });
286
294
  for (const message of history) {
287
295
  if (message.role === 'system') continue;
@@ -295,7 +303,7 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
295
303
  }
296
304
  }
297
305
  }
298
- apiMessages.push({ role: 'user', content: trimmed });
306
+ apiMessages.push({ role: 'user', content: userContent });
299
307
 
300
308
  const pending: PendingUpdates = {
301
309
  assistantId: assistantMessage.id,
@@ -351,8 +359,7 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
351
359
  updateAssistant(pending.assistantId, (last) => ({
352
360
  researchProgress: {
353
361
  ...event.progress,
354
- subQuestions:
355
- event.progress.subQuestions ?? last.researchProgress?.subQuestions,
362
+ subQuestions: event.progress.subQuestions ?? last.researchProgress?.subQuestions,
356
363
  },
357
364
  }));
358
365
  return;
@@ -60,7 +60,12 @@ const ACTIVE_INTENSITY = 0.35;
60
60
  * reanimated-web; the combiner is a pure-read `useDerivedValue`. Effect-free — no
61
61
  * React `useEffect`.
62
62
  */
63
- export function useAmbientWave({ voice, isTTSPlaying, ttsWaveAmplitude, isGenerating }: UseAmbientWaveOptions): UseAmbientWaveResult {
63
+ export function useAmbientWave({
64
+ voice,
65
+ isTTSPlaying,
66
+ ttsWaveAmplitude,
67
+ isGenerating,
68
+ }: UseAmbientWaveOptions): UseAmbientWaveResult {
64
69
  // ── Idle breath: subtle 0.04↔0.09 swell (~7s), started once on mount ──
65
70
  const idleBreath = useSharedValue(0.04);
66
71
  useAnimatedReaction(
@@ -131,14 +136,26 @@ export function useAmbientWave({ voice, isTTSPlaying, ttsWaveAmplitude, isGenera
131
136
  // ── Combined amplitude — pure reads only (ticks on web) ──
132
137
  const voiceAmplitude = voice?.waveAmplitude;
133
138
  const waveAmplitude = useDerivedValue(
134
- () => Math.max(idleBreath.value, voiceAmplitude ? voiceAmplitude.value : 0, ttsWaveAmplitude.value, stt.value, thinkingAmp.value),
139
+ () =>
140
+ Math.max(
141
+ idleBreath.value,
142
+ voiceAmplitude ? voiceAmplitude.value : 0,
143
+ ttsWaveAmplitude.value,
144
+ stt.value,
145
+ thinkingAmp.value,
146
+ ),
135
147
  [voiceAmplitude, ttsWaveAmplitude],
136
148
  );
137
149
 
138
150
  // ── Mode → palette state + overlay intensity (plain derived) ──
139
151
  return useMemo<UseAmbientWaveResult>(() => {
140
152
  if (voice?.isActive && voice.isConnected) {
141
- return { waveAmplitude, agentState: voice.agentState, intensity: ACTIVE_INTENSITY, mode: 'voice' };
153
+ return {
154
+ waveAmplitude,
155
+ agentState: voice.agentState,
156
+ intensity: ACTIVE_INTENSITY,
157
+ mode: 'voice',
158
+ };
142
159
  }
143
160
  if (isTTSPlaying) {
144
161
  return { waveAmplitude, agentState: 'speaking', intensity: ACTIVE_INTENSITY, mode: 'tts' };
@@ -147,8 +164,21 @@ export function useAmbientWave({ voice, isTTSPlaying, ttsWaveAmplitude, isGenera
147
164
  return { waveAmplitude, agentState: 'listening', intensity: ACTIVE_INTENSITY, mode: 'stt' };
148
165
  }
149
166
  if (isGenerating) {
150
- return { waveAmplitude, agentState: 'thinking', intensity: THINKING_INTENSITY, mode: 'thinking' };
167
+ return {
168
+ waveAmplitude,
169
+ agentState: 'thinking',
170
+ intensity: THINKING_INTENSITY,
171
+ mode: 'thinking',
172
+ };
151
173
  }
152
174
  return { waveAmplitude, agentState: 'idle', intensity: IDLE_INTENSITY, mode: 'idle' };
153
- }, [waveAmplitude, voice?.isActive, voice?.isConnected, voice?.agentState, isTTSPlaying, sttIsRecording, isGenerating]);
175
+ }, [
176
+ waveAmplitude,
177
+ voice?.isActive,
178
+ voice?.isConnected,
179
+ voice?.agentState,
180
+ isTTSPlaying,
181
+ sttIsRecording,
182
+ isGenerating,
183
+ ]);
154
184
  }
@@ -70,18 +70,21 @@ export function useSoundEffects(enabled: boolean = false, sounds: SoundSources =
70
70
  };
71
71
  }, [enabled, sounds]);
72
72
 
73
- const play = useCallback((sound: SoundName) => {
74
- if (!enabled) return;
75
- const player = playersRef.current.get(sound);
76
- if (player) {
77
- try {
78
- player.seekTo(0);
79
- player.play();
80
- } catch {
81
- // Playback failed — ignore
73
+ const play = useCallback(
74
+ (sound: SoundName) => {
75
+ if (!enabled) return;
76
+ const player = playersRef.current.get(sound);
77
+ if (player) {
78
+ try {
79
+ player.seekTo(0);
80
+ player.play();
81
+ } catch {
82
+ // Playback failed — ignore
83
+ }
82
84
  }
83
- }
84
- }, [enabled]);
85
+ },
86
+ [enabled],
87
+ );
85
88
 
86
89
  return { play };
87
90
  }
@@ -8,7 +8,11 @@ import {
8
8
  } from '../lib/speech-recognition';
9
9
  import type { SpeechRecognitionSession } from '../lib/speech-recognition-types';
10
10
  import type { SpeechRecognizerChoice } from '../lib/speech-recognition-types';
11
- import { speechFailureCode, VOICE_ERROR_MESSAGES, type VoiceErrorCode } from '../lib/speech-messages';
11
+ import {
12
+ speechFailureCode,
13
+ VOICE_ERROR_MESSAGES,
14
+ type VoiceErrorCode,
15
+ } from '../lib/speech-messages';
12
16
  import { defaultSpeechLanguage } from '../lib/speech-language';
13
17
 
14
18
  // ============== OPTIONS ==============
@@ -81,7 +85,10 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
81
85
  const currentTextRef = useRef('');
82
86
  const restartsRef = useRef(0);
83
87
  /** The recognizer and tag this dictation uses, chosen once when it starts. */
84
- const recognizerRef = useRef<{ lang: string; service?: string; silent: boolean }>({ lang, silent: false });
88
+ const recognizerRef = useRef<{ lang: string; service?: string; silent: boolean }>({
89
+ lang,
90
+ silent: false,
91
+ });
85
92
  const endWaitersRef = useRef<Array<() => void>>([]);
86
93
  const lastMeteringRef = useRef(0);
87
94
  const mountedRef = useRef(true);