@alia.onl/sdk 15.7.0 → 19.0.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 +255 -12
- package/README.md +241 -155
- package/package.json +17 -5
- package/src/animations.ts +0 -1
- package/src/components/AliaAskProvider.tsx +127 -0
- package/src/components/AliaChatContent.tsx +604 -392
- package/src/components/AliaChatHeader.tsx +14 -38
- package/src/components/AliaChatScreen.tsx +56 -31
- package/src/components/AliaChatSheet.tsx +110 -36
- package/src/components/AliaComposer.tsx +1 -1
- package/src/components/AliaParticipants.tsx +202 -0
- package/src/components/AliaParticipantsHeader.tsx +60 -0
- package/src/components/AliaSharedConversation.tsx +86 -0
- package/src/components/AliaWelcomeMessage.tsx +18 -5
- package/src/components/Image.tsx +12 -0
- package/src/components/Markdown.tsx +498 -14
- package/src/components/PlanPreviewCard.tsx +7 -49
- package/src/components/ReplyMarkdown.tsx +359 -0
- package/src/components/RichBlocks.tsx +195 -0
- package/src/components/canvas/CanvasComponent.tsx +59 -0
- package/src/components/canvas/ChartRenderer.tsx +73 -0
- package/src/components/canvas/CodeRenderer.tsx +24 -0
- package/src/components/canvas/FormRenderer.tsx +101 -0
- package/src/components/canvas/MarkdownRenderer.tsx +14 -0
- package/src/components/canvas/TableRenderer.tsx +47 -0
- package/src/components/thought/FilesAndSources.tsx +120 -0
- package/src/components/thought/ThoughtPanel.tsx +417 -0
- package/src/components/thought/ToolStep.tsx +141 -0
- package/src/components/thread/AgentResultCard.tsx +125 -0
- package/src/components/thread/AgentTaskCard.tsx +127 -0
- package/src/components/thread/AliaThread.tsx +401 -0
- package/src/components/thread/MessageBlockBoundary.tsx +78 -0
- package/src/components/thread/NewConversationOffer.tsx +54 -0
- package/src/components/thread/ThreadRow.tsx +458 -0
- package/src/components/thread/ThreadSearch.tsx +129 -0
- package/src/components/thread/ToolResultCard.tsx +254 -0
- package/src/components/thread/TurnMenu.tsx +54 -0
- package/src/components/thread/TurnStatusLine.tsx +34 -0
- package/src/components/voice/AliaVoiceCall.tsx +21 -0
- package/src/components/voice/VoiceControls.tsx +2 -28
- package/src/components/voice/VoiceSession.tsx +39 -11
- package/src/csp.ts +50 -0
- package/src/hooks/useAliaActivity.ts +3 -3
- package/src/hooks/useAtBottom.ts +51 -0
- package/src/hooks/useAttachMenu.ts +94 -0
- package/src/hooks/useChatComposer.ts +299 -0
- package/src/hooks/useChatVoice.ts +129 -0
- package/src/hooks/useDictation.ts +2 -3
- package/src/hooks/useDocumentPicker.ts +3 -29
- package/src/hooks/useFailedTurnToast.ts +43 -0
- package/src/hooks/useMessageVote.ts +50 -0
- package/src/hooks/useOpenThought.ts +79 -0
- package/src/hooks/usePendingQuestions.ts +2 -2
- package/src/hooks/useReadAloud.ts +92 -0
- package/src/hooks/useShareHandler.ts +24 -0
- package/src/hooks/useSoundEffects.ts +1 -1
- package/src/hooks/useTTS.ts +2 -3
- package/src/hooks/useThoughtScope.ts +59 -0
- package/src/hooks/useThreadRows.ts +145 -0
- package/src/hooks/useThreadView.ts +68 -0
- package/src/hooks/useTimelineWindow.ts +256 -0
- package/src/hooks/useVoiceLauncher.ts +180 -0
- package/src/hooks/useVoiceRoom.ts +2 -6
- package/src/index.ts +43 -29
- package/src/lib/activity-store.ts +1 -1
- package/src/lib/add-menu.ts +7 -0
- package/src/lib/call-thread.ts +33 -0
- package/src/lib/character-runtime.ts +6 -4
- package/src/lib/citations.ts +79 -2
- package/src/lib/clipboard.ts +23 -0
- package/src/lib/content-utils.ts +10 -0
- package/src/lib/copy-en.ts +284 -0
- package/src/lib/copy.ts +32 -0
- package/src/lib/document-picker.ts +31 -0
- package/src/lib/flights.ts +22 -13
- package/src/lib/focus-return.ts +66 -0
- package/src/lib/keyboard.native.tsx +19 -0
- package/src/lib/keyboard.tsx +9 -0
- package/src/lib/labels.tsx +69 -17
- package/src/lib/origins.ts +15 -0
- package/src/lib/participants.ts +86 -0
- package/src/lib/power-level-options.ts +66 -0
- package/src/lib/questions.ts +2 -2
- package/src/lib/task-labels.ts +4 -4
- package/src/lib/tool-call-status.ts +1 -1
- package/src/lib/tool-icons.ts +36 -0
- package/src/lib/tool-renderers.tsx +48 -0
- package/src/lib/turn-state.ts +2 -2
- package/src/lib/voice-audio-engine.ts +14 -158
- package/src/lib/voice-call-request.ts +49 -0
- package/src/lib/voice-call.ts +76 -34
- package/src/lib/voice-controls-labels.ts +31 -0
- package/src/lib/voice-protocol.ts +32 -14
- package/src/lib/voice-worklets.ts +19 -0
- package/src/lib/work-log.ts +6 -6
- package/src/runtime/agents.ts +151 -0
- package/src/runtime/api.ts +123 -0
- package/src/runtime/attachment-utils.ts +96 -0
- package/src/runtime/composer-draft-store.ts +270 -0
- package/src/runtime/config.tsx +186 -0
- package/src/runtime/conversation-store.ts +49 -0
- package/src/runtime/conversations.ts +557 -0
- package/src/runtime/error-utils.ts +104 -0
- package/src/runtime/host-tools.ts +140 -0
- package/src/runtime/index.ts +113 -0
- package/src/runtime/message-history.ts +124 -0
- package/src/runtime/model.ts +39 -0
- package/src/runtime/oxy-files.ts +37 -0
- package/src/runtime/power-levels.ts +66 -0
- package/src/runtime/query-keys.ts +34 -0
- package/src/runtime/random-uuid.ts +25 -0
- package/src/runtime/routes.ts +51 -0
- package/src/runtime/sharing.ts +165 -0
- package/src/runtime/sse-frame-reader.ts +88 -0
- package/src/runtime/stream-events.ts +146 -0
- package/src/runtime/stream-outcome.ts +21 -0
- package/src/runtime/thought-store.ts +103 -0
- package/src/runtime/thread/agent-activity.ts +261 -0
- package/src/runtime/thread/automation-labels.ts +79 -0
- package/src/runtime/thread/conversation-export.ts +135 -0
- package/src/runtime/thread/message-days.ts +120 -0
- package/src/runtime/thread/message-processor.ts +93 -0
- package/src/runtime/thread/task-utils.ts +11 -0
- package/src/runtime/thread/thought-utils.ts +752 -0
- package/src/runtime/thread/thread-history.ts +139 -0
- package/src/runtime/thread/timeline.ts +202 -0
- package/src/runtime/thread/title-tags.ts +8 -0
- package/src/runtime/thread/tool-cards.ts +383 -0
- package/src/runtime/turn-failure.ts +108 -0
- package/src/runtime/types.ts +148 -0
- package/src/runtime/usage-limit-error.ts +44 -0
- package/src/runtime/use-agent-activity.ts +80 -0
- package/src/runtime/use-conversation-stream.ts +1427 -0
- package/src/runtime/use-conversation-thread.ts +556 -0
- package/src/runtime/use-start-conversation.ts +107 -0
- package/src/runtime/use-thread-history.ts +64 -0
- package/src/runtime/use-thread-search.ts +95 -0
- package/src/runtime/use-turn-edit.ts +99 -0
- package/src/tools.ts +2 -2
- package/src/types.ts +18 -47
- package/worklets/voice-processors.js +152 -0
- package/src/components/AliaChatMessageList.tsx +0 -532
- package/src/components/ThinkingBlock.tsx +0 -43
- package/src/hooks/useAliaChat.ts +0 -573
- package/src/lib/api-url.ts +0 -2
- package/src/lib/catalogue.ts +0 -236
- package/src/lib/chat-stream.ts +0 -655
- package/src/lib/chat-transport.ts +0 -98
- package/src/lib/delegation.ts +0 -34
- package/src/lib/work-log-copy.ts +0 -61
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useInfiniteQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useAliaApi } from './api';
|
|
4
|
+
import { errorStatus } from './error-utils';
|
|
5
|
+
import { aliaQueryKeys } from './query-keys';
|
|
6
|
+
import { AGENT_ROUTES } from './routes';
|
|
7
|
+
import { type ThreadPage, threadHistory } from './thread/thread-history';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Everything said in this thread before the conversation on screen, a page at a time, going back.
|
|
11
|
+
*
|
|
12
|
+
* The screen streams into the most recent conversation of the thread and knows nothing older;
|
|
13
|
+
* the thread's messages route is what crosses the seams into the stretches before it. Pages are
|
|
14
|
+
* pulled as the reader approaches the top, never eagerly — a thread has no end going backwards,
|
|
15
|
+
* and the first page is already everything a reader who does not scroll will see.
|
|
16
|
+
*
|
|
17
|
+
* `nextCursor` is what says whether anything older remains, and it is the server's answer rather
|
|
18
|
+
* than a count: a page that comes back empty after its live messages are dropped is ordinary, and
|
|
19
|
+
* treating that as the end would stop the history dead on any thread whose current conversation
|
|
20
|
+
* is long.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** How many messages a page carries. The server clamps its own maximum. */
|
|
24
|
+
const PAGE_SIZE = 50;
|
|
25
|
+
|
|
26
|
+
/** A 404 is the answer — the agent does not exist or is not the person's, and asking again does not change it. */
|
|
27
|
+
export function retryThreadRead(failureCount: number, error: unknown): boolean {
|
|
28
|
+
return errorStatus(error) !== 404 && failureCount < 2;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useThreadHistory(
|
|
32
|
+
handle: string | undefined,
|
|
33
|
+
activeConversationId: string | undefined,
|
|
34
|
+
) {
|
|
35
|
+
const api = useAliaApi();
|
|
36
|
+
const query = useInfiniteQuery({
|
|
37
|
+
queryKey: aliaQueryKeys.threads.messages(handle ?? ''),
|
|
38
|
+
queryFn: async ({ pageParam }): Promise<ThreadPage> => {
|
|
39
|
+
const { data } = await api.get<ThreadPage>(AGENT_ROUTES.threadMessages(handle ?? ''), {
|
|
40
|
+
params: { limit: PAGE_SIZE, before: pageParam },
|
|
41
|
+
});
|
|
42
|
+
return data;
|
|
43
|
+
},
|
|
44
|
+
initialPageParam: undefined as string | undefined,
|
|
45
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
46
|
+
// Nothing to page through until the thread has answered which conversation is the live one:
|
|
47
|
+
// without it every message would be history, including the ones already on screen.
|
|
48
|
+
enabled: (handle?.length ?? 0) > 0 && activeConversationId !== undefined,
|
|
49
|
+
retry: retryThreadRead,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const messages = useMemo(
|
|
53
|
+
() => threadHistory(query.data?.pages ?? [], activeConversationId ?? ''),
|
|
54
|
+
[query.data?.pages, activeConversationId],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
messages,
|
|
59
|
+
/** Whether a page older than the ones held remains to be asked for. */
|
|
60
|
+
hasMore: query.hasNextPage,
|
|
61
|
+
isLoadingMore: query.isFetchingNextPage,
|
|
62
|
+
loadMore: query.fetchNextPage,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useAliaApi } from './api';
|
|
4
|
+
import { aliaQueryKeys } from './query-keys';
|
|
5
|
+
import { AGENT_ROUTES } from './routes';
|
|
6
|
+
import {
|
|
7
|
+
type ThreadMessage,
|
|
8
|
+
type ThreadPage,
|
|
9
|
+
type ThreadSearchHit,
|
|
10
|
+
threadHistory,
|
|
11
|
+
} from './thread/thread-history';
|
|
12
|
+
import { retryThreadRead } from './use-thread-history';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Finding something said earlier in a thread, and getting back to it.
|
|
16
|
+
*
|
|
17
|
+
* Two halves, and the second is why the first is worth anything: a hit tells you the message
|
|
18
|
+
* exists, and its CURSOR is what opens the stretch of thread around it — however many
|
|
19
|
+
* conversations back it lives. Without that, a result list is a wall you can read and not reach.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** How many hits to ask for. The server clamps its own maximum. */
|
|
23
|
+
const HIT_LIMIT = 30;
|
|
24
|
+
|
|
25
|
+
export function useThreadSearch(handle: string | undefined, query: string) {
|
|
26
|
+
const api = useAliaApi();
|
|
27
|
+
const trimmed = query.trim();
|
|
28
|
+
|
|
29
|
+
return useQuery({
|
|
30
|
+
queryKey: aliaQueryKeys.threads.search(handle ?? '', trimmed),
|
|
31
|
+
queryFn: async (): Promise<ThreadSearchHit[]> => {
|
|
32
|
+
const { data } = await api.get<{ hits: ThreadSearchHit[] }>(
|
|
33
|
+
AGENT_ROUTES.threadSearch(handle ?? ''),
|
|
34
|
+
{ params: { q: trimmed, limit: HIT_LIMIT } },
|
|
35
|
+
);
|
|
36
|
+
return data.hits;
|
|
37
|
+
},
|
|
38
|
+
// An empty query is not a search, and asking for one would return the thread's most recent
|
|
39
|
+
// everything under the guise of a result.
|
|
40
|
+
enabled: (handle?.length ?? 0) > 0 && trimmed.length > 0,
|
|
41
|
+
// What was said cannot change, so a query already asked is answered from the cache.
|
|
42
|
+
staleTime: 1000 * 60 * 5,
|
|
43
|
+
retry: retryThreadRead,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** How many messages a window carries, half of them before the hit. */
|
|
48
|
+
const WINDOW_SIZE = 40;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The stretch of thread around one message, opened from a search hit.
|
|
52
|
+
*
|
|
53
|
+
* `at=` rather than `before=`: `before` is exclusive, so the one message the window exists to
|
|
54
|
+
* reveal would be the one missing from it.
|
|
55
|
+
*
|
|
56
|
+
* Reading further back from here is the same paging as anywhere else — the window's own oldest
|
|
57
|
+
* cursor continues it — so this is an infinite query too, and only its first page is addressed
|
|
58
|
+
* differently. There is no paging FORWARD: the way back to the present is leaving the window, not
|
|
59
|
+
* walking to it.
|
|
60
|
+
*/
|
|
61
|
+
export function useThreadWindow(handle: string | undefined, cursor: string | null) {
|
|
62
|
+
const api = useAliaApi();
|
|
63
|
+
const query = useInfiniteQuery({
|
|
64
|
+
queryKey: aliaQueryKeys.threads.window(handle ?? '', cursor ?? ''),
|
|
65
|
+
queryFn: async ({ pageParam }): Promise<ThreadPage> => {
|
|
66
|
+
const { data } = await api.get<ThreadPage>(AGENT_ROUTES.threadMessages(handle ?? ''), {
|
|
67
|
+
params:
|
|
68
|
+
pageParam === undefined
|
|
69
|
+
? { limit: WINDOW_SIZE, at: cursor ?? undefined }
|
|
70
|
+
: { limit: WINDOW_SIZE, before: pageParam },
|
|
71
|
+
});
|
|
72
|
+
return data;
|
|
73
|
+
},
|
|
74
|
+
initialPageParam: undefined as string | undefined,
|
|
75
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
76
|
+
enabled: (handle?.length ?? 0) > 0 && cursor !== null,
|
|
77
|
+
retry: retryThreadRead,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Nothing is dropped here, unlike the tail: a window is a view of the PAST, and the live
|
|
81
|
+
// conversation is not on screen underneath it to be duplicated. The empty string is a
|
|
82
|
+
// conversation id no message has.
|
|
83
|
+
const messages: ThreadMessage[] = useMemo(
|
|
84
|
+
() => threadHistory(query.data?.pages ?? [], ''),
|
|
85
|
+
[query.data?.pages],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
messages,
|
|
90
|
+
hasMore: query.hasNextPage,
|
|
91
|
+
isLoadingMore: query.isFetchingNextPage,
|
|
92
|
+
loadMore: query.fetchNextPage,
|
|
93
|
+
isLoading: query.isPending && cursor !== null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { getTextFromContent } from '../lib/content-utils';
|
|
3
|
+
import type { Message, SendOptions } from './types';
|
|
4
|
+
import type { AliaAttachment } from '../types';
|
|
5
|
+
import { useComposerDraft, useComposerDraftStore, type DraftTarget } from './composer-draft-store';
|
|
6
|
+
|
|
7
|
+
export interface TurnEditOptions {
|
|
8
|
+
/** The composer the edit happens in: this chat's draft. */
|
|
9
|
+
target: DraftTarget;
|
|
10
|
+
messages: readonly Message[];
|
|
11
|
+
/** The options a question was sent with, when this screen sent it. */
|
|
12
|
+
turnOptionsOf?: (userMessageId: string) => SendOptions | undefined;
|
|
13
|
+
/** Send the rewrite in place of the question (`useChatConversation.editMessage`). */
|
|
14
|
+
onEditMessage?: (
|
|
15
|
+
messageId: string,
|
|
16
|
+
text: string,
|
|
17
|
+
options?: SendOptions,
|
|
18
|
+
attachments?: AliaAttachment[],
|
|
19
|
+
) => Promise<boolean>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Rewriting a question already sent, in the composer (#608 §7).
|
|
24
|
+
*
|
|
25
|
+
* Before the template this was an "Editing message" strip over the input, with
|
|
26
|
+
* the id in the screen's own `useState` — so leaving the chat and coming back
|
|
27
|
+
* forgot the edit and the next send became a new turn. The edit lives in the
|
|
28
|
+
* chat's draft now (`composer-draft-store.ts`), like the text it is made of:
|
|
29
|
+
* per account and per conversation, and gone when the draft is.
|
|
30
|
+
*
|
|
31
|
+
* - `start` loads the question's words and the connector and skills it was sent
|
|
32
|
+
* with into the composer, keeping aside what the composer held. A question
|
|
33
|
+
* this screen did not send has no record of its options, so the composer's
|
|
34
|
+
* own choice stays.
|
|
35
|
+
* - `cancel` gives the composer back what it held.
|
|
36
|
+
* - `submit` sends the rewrite; a failure leaves the composer still editing,
|
|
37
|
+
* with the words back in it (the send path restores those).
|
|
38
|
+
*
|
|
39
|
+
* `editing` is the edit only while its question is still in the thread: a
|
|
40
|
+
* conversation cleared, or cut back by another edit, takes it away, and the
|
|
41
|
+
* composer goes back to writing a new turn.
|
|
42
|
+
*/
|
|
43
|
+
export function useTurnEdit({ target, messages, turnOptionsOf, onEditMessage }: TurnEditOptions) {
|
|
44
|
+
const draftEdit = useComposerDraft(target).editing;
|
|
45
|
+
const editing =
|
|
46
|
+
draftEdit !== undefined && messages.some((m) => m.id === draftEdit.messageId)
|
|
47
|
+
? draftEdit
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
// Read through a ref: `start` is a prop of every row in the thread, and an
|
|
51
|
+
// identity that changed per streamed token would re-render all of them.
|
|
52
|
+
const latestMessages = useRef(messages);
|
|
53
|
+
latestMessages.current = messages;
|
|
54
|
+
|
|
55
|
+
const start = useCallback(
|
|
56
|
+
(messageId: string) => {
|
|
57
|
+
const message = latestMessages.current.find((m) => m.id === messageId);
|
|
58
|
+
if (message === undefined) return;
|
|
59
|
+
const store = useComposerDraftStore.getState();
|
|
60
|
+
const current = store.drafts[target ?? ''];
|
|
61
|
+
const sentWith = turnOptionsOf?.(messageId);
|
|
62
|
+
store.startEdit(store.address(target), {
|
|
63
|
+
messageId,
|
|
64
|
+
text: getTextFromContent(message.content),
|
|
65
|
+
mcpServerId: sentWith?.mcpServerId ?? current?.mcpServerId ?? null,
|
|
66
|
+
skillNames: sentWith?.skillNames ?? current?.skillNames ?? [],
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
[target, turnOptionsOf],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const cancel = useCallback(() => {
|
|
73
|
+
const store = useComposerDraftStore.getState();
|
|
74
|
+
store.cancelEdit(store.address(target));
|
|
75
|
+
}, [target]);
|
|
76
|
+
|
|
77
|
+
/** Send `text` in place of the question being edited. The caller has cleared the draft. */
|
|
78
|
+
const submit = useCallback(
|
|
79
|
+
async (
|
|
80
|
+
text: string,
|
|
81
|
+
options?: SendOptions,
|
|
82
|
+
attachments?: AliaAttachment[],
|
|
83
|
+
): Promise<boolean> => {
|
|
84
|
+
if (editing === undefined || onEditMessage === undefined) return false;
|
|
85
|
+
const address = useComposerDraftStore.getState().address(target);
|
|
86
|
+
const sent = await onEditMessage(editing.messageId, text, options, attachments);
|
|
87
|
+
if (!sent) useComposerDraftStore.getState().restore(address, { editing });
|
|
88
|
+
return sent;
|
|
89
|
+
},
|
|
90
|
+
[editing, onEditMessage, target],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
editing: onEditMessage === undefined ? undefined : editing,
|
|
95
|
+
start: onEditMessage === undefined ? undefined : start,
|
|
96
|
+
cancel,
|
|
97
|
+
submit,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -22,5 +22,5 @@ export type { AskedOption, AskedQuestion, QuestionAnswer } from './lib/questions
|
|
|
22
22
|
export { classifyIntake, MAX_ATTACHMENT_BYTES } from './lib/attachment-intake';
|
|
23
23
|
export { toolCallStatus } from './lib/tool-call-status';
|
|
24
24
|
export type { ToolCallStatus } from './lib/tool-call-status';
|
|
25
|
-
export {
|
|
26
|
-
export type { Translate } from './lib/
|
|
25
|
+
export { englishCopy } from './lib/copy';
|
|
26
|
+
export type { Translate } from './lib/copy';
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ComponentType } from 'react';
|
|
2
1
|
import type { SharedValue } from 'react-native-reanimated';
|
|
3
2
|
import type { VoiceErrorCode } from './lib/speech-messages';
|
|
4
3
|
import type { VoiceCloseReason } from './lib/voice-protocol';
|
|
@@ -9,6 +8,8 @@ import type { VoiceCloseReason } from './lib/voice-protocol';
|
|
|
9
8
|
export interface AliaSuggestion {
|
|
10
9
|
id: string;
|
|
11
10
|
text: string;
|
|
11
|
+
/** It has blanks to fill: picking it puts it in the composer for the person to finish, instead of sending it. */
|
|
12
|
+
template?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
// ── Composer Types ──
|
|
@@ -51,43 +52,10 @@ export interface PlanStep {
|
|
|
51
52
|
toolName?: string;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
/** A plan the agent shows before it works (`alia.plan_preview`). Shown only: nothing waits on it. */
|
|
56
|
+
export interface MessagePlan {
|
|
55
57
|
planId: string;
|
|
56
58
|
steps: PlanStep[];
|
|
57
|
-
approved: boolean;
|
|
58
|
-
rejected: boolean;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface ChatMessage {
|
|
62
|
-
id: string;
|
|
63
|
-
role: 'user' | 'assistant' | 'system';
|
|
64
|
-
content: string | Array<{ type: string; [key: string]: unknown }>;
|
|
65
|
-
toolInvocations?: ToolInvocation[];
|
|
66
|
-
createdAt: number;
|
|
67
|
-
/** Extended thinking / reasoning content */
|
|
68
|
-
thinking?: string;
|
|
69
|
-
/** Deep research progress metadata */
|
|
70
|
-
researchProgress?: ResearchProgress;
|
|
71
|
-
/** Plan preview data (pending approval) */
|
|
72
|
-
pendingPlan?: PendingPlan;
|
|
73
|
-
/** Message origin: text chat or voice */
|
|
74
|
-
source?: 'text' | 'voice';
|
|
75
|
-
/** Speaker identity in voice mode */
|
|
76
|
-
speaker?: 'primary';
|
|
77
|
-
/** Whether a voice transcript is still streaming */
|
|
78
|
-
isStreaming?: boolean;
|
|
79
|
-
/** Delegated agent identity */
|
|
80
|
-
/** `character` is the agent's look as the server stores it (JSON), parsed where it is drawn. */
|
|
81
|
-
agentInfo?: { id: string; name: string; character?: unknown };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface ToolInvocation {
|
|
85
|
-
toolName: string;
|
|
86
|
-
state: 'call' | 'partial-call' | 'result';
|
|
87
|
-
args?: Record<string, unknown>;
|
|
88
|
-
result?: unknown;
|
|
89
|
-
/** OpenAI-format tool call ID for matching calls to results */
|
|
90
|
-
toolCallId?: string;
|
|
91
59
|
}
|
|
92
60
|
|
|
93
61
|
// ── Voice Types ──
|
|
@@ -173,18 +141,21 @@ export interface VoiceSessionProps {
|
|
|
173
141
|
locale?: string;
|
|
174
142
|
/** Which of the product's two voices answers. */
|
|
175
143
|
voice?: 'male' | 'female';
|
|
176
|
-
/**
|
|
177
|
-
|
|
144
|
+
/** Alia stored messages of the call in the conversation: refresh the thread. */
|
|
145
|
+
onMessagesSaved?: (conversationId: string, messageIds: readonly string[]) => void;
|
|
178
146
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
147
|
+
* What the call tells the person, in the chat's language (`labels`): a failure, a call moved
|
|
148
|
+
* to another device, the time limit near. Omitted, Bloom's toast.
|
|
181
149
|
*/
|
|
182
|
-
|
|
150
|
+
onNotice?: (notice: VoiceNotice) => void;
|
|
151
|
+
/** Called on every room, agent, or transcript change. */
|
|
152
|
+
onStateChange: (state: VoiceSessionState) => void;
|
|
153
|
+
/** The call is over: hung up, failed, ended by the server or dropped. The person was told why. */
|
|
154
|
+
onEnd: () => void;
|
|
183
155
|
}
|
|
184
156
|
|
|
185
|
-
/**
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
export type VoiceSessionComponent = ComponentType<VoiceSessionProps>;
|
|
157
|
+
/** Something the call tells the person: a failure, how it ended, or that it is about to end. */
|
|
158
|
+
export interface VoiceNotice {
|
|
159
|
+
kind: 'error' | 'info' | 'warning';
|
|
160
|
+
text: string;
|
|
161
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The web voice call's two AudioWorklet processors (`src/lib/voice-audio-engine.ts`).
|
|
2
|
+
//
|
|
3
|
+
// Served from Alia's web origin, content-addressed (`/sdk/voice-worklets/<revision>.js`,
|
|
4
|
+
// `src/lib/voice-worklets.ts`), so a host page loads it under `script-src https://alia.onl`
|
|
5
|
+
// instead of a `blob:` URL its CSP would have to allow. Plain JavaScript with no imports: a
|
|
6
|
+
// worklet module runs in the AudioWorkletGlobalScope. Every number it needs arrives in
|
|
7
|
+
// `processorOptions`, so the bytes stay the same while the engine's settings change.
|
|
8
|
+
|
|
9
|
+
class AliaVoiceCapture extends AudioWorkletProcessor {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super();
|
|
12
|
+
const { streamSampleRate, frameSamples } = options.processorOptions;
|
|
13
|
+
this.ratio = sampleRate / streamSampleRate;
|
|
14
|
+
this.frameSamples = frameSamples;
|
|
15
|
+
this.t = 0;
|
|
16
|
+
this.prev = 0;
|
|
17
|
+
this.frame = new Int16Array(frameSamples);
|
|
18
|
+
this.filled = 0;
|
|
19
|
+
}
|
|
20
|
+
process(inputs) {
|
|
21
|
+
const input = inputs[0]?.[0];
|
|
22
|
+
if (!input || input.length === 0) return true;
|
|
23
|
+
const n = input.length;
|
|
24
|
+
while (this.t < n - 1) {
|
|
25
|
+
const i = Math.floor(this.t);
|
|
26
|
+
const f = this.t - i;
|
|
27
|
+
const a = i < 0 ? this.prev : input[i];
|
|
28
|
+
const b = input[i + 1];
|
|
29
|
+
let s = a + (b - a) * f;
|
|
30
|
+
s = s < -1 ? -1 : s > 1 ? 1 : s;
|
|
31
|
+
this.frame[this.filled++] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
32
|
+
if (this.filled === this.frame.length) {
|
|
33
|
+
const out = this.frame;
|
|
34
|
+
this.port.postMessage(out.buffer, [out.buffer]);
|
|
35
|
+
this.frame = new Int16Array(this.frameSamples);
|
|
36
|
+
this.filled = 0;
|
|
37
|
+
}
|
|
38
|
+
this.t += this.ratio;
|
|
39
|
+
}
|
|
40
|
+
this.t -= n;
|
|
41
|
+
this.prev = input[n - 1];
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
registerProcessor('alia-voice-capture', AliaVoiceCapture);
|
|
46
|
+
|
|
47
|
+
class AliaVoicePlayback extends AudioWorkletProcessor {
|
|
48
|
+
constructor(options) {
|
|
49
|
+
super();
|
|
50
|
+
const { streamSampleRate, prebufferMs, prebufferIdleMs } = options.processorOptions;
|
|
51
|
+
this.streamSampleRate = streamSampleRate;
|
|
52
|
+
this.prebufferSamples = (streamSampleRate * prebufferMs) / 1000;
|
|
53
|
+
this.prebufferIdleSeconds = prebufferIdleMs / 1000;
|
|
54
|
+
this.step = streamSampleRate / sampleRate;
|
|
55
|
+
this.queue = [];
|
|
56
|
+
this.queued = 0;
|
|
57
|
+
this.pos = 0;
|
|
58
|
+
this.buffering = true;
|
|
59
|
+
this.lastChunkAt = 0;
|
|
60
|
+
this.item = null;
|
|
61
|
+
this.played = 0;
|
|
62
|
+
this.sinceReport = 0;
|
|
63
|
+
this.sumSquares = 0;
|
|
64
|
+
this.count = 0;
|
|
65
|
+
this.port.onmessage = (event) => {
|
|
66
|
+
const message = event.data;
|
|
67
|
+
if (message.type === 'chunk') {
|
|
68
|
+
this.queue.push({ itemId: message.itemId, samples: message.samples });
|
|
69
|
+
this.queued += message.samples.length;
|
|
70
|
+
this.lastChunkAt = currentTime;
|
|
71
|
+
} else if (message.type === 'flush') {
|
|
72
|
+
const playedMs = this.item === null ? null : this.playedMs();
|
|
73
|
+
this.port.postMessage({ type: 'flushed', itemId: this.item, playedMs });
|
|
74
|
+
this.queue = [];
|
|
75
|
+
this.queued = 0;
|
|
76
|
+
this.pos = 0;
|
|
77
|
+
this.buffering = true;
|
|
78
|
+
this.item = null;
|
|
79
|
+
this.played = 0;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
playedMs() {
|
|
84
|
+
return (this.played / this.streamSampleRate) * 1000;
|
|
85
|
+
}
|
|
86
|
+
report(force) {
|
|
87
|
+
if (!force && this.sinceReport < sampleRate / 20) return;
|
|
88
|
+
const rms = this.count === 0 ? 0 : Math.sqrt(this.sumSquares / this.count);
|
|
89
|
+
this.port.postMessage({ type: 'progress', itemId: this.item, playedMs: this.playedMs(), rms });
|
|
90
|
+
this.sinceReport = 0;
|
|
91
|
+
this.sumSquares = 0;
|
|
92
|
+
this.count = 0;
|
|
93
|
+
}
|
|
94
|
+
process(_inputs, outputs) {
|
|
95
|
+
const out = outputs[0][0];
|
|
96
|
+
if (this.buffering) {
|
|
97
|
+
const ready =
|
|
98
|
+
this.queued >= this.prebufferSamples ||
|
|
99
|
+
(this.queued > 0 && currentTime - this.lastChunkAt > this.prebufferIdleSeconds);
|
|
100
|
+
if (!ready) {
|
|
101
|
+
out.fill(0);
|
|
102
|
+
for (let c = 1; c < outputs[0].length; c++) outputs[0][c].fill(0);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
this.buffering = false;
|
|
106
|
+
}
|
|
107
|
+
for (let k = 0; k < out.length; k++) {
|
|
108
|
+
const head = this.queue[0];
|
|
109
|
+
if (!head) {
|
|
110
|
+
out[k] = 0;
|
|
111
|
+
if (!this.buffering) {
|
|
112
|
+
this.buffering = true;
|
|
113
|
+
this.report(true);
|
|
114
|
+
this.port.postMessage({ type: 'idle' });
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (head.itemId !== this.item) {
|
|
119
|
+
this.item = head.itemId;
|
|
120
|
+
this.played = 0;
|
|
121
|
+
this.port.postMessage({ type: 'started', itemId: head.itemId });
|
|
122
|
+
}
|
|
123
|
+
const i = Math.floor(this.pos);
|
|
124
|
+
const f = this.pos - i;
|
|
125
|
+
const a = head.samples[i];
|
|
126
|
+
const b = i + 1 < head.samples.length ? head.samples[i + 1] : a;
|
|
127
|
+
const s = a + (b - a) * f;
|
|
128
|
+
out[k] = s;
|
|
129
|
+
this.sumSquares += s * s;
|
|
130
|
+
this.count++;
|
|
131
|
+
this.pos += this.step;
|
|
132
|
+
const consumed = Math.floor(this.pos);
|
|
133
|
+
if (consumed > 0) {
|
|
134
|
+
const taken = Math.min(consumed, head.samples.length);
|
|
135
|
+
this.played += taken;
|
|
136
|
+
this.queued -= taken;
|
|
137
|
+
if (consumed >= head.samples.length) {
|
|
138
|
+
this.queue.shift();
|
|
139
|
+
this.pos = 0;
|
|
140
|
+
} else {
|
|
141
|
+
head.samples = head.samples.subarray(consumed);
|
|
142
|
+
this.pos -= consumed;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (let c = 1; c < outputs[0].length; c++) outputs[0][c].set(out);
|
|
147
|
+
this.sinceReport += out.length;
|
|
148
|
+
this.report(false);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
registerProcessor('alia-voice-playback', AliaVoicePlayback);
|