@vertesia/ui 1.6.0-dev.20261004.085740Z → 1.6.0-dev.20261008.064737Z

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 (183) hide show
  1. package/lib/boot/vite.d.ts +3 -0
  2. package/lib/boot/vite.d.ts.map +1 -1
  3. package/lib/boot/vite.js +12 -0
  4. package/lib/boot/vite.js.map +1 -1
  5. package/lib/core/components/shadcn/tabs.d.ts +2 -1
  6. package/lib/core/components/shadcn/tabs.d.ts.map +1 -1
  7. package/lib/core/components/shadcn/tabs.js +6 -4
  8. package/lib/core/components/shadcn/tabs.js.map +1 -1
  9. package/lib/env/index.d.ts +8 -0
  10. package/lib/env/index.d.ts.map +1 -1
  11. package/lib/env/index.js.map +1 -1
  12. package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts +1 -1
  13. package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
  14. package/lib/features/agent/chat/AgentRequestInputOverlay.js +16 -10
  15. package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
  16. package/lib/features/agent/chat/AskUserWidget.d.ts +7 -1
  17. package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
  18. package/lib/features/agent/chat/AskUserWidget.js +15 -15
  19. package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
  20. package/lib/features/media-viewer/ImagePanel.d.ts +1 -1
  21. package/lib/features/media-viewer/ImagePanel.d.ts.map +1 -1
  22. package/lib/features/media-viewer/ImagePanel.js +40 -10
  23. package/lib/features/media-viewer/ImagePanel.js.map +1 -1
  24. package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
  25. package/lib/features/store/types/IntakePolicyEditor.js +1 -1
  26. package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
  27. package/lib/i18n/locales/ar.json +14 -0
  28. package/lib/i18n/locales/de.json +14 -0
  29. package/lib/i18n/locales/en.json +14 -0
  30. package/lib/i18n/locales/es.json +14 -0
  31. package/lib/i18n/locales/fr.json +14 -0
  32. package/lib/i18n/locales/it.json +14 -0
  33. package/lib/i18n/locales/ja.json +14 -0
  34. package/lib/i18n/locales/ko.json +14 -0
  35. package/lib/i18n/locales/pt.json +14 -0
  36. package/lib/i18n/locales/ru.json +14 -0
  37. package/lib/i18n/locales/tr.json +14 -0
  38. package/lib/i18n/locales/zh-TW.json +14 -0
  39. package/lib/i18n/locales/zh.json +14 -0
  40. package/lib/session/auth/oauth.d.ts.map +1 -1
  41. package/lib/session/auth/oauth.js +5 -3
  42. package/lib/session/auth/oauth.js.map +1 -1
  43. package/lib/shell/AuthScreenPreview.d.ts.map +1 -1
  44. package/lib/shell/AuthScreenPreview.js +1 -0
  45. package/lib/shell/AuthScreenPreview.js.map +1 -1
  46. package/lib/shell/login/SignInFlow.d.ts +3 -1
  47. package/lib/shell/login/SignInFlow.d.ts.map +1 -1
  48. package/lib/shell/login/SignInFlow.js +13 -4
  49. package/lib/shell/login/SignInFlow.js.map +1 -1
  50. package/lib/shell/login/SignInIcons.d.ts.map +1 -1
  51. package/lib/shell/login/SignInIcons.js +2 -1
  52. package/lib/shell/login/SignInIcons.js.map +1 -1
  53. package/lib/shell/login/SignInPasswordStep.d.ts +9 -0
  54. package/lib/shell/login/SignInPasswordStep.d.ts.map +1 -0
  55. package/lib/shell/login/SignInPasswordStep.js +58 -0
  56. package/lib/shell/login/SignInPasswordStep.js.map +1 -0
  57. package/lib/shell/login/SignInPrimitives.d.ts +13 -0
  58. package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
  59. package/lib/shell/login/SignInPrimitives.js +11 -1
  60. package/lib/shell/login/SignInPrimitives.js.map +1 -1
  61. package/lib/shell/login/SignInProvidersStep.d.ts +3 -1
  62. package/lib/shell/login/SignInProvidersStep.d.ts.map +1 -1
  63. package/lib/shell/login/SignInProvidersStep.js +4 -2
  64. package/lib/shell/login/SignInProvidersStep.js.map +1 -1
  65. package/lib/shell/login/SignInReturningStep.d.ts +3 -1
  66. package/lib/shell/login/SignInReturningStep.d.ts.map +1 -1
  67. package/lib/shell/login/SignInReturningStep.js +18 -8
  68. package/lib/shell/login/SignInReturningStep.js.map +1 -1
  69. package/lib/shell/login/SignInTenantStep.d.ts +3 -1
  70. package/lib/shell/login/SignInTenantStep.d.ts.map +1 -1
  71. package/lib/shell/login/SignInTenantStep.js +2 -2
  72. package/lib/shell/login/SignInTenantStep.js.map +1 -1
  73. package/lib/shell/login/SignInWithProviderButton.d.ts +5 -3
  74. package/lib/shell/login/SignInWithProviderButton.d.ts.map +1 -1
  75. package/lib/shell/login/SignInWithProviderButton.js +8 -7
  76. package/lib/shell/login/SignInWithProviderButton.js.map +1 -1
  77. package/lib/shell/login/SigninScreen.d.ts.map +1 -1
  78. package/lib/shell/login/SigninScreen.js +9 -3
  79. package/lib/shell/login/SigninScreen.js.map +1 -1
  80. package/lib/shell/login/signInUtils.d.ts +24 -4
  81. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  82. package/lib/shell/login/signInUtils.js +53 -1
  83. package/lib/shell/login/signInUtils.js.map +1 -1
  84. package/lib/vertesia-ui-boot.js.map +1 -1
  85. package/lib/vertesia-ui-core.js +42 -42
  86. package/lib/vertesia-ui-core.js.map +1 -1
  87. package/lib/vertesia-ui-env.js.map +1 -1
  88. package/lib/vertesia-ui-features.js +339 -339
  89. package/lib/vertesia-ui-features.js.map +1 -1
  90. package/lib/vertesia-ui-i18n.B0orbJb6.chunk.js +5 -0
  91. package/lib/vertesia-ui-i18n.B0orbJb6.chunk.js.map +1 -0
  92. package/lib/vertesia-ui-i18n.BChwIQVA.chunk.js +5 -0
  93. package/lib/vertesia-ui-i18n.BChwIQVA.chunk.js.map +1 -0
  94. package/lib/vertesia-ui-i18n.BTEcAVQm.chunk.js +5 -0
  95. package/lib/vertesia-ui-i18n.BTEcAVQm.chunk.js.map +1 -0
  96. package/lib/vertesia-ui-i18n.BeJQKbv7.chunk.js +5 -0
  97. package/lib/vertesia-ui-i18n.BeJQKbv7.chunk.js.map +1 -0
  98. package/lib/vertesia-ui-i18n.BpGr0-5g.chunk.js +5 -0
  99. package/lib/vertesia-ui-i18n.BpGr0-5g.chunk.js.map +1 -0
  100. package/lib/vertesia-ui-i18n.Bs8x-1wI.chunk.js +5 -0
  101. package/lib/vertesia-ui-i18n.Bs8x-1wI.chunk.js.map +1 -0
  102. package/lib/vertesia-ui-i18n.BymQoOm0.chunk.js +5 -0
  103. package/lib/vertesia-ui-i18n.BymQoOm0.chunk.js.map +1 -0
  104. package/lib/vertesia-ui-i18n.CNT_wabV.chunk.js +5 -0
  105. package/lib/vertesia-ui-i18n.CNT_wabV.chunk.js.map +1 -0
  106. package/lib/vertesia-ui-i18n.Cxh1iVk0.chunk.js +5 -0
  107. package/lib/vertesia-ui-i18n.Cxh1iVk0.chunk.js.map +1 -0
  108. package/lib/vertesia-ui-i18n.DLYCM9fV.chunk.js +5 -0
  109. package/lib/vertesia-ui-i18n.DLYCM9fV.chunk.js.map +1 -0
  110. package/lib/vertesia-ui-i18n.QvAVguDs.chunk.js +5 -0
  111. package/lib/vertesia-ui-i18n.QvAVguDs.chunk.js.map +1 -0
  112. package/lib/vertesia-ui-i18n.js +2 -2
  113. package/lib/vertesia-ui-i18n.js.map +1 -1
  114. package/lib/vertesia-ui-i18n.smozeSmI.chunk.js +5 -0
  115. package/lib/vertesia-ui-i18n.smozeSmI.chunk.js.map +1 -0
  116. package/lib/vertesia-ui-layout.js +15 -15
  117. package/lib/vertesia-ui-layout.js.map +1 -1
  118. package/lib/vertesia-ui-rich-text.js +40 -40
  119. package/lib/vertesia-ui-rich-text.js.map +1 -1
  120. package/lib/vertesia-ui-router.js.map +1 -1
  121. package/lib/vertesia-ui-session.js +1 -1
  122. package/lib/vertesia-ui-session.js.map +1 -1
  123. package/lib/vertesia-ui-shell.js +54 -47
  124. package/lib/vertesia-ui-shell.js.map +1 -1
  125. package/lib/vertesia-ui-widgets.js +47 -47
  126. package/lib/vertesia-ui-widgets.js.map +1 -1
  127. package/package.json +20 -20
  128. package/src/boot/vite.ts +15 -0
  129. package/src/core/components/shadcn/tabs.tsx +9 -3
  130. package/src/env/index.ts +10 -1
  131. package/src/features/agent/chat/AgentRequestInputOverlay.tsx +73 -34
  132. package/src/features/agent/chat/AskUserWidget.tsx +222 -205
  133. package/src/features/media-viewer/ImagePanel.tsx +51 -10
  134. package/src/features/store/types/IntakePolicyEditor.tsx +2 -1
  135. package/src/i18n/locales/ar.json +14 -0
  136. package/src/i18n/locales/de.json +14 -0
  137. package/src/i18n/locales/en.json +14 -0
  138. package/src/i18n/locales/es.json +14 -0
  139. package/src/i18n/locales/fr.json +14 -0
  140. package/src/i18n/locales/it.json +14 -0
  141. package/src/i18n/locales/ja.json +14 -0
  142. package/src/i18n/locales/ko.json +14 -0
  143. package/src/i18n/locales/pt.json +14 -0
  144. package/src/i18n/locales/ru.json +14 -0
  145. package/src/i18n/locales/tr.json +14 -0
  146. package/src/i18n/locales/zh-TW.json +14 -0
  147. package/src/i18n/locales/zh.json +14 -0
  148. package/src/session/auth/oauth.ts +8 -3
  149. package/src/shell/AuthScreenPreview.tsx +1 -0
  150. package/src/shell/login/SignInFlow.tsx +23 -2
  151. package/src/shell/login/SignInIcons.tsx +2 -1
  152. package/src/shell/login/SignInPasswordStep.tsx +103 -0
  153. package/src/shell/login/SignInPrimitives.tsx +53 -1
  154. package/src/shell/login/SignInProvidersStep.tsx +8 -4
  155. package/src/shell/login/SignInReturningStep.tsx +20 -8
  156. package/src/shell/login/SignInTenantStep.tsx +6 -2
  157. package/src/shell/login/SignInWithProviderButton.tsx +11 -7
  158. package/src/shell/login/SigninScreen.tsx +8 -3
  159. package/src/shell/login/signInUtils.ts +68 -8
  160. package/lib/vertesia-ui-i18n.BCPusS2_.chunk.js +0 -5
  161. package/lib/vertesia-ui-i18n.BCPusS2_.chunk.js.map +0 -1
  162. package/lib/vertesia-ui-i18n.BHWlHchf.chunk.js +0 -5
  163. package/lib/vertesia-ui-i18n.BHWlHchf.chunk.js.map +0 -1
  164. package/lib/vertesia-ui-i18n.BkNx8SZu.chunk.js +0 -5
  165. package/lib/vertesia-ui-i18n.BkNx8SZu.chunk.js.map +0 -1
  166. package/lib/vertesia-ui-i18n.C1LasLrO.chunk.js +0 -5
  167. package/lib/vertesia-ui-i18n.C1LasLrO.chunk.js.map +0 -1
  168. package/lib/vertesia-ui-i18n.D8PQ5yKU.chunk.js +0 -5
  169. package/lib/vertesia-ui-i18n.D8PQ5yKU.chunk.js.map +0 -1
  170. package/lib/vertesia-ui-i18n.DUtVV1Bq.chunk.js +0 -5
  171. package/lib/vertesia-ui-i18n.DUtVV1Bq.chunk.js.map +0 -1
  172. package/lib/vertesia-ui-i18n.DgA36_k5.chunk.js +0 -5
  173. package/lib/vertesia-ui-i18n.DgA36_k5.chunk.js.map +0 -1
  174. package/lib/vertesia-ui-i18n.DmG-fjjZ.chunk.js +0 -5
  175. package/lib/vertesia-ui-i18n.DmG-fjjZ.chunk.js.map +0 -1
  176. package/lib/vertesia-ui-i18n.DreWvQIk.chunk.js +0 -5
  177. package/lib/vertesia-ui-i18n.DreWvQIk.chunk.js.map +0 -1
  178. package/lib/vertesia-ui-i18n.Pp8Tj48d.chunk.js +0 -5
  179. package/lib/vertesia-ui-i18n.Pp8Tj48d.chunk.js.map +0 -1
  180. package/lib/vertesia-ui-i18n.gais--x5.chunk.js +0 -5
  181. package/lib/vertesia-ui-i18n.gais--x5.chunk.js.map +0 -1
  182. package/lib/vertesia-ui-i18n.xkqaKpY_.chunk.js +0 -5
  183. package/lib/vertesia-ui-i18n.xkqaKpY_.chunk.js.map +0 -1
@@ -64,6 +64,12 @@ export interface AskUserWidgetProps {
64
64
  compact?: boolean;
65
65
  /** Render as a resolved transcript prompt, without pending controls */
66
66
  answered?: boolean;
67
+ /** Optional action beside the question, inside the existing card header. */
68
+ headerAction?: React.ReactNode;
69
+ /** ID of the response controls for an accessible disclosure action. */
70
+ responseControlsId?: string;
71
+ /** Hide response controls while keeping drafts and selections mounted. */
72
+ responseControlsHidden?: boolean;
67
73
 
68
74
  // Styling props for full customization
69
75
  /** Additional className for the outer container */
@@ -146,6 +152,9 @@ export function AskUserWidget({
146
152
  hideBorder = false,
147
153
  compact = false,
148
154
  answered = false,
155
+ headerAction,
156
+ responseControlsId,
157
+ responseControlsHidden = false,
149
158
  // Styling props
150
159
  className,
151
160
  cardClassName,
@@ -260,124 +269,127 @@ export function AskUserWidget({
260
269
  )}
261
270
  </div>
262
271
  </div>
272
+ {headerAction}
263
273
  </div>
264
274
  </div>
265
275
 
266
- {safeOptions.length > 0 && (
267
- <div className={cn('flex flex-col gap-1.5 px-3 pb-3 pt-0', optionsClassName)}>
268
- {multiSelect ? (
269
- <>
270
- {safeOptions.map((option) => {
271
- const selected = selectedOptions.has(option.id);
272
- return (
273
- <label
274
- key={option.id}
275
- className={cn(
276
- 'flex cursor-pointer items-start gap-2.5 rounded-md border px-3 py-2 text-start transition-colors',
277
- selected
278
- ? 'border-info/60 bg-info/10'
279
- : 'border-border bg-background/70 hover:bg-mixer-muted/15',
280
- isLoading && 'cursor-not-allowed opacity-50',
281
- buttonClassName,
282
- )}
283
- >
284
- <input
285
- type="checkbox"
286
- checked={selected}
287
- onChange={() => toggleOption(option.id)}
288
- disabled={isLoading}
289
- className="mt-1 size-4 rounded border-border bg-background text-info focus:ring-info"
290
- />
291
- <span className="min-w-0 flex-1">
292
- <span className="flex items-center gap-2 text-sm font-medium leading-5 text-foreground">
293
- {option.icon}
294
- <span className="break-words">{option.label}</span>
295
- </span>
296
- {option.description && (
297
- <span className="mt-0.5 block break-words text-xs leading-5 text-muted">
298
- {option.description}
299
- </span>
276
+ <div id={responseControlsId} hidden={responseControlsHidden}>
277
+ {safeOptions.length > 0 && (
278
+ <div className={cn('flex flex-col gap-1.5 px-3 pb-3 pt-0', optionsClassName)}>
279
+ {multiSelect ? (
280
+ <>
281
+ {safeOptions.map((option) => {
282
+ const selected = selectedOptions.has(option.id);
283
+ return (
284
+ <label
285
+ key={option.id}
286
+ className={cn(
287
+ 'flex cursor-pointer items-start gap-2.5 rounded-md border px-3 py-2 text-start transition-colors',
288
+ selected
289
+ ? 'border-info/60 bg-info/10'
290
+ : 'border-border bg-background/70 hover:bg-mixer-muted/15',
291
+ isLoading && 'cursor-not-allowed opacity-50',
292
+ buttonClassName,
300
293
  )}
301
- </span>
302
- </label>
303
- );
304
- })}
305
- <div className="pt-1">
306
- <Button
307
- size="sm"
308
- onClick={handleMultiSubmit}
309
- disabled={isLoading || selectedOptions.size === 0}
310
- className="inline-flex items-center gap-2"
294
+ >
295
+ <input
296
+ type="checkbox"
297
+ checked={selected}
298
+ onChange={() => toggleOption(option.id)}
299
+ disabled={isLoading}
300
+ className="mt-1 size-4 rounded border-border bg-background text-info focus:ring-info"
301
+ />
302
+ <span className="min-w-0 flex-1">
303
+ <span className="flex items-center gap-2 text-sm font-medium leading-5 text-foreground">
304
+ {option.icon}
305
+ <span className="break-words">{option.label}</span>
306
+ </span>
307
+ {option.description && (
308
+ <span className="mt-0.5 block break-words text-xs leading-5 text-muted">
309
+ {option.description}
310
+ </span>
311
+ )}
312
+ </span>
313
+ </label>
314
+ );
315
+ })}
316
+ <div className="pt-1">
317
+ <Button
318
+ size="sm"
319
+ onClick={handleMultiSubmit}
320
+ disabled={isLoading || selectedOptions.size === 0}
321
+ className="inline-flex items-center gap-2"
322
+ >
323
+ <Send className="size-4" />
324
+ {selectedOptions.size > 0
325
+ ? t('agent.submitSelectionCount', { count: selectedOptions.size })
326
+ : t('agent.submitSelection')}
327
+ </Button>
328
+ </div>
329
+ </>
330
+ ) : (
331
+ safeOptions.map((option) => (
332
+ <button
333
+ type="button"
334
+ key={option.id}
335
+ onClick={() => onSelect?.(option.id)}
336
+ disabled={isLoading}
337
+ className={cn(
338
+ 'flex w-full cursor-pointer items-start gap-2.5 rounded-md border border-border bg-background/70 px-3 py-2 text-start transition-colors',
339
+ 'hover:bg-mixer-muted/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
340
+ isLoading && 'cursor-not-allowed opacity-50',
341
+ buttonClassName,
342
+ )}
311
343
  >
312
- <Send className="size-4" />
313
- {selectedOptions.size > 0
314
- ? t('agent.submitSelectionCount', { count: selectedOptions.size })
315
- : t('agent.submitSelection')}
316
- </Button>
317
- </div>
318
- </>
319
- ) : (
320
- safeOptions.map((option) => (
321
- <button
322
- type="button"
323
- key={option.id}
324
- onClick={() => onSelect?.(option.id)}
344
+ <span
345
+ className="mt-1 size-4 flex-shrink-0 rounded-full border border-border"
346
+ aria-hidden="true"
347
+ />
348
+ <span className="min-w-0 flex-1">
349
+ <span className="flex items-center gap-2 text-sm font-medium leading-5 text-foreground">
350
+ {option.icon}
351
+ <span className="break-words">{option.label}</span>
352
+ </span>
353
+ {option.description && (
354
+ <span className="mt-0.5 block break-words text-xs leading-5 text-muted">
355
+ {option.description}
356
+ </span>
357
+ )}
358
+ </span>
359
+ </button>
360
+ ))
361
+ )}
362
+ </div>
363
+ )}
364
+
365
+ {allowFreeResponse && (
366
+ <div className={cn('px-3 pb-3 pt-0', inputContainerClassName)}>
367
+ <div className="flex gap-2">
368
+ <input
369
+ ref={inputRef}
370
+ type="text"
371
+ value={inputValue}
372
+ onChange={(e) => setInputValue(e.target.value)}
373
+ onKeyDown={handleKeyDown}
374
+ placeholder={resolvedPlaceholder}
325
375
  disabled={isLoading}
326
376
  className={cn(
327
- 'flex w-full cursor-pointer items-start gap-2.5 rounded-md border border-border bg-background/70 px-3 py-2 text-start transition-colors',
328
- 'hover:bg-mixer-muted/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
329
- isLoading && 'cursor-not-allowed opacity-50',
330
- buttonClassName,
377
+ 'min-w-0 flex-1 rounded-md border border-border bg-background/70 px-3 py-2 text-sm text-foreground focus:border-transparent focus:ring-2 focus:ring-ring',
378
+ inputClassName,
331
379
  )}
380
+ />
381
+ <Button
382
+ size="sm"
383
+ onClick={handleSubmit}
384
+ disabled={isLoading || !inputValue.trim()}
385
+ className={submitButtonClassName}
332
386
  >
333
- <span
334
- className="mt-1 size-4 flex-shrink-0 rounded-full border border-border"
335
- aria-hidden="true"
336
- />
337
- <span className="min-w-0 flex-1">
338
- <span className="flex items-center gap-2 text-sm font-medium leading-5 text-foreground">
339
- {option.icon}
340
- <span className="break-words">{option.label}</span>
341
- </span>
342
- {option.description && (
343
- <span className="mt-0.5 block break-words text-xs leading-5 text-muted">
344
- {option.description}
345
- </span>
346
- )}
347
- </span>
348
- </button>
349
- ))
350
- )}
351
- </div>
352
- )}
353
-
354
- {allowFreeResponse && (
355
- <div className={cn('px-3 pb-3 pt-0', inputContainerClassName)}>
356
- <div className="flex gap-2">
357
- <input
358
- ref={inputRef}
359
- type="text"
360
- value={inputValue}
361
- onChange={(e) => setInputValue(e.target.value)}
362
- onKeyDown={handleKeyDown}
363
- placeholder={resolvedPlaceholder}
364
- disabled={isLoading}
365
- className={cn(
366
- 'min-w-0 flex-1 rounded-md border border-border bg-background/70 px-3 py-2 text-sm text-foreground focus:border-transparent focus:ring-2 focus:ring-ring',
367
- inputClassName,
368
- )}
369
- />
370
- <Button
371
- size="sm"
372
- onClick={handleSubmit}
373
- disabled={isLoading || !inputValue.trim()}
374
- className={submitButtonClassName}
375
- >
376
- {isLoading ? '...' : resolvedSubmitLabel}
377
- </Button>
387
+ {isLoading ? '...' : resolvedSubmitLabel}
388
+ </Button>
389
+ </div>
378
390
  </div>
379
- </div>
380
- )}
391
+ )}
392
+ </div>
381
393
  </div>
382
394
  </div>
383
395
  );
@@ -408,124 +420,129 @@ export function AskUserWidget({
408
420
  )}
409
421
  </div>
410
422
  </div>
423
+ {headerAction}
411
424
  </div>
412
425
  </div>
413
426
 
414
- {/* Options */}
415
- {safeOptions.length > 0 && (
416
- <div className={`px-4 pb-3 pt-1 ${optionsClassName || ''}`}>
417
- {multiSelect ? (
418
- /* Multi-select mode with checkboxes */
419
- <div className="space-y-2">
420
- {safeOptions.map((option) => (
421
- <label
422
- key={option.id}
423
- className={`flex items-start gap-3 px-4 py-3 rounded-lg cursor-pointer transition-colors
427
+ <div id={responseControlsId} hidden={responseControlsHidden}>
428
+ {/* Options */}
429
+ {safeOptions.length > 0 && (
430
+ <div className={`px-4 pb-3 pt-1 ${optionsClassName || ''}`}>
431
+ {multiSelect ? (
432
+ /* Multi-select mode with checkboxes */
433
+ <div className="space-y-2">
434
+ {safeOptions.map((option) => (
435
+ <label
436
+ key={option.id}
437
+ className={`flex items-start gap-3 px-4 py-3 rounded-lg cursor-pointer transition-colors
424
438
  ${
425
439
  selectedOptions.has(option.id)
426
440
  ? 'bg-info border border-info'
427
441
  : 'bg-card text-card-foreground border border-border hover:bg-muted'
428
442
  }
429
443
  ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
430
- >
431
- <input
432
- type="checkbox"
433
- checked={selectedOptions.has(option.id)}
434
- onChange={() => toggleOption(option.id)}
435
- disabled={isLoading}
436
- className="mt-0.5 w-4 h-4 rounded border-border text-info focus:ring-info bg-background"
437
- />
438
- <div className="flex-1 overflow-hidden">
439
- <div className="flex items-center gap-2 text-sm font-medium text-foreground break-words">
440
- {option.icon}
441
- <span className="break-words">{option.label}</span>
442
- </div>
443
- {option.description && (
444
- <div className="mt-1 text-sm text-muted break-words whitespace-pre-wrap">
445
- {option.description}
444
+ >
445
+ <input
446
+ type="checkbox"
447
+ checked={selectedOptions.has(option.id)}
448
+ onChange={() => toggleOption(option.id)}
449
+ disabled={isLoading}
450
+ className="mt-0.5 w-4 h-4 rounded border-border text-info focus:ring-info bg-background"
451
+ />
452
+ <div className="flex-1 overflow-hidden">
453
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground break-words">
454
+ {option.icon}
455
+ <span className="break-words">{option.label}</span>
446
456
  </div>
447
- )}
448
- </div>
449
- </label>
450
- ))}
451
- <div className="pt-2">
452
- <Button
453
- size="sm"
454
- onClick={handleMultiSubmit}
455
- disabled={isLoading || selectedOptions.size === 0}
456
- className="flex items-center gap-2"
457
- >
458
- <Send className="size-4" />
459
- {selectedOptions.size > 0
460
- ? t('agent.submitSelectionCount', { count: selectedOptions.size })
461
- : t('agent.submitSelection')}
462
- </Button>
457
+ {option.description && (
458
+ <div className="mt-1 text-sm text-muted break-words whitespace-pre-wrap">
459
+ {option.description}
460
+ </div>
461
+ )}
462
+ </div>
463
+ </label>
464
+ ))}
465
+ <div className="pt-2">
466
+ <Button
467
+ size="sm"
468
+ onClick={handleMultiSubmit}
469
+ disabled={isLoading || selectedOptions.size === 0}
470
+ className="flex items-center gap-2"
471
+ >
472
+ <Send className="size-4" />
473
+ {selectedOptions.size > 0
474
+ ? t('agent.submitSelectionCount', { count: selectedOptions.size })
475
+ : t('agent.submitSelection')}
476
+ </Button>
477
+ </div>
463
478
  </div>
464
- </div>
465
- ) : (
466
- /* Single-select mode - always use full-width card layout for clarity */
467
- <div className="flex flex-col gap-2 w-full">
468
- {safeOptions.map((option) => (
469
- <Button
470
- variant="unstyled"
471
- key={option.id}
472
- onClick={() => onSelect?.(option.id)}
473
- disabled={isLoading}
474
- className={`w-full h-auto whitespace-normal text-start px-4 py-3 rounded-lg border border-border
479
+ ) : (
480
+ /* Single-select mode - always use full-width card layout for clarity */
481
+ <div className="flex flex-col gap-2 w-full">
482
+ {safeOptions.map((option) => (
483
+ <Button
484
+ variant="unstyled"
485
+ key={option.id}
486
+ onClick={() => onSelect?.(option.id)}
487
+ disabled={isLoading}
488
+ className={`w-full h-auto whitespace-normal text-start px-4 py-3 rounded-lg border border-border
475
489
  bg-card text-card-foreground
476
490
  hover:bg-muted hover:border-border
477
491
  focus:outline-none focus:ring-2 focus:ring-info focus:ring-offset-1
478
492
  transition-colors
479
493
  ${isLoading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
480
494
  ${buttonClassName || ''}`}
481
- >
482
- <div className="flex items-start gap-3">
483
- {option.icon && (
484
- <span className="flex-shrink-0 mt-0.5 text-muted">{option.icon}</span>
485
- )}
486
- <div className="flex-1 overflow-hidden">
487
- <div className="font-medium text-sm text-foreground break-words text-center">
488
- {option.label}
489
- </div>
490
- {option.description && (
491
- <div className="mt-1 text-sm text-muted break-words whitespace-pre-wrap">
492
- {option.description}
493
- </div>
495
+ >
496
+ <div className="flex items-start gap-3">
497
+ {option.icon && (
498
+ <span className="flex-shrink-0 mt-0.5 text-muted">
499
+ {option.icon}
500
+ </span>
494
501
  )}
502
+ <div className="flex-1 overflow-hidden">
503
+ <div className="font-medium text-sm text-foreground break-words text-center">
504
+ {option.label}
505
+ </div>
506
+ {option.description && (
507
+ <div className="mt-1 text-sm text-muted break-words whitespace-pre-wrap">
508
+ {option.description}
509
+ </div>
510
+ )}
511
+ </div>
495
512
  </div>
496
- </div>
497
- </Button>
498
- ))}
499
- </div>
500
- )}
501
- </div>
502
- )}
513
+ </Button>
514
+ ))}
515
+ </div>
516
+ )}
517
+ </div>
518
+ )}
503
519
 
504
- {/* Free-form input */}
505
- {allowFreeResponse && (
506
- <div className={`px-4 pb-3 pt-1 ${inputContainerClassName || ''}`}>
507
- <div className="flex gap-2">
508
- <input
509
- ref={inputRef}
510
- type="text"
511
- value={inputValue}
512
- onChange={(e) => setInputValue(e.target.value)}
513
- onKeyDown={handleKeyDown}
514
- placeholder={resolvedPlaceholder}
515
- disabled={isLoading}
516
- className={`flex-1 px-3 py-2 text-sm border border-border rounded-md bg-card text-card-foreground focus:ring-2 focus:ring-info focus:border-transparent ${inputClassName || ''}`}
517
- />
518
- <Button
519
- size="sm"
520
- onClick={handleSubmit}
521
- disabled={isLoading || !inputValue.trim()}
522
- className={submitButtonClassName}
523
- >
524
- {isLoading ? '...' : resolvedSubmitLabel}
525
- </Button>
520
+ {/* Free-form input */}
521
+ {allowFreeResponse && (
522
+ <div className={`px-4 pb-3 pt-1 ${inputContainerClassName || ''}`}>
523
+ <div className="flex gap-2">
524
+ <input
525
+ ref={inputRef}
526
+ type="text"
527
+ value={inputValue}
528
+ onChange={(e) => setInputValue(e.target.value)}
529
+ onKeyDown={handleKeyDown}
530
+ placeholder={resolvedPlaceholder}
531
+ disabled={isLoading}
532
+ className={`flex-1 px-3 py-2 text-sm border border-border rounded-md bg-card text-card-foreground focus:ring-2 focus:ring-info focus:border-transparent ${inputClassName || ''}`}
533
+ />
534
+ <Button
535
+ size="sm"
536
+ onClick={handleSubmit}
537
+ disabled={isLoading || !inputValue.trim()}
538
+ className={submitButtonClassName}
539
+ >
540
+ {isLoading ? '...' : resolvedSubmitLabel}
541
+ </Button>
542
+ </div>
526
543
  </div>
527
- </div>
528
- )}
544
+ )}
545
+ </div>
529
546
  </div>
530
547
  </div>
531
548
  );
@@ -1,5 +1,6 @@
1
1
  import { ContentNature, type ContentObject, ImageRenditionFormat } from '@vertesia/common';
2
- import { Spinner } from '@vertesia/ui/core';
2
+ import { Button, Spinner } from '@vertesia/ui/core';
3
+ import { useUITranslation } from '@vertesia/ui/i18n';
3
4
  import { useUserSession } from '@vertesia/ui/session';
4
5
  import { useEffect, useState } from 'react';
5
6
  import { WEB_SUPPORTED_IMAGE_FORMATS } from './formats.js';
@@ -19,12 +20,27 @@ interface ImagePanelProps {
19
20
  * Renders an image from a direct URL, a storage source path, or a Vertesia ContentObject.
20
21
  * Resolution priority: `url` > `source` > `object`.
21
22
  */
22
- export function ImagePanel({ url, source, object, className }: ImagePanelProps) {
23
+ export function ImagePanel(props: ImagePanelProps) {
24
+ const [attempt, setAttempt] = useState(0);
25
+ const identity =
26
+ props.url ??
27
+ props.source ??
28
+ `${props.object?.id}:${props.object?.content?.source}:${props.object?.content?.etag}`;
29
+ return (
30
+ <ImagePanelContent key={`${identity}:${attempt}`} {...props} onRetry={() => setAttempt((value) => value + 1)} />
31
+ );
32
+ }
33
+
34
+ function ImagePanelContent({ url, source, object, className, onRetry }: ImagePanelProps & { onRetry: () => void }) {
35
+ const { t } = useUITranslation();
36
+ const [failed, setFailed] = useState(false);
23
37
  const { client } = useUserSession();
24
38
  const [imageUrl, setImageUrl] = useState<string | undefined>(url);
25
39
  const [isLoading, setIsLoading] = useState<boolean>(!url && (!!source || !!object));
26
40
 
27
41
  useEffect(() => {
42
+ let active = true;
43
+ setFailed(false);
28
44
  if (url) {
29
45
  setImageUrl(url);
30
46
  setIsLoading(false);
@@ -37,17 +53,17 @@ export function ImagePanel({ url, source, object, className }: ImagePanelProps)
37
53
  try {
38
54
  if (source) {
39
55
  const downloadUrl = await client.files.getDownloadUrl(source);
40
- setImageUrl(downloadUrl.url);
56
+ if (active) setImageUrl(downloadUrl.url);
41
57
  return;
42
58
  }
43
59
 
44
60
  if (!object) return;
45
61
 
46
- const isImage = object.metadata?.type === ContentNature.Image;
62
+ const mime = object.content?.type?.split(';', 1)[0].trim().toLowerCase();
63
+ const isImage = object.metadata?.type === ContentNature.Image || mime?.startsWith('image/');
47
64
  if (!isImage) return;
48
65
 
49
- const isOriginalWebSupported =
50
- object.content?.type && WEB_SUPPORTED_IMAGE_FORMATS.includes(object.content.type);
66
+ const isOriginalWebSupported = mime && WEB_SUPPORTED_IMAGE_FORMATS.includes(mime);
51
67
 
52
68
  try {
53
69
  const rendition = await client.objects.getRendition(object.id, {
@@ -56,7 +72,7 @@ export function ImagePanel({ url, source, object, className }: ImagePanelProps)
56
72
  sign_url: true,
57
73
  });
58
74
  if (rendition.status === 'found' && rendition.renditions?.length) {
59
- setImageUrl(rendition.renditions[0]);
75
+ if (active) setImageUrl(rendition.renditions[0]);
60
76
  return;
61
77
  }
62
78
  } catch {
@@ -65,10 +81,14 @@ export function ImagePanel({ url, source, object, className }: ImagePanelProps)
65
81
 
66
82
  if (isOriginalWebSupported && object.content?.source) {
67
83
  const downloadUrl = await client.files.getDownloadUrl(object.content.source);
68
- setImageUrl(downloadUrl.url);
84
+ if (active) setImageUrl(downloadUrl.url);
85
+ } else if (active) {
86
+ setFailed(true);
69
87
  }
88
+ } catch {
89
+ if (active) setFailed(true);
70
90
  } finally {
71
- setIsLoading(false);
91
+ if (active) setIsLoading(false);
72
92
  }
73
93
  };
74
94
 
@@ -78,6 +98,9 @@ export function ImagePanel({ url, source, object, className }: ImagePanelProps)
78
98
  } else {
79
99
  setIsLoading(false);
80
100
  }
101
+ return () => {
102
+ active = false;
103
+ };
81
104
  }, [url, source, object, client]);
82
105
 
83
106
  if (isLoading) {
@@ -88,9 +111,27 @@ export function ImagePanel({ url, source, object, className }: ImagePanelProps)
88
111
  );
89
112
  }
90
113
 
114
+ if (failed) {
115
+ return (
116
+ <div role="alert" className={`flex flex-col items-center gap-2 p-4 ${className ?? ''}`.trim()}>
117
+ <p>{t('store.failedToLoadDocument')}</p>
118
+ <Button variant="outline" onClick={onRetry}>
119
+ {t('agent.retry')}
120
+ </Button>
121
+ </div>
122
+ );
123
+ }
124
+
91
125
  if (!imageUrl) {
92
126
  return null;
93
127
  }
94
128
 
95
- return <img src={imageUrl} alt={object?.name} className={`w-full object-contain ${className ?? ''}`.trim()} />;
129
+ return (
130
+ <img
131
+ src={imageUrl}
132
+ alt={object?.name ?? t('intakePolicy.option.image')}
133
+ onError={() => setFailed(true)}
134
+ className={`w-full object-contain ${className ?? ''}`.trim()}
135
+ />
136
+ );
96
137
  }
@@ -547,6 +547,7 @@ export function IntakePolicyEditor({
547
547
  <div className="flex h-full min-h-0 w-full flex-col gap-2">
548
548
  <IntakeSummary policy={summaryPolicy} />
549
549
  <Tabs
550
+ orientation="vertical"
550
551
  tabs={leadingTabs ? [...leadingTabs, ...policyTabs] : policyTabs}
551
552
  current={activeTab}
552
553
  onTabChange={onTabChange}
@@ -554,7 +555,7 @@ export function IntakePolicyEditor({
554
555
  fullHeight
555
556
  className="px-0 min-h-0 flex-1 flex-row!"
556
557
  >
557
- <TabsBar className="py-2 w-48 shrink-0" direction="vertical" />
558
+ <TabsBar className="py-2 w-48 shrink-0" />
558
559
  <TabsPanel className="min-h-0 min-w-0 flex-1 overflow-auto pb-0!" />
559
560
  </Tabs>
560
561
  </div>