@flamingo-stack/openframe-frontend-core 0.0.438 → 0.0.439
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-LR6A67KZ.js → chunk-2ELBNZJ3.js} +2 -2
- package/dist/{chunk-6R5SZPAQ.cjs → chunk-3WJQL4DN.cjs} +15 -15
- package/dist/{chunk-6R5SZPAQ.cjs.map → chunk-3WJQL4DN.cjs.map} +1 -1
- package/dist/{chunk-KBFQF4AV.js → chunk-7WM77CSW.js} +2 -2
- package/dist/{chunk-B5SGPIJ4.cjs → chunk-AUJXLVXA.cjs} +37 -37
- package/dist/{chunk-B5SGPIJ4.cjs.map → chunk-AUJXLVXA.cjs.map} +1 -1
- package/dist/{chunk-X66KRL5G.cjs → chunk-C3FH53AP.cjs} +11 -11
- package/dist/{chunk-X66KRL5G.cjs.map → chunk-C3FH53AP.cjs.map} +1 -1
- package/dist/{chunk-W2PRXXUD.js → chunk-COMBYYQY.js} +2 -2
- package/dist/{chunk-RTHOCS3S.js → chunk-EGRZHR7J.js} +2 -2
- package/dist/{chunk-D5RTZ2WN.js → chunk-F7YVI54P.js} +4 -4
- package/dist/{chunk-5D5SJOLE.js → chunk-G5FALXNO.js} +4 -4
- package/dist/{chunk-2DCP2TIF.cjs → chunk-HTVVXVJL.cjs} +1966 -1490
- package/dist/chunk-HTVVXVJL.cjs.map +1 -0
- package/dist/{chunk-WF7VGEYR.cjs → chunk-IAN7GUIV.cjs} +26 -26
- package/dist/{chunk-WF7VGEYR.cjs.map → chunk-IAN7GUIV.cjs.map} +1 -1
- package/dist/{chunk-SYTL2VIU.cjs → chunk-M3LD6DNE.cjs} +7 -7
- package/dist/{chunk-SYTL2VIU.cjs.map → chunk-M3LD6DNE.cjs.map} +1 -1
- package/dist/{chunk-YKP2F2NC.cjs → chunk-M6QGB6DC.cjs} +26 -26
- package/dist/{chunk-YKP2F2NC.cjs.map → chunk-M6QGB6DC.cjs.map} +1 -1
- package/dist/{chunk-6ETVJ5ON.js → chunk-NQQ4NLUT.js} +6577 -6101
- package/dist/chunk-NQQ4NLUT.js.map +1 -0
- package/dist/{chunk-A25RPWYF.js → chunk-P7PISHM5.js} +2 -2
- package/dist/{chunk-44HNH57W.js → chunk-PBTY7AH5.js} +4 -4
- package/dist/{chunk-ZEY7A3MP.js → chunk-PEIR7D25.js} +2 -2
- package/dist/{chunk-DTYPYYKD.cjs → chunk-R46QEE3R.cjs} +59 -59
- package/dist/{chunk-DTYPYYKD.cjs.map → chunk-R46QEE3R.cjs.map} +1 -1
- package/dist/{chunk-H2TQ44VD.cjs → chunk-RQQZF6G6.cjs} +31 -31
- package/dist/{chunk-H2TQ44VD.cjs.map → chunk-RQQZF6G6.cjs.map} +1 -1
- package/dist/{chunk-CMP56RUC.cjs → chunk-VWGF6IFR.cjs} +62 -62
- package/dist/{chunk-CMP56RUC.cjs.map → chunk-VWGF6IFR.cjs.map} +1 -1
- package/dist/{chunk-POFDZH2C.js → chunk-WAGTZVQN.js} +4 -4
- package/dist/{chunk-XFSDFO6U.js → chunk-WPVBTVI3.js} +2 -2
- package/dist/{chunk-VYG2CJFJ.cjs → chunk-WZ6TCAYQ.cjs} +9 -9
- package/dist/{chunk-VYG2CJFJ.cjs.map → chunk-WZ6TCAYQ.cjs.map} +1 -1
- package/dist/{chunk-PCMDRDYX.cjs → chunk-ZMUFTGL7.cjs} +5 -5
- package/dist/{chunk-PCMDRDYX.cjs.map → chunk-ZMUFTGL7.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-archive-page.d.ts +28 -8
- package/dist/components/chat/chat-archive-page.d.ts.map +1 -1
- package/dist/components/chat/chat-header-icon-button.d.ts +6 -2
- package/dist/components/chat/chat-header-icon-button.d.ts.map +1 -1
- package/dist/components/chat/chat-header-search-field.d.ts +25 -0
- package/dist/components/chat/chat-header-search-field.d.ts.map +1 -0
- package/dist/components/chat/chat-list-empty-state.d.ts +20 -0
- package/dist/components/chat/chat-list-empty-state.d.ts.map +1 -0
- package/dist/components/chat/chat-panel-header.d.ts +19 -9
- package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts +15 -6
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts +2 -0
- package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +4 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +2 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +5 -3
- package/dist/components/chat/mingo-chat-history.d.ts +5 -10
- package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
- package/dist/components/chat/mingo-history-rail.d.ts +54 -0
- package/dist/components/chat/mingo-history-rail.d.ts.map +1 -0
- package/dist/components/chat/mingo-welcome.d.ts +6 -25
- package/dist/components/chat/mingo-welcome.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 +130 -128
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +13 -11
- 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 +4 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -3
- package/package.json +1 -1
- package/src/components/chat/chat-archive-page.tsx +89 -73
- package/src/components/chat/chat-header-icon-button.tsx +12 -5
- package/src/components/chat/chat-header-search-field.tsx +144 -0
- package/src/components/chat/chat-list-empty-state.tsx +44 -0
- package/src/components/chat/chat-panel-header.tsx +76 -29
- package/src/components/chat/embeddable-chat.tsx +548 -125
- package/src/components/chat/guide-welcome.tsx +4 -4
- package/src/components/chat/hooks/use-chat-dialog-manager.ts +59 -5
- package/src/components/chat/index.ts +2 -0
- package/src/components/chat/mingo-chat-history.tsx +46 -100
- package/src/components/chat/mingo-history-rail.tsx +161 -0
- package/src/components/chat/mingo-welcome.tsx +17 -98
- package/src/stories/MingoWelcome.stories.tsx +2 -11
- package/dist/chunk-2DCP2TIF.cjs.map +0 -1
- package/dist/chunk-6ETVJ5ON.js.map +0 -1
- /package/dist/{chunk-LR6A67KZ.js.map → chunk-2ELBNZJ3.js.map} +0 -0
- /package/dist/{chunk-KBFQF4AV.js.map → chunk-7WM77CSW.js.map} +0 -0
- /package/dist/{chunk-W2PRXXUD.js.map → chunk-COMBYYQY.js.map} +0 -0
- /package/dist/{chunk-RTHOCS3S.js.map → chunk-EGRZHR7J.js.map} +0 -0
- /package/dist/{chunk-D5RTZ2WN.js.map → chunk-F7YVI54P.js.map} +0 -0
- /package/dist/{chunk-5D5SJOLE.js.map → chunk-G5FALXNO.js.map} +0 -0
- /package/dist/{chunk-A25RPWYF.js.map → chunk-P7PISHM5.js.map} +0 -0
- /package/dist/{chunk-44HNH57W.js.map → chunk-PBTY7AH5.js.map} +0 -0
- /package/dist/{chunk-ZEY7A3MP.js.map → chunk-PEIR7D25.js.map} +0 -0
- /package/dist/{chunk-POFDZH2C.js.map → chunk-WAGTZVQN.js.map} +0 -0
- /package/dist/{chunk-XFSDFO6U.js.map → chunk-WPVBTVI3.js.map} +0 -0
|
@@ -168,17 +168,17 @@ export function GuideWelcome({
|
|
|
168
168
|
cornerColor="var(--ods-flamingo-cyan-base)"
|
|
169
169
|
/>
|
|
170
170
|
)}
|
|
171
|
-
<div className="flex w-full flex-col gap-
|
|
172
|
-
<p className="text-
|
|
171
|
+
<div className="flex w-full flex-col gap-[var(--spacing-system-l)]">
|
|
172
|
+
<p className="text-h2 text-ods-text-primary">{title}</p>
|
|
173
173
|
{/* Sub-line: while the greeting is still being fetched show a
|
|
174
174
|
one-line skeleton; once settled, render the greeting (admin
|
|
175
175
|
copy / host override) or nothing — no built-in default, so by
|
|
176
176
|
default the empty state shows the title alone. */}
|
|
177
177
|
{subtitle ? (
|
|
178
|
-
<p className="text-
|
|
178
|
+
<p className="text-h4 text-ods-text-secondary">{subtitle}</p>
|
|
179
179
|
) : subtitleLoading ? (
|
|
180
180
|
<div className="flex w-full justify-center">
|
|
181
|
-
<Skeleton className="h-
|
|
181
|
+
<Skeleton className="h-5 w-3/4 max-w-80 rounded-sm" />
|
|
182
182
|
</div>
|
|
183
183
|
) : null}
|
|
184
184
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo, useState } from 'react'
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
4
4
|
import type { DialogItem } from '../types/component.types'
|
|
5
5
|
import type {
|
|
6
6
|
FetchDialogsParams,
|
|
@@ -84,28 +84,68 @@ export function useChatDialogManager({
|
|
|
84
84
|
const [archivedDialogs, setArchivedDialogs] = useState<DialogItem[]>([])
|
|
85
85
|
const [archivedCursor, setArchivedCursor] = useState<string | null>(null)
|
|
86
86
|
const [archivedLoading, setArchivedLoading] = useState(false)
|
|
87
|
+
// A fetch is in flight, tracked SEPARATELY from the delayed skeleton
|
|
88
|
+
// (`archivedLoading`). Set synchronously so the empty state stays suppressed
|
|
89
|
+
// during the 120ms skeleton delay — otherwise an uncached first open flashes
|
|
90
|
+
// "No archived chats" before the skeleton/data appears.
|
|
91
|
+
const [archivedPending, setArchivedPending] = useState(false)
|
|
92
|
+
// Monotonic request id — a background page-1 refresh and a pagination fetch
|
|
93
|
+
// can overlap (the retained list stays interactive while page 1 revalidates).
|
|
94
|
+
// Only the LATEST request may mutate the list / cursor / loading flags, so a
|
|
95
|
+
// slow earlier response can't clobber newer rows, append stale ones, or clear
|
|
96
|
+
// the shared loading flag while another request is still in flight.
|
|
97
|
+
const archivedRequestIdRef = useRef(0)
|
|
87
98
|
const loadArchivedPage = useCallback(
|
|
88
99
|
async (cursor?: string): Promise<void> => {
|
|
89
100
|
if (!fetchArchivedDialogs) return
|
|
90
|
-
|
|
101
|
+
const requestId = ++archivedRequestIdRef.current
|
|
102
|
+
const isCurrent = () => archivedRequestIdRef.current === requestId
|
|
103
|
+
// Only a first-page load drives the full-view skeleton, and only if it's
|
|
104
|
+
// actually slow: a 120ms delay means a cached/fast fetch (the host caches
|
|
105
|
+
// page 1) resolves first and never flashes a skeleton. Pagination
|
|
106
|
+
// (`cursor`) keeps the current list visible.
|
|
107
|
+
const isInitial = !cursor
|
|
108
|
+
setArchivedPending(true)
|
|
109
|
+
let skeletonTimer: ReturnType<typeof setTimeout> | undefined
|
|
110
|
+
if (isInitial) {
|
|
111
|
+
// Delay the full-view skeleton so a cached/fast page-1 fetch never
|
|
112
|
+
// flashes it (and skip it if a newer request already superseded us).
|
|
113
|
+
skeletonTimer = setTimeout(() => {
|
|
114
|
+
if (isCurrent()) setArchivedLoading(true)
|
|
115
|
+
}, 120)
|
|
116
|
+
} else {
|
|
117
|
+
// Pagination shows its "load more" indicator immediately (the list
|
|
118
|
+
// stays visible), so no delay here.
|
|
119
|
+
setArchivedLoading(true)
|
|
120
|
+
}
|
|
91
121
|
try {
|
|
92
122
|
const result = await fetchArchivedDialogs({ cursor, limit: 20 })
|
|
123
|
+
// A newer request superseded this one — drop the response so overlapping
|
|
124
|
+
// refresh / pagination can't append stale rows or clobber newer results.
|
|
125
|
+
if (!isCurrent()) return
|
|
93
126
|
setArchivedCursor(result.nextCursor)
|
|
94
127
|
setArchivedDialogs((prev) =>
|
|
95
128
|
cursor ? [...prev, ...result.dialogs] : result.dialogs,
|
|
96
129
|
)
|
|
97
130
|
} catch (err) {
|
|
131
|
+
if (!isCurrent()) return
|
|
98
132
|
console.error('[useChatDialogManager] fetchArchivedDialogs failed:', err)
|
|
99
133
|
} finally {
|
|
100
|
-
|
|
134
|
+
if (skeletonTimer) clearTimeout(skeletonTimer)
|
|
135
|
+
// Only the latest request owns the shared loading / pending flags.
|
|
136
|
+
if (isCurrent()) {
|
|
137
|
+
setArchivedLoading(false)
|
|
138
|
+
setArchivedPending(false)
|
|
139
|
+
}
|
|
101
140
|
}
|
|
102
141
|
},
|
|
103
142
|
[fetchArchivedDialogs],
|
|
104
143
|
)
|
|
105
144
|
const openArchive = useCallback(() => {
|
|
106
145
|
setArchiveOpen(true)
|
|
107
|
-
|
|
108
|
-
|
|
146
|
+
// Don't clear the list — keep any already-loaded page visible and refresh
|
|
147
|
+
// page 1 in the background (stale-while-revalidate), so reopening the
|
|
148
|
+
// archive within a session doesn't blank out to a skeleton.
|
|
109
149
|
void loadArchivedPage()
|
|
110
150
|
}, [loadArchivedPage])
|
|
111
151
|
const closeArchive = useCallback(() => setArchiveOpen(false), [])
|
|
@@ -183,6 +223,18 @@ export function useChatDialogManager({
|
|
|
183
223
|
if (wasArchived) setArchiveOpen(true)
|
|
184
224
|
}, [isViewingArchived, selectDialog, clearMessages])
|
|
185
225
|
|
|
226
|
+
// "Start New Chat" reset — clears the open conversation and ALWAYS lands on
|
|
227
|
+
// the new-chat welcome. Unlike `handleBack`, it never reopens the archive
|
|
228
|
+
// (backing out of an archived chat returns to the archive; starting a NEW
|
|
229
|
+
// chat must not), so the archive is force-closed here regardless of state.
|
|
230
|
+
const resetToNewChat = useCallback(() => {
|
|
231
|
+
setViewingArchivedId(null)
|
|
232
|
+
setOpeningDialogId(null)
|
|
233
|
+
selectDialog(null)
|
|
234
|
+
clearMessages()
|
|
235
|
+
setArchiveOpen(false)
|
|
236
|
+
}, [selectDialog, clearMessages])
|
|
237
|
+
|
|
186
238
|
// Active-conversation dialog — resolved from the active list, or the
|
|
187
239
|
// archived list when an archived chat is open (archived dialogs aren't in
|
|
188
240
|
// `dialogs`, so the header title and ⋯ / restore actions need this).
|
|
@@ -202,6 +254,7 @@ export function useChatDialogManager({
|
|
|
202
254
|
archivedDialogs,
|
|
203
255
|
archivedCursor,
|
|
204
256
|
archivedLoading,
|
|
257
|
+
archivedPending,
|
|
205
258
|
openArchive,
|
|
206
259
|
closeArchive,
|
|
207
260
|
loadArchivedPage,
|
|
@@ -212,6 +265,7 @@ export function useChatDialogManager({
|
|
|
212
265
|
// archived conversation
|
|
213
266
|
isViewingArchived,
|
|
214
267
|
handleBack,
|
|
268
|
+
resetToNewChat,
|
|
215
269
|
activeDialog,
|
|
216
270
|
// action modals
|
|
217
271
|
renameTarget,
|
|
@@ -31,8 +31,10 @@ export * from './msp-organization-card'
|
|
|
31
31
|
export * from './msp-organization-card-skeleton'
|
|
32
32
|
export * from './guide-mode-banner'
|
|
33
33
|
export * from './mingo-chat-history'
|
|
34
|
+
export * from './mingo-history-rail'
|
|
34
35
|
export * from './mingo-chat-modals'
|
|
35
36
|
export * from './chat-header-icon-button'
|
|
37
|
+
export * from './chat-header-search-field'
|
|
36
38
|
export * from './chat-panel-header'
|
|
37
39
|
export * from './chat-composer'
|
|
38
40
|
export * from './chat-context-picker'
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { cn } from '../../utils/cn'
|
|
5
|
-
import { useDebounce } from '../../hooks/ui/use-debounce'
|
|
6
5
|
import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
|
|
7
6
|
import { Button } from '../ui/button'
|
|
8
|
-
import { Input } from '../ui/input'
|
|
9
7
|
import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
|
|
10
|
-
import { Ellipsis01Icon,
|
|
8
|
+
import { Ellipsis01Icon, SearchXmarkIcon } from '../icons-v2-generated'
|
|
9
|
+
import { ChatListEmptyState } from './chat-list-empty-state'
|
|
11
10
|
import type { DialogItem } from './types/component.types'
|
|
12
11
|
|
|
13
12
|
// =============================================================================
|
|
@@ -27,12 +26,10 @@ export interface MingoChatHistoryProps {
|
|
|
27
26
|
/** Request archive — enables the row "Archive chat" action. The host opens
|
|
28
27
|
* the Archive confirmation modal. */
|
|
29
28
|
onRequestArchive?: (dialog: DialogItem) => void
|
|
30
|
-
/**
|
|
31
|
-
* the
|
|
29
|
+
/** Current server-side search term. Drives the "No chats found" empty state;
|
|
30
|
+
* the search INPUT lives in the panel header, not in this list. The host
|
|
31
|
+
* owns the term and refetches server-side — the list does no filtering. */
|
|
32
32
|
searchQuery?: string
|
|
33
|
-
/** Emit the (debounced) search term — enables the dialog search bar above the
|
|
34
|
-
* list. Omit to hide the search bar entirely. */
|
|
35
|
-
onSearchChange?: (query: string) => void
|
|
36
33
|
/** Whether more dialogs remain (cursor pagination). */
|
|
37
34
|
hasMore?: boolean
|
|
38
35
|
/** True while the next page is loading. */
|
|
@@ -134,11 +131,21 @@ function MingoChatHistoryRow({
|
|
|
134
131
|
return (
|
|
135
132
|
<div
|
|
136
133
|
className={cn(
|
|
137
|
-
'group/row flex h-12 items-center
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
'group/row relative flex h-12 items-center border-b border-ods-border last:border-b-0 transition-colors',
|
|
135
|
+
// Active (selected) dialog — Figma 259:91610: an open-yellow-secondary
|
|
136
|
+
// fill, a 4px open-yellow accent bar down the leading edge, and yellow
|
|
137
|
+
// title text. Inactive rows keep the dark surface with a hover tint.
|
|
138
|
+
isActive
|
|
139
|
+
? 'bg-ods-open-yellow-secondary'
|
|
140
|
+
: 'bg-ods-bg hover:bg-ods-bg-hover',
|
|
140
141
|
)}
|
|
141
142
|
>
|
|
143
|
+
{isActive ? (
|
|
144
|
+
<span
|
|
145
|
+
aria-hidden
|
|
146
|
+
className="pointer-events-none absolute inset-y-0 left-0 w-1 bg-ods-open-yellow"
|
|
147
|
+
/>
|
|
148
|
+
) : null}
|
|
142
149
|
<div
|
|
143
150
|
role="button"
|
|
144
151
|
tabIndex={0}
|
|
@@ -149,7 +156,7 @@ function MingoChatHistoryRow({
|
|
|
149
156
|
onSelect?.(dialog.id)
|
|
150
157
|
}
|
|
151
158
|
}}
|
|
152
|
-
className="flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xsf)] p-[var(--spacing-system-s)] cursor-pointer focus:outline-none focus-visible:
|
|
159
|
+
className="flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xsf)] p-[var(--spacing-system-s)] cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent"
|
|
153
160
|
>
|
|
154
161
|
{unread > 0 ? (
|
|
155
162
|
<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">
|
|
@@ -157,7 +164,10 @@ function MingoChatHistoryRow({
|
|
|
157
164
|
</span>
|
|
158
165
|
) : null}
|
|
159
166
|
<span
|
|
160
|
-
className=
|
|
167
|
+
className={cn(
|
|
168
|
+
'min-w-0 flex-1 truncate text-h4',
|
|
169
|
+
isActive ? 'text-ods-open-yellow' : 'text-ods-text-primary',
|
|
170
|
+
)}
|
|
161
171
|
title={title}
|
|
162
172
|
>
|
|
163
173
|
{title}
|
|
@@ -222,61 +232,39 @@ function MingoChatHistoryRow({
|
|
|
222
232
|
*/
|
|
223
233
|
export function MingoChatHistorySkeleton({
|
|
224
234
|
className,
|
|
225
|
-
searchable = false,
|
|
226
235
|
}: {
|
|
227
236
|
className?: string
|
|
228
|
-
/** Render a search-bar placeholder above the groups — match the real list,
|
|
229
|
-
* which shows the search bar only when search is wired. */
|
|
230
|
-
searchable?: boolean
|
|
231
237
|
}) {
|
|
232
|
-
// Mirror the real grouped list: a group label, then a bordered
|
|
233
|
-
// `h-12` rows
|
|
234
|
-
//
|
|
235
|
-
// a
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
const groups: ReadonlyArray<
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
// Mirror the real grouped list exactly: a small group label, then a bordered
|
|
239
|
+
// rounded container of `h-12` rows, each with a single title bar of varying
|
|
240
|
+
// width (no unread-badge squares — the real rows currently never show one, so
|
|
241
|
+
// a badge placeholder would mis-promise the layout). `bg-ods-border` matches
|
|
242
|
+
// the kit's canonical `Skeleton` placeholder colour; `bg-ods-bg` rows read a
|
|
243
|
+
// touch darker than the `bg-ods-card` panel so the bars stay legible.
|
|
244
|
+
const groups: ReadonlyArray<ReadonlyArray<string>> = [
|
|
245
|
+
['w-3/5', 'w-4/5', 'w-2/5', 'w-3/4'],
|
|
246
|
+
['w-1/2', 'w-2/3'],
|
|
241
247
|
]
|
|
242
248
|
return (
|
|
243
249
|
<div
|
|
244
250
|
aria-hidden
|
|
245
251
|
// `overflow-hidden` clips the placeholder rows to the flex-1 box (the real
|
|
246
|
-
// list scrolls; the skeleton just needs to stay within bounds).
|
|
247
|
-
// the rows bleed past their allotment and render under the pinned composer
|
|
248
|
-
// — which has no surface of its own — so the input appears to overlap them.
|
|
252
|
+
// list scrolls; the skeleton just needs to stay within bounds).
|
|
249
253
|
className={cn(
|
|
250
254
|
'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-hidden',
|
|
251
255
|
className,
|
|
252
256
|
)}
|
|
253
257
|
>
|
|
254
|
-
{
|
|
255
|
-
h-11/12) with a leading icon + text bar so the loading state matches
|
|
256
|
-
the searchable layout. */}
|
|
257
|
-
{searchable && (
|
|
258
|
-
<div className="flex h-11 md:h-12 shrink-0 items-center gap-2 rounded-[6px] border border-ods-border bg-ods-card px-3">
|
|
259
|
-
<div className="size-4 md:size-6 shrink-0 animate-pulse rounded bg-ods-border" />
|
|
260
|
-
<div className="h-4 w-32 animate-pulse rounded bg-ods-border" />
|
|
261
|
-
</div>
|
|
262
|
-
)}
|
|
263
|
-
{groups.map((group, g) => (
|
|
258
|
+
{groups.map((widths, g) => (
|
|
264
259
|
<div key={g} className="flex shrink-0 flex-col gap-[var(--spacing-system-xxs)]">
|
|
265
|
-
{/* Group label (Today /
|
|
260
|
+
{/* Group label (Today / Older). */}
|
|
266
261
|
<div className="h-3 w-16 animate-pulse rounded bg-ods-border" />
|
|
267
262
|
<div className="overflow-hidden rounded-md border border-ods-border">
|
|
268
|
-
{
|
|
263
|
+
{widths.map((w, i) => (
|
|
269
264
|
<div
|
|
270
265
|
key={i}
|
|
271
|
-
|
|
272
|
-
// than the `bg-ods-card` (#212121) panel, which is what makes
|
|
273
|
-
// the `bg-ods-border` (#3a3a3a) placeholders read clearly. On
|
|
274
|
-
// the bare card they'd be muddy (too close in value).
|
|
275
|
-
className="flex h-12 items-center gap-[var(--spacing-system-xsf)] bg-ods-bg border-b border-ods-border px-[var(--spacing-system-s)] last:border-b-0"
|
|
266
|
+
className="flex h-12 items-center bg-ods-bg border-b border-ods-border px-[var(--spacing-system-s)] last:border-b-0"
|
|
276
267
|
>
|
|
277
|
-
{i < group.withBadge && (
|
|
278
|
-
<div className="h-8 w-8 shrink-0 animate-pulse rounded-md bg-ods-border" />
|
|
279
|
-
)}
|
|
280
268
|
<div className={cn('h-4 animate-pulse rounded bg-ods-border', w)} />
|
|
281
269
|
</div>
|
|
282
270
|
))}
|
|
@@ -287,46 +275,6 @@ export function MingoChatHistorySkeleton({
|
|
|
287
275
|
)
|
|
288
276
|
}
|
|
289
277
|
|
|
290
|
-
// =============================================================================
|
|
291
|
-
// Search bar
|
|
292
|
-
// =============================================================================
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Dialog search field. Holds its own input text for snappy typing and emits
|
|
296
|
-
* the DEBOUNCED term via `onSearchChange` — the host owns the search state
|
|
297
|
-
* and refetches the dialog list server-side, so the list never
|
|
298
|
-
* filters locally. `initialValue` only seeds the field on mount.
|
|
299
|
-
*/
|
|
300
|
-
function DialogSearchBar({
|
|
301
|
-
initialValue,
|
|
302
|
-
onSearchChange,
|
|
303
|
-
}: {
|
|
304
|
-
initialValue?: string
|
|
305
|
-
onSearchChange: (query: string) => void
|
|
306
|
-
}) {
|
|
307
|
-
const [value, setValue] = React.useState(initialValue ?? '')
|
|
308
|
-
const debounced = useDebounce(value, 300)
|
|
309
|
-
const lastEmitted = React.useRef(initialValue ?? '')
|
|
310
|
-
|
|
311
|
-
React.useEffect(() => {
|
|
312
|
-
if (debounced === lastEmitted.current) return
|
|
313
|
-
lastEmitted.current = debounced
|
|
314
|
-
onSearchChange(debounced)
|
|
315
|
-
}, [debounced, onSearchChange])
|
|
316
|
-
|
|
317
|
-
return (
|
|
318
|
-
<Input
|
|
319
|
-
type="text"
|
|
320
|
-
value={value}
|
|
321
|
-
onChange={(e) => setValue(e.target.value)}
|
|
322
|
-
placeholder="Search for Chats"
|
|
323
|
-
aria-label="Search chats"
|
|
324
|
-
startAdornment={<SearchIcon />}
|
|
325
|
-
className="shrink-0"
|
|
326
|
-
/>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
278
|
// =============================================================================
|
|
331
279
|
// Component
|
|
332
280
|
// =============================================================================
|
|
@@ -349,7 +297,6 @@ export function MingoChatHistory({
|
|
|
349
297
|
onRequestRename,
|
|
350
298
|
onRequestArchive,
|
|
351
299
|
searchQuery,
|
|
352
|
-
onSearchChange,
|
|
353
300
|
hasMore = false,
|
|
354
301
|
isLoadingMore = false,
|
|
355
302
|
onLoadMore,
|
|
@@ -388,13 +335,8 @@ export function MingoChatHistory({
|
|
|
388
335
|
|
|
389
336
|
return (
|
|
390
337
|
<div className={cn('relative flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]', className)}>
|
|
391
|
-
{/*
|
|
392
|
-
|
|
393
|
-
<DialogSearchBar initialValue={searchQuery} onSearchChange={onSearchChange} />
|
|
394
|
-
) : null}
|
|
395
|
-
|
|
396
|
-
{/* Scroll region — `relative` so the fades anchor here (below the search
|
|
397
|
-
bar), not over it. */}
|
|
338
|
+
{/* Scroll region — the search INPUT now lives in the panel header
|
|
339
|
+
(Figma 116:51217), so the list is just the grouped rows + fades. */}
|
|
398
340
|
<div className="relative flex flex-1 min-h-0 flex-col">
|
|
399
341
|
<div
|
|
400
342
|
ref={scrollRef}
|
|
@@ -402,9 +344,13 @@ export function MingoChatHistory({
|
|
|
402
344
|
className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
|
|
403
345
|
>
|
|
404
346
|
{noSearchResults ? (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
347
|
+
// No search matches — same centred glyph + title + caption layout as
|
|
348
|
+
// the other chat-list empty states (Figma 113:60939).
|
|
349
|
+
<ChatListEmptyState
|
|
350
|
+
icon={<SearchXmarkIcon size={24} />}
|
|
351
|
+
title="No Chats Found"
|
|
352
|
+
description="Try a different search term"
|
|
353
|
+
/>
|
|
408
354
|
) : (
|
|
409
355
|
groups.map((group) => (
|
|
410
356
|
<div
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
import { Button } from '../ui/button'
|
|
6
|
+
import { ChatsIcon } from '../icons-v2-generated/communication/chats-icon'
|
|
7
|
+
import { PlusCircleIcon } from '../icons-v2-generated/signs-and-symbols/plus-circle-icon'
|
|
8
|
+
import { AlertCircleIcon, Refresh01RightIcon } from '../icons-v2-generated'
|
|
9
|
+
import { MingoChatHistory, MingoChatHistorySkeleton } from './mingo-chat-history'
|
|
10
|
+
import { ChatListEmptyState } from './chat-list-empty-state'
|
|
11
|
+
import type { DialogItem } from './types/component.types'
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export interface MingoHistoryRailProps {
|
|
18
|
+
/** Dialogs to list, assumed already sorted newest-first by the host. */
|
|
19
|
+
dialogs: ReadonlyArray<DialogItem>
|
|
20
|
+
/** Currently-open dialog id (highlighted). */
|
|
21
|
+
activeDialogId?: string
|
|
22
|
+
/** Open a dialog. */
|
|
23
|
+
onSelectDialog?: (id: string) => void
|
|
24
|
+
/** Start a fresh chat — clears the open conversation so the chat block shows
|
|
25
|
+
* the welcome. Rendered as the pinned "Start New Chat" button above the list. */
|
|
26
|
+
onNewChat?: () => void
|
|
27
|
+
/** Show the "Start New Chat" button even when there are no chats yet. Needed
|
|
28
|
+
* for the narrow single-column list, where there's no composer to fall back
|
|
29
|
+
* on; the wide rail leaves it off (the chat block already has a composer). */
|
|
30
|
+
newChatAlways?: boolean
|
|
31
|
+
/** Request rename — enables the row "Rename chat" action. */
|
|
32
|
+
onRequestRename?: (dialog: DialogItem) => void
|
|
33
|
+
/** Request archive — enables the row "Archive chat" action. */
|
|
34
|
+
onRequestArchive?: (dialog: DialogItem) => void
|
|
35
|
+
/** Current server-side search term. Drives the list's "No chats found"
|
|
36
|
+
* empty state; the search INPUT itself lives in the panel header now, not
|
|
37
|
+
* in the rail body. */
|
|
38
|
+
searchQuery?: string
|
|
39
|
+
/** Whether more dialogs remain (cursor pagination). */
|
|
40
|
+
hasMore?: boolean
|
|
41
|
+
/** True while the next page is loading. */
|
|
42
|
+
isLoadingMore?: boolean
|
|
43
|
+
/** Fetch the next page. */
|
|
44
|
+
onLoadMore?: () => void
|
|
45
|
+
/** True while the FIRST page of dialogs is still loading. Renders a skeleton
|
|
46
|
+
* so the rail doesn't flash the empty state before the list arrives. */
|
|
47
|
+
isLoadingHistory?: boolean
|
|
48
|
+
/** The dialog-list load FAILED with nothing cached — renders an error + retry
|
|
49
|
+
* block instead of the empty state. */
|
|
50
|
+
loadError?: boolean
|
|
51
|
+
/** Retry handler for the `loadError` state. */
|
|
52
|
+
onRetry?: () => void
|
|
53
|
+
/** Appended to the root element. */
|
|
54
|
+
className?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// Component
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* MingoHistoryRail — the persistent "Current Chats" left column of the split
|
|
63
|
+
* (wide) Mingo layout (Figma 113:60931 / 113:63630).
|
|
64
|
+
*
|
|
65
|
+
* The dialog history that the stacked layout renders inline in the Mingo empty
|
|
66
|
+
* state is hoisted here into a fixed-width rail beside the chat block. On top
|
|
67
|
+
* sits a pinned "Start New Chat" button (shown once the user has chats); below
|
|
68
|
+
* it the grouped Today / Yesterday / Older list (`MingoChatHistory`). With no
|
|
69
|
+
* chats it collapses to a centred empty state; loading / error states mirror
|
|
70
|
+
* the stacked empty state so the two layouts stay consistent.
|
|
71
|
+
*/
|
|
72
|
+
export function MingoHistoryRail({
|
|
73
|
+
dialogs,
|
|
74
|
+
activeDialogId,
|
|
75
|
+
onSelectDialog,
|
|
76
|
+
onNewChat,
|
|
77
|
+
newChatAlways = false,
|
|
78
|
+
onRequestRename,
|
|
79
|
+
onRequestArchive,
|
|
80
|
+
searchQuery,
|
|
81
|
+
hasMore = false,
|
|
82
|
+
isLoadingMore = false,
|
|
83
|
+
onLoadMore,
|
|
84
|
+
isLoadingHistory = false,
|
|
85
|
+
loadError = false,
|
|
86
|
+
onRetry,
|
|
87
|
+
className,
|
|
88
|
+
}: MingoHistoryRailProps) {
|
|
89
|
+
const hasSearch = !!searchQuery?.trim()
|
|
90
|
+
// The list (vs. the empty state) shows whenever there are chats OR an active
|
|
91
|
+
// search is running — a no-match query must keep the search bar mounted
|
|
92
|
+
// rather than flash the "No Current Chats" state.
|
|
93
|
+
const hasList = dialogs.length > 0 || hasSearch
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className={cn(
|
|
98
|
+
'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] p-[var(--spacing-system-m)]',
|
|
99
|
+
className,
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{/* Pinned "Start New Chat". In the wide rail it's offered only once the
|
|
103
|
+
user has chats (the chat block's composer is the empty-state entry);
|
|
104
|
+
the narrow list forces it on (`newChatAlways`) since it has no composer. */}
|
|
105
|
+
{onNewChat && (hasList || newChatAlways) && (
|
|
106
|
+
<Button
|
|
107
|
+
variant="outline"
|
|
108
|
+
fullWidth
|
|
109
|
+
leftIcon={<PlusCircleIcon size={20} />}
|
|
110
|
+
onClick={onNewChat}
|
|
111
|
+
className="shrink-0 justify-start"
|
|
112
|
+
>
|
|
113
|
+
Start New Chat
|
|
114
|
+
</Button>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{loadError ? (
|
|
118
|
+
<div className="flex flex-1 min-h-0 flex-col items-center justify-center gap-[var(--spacing-system-m)] text-center">
|
|
119
|
+
<AlertCircleIcon className="h-8 w-8 text-ods-text-secondary shrink-0" />
|
|
120
|
+
<div className="flex flex-col gap-[var(--spacing-system-xxs)]">
|
|
121
|
+
<p className="text-h4 text-ods-text-primary">Couldn’t load your chats</p>
|
|
122
|
+
<p className="text-h6 text-ods-text-secondary">
|
|
123
|
+
Something went wrong reaching the server. Try again.
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
{onRetry && (
|
|
127
|
+
<Button
|
|
128
|
+
variant="outline"
|
|
129
|
+
size="small"
|
|
130
|
+
leftIcon={<Refresh01RightIcon />}
|
|
131
|
+
onClick={onRetry}
|
|
132
|
+
>
|
|
133
|
+
Try again
|
|
134
|
+
</Button>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
) : isLoadingHistory ? (
|
|
138
|
+
<MingoChatHistorySkeleton />
|
|
139
|
+
) : hasList ? (
|
|
140
|
+
<MingoChatHistory
|
|
141
|
+
dialogs={dialogs}
|
|
142
|
+
activeDialogId={activeDialogId}
|
|
143
|
+
onSelectDialog={onSelectDialog}
|
|
144
|
+
onRequestRename={onRequestRename}
|
|
145
|
+
onRequestArchive={onRequestArchive}
|
|
146
|
+
searchQuery={searchQuery}
|
|
147
|
+
hasMore={hasMore}
|
|
148
|
+
isLoadingMore={isLoadingMore}
|
|
149
|
+
onLoadMore={onLoadMore}
|
|
150
|
+
/>
|
|
151
|
+
) : (
|
|
152
|
+
// Figma 113:60939 — no-chats empty state.
|
|
153
|
+
<ChatListEmptyState
|
|
154
|
+
icon={<ChatsIcon size={24} />}
|
|
155
|
+
title="No Current Chats"
|
|
156
|
+
description="Previous Mingo sessions will show here"
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|