@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.
- package/CHANGELOG.md +67 -0
- package/README.md +24 -0
- package/package.json +4 -6
- package/src/components/AliaAvatar.tsx +66 -0
- package/src/components/AliaChatContent.tsx +236 -112
- package/src/components/AliaChatHeader.tsx +118 -0
- package/src/components/AliaChatMessageList.tsx +197 -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/QuestionCard.tsx +62 -0
- 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 +26 -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/questions.ts +78 -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 +18 -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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# @alia.onl/sdk
|
|
2
2
|
|
|
3
|
+
## 11.1.0
|
|
4
|
+
|
|
5
|
+
Alia can ask clarifying questions, as ChatGPT and Claude do.
|
|
6
|
+
|
|
7
|
+
- **New:** `QuestionCard` (Bloom's `Questionnaire`: single or multiple choice, a free-text
|
|
8
|
+
"Other", one question per step), drawn under a reply that asked. `AliaChatMessageList`
|
|
9
|
+
renders it from the turn's `askUserQuestion` tool call; `AliaChatContent` sends the
|
|
10
|
+
answers as the person's next message (`onAnswerQuestions` on the list, for hosts that
|
|
11
|
+
compose their own).
|
|
12
|
+
- **New, pure** (root entry and `@alia.onl/sdk/tools`): `askedQuestions`, `formatAnswers`,
|
|
13
|
+
`isQuestionInvocation`, `ASK_USER_QUESTION_TOOL`. The question is the stored tool call, so
|
|
14
|
+
it survives a reload; it is answered once any later message exists, typed or picked.
|
|
15
|
+
- The turn **ends** at the card: the server stops after the call and writes no answer under it.
|
|
16
|
+
|
|
17
|
+
## 11.0.0
|
|
18
|
+
|
|
19
|
+
The chat UI is Bloom's, not a copy of it. Breaking:
|
|
20
|
+
|
|
21
|
+
- **Bloom is a peer dependency** (`@oxy.so/bloom ^9.0.0`; with `@oxy.so/services`
|
|
22
|
+
11.2.0-oxy-one.5 and `@oxy.so/core` 4.5.0-oxy-one.1 it is the newest set that
|
|
23
|
+
satisfies every peer range). The host must mount `BloomProvider`.
|
|
24
|
+
- **Alia's avatar is Bloom's 3D character.** The host mounts `AgentAvatarProvider`
|
|
25
|
+
with the URL of the hosted `character-runtime` (see the README). New:
|
|
26
|
+
`AliaAvatar`. `IdentityMark` stays only for an agent's likeness.
|
|
27
|
+
- **Removed exports:** `Button`, `buttonVariants`, `buttonTextVariants`, `Text`,
|
|
28
|
+
`TextClassContext` (use `@oxy.so/bloom/button` and `typography`), and
|
|
29
|
+
`ThinkingIndicator` (`AgentThinking`) and `Reasoning`, `ReasoningTrigger`,
|
|
30
|
+
`ReasoningContent` (the reasoning block is `AgentThinking` with children). `zeego`, `class-variance-authority` and
|
|
31
|
+
both `@radix-ui` packages are no longer dependencies.
|
|
32
|
+
- **Composer:** Bloom's `ComposerPill` with a Photos entry in its add menu; the picked photos are its own `attachments`, not a strip mounted above it. Photos are
|
|
33
|
+
now actually sent, as `image_url` parts: `useAliaChat().send(text, { images })`
|
|
34
|
+
(the old composer collected attachments and never sent them). The autocomplete
|
|
35
|
+
props (`useSuggestions`, `onSuggestionUsed`) are gone; nobody passed them.
|
|
36
|
+
- **Sheet:** `AliaChatSheet` is Bloom's `BottomSheet`, detached, radius 40. The
|
|
37
|
+
header is Bloom's `PageHeader` (avatar over a name capsule; press it for a wave).
|
|
38
|
+
`AliaChatScreen` uses the same header; `headerLeft` and `headerRight` still work.
|
|
39
|
+
- **Chat parts:** the thread uses `AiChatUserMessage` and `AiChatFeedbackRow`, replies
|
|
40
|
+
render code through Bloom's `CodeBlock`, research through `WebSearch`, and
|
|
41
|
+
reasoning through `AgentThinking`. The welcome is the app's: the greeting at
|
|
42
|
+
display size and the suggestions as compact rows.
|
|
43
|
+
- **Voice controls:** `VoiceControls` is Bloom's `CallControls`; its `primaryColor` prop
|
|
44
|
+
is gone (Bloom paints from the theme). The plan card is `Card`, `Item` and `Button`;
|
|
45
|
+
editing a message uses Bloom's `Textarea`; the agent line shows a Bloom `Avatar`.
|
|
46
|
+
Colours come from Bloom's theme and tokens, not hard-coded values or the host's
|
|
47
|
+
shadcn classes. `lucide-react-native` and `expo-blur` are no longer used here.
|
|
48
|
+
- **The turn's work is drawn as the app draws it.** Tool calls are Bloom's `TaskList` and
|
|
49
|
+
`WebSearch`, driven by `revealed` from the calls that have really arrived, through one
|
|
50
|
+
shared mapping: `webSearchLog`, `taskListLog` and `isWebInvocation` (root entry), and the
|
|
51
|
+
pure `@alia.onl/sdk/tools` entry (`toolCallStatus`, `extractCitationSources`,
|
|
52
|
+
`getToolDoneLabel`, `getToolPillLabel`), which the app now imports instead of keeping
|
|
53
|
+
copies. `ResearchProgressCard` and the `onToolResultPress` prop are gone: research is a
|
|
54
|
+
step of the `WebSearch` trail, and a tool row is not a button. The words of the log are
|
|
55
|
+
English unless the host passes `labels.workLog` (a translator keyed like the app's).
|
|
56
|
+
- **One animated background.** The ambient gradient (three blurred, drifting colour fields
|
|
57
|
+
that answer the live audio level) is the app's `AmbientField`, now exported by the SDK
|
|
58
|
+
with `PARALLAX_DURATION` and `PARALLAX_EASE`; the chat draws it as its container's
|
|
59
|
+
background and the app imports it from here. `VoiceOverlay`, `AudioWaveVisualizer` and the
|
|
60
|
+
`primaryColor` props that fed them are gone.
|
|
61
|
+
- **Errors:** a failed voice call or dictation goes to `onError` when the host gives one and
|
|
62
|
+
to Bloom's `toast` otherwise (the host needs a `ToastOutlet`; `OxyProvider` mounts one). A
|
|
63
|
+
voice session reports why it ended: `onEnd(error?)`.
|
|
64
|
+
- **Thread:** the messages are a Bloom `AiChatThread` inside `AiChatContainer` with
|
|
65
|
+
floating chrome, so the conversation scrolls behind the header and the composer with
|
|
66
|
+
Bloom's edge fades. The `scrollOffsetY` plumbing is gone.
|
|
67
|
+
- **New:** `labels` on `AliaChatSheet` and `AliaChatScreen` to translate the chat's
|
|
68
|
+
own copy; `AliaChatLabels` and `DEFAULT_LABELS` are exported.
|
|
69
|
+
|
|
3
70
|
## 10.1.1
|
|
4
71
|
|
|
5
72
|
`ChatTextInput` no longer passes `placeholderClassName`. No styling runtime ever
|
package/README.md
CHANGED
|
@@ -22,6 +22,30 @@ listed under `peerDependencies` in `package.json` are yours to install. Metro
|
|
|
22
22
|
must resolve package `exports` (`resolver.unstable_enablePackageExports = true`,
|
|
23
23
|
the default in Expo SDK 53+), because every icon import is a subpath.
|
|
24
24
|
|
|
25
|
+
The chat is built from Bloom (`@oxy.so/bloom`, a peer dependency): mount Bloom's
|
|
26
|
+
`BloomProvider` above it, with the one `KeyboardProvider` your app already has.
|
|
27
|
+
|
|
28
|
+
Alia's avatar is Bloom's 3D character (`AgentAvatar`), which draws through a
|
|
29
|
+
runtime Bloom ships as static files (about 11 MB). Serve the package's
|
|
30
|
+
`assets/character-runtime/` directory unchanged and give the URL of `runtime.mjs`
|
|
31
|
+
to Bloom's provider, once, near the root:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { AgentAvatarProvider } from '@oxy.so/bloom/agent-avatar';
|
|
35
|
+
|
|
36
|
+
<AgentAvatarProvider runtimeUrl="/bloom-character/runtime.mjs">…</AgentAvatarProvider>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Without it the avatar shows Bloom's "unavailable" state. Native avatars run in a
|
|
40
|
+
WebView, so there the URL must be absolute and reachable from the device.
|
|
41
|
+
|
|
42
|
+
Failures (a voice call that cannot start, a dictation error) are shown with Bloom's `toast`,
|
|
43
|
+
so the app needs a `ToastOutlet` (`OxyProvider` mounts one), or pass `onError` to handle them.
|
|
44
|
+
|
|
45
|
+
The chat's own copy is English. Pass `labels` to `AliaChatSheet` or
|
|
46
|
+
`AliaChatScreen` to translate it (`AliaChatLabels`; anything omitted stays
|
|
47
|
+
English). Bloom's own strings follow `BloomProvider`'s `locale`.
|
|
48
|
+
|
|
25
49
|
Two entry points:
|
|
26
50
|
|
|
27
51
|
- `@alia.onl/sdk` — text chat, dictation, read-aloud, catalogue, primitives.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia.onl/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"description": "Alia AI chat SDK — reusable chat UI, voice, and streaming components for Oxy apps",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.ts",
|
|
11
11
|
"./content": "./src/lib/content-utils.ts",
|
|
12
|
+
"./tools": "./src/tools.ts",
|
|
12
13
|
"./voice": "./src/voice.ts",
|
|
13
14
|
"./package.json": "./package.json"
|
|
14
15
|
},
|
|
@@ -29,19 +30,16 @@
|
|
|
29
30
|
"check:entries": "node scripts/check-entry-isolation.mjs"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@radix-ui/react-dropdown-menu": "^2.1.20",
|
|
33
|
-
"@radix-ui/react-icons": "^1.3.2",
|
|
34
|
-
"class-variance-authority": "^0.7.1",
|
|
35
33
|
"clsx": "^2.1.1",
|
|
36
|
-
"lucide-react-native": "^1.24.0",
|
|
37
34
|
"react-native-markdown-display": "^7.0.2",
|
|
38
35
|
"tailwind-merge": "^3.6.0",
|
|
39
|
-
"zeego": "^3.0.6",
|
|
40
36
|
"zustand": "^5.0.15"
|
|
41
37
|
},
|
|
42
38
|
"peerDependencies": {
|
|
43
39
|
"react": "^19.2.3",
|
|
44
40
|
"react-native": "^0.86.0",
|
|
41
|
+
"@expo/vector-icons": "^15.1.1",
|
|
42
|
+
"@oxy.so/bloom": "^9.0.0",
|
|
45
43
|
"@oxy.so/core": "^3.0.0 || ^4.0.0",
|
|
46
44
|
"@oxy.so/services": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
|
47
45
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AgentAvatar, type AvatarConfig, DEFAULT_CONFIG } from '@oxy.so/bloom/agent-avatar';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
/** What Alia is doing; the avatar plays a working turn each time it starts to think or work. */
|
|
5
|
+
export type AliaAvatarState = 'idle' | 'thinking' | 'working' | 'writing';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Alia's likeness in Bloom's 3D character engine: the six-lobed flower in orchid,
|
|
9
|
+
* plain oval eyes, nothing on its head. The engine ids come from Bloom's catalog
|
|
10
|
+
* (`character-shapes.ts`, `character-colors.ts`); change them here and nowhere else.
|
|
11
|
+
*/
|
|
12
|
+
const ALIA_AVATAR: AvatarConfig = {
|
|
13
|
+
...DEFAULT_CONFIG,
|
|
14
|
+
character: {
|
|
15
|
+
preset: 'blue_beret',
|
|
16
|
+
selections: {
|
|
17
|
+
shape: 'six_lobed_flower',
|
|
18
|
+
color: 'orchid',
|
|
19
|
+
eyes: 'oval',
|
|
20
|
+
eyewear: 'none',
|
|
21
|
+
accessory: 'none',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const BUSY: ReadonlySet<AliaAvatarState> = new Set(['thinking', 'working']);
|
|
27
|
+
|
|
28
|
+
export interface AliaAvatarProps {
|
|
29
|
+
/** Width and height in px. */
|
|
30
|
+
size: number;
|
|
31
|
+
/** What Alia is doing. Default `idle`. */
|
|
32
|
+
state?: AliaAvatarState;
|
|
33
|
+
/** Follows the pointer and reacts to touch, as Bloom's engine does. Default `false`. */
|
|
34
|
+
interactive?: boolean;
|
|
35
|
+
/** Change it to replay the avatar's reaction (a wave) — for a button that wraps it. */
|
|
36
|
+
reactionKey?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Bloom's `AgentAvatar` dressed as Alia. It draws through the 3D runtime, so the host
|
|
41
|
+
* app must mount `AgentAvatarProvider` with the URL of the hosted `character-runtime`;
|
|
42
|
+
* without it Bloom shows its own "unavailable" state.
|
|
43
|
+
*/
|
|
44
|
+
export function AliaAvatar({
|
|
45
|
+
size,
|
|
46
|
+
state = 'idle',
|
|
47
|
+
interactive = false,
|
|
48
|
+
reactionKey,
|
|
49
|
+
}: AliaAvatarProps) {
|
|
50
|
+
const [workingKey, setWorkingKey] = useState(0);
|
|
51
|
+
const busy = BUSY.has(state);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (busy) setWorkingKey((key) => key + 1);
|
|
54
|
+
}, [busy]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<AgentAvatar
|
|
58
|
+
config={ALIA_AVATAR}
|
|
59
|
+
size={size}
|
|
60
|
+
label="Alia"
|
|
61
|
+
workingKey={workingKey}
|
|
62
|
+
interactive={interactive}
|
|
63
|
+
reactionKey={reactionKey}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Owns the text chat; voice is an injected capability (`voiceSession`) so that
|
|
5
5
|
* nothing here reaches `livekit-client` at module-evaluation time. Containers
|
|
6
|
-
* only handle layout chrome (sheet modal, full-screen SafeAreaView)
|
|
7
|
-
*
|
|
6
|
+
* only handle layout chrome (sheet modal, full-screen SafeAreaView). The
|
|
7
|
+
* breadcrumb header is Bloom's `AiChatContainer`, the one the app's conversation uses.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React, {
|
|
11
|
+
createContext,
|
|
11
12
|
Suspense,
|
|
13
|
+
useContext,
|
|
12
14
|
useState,
|
|
13
15
|
useCallback,
|
|
14
16
|
useEffect,
|
|
@@ -16,36 +18,30 @@ import React, {
|
|
|
16
18
|
useRef,
|
|
17
19
|
} from 'react';
|
|
18
20
|
import { View, StyleSheet } from 'react-native';
|
|
19
|
-
import { useSharedValue } from 'react-native-reanimated';
|
|
20
|
-
import type { SharedValue } from 'react-native-reanimated';
|
|
21
|
-
import Volume2 from 'lucide-react-native/icons/volume-2';
|
|
22
21
|
import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
|
|
23
22
|
import { useTTS } from '../hooks/useTTS';
|
|
23
|
+
import { useSpeechToText } from '../hooks/useSpeechToText';
|
|
24
|
+
import { useImagePicker } from '../hooks/useImagePicker';
|
|
25
|
+
import { imageUriToDataUrl } from '../lib/image-data-url';
|
|
24
26
|
import { useAmbientWave } from '../hooks/useAmbientWave';
|
|
25
|
-
import {
|
|
27
|
+
import { AmbientField } from './AmbientField';
|
|
26
28
|
import { AliaChatMessageList } from './AliaChatMessageList';
|
|
27
29
|
import { AliaWelcomeMessage, type WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import type
|
|
31
|
-
import
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} from '
|
|
37
|
-
import type {
|
|
30
|
+
import { ComposerLoader } from '@oxy.so/bloom/composer-loader';
|
|
31
|
+
import { AiChatContainer } from '@oxy.so/bloom/ai-chat';
|
|
32
|
+
import { ComposerPill, type ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
|
|
33
|
+
import { RiImageLine } from '@oxy.so/bloom/icons';
|
|
34
|
+
import { toast } from '@oxy.so/bloom/toast';
|
|
35
|
+
import { Button } from '@oxy.so/bloom/button';
|
|
36
|
+
import { type AliaChatLabels, AliaChatLabelsProvider, useLabels } from '../lib/labels';
|
|
37
|
+
import { AliaAvatar, type AliaAvatarState } from './AliaAvatar';
|
|
38
|
+
import { VoiceModeIcon } from './VoiceModeIcon';
|
|
39
|
+
import type { ChatMessage, VoiceMessage, VoiceSessionComponent, VoiceSessionState } from '../types';
|
|
38
40
|
|
|
39
41
|
export interface AliaChatContentProps {
|
|
40
42
|
clientContext?: string;
|
|
41
43
|
model?: string;
|
|
42
44
|
apiUrl?: string;
|
|
43
|
-
/** Shared value for scroll offset (used by sheet for pan-to-dismiss) */
|
|
44
|
-
scrollOffsetY?: SharedValue<number>;
|
|
45
|
-
/** Injectable autocomplete hook */
|
|
46
|
-
useSuggestions?: (query: string) => { data: Completion[] | undefined; isLoading: boolean };
|
|
47
|
-
/** Called when a suggestion is selected from autocomplete */
|
|
48
|
-
onSuggestionUsed?: (suggestionId: string) => void;
|
|
49
45
|
/** Error handler (e.g. toast) */
|
|
50
46
|
onError?: (message: string) => void;
|
|
51
47
|
// Welcome message
|
|
@@ -58,13 +54,19 @@ export interface AliaChatContentProps {
|
|
|
58
54
|
onThumbsDown?: (messageId: string) => void;
|
|
59
55
|
onApprovePlan?: (planId: string) => void;
|
|
60
56
|
onRejectPlan?: (planId: string) => void;
|
|
61
|
-
onToolResultPress?: (messageId: string) => void;
|
|
62
57
|
/** Override markdown renderer (app passes CustomMarkdown) */
|
|
63
58
|
renderMarkdown?: (content: string) => React.ReactNode;
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
|
|
59
|
+
/** Breadcrumb title beside the brand mark. Default `Alia`; `null` drops the breadcrumb. */
|
|
60
|
+
title?: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Above the breadcrumb row — a back button, a menu, or a whole custom header
|
|
63
|
+
* (given `title={null}`). A function receives the clear handler.
|
|
64
|
+
*/
|
|
65
|
+
headerLeading?:
|
|
66
|
+
| React.ReactNode
|
|
67
|
+
| ((state: { hasMessages: boolean; clear: () => void }) => React.ReactNode);
|
|
68
|
+
/** Right end of the breadcrumb row; receives the clear handler. Replaces Bloom's share/more. */
|
|
69
|
+
actions?: (state: { hasMessages: boolean; clear: () => void }) => React.ReactNode;
|
|
68
70
|
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
69
71
|
isDarkMode?: boolean;
|
|
70
72
|
/**
|
|
@@ -75,6 +77,31 @@ export interface AliaChatContentProps {
|
|
|
75
77
|
voiceSession?: VoiceSessionComponent;
|
|
76
78
|
/** Internal sheet lifecycle bridge: receives the live request canceller. */
|
|
77
79
|
onStopReady?: (stop: (() => void) | null) => void;
|
|
80
|
+
/** Overrides for the chat's own copy; anything omitted is English. */
|
|
81
|
+
labels?: Partial<AliaChatLabels>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The live state of the chat, read by the avatar wherever the header or welcome draws it. */
|
|
85
|
+
const MarkStateContext = createContext<AliaAvatarState>('idle');
|
|
86
|
+
|
|
87
|
+
/** Alia's avatar at any size, following the chat's live state. */
|
|
88
|
+
export function AliaChatMark({ size, reactionKey }: { size: number; reactionKey?: number }) {
|
|
89
|
+
return <AliaAvatar size={size} state={useContext(MarkStateContext)} reactionKey={reactionKey} />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `AiChatContainer`'s `projectIcon` takes a component that is handed its size as `width`. */
|
|
93
|
+
function BreadcrumbMark({ width }: { width?: number }) {
|
|
94
|
+
return <AliaChatMark size={width ?? 16} />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The add menu: photos are the one attachment the chat API carries (`image_url` parts). */
|
|
98
|
+
function addMenu(labels: AliaChatLabels): ComposerPanelAddMenuGroup[] {
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
label: labels.addMenuGroup,
|
|
102
|
+
rows: [{ id: 'photos', label: labels.addPhotos, icon: RiImageLine }],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
/** Adapt a voice message into the chat message format. */
|
|
@@ -90,13 +117,10 @@ function adaptVoiceMessage(vm: VoiceMessage): ChatMessage {
|
|
|
90
117
|
};
|
|
91
118
|
}
|
|
92
119
|
|
|
93
|
-
|
|
120
|
+
function AliaChatContentBody({
|
|
94
121
|
clientContext,
|
|
95
122
|
model,
|
|
96
123
|
apiUrl,
|
|
97
|
-
scrollOffsetY: externalScrollOffsetY,
|
|
98
|
-
useSuggestions,
|
|
99
|
-
onSuggestionUsed,
|
|
100
124
|
onError,
|
|
101
125
|
welcomeGreeting,
|
|
102
126
|
welcomeSubtitle,
|
|
@@ -106,14 +130,25 @@ export function AliaChatContent({
|
|
|
106
130
|
onThumbsDown,
|
|
107
131
|
onApprovePlan,
|
|
108
132
|
onRejectPlan,
|
|
109
|
-
onToolResultPress,
|
|
110
133
|
renderMarkdown,
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
title = 'Alia',
|
|
135
|
+
headerLeading,
|
|
136
|
+
actions,
|
|
113
137
|
isDarkMode,
|
|
114
138
|
voiceSession: VoiceSession,
|
|
115
139
|
onStopReady,
|
|
116
140
|
}: AliaChatContentProps) {
|
|
141
|
+
const labels = useLabels();
|
|
142
|
+
const addMenuGroups = useMemo(() => addMenu(labels), [labels]);
|
|
143
|
+
|
|
144
|
+
// A failure reaches the host's handler when it has one, and Bloom's toast otherwise.
|
|
145
|
+
const reportError = useCallback(
|
|
146
|
+
(message: string, title?: string) => {
|
|
147
|
+
if (onError) onError(title ? `${title}: ${message}` : message);
|
|
148
|
+
else toast.error(title ?? message, title ? { description: message } : undefined);
|
|
149
|
+
},
|
|
150
|
+
[onError],
|
|
151
|
+
);
|
|
117
152
|
// ── Chat ──
|
|
118
153
|
const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
|
|
119
154
|
const { messages, setMessages, send, isStreaming, stop, clear } = useAliaChat(chatOptions);
|
|
@@ -126,16 +161,64 @@ export function AliaChatContent({
|
|
|
126
161
|
// ── Input state ──
|
|
127
162
|
const [inputValue, setInputValue] = useState('');
|
|
128
163
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
164
|
+
// Picked photos wait here, as tiles above the field, until the turn is sent.
|
|
165
|
+
const [photos, setPhotos] = useState<Array<{ id: string; name: string; uri: string }>>([]);
|
|
166
|
+
const { pickImage } = useImagePicker();
|
|
167
|
+
|
|
168
|
+
const handleAddMenuSelect = useCallback(
|
|
169
|
+
async (rowId: string) => {
|
|
170
|
+
if (rowId !== 'photos') return;
|
|
171
|
+
const assets = await pickImage();
|
|
172
|
+
if (!assets?.length) return;
|
|
173
|
+
setPhotos((prev) => [
|
|
174
|
+
...prev,
|
|
175
|
+
...assets.map((asset) => ({
|
|
176
|
+
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
177
|
+
name: asset.name,
|
|
178
|
+
uri: asset.uri,
|
|
179
|
+
})),
|
|
180
|
+
]);
|
|
181
|
+
},
|
|
182
|
+
[pickImage],
|
|
183
|
+
);
|
|
135
184
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
185
|
+
const handleSubmit = useCallback(
|
|
186
|
+
async (value: string) => {
|
|
187
|
+
const text = value.trim();
|
|
188
|
+
if (!text && photos.length === 0) return;
|
|
189
|
+
const pending = photos;
|
|
190
|
+
setInputValue('');
|
|
191
|
+
setPhotos([]);
|
|
192
|
+
try {
|
|
193
|
+
const images = await Promise.all(pending.map((photo) => imageUriToDataUrl(photo.uri)));
|
|
194
|
+
send(text, { images });
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// Keep the draft: the photos could not be read, nothing was sent.
|
|
197
|
+
setInputValue(value);
|
|
198
|
+
setPhotos(pending);
|
|
199
|
+
reportError(err instanceof Error ? err.message : 'Could not attach the photos');
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
[send, photos, reportError],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// ── Dictation: the pill's mic toggles it, the transcript lands in the field ──
|
|
206
|
+
const stt = useSpeechToText();
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (stt.error) reportError(stt.error);
|
|
209
|
+
}, [stt.error, reportError]);
|
|
210
|
+
const sttRef = useRef(stt);
|
|
211
|
+
sttRef.current = stt;
|
|
212
|
+
const handleListeningChange = useCallback(async (listening: boolean) => {
|
|
213
|
+
const current = sttRef.current;
|
|
214
|
+
if (listening) {
|
|
215
|
+
if (!current.isRecording && !current.isTranscribing) current.startRecording();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!current.isRecording) return;
|
|
219
|
+
const text = await current.stopAndTranscribe();
|
|
220
|
+
if (text) setInputValue((prev) => (prev ? `${prev} ${text}` : text));
|
|
221
|
+
}, []);
|
|
139
222
|
|
|
140
223
|
// ── TTS ──
|
|
141
224
|
const tts = useTTS({ apiUrl });
|
|
@@ -162,11 +245,17 @@ export function AliaChatContent({
|
|
|
162
245
|
|
|
163
246
|
// Unmounting the session disconnects the room; the transcript merged above
|
|
164
247
|
// stays in the chat.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
248
|
+
// A call that fails ends the same way a hang-up does, but the person is told why: a
|
|
249
|
+
// button that silently does nothing reads as broken.
|
|
250
|
+
const deactivateVoice = useCallback(
|
|
251
|
+
(error?: string) => {
|
|
252
|
+
setIsVoiceActive(false);
|
|
253
|
+
setVoiceState(null);
|
|
254
|
+
textSnapshotRef.current = [];
|
|
255
|
+
if (error) reportError(error, labels.voiceFailed);
|
|
256
|
+
},
|
|
257
|
+
[reportError, labels.voiceFailed],
|
|
258
|
+
);
|
|
170
259
|
|
|
171
260
|
// ── TTS read aloud handler ──
|
|
172
261
|
const handleReadAloud = useCallback(
|
|
@@ -178,7 +267,7 @@ export function AliaChatContent({
|
|
|
178
267
|
|
|
179
268
|
// ── Mark state (drives the header brand mark) ──
|
|
180
269
|
const voiceAgentState = voiceState?.agentState;
|
|
181
|
-
const markState = useMemo<
|
|
270
|
+
const markState = useMemo<AliaAvatarState>(() => {
|
|
182
271
|
if (isVoiceActive) {
|
|
183
272
|
if (voiceAgentState === 'speaking') return 'writing';
|
|
184
273
|
if (voiceAgentState === 'thinking') return 'thinking';
|
|
@@ -200,6 +289,7 @@ export function AliaChatContent({
|
|
|
200
289
|
subtitle={welcomeSubtitle}
|
|
201
290
|
suggestions={welcomeSuggestions}
|
|
202
291
|
onSuggestionPress={send}
|
|
292
|
+
hideMark
|
|
203
293
|
/>
|
|
204
294
|
);
|
|
205
295
|
}, [welcomeGreeting, welcomeSubtitle, welcomeSuggestions, send, messages.length]);
|
|
@@ -219,81 +309,108 @@ export function AliaChatContent({
|
|
|
219
309
|
isGenerating: isStreaming,
|
|
220
310
|
});
|
|
221
311
|
|
|
312
|
+
// The welcome and a call are the field's moments, so they get the intro's full strength; a
|
|
313
|
+
// conversation being read keeps the hook's subtle one, so the text stays easy to read.
|
|
314
|
+
const ambientIntensity = isVoiceActive || messages.length === 0 ? 1 : wave.intensity;
|
|
315
|
+
|
|
222
316
|
// ── Voice activate button for empty submit (only if voice was provided) ──
|
|
223
317
|
const voiceActivateButton = useMemo(
|
|
224
318
|
() =>
|
|
225
319
|
VoiceSession ? (
|
|
226
320
|
<Button
|
|
227
|
-
|
|
321
|
+
iconOnly
|
|
322
|
+
tone="action"
|
|
323
|
+
size="md"
|
|
324
|
+
accessibilityLabel={labels.startVoice}
|
|
228
325
|
onPress={activateVoice}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<Volume2 size={16} color="white" />
|
|
232
|
-
</Button>
|
|
326
|
+
leadingIcon={VoiceModeIcon}
|
|
327
|
+
/>
|
|
233
328
|
) : undefined,
|
|
234
|
-
[VoiceSession, activateVoice],
|
|
329
|
+
[VoiceSession, activateVoice, labels.startVoice],
|
|
235
330
|
);
|
|
236
331
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
/>
|
|
250
|
-
|
|
251
|
-
{/* Welcome or Messages */}
|
|
252
|
-
<AliaChatMessageList
|
|
253
|
-
messages={messages}
|
|
254
|
-
isStreaming={isStreaming}
|
|
255
|
-
scrollOffsetY={scrollOffsetY}
|
|
256
|
-
onReadAloud={handleReadAloud}
|
|
257
|
-
ttsActiveMessageId={tts.activeMessageId}
|
|
258
|
-
ttsPlaybackState={tts.playbackState}
|
|
259
|
-
onEditMessage={onEditMessage}
|
|
260
|
-
onThumbsUp={onThumbsUp}
|
|
261
|
-
onThumbsDown={onThumbsDown}
|
|
262
|
-
onApprovePlan={onApprovePlan}
|
|
263
|
-
onRejectPlan={onRejectPlan}
|
|
264
|
-
onToolResultPress={onToolResultPress}
|
|
265
|
-
renderMarkdown={renderMarkdown}
|
|
266
|
-
welcomeComponent={welcomeComponent}
|
|
267
|
-
/>
|
|
268
|
-
|
|
269
|
-
{/* Input or Voice Controls */}
|
|
270
|
-
{VoiceSession && isVoiceActive ? (
|
|
271
|
-
/* Suspense so the session may be handed to us as a React.lazy import. */
|
|
272
|
-
<Suspense fallback={null}>
|
|
273
|
-
<VoiceSession
|
|
274
|
-
apiUrl={apiUrl}
|
|
275
|
-
onStateChange={setVoiceState}
|
|
276
|
-
onEnd={deactivateVoice}
|
|
277
|
-
/>
|
|
278
|
-
</Suspense>
|
|
279
|
-
) : (
|
|
280
|
-
<View style={styles.inputContainer}>
|
|
281
|
-
<PromptInput
|
|
332
|
+
const composer =
|
|
333
|
+
VoiceSession && isVoiceActive ? (
|
|
334
|
+
/* Suspense so the session may be handed to us as a React.lazy import. */
|
|
335
|
+
<Suspense fallback={null}>
|
|
336
|
+
<VoiceSession apiUrl={apiUrl} onStateChange={setVoiceState} onEnd={deactivateVoice} />
|
|
337
|
+
</Suspense>
|
|
338
|
+
) : (
|
|
339
|
+
<>
|
|
340
|
+
<ComposerLoader active={isStreaming}>
|
|
341
|
+
<ComposerPill
|
|
342
|
+
surface={false}
|
|
343
|
+
frosted={isStreaming}
|
|
282
344
|
value={inputValue}
|
|
283
345
|
onValueChange={setInputValue}
|
|
284
346
|
onSubmit={handleSubmit}
|
|
285
|
-
|
|
347
|
+
busy={isStreaming}
|
|
286
348
|
onStop={stop}
|
|
287
349
|
emptyAction={voiceActivateButton}
|
|
288
|
-
placeholder=
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
350
|
+
placeholder={labels.placeholder}
|
|
351
|
+
addMenu={addMenuGroups}
|
|
352
|
+
onAddMenuSelect={handleAddMenuSelect}
|
|
353
|
+
attachments={photos.map((photo) => ({
|
|
354
|
+
id: photo.id,
|
|
355
|
+
name: photo.name,
|
|
356
|
+
kind: 'image' as const,
|
|
357
|
+
src: photo.uri,
|
|
358
|
+
}))}
|
|
359
|
+
onRemoveAttachment={(id) => setPhotos((prev) => prev.filter((p) => p.id !== id))}
|
|
360
|
+
voiceState={
|
|
361
|
+
stt.isTranscribing ? 'transcribing' : stt.isRecording ? 'recording' : 'idle'
|
|
362
|
+
}
|
|
363
|
+
onVoiceStateChange={(next) => void handleListeningChange(next === 'recording')}
|
|
364
|
+
/>
|
|
365
|
+
</ComposerLoader>
|
|
366
|
+
</>
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<MarkStateContext.Provider value={markState}>
|
|
371
|
+
<AiChatContainer
|
|
372
|
+
project={title ?? undefined}
|
|
373
|
+
projectIcon={BreadcrumbMark}
|
|
374
|
+
header={
|
|
375
|
+
typeof headerLeading === 'function'
|
|
376
|
+
? headerLeading({ hasMessages: messages.length > 0, clear })
|
|
377
|
+
: headerLeading
|
|
378
|
+
}
|
|
379
|
+
actions={actions?.({ hasMessages: messages.length > 0, clear })}
|
|
380
|
+
composer={composer}
|
|
381
|
+
// The thread scrolls behind the header and the composer, which fade in over it — as
|
|
382
|
+
// in the app's conversation. The wave is the container's background layer, between
|
|
383
|
+
// its surface and the turns.
|
|
384
|
+
floatingChrome
|
|
385
|
+
background={
|
|
386
|
+
<AmbientField
|
|
387
|
+
waveAmplitude={wave.waveAmplitude}
|
|
388
|
+
agentState={wave.agentState}
|
|
389
|
+
intensity={ambientIntensity}
|
|
390
|
+
isDarkMode={isDarkMode}
|
|
391
|
+
/>
|
|
392
|
+
}
|
|
393
|
+
>
|
|
394
|
+
<View style={styles.body}>
|
|
395
|
+
{/* Welcome or Messages */}
|
|
396
|
+
<AliaChatMessageList
|
|
397
|
+
messages={messages}
|
|
398
|
+
isStreaming={isStreaming}
|
|
399
|
+
onReadAloud={handleReadAloud}
|
|
400
|
+
ttsActiveMessageId={tts.activeMessageId}
|
|
401
|
+
ttsPlaybackState={tts.playbackState}
|
|
402
|
+
onEditMessage={onEditMessage}
|
|
403
|
+
onThumbsUp={onThumbsUp}
|
|
404
|
+
onThumbsDown={onThumbsDown}
|
|
405
|
+
onApprovePlan={onApprovePlan}
|
|
406
|
+
onRejectPlan={onRejectPlan}
|
|
407
|
+
onAnswerQuestions={(message) => send(message)}
|
|
408
|
+
renderMarkdown={renderMarkdown}
|
|
409
|
+
welcomeComponent={welcomeComponent}
|
|
293
410
|
/>
|
|
294
411
|
</View>
|
|
295
|
-
|
|
296
|
-
</
|
|
412
|
+
</AiChatContainer>
|
|
413
|
+
</MarkStateContext.Provider>
|
|
297
414
|
);
|
|
298
415
|
}
|
|
299
416
|
|
|
@@ -302,9 +419,16 @@ const styles = StyleSheet.create({
|
|
|
302
419
|
flex: 1,
|
|
303
420
|
position: 'relative',
|
|
304
421
|
},
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
paddingTop: 8,
|
|
308
|
-
paddingBottom: 8,
|
|
422
|
+
body: {
|
|
423
|
+
flex: 1,
|
|
309
424
|
},
|
|
310
425
|
});
|
|
426
|
+
|
|
427
|
+
/** The chat: header, thread and composer, with the host's labels in reach of all of it. */
|
|
428
|
+
export function AliaChatContent({ labels, ...props }: AliaChatContentProps) {
|
|
429
|
+
return (
|
|
430
|
+
<AliaChatLabelsProvider labels={labels}>
|
|
431
|
+
<AliaChatContentBody {...props} />
|
|
432
|
+
</AliaChatLabelsProvider>
|
|
433
|
+
);
|
|
434
|
+
}
|