@flamingo-stack/openframe-frontend-core 0.0.219 → 0.0.220-snapshot.20260602172647
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-F3FO2ZZZ.cjs → chunk-4PBV66HQ.cjs} +7 -7
- package/dist/{chunk-F3FO2ZZZ.cjs.map → chunk-4PBV66HQ.cjs.map} +1 -1
- package/dist/{chunk-EDW2NVRV.js → chunk-4WZOFD46.js} +37 -37
- package/dist/{chunk-EDW2NVRV.js.map → chunk-4WZOFD46.js.map} +1 -1
- package/dist/{chunk-YX3YQNC4.cjs → chunk-73YDB6AT.cjs} +13 -13
- package/dist/{chunk-YX3YQNC4.cjs.map → chunk-73YDB6AT.cjs.map} +1 -1
- package/dist/{chunk-MPHDM2VZ.cjs → chunk-7TQNW2AM.cjs} +30 -30
- package/dist/{chunk-MPHDM2VZ.cjs.map → chunk-7TQNW2AM.cjs.map} +1 -1
- package/dist/{chunk-65CPJ4SX.cjs → chunk-C6ASEPZL.cjs} +30 -30
- package/dist/{chunk-65CPJ4SX.cjs.map → chunk-C6ASEPZL.cjs.map} +1 -1
- package/dist/{chunk-DRPECAXO.js → chunk-CPIX5AAR.js} +2 -2
- package/dist/{chunk-SRA2QYK6.js → chunk-E2AWBQEU.js} +4 -4
- package/dist/{chunk-SZXKKEUH.cjs → chunk-E6B4B7GM.cjs} +46 -30
- package/dist/chunk-E6B4B7GM.cjs.map +1 -0
- package/dist/{chunk-XG7DFRJL.js → chunk-FOOQFRJR.js} +3 -3
- package/dist/{chunk-A3PL6ZCF.js → chunk-IS4IZC7N.js} +6388 -5128
- package/dist/chunk-IS4IZC7N.js.map +1 -0
- package/dist/{chunk-ZGBXHK26.cjs → chunk-JMGSJHFP.cjs} +12 -12
- package/dist/{chunk-ZGBXHK26.cjs.map → chunk-JMGSJHFP.cjs.map} +1 -1
- package/dist/{chunk-SL3RGBPX.cjs → chunk-QNYH3WUU.cjs} +9 -9
- package/dist/{chunk-SL3RGBPX.cjs.map → chunk-QNYH3WUU.cjs.map} +1 -1
- package/dist/{chunk-24Q2WLIU.js → chunk-QYRV6MKX.js} +2 -2
- package/dist/{chunk-ZII7TNVA.js → chunk-SRCEVQYA.js} +3 -3
- package/dist/{chunk-Y3MXGCOW.js → chunk-YZDUOUMB.js} +46 -30
- package/dist/chunk-YZDUOUMB.js.map +1 -0
- package/dist/{chunk-7UZLRI7W.cjs → chunk-ZAGQXSAP.cjs} +3292 -2032
- package/dist/chunk-ZAGQXSAP.cjs.map +1 -0
- package/dist/components/chat/chat-archive-page.d.ts +25 -0
- package/dist/components/chat/chat-archive-page.d.ts.map +1 -0
- package/dist/components/chat/chat-composer.d.ts +29 -0
- package/dist/components/chat/chat-composer.d.ts.map +1 -0
- package/dist/components/chat/chat-header-icon-button.d.ts +14 -0
- package/dist/components/chat/chat-header-icon-button.d.ts.map +1 -0
- package/dist/components/chat/chat-panel-header.d.ts +32 -0
- package/dist/components/chat/chat-panel-header.d.ts.map +1 -0
- package/dist/components/chat/embeddable-chat.d.ts +18 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/guide-mode-banner.d.ts +16 -0
- package/dist/components/chat/guide-mode-banner.d.ts.map +1 -0
- package/dist/components/chat/guide-welcome.d.ts +42 -0
- package/dist/components/chat/guide-welcome.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts +58 -0
- package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +26 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +29 -5
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +9 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +28 -4
- package/dist/components/chat/mingo-chat-history.d.ts +37 -0
- package/dist/components/chat/mingo-chat-history.d.ts.map +1 -0
- package/dist/components/chat/mingo-chat-modals.d.ts +50 -0
- package/dist/components/chat/mingo-chat-modals.d.ts.map +1 -0
- package/dist/components/chat/mingo-onboarding-card.d.ts.map +1 -1
- package/dist/components/chat/mingo-welcome.d.ts +78 -0
- package/dist/components/chat/mingo-welcome.d.ts.map +1 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts +6 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +6 -6
- package/dist/components/contact/index.js +5 -5
- package/dist/components/features/index.cjs +5 -5
- package/dist/components/features/index.js +4 -4
- package/dist/components/icons-v2-generated/brand-logos/fleet-mdm-logo-grey-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/fleet-mdm-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/index.cjs +2 -2
- package/dist/components/icons-v2-generated/index.js +1 -1
- package/dist/components/index.cjs +128 -104
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +31 -7
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-heading.d.ts +7 -6
- package/dist/components/layout/page-heading.d.ts.map +1 -1
- package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
- package/dist/components/navigation/header-mingo-button.d.ts +4 -2
- package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +5 -5
- package/dist/components/navigation/index.js +4 -4
- package/dist/components/onboarding-guides/index.cjs +28 -28
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/tickets/index.cjs +88 -88
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/dropdown-menu.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 +29 -5
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +28 -4
- package/dist/components/ui/modal-v2.d.ts.map +1 -1
- package/dist/components/ui/more-actions-menu.d.ts +8 -1
- package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
- package/dist/components/ui/portal-container.d.ts +21 -0
- package/dist/components/ui/portal-container.d.ts.map +1 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +29 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +28 -4
- package/package.json +1 -1
- package/src/components/chat/chat-archive-page.tsx +93 -0
- package/src/components/chat/chat-composer.tsx +99 -0
- package/src/components/chat/chat-header-icon-button.tsx +36 -0
- package/src/components/chat/chat-panel-header.tsx +114 -0
- package/src/components/chat/embeddable-chat.tsx +388 -311
- package/src/components/chat/guide-mode-banner.tsx +75 -0
- package/src/components/chat/guide-welcome.tsx +199 -0
- package/src/components/chat/hooks/use-chat-dialog-manager.ts +227 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +85 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +8 -0
- package/src/components/chat/hooks/use-unified-chat.ts +12 -0
- package/src/components/chat/index.ts +9 -0
- package/src/components/chat/mingo-chat-history.tsx +308 -0
- package/src/components/chat/mingo-chat-modals.tsx +223 -0
- package/src/components/chat/mingo-onboarding-card.tsx +5 -8
- package/src/components/chat/mingo-welcome.tsx +396 -0
- package/src/components/chat/types/unified-chat-state.types.ts +8 -0
- package/src/components/icons-v2/brand-logos/fleet-mdm-logo-grey.svg +6 -6
- package/src/components/icons-v2/brand-logos/fleet-mdm-logo.svg +6 -6
- package/src/components/icons-v2-generated/brand-logos/fleet-mdm-logo-grey-icon.tsx +2 -22
- package/src/components/icons-v2-generated/brand-logos/fleet-mdm-logo-icon.tsx +22 -2
- package/src/components/layout/page-heading.tsx +13 -7
- package/src/components/navigation/app-header.tsx +12 -12
- package/src/components/navigation/app-layout-drawer.tsx +25 -15
- package/src/components/navigation/header-mingo-button.tsx +22 -7
- package/src/components/ui/dropdown-menu.tsx +9 -3
- package/src/components/ui/modal-v2.tsx +33 -3
- package/src/components/ui/more-actions-menu.tsx +15 -2
- package/src/components/ui/portal-container.tsx +28 -0
- package/src/components/ui/tooltip.tsx +9 -3
- package/src/stories/AppLayoutSidebar.stories.tsx +184 -0
- package/src/stories/EmbeddableChat.stories.tsx +114 -0
- package/src/stories/GuideWelcome.stories.tsx +124 -0
- package/src/stories/MingoChatModals.stories.tsx +82 -0
- package/src/stories/MingoWelcome.stories.tsx +119 -0
- package/dist/chunk-7UZLRI7W.cjs.map +0 -1
- package/dist/chunk-A3PL6ZCF.js.map +0 -1
- package/dist/chunk-SZXKKEUH.cjs.map +0 -1
- package/dist/chunk-Y3MXGCOW.js.map +0 -1
- /package/dist/{chunk-DRPECAXO.js.map → chunk-CPIX5AAR.js.map} +0 -0
- /package/dist/{chunk-SRA2QYK6.js.map → chunk-E2AWBQEU.js.map} +0 -0
- /package/dist/{chunk-XG7DFRJL.js.map → chunk-FOOQFRJR.js.map} +0 -0
- /package/dist/{chunk-24Q2WLIU.js.map → chunk-QYRV6MKX.js.map} +0 -0
- /package/dist/{chunk-ZII7TNVA.js.map → chunk-SRCEVQYA.js.map} +0 -0
|
@@ -21,6 +21,15 @@ export * from './tool-execution-display'
|
|
|
21
21
|
export * from './tool-call-blocks'
|
|
22
22
|
export * from './mingo-onboarding-card'
|
|
23
23
|
export * from './mingo-onboarding-card-skeleton'
|
|
24
|
+
export * from './mingo-welcome'
|
|
25
|
+
export * from './guide-welcome'
|
|
26
|
+
export * from './guide-mode-banner'
|
|
27
|
+
export * from './mingo-chat-history'
|
|
28
|
+
export * from './mingo-chat-modals'
|
|
29
|
+
export * from './chat-header-icon-button'
|
|
30
|
+
export * from './chat-panel-header'
|
|
31
|
+
export * from './chat-composer'
|
|
32
|
+
export * from './chat-archive-page'
|
|
24
33
|
export * from './model-display'
|
|
25
34
|
export * from './chat-sidebar'
|
|
26
35
|
export type { ChatRef } from './chat-ref.types'
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
import { MoreActionsMenu } from '../ui/more-actions-menu'
|
|
6
|
+
import { Ellipsis01Icon } from '../icons-v2-generated'
|
|
7
|
+
import type { DialogItem } from './types/component.types'
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export interface MingoChatHistoryProps {
|
|
14
|
+
/** Dialogs to list, assumed already sorted newest-first by the host. */
|
|
15
|
+
dialogs: ReadonlyArray<DialogItem>
|
|
16
|
+
/** Currently-open dialog id (highlighted). */
|
|
17
|
+
activeDialogId?: string
|
|
18
|
+
/** Open a dialog. */
|
|
19
|
+
onSelectDialog?: (id: string) => void
|
|
20
|
+
/** Request rename — enables the row "Rename chat" action. The host opens
|
|
21
|
+
* the Rename modal; the list does no inline editing. */
|
|
22
|
+
onRequestRename?: (dialog: DialogItem) => void
|
|
23
|
+
/** Request archive — enables the row "Archive chat" action. The host opens
|
|
24
|
+
* the Archive confirmation modal. */
|
|
25
|
+
onRequestArchive?: (dialog: DialogItem) => void
|
|
26
|
+
/** Whether more dialogs remain (cursor pagination). */
|
|
27
|
+
hasMore?: boolean
|
|
28
|
+
/** True while the next page is loading. */
|
|
29
|
+
isLoadingMore?: boolean
|
|
30
|
+
/** Fetch the next page — fired when the bottom sentinel scrolls into view. */
|
|
31
|
+
onLoadMore?: () => void
|
|
32
|
+
/** Appended to the root element. */
|
|
33
|
+
className?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DialogGroup {
|
|
37
|
+
key: string
|
|
38
|
+
label: string
|
|
39
|
+
items: DialogItem[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Date grouping — Today / Yesterday / Older
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
function dialogTime(d: DialogItem): number | null {
|
|
47
|
+
if (!d.timestamp) return null
|
|
48
|
+
const t =
|
|
49
|
+
typeof d.timestamp === 'string'
|
|
50
|
+
? Date.parse(d.timestamp)
|
|
51
|
+
: d.timestamp.getTime()
|
|
52
|
+
return Number.isNaN(t) ? null : t
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function groupDialogs(dialogs: ReadonlyArray<DialogItem>): DialogGroup[] {
|
|
56
|
+
const now = new Date()
|
|
57
|
+
const startOfToday = new Date(
|
|
58
|
+
now.getFullYear(),
|
|
59
|
+
now.getMonth(),
|
|
60
|
+
now.getDate(),
|
|
61
|
+
).getTime()
|
|
62
|
+
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000
|
|
63
|
+
|
|
64
|
+
const today: DialogItem[] = []
|
|
65
|
+
const yesterday: DialogItem[] = []
|
|
66
|
+
const older: DialogItem[] = []
|
|
67
|
+
for (const d of dialogs) {
|
|
68
|
+
const t = dialogTime(d)
|
|
69
|
+
if (t !== null && t >= startOfToday) today.push(d)
|
|
70
|
+
else if (t !== null && t >= startOfYesterday) yesterday.push(d)
|
|
71
|
+
else older.push(d) // includes timestamp-less dialogs
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
{ key: 'today', label: 'Today', items: today },
|
|
76
|
+
{ key: 'yesterday', label: 'Yesterday', items: yesterday },
|
|
77
|
+
{ key: 'older', label: 'Older', items: older },
|
|
78
|
+
].filter((g) => g.items.length > 0)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// Row
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
interface RowProps {
|
|
86
|
+
dialog: DialogItem
|
|
87
|
+
isActive: boolean
|
|
88
|
+
onSelect?: (id: string) => void
|
|
89
|
+
onRequestRename?: (dialog: DialogItem) => void
|
|
90
|
+
onRequestArchive?: (dialog: DialogItem) => void
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function MingoChatHistoryRow({
|
|
94
|
+
dialog,
|
|
95
|
+
isActive,
|
|
96
|
+
onSelect,
|
|
97
|
+
onRequestRename,
|
|
98
|
+
onRequestArchive,
|
|
99
|
+
}: RowProps) {
|
|
100
|
+
const title = dialog.title || 'Untitled Chat'
|
|
101
|
+
const unread = dialog.unreadMessagesCount ?? 0
|
|
102
|
+
const hasMenu = !!onRequestRename || !!onRequestArchive
|
|
103
|
+
// Keep the `⋯` visible while its menu is open — once Radix moves focus into
|
|
104
|
+
// the portalled content the row loses hover/focus-within.
|
|
105
|
+
const [menuOpen, setMenuOpen] = React.useState(false)
|
|
106
|
+
|
|
107
|
+
const menuItems = [
|
|
108
|
+
onRequestRename && {
|
|
109
|
+
label: 'Rename chat',
|
|
110
|
+
onClick: () => onRequestRename(dialog),
|
|
111
|
+
},
|
|
112
|
+
onRequestArchive && {
|
|
113
|
+
label: 'Archive chat',
|
|
114
|
+
onClick: () => onRequestArchive(dialog),
|
|
115
|
+
},
|
|
116
|
+
].filter(Boolean) as { label: string; onClick: () => void }[]
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
role="button"
|
|
121
|
+
tabIndex={0}
|
|
122
|
+
onClick={() => onSelect?.(dialog.id)}
|
|
123
|
+
onKeyDown={(e) => {
|
|
124
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
125
|
+
e.preventDefault()
|
|
126
|
+
onSelect?.(dialog.id)
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
className={cn(
|
|
130
|
+
'group/row flex h-12 items-center gap-[var(--spacing-system-xsf)] px-[var(--spacing-system-s)]',
|
|
131
|
+
'bg-ods-bg border-b border-ods-border last:border-b-0',
|
|
132
|
+
'cursor-pointer transition-colors hover:bg-ods-bg-hover focus:outline-none focus-visible:bg-ods-bg-hover',
|
|
133
|
+
isActive && 'bg-ods-bg-hover',
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
{unread > 0 ? (
|
|
137
|
+
<span className="flex h-8 min-w-8 shrink-0 items-center justify-center rounded-md bg-ods-accent px-[var(--spacing-system-xsf)] text-h5 text-ods-text-on-accent">
|
|
138
|
+
{unread > 99 ? '99+' : unread}
|
|
139
|
+
</span>
|
|
140
|
+
) : null}
|
|
141
|
+
<span
|
|
142
|
+
className="min-w-0 flex-1 truncate text-h4 text-ods-text-primary"
|
|
143
|
+
title={title}
|
|
144
|
+
>
|
|
145
|
+
{title}
|
|
146
|
+
</span>
|
|
147
|
+
{hasMenu ? (
|
|
148
|
+
// Stop propagation so opening the menu doesn't also select the dialog.
|
|
149
|
+
// Hidden until the row is hovered/focused (or its menu is open).
|
|
150
|
+
<span
|
|
151
|
+
onClick={(e) => e.stopPropagation()}
|
|
152
|
+
className={cn(
|
|
153
|
+
'shrink-0 transition-opacity',
|
|
154
|
+
menuOpen
|
|
155
|
+
? 'opacity-100'
|
|
156
|
+
: 'opacity-0 group-hover/row:opacity-100 focus-within:opacity-100',
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
<MoreActionsMenu
|
|
160
|
+
ariaLabel="Chat actions"
|
|
161
|
+
items={menuItems}
|
|
162
|
+
open={menuOpen}
|
|
163
|
+
onOpenChange={setMenuOpen}
|
|
164
|
+
// Don't return focus (and its ring) to the `⋯` trigger on close.
|
|
165
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
166
|
+
trigger={
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
aria-label="Chat actions"
|
|
170
|
+
className="flex size-6 items-center justify-center rounded-md text-ods-text-secondary transition-colors hover:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
|
|
171
|
+
>
|
|
172
|
+
<Ellipsis01Icon size={24} />
|
|
173
|
+
</button>
|
|
174
|
+
}
|
|
175
|
+
/>
|
|
176
|
+
</span>
|
|
177
|
+
) : null}
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// Component
|
|
184
|
+
// =============================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* MingoChatHistory — Figma node `7532:223950`.
|
|
188
|
+
*
|
|
189
|
+
* The returning-user variation of the Mingo empty state: the dialog history
|
|
190
|
+
* rendered inline in the main panel, grouped into Today / Yesterday / Older
|
|
191
|
+
* sections. Each row shows an optional unread badge, the title, and a `⋯`
|
|
192
|
+
* menu (Rename / Archive — each gated on the matching handler and surfaced as
|
|
193
|
+
* a request the host fulfils via a modal). Owns its own scroll + bottom/top
|
|
194
|
+
* fade and an infinite-scroll
|
|
195
|
+
* sentinel that fires `onLoadMore`.
|
|
196
|
+
*/
|
|
197
|
+
export function MingoChatHistory({
|
|
198
|
+
dialogs,
|
|
199
|
+
activeDialogId,
|
|
200
|
+
onSelectDialog,
|
|
201
|
+
onRequestRename,
|
|
202
|
+
onRequestArchive,
|
|
203
|
+
hasMore = false,
|
|
204
|
+
isLoadingMore = false,
|
|
205
|
+
onLoadMore,
|
|
206
|
+
className,
|
|
207
|
+
}: MingoChatHistoryProps) {
|
|
208
|
+
const groups = React.useMemo(() => groupDialogs(dialogs), [dialogs])
|
|
209
|
+
|
|
210
|
+
// Scroll-fade affordances (same pattern as MingoWelcome's greeting region).
|
|
211
|
+
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
212
|
+
const [fade, setFade] = React.useState({ top: false, bottom: false })
|
|
213
|
+
const updateFade = React.useCallback(() => {
|
|
214
|
+
const el = scrollRef.current
|
|
215
|
+
if (!el) return
|
|
216
|
+
const top = el.scrollTop > 1
|
|
217
|
+
const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1
|
|
218
|
+
setFade((p) => (p.top === top && p.bottom === bottom ? p : { top, bottom }))
|
|
219
|
+
}, [])
|
|
220
|
+
React.useEffect(() => {
|
|
221
|
+
updateFade()
|
|
222
|
+
const el = scrollRef.current
|
|
223
|
+
if (!el || typeof ResizeObserver === 'undefined') return
|
|
224
|
+
const ro = new ResizeObserver(updateFade)
|
|
225
|
+
ro.observe(el)
|
|
226
|
+
return () => ro.disconnect()
|
|
227
|
+
}, [updateFade, dialogs])
|
|
228
|
+
|
|
229
|
+
// Infinite scroll — load the next page when the sentinel enters the
|
|
230
|
+
// scroll viewport.
|
|
231
|
+
const sentinelRef = React.useRef<HTMLDivElement>(null)
|
|
232
|
+
const onLoadMoreRef = React.useRef(onLoadMore)
|
|
233
|
+
onLoadMoreRef.current = onLoadMore
|
|
234
|
+
const isLoadingMoreRef = React.useRef(isLoadingMore)
|
|
235
|
+
isLoadingMoreRef.current = isLoadingMore
|
|
236
|
+
React.useEffect(() => {
|
|
237
|
+
const root = scrollRef.current
|
|
238
|
+
const sentinel = sentinelRef.current
|
|
239
|
+
if (!root || !sentinel || !hasMore || typeof IntersectionObserver === 'undefined')
|
|
240
|
+
return
|
|
241
|
+
const io = new IntersectionObserver(
|
|
242
|
+
([entry]) => {
|
|
243
|
+
if (entry.isIntersecting && !isLoadingMoreRef.current) {
|
|
244
|
+
onLoadMoreRef.current?.()
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{ root, rootMargin: '120px', threshold: 0.1 },
|
|
248
|
+
)
|
|
249
|
+
io.observe(sentinel)
|
|
250
|
+
return () => io.disconnect()
|
|
251
|
+
}, [hasMore])
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div className={cn('relative flex flex-1 min-h-0 flex-col', className)}>
|
|
255
|
+
<div
|
|
256
|
+
ref={scrollRef}
|
|
257
|
+
onScroll={updateFade}
|
|
258
|
+
className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
|
|
259
|
+
>
|
|
260
|
+
{groups.map((group) => (
|
|
261
|
+
<div
|
|
262
|
+
key={group.key}
|
|
263
|
+
className="flex flex-col gap-[var(--spacing-system-xxs)]"
|
|
264
|
+
>
|
|
265
|
+
<p className="text-h5 text-ods-text-secondary">{group.label}</p>
|
|
266
|
+
<div className="overflow-hidden rounded-md border border-ods-border">
|
|
267
|
+
{group.items.map((dialog) => (
|
|
268
|
+
<MingoChatHistoryRow
|
|
269
|
+
key={dialog.id}
|
|
270
|
+
dialog={dialog}
|
|
271
|
+
isActive={dialog.id === activeDialogId}
|
|
272
|
+
onSelect={onSelectDialog}
|
|
273
|
+
onRequestRename={onRequestRename}
|
|
274
|
+
onRequestArchive={onRequestArchive}
|
|
275
|
+
/>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
))}
|
|
280
|
+
{hasMore ? <div ref={sentinelRef} className="h-px shrink-0" /> : null}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Scroll-fade — only while content is hidden in that direction. */}
|
|
284
|
+
<div
|
|
285
|
+
aria-hidden
|
|
286
|
+
className={cn(
|
|
287
|
+
'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
|
|
288
|
+
fade.top ? 'opacity-100' : 'opacity-0',
|
|
289
|
+
)}
|
|
290
|
+
style={{
|
|
291
|
+
background:
|
|
292
|
+
'linear-gradient(0deg, transparent 0%, var(--color-bg-card) 100%)',
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
<div
|
|
296
|
+
aria-hidden
|
|
297
|
+
className={cn(
|
|
298
|
+
'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
|
|
299
|
+
fade.bottom ? 'opacity-100' : 'opacity-0',
|
|
300
|
+
)}
|
|
301
|
+
style={{
|
|
302
|
+
background:
|
|
303
|
+
'linear-gradient(180deg, transparent 0%, var(--color-bg-card) 100%)',
|
|
304
|
+
}}
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
import {
|
|
6
|
+
ModalV2,
|
|
7
|
+
ModalV2Header,
|
|
8
|
+
ModalV2Title,
|
|
9
|
+
ModalV2Footer,
|
|
10
|
+
} from '../ui/modal-v2'
|
|
11
|
+
import type { DialogItem } from './types/component.types'
|
|
12
|
+
|
|
13
|
+
// Shared button styling for the modal footers (Figma `button-full`):
|
|
14
|
+
// full-width, `text-h3` (DM Sans Bold 18px), 12/16 padding, rounded-md.
|
|
15
|
+
const footerBtn =
|
|
16
|
+
'flex-1 min-w-0 rounded-md px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)] text-h3 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent disabled:opacity-50 disabled:cursor-not-allowed'
|
|
17
|
+
const cancelBtn = cn(
|
|
18
|
+
footerBtn,
|
|
19
|
+
'border border-ods-border bg-transparent text-ods-text-primary hover:bg-ods-bg-hover',
|
|
20
|
+
)
|
|
21
|
+
const saveBtn = cn(
|
|
22
|
+
footerBtn,
|
|
23
|
+
'bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover',
|
|
24
|
+
)
|
|
25
|
+
const dangerBtn = cn(
|
|
26
|
+
footerBtn,
|
|
27
|
+
'bg-ods-error text-ods-text-on-accent hover:opacity-90',
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Rename
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export interface RenameChatModalProps {
|
|
35
|
+
isOpen: boolean
|
|
36
|
+
/** Current chat name, used to seed the input each time the modal opens. */
|
|
37
|
+
initialName?: string
|
|
38
|
+
onClose: () => void
|
|
39
|
+
/** Fired with the trimmed new name when Save is pressed. */
|
|
40
|
+
onSave: (name: string) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Rename Chat modal — Figma node `7592:225962`. */
|
|
44
|
+
export function RenameChatModal({
|
|
45
|
+
isOpen,
|
|
46
|
+
initialName = '',
|
|
47
|
+
onClose,
|
|
48
|
+
onSave,
|
|
49
|
+
}: RenameChatModalProps) {
|
|
50
|
+
const [name, setName] = React.useState(initialName)
|
|
51
|
+
// Reseed whenever the modal (re)opens — possibly for a different chat.
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
if (isOpen) setName(initialName)
|
|
54
|
+
}, [isOpen, initialName])
|
|
55
|
+
|
|
56
|
+
const canSave = name.trim().length > 0
|
|
57
|
+
const save = () => {
|
|
58
|
+
if (canSave) onSave(name.trim())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ModalV2 isOpen={isOpen} onClose={onClose} className="md:max-w-[600px]">
|
|
63
|
+
<ModalV2Header>
|
|
64
|
+
<ModalV2Title>Rename Chat</ModalV2Title>
|
|
65
|
+
</ModalV2Header>
|
|
66
|
+
<div className="flex w-full flex-col gap-[var(--spacing-system-xxs)]">
|
|
67
|
+
<label htmlFor="rename-chat-input" className="text-h4 text-ods-text-primary">
|
|
68
|
+
Chat Name
|
|
69
|
+
</label>
|
|
70
|
+
<input
|
|
71
|
+
id="rename-chat-input"
|
|
72
|
+
autoFocus
|
|
73
|
+
value={name}
|
|
74
|
+
onChange={(e) => setName(e.target.value)}
|
|
75
|
+
onKeyDown={(e) => {
|
|
76
|
+
if (e.key === 'Enter') save()
|
|
77
|
+
}}
|
|
78
|
+
className="w-full rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-sf)] text-h4 text-ods-text-primary focus:outline-none focus-visible:border-ods-accent"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<ModalV2Footer>
|
|
82
|
+
<button type="button" onClick={onClose} className={cancelBtn}>
|
|
83
|
+
Cancel
|
|
84
|
+
</button>
|
|
85
|
+
<button type="button" onClick={save} disabled={!canSave} className={saveBtn}>
|
|
86
|
+
Save
|
|
87
|
+
</button>
|
|
88
|
+
</ModalV2Footer>
|
|
89
|
+
</ModalV2>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Archive
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
export interface ArchiveChatModalProps {
|
|
98
|
+
isOpen: boolean
|
|
99
|
+
onClose: () => void
|
|
100
|
+
/** Fired when the destructive "Archive Chat" button is pressed. */
|
|
101
|
+
onConfirm: () => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Archive Chat confirmation modal — Figma node `7592:226181`. */
|
|
105
|
+
export function ArchiveChatModal({
|
|
106
|
+
isOpen,
|
|
107
|
+
onClose,
|
|
108
|
+
onConfirm,
|
|
109
|
+
}: ArchiveChatModalProps) {
|
|
110
|
+
return (
|
|
111
|
+
<ModalV2 isOpen={isOpen} onClose={onClose} className="md:max-w-[600px]">
|
|
112
|
+
<ModalV2Header>
|
|
113
|
+
<ModalV2Title>Archive Chat</ModalV2Title>
|
|
114
|
+
</ModalV2Header>
|
|
115
|
+
<p className="w-full text-h4 text-ods-text-primary">
|
|
116
|
+
This chat will be hidden from your current chats.
|
|
117
|
+
</p>
|
|
118
|
+
<ModalV2Footer>
|
|
119
|
+
<button type="button" onClick={onClose} className={cancelBtn}>
|
|
120
|
+
Cancel
|
|
121
|
+
</button>
|
|
122
|
+
<button type="button" onClick={onConfirm} className={dangerBtn}>
|
|
123
|
+
Archive Chat
|
|
124
|
+
</button>
|
|
125
|
+
</ModalV2Footer>
|
|
126
|
+
</ModalV2>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// Unarchive
|
|
132
|
+
// =============================================================================
|
|
133
|
+
|
|
134
|
+
export interface UnarchiveChatModalProps {
|
|
135
|
+
isOpen: boolean
|
|
136
|
+
onClose: () => void
|
|
137
|
+
/** Fired when the "Unarchive Chat" button is pressed. */
|
|
138
|
+
onConfirm: () => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Unarchive (restore) Chat confirmation modal — restores an archived chat
|
|
142
|
+
* back to the current chats so the user can continue it. */
|
|
143
|
+
export function UnarchiveChatModal({
|
|
144
|
+
isOpen,
|
|
145
|
+
onClose,
|
|
146
|
+
onConfirm,
|
|
147
|
+
}: UnarchiveChatModalProps) {
|
|
148
|
+
return (
|
|
149
|
+
<ModalV2 isOpen={isOpen} onClose={onClose} className="md:max-w-[600px]">
|
|
150
|
+
<ModalV2Header>
|
|
151
|
+
<ModalV2Title>Unarchive Chat</ModalV2Title>
|
|
152
|
+
</ModalV2Header>
|
|
153
|
+
<p className="w-full text-h4 text-ods-text-primary">
|
|
154
|
+
This chat will be moved back to your current chats.
|
|
155
|
+
</p>
|
|
156
|
+
<ModalV2Footer>
|
|
157
|
+
<button type="button" onClick={onClose} className={cancelBtn}>
|
|
158
|
+
Cancel
|
|
159
|
+
</button>
|
|
160
|
+
<button type="button" onClick={onConfirm} className={saveBtn}>
|
|
161
|
+
Unarchive Chat
|
|
162
|
+
</button>
|
|
163
|
+
</ModalV2Footer>
|
|
164
|
+
</ModalV2>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Composite — all three dialog-action modals
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
export interface ChatDialogModalsProps {
|
|
173
|
+
/** Dialog pending rename (`null` = closed). */
|
|
174
|
+
renameTarget: DialogItem | null
|
|
175
|
+
setRenameTarget: (dialog: DialogItem | null) => void
|
|
176
|
+
onConfirmRename: (name: string) => void
|
|
177
|
+
/** Dialog pending archive (`null` = closed). */
|
|
178
|
+
archiveTarget: DialogItem | null
|
|
179
|
+
setArchiveTarget: (dialog: DialogItem | null) => void
|
|
180
|
+
onConfirmArchive: () => void
|
|
181
|
+
/** Dialog pending restore/unarchive (`null` = closed). */
|
|
182
|
+
restoreTarget: DialogItem | null
|
|
183
|
+
setRestoreTarget: (dialog: DialogItem | null) => void
|
|
184
|
+
onConfirmRestore: () => void
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Renders the Rename / Archive / Unarchive modals together, each driven by its
|
|
189
|
+
* target dialog. Pair with `useChatDialogManager`, which produces exactly this
|
|
190
|
+
* prop shape.
|
|
191
|
+
*/
|
|
192
|
+
export function ChatDialogModals({
|
|
193
|
+
renameTarget,
|
|
194
|
+
setRenameTarget,
|
|
195
|
+
onConfirmRename,
|
|
196
|
+
archiveTarget,
|
|
197
|
+
setArchiveTarget,
|
|
198
|
+
onConfirmArchive,
|
|
199
|
+
restoreTarget,
|
|
200
|
+
setRestoreTarget,
|
|
201
|
+
onConfirmRestore,
|
|
202
|
+
}: ChatDialogModalsProps) {
|
|
203
|
+
return (
|
|
204
|
+
<>
|
|
205
|
+
<RenameChatModal
|
|
206
|
+
isOpen={renameTarget != null}
|
|
207
|
+
initialName={renameTarget?.title ?? ''}
|
|
208
|
+
onClose={() => setRenameTarget(null)}
|
|
209
|
+
onSave={onConfirmRename}
|
|
210
|
+
/>
|
|
211
|
+
<ArchiveChatModal
|
|
212
|
+
isOpen={archiveTarget != null}
|
|
213
|
+
onClose={() => setArchiveTarget(null)}
|
|
214
|
+
onConfirm={onConfirmArchive}
|
|
215
|
+
/>
|
|
216
|
+
<UnarchiveChatModal
|
|
217
|
+
isOpen={restoreTarget != null}
|
|
218
|
+
onClose={() => setRestoreTarget(null)}
|
|
219
|
+
onConfirm={onConfirmRestore}
|
|
220
|
+
/>
|
|
221
|
+
</>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { cn } from '../../utils/cn'
|
|
5
|
+
import { Button } from '../ui/button'
|
|
5
6
|
|
|
6
7
|
export interface MingoOnboardingCardAction {
|
|
7
8
|
/** Stable React key. */
|
|
@@ -91,22 +92,18 @@ export function MingoOnboardingCard({
|
|
|
91
92
|
{hasActions ? (
|
|
92
93
|
<div className="flex flex-wrap items-center gap-[var(--spacing-system-xxs)] mt-[var(--spacing-system-xs)]">
|
|
93
94
|
{actions!.map((action) => (
|
|
94
|
-
<
|
|
95
|
+
<Button
|
|
95
96
|
key={action.id}
|
|
96
97
|
type="button"
|
|
98
|
+
variant="outline"
|
|
99
|
+
size="small"
|
|
97
100
|
onClick={(e) => {
|
|
98
101
|
e.stopPropagation()
|
|
99
102
|
action.onClick(e)
|
|
100
103
|
}}
|
|
101
|
-
className={cn(
|
|
102
|
-
'inline-flex h-7 items-center justify-center px-[var(--spacing-system-xs)] rounded-md',
|
|
103
|
-
'border border-ods-border bg-transparent text-h6 text-ods-text-primary',
|
|
104
|
-
'transition-colors hover:bg-ods-bg-hover hover:border-ods-text-secondary',
|
|
105
|
-
'focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',
|
|
106
|
-
)}
|
|
107
104
|
>
|
|
108
105
|
{action.label}
|
|
109
|
-
</
|
|
106
|
+
</Button>
|
|
110
107
|
))}
|
|
111
108
|
</div>
|
|
112
109
|
) : null}
|