@flamingo-stack/openframe-frontend-core 0.0.244 → 0.0.245
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-DV3YZWGN.cjs → chunk-3TDLFCCS.cjs} +33 -20
- package/dist/chunk-3TDLFCCS.cjs.map +1 -0
- package/dist/{chunk-ORJREQ2W.js → chunk-4XLJWX2N.js} +4 -4
- package/dist/{chunk-P5C732SC.js → chunk-7ZHFCEJH.js} +6 -6
- package/dist/{chunk-EJ7J4QAD.cjs → chunk-BKPS6M2F.cjs} +12 -12
- package/dist/{chunk-EJ7J4QAD.cjs.map → chunk-BKPS6M2F.cjs.map} +1 -1
- package/dist/{chunk-EPXROXYC.cjs → chunk-CZITXP77.cjs} +645 -713
- package/dist/chunk-CZITXP77.cjs.map +1 -0
- package/dist/{chunk-Z6GGYZDR.cjs → chunk-DUZBTUXF.cjs} +27 -27
- package/dist/{chunk-Z6GGYZDR.cjs.map → chunk-DUZBTUXF.cjs.map} +1 -1
- package/dist/{chunk-M7DPUWAL.js → chunk-FRRMXLU5.js} +120 -188
- package/dist/chunk-FRRMXLU5.js.map +1 -0
- package/dist/chunk-GIDH6UL4.js +287 -0
- package/dist/chunk-GIDH6UL4.js.map +1 -0
- package/dist/{chunk-PTSAF7T2.js → chunk-JJV3XGCY.js} +2 -2
- package/dist/{chunk-APGMNHPY.js → chunk-MKVKVJQQ.js} +32 -19
- package/dist/chunk-MKVKVJQQ.js.map +1 -0
- package/dist/chunk-ML2ROCYA.cjs +287 -0
- package/dist/chunk-ML2ROCYA.cjs.map +1 -0
- package/dist/{chunk-STKMIMLP.cjs → chunk-MP5HOP7N.cjs} +12 -12
- package/dist/{chunk-STKMIMLP.cjs.map → chunk-MP5HOP7N.cjs.map} +1 -1
- package/dist/{chunk-NKGNRFSS.js → chunk-SEVJT2HP.js} +8 -8
- package/dist/{chunk-FSS2MIEA.cjs → chunk-SGJBUL2Y.cjs} +20 -3
- package/dist/chunk-SGJBUL2Y.cjs.map +1 -0
- package/dist/{chunk-WD2MZVJI.js → chunk-V35PZSUA.js} +5 -5
- package/dist/{chunk-LJGBFD52.js → chunk-VAUI3CND.js} +18 -1
- package/dist/chunk-VAUI3CND.js.map +1 -0
- package/dist/{chunk-OB45JHDY.cjs → chunk-VFKQMAUF.cjs} +3 -3
- package/dist/{chunk-OB45JHDY.cjs.map → chunk-VFKQMAUF.cjs.map} +1 -1
- package/dist/{chunk-RJC3L5DT.cjs → chunk-VXP553WT.cjs} +7 -7
- package/dist/{chunk-RJC3L5DT.cjs.map → chunk-VXP553WT.cjs.map} +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +6 -16
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +5 -15
- package/dist/components/chat/types/entities/content-ref.d.ts +5 -20
- package/dist/components/chat/types/entities/content-ref.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +0 -1
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +7 -7
- package/dist/components/contact/index.js +6 -6
- package/dist/components/faq/faq-section.d.ts +7 -1
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +6 -6
- package/dist/components/features/index.js +5 -5
- package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
- package/dist/components/index.cjs +87 -93
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +31 -37
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +30 -30
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +11 -11
- package/dist/components/related-content/index.cjs +26 -0
- package/dist/components/related-content/index.cjs.map +1 -0
- package/dist/components/related-content/index.d.ts +2 -0
- package/dist/components/related-content/index.d.ts.map +1 -0
- package/dist/components/related-content/index.js +26 -0
- package/dist/components/related-content/index.js.map +1 -0
- package/dist/components/related-content/related-content-section.d.ts +149 -0
- package/dist/components/related-content/related-content-section.d.ts.map +1 -0
- package/dist/components/tickets/index.cjs +73 -73
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +15 -15
- package/dist/components/ui/index.cjs +9 -19
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +13 -23
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +5 -5
- package/dist/index.cjs +17 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +55 -49
- package/dist/types/content-ref.d.ts +35 -0
- package/dist/types/content-ref.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/content-ref-groups.d.ts +58 -0
- package/dist/utils/content-ref-groups.d.ts.map +1 -0
- package/dist/utils/extract-items.d.ts +18 -0
- package/dist/utils/extract-items.d.ts.map +1 -0
- package/dist/utils/index.cjs +81 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +4 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +74 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts +6 -0
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/suggestion-url.d.ts +26 -0
- package/dist/utils/suggestion-url.d.ts.map +1 -0
- package/package.json +7 -1
- package/src/components/chat/chat-message-list.tsx +49 -74
- package/src/components/chat/hooks/use-chat-card-item.ts +3 -38
- package/src/components/chat/types/entities/content-ref.ts +5 -21
- package/src/components/chat/utils/index.ts +0 -12
- package/src/components/faq/faq-section.tsx +69 -24
- package/src/components/features/policy-configuration-panel.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/related-content/index.ts +9 -0
- package/src/components/related-content/related-content-section.tsx +630 -0
- package/src/types/content-ref.ts +34 -0
- package/src/types/index.ts +1 -0
- package/src/utils/content-ref-groups.ts +89 -0
- package/src/utils/extract-items.ts +48 -0
- package/src/utils/index.ts +16 -1
- package/src/utils/list-url.ts +9 -0
- package/src/utils/suggestion-url.ts +39 -0
- package/dist/chunk-APGMNHPY.js.map +0 -1
- package/dist/chunk-DV3YZWGN.cjs.map +0 -1
- package/dist/chunk-EPXROXYC.cjs.map +0 -1
- package/dist/chunk-FSS2MIEA.cjs.map +0 -1
- package/dist/chunk-LJGBFD52.js.map +0 -1
- package/dist/chunk-M7DPUWAL.js.map +0 -1
- package/dist/components/chat/utils/history-merge.d.ts +0 -92
- package/dist/components/chat/utils/history-merge.d.ts.map +0 -1
- package/src/components/chat/__tests__/chat-message-list.test.tsx +0 -92
- package/src/components/chat/utils/__tests__/history-merge.test.ts +0 -339
- package/src/components/chat/utils/history-merge.ts +0 -255
- /package/dist/{chunk-ORJREQ2W.js.map → chunk-4XLJWX2N.js.map} +0 -0
- /package/dist/{chunk-P5C732SC.js.map → chunk-7ZHFCEJH.js.map} +0 -0
- /package/dist/{chunk-PTSAF7T2.js.map → chunk-JJV3XGCY.js.map} +0 -0
- /package/dist/{chunk-NKGNRFSS.js.map → chunk-SEVJT2HP.js.map} +0 -0
- /package/dist/{chunk-WD2MZVJI.js.map → chunk-V35PZSUA.js.map} +0 -0
package/dist/utils/list-url.d.ts
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
* Pure + server-safe (no React, no browser APIs) so the hub's server-side
|
|
28
28
|
* mappers can import it from `@flamingo-stack/openframe-frontend-core/utils`.
|
|
29
29
|
*/
|
|
30
|
+
/** Resolve a ContentRef rail-vocab type to its canonical `documentType`
|
|
31
|
+
* (registry vocabulary) — `blog_post_existing` → `blog_post`, everything
|
|
32
|
+
* else passes through. Exported so consumers comparing rail group keys to
|
|
33
|
+
* registry entityTypes (e.g. the related-content rail's same-type-first
|
|
34
|
+
* ordering) share THIS alias map instead of re-declaring it. */
|
|
35
|
+
export declare function canonicalContentRefType(contentRefType: string): string;
|
|
30
36
|
/**
|
|
31
37
|
* Build a list-API URL that returns full rows for the given ids, or `null`
|
|
32
38
|
* when the type has no list endpoint (the caller skips fetching rather than
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-url.d.ts","sourceRoot":"","sources":["../../src/utils/list-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"list-url.d.ts","sourceRoot":"","sources":["../../src/utils/list-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAWH;;;;iEAIiE;AACjE,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEtE;AA4BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,MAAM,GAAG,IAAI,CAa5F"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fetch-URL composer for the two suggestion sections (FaqSection +
|
|
3
|
+
* RelatedContentSection) — ONE owner of the entityType/entityId/count query
|
|
4
|
+
* shape so the sections can never drift. Pure + server-safe.
|
|
5
|
+
*
|
|
6
|
+
* Byte-parity contract with the original `buildFaqsUrl`: when only
|
|
7
|
+
* entityType/entityId are set, the param order is `entityType`, `entityId`;
|
|
8
|
+
* with nothing set, the bare `path` is returned (no `?`).
|
|
9
|
+
*/
|
|
10
|
+
export interface SuggestionUrlOptions {
|
|
11
|
+
/** Fetch-URL prefix for third-party embeds / reverse proxies
|
|
12
|
+
* ('' = same-origin relative). */
|
|
13
|
+
apiBaseUrl?: string;
|
|
14
|
+
/** Both required together for entity scope; partial → bare path. */
|
|
15
|
+
entityType?: string;
|
|
16
|
+
entityId?: number | string;
|
|
17
|
+
/** Maps to the server's `count` param. Undefined → param not sent
|
|
18
|
+
* (server default applies). */
|
|
19
|
+
count?: number;
|
|
20
|
+
/** Extra query params appended after the shared trio (e.g. the
|
|
21
|
+
* related-content `excludeTypes` deny-list). Empty/undefined values
|
|
22
|
+
* are skipped. */
|
|
23
|
+
extraParams?: Record<string, string | undefined>;
|
|
24
|
+
}
|
|
25
|
+
export declare function buildSuggestionUrl(path: string, opts?: SuggestionUrlOptions): string;
|
|
26
|
+
//# sourceMappingURL=suggestion-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggestion-url.d.ts","sourceRoot":"","sources":["../../src/utils/suggestion-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC;uCACmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC1B;oCACgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;uBAEmB;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CACjD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,MAAM,CAaxF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flamingo-stack/openframe-frontend-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.245",
|
|
4
4
|
"description": "Shared design system and components for all Flamingo platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,6 +60,12 @@
|
|
|
60
60
|
"require": "./dist/components/faq/index.cjs",
|
|
61
61
|
"default": "./dist/components/faq/index.js"
|
|
62
62
|
},
|
|
63
|
+
"./components/related-content": {
|
|
64
|
+
"types": "./dist/components/related-content/index.d.ts",
|
|
65
|
+
"import": "./dist/components/related-content/index.js",
|
|
66
|
+
"require": "./dist/components/related-content/index.cjs",
|
|
67
|
+
"default": "./dist/components/related-content/index.js"
|
|
68
|
+
},
|
|
63
69
|
"./components/faq/json-ld": {
|
|
64
70
|
"types": "./dist/components/faq/json-ld.d.ts",
|
|
65
71
|
"import": "./dist/components/faq/json-ld.js",
|
|
@@ -130,11 +130,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
130
130
|
onLoadMoreRef.current = onLoadMore
|
|
131
131
|
const isFetchingRef = useRef(isFetchingNextPage)
|
|
132
132
|
isFetchingRef.current = isFetchingNextPage
|
|
133
|
-
// Fresh dialogId for the IntersectionObserver callback (its effect must
|
|
134
|
-
// not depend on `dialogId` or it would re-create on every dialog switch
|
|
135
|
-
// mid-stream; the ref keeps the snapshot's dialog tag current).
|
|
136
|
-
const dialogIdRef = useRef(dialogId)
|
|
137
|
-
dialogIdRef.current = dialogId
|
|
138
133
|
|
|
139
134
|
// Tracks previous render state for explicit "force-stick" decisions
|
|
140
135
|
// (dialog change, first message, new user message). The library
|
|
@@ -145,19 +140,15 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
145
140
|
lastMessageId: string | undefined
|
|
146
141
|
}>({ dialogId: undefined, messageCount: 0, lastMessageId: undefined })
|
|
147
142
|
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// the top (and the still-visible sentinel never re-fires).
|
|
156
|
-
const loadOlderAnchorRef = useRef<{
|
|
157
|
-
dialogId: string | undefined
|
|
143
|
+
// Snapshot of the first message id + scrollHeight observed BEFORE
|
|
144
|
+
// a load-older prepend, so we can preserve the user's viewport
|
|
145
|
+
// position when older messages stream in above their current
|
|
146
|
+
// reading position.
|
|
147
|
+
const prependRef = useRef<{
|
|
148
|
+
firstMessageId: string | undefined
|
|
149
|
+
firstMessageContent: unknown
|
|
158
150
|
scrollHeight: number
|
|
159
|
-
|
|
160
|
-
} | null>(null)
|
|
151
|
+
}>({ firstMessageId: undefined, firstMessageContent: undefined, scrollHeight: 0 })
|
|
161
152
|
|
|
162
153
|
// ---- Force-stick: dialog change / first-load / new user message --
|
|
163
154
|
useEffect(() => {
|
|
@@ -165,7 +156,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
165
156
|
|
|
166
157
|
const dialogChanged = dialogId !== prevRenderRef.current.dialogId
|
|
167
158
|
const prevCount = prevRenderRef.current.messageCount
|
|
168
|
-
const prevLastId = prevRenderRef.current.lastMessageId
|
|
169
159
|
const newCount = messages.length
|
|
170
160
|
const currentLastId = messages[messages.length - 1]?.id
|
|
171
161
|
prevRenderRef.current = { dialogId, messageCount: newCount, lastMessageId: currentLastId }
|
|
@@ -187,18 +177,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
187
177
|
// user message DID arrive). When the user just sent a
|
|
188
178
|
// message, they expect their bubble pinned to the bottom,
|
|
189
179
|
// regardless of where they were scrolled.
|
|
190
|
-
|
|
191
|
-
// The tail must be located via the previous LAST message id,
|
|
192
|
-
// not `slice(prevCount)`: when load-older PREPENDS a page,
|
|
193
|
-
// count grows but the new messages are at the HEAD —
|
|
194
|
-
// `slice(prevCount)` would return already-rendered tail
|
|
195
|
-
// messages (which contain user bubbles) and force-scroll the
|
|
196
|
-
// reader away from the older page they just loaded. With the
|
|
197
|
-
// id-based slice a pure prepend yields an empty tail → no
|
|
198
|
-
// scroll, and the prepend-anchoring effect below keeps their
|
|
199
|
-
// viewport position.
|
|
200
|
-
const prevLastIdx = prevLastId ? messages.findIndex((m) => m.id === prevLastId) : -1
|
|
201
|
-
const newSlice = prevLastIdx >= 0 ? messages.slice(prevLastIdx + 1) : messages.slice(prevCount)
|
|
180
|
+
const newSlice = messages.slice(prevCount)
|
|
202
181
|
const hasNewUser = newSlice.some((m) => m.role === 'user')
|
|
203
182
|
if (hasNewUser) {
|
|
204
183
|
void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
|
|
@@ -251,35 +230,49 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
251
230
|
}, [autoScroll, messages, dialogId, scrollToBottom, contentRef])
|
|
252
231
|
|
|
253
232
|
// ---- Prepend anchoring (load-older) ------------------------------
|
|
254
|
-
// The library doesn't preserve user position when content
|
|
255
|
-
// the viewport. We do
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// write lands before paint (no visible jump).
|
|
259
|
-
// (new ids on top) and a boundary-merge (first bubble grows in place) —
|
|
260
|
-
// it measures total height delta, not message identity.
|
|
233
|
+
// The library doesn't preserve user position when content prepends
|
|
234
|
+
// ABOVE the viewport. We do it ourselves: snapshot scrollHeight
|
|
235
|
+
// BEFORE the new render, then add the delta after to keep the
|
|
236
|
+
// user on the same message they were reading. `useLayoutEffect`
|
|
237
|
+
// so the scrollTop write lands before paint (no visible jump).
|
|
261
238
|
useLayoutEffect(() => {
|
|
262
239
|
const el = scrollRef.current
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
240
|
+
if (!el) {
|
|
241
|
+
prependRef.current = { firstMessageId: undefined, firstMessageContent: undefined, scrollHeight: 0 }
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const currentFirstId = messages[0]?.id
|
|
246
|
+
const currentFirstContent = messages[0]?.content
|
|
247
|
+
const prevFirstId = prependRef.current.firstMessageId
|
|
248
|
+
const prevHeight = prependRef.current.scrollHeight
|
|
249
|
+
|
|
250
|
+
if (currentFirstId !== prevFirstId) {
|
|
251
|
+
// First-id changed → older messages prepended (verified by
|
|
252
|
+
// confirming the previous-first is still in the array at
|
|
253
|
+
// idx > 0; otherwise this is some other mutation).
|
|
254
|
+
if (prevFirstId && prevHeight > 0) {
|
|
255
|
+
const prevIdx = messages.findIndex((m) => m.id === prevFirstId)
|
|
256
|
+
if (prevIdx > 0) {
|
|
257
|
+
const addedHeight = el.scrollHeight - prevHeight
|
|
258
|
+
if (addedHeight > 0) el.scrollTop += addedHeight
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
prependRef.current = {
|
|
262
|
+
firstMessageId: currentFirstId,
|
|
263
|
+
firstMessageContent: currentFirstContent,
|
|
264
|
+
scrollHeight: el.scrollHeight,
|
|
265
|
+
}
|
|
269
266
|
return
|
|
270
267
|
}
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (addedHeight <= 0) return
|
|
280
|
-
loadOlderAnchorRef.current = null
|
|
281
|
-
el.scrollTop = anchor.scrollTop + addedHeight
|
|
282
|
-
}, [messages, dialogId, scrollRef])
|
|
268
|
+
// Always keep `scrollHeight` snapshot fresh so the next prepend
|
|
269
|
+
// computes the right delta. The id/content updates only when
|
|
270
|
+
// those actually change to avoid churn.
|
|
271
|
+
prependRef.current.scrollHeight = el.scrollHeight
|
|
272
|
+
if (currentFirstContent !== prependRef.current.firstMessageContent) {
|
|
273
|
+
prependRef.current.firstMessageContent = currentFirstContent
|
|
274
|
+
}
|
|
275
|
+
}, [messages, scrollRef])
|
|
283
276
|
|
|
284
277
|
// ---- Per-message scrollAnchor (server-driven) --------------------
|
|
285
278
|
// Server emits `scrollAnchor: 'top'` on the metadata leading frame
|
|
@@ -389,14 +382,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
389
382
|
// ---- Load-more (infinite-scroll UP) ------------------------------
|
|
390
383
|
// Distinct from stick-to-bottom; uses its own IntersectionObserver
|
|
391
384
|
// on the top sentinel.
|
|
392
|
-
//
|
|
393
|
-
// `isFetchingNextPage` is in the deps so the observer is RE-CREATED when a
|
|
394
|
-
// fetch completes: IntersectionObserver fires no new callback while an
|
|
395
|
-
// element stays continuously intersecting, so after an older page lands
|
|
396
|
-
// with the sentinel still in view (small prepend, or rootMargin overlap)
|
|
397
|
-
// nothing would re-trigger until the user scrolled it out and back in.
|
|
398
|
-
// Re-observing re-evaluates the current intersection and fires again if
|
|
399
|
-
// the sentinel is still visible — load continues until it's pushed out.
|
|
400
385
|
useEffect(() => {
|
|
401
386
|
const scrollContainer = scrollRef.current
|
|
402
387
|
const sentinelElement = sentinelRef.current
|
|
@@ -407,16 +392,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
407
392
|
const entry = entries[0]
|
|
408
393
|
if (!entry) return
|
|
409
394
|
if (entry.isIntersecting && !isFetchingRef.current) {
|
|
410
|
-
// Snapshot pre-load geometry so the prepend-anchoring layout
|
|
411
|
-
// effect can re-pin scrollTop once the taller content commits.
|
|
412
|
-
const el = scrollRef.current
|
|
413
|
-
if (el) {
|
|
414
|
-
loadOlderAnchorRef.current = {
|
|
415
|
-
dialogId: dialogIdRef.current,
|
|
416
|
-
scrollHeight: el.scrollHeight,
|
|
417
|
-
scrollTop: el.scrollTop,
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
395
|
onLoadMoreRef.current?.()
|
|
421
396
|
}
|
|
422
397
|
},
|
|
@@ -424,7 +399,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
424
399
|
)
|
|
425
400
|
observer.observe(sentinelElement)
|
|
426
401
|
return () => observer.disconnect()
|
|
427
|
-
}, [hasNextPage,
|
|
402
|
+
}, [hasNextPage, scrollRef])
|
|
428
403
|
|
|
429
404
|
// Expose the scroll container ref to parents that need it (rare,
|
|
430
405
|
// but the existing public contract). Library's `scrollRef` is a
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { useQuery } from '@tanstack/react-query'
|
|
23
23
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
24
24
|
import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
|
|
25
|
+
import { extractItems, extractItemId } from '../../../utils/extract-items'
|
|
25
26
|
|
|
26
27
|
export interface UseChatCardItemResult<T = unknown> {
|
|
27
28
|
item: T | undefined
|
|
@@ -29,44 +30,8 @@ export interface UseChatCardItemResult<T = unknown> {
|
|
|
29
30
|
isError: boolean
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* point — callers always read `Item[]`. */
|
|
35
|
-
function extractItems(data: unknown): unknown[] {
|
|
36
|
-
if (!data || typeof data !== 'object') return []
|
|
37
|
-
const obj = data as Record<string, unknown>
|
|
38
|
-
if (Array.isArray(obj.items)) return obj.items
|
|
39
|
-
if (Array.isArray(obj.posts)) return obj.posts
|
|
40
|
-
if (Array.isArray(obj.campaigns)) return obj.campaigns
|
|
41
|
-
if (Array.isArray(obj.completed) || Array.isArray(obj.inProgress)) {
|
|
42
|
-
// Delivery endpoint splits into completed/inProgress arrays — flatten.
|
|
43
|
-
const completed = Array.isArray(obj.completed) ? obj.completed : []
|
|
44
|
-
const inProgress = Array.isArray(obj.inProgress) ? obj.inProgress : []
|
|
45
|
-
return [...completed, ...inProgress]
|
|
46
|
-
}
|
|
47
|
-
if (Array.isArray(obj.data)) return obj.data
|
|
48
|
-
if (Array.isArray(data)) return data
|
|
49
|
-
return []
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Extract a stable id from a fetched item — different shapes use
|
|
53
|
-
* `id` vs `external_id`. Used by the chat loader to match the fetched
|
|
54
|
-
* array element back to the marker's id. */
|
|
55
|
-
function extractItemId(type: string, item: unknown): string | null {
|
|
56
|
-
if (!item || typeof item !== 'object') return null
|
|
57
|
-
const obj = item as Record<string, unknown>
|
|
58
|
-
if (type === 'roadmap_item' || type === 'delivery_item' || type === 'internal_task') {
|
|
59
|
-
const ext = obj.external_id
|
|
60
|
-
if (typeof ext === 'string') return ext
|
|
61
|
-
}
|
|
62
|
-
// RoadmapItem-shaped responses use `id` to carry the external_id; the
|
|
63
|
-
// mapper renames external_id→id at the API boundary. Treat both id and
|
|
64
|
-
// external_id as primary-key candidates for these clickup-backed types.
|
|
65
|
-
const id = obj.id
|
|
66
|
-
if (typeof id === 'string') return id
|
|
67
|
-
if (typeof id === 'number') return String(id)
|
|
68
|
-
return null
|
|
69
|
-
}
|
|
33
|
+
// `extractItems` / `extractItemId` hoisted to `src/utils/extract-items.ts`
|
|
34
|
+
// (shared with the related-content rail — react-query-free home).
|
|
70
35
|
|
|
71
36
|
export function useChatCardItem<T = unknown>(
|
|
72
37
|
type: string,
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Lifted from `lib/data/investor-update-utils-shared.ts` in the hub
|
|
7
|
-
* (the server-side data utils in the same file stay hub-side; only the
|
|
8
|
-
* TYPE moves here).
|
|
2
|
+
* RELOCATED — the canonical `ContentRef` now lives at `src/types/content-ref.ts`
|
|
3
|
+
* (server-safe home, shared by the related-content rail + chat entities).
|
|
4
|
+
* This stub keeps the chat entities barrel + `entities/investor-update.ts`
|
|
5
|
+
* compiling unchanged. One definition package-wide.
|
|
9
6
|
*/
|
|
10
|
-
|
|
11
|
-
export interface ContentRef {
|
|
12
|
-
type: string;
|
|
13
|
-
id: string;
|
|
14
|
-
slug: string;
|
|
15
|
-
url: string;
|
|
16
|
-
targetPlatform?: string | null;
|
|
17
|
-
title: string;
|
|
18
|
-
summary?: string;
|
|
19
|
-
image_url?: string;
|
|
20
|
-
image_bg_color?: string;
|
|
21
|
-
visibility: 'public' | 'internal';
|
|
22
|
-
display_order: number;
|
|
23
|
-
}
|
|
7
|
+
export type { ContentRef, ContentRefWithReason } from '../../../../types/content-ref';
|
|
@@ -31,18 +31,6 @@ export {
|
|
|
31
31
|
extractIncompleteMessageState,
|
|
32
32
|
} from './extract-incomplete-message-state'
|
|
33
33
|
|
|
34
|
-
// History <-> realtime reconciliation
|
|
35
|
-
export {
|
|
36
|
-
mergeHistoryWithRealtime,
|
|
37
|
-
computeHistoryPrepend,
|
|
38
|
-
flattenMessagePagesChronological,
|
|
39
|
-
maxPersistedStreamSeq,
|
|
40
|
-
SYNTHETIC_REALTIME_ID_PREFIXES,
|
|
41
|
-
type MergeableChatMessage,
|
|
42
|
-
type HistoryMergeInput,
|
|
43
|
-
type HistoryPrependResult,
|
|
44
|
-
} from './history-merge'
|
|
45
|
-
|
|
46
34
|
// Tool call helpers
|
|
47
35
|
export { getCommandText } from './tool-call-helpers'
|
|
48
36
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import React from 'react'
|
|
3
|
+
import React, { useMemo } from 'react'
|
|
4
4
|
import type { Faq } from '../../types/faq'
|
|
5
5
|
import { FaqAccordion, type FaqItem } from '../faq-accordion'
|
|
6
6
|
import { useSelfFetch } from '../../hooks/use-self-fetch'
|
|
7
|
+
import { buildSuggestionUrl } from '../../utils/suggestion-url'
|
|
7
8
|
import { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'
|
|
8
9
|
|
|
9
10
|
export interface FaqSectionProps {
|
|
@@ -28,16 +29,26 @@ export interface FaqSectionProps {
|
|
|
28
29
|
/** Overrides for the JSON-LD's name/description/url. */
|
|
29
30
|
jsonLd?: FaqSchemaOptions
|
|
30
31
|
className?: string
|
|
32
|
+
/** Maps to /api/faqs `?count=` (the 5-tier fill target). Absent → param
|
|
33
|
+
* not sent (server default applies). */
|
|
34
|
+
minResults?: number
|
|
35
|
+
/** Fetch-URL prefix for third-party embeds / reverse proxies
|
|
36
|
+
* ('' = same-origin relative). */
|
|
37
|
+
apiBaseUrl?: string
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
const DEFAULT_HEADING_TEXT = 'Frequently Asked Questions'
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
/** URL composition shared with RelatedContentSection (`buildSuggestionUrl`)
|
|
43
|
+
* — byte-identical to the historical `buildFaqsUrl` output when
|
|
44
|
+
* `minResults`/`apiBaseUrl` are absent. */
|
|
45
|
+
function buildFaqsUrl(
|
|
46
|
+
entityType?: string,
|
|
47
|
+
entityId?: number | string,
|
|
48
|
+
minResults?: number,
|
|
49
|
+
apiBaseUrl = '',
|
|
50
|
+
): string {
|
|
51
|
+
return buildSuggestionUrl('/api/faqs', { apiBaseUrl, entityType, entityId, count: minResults })
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
function groupBySection(faqs: Faq[]): Array<{ section: string | null; items: FaqItem[] }> {
|
|
@@ -97,9 +108,16 @@ export function FaqSection({
|
|
|
97
108
|
emitJsonLd = false,
|
|
98
109
|
jsonLd,
|
|
99
110
|
className,
|
|
111
|
+
minResults,
|
|
112
|
+
apiBaseUrl = '',
|
|
100
113
|
}: FaqSectionProps) {
|
|
101
|
-
const url = buildFaqsUrl(entityType, entityId)
|
|
102
|
-
|
|
114
|
+
const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl)
|
|
115
|
+
// Memoized — useSelfFetch re-syncs on [initialData]; a fresh per-render
|
|
116
|
+
// wrapper object would setState-loop under re-rendering parents.
|
|
117
|
+
const initialData = useMemo<FaqsResponse | undefined>(
|
|
118
|
+
() => (initialFaqs ? { faqs: initialFaqs } : undefined),
|
|
119
|
+
[initialFaqs],
|
|
120
|
+
)
|
|
103
121
|
const { data, isLoading, error } = useSelfFetch<FaqsResponse>(url, { initialData })
|
|
104
122
|
|
|
105
123
|
const faqs = data?.faqs ?? []
|
|
@@ -116,7 +134,24 @@ export function FaqSection({
|
|
|
116
134
|
return null
|
|
117
135
|
}
|
|
118
136
|
|
|
137
|
+
// Embedded mode, zero FAQs after load: render nothing — an embedded host
|
|
138
|
+
// page (blog/case-study detail) must not show an empty section shell.
|
|
139
|
+
// The standalone /faqs page (heading set) keeps its existing behavior.
|
|
140
|
+
if (!isLoading && !error && faqs.length === 0 && !showHeading) {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
119
144
|
if (isLoading && faqs.length === 0) {
|
|
145
|
+
// Embedded mode renders skeletons WITHOUT the standalone page shell —
|
|
146
|
+
// a host detail page already provides <main> + gutters; nesting them
|
|
147
|
+
// here would emit invalid markup and double padding.
|
|
148
|
+
if (!showHeading) {
|
|
149
|
+
return (
|
|
150
|
+
<div className={className}>
|
|
151
|
+
<FaqSkeleton />
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
120
155
|
return (
|
|
121
156
|
<main className={className ?? 'bg-ods-bg'}>
|
|
122
157
|
<div className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto">
|
|
@@ -136,23 +171,33 @@ export function FaqSection({
|
|
|
136
171
|
|
|
137
172
|
const schema = emitJsonLd ? buildFaqJsonLdFromFaqs(faqs, jsonLd) : null
|
|
138
173
|
|
|
174
|
+
const groupNodes = groups.map(({ section, items }) => (
|
|
175
|
+
<div key={section || 'default'} className="space-y-4">
|
|
176
|
+
{section && (
|
|
177
|
+
<h2 className="text-h2 tracking-[-0.04em] text-ods-text-primary mb-3 md:mb-4">
|
|
178
|
+
{section}
|
|
179
|
+
</h2>
|
|
180
|
+
)}
|
|
181
|
+
<FaqAccordion items={items} />
|
|
182
|
+
</div>
|
|
183
|
+
))
|
|
184
|
+
|
|
185
|
+
// Embedded mode (heading === null): no <main> page shell, no page gutters —
|
|
186
|
+
// the host page owns layout. Standalone mode keeps the historical markup.
|
|
187
|
+
const body = showHeading ? (
|
|
188
|
+
<main className={className ?? 'bg-ods-bg'}>
|
|
189
|
+
<div className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10">
|
|
190
|
+
{headingNode}
|
|
191
|
+
{groupNodes}
|
|
192
|
+
</div>
|
|
193
|
+
</main>
|
|
194
|
+
) : (
|
|
195
|
+
<section className={className ?? 'space-y-10'}>{groupNodes}</section>
|
|
196
|
+
)
|
|
197
|
+
|
|
139
198
|
return (
|
|
140
199
|
<>
|
|
141
|
-
|
|
142
|
-
<div className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10">
|
|
143
|
-
{showHeading && headingNode}
|
|
144
|
-
{groups.map(({ section, items }) => (
|
|
145
|
-
<div key={section || 'default'} className="space-y-4">
|
|
146
|
-
{section && (
|
|
147
|
-
<h2 className="text-h2 tracking-[-0.04em] text-ods-text-primary mb-3 md:mb-4">
|
|
148
|
-
{section}
|
|
149
|
-
</h2>
|
|
150
|
-
)}
|
|
151
|
-
<FaqAccordion items={items} />
|
|
152
|
-
</div>
|
|
153
|
-
))}
|
|
154
|
-
</div>
|
|
155
|
-
</main>
|
|
200
|
+
{body}
|
|
156
201
|
{schema && (
|
|
157
202
|
<script
|
|
158
203
|
type="application/ld+json"
|
package/src/components/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from './faq-accordion'
|
|
|
26
26
|
// dedicated server-safe subpath "./components/faq/json-ld" (built without the
|
|
27
27
|
// client banner under the server/universal block of tsup.config.ts).
|
|
28
28
|
export * from './faq'
|
|
29
|
+
export * from './related-content'
|
|
29
30
|
export * from './filter-chip'
|
|
30
31
|
export * from './footer'
|
|
31
32
|
export * from './unified-filter-logic'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// CLIENT subpath barrel — `@flamingo-stack/openframe-frontend-core/components/related-content`
|
|
2
|
+
export {
|
|
3
|
+
RelatedContentSection,
|
|
4
|
+
type RelatedContentSectionProps,
|
|
5
|
+
type CardLinkProvider,
|
|
6
|
+
type CardLinkProviderProps,
|
|
7
|
+
type CardLinkAnchorProps,
|
|
8
|
+
type AdminCampaignCardSlot,
|
|
9
|
+
} from './related-content-section'
|