@ciridae/ui 1.0.1 → 1.0.3
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/app/auth.d.ts +10 -0
- package/dist/app/auth.js +60 -0
- package/dist/app/error-boundary.d.ts +18 -0
- package/dist/app/error-boundary.js +45 -0
- package/dist/app/feedback.d.ts +12 -0
- package/dist/app/feedback.js +128 -0
- package/dist/app/index.d.ts +8 -0
- package/dist/app/index.js +8 -0
- package/dist/app/request-errors.d.ts +63 -0
- package/dist/app/request-errors.js +172 -0
- package/dist/app/sentry.d.ts +18 -0
- package/dist/app/sentry.js +52 -0
- package/dist/app/telemetry.d.ts +7 -0
- package/dist/app/telemetry.js +28 -0
- package/dist/app/trail.d.ts +9 -0
- package/dist/app/trail.js +18 -0
- package/dist/chat/chat-query-keys.d.ts +3 -3
- package/dist/chat/contract.d.ts +15 -15
- package/dist/chat/conversation-items-query-options.d.ts +114 -114
- package/dist/chat/conversation-source.d.ts +75 -0
- package/dist/chat/conversation-source.js +1 -0
- package/dist/chat/message-text.d.ts +1 -1
- package/dist/chat/message-text.js +114 -46
- package/dist/chat/use-conversation-list.d.ts +6 -6
- package/dist/components/activity-feed.d.ts +2 -2
- package/dist/components/agent-work.d.ts +3 -3
- package/dist/components/app-frame.d.ts +79 -0
- package/dist/components/app-frame.js +92 -0
- package/dist/components/app-navigation.d.ts +3 -3
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/brand-icons.d.ts +3 -3
- package/dist/components/bulk-action-bar.d.ts +1 -1
- package/dist/components/button.d.ts +4 -4
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/centered-page.d.ts +1 -1
- package/dist/components/chart.d.ts +22 -2
- package/dist/components/chart.js +239 -44
- package/dist/components/chat-client-source.d.ts +16 -0
- package/dist/components/chat-client-source.js +256 -0
- package/dist/components/chat-page.d.ts +9 -8
- package/dist/components/chat-page.js +7 -11
- package/dist/components/chat-sheet-controller.d.ts +10 -14
- package/dist/components/chat-sheet-controller.js +2 -3
- package/dist/components/chat-sheet.d.ts +10 -11
- package/dist/components/chat-sheet.js +135 -273
- package/dist/components/chat.d.ts +22 -8
- package/dist/components/chat.js +128 -33
- package/dist/components/checkbox-item.d.ts +1 -1
- package/dist/components/checkbox-list.d.ts +1 -1
- package/dist/components/chip.d.ts +1 -1
- package/dist/components/collapsible.d.ts +1 -1
- package/dist/components/color-theme.d.ts +1 -1
- package/dist/components/column-settings.d.ts +1 -1
- package/dist/components/command-menu.d.ts +1 -1
- package/dist/components/command-palette.d.ts +3 -3
- package/dist/components/command-palette.js +1 -2
- package/dist/components/copyable-id.d.ts +22 -5
- package/dist/components/copyable-id.js +46 -6
- package/dist/components/data-display.d.ts +4 -4
- package/dist/components/data-grid.d.ts +10 -10
- package/dist/components/date-range-picker.d.ts +1 -1
- package/dist/components/date-time.d.ts +1 -1
- package/dist/components/dialog-height-context.d.ts +1 -1
- package/dist/components/document-viewer.d.ts +2 -2
- package/dist/components/drawer.d.ts +6 -6
- package/dist/components/dropzone.d.ts +4 -4
- package/dist/components/editable-field.d.ts +2 -2
- package/dist/components/email-input.d.ts +1 -1
- package/dist/components/environment-frame.d.ts +1 -1
- package/dist/components/expandable-text.d.ts +1 -1
- package/dist/components/feedback-screenshot.d.ts +1 -1
- package/dist/components/feedback-sheet.d.ts +18 -3
- package/dist/components/feedback-sheet.js +216 -55
- package/dist/components/field-context.d.ts +1 -1
- package/dist/components/filter-bar.d.ts +3 -3
- package/dist/components/filter-bar.js +4 -1
- package/dist/components/form-controls.d.ts +4 -4
- package/dist/components/form-layout.d.ts +7 -7
- package/dist/components/hotkeys.d.ts +2 -2
- package/dist/components/icon.d.ts +2 -2
- package/dist/components/input.d.ts +4 -4
- package/dist/components/input.js +3 -2
- package/dist/components/keyboard-shortcuts.d.ts +2 -2
- package/dist/components/keycap.d.ts +2 -2
- package/dist/components/layout.d.ts +15 -15
- package/dist/components/lightbox.d.ts +9 -3
- package/dist/components/lightbox.js +11 -8
- package/dist/components/link.d.ts +1 -1
- package/dist/components/locale.d.ts +32 -2
- package/dist/components/locale.js +25 -1
- package/dist/components/log-list.d.ts +3 -3
- package/dist/components/mention-input.d.ts +2 -2
- package/dist/components/menu.d.ts +2 -2
- package/dist/components/meter.d.ts +1 -1
- package/dist/components/navigation.d.ts +2 -2
- package/dist/components/notification-tray.d.ts +1 -1
- package/dist/components/overlays.d.ts +4 -4
- package/dist/components/page-frame.d.ts +4 -4
- package/dist/components/panel.d.ts +4 -4
- package/dist/components/profile-header.d.ts +1 -1
- package/dist/components/progress.d.ts +1 -1
- package/dist/components/project-avatar.d.ts +1 -1
- package/dist/components/radio-card.d.ts +2 -2
- package/dist/components/record.d.ts +4 -4
- package/dist/components/resolved-file-url.d.ts +19 -0
- package/dist/components/resolved-file-url.js +49 -0
- package/dist/components/scroll-area.d.ts +1 -1
- package/dist/components/secret-dialog.d.ts +1 -1
- package/dist/components/segmented-bar.d.ts +1 -1
- package/dist/components/select.d.ts +4 -4
- package/dist/components/settings.d.ts +4 -4
- package/dist/components/shell-skeleton.d.ts +1 -1
- package/dist/components/shell.d.ts +19 -19
- package/dist/components/shell.js +3 -1
- package/dist/components/side-panel.d.ts +3 -3
- package/dist/components/sidebar.d.ts +6 -6
- package/dist/components/site-banner.d.ts +2 -2
- package/dist/components/skeleton.d.ts +1 -1
- package/dist/components/skeletons.d.ts +5 -5
- package/dist/components/stages.d.ts +5 -5
- package/dist/components/stat-card.d.ts +3 -3
- package/dist/components/status-banner.d.ts +1 -1
- package/dist/components/status.d.ts +2 -2
- package/dist/components/steps.d.ts +1 -1
- package/dist/components/switch.d.ts +1 -1
- package/dist/components/tabs.d.ts +8 -8
- package/dist/components/theme-customizer.d.ts +2 -2
- package/dist/components/toast.d.ts +1 -1
- package/dist/components/use-new-version-available.d.ts +6 -0
- package/dist/components/use-new-version-available.js +29 -0
- package/dist/components/user-chip.d.ts +3 -3
- package/dist/components/wizard.d.ts +2 -2
- package/dist/feedback/contract.d.ts +136 -136
- package/dist/feedback/feedback-controller.d.ts +6 -6
- package/dist/feedback/generated/zod.gen.d.ts +28 -28
- package/dist/feedback/operator-contract.d.ts +11 -11
- package/dist/feedback/operator-controller.d.ts +32 -13
- package/dist/feedback/operator-controller.js +44 -11
- package/dist/styled-system/styles.css +203 -59
- package/dist/theme/tokens.js +17 -8
- package/package.json +32 -3
|
@@ -2,32 +2,32 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
/*
|
|
3
3
|
* The Ask Ciridae sheet: the agent's conversation about the page the user is on, as a chat.
|
|
4
4
|
* Docked, it is an aside beside the page inside `AppShell`; as a drawer, it is the lib's sheet
|
|
5
|
-
* over the page. The host owns the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* over the page. The host owns the conversation's `source` (the lib's chat client through
|
|
6
|
+
* `useChatClientSource`, or the app's own hooks), the lines the page adds to the thread and what
|
|
7
|
+
* a mention does on the page; the sheet owns the thread, the composer, the files waiting to go
|
|
8
|
+
* with a message and the in-flight states. Files dropped on the conversation, pasted
|
|
9
|
+
* into it or picked with the paperclip upload as they arrive. `ChatConversation` is the same
|
|
10
|
+
* thread and composer without the sheet, such as for a full page.
|
|
10
11
|
*
|
|
11
12
|
* `variant="agent"`, the default, is one long-running thread with the main agent: the tasks it
|
|
12
13
|
* runs post cards into the thread under day labels, and a line over the thread sums up what it
|
|
13
14
|
* is working on and what waits on the user. `variant="conversations"` is a chat per question.
|
|
14
15
|
*/
|
|
15
16
|
import { ArrowsOutSimpleIcon, ArrowSquareOutIcon, PaperclipIcon, SidebarSimpleIcon, SparkleIcon, XIcon, } from "@phosphor-icons/react";
|
|
16
|
-
import {
|
|
17
|
+
import { useRef, useState, } from "react";
|
|
17
18
|
import { css, cx } from "#primitives/css";
|
|
18
19
|
import { parseMentions } from "../chat/contract.js";
|
|
19
|
-
import { describeApprovalRequest } from "../chat/describe-tool-call.js";
|
|
20
20
|
import { MessageText, splitSources } from "../chat/message-text.js";
|
|
21
|
-
import { useChatConversation } from "../chat/use-chat-conversation.js";
|
|
22
21
|
import { AgentTaskCard, AgentWork } from "./agent-work.js";
|
|
23
|
-
import {
|
|
24
|
-
import { ChatComposer, ChatSuggestions, ChatThread } from "./chat.js";
|
|
22
|
+
import { IconButton } from "./button.js";
|
|
23
|
+
import { ChatComposer, ChatSuggestions, ChatSystemLine, ChatThread, } from "./chat.js";
|
|
25
24
|
import { CopyButton } from "./copyable-id.js";
|
|
26
25
|
import { Drawer, DrawerBody } from "./drawer.js";
|
|
27
|
-
import { Dropzone, DropzoneFileList } from "./dropzone.js";
|
|
28
|
-
import { icon } from "./icon.js";
|
|
26
|
+
import { Dropzone, DropzoneFileList, useDropzonePicker } from "./dropzone.js";
|
|
27
|
+
import { icon, Spinner } from "./icon.js";
|
|
29
28
|
import { heading } from "./layout.js";
|
|
30
29
|
import { useCiridaeLocale } from "./locale.js";
|
|
30
|
+
export { useChatClientConversations, useChatClientSource, } from "./chat-client-source.js";
|
|
31
31
|
/* The docked aside, drawn like the shell's main panel beside it. */
|
|
32
32
|
const docked = css({
|
|
33
33
|
display: "flex",
|
|
@@ -133,7 +133,7 @@ const dockedHeader = css({
|
|
|
133
133
|
paddingInlineEnd: "panel.default",
|
|
134
134
|
borderBottom: "1px solid token(colors.border.control)",
|
|
135
135
|
});
|
|
136
|
-
const headerActions = css({ display: "flex", alignItems: "center", gap: "layout.
|
|
136
|
+
const headerActions = css({ display: "flex", alignItems: "center", gap: "layout.tightGap" });
|
|
137
137
|
const titleLine = css({
|
|
138
138
|
display: "inline-flex",
|
|
139
139
|
alignItems: "center",
|
|
@@ -166,54 +166,7 @@ const contextChip = css({
|
|
|
166
166
|
});
|
|
167
167
|
/* The thread's scrolling area, between the header and the composer. */
|
|
168
168
|
const threadScroll = css({ flex: "1", minHeight: "0", padding: "none", overflowY: "auto" });
|
|
169
|
-
|
|
170
|
-
const card = css({
|
|
171
|
-
display: "grid",
|
|
172
|
-
gap: "control.iconGap",
|
|
173
|
-
width: "100%",
|
|
174
|
-
maxWidth: "chat.bubble",
|
|
175
|
-
paddingBlock: "control.block",
|
|
176
|
-
paddingInline: "control.compactInline",
|
|
177
|
-
border: "1px solid token(colors.border.control)",
|
|
178
|
-
borderRadius: "field",
|
|
179
|
-
background: "bg.card",
|
|
180
|
-
fontSize: "caption",
|
|
181
|
-
lineHeight: "log",
|
|
182
|
-
});
|
|
183
|
-
const cardLabel = css({ color: "fg.muted", fontSize: "code", lineHeight: "compact" });
|
|
184
|
-
const argumentList = css({
|
|
185
|
-
display: "grid",
|
|
186
|
-
gridTemplateColumns: "auto minmax(0, 1fr)",
|
|
187
|
-
columnGap: "layout.controlGap",
|
|
188
|
-
margin: "none",
|
|
189
|
-
fontSize: "code",
|
|
190
|
-
lineHeight: "compact",
|
|
191
|
-
"& > dt": { color: "fg.muted", whiteSpace: "nowrap" },
|
|
192
|
-
"& > dd": { margin: "none", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
193
|
-
});
|
|
194
|
-
const cardActions = css({ display: "flex", justifyContent: "flex-end", gap: "layout.controlGap" });
|
|
195
|
-
// Option rows run to the card's edges, split by its hairlines.
|
|
196
|
-
const optionList = css({
|
|
197
|
-
marginInline: "calc(token(spacing.control.compactInline) * -1)",
|
|
198
|
-
marginBlockEnd: "calc(token(spacing.control.block) * -1)",
|
|
199
|
-
"& > button": {
|
|
200
|
-
display: "block",
|
|
201
|
-
width: "100%",
|
|
202
|
-
paddingBlock: "control.compactBlock",
|
|
203
|
-
paddingInline: "control.compactInline",
|
|
204
|
-
border: "0",
|
|
205
|
-
borderTop: "1px solid token(colors.border.control)",
|
|
206
|
-
background: "transparent",
|
|
207
|
-
color: "fg.primary",
|
|
208
|
-
font: "inherit",
|
|
209
|
-
textAlign: "start",
|
|
210
|
-
cursor: "pointer",
|
|
211
|
-
_hover: { background: "bg.hover" },
|
|
212
|
-
_focusVisible: { focusRing: "inset" },
|
|
213
|
-
"&:last-child": { borderBottomLeftRadius: "field", borderBottomRightRadius: "field" },
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
const optionDetail = css({ display: "block", color: "fg.muted", fontSize: "code" });
|
|
169
|
+
const centered = css({ display: "grid", placeItems: "center", height: "100%" });
|
|
217
170
|
/* The thread and composer, the drop target for files; it fills what the sheet or page gives it. */
|
|
218
171
|
const conversationRoot = css({
|
|
219
172
|
display: "flex",
|
|
@@ -230,13 +183,12 @@ const composerFooter = css({
|
|
|
230
183
|
borderTop: "1px solid token(colors.border.control)",
|
|
231
184
|
"&[data-layout=wide]": { borderTop: "0", paddingBlockEnd: "panel.default" },
|
|
232
185
|
});
|
|
186
|
+
/* Over the composer, what goes with it: an error. */
|
|
187
|
+
const footerStack = css({ display: "grid", gap: "layout.tightGap", minWidth: "0" });
|
|
233
188
|
/* Full page: the thread and composer in one readable column down the middle. */
|
|
234
189
|
const wideColumn = css({ width: "100%", maxWidth: "chat.thread", marginInline: "auto" });
|
|
235
|
-
const hiddenInput = css({ display: "none" });
|
|
236
190
|
/* In a drawer, the context row and the conversation stack under its header. */
|
|
237
191
|
const drawerStack = css({ display: "flex", flex: "1", minHeight: "0", flexDirection: "column" });
|
|
238
|
-
/** How many of an approval's arguments show before the rest become "+N". */
|
|
239
|
-
const maximumArgumentRows = 3;
|
|
240
192
|
export function ChatSheet({ open, onOpenChange, title, context, placement = "drawer", onPlacementChange, onOpenFullPage, ...conversationProps }) {
|
|
241
193
|
const { variant = "agent", tasks } = conversationProps;
|
|
242
194
|
const { labels } = useCiridaeLocale();
|
|
@@ -245,7 +197,7 @@ export function ChatSheet({ open, onOpenChange, title, context, placement = "dra
|
|
|
245
197
|
const titleContent = (_jsxs("span", { className: titleLine, children: [_jsx(SparkleIcon, { className: icon({ size: "small" }), weight: "fill" }), title] }));
|
|
246
198
|
const actions = (_jsxs(_Fragment, { children: [onOpenFullPage && (_jsx(IconButton, { "aria-label": labels.chat.openFullPage, variant: "ghost", size: "small", onClick: onOpenFullPage, children: _jsx(ArrowsOutSimpleIcon, { className: icon({ size: "small" }) }) })), onPlacementChange &&
|
|
247
199
|
(placement === "docked" ? (_jsx(IconButton, { "aria-label": labels.chat.undock, variant: "ghost", size: "small", onClick: () => onPlacementChange("drawer"), children: _jsx(ArrowSquareOutIcon, { className: icon({ size: "small" }) }) })) : (_jsx(IconButton, { "aria-label": labels.chat.dock, variant: "ghost", size: "small", onClick: () => onPlacementChange("docked"), children: _jsx(SidebarSimpleIcon, { className: icon({ size: "small" }), mirrored: true }) })))] }));
|
|
248
|
-
const contextContent = (_jsxs(_Fragment, { children: [context && context.length > 0 && (_jsxs("div", { className: contextRow, children: [
|
|
200
|
+
const contextContent = (_jsxs(_Fragment, { children: [context && context.length > 0 && (_jsxs("div", { className: contextRow, children: [labels.chat.reading, context.map((item) => (_jsxs("span", { className: contextChip, "data-ciridae-ui": "", children: [item.icon, item.label] }, item.label)))] })), variant === "agent" && tasks && _jsx(AgentWork, { layout: "summary", tasks: tasks })] }));
|
|
249
201
|
if (placement === "docked") {
|
|
250
202
|
if (!open)
|
|
251
203
|
return null;
|
|
@@ -254,15 +206,17 @@ export function ChatSheet({ open, onOpenChange, title, context, placement = "dra
|
|
|
254
206
|
return (_jsx(Drawer, { open: open, onOpenChange: onOpenChange, size: "narrow", title: title, icon: _jsx(SparkleIcon, { className: icon({ size: "small" }), weight: "fill" }), actions: actions, children: _jsxs("div", { className: drawerStack, children: [contextContent, _jsx(ChatConversation, { ...conversationProps, scrollBody: DrawerBody })] }) }));
|
|
255
207
|
}
|
|
256
208
|
/* The files for the next message: each uploads as it is added; send waits until all have. */
|
|
257
|
-
function useComposerFiles(
|
|
209
|
+
function useComposerFiles(upload, onUploadError) {
|
|
258
210
|
const [entries, setEntries] = useState([]);
|
|
259
211
|
const remove = (file) => setEntries((existing) => existing.filter((entry) => entry.file !== file));
|
|
260
212
|
const add = (files) => {
|
|
213
|
+
if (!upload)
|
|
214
|
+
return;
|
|
261
215
|
setEntries((existing) => [...existing, ...files.map((file) => ({ file, attachment: null }))]);
|
|
262
216
|
for (const file of files) {
|
|
263
|
-
|
|
217
|
+
upload(file).then((attachment) => setEntries((existing) => existing.map((entry) => (entry.file === file ? { file, attachment } : entry))), (error) => {
|
|
264
218
|
remove(file);
|
|
265
|
-
|
|
219
|
+
onUploadError(error);
|
|
266
220
|
});
|
|
267
221
|
}
|
|
268
222
|
};
|
|
@@ -279,54 +233,53 @@ function useComposerFiles(client) {
|
|
|
279
233
|
* The thread and the composer: everything that reads or writes the conversation. `layout="wide"`
|
|
280
234
|
* sets both in one readable column for a full page; the sheet uses `sheet`.
|
|
281
235
|
*/
|
|
282
|
-
export function ChatConversation({
|
|
236
|
+
export function ChatConversation({ source, placeholder, draft, onDraftChange, composerRef, mentions, mentionMark, onMentionSelect, mentionHref, events = [], assistantName: ownAssistantName, suggestions, copyReplies = false, variant = "agent", tasks, onTaskReply, layout = "sheet", scrollBody: ScrollBody = "div", }) {
|
|
283
237
|
const { labels } = useCiridaeLocale();
|
|
284
238
|
const assistantName = ownAssistantName ?? labels.chat.assistantName;
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const [
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
239
|
+
const [ownDraft, setOwnDraft] = useState("");
|
|
240
|
+
const currentDraft = draft ?? ownDraft;
|
|
241
|
+
const setDraft = (next) => {
|
|
242
|
+
setOwnDraft(next);
|
|
243
|
+
onDraftChange?.(next);
|
|
244
|
+
};
|
|
245
|
+
const [sending, setSending] = useState(false);
|
|
246
|
+
const [composerError, setComposerError] = useState(null);
|
|
247
|
+
const showError = (error) => setComposerError(error instanceof Error ? error.message : String(error));
|
|
248
|
+
const files = useComposerFiles(source.upload, showError);
|
|
249
|
+
const send = async (text) => {
|
|
250
|
+
setSending(true);
|
|
251
|
+
setComposerError(null);
|
|
295
252
|
try {
|
|
296
|
-
await
|
|
253
|
+
await source.send({ text, attachments: files.attachments });
|
|
254
|
+
files.clear();
|
|
297
255
|
}
|
|
298
256
|
catch (error) {
|
|
299
|
-
|
|
257
|
+
// The composer cleared as it sent: the text comes back, with the files still waiting.
|
|
258
|
+
setDraft(text);
|
|
259
|
+
showError(error);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
setSending(false);
|
|
300
263
|
}
|
|
301
264
|
};
|
|
302
|
-
const renderReply = (content, plain) => (_jsx(MessageText, { content: content, plain: plain, onMentionSelect: onMentionSelect, mentionHref: mentionHref }));
|
|
303
265
|
const entries = buildThread({
|
|
304
266
|
labels: labels.chat,
|
|
305
|
-
|
|
306
|
-
pendingMessages: state.pendingMessages,
|
|
307
|
-
response: state.response,
|
|
308
|
-
isResponseActive: state.isResponseActive,
|
|
267
|
+
source,
|
|
309
268
|
events,
|
|
310
269
|
tasks: variant === "agent" ? (tasks ?? []) : [],
|
|
311
|
-
|
|
312
|
-
renderReply,
|
|
270
|
+
renderReply: (content, plain) => (_jsx(MessageText, { content: content, plain: plain, onMentionSelect: onMentionSelect, mentionHref: mentionHref })),
|
|
313
271
|
replyActions: copyReplies ? (content) => _jsx(CopyReplyButton, { content: content }) : undefined,
|
|
314
|
-
|
|
315
|
-
retryPending: (message) => {
|
|
316
|
-
state.discardPendingMessage(message.localId);
|
|
317
|
-
void send(message.content, message.attachments);
|
|
318
|
-
},
|
|
319
|
-
retryMessage: (message) => void send(message.content, message.attachments),
|
|
272
|
+
taskCard: (task) => _jsx(AgentTaskCard, { task: task, onReply: onTaskReply }),
|
|
320
273
|
});
|
|
321
274
|
const wide = layout === "wide";
|
|
322
|
-
const showSuggestions = entries.length === 0 && !
|
|
323
|
-
return (_jsxs(Dropzone, { onFiles: files.add, className: conversationRoot, children: [_jsx(ScrollBody, { className: threadScroll, children: showSuggestions ? (_jsx(ChatSuggestions, { className: cx(wide && wideColumn), suggestions: suggestions, onSelect: (suggestion) => void send(suggestion
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
275
|
+
const showSuggestions = entries.length === 0 && !source.loading && suggestions !== undefined && suggestions.length > 0;
|
|
276
|
+
return (_jsxs(Dropzone, { onFiles: files.add, disabled: !source.upload, className: conversationRoot, children: [_jsx(ScrollBody, { className: threadScroll, children: source.loading && entries.length === 0 ? (_jsx("div", { className: centered, children: _jsx(Spinner, { label: labels.common.loading }) })) : showSuggestions ? (_jsx(ChatSuggestions, { className: cx(wide && wideColumn), suggestions: suggestions, onSelect: (suggestion) => void send(suggestion) })) : (_jsx(ChatThread, { className: cx(wide && wideColumn), entries: entries, density: "compact", attachmentTiles: true, dayLabels: true, resolveAttachmentUrl: source.resolveAttachmentUrl, assistant: { name: assistantName, kind: "agent" }, renderCustom: (content) => content })) }), _jsx("div", { className: composerFooter, "data-layout": layout, children: _jsxs("div", { className: cx(footerStack, wide && wideColumn), children: [composerError && (_jsx(ChatSystemLine, { tone: "error", role: "alert", children: composerError })), _jsx(ChatComposer, { variant: "sheet", placeholder: placeholder, autoFocus: true, value: currentDraft, onValueChange: setDraft, inputProps: { ref: composerRef }, mentions: mentions, mentionMark: mentionMark, sendDisabled: files.uploading || sending, sendsWithoutText: files.attachments.length > 0, onPasteFiles: source.upload ? files.add : undefined, attachments: files.files.length > 0 && (_jsx(DropzoneFileList, { variant: "tiles", files: files.files, pending: files.uploading, onRemove: files.remove })), onSend: (content) => void send(content), tools: source.upload && _jsx(AttachFilesButton, {}) })] }) })] }));
|
|
277
|
+
}
|
|
278
|
+
/** The paperclip: the enclosing `Dropzone`'s file picker. */
|
|
279
|
+
function AttachFilesButton() {
|
|
280
|
+
const { labels } = useCiridaeLocale();
|
|
281
|
+
const picker = useDropzonePicker();
|
|
282
|
+
return (_jsx(IconButton, { "aria-label": labels.chat.attachFile, size: "compact", variant: "ghost", disabled: picker.disabled, onClick: picker.openPicker, children: _jsx(PaperclipIcon, { className: icon({ size: "small" }) }) }));
|
|
330
283
|
}
|
|
331
284
|
/** Copies a reply's text, its mentions as their labels, and shows a tick for a moment. */
|
|
332
285
|
function CopyReplyButton({ content }) {
|
|
@@ -334,88 +287,23 @@ function CopyReplyButton({ content }) {
|
|
|
334
287
|
.map((part) => (typeof part === "string" ? part : part.label))
|
|
335
288
|
.join("") }));
|
|
336
289
|
}
|
|
337
|
-
/**
|
|
338
|
-
|
|
290
|
+
/**
|
|
291
|
+
* The source's items, the page's events, the agent's tasks and how the latest turn stands, in one
|
|
292
|
+
* timeline: the turn's typing, failure or stop at its foot.
|
|
293
|
+
*/
|
|
294
|
+
function buildThread({ labels, source: { items, status, failureMessage, retry }, events, tasks, renderReply, replyActions, taskCard, }) {
|
|
339
295
|
const entries = [];
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
switch (item.type) {
|
|
352
|
-
case "message": {
|
|
353
|
-
const { content } = item;
|
|
354
|
-
const { sources } = splitSources(content);
|
|
355
|
-
entries.push({
|
|
356
|
-
kind: "message",
|
|
357
|
-
id: item.id,
|
|
358
|
-
side: item.role,
|
|
359
|
-
at,
|
|
360
|
-
// Files sent alone keep an empty body, so the thread draws their tiles with no bubble.
|
|
361
|
-
body: content.trim() ? renderReply(content, item.role === "user") : "",
|
|
362
|
-
attachments: toAttachments(item.attachments),
|
|
363
|
-
sources: item.role === "assistant"
|
|
364
|
-
? sources.map((source) => ({
|
|
365
|
-
id: source.id,
|
|
366
|
-
label: source.label,
|
|
367
|
-
href: source.href,
|
|
368
|
-
}))
|
|
369
|
-
: undefined,
|
|
370
|
-
actions: item.role === "assistant" && !turnUnfinished ? replyActions?.(content) : undefined,
|
|
371
|
-
});
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
case "approval_request":
|
|
375
|
-
if (item.decision === null && waitingOnUser) {
|
|
376
|
-
entries.push({
|
|
377
|
-
kind: "custom",
|
|
378
|
-
id: item.id,
|
|
379
|
-
side: "assistant",
|
|
380
|
-
at,
|
|
381
|
-
data: { kind: "approval", item },
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
else if (item.decision !== null) {
|
|
385
|
-
entries.push({
|
|
386
|
-
kind: "line",
|
|
387
|
-
id: item.id,
|
|
388
|
-
at,
|
|
389
|
-
text: `${describeApprovalRequest(item.tool)} · ${item.decision === "allow" ? "allowed" : "denied"}`,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
break;
|
|
393
|
-
case "question":
|
|
394
|
-
if (item.answers === null && waitingOnUser) {
|
|
395
|
-
entries.push({
|
|
396
|
-
kind: "custom",
|
|
397
|
-
id: item.id,
|
|
398
|
-
side: "assistant",
|
|
399
|
-
at,
|
|
400
|
-
data: { kind: "question", item },
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
else if (item.answers !== null) {
|
|
404
|
-
entries.push({
|
|
405
|
-
kind: "line",
|
|
406
|
-
id: item.id,
|
|
407
|
-
at,
|
|
408
|
-
text: labels.answered(item.answers.flat().filter(Boolean).join(", ")),
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
break;
|
|
412
|
-
// What the agent runs on its way to a reply stays behind its typing.
|
|
413
|
-
case "function_call":
|
|
414
|
-
case "function_call_output":
|
|
415
|
-
case "reasoning":
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
296
|
+
// The latest turn is everything after the user's last message; its replies are done once it is.
|
|
297
|
+
let latestTurnStart = -1;
|
|
298
|
+
items.forEach((item, index) => {
|
|
299
|
+
if (item.kind === "message" && item.role === "user")
|
|
300
|
+
latestTurnStart = index;
|
|
301
|
+
});
|
|
302
|
+
const latestTurnUnfinished = status === "running" || status === "failed";
|
|
303
|
+
items.forEach((item, index) => {
|
|
304
|
+
const inUnfinishedTurn = latestTurnUnfinished && index > latestTurnStart;
|
|
305
|
+
entries.push(...toEntries(item, inUnfinishedTurn));
|
|
306
|
+
});
|
|
419
307
|
for (const event of events) {
|
|
420
308
|
entries.push({
|
|
421
309
|
kind: "line",
|
|
@@ -432,7 +320,7 @@ function buildThread({ items, pendingMessages, response, isResponseActive, event
|
|
|
432
320
|
id: `task-${task.id}`,
|
|
433
321
|
side: "assistant",
|
|
434
322
|
at: Date.parse(task.ask.askedAt),
|
|
435
|
-
data:
|
|
323
|
+
data: taskCard(task),
|
|
436
324
|
});
|
|
437
325
|
}
|
|
438
326
|
if ((task.status === "done" || task.status === "failed") && task.summary) {
|
|
@@ -447,98 +335,72 @@ function buildThread({ items, pendingMessages, response, isResponseActive, event
|
|
|
447
335
|
}
|
|
448
336
|
}
|
|
449
337
|
entries.sort((first, second) => first.at - second.at);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
338
|
+
const lastItem = items[items.length - 1];
|
|
339
|
+
const at = Math.max(lastItem?.at ?? 0, entries[entries.length - 1]?.at ?? 0) || Date.now();
|
|
340
|
+
appendTurnEnding(entries, { status, retry, failureMessage, lastItem, at, labels });
|
|
341
|
+
return entries;
|
|
342
|
+
function toEntries(item, inUnfinishedTurn) {
|
|
343
|
+
switch (item.kind) {
|
|
344
|
+
case "message": {
|
|
345
|
+
const assistant = item.role === "assistant";
|
|
346
|
+
const message = {
|
|
347
|
+
kind: "message",
|
|
348
|
+
id: item.id,
|
|
349
|
+
side: item.role,
|
|
350
|
+
at: item.at,
|
|
351
|
+
// Files sent alone keep an empty body, so the thread draws their tiles with no bubble.
|
|
352
|
+
body: item.text.trim() ? renderReply(item.text, !assistant) : "",
|
|
353
|
+
attachments: item.attachments,
|
|
354
|
+
sending: item.sending,
|
|
355
|
+
sources: assistant
|
|
356
|
+
? splitSources(item.text).sources.map((source) => ({
|
|
357
|
+
id: source.id,
|
|
358
|
+
label: source.label,
|
|
359
|
+
href: source.href,
|
|
360
|
+
}))
|
|
361
|
+
: undefined,
|
|
362
|
+
actions: assistant && !inUnfinishedTurn ? replyActions?.(item.text) : undefined,
|
|
363
|
+
};
|
|
364
|
+
if (!item.failure)
|
|
365
|
+
return [message];
|
|
366
|
+
return [
|
|
367
|
+
message,
|
|
368
|
+
{
|
|
369
|
+
kind: "failure",
|
|
370
|
+
id: `failure-${item.id}`,
|
|
371
|
+
at: item.at,
|
|
372
|
+
message: item.failure.message ?? labels.notSent,
|
|
373
|
+
onRetry: item.failure.retry,
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
case "line":
|
|
378
|
+
return [{ kind: "line", id: item.id, at: item.at, text: item.text, tone: item.tone }];
|
|
379
|
+
case "card":
|
|
380
|
+
return [{ kind: "custom", id: item.id, side: "assistant", at: item.at, data: item.content }];
|
|
492
381
|
}
|
|
493
382
|
}
|
|
494
|
-
const last = entries[entries.length - 1];
|
|
495
|
-
if (isResponseActive &&
|
|
496
|
-
response?.status !== "requires_action" &&
|
|
497
|
-
!(last?.kind === "message" && last.side === "assistant")) {
|
|
498
|
-
entries.push({ kind: "typing", id: "typing", at: last?.at ?? Date.now() });
|
|
499
|
-
}
|
|
500
|
-
return entries;
|
|
501
|
-
}
|
|
502
|
-
/** A tool call waiting for the user: what it would do, its arguments, and Deny or Allow. */
|
|
503
|
-
function ApprovalCard({ item, onDecide, }) {
|
|
504
|
-
const { labels } = useCiridaeLocale();
|
|
505
|
-
const rows = listArgumentRows(item.arguments);
|
|
506
|
-
return (_jsxs("div", { className: card, children: [_jsx("span", { children: describeApprovalRequest(item.tool) }), rows.length > 0 && (_jsxs("dl", { className: argumentList, children: [rows.slice(0, maximumArgumentRows).map(([name, value]) => (_jsxs(Fragment, { children: [_jsx("dt", { children: name }), _jsx("dd", { children: value })] }, name))), rows.length > maximumArgumentRows && (_jsxs("dd", { className: cardLabel, children: ["+", rows.length - maximumArgumentRows] }))] })), _jsxs("div", { className: cardActions, children: [_jsx(Button, { variant: "secondary", onClick: () => void onDecide(item, "deny"), children: labels.chat.deny }), _jsx(Button, { onClick: () => void onDecide(item, "allow"), children: labels.chat.allow })] })] }));
|
|
507
383
|
}
|
|
508
|
-
/** The
|
|
509
|
-
function
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
384
|
+
/** The latest turn's foot: the agent typing while it runs, its failure with Retry, or Stopped. */
|
|
385
|
+
function appendTurnEnding(entries, { status, retry, failureMessage, lastItem, at, labels, }) {
|
|
386
|
+
switch (status) {
|
|
387
|
+
case "running":
|
|
388
|
+
// Once its reply or a card waiting on the user is there, the agent is no longer typing.
|
|
389
|
+
if (lastItem?.kind !== "card" &&
|
|
390
|
+
!(lastItem?.kind === "message" && lastItem.role === "assistant"))
|
|
391
|
+
entries.push({ kind: "typing", id: "typing", at });
|
|
392
|
+
return;
|
|
393
|
+
case "failed": {
|
|
394
|
+
const message = failureMessage ?? labels.failed;
|
|
395
|
+
entries.push(retry
|
|
396
|
+
? { kind: "failure", id: "turn-failure", at, message, onRetry: retry }
|
|
397
|
+
: { kind: "line", id: "turn-failure", at, text: message, tone: "error" });
|
|
398
|
+
return;
|
|
519
399
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
return (item.type === "message" &&
|
|
526
|
-
item.role === "user" &&
|
|
527
|
-
item.content === message.content &&
|
|
528
|
-
Date.parse(item.created_at) >= message.sentAt - 60000);
|
|
529
|
-
}
|
|
530
|
-
/** A call's JSON arguments as name and value pairs, values shortened to one line. */
|
|
531
|
-
function listArgumentRows(argumentsJson) {
|
|
532
|
-
try {
|
|
533
|
-
const parsed = JSON.parse(argumentsJson);
|
|
534
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
535
|
-
return [];
|
|
536
|
-
return Object.entries(parsed).map(([name, value]) => [
|
|
537
|
-
name.replace(/[_-]+/g, " "),
|
|
538
|
-
typeof value === "string" ? value : JSON.stringify(value),
|
|
539
|
-
]);
|
|
540
|
-
}
|
|
541
|
-
catch {
|
|
542
|
-
return [];
|
|
400
|
+
case "stopped":
|
|
401
|
+
entries.push({ kind: "line", id: "turn-stopped", at, text: labels.stopped });
|
|
402
|
+
return;
|
|
403
|
+
case "idle":
|
|
404
|
+
return;
|
|
543
405
|
}
|
|
544
406
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type ComponentProps, type ReactNode } from "react";
|
|
2
2
|
import { type MessageSide } from "../chat/message-group-position.js";
|
|
3
3
|
import { type MentionMark, type MentionTrigger } from "./mention-input.js";
|
|
4
|
+
import { type ResolveFileUrl } from "./resolved-file-url.js";
|
|
5
|
+
export { MessageText, splitSources, type MessageSource, type MessageTextProps, } from "../chat/message-text.js";
|
|
6
|
+
export type { ResolvableFile, ResolveFileUrl } from "./resolved-file-url.js";
|
|
4
7
|
export type ChatComposerProps = Omit<ComponentProps<"div">, "onSubmit"> & {
|
|
5
8
|
/** The text, each mention in it as `[@label](mention://kind/id)`. */
|
|
6
9
|
value?: string;
|
|
@@ -18,6 +21,11 @@ export type ChatComposerProps = Omit<ComponentProps<"div">, "onSubmit"> & {
|
|
|
18
21
|
sendsWithoutText?: boolean;
|
|
19
22
|
/** What goes with the message, such as its files as chips, just above the text. */
|
|
20
23
|
attachments?: ReactNode;
|
|
24
|
+
/**
|
|
25
|
+
* Files pasted into the composer, such as a screenshot or files copied from the desktop, to
|
|
26
|
+
* attach as a drop or the picker would. Rich text pasted with a picture of itself stays text.
|
|
27
|
+
*/
|
|
28
|
+
onPasteFiles?: (files: File[]) => void;
|
|
21
29
|
/**
|
|
22
30
|
* `page` is a chat page's wide card; `sheet` the field at the foot of the agent sheet; `feed`
|
|
23
31
|
* a comment field, which sends on Cmd/Ctrl+Enter rather than Enter.
|
|
@@ -33,21 +41,21 @@ export type ChatComposerProps = Omit<ComponentProps<"div">, "onSubmit"> & {
|
|
|
33
41
|
/** Props for the text field itself, such as a ref. */
|
|
34
42
|
inputProps?: Omit<ComponentProps<"div">, "children" | "defaultValue" | "onChange" | "onSubmit" | "placeholder">;
|
|
35
43
|
};
|
|
36
|
-
export declare function ChatComposer({ value, defaultValue, onValueChange, onSend, placeholder, ariaLabel: ownAriaLabel, tools, actions, submitLabel: ownSubmitLabel, sendDisabled, sendsWithoutText, attachments, variant, autoFocus, mentions, mentionMark, mentionsLabel, inputProps, className, ...props }: ChatComposerProps): import("
|
|
44
|
+
export declare function ChatComposer({ value, defaultValue, onValueChange, onSend, placeholder, ariaLabel: ownAriaLabel, tools, actions, submitLabel: ownSubmitLabel, sendDisabled, sendsWithoutText, attachments, onPasteFiles, variant, autoFocus, mentions, mentionMark, mentionsLabel, inputProps, className, ...props }: ChatComposerProps): import("react").JSX.Element;
|
|
37
45
|
export type ChatBubbleProps = ComponentProps<"div"> & {
|
|
38
46
|
side: "user" | "assistant";
|
|
39
47
|
/** Where the bubble sits in its run of messages from one side; see `listMessageGroupPositions`. */
|
|
40
48
|
groupPosition?: "single" | "first" | "middle" | "last";
|
|
41
49
|
};
|
|
42
50
|
/** A message bubble in the agent sheet. */
|
|
43
|
-
export declare function ChatBubble({ side, groupPosition, className, ...props }: ChatBubbleProps): import("
|
|
51
|
+
export declare function ChatBubble({ side, groupPosition, className, ...props }: ChatBubbleProps): import("react").JSX.Element;
|
|
44
52
|
/** A centered line between bubbles: a finished tool call, a decision, something the page did. */
|
|
45
53
|
export declare function ChatSystemLine({ tone, className, ...props }: ComponentProps<"div"> & {
|
|
46
54
|
tone?: "muted" | "error";
|
|
47
|
-
}): import("
|
|
55
|
+
}): import("react").JSX.Element;
|
|
48
56
|
/** Three pulsing dots: the assistant is typing. */
|
|
49
|
-
export declare function ChatTypingDots({ className, ...props }: ComponentProps<"span">): import("
|
|
50
|
-
export declare function ChatAction({ className, ...props }: ComponentProps<"button">): import("
|
|
57
|
+
export declare function ChatTypingDots({ className, ...props }: ComponentProps<"span">): import("react").JSX.Element;
|
|
58
|
+
export declare function ChatAction({ className, ...props }: ComponentProps<"button">): import("react").JSX.Element;
|
|
51
59
|
/** Who wrote a message: drawn as the avatar beside its group and named in the group's footer. */
|
|
52
60
|
export type ChatAuthor = {
|
|
53
61
|
name: string;
|
|
@@ -58,7 +66,8 @@ export type ChatAuthor = {
|
|
|
58
66
|
};
|
|
59
67
|
/**
|
|
60
68
|
* A file sent with a message, linked when it has a `url`. With the thread's `attachmentTiles`, an
|
|
61
|
-
* image (`mediaType` `image/*`)
|
|
69
|
+
* image (`mediaType` `image/*`) shows as a thumbnail; one with no `url` loads through the thread's
|
|
70
|
+
* `resolveAttachmentUrl`.
|
|
62
71
|
*/
|
|
63
72
|
export type ChatAttachment = {
|
|
64
73
|
id: string;
|
|
@@ -123,13 +132,18 @@ export type ChatThreadProps<Custom = never> = {
|
|
|
123
132
|
attachmentTiles?: boolean;
|
|
124
133
|
/** Heads each day's entries with its date, for a thread that runs over days. */
|
|
125
134
|
dayLabels?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Loads the link of an attachment with no `url`, such as a file the service serves only with a
|
|
137
|
+
* bearer token, for its thumbnail and the lightbox; see `ResolveFileUrl`.
|
|
138
|
+
*/
|
|
139
|
+
resolveAttachmentUrl?: ResolveFileUrl<ChatAttachment>;
|
|
126
140
|
className?: string;
|
|
127
141
|
};
|
|
128
|
-
export declare function ChatThread<Custom = never>({ entries, assistant, renderCustom, formatTime, density, attachmentTiles, dayLabels, className, }: ChatThreadProps<Custom>): import("
|
|
142
|
+
export declare function ChatThread<Custom = never>({ entries, assistant, renderCustom, formatTime, density, attachmentTiles, dayLabels, resolveAttachmentUrl, className, }: ChatThreadProps<Custom>): import("react").JSX.Element;
|
|
129
143
|
export type ChatSuggestionsProps = {
|
|
130
144
|
suggestions: readonly string[];
|
|
131
145
|
onSelect: (suggestion: string) => void;
|
|
132
146
|
className?: string;
|
|
133
147
|
};
|
|
134
148
|
/** Prompts to start a conversation from, stacked at the foot of an empty thread. */
|
|
135
|
-
export declare function ChatSuggestions({ suggestions, onSelect, className }: ChatSuggestionsProps): import("
|
|
149
|
+
export declare function ChatSuggestions({ suggestions, onSelect, className }: ChatSuggestionsProps): import("react").JSX.Element;
|