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