@shepai/cli 1.53.1 → 1.54.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/apis/json-schema/NotificationEvent.yaml +4 -0
- package/dist/packages/core/src/domain/generated/output.d.ts +4 -0
- package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts +4 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.js +19 -6
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts +5 -3
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.js +21 -13
- package/dist/src/presentation/cli/commands/ui.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/ui.command.js +2 -1
- package/dist/src/presentation/web/app/api/agent-events/health/route.d.ts +13 -0
- package/dist/src/presentation/web/app/api/agent-events/health/route.d.ts.map +1 -0
- package/dist/src/presentation/web/app/api/agent-events/health/route.js +60 -0
- package/dist/src/presentation/web/app/api/agent-events/route.d.ts +8 -4
- package/dist/src/presentation/web/app/api/agent-events/route.d.ts.map +1 -1
- package/dist/src/presentation/web/app/api/agent-events/route.js +227 -55
- package/dist/src/presentation/web/app/layout.d.ts.map +1 -1
- package/dist/src/presentation/web/app/layout.js +14 -0
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts +5 -1
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +29 -1
- package/dist/src/presentation/web/components/common/repository-node/repository-node.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/repository-node/repository-node.js +1 -1
- package/dist/src/presentation/web/components/common/sound-toggle/index.d.ts +2 -0
- package/dist/src/presentation/web/components/common/sound-toggle/index.d.ts.map +1 -0
- package/dist/src/presentation/web/components/common/sound-toggle/index.js +1 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.d.ts +2 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.d.ts.map +1 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.js +9 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.d.ts +8 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.d.ts.map +1 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.js +17 -0
- package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +14 -2
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts +1 -0
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +151 -19
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.js +1 -1
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.d.ts +1 -0
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.js +31 -1
- package/dist/src/presentation/web/components/layouts/app-shell/app-shell.d.ts.map +1 -1
- package/dist/src/presentation/web/components/layouts/app-shell/app-shell.js +2 -1
- package/dist/src/presentation/web/dev-server.js +14 -0
- package/dist/src/presentation/web/hooks/use-agent-events.d.ts +7 -0
- package/dist/src/presentation/web/hooks/use-agent-events.d.ts.map +1 -1
- package/dist/src/presentation/web/hooks/use-agent-events.js +135 -47
- package/dist/src/presentation/web/hooks/use-notifications.d.ts.map +1 -1
- package/dist/src/presentation/web/hooks/use-notifications.js +56 -16
- package/dist/src/presentation/web/hooks/use-notifications.stories.d.ts.map +1 -1
- package/dist/src/presentation/web/hooks/use-notifications.stories.js +28 -9
- package/dist/src/presentation/web/hooks/use-sound-enabled.d.ts +6 -0
- package/dist/src/presentation/web/hooks/use-sound-enabled.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-sound-enabled.js +27 -0
- package/dist/src/presentation/web/hooks/use-sound.d.ts +15 -0
- package/dist/src/presentation/web/hooks/use-sound.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-sound.js +83 -0
- package/dist/src/presentation/web/hooks/use-sound.stories.d.ts +8 -0
- package/dist/src/presentation/web/hooks/use-sound.stories.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-sound.stories.js +67 -0
- package/dist/src/presentation/web/public/agent-events-sw.d.ts +35 -0
- package/dist/src/presentation/web/public/agent-events-sw.d.ts.map +1 -0
- package/dist/src/presentation/web/public/agent-events-sw.js +140 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-path-routes-manifest.json +1 -0
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/.tsbuildinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/routes-manifest.json +6 -0
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/api/agent-events/health/route/app-paths-manifest.json +3 -0
- package/web/.next/server/app/api/agent-events/health/route/build-manifest.json +11 -0
- package/web/.next/server/app/api/agent-events/health/route/server-reference-manifest.json +4 -0
- package/web/.next/server/app/api/agent-events/health/route.js +6 -0
- package/web/.next/server/app/api/agent-events/health/route.js.map +5 -0
- package/web/.next/server/app/api/agent-events/health/route.js.nft.json +1 -0
- package/web/.next/server/app/api/agent-events/health/route_client-reference-manifest.js +2 -0
- package/web/.next/server/app/api/agent-events/route.js +2 -1
- package/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
- package/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/version/page.js +1 -1
- package/web/.next/server/app/version/page.js.nft.json +1 -1
- package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app-paths-manifest.json +1 -0
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +6 -0
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -0
- package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js +3 -0
- package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js.map +1 -0
- package/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js.map +1 -0
- package/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js.map +1 -0
- package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/{standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__249c74f6._.js → server/chunks/ssr/[root-of-the-server]__6e8b5181._.js} +2 -2
- package/web/.next/server/chunks/ssr/{_1fafa9db._.js → _1632a21f._.js} +2 -2
- package/web/.next/server/chunks/ssr/_1632a21f._.js.map +1 -0
- package/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/src/presentation/web/.next/app-path-routes-manifest.json +1 -0
- package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/routes-manifest.json +6 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/build-manifest.json +11 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js +6 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js.map +5 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js.nft.json +1 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/route.js +2 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/standalone/src/presentation/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app-paths-manifest.json +1 -0
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +6 -0
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js +3 -0
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js +3 -0
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js +3 -0
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/{server/chunks/ssr/[root-of-the-server]__249c74f6._.js → standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__6e8b5181._.js} +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{_1fafa9db._.js → _1632a21f._.js} +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/app/api/agent-events/health/route.ts +69 -0
- package/web/.next/standalone/src/presentation/web/app/api/agent-events/route.ts +258 -54
- package/web/.next/standalone/src/presentation/web/app/layout.tsx +14 -0
- package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +39 -2
- package/web/.next/standalone/src/presentation/web/components/common/repository-node/repository-node.tsx +1 -0
- package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/index.ts +1 -0
- package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.tsx +27 -0
- package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/sound-toggle.tsx +21 -0
- package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +15 -1
- package/web/.next/standalone/src/presentation/web/components/features/control-center/use-control-center-state.ts +194 -31
- package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.stories.tsx +41 -0
- package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.tsx +1 -1
- package/web/.next/standalone/src/presentation/web/components/layouts/app-shell/app-shell.tsx +3 -1
- package/web/.next/standalone/src/presentation/web/dev-server.ts +21 -0
- package/web/.next/standalone/src/presentation/web/hooks/use-agent-events.ts +161 -52
- package/web/.next/standalone/src/presentation/web/hooks/use-notifications.stories.tsx +39 -10
- package/web/.next/standalone/src/presentation/web/hooks/use-notifications.ts +68 -15
- package/web/.next/standalone/src/presentation/web/hooks/use-sound-enabled.ts +38 -0
- package/web/.next/standalone/src/presentation/web/hooks/use-sound.stories.tsx +109 -0
- package/web/.next/standalone/src/presentation/web/hooks/use-sound.ts +101 -0
- package/web/.next/standalone/src/presentation/web/public/agent-events-sw.js +164 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/button.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/caution.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/celebration.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/disabled.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/notification.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/progress_loop.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/ringtone_loop.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/select.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_01.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_02.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_03.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_04.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_05.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_01.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_02.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_03.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_04.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_05.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/toggle_off.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/toggle_on.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/transition_down.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/transition_up.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_01.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_02.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_03.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_04.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_05.wav +0 -0
- package/web/.next/standalone/src/presentation/web/server.js +1 -1
- package/web/.next/static/chunks/1f13bc20e938335c.css +2 -0
- package/web/.next/static/chunks/702384d364482e4b.js +10 -0
- package/web/.next/static/chunks/{68fb91b6eca14536.js → 8f4461aee9a05741.js} +2 -2
- package/web/.next/static/chunks/9a82d8c76f175235.js +1 -0
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- package/web/.next/types/link.d.ts +1 -0
- package/web/.next/types/routes.d.ts +2 -1
- package/web/.next/types/validator.ts +9 -0
- package/web/public/agent-events-sw.js +164 -0
- package/web/public/sounds/button.wav +0 -0
- package/web/public/sounds/caution.wav +0 -0
- package/web/public/sounds/celebration.wav +0 -0
- package/web/public/sounds/disabled.wav +0 -0
- package/web/public/sounds/notification.wav +0 -0
- package/web/public/sounds/progress_loop.wav +0 -0
- package/web/public/sounds/ringtone_loop.wav +0 -0
- package/web/public/sounds/select.wav +0 -0
- package/web/public/sounds/swipe.wav +0 -0
- package/web/public/sounds/swipe_01.wav +0 -0
- package/web/public/sounds/swipe_02.wav +0 -0
- package/web/public/sounds/swipe_03.wav +0 -0
- package/web/public/sounds/swipe_04.wav +0 -0
- package/web/public/sounds/swipe_05.wav +0 -0
- package/web/public/sounds/tap_01.wav +0 -0
- package/web/public/sounds/tap_02.wav +0 -0
- package/web/public/sounds/tap_03.wav +0 -0
- package/web/public/sounds/tap_04.wav +0 -0
- package/web/public/sounds/tap_05.wav +0 -0
- package/web/public/sounds/toggle_off.wav +0 -0
- package/web/public/sounds/toggle_on.wav +0 -0
- package/web/public/sounds/transition_down.wav +0 -0
- package/web/public/sounds/transition_up.wav +0 -0
- package/web/public/sounds/type_01.wav +0 -0
- package/web/public/sounds/type_02.wav +0 -0
- package/web/public/sounds/type_03.wav +0 -0
- package/web/public/sounds/type_04.wav +0 -0
- package/web/public/sounds/type_05.wav +0 -0
- package/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js +0 -6
- package/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js.map +0 -1
- package/web/.next/server/chunks/ssr/_1fafa9db._.js.map +0 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js +0 -6
- package/web/.next/static/chunks/212e3c682d3b7f50.js +0 -10
- package/web/.next/static/chunks/22f30ce91a3c79c1.js +0 -1
- package/web/.next/static/chunks/476fe030d242c153.css +0 -2
- /package/web/.next/server/chunks/ssr/{[root-of-the-server]__249c74f6._.js.map → [root-of-the-server]__6e8b5181._.js.map} +0 -0
- /package/web/.next/static/{EWkET8BUeCcZJxcYQpxDY → RwzxYU9D2fPc_4j5RYu0v}/_buildManifest.js +0 -0
- /package/web/.next/static/{EWkET8BUeCcZJxcYQpxDY → RwzxYU9D2fPc_4j5RYu0v}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{EWkET8BUeCcZJxcYQpxDY → RwzxYU9D2fPc_4j5RYu0v}/_ssgManifest.js +0 -0
|
@@ -15,66 +15,178 @@ export interface UseAgentEventsResult {
|
|
|
15
15
|
connectionStatus: ConnectionStatus;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const SW_PATH = '/agent-events-sw.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook that receives real-time agent notification events via a Service Worker.
|
|
22
|
+
*
|
|
23
|
+
* The SW maintains a single SSE connection to /api/agent-events and
|
|
24
|
+
* broadcasts events to all open tabs. Falls back to direct EventSource
|
|
25
|
+
* when Service Workers are unavailable.
|
|
26
|
+
*/
|
|
23
27
|
export function useAgentEvents(options?: UseAgentEventsOptions): UseAgentEventsResult {
|
|
24
28
|
const [events, setEvents] = useState<NotificationEvent[]>([]);
|
|
25
29
|
const [lastEvent, setLastEvent] = useState<NotificationEvent | null>(null);
|
|
26
30
|
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
|
|
27
31
|
|
|
28
|
-
const backoffRef = useRef(BASE_BACKOFF_MS);
|
|
29
|
-
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
30
|
-
const stableTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
31
|
-
const esRef = useRef<EventSource | null>(null);
|
|
32
|
-
|
|
33
32
|
const runId = options?.runId;
|
|
33
|
+
const swRef = useRef<ServiceWorker | null>(null);
|
|
34
|
+
|
|
35
|
+
const onMessage = useCallback((event: MessageEvent) => {
|
|
36
|
+
const msg = event.data;
|
|
37
|
+
if (!msg || typeof msg !== 'object') return;
|
|
38
|
+
|
|
39
|
+
if (msg.type === 'notification') {
|
|
40
|
+
const parsed = msg.data as NotificationEvent;
|
|
41
|
+
setEvents((prev) => [...prev, parsed]);
|
|
42
|
+
setLastEvent(parsed);
|
|
43
|
+
} else if (msg.type === 'status') {
|
|
44
|
+
setConnectionStatus(msg.status as ConnectionStatus);
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (typeof window === 'undefined') return;
|
|
50
|
+
|
|
51
|
+
// ?sse=direct bypasses the Service Worker for debugging
|
|
52
|
+
const bypassSW =
|
|
53
|
+
typeof location !== 'undefined' && new URLSearchParams(location.search).has('sse');
|
|
54
|
+
|
|
55
|
+
// Fallback: direct EventSource when SW not supported or bypassed
|
|
56
|
+
if (!navigator.serviceWorker || bypassSW) {
|
|
57
|
+
return connectDirectEventSource(runId, setEvents, setLastEvent, setConnectionStatus);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
const fallbackCleanupRef = { current: undefined as (() => void) | undefined };
|
|
34
62
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
function subscribeToWorker(worker: ServiceWorker) {
|
|
64
|
+
if (cancelled) return;
|
|
65
|
+
swRef.current = worker;
|
|
66
|
+
worker.postMessage({ type: 'subscribe', runId });
|
|
67
|
+
setConnectionStatus('connecting');
|
|
38
68
|
}
|
|
39
69
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
// Listen for messages from whatever SW controls this page
|
|
71
|
+
navigator.serviceWorker.addEventListener('message', onMessage);
|
|
72
|
+
|
|
73
|
+
// Re-subscribe when SW controller changes (e.g., after SW update via skipWaiting)
|
|
74
|
+
function handleControllerChange() {
|
|
75
|
+
if (cancelled) return;
|
|
76
|
+
const newController = navigator.serviceWorker.controller;
|
|
77
|
+
if (newController) {
|
|
78
|
+
swRef.current = newController;
|
|
79
|
+
newController.postMessage({ type: 'subscribe', runId });
|
|
80
|
+
setConnectionStatus('connecting');
|
|
81
|
+
}
|
|
43
82
|
}
|
|
83
|
+
navigator.serviceWorker.addEventListener('controllerchange', handleControllerChange);
|
|
84
|
+
|
|
85
|
+
navigator.serviceWorker
|
|
86
|
+
.register(SW_PATH, { scope: '/' })
|
|
87
|
+
.then((registration) => {
|
|
88
|
+
if (cancelled) return;
|
|
89
|
+
|
|
90
|
+
// Use the active worker, or wait for it to activate
|
|
91
|
+
const sw = registration.active ?? registration.installing ?? registration.waiting;
|
|
92
|
+
if (!sw) {
|
|
93
|
+
// No worker at all — fall back to direct EventSource
|
|
94
|
+
fallbackCleanupRef.current = connectDirectEventSource(
|
|
95
|
+
runId,
|
|
96
|
+
setEvents,
|
|
97
|
+
setLastEvent,
|
|
98
|
+
setConnectionStatus
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (sw.state === 'activated') {
|
|
104
|
+
subscribeToWorker(sw);
|
|
105
|
+
} else {
|
|
106
|
+
sw.addEventListener('statechange', () => {
|
|
107
|
+
if (sw.state === 'activated') subscribeToWorker(sw);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.catch(() => {
|
|
112
|
+
// SW registration failed — fall back to direct EventSource
|
|
113
|
+
if (!cancelled) {
|
|
114
|
+
fallbackCleanupRef.current = connectDirectEventSource(
|
|
115
|
+
runId,
|
|
116
|
+
setEvents,
|
|
117
|
+
setLastEvent,
|
|
118
|
+
setConnectionStatus
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return () => {
|
|
124
|
+
cancelled = true;
|
|
125
|
+
navigator.serviceWorker.removeEventListener('message', onMessage);
|
|
126
|
+
navigator.serviceWorker.removeEventListener('controllerchange', handleControllerChange);
|
|
127
|
+
swRef.current?.postMessage({ type: 'unsubscribe' });
|
|
128
|
+
swRef.current = null;
|
|
129
|
+
fallbackCleanupRef.current?.();
|
|
130
|
+
};
|
|
131
|
+
}, [runId, onMessage]);
|
|
132
|
+
|
|
133
|
+
return { events, lastEvent, connectionStatus };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- EventSource fallback (identical to the previous implementation) ---
|
|
137
|
+
|
|
138
|
+
const BASE_BACKOFF_MS = 1000;
|
|
139
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
140
|
+
const STABLE_CONNECTION_MS = 5_000;
|
|
141
|
+
|
|
142
|
+
function connectDirectEventSource(
|
|
143
|
+
runId: string | undefined,
|
|
144
|
+
setEvents: React.Dispatch<React.SetStateAction<NotificationEvent[]>>,
|
|
145
|
+
setLastEvent: React.Dispatch<React.SetStateAction<NotificationEvent | null>>,
|
|
146
|
+
setConnectionStatus: React.Dispatch<React.SetStateAction<ConnectionStatus>>
|
|
147
|
+
): () => void {
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
149
|
+
if (typeof EventSource === 'undefined') return () => {};
|
|
150
|
+
|
|
151
|
+
let es: EventSource | null = null;
|
|
152
|
+
let stopped = false;
|
|
153
|
+
let backoff = BASE_BACKOFF_MS;
|
|
154
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
155
|
+
let stableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
156
|
+
|
|
157
|
+
function connect() {
|
|
158
|
+
if (stopped) return;
|
|
44
159
|
|
|
45
160
|
const url = runId
|
|
46
161
|
? `/api/agent-events?runId=${encodeURIComponent(runId)}`
|
|
47
162
|
: '/api/agent-events';
|
|
48
163
|
|
|
49
|
-
|
|
50
|
-
esRef.current = es;
|
|
164
|
+
es = new EventSource(url);
|
|
51
165
|
setConnectionStatus('connecting');
|
|
52
166
|
|
|
53
167
|
es.onopen = () => {
|
|
54
168
|
setConnectionStatus('connected');
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
backoffRef.current = BASE_BACKOFF_MS;
|
|
169
|
+
stableTimer = setTimeout(() => {
|
|
170
|
+
stableTimer = null;
|
|
171
|
+
backoff = BASE_BACKOFF_MS;
|
|
59
172
|
}, STABLE_CONNECTION_MS);
|
|
60
173
|
};
|
|
61
174
|
|
|
62
175
|
es.onerror = () => {
|
|
63
|
-
es
|
|
64
|
-
|
|
176
|
+
es?.close();
|
|
177
|
+
es = null;
|
|
65
178
|
setConnectionStatus('disconnected');
|
|
66
179
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
stableTimerRef.current = null;
|
|
180
|
+
if (stableTimer !== null) {
|
|
181
|
+
clearTimeout(stableTimer);
|
|
182
|
+
stableTimer = null;
|
|
71
183
|
}
|
|
72
184
|
|
|
73
|
-
const delay =
|
|
74
|
-
|
|
185
|
+
const delay = backoff;
|
|
186
|
+
backoff = Math.min(delay * 2, MAX_BACKOFF_MS);
|
|
75
187
|
|
|
76
|
-
|
|
77
|
-
|
|
188
|
+
reconnectTimer = setTimeout(() => {
|
|
189
|
+
reconnectTimer = null;
|
|
78
190
|
connect();
|
|
79
191
|
}, delay);
|
|
80
192
|
};
|
|
@@ -84,26 +196,23 @@ export function useAgentEvents(options?: UseAgentEventsOptions): UseAgentEventsR
|
|
|
84
196
|
setEvents((prev) => [...prev, parsed]);
|
|
85
197
|
setLastEvent(parsed);
|
|
86
198
|
}) as EventListener);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
connect();
|
|
199
|
+
}
|
|
91
200
|
|
|
92
|
-
|
|
93
|
-
if (reconnectTimerRef.current !== null) {
|
|
94
|
-
clearTimeout(reconnectTimerRef.current);
|
|
95
|
-
reconnectTimerRef.current = null;
|
|
96
|
-
}
|
|
97
|
-
if (stableTimerRef.current !== null) {
|
|
98
|
-
clearTimeout(stableTimerRef.current);
|
|
99
|
-
stableTimerRef.current = null;
|
|
100
|
-
}
|
|
101
|
-
if (esRef.current) {
|
|
102
|
-
esRef.current.close();
|
|
103
|
-
esRef.current = null;
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
}, [connect]);
|
|
201
|
+
connect();
|
|
107
202
|
|
|
108
|
-
return
|
|
203
|
+
return () => {
|
|
204
|
+
stopped = true;
|
|
205
|
+
if (reconnectTimer !== null) {
|
|
206
|
+
clearTimeout(reconnectTimer);
|
|
207
|
+
reconnectTimer = null;
|
|
208
|
+
}
|
|
209
|
+
if (stableTimer !== null) {
|
|
210
|
+
clearTimeout(stableTimer);
|
|
211
|
+
stableTimer = null;
|
|
212
|
+
}
|
|
213
|
+
if (es) {
|
|
214
|
+
es.close();
|
|
215
|
+
es = null;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
109
218
|
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
4
|
import { Toaster } from '@/components/ui/sonner';
|
|
5
5
|
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { useSound } from './use-sound';
|
|
6
7
|
|
|
7
8
|
/** Mirrors NotificationSeverity enum values (inlined to avoid @shepai/core import in Storybook). */
|
|
8
9
|
type Severity = 'info' | 'warning' | 'success' | 'error';
|
|
9
10
|
|
|
11
|
+
const SEVERITY_SOUNDS: Record<Severity, string> = {
|
|
12
|
+
success: 'celebration',
|
|
13
|
+
error: 'caution',
|
|
14
|
+
warning: 'notification',
|
|
15
|
+
info: 'button',
|
|
16
|
+
};
|
|
17
|
+
|
|
10
18
|
interface DemoEvent {
|
|
11
19
|
eventType: string;
|
|
12
20
|
severity: Severity;
|
|
@@ -15,16 +23,37 @@ interface DemoEvent {
|
|
|
15
23
|
phaseName?: string;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
function
|
|
19
|
-
const
|
|
20
|
-
|
|
26
|
+
function NotificationDemo() {
|
|
27
|
+
const successSound = useSound('celebration', { volume: 0.5 });
|
|
28
|
+
const errorSound = useSound('caution', { volume: 0.5 });
|
|
29
|
+
const warningSound = useSound('notification', { volume: 0.5 });
|
|
30
|
+
const infoSound = useSound('button', { volume: 0.5 });
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
const soundsByName = useMemo<Record<string, { play: () => void }>>(
|
|
33
|
+
() => ({
|
|
34
|
+
celebration: successSound,
|
|
35
|
+
caution: errorSound,
|
|
36
|
+
notification: warningSound,
|
|
37
|
+
button: infoSound,
|
|
38
|
+
}),
|
|
39
|
+
[successSound, errorSound, warningSound, infoSound]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const simulateNotification = useCallback(
|
|
43
|
+
(event: DemoEvent) => {
|
|
44
|
+
const method = event.severity ?? 'info';
|
|
45
|
+
toast[method](event.featureName, { description: event.message });
|
|
46
|
+
|
|
47
|
+
if (globalThis.Notification?.permission === 'granted') {
|
|
48
|
+
new Notification(event.featureName, { body: event.message });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const soundName = SEVERITY_SOUNDS[event.severity];
|
|
52
|
+
soundsByName[soundName]?.play();
|
|
53
|
+
},
|
|
54
|
+
[soundsByName]
|
|
55
|
+
);
|
|
26
56
|
|
|
27
|
-
function NotificationDemo() {
|
|
28
57
|
const [browserPermission, setBrowserPermission] = useState<NotificationPermission>(
|
|
29
58
|
typeof globalThis.Notification !== 'undefined' ? globalThis.Notification.permission : 'default'
|
|
30
59
|
);
|
|
@@ -52,7 +81,7 @@ function NotificationDemo() {
|
|
|
52
81
|
<div className="flex flex-col gap-2">
|
|
53
82
|
<h3 className="text-sm font-semibold">Simulate Notification Events</h3>
|
|
54
83
|
<p className="text-muted-foreground text-xs">
|
|
55
|
-
Click a button to fire a toast
|
|
84
|
+
Click a button to fire a toast, browser notification, and sound.
|
|
56
85
|
</p>
|
|
57
86
|
<div className="flex flex-wrap gap-2">
|
|
58
87
|
<Button
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { toast } from 'sonner';
|
|
5
5
|
import type { NotificationEvent } from '@shepai/core/domain/generated/output';
|
|
6
|
-
import { NotificationSeverity } from '@shepai/core/domain/generated/output';
|
|
6
|
+
import { NotificationEventType, NotificationSeverity } from '@shepai/core/domain/generated/output';
|
|
7
7
|
import { useAgentEventsContext } from './agent-events-provider';
|
|
8
|
+
import { useSound } from './use-sound';
|
|
8
9
|
|
|
9
10
|
export interface UseNotificationsResult {
|
|
10
11
|
requestBrowserPermission: () => Promise<void>;
|
|
@@ -20,18 +21,56 @@ const SEVERITY_TO_TOAST: Record<NotificationSeverity, 'success' | 'error' | 'war
|
|
|
20
21
|
|
|
21
22
|
function dispatchToast(event: NotificationEvent): void {
|
|
22
23
|
const method = SEVERITY_TO_TOAST[event.severity] ?? 'info';
|
|
23
|
-
|
|
24
|
+
const isActionable = event.eventType === NotificationEventType.WaitingApproval;
|
|
25
|
+
toast[method](event.featureName, {
|
|
26
|
+
description: event.message,
|
|
27
|
+
...(isActionable && {
|
|
28
|
+
action: {
|
|
29
|
+
label: 'Review',
|
|
30
|
+
onClick: () => {
|
|
31
|
+
window.dispatchEvent(
|
|
32
|
+
new CustomEvent('shep:select-feature', { detail: { featureId: event.featureId } })
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
function dispatchBrowserNotification(event: NotificationEvent): void {
|
|
27
41
|
if (globalThis.Notification?.permission !== 'granted') {
|
|
28
42
|
return;
|
|
29
43
|
}
|
|
30
|
-
new Notification(event.featureName
|
|
44
|
+
new Notification(`Shep: ${event.featureName}`, {
|
|
45
|
+
body: event.message,
|
|
46
|
+
icon: '/favicon-light.svg',
|
|
47
|
+
});
|
|
31
48
|
}
|
|
32
49
|
|
|
50
|
+
const SEVERITY_TO_SOUND = {
|
|
51
|
+
[NotificationSeverity.Success]: 'celebration',
|
|
52
|
+
[NotificationSeverity.Error]: 'caution',
|
|
53
|
+
[NotificationSeverity.Warning]: 'notification',
|
|
54
|
+
[NotificationSeverity.Info]: 'button',
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
33
57
|
export function useNotifications(): UseNotificationsResult {
|
|
34
|
-
const {
|
|
58
|
+
const { events } = useAgentEventsContext();
|
|
59
|
+
|
|
60
|
+
const successSound = useSound('celebration', { volume: 0.5 });
|
|
61
|
+
const errorSound = useSound('caution', { volume: 0.5 });
|
|
62
|
+
const warningSound = useSound('notification', { volume: 0.5 });
|
|
63
|
+
const infoSound = useSound('button', { volume: 0.5 });
|
|
64
|
+
|
|
65
|
+
const soundsByName = useMemo<Record<string, { play: () => void }>>(
|
|
66
|
+
() => ({
|
|
67
|
+
celebration: successSound,
|
|
68
|
+
caution: errorSound,
|
|
69
|
+
notification: warningSound,
|
|
70
|
+
button: infoSound,
|
|
71
|
+
}),
|
|
72
|
+
[successSound, errorSound, warningSound, infoSound]
|
|
73
|
+
);
|
|
35
74
|
|
|
36
75
|
const [browserPermissionState, setBrowserPermissionState] = useState<NotificationPermission>(
|
|
37
76
|
() => {
|
|
@@ -40,20 +79,34 @@ export function useNotifications(): UseNotificationsResult {
|
|
|
40
79
|
}
|
|
41
80
|
);
|
|
42
81
|
|
|
43
|
-
// Track
|
|
44
|
-
|
|
82
|
+
// Track how many events from the array we've already processed.
|
|
83
|
+
// Using the array index (instead of lastEvent) prevents React batching
|
|
84
|
+
// from silently dropping events when multiple SSE messages arrive together.
|
|
85
|
+
const processedCountRef = useRef(0);
|
|
45
86
|
|
|
46
87
|
useEffect(() => {
|
|
47
|
-
if (
|
|
88
|
+
if (events.length <= processedCountRef.current) return;
|
|
48
89
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (processedRef.current.has(key)) return;
|
|
52
|
-
processedRef.current.add(key);
|
|
90
|
+
const newEvents = events.slice(processedCountRef.current);
|
|
91
|
+
processedCountRef.current = events.length;
|
|
53
92
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
93
|
+
for (const event of newEvents) {
|
|
94
|
+
// Only notify for actionable events and completion celebrations
|
|
95
|
+
if (
|
|
96
|
+
event.severity !== NotificationSeverity.Error &&
|
|
97
|
+
event.severity !== NotificationSeverity.Warning &&
|
|
98
|
+
event.severity !== NotificationSeverity.Success
|
|
99
|
+
) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
dispatchToast(event);
|
|
104
|
+
dispatchBrowserNotification(event);
|
|
105
|
+
|
|
106
|
+
const soundName = SEVERITY_TO_SOUND[event.severity];
|
|
107
|
+
soundsByName[soundName]?.play();
|
|
108
|
+
}
|
|
109
|
+
}, [events, soundsByName]);
|
|
57
110
|
|
|
58
111
|
const requestBrowserPermission = useCallback(async () => {
|
|
59
112
|
if (typeof globalThis.Notification === 'undefined') return;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = 'shep-sound-enabled';
|
|
6
|
+
const SYNC_EVENT = 'shep:sound-toggle';
|
|
7
|
+
|
|
8
|
+
export interface UseSoundEnabledResult {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
toggle: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useSoundEnabled(): UseSoundEnabledResult {
|
|
14
|
+
const [enabled, setEnabled] = useState(true);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
18
|
+
if (stored === 'false') setEnabled(false);
|
|
19
|
+
|
|
20
|
+
// Sync across all useSoundEnabled instances in the same tab
|
|
21
|
+
const onSync = (e: Event) => {
|
|
22
|
+
setEnabled((e as CustomEvent<boolean>).detail);
|
|
23
|
+
};
|
|
24
|
+
window.addEventListener(SYNC_EVENT, onSync);
|
|
25
|
+
return () => window.removeEventListener(SYNC_EVENT, onSync);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const toggle = useCallback(() => {
|
|
29
|
+
setEnabled((prev) => {
|
|
30
|
+
const next = !prev;
|
|
31
|
+
localStorage.setItem(STORAGE_KEY, String(next));
|
|
32
|
+
window.dispatchEvent(new CustomEvent(SYNC_EVENT, { detail: next }));
|
|
33
|
+
return next;
|
|
34
|
+
});
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
return { enabled, toggle };
|
|
38
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { useSound, SOUND_NAMES, type SoundName } from './use-sound';
|
|
5
|
+
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
/* Helpers */
|
|
8
|
+
/* ------------------------------------------------------------------ */
|
|
9
|
+
|
|
10
|
+
const CATEGORIES: Record<string, SoundName[]> = {
|
|
11
|
+
'UI Actions': ['button', 'select', 'disabled'],
|
|
12
|
+
Toggles: ['toggle_on', 'toggle_off'],
|
|
13
|
+
Transitions: ['transition_up', 'transition_down'],
|
|
14
|
+
Alerts: ['notification', 'caution', 'celebration'],
|
|
15
|
+
Swipes: ['swipe', 'swipe_01', 'swipe_02', 'swipe_03', 'swipe_04', 'swipe_05'],
|
|
16
|
+
Taps: ['tap_01', 'tap_02', 'tap_03', 'tap_04', 'tap_05'],
|
|
17
|
+
Typing: ['type_01', 'type_02', 'type_03', 'type_04', 'type_05'],
|
|
18
|
+
Loops: ['progress_loop', 'ringtone_loop'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/* ------------------------------------------------------------------ */
|
|
22
|
+
/* Single sound player (used per button) */
|
|
23
|
+
/* ------------------------------------------------------------------ */
|
|
24
|
+
|
|
25
|
+
function SoundButton({ name, volume }: { name: SoundName; volume: number }) {
|
|
26
|
+
const isLoop = name.includes('loop');
|
|
27
|
+
const { play, stop } = useSound(name, { volume, loop: isLoop });
|
|
28
|
+
const [playing, setPlaying] = useState(false);
|
|
29
|
+
|
|
30
|
+
const handleClick = () => {
|
|
31
|
+
if (isLoop && playing) {
|
|
32
|
+
stop();
|
|
33
|
+
setPlaying(false);
|
|
34
|
+
} else {
|
|
35
|
+
play();
|
|
36
|
+
setPlaying(true);
|
|
37
|
+
if (!isLoop) {
|
|
38
|
+
setTimeout(() => setPlaying(false), 2000);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Button
|
|
45
|
+
size="sm"
|
|
46
|
+
variant={playing ? 'default' : 'outline'}
|
|
47
|
+
onClick={handleClick}
|
|
48
|
+
className="min-w-[120px] font-mono text-xs"
|
|
49
|
+
>
|
|
50
|
+
{isLoop && playing ? `Stop ${name}` : name}
|
|
51
|
+
</Button>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ------------------------------------------------------------------ */
|
|
56
|
+
/* Full catalog demo */
|
|
57
|
+
/* ------------------------------------------------------------------ */
|
|
58
|
+
|
|
59
|
+
function SoundCatalog({ volume }: { volume: number }) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex w-[560px] flex-col gap-6">
|
|
62
|
+
<div className="flex flex-col gap-1">
|
|
63
|
+
<h2 className="text-base font-semibold">Sound Library</h2>
|
|
64
|
+
<p className="text-muted-foreground text-sm">
|
|
65
|
+
{SOUND_NAMES.length} sounds available at{' '}
|
|
66
|
+
<code className="bg-muted rounded px-1 py-0.5 text-xs">/sounds/*.wav</code>
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{Object.entries(CATEGORIES).map(([category, sounds]) => (
|
|
71
|
+
<div key={category} className="flex flex-col gap-2">
|
|
72
|
+
<h3 className="text-sm font-semibold">{category}</h3>
|
|
73
|
+
<div className="flex flex-wrap gap-2">
|
|
74
|
+
{sounds.map((name) => (
|
|
75
|
+
<SoundButton key={name} name={name} volume={volume} />
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ------------------------------------------------------------------ */
|
|
85
|
+
/* Storybook meta */
|
|
86
|
+
/* ------------------------------------------------------------------ */
|
|
87
|
+
|
|
88
|
+
const meta: Meta = {
|
|
89
|
+
title: 'Hooks/useSound',
|
|
90
|
+
parameters: { layout: 'centered' },
|
|
91
|
+
tags: ['autodocs'],
|
|
92
|
+
argTypes: {
|
|
93
|
+
volume: {
|
|
94
|
+
control: { type: 'range', min: 0, max: 1, step: 0.1 },
|
|
95
|
+
description: 'Playback volume (0 to 1)',
|
|
96
|
+
defaultValue: 0.5,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
args: {
|
|
100
|
+
volume: 0.5,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default meta;
|
|
105
|
+
type Story = StoryObj<{ volume: number }>;
|
|
106
|
+
|
|
107
|
+
export const Catalog: Story = {
|
|
108
|
+
render: ({ volume }) => <SoundCatalog volume={volume} />,
|
|
109
|
+
};
|