@flamingo-stack/openframe-frontend-core 0.0.186 → 0.0.187

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.
Files changed (35) hide show
  1. package/dist/{chunk-SAWVZ5LA.cjs → chunk-EUTOT74J.cjs} +1261 -1159
  2. package/dist/chunk-EUTOT74J.cjs.map +1 -0
  3. package/dist/{chunk-LI2C4ZCU.js → chunk-M36UJN3T.js} +10587 -10485
  4. package/dist/chunk-M36UJN3T.js.map +1 -0
  5. package/dist/components/chat/block-card.d.ts +39 -0
  6. package/dist/components/chat/block-card.d.ts.map +1 -0
  7. package/dist/components/chat/chat-message-enhanced.d.ts +2 -1
  8. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  9. package/dist/components/chat/chat-ref.types.d.ts +6 -0
  10. package/dist/components/chat/chat-ref.types.d.ts.map +1 -1
  11. package/dist/components/chat/chat-video-entity-card.d.ts +30 -0
  12. package/dist/components/chat/chat-video-entity-card.d.ts.map +1 -0
  13. package/dist/components/chat/index.d.ts +2 -0
  14. package/dist/components/chat/index.d.ts.map +1 -1
  15. package/dist/components/features/index.cjs +2 -2
  16. package/dist/components/features/index.js +1 -1
  17. package/dist/components/index.cjs +6 -2
  18. package/dist/components/index.cjs.map +1 -1
  19. package/dist/components/index.js +5 -1
  20. package/dist/components/navigation/index.cjs +2 -2
  21. package/dist/components/navigation/index.js +1 -1
  22. package/dist/components/ui/index.cjs +6 -2
  23. package/dist/components/ui/index.cjs.map +1 -1
  24. package/dist/components/ui/index.js +5 -1
  25. package/dist/index.cjs +6 -2
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +5 -1
  28. package/package.json +1 -1
  29. package/src/components/chat/block-card.tsx +44 -0
  30. package/src/components/chat/chat-message-enhanced.tsx +168 -25
  31. package/src/components/chat/chat-ref.types.ts +6 -0
  32. package/src/components/chat/chat-video-entity-card.tsx +66 -0
  33. package/src/components/chat/index.ts +2 -0
  34. package/dist/chunk-LI2C4ZCU.js.map +0 -1
  35. package/dist/chunk-SAWVZ5LA.cjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { forwardRef, memo, useMemo } from "react"
3
+ import React, { forwardRef, memo, useMemo } from "react"
4
4
  import { cn } from "../../utils/cn"
5
5
  import { SquareAvatar } from "../ui/square-avatar"
6
6
  import { ChatTypingIndicator } from "./chat-typing-indicator"
@@ -14,8 +14,17 @@ import { ThinkingDisplay } from "./thinking-display"
14
14
  import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
15
15
  import type { ChatRef } from "./chat-ref.types"
16
16
  import { remarkCardLinks } from "./remark-card-links"
17
+ import { BlockCard, type BlockCardProps } from "./block-card"
17
18
  import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from "./types"
18
19
 
20
+ /**
21
+ * Same regex shape as `remarkCardLinks` — kept in lockstep so the
22
+ * pre-scan and the remark plugin see the SAME set of markers. If the
23
+ * grammar widens (today: snake_case OR kebab-case; closer `]` OR `)`),
24
+ * both files must update.
25
+ */
26
+ const CARD_MARKER_REGEX = /\[card:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)[\])]/g
27
+
19
28
  function normalizeContent(content: MessageContent): MessageSegment[] {
20
29
  if (typeof content === 'string') {
21
30
  return content ? [{ type: 'text', text: content }] : []
@@ -63,15 +72,113 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
63
72
  () => (hasMarkerSupport ? [remarkCardLinks] : []),
64
73
  [hasMarkerSupport],
65
74
  )
75
+
76
+ const segments = useMemo(() => normalizeContent(content), [content])
77
+
78
+ /**
79
+ * Per-message rendering plan for `[card://type:id]` markers.
80
+ *
81
+ * Block-bearing markers SPLIT their containing text segment so the
82
+ * block payload (e.g. video player) renders AT THE MARKER POSITION
83
+ * in the text flow — not at the end of the segment, which causes
84
+ * the block to "appear high and drift down" while text streams in.
85
+ *
86
+ * Per-segment output is an array of parts:
87
+ * - `{ kind: 'text' }` — substring of the segment text, rendered
88
+ * through `<SimpleMarkdownRenderer>`. Ends
89
+ * with the block marker so the inline pill
90
+ * lands at the right spot via the `<a>`
91
+ * override.
92
+ * - `{ kind: 'block' }` — block payload, rendered as a sibling
93
+ * BELOW the preceding text chunk and above
94
+ * the next one.
95
+ *
96
+ * Inline-only markers (no `<BlockCard>` wrapper) do NOT split the
97
+ * segment; they're handled by the override at marker position via
98
+ * the shared `inlineByKey` map.
99
+ *
100
+ * Streaming behaviour: as a marker becomes complete in the streamed
101
+ * text, the regex matches, the segment splits at that point, and
102
+ * the block card lands right after the inline pill. Subsequent
103
+ * tokens render in the trailing chunk — block stays in position.
104
+ */
105
+ const renderingPlan = useMemo(() => {
106
+ if (!hasMarkerSupport) return null
107
+ const refs = chatRefs ?? {}
108
+ const render = renderEntityCard
109
+ const inlineByKey = new Map<string, React.ReactNode>()
110
+ type SegmentPart =
111
+ | { kind: 'text'; text: string }
112
+ | { kind: 'block'; key: string; node: React.ReactNode }
113
+ const partsBySegment = new Map<number, SegmentPart[]>()
114
+ if (!render) return { inlineByKey, partsBySegment }
115
+ const seenRendered = new Map<string, React.ReactNode>()
116
+ segments.forEach((segment, segIdx) => {
117
+ if (segment.type !== 'text') return
118
+ const text = segment.text
119
+ const parts: SegmentPart[] = []
120
+ let cursor = 0
121
+ CARD_MARKER_REGEX.lastIndex = 0
122
+ let match: RegExpExecArray | null
123
+ while ((match = CARD_MARKER_REGEX.exec(text)) !== null) {
124
+ const cardType = match[1]
125
+ const cardId = match[2]
126
+ const key = `${cardType}:${cardId}`
127
+ // Dedup renderEntityCard calls per unique key across the
128
+ // whole message — same key emitted twice (rare but possible)
129
+ // returns the cached node so React reuses the same instance.
130
+ let rendered = seenRendered.get(key)
131
+ if (!seenRendered.has(key)) {
132
+ const refMatch = refs[key]
133
+ rendered = refMatch ? render(refMatch) : undefined
134
+ seenRendered.set(key, rendered)
135
+ }
136
+ if (React.isValidElement(rendered) && rendered.type === BlockCard) {
137
+ const props = rendered.props as BlockCardProps
138
+ const markerEnd = match.index + match[0].length
139
+ // Text chunk INCLUDING the marker — the inline pill renders
140
+ // at the marker position via the `<a>` override.
141
+ parts.push({ kind: 'text', text: text.slice(cursor, markerEnd) })
142
+ parts.push({ kind: 'block', key, node: props.children })
143
+ cursor = markerEnd
144
+ const refMatch = refs[key]
145
+ inlineByKey.set(
146
+ key,
147
+ props.inline != null
148
+ ? props.inline
149
+ : <span className="text-ods-text-primary font-medium">{refMatch?.title ?? cardId}</span>,
150
+ )
151
+ } else if (rendered != null) {
152
+ // Inline-only — no split; remembered for the override.
153
+ inlineByKey.set(key, rendered)
154
+ }
155
+ }
156
+ // Trailing text after the last block marker (or the entire
157
+ // segment when no block markers fired).
158
+ if (cursor < text.length) {
159
+ parts.push({ kind: 'text', text: text.slice(cursor) })
160
+ }
161
+ // Only register the split plan when at least one block marker
162
+ // fired — otherwise the segment renders as one SimpleMarkdown-
163
+ // Renderer call (existing behaviour preserved for the
164
+ // overwhelming majority of segments that have no block cards).
165
+ if (parts.some((p) => p.kind === 'block')) {
166
+ partsBySegment.set(segIdx, parts)
167
+ }
168
+ })
169
+ return { inlineByKey, partsBySegment }
170
+ }, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
171
+
66
172
  const cardComponentOverrides = useMemo(() => {
67
173
  if (!hasMarkerSupport) return undefined
68
174
  const refs = chatRefs ?? {}
69
- const render = renderEntityCard
175
+ const inlineByKey = renderingPlan?.inlineByKey
70
176
  return {
71
- // Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`
72
- // and delegate rendering to the host. Other href schemes pass
73
- // through unchanged react-markdown's default `<a>` handler covers
74
- // them.
177
+ // Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`.
178
+ // The render result was pre-computed in `renderingPlan` so block-level
179
+ // payloads (e.g. video player cards) can be hoisted out of the
180
+ // paragraph as siblings — the inline pill stays at the marker
181
+ // position. Other href schemes pass through unchanged.
75
182
  a: ({ href, children, className: linkClassName, ...rest }: any) => {
76
183
  if (typeof href === 'string' && href.startsWith('card://')) {
77
184
  const stripped = href.slice('card://'.length)
@@ -80,15 +187,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
80
187
  const cardType = stripped.slice(0, sepIdx)
81
188
  const cardId = stripped.slice(sepIdx + 1)
82
189
  const key = `${cardType}:${cardId}`
83
- const refMatch: ChatRef | undefined = refs[key]
84
- if (refMatch && render) {
85
- const rendered = render(refMatch)
86
- if (rendered != null) return rendered
87
- }
190
+ const inline = inlineByKey?.get(key)
191
+ if (inline != null) return inline
88
192
  // No renderer, no ref, OR renderer returned null — fall back
89
193
  // to plain text title-only. Use any same-type ref's title if
90
194
  // available; otherwise the bare cardId. Never render the
91
195
  // literal `card://` URL.
196
+ const refMatch: ChatRef | undefined = refs[key]
92
197
  const fallbackTitle = (refMatch?.title)
93
198
  ?? Object.values(refs).find((r) => r.type === cardType)?.title
94
199
  ?? cardId
@@ -108,7 +213,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
108
213
  )
109
214
  },
110
215
  }
111
- }, [hasMarkerSupport, chatRefs, renderEntityCard])
216
+ }, [hasMarkerSupport, chatRefs, renderingPlan])
112
217
 
113
218
  const getAvatarProps = () => {
114
219
  const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
@@ -133,7 +238,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
133
238
  }
134
239
 
135
240
  const avatarProps = getAvatarProps()
136
- const segments = normalizeContent(content)
137
241
 
138
242
  const isSystem = authorType === 'system'
139
243
 
@@ -196,19 +300,58 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
196
300
  {(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
197
301
  {segments.map((segment, index) => {
198
302
  if (segment.type === 'text') {
303
+ const parts = renderingPlan?.partsBySegment.get(index)
304
+ const wrapperClass = cn(
305
+ "min-w-0 w-full break-words text-h4",
306
+ isError ? "text-ods-error" : "text-ods-text-primary",
307
+ )
308
+ // No block markers in this segment → single
309
+ // SimpleMarkdownRenderer call (existing behaviour
310
+ // preserved for the vast majority of messages).
311
+ if (!parts || parts.length === 0) {
312
+ return (
313
+ <div key={index} className={wrapperClass}>
314
+ <SimpleMarkdownRenderer
315
+ content={segment.text}
316
+ textSize="compact"
317
+ additionalRemarkPlugins={cardRemarkPlugins}
318
+ componentOverrides={cardComponentOverrides}
319
+ />
320
+ </div>
321
+ )
322
+ }
323
+ // Block markers present → split text at each marker
324
+ // and interleave block payloads. Each text chunk
325
+ // includes its trailing marker so the inline pill
326
+ // renders at the right position via the `<a>`
327
+ // override. Block payloads land AS SIBLINGS between
328
+ // text chunks — HTML-valid (block DOM never nests
329
+ // inside `<p>`) AND positionally correct (block
330
+ // appears where the marker is in the flow, not at
331
+ // the segment's end). Stable React keys come from
332
+ // the card key (block) and chunk position (text);
333
+ // streaming token-by-token reuses the same React
334
+ // instances so `<Video>` doesn't remount mid-play.
199
335
  return (
200
- <div key={index} className={cn(
201
- "min-w-0 w-full break-words text-h4",
202
- isError
203
- ? "text-ods-error"
204
- : "text-ods-text-primary"
205
- )}>
206
- <SimpleMarkdownRenderer
207
- content={segment.text}
208
- textSize="compact"
209
- additionalRemarkPlugins={cardRemarkPlugins}
210
- componentOverrides={cardComponentOverrides}
211
- />
336
+ <div key={index} className={wrapperClass}>
337
+ {parts.map((part, pIdx) => {
338
+ if (part.kind === 'text') {
339
+ return (
340
+ <SimpleMarkdownRenderer
341
+ key={`t-${pIdx}`}
342
+ content={part.text}
343
+ textSize="compact"
344
+ additionalRemarkPlugins={cardRemarkPlugins}
345
+ componentOverrides={cardComponentOverrides}
346
+ />
347
+ )
348
+ }
349
+ return (
350
+ <div key={`b-${part.key}`} className="my-3">
351
+ {part.node}
352
+ </div>
353
+ )
354
+ })}
212
355
  </div>
213
356
  )
214
357
  } else if (segment.type === 'tool_execution') {
@@ -14,6 +14,12 @@ export interface ChatRef {
14
14
  /** documentType from the host's RAG config (e.g. 'webinar', 'customer_interview').
15
15
  * Treated as opaque by the OSS-lib — the host owns the type vocabulary. */
16
16
  type: string
17
+ /** RagTableConfig.id (e.g. 'data-room-docs', 'webinars'). Drives the
18
+ * icon + label lookup in the host's `RAG_SOURCE_DISPLAY` registry —
19
+ * same direct-keyed lookup the chips + search results use. Optional
20
+ * for backward-compat with older wire payloads; when absent the host
21
+ * resolver falls back to reverse-mapping `type → sourceRepo`. */
22
+ sourceRepo?: string
17
23
  /** Primary-key value. Opaque string downstream. */
18
24
  id: string
19
25
  /** Display title — used for fallback rendering when the host's renderer
@@ -0,0 +1,66 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { EntityVideoSection } from '../features/entity-video-section'
5
+ import type { ChatRef } from './chat-ref.types'
6
+
7
+ /**
8
+ * <ChatVideoEntityCard> — chat-side inline render for video-bearing
9
+ * entity refs. Reuses `<EntityVideoSection>` (the SAME component the
10
+ * detail pages use for customer interviews, investor updates, product
11
+ * releases, and case studies) so the chat surface matches the public
12
+ * pages 1:1: Full Video / Highlights tabs, YouTube facade vs Mux HLS
13
+ * routing, native fullscreen, captions, posters.
14
+ *
15
+ * Stripped-chrome rendering: just the player + tabs, no title, no
16
+ * date, no Ask button. The inline pill at the marker position (in the
17
+ * paragraph above) already shows the title, date, and Ask affordance
18
+ * via the host's existing compact entity card. Duplicating those
19
+ * here doubles the UX and steals vertical space inside the chat
20
+ * panel. v6.1 §B.2.7.
21
+ *
22
+ * Activation contract: the host's dispatch routes here only when
23
+ * `chatRef.metadata.videoUrl`, `youtubeUrl`, or `highlightVideoUrl`
24
+ * is a non-empty string. URL safety is handled inside `<Video>` (the
25
+ * single source of truth for player URL routing) — no duplicate
26
+ * sanitization at the card level.
27
+ */
28
+ export interface ChatVideoEntityCardProps {
29
+ /** Required. `metadata.videoUrl` / `youtubeUrl` / `highlightVideoUrl`
30
+ * control routing inside `<EntityVideoSection>`. */
31
+ chatRef: ChatRef
32
+ }
33
+
34
+ function readString(value: unknown): string | null {
35
+ return typeof value === 'string' && value.length > 0 ? value : null
36
+ }
37
+
38
+ export function ChatVideoEntityCard({
39
+ chatRef,
40
+ }: ChatVideoEntityCardProps): React.ReactElement {
41
+ const m = chatRef.metadata ?? {}
42
+ const videoUrl = readString(m.videoUrl)
43
+ const youtubeUrl = readString(m.youtubeUrl)
44
+ const poster = readString(m.videoPoster)
45
+ const highlightUrl = readString(m.highlightVideoUrl)
46
+ const highlightPoster = readString(m.highlightVideoPoster)
47
+
48
+ // No wrapping Card / header — the player is the whole card. The
49
+ // 16:9 aspect / rounded-corners / border come from <Video>'s own
50
+ // `layout="centered"` styling inside <EntityVideoSection>.
51
+ return (
52
+ <EntityVideoSection
53
+ mainVideoUrl={videoUrl}
54
+ youtubeUrl={youtubeUrl}
55
+ highlightVideoUrl={highlightUrl}
56
+ highlightVideoThumbnail={highlightPoster}
57
+ mainVideoPoster={poster}
58
+ title={chatRef.title}
59
+ // Intentionally omitted for chat density:
60
+ // videoSummary — assistant text above already covers it.
61
+ // MarkdownRenderer — unused when videoSummary is omitted.
62
+ // videoBites — separate ask; chat doesn't surface bites
63
+ // today (planned follow-up).
64
+ />
65
+ )
66
+ }
@@ -12,6 +12,8 @@ export * from './chat-input'
12
12
  export * from './slash-command-suggestions'
13
13
  export * from './chat-message-enhanced'
14
14
  export * from './chat-message-list'
15
+ export * from './chat-video-entity-card'
16
+ export * from './block-card'
15
17
  export * from './chat-quick-action'
16
18
  export * from './chat-ticket-item'
17
19
  export * from './chat-ticket-list'