six-degrees 0.4.3 → 0.4.5
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +3 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +41 -13
- package/.next/standalone/.next/routes-manifest.json +12 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +18 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +18 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/route.js +8 -0
- package/.next/standalone/.next/server/app/api/social/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +22 -21
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +20 -19
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +20 -19
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch/page.js +1 -1
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +22 -21
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/page.js +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +22 -21
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/profile/page.js +1 -1
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +22 -21
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/queue/page.js +1 -1
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +22 -21
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/scores/page.js +1 -1
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +22 -21
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/settings/page.js +1 -1
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +22 -21
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/setup/page.js +1 -1
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +22 -21
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/social/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/social/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/social/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/social/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/social/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/social/page.js +13 -0
- package/.next/standalone/.next/server/app/social/page.js.map +5 -0
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/social.html +1 -0
- package/.next/standalone/.next/server/app/social.meta +26 -0
- package/.next/standalone/.next/server/app/social.rsc +26 -0
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +26 -0
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +30 -0
- package/.next/standalone/.next/server/app-paths-manifest.json +3 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +8 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0uag-k5._.js → [root-of-the-server]__1dmc7-t._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_route_actions_1_3c6qn.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16hs8kw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1feicqq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jeztd-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0hw_qnj._.js → _0wxbq78._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1cyxocj._.js → _1ap1tu_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1mj739k._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_social_page_actions_176i1kl.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +17 -4
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/prefetch-hints.json +14 -0
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/04a3zockahetx.js +1 -0
- package/.next/standalone/.next/static/chunks/{2wwmljnf7yn_5.js → 0a5iadnmt9u9k.js} +1 -1
- package/.next/standalone/.next/static/chunks/0g_p4p7oci18u.js +1 -0
- package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +1 -0
- package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +1 -0
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +1 -0
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +1 -0
- package/.next/standalone/.next/static/chunks/{1zwjy1mmpkdrg.js → 24m745q_85u34.js} +2 -2
- package/.next/standalone/.next/static/chunks/2_zu7rmb8flzl.js +2 -0
- package/.next/standalone/.next/static/chunks/38wwtni5e1ec1.js +31 -0
- package/.next/standalone/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
- package/.next/{static/chunks/0_3ubivay59bq.js → standalone/.next/static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/.next/standalone/CHANGELOG.md +58 -0
- package/.next/standalone/app/api/network/route.js +11 -0
- package/.next/standalone/app/api/scraper/route.js +2 -0
- package/.next/standalone/app/api/social/route.js +96 -0
- package/.next/standalone/app/components/AutoScanButton.js +41 -0
- package/.next/standalone/app/components/ChainView.js +7 -0
- package/.next/standalone/app/components/FilterPanel.js +6 -0
- package/.next/standalone/app/components/ForceGraph.js +257 -39
- package/.next/standalone/app/components/GalaxyLab.js +275 -0
- package/.next/standalone/app/components/ScanStatusBar.js +22 -8
- package/.next/standalone/app/components/SeparationView.js +9 -1
- package/.next/standalone/app/components/Sidebar.js +88 -26
- package/.next/standalone/app/components/SocialAutoSync.js +34 -0
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +85 -0
- package/.next/standalone/app/layout.js +3 -0
- package/.next/standalone/app/page.js +29 -0
- package/.next/standalone/app/scores/page.js +3 -0
- package/.next/standalone/app/setup/page.js +1 -1
- package/.next/standalone/app/social/page.js +281 -0
- package/.next/standalone/lib/experimental-client.js +38 -0
- package/.next/standalone/lib/galaxy-export.js +152 -0
- package/.next/standalone/lib/galaxy-lab.js +296 -0
- package/.next/standalone/lib/insights.js +54 -0
- package/.next/standalone/lib/linkedin-export.js +156 -0
- package/.next/standalone/lib/network-health.js +42 -0
- package/.next/standalone/lib/network.js +1 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +118 -2
- package/.next/static/chunks/04a3zockahetx.js +1 -0
- package/.next/static/chunks/{2wwmljnf7yn_5.js → 0a5iadnmt9u9k.js} +1 -1
- package/.next/static/chunks/0g_p4p7oci18u.js +1 -0
- package/.next/static/chunks/0y1yms5kez2f3.js +1 -0
- package/.next/static/chunks/14m1zu-8warte.js +1 -0
- package/.next/static/chunks/1e1n0kkz-psdb.js +1 -0
- package/.next/static/chunks/236f0jtp68j_h.js +1 -0
- package/.next/static/chunks/{1zwjy1mmpkdrg.js → 24m745q_85u34.js} +2 -2
- package/.next/static/chunks/2_zu7rmb8flzl.js +2 -0
- package/.next/static/chunks/38wwtni5e1ec1.js +31 -0
- package/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
- package/.next/{standalone/.next/static/chunks/0_3ubivay59bq.js → static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/CHANGELOG.md +58 -0
- package/lib/experimental-client.js +38 -0
- package/lib/galaxy-export.js +152 -0
- package/lib/galaxy-lab.js +296 -0
- package/lib/insights.js +54 -0
- package/lib/linkedin-export.js +156 -0
- package/lib/network-health.js +42 -0
- package/lib/network.js +1 -0
- package/package.json +1 -1
- package/scripts/scrape.py +118 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d3vjsq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m4vk0b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_012mop3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1163x49._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1av11ws._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1ig7em7._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +0 -3
- package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
- package/.next/standalone/.next/static/chunks/0f4ugd6v3ph0-.js +0 -1
- package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +0 -1
- package/.next/standalone/.next/static/chunks/10ogurbcuf3nq.js +0 -1
- package/.next/standalone/.next/static/chunks/1489t7jrjojk_.js +0 -1
- package/.next/standalone/.next/static/chunks/1zunq4tk-u3ng.js +0 -1
- package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +0 -2
- package/.next/standalone/.next/static/chunks/2pyo4j84prgp6.js +0 -18
- package/.next/standalone/.next/static/chunks/3kllnkb3hof1s.js +0 -1
- package/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
- package/.next/static/chunks/0f4ugd6v3ph0-.js +0 -1
- package/.next/static/chunks/0l1568qff0efj.js +0 -1
- package/.next/static/chunks/10ogurbcuf3nq.js +0 -1
- package/.next/static/chunks/1489t7jrjojk_.js +0 -1
- package/.next/static/chunks/1zunq4tk-u3ng.js +0 -1
- package/.next/static/chunks/2p5yt1o9tnxjn.js +0 -2
- package/.next/static/chunks/2pyo4j84prgp6.js +0 -18
- package/.next/static/chunks/3kllnkb3hof1s.js +0 -1
- /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_ssgManifest.js +0 -0
- /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_buildManifest.js +0 -0
- /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// The Galaxy's physics lab (experimental): Obsidian-style sliders for the forces
|
|
2
|
+
// that lay the Galaxy out, and a replay of your network growing by the date you
|
|
3
|
+
// connected. The Filter panel writes here (GalaxyLab.js); the Galaxy reads it
|
|
4
|
+
// (ForceGraph.js) and changes the running layout in place, never rebuilding.
|
|
5
|
+
//
|
|
6
|
+
// Two stores: the sliders, remembered in this browser, and the replay's clock,
|
|
7
|
+
// which isn't. Both follow useSyncExternalStore's rules: a snapshot only changes
|
|
8
|
+
// when something in it does.
|
|
9
|
+
|
|
10
|
+
import { keyFor } from './separation.js';
|
|
11
|
+
import { warmthOf } from './linkedin-export.js';
|
|
12
|
+
|
|
13
|
+
const KEY = 'six-degrees-galaxy-lab';
|
|
14
|
+
const LAYOUTS_KEY = 'six-degrees-galaxy-layouts';
|
|
15
|
+
|
|
16
|
+
/** The layout as it has always been. Every slider is a multiple of today's value, or 0 for off. */
|
|
17
|
+
export const LAB_DEFAULTS = Object.freeze({
|
|
18
|
+
on: false, // the lab is on: without it, the Galaxy ignores everything below
|
|
19
|
+
gravity: 0, // a pull on every dot towards you, in the middle
|
|
20
|
+
rings: 1, // how hard each tier (or degree) holds its ring; 0 lets it find its own shape
|
|
21
|
+
push: 15, // how hard dots push each other apart
|
|
22
|
+
pull: 1, // how hard each person pulls the people who came through them
|
|
23
|
+
distance: 1, // how far out those people sit
|
|
24
|
+
dotSize: 1,
|
|
25
|
+
lines: 1,
|
|
26
|
+
sizeBy: 'power', // 'power' (power score) or 'reach' (how many hang off them)
|
|
27
|
+
names: 'key', // 'key' (you, S tier, catalysts) or 'all' (all your connections)
|
|
28
|
+
branch: true, // hovering a dot lights up its branch and dims the rest
|
|
29
|
+
colourBy: 'tier', // 'tier', 'degree', 'company' or 'warmth' (from the Social tab)
|
|
30
|
+
speed: 15, // a replay's length, in seconds
|
|
31
|
+
loop: false, // start the replay again when it ends
|
|
32
|
+
labels: true, // names on the Galaxy at all: the Filter panel's switch, honoured with the lab off too
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** The slider settings a saved layout keeps (not whether the lab is on, nor the names switch). */
|
|
36
|
+
const LAYOUT_KEYS = ['gravity', 'rings', 'push', 'pull', 'distance', 'dotSize', 'lines', 'sizeBy', 'names', 'branch', 'colourBy', 'speed', 'loop'];
|
|
37
|
+
|
|
38
|
+
/** Let the layout find its own shape: no rings, each connection's circle bunched round them. */
|
|
39
|
+
export const CLUSTERS = Object.freeze({ gravity: 0.25, rings: 0, push: 25, pull: 2.5, distance: 0.35, sizeBy: 'reach' });
|
|
40
|
+
|
|
41
|
+
const listeners = new Set();
|
|
42
|
+
let cache = null;
|
|
43
|
+
|
|
44
|
+
function read() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = JSON.parse(localStorage.getItem(KEY) || 'null');
|
|
47
|
+
return raw && typeof raw === 'object' ? { ...LAB_DEFAULTS, ...raw } : LAB_DEFAULTS;
|
|
48
|
+
} catch { return LAB_DEFAULTS; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function labNow() {
|
|
52
|
+
if (!cache) cache = read();
|
|
53
|
+
return cache;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function setLab(patch) {
|
|
57
|
+
cache = patch === null ? { ...LAB_DEFAULTS, on: labNow().on, labels: labNow().labels } : { ...labNow(), ...patch };
|
|
58
|
+
try { localStorage.setItem(KEY, JSON.stringify(cache)); } catch {}
|
|
59
|
+
listeners.forEach((f) => f());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function watchLab(f) {
|
|
63
|
+
listeners.add(f);
|
|
64
|
+
const other = (e) => { if (e.key === KEY) { cache = null; f(); } };
|
|
65
|
+
window.addEventListener('storage', other);
|
|
66
|
+
return () => { listeners.delete(f); window.removeEventListener('storage', other); };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** What the Galaxy should use: the sliders while the lab is on, today's layout otherwise (with the names switch). */
|
|
70
|
+
export const effectiveLab = (lab) => (lab.on ? lab : lab.labels === LAB_DEFAULTS.labels ? LAB_DEFAULTS : { ...LAB_DEFAULTS, labels: lab.labels });
|
|
71
|
+
|
|
72
|
+
// Saved layouts: your own named slider settings, in this browser.
|
|
73
|
+
let layoutsCache = null;
|
|
74
|
+
const layoutListeners = new Set();
|
|
75
|
+
export function layoutsNow() {
|
|
76
|
+
if (!layoutsCache) {
|
|
77
|
+
try { layoutsCache = JSON.parse(localStorage.getItem(LAYOUTS_KEY) || '[]'); } catch { layoutsCache = []; }
|
|
78
|
+
if (!Array.isArray(layoutsCache)) layoutsCache = [];
|
|
79
|
+
}
|
|
80
|
+
return layoutsCache;
|
|
81
|
+
}
|
|
82
|
+
function writeLayouts(list) {
|
|
83
|
+
layoutsCache = list;
|
|
84
|
+
try { localStorage.setItem(LAYOUTS_KEY, JSON.stringify(list)); } catch {}
|
|
85
|
+
layoutListeners.forEach((f) => f());
|
|
86
|
+
}
|
|
87
|
+
export function watchLayouts(f) { layoutListeners.add(f); return () => layoutListeners.delete(f); }
|
|
88
|
+
export function saveLayout(name) {
|
|
89
|
+
const label = String(name || '').trim().slice(0, 40);
|
|
90
|
+
if (!label) return;
|
|
91
|
+
const lab = labNow();
|
|
92
|
+
const settings = Object.fromEntries(LAYOUT_KEYS.map((k) => [k, lab[k]]));
|
|
93
|
+
writeLayouts([...layoutsNow().filter((l) => l.name !== label), { name: label, settings }]);
|
|
94
|
+
}
|
|
95
|
+
export function applyLayout(name) {
|
|
96
|
+
const found = layoutsNow().find((l) => l.name === name);
|
|
97
|
+
if (found) setLab(Object.fromEntries(LAYOUT_KEYS.filter((k) => k in found.settings).map((k) => [k, found.settings[k]])));
|
|
98
|
+
}
|
|
99
|
+
export function forgetLayout(name) { writeLayouts(layoutsNow().filter((l) => l.name !== name)); }
|
|
100
|
+
|
|
101
|
+
// The replay's clock. `at` is a time in ms (null: everything shows); `min` and
|
|
102
|
+
// `max` are the first and last dates in the network on screen, set by the Galaxy
|
|
103
|
+
// (both null when it has no dates, as in the sample network).
|
|
104
|
+
// The same store holds the lab's other passing state: what Find is looking
|
|
105
|
+
// for (and a count that asks the Galaxy to fly to the first match), and the
|
|
106
|
+
// Social tab's data once it's loaded.
|
|
107
|
+
const clockListeners = new Set();
|
|
108
|
+
let clock = { at: null, playing: false, min: null, max: null, of: 0, find: '', fly: 0, found: 0, social: undefined };
|
|
109
|
+
let raf = null;
|
|
110
|
+
|
|
111
|
+
export const clockNow = () => clock;
|
|
112
|
+
|
|
113
|
+
export function watchClock(f) {
|
|
114
|
+
clockListeners.add(f);
|
|
115
|
+
return () => clockListeners.delete(f);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function setClock(patch) {
|
|
119
|
+
const next = { ...clock, ...patch };
|
|
120
|
+
if (Object.keys(patch).every((k) => Object.is(next[k], clock[k]))) return;
|
|
121
|
+
clock = next;
|
|
122
|
+
clockListeners.forEach((f) => f());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Play from where the slider is (or the start) to the last date, over the lab's replay length. */
|
|
126
|
+
export function play() {
|
|
127
|
+
if (clock.min == null || clock.max == null || clock.max <= clock.min) return;
|
|
128
|
+
const span = clock.max - clock.min;
|
|
129
|
+
const REPLAY_MS = Math.max(1, Number(labNow().speed) || 15) * 1000;
|
|
130
|
+
const from = clock.at == null || clock.at >= clock.max ? clock.min : clock.at;
|
|
131
|
+
const started = performance.now() - ((from - clock.min) / span) * REPLAY_MS;
|
|
132
|
+
setClock({ at: from, playing: true });
|
|
133
|
+
const step = (now) => {
|
|
134
|
+
const at = Math.min(clock.max, clock.min + ((now - started) / REPLAY_MS) * span);
|
|
135
|
+
setClock({ at });
|
|
136
|
+
if (at >= clock.max) {
|
|
137
|
+
raf = null;
|
|
138
|
+
setClock({ playing: false });
|
|
139
|
+
if (labNow().loop) setTimeout(() => { if (!raf && clock.at === clock.max) { setClock({ at: clock.min }); play(); } }, 1200);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
raf = requestAnimationFrame(step);
|
|
143
|
+
};
|
|
144
|
+
cancelAnimationFrame(raf);
|
|
145
|
+
raf = requestAnimationFrame(step);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function pause() {
|
|
149
|
+
cancelAnimationFrame(raf);
|
|
150
|
+
raf = null;
|
|
151
|
+
setClock({ playing: false });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Back to everything showing. */
|
|
155
|
+
export function stopReplay() {
|
|
156
|
+
pause();
|
|
157
|
+
setClock({ at: null });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* When each dot appears in the replay: your connections on the day you
|
|
162
|
+
* connected, and everyone in a circle with the connection whose circle it is
|
|
163
|
+
* (they came with them). Undated connections are there from the start.
|
|
164
|
+
* `parentOf` maps an id to the id it hangs off, as the Galaxy draws it.
|
|
165
|
+
*/
|
|
166
|
+
export function bornTimes(nodes, parentOf) {
|
|
167
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
168
|
+
const memo = new Map();
|
|
169
|
+
const bornOf = (n, depth = 0) => {
|
|
170
|
+
if (memo.has(n.id)) return memo.get(n.id);
|
|
171
|
+
let t = -Infinity;
|
|
172
|
+
const p = parentOf.get(n.id);
|
|
173
|
+
if (n.degree === 1 || p == null || depth > 6) {
|
|
174
|
+
const own = n.connected_date ? Date.parse(n.connected_date) : NaN;
|
|
175
|
+
t = Number.isNaN(own) ? -Infinity : own;
|
|
176
|
+
}
|
|
177
|
+
if (p != null && byId.has(p) && depth <= 6) t = Math.max(t, bornOf(byId.get(p), depth + 1));
|
|
178
|
+
memo.set(n.id, t);
|
|
179
|
+
return t;
|
|
180
|
+
};
|
|
181
|
+
return nodes.map((n) => bornOf(n));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** How many hang off each dot, all the way down (not counting itself). */
|
|
185
|
+
export function reachCounts(nodes, parentOf) {
|
|
186
|
+
const out = new Map(nodes.map((n) => [n.id, 0]));
|
|
187
|
+
for (const n of nodes) {
|
|
188
|
+
let p = parentOf.get(n.id);
|
|
189
|
+
for (let hops = 0; p != null && hops < 7; hops++) {
|
|
190
|
+
out.set(p, (out.get(p) || 0) + 1);
|
|
191
|
+
p = parentOf.get(p);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The Social tab's data, loaded once: messages merged per profile, your jobs and your posts. */
|
|
198
|
+
let socialLoad = null;
|
|
199
|
+
export function loadSocial() {
|
|
200
|
+
if (!socialLoad) {
|
|
201
|
+
socialLoad = fetch('/api/social').then((r) => (r.ok ? r.json() : {})).then((d) => {
|
|
202
|
+
setClock({ social: d.social ? mergeSocial(d.social) : null });
|
|
203
|
+
}).catch(() => setClock({ social: null }));
|
|
204
|
+
}
|
|
205
|
+
return socialLoad;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The export and the live sync together, per profile key, and the "now" warmth
|
|
210
|
+
* is judged at: the newest thing the data knows, so an older export reads as it was.
|
|
211
|
+
*/
|
|
212
|
+
export function mergeSocial(social) {
|
|
213
|
+
const merged = new Map();
|
|
214
|
+
for (const [url, v] of Object.entries(social?.people || {})) merged.set(keyFor({ profile_url: url }), { ...v });
|
|
215
|
+
for (const [url, v] of Object.entries(social?.live || {})) {
|
|
216
|
+
const k = keyFor({ profile_url: url });
|
|
217
|
+
const cur = merged.get(k) || { total: 0, recent: 0 };
|
|
218
|
+
if (v.last && (!cur.last || v.last > cur.last)) cur.last = v.last;
|
|
219
|
+
cur.unread = v.unread;
|
|
220
|
+
cur.total = Math.max(cur.total || 0, 1);
|
|
221
|
+
merged.set(k, cur);
|
|
222
|
+
}
|
|
223
|
+
let asOf = Math.max(social?.asOf || 0, social?.liveAt ? Date.parse(social.liveAt) || 0 : 0);
|
|
224
|
+
for (const v of merged.values()) if ((v.last || 0) > asOf) asOf = v.last;
|
|
225
|
+
return { merged, asOf, chapters: social?.chapters || [], posts: social?.posts?.posts || [] };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Your job starts and posts, oldest first, for the replay's timeline. */
|
|
229
|
+
export function milestones(social) {
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const c of social?.chapters || []) if (Number.isFinite(c.from)) out.push({ t: c.from, kind: 'job', label: c.company });
|
|
232
|
+
for (const p of social?.posts || []) if (Number.isFinite(p.t)) out.push({ t: p.t, kind: 'post', label: 'You posted' });
|
|
233
|
+
return out.sort((a, b) => a.t - b.t);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Where you worked at a moment: the latest job started by then. */
|
|
237
|
+
export function chapterAt(social, t) {
|
|
238
|
+
let at = null;
|
|
239
|
+
for (const c of social?.chapters || []) if (Number.isFinite(c.from) && c.from <= t && (!at || c.from > at.from)) at = c;
|
|
240
|
+
return at && (!Number.isFinite(at.to) || at.to > t) ? at : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const DEGREE_COLOURS = { 1: '#4FC3F7', 2: '#BA68C8', 3: '#FFB74D' };
|
|
244
|
+
const COMPANY_COLOURS = ['#FF6B6B', '#4ECDC4', '#FFD93D', '#A29BFE', '#00B894', '#FD79A8', '#74B9FF', '#E17055'];
|
|
245
|
+
const WARMTH_COLOURS = { warm: '#FF7043', cool: '#4FC3F7', dormant: '#8D6E63', never: '#546E7A' };
|
|
246
|
+
const OTHER = '#4a5568';
|
|
247
|
+
const companyKey = (c) => String(c || '').trim().toLowerCase();
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* How to colour the Galaxy's dots, like Obsidian's groups: by tier (as it
|
|
251
|
+
* always has), degree, company (the eight most common, the rest grey) or, with
|
|
252
|
+
* the Social tab's data, how warm you are with each of your connections.
|
|
253
|
+
* Returns { of(person) -> colour, legend: [[label, colour]] }.
|
|
254
|
+
*/
|
|
255
|
+
export function colourScheme(mode, people, tierColors, social) {
|
|
256
|
+
if (mode === 'degree') {
|
|
257
|
+
return {
|
|
258
|
+
of: (d) => DEGREE_COLOURS[Math.min(d.degree || 1, 3)],
|
|
259
|
+
legend: [['1st', DEGREE_COLOURS[1]], ['2nd', DEGREE_COLOURS[2]], ['3rd+', DEGREE_COLOURS[3]]],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (mode === 'company') {
|
|
263
|
+
const counts = new Map();
|
|
264
|
+
for (const p of people) {
|
|
265
|
+
const k = companyKey(p.company);
|
|
266
|
+
if (!k) continue;
|
|
267
|
+
const c = counts.get(k) || { name: String(p.company).trim(), n: 0 };
|
|
268
|
+
c.n += 1;
|
|
269
|
+
counts.set(k, c);
|
|
270
|
+
}
|
|
271
|
+
const top = [...counts.entries()].filter(([, c]) => c.n > 1).sort((a, b) => b[1].n - a[1].n).slice(0, COMPANY_COLOURS.length);
|
|
272
|
+
const colour = new Map(top.map(([k], i) => [k, COMPANY_COLOURS[i]]));
|
|
273
|
+
return {
|
|
274
|
+
of: (d) => colour.get(companyKey(d.company)) || OTHER,
|
|
275
|
+
legend: [...top.map(([, c], i) => [c.name, COMPANY_COLOURS[i]]), ['Other', OTHER]],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (mode === 'warmth' && social) {
|
|
279
|
+
return {
|
|
280
|
+
of: (d) => ((d.degree || 1) === 1 ? WARMTH_COLOURS[warmthOf(social.merged.get(keyFor(d)), social.asOf)] : '#2d3748'),
|
|
281
|
+
legend: [['Warm', WARMTH_COLOURS.warm], ['Cool', WARMTH_COLOURS.cool], ['Dormant', WARMTH_COLOURS.dormant], ['Never messaged', WARMTH_COLOURS.never]],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return { of: (d) => tierColors[d.tier] || '#666', legend: Object.entries(tierColors) };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Who Find matches: name, company, role or headline containing the text (2 letters or more). */
|
|
288
|
+
export function findMatches(nodes, query) {
|
|
289
|
+
const q = String(query || '').trim().toLowerCase();
|
|
290
|
+
if (q.length < 2) return null;
|
|
291
|
+
const hit = new Set();
|
|
292
|
+
for (const n of nodes) {
|
|
293
|
+
if ([n.name, n.company, n.role, n.headline].some((v) => v && String(v).toLowerCase().includes(q))) hit.add(n.id);
|
|
294
|
+
}
|
|
295
|
+
return hit;
|
|
296
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Insights on a connection's card (Graph Study §10, items 16 and 19): six plain
|
|
2
|
+
// numbers from what the app already keeps, no new scanning. For one of your
|
|
3
|
+
// connections; null for a 2nd-degree person.
|
|
4
|
+
//
|
|
5
|
+
// circle how many people are in their scanned circle
|
|
6
|
+
// only how many of them none of your other connections reach (lib/brokerage.js)
|
|
7
|
+
// twin whose circle overlaps theirs most, when it's real (15%+, 3+ people)
|
|
8
|
+
// mix their circle by tier
|
|
9
|
+
// bars how much of their circle is scanned, 0–5 (lib/reach.js scanBars)
|
|
10
|
+
// rank where they stand among your connections by who only they reach
|
|
11
|
+
// companies where the most people in their circle work, the top two
|
|
12
|
+
//
|
|
13
|
+
// Plain functions, no React; tests/insights.test.mjs.
|
|
14
|
+
|
|
15
|
+
import { keyFor } from './separation.js';
|
|
16
|
+
import { scanBars } from './reach.js';
|
|
17
|
+
|
|
18
|
+
const TIERS = ['S', 'A', 'B', 'C', 'D'];
|
|
19
|
+
|
|
20
|
+
export function profileInsights(person, { degree2 = [], exclusive, overlap, reach } = {}) {
|
|
21
|
+
if (!person || person.degree === 2) return null;
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const mix = { S: 0, A: 0, B: 0, C: 0, D: 0 };
|
|
24
|
+
const at = new Map();
|
|
25
|
+
for (const row of degree2) {
|
|
26
|
+
if (row?.source_connection_id !== person.id) continue;
|
|
27
|
+
const key = keyFor(row);
|
|
28
|
+
if (seen.has(key)) continue;
|
|
29
|
+
seen.add(key);
|
|
30
|
+
mix[TIERS.includes(row.tier) ? row.tier : 'D'] += 1;
|
|
31
|
+
const company = String(row.company || '').trim();
|
|
32
|
+
if (company) at.set(company, (at.get(company) || 0) + 1);
|
|
33
|
+
}
|
|
34
|
+
const companies = [...at.entries()].filter(([, n]) => n >= 2)
|
|
35
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 2).map(([name, count]) => ({ name, count }));
|
|
36
|
+
const circle = seen.size;
|
|
37
|
+
const ex = exclusive?.get(person.id) || { reach: 0, only: 0, total: 0 };
|
|
38
|
+
const ov = overlap?.get(person.id);
|
|
39
|
+
const twin = ov && ov.share >= 0.15 && ov.shared >= 3 ? { id: ov.with, share: ov.share, shared: ov.shared } : null;
|
|
40
|
+
const ranked = [...(exclusive?.entries() || [])]
|
|
41
|
+
.sort((a, b) => b[1].reach - a[1].reach || String(a[0]).localeCompare(String(b[0])));
|
|
42
|
+
const place = ranked.findIndex(([id]) => id === person.id);
|
|
43
|
+
return {
|
|
44
|
+
circle,
|
|
45
|
+
only: ex.only,
|
|
46
|
+
onlyShare: circle ? ex.only / circle : 0,
|
|
47
|
+
twin,
|
|
48
|
+
mix,
|
|
49
|
+
bars: reach ? scanBars(person, reach) : null,
|
|
50
|
+
rank: place >= 0 ? place + 1 : null,
|
|
51
|
+
of: ranked.length,
|
|
52
|
+
companies,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// The Social tab (experimental): what your own LinkedIn data export says about
|
|
2
|
+
// your relationships. The export is the folder LinkedIn emails you from
|
|
3
|
+
// Settings → Data privacy → Get a copy of your data; nothing here talks to
|
|
4
|
+
// LinkedIn. It runs in the browser on the files you pick, and what it keeps is
|
|
5
|
+
// numbers and dates only:
|
|
6
|
+
//
|
|
7
|
+
// people per profile: when you last messaged, who wrote last, how many
|
|
8
|
+
// messages (1:1 conversations only). Never a message's text.
|
|
9
|
+
// chapters your jobs (Positions.csv) and the connections made in each
|
|
10
|
+
// posts your posts (Shares.csv) and the connections that came the week after
|
|
11
|
+
// invites requests to and from you (Invitations.csv), when the export has them
|
|
12
|
+
//
|
|
13
|
+
// Plain functions, no React; tests/linkedin-export.test.mjs.
|
|
14
|
+
|
|
15
|
+
import { parseCsv } from './csv.js';
|
|
16
|
+
import { keyFor } from './separation.js';
|
|
17
|
+
|
|
18
|
+
const DAY = 86400000;
|
|
19
|
+
|
|
20
|
+
/** The rows of a CSV as objects, from the first line that has `mustHave` among its columns (exports put notes above the header). */
|
|
21
|
+
export function readTable(text, mustHave) {
|
|
22
|
+
const rows = parseCsv(String(text || ''));
|
|
23
|
+
const at = rows.findIndex((r) => r.some((c) => c.trim().toLowerCase() === mustHave.toLowerCase()));
|
|
24
|
+
if (at < 0) return [];
|
|
25
|
+
const head = rows[at].map((c) => c.trim().toLowerCase());
|
|
26
|
+
return rows.slice(at + 1)
|
|
27
|
+
.filter((r) => r.some((c) => c.trim()))
|
|
28
|
+
.map((r) => Object.fromEntries(head.map((h, i) => [h, (r[i] ?? '').trim()])));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A date in any of the export's formats, as ms; null when it can't be read. */
|
|
32
|
+
export function exportDate(s) {
|
|
33
|
+
const v = String(s || '').trim();
|
|
34
|
+
if (!v) return null;
|
|
35
|
+
let m = v.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\s*UTC)?$/);
|
|
36
|
+
if (m) return Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +(m[6] || 0));
|
|
37
|
+
m = v.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4}),?\s+(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
|
|
38
|
+
if (m) {
|
|
39
|
+
const y = +m[3] < 100 ? 2000 + +m[3] : +m[3];
|
|
40
|
+
let h = +m[4] % 12;
|
|
41
|
+
if ((m[6] || '').toUpperCase() === 'PM') h += 12;
|
|
42
|
+
return Date.UTC(y, +m[1] - 1, +m[2], h, +m[5]);
|
|
43
|
+
}
|
|
44
|
+
m = v.match(/^([A-Za-z]{3,9})\s+(\d{4})$/); // "Jan 2020"
|
|
45
|
+
if (m) { const t = Date.parse(`1 ${m[1]} ${m[2]} UTC`); return Number.isNaN(t) ? null : t; }
|
|
46
|
+
const t = Date.parse(/\d{4}$/.test(v) ? `${v} UTC` : v); // "01 May 2024"
|
|
47
|
+
return Number.isNaN(t) ? null : t;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const urlKey = (u) => keyFor({ profile_url: u });
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* From messages.csv, per person (by profile): { last, lastFromThem, total, recent }.
|
|
54
|
+
* `self` is you: the sender in the most conversations. Only 1:1 conversations
|
|
55
|
+
* count; group threads say little about one relationship. No text is read.
|
|
56
|
+
*/
|
|
57
|
+
export function messageStats(rows = []) {
|
|
58
|
+
const convos = new Map();
|
|
59
|
+
const senders = new Map();
|
|
60
|
+
for (const r of rows) {
|
|
61
|
+
const from = urlKey(r['sender profile url']);
|
|
62
|
+
const id = r['conversation id'];
|
|
63
|
+
if (!id || !from) continue;
|
|
64
|
+
const to = String(r['recipient profile urls'] || '').split(/[,;\s]+/).filter(Boolean).map(urlKey);
|
|
65
|
+
const t = exportDate(r.date);
|
|
66
|
+
if (t == null) continue;
|
|
67
|
+
if (!convos.has(id)) convos.set(id, { people: new Set(), msgs: [] });
|
|
68
|
+
const c = convos.get(id);
|
|
69
|
+
c.people.add(from);
|
|
70
|
+
to.forEach((p) => c.people.add(p));
|
|
71
|
+
c.msgs.push({ from, t });
|
|
72
|
+
if (!senders.has(from)) senders.set(from, new Set());
|
|
73
|
+
senders.get(from).add(id);
|
|
74
|
+
}
|
|
75
|
+
let self = null;
|
|
76
|
+
for (const [who, ids] of senders) if (!self || ids.size > senders.get(self).size) self = who;
|
|
77
|
+
let asOf = 0;
|
|
78
|
+
const people = new Map();
|
|
79
|
+
for (const c of convos.values()) {
|
|
80
|
+
const others = [...c.people].filter((p) => p !== self);
|
|
81
|
+
if (others.length !== 1 || !c.people.has(self)) continue;
|
|
82
|
+
const other = others[0];
|
|
83
|
+
const s = people.get(other) || { last: 0, lastFromThem: false, total: 0, times: [] };
|
|
84
|
+
for (const m of c.msgs) {
|
|
85
|
+
s.total += 1;
|
|
86
|
+
s.times.push(m.t);
|
|
87
|
+
if (m.t >= s.last) { s.last = m.t; s.lastFromThem = m.from !== self; }
|
|
88
|
+
if (m.t > asOf) asOf = m.t;
|
|
89
|
+
}
|
|
90
|
+
people.set(other, s);
|
|
91
|
+
}
|
|
92
|
+
for (const s of people.values()) {
|
|
93
|
+
s.recent = s.times.filter((t) => t > asOf - 90 * DAY).length;
|
|
94
|
+
delete s.times;
|
|
95
|
+
}
|
|
96
|
+
return { self, asOf: asOf || null, people };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Warm (in the last 30 days, or 3+ in 90), cool (within a year), dormant (longer ago), or never. */
|
|
100
|
+
export function warmthOf(stat, asOf) {
|
|
101
|
+
if (!stat || !stat.total) return 'never';
|
|
102
|
+
const age = asOf - stat.last;
|
|
103
|
+
if (age <= 30 * DAY || stat.recent >= 3) return 'warm';
|
|
104
|
+
return age <= 365 * DAY ? 'cool' : 'dormant';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** People who wrote last, 3 to 30 days before the export: replies you might owe. */
|
|
108
|
+
export function repliesWaiting(stats) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const [url, s] of stats.people) {
|
|
111
|
+
const age = stats.asOf - s.last;
|
|
112
|
+
if (s.lastFromThem && age >= 3 * DAY && age <= 30 * DAY) out.push({ url, days: Math.round(age / DAY), total: s.total });
|
|
113
|
+
}
|
|
114
|
+
return out.sort((a, b) => a.days - b.days);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Your jobs, oldest first, each with the connections made while you were there. */
|
|
118
|
+
export function careerChapters(positions = [], connections = [], asOf = Date.now()) {
|
|
119
|
+
const jobs = positions
|
|
120
|
+
.map((p) => ({ company: p['company name'] || '', title: p.title || '', from: exportDate(p['started on']), to: exportDate(p['finished on']) }))
|
|
121
|
+
.filter((j) => j.company && j.from != null)
|
|
122
|
+
.sort((a, b) => a.from - b.from);
|
|
123
|
+
const made = connections.map((c) => ({ url: urlKey(c.url || c.profile_url), t: exportDate(c['connected on'] || c.connected_on) })).filter((c) => c.t != null);
|
|
124
|
+
return jobs.map((j) => {
|
|
125
|
+
const end = j.to ?? asOf;
|
|
126
|
+
const inside = made.filter((c) => c.t >= j.from && c.t < end);
|
|
127
|
+
return { ...j, to: j.to, made: inside.length, people: inside.map((c) => c.url) };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Your posts and the connections made in the 7 days after each, against your
|
|
133
|
+
* usual week. Something that happened after a post, not proof it came from it.
|
|
134
|
+
*/
|
|
135
|
+
export function postingEffect(shares = [], connections = []) {
|
|
136
|
+
const posts = shares.map((s) => ({ t: exportDate(s.date), link: s.sharelink || s['share link'] || '' })).filter((p) => p.t != null).sort((a, b) => a.t - b.t);
|
|
137
|
+
const made = connections.map((c) => exportDate(c['connected on'] || c.connected_on)).filter((t) => t != null).sort((a, b) => a - b);
|
|
138
|
+
if (!made.length) return { posts: [], usualWeek: 0, afterPosts: 0 };
|
|
139
|
+
const span = Math.max(DAY * 7, made[made.length - 1] - made[0]);
|
|
140
|
+
const usualWeek = (made.length / span) * 7 * DAY;
|
|
141
|
+
const counted = posts.map((p) => ({ ...p, week: made.filter((t) => t >= p.t && t < p.t + 7 * DAY).length }));
|
|
142
|
+
const afterPosts = counted.length ? counted.reduce((n, p) => n + p.week, 0) / counted.length : 0;
|
|
143
|
+
return { posts: counted, usualWeek, afterPosts };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Requests to and from you, when the export has them. */
|
|
147
|
+
export function invitationSplit(rows = []) {
|
|
148
|
+
let incoming = 0;
|
|
149
|
+
let outgoing = 0;
|
|
150
|
+
for (const r of rows) {
|
|
151
|
+
const d = String(r.direction || '').toUpperCase();
|
|
152
|
+
if (d === 'INCOMING') incoming += 1;
|
|
153
|
+
else if (d === 'OUTGOING') outgoing += 1;
|
|
154
|
+
}
|
|
155
|
+
return { incoming, outgoing, total: incoming + outgoing };
|
|
156
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Network health for the Scores tab (item 41, Graph Study §8.5/§10.2), from
|
|
2
|
+
// what the app already keeps. No percentiles against other people: there is no
|
|
3
|
+
// data to compare with, so every number is about your own network.
|
|
4
|
+
//
|
|
5
|
+
// circles how many of your connections have a scanned circle
|
|
6
|
+
// early fewer than 5: say the numbers are an early estimate
|
|
7
|
+
// twoWays of everyone you reach through a circle, the share reached two or
|
|
8
|
+
// more ways (lib/brokerage.js redundancy)
|
|
9
|
+
// effective Burt's effective size of your own network, N − 2t/N, with t the ties
|
|
10
|
+
// between your connections that scans have kept (lib/ties.js), and
|
|
11
|
+
// efficiency ES/N. Honest only as far as those ties go.
|
|
12
|
+
// top your five connections by who only they reach
|
|
13
|
+
//
|
|
14
|
+
// Plain functions, no React; tests/network-health.test.mjs.
|
|
15
|
+
|
|
16
|
+
import { exclusiveReach, redundancy } from './brokerage.js';
|
|
17
|
+
|
|
18
|
+
export function networkHealth({ degree1 = [], degree2 = [], ties = 0 } = {}) {
|
|
19
|
+
const circleOf = new Map();
|
|
20
|
+
for (const row of degree2) {
|
|
21
|
+
if (row?.source_connection_id != null) circleOf.set(row.source_connection_id, (circleOf.get(row.source_connection_id) || 0) + 1);
|
|
22
|
+
}
|
|
23
|
+
const only = exclusiveReach(degree2, degree1);
|
|
24
|
+
const n = degree1.length;
|
|
25
|
+
const t = Math.max(0, Number(ties) || 0);
|
|
26
|
+
const es = n ? n - (2 * t) / n : 0;
|
|
27
|
+
const byId = new Map(degree1.map((c) => [c.id, c]));
|
|
28
|
+
const top = [...only.entries()]
|
|
29
|
+
.sort((a, b) => b[1].reach - a[1].reach || String(a[0]).localeCompare(String(b[0])))
|
|
30
|
+
.slice(0, 5)
|
|
31
|
+
.map(([id, r]) => {
|
|
32
|
+
const c = byId.get(id) || {};
|
|
33
|
+
return { id, name: c.name || '', company: c.company || '', tier: c.tier || '', circle: circleOf.get(id) || 0, only: r.only, share: r.total ? r.only / r.total : 0 };
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
circles: circleOf.size,
|
|
37
|
+
early: circleOf.size < 5,
|
|
38
|
+
twoWays: redundancy(degree2, degree1),
|
|
39
|
+
effective: { n, ties: t, size: es, efficiency: n ? es / n : 0 },
|
|
40
|
+
top,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "six-degrees",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "six-degrees",
|
|
9
|
-
"version": "0.4.
|
|
9
|
+
"version": "0.4.5",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"os": [
|
|
12
12
|
"darwin",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "six-degrees",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Blake Burford",
|