@flamingo-stack/openframe-frontend-core 0.0.438-snapshot.20260716203356 → 0.0.438
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-HTVVXVJL.cjs → chunk-2DCP2TIF.cjs} +1490 -1966
- package/dist/chunk-2DCP2TIF.cjs.map +1 -0
- package/dist/{chunk-PBTY7AH5.js → chunk-44HNH57W.js} +4 -4
- package/dist/{chunk-G5FALXNO.js → chunk-5D5SJOLE.js} +4 -4
- package/dist/{chunk-NQQ4NLUT.js → chunk-6ETVJ5ON.js} +6101 -6577
- package/dist/chunk-6ETVJ5ON.js.map +1 -0
- package/dist/{chunk-3WJQL4DN.cjs → chunk-6R5SZPAQ.cjs} +15 -15
- package/dist/{chunk-3WJQL4DN.cjs.map → chunk-6R5SZPAQ.cjs.map} +1 -1
- package/dist/{chunk-P7PISHM5.js → chunk-A25RPWYF.js} +2 -2
- package/dist/{chunk-AUJXLVXA.cjs → chunk-B5SGPIJ4.cjs} +37 -37
- package/dist/{chunk-AUJXLVXA.cjs.map → chunk-B5SGPIJ4.cjs.map} +1 -1
- package/dist/{chunk-VWGF6IFR.cjs → chunk-CMP56RUC.cjs} +62 -62
- package/dist/{chunk-VWGF6IFR.cjs.map → chunk-CMP56RUC.cjs.map} +1 -1
- package/dist/{chunk-F7YVI54P.js → chunk-D5RTZ2WN.js} +4 -4
- package/dist/{chunk-R46QEE3R.cjs → chunk-DTYPYYKD.cjs} +59 -59
- package/dist/{chunk-R46QEE3R.cjs.map → chunk-DTYPYYKD.cjs.map} +1 -1
- package/dist/{chunk-RQQZF6G6.cjs → chunk-H2TQ44VD.cjs} +31 -31
- package/dist/{chunk-RQQZF6G6.cjs.map → chunk-H2TQ44VD.cjs.map} +1 -1
- package/dist/{chunk-7WM77CSW.js → chunk-KBFQF4AV.js} +2 -2
- package/dist/{chunk-2ELBNZJ3.js → chunk-LR6A67KZ.js} +2 -2
- package/dist/{chunk-ZMUFTGL7.cjs → chunk-PCMDRDYX.cjs} +5 -5
- package/dist/{chunk-ZMUFTGL7.cjs.map → chunk-PCMDRDYX.cjs.map} +1 -1
- package/dist/{chunk-WAGTZVQN.js → chunk-POFDZH2C.js} +4 -4
- package/dist/{chunk-EGRZHR7J.js → chunk-RTHOCS3S.js} +2 -2
- package/dist/{chunk-M3LD6DNE.cjs → chunk-SYTL2VIU.cjs} +7 -7
- package/dist/{chunk-M3LD6DNE.cjs.map → chunk-SYTL2VIU.cjs.map} +1 -1
- package/dist/{chunk-WZ6TCAYQ.cjs → chunk-VYG2CJFJ.cjs} +9 -9
- package/dist/{chunk-WZ6TCAYQ.cjs.map → chunk-VYG2CJFJ.cjs.map} +1 -1
- package/dist/{chunk-COMBYYQY.js → chunk-W2PRXXUD.js} +2 -2
- package/dist/{chunk-IAN7GUIV.cjs → chunk-WF7VGEYR.cjs} +26 -26
- package/dist/{chunk-IAN7GUIV.cjs.map → chunk-WF7VGEYR.cjs.map} +1 -1
- package/dist/{chunk-C3FH53AP.cjs → chunk-X66KRL5G.cjs} +11 -11
- package/dist/{chunk-C3FH53AP.cjs.map → chunk-X66KRL5G.cjs.map} +1 -1
- package/dist/{chunk-WPVBTVI3.js → chunk-XFSDFO6U.js} +2 -2
- package/dist/{chunk-M6QGB6DC.cjs → chunk-YKP2F2NC.cjs} +26 -26
- package/dist/{chunk-M6QGB6DC.cjs.map → chunk-YKP2F2NC.cjs.map} +1 -1
- package/dist/{chunk-PEIR7D25.js → chunk-ZEY7A3MP.js} +2 -2
- 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 +8 -28
- package/dist/components/chat/chat-archive-page.d.ts.map +1 -1
- package/dist/components/chat/chat-header-icon-button.d.ts +2 -6
- package/dist/components/chat/chat-header-icon-button.d.ts.map +1 -1
- package/dist/components/chat/chat-panel-header.d.ts +9 -19
- package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts +6 -15
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts +0 -2
- package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +0 -2
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +3 -5
- package/dist/components/chat/mingo-chat-history.d.ts +10 -5
- package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
- package/dist/components/chat/mingo-welcome.d.ts +25 -6
- 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 +128 -130
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +11 -13
- 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 +2 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +3 -5
- package/dist/index.cjs +2 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -5
- package/package.json +1 -1
- package/src/components/chat/chat-archive-page.tsx +73 -89
- package/src/components/chat/chat-header-icon-button.tsx +5 -12
- package/src/components/chat/chat-panel-header.tsx +29 -76
- package/src/components/chat/embeddable-chat.tsx +125 -548
- package/src/components/chat/guide-welcome.tsx +4 -4
- package/src/components/chat/hooks/use-chat-dialog-manager.ts +5 -59
- package/src/components/chat/index.ts +0 -2
- package/src/components/chat/mingo-chat-history.tsx +100 -46
- package/src/components/chat/mingo-welcome.tsx +98 -17
- package/src/stories/MingoWelcome.stories.tsx +11 -2
- package/dist/chunk-HTVVXVJL.cjs.map +0 -1
- package/dist/chunk-NQQ4NLUT.js.map +0 -1
- package/dist/components/chat/chat-header-search-field.d.ts +0 -25
- package/dist/components/chat/chat-header-search-field.d.ts.map +0 -1
- package/dist/components/chat/chat-list-empty-state.d.ts +0 -20
- package/dist/components/chat/chat-list-empty-state.d.ts.map +0 -1
- package/dist/components/chat/mingo-history-rail.d.ts +0 -54
- package/dist/components/chat/mingo-history-rail.d.ts.map +0 -1
- package/src/components/chat/chat-header-search-field.tsx +0 -144
- package/src/components/chat/chat-list-empty-state.tsx +0 -44
- package/src/components/chat/mingo-history-rail.tsx +0 -161
- /package/dist/{chunk-PBTY7AH5.js.map → chunk-44HNH57W.js.map} +0 -0
- /package/dist/{chunk-G5FALXNO.js.map → chunk-5D5SJOLE.js.map} +0 -0
- /package/dist/{chunk-P7PISHM5.js.map → chunk-A25RPWYF.js.map} +0 -0
- /package/dist/{chunk-F7YVI54P.js.map → chunk-D5RTZ2WN.js.map} +0 -0
- /package/dist/{chunk-7WM77CSW.js.map → chunk-KBFQF4AV.js.map} +0 -0
- /package/dist/{chunk-2ELBNZJ3.js.map → chunk-LR6A67KZ.js.map} +0 -0
- /package/dist/{chunk-WAGTZVQN.js.map → chunk-POFDZH2C.js.map} +0 -0
- /package/dist/{chunk-EGRZHR7J.js.map → chunk-RTHOCS3S.js.map} +0 -0
- /package/dist/{chunk-COMBYYQY.js.map → chunk-W2PRXXUD.js.map} +0 -0
- /package/dist/{chunk-WPVBTVI3.js.map → chunk-XFSDFO6U.js.map} +0 -0
- /package/dist/{chunk-PEIR7D25.js.map → chunk-ZEY7A3MP.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-1">
|
|
172
|
+
<p className="text-h4 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-h6 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-4 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,
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
4
4
|
import type { DialogItem } from '../types/component.types'
|
|
5
5
|
import type {
|
|
6
6
|
FetchDialogsParams,
|
|
@@ -84,68 +84,28 @@ 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)
|
|
98
87
|
const loadArchivedPage = useCallback(
|
|
99
88
|
async (cursor?: string): Promise<void> => {
|
|
100
89
|
if (!fetchArchivedDialogs) return
|
|
101
|
-
|
|
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
|
-
}
|
|
90
|
+
setArchivedLoading(true)
|
|
121
91
|
try {
|
|
122
92
|
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
|
|
126
93
|
setArchivedCursor(result.nextCursor)
|
|
127
94
|
setArchivedDialogs((prev) =>
|
|
128
95
|
cursor ? [...prev, ...result.dialogs] : result.dialogs,
|
|
129
96
|
)
|
|
130
97
|
} catch (err) {
|
|
131
|
-
if (!isCurrent()) return
|
|
132
98
|
console.error('[useChatDialogManager] fetchArchivedDialogs failed:', err)
|
|
133
99
|
} finally {
|
|
134
|
-
|
|
135
|
-
// Only the latest request owns the shared loading / pending flags.
|
|
136
|
-
if (isCurrent()) {
|
|
137
|
-
setArchivedLoading(false)
|
|
138
|
-
setArchivedPending(false)
|
|
139
|
-
}
|
|
100
|
+
setArchivedLoading(false)
|
|
140
101
|
}
|
|
141
102
|
},
|
|
142
103
|
[fetchArchivedDialogs],
|
|
143
104
|
)
|
|
144
105
|
const openArchive = useCallback(() => {
|
|
145
106
|
setArchiveOpen(true)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// archive within a session doesn't blank out to a skeleton.
|
|
107
|
+
setArchivedDialogs([])
|
|
108
|
+
setArchivedCursor(null)
|
|
149
109
|
void loadArchivedPage()
|
|
150
110
|
}, [loadArchivedPage])
|
|
151
111
|
const closeArchive = useCallback(() => setArchiveOpen(false), [])
|
|
@@ -223,18 +183,6 @@ export function useChatDialogManager({
|
|
|
223
183
|
if (wasArchived) setArchiveOpen(true)
|
|
224
184
|
}, [isViewingArchived, selectDialog, clearMessages])
|
|
225
185
|
|
|
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
|
-
|
|
238
186
|
// Active-conversation dialog — resolved from the active list, or the
|
|
239
187
|
// archived list when an archived chat is open (archived dialogs aren't in
|
|
240
188
|
// `dialogs`, so the header title and ⋯ / restore actions need this).
|
|
@@ -254,7 +202,6 @@ export function useChatDialogManager({
|
|
|
254
202
|
archivedDialogs,
|
|
255
203
|
archivedCursor,
|
|
256
204
|
archivedLoading,
|
|
257
|
-
archivedPending,
|
|
258
205
|
openArchive,
|
|
259
206
|
closeArchive,
|
|
260
207
|
loadArchivedPage,
|
|
@@ -265,7 +212,6 @@ export function useChatDialogManager({
|
|
|
265
212
|
// archived conversation
|
|
266
213
|
isViewingArchived,
|
|
267
214
|
handleBack,
|
|
268
|
-
resetToNewChat,
|
|
269
215
|
activeDialog,
|
|
270
216
|
// action modals
|
|
271
217
|
renameTarget,
|
|
@@ -31,10 +31,8 @@ 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'
|
|
35
34
|
export * from './mingo-chat-modals'
|
|
36
35
|
export * from './chat-header-icon-button'
|
|
37
|
-
export * from './chat-header-search-field'
|
|
38
36
|
export * from './chat-panel-header'
|
|
39
37
|
export * from './chat-composer'
|
|
40
38
|
export * from './chat-context-picker'
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { cn } from '../../utils/cn'
|
|
5
|
+
import { useDebounce } from '../../hooks/ui/use-debounce'
|
|
5
6
|
import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
|
|
6
7
|
import { Button } from '../ui/button'
|
|
8
|
+
import { Input } from '../ui/input'
|
|
7
9
|
import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
|
|
8
|
-
import { Ellipsis01Icon,
|
|
9
|
-
import { ChatListEmptyState } from './chat-list-empty-state'
|
|
10
|
+
import { Ellipsis01Icon, SearchIcon } from '../icons-v2-generated'
|
|
10
11
|
import type { DialogItem } from './types/component.types'
|
|
11
12
|
|
|
12
13
|
// =============================================================================
|
|
@@ -26,10 +27,12 @@ export interface MingoChatHistoryProps {
|
|
|
26
27
|
/** Request archive — enables the row "Archive chat" action. The host opens
|
|
27
28
|
* the Archive confirmation modal. */
|
|
28
29
|
onRequestArchive?: (dialog: DialogItem) => void
|
|
29
|
-
/**
|
|
30
|
-
* the
|
|
31
|
-
* owns the term and refetches server-side — the list does no filtering. */
|
|
30
|
+
/** Seed value for the search input. The host owns the search term and refetches
|
|
31
|
+
* the dialog list server-side; the list itself 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
|
|
33
36
|
/** Whether more dialogs remain (cursor pagination). */
|
|
34
37
|
hasMore?: boolean
|
|
35
38
|
/** True while the next page is loading. */
|
|
@@ -131,21 +134,11 @@ function MingoChatHistoryRow({
|
|
|
131
134
|
return (
|
|
132
135
|
<div
|
|
133
136
|
className={cn(
|
|
134
|
-
'group/row
|
|
135
|
-
|
|
136
|
-
|
|
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',
|
|
137
|
+
'group/row flex h-12 items-center bg-ods-bg border-b border-ods-border last:border-b-0',
|
|
138
|
+
'transition-colors hover:bg-ods-bg-hover',
|
|
139
|
+
isActive && 'bg-ods-bg-hover',
|
|
141
140
|
)}
|
|
142
141
|
>
|
|
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}
|
|
149
142
|
<div
|
|
150
143
|
role="button"
|
|
151
144
|
tabIndex={0}
|
|
@@ -156,7 +149,7 @@ function MingoChatHistoryRow({
|
|
|
156
149
|
onSelect?.(dialog.id)
|
|
157
150
|
}
|
|
158
151
|
}}
|
|
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:
|
|
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:bg-ods-bg-hover"
|
|
160
153
|
>
|
|
161
154
|
{unread > 0 ? (
|
|
162
155
|
<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">
|
|
@@ -164,10 +157,7 @@ function MingoChatHistoryRow({
|
|
|
164
157
|
</span>
|
|
165
158
|
) : null}
|
|
166
159
|
<span
|
|
167
|
-
className=
|
|
168
|
-
'min-w-0 flex-1 truncate text-h4',
|
|
169
|
-
isActive ? 'text-ods-open-yellow' : 'text-ods-text-primary',
|
|
170
|
-
)}
|
|
160
|
+
className="min-w-0 flex-1 truncate text-h4 text-ods-text-primary"
|
|
171
161
|
title={title}
|
|
172
162
|
>
|
|
173
163
|
{title}
|
|
@@ -232,39 +222,61 @@ function MingoChatHistoryRow({
|
|
|
232
222
|
*/
|
|
233
223
|
export function MingoChatHistorySkeleton({
|
|
234
224
|
className,
|
|
225
|
+
searchable = false,
|
|
235
226
|
}: {
|
|
236
227
|
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
|
|
237
231
|
}) {
|
|
238
|
-
// Mirror the real grouped list
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// a
|
|
242
|
-
//
|
|
243
|
-
//
|
|
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'],
|
|
232
|
+
// Mirror the real grouped list: a group label, then a bordered container of
|
|
233
|
+
// `h-12` rows. `withBadge` adds the leading unread-badge square on the first
|
|
234
|
+
// N rows; `widths` varies the title-bar length so the placeholder reads like
|
|
235
|
+
// a list of differing-length chat titles, not identical bars. `bg-ods-border`
|
|
236
|
+
// (not `bg-ods-bg-secondary`) so the placeholders are actually visible on the
|
|
237
|
+
// dark surface.
|
|
238
|
+
const groups: ReadonlyArray<{ withBadge: number; widths: ReadonlyArray<string> }> = [
|
|
239
|
+
{ withBadge: 2, widths: ['w-3/5', 'w-4/5', 'w-2/5', 'w-3/4'] },
|
|
240
|
+
{ withBadge: 0, widths: ['w-1/2', 'w-3/5', 'w-2/3'] },
|
|
247
241
|
]
|
|
248
242
|
return (
|
|
249
243
|
<div
|
|
250
244
|
aria-hidden
|
|
251
245
|
// `overflow-hidden` clips the placeholder rows to the flex-1 box (the real
|
|
252
|
-
// list scrolls; the skeleton just needs to stay within bounds).
|
|
246
|
+
// list scrolls; the skeleton just needs to stay within bounds). Without it
|
|
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.
|
|
253
249
|
className={cn(
|
|
254
250
|
'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-hidden',
|
|
255
251
|
className,
|
|
256
252
|
)}
|
|
257
253
|
>
|
|
258
|
-
{
|
|
254
|
+
{/* Search-bar placeholder — mirrors the real `Input` (border, rounded,
|
|
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) => (
|
|
259
264
|
<div key={g} className="flex shrink-0 flex-col gap-[var(--spacing-system-xxs)]">
|
|
260
|
-
{/* Group label (Today /
|
|
265
|
+
{/* Group label (Today / Yesterday). */}
|
|
261
266
|
<div className="h-3 w-16 animate-pulse rounded bg-ods-border" />
|
|
262
267
|
<div className="overflow-hidden rounded-md border border-ods-border">
|
|
263
|
-
{widths.map((w, i) => (
|
|
268
|
+
{group.widths.map((w, i) => (
|
|
264
269
|
<div
|
|
265
270
|
key={i}
|
|
266
|
-
|
|
271
|
+
// Match the real row surface: `bg-ods-bg` (#161616) sits darker
|
|
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"
|
|
267
276
|
>
|
|
277
|
+
{i < group.withBadge && (
|
|
278
|
+
<div className="h-8 w-8 shrink-0 animate-pulse rounded-md bg-ods-border" />
|
|
279
|
+
)}
|
|
268
280
|
<div className={cn('h-4 animate-pulse rounded bg-ods-border', w)} />
|
|
269
281
|
</div>
|
|
270
282
|
))}
|
|
@@ -275,6 +287,46 @@ export function MingoChatHistorySkeleton({
|
|
|
275
287
|
)
|
|
276
288
|
}
|
|
277
289
|
|
|
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
|
+
|
|
278
330
|
// =============================================================================
|
|
279
331
|
// Component
|
|
280
332
|
// =============================================================================
|
|
@@ -297,6 +349,7 @@ export function MingoChatHistory({
|
|
|
297
349
|
onRequestRename,
|
|
298
350
|
onRequestArchive,
|
|
299
351
|
searchQuery,
|
|
352
|
+
onSearchChange,
|
|
300
353
|
hasMore = false,
|
|
301
354
|
isLoadingMore = false,
|
|
302
355
|
onLoadMore,
|
|
@@ -335,8 +388,13 @@ export function MingoChatHistory({
|
|
|
335
388
|
|
|
336
389
|
return (
|
|
337
390
|
<div className={cn('relative flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]', className)}>
|
|
338
|
-
{/*
|
|
339
|
-
|
|
391
|
+
{/* Pinned search bar — stays put while the grouped list scrolls below. */}
|
|
392
|
+
{onSearchChange ? (
|
|
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. */}
|
|
340
398
|
<div className="relative flex flex-1 min-h-0 flex-col">
|
|
341
399
|
<div
|
|
342
400
|
ref={scrollRef}
|
|
@@ -344,13 +402,9 @@ export function MingoChatHistory({
|
|
|
344
402
|
className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
|
|
345
403
|
>
|
|
346
404
|
{noSearchResults ? (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
icon={<SearchXmarkIcon size={24} />}
|
|
351
|
-
title="No Chats Found"
|
|
352
|
-
description="Try a different search term"
|
|
353
|
-
/>
|
|
405
|
+
<p className="py-[var(--spacing-system-m)] text-center text-h5 text-ods-text-secondary">
|
|
406
|
+
No chats found
|
|
407
|
+
</p>
|
|
354
408
|
) : (
|
|
355
409
|
groups.map((group) => (
|
|
356
410
|
<div
|
|
@@ -10,6 +10,10 @@ import { Button } from '../ui/button'
|
|
|
10
10
|
import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
|
|
11
11
|
import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'
|
|
12
12
|
import {
|
|
13
|
+
Rocket01Icon,
|
|
14
|
+
BracketCurlyIcon,
|
|
15
|
+
SearchIcon,
|
|
16
|
+
LayersIcon,
|
|
13
17
|
CompassIcon,
|
|
14
18
|
Arrow01DownIcon,
|
|
15
19
|
AlertCircleIcon,
|
|
@@ -20,6 +24,17 @@ import {
|
|
|
20
24
|
// Types
|
|
21
25
|
// =============================================================================
|
|
22
26
|
|
|
27
|
+
/** A single capability cell in the 2-up feature grid. */
|
|
28
|
+
export interface MingoFeatureCard {
|
|
29
|
+
/** Stable React key. */
|
|
30
|
+
id: string
|
|
31
|
+
/** Leading 16×16 icon (monochrome `ods-text-secondary`). */
|
|
32
|
+
icon?: React.ReactNode
|
|
33
|
+
/** Caption text. Line breaks (`\n`) are honoured (`whitespace-pre-line`)
|
|
34
|
+
* so the two-line wrap stays fixed regardless of panel width. */
|
|
35
|
+
text: React.ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
/** "New to OpenFrame?" one-time notification config. `null` hides the card. */
|
|
24
39
|
export interface MingoWelcomePromo {
|
|
25
40
|
title: React.ReactNode
|
|
@@ -42,10 +57,14 @@ export interface MingoQuickAction {
|
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
export interface MingoWelcomeProps {
|
|
45
|
-
/**
|
|
60
|
+
/** First name woven into the greeting. Empty/undefined → no-name variant. */
|
|
61
|
+
userName?: string
|
|
62
|
+
/** Greeting heading. Defaults to `Hey{, name}, I'm Mingo`. */
|
|
46
63
|
title?: React.ReactNode
|
|
47
64
|
/** Greeting sub-line under the heading. */
|
|
48
65
|
subtitle?: React.ReactNode
|
|
66
|
+
/** 2-up capability grid. Defaults to the four OpenFrame highlights. */
|
|
67
|
+
featureCards?: ReadonlyArray<MingoFeatureCard>
|
|
49
68
|
/** One-time "New to OpenFrame?" notification below the grid. `null` hides
|
|
50
69
|
* it; omitting falls back to the OpenFrame default (only rendered when
|
|
51
70
|
* `onStartGuideChat` is wired, i.e. Guide mode exists to advertise). */
|
|
@@ -83,6 +102,10 @@ export interface MingoWelcomeProps {
|
|
|
83
102
|
loadError?: boolean
|
|
84
103
|
/** Retry handler for the `loadError` state. */
|
|
85
104
|
onRetry?: () => void
|
|
105
|
+
/** Whether the dialog history has a search bar — when true, the loading
|
|
106
|
+
* skeleton includes a search-bar placeholder so it matches the loaded
|
|
107
|
+
* layout. Ignored once `dialogHistory` is provided. */
|
|
108
|
+
historySearchable?: boolean
|
|
86
109
|
/** When provided, renders the "Start Guide Chat" chip (the only wired
|
|
87
110
|
* action — switches the host chat to Guide mode) and enables the default
|
|
88
111
|
* promo notification. When omitted, both are suppressed. */
|
|
@@ -98,6 +121,29 @@ export interface MingoWelcomeProps {
|
|
|
98
121
|
const DEFAULT_SUBTITLE =
|
|
99
122
|
'Ready to help with your technical tasks. What can I do for you?'
|
|
100
123
|
|
|
124
|
+
const DEFAULT_FEATURE_CARDS: ReadonlyArray<MingoFeatureCard> = [
|
|
125
|
+
{
|
|
126
|
+
id: 'answers',
|
|
127
|
+
icon: <Rocket01Icon size={16} />,
|
|
128
|
+
text: 'Get instant answers about\ndevices, tickets, and clients',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'scripts',
|
|
132
|
+
icon: <BracketCurlyIcon size={16} />,
|
|
133
|
+
text: 'Run scripts and queries\nthrough natural language',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'summarize',
|
|
137
|
+
icon: <SearchIcon size={16} />,
|
|
138
|
+
text: 'Summarize long ticket\nthreads or activity history',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'delegate',
|
|
142
|
+
icon: <LayersIcon size={16} />,
|
|
143
|
+
text: 'Delegate tasks, let Mingo\nwork in the background',
|
|
144
|
+
},
|
|
145
|
+
]
|
|
146
|
+
|
|
101
147
|
const DEFAULT_PROMO: MingoWelcomePromo = {
|
|
102
148
|
title: 'New to OpenFrame?',
|
|
103
149
|
description: 'Start a Guide Chat to learn how it works and how to set it up.',
|
|
@@ -110,12 +156,13 @@ const DEFAULT_PROMO_STORAGE_KEY = 'mingo-welcome:promo-dismissed'
|
|
|
110
156
|
// =============================================================================
|
|
111
157
|
|
|
112
158
|
/**
|
|
113
|
-
* MingoWelcome — Figma node `
|
|
159
|
+
* MingoWelcome — Figma node `7532:222444`.
|
|
114
160
|
*
|
|
115
161
|
* Default (Mingo-mode) chat empty state: a vertically-centred greeting that
|
|
116
|
-
* grows to fill available height, then a pinned stack of
|
|
117
|
-
* "New to OpenFrame?" notification and a
|
|
118
|
-
* empty state keeps the slash-command
|
|
162
|
+
* grows to fill available height, then a pinned stack of a 2-up capability
|
|
163
|
+
* grid, an optional one-time "New to OpenFrame?" notification, and a
|
|
164
|
+
* quick-action chip row. The Guide-mode empty state keeps the slash-command
|
|
165
|
+
* onboarding list.
|
|
119
166
|
*
|
|
120
167
|
* Content is configurable (props) with OpenFrame defaults so the kit stays
|
|
121
168
|
* platform-agnostic. The "Start Guide Chat" chip is the only wired action —
|
|
@@ -124,8 +171,10 @@ const DEFAULT_PROMO_STORAGE_KEY = 'mingo-welcome:promo-dismissed'
|
|
|
124
171
|
* storage so it shows only until the user closes it once.
|
|
125
172
|
*/
|
|
126
173
|
export function MingoWelcome({
|
|
174
|
+
userName,
|
|
127
175
|
title,
|
|
128
176
|
subtitle = DEFAULT_SUBTITLE,
|
|
177
|
+
featureCards = DEFAULT_FEATURE_CARDS,
|
|
129
178
|
promo,
|
|
130
179
|
promoStorageKey = DEFAULT_PROMO_STORAGE_KEY,
|
|
131
180
|
promoStorage = 'local',
|
|
@@ -137,12 +186,12 @@ export function MingoWelcome({
|
|
|
137
186
|
isLoadingHistory = false,
|
|
138
187
|
loadError = false,
|
|
139
188
|
onRetry,
|
|
189
|
+
historySearchable = false,
|
|
140
190
|
onStartGuideChat,
|
|
141
191
|
className,
|
|
142
192
|
}: MingoWelcomeProps) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const heading = title ?? "Hey, I'm Mingo"
|
|
193
|
+
const heading =
|
|
194
|
+
title ?? (userName ? `Hey ${userName}, I'm Mingo` : "Hey, I'm Mingo")
|
|
146
195
|
|
|
147
196
|
// `promo` omitted → fall back to the OpenFrame default, but only when guide
|
|
148
197
|
// mode exists to advertise (otherwise the notification points nowhere).
|
|
@@ -187,6 +236,10 @@ export function MingoWelcome({
|
|
|
187
236
|
// only while content is hidden in that direction).
|
|
188
237
|
const { scrollRef, fadeTop, fadeBottom, update: updateScrollFade } = useScrollFade<HTMLDivElement>()
|
|
189
238
|
|
|
239
|
+
const cellCount = featureCards.length
|
|
240
|
+
// Last row's first index — used to drop the bottom divider on the final row.
|
|
241
|
+
const lastRowStart = cellCount - ((cellCount % 2) || 2)
|
|
242
|
+
|
|
190
243
|
// While we don't yet know whether the user is new or returning (first page
|
|
191
244
|
// loading, or it errored with nothing cached), suppress the pinned region —
|
|
192
245
|
// the "New to OpenFrame?" promo + quick-action chips. Showing them over the
|
|
@@ -231,7 +284,7 @@ export function MingoWelcome({
|
|
|
231
284
|
)}
|
|
232
285
|
</div>
|
|
233
286
|
) : isLoadingHistory ? (
|
|
234
|
-
<MingoChatHistorySkeleton />
|
|
287
|
+
<MingoChatHistorySkeleton searchable={historySearchable} />
|
|
235
288
|
) : (
|
|
236
289
|
<>
|
|
237
290
|
{/* Scrollable region — only the greeting + grid scroll; the notification
|
|
@@ -256,18 +309,46 @@ export function MingoWelcome({
|
|
|
256
309
|
eyesColor="var(--ods-flamingo-cyan-base)"
|
|
257
310
|
cornerColor="var(--ods-flamingo-cyan-base)"
|
|
258
311
|
/>
|
|
259
|
-
<div className="flex w-full flex-col gap-
|
|
260
|
-
<p className="text-
|
|
261
|
-
<p className="text-
|
|
312
|
+
<div className="flex w-full flex-col gap-1">
|
|
313
|
+
<p className="text-h4 text-ods-text-primary">{heading}</p>
|
|
314
|
+
<p className="text-h6 text-ods-text-secondary">{subtitle}</p>
|
|
262
315
|
</div>
|
|
263
316
|
</div>
|
|
317
|
+
|
|
318
|
+
{/* 2-up capability grid. `shrink-0` keeps every cell at its natural
|
|
319
|
+
height so the bottom row is never clipped (the root scrolls instead).
|
|
320
|
+
Cells share the lighter `ods-card` surface; 1px `ods-border` dividers
|
|
321
|
+
(right on the left column, bottom on every row but the last) draw the
|
|
322
|
+
inner cross. Captions use explicit `\n` breaks (`whitespace-pre-line`)
|
|
323
|
+
so the two-line wrap is fixed and never reflows with panel width. */}
|
|
324
|
+
{cellCount > 0 && (
|
|
325
|
+
<div className="grid shrink-0 grid-cols-2 overflow-hidden rounded-md border border-ods-border bg-ods-card">
|
|
326
|
+
{featureCards.map((card, i) => (
|
|
327
|
+
<div
|
|
328
|
+
key={card.id}
|
|
329
|
+
className={cn(
|
|
330
|
+
'flex flex-col items-center justify-center gap-[var(--spacing-system-m)] p-[var(--spacing-system-m)] text-center',
|
|
331
|
+
i % 2 === 0 && 'border-r border-ods-border',
|
|
332
|
+
i < lastRowStart && 'border-b border-ods-border',
|
|
333
|
+
)}
|
|
334
|
+
>
|
|
335
|
+
{card.icon ? (
|
|
336
|
+
<span className="flex size-4 shrink-0 items-center justify-center text-ods-text-primary">
|
|
337
|
+
{card.icon}
|
|
338
|
+
</span>
|
|
339
|
+
) : null}
|
|
340
|
+
<p className="text-h4 text-ods-text-secondary whitespace-pre-line">
|
|
341
|
+
{card.text}
|
|
342
|
+
</p>
|
|
343
|
+
</div>
|
|
344
|
+
))}
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
264
347
|
</div>
|
|
265
348
|
|
|
266
|
-
{/* Edge scroll-fades — visible only when content is hidden beyond them.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<ScrollFadeOverlay edge="top" visible={fadeTop} className="h-12" />
|
|
270
|
-
<ScrollFadeOverlay edge="bottom" visible={fadeBottom} className="h-12" />
|
|
349
|
+
{/* Edge scroll-fades — visible only when content is hidden beyond them. */}
|
|
350
|
+
<ScrollFadeOverlay edge="top" visible={fadeTop} color="var(--color-bg-card)" className="h-12" />
|
|
351
|
+
<ScrollFadeOverlay edge="bottom" visible={fadeBottom} color="var(--color-bg-card)" className="h-12" />
|
|
271
352
|
</div>
|
|
272
353
|
</>
|
|
273
354
|
))}
|
|
@@ -26,13 +26,14 @@ const meta: Meta<typeof MingoWelcome> = {
|
|
|
26
26
|
docs: {
|
|
27
27
|
description: {
|
|
28
28
|
component:
|
|
29
|
-
'Figma node `
|
|
29
|
+
'Figma node `7532:222444`. Default (Mingo-mode) chat empty state — a vertically-centred greeting, a 2-up capability grid, an optional "New to OpenFrame?" Guide-chat promo, and a quick-action chip row. Content is configurable with OpenFrame defaults; the only wired action is "Start Guide Chat" (`onStartGuideChat`).',
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
argTypes: {
|
|
34
34
|
title: { control: false },
|
|
35
35
|
subtitle: { control: false },
|
|
36
|
+
featureCards: { control: false },
|
|
36
37
|
promo: { control: false },
|
|
37
38
|
quickActions: { control: false },
|
|
38
39
|
onStartGuideChat: { control: false },
|
|
@@ -60,6 +61,14 @@ export const Default: Story = {
|
|
|
60
61
|
},
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** Greeting personalised with a first name. */
|
|
65
|
+
export const WithUserName: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
userName: 'Grace',
|
|
68
|
+
onStartGuideChat: () => console.log('switch to guide mode'),
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
/** Guide mode not configured → the promo card and "Start Guide Chat" chip are
|
|
64
73
|
* suppressed (no dead-end CTA). */
|
|
65
74
|
export const WithoutGuide: Story = {
|
|
@@ -91,7 +100,7 @@ export const ReturningUser: Story = {
|
|
|
91
100
|
},
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
/** Wide panel (e.g. in-layout / desktop). */
|
|
103
|
+
/** Wide panel (e.g. in-layout / desktop) — the grid columns read roomy. */
|
|
95
104
|
export const Wide: Story = {
|
|
96
105
|
args: {
|
|
97
106
|
onStartGuideChat: () => console.log('switch to guide mode'),
|