@oxy.so/bloom 4.12.0 → 4.13.0
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/docs/ai-chat.mdx +31 -5
- package/lib/commonjs/ai-chat/AiChatContainer.js +390 -116
- package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatShell.js +211 -176
- package/lib/commonjs/ai-chat/AiChatShell.js.map +1 -1
- package/lib/commonjs/ai-chat/context.js +10 -1
- package/lib/commonjs/ai-chat/context.js.map +1 -1
- package/lib/commonjs/ai-chat/shared.js +41 -2
- package/lib/commonjs/ai-chat/shared.js.map +1 -1
- package/lib/module/ai-chat/AiChatContainer.js +394 -120
- package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/module/ai-chat/AiChatShell.js +213 -178
- package/lib/module/ai-chat/AiChatShell.js.map +1 -1
- package/lib/module/ai-chat/context.js +9 -0
- package/lib/module/ai-chat/context.js.map +1 -1
- package/lib/module/ai-chat/shared.js +40 -1
- package/lib/module/ai-chat/shared.js.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts +8 -3
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/context.d.ts +14 -0
- package/lib/typescript/commonjs/ai-chat/context.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/shared.d.ts +24 -2
- package/lib/typescript/commonjs/ai-chat/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/types.d.ts +19 -0
- package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts +8 -3
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatShell.d.ts +1 -1
- package/lib/typescript/module/ai-chat/AiChatShell.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/context.d.ts +14 -0
- package/lib/typescript/module/ai-chat/context.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/shared.d.ts +24 -2
- package/lib/typescript/module/ai-chat/shared.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/types.d.ts +19 -0
- package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ai-chat/AiChatContainer.tsx +361 -88
- package/src/ai-chat/AiChatShell.tsx +39 -13
- package/src/ai-chat/context.ts +15 -0
- package/src/ai-chat/shared.ts +45 -2
- package/src/ai-chat/types.ts +19 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { ScrollView,
|
|
3
|
+
import React, { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { ScrollView, View } from 'react-native';
|
|
5
5
|
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
|
|
6
6
|
import { AgentThinking } from "../agent-thinking/index.js";
|
|
7
7
|
import { Breadcrumb, BreadcrumbItem } from "../breadcrumb/index.js";
|
|
@@ -12,8 +12,9 @@ import { EdgeScrim, SCRIM_TAIL_RATIO } from "../page-header/EdgeScrim.js";
|
|
|
12
12
|
import { parseRgba } from "../theme/color-utils.js";
|
|
13
13
|
import { useIsomorphicLayoutEffect } from "../theme/use-isomorphic-layout-effect.js";
|
|
14
14
|
import { GlyphAction } from "./AiChatControls.js";
|
|
15
|
-
import { AiChatFloatingChromeContext } from "./context.js";
|
|
16
|
-
import { CONTAINER_RADIUS, dataHook, IS_WEB, useAiChatPalette, useAiChatWebCss } from "./shared.js";
|
|
15
|
+
import { AiChatDocumentGutterContext, AiChatFloatingChromeContext, useAiChatShell } from "./context.js";
|
|
16
|
+
import { CONTAINER_RADIUS, dataHook, DOCUMENT_LAYER, IS_WEB, SHELL_GUTTER, useAiChatPalette, useAiChatWebCss } from "./shared.js";
|
|
17
|
+
import { WEB_POSITION_STICKY, webViewportHeightMinus } from "../styles/web-view-style.js";
|
|
17
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
19
|
const DEFAULT_LABELS = {
|
|
19
20
|
breadcrumb: 'Chat location',
|
|
@@ -23,12 +24,8 @@ const DEFAULT_LABELS = {
|
|
|
23
24
|
|
|
24
25
|
/** The thread's own top padding: the gap between the header and the first turn. */
|
|
25
26
|
const THREAD_PAD_TOP = 16;
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
* the last turn at rest ends where the fade begins, and the distance over which
|
|
29
|
-
* the bottom fade ramps in as the transcript slides under it.
|
|
30
|
-
*/
|
|
31
|
-
const FOOTER_FADE = 24;
|
|
27
|
+
/** The footer's top padding: the gap between the last turn at rest and the composer. */
|
|
28
|
+
const FOOTER_GAP = 12;
|
|
32
29
|
/** Below this many px of overlap an edge counts as clear (sub-pixel scroll noise). */
|
|
33
30
|
const EDGE_THRESHOLD = 1;
|
|
34
31
|
const HIDDEN_FROM_A11Y = {
|
|
@@ -62,14 +59,19 @@ const HIDDEN_FROM_A11Y = {
|
|
|
62
59
|
* an edge scrim of the surface (opaque at the top, gone
|
|
63
60
|
* `SCRIM_TAIL_RATIO` below the block) fades in over the first 16px
|
|
64
61
|
* of transcript scrolled under it
|
|
65
|
-
* footer absolute at the bottom,
|
|
66
|
-
*
|
|
67
|
-
* 24px of transcript below the fold
|
|
62
|
+
* footer absolute at the bottom, with no band of its own: the transcript
|
|
63
|
+
* passes behind the composer and stays in sight around it
|
|
68
64
|
*
|
|
69
65
|
* Both blocks are `pointerEvents="box-none"`, so the transcript under their
|
|
70
66
|
* empty space still scrolls and takes clicks. The measured heights reach an
|
|
71
67
|
* `AiChatThread` child through context (its content padding) and any other
|
|
72
68
|
* child through `useAiChatChromeInsets()`.
|
|
69
|
+
*
|
|
70
|
+
* Under a shell with `scroll="document"` the chrome always floats, since the
|
|
71
|
+
* document is what moves: the header and the footer are `sticky` in the flow,
|
|
72
|
+
* pinned to the screen's top and bottom gutter, the `background` slot and the
|
|
73
|
+
* card's frame are layers the size of the screen, and the card grows with the
|
|
74
|
+
* conversation. At rest nothing overlaps, so the insets are zero.
|
|
73
75
|
*/
|
|
74
76
|
export function AiChatContainer({
|
|
75
77
|
project,
|
|
@@ -97,6 +99,8 @@ export function AiChatContainer({
|
|
|
97
99
|
...DEFAULT_LABELS,
|
|
98
100
|
...labels
|
|
99
101
|
}), [labels]);
|
|
102
|
+
const documentScroll = useAiChatShell()?.documentScroll ?? false;
|
|
103
|
+
const gutterColor = useContext(AiChatDocumentGutterContext);
|
|
100
104
|
const hasCrumbRow = title != null || project != null || onShare || onMore || actions != null;
|
|
101
105
|
const crumbRow = hasCrumbRow ? /*#__PURE__*/_jsxs(View, {
|
|
102
106
|
pointerEvents: "box-none",
|
|
@@ -179,6 +183,35 @@ export function AiChatContainer({
|
|
|
179
183
|
borderRadius: CONTAINER_RADIUS,
|
|
180
184
|
backgroundColor: surface ? palette.secondary : 'transparent'
|
|
181
185
|
}, style];
|
|
186
|
+
if (documentScroll) {
|
|
187
|
+
return /*#__PURE__*/_jsx(FloatingChrome, {
|
|
188
|
+
documentScroll: true,
|
|
189
|
+
testID: testID,
|
|
190
|
+
rootStyle: [{
|
|
191
|
+
// Grows with the conversation, one screen at the least; nothing
|
|
192
|
+
// clips it, so the header and composer inside can stick.
|
|
193
|
+
minHeight: webViewportHeightMinus(SHELL_GUTTER * 2),
|
|
194
|
+
minWidth: 0,
|
|
195
|
+
flex: 1,
|
|
196
|
+
flexDirection: 'column',
|
|
197
|
+
borderRadius: CONTAINER_RADIUS,
|
|
198
|
+
backgroundColor: surface ? palette.secondary : 'transparent'
|
|
199
|
+
}, style],
|
|
200
|
+
backgroundLayer: background ? /*#__PURE__*/_jsx(View, {
|
|
201
|
+
pointerEvents: "none",
|
|
202
|
+
style: [DOCUMENT_LAYER, {
|
|
203
|
+
overflow: 'hidden'
|
|
204
|
+
}],
|
|
205
|
+
children: background
|
|
206
|
+
}) : null,
|
|
207
|
+
header: header,
|
|
208
|
+
crumbRow: crumbRow,
|
|
209
|
+
footerBody: footerBody,
|
|
210
|
+
fadeColor: surface ? palette.secondary : null,
|
|
211
|
+
gutterColor: gutterColor,
|
|
212
|
+
children: children
|
|
213
|
+
});
|
|
214
|
+
}
|
|
182
215
|
if (floatingChrome) {
|
|
183
216
|
return /*#__PURE__*/_jsx(FloatingChrome, {
|
|
184
217
|
testID: testID,
|
|
@@ -208,17 +241,24 @@ export function AiChatContainer({
|
|
|
208
241
|
})]
|
|
209
242
|
});
|
|
210
243
|
}
|
|
244
|
+
const NO_INSETS = {
|
|
245
|
+
top: 0,
|
|
246
|
+
bottom: 0
|
|
247
|
+
};
|
|
248
|
+
|
|
211
249
|
/**
|
|
212
|
-
* `AiChatContainer floatingChrome
|
|
213
|
-
*
|
|
214
|
-
* carries none of
|
|
250
|
+
* `AiChatContainer floatingChrome` (and every container under a document-
|
|
251
|
+
* scrolled shell): the same parts, the chrome floated over the transcript.
|
|
252
|
+
* Everything it measures lives here, so the stacked path above carries none of
|
|
253
|
+
* the state.
|
|
215
254
|
*
|
|
216
|
-
* The edge
|
|
217
|
-
* reports, so a scroll frame re-renders nothing; the
|
|
218
|
-
*
|
|
255
|
+
* The header's edge opacity is a shared value written from the thread's scroll
|
|
256
|
+
* reports, so a scroll frame re-renders nothing; the boolean only mounts and
|
|
257
|
+
* unmounts the scrim, and changes once per crossing. Same model as the
|
|
219
258
|
* sidebar's `SidebarScrollArea`, whose scrim this reuses.
|
|
220
259
|
*/
|
|
221
260
|
function FloatingChrome({
|
|
261
|
+
documentScroll = false,
|
|
222
262
|
testID,
|
|
223
263
|
rootStyle,
|
|
224
264
|
backgroundLayer,
|
|
@@ -226,32 +266,30 @@ function FloatingChrome({
|
|
|
226
266
|
crumbRow,
|
|
227
267
|
footerBody,
|
|
228
268
|
fadeColor,
|
|
269
|
+
gutterColor = null,
|
|
229
270
|
children
|
|
230
271
|
}) {
|
|
231
|
-
const [insets, setInsets] = useState(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
});
|
|
235
|
-
const [edges, setEdges] = useState({
|
|
236
|
-
top: false,
|
|
237
|
-
bottom: false
|
|
238
|
-
});
|
|
272
|
+
const [insets, setInsets] = useState(NO_INSETS);
|
|
273
|
+
const [headerHeight, setHeaderHeight] = useState(0);
|
|
274
|
+
const [scrolled, setScrolled] = useState(false);
|
|
239
275
|
const topFade = useSharedValue(0);
|
|
240
|
-
const bottomFade = useSharedValue(0);
|
|
241
276
|
const onHeaderLayout = useCallback(event => {
|
|
242
277
|
const top = Math.round(event.nativeEvent.layout.height);
|
|
243
|
-
|
|
278
|
+
setHeaderHeight(top);
|
|
279
|
+
// In the flow the header covers nothing at rest: no inset to publish.
|
|
280
|
+
if (!documentScroll) setInsets(previous => previous.top === top ? previous : {
|
|
244
281
|
...previous,
|
|
245
282
|
top
|
|
246
283
|
});
|
|
247
|
-
}, []);
|
|
284
|
+
}, [documentScroll]);
|
|
248
285
|
const onFooterLayout = useCallback(event => {
|
|
286
|
+
if (documentScroll) return;
|
|
249
287
|
const bottom = Math.round(event.nativeEvent.layout.height);
|
|
250
288
|
setInsets(previous => previous.bottom === bottom ? previous : {
|
|
251
289
|
...previous,
|
|
252
290
|
bottom
|
|
253
291
|
});
|
|
254
|
-
}, []);
|
|
292
|
+
}, [documentScroll]);
|
|
255
293
|
const reportScroll = useCallback(({
|
|
256
294
|
offset,
|
|
257
295
|
viewport,
|
|
@@ -260,18 +298,12 @@ function FloatingChrome({
|
|
|
260
298
|
const range = Math.max(0, content - viewport);
|
|
261
299
|
const y = Math.max(0, Math.min(offset, range));
|
|
262
300
|
const overflow = viewport > 0 && range > EDGE_THRESHOLD;
|
|
263
|
-
// The first turn rests THREAD_PAD_TOP below the header
|
|
264
|
-
//
|
|
265
|
-
// scrolled and each ramp spans the gap it is closing.
|
|
266
|
-
const top = overflow && y > EDGE_THRESHOLD;
|
|
267
|
-
const bottom = overflow && range - y > EDGE_THRESHOLD;
|
|
301
|
+
// The first turn rests THREAD_PAD_TOP below the header, so the overlap
|
|
302
|
+
// starts at the first px scrolled and the ramp spans the gap it closes.
|
|
268
303
|
topFade.value = overflow ? Math.min(1, y / THREAD_PAD_TOP) : 0;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
bottom
|
|
273
|
-
});
|
|
274
|
-
}, [topFade, bottomFade]);
|
|
304
|
+
const next = overflow && y > EDGE_THRESHOLD;
|
|
305
|
+
setScrolled(previous => previous === next ? previous : next);
|
|
306
|
+
}, [topFade]);
|
|
275
307
|
const chrome = useMemo(() => ({
|
|
276
308
|
insets,
|
|
277
309
|
reportScroll
|
|
@@ -284,92 +316,110 @@ function FloatingChrome({
|
|
|
284
316
|
const topStyle = useAnimatedStyle(() => ({
|
|
285
317
|
opacity: topFade.value * alpha
|
|
286
318
|
}), [topFade, alpha]);
|
|
287
|
-
const bottomStyle = useAnimatedStyle(() => ({
|
|
288
|
-
opacity: bottomFade.value * alpha
|
|
289
|
-
}), [bottomFade, alpha]);
|
|
290
319
|
const prefix = testID ?? 'ai-chat';
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
320
|
+
|
|
321
|
+
// Absolute over a card of fixed height; sticky in the flow of a card that
|
|
322
|
+
// grows with the document, pinned to the screen's gutter.
|
|
323
|
+
const headerPosition = documentScroll ? {
|
|
324
|
+
position: WEB_POSITION_STICKY,
|
|
325
|
+
top: SHELL_GUTTER,
|
|
326
|
+
zIndex: 2
|
|
327
|
+
} : {
|
|
328
|
+
position: 'absolute',
|
|
329
|
+
top: 0,
|
|
330
|
+
left: 0,
|
|
331
|
+
right: 0
|
|
332
|
+
};
|
|
333
|
+
const footerPosition = documentScroll ? {
|
|
334
|
+
position: WEB_POSITION_STICKY,
|
|
335
|
+
bottom: SHELL_GUTTER,
|
|
336
|
+
zIndex: 2
|
|
337
|
+
} : {
|
|
338
|
+
position: 'absolute',
|
|
339
|
+
left: 0,
|
|
340
|
+
right: 0,
|
|
341
|
+
bottom: 0
|
|
342
|
+
};
|
|
343
|
+
const top = /*#__PURE__*/_jsxs(View, {
|
|
344
|
+
testID: `${prefix}-chrome-top`,
|
|
345
|
+
pointerEvents: "box-none",
|
|
346
|
+
onLayout: onHeaderLayout,
|
|
347
|
+
style: headerPosition,
|
|
348
|
+
children: [solid && scrolled && headerHeight > 0 ? /*#__PURE__*/_jsx(Animated.View, {
|
|
349
|
+
testID: `${prefix}-fade-top`,
|
|
350
|
+
pointerEvents: "none",
|
|
351
|
+
...HIDDEN_FROM_A11Y,
|
|
352
|
+
style: [{
|
|
304
353
|
position: 'absolute',
|
|
305
354
|
top: 0,
|
|
306
355
|
left: 0,
|
|
307
|
-
right: 0
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
testID: `${prefix}-chrome-bottom`,
|
|
356
|
+
right: 0,
|
|
357
|
+
// Past the block's own bottom: a ramp that ends where the layout
|
|
358
|
+
// does is a line across the transcript again.
|
|
359
|
+
height: Math.round(headerHeight * (1 + SCRIM_TAIL_RATIO)),
|
|
360
|
+
// The card's own corners, since nothing clips a document card.
|
|
361
|
+
borderTopLeftRadius: CONTAINER_RADIUS,
|
|
362
|
+
borderTopRightRadius: CONTAINER_RADIUS,
|
|
363
|
+
overflow: 'hidden'
|
|
364
|
+
}, topStyle],
|
|
365
|
+
children: /*#__PURE__*/_jsx(EdgeScrim, {
|
|
366
|
+
color: solid
|
|
367
|
+
})
|
|
368
|
+
}) : null, header, crumbRow]
|
|
369
|
+
});
|
|
370
|
+
const bottom = /*#__PURE__*/_jsx(View, {
|
|
371
|
+
testID: `${prefix}-chrome-bottom`,
|
|
372
|
+
pointerEvents: "box-none",
|
|
373
|
+
onLayout: onFooterLayout,
|
|
374
|
+
style: footerPosition,
|
|
375
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
328
376
|
pointerEvents: "box-none",
|
|
329
|
-
onLayout: onFooterLayout,
|
|
330
377
|
style: {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
378
|
+
width: '100%',
|
|
379
|
+
flexDirection: 'column',
|
|
380
|
+
gap: 10,
|
|
381
|
+
paddingLeft: 10,
|
|
382
|
+
paddingRight: 10,
|
|
383
|
+
paddingTop: FOOTER_GAP,
|
|
384
|
+
paddingBottom: 10
|
|
335
385
|
},
|
|
336
|
-
children:
|
|
337
|
-
|
|
386
|
+
children: footerBody
|
|
387
|
+
})
|
|
388
|
+
});
|
|
389
|
+
const thread = /*#__PURE__*/_jsx(AiChatFloatingChromeContext.Provider, {
|
|
390
|
+
value: chrome,
|
|
391
|
+
children: children
|
|
392
|
+
});
|
|
393
|
+
if (documentScroll) {
|
|
394
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
395
|
+
testID: testID,
|
|
396
|
+
style: rootStyle,
|
|
397
|
+
children: [backgroundLayer, gutterColor ?
|
|
398
|
+
/*#__PURE__*/
|
|
399
|
+
// The card's frame, pinned to the screen: a ring of the gutter's
|
|
400
|
+
// colour painted over whatever of the conversation scrolls past the
|
|
401
|
+
// card's visible edge, so the card keeps its rounded corners and its
|
|
402
|
+
// gutter at the top and the bottom of the screen. Clipped to the
|
|
403
|
+
// gutter sideways (the family sheet), so it never reaches the columns
|
|
404
|
+
// beside the card.
|
|
405
|
+
_jsx(View, {
|
|
406
|
+
testID: `${prefix}-frame`,
|
|
338
407
|
pointerEvents: "none",
|
|
339
408
|
...HIDDEN_FROM_A11Y,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
style: {
|
|
355
|
-
flex: 1,
|
|
356
|
-
backgroundColor: solid
|
|
357
|
-
}
|
|
358
|
-
})]
|
|
359
|
-
}) : null, /*#__PURE__*/_jsx(View, {
|
|
360
|
-
pointerEvents: "box-none",
|
|
361
|
-
style: {
|
|
362
|
-
width: '100%',
|
|
363
|
-
flexDirection: 'column',
|
|
364
|
-
gap: 10,
|
|
365
|
-
paddingLeft: 10,
|
|
366
|
-
paddingRight: 10,
|
|
367
|
-
paddingTop: FOOTER_FADE,
|
|
368
|
-
paddingBottom: 10
|
|
369
|
-
},
|
|
370
|
-
children: footerBody
|
|
371
|
-
})]
|
|
372
|
-
})]
|
|
409
|
+
...dataHook('bloomAiChatFrame'),
|
|
410
|
+
style: [DOCUMENT_LAYER, {
|
|
411
|
+
zIndex: 1,
|
|
412
|
+
boxShadow: `0 0 0 ${SHELL_GUTTER}px ${gutterColor}`
|
|
413
|
+
}]
|
|
414
|
+
}) : null, top, thread, bottom]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
418
|
+
testID: testID,
|
|
419
|
+
style: [rootStyle, {
|
|
420
|
+
position: 'relative'
|
|
421
|
+
}],
|
|
422
|
+
children: [backgroundLayer, thread, top, bottom]
|
|
373
423
|
});
|
|
374
424
|
}
|
|
375
425
|
|
|
@@ -402,7 +452,19 @@ function FloatingChrome({
|
|
|
402
452
|
* motion they pass under it. It also reports its position so the container
|
|
403
453
|
* knows when to fade each edge. Anywhere else nothing of this exists.
|
|
404
454
|
*/
|
|
405
|
-
export const AiChatThread = /*#__PURE__*/forwardRef(function AiChatThread({
|
|
455
|
+
export const AiChatThread = /*#__PURE__*/forwardRef(function AiChatThread(props, ref) {
|
|
456
|
+
const documentScroll = useAiChatShell()?.documentScroll ?? false;
|
|
457
|
+
return documentScroll ? /*#__PURE__*/_jsx(DocumentThread, {
|
|
458
|
+
ref: ref,
|
|
459
|
+
...props
|
|
460
|
+
}) : /*#__PURE__*/_jsx(ScrollThread, {
|
|
461
|
+
ref: ref,
|
|
462
|
+
...props
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
/** The thread inside a card of fixed height: a `ScrollView` of its own. */
|
|
467
|
+
const ScrollThread = /*#__PURE__*/forwardRef(function ScrollThread({
|
|
406
468
|
children,
|
|
407
469
|
autoFollow = true,
|
|
408
470
|
followAnimated = true,
|
|
@@ -598,4 +660,216 @@ export const AiChatThread = /*#__PURE__*/forwardRef(function AiChatThread({
|
|
|
598
660
|
children: children
|
|
599
661
|
});
|
|
600
662
|
});
|
|
663
|
+
|
|
664
|
+
/** The window's scroll position and extent, px. */
|
|
665
|
+
function readWindow() {
|
|
666
|
+
const page = document.scrollingElement ?? document.documentElement;
|
|
667
|
+
return {
|
|
668
|
+
y: window.scrollY,
|
|
669
|
+
viewport: window.innerHeight,
|
|
670
|
+
end: page.scrollHeight
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function scrollWindowTo(top, animated) {
|
|
674
|
+
window.scrollTo({
|
|
675
|
+
top,
|
|
676
|
+
behavior: animated ? 'smooth' : 'auto'
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The thread under a document-scrolled shell (web): no scroller of its own —
|
|
682
|
+
* the turns grow the page and the window scrolls them. Same props, same handle,
|
|
683
|
+
* same behaviour: it follows the newest turn, fires `onStartReached` near the
|
|
684
|
+
* top, holds the reader still while a page lands above, and reports its
|
|
685
|
+
* position to the container.
|
|
686
|
+
*
|
|
687
|
+
* Offsets keep the thread's own frame: `0` is the thread's top, so a row's
|
|
688
|
+
* `onLayout` y is still a `scrollToOffset` target, and `contentSize` runs to the
|
|
689
|
+
* END OF THE PAGE, so `contentSize − offset − viewport` is still the distance
|
|
690
|
+
* left to scroll. The browser's scroll anchoring is off inside the thread (the
|
|
691
|
+
* family sheet): it would move the page for growth the thread already accounts
|
|
692
|
+
* for, and only in some browsers.
|
|
693
|
+
*/
|
|
694
|
+
const DocumentThread = /*#__PURE__*/forwardRef(function DocumentThread({
|
|
695
|
+
children,
|
|
696
|
+
autoFollow = true,
|
|
697
|
+
followAnimated = true,
|
|
698
|
+
followThreshold,
|
|
699
|
+
onStartReached,
|
|
700
|
+
onStartReachedThreshold = 300,
|
|
701
|
+
maintainStartPosition = false,
|
|
702
|
+
onScroll,
|
|
703
|
+
style,
|
|
704
|
+
testID
|
|
705
|
+
}, ref) {
|
|
706
|
+
const chrome = useContext(AiChatFloatingChromeContext);
|
|
707
|
+
const nodeRef = useRef(null);
|
|
708
|
+
const lastHeight = useRef(0);
|
|
709
|
+
/** An `onStartReached` is out and its page has not landed yet. */
|
|
710
|
+
const pageOut = useRef(false);
|
|
711
|
+
/** The reader has left the top zone, so the next approach may fire again. */
|
|
712
|
+
const startArmed = useRef(true);
|
|
713
|
+
|
|
714
|
+
/** The thread's top, px from the top of the page. */
|
|
715
|
+
const threadTop = useCallback(() => {
|
|
716
|
+
const node = nodeRef.current;
|
|
717
|
+
return node ? node.getBoundingClientRect().top + window.scrollY : 0;
|
|
718
|
+
}, []);
|
|
719
|
+
useImperativeHandle(ref, () => ({
|
|
720
|
+
scrollToEnd: options => scrollWindowTo(readWindow().end, options?.animated ?? true),
|
|
721
|
+
scrollToOffset: ({
|
|
722
|
+
offset: y,
|
|
723
|
+
animated = false
|
|
724
|
+
}) => scrollWindowTo(threadTop() + y, animated),
|
|
725
|
+
getScrollView: () => null
|
|
726
|
+
}), [threadTop]);
|
|
727
|
+
|
|
728
|
+
// The latest handlers, read by one window listener that is never rebound.
|
|
729
|
+
const handlers = useRef({
|
|
730
|
+
chrome,
|
|
731
|
+
onScroll,
|
|
732
|
+
onStartReached,
|
|
733
|
+
onStartReachedThreshold
|
|
734
|
+
});
|
|
735
|
+
useIsomorphicLayoutEffect(() => {
|
|
736
|
+
handlers.current = {
|
|
737
|
+
chrome,
|
|
738
|
+
onScroll,
|
|
739
|
+
onStartReached,
|
|
740
|
+
onStartReachedThreshold
|
|
741
|
+
};
|
|
742
|
+
});
|
|
743
|
+
const report = useCallback(() => {
|
|
744
|
+
const {
|
|
745
|
+
chrome: frame,
|
|
746
|
+
onScroll: listener,
|
|
747
|
+
onStartReached: startReached,
|
|
748
|
+
onStartReachedThreshold: zone
|
|
749
|
+
} = handlers.current;
|
|
750
|
+
const {
|
|
751
|
+
y,
|
|
752
|
+
viewport,
|
|
753
|
+
end
|
|
754
|
+
} = readWindow();
|
|
755
|
+
frame?.reportScroll({
|
|
756
|
+
offset: y,
|
|
757
|
+
viewport,
|
|
758
|
+
content: end
|
|
759
|
+
});
|
|
760
|
+
const top = threadTop();
|
|
761
|
+
const offset = y - top;
|
|
762
|
+
if (startReached) {
|
|
763
|
+
if (offset <= zone) {
|
|
764
|
+
if (startArmed.current) {
|
|
765
|
+
startArmed.current = false;
|
|
766
|
+
pageOut.current = true;
|
|
767
|
+
startReached();
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
startArmed.current = true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
listener?.({
|
|
774
|
+
nativeEvent: {
|
|
775
|
+
contentOffset: {
|
|
776
|
+
x: 0,
|
|
777
|
+
y: offset
|
|
778
|
+
},
|
|
779
|
+
layoutMeasurement: {
|
|
780
|
+
width: window.innerWidth,
|
|
781
|
+
height: viewport
|
|
782
|
+
},
|
|
783
|
+
contentSize: {
|
|
784
|
+
width: window.innerWidth,
|
|
785
|
+
height: end - top
|
|
786
|
+
},
|
|
787
|
+
contentInset: {
|
|
788
|
+
top: 0,
|
|
789
|
+
left: 0,
|
|
790
|
+
bottom: 0,
|
|
791
|
+
right: 0
|
|
792
|
+
},
|
|
793
|
+
zoomScale: 1
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}, [threadTop]);
|
|
797
|
+
useEffect(() => {
|
|
798
|
+
let frame = 0;
|
|
799
|
+
const onWindowChange = () => {
|
|
800
|
+
if (frame !== 0) return;
|
|
801
|
+
frame = requestAnimationFrame(() => {
|
|
802
|
+
frame = 0;
|
|
803
|
+
report();
|
|
804
|
+
});
|
|
805
|
+
};
|
|
806
|
+
window.addEventListener('scroll', onWindowChange, {
|
|
807
|
+
passive: true
|
|
808
|
+
});
|
|
809
|
+
window.addEventListener('resize', onWindowChange);
|
|
810
|
+
report();
|
|
811
|
+
return () => {
|
|
812
|
+
cancelAnimationFrame(frame);
|
|
813
|
+
window.removeEventListener('scroll', onWindowChange);
|
|
814
|
+
window.removeEventListener('resize', onWindowChange);
|
|
815
|
+
};
|
|
816
|
+
}, [report]);
|
|
817
|
+
const onLayout = useCallback(event => {
|
|
818
|
+
const height = event.nativeEvent.layout.height;
|
|
819
|
+
const previous = lastHeight.current;
|
|
820
|
+
lastHeight.current = height;
|
|
821
|
+
report();
|
|
822
|
+
// The page is shared with whatever was on it before — another route
|
|
823
|
+
// left it scrolled wherever it was — so a following thread opens at its
|
|
824
|
+
// newest turn rather than at that offset.
|
|
825
|
+
if (previous === 0) {
|
|
826
|
+
if (autoFollow) scrollWindowTo(readWindow().end, false);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (height <= previous + 0.5) return;
|
|
830
|
+
const growth = height - previous;
|
|
831
|
+
|
|
832
|
+
// A page landing above the reader: keep the turn they are on where it is.
|
|
833
|
+
if (maintainStartPosition && pageOut.current) {
|
|
834
|
+
pageOut.current = false;
|
|
835
|
+
window.scrollBy({
|
|
836
|
+
top: growth,
|
|
837
|
+
behavior: 'auto'
|
|
838
|
+
});
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
pageOut.current = false;
|
|
842
|
+
if (!autoFollow) return;
|
|
843
|
+
if (followThreshold !== undefined) {
|
|
844
|
+
// Measured against the page BEFORE the growth: was the reader near the
|
|
845
|
+
// end when this arrived?
|
|
846
|
+
const {
|
|
847
|
+
y,
|
|
848
|
+
viewport,
|
|
849
|
+
end
|
|
850
|
+
} = readWindow();
|
|
851
|
+
if (end - growth - y - viewport > followThreshold) return;
|
|
852
|
+
}
|
|
853
|
+
scrollWindowTo(readWindow().end, followAnimated);
|
|
854
|
+
}, [autoFollow, followAnimated, followThreshold, maintainStartPosition, report]);
|
|
855
|
+
return /*#__PURE__*/_jsx(View, {
|
|
856
|
+
ref: nodeRef,
|
|
857
|
+
testID: testID,
|
|
858
|
+
...dataHook('bloomAiChatThread'),
|
|
859
|
+
onLayout: onLayout,
|
|
860
|
+
style: [{
|
|
861
|
+
// Fills the card between the header and the composer, and anchors a
|
|
862
|
+
// short exchange just above the composer, as the scroller does.
|
|
863
|
+
width: '100%',
|
|
864
|
+
flexGrow: 1,
|
|
865
|
+
flexDirection: 'column',
|
|
866
|
+
justifyContent: 'flex-end',
|
|
867
|
+
gap: 12,
|
|
868
|
+
paddingLeft: 16,
|
|
869
|
+
paddingRight: 16,
|
|
870
|
+
paddingTop: THREAD_PAD_TOP
|
|
871
|
+
}, style],
|
|
872
|
+
children: children
|
|
873
|
+
});
|
|
874
|
+
});
|
|
601
875
|
//# sourceMappingURL=AiChatContainer.js.map
|