@oxy.so/bloom 3.3.0 → 3.5.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/adoption-matrix.mdx +2 -2
- package/docs/composer-panel.mdx +34 -2
- package/lib/commonjs/composer-panel/AddMenu.js +18 -0
- package/lib/commonjs/composer-panel/AddMenu.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPillBase.js +89 -9
- package/lib/commonjs/composer-panel/ComposerPillBase.js.map +1 -1
- package/lib/module/composer-panel/AddMenu.js +18 -0
- package/lib/module/composer-panel/AddMenu.js.map +1 -1
- package/lib/module/composer-panel/ComposerPillBase.js +89 -9
- package/lib/module/composer-panel/ComposerPillBase.js.map +1 -1
- package/lib/typescript/commonjs/composer-panel/AddMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPillBase.d.ts +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPillBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/types.d.ts +39 -1
- package/lib/typescript/commonjs/composer-panel/types.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/AddMenu.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerPillBase.d.ts +1 -1
- package/lib/typescript/module/composer-panel/ComposerPillBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/types.d.ts +39 -1
- package/lib/typescript/module/composer-panel/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/composer-panel/AddMenu.tsx +17 -0
- package/src/composer-panel/ComposerPillBase.tsx +107 -9
- package/src/composer-panel/types.ts +48 -1
|
@@ -43,7 +43,10 @@ const EASE_OUT = Easing.bezier(0, 0, 0.58, 1);
|
|
|
43
43
|
const GLASS_MS = 480;
|
|
44
44
|
/** `max-width: 639px`. */
|
|
45
45
|
const COMPACT_WIDTH = 640;
|
|
46
|
+
/** The pill at one line: the field's 20px line box plus 8px padding either side, in a 52 box. */
|
|
46
47
|
const PILL_HEIGHT = 52;
|
|
48
|
+
/** One line of `body-regular`, which is what the field was fixed at. */
|
|
49
|
+
const LINE_HEIGHT = 20;
|
|
47
50
|
|
|
48
51
|
const DEFAULT_LABELS: Required<ComposerPillLabels> = {
|
|
49
52
|
message: 'Message',
|
|
@@ -155,6 +158,13 @@ function ModelRow({
|
|
|
155
158
|
}) {
|
|
156
159
|
const [hovered, setHovered] = useState(false);
|
|
157
160
|
const [focused, setFocused] = useState(false);
|
|
161
|
+
/**
|
|
162
|
+
* The field's measured content height, from `onContentSizeChange`.
|
|
163
|
+
*
|
|
164
|
+
* Measured rather than counted: a draft wraps, so the number of LINES is not
|
|
165
|
+
* the number of newlines, and only the field knows how its own text laid out.
|
|
166
|
+
*/
|
|
167
|
+
const [contentHeight, setContentHeight] = useState(LINE_HEIGHT);
|
|
158
168
|
return (
|
|
159
169
|
<Pressable
|
|
160
170
|
{...dataHook('bloomComposerRow')}
|
|
@@ -409,6 +419,9 @@ export function ComposerPillBase({
|
|
|
409
419
|
inputRef,
|
|
410
420
|
labels: labelOverrides,
|
|
411
421
|
style,
|
|
422
|
+
maxLines = 8,
|
|
423
|
+
emptyAction,
|
|
424
|
+
onKeyPress: onKeyPressProp,
|
|
412
425
|
testID,
|
|
413
426
|
}: ComposerPillProps) {
|
|
414
427
|
useComposerWebCss();
|
|
@@ -438,6 +451,13 @@ export function ComposerPillBase({
|
|
|
438
451
|
},
|
|
439
452
|
});
|
|
440
453
|
const [focused, setFocused] = useState(false);
|
|
454
|
+
/**
|
|
455
|
+
* The field's measured content height, from `onContentSizeChange`.
|
|
456
|
+
*
|
|
457
|
+
* Measured rather than counted: a draft wraps, so the number of LINES is not
|
|
458
|
+
* the number of newlines, and only the field knows how its own text laid out.
|
|
459
|
+
*/
|
|
460
|
+
const [contentHeight, setContentHeight] = useState(LINE_HEIGHT);
|
|
441
461
|
const [modelWidth, setModelWidth] = useState(0);
|
|
442
462
|
|
|
443
463
|
// The chat's add surface is a step lighter than the Composer Panel's:
|
|
@@ -466,21 +486,79 @@ export function ComposerPillBase({
|
|
|
466
486
|
|
|
467
487
|
const onKeyPress = useCallback(
|
|
468
488
|
(event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
|
469
|
-
|
|
470
|
-
|
|
489
|
+
/*
|
|
490
|
+
* The host sees the key first, and may take it.
|
|
491
|
+
*
|
|
492
|
+
* A composer with a suggestion list over it needs the arrows, Enter and
|
|
493
|
+
* Escape to drive the list, not the field — and there was no way to ask
|
|
494
|
+
* for them, so a host could only offer a list its keyboard could not
|
|
495
|
+
* reach. It runs BEFORE the Enter rule below and a `defaultPrevented`
|
|
496
|
+
* event stops here, which is the only ordering that lets the list win
|
|
497
|
+
* Enter while the field keeps it the rest of the time.
|
|
498
|
+
*/
|
|
499
|
+
onKeyPressProp?.(event);
|
|
500
|
+
if (event.defaultPrevented) return;
|
|
501
|
+
|
|
502
|
+
const native: TextInputKeyPressEventData & { shiftKey?: boolean; isComposing?: boolean } = event.nativeEvent;
|
|
503
|
+
/*
|
|
504
|
+
* Shift+Enter is a NEWLINE, not a send.
|
|
505
|
+
*
|
|
506
|
+
* This handler used to read only the key, so a person holding shift to
|
|
507
|
+
* break a line sent the half-written message instead — on a field that
|
|
508
|
+
* could not have held the line anyway. `ComposerPanelBase` has always
|
|
509
|
+
* had the clause (`… || native.shiftKey || …`); the pill did not, and
|
|
510
|
+
* the two composers disagreeing about the most-used key in a composer
|
|
511
|
+
* was a difference nobody chose.
|
|
512
|
+
*/
|
|
513
|
+
if (
|
|
514
|
+
!IS_WEB
|
|
515
|
+
|| native.key !== 'Enter'
|
|
516
|
+
|| native.shiftKey
|
|
517
|
+
|| native.isComposing
|
|
518
|
+
|| disabled
|
|
519
|
+
|| busy
|
|
520
|
+
) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
471
523
|
event.preventDefault();
|
|
472
524
|
submit();
|
|
473
525
|
},
|
|
474
|
-
[submit, disabled, busy],
|
|
526
|
+
[submit, disabled, busy, onKeyPressProp],
|
|
475
527
|
);
|
|
476
528
|
|
|
529
|
+
/*
|
|
530
|
+
* How tall the pill is: one line, or as many as the draft needs up to
|
|
531
|
+
* `maxLines`.
|
|
532
|
+
*
|
|
533
|
+
* It was `height: PILL_HEIGHT` with a `height: 20` field and no `multiline`
|
|
534
|
+
* — under react-native-web that is an `<input>`, which cannot hold a newline
|
|
535
|
+
* and cannot grow. A composer for an assistant has to hold a paragraph, so
|
|
536
|
+
* the field is multiline and the pill measures it.
|
|
537
|
+
*
|
|
538
|
+
* The radius stays at 9999 while it is one line and squares off to 26 beyond
|
|
539
|
+
* that: a fully-round multi-line box puts the first and last lines inside
|
|
540
|
+
* the curve, where the text meets the edge.
|
|
541
|
+
*/
|
|
542
|
+
const lines = Math.min(maxLines, Math.max(1, Math.ceil(contentHeight / LINE_HEIGHT) || 1));
|
|
543
|
+
const fieldHeight = lines * LINE_HEIGHT;
|
|
544
|
+
/*
|
|
545
|
+
* At one line the pill is exactly what it always was. Its 52 comes from the
|
|
546
|
+
* CONTROLS — a 36px disc inside 8px padding — not from the 20px field, so
|
|
547
|
+
* growing from the field's height would have SHRUNK the resting pill to 36.
|
|
548
|
+
* Each line after the first adds its own height and nothing else moves.
|
|
549
|
+
*/
|
|
550
|
+
const pillHeight = PILL_HEIGHT + (lines - 1) * LINE_HEIGHT;
|
|
551
|
+
const multiLine = lines > 1;
|
|
552
|
+
|
|
477
553
|
const pillStyle: WebCssStyle = {
|
|
478
554
|
width: '100%',
|
|
479
|
-
height:
|
|
555
|
+
height: pillHeight,
|
|
480
556
|
flexDirection: 'row',
|
|
481
|
-
|
|
557
|
+
// The controls sit with the LAST line once the field has grown, which is
|
|
558
|
+
// where the caret is; centred, they would drift to the middle of the draft.
|
|
559
|
+
alignItems: multiLine ? 'flex-end' : 'center',
|
|
482
560
|
gap: 10,
|
|
483
|
-
borderRadius: 9999,
|
|
561
|
+
borderRadius: multiLine ? 26 : 9999,
|
|
484
562
|
padding: 8,
|
|
485
563
|
backgroundColor: surface ? palette.surface : 'transparent',
|
|
486
564
|
boxShadow: surface ? palette.shadowXs : undefined,
|
|
@@ -488,10 +566,12 @@ export function ComposerPillBase({
|
|
|
488
566
|
};
|
|
489
567
|
|
|
490
568
|
const fieldStyle: WebCssStyle = {
|
|
491
|
-
height:
|
|
569
|
+
height: fieldHeight,
|
|
492
570
|
minWidth: 0,
|
|
493
571
|
flex: 1,
|
|
494
|
-
|
|
572
|
+
// The fade that hints at overflowing text is a ONE-LINE affordance: on a
|
|
573
|
+
// grown field it would dim the end of every line.
|
|
574
|
+
...(IS_WEB && !focused && !multiLine
|
|
495
575
|
? {
|
|
496
576
|
maskImage: 'linear-gradient(to right, #000 calc(100% - 32px), transparent 100%)',
|
|
497
577
|
WebkitMaskImage: 'linear-gradient(to right, #000 calc(100% - 32px), transparent 100%)',
|
|
@@ -531,9 +611,14 @@ export function ComposerPillBase({
|
|
|
531
611
|
selectionColor={palette.accent500}
|
|
532
612
|
cursorColor={palette.accent500}
|
|
533
613
|
returnKeyType="send"
|
|
614
|
+
multiline
|
|
615
|
+
onContentSizeChange={(event) => setContentHeight(event.nativeEvent.contentSize.height)}
|
|
534
616
|
style={{
|
|
535
617
|
width: '100%',
|
|
536
|
-
height:
|
|
618
|
+
height: fieldHeight,
|
|
619
|
+
// react-native-web gives a multiline field a resize grip and a
|
|
620
|
+
// scrollbar it does not need: the pill owns the height.
|
|
621
|
+
...(IS_WEB ? { resize: 'none', overflowY: lines >= maxLines ? 'auto' : 'hidden' } : null),
|
|
537
622
|
padding: 0,
|
|
538
623
|
margin: 0,
|
|
539
624
|
...TYPE_SCALE['body-regular'],
|
|
@@ -574,8 +659,21 @@ export function ComposerPillBase({
|
|
|
574
659
|
palette={controlPalette}
|
|
575
660
|
/>
|
|
576
661
|
</View>
|
|
662
|
+
{/* What the trailing control is, in the order the states rank.
|
|
663
|
+
*
|
|
664
|
+
* Stop wins: a turn in flight is the one thing a person needs to be
|
|
665
|
+
* able to reach, and `StopButton` deliberately takes no `disabled`.
|
|
666
|
+
*
|
|
667
|
+
* Then `emptyAction`, if the host gave one and there is nothing to
|
|
668
|
+
* send. An assistant with a voice mode puts it here — the slot where
|
|
669
|
+
* send WOULD be, which is where the thumb already is — and it is the
|
|
670
|
+
* host's control, not a Bloom affordance, because only the host knows
|
|
671
|
+
* whether it has one. Without it the send button simply sits there
|
|
672
|
+
* disabled, as it always did. */}
|
|
577
673
|
{busy && onStop ? (
|
|
578
674
|
<StopButton onPress={onStop} label={labels.stop} palette={palette} />
|
|
675
|
+
) : emptyAction !== undefined && text.trim() === '' ? (
|
|
676
|
+
emptyAction
|
|
579
677
|
) : (
|
|
580
678
|
<SendButton disabled={disabled} onPress={submit} label={labels.send} palette={palette} />
|
|
581
679
|
)}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode, RefObject } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
NativeSyntheticEvent,
|
|
4
|
+
StyleProp,
|
|
5
|
+
TextInput,
|
|
6
|
+
TextInputKeyPressEventData,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
3
9
|
|
|
4
10
|
import type { Props as IconProps } from '../icons/shared';
|
|
5
11
|
|
|
@@ -33,6 +39,15 @@ export interface ComposerPanelAddMenuRow {
|
|
|
33
39
|
iconSize?: 20 | 24;
|
|
34
40
|
/** Your own 24px artwork, drawn instead of `icon`. */
|
|
35
41
|
image?: ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Draw the row as a SWITCH, ticked or not, instead of an action.
|
|
44
|
+
*
|
|
45
|
+
* Omit it and the row is an action, as every row was before. A host whose
|
|
46
|
+
* menu holds toggles — web search on, deep research off — could otherwise
|
|
47
|
+
* only offer rows that changed something and then looked identical either
|
|
48
|
+
* way.
|
|
49
|
+
*/
|
|
50
|
+
checked?: boolean;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
export interface ComposerPanelAddMenuGroup {
|
|
@@ -395,6 +410,38 @@ export interface ComposerPillProps {
|
|
|
395
410
|
*/
|
|
396
411
|
glass?: boolean;
|
|
397
412
|
|
|
413
|
+
/**
|
|
414
|
+
* How many lines the pill grows to before the field scrolls instead. Default
|
|
415
|
+
* `8`.
|
|
416
|
+
*
|
|
417
|
+
* The pill is one line at rest and measures its own content as the draft
|
|
418
|
+
* wraps — a composer for an assistant has to hold a paragraph. `1` keeps the
|
|
419
|
+
* fixed single-line box this had before it could grow.
|
|
420
|
+
*/
|
|
421
|
+
maxLines?: number;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Drawn where send would be, while the draft is empty and no turn is in
|
|
425
|
+
* flight.
|
|
426
|
+
*
|
|
427
|
+
* An assistant with a voice mode puts its call button here — the slot the
|
|
428
|
+
* thumb is already over. Omit it and send simply sits there disabled, as
|
|
429
|
+
* before. A stop always wins over it: a turn in flight is the one thing a
|
|
430
|
+
* person has to be able to reach.
|
|
431
|
+
*/
|
|
432
|
+
emptyAction?: ReactNode;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The field's key events, BEFORE the composer's own Enter rule, so a host
|
|
436
|
+
* can take a key from it.
|
|
437
|
+
*
|
|
438
|
+
* Calling `preventDefault()` stops the composer acting on that key. That is
|
|
439
|
+
* what a suggestion list over the composer needs: the arrows, Enter and
|
|
440
|
+
* Escape have to drive the list rather than the field, and without this a
|
|
441
|
+
* host could only offer a list its keyboard could not reach.
|
|
442
|
+
*/
|
|
443
|
+
onKeyPress?: (event: NativeSyntheticEvent<TextInputKeyPressEventData>) => void;
|
|
444
|
+
|
|
398
445
|
/** The field itself, for focus or a scripted demo. */
|
|
399
446
|
inputRef?: RefObject<TextInput | null>;
|
|
400
447
|
labels?: ComposerPillLabels;
|