six-degrees 0.4.10 → 0.5.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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +9 -9
- package/.next/standalone/.next/routes-manifest.json +6 -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.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 +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +3 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js +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 +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 +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 +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/crm/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
- 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 +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 +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.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 +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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 +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
- 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.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 +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
- package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.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/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- 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/03bziijw5srol.js +1 -0
- package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/.next/standalone/CHANGELOG.md +235 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/ingest/route.js +40 -1
- package/.next/standalone/app/api/scraper/route.js +12 -2
- package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- package/.next/standalone/app/components/AppTabs.js +59 -0
- package/.next/standalone/app/components/ChainView.js +303 -89
- package/.next/standalone/app/components/CirclePanel.js +162 -0
- package/.next/standalone/app/components/CompanyScores.js +2 -2
- package/.next/standalone/app/components/FieldStep.js +4 -4
- package/.next/standalone/app/components/FilterPanel.js +121 -183
- package/.next/standalone/app/components/ForceGraph.js +41 -3
- package/.next/standalone/app/components/GalaxyLab.js +26 -31
- package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
- package/.next/standalone/app/components/MapControls.js +154 -0
- package/.next/standalone/app/components/NoteDetail.js +118 -0
- package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
- package/.next/standalone/app/components/PeopleMap.js +308 -0
- package/.next/standalone/app/components/ScanRadar.js +129 -0
- package/.next/standalone/app/components/ScanStatusBar.js +72 -27
- package/.next/standalone/app/components/SeparationView.js +626 -118
- package/.next/standalone/app/components/Sidebar.js +28 -2
- package/.next/standalone/app/components/settings/DataSection.js +12 -3
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
- package/.next/standalone/app/components/settings/SectorSection.js +2 -2
- package/.next/standalone/app/components/settings/TierSection.js +1 -1
- package/.next/standalone/app/components/settings/TitleSection.js +149 -0
- package/.next/standalone/app/components/views.js +15 -14
- package/.next/standalone/app/globals.css +11 -0
- package/.next/standalone/app/page.js +89 -156
- package/.next/standalone/app/paths/page.js +39 -27
- package/.next/standalone/app/profile/page.js +33 -5
- package/.next/standalone/app/queue/page.js +56 -34
- package/.next/standalone/app/scores/page.js +16 -66
- package/.next/standalone/app/settings/page.js +27 -13
- package/.next/standalone/app/setup/page.js +298 -205
- package/.next/standalone/app/social/Crm.js +683 -0
- package/.next/standalone/app/social/SocialHub.js +374 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +10 -351
- package/.next/standalone/lib/chain-layout.js +105 -35
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- package/.next/standalone/lib/degrees.js +0 -6
- package/.next/standalone/lib/dot-rings.js +25 -0
- package/.next/standalone/lib/galaxy-lab.js +65 -6
- package/.next/standalone/lib/ingest.js +2 -0
- package/.next/standalone/lib/island.js +40 -0
- package/.next/standalone/lib/linkedin-export.js +267 -46
- package/.next/standalone/lib/linkedin-limits.js +6 -2
- package/.next/standalone/lib/map-heat.js +20 -0
- package/.next/standalone/lib/network-health.js +1 -1
- package/.next/standalone/lib/notifications.js +121 -0
- package/.next/standalone/lib/people-map.js +82 -0
- package/.next/standalone/lib/rarity.js +45 -3
- package/.next/standalone/lib/reach.js +7 -6
- package/.next/standalone/lib/rpc.js +15 -4
- package/.next/standalone/lib/scan-pace.js +46 -0
- package/.next/standalone/lib/scoring.js +39 -8
- package/.next/standalone/lib/scraper-client.js +19 -6
- package/.next/standalone/lib/sector-focus.js +4 -4
- package/.next/standalone/lib/separation.js +137 -10
- package/.next/standalone/lib/settings-effects.js +20 -11
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/lib/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- package/.next/standalone/lib/tier-grid.js +99 -0
- package/.next/standalone/lib/title-ranking.js +83 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +177 -35
- package/.next/static/chunks/03bziijw5srol.js +1 -0
- package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/CHANGELOG.md +235 -0
- package/lib/chain-layout.js +105 -35
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- package/lib/degrees.js +0 -6
- package/lib/dot-rings.js +25 -0
- package/lib/galaxy-lab.js +65 -6
- package/lib/ingest.js +2 -0
- package/lib/island.js +40 -0
- package/lib/linkedin-export.js +267 -46
- package/lib/linkedin-limits.js +6 -2
- package/lib/map-heat.js +20 -0
- package/lib/network-health.js +1 -1
- package/lib/notifications.js +121 -0
- package/lib/people-map.js +82 -0
- package/lib/rarity.js +45 -3
- package/lib/reach.js +7 -6
- package/lib/rpc.js +15 -4
- package/lib/scan-pace.js +46 -0
- package/lib/scoring.js +39 -8
- package/lib/scraper-client.js +19 -6
- package/lib/sector-focus.js +4 -4
- package/lib/separation.js +137 -10
- package/lib/settings-effects.js +20 -11
- package/lib/settings.js +3 -0
- package/lib/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/lib/tier-grid.js +99 -0
- package/lib/title-ranking.js +83 -0
- package/package.json +1 -1
- package/scripts/scrape.py +177 -35
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
- package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- package/.next/standalone/app/components/OrbitGraph.js +0 -737
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Social (experimental): what your own LinkedIn data says about your
|
|
4
|
+
// relationships (lib/linkedin-export.js). Two sources, both yours:
|
|
5
|
+
// - the export LinkedIn emails you (Settings → Data privacy → Get a copy of
|
|
6
|
+
// your data), read here in the browser: full history, no traffic to LinkedIn;
|
|
7
|
+
// - an experimental live sync (the scanner's --messages): your messages list,
|
|
8
|
+
// read once in your own Chrome, by hand or once a day.
|
|
9
|
+
// Kept on this computer: dates, who wrote last and counts, the names of people
|
|
10
|
+
// who aren't your connections, and the messages themselves (and the notes sent
|
|
11
|
+
// with requests) only if you turn on Keep my messages, in a file of their own
|
|
12
|
+
// that switching it off deletes. The CRM (Crm.js) is the main section: everyone
|
|
13
|
+
// you've been in touch with, and your own stage, tags, notes and follow-ups.
|
|
14
|
+
|
|
15
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
16
|
+
import Link from 'next/link';
|
|
17
|
+
import { useUser } from '../components/UserProvider';
|
|
18
|
+
import { loadNetwork } from '../../lib/network';
|
|
19
|
+
import {
|
|
20
|
+
readTable, messageStats, careerChapters, postingEffect, invitationSplit, warmthOf, buildConversations, conversationSummary, buildInvitations,
|
|
21
|
+
} from '../../lib/linkedin-export';
|
|
22
|
+
import { mergeSocial } from '../../lib/galaxy-lab';
|
|
23
|
+
import { keyFor } from '../../lib/separation';
|
|
24
|
+
import { runScrape, watchScanner, scannerNow } from '../../lib/scraper-client';
|
|
25
|
+
import { Body, LINE, FONT } from '../components/ui';
|
|
26
|
+
import { IS_DEMO } from '../../lib/demo';
|
|
27
|
+
import Crm from './Crm';
|
|
28
|
+
|
|
29
|
+
const WARMTH = { warm: ['Warm', '#ff9f43'], cool: ['Cool', '#3498DB'], dormant: ['Dormant', '#8899aa'], never: ['Never messaged', '#556'] };
|
|
30
|
+
const FILES = { 'connections.csv': 'first name', 'messages.csv': 'conversation id', 'invitations.csv': 'direction', 'positions.csv': 'company name', 'shares.csv': 'date' };
|
|
31
|
+
const day = (t) => (t ? new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
|
|
32
|
+
// Messages go to the server in parts of about this many characters: a request
|
|
33
|
+
// here can carry 10 MB, and a character can take up to 3 bytes.
|
|
34
|
+
const PART_CHARS = 2_500_000;
|
|
35
|
+
|
|
36
|
+
/** An import's messages, in parts that each fit in one request. */
|
|
37
|
+
async function sendMessages(conversations) {
|
|
38
|
+
const parts = [];
|
|
39
|
+
let cur = {};
|
|
40
|
+
let size = 0;
|
|
41
|
+
for (const c of conversations) {
|
|
42
|
+
for (const m of c.messages) {
|
|
43
|
+
const n = JSON.stringify(m).length + 2;
|
|
44
|
+
if (size + n > PART_CHARS && size > 0) { parts.push(cur); cur = {}; size = 0; }
|
|
45
|
+
if (!cur[c.id]) { cur[c.id] = { title: c.title || '', messages: [] }; size += c.id.length + (c.title || '').length + 40; }
|
|
46
|
+
cur[c.id].messages.push(m);
|
|
47
|
+
size += n;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
parts.push(cur);
|
|
51
|
+
for (let i = 0; i < parts.length; i++) {
|
|
52
|
+
const r = await fetch('/api/social/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ part: i, parts: parts.length, threads: parts[i] }) });
|
|
53
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'The messages couldn’t be saved.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The notes sent with requests, when the messages are kept: they ride in the same file and go with it. */
|
|
58
|
+
async function sendInviteNotes(invitations) {
|
|
59
|
+
const invites = {};
|
|
60
|
+
for (const i of invitations) if (i.note) invites[i.id] = i.note;
|
|
61
|
+
const r = await fetch('/api/social/messages', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ invites }) });
|
|
62
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'The requests’ notes couldn’t be saved.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Social, the CRM and your LinkedIn export, as part of Outlink (Blake,
|
|
67
|
+
* 2026-10-02: "make outlink a one thing with the dms follow up and etc mixed
|
|
68
|
+
* with the social aspect of adding those people and keeping track"). `embedded`
|
|
69
|
+
* leaves out its own header: Outlink's tabs and notch are above it.
|
|
70
|
+
*/
|
|
71
|
+
export default function SocialHub({ embedded = false }) {
|
|
72
|
+
const { userId } = useUser();
|
|
73
|
+
const [social, setSocial] = useState(undefined);
|
|
74
|
+
const [net, setNet] = useState(null);
|
|
75
|
+
const [keepEmails, setKeepEmails] = useState(false);
|
|
76
|
+
const [note, setNote] = useState(null);
|
|
77
|
+
const [busy, setBusy] = useState(false);
|
|
78
|
+
const [job, setJob] = useState(() => scannerNow());
|
|
79
|
+
const [switching, setSwitching] = useState(false);
|
|
80
|
+
const [listToken, setListToken] = useState(0);
|
|
81
|
+
const [crmToken, setCrmToken] = useState(0);
|
|
82
|
+
// The last export's messages.csv and Invitations.csv, in this window only, so
|
|
83
|
+
// turning Keep my messages on right after an import needn't ask for the folder again.
|
|
84
|
+
const lastMessages = useRef(null);
|
|
85
|
+
const lastInvites = useRef(null);
|
|
86
|
+
const keep = social?.keepMessages === true;
|
|
87
|
+
|
|
88
|
+
// The Conversations list reads again whenever this does (listToken).
|
|
89
|
+
const reload = () => fetch('/api/social').then((r) => r.json()).then((d) => setSocial(d.social || null)).catch(() => setSocial(null))
|
|
90
|
+
.finally(() => setListToken((n) => n + 1));
|
|
91
|
+
useEffect(() => { reload(); }, []);
|
|
92
|
+
useEffect(() => watchScanner(() => setJob(scannerNow())), []);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!userId) return undefined;
|
|
95
|
+
let live = true;
|
|
96
|
+
loadNetwork(userId).then((n) => { if (live) setNet(n); }).catch(() => {});
|
|
97
|
+
return () => { live = false; };
|
|
98
|
+
}, [userId]);
|
|
99
|
+
|
|
100
|
+
async function importFolder(list) {
|
|
101
|
+
setBusy(true);
|
|
102
|
+
setNote(null);
|
|
103
|
+
try {
|
|
104
|
+
const found = {};
|
|
105
|
+
for (const f of Array.from(list || [])) {
|
|
106
|
+
const name = f.name.toLowerCase();
|
|
107
|
+
if (FILES[name] && !found[name]) found[name] = readTable(await f.text(), FILES[name]);
|
|
108
|
+
}
|
|
109
|
+
if (!Object.keys(found).length) throw new Error('That folder doesn’t have LinkedIn’s export files in it (Connections.csv, messages.csv and so on).');
|
|
110
|
+
const conns = found['connections.csv'] || [];
|
|
111
|
+
const stats = found['messages.csv'] ? messageStats(found['messages.csv']) : { asOf: null, people: new Map() };
|
|
112
|
+
// The words are read out of messages.csv only when Keep my messages is on.
|
|
113
|
+
const built = found['messages.csv'] ? buildConversations(found['messages.csv'], { keepText: keep }) : null;
|
|
114
|
+
const invitations = found['invitations.csv'] ? buildInvitations(found['invitations.csv'], { keepText: keep }) : null;
|
|
115
|
+
lastMessages.current = found['messages.csv'] || null;
|
|
116
|
+
lastInvites.current = found['invitations.csv'] || null;
|
|
117
|
+
const people = {};
|
|
118
|
+
for (const [url, v] of stats.people) people[url] = { last: v.last, lastFromThem: v.lastFromThem, total: v.total, recent: v.recent };
|
|
119
|
+
if (keepEmails) {
|
|
120
|
+
for (const c of conns) {
|
|
121
|
+
const e = c['email address'];
|
|
122
|
+
if (!e) continue;
|
|
123
|
+
const k = keyFor({ profile_url: c.url });
|
|
124
|
+
people[k] = { ...(people[k] || {}), email: e };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const asOf = stats.asOf || Date.now();
|
|
128
|
+
const body = {
|
|
129
|
+
asOf,
|
|
130
|
+
people,
|
|
131
|
+
conversations: built ? built.conversations.map(conversationSummary) : [],
|
|
132
|
+
chapters: careerChapters(found['positions.csv'] || [], conns, asOf),
|
|
133
|
+
posts: found['shares.csv'] ? postingEffect(found['shares.csv'], conns) : null,
|
|
134
|
+
invites: found['invitations.csv'] ? invitationSplit(found['invitations.csv']) : null,
|
|
135
|
+
// One record per request, without the notes: those go with the messages, below.
|
|
136
|
+
invitations: (invitations || []).map(({ note, ...rest }) => rest),
|
|
137
|
+
// Names as the export spells them, so people who aren't connections can be listed.
|
|
138
|
+
names: built ? built.names : {},
|
|
139
|
+
emails: keepEmails,
|
|
140
|
+
};
|
|
141
|
+
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
142
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'It couldn’t be saved.');
|
|
143
|
+
if (built && keep) await sendMessages(built.conversations);
|
|
144
|
+
if (invitations && keep) await sendInviteNotes(invitations);
|
|
145
|
+
const odd = built ? [
|
|
146
|
+
built.drafts ? `${built.drafts.toLocaleString()} unsent ${built.drafts === 1 ? 'draft' : 'drafts'} left out` : '',
|
|
147
|
+
built.skipped ? `${built.skipped.toLocaleString()} ${built.skipped === 1 ? 'row' : 'rows'} with no conversation or date left out` : '',
|
|
148
|
+
].filter(Boolean) : [];
|
|
149
|
+
setNote({
|
|
150
|
+
good: true,
|
|
151
|
+
text: `Read ${Object.keys(found).length} of LinkedIn’s files: ${stats.people.size.toLocaleString()} people you’ve messaged one to one`
|
|
152
|
+
+ (built ? `, ${built.conversations.length.toLocaleString()} conversations${keep ? ', their messages kept on this computer' : ''}` : '')
|
|
153
|
+
+ (invitations ? `, ${invitations.length.toLocaleString()} connection requests` : '')
|
|
154
|
+
+ `.${odd.length ? ` (${odd.join('; ')}.)` : ''}`,
|
|
155
|
+
});
|
|
156
|
+
await reload();
|
|
157
|
+
} catch (e) {
|
|
158
|
+
setNote({ good: false, text: e.message });
|
|
159
|
+
} finally {
|
|
160
|
+
setBusy(false);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The daily sync reads the top of the list; `full` scrolls until the whole
|
|
165
|
+
// history has loaded (scripts/scrape.py --full-history), at the same pace.
|
|
166
|
+
async function syncNow(full = false) {
|
|
167
|
+
setNote(null);
|
|
168
|
+
try {
|
|
169
|
+
const end = await runScrape(full ? 'messages-full' : 'messages');
|
|
170
|
+
setNote(end?.exitCode === 0
|
|
171
|
+
? { good: true, text: full ? 'Your whole messages history is read.' : 'Your messages list is read.' }
|
|
172
|
+
: { good: false, text: 'The sync stopped before it finished. The Scan page has the log.' });
|
|
173
|
+
await reload();
|
|
174
|
+
} catch (e) {
|
|
175
|
+
setNote({ good: false, text: e.message });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function setAuto(on) {
|
|
180
|
+
await fetch('/api/social', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ autoSync: on }) });
|
|
181
|
+
reload();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Keep my messages. On: from now on the export's and the sync's words are
|
|
185
|
+
// kept, and if an export was read in this window, its messages are kept now.
|
|
186
|
+
// Off, once you've said so: the kept messages are deleted.
|
|
187
|
+
async function setKeep(on) {
|
|
188
|
+
if (!on && !window.confirm('Delete the messages kept on this computer? The list of conversations stays; the messages themselves are deleted.')) return;
|
|
189
|
+
setSwitching(true);
|
|
190
|
+
setNote(null);
|
|
191
|
+
try {
|
|
192
|
+
if (!on) await fetch('/api/social/messages', { method: 'DELETE' });
|
|
193
|
+
const r = await fetch('/api/social', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keepMessages: on }) });
|
|
194
|
+
if (!r.ok) throw new Error('The switch couldn’t be saved.');
|
|
195
|
+
if (on && (lastMessages.current || lastInvites.current)) {
|
|
196
|
+
if (lastMessages.current) await sendMessages(buildConversations(lastMessages.current, { keepText: true }).conversations);
|
|
197
|
+
if (lastInvites.current) await sendInviteNotes(buildInvitations(lastInvites.current, { keepText: true }));
|
|
198
|
+
setNote({ good: true, text: 'Your messages are kept on this computer now.' });
|
|
199
|
+
} else if (on) {
|
|
200
|
+
setNote({ good: true, text: 'On. Choose your export folder again, or sync your messages, to bring the messages in.' });
|
|
201
|
+
} else {
|
|
202
|
+
setNote({ good: true, text: 'Off. The kept messages are deleted.' });
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
setNote({ good: false, text: e.message });
|
|
206
|
+
} finally {
|
|
207
|
+
setSwitching(false);
|
|
208
|
+
reload();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Forget it: the export's and the sync's findings, and the messages. Your CRM
|
|
213
|
+
// notes are your own work, so they're a question of their own, asked after.
|
|
214
|
+
async function forget() {
|
|
215
|
+
await fetch('/api/social', { method: 'DELETE' });
|
|
216
|
+
lastMessages.current = null;
|
|
217
|
+
lastInvites.current = null;
|
|
218
|
+
let text = 'Forgotten. Nothing from your export or messages is kept here now, the messages themselves included.';
|
|
219
|
+
if (window.confirm('Also delete your CRM: the stages, tags, notes and follow-up dates you added? This can’t be undone. Cancel keeps them.')) {
|
|
220
|
+
await fetch('/api/social/crm', { method: 'DELETE' });
|
|
221
|
+
setCrmToken((n) => n + 1);
|
|
222
|
+
text += ' Your CRM notes are deleted too.';
|
|
223
|
+
} else {
|
|
224
|
+
text += ' Your CRM notes are kept.';
|
|
225
|
+
}
|
|
226
|
+
setNote({ good: true, text });
|
|
227
|
+
reload();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// What each person's messages say, the export and the live sync together, and
|
|
231
|
+
// "now" for warmth: the newest thing the data knows, so an older export reads as it was.
|
|
232
|
+
const { merged, asOf } = useMemo(() => mergeSocial(social), [social]);
|
|
233
|
+
|
|
234
|
+
const warmth = useMemo(() => {
|
|
235
|
+
const counts = { warm: 0, cool: 0, dormant: 0, never: 0 };
|
|
236
|
+
for (const c of net?.degree1 || []) counts[warmthOf(merged.get(keyFor(c)), asOf)] += 1;
|
|
237
|
+
return counts;
|
|
238
|
+
}, [net, merged, asOf]);
|
|
239
|
+
|
|
240
|
+
if (IS_DEMO) return <div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#aaa', padding: 40, fontFamily: FONT }}>Not part of the demo.</div>;
|
|
241
|
+
const running = job?.running;
|
|
242
|
+
const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)', marginTop: 10 };
|
|
243
|
+
const h2 = { fontSize: 16, margin: '24px 0 6px' };
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div style={embedded ? { color: '#fff', fontFamily: FONT } : { minHeight: '100vh', background: '#0a0a1a', color: '#fff', fontFamily: FONT }}>
|
|
247
|
+
{!embedded && (
|
|
248
|
+
<header style={{ padding: '16px 24px', borderBottom: LINE, display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
249
|
+
<Link href="/" style={{ color: '#888', textDecoration: 'none', fontSize: 13, fontWeight: 600, padding: '6px 14px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', border: LINE }}>← Back to Map</Link>
|
|
250
|
+
<h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, color: '#FFD700' }}>Social <span style={{ fontSize: 12, color: '#8b9a9a', fontWeight: 600 }}>experimental</span></h1>
|
|
251
|
+
</header>
|
|
252
|
+
)}
|
|
253
|
+
<main style={{ maxWidth: 1180, margin: '0 auto', padding: '8px 16px 64px' }}>
|
|
254
|
+
<Body style={{ marginTop: 16 }}>
|
|
255
|
+
Everyone you’ve been in touch with on LinkedIn, connections or not: who’s waiting on you, who
|
|
256
|
+
hasn’t answered, what you’ve sent and received, and your own notes and follow-ups on each, from
|
|
257
|
+
your own LinkedIn data. Only dates, who wrote last, counts and names are kept on this computer, and what
|
|
258
|
+
messages say only if you turn on <i>Keep my messages</i> below.
|
|
259
|
+
</Body>
|
|
260
|
+
|
|
261
|
+
<h2 style={h2}>Your LinkedIn export</h2>
|
|
262
|
+
<div style={box}>
|
|
263
|
+
<Body style={{ margin: 0 }}>
|
|
264
|
+
On LinkedIn: <i>Settings → Data privacy → Get a copy of your data</i>, pick the larger archive, and unzip the
|
|
265
|
+
file it emails you. Then choose that folder here. It’s read in this window; nothing goes to LinkedIn.
|
|
266
|
+
</Body>
|
|
267
|
+
<label style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12.5, color: '#b8c4c4', marginTop: 10 }}>
|
|
268
|
+
<input type="checkbox" checked={keepEmails} onChange={(e) => setKeepEmails(e.target.checked)} style={{ marginTop: 3 }} />
|
|
269
|
+
<span>Also keep email addresses from it, on this computer only, for one-click email.</span>
|
|
270
|
+
</label>
|
|
271
|
+
<div style={{ marginTop: 10, display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
272
|
+
<label style={{ padding: '7px 14px', borderRadius: 8, background: 'rgba(52,152,219,0.18)', color: '#cfe6f7', cursor: busy ? 'default' : 'pointer', fontSize: 13 }}>
|
|
273
|
+
{busy ? 'Reading…' : social ? 'Choose a newer export' : 'Choose the export folder'}
|
|
274
|
+
<input type="file" webkitdirectory="" directory="" multiple disabled={busy} style={{ display: 'none' }} onChange={(e) => importFolder(e.target.files)} />
|
|
275
|
+
</label>
|
|
276
|
+
{social && <span style={{ fontSize: 12, color: '#8b9a9a' }}>From an export up to {day(social.asOf)}{social.liveAt ? ` · messages synced ${day(Date.parse(social.liveAt))}` : ''}</span>}
|
|
277
|
+
{social && <button onClick={forget} style={{ background: 'none', border: 'none', color: '#ff9b9b', cursor: 'pointer', fontSize: 12 }}>Forget it</button>}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<h2 style={h2}>Live messages sync <span style={{ fontSize: 11, color: '#FFD700' }}>experimental</span></h2>
|
|
282
|
+
<div style={box}>
|
|
283
|
+
<Body style={{ margin: 0 }}>
|
|
284
|
+
Reads your messages list once, in your own Chrome: who each conversation is with, connections or not, when it
|
|
285
|
+
was last active, whether it’s unread and who wrote last. It uses no search budget and opens no
|
|
286
|
+
conversation. The newest message’s words are kept only if <i>Keep my messages</i> is on. The daily sync
|
|
287
|
+
reads the top of the list; <i>Read my whole history</i> keeps scrolling until no more conversations load
|
|
288
|
+
(at most 60 scrolls, about 1,000 conversations), at the same slow, fixed pace, so it takes a few minutes.
|
|
289
|
+
</Body>
|
|
290
|
+
<div style={{ marginTop: 10, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
291
|
+
<button onClick={() => syncNow(false)} disabled={running} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', background: running ? '#333' : 'rgba(0,255,136,0.15)', color: running ? '#777' : '#00ff88', cursor: running ? 'default' : 'pointer', fontSize: 13 }}>
|
|
292
|
+
{running ? 'The scanner is busy' : 'Sync messages now'}
|
|
293
|
+
</button>
|
|
294
|
+
<button onClick={() => syncNow(true)} disabled={running} style={{ padding: '7px 14px', borderRadius: 8, border: LINE, background: 'none', color: running ? '#777' : '#cfe6f7', cursor: running ? 'default' : 'pointer', fontSize: 13 }}>
|
|
295
|
+
Read my whole history
|
|
296
|
+
</button>
|
|
297
|
+
<label style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 12.5, color: '#b8c4c4' }}>
|
|
298
|
+
<input type="checkbox" checked={social?.autoSync === true} onChange={(e) => setAuto(e.target.checked)} />
|
|
299
|
+
Once a day while the app is open (daytime only)
|
|
300
|
+
</label>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
{note && <div style={{ marginTop: 10, fontSize: 13, color: note.good ? '#00ff88' : '#ff9b9b' }}>{note.text}</div>}
|
|
304
|
+
|
|
305
|
+
<h2 style={h2}>People</h2>
|
|
306
|
+
<Crm net={net} asOf={social ? asOf : null} keep={keep} onKeep={setKeep} switching={switching || busy} token={listToken} crmToken={crmToken} />
|
|
307
|
+
|
|
308
|
+
{social && (
|
|
309
|
+
<>
|
|
310
|
+
<h2 style={h2}>Warmth</h2>
|
|
311
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 10 }}>
|
|
312
|
+
{Object.entries(WARMTH).map(([k, [label, color]]) => (
|
|
313
|
+
<div key={k} style={{ ...box, marginTop: 0 }}>
|
|
314
|
+
<div style={{ fontSize: 22, fontWeight: 700, color }}>{(warmth[k] || 0).toLocaleString()}</div>
|
|
315
|
+
<div style={{ fontSize: 12.5, color: '#b8c4c4' }}>{label}</div>
|
|
316
|
+
</div>
|
|
317
|
+
))}
|
|
318
|
+
</div>
|
|
319
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 6 }}>
|
|
320
|
+
Of your connections. Warm: messaged in the last 30 days, or 3+ messages in 90. Cool: within a year. Dormant: longer ago.
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{social.chapters?.length > 0 && (
|
|
324
|
+
<>
|
|
325
|
+
<h2 style={h2}>Career chapters</h2>
|
|
326
|
+
<div style={{ ...box, padding: 0 }}>
|
|
327
|
+
{social.chapters.map((c) => {
|
|
328
|
+
const warm = (c.people || []).filter((u) => ['warm', 'cool'].includes(warmthOf(merged.get(keyFor({ profile_url: u })), asOf))).length;
|
|
329
|
+
return (
|
|
330
|
+
<div key={`${c.company}-${c.from}`} style={{ display: 'flex', gap: 10, padding: '8px 14px', borderBottom: '1px solid rgba(255,255,255,0.04)', fontSize: 13, alignItems: 'baseline' }}>
|
|
331
|
+
<span style={{ flex: 1 }}><b>{c.company}</b> <span style={{ color: '#8b9a9a' }}>{c.title} · {new Date(c.from).getFullYear()}–{c.to ? new Date(c.to).getFullYear() : 'now'}</span></span>
|
|
332
|
+
<span style={{ color: '#b8c4c4' }}>{c.made.toLocaleString()} connections made</span>
|
|
333
|
+
<span style={{ color: '#ff9f43', minWidth: 90, textAlign: 'right' }}>{c.made ? `${Math.round((100 * warm) / c.made)}% still in touch` : ''}</span>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
})}
|
|
337
|
+
</div>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
{social.posts?.posts?.length > 0 && (
|
|
342
|
+
<>
|
|
343
|
+
<h2 style={h2}>Does posting work?</h2>
|
|
344
|
+
<div style={box}>
|
|
345
|
+
<div style={{ fontSize: 13 }}>
|
|
346
|
+
In the week after a post you made <b>{social.posts.afterPosts.toFixed(1)}</b> new connections on average; a usual week, <b>{social.posts.usualWeek.toFixed(1)}</b>.
|
|
347
|
+
</div>
|
|
348
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 4 }}>What came after a post, not proof it came from it.</div>
|
|
349
|
+
{[...social.posts.posts].sort((a, b) => b.week - a.week).slice(0, 3).map((p) => (
|
|
350
|
+
<div key={p.t} style={{ fontSize: 12.5, color: '#b8c4c4', marginTop: 6 }}>
|
|
351
|
+
{day(p.t)}: {p.week} new connections that week {p.link ? <a href={p.link} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Post ↗</a> : null}
|
|
352
|
+
</div>
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
</>
|
|
356
|
+
)}
|
|
357
|
+
|
|
358
|
+
{social.invites?.total > 0 && (
|
|
359
|
+
<>
|
|
360
|
+
<h2 style={h2}>Who reaches out</h2>
|
|
361
|
+
<div style={box}>
|
|
362
|
+
<span style={{ fontSize: 13 }}>
|
|
363
|
+
{social.invites.incoming.toLocaleString()} requests came to you and you sent {social.invites.outgoing.toLocaleString()}
|
|
364
|
+
{' '}({Math.round((100 * social.invites.incoming) / social.invites.total)}% came to you), as far as your export shows.
|
|
365
|
+
</span>
|
|
366
|
+
</div>
|
|
367
|
+
</>
|
|
368
|
+
)}
|
|
369
|
+
</>
|
|
370
|
+
)}
|
|
371
|
+
</main>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// One conversation's messages in the Social tab's CRM (Crm.js), newest at the
|
|
4
|
+
// bottom. Only while Keep my messages is on: the words live only in the app's
|
|
5
|
+
// data folder (lib/social-store.js), and GET /api/social/messages serves them
|
|
6
|
+
// a page at a time.
|
|
7
|
+
|
|
8
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
9
|
+
import { LINE } from '../components/ui';
|
|
10
|
+
|
|
11
|
+
const THREAD_PAGE = 100;
|
|
12
|
+
const EARLIER_PAGE = 200;
|
|
13
|
+
const when = (t) => (t ? new Date(t).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: 'numeric', minute: '2-digit' }) : '');
|
|
14
|
+
|
|
15
|
+
// One conversation's messages, newest at the bottom, a page at a time: the
|
|
16
|
+
// newest 100 first and 200 more each time you ask, so a thread of thousands
|
|
17
|
+
// opens as fast as a short one.
|
|
18
|
+
export default function Thread({ id, keep, group, token }) {
|
|
19
|
+
const [t, setT] = useState(null);
|
|
20
|
+
const [err, setErr] = useState(null);
|
|
21
|
+
const scroller = useRef(null);
|
|
22
|
+
const keepAt = useRef(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!keep) return undefined;
|
|
26
|
+
let live = true;
|
|
27
|
+
fetch(`/api/social/messages?id=${encodeURIComponent(id)}&limit=${THREAD_PAGE}`).then((r) => r.json())
|
|
28
|
+
.then((d) => { if (live) { if (d.error) setErr(d.error); else { keepAt.current = 'bottom'; setT(d); } } })
|
|
29
|
+
.catch(() => { if (live) setErr('It couldn’t be read.'); });
|
|
30
|
+
return () => { live = false; };
|
|
31
|
+
}, [id, keep, token]);
|
|
32
|
+
|
|
33
|
+
// Opened at the newest; after "Show earlier", where you were stays put.
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
|
+
const el = scroller.current;
|
|
36
|
+
if (!el || !keepAt.current) return;
|
|
37
|
+
if (keepAt.current === 'bottom') el.scrollTop = el.scrollHeight;
|
|
38
|
+
else el.scrollTop = el.scrollHeight - keepAt.current;
|
|
39
|
+
keepAt.current = null;
|
|
40
|
+
}, [t]);
|
|
41
|
+
|
|
42
|
+
async function earlier() {
|
|
43
|
+
const el = scroller.current;
|
|
44
|
+
const d = await fetch(`/api/social/messages?id=${encodeURIComponent(id)}&before=${t.start}&limit=${EARLIER_PAGE}`).then((r) => r.json()).catch(() => null);
|
|
45
|
+
if (!d || d.error) return;
|
|
46
|
+
keepAt.current = el ? el.scrollHeight - el.scrollTop : null;
|
|
47
|
+
setT({ ...t, start: d.start, messages: [...d.messages, ...t.messages] });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pad = { padding: '10px 14px 14px', fontSize: 12.5, color: '#8b9a9a' };
|
|
51
|
+
if (!keep) return <div style={pad}>Turn on <b>Keep my messages</b> to read them here.</div>;
|
|
52
|
+
if (err) return <div style={{ ...pad, color: '#ff9b9b' }}>{err}</div>;
|
|
53
|
+
if (!t) return <div style={pad}>Opening…</div>;
|
|
54
|
+
if (!t.total) {
|
|
55
|
+
return (
|
|
56
|
+
<div style={pad}>
|
|
57
|
+
No messages are kept for this conversation yet. Choose your export folder again, or sync your messages, to
|
|
58
|
+
bring them in.
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
<div ref={scroller} style={{ maxHeight: 440, overflowY: 'auto', padding: '8px 14px 12px', background: 'rgba(0,0,0,0.2)' }}>
|
|
64
|
+
{t.start > 0 && (
|
|
65
|
+
<div style={{ textAlign: 'center', margin: '4px 0 10px' }}>
|
|
66
|
+
<button onClick={earlier} style={{ background: 'rgba(255,255,255,0.06)', border: LINE, borderRadius: 6, color: '#cfe6f7', cursor: 'pointer', fontSize: 12, padding: '4px 12px' }}>
|
|
67
|
+
Show earlier ({t.start.toLocaleString()} more)
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
{t.messages.map((m, i) => (
|
|
72
|
+
<div key={t.start + i} style={{ display: 'flex', justifyContent: m.fromMe ? 'flex-end' : 'flex-start', margin: '6px 0' }}>
|
|
73
|
+
<div style={{ maxWidth: '72%' }}>
|
|
74
|
+
<div style={{
|
|
75
|
+
padding: '7px 11px', borderRadius: 12, fontSize: 13, lineHeight: 1.45, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere',
|
|
76
|
+
background: m.fromMe ? 'rgba(52,152,219,0.35)' : 'rgba(255,255,255,0.08)', color: '#eef',
|
|
77
|
+
borderBottomRightRadius: m.fromMe ? 4 : 12, borderBottomLeftRadius: m.fromMe ? 12 : 4,
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{m.text || <i style={{ color: '#8b9a9a' }}>(no words: an attachment or a reaction)</i>}
|
|
81
|
+
</div>
|
|
82
|
+
<div style={{ fontSize: 10.5, color: '#778', marginTop: 2, textAlign: m.fromMe ? 'right' : 'left' }}>
|
|
83
|
+
{m.fromMe ? 'You' : group ? 'Someone in the group' : 'Them'} · {when(m.t)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|