@gedatou/cadenza-ai 0.7.1
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/README.md +71 -0
- package/dist/anthropic-DdrANeO1.mjs +236 -0
- package/dist/bedrock-534H9yfk.mjs +75 -0
- package/dist/catalog-CvvS2JaW.mjs +32 -0
- package/dist/deepseek-C1b74T2F.mjs +55 -0
- package/dist/gemini-DcJ5Kr3h.mjs +229 -0
- package/dist/grok-CffQuBd6.mjs +77 -0
- package/dist/groq-Ba6TTYLp.mjs +98 -0
- package/dist/index.d.mts +1056 -0
- package/dist/index.mjs +2809 -0
- package/dist/llmgateway-Pt5CS6aQ.mjs +211 -0
- package/dist/mistral-BGMwwbU9.mjs +107 -0
- package/dist/mock/index.d.mts +168 -0
- package/dist/mock/index.mjs +467 -0
- package/dist/ollama-BBJs-3mK.mjs +80 -0
- package/dist/openai--FIeahIH.mjs +153 -0
- package/dist/openrouter-q-lx-2Cb.mjs +167 -0
- package/dist/preset-CEyIPxfS.mjs +157 -0
- package/dist/preset-DRT0T14q.d.mts +19 -0
- package/dist/providers/anthropic.d.mts +6 -0
- package/dist/providers/anthropic.mjs +11 -0
- package/dist/providers/bedrock.d.mts +6 -0
- package/dist/providers/bedrock.mjs +11 -0
- package/dist/providers/byteplus.d.mts +6 -0
- package/dist/providers/byteplus.mjs +21 -0
- package/dist/providers/deepseek.d.mts +35 -0
- package/dist/providers/deepseek.mjs +283 -0
- package/dist/providers/gemini.d.mts +6 -0
- package/dist/providers/gemini.mjs +11 -0
- package/dist/providers/grok.d.mts +6 -0
- package/dist/providers/grok.mjs +11 -0
- package/dist/providers/groq.d.mts +6 -0
- package/dist/providers/groq.mjs +11 -0
- package/dist/providers/llmgateway.d.mts +6 -0
- package/dist/providers/llmgateway.mjs +11 -0
- package/dist/providers/mistral.d.mts +6 -0
- package/dist/providers/mistral.mjs +11 -0
- package/dist/providers/ollama.d.mts +9 -0
- package/dist/providers/ollama.mjs +28 -0
- package/dist/providers/openai-compatible.d.mts +32 -0
- package/dist/providers/openai-compatible.mjs +45 -0
- package/dist/providers/openai.d.mts +6 -0
- package/dist/providers/openai.mjs +11 -0
- package/dist/providers/openrouter.d.mts +6 -0
- package/dist/providers/openrouter.mjs +11 -0
- package/dist/providers/vercel-gateway.d.mts +6 -0
- package/dist/providers/vercel-gateway.mjs +11 -0
- package/dist/providers/vertex.d.mts +6 -0
- package/dist/providers/vertex.mjs +11 -0
- package/dist/server/index.d.mts +173 -0
- package/dist/server/index.mjs +440 -0
- package/dist/thinking-CH2uL1dr.mjs +26 -0
- package/dist/types-56Hn7Qq2.d.mts +46 -0
- package/dist/vercel-gateway-DW3Z0Os1.mjs +109 -0
- package/dist/vertex-hg4Dt54O.mjs +20 -0
- package/package.json +114 -0
- package/styles.css +3 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2809 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { n as modelRef, r as parseModelRef, t as createCatalog } from "./catalog-CvvS2JaW.mjs";
|
|
3
|
+
import { t as anthropic } from "./anthropic-DdrANeO1.mjs";
|
|
4
|
+
import { t as bedrock } from "./bedrock-534H9yfk.mjs";
|
|
5
|
+
import { t as deepseek } from "./deepseek-C1b74T2F.mjs";
|
|
6
|
+
import { t as gemini } from "./gemini-DcJ5Kr3h.mjs";
|
|
7
|
+
import { t as grok } from "./grok-CffQuBd6.mjs";
|
|
8
|
+
import { t as groq } from "./groq-Ba6TTYLp.mjs";
|
|
9
|
+
import { t as llmgateway } from "./llmgateway-Pt5CS6aQ.mjs";
|
|
10
|
+
import { t as mistral } from "./mistral-BGMwwbU9.mjs";
|
|
11
|
+
import { t as ollama } from "./ollama-BBJs-3mK.mjs";
|
|
12
|
+
import { t as openai } from "./openai--FIeahIH.mjs";
|
|
13
|
+
import { t as openrouter } from "./openrouter-q-lx-2Cb.mjs";
|
|
14
|
+
import { t as vercelGateway } from "./vercel-gateway-DW3Z0Os1.mjs";
|
|
15
|
+
import { t as vertex } from "./vertex-hg4Dt54O.mjs";
|
|
16
|
+
import { n as clampThinkingLevel, r as supportedThinkingLevels, t as THINKING_LEVELS } from "./thinking-CH2uL1dr.mjs";
|
|
17
|
+
import { Children, createContext, isValidElement, memo, use, useCallback, useEffect, useEffectEvent, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
18
|
+
import { defaultByokStorage, defaultByokStorage as defaultByokStorage$1, defineByok, defineByok as defineByok$1, isPasskeyStorageSupported, memoryStorage, memoryStorage as memoryStorage$1, passkeyStorage } from "@tanstack/ai-client/byok";
|
|
19
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
20
|
+
import { EventType, EventType as EventType$1, fromSpecTokenUsage, fromSpecTokenUsage as fromSpecTokenUsage$1, generateMessageId, parsePartialJSON, parsePartialJSON as parsePartialJSON$1, toolDefinition } from "@tanstack/ai/client";
|
|
21
|
+
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentGroup, AttachmentMedia, AttachmentTitle, Bubble, BubbleContent, Button, Collapsible, CollapsiblePanel, CollapsibleTrigger, Combobox, ComboboxCollection, ComboboxEmpty, ComboboxGroup, ComboboxGroupLabel, ComboboxInput, ComboboxItem, ComboboxList, ComboboxPopup, ComboboxTrigger, ComboboxValue, Dialog, DialogBody, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogPopup, DialogTitle, Empty, Field, FieldError, FieldLabel, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea, Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemTitle, Marker, MarkerContent, Message, MessageContent, MessageFooter, MessageScroller, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, Progress, ScrollArea, Select, SelectItem, SelectPopup, SelectTrigger, SelectValue, Spinner, Toggle, Tooltip, TooltipPopup, TooltipTrigger, cn, createChangeEventDetails, createGenericEventDetails, dataAttr } from "@gedatou/cadenza-ui";
|
|
22
|
+
import { useControllableState } from "@gedatou/cadenza-utils";
|
|
23
|
+
import { IconArchive, IconArrowUp, IconBrain, IconCheck, IconClock, IconFileText, IconKey, IconMicrophone, IconPaperclip, IconPencil, IconPhoto, IconPlayerStop, IconServer, IconTrash, IconWorld, IconX } from "@tabler/icons-react";
|
|
24
|
+
import { useAudioRecorder, useByok } from "@tanstack/ai-react";
|
|
25
|
+
import { cjk } from "@streamdown/cjk";
|
|
26
|
+
import { code } from "@streamdown/code";
|
|
27
|
+
import { math } from "@streamdown/math";
|
|
28
|
+
import { Streamdown } from "streamdown";
|
|
29
|
+
export * from "@tanstack/ai-react";
|
|
30
|
+
//#region src/catalog/cost.ts
|
|
31
|
+
/**
|
|
32
|
+
* USD;`cost` 缺失返回 undefined。
|
|
33
|
+
*
|
|
34
|
+
* 提示 token 分三档计价:命中缓存的按 `cacheRead`,写入缓存的按 `cacheWrite`
|
|
35
|
+
* (Anthropic 是 input 的 1.25×),其余按 `input`;两个缓存价缺省都回落到 `input`。
|
|
36
|
+
* 两个明细都当作含在 `promptTokens` 里扣除——`usage.ts` 的 `addTokenUsage` 正是这样
|
|
37
|
+
* 累加它们的,而 `Math.max(…, 0)` 兜住厂商把它们单列在外的情形。
|
|
38
|
+
*/
|
|
39
|
+
function estimateCost(model, usage) {
|
|
40
|
+
const cost = model.cost;
|
|
41
|
+
if (!cost) return void 0;
|
|
42
|
+
const cached = usage.promptTokensDetails?.cachedTokens ?? 0;
|
|
43
|
+
const written = usage.promptTokensDetails?.cacheWriteTokens ?? 0;
|
|
44
|
+
const fresh = Math.max(usage.promptTokens - cached - written, 0);
|
|
45
|
+
const cacheRead = cost.cacheRead ?? cost.input;
|
|
46
|
+
const cacheWrite = cost.cacheWrite ?? cost.input;
|
|
47
|
+
return (fresh * cost.input + cached * cacheRead + written * cacheWrite + usage.completionTokens * cost.output) / 1e6;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/catalog/usage-metrics.ts
|
|
51
|
+
const MODALITIES = [
|
|
52
|
+
"text",
|
|
53
|
+
"image",
|
|
54
|
+
"audio",
|
|
55
|
+
"video",
|
|
56
|
+
"document"
|
|
57
|
+
];
|
|
58
|
+
/** 分母为零或缺失时返回 undefined,不返回 0。 */
|
|
59
|
+
function ratio(part, whole) {
|
|
60
|
+
if (part === void 0 || whole === void 0 || whole <= 0) return void 0;
|
|
61
|
+
return part / whole;
|
|
62
|
+
}
|
|
63
|
+
function slices(details, whole) {
|
|
64
|
+
if (!details || whole <= 0) return [];
|
|
65
|
+
const by = details;
|
|
66
|
+
return MODALITIES.map((kind) => ({
|
|
67
|
+
kind,
|
|
68
|
+
tokens: by[`${kind}Tokens`] ?? 0,
|
|
69
|
+
share: (by[`${kind}Tokens`] ?? 0) / whole
|
|
70
|
+
})).filter((s) => s.tokens > 0).sort((a, b) => b.tokens - a.tokens);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `TokenUsage` 里读得出来的比例,一次算齐。
|
|
74
|
+
*
|
|
75
|
+
* 纯函数,和 `estimateCost` 并排住在目录层:这些数字两端都用得上,且与 React 无关。
|
|
76
|
+
* 输入可以是 `useUsageTracker` 的任意一个粒度——`total` / `lastRun` / `byMessage` 的某一条。
|
|
77
|
+
*
|
|
78
|
+
* `model` 只有两项指标需要(`contextRatio` 要 `contextWindow`,`cacheSavings` 要价格),
|
|
79
|
+
* 不给就是那两项为 `undefined`,其余照算。
|
|
80
|
+
*/
|
|
81
|
+
function usageMetrics(usage, model) {
|
|
82
|
+
const prompt = usage.promptTokensDetails;
|
|
83
|
+
const completion = usage.completionTokensDetails;
|
|
84
|
+
const cached = prompt?.cachedTokens;
|
|
85
|
+
const cost = model?.cost;
|
|
86
|
+
const savings = cached !== void 0 && cost?.cacheRead !== void 0 ? cached * (cost.input - cost.cacheRead) / 1e6 : void 0;
|
|
87
|
+
return {
|
|
88
|
+
outputRatio: ratio(usage.completionTokens, usage.promptTokens),
|
|
89
|
+
cacheHitRate: ratio(cached, usage.promptTokens),
|
|
90
|
+
cacheWriteRate: ratio(prompt?.cacheWriteTokens, usage.promptTokens),
|
|
91
|
+
reasoningShare: ratio(completion?.reasoningTokens, usage.completionTokens),
|
|
92
|
+
contextRatio: ratio(usage.promptTokens, model?.contextWindow),
|
|
93
|
+
cacheSavings: savings,
|
|
94
|
+
promptModalities: slices(prompt, usage.promptTokens),
|
|
95
|
+
completionModalities: slices(completion, usage.completionTokens)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/catalog/index.ts
|
|
100
|
+
const providers = {
|
|
101
|
+
openai,
|
|
102
|
+
anthropic,
|
|
103
|
+
gemini,
|
|
104
|
+
grok,
|
|
105
|
+
groq,
|
|
106
|
+
mistral,
|
|
107
|
+
openrouter,
|
|
108
|
+
vercelGateway,
|
|
109
|
+
llmgateway,
|
|
110
|
+
bedrock,
|
|
111
|
+
vertex,
|
|
112
|
+
ollama,
|
|
113
|
+
deepseek
|
|
114
|
+
};
|
|
115
|
+
const defaultCatalog = createCatalog(Object.values(providers));
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/runtime/attachments.ts
|
|
118
|
+
/** 3 MiB: base64 in a JSON body, comfortably under common function payload limits. */
|
|
119
|
+
const DEFAULT_MAX_ATTACHMENT_BYTES = 3145728;
|
|
120
|
+
const DOCUMENT_MIMES = /* @__PURE__ */ new Set([
|
|
121
|
+
"application/pdf",
|
|
122
|
+
"text/plain",
|
|
123
|
+
"text/markdown"
|
|
124
|
+
]);
|
|
125
|
+
const EXTENSION_MIME = {
|
|
126
|
+
pdf: "application/pdf",
|
|
127
|
+
md: "text/markdown",
|
|
128
|
+
txt: "text/plain"
|
|
129
|
+
};
|
|
130
|
+
function attachmentKindOf(mimeType) {
|
|
131
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
132
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
133
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
134
|
+
if (DOCUMENT_MIMES.has(mimeType)) return "document";
|
|
135
|
+
}
|
|
136
|
+
function mimeOf(file) {
|
|
137
|
+
if (file.type !== "") return file.type;
|
|
138
|
+
const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
|
|
139
|
+
return EXTENSION_MIME[ext] ?? "";
|
|
140
|
+
}
|
|
141
|
+
function toBase64(file) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const reader = new FileReader();
|
|
144
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("cadenza-ai: could not read the file"));
|
|
145
|
+
reader.onload = () => resolve(String(reader.result).split(",")[1] ?? "");
|
|
146
|
+
reader.readAsDataURL(file);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Inline a file as a `data` content part; rejects unsupported types and oversize files. */
|
|
150
|
+
async function fileToContentPart(file, options = {}) {
|
|
151
|
+
const maxBytes = options.maxBytes ?? 3145728;
|
|
152
|
+
if (file.size > maxBytes) throw new Error(`cadenza-ai: "${file.name}" exceeds the ${maxBytes} byte attachment limit.`);
|
|
153
|
+
const mimeType = mimeOf(file);
|
|
154
|
+
const kind = attachmentKindOf(mimeType);
|
|
155
|
+
if (!kind) throw new Error(`cadenza-ai: "${file.name}" (${mimeType || "unknown type"}) is not an accepted attachment.`);
|
|
156
|
+
return {
|
|
157
|
+
type: kind,
|
|
158
|
+
source: {
|
|
159
|
+
type: "data",
|
|
160
|
+
value: await toBase64(file),
|
|
161
|
+
mimeType
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const ACCEPT = {
|
|
166
|
+
image: ["image/*"],
|
|
167
|
+
audio: ["audio/*"],
|
|
168
|
+
video: ["video/*"],
|
|
169
|
+
document: [
|
|
170
|
+
"application/pdf",
|
|
171
|
+
"text/plain",
|
|
172
|
+
"text/markdown"
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
let counter = 0;
|
|
176
|
+
function nextId() {
|
|
177
|
+
counter += 1;
|
|
178
|
+
return `attachment-${Date.now().toString(36)}-${counter}`;
|
|
179
|
+
}
|
|
180
|
+
function revoke(item) {
|
|
181
|
+
if (item.previewUrl !== void 0) URL.revokeObjectURL(item.previewUrl);
|
|
182
|
+
}
|
|
183
|
+
/** The composer's pending attachments, before they become parts of a sent message. */
|
|
184
|
+
const ALL_KINDS = [
|
|
185
|
+
"image",
|
|
186
|
+
"audio",
|
|
187
|
+
"video",
|
|
188
|
+
"document"
|
|
189
|
+
];
|
|
190
|
+
function useAttachmentDraft(options = {}) {
|
|
191
|
+
const accepted = options.accept;
|
|
192
|
+
const kinds = useMemo(() => accepted ?? ALL_KINDS, [accepted]);
|
|
193
|
+
const maxBytes = options.maxBytes ?? 3145728;
|
|
194
|
+
const [items, setItems] = useState([]);
|
|
195
|
+
const itemsRef = useRef(items);
|
|
196
|
+
itemsRef.current = items;
|
|
197
|
+
const add = useCallback((incoming) => {
|
|
198
|
+
const next = incoming.map((entry) => {
|
|
199
|
+
if (entry instanceof File) {
|
|
200
|
+
const mimeType = mimeOf(entry);
|
|
201
|
+
const kind = attachmentKindOf(mimeType);
|
|
202
|
+
const base = {
|
|
203
|
+
id: nextId(),
|
|
204
|
+
name: entry.name,
|
|
205
|
+
mimeType,
|
|
206
|
+
size: entry.size,
|
|
207
|
+
file: entry
|
|
208
|
+
};
|
|
209
|
+
if (!kind || !kinds.includes(kind)) return {
|
|
210
|
+
...base,
|
|
211
|
+
kind: kind ?? "document",
|
|
212
|
+
state: "error",
|
|
213
|
+
error: "unsupported"
|
|
214
|
+
};
|
|
215
|
+
if (entry.size > maxBytes) return {
|
|
216
|
+
...base,
|
|
217
|
+
kind,
|
|
218
|
+
state: "error",
|
|
219
|
+
error: "too-large"
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
...base,
|
|
223
|
+
kind,
|
|
224
|
+
state: "idle",
|
|
225
|
+
previewUrl: kind === "image" ? URL.createObjectURL(entry) : void 0
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const kind = entry.type === "text" ? void 0 : entry.type;
|
|
229
|
+
const ok = kind !== void 0 && kinds.includes(kind);
|
|
230
|
+
const mimeType = entry.type === "text" ? "text/plain" : entry.source.mimeType ?? "";
|
|
231
|
+
return {
|
|
232
|
+
id: nextId(),
|
|
233
|
+
kind: kind ?? "document",
|
|
234
|
+
state: ok ? "done" : "error",
|
|
235
|
+
name: kind ?? "text",
|
|
236
|
+
mimeType,
|
|
237
|
+
size: 0,
|
|
238
|
+
part: entry,
|
|
239
|
+
...ok ? {} : { error: "unsupported" }
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
setItems((current) => [...current, ...next]);
|
|
243
|
+
}, [kinds, maxBytes]);
|
|
244
|
+
const remove = useCallback((id) => {
|
|
245
|
+
itemsRef.current.filter((i) => i.id === id).forEach(revoke);
|
|
246
|
+
setItems((current) => current.filter((i) => i.id !== id));
|
|
247
|
+
}, []);
|
|
248
|
+
const clear = useCallback(() => {
|
|
249
|
+
itemsRef.current.forEach(revoke);
|
|
250
|
+
setItems([]);
|
|
251
|
+
}, []);
|
|
252
|
+
useEffect(() => () => itemsRef.current.forEach(revoke), []);
|
|
253
|
+
const toParts = useCallback(async () => {
|
|
254
|
+
return Promise.all(itemsRef.current.filter((i) => i.state !== "error").map((i) => i.part ? Promise.resolve(i.part) : fileToContentPart(i.file, { maxBytes })));
|
|
255
|
+
}, [maxBytes]);
|
|
256
|
+
const accept = useMemo(() => kinds.flatMap((k) => ACCEPT[k]).join(","), [kinds]);
|
|
257
|
+
return useMemo(() => ({
|
|
258
|
+
items,
|
|
259
|
+
add,
|
|
260
|
+
remove,
|
|
261
|
+
clear,
|
|
262
|
+
toParts,
|
|
263
|
+
accept
|
|
264
|
+
}), [
|
|
265
|
+
items,
|
|
266
|
+
add,
|
|
267
|
+
remove,
|
|
268
|
+
clear,
|
|
269
|
+
toParts,
|
|
270
|
+
accept
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/runtime/byok.ts
|
|
275
|
+
function keylessCoverage(catalog) {
|
|
276
|
+
return Object.fromEntries(catalog.providers.filter((p) => !p.keyRequired).map((p) => [p.id, true]));
|
|
277
|
+
}
|
|
278
|
+
const BYOK_ERRORS = /* @__PURE__ */ new Set([
|
|
279
|
+
"ByokBlockedError",
|
|
280
|
+
"ByokMissingError",
|
|
281
|
+
"ByokUnresolvedProviderError"
|
|
282
|
+
]);
|
|
283
|
+
/** True for the errors a send throws when the client could not attach a key — the dialog is already asking; the send can run again once the key is in. */
|
|
284
|
+
function isByokError(error) {
|
|
285
|
+
return error instanceof Error && BYOK_ERRORS.has(error.name);
|
|
286
|
+
}
|
|
287
|
+
/** A `ByokClient` with the house defaults, ready for `useChat({ byok })`. */
|
|
288
|
+
function createByok(options = {}) {
|
|
289
|
+
const byok = defineByok$1({ storage: options.storage ?? (options.persistent ? defaultByokStorage$1() : memoryStorage$1()) });
|
|
290
|
+
if (options.catalog) byok.setServerCoverage(keylessCoverage(options.catalog));
|
|
291
|
+
return byok;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Fetch `/api/ai/catalog` once and merge its `coverage` into the client, so
|
|
295
|
+
* `prepare()` stops asking for keys the server already holds.
|
|
296
|
+
*/
|
|
297
|
+
function useServerCoverage(byok, url = "/api/ai/catalog") {
|
|
298
|
+
const [state, setState] = useState({
|
|
299
|
+
coverage: void 0,
|
|
300
|
+
providers: void 0,
|
|
301
|
+
error: void 0
|
|
302
|
+
});
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
let cancelled = false;
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
fetch(url, { signal: controller.signal }).then(async (res) => {
|
|
307
|
+
if (!res.ok) throw new Error(`cadenza-ai: ${url} answered ${res.status}`);
|
|
308
|
+
return res.json();
|
|
309
|
+
}).then((json) => {
|
|
310
|
+
if (cancelled) return;
|
|
311
|
+
byok.setServerCoverage(json.coverage);
|
|
312
|
+
setState({
|
|
313
|
+
coverage: json.coverage,
|
|
314
|
+
providers: json.providers,
|
|
315
|
+
error: void 0
|
|
316
|
+
});
|
|
317
|
+
}).catch((error) => {
|
|
318
|
+
if (!cancelled) setState((s) => ({
|
|
319
|
+
...s,
|
|
320
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
321
|
+
}));
|
|
322
|
+
});
|
|
323
|
+
return () => {
|
|
324
|
+
cancelled = true;
|
|
325
|
+
controller.abort();
|
|
326
|
+
};
|
|
327
|
+
}, [byok, url]);
|
|
328
|
+
return state;
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/runtime/messages.ts
|
|
332
|
+
/** The message's text parts, joined by blank lines; tool calls and media are skipped. */
|
|
333
|
+
function messageText(message) {
|
|
334
|
+
return message.parts.filter((p) => p.type === "text").map((p) => p.content).join("\n\n");
|
|
335
|
+
}
|
|
336
|
+
/** A plain Markdown transcript, for copy / download. */
|
|
337
|
+
function messagesToMarkdown(messages, options = {}) {
|
|
338
|
+
const out = [];
|
|
339
|
+
if (options.title !== void 0) out.push(`# ${options.title}`, "");
|
|
340
|
+
for (const message of messages) {
|
|
341
|
+
out.push(`**${message.role === "user" ? "User" : "Assistant"}**`, "");
|
|
342
|
+
for (const part of message.parts) if (part.type === "text") out.push(part.content, "");
|
|
343
|
+
else if (part.type === "thinking" && options.includeThinking) out.push(...part.content.split("\n").map((line) => `> ${line}`), "");
|
|
344
|
+
}
|
|
345
|
+
return `${out.join("\n").trimEnd()}\n`;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Linear edit-and-resend (spec Q2): stop any run, drop the edited user message
|
|
349
|
+
* and everything after it, then send the new content in its place.
|
|
350
|
+
*/
|
|
351
|
+
async function editAndResend(chat, messageId, content) {
|
|
352
|
+
const index = chat.messages.findIndex((m) => m.id === messageId);
|
|
353
|
+
if (index === -1 || chat.messages[index]?.role !== "user") throw new Error("cadenza-ai: editAndResend needs the id of a user message.");
|
|
354
|
+
if (chat.status !== "ready") chat.stop();
|
|
355
|
+
chat.setMessages(chat.messages.slice(0, index));
|
|
356
|
+
await chat.sendMessage(content);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* A thinking part is done when the run is no longer streaming, when the
|
|
360
|
+
* provider signed it, or when a later text / tool-call part has started.
|
|
361
|
+
*/
|
|
362
|
+
function isThinkingComplete(message, partIndex, status) {
|
|
363
|
+
if (status !== "streaming") return true;
|
|
364
|
+
if (message.parts[partIndex]?.signature !== void 0) return true;
|
|
365
|
+
return message.parts.slice(partIndex + 1).some((p) => p.type === "text" || p.type === "tool-call");
|
|
366
|
+
}
|
|
367
|
+
function asSource(value) {
|
|
368
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
369
|
+
const { url, title, snippet } = value;
|
|
370
|
+
if (typeof url !== "string") return void 0;
|
|
371
|
+
return {
|
|
372
|
+
url,
|
|
373
|
+
...typeof title === "string" ? { title } : {},
|
|
374
|
+
...typeof snippet === "string" ? { snippet } : {}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function collect(value, into) {
|
|
378
|
+
if (Array.isArray(value)) for (const item of value) {
|
|
379
|
+
const source = asSource(item);
|
|
380
|
+
if (source) into.push(source);
|
|
381
|
+
}
|
|
382
|
+
else if (typeof value === "object" && value !== null && "results" in value) collect(value.results, into);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Citations from provider-executed web search: an explicit `metadata.sources`
|
|
386
|
+
* list, or the result payload of a search tool the provider ran itself.
|
|
387
|
+
*/
|
|
388
|
+
function sourcesOf(message) {
|
|
389
|
+
const found = [];
|
|
390
|
+
for (const part of message.parts) {
|
|
391
|
+
if (part.type !== "tool-call") continue;
|
|
392
|
+
const metadata = part.metadata ?? {};
|
|
393
|
+
collect(metadata.sources, found);
|
|
394
|
+
if (metadata.providerExecuted === true && /search/i.test(part.name)) collect(part.output, found);
|
|
395
|
+
}
|
|
396
|
+
const seen = /* @__PURE__ */ new Set();
|
|
397
|
+
return found.filter((s) => seen.has(s.url) ? false : (seen.add(s.url), true));
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/runtime/renderers.tsx
|
|
401
|
+
const DEFAULT_PART_LABELS = {
|
|
402
|
+
thinking: "Thinking…",
|
|
403
|
+
thought: "Thought for ",
|
|
404
|
+
toolPending: "Preparing",
|
|
405
|
+
toolRunning: "Running",
|
|
406
|
+
toolApprovalRequested: "Needs approval",
|
|
407
|
+
toolApproved: "Approved",
|
|
408
|
+
toolDenied: "Denied",
|
|
409
|
+
toolDone: "Done",
|
|
410
|
+
toolFailed: "Failed",
|
|
411
|
+
toolGroup: (count) => `Ran ${count} tools`,
|
|
412
|
+
approve: "Approve",
|
|
413
|
+
deny: "Deny",
|
|
414
|
+
sources: (count) => `${count} sources`
|
|
415
|
+
};
|
|
416
|
+
/** Identity with a type check, so a registry can be declared outside a component. */
|
|
417
|
+
function definePartRenderers(renderers) {
|
|
418
|
+
return renderers;
|
|
419
|
+
}
|
|
420
|
+
const PartRenderersContext = createContext({
|
|
421
|
+
renderers: {},
|
|
422
|
+
labels: DEFAULT_PART_LABELS
|
|
423
|
+
});
|
|
424
|
+
if (process.env.NODE_ENV !== "production") PartRenderersContext.displayName = "PartRenderersContext";
|
|
425
|
+
function PartRenderersProvider({ renderers, labels, children }) {
|
|
426
|
+
const parent = use(PartRenderersContext);
|
|
427
|
+
const value = useMemo(() => ({
|
|
428
|
+
renderers: renderers ? {
|
|
429
|
+
...parent.renderers,
|
|
430
|
+
...renderers,
|
|
431
|
+
...parent.renderers.toolCall ?? renderers.toolCall ? { toolCall: {
|
|
432
|
+
...parent.renderers.toolCall,
|
|
433
|
+
...renderers.toolCall
|
|
434
|
+
} } : {}
|
|
435
|
+
} : parent.renderers,
|
|
436
|
+
labels: labels ? {
|
|
437
|
+
...parent.labels,
|
|
438
|
+
...labels
|
|
439
|
+
} : parent.labels
|
|
440
|
+
}), [
|
|
441
|
+
renderers,
|
|
442
|
+
labels,
|
|
443
|
+
parent
|
|
444
|
+
]);
|
|
445
|
+
return /* @__PURE__ */ jsx(PartRenderersContext, {
|
|
446
|
+
value,
|
|
447
|
+
children
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function usePartRenderers() {
|
|
451
|
+
return use(PartRenderersContext);
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/runtime/stored-state.ts
|
|
455
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
456
|
+
const cache = /* @__PURE__ */ new Map();
|
|
457
|
+
function storage() {
|
|
458
|
+
try {
|
|
459
|
+
return typeof window === "undefined" ? void 0 : window.localStorage ?? void 0;
|
|
460
|
+
} catch {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function snapshot(key, initial) {
|
|
465
|
+
const store = storage();
|
|
466
|
+
const hit = cache.get(key);
|
|
467
|
+
if (!store || hit?.detached) return hit ? hit.value : seed(key, null, initial);
|
|
468
|
+
const raw = store.getItem(key);
|
|
469
|
+
if (hit && hit.raw === raw) return hit.value;
|
|
470
|
+
if (raw === null) return seed(key, null, initial);
|
|
471
|
+
try {
|
|
472
|
+
return seed(key, raw, JSON.parse(raw));
|
|
473
|
+
} catch {
|
|
474
|
+
return seed(key, raw, initial);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function seed(key, raw, value) {
|
|
478
|
+
cache.set(key, {
|
|
479
|
+
raw,
|
|
480
|
+
value
|
|
481
|
+
});
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
484
|
+
function emit(key) {
|
|
485
|
+
listeners.get(key)?.forEach((l) => l());
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* `useState` whose value round-trips through `localStorage[key]` and stays in
|
|
489
|
+
* sync across hooks and tabs. Falls back to `initial` on the server and to an
|
|
490
|
+
* in-memory copy where storage is unavailable; writes never throw.
|
|
491
|
+
*/
|
|
492
|
+
function useStoredState(key, initial) {
|
|
493
|
+
const subscribe = useCallback((listener) => {
|
|
494
|
+
let set = listeners.get(key);
|
|
495
|
+
if (!set) {
|
|
496
|
+
set = /* @__PURE__ */ new Set();
|
|
497
|
+
listeners.set(key, set);
|
|
498
|
+
}
|
|
499
|
+
set.add(listener);
|
|
500
|
+
const onStorage = (event) => {
|
|
501
|
+
if (event.key === key) listener();
|
|
502
|
+
};
|
|
503
|
+
window.addEventListener("storage", onStorage);
|
|
504
|
+
return () => {
|
|
505
|
+
set.delete(listener);
|
|
506
|
+
window.removeEventListener("storage", onStorage);
|
|
507
|
+
};
|
|
508
|
+
}, [key]);
|
|
509
|
+
return [useSyncExternalStore(subscribe, () => snapshot(key, initial), () => initial), useCallback((next) => {
|
|
510
|
+
const raw = JSON.stringify(next);
|
|
511
|
+
let stored = null;
|
|
512
|
+
let detached = false;
|
|
513
|
+
try {
|
|
514
|
+
storage()?.setItem(key, raw);
|
|
515
|
+
stored = storage()?.getItem(key) ?? null;
|
|
516
|
+
} catch {
|
|
517
|
+
detached = true;
|
|
518
|
+
}
|
|
519
|
+
cache.set(key, {
|
|
520
|
+
raw: stored,
|
|
521
|
+
value: next,
|
|
522
|
+
detached
|
|
523
|
+
});
|
|
524
|
+
emit(key);
|
|
525
|
+
}, [key])];
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/runtime/selection.ts
|
|
529
|
+
function firstSelection(catalog) {
|
|
530
|
+
const model = catalog.models[0];
|
|
531
|
+
return {
|
|
532
|
+
provider: model?.provider ?? "",
|
|
533
|
+
model: model?.id ?? "",
|
|
534
|
+
thinking: "off",
|
|
535
|
+
search: false
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/** 只有模型声明了 `search` 才可能是 on——同 `clampThinkingLevel` 的道理:能力由目录说了算,不由存储说了算。 */
|
|
539
|
+
function clampSearch(model, on) {
|
|
540
|
+
return on === true && model?.search === true;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* 存储里那份,修成控件能用的形状。两条规矩,来源不同:
|
|
544
|
+
*
|
|
545
|
+
* 1. **字段必须存在、类型必须对。** 控件把 `undefined` 当受控值会出事——
|
|
546
|
+
* `useControllableState` 在首帧按 `value !== undefined` 锁死受控性且不再改判,
|
|
547
|
+
* 于是 `SearchToggle` / `ThinkingToggle` 整场会话都改吃自己的内部状态,第一次
|
|
548
|
+
* 按下还会触发受控性告警。这一条无条件执行。
|
|
549
|
+
* 2. **按能力裁剪,只在目录认识这个模型时做。** 「目录里没这条」和「这个模型没这个
|
|
550
|
+
* 能力」是两回事:目录改名或删掉一个条目,就会让所有 `getModel` 落空,把它当成
|
|
551
|
+
* 「什么都不支持」会静默擦掉用户的选择。
|
|
552
|
+
*
|
|
553
|
+
* 裁剪只作用于读出来的这一份。写回存储的永远是原值展开——目录暂时不认识某个模型,
|
|
554
|
+
* 不该成为把它的档位和搜索开关抹平的理由,存储里那些我们不认识的键也一并留着。
|
|
555
|
+
*/
|
|
556
|
+
function normalize(model, stored) {
|
|
557
|
+
const base = {
|
|
558
|
+
provider: stored.provider ?? "",
|
|
559
|
+
model: stored.model ?? "",
|
|
560
|
+
thinking: stored.thinking ?? "off",
|
|
561
|
+
search: stored.search === true
|
|
562
|
+
};
|
|
563
|
+
if (model === void 0) return base;
|
|
564
|
+
return {
|
|
565
|
+
...base,
|
|
566
|
+
thinking: clampThinkingLevel(model, base.thinking),
|
|
567
|
+
search: clampSearch(model, base.search)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
/** The user's model + thinking choice, persisted, always valid for the chosen model. */
|
|
571
|
+
function useModelSelection(options = {}) {
|
|
572
|
+
const catalog = options.catalog ?? defaultCatalog;
|
|
573
|
+
const [stored, setStored] = useStoredState(options.key ?? "cadenza-ai:selection", options.initial ?? firstSelection(catalog));
|
|
574
|
+
const model = catalog.getModel(modelRef({
|
|
575
|
+
provider: stored.provider ?? "",
|
|
576
|
+
id: stored.model ?? ""
|
|
577
|
+
}));
|
|
578
|
+
const provider = catalog.getProvider(stored.provider ?? "");
|
|
579
|
+
const selection = useMemo(() => normalize(model, stored), [model, stored]);
|
|
580
|
+
return {
|
|
581
|
+
selection,
|
|
582
|
+
model,
|
|
583
|
+
provider,
|
|
584
|
+
setModel: useCallback((ref) => {
|
|
585
|
+
const { provider: nextProvider, id } = parseModelRef(ref);
|
|
586
|
+
const next = catalog.getModel(ref);
|
|
587
|
+
setStored({
|
|
588
|
+
...stored,
|
|
589
|
+
provider: nextProvider,
|
|
590
|
+
model: id,
|
|
591
|
+
thinking: clampThinkingLevel(next, stored.thinking ?? "off"),
|
|
592
|
+
search: clampSearch(next, stored.search)
|
|
593
|
+
});
|
|
594
|
+
}, [
|
|
595
|
+
catalog,
|
|
596
|
+
stored,
|
|
597
|
+
setStored
|
|
598
|
+
]),
|
|
599
|
+
setThinking: useCallback((level) => {
|
|
600
|
+
setStored({
|
|
601
|
+
...stored,
|
|
602
|
+
thinking: clampThinkingLevel(model, level)
|
|
603
|
+
});
|
|
604
|
+
}, [
|
|
605
|
+
model,
|
|
606
|
+
stored,
|
|
607
|
+
setStored
|
|
608
|
+
]),
|
|
609
|
+
setSearch: useCallback((on) => {
|
|
610
|
+
setStored({
|
|
611
|
+
...stored,
|
|
612
|
+
search: clampSearch(model, on)
|
|
613
|
+
});
|
|
614
|
+
}, [
|
|
615
|
+
model,
|
|
616
|
+
stored,
|
|
617
|
+
setStored
|
|
618
|
+
]),
|
|
619
|
+
forwardedProps: selection
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/runtime/threads.ts
|
|
624
|
+
function newThreadId() {
|
|
625
|
+
return `thread-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
626
|
+
}
|
|
627
|
+
function sortThreads(threads) {
|
|
628
|
+
return [...threads].sort((a, b) => Number(a.archived) - Number(b.archived) || b.updatedAt - a.updatedAt);
|
|
629
|
+
}
|
|
630
|
+
function createThreadIndex(options = {}) {
|
|
631
|
+
const key = options.key ?? "cadenza-ai:threads";
|
|
632
|
+
const local = (options.storage ?? "local") === "local" && typeof window !== "undefined";
|
|
633
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
634
|
+
const tombstones = /* @__PURE__ */ new Set();
|
|
635
|
+
let threads = load();
|
|
636
|
+
let sorted = sortThreads(threads);
|
|
637
|
+
function load() {
|
|
638
|
+
if (!local) return [];
|
|
639
|
+
try {
|
|
640
|
+
const raw = window.localStorage.getItem(key);
|
|
641
|
+
return raw === null ? [] : JSON.parse(raw);
|
|
642
|
+
} catch {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function commit(next) {
|
|
647
|
+
threads = next;
|
|
648
|
+
sorted = sortThreads(next);
|
|
649
|
+
if (local) try {
|
|
650
|
+
window.localStorage.setItem(key, JSON.stringify(next));
|
|
651
|
+
} catch {}
|
|
652
|
+
listeners.forEach((l) => l());
|
|
653
|
+
}
|
|
654
|
+
function get(id) {
|
|
655
|
+
return threads.find((t) => t.id === id);
|
|
656
|
+
}
|
|
657
|
+
function touch(id, patch) {
|
|
658
|
+
const now = Date.now();
|
|
659
|
+
const existing = get(id);
|
|
660
|
+
const next = existing ? {
|
|
661
|
+
...existing,
|
|
662
|
+
...patch,
|
|
663
|
+
updatedAt: patch.updatedAt ?? now
|
|
664
|
+
} : {
|
|
665
|
+
id,
|
|
666
|
+
title: "",
|
|
667
|
+
createdAt: now,
|
|
668
|
+
updatedAt: now,
|
|
669
|
+
messageCount: 0,
|
|
670
|
+
preview: "",
|
|
671
|
+
archived: false,
|
|
672
|
+
...patch
|
|
673
|
+
};
|
|
674
|
+
if (!existing && tombstones.has(id)) return next;
|
|
675
|
+
commit(existing ? threads.map((t) => t.id === id ? next : t) : [...threads, next]);
|
|
676
|
+
return next;
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
list: () => sorted,
|
|
680
|
+
get,
|
|
681
|
+
create: (init) => {
|
|
682
|
+
const id = init?.id ?? newThreadId();
|
|
683
|
+
tombstones.delete(id);
|
|
684
|
+
return touch(id, init ?? {});
|
|
685
|
+
},
|
|
686
|
+
touch,
|
|
687
|
+
rename: (id, title) => {
|
|
688
|
+
touch(id, { title });
|
|
689
|
+
},
|
|
690
|
+
archive: (id, archived) => {
|
|
691
|
+
touch(id, { archived });
|
|
692
|
+
},
|
|
693
|
+
remove: (id) => {
|
|
694
|
+
tombstones.add(id);
|
|
695
|
+
commit(threads.filter((t) => t.id !== id));
|
|
696
|
+
},
|
|
697
|
+
wasRemoved: (id) => tombstones.has(id),
|
|
698
|
+
subscribe: (listener) => {
|
|
699
|
+
listeners.add(listener);
|
|
700
|
+
const onStorage = (event) => {
|
|
701
|
+
if (local && event.key === key) {
|
|
702
|
+
threads = load();
|
|
703
|
+
sorted = sortThreads(threads);
|
|
704
|
+
listener();
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
if (local) window.addEventListener("storage", onStorage);
|
|
708
|
+
return () => {
|
|
709
|
+
listeners.delete(listener);
|
|
710
|
+
if (local) window.removeEventListener("storage", onStorage);
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const EMPTY = [];
|
|
716
|
+
function useThreadIndex(index) {
|
|
717
|
+
return useSyncExternalStore(index.subscribe, index.list, () => EMPTY);
|
|
718
|
+
}
|
|
719
|
+
function textOf(message) {
|
|
720
|
+
return message?.parts.map((p) => p.type === "text" ? p.content : "").join(" ").trim() ?? "";
|
|
721
|
+
}
|
|
722
|
+
/** Title from the first user message; truncated with an ellipsis past `max` characters. */
|
|
723
|
+
function threadTitleFrom(messages, max = 40) {
|
|
724
|
+
const text = textOf(messages.find((m) => m.role === "user")).replace(/\s+/g, " ");
|
|
725
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
726
|
+
}
|
|
727
|
+
function threadPersistence(index, base, options = {}) {
|
|
728
|
+
const derive = options.title === void 0 ? threadTitleFrom : options.title;
|
|
729
|
+
return {
|
|
730
|
+
getItem: (id) => index.wasRemoved(id) ? null : base.getItem(id),
|
|
731
|
+
setItem: async (id, state) => {
|
|
732
|
+
if (index.wasRemoved(id)) return;
|
|
733
|
+
await base.setItem(id, Array.isArray(state) ? { messages: state } : state);
|
|
734
|
+
const messages = Array.isArray(state) ? state : state.messages;
|
|
735
|
+
const last = messages.at(-1);
|
|
736
|
+
const own = index.get(id)?.title ?? "";
|
|
737
|
+
index.touch(id, {
|
|
738
|
+
messageCount: messages.length,
|
|
739
|
+
preview: textOf(last).slice(0, 200),
|
|
740
|
+
...own === "" && derive !== false ? { title: derive(messages) } : {}
|
|
741
|
+
});
|
|
742
|
+
},
|
|
743
|
+
removeItem: async (id) => {
|
|
744
|
+
await base.removeItem(id);
|
|
745
|
+
index.remove(id);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const DAY = 864e5;
|
|
750
|
+
/** Buckets by local calendar day of `updatedAt`; empty buckets are omitted. */
|
|
751
|
+
function groupThreadsByDay(threads, now = Date.now()) {
|
|
752
|
+
const today = new Date(now).toDateString();
|
|
753
|
+
const yesterday = (/* @__PURE__ */ new Date(now - DAY)).toDateString();
|
|
754
|
+
const buckets = {
|
|
755
|
+
today: [],
|
|
756
|
+
yesterday: [],
|
|
757
|
+
earlier: []
|
|
758
|
+
};
|
|
759
|
+
for (const t of threads) {
|
|
760
|
+
const day = new Date(t.updatedAt).toDateString();
|
|
761
|
+
buckets[day === today ? "today" : day === yesterday ? "yesterday" : "earlier"].push(t);
|
|
762
|
+
}
|
|
763
|
+
return [
|
|
764
|
+
"today",
|
|
765
|
+
"yesterday",
|
|
766
|
+
"earlier"
|
|
767
|
+
].filter((label) => buckets[label].length > 0).map((label) => ({
|
|
768
|
+
label,
|
|
769
|
+
threads: buckets[label]
|
|
770
|
+
}));
|
|
771
|
+
}
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/runtime/usage.ts
|
|
774
|
+
function add(a, b) {
|
|
775
|
+
return a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Sum the keys both sides actually carry, rather than a hand-written list.
|
|
779
|
+
*
|
|
780
|
+
* Naming the fields dropped every other one: `PromptTokensDetails` also holds
|
|
781
|
+
* `audioTokens` / `imageTokens` / `videoTokens` / `textTokens` /
|
|
782
|
+
* `documentTokens`, and a two-run sum silently threw the modality breakdown
|
|
783
|
+
* away — present on a single run, gone the moment a second one landed.
|
|
784
|
+
*/
|
|
785
|
+
function addDetails(a, b) {
|
|
786
|
+
if (a === void 0 && b === void 0) return void 0;
|
|
787
|
+
const left = a;
|
|
788
|
+
const right = b;
|
|
789
|
+
const out = {};
|
|
790
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})])) out[key] = add(left?.[key], right?.[key]);
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
/** Field-wise sum, including the nested detail breakdowns and the provider's own cost. */
|
|
794
|
+
function addTokenUsage(a, b) {
|
|
795
|
+
if (!a) return b;
|
|
796
|
+
const promptTokensDetails = addDetails(a.promptTokensDetails, b.promptTokensDetails);
|
|
797
|
+
const completionTokensDetails = addDetails(a.completionTokensDetails, b.completionTokensDetails);
|
|
798
|
+
const cost = add(a.cost, b.cost);
|
|
799
|
+
return {
|
|
800
|
+
promptTokens: a.promptTokens + b.promptTokens,
|
|
801
|
+
completionTokens: a.completionTokens + b.completionTokens,
|
|
802
|
+
totalTokens: a.totalTokens + b.totalTokens,
|
|
803
|
+
...cost !== void 0 ? { cost } : {},
|
|
804
|
+
...promptTokensDetails ? { promptTokensDetails } : {},
|
|
805
|
+
...completionTokensDetails ? { completionTokensDetails } : {}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const ZERO = {
|
|
809
|
+
promptTokens: 0,
|
|
810
|
+
completionTokens: 0,
|
|
811
|
+
totalTokens: 0
|
|
812
|
+
};
|
|
813
|
+
function usageOf(chunk) {
|
|
814
|
+
const usage = chunk.usage;
|
|
815
|
+
if (usage === void 0) return void 0;
|
|
816
|
+
if (Array.isArray(usage)) return fromSpecTokenUsage$1(usage, chunk.metadata?.tanstack?.usage);
|
|
817
|
+
return usage;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Sums every `RUN_FINISHED` of a run (tool loops finish once per iteration)
|
|
821
|
+
* and books the sum against the assistant message that closes the run.
|
|
822
|
+
*/
|
|
823
|
+
function useUsageTracker() {
|
|
824
|
+
const pendingRef = useRef(/* @__PURE__ */ new Map());
|
|
825
|
+
const [state, setState] = useState(() => ({
|
|
826
|
+
total: ZERO,
|
|
827
|
+
lastRun: void 0,
|
|
828
|
+
byMessage: /* @__PURE__ */ new Map()
|
|
829
|
+
}));
|
|
830
|
+
const onChunk = useCallback((chunk) => {
|
|
831
|
+
if (chunk.type !== EventType$1.RUN_FINISHED) return;
|
|
832
|
+
const usage = usageOf(chunk);
|
|
833
|
+
if (!usage) return;
|
|
834
|
+
pendingRef.current.set(chunk.runId, addTokenUsage(pendingRef.current.get(chunk.runId), usage));
|
|
835
|
+
}, []);
|
|
836
|
+
const onFinish = useCallback((message) => {
|
|
837
|
+
const runId = pendingRef.current.keys().next().value;
|
|
838
|
+
const run = runId === void 0 ? void 0 : pendingRef.current.get(runId);
|
|
839
|
+
if (runId !== void 0) pendingRef.current.delete(runId);
|
|
840
|
+
if (!run) return;
|
|
841
|
+
const booked = run;
|
|
842
|
+
setState((s) => {
|
|
843
|
+
const byMessage = new Map(s.byMessage);
|
|
844
|
+
byMessage.set(message.id, booked);
|
|
845
|
+
return {
|
|
846
|
+
total: addTokenUsage(s.total, booked),
|
|
847
|
+
lastRun: booked,
|
|
848
|
+
byMessage
|
|
849
|
+
};
|
|
850
|
+
});
|
|
851
|
+
}, []);
|
|
852
|
+
const reset = useCallback(() => {
|
|
853
|
+
pendingRef.current.clear();
|
|
854
|
+
setState({
|
|
855
|
+
total: ZERO,
|
|
856
|
+
lastRun: void 0,
|
|
857
|
+
byMessage: /* @__PURE__ */ new Map()
|
|
858
|
+
});
|
|
859
|
+
}, []);
|
|
860
|
+
return useMemo(() => ({
|
|
861
|
+
onChunk,
|
|
862
|
+
onFinish,
|
|
863
|
+
reset,
|
|
864
|
+
...state
|
|
865
|
+
}), [
|
|
866
|
+
onChunk,
|
|
867
|
+
onFinish,
|
|
868
|
+
reset,
|
|
869
|
+
state
|
|
870
|
+
]);
|
|
871
|
+
}
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/view/approval.tsx
|
|
874
|
+
const ApprovalContext = createContext(void 0);
|
|
875
|
+
if (process.env.NODE_ENV !== "production") ApprovalContext.displayName = "ApprovalContext";
|
|
876
|
+
function useApprovalContext() {
|
|
877
|
+
const context = use(ApprovalContext);
|
|
878
|
+
if (context === void 0) throw new Error("cadenza-ai: ApprovalContext is missing. Approval parts must be placed within <ApprovalActions>.");
|
|
879
|
+
return context;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The approve / deny row under a tool call. Resolves through the interrupt;
|
|
883
|
+
* both buttons disable once `interrupt.status` leaves `pending`. The choice
|
|
884
|
+
* lands on the row as `data-approved` / `data-denied`.
|
|
885
|
+
*/
|
|
886
|
+
function ApprovalActions({ interrupt, children, className }) {
|
|
887
|
+
const [choice, setChoice] = useState(void 0);
|
|
888
|
+
const responded = interrupt.status !== "pending";
|
|
889
|
+
const value = useMemo(() => ({
|
|
890
|
+
interrupt,
|
|
891
|
+
responded,
|
|
892
|
+
choose: setChoice
|
|
893
|
+
}), [interrupt, responded]);
|
|
894
|
+
return /* @__PURE__ */ jsx(ApprovalContext, {
|
|
895
|
+
value,
|
|
896
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
897
|
+
role: "group",
|
|
898
|
+
"data-slot": "approval-actions",
|
|
899
|
+
"data-approved": dataAttr(choice === true),
|
|
900
|
+
"data-denied": dataAttr(choice === false),
|
|
901
|
+
className,
|
|
902
|
+
children
|
|
903
|
+
})
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
function ApprovalApprove({ editedArgs, disabled, onClick, ...props }) {
|
|
907
|
+
const { interrupt, responded, choose } = useApprovalContext();
|
|
908
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
909
|
+
"data-slot": "approval-approve",
|
|
910
|
+
size: "sm",
|
|
911
|
+
...props,
|
|
912
|
+
disabled: disabled || responded,
|
|
913
|
+
onClick: (event) => {
|
|
914
|
+
onClick?.(event);
|
|
915
|
+
if (event.defaultPrevented) return;
|
|
916
|
+
choose(true);
|
|
917
|
+
interrupt.resolveInterrupt(true, { editedArgs });
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
function ApprovalDeny({ disabled, onClick, ...props }) {
|
|
922
|
+
const { interrupt, responded, choose } = useApprovalContext();
|
|
923
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
924
|
+
"data-slot": "approval-deny",
|
|
925
|
+
size: "sm",
|
|
926
|
+
variant: "outline",
|
|
927
|
+
...props,
|
|
928
|
+
disabled: disabled || responded,
|
|
929
|
+
onClick: (event) => {
|
|
930
|
+
onClick?.(event);
|
|
931
|
+
if (event.defaultPrevented) return;
|
|
932
|
+
choose(false);
|
|
933
|
+
interrupt.resolveInterrupt(false);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/view/byok-key-dialog.tsx
|
|
939
|
+
const DEFAULT_BYOK_KEY_DIALOG_LABELS = {
|
|
940
|
+
title: "API keys",
|
|
941
|
+
description: "Keys stay in this browser and are sent per request in a header.",
|
|
942
|
+
save: "Save",
|
|
943
|
+
clear: "Clear",
|
|
944
|
+
confirm: "Confirm",
|
|
945
|
+
unlock: "Unlock",
|
|
946
|
+
close: "Close",
|
|
947
|
+
serverKey: "Server key"
|
|
948
|
+
};
|
|
949
|
+
const ByokKeyDialogContext = createContext(null);
|
|
950
|
+
if (process.env.NODE_ENV !== "production") ByokKeyDialogContext.displayName = "ByokKeyDialogContext";
|
|
951
|
+
function useByokKeyDialog() {
|
|
952
|
+
const context = use(ByokKeyDialogContext);
|
|
953
|
+
if (context === null) throw new Error("cadenza-ai: ByokKeyDialogContext is missing. ByokKeyDialog parts must be placed within <ByokKeyDialog>.");
|
|
954
|
+
return context;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* The key-entry dialog for a `ByokClient`. Subscribes to the client and opens
|
|
958
|
+
* itself whenever it raises a prompt (`prepare()` blocked on a missing key, or
|
|
959
|
+
* the server answered `byok_missing`), focusing that provider's input.
|
|
960
|
+
*/
|
|
961
|
+
function ByokKeyDialog({ byok, catalog, coverage, open: openProp, defaultOpen, onOpenChange, labels: labelsProp, children }) {
|
|
962
|
+
const snapshot = useByok(byok);
|
|
963
|
+
const [open, setOpen] = useControllableState({
|
|
964
|
+
value: openProp,
|
|
965
|
+
defaultValue: defaultOpen,
|
|
966
|
+
fallback: false
|
|
967
|
+
});
|
|
968
|
+
const labels = useMemo(() => ({
|
|
969
|
+
...DEFAULT_BYOK_KEY_DIALOG_LABELS,
|
|
970
|
+
...labelsProp
|
|
971
|
+
}), [labelsProp]);
|
|
972
|
+
const change = (next, details) => {
|
|
973
|
+
onOpenChange?.(next, details);
|
|
974
|
+
if (!details.isCanceled) setOpen(next);
|
|
975
|
+
};
|
|
976
|
+
const openForPrompt = useEffectEvent(() => change(true, createChangeEventDetails("none")));
|
|
977
|
+
useEffect(() => {
|
|
978
|
+
if (snapshot.prompt) openForPrompt();
|
|
979
|
+
}, [snapshot.prompt]);
|
|
980
|
+
const promptRef = useRef(null);
|
|
981
|
+
useEffect(() => {
|
|
982
|
+
if (!open || snapshot.prompt === null) return;
|
|
983
|
+
const timers = [0, 250].map((delay) => setTimeout(() => {
|
|
984
|
+
if (document.activeElement !== promptRef.current) promptRef.current?.focus();
|
|
985
|
+
}, delay));
|
|
986
|
+
return () => timers.forEach(clearTimeout);
|
|
987
|
+
}, [open, snapshot.prompt]);
|
|
988
|
+
const formId = useId();
|
|
989
|
+
const [epoch, setEpoch] = useState(0);
|
|
990
|
+
const context = useMemo(() => ({
|
|
991
|
+
byok,
|
|
992
|
+
catalog,
|
|
993
|
+
coverage,
|
|
994
|
+
labels,
|
|
995
|
+
snapshot,
|
|
996
|
+
promptRef
|
|
997
|
+
}), [
|
|
998
|
+
byok,
|
|
999
|
+
catalog,
|
|
1000
|
+
coverage,
|
|
1001
|
+
labels,
|
|
1002
|
+
snapshot
|
|
1003
|
+
]);
|
|
1004
|
+
const confirm = async (event) => {
|
|
1005
|
+
event.preventDefault();
|
|
1006
|
+
const entries = Array.from(new FormData(event.currentTarget).entries());
|
|
1007
|
+
try {
|
|
1008
|
+
for (const [id, value] of entries) if (typeof value === "string" && value.trim() !== "") await byok.update(id, value.trim());
|
|
1009
|
+
} catch {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
setEpoch((e) => e + 1);
|
|
1013
|
+
change(false, createChangeEventDetails("confirm", event.nativeEvent));
|
|
1014
|
+
};
|
|
1015
|
+
return /* @__PURE__ */ jsx(ByokKeyDialogContext, {
|
|
1016
|
+
value: context,
|
|
1017
|
+
children: /* @__PURE__ */ jsx(Dialog, {
|
|
1018
|
+
open,
|
|
1019
|
+
onOpenChange: change,
|
|
1020
|
+
children: /* @__PURE__ */ jsxs(DialogPopup, {
|
|
1021
|
+
"data-slot": "byok-key-dialog",
|
|
1022
|
+
initialFocus: () => promptRef.current ?? true,
|
|
1023
|
+
children: [
|
|
1024
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
1025
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: labels.title }),
|
|
1026
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: labels.description }),
|
|
1027
|
+
/* @__PURE__ */ jsxs("p", {
|
|
1028
|
+
"data-slot": "byok-key-dialog-storage",
|
|
1029
|
+
"data-persistent": dataAttr(byok.storage.persistent),
|
|
1030
|
+
className: "text-xs text-muted-foreground",
|
|
1031
|
+
children: [byok.storage.label, byok.storage.warning !== void 0 && ` — ${byok.storage.warning}`]
|
|
1032
|
+
})
|
|
1033
|
+
] }),
|
|
1034
|
+
/* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsx("form", {
|
|
1035
|
+
id: formId,
|
|
1036
|
+
className: "flex flex-col gap-4",
|
|
1037
|
+
onSubmit: (event) => void confirm(event),
|
|
1038
|
+
children: children ?? catalog.providers.map((p) => /* @__PURE__ */ jsx(ByokKeyDialogProvider, { provider: p.id }, p.id))
|
|
1039
|
+
}, epoch) }),
|
|
1040
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
1041
|
+
snapshot.locked && /* @__PURE__ */ jsx(Button, {
|
|
1042
|
+
onClick: (event) => {
|
|
1043
|
+
const native = event.nativeEvent;
|
|
1044
|
+
byok.unlock().then(() => change(false, createChangeEventDetails("confirm", native)), () => {});
|
|
1045
|
+
},
|
|
1046
|
+
children: labels.unlock
|
|
1047
|
+
}),
|
|
1048
|
+
/* @__PURE__ */ jsx(DialogClose, {
|
|
1049
|
+
render: /* @__PURE__ */ jsx(Button, { variant: "outline" }),
|
|
1050
|
+
children: labels.close
|
|
1051
|
+
}),
|
|
1052
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1053
|
+
form: formId,
|
|
1054
|
+
type: "submit",
|
|
1055
|
+
children: labels.confirm
|
|
1056
|
+
})
|
|
1057
|
+
] })
|
|
1058
|
+
]
|
|
1059
|
+
})
|
|
1060
|
+
})
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* One provider's row: label, password input, save / clear icon buttons.
|
|
1065
|
+
* `data-key-status` mirrors the client's `empty | set | locked | error`;
|
|
1066
|
+
* `data-server-key` marks a provider the server can key on its own;
|
|
1067
|
+
* `data-prompted` is on while the client is asking for this provider's key.
|
|
1068
|
+
*/
|
|
1069
|
+
function ByokKeyDialogProvider({ provider: id, children, className }) {
|
|
1070
|
+
const { byok, catalog, coverage, labels, snapshot, promptRef } = useByokKeyDialog();
|
|
1071
|
+
const inputId = useId();
|
|
1072
|
+
const [draft, setDraft] = useState("");
|
|
1073
|
+
const provider = catalog.getProvider(id);
|
|
1074
|
+
if (provider === void 0) throw new Error(`cadenza-ai: ByokKeyDialogProvider: unknown provider "${id}".`);
|
|
1075
|
+
const status = snapshot.status[id] ?? { state: "empty" };
|
|
1076
|
+
const serverKey = !provider.keyRequired || coverage?.[id] === true;
|
|
1077
|
+
const key = draft.trim();
|
|
1078
|
+
const prompted = snapshot.prompt?.provider === id;
|
|
1079
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1080
|
+
"data-slot": "byok-key-dialog-provider",
|
|
1081
|
+
"data-provider": id,
|
|
1082
|
+
"data-key-status": status.state,
|
|
1083
|
+
"data-server-key": dataAttr(serverKey),
|
|
1084
|
+
"data-prompted": dataAttr(prompted),
|
|
1085
|
+
className: cn("flex items-end gap-2", className),
|
|
1086
|
+
children: [
|
|
1087
|
+
/* @__PURE__ */ jsxs(Field, {
|
|
1088
|
+
className: "flex-1",
|
|
1089
|
+
children: [
|
|
1090
|
+
/* @__PURE__ */ jsxs(FieldLabel, {
|
|
1091
|
+
htmlFor: inputId,
|
|
1092
|
+
children: [provider.label, serverKey && /* @__PURE__ */ jsx(IconServer, {
|
|
1093
|
+
"aria-label": labels.serverKey,
|
|
1094
|
+
className: "text-muted-foreground block-4 inline-4"
|
|
1095
|
+
})]
|
|
1096
|
+
}),
|
|
1097
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1098
|
+
id: inputId,
|
|
1099
|
+
ref: prompted ? promptRef : void 0,
|
|
1100
|
+
name: id,
|
|
1101
|
+
type: "password",
|
|
1102
|
+
autoComplete: "off",
|
|
1103
|
+
onKeyDown: (event) => {
|
|
1104
|
+
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
|
|
1105
|
+
event.preventDefault();
|
|
1106
|
+
event.currentTarget.form?.requestSubmit();
|
|
1107
|
+
}
|
|
1108
|
+
},
|
|
1109
|
+
placeholder: status.state === "empty" ? void 0 : status.masked,
|
|
1110
|
+
value: draft,
|
|
1111
|
+
onValueChange: setDraft
|
|
1112
|
+
}),
|
|
1113
|
+
status.state === "error" && /* @__PURE__ */ jsx(FieldError, { children: status.message })
|
|
1114
|
+
]
|
|
1115
|
+
}),
|
|
1116
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1117
|
+
variant: "outline",
|
|
1118
|
+
size: "icon-sm",
|
|
1119
|
+
"aria-label": labels.save,
|
|
1120
|
+
disabled: key === "",
|
|
1121
|
+
onClick: () => void byok.update(id, key).then(() => setDraft(""), () => {}),
|
|
1122
|
+
children: /* @__PURE__ */ jsx(IconCheck, {})
|
|
1123
|
+
}),
|
|
1124
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1125
|
+
variant: "ghost",
|
|
1126
|
+
size: "icon-sm",
|
|
1127
|
+
"aria-label": labels.clear,
|
|
1128
|
+
disabled: status.state === "empty",
|
|
1129
|
+
onClick: () => void byok.clear(id).catch(() => {}),
|
|
1130
|
+
children: /* @__PURE__ */ jsx(IconX, {})
|
|
1131
|
+
}),
|
|
1132
|
+
children
|
|
1133
|
+
]
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
//#endregion
|
|
1137
|
+
//#region src/view/composer.tsx
|
|
1138
|
+
const ComposerContext = createContext(null);
|
|
1139
|
+
if (process.env.NODE_ENV !== "production") ComposerContext.displayName = "ComposerContext";
|
|
1140
|
+
function useComposer() {
|
|
1141
|
+
const context = use(ComposerContext);
|
|
1142
|
+
if (context === null) throw new Error("cadenza-ai: ComposerContext is missing. Composer parts must be placed within <Composer>.");
|
|
1143
|
+
return context;
|
|
1144
|
+
}
|
|
1145
|
+
function isBusy(status) {
|
|
1146
|
+
return status === "submitted" || status === "streaming";
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* The input area: a `<form>` around an `InputGroup`, so `ComposerTextarea` and
|
|
1150
|
+
* `ComposerToolbar` stay direct children of the group (its auto-height and
|
|
1151
|
+
* block-end addon rules key off `>` combinators).
|
|
1152
|
+
*/
|
|
1153
|
+
function Composer({ allowEmpty = false, children, className, defaultValue, disabled = false, editing = false, onDragLeave, onDragOver, onDrop, onEditCancel, onFiles, onPaste, onStop, onValueChange, onValueCommitted, status, value: valueProp, ...props }) {
|
|
1154
|
+
const [value, setValueState] = useControllableState({
|
|
1155
|
+
value: valueProp,
|
|
1156
|
+
defaultValue,
|
|
1157
|
+
fallback: ""
|
|
1158
|
+
});
|
|
1159
|
+
const [dragging, setDragging] = useState(false);
|
|
1160
|
+
const latestRef = useRef({
|
|
1161
|
+
value,
|
|
1162
|
+
disabled,
|
|
1163
|
+
editing,
|
|
1164
|
+
status,
|
|
1165
|
+
allowEmpty,
|
|
1166
|
+
onValueChange,
|
|
1167
|
+
onValueCommitted,
|
|
1168
|
+
onStop,
|
|
1169
|
+
onEditCancel
|
|
1170
|
+
});
|
|
1171
|
+
latestRef.current = {
|
|
1172
|
+
value,
|
|
1173
|
+
disabled,
|
|
1174
|
+
editing,
|
|
1175
|
+
status,
|
|
1176
|
+
allowEmpty,
|
|
1177
|
+
onValueChange,
|
|
1178
|
+
onValueCommitted,
|
|
1179
|
+
onStop,
|
|
1180
|
+
onEditCancel
|
|
1181
|
+
};
|
|
1182
|
+
const committable = allowEmpty || value.trim() !== "";
|
|
1183
|
+
const context = useMemo(() => {
|
|
1184
|
+
const setValue = (next, details = createChangeEventDetails("none")) => {
|
|
1185
|
+
latestRef.current.onValueChange?.(next, details);
|
|
1186
|
+
if (details.isCanceled) return;
|
|
1187
|
+
setValueState(next);
|
|
1188
|
+
};
|
|
1189
|
+
return {
|
|
1190
|
+
value,
|
|
1191
|
+
setValue,
|
|
1192
|
+
status,
|
|
1193
|
+
editing,
|
|
1194
|
+
disabled,
|
|
1195
|
+
committable,
|
|
1196
|
+
submit: (details) => {
|
|
1197
|
+
const text = latestRef.current.value;
|
|
1198
|
+
if (latestRef.current.disabled || !latestRef.current.allowEmpty && text.trim() === "") return;
|
|
1199
|
+
latestRef.current.onValueCommitted(text, details);
|
|
1200
|
+
setValue("");
|
|
1201
|
+
},
|
|
1202
|
+
stop: (details) => {
|
|
1203
|
+
if (isBusy(latestRef.current.status)) latestRef.current.onStop?.(details);
|
|
1204
|
+
},
|
|
1205
|
+
cancelEdit: (details) => latestRef.current.onEditCancel?.(details)
|
|
1206
|
+
};
|
|
1207
|
+
}, [
|
|
1208
|
+
value,
|
|
1209
|
+
status,
|
|
1210
|
+
editing,
|
|
1211
|
+
disabled,
|
|
1212
|
+
committable,
|
|
1213
|
+
setValueState
|
|
1214
|
+
]);
|
|
1215
|
+
return /* @__PURE__ */ jsx(ComposerContext, {
|
|
1216
|
+
value: context,
|
|
1217
|
+
children: /* @__PURE__ */ jsx("form", {
|
|
1218
|
+
className: cn("flex flex-col gap-2", className),
|
|
1219
|
+
"data-slot": "composer",
|
|
1220
|
+
"data-submitted": dataAttr(status === "submitted"),
|
|
1221
|
+
"data-streaming": dataAttr(status === "streaming"),
|
|
1222
|
+
"data-error": dataAttr(status === "error"),
|
|
1223
|
+
"data-dragging": dataAttr(dragging),
|
|
1224
|
+
"data-editing": dataAttr(editing),
|
|
1225
|
+
...props,
|
|
1226
|
+
onSubmit: (event) => {
|
|
1227
|
+
event.preventDefault();
|
|
1228
|
+
context.submit(createGenericEventDetails("none", event.nativeEvent));
|
|
1229
|
+
},
|
|
1230
|
+
onDragOver: (event) => {
|
|
1231
|
+
onDragOver?.(event);
|
|
1232
|
+
if (event.defaultPrevented || !onFiles) return;
|
|
1233
|
+
event.preventDefault();
|
|
1234
|
+
setDragging(true);
|
|
1235
|
+
},
|
|
1236
|
+
onDragLeave: (event) => {
|
|
1237
|
+
onDragLeave?.(event);
|
|
1238
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1239
|
+
},
|
|
1240
|
+
onDrop: (event) => {
|
|
1241
|
+
onDrop?.(event);
|
|
1242
|
+
setDragging(false);
|
|
1243
|
+
if (event.defaultPrevented || !onFiles) return;
|
|
1244
|
+
const files = Array.from(event.dataTransfer.files);
|
|
1245
|
+
if (files.length === 0) return;
|
|
1246
|
+
event.preventDefault();
|
|
1247
|
+
onFiles(files, createGenericEventDetails("drag", event.nativeEvent));
|
|
1248
|
+
},
|
|
1249
|
+
onPaste: (event) => {
|
|
1250
|
+
onPaste?.(event);
|
|
1251
|
+
if (event.defaultPrevented || !onFiles) return;
|
|
1252
|
+
const files = Array.from(event.clipboardData.files);
|
|
1253
|
+
if (files.length === 0) return;
|
|
1254
|
+
event.preventDefault();
|
|
1255
|
+
onFiles(files, createGenericEventDetails("input-paste", event.nativeEvent));
|
|
1256
|
+
},
|
|
1257
|
+
children: /* @__PURE__ */ jsx(InputGroup, { children })
|
|
1258
|
+
})
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
/** The draft text. Enter commits, Shift+Enter breaks a line, IME composition is left alone, Escape cancels or stops. */
|
|
1262
|
+
function ComposerTextarea({ className, onChange, onKeyDown, ...props }) {
|
|
1263
|
+
const { value, setValue, editing, disabled, submit, stop, cancelEdit } = useComposer();
|
|
1264
|
+
return /* @__PURE__ */ jsx(InputGroupTextarea, {
|
|
1265
|
+
className: cn("field-sizing-content max-block-48", className),
|
|
1266
|
+
"data-slot": "composer-textarea",
|
|
1267
|
+
disabled,
|
|
1268
|
+
rows: 1,
|
|
1269
|
+
...props,
|
|
1270
|
+
value,
|
|
1271
|
+
onChange: (event) => {
|
|
1272
|
+
onChange?.(event);
|
|
1273
|
+
if (!event.defaultPrevented) setValue(event.target.value, createChangeEventDetails("input-change", event.nativeEvent));
|
|
1274
|
+
},
|
|
1275
|
+
onKeyDown: (event) => {
|
|
1276
|
+
onKeyDown?.(event);
|
|
1277
|
+
if (event.defaultPrevented || event.nativeEvent.isComposing || event.key === "Process") return;
|
|
1278
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1279
|
+
event.preventDefault();
|
|
1280
|
+
submit(createGenericEventDetails("keyboard", event.nativeEvent));
|
|
1281
|
+
} else if (event.key === "Escape") {
|
|
1282
|
+
if (editing) cancelEdit(createGenericEventDetails("escape-key", event.nativeEvent));
|
|
1283
|
+
else stop(createGenericEventDetails("escape-key", event.nativeEvent));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
/** The row under the textarea: attach, dictate, pickers, submit. */
|
|
1289
|
+
function ComposerToolbar(props) {
|
|
1290
|
+
return /* @__PURE__ */ jsx(InputGroupAddon, {
|
|
1291
|
+
align: "block-end",
|
|
1292
|
+
"data-slot": "composer-toolbar",
|
|
1293
|
+
...props
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
/** Send while idle, stop while busy. Disabled on an empty draft. */
|
|
1297
|
+
function ComposerSubmit({ children, ...props }) {
|
|
1298
|
+
const { committable, status, disabled, stop } = useComposer();
|
|
1299
|
+
if (isBusy(status)) return /* @__PURE__ */ jsx(Button, {
|
|
1300
|
+
"aria-label": "Stop",
|
|
1301
|
+
"data-slot": "composer-submit",
|
|
1302
|
+
size: "icon-sm",
|
|
1303
|
+
type: "button",
|
|
1304
|
+
...props,
|
|
1305
|
+
onClick: (event) => {
|
|
1306
|
+
props.onClick?.(event);
|
|
1307
|
+
if (!event.defaultPrevented) stop(createGenericEventDetails("none", event.nativeEvent));
|
|
1308
|
+
},
|
|
1309
|
+
children: children ?? /* @__PURE__ */ jsx(IconPlayerStop, {})
|
|
1310
|
+
});
|
|
1311
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1312
|
+
"aria-label": "Send",
|
|
1313
|
+
"data-slot": "composer-submit",
|
|
1314
|
+
disabled: disabled || !committable,
|
|
1315
|
+
size: "icon-sm",
|
|
1316
|
+
type: "submit",
|
|
1317
|
+
...props,
|
|
1318
|
+
children: children ?? /* @__PURE__ */ jsx(IconArrowUp, {})
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
/** The pending-attachments strip; `state` maps straight onto `Attachment`'s. Renders nothing while empty. */
|
|
1322
|
+
function ComposerAttachments({ items, onRemove, className }) {
|
|
1323
|
+
if (items.length === 0) return null;
|
|
1324
|
+
return /* @__PURE__ */ jsx(AttachmentGroup, {
|
|
1325
|
+
className,
|
|
1326
|
+
"data-slot": "composer-attachments",
|
|
1327
|
+
children: items.map((item) => /* @__PURE__ */ jsxs(Attachment, {
|
|
1328
|
+
size: "sm",
|
|
1329
|
+
state: item.state,
|
|
1330
|
+
children: [
|
|
1331
|
+
/* @__PURE__ */ jsx(AttachmentMedia, {
|
|
1332
|
+
variant: item.previewUrl === void 0 ? "icon" : "image",
|
|
1333
|
+
children: item.previewUrl === void 0 ? /* @__PURE__ */ jsx(IconPaperclip, {}) : /* @__PURE__ */ jsx("img", {
|
|
1334
|
+
alt: "",
|
|
1335
|
+
src: item.previewUrl
|
|
1336
|
+
})
|
|
1337
|
+
}),
|
|
1338
|
+
/* @__PURE__ */ jsx(AttachmentContent, { children: /* @__PURE__ */ jsx(AttachmentTitle, { children: item.name }) }),
|
|
1339
|
+
/* @__PURE__ */ jsx(AttachmentActions, { children: /* @__PURE__ */ jsx(AttachmentAction, {
|
|
1340
|
+
"aria-label": "Remove",
|
|
1341
|
+
onClick: (event) => onRemove(item.id, createGenericEventDetails("none", event.nativeEvent)),
|
|
1342
|
+
children: /* @__PURE__ */ jsx(IconX, {})
|
|
1343
|
+
}) })
|
|
1344
|
+
]
|
|
1345
|
+
}, item.id))
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
/** Opens the file picker; the chosen files go to `onFiles`. */
|
|
1349
|
+
function ComposerAttach({ accept, children, multiple = true, onClick, onFiles, ...props }) {
|
|
1350
|
+
const inputRef = useRef(null);
|
|
1351
|
+
const { disabled } = useComposer();
|
|
1352
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
1353
|
+
accept,
|
|
1354
|
+
hidden: true,
|
|
1355
|
+
multiple,
|
|
1356
|
+
ref: inputRef,
|
|
1357
|
+
tabIndex: -1,
|
|
1358
|
+
type: "file",
|
|
1359
|
+
onChange: (event) => {
|
|
1360
|
+
const files = Array.from(event.target.files ?? []);
|
|
1361
|
+
event.target.value = "";
|
|
1362
|
+
if (files.length > 0) onFiles(files, createGenericEventDetails("input-change", event.nativeEvent));
|
|
1363
|
+
}
|
|
1364
|
+
}), /* @__PURE__ */ jsx(InputGroupButton, {
|
|
1365
|
+
"aria-label": "Attach",
|
|
1366
|
+
"data-slot": "composer-attach",
|
|
1367
|
+
disabled,
|
|
1368
|
+
size: "icon-xs",
|
|
1369
|
+
...props,
|
|
1370
|
+
onClick: (event) => {
|
|
1371
|
+
onClick?.(event);
|
|
1372
|
+
if (!event.defaultPrevented) inputRef.current?.click();
|
|
1373
|
+
},
|
|
1374
|
+
children: children ?? /* @__PURE__ */ jsx(IconPaperclip, {})
|
|
1375
|
+
})] });
|
|
1376
|
+
}
|
|
1377
|
+
const noSubscribe = () => () => {};
|
|
1378
|
+
/** Press to record, press again to stop; the finished recording's audio part goes to `onRecording`. */
|
|
1379
|
+
function ComposerDictate({ children, onClick, onRecording, ...props }) {
|
|
1380
|
+
const { disabled } = useComposer();
|
|
1381
|
+
const recorder = useAudioRecorder();
|
|
1382
|
+
const supported = useSyncExternalStore(noSubscribe, () => recorder.isSupported, () => false);
|
|
1383
|
+
return /* @__PURE__ */ jsx(InputGroupButton, {
|
|
1384
|
+
"aria-label": "Dictate",
|
|
1385
|
+
"aria-pressed": recorder.isRecording,
|
|
1386
|
+
"data-recording": dataAttr(recorder.isRecording),
|
|
1387
|
+
"data-slot": "composer-dictate",
|
|
1388
|
+
disabled: disabled || !supported,
|
|
1389
|
+
size: "icon-xs",
|
|
1390
|
+
...props,
|
|
1391
|
+
onClick: (event) => {
|
|
1392
|
+
onClick?.(event);
|
|
1393
|
+
if (event.defaultPrevented) return;
|
|
1394
|
+
if (recorder.isRecording) recorder.stop().then((recording) => onRecording(recording.part, createGenericEventDetails("imperative-action", event.nativeEvent)), () => {});
|
|
1395
|
+
else recorder.start().catch(() => {});
|
|
1396
|
+
},
|
|
1397
|
+
children: children ?? /* @__PURE__ */ jsx(IconMicrophone, {})
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
//#endregion
|
|
1401
|
+
//#region src/view/context-usage.tsx
|
|
1402
|
+
const DEFAULT_CONTEXT_USAGE_LABELS = {
|
|
1403
|
+
prompt: "Prompt",
|
|
1404
|
+
completion: "Completion",
|
|
1405
|
+
cached: "Cached",
|
|
1406
|
+
total: "Total",
|
|
1407
|
+
cost: "Cost",
|
|
1408
|
+
cacheHit: "Cache hit",
|
|
1409
|
+
outputRatio: "Out per in",
|
|
1410
|
+
reasoningShare: "Reasoning",
|
|
1411
|
+
cacheSaved: "Saved by cache"
|
|
1412
|
+
};
|
|
1413
|
+
const USD$1 = new Intl.NumberFormat("en-US", {
|
|
1414
|
+
style: "currency",
|
|
1415
|
+
currency: "USD",
|
|
1416
|
+
maximumFractionDigits: 4
|
|
1417
|
+
});
|
|
1418
|
+
const PERCENT$1 = new Intl.NumberFormat("en-US", {
|
|
1419
|
+
style: "percent",
|
|
1420
|
+
maximumFractionDigits: 1
|
|
1421
|
+
});
|
|
1422
|
+
/**
|
|
1423
|
+
* How full the context is (spec O1): a bar for `promptTokens / contextWindow`
|
|
1424
|
+
* beside the number, with the token breakdown and `estimateCost` in a tooltip.
|
|
1425
|
+
* The whole thing is the tooltip's trigger; `data-ratio` (two decimals) is
|
|
1426
|
+
* the bar's value for styling from outside.
|
|
1427
|
+
*/
|
|
1428
|
+
function ContextUsage({ model, usage, labels: labelsProp, children, className }) {
|
|
1429
|
+
const labels = useMemo(() => ({
|
|
1430
|
+
...DEFAULT_CONTEXT_USAGE_LABELS,
|
|
1431
|
+
...labelsProp
|
|
1432
|
+
}), [labelsProp]);
|
|
1433
|
+
const id = useId();
|
|
1434
|
+
const window = model?.contextWindow;
|
|
1435
|
+
const ratio = window !== void 0 && window > 0 ? usage.promptTokens / window : void 0;
|
|
1436
|
+
const cached = usage.promptTokensDetails?.cachedTokens;
|
|
1437
|
+
const cost = model && estimateCost(model, usage);
|
|
1438
|
+
const metrics = useMemo(() => usageMetrics(usage, model), [usage, model]);
|
|
1439
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsxs(TooltipTrigger, {
|
|
1440
|
+
"data-slot": "context-usage",
|
|
1441
|
+
"data-ratio": ratio?.toFixed(2),
|
|
1442
|
+
className: cn(`
|
|
1443
|
+
inline-flex items-center gap-2 text-xs text-muted-foreground
|
|
1444
|
+
tabular-nums
|
|
1445
|
+
`, className),
|
|
1446
|
+
children: [ratio !== void 0 && /* @__PURE__ */ jsx(Progress, {
|
|
1447
|
+
value: Math.min(ratio, 1) * 100,
|
|
1448
|
+
"aria-labelledby": id,
|
|
1449
|
+
className: "inline-16"
|
|
1450
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
1451
|
+
id,
|
|
1452
|
+
children: [
|
|
1453
|
+
/* @__PURE__ */ jsx("span", {
|
|
1454
|
+
"data-slot": "context-usage-tokens",
|
|
1455
|
+
children: usage.promptTokens
|
|
1456
|
+
}),
|
|
1457
|
+
children !== void 0 && " ",
|
|
1458
|
+
children
|
|
1459
|
+
]
|
|
1460
|
+
})]
|
|
1461
|
+
}), /* @__PURE__ */ jsx(TooltipPopup, { children: /* @__PURE__ */ jsxs("dl", {
|
|
1462
|
+
"data-slot": "context-usage-details",
|
|
1463
|
+
className: "grid grid-cols-[auto_auto] gap-x-3 tabular-nums",
|
|
1464
|
+
children: [
|
|
1465
|
+
/* @__PURE__ */ jsx("dt", { children: labels.prompt }),
|
|
1466
|
+
/* @__PURE__ */ jsx("dd", {
|
|
1467
|
+
className: "text-end",
|
|
1468
|
+
children: usage.promptTokens
|
|
1469
|
+
}),
|
|
1470
|
+
/* @__PURE__ */ jsx("dt", { children: labels.completion }),
|
|
1471
|
+
/* @__PURE__ */ jsx("dd", {
|
|
1472
|
+
className: "text-end",
|
|
1473
|
+
children: usage.completionTokens
|
|
1474
|
+
}),
|
|
1475
|
+
cached !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: labels.cached }), /* @__PURE__ */ jsx("dd", {
|
|
1476
|
+
className: "text-end",
|
|
1477
|
+
children: cached
|
|
1478
|
+
})] }),
|
|
1479
|
+
/* @__PURE__ */ jsx("dt", { children: labels.total }),
|
|
1480
|
+
/* @__PURE__ */ jsx("dd", {
|
|
1481
|
+
className: "text-end",
|
|
1482
|
+
children: usage.totalTokens
|
|
1483
|
+
}),
|
|
1484
|
+
metrics.cacheHitRate !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: labels.cacheHit }), /* @__PURE__ */ jsx("dd", {
|
|
1485
|
+
className: "text-end",
|
|
1486
|
+
children: PERCENT$1.format(metrics.cacheHitRate)
|
|
1487
|
+
})] }),
|
|
1488
|
+
metrics.outputRatio !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: labels.outputRatio }), /* @__PURE__ */ jsx("dd", {
|
|
1489
|
+
className: "text-end",
|
|
1490
|
+
children: `${metrics.outputRatio.toFixed(2)}×`
|
|
1491
|
+
})] }),
|
|
1492
|
+
metrics.reasoningShare !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: labels.reasoningShare }), /* @__PURE__ */ jsx("dd", {
|
|
1493
|
+
className: "text-end",
|
|
1494
|
+
children: PERCENT$1.format(metrics.reasoningShare)
|
|
1495
|
+
})] }),
|
|
1496
|
+
cost !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: labels.cost }), /* @__PURE__ */ jsx("dd", {
|
|
1497
|
+
className: "text-end",
|
|
1498
|
+
children: USD$1.format(cost)
|
|
1499
|
+
})] }),
|
|
1500
|
+
metrics.cacheSavings !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: labels.cacheSaved }), /* @__PURE__ */ jsx("dd", {
|
|
1501
|
+
className: "text-end",
|
|
1502
|
+
children: USD$1.format(metrics.cacheSavings)
|
|
1503
|
+
})] })
|
|
1504
|
+
]
|
|
1505
|
+
}) })] });
|
|
1506
|
+
}
|
|
1507
|
+
//#endregion
|
|
1508
|
+
//#region src/view/markdown.tsx
|
|
1509
|
+
const PLUGINS = {
|
|
1510
|
+
code,
|
|
1511
|
+
math,
|
|
1512
|
+
cjk
|
|
1513
|
+
};
|
|
1514
|
+
const SHIKI_THEME = ["github-light-default", "vesper"];
|
|
1515
|
+
const CONTROLS = {
|
|
1516
|
+
code: {
|
|
1517
|
+
copy: true,
|
|
1518
|
+
download: false
|
|
1519
|
+
},
|
|
1520
|
+
table: {
|
|
1521
|
+
copy: true,
|
|
1522
|
+
download: false,
|
|
1523
|
+
fullscreen: false
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
function Anchor({ node: _node, ...props }) {
|
|
1527
|
+
return /* @__PURE__ */ jsx("a", {
|
|
1528
|
+
...props,
|
|
1529
|
+
target: "_blank",
|
|
1530
|
+
rel: "noreferrer"
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
const COMPONENTS = { a: Anchor };
|
|
1534
|
+
/** Product-grade Markdown: streamdown with code / math / CJK plugins and safe links. */
|
|
1535
|
+
function Markdown({ content, streaming = false, translations, className }) {
|
|
1536
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1537
|
+
"data-slot": "markdown",
|
|
1538
|
+
"data-streaming": dataAttr(streaming),
|
|
1539
|
+
className: "[&>div]:flex [&>div]:flex-col [&>div]:gap-4 [&>div]:space-y-0",
|
|
1540
|
+
children: /* @__PURE__ */ jsx(Streamdown, {
|
|
1541
|
+
mode: streaming ? "streaming" : "static",
|
|
1542
|
+
isAnimating: streaming,
|
|
1543
|
+
parseIncompleteMarkdown: true,
|
|
1544
|
+
plugins: PLUGINS,
|
|
1545
|
+
shikiTheme: SHIKI_THEME,
|
|
1546
|
+
controls: CONTROLS,
|
|
1547
|
+
dir: "auto",
|
|
1548
|
+
translations,
|
|
1549
|
+
components: COMPONENTS,
|
|
1550
|
+
className,
|
|
1551
|
+
children: content
|
|
1552
|
+
})
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
//#endregion
|
|
1556
|
+
//#region src/view/media-part.tsx
|
|
1557
|
+
function nameOf(metadata) {
|
|
1558
|
+
const name = metadata?.name;
|
|
1559
|
+
return typeof name === "string" ? name : void 0;
|
|
1560
|
+
}
|
|
1561
|
+
/** A message's media part rendered by type: image card, native audio / video, document card. */
|
|
1562
|
+
function MediaPart({ part, className }) {
|
|
1563
|
+
const { source } = part;
|
|
1564
|
+
const src = source.type === "data" ? `data:${source.mimeType};base64,${source.value}` : source.value;
|
|
1565
|
+
const name = nameOf(part.metadata);
|
|
1566
|
+
switch (part.type) {
|
|
1567
|
+
case "image": return /* @__PURE__ */ jsx(Attachment, {
|
|
1568
|
+
"data-slot": "media-part",
|
|
1569
|
+
"data-type": "image",
|
|
1570
|
+
size: "sm",
|
|
1571
|
+
orientation: "vertical",
|
|
1572
|
+
className,
|
|
1573
|
+
children: /* @__PURE__ */ jsx(AttachmentMedia, {
|
|
1574
|
+
variant: "image",
|
|
1575
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
1576
|
+
src,
|
|
1577
|
+
alt: name ?? ""
|
|
1578
|
+
})
|
|
1579
|
+
})
|
|
1580
|
+
});
|
|
1581
|
+
case "audio": return /* @__PURE__ */ jsx("audio", {
|
|
1582
|
+
"data-slot": "media-part",
|
|
1583
|
+
"data-type": "audio",
|
|
1584
|
+
controls: true,
|
|
1585
|
+
src,
|
|
1586
|
+
className
|
|
1587
|
+
});
|
|
1588
|
+
case "video": return /* @__PURE__ */ jsx("video", {
|
|
1589
|
+
"data-slot": "media-part",
|
|
1590
|
+
"data-type": "video",
|
|
1591
|
+
controls: true,
|
|
1592
|
+
src,
|
|
1593
|
+
className
|
|
1594
|
+
});
|
|
1595
|
+
case "document": return /* @__PURE__ */ jsxs(Attachment, {
|
|
1596
|
+
"data-slot": "media-part",
|
|
1597
|
+
"data-type": "document",
|
|
1598
|
+
size: "sm",
|
|
1599
|
+
className,
|
|
1600
|
+
children: [/* @__PURE__ */ jsx(AttachmentMedia, { children: /* @__PURE__ */ jsx(IconFileText, {}) }), /* @__PURE__ */ jsx(AttachmentContent, { children: /* @__PURE__ */ jsx(AttachmentTitle, { children: name ?? source.mimeType?.split("/")[1] ?? source.value }) })]
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
//#endregion
|
|
1605
|
+
//#region src/view/model-picker.tsx
|
|
1606
|
+
function formatContext(tokens) {
|
|
1607
|
+
return tokens >= 1e6 ? `${Math.round(tokens / 1e5) / 10}M` : `${Math.round(tokens / 1e3)}k`;
|
|
1608
|
+
}
|
|
1609
|
+
/** A searchable, provider-grouped model list; the value is the model ref. */
|
|
1610
|
+
function ModelPicker({ byok, catalog, className, defaultValue, disabledProviders, onValueChange, value }) {
|
|
1611
|
+
const groups = useMemo(() => catalog.providers.map((provider) => ({
|
|
1612
|
+
value: provider.id,
|
|
1613
|
+
items: provider.models.map(modelRef)
|
|
1614
|
+
})), [catalog]);
|
|
1615
|
+
const haystack = useMemo(() => new Map(catalog.models.map((model) => {
|
|
1616
|
+
const provider = catalog.getProvider(model.provider);
|
|
1617
|
+
return [modelRef(model), `${model.name} ${model.id} ${provider?.label ?? ""} ${model.provider}`.toLowerCase()];
|
|
1618
|
+
})), [catalog]);
|
|
1619
|
+
return /* @__PURE__ */ jsxs(Combobox, {
|
|
1620
|
+
defaultValue,
|
|
1621
|
+
filter: (ref, query) => haystack.get(ref)?.includes(query.trim().toLowerCase()) === true,
|
|
1622
|
+
items: groups,
|
|
1623
|
+
value,
|
|
1624
|
+
onValueChange: (ref, details) => {
|
|
1625
|
+
if (ref !== null) onValueChange(ref, details);
|
|
1626
|
+
},
|
|
1627
|
+
children: [/* @__PURE__ */ jsx(ComboboxTrigger, {
|
|
1628
|
+
className,
|
|
1629
|
+
"data-slot": "model-picker",
|
|
1630
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
1631
|
+
size: "sm",
|
|
1632
|
+
variant: "outline"
|
|
1633
|
+
}),
|
|
1634
|
+
children: /* @__PURE__ */ jsx(ComboboxValue, { children: (ref) => ref === null ? null : catalog.getModel(ref)?.name ?? ref })
|
|
1635
|
+
}), /* @__PURE__ */ jsxs(ComboboxPopup, {
|
|
1636
|
+
className: "inline-80",
|
|
1637
|
+
children: [
|
|
1638
|
+
/* @__PURE__ */ jsx(ComboboxInput, {
|
|
1639
|
+
"aria-label": "Search models",
|
|
1640
|
+
clearable: false,
|
|
1641
|
+
trigger: false
|
|
1642
|
+
}),
|
|
1643
|
+
/* @__PURE__ */ jsx(ComboboxEmpty, {}),
|
|
1644
|
+
/* @__PURE__ */ jsx(ComboboxList, { children: (group) => {
|
|
1645
|
+
const provider = catalog.getProvider(group.value);
|
|
1646
|
+
const keyMissing = provider?.keyRequired === true && byok?.status[group.value]?.state === "empty";
|
|
1647
|
+
return /* @__PURE__ */ jsxs(ComboboxGroup, {
|
|
1648
|
+
items: group.items,
|
|
1649
|
+
children: [/* @__PURE__ */ jsxs(ComboboxGroupLabel, {
|
|
1650
|
+
className: "flex items-center gap-1",
|
|
1651
|
+
"data-key-missing": dataAttr(keyMissing),
|
|
1652
|
+
children: [provider?.label ?? group.value, keyMissing && /* @__PURE__ */ jsx(IconKey, {
|
|
1653
|
+
"aria-label": "Key missing",
|
|
1654
|
+
className: "block-3.5 inline-3.5"
|
|
1655
|
+
})]
|
|
1656
|
+
}), /* @__PURE__ */ jsx(ComboboxCollection, { children: (ref) => {
|
|
1657
|
+
const model = catalog.getModel(ref);
|
|
1658
|
+
return /* @__PURE__ */ jsxs(ComboboxItem, {
|
|
1659
|
+
disabled: disabledProviders?.includes(group.value),
|
|
1660
|
+
value: ref,
|
|
1661
|
+
children: [
|
|
1662
|
+
/* @__PURE__ */ jsx("span", {
|
|
1663
|
+
className: "flex-1 truncate",
|
|
1664
|
+
children: model?.name ?? ref
|
|
1665
|
+
}),
|
|
1666
|
+
model?.reasoning === true && /* @__PURE__ */ jsx(IconBrain, {
|
|
1667
|
+
"aria-label": "Reasoning",
|
|
1668
|
+
className: "text-muted-foreground"
|
|
1669
|
+
}),
|
|
1670
|
+
model?.input.includes("image") === true && /* @__PURE__ */ jsx(IconPhoto, {
|
|
1671
|
+
"aria-label": "Vision",
|
|
1672
|
+
className: "text-muted-foreground"
|
|
1673
|
+
}),
|
|
1674
|
+
model?.contextWindow !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
1675
|
+
className: "\n text-xs text-muted-foreground tabular-nums\n ",
|
|
1676
|
+
children: formatContext(model.contextWindow)
|
|
1677
|
+
})
|
|
1678
|
+
]
|
|
1679
|
+
}, ref);
|
|
1680
|
+
} })]
|
|
1681
|
+
}, group.value);
|
|
1682
|
+
} })
|
|
1683
|
+
]
|
|
1684
|
+
})]
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
/** The model's supported thinking levels; renders nothing when there is no choice to make. */
|
|
1688
|
+
function ThinkingLevelPicker({ className, defaultValue, model, onValueChange, value }) {
|
|
1689
|
+
const levels = supportedThinkingLevels(model);
|
|
1690
|
+
if (levels.length <= 1) return null;
|
|
1691
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
1692
|
+
clearable: false,
|
|
1693
|
+
defaultValue,
|
|
1694
|
+
value,
|
|
1695
|
+
onValueChange: (level, details) => {
|
|
1696
|
+
if (level !== null) onValueChange(level, details);
|
|
1697
|
+
},
|
|
1698
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
1699
|
+
"aria-label": "Thinking level",
|
|
1700
|
+
className,
|
|
1701
|
+
"data-slot": "thinking-level-picker",
|
|
1702
|
+
size: "sm",
|
|
1703
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
1704
|
+
}), /* @__PURE__ */ jsx(SelectPopup, { children: levels.map((level) => /* @__PURE__ */ jsx(SelectItem, {
|
|
1705
|
+
value: level,
|
|
1706
|
+
children: level
|
|
1707
|
+
}, level)) })]
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* The one-press deep-thinking switch, DeepSeek style: a pill that lights up
|
|
1712
|
+
* while thinking is on. Renders nothing for a model without reasoning; a model
|
|
1713
|
+
* that cannot switch thinking off (no `'off'` level) shows it pressed and
|
|
1714
|
+
* disabled. `data-thinking` mirrors the on state.
|
|
1715
|
+
*/
|
|
1716
|
+
function ThinkingToggle({ children, className, defaultValue, model, onLevel = "high", onValueChange, value }) {
|
|
1717
|
+
const levels = supportedThinkingLevels(model);
|
|
1718
|
+
const [level, setLevel] = useControllableState({
|
|
1719
|
+
value,
|
|
1720
|
+
defaultValue,
|
|
1721
|
+
fallback: "off"
|
|
1722
|
+
});
|
|
1723
|
+
if (levels.length <= 1) return null;
|
|
1724
|
+
const canOff = levels.includes("off");
|
|
1725
|
+
const on = level !== "off";
|
|
1726
|
+
return /* @__PURE__ */ jsxs(Toggle, {
|
|
1727
|
+
"data-slot": "thinking-toggle",
|
|
1728
|
+
"data-thinking": dataAttr(on),
|
|
1729
|
+
pressed: on,
|
|
1730
|
+
disabled: !canOff,
|
|
1731
|
+
size: "sm",
|
|
1732
|
+
variant: "outline",
|
|
1733
|
+
className: cn(`
|
|
1734
|
+
gap-1.5 rounded-full
|
|
1735
|
+
data-pressed:border-primary/40 data-pressed:bg-primary/10
|
|
1736
|
+
data-pressed:text-primary
|
|
1737
|
+
data-pressed:hover:bg-primary/15
|
|
1738
|
+
`, className),
|
|
1739
|
+
onPressedChange: (pressed, details) => {
|
|
1740
|
+
const clamped = clampThinkingLevel(model, onLevel);
|
|
1741
|
+
const next = pressed ? clamped === "off" ? levels[levels.length - 1] : clamped : "off";
|
|
1742
|
+
setLevel(next);
|
|
1743
|
+
onValueChange(next, details);
|
|
1744
|
+
},
|
|
1745
|
+
children: [/* @__PURE__ */ jsx(IconBrain, { "aria-hidden": true }), children]
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* The one-press web-search switch, the same pill as `ThinkingToggle`. Renders
|
|
1750
|
+
* nothing for a model without `search`, so it appears and disappears with the
|
|
1751
|
+
* model the way the thinking controls do. `data-search` mirrors the on state.
|
|
1752
|
+
*
|
|
1753
|
+
* The search itself runs on the provider's side; the caller's job is to put the
|
|
1754
|
+
* answer into `forwardedProps` so the server can hand that provider its
|
|
1755
|
+
* built-in tool (`createChatHandler`'s function form of `tools`).
|
|
1756
|
+
*/
|
|
1757
|
+
function SearchToggle({ children, className, defaultValue, model, onValueChange, value }) {
|
|
1758
|
+
const [on, setOn] = useControllableState({
|
|
1759
|
+
value,
|
|
1760
|
+
defaultValue,
|
|
1761
|
+
fallback: false
|
|
1762
|
+
});
|
|
1763
|
+
if (model?.search !== true) return null;
|
|
1764
|
+
return /* @__PURE__ */ jsxs(Toggle, {
|
|
1765
|
+
"data-slot": "search-toggle",
|
|
1766
|
+
"data-search": dataAttr(on),
|
|
1767
|
+
pressed: on,
|
|
1768
|
+
size: "sm",
|
|
1769
|
+
variant: "outline",
|
|
1770
|
+
className: cn(`
|
|
1771
|
+
gap-1.5 rounded-full
|
|
1772
|
+
data-pressed:border-primary/40 data-pressed:bg-primary/10
|
|
1773
|
+
data-pressed:text-primary
|
|
1774
|
+
data-pressed:hover:bg-primary/15
|
|
1775
|
+
`, className),
|
|
1776
|
+
onPressedChange: (next, details) => {
|
|
1777
|
+
setOn(next);
|
|
1778
|
+
onValueChange(next, details);
|
|
1779
|
+
},
|
|
1780
|
+
children: [/* @__PURE__ */ jsx(IconWorld, { "aria-hidden": true }), children]
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
//#endregion
|
|
1784
|
+
//#region src/view/queue.tsx
|
|
1785
|
+
function queuedText(message) {
|
|
1786
|
+
if (typeof message.content === "string") return message.content;
|
|
1787
|
+
const { content } = message.content;
|
|
1788
|
+
if (typeof content === "string") return content;
|
|
1789
|
+
return content.map((part) => part.type === "text" ? part.content : `[${part.type}]`).join(" ");
|
|
1790
|
+
}
|
|
1791
|
+
/** Messages waiting behind the current turn, each with a cancel. */
|
|
1792
|
+
function QueueList({ children, className, onCancel, queue }) {
|
|
1793
|
+
return /* @__PURE__ */ jsxs(ItemGroup, {
|
|
1794
|
+
className,
|
|
1795
|
+
"data-slot": "queue-list",
|
|
1796
|
+
children: [queue.map((message) => /* @__PURE__ */ jsxs(Item, {
|
|
1797
|
+
"data-slot": "queue-list-item",
|
|
1798
|
+
size: "xs",
|
|
1799
|
+
children: [/* @__PURE__ */ jsx(ItemContent, { children: /* @__PURE__ */ jsx(ItemTitle, { children: queuedText(message) }) }), /* @__PURE__ */ jsx(ItemActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
1800
|
+
"aria-label": "Cancel",
|
|
1801
|
+
size: "icon-xs",
|
|
1802
|
+
variant: "ghost",
|
|
1803
|
+
onClick: (event) => onCancel(message.id, createGenericEventDetails("none", event.nativeEvent)),
|
|
1804
|
+
children: /* @__PURE__ */ jsx(IconX, {})
|
|
1805
|
+
}) })]
|
|
1806
|
+
}, message.id)), children]
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
//#endregion
|
|
1810
|
+
//#region src/view/reasoning.tsx
|
|
1811
|
+
function elapsedSeconds(complete, startedAt) {
|
|
1812
|
+
return complete ? Math.max(1, Math.round((Date.now() - startedAt) / 1e3)) : void 0;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* A thinking block: open while streaming (shimmering trigger), folds itself
|
|
1816
|
+
* once when `complete` flips — unless the user has already toggled it by hand.
|
|
1817
|
+
*/
|
|
1818
|
+
function Reasoning({ content, complete, startedAt: startedAtProp, open: openProp, defaultOpen, onOpenChange, children, className }) {
|
|
1819
|
+
const [open, setOpenState] = useControllableState({
|
|
1820
|
+
value: openProp,
|
|
1821
|
+
defaultValue: defaultOpen,
|
|
1822
|
+
fallback: !complete
|
|
1823
|
+
});
|
|
1824
|
+
const manualRef = useRef(false);
|
|
1825
|
+
const completeRef = useRef(complete);
|
|
1826
|
+
const [startedAt] = useState(() => startedAtProp ?? Date.now());
|
|
1827
|
+
const [seconds, setSeconds] = useState(() => elapsedSeconds(complete, startedAt));
|
|
1828
|
+
const setOpen = (next, details) => {
|
|
1829
|
+
onOpenChange?.(next, details);
|
|
1830
|
+
if (details.isCanceled) return;
|
|
1831
|
+
setOpenState(next);
|
|
1832
|
+
};
|
|
1833
|
+
const onComplete = useEffectEvent(() => {
|
|
1834
|
+
setSeconds(elapsedSeconds(true, startedAt));
|
|
1835
|
+
if (!manualRef.current) setOpen(false, createChangeEventDetails("none"));
|
|
1836
|
+
});
|
|
1837
|
+
useEffect(() => {
|
|
1838
|
+
const was = completeRef.current;
|
|
1839
|
+
completeRef.current = complete;
|
|
1840
|
+
if (complete && !was) onComplete();
|
|
1841
|
+
}, [complete]);
|
|
1842
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
1843
|
+
"data-slot": "reasoning",
|
|
1844
|
+
"data-complete": dataAttr(complete),
|
|
1845
|
+
"data-open": dataAttr(open),
|
|
1846
|
+
open,
|
|
1847
|
+
onOpenChange: (next, details) => {
|
|
1848
|
+
manualRef.current = true;
|
|
1849
|
+
setOpen(next, details);
|
|
1850
|
+
},
|
|
1851
|
+
className: cn("flex flex-col", className),
|
|
1852
|
+
children: [/* @__PURE__ */ jsx(CollapsibleTrigger, {
|
|
1853
|
+
"data-slot": "reasoning-trigger",
|
|
1854
|
+
render: /* @__PURE__ */ jsx(Marker, { render: /* @__PURE__ */ jsx("button", { type: "button" }) }),
|
|
1855
|
+
children: /* @__PURE__ */ jsxs(MarkerContent, {
|
|
1856
|
+
className: complete ? void 0 : "shimmer",
|
|
1857
|
+
children: [children, seconds !== void 0 && /* @__PURE__ */ jsxs("span", {
|
|
1858
|
+
"data-slot": "reasoning-duration",
|
|
1859
|
+
children: [seconds, "s"]
|
|
1860
|
+
})]
|
|
1861
|
+
})
|
|
1862
|
+
}), /* @__PURE__ */ jsx(CollapsiblePanel, {
|
|
1863
|
+
keepMounted: true,
|
|
1864
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1865
|
+
className: "pbs-2",
|
|
1866
|
+
children: /* @__PURE__ */ jsx(Markdown, {
|
|
1867
|
+
content,
|
|
1868
|
+
streaming: !complete,
|
|
1869
|
+
className: "text-sm text-muted-foreground"
|
|
1870
|
+
})
|
|
1871
|
+
})
|
|
1872
|
+
})]
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
//#endregion
|
|
1876
|
+
//#region src/view/sources.tsx
|
|
1877
|
+
/** The links a provider-executed search returned (spec T12), folded behind a trigger. */
|
|
1878
|
+
function Sources({ sources, children, open: openProp, defaultOpen, onOpenChange, className }) {
|
|
1879
|
+
const [open, setOpen] = useControllableState({
|
|
1880
|
+
value: openProp,
|
|
1881
|
+
defaultValue: defaultOpen,
|
|
1882
|
+
fallback: false
|
|
1883
|
+
});
|
|
1884
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
1885
|
+
"data-slot": "sources",
|
|
1886
|
+
"data-count": sources.length,
|
|
1887
|
+
open,
|
|
1888
|
+
onOpenChange: (next, details) => {
|
|
1889
|
+
onOpenChange?.(next, details);
|
|
1890
|
+
if (!details.isCanceled) setOpen(next);
|
|
1891
|
+
},
|
|
1892
|
+
className: cn("flex flex-col text-sm", className),
|
|
1893
|
+
children: [/* @__PURE__ */ jsx(CollapsibleTrigger, {
|
|
1894
|
+
"data-slot": "sources-trigger",
|
|
1895
|
+
className: "text-muted-foreground",
|
|
1896
|
+
children
|
|
1897
|
+
}), /* @__PURE__ */ jsx(CollapsiblePanel, { children: /* @__PURE__ */ jsx("ol", {
|
|
1898
|
+
className: "flex list-decimal flex-col gap-1 ps-5 pbs-2",
|
|
1899
|
+
children: sources.map((source) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("a", {
|
|
1900
|
+
href: source.url,
|
|
1901
|
+
target: "_blank",
|
|
1902
|
+
rel: "noreferrer",
|
|
1903
|
+
className: "underline underline-offset-4",
|
|
1904
|
+
children: source.title ?? source.url
|
|
1905
|
+
}), source.snippet !== void 0 && /* @__PURE__ */ jsx("p", {
|
|
1906
|
+
className: "text-muted-foreground",
|
|
1907
|
+
children: source.snippet
|
|
1908
|
+
})] }, source.url))
|
|
1909
|
+
}) })]
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
//#endregion
|
|
1913
|
+
//#region src/view/structured-output.tsx
|
|
1914
|
+
function jsonBlock$1(value) {
|
|
1915
|
+
return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
|
1916
|
+
}
|
|
1917
|
+
/** `useChat({ outputSchema })`'s part (spec T23): partial JSON while streaming, the validated data once complete. */
|
|
1918
|
+
function StructuredOutput({ part, className }) {
|
|
1919
|
+
const streaming = part.status === "streaming";
|
|
1920
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1921
|
+
"data-slot": "structured-output",
|
|
1922
|
+
"data-streaming": dataAttr(streaming),
|
|
1923
|
+
"data-complete": dataAttr(part.status === "complete"),
|
|
1924
|
+
"data-error": dataAttr(part.status === "error"),
|
|
1925
|
+
className: cn("flex flex-col gap-2", className),
|
|
1926
|
+
children: [
|
|
1927
|
+
streaming && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Markdown, {
|
|
1928
|
+
content: jsonBlock$1(part.partial),
|
|
1929
|
+
streaming: true
|
|
1930
|
+
}), /* @__PURE__ */ jsx(Spinner, {
|
|
1931
|
+
"aria-hidden": true,
|
|
1932
|
+
className: "block-[1em] inline-[1em]"
|
|
1933
|
+
})] }),
|
|
1934
|
+
part.status === "complete" && /* @__PURE__ */ jsx(Markdown, { content: jsonBlock$1(part.data) }),
|
|
1935
|
+
part.status === "error" && /* @__PURE__ */ jsx("p", {
|
|
1936
|
+
"data-slot": "structured-output-error",
|
|
1937
|
+
className: "text-sm text-destructive",
|
|
1938
|
+
children: part.errorMessage
|
|
1939
|
+
})
|
|
1940
|
+
]
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
//#endregion
|
|
1944
|
+
//#region src/view/suggestions.tsx
|
|
1945
|
+
const SuggestionsContext = createContext(null);
|
|
1946
|
+
if (process.env.NODE_ENV !== "production") SuggestionsContext.displayName = "SuggestionsContext";
|
|
1947
|
+
function useSuggestionsContext() {
|
|
1948
|
+
const context = use(SuggestionsContext);
|
|
1949
|
+
if (context === null) throw new Error("cadenza-ai: SuggestionsContext is missing. Suggestions parts must be placed within <Suggestions>.");
|
|
1950
|
+
return context;
|
|
1951
|
+
}
|
|
1952
|
+
/** A horizontally scrolling row of prompt chips. */
|
|
1953
|
+
function Suggestions({ children, className, onValueChange }) {
|
|
1954
|
+
const context = useMemo(() => ({ onValueChange }), [onValueChange]);
|
|
1955
|
+
return /* @__PURE__ */ jsx(SuggestionsContext, {
|
|
1956
|
+
value: context,
|
|
1957
|
+
children: /* @__PURE__ */ jsx(ScrollArea, {
|
|
1958
|
+
className: cn("inline-full", className),
|
|
1959
|
+
orientation: "horizontal",
|
|
1960
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1961
|
+
className: "flex gap-2 inline-max",
|
|
1962
|
+
"data-slot": "suggestions",
|
|
1963
|
+
role: "group",
|
|
1964
|
+
children
|
|
1965
|
+
})
|
|
1966
|
+
})
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
function SuggestionsItem({ onClick, value, ...props }) {
|
|
1970
|
+
const { onValueChange } = useSuggestionsContext();
|
|
1971
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1972
|
+
"data-slot": "suggestions-item",
|
|
1973
|
+
"data-value": value,
|
|
1974
|
+
size: "sm",
|
|
1975
|
+
variant: "outline",
|
|
1976
|
+
...props,
|
|
1977
|
+
onClick: (event) => {
|
|
1978
|
+
onClick?.(event);
|
|
1979
|
+
if (!event.defaultPrevented) onValueChange(value, createChangeEventDetails("item-press", event.nativeEvent));
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
//#endregion
|
|
1984
|
+
//#region src/view/thread-list.tsx
|
|
1985
|
+
const ThreadListContext = createContext(null);
|
|
1986
|
+
if (process.env.NODE_ENV !== "production") ThreadListContext.displayName = "ThreadListContext";
|
|
1987
|
+
function useThreadList() {
|
|
1988
|
+
const context = use(ThreadListContext);
|
|
1989
|
+
if (context === null) throw new Error("cadenza-ai: ThreadListContext is missing. ThreadList parts must be placed within <ThreadList>.");
|
|
1990
|
+
return context;
|
|
1991
|
+
}
|
|
1992
|
+
const ThreadListItemContext = createContext(null);
|
|
1993
|
+
if (process.env.NODE_ENV !== "production") ThreadListItemContext.displayName = "ThreadListItemContext";
|
|
1994
|
+
function useThreadListItemContext() {
|
|
1995
|
+
const context = use(ThreadListItemContext);
|
|
1996
|
+
if (context === null) throw new Error("cadenza-ai: ThreadListItemContext is missing. ThreadListItem parts must be placed within <ThreadListItem>.");
|
|
1997
|
+
return context;
|
|
1998
|
+
}
|
|
1999
|
+
/** The row a `ThreadListItem` action sits in. */
|
|
2000
|
+
function useThreadListItem() {
|
|
2001
|
+
const { thread, active } = useThreadListItemContext();
|
|
2002
|
+
return {
|
|
2003
|
+
thread,
|
|
2004
|
+
active
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* The scrolling list of threads. `ScrollArea` root > `div[role=list]`; the
|
|
2009
|
+
* current thread is the controlled `value`, and selecting a row (or `ThreadListNew`
|
|
2010
|
+
* creating one) reports through `onValueChange(id, details)`.
|
|
2011
|
+
*/
|
|
2012
|
+
function ThreadList({ index, threads, value, defaultValue, onValueChange, children, className, untitled }) {
|
|
2013
|
+
const [current, setCurrent] = useControllableState({
|
|
2014
|
+
value,
|
|
2015
|
+
defaultValue
|
|
2016
|
+
});
|
|
2017
|
+
const select = useCallback((id, details) => {
|
|
2018
|
+
onValueChange(id, details);
|
|
2019
|
+
if (!details.isCanceled) setCurrent(id);
|
|
2020
|
+
}, [onValueChange, setCurrent]);
|
|
2021
|
+
const context = useMemo(() => ({
|
|
2022
|
+
index,
|
|
2023
|
+
value: current,
|
|
2024
|
+
select,
|
|
2025
|
+
untitled
|
|
2026
|
+
}), [
|
|
2027
|
+
index,
|
|
2028
|
+
current,
|
|
2029
|
+
select,
|
|
2030
|
+
untitled
|
|
2031
|
+
]);
|
|
2032
|
+
return /* @__PURE__ */ jsx(ThreadListContext, {
|
|
2033
|
+
value: context,
|
|
2034
|
+
children: /* @__PURE__ */ jsx(ScrollArea, {
|
|
2035
|
+
className,
|
|
2036
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2037
|
+
role: "list",
|
|
2038
|
+
"data-slot": "thread-list",
|
|
2039
|
+
className: "flex flex-col gap-1",
|
|
2040
|
+
children: children ?? threads.map((thread) => /* @__PURE__ */ jsx(ThreadListItem, { thread }, thread.id))
|
|
2041
|
+
})
|
|
2042
|
+
})
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
function ThreadListGroup({ children, className }) {
|
|
2046
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2047
|
+
role: "group",
|
|
2048
|
+
"data-slot": "thread-list-group",
|
|
2049
|
+
className: cn(`flex flex-col gap-1`, className),
|
|
2050
|
+
children
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
function ThreadListGroupLabel({ className, ...props }) {
|
|
2054
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2055
|
+
"data-slot": "thread-list-group-label",
|
|
2056
|
+
className: cn("ps-3 pbs-2 text-xs text-muted-foreground", className),
|
|
2057
|
+
...props
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* One row: an `Item` whose content is a press target for selection, or — while
|
|
2062
|
+
* renaming — an inline `Input` (Enter / blur commit, Escape cancels). Mirrors its
|
|
2063
|
+
* state as `data-active` (+ `aria-current="page"`), `data-archived`, `data-renaming`.
|
|
2064
|
+
*/
|
|
2065
|
+
function ThreadListItem({ thread, children, className }) {
|
|
2066
|
+
const { index, value, select, untitled } = useThreadList();
|
|
2067
|
+
const [renaming, setRenaming] = useState(false);
|
|
2068
|
+
const settledRef = useRef(false);
|
|
2069
|
+
const active = value === thread.id;
|
|
2070
|
+
const context = useMemo(() => ({
|
|
2071
|
+
thread,
|
|
2072
|
+
active,
|
|
2073
|
+
renaming,
|
|
2074
|
+
setRenaming: (next) => {
|
|
2075
|
+
settledRef.current = false;
|
|
2076
|
+
setRenaming(next);
|
|
2077
|
+
}
|
|
2078
|
+
}), [
|
|
2079
|
+
thread,
|
|
2080
|
+
active,
|
|
2081
|
+
renaming
|
|
2082
|
+
]);
|
|
2083
|
+
const finish = (title) => {
|
|
2084
|
+
if (settledRef.current) return;
|
|
2085
|
+
settledRef.current = true;
|
|
2086
|
+
const next = title?.trim() ?? "";
|
|
2087
|
+
if (next !== "" && next !== thread.title) index.rename(thread.id, next);
|
|
2088
|
+
setRenaming(false);
|
|
2089
|
+
};
|
|
2090
|
+
const onKeyDown = (event) => {
|
|
2091
|
+
if (event.key === "Enter") finish(event.currentTarget.value);
|
|
2092
|
+
else if (event.key === "Escape") finish(null);
|
|
2093
|
+
};
|
|
2094
|
+
return /* @__PURE__ */ jsx(ThreadListItemContext, {
|
|
2095
|
+
value: context,
|
|
2096
|
+
children: /* @__PURE__ */ jsxs(Item, {
|
|
2097
|
+
role: "listitem",
|
|
2098
|
+
size: "sm",
|
|
2099
|
+
"data-slot": "thread-list-item",
|
|
2100
|
+
"data-active": dataAttr(active),
|
|
2101
|
+
"data-archived": dataAttr(thread.archived),
|
|
2102
|
+
"data-renaming": dataAttr(renaming),
|
|
2103
|
+
"aria-current": active ? "page" : void 0,
|
|
2104
|
+
className: cn("data-active:bg-muted", className),
|
|
2105
|
+
children: [/* @__PURE__ */ jsx(ItemContent, { children: renaming ? /* @__PURE__ */ jsx(Input, {
|
|
2106
|
+
autoFocus: true,
|
|
2107
|
+
defaultValue: thread.title,
|
|
2108
|
+
onKeyDown,
|
|
2109
|
+
onBlur: (event) => finish(event.currentTarget.value)
|
|
2110
|
+
}) : /* @__PURE__ */ jsxs("button", {
|
|
2111
|
+
type: "button",
|
|
2112
|
+
className: "\n flex flex-col items-start rounded-md text-start outline-none\n min-inline-0\n ",
|
|
2113
|
+
onClick: (event) => select(thread.id, createChangeEventDetails("item-press", event.nativeEvent)),
|
|
2114
|
+
children: [/* @__PURE__ */ jsx(ItemTitle, { children: thread.title !== "" ? thread.title : untitled }), thread.preview !== "" && /* @__PURE__ */ jsx(ItemDescription, { children: thread.preview })]
|
|
2115
|
+
}) }), children !== void 0 && /* @__PURE__ */ jsx(ItemActions, { children })]
|
|
2116
|
+
})
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
function handled(onClick, event) {
|
|
2120
|
+
onClick?.(event);
|
|
2121
|
+
return event.defaultPrevented;
|
|
2122
|
+
}
|
|
2123
|
+
/** Switches the row to its inline rename input. */
|
|
2124
|
+
function ThreadListRename({ children, onClick, ...props }) {
|
|
2125
|
+
const { setRenaming } = useThreadListItemContext();
|
|
2126
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
2127
|
+
"data-slot": "thread-list-rename",
|
|
2128
|
+
variant: "ghost",
|
|
2129
|
+
size: "icon-xs",
|
|
2130
|
+
...props,
|
|
2131
|
+
onClick: (event) => {
|
|
2132
|
+
if (!handled(onClick, event)) setRenaming(true);
|
|
2133
|
+
},
|
|
2134
|
+
children: children ?? /* @__PURE__ */ jsx(IconPencil, {})
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
/** Toggles `archived` through the index. */
|
|
2138
|
+
function ThreadListArchive({ children, onClick, ...props }) {
|
|
2139
|
+
const { index } = useThreadList();
|
|
2140
|
+
const { thread } = useThreadListItemContext();
|
|
2141
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
2142
|
+
"data-slot": "thread-list-archive",
|
|
2143
|
+
variant: "ghost",
|
|
2144
|
+
size: "icon-xs",
|
|
2145
|
+
...props,
|
|
2146
|
+
onClick: (event) => {
|
|
2147
|
+
if (!handled(onClick, event)) index.archive(thread.id, !thread.archived);
|
|
2148
|
+
},
|
|
2149
|
+
children: children ?? /* @__PURE__ */ jsx(IconArchive, {})
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
/** Removes the thread from the index. Confirmation is the caller's — wrap it in an `AlertDialog`. */
|
|
2153
|
+
function ThreadListDelete({ children, onClick, ...props }) {
|
|
2154
|
+
const { index } = useThreadList();
|
|
2155
|
+
const { thread } = useThreadListItemContext();
|
|
2156
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
2157
|
+
"data-slot": "thread-list-delete",
|
|
2158
|
+
variant: "ghost",
|
|
2159
|
+
size: "icon-xs",
|
|
2160
|
+
...props,
|
|
2161
|
+
onClick: (event) => {
|
|
2162
|
+
if (!handled(onClick, event)) index.remove(thread.id);
|
|
2163
|
+
},
|
|
2164
|
+
children: children ?? /* @__PURE__ */ jsx(IconTrash, {})
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
/** Creates an empty thread and selects it (`reason: 'item-press'`). */
|
|
2168
|
+
function ThreadListNew({ onClick, ...props }) {
|
|
2169
|
+
const { index, select } = useThreadList();
|
|
2170
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
2171
|
+
"data-slot": "thread-list-new",
|
|
2172
|
+
variant: "outline",
|
|
2173
|
+
...props,
|
|
2174
|
+
onClick: (event) => {
|
|
2175
|
+
if (handled(onClick, event)) return;
|
|
2176
|
+
const thread = index.create();
|
|
2177
|
+
select(thread.id, createChangeEventDetails("item-press", event.nativeEvent));
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
//#endregion
|
|
2182
|
+
//#region src/view/tool-call.tsx
|
|
2183
|
+
function useOpen({ open, defaultOpen, onOpenChange }, fallback = false) {
|
|
2184
|
+
const [value, setValue] = useControllableState({
|
|
2185
|
+
value: open,
|
|
2186
|
+
defaultValue: defaultOpen,
|
|
2187
|
+
fallback
|
|
2188
|
+
});
|
|
2189
|
+
return [value, (next, details) => {
|
|
2190
|
+
onOpenChange?.(next, details);
|
|
2191
|
+
if (!details.isCanceled) setValue(next);
|
|
2192
|
+
}];
|
|
2193
|
+
}
|
|
2194
|
+
function parseJson(value) {
|
|
2195
|
+
if (typeof value !== "string") return value;
|
|
2196
|
+
try {
|
|
2197
|
+
return parsePartialJSON$1(value) ?? value;
|
|
2198
|
+
} catch {
|
|
2199
|
+
return value;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
function jsonBlock(value) {
|
|
2203
|
+
return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* A collapsible card for one tool call: header = tool name + state icon (no
|
|
2207
|
+
* words), body = the JSON input and output as Markdown code blocks, then
|
|
2208
|
+
* `children`.
|
|
2209
|
+
*/
|
|
2210
|
+
function ToolCallCard({ part, result, interrupt: _interrupt, streaming = false, open: openProp, defaultOpen, onOpenChange, children, className }) {
|
|
2211
|
+
const { labels } = usePartRenderers();
|
|
2212
|
+
const state = {
|
|
2213
|
+
pending: part.state === "awaiting-input" || part.state === "input-streaming" || part.state === "input-complete",
|
|
2214
|
+
approvalRequested: part.state === "approval-requested",
|
|
2215
|
+
approvalResponded: part.state === "approval-responded",
|
|
2216
|
+
complete: part.state === "complete",
|
|
2217
|
+
error: part.state === "error" || result?.state === "error"
|
|
2218
|
+
};
|
|
2219
|
+
const [open, setOpen] = useOpen({
|
|
2220
|
+
open: openProp,
|
|
2221
|
+
defaultOpen,
|
|
2222
|
+
onOpenChange
|
|
2223
|
+
}, state.approvalRequested);
|
|
2224
|
+
const manualRef = useRef(false);
|
|
2225
|
+
const requestedRef = useRef(state.approvalRequested);
|
|
2226
|
+
const openForApproval = useEffectEvent(() => {
|
|
2227
|
+
if (!manualRef.current) setOpen(true, createChangeEventDetails("none"));
|
|
2228
|
+
});
|
|
2229
|
+
useEffect(() => {
|
|
2230
|
+
const was = requestedRef.current;
|
|
2231
|
+
requestedRef.current = state.approvalRequested;
|
|
2232
|
+
if (state.approvalRequested && !was) openForApproval();
|
|
2233
|
+
}, [state.approvalRequested]);
|
|
2234
|
+
const stateLabel = state.error ? labels.toolFailed : state.complete ? labels.toolDone : state.approvalRequested ? labels.toolApprovalRequested : state.approvalResponded ? part.approval?.approved === false ? labels.toolDenied : labels.toolApproved : part.state === "input-complete" ? labels.toolRunning : labels.toolPending;
|
|
2235
|
+
const input = part.input ?? (part.arguments === "" ? void 0 : parseJson(part.arguments));
|
|
2236
|
+
const output = part.output ?? result?.content;
|
|
2237
|
+
const error = result?.error;
|
|
2238
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
2239
|
+
"data-slot": "tool-call-card",
|
|
2240
|
+
"data-pending": dataAttr(state.pending),
|
|
2241
|
+
"data-approval-requested": dataAttr(state.approvalRequested),
|
|
2242
|
+
"data-approval-responded": dataAttr(state.approvalResponded),
|
|
2243
|
+
"data-complete": dataAttr(state.complete),
|
|
2244
|
+
"data-error": dataAttr(state.error),
|
|
2245
|
+
"data-streaming": dataAttr(streaming),
|
|
2246
|
+
open,
|
|
2247
|
+
onOpenChange: (next, details) => {
|
|
2248
|
+
manualRef.current = true;
|
|
2249
|
+
setOpen(next, details);
|
|
2250
|
+
},
|
|
2251
|
+
className: cn("rounded-lg border bg-card text-sm text-card-foreground", className),
|
|
2252
|
+
children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
2253
|
+
"data-slot": "tool-call-card-trigger",
|
|
2254
|
+
className: "\n flex items-center gap-2 px-3 py-2 text-start inline-full\n hover:bg-muted/50\n ",
|
|
2255
|
+
children: [
|
|
2256
|
+
/* @__PURE__ */ jsx("span", {
|
|
2257
|
+
"data-slot": "tool-call-name",
|
|
2258
|
+
className: "flex-1 truncate font-mono min-inline-0",
|
|
2259
|
+
children: part.name
|
|
2260
|
+
}),
|
|
2261
|
+
/* @__PURE__ */ jsx("span", {
|
|
2262
|
+
className: "sr-only",
|
|
2263
|
+
"data-slot": "tool-call-state",
|
|
2264
|
+
children: stateLabel
|
|
2265
|
+
}),
|
|
2266
|
+
state.pending && /* @__PURE__ */ jsx(Spinner, {
|
|
2267
|
+
"aria-hidden": true,
|
|
2268
|
+
className: "block-[1em] inline-[1em]"
|
|
2269
|
+
}),
|
|
2270
|
+
state.approvalRequested && /* @__PURE__ */ jsx(IconClock, {
|
|
2271
|
+
"aria-hidden": true,
|
|
2272
|
+
className: "block-[1em] inline-[1em]"
|
|
2273
|
+
}),
|
|
2274
|
+
state.complete && /* @__PURE__ */ jsx(IconCheck, {
|
|
2275
|
+
"aria-hidden": true,
|
|
2276
|
+
className: "block-[1em] inline-[1em]"
|
|
2277
|
+
}),
|
|
2278
|
+
state.error && /* @__PURE__ */ jsx(IconX, {
|
|
2279
|
+
"aria-hidden": true,
|
|
2280
|
+
className: "text-destructive block-[1em] inline-[1em]"
|
|
2281
|
+
})
|
|
2282
|
+
]
|
|
2283
|
+
}), /* @__PURE__ */ jsx(CollapsiblePanel, {
|
|
2284
|
+
keepMounted: true,
|
|
2285
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2286
|
+
className: "flex flex-col gap-2 border-bs px-3 py-2",
|
|
2287
|
+
children: [
|
|
2288
|
+
input !== void 0 && /* @__PURE__ */ jsx(Markdown, {
|
|
2289
|
+
content: jsonBlock(input),
|
|
2290
|
+
streaming: state.pending && streaming
|
|
2291
|
+
}),
|
|
2292
|
+
output !== void 0 && /* @__PURE__ */ jsx(Markdown, { content: jsonBlock(parseJson(output)) }),
|
|
2293
|
+
error !== void 0 && /* @__PURE__ */ jsx("p", {
|
|
2294
|
+
"data-slot": "tool-call-error",
|
|
2295
|
+
className: "text-destructive",
|
|
2296
|
+
children: error
|
|
2297
|
+
}),
|
|
2298
|
+
children
|
|
2299
|
+
]
|
|
2300
|
+
})
|
|
2301
|
+
})]
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
/** The group's title — a `CollapsibleTrigger`, wording from the caller. */
|
|
2305
|
+
function ToolCallGroupTrigger({ className, ...props }) {
|
|
2306
|
+
return /* @__PURE__ */ jsx(CollapsibleTrigger, {
|
|
2307
|
+
"data-slot": "tool-call-group-trigger",
|
|
2308
|
+
className: cn(`text-sm text-muted-foreground`, className),
|
|
2309
|
+
...props
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Folds a run of consecutive tool calls (spec G6). Position contract like
|
|
2314
|
+
* `DialogTrigger`: the first child is the `ToolCallGroupTrigger`, the rest
|
|
2315
|
+
* go into the panel.
|
|
2316
|
+
*/
|
|
2317
|
+
function ToolCallGroup({ count, children, open: openProp, defaultOpen, onOpenChange, className }) {
|
|
2318
|
+
const [open, setOpen] = useOpen({
|
|
2319
|
+
open: openProp,
|
|
2320
|
+
defaultOpen,
|
|
2321
|
+
onOpenChange
|
|
2322
|
+
});
|
|
2323
|
+
const [trigger, ...body] = Children.toArray(children);
|
|
2324
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
2325
|
+
"data-slot": "tool-call-group",
|
|
2326
|
+
"data-count": count,
|
|
2327
|
+
open,
|
|
2328
|
+
onOpenChange: setOpen,
|
|
2329
|
+
className: cn("flex flex-col", className),
|
|
2330
|
+
children: [trigger, /* @__PURE__ */ jsx(CollapsiblePanel, {
|
|
2331
|
+
keepMounted: true,
|
|
2332
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2333
|
+
className: "flex flex-col gap-2 pbs-2",
|
|
2334
|
+
children: body
|
|
2335
|
+
})
|
|
2336
|
+
})]
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
//#endregion
|
|
2340
|
+
//#region src/view/transcript.tsx
|
|
2341
|
+
const TranscriptContext = createContext(null);
|
|
2342
|
+
if (process.env.NODE_ENV !== "production") TranscriptContext.displayName = "TranscriptContext";
|
|
2343
|
+
const NO_INTERRUPTS = [];
|
|
2344
|
+
/** Hands `useChat()`'s run state to every Transcript part below it. */
|
|
2345
|
+
function TranscriptProvider({ status, interrupts = NO_INTERRUPTS, addToolApprovalResponse, children }) {
|
|
2346
|
+
const value = useMemo(() => ({
|
|
2347
|
+
status,
|
|
2348
|
+
interrupts,
|
|
2349
|
+
addToolApprovalResponse
|
|
2350
|
+
}), [
|
|
2351
|
+
status,
|
|
2352
|
+
interrupts,
|
|
2353
|
+
addToolApprovalResponse
|
|
2354
|
+
]);
|
|
2355
|
+
return /* @__PURE__ */ jsx(TranscriptContext, {
|
|
2356
|
+
value,
|
|
2357
|
+
children
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
function useTranscript() {
|
|
2361
|
+
const value = use(TranscriptContext);
|
|
2362
|
+
if (value === null) throw new Error("cadenza-ai: TranscriptContext is missing. Transcript parts must be placed within <TranscriptProvider>.");
|
|
2363
|
+
return value;
|
|
2364
|
+
}
|
|
2365
|
+
const TranscriptFrameContext = createContext({
|
|
2366
|
+
anchorTurns: true,
|
|
2367
|
+
anchorId: null
|
|
2368
|
+
});
|
|
2369
|
+
if (process.env.NODE_ENV !== "production") TranscriptFrameContext.displayName = "TranscriptFrameContext";
|
|
2370
|
+
/** Ran on the provider's side (Anthropic's or DeepSeek's server search), by the convention adapters stamp on the call. */
|
|
2371
|
+
function providerExecuted(part) {
|
|
2372
|
+
return part.metadata?.providerExecuted === true;
|
|
2373
|
+
}
|
|
2374
|
+
/** The scrolling frame: `MessageScroller` with the house viewport, one row per `TranscriptMessage`. */
|
|
2375
|
+
function Transcript({ children, autoScroll = true, defaultScrollPosition = "end", anchorTurns = true, previousPeek, after, className, ...viewport }) {
|
|
2376
|
+
useTranscript();
|
|
2377
|
+
const anchorId = useMemo(() => {
|
|
2378
|
+
let id = null;
|
|
2379
|
+
for (const child of Children.toArray(children)) if (isValidElement(child) && child.props.message?.role === "user") id = child.props.message.id;
|
|
2380
|
+
return id;
|
|
2381
|
+
}, [children]);
|
|
2382
|
+
const frame = useMemo(() => ({
|
|
2383
|
+
anchorTurns,
|
|
2384
|
+
anchorId
|
|
2385
|
+
}), [anchorTurns, anchorId]);
|
|
2386
|
+
return /* @__PURE__ */ jsx(TranscriptFrameContext, {
|
|
2387
|
+
value: frame,
|
|
2388
|
+
children: /* @__PURE__ */ jsx(MessageScrollerProvider, {
|
|
2389
|
+
autoScroll,
|
|
2390
|
+
defaultScrollPosition,
|
|
2391
|
+
scrollPreviousItemPeek: previousPeek,
|
|
2392
|
+
children: /* @__PURE__ */ jsxs(MessageScroller, {
|
|
2393
|
+
"data-slot": "transcript",
|
|
2394
|
+
className: cn(`relative flex flex-1 flex-col min-block-0`, className),
|
|
2395
|
+
children: [/* @__PURE__ */ jsx(MessageScrollerViewport, {
|
|
2396
|
+
...viewport,
|
|
2397
|
+
className: "*:[[role=presentation]]:min-inline-0!",
|
|
2398
|
+
children: /* @__PURE__ */ jsx(MessageScrollerContent, {
|
|
2399
|
+
className: "flex flex-col gap-6 p-4",
|
|
2400
|
+
children
|
|
2401
|
+
})
|
|
2402
|
+
}), after]
|
|
2403
|
+
})
|
|
2404
|
+
})
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
function TranscriptMessageImpl({ message, align, streaming = false, children, className }) {
|
|
2408
|
+
const user = message.role === "user";
|
|
2409
|
+
const { anchorTurns, anchorId } = use(TranscriptFrameContext);
|
|
2410
|
+
const anchor = anchorTurns && user && (anchorId === null || anchorId === message.id);
|
|
2411
|
+
const parts = [];
|
|
2412
|
+
const actions = [];
|
|
2413
|
+
Children.forEach(children, (child) => {
|
|
2414
|
+
if (isValidElement(child) && child.type === TranscriptActions) actions.push(child);
|
|
2415
|
+
else if (child !== null && child !== void 0 && child !== false) parts.push(child);
|
|
2416
|
+
});
|
|
2417
|
+
return /* @__PURE__ */ jsx(MessageScrollerItem, {
|
|
2418
|
+
messageId: message.id,
|
|
2419
|
+
scrollAnchor: anchor,
|
|
2420
|
+
"data-slot": "transcript-message",
|
|
2421
|
+
"data-role": message.role,
|
|
2422
|
+
"data-streaming": dataAttr(streaming),
|
|
2423
|
+
className: cn("[content-visibility:visible]", className),
|
|
2424
|
+
children: /* @__PURE__ */ jsx(Message, {
|
|
2425
|
+
align: align ?? (user ? "end" : "start"),
|
|
2426
|
+
children: /* @__PURE__ */ jsxs(MessageContent, { children: [/* @__PURE__ */ jsx(Bubble, {
|
|
2427
|
+
variant: user ? "muted" : "ghost",
|
|
2428
|
+
className: user ? void 0 : `min-inline-full`,
|
|
2429
|
+
children: /* @__PURE__ */ jsx(BubbleContent, {
|
|
2430
|
+
className: user ? void 0 : "min-inline-full",
|
|
2431
|
+
children: parts.length > 0 ? parts : /* @__PURE__ */ jsx(TranscriptParts, { message })
|
|
2432
|
+
})
|
|
2433
|
+
}), actions] })
|
|
2434
|
+
})
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
/** One row. Memoised so a streaming row re-renders alone. */
|
|
2438
|
+
const TranscriptMessage = memo(TranscriptMessageImpl);
|
|
2439
|
+
if (process.env.NODE_ENV !== "production") TranscriptMessage.displayName = "TranscriptMessage";
|
|
2440
|
+
function isApproval(interrupt) {
|
|
2441
|
+
return interrupt.kind === "tool-approval";
|
|
2442
|
+
}
|
|
2443
|
+
/** Dispatches every part of a message through the renderer registry and the default parts. */
|
|
2444
|
+
function TranscriptParts({ message, className }) {
|
|
2445
|
+
const { renderers, labels } = usePartRenderers();
|
|
2446
|
+
const { status, interrupts, addToolApprovalResponse } = useTranscript();
|
|
2447
|
+
const results = useMemo(() => {
|
|
2448
|
+
const map = /* @__PURE__ */ new Map();
|
|
2449
|
+
for (const part of message.parts) if (part.type === "tool-result") map.set(part.toolCallId, part);
|
|
2450
|
+
return map;
|
|
2451
|
+
}, [message.parts]);
|
|
2452
|
+
const lastIndex = message.parts.length - 1;
|
|
2453
|
+
const renderTool = (part, index) => {
|
|
2454
|
+
const result = results.get(part.id);
|
|
2455
|
+
const interrupt = interrupts.filter(isApproval).find((i) => i.toolCallId === part.id);
|
|
2456
|
+
const streaming = status === "streaming" && index === lastIndex;
|
|
2457
|
+
const custom = renderers.toolCall?.[part.name] ?? renderers.toolCall?.default;
|
|
2458
|
+
if (custom) return custom({
|
|
2459
|
+
part,
|
|
2460
|
+
result,
|
|
2461
|
+
interrupt,
|
|
2462
|
+
streaming
|
|
2463
|
+
});
|
|
2464
|
+
let approval = null;
|
|
2465
|
+
if (part.state === "approval-requested") {
|
|
2466
|
+
if (interrupt) approval = /* @__PURE__ */ jsxs(ApprovalActions, {
|
|
2467
|
+
interrupt,
|
|
2468
|
+
children: [/* @__PURE__ */ jsx(ApprovalApprove, { children: labels.approve }), /* @__PURE__ */ jsx(ApprovalDeny, { children: labels.deny })]
|
|
2469
|
+
});
|
|
2470
|
+
else if (part.approval && addToolApprovalResponse) {
|
|
2471
|
+
const id = part.approval.id;
|
|
2472
|
+
approval = /* @__PURE__ */ jsxs("div", {
|
|
2473
|
+
role: "group",
|
|
2474
|
+
"data-slot": "approval-actions",
|
|
2475
|
+
className: "flex gap-2",
|
|
2476
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2477
|
+
size: "sm",
|
|
2478
|
+
onClick: () => void addToolApprovalResponse({
|
|
2479
|
+
id,
|
|
2480
|
+
approved: true
|
|
2481
|
+
}),
|
|
2482
|
+
children: labels.approve
|
|
2483
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2484
|
+
size: "sm",
|
|
2485
|
+
variant: "outline",
|
|
2486
|
+
onClick: () => void addToolApprovalResponse({
|
|
2487
|
+
id,
|
|
2488
|
+
approved: false
|
|
2489
|
+
}),
|
|
2490
|
+
children: labels.deny
|
|
2491
|
+
})]
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return /* @__PURE__ */ jsx(ToolCallCard, {
|
|
2496
|
+
part,
|
|
2497
|
+
result,
|
|
2498
|
+
interrupt,
|
|
2499
|
+
streaming,
|
|
2500
|
+
children: approval
|
|
2501
|
+
});
|
|
2502
|
+
};
|
|
2503
|
+
/**
|
|
2504
|
+
* The `tool-result` parts a tool-call scan walked past. They carry no node of
|
|
2505
|
+
* their own by default — the card renders the output — but a caller who
|
|
2506
|
+
* registers `renderers.toolResult` has asked for one, and the scan used to
|
|
2507
|
+
* advance `index` straight over them so that renderer was never reached.
|
|
2508
|
+
*/
|
|
2509
|
+
const walkedResults = (from, to) => {
|
|
2510
|
+
const render = renderers.toolResult;
|
|
2511
|
+
if (!render) return [];
|
|
2512
|
+
const out = [];
|
|
2513
|
+
for (let i = from; i < to; i += 1) {
|
|
2514
|
+
const part = message.parts[i];
|
|
2515
|
+
if (part.type === "tool-result") out.push(/* @__PURE__ */ jsx("div", { children: render({ part }) }, `tool-result-${i}`));
|
|
2516
|
+
}
|
|
2517
|
+
return out;
|
|
2518
|
+
};
|
|
2519
|
+
const nodes = [];
|
|
2520
|
+
let index = 0;
|
|
2521
|
+
while (index <= lastIndex) {
|
|
2522
|
+
const part = message.parts[index];
|
|
2523
|
+
if (part.type === "tool-call") {
|
|
2524
|
+
if (providerExecuted(part)) {
|
|
2525
|
+
nodes.push(/* @__PURE__ */ jsx("div", { children: renderTool(part, index) }, part.id));
|
|
2526
|
+
let next = index + 1;
|
|
2527
|
+
while (next <= lastIndex && message.parts[next].type === "tool-result") next += 1;
|
|
2528
|
+
nodes.push(...walkedResults(index + 1, next));
|
|
2529
|
+
index = next;
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
const run = [];
|
|
2533
|
+
let cursor = index;
|
|
2534
|
+
while (cursor <= lastIndex) {
|
|
2535
|
+
const next = message.parts[cursor];
|
|
2536
|
+
if (next.type === "tool-call") {
|
|
2537
|
+
if (providerExecuted(next)) break;
|
|
2538
|
+
run.push([next, cursor]);
|
|
2539
|
+
} else if (next.type !== "tool-result") break;
|
|
2540
|
+
cursor += 1;
|
|
2541
|
+
}
|
|
2542
|
+
const cards = run.map(([p, i]) => /* @__PURE__ */ jsx("div", { children: renderTool(p, i) }, p.id));
|
|
2543
|
+
nodes.push(run.length > 1 ? /* @__PURE__ */ jsxs(ToolCallGroup, {
|
|
2544
|
+
count: run.length,
|
|
2545
|
+
children: [/* @__PURE__ */ jsx(ToolCallGroupTrigger, { children: labels.toolGroup(run.length) }), cards]
|
|
2546
|
+
}, `group-${index}`) : cards[0]);
|
|
2547
|
+
nodes.push(...walkedResults(index, cursor));
|
|
2548
|
+
index = cursor;
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2551
|
+
const key = `${part.type}-${index}`;
|
|
2552
|
+
switch (part.type) {
|
|
2553
|
+
case "text": {
|
|
2554
|
+
const streaming = status === "streaming" && index === lastIndex;
|
|
2555
|
+
nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.text ? renderers.text({
|
|
2556
|
+
part,
|
|
2557
|
+
message,
|
|
2558
|
+
streaming
|
|
2559
|
+
}) : /* @__PURE__ */ jsx(Markdown, {
|
|
2560
|
+
content: part.content,
|
|
2561
|
+
streaming
|
|
2562
|
+
}) }, key));
|
|
2563
|
+
break;
|
|
2564
|
+
}
|
|
2565
|
+
case "thinking": {
|
|
2566
|
+
const complete = isThinkingComplete(message, index, status);
|
|
2567
|
+
nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.thinking ? renderers.thinking({
|
|
2568
|
+
part,
|
|
2569
|
+
complete
|
|
2570
|
+
}) : /* @__PURE__ */ jsx(Reasoning, {
|
|
2571
|
+
content: part.content,
|
|
2572
|
+
complete,
|
|
2573
|
+
children: complete ? labels.thought : labels.thinking
|
|
2574
|
+
}) }, key));
|
|
2575
|
+
break;
|
|
2576
|
+
}
|
|
2577
|
+
case "tool-result":
|
|
2578
|
+
if (renderers.toolResult) nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.toolResult({ part }) }, key));
|
|
2579
|
+
break;
|
|
2580
|
+
case "image":
|
|
2581
|
+
nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.image ? renderers.image({
|
|
2582
|
+
part,
|
|
2583
|
+
message
|
|
2584
|
+
}) : /* @__PURE__ */ jsx(MediaPart, { part }) }, key));
|
|
2585
|
+
break;
|
|
2586
|
+
case "audio":
|
|
2587
|
+
nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.audio ? renderers.audio({
|
|
2588
|
+
part,
|
|
2589
|
+
message
|
|
2590
|
+
}) : /* @__PURE__ */ jsx(MediaPart, { part }) }, key));
|
|
2591
|
+
break;
|
|
2592
|
+
case "video":
|
|
2593
|
+
nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.video ? renderers.video({
|
|
2594
|
+
part,
|
|
2595
|
+
message
|
|
2596
|
+
}) : /* @__PURE__ */ jsx(MediaPart, { part }) }, key));
|
|
2597
|
+
break;
|
|
2598
|
+
case "document":
|
|
2599
|
+
nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.document ? renderers.document({
|
|
2600
|
+
part,
|
|
2601
|
+
message
|
|
2602
|
+
}) : /* @__PURE__ */ jsx(MediaPart, { part }) }, key));
|
|
2603
|
+
break;
|
|
2604
|
+
case "structured-output":
|
|
2605
|
+
nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.structuredOutput ? renderers.structuredOutput({ part }) : /* @__PURE__ */ jsx(StructuredOutput, { part }) }, key));
|
|
2606
|
+
break;
|
|
2607
|
+
case "ui-resource": if (renderers.uiResource) nodes.push(/* @__PURE__ */ jsx("div", { children: renderers.uiResource({ part }) }, key));
|
|
2608
|
+
}
|
|
2609
|
+
index += 1;
|
|
2610
|
+
}
|
|
2611
|
+
const sources = useMemo(() => sourcesOf(message), [message]);
|
|
2612
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2613
|
+
"data-slot": "transcript-parts",
|
|
2614
|
+
className: cn("flex flex-col gap-3", className),
|
|
2615
|
+
children: [nodes, sources.length > 0 && /* @__PURE__ */ jsx(Sources, {
|
|
2616
|
+
sources,
|
|
2617
|
+
children: labels.sources(sources.length)
|
|
2618
|
+
})]
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
/** The row's toolbar, hidden while a run streams. Place it as a child of `TranscriptMessage`. */
|
|
2622
|
+
function TranscriptActions({ children, className }) {
|
|
2623
|
+
const { status } = useTranscript();
|
|
2624
|
+
return /* @__PURE__ */ jsx(MessageFooter, { children: /* @__PURE__ */ jsx("div", {
|
|
2625
|
+
role: "toolbar",
|
|
2626
|
+
"data-slot": "transcript-actions",
|
|
2627
|
+
"data-hidden": dataAttr(status === "streaming"),
|
|
2628
|
+
className: cn(`
|
|
2629
|
+
flex items-center gap-1
|
|
2630
|
+
data-hidden:invisible
|
|
2631
|
+
`, className),
|
|
2632
|
+
children
|
|
2633
|
+
}) });
|
|
2634
|
+
}
|
|
2635
|
+
function TranscriptAction({ className, ...props }) {
|
|
2636
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
2637
|
+
variant: "ghost",
|
|
2638
|
+
size: "icon-xs",
|
|
2639
|
+
"data-slot": "transcript-action",
|
|
2640
|
+
className,
|
|
2641
|
+
...props
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
function TranscriptEmpty(props) {
|
|
2645
|
+
return /* @__PURE__ */ jsx(Empty, {
|
|
2646
|
+
"data-slot": "transcript-empty",
|
|
2647
|
+
...props
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
/** Shown between submit and the first chunk. */
|
|
2651
|
+
function TranscriptPending({ children, className }) {
|
|
2652
|
+
return /* @__PURE__ */ jsx(Marker, {
|
|
2653
|
+
role: "status",
|
|
2654
|
+
"data-slot": "transcript-pending",
|
|
2655
|
+
className,
|
|
2656
|
+
children: /* @__PURE__ */ jsx(MarkerContent, {
|
|
2657
|
+
className: "shimmer",
|
|
2658
|
+
children
|
|
2659
|
+
})
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
function TranscriptError({ error, children, className }) {
|
|
2663
|
+
const code = error.code;
|
|
2664
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2665
|
+
role: "alert",
|
|
2666
|
+
"data-slot": "transcript-error",
|
|
2667
|
+
"data-code": typeof code === "string" ? code : void 0,
|
|
2668
|
+
className: cn(`
|
|
2669
|
+
rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2
|
|
2670
|
+
text-sm text-destructive
|
|
2671
|
+
`, className),
|
|
2672
|
+
children
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
//#endregion
|
|
2676
|
+
//#region src/view/usage-stats.tsx
|
|
2677
|
+
const DEFAULT_USAGE_STATS_LABELS = {
|
|
2678
|
+
cacheHit: "Cache hit",
|
|
2679
|
+
cacheWrite: "Cache write",
|
|
2680
|
+
outputRatio: "Out per in",
|
|
2681
|
+
reasoningShare: "Reasoning",
|
|
2682
|
+
contextUsed: "Context used",
|
|
2683
|
+
cacheSaved: "Saved by cache",
|
|
2684
|
+
promptModalities: "Prompt by modality",
|
|
2685
|
+
completionModalities: "Completion by modality",
|
|
2686
|
+
modality: {
|
|
2687
|
+
text: "Text",
|
|
2688
|
+
image: "Image",
|
|
2689
|
+
audio: "Audio",
|
|
2690
|
+
video: "Video",
|
|
2691
|
+
document: "Document"
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
const PERCENT = new Intl.NumberFormat("en-US", {
|
|
2695
|
+
style: "percent",
|
|
2696
|
+
maximumFractionDigits: 1
|
|
2697
|
+
});
|
|
2698
|
+
const USD = new Intl.NumberFormat("en-US", {
|
|
2699
|
+
style: "currency",
|
|
2700
|
+
currency: "USD",
|
|
2701
|
+
maximumFractionDigits: 4
|
|
2702
|
+
});
|
|
2703
|
+
function Tile({ label, value, bar, children }) {
|
|
2704
|
+
const id = useId();
|
|
2705
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2706
|
+
"data-slot": "usage-stat",
|
|
2707
|
+
className: "flex flex-col gap-1 rounded-md border p-3",
|
|
2708
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
2709
|
+
id,
|
|
2710
|
+
className: "text-xs text-muted-foreground",
|
|
2711
|
+
children: label
|
|
2712
|
+
}), /* @__PURE__ */ jsxs("dd", {
|
|
2713
|
+
className: "flex flex-col gap-1.5",
|
|
2714
|
+
children: [
|
|
2715
|
+
/* @__PURE__ */ jsx("span", {
|
|
2716
|
+
className: "text-lg tabular-nums",
|
|
2717
|
+
children: value
|
|
2718
|
+
}),
|
|
2719
|
+
bar !== void 0 && /* @__PURE__ */ jsx(Progress, {
|
|
2720
|
+
value: Math.min(bar, 1) * 100,
|
|
2721
|
+
"aria-labelledby": id
|
|
2722
|
+
}),
|
|
2723
|
+
children
|
|
2724
|
+
]
|
|
2725
|
+
})]
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
function Breakdown({ slices, labels }) {
|
|
2729
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2730
|
+
className: "flex flex-col gap-0.5 text-xs text-muted-foreground",
|
|
2731
|
+
children: slices.map((slice) => /* @__PURE__ */ jsxs("span", {
|
|
2732
|
+
className: "flex justify-between gap-2 tabular-nums",
|
|
2733
|
+
children: [/* @__PURE__ */ jsx("span", { children: labels.modality[slice.kind] }), /* @__PURE__ */ jsx("span", { children: PERCENT.format(slice.share) })]
|
|
2734
|
+
}, slice.kind))
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* The ratios inside a `TokenUsage`, one tile each: how much of the prompt came
|
|
2739
|
+
* from cache, how much answer a prompt bought, how much of the answer was
|
|
2740
|
+
* thinking, how full the context is, and what the cache saved.
|
|
2741
|
+
*
|
|
2742
|
+
* Every tile is skipped when its number is unknown — a rate with no denominator
|
|
2743
|
+
* is not zero — so a provider that reports no cache detail simply shows fewer
|
|
2744
|
+
* tiles rather than a row of 0%.
|
|
2745
|
+
*
|
|
2746
|
+
* It renders one `usage`. Which one is the caller's to decide: `useUsageTracker`
|
|
2747
|
+
* hands over `total`, `lastRun` and `byMessage`, and a switch between them is a
|
|
2748
|
+
* few lines the caller writes anyway, keeping that wording and those variants
|
|
2749
|
+
* where they belong.
|
|
2750
|
+
*/
|
|
2751
|
+
function UsageStats({ usage, model, labels: labelsProp, className }) {
|
|
2752
|
+
const labels = useMemo(() => ({
|
|
2753
|
+
...DEFAULT_USAGE_STATS_LABELS,
|
|
2754
|
+
...labelsProp
|
|
2755
|
+
}), [labelsProp]);
|
|
2756
|
+
const m = useMemo(() => usageMetrics(usage, model), [usage, model]);
|
|
2757
|
+
return /* @__PURE__ */ jsxs("dl", {
|
|
2758
|
+
"data-slot": "usage-stats",
|
|
2759
|
+
className: cn("grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-2", className),
|
|
2760
|
+
children: [
|
|
2761
|
+
m.cacheHitRate !== void 0 && /* @__PURE__ */ jsx(Tile, {
|
|
2762
|
+
label: labels.cacheHit,
|
|
2763
|
+
value: PERCENT.format(m.cacheHitRate),
|
|
2764
|
+
bar: m.cacheHitRate
|
|
2765
|
+
}),
|
|
2766
|
+
m.cacheWriteRate !== void 0 && /* @__PURE__ */ jsx(Tile, {
|
|
2767
|
+
label: labels.cacheWrite,
|
|
2768
|
+
value: PERCENT.format(m.cacheWriteRate),
|
|
2769
|
+
bar: m.cacheWriteRate
|
|
2770
|
+
}),
|
|
2771
|
+
m.outputRatio !== void 0 && /* @__PURE__ */ jsx(Tile, {
|
|
2772
|
+
label: labels.outputRatio,
|
|
2773
|
+
value: `${m.outputRatio.toFixed(2)}×`
|
|
2774
|
+
}),
|
|
2775
|
+
m.reasoningShare !== void 0 && /* @__PURE__ */ jsx(Tile, {
|
|
2776
|
+
label: labels.reasoningShare,
|
|
2777
|
+
value: PERCENT.format(m.reasoningShare),
|
|
2778
|
+
bar: m.reasoningShare
|
|
2779
|
+
}),
|
|
2780
|
+
m.contextRatio !== void 0 && /* @__PURE__ */ jsx(Tile, {
|
|
2781
|
+
label: labels.contextUsed,
|
|
2782
|
+
value: PERCENT.format(m.contextRatio),
|
|
2783
|
+
bar: m.contextRatio
|
|
2784
|
+
}),
|
|
2785
|
+
m.cacheSavings !== void 0 && /* @__PURE__ */ jsx(Tile, {
|
|
2786
|
+
label: labels.cacheSaved,
|
|
2787
|
+
value: USD.format(m.cacheSavings)
|
|
2788
|
+
}),
|
|
2789
|
+
m.promptModalities.length > 1 && /* @__PURE__ */ jsx(Tile, {
|
|
2790
|
+
label: labels.promptModalities,
|
|
2791
|
+
value: String(usage.promptTokens),
|
|
2792
|
+
children: /* @__PURE__ */ jsx(Breakdown, {
|
|
2793
|
+
slices: m.promptModalities,
|
|
2794
|
+
labels
|
|
2795
|
+
})
|
|
2796
|
+
}),
|
|
2797
|
+
m.completionModalities.length > 1 && /* @__PURE__ */ jsx(Tile, {
|
|
2798
|
+
label: labels.completionModalities,
|
|
2799
|
+
value: String(usage.completionTokens),
|
|
2800
|
+
children: /* @__PURE__ */ jsx(Breakdown, {
|
|
2801
|
+
slices: m.completionModalities,
|
|
2802
|
+
labels
|
|
2803
|
+
})
|
|
2804
|
+
})
|
|
2805
|
+
]
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
//#endregion
|
|
2809
|
+
export { ApprovalActions, ApprovalApprove, ApprovalDeny, ByokKeyDialog, ByokKeyDialogProvider, Composer, ComposerAttach, ComposerAttachments, ComposerDictate, ComposerSubmit, ComposerTextarea, ComposerToolbar, ContextUsage, DEFAULT_BYOK_KEY_DIALOG_LABELS, DEFAULT_CONTEXT_USAGE_LABELS, DEFAULT_MAX_ATTACHMENT_BYTES, DEFAULT_PART_LABELS, DEFAULT_USAGE_STATS_LABELS, EventType, Markdown, MediaPart, ModelPicker, PartRenderersProvider, QueueList, Reasoning, SearchToggle, Sources, StructuredOutput, Suggestions, SuggestionsItem, THINKING_LEVELS, ThinkingLevelPicker, ThinkingToggle, ThreadList, ThreadListArchive, ThreadListDelete, ThreadListGroup, ThreadListGroupLabel, ThreadListItem, ThreadListNew, ThreadListRename, ToolCallCard, ToolCallGroup, ToolCallGroupTrigger, Transcript, TranscriptAction, TranscriptActions, TranscriptEmpty, TranscriptError, TranscriptMessage, TranscriptParts, TranscriptPending, TranscriptProvider, UsageStats, addTokenUsage, attachmentKindOf, clampThinkingLevel, createByok, createCatalog, createThreadIndex, defaultByokStorage, defaultCatalog, defineByok, definePartRenderers, editAndResend, estimateCost, fileToContentPart, fromSpecTokenUsage, generateMessageId, groupThreadsByDay, isByokError, isPasskeyStorageSupported, isThinkingComplete, memoryStorage, messageText, messagesToMarkdown, modelRef, newThreadId, parseModelRef, parsePartialJSON, passkeyStorage, providers, sourcesOf, supportedThinkingLevels, threadPersistence, threadTitleFrom, toolDefinition, usageMetrics, useAttachmentDraft, useComposer, useModelSelection, usePartRenderers, useServerCoverage, useStoredState, useThreadIndex, useThreadListItem, useTranscript, useUsageTracker };
|