@flamingo-stack/openframe-frontend-core 0.0.458-snapshot.20260720215024 → 0.0.458
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-BLZZXZNM.cjs → chunk-5USSSRAL.cjs} +26 -26
- package/dist/{chunk-BLZZXZNM.cjs.map → chunk-5USSSRAL.cjs.map} +1 -1
- package/dist/{chunk-CGNDAJ7V.js → chunk-6B27O3ZB.js} +4 -4
- package/dist/{chunk-P5JQDFRM.cjs → chunk-7IRPMNRL.cjs} +60 -60
- package/dist/chunk-7IRPMNRL.cjs.map +1 -0
- package/dist/{chunk-4XDWNAZW.cjs → chunk-DQ3EHM6G.cjs} +9 -9
- package/dist/{chunk-4XDWNAZW.cjs.map → chunk-DQ3EHM6G.cjs.map} +1 -1
- package/dist/{chunk-7KSEINAE.cjs → chunk-FTXDIDNX.cjs} +15 -15
- package/dist/{chunk-7KSEINAE.cjs.map → chunk-FTXDIDNX.cjs.map} +1 -1
- package/dist/{chunk-4R6H2U77.js → chunk-H7ETLG65.js} +4 -4
- package/dist/{chunk-LAOO7XHO.js → chunk-HWTUVLYB.js} +2 -2
- package/dist/{chunk-F2NMWV44.js → chunk-IDKDOQZL.js} +2 -2
- package/dist/{chunk-BQ7PMWLM.cjs → chunk-KIIOBK7G.cjs} +27 -28
- package/dist/chunk-KIIOBK7G.cjs.map +1 -0
- package/dist/{chunk-QO2XD3FR.js → chunk-KQGFKG6Y.js} +2 -2
- package/dist/{chunk-3N3TMZ62.cjs → chunk-L676XVP4.cjs} +7 -7
- package/dist/{chunk-3N3TMZ62.cjs.map → chunk-L676XVP4.cjs.map} +1 -1
- package/dist/{chunk-F7F43U7Q.cjs → chunk-LDAOPWXZ.cjs} +11 -11
- package/dist/{chunk-F7F43U7Q.cjs.map → chunk-LDAOPWXZ.cjs.map} +1 -1
- package/dist/{chunk-ADD3FPDG.cjs → chunk-MKLP2SOZ.cjs} +4886 -7004
- package/dist/chunk-MKLP2SOZ.cjs.map +1 -0
- package/dist/{chunk-VXJH5TDN.js → chunk-QL7ODLJA.js} +10 -10
- package/dist/chunk-QL7ODLJA.js.map +1 -0
- package/dist/{chunk-OR5AMXYN.js → chunk-R5QYMUKN.js} +2 -2
- package/dist/{chunk-REWRUV3K.js → chunk-TNLFAJPA.js} +9673 -11791
- package/dist/chunk-TNLFAJPA.js.map +1 -0
- package/dist/{chunk-XF5ROTMQ.js → chunk-U2YDR5DT.js} +2 -2
- package/dist/{chunk-PXZ7YQ5V.cjs → chunk-VTQSB6QB.cjs} +5 -5
- package/dist/{chunk-PXZ7YQ5V.cjs.map → chunk-VTQSB6QB.cjs.map} +1 -1
- package/dist/{chunk-UI7CIUAR.js → chunk-WGJPTFRH.js} +2 -2
- package/dist/{chunk-WCE66C2Z.cjs → chunk-XUNJILJT.cjs} +62 -62
- package/dist/{chunk-WCE66C2Z.cjs.map → chunk-XUNJILJT.cjs.map} +1 -1
- package/dist/{chunk-APF5QBAU.js → chunk-YQBPIUZU.js} +2 -2
- package/dist/{chunk-TA7MFVUS.cjs → chunk-YV2YA3QH.cjs} +31 -31
- package/dist/{chunk-TA7MFVUS.cjs.map → chunk-YV2YA3QH.cjs.map} +1 -1
- package/dist/{chunk-QSGZE6AZ.cjs → chunk-ZDJ4ZUQ4.cjs} +37 -37
- package/dist/{chunk-QSGZE6AZ.cjs.map → chunk-ZDJ4ZUQ4.cjs.map} +1 -1
- package/dist/{chunk-LJKZIIK4.js → chunk-ZHQQRYMB.js} +5 -6
- package/dist/chunk-ZHQQRYMB.js.map +1 -0
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chart.d.ts +1 -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/hooks/index.d.ts +1 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +48 -15
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +6 -0
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +47 -3
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -10
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +0 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +13 -21
- package/dist/components/chat/types/api.types.d.ts +81 -1
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +90 -2
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chunk-parser.d.ts +25 -0
- package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -0
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +6 -34
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +3 -2
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +11 -59
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +129 -171
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +21 -63
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +2 -44
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +2 -1
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +13 -55
- package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/simple-markdown-renderer.d.ts +73 -0
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -0
- package/dist/index.cjs +2 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -71
- package/dist/utils/index.cjs +40 -169
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +41 -162
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
- package/package.json +1 -7
- package/src/components/chat/__tests__/chat-message-list.test.tsx +5 -293
- package/src/components/chat/chat-message-enhanced.tsx +17 -111
- package/src/components/chat/chat-message-list.tsx +113 -367
- package/src/components/chat/embeddable-chat.tsx +0 -6
- package/src/components/chat/hooks/.index.md +1 -1
- package/src/components/chat/hooks/.use-nats-chat-adapter.md +1 -1
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +50 -0
- package/src/components/chat/hooks/index.ts +1 -0
- package/src/components/chat/hooks/use-chat.ts +0 -8
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +822 -243
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +479 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +666 -318
- package/src/components/chat/index.ts +0 -5
- package/src/components/chat/thinking-display.tsx +1 -1
- package/src/components/chat/types/.api.types.md +3 -1
- package/src/components/chat/types/.processing.types.md +23 -6
- package/src/components/chat/types/api.types.ts +73 -6
- package/src/components/chat/types/processing.types.ts +52 -11
- package/src/components/chat/utils/.chunk-parser.md +51 -0
- package/src/components/chat/utils/.index.md +5 -0
- package/src/components/chat/utils/chunk-parser.ts +255 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +5 -63
- package/src/components/chat/utils/index.ts +8 -5
- package/src/components/chat/utils/process-historical-messages.ts +383 -352
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +4 -22
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +4 -19
- package/src/components/ui/.simple-markdown-renderer.md +40 -0
- package/src/components/ui/index.ts +2 -1
- package/src/components/ui/release-changelog-section.tsx +1 -1
- package/src/components/ui/rich-markdown-renderer.tsx +1223 -0
- package/src/components/ui/simple-markdown-renderer.tsx +965 -0
- package/src/components/ui/ticket-info-section.tsx +1 -1
- package/src/utils/index.ts +0 -1
- package/src/utils/markdown-section-extractor.ts +56 -41
- package/dist/chat-protocol/decode.d.ts +0 -58
- package/dist/chat-protocol/decode.d.ts.map +0 -1
- package/dist/chat-protocol/encode.d.ts +0 -44
- package/dist/chat-protocol/encode.d.ts.map +0 -1
- package/dist/chat-protocol/env-flag.d.ts +0 -35
- package/dist/chat-protocol/env-flag.d.ts.map +0 -1
- package/dist/chat-protocol/events.d.ts +0 -192
- package/dist/chat-protocol/events.d.ts.map +0 -1
- package/dist/chat-protocol/frames.d.ts +0 -206
- package/dist/chat-protocol/frames.d.ts.map +0 -1
- package/dist/chat-protocol/index.cjs +0 -493
- package/dist/chat-protocol/index.cjs.map +0 -1
- package/dist/chat-protocol/index.d.ts +0 -18
- package/dist/chat-protocol/index.d.ts.map +0 -1
- package/dist/chat-protocol/index.js +0 -477
- package/dist/chat-protocol/index.js.map +0 -1
- package/dist/chat-protocol/ip-normalize.d.ts +0 -44
- package/dist/chat-protocol/ip-normalize.d.ts.map +0 -1
- package/dist/chat-protocol/nats-decoder.d.ts +0 -26
- package/dist/chat-protocol/nats-decoder.d.ts.map +0 -1
- package/dist/chunk-ADD3FPDG.cjs.map +0 -1
- package/dist/chunk-BQ7PMWLM.cjs.map +0 -1
- package/dist/chunk-LJKZIIK4.js.map +0 -1
- package/dist/chunk-P5JQDFRM.cjs.map +0 -1
- package/dist/chunk-REWRUV3K.js.map +0 -1
- package/dist/chunk-VXJH5TDN.js.map +0 -1
- package/dist/components/chat/stream/chat-dialog-store.d.ts +0 -184
- package/dist/components/chat/stream/chat-dialog-store.d.ts.map +0 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -367
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +0 -1
- package/dist/components/chat/stream/delta-batcher.d.ts +0 -51
- package/dist/components/chat/stream/delta-batcher.d.ts.map +0 -1
- package/dist/components/chat/stream/index.d.ts +0 -17
- package/dist/components/chat/stream/index.d.ts.map +0 -1
- package/dist/components/chat/stream/message-mutations.d.ts +0 -77
- package/dist/components/chat/stream/message-mutations.d.ts.map +0 -1
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +0 -22
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +0 -1
- package/dist/components/ui/markdown/base-components.d.ts +0 -56
- package/dist/components/ui/markdown/base-components.d.ts.map +0 -1
- package/dist/components/ui/markdown/engine.d.ts +0 -99
- package/dist/components/ui/markdown/engine.d.ts.map +0 -1
- package/dist/components/ui/markdown/heading-ids.d.ts +0 -65
- package/dist/components/ui/markdown/heading-ids.d.ts.map +0 -1
- package/dist/components/ui/markdown/index.d.ts +0 -18
- package/dist/components/ui/markdown/index.d.ts.map +0 -1
- package/dist/components/ui/markdown/mermaid-diagram.d.ts +0 -79
- package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +0 -1
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts +0 -14
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +0 -1
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -40
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/markdown/rich/shortcodes.d.ts +0 -8
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +0 -1
- package/dist/components/ui/markdown/sanitize.d.ts +0 -153
- package/dist/components/ui/markdown/sanitize.d.ts.map +0 -1
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +0 -23
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/markdown/streaming.d.ts +0 -78
- package/dist/components/ui/markdown/streaming.d.ts.map +0 -1
- package/dist/components/ui/markdown/text-size.d.ts +0 -34
- package/dist/components/ui/markdown/text-size.d.ts.map +0 -1
- package/dist/utils/markdown-fences.d.ts +0 -42
- package/dist/utils/markdown-fences.d.ts.map +0 -1
- package/dist/utils/markdown-heading-id.d.ts +0 -85
- package/dist/utils/markdown-heading-id.d.ts.map +0 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -307
- package/src/chat-protocol/__tests__/chat-protocol.test.ts +0 -515
- package/src/chat-protocol/__tests__/env-flag.test.ts +0 -52
- package/src/chat-protocol/__tests__/ip-normalize.test.ts +0 -136
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -228
- package/src/chat-protocol/decode.ts +0 -277
- package/src/chat-protocol/encode.ts +0 -71
- package/src/chat-protocol/env-flag.ts +0 -39
- package/src/chat-protocol/events.ts +0 -228
- package/src/chat-protocol/frames.ts +0 -238
- package/src/chat-protocol/index.ts +0 -21
- package/src/chat-protocol/ip-normalize.ts +0 -146
- package/src/chat-protocol/nats-decoder.ts +0 -252
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -207
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -209
- package/src/components/chat/hooks/__tests__/__snapshots__/persisted-state-v1-golden.test.ts.snap +0 -297
- package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
- package/src/components/chat/hooks/__tests__/persisted-state-v1-golden.test.ts +0 -272
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +0 -318
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +0 -937
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +0 -756
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +0 -460
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -894
- package/src/components/chat/stream/__tests__/delta-batcher.test.ts +0 -255
- package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +0 -98
- package/src/components/chat/stream/chat-dialog-store.ts +0 -536
- package/src/components/chat/stream/chat-stream-reducer.ts +0 -1796
- package/src/components/chat/stream/delta-batcher.ts +0 -159
- package/src/components/chat/stream/index.ts +0 -55
- package/src/components/chat/stream/message-mutations.ts +0 -338
- package/src/components/chat/stream/use-chat-stream-reducer.ts +0 -126
- package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +0 -418
- package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +0 -599
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -317
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +0 -270
- package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +0 -2420
- package/src/components/ui/__tests__/markdown-parity.test.tsx +0 -2528
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +0 -115
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +0 -95
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +0 -309
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +0 -388
- package/src/components/ui/markdown/base-components.tsx +0 -341
- package/src/components/ui/markdown/engine.tsx +0 -315
- package/src/components/ui/markdown/heading-ids.ts +0 -239
- package/src/components/ui/markdown/index.ts +0 -49
- package/src/components/ui/markdown/mermaid-diagram.tsx +0 -291
- package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -199
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +0 -184
- package/src/components/ui/markdown/rich/shortcodes.ts +0 -170
- package/src/components/ui/markdown/sanitize.ts +0 -3499
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +0 -28
- package/src/components/ui/markdown/streaming.ts +0 -362
- package/src/components/ui/markdown/text-size.ts +0 -106
- package/src/utils/markdown-fences.ts +0 -127
- package/src/utils/markdown-heading-id.ts +0 -348
- /package/dist/{chunk-CGNDAJ7V.js.map → chunk-6B27O3ZB.js.map} +0 -0
- /package/dist/{chunk-4R6H2U77.js.map → chunk-H7ETLG65.js.map} +0 -0
- /package/dist/{chunk-LAOO7XHO.js.map → chunk-HWTUVLYB.js.map} +0 -0
- /package/dist/{chunk-F2NMWV44.js.map → chunk-IDKDOQZL.js.map} +0 -0
- /package/dist/{chunk-QO2XD3FR.js.map → chunk-KQGFKG6Y.js.map} +0 -0
- /package/dist/{chunk-OR5AMXYN.js.map → chunk-R5QYMUKN.js.map} +0 -0
- /package/dist/{chunk-XF5ROTMQ.js.map → chunk-U2YDR5DT.js.map} +0 -0
- /package/dist/{chunk-UI7CIUAR.js.map → chunk-WGJPTFRH.js.map} +0 -0
- /package/dist/{chunk-APF5QBAU.js.map → chunk-YQBPIUZU.js.map} +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useRef,
|
|
3
|
+
import { useRef, useEffect, useLayoutEffect, useImperativeHandle, forwardRef } from "react"
|
|
4
4
|
import { useStickToBottom } from "use-stick-to-bottom"
|
|
5
5
|
import { cn } from "../../utils/cn"
|
|
6
6
|
import { OverlayScrollArea } from '../ui/overlay-scroll-area'
|
|
7
7
|
import { ChatMessageEnhanced } from "./chat-message-enhanced"
|
|
8
8
|
import { ChatMessageListSkeleton } from "./chat-message-skeleton"
|
|
9
|
-
import { DotsLoaderIcon
|
|
9
|
+
import { DotsLoaderIcon } from "../icons-v2-generated"
|
|
10
10
|
import { CyclingPhrase } from "./cycling-phrase"
|
|
11
11
|
import type { ChatMessageListProps } from "./types"
|
|
12
12
|
import { SCROLL_ANCHOR } from "./types/message.types"
|
|
@@ -28,11 +28,6 @@ const STREAMING_WORDS = [
|
|
|
28
28
|
'Riffing',
|
|
29
29
|
] as const
|
|
30
30
|
|
|
31
|
-
/** Distance from the bottom (px) still counted as "at the bottom".
|
|
32
|
-
* Mirrors `use-stick-to-bottom`'s internal `STICK_TO_BOTTOM_OFFSET_PX`
|
|
33
|
-
* so the jump-to-bottom button and the library agree on the boundary. */
|
|
34
|
-
const BOTTOM_THRESHOLD_PX = 70
|
|
35
|
-
|
|
36
31
|
/*
|
|
37
32
|
* Stick-to-bottom: `use-stick-to-bottom` (stackblitz-labs)
|
|
38
33
|
*
|
|
@@ -59,9 +54,6 @@ const BOTTOM_THRESHOLD_PX = 70
|
|
|
59
54
|
* chat / sent a message; show me the bottom immediately").
|
|
60
55
|
* - **Load-more sentinel** (top-of-list IntersectionObserver) for
|
|
61
56
|
* infinite-scroll-UP. Distinct concern from stick-to-bottom.
|
|
62
|
-
* - **Bottom-follow intent** (see `followBottomRef` below). The
|
|
63
|
-
* library's follow is gated on its own `isAtBottom` flag, which this
|
|
64
|
-
* list's layout can lose silently; we own the INTENT and re-assert.
|
|
65
57
|
*
|
|
66
58
|
* `useStickToBottom`'s returned refs are MUTABLE refs + ref callbacks
|
|
67
59
|
* — pass them directly as `ref={scrollRef}` / `ref={contentRef}`.
|
|
@@ -151,45 +143,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
151
143
|
const dialogIdRef = useRef(dialogId)
|
|
152
144
|
dialogIdRef.current = dialogId
|
|
153
145
|
|
|
154
|
-
// ---- Bottom-follow intent (list-owned lock) ----------------------
|
|
155
|
-
// The library follows the bottom only while ITS OWN `isAtBottom` flag
|
|
156
|
-
// is true, and this list's layout loses that flag silently in two
|
|
157
|
-
// ways:
|
|
158
|
-
//
|
|
159
|
-
// 1. **The scroller's own box changes.** The library's
|
|
160
|
-
// ResizeObserver watches `contentRef` (INSIDE the scroller) only.
|
|
161
|
-
// The streaming loader unmounting and the host's source-chip row
|
|
162
|
-
// mounting — both siblings BELOW the scroller (see render) —
|
|
163
|
-
// change the scroller's `clientHeight`, which moves the bottom
|
|
164
|
-
// without emitting anything the library can observe.
|
|
165
|
-
// 2. **Resize-induced scroll events read as a user gesture.**
|
|
166
|
-
// Fetch-mode entity cards mount as a skeleton and settle to a
|
|
167
|
-
// different height; a shrink clamps `scrollTop` down, and the
|
|
168
|
-
// library's scroll handler can see that as a scroll-UP and
|
|
169
|
-
// permanently escape the lock (its `resizeDifference` guard is a
|
|
170
|
-
// `setTimeout(…, 1)` race, so this is intermittent).
|
|
171
|
-
//
|
|
172
|
-
// Either way the thread stops following mid-turn. It bites hardest on
|
|
173
|
-
// card-heavy answers — a slash-command listing (`/getting-started`)
|
|
174
|
-
// renders a dozen fetch-mode cards with cover images that keep
|
|
175
|
-
// resolving for SECONDS after the stream ends, so everything after
|
|
176
|
-
// the lock was lost lands below the fold.
|
|
177
|
-
//
|
|
178
|
-
// Fix: this list owns the INTENT. Sending a message (or opening a
|
|
179
|
-
// dialog) arms it; only a real user scroll-up gesture — or a
|
|
180
|
-
// top-anchored turn, which deliberately parks elsewhere — disarms it.
|
|
181
|
-
// While armed, ANY geometry change re-asserts `scrollToBottom`, and
|
|
182
|
-
// because that call also re-sets the library's `isAtBottom`, a lost
|
|
183
|
-
// lock self-heals instead of being permanent.
|
|
184
|
-
const followBottomRef = useRef(false)
|
|
185
|
-
|
|
186
|
-
// Drives the jump-to-bottom affordance. Derived from GEOMETRY, not
|
|
187
|
-
// from the library's `isAtBottom`: that flag is the one this list
|
|
188
|
-
// loses silently (see above), and a stale `true` would hide the
|
|
189
|
-
// button in exactly the situation it exists for. Threshold mirrors
|
|
190
|
-
// the library's own `STICK_TO_BOTTOM_OFFSET_PX`.
|
|
191
|
-
const [atBottom, setAtBottom] = useState(true)
|
|
192
|
-
|
|
193
146
|
// Tracks previous render state for explicit "force-stick" decisions
|
|
194
147
|
// (dialog change, first message, new user message). The library
|
|
195
148
|
// covers ongoing streaming; these are about INTENT transitions.
|
|
@@ -226,13 +179,11 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
226
179
|
|
|
227
180
|
if (dialogChanged) {
|
|
228
181
|
// Opening a different chat — instant snap to bottom.
|
|
229
|
-
followBottomRef.current = true
|
|
230
182
|
void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
|
|
231
183
|
return
|
|
232
184
|
}
|
|
233
185
|
if (newCount > 0 && prevCount === 0) {
|
|
234
186
|
// First message arrived in an empty chat — instant snap.
|
|
235
|
-
followBottomRef.current = true
|
|
236
187
|
void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
|
|
237
188
|
return
|
|
238
189
|
}
|
|
@@ -257,193 +208,54 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
257
208
|
const newSlice = prevLastIdx >= 0 ? messages.slice(prevLastIdx + 1) : messages.slice(prevCount)
|
|
258
209
|
const hasNewUser = newSlice.some((m) => m.role === 'user')
|
|
259
210
|
if (hasNewUser) {
|
|
260
|
-
// Arms the follow intent for the WHOLE turn: the answer's
|
|
261
|
-
// async growth (attachment images, streamed tokens, entity
|
|
262
|
-
// cards resolving out of their skeletons long after the
|
|
263
|
-
// stream ends) is re-asserted by the follow effect below.
|
|
264
|
-
// This replaces the old per-`<img>` `load`-handler hack —
|
|
265
|
-
// same defect class, but the ResizeObserver sees every
|
|
266
|
-
// source of growth, not just images present at send time.
|
|
267
|
-
followBottomRef.current = true
|
|
268
211
|
void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
|
|
212
|
+
|
|
213
|
+
// Image-attachment growth follow-up: if the new user message
|
|
214
|
+
// carries inline `<img>` elements (chat-attachment markdown
|
|
215
|
+
// emits `` which the markdown
|
|
216
|
+
// renderer turns into `<img>` / Next.js `<Image>`), those
|
|
217
|
+
// images load AFTER the initial paint. Each image-load grows
|
|
218
|
+
// the bubble height, but `useStickToBottom`'s `resize:
|
|
219
|
+
// 'smooth'` spring sometimes loses anchor under rapid
|
|
220
|
+
// resize events from Next.js Image's progressive
|
|
221
|
+
// dimensioning (observed: scroll lands 50-100px short of
|
|
222
|
+
// bottom — exactly one image-load short).
|
|
223
|
+
//
|
|
224
|
+
// Fix: after the initial scrollToBottom snaps, scan the
|
|
225
|
+
// content area for `<img>` elements that haven't loaded
|
|
226
|
+
// yet, and attach one-time `load` handlers that re-trigger
|
|
227
|
+
// `scrollToBottom`. Idempotent — runs for any image,
|
|
228
|
+
// affects only the new-user-message diff (effect dep is
|
|
229
|
+
// `messages`).
|
|
230
|
+
//
|
|
231
|
+
// `requestAnimationFrame` so the DOM has the new bubble
|
|
232
|
+
// before we query. Without rAF the new-message `<img>`s
|
|
233
|
+
// haven't been committed yet — querySelectorAll returns
|
|
234
|
+
// an empty set.
|
|
235
|
+
requestAnimationFrame(() => {
|
|
236
|
+
const el = contentRef.current
|
|
237
|
+
if (!el) return
|
|
238
|
+
const imgs = el.querySelectorAll('img')
|
|
239
|
+
imgs.forEach((img) => {
|
|
240
|
+
if (img.complete) return
|
|
241
|
+
const onLoad = () => {
|
|
242
|
+
img.removeEventListener('load', onLoad)
|
|
243
|
+
img.removeEventListener('error', onLoad)
|
|
244
|
+
// `ignoreEscapes: true` because the user may have
|
|
245
|
+
// accidentally moved scroll during the image-load
|
|
246
|
+
// window; we want their fresh send to be visible.
|
|
247
|
+
void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
|
|
248
|
+
}
|
|
249
|
+
img.addEventListener('load', onLoad, { once: true })
|
|
250
|
+
img.addEventListener('error', onLoad, { once: true })
|
|
251
|
+
})
|
|
252
|
+
})
|
|
269
253
|
}
|
|
270
254
|
// Assistant-only new messages → the library's resize-watch
|
|
271
255
|
// already keeps the bottom locked when the user hasn't
|
|
272
256
|
// escaped. No explicit call needed; spring animation runs.
|
|
273
257
|
}
|
|
274
|
-
}, [autoScroll, messages, dialogId, scrollToBottom])
|
|
275
|
-
|
|
276
|
-
// ---- Follow the bottom while the intent is armed -----------------
|
|
277
|
-
// Installs once per mount (all deps are stable). See the
|
|
278
|
-
// `followBottomRef` comment above for WHY this exists.
|
|
279
|
-
useEffect(() => {
|
|
280
|
-
if (!autoScroll) return
|
|
281
|
-
const scroller = scrollRef.current
|
|
282
|
-
const content = contentRef.current
|
|
283
|
-
if (!scroller || !content) return
|
|
284
|
-
|
|
285
|
-
// Re-assert on ANY geometry change. Deliberately WITHOUT
|
|
286
|
-
// `ignoreEscapes`: the library must stay free to hand control back
|
|
287
|
-
// on a real gesture mid-animation. Re-asserting is precisely what
|
|
288
|
-
// re-sets its `isAtBottom`, so a silently-lost lock recovers on the
|
|
289
|
-
// next resize instead of staying broken for the rest of the turn.
|
|
290
|
-
// Cheap to call repeatedly — the library returns the in-flight
|
|
291
|
-
// promise when an animation with the same behaviour is running.
|
|
292
|
-
const measure = () => {
|
|
293
|
-
const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight
|
|
294
|
-
setAtBottom(distance <= BOTTOM_THRESHOLD_PX)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const reassert = () => {
|
|
298
|
-
if (followBottomRef.current) void scrollToBottom({ animation: 'smooth' })
|
|
299
|
-
// Measured on every geometry change, follow armed or not: a
|
|
300
|
-
// sibling growing below the scroller moves the bottom without
|
|
301
|
-
// any scroll event, and the button has to notice.
|
|
302
|
-
measure()
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const ro = new ResizeObserver(reassert)
|
|
306
|
-
ro.observe(content) // tokens, entity cards, images
|
|
307
|
-
ro.observe(scroller) // streaming loader / source chips below it
|
|
308
|
-
|
|
309
|
-
// --- user-intent disarm ---------------------------------------
|
|
310
|
-
// Only a deliberate move AWAY from the bottom releases the lock.
|
|
311
|
-
// Resize-derived scroll events are NOT gestures, which is exactly
|
|
312
|
-
// the distinction the library's heuristic gets wrong.
|
|
313
|
-
const disarm = () => {
|
|
314
|
-
followBottomRef.current = false
|
|
315
|
-
}
|
|
316
|
-
const onWheel = (e: WheelEvent) => {
|
|
317
|
-
if (e.deltaY < 0) disarm()
|
|
318
|
-
}
|
|
319
|
-
const onKeyDown = (e: KeyboardEvent) => {
|
|
320
|
-
if (e.key === 'ArrowUp' || e.key === 'PageUp' || e.key === 'Home') disarm()
|
|
321
|
-
}
|
|
322
|
-
let touchY: number | null = null
|
|
323
|
-
const onTouchStart = (e: TouchEvent) => {
|
|
324
|
-
touchY = e.touches[0]?.clientY ?? null
|
|
325
|
-
}
|
|
326
|
-
const onTouchMove = (e: TouchEvent) => {
|
|
327
|
-
const y = e.touches[0]?.clientY
|
|
328
|
-
if (y == null || touchY == null) return
|
|
329
|
-
if (y > touchY) disarm() // finger travels DOWN → content moves up
|
|
330
|
-
touchY = y
|
|
331
|
-
}
|
|
332
|
-
// Scrollbar drag emits neither wheel nor touch — a scrollTop
|
|
333
|
-
// DECREASE while the pointer is held down is the same intent.
|
|
334
|
-
//
|
|
335
|
-
// ALL pointer listeners are on `window`, deliberately. The viewport is
|
|
336
|
-
// wrapped in `OverlayScrollArea`, and OverlayScrollbars appends its
|
|
337
|
-
// scrollbar handles to the HOST element, NOT to the viewport we hold in
|
|
338
|
-
// `scroller`. A `pointerdown` on the overlay thumb therefore never
|
|
339
|
-
// reaches `scroller`, so listening there would leave `pointerDown`
|
|
340
|
-
// false and the user would be yanked back down mid-drag — the exact
|
|
341
|
-
// gesture this block exists to honor. `window` covers the native
|
|
342
|
-
// scrollbar and the overlay thumb alike.
|
|
343
|
-
//
|
|
344
|
-
// The POINTER listeners use CAPTURE phase. The currently-installed
|
|
345
|
-
// OverlayScrollbars was checked and calls no `stopPropagation` on its
|
|
346
|
-
// pointer path, so bubble would work today — but capture makes the
|
|
347
|
-
// guarantee structural instead of contingent on a third party's
|
|
348
|
-
// internals (a library bump, or any host component between us and
|
|
349
|
-
// `window`, could otherwise strand `pointerDown` at `false` and
|
|
350
|
-
// reintroduce the yank-back bug).
|
|
351
|
-
//
|
|
352
|
-
// `blur` deliberately does NOT use capture. Focus events don't bubble
|
|
353
|
-
// but they DO capture, so a capture listener on `window` fires for
|
|
354
|
-
// EVERY element blur in the document (the focus-delegation idiom) —
|
|
355
|
-
// any focus change during a hold, e.g. the chat input blurring as the
|
|
356
|
-
// user presses down on the scrollbar, would clear `pointerDown` and
|
|
357
|
-
// re-open the very yank-back bug this block prevents. A TRUE window
|
|
358
|
-
// blur targets `window` itself, so an at-target (bubble-phase)
|
|
359
|
-
// listener still receives it: capture buys nothing and costs
|
|
360
|
-
// correctness. Verified in jsdom: descendant blur reaches the capture
|
|
361
|
-
// listener only; window blur reaches both.
|
|
362
|
-
//
|
|
363
|
-
// `pointerDown` must be cleared by every way a drag can END, not just
|
|
364
|
-
// `pointerup`: a button released OUTSIDE the window, or the tab losing
|
|
365
|
-
// focus mid-drag, delivers no `pointerup` at all and would leave the
|
|
366
|
-
// flag stuck true. From then on ANY resize-induced scrollTop clamp — a
|
|
367
|
-
// fetch-mode card settling from skeleton to a shorter height, i.e. the
|
|
368
|
-
// exact case this whole block exists for — reads as a scroll-up gesture
|
|
369
|
-
// and drops the lock. Same for merely HOLDING the button down (text
|
|
370
|
-
// selection) while a card shrinks. Hence `pointercancel` + window
|
|
371
|
-
// `blur`.
|
|
372
|
-
let pointerDown = false
|
|
373
|
-
let lastScrollTop = scroller.scrollTop
|
|
374
|
-
const onPointerDown = () => {
|
|
375
|
-
pointerDown = true
|
|
376
|
-
}
|
|
377
|
-
// A drag that RELEASES at the live end expresses the same intent as
|
|
378
|
-
// clicking jump-to-bottom. The scroll events during that drag can't
|
|
379
|
-
// re-arm (see the gate in `onScroll`), and releasing the button emits
|
|
380
|
-
// no further `scroll`, so the re-arm is evaluated once here, when the
|
|
381
|
-
// gesture is actually over.
|
|
382
|
-
const rearmIfAtBottom = () => {
|
|
383
|
-
if (followBottomRef.current) return
|
|
384
|
-
const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight
|
|
385
|
-
if (distance <= BOTTOM_THRESHOLD_PX) followBottomRef.current = true
|
|
386
|
-
}
|
|
387
|
-
const endPointer = () => {
|
|
388
|
-
pointerDown = false
|
|
389
|
-
rearmIfAtBottom()
|
|
390
|
-
}
|
|
391
|
-
const onScroll = () => {
|
|
392
|
-
const st = scroller.scrollTop
|
|
393
|
-
if (pointerDown && st < lastScrollTop) disarm()
|
|
394
|
-
lastScrollTop = st
|
|
395
|
-
// Landing at the bottom by hand is the SAME expressed intent as
|
|
396
|
-
// clicking jump-to-bottom, so it re-arms a lock an earlier scroll-up
|
|
397
|
-
// released. Without it the thread sits at-bottom (button hidden,
|
|
398
|
-
// because `atBottom` is true) and then silently drifts away as the
|
|
399
|
-
// answer grows.
|
|
400
|
-
//
|
|
401
|
-
// Gated on the drag being OVER (`!pointerDown`). A mid-drag `scroll`
|
|
402
|
-
// is not a gesture that has ended, and a short scrollbar drag that
|
|
403
|
-
// starts at the live end stays inside the 70px band — so re-arming
|
|
404
|
-
// here would undo the `disarm()` two lines above on the very same
|
|
405
|
-
// event and yank the reader back down mid-drag. Releasing the button
|
|
406
|
-
// at the bottom still re-arms on the next scroll (or, if none
|
|
407
|
-
// follows, on the next deliberate return to the end).
|
|
408
|
-
if (
|
|
409
|
-
!pointerDown &&
|
|
410
|
-
!followBottomRef.current &&
|
|
411
|
-
scroller.scrollHeight - st - scroller.clientHeight <= BOTTOM_THRESHOLD_PX
|
|
412
|
-
) {
|
|
413
|
-
followBottomRef.current = true
|
|
414
|
-
}
|
|
415
|
-
measure()
|
|
416
|
-
}
|
|
417
|
-
measure()
|
|
418
|
-
|
|
419
|
-
const winCapture = { capture: true, passive: true } as const
|
|
420
|
-
|
|
421
|
-
scroller.addEventListener('wheel', onWheel, { passive: true })
|
|
422
|
-
scroller.addEventListener('keydown', onKeyDown)
|
|
423
|
-
scroller.addEventListener('touchstart', onTouchStart, { passive: true })
|
|
424
|
-
scroller.addEventListener('touchmove', onTouchMove, { passive: true })
|
|
425
|
-
window.addEventListener('pointerdown', onPointerDown, winCapture)
|
|
426
|
-
scroller.addEventListener('scroll', onScroll, { passive: true })
|
|
427
|
-
window.addEventListener('pointerup', endPointer, winCapture)
|
|
428
|
-
window.addEventListener('pointercancel', endPointer, winCapture)
|
|
429
|
-
// Bubble phase, NOT capture — see the note above.
|
|
430
|
-
window.addEventListener('blur', endPointer, { passive: true })
|
|
431
|
-
|
|
432
|
-
return () => {
|
|
433
|
-
ro.disconnect()
|
|
434
|
-
scroller.removeEventListener('wheel', onWheel)
|
|
435
|
-
scroller.removeEventListener('keydown', onKeyDown)
|
|
436
|
-
scroller.removeEventListener('touchstart', onTouchStart)
|
|
437
|
-
scroller.removeEventListener('touchmove', onTouchMove)
|
|
438
|
-
// `capture` must match the add call or the listener is not removed.
|
|
439
|
-
window.removeEventListener('pointerdown', onPointerDown, { capture: true })
|
|
440
|
-
scroller.removeEventListener('scroll', onScroll)
|
|
441
|
-
window.removeEventListener('pointerup', endPointer, { capture: true })
|
|
442
|
-
window.removeEventListener('pointercancel', endPointer, { capture: true })
|
|
443
|
-
// Added WITHOUT capture — the remove must match.
|
|
444
|
-
window.removeEventListener('blur', endPointer)
|
|
445
|
-
}
|
|
446
|
-
}, [autoScroll, scrollRef, contentRef, scrollToBottom])
|
|
258
|
+
}, [autoScroll, messages, dialogId, scrollToBottom, contentRef])
|
|
447
259
|
|
|
448
260
|
// ---- Passive-demo hard pin (pinBottom) ---------------------------
|
|
449
261
|
// Scripted in-page replays (the Fae/Mingo demos) have no human at the
|
|
@@ -573,10 +385,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
573
385
|
// `stopScroll` MUST precede scrollIntoView — the imperative scrollTop
|
|
574
386
|
// write is what the library's geometry tracker uses to flip
|
|
575
387
|
// `escapedFromLock`, so any in-flight scrollToBottom spring is
|
|
576
|
-
// first cancelled.
|
|
577
|
-
// same reason: this turn deliberately parks at the message TOP, so
|
|
578
|
-
// its own async growth must NOT drag the reader to the bottom.
|
|
579
|
-
followBottomRef.current = false
|
|
388
|
+
// first cancelled.
|
|
580
389
|
stopScroll()
|
|
581
390
|
// Instant (not smooth): the markdown body contains async <img>
|
|
582
391
|
// children whose loads race a 300ms smooth animation and leave
|
|
@@ -720,139 +529,76 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
720
529
|
!(pendingApprovals && pendingApprovals.length > 0)
|
|
721
530
|
|
|
722
531
|
return (
|
|
723
|
-
<div className="flex-1 min-h-0 flex flex-col">
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
532
|
+
<div className="relative flex-1 min-h-0 flex flex-col">
|
|
533
|
+
<OverlayScrollArea
|
|
534
|
+
viewportRef={setScrollRef}
|
|
535
|
+
className="flex-1 min-h-0"
|
|
536
|
+
contentClassName={cn(
|
|
537
|
+
"flex flex-col overflow-x-hidden",
|
|
538
|
+
// `overscroll-contain` (default ON, opt-out via `overscrollContain={false}`):
|
|
539
|
+
// reaching the top/bottom of the thread must NOT chain the wheel/
|
|
540
|
+
// touch scroll to the page behind the chat — e.g. the company-hub
|
|
541
|
+
// deck (native body scroll + sticky slide panels) would otherwise
|
|
542
|
+
// advance slides while you scroll the chat (fixed in #1501; kept as
|
|
543
|
+
// the prop default here). Passive in-page DEMO chats disable it so
|
|
544
|
+
// the surrounding page keeps scrolling normally when the pointer is
|
|
545
|
+
// over the (non-interactive) thread.
|
|
546
|
+
overscrollContain && "overscroll-contain",
|
|
547
|
+
className,
|
|
548
|
+
)}
|
|
549
|
+
{...props}
|
|
550
|
+
>
|
|
551
|
+
<div
|
|
552
|
+
ref={setContentRef}
|
|
553
|
+
className={cn(
|
|
554
|
+
// `fullWidth=true` drops the centered-narrow column for
|
|
555
|
+
// side-panel hosts (e.g. multi-platform-hub Mingo). Same
|
|
556
|
+
// semantics as ChatHeader / ChatInput / ChatFooter.
|
|
557
|
+
fullWidth
|
|
558
|
+
? "flex w-full flex-col pb-[var(--spacing-system-xs)] min-w-0"
|
|
559
|
+
: "mx-auto flex w-full max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)] min-w-0",
|
|
560
|
+
contentClassName ?? "px-[var(--spacing-system-m)]",
|
|
746
561
|
)}
|
|
747
|
-
{
|
|
562
|
+
style={{ minHeight: '100%' }}
|
|
748
563
|
>
|
|
749
|
-
|
|
750
|
-
ref={
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
)
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
// execution, approval card, thinking) counts as visible.
|
|
788
|
-
const isEmptyPendingTurn =
|
|
789
|
-
index === messages.length - 1 &&
|
|
790
|
-
isTyping &&
|
|
791
|
-
message.role === 'assistant' &&
|
|
792
|
-
!hasNonEmptyContent(message.content) &&
|
|
793
|
-
(!Array.isArray(message.content) ||
|
|
794
|
-
message.content.every((s) => s.type === 'text'))
|
|
795
|
-
if (isEmptyPendingTurn) return null
|
|
796
|
-
return (
|
|
797
|
-
<ChatMessageEnhanced
|
|
798
|
-
key={message.id}
|
|
799
|
-
ref={getRegisterMessageEl(message.id)}
|
|
800
|
-
role={message.role}
|
|
801
|
-
name={message.name}
|
|
802
|
-
content={message.content}
|
|
803
|
-
timestamp={message.timestamp}
|
|
804
|
-
isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
|
|
805
|
-
avatar={showAvatars ? message.avatar : null}
|
|
806
|
-
showAvatar={showAvatars}
|
|
807
|
-
assistantType={message.assistantType || assistantType}
|
|
808
|
-
approvalVariant={approvalVariant}
|
|
809
|
-
authorType={message.authorType}
|
|
810
|
-
assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
|
|
811
|
-
chatRefs={message.chatRefs}
|
|
812
|
-
contextItems={message.contextItems}
|
|
813
|
-
resolveContextIcon={resolveContextIcon}
|
|
814
|
-
renderContextItem={renderContextItem}
|
|
815
|
-
renderMention={renderMention}
|
|
816
|
-
renderEntityCard={renderEntityCard}
|
|
817
|
-
NavLinkAnchor={NavLinkAnchor}
|
|
818
|
-
/>
|
|
819
|
-
)
|
|
820
|
-
})}
|
|
821
|
-
</div>
|
|
822
|
-
</OverlayScrollArea>
|
|
823
|
-
|
|
824
|
-
{/* Jump to bottom. The manual escape hatch from auto-scroll that
|
|
825
|
-
every 2026 chat surface ships (AI Elements calls it
|
|
826
|
-
`ConversationScrollButton`) and the WCAG 2.2.2 counterpart to
|
|
827
|
-
moving content: the reader can always get back to the live
|
|
828
|
-
end without hunting for it. Positioned inside the scroller's
|
|
829
|
-
OWN `relative` wrapper above, so `bottom` really is the
|
|
830
|
-
scroller's lower edge and not the outer column's (which also
|
|
831
|
-
carries the streaming loader). It costs the thread no layout
|
|
832
|
-
height either way — a sibling would shrink the scroller and
|
|
833
|
-
move the very bottom it points at. Hidden while at the bottom
|
|
834
|
-
and in passive demo hosts. */}
|
|
835
|
-
{autoScroll && !atBottom && messages.length > 0 && (
|
|
836
|
-
<button
|
|
837
|
-
type="button"
|
|
838
|
-
onClick={() => {
|
|
839
|
-
// Clicking IS the intent to return to the live end, so it
|
|
840
|
-
// re-arms the follow lock a scroll-up had released.
|
|
841
|
-
followBottomRef.current = true
|
|
842
|
-
void scrollToBottom({ animation: 'smooth' })
|
|
843
|
-
}}
|
|
844
|
-
aria-label="Scroll to latest message"
|
|
845
|
-
className={cn(
|
|
846
|
-
"absolute bottom-[var(--spacing-system-s)] left-1/2 -translate-x-1/2 z-10",
|
|
847
|
-
"flex items-center justify-center size-9 rounded-full",
|
|
848
|
-
"bg-ods-card text-ods-text-primary border border-ods-border shadow-lg",
|
|
849
|
-
"hover:bg-ods-bg-hover transition-colors",
|
|
850
|
-
)}
|
|
851
|
-
>
|
|
852
|
-
<Arrow02DownIcon size={16} />
|
|
853
|
-
</button>
|
|
854
|
-
)}
|
|
855
|
-
</div>
|
|
564
|
+
{hasNextPage && (
|
|
565
|
+
<div ref={sentinelRef} className="h-px" />
|
|
566
|
+
)}
|
|
567
|
+
<div className="flex-1" />
|
|
568
|
+
{messages.map((message, index) => {
|
|
569
|
+
// Hidden messages (synthetic continuation prompts the host
|
|
570
|
+
// injects after an approval card) are part of the API
|
|
571
|
+
// conversation history but never render. Skipping here
|
|
572
|
+
// keeps the visible thread coherent — see
|
|
573
|
+
// `Message.hidden` doc-comment in message.types.ts.
|
|
574
|
+
if (message.hidden) return null
|
|
575
|
+
return (
|
|
576
|
+
<ChatMessageEnhanced
|
|
577
|
+
key={message.id}
|
|
578
|
+
ref={getRegisterMessageEl(message.id)}
|
|
579
|
+
role={message.role}
|
|
580
|
+
name={message.name}
|
|
581
|
+
content={message.content}
|
|
582
|
+
timestamp={message.timestamp}
|
|
583
|
+
isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
|
|
584
|
+
avatar={showAvatars ? message.avatar : null}
|
|
585
|
+
showAvatar={showAvatars}
|
|
586
|
+
assistantType={message.assistantType || assistantType}
|
|
587
|
+
approvalVariant={approvalVariant}
|
|
588
|
+
authorType={message.authorType}
|
|
589
|
+
assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
|
|
590
|
+
chatRefs={message.chatRefs}
|
|
591
|
+
contextItems={message.contextItems}
|
|
592
|
+
resolveContextIcon={resolveContextIcon}
|
|
593
|
+
renderContextItem={renderContextItem}
|
|
594
|
+
renderMention={renderMention}
|
|
595
|
+
renderEntityCard={renderEntityCard}
|
|
596
|
+
NavLinkAnchor={NavLinkAnchor}
|
|
597
|
+
/>
|
|
598
|
+
)
|
|
599
|
+
})}
|
|
600
|
+
</div>
|
|
601
|
+
</OverlayScrollArea>
|
|
856
602
|
|
|
857
603
|
{/* Footer-pinned streaming loader — outside the scroller so it
|
|
858
604
|
doesn't jitter as the streaming message grows. Color is set
|
|
@@ -1510,12 +1510,6 @@ function EmbeddableChatInner({
|
|
|
1510
1510
|
content: m.segments && m.segments.length > 0 ? m.segments : m.content,
|
|
1511
1511
|
timestamp,
|
|
1512
1512
|
assistantType: m.role === 'assistant' ? ('mingo' as const) : undefined,
|
|
1513
|
-
// `hidden` is load-bearing, NOT cosmetic: it carries synthetic rows
|
|
1514
|
-
// (e.g. an auto-continuation directive) that the LLM must see but the
|
|
1515
|
-
// reader must not. Dropping it here made the raw directive text render
|
|
1516
|
-
// as an ordinary bubble. This field-by-field rebuild has to forward it
|
|
1517
|
-
// explicitly — see `Message.hidden` and `chat-message-list`'s skip.
|
|
1518
|
-
...(m.hidden ? { hidden: true } : {}),
|
|
1519
1513
|
...(m.chatRefs ? { chatRefs: m.chatRefs } : {}),
|
|
1520
1514
|
...(m.scrollAnchor ? { scrollAnchor: m.scrollAnchor } : {}),
|
|
1521
1515
|
// Forward attached context items so the user bubble renders its chips.
|
|
@@ -5,7 +5,7 @@ Barrel export file for the chat hooks module, re-exporting all public hooks from
|
|
|
5
5
|
|
|
6
6
|
| Export Group | Hooks |
|
|
7
7
|
|---|---|
|
|
8
|
-
| **Realtime / Streaming** | `useChunkCatchup`, `useJetstreamDialogSubscription`, `useNatsDialogSubscription` |
|
|
8
|
+
| **Realtime / Streaming** | `useChunkCatchup`, `useRealtimeChunkProcessor`, `useJetstreamDialogSubscription`, `useNatsDialogSubscription` |
|
|
9
9
|
| **UI Utilities** | `useCollapsible`, `useCloseOnNavigation`, `useChatCardItem` |
|
|
10
10
|
| **Chat Core** | `useChat`, `useSSE`, `useChatIdentity`, `useChatAttachments`, `useChatAttachmentImageGallery`, `useSlashCommands` |
|
|
11
11
|
| **Transport Adapters** | `useSSEChatAdapter`, `useNatsChatAdapter` |
|
|
@@ -21,7 +21,7 @@ Main hook. Composes three internal hooks:
|
|
|
21
21
|
```text
|
|
22
22
|
useNatsDialogSubscription → live NATS event tail
|
|
23
23
|
↓
|
|
24
|
-
|
|
24
|
+
useRealtimeChunkProcessor → chunk → MessageSegment[]
|
|
25
25
|
↓
|
|
26
26
|
useChunkCatchup → back-fills missed chunks on reconnect/activation
|
|
27
27
|
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!-- source-hash: e37a3772ece6c09122bfe5d89ce800e0 -->
|
|
2
|
+
Real-time NATS streaming chunk processor hook that converts raw streaming messages into typed message segments, managing stream state, tool execution, and approval workflows.
|
|
3
|
+
|
|
4
|
+
## Key Components
|
|
5
|
+
|
|
6
|
+
| Export | Type | Description |
|
|
7
|
+
|--------|------|-------------|
|
|
8
|
+
| `useRealtimeChunkProcessor` | Hook | Primary hook for processing NATS stream chunks into UI-ready message segments |
|
|
9
|
+
| `accumulatorRef` | Internal Ref | Holds a `MessageSegmentAccumulator` instance that tracks and builds segments across chunk events |
|
|
10
|
+
| `isInStreamRef` | Internal Ref | Tracks whether a `MESSAGE_START`/`MESSAGE_END` boundary is active |
|
|
11
|
+
| `hasEverStreamedRef` | Internal Ref | Distinguishes cold-start vs. resumed-dialog continuation mode |
|
|
12
|
+
| `pendingEscalatedRef` | Internal Ref | Tracks escalated (non-display) approval requests awaiting results |
|
|
13
|
+
|
|
14
|
+
### Handled Chunk Actions
|
|
15
|
+
|
|
16
|
+
| Action | Behavior |
|
|
17
|
+
|--------|----------|
|
|
18
|
+
| `message_start` / `message_end` | Opens/closes stream window, resets segment accumulator |
|
|
19
|
+
| `text` / `thinking` | Appends to accumulator; uses append mode post-stream |
|
|
20
|
+
| `tool_execution` | Routes to accumulator in-stream, or `onToolExecuted` post-stream |
|
|
21
|
+
| `approval_request` | Renders inline or escalates based on `displayApprovalTypes` |
|
|
22
|
+
| `approval_batch` | Renders batch card or unfolds into legacy cards if `batchApprovalsEnabled = false` |
|
|
23
|
+
| `approval_result` | Resolves escalated or inline approvals, updates accumulator status |
|
|
24
|
+
| `error` | Parses error detail and emits error segment |
|
|
25
|
+
|
|
26
|
+
## Usage Example
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const { processChunk } = useRealtimeChunkProcessor({
|
|
30
|
+
callbacks: {
|
|
31
|
+
onSegmentsUpdate: (segments, opts) => updateMessage(segments, opts),
|
|
32
|
+
onStreamStart: () => setStreaming(true),
|
|
33
|
+
onStreamEnd: () => setStreaming(false),
|
|
34
|
+
onApprove: (requestId) => approveToolCall(requestId),
|
|
35
|
+
onReject: (requestId) => rejectToolCall(requestId),
|
|
36
|
+
onEscalatedApproval: (requestId, data) => notifyTechnician(requestId, data),
|
|
37
|
+
onApprovalResolved: (requestId, status) => syncStatus(requestId, status),
|
|
38
|
+
onError: (error) => console.error(error),
|
|
39
|
+
},
|
|
40
|
+
displayApprovalTypes: ['CLIENT'],
|
|
41
|
+
approvalStatuses: { 'req-123': 'pending' },
|
|
42
|
+
enableThinking: false,
|
|
43
|
+
batchApprovalsEnabled: true,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Feed raw NATS chunks directly
|
|
47
|
+
natsSubscription.on('message', (msg) => processChunk(msg.data))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> **Note:** `batchApprovalsEnabled` defaults to `true`. Pass `false` to fall back to legacy per-call approval cards. Consumers using `onApprovalResolved` should omit `onSegmentsUpdate` handling for approval results to avoid bubble overwrites during status flips.
|
|
@@ -5,6 +5,7 @@ export * from './use-collapsible'
|
|
|
5
5
|
export * from './use-scripted-stream'
|
|
6
6
|
export * from './use-jetstream-dialog-subscription'
|
|
7
7
|
export * from './use-nats-dialog-subscription'
|
|
8
|
+
export * from './use-realtime-chunk-processor'
|
|
8
9
|
|
|
9
10
|
// Chat hooks migrated from hub `hooks/*` (Task #7).
|
|
10
11
|
export * from './use-slash-commands'
|