@alia.onl/sdk 15.8.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 +242 -13
- package/README.md +223 -151
- package/package.json +15 -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/hooks/useAliaActivity.ts +2 -2
- 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/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/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 +0 -3
- 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/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/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-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/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/src/components/AliaChatMessageList.tsx +0 -532
- package/src/components/ThinkingBlock.tsx +0 -43
- package/src/hooks/useAliaChat.ts +0 -573
- 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,359 @@
|
|
|
1
|
+
import { CodeBlock } from '@oxy.so/bloom/code';
|
|
2
|
+
import { Text } from '@oxy.so/bloom/typography';
|
|
3
|
+
import * as Clipboard from 'expo-clipboard';
|
|
4
|
+
import * as WebBrowser from 'expo-web-browser';
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
import { Platform, Pressable, View } from 'react-native';
|
|
7
|
+
import {
|
|
8
|
+
type CitationSource,
|
|
9
|
+
extractCitationSources,
|
|
10
|
+
linkifyCitations,
|
|
11
|
+
type ReferenceEntry,
|
|
12
|
+
splitReferences,
|
|
13
|
+
} from '../lib/citations';
|
|
14
|
+
import { useLabels } from '../lib/labels';
|
|
15
|
+
import type { ToolInvocation } from '../runtime/types';
|
|
16
|
+
import { AliaMarkdown, type RenderCodeBlock } from './Markdown';
|
|
17
|
+
import { Banner, CompactList, Comparison, Credibility, RichImage, Timeline } from './RichBlocks';
|
|
18
|
+
|
|
19
|
+
// Cheap pre-check: plain markdown (the overwhelmingly common case) skips the
|
|
20
|
+
// six per-pattern regex scans below with a single pass.
|
|
21
|
+
const SPECIAL_BLOCK_HINT_RE =
|
|
22
|
+
/\[(?:ALIA_)?(?:COMPACTLIST|BANNER|COMPARISON|TIMELINE|IMAGE|CREDIBILITY)/;
|
|
23
|
+
|
|
24
|
+
// Parse special blocks from content
|
|
25
|
+
function parseSpecialBlocks(
|
|
26
|
+
content: string,
|
|
27
|
+
): Array<{ type: 'text' | 'block'; content: string; blockType?: string; data?: any }> {
|
|
28
|
+
if (!SPECIAL_BLOCK_HINT_RE.test(content)) {
|
|
29
|
+
return [{ type: 'text', content }];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const blocks: Array<{ type: 'text' | 'block'; content: string; blockType?: string; data?: any }> =
|
|
33
|
+
[];
|
|
34
|
+
|
|
35
|
+
const patterns = [
|
|
36
|
+
{
|
|
37
|
+
name: 'COMPACTLIST',
|
|
38
|
+
regex: /\[(?:ALIA_)?COMPACTLIST title="([^"]+)"\]([\s\S]*?)\[\/(?:ALIA_)?COMPACTLIST\]/g,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'BANNER',
|
|
42
|
+
regex: /\[(?:ALIA_)?BANNER type="([^"]+)" title="([^"]+)"\]([\s\S]*?)\[\/(?:ALIA_)?BANNER\]/g,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'COMPARISON',
|
|
46
|
+
regex: /\[(?:ALIA_)?COMPARISON title="([^"]+)"\]([\s\S]*?)\[\/(?:ALIA_)?COMPARISON\]/g,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'TIMELINE',
|
|
50
|
+
regex: /\[(?:ALIA_)?TIMELINE title="([^"]+)"\]([\s\S]*?)\[\/(?:ALIA_)?TIMELINE\]/g,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'IMAGE',
|
|
54
|
+
regex:
|
|
55
|
+
/\[(?:ALIA_)?IMAGE url="([^"]+)"(?:\s+title="([^"]*)")?\s*(?:caption="([^"]*)")?\s*\/\]/g,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'CREDIBILITY',
|
|
59
|
+
regex: /\[(?:ALIA_)?CREDIBILITY level="(\d+)" source="([^"]+)"\s*\/\]/g,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
let lastIndex = 0;
|
|
64
|
+
const matches: Array<{ index: number; length: number; block: any }> = [];
|
|
65
|
+
|
|
66
|
+
// Find all matches
|
|
67
|
+
patterns.forEach(({ name, regex }) => {
|
|
68
|
+
let match;
|
|
69
|
+
const regexCopy = new RegExp(regex.source, regex.flags);
|
|
70
|
+
while ((match = regexCopy.exec(content)) !== null) {
|
|
71
|
+
matches.push({
|
|
72
|
+
index: match.index,
|
|
73
|
+
length: match[0].length,
|
|
74
|
+
block: { type: name, match, fullMatch: match[0] },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Sort matches by index
|
|
80
|
+
matches.sort((a, b) => a.index - b.index);
|
|
81
|
+
|
|
82
|
+
// Build blocks array
|
|
83
|
+
matches.forEach((m) => {
|
|
84
|
+
// Add text before block
|
|
85
|
+
if (lastIndex < m.index) {
|
|
86
|
+
const textContent = content.substring(lastIndex, m.index).trim();
|
|
87
|
+
if (textContent) {
|
|
88
|
+
blocks.push({ type: 'text', content: textContent });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add block
|
|
93
|
+
blocks.push({
|
|
94
|
+
type: 'block',
|
|
95
|
+
content: m.block.fullMatch,
|
|
96
|
+
blockType: m.block.type,
|
|
97
|
+
data: parseBlockData(m.block.type, m.block.match),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
lastIndex = m.index + m.length;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Add remaining text
|
|
104
|
+
if (lastIndex < content.length) {
|
|
105
|
+
const textContent = content.substring(lastIndex).trim();
|
|
106
|
+
if (textContent) {
|
|
107
|
+
blocks.push({ type: 'text', content: textContent });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If no blocks found, return all as text
|
|
112
|
+
if (blocks.length === 0) {
|
|
113
|
+
blocks.push({ type: 'text', content });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return blocks;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseBlockData(type: string, match: RegExpExecArray): any {
|
|
120
|
+
try {
|
|
121
|
+
switch (type) {
|
|
122
|
+
case 'COMPACTLIST': {
|
|
123
|
+
const title = match[1];
|
|
124
|
+
const itemsText = match[2];
|
|
125
|
+
const items = itemsText
|
|
126
|
+
.split('\n')
|
|
127
|
+
.filter((line) => line.trim().startsWith('-'))
|
|
128
|
+
.map((line) => {
|
|
129
|
+
try {
|
|
130
|
+
const jsonStr = line.trim().substring(1).trim();
|
|
131
|
+
return JSON.parse(jsonStr);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
return { title, items };
|
|
138
|
+
}
|
|
139
|
+
case 'BANNER': {
|
|
140
|
+
return {
|
|
141
|
+
type: match[1],
|
|
142
|
+
title: match[2],
|
|
143
|
+
content: match[3].trim(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
case 'COMPARISON': {
|
|
147
|
+
const title = match[1];
|
|
148
|
+
const content = match[2];
|
|
149
|
+
const leftMatch = content.match(/LEFT:\s*({.*?})/s);
|
|
150
|
+
const rightMatch = content.match(/RIGHT:\s*({.*?})/s);
|
|
151
|
+
const conclusionMatch = content.match(/CONCLUSION:\s*(.*?)$/s);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
title,
|
|
155
|
+
left: leftMatch ? JSON.parse(leftMatch[1]) : {},
|
|
156
|
+
right: rightMatch ? JSON.parse(rightMatch[1]) : {},
|
|
157
|
+
conclusion: conclusionMatch ? conclusionMatch[1].trim() : undefined,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
case 'TIMELINE': {
|
|
161
|
+
const title = match[1];
|
|
162
|
+
const itemsText = match[2];
|
|
163
|
+
const items = itemsText
|
|
164
|
+
.split('\n')
|
|
165
|
+
.filter((line) => line.trim().startsWith('-'))
|
|
166
|
+
.map((line) => {
|
|
167
|
+
try {
|
|
168
|
+
const jsonStr = line.trim().substring(1).trim();
|
|
169
|
+
return JSON.parse(jsonStr);
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
.filter(Boolean);
|
|
175
|
+
return { title, items };
|
|
176
|
+
}
|
|
177
|
+
case 'IMAGE': {
|
|
178
|
+
return {
|
|
179
|
+
url: match[1],
|
|
180
|
+
title: match[2] || undefined,
|
|
181
|
+
caption: match[3] || undefined,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
case 'CREDIBILITY': {
|
|
185
|
+
return {
|
|
186
|
+
level: parseInt(match[1], 10),
|
|
187
|
+
source: match[2],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.error('Error parsing block data:', e);
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderBlock(blockType: string, data: any, key: number) {
|
|
200
|
+
switch (blockType) {
|
|
201
|
+
case 'COMPACTLIST':
|
|
202
|
+
return <CompactList key={key} {...data} />;
|
|
203
|
+
case 'BANNER':
|
|
204
|
+
return <Banner key={key} {...data} />;
|
|
205
|
+
case 'COMPARISON':
|
|
206
|
+
return <Comparison key={key} {...data} />;
|
|
207
|
+
case 'TIMELINE':
|
|
208
|
+
return <Timeline key={key} {...data} />;
|
|
209
|
+
case 'IMAGE':
|
|
210
|
+
return <RichImage key={key} {...data} />;
|
|
211
|
+
case 'CREDIBILITY':
|
|
212
|
+
return <Credibility key={key} {...data} />;
|
|
213
|
+
default:
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Open a source the way the Sources tab does: a new tab on web, the in-app browser on native. */
|
|
219
|
+
function openSource(url: string): void {
|
|
220
|
+
if (Platform.OS === 'web') {
|
|
221
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
222
|
+
} else {
|
|
223
|
+
void WebBrowser.openBrowserAsync(url);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The references section of a research answer, as links.
|
|
229
|
+
*
|
|
230
|
+
* The API now writes each entry as a Markdown link, and the SDK's renderer
|
|
231
|
+
* would make those pressable on its own — but not ACCESSIBLE: its link rule
|
|
232
|
+
* is a `Text` with `onPress`, which a screen reader reads as text and a
|
|
233
|
+
* keyboard cannot reach. So the section is cut off the Markdown and drawn
|
|
234
|
+
* here, one `accessibilityRole="link"` per entry — an `<a href>` on web, so
|
|
235
|
+
* Tab reaches it and Enter opens it — named "Source n: title". Older answers,
|
|
236
|
+
* whose entries were a title over a bare URL, split the same way.
|
|
237
|
+
*/
|
|
238
|
+
function ReferenceList({
|
|
239
|
+
entries,
|
|
240
|
+
onCitationPress,
|
|
241
|
+
}: {
|
|
242
|
+
entries: ReferenceEntry[];
|
|
243
|
+
onCitationPress?: (source: CitationSource) => void;
|
|
244
|
+
}) {
|
|
245
|
+
const { t } = useLabels();
|
|
246
|
+
return (
|
|
247
|
+
<View className="mt-3 gap-1 border-t border-border pt-3" accessibilityRole="list">
|
|
248
|
+
<Text className="text-xs font-medium text-muted-foreground">{t('thought.references')}</Text>
|
|
249
|
+
{entries.map((entry) => {
|
|
250
|
+
const press = () => {
|
|
251
|
+
if (onCitationPress) onCitationPress(entry);
|
|
252
|
+
else openSource(entry.url);
|
|
253
|
+
};
|
|
254
|
+
// On web the anchor itself navigates (new tab, no opener); `onPress`
|
|
255
|
+
// is wired only when the caller wants the press instead of the
|
|
256
|
+
// navigation, and the default is then prevented.
|
|
257
|
+
const webAnchor =
|
|
258
|
+
Platform.OS === 'web'
|
|
259
|
+
? ({
|
|
260
|
+
href: entry.url,
|
|
261
|
+
hrefAttrs: { target: '_blank', rel: 'noopener noreferrer' },
|
|
262
|
+
} as object)
|
|
263
|
+
: {};
|
|
264
|
+
const onPress =
|
|
265
|
+
Platform.OS === 'web'
|
|
266
|
+
? onCitationPress
|
|
267
|
+
? (e: { preventDefault?: () => void }) => {
|
|
268
|
+
e.preventDefault?.();
|
|
269
|
+
press();
|
|
270
|
+
}
|
|
271
|
+
: undefined
|
|
272
|
+
: press;
|
|
273
|
+
return (
|
|
274
|
+
<Pressable
|
|
275
|
+
key={entry.id}
|
|
276
|
+
accessibilityRole="link"
|
|
277
|
+
accessibilityLabel={t('thought.sourceLabel', { n: entry.id, title: entry.title })}
|
|
278
|
+
className="flex-row items-start gap-2 rounded-md py-0.5 active:opacity-70"
|
|
279
|
+
onPress={onPress}
|
|
280
|
+
{...webAnchor}
|
|
281
|
+
>
|
|
282
|
+
<Text className="text-sm text-muted-foreground">[{entry.id}]</Text>
|
|
283
|
+
<Text className="flex-1 text-sm text-primary underline" numberOfLines={2}>
|
|
284
|
+
{entry.title}
|
|
285
|
+
</Text>
|
|
286
|
+
<Text className="text-xs text-muted-foreground" numberOfLines={1}>
|
|
287
|
+
{entry.domain}
|
|
288
|
+
</Text>
|
|
289
|
+
</Pressable>
|
|
290
|
+
);
|
|
291
|
+
})}
|
|
292
|
+
</View>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Copy through Expo's clipboard, which Bloom's card needs on native (web has its own). */
|
|
297
|
+
const copyCode = async (code: string) => {
|
|
298
|
+
await Clipboard.setStringAsync(code);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Fenced code as Bloom's code card: the language chip, a copy button and
|
|
303
|
+
* numbered lines. Module-level so its identity is stable — it is part of the
|
|
304
|
+
* SDK renderer's rules memo.
|
|
305
|
+
*/
|
|
306
|
+
const renderCodeBlock: RenderCodeBlock = (code, language) => (
|
|
307
|
+
<CodeBlock code={code} language={language} onCopy={copyCode} />
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The chat's Markdown, plus what a research answer needs on top of it.
|
|
312
|
+
*
|
|
313
|
+
* `toolInvocations` (the message's persisted record) and `researchSources`
|
|
314
|
+
* (the live progress event, before persistence) are where the `[n]` markers
|
|
315
|
+
* resolve: a marker naming a known source becomes a link to it, titled
|
|
316
|
+
* "Source n: title", and the References section becomes the accessible list
|
|
317
|
+
* above. Markers the model spelled as `【n†…】` or `[n, m]` in an older answer
|
|
318
|
+
* are normalised first. A message with neither prop renders exactly as
|
|
319
|
+
* before.
|
|
320
|
+
*
|
|
321
|
+
* `onCitationPress` replaces opening the URL for the references list. The
|
|
322
|
+
* inline markers are rendered by the SDK's link rule, which opens the URL
|
|
323
|
+
* itself and offers no hook, so they do not reach it.
|
|
324
|
+
*/
|
|
325
|
+
export function ReplyMarkdown({
|
|
326
|
+
content,
|
|
327
|
+
toolInvocations,
|
|
328
|
+
researchSources,
|
|
329
|
+
onCitationPress,
|
|
330
|
+
}: {
|
|
331
|
+
content: string;
|
|
332
|
+
toolInvocations?: ToolInvocation[];
|
|
333
|
+
researchSources?: Array<{ id: number; url: string; title: string }> | null;
|
|
334
|
+
onCitationPress?: (source: CitationSource) => void;
|
|
335
|
+
}) {
|
|
336
|
+
const sources = useMemo(
|
|
337
|
+
() => extractCitationSources(toolInvocations, researchSources),
|
|
338
|
+
[toolInvocations, researchSources],
|
|
339
|
+
);
|
|
340
|
+
const { body, references } = useMemo(() => splitReferences(content, sources), [content, sources]);
|
|
341
|
+
const linked = useMemo(() => linkifyCitations(body, sources), [body, sources]);
|
|
342
|
+
const blocks = useMemo(() => parseSpecialBlocks(linked), [linked]);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<View>
|
|
346
|
+
{blocks.map((block, idx) => {
|
|
347
|
+
if (block.type === 'text') {
|
|
348
|
+
return (
|
|
349
|
+
<AliaMarkdown key={idx} content={block.content} renderCodeBlock={renderCodeBlock} />
|
|
350
|
+
);
|
|
351
|
+
} else if (block.type === 'block' && block.blockType) {
|
|
352
|
+
return renderBlock(block.blockType, block.data, idx);
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
})}
|
|
356
|
+
{references ? <ReferenceList entries={references} onCitationPress={onCitationPress} /> : null}
|
|
357
|
+
</View>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { useLabels } from '../lib/labels';
|
|
2
|
+
import {
|
|
3
|
+
Admonition,
|
|
4
|
+
AdmonitionContent,
|
|
5
|
+
AdmonitionIcon,
|
|
6
|
+
AdmonitionRoot,
|
|
7
|
+
AdmonitionRow,
|
|
8
|
+
AdmonitionText,
|
|
9
|
+
} from '@oxy.so/bloom/admonition';
|
|
10
|
+
import { Card, CardBody, CardHeader, CardTitle } from '@oxy.so/bloom/card';
|
|
11
|
+
import { RiExternalLinkLine } from '@oxy.so/bloom/icons/RiExternalLinkLine';
|
|
12
|
+
import { Item } from '@oxy.so/bloom/item';
|
|
13
|
+
import { Rating } from '@oxy.so/bloom/rating';
|
|
14
|
+
import { Muted, Text } from '@oxy.so/bloom/typography';
|
|
15
|
+
import { Image } from './Image';
|
|
16
|
+
import { Linking, View } from 'react-native';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The rich blocks a reply can carry (```compactlist, ```banner, …), drawn with
|
|
20
|
+
* Bloom: a card per block, `Item` rows, `Admonition` for a banner and `Rating`
|
|
21
|
+
* for a source's credibility. `ReplyMarkdown.tsx` parses the fences.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type CompactListItem = {
|
|
25
|
+
title: string;
|
|
26
|
+
href?: string;
|
|
27
|
+
meta?: string;
|
|
28
|
+
image?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function CompactList({ title, items }: { title: string; items: CompactListItem[] }) {
|
|
32
|
+
return (
|
|
33
|
+
<Card appearance="outline" className="my-2">
|
|
34
|
+
<CardHeader>
|
|
35
|
+
<CardTitle>{title}</CardTitle>
|
|
36
|
+
</CardHeader>
|
|
37
|
+
<CardBody>
|
|
38
|
+
<View role="list">
|
|
39
|
+
{items.map((item, index) => (
|
|
40
|
+
<Item
|
|
41
|
+
key={index}
|
|
42
|
+
role="listitem"
|
|
43
|
+
title={item.title}
|
|
44
|
+
subtitle={item.meta}
|
|
45
|
+
leading={
|
|
46
|
+
item.image ? (
|
|
47
|
+
<Image
|
|
48
|
+
source={{ uri: item.image }}
|
|
49
|
+
className="h-10 w-10 rounded"
|
|
50
|
+
contentFit="cover"
|
|
51
|
+
/>
|
|
52
|
+
) : undefined
|
|
53
|
+
}
|
|
54
|
+
trailing={item.href ? <RiExternalLinkLine size="sm" /> : undefined}
|
|
55
|
+
onPress={item.href ? () => void Linking.openURL(item.href!) : undefined}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</View>
|
|
59
|
+
</CardBody>
|
|
60
|
+
</Card>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type BannerType = 'info' | 'success' | 'warning' | 'danger';
|
|
65
|
+
|
|
66
|
+
/** A block's tone, in Bloom's admonition vocabulary. */
|
|
67
|
+
const ADMONITION = { info: 'info', success: 'tip', warning: 'warning', danger: 'error' } as const;
|
|
68
|
+
|
|
69
|
+
export function Banner({
|
|
70
|
+
type = 'info',
|
|
71
|
+
title,
|
|
72
|
+
content,
|
|
73
|
+
}: {
|
|
74
|
+
type?: BannerType;
|
|
75
|
+
title: string;
|
|
76
|
+
content: string;
|
|
77
|
+
}) {
|
|
78
|
+
return (
|
|
79
|
+
<View className="my-2">
|
|
80
|
+
<AdmonitionRoot type={Object.hasOwn(ADMONITION, type) ? ADMONITION[type] : 'info'}>
|
|
81
|
+
<AdmonitionRow>
|
|
82
|
+
<AdmonitionIcon />
|
|
83
|
+
<AdmonitionContent>
|
|
84
|
+
<Text variant="body-medium">{title}</Text>
|
|
85
|
+
<AdmonitionText>{content}</AdmonitionText>
|
|
86
|
+
</AdmonitionContent>
|
|
87
|
+
</AdmonitionRow>
|
|
88
|
+
</AdmonitionRoot>
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type ComparisonSide = {
|
|
94
|
+
title: string;
|
|
95
|
+
content: string;
|
|
96
|
+
source?: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function ComparisonSideCard({ side }: { side: ComparisonSide }) {
|
|
100
|
+
const { t } = useLabels();
|
|
101
|
+
return (
|
|
102
|
+
<Card appearance="outline">
|
|
103
|
+
<CardBody>
|
|
104
|
+
<Text variant="body-medium">{side.title}</Text>
|
|
105
|
+
<Muted>{side.content}</Muted>
|
|
106
|
+
{side.source ? <Muted>{t('chat.richBlocks.source', { source: side.source })}</Muted> : null}
|
|
107
|
+
</CardBody>
|
|
108
|
+
</Card>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function Comparison({
|
|
113
|
+
title,
|
|
114
|
+
left,
|
|
115
|
+
right,
|
|
116
|
+
conclusion,
|
|
117
|
+
}: {
|
|
118
|
+
title: string;
|
|
119
|
+
left: ComparisonSide;
|
|
120
|
+
right: ComparisonSide;
|
|
121
|
+
conclusion?: string;
|
|
122
|
+
}) {
|
|
123
|
+
return (
|
|
124
|
+
<Card appearance="outline" className="my-2">
|
|
125
|
+
<CardHeader>
|
|
126
|
+
<CardTitle>{title}</CardTitle>
|
|
127
|
+
</CardHeader>
|
|
128
|
+
<CardBody>
|
|
129
|
+
<View className="gap-2">
|
|
130
|
+
<ComparisonSideCard side={left} />
|
|
131
|
+
<ComparisonSideCard side={right} />
|
|
132
|
+
{conclusion ? <Admonition type="tip">{conclusion}</Admonition> : null}
|
|
133
|
+
</View>
|
|
134
|
+
</CardBody>
|
|
135
|
+
</Card>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type TimelineItem = {
|
|
140
|
+
date: string;
|
|
141
|
+
title: string;
|
|
142
|
+
description?: string;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export function Timeline({ title, items }: { title: string; items: TimelineItem[] }) {
|
|
146
|
+
return (
|
|
147
|
+
<Card appearance="outline" className="my-2">
|
|
148
|
+
<CardHeader>
|
|
149
|
+
<CardTitle>{title}</CardTitle>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardBody>
|
|
152
|
+
<View role="list">
|
|
153
|
+
{items.map((item, index) => (
|
|
154
|
+
<Item
|
|
155
|
+
key={index}
|
|
156
|
+
role="listitem"
|
|
157
|
+
leading={<Muted>{item.date}</Muted>}
|
|
158
|
+
title={item.title}
|
|
159
|
+
subtitle={item.description}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
</View>
|
|
163
|
+
</CardBody>
|
|
164
|
+
</Card>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function RichImage({
|
|
169
|
+
url,
|
|
170
|
+
title,
|
|
171
|
+
caption,
|
|
172
|
+
}: {
|
|
173
|
+
url: string;
|
|
174
|
+
title?: string;
|
|
175
|
+
caption?: string;
|
|
176
|
+
}) {
|
|
177
|
+
return (
|
|
178
|
+
<View className="my-2 gap-1">
|
|
179
|
+
<Image source={{ uri: url }} className="aspect-video w-full rounded-lg" contentFit="cover" />
|
|
180
|
+
{title ? <Text variant="body-medium">{title}</Text> : null}
|
|
181
|
+
{caption ? <Muted>{caption}</Muted> : null}
|
|
182
|
+
</View>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** How far a source can be trusted, on a five-point scale. */
|
|
187
|
+
export function Credibility({ level, source }: { level: number; source: string }) {
|
|
188
|
+
const { t } = useLabels();
|
|
189
|
+
return (
|
|
190
|
+
<View className="my-2 flex-row items-center gap-2">
|
|
191
|
+
<Rating value={Math.max(0, Math.min(5, level))} size="sm" />
|
|
192
|
+
<Muted>{t('chat.richBlocks.source', { source })}</Muted>
|
|
193
|
+
</View>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useLabels } from '../../lib/labels';
|
|
2
|
+
import { Muted, Text } from '@oxy.so/bloom/typography';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import { ChartRenderer } from './ChartRenderer';
|
|
5
|
+
import { CodeRenderer } from './CodeRenderer';
|
|
6
|
+
import { FormRenderer } from './FormRenderer';
|
|
7
|
+
import { MarkdownRenderer } from './MarkdownRenderer';
|
|
8
|
+
import { TableRenderer } from './TableRenderer';
|
|
9
|
+
|
|
10
|
+
interface CanvasComponentProps {
|
|
11
|
+
component: {
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
title: string;
|
|
15
|
+
data: any;
|
|
16
|
+
};
|
|
17
|
+
onFormSubmit?: (formData: Record<string, any>) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CanvasComponent({ component, onFormSubmit }: CanvasComponentProps) {
|
|
21
|
+
const { t } = useLabels();
|
|
22
|
+
|
|
23
|
+
// Code carries its own header (the file name), and a chart card its own title.
|
|
24
|
+
if (component.type === 'code')
|
|
25
|
+
return <CodeRenderer data={component.data} filename={component.title} />;
|
|
26
|
+
if (component.type === 'artifact' && component.data.language)
|
|
27
|
+
return (
|
|
28
|
+
<CodeRenderer
|
|
29
|
+
data={{ language: component.data.language, code: component.data.content }}
|
|
30
|
+
filename={component.title}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
if (component.type === 'chart')
|
|
34
|
+
return <ChartRenderer data={component.data} title={component.title} />;
|
|
35
|
+
|
|
36
|
+
const content = (() => {
|
|
37
|
+
switch (component.type) {
|
|
38
|
+
case 'table':
|
|
39
|
+
return <TableRenderer data={component.data} title={component.title} />;
|
|
40
|
+
case 'form':
|
|
41
|
+
return <FormRenderer data={component.data} onSubmit={onFormSubmit} />;
|
|
42
|
+
case 'image':
|
|
43
|
+
return <Muted>{component.data.alt || t('panels.canvas.image')}</Muted>;
|
|
44
|
+
case 'markdown':
|
|
45
|
+
return <MarkdownRenderer data={component.data} />;
|
|
46
|
+
case 'artifact':
|
|
47
|
+
return <MarkdownRenderer data={{ content: component.data.content }} />;
|
|
48
|
+
default:
|
|
49
|
+
return <Muted>{t('panels.canvas.unsupported', { type: component.type })}</Muted>;
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View className="gap-3">
|
|
55
|
+
<Text variant="body-semibold">{component.title}</Text>
|
|
56
|
+
{content}
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useLabels } from '../../lib/labels';
|
|
2
|
+
import { BarListCard, LineChartCard } from '@oxy.so/bloom/chart-cards';
|
|
3
|
+
|
|
4
|
+
interface ChartData {
|
|
5
|
+
chartType: 'bar' | 'line' | 'pie';
|
|
6
|
+
labels: string[];
|
|
7
|
+
datasets: { label: string; values: number[] }[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ChartRendererProps {
|
|
11
|
+
data: ChartData;
|
|
12
|
+
/** The canvas component's title, used as the chart's name. */
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const NUMBER = new Intl.NumberFormat();
|
|
17
|
+
const format = (value: number) => NUMBER.format(value);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A generated chart on Bloom's chart cards, painted from Bloom's chart palette.
|
|
21
|
+
*
|
|
22
|
+
* - one `line` series → `LineChartCard`;
|
|
23
|
+
* - a `pie` → `BarListCard` in shares of the whole, which is what a pie says;
|
|
24
|
+
* - bars, or several series → `BarListCard` in raw values, one tab per series.
|
|
25
|
+
*/
|
|
26
|
+
export function ChartRenderer({ data, title }: ChartRendererProps) {
|
|
27
|
+
const { t } = useLabels();
|
|
28
|
+
const { chartType, labels, datasets } = data;
|
|
29
|
+
const itemsOf = (values: number[]) =>
|
|
30
|
+
labels.map((label, i) => ({ label, value: values[i] || 0 }));
|
|
31
|
+
|
|
32
|
+
if (chartType === 'line' && datasets.length === 1) {
|
|
33
|
+
const series = datasets[0];
|
|
34
|
+
return (
|
|
35
|
+
<LineChartCard
|
|
36
|
+
title={series.label || title}
|
|
37
|
+
data={itemsOf(series.values)}
|
|
38
|
+
format={format}
|
|
39
|
+
formatAxisValue={format}
|
|
40
|
+
getPointTitle={(point) => point.label}
|
|
41
|
+
accessibilityLabel={title ?? series.label}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (chartType === 'pie' || datasets.length <= 1) {
|
|
47
|
+
const series = datasets[0];
|
|
48
|
+
return (
|
|
49
|
+
<BarListCard
|
|
50
|
+
title={series?.label || title}
|
|
51
|
+
metricLabel={chartType === 'pie' ? t('panels.chart.share') : t('panels.chart.value')}
|
|
52
|
+
metric={chartType === 'pie' ? 'share' : 'value'}
|
|
53
|
+
format={format}
|
|
54
|
+
items={itemsOf(series?.values ?? [])}
|
|
55
|
+
limit={labels.length}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<BarListCard
|
|
62
|
+
metricLabel={t('panels.chart.value')}
|
|
63
|
+
metric="value"
|
|
64
|
+
format={format}
|
|
65
|
+
tabs={datasets.map((series, i) => ({
|
|
66
|
+
id: `series-${i}`,
|
|
67
|
+
label: series.label,
|
|
68
|
+
items: itemsOf(series.values),
|
|
69
|
+
}))}
|
|
70
|
+
limit={labels.length}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|