@flamingo-stack/openframe-frontend-core 0.0.223 → 0.0.224
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-4WBVMIKQ.cjs → chunk-7JGR67H2.cjs} +24 -24
- package/dist/{chunk-4WBVMIKQ.cjs.map → chunk-7JGR67H2.cjs.map} +1 -1
- package/dist/{chunk-ENDUMC7Z.cjs → chunk-AENTBOSN.cjs} +118 -70
- package/dist/chunk-AENTBOSN.cjs.map +1 -0
- package/dist/{chunk-4VHWSAQR.js → chunk-BNYPQCKL.js} +304 -256
- package/dist/chunk-BNYPQCKL.js.map +1 -0
- package/dist/{chunk-GYYGA24C.js → chunk-LEHEHEVW.js} +2 -2
- package/dist/{chunk-TA6Z2AKD.js → chunk-OAOLVEYW.js} +2 -2
- package/dist/{chunk-7TKYFZ3L.cjs → chunk-SZCTY3YF.cjs} +7 -7
- package/dist/{chunk-7TKYFZ3L.cjs.map → chunk-SZCTY3YF.cjs.map} +1 -1
- package/dist/{chunk-ISWWP2BH.js → chunk-V6ME2VMU.js} +2 -2
- package/dist/{chunk-ABBU7QOM.cjs → chunk-WIAQRCJ5.cjs} +10 -10
- package/dist/{chunk-ABBU7QOM.cjs.map → chunk-WIAQRCJ5.cjs.map} +1 -1
- package/dist/{chunk-WOKB6RJ7.js → chunk-YEOM52ZR.js} +2 -2
- package/dist/{chunk-KXLTWANQ.cjs → chunk-ZCHTRQBO.cjs} +9 -9
- package/dist/{chunk-KXLTWANQ.cjs.map → chunk-ZCHTRQBO.cjs.map} +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +10 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/index.cjs +77 -77
- package/dist/components/index.js +4 -4
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +25 -25
- package/dist/components/onboarding-guides/index.js +3 -3
- package/dist/components/tickets/index.cjs +64 -64
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +9 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-message-enhanced.tsx +67 -14
- package/src/components/chat/embeddable-chat.tsx +51 -13
- package/src/components/chat/hooks/use-chat-card-item.ts +17 -1
- package/src/components/chat/hooks/use-unified-chat.ts +41 -29
- package/src/components/chat/types/unified-chat-state.types.ts +11 -0
- package/src/components/navigation/app-layout-drawer.tsx +4 -4
- package/src/components/ui/simple-markdown-renderer.tsx +12 -2
- package/dist/chunk-4VHWSAQR.js.map +0 -1
- package/dist/chunk-ENDUMC7Z.cjs.map +0 -1
- /package/dist/{chunk-GYYGA24C.js.map → chunk-LEHEHEVW.js.map} +0 -0
- /package/dist/{chunk-TA6Z2AKD.js.map → chunk-OAOLVEYW.js.map} +0 -0
- /package/dist/{chunk-ISWWP2BH.js.map → chunk-V6ME2VMU.js.map} +0 -0
- /package/dist/{chunk-WOKB6RJ7.js.map → chunk-YEOM52ZR.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import React, { forwardRef, memo, useMemo } from "react"
|
|
3
|
+
import React, { forwardRef, memo, useMemo, useRef } from "react"
|
|
4
4
|
import { cn } from "../../utils/cn"
|
|
5
5
|
import { SquareAvatar } from "../ui/square-avatar"
|
|
6
6
|
import { ToolExecutionDisplay } from "./tool-execution-display"
|
|
@@ -57,6 +57,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
57
57
|
|
|
58
58
|
const segments = useMemo(() => normalizeContent(content), [content])
|
|
59
59
|
|
|
60
|
+
// Cross-render cache of rendered inline-card nodes, keyed by `type:id`.
|
|
61
|
+
// A fetch-mode card lives inside the assistant message, which re-renders on
|
|
62
|
+
// every stream chunk (and the whole list re-renders when a new message
|
|
63
|
+
// arrives). `renderEntityCard` produces a FRESH element each time, so React
|
|
64
|
+
// re-mounts the card — closing any open menu/popover and re-triggering its
|
|
65
|
+
// fetch. Caching the produced node by key and returning the SAME element
|
|
66
|
+
// reference lets React bail out of re-rendering that subtree, so the card
|
|
67
|
+
// (and its open menu) survives across chunks. Invalidated per key when the
|
|
68
|
+
// backing ref or the render fn identity changes.
|
|
69
|
+
const renderedCardNodeCache = useRef(
|
|
70
|
+
new Map<string, { refMatch: ChatRef | undefined; render: ((ref: ChatRef) => React.ReactNode) | undefined; node: React.ReactNode }>(),
|
|
71
|
+
)
|
|
72
|
+
|
|
60
73
|
/**
|
|
61
74
|
* Per-message rendering plan for `[card://type:id]` markers.
|
|
62
75
|
*
|
|
@@ -94,7 +107,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
94
107
|
| { kind: 'block'; key: string; node: React.ReactNode }
|
|
95
108
|
const partsBySegment = new Map<number, SegmentPart[]>()
|
|
96
109
|
if (!render) return { inlineByKey, partsBySegment }
|
|
97
|
-
const
|
|
110
|
+
const cache = renderedCardNodeCache.current
|
|
111
|
+
const usedKeys = new Set<string>()
|
|
112
|
+
// Card keys already emitted as a hoisted block (`b-<key>`). The same
|
|
113
|
+
// marker can legitimately appear twice in one message (LLM references
|
|
114
|
+
// the same entity twice); we hoist the FIRST occurrence and skip the
|
|
115
|
+
// duplicates so two siblings never collide on the same React key.
|
|
116
|
+
const emittedBlockKeys = new Set<string>()
|
|
98
117
|
segments.forEach((segment, segIdx) => {
|
|
99
118
|
if (segment.type !== 'text') return
|
|
100
119
|
const text = segment.text
|
|
@@ -106,11 +125,15 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
106
125
|
const cardType = match[1]
|
|
107
126
|
const cardId = match[2]
|
|
108
127
|
const key = `${cardType}:${cardId}`
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
usedKeys.add(key)
|
|
129
|
+
// Reuse the cached node when neither the backing ref nor the render
|
|
130
|
+
// fn changed — returning the SAME element reference across renders is
|
|
131
|
+
// what stops React from re-mounting the card (and closing its open
|
|
132
|
+
// menu / re-fetching) on every stream chunk. Also dedups the same key
|
|
133
|
+
// emitted twice within one message.
|
|
134
|
+
const refMatch = refs[key]
|
|
135
|
+
let entry = cache.get(key)
|
|
136
|
+
if (!entry || entry.refMatch !== refMatch || entry.render !== render) {
|
|
114
137
|
// Always invoke render() — even when the metadata map has
|
|
115
138
|
// no entry for this marker. Fetch-mode card types
|
|
116
139
|
// (delivery_item, roadmap_item, internal_task, etc.) don't
|
|
@@ -129,25 +152,30 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
129
152
|
// `ref.title` shows the id rather than `undefined`) and
|
|
130
153
|
// `url` to null (matches the no-link semantics fetch-mode
|
|
131
154
|
// cards rely on — they resolve their own URL after fetch).
|
|
132
|
-
const refMatch = refs[key]
|
|
133
155
|
const refForRender: ChatRef = refMatch ?? {
|
|
134
156
|
type: cardType,
|
|
135
157
|
id: cardId,
|
|
136
158
|
title: cardId,
|
|
137
159
|
url: null,
|
|
138
160
|
}
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
entry = { refMatch, render, node: render(refForRender) }
|
|
162
|
+
cache.set(key, entry)
|
|
141
163
|
}
|
|
164
|
+
const rendered = entry.node
|
|
142
165
|
if (React.isValidElement(rendered) && rendered.type === BlockCard) {
|
|
143
166
|
const props = rendered.props as BlockCardProps
|
|
144
167
|
const markerEnd = match.index + match[0].length
|
|
145
168
|
// Text chunk INCLUDING the marker — the inline pill renders
|
|
146
169
|
// at the marker position via the `<a>` override.
|
|
147
170
|
parts.push({ kind: 'text', text: text.slice(cursor, markerEnd) })
|
|
148
|
-
|
|
171
|
+
// Hoist the block payload only on the FIRST occurrence of this key
|
|
172
|
+
// — a repeated marker still gets its inline pill (text + override
|
|
173
|
+
// above) but must not push a second `b-<key>` sibling.
|
|
174
|
+
if (!emittedBlockKeys.has(key)) {
|
|
175
|
+
emittedBlockKeys.add(key)
|
|
176
|
+
parts.push({ kind: 'block', key, node: props.children })
|
|
177
|
+
}
|
|
149
178
|
cursor = markerEnd
|
|
150
|
-
const refMatch = refs[key]
|
|
151
179
|
inlineByKey.set(
|
|
152
180
|
key,
|
|
153
181
|
props.inline != null
|
|
@@ -155,8 +183,26 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
155
183
|
: <span className="text-ods-text-primary font-medium">{refMatch?.title ?? cardId}</span>,
|
|
156
184
|
)
|
|
157
185
|
} else if (rendered != null) {
|
|
158
|
-
//
|
|
159
|
-
|
|
186
|
+
// Hoist fetch-mode entity cards (roadmap/blog/case-study/release/…)
|
|
187
|
+
// OUT of the markdown as stable-keyed block siblings — exactly like
|
|
188
|
+
// BlockCard. Rendered INSIDE `<SimpleMarkdownRenderer>` they remount
|
|
189
|
+
// on every streaming re-parse (react-markdown rebuilds its subtree
|
|
190
|
+
// token-by-token), which closes any open menu/popover and re-fires
|
|
191
|
+
// the card's fetch. As a sibling keyed by the card key (`b-<key>`)
|
|
192
|
+
// the card survives streaming: appending more text/markers only adds
|
|
193
|
+
// NEW siblings, existing cards keep their React instance.
|
|
194
|
+
//
|
|
195
|
+
// We EXCLUDE the marker from the surrounding text (cursor jumps past
|
|
196
|
+
// it) so no inline pill renders — the full card IS the content.
|
|
197
|
+
// Hoist only the FIRST occurrence; a duplicate marker is dropped
|
|
198
|
+
// entirely (no inline pill for hoisted cards) so we never push a
|
|
199
|
+
// second sibling colliding on the same `b-<key>` React key.
|
|
200
|
+
parts.push({ kind: 'text', text: text.slice(cursor, match.index) })
|
|
201
|
+
if (!emittedBlockKeys.has(key)) {
|
|
202
|
+
emittedBlockKeys.add(key)
|
|
203
|
+
parts.push({ kind: 'block', key, node: rendered })
|
|
204
|
+
}
|
|
205
|
+
cursor = match.index + match[0].length
|
|
160
206
|
}
|
|
161
207
|
}
|
|
162
208
|
// Trailing text after the last block marker (or the entire
|
|
@@ -172,6 +218,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
172
218
|
partsBySegment.set(segIdx, parts)
|
|
173
219
|
}
|
|
174
220
|
})
|
|
221
|
+
// Drop cached nodes for markers no longer present so the cache can't
|
|
222
|
+
// grow unbounded as a long message's markers change.
|
|
223
|
+
if (cache.size > usedKeys.size) {
|
|
224
|
+
for (const k of cache.keys()) {
|
|
225
|
+
if (!usedKeys.has(k)) cache.delete(k)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
175
228
|
return { inlineByKey, partsBySegment }
|
|
176
229
|
}, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
|
|
177
230
|
|
|
@@ -934,10 +934,43 @@ function EmbeddableChatInner({
|
|
|
934
934
|
[discussRef, displayRef, resolvedBaseRoute, chipBasePlatform, extras],
|
|
935
935
|
)
|
|
936
936
|
|
|
937
|
+
// Stable assistant-icon element. `<ChatMessageList>` forwards this prop to
|
|
938
|
+
// every assistant `<ChatMessageEnhanced>`, whose memo compares it BY
|
|
939
|
+
// REFERENCE. Created inline in JSX it was a fresh element on every render,
|
|
940
|
+
// defeating the memo for all assistant messages (re-rendering completed
|
|
941
|
+
// bubbles — and re-mounting their inline cards — on every realtime chunk).
|
|
942
|
+
const mingoAssistantIcon = useMemo(
|
|
943
|
+
() => <MingoIcon className="h-6 w-6" cornerColor="var(--ods-flamingo-cyan-base)" />,
|
|
944
|
+
[],
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
// Stable per-message timestamps. The memoized `<ChatMessageEnhanced>`
|
|
948
|
+
// compares `timestamp.getTime()`, so we must NOT stamp `new Date()` fresh
|
|
949
|
+
// on each render: a moving clock makes every message's timestamp differ
|
|
950
|
+
// between renders, defeating memoization and forcing the WHOLE list (and
|
|
951
|
+
// every open menu/card inside it) to re-render on every realtime chunk.
|
|
952
|
+
// Prefer the host's real timestamp; otherwise stamp once per id and reuse
|
|
953
|
+
// it (keeps the displayed time stable and lets memoization hold even when
|
|
954
|
+
// the host omits a timestamp).
|
|
955
|
+
const timestampCacheRef = useRef<Map<string, Date>>(new Map())
|
|
956
|
+
|
|
937
957
|
// Map docMessages → lib's Message type, forwarding chatRefs + scrollAnchor.
|
|
938
|
-
const messages: Message[] = useMemo(
|
|
939
|
-
|
|
940
|
-
|
|
958
|
+
const messages: Message[] = useMemo(() => {
|
|
959
|
+
const cache = timestampCacheRef.current
|
|
960
|
+
const seenIds = new Set<string>()
|
|
961
|
+
|
|
962
|
+
const mapped = rawMessages.map((m) => {
|
|
963
|
+
seenIds.add(m.id)
|
|
964
|
+
let timestamp: Date
|
|
965
|
+
if (m.timestamp != null) {
|
|
966
|
+
timestamp = new Date(m.timestamp)
|
|
967
|
+
} else {
|
|
968
|
+
const cached = cache.get(m.id)
|
|
969
|
+
timestamp = cached ?? new Date()
|
|
970
|
+
if (!cached) cache.set(m.id, timestamp)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return {
|
|
941
974
|
id: m.id,
|
|
942
975
|
role: m.role as 'user' | 'assistant',
|
|
943
976
|
// Host-supplied per-message name/avatar win (e.g. the signed-in user's
|
|
@@ -949,13 +982,23 @@ function EmbeddableChatInner({
|
|
|
949
982
|
// name color as the standalone /mingo page (user → 'admin').
|
|
950
983
|
...(m.authorType ? { authorType: m.authorType } : {}),
|
|
951
984
|
content: m.segments && m.segments.length > 0 ? m.segments : m.content,
|
|
952
|
-
timestamp
|
|
985
|
+
timestamp,
|
|
953
986
|
assistantType: m.role === 'assistant' ? ('mingo' as const) : undefined,
|
|
954
987
|
...(m.chatRefs ? { chatRefs: m.chatRefs } : {}),
|
|
955
988
|
...(m.scrollAnchor ? { scrollAnchor: m.scrollAnchor } : {}),
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
|
|
989
|
+
}
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
// Drop cached fallbacks for messages no longer present so the map can't
|
|
993
|
+
// grow unbounded across a long-lived session.
|
|
994
|
+
if (cache.size > seenIds.size) {
|
|
995
|
+
for (const id of cache.keys()) {
|
|
996
|
+
if (!seenIds.has(id)) cache.delete(id)
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return mapped
|
|
1001
|
+
}, [rawMessages])
|
|
959
1002
|
|
|
960
1003
|
const handleSend = useCallback(
|
|
961
1004
|
(text: string) => {
|
|
@@ -1254,12 +1297,7 @@ function EmbeddableChatInner({
|
|
|
1254
1297
|
isTyping={chatLoading}
|
|
1255
1298
|
autoScroll={true}
|
|
1256
1299
|
assistantType="mingo"
|
|
1257
|
-
assistantIcon={
|
|
1258
|
-
<MingoIcon
|
|
1259
|
-
className="h-6 w-6"
|
|
1260
|
-
cornerColor="var(--ods-flamingo-cyan-base)"
|
|
1261
|
-
/>
|
|
1262
|
-
}
|
|
1300
|
+
assistantIcon={mingoAssistantIcon}
|
|
1263
1301
|
renderEntityCard={renderEntityCard}
|
|
1264
1302
|
NavLinkAnchor={NavLinkAnchorViaRuntime}
|
|
1265
1303
|
className="flex-1"
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { useQuery } from '@tanstack/react-query'
|
|
23
23
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
24
|
+
import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
|
|
24
25
|
|
|
25
26
|
export interface UseChatCardItemResult<T = unknown> {
|
|
26
27
|
item: T | undefined
|
|
@@ -80,7 +81,13 @@ export function useChatCardItem<T = unknown>(
|
|
|
80
81
|
queryKey: ['chat-card-item', type, id],
|
|
81
82
|
queryFn: async (): Promise<T | null> => {
|
|
82
83
|
if (!url) return null
|
|
83
|
-
|
|
84
|
+
// Go through `embedAuthedFetch` (NOT bare `fetch`) so the request
|
|
85
|
+
// rides the same auth path as the chat stream/commands: it consults
|
|
86
|
+
// the host-registered `EmbedAuthAdapter` (cookie `credentials:'include'`
|
|
87
|
+
// cross-origin, dev-ticket Bearer, 401 refresh-and-retry). Bare `fetch`
|
|
88
|
+
// sent no credentials, so list endpoints behind the gateway returned
|
|
89
|
+
// 401 and the card rendered blank.
|
|
90
|
+
const res = await embedAuthedFetch(url)
|
|
84
91
|
if (!res.ok) return null
|
|
85
92
|
const data = await res.json()
|
|
86
93
|
const items = extractItems(data)
|
|
@@ -93,6 +100,15 @@ export function useChatCardItem<T = unknown>(
|
|
|
93
100
|
// user toggles back to the tab is wasteful (the row's id-keyed
|
|
94
101
|
// payload is effectively immutable).
|
|
95
102
|
refetchOnWindowFocus: false,
|
|
103
|
+
// The id-keyed payload is effectively immutable, and a fetch-mode card
|
|
104
|
+
// inside a STREAMING assistant message re-mounts on every chunk as the
|
|
105
|
+
// surrounding markdown re-parses. Without a staleTime that meant a fresh
|
|
106
|
+
// network fetch + loading-skeleton flash on every single chunk. Treat the
|
|
107
|
+
// row as fresh for 5 min (matches the public pages) and keep it cached
|
|
108
|
+
// long after unmount so remounts resolve synchronously from cache — no
|
|
109
|
+
// refetch, no skeleton flash.
|
|
110
|
+
staleTime: 5 * 60 * 1000,
|
|
111
|
+
gcTime: 30 * 60 * 1000,
|
|
96
112
|
})
|
|
97
113
|
return {
|
|
98
114
|
item: (query.data ?? undefined) as T | undefined,
|
|
@@ -134,27 +134,39 @@ export function useUnifiedChat(
|
|
|
134
134
|
? sseState
|
|
135
135
|
: (mingoStateOverride ?? natsState)
|
|
136
136
|
|
|
137
|
+
// Live ref to the active state. The injected `mingoState` (and the SSE/NATS
|
|
138
|
+
// adapters) hand back a NEW state object on EVERY streaming chunk
|
|
139
|
+
// (`messages`/`streamingPhase`/`tokenUsage` change), so binding the forwards
|
|
140
|
+
// below to `[activeState]` would recreate them each chunk. That churn
|
|
141
|
+
// cascades into consumers' memo deps — most visibly `renderEntityCard` in
|
|
142
|
+
// `embeddable-chat`, whose new identity defeats `ChatMessageEnhanced`'s memo
|
|
143
|
+
// and re-renders (re-mounts inline cards on) every message each chunk.
|
|
144
|
+
// Reading through the ref keeps the forwards referentially STABLE while
|
|
145
|
+
// still calling the latest active state.
|
|
146
|
+
const activeStateRef = useRef(activeState)
|
|
147
|
+
activeStateRef.current = activeState
|
|
148
|
+
|
|
137
149
|
// Re-wrap so the returned identity is stable for the active state.
|
|
138
150
|
// Consumers shouldn't see the inactive adapter's state at all.
|
|
139
|
-
const stopMessage = useCallback(() =>
|
|
151
|
+
const stopMessage = useCallback(() => activeStateRef.current.stopMessage(), [])
|
|
140
152
|
const clearMessages = useCallback(
|
|
141
|
-
() =>
|
|
142
|
-
[
|
|
153
|
+
() => activeStateRef.current.clearMessages(),
|
|
154
|
+
[],
|
|
143
155
|
)
|
|
144
156
|
const sendMessage = useCallback(
|
|
145
157
|
(text: string, opts?: Parameters<UnifiedChatState['sendMessage']>[1]) =>
|
|
146
|
-
|
|
147
|
-
[
|
|
158
|
+
activeStateRef.current.sendMessage(text, opts),
|
|
159
|
+
[],
|
|
148
160
|
)
|
|
149
161
|
const discussRef = useCallback(
|
|
150
162
|
(ref: Parameters<UnifiedChatState['discussRef']>[0]) =>
|
|
151
|
-
|
|
152
|
-
[
|
|
163
|
+
activeStateRef.current.discussRef(ref),
|
|
164
|
+
[],
|
|
153
165
|
)
|
|
154
166
|
const displayRef = useCallback(
|
|
155
167
|
(ref: Parameters<UnifiedChatState['displayRef']>[0]) =>
|
|
156
|
-
|
|
157
|
-
[
|
|
168
|
+
activeStateRef.current.displayRef(ref),
|
|
169
|
+
[],
|
|
158
170
|
)
|
|
159
171
|
|
|
160
172
|
// Dialog-management forwards — one thin wrapper per action so the
|
|
@@ -162,45 +174,45 @@ export function useUnifiedChat(
|
|
|
162
174
|
// identity does. We don't recreate per-call to avoid spurious child
|
|
163
175
|
// re-renders downstream.
|
|
164
176
|
const selectDialog = useCallback(
|
|
165
|
-
(id: string | null) =>
|
|
166
|
-
[
|
|
177
|
+
(id: string | null) => activeStateRef.current.selectDialog(id),
|
|
178
|
+
[],
|
|
167
179
|
)
|
|
168
180
|
const startNewDialog = useCallback(
|
|
169
|
-
() =>
|
|
170
|
-
[
|
|
181
|
+
() => activeStateRef.current.startNewDialog(),
|
|
182
|
+
[],
|
|
171
183
|
)
|
|
172
184
|
const deleteDialog = useCallback(
|
|
173
|
-
(id: string) =>
|
|
174
|
-
[
|
|
185
|
+
(id: string) => activeStateRef.current.deleteDialog(id),
|
|
186
|
+
[],
|
|
175
187
|
)
|
|
176
188
|
const renameDialog = useCallback(
|
|
177
|
-
(id: string, title: string) =>
|
|
178
|
-
[
|
|
189
|
+
(id: string, title: string) => activeStateRef.current.renameDialog(id, title),
|
|
190
|
+
[],
|
|
179
191
|
)
|
|
180
192
|
const archiveDialog = useCallback(
|
|
181
|
-
(id: string) =>
|
|
182
|
-
[
|
|
193
|
+
(id: string) => activeStateRef.current.archiveDialog(id),
|
|
194
|
+
[],
|
|
183
195
|
)
|
|
184
196
|
const loadMoreDialogs = useCallback(
|
|
185
|
-
() =>
|
|
186
|
-
[
|
|
197
|
+
() => activeStateRef.current.loadMoreDialogs(),
|
|
198
|
+
[],
|
|
187
199
|
)
|
|
188
200
|
const reloadDialogs = useCallback(
|
|
189
|
-
() =>
|
|
190
|
-
[
|
|
201
|
+
() => activeStateRef.current.reloadDialogs(),
|
|
202
|
+
[],
|
|
191
203
|
)
|
|
192
204
|
const loadMoreMessages = useCallback(
|
|
193
|
-
() =>
|
|
194
|
-
[
|
|
205
|
+
() => activeStateRef.current.loadMoreMessages(),
|
|
206
|
+
[],
|
|
195
207
|
)
|
|
196
208
|
const approveRequest = useCallback(
|
|
197
|
-
(requestId: string) =>
|
|
198
|
-
[
|
|
209
|
+
(requestId: string) => activeStateRef.current.approveRequest(requestId),
|
|
210
|
+
[],
|
|
199
211
|
)
|
|
200
212
|
const rejectRequest = useCallback(
|
|
201
213
|
(requestId: string, reason?: string) =>
|
|
202
|
-
|
|
203
|
-
[
|
|
214
|
+
activeStateRef.current.rejectRequest(requestId, reason),
|
|
215
|
+
[],
|
|
204
216
|
)
|
|
205
217
|
|
|
206
218
|
return useMemo<UnifiedChatState>(
|
|
@@ -143,6 +143,17 @@ export interface UnifiedChatMessage {
|
|
|
143
143
|
content: string
|
|
144
144
|
segments?: MessageSegment[]
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Optional host-supplied message timestamp (creation/send time). Rendered
|
|
148
|
+
* as the message-row time and, critically, fed into the memoized message's
|
|
149
|
+
* equality check. Hosts SHOULD pass a stable value (the message's real
|
|
150
|
+
* `createdAt`) — when omitted the panel stamps a stable per-id fallback so
|
|
151
|
+
* memoization still holds. Never re-derive this as "now" per render: a
|
|
152
|
+
* moving clock defeats memoization and re-renders every message (collapsing
|
|
153
|
+
* open menus/cards) on each realtime chunk.
|
|
154
|
+
*/
|
|
155
|
+
timestamp?: Date | string | number
|
|
156
|
+
|
|
146
157
|
/** Guide/SSE-only: document citations. Undefined in Mingo mode. */
|
|
147
158
|
sources?: ChatSource[]
|
|
148
159
|
|
|
@@ -339,12 +339,12 @@ function AppLayoutDrawerResizeHandle({
|
|
|
339
339
|
|
|
340
340
|
const trackPosition =
|
|
341
341
|
side === "right"
|
|
342
|
-
? "right-full top-4 bottom-4 w-
|
|
342
|
+
? "right-full top-4 bottom-4 w-6 items-center justify-end pr-1"
|
|
343
343
|
: side === "left"
|
|
344
|
-
? "left-full top-4 bottom-4 w-
|
|
344
|
+
? "left-full top-4 bottom-4 w-6 items-center justify-start pl-1"
|
|
345
345
|
: side === "bottom"
|
|
346
|
-
? "bottom-full left-4 right-4 h-
|
|
347
|
-
: "top-full left-4 right-4 h-
|
|
346
|
+
? "bottom-full left-4 right-4 h-6 justify-center items-end pb-1"
|
|
347
|
+
: "top-full left-4 right-4 h-6 justify-center items-start pt-1"
|
|
348
348
|
|
|
349
349
|
const cursorClass = isHorizontal ? "cursor-col-resize" : "cursor-row-resize"
|
|
350
350
|
const gripClass = isHorizontal ? "h-10 w-1" : "w-10 h-1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
|
3
|
+
import React, { useEffect, useState, useRef, useMemo, useCallback, memo } from 'react';
|
|
4
4
|
import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown';
|
|
5
5
|
import type { PluggableList } from 'unified';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
@@ -583,7 +583,7 @@ export interface SimpleMarkdownRendererProps {
|
|
|
583
583
|
// ---------------------------------------------------------------------------
|
|
584
584
|
// Component
|
|
585
585
|
// ---------------------------------------------------------------------------
|
|
586
|
-
|
|
586
|
+
const SimpleMarkdownRendererImpl: React.FC<SimpleMarkdownRendererProps> = ({
|
|
587
587
|
content,
|
|
588
588
|
className = "",
|
|
589
589
|
sectionIds,
|
|
@@ -947,3 +947,13 @@ export const SimpleMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({
|
|
|
947
947
|
</div>
|
|
948
948
|
);
|
|
949
949
|
};
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Memoized so a parent re-render with UNCHANGED props (same `content` string,
|
|
953
|
+
* same memoized `componentOverrides`/plugins) does NOT re-parse the markdown.
|
|
954
|
+
* Re-parsing rebuilds the entire react-markdown subtree, which RE-MOUNTS any
|
|
955
|
+
* embedded inline entity cards — closing their open menus and re-triggering
|
|
956
|
+
* their fetch on every chat re-render (streaming chunk AND scroll). With
|
|
957
|
+
* stable props the renderer bails, so completed messages' cards stay mounted.
|
|
958
|
+
*/
|
|
959
|
+
export const SimpleMarkdownRenderer = memo(SimpleMarkdownRendererImpl);
|