@flamingo-stack/openframe-frontend-core 0.0.186 → 0.0.188
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-FMWHOUFE.js → chunk-6XYXWVYY.js} +2 -2
- package/dist/{chunk-ALW3D72O.cjs → chunk-HIMEIPED.cjs} +9 -9
- package/dist/{chunk-ALW3D72O.cjs.map → chunk-HIMEIPED.cjs.map} +1 -1
- package/dist/{chunk-RZ3HHPQH.js → chunk-J2C2TI5Z.js} +2174 -2119
- package/dist/chunk-J2C2TI5Z.js.map +1 -0
- package/dist/{chunk-TMD5LDX4.cjs → chunk-VJTFBYVG.cjs} +57 -2
- package/dist/{chunk-TMD5LDX4.cjs.map → chunk-VJTFBYVG.cjs.map} +1 -1
- package/dist/{chunk-SAWVZ5LA.cjs → chunk-W5AWCFKE.cjs} +1641 -1537
- package/dist/chunk-W5AWCFKE.cjs.map +1 -0
- package/dist/{chunk-LI2C4ZCU.js → chunk-YOMHP4V3.js} +17410 -17306
- package/dist/chunk-YOMHP4V3.js.map +1 -0
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
- 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-message-list.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/context-compaction-display.d.ts.map +1 -1
- package/dist/components/chat/index.d.ts +2 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
- package/dist/components/chat/types/api.types.d.ts +2 -6
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +20 -0
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/network.types.d.ts +2 -0
- package/dist/components/chat/types/network.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +2 -6
- 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/extract-incomplete-message-state.d.ts +2 -6
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +2 -6
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/chat/utils/tool-call-helpers.d.ts +21 -2
- package/dist/components/chat/utils/tool-call-helpers.d.ts.map +1 -1
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/icons-v2-generated/index.cjs +4 -2
- package/dist/components/icons-v2-generated/index.cjs.map +1 -1
- package/dist/components/icons-v2-generated/index.d.ts +1 -0
- package/dist/components/icons-v2-generated/index.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/index.js +3 -1
- package/dist/components/icons-v2-generated/loaders/dots-loader-icon.d.ts +8 -0
- package/dist/components/icons-v2-generated/loaders/dots-loader-icon.d.ts.map +1 -0
- package/dist/components/icons-v2-generated/loaders/index.d.ts +2 -0
- package/dist/components/icons-v2-generated/loaders/index.d.ts.map +1 -0
- package/dist/components/index.cjs +6 -4
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +7 -5
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +0 -1
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +7 -5
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +6 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -5
- package/package.json +1 -1
- package/src/components/chat/approval-batch-message.tsx +4 -5
- package/src/components/chat/block-card.tsx +44 -0
- package/src/components/chat/chat-message-enhanced.tsx +168 -62
- package/src/components/chat/chat-message-list.tsx +57 -0
- 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/context-compaction-display.tsx +2 -3
- package/src/components/chat/index.ts +2 -0
- package/src/components/chat/thinking-display.tsx +2 -2
- package/src/components/chat/tool-execution-display.tsx +14 -11
- package/src/components/chat/types/api.types.ts +2 -2
- package/src/components/chat/types/message.types.ts +21 -0
- package/src/components/chat/types/network.types.ts +2 -0
- package/src/components/chat/types/processing.types.ts +2 -6
- package/src/components/chat/utils/chunk-parser.ts +2 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +4 -2
- package/src/components/chat/utils/message-segment-accumulator.ts +11 -2
- package/src/components/chat/utils/process-historical-messages.ts +2 -0
- package/src/components/chat/utils/tool-call-helpers.ts +97 -14
- package/src/components/icons-v2/loaders/dots-loader.svg +1 -0
- package/src/components/icons-v2-generated/index.ts +1 -0
- package/src/components/icons-v2-generated/loaders/dots-loader-icon.tsx +53 -0
- package/src/components/icons-v2-generated/loaders/index.ts +1 -0
- package/src/components/ui/index.ts +0 -2
- package/src/stories/DotsLoaderIcon.stories.tsx +103 -0
- package/dist/chunk-LI2C4ZCU.js.map +0 -1
- package/dist/chunk-RZ3HHPQH.js.map +0 -1
- package/dist/chunk-SAWVZ5LA.cjs.map +0 -1
- package/dist/components/ui/pulse-dots.d.ts +0 -7
- package/dist/components/ui/pulse-dots.d.ts.map +0 -1
- package/src/components/ui/pulse-dots.tsx +0 -56
- /package/dist/{chunk-FMWHOUFE.js.map → chunk-6XYXWVYY.js.map} +0 -0
|
@@ -1,10 +1,8 @@
|
|
|
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
|
-
import { ChatTypingIndicator } from "./chat-typing-indicator"
|
|
7
|
-
import { CyclingPhrase } from "./cycling-phrase"
|
|
8
6
|
import { ToolExecutionDisplay } from "./tool-execution-display"
|
|
9
7
|
import { ApprovalRequestMessage } from "./approval-request-message"
|
|
10
8
|
import { ApprovalBatchMessage } from "./approval-batch-message"
|
|
@@ -14,8 +12,17 @@ import { ThinkingDisplay } from "./thinking-display"
|
|
|
14
12
|
import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
|
|
15
13
|
import type { ChatRef } from "./chat-ref.types"
|
|
16
14
|
import { remarkCardLinks } from "./remark-card-links"
|
|
15
|
+
import { BlockCard, type BlockCardProps } from "./block-card"
|
|
17
16
|
import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from "./types"
|
|
18
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Same regex shape as `remarkCardLinks` — kept in lockstep so the
|
|
20
|
+
* pre-scan and the remark plugin see the SAME set of markers. If the
|
|
21
|
+
* grammar widens (today: snake_case OR kebab-case; closer `]` OR `)`),
|
|
22
|
+
* both files must update.
|
|
23
|
+
*/
|
|
24
|
+
const CARD_MARKER_REGEX = /\[card:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)[\])]/g
|
|
25
|
+
|
|
19
26
|
function normalizeContent(content: MessageContent): MessageSegment[] {
|
|
20
27
|
if (typeof content === 'string') {
|
|
21
28
|
return content ? [{ type: 'text', text: content }] : []
|
|
@@ -23,22 +30,6 @@ function normalizeContent(content: MessageContent): MessageSegment[] {
|
|
|
23
30
|
return content
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
// Flamingo-themed cycling words shown next to the streaming indicator
|
|
27
|
-
// (Claude-Code-style activity hint). Mix of classic AI verbs, flamingo
|
|
28
|
-
// behaviours (strut/wade/preen/flock), and one Mingo neologism.
|
|
29
|
-
const STREAMING_WORDS = [
|
|
30
|
-
'Thinking',
|
|
31
|
-
'Vibing',
|
|
32
|
-
'Mingoing',
|
|
33
|
-
'Strutting',
|
|
34
|
-
'Pondering',
|
|
35
|
-
'Wading',
|
|
36
|
-
'Hatching',
|
|
37
|
-
'Preening',
|
|
38
|
-
'Conjuring',
|
|
39
|
-
'Riffing',
|
|
40
|
-
] as const
|
|
41
|
-
|
|
42
33
|
const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>(
|
|
43
34
|
({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, renderEntityCard, ...props }, ref) => {
|
|
44
35
|
const isUser = role === 'user'
|
|
@@ -63,15 +54,113 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
63
54
|
() => (hasMarkerSupport ? [remarkCardLinks] : []),
|
|
64
55
|
[hasMarkerSupport],
|
|
65
56
|
)
|
|
57
|
+
|
|
58
|
+
const segments = useMemo(() => normalizeContent(content), [content])
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Per-message rendering plan for `[card://type:id]` markers.
|
|
62
|
+
*
|
|
63
|
+
* Block-bearing markers SPLIT their containing text segment so the
|
|
64
|
+
* block payload (e.g. video player) renders AT THE MARKER POSITION
|
|
65
|
+
* in the text flow — not at the end of the segment, which causes
|
|
66
|
+
* the block to "appear high and drift down" while text streams in.
|
|
67
|
+
*
|
|
68
|
+
* Per-segment output is an array of parts:
|
|
69
|
+
* - `{ kind: 'text' }` — substring of the segment text, rendered
|
|
70
|
+
* through `<SimpleMarkdownRenderer>`. Ends
|
|
71
|
+
* with the block marker so the inline pill
|
|
72
|
+
* lands at the right spot via the `<a>`
|
|
73
|
+
* override.
|
|
74
|
+
* - `{ kind: 'block' }` — block payload, rendered as a sibling
|
|
75
|
+
* BELOW the preceding text chunk and above
|
|
76
|
+
* the next one.
|
|
77
|
+
*
|
|
78
|
+
* Inline-only markers (no `<BlockCard>` wrapper) do NOT split the
|
|
79
|
+
* segment; they're handled by the override at marker position via
|
|
80
|
+
* the shared `inlineByKey` map.
|
|
81
|
+
*
|
|
82
|
+
* Streaming behaviour: as a marker becomes complete in the streamed
|
|
83
|
+
* text, the regex matches, the segment splits at that point, and
|
|
84
|
+
* the block card lands right after the inline pill. Subsequent
|
|
85
|
+
* tokens render in the trailing chunk — block stays in position.
|
|
86
|
+
*/
|
|
87
|
+
const renderingPlan = useMemo(() => {
|
|
88
|
+
if (!hasMarkerSupport) return null
|
|
89
|
+
const refs = chatRefs ?? {}
|
|
90
|
+
const render = renderEntityCard
|
|
91
|
+
const inlineByKey = new Map<string, React.ReactNode>()
|
|
92
|
+
type SegmentPart =
|
|
93
|
+
| { kind: 'text'; text: string }
|
|
94
|
+
| { kind: 'block'; key: string; node: React.ReactNode }
|
|
95
|
+
const partsBySegment = new Map<number, SegmentPart[]>()
|
|
96
|
+
if (!render) return { inlineByKey, partsBySegment }
|
|
97
|
+
const seenRendered = new Map<string, React.ReactNode>()
|
|
98
|
+
segments.forEach((segment, segIdx) => {
|
|
99
|
+
if (segment.type !== 'text') return
|
|
100
|
+
const text = segment.text
|
|
101
|
+
const parts: SegmentPart[] = []
|
|
102
|
+
let cursor = 0
|
|
103
|
+
CARD_MARKER_REGEX.lastIndex = 0
|
|
104
|
+
let match: RegExpExecArray | null
|
|
105
|
+
while ((match = CARD_MARKER_REGEX.exec(text)) !== null) {
|
|
106
|
+
const cardType = match[1]
|
|
107
|
+
const cardId = match[2]
|
|
108
|
+
const key = `${cardType}:${cardId}`
|
|
109
|
+
// Dedup renderEntityCard calls per unique key across the
|
|
110
|
+
// whole message — same key emitted twice (rare but possible)
|
|
111
|
+
// returns the cached node so React reuses the same instance.
|
|
112
|
+
let rendered = seenRendered.get(key)
|
|
113
|
+
if (!seenRendered.has(key)) {
|
|
114
|
+
const refMatch = refs[key]
|
|
115
|
+
rendered = refMatch ? render(refMatch) : undefined
|
|
116
|
+
seenRendered.set(key, rendered)
|
|
117
|
+
}
|
|
118
|
+
if (React.isValidElement(rendered) && rendered.type === BlockCard) {
|
|
119
|
+
const props = rendered.props as BlockCardProps
|
|
120
|
+
const markerEnd = match.index + match[0].length
|
|
121
|
+
// Text chunk INCLUDING the marker — the inline pill renders
|
|
122
|
+
// at the marker position via the `<a>` override.
|
|
123
|
+
parts.push({ kind: 'text', text: text.slice(cursor, markerEnd) })
|
|
124
|
+
parts.push({ kind: 'block', key, node: props.children })
|
|
125
|
+
cursor = markerEnd
|
|
126
|
+
const refMatch = refs[key]
|
|
127
|
+
inlineByKey.set(
|
|
128
|
+
key,
|
|
129
|
+
props.inline != null
|
|
130
|
+
? props.inline
|
|
131
|
+
: <span className="text-ods-text-primary font-medium">{refMatch?.title ?? cardId}</span>,
|
|
132
|
+
)
|
|
133
|
+
} else if (rendered != null) {
|
|
134
|
+
// Inline-only — no split; remembered for the override.
|
|
135
|
+
inlineByKey.set(key, rendered)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Trailing text after the last block marker (or the entire
|
|
139
|
+
// segment when no block markers fired).
|
|
140
|
+
if (cursor < text.length) {
|
|
141
|
+
parts.push({ kind: 'text', text: text.slice(cursor) })
|
|
142
|
+
}
|
|
143
|
+
// Only register the split plan when at least one block marker
|
|
144
|
+
// fired — otherwise the segment renders as one SimpleMarkdown-
|
|
145
|
+
// Renderer call (existing behaviour preserved for the
|
|
146
|
+
// overwhelming majority of segments that have no block cards).
|
|
147
|
+
if (parts.some((p) => p.kind === 'block')) {
|
|
148
|
+
partsBySegment.set(segIdx, parts)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
return { inlineByKey, partsBySegment }
|
|
152
|
+
}, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
|
|
153
|
+
|
|
66
154
|
const cardComponentOverrides = useMemo(() => {
|
|
67
155
|
if (!hasMarkerSupport) return undefined
|
|
68
156
|
const refs = chatRefs ?? {}
|
|
69
|
-
const
|
|
157
|
+
const inlineByKey = renderingPlan?.inlineByKey
|
|
70
158
|
return {
|
|
71
|
-
// Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
159
|
+
// Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`.
|
|
160
|
+
// The render result was pre-computed in `renderingPlan` so block-level
|
|
161
|
+
// payloads (e.g. video player cards) can be hoisted out of the
|
|
162
|
+
// paragraph as siblings — the inline pill stays at the marker
|
|
163
|
+
// position. Other href schemes pass through unchanged.
|
|
75
164
|
a: ({ href, children, className: linkClassName, ...rest }: any) => {
|
|
76
165
|
if (typeof href === 'string' && href.startsWith('card://')) {
|
|
77
166
|
const stripped = href.slice('card://'.length)
|
|
@@ -80,15 +169,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
80
169
|
const cardType = stripped.slice(0, sepIdx)
|
|
81
170
|
const cardId = stripped.slice(sepIdx + 1)
|
|
82
171
|
const key = `${cardType}:${cardId}`
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
const rendered = render(refMatch)
|
|
86
|
-
if (rendered != null) return rendered
|
|
87
|
-
}
|
|
172
|
+
const inline = inlineByKey?.get(key)
|
|
173
|
+
if (inline != null) return inline
|
|
88
174
|
// No renderer, no ref, OR renderer returned null — fall back
|
|
89
175
|
// to plain text title-only. Use any same-type ref's title if
|
|
90
176
|
// available; otherwise the bare cardId. Never render the
|
|
91
177
|
// literal `card://` URL.
|
|
178
|
+
const refMatch: ChatRef | undefined = refs[key]
|
|
92
179
|
const fallbackTitle = (refMatch?.title)
|
|
93
180
|
?? Object.values(refs).find((r) => r.type === cardType)?.title
|
|
94
181
|
?? cardId
|
|
@@ -108,7 +195,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
108
195
|
)
|
|
109
196
|
},
|
|
110
197
|
}
|
|
111
|
-
}, [hasMarkerSupport, chatRefs,
|
|
198
|
+
}, [hasMarkerSupport, chatRefs, renderingPlan])
|
|
112
199
|
|
|
113
200
|
const getAvatarProps = () => {
|
|
114
201
|
const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
|
|
@@ -133,20 +220,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
133
220
|
}
|
|
134
221
|
|
|
135
222
|
const avatarProps = getAvatarProps()
|
|
136
|
-
const segments = normalizeContent(content)
|
|
137
223
|
|
|
138
224
|
const isSystem = authorType === 'system'
|
|
139
225
|
|
|
140
|
-
// While `isTyping` says streaming is active, the agent is actually
|
|
141
|
-
// PAUSED whenever the last segment is an approval awaiting user
|
|
142
|
-
// input — pending status (or undefined, which is the initial state
|
|
143
|
-
// before backend confirms). Showing the typing indicator in this
|
|
144
|
-
// state would lie to the user about what's happening.
|
|
145
|
-
const lastSegment = segments[segments.length - 1]
|
|
146
|
-
const isPausedOnApproval = !!lastSegment
|
|
147
|
-
&& (lastSegment.type === 'approval_request' || lastSegment.type === 'approval_batch')
|
|
148
|
-
&& (lastSegment.status === undefined || lastSegment.status === 'pending')
|
|
149
|
-
|
|
150
226
|
return (
|
|
151
227
|
<div
|
|
152
228
|
ref={ref}
|
|
@@ -196,19 +272,58 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
196
272
|
{(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
|
|
197
273
|
{segments.map((segment, index) => {
|
|
198
274
|
if (segment.type === 'text') {
|
|
275
|
+
const parts = renderingPlan?.partsBySegment.get(index)
|
|
276
|
+
const wrapperClass = cn(
|
|
277
|
+
"min-w-0 w-full break-words text-h4",
|
|
278
|
+
isError ? "text-ods-error" : "text-ods-text-primary",
|
|
279
|
+
)
|
|
280
|
+
// No block markers in this segment → single
|
|
281
|
+
// SimpleMarkdownRenderer call (existing behaviour
|
|
282
|
+
// preserved for the vast majority of messages).
|
|
283
|
+
if (!parts || parts.length === 0) {
|
|
284
|
+
return (
|
|
285
|
+
<div key={index} className={wrapperClass}>
|
|
286
|
+
<SimpleMarkdownRenderer
|
|
287
|
+
content={segment.text}
|
|
288
|
+
textSize="compact"
|
|
289
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
290
|
+
componentOverrides={cardComponentOverrides}
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
// Block markers present → split text at each marker
|
|
296
|
+
// and interleave block payloads. Each text chunk
|
|
297
|
+
// includes its trailing marker so the inline pill
|
|
298
|
+
// renders at the right position via the `<a>`
|
|
299
|
+
// override. Block payloads land AS SIBLINGS between
|
|
300
|
+
// text chunks — HTML-valid (block DOM never nests
|
|
301
|
+
// inside `<p>`) AND positionally correct (block
|
|
302
|
+
// appears where the marker is in the flow, not at
|
|
303
|
+
// the segment's end). Stable React keys come from
|
|
304
|
+
// the card key (block) and chunk position (text);
|
|
305
|
+
// streaming token-by-token reuses the same React
|
|
306
|
+
// instances so `<Video>` doesn't remount mid-play.
|
|
199
307
|
return (
|
|
200
|
-
<div key={index} className={
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
308
|
+
<div key={index} className={wrapperClass}>
|
|
309
|
+
{parts.map((part, pIdx) => {
|
|
310
|
+
if (part.kind === 'text') {
|
|
311
|
+
return (
|
|
312
|
+
<SimpleMarkdownRenderer
|
|
313
|
+
key={`t-${pIdx}`}
|
|
314
|
+
content={part.text}
|
|
315
|
+
textSize="compact"
|
|
316
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
317
|
+
componentOverrides={cardComponentOverrides}
|
|
318
|
+
/>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
return (
|
|
322
|
+
<div key={`b-${part.key}`} className="my-3">
|
|
323
|
+
{part.node}
|
|
324
|
+
</div>
|
|
325
|
+
)
|
|
326
|
+
})}
|
|
212
327
|
</div>
|
|
213
328
|
)
|
|
214
329
|
} else if (segment.type === 'tool_execution') {
|
|
@@ -265,15 +380,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
265
380
|
}
|
|
266
381
|
return null
|
|
267
382
|
})}
|
|
268
|
-
{isTyping && !isPausedOnApproval && (
|
|
269
|
-
<div className="flex items-center gap-3">
|
|
270
|
-
<ChatTypingIndicator />
|
|
271
|
-
<CyclingPhrase
|
|
272
|
-
words={STREAMING_WORDS}
|
|
273
|
-
className="text-ods-text-secondary text-body-sm"
|
|
274
|
-
/>
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
383
|
</div>}
|
|
278
384
|
</div>
|
|
279
385
|
</div>
|
|
@@ -5,8 +5,26 @@ import { useStickToBottom } from "use-stick-to-bottom"
|
|
|
5
5
|
import { cn } from "../../utils/cn"
|
|
6
6
|
import { ChatMessageEnhanced } from "./chat-message-enhanced"
|
|
7
7
|
import { ChatMessageListSkeleton } from "./chat-message-skeleton"
|
|
8
|
+
import { DotsLoaderIcon } from "../icons-v2-generated"
|
|
9
|
+
import { CyclingPhrase } from "./cycling-phrase"
|
|
8
10
|
import type { ChatMessageListProps } from "./types"
|
|
9
11
|
|
|
12
|
+
// Flamingo-themed cycling words shown next to the streaming indicator
|
|
13
|
+
// (Claude-Code-style activity hint). Mix of classic AI verbs, flamingo
|
|
14
|
+
// behaviours (strut/wade/preen/flock), and one Mingo neologism.
|
|
15
|
+
const STREAMING_WORDS = [
|
|
16
|
+
'Thinking',
|
|
17
|
+
'Vibing',
|
|
18
|
+
'Mingoing',
|
|
19
|
+
'Strutting',
|
|
20
|
+
'Pondering',
|
|
21
|
+
'Wading',
|
|
22
|
+
'Hatching',
|
|
23
|
+
'Preening',
|
|
24
|
+
'Conjuring',
|
|
25
|
+
'Riffing',
|
|
26
|
+
] as const
|
|
27
|
+
|
|
10
28
|
/*
|
|
11
29
|
* Stick-to-bottom: `use-stick-to-bottom` (stackblitz-labs)
|
|
12
30
|
*
|
|
@@ -243,6 +261,26 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
243
261
|
contentRef(el)
|
|
244
262
|
}
|
|
245
263
|
|
|
264
|
+
// Footer-pinned streaming loader. Rendered OUTSIDE the scroller (a
|
|
265
|
+
// sibling of the scroll div, like the sticky approvals below) so it
|
|
266
|
+
// stays put while messages stream and grow — no jitter from the
|
|
267
|
+
// last-message height changing under it.
|
|
268
|
+
//
|
|
269
|
+
// Hidden when the agent is actually PAUSED waiting on the user: the
|
|
270
|
+
// last message's last segment is a pending approval (inline), or
|
|
271
|
+
// there are escalated pending approvals shown in the sticky bar.
|
|
272
|
+
const lastMessage = messages[messages.length - 1]
|
|
273
|
+
const lastSegments = Array.isArray(lastMessage?.content) ? lastMessage.content : null
|
|
274
|
+
const lastSegment = lastSegments?.[lastSegments.length - 1]
|
|
275
|
+
const isPausedOnApproval =
|
|
276
|
+
!!lastSegment &&
|
|
277
|
+
(lastSegment.type === 'approval_request' || lastSegment.type === 'approval_batch') &&
|
|
278
|
+
(lastSegment.status === undefined || lastSegment.status === 'pending')
|
|
279
|
+
const showStreamingLoader =
|
|
280
|
+
isTyping &&
|
|
281
|
+
!isPausedOnApproval &&
|
|
282
|
+
!(pendingApprovals && pendingApprovals.length > 0)
|
|
283
|
+
|
|
246
284
|
return (
|
|
247
285
|
<div className="relative flex-1 min-h-0 flex flex-col">
|
|
248
286
|
<div
|
|
@@ -286,6 +324,25 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
286
324
|
</div>
|
|
287
325
|
</div>
|
|
288
326
|
|
|
327
|
+
{/* Footer-pinned streaming loader — outside the scroller so it
|
|
328
|
+
doesn't jitter as the streaming message grows. Color is set
|
|
329
|
+
via inline style (CSS var) rather than a Tailwind class so it
|
|
330
|
+
is JIT-independent when this lib is consumed via yalc. */}
|
|
331
|
+
{showStreamingLoader && (
|
|
332
|
+
<div
|
|
333
|
+
className={cn(
|
|
334
|
+
"mx-auto w-full max-w-3xl flex items-center gap-1 py-2",
|
|
335
|
+
contentClassName || "px-4",
|
|
336
|
+
)}
|
|
337
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
338
|
+
role="status"
|
|
339
|
+
aria-live="polite"
|
|
340
|
+
>
|
|
341
|
+
<DotsLoaderIcon className="w-6 h-6" />
|
|
342
|
+
<CyclingPhrase words={STREAMING_WORDS} className="text-sm" />
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
|
|
289
346
|
{/* Sticky Pending Approvals — outside the scroller; same
|
|
290
347
|
structure as the v1 baseline. The library's RO watches
|
|
291
348
|
`contentRef` (INSIDE the scroller), so height changes to
|
|
@@ -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
|
+
}
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
import { forwardRef } from "react"
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../utils/cn"
|
|
6
|
-
import { CheckCircleIcon } from "../icons-v2-generated"
|
|
7
|
-
import { PulseDots } from "../ui/pulse-dots"
|
|
6
|
+
import { CheckCircleIcon, DotsLoaderIcon } from "../icons-v2-generated"
|
|
8
7
|
import type { ContextCompactionDisplayProps } from "./types"
|
|
9
8
|
|
|
10
9
|
|
|
@@ -28,7 +27,7 @@ const ContextCompactionDisplay = forwardRef<HTMLDivElement, ContextCompactionDis
|
|
|
28
27
|
{label}
|
|
29
28
|
</span>
|
|
30
29
|
{isStarted ? (
|
|
31
|
-
<
|
|
30
|
+
<DotsLoaderIcon size={16} className="text-ods-text-secondary" />
|
|
32
31
|
) : (
|
|
33
32
|
<CheckCircleIcon className="w-4 h-4 text-ods-success shrink-0" />
|
|
34
33
|
)}
|
|
@@ -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'
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { forwardRef, useState } from "react"
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../utils/cn"
|
|
6
|
-
import {
|
|
6
|
+
import { DotsLoaderIcon } from "../icons-v2-generated"
|
|
7
7
|
import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
|
|
8
8
|
import { ExpandChevron } from "./expand-chevron"
|
|
9
9
|
import { useCollapsible } from "./hooks/use-collapsible"
|
|
@@ -97,7 +97,7 @@ const ThinkingDisplay = forwardRef<HTMLDivElement, ThinkingDisplayProps>(
|
|
|
97
97
|
<span className="text-sm font-medium leading-snug text-ods-text-secondary">
|
|
98
98
|
{label}:
|
|
99
99
|
</span>
|
|
100
|
-
{isStreaming && <
|
|
100
|
+
{isStreaming && <DotsLoaderIcon size={16} className="text-ods-text-secondary" />}
|
|
101
101
|
</div>
|
|
102
102
|
|
|
103
103
|
<div className="flex-1 min-w-0" style={containerStyle}>
|
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
import { forwardRef, useMemo, useState } from "react"
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../utils/cn"
|
|
6
|
-
import { CheckCircleIcon, XmarkCircleIcon } from "../icons-v2-generated"
|
|
6
|
+
import { CheckCircleIcon, DotsLoaderIcon, XmarkCircleIcon } from "../icons-v2-generated"
|
|
7
7
|
import { ToolType } from "../platform"
|
|
8
8
|
import { ToolIcon } from "../tool-icon"
|
|
9
|
-
import { PulseDots } from "../ui/pulse-dots"
|
|
10
9
|
import { ExpandChevron } from "./expand-chevron"
|
|
11
10
|
import { useCollapsible } from "./hooks/use-collapsible"
|
|
12
11
|
import { ArgRow, ResultBlock } from "./tool-call-blocks"
|
|
13
|
-
import { COMMAND_BODY_ARG_KEYS } from "./utils/tool-call-helpers"
|
|
12
|
+
import { COMMAND_BODY_ARG_KEYS, getToolCallTitle } from "./utils/tool-call-helpers"
|
|
14
13
|
import type { ToolExecutionDisplayProps } from "./types"
|
|
15
14
|
|
|
16
15
|
const COMMAND_BODY_KEYS = new Set<string>(COMMAND_BODY_ARG_KEYS)
|
|
@@ -24,11 +23,15 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
|
|
|
24
23
|
const isExecuted = message.type === "EXECUTED_TOOL"
|
|
25
24
|
const integratedToolType = (message.integratedToolType as ToolType) || ("OPENFRAME" as ToolType)
|
|
26
25
|
|
|
27
|
-
const previewText = useMemo(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
const previewText = useMemo(
|
|
27
|
+
() =>
|
|
28
|
+
getToolCallTitle({
|
|
29
|
+
args: message.parameters,
|
|
30
|
+
title: message.toolTitle,
|
|
31
|
+
name: message.toolFunction,
|
|
32
|
+
}),
|
|
33
|
+
[message.parameters, message.toolTitle, message.toolFunction],
|
|
34
|
+
)
|
|
32
35
|
|
|
33
36
|
const argEntries = useMemo<Array<[string, unknown]>>(() => {
|
|
34
37
|
if (!message.parameters || typeof message.parameters !== "object") return []
|
|
@@ -41,7 +44,7 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
|
|
|
41
44
|
const hasBody = argEntries.length > 0 || hasResult || isExecuting
|
|
42
45
|
|
|
43
46
|
const renderStatusIcon = () => {
|
|
44
|
-
if (isExecuting) return <
|
|
47
|
+
if (isExecuting) return <DotsLoaderIcon size={16} className="text-ods-text-secondary" />
|
|
45
48
|
if (isExecuted && message.success === true) return <CheckCircleIcon className="w-4 h-4 text-ods-success" />
|
|
46
49
|
if (isExecuted && message.success === false) return <XmarkCircleIcon className="w-4 h-4 text-ods-error" />
|
|
47
50
|
return null
|
|
@@ -72,7 +75,7 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
|
|
|
72
75
|
: "text-ods-text-secondary line-clamp-2 max-h-10 break-all",
|
|
73
76
|
)}
|
|
74
77
|
>
|
|
75
|
-
{
|
|
78
|
+
{previewText}
|
|
76
79
|
</div>
|
|
77
80
|
<div className="flex items-center justify-center shrink-0 w-5 h-5">{renderStatusIcon()}</div>
|
|
78
81
|
<div className="flex items-center justify-center shrink-0 w-5 h-5">
|
|
@@ -91,7 +94,7 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
|
|
|
91
94
|
{isExecuting && (
|
|
92
95
|
<div className="flex flex-col gap-1 items-start w-full">
|
|
93
96
|
<span className="text-ods-text-secondary">Result:</span>
|
|
94
|
-
<
|
|
97
|
+
<DotsLoaderIcon size={16} className="text-ods-text-secondary" />
|
|
95
98
|
</div>
|
|
96
99
|
)}
|
|
97
100
|
</div>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ChunkData, NatsMessageType, FetchChunksFunction } from './network.types'
|
|
7
7
|
import type { ChatType, ChatApprovalStatus } from './chat.types'
|
|
8
|
-
import type { MessageSegment, PendingToolCallData, TokenUsageData } from './message.types'
|
|
8
|
+
import type { MessageSegment, PendingToolCallData, TokenUsageData, ExecutingToolState } from './message.types'
|
|
9
9
|
|
|
10
10
|
// ========== Hook Options ==========
|
|
11
11
|
|
|
@@ -150,7 +150,7 @@ export interface UseRealtimeChunkProcessorOptions {
|
|
|
150
150
|
/** Pending approvals that haven't been resolved */
|
|
151
151
|
pendingApprovals?: Map<string, { command: string; explanation?: string; approvalType: string }>
|
|
152
152
|
/** Executing tools waiting for completion */
|
|
153
|
-
executingTools?: Map<string,
|
|
153
|
+
executingTools?: Map<string, ExecutingToolState>
|
|
154
154
|
/** Escalated approvals */
|
|
155
155
|
escalatedApprovals?: Map<
|
|
156
156
|
string,
|
|
@@ -35,6 +35,8 @@ export interface ToolExecutionData {
|
|
|
35
35
|
type: 'EXECUTING_TOOL' | 'EXECUTED_TOOL'
|
|
36
36
|
integratedToolType: string
|
|
37
37
|
toolFunction: string
|
|
38
|
+
/** Backend-issued human-readable title (mirrors `PendingToolCallData.toolTitle`). */
|
|
39
|
+
toolTitle?: string
|
|
38
40
|
parameters?: Record<string, any>
|
|
39
41
|
result?: string
|
|
40
42
|
success?: boolean
|
|
@@ -46,6 +48,21 @@ export interface ToolExecutionData {
|
|
|
46
48
|
toolExecutionRequestId?: string
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Snapshot of an in-flight tool kept between the `EXECUTING_TOOL` and
|
|
53
|
+
* `EXECUTED_TOOL` events. The backend only sends `toolTitle` on
|
|
54
|
+
* `EXECUTING_TOOL`; carrying this state lets the accumulator restore it onto
|
|
55
|
+
* the merged `EXECUTED_TOOL` segment instead of falling back to the raw
|
|
56
|
+
* `toolFunction`.
|
|
57
|
+
*/
|
|
58
|
+
export interface ExecutingToolState {
|
|
59
|
+
integratedToolType: string
|
|
60
|
+
toolFunction: string
|
|
61
|
+
/** Mirrors {@link ToolExecutionData.toolTitle}; absent on `EXECUTED_TOOL`. */
|
|
62
|
+
toolTitle?: string
|
|
63
|
+
parameters?: Record<string, any>
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
// ========== Approval Request Types ==========
|
|
50
67
|
|
|
51
68
|
export interface ApprovalRequestData {
|
|
@@ -171,6 +188,8 @@ export interface ExecutingToolMessageData extends MessageDataBase {
|
|
|
171
188
|
type: 'EXECUTING_TOOL'
|
|
172
189
|
integratedToolType?: string
|
|
173
190
|
toolFunction?: string
|
|
191
|
+
/** Backend-issued human-readable title (wire field, mirrors `ChunkData.title`). */
|
|
192
|
+
title?: string
|
|
174
193
|
parameters?: Record<string, any>
|
|
175
194
|
toolExecutionRequestId?: string
|
|
176
195
|
}
|
|
@@ -179,6 +198,8 @@ export interface ExecutedToolMessageData extends MessageDataBase {
|
|
|
179
198
|
type: 'EXECUTED_TOOL'
|
|
180
199
|
integratedToolType?: string
|
|
181
200
|
toolFunction?: string
|
|
201
|
+
/** Backend-issued human-readable title (wire field, mirrors `ChunkData.title`). */
|
|
202
|
+
title?: string
|
|
182
203
|
parameters?: Record<string, any>
|
|
183
204
|
result?: string
|
|
184
205
|
success?: boolean
|
|
@@ -38,6 +38,8 @@ export interface ChunkData {
|
|
|
38
38
|
text?: string
|
|
39
39
|
integratedToolType?: string
|
|
40
40
|
toolFunction?: string
|
|
41
|
+
/** Execution chunks carry the human-readable title as `title`. */
|
|
42
|
+
title?: string
|
|
41
43
|
parameters?: Record<string, any>
|
|
42
44
|
result?: string
|
|
43
45
|
success?: boolean
|