@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,383 @@
|
|
|
1
|
+
import type { AutomationTrigger, ToolInvocation } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The cards a tool result draws inside the assistant turn, as data.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is pure: the snapshot the tool stored with the message goes
|
|
7
|
+
* in, and what Bloom's `LineChartCard`, `Card` and `Item` are handed comes
|
|
8
|
+
* out. A thread reopened next month shows the numbers it was answered with,
|
|
9
|
+
* because nothing is fetched — every range the card offers is already in the
|
|
10
|
+
* snapshot. The producers are `packages/api/src/lib/tools/{weather,market,
|
|
11
|
+
* faircoin}.ts` and the scheduling tool.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type Translate = (key: string, params?: Record<string, string>) => string;
|
|
15
|
+
|
|
16
|
+
/** The tool results that are a card rather than a step of the turn's work. */
|
|
17
|
+
const CARD_TYPES = new Set(['weather', 'market', 'faircoin', 'scheduled-task']);
|
|
18
|
+
|
|
19
|
+
export type ToolCard =
|
|
20
|
+
| { type: 'weather'; data: WeatherCardData }
|
|
21
|
+
| { type: 'market'; data: MarketCardData }
|
|
22
|
+
| { type: 'faircoin'; data: FairCoinCardData }
|
|
23
|
+
| { type: 'scheduled-task'; data: ScheduledTaskCardData };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The card a finished call returned, if it is one this conversation draws.
|
|
27
|
+
*
|
|
28
|
+
* It checks the card's NAME, not the shape of its `data`, which is then cast
|
|
29
|
+
* unchecked — so each card renders inside its own error boundary, and the
|
|
30
|
+
* builders below guard every field they read.
|
|
31
|
+
*/
|
|
32
|
+
export function cardOf(t: ToolInvocation): ToolCard | null {
|
|
33
|
+
if (t.state !== 'result') return null;
|
|
34
|
+
const card = (t.result as { card?: { type?: string; data?: unknown } } | undefined)?.card;
|
|
35
|
+
if (card?.type === undefined || !CARD_TYPES.has(card.type) || !card.data) return null;
|
|
36
|
+
return { type: card.type, data: card.data } as ToolCard;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Shared formatting
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/** `[msSinceEpoch, price]`, exactly as the tool stored it. */
|
|
44
|
+
export type PricePoint = [number, number];
|
|
45
|
+
|
|
46
|
+
/** A coin worth 0.000023 is not "0.00", and one worth 60000 is not "60000.000000". */
|
|
47
|
+
const priceDigits = (value: number) => (Math.abs(value) < 1 ? 6 : 2);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* ISO 4217 is three letters and `Intl` throws a `RangeError` on anything that
|
|
51
|
+
* is not. The currency arrives from whatever the reader typed, so a typo must
|
|
52
|
+
* not take the whole card down.
|
|
53
|
+
*/
|
|
54
|
+
const isCurrencyCode = (code: string) => /^[A-Za-z]{3}$/.test(code);
|
|
55
|
+
|
|
56
|
+
export function formatPrice(value: number, currency: string, locale: string): string {
|
|
57
|
+
const options: Intl.NumberFormatOptions = {
|
|
58
|
+
minimumFractionDigits: 2,
|
|
59
|
+
maximumFractionDigits: priceDigits(value),
|
|
60
|
+
};
|
|
61
|
+
if (!isCurrencyCode(currency)) {
|
|
62
|
+
return `${value.toLocaleString(locale, options)} ${currency.toUpperCase()}`;
|
|
63
|
+
}
|
|
64
|
+
return value.toLocaleString(locale, {
|
|
65
|
+
...options,
|
|
66
|
+
style: 'currency',
|
|
67
|
+
currency: currency.toUpperCase(),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The axis labels a price scale, not a currency: no symbol, compact, and three
|
|
73
|
+
* significant digits — the chart's axis is 44px wide, and "60,000" does not fit
|
|
74
|
+
* in it where "60K" does.
|
|
75
|
+
*/
|
|
76
|
+
const formatAxis = (value: number, locale: string) =>
|
|
77
|
+
value.toLocaleString(locale, { notation: 'compact', maximumSignificantDigits: 3 });
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The scale names go through a translation whole — Spanish "billón" is 10¹²,
|
|
81
|
+
* not 10⁹ — so a suffix is never appended in one language and reused in the
|
|
82
|
+
* next.
|
|
83
|
+
*/
|
|
84
|
+
const SCALES = [
|
|
85
|
+
{ at: 1e12, key: 'trillion' },
|
|
86
|
+
{ at: 1e9, key: 'billion' },
|
|
87
|
+
{ at: 1e6, key: 'million' },
|
|
88
|
+
{ at: 1e3, key: 'thousand' },
|
|
89
|
+
] as const;
|
|
90
|
+
|
|
91
|
+
function formatCompact(
|
|
92
|
+
value: number,
|
|
93
|
+
namespace: string,
|
|
94
|
+
locale: string,
|
|
95
|
+
t: Translate,
|
|
96
|
+
fallback: string,
|
|
97
|
+
): string {
|
|
98
|
+
const scale = SCALES.find((s) => Math.abs(value) >= s.at);
|
|
99
|
+
if (!scale) return fallback;
|
|
100
|
+
return t(`${namespace}.scale.${scale.key}`, {
|
|
101
|
+
value: (value / scale.at).toLocaleString(locale, { maximumFractionDigits: 2 }),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A field the row did not carry reads the same as one that could not be
|
|
107
|
+
* priced: `null` from the tool, `undefined` from an older row and a `NaN` that
|
|
108
|
+
* survived a parse are all "no number".
|
|
109
|
+
*/
|
|
110
|
+
const quoted = (value: number | null | undefined): value is number =>
|
|
111
|
+
typeof value === 'number' && Number.isFinite(value);
|
|
112
|
+
|
|
113
|
+
/** A series point as the chart reads it, labelled with its moment. */
|
|
114
|
+
function toPoints(series: readonly PricePoint[], intraday: boolean, locale: string) {
|
|
115
|
+
return series.map(([at, value]) => ({
|
|
116
|
+
label: new Date(at).toLocaleString(
|
|
117
|
+
locale,
|
|
118
|
+
intraday
|
|
119
|
+
? { hour: 'numeric', minute: '2-digit' }
|
|
120
|
+
: { day: 'numeric', month: 'short', year: 'numeric' },
|
|
121
|
+
),
|
|
122
|
+
value,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** First-to-last change as a ratio, or none for a run that started at nothing. */
|
|
127
|
+
function runDelta(series: readonly PricePoint[]): number | undefined {
|
|
128
|
+
const first = series[0][1];
|
|
129
|
+
const last = series[series.length - 1][1];
|
|
130
|
+
return first === 0 ? undefined : (last - first) / first;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface PriceChartRange {
|
|
134
|
+
id: string;
|
|
135
|
+
label: string;
|
|
136
|
+
data: { label: string; value: number }[];
|
|
137
|
+
headline: number;
|
|
138
|
+
delta?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** What `LineChartCard` is handed, plus the lines the card cannot draw itself. */
|
|
142
|
+
export interface PriceChart {
|
|
143
|
+
title: string;
|
|
144
|
+
ranges: PriceChartRange[];
|
|
145
|
+
defaultRange: string;
|
|
146
|
+
format: (value: number) => string;
|
|
147
|
+
formatAxisValue: (value: number) => string;
|
|
148
|
+
/** Secondary lines under the chart, in reading order. */
|
|
149
|
+
notes: string[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Market
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
export interface MarketCardData {
|
|
157
|
+
id: string;
|
|
158
|
+
name: string;
|
|
159
|
+
symbol: string;
|
|
160
|
+
currency: string;
|
|
161
|
+
price?: number;
|
|
162
|
+
changePct?: number;
|
|
163
|
+
changeAbs?: number;
|
|
164
|
+
marketCap?: number;
|
|
165
|
+
volume24h?: number;
|
|
166
|
+
/** Optional per key: a row stored by an older version of the tool is not a crash. */
|
|
167
|
+
series: Record<string, PricePoint[] | undefined>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The order the switcher offers, not the order the tool happens to store. */
|
|
171
|
+
const MARKET_RANGES = ['1D', '5D', '1M', '6M', 'YTD', '1Y', '5Y', 'MAX'] as const;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A quote as a line chart, or `null` when no range has two points to draw —
|
|
175
|
+
* the card then says the price in words.
|
|
176
|
+
*
|
|
177
|
+
* A range with one point cannot be drawn and a range with none was never
|
|
178
|
+
* stored, so neither is offered (YTD in the first days of January is the real
|
|
179
|
+
* case). 1D reports the tool's own change rather than the ends of the series,
|
|
180
|
+
* which starts whenever the window opened.
|
|
181
|
+
*/
|
|
182
|
+
export function marketChart(data: MarketCardData, t: Translate, locale: string): PriceChart | null {
|
|
183
|
+
const series = data.series ?? {};
|
|
184
|
+
const ranges: PriceChartRange[] = [];
|
|
185
|
+
for (const id of MARKET_RANGES) {
|
|
186
|
+
const run = series[id];
|
|
187
|
+
if (!run || run.length < 2) continue;
|
|
188
|
+
ranges.push({
|
|
189
|
+
id,
|
|
190
|
+
label: t(`market.range.${id}`),
|
|
191
|
+
data: toPoints(run, id === '1D', locale),
|
|
192
|
+
headline: quoted(data.price) ? data.price : run[run.length - 1][1],
|
|
193
|
+
delta: id === '1D' && quoted(data.changePct) ? data.changePct / 100 : runDelta(run),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (ranges.length === 0) return null;
|
|
197
|
+
|
|
198
|
+
const format = (value: number) => formatPrice(value, data.currency, locale);
|
|
199
|
+
const compact = (value: number) => {
|
|
200
|
+
const scaled = formatCompact(value, 'market', locale, t, '');
|
|
201
|
+
return scaled === '' ? format(value) : `${scaled} ${data.currency.toUpperCase()}`;
|
|
202
|
+
};
|
|
203
|
+
const notes: string[] = [];
|
|
204
|
+
const intraday = (series['1D'] ?? []).map(([, price]) => price);
|
|
205
|
+
if (intraday.length >= 2) {
|
|
206
|
+
notes.push(
|
|
207
|
+
[
|
|
208
|
+
`${t('market.open')} ${format(intraday[0])}`,
|
|
209
|
+
`${t('market.dayHigh')} ${format(Math.max(...intraday))}`,
|
|
210
|
+
`${t('market.dayLow')} ${format(Math.min(...intraday))}`,
|
|
211
|
+
].join(' · '),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const totals = [
|
|
215
|
+
quoted(data.volume24h) ? `${t('market.volume')} ${compact(data.volume24h)}` : null,
|
|
216
|
+
quoted(data.marketCap) ? `${t('market.marketCap')} ${compact(data.marketCap)}` : null,
|
|
217
|
+
].filter((part): part is string => part !== null);
|
|
218
|
+
if (totals.length > 0) notes.push(totals.join(' · '));
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
title: `${data.name} · ${data.symbol}`,
|
|
222
|
+
ranges,
|
|
223
|
+
defaultRange: ranges.some((r) => r.id === '1D') ? '1D' : ranges[0].id,
|
|
224
|
+
format,
|
|
225
|
+
formatAxisValue: (value) => formatAxis(value, locale),
|
|
226
|
+
notes,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// FairCoin
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
export interface FairCoinCardData {
|
|
235
|
+
/** `null` is a real answer: the pool had no indexed price when asked. */
|
|
236
|
+
price: number | null;
|
|
237
|
+
changePct: number | null;
|
|
238
|
+
volume24h: number | null;
|
|
239
|
+
liquidityUsd: number | null;
|
|
240
|
+
marketCapUsd: number | null;
|
|
241
|
+
/** Where the number came from, which is the one thing this card cannot drop. */
|
|
242
|
+
source: string;
|
|
243
|
+
updatedAt: string;
|
|
244
|
+
/**
|
|
245
|
+
* Absent key and empty array mean different things: the tool omits a period
|
|
246
|
+
* whose fetch failed, and stores `[]` for one with no samples yet.
|
|
247
|
+
*/
|
|
248
|
+
series: Record<string, PricePoint[] | undefined>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Exactly what the explorer retains, in the order the switcher offers them. */
|
|
252
|
+
const FAIRCOIN_RANGES = ['24h', '7d', '30d', '1y', 'all'] as const;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* FairCoin's price as a line chart, with its provenance ALWAYS in the notes.
|
|
256
|
+
*
|
|
257
|
+
* FairCoin has no exchange listing: the price is GeckoTerminal's indexed price
|
|
258
|
+
* for a thin pool, so where it came from and when is printed in the reading
|
|
259
|
+
* order — a number off a thin pool that looks exactly like an exchange quote
|
|
260
|
+
* is the one thing this card must not ship. `chart` is `null` when no period
|
|
261
|
+
* has two samples; the notes still say why.
|
|
262
|
+
*/
|
|
263
|
+
export function faircoinChart(
|
|
264
|
+
data: FairCoinCardData,
|
|
265
|
+
t: Translate,
|
|
266
|
+
locale: string,
|
|
267
|
+
): { chart: PriceChart | null; notes: string[] } {
|
|
268
|
+
const series = data.series ?? {};
|
|
269
|
+
const format = (value: number) => formatPrice(value, 'USD', locale);
|
|
270
|
+
const ranges: PriceChartRange[] = [];
|
|
271
|
+
for (const id of FAIRCOIN_RANGES) {
|
|
272
|
+
const run = series[id];
|
|
273
|
+
if (!run || run.length < 2) continue;
|
|
274
|
+
ranges.push({
|
|
275
|
+
id,
|
|
276
|
+
label: t(`faircoin.range.${id}`),
|
|
277
|
+
data: toPoints(run, id === '24h', locale),
|
|
278
|
+
headline: quoted(data.price) ? data.price : run[run.length - 1][1],
|
|
279
|
+
delta: id === '24h' && quoted(data.changePct) ? data.changePct / 100 : runDelta(run),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const notes: string[] = [t('faircoin.indexed')];
|
|
284
|
+
const at = Date.parse(data.updatedAt);
|
|
285
|
+
// An unparseable stamp is left out rather than printed raw: the source alone
|
|
286
|
+
// is still true.
|
|
287
|
+
notes.push(
|
|
288
|
+
[
|
|
289
|
+
t('faircoin.source', { source: data.source }),
|
|
290
|
+
Number.isNaN(at)
|
|
291
|
+
? null
|
|
292
|
+
: t('faircoin.updated', {
|
|
293
|
+
time: new Date(at).toLocaleString(locale, { dateStyle: 'medium', timeStyle: 'short' }),
|
|
294
|
+
}),
|
|
295
|
+
]
|
|
296
|
+
.filter((part): part is string => part !== null)
|
|
297
|
+
.join(' · '),
|
|
298
|
+
);
|
|
299
|
+
if (!quoted(data.price)) notes.push(t('faircoin.noPriceHint'));
|
|
300
|
+
// A period the explorer never answered for is named; one it answered with too
|
|
301
|
+
// few samples is said out loud, so neither reads as a period nobody asked about.
|
|
302
|
+
const missing = FAIRCOIN_RANGES.filter((id) => series[id] === undefined);
|
|
303
|
+
if (missing.length > 0) {
|
|
304
|
+
notes.push(
|
|
305
|
+
t('faircoin.unavailable', {
|
|
306
|
+
ranges: missing.map((id) => t(`faircoin.range.${id}`)).join(', '),
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (ranges.length === 0 && missing.length < FAIRCOIN_RANGES.length) {
|
|
311
|
+
const anySample = FAIRCOIN_RANGES.some((id) => (series[id]?.length ?? 0) === 1);
|
|
312
|
+
notes.push(t(anySample ? 'faircoin.noChart' : 'faircoin.noSamples'));
|
|
313
|
+
}
|
|
314
|
+
// FairCoin's scale strings are SUFFIXES ("K", " mil"), unlike the market's
|
|
315
|
+
// templates, so the number is written here and the word appended.
|
|
316
|
+
const compact = (value: number) => {
|
|
317
|
+
const scale = SCALES.find((s) => Math.abs(value) >= s.at);
|
|
318
|
+
if (!scale) return format(value);
|
|
319
|
+
const short = (value / scale.at).toLocaleString(locale, { maximumFractionDigits: 2 });
|
|
320
|
+
return `$${short}${t(`faircoin.scale.${scale.key}`)}`;
|
|
321
|
+
};
|
|
322
|
+
const totals = [
|
|
323
|
+
quoted(data.volume24h) ? `${t('faircoin.volume')} ${compact(data.volume24h)}` : null,
|
|
324
|
+
quoted(data.liquidityUsd) ? `${t('faircoin.liquidity')} ${compact(data.liquidityUsd)}` : null,
|
|
325
|
+
quoted(data.marketCapUsd) ? `${t('faircoin.marketCap')} ${compact(data.marketCapUsd)}` : null,
|
|
326
|
+
].filter((part): part is string => part !== null);
|
|
327
|
+
if (totals.length > 0) notes.push(totals.join(' · '));
|
|
328
|
+
|
|
329
|
+
if (ranges.length === 0) return { chart: null, notes };
|
|
330
|
+
return {
|
|
331
|
+
chart: {
|
|
332
|
+
title: `${t('faircoin.name')} · ${t('faircoin.symbol')}`,
|
|
333
|
+
ranges,
|
|
334
|
+
defaultRange: ranges.some((r) => r.id === '24h') ? '24h' : ranges[0].id,
|
|
335
|
+
format,
|
|
336
|
+
formatAxisValue: (value) => formatAxis(value, locale),
|
|
337
|
+
notes: [],
|
|
338
|
+
},
|
|
339
|
+
notes,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// Weather
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
export interface WeatherCardData {
|
|
348
|
+
place: string;
|
|
349
|
+
timezone: string;
|
|
350
|
+
current: { temperature: number; humidity: number; windSpeed: number; condition: string };
|
|
351
|
+
hourly: { time: string; temperature: number; precipitationChance: number }[];
|
|
352
|
+
daily: { date: string; condition: string; high: number; low: number }[];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Celsius on the wire; what the reader sees is their choice, not the answer's. */
|
|
356
|
+
export type TemperatureUnit = 'C' | 'F';
|
|
357
|
+
|
|
358
|
+
export const degrees = (celsius: number, unit: TemperatureUnit) =>
|
|
359
|
+
`${Math.round(unit === 'C' ? celsius : (celsius * 9) / 5 + 32)}°`;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The weekday a forecast date names, in the reader's language.
|
|
363
|
+
*
|
|
364
|
+
* `YYYY-MM-DD` parses as UTC midnight, which is the previous evening west of
|
|
365
|
+
* Greenwich — so the day is read at noon UTC, which is the same calendar day
|
|
366
|
+
* everywhere a person lives.
|
|
367
|
+
*/
|
|
368
|
+
export function forecastWeekday(date: string, locale: string): string {
|
|
369
|
+
const at = Date.parse(`${date}T12:00:00Z`);
|
|
370
|
+
if (Number.isNaN(at)) return date;
|
|
371
|
+
return new Date(at).toLocaleDateString(locale, { weekday: 'short', timeZone: 'UTC' });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Scheduled task
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
export interface ScheduledTaskCardData {
|
|
379
|
+
id: string;
|
|
380
|
+
objective: string;
|
|
381
|
+
enabled: boolean;
|
|
382
|
+
trigger: AutomationTrigger;
|
|
383
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A turn that did not get its answer, and what the thread shows for it.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a thing at all
|
|
5
|
+
*
|
|
6
|
+
* A hosted-inference failure reaches the client as a typed error, never as
|
|
7
|
+
* an answer: on the open stream, an `{"error": {code, retryable, reference}}`
|
|
8
|
+
* frame and no stop chunk; before the stream opened, an HTTP 503/500 with the
|
|
9
|
+
* same envelope (`packages/api/src/routes/v1/chat-completions.ts`,
|
|
10
|
+
* `failTurn`). Older servers answered a failure BEFORE any output with HTTP
|
|
11
|
+
* 200 and an ordinary content delta — "all models are busy, try again in a few
|
|
12
|
+
* seconds" — flagged only by `alia_meta: { synthetic: true, retryable: true }`
|
|
13
|
+
* on the chunk; that flag is still read, so a stand-in is never rendered.
|
|
14
|
+
* Rendered as prose a stand-in reads as Alia declining; rolled back either
|
|
15
|
+
* vanishes with a toast, and once the toast is gone there is nothing on screen
|
|
16
|
+
* that says what happened or offers to try again.
|
|
17
|
+
*
|
|
18
|
+
* So a failed send keeps the person's turn in the thread and hangs this on it:
|
|
19
|
+
* a persistent error the row is drawn with, and the material a retry needs.
|
|
20
|
+
*
|
|
21
|
+
* ## What the server persists when the reply is synthetic — measured, not assumed
|
|
22
|
+
*
|
|
23
|
+
* Nothing. `saveConversationResult` is called from `lib/chat/provider-loop.ts`
|
|
24
|
+
* only after a stream COMPLETES (`:331`); every other exit returns
|
|
25
|
+
* `{ status: 'exhausted' }` (`:458`) or throws, and both the route's synthetic
|
|
26
|
+
* branch and its outer `catch` write the stand-in chunk without ever reaching
|
|
27
|
+
* the saver. So the user message of a failed turn is not in the database, and
|
|
28
|
+
* a retry can simply re-send it through the same path — no second user row is
|
|
29
|
+
* created because no first one was. (Were a turn ever saved before its tail
|
|
30
|
+
* failed, `saveConversation` converges storage on the client's history anyway,
|
|
31
|
+
* by full rewrite on divergence.)
|
|
32
|
+
*/
|
|
33
|
+
export interface FailedTurn {
|
|
34
|
+
/** The person's message the missing answer belonged to. */
|
|
35
|
+
userMessageId: string;
|
|
36
|
+
/**
|
|
37
|
+
* The row the error is drawn UNDER: the user message when nothing came
|
|
38
|
+
* back, or the assistant message when real output arrived before the tail
|
|
39
|
+
* failed — in which case the output is kept and the error sits after it.
|
|
40
|
+
*/
|
|
41
|
+
anchorMessageId: string;
|
|
42
|
+
/** Whether trying again is offered. `false` when the server said so. */
|
|
43
|
+
retryable: boolean;
|
|
44
|
+
/** Real output arrived before the failure; the error is a tail, not the answer. */
|
|
45
|
+
partial: boolean;
|
|
46
|
+
/** The server's own words, when it sent any worth repeating. */
|
|
47
|
+
detail?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What a chunk's `alia_meta` says about itself. */
|
|
51
|
+
export interface AliaMeta {
|
|
52
|
+
/** The content is a stand-in the server wrote, not something the model said. */
|
|
53
|
+
synthetic: boolean;
|
|
54
|
+
/** Sending the same turn again is worth a try. Defaults to `true`. */
|
|
55
|
+
retryable: boolean;
|
|
56
|
+
/** Stable product error code; never an upstream/provider message. */
|
|
57
|
+
code?: string;
|
|
58
|
+
/** Safe Alia run identifier support can use to trace the failed turn. */
|
|
59
|
+
reference?: string;
|
|
60
|
+
retryAfter?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read `alia_meta` off a parsed stream chunk.
|
|
65
|
+
*
|
|
66
|
+
* Tolerant of every shape a chunk can have — no meta, meta with only one of
|
|
67
|
+
* the flags, a non-object where an object was expected — because this runs
|
|
68
|
+
* on every content delta and a throw here would take the whole stream down.
|
|
69
|
+
*/
|
|
70
|
+
export function readAliaMeta(chunk: unknown): AliaMeta {
|
|
71
|
+
const meta = (chunk as { alia_meta?: unknown } | null | undefined)?.alia_meta;
|
|
72
|
+
if (meta === null || typeof meta !== 'object') {
|
|
73
|
+
return { synthetic: false, retryable: true };
|
|
74
|
+
}
|
|
75
|
+
const { synthetic, retryable, error } = meta as {
|
|
76
|
+
synthetic?: unknown;
|
|
77
|
+
retryable?: unknown;
|
|
78
|
+
error?: unknown;
|
|
79
|
+
};
|
|
80
|
+
const failure =
|
|
81
|
+
error !== null && typeof error === 'object'
|
|
82
|
+
? (error as { code?: unknown; reference?: unknown; retryAfter?: unknown })
|
|
83
|
+
: null;
|
|
84
|
+
return {
|
|
85
|
+
synthetic: synthetic === true,
|
|
86
|
+
retryable: retryable !== false,
|
|
87
|
+
...(typeof failure?.code === 'string' ? { code: failure.code } : {}),
|
|
88
|
+
...(typeof failure?.reference === 'string' ? { reference: failure.reference } : {}),
|
|
89
|
+
...(typeof failure?.retryAfter === 'number' ? { retryAfter: failure.retryAfter } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The line under a failed turn's card: the stable code and the run reference.
|
|
95
|
+
*
|
|
96
|
+
* Never the server's prose. The card's own line is the app's, in the reader's
|
|
97
|
+
* language; this is what support needs, and both halves are language-free.
|
|
98
|
+
* Empty when the server sent neither.
|
|
99
|
+
*/
|
|
100
|
+
export function failureDetail(failure: { code?: string; reference?: string }): string | undefined {
|
|
101
|
+
const detail = [
|
|
102
|
+
failure.code,
|
|
103
|
+
failure.reference === undefined ? undefined : `Ref ${failure.reference}`,
|
|
104
|
+
]
|
|
105
|
+
.filter((value): value is string => typeof value === 'string' && value !== '')
|
|
106
|
+
.join(' · ');
|
|
107
|
+
return detail === '' ? undefined : detail;
|
|
108
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { MessagePlan, ResearchProgress } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The canonical tool invocation: streaming, the conversation hooks and the
|
|
5
|
+
* thread UI all read this one shape.
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolInvocation {
|
|
8
|
+
toolCallId: string;
|
|
9
|
+
toolName: string;
|
|
10
|
+
state: 'partial-call' | 'call' | 'result';
|
|
11
|
+
args?: Record<string, unknown>;
|
|
12
|
+
// Tool output is dynamically shaped per tool and consumed structurally
|
|
13
|
+
// (e.g. `result.results`, `result.url`) by the thread UI; typing it more
|
|
14
|
+
// narrowly would require validating every tool's output shape.
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural per-tool output, see above
|
|
16
|
+
result?: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** What a message carries to the model: plain text, or multi-part once images are attached. */
|
|
20
|
+
export type MessageContent = string | Array<{ type: string; [key: string]: unknown }>;
|
|
21
|
+
|
|
22
|
+
export interface Message {
|
|
23
|
+
id: string;
|
|
24
|
+
role: 'user' | 'assistant' | 'system';
|
|
25
|
+
content: MessageContent;
|
|
26
|
+
/** Extended thinking content (when thinking mode is enabled). */
|
|
27
|
+
thinking?: string;
|
|
28
|
+
toolInvocations?: ToolInvocation[];
|
|
29
|
+
/** Voice fields (optional, only present for voice-originated messages). */
|
|
30
|
+
source?: 'text' | 'voice';
|
|
31
|
+
/** Speaker identity in voice mode. */
|
|
32
|
+
speaker?: 'primary';
|
|
33
|
+
/**
|
|
34
|
+
* The turn is being streamed into this message right now.
|
|
35
|
+
*
|
|
36
|
+
* Set by `useConversationStream` on the assistant placeholder it appends and
|
|
37
|
+
* cleared by it when the stream ends, whichever way it ends; voice sets it
|
|
38
|
+
* on its own rows. Absent on a message read back from the server.
|
|
39
|
+
*/
|
|
40
|
+
isStreaming?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* How the turn that wrote this message ended, once it has: on its own
|
|
43
|
+
* (`completed`), on an error (`failed`), or because the person stopped it
|
|
44
|
+
* (`cancelled`). Stamped by `useConversationStream` alongside clearing
|
|
45
|
+
* `isStreaming`, and absent on a message the server sent.
|
|
46
|
+
*/
|
|
47
|
+
turnOutcome?: 'completed' | 'failed' | 'cancelled';
|
|
48
|
+
/**
|
|
49
|
+
* The server does not hold this message yet, so nothing can be addressed to
|
|
50
|
+
* it by id — a vote, a read-aloud clip stored on it.
|
|
51
|
+
*
|
|
52
|
+
* Set on every message of a turn the stream sends (the question, the reply,
|
|
53
|
+
* a delegated agent's answer) and cleared when that turn completes, which is
|
|
54
|
+
* when the server has stored it under these same ids. A turn that failed or
|
|
55
|
+
* was stopped keeps it.
|
|
56
|
+
*/
|
|
57
|
+
unsaved?: true;
|
|
58
|
+
/**
|
|
59
|
+
* The consulted agent that wrote this message (`consultAgent`, ADR 0020 §5): one message per
|
|
60
|
+
* consultation, drawn under the agent's own identity.
|
|
61
|
+
*
|
|
62
|
+
* `character` is the agent's look as the server stores it (JSON), parsed
|
|
63
|
+
* where it is drawn; absent, the default look.
|
|
64
|
+
*/
|
|
65
|
+
agentInfo?: {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
character?: unknown;
|
|
69
|
+
handle?: string;
|
|
70
|
+
color?: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* When the message was written, ISO — what the date separators are derived
|
|
74
|
+
* from, in the reader's own timezone. Absent means the turn has not been
|
|
75
|
+
* persisted yet and carries no stamp of its own.
|
|
76
|
+
*/
|
|
77
|
+
createdAt?: string;
|
|
78
|
+
// Streaming UI metadata (deep research, plan preview)
|
|
79
|
+
researchProgress?: ResearchProgress;
|
|
80
|
+
plan?: MessagePlan;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface Conversation {
|
|
84
|
+
id: string;
|
|
85
|
+
title: string;
|
|
86
|
+
lastMessage?: string;
|
|
87
|
+
source?: string;
|
|
88
|
+
/**
|
|
89
|
+
* The agent this conversation is a stretch of, or none.
|
|
90
|
+
*
|
|
91
|
+
* NULLABLE, not optional-absent: the column is `agent_id text` and the list
|
|
92
|
+
* route sends `c.agentId` straight through, so an ordinary conversation
|
|
93
|
+
* arrives carrying an explicit `null`.
|
|
94
|
+
*/
|
|
95
|
+
agentId?: string | null;
|
|
96
|
+
createdAt: Date;
|
|
97
|
+
updatedAt: Date;
|
|
98
|
+
messages: Message[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The run a delegated agent started for a turn: its session is `turnId`, in the agent's own room. */
|
|
102
|
+
export interface AgentRun {
|
|
103
|
+
turnId: string;
|
|
104
|
+
agentId: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** One page of the paginated conversation list. */
|
|
108
|
+
export interface ConversationsPage {
|
|
109
|
+
conversations: Conversation[];
|
|
110
|
+
nextCursor: string | null;
|
|
111
|
+
hasMore: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** React Query infinite-query cache shape for the conversation list. */
|
|
115
|
+
export interface ConversationsInfiniteData {
|
|
116
|
+
pages: ConversationsPage[];
|
|
117
|
+
pageParams: unknown[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** What a chat turn is sent with. */
|
|
121
|
+
export interface SendOptions {
|
|
122
|
+
/** `null` explicitly withholds MCP tools; omission preserves legacy callers. */
|
|
123
|
+
mcpServerId?: string | null;
|
|
124
|
+
/**
|
|
125
|
+
* The skills chosen for THIS message, by name. Per turn rather than per
|
|
126
|
+
* session; omitted means the person chose none.
|
|
127
|
+
*/
|
|
128
|
+
skillNames?: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A resource an automation acts on or listens to. */
|
|
132
|
+
export interface AutomationResource {
|
|
133
|
+
appId: string;
|
|
134
|
+
effectiveAccountId: string;
|
|
135
|
+
resourceType: string;
|
|
136
|
+
resourceId: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** What starts a scheduled task. */
|
|
140
|
+
export type AutomationTrigger =
|
|
141
|
+
| { type: 'manual' }
|
|
142
|
+
| {
|
|
143
|
+
type: 'event';
|
|
144
|
+
appId: string | null;
|
|
145
|
+
eventType: string | null;
|
|
146
|
+
resource?: AutomationResource | null;
|
|
147
|
+
}
|
|
148
|
+
| { type: 'schedule'; cron: string | null; timezone: string | null };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type UsageLimitType = 'credits' | 'rate_limit' | 'model_access';
|
|
2
|
+
|
|
3
|
+
export interface UsageLimitDetails {
|
|
4
|
+
type: UsageLimitType;
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
retryable: boolean;
|
|
8
|
+
retryAfterSeconds?: number;
|
|
9
|
+
suggestedAction?: 'upgrade' | 'wait';
|
|
10
|
+
limitType?: string;
|
|
11
|
+
current?: number;
|
|
12
|
+
limit?: number;
|
|
13
|
+
tier?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class UsageLimitError extends Error {
|
|
17
|
+
public details: UsageLimitDetails;
|
|
18
|
+
|
|
19
|
+
constructor(details: UsageLimitDetails) {
|
|
20
|
+
super(details.message);
|
|
21
|
+
this.name = 'UsageLimitError';
|
|
22
|
+
this.details = details;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get isCreditsError(): boolean {
|
|
26
|
+
return this.details.type === 'credits';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get isRateLimitError(): boolean {
|
|
30
|
+
return this.details.type === 'rate_limit';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get isModelAccessError(): boolean {
|
|
34
|
+
return this.details.type === 'model_access';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get shouldShowUpgrade(): boolean {
|
|
38
|
+
return (
|
|
39
|
+
this.details.suggestedAction === 'upgrade' ||
|
|
40
|
+
this.details.type === 'credits' ||
|
|
41
|
+
this.details.type === 'model_access'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|