agentic-ui-kit 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.js +2 -2
- package/dist/index.js +910 -36
- package/dist/{src-D01l3nBc.js → src-BrPYkHdt.js} +285 -1
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +6 -0
- package/dist/types/core/src/ask-user.d.ts +81 -0
- package/dist/types/core/src/index.d.ts +4 -0
- package/dist/types/core/src/ui-components.d.ts +103 -0
- package/dist/types/react/src/components/activity-group.d.ts +1 -1
- package/dist/types/react/src/components/ask-user.d.ts +17 -0
- package/dist/types/react/src/components/chat-list.d.ts +12 -0
- package/dist/types/react/src/components/markdown.d.ts +17 -1
- package/dist/types/react/src/components/tool-prompt.d.ts +14 -0
- package/dist/types/react/src/components/tools.d.ts +16 -0
- package/dist/types/react/src/components/ui-components.d.ts +42 -0
- package/dist/types/react/src/components/ui-tree.d.ts +15 -5
- package/dist/types/react/src/hooks/use-chat.d.ts +21 -1
- package/dist/types/react/src/index.d.ts +11 -6
- package/dist/types/react/src/primitives/chat-theme.d.ts +3 -1
- package/dist/types/react/src/primitives/icons.d.ts +10 -0
- package/dist/types/react/src/primitives/labels.d.ts +36 -0
- package/dist/types/react/src/primitives/panel.d.ts +3 -1
- package/dist/types/react/src/views/chat-view.d.ts +12 -2
- package/dist/types/react/src/views/sidebar.d.ts +4 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as widgetLoaderMessages, C as readAskUserResult, D as resolveReplaces, F as activityTokens, H as settleToolCall, I as applyEvent, L as awaitingToolCalls, M as readEventStream, P as toBlocks, S as readAskUserInput, W as salvageJson, b as uiActionSchema, c as parseUiMarkup, g as matchesSchema, h as isStateRef, r as uiSpecSet, w as replacedId } from "./src-BrPYkHdt.js";
|
|
3
3
|
import { clsx } from "clsx";
|
|
4
4
|
import { extendTailwindMerge } from "tailwind-merge";
|
|
5
5
|
import { Children, Component, Fragment, createContext, createElement, isValidElement, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
6
|
import { Autocomplete, Button as Button$1, Dialog, DialogTrigger, DialogTrigger as DialogTrigger$1, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, ToggleButton, ToggleButtonGroup, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, Mic, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
|
|
8
|
+
import { AlertTriangle, ArrowDown, ArrowRight, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, GripVertical, Info, MessageSquare, Mic, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
|
|
9
9
|
import { UNSAFE_PortalProvider } from "react-aria";
|
|
10
10
|
import { createMathPlugin } from "@streamdown/math";
|
|
11
11
|
import { Streamdown, defaultComponents, defaultRehypePlugins } from "streamdown";
|
|
@@ -204,6 +204,10 @@ const defaultIcons = {
|
|
|
204
204
|
info: /* @__PURE__ */ jsx(Info, {}),
|
|
205
205
|
warning: /* @__PURE__ */ jsx(AlertTriangle, {}),
|
|
206
206
|
approval: /* @__PURE__ */ jsx(ShieldQuestion, {}),
|
|
207
|
+
askPrevious: /* @__PURE__ */ jsx(ChevronLeft, {}),
|
|
208
|
+
askPagerNext: /* @__PURE__ */ jsx(ChevronRight, {}),
|
|
209
|
+
askNext: /* @__PURE__ */ jsx(ArrowRight, {}),
|
|
210
|
+
dragHandle: /* @__PURE__ */ jsx(GripVertical, {}),
|
|
207
211
|
web: /* @__PURE__ */ jsx(Globe, {}),
|
|
208
212
|
pin: /* @__PURE__ */ jsx(Pin, {}),
|
|
209
213
|
unpin: /* @__PURE__ */ jsx(PinOff, {}),
|
|
@@ -216,7 +220,8 @@ const defaultIcons = {
|
|
|
216
220
|
folder: /* @__PURE__ */ jsx(Folder, {}),
|
|
217
221
|
folderOpen: /* @__PURE__ */ jsx(FolderOpen, {}),
|
|
218
222
|
file: /* @__PURE__ */ jsx(FileText, {}),
|
|
219
|
-
remove: /* @__PURE__ */ jsx(X, {})
|
|
223
|
+
remove: /* @__PURE__ */ jsx(X, {}),
|
|
224
|
+
chatList: /* @__PURE__ */ jsx(MessageSquare, {})
|
|
220
225
|
};
|
|
221
226
|
const IconsContext = createContext(defaultIcons);
|
|
222
227
|
/** Merges `icons` over the icons of the enclosing provider (or the defaults). */
|
|
@@ -300,6 +305,24 @@ const defaultLabels = {
|
|
|
300
305
|
tokenCount: (count) => `${count} tokens`,
|
|
301
306
|
replyPlaceholder: "Reply…",
|
|
302
307
|
replyDirectly: "Or reply directly…",
|
|
308
|
+
askUserPrompt: "Question for you",
|
|
309
|
+
askUserPager: (current, total) => `${current} of ${total}`,
|
|
310
|
+
askUserPrevious: "Previous question",
|
|
311
|
+
askUserNext: "Next question",
|
|
312
|
+
askUserContinue: "Continue",
|
|
313
|
+
askUserSubmit: "Send answers",
|
|
314
|
+
askUserClose: "Close and decline",
|
|
315
|
+
askUserSomethingElse: "Something else",
|
|
316
|
+
askUserSkip: "Skip",
|
|
317
|
+
askUserSelected: (count) => `${count} selected`,
|
|
318
|
+
askUserRankHint: "Drag to re-order",
|
|
319
|
+
askUserReorder: (option) => `Move ${option}`,
|
|
320
|
+
askUserReorderKeys: "Press Alt with the up or down arrow to move",
|
|
321
|
+
askUserPicked: (options) => options.join(", "),
|
|
322
|
+
askUserRanked: (options) => `Ranked: ${options.map((o, i) => `${i + 1}. ${o}`).join(" ")}`,
|
|
323
|
+
askUserSkipped: "Skipped",
|
|
324
|
+
askUserDeclined: "You declined to answer",
|
|
325
|
+
askUserAnswers: "Your answers",
|
|
303
326
|
message: "Message",
|
|
304
327
|
stop: "Stop",
|
|
305
328
|
send: "Send",
|
|
@@ -718,7 +741,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
|
|
|
718
741
|
* A titled region that fills its container and lets the body scroll: a dock panel, an agent pane in a
|
|
719
742
|
* grid, a settings card. A `ChatView` placed in the body sizes itself to the panel.
|
|
720
743
|
*/
|
|
721
|
-
function Panel({ title, actions, footer, children, variant = "card", className, ...props }) {
|
|
744
|
+
function Panel({ title, actions, footer, children, variant = "card", padded = false, className, ...props }) {
|
|
722
745
|
return /* @__PURE__ */ jsxs("section", {
|
|
723
746
|
"aria-label": props["aria-label"],
|
|
724
747
|
className: cx("flex min-h-0 min-w-0 flex-col overflow-hidden bg-chat-bg font-chat-sans text-chat-fg", variant === "card" && "rounded-chat-md border border-chat-border", className),
|
|
@@ -734,7 +757,7 @@ function Panel({ title, actions, footer, children, variant = "card", className,
|
|
|
734
757
|
})]
|
|
735
758
|
}),
|
|
736
759
|
/* @__PURE__ */ jsx("div", {
|
|
737
|
-
className: "min-h-0 flex-1 overflow-y-auto [scrollbar-width:thin]",
|
|
760
|
+
className: cx("min-h-0 flex-1 overflow-y-auto [scrollbar-width:thin]", padded && "p-3"),
|
|
738
761
|
children
|
|
739
762
|
}),
|
|
740
763
|
footer && /* @__PURE__ */ jsx("footer", {
|
|
@@ -794,8 +817,25 @@ function useReducedMotion() {
|
|
|
794
817
|
return useCallback(() => shouldReduceMotion(motion), [motion]);
|
|
795
818
|
}
|
|
796
819
|
// src/primitives/highlight.ts
|
|
820
|
+
/**
|
|
821
|
+
* Shiki with its JavaScript regex engine, not the default Oniguruma WebAssembly one, so a page needs no
|
|
822
|
+
* `'wasm-unsafe-eval'` in its Content Security Policy. Languages and themes load on demand.
|
|
823
|
+
*/
|
|
797
824
|
let shiki;
|
|
798
|
-
const loadShiki = () => shiki ??=
|
|
825
|
+
const loadShiki = () => shiki ??= (async () => {
|
|
826
|
+
const [lib, { createJavaScriptRegexEngine }] = await Promise.all([import("shiki"), import("shiki/engine/javascript")]);
|
|
827
|
+
return {
|
|
828
|
+
lib,
|
|
829
|
+
highlighter: await lib.createHighlighter({
|
|
830
|
+
themes: [],
|
|
831
|
+
langs: [],
|
|
832
|
+
engine: createJavaScriptRegexEngine({ forgiving: true })
|
|
833
|
+
})
|
|
834
|
+
};
|
|
835
|
+
})().catch((error) => {
|
|
836
|
+
shiki = void 0;
|
|
837
|
+
throw error;
|
|
838
|
+
});
|
|
799
839
|
const cache = /* @__PURE__ */ new Map();
|
|
800
840
|
const warned = /* @__PURE__ */ new Set();
|
|
801
841
|
/** The theme id if Shiki bundles it; otherwise the default, with one console.warn per unknown id. */
|
|
@@ -808,6 +848,13 @@ function pick(id, fallback, known) {
|
|
|
808
848
|
}
|
|
809
849
|
return fallback;
|
|
810
850
|
}
|
|
851
|
+
let failedWarned = false;
|
|
852
|
+
/** One console.warn when highlighting breaks (a blocked chunk, a CSP violation), instead of silently showing plain code. */
|
|
853
|
+
function warnFailed(error) {
|
|
854
|
+
if (failedWarned) return;
|
|
855
|
+
failedWarned = true;
|
|
856
|
+
console.warn("agentic-ui-kit: code highlighting failed, showing plain code.", error);
|
|
857
|
+
}
|
|
811
858
|
/** Parses "--a:#fff;--b:#000" into an object. */
|
|
812
859
|
function parseVars(css) {
|
|
813
860
|
const vars = {};
|
|
@@ -821,15 +868,17 @@ async function highlight(code, language, light, dark) {
|
|
|
821
868
|
const key = `${light}\n${dark}\n${language}\n${code}`;
|
|
822
869
|
const hit = cache.get(key);
|
|
823
870
|
if (hit) return hit;
|
|
824
|
-
const {
|
|
871
|
+
const { lib: { bundledLanguages, bundledLanguagesAlias, bundledThemes }, highlighter } = await loadShiki();
|
|
825
872
|
const lang = language.toLowerCase();
|
|
826
873
|
if (!(lang in bundledLanguages) && !(lang in bundledLanguagesAlias)) return void 0;
|
|
827
|
-
const
|
|
874
|
+
const themes = {
|
|
875
|
+
light: pick(light, DEFAULT_CODE_THEMES.light, bundledThemes),
|
|
876
|
+
dark: pick(dark, DEFAULT_CODE_THEMES.dark, bundledThemes)
|
|
877
|
+
};
|
|
878
|
+
await Promise.all([highlighter.loadLanguage(lang), highlighter.loadTheme(themes.light, themes.dark)]);
|
|
879
|
+
const { tokens, bg, fg } = highlighter.codeToTokens(code, {
|
|
828
880
|
lang,
|
|
829
|
-
themes
|
|
830
|
-
light: pick(light, DEFAULT_CODE_THEMES.light, bundledThemes),
|
|
831
|
-
dark: pick(dark, DEFAULT_CODE_THEMES.dark, bundledThemes)
|
|
832
|
-
},
|
|
881
|
+
themes,
|
|
833
882
|
defaultColor: false
|
|
834
883
|
});
|
|
835
884
|
const result = {
|
|
@@ -854,7 +903,10 @@ function useHighlight(code, language) {
|
|
|
854
903
|
useEffect(() => {
|
|
855
904
|
if (!language) return setResult(void 0);
|
|
856
905
|
let cancelled = false;
|
|
857
|
-
highlight(code, language, light, dark).then((r) => !cancelled && setResult(r), () =>
|
|
906
|
+
highlight(code, language, light, dark).then((r) => !cancelled && setResult(r), (error) => {
|
|
907
|
+
warnFailed(error);
|
|
908
|
+
if (!cancelled) setResult(void 0);
|
|
909
|
+
});
|
|
858
910
|
return () => {
|
|
859
911
|
cancelled = true;
|
|
860
912
|
};
|
|
@@ -900,12 +952,15 @@ function CodeBlock({ code, language, label = language, variant = "plain", copy =
|
|
|
900
952
|
/**
|
|
901
953
|
* Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
|
|
902
954
|
* element instead of `<body>`, so they get the same theme. An app themed as a whole (variables on
|
|
903
|
-
* `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`.
|
|
955
|
+
* `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`. The element is
|
|
956
|
+
* `height: 100%` of its parent by default (so `ChatView` has a height without Tailwind); a `className` such
|
|
957
|
+
* as `h-screen`, or a style rule of your own, replaces that.
|
|
904
958
|
*/
|
|
905
959
|
function ChatTheme({ theme, codeThemes, motion, labels, icons, children, className }) {
|
|
906
960
|
const portal = useRef(null);
|
|
907
961
|
const getContainer = useCallback(() => portal.current, []);
|
|
908
962
|
return /* @__PURE__ */ jsxs("div", {
|
|
963
|
+
"data-chat-root": "",
|
|
909
964
|
"data-chat-theme": theme,
|
|
910
965
|
"data-chat-motion": motion,
|
|
911
966
|
className: clsx("bg-chat-bg font-chat-sans text-chat-fg", className),
|
|
@@ -1037,32 +1092,36 @@ function userMessage(text, parentId, attachments = []) {
|
|
|
1037
1092
|
};
|
|
1038
1093
|
}
|
|
1039
1094
|
const wantsApproval = (part, tool) => part.approval === void 0 && (part.needsApproval === true || (typeof tool?.approval === "function" ? tool.approval(part.input) : tool?.approval === true));
|
|
1095
|
+
const wantsPrompt = (part, promptTools, tool) => part.approval !== "denied" && (promptTools.includes(part.name) || tool?.view?.Prompt !== void 0);
|
|
1040
1096
|
const DECLINED = "Declined by the user";
|
|
1041
1097
|
/**
|
|
1042
1098
|
* A linear conversation driven by a Transport: sends, streams the reply through `applyEvent`, stops and
|
|
1043
1099
|
* retries. With an `AgentProvider` it also runs the agent loop: client tools run in the page, approvals
|
|
1044
1100
|
* wait for the user, then the reply continues; every finished tool call reaches its tool's `onResult`.
|
|
1045
1101
|
*/
|
|
1046
|
-
function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12, resume = false }) {
|
|
1102
|
+
function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12, resume = false, promptTools = ["ask_user"] }) {
|
|
1047
1103
|
const contextAgent = useAgentRegistry();
|
|
1048
1104
|
const registry = agent === void 0 ? contextAgent : agent;
|
|
1049
1105
|
const [messages, setMessages] = useState(initialMessages);
|
|
1050
1106
|
const [busy, setBusy] = useState(false);
|
|
1051
1107
|
const [approvals, setApprovals] = useState([]);
|
|
1108
|
+
const [prompts, setPrompts] = useState([]);
|
|
1052
1109
|
const run = useRef(null);
|
|
1053
1110
|
const latest = useRef({
|
|
1054
1111
|
messages,
|
|
1055
1112
|
transport,
|
|
1056
1113
|
conversationId,
|
|
1057
1114
|
onTitle,
|
|
1058
|
-
registry
|
|
1115
|
+
registry,
|
|
1116
|
+
promptTools
|
|
1059
1117
|
});
|
|
1060
1118
|
latest.current = {
|
|
1061
1119
|
messages,
|
|
1062
1120
|
transport,
|
|
1063
1121
|
conversationId,
|
|
1064
1122
|
onTitle,
|
|
1065
|
-
registry
|
|
1123
|
+
registry,
|
|
1124
|
+
promptTools
|
|
1066
1125
|
};
|
|
1067
1126
|
const loop = useRef({
|
|
1068
1127
|
options: {},
|
|
@@ -1177,6 +1236,15 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1177
1236
|
setBusy(false);
|
|
1178
1237
|
return;
|
|
1179
1238
|
}
|
|
1239
|
+
const prompted = awaiting.filter((p) => wantsPrompt(p, latest.current.promptTools, registry?.tool(p.name)));
|
|
1240
|
+
if (prompted.length > 0) {
|
|
1241
|
+
setPrompts(prompted.map((part) => ({
|
|
1242
|
+
messageId: reply.id,
|
|
1243
|
+
part
|
|
1244
|
+
})));
|
|
1245
|
+
setBusy(false);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1180
1248
|
const controller = new AbortController();
|
|
1181
1249
|
const current = {
|
|
1182
1250
|
controller,
|
|
@@ -1265,6 +1333,29 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1265
1333
|
advance,
|
|
1266
1334
|
commit
|
|
1267
1335
|
]);
|
|
1336
|
+
const settlePrompt = useCallback((partId, fields) => {
|
|
1337
|
+
const pending = prompts.find((p) => p.part.id === partId);
|
|
1338
|
+
if (!pending) return;
|
|
1339
|
+
const message = latest.current.messages.find((m) => m.id === pending.messageId);
|
|
1340
|
+
if (!message) return;
|
|
1341
|
+
const next = settleToolCall(message, partId, fields);
|
|
1342
|
+
commit(next);
|
|
1343
|
+
const rest = prompts.filter((p) => p.part.id !== partId);
|
|
1344
|
+
setPrompts(rest);
|
|
1345
|
+
if (rest.length === 0) advance(next);
|
|
1346
|
+
}, [
|
|
1347
|
+
prompts,
|
|
1348
|
+
advance,
|
|
1349
|
+
commit
|
|
1350
|
+
]);
|
|
1351
|
+
const answer = useCallback((partId, result) => settlePrompt(partId, {
|
|
1352
|
+
status: "done",
|
|
1353
|
+
result
|
|
1354
|
+
}), [settlePrompt]);
|
|
1355
|
+
const decline = useCallback((partId) => settlePrompt(partId, {
|
|
1356
|
+
status: "error",
|
|
1357
|
+
error: DECLINED
|
|
1358
|
+
}), [settlePrompt]);
|
|
1268
1359
|
/** Declines whatever is waiting, e.g. because the user moved on. */
|
|
1269
1360
|
const declineAll = useCallback(() => {
|
|
1270
1361
|
for (const { messageId, part } of approvals) {
|
|
@@ -1280,6 +1371,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1280
1371
|
error: DECLINED
|
|
1281
1372
|
}));
|
|
1282
1373
|
setApprovals([]);
|
|
1374
|
+
setPrompts([]);
|
|
1283
1375
|
}, [approvals, commit]);
|
|
1284
1376
|
const send = useCallback((text, options = {}) => {
|
|
1285
1377
|
const attachments = options.attachments ?? [];
|
|
@@ -1304,6 +1396,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1304
1396
|
const index = messages.findIndex((m) => m.id === assistantId);
|
|
1305
1397
|
if (index < 1 || messages[index - 1].role !== "user") return;
|
|
1306
1398
|
setApprovals([]);
|
|
1399
|
+
setPrompts([]);
|
|
1307
1400
|
loop.current = {
|
|
1308
1401
|
options,
|
|
1309
1402
|
step: 0
|
|
@@ -1339,6 +1432,9 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1339
1432
|
busy,
|
|
1340
1433
|
approvals,
|
|
1341
1434
|
respond,
|
|
1435
|
+
prompts,
|
|
1436
|
+
answer,
|
|
1437
|
+
decline,
|
|
1342
1438
|
send,
|
|
1343
1439
|
stop,
|
|
1344
1440
|
retry
|
|
@@ -2178,17 +2274,18 @@ function Node({ node, registry }) {
|
|
|
2178
2274
|
}) });
|
|
2179
2275
|
}
|
|
2180
2276
|
/** Draws a `render_ui` tree with the registered components. Unknown components and bad props render nothing. */
|
|
2181
|
-
function UiTree({ id, root, state: initial, streaming = false }) {
|
|
2277
|
+
function UiTree({ id, stateKey, root, state: initial, streaming = false }) {
|
|
2278
|
+
const storeKey = stateKey ?? id;
|
|
2182
2279
|
const registry = useContext(RegistryContext);
|
|
2183
2280
|
const host = useWidgetHost();
|
|
2184
|
-
const [local, setLocal] = useState(() => registry?.persist?.load(
|
|
2281
|
+
const [local, setLocal] = useState(() => registry?.persist?.load(storeKey) ?? {});
|
|
2185
2282
|
const changed = useRef(false);
|
|
2186
2283
|
const latest = useRef(host);
|
|
2187
2284
|
latest.current = host;
|
|
2188
2285
|
useEffect(() => {
|
|
2189
|
-
if (changed.current) registry?.persist?.save(
|
|
2286
|
+
if (changed.current) registry?.persist?.save(storeKey, local);
|
|
2190
2287
|
}, [
|
|
2191
|
-
|
|
2288
|
+
storeKey,
|
|
2192
2289
|
local,
|
|
2193
2290
|
registry
|
|
2194
2291
|
]);
|
|
@@ -2239,15 +2336,20 @@ function useUiMarkupLanguage() {
|
|
|
2239
2336
|
return registry && registry.markup !== false ? registry.markup : void 0;
|
|
2240
2337
|
}
|
|
2241
2338
|
/** A ```ui block from a reply, drawn with the registered components. Safe on a block that is still streaming. */
|
|
2242
|
-
function UiMarkupBlock({ id, source, streaming = false }) {
|
|
2339
|
+
function UiMarkupBlock({ id, stateKey, source, streaming = false }) {
|
|
2243
2340
|
const { nodes, state } = useMemo(() => parseUiMarkup(source), [source]);
|
|
2244
2341
|
return /* @__PURE__ */ jsx(UiTree, {
|
|
2245
2342
|
id,
|
|
2343
|
+
...stateKey && { stateKey },
|
|
2246
2344
|
root: nodes,
|
|
2247
2345
|
state,
|
|
2248
2346
|
streaming
|
|
2249
2347
|
});
|
|
2250
2348
|
}
|
|
2349
|
+
function stateKeyOf(options, part) {
|
|
2350
|
+
const stateKey = options.stateKey?.(part);
|
|
2351
|
+
return stateKey ? { stateKey } : {};
|
|
2352
|
+
}
|
|
2251
2353
|
/**
|
|
2252
2354
|
* A tool view that draws a call's (streaming) JSX markup with your registered components, for whatever tool
|
|
2253
2355
|
* your harness uses; the suggested one is core's `showUiTool`. Register it under your tool's name:
|
|
@@ -2256,12 +2358,13 @@ function UiMarkupBlock({ id, source, streaming = false }) {
|
|
|
2256
2358
|
function uiMarkupToolView(read = (input) => {
|
|
2257
2359
|
const markup = input?.markup;
|
|
2258
2360
|
return typeof markup === "string" ? markup : void 0;
|
|
2259
|
-
}) {
|
|
2361
|
+
}, options = {}) {
|
|
2260
2362
|
function UiMarkupToolBlock({ part }) {
|
|
2261
2363
|
const markup = read(part.input ?? part.partialInput);
|
|
2262
2364
|
if (!markup) return null;
|
|
2263
2365
|
return /* @__PURE__ */ jsx(UiMarkupBlock, {
|
|
2264
2366
|
id: part.id,
|
|
2367
|
+
...stateKeyOf(options, part),
|
|
2265
2368
|
source: markup,
|
|
2266
2369
|
streaming: part.status === "streaming" || part.status === "input"
|
|
2267
2370
|
});
|
|
@@ -2285,13 +2388,14 @@ function readTreeInput(input) {
|
|
|
2285
2388
|
* your tool's input to the tree and its starting state; the default reads `{ root, state }`, the shape of
|
|
2286
2389
|
* the suggested `renderUiTool`.
|
|
2287
2390
|
*/
|
|
2288
|
-
function uiTreeToolView(read = readTreeInput) {
|
|
2391
|
+
function uiTreeToolView(read = readTreeInput, options = {}) {
|
|
2289
2392
|
function UiTreeToolBlock({ part }) {
|
|
2290
2393
|
const { root, state } = read(part.input ?? part.partialInput);
|
|
2291
2394
|
if (!root) return null;
|
|
2292
2395
|
const ok = typeof state === "object" && state !== null;
|
|
2293
2396
|
return /* @__PURE__ */ jsx(UiTree, {
|
|
2294
2397
|
id: part.id,
|
|
2398
|
+
...stateKeyOf(options, part),
|
|
2295
2399
|
root,
|
|
2296
2400
|
...ok && { state },
|
|
2297
2401
|
streaming: part.status === "streaming" || part.status === "input"
|
|
@@ -2384,7 +2488,19 @@ function rehypeWordFade() {
|
|
|
2384
2488
|
return (tree) => wrap(tree);
|
|
2385
2489
|
}
|
|
2386
2490
|
// src/components/markdown.tsx
|
|
2387
|
-
const
|
|
2491
|
+
const [sanitize, sanitizeSchema] = defaultRehypePlugins.sanitize;
|
|
2492
|
+
const rehypePlugins = Object.values({
|
|
2493
|
+
...defaultRehypePlugins,
|
|
2494
|
+
sanitize: [sanitize, {
|
|
2495
|
+
...sanitizeSchema,
|
|
2496
|
+
tagNames: sanitizeSchema.tagNames?.filter((t) => t !== "source"),
|
|
2497
|
+
protocols: {
|
|
2498
|
+
...sanitizeSchema.protocols,
|
|
2499
|
+
src: [...sanitizeSchema.protocols?.src ?? [], "data"]
|
|
2500
|
+
}
|
|
2501
|
+
}]
|
|
2502
|
+
});
|
|
2503
|
+
const fadePlugins = [...rehypePlugins, rehypeWordFade];
|
|
2388
2504
|
const math = createMathPlugin({ singleDollarTextMath: true });
|
|
2389
2505
|
/** Streamdown's own utility classes get this prefix, so neither they nor the host's classes apply. */
|
|
2390
2506
|
const PREFIX = "llmchat";
|
|
@@ -2417,6 +2533,15 @@ function Code({ node, className, children }) {
|
|
|
2417
2533
|
});
|
|
2418
2534
|
}
|
|
2419
2535
|
const components = {
|
|
2536
|
+
...Object.fromEntries([
|
|
2537
|
+
"source",
|
|
2538
|
+
"video",
|
|
2539
|
+
"audio",
|
|
2540
|
+
"track",
|
|
2541
|
+
"iframe",
|
|
2542
|
+
"embed",
|
|
2543
|
+
"object"
|
|
2544
|
+
].map((t) => [t, () => null])),
|
|
2420
2545
|
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment$1, { children }),
|
|
2421
2546
|
code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
|
|
2422
2547
|
node,
|
|
@@ -2425,11 +2550,18 @@ const components = {
|
|
|
2425
2550
|
})
|
|
2426
2551
|
};
|
|
2427
2552
|
const LinksContext = createContext(void 0);
|
|
2428
|
-
function MarkdownLinksProvider({ renderLink, renderLinkList, children }) {
|
|
2553
|
+
function MarkdownLinksProvider({ renderLink, renderLinkList, renderImage, imageOrigins, children }) {
|
|
2429
2554
|
const value = useMemo(() => ({
|
|
2430
2555
|
renderLink,
|
|
2431
|
-
renderLinkList
|
|
2432
|
-
|
|
2556
|
+
renderLinkList,
|
|
2557
|
+
renderImage,
|
|
2558
|
+
imageOrigins
|
|
2559
|
+
}), [
|
|
2560
|
+
renderLink,
|
|
2561
|
+
renderLinkList,
|
|
2562
|
+
renderImage,
|
|
2563
|
+
imageOrigins
|
|
2564
|
+
]);
|
|
2433
2565
|
return /* @__PURE__ */ jsx(LinksContext.Provider, {
|
|
2434
2566
|
value,
|
|
2435
2567
|
children
|
|
@@ -2446,6 +2578,32 @@ function soleLink(children) {
|
|
|
2446
2578
|
};
|
|
2447
2579
|
return props.node?.tagName === "p" ? soleLink(props.children) : void 0;
|
|
2448
2580
|
}
|
|
2581
|
+
const originOf = (url, base) => {
|
|
2582
|
+
try {
|
|
2583
|
+
return new URL(url, base).origin;
|
|
2584
|
+
} catch {
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
/** Whether an image may load on render. Relative and protocol-relative URLs count as the page's origin or the one they name. */
|
|
2589
|
+
function imageAllowed(src, origins) {
|
|
2590
|
+
if (/^data:image\//i.test(src)) return true;
|
|
2591
|
+
if (origins === "all") return /^(https?:)?\/\//i.test(src) || !/^[a-z][a-z\d+.-]*:/i.test(src);
|
|
2592
|
+
if (!origins?.length) return false;
|
|
2593
|
+
const origin = originOf(src, typeof location === "undefined" ? void 0 : location.href);
|
|
2594
|
+
return origin !== void 0 && /^https?:/.test(origin) && origins.some((o) => (originOf(o) ?? o) === origin);
|
|
2595
|
+
}
|
|
2596
|
+
/** What an image the host did not allow becomes: a link to it (alt text or host name), opened only by the reader. */
|
|
2597
|
+
function ImageLink({ src, alt }) {
|
|
2598
|
+
const url = URL.canParse(src) ? new URL(src) : void 0;
|
|
2599
|
+
if (url?.protocol !== "http:" && url?.protocol !== "https:") return /* @__PURE__ */ jsx(Fragment$1, { children: alt });
|
|
2600
|
+
return /* @__PURE__ */ jsx("a", {
|
|
2601
|
+
href: src,
|
|
2602
|
+
target: "_blank",
|
|
2603
|
+
rel: "noopener noreferrer",
|
|
2604
|
+
children: alt || url.host
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2449
2607
|
function listOf(ordered, props, renderLinkList) {
|
|
2450
2608
|
const Default = ordered ? defaultComponents.ol : defaultComponents.ul;
|
|
2451
2609
|
const { children } = props;
|
|
@@ -2488,9 +2646,27 @@ function BlockScope({ id, streaming, children }) {
|
|
|
2488
2646
|
});
|
|
2489
2647
|
}
|
|
2490
2648
|
const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
|
|
2491
|
-
const { renderLink, renderLinkList } = useContext(LinksContext) ?? {};
|
|
2649
|
+
const { renderLink, renderLinkList, renderImage, imageOrigins } = useContext(LinksContext) ?? {};
|
|
2492
2650
|
const withLinks = useMemo(() => ({
|
|
2493
2651
|
...components,
|
|
2652
|
+
img: ({ src, alt }) => {
|
|
2653
|
+
if (typeof src !== "string" || !src) return /* @__PURE__ */ jsx(Fragment$1, { children: alt });
|
|
2654
|
+
if (renderImage) return /* @__PURE__ */ jsx(Fragment$1, { children: renderImage({
|
|
2655
|
+
src,
|
|
2656
|
+
alt: alt ?? ""
|
|
2657
|
+
}) });
|
|
2658
|
+
if (imageAllowed(src, imageOrigins)) return /* @__PURE__ */ jsx("img", {
|
|
2659
|
+
src,
|
|
2660
|
+
alt: alt ?? "",
|
|
2661
|
+
loading: "lazy",
|
|
2662
|
+
referrerPolicy: "no-referrer",
|
|
2663
|
+
className: "max-w-full rounded-chat-md"
|
|
2664
|
+
});
|
|
2665
|
+
return /* @__PURE__ */ jsx(ImageLink, {
|
|
2666
|
+
src,
|
|
2667
|
+
alt: alt ?? ""
|
|
2668
|
+
});
|
|
2669
|
+
},
|
|
2494
2670
|
...renderLink && { a: ({ href, children }) => {
|
|
2495
2671
|
const custom = href ? renderLink({
|
|
2496
2672
|
href,
|
|
@@ -2507,11 +2683,16 @@ const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
|
|
|
2507
2683
|
ul: (props) => listOf(false, props, renderLinkList),
|
|
2508
2684
|
ol: (props) => listOf(true, props, renderLinkList)
|
|
2509
2685
|
}
|
|
2510
|
-
}), [
|
|
2686
|
+
}), [
|
|
2687
|
+
renderLink,
|
|
2688
|
+
renderLinkList,
|
|
2689
|
+
renderImage,
|
|
2690
|
+
imageOrigins
|
|
2691
|
+
]);
|
|
2511
2692
|
return /* @__PURE__ */ jsx(Streamdown, {
|
|
2512
2693
|
prefix: PREFIX,
|
|
2513
2694
|
mode: animating || fade ? "streaming" : "static",
|
|
2514
|
-
|
|
2695
|
+
rehypePlugins: fade ? fadePlugins : rehypePlugins,
|
|
2515
2696
|
plugins: { math },
|
|
2516
2697
|
components: withLinks,
|
|
2517
2698
|
controls: false,
|
|
@@ -2520,6 +2701,406 @@ const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
|
|
|
2520
2701
|
children: text
|
|
2521
2702
|
});
|
|
2522
2703
|
});
|
|
2704
|
+
// src/components/ask-user.tsx
|
|
2705
|
+
const rowClass$1 = "flex w-full cursor-pointer items-center gap-3 rounded-chat-md px-2.5 py-2 text-left text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none hover:bg-chat-hover focus-visible:ring-2 focus-visible:ring-chat-focus";
|
|
2706
|
+
/** Moves focus to the previous or next answer row of a question, without wrapping. */
|
|
2707
|
+
function moveFocus(root, from, delta) {
|
|
2708
|
+
const rows = [...root.querySelectorAll("[data-ask-row]")];
|
|
2709
|
+
const i = rows.findIndex((row) => row === from || row.contains(from));
|
|
2710
|
+
rows[Math.min(rows.length - 1, Math.max(0, i + delta))]?.focus();
|
|
2711
|
+
}
|
|
2712
|
+
const isTextField = (target) => target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
|
|
2713
|
+
/** The 1-based number a plain digit key stands for, or undefined. */
|
|
2714
|
+
function digitOf(e) {
|
|
2715
|
+
if (e.altKey || e.ctrlKey || e.metaKey || isTextField(e.target)) return void 0;
|
|
2716
|
+
return /^[1-9]$/.test(e.key) ? Number(e.key) : void 0;
|
|
2717
|
+
}
|
|
2718
|
+
function Num({ children, active }) {
|
|
2719
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2720
|
+
"aria-hidden": true,
|
|
2721
|
+
className: clsx("flex size-6 shrink-0 items-center justify-center rounded-chat-sm border text-(length:--chat-ui-font-size-sm) tabular-nums", active ? "border-chat-accent bg-chat-accent text-chat-accent-fg" : "border-chat-border-strong text-chat-muted"),
|
|
2722
|
+
children
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
function Check$1({ checked }) {
|
|
2726
|
+
const icons = useIcons();
|
|
2727
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2728
|
+
"aria-hidden": true,
|
|
2729
|
+
className: clsx("flex size-5 shrink-0 items-center justify-center rounded-chat-sm border [&_svg]:size-3.5", checked ? "border-chat-accent bg-chat-accent text-chat-accent-fg" : "border-chat-border-strong"),
|
|
2730
|
+
children: checked && icons.check
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
/** The "Something else" row: a text input the number key after the last option focuses. */
|
|
2734
|
+
function OtherRow({ number, value, onChange, onEnter, leading }) {
|
|
2735
|
+
const labels = useLabels();
|
|
2736
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2737
|
+
className: clsx(rowClass$1, "cursor-text"),
|
|
2738
|
+
children: [leading, /* @__PURE__ */ jsx("input", {
|
|
2739
|
+
"data-ask-row": true,
|
|
2740
|
+
"data-ask-other": true,
|
|
2741
|
+
value,
|
|
2742
|
+
onChange: (e) => onChange(e.target.value),
|
|
2743
|
+
onKeyDown: (e) => {
|
|
2744
|
+
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
|
|
2745
|
+
e.preventDefault();
|
|
2746
|
+
onEnter();
|
|
2747
|
+
}
|
|
2748
|
+
},
|
|
2749
|
+
placeholder: labels.askUserSomethingElse,
|
|
2750
|
+
"aria-label": labels.askUserSomethingElse,
|
|
2751
|
+
"aria-keyshortcuts": String(number),
|
|
2752
|
+
className: "min-w-0 flex-1 bg-transparent text-chat-fg outline-none placeholder:text-chat-subtle"
|
|
2753
|
+
})]
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
function Footer({ start, onSkip, next }) {
|
|
2757
|
+
const labels = useLabels();
|
|
2758
|
+
const icons = useIcons();
|
|
2759
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2760
|
+
className: "mt-2 flex items-center gap-2 px-1",
|
|
2761
|
+
children: [
|
|
2762
|
+
/* @__PURE__ */ jsx("span", {
|
|
2763
|
+
className: "min-w-0 flex-1 truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
2764
|
+
children: start
|
|
2765
|
+
}),
|
|
2766
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2767
|
+
size: "sm",
|
|
2768
|
+
variant: "ghost",
|
|
2769
|
+
className: "text-chat-muted",
|
|
2770
|
+
onPress: onSkip,
|
|
2771
|
+
children: labels.askUserSkip
|
|
2772
|
+
}),
|
|
2773
|
+
next && /* @__PURE__ */ jsx(IconButton, {
|
|
2774
|
+
label: next.label,
|
|
2775
|
+
variant: "primary",
|
|
2776
|
+
size: "sm",
|
|
2777
|
+
isDisabled: next.disabled,
|
|
2778
|
+
onPress: next.onPress,
|
|
2779
|
+
children: icons.askNext
|
|
2780
|
+
})
|
|
2781
|
+
]
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
function SingleQuestion({ question, initial, onAnswer, onSkip }) {
|
|
2785
|
+
const { options } = question;
|
|
2786
|
+
const chosen = typeof initial?.answer === "string" ? initial.answer : void 0;
|
|
2787
|
+
const [other, setOther] = useState(chosen !== void 0 && !options.includes(chosen) ? chosen : "");
|
|
2788
|
+
const root = useRef(null);
|
|
2789
|
+
const submitOther = () => other.trim() && onAnswer(other.trim());
|
|
2790
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2791
|
+
ref: root,
|
|
2792
|
+
role: "group",
|
|
2793
|
+
"aria-label": question.question,
|
|
2794
|
+
onKeyDown: (e) => {
|
|
2795
|
+
const n = digitOf(e);
|
|
2796
|
+
if (n !== void 0 && n <= options.length) onAnswer(options[n - 1]);
|
|
2797
|
+
else if (n === options.length + 1) root.current?.querySelector("[data-ask-other]")?.focus();
|
|
2798
|
+
else return;
|
|
2799
|
+
e.preventDefault();
|
|
2800
|
+
},
|
|
2801
|
+
children: [
|
|
2802
|
+
options.map((option, i) => /* @__PURE__ */ jsxs("button", {
|
|
2803
|
+
type: "button",
|
|
2804
|
+
"data-ask-row": true,
|
|
2805
|
+
onClick: () => onAnswer(option),
|
|
2806
|
+
className: rowClass$1,
|
|
2807
|
+
children: [/* @__PURE__ */ jsx(Num, {
|
|
2808
|
+
active: option === chosen,
|
|
2809
|
+
children: i + 1
|
|
2810
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2811
|
+
className: "min-w-0 flex-1",
|
|
2812
|
+
children: option
|
|
2813
|
+
})]
|
|
2814
|
+
}, i)),
|
|
2815
|
+
/* @__PURE__ */ jsx(OtherRow, {
|
|
2816
|
+
number: options.length + 1,
|
|
2817
|
+
value: other,
|
|
2818
|
+
onChange: setOther,
|
|
2819
|
+
onEnter: submitOther,
|
|
2820
|
+
leading: /* @__PURE__ */ jsx(Num, {
|
|
2821
|
+
active: other !== "" && other === chosen,
|
|
2822
|
+
children: options.length + 1
|
|
2823
|
+
})
|
|
2824
|
+
}),
|
|
2825
|
+
/* @__PURE__ */ jsx(Footer, { onSkip })
|
|
2826
|
+
]
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
function MultipleQuestion({ question, initial, last, onAnswer, onSkip }) {
|
|
2830
|
+
const labels = useLabels();
|
|
2831
|
+
const { options } = question;
|
|
2832
|
+
const before = Array.isArray(initial?.answer) ? initial.answer : [];
|
|
2833
|
+
const [picked, setPicked] = useState(() => new Set(options.flatMap((o, i) => before.includes(o) ? [i] : [])));
|
|
2834
|
+
const [other, setOther] = useState(before.find((a) => !options.includes(a)) ?? "");
|
|
2835
|
+
const root = useRef(null);
|
|
2836
|
+
const answer = [...options.filter((_, i) => picked.has(i)), ...other.trim() ? [other.trim()] : []];
|
|
2837
|
+
const toggle = (i) => setPicked((set) => (set.delete(i) || set.add(i), new Set(set)));
|
|
2838
|
+
const next = () => answer.length > 0 && onAnswer(answer);
|
|
2839
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2840
|
+
ref: root,
|
|
2841
|
+
role: "group",
|
|
2842
|
+
"aria-label": question.question,
|
|
2843
|
+
onKeyDown: (e) => {
|
|
2844
|
+
const n = digitOf(e);
|
|
2845
|
+
if (n !== void 0 && n <= options.length) toggle(n - 1);
|
|
2846
|
+
else if (n === options.length + 1) root.current?.querySelector("[data-ask-other]")?.focus();
|
|
2847
|
+
else return;
|
|
2848
|
+
e.preventDefault();
|
|
2849
|
+
},
|
|
2850
|
+
children: [
|
|
2851
|
+
options.map((option, i) => /* @__PURE__ */ jsxs("button", {
|
|
2852
|
+
type: "button",
|
|
2853
|
+
role: "checkbox",
|
|
2854
|
+
"aria-checked": picked.has(i),
|
|
2855
|
+
"data-ask-row": true,
|
|
2856
|
+
onClick: () => toggle(i),
|
|
2857
|
+
onKeyDown: (e) => {
|
|
2858
|
+
if (e.key !== "Enter" || answer.length === 0) return;
|
|
2859
|
+
e.preventDefault();
|
|
2860
|
+
e.stopPropagation();
|
|
2861
|
+
next();
|
|
2862
|
+
},
|
|
2863
|
+
className: rowClass$1,
|
|
2864
|
+
children: [/* @__PURE__ */ jsx(Check$1, { checked: picked.has(i) }), /* @__PURE__ */ jsx("span", {
|
|
2865
|
+
className: "min-w-0 flex-1",
|
|
2866
|
+
children: option
|
|
2867
|
+
})]
|
|
2868
|
+
}, i)),
|
|
2869
|
+
/* @__PURE__ */ jsx(OtherRow, {
|
|
2870
|
+
number: options.length + 1,
|
|
2871
|
+
value: other,
|
|
2872
|
+
onChange: setOther,
|
|
2873
|
+
onEnter: next,
|
|
2874
|
+
leading: /* @__PURE__ */ jsx(Check$1, { checked: other.trim() !== "" })
|
|
2875
|
+
}),
|
|
2876
|
+
/* @__PURE__ */ jsx(Footer, {
|
|
2877
|
+
start: labels.askUserSelected(answer.length),
|
|
2878
|
+
onSkip,
|
|
2879
|
+
next: {
|
|
2880
|
+
label: last ? labels.askUserSubmit : labels.askUserContinue,
|
|
2881
|
+
disabled: answer.length === 0,
|
|
2882
|
+
onPress: next
|
|
2883
|
+
}
|
|
2884
|
+
})
|
|
2885
|
+
]
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
function RankQuestion({ question, initial, last, onAnswer, onSkip }) {
|
|
2889
|
+
const labels = useLabels();
|
|
2890
|
+
const icons = useIcons();
|
|
2891
|
+
const { options } = question;
|
|
2892
|
+
const [order, setOrder] = useState(() => {
|
|
2893
|
+
const before = Array.isArray(initial?.answer) ? initial.answer : [];
|
|
2894
|
+
const pool = options.map((o, i) => [o, i]);
|
|
2895
|
+
const restored = before.flatMap((text) => {
|
|
2896
|
+
const at = pool.findIndex(([o]) => o === text);
|
|
2897
|
+
return at === -1 ? [] : pool.splice(at, 1).map(([, i]) => i);
|
|
2898
|
+
});
|
|
2899
|
+
return restored.length === options.length ? restored : options.map((_, i) => i);
|
|
2900
|
+
});
|
|
2901
|
+
const rows = useRef(/* @__PURE__ */ new Map());
|
|
2902
|
+
const focusAfter = useRef(null);
|
|
2903
|
+
const dragging = useRef(null);
|
|
2904
|
+
useLayoutEffect(() => {
|
|
2905
|
+
if (focusAfter.current !== null) rows.current.get(focusAfter.current)?.focus();
|
|
2906
|
+
focusAfter.current = null;
|
|
2907
|
+
}, [order]);
|
|
2908
|
+
const move = (from, to) => {
|
|
2909
|
+
if (to < 0 || to >= order.length || from === to) return;
|
|
2910
|
+
setOrder((list) => {
|
|
2911
|
+
const next = [...list];
|
|
2912
|
+
next.splice(to, 0, next.splice(from, 1)[0]);
|
|
2913
|
+
return next;
|
|
2914
|
+
});
|
|
2915
|
+
};
|
|
2916
|
+
const send = () => onAnswer(order.map((i) => options[i]));
|
|
2917
|
+
const onDragOver = (position) => (e) => {
|
|
2918
|
+
e.preventDefault();
|
|
2919
|
+
if (dragging.current === null || dragging.current === position) return;
|
|
2920
|
+
move(dragging.current, position);
|
|
2921
|
+
dragging.current = position;
|
|
2922
|
+
};
|
|
2923
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2924
|
+
role: "group",
|
|
2925
|
+
"aria-label": question.question,
|
|
2926
|
+
children: [/* @__PURE__ */ jsx("ol", { children: order.map((option, position) => /* @__PURE__ */ jsxs("li", {
|
|
2927
|
+
ref: (el) => void (el ? rows.current.set(option, el) : rows.current.delete(option)),
|
|
2928
|
+
"data-ask-row": true,
|
|
2929
|
+
tabIndex: 0,
|
|
2930
|
+
draggable: true,
|
|
2931
|
+
"aria-label": `${position + 1}. ${options[option]}`,
|
|
2932
|
+
"aria-description": labels.askUserReorderKeys,
|
|
2933
|
+
"aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown",
|
|
2934
|
+
onDragStart: () => dragging.current = position,
|
|
2935
|
+
onDragOver: onDragOver(position),
|
|
2936
|
+
onDragEnd: () => dragging.current = null,
|
|
2937
|
+
onDrop: (e) => e.preventDefault(),
|
|
2938
|
+
onKeyDown: (e) => {
|
|
2939
|
+
if (e.key === "Enter") send();
|
|
2940
|
+
else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
2941
|
+
const delta = e.key === "ArrowUp" ? -1 : 1;
|
|
2942
|
+
if (e.altKey) {
|
|
2943
|
+
focusAfter.current = option;
|
|
2944
|
+
move(position, position + delta);
|
|
2945
|
+
} else moveFocus(e.currentTarget.parentElement, e.currentTarget, delta);
|
|
2946
|
+
} else return;
|
|
2947
|
+
e.preventDefault();
|
|
2948
|
+
},
|
|
2949
|
+
className: clsx(rowClass$1, "cursor-grab"),
|
|
2950
|
+
children: [
|
|
2951
|
+
/* @__PURE__ */ jsx("span", {
|
|
2952
|
+
"aria-hidden": true,
|
|
2953
|
+
className: "text-chat-subtle",
|
|
2954
|
+
children: /* @__PURE__ */ jsx(IconSlot, {
|
|
2955
|
+
className: "[&_svg]:size-4",
|
|
2956
|
+
children: icons.dragHandle
|
|
2957
|
+
})
|
|
2958
|
+
}),
|
|
2959
|
+
/* @__PURE__ */ jsx(Num, { children: position + 1 }),
|
|
2960
|
+
/* @__PURE__ */ jsx("span", {
|
|
2961
|
+
className: "min-w-0 flex-1",
|
|
2962
|
+
children: options[option]
|
|
2963
|
+
})
|
|
2964
|
+
]
|
|
2965
|
+
}, option)) }), /* @__PURE__ */ jsx(Footer, {
|
|
2966
|
+
start: labels.askUserRankHint,
|
|
2967
|
+
onSkip,
|
|
2968
|
+
next: {
|
|
2969
|
+
label: last ? labels.askUserSubmit : labels.askUserContinue,
|
|
2970
|
+
disabled: false,
|
|
2971
|
+
onPress: send
|
|
2972
|
+
}
|
|
2973
|
+
})]
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
const questionViews = {
|
|
2977
|
+
single: SingleQuestion,
|
|
2978
|
+
multiple: MultipleQuestion,
|
|
2979
|
+
rank: RankQuestion
|
|
2980
|
+
};
|
|
2981
|
+
/**
|
|
2982
|
+
* The `ask_user` prompt: the model's questions one at a time with a pager, answered by clicking or the keyboard
|
|
2983
|
+
* (number keys, arrows, Enter; Alt+arrows reorder a rank). Submits `{ answers }` (an `AskUserResult`) after the
|
|
2984
|
+
* last question; Skip leaves a question out, the close button or Escape declines everything.
|
|
2985
|
+
*/
|
|
2986
|
+
function AskUserPrompt({ part, onSubmit, onDecline }) {
|
|
2987
|
+
const labels = useLabels();
|
|
2988
|
+
const icons = useIcons();
|
|
2989
|
+
const questions = readAskUserInput(part.input);
|
|
2990
|
+
const [index, setIndex] = useState(0);
|
|
2991
|
+
const [answers, setAnswers] = useState([]);
|
|
2992
|
+
const root = useRef(null);
|
|
2993
|
+
useEffect(() => {
|
|
2994
|
+
root.current?.querySelector("[data-ask-row]:not([data-ask-other])")?.focus();
|
|
2995
|
+
}, [index]);
|
|
2996
|
+
const question = questions[index];
|
|
2997
|
+
if (!question) return null;
|
|
2998
|
+
const Question = questionViews[question.kind];
|
|
2999
|
+
const settle = (answer) => {
|
|
3000
|
+
const next = answers.slice();
|
|
3001
|
+
next[index] = {
|
|
3002
|
+
question: question.question,
|
|
3003
|
+
kind: question.kind,
|
|
3004
|
+
...answer
|
|
3005
|
+
};
|
|
3006
|
+
setAnswers(next);
|
|
3007
|
+
for (let step = 1; step <= questions.length; step++) {
|
|
3008
|
+
const at = (index + step) % questions.length;
|
|
3009
|
+
if (next[at] === void 0) return setIndex(at);
|
|
3010
|
+
}
|
|
3011
|
+
onSubmit({ answers: questions.map((_, i) => next[i]) });
|
|
3012
|
+
};
|
|
3013
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3014
|
+
ref: root,
|
|
3015
|
+
role: "dialog",
|
|
3016
|
+
"aria-label": labels.askUserPrompt,
|
|
3017
|
+
onKeyDown: (e) => {
|
|
3018
|
+
if (e.key !== "Escape") return;
|
|
3019
|
+
e.preventDefault();
|
|
3020
|
+
onDecline();
|
|
3021
|
+
},
|
|
3022
|
+
className: "flex flex-col gap-1 rounded-chat-lg border border-chat-border-strong bg-chat-surface p-3 font-chat-sans text-chat-fg shadow-chat-popover",
|
|
3023
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
3024
|
+
className: "flex items-start gap-2 px-1 pb-1",
|
|
3025
|
+
children: [
|
|
3026
|
+
/* @__PURE__ */ jsx("p", {
|
|
3027
|
+
className: "min-w-0 flex-1 text-(length:--chat-ui-font-size) leading-5 font-medium",
|
|
3028
|
+
children: question.question
|
|
3029
|
+
}),
|
|
3030
|
+
questions.length > 1 && /* @__PURE__ */ jsxs("div", {
|
|
3031
|
+
className: "flex shrink-0 items-center gap-0.5 text-(length:--chat-ui-font-size-sm) text-chat-muted",
|
|
3032
|
+
children: [
|
|
3033
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
3034
|
+
label: labels.askUserPrevious,
|
|
3035
|
+
size: "sm",
|
|
3036
|
+
isDisabled: index === 0,
|
|
3037
|
+
onPress: () => setIndex(index - 1),
|
|
3038
|
+
children: icons.askPrevious
|
|
3039
|
+
}),
|
|
3040
|
+
/* @__PURE__ */ jsx("span", {
|
|
3041
|
+
className: "tabular-nums",
|
|
3042
|
+
children: labels.askUserPager(index + 1, questions.length)
|
|
3043
|
+
}),
|
|
3044
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
3045
|
+
label: labels.askUserNext,
|
|
3046
|
+
size: "sm",
|
|
3047
|
+
isDisabled: index === questions.length - 1,
|
|
3048
|
+
onPress: () => setIndex(index + 1),
|
|
3049
|
+
children: icons.askPagerNext
|
|
3050
|
+
})
|
|
3051
|
+
]
|
|
3052
|
+
}),
|
|
3053
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
3054
|
+
label: labels.askUserClose,
|
|
3055
|
+
size: "sm",
|
|
3056
|
+
onPress: onDecline,
|
|
3057
|
+
children: icons.remove
|
|
3058
|
+
})
|
|
3059
|
+
]
|
|
3060
|
+
}), /* @__PURE__ */ jsx(Question, {
|
|
3061
|
+
question,
|
|
3062
|
+
initial: answers[index],
|
|
3063
|
+
last: answers.filter((a, i) => a !== void 0 && i !== index).length === questions.length - 1,
|
|
3064
|
+
onAnswer: (answer) => settle({ answer }),
|
|
3065
|
+
onSkip: () => settle({ skipped: true })
|
|
3066
|
+
}, index)]
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
/** The user's answers as a card in the message: each question with its answer, "Ranked: 1. …", or "Skipped". */
|
|
3070
|
+
function AskUserAnswers({ part }) {
|
|
3071
|
+
const labels = useLabels();
|
|
3072
|
+
if (part.status === "error") return /* @__PURE__ */ jsx("p", {
|
|
3073
|
+
className: "font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
|
|
3074
|
+
children: labels.askUserDeclined
|
|
3075
|
+
});
|
|
3076
|
+
const answers = part.status === "done" ? readAskUserResult(part.result) : void 0;
|
|
3077
|
+
if (!answers) return null;
|
|
3078
|
+
const text = (a) => {
|
|
3079
|
+
if (a.skipped || a.answer === void 0) return labels.askUserSkipped;
|
|
3080
|
+
if (typeof a.answer === "string") return a.answer;
|
|
3081
|
+
return a.kind === "rank" ? labels.askUserRanked(a.answer) : labels.askUserPicked(a.answer);
|
|
3082
|
+
};
|
|
3083
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
3084
|
+
"aria-label": labels.askUserAnswers,
|
|
3085
|
+
className: "flex flex-col gap-3 rounded-chat-lg border border-chat-border bg-chat-surface px-4 py-3 font-chat-sans text-(length:--chat-ui-font-size) leading-5",
|
|
3086
|
+
children: answers.map((a, i) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("p", {
|
|
3087
|
+
className: "text-chat-muted",
|
|
3088
|
+
children: a.question
|
|
3089
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
3090
|
+
className: clsx("text-chat-fg", (a.skipped || a.answer === void 0) && "text-chat-subtle"),
|
|
3091
|
+
children: text(a)
|
|
3092
|
+
})] }, i))
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* The view of the `ask_user` tool (`askUserTool` in `agentic-ui-kit/core`): the prompt over the composer while
|
|
3097
|
+
* the call awaits the user, then the answers card in the message. Registered in `defaultToolViews`.
|
|
3098
|
+
*/
|
|
3099
|
+
const askUserToolView = {
|
|
3100
|
+
label: (_, labels) => labels.askUserPrompt,
|
|
3101
|
+
Prompt: AskUserPrompt,
|
|
3102
|
+
Block: AskUserAnswers
|
|
3103
|
+
};
|
|
2523
3104
|
// src/components/tools.tsx
|
|
2524
3105
|
const ToolViewsContext = createContext(void 0);
|
|
2525
3106
|
function ToolViewsProvider({ tools, children }) {
|
|
@@ -2700,6 +3281,7 @@ const memoryTool = {
|
|
|
2700
3281
|
summary: (n, labels) => labels.recalledMemory(n)
|
|
2701
3282
|
};
|
|
2702
3283
|
const defaultToolViews = {
|
|
3284
|
+
ask_user: askUserToolView,
|
|
2703
3285
|
bash: commandTool,
|
|
2704
3286
|
shell: commandTool,
|
|
2705
3287
|
web_search: webSearchTool,
|
|
@@ -2786,10 +3368,12 @@ function TokenCounter({ tokens, variant = "standalone", className }) {
|
|
|
2786
3368
|
* row per step. Each row expands on its own; tool rows render through the registered `ToolView`.
|
|
2787
3369
|
* A group of one step skips the rows: the heading is the step's own label and the box holds its detail.
|
|
2788
3370
|
*/
|
|
2789
|
-
function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
3371
|
+
function ActivityGroup({ parts: all, defaultExpanded = false, className }) {
|
|
3372
|
+
const parts = all.filter((p) => !bareThought(p) || p.title || p.durationMs);
|
|
2790
3373
|
const views = useToolViews();
|
|
2791
3374
|
const labels = useLabels();
|
|
2792
3375
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
3376
|
+
if (parts.length === 0) return null;
|
|
2793
3377
|
const live = parts.some(isWorking);
|
|
2794
3378
|
const tokens = activityTokens(parts);
|
|
2795
3379
|
const after = live && tokens > 0 && /* @__PURE__ */ jsx(TokenCounter, {
|
|
@@ -2855,8 +3439,16 @@ function Shimmer({ children }) {
|
|
|
2855
3439
|
children
|
|
2856
3440
|
});
|
|
2857
3441
|
}
|
|
3442
|
+
/** A finished thought with nothing to open: hidden reasoning, or no text. It is a plain line, not a disclosure. */
|
|
3443
|
+
function bareThought(part) {
|
|
3444
|
+
return part.type === "thinking" && part.status === "done" && (part.visibility === "hidden" || !part.text);
|
|
3445
|
+
}
|
|
2858
3446
|
function SingleStep({ part, live, after, expanded, onExpandedChange }) {
|
|
2859
3447
|
const { summary, detail } = useStepContent(part);
|
|
3448
|
+
if (bareThought(part)) return /* @__PURE__ */ jsxs("div", {
|
|
3449
|
+
className: "flex items-center gap-1.5 text-chat-muted text-(length:--chat-activity-font-size)",
|
|
3450
|
+
children: [/* @__PURE__ */ jsx(Lead, { live }), summary]
|
|
3451
|
+
});
|
|
2860
3452
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
2861
3453
|
isExpanded: expanded,
|
|
2862
3454
|
onExpandedChange,
|
|
@@ -2879,6 +3471,10 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
|
|
|
2879
3471
|
function Step({ part }) {
|
|
2880
3472
|
const { summary, detail } = useStepContent(part);
|
|
2881
3473
|
const live = isWorking(part);
|
|
3474
|
+
if (bareThought(part)) return /* @__PURE__ */ jsx("div", {
|
|
3475
|
+
className: "px-3 py-2 text-chat-muted text-(length:--chat-activity-font-size)",
|
|
3476
|
+
children: summary
|
|
3477
|
+
});
|
|
2882
3478
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
2883
3479
|
className: "px-3 py-2",
|
|
2884
3480
|
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
@@ -3347,6 +3943,246 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
3347
3943
|
]
|
|
3348
3944
|
});
|
|
3349
3945
|
}
|
|
3946
|
+
// src/components/tool-prompt.tsx
|
|
3947
|
+
/**
|
|
3948
|
+
* Shows the `Prompt` of a pending call's tool view, above the composer (`ChatView` does this for you). Apps
|
|
3949
|
+
* that lay out the chat themselves render it from `useChat().prompts[0]`. Renders nothing if the tool's view
|
|
3950
|
+
* has no `Prompt`.
|
|
3951
|
+
*/
|
|
3952
|
+
function ToolPromptCard({ prompt, onAnswer, onDecline }) {
|
|
3953
|
+
const { part } = prompt;
|
|
3954
|
+
const Prompt = useToolView(part.name).Prompt;
|
|
3955
|
+
if (!Prompt) return null;
|
|
3956
|
+
return /* @__PURE__ */ jsx(Prompt, {
|
|
3957
|
+
part,
|
|
3958
|
+
onSubmit: (result) => onAnswer(part.id, result),
|
|
3959
|
+
onDecline: () => onDecline(part.id)
|
|
3960
|
+
}, part.id);
|
|
3961
|
+
}
|
|
3962
|
+
// src/components/ui-components.tsx
|
|
3963
|
+
const toneClass = {
|
|
3964
|
+
default: "text-chat-fg",
|
|
3965
|
+
accent: "text-chat-accent",
|
|
3966
|
+
good: "text-chat-check",
|
|
3967
|
+
warning: "text-chat-warning",
|
|
3968
|
+
bad: "text-chat-danger"
|
|
3969
|
+
};
|
|
3970
|
+
const fillClass = {
|
|
3971
|
+
default: "bg-chat-fg",
|
|
3972
|
+
accent: "bg-chat-accent",
|
|
3973
|
+
good: "bg-chat-check",
|
|
3974
|
+
warning: "bg-chat-warning",
|
|
3975
|
+
bad: "bg-chat-danger"
|
|
3976
|
+
};
|
|
3977
|
+
/** "New" / "Changed": what the model added stands out in the accent. */
|
|
3978
|
+
function Tag({ children, quiet = false }) {
|
|
3979
|
+
return /* @__PURE__ */ jsx("span", {
|
|
3980
|
+
className: `rounded-chat-sm px-1.5 text-(length:--chat-ui-font-size-sm) font-medium ${quiet ? "border border-chat-border-strong text-chat-muted" : "bg-chat-accent text-chat-accent-fg"}`,
|
|
3981
|
+
children
|
|
3982
|
+
});
|
|
3983
|
+
}
|
|
3984
|
+
const Stack = ({ direction = "column", gap = "md", children }) => /* @__PURE__ */ jsx("div", {
|
|
3985
|
+
className: `flex ${direction === "row" ? "flex-row flex-wrap" : "flex-col"} ${{
|
|
3986
|
+
sm: "gap-1.5",
|
|
3987
|
+
md: "gap-3",
|
|
3988
|
+
lg: "gap-5"
|
|
3989
|
+
}[gap]}`,
|
|
3990
|
+
children
|
|
3991
|
+
});
|
|
3992
|
+
const Grid = ({ columns = 2, children }) => /* @__PURE__ */ jsx("div", {
|
|
3993
|
+
className: "grid gap-3",
|
|
3994
|
+
style: { gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${Math.floor(36 / columns)}rem), 1fr))` },
|
|
3995
|
+
children
|
|
3996
|
+
});
|
|
3997
|
+
const Card = ({ title, subtitle, tag, tone = "default", children }) => /* @__PURE__ */ jsxs("section", {
|
|
3998
|
+
className: `rounded-chat-lg border p-3 ${tone === "accent" ? "border-chat-accent" : "border-chat-border"}`,
|
|
3999
|
+
children: [
|
|
4000
|
+
(title || tag) && /* @__PURE__ */ jsxs("header", {
|
|
4001
|
+
className: "mb-1 flex items-center gap-2",
|
|
4002
|
+
children: [title && /* @__PURE__ */ jsx("h3", {
|
|
4003
|
+
className: "font-chat-sans text-(length:--chat-ui-font-size) font-semibold",
|
|
4004
|
+
children: title
|
|
4005
|
+
}), tag && /* @__PURE__ */ jsx(Tag, {
|
|
4006
|
+
quiet: tone !== "accent",
|
|
4007
|
+
children: tag
|
|
4008
|
+
})]
|
|
4009
|
+
}),
|
|
4010
|
+
subtitle && /* @__PURE__ */ jsx("p", {
|
|
4011
|
+
className: "mb-2 text-(length:--chat-ui-font-size-sm) text-chat-muted",
|
|
4012
|
+
children: subtitle
|
|
4013
|
+
}),
|
|
4014
|
+
children
|
|
4015
|
+
]
|
|
4016
|
+
});
|
|
4017
|
+
const Heading$1 = ({ text }) => /* @__PURE__ */ jsx("h2", {
|
|
4018
|
+
className: "font-chat-sans text-(length:--chat-ui-font-size) font-semibold",
|
|
4019
|
+
children: text
|
|
4020
|
+
});
|
|
4021
|
+
const Text$1 = ({ text, tone = "default" }) => /* @__PURE__ */ jsx("p", {
|
|
4022
|
+
className: `font-chat-sans text-(length:--chat-ui-font-size) leading-relaxed ${tone === "muted" ? "text-chat-muted" : ""}`,
|
|
4023
|
+
children: text
|
|
4024
|
+
});
|
|
4025
|
+
const Metric = ({ label, value, note, tone = "default" }) => /* @__PURE__ */ jsxs("div", {
|
|
4026
|
+
className: "min-w-24",
|
|
4027
|
+
children: [
|
|
4028
|
+
/* @__PURE__ */ jsx("div", {
|
|
4029
|
+
className: "text-(length:--chat-ui-font-size-sm) text-chat-muted",
|
|
4030
|
+
children: label
|
|
4031
|
+
}),
|
|
4032
|
+
/* @__PURE__ */ jsx("div", {
|
|
4033
|
+
className: `font-chat-sans text-xl font-semibold tabular-nums ${toneClass[tone]}`,
|
|
4034
|
+
children: value
|
|
4035
|
+
}),
|
|
4036
|
+
note && /* @__PURE__ */ jsx("div", {
|
|
4037
|
+
className: "text-(length:--chat-ui-font-size-sm) text-chat-subtle",
|
|
4038
|
+
children: note
|
|
4039
|
+
})
|
|
4040
|
+
]
|
|
4041
|
+
});
|
|
4042
|
+
const List = ({ items }) => /* @__PURE__ */ jsx("ul", {
|
|
4043
|
+
className: "divide-y divide-chat-divider",
|
|
4044
|
+
children: items.map((item, i) => /* @__PURE__ */ jsxs("li", {
|
|
4045
|
+
className: "flex items-baseline gap-2 py-1.5 font-chat-sans text-(length:--chat-ui-font-size)",
|
|
4046
|
+
children: [
|
|
4047
|
+
/* @__PURE__ */ jsx("span", {
|
|
4048
|
+
className: item.tag ? "text-chat-accent" : "",
|
|
4049
|
+
children: item.label
|
|
4050
|
+
}),
|
|
4051
|
+
item.tag && /* @__PURE__ */ jsx(Tag, { children: item.tag }),
|
|
4052
|
+
item.detail && /* @__PURE__ */ jsx("span", {
|
|
4053
|
+
className: "ml-auto text-chat-muted tabular-nums",
|
|
4054
|
+
children: item.detail
|
|
4055
|
+
})
|
|
4056
|
+
]
|
|
4057
|
+
}, i))
|
|
4058
|
+
});
|
|
4059
|
+
const Table = ({ columns, rows }) => /* @__PURE__ */ jsx("div", {
|
|
4060
|
+
className: "max-w-full overflow-x-auto",
|
|
4061
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
4062
|
+
className: "w-full border-collapse text-left font-chat-sans text-(length:--chat-ui-font-size)",
|
|
4063
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
|
|
4064
|
+
className: "border-b border-chat-border-strong",
|
|
4065
|
+
children: columns.map((c, i) => /* @__PURE__ */ jsx("th", {
|
|
4066
|
+
scope: "col",
|
|
4067
|
+
className: "px-2 py-1.5 font-medium text-chat-muted",
|
|
4068
|
+
children: c
|
|
4069
|
+
}, i))
|
|
4070
|
+
}) }), /* @__PURE__ */ jsx("tbody", {
|
|
4071
|
+
className: "divide-y divide-chat-divider",
|
|
4072
|
+
children: rows.map((row, r) => /* @__PURE__ */ jsx("tr", { children: columns.map((_, c) => /* @__PURE__ */ jsx("td", {
|
|
4073
|
+
className: `px-2 py-1.5 ${typeof row[c] === "number" ? "tabular-nums" : ""}`,
|
|
4074
|
+
children: row[c]
|
|
4075
|
+
}, c)) }, r))
|
|
4076
|
+
})]
|
|
4077
|
+
})
|
|
4078
|
+
});
|
|
4079
|
+
const Badge$1 = ({ label, tone = "default" }) => /* @__PURE__ */ jsx("span", {
|
|
4080
|
+
className: `inline-block self-start rounded-chat-sm border border-current px-1.5 font-chat-sans text-(length:--chat-ui-font-size-sm) font-medium ${toneClass[tone]}`,
|
|
4081
|
+
children: label
|
|
4082
|
+
});
|
|
4083
|
+
const Progress = ({ label, value, max = 100, tone = "accent" }) => {
|
|
4084
|
+
const share = Math.min(1, Math.max(0, value / max));
|
|
4085
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4086
|
+
className: "flex flex-col gap-1",
|
|
4087
|
+
children: [label && /* @__PURE__ */ jsxs("div", {
|
|
4088
|
+
className: "flex justify-between text-(length:--chat-ui-font-size-sm) text-chat-muted",
|
|
4089
|
+
children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsxs("span", {
|
|
4090
|
+
className: "tabular-nums",
|
|
4091
|
+
children: [Math.round(share * 100), "%"]
|
|
4092
|
+
})]
|
|
4093
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
4094
|
+
role: "progressbar",
|
|
4095
|
+
"aria-label": label ?? "Progress",
|
|
4096
|
+
"aria-valuemin": 0,
|
|
4097
|
+
"aria-valuemax": max,
|
|
4098
|
+
"aria-valuenow": Math.min(value, max),
|
|
4099
|
+
className: "h-1.5 overflow-hidden rounded-full bg-chat-border",
|
|
4100
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
4101
|
+
className: `h-full rounded-full ${fillClass[tone]}`,
|
|
4102
|
+
style: { width: `${share * 100}%` }
|
|
4103
|
+
})
|
|
4104
|
+
})]
|
|
4105
|
+
});
|
|
4106
|
+
};
|
|
4107
|
+
const Checklist = function Checklist({ stateKey, items }) {
|
|
4108
|
+
const [state, set] = useUiState();
|
|
4109
|
+
const done = state[stateKey] ?? [];
|
|
4110
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
4111
|
+
className: "flex flex-col gap-1",
|
|
4112
|
+
children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
|
|
4113
|
+
className: "flex items-center gap-2 font-chat-sans text-(length:--chat-ui-font-size)",
|
|
4114
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
4115
|
+
type: "checkbox",
|
|
4116
|
+
className: "accent-(--chat-accent)",
|
|
4117
|
+
checked: done.includes(item),
|
|
4118
|
+
onChange: (e) => set(stateKey, e.target.checked ? [...done, item] : done.filter((d) => d !== item))
|
|
4119
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
4120
|
+
className: done.includes(item) ? "text-chat-subtle line-through" : "",
|
|
4121
|
+
children: item
|
|
4122
|
+
})]
|
|
4123
|
+
}) }, item))
|
|
4124
|
+
});
|
|
4125
|
+
};
|
|
4126
|
+
const Tabs = function Tabs({ stateKey, value, options }) {
|
|
4127
|
+
const [, set] = useUiState();
|
|
4128
|
+
const current = value ?? options[0].id;
|
|
4129
|
+
return /* @__PURE__ */ jsx("div", {
|
|
4130
|
+
role: "tablist",
|
|
4131
|
+
className: "inline-flex max-w-full flex-wrap gap-1 self-start rounded-chat-md border border-chat-border p-0.5",
|
|
4132
|
+
children: options.map((o) => /* @__PURE__ */ jsx(Button, {
|
|
4133
|
+
size: "sm",
|
|
4134
|
+
variant: o.id === current ? "subtle" : "ghost",
|
|
4135
|
+
onPress: () => set(stateKey, o.id),
|
|
4136
|
+
"aria-selected": o.id === current,
|
|
4137
|
+
children: o.label
|
|
4138
|
+
}, o.id))
|
|
4139
|
+
});
|
|
4140
|
+
};
|
|
4141
|
+
const Show = ({ value, equals, fallback, children }) => value === equals || value === void 0 && fallback ? /* @__PURE__ */ jsx(Fragment$1, { children }) : null;
|
|
4142
|
+
/**
|
|
4143
|
+
* The drawing of each component, by name and apart from its spec. Take one to reuse in your own
|
|
4144
|
+
* component: `{ ...uiSpecs.Card, render: uiRenders.Card }`.
|
|
4145
|
+
*/
|
|
4146
|
+
const uiRenders = {
|
|
4147
|
+
Stack,
|
|
4148
|
+
Grid,
|
|
4149
|
+
Card,
|
|
4150
|
+
Heading: Heading$1,
|
|
4151
|
+
Text: Text$1,
|
|
4152
|
+
Metric,
|
|
4153
|
+
List,
|
|
4154
|
+
Table,
|
|
4155
|
+
Badge: Badge$1,
|
|
4156
|
+
Progress,
|
|
4157
|
+
Checklist,
|
|
4158
|
+
Tabs,
|
|
4159
|
+
Show,
|
|
4160
|
+
Button: function UiButton({ label, variant = "secondary", onPress }) {
|
|
4161
|
+
const run = useUiAction();
|
|
4162
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
4163
|
+
size: "sm",
|
|
4164
|
+
className: "self-start",
|
|
4165
|
+
variant: variant === "primary" ? "primary" : "subtle",
|
|
4166
|
+
onPress: () => run(onPress),
|
|
4167
|
+
children: label
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
};
|
|
4171
|
+
/**
|
|
4172
|
+
* The ready components for `UiComponentsProvider`, in the order of `uiComponentSpecs`. Everything by default;
|
|
4173
|
+
* `include` and `exclude` take component names or the groups `"display"` (Stack, Grid, Card, Heading, Text,
|
|
4174
|
+
* Metric, List, Table, Badge, Progress) and `"interactive"` (Checklist, Tabs, Show, Button). `renders` swaps the
|
|
4175
|
+
* drawing of single components. Build the model's tool from the same picks so it only hears of what you
|
|
4176
|
+
* registered: `uiSpecSet({ include })` in `agentic-ui-kit/core`, or strip the renders from this list.
|
|
4177
|
+
*/
|
|
4178
|
+
function uiComponentSet({ renders = {}, ...selection } = {}) {
|
|
4179
|
+
return uiSpecSet(selection).map((spec) => ({
|
|
4180
|
+
...spec,
|
|
4181
|
+
render: renders[spec.name] ?? uiRenders[spec.name]
|
|
4182
|
+
}));
|
|
4183
|
+
}
|
|
4184
|
+
/** The whole set: `uiComponentSet()`. */
|
|
4185
|
+
const uiComponents = uiComponentSet();
|
|
3350
4186
|
// src/components/approval-card.tsx
|
|
3351
4187
|
/**
|
|
3352
4188
|
* Asks the user before a tool runs, above the composer. Shows the tool's label and what it will do: its
|
|
@@ -4572,6 +5408,34 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
4572
5408
|
}, section.id))
|
|
4573
5409
|
});
|
|
4574
5410
|
}
|
|
5411
|
+
/**
|
|
5412
|
+
* An icon button that opens a `ChatList` in a popover, for `Sidebar`'s `railActions`: the list itself is
|
|
5413
|
+
* hidden on the collapsed rail, so this keeps recent chats reachable. Picking a chat closes the popover.
|
|
5414
|
+
*/
|
|
5415
|
+
function ChatListRailButton({ label, popoverProps, onSelect, className, ...list }) {
|
|
5416
|
+
const icons = useIcons();
|
|
5417
|
+
const labels = useLabels();
|
|
5418
|
+
const name = label ?? labels.chats;
|
|
5419
|
+
return /* @__PURE__ */ jsxs(DialogTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
5420
|
+
label: name,
|
|
5421
|
+
children: icons.chatList
|
|
5422
|
+
}), /* @__PURE__ */ jsx(Popover, {
|
|
5423
|
+
placement: "right top",
|
|
5424
|
+
...popoverProps,
|
|
5425
|
+
children: /* @__PURE__ */ jsx(Dialog, {
|
|
5426
|
+
"aria-label": name,
|
|
5427
|
+
className: "max-h-[70dvh] w-64 overflow-y-auto p-2 outline-none [scrollbar-width:thin]",
|
|
5428
|
+
children: ({ close }) => /* @__PURE__ */ jsx(ChatList, {
|
|
5429
|
+
...list,
|
|
5430
|
+
className,
|
|
5431
|
+
onSelect: (id) => {
|
|
5432
|
+
onSelect?.(id);
|
|
5433
|
+
close();
|
|
5434
|
+
}
|
|
5435
|
+
})
|
|
5436
|
+
})
|
|
5437
|
+
})] });
|
|
5438
|
+
}
|
|
4575
5439
|
function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
4576
5440
|
const icons = useIcons();
|
|
4577
5441
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
@@ -4677,7 +5541,7 @@ const densityVars = {
|
|
|
4677
5541
|
}
|
|
4678
5542
|
};
|
|
4679
5543
|
/** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
|
|
4680
|
-
function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, onReadAloud, messageActions, renderPart, widgets, header, composer, emptyState, disclaimer, renderComposer, density = "full", contentWidth, contentPadding, className }) {
|
|
5544
|
+
function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, prompts, onAnswer, onDecline, onRetry, onEdit, onFeedback, onReadAloud, messageActions, renderPart, widgets, header, composer, emptyState, disclaimer, renderComposer, density = "full", contentWidth, contentPadding, className }) {
|
|
4681
5545
|
const style = {
|
|
4682
5546
|
...densityVars[density],
|
|
4683
5547
|
...contentWidth && { "--chat-content-width": contentWidth },
|
|
@@ -4692,6 +5556,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
4692
5556
|
const [body, short] = useShortPane();
|
|
4693
5557
|
const labels = useLabels();
|
|
4694
5558
|
const pending = onApprove && approvals?.[0];
|
|
5559
|
+
const asking = !pending && onAnswer ? prompts?.[0] : void 0;
|
|
4695
5560
|
const box = (variant) => {
|
|
4696
5561
|
if (composer === false) return null;
|
|
4697
5562
|
const element = /* @__PURE__ */ jsx(Composer, {
|
|
@@ -4700,7 +5565,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
4700
5565
|
onSubmit: onSend,
|
|
4701
5566
|
...onStop && { onStop },
|
|
4702
5567
|
...composer,
|
|
4703
|
-
...pending && { placeholder: labels.replyDirectly }
|
|
5568
|
+
...(pending || asking) && { placeholder: labels.replyDirectly }
|
|
4704
5569
|
});
|
|
4705
5570
|
return renderComposer ? renderComposer(element, variant === "centered" ? "empty" : "docked") : element;
|
|
4706
5571
|
};
|
|
@@ -4737,6 +5602,14 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
4737
5602
|
onRespond: onApprove,
|
|
4738
5603
|
className: "mb-2"
|
|
4739
5604
|
}, pending.part.id),
|
|
5605
|
+
asking && /* @__PURE__ */ jsx("div", {
|
|
5606
|
+
className: "mb-2",
|
|
5607
|
+
children: /* @__PURE__ */ jsx(ToolPromptCard, {
|
|
5608
|
+
prompt: asking,
|
|
5609
|
+
onAnswer,
|
|
5610
|
+
onDecline: onDecline ?? (() => {})
|
|
5611
|
+
})
|
|
5612
|
+
}),
|
|
4740
5613
|
box("docked"),
|
|
4741
5614
|
/* @__PURE__ */ jsx("div", {
|
|
4742
5615
|
className: clsx("flex items-center justify-center py-1 pb-[max(0.25rem,env(safe-area-inset-bottom))] text-center font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle", short ? "min-h-2" : "min-h-6"),
|
|
@@ -5108,7 +5981,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut, badge, desc
|
|
|
5108
5981
|
}
|
|
5109
5982
|
const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
|
|
5110
5983
|
/** The account row at the bottom: avatar, name, plan, and a menu. */
|
|
5111
|
-
function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
5984
|
+
function SidebarUser({ name, detail, avatarUrl, menu, popoverProps }) {
|
|
5112
5985
|
const icons = useIcons();
|
|
5113
5986
|
const { collapsed } = useSidebar();
|
|
5114
5987
|
const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
|
|
@@ -5140,7 +6013,8 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
5140
6013
|
if (!menu) return button;
|
|
5141
6014
|
return /* @__PURE__ */ jsxs(MenuTrigger, { children: [button, /* @__PURE__ */ jsx(Popover, {
|
|
5142
6015
|
placement: "top start",
|
|
6016
|
+
...popoverProps,
|
|
5143
6017
|
children: menu
|
|
5144
6018
|
})] });
|
|
5145
6019
|
}
|
|
5146
|
-
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextMeter, ContextPanel, CopyButton, CopyField, DRAWER_QUERY, DialogTrigger, DictationButton, DictationControls, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, PopoverTrigger, SegmentedControl, Sidebar, SidebarItem, SidebarList, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
|
|
6020
|
+
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AskUserAnswers, AskUserPrompt, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatListRailButton, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextMeter, ContextPanel, CopyButton, CopyField, DRAWER_QUERY, DialogTrigger, DictationButton, DictationControls, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, PopoverTrigger, SegmentedControl, Sidebar, SidebarItem, SidebarList, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolPromptCard, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, askUserToolView, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiComponentSet, uiComponents, uiMarkupToolView, uiRenders, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
|