@elabs-ai/components-ai 4.1.0 → 5.0.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/README.md +3 -3
- package/dist/{_audio-player-media-chrome-T3XVXWRZ.js → _audio-player-media-chrome.js} +1 -1
- package/dist/_code-block-theme.js +12 -0
- package/dist/_code-block-theme.js.map +1 -0
- package/dist/{_flow-boundary-SHNWLQG5.js → _flow-boundary.js} +1 -1
- package/dist/_flow-boundary.js.map +1 -0
- package/dist/_flow-lazy.js +8 -0
- package/dist/_flow-lazy.js.map +1 -0
- package/dist/_lazy-boundary-conformance.js +2 -0
- package/dist/_lazy-boundary-conformance.js.map +1 -0
- package/dist/_lazy-cjk.js +13 -0
- package/dist/_lazy-cjk.js.map +1 -0
- package/dist/_lazy-engine-boundary.js +9 -0
- package/dist/_lazy-engine-boundary.js.map +1 -0
- package/dist/_lazy-math.js +13 -0
- package/dist/_lazy-math.js.map +1 -0
- package/dist/_lazy-mermaid.js +12 -0
- package/dist/_lazy-mermaid.js.map +1 -0
- package/dist/_mermaid-error-panel.js +9 -0
- package/dist/_mermaid-error-panel.js.map +1 -0
- package/dist/{_persona-rive-JEG44YHX.js → _persona-rive.js} +19 -24
- package/dist/_persona-rive.js.map +1 -0
- package/dist/_streamdown-i18n.js +21 -0
- package/dist/_streamdown-i18n.js.map +1 -0
- package/dist/_streamdown-safety.js +10 -0
- package/dist/_streamdown-safety.js.map +1 -0
- package/dist/_theme-scope-store.js +13 -0
- package/dist/_theme-scope-store.js.map +1 -0
- package/dist/a2ui/a2ui-surface.js +17 -0
- package/dist/a2ui/a2ui-surface.js.map +1 -0
- package/dist/a2ui/core/catalog.generated.js +10 -0
- package/dist/a2ui/core/catalog.generated.js.map +1 -0
- package/dist/a2ui/core/complete-json.js +10 -0
- package/dist/a2ui/core/complete-json.js.map +1 -0
- package/dist/a2ui/core/index.js +37 -0
- package/dist/a2ui/core/index.js.map +1 -0
- package/dist/a2ui/core/schema.js +11 -0
- package/dist/a2ui/core/schema.js.map +1 -0
- package/dist/a2ui/core/spec.js +10 -0
- package/dist/a2ui/core/spec.js.map +1 -0
- package/dist/a2ui/core/validate.js +16 -0
- package/dist/a2ui/core/validate.js.map +1 -0
- package/dist/a2ui/index.js +54 -0
- package/dist/a2ui/index.js.map +1 -0
- package/dist/a2ui/ui-catalog.js +18 -0
- package/dist/a2ui/ui-catalog.js.map +1 -0
- package/dist/agent-event.js +10 -0
- package/dist/agent-event.js.map +1 -0
- package/dist/agent-timeline.js +11 -0
- package/dist/agent-timeline.js.map +1 -0
- package/dist/agent.js +25 -0
- package/dist/agent.js.map +1 -0
- package/dist/artifact.js +23 -0
- package/dist/artifact.js.map +1 -0
- package/dist/asset-preview.js +22 -0
- package/dist/asset-preview.js.map +1 -0
- package/dist/attachments.js +33 -0
- package/dist/attachments.js.map +1 -0
- package/dist/audio-player.js +30 -0
- package/dist/audio-player.js.map +1 -0
- package/dist/audio-visualizer.js +9 -0
- package/dist/audio-visualizer.js.map +1 -0
- package/dist/canvas.js +10 -0
- package/dist/canvas.js.map +1 -0
- package/dist/chain-of-thought.js +22 -0
- package/dist/chain-of-thought.js.map +1 -0
- package/dist/chat-greeting.js +8 -0
- package/dist/chat-greeting.js.map +1 -0
- package/dist/chat-shell.js +9 -0
- package/dist/chat-shell.js.map +1 -0
- package/dist/checkpoint.js +13 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/chunk-2XOIG5TG.js +72 -0
- package/dist/chunk-2XOIG5TG.js.map +1 -0
- package/dist/chunk-34CQ2VHM.js +172 -0
- package/dist/chunk-34CQ2VHM.js.map +1 -0
- package/dist/chunk-3TDHPXZZ.js +60 -0
- package/dist/chunk-3TDHPXZZ.js.map +1 -0
- package/dist/chunk-3XNPMDZY.js +478 -0
- package/dist/chunk-3XNPMDZY.js.map +1 -0
- package/dist/chunk-3YPT3V7L.js +56 -0
- package/dist/chunk-3YPT3V7L.js.map +1 -0
- package/dist/chunk-475UEU2Q.js +87 -0
- package/dist/chunk-475UEU2Q.js.map +1 -0
- package/dist/chunk-4CBTII2I.js +95 -0
- package/dist/chunk-4CBTII2I.js.map +1 -0
- package/dist/chunk-4XRNIZMG.js +22 -0
- package/dist/chunk-4XRNIZMG.js.map +1 -0
- package/dist/chunk-5NNWUKK5.js +2 -0
- package/dist/chunk-5NNWUKK5.js.map +1 -0
- package/dist/chunk-5UOMU27Z.js +164 -0
- package/dist/chunk-5UOMU27Z.js.map +1 -0
- package/dist/chunk-5VGYZ62I.js +264 -0
- package/dist/chunk-5VGYZ62I.js.map +1 -0
- package/dist/chunk-6IQPH2OS.js +21 -0
- package/dist/chunk-6IQPH2OS.js.map +1 -0
- package/dist/chunk-6PB2MXCI.js +482 -0
- package/dist/chunk-6PB2MXCI.js.map +1 -0
- package/dist/chunk-7FD5CZEU.js +215 -0
- package/dist/chunk-7FD5CZEU.js.map +1 -0
- package/dist/chunk-7RL6KZDF.js +266 -0
- package/dist/chunk-7RL6KZDF.js.map +1 -0
- package/dist/chunk-ADOYAGGK.js +482 -0
- package/dist/chunk-ADOYAGGK.js.map +1 -0
- package/dist/chunk-AXMNJMMH.js +984 -0
- package/dist/chunk-AXMNJMMH.js.map +1 -0
- package/dist/chunk-BA3AEMOU.js +21 -0
- package/dist/chunk-BA3AEMOU.js.map +1 -0
- package/dist/chunk-BBHFOYML.js +336 -0
- package/dist/chunk-BBHFOYML.js.map +1 -0
- package/dist/chunk-BHUVISIY.js +221 -0
- package/dist/chunk-BHUVISIY.js.map +1 -0
- package/dist/chunk-BM2LWERA.js +100 -0
- package/dist/chunk-BM2LWERA.js.map +1 -0
- package/dist/chunk-CMOYXNPJ.js +211 -0
- package/dist/chunk-CMOYXNPJ.js.map +1 -0
- package/dist/chunk-CZP3RHC4.js +74 -0
- package/dist/chunk-CZP3RHC4.js.map +1 -0
- package/dist/chunk-DDG5JHQT.js +112 -0
- package/dist/chunk-DDG5JHQT.js.map +1 -0
- package/dist/chunk-DJODXO7I.js +38 -0
- package/dist/chunk-DJODXO7I.js.map +1 -0
- package/dist/chunk-DWJDJLPU.js +192 -0
- package/dist/chunk-DWJDJLPU.js.map +1 -0
- package/dist/chunk-DZW6LM22.js +45 -0
- package/dist/chunk-DZW6LM22.js.map +1 -0
- package/dist/chunk-ELGCZLZF.js +305 -0
- package/dist/chunk-ELGCZLZF.js.map +1 -0
- package/dist/chunk-EV3U6ZZB.js +209 -0
- package/dist/chunk-EV3U6ZZB.js.map +1 -0
- package/dist/chunk-EY7LVUKQ.js +87 -0
- package/dist/chunk-EY7LVUKQ.js.map +1 -0
- package/dist/chunk-F4C74F7M.js +230 -0
- package/dist/chunk-F4C74F7M.js.map +1 -0
- package/dist/chunk-FERYDFZA.js +108 -0
- package/dist/chunk-FERYDFZA.js.map +1 -0
- package/dist/chunk-FUIYG2DS.js +166 -0
- package/dist/chunk-FUIYG2DS.js.map +1 -0
- package/dist/chunk-GKYZT3LA.js +136 -0
- package/dist/chunk-GKYZT3LA.js.map +1 -0
- package/dist/chunk-H42LRUQ4.js +102 -0
- package/dist/chunk-H42LRUQ4.js.map +1 -0
- package/dist/chunk-HVXM2G5F.js +222 -0
- package/dist/chunk-HVXM2G5F.js.map +1 -0
- package/dist/chunk-I4RDAGVY.js +53 -0
- package/dist/chunk-I4RDAGVY.js.map +1 -0
- package/dist/chunk-IQJG4IDX.js +208 -0
- package/dist/chunk-IQJG4IDX.js.map +1 -0
- package/dist/chunk-IX2PXG4Y.js +144 -0
- package/dist/chunk-IX2PXG4Y.js.map +1 -0
- package/dist/chunk-IXICSOOP.js +287 -0
- package/dist/chunk-IXICSOOP.js.map +1 -0
- package/dist/chunk-JY5HSN4D.js +84 -0
- package/dist/chunk-JY5HSN4D.js.map +1 -0
- package/dist/chunk-LMNSBOBG.js +19 -0
- package/dist/chunk-LMNSBOBG.js.map +1 -0
- package/dist/chunk-MBI7D6IL.js +42 -0
- package/dist/chunk-MBI7D6IL.js.map +1 -0
- package/dist/chunk-MLFMYCPH.js +192 -0
- package/dist/chunk-MLFMYCPH.js.map +1 -0
- package/dist/chunk-MOK66734.js +33 -0
- package/dist/chunk-MOK66734.js.map +1 -0
- package/dist/chunk-MXTAWPYM.js +196 -0
- package/dist/chunk-MXTAWPYM.js.map +1 -0
- package/dist/chunk-NCL2UZKI.js +96 -0
- package/dist/chunk-NCL2UZKI.js.map +1 -0
- package/dist/chunk-NET4YLRY.js +107 -0
- package/dist/chunk-NET4YLRY.js.map +1 -0
- package/dist/chunk-NIG67CFM.js +329 -0
- package/dist/chunk-NIG67CFM.js.map +1 -0
- package/dist/chunk-NMVNNO3L.js +10 -0
- package/dist/chunk-NMVNNO3L.js.map +1 -0
- package/dist/chunk-NP7VID4K.js +2 -0
- package/dist/chunk-NP7VID4K.js.map +1 -0
- package/dist/chunk-NPUCMAG2.js +382 -0
- package/dist/chunk-NPUCMAG2.js.map +1 -0
- package/dist/chunk-NSCLRHES.js +102 -0
- package/dist/chunk-NSCLRHES.js.map +1 -0
- package/dist/chunk-OEUXHVFW.js +86 -0
- package/dist/chunk-OEUXHVFW.js.map +1 -0
- package/dist/chunk-ONBHFIMZ.js +137 -0
- package/dist/chunk-ONBHFIMZ.js.map +1 -0
- package/dist/chunk-OPH2HHL2.js +228 -0
- package/dist/chunk-OPH2HHL2.js.map +1 -0
- package/dist/chunk-OQERHIQP.js +287 -0
- package/dist/chunk-OQERHIQP.js.map +1 -0
- package/dist/chunk-P6YJAZFG.js +103 -0
- package/dist/chunk-P6YJAZFG.js.map +1 -0
- package/dist/chunk-PG7ERDOD.js +65 -0
- package/dist/chunk-PG7ERDOD.js.map +1 -0
- package/dist/chunk-PIB5JBMA.js +90 -0
- package/dist/chunk-PIB5JBMA.js.map +1 -0
- package/dist/chunk-PQ5BPIWQ.js +22 -0
- package/dist/chunk-PQ5BPIWQ.js.map +1 -0
- package/dist/chunk-PQBF6ZIQ.js +55 -0
- package/dist/chunk-PQBF6ZIQ.js.map +1 -0
- package/dist/chunk-PTWCTSZL.js +173 -0
- package/dist/chunk-PTWCTSZL.js.map +1 -0
- package/dist/chunk-QJUNPVOS.js +101 -0
- package/dist/chunk-QJUNPVOS.js.map +1 -0
- package/dist/chunk-QUDZOTRV.js +43 -0
- package/dist/chunk-QUDZOTRV.js.map +1 -0
- package/dist/chunk-RB7G65JC.js +213 -0
- package/dist/chunk-RB7G65JC.js.map +1 -0
- package/dist/chunk-RPHAOR7P.js +45 -0
- package/dist/chunk-RPHAOR7P.js.map +1 -0
- package/dist/chunk-RTCSH2AN.js +331 -0
- package/dist/chunk-RTCSH2AN.js.map +1 -0
- package/dist/chunk-S3ISPW3A.js +28 -0
- package/dist/chunk-S3ISPW3A.js.map +1 -0
- package/dist/chunk-SEVN656N.js +36 -0
- package/dist/chunk-SEVN656N.js.map +1 -0
- package/dist/chunk-SKVG672B.js +64 -0
- package/dist/chunk-SKVG672B.js.map +1 -0
- package/dist/chunk-SPQIU7GA.js +236 -0
- package/dist/chunk-SPQIU7GA.js.map +1 -0
- package/dist/chunk-SSGJ6MEM.js +1076 -0
- package/dist/chunk-SSGJ6MEM.js.map +1 -0
- package/dist/chunk-SSKZ2AED.js +118 -0
- package/dist/chunk-SSKZ2AED.js.map +1 -0
- package/dist/chunk-T56JFGXI.js +561 -0
- package/dist/chunk-T56JFGXI.js.map +1 -0
- package/dist/chunk-TCB6O74A.js +137 -0
- package/dist/chunk-TCB6O74A.js.map +1 -0
- package/dist/chunk-TTEYPO6X.js +57 -0
- package/dist/chunk-TTEYPO6X.js.map +1 -0
- package/dist/chunk-TWPHAJNM.js +462 -0
- package/dist/chunk-TWPHAJNM.js.map +1 -0
- package/dist/chunk-TXJJLP4O.js +336 -0
- package/dist/chunk-TXJJLP4O.js.map +1 -0
- package/dist/chunk-TZ6TB7VM.js +245 -0
- package/dist/chunk-TZ6TB7VM.js.map +1 -0
- package/dist/chunk-UBLKJJ7K.js +278 -0
- package/dist/chunk-UBLKJJ7K.js.map +1 -0
- package/dist/chunk-UCJ6IUFS.js +52 -0
- package/dist/chunk-UCJ6IUFS.js.map +1 -0
- package/dist/chunk-UCKFJ3QF.js +197 -0
- package/dist/chunk-UCKFJ3QF.js.map +1 -0
- package/dist/chunk-UPXSLILN.js +20 -0
- package/dist/chunk-UPXSLILN.js.map +1 -0
- package/dist/chunk-V4DORGWJ.js +92 -0
- package/dist/chunk-V4DORGWJ.js.map +1 -0
- package/dist/chunk-V6E5DYZD.js +80 -0
- package/dist/chunk-V6E5DYZD.js.map +1 -0
- package/dist/chunk-VLLYKF3P.js +96 -0
- package/dist/chunk-VLLYKF3P.js.map +1 -0
- package/dist/chunk-VLNBTOH6.js +43 -0
- package/dist/chunk-VLNBTOH6.js.map +1 -0
- package/dist/chunk-WMDSMSPL.js +303 -0
- package/dist/chunk-WMDSMSPL.js.map +1 -0
- package/dist/chunk-WU5BLGTF.js +149 -0
- package/dist/chunk-WU5BLGTF.js.map +1 -0
- package/dist/chunk-WYTZVSGK.js +245 -0
- package/dist/chunk-WYTZVSGK.js.map +1 -0
- package/dist/chunk-X47QMM6K.js +187 -0
- package/dist/chunk-X47QMM6K.js.map +1 -0
- package/dist/chunk-XNBKO32Y.js +174 -0
- package/dist/chunk-XNBKO32Y.js.map +1 -0
- package/dist/chunk-YHUSW7D2.js +40 -0
- package/dist/chunk-YHUSW7D2.js.map +1 -0
- package/dist/chunk-YSKD7UXX.js +126 -0
- package/dist/chunk-YSKD7UXX.js.map +1 -0
- package/dist/chunk-YYRDGWOU.js +72 -0
- package/dist/chunk-YYRDGWOU.js.map +1 -0
- package/dist/chunk-ZIGZMWM7.js +19 -0
- package/dist/chunk-ZIGZMWM7.js.map +1 -0
- package/dist/chunk-ZKRTUKNP.js +40 -0
- package/dist/chunk-ZKRTUKNP.js.map +1 -0
- package/dist/code-block.js +38 -0
- package/dist/code-block.js.map +1 -0
- package/dist/commit.js +51 -0
- package/dist/commit.js.map +1 -0
- package/dist/composer.js +15 -0
- package/dist/composer.js.map +1 -0
- package/dist/confirmation.js +53 -0
- package/dist/confirmation.js.map +1 -0
- package/dist/connection.js +8 -0
- package/dist/connection.js.map +1 -0
- package/dist/context-panel.js +25 -0
- package/dist/context-panel.js.map +1 -0
- package/dist/controls.js +10 -0
- package/dist/controls.js.map +1 -0
- package/dist/conversation.js +19 -0
- package/dist/conversation.js.map +1 -0
- package/dist/diff-view.js +15 -0
- package/dist/diff-view.js.map +1 -0
- package/dist/edge.js +10 -0
- package/dist/edge.js.map +1 -0
- package/dist/environment-variables.js +29 -0
- package/dist/environment-variables.js.map +1 -0
- package/dist/file-tree.js +23 -0
- package/dist/file-tree.js.map +1 -0
- package/dist/gallery.js +9 -0
- package/dist/gallery.js.map +1 -0
- package/dist/grouped-parts.js +21 -0
- package/dist/grouped-parts.js.map +1 -0
- package/dist/image.js +9 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +403 -108
- package/dist/index.js +671 -13205
- package/dist/index.js.map +1 -1
- package/dist/inline-citation.js +39 -0
- package/dist/inline-citation.js.map +1 -0
- package/dist/jsx-preview.js +17 -0
- package/dist/jsx-preview.js.map +1 -0
- package/dist/markdown-view.js +17 -0
- package/dist/markdown-view.js.map +1 -0
- package/dist/message-compare.js +12 -0
- package/dist/message-compare.js.map +1 -0
- package/dist/message-edit.js +19 -0
- package/dist/message-edit.js.map +1 -0
- package/dist/message-feedback.js +11 -0
- package/dist/message-feedback.js.map +1 -0
- package/dist/message-form-spec.js +38 -0
- package/dist/message-form-spec.js.map +1 -0
- package/dist/message-form.js +26 -0
- package/dist/message-form.js.map +1 -0
- package/dist/message-table-spec.js +26 -0
- package/dist/message-table-spec.js.map +1 -0
- package/dist/message-table.js +12 -0
- package/dist/message-table.js.map +1 -0
- package/dist/message.js +51 -0
- package/dist/message.js.map +1 -0
- package/dist/mic-selector.js +27 -0
- package/dist/mic-selector.js.map +1 -0
- package/dist/model-provider-logo.js +13 -0
- package/dist/model-provider-logo.js.map +1 -0
- package/dist/motion-config.js +9 -0
- package/dist/motion-config.js.map +1 -0
- package/dist/node.js +22 -0
- package/dist/node.js.map +1 -0
- package/dist/open-in-chat.js +31 -0
- package/dist/open-in-chat.js.map +1 -0
- package/dist/package-info.js +25 -0
- package/dist/package-info.js.map +1 -0
- package/dist/panel.js +10 -0
- package/dist/panel.js.map +1 -0
- package/dist/part-groups.js +18 -0
- package/dist/part-groups.js.map +1 -0
- package/dist/permission-mode-select.js +9 -0
- package/dist/permission-mode-select.js.map +1 -0
- package/dist/persona-sources.js +8 -0
- package/dist/persona-sources.js.map +1 -0
- package/dist/persona.js +14 -0
- package/dist/persona.js.map +1 -0
- package/dist/plan.js +32 -0
- package/dist/plan.js.map +1 -0
- package/dist/prompt-input-effort.js +9 -0
- package/dist/prompt-input-effort.js.map +1 -0
- package/dist/prompt-input-mode.js +10 -0
- package/dist/prompt-input-mode.js.map +1 -0
- package/dist/prompt-input-slash.js +12 -0
- package/dist/prompt-input-slash.js.map +1 -0
- package/dist/prompt-input.js +89 -0
- package/dist/prompt-input.js.map +1 -0
- package/dist/queue.js +37 -0
- package/dist/queue.js.map +1 -0
- package/dist/reasoning.js +23 -0
- package/dist/reasoning.js.map +1 -0
- package/dist/sandbox.js +28 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schema-display.js +33 -0
- package/dist/schema-display.js.map +1 -0
- package/dist/selection-toolbar.js +11 -0
- package/dist/selection-toolbar.js.map +1 -0
- package/dist/session-header.js +9 -0
- package/dist/session-header.js.map +1 -0
- package/dist/session-status-bar.js +9 -0
- package/dist/session-status-bar.js.map +1 -0
- package/dist/shimmer.js +9 -0
- package/dist/shimmer.js.map +1 -0
- package/dist/snippet.js +17 -0
- package/dist/snippet.js.map +1 -0
- package/dist/sources.js +17 -0
- package/dist/sources.js.map +1 -0
- package/dist/speech-input.js +9 -0
- package/dist/speech-input.js.map +1 -0
- package/dist/stack-trace.js +27 -0
- package/dist/stack-trace.js.map +1 -0
- package/dist/suggestion.js +16 -0
- package/dist/suggestion.js.map +1 -0
- package/dist/task.js +18 -0
- package/dist/task.js.map +1 -0
- package/dist/test-results.js +43 -0
- package/dist/test-results.js.map +1 -0
- package/dist/token-usage.js +27 -0
- package/dist/token-usage.js.map +1 -0
- package/dist/tool-result-card.js +9 -0
- package/dist/tool-result-card.js.map +1 -0
- package/dist/tool.js +27 -0
- package/dist/tool.js.map +1 -0
- package/dist/toolbar.js +10 -0
- package/dist/toolbar.js.map +1 -0
- package/dist/transcription.js +11 -0
- package/dist/transcription.js.map +1 -0
- package/dist/turn-status.js +9 -0
- package/dist/turn-status.js.map +1 -0
- package/dist/use-audio-level.js +9 -0
- package/dist/use-audio-level.js.map +1 -0
- package/dist/voice-selector.js +47 -0
- package/dist/voice-selector.js.map +1 -0
- package/dist/web-preview.js +19 -0
- package/dist/web-preview.js.map +1 -0
- package/package.json +12 -10
- package/schemas/a2ui-surface.v1.schema.json +5425 -0
- package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
- package/src/__contract__/chat-shell.contract.test.tsx +49 -0
- package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
- package/src/__contract__/image.contract.test.tsx +49 -0
- package/src/__contract__/markdown-view.contract.test.tsx +49 -0
- package/src/__contract__/message-feedback.contract.test.tsx +49 -0
- package/src/__contract__/message-form.contract.test.tsx +49 -0
- package/src/__contract__/message-table.contract.test.tsx +49 -0
- package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
- package/src/__contract__/persona.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
- package/src/_chat-shell-rail.tsx +4 -2
- package/src/_flow-boundary.tsx +2 -2
- package/src/_lazy-cjk.test.ts +43 -0
- package/src/_lazy-cjk.ts +73 -0
- package/src/_lazy-math.test.ts +63 -0
- package/src/_lazy-math.ts +90 -0
- package/src/_persona-rive.tsx +27 -41
- package/src/_streamdown-i18n.ts +73 -21
- package/src/_streamdown-safety.ts +2 -2
- package/src/_theme-scope-store.test.ts +83 -0
- package/src/_theme-scope-store.ts +103 -0
- package/src/a2ui/a2ui-surface.stories.tsx +662 -0
- package/src/a2ui/a2ui-surface.test.tsx +201 -0
- package/src/a2ui/a2ui-surface.tsx +354 -0
- package/src/a2ui/catalog.source.json +772 -0
- package/src/a2ui/core/a2ui-core.test.ts +178 -0
- package/src/a2ui/core/catalog.generated.ts +1006 -0
- package/src/a2ui/core/complete-json.ts +81 -0
- package/src/a2ui/core/index.ts +10 -0
- package/src/a2ui/core/schema.ts +111 -0
- package/src/a2ui/core/spec.ts +148 -0
- package/src/a2ui/core/validate.ts +266 -0
- package/src/a2ui/index.ts +7 -0
- package/src/a2ui/ui-catalog.tsx +225 -0
- package/src/a2ui/wave1-chart-spec.test.ts +87 -0
- package/src/a2ui/wave2-chart-spec.test.ts +108 -0
- package/src/agent-event.tsx +1 -1
- package/src/agent.tsx +18 -13
- package/src/agentic-workspace.stories.tsx +6 -45
- package/src/artifact.tsx +2 -2
- package/src/asset-preview.test.tsx +40 -0
- package/src/asset-preview.tsx +83 -12
- package/src/attachments.tsx +7 -4
- package/src/chat-shell.test.tsx +40 -10
- package/src/chat-shell.tsx +90 -44
- package/src/chat.stories.tsx +47 -42
- package/src/code-block.test.tsx +100 -1
- package/src/code-block.tsx +166 -103
- package/src/commit.tsx +30 -41
- package/src/composer.stories.tsx +4 -5
- package/src/composer.test.tsx +45 -0
- package/src/composer.tsx +104 -21
- package/src/confirmation.tsx +1 -1
- package/src/context-panel.stories.tsx +1 -1
- package/src/context-panel.tsx +2 -2
- package/src/conversation.stories.tsx +24 -0
- package/src/conversation.test.tsx +69 -0
- package/src/conversation.tsx +47 -8
- package/src/diff-view.stories.tsx +79 -0
- package/src/diff-view.test.tsx +52 -2
- package/src/diff-view.tsx +92 -36
- package/src/environment-variables.tsx +20 -37
- package/src/file-tree.test.tsx +21 -0
- package/src/file-tree.tsx +33 -16
- package/src/gallery.stories.tsx +42 -0
- package/src/gallery.tsx +2 -2
- package/src/grouped-parts.stories.tsx +23 -0
- package/src/grouped-parts.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/inline-citation.tsx +5 -5
- package/src/jsx-preview.tsx +151 -43
- package/src/markdown-view.test.tsx +7 -3
- package/src/markdown-view.tsx +8 -1
- package/src/message-compare.stories.tsx +40 -0
- package/src/message-compare.tsx +2 -1
- package/src/message-form.stories.tsx +36 -3
- package/src/message-form.test.tsx +8 -2
- package/src/message-form.tsx +16 -8
- package/src/message-table.stories.tsx +26 -2
- package/src/message-table.test.tsx +7 -0
- package/src/message-table.tsx +9 -5
- package/src/message.test.tsx +73 -3
- package/src/message.tsx +36 -12
- package/src/model-provider-logo.test.tsx +57 -3
- package/src/model-provider-logo.tsx +45 -11
- package/src/open-in-chat.tsx +50 -29
- package/src/package-info.tsx +23 -20
- package/src/prompt-input-slash.stories.tsx +1 -1
- package/src/prompt-input.test.tsx +67 -1
- package/src/prompt-input.tsx +42 -4
- package/src/queue.tsx +4 -4
- package/src/reasoning.stories.tsx +27 -0
- package/src/reasoning.tsx +17 -6
- package/src/sandbox.tsx +3 -3
- package/src/schema-display.test.tsx +56 -0
- package/src/schema-display.tsx +68 -37
- package/src/session-header.stories.tsx +35 -1
- package/src/session-header.tsx +1 -1
- package/src/snippet.test.tsx +6 -2
- package/src/snippet.tsx +14 -34
- package/src/sources.stories.tsx +30 -0
- package/src/sources.tsx +4 -1
- package/src/speech-input.test.tsx +109 -0
- package/src/speech-input.tsx +31 -3
- package/src/stack-trace.tsx +20 -36
- package/src/streamdown-i18n.test.tsx +23 -0
- package/src/templates-ai-assistant.stories.tsx +151 -87
- package/src/test-results.tsx +47 -27
- package/src/token-usage.tsx +6 -6
- package/src/tool.stories.tsx +39 -0
- package/src/tool.test.tsx +65 -0
- package/src/tool.tsx +61 -25
- package/src/toolbar.tsx +0 -10
- package/src/transcription.tsx +1 -1
- package/src/voice-selector.tsx +5 -5
- package/src/web-preview.test.tsx +51 -1
- package/src/web-preview.tsx +39 -8
- package/dist/_flow-boundary-SHNWLQG5.js.map +0 -1
- package/dist/_persona-rive-JEG44YHX.js.map +0 -1
- /package/dist/{_audio-player-media-chrome-T3XVXWRZ.js.map → _audio-player-media-chrome.js.map} +0 -0
package/src/composer.test.tsx
CHANGED
|
@@ -531,3 +531,48 @@ describe("Composer — shortcut hints change with a busy state (#107)", () => {
|
|
|
531
531
|
expect(shortcutRow).not.toContainElement(stopButton);
|
|
532
532
|
});
|
|
533
533
|
});
|
|
534
|
+
|
|
535
|
+
describe("Composer — mentions slot", () => {
|
|
536
|
+
const ROSTER = [
|
|
537
|
+
{ id: "u-ada", label: "Ada Lovelace" },
|
|
538
|
+
{ id: "u-grace", label: "Grace Hopper" },
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
it("opens the roster on @, inserts on Enter without submitting, then submits and resets", async () => {
|
|
542
|
+
const user = userEvent.setup();
|
|
543
|
+
const onSubmit = vi.fn();
|
|
544
|
+
const onValueChange = vi.fn();
|
|
545
|
+
render(
|
|
546
|
+
<Composer
|
|
547
|
+
onSubmit={onSubmit}
|
|
548
|
+
showAttach={false}
|
|
549
|
+
showVoice={false}
|
|
550
|
+
mentions={{ options: ROSTER, onValueChange }}
|
|
551
|
+
/>,
|
|
552
|
+
);
|
|
553
|
+
const field = screen.getByRole("textbox") as HTMLTextAreaElement;
|
|
554
|
+
// The composer's own field — what PromptInput's submit reads.
|
|
555
|
+
expect(field).toHaveAttribute("name", "message");
|
|
556
|
+
|
|
557
|
+
await user.click(field);
|
|
558
|
+
await user.keyboard("hi @ada");
|
|
559
|
+
expect(field).toHaveAttribute("data-state", "open");
|
|
560
|
+
await user.keyboard("{Enter}");
|
|
561
|
+
expect(field.value).toBe("hi @Ada Lovelace ");
|
|
562
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
563
|
+
expect(onValueChange).toHaveBeenLastCalledWith(
|
|
564
|
+
expect.objectContaining({ mentions: [expect.objectContaining({ id: "u-ada" })] }),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
await user.keyboard("{Enter}");
|
|
568
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
569
|
+
expect(onSubmit.mock.calls[0]![0].text.trim()).toBe("hi @Ada Lovelace");
|
|
570
|
+
expect(field.value).toBe("");
|
|
571
|
+
expect(onValueChange).toHaveBeenLastCalledWith({ text: "", mentions: [] });
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("refuses slashCommands and mentions together at the type level", () => {
|
|
575
|
+
// @ts-expect-error — the field takes a slash palette OR a mention roster.
|
|
576
|
+
render(<Composer slashCommands={[]} mentions={{ options: ROSTER }} />);
|
|
577
|
+
});
|
|
578
|
+
});
|
package/src/composer.tsx
CHANGED
|
@@ -2,7 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useRef, useState, type ComponentProps, type ReactNode } from "react";
|
|
4
4
|
import { ArrowUp, Mic, Paperclip, Sparkles } from "lucide-react";
|
|
5
|
-
import {
|
|
5
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
6
|
+
import {
|
|
7
|
+
cn,
|
|
8
|
+
Kbd,
|
|
9
|
+
MentionInput,
|
|
10
|
+
MentionInputContent,
|
|
11
|
+
MentionInputEmpty,
|
|
12
|
+
MentionInputItem,
|
|
13
|
+
MentionInputList,
|
|
14
|
+
MentionInputTextarea,
|
|
15
|
+
useLocale,
|
|
16
|
+
type MentionOption,
|
|
17
|
+
type MentionValue,
|
|
18
|
+
} from "@elabs-ai/components-ui";
|
|
6
19
|
|
|
7
20
|
import {
|
|
8
21
|
PromptInput,
|
|
@@ -62,7 +75,29 @@ export interface ComposerShortcut {
|
|
|
62
75
|
label: string;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
/**
|
|
79
|
+
* An `@`-mention roster for the composer field (`MentionInput` from
|
|
80
|
+
* `@elabs-ai/components-ui`). The field keeps its own submit contract; the
|
|
81
|
+
* mentions ride alongside the text in a `MentionValue`.
|
|
82
|
+
*
|
|
83
|
+
* Read the mentions on submit from `value` (controlled) or from
|
|
84
|
+
* `onValueChange`, and resolve them with `serializeMentions(value)`
|
|
85
|
+
* (`{ text, mentionedIds }`). `Composer` resets the value to empty after a
|
|
86
|
+
* submit it accepts — through `onValueChange`, so a controlled caller's state
|
|
87
|
+
* clears too.
|
|
88
|
+
*/
|
|
89
|
+
export interface ComposerMentions {
|
|
90
|
+
/** Who (or what) can be mentioned. */
|
|
91
|
+
options: MentionOption[];
|
|
92
|
+
/** Controlled value. Omit for an uncontrolled roster. */
|
|
93
|
+
value?: MentionValue;
|
|
94
|
+
/** Fires on every edit, pick and post-submit reset. */
|
|
95
|
+
onValueChange?: (value: MentionValue) => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const EMPTY_MENTION_VALUE: MentionValue = { text: "", mentions: [] };
|
|
99
|
+
|
|
100
|
+
export interface ComposerBaseProps {
|
|
66
101
|
/** Submit handler — receives the assembled message (text + attachments). */
|
|
67
102
|
onSubmit?: PromptInputProps["onSubmit"];
|
|
68
103
|
/** Textarea placeholder. Default `"Ask me anything…"`. */
|
|
@@ -96,20 +131,6 @@ export interface ComposerProps {
|
|
|
96
131
|
* and is entirely yours; `aria-label` names the scale.
|
|
97
132
|
*/
|
|
98
133
|
effort?: ComposerEffortProps;
|
|
99
|
-
/**
|
|
100
|
-
* Slash-command palette. When set (and non-empty) the textarea becomes a
|
|
101
|
-
* `PromptInputSlashTextarea` wrapped in a `PromptInputSlash`, so typing `/`
|
|
102
|
-
* at the start of a line opens a filtered command list — nothing else about
|
|
103
|
-
* the composer changes.
|
|
104
|
-
*
|
|
105
|
-
* Note this makes the textarea REACT-CONTROLLED (the palette needs to read
|
|
106
|
-
* and splice the text), which is why `Composer` clears its own copy of the
|
|
107
|
-
* text on submit: `PromptInput`'s `form.reset()` reaches the DOM node, not
|
|
108
|
-
* React state.
|
|
109
|
-
*/
|
|
110
|
-
slashCommands?: PromptInputSlashProps["commands"];
|
|
111
|
-
/** Fires with the chosen slash command (alongside the text splice). */
|
|
112
|
-
onSlashCommand?: PromptInputSlashProps["onSelect"];
|
|
113
134
|
/**
|
|
114
135
|
* Override the DEFAULT tool buttons (today: the attach button) — e.g. to add
|
|
115
136
|
* a web-search or connect-data control. Render `PromptInputButton`s /
|
|
@@ -185,6 +206,38 @@ export interface ComposerProps {
|
|
|
185
206
|
className?: string;
|
|
186
207
|
}
|
|
187
208
|
|
|
209
|
+
/**
|
|
210
|
+
* The field enhancements that each take over the textarea — a slash palette OR
|
|
211
|
+
* a mention roster, never both. The union makes the impossible combination a
|
|
212
|
+
* type error instead of a silent precedence rule.
|
|
213
|
+
*/
|
|
214
|
+
export type ComposerFieldProps =
|
|
215
|
+
| {
|
|
216
|
+
/**
|
|
217
|
+
* Slash-command palette. When set (and non-empty) the textarea becomes a
|
|
218
|
+
* `PromptInputSlashTextarea` wrapped in a `PromptInputSlash`, so typing `/`
|
|
219
|
+
* at the start of a line opens a filtered command list — nothing else about
|
|
220
|
+
* the composer changes.
|
|
221
|
+
*
|
|
222
|
+
* Note this makes the textarea REACT-CONTROLLED (the palette needs to read
|
|
223
|
+
* and splice the text), which is why `Composer` clears its own copy of the
|
|
224
|
+
* text on submit: `PromptInput`'s `form.reset()` reaches the DOM node, not
|
|
225
|
+
* React state.
|
|
226
|
+
*/
|
|
227
|
+
slashCommands?: PromptInputSlashProps["commands"];
|
|
228
|
+
/** Fires with the chosen slash command (alongside the text splice). */
|
|
229
|
+
onSlashCommand?: PromptInputSlashProps["onSelect"];
|
|
230
|
+
mentions?: never;
|
|
231
|
+
}
|
|
232
|
+
| {
|
|
233
|
+
/** `@`-mention roster on the field — see `ComposerMentions`. */
|
|
234
|
+
mentions?: ComposerMentions;
|
|
235
|
+
slashCommands?: never;
|
|
236
|
+
onSlashCommand?: never;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export type ComposerProps = ComposerBaseProps & ComposerFieldProps;
|
|
240
|
+
|
|
188
241
|
/**
|
|
189
242
|
* Composer — the standard brand-ui AI chat input.
|
|
190
243
|
*
|
|
@@ -202,9 +255,10 @@ export interface ComposerProps {
|
|
|
202
255
|
*
|
|
203
256
|
* **Composer is the chat input. Every control the `PromptInput` family ships
|
|
204
257
|
* is reachable from a `Composer` prop; drop to `PromptInput` only for a
|
|
205
|
-
* bespoke shell.** `modelPicker`, `mode`, `effort
|
|
206
|
-
*
|
|
207
|
-
* `PromptInputMode`, `PromptInputEffort
|
|
258
|
+
* bespoke shell.** `modelPicker`, `mode`, `effort`, `slashCommands` and
|
|
259
|
+
* `mentions` are the slots that make that true — they render `ModelPicker`,
|
|
260
|
+
* `PromptInputMode`, `PromptInputEffort`, `PromptInputSlash` and
|
|
261
|
+
* `MentionInput` respectively,
|
|
208
262
|
* in the footer order `attach · modelPicker · mode · effort │ voice · send`
|
|
209
263
|
* (the same left-to-right arrangement `TerminalComposer` uses, so the chat and
|
|
210
264
|
* console skins agree).
|
|
@@ -218,6 +272,7 @@ export function Composer({
|
|
|
218
272
|
effort,
|
|
219
273
|
slashCommands,
|
|
220
274
|
onSlashCommand,
|
|
275
|
+
mentions,
|
|
221
276
|
tools,
|
|
222
277
|
sendStatus,
|
|
223
278
|
onStop,
|
|
@@ -239,6 +294,12 @@ export function Composer({
|
|
|
239
294
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
240
295
|
const [text, setText] = useState("");
|
|
241
296
|
const hasSlash = slashCommands !== undefined && slashCommands.length > 0;
|
|
297
|
+
// Always called (hook order); only read when `mentions` is set.
|
|
298
|
+
const [mentionValue, setMentionValue] = useControllableState<MentionValue>({
|
|
299
|
+
prop: mentions?.value,
|
|
300
|
+
defaultProp: EMPTY_MENTION_VALUE,
|
|
301
|
+
onChange: mentions?.onValueChange,
|
|
302
|
+
});
|
|
242
303
|
|
|
243
304
|
// #107: "Composer shortcut hints change with a busy state" — derived from
|
|
244
305
|
// the existing canonical `sendStatus`, never a second boolean prop. The
|
|
@@ -258,9 +319,12 @@ export function Composer({
|
|
|
258
319
|
// keep rendering the sent text straight back. Clearing here is a no-op in
|
|
259
320
|
// the uncontrolled (non-slash) arrangement.
|
|
260
321
|
setText("");
|
|
261
|
-
|
|
322
|
+
const result = onSubmit?.(message, event);
|
|
323
|
+
// `form.reset()` cannot clear a value MentionInput controls either.
|
|
324
|
+
if (mentions) setMentionValue(EMPTY_MENTION_VALUE);
|
|
325
|
+
return result;
|
|
262
326
|
},
|
|
263
|
-
[onSubmit],
|
|
327
|
+
[onSubmit, mentions, setMentionValue],
|
|
264
328
|
);
|
|
265
329
|
|
|
266
330
|
const resolvedPlaceholder = placeholder ?? t("ai.composer.placeholder");
|
|
@@ -305,6 +369,25 @@ export function Composer({
|
|
|
305
369
|
placeholder={resolvedPlaceholder}
|
|
306
370
|
/>
|
|
307
371
|
</PromptInputSlash>
|
|
372
|
+
) : mentions ? (
|
|
373
|
+
<MentionInput
|
|
374
|
+
options={mentions.options}
|
|
375
|
+
value={mentionValue}
|
|
376
|
+
onValueChange={setMentionValue}
|
|
377
|
+
>
|
|
378
|
+
{/* `asChild` lends the mention behaviour to the composer's own
|
|
379
|
+
textarea, so `name="message"` — what PromptInput's submit
|
|
380
|
+
reads — survives. */}
|
|
381
|
+
<MentionInputTextarea asChild>
|
|
382
|
+
<PromptInputTextarea placeholder={resolvedPlaceholder} />
|
|
383
|
+
</MentionInputTextarea>
|
|
384
|
+
<MentionInputContent>
|
|
385
|
+
<MentionInputList>
|
|
386
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
387
|
+
</MentionInputList>
|
|
388
|
+
<MentionInputEmpty />
|
|
389
|
+
</MentionInputContent>
|
|
390
|
+
</MentionInput>
|
|
308
391
|
) : (
|
|
309
392
|
<PromptInputTextarea placeholder={resolvedPlaceholder} />
|
|
310
393
|
)}
|
package/src/confirmation.tsx
CHANGED
|
@@ -251,7 +251,7 @@ export const ConfirmationActions = ({ className, ...props }: ConfirmationActions
|
|
|
251
251
|
export type ConfirmationActionProps = ComponentProps<typeof Button>;
|
|
252
252
|
|
|
253
253
|
export const ConfirmationAction = ({ className, ...props }: ConfirmationActionProps) => (
|
|
254
|
-
<Button className={cn("h-8 px-3 text-
|
|
254
|
+
<Button className={cn("h-8 px-3 text-body", className)} type="button" {...props} />
|
|
255
255
|
);
|
|
256
256
|
|
|
257
257
|
export type ConfirmationApproveProps = ConfirmationActionProps;
|
|
@@ -102,7 +102,7 @@ function WorkspaceDemo(providerProps: Omit<ContextPanelProviderProps, "children"
|
|
|
102
102
|
<ContextPanelProvider {...providerProps}>
|
|
103
103
|
<div className="flex h-[520px] w-full overflow-hidden rounded-xl border bg-background">
|
|
104
104
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
105
|
-
<header className="flex h-
|
|
105
|
+
<header className="flex h-header shrink-0 items-center justify-between border-b px-4">
|
|
106
106
|
<span className="text-subtitle">Agentic workspace</span>
|
|
107
107
|
<ContextPanelTrigger />
|
|
108
108
|
</header>
|
package/src/context-panel.tsx
CHANGED
|
@@ -382,7 +382,7 @@ export const ContextPanel = forwardRef<HTMLDivElement, ContextPanelProps>(functi
|
|
|
382
382
|
>
|
|
383
383
|
<SheetHeader className="sr-only">
|
|
384
384
|
<SheetTitle>{t("ai.contextPanel.title")}</SheetTitle>
|
|
385
|
-
<SheetDescription>
|
|
385
|
+
<SheetDescription>{t("ai.contextPanel.description")}</SheetDescription>
|
|
386
386
|
</SheetHeader>
|
|
387
387
|
<div className="flex h-full w-full flex-col">{children}</div>
|
|
388
388
|
</SheetContent>
|
|
@@ -469,7 +469,7 @@ export const ContextPanelHeader = forwardRef<HTMLDivElement, ContextPanelHeaderP
|
|
|
469
469
|
<div
|
|
470
470
|
ref={ref}
|
|
471
471
|
data-slot="context-panel-header"
|
|
472
|
-
className={cn("flex h-
|
|
472
|
+
className={cn("flex h-header shrink-0 items-center gap-2 border-b px-3", className)}
|
|
473
473
|
{...props}
|
|
474
474
|
>
|
|
475
475
|
{isDetail ? (
|
|
@@ -5,6 +5,7 @@ import { Message, MessageContent } from "./message";
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: "AI/Conversation",
|
|
7
7
|
component: Conversation,
|
|
8
|
+
tags: ["autodocs"],
|
|
8
9
|
parameters: {
|
|
9
10
|
layout: "padded",
|
|
10
11
|
docs: {
|
|
@@ -34,6 +35,29 @@ export const Default: Story = {
|
|
|
34
35
|
),
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* `isStreaming` suppresses the live-region chatter while tokens are still
|
|
40
|
+
* arriving (`aria-live="off"` + `aria-busy`) — see the prop's doc on
|
|
41
|
+
* `ConversationProps` for why a `"polite"` region would otherwise announce
|
|
42
|
+
* every incoming token.
|
|
43
|
+
*/
|
|
44
|
+
export const Streaming: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<div className="flex h-72 flex-col rounded-lg border">
|
|
47
|
+
<Conversation isStreaming>
|
|
48
|
+
<ConversationContent>
|
|
49
|
+
<Message from="user">
|
|
50
|
+
<MessageContent>Summarize the deploy log.</MessageContent>
|
|
51
|
+
</Message>
|
|
52
|
+
<Message from="assistant">
|
|
53
|
+
<MessageContent>Deploying to production</MessageContent>
|
|
54
|
+
</Message>
|
|
55
|
+
</ConversationContent>
|
|
56
|
+
</Conversation>
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
|
|
37
61
|
/**
|
|
38
62
|
* **Layout lock (#72).** With zero messages, `ConversationEmptyState` must render
|
|
39
63
|
* vertically centred in the conversation canvas — not pinned to the top with a
|
|
@@ -16,6 +16,7 @@ import type { UIMessage } from "ai";
|
|
|
16
16
|
import {
|
|
17
17
|
Conversation,
|
|
18
18
|
ConversationContent,
|
|
19
|
+
ConversationDownload,
|
|
19
20
|
ConversationEmptyState,
|
|
20
21
|
messagesToMarkdown,
|
|
21
22
|
} from "./conversation";
|
|
@@ -44,6 +45,22 @@ describe("Conversation — transcript region", () => {
|
|
|
44
45
|
expect(screen.getByRole("log")).toBeInTheDocument();
|
|
45
46
|
});
|
|
46
47
|
|
|
48
|
+
it("lets the content grow past the viewport so its composer inset stays scrollable", () => {
|
|
49
|
+
render(
|
|
50
|
+
<Conversation>
|
|
51
|
+
<ConversationContent>
|
|
52
|
+
<p>turn</p>
|
|
53
|
+
</ConversationContent>
|
|
54
|
+
</Conversation>,
|
|
55
|
+
);
|
|
56
|
+
const content = screen.getByText("turn").parentElement!;
|
|
57
|
+
// `min-h-0` clamps the content to the viewport height: the turns then
|
|
58
|
+
// overflow it and the bottom padding (the floating-composer inset) is
|
|
59
|
+
// left behind, so the last turn scrolls under the composer.
|
|
60
|
+
expect(content).not.toHaveClass("min-h-0");
|
|
61
|
+
expect(content).toHaveClass("shrink-0", "flex-1");
|
|
62
|
+
});
|
|
63
|
+
|
|
47
64
|
it("renders its children inside the log, in order", () => {
|
|
48
65
|
render(
|
|
49
66
|
<Conversation>
|
|
@@ -69,6 +86,46 @@ describe("Conversation — transcript region", () => {
|
|
|
69
86
|
expect(screen.getByRole("log")).toHaveClass("extra");
|
|
70
87
|
expect(screen.getByRole("log")).toHaveClass("flex-1");
|
|
71
88
|
});
|
|
89
|
+
|
|
90
|
+
it('defaults to aria-live="polite" and no aria-busy (not streaming)', () => {
|
|
91
|
+
render(
|
|
92
|
+
<Conversation>
|
|
93
|
+
<ConversationContent>x</ConversationContent>
|
|
94
|
+
</Conversation>,
|
|
95
|
+
);
|
|
96
|
+
const log = screen.getByRole("log");
|
|
97
|
+
expect(log).toHaveAttribute("aria-live", "polite");
|
|
98
|
+
expect(log).not.toHaveAttribute("aria-busy");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('suppresses aria-live and sets aria-busy while isStreaming (perf review §2 — role="log" must not announce every token)', () => {
|
|
102
|
+
render(
|
|
103
|
+
<Conversation isStreaming>
|
|
104
|
+
<ConversationContent>x</ConversationContent>
|
|
105
|
+
</Conversation>,
|
|
106
|
+
);
|
|
107
|
+
const log = screen.getByRole("log");
|
|
108
|
+
expect(log).toHaveAttribute("aria-live", "off");
|
|
109
|
+
expect(log).toHaveAttribute("aria-busy", "true");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns to aria-live=polite once streaming settles, so the final content is announced", () => {
|
|
113
|
+
const { rerender } = render(
|
|
114
|
+
<Conversation isStreaming>
|
|
115
|
+
<ConversationContent>partial</ConversationContent>
|
|
116
|
+
</Conversation>,
|
|
117
|
+
);
|
|
118
|
+
expect(screen.getByRole("log")).toHaveAttribute("aria-live", "off");
|
|
119
|
+
|
|
120
|
+
rerender(
|
|
121
|
+
<Conversation isStreaming={false}>
|
|
122
|
+
<ConversationContent>final</ConversationContent>
|
|
123
|
+
</Conversation>,
|
|
124
|
+
);
|
|
125
|
+
const log = screen.getByRole("log");
|
|
126
|
+
expect(log).toHaveAttribute("aria-live", "polite");
|
|
127
|
+
expect(log).not.toHaveAttribute("aria-busy");
|
|
128
|
+
});
|
|
72
129
|
});
|
|
73
130
|
|
|
74
131
|
describe("ConversationEmptyState", () => {
|
|
@@ -108,6 +165,18 @@ describe("ConversationEmptyState", () => {
|
|
|
108
165
|
});
|
|
109
166
|
});
|
|
110
167
|
|
|
168
|
+
describe("ConversationDownload — accessible name (accessibility.md)", () => {
|
|
169
|
+
it("names the icon-only download control", () => {
|
|
170
|
+
render(<ConversationDownload messages={messages} />);
|
|
171
|
+
expect(screen.getByRole("button", { name: "Download conversation" })).toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("lets a caller override the accessible name", () => {
|
|
175
|
+
render(<ConversationDownload aria-label="Export chat" messages={messages} />);
|
|
176
|
+
expect(screen.getByRole("button", { name: "Export chat" })).toBeInTheDocument();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
111
180
|
describe("messagesToMarkdown — the download serializer", () => {
|
|
112
181
|
it("labels each message by role and joins every text part", () => {
|
|
113
182
|
expect(messagesToMarkdown(messages)).toBe(
|
package/src/conversation.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Button, downloadBlob } from "@elabs-ai/components-ui";
|
|
3
|
+
import { Button, downloadBlob, useLocale } from "@elabs-ai/components-ui";
|
|
4
4
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
5
5
|
import type { UIMessage } from "ai";
|
|
6
6
|
import { ArrowDownIcon, DownloadIcon } from "lucide-react";
|
|
@@ -8,14 +8,27 @@ import type { ComponentProps } from "react";
|
|
|
8
8
|
import { useCallback } from "react";
|
|
9
9
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
10
10
|
|
|
11
|
-
export type ConversationProps = ComponentProps<typeof StickToBottom
|
|
11
|
+
export type ConversationProps = ComponentProps<typeof StickToBottom> & {
|
|
12
|
+
/**
|
|
13
|
+
* The assistant response is still arriving (loading-states.md
|
|
14
|
+
* `isStreaming`). `role="log"` otherwise announces every incoming token to
|
|
15
|
+
* assistive tech as it streams in — deafening. While `true`, `aria-live`
|
|
16
|
+
* is suppressed (`"off"`) and `aria-busy` is set; once streaming ends,
|
|
17
|
+
* `aria-live` returns to `"polite"` so the settled content is what
|
|
18
|
+
* actually gets announced.
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
isStreaming?: boolean;
|
|
22
|
+
};
|
|
12
23
|
|
|
13
|
-
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
|
24
|
+
export const Conversation = ({ className, isStreaming = false, ...props }: ConversationProps) => (
|
|
14
25
|
<StickToBottom
|
|
15
26
|
className={cn("relative flex-1 overflow-y-hidden", className)}
|
|
16
27
|
initial="smooth"
|
|
17
28
|
resize="smooth"
|
|
18
29
|
role="log"
|
|
30
|
+
aria-live={isStreaming ? "off" : "polite"}
|
|
31
|
+
aria-busy={isStreaming || undefined}
|
|
19
32
|
{...props}
|
|
20
33
|
/>
|
|
21
34
|
);
|
|
@@ -35,7 +48,19 @@ export const ConversationContent = ({
|
|
|
35
48
|
// can't resolve against a block-level parent whose own height is
|
|
36
49
|
// `min-height`-only (the #72 empty-state-pinned-to-the-top bug).
|
|
37
50
|
scrollClassName={cn("flex flex-col", scrollClassName)}
|
|
38
|
-
|
|
51
|
+
// Inside a `ChatShell variant="bare"` the composer floats over the
|
|
52
|
+
// transcript: the shell publishes `--chat-composer-inset` (its measured
|
|
53
|
+
// height) so the last turn scrolls clear of it, and `--chat-column` so
|
|
54
|
+
// turns share the composer's reading column. Outside such a shell both
|
|
55
|
+
// variables are unset: the inset falls back to 0 and `max-width` to none.
|
|
56
|
+
// `shrink-0`, never `min-h-0`: the content div must grow with its turns.
|
|
57
|
+
// Clamped to the viewport height, the turns overflow it and its bottom
|
|
58
|
+
// padding stays behind at the viewport's edge, so the last turn ends up
|
|
59
|
+
// under the floating composer instead of above it.
|
|
60
|
+
className={cn(
|
|
61
|
+
"mx-auto flex w-full max-w-(--chat-column) flex-1 shrink-0 flex-col gap-8 p-4 pb-[calc(var(--chat-composer-inset,0px)+--spacing(4))]",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
39
64
|
{...props}
|
|
40
65
|
/>
|
|
41
66
|
);
|
|
@@ -68,8 +93,8 @@ export const ConversationEmptyState = ({
|
|
|
68
93
|
<>
|
|
69
94
|
{icon && <div className="text-muted-foreground">{icon}</div>}
|
|
70
95
|
<div className="space-y-1">
|
|
71
|
-
<h3 className="font-medium text-
|
|
72
|
-
{description && <p className="text-muted-foreground text-
|
|
96
|
+
<h3 className="font-medium text-body">{title}</h3>
|
|
97
|
+
{description && <p className="text-muted-foreground text-body">{description}</p>}
|
|
73
98
|
</div>
|
|
74
99
|
{actions ? <div className="mt-1 flex items-center gap-2">{actions}</div> : null}
|
|
75
100
|
</>
|
|
@@ -84,6 +109,7 @@ export const ConversationScrollButton = ({
|
|
|
84
109
|
...props
|
|
85
110
|
}: ConversationScrollButtonProps) => {
|
|
86
111
|
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
|
112
|
+
const { t } = useLocale();
|
|
87
113
|
|
|
88
114
|
const handleScrollToBottom = useCallback(() => {
|
|
89
115
|
scrollToBottom();
|
|
@@ -93,9 +119,18 @@ export const ConversationScrollButton = ({
|
|
|
93
119
|
!isAtBottom && (
|
|
94
120
|
<Button
|
|
95
121
|
className={cn(
|
|
96
|
-
|
|
122
|
+
// Was `dark:bg-background dark:hover:bg-muted` — a hardcoded-dark
|
|
123
|
+
// branch invisible to any other registered theme (styling-and-tokens.md).
|
|
124
|
+
// `bg-background` already matches the `outline` variant's own default;
|
|
125
|
+
// the quieter `hover:bg-muted` (in place of `outline`'s default
|
|
126
|
+
// `hover:bg-accent`) now applies in every theme, not only the two
|
|
127
|
+
// shipped ones.
|
|
128
|
+
// `--chat-composer-inset`: lift the button clear of a floating
|
|
129
|
+
// `ChatShell variant="bare"` composer (0 anywhere else).
|
|
130
|
+
"absolute bottom-[calc(var(--chat-composer-inset,0px)+--spacing(4))] left-[50%] z-30 translate-x-[-50%] rounded-full bg-background hover:bg-muted",
|
|
97
131
|
className,
|
|
98
132
|
)}
|
|
133
|
+
aria-label={t("ai.turnStatus.scrollToBottom")}
|
|
99
134
|
onClick={handleScrollToBottom}
|
|
100
135
|
size="icon"
|
|
101
136
|
type="button"
|
|
@@ -138,6 +173,7 @@ export const ConversationDownload = ({
|
|
|
138
173
|
children,
|
|
139
174
|
...props
|
|
140
175
|
}: ConversationDownloadProps) => {
|
|
176
|
+
const { t } = useLocale();
|
|
141
177
|
const handleDownload = useCallback(() => {
|
|
142
178
|
const markdown = messagesToMarkdown(messages, formatMessage);
|
|
143
179
|
downloadBlob(new Blob([markdown], { type: "text/markdown" }), filename);
|
|
@@ -146,9 +182,12 @@ export const ConversationDownload = ({
|
|
|
146
182
|
return (
|
|
147
183
|
<Button
|
|
148
184
|
className={cn(
|
|
149
|
-
|
|
185
|
+
// See `ConversationScrollButton` — was a hardcoded-dark-only branch;
|
|
186
|
+
// `bg-background hover:bg-muted` now applies in every theme.
|
|
187
|
+
"absolute top-4 end-4 rounded-full bg-background hover:bg-muted",
|
|
150
188
|
className,
|
|
151
189
|
)}
|
|
190
|
+
aria-label={t("ai.conversation.download")}
|
|
152
191
|
onClick={handleDownload}
|
|
153
192
|
size="icon"
|
|
154
193
|
type="button"
|
|
@@ -194,3 +194,82 @@ export const ChangeReviewComposition: Story = {
|
|
|
194
194
|
</div>
|
|
195
195
|
),
|
|
196
196
|
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Focus indicator on expandable line (#313): Tab to the "show more" button
|
|
200
|
+
* and assert the compound indicator's OUTLINE layer (`outlineStyle: "solid"`,
|
|
201
|
+
* `--ring-contour`) that `focus-ring` adds — not just `boxShadow !== "none"`,
|
|
202
|
+
* which the legacy `ring-2` pattern also produces.
|
|
203
|
+
*
|
|
204
|
+
* The prior version passed a `hiddenLineCount` prop that `DiffViewProps`
|
|
205
|
+
* (`diff-view.tsx:77`) does not define — React spread it onto the DOM as a
|
|
206
|
+
* meaningless `hiddenlinecount` attribute and warned about it, and the "show
|
|
207
|
+
* more" row NEVER rendered (a real diff never mentions "50" anywhere).
|
|
208
|
+
* `getByRole("button", …)` then always threw before the focus check ran.
|
|
209
|
+
* `CollapsedRow` (`diff-view.tsx:289`) only appears when `collapseDiffRows`
|
|
210
|
+
* (`packages/ui/src/lib/diff-rows.ts`) actually collapses a context run
|
|
211
|
+
* longer than `contextLines` — so this story now supplies a real 10-line
|
|
212
|
+
* context run with `contextLines={2}`, which collapses to
|
|
213
|
+
* `top(1) + collapsed(8) + bottom(1)`, i.e. a real "Show 8 more lines"
|
|
214
|
+
* button. The button's accessible name is
|
|
215
|
+
* `t("ai.diffView.showMore", { count })`, not the literal "Show more" —
|
|
216
|
+
* matched by a count-tolerant regex so it doesn't drift with the wording.
|
|
217
|
+
*/
|
|
218
|
+
export const FocusIndicatorExpand: Story = {
|
|
219
|
+
name: "Keyboard focus — expand",
|
|
220
|
+
render: () => (
|
|
221
|
+
<div className="max-w-2xl">
|
|
222
|
+
<DiffView
|
|
223
|
+
lines={Array.from({ length: 10 }, (_, i) => ({
|
|
224
|
+
type: "context" as const,
|
|
225
|
+
oldNumber: i + 1,
|
|
226
|
+
newNumber: i + 1,
|
|
227
|
+
text: `line ${i + 1}`,
|
|
228
|
+
}))}
|
|
229
|
+
contextLines={2}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
),
|
|
233
|
+
play: async ({ canvas, userEvent }) => {
|
|
234
|
+
const button = canvas.getByRole("button", { name: /show \d+ more lines?/i });
|
|
235
|
+
await userEvent.tab();
|
|
236
|
+
await expect(button).toHaveFocus();
|
|
237
|
+
const focused = getComputedStyle(button);
|
|
238
|
+
await expect(focused.boxShadow).not.toBe("none");
|
|
239
|
+
await expect(focused.outlineStyle).toBe("solid");
|
|
240
|
+
await expect(parseFloat(focused.outlineWidth)).toBeGreaterThan(0);
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Focus indicator on scrollable pager (#313): Tab to the diff body and
|
|
246
|
+
* assert the compound indicator's OUTLINE layer on the actual scroll
|
|
247
|
+
* container. The prior version located it via `getByText(/^export/)`, which
|
|
248
|
+
* throws "multiple elements" under `variant="split"` (the same line renders
|
|
249
|
+
* once per pane) — and even resolved, `scrollDiv.parentElement` was never
|
|
250
|
+
* the scroll container itself (the text sits several nodes deep inside it).
|
|
251
|
+
* Query the tabbable scroll container directly — it carries `tabIndex={0}`
|
|
252
|
+
* and `focus-ring-inset` (`diff-view.tsx`'s `DiffViewPager`) — instead of
|
|
253
|
+
* inferring it from rendered content.
|
|
254
|
+
*/
|
|
255
|
+
export const FocusIndicatorPager: Story = {
|
|
256
|
+
name: "Keyboard focus — pager",
|
|
257
|
+
render: () => (
|
|
258
|
+
<div className="max-w-2xl">
|
|
259
|
+
<DiffView lines={SAMPLE_LINES} pager variant="split" />
|
|
260
|
+
</div>
|
|
261
|
+
),
|
|
262
|
+
play: async ({ canvas, userEvent }) => {
|
|
263
|
+
const pager = canvas.getByRole("region");
|
|
264
|
+
const scrollDiv = pager.querySelector<HTMLElement>('[tabindex="0"]');
|
|
265
|
+
if (scrollDiv == null) {
|
|
266
|
+
throw new Error("expected a tabbable scroll container inside the pager region");
|
|
267
|
+
}
|
|
268
|
+
await userEvent.tab();
|
|
269
|
+
await expect(scrollDiv).toHaveFocus();
|
|
270
|
+
const focused = getComputedStyle(scrollDiv);
|
|
271
|
+
await expect(focused.boxShadow).not.toBe("none");
|
|
272
|
+
await expect(focused.outlineStyle).toBe("solid");
|
|
273
|
+
await expect(parseFloat(focused.outlineWidth)).toBeGreaterThan(0);
|
|
274
|
+
},
|
|
275
|
+
};
|
package/src/diff-view.test.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { render, screen, within } from "@testing-library/react";
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
import { type DiffLine } from "@elabs-ai/components-ui";
|
|
5
5
|
import { DiffView } from "./diff-view";
|
|
@@ -162,6 +162,56 @@ describe("DiffView — pager (absorbs CodexDiff)", () => {
|
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
describe("DiffView — old/new sides tokenize SEPARATELY (perf review §2 — a deleted comment opener must not swallow the added side)", () => {
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
document.documentElement.removeAttribute("data-theme");
|
|
168
|
+
for (const token of [
|
|
169
|
+
"--code-foreground",
|
|
170
|
+
"--code-comment",
|
|
171
|
+
"--code-keyword",
|
|
172
|
+
"--code-number",
|
|
173
|
+
]) {
|
|
174
|
+
document.documentElement.style.removeProperty(token);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not let an unterminated block comment on the del side swallow the add side's tokens", async () => {
|
|
179
|
+
// Distinct resolved colors per scope (comment vs. keyword vs. number) so
|
|
180
|
+
// a merged/degenerate single-token line is distinguishable from a real,
|
|
181
|
+
// multi-token breakdown — see the module doc comment on `useDiffTokens`.
|
|
182
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
183
|
+
document.documentElement.style.setProperty("--code-foreground", "oklch(0.1 0 0)");
|
|
184
|
+
document.documentElement.style.setProperty("--code-comment", "oklch(0.5 0 0)");
|
|
185
|
+
document.documentElement.style.setProperty("--code-keyword", "oklch(0.3 0.2 260)");
|
|
186
|
+
document.documentElement.style.setProperty("--code-number", "oklch(0.4 0.2 140)");
|
|
187
|
+
|
|
188
|
+
const lines: DiffLine[] = [
|
|
189
|
+
{ type: "del", oldNumber: 1, text: "/* unterminated" },
|
|
190
|
+
{ type: "add", newNumber: 1, text: "const value = 1;" },
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const { container } = renderDiffView(<DiffView language="tsx" lines={lines} />);
|
|
194
|
+
|
|
195
|
+
// Combining old+new into ONE document (the bug) makes Shiki carry the
|
|
196
|
+
// del line's still-open `/*` across the line boundary, so the ENTIRE add
|
|
197
|
+
// line becomes one comment-colored token. Tokenized as its own document,
|
|
198
|
+
// `const value = 1;` is real code and splits into several distinctly
|
|
199
|
+
// colored spans.
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
const addLineText = container.querySelector(
|
|
202
|
+
'[data-diff-type="add"] [data-slot="diff-view-line-text"]',
|
|
203
|
+
);
|
|
204
|
+
const spans = addLineText?.querySelectorAll("span[style]") ?? [];
|
|
205
|
+
expect(spans.length).toBeGreaterThan(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const addLineText = container.querySelector(
|
|
209
|
+
'[data-diff-type="add"] [data-slot="diff-view-line-text"]',
|
|
210
|
+
);
|
|
211
|
+
expect(addLineText?.textContent).toContain("const value = 1;");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
165
215
|
describe("DiffView — contextLines collapsing", () => {
|
|
166
216
|
const longContext: DiffLine[] = Array.from({ length: 10 }, (_, i) => ({
|
|
167
217
|
type: "context" as const,
|