@oxy.so/bloom 4.19.0 → 4.20.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.
Files changed (141) hide show
  1. package/docs/adoption-matrix.mdx +3 -3
  2. package/docs/ai-chat.mdx +32 -1
  3. package/docs/composer-panel.mdx +4 -0
  4. package/lib/commonjs/ai-chat/AiChat.js +2 -1
  5. package/lib/commonjs/ai-chat/AiChat.js.map +1 -1
  6. package/lib/commonjs/ai-chat/AiChat.web.js +2 -1
  7. package/lib/commonjs/ai-chat/AiChat.web.js.map +1 -1
  8. package/lib/commonjs/ai-chat/AiChatControls.js +60 -4
  9. package/lib/commonjs/ai-chat/AiChatControls.js.map +1 -1
  10. package/lib/commonjs/ai-chat/AiChatFeedbackRowBase.js +40 -12
  11. package/lib/commonjs/ai-chat/AiChatFeedbackRowBase.js.map +1 -1
  12. package/lib/commonjs/ai-chat/AiChatMessages.js +31 -5
  13. package/lib/commonjs/ai-chat/AiChatMessages.js.map +1 -1
  14. package/lib/commonjs/ai-chat/bindAiChat.js +10 -0
  15. package/lib/commonjs/ai-chat/bindAiChat.js.map +1 -1
  16. package/lib/commonjs/ai-chat/index.js +1 -1
  17. package/lib/commonjs/ai-chat/index.js.map +1 -1
  18. package/lib/commonjs/ai-chat/index.web.js +1 -1
  19. package/lib/commonjs/ai-chat/index.web.js.map +1 -1
  20. package/lib/commonjs/ai-chat/shared.js +11 -2
  21. package/lib/commonjs/ai-chat/shared.js.map +1 -1
  22. package/lib/commonjs/composer-panel/AttachmentStrip.js +108 -8
  23. package/lib/commonjs/composer-panel/AttachmentStrip.js.map +1 -1
  24. package/lib/commonjs/composer-panel/ComposerPanelBase.js +6 -2
  25. package/lib/commonjs/composer-panel/ComposerPanelBase.js.map +1 -1
  26. package/lib/commonjs/composer-panel/ComposerPillBase.js +2 -1
  27. package/lib/commonjs/composer-panel/ComposerPillBase.js.map +1 -1
  28. package/lib/commonjs/composer-panel/shared.js +2 -0
  29. package/lib/commonjs/composer-panel/shared.js.map +1 -1
  30. package/lib/commonjs/composer-panel/use-attachment-queue.js +5 -2
  31. package/lib/commonjs/composer-panel/use-attachment-queue.js.map +1 -1
  32. package/lib/commonjs/input-otp/InputOtp.js +13 -3
  33. package/lib/commonjs/input-otp/InputOtp.js.map +1 -1
  34. package/lib/module/ai-chat/AiChat.js +1 -0
  35. package/lib/module/ai-chat/AiChat.js.map +1 -1
  36. package/lib/module/ai-chat/AiChat.web.js +1 -0
  37. package/lib/module/ai-chat/AiChat.web.js.map +1 -1
  38. package/lib/module/ai-chat/AiChatControls.js +58 -4
  39. package/lib/module/ai-chat/AiChatControls.js.map +1 -1
  40. package/lib/module/ai-chat/AiChatFeedbackRowBase.js +41 -13
  41. package/lib/module/ai-chat/AiChatFeedbackRowBase.js.map +1 -1
  42. package/lib/module/ai-chat/AiChatMessages.js +30 -4
  43. package/lib/module/ai-chat/AiChatMessages.js.map +1 -1
  44. package/lib/module/ai-chat/bindAiChat.js +11 -1
  45. package/lib/module/ai-chat/bindAiChat.js.map +1 -1
  46. package/lib/module/ai-chat/index.js +2 -2
  47. package/lib/module/ai-chat/index.js.map +1 -1
  48. package/lib/module/ai-chat/index.web.js +2 -2
  49. package/lib/module/ai-chat/index.web.js.map +1 -1
  50. package/lib/module/ai-chat/shared.js +11 -2
  51. package/lib/module/ai-chat/shared.js.map +1 -1
  52. package/lib/module/composer-panel/AttachmentStrip.js +108 -8
  53. package/lib/module/composer-panel/AttachmentStrip.js.map +1 -1
  54. package/lib/module/composer-panel/ComposerPanelBase.js +6 -2
  55. package/lib/module/composer-panel/ComposerPanelBase.js.map +1 -1
  56. package/lib/module/composer-panel/ComposerPillBase.js +2 -1
  57. package/lib/module/composer-panel/ComposerPillBase.js.map +1 -1
  58. package/lib/module/composer-panel/shared.js +2 -0
  59. package/lib/module/composer-panel/shared.js.map +1 -1
  60. package/lib/module/composer-panel/use-attachment-queue.js +5 -2
  61. package/lib/module/composer-panel/use-attachment-queue.js.map +1 -1
  62. package/lib/module/input-otp/InputOtp.js +13 -3
  63. package/lib/module/input-otp/InputOtp.js.map +1 -1
  64. package/lib/typescript/commonjs/ai-chat/AiChat.d.ts +4 -0
  65. package/lib/typescript/commonjs/ai-chat/AiChat.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/ai-chat/AiChat.web.d.ts +4 -0
  67. package/lib/typescript/commonjs/ai-chat/AiChat.web.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ai-chat/AiChatControls.d.ts +22 -1
  69. package/lib/typescript/commonjs/ai-chat/AiChatControls.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ai-chat/AiChatFeedbackRowBase.d.ts +5 -2
  71. package/lib/typescript/commonjs/ai-chat/AiChatFeedbackRowBase.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ai-chat/AiChatMessages.d.ts +1 -1
  73. package/lib/typescript/commonjs/ai-chat/AiChatMessages.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ai-chat/bindAiChat.d.ts +5 -1
  75. package/lib/typescript/commonjs/ai-chat/bindAiChat.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ai-chat/index.d.ts +3 -3
  77. package/lib/typescript/commonjs/ai-chat/index.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/ai-chat/index.web.d.ts +3 -3
  79. package/lib/typescript/commonjs/ai-chat/index.web.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ai-chat/shared.d.ts +2 -2
  81. package/lib/typescript/commonjs/ai-chat/shared.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ai-chat/types.d.ts +43 -5
  83. package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/composer-panel/AttachmentStrip.d.ts +10 -1
  85. package/lib/typescript/commonjs/composer-panel/AttachmentStrip.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/composer-panel/ComposerPanelBase.d.ts +1 -1
  87. package/lib/typescript/commonjs/composer-panel/ComposerPanelBase.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/composer-panel/shared.d.ts +3 -0
  89. package/lib/typescript/commonjs/composer-panel/shared.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/composer-panel/types.d.ts +11 -0
  91. package/lib/typescript/commonjs/composer-panel/types.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/composer-panel/use-attachment-queue.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/input-otp/InputOtp.d.ts.map +1 -1
  94. package/lib/typescript/module/ai-chat/AiChat.d.ts +4 -0
  95. package/lib/typescript/module/ai-chat/AiChat.d.ts.map +1 -1
  96. package/lib/typescript/module/ai-chat/AiChat.web.d.ts +4 -0
  97. package/lib/typescript/module/ai-chat/AiChat.web.d.ts.map +1 -1
  98. package/lib/typescript/module/ai-chat/AiChatControls.d.ts +22 -1
  99. package/lib/typescript/module/ai-chat/AiChatControls.d.ts.map +1 -1
  100. package/lib/typescript/module/ai-chat/AiChatFeedbackRowBase.d.ts +5 -2
  101. package/lib/typescript/module/ai-chat/AiChatFeedbackRowBase.d.ts.map +1 -1
  102. package/lib/typescript/module/ai-chat/AiChatMessages.d.ts +1 -1
  103. package/lib/typescript/module/ai-chat/AiChatMessages.d.ts.map +1 -1
  104. package/lib/typescript/module/ai-chat/bindAiChat.d.ts +5 -1
  105. package/lib/typescript/module/ai-chat/bindAiChat.d.ts.map +1 -1
  106. package/lib/typescript/module/ai-chat/index.d.ts +3 -3
  107. package/lib/typescript/module/ai-chat/index.d.ts.map +1 -1
  108. package/lib/typescript/module/ai-chat/index.web.d.ts +3 -3
  109. package/lib/typescript/module/ai-chat/index.web.d.ts.map +1 -1
  110. package/lib/typescript/module/ai-chat/shared.d.ts +2 -2
  111. package/lib/typescript/module/ai-chat/shared.d.ts.map +1 -1
  112. package/lib/typescript/module/ai-chat/types.d.ts +43 -5
  113. package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
  114. package/lib/typescript/module/composer-panel/AttachmentStrip.d.ts +10 -1
  115. package/lib/typescript/module/composer-panel/AttachmentStrip.d.ts.map +1 -1
  116. package/lib/typescript/module/composer-panel/ComposerPanelBase.d.ts +1 -1
  117. package/lib/typescript/module/composer-panel/ComposerPanelBase.d.ts.map +1 -1
  118. package/lib/typescript/module/composer-panel/shared.d.ts +3 -0
  119. package/lib/typescript/module/composer-panel/shared.d.ts.map +1 -1
  120. package/lib/typescript/module/composer-panel/types.d.ts +11 -0
  121. package/lib/typescript/module/composer-panel/types.d.ts.map +1 -1
  122. package/lib/typescript/module/composer-panel/use-attachment-queue.d.ts.map +1 -1
  123. package/lib/typescript/module/input-otp/InputOtp.d.ts.map +1 -1
  124. package/package.json +1 -1
  125. package/src/ai-chat/AiChat.tsx +1 -0
  126. package/src/ai-chat/AiChat.web.tsx +1 -0
  127. package/src/ai-chat/AiChatControls.tsx +58 -3
  128. package/src/ai-chat/AiChatFeedbackRowBase.tsx +45 -17
  129. package/src/ai-chat/AiChatMessages.tsx +24 -1
  130. package/src/ai-chat/bindAiChat.tsx +19 -2
  131. package/src/ai-chat/index.ts +3 -1
  132. package/src/ai-chat/index.web.ts +3 -1
  133. package/src/ai-chat/shared.ts +11 -2
  134. package/src/ai-chat/types.ts +45 -5
  135. package/src/composer-panel/AttachmentStrip.tsx +98 -6
  136. package/src/composer-panel/ComposerPanelBase.tsx +4 -0
  137. package/src/composer-panel/ComposerPillBase.tsx +1 -0
  138. package/src/composer-panel/shared.ts +5 -0
  139. package/src/composer-panel/types.ts +11 -0
  140. package/src/composer-panel/use-attachment-queue.ts +5 -2
  141. package/src/input-otp/InputOtp.tsx +11 -2
@@ -4,7 +4,7 @@ import { AiChatContainerBase } from './AiChatContainer';
4
4
  import { AiChatFeedbackRowBase } from './AiChatFeedbackRowBase';
5
5
  import { AiChatGalleryPanelBase } from './AiChatGalleryPanelBase';
6
6
  import { AiChatImageGenerationBase } from './AiChatImageGenerationBase';
7
- import { AiChatAssistantMessageBase } from './AiChatMessages';
7
+ import { AiChatAssistantMessageBase, AiChatUserMessageBase } from './AiChatMessages';
8
8
  import { AiChatPlatformContext, type AiChatPlatform } from './context';
9
9
  import type {
10
10
  AiChatAssistantMessageProps,
@@ -12,6 +12,7 @@ import type {
12
12
  AiChatFeedbackRowProps,
13
13
  AiChatGalleryPanelProps,
14
14
  AiChatImageGenerationProps,
15
+ AiChatUserMessageProps,
15
16
  } from './types';
16
17
 
17
18
  /**
@@ -29,6 +30,15 @@ export function bindAiChat(platform: AiChatPlatform) {
29
30
  }
30
31
  AiChatFeedbackRow.displayName = 'AiChatFeedbackRow';
31
32
 
33
+ function AiChatUserMessage(props: AiChatUserMessageProps) {
34
+ return (
35
+ <AiChatPlatformContext.Provider value={platform}>
36
+ <AiChatUserMessageBase {...props} />
37
+ </AiChatPlatformContext.Provider>
38
+ );
39
+ }
40
+ AiChatUserMessage.displayName = 'AiChatUserMessage';
41
+
32
42
  function AiChatAssistantMessage(props: AiChatAssistantMessageProps) {
33
43
  return (
34
44
  <AiChatPlatformContext.Provider value={platform}>
@@ -65,5 +75,12 @@ export function bindAiChat(platform: AiChatPlatform) {
65
75
  }
66
76
  AiChatContainer.displayName = 'AiChatContainer';
67
77
 
68
- return { AiChatFeedbackRow, AiChatAssistantMessage, AiChatImageGeneration, AiChatGalleryPanel, AiChatContainer };
78
+ return {
79
+ AiChatFeedbackRow,
80
+ AiChatUserMessage,
81
+ AiChatAssistantMessage,
82
+ AiChatImageGeneration,
83
+ AiChatGalleryPanel,
84
+ AiChatContainer,
85
+ };
69
86
  }
@@ -4,6 +4,7 @@ export {
4
4
  AiChatFeedbackRow,
5
5
  AiChatGalleryPanel,
6
6
  AiChatImageGeneration,
7
+ AiChatUserMessage,
7
8
  } from './AiChat';
8
9
  export { AiChatCodePanel } from './AiChatCodePanel';
9
10
  export { AiChatThread } from './AiChatContainer';
@@ -13,7 +14,6 @@ export {
13
14
  AiChatLinkChip,
14
15
  AiChatMessageLine,
15
16
  AiChatStrong,
16
- AiChatUserMessage,
17
17
  } from './AiChatMessages';
18
18
  export { AiChatMobileHeader, AiChatResizeHandle, AiChatShell } from './AiChatShell';
19
19
  export { useAiChatChromeInsets, useAiChatShell } from './context';
@@ -27,6 +27,7 @@ export type {
27
27
  AiChatCodePanelProps,
28
28
  AiChatContainerLabels,
29
29
  AiChatContainerProps,
30
+ AiChatCopyResult,
30
31
  AiChatFeedbackLabels,
31
32
  AiChatFeedbackRowProps,
32
33
  AiChatGalleryPanelLabels,
@@ -46,5 +47,6 @@ export type {
46
47
  AiChatStrongProps,
47
48
  AiChatThreadHandle,
48
49
  AiChatThreadProps,
50
+ AiChatTurnAction,
49
51
  AiChatUserMessageProps,
50
52
  } from './types';
@@ -9,6 +9,7 @@ export {
9
9
  AiChatFeedbackRow,
10
10
  AiChatGalleryPanel,
11
11
  AiChatImageGeneration,
12
+ AiChatUserMessage,
12
13
  } from './AiChat.web';
13
14
  export { AiChatCodePanel } from './AiChatCodePanel';
14
15
  export { AiChatThread } from './AiChatContainer';
@@ -18,7 +19,6 @@ export {
18
19
  AiChatLinkChip,
19
20
  AiChatMessageLine,
20
21
  AiChatStrong,
21
- AiChatUserMessage,
22
22
  } from './AiChatMessages';
23
23
  export { AiChatMobileHeader, AiChatResizeHandle, AiChatShell } from './AiChatShell';
24
24
  export { useAiChatChromeInsets, useAiChatShell } from './context';
@@ -32,6 +32,7 @@ export type {
32
32
  AiChatCodePanelProps,
33
33
  AiChatContainerLabels,
34
34
  AiChatContainerProps,
35
+ AiChatCopyResult,
35
36
  AiChatFeedbackLabels,
36
37
  AiChatFeedbackRowProps,
37
38
  AiChatGalleryPanelLabels,
@@ -51,5 +52,6 @@ export type {
51
52
  AiChatStrongProps,
52
53
  AiChatThreadHandle,
53
54
  AiChatThreadProps,
55
+ AiChatTurnAction,
54
56
  AiChatUserMessageProps,
55
57
  } from './types';
@@ -176,7 +176,7 @@ const STYLE_ID = 'bloom-ai-chat-web-css';
176
176
  * - the split-flap countdown's two leaves (`flap-fall` / `flap-rise`);
177
177
  * - the generated image's feathered radial reveal, a registered `--bloom-ai-reveal`
178
178
  * percentage transitioning 0% → 132% over 1550ms;
179
- * - the resize grip's hover reveal;
179
+ * - the resize grip's hover reveal, and a user turn's actions (pointer only);
180
180
  * - the composer field's placeholder colour;
181
181
  * - a document-scrolled thread out of the browser's scroll anchoring.
182
182
  */
@@ -260,6 +260,14 @@ export const AI_CHAT_WEB_CSS = `
260
260
  [data-bloom-ai-chat-grip-pill="dragging"] { opacity: 1; }
261
261
  [data-bloom-ai-chat-grip] { cursor: col-resize; touch-action: none; outline: none; }
262
262
  [data-bloom-ai-chat-dragging="on"] { cursor: col-resize; user-select: none; }
263
+ @media (hover: hover) {
264
+ [data-bloom-ai-chat-turn] [data-bloom-ai-chat-turn-actions] {
265
+ opacity: 0;
266
+ transition: opacity ${TRANSITION_MS}ms ease;
267
+ }
268
+ [data-bloom-ai-chat-turn]:hover [data-bloom-ai-chat-turn-actions],
269
+ [data-bloom-ai-chat-turn]:focus-within [data-bloom-ai-chat-turn-actions] { opacity: 1; }
270
+ }
263
271
  [data-bloom-ai-chat-chip] {
264
272
  display: inline-flex;
265
273
  max-width: calc(100% - 4px);
@@ -275,7 +283,8 @@ export const AI_CHAT_WEB_CSS = `
275
283
  [data-bloom-ai-chat-control],
276
284
  [data-bloom-ai-chat-tile] [data-bloom-ai-chat-scrim],
277
285
  [data-bloom-ai-chat-tile-image],
278
- [data-bloom-ai-chat-grip] [data-bloom-ai-chat-grip-pill] { transition: none; }
286
+ [data-bloom-ai-chat-grip] [data-bloom-ai-chat-grip-pill],
287
+ [data-bloom-ai-chat-turn] [data-bloom-ai-chat-turn-actions] { transition: none; }
279
288
  [data-bloom-ai-chat-flap] { animation-duration: 1ms; animation-delay: 0ms; }
280
289
  }
281
290
  `;
@@ -30,14 +30,47 @@ export interface AiChatFeedbackLabels {
30
30
  copied?: string;
31
31
  }
32
32
 
33
+ /**
34
+ * What a copy handler reports. `void` (or `true`) is a copy that happened;
35
+ * `false` is one that did not. A promise is waited for: the confirmation shows
36
+ * once it resolves, and never if it resolves `false` or rejects.
37
+ */
38
+ export type AiChatCopyResult = void | boolean | Promise<void | boolean>;
39
+
40
+ /**
41
+ * One extra action on a turn — read aloud, regenerate, edit. Drawn with the
42
+ * feedback row's own button: a 16px glyph on a 28px square, a tooltip naming it.
43
+ */
44
+ export interface AiChatTurnAction {
45
+ /** Stable React key, and the suffix of the button's `testID`. */
46
+ key: string;
47
+ /** The accessible name, and the tooltip's copy. */
48
+ label: string;
49
+ /** A Bloom icon component (`RiVolumeUpLine`, not `<RiVolumeUpLine />`). */
50
+ icon: AiChatIconComponent;
51
+ onPress: () => void;
52
+ /**
53
+ * Makes the action a TOGGLE (read aloud while it is speaking): `true` paints
54
+ * the hover surface and the primary glyph, and announces it pressed. Leave it
55
+ * undefined for a plain button — `false` announces "not pressed".
56
+ */
57
+ active?: boolean;
58
+ /** Dimmed to the shared disabled opacity, unfocusable, announced disabled. */
59
+ disabled?: boolean;
60
+ }
61
+
33
62
  export interface AiChatFeedbackRowProps {
34
63
  onLike?: () => void;
35
64
  onDislike?: () => void;
36
65
  /**
37
- * Copy handler. The glyph swaps to a check and the tooltip reads "Copied!" for
38
- * 1.6s either way.
66
+ * Copy handler. The glyph swaps to a check and the tooltip reads "Copied!"
67
+ * for 1.6s once the copy has happened: at once for a handler that returns
68
+ * nothing (or `true`), after the promise resolves for an async one, and not
69
+ * at all when it returns or resolves `false`, throws or rejects.
39
70
  */
40
- onCopy?: () => void;
71
+ onCopy?: () => AiChatCopyResult;
72
+ /** More buttons after copy, in order — read aloud, regenerate… */
73
+ actions?: ReadonlyArray<AiChatTurnAction>;
41
74
  labels?: AiChatFeedbackLabels;
42
75
  style?: StyleProp<ViewStyle>;
43
76
  testID?: string;
@@ -50,6 +83,13 @@ export interface AiChatFeedbackRowProps {
50
83
  export interface AiChatUserMessageProps {
51
84
  /** `AiChatMessageLine`s — or a string, wrapped in one. */
52
85
  children: ReactNode;
86
+ /**
87
+ * Buttons under the card, right-aligned — copy, edit. On a web pointer they
88
+ * fade in while the turn is hovered or holds focus; they stay in the tab order
89
+ * and the accessibility tree throughout, and on native (and touch web) they
90
+ * are always shown.
91
+ */
92
+ actions?: ReadonlyArray<AiChatTurnAction>;
53
93
  /** Skip the blur-in (a turn restored from history). Default `true`. */
54
94
  animate?: boolean;
55
95
  style?: StyleProp<ViewStyle>;
@@ -64,7 +104,7 @@ export interface AiChatAssistantMessageProps {
64
104
  children: ReactNode;
65
105
  /** The like / dislike / copy row under the reply. Default `true`. */
66
106
  feedback?: boolean;
67
- /** Feedback handlers and labels. */
107
+ /** Feedback handlers, extra `actions` and labels. */
68
108
  feedbackProps?: Omit<AiChatFeedbackRowProps, 'style' | 'testID'>;
69
109
  /** Skip the staggered blur-in. Default `true`. */
70
110
  animate?: boolean;
@@ -141,7 +181,7 @@ export interface AiChatImageGenerationProps {
141
181
  /** Feedback handlers under the landed image. */
142
182
  onLike?: () => void;
143
183
  onDislike?: () => void;
144
- onCopy?: () => void;
184
+ onCopy?: () => AiChatCopyResult;
145
185
  labels?: AiChatImageGenerationLabels;
146
186
  feedbackLabels?: AiChatFeedbackLabels;
147
187
  style?: StyleProp<ViewStyle>;
@@ -10,6 +10,8 @@ import Animated, {
10
10
  import Svg, { Path } from 'react-native-svg';
11
11
 
12
12
  import { mixColor } from '../button/shared';
13
+ import { RiErrorWarningLine } from '../icons/remix/RiErrorWarningLine';
14
+ import { RiRefreshLine } from '../icons/remix/RiRefreshLine';
13
15
  import { Text } from '../typography';
14
16
  import type { WebCssStyle } from '../styles/web-view-style';
15
17
  import {
@@ -101,6 +103,41 @@ function TileDismiss({
101
103
  );
102
104
  }
103
105
 
106
+ /**
107
+ * A failed tile's retry: a 24px disc on the card surface where the kind glyph
108
+ * sits (3, 4), a 14px refresh glyph in the error foreground, text-primary on
109
+ * hover.
110
+ */
111
+ function TileRetry({ label, onPress, palette }: { label: string; onPress: () => void; palette: ComposerPalette }) {
112
+ const [hovered, setHovered] = useState(false);
113
+ const style: WebCssStyle = {
114
+ position: 'absolute',
115
+ top: 3,
116
+ left: 3,
117
+ width: 24,
118
+ height: 24,
119
+ borderRadius: 12,
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ backgroundColor: palette.surface,
123
+ cursor: 'pointer',
124
+ '--bloom-composer-ring': palette.focusRing,
125
+ };
126
+ return (
127
+ <Pressable
128
+ {...dataHook('bloomComposerControl', 'offset')}
129
+ accessibilityRole="button"
130
+ accessibilityLabel={label}
131
+ onPress={onPress}
132
+ onHoverIn={() => setHovered(true)}
133
+ onHoverOut={() => setHovered(false)}
134
+ hitSlop={4}
135
+ style={style}>
136
+ <RiRefreshLine width={14} height={14} fill={hovered ? palette.text : palette.errorText} />
137
+ </Pressable>
138
+ );
139
+ }
140
+
104
141
  /** The upload percentage's box: top 3 / right 3, blurring out as it fades. */
105
142
  function percentStyle(inFlight: boolean): WebCssStyle {
106
143
  return {
@@ -123,7 +160,9 @@ interface TileProps {
123
160
  attachment: ComposerPanelAttachment;
124
161
  palette: ComposerPalette;
125
162
  onRemove?: () => void;
163
+ onRetry?: () => void;
126
164
  removeLabel: string;
165
+ retryLabel: string;
127
166
  }
128
167
 
129
168
  /**
@@ -134,9 +173,23 @@ interface TileProps {
134
173
  * percentage (9/16 medium, tabular; white over a photo, accent-500 otherwise)
135
174
  * sits at top 3 / right 3; once landed it blurs out as the dismiss blurs in on
136
175
  * the same spot.
176
+ *
177
+ * A FAILED tile (`error`) drops the ring and percentage and fills with the
178
+ * error subtle surface under an error-foreground border: the warning glyph (or
179
+ * the retry button) at (3, 4) and the message in 9/11 medium, two lines at
180
+ * most, from (5, 30). The dismiss shows as on a landed tile.
137
181
  */
138
- export const AttachmentTile = memo(function AttachmentTile({ attachment, palette, onRemove, removeLabel }: TileProps) {
139
- const { progress } = attachment;
182
+ export const AttachmentTile = memo(function AttachmentTile({
183
+ attachment,
184
+ palette,
185
+ onRemove,
186
+ onRetry,
187
+ removeLabel,
188
+ retryLabel,
189
+ }: TileProps) {
190
+ const { error } = attachment;
191
+ const failed = !!error;
192
+ const progress = failed ? undefined : attachment.progress;
140
193
  const inFlight = progress !== undefined;
141
194
  const image = attachment.kind === 'image' ? attachment.src : undefined;
142
195
  const dash = inFlight && progress < 100 ? (Math.max(0, progress) / 100) * TILE_RING_LENGTH : TILE_RING_LENGTH + 2;
@@ -144,8 +197,8 @@ export const AttachmentTile = memo(function AttachmentTile({ attachment, palette
144
197
 
145
198
  return (
146
199
  <View
147
- accessibilityLabel={attachment.name}
148
- {...(IS_WEB ? { title: attachment.name } : {})}
200
+ accessibilityLabel={failed ? `${attachment.name}: ${error}` : attachment.name}
201
+ {...(IS_WEB ? { title: failed ? `${attachment.name}: ${error}` : attachment.name } : {})}
149
202
  style={{
150
203
  position: 'relative',
151
204
  width: TILE,
@@ -153,7 +206,7 @@ export const AttachmentTile = memo(function AttachmentTile({ attachment, palette
153
206
  flexShrink: 0,
154
207
  borderRadius: TILE_RADIUS,
155
208
  borderWidth: 1,
156
- borderColor: palette.tileBorder,
209
+ borderColor: failed ? palette.errorText : palette.tileBorder,
157
210
  }}>
158
211
  {image ? (
159
212
  <View
@@ -196,6 +249,41 @@ export const AttachmentTile = memo(function AttachmentTile({ attachment, palette
196
249
  </View>
197
250
  )}
198
251
 
252
+ {failed ? (
253
+ <View
254
+ style={{
255
+ position: 'absolute',
256
+ top: 0,
257
+ left: 0,
258
+ right: 0,
259
+ bottom: 0,
260
+ borderRadius: TILE_RADIUS - 1,
261
+ backgroundColor: palette.errorSurface,
262
+ }}>
263
+ {onRetry ? null : (
264
+ <View style={{ position: 'absolute', top: 4, left: 3, width: 24, height: 24, alignItems: 'center', justifyContent: 'center' }}>
265
+ <RiErrorWarningLine width={20} height={20} fill={palette.errorText} />
266
+ </View>
267
+ )}
268
+ <Text
269
+ numberOfLines={2}
270
+ style={{
271
+ position: 'absolute',
272
+ top: 30,
273
+ left: 5,
274
+ right: 5,
275
+ // 9px has no type-ramp step; spelled out literally.
276
+ fontSize: 9,
277
+ lineHeight: 11,
278
+ fontWeight: '500',
279
+ color: palette.errorText,
280
+ }}>
281
+ {error}
282
+ </Text>
283
+ {onRetry ? <TileRetry label={`${retryLabel} ${attachment.name}`} onPress={onRetry} palette={palette} /> : null}
284
+ </View>
285
+ ) : null}
286
+
199
287
  {/* Upload ring over the border: the svg spans the border box. */}
200
288
  <View
201
289
  pointerEvents="none"
@@ -288,7 +376,9 @@ interface StripProps {
288
376
  attachments: ReadonlyArray<ComposerPanelAttachment>;
289
377
  palette: ComposerPalette;
290
378
  onRemove?: (id: string) => void;
379
+ onRetry?: (id: string) => void;
291
380
  removeLabel: string;
381
+ retryLabel: string;
292
382
  }
293
383
 
294
384
  /**
@@ -296,7 +386,7 @@ interface StripProps {
296
386
  * scale 0.8 → 1, fade and un-blur on arrival over 280ms `cubic-bezier(0.22, 1,
297
387
  * 0.36, 1)`, and play it backwards on dismissal before leaving the row.
298
388
  */
299
- export function AttachmentStrip({ attachments, palette, onRemove, removeLabel }: StripProps) {
389
+ export function AttachmentStrip({ attachments, palette, onRemove, onRetry, removeLabel, retryLabel }: StripProps) {
300
390
  const [leaving, setLeaving] = useState<ReadonlyArray<ComposerPanelAttachment>>([]);
301
391
  const previous = useRef(attachments);
302
392
 
@@ -330,7 +420,9 @@ export function AttachmentStrip({ attachments, palette, onRemove, removeLabel }:
330
420
  attachment={attachment}
331
421
  palette={palette}
332
422
  removeLabel={removeLabel}
423
+ retryLabel={retryLabel}
333
424
  onRemove={onRemove && !isLeaving ? () => onRemove(attachment.id) : undefined}
425
+ onRetry={onRetry && !isLeaving ? () => onRetry(attachment.id) : undefined}
334
426
  />
335
427
  </PresenceTile>
336
428
  ))}
@@ -50,6 +50,7 @@ const DEFAULT_LABELS: Required<ComposerPanelLabels> = {
50
50
  send: 'Send message',
51
51
  stop: 'Stop generating',
52
52
  remove: 'Remove',
53
+ retry: 'Retry',
53
54
  };
54
55
 
55
56
  /**
@@ -143,6 +144,7 @@ export function ComposerPanelBase({
143
144
  onListeningChange,
144
145
  attachments,
145
146
  onRemoveAttachment,
147
+ onAttachmentRetry,
146
148
  status,
147
149
  emptyAction,
148
150
  onKeyPress: onKeyPressProp,
@@ -235,7 +237,9 @@ export function ComposerPanelBase({
235
237
  attachments={attachments ?? []}
236
238
  palette={palette}
237
239
  onRemove={onRemoveAttachment}
240
+ onRetry={onAttachmentRetry}
238
241
  removeLabel={labels.remove}
242
+ retryLabel={labels.retry}
239
243
  />
240
244
  </View>
241
245
  </Collapse>
@@ -690,6 +690,7 @@ const DEFAULT_PANEL_LABELS = {
690
690
  send: 'Send message',
691
691
  stop: 'Stop generating',
692
692
  remove: 'Remove',
693
+ retry: 'Retry',
693
694
  };
694
695
 
695
696
  /**
@@ -74,6 +74,9 @@ export interface ComposerPalette {
74
74
  accent500: string;
75
75
  /** `border-focus-ring`. */
76
76
  focusRing: string;
77
+ /** A failed tile: the error subtle surface and its paired foreground. */
78
+ errorSurface: string;
79
+ errorText: string;
77
80
  /** `shadow-xs`, `shadow-dropdown` and the card's raw Figma shadow. */
78
81
  shadowXs: string;
79
82
  shadowDropdown: string;
@@ -121,6 +124,8 @@ export function resolveComposerPalette(theme: Theme): ComposerPalette {
121
124
  accent400: c.primary,
122
125
  accent500: c.primarySubtleForeground,
123
126
  focusRing: c.primary,
127
+ errorSurface: c.errorSubtle,
128
+ errorText: c.errorSubtleForeground,
124
129
  shadowXs: dark ? BUTTON_SHADOW.dark : BUTTON_SHADOW.light,
125
130
  shadowDropdown: dark ? MENU_SHADOW.dark : MENU_SHADOW.light,
126
131
  shadowCard: '0 1px 0.5px 0 rgba(0, 0, 0, 0.02), 0 4px 2px 0 rgba(0, 0, 0, 0.02)',
@@ -80,6 +80,13 @@ export interface ComposerPanelAttachment {
80
80
  * once the file has landed — that also reveals the dismiss.
81
81
  */
82
82
  progress?: number;
83
+ /**
84
+ * The upload failed, and why ("Too large", "Network error"). The tile turns
85
+ * to the error surface and shows this in place of the name; the ring and the
86
+ * percentage go (any `progress` is ignored) and the dismiss comes back. With
87
+ * `ComposerPanel`'s `onAttachmentRetry` it also gets a retry button.
88
+ */
89
+ error?: string;
83
90
  }
84
91
 
85
92
  // ---------------------------------------------------------------------------
@@ -195,6 +202,8 @@ export interface ComposerPanelLabels {
195
202
  stop?: string;
196
203
  /** Default `'Remove'` — prefixed to the file name on a tile's dismiss. */
197
204
  remove?: string;
205
+ /** Default `'Retry'` — prefixed to the file name on a failed tile's retry. */
206
+ retry?: string;
198
207
  }
199
208
 
200
209
  export interface ComposerPanelProps {
@@ -256,6 +265,8 @@ export interface ComposerPanelProps {
256
265
  /** Tiles above the prompt. Each one's `progress` drives its upload ring. */
257
266
  attachments?: ReadonlyArray<ComposerPanelAttachment>;
258
267
  onRemoveAttachment?: (id: string) => void;
268
+ /** Draws a retry button on every tile with an `error`; called with its id. */
269
+ onAttachmentRetry?: (id: string) => void;
259
270
 
260
271
  /** The tab on the card's top edge — typically `<ComposerPanelStatusTab />`. */
261
272
  status?: ReactNode;
@@ -4,8 +4,11 @@ import type { ComposerAttachmentsProps, ComposerPanelAttachment } from './types'
4
4
 
5
5
  const TICK_MS = 50;
6
6
 
7
- /** Anything carrying a progress value — the closed ring at 100 included — is still the queue's. */
8
- const inFlight = (attachment: ComposerPanelAttachment) => attachment.progress !== undefined;
7
+ /**
8
+ * Anything carrying a progress value — the closed ring at 100 included — is
9
+ * still the queue's, unless it failed: a failed file is done with.
10
+ */
11
+ const inFlight = (attachment: ComposerPanelAttachment) => attachment.progress !== undefined && !attachment.error;
9
12
 
10
13
  /** Keep the parent's order; keep our progress for files we already know. */
11
14
  function reconcile(
@@ -291,7 +291,13 @@ export function InputOtp({
291
291
  autoFocus={autoFocus && index === 0}
292
292
  // Long enough to accept a full autofilled code in one box.
293
293
  maxLength={length}
294
- selectTextOnFocus
294
+ // Typing into a filled box must REPLACE its digit, so the digit is
295
+ // selected on focus. Web's `selectTextOnFocus` does that. Android's
296
+ // does not: it selects on the input's next layout, and focusing a box
297
+ // lays nothing out, so the keystroke landed beside the old digit and
298
+ // `writeFrom` spread both across two boxes. Native selects in
299
+ // `onFocus` instead (see Search, OxyHQ/Mention#1126).
300
+ selectTextOnFocus={IS_WEB}
295
301
  caretHidden={false}
296
302
  editable={!disabled}
297
303
  accessibilityLabel={`Digit ${index + 1} of ${length}`}
@@ -300,7 +306,10 @@ export function InputOtp({
300
306
  value={digit === ' ' ? '' : digit}
301
307
  onChangeText={(text) => writeFrom(index, text)}
302
308
  onKeyPress={(event) => onKeyPress(event, index)}
303
- onFocus={() => setFocusedIndex(index)}
309
+ onFocus={() => {
310
+ setFocusedIndex(index);
311
+ if (!IS_WEB && digit !== '' && digit !== ' ') inputsRef.current[index]?.setSelection(0, 1);
312
+ }}
304
313
  onBlur={() => setFocusedIndex((current) => (current === index ? null : current))}
305
314
  keyboardAppearance={theme.isDark ? 'dark' : 'light'}
306
315
  style={[