@alia.onl/sdk 12.0.0 → 13.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 +120 -0
- package/README.md +10 -12
- package/package.json +3 -3
- package/src/components/AliaAvatar.tsx +5 -1
- package/src/components/AliaChatContent.tsx +233 -57
- package/src/components/AliaChatHeader.tsx +100 -67
- package/src/components/AliaChatMessageList.tsx +173 -129
- package/src/components/AliaChatScreen.tsx +44 -16
- package/src/components/AliaChatSheet.tsx +155 -61
- package/src/components/AliaWelcomeMessage.tsx +26 -53
- package/src/components/ComposerSuggestions.tsx +45 -0
- package/src/components/Markdown.tsx +18 -24
- package/src/components/QuestionCard.tsx +35 -33
- package/src/components/ThinkingBlock.tsx +9 -13
- package/src/components/VoiceModeIcon.tsx +15 -7
- package/src/components/voice/VoiceControls.tsx +17 -9
- package/src/hooks/useAliaChat.ts +124 -5
- package/src/hooks/useComposerSuggestions.ts +84 -0
- package/src/index.ts +10 -3
- package/src/lib/agent-character.ts +28 -0
- package/src/lib/character-runtime.ts +10 -0
- package/src/lib/chat-stream.ts +20 -3
- package/src/lib/citations.ts +59 -0
- package/src/lib/delegation.ts +34 -0
- package/src/lib/flights.ts +24 -0
- package/src/lib/greeting.ts +15 -0
- package/src/lib/labels.tsx +17 -2
- package/src/lib/questions.ts +15 -1
- package/src/lib/suggestions.ts +53 -0
- package/src/lib/use-after-delay.ts +19 -0
- package/src/lib/work-log.ts +2 -1
- package/src/tools.ts +7 -1
- package/src/types.ts +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,125 @@
|
|
|
1
1
|
# @alia.onl/sdk
|
|
2
2
|
|
|
3
|
+
## 13.1.0
|
|
4
|
+
|
|
5
|
+
The chat works in any app without host setup it did not document in code:
|
|
6
|
+
|
|
7
|
+
- **The avatar loads.** The chat mounts Bloom's `AgentAvatarProvider` itself, pointing at Alia's
|
|
8
|
+
shared runtime (`ALIA_CHARACTER_RUNTIME_URL`, served with CORS open); a host that serves its own
|
|
9
|
+
passes `characterRuntimeUrl`. Before, an app that had not hosted the 214 MB runtime showed
|
|
10
|
+
Bloom's "unavailable" state.
|
|
11
|
+
- **The empty chat greets.** With no `welcomeGreeting` it greets the signed-in person by name
|
|
12
|
+
(`labels.greeting` / `labels.greetingNamed`). Before, it drew nothing.
|
|
13
|
+
|
|
14
|
+
## 13.0.0
|
|
15
|
+
|
|
16
|
+
Fixes found by driving the sheet end to end (the sheet shows plans, links citations, nests lists):
|
|
17
|
+
|
|
18
|
+
- **A plan is drawn.** `plan_preview` was parsed but the sheet passed no handlers, so the card
|
|
19
|
+
never showed. `useAliaChat` now returns `approvePlan` / `rejectPlan` (approve collapses the
|
|
20
|
+
card to "approved"; reject collapses it and stops the turn) and `AliaChatContent` uses them
|
|
21
|
+
unless the host passes its own.
|
|
22
|
+
- **Citations link.** `[n]` markers in a reply link to their sources (`linkifyCitations`,
|
|
23
|
+
`normalizeCitationMarkers`, `citationLabel` are exported from `@alia.onl/sdk/tools`; the app
|
|
24
|
+
re-exports them instead of keeping a copy).
|
|
25
|
+
- **Nested lists** draw under their parent instead of inline beside it.
|
|
26
|
+
- A finished research step reads "Researched", not "Researching".
|
|
27
|
+
- A turn that ends at a question card with no sentence before it counts as answered.
|
|
28
|
+
|
|
29
|
+
- **BREAKING: an agent is drawn with the look the server stores for it.** `agentCharacter(id)` (a
|
|
30
|
+
look hashed from the id) is removed; `readAgentCharacter(value)` parses a stored character
|
|
31
|
+
(Bloom's `parseAvatarCharacter`) and falls back on `DEFAULT_AGENT_CHARACTER`. The `alia.agent`
|
|
32
|
+
event carries `agentCharacter`, and `agentInfo` has `character`. `AliaChatAgent` has `character`;
|
|
33
|
+
`AliaChatSheet` / `AliaChatScreen` take `agents` (the host's directory) so a delegation's
|
|
34
|
+
thinking avatar is the agent's own before its answer arrives. Needs a server that sends
|
|
35
|
+
`agentCharacter`; release this SDK first, since the stream parser rejects unknown keys.
|
|
36
|
+
- **The one who answers thinks with its own avatar, and it is the same avatar that settles under
|
|
37
|
+
the reply** (Bloom's `AgentReplyIdentity`, as in the multi-agent chat). While Alia works, her
|
|
38
|
+
avatar is the indicator, large with "Thinking…", where the `AgentThinking` line was; when her
|
|
39
|
+
text starts it shrinks under it with her name. A call to an agent (`delegateToAgent` /
|
|
40
|
+
`askAgent`, its `agentId` and `agentName` or `agent` arguments) opens the agent's message at
|
|
41
|
+
once, empty and thinking, and its answer fills that same message, so the avatar never remounts;
|
|
42
|
+
an agent that never answers leaves no message. Alia's reply now carries her identity row, as the
|
|
43
|
+
agents' do.
|
|
44
|
+
The reasoning is part of that same component (`AgentReplyIdentity`'s `reasoning`): while the
|
|
45
|
+
turn thinks, "Thinking… ⌃" folds it under the avatar's row, and once the reply starts it is the
|
|
46
|
+
closed "Thought for N seconds" block above it (`ThinkingBlock` draws nothing while it streams, so
|
|
47
|
+
there is one thinking indicator, not two).
|
|
48
|
+
- **No jumps of height, and no flash of the wrong thinker.** A reply, a new row, the reasoning and
|
|
49
|
+
a thinking agent's avatar open and close with their height eased (Bloom's `Collapsible`, now
|
|
50
|
+
with `appear`) instead of popping. Alia is shown thinking only once she has for a moment, so a
|
|
51
|
+
turn she hands to an agent at once shows the agent thinking, not Alia for an instant first.
|
|
52
|
+
- **Bubbles are equal on every side as the eye reads them.** An agent's bubble sets its top and
|
|
53
|
+
bottom padding from the reply's text metrics (`AgentReplyBubble`'s `text`, 16 on 28 here), so
|
|
54
|
+
the gap from the edge to the text is the same above, below and at the sides. Nothing is cut:
|
|
55
|
+
an open `Collapsible` no longer clips.
|
|
56
|
+
- **The greeting's avatar flies up into the header's profile** as the conversation begins
|
|
57
|
+
(Bloom's `avatar-flight`: `useAvatarFlights`, `AvatarAnchor`, `AvatarFlightLayer`), across the
|
|
58
|
+
main and conversation sheets. `AgentProfile`'s new `avatarHidden` keeps the header's avatar
|
|
59
|
+
waiting for it. Needs a Bloom with `avatar-flight` (after 10.3).
|
|
60
|
+
- **`AliaChatSheet` opens the conversation in a second sheet stacked over the main one.** The
|
|
61
|
+
main sheet stays on the greeting, the suggestions and the composer; the first turn (typed or a
|
|
62
|
+
suggestion) opens a fresh conversation sheet on top, whose back button (and the menu's Clear)
|
|
63
|
+
closes just that sheet, back to the main screen. A phone gets sheets flush with the screen
|
|
64
|
+
edges, with the composer clearing the home indicator; from tablet width (768 px) they float.
|
|
65
|
+
`AliaChatContent` gains `onStart` (hand an empty chat's first turn to the host) and
|
|
66
|
+
`initialTurn` (a turn sent once on mount).
|
|
67
|
+
- **The empty chat looks and behaves like the Alia app's, from the same code.** The avatar sits
|
|
68
|
+
beside the greeting (`AliaWelcomeMessage`, now just the greeting: always side by side, no
|
|
69
|
+
`hideMark`, no suggestions) and the header shows Alia's profile only once there is a
|
|
70
|
+
conversation. The suggestions are one-line rows **over the composer** (`ComposerSuggestions`),
|
|
71
|
+
and from two typed characters they become matches with the typed part highlighted, arrows /
|
|
72
|
+
Enter / Escape through the composer's `onKeyPress`. The app now draws its greeting and its
|
|
73
|
+
suggestions with these same components and only supplies its API's data.
|
|
74
|
+
- **Several agents in one turn.** A delegated agent's answer (`alia.agent`) is now its own
|
|
75
|
+
message with its name and avatar, placed before Alia's reply as the app places it, so a turn
|
|
76
|
+
with Lumi, Atlas and Sol shows three speakers. Before, every answer was merged into one message
|
|
77
|
+
under the last agent's name. Alia's reply is dropped when she said nothing after handing over.
|
|
78
|
+
- **A delegated agent shows its 3D avatar.** The speaker line of an agent's message is Bloom's
|
|
79
|
+
`AgentAvatar`, not an image (an agent's account has none): the character comes from the agent's
|
|
80
|
+
id (`agentCharacter(id)`, exported), so the same agent looks the same everywhere and two agents
|
|
81
|
+
in a turn are told apart. The app draws its agents' messages with the same avatar.
|
|
82
|
+
- **Agents are bubbles, and the header holds the team.** As in Bloom's multi-agent chat, a
|
|
83
|
+
delegated agent's reply is a tinted bubble with its avatar and name beneath it. Once agents have
|
|
84
|
+
answered, the header profile stacks Alia with up to two of them and names them together
|
|
85
|
+
("Alia + Lumi + Atlas"). `headerLeading` / `actions` receive `agents` in their state
|
|
86
|
+
(`AliaChatState`, `AliaChatAgent`), and `AliaChatHeader` takes `agents`.
|
|
87
|
+
- **The row under every reply is Bloom's multi-agent one**: helpful, unhelpful and copy as 22px
|
|
88
|
+
tiles with 12px icons (read aloud is a fourth tile where the host reads), replacing the
|
|
89
|
+
`AiChatFeedbackRow`. New label keys `helpful` and `unhelpful`.
|
|
90
|
+
- **Built on Bloom's `agent-reply`** (peer `@oxy.so/bloom ^10.1.1`): replies, their identity line and
|
|
91
|
+
action tiles, and the header profile are Bloom's `AgentReply`, `AgentReplyBubble`,
|
|
92
|
+
`AgentReplyIdentity`, `AgentReplyActions` and `AgentProfile`, the pieces `MultiAgentChat` is made
|
|
93
|
+
of, not copies. Pressing the profile makes every avatar in it wave.
|
|
94
|
+
- **Questions are asked above the composer, not in the thread.** What Alia asks in her last turn
|
|
95
|
+
(`askUserQuestion`) docks above the composer as Bloom's `Questionnaire`, with a dismiss; typing a
|
|
96
|
+
reply answers just as well. The thread keeps the questions as plain lines (`AskedQuestions`).
|
|
97
|
+
**Breaking:** `QuestionCard` is now the docked questionnaire (`onSubmit` required, new
|
|
98
|
+
`onDismiss`) and no longer draws a read-only variant; the `onAnswerQuestions` prop of the message
|
|
99
|
+
list is gone. `pendingQuestions` joins `@alia.onl/sdk/tools`.
|
|
100
|
+
- **The voice bar is Bloom's voice controls.** `VoiceControls` draws Bloom's `@oxy.so/bloom/voice`
|
|
101
|
+
`VoiceControls` (microphone and end) under the status text, instead of `CallControls`, which is
|
|
102
|
+
made for a dark call stage and lost its captions on the chat's light surface.
|
|
103
|
+
- **The header is back, the profile, share and a menu.** Bloom's `PageHeader` with a back button at
|
|
104
|
+
its start, and at its end share and a three-dots menu (Bloom's `DropdownMenu`, aligned to its
|
|
105
|
+
button) holding Clear and the host's rows. Over the empty chat there is no bar: nothing to do
|
|
106
|
+
there. In the sheet, back returns to the main screen (a fresh chat) and the sheet stays open.
|
|
107
|
+
**Breaking:** `AliaChatHeader` / `AliaChatSheet` / `AliaChatScreen` lose `onClose`; they take
|
|
108
|
+
`onBack` (screen), `onShare` and `headerMenu`; the labels `close` becomes `back`, `share`,
|
|
109
|
+
`more`.
|
|
110
|
+
- **The chat follows Bloom's theme, not the system's scheme.** `AliaMarkdown` and the sheet read
|
|
111
|
+
the app's resolved mode: a person who chose light on a dark OS got white text on a light surface.
|
|
112
|
+
- **Every icon is Bloom's.** The voice button is `RiVolumeUpLine` (it was Entypo); the app's clock,
|
|
113
|
+
robot and connector glyphs are `RiTimeLine`, `RiRobot2Line`, `RiPlugLine` and the SVG generator
|
|
114
|
+
is gone; `@expo/vector-icons` is no longer a peer of the SDK.
|
|
115
|
+
- **Greeting.** The avatar (88px) above the text, both centred; `max-w-xs`, so it always sets in two lines; more room above than below. The Alia app draws the same.
|
|
116
|
+
- Peer `@oxy.so/bloom ^10.2.1` (scrollbar, pill groups, header scrim, voice controls).
|
|
117
|
+
- **Breaking:** `WelcomeSuggestion { id, title, description }` becomes `AliaSuggestion { id, text }`
|
|
118
|
+
(`text` is what is shown and what is sent). New optional props on the sheet, screen and content:
|
|
119
|
+
`suggestionMatches` and `onSuggestionQuery(query)`, so a host can search as the person types.
|
|
120
|
+
New exports: `ComposerSuggestions`, `useComposerSuggestions`, `useSuggestionQuery`,
|
|
121
|
+
`composerCompletions`, `Completion`, `AliaSuggestion`.
|
|
122
|
+
|
|
3
123
|
## 12.0.0
|
|
4
124
|
|
|
5
125
|
Breaking: Bloom 10 peer; the `header` render prop replaces the breadcrumb path (`title`, `actions`)
|
package/README.md
CHANGED
|
@@ -25,20 +25,18 @@ the default in Expo SDK 53+), because every icon import is a subpath.
|
|
|
25
25
|
The chat is built from Bloom (`@oxy.so/bloom`, a peer dependency): mount Bloom's
|
|
26
26
|
`BloomProvider` above it, with the one `KeyboardProvider` your app already has.
|
|
27
27
|
|
|
28
|
-
Alia's avatar is Bloom's 3D character (`AgentAvatar`)
|
|
29
|
-
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
28
|
+
Alia's avatar is Bloom's 3D character (`AgentAvatar`). It draws through a runtime of
|
|
29
|
+
static files (about 214 MB), which the chat loads from Alia's own origin
|
|
30
|
+
(`ALIA_CHARACTER_RUNTIME_URL`, served with CORS open), so the app hosts nothing and mounts
|
|
31
|
+
no avatar provider. An app with a strict CSP allows `https://alia.onl` in `script-src` and
|
|
32
|
+
`connect-src` (and `'wasm-unsafe-eval'`). To serve your own copy instead, serve the Bloom
|
|
33
|
+
package's `assets/character-runtime/` directory unchanged and pass the URL of `runtime.mjs`
|
|
34
|
+
as `characterRuntimeUrl` to `AliaChatSheet` or `AliaChatScreen`; native avatars run in a
|
|
40
35
|
WebView, so there the URL must be absolute and reachable from the device.
|
|
41
36
|
|
|
37
|
+
The empty chat greets the signed-in person by name ("Hi Nate, what's on your mind?"); pass
|
|
38
|
+
`welcomeGreeting` to say something else, or `labels.greeting` / `labels.greetingNamed` to translate it.
|
|
39
|
+
|
|
42
40
|
Failures (a voice call that cannot start, a dictation error) are shown with Bloom's `toast`,
|
|
43
41
|
so the app needs a `ToastOutlet` (`OxyProvider` mounts one), or pass `onError` to handle them.
|
|
44
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia.onl/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.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",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"clsx": "^2.1.1",
|
|
34
34
|
"react-native-markdown-display": "^7.0.2",
|
|
35
|
-
"tailwind-merge": "^3.
|
|
35
|
+
"tailwind-merge": "^3.7.0",
|
|
36
36
|
"zustand": "^5.0.15"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "^19.2.3",
|
|
40
40
|
"react-native": "^0.86.0",
|
|
41
41
|
"@expo/vector-icons": "^15.1.1",
|
|
42
|
-
"@oxy.so/bloom": ">=10.
|
|
42
|
+
"@oxy.so/bloom": ">=10.4.0",
|
|
43
43
|
"@oxy.so/core": ">=3.0.0",
|
|
44
44
|
"@oxy.so/services": ">=8.0.0",
|
|
45
45
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
@@ -9,7 +9,11 @@ export type AliaAvatarState = 'idle' | 'thinking' | 'working' | 'writing';
|
|
|
9
9
|
* eyes, nothing on its head. It is `blue_beret` with the hat taken off and the paint changed,
|
|
10
10
|
* so Bloom retargets that preset's animation onto it; change it here and nowhere else.
|
|
11
11
|
*/
|
|
12
|
-
const ALIA_AVATAR: AvatarCharacter = {
|
|
12
|
+
export const ALIA_AVATAR: AvatarCharacter = {
|
|
13
|
+
preset: 'blue_beret',
|
|
14
|
+
accessory: 'none',
|
|
15
|
+
color: 'orchid',
|
|
16
|
+
};
|
|
13
17
|
|
|
14
18
|
const BUSY: ReadonlySet<AliaAvatarState> = new Set(['thinking', 'working']);
|
|
15
19
|
|
|
@@ -16,10 +16,24 @@ import { useImagePicker } from '../hooks/useImagePicker';
|
|
|
16
16
|
import { imageUriToDataUrl } from '../lib/image-data-url';
|
|
17
17
|
import { useAmbientWave } from '../hooks/useAmbientWave';
|
|
18
18
|
import { AmbientField } from './AmbientField';
|
|
19
|
+
import { QuestionCard } from './QuestionCard';
|
|
20
|
+
import { getAccountDisplayName } from '@oxy.so/core';
|
|
21
|
+
import { useOxy } from '@oxy.so/services/ui/client';
|
|
22
|
+
import { AgentAvatarProvider } from '@oxy.so/bloom/agent-avatar';
|
|
23
|
+
import { ALIA_CHARACTER_RUNTIME_URL } from '../lib/character-runtime';
|
|
24
|
+
import { pendingQuestions } from '../lib/questions';
|
|
19
25
|
import { AliaChatMessageList } from './AliaChatMessageList';
|
|
20
|
-
import {
|
|
26
|
+
import { AliaFlightsProvider, ALIA_TO_PROFILE } from '../lib/flights';
|
|
27
|
+
import { AliaWelcomeMessage } from './AliaWelcomeMessage';
|
|
28
|
+
import { ComposerSuggestions } from './ComposerSuggestions';
|
|
29
|
+
import { useComposerSuggestions, useSuggestionQuery } from '../hooks/useComposerSuggestions';
|
|
21
30
|
import { ComposerLoader } from '@oxy.so/bloom/composer-loader';
|
|
22
31
|
import { AiChatContainer } from '@oxy.so/bloom/ai-chat';
|
|
32
|
+
import {
|
|
33
|
+
AvatarFlightLayer,
|
|
34
|
+
useAvatarFlights,
|
|
35
|
+
type AvatarFlights,
|
|
36
|
+
} from '@oxy.so/bloom/avatar-flight';
|
|
23
37
|
import { ComposerPill, type ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
|
|
24
38
|
import { RiImageLine } from '@oxy.so/bloom/icons';
|
|
25
39
|
import { toast } from '@oxy.so/bloom/toast';
|
|
@@ -27,7 +41,46 @@ import { Button } from '@oxy.so/bloom/button';
|
|
|
27
41
|
import { type AliaChatLabels, AliaChatLabelsProvider, useLabels } from '../lib/labels';
|
|
28
42
|
import type { AliaAvatarState } from './AliaAvatar';
|
|
29
43
|
import { VoiceModeIcon } from './VoiceModeIcon';
|
|
30
|
-
import type {
|
|
44
|
+
import type {
|
|
45
|
+
AliaSuggestion,
|
|
46
|
+
ChatMessage,
|
|
47
|
+
VoiceMessage,
|
|
48
|
+
VoiceSessionComponent,
|
|
49
|
+
VoiceSessionState,
|
|
50
|
+
} from '../types';
|
|
51
|
+
|
|
52
|
+
/** The distinct delegated agents among `messages`, in the order they first answered. */
|
|
53
|
+
function participatingAgents(messages: readonly ChatMessage[]): AliaChatAgent[] {
|
|
54
|
+
const seen = new Map<string, AliaChatAgent>();
|
|
55
|
+
for (const { agentInfo } of messages) {
|
|
56
|
+
if (agentInfo && !seen.has(agentInfo.id)) seen.set(agentInfo.id, agentInfo);
|
|
57
|
+
}
|
|
58
|
+
return [...seen.values()];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** An agent that answered in the conversation, as the header shows it beside Alia. */
|
|
62
|
+
export interface AliaChatAgent {
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
/** Its look, as the server stores it (JSON); absent, the default look. */
|
|
66
|
+
character?: unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** What the chat tells its header and actions: whether there is a conversation to clear, and who took part. */
|
|
70
|
+
export interface AliaChatState {
|
|
71
|
+
hasMessages: boolean;
|
|
72
|
+
clear: () => void;
|
|
73
|
+
/** What Alia is doing; the header's avatar shows it. */
|
|
74
|
+
markState: AliaAvatarState;
|
|
75
|
+
/** The delegated agents that have answered, in the order they first did. */
|
|
76
|
+
agents: AliaChatAgent[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A turn of the person's: the text, and the photos (`data:` or `https:` URLs) sent with it. */
|
|
80
|
+
export interface AliaTurn {
|
|
81
|
+
text: string;
|
|
82
|
+
images?: ReadonlyArray<string>;
|
|
83
|
+
}
|
|
31
84
|
|
|
32
85
|
export interface AliaChatContentProps {
|
|
33
86
|
clientContext?: string;
|
|
@@ -35,10 +88,15 @@ export interface AliaChatContentProps {
|
|
|
35
88
|
apiUrl?: string;
|
|
36
89
|
/** Error handler (e.g. toast) */
|
|
37
90
|
onError?: (message: string) => void;
|
|
38
|
-
|
|
91
|
+
/** The empty chat's greeting; omitted, Alia greets the signed-in person by name (`labels`). */
|
|
39
92
|
welcomeGreeting?: string;
|
|
40
93
|
welcomeSubtitle?: string;
|
|
41
|
-
|
|
94
|
+
/** The lines offered over the composer on an empty chat. */
|
|
95
|
+
welcomeSuggestions?: AliaSuggestion[];
|
|
96
|
+
/** What the host found for the typed text (see `onSuggestionQuery`). */
|
|
97
|
+
suggestionMatches?: AliaSuggestion[];
|
|
98
|
+
/** The typed text, once typing has settled, for the host to search suggestions with. */
|
|
99
|
+
onSuggestionQuery?: (query: string) => void;
|
|
42
100
|
// Message action callbacks
|
|
43
101
|
onEditMessage?: (messageId: string, newContent: string) => void;
|
|
44
102
|
onThumbsUp?: (messageId: string) => void;
|
|
@@ -47,12 +105,11 @@ export interface AliaChatContentProps {
|
|
|
47
105
|
onRejectPlan?: (planId: string) => void;
|
|
48
106
|
/** Override markdown renderer (app passes CustomMarkdown) */
|
|
49
107
|
renderMarkdown?: (content: string) => React.ReactNode;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}) => React.ReactNode;
|
|
108
|
+
/**
|
|
109
|
+
* The chat's top bar. Receives what it needs from the chat: whether there is anything to clear,
|
|
110
|
+
* how, who took part, and what Alia is doing.
|
|
111
|
+
*/
|
|
112
|
+
header?: (state: AliaChatState) => React.ReactNode;
|
|
56
113
|
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
57
114
|
isDarkMode?: boolean;
|
|
58
115
|
/**
|
|
@@ -61,10 +118,31 @@ export interface AliaChatContentProps {
|
|
|
61
118
|
* from this module. May be a `React.lazy` component.
|
|
62
119
|
*/
|
|
63
120
|
voiceSession?: VoiceSessionComponent;
|
|
121
|
+
/**
|
|
122
|
+
* Space the composer keeps clear at the bottom edge, in px — the home indicator, when the
|
|
123
|
+
* chat is flush with the screen (a phone sheet). The ambient background still runs behind it.
|
|
124
|
+
*/
|
|
125
|
+
bottomInset?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Hands the first turn of an empty chat to the host instead of sending it here — a sheet opens
|
|
128
|
+
* the conversation in a new sheet over this one. Omitted, the chat sends it itself.
|
|
129
|
+
*/
|
|
130
|
+
onStart?: (turn: AliaTurn) => void;
|
|
131
|
+
/** The agents the host knows, with their look: a delegation's thinking avatar is drawn from them. */
|
|
132
|
+
agents?: UseAliaChatOptions['agents'];
|
|
133
|
+
/**
|
|
134
|
+
* The avatar flights this chat takes part in, when a sheet shares them across its chats (the
|
|
135
|
+
* greeting's avatar flies into a header in another sheet). Omitted, the chat has its own.
|
|
136
|
+
*/
|
|
137
|
+
flights?: AvatarFlights;
|
|
138
|
+
/** A turn sent once as this chat mounts: the conversation a sheet was opened for. */
|
|
139
|
+
initialTurn?: AliaTurn;
|
|
64
140
|
/** Internal sheet lifecycle bridge: receives the live request canceller. */
|
|
65
141
|
onStopReady?: (stop: (() => void) | null) => void;
|
|
66
142
|
/** Overrides for the chat's own copy; anything omitted is English. */
|
|
67
143
|
labels?: Partial<AliaChatLabels>;
|
|
144
|
+
/** Where the 3D avatar's runtime is served from; omitted, Alia's shared copy (`ALIA_CHARACTER_RUNTIME_URL`). */
|
|
145
|
+
characterRuntimeUrl?: string;
|
|
68
146
|
}
|
|
69
147
|
|
|
70
148
|
/** The add menu: photos are the one attachment the chat API carries (`image_url` parts). */
|
|
@@ -98,6 +176,8 @@ function AliaChatContentBody({
|
|
|
98
176
|
welcomeGreeting,
|
|
99
177
|
welcomeSubtitle,
|
|
100
178
|
welcomeSuggestions,
|
|
179
|
+
suggestionMatches,
|
|
180
|
+
onSuggestionQuery,
|
|
101
181
|
onEditMessage,
|
|
102
182
|
onThumbsUp,
|
|
103
183
|
onThumbsDown,
|
|
@@ -107,8 +187,15 @@ function AliaChatContentBody({
|
|
|
107
187
|
header,
|
|
108
188
|
isDarkMode,
|
|
109
189
|
voiceSession: VoiceSession,
|
|
190
|
+
bottomInset = 0,
|
|
191
|
+
onStart,
|
|
192
|
+
agents: knownAgents,
|
|
193
|
+
flights: sharedFlights,
|
|
194
|
+
initialTurn,
|
|
110
195
|
onStopReady,
|
|
111
196
|
}: AliaChatContentProps) {
|
|
197
|
+
const ownFlights = useAvatarFlights();
|
|
198
|
+
const flights = sharedFlights ?? ownFlights;
|
|
112
199
|
const labels = useLabels();
|
|
113
200
|
const addMenuGroups = useMemo(() => addMenu(labels), [labels]);
|
|
114
201
|
|
|
@@ -121,8 +208,46 @@ function AliaChatContentBody({
|
|
|
121
208
|
[onError],
|
|
122
209
|
);
|
|
123
210
|
// ── Chat ──
|
|
124
|
-
const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
|
|
125
|
-
const { messages, setMessages, send, isStreaming, stop, clear } =
|
|
211
|
+
const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext, agents: knownAgents };
|
|
212
|
+
const { messages, setMessages, send, isStreaming, stop, clear, approvePlan, rejectPlan } =
|
|
213
|
+
useAliaChat(chatOptions);
|
|
214
|
+
const agents = useMemo(() => participatingAgents(messages), [messages]);
|
|
215
|
+
// What Alia asked in her last turn and the person has not answered: asked above the composer,
|
|
216
|
+
// not in the thread. Any later message, typed or sent from the card, is the answer.
|
|
217
|
+
const lastMessage = messages.at(-1);
|
|
218
|
+
const questionsAsked = useMemo(
|
|
219
|
+
() => pendingQuestions(messages, isStreaming),
|
|
220
|
+
[messages, isStreaming],
|
|
221
|
+
);
|
|
222
|
+
const [dismissedQuestionsOf, setDismissedQuestionsOf] = useState<string | null>(null);
|
|
223
|
+
const asking = questionsAsked.length > 0 && dismissedQuestionsOf !== lastMessage?.id;
|
|
224
|
+
|
|
225
|
+
// The first turn of an empty chat goes to the host when it asked for it; every other turn is sent here.
|
|
226
|
+
const startsElsewhere = onStart !== undefined && messages.length === 0;
|
|
227
|
+
const firstTurn = messages.length === 0;
|
|
228
|
+
const submitTurn = useCallback(
|
|
229
|
+
async (text: string, images?: ReadonlyArray<string>) => {
|
|
230
|
+
if (startsElsewhere) {
|
|
231
|
+
// The host's conversation sheet opens first so that the flight, drawn when it launches,
|
|
232
|
+
// is above it; this chat stays on the greeting, so the avatar can still be measured.
|
|
233
|
+
onStart({ text, images });
|
|
234
|
+
await flights.launch([ALIA_TO_PROFILE]);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// The greeting leaves with the first turn: measure it before it does.
|
|
238
|
+
if (firstTurn) await flights.launch([ALIA_TO_PROFILE]);
|
|
239
|
+
send(text, images ? { images } : undefined);
|
|
240
|
+
},
|
|
241
|
+
[startsElsewhere, onStart, firstTurn, flights, send],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// The conversation this chat was opened for, sent once.
|
|
245
|
+
const initialSent = useRef(false);
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (!initialTurn || initialSent.current) return;
|
|
248
|
+
initialSent.current = true;
|
|
249
|
+
send(initialTurn.text, initialTurn.images ? { images: initialTurn.images } : undefined);
|
|
250
|
+
}, [initialTurn, send]);
|
|
126
251
|
|
|
127
252
|
useEffect(() => {
|
|
128
253
|
onStopReady?.(stop);
|
|
@@ -156,6 +281,26 @@ function AliaChatContentBody({
|
|
|
156
281
|
[pickImage],
|
|
157
282
|
);
|
|
158
283
|
|
|
284
|
+
// Suggestions over the composer on an empty chat, as the Alia app has them.
|
|
285
|
+
const suggestionQuery = useSuggestionQuery(inputValue, messages.length === 0);
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
onSuggestionQuery?.(suggestionQuery);
|
|
288
|
+
}, [suggestionQuery, onSuggestionQuery]);
|
|
289
|
+
const handlePickSuggestion = useCallback(
|
|
290
|
+
(suggestion: AliaSuggestion) => {
|
|
291
|
+
setInputValue('');
|
|
292
|
+
submitTurn(suggestion.text);
|
|
293
|
+
},
|
|
294
|
+
[submitTurn],
|
|
295
|
+
);
|
|
296
|
+
const suggestions = useComposerSuggestions({
|
|
297
|
+
draft: inputValue,
|
|
298
|
+
enabled: messages.length === 0,
|
|
299
|
+
welcome: welcomeSuggestions,
|
|
300
|
+
matches: suggestionMatches,
|
|
301
|
+
onPick: handlePickSuggestion,
|
|
302
|
+
});
|
|
303
|
+
|
|
159
304
|
const handleSubmit = useCallback(
|
|
160
305
|
async (value: string) => {
|
|
161
306
|
const text = value.trim();
|
|
@@ -167,7 +312,7 @@ function AliaChatContentBody({
|
|
|
167
312
|
const images = await Promise.all(
|
|
168
313
|
pending.map((photo) => imageUriToDataUrl(photo.uri, photo.mimeType)),
|
|
169
314
|
);
|
|
170
|
-
|
|
315
|
+
submitTurn(text, images);
|
|
171
316
|
} catch (err) {
|
|
172
317
|
// Keep the draft: the photos could not be read, nothing was sent.
|
|
173
318
|
setInputValue(value);
|
|
@@ -175,7 +320,7 @@ function AliaChatContentBody({
|
|
|
175
320
|
reportError(err instanceof Error ? err.message : 'Could not attach the photos');
|
|
176
321
|
}
|
|
177
322
|
},
|
|
178
|
-
[
|
|
323
|
+
[submitTurn, photos, reportError],
|
|
179
324
|
);
|
|
180
325
|
|
|
181
326
|
// ── Dictation: the pill's mic toggles it, the transcript lands in the field ──
|
|
@@ -260,18 +405,17 @@ function AliaChatContentBody({
|
|
|
260
405
|
return 'writing';
|
|
261
406
|
}, [messages, isStreaming, isVoiceActive, voiceAgentState]);
|
|
262
407
|
|
|
408
|
+
const { user } = useOxy();
|
|
409
|
+
const greeting =
|
|
410
|
+
welcomeGreeting ?? (user ? labels.greetingNamed(getAccountDisplayName(user)) : labels.greeting);
|
|
263
411
|
const welcomeComponent = useMemo(() => {
|
|
264
|
-
if (
|
|
412
|
+
if (messages.length > 0) return undefined;
|
|
265
413
|
return (
|
|
266
|
-
<
|
|
267
|
-
greeting={
|
|
268
|
-
|
|
269
|
-
suggestions={welcomeSuggestions}
|
|
270
|
-
onSuggestionPress={send}
|
|
271
|
-
hideMark
|
|
272
|
-
/>
|
|
414
|
+
<View className="flex-1 items-center justify-center px-4 py-10">
|
|
415
|
+
<AliaWelcomeMessage greeting={greeting} subtitle={welcomeSubtitle} />
|
|
416
|
+
</View>
|
|
273
417
|
);
|
|
274
|
-
}, [
|
|
418
|
+
}, [greeting, welcomeSubtitle, messages.length]);
|
|
275
419
|
|
|
276
420
|
// ── Ambient wave — one persistent overlay across idle/voice/TTS/STT ──
|
|
277
421
|
const wave = useAmbientWave({
|
|
@@ -327,8 +471,24 @@ function AliaChatContentBody({
|
|
|
327
471
|
</Suspense>
|
|
328
472
|
) : (
|
|
329
473
|
<>
|
|
474
|
+
<ComposerSuggestions
|
|
475
|
+
completions={suggestions.completions}
|
|
476
|
+
selected={suggestions.selected}
|
|
477
|
+
onPick={handlePickSuggestion}
|
|
478
|
+
/>
|
|
479
|
+
{/* Beside the loader, not inside the pill: its light traces the box it wraps. */}
|
|
480
|
+
{asking ? (
|
|
481
|
+
<View className="mb-2">
|
|
482
|
+
<QuestionCard
|
|
483
|
+
questions={questionsAsked}
|
|
484
|
+
onSubmit={(message) => send(message)}
|
|
485
|
+
onDismiss={() => setDismissedQuestionsOf(lastMessage?.id ?? null)}
|
|
486
|
+
/>
|
|
487
|
+
</View>
|
|
488
|
+
) : null}
|
|
330
489
|
<ComposerLoader active={isStreaming}>
|
|
331
490
|
<ComposerPill
|
|
491
|
+
onKeyPress={suggestions.onKeyPress}
|
|
332
492
|
surface={false}
|
|
333
493
|
frosted={isStreaming}
|
|
334
494
|
value={inputValue}
|
|
@@ -353,8 +513,8 @@ function AliaChatContentBody({
|
|
|
353
513
|
|
|
354
514
|
const hasMessages = messages.length > 0;
|
|
355
515
|
const headerNode = useMemo(
|
|
356
|
-
() => header?.({ hasMessages, clear, markState }),
|
|
357
|
-
[header, hasMessages, clear, markState],
|
|
516
|
+
() => header?.({ hasMessages, clear, markState, agents }),
|
|
517
|
+
[header, hasMessages, clear, markState, agents],
|
|
358
518
|
);
|
|
359
519
|
const background = useMemo(
|
|
360
520
|
() => (
|
|
@@ -370,42 +530,58 @@ function AliaChatContentBody({
|
|
|
370
530
|
);
|
|
371
531
|
|
|
372
532
|
return (
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
533
|
+
<AliaFlightsProvider value={flights}>
|
|
534
|
+
<AiChatContainer
|
|
535
|
+
header={headerNode}
|
|
536
|
+
composer={
|
|
537
|
+
bottomInset > 0 ? (
|
|
538
|
+
<View style={{ paddingBottom: bottomInset }}>{composer}</View>
|
|
539
|
+
) : (
|
|
540
|
+
composer
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
// The thread scrolls behind the header and the composer, which fade in over it — as
|
|
544
|
+
// in the app's conversation. The wave is the container's background layer, between
|
|
545
|
+
// its surface and the turns.
|
|
546
|
+
floatingChrome
|
|
547
|
+
// The sheet or screen around it owns the fill and the shape; the container adds neither.
|
|
548
|
+
surface={false}
|
|
549
|
+
background={background}
|
|
550
|
+
>
|
|
551
|
+
<View className="flex-1">
|
|
552
|
+
{/* Welcome or Messages */}
|
|
553
|
+
<AliaChatMessageList
|
|
554
|
+
messages={messages}
|
|
555
|
+
isStreaming={isStreaming}
|
|
556
|
+
onReadAloud={handleReadAloud}
|
|
557
|
+
ttsActiveMessageId={tts.activeMessageId}
|
|
558
|
+
ttsPlaybackState={tts.playbackState}
|
|
559
|
+
onEditMessage={onEditMessage}
|
|
560
|
+
onThumbsUp={onThumbsUp}
|
|
561
|
+
onThumbsDown={onThumbsDown}
|
|
562
|
+
onApprovePlan={onApprovePlan ?? approvePlan}
|
|
563
|
+
onRejectPlan={onRejectPlan ?? rejectPlan}
|
|
564
|
+
renderMarkdown={renderMarkdown}
|
|
565
|
+
welcomeComponent={welcomeComponent}
|
|
566
|
+
/>
|
|
567
|
+
</View>
|
|
568
|
+
</AiChatContainer>
|
|
569
|
+
{sharedFlights ? null : <AvatarFlightLayer flights={ownFlights} />}
|
|
570
|
+
</AliaFlightsProvider>
|
|
401
571
|
);
|
|
402
572
|
}
|
|
403
573
|
|
|
404
574
|
/** The chat: header, thread and composer, with the host's labels in reach of all of it. */
|
|
405
|
-
export function AliaChatContent({
|
|
575
|
+
export function AliaChatContent({
|
|
576
|
+
labels,
|
|
577
|
+
characterRuntimeUrl = ALIA_CHARACTER_RUNTIME_URL,
|
|
578
|
+
...props
|
|
579
|
+
}: AliaChatContentProps) {
|
|
406
580
|
return (
|
|
407
|
-
<
|
|
408
|
-
<
|
|
409
|
-
|
|
581
|
+
<AgentAvatarProvider runtimeUrl={characterRuntimeUrl}>
|
|
582
|
+
<AliaChatLabelsProvider labels={labels}>
|
|
583
|
+
<AliaChatContentBody {...props} />
|
|
584
|
+
</AliaChatLabelsProvider>
|
|
585
|
+
</AgentAvatarProvider>
|
|
410
586
|
);
|
|
411
587
|
}
|