@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/src/lib/citations.ts
CHANGED
|
@@ -54,3 +54,62 @@ export function extractCitationSources(
|
|
|
54
54
|
|
|
55
55
|
return [...byId.values()].sort((a, b) => a.id - b.id);
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
const FULLWIDTH_MARKER_RE = /【\s*(\d{1,3})\s*(?:†[^】]*)?】/g;
|
|
59
|
+
const ASCII_DAGGER_MARKER_RE = /\[(\d{1,3})†[^\]]*\]/g;
|
|
60
|
+
const COMMA_LIST_RE = /\[(\d{1,3}(?:\s*,\s*\d{1,3})+)\](?![(:])/g;
|
|
61
|
+
const SINGLE_MARKER_RE = /\[(\d{1,3})\](?![(:])/g;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rewrite every citation marker into the `[n]` form; with `knownIds`, drop
|
|
65
|
+
* the ones that name no source. See the API twin for the reasoning.
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeCitationMarkers(text: string, knownIds?: Iterable<number>): string {
|
|
68
|
+
const known = knownIds ? new Set(knownIds) : null;
|
|
69
|
+
|
|
70
|
+
let out = text
|
|
71
|
+
.replace(FULLWIDTH_MARKER_RE, '[$1]')
|
|
72
|
+
.replace(ASCII_DAGGER_MARKER_RE, '[$1]')
|
|
73
|
+
.replace(COMMA_LIST_RE, (_m, list: string) =>
|
|
74
|
+
list
|
|
75
|
+
.split(',')
|
|
76
|
+
.map((n) => `[${n.trim()}]`)
|
|
77
|
+
.join(''),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (known && known.size > 0) {
|
|
81
|
+
out = out.replace(SINGLE_MARKER_RE, (m, n: string) => (known.has(Number(n)) ? m : ''));
|
|
82
|
+
out = out.replace(/[ \t]+([.,;:!?])/g, '$1').replace(/[ \t]{2,}/g, ' ');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The accessible name of one citation: what a screen reader says for the
|
|
90
|
+
* link, and what `CustomMarkdown` puts in the link's title.
|
|
91
|
+
*/
|
|
92
|
+
export function citationLabel(source: CitationSource): string {
|
|
93
|
+
return `Source ${source.id}: ${source.title}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `[n]` markers that name a known source become Markdown links to it, so the
|
|
98
|
+
* SDK's Markdown renderer makes them pressable. Markers with no source are
|
|
99
|
+
* left as text: dropping them here would hide that the model cited something
|
|
100
|
+
* the reader cannot check, and the API already drops them for new answers.
|
|
101
|
+
*
|
|
102
|
+
* Runs on the normalised text, so legacy `【n†…】` markers link too. The
|
|
103
|
+
* link title carries the accessible name; a title containing quotes would end
|
|
104
|
+
* it early, so those are folded to apostrophes.
|
|
105
|
+
*/
|
|
106
|
+
export function linkifyCitations(text: string, sources: CitationSource[]): string {
|
|
107
|
+
if (sources.length === 0) return normalizeCitationMarkers(text);
|
|
108
|
+
const byId = new Map(sources.map((s) => [s.id, s]));
|
|
109
|
+
return normalizeCitationMarkers(text).replace(SINGLE_MARKER_RE, (m, n: string) => {
|
|
110
|
+
const source = byId.get(Number(n));
|
|
111
|
+
if (!source) return m;
|
|
112
|
+
const title = citationLabel(source).replace(/"/g, "'");
|
|
113
|
+
return `[${n}](${source.url} "${title}")`;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ToolInvocation } from '../types';
|
|
2
|
+
|
|
3
|
+
/** The tools that hand a question to another agent. */
|
|
4
|
+
const DELEGATION_TOOLS = new Set(['delegateToAgent', 'askAgent']);
|
|
5
|
+
|
|
6
|
+
export interface WorkingAgent {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const text = (value: unknown): string | undefined =>
|
|
12
|
+
typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The agent a delegation call is for, from its arguments: an id (`agentId`) and a name
|
|
16
|
+
* (`agentName`, or `agent` when it is a name). Whichever is missing falls back on the other, so
|
|
17
|
+
* the placeholder and the answer that follows agree on who it is as long as the call names them.
|
|
18
|
+
*/
|
|
19
|
+
export function delegatedAgent(invocation: ToolInvocation): WorkingAgent | null {
|
|
20
|
+
if (!DELEGATION_TOOLS.has(invocation.toolName)) return null;
|
|
21
|
+
const args = invocation.args ?? {};
|
|
22
|
+
const id = text(args.agentId) ?? text(args.agent_id) ?? text(args.agent) ?? text(args.name);
|
|
23
|
+
const name = text(args.agentName) ?? text(args.agent) ?? text(args.name) ?? id;
|
|
24
|
+
if (!id || !name) return null;
|
|
25
|
+
return { id, name };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The id of an agent's message in a turn. A delegation call creates it empty (the agent is
|
|
30
|
+
* thinking) and the agent's answer fills the same message, so one element carries the avatar from
|
|
31
|
+
* thinking to answered.
|
|
32
|
+
*/
|
|
33
|
+
export const agentMessageId = (assistantId: string, agentId: string) =>
|
|
34
|
+
`${assistantId}-agent-${agentId}`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { AvatarFlights, AvatarFlightSpec } from '@oxy.so/bloom/avatar-flight';
|
|
3
|
+
|
|
4
|
+
import { ALIA_AVATAR } from '../components/AliaAvatar';
|
|
5
|
+
|
|
6
|
+
/** Where Alia's avatar is in the chat: large in the greeting, then small in the header's profile. */
|
|
7
|
+
export const ALIA_ANCHORS = { welcome: 'alia:welcome', profile: 'alia:profile' } as const;
|
|
8
|
+
|
|
9
|
+
/** The greeting's avatar flies up into the header's profile as the conversation begins. */
|
|
10
|
+
export const ALIA_TO_PROFILE: AvatarFlightSpec = {
|
|
11
|
+
key: 'alia',
|
|
12
|
+
character: ALIA_AVATAR,
|
|
13
|
+
from: ALIA_ANCHORS.welcome,
|
|
14
|
+
to: ALIA_ANCHORS.profile,
|
|
15
|
+
duration: 800,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const FlightsContext = createContext<AvatarFlights | null>(null);
|
|
19
|
+
|
|
20
|
+
/** The chat's avatar flights, for the greeting and the header that share them. */
|
|
21
|
+
export const AliaFlightsProvider = FlightsContext.Provider;
|
|
22
|
+
|
|
23
|
+
/** `null` outside a chat: the greeting and the header then draw their avatars without flying. */
|
|
24
|
+
export const useAliaFlights = () => useContext(FlightsContext);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Commas of the languages the apps greet in: Latin, fullwidth, ideographic and Arabic. */
|
|
2
|
+
const COMMA = /[,,、،]/;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A greeting in two lines: the address ("Hi Nate,") over the rest ("what's on your mind?"). It
|
|
6
|
+
* breaks after the first comma, which keeps the greeting one string a translator writes once; a
|
|
7
|
+
* greeting without one (or with nothing after it) stays a single line.
|
|
8
|
+
*/
|
|
9
|
+
export function greetingLines(greeting: string): [string] | [string, string] {
|
|
10
|
+
const at = greeting.search(COMMA);
|
|
11
|
+
if (at < 0) return [greeting];
|
|
12
|
+
const first = greeting.slice(0, at + 1);
|
|
13
|
+
const rest = greeting.slice(at + 1).trim();
|
|
14
|
+
return rest === '' ? [greeting] : [first, rest];
|
|
15
|
+
}
|
package/src/lib/labels.tsx
CHANGED
|
@@ -5,7 +5,9 @@ import { englishWorkLog, type Translate } from './work-log-copy';
|
|
|
5
5
|
export interface AliaChatLabels {
|
|
6
6
|
// Header
|
|
7
7
|
clear: string;
|
|
8
|
-
|
|
8
|
+
back: string;
|
|
9
|
+
share: string;
|
|
10
|
+
more: string;
|
|
9
11
|
chatActions: string;
|
|
10
12
|
// Composer
|
|
11
13
|
placeholder: string;
|
|
@@ -13,8 +15,15 @@ export interface AliaChatLabels {
|
|
|
13
15
|
addPhotos: string;
|
|
14
16
|
startVoice: string;
|
|
15
17
|
voiceFailed: string;
|
|
18
|
+
// Welcome
|
|
19
|
+
/** The empty chat's greeting for a signed-out visitor. */
|
|
20
|
+
greeting: string;
|
|
21
|
+
/** The empty chat's greeting for a named person. */
|
|
22
|
+
greetingNamed: (name: string) => string;
|
|
16
23
|
// Messages
|
|
17
24
|
copy: string;
|
|
25
|
+
helpful: string;
|
|
26
|
+
unhelpful: string;
|
|
18
27
|
copied: string;
|
|
19
28
|
edit: string;
|
|
20
29
|
save: string;
|
|
@@ -38,14 +47,20 @@ export interface AliaChatLabels {
|
|
|
38
47
|
|
|
39
48
|
export const DEFAULT_LABELS: AliaChatLabels = {
|
|
40
49
|
clear: 'Clear',
|
|
41
|
-
|
|
50
|
+
back: 'Back',
|
|
51
|
+
share: 'Share',
|
|
52
|
+
more: 'More',
|
|
42
53
|
chatActions: 'Chat actions',
|
|
43
54
|
placeholder: 'Ask Alia...',
|
|
44
55
|
addMenuGroup: 'Add',
|
|
45
56
|
addPhotos: 'Photos',
|
|
46
57
|
startVoice: 'Start voice',
|
|
47
58
|
voiceFailed: "Couldn't start the voice call",
|
|
59
|
+
greeting: "Hi, what's on your mind?",
|
|
60
|
+
greetingNamed: (name) => `Hi ${name}, what's on your mind?`,
|
|
48
61
|
copy: 'Copy',
|
|
62
|
+
helpful: 'Helpful response',
|
|
63
|
+
unhelpful: 'Unhelpful response',
|
|
49
64
|
copied: 'Copied',
|
|
50
65
|
edit: 'Edit',
|
|
51
66
|
save: 'Save',
|
package/src/lib/questions.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// call: it is stored with the message, survives a reload, and is answered by the person's
|
|
5
5
|
// next message. Pure functions, no React Native, so a host's models and tests can use them.
|
|
6
6
|
|
|
7
|
-
import type { ToolInvocation } from '../types';
|
|
7
|
+
import type { ChatMessage, ToolInvocation } from '../types';
|
|
8
8
|
|
|
9
9
|
/** The tool's name on the wire. */
|
|
10
10
|
export const ASK_USER_QUESTION_TOOL = 'askUserQuestion';
|
|
@@ -76,3 +76,17 @@ export function formatAnswers(questions: AskedQuestion[], answers: QuestionAnswe
|
|
|
76
76
|
})
|
|
77
77
|
.join('\n\n');
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* What Alia asked in her last turn and the person has not answered yet: the questions of the
|
|
82
|
+
* last message when it is hers and finished. A message after them, typed or sent from the card,
|
|
83
|
+
* is the answer, so nothing is pending then.
|
|
84
|
+
*/
|
|
85
|
+
export function pendingQuestions(
|
|
86
|
+
messages: readonly Pick<ChatMessage, 'role' | 'toolInvocations'>[],
|
|
87
|
+
isStreaming: boolean,
|
|
88
|
+
): AskedQuestion[] {
|
|
89
|
+
const last = messages.at(-1);
|
|
90
|
+
if (isStreaming || last?.role !== 'assistant') return [];
|
|
91
|
+
return (last.toolInvocations ?? []).flatMap((invocation) => askedQuestions(invocation) ?? []);
|
|
92
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AliaSuggestion } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which suggestions a draft gets, shared by the Alia app and the SDK chat so they offer the
|
|
5
|
+
* same rows: the welcome suggestions while the draft is short, and from two characters the
|
|
6
|
+
* search's matches with the typed part located inside each.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Up to this many rows. */
|
|
10
|
+
export const MAX_ROWS = 6;
|
|
11
|
+
/** The draft length at which the search takes over from the welcome list. */
|
|
12
|
+
export const SEARCH_FROM = 2;
|
|
13
|
+
/** How long typing settles before the search is asked. */
|
|
14
|
+
export const SEARCH_DEBOUNCE_MS = 200;
|
|
15
|
+
|
|
16
|
+
export interface Completion<T extends AliaSuggestion = AliaSuggestion> {
|
|
17
|
+
suggestion: T;
|
|
18
|
+
/** The typed text's range inside `suggestion.text`, or an empty range. */
|
|
19
|
+
matchStart: number;
|
|
20
|
+
matchEnd: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Which rows to show for a draft: welcome while short, matches from two characters. */
|
|
24
|
+
export function composerCompletions<T extends AliaSuggestion>(
|
|
25
|
+
draft: string,
|
|
26
|
+
welcome: readonly T[] | undefined,
|
|
27
|
+
matches: readonly T[] | undefined,
|
|
28
|
+
): Completion<T>[] {
|
|
29
|
+
const query = draft.trim();
|
|
30
|
+
if (query.length < SEARCH_FROM) {
|
|
31
|
+
return (welcome ?? []).slice(0, MAX_ROWS).map((suggestion) => ({
|
|
32
|
+
suggestion,
|
|
33
|
+
matchStart: 0,
|
|
34
|
+
matchEnd: 0,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
const lower = query.toLowerCase();
|
|
38
|
+
const seen = new Set<string>();
|
|
39
|
+
const rows: Completion<T>[] = [];
|
|
40
|
+
for (const suggestion of matches ?? []) {
|
|
41
|
+
if (rows.length >= MAX_ROWS) break;
|
|
42
|
+
const text = suggestion.text.toLowerCase();
|
|
43
|
+
if (seen.has(text)) continue;
|
|
44
|
+
seen.add(text);
|
|
45
|
+
const start = text.indexOf(lower);
|
|
46
|
+
rows.push({
|
|
47
|
+
suggestion,
|
|
48
|
+
matchStart: start === -1 ? 0 : start,
|
|
49
|
+
matchEnd: start === -1 ? 0 : start + query.length,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return rows;
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `active`, but only once it has held for `delay` ms; false again the moment it stops. For an
|
|
5
|
+
* indicator that must not flash for a state that ends at once (Alia "thinking" for the instant
|
|
6
|
+
* before she hands the turn to an agent, where the agent is the one that thinks).
|
|
7
|
+
*/
|
|
8
|
+
export function useAfterDelay(active: boolean, delay: number): boolean {
|
|
9
|
+
const [settled, setSettled] = useState(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!active) {
|
|
12
|
+
setSettled(false);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const timer = setTimeout(() => setSettled(true), delay);
|
|
16
|
+
return () => clearTimeout(timer);
|
|
17
|
+
}, [active, delay]);
|
|
18
|
+
return settled;
|
|
19
|
+
}
|
package/src/lib/work-log.ts
CHANGED
|
@@ -48,6 +48,7 @@ function resultOf(inv: Pick<ToolInvocation, 'result'>): Record<string, unknown>
|
|
|
48
48
|
|
|
49
49
|
/** The research the live stream reports before the answer is persisted. */
|
|
50
50
|
export interface LiveResearch {
|
|
51
|
+
isComplete?: boolean;
|
|
51
52
|
currentQuery?: string;
|
|
52
53
|
sourcesFound?: number;
|
|
53
54
|
sources?: Array<{ id?: unknown; url?: unknown; title?: unknown }> | null;
|
|
@@ -266,7 +267,7 @@ export function webSearchLog(
|
|
|
266
267
|
mapped.push({
|
|
267
268
|
arrived: true,
|
|
268
269
|
step: {
|
|
269
|
-
label: t('chat.bloom.researching'),
|
|
270
|
+
label: t(research.isComplete ? 'chat.bloom.researched' : 'chat.bloom.researching'),
|
|
270
271
|
query: str(research.currentQuery),
|
|
271
272
|
icon: RiBookOpenLine,
|
|
272
273
|
...(found > 0 ? { meta: t('chat.bloom.sourceCount', { count: found }) } : {}),
|
package/src/tools.ts
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// Pure functions: the status of a call, the numbered sources of a research answer, the
|
|
3
3
|
// words for a tool. They load no React Native, so a host's models and tests can use them.
|
|
4
4
|
|
|
5
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
citationLabel,
|
|
7
|
+
extractCitationSources,
|
|
8
|
+
linkifyCitations,
|
|
9
|
+
normalizeCitationMarkers,
|
|
10
|
+
} from './lib/citations';
|
|
6
11
|
export { domainOf } from './lib/domain';
|
|
7
12
|
export type { CitationSource } from './lib/citations';
|
|
8
13
|
export { getToolDoneLabel, getToolPillLabel } from './lib/task-labels';
|
|
@@ -11,6 +16,7 @@ export {
|
|
|
11
16
|
askedQuestions,
|
|
12
17
|
formatAnswers,
|
|
13
18
|
isQuestionInvocation,
|
|
19
|
+
pendingQuestions,
|
|
14
20
|
} from './lib/questions';
|
|
15
21
|
export type { AskedOption, AskedQuestion, QuestionAnswer } from './lib/questions';
|
|
16
22
|
export { toolCallStatus } from './lib/tool-call-status';
|
package/src/types.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ComponentType } from 'react';
|
|
2
2
|
import type { SharedValue } from 'react-native-reanimated';
|
|
3
3
|
|
|
4
|
-
// ──
|
|
4
|
+
// ── Suggestion Types ──
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** A line the person can pick: `text` is what is shown and what is sent. */
|
|
7
|
+
export interface AliaSuggestion {
|
|
7
8
|
id: string;
|
|
8
|
-
|
|
9
|
-
description: string;
|
|
9
|
+
text: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
// ── Chat Message Types ──
|
|
@@ -63,7 +63,8 @@ export interface ChatMessage {
|
|
|
63
63
|
/** Cached TTS audio URL for read-aloud */
|
|
64
64
|
audioUrl?: string;
|
|
65
65
|
/** Delegated agent identity */
|
|
66
|
-
|
|
66
|
+
/** `character` is the agent's look as the server stores it (JSON), parsed where it is drawn. */
|
|
67
|
+
agentInfo?: { id: string; name: string; character?: unknown };
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export interface ToolInvocation {
|