@flamingo-stack/openframe-frontend-core 0.0.420 → 0.0.421-snapshot.20260713170448
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-WBLTGO7G.cjs → chunk-2JNFQEVX.cjs} +26 -26
- package/dist/{chunk-WBLTGO7G.cjs.map → chunk-2JNFQEVX.cjs.map} +1 -1
- package/dist/{chunk-T55A3BQD.js → chunk-3BGWK5QI.js} +4 -4
- package/dist/{chunk-ZZFLE5LG.js → chunk-4BWQYGIY.js} +2 -2
- package/dist/{chunk-KEWTMPHB.js → chunk-4TMMYZTY.js} +2 -2
- package/dist/{chunk-U7EUN5FZ.js → chunk-6F5OKJTW.js} +4 -4
- package/dist/{chunk-KLMLQ35Y.cjs → chunk-A2IRUIUJ.cjs} +37 -37
- package/dist/{chunk-KLMLQ35Y.cjs.map → chunk-A2IRUIUJ.cjs.map} +1 -1
- package/dist/{chunk-KPLRIPEH.cjs → chunk-B62DI2SC.cjs} +15 -15
- package/dist/{chunk-KPLRIPEH.cjs.map → chunk-B62DI2SC.cjs.map} +1 -1
- package/dist/{chunk-PUXTMLKC.cjs → chunk-BF5BXKM7.cjs} +62 -62
- package/dist/{chunk-PUXTMLKC.cjs.map → chunk-BF5BXKM7.cjs.map} +1 -1
- package/dist/{chunk-PAF4EFRR.js → chunk-DUHM44CQ.js} +2 -2
- package/dist/{chunk-YYF2AYM3.js → chunk-HFK33T6A.js} +4 -4
- package/dist/{chunk-J7WFE7WL.js → chunk-JSL3VXGJ.js} +2 -2
- package/dist/{chunk-BXXXBZYY.cjs → chunk-KUDLV25M.cjs} +190 -154
- package/dist/chunk-KUDLV25M.cjs.map +1 -0
- package/dist/{chunk-OBMYKFBO.js → chunk-LHM2VUTO.js} +2 -2
- package/dist/{chunk-AMUTZJZX.cjs → chunk-LWC2U5OC.cjs} +9 -9
- package/dist/{chunk-AMUTZJZX.cjs.map → chunk-LWC2U5OC.cjs.map} +1 -1
- package/dist/{chunk-2AN4EHN6.cjs → chunk-O3FSSWV4.cjs} +7 -7
- package/dist/{chunk-2AN4EHN6.cjs.map → chunk-O3FSSWV4.cjs.map} +1 -1
- package/dist/{chunk-PHJ6335O.cjs → chunk-PO4PXWZN.cjs} +5 -5
- package/dist/{chunk-PHJ6335O.cjs.map → chunk-PO4PXWZN.cjs.map} +1 -1
- package/dist/{chunk-ORDDVXDS.cjs → chunk-RGKGRS2T.cjs} +59 -59
- package/dist/{chunk-ORDDVXDS.cjs.map → chunk-RGKGRS2T.cjs.map} +1 -1
- package/dist/{chunk-5DTE27A4.cjs → chunk-ULUH2X53.cjs} +11 -11
- package/dist/{chunk-5DTE27A4.cjs.map → chunk-ULUH2X53.cjs.map} +1 -1
- package/dist/{chunk-NIS5VXZJ.cjs → chunk-WAQH35VG.cjs} +26 -26
- package/dist/{chunk-NIS5VXZJ.cjs.map → chunk-WAQH35VG.cjs.map} +1 -1
- package/dist/{chunk-G4W5D7L4.js → chunk-WQEEGFDM.js} +2 -2
- package/dist/{chunk-GC4355MW.js → chunk-XFYQV2K2.js} +2 -2
- package/dist/{chunk-3AJOKFJ4.js → chunk-YJDVAT7J.js} +66 -30
- package/dist/chunk-YJDVAT7J.js.map +1 -0
- package/dist/{chunk-4OZLRBOH.js → chunk-YUMFRC3D.js} +4 -4
- package/dist/{chunk-NDYJ37LY.cjs → chunk-YZQNKMFQ.cjs} +31 -31
- package/dist/{chunk-NDYJ37LY.cjs.map → chunk-YZQNKMFQ.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts +9 -0
- package/dist/components/chat/embeddable-chat.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 +2 -2
- package/dist/components/chat/index.js +1 -1
- 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 +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +128 -128
- package/dist/components/index.js +9 -9
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tabs.d.ts +10 -0
- package/dist/components/tabs.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/dashboard-info-card.d.ts +26 -12
- package/dist/components/ui/dashboard-info-card.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/components/ui/tabs.d.ts +11 -0
- package/dist/components/ui/tabs.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-input.tsx +5 -1
- package/src/components/chat/embeddable-chat.tsx +17 -3
- 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/tabs.tsx +10 -0
- package/src/components/ui/dashboard-info-card.tsx +70 -45
- package/src/components/ui/tab-navigation.tsx +5 -2
- package/src/components/ui/tabs.tsx +11 -0
- 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/DashboardInfoCard.stories.tsx +94 -9
- package/src/stories/EmbeddableChat.stories.tsx +159 -0
- package/src/stories/ErrorMessageDisplay.stories.tsx +66 -0
- package/src/stories/ThinkingDisplay.stories.tsx +63 -0
- package/src/stories/TicketCard.stories.tsx +245 -0
- package/dist/chunk-3AJOKFJ4.js.map +0 -1
- package/dist/chunk-BXXXBZYY.cjs.map +0 -1
- /package/dist/{chunk-T55A3BQD.js.map → chunk-3BGWK5QI.js.map} +0 -0
- /package/dist/{chunk-ZZFLE5LG.js.map → chunk-4BWQYGIY.js.map} +0 -0
- /package/dist/{chunk-KEWTMPHB.js.map → chunk-4TMMYZTY.js.map} +0 -0
- /package/dist/{chunk-U7EUN5FZ.js.map → chunk-6F5OKJTW.js.map} +0 -0
- /package/dist/{chunk-PAF4EFRR.js.map → chunk-DUHM44CQ.js.map} +0 -0
- /package/dist/{chunk-YYF2AYM3.js.map → chunk-HFK33T6A.js.map} +0 -0
- /package/dist/{chunk-J7WFE7WL.js.map → chunk-JSL3VXGJ.js.map} +0 -0
- /package/dist/{chunk-OBMYKFBO.js.map → chunk-LHM2VUTO.js.map} +0 -0
- /package/dist/{chunk-G4W5D7L4.js.map → chunk-WQEEGFDM.js.map} +0 -0
- /package/dist/{chunk-GC4355MW.js.map → chunk-XFYQV2K2.js.map} +0 -0
- /package/dist/{chunk-4OZLRBOH.js.map → chunk-YUMFRC3D.js.map} +0 -0
|
@@ -80,6 +80,12 @@ export function useRealtimeChunkProcessor(
|
|
|
80
80
|
// by the host flag (optimistic) or by an in-order DIRECT_MESSAGE chunk.
|
|
81
81
|
const directModeFlagRef = useRef(isDirectMode)
|
|
82
82
|
const sawDirectMessageRef = useRef(false)
|
|
83
|
+
// One-shot teardown guard for the barrier. Busy state can be asserted
|
|
84
|
+
// OUTSIDE an open stream (onAgentBusy on tool/approval chunks), so the
|
|
85
|
+
// teardown must fire once even when isInStreamRef is false — otherwise a
|
|
86
|
+
// human takeover right after an approval leaves the consumer's composer
|
|
87
|
+
// locked forever (all releasing AI chunks get dropped by the barrier).
|
|
88
|
+
const directTeardownFiredRef = useRef(false)
|
|
83
89
|
useEffect(() => {
|
|
84
90
|
directModeFlagRef.current = isDirectMode
|
|
85
91
|
}, [isDirectMode])
|
|
@@ -110,10 +116,14 @@ export function useRealtimeChunkProcessor(
|
|
|
110
116
|
if (action.action === 'direct_message') sawDirectMessageRef.current = true
|
|
111
117
|
|
|
112
118
|
if ((directModeFlagRef.current || sawDirectMessageRef.current) && !DIRECT_MODE_ALLOWED.has(action.action)) {
|
|
113
|
-
// Hard stop: tear down
|
|
114
|
-
// indicator and half-rendered bubble clear, then drop
|
|
115
|
-
|
|
119
|
+
// Hard stop: tear down AI activity exactly once so the typing
|
|
120
|
+
// indicator, composer lock, and half-rendered bubble clear, then drop
|
|
121
|
+
// the chunk. Fires even when no stream is open — an onAgentBusy lock
|
|
122
|
+
// (approved commands executing post-MESSAGE_END) has no other release
|
|
123
|
+
// once the barrier starts dropping the continuation chunks.
|
|
124
|
+
if (isInStreamRef.current || !directTeardownFiredRef.current) {
|
|
116
125
|
isInStreamRef.current = false
|
|
126
|
+
directTeardownFiredRef.current = true
|
|
117
127
|
callbacks.onStreamEnd?.()
|
|
118
128
|
accumulator.resetSegments()
|
|
119
129
|
}
|
|
@@ -164,6 +174,12 @@ export function useRealtimeChunkProcessor(
|
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
case 'tool_execution': {
|
|
177
|
+
// A starting tool run means the agent's turn is in progress even
|
|
178
|
+
// when this lands after MESSAGE_END (approved commands execute
|
|
179
|
+
// between the approval bubble and the continuation stream).
|
|
180
|
+
if (action.segment.data.type === 'EXECUTING_TOOL') {
|
|
181
|
+
callbacks.onAgentBusy?.()
|
|
182
|
+
}
|
|
167
183
|
// Post-MESSAGE_END tool chunks (cancellations / async batch
|
|
168
184
|
// results for a batch in a prior bubble) flow only through the
|
|
169
185
|
// cross-message updater. Skipping the accumulator avoids
|
|
@@ -254,6 +270,11 @@ export function useRealtimeChunkProcessor(
|
|
|
254
270
|
|
|
255
271
|
case 'approval_result': {
|
|
256
272
|
const { requestId, approved, approvalType, resolvedByName } = action
|
|
273
|
+
// Approved → the agent resumes to execute the command(s); surface
|
|
274
|
+
// busy immediately so the composer locks before EXECUTING_TOOL
|
|
275
|
+
// lands. Rejection keeps the input free — the user may want to
|
|
276
|
+
// type a correction right away.
|
|
277
|
+
if (approved) callbacks.onAgentBusy?.()
|
|
257
278
|
const escalatedData = pendingEscalatedRef.current.get(requestId)
|
|
258
279
|
const status: ChatApprovalStatus = approved ? 'approved' : 'rejected'
|
|
259
280
|
|
|
@@ -397,6 +418,7 @@ export function useRealtimeChunkProcessor(
|
|
|
397
418
|
pendingEscalatedRef.current.clear()
|
|
398
419
|
hasInitializedWithData.current = false
|
|
399
420
|
sawDirectMessageRef.current = false
|
|
421
|
+
directTeardownFiredRef.current = false
|
|
400
422
|
}, [])
|
|
401
423
|
|
|
402
424
|
const updateApprovalStatus = useCallback(
|
|
@@ -199,6 +199,16 @@ export interface RealtimeChunkCallbacks {
|
|
|
199
199
|
* outlived its message scope. Idempotent.
|
|
200
200
|
*/
|
|
201
201
|
onToolExecuted?: (segment: ToolExecutionSegment) => void
|
|
202
|
+
/**
|
|
203
|
+
* Called when a chunk implies the agent is actively working OUTSIDE an open
|
|
204
|
+
* MESSAGE_START/MESSAGE_END window: an `EXECUTING_TOOL` chunk, or an
|
|
205
|
+
* `APPROVAL_RESULT` that approved execution. Approved commands run between
|
|
206
|
+
* the approval bubble's MESSAGE_END and the continuation stream, so without
|
|
207
|
+
* this signal a phase keyed on stream boundaries reads as idle while tools
|
|
208
|
+
* execute. The busy window is closed by the continuation's MESSAGE_END, an
|
|
209
|
+
* ERROR chunk, or a user stop.
|
|
210
|
+
*/
|
|
211
|
+
onAgentBusy?: () => void
|
|
202
212
|
/** Called when a DIALOG_CLOSED chunk is received */
|
|
203
213
|
onDialogClosed?: () => void
|
|
204
214
|
}
|
package/src/components/tabs.tsx
CHANGED
|
@@ -5,8 +5,16 @@ import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
|
5
5
|
|
|
6
6
|
import { cn } from "../utils/cn"
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Use `TabNavigation` from `components/ui/tab-navigation` — the
|
|
10
|
+
* single canonical tab component (ODS tokens, URL-sync, overflow scroll
|
|
11
|
+
* shadows). This Radix-based `Tabs` set styles itself with legacy shadcn tokens
|
|
12
|
+
* (`bg-muted`, `text-foreground`, `ring-ring`) that don't exist in ODS and is
|
|
13
|
+
* kept only for un-migrated call sites. Do not use in new code.
|
|
14
|
+
*/
|
|
8
15
|
const Tabs = TabsPrimitive.Root
|
|
9
16
|
|
|
17
|
+
/** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */
|
|
10
18
|
const TabsList = React.forwardRef<
|
|
11
19
|
React.ElementRef<typeof TabsPrimitive.List>,
|
|
12
20
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
@@ -22,6 +30,7 @@ const TabsList = React.forwardRef<
|
|
|
22
30
|
))
|
|
23
31
|
TabsList.displayName = TabsPrimitive.List.displayName
|
|
24
32
|
|
|
33
|
+
/** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */
|
|
25
34
|
const TabsTrigger = React.forwardRef<
|
|
26
35
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
27
36
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
@@ -37,6 +46,7 @@ const TabsTrigger = React.forwardRef<
|
|
|
37
46
|
))
|
|
38
47
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
39
48
|
|
|
49
|
+
/** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */
|
|
40
50
|
const TabsContent = React.forwardRef<
|
|
41
51
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
42
52
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
@@ -17,11 +17,12 @@ import { Tag } from './tag'
|
|
|
17
17
|
export type DashboardInfoCardPercentageDisplay = 'auto' | 'plain' | 'tag'
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Size of the progress ring in px. A single number
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* Size of the progress ring in px. A single number sets the ring size from the
|
|
21
|
+
* Tailwind `md` (800px) breakpoint up — on mobile the ring always shrinks to
|
|
22
|
+
* the 24px Figma spec. The responsive form sets `base` (mobile) explicitly,
|
|
23
|
+
* plus optional `md` / `lg` (1280px) overrides — e.g. `{ base: 24, lg: 56 }`
|
|
24
|
+
* keeps the small ring through tablet, and `{ base: 40 }` pins 40px at every
|
|
25
|
+
* breakpoint.
|
|
25
26
|
*/
|
|
26
27
|
export type DashboardInfoCardProgressSize =
|
|
27
28
|
| number
|
|
@@ -31,6 +32,19 @@ export interface DashboardInfoCardProps {
|
|
|
31
32
|
title?: string
|
|
32
33
|
/** Node rendered in place of the title text. Takes precedence over `title`. */
|
|
33
34
|
titleSlot?: React.ReactNode
|
|
35
|
+
/**
|
|
36
|
+
* Tag rendered in the title row, after the title (Figma "status" variant) —
|
|
37
|
+
* e.g. `<Tag variant="outline" label="AI-Assistance" />` or a TicketStatusTag.
|
|
38
|
+
* Works with or without a `title`.
|
|
39
|
+
*/
|
|
40
|
+
titleTag?: React.ReactNode
|
|
41
|
+
/**
|
|
42
|
+
* Icon or image shown in a bordered square slot on the left (Figma "icon"
|
|
43
|
+
* variant): 32px box / 16px content on mobile, 56px box / 24px content from
|
|
44
|
+
* `md` up. The passed node is stretched to fill the content area, so plain
|
|
45
|
+
* icon components and `<img>` both work.
|
|
46
|
+
*/
|
|
47
|
+
icon?: React.ReactNode
|
|
34
48
|
value: string | number
|
|
35
49
|
percentage?: number
|
|
36
50
|
showProgress?: boolean
|
|
@@ -47,11 +61,11 @@ export interface DashboardInfoCardProps {
|
|
|
47
61
|
*/
|
|
48
62
|
progressOverflow?: CircularProgressOverflow
|
|
49
63
|
/**
|
|
50
|
-
* Size of the circular progress ring. Defaults to
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
64
|
+
* Size of the circular progress ring. Defaults to the Figma spec
|
|
65
|
+
* `{ base: 24, md: 56 }` — 24px on mobile, 56px on tablet and desktop. A
|
|
66
|
+
* number changes only the tablet/desktop size (mobile stays 24px); use
|
|
67
|
+
* `{ base, md?, lg? }` for full control. Stroke width scales proportionally
|
|
68
|
+
* with the size.
|
|
55
69
|
*/
|
|
56
70
|
progressSize?: DashboardInfoCardProgressSize
|
|
57
71
|
className?: string
|
|
@@ -62,7 +76,7 @@ export interface DashboardInfoCardProps {
|
|
|
62
76
|
href?: string
|
|
63
77
|
/** Tooltip content shown on a question-mark icon next to the value */
|
|
64
78
|
tooltip?: React.ReactNode
|
|
65
|
-
/** Override the value text className (default: text-h2) */
|
|
79
|
+
/** Override the value text className (default: `text-h3 md:text-h2` per Figma) */
|
|
66
80
|
valueClassName?: string
|
|
67
81
|
/** Secondary text rendered beside the value (e.g. an entry/item count). */
|
|
68
82
|
subValue?: React.ReactNode
|
|
@@ -71,6 +85,8 @@ export interface DashboardInfoCardProps {
|
|
|
71
85
|
export function DashboardInfoCard({
|
|
72
86
|
title,
|
|
73
87
|
titleSlot,
|
|
88
|
+
titleTag,
|
|
89
|
+
icon,
|
|
74
90
|
value,
|
|
75
91
|
percentage,
|
|
76
92
|
showProgress = false,
|
|
@@ -125,34 +141,26 @@ export function DashboardInfoCard({
|
|
|
125
141
|
showLabel: false,
|
|
126
142
|
}
|
|
127
143
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (typeof progressSize === 'number') {
|
|
134
|
-
return <CircularProgress {...common} size={progressSize} strokeWidth={strokeFor(progressSize)} />
|
|
135
|
-
}
|
|
144
|
+
// Figma spec: the mobile ring is always 24px. The default is 56px from
|
|
145
|
+
// `md` (800px) up; a numeric `progressSize` overrides only that md+ size.
|
|
146
|
+
const size = typeof progressSize === 'number'
|
|
147
|
+
? { base: 24, md: progressSize }
|
|
148
|
+
: progressSize ?? { base: 24, md: 56 }
|
|
136
149
|
|
|
137
|
-
// Responsive:
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
const { base, md, lg } =
|
|
150
|
+
// Responsive: one ring per provided breakpoint, toggled with Tailwind
|
|
151
|
+
// display utilities so only one shows at a time. The class names must stay
|
|
152
|
+
// static string literals — Tailwind's JIT scanner can't see
|
|
153
|
+
// dynamically-built names (e.g. `${bp}:hidden`).
|
|
154
|
+
const { base, md, lg } = size
|
|
142
155
|
|
|
143
|
-
const rings: Array<{ key: string; size: number; className: string }> = [
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
rings.push({ key: '
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
rings.push({ key: 'md', size: md, className: 'hidden md:block' })
|
|
151
|
-
} else if (lg !== undefined) {
|
|
152
|
-
rings.push({ key: 'base', size: base, className: 'lg:hidden' })
|
|
156
|
+
const rings: Array<{ key: string; size: number; className: string }> = [
|
|
157
|
+
{ key: 'base', size: base, className: md !== undefined ? 'md:hidden' : lg !== undefined ? 'lg:hidden' : '' },
|
|
158
|
+
]
|
|
159
|
+
if (md !== undefined) {
|
|
160
|
+
rings.push({ key: 'md', size: md, className: lg !== undefined ? 'hidden md:block lg:hidden' : 'hidden md:block' })
|
|
161
|
+
}
|
|
162
|
+
if (lg !== undefined) {
|
|
153
163
|
rings.push({ key: 'lg', size: lg, className: 'hidden lg:block' })
|
|
154
|
-
} else {
|
|
155
|
-
rings.push({ key: 'base', size: base, className: '' })
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
return (
|
|
@@ -172,18 +180,31 @@ export function DashboardInfoCard({
|
|
|
172
180
|
|
|
173
181
|
const cardContent = (
|
|
174
182
|
<>
|
|
183
|
+
{/* Icon slot (Figma "icon" variant): 32px box / 16px content on mobile,
|
|
184
|
+
56px box / 24px content from `md` up. */}
|
|
185
|
+
{icon && (
|
|
186
|
+
<div className="shrink-0 size-8 md:size-14 p-[var(--spacing-system-xxs)] bg-ods-bg border border-ods-border rounded-sm flex items-center justify-center text-ods-text-primary">
|
|
187
|
+
<span className="size-4 md:size-6 flex items-center justify-center [&>*]:size-full">
|
|
188
|
+
{icon}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
175
193
|
{/* Content section */}
|
|
176
|
-
<div className="flex-1 flex flex-col">
|
|
177
|
-
{/* Title */}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
<div className="flex-1 min-w-0 flex flex-col">
|
|
195
|
+
{/* Title row: caption and/or tag (Figma "status" variant) */}
|
|
196
|
+
<div className="flex items-center gap-[var(--spacing-system-xxs)]">
|
|
197
|
+
{titleSlot ?? (title !== undefined && (
|
|
198
|
+
<p className="text-h5 text-ods-text-secondary truncate">
|
|
199
|
+
{title}
|
|
200
|
+
</p>
|
|
201
|
+
))}
|
|
202
|
+
{titleTag}
|
|
203
|
+
</div>
|
|
183
204
|
|
|
184
205
|
{/* Value and percentage */}
|
|
185
206
|
<div className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
186
|
-
<p className={cn("text-h2 text-ods-text-primary", valueClassName)}>
|
|
207
|
+
<p className={cn("text-h3 md:text-h2 text-ods-text-primary truncate", valueClassName)}>
|
|
187
208
|
{formattedValue}
|
|
188
209
|
</p>
|
|
189
210
|
{subValue && (
|
|
@@ -207,7 +228,11 @@ export function DashboardInfoCard({
|
|
|
207
228
|
</>
|
|
208
229
|
)
|
|
209
230
|
|
|
210
|
-
|
|
231
|
+
// Figma spec: mobile 8px padding / 8px gap / exactly 64px tall; tablet+ 16px
|
|
232
|
+
// padding / 16px gap / exactly 104px tall; 6px radius. Fixed height (not
|
|
233
|
+
// min-h) — with a 32px titleTag the natural content height slightly exceeds
|
|
234
|
+
// the spec, and Figma's frame is fixed; items-center absorbs the difference.
|
|
235
|
+
const baseClassName = 'bg-ods-card border border-ods-border rounded-md p-[var(--spacing-system-xsf)] md:p-[var(--spacing-system-m)] h-16 md:h-[104px] flex gap-[var(--spacing-system-s)] md:gap-[var(--spacing-system-m)] items-center transition-all'
|
|
211
236
|
|
|
212
237
|
if (href) {
|
|
213
238
|
return (
|
|
@@ -228,9 +228,12 @@ export function TabNavigation({
|
|
|
228
228
|
"flex gap-[var(--spacing-system-xxs)] items-center justify-center p-[var(--spacing-system-m)] relative shrink-0 cursor-pointer",
|
|
229
229
|
"transition-all duration-200 bg-transparent border-none outline-none",
|
|
230
230
|
stretchTabs && 'flex-1',
|
|
231
|
+
// Known limitation: ODS color vars hold hex values, so Tailwind
|
|
232
|
+
// alpha modifiers (to-ods-accent/10) silently produce no CSS.
|
|
233
|
+
// color-mix() accepts hex, so we derive the 10%-alpha stop from the token.
|
|
231
234
|
isActive
|
|
232
|
-
? 'bg-gradient-to-b from-
|
|
233
|
-
: 'hover:bg-gradient-to-b hover:from-
|
|
235
|
+
? 'bg-gradient-to-b from-transparent to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]'
|
|
236
|
+
: 'hover:bg-gradient-to-b hover:from-transparent hover:to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]'
|
|
234
237
|
)}
|
|
235
238
|
>
|
|
236
239
|
{tab.icon ? (
|
|
@@ -5,8 +5,17 @@ import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
|
5
5
|
|
|
6
6
|
import { cn } from "../../utils/cn"
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Use `TabNavigation` from `components/ui/tab-navigation` — the
|
|
10
|
+
* single canonical tab component (ODS tokens, URL-sync, overflow scroll
|
|
11
|
+
* shadows). This Radix-based `Tabs` set (a duplicate of `components/tabs.tsx`)
|
|
12
|
+
* styles itself with legacy shadcn tokens (`bg-muted`, `text-foreground`,
|
|
13
|
+
* `ring-ring`) that don't exist in ODS and is kept only for un-migrated call
|
|
14
|
+
* sites. Do not use in new code.
|
|
15
|
+
*/
|
|
8
16
|
const Tabs = TabsPrimitive.Root
|
|
9
17
|
|
|
18
|
+
/** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */
|
|
10
19
|
const TabsList = React.forwardRef<
|
|
11
20
|
React.ElementRef<typeof TabsPrimitive.List>,
|
|
12
21
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
@@ -22,6 +31,7 @@ const TabsList = React.forwardRef<
|
|
|
22
31
|
))
|
|
23
32
|
TabsList.displayName = TabsPrimitive.List.displayName
|
|
24
33
|
|
|
34
|
+
/** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */
|
|
25
35
|
const TabsTrigger = React.forwardRef<
|
|
26
36
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
27
37
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
@@ -37,6 +47,7 @@ const TabsTrigger = React.forwardRef<
|
|
|
37
47
|
))
|
|
38
48
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
39
49
|
|
|
50
|
+
/** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */
|
|
40
51
|
const TabsContent = React.forwardRef<
|
|
41
52
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
42
53
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
@@ -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
|
+
};
|