@flamingo-stack/openframe-frontend-core 0.0.421 → 0.0.422-snapshot.20260713170838
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/{chunk-5TL2VVEN.js → chunk-2Q2SKYCZ.js} +5 -5
- package/dist/{chunk-HBTKCED7.js → chunk-4ACSUJ7Q.js} +13 -6
- package/dist/chunk-4ACSUJ7Q.js.map +1 -0
- package/dist/{chunk-FHAG7O27.js → chunk-7W4474XS.js} +2 -2
- package/dist/{chunk-5WXUR7LF.js → chunk-B2255TIZ.js} +55 -23
- package/dist/chunk-B2255TIZ.js.map +1 -0
- package/dist/{chunk-RKNF2DHY.cjs → chunk-CBY3VQKJ.cjs} +59 -59
- package/dist/{chunk-RKNF2DHY.cjs.map → chunk-CBY3VQKJ.cjs.map} +1 -1
- package/dist/{chunk-WDZATJTF.cjs → chunk-FVI2ESVQ.cjs} +523 -491
- package/dist/chunk-FVI2ESVQ.cjs.map +1 -0
- package/dist/{chunk-UKIVJU3A.js → chunk-GR2CMONR.js} +4 -4
- package/dist/{chunk-PMFK7ZKC.cjs → chunk-HGBEKYXL.cjs} +5 -5
- package/dist/{chunk-PMFK7ZKC.cjs.map → chunk-HGBEKYXL.cjs.map} +1 -1
- package/dist/{chunk-LHJHMZYU.js → chunk-I4YIWY3O.js} +4 -4
- package/dist/{chunk-E6WANQ6K.cjs → chunk-IMK6RUEC.cjs} +13 -6
- package/dist/chunk-IMK6RUEC.cjs.map +1 -0
- package/dist/{chunk-E5HKIBRS.cjs → chunk-IRIPRXO5.cjs} +9 -9
- package/dist/{chunk-E5HKIBRS.cjs.map → chunk-IRIPRXO5.cjs.map} +1 -1
- package/dist/{chunk-TJ7HD4KI.js → chunk-JPZE2P4Y.js} +4 -4
- package/dist/{chunk-4PS4F4WL.cjs → chunk-KNO5CB3W.cjs} +12 -12
- package/dist/{chunk-4PS4F4WL.cjs.map → chunk-KNO5CB3W.cjs.map} +1 -1
- package/dist/{chunk-4XTIANMS.js → chunk-LK3KXZIN.js} +3 -3
- package/dist/{chunk-XVWDKBBF.js → chunk-MOBZMJAP.js} +2 -2
- package/dist/{chunk-5U6C3RXX.cjs → chunk-MSZ4WBCD.cjs} +11 -11
- package/dist/{chunk-5U6C3RXX.cjs.map → chunk-MSZ4WBCD.cjs.map} +1 -1
- package/dist/{chunk-ISUMLEJ6.js → chunk-NNF5XOMH.js} +3 -3
- package/dist/{chunk-YLSDXDD7.cjs → chunk-OTNL36LC.cjs} +37 -37
- package/dist/{chunk-YLSDXDD7.cjs.map → chunk-OTNL36LC.cjs.map} +1 -1
- package/dist/{chunk-RQXFDMRY.cjs → chunk-SMUKHJJM.cjs} +19 -19
- package/dist/{chunk-RQXFDMRY.cjs.map → chunk-SMUKHJJM.cjs.map} +1 -1
- package/dist/{chunk-JMHU37Y6.js → chunk-SVUDKNS7.js} +3 -3
- package/dist/{chunk-UZOYTAK7.cjs → chunk-TIFJIHV2.cjs} +82 -82
- package/dist/{chunk-UZOYTAK7.cjs.map → chunk-TIFJIHV2.cjs.map} +1 -1
- package/dist/{chunk-6HCWW4YM.cjs → chunk-TMSP5FYV.cjs} +26 -26
- package/dist/{chunk-6HCWW4YM.cjs.map → chunk-TMSP5FYV.cjs.map} +1 -1
- package/dist/{chunk-5UZULT35.cjs → chunk-WO2NOBF3.cjs} +31 -31
- package/dist/{chunk-5UZULT35.cjs.map → chunk-WO2NOBF3.cjs.map} +1 -1
- package/dist/{chunk-PCM4HJO4.cjs → chunk-WOHMJJKD.cjs} +30 -30
- package/dist/{chunk-PCM4HJO4.cjs.map → chunk-WOHMJJKD.cjs.map} +1 -1
- package/dist/{chunk-UHSVSDF5.js → chunk-WWS3R3UD.js} +2 -2
- package/dist/{chunk-ZXWSOOEH.js → chunk-YOEQWHUD.js} +2 -2
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/chat/types/api.types.d.ts +10 -0
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +21 -21
- package/dist/components/help-center-pages/index.js +12 -12
- package/dist/components/index.cjs +153 -153
- package/dist/components/index.js +10 -10
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/related-content/index.cjs +4 -4
- package/dist/components/related-content/index.js +3 -3
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/actions-menu.d.ts.map +1 -1
- package/dist/components/ui/button/split-button.d.ts +6 -0
- package/dist/components/ui/button/split-button.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/components/ui/page-actions.d.ts.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/src/components/chat/chat-input.tsx +5 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +52 -3
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +25 -3
- package/src/components/chat/types/api.types.ts +10 -0
- package/src/components/features/time-tracker/time-tracker-panel.tsx +1 -0
- package/src/components/ui/actions-menu.tsx +1 -0
- package/src/components/ui/button/split-button.tsx +18 -3
- package/src/components/ui/page-actions.tsx +16 -14
- package/src/stories/ApprovalRequestMessageClient.stories.tsx +89 -0
- package/src/stories/ChatMessageEnhanced.stories.tsx +219 -0
- package/src/stories/ChatTypingIndicator.stories.tsx +51 -0
- package/src/stories/ContextCompactionDisplay.stories.tsx +44 -0
- package/src/stories/ErrorMessageDisplay.stories.tsx +66 -0
- package/src/stories/ThinkingDisplay.stories.tsx +63 -0
- package/dist/chunk-5WXUR7LF.js.map +0 -1
- package/dist/chunk-E6WANQ6K.cjs.map +0 -1
- package/dist/chunk-HBTKCED7.js.map +0 -1
- package/dist/chunk-WDZATJTF.cjs.map +0 -1
- /package/dist/{chunk-5TL2VVEN.js.map → chunk-2Q2SKYCZ.js.map} +0 -0
- /package/dist/{chunk-FHAG7O27.js.map → chunk-7W4474XS.js.map} +0 -0
- /package/dist/{chunk-UKIVJU3A.js.map → chunk-GR2CMONR.js.map} +0 -0
- /package/dist/{chunk-LHJHMZYU.js.map → chunk-I4YIWY3O.js.map} +0 -0
- /package/dist/{chunk-TJ7HD4KI.js.map → chunk-JPZE2P4Y.js.map} +0 -0
- /package/dist/{chunk-4XTIANMS.js.map → chunk-LK3KXZIN.js.map} +0 -0
- /package/dist/{chunk-XVWDKBBF.js.map → chunk-MOBZMJAP.js.map} +0 -0
- /package/dist/{chunk-ISUMLEJ6.js.map → chunk-NNF5XOMH.js.map} +0 -0
- /package/dist/{chunk-JMHU37Y6.js.map → chunk-SVUDKNS7.js.map} +0 -0
- /package/dist/{chunk-UHSVSDF5.js.map → chunk-WWS3R3UD.js.map} +0 -0
- /package/dist/{chunk-ZXWSOOEH.js.map → chunk-YOEQWHUD.js.map} +0 -0
|
@@ -90,6 +90,7 @@ const SecondaryAction: React.FC<{ action: ActionsMenuItemIconAction }> = ({ acti
|
|
|
90
90
|
|
|
91
91
|
const classes = cn(
|
|
92
92
|
SECONDARY_ACTION_CLASSES,
|
|
93
|
+
action.openInNewTab && "max-md:hidden",
|
|
93
94
|
action.disabled && "cursor-not-allowed opacity-60 pointer-events-none",
|
|
94
95
|
);
|
|
95
96
|
|
|
@@ -32,6 +32,7 @@ const splitHalfVariants = cva(splitHalfBase, {
|
|
|
32
32
|
small: "h-6 md:h-8 px-[var(--spacing-system-xs)] text-h5 [&_svg]:h-3 [&_svg]:w-3 md:[&_svg]:h-4 md:[&_svg]:w-4",
|
|
33
33
|
},
|
|
34
34
|
side: { main: "", icon: "" },
|
|
35
|
+
soloOnMobile: { true: "", false: "" },
|
|
35
36
|
},
|
|
36
37
|
compoundVariants: [
|
|
37
38
|
// Rounded corners + per-variant seam. The icon-side's left border is the divider.
|
|
@@ -55,8 +56,12 @@ const splitHalfVariants = cva(splitHalfBase, {
|
|
|
55
56
|
// Icon half: per Figma, narrower than main height (default: 40×48; small: 32×32).
|
|
56
57
|
{ side: "icon", size: "default", class: "w-10 px-0" },
|
|
57
58
|
{ side: "icon", size: "small", class: "w-6 md:w-8 px-0" },
|
|
59
|
+
|
|
60
|
+
{ side: "icon", soloOnMobile: true, class: "max-md:hidden" },
|
|
61
|
+
{ side: "main", soloOnMobile: true, class: "max-md:rounded-r-md" },
|
|
62
|
+
{ variant: "outline", side: "main", soloOnMobile: true, class: "max-md:border-r" },
|
|
58
63
|
],
|
|
59
|
-
defaultVariants: { variant: "accent", size: "default", side: "main" },
|
|
64
|
+
defaultVariants: { variant: "accent", size: "default", side: "main", soloOnMobile: false },
|
|
60
65
|
})
|
|
61
66
|
|
|
62
67
|
type SplitButtonVariant = NonNullable<VariantProps<typeof splitHalfVariants>["variant"]>
|
|
@@ -68,6 +73,11 @@ interface SplitButtonIconAction {
|
|
|
68
73
|
"aria-label": string
|
|
69
74
|
onClick?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>
|
|
70
75
|
href?: string
|
|
76
|
+
/**
|
|
77
|
+
* Opens `href` with `target="_blank"`. Also marks the half as a new-tab
|
|
78
|
+
* affordance, which hides it below `md` (set it for `onClick` handlers that
|
|
79
|
+
* open a new tab/window themselves).
|
|
80
|
+
*/
|
|
71
81
|
openInNewTab?: boolean
|
|
72
82
|
prefetch?: boolean
|
|
73
83
|
disabled?: boolean
|
|
@@ -101,6 +111,7 @@ interface HalfOptions {
|
|
|
101
111
|
variant: SplitButtonVariant
|
|
102
112
|
size: SplitButtonSize
|
|
103
113
|
side: "main" | "icon"
|
|
114
|
+
soloOnMobile?: boolean
|
|
104
115
|
href?: string
|
|
105
116
|
openInNewTab?: boolean
|
|
106
117
|
prefetch?: boolean
|
|
@@ -113,8 +124,8 @@ interface HalfOptions {
|
|
|
113
124
|
children: React.ReactNode
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
function Half({ variant, size, side, href, openInNewTab, prefetch, onClick, disabled, grow, type = "button", ariaLabel, children }: HalfOptions) {
|
|
117
|
-
const classes = cn(splitHalfVariants({ variant, size, side }), grow && "flex-1")
|
|
127
|
+
function Half({ variant, size, side, soloOnMobile, href, openInNewTab, prefetch, onClick, disabled, grow, type = "button", ariaLabel, children }: HalfOptions) {
|
|
128
|
+
const classes = cn(splitHalfVariants({ variant, size, side, soloOnMobile }), grow && "flex-1")
|
|
118
129
|
|
|
119
130
|
if (href) {
|
|
120
131
|
return (
|
|
@@ -169,6 +180,8 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
|
|
|
169
180
|
},
|
|
170
181
|
ref,
|
|
171
182
|
) {
|
|
183
|
+
const soloOnMobile = !!iconAction.openInNewTab
|
|
184
|
+
|
|
172
185
|
return (
|
|
173
186
|
<div
|
|
174
187
|
ref={ref}
|
|
@@ -180,6 +193,7 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
|
|
|
180
193
|
variant={variant}
|
|
181
194
|
size={size}
|
|
182
195
|
side="main"
|
|
196
|
+
soloOnMobile={soloOnMobile}
|
|
183
197
|
href={href}
|
|
184
198
|
openInNewTab={openInNewTab}
|
|
185
199
|
prefetch={prefetch}
|
|
@@ -197,6 +211,7 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
|
|
|
197
211
|
variant={variant}
|
|
198
212
|
size={size}
|
|
199
213
|
side="icon"
|
|
214
|
+
soloOnMobile={soloOnMobile}
|
|
200
215
|
href={iconAction.href}
|
|
201
216
|
openInNewTab={iconAction.openInNewTab}
|
|
202
217
|
prefetch={iconAction.prefetch}
|
|
@@ -245,6 +245,12 @@ function IconButtonsVariant({
|
|
|
245
245
|
const singleAction = isSingleAction ? actions[0] : null
|
|
246
246
|
const useSingleActionMobile = isSingleAction && !hasMenuActions
|
|
247
247
|
|
|
248
|
+
const mobileMenuGroups = [
|
|
249
|
+
{ items: actions.flatMap(actionToMenuItems) },
|
|
250
|
+
...(menuActions ?? []),
|
|
251
|
+
]
|
|
252
|
+
const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0)
|
|
253
|
+
|
|
248
254
|
return (
|
|
249
255
|
<>
|
|
250
256
|
{/* Desktop: every action as an icon button + optional overflow menu */}
|
|
@@ -262,14 +268,9 @@ function IconButtonsVariant({
|
|
|
262
268
|
<div className={cn('flex md:hidden', className)}>
|
|
263
269
|
{useSingleActionMobile && singleAction ? (
|
|
264
270
|
renderActionButton(singleAction, { iconOnly: true })
|
|
265
|
-
) : (
|
|
266
|
-
<ActionsMenuDropdown
|
|
267
|
-
|
|
268
|
-
{ items: actions.flatMap(actionToMenuItems) },
|
|
269
|
-
...(menuActions ?? []),
|
|
270
|
-
]}
|
|
271
|
-
/>
|
|
272
|
-
)}
|
|
271
|
+
) : hasMobileMenuItems ? (
|
|
272
|
+
<ActionsMenuDropdown groups={mobileMenuGroups} />
|
|
273
|
+
) : null}
|
|
273
274
|
</div>
|
|
274
275
|
</>
|
|
275
276
|
)
|
|
@@ -328,6 +329,12 @@ function MenuPrimaryVariant({
|
|
|
328
329
|
const desktopActions = actions.filter(a => !a.showOnlyMobile)
|
|
329
330
|
const hasMenuActions = menuActions.some(g => g.items.length > 0)
|
|
330
331
|
|
|
332
|
+
const mobileMenuGroups = [
|
|
333
|
+
{ items: actions.flatMap(actionToMenuItems) },
|
|
334
|
+
...menuActions,
|
|
335
|
+
]
|
|
336
|
+
const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0)
|
|
337
|
+
|
|
331
338
|
return (
|
|
332
339
|
<>
|
|
333
340
|
<div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
|
|
@@ -341,12 +348,7 @@ function MenuPrimaryVariant({
|
|
|
341
348
|
</div>
|
|
342
349
|
|
|
343
350
|
<div className={cn('flex md:hidden', className)}>
|
|
344
|
-
<ActionsMenuDropdown
|
|
345
|
-
groups={[
|
|
346
|
-
{ items: actions.flatMap(actionToMenuItems) },
|
|
347
|
-
...menuActions,
|
|
348
|
-
]}
|
|
349
|
-
/>
|
|
351
|
+
{hasMobileMenuItems ? <ActionsMenuDropdown groups={mobileMenuGroups} /> : null}
|
|
350
352
|
</div>
|
|
351
353
|
</>
|
|
352
354
|
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
import { ApprovalRequestMessage } from "../components/chat/approval-request-message";
|
|
5
|
+
import type { ApprovalRequestData } from "../components/chat/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CLIENT (end-user Fae desktop app) variant of the single-command approval
|
|
9
|
+
* card — Figma node 203-11947 ("fae-approval-block").
|
|
10
|
+
*
|
|
11
|
+
* Same component and props as the Admin card, driven by `variant="client"`.
|
|
12
|
+
* The end client sees ONLY the BE-generated title (`explanation`) plus the
|
|
13
|
+
* Approve/Reject buttons, or the full-text resolved pill
|
|
14
|
+
* ("APPROVED BY JOHN SMITH"). The raw command is never rendered.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const baseData: ApprovalRequestData = {
|
|
18
|
+
command: 'brew install --cask slack',
|
|
19
|
+
requestId: "req-client-1",
|
|
20
|
+
approvalType: "CLIENT",
|
|
21
|
+
// BE-generated human-readable title — the ONLY content the client sees.
|
|
22
|
+
explanation: "Install Slack via Homebrew Cask on macOS",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const plainDecorator =
|
|
26
|
+
(width?: number) => (Story: React.ComponentType) => (
|
|
27
|
+
<div style={{ maxWidth: width ?? 400, background: "var(--color-bg)" }}>
|
|
28
|
+
<Story />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const meta = {
|
|
33
|
+
title: "Chat/Client/ApprovalRequestMessage",
|
|
34
|
+
component: ApprovalRequestMessage,
|
|
35
|
+
tags: ["autodocs"],
|
|
36
|
+
args: { variant: "client" },
|
|
37
|
+
parameters: {
|
|
38
|
+
docs: {
|
|
39
|
+
description: {
|
|
40
|
+
component:
|
|
41
|
+
"CLIENT (Fae end-user) single-command approval card — title-only with Approve/Reject or a full-text status pill ('APPROVED BY {NAME}'). The raw command is never shown. Gated by variant='client' (Figma 203-11947).",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
decorators: [plainDecorator()],
|
|
46
|
+
} satisfies Meta<typeof ApprovalRequestMessage>;
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<typeof meta>;
|
|
50
|
+
|
|
51
|
+
/** Pending — BE title + Approve / Reject buttons. No command block. */
|
|
52
|
+
export const Pending: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
data: baseData,
|
|
55
|
+
status: "pending",
|
|
56
|
+
onApprove: () => {},
|
|
57
|
+
onReject: () => {},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Approved with resolver — green "APPROVED BY JOHN SMITH" pill. */
|
|
62
|
+
export const ApprovedByUser: Story = {
|
|
63
|
+
args: { data: baseData, status: "approved", resolvedByName: "John Smith" },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Approved without a resolver name — plain "APPROVED" pill (system action). */
|
|
67
|
+
export const Approved: Story = {
|
|
68
|
+
args: { data: baseData, status: "approved" },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Rejected with resolver — red "REJECTED BY JOHN SMITH" pill. */
|
|
72
|
+
export const RejectedByUser: Story = {
|
|
73
|
+
args: { data: baseData, status: "rejected", resolvedByName: "John Smith" },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Cancelled — grey status pill. */
|
|
77
|
+
export const Cancelled: Story = {
|
|
78
|
+
args: { data: baseData, status: "cancelled" },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Missing BE title — falls back to the "Approval required" placeholder. */
|
|
82
|
+
export const NoTitleFallback: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
data: { ...baseData, explanation: undefined },
|
|
85
|
+
status: "pending",
|
|
86
|
+
onApprove: () => {},
|
|
87
|
+
onReject: () => {},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
import { ChatMessageEnhanced } from "../components/chat/chat-message-enhanced";
|
|
5
|
+
import { ChatTypingIndicator } from "../components/chat/chat-typing-indicator";
|
|
6
|
+
import type { MessageSegment } from "../components/chat/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `<ChatMessageEnhanced>` — THE chat message renderer used by
|
|
10
|
+
* ChatMessageList / EmbeddableChat. Renders the avatar + name + timestamp
|
|
11
|
+
* header (Fae pink / Mingo cyan / user grey) and every message-segment type:
|
|
12
|
+
* text (markdown), thinking, tool_execution, approval_request/batch, error,
|
|
13
|
+
* context_compaction.
|
|
14
|
+
*
|
|
15
|
+
* The Fae conversation scenes reproduce the Fae desktop-app flow: greeting →
|
|
16
|
+
* user request → approval block → typing dots (Figma "fae-approval-block").
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Fixed time-of-day so the header always reads "2:47 PM" — messages dated
|
|
20
|
+
// today render time-only (see formatMessageTimestamp).
|
|
21
|
+
const at247 = () => {
|
|
22
|
+
const d = new Date();
|
|
23
|
+
d.setHours(14, 47, 0, 0);
|
|
24
|
+
return d;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const panelDecorator = (Story: React.ComponentType) => (
|
|
28
|
+
<div
|
|
29
|
+
style={{ maxWidth: 640, background: "var(--color-bg)" }}
|
|
30
|
+
className="p-[var(--spacing-system-m)]"
|
|
31
|
+
>
|
|
32
|
+
<Story />
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const meta = {
|
|
37
|
+
title: "Chat/ChatMessageEnhanced",
|
|
38
|
+
component: ChatMessageEnhanced,
|
|
39
|
+
tags: ["autodocs"],
|
|
40
|
+
parameters: {
|
|
41
|
+
docs: {
|
|
42
|
+
description: {
|
|
43
|
+
component:
|
|
44
|
+
"Single chat message row — avatar + colored author name + timestamp header, plus the full segment renderer (text/thinking/tool/approval/error/compaction). Used by ChatMessageList in both the admin (Mingo) and client (Fae) chats.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
decorators: [panelDecorator],
|
|
49
|
+
} satisfies Meta<typeof ChatMessageEnhanced>;
|
|
50
|
+
|
|
51
|
+
export default meta;
|
|
52
|
+
type Story = StoryObj<typeof meta>;
|
|
53
|
+
|
|
54
|
+
/** Assistant (Fae) text message — pink mono name + timestamp. */
|
|
55
|
+
export const AssistantFae: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
role: "assistant",
|
|
58
|
+
name: "Fae",
|
|
59
|
+
content: "Good morning! How can I help you today?",
|
|
60
|
+
timestamp: at247(),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Assistant (Mingo) text message — cyan mono name. */
|
|
65
|
+
export const AssistantMingo: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
role: "assistant",
|
|
68
|
+
name: "Mingo",
|
|
69
|
+
assistantType: "mingo",
|
|
70
|
+
content: "I've pulled the device list for Acme Corp — 3 endpoints are offline.",
|
|
71
|
+
timestamp: at247(),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** User message — grey mono name, no avatar block until user avatars ship. */
|
|
76
|
+
export const UserMessage: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
role: "user",
|
|
79
|
+
name: "John Smith",
|
|
80
|
+
content: "hey can u install slack on my mac",
|
|
81
|
+
timestamp: at247(),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fae flow, step 1 — greeting, user request, and the assistant "typing"
|
|
87
|
+
* row (empty message + `<ChatTypingIndicator>`, as the Fae desktop host
|
|
88
|
+
* renders it while the reply streams in).
|
|
89
|
+
*/
|
|
90
|
+
export const FaeConversationTyping: Story = {
|
|
91
|
+
args: { role: "assistant", content: "" },
|
|
92
|
+
render: () => (
|
|
93
|
+
<div className="flex flex-col">
|
|
94
|
+
<ChatMessageEnhanced
|
|
95
|
+
role="assistant"
|
|
96
|
+
name="Fae"
|
|
97
|
+
content="Good morning! How can I help you today?"
|
|
98
|
+
timestamp={at247()}
|
|
99
|
+
/>
|
|
100
|
+
<ChatMessageEnhanced
|
|
101
|
+
role="user"
|
|
102
|
+
name="John Smith"
|
|
103
|
+
content="hey can u install slack on my mac"
|
|
104
|
+
timestamp={at247()}
|
|
105
|
+
/>
|
|
106
|
+
<ChatMessageEnhanced role="assistant" name="Fae" content="" isTyping timestamp={at247()} />
|
|
107
|
+
<ChatTypingIndicator size="sm" />
|
|
108
|
+
</div>
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const approvedSegments: MessageSegment[] = [
|
|
113
|
+
{ type: "text", text: "On it! Just need your quick approval here." },
|
|
114
|
+
{
|
|
115
|
+
type: "approval_request",
|
|
116
|
+
data: {
|
|
117
|
+
command: "brew install --cask slack",
|
|
118
|
+
requestId: "req-1",
|
|
119
|
+
approvalType: "CLIENT",
|
|
120
|
+
explanation: "Install Slack via Homebrew Cask on macOS",
|
|
121
|
+
},
|
|
122
|
+
status: "approved",
|
|
123
|
+
resolvedByName: "John Smith",
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fae flow, step 2 — the reply carries an `approval_request` segment
|
|
129
|
+
* rendered as the CLIENT card with the resolved "APPROVED BY JOHN SMITH"
|
|
130
|
+
* pill (`approvalVariant="client"`), then Fae keeps typing.
|
|
131
|
+
*/
|
|
132
|
+
export const FaeConversationApproved: Story = {
|
|
133
|
+
args: { role: "assistant", content: "" },
|
|
134
|
+
render: () => (
|
|
135
|
+
<div className="flex flex-col">
|
|
136
|
+
<ChatMessageEnhanced
|
|
137
|
+
role="assistant"
|
|
138
|
+
name="Fae"
|
|
139
|
+
content="Good morning! How can I help you today?"
|
|
140
|
+
timestamp={at247()}
|
|
141
|
+
/>
|
|
142
|
+
<ChatMessageEnhanced
|
|
143
|
+
role="user"
|
|
144
|
+
name="John Smith"
|
|
145
|
+
content="hey can u install slack on my mac"
|
|
146
|
+
timestamp={at247()}
|
|
147
|
+
/>
|
|
148
|
+
<ChatMessageEnhanced
|
|
149
|
+
role="assistant"
|
|
150
|
+
name="Fae"
|
|
151
|
+
content={approvedSegments}
|
|
152
|
+
approvalVariant="client"
|
|
153
|
+
timestamp={at247()}
|
|
154
|
+
/>
|
|
155
|
+
<ChatMessageEnhanced role="assistant" name="Fae" content="" isTyping timestamp={at247()} />
|
|
156
|
+
<ChatTypingIndicator size="sm" />
|
|
157
|
+
</div>
|
|
158
|
+
),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Pending approval inside a message — Approve/Reject buttons (client card). */
|
|
162
|
+
export const PendingApprovalSegment: Story = {
|
|
163
|
+
args: {
|
|
164
|
+
role: "assistant",
|
|
165
|
+
name: "Fae",
|
|
166
|
+
approvalVariant: "client",
|
|
167
|
+
timestamp: at247(),
|
|
168
|
+
content: [
|
|
169
|
+
{ type: "text", text: "On it! Just need your quick approval here." },
|
|
170
|
+
{
|
|
171
|
+
type: "approval_request",
|
|
172
|
+
data: {
|
|
173
|
+
command: "brew install --cask slack",
|
|
174
|
+
requestId: "req-2",
|
|
175
|
+
approvalType: "CLIENT",
|
|
176
|
+
explanation: "Install Slack via Homebrew Cask on macOS",
|
|
177
|
+
},
|
|
178
|
+
status: "pending",
|
|
179
|
+
onApprove: () => {},
|
|
180
|
+
onReject: () => {},
|
|
181
|
+
},
|
|
182
|
+
] satisfies MessageSegment[],
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** Every remaining segment type in one message — thinking, tool execution, context compaction, error. */
|
|
187
|
+
export const AllSegmentTypes: Story = {
|
|
188
|
+
args: {
|
|
189
|
+
role: "assistant",
|
|
190
|
+
name: "Fae",
|
|
191
|
+
timestamp: at247(),
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: "thinking",
|
|
195
|
+
text: "The user wants Slack installed. Homebrew is present on the endpoint, so `brew install --cask slack` is the cleanest path.",
|
|
196
|
+
},
|
|
197
|
+
{ type: "text", text: "Installing Slack now." },
|
|
198
|
+
{
|
|
199
|
+
type: "tool_execution",
|
|
200
|
+
data: {
|
|
201
|
+
type: "EXECUTED_TOOL",
|
|
202
|
+
integratedToolType: "OPENFRAME_RMM",
|
|
203
|
+
toolFunction: "run_script",
|
|
204
|
+
toolTitle: "Install Slack via Homebrew Cask",
|
|
205
|
+
parameters: { command: "brew install --cask slack" },
|
|
206
|
+
result: "🍺 slack was successfully installed!",
|
|
207
|
+
success: true,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{ type: "context_compaction", status: "completed" },
|
|
211
|
+
{
|
|
212
|
+
type: "error",
|
|
213
|
+
title: "Notification delivery failed",
|
|
214
|
+
details: "Slack webhook returned 429 — the completion ping was skipped. The install itself succeeded.",
|
|
215
|
+
},
|
|
216
|
+
{ type: "text", text: "Done — Slack is installed and ready to use." },
|
|
217
|
+
] satisfies MessageSegment[],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
import { ChatTypingIndicator } from "../components/chat/chat-typing-indicator";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Three pulsing dots shown while the assistant reply streams in — the
|
|
8
|
+
* "Fae is typing" affordance under the message header. Also rendered
|
|
9
|
+
* inside `<ChatInput>` (size="sm") while a send is in flight.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const plainDecorator = (Story: React.ComponentType) => (
|
|
13
|
+
<div style={{ maxWidth: 400, background: "var(--color-bg)" }} className="p-4">
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const meta = {
|
|
19
|
+
title: "Chat/ChatTypingIndicator",
|
|
20
|
+
component: ChatTypingIndicator,
|
|
21
|
+
tags: ["autodocs"],
|
|
22
|
+
parameters: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
component:
|
|
26
|
+
"Pulsing three-dot typing indicator. Sizes sm/md/lg; optional 'Assistant is typing' label; dot color overridable via dotClassName.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
decorators: [plainDecorator],
|
|
31
|
+
} satisfies Meta<typeof ChatTypingIndicator>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
/** Default — medium dots, no label. */
|
|
37
|
+
export const Default: Story = {};
|
|
38
|
+
|
|
39
|
+
/** Small — as rendered inside ChatInput while a send is in flight. */
|
|
40
|
+
export const Small: Story = { args: { size: "sm" } };
|
|
41
|
+
|
|
42
|
+
/** Large dots. */
|
|
43
|
+
export const Large: Story = { args: { size: "lg" } };
|
|
44
|
+
|
|
45
|
+
/** With the "Assistant is typing" label. */
|
|
46
|
+
export const WithText: Story = { args: { showText: true } };
|
|
47
|
+
|
|
48
|
+
/** Muted dots via dotClassName override. */
|
|
49
|
+
export const MutedDots: Story = {
|
|
50
|
+
args: { dotClassName: "bg-ods-text-secondary" },
|
|
51
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
import { ContextCompactionDisplay } from "../components/chat/context-compaction-display";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inline status pill for `context_compaction` message segments — shown when
|
|
8
|
+
* the conversation hits the context limit and earlier messages get
|
|
9
|
+
* summarized. `started` shows a dots loader, `completed` a green check.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const plainDecorator = (Story: React.ComponentType) => (
|
|
13
|
+
<div style={{ maxWidth: 640, background: "var(--color-bg)" }} className="p-4">
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const meta = {
|
|
19
|
+
title: "Chat/ContextCompactionDisplay",
|
|
20
|
+
component: ContextCompactionDisplay,
|
|
21
|
+
tags: ["autodocs"],
|
|
22
|
+
parameters: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
component:
|
|
26
|
+
"Renders a `context_compaction` message segment — 'Context limit reached…' with a dots loader while running, 'Earlier context summarized.' with a check once done.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
decorators: [plainDecorator],
|
|
31
|
+
} satisfies Meta<typeof ContextCompactionDisplay>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
/** In progress — summarizing earlier messages. */
|
|
37
|
+
export const Started: Story = {
|
|
38
|
+
args: { status: "started" },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Finished — earlier context summarized. */
|
|
42
|
+
export const Completed: Story = {
|
|
43
|
+
args: { status: "completed" },
|
|
44
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
import { ErrorMessageDisplay } from "../components/chat/error-message-display";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inline error card for `error` message segments — mono uppercase title
|
|
8
|
+
* with an alert icon; optional details expand under the chevron.
|
|
9
|
+
* `type` tints the icon: error (red) / warning (yellow) / info (grey).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const plainDecorator = (Story: React.ComponentType) => (
|
|
13
|
+
<div style={{ maxWidth: 640, background: "var(--color-bg)" }} className="p-4">
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const meta = {
|
|
19
|
+
title: "Chat/ErrorMessageDisplay",
|
|
20
|
+
component: ErrorMessageDisplay,
|
|
21
|
+
tags: ["autodocs"],
|
|
22
|
+
parameters: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
component:
|
|
26
|
+
"Renders an `error` message segment — collapsed mono title row, expandable details. Icon tint follows type: error / warning / info.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
decorators: [plainDecorator],
|
|
31
|
+
} satisfies Meta<typeof ErrorMessageDisplay>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
/** Error with expandable details. */
|
|
37
|
+
export const WithDetails: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
title: "Tool execution failed",
|
|
40
|
+
details:
|
|
41
|
+
"run_script exited with code 1: brew not found on this endpoint. Install Homebrew first or use the direct .dmg fallback.",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Title only — no chevron, row is not clickable. */
|
|
46
|
+
export const TitleOnly: Story = {
|
|
47
|
+
args: { title: "Stream disconnected — retrying" },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Warning tint. */
|
|
51
|
+
export const Warning: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
type: "warning",
|
|
54
|
+
title: "Approval expired",
|
|
55
|
+
details: "The approval request timed out after 15 minutes and was cancelled automatically.",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Info tint. */
|
|
60
|
+
export const Info: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
type: "info",
|
|
63
|
+
title: "Session resumed",
|
|
64
|
+
details: "Reconnected to the stream; missed chunks were replayed from history.",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
import { ThinkingDisplay } from "../components/chat/thinking-display";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collapsible "Thought" block for `thinking` message segments — muted
|
|
8
|
+
* markdown inside a bordered card, one line collapsed, expandable via the
|
|
9
|
+
* chevron. While the segment is still streaming the label reads "Thinking"
|
|
10
|
+
* and the collapse transition is disabled.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const SHORT_THOUGHT =
|
|
14
|
+
"The user wants Slack installed. Homebrew is present, so `brew install --cask slack` is the cleanest path.";
|
|
15
|
+
|
|
16
|
+
const LONG_THOUGHT = `The user asked to install Slack on their Mac.
|
|
17
|
+
|
|
18
|
+
**Options considered:**
|
|
19
|
+
|
|
20
|
+
1. Direct \`.dmg\` download — needs manual mounting and copying, hard to automate.
|
|
21
|
+
2. Homebrew Cask — \`brew install --cask slack\`, idempotent and scriptable.
|
|
22
|
+
3. Mac App Store via \`mas\` — requires the user to be signed in to the store.
|
|
23
|
+
|
|
24
|
+
Homebrew is already present on this endpoint (verified in the device inventory), so option 2 is the cleanest path. The install needs client approval because it modifies installed software.`;
|
|
25
|
+
|
|
26
|
+
const plainDecorator = (Story: React.ComponentType) => (
|
|
27
|
+
<div style={{ maxWidth: 640, background: "var(--color-bg)" }} className="p-4">
|
|
28
|
+
<Story />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const meta = {
|
|
33
|
+
title: "Chat/ThinkingDisplay",
|
|
34
|
+
component: ThinkingDisplay,
|
|
35
|
+
tags: ["autodocs"],
|
|
36
|
+
parameters: {
|
|
37
|
+
docs: {
|
|
38
|
+
description: {
|
|
39
|
+
component:
|
|
40
|
+
"Renders a `thinking` message segment — collapsed single-line 'Thought' card that expands to the full muted-markdown reasoning. Streaming state shows 'Thinking' with a dots loader.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
decorators: [plainDecorator],
|
|
45
|
+
} satisfies Meta<typeof ThinkingDisplay>;
|
|
46
|
+
|
|
47
|
+
export default meta;
|
|
48
|
+
type Story = StoryObj<typeof meta>;
|
|
49
|
+
|
|
50
|
+
/** Finished thought — collapsed to one line, expandable. */
|
|
51
|
+
export const Thought: Story = {
|
|
52
|
+
args: { text: SHORT_THOUGHT },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Streaming — "Thinking" label, transition disabled while tokens arrive. */
|
|
56
|
+
export const Streaming: Story = {
|
|
57
|
+
args: { text: SHORT_THOUGHT, isStreaming: true },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Long markdown thought — headings, list, inline code; expand to read. */
|
|
61
|
+
export const LongMarkdown: Story = {
|
|
62
|
+
args: { text: LONG_THOUGHT },
|
|
63
|
+
};
|