@oxy.so/bloom 3.2.1 → 3.4.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 +3 -3
- package/docs/ai-chat.mdx +31 -3
- package/docs/composer-panel.mdx +41 -6
- package/lib/commonjs/ai-chat/AiChatContainer.js +122 -14
- package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatShell.js +46 -6
- package/lib/commonjs/ai-chat/AiChatShell.js.map +1 -1
- package/lib/commonjs/ai-chat/context.js +5 -1
- package/lib/commonjs/ai-chat/context.js.map +1 -1
- package/lib/commonjs/ai-chat/index.js +7 -0
- package/lib/commonjs/ai-chat/index.js.map +1 -1
- package/lib/commonjs/ai-chat/index.web.js +7 -0
- package/lib/commonjs/ai-chat/index.web.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerControls.js +61 -0
- package/lib/commonjs/composer-panel/ComposerControls.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPanelBase.js +12 -4
- package/lib/commonjs/composer-panel/ComposerPanelBase.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPillBase.js +186 -85
- package/lib/commonjs/composer-panel/ComposerPillBase.js.map +1 -1
- package/lib/commonjs/composer-panel/EffortSlider.js +24 -15
- package/lib/commonjs/composer-panel/EffortSlider.js.map +1 -1
- package/lib/commonjs/composer-panel/ModelPickerBase.js +15 -4
- package/lib/commonjs/composer-panel/ModelPickerBase.js.map +1 -1
- package/lib/module/ai-chat/AiChatContainer.js +122 -14
- package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/module/ai-chat/AiChatShell.js +46 -6
- package/lib/module/ai-chat/AiChatShell.js.map +1 -1
- package/lib/module/ai-chat/context.js +5 -1
- package/lib/module/ai-chat/context.js.map +1 -1
- package/lib/module/ai-chat/index.js +1 -0
- package/lib/module/ai-chat/index.js.map +1 -1
- package/lib/module/ai-chat/index.web.js +1 -0
- package/lib/module/ai-chat/index.web.js.map +1 -1
- package/lib/module/composer-panel/ComposerControls.js +60 -0
- package/lib/module/composer-panel/ComposerControls.js.map +1 -1
- package/lib/module/composer-panel/ComposerPanelBase.js +13 -5
- package/lib/module/composer-panel/ComposerPanelBase.js.map +1 -1
- package/lib/module/composer-panel/ComposerPillBase.js +187 -86
- package/lib/module/composer-panel/ComposerPillBase.js.map +1 -1
- package/lib/module/composer-panel/EffortSlider.js +24 -15
- package/lib/module/composer-panel/EffortSlider.js.map +1 -1
- package/lib/module/composer-panel/ModelPickerBase.js +15 -4
- package/lib/module/composer-panel/ModelPickerBase.js.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts +28 -5
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts +11 -2
- package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/context.d.ts +19 -1
- package/lib/typescript/commonjs/ai-chat/context.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/index.d.ts +3 -1
- package/lib/typescript/commonjs/ai-chat/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/index.web.d.ts +3 -1
- package/lib/typescript/commonjs/ai-chat/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/types.d.ts +101 -3
- package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerControls.d.ts +14 -0
- package/lib/typescript/commonjs/composer-panel/ComposerControls.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPanelBase.d.ts +3 -2
- package/lib/typescript/commonjs/composer-panel/ComposerPanelBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPillBase.d.ts +3 -2
- package/lib/typescript/commonjs/composer-panel/ComposerPillBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/EffortSlider.d.ts +10 -2
- package/lib/typescript/commonjs/composer-panel/EffortSlider.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ModelPickerBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/types.d.ts +70 -20
- package/lib/typescript/commonjs/composer-panel/types.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts +28 -5
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatShell.d.ts +11 -2
- package/lib/typescript/module/ai-chat/AiChatShell.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/context.d.ts +19 -1
- package/lib/typescript/module/ai-chat/context.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/index.d.ts +3 -1
- package/lib/typescript/module/ai-chat/index.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/index.web.d.ts +3 -1
- package/lib/typescript/module/ai-chat/index.web.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/types.d.ts +101 -3
- package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerControls.d.ts +14 -0
- package/lib/typescript/module/composer-panel/ComposerControls.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerPanelBase.d.ts +3 -2
- package/lib/typescript/module/composer-panel/ComposerPanelBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerPillBase.d.ts +3 -2
- package/lib/typescript/module/composer-panel/ComposerPillBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/EffortSlider.d.ts +10 -2
- package/lib/typescript/module/composer-panel/EffortSlider.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ModelPickerBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/types.d.ts +70 -20
- package/lib/typescript/module/composer-panel/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/support/adoption-matrix.ts +664 -0
- package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
- package/src/__tests__/support/composite.ts +68 -0
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/__tests__/support/press-host.ts +30 -0
- package/src/__tests__/support/rendered-style.ts +99 -0
- package/src/__tests__/support/unread-hook-fixture.ts +33 -0
- package/src/ai-chat/AiChatContainer.tsx +137 -16
- package/src/ai-chat/AiChatShell.tsx +59 -5
- package/src/ai-chat/context.ts +19 -1
- package/src/ai-chat/index.ts +3 -0
- package/src/ai-chat/index.web.ts +3 -0
- package/src/ai-chat/types.ts +104 -3
- package/src/composer-panel/ComposerControls.tsx +54 -0
- package/src/composer-panel/ComposerPanelBase.tsx +13 -5
- package/src/composer-panel/ComposerPillBase.tsx +178 -66
- package/src/composer-panel/EffortSlider.tsx +38 -18
- package/src/composer-panel/ModelPickerBase.tsx +23 -7
- package/src/composer-panel/types.ts +71 -20
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +7682 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
package/src/ai-chat/context.ts
CHANGED
|
@@ -35,7 +35,11 @@ export function useAiChatPlatform(): AiChatPlatform {
|
|
|
35
35
|
return platform;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* What `AiChatShell` publishes: read it with `useAiChatShell()`. The chat
|
|
40
|
+
* container's mobile header runs on it, and so does anything a host needs to
|
|
41
|
+
* know about the frame around its own nav.
|
|
42
|
+
*/
|
|
39
43
|
export interface AiChatShellState {
|
|
40
44
|
/** Below `xl`: the panel is a drawer and the mobile header shows. */
|
|
41
45
|
compact: boolean;
|
|
@@ -43,7 +47,21 @@ export interface AiChatShellState {
|
|
|
43
47
|
navCollapsed: boolean;
|
|
44
48
|
hasNav: boolean;
|
|
45
49
|
hasPanel: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the nav is on screen at all: `true` from `lg` up (in flow) and,
|
|
52
|
+
* below it, only while the drawer is open.
|
|
53
|
+
*
|
|
54
|
+
* A closed drawer stays MOUNTED — it is translated offscreen, not removed —
|
|
55
|
+
* so a nav rendered into `mobileSidebar` is still focusable and still read by
|
|
56
|
+
* a screen reader unless its own tree says otherwise. This is the signal to
|
|
57
|
+
* hide it by: skip the tab order and mark the subtree hidden while it is
|
|
58
|
+
* `false`.
|
|
59
|
+
*/
|
|
60
|
+
navPresented: boolean;
|
|
61
|
+
/** The in-flow sidebar is narrowed to its rail width (`sidebarCollapsed`). */
|
|
62
|
+
sidebarCollapsed: boolean;
|
|
46
63
|
openNav: () => void;
|
|
64
|
+
closeNav: () => void;
|
|
47
65
|
openPanel: () => void;
|
|
48
66
|
panelLabel: string;
|
|
49
67
|
panelIcon: AiChatShellIcon;
|
package/src/ai-chat/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ export {
|
|
|
15
15
|
AiChatUserMessage,
|
|
16
16
|
} from './AiChatMessages';
|
|
17
17
|
export { AiChatMobileHeader, AiChatResizeHandle, AiChatShell } from './AiChatShell';
|
|
18
|
+
export { useAiChatShell } from './context';
|
|
19
|
+
export type { AiChatShellState } from './context';
|
|
18
20
|
export type {
|
|
19
21
|
AiChatAssistantMessageProps,
|
|
20
22
|
AiChatBulletListProps,
|
|
@@ -41,6 +43,7 @@ export type {
|
|
|
41
43
|
AiChatShellLabels,
|
|
42
44
|
AiChatShellProps,
|
|
43
45
|
AiChatStrongProps,
|
|
46
|
+
AiChatThreadHandle,
|
|
44
47
|
AiChatThreadProps,
|
|
45
48
|
AiChatUserMessageProps,
|
|
46
49
|
} from './types';
|
package/src/ai-chat/index.web.ts
CHANGED
|
@@ -17,6 +17,8 @@ export {
|
|
|
17
17
|
AiChatUserMessage,
|
|
18
18
|
} from './AiChatMessages';
|
|
19
19
|
export { AiChatMobileHeader, AiChatResizeHandle, AiChatShell } from './AiChatShell';
|
|
20
|
+
export { useAiChatShell } from './context';
|
|
21
|
+
export type { AiChatShellState } from './context';
|
|
20
22
|
export type {
|
|
21
23
|
AiChatAssistantMessageProps,
|
|
22
24
|
AiChatBulletListProps,
|
|
@@ -43,6 +45,7 @@ export type {
|
|
|
43
45
|
AiChatShellLabels,
|
|
44
46
|
AiChatShellProps,
|
|
45
47
|
AiChatStrongProps,
|
|
48
|
+
AiChatThreadHandle,
|
|
46
49
|
AiChatThreadProps,
|
|
47
50
|
AiChatUserMessageProps,
|
|
48
51
|
} from './types';
|
package/src/ai-chat/types.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ImageSourcePropType,
|
|
4
|
+
NativeScrollEvent,
|
|
5
|
+
NativeSyntheticEvent,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleProp,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
3
10
|
|
|
4
11
|
import type { CodeLanguage } from '../code';
|
|
5
12
|
import type { BloomIconComponent } from '../icons/icon-component';
|
|
@@ -270,8 +277,12 @@ export interface AiChatContainerLabels {
|
|
|
270
277
|
}
|
|
271
278
|
|
|
272
279
|
export interface AiChatContainerProps {
|
|
273
|
-
/**
|
|
274
|
-
|
|
280
|
+
/**
|
|
281
|
+
* The project crumb, after its folder glyph. Omit it for a chat that belongs
|
|
282
|
+
* to no project: the breadcrumb is then the title alone, with no empty crumb
|
|
283
|
+
* and no folder glyph.
|
|
284
|
+
*/
|
|
285
|
+
project?: string;
|
|
275
286
|
/** The current chat crumb. */
|
|
276
287
|
title: string;
|
|
277
288
|
/** The project crumb's glyph. Default `RiFolderLine`. */
|
|
@@ -289,14 +300,78 @@ export interface AiChatContainerProps {
|
|
|
289
300
|
working?: boolean;
|
|
290
301
|
/** `AgentThinking`'s label. Default `'Thinking'`. */
|
|
291
302
|
workingLabel?: string;
|
|
303
|
+
/**
|
|
304
|
+
* A layer filling the container, drawn ABOVE its own surface and BELOW every
|
|
305
|
+
* turn, header and composer — a host's wallpaper, gradient or animated field.
|
|
306
|
+
* It is clipped to the container's radius and never interactive
|
|
307
|
+
* (`pointerEvents: 'none'`), so an opaque node needs nothing else.
|
|
308
|
+
*
|
|
309
|
+
* Set `surface={false}` beside it when the node is translucent and should
|
|
310
|
+
* show what the container is sitting on rather than background-secondary.
|
|
311
|
+
*/
|
|
312
|
+
background?: ReactNode;
|
|
313
|
+
/**
|
|
314
|
+
* Paint the container's own background-secondary surface. Default `true`;
|
|
315
|
+
* `false` leaves the root transparent.
|
|
316
|
+
*/
|
|
317
|
+
surface?: boolean;
|
|
292
318
|
labels?: AiChatContainerLabels;
|
|
293
319
|
style?: StyleProp<ViewStyle>;
|
|
294
320
|
testID?: string;
|
|
295
321
|
}
|
|
296
322
|
|
|
323
|
+
/**
|
|
324
|
+
* What a `ref` on `AiChatThread` hands back: enough to drive the scroll a host
|
|
325
|
+
* cannot drive from props — a cursor jump, a restore, a follow of its own.
|
|
326
|
+
*/
|
|
327
|
+
export interface AiChatThreadHandle {
|
|
328
|
+
/** Jump to the newest turn. `animated` defaults to `true`. */
|
|
329
|
+
scrollToEnd: (options?: { animated?: boolean }) => void;
|
|
330
|
+
/** Scroll to an absolute offset, px from the top. `animated` defaults to `false`. */
|
|
331
|
+
scrollToOffset: (options: { offset: number; animated?: boolean }) => void;
|
|
332
|
+
/** The `ScrollView` itself, for anything this handle does not cover. */
|
|
333
|
+
getScrollView: () => ScrollView | null;
|
|
334
|
+
}
|
|
335
|
+
|
|
297
336
|
export interface AiChatThreadProps {
|
|
298
337
|
/** The turns. The thread follows the newest one with a smooth scroll as it grows. */
|
|
299
338
|
children: ReactNode;
|
|
339
|
+
/**
|
|
340
|
+
* Follow the newest turn as the content grows. Default `true` — the thread's
|
|
341
|
+
* behaviour since it shipped. `false` leaves every scroll to the host (and to
|
|
342
|
+
* the `ref`).
|
|
343
|
+
*/
|
|
344
|
+
autoFollow?: boolean;
|
|
345
|
+
/** Animate the follow. Default `true`; `false` for a restore that must not be seen travelling. */
|
|
346
|
+
followAnimated?: boolean;
|
|
347
|
+
/**
|
|
348
|
+
* Follow only while the reader is within this many px of the bottom, measured
|
|
349
|
+
* before the growth. Omit to follow every growth (the default), which is what
|
|
350
|
+
* the thread has always done.
|
|
351
|
+
*/
|
|
352
|
+
followThreshold?: number;
|
|
353
|
+
/**
|
|
354
|
+
* The reader came within `onStartReachedThreshold` of the top — a chat pages
|
|
355
|
+
* UPWARD, so this is its pagination signal, not `onEndReached`. It fires once
|
|
356
|
+
* per approach and re-arms when the reader leaves the zone.
|
|
357
|
+
*/
|
|
358
|
+
onStartReached?: () => void;
|
|
359
|
+
/** Distance from the top that fires `onStartReached`, px. Default `300`. */
|
|
360
|
+
onStartReachedThreshold?: number;
|
|
361
|
+
/**
|
|
362
|
+
* Keep the reader on the same turn while a page lands ABOVE them: the growth
|
|
363
|
+
* that follows an `onStartReached` is added to the offset instead of moving
|
|
364
|
+
* the content under the reader. Default `false`, and inert without
|
|
365
|
+
* `onStartReached` — the thread only anchors growth it asked for.
|
|
366
|
+
*
|
|
367
|
+
* This is the web-safe half of `maintainVisibleContentPosition`, which
|
|
368
|
+
* react-native-web ignores.
|
|
369
|
+
*/
|
|
370
|
+
maintainStartPosition?: boolean;
|
|
371
|
+
/** The raw scroll event, for a host tracking its own position. */
|
|
372
|
+
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
373
|
+
/** Default `16`. Only applied when something is listening to the scroll. */
|
|
374
|
+
scrollEventThrottle?: number;
|
|
300
375
|
style?: StyleProp<ViewStyle>;
|
|
301
376
|
testID?: string;
|
|
302
377
|
}
|
|
@@ -317,6 +392,15 @@ export interface AiChatShellLabels {
|
|
|
317
392
|
export interface AiChatShellProps {
|
|
318
393
|
/** The floating sidebar shown from `lg` (1024). */
|
|
319
394
|
sidebar: ReactNode;
|
|
395
|
+
/**
|
|
396
|
+
* The in-flow sidebar's column width from `lg` up. Omit and the sidebar sizes
|
|
397
|
+
* itself, as it always has.
|
|
398
|
+
*/
|
|
399
|
+
sidebarWidth?: number;
|
|
400
|
+
/** Narrow the in-flow sidebar to `collapsedSidebarWidth` — a host's icon rail. Default `false`. */
|
|
401
|
+
sidebarCollapsed?: boolean;
|
|
402
|
+
/** The collapsed column's width. Default `56`. */
|
|
403
|
+
collapsedSidebarWidth?: number;
|
|
320
404
|
/** The flat sidebar revealed under the workspace below `lg`. Default: none (no nav drawer). */
|
|
321
405
|
mobileSidebar?: ReactNode;
|
|
322
406
|
/** The chat container. */
|
|
@@ -337,6 +421,23 @@ export interface AiChatShellProps {
|
|
|
337
421
|
/** Controlled panel drawer (below `xl`). */
|
|
338
422
|
panelOpen?: boolean;
|
|
339
423
|
onPanelOpenChange?: (open: boolean) => void;
|
|
424
|
+
/**
|
|
425
|
+
* A layer filling the shell, drawn ABOVE its own background-full and BELOW
|
|
426
|
+
* the sidebar, workspace and drawers. Not interactive
|
|
427
|
+
* (`pointerEvents: 'none'`).
|
|
428
|
+
*
|
|
429
|
+
* The workspace paints background-full of its own, so a shell background is
|
|
430
|
+
* covered wherever the chat sits: pass `surface={false}` with it, which drops
|
|
431
|
+
* the paint on both, and let the chat's own `AiChatContainer` decide what it
|
|
432
|
+
* paints. A `background` on the container is the closer slot when the layer
|
|
433
|
+
* belongs to the chat rather than to the page.
|
|
434
|
+
*/
|
|
435
|
+
background?: ReactNode;
|
|
436
|
+
/**
|
|
437
|
+
* Paint the shell's own background-full, on its root and on the workspace.
|
|
438
|
+
* Default `true`; `false` leaves both transparent.
|
|
439
|
+
*/
|
|
440
|
+
surface?: boolean;
|
|
340
441
|
labels?: AiChatShellLabels;
|
|
341
442
|
style?: StyleProp<ViewStyle>;
|
|
342
443
|
testID?: string;
|
|
@@ -16,6 +16,7 @@ import { paintToCssImage, type ButtonStatePaint } from '../button/shared';
|
|
|
16
16
|
import { DISABLED_OPACITY } from '../styles/tokens';
|
|
17
17
|
import { RiArrowUpLine } from '../icons/remix/RiArrowUpLine';
|
|
18
18
|
import { RiMic2Line } from '../icons/remix/RiMic2Line';
|
|
19
|
+
import { RiStopFill } from '../icons/remix/RiStopFill';
|
|
19
20
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
20
21
|
import { CONTROL_SIZE, TRANSITION_MS, type ComposerPalette } from './shared';
|
|
21
22
|
import { dataHook, IS_WEB } from './web-hooks';
|
|
@@ -239,3 +240,56 @@ export function SendButton({
|
|
|
239
240
|
</Pressable>
|
|
240
241
|
);
|
|
241
242
|
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Stop — send's other state, drawn on the same disc so the action sits where the
|
|
246
|
+
* reader last pressed: the accent gradient (hover and active crossfading like
|
|
247
|
+
* send's) with a white 20px stop mark.
|
|
248
|
+
*
|
|
249
|
+
* It takes no `disabled`. The composer's `disabled` locks the draft for the
|
|
250
|
+
* whole turn, and the one thing that must stay possible for that whole turn is
|
|
251
|
+
* cancelling it.
|
|
252
|
+
*/
|
|
253
|
+
export function StopButton({
|
|
254
|
+
onPress,
|
|
255
|
+
label,
|
|
256
|
+
palette,
|
|
257
|
+
}: {
|
|
258
|
+
onPress?: () => void;
|
|
259
|
+
label: string;
|
|
260
|
+
palette: ComposerPalette;
|
|
261
|
+
}) {
|
|
262
|
+
const [hovered, setHovered] = useState(false);
|
|
263
|
+
const [pressed, setPressed] = useState(false);
|
|
264
|
+
const paint = palette.send;
|
|
265
|
+
const base = pressed ? paint.active : paint.rest;
|
|
266
|
+
const style: WebCssStyle = {
|
|
267
|
+
position: 'relative',
|
|
268
|
+
width: CONTROL_SIZE,
|
|
269
|
+
height: CONTROL_SIZE,
|
|
270
|
+
alignItems: 'center',
|
|
271
|
+
justifyContent: 'center',
|
|
272
|
+
borderRadius: 9999,
|
|
273
|
+
cursor: 'pointer',
|
|
274
|
+
'--bloom-composer-ring': palette.focusRing,
|
|
275
|
+
};
|
|
276
|
+
return (
|
|
277
|
+
<Pressable
|
|
278
|
+
{...dataHook('bloomComposerControl', 'stop')}
|
|
279
|
+
accessibilityRole="button"
|
|
280
|
+
accessibilityLabel={label}
|
|
281
|
+
onPress={onPress}
|
|
282
|
+
onPressIn={() => setPressed(true)}
|
|
283
|
+
onPressOut={() => setPressed(false)}
|
|
284
|
+
onHoverIn={() => setHovered(true)}
|
|
285
|
+
onHoverOut={() => setHovered(false)}
|
|
286
|
+
style={style}>
|
|
287
|
+
<GradientFill paint={base} />
|
|
288
|
+
<GradientFill paint={paint.hover} opacity={hovered && !pressed ? 1 : 0} />
|
|
289
|
+
{/* Its own positioned box, so it paints above the absolutely placed fills. */}
|
|
290
|
+
<View pointerEvents="none" style={{ position: 'relative', width: 20, height: 20 }}>
|
|
291
|
+
<RiStopFill width={20} height={20} fill="#ffffff" />
|
|
292
|
+
</View>
|
|
293
|
+
</Pressable>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -22,7 +22,7 @@ import { useTheme } from '../theme/use-theme';
|
|
|
22
22
|
import { TYPE_SCALE } from '../typography';
|
|
23
23
|
import { AddMenu } from './AddMenu';
|
|
24
24
|
import { AttachmentStrip } from './AttachmentStrip';
|
|
25
|
-
import { MicButton, SendButton } from './ComposerControls';
|
|
25
|
+
import { MicButton, SendButton, StopButton } from './ComposerControls';
|
|
26
26
|
import { ModelPickerBase } from './ModelPickerBase';
|
|
27
27
|
import { PermissionMenu } from './PermissionMenu';
|
|
28
28
|
import {
|
|
@@ -48,6 +48,7 @@ const DEFAULT_LABELS: Required<ComposerPanelLabels> = {
|
|
|
48
48
|
learnMore: 'Learn more',
|
|
49
49
|
voice: 'Voice input',
|
|
50
50
|
send: 'Send message',
|
|
51
|
+
stop: 'Stop generating',
|
|
51
52
|
remove: 'Remove',
|
|
52
53
|
};
|
|
53
54
|
|
|
@@ -107,7 +108,8 @@ function Collapse({ open, children }: { open: boolean; children: React.ReactNode
|
|
|
107
108
|
* body pt 6, prompt → controls 20
|
|
108
109
|
* prompt px 6, body-regular, grows 20px a line up to 200 then scrolls
|
|
109
110
|
* controls left: add + permission (8 apart); right: model picker, 16,
|
|
110
|
-
* then mic + send (8 apart)
|
|
111
|
+
* then mic + send (8 apart); `busy` + `onStop` puts the stop
|
|
112
|
+
* control where send was, outside `disabled`'s reach
|
|
111
113
|
*
|
|
112
114
|
* Takes its panel implementation from the family's platform binding.
|
|
113
115
|
*/
|
|
@@ -116,6 +118,8 @@ export function ComposerPanelBase({
|
|
|
116
118
|
defaultValue = '',
|
|
117
119
|
onValueChange,
|
|
118
120
|
onSubmit,
|
|
121
|
+
onStop,
|
|
122
|
+
busy = false,
|
|
119
123
|
disabled = false,
|
|
120
124
|
placeholder = 'Hi, what do you need today?',
|
|
121
125
|
permissions = COMPOSER_PANEL_PERMISSIONS,
|
|
@@ -188,10 +192,10 @@ export function ComposerPanelBase({
|
|
|
188
192
|
);
|
|
189
193
|
|
|
190
194
|
const submit = useCallback(() => {
|
|
191
|
-
if (disabled) return;
|
|
195
|
+
if (disabled || busy) return;
|
|
192
196
|
onSubmit?.(text);
|
|
193
197
|
if (value === undefined) setText('');
|
|
194
|
-
}, [disabled, onSubmit, text, value, setText]);
|
|
198
|
+
}, [disabled, busy, onSubmit, text, value, setText]);
|
|
195
199
|
|
|
196
200
|
const onKeyPress = useCallback(
|
|
197
201
|
(event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
|
@@ -310,7 +314,11 @@ export function ComposerPanelBase({
|
|
|
310
314
|
label={labels.voice}
|
|
311
315
|
palette={palette}
|
|
312
316
|
/>
|
|
313
|
-
|
|
317
|
+
{busy && onStop ? (
|
|
318
|
+
<StopButton onPress={onStop} label={labels.stop} palette={palette} />
|
|
319
|
+
) : (
|
|
320
|
+
<SendButton disabled={disabled} onPress={submit} label={labels.send} palette={palette} />
|
|
321
|
+
)}
|
|
314
322
|
</View>
|
|
315
323
|
</View>
|
|
316
324
|
</View>
|