@flamingo-stack/openframe-frontend-core 0.0.487 → 0.0.488-snapshot.20260728130942
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-4TFUB2CR.cjs → chunk-2IHN7NNB.cjs} +29 -29
- package/dist/{chunk-4TFUB2CR.cjs.map → chunk-2IHN7NNB.cjs.map} +1 -1
- package/dist/{chunk-UU4DW7JX.cjs → chunk-2RNJYJWO.cjs} +42 -42
- package/dist/{chunk-UU4DW7JX.cjs.map → chunk-2RNJYJWO.cjs.map} +1 -1
- package/dist/{chunk-W6GDTB4R.cjs → chunk-3OIB763B.cjs} +15 -15
- package/dist/{chunk-W6GDTB4R.cjs.map → chunk-3OIB763B.cjs.map} +1 -1
- package/dist/{chunk-NETEA5E4.js → chunk-5VSEMWWE.js} +6 -6
- package/dist/{chunk-K6MITU6M.cjs → chunk-5YMY4BAW.cjs} +77 -77
- package/dist/{chunk-K6MITU6M.cjs.map → chunk-5YMY4BAW.cjs.map} +1 -1
- package/dist/{chunk-KFQXO7QB.js → chunk-6GT557KN.js} +1 -1
- package/dist/chunk-6GT557KN.js.map +1 -0
- package/dist/{chunk-QBDHNUQX.js → chunk-B4YR342L.js} +6 -6
- package/dist/{chunk-SMD67GJP.js → chunk-BGWDDB7Y.js} +4 -4
- 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-G4XU7BYU.js → chunk-EU6QNO4S.js} +2 -2
- package/dist/{chunk-ITEPRN6L.js → chunk-FKPXFTZT.js} +3 -3
- package/dist/{chunk-EO7MUGY4.cjs → chunk-H3YRJ3LZ.cjs} +14 -14
- package/dist/{chunk-EO7MUGY4.cjs.map → chunk-H3YRJ3LZ.cjs.map} +1 -1
- package/dist/{chunk-TJSJPOPL.cjs → chunk-IN4IO7BL.cjs} +395 -337
- package/dist/chunk-IN4IO7BL.cjs.map +1 -0
- package/dist/{chunk-HWK5CH5N.cjs → chunk-J7H32E3R.cjs} +18 -18
- package/dist/{chunk-HWK5CH5N.cjs.map → chunk-J7H32E3R.cjs.map} +1 -1
- package/dist/{chunk-BTMYUK4P.js → chunk-K7MNWYYI.js} +3 -3
- 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-RAYDQGJV.cjs → chunk-LLEDO4P7.cjs} +75 -75
- package/dist/{chunk-RAYDQGJV.cjs.map → chunk-LLEDO4P7.cjs.map} +1 -1
- package/dist/{chunk-AUGM74CO.js → chunk-N4MTNTSX.js} +7 -7
- package/dist/{chunk-KLYHZRAD.js → chunk-PB3BTZDU.js} +2 -2
- package/dist/{chunk-QW6SPNPT.cjs → chunk-RO7Q56ZK.cjs} +40 -40
- package/dist/{chunk-QW6SPNPT.cjs.map → chunk-RO7Q56ZK.cjs.map} +1 -1
- package/dist/{chunk-CWQACI4Z.js → chunk-RW2LPL72.js} +2 -2
- package/dist/{chunk-SDNMOAT4.js → chunk-TWGYBXYV.js} +4 -4
- package/dist/{chunk-WLRBZXFB.js → chunk-VL4LUO32.js} +3 -3
- package/dist/{chunk-QL7S3CH4.cjs → chunk-VLOE7MI2.cjs} +35 -35
- package/dist/{chunk-QL7S3CH4.cjs.map → chunk-VLOE7MI2.cjs.map} +1 -1
- package/dist/{chunk-UBK5HIMH.js → chunk-W6ZUOWSB.js} +7 -7
- package/dist/{chunk-GXOUXAXU.js → chunk-X2YF5TJS.js} +2 -2
- package/dist/{chunk-BC74IKAX.js → chunk-YMJZOANX.js} +277 -219
- package/dist/chunk-YMJZOANX.js.map +1 -0
- package/dist/{chunk-6SBAF5VG.cjs → chunk-Z7Y63IFU.cjs} +7 -7
- package/dist/{chunk-6SBAF5VG.cjs.map → chunk-Z7Y63IFU.cjs.map} +1 -1
- package/dist/{chunk-KTD2MGYF.cjs → chunk-ZIAS5NNL.cjs} +7 -7
- package/dist/{chunk-KTD2MGYF.cjs.map → chunk-ZIAS5NNL.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- 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-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 +4 -4
- package/dist/components/chat/index.js +3 -3
- 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/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 +140 -140
- package/dist/components/index.js +12 -12
- 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 +4 -4
- package/dist/components/ui/index.js +3 -3
- package/dist/components/ui/query-report-table/query-report-table.d.ts.map +1 -1
- package/dist/components/ui/query-report-table/types.d.ts +2 -2
- package/dist/components/ui/query-report-table/types.d.ts.map +1 -1
- 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 +4 -4
- package/dist/index.js +3 -3
- package/package.json +1 -1
- 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-sse-chat-adapter.ts +109 -162
- 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/features/notifications/notification-drawer.tsx +1 -1
- package/src/components/ui/query-report-table/query-report-table-skeleton.tsx +1 -1
- package/src/components/ui/query-report-table/query-report-table.tsx +7 -8
- package/src/components/ui/query-report-table/types.ts +2 -2
- package/src/contexts/chat-runtime-context.tsx +8 -0
- package/dist/chunk-2B6773QM.cjs.map +0 -1
- package/dist/chunk-BC74IKAX.js.map +0 -1
- package/dist/chunk-KFQXO7QB.js.map +0 -1
- package/dist/chunk-TJSJPOPL.cjs.map +0 -1
- /package/dist/{chunk-NETEA5E4.js.map → chunk-5VSEMWWE.js.map} +0 -0
- /package/dist/{chunk-QBDHNUQX.js.map → chunk-B4YR342L.js.map} +0 -0
- /package/dist/{chunk-SMD67GJP.js.map → chunk-BGWDDB7Y.js.map} +0 -0
- /package/dist/{chunk-G4XU7BYU.js.map → chunk-EU6QNO4S.js.map} +0 -0
- /package/dist/{chunk-ITEPRN6L.js.map → chunk-FKPXFTZT.js.map} +0 -0
- /package/dist/{chunk-BTMYUK4P.js.map → chunk-K7MNWYYI.js.map} +0 -0
- /package/dist/{chunk-AUGM74CO.js.map → chunk-N4MTNTSX.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-SDNMOAT4.js.map → chunk-TWGYBXYV.js.map} +0 -0
- /package/dist/{chunk-WLRBZXFB.js.map → chunk-VL4LUO32.js.map} +0 -0
- /package/dist/{chunk-UBK5HIMH.js.map → chunk-W6ZUOWSB.js.map} +0 -0
- /package/dist/{chunk-GXOUXAXU.js.map → chunk-X2YF5TJS.js.map} +0 -0
|
@@ -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
|
}
|
|
@@ -40,17 +40,21 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { useState, useEffect, useCallback, useMemo, useRef, type MutableRefObject } from 'react'
|
|
43
|
-
import { useChat, type
|
|
43
|
+
import { useChat, type StreamFnExtraOptions } from './use-chat'
|
|
44
44
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
45
45
|
import type { ChatRef } from '../chat-ref.types'
|
|
46
46
|
import { buildChatRefKey } from '../types/chat.types'
|
|
47
47
|
import type { MessageSegment } from '../types/message.types'
|
|
48
48
|
import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'
|
|
49
|
-
import { getChatProxyAuth } from '../utils/chat-proxy-auth-storage'
|
|
50
49
|
import { chatAuthedFetch } from '../utils/chat-authed-fetch'
|
|
50
|
+
import {
|
|
51
|
+
createChatConversationStorage,
|
|
52
|
+
pruneStaleChatConversationStorage,
|
|
53
|
+
type PersistedChatConversation,
|
|
54
|
+
} from '../utils/chat-conversation-storage'
|
|
55
|
+
import type { LocalStorageAdapter } from '../../../utils/local-storage-adapter'
|
|
56
|
+
import { useChatHistoryHydration } from './use-chat-history-hydration'
|
|
51
57
|
import { parseScrollAnchor, type ScrollAnchor } from '../utils/scroll-anchor'
|
|
52
|
-
import { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../utils/auto-continuation-directive'
|
|
53
|
-
import { flattenAssistantContent } from '../utils/flatten-assistant-content'
|
|
54
58
|
import { sanitizeTitleForChat } from '../utils/slash-dispatch-utils'
|
|
55
59
|
import { defaultTableIdForDocumentType } from '../utils/source-icons'
|
|
56
60
|
import type {
|
|
@@ -146,6 +150,10 @@ export interface ChatTurnMeta {
|
|
|
146
150
|
/** Routing decision from the server's `decideRoute`. */
|
|
147
151
|
routedComplexity: string | null
|
|
148
152
|
routedThinkingBudget: number | null
|
|
153
|
+
/** Server-computed conversation trace id (`chat_conversations.id` for this
|
|
154
|
+
* thread), echoed in the leading metadata frame. Stable across turns of
|
|
155
|
+
* one thread — the correlation handle for transcripts/analytics. */
|
|
156
|
+
conversationId: string | null
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
/**
|
|
@@ -199,6 +207,7 @@ function createEmptyTurnMeta(): ChatTurnMeta {
|
|
|
199
207
|
scrollAnchor: null,
|
|
200
208
|
routedComplexity: null,
|
|
201
209
|
routedThinkingBudget: null,
|
|
210
|
+
conversationId: null,
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
213
|
|
|
@@ -215,13 +224,14 @@ function escapeThinkingTags(text: string): string {
|
|
|
215
224
|
function createDocStreamFn(
|
|
216
225
|
source: DocSource,
|
|
217
226
|
endpoints: { chatStreamUrl: string; approvalToolUrl: string },
|
|
218
|
-
messagesRef: MutableRefObject<Message[]>,
|
|
219
227
|
sourcesMapRef: MutableRefObject<Map<number, ChatSource[]>>,
|
|
220
228
|
refsMapRef: MutableRefObject<Map<number, Record<string, ChatRef>>>,
|
|
221
229
|
metaMapRef: MutableRefObject<Map<number, ChatTurnMeta>>,
|
|
222
230
|
setStreamingPhase: (phase: StreamingPhase) => void,
|
|
223
231
|
bumpMetaTick: () => void,
|
|
224
232
|
sendCountRef: MutableRefObject<number>,
|
|
233
|
+
conversationIdRef: MutableRefObject<string | null>,
|
|
234
|
+
conversationStorage: LocalStorageAdapter<PersistedChatConversation>,
|
|
225
235
|
) {
|
|
226
236
|
// CRITICAL: the decoder + buffer MUST live INSIDE the returned async-
|
|
227
237
|
// generator function (per-call closure), NOT at the factory level. A
|
|
@@ -233,22 +243,6 @@ function createDocStreamFn(
|
|
|
233
243
|
signal?: AbortSignal,
|
|
234
244
|
extra?: StreamFnExtraOptions,
|
|
235
245
|
): 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
246
|
// URL + body branch — approvalAction routes to the approval-tool
|
|
253
247
|
// endpoint, the standard chat path routes to the chat-stream endpoint.
|
|
254
248
|
const targetPath = extra?.approvalAction
|
|
@@ -258,26 +252,30 @@ function createDocStreamFn(
|
|
|
258
252
|
// it server-side via its own platform-detection — tamper-proof binding
|
|
259
253
|
// so a client on one platform can't POST a different platform's
|
|
260
254
|
// conversation.
|
|
255
|
+
//
|
|
256
|
+
// The server is the single source of conversation history: it re-reads
|
|
257
|
+
// `chat_messages` by conversation id on every turn. The wire therefore
|
|
258
|
+
// carries ONLY the new user message — never the prior conversation.
|
|
259
|
+
// The conversation id is SERVER-minted: the first message of a session
|
|
260
|
+
// sends none, the server mints one and echoes it in the leading metadata
|
|
261
|
+
// frame (captured below into `conversationIdRef`), and every later turn
|
|
262
|
+
// echoes it back.
|
|
263
|
+
const conversationId = conversationIdRef.current
|
|
261
264
|
const requestBody = extra?.approvalAction
|
|
262
265
|
? {
|
|
263
266
|
proposal_id: extra.approvalAction.proposalId,
|
|
264
267
|
action: extra.approvalAction.action,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
role: m.role,
|
|
269
|
-
content:
|
|
270
|
-
typeof m.content === 'string'
|
|
271
|
-
? m.content
|
|
272
|
-
: flattenAssistantContent(m.content),
|
|
273
|
-
})),
|
|
268
|
+
// Always present here — an approval can only happen inside an
|
|
269
|
+
// established conversation (the proposal turn captured the id).
|
|
270
|
+
conversationId,
|
|
274
271
|
}
|
|
275
272
|
: {
|
|
276
|
-
messages:
|
|
273
|
+
messages: [{ role: 'user', content: message }],
|
|
277
274
|
...(extra?.commandOverride ? { commandOverride: extra.commandOverride } : {}),
|
|
278
275
|
...(extra?.pendingAttachments && extra.pendingAttachments.length > 0
|
|
279
276
|
? { pendingAttachments: extra.pendingAttachments }
|
|
280
277
|
: {}),
|
|
278
|
+
...(conversationId ? { conversationId } : {}),
|
|
281
279
|
}
|
|
282
280
|
// `chatAuthedFetch` carries the bearer-act-as headers (+ Supabase
|
|
283
281
|
// session cookies) — same wrapper `use-chat-attachments` and
|
|
@@ -489,6 +487,18 @@ function createDocStreamFn(
|
|
|
489
487
|
})
|
|
490
488
|
bumpMetaTick()
|
|
491
489
|
}
|
|
490
|
+
// Server-minted conversation id, echoed on every turn. On the
|
|
491
|
+
// session's FIRST turn this is where the client learns its id —
|
|
492
|
+
// capture it and persist it so later turns and future visits
|
|
493
|
+
// continue the same conversation.
|
|
494
|
+
if (typeof meta.conversationId === 'string' && meta.conversationId) {
|
|
495
|
+
if (conversationIdRef.current !== meta.conversationId) {
|
|
496
|
+
conversationIdRef.current = meta.conversationId
|
|
497
|
+
conversationStorage.save({ conversationId: meta.conversationId })
|
|
498
|
+
}
|
|
499
|
+
mergeTurnMeta(metaMapRef, sendIdx, { conversationId: meta.conversationId })
|
|
500
|
+
bumpMetaTick()
|
|
501
|
+
}
|
|
492
502
|
const parsedAnchor = parseScrollAnchor(meta.scrollAnchor)
|
|
493
503
|
if (parsedAnchor !== null) {
|
|
494
504
|
mergeTurnMeta(metaMapRef, sendIdx, { scrollAnchor: parsedAnchor })
|
|
@@ -587,94 +597,25 @@ function mergeTurnMeta(
|
|
|
587
597
|
}
|
|
588
598
|
|
|
589
599
|
// =============================================================================
|
|
590
|
-
//
|
|
600
|
+
// Local persistence — the server-issued conversation id ONLY
|
|
591
601
|
// =============================================================================
|
|
602
|
+
//
|
|
603
|
+
// The ONLY thing persisted locally is the conversation id the SERVER minted
|
|
604
|
+
// and echoed in the leading metadata frame of the session's first turn.
|
|
605
|
+
// The client never generates ids. Message history lives server-side in
|
|
606
|
+
// `chat_conversations` / `chat_messages` (recorded turn-by-turn by the chat
|
|
607
|
+
// route) and is rehydrated on mount via `GET <chatStreamUrl>/history`.
|
|
608
|
+
// localStorage never stores messages, sources, refs, or send counts —
|
|
609
|
+
// the server transcript is the single source of truth for history.
|
|
592
610
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
/** localStorage history namespace used when no `source` is configured on the
|
|
611
|
+
/** localStorage namespace used when no `source` is configured on the
|
|
596
612
|
* runtime. Embedders are platform-agnostic (see `ChatRuntime.source`), so any
|
|
597
613
|
* stable string works here — the hub passes its real platform instead. */
|
|
598
614
|
const DEFAULT_CHAT_SOURCE = 'embed'
|
|
599
615
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const base = `mingo-chat-${source}-v${CHAT_STORAGE_VERSION}`
|
|
604
|
-
const auth = getChatProxyAuth()
|
|
605
|
-
if (auth?.email) {
|
|
606
|
-
return `${base}-u-${encodeURIComponent(auth.email.toLowerCase())}`
|
|
607
|
-
}
|
|
608
|
-
return base
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/** Sweep stale per-user chat-history keys. Drops any key whose email
|
|
612
|
-
* differs from the CURRENT proxy-auth identity. */
|
|
613
|
-
function pruneStaleChatStorage(source: DocSource): void {
|
|
614
|
-
if (typeof window === 'undefined') return
|
|
615
|
-
try {
|
|
616
|
-
const currentKey = chatStorageKey(source)
|
|
617
|
-
const prefix = `mingo-chat-${source}-v${CHAT_STORAGE_VERSION}-u-`
|
|
618
|
-
const legacy = `mingo-chat-${source}-v${CHAT_STORAGE_VERSION}`
|
|
619
|
-
const toRemove: string[] = []
|
|
620
|
-
for (let i = 0; i < window.localStorage.length; i++) {
|
|
621
|
-
const k = window.localStorage.key(i)
|
|
622
|
-
if (!k) continue
|
|
623
|
-
if (!k.startsWith(prefix)) continue
|
|
624
|
-
if (k === currentKey) continue
|
|
625
|
-
if (k === legacy) continue
|
|
626
|
-
toRemove.push(k)
|
|
627
|
-
}
|
|
628
|
-
for (const k of toRemove) {
|
|
629
|
-
window.localStorage.removeItem(k)
|
|
630
|
-
}
|
|
631
|
-
} catch {
|
|
632
|
-
// localStorage access blocked (Safari private mode etc.) — non-fatal.
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
interface PersistedChatState {
|
|
637
|
-
messages: Message[]
|
|
638
|
-
sources: Array<[number, ChatSource[]]>
|
|
639
|
-
/** Per-turn refs for inline object cards. */
|
|
640
|
-
refs?: Array<[number, Record<string, ChatRef>]>
|
|
641
|
-
sendCount: number
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function loadPersistedChat(source: DocSource): PersistedChatState | null {
|
|
645
|
-
if (typeof window === 'undefined') return null
|
|
646
|
-
try {
|
|
647
|
-
const raw = window.localStorage.getItem(chatStorageKey(source))
|
|
648
|
-
if (!raw) return null
|
|
649
|
-
const parsed = JSON.parse(raw) as PersistedChatState
|
|
650
|
-
if (!parsed || !Array.isArray(parsed.messages)) return null
|
|
651
|
-
// Rehydrate Date objects + run forward-compat migrations.
|
|
652
|
-
for (const m of parsed.messages) {
|
|
653
|
-
if (typeof m.timestamp === 'string') m.timestamp = new Date(m.timestamp)
|
|
654
|
-
// Forward-migration for auto-continuation directive bubbles.
|
|
655
|
-
if (
|
|
656
|
-
m.role === 'user' &&
|
|
657
|
-
!m.hidden &&
|
|
658
|
-
typeof m.content === 'string' &&
|
|
659
|
-
m.content.startsWith(AUTO_CONTINUATION_DIRECTIVE_PREFIX)
|
|
660
|
-
) {
|
|
661
|
-
m.hidden = true
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return parsed
|
|
665
|
-
} catch {
|
|
666
|
-
return null
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function savePersistedChat(source: DocSource, state: PersistedChatState) {
|
|
671
|
-
if (typeof window === 'undefined') return
|
|
672
|
-
try {
|
|
673
|
-
window.localStorage.setItem(chatStorageKey(source), JSON.stringify(state))
|
|
674
|
-
} catch {
|
|
675
|
-
// Quota exceeded or private mode — silently drop.
|
|
676
|
-
}
|
|
677
|
-
}
|
|
616
|
+
// Persistence itself lives in `../utils/chat-conversation-storage.ts` (built
|
|
617
|
+
// on the lib-standard `createLocalStorageAdapter`); the mount-time history
|
|
618
|
+
// rebuild lives in `./use-chat-history-hydration.ts`.
|
|
678
619
|
|
|
679
620
|
// =============================================================================
|
|
680
621
|
// useSseChatAdapter — public hook
|
|
@@ -698,10 +639,10 @@ export function useSseChatAdapter(
|
|
|
698
639
|
// (hub) / embedder's provider must wrap the tree.
|
|
699
640
|
const runtime = useRequiredChatRuntime()
|
|
700
641
|
// `source` is OPTIONAL — embedders are platform-agnostic (see ChatRuntime.source).
|
|
701
|
-
// Here it's used ONLY for the localStorage
|
|
702
|
-
// labels; it is NEVER sent on the wire (the hub resolves source server-side
|
|
703
|
-
// currentPlatform()). Fall back to a stable constant so the persistence key
|
|
704
|
-
// well-formed when the embedder leaves source unset.
|
|
642
|
+
// Here it's used ONLY for the conversation-id localStorage namespace + client-side
|
|
643
|
+
// meta labels; it is NEVER sent on the wire (the hub resolves source server-side
|
|
644
|
+
// via currentPlatform()). Fall back to a stable constant so the persistence key
|
|
645
|
+
// stays well-formed when the embedder leaves source unset.
|
|
705
646
|
const source = runtime.source || DEFAULT_CHAT_SOURCE
|
|
706
647
|
// Fall back to the lib-baked hub-canonical map when the embedder
|
|
707
648
|
// didn't supply an override. Keeps Ask + Display working in any
|
|
@@ -711,23 +652,24 @@ export function useSseChatAdapter(
|
|
|
711
652
|
const tableIdForDocumentType =
|
|
712
653
|
options?.tableIdForDocumentType ?? defaultTableIdForDocumentType
|
|
713
654
|
|
|
714
|
-
// Restore
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
655
|
+
// Restore the server-issued conversation id once on mount. Null = no
|
|
656
|
+
// conversation yet — the FIRST send goes out without an id, the server
|
|
657
|
+
// mints one, and the metadata-frame capture (in the stream fn) stores it.
|
|
658
|
+
// The client NEVER generates ids. This is the ONLY local persistence —
|
|
659
|
+
// message history hydrates from the server transcript below.
|
|
660
|
+
const conversationStorage = useMemo(() => createChatConversationStorage(source), [source])
|
|
661
|
+
const conversationIdRef = useRef<string | null>(null)
|
|
662
|
+
const restoredConversationRef = useRef(false)
|
|
663
|
+
if (!restoredConversationRef.current) {
|
|
664
|
+
restoredConversationRef.current = true
|
|
665
|
+
pruneStaleChatConversationStorage(source)
|
|
666
|
+
conversationIdRef.current = conversationStorage.load()?.conversationId ?? null
|
|
720
667
|
}
|
|
721
668
|
|
|
722
|
-
const sourcesMapRef = useRef<Map<number, ChatSource[]>>(
|
|
723
|
-
|
|
724
|
-
)
|
|
725
|
-
const refsMapRef = useRef<Map<number, Record<string, ChatRef>>>(
|
|
726
|
-
new Map(persistedRef.current.refs ?? []),
|
|
727
|
-
)
|
|
669
|
+
const sourcesMapRef = useRef<Map<number, ChatSource[]>>(new Map())
|
|
670
|
+
const refsMapRef = useRef<Map<number, Record<string, ChatRef>>>(new Map())
|
|
728
671
|
const metaMapRef = useRef<Map<number, ChatTurnMeta>>(new Map())
|
|
729
|
-
const
|
|
730
|
-
const sendCountRef = useRef(persistedRef.current.sendCount)
|
|
672
|
+
const sendCountRef = useRef(0)
|
|
731
673
|
const [streamingPhase, setStreamingPhase] = useState<StreamingPhase>('idle')
|
|
732
674
|
const [metaTick, setMetaTick] = useState(0)
|
|
733
675
|
const bumpMetaTick = useCallback(() => setMetaTick((t) => t + 1), [])
|
|
@@ -740,19 +682,21 @@ export function useSseChatAdapter(
|
|
|
740
682
|
chatStreamUrl: runtime.endpoints.chatStreamUrl,
|
|
741
683
|
approvalToolUrl: runtime.endpoints.approvalToolUrl,
|
|
742
684
|
},
|
|
743
|
-
messagesRef,
|
|
744
685
|
sourcesMapRef,
|
|
745
686
|
refsMapRef,
|
|
746
687
|
metaMapRef,
|
|
747
688
|
setStreamingPhase,
|
|
748
689
|
bumpMetaTick,
|
|
749
690
|
sendCountRef,
|
|
691
|
+
conversationIdRef,
|
|
692
|
+
conversationStorage,
|
|
750
693
|
),
|
|
751
694
|
[
|
|
752
695
|
source,
|
|
753
696
|
runtime.endpoints.chatStreamUrl,
|
|
754
697
|
runtime.endpoints.approvalToolUrl,
|
|
755
698
|
bumpMetaTick,
|
|
699
|
+
conversationStorage,
|
|
756
700
|
],
|
|
757
701
|
)
|
|
758
702
|
|
|
@@ -792,20 +736,6 @@ export function useSseChatAdapter(
|
|
|
792
736
|
return map
|
|
793
737
|
}, [slashCommands])
|
|
794
738
|
|
|
795
|
-
// Persist on every messages change. Sources + sendCount live in refs,
|
|
796
|
-
// so we read their current values at write time.
|
|
797
|
-
const persist = useCallback(
|
|
798
|
-
(nextMessages: Message[]) => {
|
|
799
|
-
savePersistedChat(source, {
|
|
800
|
-
messages: nextMessages,
|
|
801
|
-
sources: Array.from(sourcesMapRef.current.entries()),
|
|
802
|
-
refs: Array.from(refsMapRef.current.entries()),
|
|
803
|
-
sendCount: sendCountRef.current,
|
|
804
|
-
})
|
|
805
|
-
},
|
|
806
|
-
[source],
|
|
807
|
-
)
|
|
808
|
-
|
|
809
739
|
const {
|
|
810
740
|
messages,
|
|
811
741
|
isTyping,
|
|
@@ -813,16 +743,32 @@ export function useSseChatAdapter(
|
|
|
813
743
|
sendMessage: chatSendMessage,
|
|
814
744
|
stopMessage: chatStopMessage,
|
|
815
745
|
clearMessages: chatClearMessages,
|
|
746
|
+
hydrateMessages,
|
|
816
747
|
hasMessages,
|
|
817
748
|
} = useChat({
|
|
818
749
|
useMock: false,
|
|
819
750
|
assistantName: 'Mingo AI',
|
|
820
751
|
streamFn,
|
|
821
|
-
initialMessages: persistedRef.current.messages,
|
|
822
|
-
onMessagesChange: persist,
|
|
823
752
|
})
|
|
824
753
|
|
|
825
|
-
|
|
754
|
+
// ─── Server hydration (history SSOT) ───
|
|
755
|
+
// Mount-time rebuild of the message list from the server transcript — the
|
|
756
|
+
// single store of conversation history. Extracted to its own hook; see
|
|
757
|
+
// `use-chat-history-hydration.ts` for the full contract + failure
|
|
758
|
+
// semantics (a miss starts the UI empty, never loses server context).
|
|
759
|
+
const historyUrl =
|
|
760
|
+
runtime.endpoints.chatHistoryUrl ??
|
|
761
|
+
`${runtime.endpoints.chatStreamUrl.replace(/\/+$/, '')}/history`
|
|
762
|
+
const { isHydratingHistory, hydratedKeyRef } = useChatHistoryHydration({
|
|
763
|
+
active,
|
|
764
|
+
source,
|
|
765
|
+
historyUrl,
|
|
766
|
+
conversationIdRef,
|
|
767
|
+
refsMapRef,
|
|
768
|
+
sendCountRef,
|
|
769
|
+
hydrateMessages,
|
|
770
|
+
bumpMetaTick,
|
|
771
|
+
})
|
|
826
772
|
|
|
827
773
|
// Index sources/refs/scrollAnchor by USER-SEND count (`sendIdx`), not
|
|
828
774
|
// by assistant-message count. Each user send produces exactly ONE
|
|
@@ -1008,17 +954,17 @@ export function useSseChatAdapter(
|
|
|
1008
954
|
refsMapRef.current.clear()
|
|
1009
955
|
metaMapRef.current.clear()
|
|
1010
956
|
sendCountRef.current = 0
|
|
957
|
+
// New chat = drop the stored conversation id. The old row stays frozen
|
|
958
|
+
// server-side; the NEXT send goes out id-less and the server mints a
|
|
959
|
+
// fresh conversation (echoed back and re-captured then).
|
|
960
|
+
conversationIdRef.current = null
|
|
961
|
+
conversationStorage.clear()
|
|
962
|
+
hydratedKeyRef.current = null
|
|
1011
963
|
setStreamingPhase('idle')
|
|
1012
964
|
// Force the latestMeta useMemo to recompute with the cleared map.
|
|
1013
965
|
bumpMetaTick()
|
|
1014
966
|
chatClearMessages()
|
|
1015
|
-
|
|
1016
|
-
if (typeof window !== 'undefined') {
|
|
1017
|
-
try {
|
|
1018
|
-
window.localStorage.removeItem(chatStorageKey(source))
|
|
1019
|
-
} catch {}
|
|
1020
|
-
}
|
|
1021
|
-
}, [chatClearMessages, source, bumpMetaTick])
|
|
967
|
+
}, [chatClearMessages, conversationStorage, hydratedKeyRef, bumpMetaTick])
|
|
1022
968
|
|
|
1023
969
|
// Reset to idle whenever both flags drop off.
|
|
1024
970
|
useEffect(() => {
|
|
@@ -1047,6 +993,8 @@ export function useSseChatAdapter(
|
|
|
1047
993
|
stopMessage,
|
|
1048
994
|
clearMessages,
|
|
1049
995
|
streamingPhase,
|
|
996
|
+
/** True while the mount-time rebuild from the server transcript runs. */
|
|
997
|
+
isHydratingHistory,
|
|
1050
998
|
/** Provider key for the lib's `<ModelDisplay>` icon. */
|
|
1051
999
|
currentProvider: latestMeta?.provider ?? null,
|
|
1052
1000
|
currentModelLabel: latestMeta?.modelLabel ?? null,
|
|
@@ -1061,12 +1009,11 @@ export function useSseChatAdapter(
|
|
|
1061
1009
|
* token counts). null until the trailing usage frame lands. */
|
|
1062
1010
|
currentUsageBreakdown: latestMeta?.breakdown ?? null,
|
|
1063
1011
|
// ─── Dialog management — stubs for v1 ────────────────────────────────
|
|
1064
|
-
// Guide mode
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
// `dialogs.length === 0`.
|
|
1012
|
+
// Guide mode keeps ONE server-side conversation per stored id
|
|
1013
|
+
// (`chat_conversations`, hydrated on mount). Surfacing multiple threads
|
|
1014
|
+
// as a structured dialog list is a follow-up; for now the shape is
|
|
1015
|
+
// satisfied with empty defaults so the unified contract type-checks and
|
|
1016
|
+
// EmbeddableChat hides sidebar affordances when `dialogs.length === 0`.
|
|
1070
1017
|
dialogs: SSE_EMPTY_DIALOGS,
|
|
1071
1018
|
activeDialogId: null,
|
|
1072
1019
|
selectDialog: noopSelectDialog,
|
|
@@ -251,6 +251,12 @@ export interface UnifiedChatState {
|
|
|
251
251
|
/** Granular phase for the "Thinking..."/"Streaming..." status row above input. */
|
|
252
252
|
streamingPhase: StreamingPhase
|
|
253
253
|
|
|
254
|
+
/** True while the adapter is rebuilding the message list from the
|
|
255
|
+
* server-side transcript store (SSE/Guide mount-time hydration). Optional —
|
|
256
|
+
* transports without server hydration (NATS manages its own dialog
|
|
257
|
+
* loading) never set it. */
|
|
258
|
+
isHydratingHistory?: boolean
|
|
259
|
+
|
|
254
260
|
// ─── Actions ──────────────────────────────────────────────────────────────
|
|
255
261
|
sendMessage: (
|
|
256
262
|
text: string,
|