fleetlens 0.4.0 → 0.6.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/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/app-path-routes-manifest.json +16 -7
- package/app/apps/web/.next/build-manifest.json +7 -7
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/required-server-files.json +2 -1
- package/app/apps/web/.next/routes-manifest.json +86 -24
- package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/_global-error/page.js +1 -1
- package/app/apps/web/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +7 -7
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/_not-found/page.js +5 -4
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +9 -0
- package/app/apps/web/.next/server/app/api/{insights/saved/[key] → digest/day/[date]}/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +9 -0
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/digest/month-index/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js +7 -0
- package/app/apps/web/.next/server/app/api/{insights/months-index → digest/month-index}/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → digest/month-index}/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +9 -0
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/digest/week-index/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js +7 -0
- package/app/apps/web/.next/server/app/api/{insights/weeks-index → digest/week-index}/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/jobs/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/jobs/route.js +6 -0
- package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/jobs/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js +6 -0
- package/app/apps/web/.next/server/app/api/{insights/saved → runs/[runId]}/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js +6 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → runs/[runId]/stream}/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/runs/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/runs/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/runs/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/runs/route.js +6 -0
- package/app/apps/web/.next/server/app/api/runs/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/{insights → runs}/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/settings/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/settings/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/settings/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/settings/route.js +10 -0
- package/app/apps/web/.next/server/app/api/settings/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/{insights → settings}/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/{insights/print/[key] → day/[date]}/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/day/[date]/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/day/[date]/page.js +18 -0
- package/app/apps/web/.next/server/app/day/[date]/page.js.map +5 -0
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/day/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/day/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/day/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/day/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/day/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/{insights/print/[key] → day}/page.js +11 -10
- package/app/apps/web/.next/server/app/day/page.js.map +5 -0
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/digest/[date]/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/digest/[date]/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/digest/[date]/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/digest/[date]/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/digest/[date]/page.js +17 -0
- package/app/apps/web/.next/server/app/digest/[date]/page.js.map +5 -0
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/digest/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/digest/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/digest/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/digest/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/digest/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/digest/page.js +17 -0
- package/app/apps/web/.next/server/app/digest/page.js.map +5 -0
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/insights/[key]/page.js +7 -5
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/insights/page.js +7 -5
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/page.js +7 -6
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/parallelism/page.js +6 -5
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +6 -5
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/projects/page.js +5 -4
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/runs/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/runs/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/runs/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/runs/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/runs/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/runs/page.js +17 -0
- package/app/apps/web/.next/server/app/runs/page.js.map +5 -0
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +5 -4
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/sessions/page.js +5 -4
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/settings/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/settings/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/settings/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/settings/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/settings/page.js +18 -0
- package/app/apps/web/.next/server/app/settings/page.js.map +5 -0
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/usage/page/build-manifest.json +4 -4
- package/app/apps/web/.next/server/app/usage/page.js +6 -5
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app-paths-manifest.json +16 -7
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_digest_week_[startDate]_route_actions_0klqqsh.js +3 -0
- package/app/apps/web/.next/server/chunks/09og__next-internal_server_app_api_digest_month_[yearMonth]_route_actions_0i4ielu.js +3 -0
- package/app/apps/web/.next/server/chunks/[externals]__0o43ejh._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0-vgrh1._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__00n3f9y._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__02zukjw._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__067~im1._.js +6 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__06b4cj-._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_8zh-i._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e8n_x4._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ej_p3s._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +9 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_digest_day_[date]_route_actions_09sxie5.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_digest_month-index_route_actions_0wldpbu.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_digest_week-index_route_actions_12c2wj2.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_jobs_route_actions_08kx~8-.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_runs_[runId]_route_actions_0dfcghc.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_runs_[runId]_stream_route_actions_0.8e8-k.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_runs_route_actions_0xyx82f.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_settings_route_actions_0.xv7sx.js +3 -0
- package/app/apps/web/.next/server/chunks/node_modules_next_036my~r._.js +49 -0
- package/app/apps/web/.next/server/chunks/packages_0e7q9u_._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_0n6p2ot._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +379 -0
- package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-1_njm._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-94ji~._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__13xpdx0._.js → [root-of-the-server]__03uoeiw._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_6.inc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0d~2~le._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__08.d5ia._.js → [root-of-the-server]__0jdyefm._.js} +10 -10
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ln9jsw._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0z45d0g._.js → [root-of-the-server]__0lz4a~g._.js} +2 -48
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o1_g.~._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0euu8sa._.js → [root-of-the-server]__0u39afl._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0u5d7de._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wwa6gh._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0xy1qim._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~_8vff._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10srpu0._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__11v.eqw._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/_0fjbf--._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0jw2sm7._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0wj750k._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0woap-3._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{_0xies90._.js → _0xu2pvc._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_day_[date]_page_actions_0u2kxb..js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_day_page_actions_1042maj.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_digest_[date]_page_actions_0olu967.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_digest_page_actions_0bl.eil.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_runs_page_actions_0485qy7.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_settings_page_actions_0f6epye.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_settings_ai-features-form_tsx_01p699p._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0b-4-4j._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +6 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +16 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0xs658s._.js +10 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_runs-live-board_tsx_0r6dkdq._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_usage-charts-dashboard_tsx_0bgr_uv._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_usage-data_ts_0xtv99n._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_0i2xw~e._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0d3d4wn.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0edz1ko.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0eq.n-n.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0vz7_px.js → node_modules_next_dist_esm_build_templates_app-page_0hcvt-s.js} +3 -3
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0nd5zu0.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_11j1qy0.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12n1lw_.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
- package/app/apps/web/.next/server/functions-config-manifest.json +11 -6
- package/app/apps/web/.next/server/middleware-build-manifest.js +7 -7
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/0-yc2_pjxwwuq.js +1 -0
- package/app/apps/web/.next/static/chunks/0257pdz1-imal.js +2 -0
- package/app/apps/web/.next/static/chunks/05m.oz3kmf1p7.js +1 -0
- package/app/apps/web/.next/static/chunks/{0pqt~8bl3ukh4.js → 073081qv4uyn8.js} +1 -1
- package/app/apps/web/.next/static/chunks/{0018dy-m-kusj.js → 07lbm-sdh9c2j.js} +1 -1
- package/app/apps/web/.next/static/chunks/0858bgw2i4vob.js +1 -0
- package/app/apps/web/.next/static/chunks/0_7303jr0sf0w.js +10 -0
- package/app/apps/web/.next/static/chunks/0byz8fqw.exef.js +1 -0
- package/app/apps/web/.next/static/chunks/0coa1800xqgfj.js +1 -0
- package/app/apps/web/.next/static/chunks/0fs3gxea5h0s3.css +1 -0
- package/app/apps/web/.next/static/chunks/0k1psab.bybh9.js +14 -0
- package/app/apps/web/.next/static/chunks/0k9pksi_i.odf.js +1 -0
- package/app/apps/web/.next/static/chunks/0m62vx92~0_0d.js +1 -0
- package/app/apps/web/.next/static/chunks/0mc8aa8z.0-5l.js +8 -0
- package/app/apps/web/.next/static/chunks/0mykrrv810ra-.js +31 -0
- package/app/apps/web/.next/static/chunks/0o9ayt8sknubd.js +1 -0
- package/app/apps/web/.next/static/chunks/0osdgl2.9cl1q.js +6 -0
- package/app/apps/web/.next/static/chunks/0xfu09fn_358k.js +4 -0
- package/app/apps/web/.next/static/chunks/171za22fv684..js +6 -0
- package/app/apps/web/.next/static/chunks/17u2ww8ya5dv1.js +6 -0
- package/app/apps/web/.next/static/chunks/{turbopack-169w-1._23ra_.js → turbopack-0_p36yduv.waj.js} +1 -1
- package/app/apps/web/app/api/digest/day/[date]/route.ts +152 -0
- package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +147 -0
- package/app/apps/web/app/api/digest/month-index/route.ts +103 -0
- package/app/apps/web/app/api/digest/week/[startDate]/route.ts +149 -0
- package/app/apps/web/app/api/{insights/weeks-index → digest/week-index}/route.ts +45 -33
- package/app/apps/web/app/api/jobs/route.ts +8 -0
- package/app/apps/web/app/api/runs/[runId]/route.ts +64 -0
- package/app/apps/web/app/api/runs/[runId]/stream/route.ts +154 -0
- package/app/apps/web/app/api/runs/route.ts +266 -0
- package/app/apps/web/app/api/settings/route.ts +35 -0
- package/app/apps/web/app/day/[date]/day-view.tsx +193 -0
- package/app/apps/web/app/day/[date]/page.tsx +175 -0
- package/app/apps/web/app/day/page.tsx +8 -0
- package/app/apps/web/app/digest/[date]/page.tsx +15 -0
- package/app/apps/web/app/digest/page.tsx +7 -0
- package/app/apps/web/app/insights/[key]/page.tsx +106 -48
- package/app/apps/web/app/insights/page.tsx +133 -3
- package/app/apps/web/app/layout.tsx +23 -3
- package/app/apps/web/app/page.tsx +33 -4
- package/app/apps/web/app/parallelism/gantt-chart.tsx +67 -1
- package/app/apps/web/app/parallelism/page.tsx +8 -278
- package/app/apps/web/app/projects/[slug]/page.tsx +153 -0
- package/app/apps/web/app/projects/page.tsx +49 -4
- package/app/apps/web/app/projects/projects-view.tsx +79 -68
- package/app/apps/web/app/runs/page.tsx +21 -0
- package/app/apps/web/app/sessions/[id]/page.tsx +12 -1
- package/app/apps/web/app/sessions/[id]/session-view.tsx +569 -21
- package/app/apps/web/app/sessions/page.tsx +19 -3
- package/app/apps/web/app/sessions/sessions-grid.tsx +172 -131
- package/app/apps/web/app/settings/ai-features-form.tsx +45 -0
- package/app/apps/web/app/settings/page.tsx +20 -0
- package/app/apps/web/app/usage/page.tsx +45 -3
- package/app/apps/web/components/auto-generate-yesterday.tsx +96 -0
- package/app/apps/web/components/backfill-drawer.tsx +309 -0
- package/app/apps/web/components/dashboard-view.tsx +4 -2
- package/app/apps/web/components/date-nav.tsx +385 -0
- package/app/apps/web/components/day-digest-view.tsx +180 -0
- package/app/apps/web/components/day-digest.tsx +489 -0
- package/app/apps/web/components/flag-chip.tsx +54 -0
- package/app/apps/web/components/goal-bar.tsx +63 -0
- package/app/apps/web/components/heatmap.tsx +78 -1
- package/app/apps/web/components/helpfulness-sparkline.tsx +82 -0
- package/app/apps/web/components/insights-history.tsx +211 -0
- package/app/apps/web/components/insights-top-bar.tsx +216 -0
- package/app/apps/web/components/job-queue-widget.tsx +274 -0
- package/app/apps/web/components/live-sessions-widget.tsx +38 -3
- package/app/apps/web/components/month-digest-view.tsx +138 -0
- package/app/apps/web/components/month-digest.tsx +216 -0
- package/app/apps/web/components/outcome-mix-row.tsx +37 -0
- package/app/apps/web/components/outcome-pill.tsx +101 -0
- package/app/apps/web/components/previous-cycles-trend.tsx +155 -0
- package/app/apps/web/components/recent-days-panel.tsx +83 -0
- package/app/apps/web/components/runs-live-board.tsx +513 -0
- package/app/apps/web/components/sidebar.tsx +38 -4
- package/app/apps/web/components/usage-chart-range.tsx +58 -6
- package/app/apps/web/components/usage-chart.tsx +94 -7
- package/app/apps/web/components/usage-charts-dashboard.tsx +20 -1
- package/app/apps/web/components/week-digest-view.tsx +226 -0
- package/app/apps/web/components/week-digest.tsx +2128 -0
- package/app/apps/web/components/yesterday-hero.tsx +110 -0
- package/app/apps/web/lib/auto-week-fire.ts +37 -0
- package/app/apps/web/lib/calibration-data.ts +171 -0
- package/app/apps/web/lib/entries-index.ts +174 -0
- package/app/apps/web/lib/entries.ts +72 -0
- package/app/apps/web/lib/inflight-coalesce.test.ts +43 -0
- package/app/apps/web/lib/inflight-coalesce.ts +19 -0
- package/app/apps/web/lib/jobs.ts +170 -0
- package/app/apps/web/lib/usage-data.ts +37 -0
- package/app/apps/web/lib/validate-settings.test.ts +25 -0
- package/app/apps/web/lib/validate-settings.ts +9 -0
- package/app/apps/web/next.config.ts +1 -1
- package/app/apps/web/package.json +4 -2
- package/app/apps/web/server.js +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/app/apps/web/vitest.config.ts +9 -0
- package/dist/daemon-worker.js +1758 -713
- package/dist/index.js +11308 -1888
- package/package.json +4 -1
- package/app/apps/web/.next/server/app/api/insights/months-index/route/app-paths-manifest.json +0 -3
- package/app/apps/web/.next/server/app/api/insights/months-index/route.js +0 -7
- package/app/apps/web/.next/server/app/api/insights/months-index/route_client-reference-manifest.js +0 -3
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/app-paths-manifest.json +0 -3
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js +0 -6
- package/app/apps/web/.next/server/app/api/insights/route/app-paths-manifest.json +0 -3
- package/app/apps/web/.next/server/app/api/insights/route.js +0 -8
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/app-paths-manifest.json +0 -3
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js +0 -6
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route_client-reference-manifest.js +0 -3
- package/app/apps/web/.next/server/app/api/insights/saved/route/app-paths-manifest.json +0 -3
- package/app/apps/web/.next/server/app/api/insights/saved/route.js +0 -6
- package/app/apps/web/.next/server/app/api/insights/saved/route_client-reference-manifest.js +0 -3
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route/app-paths-manifest.json +0 -3
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js +0 -7
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route_client-reference-manifest.js +0 -3
- package/app/apps/web/.next/server/app/insights/print/[key]/page/app-paths-manifest.json +0 -3
- package/app/apps/web/.next/server/app/insights/print/[key]/page.js.nft.json +0 -1
- package/app/apps/web/.next/server/app/insights/print/[key]/page_client-reference-manifest.js +0 -3
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_months-index_route_actions_0~h~zci.js +0 -3
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_saved_[key]_route_actions_0a5j9yw.js +0 -3
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_weeks-index_route_actions_0hh-1c7.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.hmj0z._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__01h_g-p._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nh-z10._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ri7q~r._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0rr1~~x._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0s0gv.0._.js +0 -3
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_pdf_[key]_route_actions_0lu-lnx.js +0 -3
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_route_actions_0h8l5lz.js +0 -3
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_saved_route_actions_13rlblw.js +0 -3
- package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_00t2xj6.js +0 -68
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0.tsxpj._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08pen0k._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b323~k._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gx-4qa._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wv27wt._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13itnvx._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/_09n75_5._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/_0r~__0v._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_print_[key]_page_actions_0a853fw.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_parallelism_08vub.0._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0bbo_pd._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insight-report_tsx_13q2a-c._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insights-view_tsx_11730ta._.js +0 -6
- package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0nqz8dw._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0t-hj0x._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_analytics_0k4dgst.js +0 -3
- package/app/apps/web/.next/static/chunks/028_pmk40b69_.js +0 -6
- package/app/apps/web/.next/static/chunks/03.mv4y4.w-vb.js +0 -6
- package/app/apps/web/.next/static/chunks/03_7xtpi9sv4g.js +0 -4
- package/app/apps/web/.next/static/chunks/03g5dw5eskotd.js +0 -6
- package/app/apps/web/.next/static/chunks/063oimt30jeo9.js +0 -10
- package/app/apps/web/.next/static/chunks/08c1pcrocqq5s.js +0 -6
- package/app/apps/web/.next/static/chunks/0e-wrhqh4h3r5.js +0 -2
- package/app/apps/web/.next/static/chunks/0fc6dk2qtokwh.js +0 -1
- package/app/apps/web/.next/static/chunks/0i7er7_bfab36.js +0 -10
- package/app/apps/web/.next/static/chunks/0ipytyi01h0ta.js +0 -1
- package/app/apps/web/.next/static/chunks/0lsznd4_avzhv.js +0 -6
- package/app/apps/web/.next/static/chunks/0lty3v_i9mde7.js +0 -1
- package/app/apps/web/.next/static/chunks/0vekjjj4pidlz.css +0 -1
- package/app/apps/web/.next/static/chunks/137uw2-qbro07.js +0 -31
- package/app/apps/web/app/api/insights/months-index/route.ts +0 -80
- package/app/apps/web/app/api/insights/pdf/[key]/route.ts +0 -45
- package/app/apps/web/app/api/insights/route.ts +0 -508
- package/app/apps/web/app/api/insights/saved/[key]/route.ts +0 -11
- package/app/apps/web/app/api/insights/saved/route.ts +0 -9
- package/app/apps/web/app/insights/print/[key]/page.tsx +0 -73
- package/app/apps/web/components/insight-report.tsx +0 -1031
- package/app/apps/web/components/insights-view.tsx +0 -647
- package/app/apps/web/lib/ai/insights-prompt.ts +0 -71
- package/app/apps/web/lib/ai/saved-reports.ts +0 -83
- /package/app/apps/web/.next/server/app/api/{insights/months-index → digest/day/[date]}/route/build-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/months-index → digest/day/[date]}/route/server-reference-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/months-index → digest/day/[date]}/route.js.map +0 -0
- /package/app/apps/web/.next/server/app/api/{insights → digest/month/[yearMonth]}/route/build-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights → digest/month/[yearMonth]}/route/server-reference-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights → digest/month/[yearMonth]}/route.js.map +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → digest/month-index}/route/build-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → digest/month-index}/route/server-reference-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → digest/month-index}/route.js.map +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/saved → digest/week/[startDate]}/route/build-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/saved → digest/week/[startDate]}/route/server-reference-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/saved → digest/week/[startDate]}/route.js.map +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/saved/[key] → digest/week-index}/route/build-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/saved/[key] → digest/week-index}/route/server-reference-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/saved/[key] → digest/week-index}/route.js.map +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/weeks-index → jobs}/route/build-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/weeks-index → jobs}/route/server-reference-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/api/{insights/weeks-index → jobs}/route.js.map +0 -0
- /package/app/apps/web/.next/server/app/{insights/print/[key]/page → api/runs/[runId]/route}/server-reference-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/{insights/print/[key]/page.js.map → api/runs/[runId]/route.js.map} +0 -0
- /package/app/apps/web/.next/server/app/{insights/print/[key] → day/[date]}/page/next-font-manifest.json +0 -0
- /package/app/apps/web/.next/server/app/{insights/print/[key] → day/[date]}/page/react-loadable-manifest.json +0 -0
- /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 8tTWPvUhtatZEoocIhamh}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 8tTWPvUhtatZEoocIhamh}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 8tTWPvUhtatZEoocIhamh}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,2128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useState, type ReactNode } from "react";
|
|
5
|
+
import { Check, Copy } from "lucide-react";
|
|
6
|
+
import type { WeekDigest as WeekDigestType, DayHelpfulness, WeekTopSession, SessionPin } from "@claude-lens/entries";
|
|
7
|
+
import { renderWithFlagChips } from "./flag-chip";
|
|
8
|
+
import { GoalBar } from "./goal-bar";
|
|
9
|
+
|
|
10
|
+
const HELP_COLORS: Record<NonNullable<DayHelpfulness>, string> = {
|
|
11
|
+
essential: "#48bb78",
|
|
12
|
+
helpful: "#4299e1",
|
|
13
|
+
neutral: "#a0aec0",
|
|
14
|
+
unhelpful: "#f56565",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
18
|
+
|
|
19
|
+
const SHIPPED_COLLAPSE_THRESHOLD = 10;
|
|
20
|
+
const PROJECT_MIN_SHARE = 5;
|
|
21
|
+
|
|
22
|
+
const SHAPE_LABELS: Record<string, string> = {
|
|
23
|
+
"spec-review-loop": "Spec-review loop",
|
|
24
|
+
"chunk-implementation": "Chunk implementation",
|
|
25
|
+
"research-then-build": "Research-then-build",
|
|
26
|
+
"reviewer-triad": "Reviewer triad",
|
|
27
|
+
"background-coordinated": "Background-coordinated",
|
|
28
|
+
"solo-continuation": "Solo continuation",
|
|
29
|
+
"solo-design": "Solo design",
|
|
30
|
+
"solo-build": "Solo build",
|
|
31
|
+
"mixed": "Mixed",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SHAPE_COLORS: Record<string, string> = {
|
|
35
|
+
"spec-review-loop": "#9f7aea",
|
|
36
|
+
"chunk-implementation": "#4299e1",
|
|
37
|
+
"research-then-build": "#38b2ac",
|
|
38
|
+
"reviewer-triad": "#ed64a6",
|
|
39
|
+
"background-coordinated": "#ecc94b",
|
|
40
|
+
"solo-continuation": "#a0aec0",
|
|
41
|
+
"solo-design": "#48bb78",
|
|
42
|
+
"solo-build": "#ed8936",
|
|
43
|
+
"mixed": "#a0aec0",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const SHAPE_DESCRIPTIONS: Record<string, string> = {
|
|
47
|
+
"spec-review-loop": "Write a spec → dispatch reviewer subagents → revise → re-review → implement.",
|
|
48
|
+
"chunk-implementation": "Numbered Chunk/Task subagent dispatches with paired reviewers per chunk.",
|
|
49
|
+
"research-then-build": "Explore/researcher subagents map terrain upfront, then build with a clear picture.",
|
|
50
|
+
"reviewer-triad": "Three reviewer dispatches with distinct lenses (reuse / quality / efficiency) on the same diff.",
|
|
51
|
+
"background-coordinated": "A background subagent runs in parallel with the foreground session.",
|
|
52
|
+
"solo-continuation": "No subagents; first input was \"continue\" picking up from a prior session.",
|
|
53
|
+
"solo-design": "No subagents; loaded the brainstorming/writing-plans skill — design or planning session.",
|
|
54
|
+
"solo-build": "No subagents; direct hands-on work.",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const SURPRISE_LABELS: Record<string, string> = {
|
|
58
|
+
"outlier": "Outlier",
|
|
59
|
+
"novel-use": "Novel use",
|
|
60
|
+
"user-built-tool": "User-built tool",
|
|
61
|
+
"cross-week-contrast": "Cross-week contrast",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const LEAN_KIND_LABELS: Record<string, string> = {
|
|
65
|
+
"claude-md": "CLAUDE.md addition",
|
|
66
|
+
"skill": "Skill to build",
|
|
67
|
+
"hook": "Hook to wire",
|
|
68
|
+
"harness": "Harness extension",
|
|
69
|
+
"decision": "Decision to make",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const FRAME_LABELS: Record<string, string> = {
|
|
73
|
+
"teammate": "<teammate-message>",
|
|
74
|
+
"task-notification": "<task-notification>",
|
|
75
|
+
"local-command-caveat": "<local-command-caveat>",
|
|
76
|
+
"slash-command": "Slash command (<command-name>)",
|
|
77
|
+
"image-attached": "[Image #N]",
|
|
78
|
+
"handoff-prose": "Handoff prose",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const FRAME_HELP: Record<string, string> = {
|
|
82
|
+
"teammate": "From Claude's agent-teams feature — coordinator dispatching to a teammate role.",
|
|
83
|
+
"task-notification": "From Claude's Monitor tool — the user's auto-monitor triggers a session.",
|
|
84
|
+
"local-command-caveat": "Claude convention wrapping local-command output to prevent prompt-injection.",
|
|
85
|
+
"slash-command": "A custom or stock slash-command invoked by the user.",
|
|
86
|
+
"image-attached": "User opened with one or more screenshots.",
|
|
87
|
+
"handoff-prose": "Personal habit: cross-session compaction prompt copied from a prior session.",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function WeekDigest({
|
|
91
|
+
digest, aiEnabled, actions, priorDigest,
|
|
92
|
+
}: {
|
|
93
|
+
digest: WeekDigestType;
|
|
94
|
+
aiEnabled: boolean;
|
|
95
|
+
actions?: ReactNode;
|
|
96
|
+
priorDigest?: WeekDigestType | null;
|
|
97
|
+
}) {
|
|
98
|
+
const totalShipped = digest.shipped.length;
|
|
99
|
+
const dayCount = Object.values(digest.outcome_mix).reduce((a, b) => a + (b ?? 0), 0);
|
|
100
|
+
const hrs = digest.agent_min_total / 60;
|
|
101
|
+
const timeStr = hrs >= 1 ? `${hrs.toFixed(1)}h` : `${Math.round(digest.agent_min_total)}m`;
|
|
102
|
+
const delta = priorDigest ? buildDelta(digest, priorDigest) : null;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div style={{ maxWidth: 980, margin: "0 auto", padding: "24px 40px" }}>
|
|
106
|
+
<header style={{ marginBottom: 24 }}>
|
|
107
|
+
<div style={{
|
|
108
|
+
display: "flex", alignItems: "center", gap: 10, marginBottom: 8,
|
|
109
|
+
fontSize: 12, color: "var(--af-text-tertiary)", fontWeight: 500,
|
|
110
|
+
flexWrap: "wrap",
|
|
111
|
+
}}>
|
|
112
|
+
<span style={{ fontFamily: "var(--font-mono)", color: "var(--af-text-secondary)" }}>
|
|
113
|
+
{timeStr} agent time · {dayCount} active day{dayCount === 1 ? "" : "s"} · {totalShipped} PR{totalShipped === 1 ? "" : "s"}
|
|
114
|
+
</span>
|
|
115
|
+
{delta && (
|
|
116
|
+
<span style={{
|
|
117
|
+
fontFamily: "var(--font-mono)", fontSize: 11,
|
|
118
|
+
color: "var(--af-text-tertiary)",
|
|
119
|
+
padding: "1px 7px", borderRadius: 999,
|
|
120
|
+
border: "1px solid var(--af-border-subtle)",
|
|
121
|
+
}} title="vs the prior calendar week">
|
|
122
|
+
vs last week: {delta}
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
{actions && (
|
|
126
|
+
<div style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 8 }}>
|
|
127
|
+
{actions}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
{digest.headline ? (
|
|
132
|
+
<h1 style={{
|
|
133
|
+
fontSize: 26, fontWeight: 700, lineHeight: 1.3, letterSpacing: "-0.02em",
|
|
134
|
+
margin: "0 0 6px", maxWidth: 820, color: "var(--af-text)",
|
|
135
|
+
}}>
|
|
136
|
+
{renderWithFlagChips(digest.headline)}
|
|
137
|
+
</h1>
|
|
138
|
+
) : (
|
|
139
|
+
<h1 style={{ fontSize: 20, color: "var(--af-text-secondary)", margin: "0 0 14px" }}>
|
|
140
|
+
{aiEnabled
|
|
141
|
+
? "No narrative yet."
|
|
142
|
+
: `Worked ${timeStr} across ${digest.projects.length} project${digest.projects.length === 1 ? "" : "s"}.`}
|
|
143
|
+
</h1>
|
|
144
|
+
)}
|
|
145
|
+
{digest.key_pattern && (
|
|
146
|
+
<p style={{
|
|
147
|
+
fontSize: 13, fontStyle: "italic", margin: 0, lineHeight: 1.5,
|
|
148
|
+
color: "var(--af-text-secondary)", maxWidth: 820,
|
|
149
|
+
}}>
|
|
150
|
+
{renderWithFlagChips(digest.key_pattern)}
|
|
151
|
+
</p>
|
|
152
|
+
)}
|
|
153
|
+
</header>
|
|
154
|
+
|
|
155
|
+
{!aiEnabled && (
|
|
156
|
+
<div style={{
|
|
157
|
+
padding: 14, marginBottom: 24, background: "var(--af-accent-subtle)",
|
|
158
|
+
borderRadius: 8, fontSize: 13, color: "var(--af-text)",
|
|
159
|
+
}}>
|
|
160
|
+
Enable AI features in <Link href="/settings" style={{ color: "var(--af-accent)" }}>Settings</Link> to see weekly narratives.
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* ── §1: This week at a glance — totals, time distribution, goal mix ── */}
|
|
165
|
+
<WeekStatsStrip digest={digest} />
|
|
166
|
+
|
|
167
|
+
<DaysActiveBars digest={digest} />
|
|
168
|
+
|
|
169
|
+
{digest.top_goal_categories.length > 0 && (
|
|
170
|
+
<Section title="Goal mix" anchor="goals">
|
|
171
|
+
<GoalBar goals={digest.top_goal_categories} total={digest.agent_min_total} />
|
|
172
|
+
</Section>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{/* ── §2: Through the week — trajectory + standout days + project areas ── */}
|
|
176
|
+
<ThroughTheWeek digest={digest} />
|
|
177
|
+
|
|
178
|
+
{/* ── §3: Top 3 sessions — per-session deep-dive ── */}
|
|
179
|
+
{digest.top_sessions && digest.top_sessions.length > 0 && (
|
|
180
|
+
<TopSessionsSection sessions={digest.top_sessions} />
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* ── §4: Findings narrative — what worked / stalled / surprised / lean ── */}
|
|
184
|
+
<FindingsSection
|
|
185
|
+
title="What worked"
|
|
186
|
+
anchor="what-worked"
|
|
187
|
+
items={digest.what_worked ?? []}
|
|
188
|
+
tone="#48bb78"
|
|
189
|
+
sectionFallbackProse={null}
|
|
190
|
+
/>
|
|
191
|
+
|
|
192
|
+
<FindingsSection
|
|
193
|
+
title="What stalled"
|
|
194
|
+
anchor="what-stalled"
|
|
195
|
+
items={digest.what_stalled ?? []}
|
|
196
|
+
tone="#ed8936"
|
|
197
|
+
sectionFallbackProse="No stalls stuck this week."
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<SurprisesSection items={digest.what_surprised ?? []} />
|
|
201
|
+
|
|
202
|
+
<WhereToLeanSection items={digest.where_to_lean ?? []} />
|
|
203
|
+
|
|
204
|
+
{/* ── Bottom: shipped list (kept) + legacy fallback ──
|
|
205
|
+
Removed PatternRollupsFold (working_shapes + interaction_grammar
|
|
206
|
+
UI) and ByTheNumbersFold (interaction_modes). They were collapsed
|
|
207
|
+
by default and rarely expanded; the LLM's findings already cite
|
|
208
|
+
shape/grammar names by anchor, so the rollup tables were
|
|
209
|
+
redundant context. Their feeder data is still on the digest for
|
|
210
|
+
back-compat — only the rendering is gone. */}
|
|
211
|
+
<ShippedList shipped={digest.shipped} />
|
|
212
|
+
|
|
213
|
+
{!digest.working_shapes && (
|
|
214
|
+
<LegacyNarrativeFallback digest={digest} />
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function LegacyNarrativeFallback({ digest }: { digest: WeekDigestType }) {
|
|
221
|
+
const themes = digest.recurring_themes ?? [];
|
|
222
|
+
const correlations = digest.outcome_correlations ?? [];
|
|
223
|
+
const friction = digest.friction_categories ?? [];
|
|
224
|
+
const suggestions = digest.suggestions;
|
|
225
|
+
const horizon = digest.on_the_horizon;
|
|
226
|
+
const fun = digest.fun_ending;
|
|
227
|
+
const hasAny = themes.length > 0 || correlations.length > 0 || friction.length > 0
|
|
228
|
+
|| (suggestions?.claude_md_additions?.length ?? 0) > 0
|
|
229
|
+
|| horizon || fun;
|
|
230
|
+
if (!hasAny) return null;
|
|
231
|
+
return (
|
|
232
|
+
<div style={{
|
|
233
|
+
padding: "14px 16px", marginTop: 18, borderRadius: 8,
|
|
234
|
+
background: "color-mix(in srgb, var(--af-text-tertiary) 5%, var(--af-surface))",
|
|
235
|
+
border: "1px dashed var(--af-border-subtle)",
|
|
236
|
+
fontSize: 12, color: "var(--af-text-secondary)", lineHeight: 1.55,
|
|
237
|
+
}}>
|
|
238
|
+
<strong style={{ color: "var(--af-text)" }}>Legacy digest:</strong> this week was generated under a previous prompt.
|
|
239
|
+
Re-roll the digest to refresh it under the new working-shapes narrative.
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Sub-components ──────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
type StatCard = {
|
|
247
|
+
label: string;
|
|
248
|
+
primary: string;
|
|
249
|
+
detail: string;
|
|
250
|
+
href?: string;
|
|
251
|
+
title?: string;
|
|
252
|
+
visual?: ReactNode;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
function WeekStatsStrip({ digest }: { digest: WeekDigestType }) {
|
|
256
|
+
const totalHrs = digest.agent_min_total / 60;
|
|
257
|
+
const totalStr = totalHrs >= 1 ? `${totalHrs.toFixed(1)}h` : `${Math.round(digest.agent_min_total)}m`;
|
|
258
|
+
const peakHourBucket = peakHour(digest.hours_distribution);
|
|
259
|
+
|
|
260
|
+
// Per-day agent_min for the mini-bar in Agent time / Busiest day cards
|
|
261
|
+
const dayMap = new Map(digest.days_active.map(d => [d.date, d]));
|
|
262
|
+
const weekDates = weekDateLabels(digest.window.start);
|
|
263
|
+
const perDayAgent = weekDates.map(d => dayMap.get(d)?.agent_min ?? 0);
|
|
264
|
+
const maxAgent = Math.max(...perDayAgent, 1);
|
|
265
|
+
|
|
266
|
+
// Busiest day index for highlight
|
|
267
|
+
const busiestIdx = digest.busiest_day
|
|
268
|
+
? weekDates.indexOf(digest.busiest_day.date)
|
|
269
|
+
: -1;
|
|
270
|
+
|
|
271
|
+
// Longest run vs busiest day's total (for the bar visual)
|
|
272
|
+
const longestRunRatio = digest.longest_run && digest.busiest_day
|
|
273
|
+
? Math.min(1, digest.longest_run.active_min / Math.max(1, digest.busiest_day.agent_min))
|
|
274
|
+
: 0;
|
|
275
|
+
|
|
276
|
+
const cards: StatCard[] = [];
|
|
277
|
+
|
|
278
|
+
cards.push({
|
|
279
|
+
label: "Agent time",
|
|
280
|
+
primary: totalStr,
|
|
281
|
+
detail: `across ${digest.days_active.length} day${digest.days_active.length === 1 ? "" : "s"}`,
|
|
282
|
+
visual: <MiniDayBars values={perDayAgent} max={maxAgent} highlight={-1} />,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (digest.busiest_day) {
|
|
286
|
+
cards.push({
|
|
287
|
+
label: "Busiest day",
|
|
288
|
+
href: `/digest/${digest.busiest_day.date}`,
|
|
289
|
+
primary: `${dayName(digest.busiest_day.date)} ${digest.busiest_day.date.slice(5)}`,
|
|
290
|
+
detail: `${Math.round(digest.busiest_day.agent_min)}m · ${digest.busiest_day.shipped_count} PR${digest.busiest_day.shipped_count === 1 ? "" : "s"}`,
|
|
291
|
+
visual: <MiniDayBars values={perDayAgent} max={maxAgent} highlight={busiestIdx} />,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (digest.longest_run) {
|
|
296
|
+
cards.push({
|
|
297
|
+
label: "Longest single run",
|
|
298
|
+
href: `/sessions/${digest.longest_run.session_id}`,
|
|
299
|
+
title: `Session ${digest.longest_run.session_id.slice(0, 8)}`,
|
|
300
|
+
primary: `${Math.round(digest.longest_run.active_min)}m`,
|
|
301
|
+
detail: `${digest.longest_run.project_display} · ${dayName(digest.longest_run.date)} ${digest.longest_run.date.slice(5)}`,
|
|
302
|
+
visual: <MiniRatioBar ratio={longestRunRatio} caption="vs busiest day" />,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (peakHourBucket) {
|
|
307
|
+
cards.push({
|
|
308
|
+
label: "Peak hours",
|
|
309
|
+
title: `${formatHourRange(peakHourBucket.start, peakHourBucket.end)} carried ${peakHourBucket.share_pct.toFixed(0)}% of the week's agent time`,
|
|
310
|
+
primary: formatHourRange(peakHourBucket.start, peakHourBucket.end),
|
|
311
|
+
detail: `${peakHourBucket.share_pct.toFixed(0)}% of agent time`,
|
|
312
|
+
visual: <MiniHoursSparkline hours={digest.hours_distribution} peakStart={peakHourBucket.start} peakEnd={peakHourBucket.end} />,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (cards.length === 0) return null;
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<section style={{
|
|
320
|
+
display: "grid",
|
|
321
|
+
gridTemplateColumns: `repeat(${Math.min(cards.length, 4)}, minmax(0, 1fr))`,
|
|
322
|
+
gap: 10,
|
|
323
|
+
marginBottom: 20,
|
|
324
|
+
}}>
|
|
325
|
+
{cards.map((s, i) => {
|
|
326
|
+
const inner = (
|
|
327
|
+
<div style={{
|
|
328
|
+
padding: "12px 14px", borderRadius: 10,
|
|
329
|
+
background: "var(--af-surface)",
|
|
330
|
+
border: "1px solid var(--af-border-subtle)",
|
|
331
|
+
height: "100%",
|
|
332
|
+
display: "flex", flexDirection: "column", justifyContent: "space-between",
|
|
333
|
+
minHeight: 90,
|
|
334
|
+
}} title={s.title}>
|
|
335
|
+
<div>
|
|
336
|
+
<div style={{
|
|
337
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
|
|
338
|
+
textTransform: "uppercase", color: "var(--af-text-tertiary)",
|
|
339
|
+
marginBottom: 6,
|
|
340
|
+
}}>{s.label}</div>
|
|
341
|
+
<div style={{
|
|
342
|
+
fontSize: 17, fontWeight: 600, color: "var(--af-text)",
|
|
343
|
+
letterSpacing: "-0.01em", lineHeight: 1.2,
|
|
344
|
+
}}>{s.primary}</div>
|
|
345
|
+
<div style={{
|
|
346
|
+
fontSize: 10.5, color: "var(--af-text-tertiary)",
|
|
347
|
+
fontFamily: "var(--font-mono)", marginTop: 3,
|
|
348
|
+
}}>{s.detail}</div>
|
|
349
|
+
</div>
|
|
350
|
+
{s.visual && <div style={{ marginTop: 10 }}>{s.visual}</div>}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
return s.href ? (
|
|
354
|
+
<Link key={i} href={s.href} style={{ textDecoration: "none" }}>{inner}</Link>
|
|
355
|
+
) : (
|
|
356
|
+
<div key={i}>{inner}</div>
|
|
357
|
+
);
|
|
358
|
+
})}
|
|
359
|
+
</section>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** 7-day mini-bar — shows daily agent_min distribution. When highlight≥0
|
|
364
|
+
* that day glows in accent; others stay subtle. */
|
|
365
|
+
function MiniDayBars({ values, max, highlight }: { values: number[]; max: number; highlight: number }) {
|
|
366
|
+
return (
|
|
367
|
+
<div style={{ display: "flex", gap: 2, alignItems: "flex-end", height: 18 }}>
|
|
368
|
+
{values.map((v, i) => {
|
|
369
|
+
const h = max > 0 ? Math.max(1, (v / max) * 18) : 1;
|
|
370
|
+
const isHi = i === highlight;
|
|
371
|
+
return (
|
|
372
|
+
<div key={i} style={{
|
|
373
|
+
flex: 1,
|
|
374
|
+
height: h,
|
|
375
|
+
borderRadius: 1,
|
|
376
|
+
background: isHi
|
|
377
|
+
? "var(--af-accent)"
|
|
378
|
+
: v > 0
|
|
379
|
+
? "color-mix(in srgb, var(--af-accent) 30%, transparent)"
|
|
380
|
+
: "var(--af-border-subtle)",
|
|
381
|
+
}} />
|
|
382
|
+
);
|
|
383
|
+
})}
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Single horizontal bar showing a 0..1 ratio with a caption underneath. */
|
|
389
|
+
function MiniRatioBar({ ratio, caption }: { ratio: number; caption: string }) {
|
|
390
|
+
const pct = Math.max(0, Math.min(1, ratio)) * 100;
|
|
391
|
+
return (
|
|
392
|
+
<div>
|
|
393
|
+
<div style={{
|
|
394
|
+
height: 6, borderRadius: 99, overflow: "hidden",
|
|
395
|
+
background: "var(--af-border-subtle)",
|
|
396
|
+
}}>
|
|
397
|
+
<div style={{
|
|
398
|
+
width: `${pct}%`, height: "100%",
|
|
399
|
+
background: "var(--af-accent)",
|
|
400
|
+
}} />
|
|
401
|
+
</div>
|
|
402
|
+
<div style={{
|
|
403
|
+
fontSize: 9, color: "var(--af-text-tertiary)",
|
|
404
|
+
fontFamily: "var(--font-mono)", marginTop: 3,
|
|
405
|
+
}}>{Math.round(pct)}% {caption}</div>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** 24-bar sparkline of hours_distribution with the peak window glowing. */
|
|
411
|
+
function MiniHoursSparkline({ hours, peakStart, peakEnd }: { hours: number[]; peakStart: number; peakEnd: number }) {
|
|
412
|
+
const max = Math.max(...hours, 1);
|
|
413
|
+
return (
|
|
414
|
+
<div style={{ display: "flex", gap: 1, alignItems: "flex-end", height: 18 }}>
|
|
415
|
+
{hours.map((v, h) => {
|
|
416
|
+
const inPeak = h >= peakStart && h <= peakEnd;
|
|
417
|
+
const height = max > 0 ? Math.max(1, (v / max) * 18) : 1;
|
|
418
|
+
return (
|
|
419
|
+
<div key={h} style={{
|
|
420
|
+
flex: 1,
|
|
421
|
+
height,
|
|
422
|
+
borderRadius: 1,
|
|
423
|
+
background: inPeak
|
|
424
|
+
? "var(--af-accent)"
|
|
425
|
+
: v > 0
|
|
426
|
+
? "color-mix(in srgb, var(--af-accent) 22%, transparent)"
|
|
427
|
+
: "var(--af-border-subtle)",
|
|
428
|
+
}} />
|
|
429
|
+
);
|
|
430
|
+
})}
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function DaysActiveBars({ digest }: { digest: WeekDigestType }) {
|
|
436
|
+
if (digest.days_active.length === 0) return null;
|
|
437
|
+
const maxMin = Math.max(...digest.days_active.map(d => d.agent_min));
|
|
438
|
+
const dayMap = new Map(digest.days_active.map(d => [d.date, d]));
|
|
439
|
+
const dates = weekDateLabels(digest.window.start);
|
|
440
|
+
return (
|
|
441
|
+
<section style={{
|
|
442
|
+
display: "flex", gap: 6, alignItems: "stretch",
|
|
443
|
+
padding: "12px 0", marginBottom: 24,
|
|
444
|
+
}}>
|
|
445
|
+
{dates.map((date, i) => {
|
|
446
|
+
const d = dayMap.get(date);
|
|
447
|
+
const mins = d?.agent_min ?? 0;
|
|
448
|
+
const heightPct = maxMin > 0 && mins > 0 ? Math.max(8, (mins / maxMin) * 100) : 0;
|
|
449
|
+
const tone = d ? OUTCOME_TONE[d.outcome_day] : "var(--af-text-tertiary)";
|
|
450
|
+
const helpTone = d?.helpfulness_day ? HELP_COLORS[d.helpfulness_day] : null;
|
|
451
|
+
return (
|
|
452
|
+
<Link
|
|
453
|
+
key={i}
|
|
454
|
+
href={d ? `/digest/${date}` : "#"}
|
|
455
|
+
style={{
|
|
456
|
+
flex: 1, display: "flex", flexDirection: "column", alignItems: "stretch", gap: 2,
|
|
457
|
+
textDecoration: "none",
|
|
458
|
+
pointerEvents: d ? "auto" : "none",
|
|
459
|
+
}}
|
|
460
|
+
title={d
|
|
461
|
+
? `${date} · ${Math.round(mins)}m · ${d.outcome_day}${d.shipped_count > 0 ? ` · ${d.shipped_count} PR` : ""}${d.helpfulness_day ? ` · helpfulness ${d.helpfulness_day}` : ""}`
|
|
462
|
+
: "no activity"
|
|
463
|
+
}
|
|
464
|
+
>
|
|
465
|
+
<div style={{
|
|
466
|
+
height: 56, borderRadius: 4, position: "relative",
|
|
467
|
+
background: "var(--af-surface)",
|
|
468
|
+
border: `1px solid ${d ? `color-mix(in srgb, ${tone} 28%, var(--af-border-subtle))` : "var(--af-border-subtle)"}`,
|
|
469
|
+
overflow: "hidden",
|
|
470
|
+
}}>
|
|
471
|
+
{d && (
|
|
472
|
+
<div style={{
|
|
473
|
+
position: "absolute", bottom: 0, left: 0, right: 0,
|
|
474
|
+
height: `${heightPct}%`,
|
|
475
|
+
background: `color-mix(in srgb, ${tone} 28%, transparent)`,
|
|
476
|
+
}} />
|
|
477
|
+
)}
|
|
478
|
+
{d?.dominant_shape && SHAPE_COLORS[d.dominant_shape] && (
|
|
479
|
+
<div
|
|
480
|
+
title={`${SHAPE_LABELS[d.dominant_shape] ?? d.dominant_shape}`}
|
|
481
|
+
style={{
|
|
482
|
+
position: "absolute", top: 0, left: 0, right: 0,
|
|
483
|
+
height: 3,
|
|
484
|
+
background: SHAPE_COLORS[d.dominant_shape],
|
|
485
|
+
}}
|
|
486
|
+
/>
|
|
487
|
+
)}
|
|
488
|
+
{helpTone && (
|
|
489
|
+
<div style={{
|
|
490
|
+
position: "absolute", top: 4, right: 4,
|
|
491
|
+
width: 6, height: 6, borderRadius: 999,
|
|
492
|
+
background: helpTone,
|
|
493
|
+
}} title={d?.helpfulness_day ?? ""} />
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
<div style={{
|
|
497
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.06em",
|
|
498
|
+
color: "var(--af-text-tertiary)", textAlign: "center",
|
|
499
|
+
}}>
|
|
500
|
+
{DAY_LABELS[i]}
|
|
501
|
+
</div>
|
|
502
|
+
<div style={{
|
|
503
|
+
fontSize: 9, fontFamily: "var(--font-mono)",
|
|
504
|
+
color: d ? "var(--af-text-secondary)" : "var(--af-text-tertiary)",
|
|
505
|
+
textAlign: "center",
|
|
506
|
+
}}>
|
|
507
|
+
{d ? `${Math.round(mins)}m` : "—"}
|
|
508
|
+
</div>
|
|
509
|
+
</Link>
|
|
510
|
+
);
|
|
511
|
+
})}
|
|
512
|
+
</section>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function ByTheNumbersFold({ modes }: { modes: NonNullable<WeekDigestType["interaction_modes"]> }) {
|
|
517
|
+
const [open, setOpen] = useState(false);
|
|
518
|
+
return (
|
|
519
|
+
<section style={{ marginBottom: 28 }}>
|
|
520
|
+
<button
|
|
521
|
+
type="button"
|
|
522
|
+
onClick={() => setOpen(o => !o)}
|
|
523
|
+
style={{
|
|
524
|
+
display: "inline-flex", alignItems: "center", gap: 6,
|
|
525
|
+
background: "transparent", border: "none", cursor: "pointer",
|
|
526
|
+
padding: "6px 0", color: "var(--af-text-tertiary)",
|
|
527
|
+
fontSize: 11, fontWeight: 700, letterSpacing: "0.08em",
|
|
528
|
+
textTransform: "uppercase",
|
|
529
|
+
}}
|
|
530
|
+
>
|
|
531
|
+
{open ? "▾" : "▸"} By the numbers
|
|
532
|
+
</button>
|
|
533
|
+
{open && (
|
|
534
|
+
<div style={{
|
|
535
|
+
display: "grid",
|
|
536
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
537
|
+
gap: 8, marginTop: 8,
|
|
538
|
+
}}>
|
|
539
|
+
<NumberCard label="Orchestration" lines={[
|
|
540
|
+
`${modes.orchestration.subagent_calls} dispatches · ${modes.orchestration.days_with_subagents}/7 days`,
|
|
541
|
+
modes.orchestration.top_types.slice(0, 3).map(t => `${t.type} ×${t.count}`).join(" · "),
|
|
542
|
+
modes.orchestration.task_ops > 0 ? `${modes.orchestration.task_ops} TodoWrite ops` : "",
|
|
543
|
+
].filter(Boolean)} />
|
|
544
|
+
<NumberCard label="Skill-driven" lines={[
|
|
545
|
+
`${modes.skill_use.skill_calls} loads · ${modes.skill_use.days_with_skills}/7 days`,
|
|
546
|
+
modes.skill_use.top_skills.slice(0, 3).map(s => `${s.skill} ×${s.count}`).join(" · "),
|
|
547
|
+
].filter(Boolean)} />
|
|
548
|
+
<NumberCard label="Plan-gated" lines={[
|
|
549
|
+
`${modes.plan_gating.exit_plan_calls} ExitPlan · ${modes.plan_gating.days_with_plan}/7 days`,
|
|
550
|
+
]} />
|
|
551
|
+
<NumberCard label="Turn shape" lines={[
|
|
552
|
+
`${modes.turn_shape.tools_per_turn.toFixed(1)} tools/turn · ${modes.turn_shape.label}`,
|
|
553
|
+
`${modes.turn_shape.long_autonomous_days} long-autonomous days · ${modes.turn_shape.interrupts} interrupts`,
|
|
554
|
+
]} />
|
|
555
|
+
</div>
|
|
556
|
+
)}
|
|
557
|
+
</section>
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function NumberCard({ label, lines }: { label: string; lines: string[] }) {
|
|
562
|
+
return (
|
|
563
|
+
<div style={{
|
|
564
|
+
padding: "10px 12px", borderRadius: 8,
|
|
565
|
+
background: "var(--af-surface)",
|
|
566
|
+
border: "1px solid var(--af-border-subtle)",
|
|
567
|
+
}}>
|
|
568
|
+
<div style={{
|
|
569
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
|
|
570
|
+
textTransform: "uppercase", color: "var(--af-text-tertiary)",
|
|
571
|
+
marginBottom: 4,
|
|
572
|
+
}}>{label}</div>
|
|
573
|
+
{lines.map((ln, i) => (
|
|
574
|
+
<div key={i} style={{
|
|
575
|
+
fontSize: 11, color: i === 0 ? "var(--af-text)" : "var(--af-text-secondary)",
|
|
576
|
+
fontFamily: "var(--font-mono)", lineHeight: 1.45,
|
|
577
|
+
}}>{ln}</div>
|
|
578
|
+
))}
|
|
579
|
+
</div>
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const OUTCOME_TONE: Record<string, string> = {
|
|
584
|
+
shipped: "#48bb78",
|
|
585
|
+
partial: "#4299e1",
|
|
586
|
+
blocked: "#f56565",
|
|
587
|
+
exploratory: "#a0aec0",
|
|
588
|
+
trivial: "#cbd5e0",
|
|
589
|
+
idle: "#cbd5e0",
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/** Smallest window length we accept as a "peak"; below this and the window
|
|
593
|
+
* isn't carrying enough of the week's activity to be characteristic. */
|
|
594
|
+
const PEAK_HOUR_COVERAGE_THRESHOLD = 0.6;
|
|
595
|
+
|
|
596
|
+
function peakHour(hours: number[]): { start: number; end: number; share_pct: number } | null {
|
|
597
|
+
const total = hours.reduce((a, b) => a + b, 0);
|
|
598
|
+
if (total === 0) return null;
|
|
599
|
+
for (let len = 1; len <= 24; len++) {
|
|
600
|
+
let bestSum = 0, bestStart = 0;
|
|
601
|
+
for (let s = 0; s <= 24 - len; s++) {
|
|
602
|
+
let sum = 0;
|
|
603
|
+
for (let i = 0; i < len; i++) sum += hours[s + i] ?? 0;
|
|
604
|
+
if (sum > bestSum) { bestSum = sum; bestStart = s; }
|
|
605
|
+
}
|
|
606
|
+
if (bestSum / total >= PEAK_HOUR_COVERAGE_THRESHOLD) {
|
|
607
|
+
return { start: bestStart, end: bestStart + len, share_pct: (bestSum / total) * 100 };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function formatHourRange(start: number, end: number): string {
|
|
614
|
+
const fmt = (h: number) => {
|
|
615
|
+
const period = h < 12 || h === 24 ? "am" : "pm";
|
|
616
|
+
const mod = ((h % 12) || 12);
|
|
617
|
+
return `${mod}${period}`;
|
|
618
|
+
};
|
|
619
|
+
return `${fmt(start)}–${fmt(end)}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function weekDateLabels(startIso: string): string[] {
|
|
623
|
+
const out: string[] = [];
|
|
624
|
+
const start = new Date(startIso);
|
|
625
|
+
for (let i = 0; i < 7; i++) {
|
|
626
|
+
const d = new Date(start);
|
|
627
|
+
d.setDate(start.getDate() + i);
|
|
628
|
+
const y = d.getFullYear();
|
|
629
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
630
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
631
|
+
out.push(`${y}-${m}-${day}`);
|
|
632
|
+
}
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function DayChips({ dates, tone }: { dates: string[]; tone: string }) {
|
|
637
|
+
return (
|
|
638
|
+
<div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
|
|
639
|
+
{dates.map(d => (
|
|
640
|
+
<Link key={d} href={`/digest/${d}`} style={{
|
|
641
|
+
display: "inline-flex", alignItems: "center", gap: 4,
|
|
642
|
+
padding: "2px 8px", borderRadius: 999,
|
|
643
|
+
background: `color-mix(in srgb, ${tone} 12%, transparent)`,
|
|
644
|
+
border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`,
|
|
645
|
+
fontSize: 10, fontFamily: "var(--font-mono)",
|
|
646
|
+
color: tone, textDecoration: "none", fontWeight: 600,
|
|
647
|
+
}}>
|
|
648
|
+
{dayName(d)} {d.slice(5)}
|
|
649
|
+
</Link>
|
|
650
|
+
))}
|
|
651
|
+
</div>
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** "Through the week" combined section — trajectory + standout days +
|
|
656
|
+
* project areas under one heading. Trajectory + standout share the top row
|
|
657
|
+
* in two columns (60/40); project areas spread below in a 2-col grid. */
|
|
658
|
+
function ThroughTheWeek({ digest }: { digest: WeekDigestType }) {
|
|
659
|
+
const hasTrajectory = digest.trajectory && digest.trajectory.length > 0;
|
|
660
|
+
const hasStandout = digest.standout_days && digest.standout_days.length > 0;
|
|
661
|
+
const visibleProjects = digest.projects.filter(p => p.share_pct >= PROJECT_MIN_SHARE || p.shipped_count > 0);
|
|
662
|
+
const hasProjects = visibleProjects.length > 0;
|
|
663
|
+
if (!hasTrajectory && !hasStandout && !hasProjects) return null;
|
|
664
|
+
|
|
665
|
+
return (
|
|
666
|
+
<Section title="Through the week" anchor="through-the-week">
|
|
667
|
+
{(hasTrajectory || hasStandout) && (
|
|
668
|
+
<div style={{
|
|
669
|
+
display: "grid",
|
|
670
|
+
gridTemplateColumns: hasTrajectory && hasStandout ? "minmax(0, 1.4fr) minmax(0, 1fr)" : "1fr",
|
|
671
|
+
gap: 28, marginBottom: hasProjects ? 22 : 0,
|
|
672
|
+
}}>
|
|
673
|
+
{hasTrajectory && (
|
|
674
|
+
<div>
|
|
675
|
+
<SubSectionLabel>Trajectory</SubSectionLabel>
|
|
676
|
+
<ol style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }}>
|
|
677
|
+
{digest.trajectory!.map(t => (
|
|
678
|
+
<li key={t.date} style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
|
|
679
|
+
<Link href={`/digest/${t.date}`} style={{
|
|
680
|
+
fontSize: 10, fontWeight: 600, color: "var(--af-text-tertiary)",
|
|
681
|
+
fontFamily: "var(--font-mono)", flexShrink: 0, width: 64, paddingTop: 3,
|
|
682
|
+
textDecoration: "none",
|
|
683
|
+
}}>
|
|
684
|
+
{dayName(t.date)} {t.date.slice(5)}
|
|
685
|
+
</Link>
|
|
686
|
+
<span style={{ fontSize: 12.5, lineHeight: 1.55, color: "var(--af-text-secondary)" }}>
|
|
687
|
+
{renderWithFlagChips(t.line)}
|
|
688
|
+
</span>
|
|
689
|
+
</li>
|
|
690
|
+
))}
|
|
691
|
+
</ol>
|
|
692
|
+
</div>
|
|
693
|
+
)}
|
|
694
|
+
{hasStandout && (
|
|
695
|
+
<div>
|
|
696
|
+
<SubSectionLabel>Standout days</SubSectionLabel>
|
|
697
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 8 }}>
|
|
698
|
+
{digest.standout_days!.map(s => (
|
|
699
|
+
<li key={s.date} style={{
|
|
700
|
+
padding: "10px 12px", borderRadius: 8, background: "var(--af-surface)",
|
|
701
|
+
border: "1px solid var(--af-border-subtle)",
|
|
702
|
+
}}>
|
|
703
|
+
<Link href={`/digest/${s.date}`} style={{
|
|
704
|
+
fontSize: 11, fontWeight: 600,
|
|
705
|
+
color: "var(--af-accent)", textDecoration: "none",
|
|
706
|
+
fontFamily: "var(--font-mono)", marginRight: 8,
|
|
707
|
+
}}>
|
|
708
|
+
{dayName(s.date)} {s.date.slice(5)}
|
|
709
|
+
</Link>
|
|
710
|
+
<span style={{ fontSize: 12.5, lineHeight: 1.55, color: "var(--af-text)" }}>
|
|
711
|
+
{renderWithFlagChips(s.why)}
|
|
712
|
+
</span>
|
|
713
|
+
</li>
|
|
714
|
+
))}
|
|
715
|
+
</ul>
|
|
716
|
+
</div>
|
|
717
|
+
)}
|
|
718
|
+
</div>
|
|
719
|
+
)}
|
|
720
|
+
{hasProjects && (
|
|
721
|
+
<div>
|
|
722
|
+
<SubSectionLabel>Project areas</SubSectionLabel>
|
|
723
|
+
<ul style={{
|
|
724
|
+
listStyle: "none", padding: 0, margin: 0,
|
|
725
|
+
display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
|
|
726
|
+
gap: 10,
|
|
727
|
+
}}>
|
|
728
|
+
{visibleProjects.map(p => (
|
|
729
|
+
<li key={p.name} style={{
|
|
730
|
+
padding: "11px 13px", borderRadius: 8,
|
|
731
|
+
background: "var(--af-surface)",
|
|
732
|
+
border: "1px solid var(--af-border-subtle)",
|
|
733
|
+
display: "flex", flexDirection: "column",
|
|
734
|
+
}}>
|
|
735
|
+
<div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: p.description ? 6 : 0 }}>
|
|
736
|
+
<Link
|
|
737
|
+
href={`/projects/${encodeURIComponent(p.name)}`}
|
|
738
|
+
style={{ fontSize: 13, fontWeight: 600, color: "var(--af-text)", textDecoration: "none", flex: 1, minWidth: 0, letterSpacing: "-0.01em" }}
|
|
739
|
+
>
|
|
740
|
+
{p.display_name}
|
|
741
|
+
</Link>
|
|
742
|
+
<span style={{
|
|
743
|
+
fontFamily: "var(--font-mono)", fontSize: 10,
|
|
744
|
+
color: "var(--af-text-tertiary)",
|
|
745
|
+
}}>
|
|
746
|
+
{Math.round(p.agent_min)}m · {p.share_pct.toFixed(0)}% · {p.shipped_count} PR
|
|
747
|
+
</span>
|
|
748
|
+
</div>
|
|
749
|
+
{p.description && (
|
|
750
|
+
<p style={{ fontSize: 12, lineHeight: 1.5, margin: 0, color: "var(--af-text-secondary)" }}>
|
|
751
|
+
{p.description}
|
|
752
|
+
</p>
|
|
753
|
+
)}
|
|
754
|
+
</li>
|
|
755
|
+
))}
|
|
756
|
+
</ul>
|
|
757
|
+
</div>
|
|
758
|
+
)}
|
|
759
|
+
</Section>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function SubSectionLabel({ children }: { children: ReactNode }) {
|
|
764
|
+
return (
|
|
765
|
+
<div style={{
|
|
766
|
+
fontSize: 10, color: "var(--af-text-tertiary)", textTransform: "uppercase",
|
|
767
|
+
letterSpacing: "0.08em", fontWeight: 700, marginBottom: 10,
|
|
768
|
+
}}>
|
|
769
|
+
{children}
|
|
770
|
+
</div>
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function CopyBlock({ label, payload }: { label: string; payload: string }) {
|
|
775
|
+
const [copied, setCopied] = useState(false);
|
|
776
|
+
return (
|
|
777
|
+
<div style={{ position: "relative" }}>
|
|
778
|
+
<pre style={{
|
|
779
|
+
margin: 0,
|
|
780
|
+
padding: "10px 12px",
|
|
781
|
+
paddingRight: 96,
|
|
782
|
+
borderRadius: 6,
|
|
783
|
+
background: "var(--af-surface-raised)",
|
|
784
|
+
border: "1px solid var(--af-border-subtle)",
|
|
785
|
+
fontSize: 11,
|
|
786
|
+
fontFamily: "var(--font-mono)",
|
|
787
|
+
color: "var(--af-text)",
|
|
788
|
+
whiteSpace: "pre-wrap",
|
|
789
|
+
wordBreak: "break-word",
|
|
790
|
+
lineHeight: 1.55,
|
|
791
|
+
overflowX: "auto",
|
|
792
|
+
maxHeight: 280,
|
|
793
|
+
overflowY: "auto",
|
|
794
|
+
}}>{payload}</pre>
|
|
795
|
+
<button
|
|
796
|
+
type="button"
|
|
797
|
+
onClick={async () => {
|
|
798
|
+
try {
|
|
799
|
+
await navigator.clipboard.writeText(payload);
|
|
800
|
+
setCopied(true);
|
|
801
|
+
setTimeout(() => setCopied(false), 1500);
|
|
802
|
+
} catch {
|
|
803
|
+
// best-effort
|
|
804
|
+
}
|
|
805
|
+
}}
|
|
806
|
+
style={{
|
|
807
|
+
position: "absolute", top: 8, right: 8,
|
|
808
|
+
display: "inline-flex", alignItems: "center", gap: 4,
|
|
809
|
+
padding: "4px 8px", borderRadius: 5,
|
|
810
|
+
background: copied ? "color-mix(in srgb, #48bb78 18%, var(--af-surface))" : "var(--af-surface)",
|
|
811
|
+
border: `1px solid ${copied ? "#48bb78" : "var(--af-border-subtle)"}`,
|
|
812
|
+
color: copied ? "#48bb78" : "var(--af-text-secondary)",
|
|
813
|
+
fontSize: 10, fontWeight: 600, cursor: "pointer",
|
|
814
|
+
}}
|
|
815
|
+
>
|
|
816
|
+
{copied ? <><Check size={10} /> Copied</> : <><Copy size={10} /> {label}</>}
|
|
817
|
+
</button>
|
|
818
|
+
</div>
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function ShippedList({ shipped }: { shipped: WeekDigestType["shipped"] }) {
|
|
823
|
+
const [expanded, setExpanded] = useState(false);
|
|
824
|
+
if (shipped.length === 0) return null;
|
|
825
|
+
const collapsible = shipped.length >= SHIPPED_COLLAPSE_THRESHOLD;
|
|
826
|
+
const visible = collapsible && !expanded ? shipped.slice(0, 5) : shipped;
|
|
827
|
+
const hidden = shipped.length - visible.length;
|
|
828
|
+
return (
|
|
829
|
+
<Section title={`Shipped (${shipped.length})`} anchor="shipped">
|
|
830
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 4 }}>
|
|
831
|
+
{visible.map((s, i) => (
|
|
832
|
+
<li key={i} style={{
|
|
833
|
+
display: "flex", gap: 10, alignItems: "baseline",
|
|
834
|
+
padding: "6px 10px", borderRadius: 6,
|
|
835
|
+
fontSize: 12, color: "var(--af-text)",
|
|
836
|
+
}}>
|
|
837
|
+
<Link href={`/digest/${s.date}`} style={{
|
|
838
|
+
fontFamily: "var(--font-mono)", fontSize: 10,
|
|
839
|
+
color: "var(--af-text-tertiary)", flexShrink: 0, width: 56,
|
|
840
|
+
textDecoration: "none",
|
|
841
|
+
}}>
|
|
842
|
+
{dayName(s.date)} {s.date.slice(5)}
|
|
843
|
+
</Link>
|
|
844
|
+
<span style={{ flex: 1, minWidth: 0 }}>{s.title}</span>
|
|
845
|
+
<span style={{
|
|
846
|
+
fontSize: 10, color: "var(--af-text-tertiary)",
|
|
847
|
+
fontFamily: "var(--font-mono)",
|
|
848
|
+
}}>
|
|
849
|
+
{s.project}
|
|
850
|
+
</span>
|
|
851
|
+
</li>
|
|
852
|
+
))}
|
|
853
|
+
</ul>
|
|
854
|
+
{collapsible && hidden > 0 && (
|
|
855
|
+
<button
|
|
856
|
+
type="button"
|
|
857
|
+
onClick={() => setExpanded(true)}
|
|
858
|
+
style={{
|
|
859
|
+
marginTop: 6, padding: "4px 10px", borderRadius: 5,
|
|
860
|
+
background: "transparent", border: "1px solid var(--af-border-subtle)",
|
|
861
|
+
fontSize: 10, color: "var(--af-text-secondary)", cursor: "pointer",
|
|
862
|
+
fontFamily: "var(--font-mono)",
|
|
863
|
+
}}
|
|
864
|
+
>
|
|
865
|
+
+{hidden} more
|
|
866
|
+
</button>
|
|
867
|
+
)}
|
|
868
|
+
{collapsible && expanded && (
|
|
869
|
+
<button
|
|
870
|
+
type="button"
|
|
871
|
+
onClick={() => setExpanded(false)}
|
|
872
|
+
style={{
|
|
873
|
+
marginTop: 6, padding: "4px 10px", borderRadius: 5,
|
|
874
|
+
background: "transparent", border: "1px solid var(--af-border-subtle)",
|
|
875
|
+
fontSize: 10, color: "var(--af-text-secondary)", cursor: "pointer",
|
|
876
|
+
fontFamily: "var(--font-mono)",
|
|
877
|
+
}}
|
|
878
|
+
>
|
|
879
|
+
show less
|
|
880
|
+
</button>
|
|
881
|
+
)}
|
|
882
|
+
</Section>
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function buildDelta(curr: WeekDigestType, prior: WeekDigestType): string {
|
|
887
|
+
const prDelta = curr.shipped.length - prior.shipped.length;
|
|
888
|
+
const minDelta = curr.agent_min_total - prior.agent_min_total;
|
|
889
|
+
const hrDelta = minDelta / 60;
|
|
890
|
+
const prStr = `${prDelta > 0 ? "+" : ""}${prDelta} PR${Math.abs(prDelta) === 1 ? "" : "s"}`;
|
|
891
|
+
const timeStr = Math.abs(hrDelta) >= 1
|
|
892
|
+
? `${hrDelta > 0 ? "+" : ""}${hrDelta.toFixed(1)}h`
|
|
893
|
+
: `${minDelta > 0 ? "+" : ""}${Math.round(minDelta)}m`;
|
|
894
|
+
return `${prStr} · ${timeStr}`;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ─── New shape-anchored sections ─────────────────────────────────────────
|
|
898
|
+
|
|
899
|
+
function WorkingShapesSection({
|
|
900
|
+
shapes,
|
|
901
|
+
}: {
|
|
902
|
+
shapes: NonNullable<WeekDigestType["working_shapes"]>;
|
|
903
|
+
}) {
|
|
904
|
+
return (
|
|
905
|
+
<Section title="How you worked" anchor="how-you-worked">
|
|
906
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
|
|
907
|
+
{shapes.map((row, i) => {
|
|
908
|
+
const label = SHAPE_LABELS[row.shape] ?? row.shape;
|
|
909
|
+
const description = SHAPE_DESCRIPTIONS[row.shape] ?? "";
|
|
910
|
+
const dayCount = new Set(row.occurrences.map(o => o.date)).size;
|
|
911
|
+
const outcomeStr = formatOutcomeMix(row.outcome_distribution);
|
|
912
|
+
// Prefer the occurrence whose day carries a day_signature for evidence —
|
|
913
|
+
// that's the new day-first signal, more readable than a subagent prompt.
|
|
914
|
+
const sample = row.occurrences.find(o => o.day_signature)
|
|
915
|
+
?? row.occurrences.find(o => o.evidence_subagent !== null)
|
|
916
|
+
?? row.occurrences[0]!;
|
|
917
|
+
return (
|
|
918
|
+
<li key={i} style={{
|
|
919
|
+
padding: "14px 16px", borderRadius: 10,
|
|
920
|
+
background: "var(--af-surface)",
|
|
921
|
+
border: "1px solid var(--af-border-subtle)",
|
|
922
|
+
}}>
|
|
923
|
+
<div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 4, flexWrap: "wrap" }}>
|
|
924
|
+
<span style={{
|
|
925
|
+
fontSize: 13, fontWeight: 600, color: "var(--af-text)",
|
|
926
|
+
letterSpacing: "-0.01em",
|
|
927
|
+
}}>{label}</span>
|
|
928
|
+
<span style={{
|
|
929
|
+
fontFamily: "var(--font-mono)", fontSize: 11,
|
|
930
|
+
color: "var(--af-text-tertiary)",
|
|
931
|
+
}}>
|
|
932
|
+
{row.occurrences.length} session{row.occurrences.length === 1 ? "" : "s"} · {dayCount} day{dayCount === 1 ? "" : "s"}{outcomeStr ? ` · ${outcomeStr}` : ""}
|
|
933
|
+
</span>
|
|
934
|
+
</div>
|
|
935
|
+
{description && (
|
|
936
|
+
<p style={{
|
|
937
|
+
fontSize: 12, lineHeight: 1.55, margin: "0 0 8px",
|
|
938
|
+
color: "var(--af-text-secondary)",
|
|
939
|
+
}}>{description}</p>
|
|
940
|
+
)}
|
|
941
|
+
{sample.day_signature ? (
|
|
942
|
+
<div style={{
|
|
943
|
+
marginBottom: 8, padding: "8px 10px", borderRadius: 6,
|
|
944
|
+
background: "var(--af-surface-raised)",
|
|
945
|
+
borderLeft: "2px solid var(--af-accent)",
|
|
946
|
+
}}>
|
|
947
|
+
<div style={{
|
|
948
|
+
fontSize: 9, fontWeight: 600, letterSpacing: "0.04em",
|
|
949
|
+
color: "var(--af-accent)", fontFamily: "var(--font-mono)",
|
|
950
|
+
marginBottom: 3,
|
|
951
|
+
}}>
|
|
952
|
+
{dayName(sample.date)} {sample.date.slice(5)}{sample.project_display ? ` · ${sample.project_display}` : ""}
|
|
953
|
+
</div>
|
|
954
|
+
<div style={{
|
|
955
|
+
fontSize: 11, lineHeight: 1.5,
|
|
956
|
+
color: "var(--af-text)", fontStyle: "italic",
|
|
957
|
+
}}>
|
|
958
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>“</span>
|
|
959
|
+
{sample.day_signature}
|
|
960
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>”</span>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
) : sample.evidence_subagent ? (
|
|
964
|
+
<div style={{
|
|
965
|
+
marginBottom: 8, padding: "8px 10px", borderRadius: 6,
|
|
966
|
+
background: "var(--af-surface-raised)",
|
|
967
|
+
borderLeft: "2px solid var(--af-accent)",
|
|
968
|
+
}}>
|
|
969
|
+
<div style={{
|
|
970
|
+
fontSize: 9, fontWeight: 600, letterSpacing: "0.04em",
|
|
971
|
+
color: "var(--af-accent)", fontFamily: "var(--font-mono)",
|
|
972
|
+
marginBottom: 3,
|
|
973
|
+
}}>
|
|
974
|
+
{sample.evidence_subagent.type} · {dayName(sample.date)} {sample.date.slice(5)} · {sample.project_display}
|
|
975
|
+
</div>
|
|
976
|
+
<div style={{
|
|
977
|
+
fontSize: 11, lineHeight: 1.5,
|
|
978
|
+
color: "var(--af-text)", fontStyle: "italic",
|
|
979
|
+
}}>
|
|
980
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>“</span>
|
|
981
|
+
{sample.evidence_subagent.prompt_preview}
|
|
982
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>”</span>
|
|
983
|
+
</div>
|
|
984
|
+
</div>
|
|
985
|
+
) : sample.evidence_first_user ? (
|
|
986
|
+
<div style={{
|
|
987
|
+
marginBottom: 8, padding: "8px 10px", borderRadius: 6,
|
|
988
|
+
background: "var(--af-surface-raised)",
|
|
989
|
+
borderLeft: "2px solid var(--af-border)",
|
|
990
|
+
}}>
|
|
991
|
+
<div style={{
|
|
992
|
+
fontSize: 9, fontWeight: 600, letterSpacing: "0.04em",
|
|
993
|
+
color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)",
|
|
994
|
+
marginBottom: 3,
|
|
995
|
+
}}>
|
|
996
|
+
{dayName(sample.date)} {sample.date.slice(5)} · {sample.project_display}
|
|
997
|
+
</div>
|
|
998
|
+
<div style={{
|
|
999
|
+
fontSize: 11, lineHeight: 1.5,
|
|
1000
|
+
color: "var(--af-text)", fontStyle: "italic",
|
|
1001
|
+
}}>
|
|
1002
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>“</span>
|
|
1003
|
+
{sample.evidence_first_user}
|
|
1004
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>”</span>
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
) : null}
|
|
1008
|
+
{row.occurrences.length > 1 && (
|
|
1009
|
+
<div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
|
|
1010
|
+
{row.occurrences.map((o, j) => (
|
|
1011
|
+
<Link key={j} href={`/digest/${o.date}`} style={{
|
|
1012
|
+
display: "inline-flex", alignItems: "center", gap: 4,
|
|
1013
|
+
padding: "2px 8px", borderRadius: 999,
|
|
1014
|
+
background: "color-mix(in srgb, var(--af-accent) 8%, transparent)",
|
|
1015
|
+
border: "1px solid color-mix(in srgb, var(--af-accent) 22%, transparent)",
|
|
1016
|
+
fontSize: 10, fontFamily: "var(--font-mono)",
|
|
1017
|
+
color: "var(--af-accent)", textDecoration: "none", fontWeight: 600,
|
|
1018
|
+
}}>
|
|
1019
|
+
{dayName(o.date)} {o.date.slice(5)} · {o.project_display}
|
|
1020
|
+
{o.outcome ? ` · ${o.outcome}` : ""}
|
|
1021
|
+
</Link>
|
|
1022
|
+
))}
|
|
1023
|
+
</div>
|
|
1024
|
+
)}
|
|
1025
|
+
</li>
|
|
1026
|
+
);
|
|
1027
|
+
})}
|
|
1028
|
+
</ul>
|
|
1029
|
+
</Section>
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function formatOutcomeMix(dist: Partial<Record<string, number>>): string {
|
|
1034
|
+
const order = ["shipped", "partial", "blocked", "exploratory", "trivial", "idle"];
|
|
1035
|
+
const parts: string[] = [];
|
|
1036
|
+
for (const k of order) {
|
|
1037
|
+
const c = dist[k] ?? 0;
|
|
1038
|
+
if (c > 0) parts.push(`${c} ${k}`);
|
|
1039
|
+
}
|
|
1040
|
+
return parts.join(" · ");
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function InteractionGrammarSection({
|
|
1044
|
+
grammar,
|
|
1045
|
+
}: {
|
|
1046
|
+
grammar: NonNullable<WeekDigestType["interaction_grammar"]>;
|
|
1047
|
+
}) {
|
|
1048
|
+
const harnessLines = renderHarnessLines(grammar);
|
|
1049
|
+
const commLines = renderCommunicationLines(grammar);
|
|
1050
|
+
const claudeFeatureLines = renderClaudeFeatureLines(grammar);
|
|
1051
|
+
const miscLines = renderMiscGrammarLines(grammar);
|
|
1052
|
+
const allLines = [...harnessLines, ...commLines, ...claudeFeatureLines, ...miscLines];
|
|
1053
|
+
|
|
1054
|
+
if (allLines.length === 0) return null;
|
|
1055
|
+
return (
|
|
1056
|
+
<Section title="Your interaction grammar" anchor="grammar">
|
|
1057
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
1058
|
+
{harnessLines.length > 0 && (
|
|
1059
|
+
<GrammarSubsection title="Your own harness" subtitle="Skills, subagents, and slash commands you authored.">
|
|
1060
|
+
{harnessLines}
|
|
1061
|
+
</GrammarSubsection>
|
|
1062
|
+
)}
|
|
1063
|
+
{commLines.length > 0 && (
|
|
1064
|
+
<GrammarSubsection title="Communication style" subtitle="How you provide context per directive and how much you steer mid-flight.">
|
|
1065
|
+
{commLines}
|
|
1066
|
+
</GrammarSubsection>
|
|
1067
|
+
)}
|
|
1068
|
+
{claudeFeatureLines.length > 0 && (
|
|
1069
|
+
<GrammarSubsection title="Claude features in use" subtitle="Stock framings the user employs (not the user's own inventions).">
|
|
1070
|
+
{claudeFeatureLines}
|
|
1071
|
+
</GrammarSubsection>
|
|
1072
|
+
)}
|
|
1073
|
+
{miscLines.length > 0 && (
|
|
1074
|
+
<GrammarSubsection title="Other patterns" subtitle="Threads, rituals, and gates.">
|
|
1075
|
+
{miscLines}
|
|
1076
|
+
</GrammarSubsection>
|
|
1077
|
+
)}
|
|
1078
|
+
</div>
|
|
1079
|
+
</Section>
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function GrammarSubsection({ title, subtitle, children }: { title: string; subtitle: string; children: ReactNode }) {
|
|
1084
|
+
return (
|
|
1085
|
+
<div>
|
|
1086
|
+
<div style={{ marginBottom: 6 }}>
|
|
1087
|
+
<div style={{
|
|
1088
|
+
fontSize: 11, fontWeight: 600, color: "var(--af-text)",
|
|
1089
|
+
letterSpacing: "-0.01em",
|
|
1090
|
+
}}>{title}</div>
|
|
1091
|
+
<div style={{
|
|
1092
|
+
fontSize: 10.5, color: "var(--af-text-tertiary)",
|
|
1093
|
+
lineHeight: 1.45, marginTop: 1,
|
|
1094
|
+
}}>{subtitle}</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
1097
|
+
{children}
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function renderHarnessLines(grammar: NonNullable<WeekDigestType["interaction_grammar"]>): ReactNode[] {
|
|
1104
|
+
const out: ReactNode[] = [];
|
|
1105
|
+
|
|
1106
|
+
// Skill families take precedence — show the cohesive toolchain.
|
|
1107
|
+
for (const fam of grammar.skill_families ?? []) {
|
|
1108
|
+
out.push(
|
|
1109
|
+
<GrammarLine
|
|
1110
|
+
key={`fam-${fam.family}`}
|
|
1111
|
+
label={`${fam.family}-* skill family`}
|
|
1112
|
+
body={`${fam.total_count} loads across ${fam.members.length} skills: ${fam.members.join(", ")}.`}
|
|
1113
|
+
days={fam.days}
|
|
1114
|
+
/>
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Single-skill user-authored entries that aren't already covered by a family.
|
|
1119
|
+
const familyMembers = new Set(
|
|
1120
|
+
(grammar.skill_families ?? []).flatMap(f => f.members)
|
|
1121
|
+
);
|
|
1122
|
+
const standaloneSkills = grammar.user_authored_skills.filter(s => !familyMembers.has(s.skill));
|
|
1123
|
+
if (standaloneSkills.length > 0) {
|
|
1124
|
+
out.push(
|
|
1125
|
+
<GrammarLine
|
|
1126
|
+
key="standalone-skills"
|
|
1127
|
+
label="Other user-authored skills"
|
|
1128
|
+
body={standaloneSkills.slice(0, 5).map(s => `${s.skill} ×${s.count}`).join(" · ")}
|
|
1129
|
+
days={standaloneSkills.flatMap(s => s.days).filter((v, i, a) => a.indexOf(v) === i).sort()}
|
|
1130
|
+
/>
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
for (const sa of grammar.user_authored_subagents ?? []) {
|
|
1135
|
+
out.push(
|
|
1136
|
+
<div key={`sub-${sa.type}`} style={{
|
|
1137
|
+
padding: "10px 12px", borderRadius: 8,
|
|
1138
|
+
background: "var(--af-surface)",
|
|
1139
|
+
border: "1px solid color-mix(in srgb, var(--af-accent) 22%, var(--af-border-subtle))",
|
|
1140
|
+
}}>
|
|
1141
|
+
<div style={{
|
|
1142
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
|
|
1143
|
+
textTransform: "uppercase", color: "var(--af-accent)",
|
|
1144
|
+
marginBottom: 4,
|
|
1145
|
+
}}>User-authored subagent · {sa.type}</div>
|
|
1146
|
+
<div style={{ fontSize: 12, color: "var(--af-text)", lineHeight: 1.5 }}>
|
|
1147
|
+
Dispatched {sa.count} time{sa.count === 1 ? "" : "s"} on {sa.days.length} day{sa.days.length === 1 ? "" : "s"}.
|
|
1148
|
+
</div>
|
|
1149
|
+
{sa.sample_prompt_preview && (
|
|
1150
|
+
<div style={{
|
|
1151
|
+
marginTop: 6, padding: "6px 10px", borderRadius: 6,
|
|
1152
|
+
background: "var(--af-surface-raised)",
|
|
1153
|
+
borderLeft: "2px solid var(--af-accent)",
|
|
1154
|
+
fontSize: 11, lineHeight: 1.5, color: "var(--af-text)", fontStyle: "italic",
|
|
1155
|
+
}}>
|
|
1156
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>“</span>
|
|
1157
|
+
{sa.sample_prompt_preview}
|
|
1158
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>”</span>
|
|
1159
|
+
</div>
|
|
1160
|
+
)}
|
|
1161
|
+
{sa.days.length > 0 && (
|
|
1162
|
+
<div style={{ marginTop: 6 }}>
|
|
1163
|
+
<DayChips dates={sa.days} tone="var(--af-accent)" />
|
|
1164
|
+
</div>
|
|
1165
|
+
)}
|
|
1166
|
+
</div>
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Custom slash-command frame — surface here as user-authored commands the
|
|
1171
|
+
// user invoked (the harness's command surface).
|
|
1172
|
+
const slashCmd = grammar.prompt_frames.find(f => f.frame === "slash-command");
|
|
1173
|
+
if (slashCmd) {
|
|
1174
|
+
out.push(
|
|
1175
|
+
<GrammarLine
|
|
1176
|
+
key="slash-cmd"
|
|
1177
|
+
label="Custom slash-commands"
|
|
1178
|
+
body={`Invoked ${slashCmd.count} time${slashCmd.count === 1 ? "" : "s"} via <command-name> framing.`}
|
|
1179
|
+
days={slashCmd.days}
|
|
1180
|
+
/>
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return out;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function renderCommunicationLines(grammar: NonNullable<WeekDigestType["interaction_grammar"]>): ReactNode[] {
|
|
1188
|
+
const cs = grammar.communication_style;
|
|
1189
|
+
if (!cs) return [];
|
|
1190
|
+
const out: ReactNode[] = [];
|
|
1191
|
+
|
|
1192
|
+
// Verbosity histogram inline.
|
|
1193
|
+
const v = cs.verbosity_distribution;
|
|
1194
|
+
const total = v.short + v.medium + v.long + v.very_long;
|
|
1195
|
+
if (total > 0) {
|
|
1196
|
+
out.push(
|
|
1197
|
+
<GrammarLine
|
|
1198
|
+
key="verbosity"
|
|
1199
|
+
label="Prompt length distribution"
|
|
1200
|
+
body={`${v.short} short (<100c) · ${v.medium} medium (100–500c) · ${v.long} long (500–2000c) · ${v.very_long} very-long (>2000c) — across ${total} session opener${total === 1 ? "" : "s"}.`}
|
|
1201
|
+
/>
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// External refs.
|
|
1206
|
+
if (cs.external_context_refs && cs.external_context_refs.length > 0) {
|
|
1207
|
+
const byKind = new Map<string, number>();
|
|
1208
|
+
for (const r of cs.external_context_refs) {
|
|
1209
|
+
byKind.set(r.ref_kind, (byKind.get(r.ref_kind) ?? 0) + 1);
|
|
1210
|
+
}
|
|
1211
|
+
const summary = [...byKind.entries()].map(([k, c]) => `${c} ${k}`).join(" · ");
|
|
1212
|
+
out.push(
|
|
1213
|
+
<GrammarLine
|
|
1214
|
+
key="ext-refs"
|
|
1215
|
+
label="External-context references"
|
|
1216
|
+
body={`${cs.external_context_refs.length} session opener${cs.external_context_refs.length === 1 ? "" : "s"} delegated by reference rather than spelling out the work — ${summary}.`}
|
|
1217
|
+
days={[...new Set(cs.external_context_refs.map(r => r.date))].sort()}
|
|
1218
|
+
/>
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Steering intensity.
|
|
1223
|
+
const s = cs.steering;
|
|
1224
|
+
if (s && (s.total_interrupts > 0 || s.total_frustrated > 0 || s.total_dissatisfied > 0)) {
|
|
1225
|
+
const intensity = s.total_turns > 0 ? ((s.total_interrupts / s.total_turns) * 100) : 0;
|
|
1226
|
+
const parts: string[] = [];
|
|
1227
|
+
parts.push(`${s.total_interrupts} interrupt${s.total_interrupts === 1 ? "" : "s"} across ${s.total_turns} turns (${intensity.toFixed(1)}%)`);
|
|
1228
|
+
if (s.total_frustrated > 0) parts.push(`${s.total_frustrated} frustrated`);
|
|
1229
|
+
if (s.total_dissatisfied > 0) parts.push(`${s.total_dissatisfied} dissatisfied`);
|
|
1230
|
+
if (s.sessions_with_mid_run_redirect > 0) parts.push(`${s.sessions_with_mid_run_redirect} session${s.sessions_with_mid_run_redirect === 1 ? "" : "s"} with ≥2 interrupts`);
|
|
1231
|
+
out.push(
|
|
1232
|
+
<GrammarLine
|
|
1233
|
+
key="steering"
|
|
1234
|
+
label="Steering intensity"
|
|
1235
|
+
body={parts.join(" · ")}
|
|
1236
|
+
/>
|
|
1237
|
+
);
|
|
1238
|
+
} else if (s) {
|
|
1239
|
+
out.push(
|
|
1240
|
+
<GrammarLine
|
|
1241
|
+
key="steering-none"
|
|
1242
|
+
label="Steering intensity"
|
|
1243
|
+
body={`No interrupts or frustrated/dissatisfied signals across ${s.total_turns} turns — let the agent run.`}
|
|
1244
|
+
/>
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return out;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function renderClaudeFeatureLines(grammar: NonNullable<WeekDigestType["interaction_grammar"]>): ReactNode[] {
|
|
1252
|
+
const out: ReactNode[] = [];
|
|
1253
|
+
for (const f of grammar.prompt_frames) {
|
|
1254
|
+
if (f.origin !== "claude-feature") continue;
|
|
1255
|
+
if (f.frame === "slash-command") continue; // already shown under Harness
|
|
1256
|
+
const label = FRAME_LABELS[f.frame] ?? f.frame;
|
|
1257
|
+
const help = FRAME_HELP[f.frame] ?? "";
|
|
1258
|
+
out.push(
|
|
1259
|
+
<GrammarLine
|
|
1260
|
+
key={`cf-${f.frame}`}
|
|
1261
|
+
label={label}
|
|
1262
|
+
body={`${f.count} session opener${f.count === 1 ? "" : "s"}${help ? `. ${help}` : "."}`}
|
|
1263
|
+
days={f.days}
|
|
1264
|
+
/>
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function renderMiscGrammarLines(grammar: NonNullable<WeekDigestType["interaction_grammar"]>): ReactNode[] {
|
|
1271
|
+
const out: ReactNode[] = [];
|
|
1272
|
+
|
|
1273
|
+
if (grammar.brainstorming_warmup_days.length > 0) {
|
|
1274
|
+
out.push(
|
|
1275
|
+
<GrammarLine
|
|
1276
|
+
key="brainstorm"
|
|
1277
|
+
label="Brainstorming as warmup"
|
|
1278
|
+
body={`Loaded a brainstorming/writing-plans skill before tool use on ${grammar.brainstorming_warmup_days.length} day${grammar.brainstorming_warmup_days.length === 1 ? "" : "s"}.`}
|
|
1279
|
+
days={grammar.brainstorming_warmup_days}
|
|
1280
|
+
/>
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Personal-habit frames (handoff-prose).
|
|
1285
|
+
for (const f of grammar.prompt_frames) {
|
|
1286
|
+
if (f.origin !== "personal-habit") continue;
|
|
1287
|
+
out.push(
|
|
1288
|
+
<GrammarLine
|
|
1289
|
+
key={`ph-${f.frame}`}
|
|
1290
|
+
label={`${FRAME_LABELS[f.frame] ?? f.frame} (personal habit)`}
|
|
1291
|
+
body={`${f.count} session opener${f.count === 1 ? "" : "s"}. ${FRAME_HELP[f.frame] ?? ""}`}
|
|
1292
|
+
days={f.days}
|
|
1293
|
+
/>
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Threads — show all, with a "+N more" fold past 5.
|
|
1298
|
+
if (grammar.threads.length > 0) {
|
|
1299
|
+
out.push(<ThreadList key="threads" threads={grammar.threads} />);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (grammar.todo_ops_total > 0) {
|
|
1303
|
+
out.push(
|
|
1304
|
+
<GrammarLine
|
|
1305
|
+
key="todo"
|
|
1306
|
+
label="TodoWrite ops"
|
|
1307
|
+
body={`${grammar.todo_ops_total} TodoWrite operations across the week — task layer as orchestration substrate.`}
|
|
1308
|
+
/>
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (grammar.plan_mode.exit_plan_calls === 0 && grammar.plan_mode.days_with_plan === 0) {
|
|
1313
|
+
out.push(
|
|
1314
|
+
<GrammarLine
|
|
1315
|
+
key="no-plan"
|
|
1316
|
+
label="Plan Mode unused"
|
|
1317
|
+
body="No ExitPlan calls or plan-gated sessions this week. Planning happened by other means (specs / review subagents) or not at all."
|
|
1318
|
+
/>
|
|
1319
|
+
);
|
|
1320
|
+
} else {
|
|
1321
|
+
out.push(
|
|
1322
|
+
<GrammarLine
|
|
1323
|
+
key="plan-mode"
|
|
1324
|
+
label="Plan Mode"
|
|
1325
|
+
body={`${grammar.plan_mode.exit_plan_calls} ExitPlan call${grammar.plan_mode.exit_plan_calls === 1 ? "" : "s"} on ${grammar.plan_mode.days_with_plan}/7 days.`}
|
|
1326
|
+
/>
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
return out;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function ThreadList({ threads }: { threads: NonNullable<WeekDigestType["interaction_grammar"]>["threads"] }) {
|
|
1334
|
+
const [expanded, setExpanded] = useState(false);
|
|
1335
|
+
const sorted = threads.slice().sort((a, b) => b.entries.length - a.entries.length || b.total_active_min - a.total_active_min);
|
|
1336
|
+
const visible = expanded ? sorted : sorted.slice(0, 5);
|
|
1337
|
+
const hidden = sorted.length - visible.length;
|
|
1338
|
+
return (
|
|
1339
|
+
<div style={{
|
|
1340
|
+
padding: "10px 12px", borderRadius: 8,
|
|
1341
|
+
background: "var(--af-surface)",
|
|
1342
|
+
border: "1px solid var(--af-border-subtle)",
|
|
1343
|
+
}}>
|
|
1344
|
+
<div style={{
|
|
1345
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
|
|
1346
|
+
textTransform: "uppercase", color: "var(--af-text-tertiary)",
|
|
1347
|
+
marginBottom: 6,
|
|
1348
|
+
}}>Multi-day session threads · {threads.length}</div>
|
|
1349
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 4 }}>
|
|
1350
|
+
{visible.map(t => {
|
|
1351
|
+
const dayList = t.entries.map(e => e.date).sort();
|
|
1352
|
+
const project = t.entries[0]?.project_display ?? "";
|
|
1353
|
+
return (
|
|
1354
|
+
<li key={t.thread_id} style={{
|
|
1355
|
+
fontSize: 11, color: "var(--af-text)", lineHeight: 1.5,
|
|
1356
|
+
fontFamily: "var(--font-mono)",
|
|
1357
|
+
display: "flex", gap: 8, alignItems: "baseline",
|
|
1358
|
+
}}>
|
|
1359
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>{t.thread_id.slice(0, 8)}</span>
|
|
1360
|
+
<span style={{ color: "var(--af-text-secondary)" }}>{project}</span>
|
|
1361
|
+
<span style={{ color: "var(--af-text)" }}>{dayList.length}d · {Math.round(t.total_active_min)}m</span>
|
|
1362
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>{dayList[0]} → {dayList[dayList.length - 1]}</span>
|
|
1363
|
+
{t.outcome && <span style={{ color: "var(--af-text-secondary)" }}>· {t.outcome}</span>}
|
|
1364
|
+
</li>
|
|
1365
|
+
);
|
|
1366
|
+
})}
|
|
1367
|
+
</ul>
|
|
1368
|
+
{hidden > 0 && (
|
|
1369
|
+
<button
|
|
1370
|
+
type="button"
|
|
1371
|
+
onClick={() => setExpanded(true)}
|
|
1372
|
+
style={{
|
|
1373
|
+
marginTop: 6, padding: "3px 8px", borderRadius: 4,
|
|
1374
|
+
background: "transparent", border: "1px solid var(--af-border-subtle)",
|
|
1375
|
+
fontSize: 10, color: "var(--af-text-secondary)", cursor: "pointer",
|
|
1376
|
+
fontFamily: "var(--font-mono)",
|
|
1377
|
+
}}
|
|
1378
|
+
>+{hidden} more</button>
|
|
1379
|
+
)}
|
|
1380
|
+
{expanded && sorted.length > 5 && (
|
|
1381
|
+
<button
|
|
1382
|
+
type="button"
|
|
1383
|
+
onClick={() => setExpanded(false)}
|
|
1384
|
+
style={{
|
|
1385
|
+
marginTop: 6, padding: "3px 8px", borderRadius: 4,
|
|
1386
|
+
background: "transparent", border: "1px solid var(--af-border-subtle)",
|
|
1387
|
+
fontSize: 10, color: "var(--af-text-secondary)", cursor: "pointer",
|
|
1388
|
+
fontFamily: "var(--font-mono)",
|
|
1389
|
+
}}
|
|
1390
|
+
>show less</button>
|
|
1391
|
+
)}
|
|
1392
|
+
</div>
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function GrammarLine({
|
|
1397
|
+
label, body, days,
|
|
1398
|
+
}: { label: string; body: string; days?: string[] }) {
|
|
1399
|
+
return (
|
|
1400
|
+
<div style={{
|
|
1401
|
+
padding: "10px 12px", borderRadius: 8,
|
|
1402
|
+
background: "var(--af-surface)",
|
|
1403
|
+
border: "1px solid var(--af-border-subtle)",
|
|
1404
|
+
}}>
|
|
1405
|
+
<div style={{
|
|
1406
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
|
|
1407
|
+
textTransform: "uppercase", color: "var(--af-text-tertiary)",
|
|
1408
|
+
marginBottom: 4,
|
|
1409
|
+
}}>{label}</div>
|
|
1410
|
+
<div style={{ fontSize: 12, lineHeight: 1.5, color: "var(--af-text)" }}>{body}</div>
|
|
1411
|
+
{days && days.length > 0 && (
|
|
1412
|
+
<div style={{ marginTop: 6 }}>
|
|
1413
|
+
<DayChips dates={days} tone="var(--af-accent)" />
|
|
1414
|
+
</div>
|
|
1415
|
+
)}
|
|
1416
|
+
</div>
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function FindingsSection({
|
|
1421
|
+
title, anchor, items, tone, sectionFallbackProse,
|
|
1422
|
+
}: {
|
|
1423
|
+
title: string;
|
|
1424
|
+
anchor: string;
|
|
1425
|
+
items: NonNullable<WeekDigestType["what_worked"]>;
|
|
1426
|
+
tone: string;
|
|
1427
|
+
sectionFallbackProse: string | null;
|
|
1428
|
+
}) {
|
|
1429
|
+
if (!items || items.length === 0) {
|
|
1430
|
+
if (sectionFallbackProse === null) return null;
|
|
1431
|
+
return (
|
|
1432
|
+
<Section title={title} anchor={anchor}>
|
|
1433
|
+
<p style={{
|
|
1434
|
+
fontSize: 12, fontStyle: "italic",
|
|
1435
|
+
color: "var(--af-text-tertiary)",
|
|
1436
|
+
margin: 0, padding: "8px 0",
|
|
1437
|
+
}}>{sectionFallbackProse}</p>
|
|
1438
|
+
</Section>
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
return (
|
|
1442
|
+
<Section title={title} anchor={anchor}>
|
|
1443
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
|
|
1444
|
+
{items.map((item, i) => (
|
|
1445
|
+
<FindingCard key={i} item={item} tone={tone} />
|
|
1446
|
+
))}
|
|
1447
|
+
</ul>
|
|
1448
|
+
</Section>
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function FindingCard({
|
|
1453
|
+
item, tone,
|
|
1454
|
+
}: {
|
|
1455
|
+
item: NonNullable<WeekDigestType["what_worked"]>[number];
|
|
1456
|
+
tone: string;
|
|
1457
|
+
}) {
|
|
1458
|
+
const anchorLabel = renderAnchor(item.anchor);
|
|
1459
|
+
return (
|
|
1460
|
+
<li style={{
|
|
1461
|
+
padding: "12px 14px", borderRadius: 10,
|
|
1462
|
+
background: `color-mix(in srgb, ${tone} 6%, var(--af-surface))`,
|
|
1463
|
+
border: `1px solid color-mix(in srgb, ${tone} 22%, var(--af-border))`,
|
|
1464
|
+
}}>
|
|
1465
|
+
<div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 4, flexWrap: "wrap" }}>
|
|
1466
|
+
<span style={{
|
|
1467
|
+
fontSize: 13, fontWeight: 600, color: "var(--af-text)",
|
|
1468
|
+
letterSpacing: "-0.01em", flex: 1, minWidth: 0,
|
|
1469
|
+
}}>{item.title}</span>
|
|
1470
|
+
<span style={{
|
|
1471
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.04em",
|
|
1472
|
+
textTransform: "uppercase", color: tone,
|
|
1473
|
+
padding: "1px 7px", borderRadius: 999,
|
|
1474
|
+
background: `color-mix(in srgb, ${tone} 14%, transparent)`,
|
|
1475
|
+
border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`,
|
|
1476
|
+
fontFamily: "var(--font-mono)",
|
|
1477
|
+
}} title={`Anchor: ${item.anchor}`}>{anchorLabel}</span>
|
|
1478
|
+
</div>
|
|
1479
|
+
<p style={{ fontSize: 12, lineHeight: 1.6, margin: "0 0 8px", color: "var(--af-text)" }}>
|
|
1480
|
+
{item.detail}
|
|
1481
|
+
</p>
|
|
1482
|
+
<div style={{
|
|
1483
|
+
padding: "6px 10px", borderRadius: 6,
|
|
1484
|
+
background: "var(--af-surface-raised)",
|
|
1485
|
+
borderLeft: `2px solid color-mix(in srgb, ${tone} 50%, transparent)`,
|
|
1486
|
+
display: "flex", gap: 8, alignItems: "flex-start",
|
|
1487
|
+
}}>
|
|
1488
|
+
<Link href={`/digest/${item.evidence.date}`} style={{
|
|
1489
|
+
fontSize: 10, fontWeight: 600, color: tone,
|
|
1490
|
+
fontFamily: "var(--font-mono)", flexShrink: 0,
|
|
1491
|
+
textDecoration: "none", paddingTop: 2,
|
|
1492
|
+
}}>
|
|
1493
|
+
{dayName(item.evidence.date)} {item.evidence.date.slice(5)}
|
|
1494
|
+
</Link>
|
|
1495
|
+
<span style={{ fontSize: 11, lineHeight: 1.5, color: "var(--af-text)", fontStyle: "italic" }}>
|
|
1496
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>“</span>
|
|
1497
|
+
{item.evidence.quote}
|
|
1498
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>”</span>
|
|
1499
|
+
</span>
|
|
1500
|
+
</div>
|
|
1501
|
+
</li>
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function renderAnchor(anchor: string): string {
|
|
1506
|
+
if (anchor === "plan-mode-gap") return "Plan-mode gap";
|
|
1507
|
+
if (anchor.startsWith("interaction_grammar.")) {
|
|
1508
|
+
const k = anchor.slice("interaction_grammar.".length);
|
|
1509
|
+
return k.replace(/_/g, " ");
|
|
1510
|
+
}
|
|
1511
|
+
return SHAPE_LABELS[anchor] ?? anchor;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function SurprisesSection({
|
|
1515
|
+
items,
|
|
1516
|
+
}: {
|
|
1517
|
+
items: NonNullable<WeekDigestType["what_surprised"]>;
|
|
1518
|
+
}) {
|
|
1519
|
+
if (!items || items.length === 0) return null;
|
|
1520
|
+
return (
|
|
1521
|
+
<Section title="What surprised" anchor="what-surprised">
|
|
1522
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
|
|
1523
|
+
{items.map((item, i) => {
|
|
1524
|
+
const tone = "#b794f4";
|
|
1525
|
+
return (
|
|
1526
|
+
<li key={i} style={{
|
|
1527
|
+
padding: "12px 14px", borderRadius: 10,
|
|
1528
|
+
background: `color-mix(in srgb, ${tone} 6%, var(--af-surface))`,
|
|
1529
|
+
border: `1px solid color-mix(in srgb, ${tone} 22%, var(--af-border))`,
|
|
1530
|
+
}}>
|
|
1531
|
+
<div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 4, flexWrap: "wrap" }}>
|
|
1532
|
+
<span style={{
|
|
1533
|
+
fontSize: 13, fontWeight: 600, color: "var(--af-text)",
|
|
1534
|
+
letterSpacing: "-0.01em", flex: 1, minWidth: 0,
|
|
1535
|
+
}}>{item.title}</span>
|
|
1536
|
+
<span style={{
|
|
1537
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.04em",
|
|
1538
|
+
textTransform: "uppercase", color: tone,
|
|
1539
|
+
padding: "1px 7px", borderRadius: 999,
|
|
1540
|
+
background: `color-mix(in srgb, ${tone} 14%, transparent)`,
|
|
1541
|
+
border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`,
|
|
1542
|
+
fontFamily: "var(--font-mono)",
|
|
1543
|
+
}}>{SURPRISE_LABELS[item.surprise_kind] ?? item.surprise_kind}</span>
|
|
1544
|
+
</div>
|
|
1545
|
+
<p style={{ fontSize: 12, lineHeight: 1.6, margin: "0 0 8px", color: "var(--af-text)" }}>
|
|
1546
|
+
{item.detail}
|
|
1547
|
+
</p>
|
|
1548
|
+
<div style={{
|
|
1549
|
+
padding: "6px 10px", borderRadius: 6,
|
|
1550
|
+
background: "var(--af-surface-raised)",
|
|
1551
|
+
borderLeft: `2px solid color-mix(in srgb, ${tone} 50%, transparent)`,
|
|
1552
|
+
display: "flex", gap: 8, alignItems: "flex-start",
|
|
1553
|
+
}}>
|
|
1554
|
+
<Link href={`/digest/${item.evidence.date}`} style={{
|
|
1555
|
+
fontSize: 10, fontWeight: 600, color: tone,
|
|
1556
|
+
fontFamily: "var(--font-mono)", flexShrink: 0,
|
|
1557
|
+
textDecoration: "none", paddingTop: 2,
|
|
1558
|
+
}}>
|
|
1559
|
+
{dayName(item.evidence.date)} {item.evidence.date.slice(5)}
|
|
1560
|
+
</Link>
|
|
1561
|
+
<span style={{ fontSize: 11, lineHeight: 1.5, color: "var(--af-text)", fontStyle: "italic" }}>
|
|
1562
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>“</span>
|
|
1563
|
+
{item.evidence.quote}
|
|
1564
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>”</span>
|
|
1565
|
+
</span>
|
|
1566
|
+
</div>
|
|
1567
|
+
</li>
|
|
1568
|
+
);
|
|
1569
|
+
})}
|
|
1570
|
+
</ul>
|
|
1571
|
+
</Section>
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function WhereToLeanSection({
|
|
1576
|
+
items,
|
|
1577
|
+
}: {
|
|
1578
|
+
items: NonNullable<WeekDigestType["where_to_lean"]>;
|
|
1579
|
+
}) {
|
|
1580
|
+
if (!items || items.length === 0) return null;
|
|
1581
|
+
return (
|
|
1582
|
+
<Section title="Where to lean" anchor="where-to-lean">
|
|
1583
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 12 }}>
|
|
1584
|
+
{items.map((item, i) => {
|
|
1585
|
+
const tone = "var(--af-accent)";
|
|
1586
|
+
const anchorLabel = renderAnchor(item.anchor);
|
|
1587
|
+
return (
|
|
1588
|
+
<li key={i} style={{
|
|
1589
|
+
padding: "14px 16px", borderRadius: 10,
|
|
1590
|
+
background: "var(--af-surface)",
|
|
1591
|
+
border: "1px solid var(--af-border-subtle)",
|
|
1592
|
+
}}>
|
|
1593
|
+
<div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 6, flexWrap: "wrap" }}>
|
|
1594
|
+
<span style={{
|
|
1595
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.06em",
|
|
1596
|
+
textTransform: "uppercase", color: tone,
|
|
1597
|
+
padding: "1px 7px", borderRadius: 999,
|
|
1598
|
+
background: `color-mix(in srgb, ${tone} 14%, transparent)`,
|
|
1599
|
+
border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`,
|
|
1600
|
+
fontFamily: "var(--font-mono)",
|
|
1601
|
+
}}>{LEAN_KIND_LABELS[item.lean_kind] ?? item.lean_kind}</span>
|
|
1602
|
+
<span style={{ fontSize: 13, fontWeight: 600, color: "var(--af-text)", flex: 1, minWidth: 0 }}>
|
|
1603
|
+
{item.title}
|
|
1604
|
+
</span>
|
|
1605
|
+
<span style={{
|
|
1606
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.04em",
|
|
1607
|
+
textTransform: "uppercase", color: "var(--af-text-tertiary)",
|
|
1608
|
+
fontFamily: "var(--font-mono)",
|
|
1609
|
+
}} title={`Anchor: ${item.anchor}`}>↳ {anchorLabel}</span>
|
|
1610
|
+
</div>
|
|
1611
|
+
<p style={{ fontSize: 12, lineHeight: 1.6, margin: "0 0 10px", color: "var(--af-text)" }}>
|
|
1612
|
+
{item.detail}
|
|
1613
|
+
</p>
|
|
1614
|
+
{item.copyable && (
|
|
1615
|
+
<CopyBlock label={`Copy ${item.lean_kind === "claude-md" ? "block" : "snippet"}`} payload={item.copyable} />
|
|
1616
|
+
)}
|
|
1617
|
+
<div style={{
|
|
1618
|
+
marginTop: 8, fontSize: 10, color: "var(--af-text-tertiary)",
|
|
1619
|
+
fontFamily: "var(--font-mono)",
|
|
1620
|
+
}}>
|
|
1621
|
+
Evidence · <Link href={`/digest/${item.evidence.date}`} style={{ color: "var(--af-text-tertiary)", textDecoration: "underline" }}>
|
|
1622
|
+
{dayName(item.evidence.date)} {item.evidence.date.slice(5)}
|
|
1623
|
+
</Link> · “{item.evidence.quote.slice(0, 100)}{item.evidence.quote.length > 100 ? "…" : ""}”
|
|
1624
|
+
</div>
|
|
1625
|
+
</li>
|
|
1626
|
+
);
|
|
1627
|
+
})}
|
|
1628
|
+
</ul>
|
|
1629
|
+
</Section>
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
1634
|
+
|
|
1635
|
+
function Section({ title, anchor, children }: { title: string; anchor?: string; children: ReactNode }) {
|
|
1636
|
+
return (
|
|
1637
|
+
<section id={anchor} style={{ marginBottom: 28, scrollMarginTop: 24 }}>
|
|
1638
|
+
<h2 style={sectionTitleStyle()}>{title}</h2>
|
|
1639
|
+
{children}
|
|
1640
|
+
</section>
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function sectionTitleStyle(): React.CSSProperties {
|
|
1645
|
+
return {
|
|
1646
|
+
fontSize: 11, fontWeight: 700, letterSpacing: "0.08em",
|
|
1647
|
+
textTransform: "uppercase", color: "var(--af-text-tertiary)",
|
|
1648
|
+
margin: "0 0 12px",
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function cardStyle(): React.CSSProperties {
|
|
1653
|
+
return {
|
|
1654
|
+
padding: "12px 14px", borderRadius: 8,
|
|
1655
|
+
background: "var(--af-surface)",
|
|
1656
|
+
border: "1px solid var(--af-border-subtle)",
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function dayName(date: string): string {
|
|
1661
|
+
const d = new Date(`${date}T12:00:00`);
|
|
1662
|
+
return d.toLocaleDateString("en-US", { weekday: "short" });
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function formatRange(startIso: string, endIso: string): string {
|
|
1666
|
+
const s = new Date(startIso);
|
|
1667
|
+
const e = new Date(endIso);
|
|
1668
|
+
const fmt = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1669
|
+
return `${fmt(s)} – ${fmt(e)}`;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// ─── Top sessions section ─────────────────────────────────────────────────
|
|
1673
|
+
|
|
1674
|
+
const PIN_COLORS: Record<string, string> = {
|
|
1675
|
+
"user-steering": "#ed8936",
|
|
1676
|
+
"subagent-burst": "#9f7aea",
|
|
1677
|
+
"long-autonomous": "#4299e1",
|
|
1678
|
+
"plan-mode": "#38b2ac",
|
|
1679
|
+
"pr-ship": "#48bb78",
|
|
1680
|
+
"harness-chain": "#ed64a6",
|
|
1681
|
+
"interrupt": "#f56565",
|
|
1682
|
+
"brainstorm-loop": "#9f7aea",
|
|
1683
|
+
"agent-loop": "#f56565",
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
const PIN_LABELS: Record<string, string> = {
|
|
1687
|
+
"user-steering": "Steering",
|
|
1688
|
+
"subagent-burst": "Subagents",
|
|
1689
|
+
"long-autonomous": "Autonomous",
|
|
1690
|
+
"plan-mode": "Plan",
|
|
1691
|
+
"pr-ship": "Shipped",
|
|
1692
|
+
"harness-chain": "Harness",
|
|
1693
|
+
"interrupt": "Interrupt",
|
|
1694
|
+
"brainstorm-loop": "Brainstorm",
|
|
1695
|
+
"agent-loop": "Loop",
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
function TopSessionsSection({ sessions }: { sessions: WeekTopSession[] }) {
|
|
1699
|
+
return (
|
|
1700
|
+
<Section title={`Top ${sessions.length} session${sessions.length === 1 ? "" : "s"} this week`} anchor="top-sessions">
|
|
1701
|
+
<p style={{
|
|
1702
|
+
fontSize: 12, color: "var(--af-text-tertiary)", marginTop: -4,
|
|
1703
|
+
marginBottom: 14, fontStyle: "italic", lineHeight: 1.5, maxWidth: 720,
|
|
1704
|
+
}}>
|
|
1705
|
+
Picked by significance — active time × subagents × ships, capped to one per project. Each card walks the timeline with annotated moments to surface the texture of how you drove the agent.
|
|
1706
|
+
</p>
|
|
1707
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
1708
|
+
{sessions.map(s => <TopSessionCard key={s.session_id} session={s} />)}
|
|
1709
|
+
</div>
|
|
1710
|
+
</Section>
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function TopSessionCard({ session }: { session: WeekTopSession }) {
|
|
1715
|
+
const shapeLabel = session.working_shape ? (SHAPE_LABELS[session.working_shape] ?? session.working_shape) : null;
|
|
1716
|
+
const shapeColor = session.working_shape ? (SHAPE_COLORS[session.working_shape] ?? "var(--af-text-tertiary)") : "var(--af-text-tertiary)";
|
|
1717
|
+
const wallH = session.wall_min / 60;
|
|
1718
|
+
const activeH = session.active_min / 60;
|
|
1719
|
+
const wallStr = wallH >= 1 ? `${wallH.toFixed(1)}h wall` : `${Math.round(session.wall_min)}m wall`;
|
|
1720
|
+
const activeStr = activeH >= 1 ? `${activeH.toFixed(1)}h active` : `${Math.round(session.active_min)}m active`;
|
|
1721
|
+
|
|
1722
|
+
return (
|
|
1723
|
+
<div style={{
|
|
1724
|
+
borderRadius: 10,
|
|
1725
|
+
background: "var(--af-surface)",
|
|
1726
|
+
border: "1px solid var(--af-border-subtle)",
|
|
1727
|
+
borderTop: `3px solid ${shapeColor}`,
|
|
1728
|
+
overflow: "hidden",
|
|
1729
|
+
}}>
|
|
1730
|
+
<div style={{ padding: "16px 18px" }}>
|
|
1731
|
+
{/* Header row */}
|
|
1732
|
+
<div style={{
|
|
1733
|
+
display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap",
|
|
1734
|
+
marginBottom: 6,
|
|
1735
|
+
}}>
|
|
1736
|
+
<Link
|
|
1737
|
+
href={`/sessions/${session.session_id}`}
|
|
1738
|
+
style={{
|
|
1739
|
+
fontSize: 14, fontWeight: 600, color: "var(--af-text)",
|
|
1740
|
+
letterSpacing: "-0.01em", textDecoration: "none",
|
|
1741
|
+
}}
|
|
1742
|
+
>
|
|
1743
|
+
{session.project_display}
|
|
1744
|
+
</Link>
|
|
1745
|
+
<Link
|
|
1746
|
+
href={`/digest/${session.date}`}
|
|
1747
|
+
style={{
|
|
1748
|
+
fontSize: 11, fontFamily: "var(--font-mono)",
|
|
1749
|
+
color: "var(--af-accent)", textDecoration: "none",
|
|
1750
|
+
}}
|
|
1751
|
+
>
|
|
1752
|
+
{dayName(session.date)} {session.date.slice(5)}
|
|
1753
|
+
</Link>
|
|
1754
|
+
<span style={{
|
|
1755
|
+
fontSize: 11, color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)",
|
|
1756
|
+
}}>
|
|
1757
|
+
{activeStr} · {wallStr} · {session.turn_count} turns
|
|
1758
|
+
</span>
|
|
1759
|
+
{shapeLabel && (
|
|
1760
|
+
<span style={{
|
|
1761
|
+
padding: "2px 9px", borderRadius: 999, fontSize: 11, fontWeight: 600,
|
|
1762
|
+
background: `color-mix(in srgb, ${shapeColor} 14%, transparent)`,
|
|
1763
|
+
color: shapeColor,
|
|
1764
|
+
}}>
|
|
1765
|
+
{shapeLabel}
|
|
1766
|
+
</span>
|
|
1767
|
+
)}
|
|
1768
|
+
{session.outcome === "shipped" && session.shipped_prs.length > 0 && (
|
|
1769
|
+
<span style={{
|
|
1770
|
+
padding: "2px 9px", borderRadius: 999, fontSize: 11, fontWeight: 600,
|
|
1771
|
+
background: "color-mix(in srgb, #48bb78 14%, transparent)",
|
|
1772
|
+
color: "#48bb78",
|
|
1773
|
+
}}>
|
|
1774
|
+
✓ {session.shipped_prs.length} PR{session.shipped_prs.length === 1 ? "" : "s"}
|
|
1775
|
+
</span>
|
|
1776
|
+
)}
|
|
1777
|
+
</div>
|
|
1778
|
+
|
|
1779
|
+
{/* day_signature italic subhead */}
|
|
1780
|
+
{session.day_signature && (
|
|
1781
|
+
<p style={{
|
|
1782
|
+
fontSize: 12, fontStyle: "italic", color: "var(--af-text-secondary)",
|
|
1783
|
+
margin: "0 0 14px", lineHeight: 1.5, maxWidth: 760,
|
|
1784
|
+
}}>
|
|
1785
|
+
“{session.day_signature}”
|
|
1786
|
+
</p>
|
|
1787
|
+
)}
|
|
1788
|
+
|
|
1789
|
+
{/* Structured deep-dive: why-picked / what-happened / what-worked / what-hit-friction */}
|
|
1790
|
+
<NarrativeBlock session={session} />
|
|
1791
|
+
|
|
1792
|
+
{/* Steering summary */}
|
|
1793
|
+
{session.steering_summary && (
|
|
1794
|
+
<p style={{
|
|
1795
|
+
fontSize: 12, lineHeight: 1.55, margin: "10px 0 12px",
|
|
1796
|
+
color: "var(--af-text-secondary)", maxWidth: 780,
|
|
1797
|
+
paddingLeft: 10, borderLeft: "2px solid var(--af-border-subtle)",
|
|
1798
|
+
}}>
|
|
1799
|
+
<span style={{
|
|
1800
|
+
fontSize: 9, fontWeight: 700, letterSpacing: "0.06em",
|
|
1801
|
+
textTransform: "uppercase", color: "var(--af-text-tertiary)",
|
|
1802
|
+
marginRight: 6,
|
|
1803
|
+
}}>steering</span>
|
|
1804
|
+
{session.steering_summary}
|
|
1805
|
+
</p>
|
|
1806
|
+
)}
|
|
1807
|
+
|
|
1808
|
+
{/* Harness signature chips */}
|
|
1809
|
+
<HarnessChips session={session} />
|
|
1810
|
+
|
|
1811
|
+
{/* Timeline minimap */}
|
|
1812
|
+
<SessionTimelineMinimap session={session} />
|
|
1813
|
+
|
|
1814
|
+
{/* Pin list */}
|
|
1815
|
+
{session.pins.length > 0 && (
|
|
1816
|
+
<ol style={{
|
|
1817
|
+
listStyle: "none", padding: 0, margin: "12px 0 0",
|
|
1818
|
+
display: "flex", flexDirection: "column", gap: 6,
|
|
1819
|
+
}}>
|
|
1820
|
+
{session.pins.map((pin, i) => <PinRow key={i} pin={pin} />)}
|
|
1821
|
+
</ol>
|
|
1822
|
+
)}
|
|
1823
|
+
</div>
|
|
1824
|
+
</div>
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function NarrativeBlock({ session }: { session: WeekTopSession }) {
|
|
1829
|
+
// Backward-compat: cached pre-refactor digests only have session_summary.
|
|
1830
|
+
// Render the new structured fields when present; fall back to the old
|
|
1831
|
+
// single summary otherwise.
|
|
1832
|
+
const hasStructured = session.why_picked || session.what_worked || session.what_hit_friction;
|
|
1833
|
+
if (!hasStructured) {
|
|
1834
|
+
if (!session.session_summary) return null;
|
|
1835
|
+
return (
|
|
1836
|
+
<p style={{
|
|
1837
|
+
fontSize: 13, lineHeight: 1.55, margin: "0 0 8px",
|
|
1838
|
+
color: "var(--af-text)", maxWidth: 780,
|
|
1839
|
+
}}>
|
|
1840
|
+
{session.session_summary}
|
|
1841
|
+
</p>
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
return (
|
|
1845
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 10, margin: "0 0 6px" }}>
|
|
1846
|
+
{session.why_picked && (
|
|
1847
|
+
<NarrativeRow
|
|
1848
|
+
label="Why this session"
|
|
1849
|
+
tone="var(--af-text-tertiary)"
|
|
1850
|
+
accent="var(--af-text-tertiary)"
|
|
1851
|
+
text={session.why_picked}
|
|
1852
|
+
/>
|
|
1853
|
+
)}
|
|
1854
|
+
{session.session_summary && (
|
|
1855
|
+
<NarrativeRow
|
|
1856
|
+
label="What happened"
|
|
1857
|
+
tone="var(--af-text)"
|
|
1858
|
+
accent="var(--af-text-secondary)"
|
|
1859
|
+
text={session.session_summary}
|
|
1860
|
+
weight={500}
|
|
1861
|
+
/>
|
|
1862
|
+
)}
|
|
1863
|
+
{session.what_worked && (
|
|
1864
|
+
<NarrativeRow
|
|
1865
|
+
label="What worked"
|
|
1866
|
+
tone="var(--af-text)"
|
|
1867
|
+
accent="#48bb78"
|
|
1868
|
+
text={session.what_worked}
|
|
1869
|
+
/>
|
|
1870
|
+
)}
|
|
1871
|
+
{session.what_hit_friction && (
|
|
1872
|
+
<NarrativeRow
|
|
1873
|
+
label="What hit friction"
|
|
1874
|
+
tone="var(--af-text)"
|
|
1875
|
+
accent="#ed8936"
|
|
1876
|
+
text={session.what_hit_friction}
|
|
1877
|
+
/>
|
|
1878
|
+
)}
|
|
1879
|
+
</div>
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function NarrativeRow({ label, tone, accent, text, weight = 400 }: {
|
|
1884
|
+
label: string;
|
|
1885
|
+
tone: string;
|
|
1886
|
+
accent: string;
|
|
1887
|
+
text: string;
|
|
1888
|
+
weight?: number;
|
|
1889
|
+
}) {
|
|
1890
|
+
return (
|
|
1891
|
+
<div style={{ display: "flex", gap: 12, alignItems: "flex-start", maxWidth: 800 }}>
|
|
1892
|
+
<span style={{
|
|
1893
|
+
flexShrink: 0, width: 132, paddingTop: 2,
|
|
1894
|
+
fontSize: 9.5, fontWeight: 700, letterSpacing: "0.06em",
|
|
1895
|
+
textTransform: "uppercase", color: accent,
|
|
1896
|
+
}}>
|
|
1897
|
+
{label}
|
|
1898
|
+
</span>
|
|
1899
|
+
<span style={{
|
|
1900
|
+
fontSize: 13, lineHeight: 1.55, color: tone, fontWeight: weight,
|
|
1901
|
+
}}>
|
|
1902
|
+
{text}
|
|
1903
|
+
</span>
|
|
1904
|
+
</div>
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function HarnessChips({ session }: { session: WeekTopSession }) {
|
|
1909
|
+
const { user_authored_skills, user_authored_subagents, stock_skills, top_tools } = session;
|
|
1910
|
+
const hasAny = user_authored_skills.length > 0 || user_authored_subagents.length > 0
|
|
1911
|
+
|| stock_skills.length > 0 || top_tools.length > 0;
|
|
1912
|
+
if (!hasAny) return null;
|
|
1913
|
+
return (
|
|
1914
|
+
<div style={{ marginBottom: 4 }}>
|
|
1915
|
+
{user_authored_subagents.length > 0 && (
|
|
1916
|
+
<ChipRow label="Your subagents">
|
|
1917
|
+
{user_authored_subagents.map(sa => (
|
|
1918
|
+
<Chip key={sa.type} tone="user">
|
|
1919
|
+
{sa.type}{sa.count > 1 ? ` ×${sa.count}` : ""}
|
|
1920
|
+
</Chip>
|
|
1921
|
+
))}
|
|
1922
|
+
</ChipRow>
|
|
1923
|
+
)}
|
|
1924
|
+
{(user_authored_skills.length > 0 || stock_skills.length > 0) && (
|
|
1925
|
+
<ChipRow label="Skills loaded">
|
|
1926
|
+
{user_authored_skills.map(s => (<Chip key={s} tone="user">{s}</Chip>))}
|
|
1927
|
+
{stock_skills.slice(0, 6).map(s => (<Chip key={s} tone="stock">{s}</Chip>))}
|
|
1928
|
+
</ChipRow>
|
|
1929
|
+
)}
|
|
1930
|
+
{top_tools.length > 0 && (
|
|
1931
|
+
<ChipRow label="Top tools">
|
|
1932
|
+
{top_tools.slice(0, 4).map((t, i) => (<Chip key={i}>{t}</Chip>))}
|
|
1933
|
+
</ChipRow>
|
|
1934
|
+
)}
|
|
1935
|
+
</div>
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function ChipRow({ label, children }: { label: string; children: ReactNode }) {
|
|
1940
|
+
return (
|
|
1941
|
+
<div style={{
|
|
1942
|
+
display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap",
|
|
1943
|
+
padding: "3px 0", fontSize: 11,
|
|
1944
|
+
}}>
|
|
1945
|
+
<span style={{
|
|
1946
|
+
minWidth: 96, color: "var(--af-text-tertiary)", fontWeight: 500,
|
|
1947
|
+
fontSize: 10, textTransform: "uppercase", letterSpacing: "0.04em",
|
|
1948
|
+
}}>
|
|
1949
|
+
{label}
|
|
1950
|
+
</span>
|
|
1951
|
+
<div style={{ display: "flex", gap: 5, flexWrap: "wrap", flex: 1 }}>{children}</div>
|
|
1952
|
+
</div>
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
function Chip({ children, tone, title }: { children: ReactNode; tone?: "user" | "stock"; title?: string }) {
|
|
1957
|
+
const bg = tone === "user" ? "var(--af-accent-subtle)" : "var(--af-border-subtle)";
|
|
1958
|
+
const fg = tone === "user" ? "var(--af-accent)" : "var(--af-text-secondary)";
|
|
1959
|
+
return (
|
|
1960
|
+
<span title={title} style={{
|
|
1961
|
+
display: "inline-flex", alignItems: "center", padding: "2px 8px",
|
|
1962
|
+
borderRadius: 999, fontSize: 10, fontFamily: "var(--font-mono)",
|
|
1963
|
+
background: bg, color: fg, whiteSpace: "nowrap",
|
|
1964
|
+
maxWidth: 280, overflow: "hidden", textOverflow: "ellipsis",
|
|
1965
|
+
}}>
|
|
1966
|
+
{children}
|
|
1967
|
+
</span>
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function SessionTimelineMinimap({ session }: { session: WeekTopSession }) {
|
|
1972
|
+
const intervals = session.timeline.active_intervals;
|
|
1973
|
+
// Compute the actual span the bars cover so we never clip an interval off
|
|
1974
|
+
// the right edge. Older cached digests stored duration_min as active_min
|
|
1975
|
+
// while intervals are positioned in wall-clock minutes — taking max() of
|
|
1976
|
+
// both makes the renderer correct regardless of which convention the
|
|
1977
|
+
// cached digest used.
|
|
1978
|
+
const observedMax = intervals.length > 0
|
|
1979
|
+
? Math.max(...intervals.map(iv => iv.end_min))
|
|
1980
|
+
: 0;
|
|
1981
|
+
const duration = Math.max(1, session.timeline.duration_min, observedMax);
|
|
1982
|
+
return (
|
|
1983
|
+
<div style={{ marginTop: 10, marginBottom: 4 }}>
|
|
1984
|
+
<div style={{
|
|
1985
|
+
position: "relative", width: "100%", height: 28,
|
|
1986
|
+
background: "var(--af-border-subtle)", borderRadius: 4,
|
|
1987
|
+
}}>
|
|
1988
|
+
{/* Active intervals */}
|
|
1989
|
+
{intervals.map((iv, i) => {
|
|
1990
|
+
const left = (iv.start_min / duration) * 100;
|
|
1991
|
+
const width = Math.max(0.6, ((iv.end_min - iv.start_min) / duration) * 100);
|
|
1992
|
+
return (
|
|
1993
|
+
<div key={i} style={{
|
|
1994
|
+
position: "absolute", top: 6, bottom: 6,
|
|
1995
|
+
left: `${left}%`, width: `${width}%`,
|
|
1996
|
+
background: "color-mix(in srgb, var(--af-accent) 30%, transparent)",
|
|
1997
|
+
borderRadius: 2,
|
|
1998
|
+
}} />
|
|
1999
|
+
);
|
|
2000
|
+
})}
|
|
2001
|
+
{/* Pins */}
|
|
2002
|
+
{session.pins.map((pin, i) => {
|
|
2003
|
+
const left = Math.max(0, Math.min(100, (pin.start_min / duration) * 100));
|
|
2004
|
+
const right = pin.end_min !== undefined
|
|
2005
|
+
? Math.max(left + 0.5, Math.min(100, (pin.end_min / duration) * 100))
|
|
2006
|
+
: left;
|
|
2007
|
+
const isSpan = pin.end_min !== undefined && right > left + 0.5;
|
|
2008
|
+
const color = PIN_COLORS[pin.kind] ?? "var(--af-accent)";
|
|
2009
|
+
return (
|
|
2010
|
+
<div key={i}>
|
|
2011
|
+
{isSpan && (
|
|
2012
|
+
<div style={{
|
|
2013
|
+
position: "absolute", top: 4, bottom: 4,
|
|
2014
|
+
left: `${left}%`, width: `${right - left}%`,
|
|
2015
|
+
background: `color-mix(in srgb, ${color} 35%, transparent)`,
|
|
2016
|
+
border: `1px solid ${color}`,
|
|
2017
|
+
borderRadius: 3,
|
|
2018
|
+
}} title={`${PIN_LABELS[pin.kind]}: ${pin.label}`} />
|
|
2019
|
+
)}
|
|
2020
|
+
<div style={{
|
|
2021
|
+
position: "absolute", top: -3, bottom: -3,
|
|
2022
|
+
left: `${left}%`, width: 3,
|
|
2023
|
+
background: color,
|
|
2024
|
+
borderRadius: 1,
|
|
2025
|
+
transform: "translateX(-1.5px)",
|
|
2026
|
+
}} title={`${PIN_LABELS[pin.kind]}: ${pin.label}`} />
|
|
2027
|
+
</div>
|
|
2028
|
+
);
|
|
2029
|
+
})}
|
|
2030
|
+
</div>
|
|
2031
|
+
{/* Min markers — wall-clock from session start; teal bars are active
|
|
2032
|
+
intervals, gaps are idle periods (>3min between events). */}
|
|
2033
|
+
<div style={{
|
|
2034
|
+
display: "flex", justifyContent: "space-between",
|
|
2035
|
+
fontSize: 9, fontFamily: "var(--font-mono)",
|
|
2036
|
+
color: "var(--af-text-tertiary)",
|
|
2037
|
+
marginTop: 3,
|
|
2038
|
+
}}>
|
|
2039
|
+
<span>0m</span>
|
|
2040
|
+
<span>{formatMin(duration / 2)}</span>
|
|
2041
|
+
<span>{formatMin(duration)} wall</span>
|
|
2042
|
+
</div>
|
|
2043
|
+
</div>
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function PinRow({ pin }: { pin: SessionPin }) {
|
|
2048
|
+
const color = PIN_COLORS[pin.kind] ?? "var(--af-accent)";
|
|
2049
|
+
const isSpan = pin.end_min !== undefined && pin.end_min > pin.start_min + 0.5;
|
|
2050
|
+
const t = isSpan
|
|
2051
|
+
? `${formatMin(pin.start_min)}–${formatMin(pin.end_min!)}`
|
|
2052
|
+
: formatMin(pin.start_min);
|
|
2053
|
+
return (
|
|
2054
|
+
<li style={{
|
|
2055
|
+
display: "flex", gap: 10, alignItems: "flex-start",
|
|
2056
|
+
fontSize: 12, lineHeight: 1.5,
|
|
2057
|
+
}}>
|
|
2058
|
+
<span style={{
|
|
2059
|
+
flexShrink: 0, width: 3, height: 3, marginTop: 7,
|
|
2060
|
+
borderRadius: 999, background: color,
|
|
2061
|
+
}} />
|
|
2062
|
+
<span style={{
|
|
2063
|
+
flexShrink: 0, fontFamily: "var(--font-mono)", fontSize: 10,
|
|
2064
|
+
color: "var(--af-text-tertiary)", paddingTop: 1, minWidth: 60,
|
|
2065
|
+
}}>
|
|
2066
|
+
{t}
|
|
2067
|
+
</span>
|
|
2068
|
+
<span style={{
|
|
2069
|
+
flexShrink: 0, padding: "1px 7px", borderRadius: 999, fontSize: 9,
|
|
2070
|
+
fontWeight: 700, letterSpacing: "0.04em", textTransform: "uppercase",
|
|
2071
|
+
background: `color-mix(in srgb, ${color} 15%, transparent)`,
|
|
2072
|
+
color: color, marginTop: 1,
|
|
2073
|
+
}}>
|
|
2074
|
+
{PIN_LABELS[pin.kind] ?? pin.kind}
|
|
2075
|
+
</span>
|
|
2076
|
+
<span style={{ color: "var(--af-text)", maxWidth: 640 }}>
|
|
2077
|
+
{pin.label}
|
|
2078
|
+
</span>
|
|
2079
|
+
</li>
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function formatMin(m: number): string {
|
|
2084
|
+
if (m >= 60) {
|
|
2085
|
+
// Round to whole minutes first, THEN split into hours+minutes — otherwise
|
|
2086
|
+
// a value like 179.6 rounds to "2h 60m" because Math.round(179.6 % 60)
|
|
2087
|
+
// wraps to 60 without bumping the hour.
|
|
2088
|
+
const total = Math.round(m);
|
|
2089
|
+
const h = Math.floor(total / 60);
|
|
2090
|
+
const min = total % 60;
|
|
2091
|
+
return `${h}h${min > 0 ? ` ${min}m` : ""}`;
|
|
2092
|
+
}
|
|
2093
|
+
return `${Math.round(m * 10) / 10}m`;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// ─── Pattern rollups fold-down (replaces inline working_shapes + grammar) ─
|
|
2097
|
+
|
|
2098
|
+
function PatternRollupsFold({
|
|
2099
|
+
shapes, grammar,
|
|
2100
|
+
}: {
|
|
2101
|
+
shapes: WeekDigestType["working_shapes"];
|
|
2102
|
+
grammar: WeekDigestType["interaction_grammar"];
|
|
2103
|
+
}) {
|
|
2104
|
+
const [open, setOpen] = useState(false);
|
|
2105
|
+
return (
|
|
2106
|
+
<section style={{ marginBottom: 28 }}>
|
|
2107
|
+
<button
|
|
2108
|
+
type="button"
|
|
2109
|
+
onClick={() => setOpen(o => !o)}
|
|
2110
|
+
style={{
|
|
2111
|
+
display: "inline-flex", alignItems: "center", gap: 6,
|
|
2112
|
+
background: "transparent", border: "none", cursor: "pointer",
|
|
2113
|
+
padding: "6px 0", color: "var(--af-text-tertiary)",
|
|
2114
|
+
fontSize: 11, fontWeight: 700, letterSpacing: "0.08em",
|
|
2115
|
+
textTransform: "uppercase",
|
|
2116
|
+
}}
|
|
2117
|
+
>
|
|
2118
|
+
{open ? "▾" : "▸"} Weekly pattern rollups
|
|
2119
|
+
</button>
|
|
2120
|
+
{open && (
|
|
2121
|
+
<div style={{ marginTop: 8 }}>
|
|
2122
|
+
{shapes && shapes.length > 0 && <WorkingShapesSection shapes={shapes} />}
|
|
2123
|
+
{grammar && <InteractionGrammarSection grammar={grammar} />}
|
|
2124
|
+
</div>
|
|
2125
|
+
)}
|
|
2126
|
+
</section>
|
|
2127
|
+
);
|
|
2128
|
+
}
|