@flamingo-stack/openframe-frontend-core 0.0.428 → 0.0.429-snapshot.20260715222543
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-43KGFHI3.js → chunk-2ELINEWQ.js} +2 -2
- package/dist/{chunk-OZTPC6VW.cjs → chunk-374KFDET.cjs} +31 -31
- package/dist/{chunk-OZTPC6VW.cjs.map → chunk-374KFDET.cjs.map} +1 -1
- package/dist/{chunk-55EIHSOQ.js → chunk-3AWPRVFM.js} +2 -2
- package/dist/{chunk-SLCANQN3.js → chunk-3OKVH6FW.js} +4 -4
- package/dist/{chunk-UXXCNZXC.js → chunk-3XWBE3SM.js} +4 -4
- package/dist/{chunk-7JUUQAAH.js → chunk-67LZDLIO.js} +2 -2
- package/dist/{chunk-YP3DO4BM.js → chunk-A6JKKKIF.js} +4 -4
- package/dist/{chunk-RR77Z27A.js → chunk-AY5TBDWZ.js} +2 -2
- package/dist/{chunk-LZYKGEN3.cjs → chunk-CJOFXDTK.cjs} +26 -26
- package/dist/{chunk-LZYKGEN3.cjs.map → chunk-CJOFXDTK.cjs.map} +1 -1
- package/dist/{chunk-I7YFFSDS.cjs → chunk-CTY37UUO.cjs} +59 -59
- package/dist/{chunk-I7YFFSDS.cjs.map → chunk-CTY37UUO.cjs.map} +1 -1
- package/dist/{chunk-NXCFIPEY.cjs → chunk-D4K53CES.cjs} +26 -26
- package/dist/{chunk-NXCFIPEY.cjs.map → chunk-D4K53CES.cjs.map} +1 -1
- package/dist/{chunk-FFDGTMZX.cjs → chunk-FL4IOCYH.cjs} +5 -5
- package/dist/{chunk-FFDGTMZX.cjs.map → chunk-FL4IOCYH.cjs.map} +1 -1
- package/dist/{chunk-7ELFQWYF.cjs → chunk-FYMD7CCA.cjs} +7 -7
- package/dist/{chunk-7ELFQWYF.cjs.map → chunk-FYMD7CCA.cjs.map} +1 -1
- package/dist/{chunk-CVYVCBMM.js → chunk-IMOJWQDR.js} +4 -4
- package/dist/{chunk-N2CTZ3Q2.cjs → chunk-KDVGC6EG.cjs} +37 -37
- package/dist/{chunk-N2CTZ3Q2.cjs.map → chunk-KDVGC6EG.cjs.map} +1 -1
- package/dist/{chunk-DA2VSDAX.js → chunk-NZBQGODM.js} +2 -2
- package/dist/{chunk-SJTNVETH.cjs → chunk-PP5KNHMK.cjs} +408 -233
- package/dist/chunk-PP5KNHMK.cjs.map +1 -0
- package/dist/{chunk-MJH4VS4L.cjs → chunk-Q7SYPXUX.cjs} +11 -11
- package/dist/{chunk-MJH4VS4L.cjs.map → chunk-Q7SYPXUX.cjs.map} +1 -1
- package/dist/{chunk-DQ5NRLCG.js → chunk-S7UXICHB.js} +2 -2
- package/dist/{chunk-KMXHSL6M.js → chunk-TDITU4KW.js} +2 -2
- package/dist/{chunk-OBDDE4WV.js → chunk-TYANQCZA.js} +3034 -2859
- package/dist/chunk-TYANQCZA.js.map +1 -0
- package/dist/{chunk-F4WTCWOY.cjs → chunk-WQN4B6CM.cjs} +15 -15
- package/dist/{chunk-F4WTCWOY.cjs.map → chunk-WQN4B6CM.cjs.map} +1 -1
- package/dist/{chunk-533XR4JK.cjs → chunk-WQR7H56P.cjs} +62 -62
- package/dist/{chunk-533XR4JK.cjs.map → chunk-WQR7H56P.cjs.map} +1 -1
- package/dist/{chunk-MO4QIPT7.cjs → chunk-ZUP2ZKS6.cjs} +9 -9
- package/dist/{chunk-MO4QIPT7.cjs.map → chunk-ZUP2ZKS6.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-message-skeleton.d.ts +17 -2
- package/dist/components/chat/chat-message-skeleton.d.ts.map +1 -1
- package/dist/components/chat/chat-panel-header.d.ts +6 -1
- package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
- package/dist/components/chat/chat-quick-action-row.d.ts +12 -2
- package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +1 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +31 -2
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-scripted-stream.d.ts +3 -0
- package/dist/components/chat/hooks/use-scripted-stream.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +14 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +13 -1
- package/dist/components/chat/quick-action-chip.d.ts +5 -1
- package/dist/components/chat/quick-action-chip.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +1 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/scripted-stream.d.ts +38 -0
- package/dist/components/chat/utils/scripted-stream.d.ts.map +1 -0
- 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/board/index.d.ts +2 -2
- package/dist/components/features/board/index.d.ts.map +1 -1
- package/dist/components/features/board/ticket-card.d.ts +22 -2
- package/dist/components/features/board/ticket-card.d.ts.map +1 -1
- package/dist/components/features/index.cjs +6 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +5 -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 +146 -128
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +27 -9
- package/dist/components/index.js.map +1 -1
- 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/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +16 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +15 -1
- package/dist/components/ui/info-card-row.d.ts +13 -0
- package/dist/components/ui/info-card-row.d.ts.map +1 -0
- package/dist/components/ui/tag.d.ts +1 -1
- package/dist/components/ui/tag.d.ts.map +1 -1
- package/dist/index.cjs +20 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -1
- package/package.json +1 -1
- package/src/components/chat/chat-message-skeleton.tsx +40 -9
- package/src/components/chat/chat-panel-header.tsx +28 -16
- package/src/components/chat/chat-quick-action-row.tsx +33 -0
- package/src/components/chat/embeddable-chat.tsx +11 -1
- package/src/components/chat/hooks/index.ts +1 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +30 -1
- package/src/components/chat/hooks/use-scripted-stream.ts +69 -0
- package/src/components/chat/quick-action-chip.tsx +6 -1
- package/src/components/chat/utils/index.ts +2 -0
- package/src/components/chat/utils/scripted-stream.ts +111 -0
- package/src/components/features/board/index.ts +2 -2
- package/src/components/features/board/ticket-card.tsx +108 -64
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/info-card-row.tsx +70 -0
- package/src/components/ui/tag.tsx +7 -0
- package/src/stories/InfoCardRow.stories.tsx +68 -0
- package/dist/chunk-OBDDE4WV.js.map +0 -1
- package/dist/chunk-SJTNVETH.cjs.map +0 -1
- /package/dist/{chunk-43KGFHI3.js.map → chunk-2ELINEWQ.js.map} +0 -0
- /package/dist/{chunk-55EIHSOQ.js.map → chunk-3AWPRVFM.js.map} +0 -0
- /package/dist/{chunk-SLCANQN3.js.map → chunk-3OKVH6FW.js.map} +0 -0
- /package/dist/{chunk-UXXCNZXC.js.map → chunk-3XWBE3SM.js.map} +0 -0
- /package/dist/{chunk-7JUUQAAH.js.map → chunk-67LZDLIO.js.map} +0 -0
- /package/dist/{chunk-YP3DO4BM.js.map → chunk-A6JKKKIF.js.map} +0 -0
- /package/dist/{chunk-RR77Z27A.js.map → chunk-AY5TBDWZ.js.map} +0 -0
- /package/dist/{chunk-CVYVCBMM.js.map → chunk-IMOJWQDR.js.map} +0 -0
- /package/dist/{chunk-DA2VSDAX.js.map → chunk-NZBQGODM.js.map} +0 -0
- /package/dist/{chunk-DQ5NRLCG.js.map → chunk-S7UXICHB.js.map} +0 -0
- /package/dist/{chunk-KMXHSL6M.js.map → chunk-TDITU4KW.js.map} +0 -0
|
@@ -6,8 +6,18 @@ interface ChatMessageSkeletonProps {
|
|
|
6
6
|
className?: string
|
|
7
7
|
showAvatar?: boolean
|
|
8
8
|
isUser?: boolean
|
|
9
|
+
/** How many body lines to draw under the name row. Default 3 (paragraph-ish
|
|
10
|
+
* reply). Pass 1 for surfaces whose real messages are single-line (e.g. the
|
|
11
|
+
* short scripted hero-demo turns), so the skeleton isn't ~3x too tall. */
|
|
12
|
+
bodyLines?: number
|
|
9
13
|
}
|
|
10
14
|
|
|
15
|
+
// Body-line width palette: full-width lines then a short trailing line, so any
|
|
16
|
+
// `bodyLines` count ends on a short line like a real wrapped message.
|
|
17
|
+
const BODY_LINE_WIDTHS = ["w-full", "w-11/12", "w-4/5", "w-5/6"]
|
|
18
|
+
const bodyLineWidth = (index: number, total: number) =>
|
|
19
|
+
index === total - 1 ? "w-3/5" : BODY_LINE_WIDTHS[index % BODY_LINE_WIDTHS.length]
|
|
20
|
+
|
|
11
21
|
/**
|
|
12
22
|
* One message-row skeleton. Mirrors `ChatMessageEnhanced`'s real layout:
|
|
13
23
|
* full panel width, no card background, and an INLINE avatar in the name row
|
|
@@ -19,7 +29,9 @@ export function ChatMessageSkeleton({
|
|
|
19
29
|
className,
|
|
20
30
|
showAvatar = true,
|
|
21
31
|
isUser = false,
|
|
32
|
+
bodyLines = 3,
|
|
22
33
|
}: ChatMessageSkeletonProps) {
|
|
34
|
+
const lines = Math.max(1, bodyLines)
|
|
23
35
|
return (
|
|
24
36
|
<div className={cn("relative py-[var(--spacing-system-s)]", className)}>
|
|
25
37
|
<div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
|
|
@@ -37,9 +49,12 @@ export function ChatMessageSkeleton({
|
|
|
37
49
|
|
|
38
50
|
{/* Body lines — percentage widths so they reflow on panel resize. */}
|
|
39
51
|
<div className="flex flex-col gap-2 pt-[var(--spacing-system-xxs)]">
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
{Array.from({ length: lines }, (_, i) => (
|
|
53
|
+
<div
|
|
54
|
+
key={i}
|
|
55
|
+
className={cn("h-4 rounded bg-ods-border animate-pulse", bodyLineWidth(i, lines))}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
43
58
|
</div>
|
|
44
59
|
</div>
|
|
45
60
|
</div>
|
|
@@ -61,6 +76,17 @@ interface ChatMessageListSkeletonProps {
|
|
|
61
76
|
* host wrapper is expected to own the panel padding; this stays empty by
|
|
62
77
|
* default so the skeleton sits flush. */
|
|
63
78
|
contentClassName?: string
|
|
79
|
+
/** Body lines per message row (forwarded to `ChatMessageSkeleton`). Default 3;
|
|
80
|
+
* pass 1 for single-line message surfaces. */
|
|
81
|
+
bodyLines?: number
|
|
82
|
+
/**
|
|
83
|
+
* FILL mode: top-anchor the rows and CLIP overflow (instead of bottom-anchoring
|
|
84
|
+
* + inner scroll). Combined with a generous `messageCount`, the rows cover the
|
|
85
|
+
* container top-to-bottom at ANY height — no per-container measuring, no empty
|
|
86
|
+
* band at the top. Use for loading surfaces that should read as "the whole
|
|
87
|
+
* panel is loading" rather than "a short thread pinned to the bottom".
|
|
88
|
+
*/
|
|
89
|
+
fill?: boolean
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
export function ChatMessageListSkeleton({
|
|
@@ -69,6 +95,8 @@ export function ChatMessageListSkeleton({
|
|
|
69
95
|
showAvatars = true,
|
|
70
96
|
fullWidth = false,
|
|
71
97
|
contentClassName,
|
|
98
|
+
bodyLines,
|
|
99
|
+
fill = false,
|
|
72
100
|
}: ChatMessageListSkeletonProps) {
|
|
73
101
|
const messages = Array.from({ length: messageCount }, (_, index) => ({
|
|
74
102
|
id: index,
|
|
@@ -79,9 +107,10 @@ export function ChatMessageListSkeleton({
|
|
|
79
107
|
<div className="relative flex-1 min-h-0 flex flex-col">
|
|
80
108
|
<div
|
|
81
109
|
className={cn(
|
|
82
|
-
"flex h-full w-full flex-col
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
"flex h-full w-full flex-col flex-1",
|
|
111
|
+
fill
|
|
112
|
+
? "overflow-hidden"
|
|
113
|
+
: "overflow-y-auto overflow-x-hidden [scroll-behavior:smooth] scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
|
|
85
114
|
className,
|
|
86
115
|
)}
|
|
87
116
|
>
|
|
@@ -92,15 +121,17 @@ export function ChatMessageListSkeleton({
|
|
|
92
121
|
: "mx-auto flex w-full max-w-ods-content-narrow flex-col min-w-0",
|
|
93
122
|
contentClassName,
|
|
94
123
|
)}
|
|
95
|
-
style={{ minHeight: '100%' }}
|
|
124
|
+
style={fill ? undefined : { minHeight: '100%' }}
|
|
96
125
|
>
|
|
97
|
-
{/* Bottom-anchor the rows like the real list
|
|
98
|
-
|
|
126
|
+
{/* Bottom-anchor the rows like the real list — unless FILL mode wants
|
|
127
|
+
them top-anchored + clipped to cover the whole panel. */}
|
|
128
|
+
{!fill && <div className="flex-1" />}
|
|
99
129
|
{messages.map((message) => (
|
|
100
130
|
<ChatMessageSkeleton
|
|
101
131
|
key={message.id}
|
|
102
132
|
showAvatar={showAvatars}
|
|
103
133
|
isUser={message.isUser}
|
|
134
|
+
bodyLines={bodyLines}
|
|
104
135
|
/>
|
|
105
136
|
))}
|
|
106
137
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
4
5
|
import { Chevron01LeftIcon } from '../icons-v2-generated/arrows/chevron-01-left-icon'
|
|
5
6
|
import {
|
|
6
7
|
Ellipsis01Icon,
|
|
@@ -42,6 +43,11 @@ export interface ChatPanelHeaderProps {
|
|
|
42
43
|
onArchive?: () => void
|
|
43
44
|
/** Open the Chat Archive page — renders the clock button (list view only). */
|
|
44
45
|
onOpenArchive?: () => void
|
|
46
|
+
/** Force the COMPACT (`h-14` desktop-style) bar at every width, skipping the
|
|
47
|
+
* full-screen mobile header. For EMBEDDED surfaces (e.g. a small preview
|
|
48
|
+
* panel) where the phone-sized full-bleed header (large `text-h2` title) is
|
|
49
|
+
* wrong regardless of viewport. Default false = viewport-responsive. */
|
|
50
|
+
compact?: boolean
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/**
|
|
@@ -61,6 +67,7 @@ export function ChatPanelHeader({
|
|
|
61
67
|
onRename,
|
|
62
68
|
onArchive,
|
|
63
69
|
onOpenArchive,
|
|
70
|
+
compact = false,
|
|
64
71
|
}: ChatPanelHeaderProps) {
|
|
65
72
|
// Desktop ⋯ menu (active, non-archived conversation only) — rename / archive.
|
|
66
73
|
const menuItems = [
|
|
@@ -70,23 +77,28 @@ export function ChatPanelHeader({
|
|
|
70
77
|
|
|
71
78
|
return (
|
|
72
79
|
<>
|
|
73
|
-
{/* Mobile (<md): a distinct layout (Figma node 7363:85532).
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
{/* Mobile (<md): a distinct full-screen layout (Figma node 7363:85532).
|
|
81
|
+
Skipped in `compact` mode — an embedded preview keeps the small bar
|
|
82
|
+
at every width. */}
|
|
83
|
+
{!compact && (
|
|
84
|
+
<ChatPanelHeaderMobile
|
|
85
|
+
className="flex md:hidden"
|
|
86
|
+
showBack={showBack}
|
|
87
|
+
title={title}
|
|
88
|
+
backAriaLabel={backAriaLabel}
|
|
89
|
+
isArchivedView={isArchivedView}
|
|
90
|
+
onBack={onBack}
|
|
91
|
+
onClose={onClose}
|
|
92
|
+
onRestore={onRestore}
|
|
93
|
+
onRename={onRename}
|
|
94
|
+
onArchive={onArchive}
|
|
95
|
+
onOpenArchive={onOpenArchive}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
87
98
|
|
|
88
|
-
{/*
|
|
89
|
-
|
|
99
|
+
{/* Compact bar: `md+` normally, or ALL widths when `compact`. Fixed-height
|
|
100
|
+
bar with full-height divider action cells. */}
|
|
101
|
+
<div className={cn('flex-shrink-0 h-14 w-full overflow-hidden border-b border-ods-border bg-ods-card', compact ? 'flex' : 'hidden md:flex')}>
|
|
90
102
|
<div className="flex flex-1 min-w-0 items-center gap-2 px-4 py-3">
|
|
91
103
|
{showBack ? (
|
|
92
104
|
<>
|
|
@@ -9,8 +9,10 @@ import { Ellipsis01Icon } from '../icons-v2-generated'
|
|
|
9
9
|
import { useAutoLimitTags } from '../../hooks/ui/use-auto-limit-tags'
|
|
10
10
|
import {
|
|
11
11
|
QuickActionChipButton,
|
|
12
|
+
QuickActionChipSkeleton,
|
|
12
13
|
renderQuickActionIcon,
|
|
13
14
|
type QuickActionIconSpec,
|
|
15
|
+
type QuickActionAccent,
|
|
14
16
|
} from './quick-action-chip'
|
|
15
17
|
|
|
16
18
|
// =============================================================================
|
|
@@ -27,6 +29,11 @@ export interface QuickActionChip {
|
|
|
27
29
|
icon?: React.ReactNode | QuickActionIconSpec
|
|
28
30
|
/** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */
|
|
29
31
|
variant?: 'primary' | 'outline'
|
|
32
|
+
/** Active single-select state — renders the accented `selected` skin
|
|
33
|
+
* (overrides `variant`). */
|
|
34
|
+
selected?: boolean
|
|
35
|
+
/** Accent for the `selected` skin (`'cyan'` = cyan twin, else pink). */
|
|
36
|
+
selectedAccent?: QuickActionAccent
|
|
30
37
|
onSelect?: () => void
|
|
31
38
|
/** Pointer/keyboard focus enters the chip — e.g. preview the full prompt in
|
|
32
39
|
* the composer. */
|
|
@@ -47,9 +54,18 @@ export interface ChatQuickActionRowProps {
|
|
|
47
54
|
* for roomy contexts (e.g. the Guide empty state) where overflow-hiding chips
|
|
48
55
|
* would defeat per-chip hover/preview. */
|
|
49
56
|
wrap?: boolean
|
|
57
|
+
/** Render `skeletonCount` loading chips (the real {@link QuickActionChipSkeleton},
|
|
58
|
+
* so geometry is 1:1 with the loaded chips) instead of `chips`. Wrap-aware. */
|
|
59
|
+
loading?: boolean
|
|
60
|
+
/** How many skeleton chips to draw when `loading`. Default 8. */
|
|
61
|
+
skeletonCount?: number
|
|
50
62
|
className?: string
|
|
51
63
|
}
|
|
52
64
|
|
|
65
|
+
// Deterministic label widths (in `ch`) so a loading wall reads like a populated
|
|
66
|
+
// one — a varied spread, not a uniform block.
|
|
67
|
+
const SKELETON_LABEL_CHS = [16, 9, 13, 7, 18, 8, 20, 11, 15, 10, 19, 12, 17, 8, 21, 14]
|
|
68
|
+
|
|
53
69
|
// =============================================================================
|
|
54
70
|
// Chip button
|
|
55
71
|
// =============================================================================
|
|
@@ -62,6 +78,8 @@ function ChipButton({ chip }: { chip: QuickActionChip }) {
|
|
|
62
78
|
label={chip.label}
|
|
63
79
|
icon={chip.icon}
|
|
64
80
|
variant={chip.variant ?? 'outline'}
|
|
81
|
+
selected={chip.selected}
|
|
82
|
+
selectedAccent={chip.selectedAccent}
|
|
65
83
|
onSelect={chip.onSelect}
|
|
66
84
|
onHoverStart={chip.onHoverStart}
|
|
67
85
|
onHoverEnd={chip.onHoverEnd}
|
|
@@ -86,6 +104,8 @@ export function ChatQuickActionRow({
|
|
|
86
104
|
leading,
|
|
87
105
|
chips,
|
|
88
106
|
wrap = false,
|
|
107
|
+
loading = false,
|
|
108
|
+
skeletonCount = 8,
|
|
89
109
|
className,
|
|
90
110
|
}: ChatQuickActionRowProps) {
|
|
91
111
|
const auto = useAutoLimitTags({
|
|
@@ -96,6 +116,19 @@ export function ChatQuickActionRow({
|
|
|
96
116
|
const visibleChips = chips.slice(0, auto.visibleCount)
|
|
97
117
|
const hiddenChips = chips.slice(auto.visibleCount)
|
|
98
118
|
|
|
119
|
+
// Loading: draw skeleton chips (real `QuickActionChipSkeleton`, 1:1 geometry)
|
|
120
|
+
// in the same wrap/inline layout — one shared skeleton for every consumer, no
|
|
121
|
+
// hand-rolled bars.
|
|
122
|
+
if (loading) {
|
|
123
|
+
return (
|
|
124
|
+
<div className={cn('flex shrink-0 items-center gap-1', wrap && 'flex-wrap', className)}>
|
|
125
|
+
{Array.from({ length: skeletonCount }, (_, i) => (
|
|
126
|
+
<QuickActionChipSkeleton key={i} labelCh={SKELETON_LABEL_CHS[i % SKELETON_LABEL_CHS.length]} />
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
if (!leading && chips.length === 0) return null
|
|
100
133
|
|
|
101
134
|
// Wrap mode: no width-measurement / "⋯" overflow — every chip renders inline
|
|
@@ -1557,7 +1557,13 @@ function EmbeddableChatInner({
|
|
|
1557
1557
|
// history has loaded (`hasMessages`). Driving the surface + content branch
|
|
1558
1558
|
// off this makes the normal-chat open animate identically to the archived
|
|
1559
1559
|
// one instead of lagging behind the message fetch.
|
|
1560
|
-
|
|
1560
|
+
// In `previewMode`, the host drives the whole state (`mingoStateOverride`) and
|
|
1561
|
+
// there is no dialog manager to set `isOpeningDialog`. So an embedded preview
|
|
1562
|
+
// that wants a message-list skeleton (real header + composer, skeleton bubbles)
|
|
1563
|
+
// signals it via `isMessagesLoading` — treat that as an open conversation so
|
|
1564
|
+
// the content branch shows the skeleton instead of the new-user welcome.
|
|
1565
|
+
const hasConversation =
|
|
1566
|
+
hasMessages || isOpeningDialog || isViewingArchived || (previewMode && isMessagesLoading)
|
|
1561
1567
|
// Opening a dialog whose history hasn't arrived yet — show a message-list
|
|
1562
1568
|
// skeleton instead of an empty thread so the open reads as "loading" rather
|
|
1563
1569
|
// than a blank flash before the bubbles stream in.
|
|
@@ -1680,6 +1686,10 @@ function EmbeddableChatInner({
|
|
|
1680
1686
|
className="flex flex-1 min-h-0 flex-col animate-in fade-in-0 duration-200"
|
|
1681
1687
|
>
|
|
1682
1688
|
<ChatPanelHeader
|
|
1689
|
+
// Embedded previews (the hero demo tabs) keep the small bar at
|
|
1690
|
+
// every width — the phone-sized full-screen header (large title)
|
|
1691
|
+
// is wrong in a small embedded panel.
|
|
1692
|
+
compact={previewMode}
|
|
1683
1693
|
// Guide-mode empty state shows a back-chevron + "Mingo Guide"
|
|
1684
1694
|
// (back returns to the default Mingo welcome); an open
|
|
1685
1695
|
// conversation shows back + the dialog title; the Mingo list
|
|
@@ -74,6 +74,7 @@ import type {
|
|
|
74
74
|
ChunkData,
|
|
75
75
|
FetchChunksFunction,
|
|
76
76
|
HistoricalMessage,
|
|
77
|
+
MessageProcessingOptions,
|
|
77
78
|
MessageSegment,
|
|
78
79
|
NatsMessageType,
|
|
79
80
|
StreamingPhase,
|
|
@@ -374,8 +375,14 @@ function updateTrailingAssistant(
|
|
|
374
375
|
* here because the unified contract surfaces errors as banners, not
|
|
375
376
|
* inline messages. (Hosts that need inline error bubbles can extend
|
|
376
377
|
* the contract later.)
|
|
378
|
+
*
|
|
379
|
+
* Exported for host reuse (e.g. scripted marketing demos that rehydrate a
|
|
380
|
+
* stored `HistoricalMessage[]` via `processHistoricalMessagesWithErrors` and
|
|
381
|
+
* feed a `previewMode` EmbeddableChat). This mapper carries the segment/content
|
|
382
|
+
* shape only; author identity (name/avatar/authorType/timestamp) that a host
|
|
383
|
+
* needs for the demo is re-attached by the host from the same processed rows.
|
|
377
384
|
*/
|
|
378
|
-
function mapProcessedToUnified(
|
|
385
|
+
export function mapProcessedToUnified(
|
|
379
386
|
processed: Array<{
|
|
380
387
|
id: string
|
|
381
388
|
role: 'user' | 'assistant' | 'error'
|
|
@@ -404,6 +411,28 @@ function mapProcessedToUnified(
|
|
|
404
411
|
return out
|
|
405
412
|
}
|
|
406
413
|
|
|
414
|
+
/**
|
|
415
|
+
* ONE-CALL `HistoricalMessage[]` -> `UnifiedChatMessage[]` for hosts replaying a
|
|
416
|
+
* stored conversation outside the live adapter (scripted marketing demos, a
|
|
417
|
+
* `previewMode` EmbeddableChat). Does the full pipeline the adapter does inline:
|
|
418
|
+
* `processHistoricalMessagesWithErrors` -> `mapProcessedToUnified` -> re-attach
|
|
419
|
+
* the author identity (name / avatar / authorType / timestamp) the segment
|
|
420
|
+
* mapper drops. Hosts no longer hand-roll the two-step + re-attach.
|
|
421
|
+
*/
|
|
422
|
+
export function historicalToUnified(
|
|
423
|
+
messages: HistoricalMessage[],
|
|
424
|
+
options: MessageProcessingOptions = {},
|
|
425
|
+
): UnifiedChatMessage[] {
|
|
426
|
+
const { messages: processed } = processHistoricalMessagesWithErrors(messages, options)
|
|
427
|
+
const byId = new Map(processed.map((p) => [p.id, p]))
|
|
428
|
+
return mapProcessedToUnified(processed).map((u) => {
|
|
429
|
+
const p = byId.get(u.id)
|
|
430
|
+
return p
|
|
431
|
+
? { ...u, name: p.name, avatar: p.avatar ?? null, authorType: p.authorType, timestamp: p.timestamp }
|
|
432
|
+
: u
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
|
|
407
436
|
// =============================================================================
|
|
408
437
|
// Hook
|
|
409
438
|
// =============================================================================
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Playback clock for {@link buildStreamFrames}: advance through pre-built
|
|
5
|
+
* scripted frames like a live chat stream — each frame holds for its own
|
|
6
|
+
* `delayMs`, then the next reveals, parking on the last. Surface-agnostic: the
|
|
7
|
+
* caller maps `frame.messages` to its own chat shape and reads `frame.phase` /
|
|
8
|
+
* `frame.typing`.
|
|
9
|
+
*
|
|
10
|
+
* `resetKey` restarts playback from the top when a NEW conversation is selected
|
|
11
|
+
* (identity change) — NOT when the SAME conversation grows (e.g. an Approve
|
|
12
|
+
* appends its continuation), which must resume from where it paused. Because the
|
|
13
|
+
* frame list's shared prefix is deterministic, keeping the index across a grow
|
|
14
|
+
* resumes seamlessly into the appended frames.
|
|
15
|
+
*
|
|
16
|
+
* `prefers-reduced-motion` (or `enabled === false`) jumps straight to the final
|
|
17
|
+
* frame (everything revealed, idle).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useEffect, useState } from 'react'
|
|
21
|
+
import type { StreamFrame } from '../utils/scripted-stream'
|
|
22
|
+
|
|
23
|
+
const EMPTY_FRAME: StreamFrame = { messages: [], phase: 'idle', typing: false, delayMs: 0 }
|
|
24
|
+
|
|
25
|
+
function prefersReducedMotion(): boolean {
|
|
26
|
+
return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useScriptedStream(frames: StreamFrame[], resetKey: unknown, enabled = true): StreamFrame {
|
|
30
|
+
const [index, setIndex] = useState(0)
|
|
31
|
+
const [reducedMotion, setReducedMotion] = useState(false)
|
|
32
|
+
|
|
33
|
+
// Restart from the top on a new conversation, synchronously during render
|
|
34
|
+
// (React's "adjust state on prop change" pattern) so the previous stream's
|
|
35
|
+
// last frame never flashes before the reset effect runs.
|
|
36
|
+
const [prevKey, setPrevKey] = useState(resetKey)
|
|
37
|
+
if (resetKey !== prevKey) {
|
|
38
|
+
setPrevKey(resetKey)
|
|
39
|
+
setIndex(0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setReducedMotion(prefersReducedMotion())
|
|
44
|
+
const mq = window.matchMedia?.('(prefers-reduced-motion: reduce)')
|
|
45
|
+
if (!mq) return
|
|
46
|
+
const onChange = () => setReducedMotion(mq.matches)
|
|
47
|
+
mq.addEventListener('change', onChange)
|
|
48
|
+
return () => mq.removeEventListener('change', onChange)
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
const playing = enabled && !reducedMotion && frames.length > 0
|
|
52
|
+
const lastIndex = Math.max(0, frames.length - 1)
|
|
53
|
+
|
|
54
|
+
// Advance one frame after the current frame's hold. When `frames` grows (an
|
|
55
|
+
// Approve appends more), this effect re-runs and, if we were parked on the old
|
|
56
|
+
// last frame, schedules the next — resuming the stream. `frames[index]` is
|
|
57
|
+
// clamped because a shrink (a shorter new conversation before the reset lands)
|
|
58
|
+
// could momentarily leave `index` past the end.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!playing || index >= lastIndex) return
|
|
61
|
+
const current = frames[Math.min(index, lastIndex)] ?? EMPTY_FRAME
|
|
62
|
+
const t = setTimeout(() => setIndex((i) => Math.min(i + 1, lastIndex)), current.delayMs)
|
|
63
|
+
return () => clearTimeout(t)
|
|
64
|
+
}, [index, frames, playing, lastIndex])
|
|
65
|
+
|
|
66
|
+
if (frames.length === 0) return EMPTY_FRAME
|
|
67
|
+
if (!playing) return frames[lastIndex]
|
|
68
|
+
return frames[Math.min(index, lastIndex)] ?? EMPTY_FRAME
|
|
69
|
+
}
|
|
@@ -145,6 +145,10 @@ export interface QuickActionChipButtonProps {
|
|
|
145
145
|
* Tag's `selected` variant (pink border + pink-secondary fill), overriding
|
|
146
146
|
* `variant`. Used by chip groups acting as tabs (OpenFrame categories). */
|
|
147
147
|
selected?: boolean
|
|
148
|
+
/** Accent for the `selected` skin: `'cyan'` uses the cyan twin, anything else
|
|
149
|
+
* (default) uses pink. Lets an agent's chip group match its own theme accent
|
|
150
|
+
* (fae pink / mingo cyan) instead of the fixed pink. */
|
|
151
|
+
selectedAccent?: QuickActionAccent
|
|
148
152
|
/** Chip scale, forwarded to `Tag` — `'large'` is the Figma "Feature Item"
|
|
149
153
|
* 48px chip (h3 bold label, 24px icon box). Default `'default'` (32px). */
|
|
150
154
|
size?: 'default' | 'large'
|
|
@@ -226,6 +230,7 @@ export function QuickActionChipButton({
|
|
|
226
230
|
lozenge,
|
|
227
231
|
variant = 'outline',
|
|
228
232
|
selected = false,
|
|
233
|
+
selectedAccent,
|
|
229
234
|
size = 'default',
|
|
230
235
|
onSelect,
|
|
231
236
|
onHoverStart,
|
|
@@ -235,7 +240,7 @@ export function QuickActionChipButton({
|
|
|
235
240
|
}: QuickActionChipButtonProps) {
|
|
236
241
|
const resolvedIcon = renderQuickActionIcon(icon)
|
|
237
242
|
const resolvedLabel = composeChipLabel(label, lozenge)
|
|
238
|
-
const tagVariant = selected ? 'selected' : variant
|
|
243
|
+
const tagVariant = selected ? (selectedAccent === 'cyan' ? 'selectedCyan' : 'selected') : variant
|
|
239
244
|
if (!interactive) {
|
|
240
245
|
return <Tag variant={tagVariant} size={size} icon={resolvedIcon} label={resolvedLabel} className={className} />
|
|
241
246
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripted-conversation replay — turn a stored `HistoricalMessage[]` into ordered
|
|
3
|
+
* "mock SSE" playback frames that animate a chat surface exactly like the live
|
|
4
|
+
* NATS/SSE stream (a `thinking` pause → assistant text token-by-token → tool /
|
|
5
|
+
* approval segments popping in → `idle`), just replayed from a script instead of
|
|
6
|
+
* a network stream.
|
|
7
|
+
*
|
|
8
|
+
* Lives in the lib (not a host) so any consumer — marketing hero demos, docs,
|
|
9
|
+
* onboarding tours — can drive a `previewMode` `EmbeddableChat` or a
|
|
10
|
+
* `ChatMessageList` from a canned conversation without re-implementing the
|
|
11
|
+
* streaming cadence. Pair with {@link useScriptedStream} (the playback clock).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { HistoricalMessage, MessageData, StreamingPhase } from '../types'
|
|
15
|
+
|
|
16
|
+
/** Normalize the single-or-array `messageData` to an array (empty when absent). */
|
|
17
|
+
export function normalizeHistoricalMessageData(md: HistoricalMessage['messageData']): MessageData[] {
|
|
18
|
+
return Array.isArray(md) ? md : md ? [md] : []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A message the user authored carries an `owner`; assistant turns have none —
|
|
22
|
+
* drives whether a turn streams (assistant) or lands at once (user). */
|
|
23
|
+
export function isHistoricalUserTurn(m: HistoricalMessage): boolean {
|
|
24
|
+
return !!m.owner
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isTextSegment = (d: MessageData): d is MessageData & { type: 'TEXT'; text: string } =>
|
|
28
|
+
(d as { type?: string })?.type === 'TEXT' && typeof (d as { text?: unknown }).text === 'string'
|
|
29
|
+
|
|
30
|
+
/** One rendered step of the mocked stream. */
|
|
31
|
+
export interface StreamFrame {
|
|
32
|
+
/** The partial message list to show at this step. */
|
|
33
|
+
messages: HistoricalMessage[]
|
|
34
|
+
phase: StreamingPhase
|
|
35
|
+
/** The chat "typing" indicator (up during the `thinking` pause). */
|
|
36
|
+
typing: boolean
|
|
37
|
+
/** Hold time (ms) before advancing to the next frame. */
|
|
38
|
+
delayMs: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Playback cadence. Assistant turns "think" then stream token-by-token; user
|
|
42
|
+
// turns land at once. Tuned to read like a real chat without dragging.
|
|
43
|
+
const THINK_MS = 550
|
|
44
|
+
const TOKEN_MS = 55 // per word revealed while streaming text
|
|
45
|
+
const USER_MS = 480 // dwell on a user turn before the reply starts thinking
|
|
46
|
+
const SEGMENT_MS = 340 // a tool / approval card pops in
|
|
47
|
+
const POST_MS = 240 // dwell after a completed assistant turn
|
|
48
|
+
|
|
49
|
+
/** Split text into tokens that KEEP surrounding whitespace, so re-joining a
|
|
50
|
+
* prefix reproduces the original spacing exactly (`"a b"` → `["a ", "b"]`). */
|
|
51
|
+
function tokenize(text: string): string[] {
|
|
52
|
+
return text.match(/\s*\S+\s*/g) ?? (text ? [text] : [])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Expand a message list into ordered playback frames reproducing the live
|
|
57
|
+
* stream: each user turn lands whole; each assistant turn first shows a
|
|
58
|
+
* `thinking` indicator, then streams its text token-by-token, then pops any
|
|
59
|
+
* tool / approval segments. Frame COUNT for a given prefix is deterministic, so
|
|
60
|
+
* a host that appends a continuation (e.g. after an Approve) resumes seamlessly
|
|
61
|
+
* from where it paused — the shared prefix's frames are unchanged.
|
|
62
|
+
*/
|
|
63
|
+
export function buildStreamFrames(messages: HistoricalMessage[]): StreamFrame[] {
|
|
64
|
+
const frames: StreamFrame[] = []
|
|
65
|
+
const revealed: HistoricalMessage[] = []
|
|
66
|
+
const push = (msgs: HistoricalMessage[], phase: StreamingPhase, typing: boolean, delayMs: number) =>
|
|
67
|
+
frames.push({ messages: msgs.slice(), phase, typing, delayMs })
|
|
68
|
+
|
|
69
|
+
messages.forEach((msg) => {
|
|
70
|
+
if (isHistoricalUserTurn(msg)) {
|
|
71
|
+
revealed.push(msg)
|
|
72
|
+
push(revealed, 'idle', false, USER_MS)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Assistant turn: a `thinking` pause, then stream this turn's segments in
|
|
77
|
+
// order. The pause is emitted ONLY when there is already content to show
|
|
78
|
+
// (`revealed.length > 0`) — an empty first frame would hand the surface a
|
|
79
|
+
// zero-message list, flashing its empty state before the opening line.
|
|
80
|
+
if (revealed.length > 0) push(revealed, 'thinking', true, THINK_MS)
|
|
81
|
+
|
|
82
|
+
const segs = normalizeHistoricalMessageData(msg.messageData)
|
|
83
|
+
const built: MessageData[] = []
|
|
84
|
+
const emit = () => push([...revealed, { ...msg, messageData: [...built] }], 'streaming', false, 0)
|
|
85
|
+
|
|
86
|
+
segs.forEach((seg) => {
|
|
87
|
+
if (isTextSegment(seg)) {
|
|
88
|
+
const tokens = tokenize(seg.text)
|
|
89
|
+
let acc = ''
|
|
90
|
+
built.push({ ...seg, text: '' })
|
|
91
|
+
tokens.forEach((tok) => {
|
|
92
|
+
acc += tok
|
|
93
|
+
built[built.length - 1] = { ...seg, text: acc }
|
|
94
|
+
push([...revealed, { ...msg, messageData: [...built] }], 'streaming', false, TOKEN_MS)
|
|
95
|
+
})
|
|
96
|
+
built[built.length - 1] = { ...seg }
|
|
97
|
+
} else {
|
|
98
|
+
// Tool / approval segment: pops in fully.
|
|
99
|
+
built.push(seg)
|
|
100
|
+
emit()
|
|
101
|
+
frames[frames.length - 1].delayMs = SEGMENT_MS
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
revealed.push(msg)
|
|
106
|
+
push(revealed, 'idle', false, POST_MS)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
if (frames.length === 0) push(revealed, 'idle', false, 0)
|
|
110
|
+
return frames
|
|
111
|
+
}
|
|
@@ -6,8 +6,8 @@ export { BoardColumn } from './board-column'
|
|
|
6
6
|
export type { BoardColumnProps } from './board-column'
|
|
7
7
|
export { BoardColumnHeader } from './board-column-header'
|
|
8
8
|
export type { BoardColumnHeaderProps } from './board-column-header'
|
|
9
|
-
export { TicketCard } from './ticket-card'
|
|
10
|
-
export type { TicketCardProps } from './ticket-card'
|
|
9
|
+
export { TicketCard, TicketCardView, TicketCardBody } from './ticket-card'
|
|
10
|
+
export type { TicketCardProps, TicketCardViewProps, TicketCardBodyProps } from './ticket-card'
|
|
11
11
|
export { BoardTicketApproval } from './board-ticket-approval'
|
|
12
12
|
export type { BoardTicketApprovalProps } from './board-ticket-approval'
|
|
13
13
|
export { TicketCardSkeleton } from './ticket-card-skeleton'
|