@flamingo-stack/openframe-frontend-core 0.0.457 → 0.0.458-snapshot.20260720215024
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/chat-protocol/decode.d.ts +58 -0
- package/dist/chat-protocol/decode.d.ts.map +1 -0
- package/dist/chat-protocol/encode.d.ts +44 -0
- package/dist/chat-protocol/encode.d.ts.map +1 -0
- package/dist/chat-protocol/env-flag.d.ts +35 -0
- package/dist/chat-protocol/env-flag.d.ts.map +1 -0
- package/dist/chat-protocol/events.d.ts +192 -0
- package/dist/chat-protocol/events.d.ts.map +1 -0
- package/dist/chat-protocol/frames.d.ts +206 -0
- package/dist/chat-protocol/frames.d.ts.map +1 -0
- package/dist/chat-protocol/index.cjs +493 -0
- package/dist/chat-protocol/index.cjs.map +1 -0
- package/dist/chat-protocol/index.d.ts +18 -0
- package/dist/chat-protocol/index.d.ts.map +1 -0
- package/dist/chat-protocol/index.js +477 -0
- package/dist/chat-protocol/index.js.map +1 -0
- package/dist/chat-protocol/ip-normalize.d.ts +44 -0
- package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
- package/dist/chat-protocol/nats-decoder.d.ts +26 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
- package/dist/{chunk-SQ3CP2IV.cjs → chunk-3N3TMZ62.cjs} +7 -7
- package/dist/{chunk-SQ3CP2IV.cjs.map → chunk-3N3TMZ62.cjs.map} +1 -1
- package/dist/{chunk-I7M6KCCS.js → chunk-4R6H2U77.js} +4 -4
- package/dist/{chunk-CCT5VCRP.cjs → chunk-4XDWNAZW.cjs} +9 -9
- package/dist/{chunk-CCT5VCRP.cjs.map → chunk-4XDWNAZW.cjs.map} +1 -1
- package/dist/{chunk-KQ36EHE4.cjs → chunk-7KSEINAE.cjs} +15 -15
- package/dist/{chunk-KQ36EHE4.cjs.map → chunk-7KSEINAE.cjs.map} +1 -1
- package/dist/{chunk-FM5DTJPA.cjs → chunk-ADD3FPDG.cjs} +6851 -4713
- package/dist/chunk-ADD3FPDG.cjs.map +1 -0
- package/dist/{chunk-RSW42GTZ.js → chunk-APF5QBAU.js} +2 -2
- package/dist/{chunk-YCIVMKRS.cjs → chunk-BLZZXZNM.cjs} +26 -26
- package/dist/{chunk-YCIVMKRS.cjs.map → chunk-BLZZXZNM.cjs.map} +1 -1
- package/dist/{chunk-YHB4MNUX.cjs → chunk-BQ7PMWLM.cjs} +28 -27
- package/dist/chunk-BQ7PMWLM.cjs.map +1 -0
- package/dist/{chunk-GJ7BN4MI.js → chunk-CGNDAJ7V.js} +4 -4
- package/dist/{chunk-R5IQJXA3.js → chunk-F2NMWV44.js} +2 -2
- package/dist/{chunk-EOVQVVDJ.cjs → chunk-F7F43U7Q.cjs} +11 -11
- package/dist/{chunk-EOVQVVDJ.cjs.map → chunk-F7F43U7Q.cjs.map} +1 -1
- package/dist/{chunk-2ZW2ZPDW.js → chunk-LAOO7XHO.js} +2 -2
- package/dist/{chunk-O7F7RZFN.js → chunk-LJKZIIK4.js} +6 -5
- package/dist/chunk-LJKZIIK4.js.map +1 -0
- package/dist/{chunk-AJGZ3YSR.js → chunk-OR5AMXYN.js} +2 -2
- package/dist/{chunk-5XFJG3TC.cjs → chunk-P5JQDFRM.cjs} +60 -60
- package/dist/chunk-P5JQDFRM.cjs.map +1 -0
- package/dist/{chunk-U4APSAAR.cjs → chunk-PXZ7YQ5V.cjs} +5 -5
- package/dist/{chunk-U4APSAAR.cjs.map → chunk-PXZ7YQ5V.cjs.map} +1 -1
- package/dist/{chunk-WXDEMTLD.js → chunk-QO2XD3FR.js} +2 -2
- package/dist/{chunk-C2TDHJBO.cjs → chunk-QSGZE6AZ.cjs} +37 -37
- package/dist/{chunk-C2TDHJBO.cjs.map → chunk-QSGZE6AZ.cjs.map} +1 -1
- package/dist/{chunk-NSL5MNS4.js → chunk-REWRUV3K.js} +11644 -9506
- package/dist/chunk-REWRUV3K.js.map +1 -0
- package/dist/{chunk-BDTQIIJR.cjs → chunk-TA7MFVUS.cjs} +31 -31
- package/dist/{chunk-BDTQIIJR.cjs.map → chunk-TA7MFVUS.cjs.map} +1 -1
- package/dist/{chunk-4423DHRX.js → chunk-UI7CIUAR.js} +2 -2
- package/dist/{chunk-5BKBG653.js → chunk-VXJH5TDN.js} +10 -10
- package/dist/chunk-VXJH5TDN.js.map +1 -0
- package/dist/{chunk-AB65PWU2.cjs → chunk-WCE66C2Z.cjs} +62 -62
- package/dist/{chunk-AB65PWU2.cjs.map → chunk-WCE66C2Z.cjs.map} +1 -1
- package/dist/{chunk-KXIIUE75.js → chunk-XF5ROTMQ.js} +2 -2
- 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 +0 -1
- 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 +15 -48
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +3 -47
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +10 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +21 -13
- package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
- package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
- package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
- package/dist/components/chat/stream/index.d.ts +17 -0
- package/dist/components/chat/stream/index.d.ts.map +1 -0
- package/dist/components/chat/stream/message-mutations.d.ts +77 -0
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/types/api.types.d.ts +1 -81
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +2 -90
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +2 -3
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
- 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/features/time-tracker/time-tracker-panel.d.ts.map +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 +171 -129
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +63 -21
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-container.d.ts.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 +44 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +55 -13
- package/dist/components/ui/markdown/base-components.d.ts +56 -0
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
- package/dist/components/ui/markdown/engine.d.ts +99 -0
- package/dist/components/ui/markdown/engine.d.ts.map +1 -0
- package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
- package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
- package/dist/components/ui/markdown/index.d.ts +18 -0
- package/dist/components/ui/markdown/index.d.ts.map +1 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
- package/dist/components/ui/markdown/sanitize.d.ts +153 -0
- package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/streaming.d.ts +78 -0
- package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
- package/dist/components/ui/markdown/text-size.d.ts +34 -0
- package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
- package/dist/components/ui/modal-v2.d.ts.map +1 -1
- package/dist/components/ui/modal.d.ts.map +1 -1
- package/dist/index.cjs +60 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +71 -13
- package/dist/utils/index.cjs +169 -40
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +162 -41
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markdown-fences.d.ts +42 -0
- package/dist/utils/markdown-fences.d.ts.map +1 -0
- package/dist/utils/markdown-heading-id.d.ts +85 -0
- package/dist/utils/markdown-heading-id.d.ts.map +1 -0
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +307 -0
- package/src/chat-protocol/__tests__/chat-protocol.test.ts +515 -0
- package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
- package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +228 -0
- package/src/chat-protocol/decode.ts +277 -0
- package/src/chat-protocol/encode.ts +71 -0
- package/src/chat-protocol/env-flag.ts +39 -0
- package/src/chat-protocol/events.ts +228 -0
- package/src/chat-protocol/frames.ts +238 -0
- package/src/chat-protocol/index.ts +21 -0
- package/src/chat-protocol/ip-normalize.ts +146 -0
- package/src/chat-protocol/nats-decoder.ts +252 -0
- package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +209 -0
- package/src/components/chat/chat-message-enhanced.tsx +111 -17
- package/src/components/chat/chat-message-list.tsx +367 -113
- package/src/components/chat/embeddable-chat.tsx +6 -0
- 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/__tests__/__snapshots__/persisted-state-v1-golden.test.ts.snap +297 -0
- 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 +272 -0
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
- package/src/components/chat/hooks/index.ts +0 -1
- package/src/components/chat/hooks/use-chat.ts +8 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -822
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +318 -666
- package/src/components/chat/index.ts +5 -0
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +937 -0
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +894 -0
- package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
- package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
- package/src/components/chat/stream/chat-dialog-store.ts +536 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +1796 -0
- package/src/components/chat/stream/delta-batcher.ts +159 -0
- package/src/components/chat/stream/index.ts +55 -0
- package/src/components/chat/stream/message-mutations.ts +338 -0
- package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
- package/src/components/chat/thinking-display.tsx +1 -1
- package/src/components/chat/types/.api.types.md +1 -3
- package/src/components/chat/types/.processing.types.md +6 -23
- package/src/components/chat/types/api.types.ts +6 -73
- package/src/components/chat/types/processing.types.ts +11 -52
- package/src/components/chat/utils/.index.md +0 -5
- package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +418 -0
- package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +599 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +317 -0
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
- package/src/components/chat/utils/index.ts +5 -8
- package/src/components/chat/utils/process-historical-messages.ts +352 -383
- package/src/components/features/notifications/notification-drawer.tsx +4 -2
- package/src/components/features/time-tracker/time-tracker-panel.tsx +8 -2
- package/src/components/layout/page-container.tsx +5 -1
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +19 -4
- package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
- package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
- package/src/components/ui/index.ts +1 -2
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
- package/src/components/ui/markdown/base-components.tsx +341 -0
- package/src/components/ui/markdown/engine.tsx +315 -0
- package/src/components/ui/markdown/heading-ids.ts +239 -0
- package/src/components/ui/markdown/index.ts +49 -0
- package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
- package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
- package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
- package/src/components/ui/markdown/sanitize.ts +3499 -0
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
- package/src/components/ui/markdown/streaming.ts +362 -0
- package/src/components/ui/markdown/text-size.ts +106 -0
- package/src/components/ui/modal-v2.tsx +6 -0
- package/src/components/ui/modal.tsx +3 -0
- package/src/components/ui/release-changelog-section.tsx +1 -1
- package/src/components/ui/ticket-info-section.tsx +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown-fences.ts +127 -0
- package/src/utils/markdown-heading-id.ts +348 -0
- package/src/utils/markdown-section-extractor.ts +41 -56
- package/dist/chunk-5BKBG653.js.map +0 -1
- package/dist/chunk-5XFJG3TC.cjs.map +0 -1
- package/dist/chunk-FM5DTJPA.cjs.map +0 -1
- package/dist/chunk-NSL5MNS4.js.map +0 -1
- package/dist/chunk-O7F7RZFN.js.map +0 -1
- package/dist/chunk-YHB4MNUX.cjs.map +0 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
- package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
- package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -50
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -479
- package/src/components/chat/utils/.chunk-parser.md +0 -51
- package/src/components/chat/utils/chunk-parser.ts +0 -255
- package/src/components/ui/.simple-markdown-renderer.md +0 -40
- package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
- package/src/components/ui/simple-markdown-renderer.tsx +0 -965
- /package/dist/{chunk-I7M6KCCS.js.map → chunk-4R6H2U77.js.map} +0 -0
- /package/dist/{chunk-RSW42GTZ.js.map → chunk-APF5QBAU.js.map} +0 -0
- /package/dist/{chunk-GJ7BN4MI.js.map → chunk-CGNDAJ7V.js.map} +0 -0
- /package/dist/{chunk-R5IQJXA3.js.map → chunk-F2NMWV44.js.map} +0 -0
- /package/dist/{chunk-2ZW2ZPDW.js.map → chunk-LAOO7XHO.js.map} +0 -0
- /package/dist/{chunk-AJGZ3YSR.js.map → chunk-OR5AMXYN.js.map} +0 -0
- /package/dist/{chunk-WXDEMTLD.js.map → chunk-QO2XD3FR.js.map} +0 -0
- /package/dist/{chunk-4423DHRX.js.map → chunk-UI7CIUAR.js.map} +0 -0
- /package/dist/{chunk-KXIIUE75.js.map → chunk-XF5ROTMQ.js.map} +0 -0
|
@@ -6,10 +6,18 @@
|
|
|
6
6
|
* counterpart is `useNatsChatAdapter` (Mingo mode). The public
|
|
7
7
|
* `useChat({ mode })` dispatches between them based on `mode.transport`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Phase 3 of the chat unification: the adapter no longer owns a parser or a
|
|
10
|
+
* message-merge layer. Raw response bytes flow
|
|
11
|
+
*
|
|
12
|
+
* fetch body → createSseFrameDecoder → ChatStreamEvent →
|
|
13
|
+
* createChatStreamReducer (transport: 'sse') — THE master reader
|
|
14
|
+
*
|
|
15
|
+
* The reducer absorbed `useChat`'s trailing-assistant merge, the
|
|
16
|
+
* `decision_resolved` receipt path, and the sendIdx-keyed
|
|
17
|
+
* sources/refs/meta maps; this file keeps only the transport wiring
|
|
18
|
+
* (fetch/abort, request-body building), localStorage persistence
|
|
19
|
+
* (PersistedChatState v1 — the sendIdx fan-out lookup below is the
|
|
20
|
+
* rehydration contract), and the public `UnifiedChatState` mapping.
|
|
13
21
|
*
|
|
14
22
|
* Two key contracts vs hub-side chat hooks:
|
|
15
23
|
*
|
|
@@ -19,46 +27,44 @@
|
|
|
19
27
|
* namespace. It is NEVER sent on the wire (the hub resolves source
|
|
20
28
|
* server-side via `currentPlatform()`).
|
|
21
29
|
*
|
|
22
|
-
* 2.
|
|
23
|
-
* its own `decideNewTab` + `useDocNavigation.navigate` plumbing for
|
|
24
|
-
* the search autocomplete (it calls `router.push`). The chat body
|
|
25
|
-
* doesn't navigate via this hook — click handlers on rendered
|
|
26
|
-
* cards/chips go through `handleChatNavClick` instead.
|
|
27
|
-
*
|
|
28
|
-
* 3. `tableIdForDocumentType` is INJECTED via the optional
|
|
30
|
+
* 2. `tableIdForDocumentType` is INJECTED via the optional
|
|
29
31
|
* `tableIdForDocumentType` parameter. Hub callers pass the
|
|
30
32
|
* registry-backed lookup from `lib/config/rag-table-config`;
|
|
31
33
|
* embedders that don't supply one fall back to
|
|
32
|
-
* `defaultTableIdForDocumentType`
|
|
33
|
-
*
|
|
34
|
-
* `displayRef` / `discussRef` Ask + Display buttons WORK out of
|
|
35
|
-
* the box. Override only when you have polymorphic / per-tenant
|
|
36
|
-
* types the default doesn't cover.
|
|
37
|
-
*
|
|
38
|
-
* Wire format and SSE-parser logic mirror the hub original byte-for-byte
|
|
39
|
-
* so server-side and client-side stay in lockstep across the migration.
|
|
34
|
+
* `defaultTableIdForDocumentType` so the `displayRef` / `discussRef`
|
|
35
|
+
* Ask + Display buttons WORK out of the box.
|
|
40
36
|
*/
|
|
41
37
|
|
|
42
|
-
import {
|
|
43
|
-
import { useChat, type Message, type StreamFnExtraOptions } from './use-chat'
|
|
38
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
44
39
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
45
40
|
import type { ChatRef } from '../chat-ref.types'
|
|
46
|
-
import {
|
|
41
|
+
import type { Message } from '../types/message.types'
|
|
47
42
|
import type { MessageSegment } from '../types/message.types'
|
|
48
43
|
import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'
|
|
49
44
|
import { getChatProxyAuth } from '../utils/chat-proxy-auth-storage'
|
|
50
45
|
import { chatAuthedFetch } from '../utils/chat-authed-fetch'
|
|
51
|
-
import {
|
|
46
|
+
import type { ScrollAnchor } from '../utils/scroll-anchor'
|
|
47
|
+
import { createSseFrameDecoder } from '../../../chat-protocol/decode'
|
|
52
48
|
import { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../utils/auto-continuation-directive'
|
|
53
49
|
import { flattenAssistantContent } from '../utils/flatten-assistant-content'
|
|
54
50
|
import { sanitizeTitleForChat } from '../utils/slash-dispatch-utils'
|
|
55
51
|
import { defaultTableIdForDocumentType } from '../utils/source-icons'
|
|
52
|
+
import type { WireCommandOverride } from '../utils/slash-dispatch-utils'
|
|
53
|
+
import type { ChatAttachment } from '../utils/chat-attachment-markdown'
|
|
56
54
|
import type {
|
|
57
55
|
UnifiedChatState,
|
|
56
|
+
UnifiedChatMessage,
|
|
58
57
|
UnifiedSendMessageOptions,
|
|
59
|
-
StreamingPhase,
|
|
60
58
|
} from '../types/unified-chat-state.types'
|
|
61
59
|
import type { DialogItem } from '../types/component.types'
|
|
60
|
+
import { createChatDialogStore, DEFAULT_DIALOG_SIDE } from '../stream/chat-dialog-store'
|
|
61
|
+
import { useChatStreamReducer } from '../stream/use-chat-stream-reducer'
|
|
62
|
+
import type { ChatDialogStore } from '../stream/chat-dialog-store'
|
|
63
|
+
import type { ChatStreamReducerOptions } from '../stream/chat-stream-reducer'
|
|
64
|
+
|
|
65
|
+
// Canonical home of the per-turn meta row moved to the stream module in
|
|
66
|
+
// Phase 3; re-exported here to keep the legacy import path stable.
|
|
67
|
+
export type { ChatTurnMeta } from '../stream/chat-stream-reducer'
|
|
62
68
|
|
|
63
69
|
// =============================================================================
|
|
64
70
|
// Public types
|
|
@@ -121,33 +127,6 @@ export interface DocChatMessage {
|
|
|
121
127
|
// `types/unified-chat-state.types.ts`.
|
|
122
128
|
export type { StreamingPhase } from '../types/unified-chat-state.types'
|
|
123
129
|
|
|
124
|
-
/** Per-turn metadata extracted from the streamed metadata frame. */
|
|
125
|
-
export interface ChatTurnMeta {
|
|
126
|
-
/** Provider key recognized by `<ModelDisplay>` for icon
|
|
127
|
-
* selection: 'anthropic' | 'openai' | 'google' (case-insensitive). */
|
|
128
|
-
provider: string | null
|
|
129
|
-
modelLabel: string | null
|
|
130
|
-
contextWindowMaxTokens: number | null
|
|
131
|
-
/** Input tokens (from message_start). Includes cached tokens. */
|
|
132
|
-
inputTokens: number | null
|
|
133
|
-
/** Output tokens (from message_delta). Only known after stream end. */
|
|
134
|
-
outputTokens: number | null
|
|
135
|
-
/** Cache hit % (read / total-input × 100). Only known after stream end. */
|
|
136
|
-
cacheHitRatePct: number | null
|
|
137
|
-
/** Cross-call usage breakdown extracted from the trailing usage frame. */
|
|
138
|
-
breakdown: {
|
|
139
|
-
haikuRewriter?: { input: number; output: number }
|
|
140
|
-
haikuClassifier?: { input: number; output: number }
|
|
141
|
-
haikuSummarizer?: { input: number; output: number }
|
|
142
|
-
routedAnswer?: { model: string; complexity: string; thinkingBudget: number }
|
|
143
|
-
} | null
|
|
144
|
-
/** Per-message viewport-positioning hint. */
|
|
145
|
-
scrollAnchor: ScrollAnchor | null
|
|
146
|
-
/** Routing decision from the server's `decideRoute`. */
|
|
147
|
-
routedComplexity: string | null
|
|
148
|
-
routedThinkingBudget: number | null
|
|
149
|
-
}
|
|
150
|
-
|
|
151
130
|
/**
|
|
152
131
|
* Optional dependency-injection options for `useSseChatAdapter`.
|
|
153
132
|
*
|
|
@@ -158,13 +137,7 @@ export interface ChatTurnMeta {
|
|
|
158
137
|
* **Defaults to `defaultTableIdForDocumentType` from
|
|
159
138
|
* `src/utils/source-icons.ts`** — a lib-baked map covering every
|
|
160
139
|
* documentType currently registered in the hub's RAG_TABLE_CONFIGS.
|
|
161
|
-
*
|
|
162
|
-
* any callback wiring.
|
|
163
|
-
*
|
|
164
|
-
* Override only when you have polymorphic / per-tenant document
|
|
165
|
-
* types that map to a different tableId than the default
|
|
166
|
-
* (hub-canonical) registry — pass your own `(docType) => tableId`
|
|
167
|
-
* callback and it wins over the default.
|
|
140
|
+
* Override only for polymorphic / per-tenant document types.
|
|
168
141
|
*/
|
|
169
142
|
export interface UseSseChatAdapterOptions {
|
|
170
143
|
tableIdForDocumentType?: (documentType: string) => string | null
|
|
@@ -183,411 +156,7 @@ export interface UseSseChatAdapterRuntimeOptions {
|
|
|
183
156
|
}
|
|
184
157
|
|
|
185
158
|
// =============================================================================
|
|
186
|
-
//
|
|
187
|
-
// =============================================================================
|
|
188
|
-
|
|
189
|
-
/** Single source of truth for a fresh `ChatTurnMeta` row. */
|
|
190
|
-
function createEmptyTurnMeta(): ChatTurnMeta {
|
|
191
|
-
return {
|
|
192
|
-
provider: null,
|
|
193
|
-
modelLabel: null,
|
|
194
|
-
contextWindowMaxTokens: null,
|
|
195
|
-
inputTokens: null,
|
|
196
|
-
outputTokens: null,
|
|
197
|
-
cacheHitRatePct: null,
|
|
198
|
-
breakdown: null,
|
|
199
|
-
scrollAnchor: null,
|
|
200
|
-
routedComplexity: null,
|
|
201
|
-
routedThinkingBudget: null,
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Escape `<` so the lib's `SimpleMarkdownRenderer` (which uses `rehypeRaw`
|
|
207
|
-
* to pass HTML through) doesn't treat XML-like tokens in Claude's
|
|
208
|
-
* thinking output as React components. Escaping `<` → `<` preserves
|
|
209
|
-
* the visible character without breaking blockquote `>` markers.
|
|
210
|
-
*/
|
|
211
|
-
function escapeThinkingTags(text: string): string {
|
|
212
|
-
return text.replace(/</g, '<')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function createDocStreamFn(
|
|
216
|
-
source: DocSource,
|
|
217
|
-
endpoints: { chatStreamUrl: string; approvalToolUrl: string },
|
|
218
|
-
messagesRef: MutableRefObject<Message[]>,
|
|
219
|
-
sourcesMapRef: MutableRefObject<Map<number, ChatSource[]>>,
|
|
220
|
-
refsMapRef: MutableRefObject<Map<number, Record<string, ChatRef>>>,
|
|
221
|
-
metaMapRef: MutableRefObject<Map<number, ChatTurnMeta>>,
|
|
222
|
-
setStreamingPhase: (phase: StreamingPhase) => void,
|
|
223
|
-
bumpMetaTick: () => void,
|
|
224
|
-
sendCountRef: MutableRefObject<number>,
|
|
225
|
-
) {
|
|
226
|
-
// CRITICAL: the decoder + buffer MUST live INSIDE the returned async-
|
|
227
|
-
// generator function (per-call closure), NOT at the factory level. A
|
|
228
|
-
// rapid send-stop-send sequence with hook-level state would feed the
|
|
229
|
-
// second stream's first chunk into the first stream's tail buffer,
|
|
230
|
-
// corrupting metadata-frame parsing.
|
|
231
|
-
return async function* (
|
|
232
|
-
message: string,
|
|
233
|
-
signal?: AbortSignal,
|
|
234
|
-
extra?: StreamFnExtraOptions,
|
|
235
|
-
): AsyncGenerator<MessageSegment> {
|
|
236
|
-
const currentMessages = messagesRef.current || []
|
|
237
|
-
// Filter `hidden:true` messages out of the API history. The approval-
|
|
238
|
-
// action turn injects a hidden user message with `content=''`.
|
|
239
|
-
// `flattenAssistantContent` joins text-segment arrays into a single
|
|
240
|
-
// string so the server sees the receipt + auto-continuation Qs.
|
|
241
|
-
const apiMessages = [
|
|
242
|
-
...currentMessages
|
|
243
|
-
.filter((m) => (m.role === 'user' || m.role === 'assistant') && !m.hidden)
|
|
244
|
-
.map((m) => ({
|
|
245
|
-
role: m.role,
|
|
246
|
-
content:
|
|
247
|
-
typeof m.content === 'string' ? m.content : flattenAssistantContent(m.content),
|
|
248
|
-
})),
|
|
249
|
-
{ role: 'user', content: message },
|
|
250
|
-
]
|
|
251
|
-
|
|
252
|
-
// URL + body branch — approvalAction routes to the approval-tool
|
|
253
|
-
// endpoint, the standard chat path routes to the chat-stream endpoint.
|
|
254
|
-
const targetPath = extra?.approvalAction
|
|
255
|
-
? endpoints.approvalToolUrl
|
|
256
|
-
: endpoints.chatStreamUrl
|
|
257
|
-
// `source` is INTENTIONALLY NOT in the body. The chat route resolves
|
|
258
|
-
// it server-side via its own platform-detection — tamper-proof binding
|
|
259
|
-
// so a client on one platform can't POST a different platform's
|
|
260
|
-
// conversation.
|
|
261
|
-
const requestBody = extra?.approvalAction
|
|
262
|
-
? {
|
|
263
|
-
proposal_id: extra.approvalAction.proposalId,
|
|
264
|
-
action: extra.approvalAction.action,
|
|
265
|
-
messages: currentMessages
|
|
266
|
-
.filter((m) => (m.role === 'user' || m.role === 'assistant') && !m.hidden)
|
|
267
|
-
.map((m) => ({
|
|
268
|
-
role: m.role,
|
|
269
|
-
content:
|
|
270
|
-
typeof m.content === 'string'
|
|
271
|
-
? m.content
|
|
272
|
-
: flattenAssistantContent(m.content),
|
|
273
|
-
})),
|
|
274
|
-
}
|
|
275
|
-
: {
|
|
276
|
-
messages: apiMessages,
|
|
277
|
-
...(extra?.commandOverride ? { commandOverride: extra.commandOverride } : {}),
|
|
278
|
-
...(extra?.pendingAttachments && extra.pendingAttachments.length > 0
|
|
279
|
-
? { pendingAttachments: extra.pendingAttachments }
|
|
280
|
-
: {}),
|
|
281
|
-
}
|
|
282
|
-
// `chatAuthedFetch` carries the bearer-act-as headers (+ Supabase
|
|
283
|
-
// session cookies) — same wrapper `use-chat-attachments` and
|
|
284
|
-
// `use-chat-identity` use, so all three chat-side fetch sites share
|
|
285
|
-
// one identity-propagation primitive.
|
|
286
|
-
const response = await chatAuthedFetch(targetPath, {
|
|
287
|
-
method: 'POST',
|
|
288
|
-
body: JSON.stringify(requestBody),
|
|
289
|
-
signal,
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
if (!response.ok) {
|
|
293
|
-
throw new Error(`Chat request failed: ${response.status}`)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const reader = response.body?.getReader()
|
|
297
|
-
if (!reader) throw new Error('No response body')
|
|
298
|
-
|
|
299
|
-
const decoder = new TextDecoder()
|
|
300
|
-
let buffer = ''
|
|
301
|
-
let inText = false
|
|
302
|
-
// Live thinking accumulator — coalesced to ~20fps to avoid a re-render
|
|
303
|
-
// storm (Anthropic emits 30-60 deltas/sec).
|
|
304
|
-
let thinkingAccum = ''
|
|
305
|
-
let lastThinkingYieldTime = 0
|
|
306
|
-
let pendingThinkingYield = false
|
|
307
|
-
const THINKING_YIELD_INTERVAL_MS = 50
|
|
308
|
-
let trailerBuffer = ''
|
|
309
|
-
let inTrailer = false
|
|
310
|
-
|
|
311
|
-
const sendIdx = sendCountRef.current - 1
|
|
312
|
-
while (true) {
|
|
313
|
-
const { done, value } = await reader.read()
|
|
314
|
-
if (done) break
|
|
315
|
-
|
|
316
|
-
const chunk = decoder.decode(value, { stream: true })
|
|
317
|
-
|
|
318
|
-
if (!inText) {
|
|
319
|
-
buffer += chunk
|
|
320
|
-
while (!inText) {
|
|
321
|
-
const recIdx = buffer.indexOf('\x1E')
|
|
322
|
-
const nullIdx = buffer.indexOf('\0')
|
|
323
|
-
if (recIdx !== -1 && (nullIdx === -1 || recIdx < nullIdx)) {
|
|
324
|
-
if (pendingThinkingYield) {
|
|
325
|
-
pendingThinkingYield = false
|
|
326
|
-
yield { type: 'thinking', text: escapeThinkingTags(thinkingAccum) }
|
|
327
|
-
}
|
|
328
|
-
inText = true
|
|
329
|
-
setStreamingPhase('streaming')
|
|
330
|
-
const after = buffer.slice(recIdx + 1)
|
|
331
|
-
buffer = ''
|
|
332
|
-
if (after) {
|
|
333
|
-
// The `after` slice may ALSO contain the `\x1F` trailing-
|
|
334
|
-
// usage sentinel — common for fixed-answer responses where
|
|
335
|
-
// the whole frame sequence arrives in ONE TCP chunk.
|
|
336
|
-
const unitIdx = after.indexOf('\x1F')
|
|
337
|
-
if (unitIdx === -1) {
|
|
338
|
-
yield { type: 'text', text: after }
|
|
339
|
-
} else {
|
|
340
|
-
const textBefore = after.slice(0, unitIdx)
|
|
341
|
-
const trailerAfter = after.slice(unitIdx + 1)
|
|
342
|
-
if (textBefore) {
|
|
343
|
-
yield { type: 'text', text: textBefore }
|
|
344
|
-
}
|
|
345
|
-
inTrailer = true
|
|
346
|
-
trailerBuffer = trailerAfter
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
break
|
|
350
|
-
}
|
|
351
|
-
if (nullIdx === -1) break // need more bytes
|
|
352
|
-
const metaStr = buffer.slice(0, nullIdx)
|
|
353
|
-
const remaining = buffer.slice(nullIdx + 1)
|
|
354
|
-
let meta: any
|
|
355
|
-
try {
|
|
356
|
-
meta = JSON.parse(metaStr)
|
|
357
|
-
} catch {
|
|
358
|
-
// Not JSON — start of answer body.
|
|
359
|
-
inText = true
|
|
360
|
-
setStreamingPhase('streaming')
|
|
361
|
-
if (buffer.length > 0) {
|
|
362
|
-
yield { type: 'text', text: buffer }
|
|
363
|
-
buffer = ''
|
|
364
|
-
}
|
|
365
|
-
break
|
|
366
|
-
}
|
|
367
|
-
if (meta.status === 'thinking') {
|
|
368
|
-
setStreamingPhase('thinking')
|
|
369
|
-
} else if (meta.kind === 'thinking-delta' && typeof meta.text === 'string') {
|
|
370
|
-
thinkingAccum += meta.text
|
|
371
|
-
const now = Date.now()
|
|
372
|
-
if (now - lastThinkingYieldTime >= THINKING_YIELD_INTERVAL_MS) {
|
|
373
|
-
lastThinkingYieldTime = now
|
|
374
|
-
pendingThinkingYield = false
|
|
375
|
-
yield { type: 'thinking', text: escapeThinkingTags(thinkingAccum) }
|
|
376
|
-
} else {
|
|
377
|
-
pendingThinkingYield = true
|
|
378
|
-
}
|
|
379
|
-
} else if (meta.kind === 'usage' && meta.stage === 'start') {
|
|
380
|
-
mergeTurnMeta(metaMapRef, sendIdx, {
|
|
381
|
-
inputTokens: meta.input_tokens ?? null,
|
|
382
|
-
})
|
|
383
|
-
bumpMetaTick()
|
|
384
|
-
} else if (
|
|
385
|
-
meta.kind === 'decision_resolved' &&
|
|
386
|
-
typeof meta.action === 'string'
|
|
387
|
-
) {
|
|
388
|
-
// Server-driven post-approve / post-reject frame.
|
|
389
|
-
const action = meta.action === 'rejected' ? 'rejected' : 'approved'
|
|
390
|
-
const toolName =
|
|
391
|
-
typeof meta.tool_name === 'string' ? meta.tool_name : undefined
|
|
392
|
-
const result = (meta.result ?? null) as
|
|
393
|
-
| { ticket_id?: string; status?: string | null; mirror_synced?: boolean }
|
|
394
|
-
| null
|
|
395
|
-
const card = (meta.card ?? null) as
|
|
396
|
-
| { type?: string; marker?: string; ref?: ChatRef }
|
|
397
|
-
| null
|
|
398
|
-
if (card?.ref?.id && card?.type) {
|
|
399
|
-
const existing = refsMapRef.current.get(sendIdx) ?? {}
|
|
400
|
-
const key = buildChatRefKey(card.type, card.ref.id)
|
|
401
|
-
refsMapRef.current.set(sendIdx, { ...existing, [key]: card.ref })
|
|
402
|
-
bumpMetaTick()
|
|
403
|
-
}
|
|
404
|
-
yield {
|
|
405
|
-
type: 'decision_resolved',
|
|
406
|
-
action,
|
|
407
|
-
ok: meta.ok === true,
|
|
408
|
-
willAutoContinue: meta.willAutoContinue === true,
|
|
409
|
-
...(toolName ? { toolName } : {}),
|
|
410
|
-
...(result ? { result } : {}),
|
|
411
|
-
...(card?.marker ? { marker: card.marker } : {}),
|
|
412
|
-
...(card?.ref ? { cardRef: card.ref } : {}),
|
|
413
|
-
...(typeof meta.receiptText === 'string'
|
|
414
|
-
? { receiptText: meta.receiptText }
|
|
415
|
-
: {}),
|
|
416
|
-
proposalId:
|
|
417
|
-
typeof meta.proposalId === 'string' ? meta.proposalId : undefined,
|
|
418
|
-
} as any
|
|
419
|
-
} else if (meta.kind === 'approval_request' && meta.proposalId) {
|
|
420
|
-
// The model called a write tool. Server persisted the
|
|
421
|
-
// proposal and sent us a CARD-READY payload.
|
|
422
|
-
const proposalId = String(meta.proposalId)
|
|
423
|
-
const toolName = String(meta.toolName ?? 'tool')
|
|
424
|
-
const headline =
|
|
425
|
-
typeof meta.title === 'string' && meta.title.length > 0
|
|
426
|
-
? meta.title
|
|
427
|
-
: toolName
|
|
428
|
-
const rawFields = Array.isArray(meta.fields)
|
|
429
|
-
? (meta.fields as Array<{ label?: string; value?: string }>)
|
|
430
|
-
: []
|
|
431
|
-
const fields: Array<{ label: string; value: string }> = []
|
|
432
|
-
for (const f of rawFields) {
|
|
433
|
-
if (!f || !f.label || !f.value) continue
|
|
434
|
-
fields.push({ label: f.label, value: f.value })
|
|
435
|
-
}
|
|
436
|
-
yield {
|
|
437
|
-
type: 'approval_request',
|
|
438
|
-
data: {
|
|
439
|
-
command: headline,
|
|
440
|
-
fields,
|
|
441
|
-
requestId: proposalId,
|
|
442
|
-
approvalType: toolName,
|
|
443
|
-
},
|
|
444
|
-
status: 'pending',
|
|
445
|
-
} as any
|
|
446
|
-
} else if (meta.kind === 'text-leading' && typeof meta.text === 'string') {
|
|
447
|
-
// Model's preamble text (the prose written BEFORE the tool_use
|
|
448
|
-
// block) — emitted as a standalone text segment.
|
|
449
|
-
yield { type: 'text', text: meta.text }
|
|
450
|
-
} else if (meta.kind === 'tool_error') {
|
|
451
|
-
const msg =
|
|
452
|
-
typeof meta.message === 'string' && meta.message.length > 0
|
|
453
|
-
? meta.message
|
|
454
|
-
: 'Could not complete the requested action right now.'
|
|
455
|
-
yield {
|
|
456
|
-
type: 'text',
|
|
457
|
-
text: `⚠️ ${msg}`,
|
|
458
|
-
} as any
|
|
459
|
-
} else if (meta.kind === 'routing') {
|
|
460
|
-
if (typeof meta.routedComplexity === 'string') {
|
|
461
|
-
mergeTurnMeta(metaMapRef, sendIdx, {
|
|
462
|
-
routedComplexity: meta.routedComplexity,
|
|
463
|
-
routedThinkingBudget:
|
|
464
|
-
typeof meta.routedThinkingBudget === 'number'
|
|
465
|
-
? meta.routedThinkingBudget
|
|
466
|
-
: null,
|
|
467
|
-
})
|
|
468
|
-
bumpMetaTick()
|
|
469
|
-
}
|
|
470
|
-
} else {
|
|
471
|
-
if (meta.sources) {
|
|
472
|
-
sourcesMapRef.current.set(sendIdx, meta.sources)
|
|
473
|
-
}
|
|
474
|
-
// Per-row refs for inline object cards.
|
|
475
|
-
if (meta.refs && typeof meta.refs === 'object') {
|
|
476
|
-
refsMapRef.current.set(sendIdx, meta.refs as Record<string, ChatRef>)
|
|
477
|
-
bumpMetaTick()
|
|
478
|
-
}
|
|
479
|
-
if (
|
|
480
|
-
meta.modelLabel ||
|
|
481
|
-
meta.contextWindowMaxTokens ||
|
|
482
|
-
meta.provider ||
|
|
483
|
-
meta.model
|
|
484
|
-
) {
|
|
485
|
-
mergeTurnMeta(metaMapRef, sendIdx, {
|
|
486
|
-
provider: meta.provider ?? null,
|
|
487
|
-
modelLabel: meta.modelLabel ?? null,
|
|
488
|
-
contextWindowMaxTokens: meta.contextWindowMaxTokens ?? null,
|
|
489
|
-
})
|
|
490
|
-
bumpMetaTick()
|
|
491
|
-
}
|
|
492
|
-
const parsedAnchor = parseScrollAnchor(meta.scrollAnchor)
|
|
493
|
-
if (parsedAnchor !== null) {
|
|
494
|
-
mergeTurnMeta(metaMapRef, sendIdx, { scrollAnchor: parsedAnchor })
|
|
495
|
-
bumpMetaTick()
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
buffer = remaining
|
|
499
|
-
}
|
|
500
|
-
} else if (inTrailer) {
|
|
501
|
-
trailerBuffer += chunk
|
|
502
|
-
} else {
|
|
503
|
-
setStreamingPhase('streaming')
|
|
504
|
-
// Trailing usage frame uses \x1F (Unit Separator) as a sentinel.
|
|
505
|
-
const sepIdx = chunk.indexOf('\x1F')
|
|
506
|
-
if (sepIdx === -1) {
|
|
507
|
-
yield { type: 'text', text: chunk }
|
|
508
|
-
} else {
|
|
509
|
-
const before = chunk.slice(0, sepIdx)
|
|
510
|
-
const after = chunk.slice(sepIdx + 1)
|
|
511
|
-
if (before) yield { type: 'text', text: before }
|
|
512
|
-
inTrailer = true
|
|
513
|
-
trailerBuffer = after
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
// Stream ended. Flush any pending thinking text that didn't make it
|
|
518
|
-
// out before the throttle window expired.
|
|
519
|
-
if (pendingThinkingYield) {
|
|
520
|
-
pendingThinkingYield = false
|
|
521
|
-
yield { type: 'thinking', text: escapeThinkingTags(thinkingAccum) }
|
|
522
|
-
}
|
|
523
|
-
// Parse trailer if present.
|
|
524
|
-
if (trailerBuffer.length > 0) {
|
|
525
|
-
try {
|
|
526
|
-
const meta = JSON.parse(trailerBuffer)
|
|
527
|
-
if (
|
|
528
|
-
meta.kind === 'usage' &&
|
|
529
|
-
(meta.stage === 'end' || meta.stage === 'display')
|
|
530
|
-
) {
|
|
531
|
-
const breakdown =
|
|
532
|
-
meta.breakdown && typeof meta.breakdown === 'object'
|
|
533
|
-
? {
|
|
534
|
-
haikuRewriter:
|
|
535
|
-
meta.breakdown.haikuRewriter &&
|
|
536
|
-
typeof meta.breakdown.haikuRewriter.input === 'number'
|
|
537
|
-
? meta.breakdown.haikuRewriter
|
|
538
|
-
: undefined,
|
|
539
|
-
haikuClassifier:
|
|
540
|
-
meta.breakdown.haikuClassifier &&
|
|
541
|
-
typeof meta.breakdown.haikuClassifier.input === 'number'
|
|
542
|
-
? meta.breakdown.haikuClassifier
|
|
543
|
-
: undefined,
|
|
544
|
-
haikuSummarizer:
|
|
545
|
-
meta.breakdown.haikuSummarizer &&
|
|
546
|
-
typeof meta.breakdown.haikuSummarizer.input === 'number'
|
|
547
|
-
? meta.breakdown.haikuSummarizer
|
|
548
|
-
: undefined,
|
|
549
|
-
routedAnswer:
|
|
550
|
-
meta.breakdown.routedAnswer &&
|
|
551
|
-
typeof meta.breakdown.routedAnswer.model === 'string'
|
|
552
|
-
? meta.breakdown.routedAnswer
|
|
553
|
-
: undefined,
|
|
554
|
-
}
|
|
555
|
-
: null
|
|
556
|
-
mergeTurnMeta(metaMapRef, sendIdx, {
|
|
557
|
-
inputTokens: meta.input_tokens ?? null,
|
|
558
|
-
outputTokens: meta.output_tokens ?? null,
|
|
559
|
-
cacheHitRatePct:
|
|
560
|
-
typeof meta.hit_rate_pct === 'number' ? meta.hit_rate_pct : null,
|
|
561
|
-
...(breakdown ? { breakdown } : {}),
|
|
562
|
-
})
|
|
563
|
-
bumpMetaTick()
|
|
564
|
-
}
|
|
565
|
-
} catch {
|
|
566
|
-
// Malformed trailer — silently ignore.
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Merge partial fields into the per-turn meta map. Preserves existing
|
|
574
|
-
* non-null values so a leading frame's data isn't wiped by a later frame
|
|
575
|
-
* that didn't include it.
|
|
576
|
-
*/
|
|
577
|
-
function mergeTurnMeta(
|
|
578
|
-
ref: MutableRefObject<Map<number, ChatTurnMeta>>,
|
|
579
|
-
sendIdx: number,
|
|
580
|
-
partial: Partial<ChatTurnMeta>,
|
|
581
|
-
): void {
|
|
582
|
-
const prev = ref.current.get(sendIdx) ?? createEmptyTurnMeta()
|
|
583
|
-
const filtered = Object.fromEntries(
|
|
584
|
-
Object.entries(partial).filter(([, v]) => v !== undefined && v !== null),
|
|
585
|
-
) as Partial<ChatTurnMeta>
|
|
586
|
-
ref.current.set(sendIdx, { ...prev, ...filtered })
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// =============================================================================
|
|
590
|
-
// localStorage persistence
|
|
159
|
+
// localStorage persistence — PersistedChatState v1
|
|
591
160
|
// =============================================================================
|
|
592
161
|
|
|
593
162
|
const CHAT_STORAGE_VERSION = 1
|
|
@@ -676,6 +245,39 @@ function savePersistedChat(source: DocSource, state: PersistedChatState) {
|
|
|
676
245
|
}
|
|
677
246
|
}
|
|
678
247
|
|
|
248
|
+
/** v1 wire `Message` → reducer message. The v1 blob stores assistant
|
|
249
|
+
* segments inside `content`; the reducer keeps `content` a string and
|
|
250
|
+
* segments separate. */
|
|
251
|
+
function persistedToReducerMessage(m: Message): UnifiedChatMessage {
|
|
252
|
+
const segments = Array.isArray(m.content) ? (m.content as MessageSegment[]) : undefined
|
|
253
|
+
return {
|
|
254
|
+
id: m.id,
|
|
255
|
+
role: m.role,
|
|
256
|
+
content: typeof m.content === 'string' ? m.content : '',
|
|
257
|
+
...(segments ? { segments } : {}),
|
|
258
|
+
...(m.name !== undefined ? { name: m.name } : {}),
|
|
259
|
+
...(m.avatar != null ? { avatar: m.avatar } : {}),
|
|
260
|
+
...(m.timestamp !== undefined ? { timestamp: m.timestamp } : {}),
|
|
261
|
+
...(m.hidden ? { hidden: true } : {}),
|
|
262
|
+
...(m.chatRefs ? { chatRefs: m.chatRefs } : {}),
|
|
263
|
+
} as UnifiedChatMessage
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Reducer message → v1 wire `Message`. Exact inverse of the loader so the
|
|
267
|
+
* persist round-trip re-saves an equivalent v1 blob (the golden contract). */
|
|
268
|
+
function reducerToPersistedMessage(m: UnifiedChatMessage): Message {
|
|
269
|
+
return {
|
|
270
|
+
id: m.id,
|
|
271
|
+
role: m.role,
|
|
272
|
+
...(m.name !== undefined ? { name: m.name } : {}),
|
|
273
|
+
content: (m.segments ?? m.content) as Message['content'],
|
|
274
|
+
...(m.timestamp !== undefined ? { timestamp: m.timestamp } : {}),
|
|
275
|
+
...(m.avatar != null ? { avatar: m.avatar } : {}),
|
|
276
|
+
...(m.hidden ? { hidden: true } : {}),
|
|
277
|
+
...(m.chatRefs ? { chatRefs: m.chatRefs } : {}),
|
|
278
|
+
} as Message
|
|
279
|
+
}
|
|
280
|
+
|
|
679
281
|
// =============================================================================
|
|
680
282
|
// useSseChatAdapter — public hook
|
|
681
283
|
// =============================================================================
|
|
@@ -697,70 +299,71 @@ export function useSseChatAdapter(
|
|
|
697
299
|
// Chat-specific code REQUIRES a runtime — the lib's `<HubRuntimeProvider>`
|
|
698
300
|
// (hub) / embedder's provider must wrap the tree.
|
|
699
301
|
const runtime = useRequiredChatRuntime()
|
|
700
|
-
// `source` is OPTIONAL — embedders are platform-agnostic (see ChatRuntime.source).
|
|
701
|
-
// Here it's used ONLY for the localStorage history namespace + client-side meta
|
|
702
|
-
// labels; it is NEVER sent on the wire (the hub resolves source server-side via
|
|
703
|
-
// currentPlatform()). Fall back to a stable constant so the persistence key stays
|
|
704
|
-
// well-formed when the embedder leaves source unset.
|
|
705
302
|
const source = runtime.source || DEFAULT_CHAT_SOURCE
|
|
706
|
-
// Fall back to the lib-baked hub-canonical map when the embedder
|
|
707
|
-
// didn't supply an override. Keeps Ask + Display working in any
|
|
708
|
-
// mount that doesn't have a custom documentType registry. Embedders
|
|
709
|
-
// with polymorphic / per-tenant types pass their own callback and it
|
|
710
|
-
// wins.
|
|
711
303
|
const tableIdForDocumentType =
|
|
712
304
|
options?.tableIdForDocumentType ?? defaultTableIdForDocumentType
|
|
713
305
|
|
|
714
|
-
//
|
|
306
|
+
// ─── Reducer wiring ────────────────────────────────────────────────────────
|
|
307
|
+
// One store per hook instance; one reducer keyed by the source. Approval
|
|
308
|
+
// card buttons fire a server-driven confirm-tool turn via `sendMessage`
|
|
309
|
+
// (hidden approval-action send) — routed through a ref so the reducer's
|
|
310
|
+
// creation-time callbacks stay stable.
|
|
311
|
+
|
|
312
|
+
const sendMessageRef = useRef<
|
|
313
|
+
(text: string, options?: InternalSendMessageOptions) => Promise<void>
|
|
314
|
+
>(async () => {})
|
|
315
|
+
|
|
316
|
+
const cardApprove = useCallback((reqId?: string): void | Promise<void> => {
|
|
317
|
+
if (!reqId) return
|
|
318
|
+
return sendMessageRef.current('', {
|
|
319
|
+
hidden: true,
|
|
320
|
+
approvalAction: { proposalId: reqId, action: 'approve' },
|
|
321
|
+
})
|
|
322
|
+
}, [])
|
|
323
|
+
const cardReject = useCallback((reqId?: string): void | Promise<void> => {
|
|
324
|
+
if (!reqId) return
|
|
325
|
+
return sendMessageRef.current('', {
|
|
326
|
+
hidden: true,
|
|
327
|
+
approvalAction: { proposalId: reqId, action: 'reject' },
|
|
328
|
+
})
|
|
329
|
+
}, [])
|
|
330
|
+
|
|
331
|
+
const createReducerOptions = useCallback(
|
|
332
|
+
(): ChatStreamReducerOptions => ({
|
|
333
|
+
transport: 'sse',
|
|
334
|
+
callbacks: { onApprove: cardApprove, onReject: cardReject },
|
|
335
|
+
}),
|
|
336
|
+
[cardApprove, cardReject],
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
const storeRef = useRef<ChatDialogStore | null>(null)
|
|
340
|
+
if (storeRef.current === null) storeRef.current = createChatDialogStore()
|
|
341
|
+
|
|
342
|
+
// Restore persisted state once on mount — the v1 blob seeds the reducer's
|
|
343
|
+
// thread + its sendIdx-keyed sources/refs maps + the send counter.
|
|
715
344
|
const persistedRef = useRef<PersistedChatState | null>(null)
|
|
716
345
|
if (persistedRef.current === null) {
|
|
717
346
|
pruneStaleChatStorage(source)
|
|
718
347
|
persistedRef.current =
|
|
719
348
|
loadPersistedChat(source) || { messages: [], sources: [], sendCount: 0 }
|
|
349
|
+
const reducer = storeRef.current.getReducer(source, DEFAULT_DIALOG_SIDE, createReducerOptions)
|
|
350
|
+
reducer.initializeWithState(persistedRef.current.messages.map(persistedToReducerMessage))
|
|
351
|
+
reducer.seedSseMaps({
|
|
352
|
+
sources: persistedRef.current.sources as Array<[number, unknown[]]>,
|
|
353
|
+
refs: persistedRef.current.refs,
|
|
354
|
+
sendCount: persistedRef.current.sendCount,
|
|
355
|
+
})
|
|
720
356
|
}
|
|
721
357
|
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
)
|
|
728
|
-
const metaMapRef = useRef<Map<number, ChatTurnMeta>>(new Map())
|
|
729
|
-
const messagesRef = useRef<Message[]>(persistedRef.current.messages)
|
|
730
|
-
const sendCountRef = useRef(persistedRef.current.sendCount)
|
|
731
|
-
const [streamingPhase, setStreamingPhase] = useState<StreamingPhase>('idle')
|
|
732
|
-
const [metaTick, setMetaTick] = useState(0)
|
|
733
|
-
const bumpMetaTick = useCallback(() => setMetaTick((t) => t + 1), [])
|
|
358
|
+
const { state, applyEvent, flushDeltas, mutate, reducer } = useChatStreamReducer({
|
|
359
|
+
store: storeRef.current,
|
|
360
|
+
dialogId: source,
|
|
361
|
+
createReducerOptions,
|
|
362
|
+
})
|
|
734
363
|
|
|
735
|
-
const
|
|
736
|
-
() =>
|
|
737
|
-
createDocStreamFn(
|
|
738
|
-
source,
|
|
739
|
-
{
|
|
740
|
-
chatStreamUrl: runtime.endpoints.chatStreamUrl,
|
|
741
|
-
approvalToolUrl: runtime.endpoints.approvalToolUrl,
|
|
742
|
-
},
|
|
743
|
-
messagesRef,
|
|
744
|
-
sourcesMapRef,
|
|
745
|
-
refsMapRef,
|
|
746
|
-
metaMapRef,
|
|
747
|
-
setStreamingPhase,
|
|
748
|
-
bumpMetaTick,
|
|
749
|
-
sendCountRef,
|
|
750
|
-
),
|
|
751
|
-
[
|
|
752
|
-
source,
|
|
753
|
-
runtime.endpoints.chatStreamUrl,
|
|
754
|
-
runtime.endpoints.approvalToolUrl,
|
|
755
|
-
bumpMetaTick,
|
|
756
|
-
],
|
|
757
|
-
)
|
|
364
|
+
const abortControllerRef = useRef<AbortController | null>(null)
|
|
758
365
|
|
|
759
|
-
//
|
|
760
|
-
// slash-command registry. Used by `displayRef` to translate an
|
|
761
|
-
// inline-card's Display click into a `/<cmd> display "<value>"`
|
|
762
|
-
// invocation, picking the canonical command for the row's RAG table.
|
|
763
|
-
//
|
|
366
|
+
// ─── Slash-command registry (displayRef lookup) ───────────────────────────
|
|
764
367
|
// Reads from the SAME react-query cache entry as `<EmbeddableChat>`'s
|
|
765
368
|
// onboarding-card list (keyed on `commandsUrl`), so Guide mode fetches
|
|
766
369
|
// `commands` ONCE. Gated on `active` so a Mingo-only panel — where this
|
|
@@ -792,138 +395,221 @@ export function useSseChatAdapter(
|
|
|
792
395
|
return map
|
|
793
396
|
}, [slashCommands])
|
|
794
397
|
|
|
795
|
-
//
|
|
796
|
-
//
|
|
398
|
+
// ─── Persistence — save on every messages change ──────────────────────────
|
|
399
|
+
// Sources/refs/sendCount live in the reducer's maps; read at write time.
|
|
797
400
|
const persist = useCallback(
|
|
798
|
-
(nextMessages:
|
|
401
|
+
(nextMessages: UnifiedChatMessage[]) => {
|
|
402
|
+
const turnMeta = reducer.state.turnMeta
|
|
799
403
|
savePersistedChat(source, {
|
|
800
|
-
messages: nextMessages,
|
|
801
|
-
sources: Array.from(
|
|
802
|
-
refs: Array.from(
|
|
803
|
-
sendCount:
|
|
404
|
+
messages: nextMessages.map(reducerToPersistedMessage),
|
|
405
|
+
sources: Array.from(turnMeta.sources.entries()) as Array<[number, ChatSource[]]>,
|
|
406
|
+
refs: Array.from(turnMeta.refs.entries()),
|
|
407
|
+
sendCount: turnMeta.sendCount,
|
|
804
408
|
})
|
|
805
409
|
},
|
|
806
|
-
[source],
|
|
410
|
+
[source, reducer],
|
|
807
411
|
)
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
persist(state.messages)
|
|
414
|
+
}, [state.messages, persist])
|
|
808
415
|
|
|
809
|
-
|
|
810
|
-
messages,
|
|
811
|
-
isTyping,
|
|
812
|
-
isStreaming,
|
|
813
|
-
sendMessage: chatSendMessage,
|
|
814
|
-
stopMessage: chatStopMessage,
|
|
815
|
-
clearMessages: chatClearMessages,
|
|
816
|
-
hasMessages,
|
|
817
|
-
} = useChat({
|
|
818
|
-
useMock: false,
|
|
819
|
-
assistantName: 'Mingo AI',
|
|
820
|
-
streamFn,
|
|
821
|
-
initialMessages: persistedRef.current.messages,
|
|
822
|
-
onMessagesChange: persist,
|
|
823
|
-
})
|
|
824
|
-
|
|
825
|
-
messagesRef.current = messages
|
|
826
|
-
|
|
827
|
-
// Index sources/refs/scrollAnchor by USER-SEND count (`sendIdx`), not
|
|
828
|
-
// by assistant-message count. Each user send produces exactly ONE
|
|
829
|
-
// refsMapRef entry server-side, but it can produce MULTIPLE assistant
|
|
830
|
-
// messages on the client (main RAG reply + post-approve card +
|
|
831
|
-
// auto-continuation prose). Counting USER sends and mapping every
|
|
832
|
-
// following assistant message to that index keeps the lookup stable.
|
|
833
|
-
let sendIdx = -1
|
|
834
|
-
const docMessages: DocChatMessage[] = messages.map((m) => {
|
|
835
|
-
const segments = Array.isArray(m.content) ? (m.content as MessageSegment[]) : undefined
|
|
836
|
-
const content =
|
|
837
|
-
typeof m.content === 'string'
|
|
838
|
-
? m.content
|
|
839
|
-
: Array.isArray(m.content)
|
|
840
|
-
? m.content
|
|
841
|
-
.filter((s) => s.type === 'text')
|
|
842
|
-
.map((s) => (s as { type: 'text'; text: string }).text)
|
|
843
|
-
.join('')
|
|
844
|
-
: ''
|
|
845
|
-
|
|
846
|
-
let sources: ChatSource[] | undefined
|
|
847
|
-
let chatRefs: Record<string, ChatRef> | undefined
|
|
848
|
-
let scrollAnchor: ScrollAnchor | undefined
|
|
849
|
-
if (m.role === 'user' && !m.hidden) {
|
|
850
|
-
sendIdx++
|
|
851
|
-
}
|
|
852
|
-
if (m.role === 'assistant') {
|
|
853
|
-
const lookupIdx = sendIdx >= 0 ? sendIdx : 0
|
|
854
|
-
sources = sourcesMapRef.current.get(lookupIdx)
|
|
855
|
-
// The receipt path stamps `chatRefs` directly onto the assistant
|
|
856
|
-
// message in useChat. Prefer that message-bound copy when present;
|
|
857
|
-
// falls back to per-turn refs map.
|
|
858
|
-
chatRefs = m.chatRefs ?? refsMapRef.current.get(lookupIdx)
|
|
859
|
-
scrollAnchor =
|
|
860
|
-
(metaMapRef.current.get(lookupIdx)?.scrollAnchor as ScrollAnchor | null) ??
|
|
861
|
-
undefined
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
return {
|
|
865
|
-
id: m.id,
|
|
866
|
-
role: m.role as 'user' | 'assistant',
|
|
867
|
-
content,
|
|
868
|
-
...(segments ? { segments } : {}),
|
|
869
|
-
...(sources ? { sources } : {}),
|
|
870
|
-
...(chatRefs ? { chatRefs } : {}),
|
|
871
|
-
...(scrollAnchor ? { scrollAnchor } : {}),
|
|
872
|
-
...(m.hidden ? { hidden: true } : {}),
|
|
873
|
-
}
|
|
874
|
-
})
|
|
416
|
+
// ─── Send / stream loop ────────────────────────────────────────────────────
|
|
875
417
|
|
|
876
418
|
/**
|
|
877
419
|
* Internal sendMessage options — union of the public
|
|
878
420
|
* `UnifiedSendMessageOptions` (semantic fields: `hidden`, `attachments`)
|
|
879
421
|
* and SSE-only internal extras (`commandOverride`, `approvalAction`)
|
|
880
|
-
* set by `discussRef` / `displayRef` /
|
|
881
|
-
*
|
|
882
|
-
* The public `UnifiedChatState.sendMessage` accepts only the narrow
|
|
883
|
-
* unified shape; this hook accepts the wider one because internal
|
|
884
|
-
* callers need it. Function-param contravariance makes this assignable.
|
|
422
|
+
* set by `discussRef` / `displayRef` / the approval-card callbacks.
|
|
885
423
|
*/
|
|
886
|
-
type InternalSendMessageOptions = UnifiedSendMessageOptions &
|
|
887
|
-
|
|
424
|
+
type InternalSendMessageOptions = UnifiedSendMessageOptions & {
|
|
425
|
+
commandOverride?: WireCommandOverride
|
|
426
|
+
approvalAction?: { proposalId: string; action: 'approve' | 'reject' }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const endpointsRef = useRef(runtime.endpoints)
|
|
430
|
+
endpointsRef.current = runtime.endpoints
|
|
888
431
|
|
|
889
432
|
const sendMessage = useCallback(
|
|
890
|
-
async (
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
433
|
+
async (text: string, sendOptions?: InternalSendMessageOptions): Promise<void> => {
|
|
434
|
+
const { hidden, attachments, commandOverride, approvalAction } = sendOptions ?? {}
|
|
435
|
+
|
|
436
|
+
// Conversation history for the server: everything BEFORE this send,
|
|
437
|
+
// `hidden:true` rows included (LLM context) but their flag filtered
|
|
438
|
+
// shape-wise like legacy — hidden messages are excluded entirely.
|
|
439
|
+
const history = reducer.state.messages
|
|
440
|
+
.filter((m) => (m.role === 'user' || m.role === 'assistant') && !m.hidden)
|
|
441
|
+
.map((m) => ({
|
|
442
|
+
role: m.role,
|
|
443
|
+
content: m.segments ? flattenAssistantContent(m.segments) : (m.content as string),
|
|
444
|
+
}))
|
|
445
|
+
|
|
446
|
+
// URL + body branch — approvalAction routes to the approval-tool
|
|
447
|
+
// endpoint, the standard chat path routes to the chat-stream endpoint.
|
|
448
|
+
// `source` is INTENTIONALLY NOT in the body: the chat route resolves
|
|
449
|
+
// it server-side via its own platform-detection — tamper-proof binding.
|
|
450
|
+
const targetPath = approvalAction
|
|
451
|
+
? endpointsRef.current.approvalToolUrl
|
|
452
|
+
: endpointsRef.current.chatStreamUrl
|
|
453
|
+
const requestBody = approvalAction
|
|
454
|
+
? {
|
|
455
|
+
proposal_id: approvalAction.proposalId,
|
|
456
|
+
action: approvalAction.action,
|
|
457
|
+
messages: history,
|
|
458
|
+
}
|
|
459
|
+
: {
|
|
460
|
+
messages: [...history, { role: 'user', content: text }],
|
|
461
|
+
...(commandOverride ? { commandOverride } : {}),
|
|
462
|
+
...(attachments && attachments.length > 0
|
|
463
|
+
? { pendingAttachments: attachments as ChatAttachment[] }
|
|
464
|
+
: {}),
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Optimistic user bubble + assistant placeholder + phase 'thinking'
|
|
468
|
+
// + sendCount++ — one reducer command.
|
|
469
|
+
mutate((r) =>
|
|
470
|
+
r.beginSseSend({ text, hidden, userName: 'You', assistantName: 'Mingo AI' }),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
const ctrl = new AbortController()
|
|
474
|
+
abortControllerRef.current = ctrl
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
// `chatAuthedFetch` carries the bearer-act-as headers (+ Supabase
|
|
478
|
+
// session cookies) — same wrapper `use-chat-attachments` and
|
|
479
|
+
// `use-chat-identity` use.
|
|
480
|
+
const response = await chatAuthedFetch(targetPath, {
|
|
481
|
+
method: 'POST',
|
|
482
|
+
body: JSON.stringify(requestBody),
|
|
483
|
+
signal: ctrl.signal,
|
|
484
|
+
})
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
throw new Error(`Chat request failed: ${response.status}`)
|
|
487
|
+
}
|
|
488
|
+
const reader = response.body?.getReader()
|
|
489
|
+
if (!reader) throw new Error('No response body')
|
|
490
|
+
|
|
491
|
+
// Decoder is per-send (a rapid send-stop-send sequence must never
|
|
492
|
+
// feed the second stream's first chunk into the first stream's
|
|
493
|
+
// tail buffer).
|
|
494
|
+
const frameDecoder = createSseFrameDecoder()
|
|
495
|
+
let finished = false
|
|
496
|
+
try {
|
|
497
|
+
while (true) {
|
|
498
|
+
const { done, value } = await reader.read()
|
|
499
|
+
if (done) break
|
|
500
|
+
if (ctrl.signal.aborted) break
|
|
501
|
+
for (const event of frameDecoder.push(value)) {
|
|
502
|
+
applyEvent(event)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
finished = true
|
|
506
|
+
} finally {
|
|
507
|
+
// `end()` always runs so the decoder settles; its events (the
|
|
508
|
+
// trailing usage frame) apply only on a CLEAN end — aborts
|
|
509
|
+
// propagate without extra state (legacy parity).
|
|
510
|
+
const endEvents = frameDecoder.end()
|
|
511
|
+
if (finished) {
|
|
512
|
+
for (const event of endEvents) applyEvent(event)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch (err) {
|
|
516
|
+
// AbortError on user-initiated stop is expected — keep the partial
|
|
517
|
+
// message, no error row.
|
|
518
|
+
if ((err as { name?: string })?.name !== 'AbortError' && !ctrl.signal.aborted) {
|
|
519
|
+
flushDeltas()
|
|
520
|
+
mutate((r) =>
|
|
521
|
+
r.failSseTurn(
|
|
522
|
+
err instanceof Error
|
|
523
|
+
? err.message
|
|
524
|
+
: 'An error occurred while processing your request.',
|
|
525
|
+
),
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
} finally {
|
|
529
|
+
if (abortControllerRef.current === ctrl) {
|
|
530
|
+
abortControllerRef.current = null
|
|
531
|
+
}
|
|
532
|
+
// Force-flush pending deltas BEFORE the completion state lands, then
|
|
533
|
+
// settle the turn (drops an empty trailing placeholder — the reject
|
|
534
|
+
// path streams no text — and returns the phase to idle).
|
|
535
|
+
flushDeltas()
|
|
536
|
+
mutate((r) => r.endSseTurn())
|
|
906
537
|
}
|
|
907
|
-
sendCountRef.current++
|
|
908
|
-
setStreamingPhase('thinking')
|
|
909
|
-
await chatSendMessage(text, sseExtras, hidden ? { hidden } : undefined)
|
|
910
538
|
},
|
|
911
|
-
[
|
|
539
|
+
[reducer, mutate, applyEvent, flushDeltas],
|
|
912
540
|
)
|
|
541
|
+
sendMessageRef.current = sendMessage
|
|
542
|
+
|
|
543
|
+
const stopMessage = useCallback(() => {
|
|
544
|
+
abortControllerRef.current?.abort()
|
|
545
|
+
flushDeltas()
|
|
546
|
+
mutate((r) => r.setPhase('idle'))
|
|
547
|
+
}, [mutate, flushDeltas])
|
|
548
|
+
|
|
549
|
+
const clearMessages = useCallback(() => {
|
|
550
|
+
mutate((r) => r.reset())
|
|
551
|
+
// Clear persisted state too so the next mount starts fresh.
|
|
552
|
+
if (typeof window !== 'undefined') {
|
|
553
|
+
try {
|
|
554
|
+
window.localStorage.removeItem(chatStorageKey(source))
|
|
555
|
+
} catch {}
|
|
556
|
+
}
|
|
557
|
+
}, [mutate, source])
|
|
558
|
+
|
|
559
|
+
// ─── Public message mapping (sendIdx fan-out lookup) ──────────────────────
|
|
560
|
+
// Index sources/refs/scrollAnchor by USER-SEND count (`sendIdx`), not by
|
|
561
|
+
// assistant-message count. Each user send produces exactly ONE refs entry
|
|
562
|
+
// server-side, but it can produce MULTIPLE assistant messages on the
|
|
563
|
+
// client (main RAG reply + post-approve card + auto-continuation prose).
|
|
564
|
+
// Counting VISIBLE user sends and mapping every following assistant
|
|
565
|
+
// message to that index keeps the lookup stable — this is the
|
|
566
|
+
// PersistedChatState v1 rehydration contract.
|
|
567
|
+
const docMessages: DocChatMessage[] = useMemo(() => {
|
|
568
|
+
const { meta, sources: sourcesMap, refs: refsMap } = state.turnMeta
|
|
569
|
+
let sendIdx = -1
|
|
570
|
+
return state.messages.map((m) => {
|
|
571
|
+
const segments = m.segments
|
|
572
|
+
const content =
|
|
573
|
+
typeof m.content === 'string' && !segments
|
|
574
|
+
? m.content
|
|
575
|
+
: segments
|
|
576
|
+
?.filter((s) => s.type === 'text')
|
|
577
|
+
.map((s) => (s as { type: 'text'; text: string }).text)
|
|
578
|
+
.join('') ?? ''
|
|
579
|
+
|
|
580
|
+
let sources: ChatSource[] | undefined
|
|
581
|
+
let chatRefs: Record<string, ChatRef> | undefined
|
|
582
|
+
let scrollAnchor: ScrollAnchor | undefined
|
|
583
|
+
if (m.role === 'user' && !m.hidden) {
|
|
584
|
+
sendIdx++
|
|
585
|
+
}
|
|
586
|
+
if (m.role === 'assistant') {
|
|
587
|
+
const lookupIdx = sendIdx >= 0 ? sendIdx : 0
|
|
588
|
+
sources = sourcesMap.get(lookupIdx) as ChatSource[] | undefined
|
|
589
|
+
// The receipt path stamps `chatRefs` directly onto the assistant
|
|
590
|
+
// message; prefer that message-bound copy when present, fall back
|
|
591
|
+
// to the per-turn refs map.
|
|
592
|
+
chatRefs = m.chatRefs ?? refsMap.get(lookupIdx)
|
|
593
|
+
scrollAnchor = (meta.get(lookupIdx)?.scrollAnchor as ScrollAnchor | null) ?? undefined
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
id: m.id,
|
|
598
|
+
role: m.role as 'user' | 'assistant',
|
|
599
|
+
content,
|
|
600
|
+
...(segments ? { segments } : {}),
|
|
601
|
+
...(sources ? { sources } : {}),
|
|
602
|
+
...(chatRefs ? { chatRefs } : {}),
|
|
603
|
+
...(scrollAnchor ? { scrollAnchor } : {}),
|
|
604
|
+
...(m.hidden ? { hidden: true } : {}),
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
}, [state.messages, state.turnMeta])
|
|
913
608
|
|
|
914
609
|
/**
|
|
915
610
|
* "Display" callback for inline cards whose registry entry sets
|
|
916
611
|
* `displayAction: true`. Parallel to `discussRef` but emits a
|
|
917
|
-
* `/<cmd> display "<title>"` slash command instead of the Discuss
|
|
918
|
-
* prose. Resolution of the slash command id for the ref's documentType
|
|
919
|
-
* happens via the `cmdIdByTableId` map hydrated from the commands
|
|
920
|
-
* endpoint.
|
|
921
|
-
*
|
|
922
|
-
* `tableIdForDocumentType` is always defined here — either the
|
|
923
|
-
* embedder-supplied override OR the lib-baked
|
|
924
|
-
* `defaultTableIdForDocumentType` fallback. Unknown documentTypes
|
|
925
|
-
* still short-circuit gracefully via the `tableId === null` check
|
|
926
|
-
* below.
|
|
612
|
+
* `/<cmd> display "<title>"` slash command instead of the Discuss prose.
|
|
927
613
|
*/
|
|
928
614
|
const displayRef = useCallback(
|
|
929
615
|
(reference: ChatRef) => {
|
|
@@ -965,10 +651,6 @@ export function useSseChatAdapter(
|
|
|
965
651
|
* prompt ("Tell me more about <title>"); the structured
|
|
966
652
|
* `commandOverride.entityIdFilter` is sent server-side via the request
|
|
967
653
|
* body so retrieval narrows to the named row.
|
|
968
|
-
*
|
|
969
|
-
* `tableIdForDocumentType` is always defined here — either the
|
|
970
|
-
* embedder-supplied override OR the lib-baked default. Unknown
|
|
971
|
-
* documentTypes still short-circuit via the `tableId === null` check.
|
|
972
654
|
*/
|
|
973
655
|
const discussRef = useCallback(
|
|
974
656
|
(reference: ChatRef) => {
|
|
@@ -998,47 +680,17 @@ export function useSseChatAdapter(
|
|
|
998
680
|
[sendMessage, tableIdForDocumentType],
|
|
999
681
|
)
|
|
1000
682
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
683
|
+
// ─── Per-turn metadata resolution ─────────────────────────────────────────
|
|
684
|
+
const latestMeta = useMemo(() => {
|
|
685
|
+
const { meta, sendCount } = state.turnMeta
|
|
686
|
+
return meta.get(sendCount - 1) ?? meta.get(sendCount - 2) ?? null
|
|
687
|
+
}, [state])
|
|
1005
688
|
|
|
1006
|
-
const
|
|
1007
|
-
sourcesMapRef.current.clear()
|
|
1008
|
-
refsMapRef.current.clear()
|
|
1009
|
-
metaMapRef.current.clear()
|
|
1010
|
-
sendCountRef.current = 0
|
|
1011
|
-
setStreamingPhase('idle')
|
|
1012
|
-
// Force the latestMeta useMemo to recompute with the cleared map.
|
|
1013
|
-
bumpMetaTick()
|
|
1014
|
-
chatClearMessages()
|
|
1015
|
-
// Clear persisted state too so the next mount starts fresh.
|
|
1016
|
-
if (typeof window !== 'undefined') {
|
|
1017
|
-
try {
|
|
1018
|
-
window.localStorage.removeItem(chatStorageKey(source))
|
|
1019
|
-
} catch {}
|
|
1020
|
-
}
|
|
1021
|
-
}, [chatClearMessages, source, bumpMetaTick])
|
|
1022
|
-
|
|
1023
|
-
// Reset to idle whenever both flags drop off.
|
|
1024
|
-
useEffect(() => {
|
|
1025
|
-
if (!isTyping && !isStreaming && streamingPhase !== 'idle') {
|
|
1026
|
-
setStreamingPhase('idle')
|
|
1027
|
-
}
|
|
1028
|
-
}, [isTyping, isStreaming, streamingPhase])
|
|
1029
|
-
|
|
1030
|
-
// Resolve the active turn's metadata.
|
|
1031
|
-
const latestMeta = useMemo(
|
|
1032
|
-
() =>
|
|
1033
|
-
metaMapRef.current.get(sendCountRef.current - 1) ??
|
|
1034
|
-
metaMapRef.current.get(sendCountRef.current - 2) ??
|
|
1035
|
-
null,
|
|
1036
|
-
[metaTick, streamingPhase],
|
|
1037
|
-
)
|
|
689
|
+
const streamingPhase = state.streamingPhase
|
|
1038
690
|
|
|
1039
691
|
return {
|
|
1040
692
|
messages: docMessages,
|
|
1041
|
-
isLoading:
|
|
693
|
+
isLoading: streamingPhase !== 'idle',
|
|
1042
694
|
sendMessage,
|
|
1043
695
|
/** "Discuss" affordance for ObjectCard. */
|
|
1044
696
|
discussRef,
|