@ciridae/ui 1.0.2 → 1.0.4
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 +89 -0
- package/dist/chat/conversation-source.js +1 -0
- package/dist/chat/describe-tool-call.d.ts +3 -3
- package/dist/chat/describe-tool-call.js +3 -3
- package/dist/chat/message-text.d.ts +1 -1
- package/dist/chat/message-text.js +116 -48
- package/dist/chat/use-conversation-list.d.ts +6 -6
- package/dist/ciridae.css +16 -4
- package/dist/components/activity-feed.d.ts +3 -2
- package/dist/components/activity-feed.js +1 -0
- package/dist/components/agent-work.d.ts +3 -3
- package/dist/components/app-frame.d.ts +91 -0
- package/dist/components/app-frame.js +134 -0
- package/dist/components/app-navigation.d.ts +30 -3
- package/dist/components/app-navigation.js +105 -14
- package/dist/components/badge.d.ts +30 -5
- package/dist/components/badge.js +134 -15
- package/dist/components/brand-icons.d.ts +3 -3
- package/dist/components/bulk-action-bar.d.ts +1 -1
- package/dist/components/bulk-action-bar.js +3 -3
- package/dist/components/button.d.ts +12 -5
- package/dist/components/button.js +77 -44
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/calendar.js +44 -17
- package/dist/components/centered-page.d.ts +1 -1
- package/dist/components/centered-page.js +2 -2
- package/dist/components/chart.d.ts +22 -2
- package/dist/components/chart.js +251 -51
- package/dist/components/chat-client-source.d.ts +16 -0
- package/dist/components/chat-client-source.js +297 -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 +11 -14
- package/dist/components/chat-sheet-controller.js +3 -3
- package/dist/components/chat-sheet.d.ts +18 -11
- package/dist/components/chat-sheet.js +188 -289
- package/dist/components/chat.d.ts +109 -19
- package/dist/components/chat.js +520 -124
- package/dist/components/checkbox-item.d.ts +6 -3
- package/dist/components/checkbox-item.js +9 -5
- package/dist/components/checkbox-list.d.ts +1 -1
- package/dist/components/chip.d.ts +6 -1
- package/dist/components/chip.js +27 -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/column-settings.js +1 -1
- package/dist/components/command-menu.d.ts +5 -1
- package/dist/components/command-menu.js +98 -32
- package/dist/components/command-palette.d.ts +7 -3
- package/dist/components/command-palette.js +20 -11
- package/dist/components/copyable-id.d.ts +22 -5
- package/dist/components/copyable-id.js +46 -6
- package/dist/components/data-display.d.ts +39 -8
- package/dist/components/data-display.js +138 -19
- package/dist/components/data-grid.d.ts +23 -11
- package/dist/components/data-grid.js +61 -26
- package/dist/components/date-range-picker.d.ts +1 -1
- package/dist/components/date-range-picker.js +9 -20
- 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 +11 -6
- package/dist/components/drawer.js +91 -54
- package/dist/components/dropzone.d.ts +4 -4
- package/dist/components/editable-field.d.ts +12 -3
- package/dist/components/editable-field.js +57 -27
- package/dist/components/email-input.d.ts +1 -1
- package/dist/components/email-input.js +6 -5
- package/dist/components/environment-frame.d.ts +2 -2
- package/dist/components/environment-frame.js +37 -6
- 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 +43 -29
- package/dist/components/form-controls.d.ts +12 -3
- package/dist/components/form-controls.js +46 -22
- package/dist/components/form-layout.d.ts +14 -10
- package/dist/components/form-layout.js +68 -23
- package/dist/components/glass-menu.d.ts +3 -0
- package/dist/components/glass-menu.js +63 -0
- package/dist/components/gradient-edge.d.ts +9 -0
- package/dist/components/gradient-edge.js +31 -0
- package/dist/components/hotkeys.d.ts +2 -2
- package/dist/components/icon.d.ts +1 -1
- package/dist/components/input.d.ts +7 -5
- package/dist/components/input.js +29 -17
- package/dist/components/keyboard-shortcuts.d.ts +2 -2
- package/dist/components/keycap.d.ts +10 -6
- package/dist/components/keycap.js +57 -19
- package/dist/components/layout.d.ts +13 -13
- package/dist/components/lightbox.d.ts +9 -3
- package/dist/components/lightbox.js +13 -9
- package/dist/components/link.d.ts +1 -1
- package/dist/components/locale.d.ts +42 -2
- package/dist/components/locale.js +43 -3
- package/dist/components/log-entry.d.ts +40 -0
- package/dist/components/log-entry.js +146 -0
- package/dist/components/log-list.d.ts +3 -3
- package/dist/components/mention-input.d.ts +2 -2
- package/dist/components/mention-input.js +4 -3
- package/dist/components/menu.d.ts +7 -3
- package/dist/components/menu.js +34 -37
- package/dist/components/meter.d.ts +10 -2
- package/dist/components/meter.js +64 -6
- package/dist/components/navigation.d.ts +21 -4
- package/dist/components/navigation.js +84 -19
- package/dist/components/notification-tray.d.ts +1 -1
- package/dist/components/notification-tray.js +5 -4
- package/dist/components/overlays.d.ts +6 -4
- package/dist/components/overlays.js +69 -40
- package/dist/components/page-frame.d.ts +4 -4
- package/dist/components/page-frame.js +1 -1
- package/dist/components/panel.d.ts +4 -4
- package/dist/components/panel.js +18 -12
- 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/radio-card.js +10 -9
- package/dist/components/record.d.ts +29 -5
- package/dist/components/record.js +147 -18
- 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 +4 -2
- package/dist/components/segmented-bar.js +16 -6
- package/dist/components/select.d.ts +9 -6
- package/dist/components/select.js +103 -127
- package/dist/components/settings.d.ts +4 -4
- package/dist/components/shell-skeleton.d.ts +1 -1
- package/dist/components/shell-skeleton.js +1 -1
- package/dist/components/shell.d.ts +21 -21
- package/dist/components/shell.js +43 -43
- package/dist/components/side-panel.d.ts +3 -3
- package/dist/components/sidebar.d.ts +13 -8
- package/dist/components/sidebar.js +55 -27
- package/dist/components/single-line-text.d.ts +2 -0
- package/dist/components/single-line-text.js +16 -0
- package/dist/components/site-banner.d.ts +5 -2
- package/dist/components/site-banner.js +17 -6
- package/dist/components/skeleton.d.ts +8 -7
- package/dist/components/skeleton.js +28 -16
- package/dist/components/skeletons.d.ts +17 -5
- package/dist/components/skeletons.js +71 -8
- package/dist/components/stages.d.ts +83 -11
- package/dist/components/stages.js +512 -156
- package/dist/components/stat-card.d.ts +12 -4
- package/dist/components/stat-card.js +92 -37
- package/dist/components/status-banner.d.ts +1 -1
- package/dist/components/status-banner.js +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/tabs.js +26 -13
- package/dist/components/theme-customizer.d.ts +2 -2
- package/dist/components/theme-toggle.d.ts +4 -0
- package/dist/components/theme-toggle.js +67 -0
- package/dist/components/toast.d.ts +10 -3
- package/dist/components/toast.js +78 -29
- 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/preset.js +289 -33
- package/dist/styled-system/styles.css +3294 -1079
- package/dist/styled-system/tokens/index.mjs +598 -22
- package/dist/styled-system/tokens/tokens.d.ts +11 -11
- package/dist/styled-system/types/prop-type.d.ts +2 -2
- package/dist/styles.css +16 -4
- package/dist/theme/color.d.ts +3 -0
- package/dist/theme/color.js +5 -0
- package/dist/theme/derive.d.ts +9 -2
- package/dist/theme/derive.js +72 -12
- package/dist/theme/palettes.d.ts +49 -6
- package/dist/theme/palettes.js +84 -15
- package/dist/theme/tokens.d.ts +3 -3
- package/dist/theme/tokens.js +107 -32
- package/package.json +40 -3
package/dist/components/chat.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowClockwiseIcon, ArrowUpIcon } from "@phosphor-icons/react";
|
|
2
|
+
import { ArrowClockwiseIcon, ArrowDownIcon, ArrowUpIcon, CaretRightIcon, LightbulbIcon, WarningCircleIcon, WrenchIcon, } from "@phosphor-icons/react";
|
|
3
3
|
import { isSameDay, isToday, isYesterday } from "date-fns";
|
|
4
4
|
import { Fragment, useEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
5
5
|
import { css, cva, cx } from "#primitives/css";
|
|
@@ -7,18 +7,20 @@ import { listMessageGroupPositions, } from "../chat/message-group-position.js";
|
|
|
7
7
|
import { Button, IconButton } from "./button.js";
|
|
8
8
|
import { Avatar } from "./data-display.js";
|
|
9
9
|
import { fileSquare, fileTypeIcon } from "./dropzone.js";
|
|
10
|
-
import { icon, Spinner } from "./icon.js";
|
|
10
|
+
import { directional, icon, Spinner } from "./icon.js";
|
|
11
11
|
import { Lightbox } from "./lightbox.js";
|
|
12
12
|
import { useCiridaeLocale } from "./locale.js";
|
|
13
|
+
import { LogGroupHeader } from "./log-entry.js";
|
|
13
14
|
import { MentionInput } from "./mention-input.js";
|
|
15
|
+
import { useResolvedFileUrl } from "./resolved-file-url.js";
|
|
16
|
+
export { MessageText, splitSources, } from "../chat/message-text.js";
|
|
14
17
|
/*
|
|
15
|
-
*
|
|
16
|
-
* composer at the foot of the agent sheet
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* line there, and Cmd/Ctrl+Enter sends.
|
|
18
|
+
* Three composers share one behaviour. `page` is the wide card of a chat page. `sheet` is the
|
|
19
|
+
* composer at the foot of the agent sheet: one soft card holding the text, which grows line by
|
|
20
|
+
* line to six before it scrolls, over a bar with the tools as bare icons on the left and a small
|
|
21
|
+
* round send on the right; what goes with the message, such as files, waits as chips over the
|
|
22
|
+
* text. `feed` is a comment field under an activity feed: a pill with its send inside, which
|
|
23
|
+
* grows into a soft card past one line; Enter breaks the line there, and Cmd/Ctrl+Enter sends.
|
|
22
24
|
*/
|
|
23
25
|
const pageComposer = css({
|
|
24
26
|
display: "flex",
|
|
@@ -45,47 +47,136 @@ const pageInput = css({
|
|
|
45
47
|
_phone: { minHeight: "chat.inputMobile", padding: "panel.comfortable" },
|
|
46
48
|
});
|
|
47
49
|
const sheetComposer = css({
|
|
50
|
+
display: "flex",
|
|
51
|
+
flexDirection: "column",
|
|
52
|
+
gap: "layout.contentGap",
|
|
53
|
+
width: "100%",
|
|
54
|
+
minWidth: "0",
|
|
55
|
+
padding: "panel.default",
|
|
56
|
+
border: "1px solid token(colors.border.control)",
|
|
57
|
+
borderRadius: "field",
|
|
58
|
+
background: "bg.field",
|
|
59
|
+
_focusWithin: { borderColor: "border.focus", boxShadow: "focus" },
|
|
60
|
+
});
|
|
61
|
+
// Wrapped lines take the body's spacing. The margins give back the leading past one line of
|
|
62
|
+
// type, so a one-line composer stays the kit's height, its text box exactly one em tall.
|
|
63
|
+
const sheetInput = css({
|
|
64
|
+
maxHeight: "6lh",
|
|
65
|
+
marginBlock: "calc((1em - 1lh) / 2)",
|
|
66
|
+
fontSize: "body",
|
|
67
|
+
fontWeight: "medium",
|
|
68
|
+
lineHeight: "body",
|
|
69
|
+
});
|
|
70
|
+
// Under the text: the tools on the left, and on the right the actions and the send, a 26px
|
|
71
|
+
// round, smaller than any other icon button.
|
|
72
|
+
const sheetBar = css({
|
|
73
|
+
display: "flex",
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
justifyContent: "space-between",
|
|
76
|
+
gap: "layout.controlGap",
|
|
77
|
+
minWidth: "0",
|
|
78
|
+
"& > [data-tools], & > [data-send]": {
|
|
79
|
+
display: "flex",
|
|
80
|
+
flexShrink: "0",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
gap: "layout.controlGap",
|
|
83
|
+
},
|
|
84
|
+
"& > [data-send] > [data-send-button]": { width: "control.send", height: "control.send" },
|
|
85
|
+
});
|
|
86
|
+
const feedComposer = css({
|
|
48
87
|
display: "flex",
|
|
49
88
|
alignItems: "flex-end",
|
|
50
89
|
gap: "layout.tightGap",
|
|
51
90
|
width: "100%",
|
|
52
91
|
minWidth: "0",
|
|
53
92
|
});
|
|
54
|
-
// The tools
|
|
55
|
-
const
|
|
93
|
+
// The tools beside the feed's pill: as tall as its send, so they stay level with its last line.
|
|
94
|
+
const feedTools = css({
|
|
56
95
|
display: "flex",
|
|
57
96
|
flexShrink: "0",
|
|
58
97
|
alignItems: "center",
|
|
59
98
|
gap: "layout.tightGap",
|
|
60
|
-
height: "control.compact",
|
|
99
|
+
height: "calc(token(sizes.control.compact) + 2 * token(spacing.control.compactBlock))",
|
|
61
100
|
});
|
|
62
|
-
const
|
|
63
|
-
|
|
101
|
+
const feedColumn = css({ display: "grid", flex: "1", minWidth: "0", gap: "layout.tightGap" });
|
|
102
|
+
// The pill's 1px stroke sits inside its padding, so a one-line pill is the kit's height: the send
|
|
103
|
+
// plus its padding above and below.
|
|
104
|
+
const feedBox = css({
|
|
64
105
|
display: "flex",
|
|
65
106
|
alignItems: "flex-end",
|
|
107
|
+
gap: "layout.controlGap",
|
|
66
108
|
minWidth: "0",
|
|
109
|
+
paddingBlock: "calc(token(spacing.control.compactBlock) - token(sizes.border.hairline))",
|
|
110
|
+
paddingInlineStart: "control.compactInline",
|
|
111
|
+
paddingInlineEnd: "control.compactBlock",
|
|
67
112
|
border: "1px solid token(colors.border.control)",
|
|
68
113
|
// Half the one-line height: a pill at one line, a soft card past it.
|
|
69
|
-
borderRadius: "calc(token(sizes.control.compact) / 2)",
|
|
70
|
-
background: "
|
|
114
|
+
borderRadius: "calc((token(sizes.control.compact) + 2 * token(spacing.control.compactBlock)) / 2)",
|
|
115
|
+
background: "token(gradients.card)",
|
|
71
116
|
_focusWithin: { borderColor: "border.focus", boxShadow: "focus" },
|
|
72
117
|
"& > [data-actions]": {
|
|
73
118
|
display: "flex",
|
|
74
119
|
flexShrink: "0",
|
|
75
120
|
alignItems: "center",
|
|
76
|
-
height: "
|
|
77
|
-
|
|
121
|
+
height: "control.compact",
|
|
122
|
+
},
|
|
123
|
+
"& > [data-actions] > [data-send-button]": {
|
|
124
|
+
width: "control.compact",
|
|
125
|
+
height: "control.compact",
|
|
78
126
|
},
|
|
79
127
|
});
|
|
80
|
-
// Each line keeps the one-line
|
|
81
|
-
|
|
128
|
+
// Each line keeps the one-line pill's rhythm, so the first wrap only adds a line. Wrapped lines
|
|
129
|
+
// take the body's spacing, as the sheet's do.
|
|
130
|
+
const feedInput = css({
|
|
82
131
|
flex: "1",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
lineHeight: "
|
|
132
|
+
paddingBlock: "calc((token(sizes.control.compact) - 1lh) / 2)",
|
|
133
|
+
maxHeight: "calc(5lh + token(sizes.control.compact))",
|
|
134
|
+
fontSize: "body",
|
|
135
|
+
fontWeight: "medium",
|
|
136
|
+
lineHeight: "body",
|
|
137
|
+
});
|
|
138
|
+
/* A tool in the sheet composer's bar: a bare icon, with no face of its own. */
|
|
139
|
+
const composerTool = css({
|
|
140
|
+
display: "inline-flex",
|
|
141
|
+
padding: "none",
|
|
142
|
+
border: "0",
|
|
143
|
+
borderRadius: "control",
|
|
144
|
+
background: "transparent",
|
|
145
|
+
color: "fg.subtle",
|
|
146
|
+
cursor: "pointer",
|
|
147
|
+
_hover: { color: "fg.primary" },
|
|
148
|
+
_focusVisible: { focusRing: "outer" },
|
|
149
|
+
_disabled: { opacity: "disabled", cursor: "not-allowed" },
|
|
150
|
+
"&[aria-pressed=true]": { color: "fg.primary" },
|
|
88
151
|
});
|
|
152
|
+
/*
|
|
153
|
+
* Dictation's level meter, in the composer's bar while the host listens: a bar per sample,
|
|
154
|
+
* newest last, spread across the room the bar leaves it. Bars move with no transition, since one
|
|
155
|
+
* would blur the motion of a meter updated several times a second.
|
|
156
|
+
*/
|
|
157
|
+
const dictationWaveform = css({
|
|
158
|
+
display: "flex",
|
|
159
|
+
flex: "1",
|
|
160
|
+
alignItems: "center",
|
|
161
|
+
justifyContent: "space-between",
|
|
162
|
+
gap: "control.trackInset",
|
|
163
|
+
minWidth: "0",
|
|
164
|
+
height: "control.shortcut",
|
|
165
|
+
overflow: "hidden",
|
|
166
|
+
"& > span": {
|
|
167
|
+
flex: "1",
|
|
168
|
+
minWidth: "1px",
|
|
169
|
+
// A spacing token, so it is read through token(); a sizes property would print its name.
|
|
170
|
+
maxWidth: "token(spacing.layout.tightGap)",
|
|
171
|
+
height: "100%",
|
|
172
|
+
borderRadius: "round",
|
|
173
|
+
background: "fg.subtle",
|
|
174
|
+
transformOrigin: "center",
|
|
175
|
+
},
|
|
176
|
+
"&[data-listening] > span": { background: "fg.secondary" },
|
|
177
|
+
});
|
|
178
|
+
/** How short a silent bar of the dictation waveform is, as a share of its full height. */
|
|
179
|
+
const silentWaveformLevel = 0.12;
|
|
89
180
|
const composerFooter = css({
|
|
90
181
|
display: "flex",
|
|
91
182
|
alignItems: "center",
|
|
@@ -131,39 +222,48 @@ const chatAction = css({
|
|
|
131
222
|
_hover: { color: "fg.primary", borderColor: "fg.muted" },
|
|
132
223
|
});
|
|
133
224
|
/*
|
|
134
|
-
* A message bubble
|
|
135
|
-
*
|
|
136
|
-
*
|
|
225
|
+
* A message bubble: the user's on the right in the selected neutral face, under its gradient edge;
|
|
226
|
+
* another author's on the left in the neutral fill. Where a bubble joins the one above or below
|
|
227
|
+
* it on its side, that corner is small, so a run of messages reads as one stack.
|
|
137
228
|
*/
|
|
138
229
|
const bubble = cva({
|
|
139
230
|
base: {
|
|
140
231
|
position: "relative",
|
|
141
232
|
maxWidth: "chat.bubble",
|
|
142
|
-
paddingBlock: "
|
|
143
|
-
paddingInline: "control.
|
|
233
|
+
paddingBlock: "field.block",
|
|
234
|
+
paddingInline: "control.tabInline",
|
|
144
235
|
borderRadius: "field",
|
|
145
|
-
|
|
146
|
-
|
|
236
|
+
color: "fg.primary",
|
|
237
|
+
fontSize: "body",
|
|
238
|
+
lineHeight: "compact",
|
|
147
239
|
overflowWrap: "anywhere",
|
|
148
240
|
},
|
|
149
241
|
variants: {
|
|
150
242
|
side: {
|
|
151
243
|
user: {
|
|
152
244
|
alignSelf: "flex-end",
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
245
|
+
// Wrapped lines take the body's spacing. The kit sets one line at the type's own height
|
|
246
|
+
// with its 1px stroke in the layout, so the padding gives back the extra leading and
|
|
247
|
+
// a one-line bubble stays the kit's height.
|
|
248
|
+
paddingBlock: "calc(token(spacing.field.block) + token(sizes.border.hairline) - (1lh - 1em) / 2)",
|
|
249
|
+
paddingInline: "calc(token(spacing.control.tabInline) + token(sizes.border.hairline))",
|
|
250
|
+
lineHeight: "body",
|
|
251
|
+
background: "token(gradients.selected)",
|
|
157
252
|
whiteSpace: "pre-wrap",
|
|
253
|
+
"&::before": { background: "token(gradients.border.selected)" },
|
|
254
|
+
"&[data-group-position=first], &[data-group-position=middle]": {
|
|
255
|
+
borderEndEndRadius: "keycap",
|
|
256
|
+
},
|
|
158
257
|
"&[data-group-position=middle], &[data-group-position=last]": {
|
|
159
258
|
borderStartEndRadius: "keycap",
|
|
160
259
|
},
|
|
161
260
|
},
|
|
162
261
|
assistant: {
|
|
163
262
|
alignSelf: "flex-start",
|
|
164
|
-
borderEndStartRadius: "keycap",
|
|
165
263
|
background: "bg.fill",
|
|
166
|
-
|
|
264
|
+
"&[data-group-position=first], &[data-group-position=middle]": {
|
|
265
|
+
borderEndStartRadius: "keycap",
|
|
266
|
+
},
|
|
167
267
|
"&[data-group-position=middle], &[data-group-position=last]": {
|
|
168
268
|
borderStartStartRadius: "keycap",
|
|
169
269
|
},
|
|
@@ -171,6 +271,17 @@ const bubble = cva({
|
|
|
171
271
|
},
|
|
172
272
|
},
|
|
173
273
|
});
|
|
274
|
+
/* An answer in a thread of `plain` replies: text on the sheet itself, the thread's full width. */
|
|
275
|
+
const plainReply = css({
|
|
276
|
+
display: "grid",
|
|
277
|
+
gap: "layout.contentGap",
|
|
278
|
+
width: "100%",
|
|
279
|
+
minWidth: "0",
|
|
280
|
+
color: "fg.primary",
|
|
281
|
+
fontSize: "body",
|
|
282
|
+
lineHeight: "body",
|
|
283
|
+
overflowWrap: "anywhere",
|
|
284
|
+
});
|
|
174
285
|
/** A line about the conversation rather than in it, centered between bubbles. */
|
|
175
286
|
const systemLine = css({
|
|
176
287
|
alignSelf: "center",
|
|
@@ -182,24 +293,56 @@ const systemLine = css({
|
|
|
182
293
|
overflowWrap: "anywhere",
|
|
183
294
|
"&[data-tone=error]": { color: "accent.red" },
|
|
184
295
|
});
|
|
185
|
-
const dayLabel = css({ marginBlock: "layout.controlGap", fontWeight: "medium" });
|
|
186
296
|
/*
|
|
187
|
-
* The thread: bubbles 4px apart
|
|
188
|
-
*
|
|
297
|
+
* The thread: bubbles 4px apart and groups 12px apart, lines centered between them. Compact
|
|
298
|
+
* keeps 8px above and below and between groups. Plain replies sit 16px in from the sheet's
|
|
299
|
+
* edges, an answer's parts 12px apart and one turn 16px from the next.
|
|
189
300
|
*/
|
|
190
301
|
const thread = css({
|
|
191
302
|
display: "flex",
|
|
192
303
|
flexDirection: "column",
|
|
193
304
|
gap: "layout.tightGap",
|
|
194
305
|
padding: "panel.default",
|
|
306
|
+
// Every line in the thread is set in the medium weight: bubbles, replies, cited cards and meta.
|
|
307
|
+
fontWeight: "medium",
|
|
308
|
+
// The thread follows its newest entry itself: the browser's scroll anchoring would hold the
|
|
309
|
+
// view on what was there as a reply lands and pull it off the foot.
|
|
310
|
+
overflowAnchor: "none",
|
|
195
311
|
"&[data-density=compact]": {
|
|
196
312
|
paddingBlock: "layout.controlGap",
|
|
197
313
|
"& > [data-group-position=single], & > [data-group-position=first]": {
|
|
198
314
|
marginTop: "layout.tightGap",
|
|
199
315
|
},
|
|
200
316
|
},
|
|
317
|
+
"&[data-reply-style=plain]": {
|
|
318
|
+
padding: "panel.comfortable",
|
|
319
|
+
"& > [data-group-position=middle], & > [data-group-position=last]": {
|
|
320
|
+
marginTop: "layout.controlGap",
|
|
321
|
+
},
|
|
322
|
+
"& > :is([data-group-position=single], [data-group-position=first]):not(:first-child)": {
|
|
323
|
+
marginTop: "layout.contentGap",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
/*
|
|
328
|
+
* Under the thread, held at the foot of the scrolling area while the reader is away from the
|
|
329
|
+
* newest entry: the jump back to it. Takes no height, so showing it moves nothing.
|
|
330
|
+
*/
|
|
331
|
+
const latestSlot = css({
|
|
332
|
+
position: "sticky",
|
|
333
|
+
bottom: "layout.controlGap",
|
|
334
|
+
zIndex: "1",
|
|
335
|
+
display: "flex",
|
|
336
|
+
alignItems: "flex-end",
|
|
337
|
+
justifyContent: "center",
|
|
338
|
+
height: "0",
|
|
339
|
+
marginTop: "calc(token(spacing.layout.tightGap) * -1)",
|
|
340
|
+
pointerEvents: "none",
|
|
341
|
+
"& > *": { pointerEvents: "auto", boxShadow: "card" },
|
|
201
342
|
});
|
|
202
|
-
|
|
343
|
+
/** How close to the bottom the reader must be for new content to keep the thread at its foot. */
|
|
344
|
+
const followDistancePx = 96;
|
|
345
|
+
/* A reply's sources under it: small links in a row, and the ones with a quote as cards. */
|
|
203
346
|
const sourceRow = css({
|
|
204
347
|
display: "flex",
|
|
205
348
|
flexWrap: "wrap",
|
|
@@ -207,14 +350,72 @@ const sourceRow = css({
|
|
|
207
350
|
columnGap: "layout.controlGap",
|
|
208
351
|
rowGap: "control.trackInset",
|
|
209
352
|
paddingInline: "layout.tightGap",
|
|
210
|
-
color: "fg.
|
|
353
|
+
color: "fg.subtle",
|
|
211
354
|
fontSize: "code",
|
|
212
355
|
lineHeight: "compact",
|
|
213
|
-
"[data-
|
|
356
|
+
"[data-avatar] > &": {
|
|
214
357
|
marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
|
|
215
358
|
},
|
|
359
|
+
"[data-reply-style=plain] > * > &": { paddingInline: "none" },
|
|
216
360
|
"& a": { color: "fg.secondary", textDecoration: "none", _hover: { color: "fg.primary" } },
|
|
217
361
|
});
|
|
362
|
+
const sourceCardList = css({
|
|
363
|
+
display: "grid",
|
|
364
|
+
gap: "layout.contentGap",
|
|
365
|
+
"[data-avatar] > &": {
|
|
366
|
+
marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
/*
|
|
370
|
+
* A source a reply cites, as a card: what it is under its icon, what it says, and when or by
|
|
371
|
+
* whom. `yellow` marks a rule or a value the reply weighs, on the warm wash.
|
|
372
|
+
*/
|
|
373
|
+
const sourceCard = css({
|
|
374
|
+
display: "grid",
|
|
375
|
+
gap: "control.iconGap",
|
|
376
|
+
padding: "panel.default",
|
|
377
|
+
border: "1px solid token(colors.border.divider)",
|
|
378
|
+
borderRadius: "action",
|
|
379
|
+
background: "token(gradients.card)",
|
|
380
|
+
color: "fg.primary",
|
|
381
|
+
fontSize: "code",
|
|
382
|
+
fontWeight: "medium",
|
|
383
|
+
lineHeight: "log",
|
|
384
|
+
textDecoration: "none",
|
|
385
|
+
"&[data-tone=yellow]": { background: "bg.warningTint" },
|
|
386
|
+
"& > [data-head]": {
|
|
387
|
+
display: "flex",
|
|
388
|
+
alignItems: "center",
|
|
389
|
+
gap: "control.iconGap",
|
|
390
|
+
minWidth: "0",
|
|
391
|
+
color: "fg.subtle",
|
|
392
|
+
lineHeight: "tight",
|
|
393
|
+
"& > svg": { flexShrink: "0", fontSize: "icon.small" },
|
|
394
|
+
},
|
|
395
|
+
"&[data-tone=yellow] > [data-head]": { color: "accent.yellowIcon" },
|
|
396
|
+
"& > [data-meta]": { color: "fg.subtle", lineHeight: "tight" },
|
|
397
|
+
"&:is(a)": { _hover: { borderColor: "border.selected" }, _focusVisible: { focusRing: "outer" } },
|
|
398
|
+
});
|
|
399
|
+
/* Buttons under a reply for what it suggests doing, always shown. */
|
|
400
|
+
const suggestedActionRow = css({
|
|
401
|
+
display: "flex",
|
|
402
|
+
flexWrap: "wrap",
|
|
403
|
+
alignItems: "center",
|
|
404
|
+
gap: "layout.tightGap",
|
|
405
|
+
"[data-avatar] > &": {
|
|
406
|
+
marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
/* A reply's closing line, such as what it read, quieter than the reply. */
|
|
410
|
+
const footnote = css({
|
|
411
|
+
margin: "none",
|
|
412
|
+
color: "fg.subtle",
|
|
413
|
+
fontSize: "code",
|
|
414
|
+
lineHeight: "log",
|
|
415
|
+
"[data-avatar] > &": {
|
|
416
|
+
marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
|
|
417
|
+
},
|
|
418
|
+
});
|
|
218
419
|
/* Files sent with a message, as tiles over its bubble: image thumbnails, or a file's icon and name. */
|
|
219
420
|
const attachmentTiles = css({
|
|
220
421
|
display: "flex",
|
|
@@ -222,7 +423,7 @@ const attachmentTiles = css({
|
|
|
222
423
|
gap: "layout.tightGap",
|
|
223
424
|
maxWidth: "chat.bubble",
|
|
224
425
|
"[data-side=user] > &": { justifyContent: "flex-end" },
|
|
225
|
-
"[data-
|
|
426
|
+
"[data-avatar] > &": {
|
|
226
427
|
marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
|
|
227
428
|
},
|
|
228
429
|
});
|
|
@@ -264,6 +465,8 @@ const entryBlock = css({
|
|
|
264
465
|
"&[data-group-position=single], &[data-group-position=first]": {
|
|
265
466
|
marginTop: "layout.controlGap",
|
|
266
467
|
},
|
|
468
|
+
// A plain reply's parts, such as its cards and buttons under its text, sit 12px apart.
|
|
469
|
+
"&[data-side=assistant]": { "[data-reply-style=plain] > &": { gap: "layout.contentGap" } },
|
|
267
470
|
// A message's own controls, such as Copy, show while it is hovered or focused.
|
|
268
471
|
"& [data-entry-actions]": {
|
|
269
472
|
display: "inline-flex",
|
|
@@ -284,6 +487,8 @@ const entryRow = css({
|
|
|
284
487
|
width: "100%",
|
|
285
488
|
minWidth: "0",
|
|
286
489
|
"[data-side=user] > &": { justifyContent: "flex-end" },
|
|
490
|
+
// A plain reply's Copy sits at the top of its text, which may run long.
|
|
491
|
+
"[data-reply-style=plain] > [data-side=assistant] > &": { alignItems: "flex-start" },
|
|
287
492
|
});
|
|
288
493
|
const avatarSlot = css({ flexShrink: "0", width: "avatar.small" });
|
|
289
494
|
const entryFooter = css({
|
|
@@ -291,11 +496,11 @@ const entryFooter = css({
|
|
|
291
496
|
alignItems: "center",
|
|
292
497
|
gap: "control.iconGap",
|
|
293
498
|
paddingInline: "layout.tightGap",
|
|
294
|
-
color: "fg.
|
|
499
|
+
color: "fg.subtle",
|
|
295
500
|
fontSize: "code",
|
|
296
501
|
lineHeight: "compact",
|
|
297
502
|
fontVariantNumeric: "tabular-nums",
|
|
298
|
-
"[data-
|
|
503
|
+
"[data-avatar] > &": {
|
|
299
504
|
marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
|
|
300
505
|
},
|
|
301
506
|
});
|
|
@@ -308,37 +513,107 @@ const attachmentList = css({
|
|
|
308
513
|
fontSize: "code",
|
|
309
514
|
"& a": { color: "inherit" },
|
|
310
515
|
});
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
516
|
+
/*
|
|
517
|
+
* The agent at work, before its reply: what it is doing beside a spinner, over three lines of
|
|
518
|
+
* the reply to come. The lines stand in for the reply's text, so each is that text's bar height.
|
|
519
|
+
*/
|
|
520
|
+
const thinking = css({
|
|
521
|
+
display: "grid",
|
|
522
|
+
gap: "layout.contentGap",
|
|
523
|
+
width: "100%",
|
|
524
|
+
minWidth: "0",
|
|
525
|
+
"& > [data-status]": {
|
|
526
|
+
display: "flex",
|
|
527
|
+
alignItems: "center",
|
|
528
|
+
gap: "layout.controlGap",
|
|
529
|
+
minWidth: "0",
|
|
530
|
+
color: "fg.secondary",
|
|
531
|
+
fontSize: "code",
|
|
532
|
+
lineHeight: "tight",
|
|
533
|
+
"& > span": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
534
|
+
},
|
|
535
|
+
"& > [data-line]": {
|
|
536
|
+
display: "block",
|
|
537
|
+
height: "calc(token(fontSizes.body) * 0.75)",
|
|
538
|
+
borderRadius: "round",
|
|
539
|
+
background: "token(gradients.skeletonFill)",
|
|
540
|
+
},
|
|
541
|
+
"& > [data-line]:nth-of-type(1)": { width: "98%" },
|
|
542
|
+
"& > [data-line]:nth-of-type(2)": { width: "87%" },
|
|
543
|
+
"& > [data-line]:nth-of-type(3)": { width: "66%" },
|
|
316
544
|
});
|
|
317
|
-
|
|
545
|
+
/* A reply the agent could not finish: a notice of why, with Try again under it. */
|
|
546
|
+
const failure = css({
|
|
547
|
+
display: "grid",
|
|
548
|
+
justifyItems: "start",
|
|
549
|
+
gap: "layout.contentGap",
|
|
550
|
+
width: "100%",
|
|
551
|
+
minWidth: "0",
|
|
552
|
+
});
|
|
553
|
+
const failureNotice = css({
|
|
318
554
|
display: "flex",
|
|
319
|
-
flexWrap: "wrap",
|
|
320
|
-
alignItems: "center",
|
|
321
555
|
gap: "layout.controlGap",
|
|
322
|
-
|
|
323
|
-
|
|
556
|
+
width: "100%",
|
|
557
|
+
paddingBlock: "panel.default",
|
|
558
|
+
paddingInline: "control.tabInline",
|
|
559
|
+
borderRadius: "action",
|
|
560
|
+
background: "token(gradients.selected)",
|
|
561
|
+
"&::before": { background: "token(gradients.border.raised)" },
|
|
562
|
+
"& > svg": { flexShrink: "0", fontSize: "icon.small", color: "accent.red" },
|
|
563
|
+
"& > [data-copy]": { display: "grid", gap: "layout.tightGap", minWidth: "0" },
|
|
564
|
+
"& [data-title]": {
|
|
565
|
+
color: "accent.red",
|
|
566
|
+
fontSize: "body",
|
|
567
|
+
fontWeight: "medium",
|
|
568
|
+
lineHeight: "tight",
|
|
569
|
+
},
|
|
570
|
+
"& [data-description]": { color: "fg.secondary", fontSize: "code", lineHeight: "log" },
|
|
324
571
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
572
|
+
/*
|
|
573
|
+
* The steps the agent took on its way to a reply, folded into one quiet line naming the first and
|
|
574
|
+
* how many more; open, a row for each, its state's icon beside what it did.
|
|
575
|
+
*/
|
|
576
|
+
const stepList = css({
|
|
577
|
+
width: "100%",
|
|
578
|
+
minWidth: "0",
|
|
579
|
+
color: "fg.subtle",
|
|
580
|
+
fontSize: "code",
|
|
581
|
+
lineHeight: "tight",
|
|
582
|
+
"& > summary": {
|
|
583
|
+
display: "inline-flex",
|
|
584
|
+
alignItems: "center",
|
|
585
|
+
gap: "control.iconGap",
|
|
586
|
+
maxWidth: "100%",
|
|
587
|
+
borderRadius: "control",
|
|
588
|
+
cursor: "pointer",
|
|
589
|
+
listStyle: "none",
|
|
590
|
+
"&::-webkit-details-marker": { display: "none" },
|
|
591
|
+
_hover: { color: "fg.primary" },
|
|
592
|
+
_focusVisible: { focusRing: "outer" },
|
|
593
|
+
"& > svg": { flexShrink: "0", transitionProperty: "rotate", transitionDuration: "fast" },
|
|
594
|
+
"& > span": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
595
|
+
},
|
|
596
|
+
"&[open] > summary > svg": { rotate: "90deg" },
|
|
597
|
+
"& > ol": {
|
|
598
|
+
display: "grid",
|
|
599
|
+
gap: "layout.controlGap",
|
|
600
|
+
margin: "none",
|
|
601
|
+
paddingBlockStart: "layout.contentGap",
|
|
602
|
+
paddingInlineStart: "none",
|
|
603
|
+
listStyle: "none",
|
|
604
|
+
},
|
|
605
|
+
"& li": {
|
|
606
|
+
display: "grid",
|
|
607
|
+
gridTemplateColumns: "token(sizes.mark.inline) minmax(0, 1fr)",
|
|
608
|
+
columnGap: "layout.controlGap",
|
|
609
|
+
alignItems: "start",
|
|
610
|
+
"& > svg": { fontSize: "icon.small" },
|
|
611
|
+
"&[data-status=failed] > svg": { color: "accent.red" },
|
|
612
|
+
"& > [data-title]": { color: "fg.secondary" },
|
|
613
|
+
"& > [data-detail]": { gridColumn: "2", lineHeight: "log", overflowWrap: "anywhere" },
|
|
337
614
|
},
|
|
338
|
-
"& > span:nth-child(2)": { animationDelay: "0.2s" },
|
|
339
|
-
"& > span:nth-child(3)": { animationDelay: "0.4s" },
|
|
340
615
|
});
|
|
341
|
-
export function ChatComposer({ value, defaultValue = "", onValueChange, onSend, placeholder, ariaLabel: ownAriaLabel, tools, actions, submitLabel: ownSubmitLabel, sendDisabled = false, sendsWithoutText = false, attachments, variant = "page", autoFocus, mentions, mentionMark, mentionsLabel, inputProps, className, ...props }) {
|
|
616
|
+
export function ChatComposer({ value, defaultValue = "", onValueChange, onSend, placeholder, ariaLabel: ownAriaLabel, tools, actions, submitLabel: ownSubmitLabel, sendDisabled = false, sendsWithoutText = false, attachments, onPasteFiles, dictation, variant = "page", autoFocus, mentions, mentionMark, mentionsLabel, inputProps, className, ...props }) {
|
|
342
617
|
const { labels } = useCiridaeLocale();
|
|
343
618
|
const ariaLabel = ownAriaLabel ?? labels.chat.message;
|
|
344
619
|
const submitLabel = ownSubmitLabel ?? labels.chat.sendMessage;
|
|
@@ -356,40 +631,72 @@ export function ChatComposer({ value, defaultValue = "", onValueChange, onSend,
|
|
|
356
631
|
onSend?.(currentValue);
|
|
357
632
|
setValue("");
|
|
358
633
|
};
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
634
|
+
// In the capture phase, ahead of the text field and of an enclosing `Dropzone`, which would
|
|
635
|
+
// take the same files again.
|
|
636
|
+
const pasteFiles = onPasteFiles
|
|
637
|
+
? (event) => {
|
|
638
|
+
const { files, types } = event.clipboardData;
|
|
639
|
+
// Rich text comes with a picture of itself, as office apps copy it: that stays text.
|
|
640
|
+
if (files.length === 0 || types.includes("text/html"))
|
|
641
|
+
return;
|
|
642
|
+
event.preventDefault();
|
|
643
|
+
event.stopPropagation();
|
|
644
|
+
onPasteFiles(Array.from(files));
|
|
645
|
+
}
|
|
646
|
+
: undefined;
|
|
647
|
+
const input = (_jsx(MentionInput, { ...inputProps, "aria-label": ariaLabel, autoFocus: autoFocus, className: cx(variant === "page" ? pageInput : variant === "sheet" ? sheetInput : feedInput, inputProps?.className), value: currentValue, onValueChange: setValue, triggers: mentions, mentionMark: mentionMark, listLabel: mentionsLabel, placeholder: placeholder, submitOn: variant === "feed" ? "mod+enter" : "enter", onSubmit: send }));
|
|
648
|
+
// The sheet's send is the bar's small round one; the feed's sits in its pill, 32 round.
|
|
649
|
+
const sendButton = (_jsx(IconButton, { "aria-label": submitLabel, disabled: !canSend, onClick: send, variant: variant === "page" ? "raised" : "primary", "data-send-button": "", children: _jsx(ArrowUpIcon, { className: icon({ size: "small" }), weight: "bold" }) }));
|
|
363
650
|
const waiting = attachments ? _jsx("div", { "data-attachments": "", children: attachments }) : null;
|
|
364
|
-
|
|
365
|
-
|
|
651
|
+
const waveform = dictation && (_jsx(ChatDictationWaveform, { levels: dictation.levels, listening: dictation.listening }));
|
|
652
|
+
if (variant === "sheet") {
|
|
653
|
+
return (_jsxs("div", { ...props, className: cx(sheetComposer, className), "data-ciridae-ui": "", onPasteCapture: pasteFiles, children: [waiting, input, _jsxs("div", { className: sheetBar, children: [_jsx("div", { "data-tools": "", children: tools }), waveform, _jsxs("div", { "data-send": "", children: [actions, sendButton] })] })] }));
|
|
366
654
|
}
|
|
367
|
-
|
|
655
|
+
if (variant === "feed") {
|
|
656
|
+
return (_jsxs("div", { ...props, className: cx(feedComposer, className), "data-ciridae-ui": "", onPasteCapture: pasteFiles, children: [tools && _jsx("div", { className: feedTools, children: tools }), _jsxs("div", { className: feedColumn, children: [waiting, _jsxs("div", { className: feedBox, children: [input, waveform, _jsxs("div", { "data-actions": "", children: [actions, sendButton] })] })] })] }));
|
|
657
|
+
}
|
|
658
|
+
return (_jsxs("div", { ...props, className: cx(pageComposer, className), "data-ciridae-ui": "", onPasteCapture: pasteFiles, children: [waiting, input, _jsxs("div", { className: composerFooter, children: [_jsx("div", { className: actionGroup, children: tools }), waveform, _jsxs("div", { className: actionGroup, children: [actions, sendButton] })] })] }));
|
|
659
|
+
}
|
|
660
|
+
/** A tool for the sheet composer's bar, such as the paperclip: its icon alone. */
|
|
661
|
+
export function ChatComposerTool({ className, ...props }) {
|
|
662
|
+
return (_jsx("button", { type: "button", ...props, className: cx(composerTool, className), "data-ciridae-ui": "" }));
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Dictation's waveform: a bar per level, each as tall as its level; flat while `listening` is
|
|
666
|
+
* off, such as while the host turns what it heard into text.
|
|
667
|
+
*/
|
|
668
|
+
export function ChatDictationWaveform({ levels, listening, className, ...props }) {
|
|
669
|
+
return (_jsx("div", { ...props, className: cx(dictationWaveform, className), "data-ciridae-ui": "", "data-listening": listening || undefined, "aria-hidden": "true", children: levels.map((level, index) => (_jsx("span", { style: {
|
|
670
|
+
transform: `scaleY(${listening ? Math.max(silentWaveformLevel, level) : silentWaveformLevel})`,
|
|
671
|
+
} }, index))) }));
|
|
368
672
|
}
|
|
369
|
-
/** A message bubble
|
|
673
|
+
/** A message bubble: the user's raised on the right, another author's filled on the left. */
|
|
370
674
|
export function ChatBubble({ side, groupPosition, className, ...props }) {
|
|
371
|
-
return (_jsx("div", { ...props, className: cx(bubble({ side }), className), "data-ciridae-ui": "", "data-group-position": groupPosition }));
|
|
675
|
+
return (_jsx("div", { ...props, className: cx(bubble({ side }), className), "data-ciridae-ui": "", "data-edge": side === "user" ? "gradient" : undefined, "data-group-position": groupPosition }));
|
|
372
676
|
}
|
|
373
677
|
/** A centered line between bubbles: a finished tool call, a decision, something the page did. */
|
|
374
678
|
export function ChatSystemLine({ tone, className, ...props }) {
|
|
375
679
|
return (_jsx("div", { ...props, className: cx(systemLine, className), "data-ciridae-ui": "", "data-tone": tone === "error" ? "error" : undefined }));
|
|
376
680
|
}
|
|
377
|
-
/** Three pulsing dots: the assistant is typing. */
|
|
378
|
-
export function ChatTypingDots({ className, ...props }) {
|
|
379
|
-
return (_jsxs("span", { ...props, className: cx(typingDots, className), "data-ciridae-ui": "", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }));
|
|
380
|
-
}
|
|
381
681
|
export function ChatAction({ className, ...props }) {
|
|
382
682
|
return (_jsx("button", { ...props, className: cx(chatAction, className), "data-ciridae-ui": "", type: props.type ?? "button" }));
|
|
383
683
|
}
|
|
684
|
+
/** A source cited in a reply, as a card of what it says under its name. */
|
|
685
|
+
export function ChatSourceCard({ label, icon, children, meta, tone = "neutral", href, className, }) {
|
|
686
|
+
const content = (_jsxs(_Fragment, { children: [_jsxs("span", { "data-head": "", children: [icon, label] }), _jsx("span", { children: children }), meta && _jsx("span", { "data-meta": "", children: meta })] }));
|
|
687
|
+
return href ? (_jsx("a", { href: href, className: cx(sourceCard, className), "data-ciridae-ui": "", "data-tone": tone, children: content })) : (_jsx("div", { className: cx(sourceCard, className), "data-ciridae-ui": "", "data-tone": tone, children: content }));
|
|
688
|
+
}
|
|
384
689
|
/*
|
|
385
690
|
* A conversation in time order. Close messages from one author read as a group: bubbles 4px
|
|
386
|
-
* apart, groups
|
|
387
|
-
* side and the author and time once under it. The nearest scrolling ancestor
|
|
388
|
-
*
|
|
691
|
+
* apart, groups 12px apart, the author's avatar beside the group's last bubble on the assistant's
|
|
692
|
+
* side and the author and time once under it. The nearest scrolling ancestor follows the newest
|
|
693
|
+
* entry while the reader is at its foot; scrolled up, reading back, a jump to the newest shows
|
|
694
|
+
* instead. A message the user sends always brings the thread to its foot.
|
|
389
695
|
*/
|
|
390
|
-
export function ChatThread({ entries, assistant, renderCustom, formatTime, density = "comfortable", attachmentTiles = false, dayLabels = false, className, }) {
|
|
696
|
+
export function ChatThread({ entries, assistant, renderCustom, formatTime, density = "comfortable", replyStyle = "bubble", attachmentTiles = false, dayLabels = false, resolveAttachmentUrl, className, }) {
|
|
391
697
|
const rootRef = useRef(null);
|
|
392
|
-
const { formatDateTime } = useCiridaeLocale();
|
|
698
|
+
const { formatDateTime, labels } = useCiridaeLocale();
|
|
699
|
+
const plain = replyStyle === "plain";
|
|
393
700
|
const authorOf = (entry) => entry.kind === "line" || entrySide(entry) === "user"
|
|
394
701
|
? undefined
|
|
395
702
|
: ((entry.kind === "message" ? entry.author : undefined) ?? assistant);
|
|
@@ -398,20 +705,16 @@ export function ChatThread({ entries, assistant, renderCustom, formatTime, densi
|
|
|
398
705
|
at: entry.at,
|
|
399
706
|
author: authorOf(entry)?.name,
|
|
400
707
|
})));
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const newDay = dayLabels && (!previous || !isSameDay(previous.at, entry.at));
|
|
412
|
-
const content = renderEntry(entry, index);
|
|
413
|
-
return newDay ? (_jsxs(Fragment, { children: [_jsx(DayLabel, { at: entry.at }), content] }, entry.id)) : (content);
|
|
414
|
-
}) }));
|
|
708
|
+
const newestUserMessage = [...entries]
|
|
709
|
+
.reverse()
|
|
710
|
+
.find((entry) => entry.kind === "message" && entry.side === "user");
|
|
711
|
+
const latest = useFollowLatest(rootRef, newestUserMessage?.id);
|
|
712
|
+
return (_jsxs("div", { ref: rootRef, className: cx(thread, className), "data-ciridae-ui": "", "data-density": density === "compact" ? "compact" : undefined, "data-reply-style": replyStyle, children: [entries.map((entry, index) => {
|
|
713
|
+
const previous = entries[index - 1];
|
|
714
|
+
const newDay = dayLabels && (!previous || !isSameDay(previous.at, entry.at));
|
|
715
|
+
const content = renderEntry(entry, index);
|
|
716
|
+
return newDay ? (_jsxs(Fragment, { children: [_jsx(DayHeading, { at: entry.at }), content] }, entry.id)) : (content);
|
|
717
|
+
}), _jsx("div", { className: latestSlot, children: latest.away && (_jsx(IconButton, { "aria-label": labels.chat.scrollToLatest, size: "compact", variant: "raised", onClick: latest.scrollToLatest, children: _jsx(ArrowDownIcon, { className: icon({ size: "small" }) }) })) })] }));
|
|
415
718
|
function renderEntry(entry, index) {
|
|
416
719
|
if (entry.kind === "line") {
|
|
417
720
|
return (_jsx(ChatSystemLine, { tone: entry.tone, children: entry.text }, entry.id));
|
|
@@ -421,51 +724,88 @@ export function ChatThread({ entries, assistant, renderCustom, formatTime, densi
|
|
|
421
724
|
const position = positions[index] ?? undefined;
|
|
422
725
|
const closesGroup = position === "single" || position === "last";
|
|
423
726
|
const message = entry.kind === "message" ? entry : null;
|
|
727
|
+
const avatar = !plain && side === "assistant";
|
|
424
728
|
const tiles = attachmentTiles && message?.attachments && message.attachments.length > 0
|
|
425
729
|
? message.attachments
|
|
426
730
|
: null;
|
|
427
|
-
|
|
731
|
+
const sourceLinks = message?.sources?.filter((source) => source.quote === undefined) ?? [];
|
|
732
|
+
const sourceCards = message?.sources?.filter((source) => source.quote !== undefined) ?? [];
|
|
733
|
+
// A plain reply drops the assistant's author and time; the user's messages keep their time.
|
|
734
|
+
const footer = closesGroup &&
|
|
735
|
+
entry.kind !== "typing" &&
|
|
736
|
+
entry.kind !== "steps" &&
|
|
737
|
+
(!plain || side === "user" || message?.sending);
|
|
738
|
+
return (_jsxs("div", { className: entryBlock, "data-side": side, "data-avatar": avatar || undefined, "data-group-position": position, children: [tiles && _jsx(AttachmentTiles, { attachments: tiles, resolveUrl: resolveAttachmentUrl }), !(tiles && message?.body === "") && (_jsxs("div", { className: entryRow, title: message ? formatDateTime(entry.at) : undefined, children: [avatar && (_jsx("span", { className: avatarSlot, children: closesGroup && author && (_jsx(Avatar, { kind: author.kind, src: author.src, label: author.name, size: "small", "aria-hidden": "true" })) })), _jsx(ThreadEntryContent, { entry: entry, side: side, groupPosition: position, renderCustom: renderCustom, attachmentTiles: attachmentTiles, plain: plain }), message?.actions && !message.sending && (_jsx("span", { "data-entry-actions": "", children: message.actions }))] })), sourceCards.length > 0 && (_jsx("div", { className: sourceCardList, children: sourceCards.map((source) => (_jsx(ChatSourceCard, { label: source.label, icon: source.icon, meta: source.meta, tone: source.tone, href: source.href, children: source.quote }, source.id))) })), sourceLinks.length > 0 && (_jsx("span", { className: sourceRow, children: sourceLinks.map((source) => source.href ? (_jsx("a", { href: source.href, children: source.label }, source.id)) : (_jsx("span", { children: source.label }, source.id))) })), message?.suggestedActions && (_jsx("div", { className: suggestedActionRow, children: message.suggestedActions })), message?.footnote && _jsx("p", { className: footnote, children: message.footnote }), footer && (_jsx("span", { className: entryFooter, children: message?.sending ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { "aria-hidden": "true", size: "compact" }), labels.chat.sending] })) : (_jsxs("span", { children: [author && (_jsxs(_Fragment, { children: [author.label ?? author.name, " · "] })), _jsx(MessageTime, { at: entry.at, format: formatTime, dated: !dayLabels })] })) }))] }, entry.id));
|
|
428
739
|
}
|
|
429
740
|
}
|
|
430
|
-
/** A day's heading in a thread that runs over days: Today, Yesterday
|
|
431
|
-
function
|
|
741
|
+
/** A day's heading in a thread that runs over days: Today, Yesterday or the weekday, then the date. */
|
|
742
|
+
function DayHeading({ at }) {
|
|
432
743
|
const { labels, formatDate } = useCiridaeLocale();
|
|
433
|
-
return (_jsx(
|
|
744
|
+
return (_jsx(LogGroupHeader, { role: "separator", "data-day-heading": "", label: isToday(at)
|
|
434
745
|
? labels.dates.today
|
|
435
746
|
: isYesterday(at)
|
|
436
747
|
? labels.dates.yesterday
|
|
437
|
-
: formatDate(at, { weekday: "long", month: "short", day: "numeric" }) }));
|
|
748
|
+
: formatDate(at, { weekday: "long" }), meta: formatDate(at, { month: "short", day: "numeric" }) }));
|
|
438
749
|
}
|
|
439
750
|
/** Prompts to start a conversation from, stacked at the foot of an empty thread. */
|
|
440
751
|
export function ChatSuggestions({ suggestions, onSelect, className }) {
|
|
441
752
|
return (_jsx("div", { className: cx(suggestionList, className), "data-ciridae-ui": "", children: suggestions.map((text) => (_jsx("button", { type: "button", className: suggestion, onClick: () => onSelect(text), children: text }, text))) }));
|
|
442
753
|
}
|
|
443
|
-
function AttachmentTiles({ attachments }) {
|
|
444
|
-
const { labels } = useCiridaeLocale();
|
|
754
|
+
function AttachmentTiles({ attachments, resolveUrl, }) {
|
|
445
755
|
const [open, setOpen] = useState(null);
|
|
446
|
-
// The files the lightbox can show, in the message's order: those
|
|
447
|
-
const viewable = attachments.
|
|
756
|
+
// The files the lightbox can show, in the message's order: those with a link or a way to one.
|
|
757
|
+
const viewable = attachments.filter((attachment) => attachment.url || resolveUrl);
|
|
448
758
|
return (_jsxs("span", { className: attachmentTiles, children: [attachments.map((attachment) => {
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return index >= 0 ? (_jsx("button", { type: "button", className: fileSquare, "data-image": image || undefined, "aria-label": labels.lightbox.preview(attachment.name), title: attachment.name, onClick: () => setOpen(index), children: content }, attachment.id)) : (_jsx("span", { className: fileSquare, title: attachment.name, children: content }, attachment.id));
|
|
453
|
-
}), _jsx(Lightbox, { items: viewable, index: open, onIndexChange: setOpen })] }));
|
|
759
|
+
const index = viewable.indexOf(attachment);
|
|
760
|
+
return (_jsx(AttachmentTile, { attachment: attachment, resolveUrl: resolveUrl, onOpen: index >= 0 ? () => setOpen(index) : undefined }, attachment.id));
|
|
761
|
+
}), _jsx(Lightbox, { items: viewable, index: open, onIndexChange: setOpen, resolveUrl: resolveUrl })] }));
|
|
454
762
|
}
|
|
455
|
-
|
|
763
|
+
/** An image's thumbnail, loaded through `resolveUrl` when it has no link, or the file's icon and name. */
|
|
764
|
+
function AttachmentTile({ attachment, resolveUrl, onOpen, }) {
|
|
456
765
|
const { labels } = useCiridaeLocale();
|
|
766
|
+
const imageType = attachment.mediaType?.startsWith("image/") ?? false;
|
|
767
|
+
const { url } = useResolvedFileUrl(attachment, resolveUrl, imageType);
|
|
768
|
+
const image = imageType && url !== null;
|
|
769
|
+
const content = image ? (_jsx("img", { src: url, alt: attachment.name })) : (_jsxs(_Fragment, { children: [fileTypeIcon({ name: attachment.name, type: attachment.mediaType ?? "" }), _jsx("span", { children: attachment.name })] }));
|
|
770
|
+
return onOpen ? (_jsx("button", { type: "button", className: fileSquare, "data-image": image || undefined, "aria-label": labels.lightbox.preview(attachment.name), title: attachment.name, onClick: onOpen, children: content })) : (_jsx("span", { className: fileSquare, "data-image": image || undefined, title: attachment.name, children: content }));
|
|
771
|
+
}
|
|
772
|
+
function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachmentTiles: tiles, plain, }) {
|
|
457
773
|
switch (entry.kind) {
|
|
458
|
-
case "message":
|
|
459
|
-
|
|
774
|
+
case "message": {
|
|
775
|
+
const attachmentNames = !tiles && entry.attachments && entry.attachments.length > 0 && (_jsx("span", { className: attachmentList, children: entry.attachments.map((attachment) => attachment.url ? (_jsx("a", { href: attachment.url, target: "_blank", rel: "noreferrer", children: attachment.name }, attachment.id)) : (_jsx("span", { children: attachment.name }, attachment.id))) }));
|
|
776
|
+
if (plain && side === "assistant") {
|
|
777
|
+
return (_jsxs("div", { className: plainReply, "aria-busy": entry.sending || undefined, children: [entry.body, attachmentNames] }));
|
|
778
|
+
}
|
|
779
|
+
return (_jsxs(ChatBubble, { side: side, groupPosition: groupPosition, "aria-busy": entry.sending || undefined, children: [entry.body, attachmentNames] }));
|
|
780
|
+
}
|
|
460
781
|
case "typing":
|
|
461
|
-
return (
|
|
782
|
+
return _jsx(AgentThinking, { label: entry.label });
|
|
783
|
+
case "steps":
|
|
784
|
+
return _jsx(AgentSteps, { steps: entry.steps });
|
|
462
785
|
case "failure":
|
|
463
|
-
return (
|
|
786
|
+
return _jsx(FailureNotice, { title: entry.title, message: entry.message, onRetry: entry.onRetry });
|
|
464
787
|
case "custom":
|
|
465
788
|
return renderCustom?.(entry.data);
|
|
466
789
|
}
|
|
467
790
|
}
|
|
468
|
-
/** The
|
|
791
|
+
/** The agent at work: what it is doing beside a spinner, over lines standing in for its reply. */
|
|
792
|
+
function AgentThinking({ label }) {
|
|
793
|
+
const { labels } = useCiridaeLocale();
|
|
794
|
+
return (_jsxs("div", { className: thinking, role: "status", children: [_jsxs("div", { "data-status": "", children: [_jsx(Spinner, { "aria-hidden": "true" }), _jsx("span", { children: label ?? labels.common.working })] }), _jsx("span", { "data-line": "" }), _jsx("span", { "data-line": "" }), _jsx("span", { "data-line": "" })] }));
|
|
795
|
+
}
|
|
796
|
+
/** The steps behind a reply, folded into a line naming the first and how many more. */
|
|
797
|
+
function AgentSteps({ steps }) {
|
|
798
|
+
const [first] = steps;
|
|
799
|
+
if (!first)
|
|
800
|
+
return null;
|
|
801
|
+
return (_jsxs("details", { className: stepList, children: [_jsxs("summary", { children: [_jsx(CaretRightIcon, { className: cx(icon({ size: "compact" }), directional) }), _jsxs("span", { children: [first.title, steps.length > 1 && ` · +${steps.length - 1}`] })] }), _jsx("ol", { children: steps.map((step) => (_jsxs("li", { "data-status": step.status, children: [step.status === "running" ? (_jsx(Spinner, { "aria-hidden": "true" })) : step.status === "failed" ? (_jsx(WarningCircleIcon, { "aria-hidden": "true" })) : step.kind === "tool" ? (_jsx(WrenchIcon, { "aria-hidden": "true" })) : (_jsx(LightbulbIcon, { "aria-hidden": "true" })), _jsx("span", { "data-title": "", children: step.title }), step.detail && _jsx("span", { "data-detail": "", children: step.detail })] }, step.id))) })] }));
|
|
802
|
+
}
|
|
803
|
+
/** A reply that did not finish: a notice of why, and Try again under it. */
|
|
804
|
+
function FailureNotice({ title, message, onRetry, }) {
|
|
805
|
+
const { labels } = useCiridaeLocale();
|
|
806
|
+
return (_jsxs("div", { className: failure, children: [_jsxs("div", { className: failureNotice, role: "alert", "data-ciridae-ui": "", "data-edge": "gradient", children: [_jsx(WarningCircleIcon, { "aria-hidden": "true" }), _jsxs("div", { "data-copy": "", children: [_jsx("span", { "data-title": "", children: title ?? labels.chat.couldNotAnswer }), message && _jsx("span", { "data-description": "", children: message })] })] }), _jsx(Button, { variant: "secondary", size: "medium", icon: _jsx(ArrowClockwiseIcon, { className: icon({ size: "small" }) }), onClick: onRetry, children: labels.common.tryAgain })] }));
|
|
807
|
+
}
|
|
808
|
+
/** The side an entry sits on: the agent's work, its steps and failures are the assistant's. */
|
|
469
809
|
function entrySide(entry) {
|
|
470
810
|
return entry.kind === "message" || entry.kind === "custom" ? entry.side : "assistant";
|
|
471
811
|
}
|
|
@@ -535,6 +875,62 @@ function MessageTime({ at, format: formatAt, dated, }) {
|
|
|
535
875
|
: formatTime(at);
|
|
536
876
|
return (_jsxs("time", { dateTime: new Date(at).toISOString(), className: messageTime, children: [_jsx("span", { "data-shown": "", children: text }), exact !== text && _jsx("span", { "data-exact": "", children: exact })] }));
|
|
537
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Keeps the nearest scrolling ancestor of `rootRef` at its foot while the reader is there, as
|
|
880
|
+
* entries land or images in them load; `away` while they have scrolled up from it. A new
|
|
881
|
+
* `sentMessageId`, the user's own message, brings it to the foot from anywhere.
|
|
882
|
+
*/
|
|
883
|
+
function useFollowLatest(rootRef, sentMessageId) {
|
|
884
|
+
const [away, setAway] = useState(false);
|
|
885
|
+
const atFootRef = useRef(true);
|
|
886
|
+
const scrollerRef = useRef(null);
|
|
887
|
+
useEffect(() => {
|
|
888
|
+
const root = rootRef.current;
|
|
889
|
+
const scroller = root && findScrollContainer(root);
|
|
890
|
+
if (!root || !scroller)
|
|
891
|
+
return;
|
|
892
|
+
scrollerRef.current = scroller;
|
|
893
|
+
const toFoot = () => {
|
|
894
|
+
scroller.scrollTop = scroller.scrollHeight;
|
|
895
|
+
};
|
|
896
|
+
// Content that grew since the last look keeps a reader at the foot there, whichever of its
|
|
897
|
+
// resize or the scroll it caused is seen first; a reader's own scroll leaves the height.
|
|
898
|
+
let lastHeight = scroller.scrollHeight;
|
|
899
|
+
const follow = () => {
|
|
900
|
+
const grew = scroller.scrollHeight !== lastHeight;
|
|
901
|
+
lastHeight = scroller.scrollHeight;
|
|
902
|
+
if (grew && atFootRef.current)
|
|
903
|
+
return toFoot();
|
|
904
|
+
const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
|
|
905
|
+
atFootRef.current = distance < followDistancePx;
|
|
906
|
+
setAway(!atFootRef.current);
|
|
907
|
+
};
|
|
908
|
+
toFoot();
|
|
909
|
+
const resizes = new ResizeObserver(follow);
|
|
910
|
+
resizes.observe(root);
|
|
911
|
+
resizes.observe(scroller);
|
|
912
|
+
scroller.addEventListener("scroll", follow, { passive: true });
|
|
913
|
+
return () => {
|
|
914
|
+
resizes.disconnect();
|
|
915
|
+
scroller.removeEventListener("scroll", follow);
|
|
916
|
+
};
|
|
917
|
+
}, [rootRef]);
|
|
918
|
+
useEffect(() => {
|
|
919
|
+
const scroller = scrollerRef.current;
|
|
920
|
+
if (!scroller || sentMessageId === undefined)
|
|
921
|
+
return;
|
|
922
|
+
atFootRef.current = true;
|
|
923
|
+
scroller.scrollTop = scroller.scrollHeight;
|
|
924
|
+
}, [sentMessageId]);
|
|
925
|
+
return {
|
|
926
|
+
away,
|
|
927
|
+
scrollToLatest: () => {
|
|
928
|
+
const scroller = scrollerRef.current;
|
|
929
|
+
atFootRef.current = true;
|
|
930
|
+
scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" });
|
|
931
|
+
},
|
|
932
|
+
};
|
|
933
|
+
}
|
|
538
934
|
function findScrollContainer(element) {
|
|
539
935
|
for (let node = element; node; node = node.parentElement) {
|
|
540
936
|
const { overflowY } = getComputedStyle(node);
|