@flamingo-stack/openframe-frontend-core 0.0.186 → 0.0.188-snapshot.20260515214641
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-LI2C4ZCU.js → chunk-HOMIFMOO.js} +17412 -17308
- package/dist/chunk-HOMIFMOO.js.map +1 -0
- package/dist/{chunk-RZ3HHPQH.js → chunk-J2C2TI5Z.js} +2174 -2119
- package/dist/chunk-J2C2TI5Z.js.map +1 -0
- package/dist/{chunk-SAWVZ5LA.cjs → chunk-SA7CUXAO.cjs} +1643 -1539
- package/dist/chunk-SA7CUXAO.cjs.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/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/dist/types/marketing.d.ts +1 -1
- package/dist/types/marketing.d.ts.map +1 -1
- package/dist/types/video-processing.d.ts +2 -1
- package/dist/types/video-processing.d.ts.map +1 -1
- 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 +177 -64
- 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/src/types/marketing.ts +1 -0
- package/src/types/video-processing.ts +3 -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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
export interface BlockCardProps {
|
|
6
|
+
/** The inline pill rendered AT the marker position inside the
|
|
7
|
+
* paragraph. Typically the existing compact entity card (e.g. the
|
|
8
|
+
* 56×56 thumbnail card for webinars / customer interviews). When
|
|
9
|
+
* omitted, the marker is replaced by the ref title in a plain
|
|
10
|
+
* inline `<span>`. */
|
|
11
|
+
inline?: React.ReactNode
|
|
12
|
+
/** The block-level content rendered as a SIBLING below the markdown
|
|
13
|
+
* paragraph. Use for elements that cannot legally nest inside
|
|
14
|
+
* `<p>` — `<div>`, `<header>`, `<EntityVideoSection>`, `<Video>`,
|
|
15
|
+
* etc. The pre-scan in `chat-message-enhanced` extracts this and
|
|
16
|
+
* appends it after the `<SimpleMarkdownRenderer>` output for the
|
|
17
|
+
* containing segment, where block content is HTML-valid. */
|
|
18
|
+
children: React.ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sentinel wrapper for `renderEntityCard` returns whose payload is
|
|
23
|
+
* block-level (divs / header / `<EntityVideoSection>` / `<Video>`) and
|
|
24
|
+
* therefore cannot legally render inside the assistant message's
|
|
25
|
+
* markdown paragraph.
|
|
26
|
+
*
|
|
27
|
+
* The chat renderer (`chat-message-enhanced.tsx`) does a pre-scan of
|
|
28
|
+
* each `text` segment's `[card://type:id]` markers, calls
|
|
29
|
+
* `renderEntityCard(ref)` for each, and detects `<BlockCard>` by React
|
|
30
|
+
* element type identity. When detected:
|
|
31
|
+
* 1. `inline` is placed AT the marker position inside the `<p>` —
|
|
32
|
+
* keeps the existing compact-card UX intact.
|
|
33
|
+
* 2. `children` is appended as a sibling BELOW the paragraph — where
|
|
34
|
+
* block content is HTML-valid and the browser doesn't eject it
|
|
35
|
+
* with a hydration mismatch.
|
|
36
|
+
*
|
|
37
|
+
* The component itself never renders — it's a tag, not a wrapper. The
|
|
38
|
+
* `return null` is unreachable in practice (the pre-scan replaces every
|
|
39
|
+
* `<BlockCard>` before render), but kept as a defensive no-op so a
|
|
40
|
+
* mis-routed `<BlockCard>` outside the chat path doesn't crash.
|
|
41
|
+
*/
|
|
42
|
+
export function BlockCard(_props: BlockCardProps): React.ReactElement | null {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
@@ -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,27 +169,32 @@ 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
|
|
95
182
|
return <span className="text-ods-text-primary">{fallbackTitle}</span>
|
|
96
183
|
}
|
|
97
184
|
}
|
|
185
|
+
// Chat-context links ALWAYS open in a new tab — same rule
|
|
186
|
+
// every chat-inline entity card uses via `navLinkProps({
|
|
187
|
+
// alwaysNewTab: true })`. The user's CURRENT context is the
|
|
188
|
+
// conversation; navigating away in-place would unmount the
|
|
189
|
+
// chat and lose the thread. Applies to absolute (http) AND
|
|
190
|
+
// relative (`/foo/bar`) hrefs so the rule is one line, not
|
|
191
|
+
// a branching check.
|
|
98
192
|
return (
|
|
99
193
|
<a
|
|
100
194
|
href={href}
|
|
101
195
|
className={linkClassName}
|
|
102
|
-
target={typeof href === 'string' && href.
|
|
103
|
-
rel={typeof href === 'string' && href.
|
|
196
|
+
target={typeof href === 'string' && href.length > 0 ? '_blank' : undefined}
|
|
197
|
+
rel={typeof href === 'string' && href.length > 0 ? 'noopener noreferrer' : undefined}
|
|
104
198
|
{...rest}
|
|
105
199
|
>
|
|
106
200
|
{children}
|
|
@@ -108,7 +202,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
108
202
|
)
|
|
109
203
|
},
|
|
110
204
|
}
|
|
111
|
-
}, [hasMarkerSupport, chatRefs,
|
|
205
|
+
}, [hasMarkerSupport, chatRefs, renderingPlan])
|
|
112
206
|
|
|
113
207
|
const getAvatarProps = () => {
|
|
114
208
|
const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
|
|
@@ -133,20 +227,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
133
227
|
}
|
|
134
228
|
|
|
135
229
|
const avatarProps = getAvatarProps()
|
|
136
|
-
const segments = normalizeContent(content)
|
|
137
230
|
|
|
138
231
|
const isSystem = authorType === 'system'
|
|
139
232
|
|
|
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
233
|
return (
|
|
151
234
|
<div
|
|
152
235
|
ref={ref}
|
|
@@ -196,19 +279,58 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
196
279
|
{(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
|
|
197
280
|
{segments.map((segment, index) => {
|
|
198
281
|
if (segment.type === 'text') {
|
|
282
|
+
const parts = renderingPlan?.partsBySegment.get(index)
|
|
283
|
+
const wrapperClass = cn(
|
|
284
|
+
"min-w-0 w-full break-words text-h4",
|
|
285
|
+
isError ? "text-ods-error" : "text-ods-text-primary",
|
|
286
|
+
)
|
|
287
|
+
// No block markers in this segment → single
|
|
288
|
+
// SimpleMarkdownRenderer call (existing behaviour
|
|
289
|
+
// preserved for the vast majority of messages).
|
|
290
|
+
if (!parts || parts.length === 0) {
|
|
291
|
+
return (
|
|
292
|
+
<div key={index} className={wrapperClass}>
|
|
293
|
+
<SimpleMarkdownRenderer
|
|
294
|
+
content={segment.text}
|
|
295
|
+
textSize="compact"
|
|
296
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
297
|
+
componentOverrides={cardComponentOverrides}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
// Block markers present → split text at each marker
|
|
303
|
+
// and interleave block payloads. Each text chunk
|
|
304
|
+
// includes its trailing marker so the inline pill
|
|
305
|
+
// renders at the right position via the `<a>`
|
|
306
|
+
// override. Block payloads land AS SIBLINGS between
|
|
307
|
+
// text chunks — HTML-valid (block DOM never nests
|
|
308
|
+
// inside `<p>`) AND positionally correct (block
|
|
309
|
+
// appears where the marker is in the flow, not at
|
|
310
|
+
// the segment's end). Stable React keys come from
|
|
311
|
+
// the card key (block) and chunk position (text);
|
|
312
|
+
// streaming token-by-token reuses the same React
|
|
313
|
+
// instances so `<Video>` doesn't remount mid-play.
|
|
199
314
|
return (
|
|
200
|
-
<div key={index} className={
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
315
|
+
<div key={index} className={wrapperClass}>
|
|
316
|
+
{parts.map((part, pIdx) => {
|
|
317
|
+
if (part.kind === 'text') {
|
|
318
|
+
return (
|
|
319
|
+
<SimpleMarkdownRenderer
|
|
320
|
+
key={`t-${pIdx}`}
|
|
321
|
+
content={part.text}
|
|
322
|
+
textSize="compact"
|
|
323
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
324
|
+
componentOverrides={cardComponentOverrides}
|
|
325
|
+
/>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
return (
|
|
329
|
+
<div key={`b-${part.key}`} className="my-3">
|
|
330
|
+
{part.node}
|
|
331
|
+
</div>
|
|
332
|
+
)
|
|
333
|
+
})}
|
|
212
334
|
</div>
|
|
213
335
|
)
|
|
214
336
|
} else if (segment.type === 'tool_execution') {
|
|
@@ -265,15 +387,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
265
387
|
}
|
|
266
388
|
return null
|
|
267
389
|
})}
|
|
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
390
|
</div>}
|
|
278
391
|
</div>
|
|
279
392
|
</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,
|