@flamingo-stack/openframe-frontend-core 0.0.290 → 0.0.291-snapshot.20260618233000
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-EL5GCMPU.cjs → chunk-2BMVBPC7.cjs} +9 -9
- package/dist/{chunk-EL5GCMPU.cjs.map → chunk-2BMVBPC7.cjs.map} +1 -1
- package/dist/{chunk-OYXZIPNM.cjs → chunk-2NJ44RTT.cjs} +27 -27
- package/dist/{chunk-OYXZIPNM.cjs.map → chunk-2NJ44RTT.cjs.map} +1 -1
- package/dist/{chunk-R4CLIWAU.js → chunk-5FK7X3EE.js} +270 -172
- package/dist/chunk-5FK7X3EE.js.map +1 -0
- package/dist/{chunk-3SDBXXDP.cjs → chunk-5PELVUFT.cjs} +26 -26
- package/dist/{chunk-3SDBXXDP.cjs.map → chunk-5PELVUFT.cjs.map} +1 -1
- package/dist/{chunk-2V6RCQ5M.cjs → chunk-5R5OODNE.cjs} +40 -40
- package/dist/{chunk-2V6RCQ5M.cjs.map → chunk-5R5OODNE.cjs.map} +1 -1
- package/dist/{chunk-ODR6A6FC.js → chunk-6FHO73AP.js} +22 -10
- package/dist/{chunk-ODR6A6FC.js.map → chunk-6FHO73AP.js.map} +1 -1
- package/dist/{chunk-KJF7SRKH.js → chunk-B2U6INNO.js} +3 -3
- package/dist/{chunk-4F3X2AOB.js → chunk-C667P6LZ.js} +5 -5
- package/dist/{chunk-UC5GB255.cjs → chunk-CDJOKNCS.cjs} +17 -17
- package/dist/{chunk-UC5GB255.cjs.map → chunk-CDJOKNCS.cjs.map} +1 -1
- package/dist/{chunk-7NM7DEUK.js → chunk-CUQH4SHH.js} +2 -2
- package/dist/{chunk-ZLN6SM2U.js → chunk-DUIWR7RQ.js} +3 -3
- package/dist/{chunk-4XMYOZFO.js → chunk-E2YXRSDG.js} +5 -5
- package/dist/{chunk-AAK6IY6Y.cjs → chunk-FFP2A77V.cjs} +10 -10
- package/dist/{chunk-AAK6IY6Y.cjs.map → chunk-FFP2A77V.cjs.map} +1 -1
- package/dist/{chunk-Z5QIVHJW.js → chunk-HTYUZXQP.js} +5 -5
- package/dist/{chunk-LVOBI2M5.js → chunk-IXDTNQF4.js} +3 -3
- package/dist/{chunk-I6ZPGKZ2.cjs → chunk-JC5RN7ZS.cjs} +6 -6
- package/dist/{chunk-I6ZPGKZ2.cjs.map → chunk-JC5RN7ZS.cjs.map} +1 -1
- package/dist/{chunk-VJ4ZWD5G.cjs → chunk-MDLWEJAV.cjs} +1072 -974
- package/dist/chunk-MDLWEJAV.cjs.map +1 -0
- package/dist/{chunk-R2KT5GDD.js → chunk-N45M3TK3.js} +14 -4
- package/dist/chunk-N45M3TK3.js.map +1 -0
- package/dist/{chunk-EI4WALN2.cjs → chunk-OXOTKEYY.cjs} +39 -29
- package/dist/chunk-OXOTKEYY.cjs.map +1 -0
- package/dist/{chunk-7L22MF3U.cjs → chunk-PZZGDS5I.cjs} +17 -17
- package/dist/{chunk-7L22MF3U.cjs.map → chunk-PZZGDS5I.cjs.map} +1 -1
- package/dist/{chunk-VRSXJ5QJ.js → chunk-SLP4KXP6.js} +3 -2
- package/dist/chunk-SLP4KXP6.js.map +1 -0
- package/dist/{chunk-7EYWERFT.js → chunk-VK4B6UGU.js} +4 -4
- package/dist/{chunk-D6RK5YXX.cjs → chunk-Z6BK4XHH.cjs} +22 -10
- package/dist/chunk-Z6BK4XHH.cjs.map +1 -0
- package/dist/{chunk-Y4JNA4W6.cjs → chunk-ZHNL2IPK.cjs} +3 -2
- package/dist/chunk-ZHNL2IPK.cjs.map +1 -0
- 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/embeddable-chat.d.ts +15 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +7 -5
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +6 -4
- package/dist/components/chat/remark-mention-chips.d.ts +30 -0
- package/dist/components/chat/remark-mention-chips.d.ts.map +1 -0
- package/dist/components/chat/types/api.types.d.ts +4 -0
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +24 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/context-item.types.d.ts +5 -0
- package/dist/components/chat/types/context-item.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +4 -0
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
- package/dist/components/chat/utils/nav-anchor-props.d.ts +8 -3
- package/dist/components/chat/utils/nav-anchor-props.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +6 -6
- package/dist/components/contact/index.js +5 -5
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +6 -6
- package/dist/components/embeds/index.js +5 -5
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +5 -5
- package/dist/components/features/index.js +4 -4
- package/dist/components/features/paths-display.d.ts +1 -1
- package/dist/components/features/paths-display.d.ts.map +1 -1
- package/dist/components/index.cjs +178 -176
- 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 +5 -5
- package/dist/components/navigation/index.js +4 -4
- package/dist/components/onboarding-guides/index.cjs +24 -24
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +63 -63
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/index.cjs +7 -5
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +6 -4
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
- package/dist/components/ui/tag.d.ts +10 -1
- package/dist/components/ui/tag.d.ts.map +1 -1
- package/dist/index.cjs +7 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -4
- package/dist/utils/index.cjs +21 -9
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +21 -9
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/scroll-into-view.d.ts +12 -0
- package/dist/utils/scroll-into-view.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-message-enhanced.tsx +71 -9
- package/src/components/chat/chat-message-list.tsx +2 -0
- package/src/components/chat/embeddable-chat.tsx +50 -6
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +1 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/remark-mention-chips.ts +72 -0
- package/src/components/chat/types/api.types.ts +1 -1
- package/src/components/chat/types/component.types.ts +18 -0
- package/src/components/chat/types/context-item.types.ts +5 -0
- package/src/components/chat/types/processing.types.ts +8 -1
- package/src/components/chat/utils/chunk-parser.ts +11 -0
- package/src/components/chat/utils/nav-anchor-props.ts +22 -4
- package/src/components/chat/utils/process-historical-messages.ts +22 -0
- package/src/components/features/.paths-display.md +1 -1
- package/src/components/features/command-box.tsx +1 -1
- package/src/components/features/paths-display.tsx +13 -14
- package/src/components/ui/simple-markdown-renderer.tsx +14 -11
- package/src/components/ui/tag.tsx +12 -2
- package/src/utils/scroll-into-view.ts +51 -9
- package/dist/chunk-D6RK5YXX.cjs.map +0 -1
- package/dist/chunk-EI4WALN2.cjs.map +0 -1
- package/dist/chunk-R2KT5GDD.js.map +0 -1
- package/dist/chunk-R4CLIWAU.js.map +0 -1
- package/dist/chunk-VJ4ZWD5G.cjs.map +0 -1
- package/dist/chunk-VRSXJ5QJ.js.map +0 -1
- package/dist/chunk-Y4JNA4W6.cjs.map +0 -1
- /package/dist/{chunk-KJF7SRKH.js.map → chunk-B2U6INNO.js.map} +0 -0
- /package/dist/{chunk-4F3X2AOB.js.map → chunk-C667P6LZ.js.map} +0 -0
- /package/dist/{chunk-7NM7DEUK.js.map → chunk-CUQH4SHH.js.map} +0 -0
- /package/dist/{chunk-ZLN6SM2U.js.map → chunk-DUIWR7RQ.js.map} +0 -0
- /package/dist/{chunk-4XMYOZFO.js.map → chunk-E2YXRSDG.js.map} +0 -0
- /package/dist/{chunk-Z5QIVHJW.js.map → chunk-HTYUZXQP.js.map} +0 -0
- /package/dist/{chunk-LVOBI2M5.js.map → chunk-IXDTNQF4.js.map} +0 -0
- /package/dist/{chunk-7EYWERFT.js.map → chunk-VK4B6UGU.js.map} +0 -0
|
@@ -33,6 +33,18 @@
|
|
|
33
33
|
*
|
|
34
34
|
* Honors `prefers-reduced-motion` (jumps instantly) and cancels on genuine user
|
|
35
35
|
* scroll intent (wheel / touch) so we never fight the user.
|
|
36
|
+
*
|
|
37
|
+
* WINDOW *OR* A SCROLLABLE ANCESTOR: the helper is not hard-wired to the window
|
|
38
|
+
* scroller. It walks up from the target to the nearest ancestor that is an
|
|
39
|
+
* actual scroll container (`overflow-y: auto | scroll | overlay` AND
|
|
40
|
+
* `scrollHeight > clientHeight`) and drives THAT element; only when none exists
|
|
41
|
+
* does it fall back to `window`. This is what makes it work inside app shells
|
|
42
|
+
* that put page content in a fixed-height `<main class="overflow-y-auto">`
|
|
43
|
+
* (e.g. OpenFrame's `AppLayout`) where the document/window never scrolls — the
|
|
44
|
+
* old window-only version was a silent no-op there. Note `overflow: clip` /
|
|
45
|
+
* `hidden` are deliberately NOT treated as scroll containers, so a list wrapper
|
|
46
|
+
* that uses `overflow-clip` only to round its corners still bubbles the scroll
|
|
47
|
+
* up to the real container (matches the `<HelpCenterCard>` list intent).
|
|
36
48
|
*/
|
|
37
49
|
export interface ScrollElementIntoViewOptions {
|
|
38
50
|
/** Pixels to subtract from the target element's `top` so it lands BELOW
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-into-view.d.ts","sourceRoot":"","sources":["../../src/utils/scroll-into-view.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"scroll-into-view.d.ts","sourceRoot":"","sources":["../../src/utils/scroll-into-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,MAAM,WAAW,4BAA4B;IAC3C;+EAC2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;oFACgF;IAChF,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB;;;6EAGyE;IACzE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9C,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAsCD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACtC,OAAO,GAAE,4BAAiC,GACzC,IAAI,CAgFN"}
|
package/package.json
CHANGED
|
@@ -12,10 +12,21 @@ import { ThinkingDisplay } from "./thinking-display"
|
|
|
12
12
|
import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
|
|
13
13
|
import type { ChatRef } from "./chat-ref.types"
|
|
14
14
|
import { remarkCardLinks } from "./remark-card-links"
|
|
15
|
+
import { remarkMentionChips } from "./remark-mention-chips"
|
|
15
16
|
import { BlockCard, type BlockCardProps } from "./entity-cards/block-card"
|
|
16
17
|
import { ChatContextChipStrip } from "./chat-context-picker"
|
|
17
18
|
import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from "./types"
|
|
18
19
|
|
|
20
|
+
/** Inline `@marker:id` mention token in the message body (sibling of the
|
|
21
|
+
* `[card://]` grammar) — used to filter out items rendered inline from the
|
|
22
|
+
* chip strip below. MUST mirror the left-boundary `(^|\s)` of `MENTION_REGEX`
|
|
23
|
+
* in `remark-mention-chips.ts`: without it this regex is WIDER than the plugin
|
|
24
|
+
* (e.g. it matches `x@device:1` mid-word, which the plugin skips), so a context
|
|
25
|
+
* item would be stripped from the chip strip yet never rendered inline — lost
|
|
26
|
+
* from display entirely. The id is capture group 2 (group 1 is the boundary).
|
|
27
|
+
* Marker lowercase; id is the mention-token charset. */
|
|
28
|
+
const MENTION_MARKER_REGEX = /(^|\s)@[a-z]+:([A-Za-z0-9_.+/=-]+)/g
|
|
29
|
+
|
|
19
30
|
/**
|
|
20
31
|
* Same regex shape as `remarkCardLinks` — kept in lockstep so the
|
|
21
32
|
* pre-scan and the remark plugin see the SAME set of markers. If the
|
|
@@ -32,7 +43,7 @@ function normalizeContent(content: MessageContent): MessageSegment[] {
|
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>(
|
|
35
|
-
({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
|
|
46
|
+
({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderMention, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
|
|
36
47
|
const isUser = role === 'user'
|
|
37
48
|
const isError = role === 'error'
|
|
38
49
|
const authorType = authorTypeProp ?? (isUser ? 'user' : assistantType === 'mingo' ? 'mingo' : 'fae')
|
|
@@ -51,13 +62,48 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
51
62
|
// title — or, if even the ref is unknown, the bare cardId. Never
|
|
52
63
|
// renders the literal `[card://...]` URL.
|
|
53
64
|
const hasMarkerSupport = !!chatRefs || !!renderEntityCard
|
|
54
|
-
const cardRemarkPlugins = useMemo(
|
|
55
|
-
() => (hasMarkerSupport ? [remarkCardLinks] : []),
|
|
56
|
-
[hasMarkerSupport],
|
|
57
|
-
)
|
|
58
65
|
|
|
59
66
|
const segments = useMemo(() => normalizeContent(content), [content])
|
|
60
67
|
|
|
68
|
+
// Inline `@marker:id` mentions: the composer commits these tokens when the
|
|
69
|
+
// user picks context via the `@`-flow, and the ASSISTANT routinely echoes
|
|
70
|
+
// the same `@device:machineId` token back in its reply. The lib detects the
|
|
71
|
+
// token and delegates rendering to the host's `renderMention` (mirror of
|
|
72
|
+
// `renderEntityCard`): the host returns a SELF-FETCHING chip per entity
|
|
73
|
+
// type. Enabled whenever the host opts in by supplying `renderMention`.
|
|
74
|
+
const hasMentionSupport = !!renderMention
|
|
75
|
+
|
|
76
|
+
// Ids that appear as `@marker:id` in the body → rendered inline, so they are
|
|
77
|
+
// excluded from the chip strip below (no duplicate inline + strip).
|
|
78
|
+
const inlineMentionIds = useMemo(() => {
|
|
79
|
+
const ids = new Set<string>()
|
|
80
|
+
if (!hasMentionSupport) return ids
|
|
81
|
+
for (const seg of segments) {
|
|
82
|
+
if (seg.type !== 'text' || !seg.text || !seg.text.includes('@')) continue
|
|
83
|
+
for (const mm of seg.text.matchAll(MENTION_MARKER_REGEX)) ids.add(mm[2])
|
|
84
|
+
}
|
|
85
|
+
return ids
|
|
86
|
+
}, [hasMentionSupport, segments])
|
|
87
|
+
|
|
88
|
+
// Chip strip = only context NOT already shown inline (e.g. `+`-added items).
|
|
89
|
+
const stripContextItems = useMemo(
|
|
90
|
+
() =>
|
|
91
|
+
inlineMentionIds.size > 0 && contextItems
|
|
92
|
+
? contextItems.filter((it) => !inlineMentionIds.has(it.id))
|
|
93
|
+
: contextItems,
|
|
94
|
+
[contextItems, inlineMentionIds],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// Markdown plugins per message: card markers (assistant) + mention tokens
|
|
98
|
+
// (user). Each is gated independently so neither fires without its data.
|
|
99
|
+
const cardRemarkPlugins = useMemo(
|
|
100
|
+
() => [
|
|
101
|
+
...(hasMarkerSupport ? [remarkCardLinks] : []),
|
|
102
|
+
...(hasMentionSupport ? [remarkMentionChips] : []),
|
|
103
|
+
],
|
|
104
|
+
[hasMarkerSupport, hasMentionSupport],
|
|
105
|
+
)
|
|
106
|
+
|
|
61
107
|
// Cross-render cache of rendered inline-card nodes, keyed by `type:id`.
|
|
62
108
|
// A fetch-mode card lives inside the assistant message, which re-renders on
|
|
63
109
|
// every stream chunk (and the whole list re-renders when a new message
|
|
@@ -230,7 +276,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
230
276
|
}, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
|
|
231
277
|
|
|
232
278
|
const cardComponentOverrides = useMemo(() => {
|
|
233
|
-
if (!hasMarkerSupport) return undefined
|
|
279
|
+
if (!hasMarkerSupport && !hasMentionSupport) return undefined
|
|
234
280
|
const refs = chatRefs ?? {}
|
|
235
281
|
const inlineByKey = renderingPlan?.inlineByKey
|
|
236
282
|
return {
|
|
@@ -240,6 +286,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
240
286
|
// paragraph as siblings — the inline pill stays at the marker
|
|
241
287
|
// position. Other href schemes pass through unchanged.
|
|
242
288
|
a: ({ href, children, className: linkClassName, ...rest }: any) => {
|
|
289
|
+
// Inline entity mention `@marker:id`, emitted as a `mention://marker:id`
|
|
290
|
+
// link by `remarkMentionChips`. Delegate to the host's `renderMention`
|
|
291
|
+
// (mirror of the `card://` → `renderEntityCard` path): the host returns
|
|
292
|
+
// a self-fetching chip for that entity type. Null/unknown → raw token.
|
|
293
|
+
if (typeof href === 'string' && href.startsWith('mention://')) {
|
|
294
|
+
const stripped = href.slice('mention://'.length)
|
|
295
|
+
const sepIdx = stripped.indexOf(':')
|
|
296
|
+
const marker = sepIdx === -1 ? stripped : stripped.slice(0, sepIdx)
|
|
297
|
+
const id = sepIdx === -1 ? '' : stripped.slice(sepIdx + 1)
|
|
298
|
+
const node = renderMention?.({ marker, id })
|
|
299
|
+
if (node != null) return <>{node}</>
|
|
300
|
+
return <span className="text-ods-text-secondary opacity-60">{children}</span>
|
|
301
|
+
}
|
|
243
302
|
if (typeof href === 'string' && href.startsWith('card://')) {
|
|
244
303
|
const stripped = href.slice('card://'.length)
|
|
245
304
|
const sepIdx = stripped.lastIndexOf(':')
|
|
@@ -313,7 +372,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
313
372
|
)
|
|
314
373
|
},
|
|
315
374
|
}
|
|
316
|
-
}, [hasMarkerSupport, chatRefs, renderingPlan, NavLinkAnchor])
|
|
375
|
+
}, [hasMarkerSupport, hasMentionSupport, renderMention, chatRefs, renderingPlan, NavLinkAnchor])
|
|
317
376
|
|
|
318
377
|
const getAvatarProps = () => {
|
|
319
378
|
const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
|
|
@@ -529,9 +588,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
529
588
|
|
|
530
589
|
{/* Attached entity-context chips (user bubbles). Read-only — no
|
|
531
590
|
remove affordance once the message is sent (Figma 31:28709). */}
|
|
532
|
-
{
|
|
591
|
+
{stripContextItems && stripContextItems.length > 0 && (
|
|
533
592
|
<ChatContextChipStrip
|
|
534
|
-
items={
|
|
593
|
+
items={stripContextItems}
|
|
535
594
|
resolveIcon={resolveContextIcon}
|
|
536
595
|
className="mt-2"
|
|
537
596
|
/>
|
|
@@ -566,6 +625,9 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
|
|
|
566
625
|
// message (it's set once on the optimistic send and never mutated).
|
|
567
626
|
prevProps.contextItems === nextProps.contextItems &&
|
|
568
627
|
prevProps.resolveContextIcon === nextProps.resolveContextIcon &&
|
|
628
|
+
// Host keeps this stable (module const / useCallback), so reference
|
|
629
|
+
// equality holds across streaming chunks.
|
|
630
|
+
prevProps.renderMention === nextProps.renderMention &&
|
|
569
631
|
prevProps.renderEntityCard === nextProps.renderEntityCard &&
|
|
570
632
|
prevProps.NavLinkAnchor === nextProps.NavLinkAnchor
|
|
571
633
|
)
|
|
@@ -107,6 +107,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
107
107
|
onLoadMore,
|
|
108
108
|
renderEntityCard,
|
|
109
109
|
resolveContextIcon,
|
|
110
|
+
renderMention,
|
|
110
111
|
NavLinkAnchor,
|
|
111
112
|
...props
|
|
112
113
|
},
|
|
@@ -538,6 +539,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
538
539
|
chatRefs={message.chatRefs}
|
|
539
540
|
contextItems={message.contextItems}
|
|
540
541
|
resolveContextIcon={resolveContextIcon}
|
|
542
|
+
renderMention={renderMention}
|
|
541
543
|
renderEntityCard={renderEntityCard}
|
|
542
544
|
NavLinkAnchor={NavLinkAnchor}
|
|
543
545
|
/>
|
|
@@ -259,6 +259,18 @@ export interface EmbeddableChatProps {
|
|
|
259
259
|
* `config.search`. Omit to disable the feature entirely.
|
|
260
260
|
*/
|
|
261
261
|
contextPicker?: ChatContextPickerConfig
|
|
262
|
+
/**
|
|
263
|
+
* Host renderer for inline AI mentions `@marker:id` (e.g. the assistant
|
|
264
|
+
* echoing `@device:<machineId>` in its reply). DIRECT MIRROR of
|
|
265
|
+
* `renderEntityCard` for the `[card://]` grammar: the lib detects the token,
|
|
266
|
+
* parses `{marker, id}`, and renders whatever the host returns — typically a
|
|
267
|
+
* SELF-FETCHING chip (each entity type has its own fetcher) that resolves its
|
|
268
|
+
* own display name by id. SEPARATE from `contextPicker`/`contextItems` (the
|
|
269
|
+
* USER's attachments). Keep the function identity stable (module const /
|
|
270
|
+
* `useCallback`) so the thread's streaming memo holds. Return null for a
|
|
271
|
+
* marker the host can't render → the lib falls back to the bare token.
|
|
272
|
+
*/
|
|
273
|
+
renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
|
|
262
274
|
}
|
|
263
275
|
|
|
264
276
|
// =============================================================================
|
|
@@ -269,6 +281,22 @@ export interface EmbeddableChatProps {
|
|
|
269
281
|
const formatRelativePath = (p: string): string =>
|
|
270
282
|
p.replace(/^\/+/, '').replace(/\/+$/, '')
|
|
271
283
|
|
|
284
|
+
/**
|
|
285
|
+
* The committed `@`-mention text token, derived from a `type:id` identity key.
|
|
286
|
+
* The TYPE is replaced with its backend mention MARKER (`markerByType`, e.g.
|
|
287
|
+
* `KB_ARTICLE → 'kb'`), falling back to a lowercased type when the host didn't
|
|
288
|
+
* declare one (`@device:…`, not `@DEVICE:…`); the `id` is left verbatim. The
|
|
289
|
+
* structured context items keep the original (upper) entity-kind for the wire
|
|
290
|
+
* enum, so this only affects the inline draft token. `[A-Za-z…]` in the input
|
|
291
|
+
* regex matches either case, so commit/strip/detect stay symmetric.
|
|
292
|
+
*/
|
|
293
|
+
const mentionTokenOf = (key: string, markerByType: Map<string, string>): string => {
|
|
294
|
+
const ci = key.indexOf(':')
|
|
295
|
+
const type = ci === -1 ? key : key.slice(0, ci)
|
|
296
|
+
const id = ci === -1 ? '' : key.slice(ci + 1)
|
|
297
|
+
return `${markerByType.get(type) ?? type.toLowerCase()}:${id}`
|
|
298
|
+
}
|
|
299
|
+
|
|
272
300
|
/**
|
|
273
301
|
* Fallback fan-out when the model didn't cite any source. Show the top-N
|
|
274
302
|
* retrieved sources instead of zero chips. Mirrors Perplexity's behavior.
|
|
@@ -662,6 +690,7 @@ function EmbeddableChatInner({
|
|
|
662
690
|
mingoWelcome,
|
|
663
691
|
guideWelcome,
|
|
664
692
|
contextPicker,
|
|
693
|
+
renderMention,
|
|
665
694
|
}: EmbeddableChatProps) {
|
|
666
695
|
// `shell === 'none'` means the consumer hosts us inside their own panel
|
|
667
696
|
// (e.g. AppLayoutDrawer in openframe-frontend). Several drawer-shell
|
|
@@ -972,6 +1001,15 @@ function EmbeddableChatInner({
|
|
|
972
1001
|
mentionKeyRef.current = null
|
|
973
1002
|
}, [activeDialogId, activeMode])
|
|
974
1003
|
|
|
1004
|
+
// Map each entity type to its backend mention marker (host-declared on the
|
|
1005
|
+
// entity type). Drives the committed `@marker:id` token; missing markers fall
|
|
1006
|
+
// back to a lowercased type inside `mentionTokenOf`.
|
|
1007
|
+
const mentionMarkerByType = useMemo(() => {
|
|
1008
|
+
const m = new Map<string, string>()
|
|
1009
|
+
for (const t of contextPicker?.entityTypes ?? []) if (t.marker) m.set(t.type, t.marker)
|
|
1010
|
+
return m
|
|
1011
|
+
}, [contextPicker?.entityTypes])
|
|
1012
|
+
|
|
975
1013
|
const toggleContextItem = useCallback(
|
|
976
1014
|
(item: ChatContextItem) => {
|
|
977
1015
|
const key = `${item.type}:${item.id}`
|
|
@@ -990,7 +1028,7 @@ function EmbeddableChatInner({
|
|
|
990
1028
|
)
|
|
991
1029
|
const grows = !alreadySelected && !mentionKeyRef.current
|
|
992
1030
|
if (grows && contextItems.length >= contextMaxItems) return
|
|
993
|
-
chatInputRef.current?.commitMention(key)
|
|
1031
|
+
chatInputRef.current?.commitMention(mentionTokenOf(key, mentionMarkerByType))
|
|
994
1032
|
setContextItems((prev) => {
|
|
995
1033
|
const withoutPrevMention = mentionKeyRef.current
|
|
996
1034
|
? prev.filter((p) => `${p.type}:${p.id}` !== mentionKeyRef.current)
|
|
@@ -1010,7 +1048,7 @@ function EmbeddableChatInner({
|
|
|
1010
1048
|
return [...prev, item]
|
|
1011
1049
|
})
|
|
1012
1050
|
},
|
|
1013
|
-
[contextMaxItems, contextItems],
|
|
1051
|
+
[contextMaxItems, contextItems, mentionMarkerByType],
|
|
1014
1052
|
)
|
|
1015
1053
|
|
|
1016
1054
|
const removeContextItem = useCallback((item: ChatContextItem) => {
|
|
@@ -1022,10 +1060,10 @@ function EmbeddableChatInner({
|
|
|
1022
1060
|
if (mentionKeyRef.current === key) {
|
|
1023
1061
|
mentionKeyRef.current = null
|
|
1024
1062
|
const cur = chatInputRef.current?.getValue() ?? ''
|
|
1025
|
-
const next = cur.replace(`@${key}`, '').replace(/\s{2,}/g, ' ').trimStart()
|
|
1063
|
+
const next = cur.replace(`@${mentionTokenOf(key, mentionMarkerByType)}`, '').replace(/\s{2,}/g, ' ').trimStart()
|
|
1026
1064
|
chatInputRef.current?.setValue(next)
|
|
1027
1065
|
}
|
|
1028
|
-
}, [])
|
|
1066
|
+
}, [mentionMarkerByType])
|
|
1029
1067
|
|
|
1030
1068
|
// Draft → context reconciliation: when the user deletes the `@type:id` token
|
|
1031
1069
|
// text, drop the matching mention item. `+`-added items have no token in the
|
|
@@ -1033,11 +1071,11 @@ function EmbeddableChatInner({
|
|
|
1033
1071
|
const handleContextValueChange = useCallback((value: string) => {
|
|
1034
1072
|
const key = mentionKeyRef.current
|
|
1035
1073
|
if (!key) return
|
|
1036
|
-
if (!value.includes(`@${key}`)) {
|
|
1074
|
+
if (!value.includes(`@${mentionTokenOf(key, mentionMarkerByType)}`)) {
|
|
1037
1075
|
setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))
|
|
1038
1076
|
mentionKeyRef.current = null
|
|
1039
1077
|
}
|
|
1040
|
-
}, [])
|
|
1078
|
+
}, [mentionMarkerByType])
|
|
1041
1079
|
|
|
1042
1080
|
const openContextPicker = useCallback(() => {
|
|
1043
1081
|
setMentionQuery(null)
|
|
@@ -1078,6 +1116,11 @@ function EmbeddableChatInner({
|
|
|
1078
1116
|
return (item: ChatContextItem) => byType.get(item.type)
|
|
1079
1117
|
}, [contextEntityTypes])
|
|
1080
1118
|
|
|
1119
|
+
// `renderMention` is HOST-provided (see the prop doc): the per-type renderer
|
|
1120
|
+
// for inline AI mentions `@marker:id` (mirror of `renderEntityCard`). The lib
|
|
1121
|
+
// forwards it verbatim to the message list and calls it from the `mention://`
|
|
1122
|
+
// override; the host returns a self-fetching chip.
|
|
1123
|
+
|
|
1081
1124
|
// Resolve base route. Hub default mapping: flamingo → /knowledge-base,
|
|
1082
1125
|
// anything else → /data-room. Embedders override per platform. An embedder that
|
|
1083
1126
|
// doesn't host an in-app doc viewer should NOT pass an empty baseRoute (that just
|
|
@@ -1515,6 +1558,7 @@ function EmbeddableChatInner({
|
|
|
1515
1558
|
assistantIcon={mingoAssistantIcon}
|
|
1516
1559
|
renderEntityCard={renderEntityCard}
|
|
1517
1560
|
resolveContextIcon={resolveContextIcon}
|
|
1561
|
+
renderMention={renderMention}
|
|
1518
1562
|
NavLinkAnchor={NavLinkAnchorViaRuntime}
|
|
1519
1563
|
className="flex-1"
|
|
1520
1564
|
// No inner `px`/`pb`: the panel wrapper already pads with
|
|
@@ -38,6 +38,7 @@ export * from './model-display'
|
|
|
38
38
|
export * from './chat-sidebar'
|
|
39
39
|
export type { ChatRef } from './chat-ref.types'
|
|
40
40
|
export { remarkCardLinks } from './remark-card-links'
|
|
41
|
+
export { remarkMentionChips } from './remark-mention-chips'
|
|
41
42
|
|
|
42
43
|
// Card-supporting UI migrated from hub `components/shared/*` + `components/blog/*`
|
|
43
44
|
export {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remark plugin that walks markdown text leaves and replaces inline entity
|
|
3
|
+
* mentions `@<marker>:<id>` (e.g. `@device:64f0a1`, `@kb:5`) with synthetic
|
|
4
|
+
* `link` mdast nodes whose `url` is `mention://<marker>:<id>` (a non-standard
|
|
5
|
+
* scheme). The downstream `<a>` component override in `chat-message-enhanced`
|
|
6
|
+
* detects that scheme and renders an inline `<Tag>`-style chip, resolving the
|
|
7
|
+
* display name from the message's `contextItems`.
|
|
8
|
+
*
|
|
9
|
+
* Sibling of `remark-card-links` (which handles the ASSISTANT-side
|
|
10
|
+
* `[card://type:id]` markers). This one handles the USER-side `@marker:id`
|
|
11
|
+
* tokens the composer commits when picking context via the `@`-mention flow.
|
|
12
|
+
*
|
|
13
|
+
* Why a remark plugin (NOT a regex on the raw markdown source): it splits only
|
|
14
|
+
* TEXT leaves, so bold / italic / list / code wrappers around a mention stay
|
|
15
|
+
* intact, and the marker disappears before any rehype-raw HTML pass.
|
|
16
|
+
*
|
|
17
|
+
* Grammar:
|
|
18
|
+
* - marker: `[a-z]+` — the backend `ContextItemType.marker()` short form
|
|
19
|
+
* (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,
|
|
20
|
+
* `query`, …). Always lowercase.
|
|
21
|
+
* - id: `[A-Za-z0-9_.+/=-]+` — the mention-token charset (UUIDs with hyphens,
|
|
22
|
+
* fleet numeric ids, etc.). Matches the composer's `MENTION_TOKEN`.
|
|
23
|
+
* - A LEFT boundary (start-of-text or whitespace) keeps `@marker:` from
|
|
24
|
+
* matching inside emails / mid-word (`user@host:1234` never starts a token
|
|
25
|
+
* because the `@` isn't at a boundary).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { Plugin } from 'unified'
|
|
29
|
+
import type { Root, Text, Link } from 'mdast'
|
|
30
|
+
import { visit, SKIP } from 'unist-util-visit'
|
|
31
|
+
|
|
32
|
+
const MENTION_REGEX = /(^|\s)@([a-z]+):([A-Za-z0-9_.+/=-]+)/g
|
|
33
|
+
|
|
34
|
+
export const remarkMentionChips: Plugin<[], Root> = () => {
|
|
35
|
+
return (tree: Root) => {
|
|
36
|
+
visit(tree, 'text', (node: Text, index, parent) => {
|
|
37
|
+
if (!parent || typeof index !== 'number') return
|
|
38
|
+
const text = node.value
|
|
39
|
+
if (!text || !text.includes('@')) return
|
|
40
|
+
|
|
41
|
+
const parts: Array<Text | Link> = []
|
|
42
|
+
let lastIndex = 0
|
|
43
|
+
MENTION_REGEX.lastIndex = 0
|
|
44
|
+
let match: RegExpExecArray | null
|
|
45
|
+
while ((match = MENTION_REGEX.exec(text)) !== null) {
|
|
46
|
+
const lead = match[1] // '' or the whitespace char before '@'
|
|
47
|
+
const marker = match[2]
|
|
48
|
+
const id = match[3]
|
|
49
|
+
const tokenStart = match.index + lead.length // position of '@'
|
|
50
|
+
if (tokenStart > lastIndex) {
|
|
51
|
+
parts.push({ type: 'text', value: text.slice(lastIndex, tokenStart) })
|
|
52
|
+
}
|
|
53
|
+
parts.push({
|
|
54
|
+
type: 'link',
|
|
55
|
+
url: `mention://${marker}:${id}`,
|
|
56
|
+
// Keep the raw token as the visible children so an unresolved mention
|
|
57
|
+
// (no matching context item) falls back to the literal text.
|
|
58
|
+
children: [{ type: 'text', value: `@${marker}:${id}` }],
|
|
59
|
+
})
|
|
60
|
+
lastIndex = match.index + match[0].length
|
|
61
|
+
}
|
|
62
|
+
if (lastIndex === 0) return // no matches
|
|
63
|
+
|
|
64
|
+
if (lastIndex < text.length) {
|
|
65
|
+
parts.push({ type: 'text', value: text.slice(lastIndex) })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
parent.children.splice(index, 1, ...parts)
|
|
69
|
+
return [SKIP, index + parts.length]
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -167,7 +167,7 @@ export interface RealtimeChunkCallbacks {
|
|
|
167
167
|
/** Called when a user message request is received (echo). `streamSeq` (when
|
|
168
168
|
* the transport carries one) lets hosts stamp the synthetic so the history
|
|
169
169
|
* merge can dedup it against its persisted twin by sequence. */
|
|
170
|
-
onUserMessage?: (text: string, metadata?: { ownerType?: string; displayName?: string; userId?: string; streamSeq?: number }) => void
|
|
170
|
+
onUserMessage?: (text: string, metadata?: { ownerType?: string; displayName?: string; userId?: string; streamSeq?: number, contextItems?: Array<{ type: string; id: string }> }) => void
|
|
171
171
|
/** Called when TOKEN_USAGE chunk is received with token stats */
|
|
172
172
|
onTokenUsage?: (data: TokenUsageData) => void
|
|
173
173
|
/** Called when a direct message is received (immediately displayed). Carries
|
|
@@ -132,6 +132,20 @@ export interface ChatMessageEnhancedProps extends Omit<HTMLAttributes<HTMLDivEle
|
|
|
132
132
|
* glyph). Optional — chips render label-only when omitted.
|
|
133
133
|
*/
|
|
134
134
|
resolveContextIcon?: (item: ChatContextItem) => ReactNode
|
|
135
|
+
/**
|
|
136
|
+
* Host renderer for inline AI mentions `@marker:id` — the ASSISTANT echoing
|
|
137
|
+
* `@device:<machineId>` (etc.) in its reply. DIRECT MIRROR of
|
|
138
|
+
* `renderEntityCard` for the `[card://]` grammar: the lib detects the token
|
|
139
|
+
* (via `remark-mention-chips`), parses `{marker, id}`, and renders whatever
|
|
140
|
+
* node the host returns — typically a SELF-FETCHING chip (each entity type
|
|
141
|
+
* has its own fetcher) that resolves its own display name by id. The lib
|
|
142
|
+
* stays data-agnostic: it knows nothing about entity types or how to fetch
|
|
143
|
+
* them. Return null/undefined for a marker the host can't render → the lib
|
|
144
|
+
* falls back to the bare token text. SEPARATE from `contextItems` (the user's
|
|
145
|
+
* own attachments). Keep the function identity stable (e.g. a module const or
|
|
146
|
+
* `useCallback`) so the message memo holds across streaming chunks.
|
|
147
|
+
*/
|
|
148
|
+
renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
|
|
135
149
|
/**
|
|
136
150
|
* Host-provided renderer for inline entity cards (v6.1 §B.2.7 — DRY
|
|
137
151
|
* duplications #2). The OSS-lib delegates all entity-specific rendering
|
|
@@ -201,6 +215,10 @@ export interface ChatMessageListProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
201
215
|
/** Lead-icon resolver for per-message context chips (maps a context item to
|
|
202
216
|
* its entity-type glyph). Forwarded to every message's ChatMessageEnhanced. */
|
|
203
217
|
resolveContextIcon?: (item: ChatContextItem) => ReactNode
|
|
218
|
+
/** Host renderer for inline AI mentions `@marker:id` (mirror of
|
|
219
|
+
* `renderEntityCard`). Forwarded verbatim to every message's
|
|
220
|
+
* ChatMessageEnhanced; returns a self-fetching chip per entity type. */
|
|
221
|
+
renderMention?: (reference: { marker: string; id: string }) => React.ReactNode
|
|
204
222
|
/** Host-provided renderer for inline entity cards. Forwarded verbatim
|
|
205
223
|
* to every message's ChatMessageEnhanced. v6.1 §B.2.7. */
|
|
206
224
|
renderEntityCard?: (reference: ChatRef) => React.ReactNode
|
|
@@ -33,6 +33,11 @@ export interface ChatContextEntityType {
|
|
|
33
33
|
label: string
|
|
34
34
|
/** Optional icon element rendered at the row lead (host-supplied). */
|
|
35
35
|
icon?: React.ReactNode
|
|
36
|
+
/** Optional backend mention marker — the SHORT token used for the committed
|
|
37
|
+
* inline `@marker:id` reference (e.g. `'device'`, `'kb'`). When omitted the
|
|
38
|
+
* composer falls back to a lowercased `type`. Lets the host map each kind to
|
|
39
|
+
* the backend's exact mention vocabulary without the lib knowing it. */
|
|
40
|
+
marker?: string
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/**
|
|
@@ -20,7 +20,14 @@ export type ParsedChunkAction =
|
|
|
20
20
|
| { action: 'approval_request'; requestId: string; command: string; explanation?: string; approvalType: string }
|
|
21
21
|
| { action: 'approval_batch'; requestId: string; approvalType: string; toolCalls: PendingToolCallData[] }
|
|
22
22
|
| { action: 'approval_result'; requestId: string; approved: boolean; approvalType: string; resolvedByName?: string | null }
|
|
23
|
-
| {
|
|
23
|
+
| {
|
|
24
|
+
action: 'message_request'
|
|
25
|
+
text: string
|
|
26
|
+
ownerType?: string
|
|
27
|
+
displayName?: string
|
|
28
|
+
userId?: string
|
|
29
|
+
contextItems?: Array<{ type: string; id: string }>
|
|
30
|
+
}
|
|
24
31
|
| { action: 'token_usage'; data: TokenUsageData }
|
|
25
32
|
| { action: 'direct_message'; text: string; ownerType?: string; displayName?: string; userId?: string }
|
|
26
33
|
| { action: 'system'; text: string }
|
|
@@ -155,6 +155,17 @@ export function parseChunkToAction(chunk: unknown): ParsedChunkAction | null {
|
|
|
155
155
|
ownerType: typeof data.ownerType === 'string' ? data.ownerType : undefined,
|
|
156
156
|
displayName: typeof data.displayName === 'string' ? data.displayName : undefined,
|
|
157
157
|
userId: typeof data.userId === 'string' ? data.userId : undefined,
|
|
158
|
+
// Entity-context refs the user attached to this message (backend
|
|
159
|
+
// `MESSAGE_REQUEST` chunk → `contextItems: [{ type, id }]`). The wire
|
|
160
|
+
// shape carries no label; the host resolves display text + icon. `label`
|
|
161
|
+
// is REQUIRED on `ChatContextItem`, so fall back to the id (same as the
|
|
162
|
+
// historical path in `process-historical-messages.ts`) — keeps both
|
|
163
|
+
// message sources producing an identical shape.
|
|
164
|
+
contextItems: Array.isArray(data.contextItems)
|
|
165
|
+
? (data.contextItems as Array<{ type?: unknown; id?: unknown }>)
|
|
166
|
+
.filter((it) => typeof it?.type === 'string' && typeof it?.id === 'string')
|
|
167
|
+
.map((it) => ({ type: it.type as string, id: it.id as string, label: it.id as string }))
|
|
168
|
+
: undefined,
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
case MESSAGE_TYPE.TOKEN_USAGE:
|
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
* `decideNewTab` fallback chain anywhere else.
|
|
16
16
|
*
|
|
17
17
|
* Rules:
|
|
18
|
-
* - `embed` mode →
|
|
19
|
-
* origin
|
|
20
|
-
* embedder
|
|
18
|
+
* - `embed` mode → new-tab, EXCEPT for same-origin absolute hrefs. The chat
|
|
19
|
+
* lives on the embedder origin and most content lives on the hub (→ new
|
|
20
|
+
* tab), but an embedder may host a few content types in-app (e.g. OpenFrame
|
|
21
|
+
* serves releases / roadmap / guides under `/help-center`). Those are
|
|
22
|
+
* emitted as ABSOLUTE same-origin URLs by the embedder's `composeContentUrl`;
|
|
23
|
+
* keep them same-tab so they soft-nav in-app instead of opening a redundant
|
|
24
|
+
* new tab on the same origin. Relative hrefs stay new-tab — in embed mode
|
|
25
|
+
* they're hub-relative (absolutized against `defaultContentOrigin`).
|
|
21
26
|
* - `host` mode → defer to the runtime's `decideNewTab` callback
|
|
22
27
|
* (cross-platform → new-tab) or the lib's default.
|
|
23
28
|
*/
|
|
@@ -25,13 +30,26 @@
|
|
|
25
30
|
import type { ChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
26
31
|
import { decideNewTab as libDecideNewTab } from './decide-new-tab'
|
|
27
32
|
|
|
33
|
+
/** An ABSOLUTE `http(s)` URL on the current page's origin. Relative hrefs return
|
|
34
|
+
* false (in embed mode they're hub-relative, not in-app), as do cross-origin
|
|
35
|
+
* and non-http URLs. SSR-safe: no `window` → false. */
|
|
36
|
+
function isSameOriginAbsoluteHref(href: string): boolean {
|
|
37
|
+
if (typeof window === 'undefined') return false
|
|
38
|
+
if (!/^https?:\/\//i.test(href)) return false
|
|
39
|
+
try {
|
|
40
|
+
return new URL(href).origin === window.location.origin
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
export function computeIsNewTab(
|
|
29
47
|
runtime: ChatRuntime,
|
|
30
48
|
href: string | null | undefined,
|
|
31
49
|
targetPlatform: string | null,
|
|
32
50
|
): boolean {
|
|
33
51
|
if (!href) return false
|
|
34
|
-
if (runtime.navigation.mode === 'embed') return
|
|
52
|
+
if (runtime.navigation.mode === 'embed') return !isSameOriginAbsoluteHref(href)
|
|
35
53
|
return (
|
|
36
54
|
runtime.navigation.decideNewTab?.({ href, targetPlatform }) ??
|
|
37
55
|
libDecideNewTab({
|
|
@@ -144,6 +144,16 @@ export function processHistoricalMessages(
|
|
|
144
144
|
const userAuthorType: AuthorType = msg.owner?.type === OWNER_TYPE.ADMIN ? 'admin' : 'user'
|
|
145
145
|
messageDataArray.forEach((data) => {
|
|
146
146
|
if (data.type === MESSAGE_TYPE.TEXT && 'text' in data && data.text) {
|
|
147
|
+
// `TextData.contextItems` (server: `[{ type, id }]`) — the entity
|
|
148
|
+
// context the user attached to this message. Surface it so the bubble
|
|
149
|
+
// renders its chip strip from history (no label on the wire → fall
|
|
150
|
+
// back to the id, matching the realtime path).
|
|
151
|
+
const rawContext = (data as { contextItems?: Array<{ type?: unknown; id?: unknown }> }).contextItems
|
|
152
|
+
const contextItems = Array.isArray(rawContext)
|
|
153
|
+
? rawContext
|
|
154
|
+
.filter((c) => typeof c?.type === 'string' && typeof c?.id === 'string')
|
|
155
|
+
.map((c) => ({ type: c.type as string, id: c.id as string, label: c.id as string }))
|
|
156
|
+
: undefined
|
|
147
157
|
processedMessages.push({
|
|
148
158
|
id: msg.id,
|
|
149
159
|
role: 'user',
|
|
@@ -152,6 +162,7 @@ export function processHistoricalMessages(
|
|
|
152
162
|
avatar: getOwnerAvatar(msg.owner),
|
|
153
163
|
authorType: userAuthorType,
|
|
154
164
|
timestamp: new Date(msg.createdAt),
|
|
165
|
+
...(contextItems && contextItems.length > 0 ? { contextItems } : {}),
|
|
155
166
|
})
|
|
156
167
|
}
|
|
157
168
|
})
|
|
@@ -522,6 +533,16 @@ export function processHistoricalMessagesWithErrors(
|
|
|
522
533
|
const userAuthorType: AuthorType = msg.owner?.type === OWNER_TYPE.ADMIN ? 'admin' : 'user'
|
|
523
534
|
messageDataArray.forEach((data) => {
|
|
524
535
|
if (data.type === MESSAGE_TYPE.TEXT && 'text' in data && data.text) {
|
|
536
|
+
// `TextData.contextItems` (server: `[{ type, id }]`) — the entity
|
|
537
|
+
// context the user attached to this message. Surface it so the bubble
|
|
538
|
+
// renders its chip strip from history (no label on the wire → fall
|
|
539
|
+
// back to the id, matching the realtime path).
|
|
540
|
+
const rawContext = (data as { contextItems?: Array<{ type?: unknown; id?: unknown }> }).contextItems
|
|
541
|
+
const contextItems = Array.isArray(rawContext)
|
|
542
|
+
? rawContext
|
|
543
|
+
.filter((c) => typeof c?.type === 'string' && typeof c?.id === 'string')
|
|
544
|
+
.map((c) => ({ type: c.type as string, id: c.id as string, label: c.id as string }))
|
|
545
|
+
: undefined
|
|
525
546
|
processedMessages.push({
|
|
526
547
|
id: msg.id,
|
|
527
548
|
role: 'user',
|
|
@@ -530,6 +551,7 @@ export function processHistoricalMessagesWithErrors(
|
|
|
530
551
|
avatar: getOwnerAvatar(msg.owner),
|
|
531
552
|
authorType: userAuthorType,
|
|
532
553
|
timestamp: new Date(msg.createdAt),
|
|
554
|
+
...(contextItems && contextItems.length > 0 ? { contextItems } : {}),
|
|
533
555
|
})
|
|
534
556
|
}
|
|
535
557
|
})
|
|
@@ -44,7 +44,7 @@ import { PathsDisplay, getOpenFramePaths } from '@flamingo/ui-kit/components/fea
|
|
|
44
44
|
| `title` | `string` | — | Optional heading above the list |
|
|
45
45
|
| `description` | `string` | — | Optional subtext below the title |
|
|
46
46
|
| `showCopyButtons` | `boolean` | `true` | Toggle copy button visibility |
|
|
47
|
-
| `copyIconSize` | `string` | `'w-
|
|
47
|
+
| `copyIconSize` | `string` | `'w-6 h-6'` | Tailwind size classes for the copy icon |
|
|
48
48
|
| `className` | `string` | — | Additional CSS classes for the container |
|
|
49
49
|
|
|
50
50
|
> **Note:** The component renders `null` when `paths` is empty or undefined. Copy buttons only render when both `showCopyButtons` is `true` **and** `onCopyPath` is provided.
|
|
@@ -98,7 +98,7 @@ export function CommandBox({
|
|
|
98
98
|
{title}
|
|
99
99
|
</div>
|
|
100
100
|
)}
|
|
101
|
-
<div className="bg-ods-
|
|
101
|
+
<div className="bg-ods-bg border border-ods-border rounded-[6px] p-4">
|
|
102
102
|
<div
|
|
103
103
|
className={cn(
|
|
104
104
|
'text-ods-text-primary font-mono text-[14px] md:text-[16px] leading-relaxed break-all',
|