@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.
- package/dist/{chunk-SAWVZ5LA.cjs → chunk-EUTOT74J.cjs} +1261 -1159
- package/dist/chunk-EUTOT74J.cjs.map +1 -0
- package/dist/{chunk-LI2C4ZCU.js → chunk-M36UJN3T.js} +10587 -10485
- package/dist/chunk-M36UJN3T.js.map +1 -0
- package/dist/components/chat/block-card.d.ts +39 -0
- package/dist/components/chat/block-card.d.ts.map +1 -0
- package/dist/components/chat/chat-message-enhanced.d.ts +2 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-ref.types.d.ts +6 -0
- package/dist/components/chat/chat-ref.types.d.ts.map +1 -1
- package/dist/components/chat/chat-video-entity-card.d.ts +30 -0
- package/dist/components/chat/chat-video-entity-card.d.ts.map +1 -0
- package/dist/components/chat/index.d.ts +2 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/index.cjs +6 -2
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +5 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/ui/index.cjs +6 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -1
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -1
- package/package.json +1 -1
- package/src/components/chat/block-card.tsx +44 -0
- package/src/components/chat/chat-message-enhanced.tsx +168 -25
- package/src/components/chat/chat-ref.types.ts +6 -0
- package/src/components/chat/chat-video-entity-card.tsx +66 -0
- package/src/components/chat/index.ts +2 -0
- package/dist/chunk-LI2C4ZCU.js.map +0 -1
- 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
|
|
175
|
+
const inlineByKey = renderingPlan?.inlineByKey
|
|
70
176
|
return {
|
|
71
|
-
// Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
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
|
|
84
|
-
if (
|
|
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,
|
|
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={
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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'
|