Skip to content

Commit 483092e

Browse files
authored
Merge pull request #2651 from GCWing/gcwing/steering-queue-redesign
feat(flow-chat): redesign steering queue and composer editing
2 parents 07ba7d0 + 21ddd96 commit 483092e

31 files changed

Lines changed: 1417 additions & 538 deletions

design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,40 @@ import {
2323
Image as ImageIcon,
2424
Info,
2525
Layers,
26+
ListEnd,
2627
ListTodo,
2728
MessageSquare,
2829
Mic,
2930
Monitor,
3031
Plus,
32+
Pencil,
3133
Rocket,
3234
Search,
3335
SearchCheck,
3436
Shield,
3537
SquareTerminal,
3638
Terminal,
3739
Timer,
40+
Trash2,
3841
Zap,
3942
type LucideIcon,
4043
} from "lucide-react";
44+
import { IconButton } from "@bitfun/ui";
4145
import {
4246
AgentControlToolCard,
4347
AgentWaitToolCard,
4448
AmbientToolCard,
4549
AmbientToolCardHeader,
4650
AskUser,
4751
ChatComposer,
52+
ChatComposerQueue,
53+
ChatComposerQueueAttachmentBadge,
54+
ChatComposerQueueHeader,
55+
ChatComposerQueueItem,
56+
ChatComposerQueueItemActions,
57+
ChatComposerQueueItemContent,
58+
ChatComposerQueueList,
59+
ChatComposerQueueTitle,
4860
CommandToolCard,
4961
ContextCompressionToolCard,
5062
DefaultToolCard,
@@ -122,6 +134,7 @@ type PreviewProps = FlowChatPreviewRenderOptions;
122134
function ChatComposerPreview({ interactive, state }: PreviewProps) {
123135
const [value, setValue] = useState("");
124136
const expanded = state === "expanded";
137+
const queued = state === "queued";
125138

126139
return (
127140
<div className="flow-chat-composer-preview">
@@ -168,6 +181,44 @@ function ChatComposerPreview({ interactive, state }: PreviewProps) {
168181
</>
169182
)}
170183
layout={expanded ? "expanded" : "compact"}
184+
queue={queued ? (
185+
<ChatComposerQueue aria-label="Wait for sending">
186+
<ChatComposerQueueHeader>
187+
<ListEnd aria-hidden="true" />
188+
<ChatComposerQueueTitle count={13}>
189+
Wait for sending
190+
</ChatComposerQueueTitle>
191+
</ChatComposerQueueHeader>
192+
<ChatComposerQueueList>
193+
<ChatComposerQueueItem>
194+
<ChatComposerQueueItemContent>
195+
Help me turn these two photos into Studio Ghibli style. Wait, maybe…
196+
</ChatComposerQueueItemContent>
197+
<ChatComposerQueueAttachmentBadge
198+
count={3}
199+
label="3 image attachments"
200+
/>
201+
<ChatComposerQueueItemActions>
202+
<IconButton
203+
aria-label="Send now"
204+
icon={<ArrowUp />}
205+
size="xs"
206+
/>
207+
<IconButton
208+
aria-label="Delete"
209+
icon={<Trash2 />}
210+
size="xs"
211+
/>
212+
<IconButton
213+
aria-label="Edit"
214+
icon={<Pencil />}
215+
size="xs"
216+
/>
217+
</ChatComposerQueueItemActions>
218+
</ChatComposerQueueItem>
219+
</ChatComposerQueueList>
220+
</ChatComposerQueue>
221+
) : undefined}
171222
startActions={(
172223
<button
173224
aria-label="Add context"
@@ -970,7 +1021,7 @@ export const flowChatPreviewDefinitions = {
9701021
},
9711022
ChatComposer: {
9721023
attention: "adaptive",
973-
codeSample: () => `import { ChatComposer } from "@bitfun/ui/flow-chat";\n\n<ChatComposer\n contextBar={<WorkspaceContext />}\n layout={multiline ? "expanded" : "compact"}\n startActions={<AddMenu />}\n endActions={<ComposerActions />}\n>\n <RichTextEditor />\n</ChatComposer>`,
1024+
codeSample: () => `import { ChatComposer } from "@bitfun/ui/flow-chat";\n\n<ChatComposer\n contextBar={<WorkspaceContext />}\n queue={pendingMessages.length ? <PendingMessageQueue items={pendingMessages} /> : undefined}\n layout={multiline ? "expanded" : "compact"}\n startActions={<AddMenu />}\n endActions={<ComposerActions />}\n>\n <RichTextEditor />\n</ChatComposer>`,
9741025
icon: MessageSquare,
9751026
render: (options) => <ChatComposerPreview {...options} />,
9761027
section: "framework",

design-system/apps/design-lab/vite/component-detail-contract.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ test("FlowChat gallery renders only the real migrated tool-card components", asy
160160

161161
test("CommandToolCard previews follow the runtime command-state presentation", async () => {
162162
const preview = await readFile(flowChatPreviewSource, "utf8");
163-
const commandPreview = /function CommandPreview[\s\S]*?\n}\n\nfunction resolveFileOperation/.exec(preview)?.[0];
163+
const commandPreview = /function CommandPreview[\s\S]*?\r?\n}\r?\n\r?\nfunction resolveFileOperation/.exec(preview)?.[0];
164164

165165
assert.ok(commandPreview);
166166
assert.match(

design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,31 @@ import type { ComponentMeta } from "../../registry.types";
22

33
export const chatComposerMeta = {
44
category: "flow-chat",
5-
description: "A slot-based FlowChat composer with a context bar and compact or expanded input anatomy.",
5+
description: "A slot-based FlowChat composer with context, pending-message queue, and compact or expanded input anatomy.",
66
maturity: "stable",
77
name: "ChatComposer",
88
props: [
99
{ defaultValue: "compact", name: "layout", type: "compact | expanded" },
1010
{ name: "contextBar", type: "ReactNode" },
11+
{ name: "queue", type: "ReactNode" },
1112
{ name: "children", type: "ReactNode" },
1213
{ name: "startActions", type: "ReactNode" },
1314
{ name: "endActions", type: "ReactNode" },
1415
{ defaultValue: "false", name: "busy", type: "boolean" },
1516
{ defaultValue: "false", name: "disabled", type: "boolean" },
1617
{ name: "surfaceClassName", type: "string" },
1718
],
18-
states: ["default", "expanded", "focus-visible", "busy", "disabled"],
19+
states: ["default", "expanded", "queued", "focus-visible", "busy", "disabled"],
1920
tokens: [
2021
"border.width.default",
2122
"color.action.neutral.border",
2223
"color.border.default",
24+
"color.content.muted",
2325
"color.content.primary",
26+
"color.content.secondary",
2427
"color.surface.panel",
28+
"color.surface.raised",
29+
"color.surface.subtle",
2530
"control.chatComposer.compactGap",
2631
"control.chatComposer.compactHeight",
2732
"control.chatComposer.compactPaddingBlock",
@@ -30,14 +35,18 @@ export const chatComposerMeta = {
3035
"control.chatComposer.controlHeight",
3136
"control.height.md",
3237
"font.family.control",
38+
"font.size.body",
3339
"font.size.small",
40+
"font.weight.medium",
3441
"font.weight.regular",
42+
"radius.lg",
3543
"radius.2xl",
3644
"radius.pill",
3745
"shadow.base",
3846
"space.1",
3947
"space.2",
4048
"space.3",
49+
"space.4",
4150
"space.6",
4251
"space.8",
4352
"space.12",

design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,38 @@
2828
padding-inline: var(--bf-space-2);
2929
}
3030

31+
.body {
32+
display: contents;
33+
}
34+
35+
.queue {
36+
min-inline-size: 0;
37+
}
38+
39+
.queue:empty {
40+
display: none;
41+
}
42+
43+
.body:has(> .queue:not(:empty)) {
44+
box-sizing: border-box;
45+
display: grid;
46+
min-inline-size: 0;
47+
inline-size: 100%;
48+
gap: var(--bf-space-2);
49+
padding: var(--bf-space-2);
50+
border: var(--bf-border-width-default) solid var(--bf-color-action-neutral-border);
51+
border-radius: var(--bf-radius-2xl);
52+
background: var(--bf-color-surface-panel);
53+
box-shadow: var(--bf-shadow-base);
54+
transition:
55+
border-color var(--bf-motion-duration-fast) var(--bf-motion-easing-standard),
56+
box-shadow var(--bf-motion-duration-fast) var(--bf-motion-easing-standard);
57+
}
58+
59+
.body:has(> .queue:not(:empty)):focus-within {
60+
border-color: var(--bf-color-border-default);
61+
}
62+
3163
.surface {
3264
display: grid;
3365
min-inline-size: 0;
@@ -79,6 +111,20 @@
79111
border-color: var(--bf-color-border-default);
80112
}
81113

114+
.body:has(> .queue:not(:empty)) .surface {
115+
border-color: transparent;
116+
background: transparent;
117+
box-shadow: none;
118+
}
119+
120+
.body:has(> .queue:not(:empty)) .surface:focus-within {
121+
border-color: transparent;
122+
}
123+
124+
.body:has(> .queue:not(:empty)) .surface[data-bf-layout="compact"] {
125+
padding-inline: 0;
126+
}
127+
82128
.content {
83129
grid-area: content;
84130
box-sizing: border-box;
@@ -136,14 +182,16 @@
136182
}
137183

138184
@media (prefers-reduced-motion: reduce) {
185+
.body,
139186
.surface {
140187
transition: none;
141188
}
142189
}
143190

144191
@media (forced-colors: active) {
145192
.surface,
146-
.surface:focus-within {
193+
.surface:focus-within,
194+
.body:has(> .queue:not(:empty)) {
147195
border-color: CanvasText;
148196
box-shadow: none;
149197
}

design-system/packages/ui/src/flow-chat/composer/ChatComposer.tsx

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface ChatComposerProps
1818
disabled?: boolean;
1919
endActions?: ReactNode;
2020
layout?: ChatComposerLayout;
21+
queue?: ReactNode;
2122
startActions?: ReactNode;
2223
surfaceClassName?: string;
2324
}
@@ -93,6 +94,7 @@ export const ChatComposer = forwardRef<HTMLDivElement, ChatComposerProps>(
9394
disabled = false,
9495
endActions,
9596
layout = "compact",
97+
queue,
9698
startActions,
9799
surfaceClassName,
98100
...props
@@ -113,7 +115,6 @@ export const ChatComposer = forwardRef<HTMLDivElement, ChatComposerProps>(
113115
<div
114116
{...props}
115117
aria-busy={busy ? true : ariaBusy}
116-
aria-disabled={disabled || undefined}
117118
className={classNames(styles.root, className)}
118119
data-bf-component="chat-composer"
119120
data-bf-state={state}
@@ -125,24 +126,32 @@ export const ChatComposer = forwardRef<HTMLDivElement, ChatComposerProps>(
125126
{contextBar}
126127
</div>
127128
)}
128-
<div
129-
className={classNames(styles.surface, surfaceClassName)}
130-
data-bf-layout={layout}
131-
data-bf-part="surface"
132-
>
133-
{hasSlot(resolvedStartActions) && (
134-
<div className={styles.startActions} data-bf-part="startActions">
135-
{resolvedStartActions}
129+
<div className={styles.body} data-bf-part="body">
130+
{hasSlot(queue) && (
131+
<div className={styles.queue} data-bf-part="queue">
132+
{queue}
136133
</div>
137134
)}
138-
<div className={styles.content} data-bf-part="content">
139-
{compoundSlots.content}
140-
</div>
141-
{hasSlot(resolvedEndActions) && (
142-
<div className={styles.endActions} data-bf-part="endActions">
143-
{resolvedEndActions}
135+
<div
136+
aria-disabled={disabled || undefined}
137+
className={classNames(styles.surface, surfaceClassName)}
138+
data-bf-layout={layout}
139+
data-bf-part="surface"
140+
>
141+
{hasSlot(resolvedStartActions) && (
142+
<div className={styles.startActions} data-bf-part="startActions">
143+
{resolvedStartActions}
144+
</div>
145+
)}
146+
<div className={styles.content} data-bf-part="content">
147+
{compoundSlots.content}
144148
</div>
145-
)}
149+
{hasSlot(resolvedEndActions) && (
150+
<div className={styles.endActions} data-bf-part="endActions">
151+
{resolvedEndActions}
152+
</div>
153+
)}
154+
</div>
146155
</div>
147156
</div>
148157
);

0 commit comments

Comments
 (0)