@flamingo-stack/openframe-frontend-core 0.0.488 → 0.0.489-snapshot.20260728173516
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-GQX6LFFA.cjs → chunk-2MKUHJ3O.cjs} +42 -42
- package/dist/{chunk-GQX6LFFA.cjs.map → chunk-2MKUHJ3O.cjs.map} +1 -1
- package/dist/{chunk-DX6AOHNU.cjs → chunk-3IEVA5AO.cjs} +77 -77
- package/dist/{chunk-DX6AOHNU.cjs.map → chunk-3IEVA5AO.cjs.map} +1 -1
- package/dist/{chunk-PNXNWGBY.js → chunk-6CFFISUC.js} +2 -2
- package/dist/{chunk-F6BYUO5U.js → chunk-6CJ2K7BD.js} +3 -3
- package/dist/{chunk-KFQXO7QB.js → chunk-6GT557KN.js} +1 -1
- package/dist/chunk-6GT557KN.js.map +1 -0
- package/dist/{chunk-AJNDDXKC.js → chunk-6QK3ONDD.js} +3 -3
- package/dist/{chunk-H7JX2BG7.cjs → chunk-7I42V2OD.cjs} +75 -75
- package/dist/{chunk-H7JX2BG7.cjs.map → chunk-7I42V2OD.cjs.map} +1 -1
- package/dist/{chunk-AHDJMEEN.js → chunk-AIRNG27G.js} +7 -7
- package/dist/{chunk-RZSA6MZL.cjs → chunk-AW63ET2A.cjs} +15 -15
- package/dist/{chunk-RZSA6MZL.cjs.map → chunk-AW63ET2A.cjs.map} +1 -1
- package/dist/{chunk-A7IPON2V.cjs → chunk-BIKOJ4FH.cjs} +5 -5
- package/dist/{chunk-A7IPON2V.cjs.map → chunk-BIKOJ4FH.cjs.map} +1 -1
- package/dist/{chunk-S52LJNMU.js → chunk-BZMOEWWS.js} +7 -7
- package/dist/{chunk-SIKCXKGB.cjs → chunk-D5LQMRPU.cjs} +7 -7
- package/dist/{chunk-SIKCXKGB.cjs.map → chunk-D5LQMRPU.cjs.map} +1 -1
- package/dist/{chunk-HQYJINFV.js → chunk-EGXCWZYP.js} +4 -4
- package/dist/{chunk-EUYIFALO.cjs → chunk-EJQUYZXF.cjs} +14 -14
- package/dist/{chunk-EUYIFALO.cjs.map → chunk-EJQUYZXF.cjs.map} +1 -1
- package/dist/{chunk-V3CNNMY3.cjs → chunk-FY5XAW3H.cjs} +18 -18
- package/dist/{chunk-V3CNNMY3.cjs.map → chunk-FY5XAW3H.cjs.map} +1 -1
- package/dist/{chunk-FOQV6RCO.cjs → chunk-GMPYR3MX.cjs} +29 -29
- package/dist/{chunk-FOQV6RCO.cjs.map → chunk-GMPYR3MX.cjs.map} +1 -1
- package/dist/{chunk-EYEIVYYF.cjs → chunk-GUTYUMGV.cjs} +7 -7
- package/dist/{chunk-EYEIVYYF.cjs.map → chunk-GUTYUMGV.cjs.map} +1 -1
- package/dist/{chunk-FZOBQG6M.cjs → chunk-IEZ3YBAP.cjs} +35 -35
- package/dist/{chunk-FZOBQG6M.cjs.map → chunk-IEZ3YBAP.cjs.map} +1 -1
- package/dist/{chunk-PECGZWQR.cjs → chunk-KP5KRRD6.cjs} +3 -3
- package/dist/{chunk-PECGZWQR.cjs.map → chunk-KP5KRRD6.cjs.map} +1 -1
- package/dist/{chunk-2B6773QM.cjs → chunk-LILQJ3UZ.cjs} +1 -1
- package/dist/chunk-LILQJ3UZ.cjs.map +1 -0
- package/dist/{chunk-SCCTJ2UI.js → chunk-MPYDX5WX.js} +4 -4
- package/dist/{chunk-YJSKQ2ZV.js → chunk-NGEFYPY7.js} +3 -3
- package/dist/{chunk-77DFPD35.js → chunk-OQIMH2WQ.js} +5515 -5377
- package/dist/chunk-OQIMH2WQ.js.map +1 -0
- package/dist/{chunk-KLYHZRAD.js → chunk-PB3BTZDU.js} +2 -2
- package/dist/{chunk-CWQACI4Z.js → chunk-RW2LPL72.js} +2 -2
- package/dist/{chunk-YEEHB4T6.cjs → chunk-UZZT65PQ.cjs} +40 -40
- package/dist/{chunk-YEEHB4T6.cjs.map → chunk-UZZT65PQ.cjs.map} +1 -1
- package/dist/{chunk-LLWBPPZ4.js → chunk-VDZO6IDR.js} +6 -6
- package/dist/{chunk-3BFX6HJM.js → chunk-VL2YMDPH.js} +6 -6
- package/dist/{chunk-5RXKOGH6.cjs → chunk-VUXNXWOQ.cjs} +496 -358
- package/dist/chunk-VUXNXWOQ.cjs.map +1 -0
- package/dist/{chunk-PXF5J24T.js → chunk-WLM3F3WU.js} +2 -2
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/guide-display.d.ts +14 -0
- package/dist/components/chat/guide-display.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-chat-history-hydration.d.ts +47 -0
- package/dist/components/chat/hooks/use-chat-history-hydration.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-chat.d.ts +1 -0
- package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +4 -0
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +6 -4
- 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 +5 -3
- package/dist/components/chat/types/component.types.d.ts +6 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +15 -2
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +3 -0
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +5 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chat-conversation-storage.d.ts +34 -0
- package/dist/components/chat/utils/chat-conversation-storage.d.ts.map +1 -0
- package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -0
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +142 -140
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +14 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/contexts/chat-runtime-context.d.ts +8 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +6 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -3
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-message-enhanced.tsx +63 -52
- package/src/components/chat/guide-display.tsx +44 -0
- package/src/components/chat/hooks/use-chat-history-hydration.ts +142 -0
- package/src/components/chat/hooks/use-chat.ts +12 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +2 -0
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +10 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +109 -162
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/types/component.types.ts +9 -0
- package/src/components/chat/types/message.types.ts +17 -1
- package/src/components/chat/types/processing.types.ts +1 -0
- package/src/components/chat/types/unified-chat-state.types.ts +6 -0
- package/src/components/chat/utils/chat-conversation-storage.ts +83 -0
- package/src/components/chat/utils/chunk-parser.ts +6 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +19 -0
- package/src/components/chat/utils/process-historical-messages.ts +6 -0
- package/src/contexts/chat-runtime-context.tsx +8 -0
- package/src/stories/ChatMessageEnhanced.stories.tsx +48 -1
- package/dist/chunk-2B6773QM.cjs.map +0 -1
- package/dist/chunk-5RXKOGH6.cjs.map +0 -1
- package/dist/chunk-77DFPD35.js.map +0 -1
- package/dist/chunk-KFQXO7QB.js.map +0 -1
- /package/dist/{chunk-PNXNWGBY.js.map → chunk-6CFFISUC.js.map} +0 -0
- /package/dist/{chunk-F6BYUO5U.js.map → chunk-6CJ2K7BD.js.map} +0 -0
- /package/dist/{chunk-AJNDDXKC.js.map → chunk-6QK3ONDD.js.map} +0 -0
- /package/dist/{chunk-AHDJMEEN.js.map → chunk-AIRNG27G.js.map} +0 -0
- /package/dist/{chunk-S52LJNMU.js.map → chunk-BZMOEWWS.js.map} +0 -0
- /package/dist/{chunk-HQYJINFV.js.map → chunk-EGXCWZYP.js.map} +0 -0
- /package/dist/{chunk-SCCTJ2UI.js.map → chunk-MPYDX5WX.js.map} +0 -0
- /package/dist/{chunk-YJSKQ2ZV.js.map → chunk-NGEFYPY7.js.map} +0 -0
- /package/dist/{chunk-KLYHZRAD.js.map → chunk-PB3BTZDU.js.map} +0 -0
- /package/dist/{chunk-CWQACI4Z.js.map → chunk-RW2LPL72.js.map} +0 -0
- /package/dist/{chunk-LLWBPPZ4.js.map → chunk-VDZO6IDR.js.map} +0 -0
- /package/dist/{chunk-3BFX6HJM.js.map → chunk-VL2YMDPH.js.map} +0 -0
- /package/dist/{chunk-PXF5J24T.js.map → chunk-WLM3F3WU.js.map} +0 -0
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import { ApprovalBatchMessage } from "./approval-batch-message"
|
|
|
11
11
|
import { ErrorMessageDisplay } from "./error-message-display"
|
|
12
12
|
import { ContextCompactionDisplay } from "./context-compaction-display"
|
|
13
13
|
import { ThinkingDisplay } from "./thinking-display"
|
|
14
|
+
import { GuideDisplay } from "./guide-display"
|
|
14
15
|
import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
|
|
15
16
|
import type { ChatRef } from "./chat-ref.types"
|
|
16
17
|
import { remarkCardLinks } from "./remark-card-links"
|
|
@@ -172,7 +173,12 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
172
173
|
// duplicates so two siblings never collide on the same React key.
|
|
173
174
|
const emittedBlockKeys = new Set<string>()
|
|
174
175
|
segments.forEach((segment, segIdx) => {
|
|
175
|
-
|
|
176
|
+
// Both markdown-bearing segment types take part: a `guide` body is
|
|
177
|
+
// authored by the same LLM and carries the same `[card://]` markers,
|
|
178
|
+
// so skipping it here left its markers with no `inlineByKey` entry and
|
|
179
|
+
// no hoisted card — the `<a card://>` override silently degraded them
|
|
180
|
+
// to a bare title / id.
|
|
181
|
+
if (segment.type !== 'text' && segment.type !== 'guide') return
|
|
176
182
|
const text = segment.text
|
|
177
183
|
const parts: SegmentPart[] = []
|
|
178
184
|
let cursor = 0
|
|
@@ -384,6 +390,52 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
384
390
|
}
|
|
385
391
|
}, [hasMarkerSupport, hasMentionSupport, renderMention, chatRefs, renderingPlan, NavLinkAnchor])
|
|
386
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Body of a markdown-bearing segment (`text` / `guide`).
|
|
395
|
+
*
|
|
396
|
+
* No block markers in this segment → a single SimpleMarkdownRenderer call
|
|
397
|
+
* (the vast majority of messages). Otherwise the text is split at each
|
|
398
|
+
* marker and block payloads are interleaved: each text chunk includes its
|
|
399
|
+
* trailing marker so the inline pill renders at the right position via the
|
|
400
|
+
* `<a>` override, and block payloads land AS SIBLINGS between text chunks —
|
|
401
|
+
* HTML-valid (block DOM never nests inside `<p>`) AND positionally correct
|
|
402
|
+
* (the block appears where the marker is in the flow, not at the segment's
|
|
403
|
+
* end). Stable React keys come from the card key (block) and chunk position
|
|
404
|
+
* (text); streaming token-by-token reuses the same React instances so
|
|
405
|
+
* `<Video>` doesn't remount mid-play.
|
|
406
|
+
*/
|
|
407
|
+
const renderSegmentBody = (segIndex: number, text: string) => {
|
|
408
|
+
const parts = renderingPlan?.partsBySegment.get(segIndex)
|
|
409
|
+
if (!parts || parts.length === 0) {
|
|
410
|
+
return (
|
|
411
|
+
<SimpleMarkdownRenderer
|
|
412
|
+
content={text}
|
|
413
|
+
textSize="compact"
|
|
414
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
415
|
+
componentOverrides={cardComponentOverrides}
|
|
416
|
+
/>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
return parts.map((part, pIdx) => {
|
|
420
|
+
if (part.kind === 'text') {
|
|
421
|
+
return (
|
|
422
|
+
<SimpleMarkdownRenderer
|
|
423
|
+
key={`t-${pIdx}`}
|
|
424
|
+
content={part.text}
|
|
425
|
+
textSize="compact"
|
|
426
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
427
|
+
componentOverrides={cardComponentOverrides}
|
|
428
|
+
/>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
return (
|
|
432
|
+
<div key={`b-${part.key}`} className="my-3">
|
|
433
|
+
{part.node}
|
|
434
|
+
</div>
|
|
435
|
+
)
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
387
439
|
const getAvatarProps = () => {
|
|
388
440
|
const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
|
|
389
441
|
const isMingo = assistantType === 'mingo'
|
|
@@ -488,60 +540,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
488
540
|
{(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
|
|
489
541
|
{segments.map((segment, index) => {
|
|
490
542
|
if (segment.type === 'text') {
|
|
491
|
-
const parts = renderingPlan?.partsBySegment.get(index)
|
|
492
|
-
const wrapperClass = cn(
|
|
493
|
-
"min-w-0 w-full break-words text-h4",
|
|
494
|
-
isError ? "text-ods-error" : "text-ods-text-primary",
|
|
495
|
-
)
|
|
496
|
-
// No block markers in this segment → single
|
|
497
|
-
// SimpleMarkdownRenderer call (existing behaviour
|
|
498
|
-
// preserved for the vast majority of messages).
|
|
499
|
-
if (!parts || parts.length === 0) {
|
|
500
|
-
return (
|
|
501
|
-
<div key={index} className={wrapperClass}>
|
|
502
|
-
<SimpleMarkdownRenderer
|
|
503
|
-
content={segment.text}
|
|
504
|
-
textSize="compact"
|
|
505
|
-
additionalRemarkPlugins={cardRemarkPlugins}
|
|
506
|
-
componentOverrides={cardComponentOverrides}
|
|
507
|
-
/>
|
|
508
|
-
</div>
|
|
509
|
-
)
|
|
510
|
-
}
|
|
511
|
-
// Block markers present → split text at each marker
|
|
512
|
-
// and interleave block payloads. Each text chunk
|
|
513
|
-
// includes its trailing marker so the inline pill
|
|
514
|
-
// renders at the right position via the `<a>`
|
|
515
|
-
// override. Block payloads land AS SIBLINGS between
|
|
516
|
-
// text chunks — HTML-valid (block DOM never nests
|
|
517
|
-
// inside `<p>`) AND positionally correct (block
|
|
518
|
-
// appears where the marker is in the flow, not at
|
|
519
|
-
// the segment's end). Stable React keys come from
|
|
520
|
-
// the card key (block) and chunk position (text);
|
|
521
|
-
// streaming token-by-token reuses the same React
|
|
522
|
-
// instances so `<Video>` doesn't remount mid-play.
|
|
523
543
|
return (
|
|
524
|
-
<div
|
|
525
|
-
{
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
additionalRemarkPlugins={cardRemarkPlugins}
|
|
533
|
-
componentOverrides={cardComponentOverrides}
|
|
534
|
-
/>
|
|
535
|
-
)
|
|
536
|
-
}
|
|
537
|
-
return (
|
|
538
|
-
<div key={`b-${part.key}`} className="my-3">
|
|
539
|
-
{part.node}
|
|
540
|
-
</div>
|
|
541
|
-
)
|
|
542
|
-
})}
|
|
544
|
+
<div
|
|
545
|
+
key={index}
|
|
546
|
+
className={cn(
|
|
547
|
+
"min-w-0 w-full break-words text-h4",
|
|
548
|
+
isError ? "text-ods-error" : "text-ods-text-primary",
|
|
549
|
+
)}
|
|
550
|
+
>
|
|
551
|
+
{renderSegmentBody(index, segment.text)}
|
|
543
552
|
</div>
|
|
544
553
|
)
|
|
554
|
+
} else if (segment.type === 'guide') {
|
|
555
|
+
return <GuideDisplay key={index}>{renderSegmentBody(index, segment.text)}</GuideDisplay>
|
|
545
556
|
} else if (segment.type === 'tool_execution') {
|
|
546
557
|
return (
|
|
547
558
|
<ToolExecutionDisplay
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../utils/cn"
|
|
6
|
+
import { CompassIcon } from "../icons-v2-generated"
|
|
7
|
+
import type { GuideDisplayProps } from "./types"
|
|
8
|
+
|
|
9
|
+
const GUIDE_LABEL = "OpenFrame Guide"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `GUIDE` segment — the assistant's how-to/documentation answer, framed as a
|
|
13
|
+
* titled card instead of a bare paragraph.
|
|
14
|
+
*
|
|
15
|
+
* Chrome only: the caller renders the markdown body and passes it as children,
|
|
16
|
+
* so a guide goes through the SAME body pipeline as a text segment
|
|
17
|
+
* (`[card://]` splitting + hoisted entity cards + mention chips). Rendering the
|
|
18
|
+
* raw markdown here instead would bypass that plan and degrade every card
|
|
19
|
+
* marker inside a guide to a bare title.
|
|
20
|
+
*/
|
|
21
|
+
const GuideDisplay = forwardRef<HTMLDivElement, GuideDisplayProps>(
|
|
22
|
+
({ className, children, ...props }, ref) => {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex flex-col gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]",
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<div className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
33
|
+
<CompassIcon size={16} className="shrink-0 text-ods-open-yellow" />
|
|
34
|
+
<span className="text-h5 text-ods-open-yellow">{GUIDE_LABEL}</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="min-w-0 w-full break-words text-h4 text-ods-text-primary">{children}</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
GuideDisplay.displayName = "GuideDisplay"
|
|
43
|
+
|
|
44
|
+
export { GuideDisplay }
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useChatHistoryHydration — mount-time rebuild of the chat message list from
|
|
5
|
+
* the SERVER-side conversation store (the `chat_conversations` /
|
|
6
|
+
* `chat_messages` SSOT behind `GET <chatStreamUrl>/history`).
|
|
7
|
+
*
|
|
8
|
+
* The client persists ONLY the server-issued conversation id (see
|
|
9
|
+
* `chat-conversation-storage.ts`); this hook turns that id back into rendered
|
|
10
|
+
* history: message bubbles, inline entity-card refs (per send index, same
|
|
11
|
+
* `sendIdx` scheme as live turns), and the send counter.
|
|
12
|
+
*
|
|
13
|
+
* Failure semantics: a fetch/parse failure simply leaves the UI empty — the
|
|
14
|
+
* SERVER still resolves full history for the next turn (it re-reads
|
|
15
|
+
* `chat_messages` by conversation id on every send), so conversation context
|
|
16
|
+
* is never lost when hydration misses.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useRef, useState, type MutableRefObject } from 'react'
|
|
20
|
+
import type { ChatRef } from '../chat-ref.types'
|
|
21
|
+
import type { Message } from './use-chat'
|
|
22
|
+
import { chatAuthedFetch } from '../utils/chat-authed-fetch'
|
|
23
|
+
import { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../utils/auto-continuation-directive'
|
|
24
|
+
|
|
25
|
+
export interface UseChatHistoryHydrationArgs {
|
|
26
|
+
/** Mirrors the adapter's `active` gate — an idle (Mingo-mode) mount never fetches. */
|
|
27
|
+
active: boolean
|
|
28
|
+
/** Chat source (= platform); part of the once-per-conversation guard key. */
|
|
29
|
+
source: string
|
|
30
|
+
/** Resolved history endpoint (`<chatStreamUrl>/history` by default). */
|
|
31
|
+
historyUrl: string
|
|
32
|
+
/** The server-issued conversation id (null = nothing to hydrate). */
|
|
33
|
+
conversationIdRef: MutableRefObject<string | null>
|
|
34
|
+
/** Per-send inline entity-card refs — repopulated from `chat_refs`. */
|
|
35
|
+
refsMapRef: MutableRefObject<Map<number, Record<string, ChatRef>>>
|
|
36
|
+
/** User-send counter — set to the hydrated user-turn count so the next
|
|
37
|
+
* live send lands on the following `sendIdx`. */
|
|
38
|
+
sendCountRef: MutableRefObject<number>
|
|
39
|
+
/** `useChat`'s injection primitive (replace-or-prepend). */
|
|
40
|
+
hydrateMessages: (history: Message[]) => void
|
|
41
|
+
/** Invalidates the adapter's `latestMeta` memo after refs repopulate. */
|
|
42
|
+
bumpMetaTick: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface UseChatHistoryHydrationResult {
|
|
46
|
+
/** True while the rebuild request is in flight. */
|
|
47
|
+
isHydratingHistory: boolean
|
|
48
|
+
/** Once-per-`source:conversationId` guard. The adapter's `clearMessages`
|
|
49
|
+
* resets it to null alongside the stored id (a fresh conversation has no
|
|
50
|
+
* server history to fetch). */
|
|
51
|
+
hydratedKeyRef: MutableRefObject<string | null>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useChatHistoryHydration({
|
|
55
|
+
active,
|
|
56
|
+
source,
|
|
57
|
+
historyUrl,
|
|
58
|
+
conversationIdRef,
|
|
59
|
+
refsMapRef,
|
|
60
|
+
sendCountRef,
|
|
61
|
+
hydrateMessages,
|
|
62
|
+
bumpMetaTick,
|
|
63
|
+
}: UseChatHistoryHydrationArgs): UseChatHistoryHydrationResult {
|
|
64
|
+
const [isHydratingHistory, setIsHydratingHistory] = useState(false)
|
|
65
|
+
const hydratedKeyRef = useRef<string | null>(null)
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!active) return
|
|
69
|
+
// No stored conversation id → nothing to hydrate (fresh visitor / after
|
|
70
|
+
// "new chat"); the first send establishes the conversation server-side.
|
|
71
|
+
const conversationId = conversationIdRef.current
|
|
72
|
+
if (!conversationId) return
|
|
73
|
+
const key = `${source}:${conversationId}`
|
|
74
|
+
if (hydratedKeyRef.current === key) return
|
|
75
|
+
hydratedKeyRef.current = key
|
|
76
|
+
let cancelled = false
|
|
77
|
+
setIsHydratingHistory(true)
|
|
78
|
+
;(async () => {
|
|
79
|
+
try {
|
|
80
|
+
const res = await chatAuthedFetch(
|
|
81
|
+
`${historyUrl}?conversationId=${encodeURIComponent(conversationId)}`,
|
|
82
|
+
{ method: 'GET' },
|
|
83
|
+
)
|
|
84
|
+
if (!res.ok) return
|
|
85
|
+
const payload = await res.json().catch(() => null)
|
|
86
|
+
// route-base successResponse envelope ({ data }) with a raw-body fallback.
|
|
87
|
+
const body = (payload?.data ?? payload) as
|
|
88
|
+
| { messages?: Array<Record<string, unknown>> }
|
|
89
|
+
| null
|
|
90
|
+
const rows = Array.isArray(body?.messages) ? body!.messages! : []
|
|
91
|
+
if (cancelled || rows.length === 0) return
|
|
92
|
+
const hydrated: Message[] = []
|
|
93
|
+
let userTurns = 0
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const role =
|
|
96
|
+
row.role === 'assistant' ? 'assistant' : row.role === 'user' ? 'user' : null
|
|
97
|
+
if (!role) continue
|
|
98
|
+
const content = typeof row.content === 'string' ? row.content : ''
|
|
99
|
+
// The approval placeholder ('') and the server-built auto-continuation
|
|
100
|
+
// directive are part of the LLM history but never rendered.
|
|
101
|
+
const hidden =
|
|
102
|
+
role === 'user' &&
|
|
103
|
+
(content === '' || content.startsWith(AUTO_CONTINUATION_DIRECTIVE_PREFIX))
|
|
104
|
+
if (role === 'user') {
|
|
105
|
+
userTurns += 1
|
|
106
|
+
} else if (row.chat_refs && typeof row.chat_refs === 'object') {
|
|
107
|
+
// Re-attach inline entity-card refs at the send index this
|
|
108
|
+
// assistant row belongs to (same sendIdx scheme as live turns).
|
|
109
|
+
refsMapRef.current.set(
|
|
110
|
+
Math.max(0, userTurns - 1),
|
|
111
|
+
row.chat_refs as Record<string, ChatRef>,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
hydrated.push({
|
|
115
|
+
id: `hydrated-${String(row.seq ?? hydrated.length)}`,
|
|
116
|
+
role,
|
|
117
|
+
content,
|
|
118
|
+
...(typeof row.created_at === 'string'
|
|
119
|
+
? { timestamp: new Date(row.created_at) }
|
|
120
|
+
: {}),
|
|
121
|
+
...(hidden ? { hidden: true } : {}),
|
|
122
|
+
} as Message)
|
|
123
|
+
}
|
|
124
|
+
if (cancelled || hydrated.length === 0) return
|
|
125
|
+
sendCountRef.current = userTurns
|
|
126
|
+
hydrateMessages(hydrated)
|
|
127
|
+
bumpMetaTick()
|
|
128
|
+
} catch {
|
|
129
|
+
// Fetch failed — start empty; the server still owns history (above).
|
|
130
|
+
} finally {
|
|
131
|
+
if (!cancelled) setIsHydratingHistory(false)
|
|
132
|
+
}
|
|
133
|
+
})()
|
|
134
|
+
return () => {
|
|
135
|
+
cancelled = true
|
|
136
|
+
}
|
|
137
|
+
// Refs are stable across renders — the effect keys on the identity-ish deps only.
|
|
138
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
139
|
+
}, [active, source, historyUrl, hydrateMessages, bumpMetaTick])
|
|
140
|
+
|
|
141
|
+
return { isHydratingHistory, hydratedKeyRef }
|
|
142
|
+
}
|
|
@@ -476,6 +476,17 @@ export function useChat({
|
|
|
476
476
|
reset()
|
|
477
477
|
}, [reset])
|
|
478
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Replace-or-prepend server-hydrated history. Called once by the SSE
|
|
481
|
+
* adapter after `GET /api/docs/chat/history` resolves. If the user already
|
|
482
|
+
* sent a message before hydration landed, the fetched history is PREPENDED
|
|
483
|
+
* so nothing typed is lost — correctness is unaffected either way because
|
|
484
|
+
* the server resolves LLM history from its own store, not from this state.
|
|
485
|
+
*/
|
|
486
|
+
const hydrateMessages = useCallback((history: Message[]) => {
|
|
487
|
+
setMessages((prev) => (prev.length === 0 ? history : [...history, ...prev]))
|
|
488
|
+
}, [])
|
|
489
|
+
|
|
479
490
|
/**
|
|
480
491
|
* Abort the in-flight streamed message. The fetch's AbortSignal terminates
|
|
481
492
|
* the upstream Anthropic request (so billing stops); the `for await` loop
|
|
@@ -496,6 +507,7 @@ export function useChat({
|
|
|
496
507
|
stopMessage,
|
|
497
508
|
handleQuickAction,
|
|
498
509
|
clearMessages,
|
|
510
|
+
hydrateMessages,
|
|
499
511
|
hasMessages: messages.length > 0,
|
|
500
512
|
}
|
|
501
513
|
}
|
|
@@ -442,6 +442,8 @@ export function appendToTrailingAssistant(
|
|
|
442
442
|
merged[merged.length - 1] = { type: 'text', text: tail.text + seg.text }
|
|
443
443
|
} else if (seg.type === 'thinking' && tail?.type === 'thinking') {
|
|
444
444
|
merged[merged.length - 1] = { type: 'thinking', text: tail.text + seg.text }
|
|
445
|
+
} else if (seg.type === 'guide' && tail?.type === 'guide') {
|
|
446
|
+
merged[merged.length - 1] = { type: 'guide', text: tail.text + seg.text }
|
|
445
447
|
} else if (seg.type === 'approval_batch') {
|
|
446
448
|
// Approval deltas must be IDEMPOTENT: the escalated-result emit can be
|
|
447
449
|
// seen twice (live + catch-up replay over hydrated history), so upsert
|
|
@@ -173,6 +173,16 @@ export function useRealtimeChunkProcessor(
|
|
|
173
173
|
break
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
case 'guide': {
|
|
177
|
+
const segments = accumulator.appendGuide(action.text)
|
|
178
|
+
if (isInStreamRef.current || !hasEverStreamedRef.current) {
|
|
179
|
+
emitSegments(segments)
|
|
180
|
+
} else {
|
|
181
|
+
emitSegments([{ type: 'guide', text: action.text }], { append: true })
|
|
182
|
+
}
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
|
|
176
186
|
case 'tool_execution': {
|
|
177
187
|
// A starting tool run means the agent's turn is in progress even
|
|
178
188
|
// when this lands after MESSAGE_END (approved commands execute
|