@flamingo-stack/openframe-frontend-core 0.0.518-1660.4292.1 → 0.0.518-1663.4312.1
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/dist/{chunk-7EFJJMNH.cjs → chunk-25JSVBPR.cjs} +7 -7
- package/dist/{chunk-7EFJJMNH.cjs.map → chunk-25JSVBPR.cjs.map} +1 -1
- package/dist/{chunk-NJADHL2G.cjs → chunk-3M2MFP2Y.cjs} +30 -2
- package/dist/chunk-3M2MFP2Y.cjs.map +1 -0
- package/dist/{chunk-QXUMLLYV.cjs → chunk-BQXHC336.cjs} +3 -3
- package/dist/{chunk-QXUMLLYV.cjs.map → chunk-BQXHC336.cjs.map} +1 -1
- package/dist/{chunk-66GH54DV.cjs → chunk-CHMQCXYO.cjs} +40 -40
- package/dist/{chunk-66GH54DV.cjs.map → chunk-CHMQCXYO.cjs.map} +1 -1
- package/dist/{chunk-RWYNJC2F.cjs → chunk-DN3GKQ47.cjs} +98 -98
- package/dist/{chunk-RWYNJC2F.cjs.map → chunk-DN3GKQ47.cjs.map} +1 -1
- package/dist/{chunk-O7L2VJDW.cjs → chunk-GH5YZSQQ.cjs} +12 -12
- package/dist/{chunk-O7L2VJDW.cjs.map → chunk-GH5YZSQQ.cjs.map} +1 -1
- package/dist/{chunk-XURRAFIB.js → chunk-GJAHN7LH.js} +404 -376
- package/dist/chunk-GJAHN7LH.js.map +1 -0
- package/dist/{chunk-FS3YSNSU.js → chunk-GS4HJA2G.js} +2 -2
- package/dist/{chunk-HIV2GIFP.cjs → chunk-GYONSC3U.cjs} +3 -3
- package/dist/{chunk-HIV2GIFP.cjs.map → chunk-GYONSC3U.cjs.map} +1 -1
- package/dist/{chunk-XURCARSX.cjs → chunk-I6IWT62M.cjs} +5 -5
- package/dist/{chunk-XURCARSX.cjs.map → chunk-I6IWT62M.cjs.map} +1 -1
- package/dist/{chunk-43ZZYJ3M.js → chunk-KRBH3PI4.js} +5 -5
- package/dist/{chunk-3LMOLDFT.cjs → chunk-KW7GDLSG.cjs} +14 -14
- package/dist/{chunk-3LMOLDFT.cjs.map → chunk-KW7GDLSG.cjs.map} +1 -1
- package/dist/{chunk-JLO2O5R4.js → chunk-LL4C2NXG.js} +5388 -5249
- package/dist/chunk-LL4C2NXG.js.map +1 -0
- package/dist/{chunk-DPFQWIZJ.cjs → chunk-LQ2R7IA2.cjs} +32 -32
- package/dist/{chunk-DPFQWIZJ.cjs.map → chunk-LQ2R7IA2.cjs.map} +1 -1
- package/dist/{chunk-P63LOLTB.js → chunk-MI7WNVWX.js} +6 -6
- package/dist/{chunk-BXRB4MQI.cjs → chunk-MKN6S7SU.cjs} +381 -242
- package/dist/chunk-MKN6S7SU.cjs.map +1 -0
- package/dist/{chunk-ZKVARGSO.js → chunk-MOZUJ7IN.js} +3 -3
- package/dist/{chunk-MIFJLUZK.js → chunk-MVY3WDFZ.js} +2 -2
- package/dist/{chunk-FXT2OZ5Q.js → chunk-NJJWY7SO.js} +3 -3
- package/dist/{chunk-H6K73RK2.cjs → chunk-O4XLTA5G.cjs} +40 -40
- package/dist/{chunk-H6K73RK2.cjs.map → chunk-O4XLTA5G.cjs.map} +1 -1
- package/dist/{chunk-MEXZZOBO.js → chunk-P6M5MN3L.js} +9 -9
- package/dist/{chunk-5KBQECAY.js → chunk-PUBL2NCH.js} +2 -2
- package/dist/{chunk-NBJ7L65H.js → chunk-PV3LXEHJ.js} +2 -2
- package/dist/{chunk-XZ6WTSVQ.js → chunk-QJHBYNHI.js} +6 -6
- package/dist/{chunk-Z3ADCYC5.js → chunk-RCBRCZUF.js} +2 -2
- package/dist/{chunk-KP4JRPUE.js → chunk-RJU7YCN3.js} +2 -2
- package/dist/{chunk-GA7BHFXD.cjs → chunk-RLVJISO4.cjs} +77 -77
- package/dist/{chunk-GA7BHFXD.cjs.map → chunk-RLVJISO4.cjs.map} +1 -1
- package/dist/{chunk-OGVIGAMZ.cjs → chunk-SXLEPNY2.cjs} +31 -31
- package/dist/{chunk-OGVIGAMZ.cjs.map → chunk-SXLEPNY2.cjs.map} +1 -1
- package/dist/{chunk-WI4XHZ2B.js → chunk-TFIPJAA5.js} +3 -3
- package/dist/{chunk-HPPCPGBR.cjs → chunk-URU6YKOA.cjs} +29 -29
- package/dist/{chunk-HPPCPGBR.cjs.map → chunk-URU6YKOA.cjs.map} +1 -1
- package/dist/{chunk-WSRPNLTY.cjs → chunk-WQDDSJWN.cjs} +18 -18
- package/dist/{chunk-WSRPNLTY.cjs.map → chunk-WQDDSJWN.cjs.map} +1 -1
- package/dist/{chunk-56OUH3UF.js → chunk-XARCNHWZ.js} +2 -2
- package/dist/{chunk-J7ZJ4KM2.cjs → chunk-YH627NWE.cjs} +4 -4
- package/dist/{chunk-J7ZJ4KM2.cjs.map → chunk-YH627NWE.cjs.map} +1 -1
- package/dist/{chunk-AI7WIW4E.js → chunk-YUQNJXSC.js} +4 -4
- package/dist/components/about-icon.d.ts +9 -0
- package/dist/components/about-icon.d.ts.map +1 -0
- package/dist/components/aspect-ratio.d.ts +4 -0
- package/dist/components/aspect-ratio.d.ts.map +1 -0
- package/dist/components/avatar.d.ts +7 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/case-studies/index.cjs +14 -14
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chart.d.ts +72 -0
- package/dist/components/chart.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts +7 -1
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +6 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +5 -3
- package/dist/components/chat/types/api.types.d.ts +22 -4
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/check-circle-icon.d.ts +7 -0
- package/dist/components/check-circle-icon.d.ts.map +1 -0
- package/dist/components/checkbox.d.ts +5 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/claude-icon.d.ts +6 -0
- package/dist/components/claude-icon.d.ts.map +1 -0
- package/dist/components/coins-icon.d.ts +9 -0
- package/dist/components/coins-icon.d.ts.map +1 -0
- package/dist/components/command.d.ts +81 -0
- package/dist/components/command.d.ts.map +1 -0
- package/dist/components/community-hub-icon.d.ts +9 -0
- package/dist/components/community-hub-icon.d.ts.map +1 -0
- package/dist/components/community-icon.d.ts +9 -0
- package/dist/components/community-icon.d.ts.map +1 -0
- package/dist/components/compare-icon.d.ts +9 -0
- package/dist/components/compare-icon.d.ts.map +1 -0
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/context-menu.d.ts +28 -0
- package/dist/components/context-menu.d.ts.map +1 -0
- package/dist/components/custom-external-link-icon.d.ts +9 -0
- package/dist/components/custom-external-link-icon.d.ts.map +1 -0
- package/dist/components/custom-fork-icon.d.ts +9 -0
- package/dist/components/custom-fork-icon.d.ts.map +1 -0
- package/dist/components/custom-icons.d.ts +8 -0
- package/dist/components/custom-icons.d.ts.map +1 -0
- package/dist/components/custom-license-icon.d.ts +9 -0
- package/dist/components/custom-license-icon.d.ts.map +1 -0
- package/dist/components/custom-star-icon.d.ts +9 -0
- package/dist/components/custom-star-icon.d.ts.map +1 -0
- package/dist/components/custom-time-icon.d.ts +9 -0
- package/dist/components/custom-time-icon.d.ts.map +1 -0
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/donut-icon.d.ts +4 -0
- package/dist/components/donut-icon.d.ts.map +1 -0
- package/dist/components/edit-profile-icon.d.ts +7 -0
- package/dist/components/edit-profile-icon.d.ts.map +1 -0
- package/dist/components/elestio-logo.d.ts +9 -0
- package/dist/components/elestio-logo.d.ts.map +1 -0
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/empty-vendor-icon.d.ts +7 -0
- package/dist/components/empty-vendor-icon.d.ts.map +1 -0
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/features/provider-button.d.ts +1 -1
- package/dist/components/features/provider-button.d.ts.map +1 -1
- package/dist/components/flamingo-logo.d.ts +9 -0
- package/dist/components/flamingo-logo.d.ts.map +1 -0
- package/dist/components/form.d.ts +26 -0
- package/dist/components/form.d.ts.map +1 -0
- package/dist/components/github-icon.d.ts +9 -0
- package/dist/components/github-icon.d.ts.map +1 -0
- package/dist/components/google-logo.d.ts +5 -0
- package/dist/components/google-logo.d.ts.map +1 -0
- package/dist/components/hamburger-icon.d.ts +9 -0
- package/dist/components/hamburger-icon.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +25 -25
- package/dist/components/help-center-pages/index.js +16 -16
- package/dist/components/hubspot-icon.d.ts +7 -0
- package/dist/components/hubspot-icon.d.ts.map +1 -0
- package/dist/components/icon-utils.d.ts +7 -0
- package/dist/components/icon-utils.d.ts.map +1 -0
- package/dist/components/icons/apple-icon.d.ts +10 -0
- package/dist/components/icons/apple-icon.d.ts.map +1 -0
- package/dist/components/icons/index.cjs +4 -2
- package/dist/components/icons/index.cjs.map +1 -1
- package/dist/components/icons/index.d.ts +1 -0
- package/dist/components/icons/index.d.ts.map +1 -1
- package/dist/components/icons/index.js +3 -1
- package/dist/components/index.cjs +222 -266
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +374 -418
- package/dist/components/index.js.map +1 -1
- package/dist/components/interactive-wrapper.d.ts +84 -0
- package/dist/components/interactive-wrapper.d.ts.map +1 -0
- package/dist/components/join-waitlist-button-stub.d.ts +10 -0
- package/dist/components/join-waitlist-button-stub.d.ts.map +1 -0
- package/dist/components/made-with-love.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +41 -41
- package/dist/components/meeting-scheduler/index.js +5 -5
- package/dist/components/menu-icon.d.ts +9 -0
- package/dist/components/menu-icon.d.ts.map +1 -0
- package/dist/components/minus-circle-icon.d.ts +7 -0
- package/dist/components/minus-circle-icon.d.ts.map +1 -0
- package/dist/components/moon-icon.d.ts +9 -0
- package/dist/components/moon-icon.d.ts.map +1 -0
- package/dist/components/ms-icon.d.ts +9 -0
- package/dist/components/ms-icon.d.ts.map +1 -0
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/open-source-features.d.ts +1 -1
- package/dist/components/open-source-features.d.ts.map +1 -1
- package/dist/components/open-source-icon.d.ts +9 -0
- package/dist/components/open-source-icon.d.ts.map +1 -0
- package/dist/components/openframe-logo.d.ts +7 -0
- package/dist/components/openframe-logo.d.ts.map +1 -0
- package/dist/components/openmsp-logo.d.ts +11 -0
- package/dist/components/openmsp-logo.d.ts.map +1 -0
- package/dist/components/plus-circle-icon.d.ts +9 -0
- package/dist/components/plus-circle-icon.d.ts.map +1 -0
- package/dist/components/progress.d.ts +7 -0
- package/dist/components/progress.d.ts.map +1 -0
- package/dist/components/provider-button.d.ts +10 -0
- package/dist/components/provider-button.d.ts.map +1 -0
- package/dist/components/reddit-icon.d.ts +10 -0
- package/dist/components/reddit-icon.d.ts.map +1 -0
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/resizable.d.ts +24 -0
- package/dist/components/resizable.d.ts.map +1 -0
- package/dist/components/scroll-area.d.ts +13 -0
- package/dist/components/scroll-area.d.ts.map +1 -0
- package/dist/components/send-icon.d.ts +7 -0
- package/dist/components/send-icon.d.ts.map +1 -0
- package/dist/components/separator.d.ts +5 -0
- package/dist/components/separator.d.ts.map +1 -0
- package/dist/components/skeleton.d.ts +4 -0
- package/dist/components/skeleton.d.ts.map +1 -0
- package/dist/components/slack-icon.d.ts +9 -0
- package/dist/components/slack-icon.d.ts.map +1 -0
- package/dist/components/slider.d.ts +5 -0
- package/dist/components/slider.d.ts.map +1 -0
- package/dist/components/smooth-accordion.d.ts +12 -0
- package/dist/components/smooth-accordion.d.ts.map +1 -0
- package/dist/components/steps.d.ts +17 -0
- package/dist/components/steps.d.ts.map +1 -0
- package/dist/components/sun-icon.d.ts +9 -0
- package/dist/components/sun-icon.d.ts.map +1 -0
- package/dist/components/switch.d.ts +5 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/textarea.d.ts +6 -0
- package/dist/components/textarea.d.ts.map +1 -0
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/components/ui/pagination-stub.d.ts +6 -0
- package/dist/components/ui/pagination-stub.d.ts.map +1 -0
- package/dist/components/ui/responsive-icons-block-stub.d.ts +7 -0
- package/dist/components/ui/responsive-icons-block-stub.d.ts.map +1 -0
- package/dist/components/user-icon.d.ts +9 -0
- package/dist/components/user-icon.d.ts.map +1 -0
- package/dist/components/vendor-directory-icon.d.ts +9 -0
- package/dist/components/vendor-directory-icon.d.ts.map +1 -0
- package/dist/components/vendors-icon.d.ts +9 -0
- package/dist/components/vendors-icon.d.ts.map +1 -0
- package/dist/components/why-it-matters.d.ts +1 -1
- package/dist/components/why-it-matters.d.ts.map +1 -1
- package/dist/components/x-icon.d.ts +9 -0
- package/dist/components/x-icon.d.ts.map +1 -0
- package/dist/components/x-logo.d.ts +7 -0
- package/dist/components/x-logo.d.ts.map +1 -0
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +8 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -3
- package/package.json +1 -1
- package/src/components/about-icon.tsx +29 -0
- package/src/components/aspect-ratio.tsx +7 -0
- package/src/components/avatar.tsx +40 -0
- package/src/components/chart.tsx +364 -0
- package/src/components/chat/chat-container.tsx +1 -1
- package/src/components/chat/hooks/.use-jetstream-dialog-subscription.md +11 -4
- package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +71 -6
- package/src/components/chat/types/api.types.ts +22 -4
- package/src/components/chat/utils/__tests__/history-merge.test.ts +99 -0
- package/src/components/check-circle-icon.tsx +29 -0
- package/src/components/checkbox.tsx +30 -0
- package/src/components/claude-icon.tsx +24 -0
- package/src/components/coins-icon.tsx +29 -0
- package/src/components/command.tsx +153 -0
- package/src/components/community-hub-icon.tsx +27 -0
- package/src/components/community-icon.tsx +27 -0
- package/src/components/compare-icon.tsx +27 -0
- package/src/components/context-menu.tsx +200 -0
- package/src/components/custom-external-link-icon.tsx +33 -0
- package/src/components/custom-fork-icon.tsx +33 -0
- package/src/components/custom-icons.tsx +47 -0
- package/src/components/custom-license-icon.tsx +33 -0
- package/src/components/custom-star-icon.tsx +29 -0
- package/src/components/custom-time-icon.tsx +33 -0
- package/src/components/donut-icon.tsx +21 -0
- package/src/components/edit-profile-icon.tsx +23 -0
- package/src/components/elestio-logo.tsx +67 -0
- package/src/components/empty-vendor-icon.tsx +44 -0
- package/src/components/features/auth-providers-list.tsx +1 -1
- package/src/components/features/platform-badge.tsx +2 -2
- package/src/components/features/provider-button.tsx +6 -2
- package/src/components/flamingo-logo.tsx +27 -0
- package/src/components/form.tsx +138 -0
- package/src/components/github-icon.tsx +26 -0
- package/src/components/google-logo.tsx +32 -0
- package/src/components/hamburger-icon.tsx +29 -0
- package/src/components/hubspot-icon.tsx +20 -0
- package/src/components/icon-utils.tsx +36 -0
- package/src/components/icons/apple-icon.tsx +30 -0
- package/src/components/icons/index.ts +1 -0
- package/src/components/interactive-wrapper.tsx +336 -0
- package/src/components/join-waitlist-button-stub.tsx +30 -0
- package/src/components/loading/openmsp-heartbeat.tsx +1 -1
- package/src/components/made-with-love.tsx +3 -4
- package/src/components/menu-icon.tsx +22 -0
- package/src/components/minus-circle-icon.tsx +28 -0
- package/src/components/moon-icon.tsx +26 -0
- package/src/components/ms-icon.tsx +31 -0
- package/src/components/open-source-features.tsx +1 -1
- package/src/components/open-source-icon.tsx +27 -0
- package/src/components/openframe-logo.tsx +46 -0
- package/src/components/openmsp-logo.tsx +36 -0
- package/src/components/plus-circle-icon.tsx +30 -0
- package/src/components/progress.tsx +27 -0
- package/src/components/provider-button.tsx +40 -0
- package/src/components/reddit-icon.tsx +50 -0
- package/src/components/resizable.tsx +45 -0
- package/src/components/scroll-area.tsx +49 -0
- package/src/components/send-icon.tsx +26 -0
- package/src/components/separator.tsx +22 -0
- package/src/components/skeleton.tsx +8 -0
- package/src/components/slack-icon.tsx +43 -0
- package/src/components/slider.tsx +28 -0
- package/src/components/smooth-accordion.tsx +124 -0
- package/src/components/steps.tsx +98 -0
- package/src/components/sun-icon.tsx +27 -0
- package/src/components/switch.tsx +29 -0
- package/src/components/textarea.tsx +22 -0
- package/src/components/ui/pagination-stub.tsx +38 -0
- package/src/components/ui/responsive-icons-block-stub.tsx +14 -0
- package/src/components/user-icon.tsx +33 -0
- package/src/components/vendor-directory-icon.tsx +27 -0
- package/src/components/vendor-tag.tsx +1 -1
- package/src/components/vendors-icon.tsx +27 -0
- package/src/components/why-it-matters.tsx +1 -1
- package/src/components/x-icon.tsx +29 -0
- package/src/components/x-logo.tsx +22 -0
- package/dist/chunk-BXRB4MQI.cjs.map +0 -1
- package/dist/chunk-JLO2O5R4.js.map +0 -1
- package/dist/chunk-NJADHL2G.cjs.map +0 -1
- package/dist/chunk-XURRAFIB.js.map +0 -1
- /package/dist/{chunk-FS3YSNSU.js.map → chunk-GS4HJA2G.js.map} +0 -0
- /package/dist/{chunk-43ZZYJ3M.js.map → chunk-KRBH3PI4.js.map} +0 -0
- /package/dist/{chunk-P63LOLTB.js.map → chunk-MI7WNVWX.js.map} +0 -0
- /package/dist/{chunk-ZKVARGSO.js.map → chunk-MOZUJ7IN.js.map} +0 -0
- /package/dist/{chunk-MIFJLUZK.js.map → chunk-MVY3WDFZ.js.map} +0 -0
- /package/dist/{chunk-FXT2OZ5Q.js.map → chunk-NJJWY7SO.js.map} +0 -0
- /package/dist/{chunk-MEXZZOBO.js.map → chunk-P6M5MN3L.js.map} +0 -0
- /package/dist/{chunk-5KBQECAY.js.map → chunk-PUBL2NCH.js.map} +0 -0
- /package/dist/{chunk-NBJ7L65H.js.map → chunk-PV3LXEHJ.js.map} +0 -0
- /package/dist/{chunk-XZ6WTSVQ.js.map → chunk-QJHBYNHI.js.map} +0 -0
- /package/dist/{chunk-Z3ADCYC5.js.map → chunk-RCBRCZUF.js.map} +0 -0
- /package/dist/{chunk-KP4JRPUE.js.map → chunk-RJU7YCN3.js.map} +0 -0
- /package/dist/{chunk-WI4XHZ2B.js.map → chunk-TFIPJAA5.js.map} +0 -0
- /package/dist/{chunk-56OUH3UF.js.map → chunk-XARCNHWZ.js.map} +0 -0
- /package/dist/{chunk-AI7WIW4E.js.map → chunk-YUQNJXSC.js.map} +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../utils/cn"
|
|
7
|
+
|
|
8
|
+
const Avatar = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<AvatarPrimitive.Root
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
19
|
+
|
|
20
|
+
const AvatarImage = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
|
|
25
|
+
))
|
|
26
|
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
27
|
+
|
|
28
|
+
const AvatarFallback = React.forwardRef<
|
|
29
|
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
31
|
+
>(({ className, ...props }, ref) => (
|
|
32
|
+
<AvatarPrimitive.Fallback
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
))
|
|
38
|
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
39
|
+
|
|
40
|
+
export { Avatar, AvatarImage, AvatarFallback }
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RechartsPrimitive from "recharts"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../utils/cn"
|
|
7
|
+
|
|
8
|
+
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
9
|
+
const THEMES = { light: "", dark: ".dark" } as const
|
|
10
|
+
|
|
11
|
+
export type ChartConfig = {
|
|
12
|
+
[k in string]: {
|
|
13
|
+
label?: React.ReactNode
|
|
14
|
+
icon?: React.ComponentType
|
|
15
|
+
} & (
|
|
16
|
+
| { color?: string; theme?: never }
|
|
17
|
+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ChartContextProps = {
|
|
22
|
+
config: ChartConfig
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
26
|
+
|
|
27
|
+
function useChart() {
|
|
28
|
+
const context = React.useContext(ChartContext)
|
|
29
|
+
|
|
30
|
+
if (!context) {
|
|
31
|
+
throw new Error("useChart must be used within a <ChartContainer />")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return context
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ChartContainer = React.forwardRef<
|
|
38
|
+
HTMLDivElement,
|
|
39
|
+
React.ComponentProps<"div"> & {
|
|
40
|
+
config: ChartConfig
|
|
41
|
+
children: React.ComponentProps<
|
|
42
|
+
typeof RechartsPrimitive.ResponsiveContainer
|
|
43
|
+
>["children"]
|
|
44
|
+
}
|
|
45
|
+
>(({ id, className, children, config, ...props }, ref) => {
|
|
46
|
+
const uniqueId = React.useId()
|
|
47
|
+
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<ChartContext.Provider value={{ config }}>
|
|
51
|
+
<div
|
|
52
|
+
data-chart={chartId}
|
|
53
|
+
ref={ref}
|
|
54
|
+
className={cn(
|
|
55
|
+
"flex aspect-video justify-center text-h6 [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
<ChartStyle id={chartId} config={config} />
|
|
61
|
+
<RechartsPrimitive.ResponsiveContainer>
|
|
62
|
+
{children}
|
|
63
|
+
</RechartsPrimitive.ResponsiveContainer>
|
|
64
|
+
</div>
|
|
65
|
+
</ChartContext.Provider>
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
ChartContainer.displayName = "Chart"
|
|
69
|
+
|
|
70
|
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
71
|
+
const colorConfig = Object.entries(config).filter(
|
|
72
|
+
([_, config]) => config.theme || config.color
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (!colorConfig.length) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<style
|
|
81
|
+
dangerouslySetInnerHTML={{
|
|
82
|
+
__html: Object.entries(THEMES)
|
|
83
|
+
.map(
|
|
84
|
+
([theme, prefix]) => `
|
|
85
|
+
${prefix} [data-chart=${id}] {
|
|
86
|
+
${colorConfig
|
|
87
|
+
.map(([key, itemConfig]) => {
|
|
88
|
+
const color =
|
|
89
|
+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
90
|
+
itemConfig.color
|
|
91
|
+
return color ? ` --color-${key}: ${color};` : null
|
|
92
|
+
})
|
|
93
|
+
.join("\n")}
|
|
94
|
+
}
|
|
95
|
+
`
|
|
96
|
+
)
|
|
97
|
+
.join("\n"),
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
104
|
+
|
|
105
|
+
const ChartTooltipContent = React.forwardRef<
|
|
106
|
+
HTMLDivElement,
|
|
107
|
+
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
108
|
+
React.ComponentProps<"div"> & {
|
|
109
|
+
hideLabel?: boolean
|
|
110
|
+
hideIndicator?: boolean
|
|
111
|
+
indicator?: "line" | "dot" | "dashed"
|
|
112
|
+
nameKey?: string
|
|
113
|
+
labelKey?: string
|
|
114
|
+
}
|
|
115
|
+
>(
|
|
116
|
+
(props: any, ref: any) => {
|
|
117
|
+
const {
|
|
118
|
+
active,
|
|
119
|
+
payload,
|
|
120
|
+
className,
|
|
121
|
+
indicator = "dot",
|
|
122
|
+
hideLabel = false,
|
|
123
|
+
hideIndicator = false,
|
|
124
|
+
label,
|
|
125
|
+
labelFormatter,
|
|
126
|
+
labelClassName,
|
|
127
|
+
formatter,
|
|
128
|
+
color,
|
|
129
|
+
nameKey,
|
|
130
|
+
labelKey,
|
|
131
|
+
...rest
|
|
132
|
+
} = props;
|
|
133
|
+
const { config } = useChart()
|
|
134
|
+
|
|
135
|
+
const tooltipLabel = React.useMemo(() => {
|
|
136
|
+
if (hideLabel || !payload?.length) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const [item] = payload
|
|
141
|
+
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
|
142
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
143
|
+
const value =
|
|
144
|
+
!labelKey && typeof label === "string"
|
|
145
|
+
? config[label as keyof typeof config]?.label || label
|
|
146
|
+
: itemConfig?.label
|
|
147
|
+
|
|
148
|
+
if (labelFormatter) {
|
|
149
|
+
return (
|
|
150
|
+
<div className={cn("font-medium", labelClassName)}>
|
|
151
|
+
{labelFormatter(value, payload)}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!value) {
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
|
161
|
+
}, [
|
|
162
|
+
label,
|
|
163
|
+
labelFormatter,
|
|
164
|
+
payload,
|
|
165
|
+
hideLabel,
|
|
166
|
+
labelClassName,
|
|
167
|
+
config,
|
|
168
|
+
labelKey,
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
if (!active || !payload?.length) {
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
ref={ref}
|
|
180
|
+
className={cn(
|
|
181
|
+
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-h6 shadow-xl",
|
|
182
|
+
className
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
{!nestLabel ? tooltipLabel : null}
|
|
186
|
+
<div className="grid gap-1.5">
|
|
187
|
+
{payload?.map((item: any, index: number) => {
|
|
188
|
+
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
189
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
190
|
+
const indicatorColor = color || item.payload.fill || item.color
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div
|
|
194
|
+
key={item.dataKey}
|
|
195
|
+
className={cn(
|
|
196
|
+
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
|
197
|
+
indicator === "dot" && "items-center"
|
|
198
|
+
)}
|
|
199
|
+
>
|
|
200
|
+
{formatter && item?.value !== undefined && item.name ? (
|
|
201
|
+
formatter(item.value, item.name, item, index, item.payload)
|
|
202
|
+
) : (
|
|
203
|
+
<>
|
|
204
|
+
{itemConfig?.icon ? (
|
|
205
|
+
<itemConfig.icon />
|
|
206
|
+
) : (
|
|
207
|
+
!hideIndicator && (
|
|
208
|
+
<div
|
|
209
|
+
className={cn(
|
|
210
|
+
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
|
211
|
+
{
|
|
212
|
+
"h-2.5 w-2.5": indicator === "dot",
|
|
213
|
+
"w-1": indicator === "line",
|
|
214
|
+
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
215
|
+
indicator === "dashed",
|
|
216
|
+
"my-0.5": nestLabel && indicator === "dashed",
|
|
217
|
+
}
|
|
218
|
+
)}
|
|
219
|
+
style={
|
|
220
|
+
{
|
|
221
|
+
"--color-bg": indicatorColor,
|
|
222
|
+
"--color-border": indicatorColor,
|
|
223
|
+
} as React.CSSProperties
|
|
224
|
+
}
|
|
225
|
+
/>
|
|
226
|
+
)
|
|
227
|
+
)}
|
|
228
|
+
<div
|
|
229
|
+
className={cn(
|
|
230
|
+
"flex flex-1 justify-between leading-none",
|
|
231
|
+
nestLabel ? "items-end" : "items-center"
|
|
232
|
+
)}
|
|
233
|
+
>
|
|
234
|
+
<div className="grid gap-1.5">
|
|
235
|
+
{nestLabel ? tooltipLabel : null}
|
|
236
|
+
<span className="text-muted-foreground">
|
|
237
|
+
{itemConfig?.label || item.name}
|
|
238
|
+
</span>
|
|
239
|
+
</div>
|
|
240
|
+
{item.value && (
|
|
241
|
+
<span className="text-code tabular-nums text-foreground">
|
|
242
|
+
{item.value.toLocaleString()}
|
|
243
|
+
</span>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)
|
|
250
|
+
})}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
ChartTooltipContent.displayName = "ChartTooltip"
|
|
257
|
+
|
|
258
|
+
const ChartLegend = RechartsPrimitive.Legend
|
|
259
|
+
|
|
260
|
+
const ChartLegendContent = React.forwardRef<
|
|
261
|
+
HTMLDivElement,
|
|
262
|
+
React.ComponentProps<"div"> &
|
|
263
|
+
{ payload?: any; verticalAlign?: any } & {
|
|
264
|
+
hideIcon?: boolean
|
|
265
|
+
nameKey?: string
|
|
266
|
+
}
|
|
267
|
+
>(
|
|
268
|
+
(
|
|
269
|
+
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
|
270
|
+
ref
|
|
271
|
+
) => {
|
|
272
|
+
const { config } = useChart()
|
|
273
|
+
|
|
274
|
+
if (!payload?.length) {
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div
|
|
280
|
+
ref={ref}
|
|
281
|
+
className={cn(
|
|
282
|
+
"flex items-center justify-center gap-4",
|
|
283
|
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
284
|
+
className
|
|
285
|
+
)}
|
|
286
|
+
>
|
|
287
|
+
{payload?.map((item: any) => {
|
|
288
|
+
const key = `${nameKey || item.dataKey || "value"}`
|
|
289
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div
|
|
293
|
+
key={item.value}
|
|
294
|
+
className={cn(
|
|
295
|
+
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
|
296
|
+
)}
|
|
297
|
+
>
|
|
298
|
+
{itemConfig?.icon && !hideIcon ? (
|
|
299
|
+
<itemConfig.icon />
|
|
300
|
+
) : (
|
|
301
|
+
<div
|
|
302
|
+
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
303
|
+
style={{
|
|
304
|
+
backgroundColor: item.color,
|
|
305
|
+
}}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
{itemConfig?.label}
|
|
309
|
+
</div>
|
|
310
|
+
)
|
|
311
|
+
})}
|
|
312
|
+
</div>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
ChartLegendContent.displayName = "ChartLegend"
|
|
317
|
+
|
|
318
|
+
// Helper to extract item config from a payload.
|
|
319
|
+
function getPayloadConfigFromPayload(
|
|
320
|
+
config: ChartConfig,
|
|
321
|
+
payload: unknown,
|
|
322
|
+
key: string
|
|
323
|
+
) {
|
|
324
|
+
if (typeof payload !== "object" || payload === null) {
|
|
325
|
+
return undefined
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const payloadPayload =
|
|
329
|
+
"payload" in payload &&
|
|
330
|
+
typeof payload.payload === "object" &&
|
|
331
|
+
payload.payload !== null
|
|
332
|
+
? payload.payload
|
|
333
|
+
: undefined
|
|
334
|
+
|
|
335
|
+
let configLabelKey: string = key
|
|
336
|
+
|
|
337
|
+
if (
|
|
338
|
+
key in payload &&
|
|
339
|
+
typeof payload[key as keyof typeof payload] === "string"
|
|
340
|
+
) {
|
|
341
|
+
configLabelKey = payload[key as keyof typeof payload] as string
|
|
342
|
+
} else if (
|
|
343
|
+
payloadPayload &&
|
|
344
|
+
key in payloadPayload &&
|
|
345
|
+
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
346
|
+
) {
|
|
347
|
+
configLabelKey = payloadPayload[
|
|
348
|
+
key as keyof typeof payloadPayload
|
|
349
|
+
] as string
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return configLabelKey in config
|
|
353
|
+
? config[configLabelKey]
|
|
354
|
+
: config[key as keyof typeof config]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export {
|
|
358
|
+
ChartContainer,
|
|
359
|
+
ChartTooltip,
|
|
360
|
+
ChartTooltipContent,
|
|
361
|
+
ChartLegend,
|
|
362
|
+
ChartLegendContent,
|
|
363
|
+
ChartStyle,
|
|
364
|
+
}
|
|
@@ -2,7 +2,7 @@ import * as React from "react"
|
|
|
2
2
|
import { cn } from "../../utils/cn"
|
|
3
3
|
import { SquareAvatar as Avatar } from "../ui/square-avatar"
|
|
4
4
|
import { Button } from "../ui/button"
|
|
5
|
-
import { PlusCircleIcon } from "../
|
|
5
|
+
import { PlusCircleIcon } from "../plus-circle-icon"
|
|
6
6
|
import { XmarkIcon } from "../icons-v2-generated/signs-and-symbols/xmark-icon"
|
|
7
7
|
import { Chevron02LeftIcon } from "../icons-v2-generated"
|
|
8
8
|
import { TicketStatusTag, resolveStatusTagProps } from "../ui/ticket-status-tag"
|
|
@@ -13,22 +13,29 @@ The single exported hook. Accepts `UseJetStreamDialogSubscriptionOptions` and re
|
|
|
13
13
|
|---|---|
|
|
14
14
|
| `isConnected` | Whether the NATS WebSocket connection is live |
|
|
15
15
|
| `isSubscribed` | Whether the JetStream consumer is active |
|
|
16
|
-
| `reconnectionCount` | Increments whenever the live tail is re-established: a NATS reconnect, a JetStream ordered-consumer recreation,
|
|
16
|
+
| `reconnectionCount` | Increments whenever the live tail is re-established: a NATS reconnect, a JetStream ordered-consumer recreation, a resync after the page was hidden, or a host reporting either via `resyncSignal`. Callers refetch persisted history on every increment; one absence counts once even when two sources report it |
|
|
17
17
|
| `currentStreamSeq` | Highest stream sequence number observed |
|
|
18
18
|
|
|
19
|
+
**Host-supplied resync (`resyncSignal` option):**
|
|
20
|
+
|
|
21
|
+
A counter the embedder bumps when it knows, from outside the page, that the tail may have missed something — an absence the page could not observe (a native shell whose window visibility never reaches `document.visibilityState`), or a write made on the page's behalf while it was away. Any increase is treated exactly like a reconnect. The value at mount is a baseline, rebased on every `dialogId` change so switching to a conversation with a lower count cannot swallow later resyncs.
|
|
22
|
+
|
|
19
23
|
**Internal refs (stable across renders):**
|
|
20
24
|
|
|
21
25
|
- `clientRef` — shared `NatsClient` instance
|
|
22
26
|
- `lastRecoveryReportRef` — timestamp floor so a flapping consumer cannot storm callers with refetches
|
|
27
|
+
- `resyncTimerRef` — trailing-edge timer collapsing resync requests within `RESYNC_COALESCE_MS`; cleared on dialog change and unmount so a request never crosses a conversation boundary
|
|
28
|
+
- `lastResyncSignalRef` — baseline for `resyncSignal`, rebased per dialog
|
|
23
29
|
- `subscriptionRef` — active `JetStreamSubscriptionHandle`
|
|
24
30
|
- `highestStreamSeqRef` — tracks max stream sequence for gap-free resume
|
|
25
31
|
|
|
26
32
|
**Effect order (declaration order matters):**
|
|
27
33
|
|
|
28
34
|
1. **Connection effect** — acquires/releases the shared NATS client when `enabled` or `wsUrl` changes; drives the `startConnectionLifecycle` which emits status events.
|
|
29
|
-
2. **Dialog reset effect** — resets `highestStreamSeqRef` when `dialogId` changes so a new dialog never inherits a stale start sequence.
|
|
30
|
-
3. **Visibility effect** — after the page has been hidden longer than `RESYNC_AFTER_HIDDEN_MS`, returning to view
|
|
31
|
-
4. **
|
|
35
|
+
2. **Dialog reset effect** — resets `highestStreamSeqRef`, rebases the `resyncSignal` baseline, and drops any pending resync when `dialogId` changes, so a new dialog never inherits a stale start sequence or the previous dialog's refetch.
|
|
36
|
+
3. **Visibility effect** — after the page has been hidden longer than `RESYNC_AFTER_HIDDEN_MS`, returning to view requests a resync: a hidden page can stop receiving without the socket ever closing, and nothing else reports that.
|
|
37
|
+
4. **Host-signal effect** — an increase in `resyncSignal` requests a resync on the same path, for absences and out-of-band writes the page cannot see.
|
|
38
|
+
5. **Subscription effect** — creates or recreates the ephemeral consumer when `isConnected`, `dialogId`, `topic`, `streamName`, or the reconnect counter change.
|
|
32
39
|
|
|
33
40
|
**Deliver policy logic:**
|
|
34
41
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
acquireClient as acquireSharedClient,
|
|
6
6
|
releaseClient as releaseSharedClient,
|
|
@@ -20,7 +20,24 @@ const DEFAULT_STREAM_NAME = 'CHAT_CHUNKS'
|
|
|
20
20
|
* absence worth resyncing for. Alt-tabbing must not churn the consumer; a
|
|
21
21
|
* window that sat in the tray must not come back showing a stale conversation.
|
|
22
22
|
*/
|
|
23
|
-
const RESYNC_AFTER_HIDDEN_MS = 10_000
|
|
23
|
+
export const RESYNC_AFTER_HIDDEN_MS = 10_000
|
|
24
|
+
/**
|
|
25
|
+
* How long resync requests are gathered before one is acted on.
|
|
26
|
+
*
|
|
27
|
+
* A single reveal can be reported twice: the page sees `visibilitychange`, and
|
|
28
|
+
* a host that knows the window was away reports it too (`resyncSignal`),
|
|
29
|
+
* neither able to tell whether the other noticed. Both land within a frame or
|
|
30
|
+
* two of each other, being driven by the same OS event, and acting on both
|
|
31
|
+
* rebuilds the consumer and refetches history twice for one reveal.
|
|
32
|
+
*
|
|
33
|
+
* Gathered on the TRAILING edge, so a request is deferred and never dropped.
|
|
34
|
+
* Two reports of one reveal collapse into one rebuild; two genuinely different
|
|
35
|
+
* signals — a notification reply landing just before the user opens the window
|
|
36
|
+
* — also collapse into one, which is correct, because a single refetch after
|
|
37
|
+
* the last of them covers both. The cost is a second of latency on a path whose
|
|
38
|
+
* next step is a network round trip.
|
|
39
|
+
*/
|
|
40
|
+
const RESYNC_COALESCE_MS = 1_000
|
|
24
41
|
/**
|
|
25
42
|
* Floor between two reported recoveries. Every recovery costs the caller a
|
|
26
43
|
* history refetch, and a consumer that cannot hold its sequence — gap, reset,
|
|
@@ -60,6 +77,7 @@ export function useJetStreamDialogSubscription({
|
|
|
60
77
|
clientConfig = {},
|
|
61
78
|
reconnectionBackoff,
|
|
62
79
|
inactiveThresholdMs,
|
|
80
|
+
resyncSignal = 0,
|
|
63
81
|
}: UseJetStreamDialogSubscriptionOptions): UseJetStreamDialogSubscriptionReturn {
|
|
64
82
|
const [isConnected, setIsConnected] = useState(false)
|
|
65
83
|
const [isSubscribed, setIsSubscribed] = useState(false)
|
|
@@ -73,6 +91,8 @@ export function useJetStreamDialogSubscription({
|
|
|
73
91
|
const [currentStreamSeq, setCurrentStreamSeq] = useState<number | null>(null)
|
|
74
92
|
|
|
75
93
|
const lastRecoveryReportRef = useRef(0)
|
|
94
|
+
const resyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
95
|
+
const lastResyncSignalRef = useRef(resyncSignal)
|
|
76
96
|
const clientRef = useRef<NatsClient | null>(null)
|
|
77
97
|
const subscriptionRef = useRef<JetStreamSubscriptionHandle | null>(null)
|
|
78
98
|
const highestStreamSeqRef = useRef<number | null>(null)
|
|
@@ -245,7 +265,26 @@ export function useJetStreamDialogSubscription({
|
|
|
245
265
|
// ignoring optStartSeq).
|
|
246
266
|
useEffect(() => {
|
|
247
267
|
highestStreamSeqRef.current = null
|
|
268
|
+
// A host's signal counts per dialog (it can carry a per-conversation term),
|
|
269
|
+
// so the baseline has to move with the dialog or a switch to one with a
|
|
270
|
+
// LOWER count would swallow every later resync for it.
|
|
271
|
+
lastResyncSignalRef.current = resyncSignal
|
|
248
272
|
setCurrentStreamSeq(null)
|
|
273
|
+
// A resync gathered for the previous dialog must not land on this one: the
|
|
274
|
+
// counter says nothing about which conversation it meant, so callers would
|
|
275
|
+
// refetch the wrong one. The dialog change supersedes it — the new dialog
|
|
276
|
+
// loads its own history regardless. As cleanup, so it covers unmount too,
|
|
277
|
+
// which is the same rule.
|
|
278
|
+
return () => {
|
|
279
|
+
if (resyncTimerRef.current !== null) {
|
|
280
|
+
clearTimeout(resyncTimerRef.current)
|
|
281
|
+
resyncTimerRef.current = null
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// `resyncSignal` is read as the new baseline, not depended on: reacting to
|
|
285
|
+
// it here would reset the baseline on the very change meant to trigger a
|
|
286
|
+
// resync, and the effect below owns that.
|
|
287
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
249
288
|
}, [dialogId])
|
|
250
289
|
|
|
251
290
|
// Coming back into view after a real absence is treated exactly like a
|
|
@@ -256,8 +295,20 @@ export function useJetStreamDialogSubscription({
|
|
|
256
295
|
// report that: the tail simply stays quiet, and the conversation on screen
|
|
257
296
|
// stays frozen at whatever was there when the window went away (the user's
|
|
258
297
|
// manual fix is a page refresh, which is exactly the history refetch this
|
|
259
|
-
// triggers).
|
|
260
|
-
//
|
|
298
|
+
// triggers). Rebuilds the consumer from the last sequence seen AND tells
|
|
299
|
+
// callers to refetch history.
|
|
300
|
+
//
|
|
301
|
+
// Every source funnels through here (see `RESYNC_COALESCE_MS`). Restarting
|
|
302
|
+
// the timer rather than dropping is what makes that safe: a request is only
|
|
303
|
+
// ever folded into the one that follows it, never discarded.
|
|
304
|
+
const requestResync = useCallback(() => {
|
|
305
|
+
if (resyncTimerRef.current !== null) clearTimeout(resyncTimerRef.current)
|
|
306
|
+
resyncTimerRef.current = setTimeout(() => {
|
|
307
|
+
resyncTimerRef.current = null
|
|
308
|
+
setReconnectionCount((c) => c + 1)
|
|
309
|
+
}, RESYNC_COALESCE_MS)
|
|
310
|
+
}, [])
|
|
311
|
+
|
|
261
312
|
useEffect(() => {
|
|
262
313
|
if (!enabled) return
|
|
263
314
|
let hiddenSince = document.visibilityState === 'hidden' ? Date.now() : 0
|
|
@@ -270,13 +321,27 @@ export function useJetStreamDialogSubscription({
|
|
|
270
321
|
const awayMs = hiddenSince === 0 ? 0 : Date.now() - hiddenSince
|
|
271
322
|
hiddenSince = 0
|
|
272
323
|
if (awayMs >= RESYNC_AFTER_HIDDEN_MS) {
|
|
273
|
-
|
|
324
|
+
requestResync()
|
|
274
325
|
}
|
|
275
326
|
}
|
|
276
327
|
|
|
277
328
|
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
278
329
|
return () => document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
279
|
-
}, [enabled])
|
|
330
|
+
}, [enabled, requestResync])
|
|
331
|
+
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
// Held, not consumed, while disabled: it fires once on enable instead.
|
|
334
|
+
// A host reports a signal against the conversation, not against this hook's
|
|
335
|
+
// readiness, so one arriving while the caller is still assembling the
|
|
336
|
+
// subscription describes a gap in the history the caller is fetching right
|
|
337
|
+
// now. Dropping it would leave that fetch's result stale with nothing left
|
|
338
|
+
// to say so.
|
|
339
|
+
if (!enabled) return
|
|
340
|
+
// The mount value is a baseline, not a signal.
|
|
341
|
+
if (resyncSignal <= lastResyncSignalRef.current) return
|
|
342
|
+
lastResyncSignalRef.current = resyncSignal
|
|
343
|
+
requestResync()
|
|
344
|
+
}, [enabled, resyncSignal, requestResync])
|
|
280
345
|
|
|
281
346
|
// Subscription lifecycle: (re)create the ephemeral JetStream consumer whenever
|
|
282
347
|
// we transition into a connected state for a dialog, and whenever the dialog
|
|
@@ -131,6 +131,22 @@ export interface UseJetStreamDialogSubscriptionOptions {
|
|
|
131
131
|
/** Consumer inactivity threshold in ms before NATS auto-cleans it. Defaults to the
|
|
132
132
|
* client's, and applies only to the first consumer — recreations use nats.ws's. */
|
|
133
133
|
inactiveThresholdMs?: number
|
|
134
|
+
/**
|
|
135
|
+
* A counter the host bumps when it knows, from outside the page, that the tail
|
|
136
|
+
* may have missed something.
|
|
137
|
+
*
|
|
138
|
+
* The hook's own detector is `visibilitychange`, which a browser tab reports
|
|
139
|
+
* honestly and an embedding shell may not — and which says nothing at all
|
|
140
|
+
* about a write made on the page's behalf while it was away. A host holding
|
|
141
|
+
* either kind of knowledge bumps this instead of hoping the page noticed.
|
|
142
|
+
*
|
|
143
|
+
* Treated exactly like a reconnect: the consumer is rebuilt from the highest
|
|
144
|
+
* sequence already seen, and `reconnectionCount` moves so callers refetch
|
|
145
|
+
* persisted history. Any increase counts; the value itself means nothing, and
|
|
146
|
+
* an increase arriving alongside a `visibilitychange` costs one rebuild, not
|
|
147
|
+
* two.
|
|
148
|
+
*/
|
|
149
|
+
resyncSignal?: number
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
export interface UseJetStreamDialogSubscriptionReturn {
|
|
@@ -138,10 +154,12 @@ export interface UseJetStreamDialogSubscriptionReturn {
|
|
|
138
154
|
isSubscribed: boolean
|
|
139
155
|
/**
|
|
140
156
|
* Incremented each time the live tail is re-established: a NATS reconnect, a
|
|
141
|
-
* JetStream ordered-consumer recreation,
|
|
142
|
-
* being hidden long enough to have missed something
|
|
143
|
-
*
|
|
144
|
-
* history has to be
|
|
157
|
+
* JetStream ordered-consumer recreation, the page returning to view after
|
|
158
|
+
* being hidden long enough to have missed something, or a host reporting the
|
|
159
|
+
* same via `resyncSignal`. All of them mean the same thing to the caller —
|
|
160
|
+
* the tail may have skipped messages, so persisted history has to be
|
|
161
|
+
* refetched. One absence counts once even when two sources report it.
|
|
162
|
+
* Starts at 0.
|
|
145
163
|
*/
|
|
146
164
|
reconnectionCount: number
|
|
147
165
|
/** Highest JetStream stream sequence observed so far (null before first delivery). */
|