@alia.onl/sdk 21.3.0 → 21.3.2
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 +30 -0
- package/README.md +15 -5
- package/package.json +1 -1
- package/src/ask.ts +6 -2
- package/src/components/AliaAskCore.tsx +183 -0
- package/src/components/AliaAskProvider.tsx +12 -163
- package/src/components/AliaAskProviderLazy.tsx +27 -0
- package/src/components/AliaChatSheet.tsx +11 -1
- package/src/components/voice/AliaVoiceCall.web.tsx +17 -0
- package/src/hooks/useAttachMenu.ts +10 -11
- package/src/hooks/useSoundEffects.ts +2 -1
- package/src/hooks/useTTS.ts +2 -1
- package/src/lib/copy-es.ts +280 -0
- package/src/lib/copy.ts +15 -9
- package/src/lib/deferred/document-picker.ts +2 -0
- package/src/lib/deferred/document-picker.web.ts +12 -0
- package/src/lib/deferred/expo-audio.ts +2 -0
- package/src/lib/deferred/expo-audio.web.ts +12 -0
- package/src/lib/deferred/image-data-url.ts +5 -0
- package/src/lib/deferred/image-data-url.web.ts +12 -0
- package/src/lib/labels-es.ts +113 -0
- package/src/lib/labels.tsx +10 -3
- package/src/lib/speech-synthesis.ts +2 -1
- package/src/runtime/attachment-utils.ts +4 -3
- package/src/runtime/conversations.ts +9 -2
- package/tsconfig.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @alia.onl/sdk
|
|
2
2
|
|
|
3
|
+
## 21.3.2
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **On web the SDK draws no async chunk boundary of its own**, so an app that lazy-loads its Alia
|
|
8
|
+
sheet keeps the chat in that chunk instead of its first paint. Expo's Metro serializer hoists
|
|
9
|
+
every module two async chunks share into `__common.js`, loaded at first paint; the SDK's own
|
|
10
|
+
`import()` calls (the call's `VoiceSession`, the root entry's `AliaAskProvider` sheet, the
|
|
11
|
+
`expo-audio`, document-picker and image readers) made such chunks inside the app's, and moved the
|
|
12
|
+
whole chat and the Bloom it draws with into `__common.js`. On web those now import statically
|
|
13
|
+
(`.web` forks); native keeps its lazy loads. The root entry's `AliaAskProvider` imports its sheet
|
|
14
|
+
statically on every platform (the root already holds the chat); `@alia.onl/sdk/ask` keeps its
|
|
15
|
+
one lazy boundary. CrowdSource's reviewer: first-paint JS 1.57 MiB gzip → 1.23 MiB, largest
|
|
16
|
+
file 5.05 MiB → 3.79 MiB, its lazy Alia chunk 29 KiB → 404 KiB gzip. `check:entries` holds it.
|
|
17
|
+
- **The source compiles under `erasableSyntaxOnly`.** The SDK ships TypeScript source, and a
|
|
18
|
+
consumer that compiles it with `erasableSyntaxOnly` (OxyHQ/website, `tsc -b`) failed with TS1294
|
|
19
|
+
on two constructor parameter properties in `runtime/conversations.ts`
|
|
20
|
+
(`ConversationNotFoundError.conversationId`, the offline store). They are now declared fields
|
|
21
|
+
assigned in the constructor, and the SDK's own `tsconfig.json` sets `erasableSyntaxOnly`, so
|
|
22
|
+
`typecheck` (CI and `prepublishOnly`) refuses parameter properties, enums and namespaces.
|
|
23
|
+
|
|
24
|
+
## 21.3.1
|
|
25
|
+
|
|
26
|
+
- Ask Alia follows the active account language resolved by Oxy Services instead of defaulting to
|
|
27
|
+
English. Changes to that language update the open sheet without replacing its conversation.
|
|
28
|
+
- Ship a complete Spanish chat catalogue, including composer, voice, menus, thought panel and
|
|
29
|
+
conversation errors. Regional Spanish tags use it; host label overrides remain supported.
|
|
30
|
+
- Apply the sheet's labels above its hooks and all its sheets, so sharing errors and the thought
|
|
31
|
+
panel use the same language as the conversation.
|
|
32
|
+
|
|
3
33
|
## 21.3.0
|
|
4
34
|
|
|
5
35
|
The SDK runs on Expo SDK 58 (React Native 0.88, React 19.3) as well as Expo 57.
|
package/README.md
CHANGED
|
@@ -56,9 +56,12 @@ The empty chat greets the signed-in person by name ("Hi Nate, what's on your min
|
|
|
56
56
|
Failures (a voice call that cannot start, a dictation error) are shown with Bloom's `toast`,
|
|
57
57
|
so the app needs a `ToastOutlet` (`OxyProvider` mounts one), or pass `onError` to handle them.
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
`AliaAskProvider` follows the language Oxy Services resolves for the active account, including
|
|
60
|
+
language changes while its sheet is open. The SDK ships English and Spanish (regional Spanish tags
|
|
61
|
+
such as `es-ES` and `es-MX` use Spanish); other languages fall back to English. `AliaChatSheet` and
|
|
62
|
+
`AliaChatScreen` select their built-in copy from `labels.locale`. Pass `labels` to override any copy
|
|
63
|
+
(`AliaChatLabels`), or `labels.t` for another catalogue. Nested overrides preserve the surrounding
|
|
64
|
+
chat's translations. Bloom's own strings follow `BloomProvider`'s `locale`.
|
|
62
65
|
|
|
63
66
|
Entry points:
|
|
64
67
|
|
|
@@ -171,7 +174,14 @@ dismiss(); // e.g. after a host tool changed the page under the sheet
|
|
|
171
174
|
|
|
172
175
|
Mount it from `@alia.onl/sdk/ask`: that entry is the provider and its hook alone, and the sheet
|
|
173
176
|
loads with the first `ask` (fetched in the background once the person is signed in), so the chat
|
|
174
|
-
stays out of the app's entry bundle. The root entry exports the same two, with the whole chat
|
|
177
|
+
stays out of the app's entry bundle. The root entry exports the same two, with the whole chat
|
|
178
|
+
(its provider imports the sheet statically).
|
|
179
|
+
|
|
180
|
+
**On web, load the chat through ONE async boundary.** Expo's Metro serializer hoists every module
|
|
181
|
+
two async chunks share into `__common.js`, which loads at first paint. The SDK draws no async chunk
|
|
182
|
+
boundary of its own on web (the call, sounds and pickers import statically there), except the one
|
|
183
|
+
of `@alia.onl/sdk/ask`. So either mount the provider from `@alia.onl/sdk/ask`, or lazy-import your
|
|
184
|
+
own module that imports the root entry — not both, or the chat they share lands in `__common.js`.
|
|
175
185
|
|
|
176
186
|
The sheet offers the person's latest conversations with its lead under the greeting
|
|
177
187
|
(`recentConversations={false}` turns it off). The code that configures the sheet sits above it, so
|
|
@@ -430,7 +440,7 @@ ships no model identifier of its own.
|
|
|
430
440
|
```sh
|
|
431
441
|
bun run typecheck # tsc --noEmit
|
|
432
442
|
bun run test # vitest
|
|
433
|
-
bun run check:entries #
|
|
443
|
+
bun run check:entries # entry isolation; on web, no async chunk boundary but the ask entry's; no retired modules
|
|
434
444
|
```
|
|
435
445
|
|
|
436
446
|
## Licence
|
package/package.json
CHANGED
package/src/ask.ts
CHANGED
|
@@ -3,5 +3,9 @@
|
|
|
3
3
|
// An app mounts the provider at its root from here rather than from the root entry, which
|
|
4
4
|
// would bring the whole chat along with it.
|
|
5
5
|
|
|
6
|
-
export { AliaAskProvider, useAskAgent } from './components/
|
|
7
|
-
export type {
|
|
6
|
+
export { AliaAskProvider, useAskAgent } from './components/AliaAskProviderLazy';
|
|
7
|
+
export type {
|
|
8
|
+
AliaAsk,
|
|
9
|
+
AliaAskAgents,
|
|
10
|
+
AliaAskProviderProps,
|
|
11
|
+
} from './components/AliaAskProviderLazy';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Ask Alia", "Ask Sindi": one sheet an app mounts once, opened on any agent from anywhere in it.
|
|
3
|
+
*
|
|
4
|
+
* The app names the agents it offers (`agents`, by a key of its own) and configures the sheet
|
|
5
|
+
* once — its host tools, renderers, words, voice, context, upsell and share. A button calls
|
|
6
|
+
* `useAskAgent().ask(key, turn)`. Signed out, the sheet is not mounted (it could not call Alia):
|
|
7
|
+
* `ask` opens Oxy's sign-in instead, and the person asks again once signed in.
|
|
8
|
+
*
|
|
9
|
+
* This module is the provider without its sheet; each entry builds one with `createAskProvider`:
|
|
10
|
+
* - `@alia.onl/sdk/ask` (`AliaAskProviderLazy`) loads the sheet with the first `ask`, through
|
|
11
|
+
* the one `import()` that keeps the chat out of the app's entry bundle;
|
|
12
|
+
* - the root entry (`AliaAskProvider`) already holds the whole chat, so its sheet is a static
|
|
13
|
+
* import. A lazy one there would put an async chunk boundary INSIDE the SDK, and on web Metro
|
|
14
|
+
* hoists every module two async chunks share into `__common.js`, loaded at first paint: an app
|
|
15
|
+
* that lazy-loads the root entry would get the chat in its first paint instead of its own chunk.
|
|
16
|
+
* Both read and write the one context below, so `useAskAgent` works under either.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useAuth, useOxy } from '@oxy.so/services/ui/client';
|
|
20
|
+
import {
|
|
21
|
+
type ComponentType,
|
|
22
|
+
createContext,
|
|
23
|
+
forwardRef,
|
|
24
|
+
type ReactNode,
|
|
25
|
+
type RefAttributes,
|
|
26
|
+
useCallback,
|
|
27
|
+
useContext,
|
|
28
|
+
useEffect,
|
|
29
|
+
useImperativeHandle,
|
|
30
|
+
useMemo,
|
|
31
|
+
useRef,
|
|
32
|
+
useState,
|
|
33
|
+
} from 'react';
|
|
34
|
+
import type { AliaTurn } from '../hooks/useChatComposer';
|
|
35
|
+
import type { AliaChatSheetProps, AliaChatSheetRef } from './AliaChatSheet';
|
|
36
|
+
import type { AliaAskOpening, AliaAskSheetProps } from './AliaAskSheet';
|
|
37
|
+
|
|
38
|
+
/** The agents an app offers, by its own key. An entry without `id` is Alia. */
|
|
39
|
+
export type AliaAskAgents = Readonly<Record<string, { id?: string }>>;
|
|
40
|
+
|
|
41
|
+
export interface AliaAskProviderProps
|
|
42
|
+
extends Pick<
|
|
43
|
+
AliaChatSheetProps,
|
|
44
|
+
| 'app'
|
|
45
|
+
| 'hostTools'
|
|
46
|
+
| 'toolRenderers'
|
|
47
|
+
| 'labels'
|
|
48
|
+
| 'voice'
|
|
49
|
+
| 'clientContext'
|
|
50
|
+
| 'onFeatureLocked'
|
|
51
|
+
| 'onShare'
|
|
52
|
+
| 'model'
|
|
53
|
+
| 'apiUrl'
|
|
54
|
+
| 'characterRuntimeUrl'
|
|
55
|
+
| 'headerMenu'
|
|
56
|
+
| 'recentConversations'
|
|
57
|
+
> {
|
|
58
|
+
/** The agents `ask` opens, by key. Omitted, the sheet only ever opens on Alia. */
|
|
59
|
+
agents?: AliaAskAgents;
|
|
60
|
+
/** The lines offered over Alia's empty composer; an agent offers its own starters. */
|
|
61
|
+
welcomeSuggestions?: AliaChatSheetProps['welcomeSuggestions'];
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AliaAsk {
|
|
66
|
+
/**
|
|
67
|
+
* Open the sheet on the agent `agentKey` (omitted, Alia), with `turn` already sent when given
|
|
68
|
+
* ("ask Sindi about this listing"). Signed out, opens Oxy's sign-in instead.
|
|
69
|
+
*/
|
|
70
|
+
ask: (agentKey?: string, turn?: AliaTurn) => void;
|
|
71
|
+
/** Open the sheet on the agent `agentKey` (omitted, Alia), on its stored conversation `conversationId`. */
|
|
72
|
+
resume: (agentKey: string | undefined, conversationId: string) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Close the sheet — a host tool that changed the page under it shows the person the result, once
|
|
75
|
+
* the turn has been written.
|
|
76
|
+
*/
|
|
77
|
+
dismiss: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const AskContext = createContext<AliaAsk | null>(null);
|
|
81
|
+
|
|
82
|
+
/** The app's way to open its one Alia sheet. Must be used under `AliaAskProvider`. */
|
|
83
|
+
export function useAskAgent(): AliaAsk {
|
|
84
|
+
const ask = useContext(AskContext);
|
|
85
|
+
if (ask === null) throw new Error('useAskAgent must be used within an AliaAskProvider');
|
|
86
|
+
return ask;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The opening that waits for the sheet of agent `key` to be the one mounted. */
|
|
90
|
+
interface Opening extends AliaAskOpening {
|
|
91
|
+
key: string | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The sheet a provider mounts: `AliaAskSheet`, imported statically or behind `lazy` + `Suspense`. */
|
|
95
|
+
export type AskSheetComponent = ComponentType<AliaAskSheetProps & RefAttributes<AliaChatSheetRef>>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Builds the provider around `Sheet`. `preload`, when given, fetches the sheet's module in the
|
|
99
|
+
* background once signed in, so the first `ask` does not wait on the network.
|
|
100
|
+
*/
|
|
101
|
+
export function createAskProvider(Sheet: AskSheetComponent, preload?: () => Promise<unknown>) {
|
|
102
|
+
/**
|
|
103
|
+
* Mounts the app's one sheet. Its ref is the same `AliaAsk` as `useAskAgent()`, for the code
|
|
104
|
+
* that configures the sheet and so sits above it — a host-tool executor that closes it.
|
|
105
|
+
*/
|
|
106
|
+
return forwardRef<AliaAsk, AliaAskProviderProps>(function AliaAskProvider(
|
|
107
|
+
{ agents, welcomeSuggestions, children, labels, ...sheet },
|
|
108
|
+
ref,
|
|
109
|
+
) {
|
|
110
|
+
const { canUsePrivateApi, signIn } = useAuth();
|
|
111
|
+
const { currentLanguage } = useOxy();
|
|
112
|
+
const localizedLabels = useMemo(
|
|
113
|
+
() => ({ ...labels, locale: labels?.locale ?? currentLanguage }),
|
|
114
|
+
[labels, currentLanguage],
|
|
115
|
+
);
|
|
116
|
+
const sheetRef = useRef<AliaChatSheetRef>(null);
|
|
117
|
+
const [opening, setOpening] = useState<Opening | null>(null);
|
|
118
|
+
const [current, setCurrent] = useState<string | undefined>(undefined);
|
|
119
|
+
/** Nothing is mounted before the first `ask`. */
|
|
120
|
+
const [asked, setAsked] = useState(false);
|
|
121
|
+
const agentsRef = useRef(agents);
|
|
122
|
+
agentsRef.current = agents;
|
|
123
|
+
|
|
124
|
+
const open = useCallback(
|
|
125
|
+
(agentKey: string | undefined, turn?: AliaTurn, conversationId?: string) => {
|
|
126
|
+
if (agentKey !== undefined && agentsRef.current?.[agentKey] === undefined) {
|
|
127
|
+
throw new Error(`AliaAskProvider has no agent "${agentKey}"`);
|
|
128
|
+
}
|
|
129
|
+
if (!canUsePrivateApi) {
|
|
130
|
+
void signIn().catch(() => undefined);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setAsked(true);
|
|
134
|
+
setCurrent(agentKey);
|
|
135
|
+
setOpening((previous) => ({
|
|
136
|
+
key: agentKey,
|
|
137
|
+
turn,
|
|
138
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
139
|
+
serial: (previous?.serial ?? 0) + 1,
|
|
140
|
+
}));
|
|
141
|
+
},
|
|
142
|
+
[canUsePrivateApi, signIn],
|
|
143
|
+
);
|
|
144
|
+
const ask = useCallback((agentKey?: string, turn?: AliaTurn) => open(agentKey, turn), [open]);
|
|
145
|
+
const resume = useCallback(
|
|
146
|
+
(agentKey: string | undefined, conversationId: string) =>
|
|
147
|
+
open(agentKey, undefined, conversationId),
|
|
148
|
+
[open],
|
|
149
|
+
);
|
|
150
|
+
const dismiss = useCallback(() => sheetRef.current?.dismiss(), []);
|
|
151
|
+
|
|
152
|
+
const opened = useCallback(() => setOpening(null), []);
|
|
153
|
+
|
|
154
|
+
// Signed in, the sheet's module comes down in the background, ahead of the first `ask`.
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (canUsePrivateApi && preload !== undefined) void preload().catch(() => undefined);
|
|
157
|
+
}, [canUsePrivateApi]);
|
|
158
|
+
|
|
159
|
+
const agentId = current === undefined ? undefined : agents?.[current]?.id;
|
|
160
|
+
const agent = useMemo(() => (agentId === undefined ? undefined : { id: agentId }), [agentId]);
|
|
161
|
+
const value = useMemo<AliaAsk>(() => ({ ask, resume, dismiss }), [ask, resume, dismiss]);
|
|
162
|
+
useImperativeHandle(ref, () => value, [value]);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<AskContext.Provider value={value}>
|
|
166
|
+
{children}
|
|
167
|
+
{canUsePrivateApi && asked ? (
|
|
168
|
+
<Sheet
|
|
169
|
+
key={agentId ?? 'alia'}
|
|
170
|
+
ref={sheetRef}
|
|
171
|
+
{...sheet}
|
|
172
|
+
labels={localizedLabels}
|
|
173
|
+
agent={agent}
|
|
174
|
+
welcomeSuggestions={agent === undefined ? welcomeSuggestions : undefined}
|
|
175
|
+
// A different agent is a new sheet: the opening waits for that one to mount.
|
|
176
|
+
opening={opening !== null && opening.key === current ? opening : null}
|
|
177
|
+
onOpened={opened}
|
|
178
|
+
/>
|
|
179
|
+
) : null}
|
|
180
|
+
</AskContext.Provider>
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -1,169 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The ask provider of the root entry (`@alia.onl/sdk`): its sheet is a static import.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* that mounts this provider at its root — imported from `@alia.onl/sdk/ask` — keeps the chat out
|
|
11
|
-
* of its entry bundle. Signed in, its module is fetched in the background so the first `ask`
|
|
12
|
-
* does not wait on the network.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { useAuth } from '@oxy.so/services/ui/client';
|
|
16
|
-
import {
|
|
17
|
-
createContext,
|
|
18
|
-
forwardRef,
|
|
19
|
-
lazy,
|
|
20
|
-
type ReactNode,
|
|
21
|
-
Suspense,
|
|
22
|
-
useCallback,
|
|
23
|
-
useContext,
|
|
24
|
-
useEffect,
|
|
25
|
-
useImperativeHandle,
|
|
26
|
-
useMemo,
|
|
27
|
-
useRef,
|
|
28
|
-
useState,
|
|
29
|
-
} from 'react';
|
|
30
|
-
import type { AliaTurn } from '../hooks/useChatComposer';
|
|
31
|
-
import type { AliaChatSheetProps, AliaChatSheetRef } from './AliaChatSheet';
|
|
32
|
-
import type { AliaAskOpening } from './AliaAskSheet';
|
|
33
|
-
|
|
34
|
-
const loadSheet = () => import('./AliaAskSheet');
|
|
35
|
-
const LazyAskSheet = lazy(() => loadSheet().then((module) => ({ default: module.AliaAskSheet })));
|
|
36
|
-
|
|
37
|
-
/** The agents an app offers, by its own key. An entry without `id` is Alia. */
|
|
38
|
-
export type AliaAskAgents = Readonly<Record<string, { id?: string }>>;
|
|
39
|
-
|
|
40
|
-
export interface AliaAskProviderProps
|
|
41
|
-
extends Pick<
|
|
42
|
-
AliaChatSheetProps,
|
|
43
|
-
| 'app'
|
|
44
|
-
| 'hostTools'
|
|
45
|
-
| 'toolRenderers'
|
|
46
|
-
| 'labels'
|
|
47
|
-
| 'voice'
|
|
48
|
-
| 'clientContext'
|
|
49
|
-
| 'onFeatureLocked'
|
|
50
|
-
| 'onShare'
|
|
51
|
-
| 'model'
|
|
52
|
-
| 'apiUrl'
|
|
53
|
-
| 'characterRuntimeUrl'
|
|
54
|
-
| 'headerMenu'
|
|
55
|
-
| 'recentConversations'
|
|
56
|
-
> {
|
|
57
|
-
/** The agents `ask` opens, by key. Omitted, the sheet only ever opens on Alia. */
|
|
58
|
-
agents?: AliaAskAgents;
|
|
59
|
-
/** The lines offered over Alia's empty composer; an agent offers its own starters. */
|
|
60
|
-
welcomeSuggestions?: AliaChatSheetProps['welcomeSuggestions'];
|
|
61
|
-
children: ReactNode;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface AliaAsk {
|
|
65
|
-
/**
|
|
66
|
-
* Open the sheet on the agent `agentKey` (omitted, Alia), with `turn` already sent when given
|
|
67
|
-
* ("ask Sindi about this listing"). Signed out, opens Oxy's sign-in instead.
|
|
68
|
-
*/
|
|
69
|
-
ask: (agentKey?: string, turn?: AliaTurn) => void;
|
|
70
|
-
/** Open the sheet on the agent `agentKey` (omitted, Alia), on its stored conversation `conversationId`. */
|
|
71
|
-
resume: (agentKey: string | undefined, conversationId: string) => void;
|
|
72
|
-
/**
|
|
73
|
-
* Close the sheet — a host tool that changed the page under it shows the person the result, once
|
|
74
|
-
* the turn has been written.
|
|
75
|
-
*/
|
|
76
|
-
dismiss: () => void;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const AskContext = createContext<AliaAsk | null>(null);
|
|
80
|
-
|
|
81
|
-
/** The app's way to open its one Alia sheet. Must be used under `AliaAskProvider`. */
|
|
82
|
-
export function useAskAgent(): AliaAsk {
|
|
83
|
-
const ask = useContext(AskContext);
|
|
84
|
-
if (ask === null) throw new Error('useAskAgent must be used within an AliaAskProvider');
|
|
85
|
-
return ask;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The opening that waits for the sheet of agent `key` to be the one mounted. */
|
|
89
|
-
interface Opening extends AliaAskOpening {
|
|
90
|
-
key: string | undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Mounts the app's one sheet. Its ref is the same `AliaAsk` as `useAskAgent()`, for the code that
|
|
95
|
-
* configures the sheet and so sits above it — a host-tool executor that closes it.
|
|
4
|
+
* The root entry already holds the whole chat, so loading `AliaAskSheet` lazily here would save
|
|
5
|
+
* nothing and cost a lot on web: it is an async chunk boundary inside the SDK, and Metro hoists
|
|
6
|
+
* every module two async chunks share into `__common.js`, which loads at first paint. An app that
|
|
7
|
+
* lazy-loads its Alia sheet would then get the chat (and the Bloom it draws with) in its first
|
|
8
|
+
* paint. An app that wants the chat out of its entry bundle mounts the provider from
|
|
9
|
+
* `@alia.onl/sdk/ask` (`AliaAskProviderLazy`) instead. See `AliaAskCore` for the provider itself.
|
|
96
10
|
*/
|
|
97
|
-
export const AliaAskProvider = forwardRef<AliaAsk, AliaAskProviderProps>(function AliaAskProvider(
|
|
98
|
-
{ agents, welcomeSuggestions, children, ...sheet },
|
|
99
|
-
ref,
|
|
100
|
-
) {
|
|
101
|
-
const { canUsePrivateApi, signIn } = useAuth();
|
|
102
|
-
const sheetRef = useRef<AliaChatSheetRef>(null);
|
|
103
|
-
const [opening, setOpening] = useState<Opening | null>(null);
|
|
104
|
-
const [current, setCurrent] = useState<string | undefined>(undefined);
|
|
105
|
-
/** Nothing is mounted before the first `ask`. */
|
|
106
|
-
const [asked, setAsked] = useState(false);
|
|
107
|
-
const agentsRef = useRef(agents);
|
|
108
|
-
agentsRef.current = agents;
|
|
109
|
-
|
|
110
|
-
const open = useCallback(
|
|
111
|
-
(agentKey: string | undefined, turn?: AliaTurn, conversationId?: string) => {
|
|
112
|
-
if (agentKey !== undefined && agentsRef.current?.[agentKey] === undefined) {
|
|
113
|
-
throw new Error(`AliaAskProvider has no agent "${agentKey}"`);
|
|
114
|
-
}
|
|
115
|
-
if (!canUsePrivateApi) {
|
|
116
|
-
void signIn().catch(() => undefined);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
setAsked(true);
|
|
120
|
-
setCurrent(agentKey);
|
|
121
|
-
setOpening((previous) => ({
|
|
122
|
-
key: agentKey,
|
|
123
|
-
turn,
|
|
124
|
-
...(conversationId === undefined ? {} : { conversationId }),
|
|
125
|
-
serial: (previous?.serial ?? 0) + 1,
|
|
126
|
-
}));
|
|
127
|
-
},
|
|
128
|
-
[canUsePrivateApi, signIn],
|
|
129
|
-
);
|
|
130
|
-
const ask = useCallback((agentKey?: string, turn?: AliaTurn) => open(agentKey, turn), [open]);
|
|
131
|
-
const resume = useCallback(
|
|
132
|
-
(agentKey: string | undefined, conversationId: string) =>
|
|
133
|
-
open(agentKey, undefined, conversationId),
|
|
134
|
-
[open],
|
|
135
|
-
);
|
|
136
|
-
const dismiss = useCallback(() => sheetRef.current?.dismiss(), []);
|
|
137
|
-
|
|
138
|
-
const opened = useCallback(() => setOpening(null), []);
|
|
139
11
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (canUsePrivateApi) void loadSheet().catch(() => undefined);
|
|
143
|
-
}, [canUsePrivateApi]);
|
|
12
|
+
import { createAskProvider } from './AliaAskCore';
|
|
13
|
+
import { AliaAskSheet } from './AliaAskSheet';
|
|
144
14
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const value = useMemo<AliaAsk>(() => ({ ask, resume, dismiss }), [ask, resume, dismiss]);
|
|
148
|
-
useImperativeHandle(ref, () => value, [value]);
|
|
15
|
+
export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './AliaAskCore';
|
|
16
|
+
export { useAskAgent } from './AliaAskCore';
|
|
149
17
|
|
|
150
|
-
|
|
151
|
-
<AskContext.Provider value={value}>
|
|
152
|
-
{children}
|
|
153
|
-
{canUsePrivateApi && asked ? (
|
|
154
|
-
<Suspense fallback={null}>
|
|
155
|
-
<LazyAskSheet
|
|
156
|
-
key={agentId ?? 'alia'}
|
|
157
|
-
ref={sheetRef}
|
|
158
|
-
{...sheet}
|
|
159
|
-
agent={agent}
|
|
160
|
-
welcomeSuggestions={agent === undefined ? welcomeSuggestions : undefined}
|
|
161
|
-
// A different agent is a new sheet: the opening waits for that one to mount.
|
|
162
|
-
opening={opening !== null && opening.key === current ? opening : null}
|
|
163
|
-
onOpened={opened}
|
|
164
|
-
/>
|
|
165
|
-
</Suspense>
|
|
166
|
-
) : null}
|
|
167
|
-
</AskContext.Provider>
|
|
168
|
-
);
|
|
169
|
-
});
|
|
18
|
+
export const AliaAskProvider = createAskProvider(AliaAskSheet);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ask provider of `@alia.onl/sdk/ask`: its sheet is loaded lazily and mounted on the first
|
|
3
|
+
* `ask`, so an app that mounts this provider at its root keeps the chat out of its entry bundle.
|
|
4
|
+
* Signed in, the sheet's module is fetched in the background so the first `ask` does not wait on
|
|
5
|
+
* the network.
|
|
6
|
+
*
|
|
7
|
+
* This `import()` is the ONE async boundary the SDK draws, and only for this entry: everything
|
|
8
|
+
* behind it (`AliaAskSheet` and the chat) imports statically on web, so Metro emits one chunk for
|
|
9
|
+
* the sheet and hoists nothing of it into `__common.js` (`scripts/check-entry-isolation.mjs`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { lazy, Suspense } from 'react';
|
|
13
|
+
import { type AskSheetComponent, createAskProvider } from './AliaAskCore';
|
|
14
|
+
|
|
15
|
+
const loadSheet = () => import('./AliaAskSheet');
|
|
16
|
+
const LazyAskSheet = lazy(() => loadSheet().then((module) => ({ default: module.AliaAskSheet })));
|
|
17
|
+
|
|
18
|
+
const SuspendedAskSheet: AskSheetComponent = (props) => (
|
|
19
|
+
<Suspense fallback={null}>
|
|
20
|
+
<LazyAskSheet {...props} />
|
|
21
|
+
</Suspense>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './AliaAskCore';
|
|
25
|
+
export { useAskAgent } from './AliaAskCore';
|
|
26
|
+
|
|
27
|
+
export const AliaAskProvider = createAskProvider(SuspendedAskSheet, loadSheet);
|
|
@@ -16,7 +16,7 @@ import { useSharedValue } from 'react-native-reanimated';
|
|
|
16
16
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
17
|
import { BREAKPOINTS } from '@oxy.so/bloom/styles';
|
|
18
18
|
import { ALIA_CHARACTER_RUNTIME_URL } from '../lib/character-runtime';
|
|
19
|
-
import type
|
|
19
|
+
import { AliaChatLabelsProvider, type AliaChatLabels } from '../lib/labels';
|
|
20
20
|
import type { AliaToolRenderers } from '../lib/tool-renderers';
|
|
21
21
|
import { useShareHandler } from '../hooks/useShareHandler';
|
|
22
22
|
import type { AliaRuntimeHost } from '../runtime/config';
|
|
@@ -109,6 +109,16 @@ export interface AliaChatSheetRef {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
112
|
+
function AliaChatSheet(props, ref) {
|
|
113
|
+
return (
|
|
114
|
+
<AliaChatLabelsProvider labels={props.labels}>
|
|
115
|
+
<AliaChatSheetBody {...props} ref={ref} />
|
|
116
|
+
</AliaChatLabelsProvider>
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const AliaChatSheetBody = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
112
122
|
(
|
|
113
123
|
{
|
|
114
124
|
agent,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The call of a conversation screen, on web: its bar, while a call is open.
|
|
3
|
+
*
|
|
4
|
+
* Native loads the call lazily (`AliaVoiceCall.tsx`) so a screen that never calls never evaluates
|
|
5
|
+
* `react-native-audio-api`. Web never reaches that engine (`lib/voice-audio-engine.ts` is the Web
|
|
6
|
+
* Audio one), and a lazy `import()` here would be an async chunk boundary inside the SDK: Metro
|
|
7
|
+
* hoists every module two async chunks share into `__common.js`, loaded at first paint, so an app
|
|
8
|
+
* that lazy-loads its Alia sheet would get the chat in its first paint. On web the call is a
|
|
9
|
+
* static import and travels in whatever chunk holds the chat.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { VoiceSessionProps } from '../../types';
|
|
13
|
+
import { VoiceSession } from './VoiceSession';
|
|
14
|
+
|
|
15
|
+
export function AliaVoiceCall(props: VoiceSessionProps) {
|
|
16
|
+
return <VoiceSession {...props} />;
|
|
17
|
+
}
|
|
@@ -2,6 +2,7 @@ import { RiFileTextLine } from '@oxy.so/bloom/icons/RiFileTextLine';
|
|
|
2
2
|
import { RiImageLine } from '@oxy.so/bloom/icons/RiImageLine';
|
|
3
3
|
import { useCallback, useMemo } from 'react';
|
|
4
4
|
import type { AliaAddMenu } from '../lib/add-menu';
|
|
5
|
+
import { loadDocumentPicker } from '../lib/deferred/document-picker';
|
|
5
6
|
import type { IntakeKind } from '../lib/attachment-intake';
|
|
6
7
|
import { useLabels } from '../lib/labels';
|
|
7
8
|
import { ALIA_FEATURES, type FeatureOffer } from '../runtime/agents';
|
|
@@ -71,17 +72,15 @@ export function useAttachMenu(
|
|
|
71
72
|
size: asset.size,
|
|
72
73
|
mimeType: asset.mimeType,
|
|
73
74
|
}))
|
|
74
|
-
: //
|
|
75
|
-
((await (await
|
|
76
|
-
(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
75
|
+
: // On native the picker loads with the first document: a chat that never attaches one never does.
|
|
76
|
+
((await (await loadDocumentPicker()).pickDocuments()) ?? []).map((asset) => ({
|
|
77
|
+
id: attachmentId('doc'),
|
|
78
|
+
type: 'document' as const,
|
|
79
|
+
name: asset.name,
|
|
80
|
+
uri: asset.uri,
|
|
81
|
+
size: asset.size,
|
|
82
|
+
mimeType: asset.mimeType,
|
|
83
|
+
}));
|
|
85
84
|
for (const attachment of picked) store.addAttachment(address, attachment);
|
|
86
85
|
},
|
|
87
86
|
[offers, onFeatureLocked, pickImage, address],
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { useEffect, useRef, useCallback } from 'react';
|
|
11
11
|
import type { AudioPlayer, AudioSource } from 'expo-audio';
|
|
12
|
+
import { loadExpoAudio } from '../lib/deferred/expo-audio';
|
|
12
13
|
import type { AgentState } from '../types';
|
|
13
14
|
|
|
14
15
|
export type SoundName = 'thinking' | 'toolCall' | 'voiceConnect' | 'voiceDisconnect';
|
|
@@ -37,7 +38,7 @@ export function useSoundEffects(enabled: boolean = false, sounds: SoundSources =
|
|
|
37
38
|
|
|
38
39
|
(async () => {
|
|
39
40
|
try {
|
|
40
|
-
const { createAudioPlayer } = await
|
|
41
|
+
const { createAudioPlayer } = await loadExpoAudio();
|
|
41
42
|
if (!mounted) return;
|
|
42
43
|
|
|
43
44
|
for (const name of SOUND_NAMES) {
|
package/src/hooks/useTTS.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { useOxy } from '@oxy.so/services/ui/client';
|
|
|
5
5
|
import { errorMessage } from '../lib/utils';
|
|
6
6
|
import { create } from 'zustand';
|
|
7
7
|
import { createAudioLevelMeter } from '../lib/audio-level';
|
|
8
|
+
import { loadExpoAudio } from '../lib/deferred/expo-audio';
|
|
8
9
|
import { requestSpeechClip } from '../lib/speech-synthesis';
|
|
9
10
|
import { ALIA_API_URL } from '../lib/origins';
|
|
10
11
|
|
|
@@ -142,7 +143,7 @@ export function useTTS(options: UseTTSOptions = {}) {
|
|
|
142
143
|
|
|
143
144
|
(async () => {
|
|
144
145
|
try {
|
|
145
|
-
const { createAudioPlayer } = await
|
|
146
|
+
const { createAudioPlayer } = await loadExpoAudio();
|
|
146
147
|
if (signal.aborted) return;
|
|
147
148
|
/**
|
|
148
149
|
* `crossOrigin` is web-only and ignored elsewhere, and it is what makes
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/** The SDK chat catalogue, matching the Spanish Alia app copy. */
|
|
2
|
+
import type { CopyEntry } from './copy-en';
|
|
3
|
+
|
|
4
|
+
export const COPY_ES: Readonly<Record<string, CopyEntry>> = {
|
|
5
|
+
'automations.cron.dailyAt': 'Todos los días a las {{time}}',
|
|
6
|
+
'automations.cron.dayPlural.0': 'domingos',
|
|
7
|
+
'automations.cron.dayPlural.1': 'lunes',
|
|
8
|
+
'automations.cron.dayPlural.2': 'martes',
|
|
9
|
+
'automations.cron.dayPlural.3': 'miércoles',
|
|
10
|
+
'automations.cron.dayPlural.4': 'jueves',
|
|
11
|
+
'automations.cron.dayPlural.5': 'viernes',
|
|
12
|
+
'automations.cron.dayPlural.6': 'sábados',
|
|
13
|
+
'automations.cron.daysAt': 'Los {{days}} a las {{time}}',
|
|
14
|
+
'automations.cron.everyHour': 'Cada hora',
|
|
15
|
+
'automations.cron.everyHours': 'Cada {{count}} horas',
|
|
16
|
+
'automations.cron.everyMinutes': { one: 'Cada minuto', other: 'Cada {{count}} minutos' },
|
|
17
|
+
'automations.cron.weekdaysAt': 'De lunes a viernes a las {{time}}',
|
|
18
|
+
'automations.cron.weekendsAt': 'Los fines de semana a las {{time}}',
|
|
19
|
+
'automations.trigger.anyApp': 'Cualquier app',
|
|
20
|
+
'automations.trigger.anyEvent': 'Cualquier evento',
|
|
21
|
+
'automations.trigger.manual': 'A petición',
|
|
22
|
+
'automations.trigger.unscheduled': 'Sin programar',
|
|
23
|
+
'chat.agentRun.by': 'por %{name}',
|
|
24
|
+
'chat.agentRun.complete': 'Tarea completada',
|
|
25
|
+
'chat.agentRun.completed': 'Completada',
|
|
26
|
+
'chat.agentRun.credits': '%{count} créditos',
|
|
27
|
+
'chat.agentRun.error': 'Error',
|
|
28
|
+
'chat.agentRun.failed': 'Fallida',
|
|
29
|
+
'chat.agentRun.planItems': '%{done}/%{total} pasos del plan',
|
|
30
|
+
'chat.agentRun.planSummary': 'Resumen del plan',
|
|
31
|
+
'chat.agentRun.steps': '%{count} pasos',
|
|
32
|
+
'chat.agentRun.taskComplete': 'Tarea completada',
|
|
33
|
+
'chat.agentRun.taskFailed': 'La tarea falló',
|
|
34
|
+
'chat.agentRun.unknown': 'Desconocido',
|
|
35
|
+
'chat.agentRun.working': 'Agente trabajando',
|
|
36
|
+
'chat.backToLatest': 'Volver a lo último',
|
|
37
|
+
'chat.blockFailed': 'Esta parte de la respuesta no se ha podido mostrar.',
|
|
38
|
+
'chat.bloom.context.freeSpace': 'Espacio libre',
|
|
39
|
+
'chat.bloom.context.mcp': 'Herramientas MCP',
|
|
40
|
+
'chat.bloom.context.memory': 'Memoria',
|
|
41
|
+
'chat.bloom.context.messages': 'Mensajes',
|
|
42
|
+
'chat.bloom.context.skills': 'Skills',
|
|
43
|
+
'chat.bloom.context.system': 'Prompt del sistema',
|
|
44
|
+
'chat.bloom.context.title': 'Ventana de contexto',
|
|
45
|
+
'chat.bloom.context.tools': 'Herramientas',
|
|
46
|
+
'chat.bloom.createdFile': 'Creó',
|
|
47
|
+
'chat.bloom.limits.close': 'Cerrar panel de créditos',
|
|
48
|
+
'chat.bloom.limits.dailyFree': 'Créditos gratis diarios',
|
|
49
|
+
'chat.bloom.limits.planUsage': 'Límites de uso del plan',
|
|
50
|
+
'chat.bloom.limits.resetsDaily': 'Se renueva cada 24 horas',
|
|
51
|
+
'chat.bloom.limits.resetsInHours': 'Se renueva en {{hours}} h {{minutes}} min',
|
|
52
|
+
'chat.bloom.limits.resetsInMinutes': 'Se renueva en {{minutes}} min',
|
|
53
|
+
'chat.bloom.limits.resetsSoon': 'Se renueva pronto',
|
|
54
|
+
'chat.bloom.limits.window': 'Límite de {{hours}} horas',
|
|
55
|
+
'chat.bloom.limits.windowFresh': 'Se renueva {{hours}} horas después de tu próximo mensaje',
|
|
56
|
+
'chat.bloom.noResults': 'Sin resultados',
|
|
57
|
+
'chat.bloom.researched': 'Investigó',
|
|
58
|
+
'chat.bloom.researching': 'Investigando',
|
|
59
|
+
'chat.bloom.resultCount': { one: '1 resultado', other: '{{count}} resultados' },
|
|
60
|
+
'chat.bloom.scrollToLatest': 'Ir a los últimos mensajes',
|
|
61
|
+
'chat.bloom.searchCount': { one: '1 búsqueda', other: '{{count}} búsquedas' },
|
|
62
|
+
'chat.bloom.searchFailed': 'La búsqueda falló',
|
|
63
|
+
'chat.bloom.searchedWeb': 'Buscó en la web',
|
|
64
|
+
'chat.bloom.sourceCount': { one: '1 fuente', other: '{{count}} fuentes' },
|
|
65
|
+
'chat.bloom.taskFailed': 'Devolvió un error',
|
|
66
|
+
'chat.bloom.taskInput': 'Con',
|
|
67
|
+
'chat.bloom.taskStopped': 'No terminó',
|
|
68
|
+
'chat.bloom.visited': 'Visitó',
|
|
69
|
+
'chat.bloom.writingFile': 'Escribiendo',
|
|
70
|
+
'chat.copied': '¡Copiado!',
|
|
71
|
+
'chat.copiedToClipboard': 'Copiado al portapapeles',
|
|
72
|
+
'chat.copy': 'Copiar',
|
|
73
|
+
'chat.copyFailed': 'No se pudo copiar al portapapeles',
|
|
74
|
+
'chat.dislike': 'No me gusta',
|
|
75
|
+
'chat.edit': 'Editar',
|
|
76
|
+
'chat.feedbackFailed': 'No se ha podido guardar tu opinión.',
|
|
77
|
+
'chat.like': 'Me gusta',
|
|
78
|
+
'chat.loadingHistory': 'Cargando mensajes anteriores…',
|
|
79
|
+
'chat.newConversationAccept': 'Empezar una nueva',
|
|
80
|
+
'chat.newConversationDismiss': 'Ahora no',
|
|
81
|
+
'chat.newConversationOffer': '¿Empezar una conversación nueva?',
|
|
82
|
+
'chat.newStretch': 'Conversación nueva',
|
|
83
|
+
'chat.readAloud': 'Leer en voz alta',
|
|
84
|
+
'chat.readAloudFailed': 'No se pudo leer en voz alta',
|
|
85
|
+
'chat.regenerate': 'Volver a generar',
|
|
86
|
+
'chat.retry': 'Reintentar',
|
|
87
|
+
'chat.richBlocks.source': 'Fuente: {{source}}',
|
|
88
|
+
'chat.searchThreadAgent': 'Agente',
|
|
89
|
+
'chat.searchThreadEmpty': 'No hay coincidencias.',
|
|
90
|
+
'chat.searchThreadFailed': 'No se pudo buscar.',
|
|
91
|
+
'chat.searchThreadHint':
|
|
92
|
+
'Busca todo lo que le has dicho a este agente, en todas las conversaciones.',
|
|
93
|
+
'chat.searchThreadPlaceholder': 'Buscar en este hilo',
|
|
94
|
+
'chat.searchThreadSearching': 'Buscando…',
|
|
95
|
+
'chat.searchThreadYou': 'Tú',
|
|
96
|
+
'chat.sources': 'Fuentes',
|
|
97
|
+
'chat.stopReading': 'Dejar de leer',
|
|
98
|
+
'chat.thanksFeedback': '¡Gracias por tu opinión!',
|
|
99
|
+
'chat.thinking': 'Pensando',
|
|
100
|
+
'chat.today': 'Hoy',
|
|
101
|
+
'chat.toolCard.openTask': 'Abrir la tarea programada %{objective}',
|
|
102
|
+
'chat.toolCard.paused': 'En pausa',
|
|
103
|
+
'chat.toolCard.scheduled': 'Programada',
|
|
104
|
+
'chat.turnActions': 'Acciones del mensaje',
|
|
105
|
+
'chat.consultFailed': '%{name} no pudo responder.',
|
|
106
|
+
'chat.shareFailed': 'No se pudo crear el enlace para compartir.',
|
|
107
|
+
'chat.sharedFailed': 'No se pudo cargar esta conversación.',
|
|
108
|
+
'chat.sharedGone': 'Esta conversación compartida ya no está disponible.',
|
|
109
|
+
'chat.turnFailed': 'Alia no ha podido responder ahora mismo.',
|
|
110
|
+
'chat.turnFailedRetryHint': 'Inténtalo de nuevo.',
|
|
111
|
+
'chat.turnInterrupted': 'Alia se interrumpió antes de terminar.',
|
|
112
|
+
'chat.working': 'Trabajando',
|
|
113
|
+
'chat.yesterday': 'Ayer',
|
|
114
|
+
'common.close': 'Cerrar',
|
|
115
|
+
'composer.cancelEdit': 'Cancelar',
|
|
116
|
+
'composer.editPlaceholder': 'Edita tu mensaje',
|
|
117
|
+
'composer.editingMessage': 'Editando mensaje',
|
|
118
|
+
'faircoin.indexed':
|
|
119
|
+
'Precio indexado del pool WFAIR/USDC en Base — FairCoin no cotiza en ningún exchange',
|
|
120
|
+
'faircoin.liquidity': 'Liquidez',
|
|
121
|
+
'faircoin.marketCap': 'Capitalización',
|
|
122
|
+
'faircoin.name': 'FairCoin',
|
|
123
|
+
'faircoin.noChart': 'Sólo hay una muestra — no bastan para trazar una línea.',
|
|
124
|
+
'faircoin.noPrice': 'Sin cotización',
|
|
125
|
+
'faircoin.noPriceHint': 'El pool WFAIR/USDC no tenía precio indexado cuando se preguntó.',
|
|
126
|
+
'faircoin.noSamples': 'Todavía no hay muestras de este periodo.',
|
|
127
|
+
'faircoin.range.1y': '1 A',
|
|
128
|
+
'faircoin.range.24h': '24 h',
|
|
129
|
+
'faircoin.range.30d': '30 D',
|
|
130
|
+
'faircoin.range.7d': '7 D',
|
|
131
|
+
'faircoin.range.all': 'Todo',
|
|
132
|
+
'faircoin.scale.billion': ' mil M',
|
|
133
|
+
'faircoin.scale.million': ' M',
|
|
134
|
+
'faircoin.scale.thousand': ' mil',
|
|
135
|
+
'faircoin.scale.trillion': ' B',
|
|
136
|
+
'faircoin.source': 'De %{source}',
|
|
137
|
+
'faircoin.symbol': 'FAIR',
|
|
138
|
+
'faircoin.unavailable': 'Sin histórico para %{ranges}',
|
|
139
|
+
'faircoin.updated': 'Actualizado %{time}',
|
|
140
|
+
'faircoin.volume': 'Volumen 24 h',
|
|
141
|
+
'market.dayHigh': 'Máximo del día',
|
|
142
|
+
'market.dayLow': 'Mínimo del día',
|
|
143
|
+
'market.marketCap': 'Capitalización',
|
|
144
|
+
'market.noChart': 'No hay histórico suficiente para dibujar este rango',
|
|
145
|
+
'market.noPrice': 'Sin cotización disponible',
|
|
146
|
+
'market.open': 'Apertura',
|
|
147
|
+
'market.range.1D': '1D',
|
|
148
|
+
'market.range.1M': '1M',
|
|
149
|
+
'market.range.1Y': '1A',
|
|
150
|
+
'market.range.5D': '5D',
|
|
151
|
+
'market.range.5Y': '5A',
|
|
152
|
+
'market.range.6M': '6M',
|
|
153
|
+
'market.range.MAX': 'MAX',
|
|
154
|
+
'market.range.YTD': 'YTD',
|
|
155
|
+
'market.volume': 'Volumen (24 h)',
|
|
156
|
+
'panels.canvas.image': 'Imagen',
|
|
157
|
+
'panels.canvas.unsupported': 'Tipo de componente no compatible: {{type}}',
|
|
158
|
+
'panels.chart.share': 'Porcentaje',
|
|
159
|
+
'panels.chart.value': 'Valor',
|
|
160
|
+
'panels.code.copied': 'Código copiado',
|
|
161
|
+
'panels.code.copy': 'Copiar código',
|
|
162
|
+
'panels.form.enter': 'Escribe {{field}}',
|
|
163
|
+
'panels.form.select': 'Selecciona…',
|
|
164
|
+
'panels.form.submit': 'Enviar',
|
|
165
|
+
'powerLevels.auto.description': 'Elige el nivel adecuado para cada mensaje',
|
|
166
|
+
'powerLevels.auto.label': 'Auto',
|
|
167
|
+
'powerLevels.high.description': 'Piensa más en las preguntas difíciles',
|
|
168
|
+
'powerLevels.high.label': 'Alto',
|
|
169
|
+
'powerLevels.instant.description': 'Lo más rápido y barato, para respuestas rápidas',
|
|
170
|
+
'powerLevels.instant.label': 'Instantáneo',
|
|
171
|
+
'powerLevels.medium.description': 'Rápido y capaz, para el día a día',
|
|
172
|
+
'powerLevels.medium.label': 'Medio',
|
|
173
|
+
'powerLevels.onDevice': 'En %{device}',
|
|
174
|
+
'powerLevels.pro.description': 'Modelos punteros; más lento y más caro',
|
|
175
|
+
'powerLevels.pro.label': 'Pro',
|
|
176
|
+
'powerLevels.ultra.description': 'El más capaz; el más lento y el más caro',
|
|
177
|
+
'powerLevels.ultra.label': 'Ultra',
|
|
178
|
+
'powerLevels.xhigh.description': 'Piensa aún más; algo más lento',
|
|
179
|
+
'powerLevels.xhigh.label': 'Muy alto',
|
|
180
|
+
'tasks.tool.done.agentSearch': 'Buscó agentes',
|
|
181
|
+
'tasks.tool.done.browse': 'Navegó',
|
|
182
|
+
'tasks.tool.done.codeInterpreter': 'Ejecutó código',
|
|
183
|
+
'tasks.tool.done.consultAgent': 'Consultó a un agente',
|
|
184
|
+
'tasks.tool.done.fileEdit': 'Editó un archivo',
|
|
185
|
+
'tasks.tool.done.generateFile': 'Generó un archivo',
|
|
186
|
+
'tasks.tool.done.shellExec': 'Ejecutó un comando',
|
|
187
|
+
'tasks.tool.done.loadTools': 'Cargó herramientas',
|
|
188
|
+
'tasks.tool.done.userMemory': 'Recordó',
|
|
189
|
+
'tasks.tool.done.webScraper': 'Leyó la página',
|
|
190
|
+
'tasks.tool.pill.agentSearch': 'Buscando agentes',
|
|
191
|
+
'tasks.tool.pill.browse': 'Navegando',
|
|
192
|
+
'tasks.tool.pill.codeInterpreter': 'Ejecutando código',
|
|
193
|
+
'tasks.tool.pill.consultAgent': 'Consultando a un agente',
|
|
194
|
+
'tasks.tool.pill.fileEdit': 'Editando archivo',
|
|
195
|
+
'tasks.tool.pill.generateFile': 'Generando archivo',
|
|
196
|
+
'tasks.tool.pill.sendDiscord': 'Discord',
|
|
197
|
+
'tasks.tool.pill.sendEmail': 'Correo',
|
|
198
|
+
'tasks.tool.pill.sendSignal': 'Signal',
|
|
199
|
+
'tasks.tool.pill.sendSlack': 'Slack',
|
|
200
|
+
'tasks.tool.pill.sendTelegramMessage': 'Telegram',
|
|
201
|
+
'tasks.tool.pill.shellExec': 'Ejecutando comando',
|
|
202
|
+
'tasks.tool.pill.loadTools': 'Cargando herramientas',
|
|
203
|
+
'tasks.tool.pill.userMemory': 'Recordando',
|
|
204
|
+
'tasks.tool.pill.webScraper': 'Leyendo la página',
|
|
205
|
+
'tasks.tool.pill.webSearch': 'Búsqueda web',
|
|
206
|
+
'thought.activity': 'Actividad',
|
|
207
|
+
'thought.audit.agent': 'Agente: {{name}}',
|
|
208
|
+
'thought.audit.fileGenerated': 'Archivo generado',
|
|
209
|
+
'thought.audit.plan': 'Plan',
|
|
210
|
+
'thought.audit.planSteps': { one: '1 paso', other: '{{count}} pasos' },
|
|
211
|
+
'thought.audit.researchComplete': 'Investigación completada',
|
|
212
|
+
'thought.audit.researchIncomplete': 'Investigación incompleta',
|
|
213
|
+
'thought.audit.researchPhase.decomposing': 'Investigación: desglosando la pregunta',
|
|
214
|
+
'thought.audit.researchPhase.finalizing': 'Investigación: terminando',
|
|
215
|
+
'thought.audit.researchPhase.follow_up': 'Investigación: profundizando',
|
|
216
|
+
'thought.audit.researchPhase.reading': 'Investigación: leyendo artículos',
|
|
217
|
+
'thought.audit.researchPhase.searching': 'Investigación: buscando fuentes',
|
|
218
|
+
'thought.audit.researchPhase.synthesizing': 'Investigación: sintetizando hallazgos',
|
|
219
|
+
'thought.audit.researching': 'Investigando',
|
|
220
|
+
'thought.cancelled': 'Detenido',
|
|
221
|
+
'thought.done': 'Listo',
|
|
222
|
+
'thought.failed': 'Falló',
|
|
223
|
+
'thought.failedAfter': 'Falló tras {{elapsed}}',
|
|
224
|
+
'thought.filesAndSources': 'Archivos y fuentes',
|
|
225
|
+
'thought.input': 'Entrada',
|
|
226
|
+
'thought.loadFailed': 'No se pudo cargar esta conversación',
|
|
227
|
+
'thought.loading': 'Cargando esta conversación…',
|
|
228
|
+
'thought.messageGone': 'Este mensaje ya no está en la conversación',
|
|
229
|
+
'thought.noActivity': 'Aún no hay acciones registradas',
|
|
230
|
+
'thought.noOutputs': 'No se generaron archivos',
|
|
231
|
+
'thought.noSources': 'No se usaron fuentes',
|
|
232
|
+
'thought.noSteps': 'No hay pasos para mostrar',
|
|
233
|
+
'thought.openOutput': 'Abrir {{name}} en el lienzo',
|
|
234
|
+
'thought.output': 'Salida',
|
|
235
|
+
'thought.outputs': 'Resultados',
|
|
236
|
+
'thought.reasoning': 'Razonamiento',
|
|
237
|
+
'thought.references': 'Referencias',
|
|
238
|
+
'thought.sourceLabel': 'Fuente {{n}}: {{title}}',
|
|
239
|
+
'thought.sources': 'Fuentes',
|
|
240
|
+
'thought.steps': 'Pasos',
|
|
241
|
+
'thought.stoppedAfter': 'Detenido tras {{elapsed}}',
|
|
242
|
+
'thought.thinking': 'Pensando',
|
|
243
|
+
'thought.thinkingFor': 'Pensando por {{seconds}}s...',
|
|
244
|
+
'thought.thoughtFor': 'Pensó por {{seconds}} segundos',
|
|
245
|
+
'thought.title': 'Pensamiento',
|
|
246
|
+
'thought.tool.browse': 'Navegando por la web',
|
|
247
|
+
'thought.tool.generateFile': 'Generando archivo',
|
|
248
|
+
'thought.tool.getCurrentDate': 'Consultando la fecha actual',
|
|
249
|
+
'thought.tool.getTimeline': 'Obteniendo la cronología',
|
|
250
|
+
'thought.tool.getWhatsAppChats': 'Cargando chats de WhatsApp',
|
|
251
|
+
'thought.tool.getWhatsAppMessages': 'Leyendo mensajes de WhatsApp',
|
|
252
|
+
'thought.tool.saveUserMemory': 'Guardando en la memoria',
|
|
253
|
+
'thought.tool.scrapeURL': 'Leyendo URL',
|
|
254
|
+
'thought.tool.searchKnowledgeBase': 'Buscando en la base de conocimiento',
|
|
255
|
+
'thought.tool.sendTelegramMessage': 'Enviando mensaje de Telegram',
|
|
256
|
+
'thought.tool.sendWhatsAppMessage': 'Enviando mensaje de WhatsApp',
|
|
257
|
+
'thought.tool.updateUserContext': 'Actualizando contexto',
|
|
258
|
+
'thought.tool.updateUserPreferences': 'Actualizando preferencias',
|
|
259
|
+
'thought.tool.webScraper': 'Leyendo página web',
|
|
260
|
+
'thought.tool.webSearch': 'Buscando en la web',
|
|
261
|
+
'thought.viewDetails': 'Ver detalles',
|
|
262
|
+
'thought.websites': 'Sitios web',
|
|
263
|
+
'thought.worked': 'Trabajó',
|
|
264
|
+
'thought.workedFor': 'Trabajó durante {{elapsed}}',
|
|
265
|
+
'thought.working': 'Trabajando…',
|
|
266
|
+
'thought.workingFor': 'Trabajando desde hace {{elapsed}}…',
|
|
267
|
+
'thought.writing': 'Escribiendo',
|
|
268
|
+
'usageLimit.inputDisabledPlaceholder': 'Límite de uso alcanzado',
|
|
269
|
+
'weather.condition.clear': 'Despejado',
|
|
270
|
+
'weather.condition.cloudy': 'Nublado',
|
|
271
|
+
'weather.condition.drizzle': 'Llovizna',
|
|
272
|
+
'weather.condition.fog': 'Niebla',
|
|
273
|
+
'weather.condition.partly-cloudy': 'Parcialmente nublado',
|
|
274
|
+
'weather.condition.rain': 'Lluvia',
|
|
275
|
+
'weather.condition.showers': 'Chubascos',
|
|
276
|
+
'weather.condition.snow': 'Nieve',
|
|
277
|
+
'weather.condition.snow-showers': 'Chubascos de nieve',
|
|
278
|
+
'weather.condition.thunderstorm': 'Tormenta',
|
|
279
|
+
'weather.toggleUnit': 'Cambiar unidad de temperatura',
|
|
280
|
+
};
|
package/src/lib/copy.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { COPY_EN, type CopyEntry } from './copy-en';
|
|
2
|
+
import { COPY_ES } from './copy-es';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A translator keyed the way the Alia app's own catalogue keys its strings, so the app hands its
|
|
@@ -21,12 +22,17 @@ const PLACEHOLDER = /\{\{\s*(\w+)\s*\}\}|%\{(\w+)\}/g;
|
|
|
21
22
|
* The default translator: the English in `copy-en.ts`, with `{{count}}` plurals and parameters.
|
|
22
23
|
* A key it does not know answers itself, so a missing string is visible rather than blank.
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
25
|
+
function createCopy(catalogue: Readonly<Record<string, CopyEntry>>): Translate {
|
|
26
|
+
return (key, params) => {
|
|
27
|
+
const entry = catalogue[key];
|
|
28
|
+
if (entry === undefined) return key;
|
|
29
|
+
const count = typeof params?.count === 'number' ? params.count : undefined;
|
|
30
|
+
return entryFor(entry, count).replace(PLACEHOLDER, (placeholder, a?: string, b?: string) => {
|
|
31
|
+
const value = params?.[(a ?? b) as string];
|
|
32
|
+
return value === undefined ? placeholder : String(value);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const englishCopy = createCopy(COPY_EN);
|
|
38
|
+
export const spanishCopy = createCopy(COPY_ES);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The document picker on web, imported with the composer.
|
|
3
|
+
*
|
|
4
|
+
* On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
|
|
5
|
+
* and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
|
|
6
|
+
* an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
|
|
7
|
+
* lazy load (`./document-picker.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as DocumentPicker from '../document-picker';
|
|
11
|
+
|
|
12
|
+
export const loadDocumentPicker = async () => DocumentPicker;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `expo-audio` on web, imported with the chat.
|
|
3
|
+
*
|
|
4
|
+
* On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
|
|
5
|
+
* and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
|
|
6
|
+
* an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
|
|
7
|
+
* lazy load (`./expo-audio.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as ExpoAudio from 'expo-audio';
|
|
11
|
+
|
|
12
|
+
export const loadExpoAudio = async () => ExpoAudio;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attachment reader on web, imported with the runtime.
|
|
3
|
+
*
|
|
4
|
+
* On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
|
|
5
|
+
* and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
|
|
6
|
+
* an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
|
|
7
|
+
* lazy load (`./image-data-url.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as ImageDataUrl from '../image-data-url';
|
|
11
|
+
|
|
12
|
+
export const loadImageDataUrl = async () => ImageDataUrl;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { AliaChatLabels, VoiceBlockReason } from './labels';
|
|
2
|
+
import { spanishCopy } from './copy';
|
|
3
|
+
import type { VoiceCloseReason } from './voice-protocol';
|
|
4
|
+
import type { VoiceErrorCode } from './speech-messages';
|
|
5
|
+
|
|
6
|
+
const VOICE_ERRORS: Record<VoiceErrorCode, string> = {
|
|
7
|
+
'not-authenticated': 'Inicia sesión para usar la voz',
|
|
8
|
+
'credential-not-supported': 'La voz solo está disponible con tu sesión de Alia',
|
|
9
|
+
'plan-required': 'El modo voz necesita un plan de pago',
|
|
10
|
+
'credits-exhausted': 'Te has quedado sin créditos para esta llamada',
|
|
11
|
+
'agent-unavailable': 'Este agente no está disponible para una llamada',
|
|
12
|
+
'invalid-request': 'No se pudo iniciar la llamada',
|
|
13
|
+
'resume-unavailable': 'No se pudo retomar la llamada: empieza una nueva',
|
|
14
|
+
'realtime-unavailable': 'La voz no está disponible ahora mismo: inténtalo más tarde',
|
|
15
|
+
'realtime-limited': 'La voz ha llegado a su límite por ahora: inténtalo más tarde',
|
|
16
|
+
'realtime-rejected': 'No se pudo iniciar la llamada con esta configuración',
|
|
17
|
+
'realtime-failed': 'La llamada ha fallado: inténtalo de nuevo',
|
|
18
|
+
'mic-denied': 'Se necesita permiso para usar el micrófono',
|
|
19
|
+
'mic-unavailable': 'No se pudo usar el micrófono: revisa tus dispositivos de entrada',
|
|
20
|
+
'connection-lost': 'Se perdió la conexión',
|
|
21
|
+
'voice-unavailable': 'La voz no está disponible en este dispositivo o navegador',
|
|
22
|
+
'too-long': 'El dictado era demasiado largo: cinco minutos como máximo',
|
|
23
|
+
'transcription-unavailable': 'El dictado no está disponible ahora mismo',
|
|
24
|
+
'transcription-limited': 'El dictado ha llegado a su límite por ahora: inténtalo más tarde',
|
|
25
|
+
'transcription-rejected': 'No se pudo transcribir esa grabación',
|
|
26
|
+
'transcription-failed': 'Falló el dictado: inténtalo de nuevo',
|
|
27
|
+
};
|
|
28
|
+
const VOICE_ENDED: Partial<Record<VoiceCloseReason, string>> = {
|
|
29
|
+
'call-moved-elsewhere': 'La llamada sigue en tu otro dispositivo',
|
|
30
|
+
'max-duration': 'La llamada llegó a su límite de 30 minutos',
|
|
31
|
+
'idle-timeout': 'La llamada terminó tras dos minutos de silencio',
|
|
32
|
+
'credits-exhausted': 'La llamada terminó: te quedaste sin créditos',
|
|
33
|
+
'server-shutdown': 'La llamada terminó mientras Alia se actualizaba: empieza una nueva',
|
|
34
|
+
};
|
|
35
|
+
const VOICE_BLOCKED: Record<VoiceBlockReason, string> = {
|
|
36
|
+
'sign-in': 'Inicia sesión para usar la voz',
|
|
37
|
+
'plan-required': 'El modo voz necesita un plan de pago',
|
|
38
|
+
'credits-exhausted': 'Te has quedado sin créditos',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Built-in Spanish, shared by every app that embeds the chat. */
|
|
42
|
+
export const SPANISH_LABELS: AliaChatLabels = {
|
|
43
|
+
clear: 'Borrar',
|
|
44
|
+
back: 'Volver',
|
|
45
|
+
share: 'Compartir',
|
|
46
|
+
more: 'Más',
|
|
47
|
+
chatActions: 'Acciones de la conversación',
|
|
48
|
+
addMenuGroup: 'Adjuntar',
|
|
49
|
+
addPhotos: 'Fotos',
|
|
50
|
+
addDocuments: 'Archivos',
|
|
51
|
+
startVoice: 'Iniciar llamada',
|
|
52
|
+
voiceEndingSoon: 'Queda un minuto de llamada',
|
|
53
|
+
composerMessage: 'Mensaje',
|
|
54
|
+
composerAddMenu: 'Añadir al chat',
|
|
55
|
+
composerModes: 'Potencia',
|
|
56
|
+
composerVoice: 'Dictar',
|
|
57
|
+
composerSend: 'Enviar mensaje',
|
|
58
|
+
composerStop: 'Detener la generación',
|
|
59
|
+
composerRemove: 'Quitar',
|
|
60
|
+
composerRetry: 'Reintentar',
|
|
61
|
+
composerUntitled: 'sin título',
|
|
62
|
+
dropHint: 'Suelta los archivos aquí para adjuntarlos',
|
|
63
|
+
dropUnavailable: 'Ahora mismo no se pueden adjuntar archivos',
|
|
64
|
+
greeting: 'Hola, ¿qué tienes en mente?',
|
|
65
|
+
copy: 'Copiar',
|
|
66
|
+
helpful: 'Respuesta útil',
|
|
67
|
+
unhelpful: 'Respuesta poco útil',
|
|
68
|
+
copied: 'Copiado',
|
|
69
|
+
edit: 'Editar',
|
|
70
|
+
save: 'Guardar',
|
|
71
|
+
cancel: 'Cancelar',
|
|
72
|
+
readAloud: 'Leer en voz alta',
|
|
73
|
+
stopReading: 'Dejar de leer',
|
|
74
|
+
planTitle: 'Este es mi plan',
|
|
75
|
+
reasoning: 'Razonamiento',
|
|
76
|
+
locale: 'es',
|
|
77
|
+
newChat: 'Nueva conversación',
|
|
78
|
+
recentConversations: 'Recientes',
|
|
79
|
+
sendFailed:
|
|
80
|
+
'No se pudo enviar el mensaje. Lo tienes de vuelta en el cuadro de texto, inténtalo otra vez.',
|
|
81
|
+
signInRequired: 'Inicia sesión para seguir conversando',
|
|
82
|
+
conversationSaveFailed: 'No se pudo guardar la conversación',
|
|
83
|
+
conversationDeleteFailed: 'No se pudo eliminar la conversación',
|
|
84
|
+
conversationCreateFailed: 'No se pudo crear la conversación',
|
|
85
|
+
conversationClearFailed: 'No se pudo borrar la conversación',
|
|
86
|
+
placeholder: (name) => `Pregúntale lo que quieras a ${name}`,
|
|
87
|
+
greetingNamed: (name) => `Hola ${name}, ¿qué tienes en mente?`,
|
|
88
|
+
fileReadFailed: (name) => `No se pudo leer ${name}: selecciona para reintentar`,
|
|
89
|
+
fileTooLarge: (name, limit) => `${name} supera ${limit} y no se ha adjuntado`,
|
|
90
|
+
fileEmpty: (name) => `${name} está vacío: no se pueden adjuntar carpetas`,
|
|
91
|
+
fileUnsupported: (name) => `No se puede enviar ${name} aquí: solo se admiten fotos`,
|
|
92
|
+
dictationRemaining: (seconds) => `Quedan ${seconds} s de dictado`,
|
|
93
|
+
thoughtFor: (seconds) => `Pensó durante ${seconds} segundos`,
|
|
94
|
+
attachmentNotSent: (name) => `No se envió ${name}: solo se pueden enviar fotos`,
|
|
95
|
+
attachmentUnreadable: (name) => `No se pudo leer ${name} y no se envió`,
|
|
96
|
+
voiceError: (code) => VOICE_ERRORS[code],
|
|
97
|
+
voiceEnded: (reason) => VOICE_ENDED[reason] ?? null,
|
|
98
|
+
voiceBlocked: (reason) => VOICE_BLOCKED[reason],
|
|
99
|
+
voiceControls: {
|
|
100
|
+
connecting: 'Conectando…',
|
|
101
|
+
reconnecting: 'Reconectando…',
|
|
102
|
+
connected: 'Conectado',
|
|
103
|
+
listening: 'Escuchando…',
|
|
104
|
+
muted: 'Silenciado',
|
|
105
|
+
thinking: 'Pensando…',
|
|
106
|
+
usingTool: 'Trabajando en ello…',
|
|
107
|
+
speaking: 'Hablando…',
|
|
108
|
+
mute: 'Silenciar',
|
|
109
|
+
unmute: 'Activar micrófono',
|
|
110
|
+
end: 'Colgar',
|
|
111
|
+
},
|
|
112
|
+
t: spanishCopy,
|
|
113
|
+
};
|
package/src/lib/labels.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createContext, type ReactNode, useContext, useMemo } from 'react';
|
|
2
|
+
import { SPANISH_LABELS } from './labels-es';
|
|
2
3
|
import { VOICE_ERROR_MESSAGES, type VoiceErrorCode } from './speech-messages';
|
|
3
4
|
import { englishCopy, type Translate } from './copy';
|
|
4
5
|
import { VOICE_CONTROLS_LABELS, type VoiceControlsLabels } from './voice-controls-labels';
|
|
@@ -178,7 +179,7 @@ export const DEFAULT_LABELS: AliaChatLabels = {
|
|
|
178
179
|
attachmentUnreadable: (name) => `${name} couldn't be read and wasn't sent`,
|
|
179
180
|
};
|
|
180
181
|
|
|
181
|
-
const LabelsContext = createContext<AliaChatLabels>(
|
|
182
|
+
const LabelsContext = createContext<AliaChatLabels | null>(null);
|
|
182
183
|
|
|
183
184
|
/** The chat's labels: the defaults, with whatever the host overrides. */
|
|
184
185
|
export function AliaChatLabelsProvider({
|
|
@@ -191,10 +192,16 @@ export function AliaChatLabelsProvider({
|
|
|
191
192
|
// Over whatever labels are already in reach, so a part that takes its own `labels` (the
|
|
192
193
|
// composer) refines the chat's copy instead of resetting it to English.
|
|
193
194
|
const inherited = useContext(LabelsContext);
|
|
194
|
-
const value = useMemo(() =>
|
|
195
|
+
const value = useMemo(() => {
|
|
196
|
+
const locale = labels?.locale ?? inherited?.locale ?? 'en';
|
|
197
|
+
const language = locale.toLowerCase().split('-')[0];
|
|
198
|
+
const defaults = language === 'es' ? SPANISH_LABELS : DEFAULT_LABELS;
|
|
199
|
+
const parent = inherited?.locale.toLowerCase().split('-')[0] === language ? inherited : null;
|
|
200
|
+
return { ...defaults, ...parent, ...labels };
|
|
201
|
+
}, [inherited, labels]);
|
|
195
202
|
return <LabelsContext.Provider value={value}>{children}</LabelsContext.Provider>;
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
export function useLabels(): AliaChatLabels {
|
|
199
|
-
return useContext(LabelsContext);
|
|
206
|
+
return useContext(LabelsContext) ?? DEFAULT_LABELS;
|
|
200
207
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createAudioLevelMeter } from './audio-level';
|
|
12
|
+
import { loadExpoAudio } from './deferred/expo-audio';
|
|
12
13
|
|
|
13
14
|
export type ProductVoice = 'male' | 'female';
|
|
14
15
|
|
|
@@ -171,7 +172,7 @@ export interface PlayClipOptions {
|
|
|
171
172
|
export async function playSpeechClip(uri: string, options: PlayClipOptions): Promise<void> {
|
|
172
173
|
const { signal, onLevel } = options;
|
|
173
174
|
if (signal.aborted) return;
|
|
174
|
-
const { createAudioPlayer } = await
|
|
175
|
+
const { createAudioPlayer } = await loadExpoAudio();
|
|
175
176
|
if (signal.aborted) return;
|
|
176
177
|
|
|
177
178
|
const player = createAudioPlayer({ uri }, { crossOrigin: 'anonymous' });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadImageDataUrl } from '../lib/deferred/image-data-url';
|
|
1
2
|
import type { AliaAttachment } from '../types';
|
|
2
3
|
|
|
3
4
|
export type MessageContentPart =
|
|
@@ -60,9 +61,9 @@ export async function buildMessageContent(
|
|
|
60
61
|
|
|
61
62
|
const parts: MessageContentPart[] = [{ type: 'text', text }];
|
|
62
63
|
|
|
63
|
-
// The readers need the platform's file access, loaded on the first attachment: a turn
|
|
64
|
-
// text (and everything that imports this module) never touches it.
|
|
65
|
-
const { imageUriToDataUrl } = await
|
|
64
|
+
// The readers need the platform's file access, loaded on the first attachment: on native a turn
|
|
65
|
+
// of plain text (and everything that imports this module) never touches it.
|
|
66
|
+
const { imageUriToDataUrl } = await loadImageDataUrl();
|
|
66
67
|
for (const att of usable) {
|
|
67
68
|
if (att.type !== 'image' && upload !== undefined) {
|
|
68
69
|
try {
|
|
@@ -30,8 +30,11 @@ const FIVE_MINUTES = 1000 * 60 * 5;
|
|
|
30
30
|
|
|
31
31
|
/** A stable signal for a URL whose server-side conversation no longer exists. */
|
|
32
32
|
export class ConversationNotFoundError extends Error {
|
|
33
|
-
|
|
33
|
+
readonly conversationId: string;
|
|
34
|
+
|
|
35
|
+
constructor(conversationId: string) {
|
|
34
36
|
super('Conversation not found');
|
|
37
|
+
this.conversationId = conversationId;
|
|
35
38
|
this.name = 'ConversationNotFoundError';
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -54,7 +57,11 @@ function messageText(message: Message | undefined): string {
|
|
|
54
57
|
* it holds nothing, and the 401 is the caller's to handle.
|
|
55
58
|
*/
|
|
56
59
|
class OfflineConversations {
|
|
57
|
-
|
|
60
|
+
private readonly store: OfflineConversationStore | null;
|
|
61
|
+
|
|
62
|
+
constructor(store: OfflineConversationStore | null) {
|
|
63
|
+
this.store = store;
|
|
64
|
+
}
|
|
58
65
|
|
|
59
66
|
get available(): boolean {
|
|
60
67
|
return this.store !== null;
|