@flamingo-stack/openframe-frontend-core 0.0.319 → 0.0.321-snapshot.20260625085235
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-V2OAUUF6.cjs → chunk-27UIZKPP.cjs} +31 -31
- package/dist/{chunk-V2OAUUF6.cjs.map → chunk-27UIZKPP.cjs.map} +1 -1
- package/dist/{chunk-JIU2I6LU.cjs → chunk-2JFIPRZM.cjs} +25 -25
- package/dist/{chunk-JIU2I6LU.cjs.map → chunk-2JFIPRZM.cjs.map} +1 -1
- package/dist/{chunk-2RQV7JLA.js → chunk-2L6XJRWU.js} +2 -2
- package/dist/{chunk-ODMCD663.js → chunk-2ZMEIVHB.js} +4 -4
- package/dist/{chunk-7FIWPFLE.js → chunk-3CUHOXPN.js} +4 -4
- package/dist/{chunk-G7CRSNFS.js → chunk-3NOGHXE5.js} +2 -2
- package/dist/{chunk-7ZHCRY2V.cjs → chunk-6GHEXJ6H.cjs} +5 -5
- package/dist/{chunk-7ZHCRY2V.cjs.map → chunk-6GHEXJ6H.cjs.map} +1 -1
- package/dist/{chunk-J4FCWWO6.cjs → chunk-AOD7KNNL.cjs} +62 -62
- package/dist/{chunk-J4FCWWO6.cjs.map → chunk-AOD7KNNL.cjs.map} +1 -1
- package/dist/{chunk-5B37J3B5.cjs → chunk-APGXPHP2.cjs} +15 -15
- package/dist/{chunk-5B37J3B5.cjs.map → chunk-APGXPHP2.cjs.map} +1 -1
- package/dist/{chunk-NG7P653A.js → chunk-HYW3BYDX.js} +2 -2
- package/dist/{chunk-ARQ3R7WZ.js → chunk-KFEIMA2T.js} +2 -2
- package/dist/{chunk-XAGD3XZV.js → chunk-KJNTCTV4.js} +4 -4
- package/dist/{chunk-APEJR6DV.cjs → chunk-M5RW5ASP.cjs} +26 -26
- package/dist/{chunk-APEJR6DV.cjs.map → chunk-M5RW5ASP.cjs.map} +1 -1
- package/dist/{chunk-7WMHKVV7.cjs → chunk-MQFGEYOG.cjs} +12 -12
- package/dist/{chunk-7WMHKVV7.cjs.map → chunk-MQFGEYOG.cjs.map} +1 -1
- package/dist/{chunk-SVPVDSHA.js → chunk-MQHD6CTN.js} +4280 -4021
- package/dist/chunk-MQHD6CTN.js.map +1 -0
- package/dist/{chunk-CGG7EWYG.cjs → chunk-NCFCQWMH.cjs} +56 -56
- package/dist/{chunk-CGG7EWYG.cjs.map → chunk-NCFCQWMH.cjs.map} +1 -1
- package/dist/{chunk-4XHPUYL7.cjs → chunk-NTEAD4ZM.cjs} +37 -37
- package/dist/{chunk-4XHPUYL7.cjs.map → chunk-NTEAD4ZM.cjs.map} +1 -1
- package/dist/{chunk-QJOSQVIR.js → chunk-PZYSR23M.js} +2 -2
- package/dist/{chunk-LF5LDJOP.js → chunk-S3X2KEFA.js} +2 -2
- package/dist/{chunk-RXCP4OV4.cjs → chunk-S6W5YPAB.cjs} +9 -9
- package/dist/{chunk-RXCP4OV4.cjs.map → chunk-S6W5YPAB.cjs.map} +1 -1
- package/dist/{chunk-ZD5ZDEL5.cjs → chunk-UFUOM6ZT.cjs} +1644 -1385
- package/dist/chunk-UFUOM6ZT.cjs.map +1 -0
- package/dist/{chunk-2D3RINOJ.js → chunk-WQ4OIM4E.js} +2 -2
- package/dist/{chunk-4YFOBTFE.js → chunk-XF3GIE5U.js} +4 -4
- package/dist/{chunk-B34YGPIB.cjs → chunk-YKIU2GXU.cjs} +7 -7
- package/dist/{chunk-B34YGPIB.cjs.map → chunk-YKIU2GXU.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-context-picker.d.ts +11 -1
- package/dist/components/chat/chat-context-picker.d.ts.map +1 -1
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/context-items-list.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts +13 -1
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/mingo-chat-history.d.ts +11 -2
- package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
- package/dist/components/chat/mingo-welcome.d.ts +5 -1
- package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
- package/dist/components/chat/remark-mention-chips.d.ts +11 -5
- package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +30 -7
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +8 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +7 -1
- package/dist/components/features/paths-display.d.ts +16 -1
- package/dist/components/features/paths-display.d.ts.map +1 -1
- package/dist/components/features/warning-block.d.ts +32 -0
- package/dist/components/features/warning-block.d.ts.map +1 -0
- 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 +126 -120
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +15 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +8 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -1
- package/package.json +1 -1
- package/src/components/chat/chat-context-picker.tsx +77 -9
- package/src/components/chat/chat-input.tsx +524 -489
- package/src/components/chat/chat-message-enhanced.tsx +4 -2
- package/src/components/chat/chat-message-list.tsx +2 -0
- package/src/components/chat/context-items-list.tsx +9 -1
- package/src/components/chat/embeddable-chat.tsx +70 -57
- package/src/components/chat/mingo-chat-history.tsx +136 -53
- package/src/components/chat/mingo-welcome.tsx +6 -1
- package/src/components/chat/remark-mention-chips.ts +13 -7
- package/src/components/chat/types/component.types.ts +31 -7
- package/src/components/features/index.ts +1 -0
- package/src/components/features/paths-display.tsx +32 -1
- package/src/components/features/warning-block.tsx +54 -0
- package/src/stories/WarningBlock.stories.tsx +167 -0
- package/dist/chunk-SVPVDSHA.js.map +0 -1
- package/dist/chunk-ZD5ZDEL5.cjs.map +0 -1
- /package/dist/{chunk-2RQV7JLA.js.map → chunk-2L6XJRWU.js.map} +0 -0
- /package/dist/{chunk-ODMCD663.js.map → chunk-2ZMEIVHB.js.map} +0 -0
- /package/dist/{chunk-7FIWPFLE.js.map → chunk-3CUHOXPN.js.map} +0 -0
- /package/dist/{chunk-G7CRSNFS.js.map → chunk-3NOGHXE5.js.map} +0 -0
- /package/dist/{chunk-NG7P653A.js.map → chunk-HYW3BYDX.js.map} +0 -0
- /package/dist/{chunk-ARQ3R7WZ.js.map → chunk-KFEIMA2T.js.map} +0 -0
- /package/dist/{chunk-XAGD3XZV.js.map → chunk-KJNTCTV4.js.map} +0 -0
- /package/dist/{chunk-QJOSQVIR.js.map → chunk-PZYSR23M.js.map} +0 -0
- /package/dist/{chunk-LF5LDJOP.js.map → chunk-S3X2KEFA.js.map} +0 -0
- /package/dist/{chunk-2D3RINOJ.js.map → chunk-WQ4OIM4E.js.map} +0 -0
- /package/dist/{chunk-4YFOBTFE.js.map → chunk-XF3GIE5U.js.map} +0 -0
|
@@ -25,7 +25,7 @@ import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from ".
|
|
|
25
25
|
* item would be stripped from the chip strip yet never rendered inline — lost
|
|
26
26
|
* from display entirely. The id is capture group 2 (group 1 is the boundary).
|
|
27
27
|
* Marker lowercase; id is the mention-token charset. */
|
|
28
|
-
const MENTION_MARKER_REGEX = /(
|
|
28
|
+
const MENTION_MARKER_REGEX = /(^|[^\w@])@[a-z]+:([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Same regex shape as `remarkCardLinks` — kept in lockstep so the
|
|
@@ -43,7 +43,7 @@ function normalizeContent(content: MessageContent): MessageSegment[] {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>(
|
|
46
|
-
({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderMention, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
|
|
46
|
+
({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderContextItem, renderMention, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
|
|
47
47
|
const isUser = role === 'user'
|
|
48
48
|
const isError = role === 'error'
|
|
49
49
|
const authorType = authorTypeProp ?? (isUser ? 'user' : assistantType === 'mingo' ? 'mingo' : 'fae')
|
|
@@ -595,6 +595,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
595
595
|
<ChatContextChipStrip
|
|
596
596
|
items={stripContextItems}
|
|
597
597
|
resolveIcon={resolveContextIcon}
|
|
598
|
+
renderItem={renderContextItem}
|
|
598
599
|
className="mt-2"
|
|
599
600
|
/>
|
|
600
601
|
)}
|
|
@@ -628,6 +629,7 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
|
|
|
628
629
|
// message (it's set once on the optimistic send and never mutated).
|
|
629
630
|
prevProps.contextItems === nextProps.contextItems &&
|
|
630
631
|
prevProps.resolveContextIcon === nextProps.resolveContextIcon &&
|
|
632
|
+
prevProps.renderContextItem === nextProps.renderContextItem &&
|
|
631
633
|
// Host keeps this stable (module const / useCallback), so reference
|
|
632
634
|
// equality holds across streaming chunks.
|
|
633
635
|
prevProps.renderMention === nextProps.renderMention &&
|
|
@@ -107,6 +107,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
107
107
|
onLoadMore,
|
|
108
108
|
renderEntityCard,
|
|
109
109
|
resolveContextIcon,
|
|
110
|
+
renderContextItem,
|
|
110
111
|
renderMention,
|
|
111
112
|
NavLinkAnchor,
|
|
112
113
|
...props
|
|
@@ -539,6 +540,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
539
540
|
chatRefs={message.chatRefs}
|
|
540
541
|
contextItems={message.contextItems}
|
|
541
542
|
resolveContextIcon={resolveContextIcon}
|
|
543
|
+
renderContextItem={renderContextItem}
|
|
542
544
|
renderMention={renderMention}
|
|
543
545
|
renderEntityCard={renderEntityCard}
|
|
544
546
|
NavLinkAnchor={NavLinkAnchor}
|
|
@@ -141,7 +141,15 @@ export function ContextItemsList({
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
return (
|
|
144
|
-
<div
|
|
144
|
+
<div
|
|
145
|
+
role="listbox"
|
|
146
|
+
aria-label="Items"
|
|
147
|
+
onScroll={onScroll}
|
|
148
|
+
// Cap to 340px but never exceed ~half the viewport, so on short screens the
|
|
149
|
+
// results list (inside the bottom-anchored picker popover) still fits and
|
|
150
|
+
// scrolls instead of overflowing past the top edge.
|
|
151
|
+
className={cn('max-h-[min(340px,45vh)] overflow-y-auto', className)}
|
|
152
|
+
>
|
|
145
153
|
{items.map(item => {
|
|
146
154
|
const selected = selectedKeys.has(itemKey(item))
|
|
147
155
|
return (
|
|
@@ -196,6 +196,8 @@ export interface EmbeddableChatProps {
|
|
|
196
196
|
params: FetchDialogsParams,
|
|
197
197
|
) => Promise<FetchDialogsResult>
|
|
198
198
|
unarchiveDialog?: (id: string) => Promise<void>
|
|
199
|
+
searchQuery?: string
|
|
200
|
+
onSearchChange?: (query: string) => void
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
/**
|
|
@@ -271,6 +273,16 @@ export interface EmbeddableChatProps {
|
|
|
271
273
|
* marker the host can't render → the lib falls back to the bare token.
|
|
272
274
|
*/
|
|
273
275
|
renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
|
|
276
|
+
/**
|
|
277
|
+
* Host renderer that REPLACES the default label-only context chip on a sent
|
|
278
|
+
* user bubble with a self-fetching entity chip — so a user's manually
|
|
279
|
+
* attached context (`contextItems`) renders IDENTICALLY to an inline
|
|
280
|
+
* `@marker:id` mention (same live name resolution + link). Mirror of
|
|
281
|
+
* `renderMention`, for the attached-chip strip instead of inline tokens.
|
|
282
|
+
* Return null for an item the host can't render → the lib falls back to the
|
|
283
|
+
* label pill. Keep the identity stable (module const / `useCallback`).
|
|
284
|
+
*/
|
|
285
|
+
renderContextItem?: (item: ChatContextItem) => React.ReactNode
|
|
274
286
|
}
|
|
275
287
|
|
|
276
288
|
// =============================================================================
|
|
@@ -691,6 +703,7 @@ function EmbeddableChatInner({
|
|
|
691
703
|
guideWelcome,
|
|
692
704
|
contextPicker,
|
|
693
705
|
renderMention,
|
|
706
|
+
renderContextItem,
|
|
694
707
|
}: EmbeddableChatProps) {
|
|
695
708
|
// `shell === 'none'` means the consumer hosts us inside their own panel
|
|
696
709
|
// (e.g. AppLayoutDrawer in openframe-frontend). Several drawer-shell
|
|
@@ -844,6 +857,8 @@ function EmbeddableChatInner({
|
|
|
844
857
|
canArchive: mingoDialogCapabilities?.canArchive ?? false,
|
|
845
858
|
fetchArchivedDialogs: mingoDialogCapabilities?.fetchArchivedDialogs,
|
|
846
859
|
unarchiveDialog: mingoDialogCapabilities?.unarchiveDialog,
|
|
860
|
+
searchQuery: mingoDialogCapabilities?.searchQuery,
|
|
861
|
+
onSearchChange: mingoDialogCapabilities?.onSearchChange,
|
|
847
862
|
}
|
|
848
863
|
}
|
|
849
864
|
return {
|
|
@@ -851,6 +866,8 @@ function EmbeddableChatInner({
|
|
|
851
866
|
canArchive: !!effectiveModes.mingo?.archiveDialog,
|
|
852
867
|
fetchArchivedDialogs: effectiveModes.mingo?.fetchArchivedDialogs,
|
|
853
868
|
unarchiveDialog: effectiveModes.mingo?.unarchiveDialog,
|
|
869
|
+
searchQuery: undefined as string | undefined,
|
|
870
|
+
onSearchChange: undefined as ((query: string) => void) | undefined,
|
|
854
871
|
}
|
|
855
872
|
}, [mingoState, mingoDialogCapabilities, effectiveModes])
|
|
856
873
|
|
|
@@ -986,9 +1003,10 @@ function EmbeddableChatInner({
|
|
|
986
1003
|
useEffect(() => {
|
|
987
1004
|
mentionActiveRef.current = mentionQuery !== null
|
|
988
1005
|
}, [mentionQuery])
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
|
|
1006
|
+
// Keys (`"<type>:<id>"`) of context items that were committed via `@` and so
|
|
1007
|
+
// have a `@type:id` token in the draft text. MULTI-mention: several coexist.
|
|
1008
|
+
// Source of truth = the input text — deleting a token's chip drops its item.
|
|
1009
|
+
const mentionKeysRef = useRef<Set<string>>(new Set())
|
|
992
1010
|
|
|
993
1011
|
// Staged picker selection is per-conversation and Mingo-only. Clear it (and the
|
|
994
1012
|
// mention bookkeeping) whenever the active dialog changes or the mode toggles,
|
|
@@ -998,7 +1016,7 @@ function EmbeddableChatInner({
|
|
|
998
1016
|
useEffect(() => {
|
|
999
1017
|
setContextItems([])
|
|
1000
1018
|
setMentionQuery(null)
|
|
1001
|
-
|
|
1019
|
+
mentionKeysRef.current.clear()
|
|
1002
1020
|
}, [activeDialogId, activeMode])
|
|
1003
1021
|
|
|
1004
1022
|
// Map each entity type to its backend mention marker (host-declared on the
|
|
@@ -1010,35 +1028,35 @@ function EmbeddableChatInner({
|
|
|
1010
1028
|
return m
|
|
1011
1029
|
}, [contextPicker?.entityTypes])
|
|
1012
1030
|
|
|
1031
|
+
// Stable type → icon resolver (entity-type glyph). Used both for the inline
|
|
1032
|
+
// composer chips (`commitMention` meta) and the message-bubble context chips.
|
|
1033
|
+
// Keyed on `entityTypes` only so an inline `contextPicker={{…}}` doesn't
|
|
1034
|
+
// rebuild it every render (its identity flows into the message memo).
|
|
1035
|
+
const contextEntityTypes = contextPicker?.entityTypes
|
|
1036
|
+
const resolveContextIcon = useMemo(() => {
|
|
1037
|
+
const byType = new Map<string, React.ReactNode>()
|
|
1038
|
+
for (const t of contextEntityTypes ?? []) byType.set(t.type, t.icon)
|
|
1039
|
+
return (item: ChatContextItem) => byType.get(item.type)
|
|
1040
|
+
}, [contextEntityTypes])
|
|
1041
|
+
|
|
1013
1042
|
const toggleContextItem = useCallback(
|
|
1014
1043
|
(item: ChatContextItem) => {
|
|
1015
1044
|
const key = `${item.type}:${item.id}`
|
|
1016
|
-
// `@`-mention flow →
|
|
1017
|
-
// (
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1045
|
+
// `@`-mention flow → MULTI-select: commit a `@type:id` chip into the draft
|
|
1046
|
+
// (prior mentions left in place) so each picked entity stays visible inline
|
|
1047
|
+
// AND rides out in the context. The `+` flow (mention inactive) is the same
|
|
1048
|
+
// multi-select on the chip strip.
|
|
1020
1049
|
if (mentionActiveRef.current) {
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
(
|
|
1028
|
-
)
|
|
1029
|
-
const grows = !alreadySelected && !mentionKeyRef.current
|
|
1030
|
-
if (grows && contextItems.length >= contextMaxItems) return
|
|
1031
|
-
chatInputRef.current?.commitMention(mentionTokenOf(key, mentionMarkerByType))
|
|
1032
|
-
setContextItems((prev) => {
|
|
1033
|
-
const withoutPrevMention = mentionKeyRef.current
|
|
1034
|
-
? prev.filter((p) => `${p.type}:${p.id}` !== mentionKeyRef.current)
|
|
1035
|
-
: prev
|
|
1036
|
-
const deduped = withoutPrevMention.filter(
|
|
1037
|
-
(p) => `${p.type}:${p.id}` !== key,
|
|
1038
|
-
)
|
|
1039
|
-
return [...deduped, item]
|
|
1050
|
+
// Already attached → no-op (the picker's `@` flow is add-then-close; the
|
|
1051
|
+
// chip already lives in the draft). Honour `maxItems` on growth.
|
|
1052
|
+
if (contextItems.some((p) => `${p.type}:${p.id}` === key)) return
|
|
1053
|
+
if (contextItems.length >= contextMaxItems) return
|
|
1054
|
+
chatInputRef.current?.commitMention(mentionTokenOf(key, mentionMarkerByType), {
|
|
1055
|
+
label: item.label,
|
|
1056
|
+
icon: resolveContextIcon(item),
|
|
1040
1057
|
})
|
|
1041
|
-
|
|
1058
|
+
setContextItems((prev) => [...prev, item])
|
|
1059
|
+
mentionKeysRef.current.add(key)
|
|
1042
1060
|
return
|
|
1043
1061
|
}
|
|
1044
1062
|
setContextItems((prev) => {
|
|
@@ -1048,32 +1066,32 @@ function EmbeddableChatInner({
|
|
|
1048
1066
|
return [...prev, item]
|
|
1049
1067
|
})
|
|
1050
1068
|
},
|
|
1051
|
-
[contextMaxItems, contextItems, mentionMarkerByType],
|
|
1069
|
+
[contextMaxItems, contextItems, mentionMarkerByType, resolveContextIcon],
|
|
1052
1070
|
)
|
|
1053
1071
|
|
|
1054
1072
|
const removeContextItem = useCallback((item: ChatContextItem) => {
|
|
1055
1073
|
const key = `${item.type}:${item.id}`
|
|
1056
1074
|
setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))
|
|
1057
|
-
// Removing
|
|
1058
|
-
// draft, keeping text and context in lockstep (the inverse of deleting
|
|
1059
|
-
//
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1075
|
+
// Removing a mention-backed chip must also strip its `@type:id` token from
|
|
1076
|
+
// the draft, keeping text and context in lockstep (the inverse of deleting
|
|
1077
|
+
// the chip in the input, which drops the item via `handleContextValueChange`).
|
|
1078
|
+
if (mentionKeysRef.current.has(key)) {
|
|
1079
|
+
mentionKeysRef.current.delete(key)
|
|
1062
1080
|
const cur = chatInputRef.current?.getValue() ?? ''
|
|
1063
1081
|
const next = cur.replace(`@${mentionTokenOf(key, mentionMarkerByType)}`, '').replace(/\s{2,}/g, ' ').trimStart()
|
|
1064
1082
|
chatInputRef.current?.setValue(next)
|
|
1065
1083
|
}
|
|
1066
1084
|
}, [mentionMarkerByType])
|
|
1067
1085
|
|
|
1068
|
-
// Draft → context reconciliation: when the user deletes
|
|
1069
|
-
//
|
|
1070
|
-
// text and are
|
|
1086
|
+
// Draft → context reconciliation: when the user deletes a `@type:id` chip from
|
|
1087
|
+
// the input, drop the matching mention item. Checks EVERY token-backed key
|
|
1088
|
+
// (multi-mention). `+`-added items have no token in the text and are untouched.
|
|
1071
1089
|
const handleContextValueChange = useCallback((value: string) => {
|
|
1072
|
-
const key
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1090
|
+
for (const key of Array.from(mentionKeysRef.current)) {
|
|
1091
|
+
if (!value.includes(`@${mentionTokenOf(key, mentionMarkerByType)}`)) {
|
|
1092
|
+
mentionKeysRef.current.delete(key)
|
|
1093
|
+
setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))
|
|
1094
|
+
}
|
|
1077
1095
|
}
|
|
1078
1096
|
}, [mentionMarkerByType])
|
|
1079
1097
|
|
|
@@ -1102,20 +1120,6 @@ function EmbeddableChatInner({
|
|
|
1102
1120
|
setContextPickerOpen(query !== null)
|
|
1103
1121
|
}, [])
|
|
1104
1122
|
|
|
1105
|
-
// Stable type → icon resolver for the message-bubble context chips. Built
|
|
1106
|
-
// from the host's `contextPicker.entityTypes`; null-safe when the feature
|
|
1107
|
-
// is off. Keyed on `entityTypes` (NOT the whole `contextPicker` object) so an
|
|
1108
|
-
// inline `contextPicker={{…}}` on the host doesn't rebuild this resolver every
|
|
1109
|
-
// render — that identity flows into the `ChatMessageEnhanced` memo, and a new
|
|
1110
|
-
// function each render would force a full markdown re-render of every message
|
|
1111
|
-
// on each keystroke.
|
|
1112
|
-
const contextEntityTypes = contextPicker?.entityTypes
|
|
1113
|
-
const resolveContextIcon = useMemo(() => {
|
|
1114
|
-
const byType = new Map<string, React.ReactNode>()
|
|
1115
|
-
for (const t of contextEntityTypes ?? []) byType.set(t.type, t.icon)
|
|
1116
|
-
return (item: ChatContextItem) => byType.get(item.type)
|
|
1117
|
-
}, [contextEntityTypes])
|
|
1118
|
-
|
|
1119
1123
|
// `renderMention` is HOST-provided (see the prop doc): the per-type renderer
|
|
1120
1124
|
// for inline AI mentions `@marker:id` (mirror of `renderEntityCard`). The lib
|
|
1121
1125
|
// forwards it verbatim to the message list and calls it from the `mention://`
|
|
@@ -1580,6 +1584,7 @@ function EmbeddableChatInner({
|
|
|
1580
1584
|
assistantIcon={mingoAssistantIcon}
|
|
1581
1585
|
renderEntityCard={renderEntityCard}
|
|
1582
1586
|
resolveContextIcon={resolveContextIcon}
|
|
1587
|
+
renderContextItem={renderContextItem}
|
|
1583
1588
|
renderMention={renderMention}
|
|
1584
1589
|
NavLinkAnchor={NavLinkAnchorViaRuntime}
|
|
1585
1590
|
className="flex-1"
|
|
@@ -1633,8 +1638,14 @@ function EmbeddableChatInner({
|
|
|
1633
1638
|
isLoadingHistory={dialogsInitialLoading}
|
|
1634
1639
|
loadError={dialogsLoadError}
|
|
1635
1640
|
onRetry={reloadDialogs}
|
|
1641
|
+
historySearchable={!!mingoCaps.onSearchChange}
|
|
1636
1642
|
dialogHistory={
|
|
1637
|
-
|
|
1643
|
+
// Keep the history (and its search bar) mounted during an
|
|
1644
|
+
// active search even at 0 results — otherwise a no-match
|
|
1645
|
+
// query would unmount the bar and flash the new-user
|
|
1646
|
+
// greeting. `MingoWelcome` renders `dialogHistory` with top
|
|
1647
|
+
// priority, so this wins over the greeting.
|
|
1648
|
+
dialogs.length > 0 || !!mingoCaps.searchQuery ? (
|
|
1638
1649
|
<MingoChatHistory
|
|
1639
1650
|
dialogs={dialogs}
|
|
1640
1651
|
onSelectDialog={handleSelectDialog}
|
|
@@ -1644,6 +1655,8 @@ function EmbeddableChatInner({
|
|
|
1644
1655
|
onRequestArchive={
|
|
1645
1656
|
mingoCaps.canArchive ? setArchiveTarget : undefined
|
|
1646
1657
|
}
|
|
1658
|
+
searchQuery={mingoCaps.searchQuery}
|
|
1659
|
+
onSearchChange={mingoCaps.onSearchChange}
|
|
1647
1660
|
hasMore={hasMoreDialogs}
|
|
1648
1661
|
isLoadingMore={isDialogsLoading && dialogs.length > 0}
|
|
1649
1662
|
onLoadMore={() => {
|
|
@@ -2,9 +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'
|
|
5
6
|
import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
|
|
6
7
|
import { Button } from '../ui/button'
|
|
7
|
-
import {
|
|
8
|
+
import { Input } from '../ui/input'
|
|
9
|
+
import { Ellipsis01Icon, SearchIcon } from '../icons-v2-generated'
|
|
8
10
|
import type { DialogItem } from './types/component.types'
|
|
9
11
|
|
|
10
12
|
// =============================================================================
|
|
@@ -24,6 +26,12 @@ export interface MingoChatHistoryProps {
|
|
|
24
26
|
/** Request archive — enables the row "Archive chat" action. The host opens
|
|
25
27
|
* the Archive confirmation modal. */
|
|
26
28
|
onRequestArchive?: (dialog: DialogItem) => void
|
|
29
|
+
/** Seed value for the search input. The host owns the search term and refetches
|
|
30
|
+
* the dialog list server-side; the list itself does no filtering. */
|
|
31
|
+
searchQuery?: string
|
|
32
|
+
/** Emit the (debounced) search term — enables the dialog search bar above the
|
|
33
|
+
* list. Omit to hide the search bar entirely. */
|
|
34
|
+
onSearchChange?: (query: string) => void
|
|
27
35
|
/** Whether more dialogs remain (cursor pagination). */
|
|
28
36
|
hasMore?: boolean
|
|
29
37
|
/** True while the next page is loading. */
|
|
@@ -211,7 +219,15 @@ function MingoChatHistoryRow({
|
|
|
211
219
|
* two layouts are mutually exclusive — swapping between them mid-load reads as
|
|
212
220
|
* a flicker). Pure ODS tokens, `animate-pulse`.
|
|
213
221
|
*/
|
|
214
|
-
export function MingoChatHistorySkeleton({
|
|
222
|
+
export function MingoChatHistorySkeleton({
|
|
223
|
+
className,
|
|
224
|
+
searchable = false,
|
|
225
|
+
}: {
|
|
226
|
+
className?: string
|
|
227
|
+
/** Render a search-bar placeholder above the groups — match the real list,
|
|
228
|
+
* which shows the search bar only when search is wired. */
|
|
229
|
+
searchable?: boolean
|
|
230
|
+
}) {
|
|
215
231
|
// Mirror the real grouped list: a group label, then a bordered container of
|
|
216
232
|
// `h-12` rows. `withBadge` adds the leading unread-badge square on the first
|
|
217
233
|
// N rows; `widths` varies the title-bar length so the placeholder reads like
|
|
@@ -234,6 +250,15 @@ export function MingoChatHistorySkeleton({ className }: { className?: string })
|
|
|
234
250
|
className,
|
|
235
251
|
)}
|
|
236
252
|
>
|
|
253
|
+
{/* Search-bar placeholder — mirrors the real `Input` (border, rounded,
|
|
254
|
+
h-11/12) with a leading icon + text bar so the loading state matches
|
|
255
|
+
the searchable layout. */}
|
|
256
|
+
{searchable && (
|
|
257
|
+
<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">
|
|
258
|
+
<div className="size-4 md:size-6 shrink-0 animate-pulse rounded bg-ods-border" />
|
|
259
|
+
<div className="h-4 w-32 animate-pulse rounded bg-ods-border" />
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
237
262
|
{groups.map((group, g) => (
|
|
238
263
|
<div key={g} className="flex shrink-0 flex-col gap-[var(--spacing-system-xxs)]">
|
|
239
264
|
{/* Group label (Today / Yesterday). */}
|
|
@@ -261,6 +286,46 @@ export function MingoChatHistorySkeleton({ className }: { className?: string })
|
|
|
261
286
|
)
|
|
262
287
|
}
|
|
263
288
|
|
|
289
|
+
// =============================================================================
|
|
290
|
+
// Search bar
|
|
291
|
+
// =============================================================================
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Dialog search field. Holds its own input text for snappy typing and emits
|
|
295
|
+
* the DEBOUNCED term via `onSearchChange` — the host owns the search state
|
|
296
|
+
* and refetches the dialog list server-side, so the list never
|
|
297
|
+
* filters locally. `initialValue` only seeds the field on mount.
|
|
298
|
+
*/
|
|
299
|
+
function DialogSearchBar({
|
|
300
|
+
initialValue,
|
|
301
|
+
onSearchChange,
|
|
302
|
+
}: {
|
|
303
|
+
initialValue?: string
|
|
304
|
+
onSearchChange: (query: string) => void
|
|
305
|
+
}) {
|
|
306
|
+
const [value, setValue] = React.useState(initialValue ?? '')
|
|
307
|
+
const debounced = useDebounce(value, 300)
|
|
308
|
+
const lastEmitted = React.useRef(initialValue ?? '')
|
|
309
|
+
|
|
310
|
+
React.useEffect(() => {
|
|
311
|
+
if (debounced === lastEmitted.current) return
|
|
312
|
+
lastEmitted.current = debounced
|
|
313
|
+
onSearchChange(debounced)
|
|
314
|
+
}, [debounced, onSearchChange])
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<Input
|
|
318
|
+
type="text"
|
|
319
|
+
value={value}
|
|
320
|
+
onChange={(e) => setValue(e.target.value)}
|
|
321
|
+
placeholder="Search for Chats"
|
|
322
|
+
aria-label="Search chats"
|
|
323
|
+
startAdornment={<SearchIcon />}
|
|
324
|
+
className="shrink-0"
|
|
325
|
+
/>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
264
329
|
// =============================================================================
|
|
265
330
|
// Component
|
|
266
331
|
// =============================================================================
|
|
@@ -282,12 +347,15 @@ export function MingoChatHistory({
|
|
|
282
347
|
onSelectDialog,
|
|
283
348
|
onRequestRename,
|
|
284
349
|
onRequestArchive,
|
|
350
|
+
searchQuery,
|
|
351
|
+
onSearchChange,
|
|
285
352
|
hasMore = false,
|
|
286
353
|
isLoadingMore = false,
|
|
287
354
|
onLoadMore,
|
|
288
355
|
className,
|
|
289
356
|
}: MingoChatHistoryProps) {
|
|
290
357
|
const groups = React.useMemo(() => groupDialogs(dialogs), [dialogs])
|
|
358
|
+
const noSearchResults = groups.length === 0 && !!searchQuery?.trim()
|
|
291
359
|
|
|
292
360
|
// Scroll-fade affordances (same pattern as MingoWelcome's greeting region).
|
|
293
361
|
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
@@ -333,58 +401,73 @@ export function MingoChatHistory({
|
|
|
333
401
|
}, [hasMore])
|
|
334
402
|
|
|
335
403
|
return (
|
|
336
|
-
<div className={cn('relative flex flex-1 min-h-0 flex-col', className)}>
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
>
|
|
342
|
-
{groups.map((group) => (
|
|
343
|
-
<div
|
|
344
|
-
key={group.key}
|
|
345
|
-
className="flex flex-col gap-[var(--spacing-system-xxs)]"
|
|
346
|
-
>
|
|
347
|
-
<p className="text-h5 text-ods-text-secondary">{group.label}</p>
|
|
348
|
-
<div className="overflow-hidden rounded-md border border-ods-border">
|
|
349
|
-
{group.items.map((dialog) => (
|
|
350
|
-
<MingoChatHistoryRow
|
|
351
|
-
key={dialog.id}
|
|
352
|
-
dialog={dialog}
|
|
353
|
-
isActive={dialog.id === activeDialogId}
|
|
354
|
-
onSelect={onSelectDialog}
|
|
355
|
-
onRequestRename={onRequestRename}
|
|
356
|
-
onRequestArchive={onRequestArchive}
|
|
357
|
-
/>
|
|
358
|
-
))}
|
|
359
|
-
</div>
|
|
360
|
-
</div>
|
|
361
|
-
))}
|
|
362
|
-
{hasMore ? <div ref={sentinelRef} className="h-px shrink-0" /> : null}
|
|
363
|
-
</div>
|
|
404
|
+
<div className={cn('relative flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]', className)}>
|
|
405
|
+
{/* Pinned search bar — stays put while the grouped list scrolls below. */}
|
|
406
|
+
{onSearchChange ? (
|
|
407
|
+
<DialogSearchBar initialValue={searchQuery} onSearchChange={onSearchChange} />
|
|
408
|
+
) : null}
|
|
364
409
|
|
|
365
|
-
{/* Scroll
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
410
|
+
{/* Scroll region — `relative` so the fades anchor here (below the search
|
|
411
|
+
bar), not over it. */}
|
|
412
|
+
<div className="relative flex flex-1 min-h-0 flex-col">
|
|
413
|
+
<div
|
|
414
|
+
ref={scrollRef}
|
|
415
|
+
onScroll={updateFade}
|
|
416
|
+
className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
|
|
417
|
+
>
|
|
418
|
+
{noSearchResults ? (
|
|
419
|
+
<p className="py-[var(--spacing-system-m)] text-center text-h5 text-ods-text-secondary">
|
|
420
|
+
No chats found
|
|
421
|
+
</p>
|
|
422
|
+
) : (
|
|
423
|
+
groups.map((group) => (
|
|
424
|
+
<div
|
|
425
|
+
key={group.key}
|
|
426
|
+
className="flex flex-col gap-[var(--spacing-system-xxs)]"
|
|
427
|
+
>
|
|
428
|
+
<p className="text-h5 text-ods-text-secondary">{group.label}</p>
|
|
429
|
+
<div className="overflow-hidden rounded-md border border-ods-border">
|
|
430
|
+
{group.items.map((dialog) => (
|
|
431
|
+
<MingoChatHistoryRow
|
|
432
|
+
key={dialog.id}
|
|
433
|
+
dialog={dialog}
|
|
434
|
+
isActive={dialog.id === activeDialogId}
|
|
435
|
+
onSelect={onSelectDialog}
|
|
436
|
+
onRequestRename={onRequestRename}
|
|
437
|
+
onRequestArchive={onRequestArchive}
|
|
438
|
+
/>
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
))
|
|
443
|
+
)}
|
|
444
|
+
{hasMore ? <div ref={sentinelRef} className="h-px shrink-0" /> : null}
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
{/* Scroll-fade — only while content is hidden in that direction. */}
|
|
448
|
+
<div
|
|
449
|
+
aria-hidden
|
|
450
|
+
className={cn(
|
|
451
|
+
'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
|
|
452
|
+
fade.top ? 'opacity-100' : 'opacity-0',
|
|
453
|
+
)}
|
|
454
|
+
style={{
|
|
455
|
+
background:
|
|
456
|
+
'linear-gradient(0deg, transparent 0%, var(--color-bg-card) 100%)',
|
|
457
|
+
}}
|
|
458
|
+
/>
|
|
459
|
+
<div
|
|
460
|
+
aria-hidden
|
|
461
|
+
className={cn(
|
|
462
|
+
'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
|
|
463
|
+
fade.bottom ? 'opacity-100' : 'opacity-0',
|
|
464
|
+
)}
|
|
465
|
+
style={{
|
|
466
|
+
background:
|
|
467
|
+
'linear-gradient(180deg, transparent 0%, var(--color-bg-card) 100%)',
|
|
468
|
+
}}
|
|
469
|
+
/>
|
|
470
|
+
</div>
|
|
388
471
|
</div>
|
|
389
472
|
)
|
|
390
473
|
}
|
|
@@ -92,6 +92,10 @@ export interface MingoWelcomeProps {
|
|
|
92
92
|
loadError?: boolean
|
|
93
93
|
/** Retry handler for the `loadError` state. */
|
|
94
94
|
onRetry?: () => void
|
|
95
|
+
/** Whether the dialog history has a search bar — when true, the loading
|
|
96
|
+
* skeleton includes a search-bar placeholder so it matches the loaded
|
|
97
|
+
* layout. Ignored once `dialogHistory` is provided. */
|
|
98
|
+
historySearchable?: boolean
|
|
95
99
|
/** When provided, renders the "Start Guide Chat" chip (the only wired
|
|
96
100
|
* action — switches the host chat to Guide mode) and enables the default
|
|
97
101
|
* promo notification. When omitted, both are suppressed. */
|
|
@@ -170,6 +174,7 @@ export function MingoWelcome({
|
|
|
170
174
|
isLoadingHistory = false,
|
|
171
175
|
loadError = false,
|
|
172
176
|
onRetry,
|
|
177
|
+
historySearchable = false,
|
|
173
178
|
onStartGuideChat,
|
|
174
179
|
className,
|
|
175
180
|
}: MingoWelcomeProps) {
|
|
@@ -287,7 +292,7 @@ export function MingoWelcome({
|
|
|
287
292
|
)}
|
|
288
293
|
</div>
|
|
289
294
|
) : isLoadingHistory ? (
|
|
290
|
-
<MingoChatHistorySkeleton />
|
|
295
|
+
<MingoChatHistorySkeleton searchable={historySearchable} />
|
|
291
296
|
) : (
|
|
292
297
|
<>
|
|
293
298
|
{/* Scrollable region — only the greeting + grid scroll; the notification
|
|
@@ -18,18 +18,24 @@
|
|
|
18
18
|
* - marker: `[a-z]+` — the backend `ContextItemType.marker()` short form
|
|
19
19
|
* (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,
|
|
20
20
|
* `query`, …). Always lowercase.
|
|
21
|
-
* - id: `[A-Za-z0-9_.+/=-]
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* - id: charset `[A-Za-z0-9_.+/=-]` (UUIDs with hyphens, fleet numeric ids,
|
|
22
|
+
* base64 global ids, etc.; matches the composer's `MENTION_TOKEN`) — but the
|
|
23
|
+
* id may NOT END in `.` or `-`, so a trailing SENTENCE period/dash isn't
|
|
24
|
+
* swallowed into the id (`@script:38.` → id `38`, with the `.` left as text).
|
|
25
|
+
* Base64 padding `=` and `+`/`/` are still valid final chars.
|
|
26
|
+
* - A LEFT boundary (start-of-text or any non-word, non-`@` character) keeps
|
|
27
|
+
* `@marker:` from matching inside emails / mid-word (`user@host:1234` never
|
|
28
|
+
* starts a token because the `@` is preceded by a word char), while still
|
|
29
|
+
* firing when the agent wraps a mention in punctuation — parentheses,
|
|
30
|
+
* brackets, quotes — e.g. `(@device:64f0a1)`. The boundary character (if
|
|
31
|
+
* any) is captured as `lead` and re-emitted as text, so it is preserved.
|
|
26
32
|
*/
|
|
27
33
|
|
|
28
34
|
import type { Plugin } from 'unified'
|
|
29
35
|
import type { Root, Text, Link } from 'mdast'
|
|
30
36
|
import { visit, SKIP } from 'unist-util-visit'
|
|
31
37
|
|
|
32
|
-
const MENTION_REGEX = /(
|
|
38
|
+
const MENTION_REGEX = /(^|[^\w@])@([a-z]+):([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
|
|
33
39
|
|
|
34
40
|
export const remarkMentionChips: Plugin<[], Root> = () => {
|
|
35
41
|
return (tree: Root) => {
|
|
@@ -43,7 +49,7 @@ export const remarkMentionChips: Plugin<[], Root> = () => {
|
|
|
43
49
|
MENTION_REGEX.lastIndex = 0
|
|
44
50
|
let match: RegExpExecArray | null
|
|
45
51
|
while ((match = MENTION_REGEX.exec(text)) !== null) {
|
|
46
|
-
const lead = match[1] // '' or the
|
|
52
|
+
const lead = match[1] // '' or the boundary char before '@' (space, '(', etc.)
|
|
47
53
|
const marker = match[2]
|
|
48
54
|
const id = match[3]
|
|
49
55
|
const tokenStart = match.index + lead.length // position of '@'
|