@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/asset-preview.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
TableHead,
|
|
31
31
|
TableHeader,
|
|
32
32
|
TableRow,
|
|
33
|
+
useLocale,
|
|
33
34
|
} from "@elabs-ai/components-ui";
|
|
34
35
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
35
36
|
import type { HTMLAttributes, ReactNode } from "react";
|
|
@@ -72,18 +73,87 @@ interface ParsedCsv {
|
|
|
72
73
|
rows: string[][];
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* A small RFC 4180-ish CSV tokenizer: handles quoted fields (commas and
|
|
78
|
+
* newlines inside quotes don't end the field/row) and the `""` escaped-quote
|
|
79
|
+
* convention. Single pass over the raw string — no line pre-split, so a
|
|
80
|
+
* quoted field spanning multiple lines survives intact. Still preview-grade
|
|
81
|
+
* (unquoted cells are trimmed for the glance table), not a full CSV grammar.
|
|
82
|
+
*/
|
|
83
|
+
function parseCsvRows(content: string): string[][] {
|
|
84
|
+
const rows: string[][] = [];
|
|
85
|
+
let row: string[] = [];
|
|
86
|
+
let field = "";
|
|
87
|
+
let wasQuoted = false;
|
|
88
|
+
let inQuotes = false;
|
|
89
|
+
|
|
90
|
+
const endField = () => {
|
|
91
|
+
row.push(wasQuoted ? field : field.trim());
|
|
92
|
+
field = "";
|
|
93
|
+
wasQuoted = false;
|
|
94
|
+
};
|
|
95
|
+
const endRow = () => {
|
|
96
|
+
endField();
|
|
97
|
+
rows.push(row);
|
|
98
|
+
row = [];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < content.length; i++) {
|
|
102
|
+
const ch = content[i];
|
|
103
|
+
|
|
104
|
+
if (inQuotes) {
|
|
105
|
+
if (ch === '"') {
|
|
106
|
+
if (content[i + 1] === '"') {
|
|
107
|
+
field += '"';
|
|
108
|
+
i += 1;
|
|
109
|
+
} else {
|
|
110
|
+
inQuotes = false;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
field += ch;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ch === '"' && field === "") {
|
|
119
|
+
inQuotes = true;
|
|
120
|
+
wasQuoted = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (ch === ",") {
|
|
125
|
+
endField();
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (ch === "\r") {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ch === "\n") {
|
|
134
|
+
endRow();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
field += ch;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A trailing field/row with no terminating newline.
|
|
142
|
+
if (field !== "" || row.length > 0) {
|
|
143
|
+
endRow();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return rows;
|
|
147
|
+
}
|
|
148
|
+
|
|
76
149
|
function parseCsv(content: string): ParsedCsv {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.filter((line) => line.length > 0);
|
|
81
|
-
const [head = "", ...rest] = lines;
|
|
82
|
-
const split = (line: string) => line.split(",").map((cell) => cell.trim());
|
|
83
|
-
return { header: split(head), rows: rest.map(split) };
|
|
150
|
+
const rows = parseCsvRows(content.trim()).filter((r) => !(r.length === 1 && r[0] === ""));
|
|
151
|
+
const [header = [], ...rest] = rows;
|
|
152
|
+
return { header, rows: rest };
|
|
84
153
|
}
|
|
85
154
|
|
|
86
155
|
const CsvPreview = ({ content }: { content: string }) => {
|
|
156
|
+
const { t } = useLocale();
|
|
87
157
|
const { header, rows } = useMemo(() => parseCsv(content), [content]);
|
|
88
158
|
return (
|
|
89
159
|
<div className="flex flex-col gap-2">
|
|
@@ -109,7 +179,7 @@ const CsvPreview = ({ content }: { content: string }) => {
|
|
|
109
179
|
</TableBody>
|
|
110
180
|
</Table>
|
|
111
181
|
<p className="text-meta text-muted-foreground">
|
|
112
|
-
{
|
|
182
|
+
{t("ai.assetPreview.rowCount", { count: rows.length })}
|
|
113
183
|
</p>
|
|
114
184
|
</div>
|
|
115
185
|
);
|
|
@@ -135,6 +205,7 @@ export const AssetPreview = ({
|
|
|
135
205
|
className,
|
|
136
206
|
...props
|
|
137
207
|
}: AssetPreviewProps) => {
|
|
208
|
+
const { t } = useLocale();
|
|
138
209
|
const [mode, setMode] = useState<AssetPreviewMode>(defaultMode);
|
|
139
210
|
const injectedRenderer = useAssetPreviewRenderer();
|
|
140
211
|
const content = asset.content ?? "";
|
|
@@ -150,7 +221,7 @@ export const AssetPreview = ({
|
|
|
150
221
|
if (injected !== null) {
|
|
151
222
|
body = injected;
|
|
152
223
|
} else if (asset.type !== "image" && content.length === 0) {
|
|
153
|
-
body = <p className="text-body text-muted-foreground">
|
|
224
|
+
body = <p className="text-body text-muted-foreground">{t("ai.assetPreview.noPreview")}</p>;
|
|
154
225
|
} else if (showRaw) {
|
|
155
226
|
body = <CodeBlock code={content} language={assetLanguage(asset)} wrap />;
|
|
156
227
|
} else {
|
|
@@ -197,7 +268,7 @@ export const AssetPreview = ({
|
|
|
197
268
|
className={cn(!showRaw && "bg-accent text-accent-foreground")}
|
|
198
269
|
onClick={() => setMode("preview")}
|
|
199
270
|
>
|
|
200
|
-
|
|
271
|
+
{t("ai.assetPreview.preview")}
|
|
201
272
|
</Button>
|
|
202
273
|
<Button
|
|
203
274
|
type="button"
|
|
@@ -207,7 +278,7 @@ export const AssetPreview = ({
|
|
|
207
278
|
className={cn(showRaw && "bg-accent text-accent-foreground")}
|
|
208
279
|
onClick={() => setMode("raw")}
|
|
209
280
|
>
|
|
210
|
-
|
|
281
|
+
{t("ai.assetPreview.raw")}
|
|
211
282
|
</Button>
|
|
212
283
|
</ArtifactActions>
|
|
213
284
|
) : null}
|
package/src/attachments.tsx
CHANGED
|
@@ -192,8 +192,8 @@ export const Attachment = ({ data, onRemove, className, children, ...props }: At
|
|
|
192
192
|
variant === "inline" && [
|
|
193
193
|
"flex h-8 cursor-pointer select-none items-center gap-1.5",
|
|
194
194
|
"rounded-md border border-border px-1.5",
|
|
195
|
-
"font-medium text-
|
|
196
|
-
"hover:bg-accent hover:text-accent-foreground
|
|
195
|
+
"font-medium text-body transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
196
|
+
"hover:bg-accent/50 hover:text-accent-foreground",
|
|
197
197
|
],
|
|
198
198
|
variant === "list" && [
|
|
199
199
|
"flex w-full items-center gap-3 rounded-lg border p-3",
|
|
@@ -283,7 +283,7 @@ export const AttachmentInfo = ({
|
|
|
283
283
|
<div className={cn("min-w-0 flex-1", className)} {...props}>
|
|
284
284
|
<span className="block truncate">{label}</span>
|
|
285
285
|
{showMediaType && data.mediaType && (
|
|
286
|
-
<span className="block truncate text-muted-foreground text-
|
|
286
|
+
<span className="block truncate text-muted-foreground text-meta">{data.mediaType}</span>
|
|
287
287
|
)}
|
|
288
288
|
</div>
|
|
289
289
|
);
|
|
@@ -385,7 +385,10 @@ export type AttachmentEmptyProps = HTMLAttributes<HTMLDivElement>;
|
|
|
385
385
|
|
|
386
386
|
export const AttachmentEmpty = ({ className, children, ...props }: AttachmentEmptyProps) => (
|
|
387
387
|
<div
|
|
388
|
-
className={cn(
|
|
388
|
+
className={cn(
|
|
389
|
+
"flex items-center justify-center p-4 text-muted-foreground text-body",
|
|
390
|
+
className,
|
|
391
|
+
)}
|
|
389
392
|
{...props}
|
|
390
393
|
>
|
|
391
394
|
{children ?? "No attachments"}
|
package/src/chat-shell.test.tsx
CHANGED
|
@@ -58,8 +58,8 @@ describe("ChatShell — slots", () => {
|
|
|
58
58
|
|
|
59
59
|
it("omits the header row entirely when no header is supplied", () => {
|
|
60
60
|
const { container } = render(<ChatShell>t</ChatShell>);
|
|
61
|
-
// An empty header would still occupy its fixed h-
|
|
62
|
-
expect(container.querySelector(".h-
|
|
61
|
+
// An empty header would still occupy its fixed h-header row.
|
|
62
|
+
expect(container.querySelector(".h-header")).toBeNull();
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
it("omits the aside rail entirely when no aside is supplied", () => {
|
|
@@ -91,16 +91,46 @@ describe("ChatShell — variants", () => {
|
|
|
91
91
|
expect(container.querySelector(".border-t")).toBeNull();
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it("makes the
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
it("makes the edge scrims inert (aria-hidden AND pointer-events-none) in both variants", () => {
|
|
95
|
+
for (const variant of ["card", "bare"] as const) {
|
|
96
|
+
const { container, unmount } = render(<ChatShell variant={variant}>t</ChatShell>);
|
|
97
|
+
const scrims = container.querySelectorAll("[aria-hidden='true']");
|
|
98
|
+
expect(scrims).toHaveLength(2);
|
|
99
|
+
for (const scrim of scrims) expect(scrim).toHaveClass("pointer-events-none");
|
|
100
|
+
unmount();
|
|
101
|
+
}
|
|
99
102
|
});
|
|
103
|
+
});
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
describe("ChatShell — floating composer", () => {
|
|
106
|
+
it("floats the composer over the transcript instead of stacking a footer bar", () => {
|
|
107
|
+
const { container } = render(
|
|
108
|
+
<ChatShell composer={<textarea aria-label="Message" />}>
|
|
109
|
+
<p>transcript</p>
|
|
110
|
+
</ChatShell>,
|
|
111
|
+
);
|
|
112
|
+
const layer = container.querySelector("[data-slot='chat-shell-composer']");
|
|
113
|
+
expect(layer).toHaveClass("absolute", "bottom-0", "pb-4");
|
|
114
|
+
// The layer passes pointer events through to the transcript; only the
|
|
115
|
+
// composer column takes input.
|
|
116
|
+
expect(layer).toHaveClass("pointer-events-none");
|
|
117
|
+
expect(screen.getByRole("textbox", { name: "Message" }).parentElement).toHaveClass(
|
|
118
|
+
"pointer-events-auto",
|
|
119
|
+
"max-w-(--chat-composer-column)",
|
|
120
|
+
);
|
|
121
|
+
expect(container.querySelector(".border-t")).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("publishes the composer height so the transcript can scroll clear of it", () => {
|
|
125
|
+
const { container, rerender } = render(
|
|
126
|
+
<ChatShell composer={<textarea aria-label="Message" />}>t</ChatShell>,
|
|
127
|
+
);
|
|
128
|
+
const root = container.firstChild as HTMLElement;
|
|
129
|
+
// jsdom has no layout, so the measured height is 0 — the contract is that
|
|
130
|
+
// the variable is written at all.
|
|
131
|
+
expect(root.style.getPropertyValue("--chat-composer-inset")).toBe("0px");
|
|
132
|
+
rerender(<ChatShell>t</ChatShell>);
|
|
133
|
+
expect(root.style.getPropertyValue("--chat-composer-inset")).toBe("");
|
|
104
134
|
});
|
|
105
135
|
});
|
|
106
136
|
|
package/src/chat-shell.tsx
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
+
import { useLayoutEffect, useRef } from "react";
|
|
4
5
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
5
6
|
|
|
6
7
|
export interface ChatShellProps {
|
|
7
8
|
/** Scrollable transcript — typically a <Conversation>. */
|
|
8
9
|
children: ReactNode;
|
|
9
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Floating composer — typically a <Composer> (the standard chat input). It
|
|
12
|
+
* floats, padded, over the bottom of the transcript, centred at
|
|
13
|
+
* `--chat-composer-column` (default `--container-3xl`) under a transcript
|
|
14
|
+
* column of `--chat-column` (default `--container-4xl`, read by
|
|
15
|
+
* `ConversationContent`); turns scroll up behind it and fade out.
|
|
16
|
+
*/
|
|
10
17
|
composer?: ReactNode;
|
|
11
18
|
/** Optional header row (title, model picker, actions). */
|
|
12
19
|
header?: ReactNode;
|
|
13
20
|
/** Optional right-side context rail (sources, files, memory). */
|
|
14
21
|
aside?: ReactNode;
|
|
15
22
|
/**
|
|
16
|
-
*
|
|
17
|
-
* - `"card"` (default) — a self-contained rounded, bordered chat card
|
|
18
|
-
* when the shell sits on a page among other
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* of a hard bordered bar. Use when the shell already lives inside a bounded
|
|
23
|
+
* Frame treatment (the transcript/composer layout is the same in both):
|
|
24
|
+
* - `"card"` (default) — a self-contained rounded, bordered chat card with a
|
|
25
|
+
* divider under the header. Use when the shell sits on a page among other
|
|
26
|
+
* content and needs its own frame.
|
|
27
|
+
* - `"bare"` — an immersive pane that fills its container with NO frame and
|
|
28
|
+
* no header divider. Use when the shell already lives inside a bounded
|
|
23
29
|
* region (an AppShell `SidebarInset`, a full-page workspace) so it doesn't
|
|
24
30
|
* draw a redundant second frame.
|
|
25
31
|
*/
|
|
@@ -28,14 +34,15 @@ export interface ChatShellProps {
|
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
|
-
* Layout shell for an AI chat built from the AI Elements: a header, a
|
|
32
|
-
* transcript (give it a <Conversation>, which is
|
|
33
|
-
*
|
|
37
|
+
* Layout shell for an AI chat built from the AI Elements: a header, a
|
|
38
|
+
* full-height scrollable transcript (give it a <Conversation>, which is
|
|
39
|
+
* `flex-1`), a composer floating over the transcript's bottom edge (a
|
|
40
|
+
* <Composer>, the standard chat input), and an optional context rail. Fills
|
|
34
41
|
* its parent's height.
|
|
35
42
|
*
|
|
36
|
-
* Two
|
|
37
|
-
*
|
|
38
|
-
*
|
|
43
|
+
* Two frames (`variant`): a framed `"card"` for in-page use, and a frameless
|
|
44
|
+
* `"bare"` immersive pane for when the shell already sits inside a bounded
|
|
45
|
+
* region and a second frame would just read as boxy.
|
|
39
46
|
*/
|
|
40
47
|
export function ChatShell({
|
|
41
48
|
children,
|
|
@@ -46,50 +53,89 @@ export function ChatShell({
|
|
|
46
53
|
className,
|
|
47
54
|
}: ChatShellProps) {
|
|
48
55
|
const bare = variant === "bare";
|
|
56
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
const composerRef = useRef<HTMLDivElement>(null);
|
|
58
|
+
const hasComposer = composer != null;
|
|
59
|
+
|
|
60
|
+
// The composer floats OVER the transcript, so the transcript has to
|
|
61
|
+
// know how tall it is — `ConversationContent` pads its bottom by
|
|
62
|
+
// `--chat-composer-inset` (so the last turn scrolls clear of the composer)
|
|
63
|
+
// and `ConversationScrollButton` sits just above it. Written straight onto
|
|
64
|
+
// the root's style: no React state, no re-render per resize.
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
const root = rootRef.current;
|
|
67
|
+
const node = composerRef.current;
|
|
68
|
+
if (!root) return;
|
|
69
|
+
if (!node) {
|
|
70
|
+
root.style.removeProperty("--chat-composer-inset");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const sync = () =>
|
|
74
|
+
root.style.setProperty("--chat-composer-inset", `${node.getBoundingClientRect().height}px`);
|
|
75
|
+
sync();
|
|
76
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
77
|
+
const observer = new ResizeObserver(sync);
|
|
78
|
+
observer.observe(node);
|
|
79
|
+
return () => observer.disconnect();
|
|
80
|
+
}, [hasComposer]);
|
|
81
|
+
|
|
49
82
|
return (
|
|
50
83
|
<div
|
|
84
|
+
ref={rootRef}
|
|
85
|
+
data-slot="chat-shell"
|
|
86
|
+
data-variant={variant}
|
|
51
87
|
className={cn(
|
|
52
|
-
|
|
88
|
+
// Reading columns: the transcript (`ConversationContent`) reads a
|
|
89
|
+
// little wider than the composer beneath it. Override per app, e.g.
|
|
90
|
+
// `className="[--chat-column:var(--container-5xl)]"`.
|
|
91
|
+
"flex h-full min-h-0 w-full overflow-hidden bg-background [--chat-column:var(--container-4xl)] [--chat-composer-column:var(--container-3xl)]",
|
|
53
92
|
bare ? null : "rounded-xl border",
|
|
54
93
|
className,
|
|
55
94
|
)}
|
|
56
95
|
>
|
|
57
96
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
58
97
|
{header ? (
|
|
59
|
-
<div className={cn("flex h-
|
|
98
|
+
<div className={cn("flex h-header shrink-0 items-center px-4", bare ? null : "border-b")}>
|
|
60
99
|
{header}
|
|
61
100
|
</div>
|
|
62
101
|
) : null}
|
|
63
102
|
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
{/* The transcript runs the FULL height of the pane and the composer
|
|
104
|
+
floats over its bottom edge, so turns scroll up behind the composer
|
|
105
|
+
and dissolve under the top edge instead of stopping at a hard bar.
|
|
106
|
+
The scrims are token-driven (`from-background`) so they fade to the
|
|
107
|
+
shell's own ground in every theme, and inert (`pointer-events-none`
|
|
108
|
+
+ `aria-hidden`) so they never block scroll, steal a tap, or get
|
|
109
|
+
announced. */}
|
|
110
|
+
<div className="relative flex min-h-0 flex-1 flex-col">
|
|
111
|
+
{children}
|
|
112
|
+
<div
|
|
113
|
+
aria-hidden
|
|
114
|
+
className="pointer-events-none absolute inset-x-0 top-0 z-10 h-8 bg-gradient-to-b from-background to-transparent"
|
|
115
|
+
/>
|
|
116
|
+
<div
|
|
117
|
+
aria-hidden
|
|
118
|
+
data-slot="chat-shell-scrim"
|
|
119
|
+
// Solid under the composer, fading out a little above it.
|
|
120
|
+
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-background from-50% to-transparent"
|
|
121
|
+
style={{ height: "calc(var(--chat-composer-inset, 0px) + 3rem)" }}
|
|
122
|
+
/>
|
|
123
|
+
{composer ? (
|
|
124
|
+
// The float layer passes pointer events through, so the
|
|
125
|
+
// transcript's scrollbar and the gutters beside the composer stay
|
|
126
|
+
// live; only the composer itself takes input. DOM order still puts
|
|
127
|
+
// it after the transcript (tab order follows reading order).
|
|
77
128
|
<div
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// divider) above the fade; card: the classic bordered footer.
|
|
89
|
-
<div className={cn("shrink-0", bare ? "relative z-20 px-4 pb-4" : "border-t p-3")}>
|
|
90
|
-
{composer}
|
|
91
|
-
</div>
|
|
92
|
-
) : null}
|
|
129
|
+
ref={composerRef}
|
|
130
|
+
data-slot="chat-shell-composer"
|
|
131
|
+
className="pointer-events-none absolute inset-x-0 bottom-0 z-20 px-4 pb-4"
|
|
132
|
+
>
|
|
133
|
+
<div className="pointer-events-auto mx-auto w-full max-w-(--chat-composer-column)">
|
|
134
|
+
{composer}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
) : null}
|
|
138
|
+
</div>
|
|
93
139
|
</div>
|
|
94
140
|
{aside ? <div className="hidden w-80 shrink-0 border-s lg:block">{aside}</div> : null}
|
|
95
141
|
</div>
|
package/src/chat.stories.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { ChatShell } from "./chat-shell";
|
|
3
4
|
import { Conversation, ConversationContent, ConversationScrollButton } from "./conversation";
|
|
4
5
|
import { Message, MessageContent, MessageResponse } from "./message";
|
|
5
6
|
import { Composer } from "./composer";
|
|
@@ -45,45 +46,49 @@ function ChatExample() {
|
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
return (
|
|
48
|
-
<div className="mx-auto
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
49
|
+
<div className="mx-auto h-[640px] max-w-4xl overflow-hidden rounded-xl border bg-background">
|
|
50
|
+
<ChatShell
|
|
51
|
+
variant="bare"
|
|
52
|
+
composer={
|
|
53
|
+
<Composer
|
|
54
|
+
placeholder="Ask about deploys…"
|
|
55
|
+
status={status === "submitted" ? "Generating…" : "Awaiting your input"}
|
|
56
|
+
sendStatus={status}
|
|
57
|
+
onSubmit={(message) => {
|
|
58
|
+
const text = message.text?.trim();
|
|
59
|
+
if (text) send(text);
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<Conversation className="flex-1">
|
|
65
|
+
<ConversationContent>
|
|
66
|
+
{messages.map((m) => (
|
|
67
|
+
<Message from={m.role} key={m.id}>
|
|
68
|
+
<MessageContent>
|
|
69
|
+
{m.reasoning ? (
|
|
70
|
+
<Reasoning>
|
|
71
|
+
<ReasoningTrigger />
|
|
72
|
+
<ReasoningContent>{m.reasoning}</ReasoningContent>
|
|
73
|
+
</Reasoning>
|
|
74
|
+
) : null}
|
|
75
|
+
<MessageResponse>{m.text}</MessageResponse>
|
|
76
|
+
{m.role === "assistant" ? (
|
|
77
|
+
<Sources>
|
|
78
|
+
<SourcesTrigger count={2} />
|
|
79
|
+
<SourcesContent>
|
|
80
|
+
<Source href="https://example.com" title="Deploy log — wk 23" />
|
|
81
|
+
<Source href="https://example.com" title="Billing runbook" />
|
|
82
|
+
</SourcesContent>
|
|
83
|
+
</Sources>
|
|
84
|
+
) : null}
|
|
85
|
+
</MessageContent>
|
|
86
|
+
</Message>
|
|
87
|
+
))}
|
|
88
|
+
</ConversationContent>
|
|
89
|
+
<ConversationScrollButton />
|
|
90
|
+
</Conversation>
|
|
91
|
+
</ChatShell>
|
|
87
92
|
</div>
|
|
88
93
|
);
|
|
89
94
|
}
|
|
@@ -100,9 +105,9 @@ const meta = {
|
|
|
100
105
|
"composed from the @elabs-ai/components-ai grammar: a scrolling " +
|
|
101
106
|
"[Conversation](?path=/story/ai-conversation--default) of " +
|
|
102
107
|
"[Messages](?path=/story/ai-message--presets) with reasoning and sources, plus a " +
|
|
103
|
-
"[Composer](?path=/story/ai-composer--default)
|
|
104
|
-
"[ChatShell](?path=/story/ai-chatshell--default)
|
|
105
|
-
"
|
|
108
|
+
"[Composer](?path=/story/ai-composer--default), laid out by " +
|
|
109
|
+
"[ChatShell](?path=/story/ai-chatshell--default): the transcript scrolls behind the " +
|
|
110
|
+
"floating composer and both sit in centred reading columns.",
|
|
106
111
|
},
|
|
107
112
|
},
|
|
108
113
|
},
|
package/src/code-block.test.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import type { CSSProperties } from "react";
|
|
5
5
|
import { afterEach, describe, expect, it } from "vitest";
|
|
6
6
|
import { render, waitFor } from "@testing-library/react";
|
|
7
|
-
import { CodeBlock } from "./code-block";
|
|
7
|
+
import { CodeBlock, CodeBlockCopyButton, highlightCode } from "./code-block";
|
|
8
8
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
|
|
@@ -189,3 +189,102 @@ describe("CodeBlock isStreaming (#269, loading-states.md)", () => {
|
|
|
189
189
|
expect(container.querySelector('[role="status"]')).toBeNull();
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
describe("CodeBlockCopyButton accessible name (a11y review)", () => {
|
|
194
|
+
it("has an aria-label so the icon-only button has an accessible name", () => {
|
|
195
|
+
const { container } = render(
|
|
196
|
+
<CodeBlock code="const a = 1;" language="tsx">
|
|
197
|
+
<CodeBlockCopyButton />
|
|
198
|
+
</CodeBlock>,
|
|
199
|
+
);
|
|
200
|
+
expect(container.querySelector("button")).toHaveAccessibleName("Copy");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("lets a consumer override the label", () => {
|
|
204
|
+
const { container } = render(
|
|
205
|
+
<CodeBlock code="const a = 1;" language="tsx">
|
|
206
|
+
<CodeBlockCopyButton aria-label="Copy snippet" />
|
|
207
|
+
</CodeBlock>,
|
|
208
|
+
);
|
|
209
|
+
expect(container.querySelector("button")).toHaveAccessibleName("Copy snippet");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ─── `highlightCode` awaited helper for the perf-fix regression locks below ──
|
|
214
|
+
|
|
215
|
+
function highlightAsync(
|
|
216
|
+
code: string,
|
|
217
|
+
language: Parameters<typeof highlightCode>[1],
|
|
218
|
+
el?: Element | null,
|
|
219
|
+
skipCache = false,
|
|
220
|
+
): Promise<NonNullable<ReturnType<typeof highlightCode>>> {
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
const cached = highlightCode(code, language, (result) => resolve(result), el, skipCache);
|
|
223
|
+
if (cached) resolve(cached);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const flattenTokenContent = (result: NonNullable<ReturnType<typeof highlightCode>>): string =>
|
|
228
|
+
result.tokens.map((line) => line.map((token) => token.content).join("")).join("\n");
|
|
229
|
+
|
|
230
|
+
describe("highlightCode cache key — no hash collision (perf review 1.4b/1.4c)", () => {
|
|
231
|
+
it("does not collide two >200-char strings sharing a length, prefix and suffix but differing in the middle", async () => {
|
|
232
|
+
const prefix = "a".repeat(150);
|
|
233
|
+
const suffix = "b".repeat(150);
|
|
234
|
+
// Same length, same first/last 100 chars — the OLD `length:first100:last100`
|
|
235
|
+
// cache key collided on exactly this shape (a mid-string edit), showing
|
|
236
|
+
// whichever of the two resolved second on top of BOTH code blocks.
|
|
237
|
+
const codeA = `${prefix}1${suffix}`;
|
|
238
|
+
const codeB = `${prefix}2${suffix}`;
|
|
239
|
+
|
|
240
|
+
await highlightAsync(codeA, "tsx");
|
|
241
|
+
await highlightAsync(codeB, "tsx");
|
|
242
|
+
|
|
243
|
+
// Re-requesting codeA must still return a SYNCHRONOUS cache hit for its
|
|
244
|
+
// OWN content — never codeB's.
|
|
245
|
+
const againA = highlightCode(codeA, "tsx");
|
|
246
|
+
expect(againA).not.toBeNull();
|
|
247
|
+
expect(flattenTokenContent(againA!)).toBe(codeA);
|
|
248
|
+
expect(flattenTokenContent(againA!)).not.toBe(codeB);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("highlightCode tokensCache is bounded (perf review 1.4a — LRU ~200 entries)", () => {
|
|
253
|
+
it("evicts the least-recently-used entry once the cache exceeds its cap", async () => {
|
|
254
|
+
const codes = Array.from({ length: 205 }, (_, i) => `const lruEvictionProbe_${i} = ${i};`);
|
|
255
|
+
|
|
256
|
+
for (const code of codes) {
|
|
257
|
+
// Sequential on purpose: each must actually populate the cache (and
|
|
258
|
+
// become the most-recently-used entry) before the next one runs.
|
|
259
|
+
|
|
260
|
+
await highlightAsync(code, "tsx");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The very first entry inserted is the least-recently-used once 205 > the
|
|
264
|
+
// 200-entry cap — it must have been evicted, so re-requesting it is a
|
|
265
|
+
// cache MISS (highlightCode returns null synchronously and kicks off a
|
|
266
|
+
// fresh async highlight instead).
|
|
267
|
+
expect(highlightCode(codes[0] as string, "tsx")).toBeNull();
|
|
268
|
+
|
|
269
|
+
// The most recently inserted entry is still a synchronous cache hit.
|
|
270
|
+
expect(highlightCode(codes[204] as string, "tsx")).not.toBeNull();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("highlightCode skipCache (perf review 1.4a — never permanently cache mid-stream)", () => {
|
|
275
|
+
it("does not persist a highlight into the shared cache when skipCache is set", async () => {
|
|
276
|
+
const code = "const streamingSkipCacheProbe = 'a';";
|
|
277
|
+
await highlightAsync(code, "tsx", undefined, true);
|
|
278
|
+
|
|
279
|
+
// A later, non-streaming request for the EXACT same code is a cache
|
|
280
|
+
// MISS — the streaming pass never wrote it in.
|
|
281
|
+
expect(highlightCode(code, "tsx")).toBeNull();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("still persists an ordinary (non-streaming) highlight", async () => {
|
|
285
|
+
const code = "const nonStreamingCacheProbe = 'b';";
|
|
286
|
+
await highlightAsync(code, "tsx");
|
|
287
|
+
|
|
288
|
+
expect(highlightCode(code, "tsx")).not.toBeNull();
|
|
289
|
+
});
|
|
290
|
+
});
|