@signalflare-ai/ui 1.0.0 → 1.2.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/CHANGELOG.md +37 -4
- package/README.md +1 -1
- package/ai/component-registry.json +1047 -183
- package/ai/component-registry.md +4241 -50
- package/ai/schemas.ts +99 -502
- package/dist/.build-complete +1 -1
- package/dist/ai/schemas.d.ts +76 -58
- package/dist/ai/schemas.d.ts.map +1 -1
- package/dist/{ai-actions-DSVeQn4e.js → ai-actions-BdUZI3Gk.js} +3 -3
- package/dist/{ai-actions-DSVeQn4e.js.map → ai-actions-BdUZI3Gk.js.map} +1 -1
- package/dist/{ai-agent-card-BXHwhWAU.js → ai-agent-card-BR2NIYhi.js} +1 -1
- package/dist/{ai-agent-card-BXHwhWAU.js.map → ai-agent-card-BR2NIYhi.js.map} +1 -1
- package/dist/{ai-approval-aa0qvjFN.js → ai-approval-Ba7mrKba.js} +2 -2
- package/dist/{ai-approval-aa0qvjFN.js.map → ai-approval-Ba7mrKba.js.map} +1 -1
- package/dist/{ai-code-block-BgtIxtZZ.js → ai-code-block-CZtoL73R.js} +3 -3
- package/dist/{ai-code-block-BgtIxtZZ.js.map → ai-code-block-CZtoL73R.js.map} +1 -1
- package/dist/ai-conversation-Cc7WlaBg.js +242 -0
- package/dist/ai-conversation-Cc7WlaBg.js.map +1 -0
- package/dist/{ai-info-banner-uFxHHwBA.js → ai-info-banner-C7EWPBj7.js} +7 -3
- package/dist/ai-info-banner-C7EWPBj7.js.map +1 -0
- package/dist/{ai-message-BjnFznXy.js → ai-message-Bp7L68U_.js} +27 -8
- package/dist/ai-message-Bp7L68U_.js.map +1 -0
- package/dist/{ai-mission-header-08__gULL.js → ai-mission-header-TiCJfTNt.js} +1 -1
- package/dist/{ai-mission-header-08__gULL.js.map → ai-mission-header-TiCJfTNt.js.map} +1 -1
- package/dist/{ai-part-group-DBtgTgAn.js → ai-part-group-DNb9I446.js} +3 -3
- package/dist/{ai-part-group-DBtgTgAn.js.map → ai-part-group-DNb9I446.js.map} +1 -1
- package/dist/{ai-prompt-input-Dy1LfxPk.js → ai-prompt-input-BVvov_KF.js} +467 -25
- package/dist/ai-prompt-input-BVvov_KF.js.map +1 -0
- package/dist/{ai-question-CHHoDJMg.js → ai-question-GPPMk7YM.js} +2 -2
- package/dist/{ai-question-CHHoDJMg.js.map → ai-question-GPPMk7YM.js.map} +1 -1
- package/dist/{ai-reasoning-CnL6ZSr5.js → ai-reasoning-_feFjk56.js} +2 -2
- package/dist/{ai-reasoning-CnL6ZSr5.js.map → ai-reasoning-_feFjk56.js.map} +1 -1
- package/dist/{ai-response-BEUg3xvd.js → ai-response-CvjV3WhV.js} +8 -3
- package/dist/ai-response-CvjV3WhV.js.map +1 -0
- package/dist/{ai-shimmer-By5_L05p.js → ai-shimmer-j6lKIrjj.js} +1 -1
- package/dist/{ai-shimmer-By5_L05p.js.map → ai-shimmer-j6lKIrjj.js.map} +1 -1
- package/dist/{ai-status-badge-BGYGWYF6.js → ai-status-badge-CSU_QOdz.js} +1 -1
- package/dist/{ai-status-badge-BGYGWYF6.js.map → ai-status-badge-CSU_QOdz.js.map} +1 -1
- package/dist/{ai-streaming-text-CMfoThV0.js → ai-streaming-text-IWW1BhvZ.js} +44 -16
- package/dist/ai-streaming-text-IWW1BhvZ.js.map +1 -0
- package/dist/{ai-subagent-DcPRqkAA.js → ai-subagent-JA4iIMW3.js} +13 -5
- package/dist/ai-subagent-JA4iIMW3.js.map +1 -0
- package/dist/{ai-suggestion-MgeCg5Ar.js → ai-suggestion-BdO6MBuH.js} +2 -2
- package/dist/{ai-suggestion-MgeCg5Ar.js.map → ai-suggestion-BdO6MBuH.js.map} +1 -1
- package/dist/{ai-task-list-Da9zIm00.js → ai-task-list-DYw4R1FA.js} +12 -5
- package/dist/ai-task-list-DYw4R1FA.js.map +1 -0
- package/dist/{ai-timeline-Cwu045IR.js → ai-timeline-C42tOUT8.js} +1 -1
- package/dist/{ai-timeline-Cwu045IR.js.map → ai-timeline-C42tOUT8.js.map} +1 -1
- package/dist/{ai-tool-Cn1O4xjP.js → ai-tool-03jOTwUI.js} +23 -10
- package/dist/ai-tool-03jOTwUI.js.map +1 -0
- package/dist/{ai-usage-bar-DjS12DMp.js → ai-usage-bar-BRf5LC_b.js} +1 -1
- package/dist/{ai-usage-bar-DjS12DMp.js.map → ai-usage-bar-BRf5LC_b.js.map} +1 -1
- package/dist/{badge-D_eaA6wv.js → badge-BheXjMc8.js} +2 -2
- package/dist/{badge-D_eaA6wv.js.map → badge-BheXjMc8.js.map} +1 -1
- package/dist/{banner-B_6oBrsu.js → banner-CcsjunJg.js} +7 -2
- package/dist/banner-CcsjunJg.js.map +1 -0
- package/dist/{breadcrumbs-BlmeYfgq.js → breadcrumbs-CouSyy3H.js} +3 -3
- package/dist/{breadcrumbs-BlmeYfgq.js.map → breadcrumbs-CouSyy3H.js.map} +1 -1
- package/dist/{button-De0267YU.js → button-CO6-qPax.js} +1 -1
- package/dist/{button-De0267YU.js.map → button-CO6-qPax.js.map} +1 -1
- package/dist/catalog.js +1 -1
- package/dist/{chart-BK3sVPnD.js → chart-Dg0qUeSc.js} +2 -2
- package/dist/{chart-BK3sVPnD.js.map → chart-Dg0qUeSc.js.map} +1 -1
- package/dist/{checkbox-DYhUmZNw.js → checkbox-D7p4QKsC.js} +2 -2
- package/dist/{checkbox-DYhUmZNw.js.map → checkbox-D7p4QKsC.js.map} +1 -1
- package/dist/{clipboard-text-ssybngLw.js → clipboard-text-kLaMogs3.js} +3 -3
- package/dist/{clipboard-text-ssybngLw.js.map → clipboard-text-kLaMogs3.js.map} +1 -1
- package/dist/{code-Cx-QSoOT.js → code-BN8InC0G.js} +2 -2
- package/dist/{code-Cx-QSoOT.js.map → code-BN8InC0G.js.map} +1 -1
- package/dist/{collapsible-DWsXeXmS.js → collapsible-D_ueZ0jz.js} +1 -1
- package/dist/{collapsible-DWsXeXmS.js.map → collapsible-D_ueZ0jz.js.map} +1 -1
- package/dist/{combobox-C0iW6a0r.js → combobox-B7TOK0U2.js} +3 -3
- package/dist/{combobox-C0iW6a0r.js.map → combobox-B7TOK0U2.js.map} +1 -1
- package/dist/{command-palette-DGzioeki.js → command-palette-CuNUyJca.js} +2 -2
- package/dist/{command-palette-DGzioeki.js.map → command-palette-CuNUyJca.js.map} +1 -1
- package/dist/components/ai-actions.js +1 -1
- package/dist/components/ai-agent-card.js +1 -1
- package/dist/components/ai-approval.js +1 -1
- package/dist/components/ai-code-block.js +1 -1
- package/dist/components/ai-conversation.js +2 -2
- package/dist/components/ai-info-banner.js +1 -1
- package/dist/components/ai-message.js +1 -1
- package/dist/components/ai-mission-header.js +1 -1
- package/dist/components/ai-part-group.js +1 -1
- package/dist/components/ai-prompt-input.js +2 -2
- package/dist/components/ai-question.js +1 -1
- package/dist/components/ai-reasoning.js +1 -1
- package/dist/components/ai-response.js +1 -1
- package/dist/components/ai-shimmer.js +1 -1
- package/dist/components/ai-status-badge.js +1 -1
- package/dist/components/ai-streaming-text.js +2 -2
- package/dist/components/ai-subagent.js +1 -1
- package/dist/components/ai-suggestion.js +1 -1
- package/dist/components/ai-task-list.js +1 -1
- package/dist/components/ai-timeline.js +1 -1
- package/dist/components/ai-tool.js +1 -1
- package/dist/components/ai-usage-bar.js +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/banner.js +1 -1
- package/dist/components/breadcrumbs.js +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/chart.js +2 -2
- package/dist/components/checkbox.js +1 -1
- package/dist/components/clipboard-text.js +1 -1
- package/dist/components/code.js +1 -1
- package/dist/components/collapsible.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/command-palette.js +1 -1
- package/dist/components/data-grid.js +1 -1
- package/dist/components/date-picker.js +1 -1
- package/dist/components/date-range-picker.js +1 -1
- package/dist/components/dialog.js +1 -1
- package/dist/components/dropdown.js +1 -1
- package/dist/components/empty.js +1 -1
- package/dist/components/field.js +1 -1
- package/dist/components/filters.js +1 -1
- package/dist/components/flow.js +1 -1
- package/dist/components/grid.js +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/label.js +1 -1
- package/dist/components/layer-card.js +1 -1
- package/dist/components/loader.js +1 -1
- package/dist/components/menubar.js +1 -1
- package/dist/components/meter.js +1 -1
- package/dist/components/pagination.js +1 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/radio.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/sensitive-input.js +1 -1
- package/dist/components/sidebar.js +1 -1
- package/dist/components/signalflare-ai-logo.js +1 -1
- package/dist/components/sparkline.js +1 -1
- package/dist/components/stat-card.js +1 -1
- package/dist/components/surface.js +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.js +1 -1
- package/dist/components/text-roll.js +1 -1
- package/dist/components/text.js +1 -1
- package/dist/components/theme-toggle.js +1 -1
- package/dist/components/toast.js +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/use-agent-harness.js +1 -1
- package/dist/{data-grid-CG76N_hK.js → data-grid-DGHmU0w3.js} +8 -8
- package/dist/{data-grid-CG76N_hK.js.map → data-grid-DGHmU0w3.js.map} +1 -1
- package/dist/{date-picker-Dqg9L4xu.js → date-picker--ox89RBy.js} +1 -1
- package/dist/{date-picker-Dqg9L4xu.js.map → date-picker--ox89RBy.js.map} +1 -1
- package/dist/{date-range-picker-D75LLINc.js → date-range-picker-DVa7QBqE.js} +1 -1
- package/dist/{date-range-picker-D75LLINc.js.map → date-range-picker-DVa7QBqE.js.map} +1 -1
- package/dist/{dialog-CyHEQXEY.js → dialog-Bv1oSFOd.js} +2 -2
- package/dist/{dialog-CyHEQXEY.js.map → dialog-Bv1oSFOd.js.map} +1 -1
- package/dist/{dist-1-gcEL2L.js → dist-B6iWiWwp.js} +25 -25
- package/dist/{dist-1-gcEL2L.js.map → dist-B6iWiWwp.js.map} +1 -1
- package/dist/{dropdown-qnEYRFXZ.js → dropdown-B_nrGXjV.js} +2 -2
- package/dist/{dropdown-qnEYRFXZ.js.map → dropdown-B_nrGXjV.js.map} +1 -1
- package/dist/{echart-DURZEyai.js → echart-CdOUaT-r.js} +1 -1
- package/dist/{echart-DURZEyai.js.map → echart-CdOUaT-r.js.map} +1 -1
- package/dist/{empty-D2TypIId.js → empty-DZnN0zKX.js} +11 -6
- package/dist/empty-DZnN0zKX.js.map +1 -0
- package/dist/{field-Y_UK1_Cg.js → field-B_yVof52.js} +2 -2
- package/dist/{field-Y_UK1_Cg.js.map → field-B_yVof52.js.map} +1 -1
- package/dist/{filters-Bw_U6ZTx.js → filters-cpJCY21R.js} +7 -7
- package/dist/{filters-Bw_U6ZTx.js.map → filters-cpJCY21R.js.map} +1 -1
- package/dist/{flow-BRsYUCJa.js → flow-B4v198ot.js} +1 -1
- package/dist/{flow-BRsYUCJa.js.map → flow-B4v198ot.js.map} +1 -1
- package/dist/genui.js +1 -1
- package/dist/{grid-qUAN9hFx.js → grid-CEd64Lnh.js} +1 -1
- package/dist/{grid-qUAN9hFx.js.map → grid-CEd64Lnh.js.map} +1 -1
- package/dist/{highlight-to-react-ClEfL81q.js → highlight-to-react-D0Yav4jk.js} +1 -1
- package/dist/{highlight-to-react-ClEfL81q.js.map → highlight-to-react-D0Yav4jk.js.map} +1 -1
- package/dist/index.js +69 -69
- package/dist/{input-DXYUjGgD.js → input-B2bbijRh.js} +2 -2
- package/dist/{input-DXYUjGgD.js.map → input-B2bbijRh.js.map} +1 -1
- package/dist/{input-DddtBN-g.js → input-ClB_E4Lb.js} +4 -4
- package/dist/{input-DddtBN-g.js.map → input-ClB_E4Lb.js.map} +1 -1
- package/dist/{label-QtJxtJ4u.js → label-DUv_urO1.js} +2 -2
- package/dist/{label-QtJxtJ4u.js.map → label-DUv_urO1.js.map} +1 -1
- package/dist/{layer-card-BME0eljh.js → layer-card-BK7eYfwn.js} +1 -1
- package/dist/{layer-card-BME0eljh.js.map → layer-card-BK7eYfwn.js.map} +1 -1
- package/dist/layout-DJHMMap2.js +6103 -0
- package/dist/layout-DJHMMap2.js.map +1 -0
- package/dist/measured-text-BI3dTJmH.js +290 -0
- package/dist/measured-text-BI3dTJmH.js.map +1 -0
- package/dist/{menubar-C8NzAjfd.js → menubar-Cxf3xeAt.js} +2 -2
- package/dist/{menubar-C8NzAjfd.js.map → menubar-Cxf3xeAt.js.map} +1 -1
- package/dist/{meter-CpmTenEr.js → meter-BFFe9l5b.js} +1 -1
- package/dist/{meter-CpmTenEr.js.map → meter-BFFe9l5b.js.map} +1 -1
- package/dist/{pagination-BVqdlONY.js → pagination-yS372Tr4.js} +2 -2
- package/dist/{pagination-BVqdlONY.js.map → pagination-yS372Tr4.js.map} +1 -1
- package/dist/{popover-BRQZ2b6z.js → popover-SRoJaCZr.js} +1 -1
- package/dist/{popover-BRQZ2b6z.js.map → popover-SRoJaCZr.js.map} +1 -1
- package/dist/{radio-BNSwOt3B.js → radio-BcwhwYNB.js} +1 -1
- package/dist/{radio-BNSwOt3B.js.map → radio-BcwhwYNB.js.map} +1 -1
- package/dist/{select-1w2aebGQ.js → select-DMhdoHMa.js} +4 -4
- package/dist/{select-1w2aebGQ.js.map → select-DMhdoHMa.js.map} +1 -1
- package/dist/{sensitive-input-82Cez3vj.js → sensitive-input-CJUpIRal.js} +3 -3
- package/dist/{sensitive-input-82Cez3vj.js.map → sensitive-input-CJUpIRal.js.map} +1 -1
- package/dist/{sidebar-CAsCmSpM.js → sidebar-D4zrlYpn.js} +2 -2
- package/dist/{sidebar-CAsCmSpM.js.map → sidebar-D4zrlYpn.js.map} +1 -1
- package/dist/{signalflare-ai-logo-DDhxMJD6.js → signalflare-ai-logo-Bipogceq.js} +1 -1
- package/dist/{signalflare-ai-logo-DDhxMJD6.js.map → signalflare-ai-logo-Bipogceq.js.map} +1 -1
- package/dist/{skeleton-line-Do3UmGk9.js → skeleton-line-CH1-h6e2.js} +1 -1
- package/dist/{skeleton-line-Do3UmGk9.js.map → skeleton-line-CH1-h6e2.js.map} +1 -1
- package/dist/{sparkline-DdbeM4Ai.js → sparkline-DHmgj1d0.js} +2 -2
- package/dist/{sparkline-DdbeM4Ai.js.map → sparkline-DHmgj1d0.js.map} +1 -1
- package/dist/src/blocks/agent-harness/agent-harness.d.ts.map +1 -1
- package/dist/src/blocks/agent-harness/agent-harness.tsx +29 -5
- package/dist/src/components/ai-conversation/ai-conversation.d.ts +69 -37
- package/dist/src/components/ai-conversation/ai-conversation.d.ts.map +1 -1
- package/dist/src/components/ai-conversation/index.d.ts +2 -1
- package/dist/src/components/ai-conversation/index.d.ts.map +1 -1
- package/dist/src/components/ai-conversation/measurement-constants.d.ts +30 -0
- package/dist/src/components/ai-conversation/measurement-constants.d.ts.map +1 -0
- package/dist/src/components/ai-info-banner/ai-info-banner.d.ts.map +1 -1
- package/dist/src/components/ai-message/ai-message.d.ts +3 -0
- package/dist/src/components/ai-message/ai-message.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/ai-prompt-input.d.ts +58 -4
- package/dist/src/components/ai-prompt-input/ai-prompt-input.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/controller.d.ts +10 -2
- package/dist/src/components/ai-prompt-input/controller.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/index.d.ts +2 -2
- package/dist/src/components/ai-prompt-input/index.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/types.d.ts +16 -0
- package/dist/src/components/ai-prompt-input/types.d.ts.map +1 -1
- package/dist/src/components/ai-response/ai-response.d.ts +12 -1
- package/dist/src/components/ai-response/ai-response.d.ts.map +1 -1
- package/dist/src/components/ai-streaming-text/ai-streaming-text.d.ts +27 -0
- package/dist/src/components/ai-streaming-text/ai-streaming-text.d.ts.map +1 -1
- package/dist/src/components/ai-streaming-text/index.d.ts +1 -1
- package/dist/src/components/ai-streaming-text/index.d.ts.map +1 -1
- package/dist/src/components/ai-subagent/ai-subagent.d.ts.map +1 -1
- package/dist/src/components/ai-task-list/ai-task-list.d.ts.map +1 -1
- package/dist/src/components/ai-tool/ai-tool.d.ts.map +1 -1
- package/dist/src/components/banner/banner.d.ts.map +1 -1
- package/dist/src/components/empty/empty.d.ts.map +1 -1
- package/dist/src/components/stat-card/stat-card.d.ts +5 -0
- package/dist/src/components/stat-card/stat-card.d.ts.map +1 -1
- package/dist/src/components/text/text.d.ts +35 -1
- package/dist/src/components/text/text.d.ts.map +1 -1
- package/dist/src/components/tooltip/tooltip.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/utils/index.d.ts +2 -0
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/measured-text.d.ts +40 -0
- package/dist/src/utils/measured-text.d.ts.map +1 -0
- package/dist/src/utils/use-measured-text.d.ts +59 -0
- package/dist/src/utils/use-measured-text.d.ts.map +1 -0
- package/dist/{stat-card-CEZscNh8.js → stat-card-Ew-ofzEm.js} +28 -10
- package/dist/stat-card-Ew-ofzEm.js.map +1 -0
- package/dist/styles/sf-binding.css +9 -1
- package/dist/styles/sf-standalone.css +2 -2
- package/dist/styles/shadcn.css +1 -1
- package/dist/styles/theme-fedramp.css +12 -3
- package/dist/styles/theme-minimal.css +104 -26
- package/dist/styles/theme-sf.css +138 -39
- package/dist/styles/theme-vesper.css +91 -0
- package/dist/{surface-BduI7Ehl.js → surface-DGwRlC0o.js} +1 -1
- package/dist/{surface-BduI7Ehl.js.map → surface-DGwRlC0o.js.map} +1 -1
- package/dist/{switch-CzZBRBL7.js → switch-BxAMfHdt.js} +2 -2
- package/dist/{switch-CzZBRBL7.js.map → switch-BxAMfHdt.js.map} +1 -1
- package/dist/{table-Rv4JMy0B.js → table-BBeAtYVZ.js} +2 -2
- package/dist/{table-Rv4JMy0B.js.map → table-BBeAtYVZ.js.map} +1 -1
- package/dist/{tabs-1cHrYoel.js → tabs-CeHu7Scn.js} +1 -1
- package/dist/{tabs-1cHrYoel.js.map → tabs-CeHu7Scn.js.map} +1 -1
- package/dist/{text-KJmGkwnf.js → text-Cqryz7rk.js} +27 -5
- package/dist/text-Cqryz7rk.js.map +1 -0
- package/dist/{text-roll-BZ3I1umc.js → text-roll-Ch52hcQj.js} +1 -1
- package/dist/{text-roll-BZ3I1umc.js.map → text-roll-Ch52hcQj.js.map} +1 -1
- package/dist/{theme-toggle-Bhu681D7.js → theme-toggle-LDfIKEqx.js} +3 -3
- package/dist/{theme-toggle-Bhu681D7.js.map → theme-toggle-LDfIKEqx.js.map} +1 -1
- package/dist/{toast-Nw28a5Cx.js → toast-CaFQNYng.js} +2 -2
- package/dist/{toast-Nw28a5Cx.js.map → toast-CaFQNYng.js.map} +1 -1
- package/dist/{tooltip-Cb7QW-7H.js → tooltip-g9lFsvcT.js} +8 -2
- package/dist/tooltip-g9lFsvcT.js.map +1 -0
- package/dist/{use-agent-harness-BMyF8pTq.js → use-agent-harness-BTcNJdw4.js} +1 -1
- package/dist/{use-agent-harness-BMyF8pTq.js.map → use-agent-harness-BTcNJdw4.js.map} +1 -1
- package/dist/utils.js +2 -1
- package/package.json +2 -1
- package/scripts/component-registry/index.ts +2 -2
- package/scripts/css-build.ts +1 -1
- package/scripts/theme-generator/config.ts +27 -141
- package/scripts/theme-generator/generate-css.ts +0 -1
- package/scripts/theme-generator/index.ts +0 -1
- package/dist/ai-conversation-CArP7C8K.js +0 -184
- package/dist/ai-conversation-CArP7C8K.js.map +0 -1
- package/dist/ai-info-banner-uFxHHwBA.js.map +0 -1
- package/dist/ai-message-BjnFznXy.js.map +0 -1
- package/dist/ai-prompt-input-Dy1LfxPk.js.map +0 -1
- package/dist/ai-response-BEUg3xvd.js.map +0 -1
- package/dist/ai-streaming-text-CMfoThV0.js.map +0 -1
- package/dist/ai-subagent-DcPRqkAA.js.map +0 -1
- package/dist/ai-task-list-Da9zIm00.js.map +0 -1
- package/dist/ai-tool-Cn1O4xjP.js.map +0 -1
- package/dist/banner-B_6oBrsu.js.map +0 -1
- package/dist/empty-D2TypIId.js.map +0 -1
- package/dist/stat-card-CEZscNh8.js.map +0 -1
- package/dist/styles/theme-blue-tint.css +0 -98
- package/dist/text-KJmGkwnf.js.map +0 -1
- package/dist/tooltip-Cb7QW-7H.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-agent-card-BXHwhWAU.js","names":[],"sources":["../src/components/ai-agent-card/ai-agent-card.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n BrainIcon,\n CheckCircleIcon,\n CircleIcon,\n CodeIcon,\n MagnifyingGlassIcon,\n RobotIcon,\n SpinnerGapIcon,\n WrenchIcon,\n XCircleIcon,\n} from \"@phosphor-icons/react\";\nimport type { ComponentProps, ElementType } from \"react\";\nimport { forwardRef } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_AGENT_CARD_VARIANTS = {\n status: {\n idle: { classes: \"\", description: \"Agent is idle, not yet started\" },\n running: { classes: \"\", description: \"Agent is actively working\" },\n completed: { classes: \"\", description: \"Agent finished successfully\" },\n error: { classes: \"\", description: \"Agent encountered an error\" },\n },\n size: {\n sm: { classes: \"\", description: \"Compact card for dense grids\" },\n md: { classes: \"\", description: \"Default card size\" },\n },\n} as const;\n\nexport const SF_AI_AGENT_CARD_DEFAULT_VARIANTS = {\n status: \"idle\",\n size: \"md\",\n} as const;\n\nexport type SFAiAgentCardStatus = keyof typeof SF_AI_AGENT_CARD_VARIANTS.status;\nexport type SFAiAgentCardSize = keyof typeof SF_AI_AGENT_CARD_VARIANTS.size;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AiAgentCardProps = Omit<ComponentProps<\"button\">, \"children\"> & {\n /** Human-readable agent name (e.g. \"Explore\", \"Execute\"). */\n name: string;\n /** Agent type ID — used for icon mapping. */\n agentType?: string;\n /** Current status. @default \"idle\" */\n status?: SFAiAgentCardStatus;\n /** Model ID being used (e.g. \"claude-haiku-3.5\"). */\n modelId?: string;\n /** What the agent is currently doing. */\n currentTask?: string;\n /** Total elapsed duration in ms. */\n duration?: number;\n /** Number of tool calls made. */\n toolCallCount?: number;\n /** Whether this card is currently selected/active. */\n selected?: boolean;\n /** Custom icon override. */\n icon?: ElementType;\n /** Card size. @default \"md\" */\n size?: SFAiAgentCardSize;\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nconst AGENT_TYPE_ICONS: Record<string, ElementType> = {\n explore: MagnifyingGlassIcon,\n search: MagnifyingGlassIcon,\n execute: CodeIcon,\n code: CodeIcon,\n plan: BrainIcon,\n think: BrainIcon,\n tool: WrenchIcon,\n build: WrenchIcon,\n};\n\nfunction getAgentIcon(\n agentType?: string,\n customIcon?: ElementType\n): ElementType {\n if (customIcon) return customIcon;\n if (agentType) {\n const lower = agentType.toLowerCase();\n for (const [key, icon] of Object.entries(AGENT_TYPE_ICONS)) {\n if (lower.includes(key)) return icon;\n }\n }\n return RobotIcon;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const s = Math.round(ms / 100) / 10;\n if (s < 60) return `${s}s`;\n const m = Math.floor(s / 60);\n const rem = Math.round(s % 60);\n return `${m}m ${rem}s`;\n}\n\nfunction getModelShortName(modelId?: string): string {\n if (!modelId) return \"\";\n return (\n modelId\n .split(\"/\")\n .pop()\n ?.replace(/^claude-/, \"\")\n .replace(/-\\d+$/, \"\") ?? modelId\n );\n}\n\n// ─── Status decorations ───────────────────────────────────────────────────────\n\nconst STATUS_DOT: Record<SFAiAgentCardStatus, string> = {\n idle: \"bg-sf-fill\",\n running: \"bg-sf-brand animate-pulse\",\n completed: \"bg-sf-success\",\n error: \"bg-sf-danger\",\n};\n\nfunction StatusIcon({\n status,\n size = 14,\n}: {\n status: SFAiAgentCardStatus;\n size?: number;\n}) {\n switch (status) {\n case \"running\":\n return (\n <SpinnerGapIcon size={size} className=\"animate-spin text-sf-brand\" />\n );\n case \"completed\":\n return <CheckCircleIcon size={size} className=\"text-sf-success\" />;\n case \"error\":\n return <XCircleIcon size={size} className=\"text-sf-danger\" />;\n default:\n return <CircleIcon size={size} className=\"text-sf-inactive\" />;\n }\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * `AiAgentCard` — compact card showing one agent's status in a commander dashboard.\n *\n * Displays: agent icon, name, status indicator, current task, model, duration,\n * and tool call count. Clickable for selection.\n *\n * @example\n * ```tsx\n * <AiAgentCard\n * name=\"Explore\"\n * agentType=\"explore\"\n * status=\"running\"\n * modelId=\"claude-haiku-3.5\"\n * currentTask=\"Scanning auth files...\"\n * duration={4200}\n * toolCallCount={3}\n * selected\n * onClick={handleSelect}\n * />\n * ```\n */\nexport const AiAgentCard = forwardRef<HTMLButtonElement, AiAgentCardProps>(\n (\n {\n name,\n agentType,\n status = SF_AI_AGENT_CARD_DEFAULT_VARIANTS.status,\n modelId,\n currentTask,\n duration,\n toolCallCount,\n selected,\n icon,\n size = SF_AI_AGENT_CARD_DEFAULT_VARIANTS.size,\n className,\n onClick,\n ...props\n },\n ref\n ) => {\n const Icon = getAgentIcon(agentType, icon);\n const modelShort = getModelShortName(modelId);\n const isSm = size === \"sm\";\n\n return (\n <button\n ref={ref}\n type=\"button\"\n aria-pressed={selected}\n onClick={onClick}\n className={cn(\n \"group flex flex-col gap-2 rounded-xl border text-left transition-all\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sf-ring focus-visible:ring-offset-1\",\n isSm ? \"p-2.5\" : \"p-3.5\",\n selected\n ? \"border-sf-line bg-sf-recessed shadow-sm\"\n : \"border-sf-line bg-sf-elevated hover:border-sf-line hover:bg-sf-tint\",\n !onClick && \"cursor-default\",\n className\n )}\n {...props}\n >\n {/* Header row */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2 min-w-0\">\n {/* Agent icon */}\n <div\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-lg\",\n isSm ? \"size-6\" : \"size-8\",\n status === \"running\"\n ? \"bg-sf-brand/10 text-sf-brand\"\n : status === \"completed\"\n ? \"bg-sf-tint text-sf-success\"\n : status === \"error\"\n ? \"bg-sf-tint text-sf-danger\"\n : \"bg-sf-fill text-sf-subtle\"\n )}\n >\n <Icon size={isSm ? 12 : 16} />\n </div>\n\n {/* Name */}\n <span\n className={cn(\n \"truncate font-medium text-sf-default\",\n isSm ? \"text-xs\" : \"text-sm\"\n )}\n >\n {name}\n </span>\n </div>\n\n {/* Status icon */}\n <StatusIcon status={status} size={isSm ? 12 : 14} />\n </div>\n\n {/* Current task */}\n {currentTask && (\n <p\n className={cn(\n \"truncate text-sf-subtle leading-snug\",\n isSm ? \"text-[10px]\" : \"text-xs\"\n )}\n >\n {currentTask}\n </p>\n )}\n\n {/* Footer row: model + stats */}\n {!isSm && (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1.5\">\n {/* Status dot + model */}\n <span\n className={cn(\n \"size-1.5 shrink-0 rounded-full\",\n STATUS_DOT[status]\n )}\n />\n {modelShort && (\n <span className=\"font-mono text-[10px] text-sf-inactive\">\n {modelShort}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 text-[10px] text-sf-subtle\">\n {typeof toolCallCount === \"number\" && toolCallCount > 0 && (\n <span className=\"flex items-center gap-0.5\">\n <WrenchIcon size={9} />\n {toolCallCount}\n </span>\n )}\n {typeof duration === \"number\" && (\n <span>{formatDuration(duration)}</span>\n )}\n </div>\n </div>\n )}\n </button>\n );\n }\n);\n\nAiAgentCard.displayName = \"AiAgentCard\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai-agent-card-BR2NIYhi.js","names":[],"sources":["../src/components/ai-agent-card/ai-agent-card.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n BrainIcon,\n CheckCircleIcon,\n CircleIcon,\n CodeIcon,\n MagnifyingGlassIcon,\n RobotIcon,\n SpinnerGapIcon,\n WrenchIcon,\n XCircleIcon,\n} from \"@phosphor-icons/react\";\nimport type { ComponentProps, ElementType } from \"react\";\nimport { forwardRef } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_AGENT_CARD_VARIANTS = {\n status: {\n idle: { classes: \"\", description: \"Agent is idle, not yet started\" },\n running: { classes: \"\", description: \"Agent is actively working\" },\n completed: { classes: \"\", description: \"Agent finished successfully\" },\n error: { classes: \"\", description: \"Agent encountered an error\" },\n },\n size: {\n sm: { classes: \"\", description: \"Compact card for dense grids\" },\n md: { classes: \"\", description: \"Default card size\" },\n },\n} as const;\n\nexport const SF_AI_AGENT_CARD_DEFAULT_VARIANTS = {\n status: \"idle\",\n size: \"md\",\n} as const;\n\nexport type SFAiAgentCardStatus = keyof typeof SF_AI_AGENT_CARD_VARIANTS.status;\nexport type SFAiAgentCardSize = keyof typeof SF_AI_AGENT_CARD_VARIANTS.size;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AiAgentCardProps = Omit<ComponentProps<\"button\">, \"children\"> & {\n /** Human-readable agent name (e.g. \"Explore\", \"Execute\"). */\n name: string;\n /** Agent type ID — used for icon mapping. */\n agentType?: string;\n /** Current status. @default \"idle\" */\n status?: SFAiAgentCardStatus;\n /** Model ID being used (e.g. \"claude-haiku-3.5\"). */\n modelId?: string;\n /** What the agent is currently doing. */\n currentTask?: string;\n /** Total elapsed duration in ms. */\n duration?: number;\n /** Number of tool calls made. */\n toolCallCount?: number;\n /** Whether this card is currently selected/active. */\n selected?: boolean;\n /** Custom icon override. */\n icon?: ElementType;\n /** Card size. @default \"md\" */\n size?: SFAiAgentCardSize;\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nconst AGENT_TYPE_ICONS: Record<string, ElementType> = {\n explore: MagnifyingGlassIcon,\n search: MagnifyingGlassIcon,\n execute: CodeIcon,\n code: CodeIcon,\n plan: BrainIcon,\n think: BrainIcon,\n tool: WrenchIcon,\n build: WrenchIcon,\n};\n\nfunction getAgentIcon(\n agentType?: string,\n customIcon?: ElementType\n): ElementType {\n if (customIcon) return customIcon;\n if (agentType) {\n const lower = agentType.toLowerCase();\n for (const [key, icon] of Object.entries(AGENT_TYPE_ICONS)) {\n if (lower.includes(key)) return icon;\n }\n }\n return RobotIcon;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const s = Math.round(ms / 100) / 10;\n if (s < 60) return `${s}s`;\n const m = Math.floor(s / 60);\n const rem = Math.round(s % 60);\n return `${m}m ${rem}s`;\n}\n\nfunction getModelShortName(modelId?: string): string {\n if (!modelId) return \"\";\n return (\n modelId\n .split(\"/\")\n .pop()\n ?.replace(/^claude-/, \"\")\n .replace(/-\\d+$/, \"\") ?? modelId\n );\n}\n\n// ─── Status decorations ───────────────────────────────────────────────────────\n\nconst STATUS_DOT: Record<SFAiAgentCardStatus, string> = {\n idle: \"bg-sf-fill\",\n running: \"bg-sf-brand animate-pulse\",\n completed: \"bg-sf-success\",\n error: \"bg-sf-danger\",\n};\n\nfunction StatusIcon({\n status,\n size = 14,\n}: {\n status: SFAiAgentCardStatus;\n size?: number;\n}) {\n switch (status) {\n case \"running\":\n return (\n <SpinnerGapIcon size={size} className=\"animate-spin text-sf-brand\" />\n );\n case \"completed\":\n return <CheckCircleIcon size={size} className=\"text-sf-success\" />;\n case \"error\":\n return <XCircleIcon size={size} className=\"text-sf-danger\" />;\n default:\n return <CircleIcon size={size} className=\"text-sf-inactive\" />;\n }\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * `AiAgentCard` — compact card showing one agent's status in a commander dashboard.\n *\n * Displays: agent icon, name, status indicator, current task, model, duration,\n * and tool call count. Clickable for selection.\n *\n * @example\n * ```tsx\n * <AiAgentCard\n * name=\"Explore\"\n * agentType=\"explore\"\n * status=\"running\"\n * modelId=\"claude-haiku-3.5\"\n * currentTask=\"Scanning auth files...\"\n * duration={4200}\n * toolCallCount={3}\n * selected\n * onClick={handleSelect}\n * />\n * ```\n */\nexport const AiAgentCard = forwardRef<HTMLButtonElement, AiAgentCardProps>(\n (\n {\n name,\n agentType,\n status = SF_AI_AGENT_CARD_DEFAULT_VARIANTS.status,\n modelId,\n currentTask,\n duration,\n toolCallCount,\n selected,\n icon,\n size = SF_AI_AGENT_CARD_DEFAULT_VARIANTS.size,\n className,\n onClick,\n ...props\n },\n ref\n ) => {\n const Icon = getAgentIcon(agentType, icon);\n const modelShort = getModelShortName(modelId);\n const isSm = size === \"sm\";\n\n return (\n <button\n ref={ref}\n type=\"button\"\n aria-pressed={selected}\n onClick={onClick}\n className={cn(\n \"group flex flex-col gap-2 rounded-xl border text-left transition-all\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sf-ring focus-visible:ring-offset-1\",\n isSm ? \"p-2.5\" : \"p-3.5\",\n selected\n ? \"border-sf-line bg-sf-recessed shadow-sm\"\n : \"border-sf-line bg-sf-elevated hover:border-sf-line hover:bg-sf-tint\",\n !onClick && \"cursor-default\",\n className\n )}\n {...props}\n >\n {/* Header row */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2 min-w-0\">\n {/* Agent icon */}\n <div\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-lg\",\n isSm ? \"size-6\" : \"size-8\",\n status === \"running\"\n ? \"bg-sf-brand/10 text-sf-brand\"\n : status === \"completed\"\n ? \"bg-sf-tint text-sf-success\"\n : status === \"error\"\n ? \"bg-sf-tint text-sf-danger\"\n : \"bg-sf-fill text-sf-subtle\"\n )}\n >\n <Icon size={isSm ? 12 : 16} />\n </div>\n\n {/* Name */}\n <span\n className={cn(\n \"truncate font-medium text-sf-default\",\n isSm ? \"text-xs\" : \"text-sm\"\n )}\n >\n {name}\n </span>\n </div>\n\n {/* Status icon */}\n <StatusIcon status={status} size={isSm ? 12 : 14} />\n </div>\n\n {/* Current task */}\n {currentTask && (\n <p\n className={cn(\n \"truncate text-sf-subtle leading-snug\",\n isSm ? \"text-[10px]\" : \"text-xs\"\n )}\n >\n {currentTask}\n </p>\n )}\n\n {/* Footer row: model + stats */}\n {!isSm && (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1.5\">\n {/* Status dot + model */}\n <span\n className={cn(\n \"size-1.5 shrink-0 rounded-full\",\n STATUS_DOT[status]\n )}\n />\n {modelShort && (\n <span className=\"font-mono text-[10px] text-sf-inactive\">\n {modelShort}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 text-[10px] text-sf-subtle\">\n {typeof toolCallCount === \"number\" && toolCallCount > 0 && (\n <span className=\"flex items-center gap-0.5\">\n <WrenchIcon size={9} />\n {toolCallCount}\n </span>\n )}\n {typeof duration === \"number\" && (\n <span>{formatDuration(duration)}</span>\n )}\n </div>\n </div>\n )}\n </button>\n );\n }\n);\n\nAiAgentCard.displayName = \"AiAgentCard\";\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { t as cn } from "./cn-YROP2_ox.js";
|
|
3
|
-
import { t as Button } from "./button-
|
|
3
|
+
import { t as Button } from "./button-CO6-qPax.js";
|
|
4
4
|
import { useCallback, useState } from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { CheckCircleIcon, ListChecksIcon, ProhibitIcon, ShieldCheckIcon } from "@phosphor-icons/react";
|
|
@@ -181,4 +181,4 @@ AiApproval.displayName = "AiApproval";
|
|
|
181
181
|
//#endregion
|
|
182
182
|
export { SF_AI_APPROVAL_DEFAULT_VARIANTS as n, SF_AI_APPROVAL_VARIANTS as r, AiApproval as t };
|
|
183
183
|
|
|
184
|
-
//# sourceMappingURL=ai-approval-
|
|
184
|
+
//# sourceMappingURL=ai-approval-Ba7mrKba.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-approval-aa0qvjFN.js","names":[],"sources":["../src/components/ai-approval/ai-approval.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n CheckCircleIcon,\n ProhibitIcon,\n ShieldCheckIcon,\n ListChecksIcon,\n} from \"@phosphor-icons/react\";\nimport type { ElementType, HTMLAttributes, ReactNode } from \"react\";\nimport { useCallback, useState } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../button\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_APPROVAL_VARIANTS = {\n kind: {\n tool: { classes: \"\", description: \"Tool call approval\" },\n plan: { classes: \"\", description: \"Plan approval\" },\n },\n status: {\n pending: { classes: \"\", description: \"Awaiting user decision\" },\n approved: { classes: \"\", description: \"Approved by user\" },\n rejected: { classes: \"\", description: \"Rejected by user\" },\n },\n} as const;\n\nexport const SF_AI_APPROVAL_DEFAULT_VARIANTS = {\n kind: \"tool\",\n status: \"pending\",\n} as const;\n\nexport type SFAiApprovalKind = keyof typeof SF_AI_APPROVAL_VARIANTS.kind;\nexport type SFAiApprovalStatus = keyof typeof SF_AI_APPROVAL_VARIANTS.status;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AiApprovalItem = {\n id: string;\n label: string;\n description?: string;\n};\n\nexport type AiApprovalProps = Omit<HTMLAttributes<HTMLDivElement>, \"title\"> & {\n /** Approval kind — `\"tool\"` for individual tool calls, `\"plan\"` for multi-step plans. */\n kind?: SFAiApprovalKind;\n /** Current approval status. */\n status?: SFAiApprovalStatus;\n /** Title text shown in the header. */\n title: string;\n /** Optional description below the title. */\n description?: string;\n /** Custom icon. Defaults to shield (tool) or list (plan). */\n icon?: ElementType;\n /**\n * For plan approvals: list of steps/items in the plan.\n * For tool approvals: can show tool name, parameters, etc.\n */\n items?: AiApprovalItem[];\n /** Called when user approves. */\n onApprove?: () => void;\n /** Called when user rejects. Receives optional feedback string. */\n onReject?: (feedback?: string) => void;\n /** Label for the approve button. Default: `\"Approve\"`. */\n approveLabel?: string;\n /** Label for the reject button. Default: `\"Reject\"`. */\n rejectLabel?: string;\n /** Show a text input for rejection feedback. Default: `false`. */\n showFeedback?: boolean;\n /** Content rendered below items and above the action buttons. */\n children?: ReactNode;\n};\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nconst KIND_ICONS: Record<SFAiApprovalKind, ElementType> = {\n tool: ShieldCheckIcon,\n plan: ListChecksIcon,\n};\n\nconst STATUS_CONFIG: Record<\n SFAiApprovalStatus,\n { icon: ElementType; label: string; className: string }\n> = {\n pending: {\n icon: ShieldCheckIcon,\n label: \"Awaiting approval\",\n className: \"text-sf-subtle\",\n },\n approved: {\n icon: CheckCircleIcon,\n label: \"Approved\",\n className: \"text-sf-success\",\n },\n rejected: {\n icon: ProhibitIcon,\n label: \"Rejected\",\n className: \"text-sf-danger\",\n },\n};\n\n/**\n * Approval card for tool calls and plan submissions.\n *\n * Maps to harness events: `tool_approval_required`, `plan_approval_required`,\n * `plan_approved`.\n *\n * @example\n * ```tsx\n * <AiApproval\n * kind=\"tool\"\n * title=\"Execute shell command\"\n * description=\"rm -rf /tmp/cache\"\n * onApprove={() => harness.respondToToolApproval({ decision: 'approve' })}\n * onReject={() => harness.respondToToolApproval({ decision: 'decline' })}\n * />\n *\n * <AiApproval\n * kind=\"plan\"\n * title=\"Deployment plan\"\n * items={[\n * { id: \"1\", label: \"Run tests\" },\n * { id: \"2\", label: \"Build production bundle\" },\n * { id: \"3\", label: \"Deploy to staging\" },\n * ]}\n * showFeedback\n * onApprove={() => harness.respondToPlanApproval({ planId, response: { action: 'approved' } })}\n * onReject={(feedback) => harness.respondToPlanApproval({ planId, response: { action: 'rejected', feedback } })}\n * />\n * ```\n */\nexport function AiApproval({\n kind = \"tool\",\n status = \"pending\",\n title,\n description,\n icon,\n items,\n onApprove,\n onReject,\n approveLabel = \"Approve\",\n rejectLabel = \"Reject\",\n showFeedback = false,\n children,\n className,\n ...props\n}: AiApprovalProps) {\n const [feedback, setFeedback] = useState(\"\");\n const isPending = status === \"pending\";\n\n const IconComponent = icon ?? KIND_ICONS[kind];\n const statusConfig = STATUS_CONFIG[status];\n const StatusIcon = statusConfig.icon;\n\n const handleReject = useCallback(() => {\n onReject?.(showFeedback ? feedback : undefined);\n }, [onReject, showFeedback, feedback]);\n\n return (\n <div\n className={cn(\n \"flex flex-col gap-2.5 rounded-lg border border-sf-line bg-sf-elevated p-3\",\n !isPending && \"opacity-75\",\n className\n )}\n {...props}\n >\n {/* Header */}\n <div className=\"flex items-start gap-2.5\">\n <div\n className={cn(\n \"mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-md\",\n isPending ? \"bg-sf-brand/10 text-sf-brand\" : statusConfig.className\n )}\n >\n {isPending ? (\n <IconComponent className=\"size-4\" weight=\"bold\" />\n ) : (\n <StatusIcon className=\"size-4\" weight=\"bold\" />\n )}\n </div>\n <div className=\"flex min-w-0 flex-col gap-0.5\">\n <span className=\"text-sm font-medium text-sf-default\">{title}</span>\n {description && (\n <span className=\"text-xs text-sf-subtle\">{description}</span>\n )}\n </div>\n {!isPending && (\n <span\n className={cn(\n \"ml-auto shrink-0 text-xs font-medium\",\n statusConfig.className\n )}\n >\n {statusConfig.label}\n </span>\n )}\n </div>\n\n {/* Items (plan steps / tool details) */}\n {items && items.length > 0 && (\n <div className=\"flex flex-col gap-1 pl-8\">\n {items.map((item, index) => (\n <div key={item.id} className=\"flex items-start gap-2 text-xs\">\n <span className=\"mt-px shrink-0 font-mono text-sf-subtle\">\n {index + 1}.\n </span>\n <div className=\"flex min-w-0 flex-col\">\n <span className=\"text-sf-default\">{item.label}</span>\n {item.description && (\n <span className=\"text-sf-subtle\">{item.description}</span>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {children}\n\n {/* Feedback input */}\n {isPending && showFeedback && (\n <textarea\n className=\"mx-0 min-h-[60px] resize-none rounded-md border border-sf-line bg-sf-base px-2.5 py-1.5 text-xs text-sf-default outline-none placeholder:text-sf-inactive focus:border-sf-ring\"\n onChange={(e) => setFeedback(e.target.value)}\n placeholder=\"Optional feedback…\"\n value={feedback}\n />\n )}\n\n {/* Action buttons */}\n {isPending && (\n <div className=\"flex items-center justify-end gap-2\">\n <Button\n onClick={handleReject}\n size=\"sm\"\n type=\"button\"\n variant=\"ghost\"\n >\n {rejectLabel}\n </Button>\n <Button onClick={onApprove} size=\"sm\" type=\"button\" variant=\"primary\">\n {approveLabel}\n </Button>\n </div>\n )}\n </div>\n );\n}\n\nAiApproval.displayName = \"AiApproval\";\n"],"mappings":";;;;;;;AAgBA,IAAa,0BAA0B;CACrC,MAAM;EACJ,MAAM;GAAE,SAAS;GAAI,aAAa;GAAsB;EACxD,MAAM;GAAE,SAAS;GAAI,aAAa;GAAiB;EACpD;CACD,QAAQ;EACN,SAAS;GAAE,SAAS;GAAI,aAAa;GAA0B;EAC/D,UAAU;GAAE,SAAS;GAAI,aAAa;GAAoB;EAC1D,UAAU;GAAE,SAAS;GAAI,aAAa;GAAoB;EAC3D;CACF;AAED,IAAa,kCAAkC;CAC7C,MAAM;CACN,QAAQ;CACT;AA6CD,IAAM,aAAoD;CACxD,MAAM;CACN,MAAM;CACP;AAED,IAAM,gBAGF;CACF,SAAS;EACP,MAAM;EACN,OAAO;EACP,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,OAAO;EACP,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,OAAO;EACP,WAAW;EACZ;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCD,SAAgB,WAAW,EACzB,OAAO,QACP,SAAS,WACT,OACA,aACA,MACA,OACA,WACA,UACA,eAAe,WACf,cAAc,UACd,eAAe,OACf,UACA,WACA,GAAG,SACe;CAClB,MAAM,CAAC,UAAU,eAAe,SAAS,GAAG;CAC5C,MAAM,YAAY,WAAW;CAE7B,MAAM,gBAAgB,QAAQ,WAAW;CACzC,MAAM,eAAe,cAAc;CACnC,MAAM,aAAa,aAAa;CAEhC,MAAM,eAAe,kBAAkB;AACrC,aAAW,eAAe,WAAW,KAAA,EAAU;IAC9C;EAAC;EAAU;EAAc;EAAS,CAAC;AAEtC,QACE,qBAAC,OAAD;EACE,WAAW,GACT,6EACA,CAAC,aAAa,cACd,UACD;EACD,GAAI;YANN;GASE,qBAAC,OAAD;IAAK,WAAU;cAAf;KACE,oBAAC,OAAD;MACE,WAAW,GACT,sEACA,YAAY,iCAAiC,aAAa,UAC3D;gBAEA,YACC,oBAAC,eAAD;OAAe,WAAU;OAAS,QAAO;OAAS,CAAA,GAElD,oBAAC,YAAD;OAAY,WAAU;OAAS,QAAO;OAAS,CAAA;MAE7C,CAAA;KACN,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,QAAD;OAAM,WAAU;iBAAuC;OAAa,CAAA,EACnE,eACC,oBAAC,QAAD;OAAM,WAAU;iBAA0B;OAAmB,CAAA,CAE3D;;KACL,CAAC,aACA,oBAAC,QAAD;MACE,WAAW,GACT,wCACA,aAAa,UACd;gBAEA,aAAa;MACT,CAAA;KAEL;;GAGL,SAAS,MAAM,SAAS,KACvB,oBAAC,OAAD;IAAK,WAAU;cACZ,MAAM,KAAK,MAAM,UAChB,qBAAC,OAAD;KAAmB,WAAU;eAA7B,CACE,qBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,QAAQ,GAAE,IACN;SACP,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,QAAD;OAAM,WAAU;iBAAmB,KAAK;OAAa,CAAA,EACpD,KAAK,eACJ,oBAAC,QAAD;OAAM,WAAU;iBAAkB,KAAK;OAAmB,CAAA,CAExD;QACF;OAVI,KAAK,GAUT,CACN;IACE,CAAA;GAGP;GAGA,aAAa,gBACZ,oBAAC,YAAD;IACE,WAAU;IACV,WAAW,MAAM,YAAY,EAAE,OAAO,MAAM;IAC5C,aAAY;IACZ,OAAO;IACP,CAAA;GAIH,aACC,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,oBAAC,QAAD;KACE,SAAS;KACT,MAAK;KACL,MAAK;KACL,SAAQ;eAEP;KACM,CAAA,EACT,oBAAC,QAAD;KAAQ,SAAS;KAAW,MAAK;KAAK,MAAK;KAAS,SAAQ;eACzD;KACM,CAAA,CACL;;GAEJ;;;AAIV,WAAW,cAAc"}
|
|
1
|
+
{"version":3,"file":"ai-approval-Ba7mrKba.js","names":[],"sources":["../src/components/ai-approval/ai-approval.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n CheckCircleIcon,\n ProhibitIcon,\n ShieldCheckIcon,\n ListChecksIcon,\n} from \"@phosphor-icons/react\";\nimport type { ElementType, HTMLAttributes, ReactNode } from \"react\";\nimport { useCallback, useState } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"../button\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_APPROVAL_VARIANTS = {\n kind: {\n tool: { classes: \"\", description: \"Tool call approval\" },\n plan: { classes: \"\", description: \"Plan approval\" },\n },\n status: {\n pending: { classes: \"\", description: \"Awaiting user decision\" },\n approved: { classes: \"\", description: \"Approved by user\" },\n rejected: { classes: \"\", description: \"Rejected by user\" },\n },\n} as const;\n\nexport const SF_AI_APPROVAL_DEFAULT_VARIANTS = {\n kind: \"tool\",\n status: \"pending\",\n} as const;\n\nexport type SFAiApprovalKind = keyof typeof SF_AI_APPROVAL_VARIANTS.kind;\nexport type SFAiApprovalStatus = keyof typeof SF_AI_APPROVAL_VARIANTS.status;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AiApprovalItem = {\n id: string;\n label: string;\n description?: string;\n};\n\nexport type AiApprovalProps = Omit<HTMLAttributes<HTMLDivElement>, \"title\"> & {\n /** Approval kind — `\"tool\"` for individual tool calls, `\"plan\"` for multi-step plans. */\n kind?: SFAiApprovalKind;\n /** Current approval status. */\n status?: SFAiApprovalStatus;\n /** Title text shown in the header. */\n title: string;\n /** Optional description below the title. */\n description?: string;\n /** Custom icon. Defaults to shield (tool) or list (plan). */\n icon?: ElementType;\n /**\n * For plan approvals: list of steps/items in the plan.\n * For tool approvals: can show tool name, parameters, etc.\n */\n items?: AiApprovalItem[];\n /** Called when user approves. */\n onApprove?: () => void;\n /** Called when user rejects. Receives optional feedback string. */\n onReject?: (feedback?: string) => void;\n /** Label for the approve button. Default: `\"Approve\"`. */\n approveLabel?: string;\n /** Label for the reject button. Default: `\"Reject\"`. */\n rejectLabel?: string;\n /** Show a text input for rejection feedback. Default: `false`. */\n showFeedback?: boolean;\n /** Content rendered below items and above the action buttons. */\n children?: ReactNode;\n};\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nconst KIND_ICONS: Record<SFAiApprovalKind, ElementType> = {\n tool: ShieldCheckIcon,\n plan: ListChecksIcon,\n};\n\nconst STATUS_CONFIG: Record<\n SFAiApprovalStatus,\n { icon: ElementType; label: string; className: string }\n> = {\n pending: {\n icon: ShieldCheckIcon,\n label: \"Awaiting approval\",\n className: \"text-sf-subtle\",\n },\n approved: {\n icon: CheckCircleIcon,\n label: \"Approved\",\n className: \"text-sf-success\",\n },\n rejected: {\n icon: ProhibitIcon,\n label: \"Rejected\",\n className: \"text-sf-danger\",\n },\n};\n\n/**\n * Approval card for tool calls and plan submissions.\n *\n * Maps to harness events: `tool_approval_required`, `plan_approval_required`,\n * `plan_approved`.\n *\n * @example\n * ```tsx\n * <AiApproval\n * kind=\"tool\"\n * title=\"Execute shell command\"\n * description=\"rm -rf /tmp/cache\"\n * onApprove={() => harness.respondToToolApproval({ decision: 'approve' })}\n * onReject={() => harness.respondToToolApproval({ decision: 'decline' })}\n * />\n *\n * <AiApproval\n * kind=\"plan\"\n * title=\"Deployment plan\"\n * items={[\n * { id: \"1\", label: \"Run tests\" },\n * { id: \"2\", label: \"Build production bundle\" },\n * { id: \"3\", label: \"Deploy to staging\" },\n * ]}\n * showFeedback\n * onApprove={() => harness.respondToPlanApproval({ planId, response: { action: 'approved' } })}\n * onReject={(feedback) => harness.respondToPlanApproval({ planId, response: { action: 'rejected', feedback } })}\n * />\n * ```\n */\nexport function AiApproval({\n kind = \"tool\",\n status = \"pending\",\n title,\n description,\n icon,\n items,\n onApprove,\n onReject,\n approveLabel = \"Approve\",\n rejectLabel = \"Reject\",\n showFeedback = false,\n children,\n className,\n ...props\n}: AiApprovalProps) {\n const [feedback, setFeedback] = useState(\"\");\n const isPending = status === \"pending\";\n\n const IconComponent = icon ?? KIND_ICONS[kind];\n const statusConfig = STATUS_CONFIG[status];\n const StatusIcon = statusConfig.icon;\n\n const handleReject = useCallback(() => {\n onReject?.(showFeedback ? feedback : undefined);\n }, [onReject, showFeedback, feedback]);\n\n return (\n <div\n className={cn(\n \"flex flex-col gap-2.5 rounded-lg border border-sf-line bg-sf-elevated p-3\",\n !isPending && \"opacity-75\",\n className\n )}\n {...props}\n >\n {/* Header */}\n <div className=\"flex items-start gap-2.5\">\n <div\n className={cn(\n \"mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-md\",\n isPending ? \"bg-sf-brand/10 text-sf-brand\" : statusConfig.className\n )}\n >\n {isPending ? (\n <IconComponent className=\"size-4\" weight=\"bold\" />\n ) : (\n <StatusIcon className=\"size-4\" weight=\"bold\" />\n )}\n </div>\n <div className=\"flex min-w-0 flex-col gap-0.5\">\n <span className=\"text-sm font-medium text-sf-default\">{title}</span>\n {description && (\n <span className=\"text-xs text-sf-subtle\">{description}</span>\n )}\n </div>\n {!isPending && (\n <span\n className={cn(\n \"ml-auto shrink-0 text-xs font-medium\",\n statusConfig.className\n )}\n >\n {statusConfig.label}\n </span>\n )}\n </div>\n\n {/* Items (plan steps / tool details) */}\n {items && items.length > 0 && (\n <div className=\"flex flex-col gap-1 pl-8\">\n {items.map((item, index) => (\n <div key={item.id} className=\"flex items-start gap-2 text-xs\">\n <span className=\"mt-px shrink-0 font-mono text-sf-subtle\">\n {index + 1}.\n </span>\n <div className=\"flex min-w-0 flex-col\">\n <span className=\"text-sf-default\">{item.label}</span>\n {item.description && (\n <span className=\"text-sf-subtle\">{item.description}</span>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {children}\n\n {/* Feedback input */}\n {isPending && showFeedback && (\n <textarea\n className=\"mx-0 min-h-[60px] resize-none rounded-md border border-sf-line bg-sf-base px-2.5 py-1.5 text-xs text-sf-default outline-none placeholder:text-sf-inactive focus:border-sf-ring\"\n onChange={(e) => setFeedback(e.target.value)}\n placeholder=\"Optional feedback…\"\n value={feedback}\n />\n )}\n\n {/* Action buttons */}\n {isPending && (\n <div className=\"flex items-center justify-end gap-2\">\n <Button\n onClick={handleReject}\n size=\"sm\"\n type=\"button\"\n variant=\"ghost\"\n >\n {rejectLabel}\n </Button>\n <Button onClick={onApprove} size=\"sm\" type=\"button\" variant=\"primary\">\n {approveLabel}\n </Button>\n </div>\n )}\n </div>\n );\n}\n\nAiApproval.displayName = \"AiApproval\";\n"],"mappings":";;;;;;;AAgBA,IAAa,0BAA0B;CACrC,MAAM;EACJ,MAAM;GAAE,SAAS;GAAI,aAAa;GAAsB;EACxD,MAAM;GAAE,SAAS;GAAI,aAAa;GAAiB;EACpD;CACD,QAAQ;EACN,SAAS;GAAE,SAAS;GAAI,aAAa;GAA0B;EAC/D,UAAU;GAAE,SAAS;GAAI,aAAa;GAAoB;EAC1D,UAAU;GAAE,SAAS;GAAI,aAAa;GAAoB;EAC3D;CACF;AAED,IAAa,kCAAkC;CAC7C,MAAM;CACN,QAAQ;CACT;AA6CD,IAAM,aAAoD;CACxD,MAAM;CACN,MAAM;CACP;AAED,IAAM,gBAGF;CACF,SAAS;EACP,MAAM;EACN,OAAO;EACP,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,OAAO;EACP,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,OAAO;EACP,WAAW;EACZ;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCD,SAAgB,WAAW,EACzB,OAAO,QACP,SAAS,WACT,OACA,aACA,MACA,OACA,WACA,UACA,eAAe,WACf,cAAc,UACd,eAAe,OACf,UACA,WACA,GAAG,SACe;CAClB,MAAM,CAAC,UAAU,eAAe,SAAS,GAAG;CAC5C,MAAM,YAAY,WAAW;CAE7B,MAAM,gBAAgB,QAAQ,WAAW;CACzC,MAAM,eAAe,cAAc;CACnC,MAAM,aAAa,aAAa;CAEhC,MAAM,eAAe,kBAAkB;AACrC,aAAW,eAAe,WAAW,KAAA,EAAU;IAC9C;EAAC;EAAU;EAAc;EAAS,CAAC;AAEtC,QACE,qBAAC,OAAD;EACE,WAAW,GACT,6EACA,CAAC,aAAa,cACd,UACD;EACD,GAAI;YANN;GASE,qBAAC,OAAD;IAAK,WAAU;cAAf;KACE,oBAAC,OAAD;MACE,WAAW,GACT,sEACA,YAAY,iCAAiC,aAAa,UAC3D;gBAEA,YACC,oBAAC,eAAD;OAAe,WAAU;OAAS,QAAO;OAAS,CAAA,GAElD,oBAAC,YAAD;OAAY,WAAU;OAAS,QAAO;OAAS,CAAA;MAE7C,CAAA;KACN,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,QAAD;OAAM,WAAU;iBAAuC;OAAa,CAAA,EACnE,eACC,oBAAC,QAAD;OAAM,WAAU;iBAA0B;OAAmB,CAAA,CAE3D;;KACL,CAAC,aACA,oBAAC,QAAD;MACE,WAAW,GACT,wCACA,aAAa,UACd;gBAEA,aAAa;MACT,CAAA;KAEL;;GAGL,SAAS,MAAM,SAAS,KACvB,oBAAC,OAAD;IAAK,WAAU;cACZ,MAAM,KAAK,MAAM,UAChB,qBAAC,OAAD;KAAmB,WAAU;eAA7B,CACE,qBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,QAAQ,GAAE,IACN;SACP,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,QAAD;OAAM,WAAU;iBAAmB,KAAK;OAAa,CAAA,EACpD,KAAK,eACJ,oBAAC,QAAD;OAAM,WAAU;iBAAkB,KAAK;OAAmB,CAAA,CAExD;QACF;OAVI,KAAK,GAUT,CACN;IACE,CAAA;GAGP;GAGA,aAAa,gBACZ,oBAAC,YAAD;IACE,WAAU;IACV,WAAW,MAAM,YAAY,EAAE,OAAO,MAAM;IAC5C,aAAY;IACZ,OAAO;IACP,CAAA;GAIH,aACC,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,oBAAC,QAAD;KACE,SAAS;KACT,MAAK;KACL,MAAK;KACL,SAAQ;eAEP;KACM,CAAA,EACT,oBAAC,QAAD;KAAQ,SAAS;KAAW,MAAK;KAAK,MAAK;KAAS,SAAQ;eACzD;KACM,CAAA,CACL;;GAEJ;;;AAIV,WAAW,cAAc"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { t as cn } from "./cn-YROP2_ox.js";
|
|
3
|
-
import { t as Button } from "./button-
|
|
4
|
-
import { t as highlightToLines } from "./highlight-to-react-
|
|
3
|
+
import { t as Button } from "./button-CO6-qPax.js";
|
|
4
|
+
import { t as highlightToLines } from "./highlight-to-react-D0Yav4jk.js";
|
|
5
5
|
import { createContext, memo, useContext, useState } from "react";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { highlight } from "sugar-high";
|
|
@@ -107,4 +107,4 @@ function AiCodeBlockCopyButton({ onCopy, onError, timeout = 2e3, children, class
|
|
|
107
107
|
//#endregion
|
|
108
108
|
export { SF_AI_CODE_BLOCK_VARIANTS as i, AiCodeBlockCopyButton as n, SF_AI_CODE_BLOCK_DEFAULT_VARIANTS as r, AiCodeBlock as t };
|
|
109
109
|
|
|
110
|
-
//# sourceMappingURL=ai-code-block-
|
|
110
|
+
//# sourceMappingURL=ai-code-block-CZtoL73R.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-code-block-
|
|
1
|
+
{"version":3,"file":"ai-code-block-CZtoL73R.js","names":[],"sources":["../src/components/ai-code-block/ai-code-block.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps, HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, memo, useContext, useState } from \"react\";\nimport { highlight } from \"sugar-high\";\n\nimport { cn } from \"../../utils/cn\";\nimport { highlightToLines } from \"../../utils/highlight-to-react\";\nimport { Button } from \"../button\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_CODE_BLOCK_VARIANTS = {} as const;\nexport const SF_AI_CODE_BLOCK_DEFAULT_VARIANTS = {} as const;\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\ninterface AiCodeBlockContextValue {\n code: string;\n}\n\nconst AiCodeBlockContext = createContext<AiCodeBlockContextValue>({ code: \"\" });\n\n// ─── AiCodeBlock ─────────────────────────────────────────────────────────────\n\nexport type AiCodeBlockProps = HTMLAttributes<HTMLDivElement> & {\n /** The raw code string to display and make copyable. */\n code: string;\n /** Language identifier for syntax highlighting (display only, no highlighting applied). */\n language?: string;\n /** Show line numbers. @default false */\n showLineNumbers?: boolean;\n /** Extra content rendered in the top-right overlay (e.g. copy button). */\n children?: ReactNode;\n};\n\n/**\n * Displays a code block with monospace formatting and an optional copy button.\n * Does not include a syntax highlighter — avoids the heavy `react-syntax-highlighter`\n * dependency. Wrap with `AiCodeBlockCopyButton` for clipboard support.\n *\n * @example\n * ```tsx\n * <AiCodeBlock code={`const x = 1;`} language=\"ts\">\n * <AiCodeBlockCopyButton />\n * </AiCodeBlock>\n * ```\n */\nexport const AiCodeBlock = memo(\n ({\n code,\n language,\n showLineNumbers = false,\n className,\n children,\n ...props\n }: AiCodeBlockProps) => {\n const highlightedLines = highlightToLines(highlight(code));\n\n return (\n <AiCodeBlockContext.Provider value={{ code }}>\n <div\n className={cn(\n \"relative w-full overflow-hidden rounded-lg border border-sf-line bg-sf-recessed font-mono text-sm text-sf-default\",\n className\n )}\n {...props}\n >\n {language && (\n <div className=\"flex items-center justify-between border-b border-sf-line px-4 py-2\">\n <span className=\"text-sf-subtle text-xs\">{language}</span>\n {children && (\n <div className=\"flex items-center gap-2\">{children}</div>\n )}\n </div>\n )}\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-collapse\">\n <tbody>\n {highlightedLines.map((line, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: line index is stable\n <tr key={i} className=\"leading-6\">\n {showLineNumbers && (\n <td className=\"select-none pr-4 pl-4 text-right text-sf-inactive\">\n {i + 1}\n </td>\n )}\n <td className={cn(\"pr-4\", !showLineNumbers && \"px-4\")}>\n <pre className=\"whitespace-pre\">{line}</pre>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {!language && children && (\n <div className=\"absolute top-2 right-2 flex items-center gap-2\">\n {children}\n </div>\n )}\n </div>\n </AiCodeBlockContext.Provider>\n );\n },\n (prev, next) =>\n prev.code === next.code &&\n prev.language === next.language &&\n prev.showLineNumbers === next.showLineNumbers\n);\n\nAiCodeBlock.displayName = \"AiCodeBlock\";\n\n// ─── AiCodeBlockCopyButton ───────────────────────────────────────────────────\n\nexport type AiCodeBlockCopyButtonProps = ComponentProps<typeof Button> & {\n /** Called after a successful copy. */\n onCopy?: () => void;\n /** Called if the clipboard API is unavailable or errors. */\n onError?: (error: Error) => void;\n /** How long (ms) to show the \"copied\" state. @default 2000 */\n timeout?: number;\n};\n\n/**\n * Copy-to-clipboard button for use inside `AiCodeBlock`.\n * Must be a descendant of `AiCodeBlock` to access the code via context.\n *\n * @example\n * ```tsx\n * <AiCodeBlock code=\"const x = 1;\" language=\"ts\">\n * <AiCodeBlockCopyButton />\n * </AiCodeBlock>\n * ```\n */\nexport function AiCodeBlockCopyButton({\n onCopy,\n onError,\n timeout = 2000,\n children,\n className,\n size = \"sm\",\n variant = \"ghost\",\n ...props\n}: AiCodeBlockCopyButtonProps) {\n const [copied, setCopied] = useState(false);\n const { code } = useContext(AiCodeBlockContext);\n\n const handleCopy = async () => {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.writeText) {\n onError?.(new Error(\"Clipboard API not available\"));\n return;\n }\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n onCopy?.();\n setTimeout(() => setCopied(false), timeout);\n } catch (err) {\n onError?.(err as Error);\n }\n };\n\n return (\n <Button\n className={cn(\"shrink-0\", className)}\n onClick={handleCopy}\n size={size}\n variant={variant}\n {...props}\n >\n {children ?? (copied ? \"Copied\" : \"Copy\")}\n </Button>\n );\n}\n"],"mappings":";;;;;;;;AAYA,IAAa,4BAA4B,EAAE;AAC3C,IAAa,oCAAoC,EAAE;AAQnD,IAAM,qBAAqB,cAAuC,EAAE,MAAM,IAAI,CAAC;;;;;;;;;;;;;AA2B/E,IAAa,cAAc,MACxB,EACC,MACA,UACA,kBAAkB,OAClB,WACA,UACA,GAAG,YACmB;CACtB,MAAM,mBAAmB,iBAAiB,UAAU,KAAK,CAAC;AAE1D,QACE,oBAAC,mBAAmB,UAApB;EAA6B,OAAO,EAAE,MAAM;YAC1C,qBAAC,OAAD;GACE,WAAW,GACT,qHACA,UACD;GACD,GAAI;aALN;IAOG,YACC,qBAAC,OAAD;KAAK,WAAU;eAAf,CACE,oBAAC,QAAD;MAAM,WAAU;gBAA0B;MAAgB,CAAA,EACzD,YACC,oBAAC,OAAD;MAAK,WAAU;MAA2B;MAAe,CAAA,CAEvD;;IAER,oBAAC,OAAD;KAAK,WAAU;eACb,oBAAC,SAAD;MAAO,WAAU;gBACf,oBAAC,SAAD,EAAA,UACG,iBAAiB,KAAK,MAAM,MAE3B,qBAAC,MAAD;OAAY,WAAU;iBAAtB,CACG,mBACC,oBAAC,MAAD;QAAI,WAAU;kBACX,IAAI;QACF,CAAA,EAEP,oBAAC,MAAD;QAAI,WAAW,GAAG,QAAQ,CAAC,mBAAmB,OAAO;kBACnD,oBAAC,OAAD;SAAK,WAAU;mBAAkB;SAAW,CAAA;QACzC,CAAA,CACF;SATI,EASJ,CACL,EACI,CAAA;MACF,CAAA;KACJ,CAAA;IACL,CAAC,YAAY,YACZ,oBAAC,OAAD;KAAK,WAAU;KACZ;KACG,CAAA;IAEJ;;EACsB,CAAA;IAGjC,MAAM,SACL,KAAK,SAAS,KAAK,QACnB,KAAK,aAAa,KAAK,YACvB,KAAK,oBAAoB,KAAK,gBACjC;AAED,YAAY,cAAc;;;;;;;;;;;;AAwB1B,SAAgB,sBAAsB,EACpC,QACA,SACA,UAAU,KACV,UACA,WACA,OAAO,MACP,UAAU,SACV,GAAG,SAC0B;CAC7B,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM;CAC3C,MAAM,EAAE,SAAS,WAAW,mBAAmB;CAE/C,MAAM,aAAa,YAAY;AAC7B,MAAI,OAAO,cAAc,eAAe,CAAC,UAAU,WAAW,WAAW;AACvE,6BAAU,IAAI,MAAM,8BAA8B,CAAC;AACnD;;AAEF,MAAI;AACF,SAAM,UAAU,UAAU,UAAU,KAAK;AACzC,aAAU,KAAK;AACf,aAAU;AACV,oBAAiB,UAAU,MAAM,EAAE,QAAQ;WACpC,KAAK;AACZ,aAAU,IAAa;;;AAI3B,QACE,oBAAC,QAAD;EACE,WAAW,GAAG,YAAY,UAAU;EACpC,SAAS;EACH;EACG;EACT,GAAI;YAEH,aAAa,SAAS,WAAW;EAC3B,CAAA"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { t as cn } from "./cn-YROP2_ox.js";
|
|
3
|
+
import { a as prepare, t as layout } from "./layout-DJHMMap2.js";
|
|
4
|
+
import { t as Button } from "./button-CO6-qPax.js";
|
|
5
|
+
import { forwardRef, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
import { ArrowDownIcon } from "@phosphor-icons/react";
|
|
8
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
9
|
+
//#region src/components/ai-conversation/ai-conversation.tsx
|
|
10
|
+
var SF_AI_CONVERSATION_VARIANTS = {};
|
|
11
|
+
var SF_AI_CONVERSATION_DEFAULT_VARIANTS = {};
|
|
12
|
+
/**
|
|
13
|
+
* Outer scroll container for a conversation. Sticks to the bottom as new
|
|
14
|
+
* messages are added. Pair with `AiConversationContent` and optionally
|
|
15
|
+
* `AiConversationScrollButton`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <AiConversation ref={scrollRef}>
|
|
20
|
+
* <AiConversationContent>
|
|
21
|
+
* {messages.map(m => <AiMessage key={m.id} from={m.role}>...</AiMessage>)}
|
|
22
|
+
* </AiConversationContent>
|
|
23
|
+
* <AiConversationScrollButton scrollRef={scrollRef} />
|
|
24
|
+
* </AiConversation>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
var AiConversation = forwardRef(({ className, children, ...props }, ref) => {
|
|
28
|
+
return /* @__PURE__ */ jsx("div", {
|
|
29
|
+
ref,
|
|
30
|
+
className: cn("relative flex-1 overflow-y-auto", className),
|
|
31
|
+
role: "log",
|
|
32
|
+
"aria-live": "polite",
|
|
33
|
+
"aria-label": "Conversation",
|
|
34
|
+
...props,
|
|
35
|
+
children
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
AiConversation.displayName = "AiConversation";
|
|
39
|
+
/**
|
|
40
|
+
* Inner content wrapper with padding. Place message components inside here.
|
|
41
|
+
*/
|
|
42
|
+
function AiConversationContent({ className, children, ...props }) {
|
|
43
|
+
return /* @__PURE__ */ jsx("div", {
|
|
44
|
+
className: cn("flex flex-col gap-0 overscroll-y-none p-4", className),
|
|
45
|
+
...props,
|
|
46
|
+
children
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Floating "scroll to bottom" button. Appears when the user has scrolled up
|
|
51
|
+
* more than `threshold` pixels from the bottom.
|
|
52
|
+
*/
|
|
53
|
+
function AiConversationScrollButton({ scrollRef, threshold = 100, className, onClick, ...props }) {
|
|
54
|
+
const [show, setShow] = useState(false);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const el = scrollRef?.current;
|
|
57
|
+
if (!el) return;
|
|
58
|
+
const check = () => {
|
|
59
|
+
setShow(el.scrollHeight - el.scrollTop - el.clientHeight > threshold);
|
|
60
|
+
};
|
|
61
|
+
check();
|
|
62
|
+
el.addEventListener("scroll", check, { passive: true });
|
|
63
|
+
return () => el.removeEventListener("scroll", check);
|
|
64
|
+
}, [scrollRef, threshold]);
|
|
65
|
+
const handleClick = useCallback((e) => {
|
|
66
|
+
scrollRef?.current?.scrollTo({
|
|
67
|
+
top: scrollRef.current.scrollHeight,
|
|
68
|
+
behavior: "smooth"
|
|
69
|
+
});
|
|
70
|
+
onClick?.(e);
|
|
71
|
+
}, [scrollRef, onClick]);
|
|
72
|
+
if (scrollRef && !show) return null;
|
|
73
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
74
|
+
"aria-label": "Scroll to bottom",
|
|
75
|
+
className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full shadow-md", className),
|
|
76
|
+
onClick: handleClick,
|
|
77
|
+
size: "sm",
|
|
78
|
+
variant: "secondary",
|
|
79
|
+
...props,
|
|
80
|
+
children: /* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-4" })
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
84
|
+
function useConversationVirtualizer({ scrollRef, items, estimateSize = 80, overscan = 8, stickThreshold = 40 }) {
|
|
85
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
86
|
+
const isAtBottomRef = useRef(true);
|
|
87
|
+
const prevCountRef = useRef(items.length);
|
|
88
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
89
|
+
const [fontsReady, setFontsReady] = useState(() => {
|
|
90
|
+
if (typeof document === "undefined") return true;
|
|
91
|
+
return document.fonts?.status === "loaded";
|
|
92
|
+
});
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (fontsReady) return;
|
|
95
|
+
if (typeof document === "undefined" || !document.fonts) {
|
|
96
|
+
setFontsReady(true);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let cancelled = false;
|
|
100
|
+
document.fonts.ready.then(() => {
|
|
101
|
+
if (!cancelled) setFontsReady(true);
|
|
102
|
+
});
|
|
103
|
+
return () => {
|
|
104
|
+
cancelled = true;
|
|
105
|
+
};
|
|
106
|
+
}, [fontsReady]);
|
|
107
|
+
useIsomorphicLayoutEffect(() => {
|
|
108
|
+
const el = scrollRef.current;
|
|
109
|
+
if (!el) return;
|
|
110
|
+
const updateWidth = () => {
|
|
111
|
+
const style = window.getComputedStyle(el);
|
|
112
|
+
const padLeft = parseFloat(style.paddingLeft) || 0;
|
|
113
|
+
const padRight = parseFloat(style.paddingRight) || 0;
|
|
114
|
+
setContainerWidth(el.clientWidth - padLeft - padRight);
|
|
115
|
+
};
|
|
116
|
+
updateWidth();
|
|
117
|
+
const observer = new ResizeObserver(updateWidth);
|
|
118
|
+
observer.observe(el);
|
|
119
|
+
return () => observer.disconnect();
|
|
120
|
+
}, [scrollRef]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const el = scrollRef.current;
|
|
123
|
+
if (!el) return;
|
|
124
|
+
const check = () => {
|
|
125
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= stickThreshold;
|
|
126
|
+
isAtBottomRef.current = atBottom;
|
|
127
|
+
setIsAtBottom(atBottom);
|
|
128
|
+
};
|
|
129
|
+
check();
|
|
130
|
+
el.addEventListener("scroll", check, { passive: true });
|
|
131
|
+
return () => el.removeEventListener("scroll", check);
|
|
132
|
+
}, [scrollRef, stickThreshold]);
|
|
133
|
+
const prevTotalSizeRef = useRef(0);
|
|
134
|
+
const virtualizer = useVirtualizer({
|
|
135
|
+
count: items.length,
|
|
136
|
+
getScrollElement: () => scrollRef.current,
|
|
137
|
+
estimateSize: (index) => {
|
|
138
|
+
const item = items[index];
|
|
139
|
+
if (!item?.measure || containerWidth <= 0) return estimateSize;
|
|
140
|
+
const { text, font = "14px sans-serif", lineHeight = 20, padding = 0, widthFraction = 1, horizontalPadding = 0, gapAfter = 0, whiteSpace = "normal", wordBreak = "normal" } = item.measure;
|
|
141
|
+
const contentWidth = Math.max(40, Math.floor(containerWidth * widthFraction) - horizontalPadding);
|
|
142
|
+
try {
|
|
143
|
+
const { height } = layout(prepare(text, font, {
|
|
144
|
+
whiteSpace,
|
|
145
|
+
wordBreak
|
|
146
|
+
}), contentWidth, lineHeight);
|
|
147
|
+
return height + padding + gapAfter;
|
|
148
|
+
} catch {
|
|
149
|
+
return estimateSize;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
overscan,
|
|
153
|
+
getItemKey: (index) => items[index]?.key ?? index
|
|
154
|
+
});
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (containerWidth > 0) virtualizer.measure();
|
|
157
|
+
}, [
|
|
158
|
+
containerWidth,
|
|
159
|
+
items,
|
|
160
|
+
virtualizer,
|
|
161
|
+
fontsReady
|
|
162
|
+
]);
|
|
163
|
+
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = () => isAtBottomRef.current;
|
|
164
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
165
|
+
const totalSize = virtualizer.getTotalSize();
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
const count = items.length;
|
|
168
|
+
if (count > prevCountRef.current && isAtBottomRef.current) requestAnimationFrame(() => {
|
|
169
|
+
virtualizer.scrollToIndex(count - 1, {
|
|
170
|
+
align: "end",
|
|
171
|
+
behavior: "smooth"
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
prevCountRef.current = count;
|
|
175
|
+
}, [items.length, virtualizer]);
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
const prev = prevTotalSizeRef.current;
|
|
178
|
+
prevTotalSizeRef.current = totalSize;
|
|
179
|
+
if (!isAtBottomRef.current || totalSize <= prev) return;
|
|
180
|
+
const el = scrollRef.current;
|
|
181
|
+
if (!el) return;
|
|
182
|
+
const dist = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
183
|
+
if (dist > 1 && dist < 300) el.scrollTop = el.scrollHeight - el.clientHeight;
|
|
184
|
+
}, [totalSize, scrollRef]);
|
|
185
|
+
const scrollToBottom = useCallback((behavior = "smooth") => {
|
|
186
|
+
if (items.length === 0) return;
|
|
187
|
+
virtualizer.scrollToIndex(items.length - 1, {
|
|
188
|
+
align: "end",
|
|
189
|
+
behavior
|
|
190
|
+
});
|
|
191
|
+
}, [virtualizer, items.length]);
|
|
192
|
+
const measureRef = useCallback((node) => {
|
|
193
|
+
if (node) virtualizer.measureElement(node);
|
|
194
|
+
}, [virtualizer]);
|
|
195
|
+
return {
|
|
196
|
+
virtualizer,
|
|
197
|
+
virtualItems,
|
|
198
|
+
totalSize,
|
|
199
|
+
isAtBottom,
|
|
200
|
+
scrollToBottom,
|
|
201
|
+
measureRef,
|
|
202
|
+
getMeasureRef: useCallback((index) => {
|
|
203
|
+
if (items[index]?.measure) return null;
|
|
204
|
+
return measureRef;
|
|
205
|
+
}, [items, measureRef])
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/components/ai-conversation/measurement-constants.ts
|
|
210
|
+
/**
|
|
211
|
+
* Shared pretext measurement constants for `useConversationVirtualizer`.
|
|
212
|
+
*
|
|
213
|
+
* These match the rendered styling of `<AiMessage>` + `<AiMessageContent>`
|
|
214
|
+
* (text-sm = 14px / line-height 20px, user bubble `max-w-[70%]` with `px-4 py-3`).
|
|
215
|
+
*
|
|
216
|
+
* Anywhere that builds `ConversationItem.measure` for plain-text AI chat messages
|
|
217
|
+
* should import from here so calibration stays in one place. If production CSS
|
|
218
|
+
* for `<AiMessage>` ever changes (font size, padding, bubble width), update these
|
|
219
|
+
* constants — pretext predictions will then automatically follow.
|
|
220
|
+
*/
|
|
221
|
+
/** CSS `font` shorthand matching `text-sm` rendering. */
|
|
222
|
+
var SF_AI_MEASURE_FONT = "14px \"Inter\", ui-sans-serif, system-ui, sans-serif";
|
|
223
|
+
/** Line height in pixels, matching `text-sm`. */
|
|
224
|
+
var SF_AI_MEASURE_LINE_HEIGHT = 20;
|
|
225
|
+
/** User bubbles: `max-w-[70%]`. */
|
|
226
|
+
var SF_AI_USER_WIDTH_FRACTION = .7;
|
|
227
|
+
/** User bubbles: `px-4` → 16px × 2 horizontal padding. */
|
|
228
|
+
var SF_AI_USER_HORIZONTAL_PADDING = 32;
|
|
229
|
+
/** User bubbles: `py-3` → 12px × 2 vertical padding. */
|
|
230
|
+
var SF_AI_USER_VERTICAL_PADDING = 24;
|
|
231
|
+
/** Assistant text: full width column, no bubble. */
|
|
232
|
+
var SF_AI_ASSISTANT_WIDTH_FRACTION = 1;
|
|
233
|
+
/** Assistant text: no horizontal padding inside the bubble. */
|
|
234
|
+
var SF_AI_ASSISTANT_HORIZONTAL_PADDING = 0;
|
|
235
|
+
/** Assistant text: no vertical padding inside the bubble. */
|
|
236
|
+
var SF_AI_ASSISTANT_VERTICAL_PADDING = 0;
|
|
237
|
+
/** Default gap between conversation items. */
|
|
238
|
+
var SF_AI_DEFAULT_MESSAGE_GAP = 12;
|
|
239
|
+
//#endregion
|
|
240
|
+
export { SF_AI_MEASURE_FONT as a, SF_AI_USER_VERTICAL_PADDING as c, AiConversationContent as d, AiConversationScrollButton as f, useConversationVirtualizer as h, SF_AI_DEFAULT_MESSAGE_GAP as i, SF_AI_USER_WIDTH_FRACTION as l, SF_AI_CONVERSATION_VARIANTS as m, SF_AI_ASSISTANT_VERTICAL_PADDING as n, SF_AI_MEASURE_LINE_HEIGHT as o, SF_AI_CONVERSATION_DEFAULT_VARIANTS as p, SF_AI_ASSISTANT_WIDTH_FRACTION as r, SF_AI_USER_HORIZONTAL_PADDING as s, SF_AI_ASSISTANT_HORIZONTAL_PADDING as t, AiConversation as u };
|
|
241
|
+
|
|
242
|
+
//# sourceMappingURL=ai-conversation-Cc7WlaBg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-conversation-Cc7WlaBg.js","names":[],"sources":["../src/components/ai-conversation/ai-conversation.tsx","../src/components/ai-conversation/measurement-constants.ts"],"sourcesContent":["\"use client\";\n\nimport { ArrowDownIcon } from \"@phosphor-icons/react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport type { Virtualizer, VirtualItem } from \"@tanstack/react-virtual\";\nimport { layout, prepare } from \"@chenglou/pretext\";\nimport type { ComponentProps, ReactNode, RefObject } from \"react\";\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"../../utils/cn\";\n\nimport { Button } from \"../button\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_CONVERSATION_VARIANTS = {} as const;\nexport const SF_AI_CONVERSATION_DEFAULT_VARIANTS = {} as const;\n\n// ─── AiConversation ──────────────────────────────────────────────────────────\n\nexport type AiConversationProps = ComponentProps<\"div\">;\n\n/**\n * Outer scroll container for a conversation. Sticks to the bottom as new\n * messages are added. Pair with `AiConversationContent` and optionally\n * `AiConversationScrollButton`.\n *\n * @example\n * ```tsx\n * <AiConversation ref={scrollRef}>\n * <AiConversationContent>\n * {messages.map(m => <AiMessage key={m.id} from={m.role}>...</AiMessage>)}\n * </AiConversationContent>\n * <AiConversationScrollButton scrollRef={scrollRef} />\n * </AiConversation>\n * ```\n */\nexport const AiConversation = forwardRef<HTMLDivElement, AiConversationProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn(\"relative flex-1 overflow-y-auto\", className)}\n role=\"log\"\n aria-live=\"polite\"\n aria-label=\"Conversation\"\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nAiConversation.displayName = \"AiConversation\";\n\n// ─── AiConversationContent ───────────────────────────────────────────────────\n\nexport type AiConversationContentProps = ComponentProps<\"div\">;\n\n/**\n * Inner content wrapper with padding. Place message components inside here.\n */\nexport function AiConversationContent({\n className,\n children,\n ...props\n}: AiConversationContentProps) {\n return (\n <div\n className={cn(\"flex flex-col gap-0 overscroll-y-none p-4\", className)}\n {...props}\n >\n {children}\n </div>\n );\n}\n\n// ─── AiConversationScrollButton ──────────────────────────────────────────────\n\nexport type AiConversationScrollButtonProps = ComponentProps<typeof Button> & {\n /**\n * Ref to the scroll container (the `AiConversation` element).\n * When omitted, the button is always visible.\n */\n scrollRef?: RefObject<HTMLElement | null>;\n /** Scroll threshold in pixels from bottom to show the button. @default 100 */\n threshold?: number;\n};\n\n/**\n * Floating \"scroll to bottom\" button. Appears when the user has scrolled up\n * more than `threshold` pixels from the bottom.\n */\nexport function AiConversationScrollButton({\n scrollRef,\n threshold = 100,\n className,\n onClick,\n ...props\n}: AiConversationScrollButtonProps) {\n const [show, setShow] = useState(false);\n\n useEffect(() => {\n const el = scrollRef?.current;\n if (!el) return;\n\n const check = () => {\n const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;\n setShow(distFromBottom > threshold);\n };\n\n check();\n el.addEventListener(\"scroll\", check, { passive: true });\n return () => el.removeEventListener(\"scroll\", check);\n }, [scrollRef, threshold]);\n\n const handleClick = useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n scrollRef?.current?.scrollTo({\n top: scrollRef.current.scrollHeight,\n behavior: \"smooth\",\n });\n onClick?.(e);\n },\n [scrollRef, onClick]\n );\n\n if (scrollRef && !show) return null;\n\n return (\n <Button\n aria-label=\"Scroll to bottom\"\n className={cn(\n \"absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full shadow-md\",\n className\n )}\n onClick={handleClick}\n size=\"sm\"\n variant=\"secondary\"\n {...props}\n >\n <ArrowDownIcon className=\"size-4\" />\n </Button>\n );\n}\n\n// ─── Virtualized Conversation ────────────────────────────────────────────────\n\n/** Measurement spec for predictive height calculation using pretext. */\nexport interface ConversationItemMeasureSpec {\n /** Text content to measure */\n text: string;\n /** CSS font shorthand (e.g. \"14px Inter, sans-serif\"). Auto-detected if omitted. */\n font?: string;\n /** Line height in pixels. Auto-detected if omitted. */\n lineHeight?: number;\n /** Additional vertical padding (top + bottom) in pixels @default 0 */\n padding?: number;\n /**\n * Fraction of column width this item occupies (0-1). Used to compute the\n * actual content width for items that are constrained (e.g. user message\n * bubbles at max-w-[70%]). @default 1\n */\n widthFraction?: number;\n /**\n * Horizontal padding (left + right) inside the content box, subtracted from\n * the available width before line wrapping. @default 0\n */\n horizontalPadding?: number;\n /**\n * Margin below this item (gap to next item). @default 0\n */\n gapAfter?: number;\n /**\n * White-space handling — must match the rendered CSS. Use `\"pre-wrap\"` when\n * the rendered content preserves explicit newlines (e.g. plain text inside\n * a <pre> or `whitespace-pre-wrap` div). @default \"normal\"\n */\n whiteSpace?: \"normal\" | \"pre-wrap\";\n /**\n * Word-break handling — match CSS `word-break`. Use `\"keep-all\"` for CJK\n * text that should not break mid-word. @default \"normal\"\n */\n wordBreak?: \"normal\" | \"keep-all\";\n}\n\n/** A single item in the virtualized conversation list. */\nexport interface ConversationItem {\n /** Unique key for this item. */\n key: string;\n /**\n * Render the item content. The virtualizer handles measurement and\n * positioning on the wrapper — just return the content.\n */\n render: () => ReactNode;\n /**\n * Optional predictive height measurement. When provided, the virtualizer\n * uses pretext to compute exact height without DOM measurement, eliminating\n * layout shift during streaming. Falls back to estimateSize + DOM measurement\n * when omitted.\n *\n * **Attach this only when rendering plain text** (no markdown). If your\n * `render` output uses `<AiResponse>` or any rich-content component whose\n * blocks have varying typography (headings, code, lists, blockquotes), omit\n * `measure` to let the virtualizer use DOM measurement instead. Pretext\n * predicts heights from a single (font, line-height) pair and cannot\n * accurately model multi-block markdown.\n *\n * Plain-text + `measure` gives jitter-free virtualization (no estimate-then-\n * adjust feedback loop). Rich content + DOM measurement is rendering-correct\n * but slightly less smooth on first paint.\n *\n * For streaming items, update `measure.text` as text grows so the virtualizer\n * can recalculate height per tick (rebuild the `items` array with the latest\n * text on each token arrival).\n */\n measure?: ConversationItemMeasureSpec;\n}\n\nexport interface UseConversationVirtualizerOptions {\n /** Ref to the scroll container element. */\n scrollRef: RefObject<HTMLElement | null>;\n /** The conversation items to virtualize. */\n items: ConversationItem[];\n /**\n * Estimated height of a single item in pixels.\n * Used before measurement. Doesn't need to be exact.\n * @default 80\n */\n estimateSize?: number;\n /**\n * Number of items to render beyond the visible area.\n * @default 8\n */\n overscan?: number;\n /**\n * Pixel distance from bottom to consider \"at bottom\" for stick-to-bottom.\n * @default 40\n */\n stickThreshold?: number;\n}\n\nexport interface UseConversationVirtualizerReturn {\n /** The TanStack virtualizer instance. */\n virtualizer: Virtualizer<HTMLElement, Element>;\n /** The virtual items to render. */\n virtualItems: VirtualItem[];\n /** Total measured/estimated height of all items. */\n totalSize: number;\n /** Whether the user is currently at the bottom of the conversation. */\n isAtBottom: boolean;\n /** Smoothly scroll to the bottom. */\n scrollToBottom: (behavior?: ScrollBehavior) => void;\n /**\n * Get the measurement ref for a virtual item by index.\n * @deprecated Use `getMeasureRef(index)` instead to respect predictive heights.\n */\n measureRef: (node: HTMLElement | null) => void;\n /**\n * Get the measurement ref for a specific item index.\n * Returns `null` for items with predictive height (no DOM measurement needed),\n * or the measurement callback for legacy items.\n */\n getMeasureRef: (index: number) => ((node: HTMLElement | null) => void) | null;\n}\n\n/**\n * `useConversationVirtualizer` — virtualizes a conversation list with\n * dynamic item heights and automatic stick-to-bottom behavior.\n *\n * Built on `@tanstack/react-virtual`. Items are measured dynamically via\n * `ResizeObserver`, so tool calls expanding, subagents collapsing, and\n * streaming text growing all work correctly.\n *\n * **Stick-to-bottom**: When the user is scrolled to (or near) the bottom,\n * new items and item resizes automatically keep the viewport pinned to the\n * bottom. When the user scrolls up, stick-to-bottom disengages until they\n * scroll back down.\n *\n * @example\n * ```tsx\n * const { virtualItems, totalSize, measureRef, isAtBottom, scrollToBottom } =\n * useConversationVirtualizer({ scrollRef, items });\n *\n * return (\n * <AiConversation ref={scrollRef}>\n * <div style={{ height: totalSize, position: \"relative\" }}>\n * {virtualItems.map((vi) => {\n * const item = items[vi.index];\n * return (\n * <div key={item.key} ref={measureRef} data-index={vi.index}\n * style={{ position: \"absolute\", top: vi.start, width: \"100%\" }}>\n * {item.render()}\n * </div>\n * );\n * })}\n * </div>\n * {!isAtBottom && <button onClick={() => scrollToBottom()}>↓</button>}\n * </AiConversation>\n * );\n * ```\n */\nconst isSSR = typeof window === \"undefined\";\nconst useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect;\n\nexport function useConversationVirtualizer({\n scrollRef,\n items,\n estimateSize = 80,\n overscan = 8,\n stickThreshold = 40,\n}: UseConversationVirtualizerOptions): UseConversationVirtualizerReturn {\n const [isAtBottom, setIsAtBottom] = useState(true);\n const isAtBottomRef = useRef(true);\n const prevCountRef = useRef(items.length);\n\n // Track container width for predictive height calculation\n const [containerWidth, setContainerWidth] = useState(0);\n\n // Track whether webfonts are ready. Pretext measures via canvas which uses\n // the browser font engine — if Inter (or any custom font) hasn't loaded\n // yet, the canvas falls back to system-ui and predictions diverge from the\n // rendered DOM. Once fonts.ready resolves we re-trigger measurement.\n const [fontsReady, setFontsReady] = useState(() => {\n if (typeof document === \"undefined\") return true;\n // `document.fonts.status === \"loaded\"` means all current FontFaces are\n // loaded. If still \"loading\" we wait for fonts.ready below.\n return document.fonts?.status === \"loaded\";\n });\n\n useEffect(() => {\n if (fontsReady) return;\n if (typeof document === \"undefined\" || !document.fonts) {\n setFontsReady(true);\n return;\n }\n let cancelled = false;\n document.fonts.ready.then(() => {\n if (!cancelled) setFontsReady(true);\n });\n return () => {\n cancelled = true;\n };\n }, [fontsReady]);\n\n useIsomorphicLayoutEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n const updateWidth = () => {\n // clientWidth excludes scrollbars but includes padding.\n // Subtract horizontal padding to get the actual content width.\n const style = window.getComputedStyle(el);\n const padLeft = parseFloat(style.paddingLeft) || 0;\n const padRight = parseFloat(style.paddingRight) || 0;\n setContainerWidth(el.clientWidth - padLeft - padRight);\n };\n\n updateWidth();\n\n const observer = new ResizeObserver(updateWidth);\n observer.observe(el);\n\n return () => observer.disconnect();\n }, [scrollRef]);\n\n // Track whether user is at the bottom\n useEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n const check = () => {\n const dist = el.scrollHeight - el.scrollTop - el.clientHeight;\n const atBottom = dist <= stickThreshold;\n isAtBottomRef.current = atBottom;\n setIsAtBottom(atBottom);\n };\n\n check();\n el.addEventListener(\"scroll\", check, { passive: true });\n return () => el.removeEventListener(\"scroll\", check);\n }, [scrollRef, stickThreshold]);\n\n // Track the previous totalSize so we can detect growth from item resizes\n const prevTotalSizeRef = useRef(0);\n\n const virtualizer = useVirtualizer({\n count: items.length,\n getScrollElement: () => scrollRef.current as HTMLElement | null,\n estimateSize: (index) => {\n const item = items[index];\n if (!item?.measure || containerWidth <= 0) {\n return estimateSize;\n }\n const {\n text,\n font = \"14px sans-serif\",\n lineHeight = 20,\n padding = 0,\n widthFraction = 1,\n horizontalPadding = 0,\n gapAfter = 0,\n whiteSpace = \"normal\",\n wordBreak = \"normal\",\n } = item.measure;\n\n // Apply the same width constraints as the rendered message bubble.\n const contentWidth = Math.max(\n 40,\n Math.floor(containerWidth * widthFraction) - horizontalPadding\n );\n\n try {\n const prepared = prepare(text, font, { whiteSpace, wordBreak });\n const { height } = layout(prepared, contentWidth, lineHeight);\n return height + padding + gapAfter;\n } catch {\n return estimateSize;\n }\n },\n overscan,\n getItemKey: (index) => items[index]?.key ?? index,\n });\n\n // When container width, item measurement specs, or font-readiness change,\n // force TanStack to recompute sizes through `estimateSize`. For predictive\n // rows this calls pretext again and still skips DOM measurement.\n useEffect(() => {\n if (containerWidth > 0) {\n virtualizer.measure();\n }\n }, [containerWidth, items, virtualizer, fontsReady]);\n\n // When items resize (streaming text, collapsible expand) and we're at\n // bottom, adjust scroll so the bottom stays pinned.\n virtualizer.shouldAdjustScrollPositionOnItemSizeChange = () =>\n isAtBottomRef.current;\n\n const virtualItems = virtualizer.getVirtualItems();\n const totalSize = virtualizer.getTotalSize();\n\n // When new items are added and we're at bottom, scroll to the end\n useEffect(() => {\n const count = items.length;\n if (count > prevCountRef.current && isAtBottomRef.current) {\n requestAnimationFrame(() => {\n virtualizer.scrollToIndex(count - 1, {\n align: \"end\",\n behavior: \"smooth\",\n });\n });\n }\n prevCountRef.current = count;\n }, [items.length, virtualizer]);\n\n // When totalSize grows (item content expanded, streaming text grew)\n // and we're stuck to bottom, smoothly follow by scrolling down.\n // This complements shouldAdjustScrollPositionOnItemSizeChange which\n // handles the instantaneous adjustment — this handles the smooth follow.\n useEffect(() => {\n const prev = prevTotalSizeRef.current;\n prevTotalSizeRef.current = totalSize;\n\n if (!isAtBottomRef.current || totalSize <= prev) return;\n\n const el = scrollRef.current;\n if (!el) return;\n\n // Only nudge if we're slightly off — the shouldAdjust callback handles\n // most cases, but measurement batching can leave a small gap.\n const dist = el.scrollHeight - el.scrollTop - el.clientHeight;\n if (dist > 1 && dist < 300) {\n el.scrollTop = el.scrollHeight - el.clientHeight;\n }\n }, [totalSize, scrollRef]);\n\n const scrollToBottom = useCallback(\n (behavior: ScrollBehavior = \"smooth\") => {\n if (items.length === 0) return;\n virtualizer.scrollToIndex(items.length - 1, { align: \"end\", behavior });\n },\n [virtualizer, items.length]\n );\n\n const measureRef = useCallback(\n (node: HTMLElement | null) => {\n if (node) virtualizer.measureElement(node);\n },\n [virtualizer]\n );\n\n // For items with predictive height, return null so TanStack doesn't remeasure.\n // For legacy items without measure spec, return the measurement callback.\n const getMeasureRef = useCallback(\n (index: number): ((node: HTMLElement | null) => void) | null => {\n const item = items[index];\n if (item?.measure) {\n return null; // Predicted height — no DOM measurement needed\n }\n return measureRef;\n },\n [items, measureRef]\n );\n\n return {\n virtualizer,\n virtualItems,\n totalSize,\n isAtBottom,\n scrollToBottom,\n measureRef,\n getMeasureRef,\n };\n}\n","/**\n * Shared pretext measurement constants for `useConversationVirtualizer`.\n *\n * These match the rendered styling of `<AiMessage>` + `<AiMessageContent>`\n * (text-sm = 14px / line-height 20px, user bubble `max-w-[70%]` with `px-4 py-3`).\n *\n * Anywhere that builds `ConversationItem.measure` for plain-text AI chat messages\n * should import from here so calibration stays in one place. If production CSS\n * for `<AiMessage>` ever changes (font size, padding, bubble width), update these\n * constants — pretext predictions will then automatically follow.\n */\n\n/** CSS `font` shorthand matching `text-sm` rendering. */\nexport const SF_AI_MEASURE_FONT =\n '14px \"Inter\", ui-sans-serif, system-ui, sans-serif';\n\n/** Line height in pixels, matching `text-sm`. */\nexport const SF_AI_MEASURE_LINE_HEIGHT = 20;\n\n/** User bubbles: `max-w-[70%]`. */\nexport const SF_AI_USER_WIDTH_FRACTION = 0.7;\n\n/** User bubbles: `px-4` → 16px × 2 horizontal padding. */\nexport const SF_AI_USER_HORIZONTAL_PADDING = 32;\n\n/** User bubbles: `py-3` → 12px × 2 vertical padding. */\nexport const SF_AI_USER_VERTICAL_PADDING = 24;\n\n/** Assistant text: full width column, no bubble. */\nexport const SF_AI_ASSISTANT_WIDTH_FRACTION = 1;\n\n/** Assistant text: no horizontal padding inside the bubble. */\nexport const SF_AI_ASSISTANT_HORIZONTAL_PADDING = 0;\n\n/** Assistant text: no vertical padding inside the bubble. */\nexport const SF_AI_ASSISTANT_VERTICAL_PADDING = 0;\n\n/** Default gap between conversation items. */\nexport const SF_AI_DEFAULT_MESSAGE_GAP = 12;\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { t as cn } from "./cn-YROP2_ox.js";
|
|
3
|
+
import { t as Text } from "./text-Cqryz7rk.js";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
import { ArrowsClockwiseIcon, InfoIcon, WarningCircleIcon } from "@phosphor-icons/react";
|
|
5
6
|
//#region src/components/ai-info-banner/ai-info-banner.tsx
|
|
@@ -63,8 +64,11 @@ function AiInfoBanner({ level = "info", icon, children, className, ...props }) {
|
|
|
63
64
|
className: cn("flex items-center justify-center gap-2 border-y px-4 py-1.5", config.borderClassName, className),
|
|
64
65
|
role: level === "error" ? "alert" : "status",
|
|
65
66
|
...props,
|
|
66
|
-
children: [/* @__PURE__ */ jsx(IconComponent, { className: cn("size-3.5 shrink-0", config.iconClassName) }), /* @__PURE__ */ jsx(
|
|
67
|
-
|
|
67
|
+
children: [/* @__PURE__ */ jsx(IconComponent, { className: cn("size-3.5 shrink-0", config.iconClassName) }), /* @__PURE__ */ jsx(Text, {
|
|
68
|
+
size: "xs",
|
|
69
|
+
variant: "secondary",
|
|
70
|
+
wrap: "balance",
|
|
71
|
+
as: "span",
|
|
68
72
|
children
|
|
69
73
|
})]
|
|
70
74
|
});
|
|
@@ -73,4 +77,4 @@ AiInfoBanner.displayName = "AiInfoBanner";
|
|
|
73
77
|
//#endregion
|
|
74
78
|
export { SF_AI_INFO_BANNER_DEFAULT_VARIANTS as n, SF_AI_INFO_BANNER_VARIANTS as r, AiInfoBanner as t };
|
|
75
79
|
|
|
76
|
-
//# sourceMappingURL=ai-info-banner-
|
|
80
|
+
//# sourceMappingURL=ai-info-banner-C7EWPBj7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-info-banner-C7EWPBj7.js","names":[],"sources":["../src/components/ai-info-banner/ai-info-banner.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n InfoIcon,\n WarningCircleIcon,\n ArrowsClockwiseIcon,\n} from \"@phosphor-icons/react\";\nimport type { ElementType, HTMLAttributes, ReactNode } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport { Text } from \"../text\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_INFO_BANNER_VARIANTS = {\n level: {\n info: { classes: \"\", description: \"Informational notice\" },\n error: { classes: \"\", description: \"Error notice\" },\n change: {\n classes: \"\",\n description: \"State change notice (model/mode switch)\",\n },\n },\n} as const;\n\nexport const SF_AI_INFO_BANNER_DEFAULT_VARIANTS = {\n level: \"info\",\n} as const;\n\nexport type SFAiInfoBannerLevel = keyof typeof SF_AI_INFO_BANNER_VARIANTS.level;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AiInfoBannerProps = HTMLAttributes<HTMLDivElement> & {\n /** Banner level controls icon and color treatment. */\n level?: SFAiInfoBannerLevel;\n /** Custom icon override. */\n icon?: ElementType;\n /** Content to display. */\n children: ReactNode;\n};\n\n// ─── Level config ─────────────────────────────────────────────────────────────\n\nconst LEVEL_CONFIG: Record<\n SFAiInfoBannerLevel,\n { icon: ElementType; iconClassName: string; borderClassName: string }\n> = {\n info: {\n icon: InfoIcon,\n iconClassName: \"text-sf-info\",\n borderClassName: \"border-sf-info/20\",\n },\n error: {\n icon: WarningCircleIcon,\n iconClassName: \"text-sf-danger\",\n borderClassName: \"border-sf-danger/20\",\n },\n change: {\n icon: ArrowsClockwiseIcon,\n iconClassName: \"text-sf-subtle\",\n borderClassName: \"border-sf-line\",\n },\n};\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * Inline conversation banner for system notices. Renders as a compact,\n * horizontally centered divider-like notice rather than a chat bubble.\n *\n * Maps to harness events: `error`, `info`, `mode_changed`, `model_changed`.\n *\n * @example\n * ```tsx\n * <AiInfoBanner level=\"error\">\n * Connection lost. Retrying in 3s…\n * </AiInfoBanner>\n *\n * <AiInfoBanner level=\"change\">\n * Switched to claude-opus-4-6\n * </AiInfoBanner>\n *\n * <AiInfoBanner level=\"info\">\n * Follow-up queued — will send after current response\n * </AiInfoBanner>\n * ```\n */\nexport function AiInfoBanner({\n level = \"info\",\n icon,\n children,\n className,\n ...props\n}: AiInfoBannerProps) {\n const config = LEVEL_CONFIG[level];\n const IconComponent = icon ?? config.icon;\n\n return (\n <div\n className={cn(\n \"flex items-center justify-center gap-2 border-y px-4 py-1.5\",\n config.borderClassName,\n className\n )}\n role={level === \"error\" ? \"alert\" : \"status\"}\n {...props}\n >\n <IconComponent\n className={cn(\"size-3.5 shrink-0\", config.iconClassName)}\n />\n <Text size=\"xs\" variant=\"secondary\" wrap=\"balance\" as=\"span\">\n {children}\n </Text>\n </div>\n );\n}\n\nAiInfoBanner.displayName = \"AiInfoBanner\";\n"],"mappings":";;;;;;AAcA,IAAa,6BAA6B,EACxC,OAAO;CACL,MAAM;EAAE,SAAS;EAAI,aAAa;EAAwB;CAC1D,OAAO;EAAE,SAAS;EAAI,aAAa;EAAgB;CACnD,QAAQ;EACN,SAAS;EACT,aAAa;EACd;CACF,EACF;AAED,IAAa,qCAAqC,EAChD,OAAO,QACR;AAiBD,IAAM,eAGF;CACF,MAAM;EACJ,MAAM;EACN,eAAe;EACf,iBAAiB;EAClB;CACD,OAAO;EACL,MAAM;EACN,eAAe;EACf,iBAAiB;EAClB;CACD,QAAQ;EACN,MAAM;EACN,eAAe;EACf,iBAAiB;EAClB;CACF;;;;;;;;;;;;;;;;;;;;;;AAyBD,SAAgB,aAAa,EAC3B,QAAQ,QACR,MACA,UACA,WACA,GAAG,SACiB;CACpB,MAAM,SAAS,aAAa;CAC5B,MAAM,gBAAgB,QAAQ,OAAO;AAErC,QACE,qBAAC,OAAD;EACE,WAAW,GACT,+DACA,OAAO,iBACP,UACD;EACD,MAAM,UAAU,UAAU,UAAU;EACpC,GAAI;YAPN,CASE,oBAAC,eAAD,EACE,WAAW,GAAG,qBAAqB,OAAO,cAAc,EACxD,CAAA,EACF,oBAAC,MAAD;GAAM,MAAK;GAAK,SAAQ;GAAY,MAAK;GAAU,IAAG;GACnD;GACI,CAAA,CACH;;;AAIV,aAAa,cAAc"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { t as cn } from "./cn-YROP2_ox.js";
|
|
3
|
-
import { t as
|
|
4
|
-
import { t as
|
|
3
|
+
import { t as Text } from "./text-Cqryz7rk.js";
|
|
4
|
+
import { t as Tooltip } from "./tooltip-g9lFsvcT.js";
|
|
5
|
+
import { t as Button } from "./button-CO6-qPax.js";
|
|
5
6
|
import { createContext, memo, useCallback, useContext, useEffect, useState } from "react";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
import { CaretLeftIcon, CaretRightIcon, FileIcon, FileTextIcon, ImageIcon, XIcon } from "@phosphor-icons/react";
|
|
8
9
|
//#region src/components/ai-message/ai-message.tsx
|
|
10
|
+
var AiMessageContext = createContext(null);
|
|
9
11
|
var SF_AI_MESSAGE_VARIANTS = { from: {
|
|
10
12
|
user: {
|
|
11
13
|
classes: "ml-auto w-full max-w-[70%] justify-end is-user",
|
|
@@ -38,19 +40,36 @@ var SF_AI_MESSAGE_DEFAULT_VARIANTS = { from: "assistant" };
|
|
|
38
40
|
* ```
|
|
39
41
|
*/
|
|
40
42
|
function AiMessage({ className, from, ...props }) {
|
|
41
|
-
return /* @__PURE__ */ jsx(
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
return /* @__PURE__ */ jsx(AiMessageContext.Provider, {
|
|
44
|
+
value: { from },
|
|
45
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
46
|
+
className: cn("group flex flex-col gap-2 p-4", SF_AI_MESSAGE_VARIANTS.from[from].classes, className),
|
|
47
|
+
...props
|
|
48
|
+
})
|
|
44
49
|
});
|
|
45
50
|
}
|
|
46
51
|
/**
|
|
52
|
+
* Detects if children is plain text (string or number) suitable for shrink wrapping.
|
|
53
|
+
*/
|
|
54
|
+
function isPlainText(children) {
|
|
55
|
+
return typeof children === "string" || typeof children === "number";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
47
58
|
* Content bubble. Applies user/assistant bubble styles via group selectors.
|
|
59
|
+
*
|
|
60
|
+
* For user messages with plain text content, uses `Text wrap="shrink"` so short
|
|
61
|
+
* messages don't stretch to the full 70% container width.
|
|
48
62
|
*/
|
|
49
63
|
function AiMessageContent({ children, className, ...props }) {
|
|
64
|
+
const shouldShrink = useContext(AiMessageContext)?.from === "user" && isPlainText(children);
|
|
50
65
|
return /* @__PURE__ */ jsx("div", {
|
|
51
|
-
className: cn("flex w-fit max-w-full flex-col gap-2 overflow-hidden text-sm", "group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-sf-control group-[.is-user]:px-4 group-[.is-user]:py-3", "group-[.is-assistant]:text-sf-default", className),
|
|
66
|
+
className: cn("flex w-fit max-w-full flex-col gap-2 overflow-hidden whitespace-pre-wrap text-sm", "group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-sf-control group-[.is-user]:px-4 group-[.is-user]:py-3", "group-[.is-assistant]:text-sf-default", className),
|
|
52
67
|
...props,
|
|
53
|
-
children
|
|
68
|
+
children: shouldShrink ? /* @__PURE__ */ jsx(Text, {
|
|
69
|
+
wrap: "shrink",
|
|
70
|
+
as: "span",
|
|
71
|
+
children
|
|
72
|
+
}) : children
|
|
54
73
|
});
|
|
55
74
|
}
|
|
56
75
|
/**
|
|
@@ -283,4 +302,4 @@ AiMessageAttachment.displayName = "AiMessageAttachment";
|
|
|
283
302
|
//#endregion
|
|
284
303
|
export { AiMessageAttachments as a, AiMessageBranchSelector as c, SF_AI_MESSAGE_DEFAULT_VARIANTS as d, SF_AI_MESSAGE_VARIANTS as f, AiMessageAttachment as i, AiMessageContent as l, AiMessageAction as n, AiMessageBranch as o, AiMessageActions as r, AiMessageBranchContent as s, AiMessage as t, AiMessageToolbar as u };
|
|
285
304
|
|
|
286
|
-
//# sourceMappingURL=ai-message-
|
|
305
|
+
//# sourceMappingURL=ai-message-Bp7L68U_.js.map
|