@@ -9,6 +9,12 @@ import { computeShareCardMetrics } from "@/lib/vcp/metrics";
99import { isVibeAxes } from "@/lib/vcp/validators" ;
1010import { ShareCard , ShareActions } from "@/components/share" ;
1111import type { ShareImageTemplate , ShareCardMetric } from "@/components/share" ;
12+ import {
13+ RepoIdentitySection ,
14+ RepoAxesSection ,
15+ RepoMetricsGrid ,
16+ ProfileContributionCard ,
17+ } from "@/components/vcp/repo" ;
1218
1319type Job = {
1420 id : string ;
@@ -177,15 +183,6 @@ function isNarrativeJson(v: unknown): v is NarrativeJson {
177183 return true ;
178184}
179185
180- const VIBE_AXIS_KEYS : ( keyof VibeAxes ) [ ] = [
181- "automation_heaviness" ,
182- "guardrail_strength" ,
183- "iteration_loop_intensity" ,
184- "planning_signal" ,
185- "surface_area_per_change" ,
186- "shipping_rhythm" ,
187- ] as const ;
188-
189186function isVibeInsightsRow ( v : unknown ) : v is VibeInsightsRow {
190187 if ( ! isRecord ( v ) ) return false ;
191188 if ( ! isVibeAxes ( v . axes_json ) ) return false ;
@@ -459,19 +456,14 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
459456
460457 const persona = wrapped . persona ;
461458 const shareTemplate = wrapped . share_template ;
462- const profileContributionLabel = ( ( ) => {
463- if ( ! profileContribution ) return null ;
464- if ( profileContribution . includedInProfile === true ) return "Included in your Unified VCP" ;
465- if ( profileContribution . includedInProfile === false ) return "Not yet included in your Unified VCP" ;
466- return "Unified VCP impact" ;
467- } ) ( ) ;
468459
469460 const shareText = useMemo ( ( ) => {
470461 if ( ! shareTemplate ) return "" ;
471462 const metricsLine = shareTemplate . metrics
472463 . map ( ( metric ) => `${ metric . label } : ${ metric . value } ` )
473464 . join ( " · " ) ;
474- return `${ shareTemplate . headline } \n${ shareTemplate . subhead } \n${ metricsLine } \n#VCP` ;
465+ const taglineLine = shareTemplate . tagline ?? shareTemplate . subhead ;
466+ return `${ shareTemplate . headline } \n${ taglineLine } \n${ metricsLine } \n#VCP` ;
475467 } , [ shareTemplate ] ) ;
476468
477469 const shareUrl = useMemo ( ( ) => {
@@ -485,7 +477,8 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
485477 . slice ( 0 , 3 )
486478 . map ( ( metric ) => `${ metric . label } : ${ metric . value } ` )
487479 . join ( " · " ) ;
488- return `${ shareTemplate . headline } — ${ shareTemplate . subhead } \n${ metricsLine } \n#VCP` ;
480+ const taglineLine = shareTemplate . tagline ?? shareTemplate . subhead ;
481+ return `${ shareTemplate . headline } — ${ taglineLine } \n${ metricsLine } \n#VCP` ;
489482 } , [ shareTemplate ] ) ;
490483
491484 const storyEndpoint = data ?. userId
@@ -523,11 +516,17 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
523516 ] ;
524517 } , [ persona , parsedVibeInsights , wrapped , insightsJson ] ) ;
525518
519+ const repoAxes =
520+ parsedVibeInsights && isVibeAxes ( parsedVibeInsights . axes_json )
521+ ? parsedVibeInsights . axes_json
522+ : null ;
523+
526524 const shareImageTemplate : ShareImageTemplate | null = shareTemplate
527525 ? {
528526 colors : shareTemplate . colors ,
529527 headline : shareTemplate . headline ,
530528 subhead : shareTemplate . subhead ,
529+ tagline : shareTemplate . tagline ?? shareTemplate . subhead ,
531530 metrics : shareCardMetrics ,
532531 persona_archetype : shareTemplate . persona_archetype ,
533532 }
@@ -813,7 +812,7 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
813812 } }
814813 colors = { shareTemplate . colors }
815814 avatarUrl = { data ?. userAvatarUrl }
816- tagline = { shareTemplate . subhead ?? persona . description }
815+ tagline = { shareTemplate . tagline ?? persona . description }
817816 />
818817 < ShareActions
819818 shareUrl = { shareUrl }
@@ -887,127 +886,26 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
887886 < div className = "relative overflow-hidden rounded-[2rem] border border-black/5 bg-white shadow-sm" >
888887 < div className = "absolute inset-0 bg-gradient-to-br from-violet-500/8 via-transparent to-indigo-500/8" />
889888 < div className = "relative p-8" >
890- < div className = "max-w-2xl" >
891- < p className = "text-xs font-semibold uppercase tracking-[0.25em] text-zinc-500" > Your vibe</ p >
892- < h2 className = "mt-3 text-4xl font-semibold tracking-tight text-zinc-950" >
893- { persona . label }
894- </ h2 >
895- < p className = "mt-3 text-base text-zinc-600" > { persona . description } </ p >
896- { narrative ?. summary ? (
897- < p className = "mt-3 text-sm font-medium text-zinc-800" > { narrative . summary } </ p >
898- ) : null }
899- </ div >
900-
901- < div className = "mt-5 rounded-2xl border border-black/5 bg-white/60 p-4 backdrop-blur" >
902- < details >
903- < summary className = "cursor-pointer text-xs font-semibold uppercase tracking-[0.25em] text-zinc-500" >
904- How we got this
905- </ summary >
906- < div className = "mt-3 space-y-3 text-sm text-zinc-700" >
907- < p >
908- This report is inferred from Git/PR metadata (commit timing, commit size,
909- file paths, and message patterns). We do not use your prompts, IDE workflow,
910- PR comments, or code content—so this is an informed guess based on what lands
911- in Git.
912- </ p >
913- < div >
914- < p className = "text-xs font-semibold uppercase tracking-[0.25em] text-zinc-500" >
915- Matched criteria
916- </ p >
917- { matchedCriteria . length > 0 ? (
918- < div className = "mt-2 flex flex-wrap gap-2" >
919- { matchedCriteria . map ( ( c ) => (
920- < span
921- key = { c }
922- className = "rounded-full border border-black/10 bg-white px-3 py-1 text-xs text-zinc-700"
923- >
924- { formatMetricLabel ( c ) }
925- </ span >
926- ) ) }
927- </ div >
928- ) : (
929- < p className = "mt-2 text-sm text-zinc-600" >
930- This report didn’t include explicit matched criteria.
931- </ p >
932- ) }
933- </ div >
934- < div >
935- < Link
936- href = "/methodology"
937- className = "text-xs font-semibold uppercase tracking-[0.25em] text-zinc-700 underline decoration-zinc-400 underline-offset-4"
938- >
939- Methodology
940- </ Link >
941- </ div >
942- </ div >
943- </ details >
944- </ div >
889+ < RepoIdentitySection
890+ persona = { persona }
891+ narrative = { narrative ? { summary : narrative . summary } : null }
892+ matchedCriteria = { matchedCriteria }
893+ />
945894
946- < div className = "mt-6 grid gap-3 rounded-2xl border border-black/5 bg-white/60 p-4 backdrop-blur sm:grid-cols-2 lg:grid-cols-5" >
947- < div className = "text-center" >
948- < p className = "text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400" > Streak</ p >
949- < p className = "mt-1 text-2xl font-semibold text-zinc-900" >
950- { wrapped . streak . longest_days } day{ wrapped . streak . longest_days === 1 ? "" : "s" }
951- </ p >
952- { wrapped . streak . start_day && wrapped . streak . end_day ? (
953- < p className = "mt-1 text-xs text-zinc-500" > { wrapped . streak . start_day } → { wrapped . streak . end_day } </ p >
954- ) : null }
955- </ div >
956- < div className = "text-center" >
957- < p className = "text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400" > Peak day</ p >
958- < p className = "mt-1 text-2xl font-semibold text-zinc-900" >
959- { wrapped . timing . peak_weekday !== null ? weekdayName ( wrapped . timing . peak_weekday ) : "—" }
960- </ p >
961- { wrapped . timing . peak_window ? (
962- < p className = "mt-1 text-xs text-zinc-500" > { formatMetricLabel ( wrapped . timing . peak_window ) } (UTC)</ p >
963- ) : null }
964- </ div >
965- < div className = "text-center" >
966- < p className = "text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400" > Focus</ p >
967- < p className = "mt-1 text-2xl font-semibold capitalize text-zinc-900" >
968- { wrapped . commits . top_category ?? "—" }
969- </ p >
970- { wrapped . commits . top_category ? (
971- < p className = "mt-1 text-xs text-zinc-500" >
972- { wrapped . commits . category_counts [ wrapped . commits . top_category ] ?? 0 } of { wrapped . totals . commits } commits
973- </ p >
974- ) : null }
895+ { repoAxes ? (
896+ < div className = "mt-6" >
897+ < RepoAxesSection axes = { repoAxes } />
975898 </ div >
976- < div className = "text-center" >
977- < p className = "text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400" > Build vs Fix</ p >
978- < p className = "mt-1 text-2xl font-semibold text-zinc-900" >
979- { wrapped . commits . features_per_fix !== null
980- ? `${ wrapped . commits . features_per_fix . toFixed ( 1 ) } : 1`
981- : wrapped . commits . fixes_per_feature !== null
982- ? `1 : ${ wrapped . commits . fixes_per_feature . toFixed ( 1 ) } `
983- : "—" }
984- </ p >
985- < p className = "mt-1 text-xs text-zinc-500" >
986- { wrapped . commits . features_per_fix !== null
987- ? "features per fix"
988- : wrapped . commits . fixes_per_feature !== null
989- ? "fixes per feature"
990- : "balanced" }
991- </ p >
992- </ div >
993- < div className = "text-center" >
994- < p className = "text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400" > Scope</ p >
995- < p className = "mt-1 text-2xl font-semibold text-zinc-900" >
996- { wrapped . chunkiness . avg_files_changed !== null
997- ? `${ wrapped . chunkiness . avg_files_changed . toFixed ( 1 ) } `
998- : "—" }
999- </ p >
1000- < p className = "mt-1 text-xs text-zinc-500" >
1001- { wrapped . chunkiness . label === "chunker"
1002- ? "files/commit (wide)"
1003- : wrapped . chunkiness . label === "mixer"
1004- ? "files/commit (balanced)"
1005- : wrapped . chunkiness . label === "slicer"
1006- ? "files/commit (focused)"
1007- : "files/commit" }
1008- </ p >
1009- </ div >
1010- </ div >
899+ ) : null }
900+
901+ < RepoMetricsGrid
902+ className = "mt-6"
903+ streak = { wrapped . streak }
904+ timing = { wrapped . timing }
905+ commits = { wrapped . commits }
906+ chunkiness = { wrapped . chunkiness }
907+ totals = { wrapped . totals }
908+ />
1011909
1012910 { /* Artifact Traceability Section */ }
1013911 { wrapped . artifact_traceability ? (
@@ -1170,55 +1068,15 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
11701068 </ div >
11711069
11721070 { profileContribution ? (
1173- < div className = "mt-6 rounded-2xl border border-black/5 bg-white/60 p-4 backdrop-blur" >
1174- < p className = "text-xs font-semibold uppercase tracking-[0.25em] text-zinc-500" >
1175- { profileContributionLabel }
1176- </ p >
1177- < div className = "mt-3 grid gap-3 sm:grid-cols-3" >
1178- < div >
1179- < p className = "text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400" > This repo</ p >
1180- < p className = "mt-1 text-sm font-semibold text-zinc-900" >
1181- { profileContribution . repoName ?? "—" }
1182- </ p >
1183- < p className = "mt-1 text-xs text-zinc-600" >
1184- { typeof profileContribution . jobCommitCount === "number"
1185- ? `${ profileContribution . jobCommitCount } commits`
1186- : "Commit count unavailable" }
1187- </ p >
1188- </ div >
1189- < div >
1190- < p className = "text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400" > Your Unified VCP</ p >
1191- < p className = "mt-1 text-sm font-semibold text-zinc-900" >
1192- { typeof profileContribution . profileTotalRepos === "number"
1193- ? `${ profileContribution . profileTotalRepos } repos`
1194- : "—" }
1195- </ p >
1196- < p className = "mt-1 text-xs text-zinc-600" >
1197- { typeof profileContribution . profileTotalCommits === "number"
1198- ? `${ profileContribution . profileTotalCommits } commits`
1199- : "—" }
1200- </ p >
1201- </ div >
1202- < div >
1203- < p className = "text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400" > Current persona</ p >
1204- < p className = "mt-1 text-sm font-semibold text-zinc-900" >
1205- { profileContribution . profilePersonaName ?? "—" }
1206- </ p >
1207- { profileContribution . profileUpdatedAt ? (
1208- < p className = "mt-1 text-xs text-zinc-600" >
1209- Updated { fmtDate ( profileContribution . profileUpdatedAt ) }
1210- </ p >
1211- ) : (
1212- < p className = "mt-1 text-xs text-zinc-600" > —</ p >
1213- ) }
1214- </ div >
1215- </ div >
1216- { profileContribution . includedInProfile === false ? (
1217- < p className = "mt-3 text-xs text-zinc-500" >
1218- The profile aggregate can lag behind analysis completion by a moment.
1219- </ p >
1220- ) : null }
1221- </ div >
1071+ < ProfileContributionCard
1072+ contribution = { profileContribution }
1073+ isRebuilding = { rebuildingProfile }
1074+ rebuildStatus = { profileRebuildStatus }
1075+ onRebuild = {
1076+ profileContribution . includedInProfile === false ? triggerProfileRebuild : undefined
1077+ }
1078+ className = "mt-6"
1079+ />
12221080 ) : null }
12231081 </ div >
12241082 </ div >
0 commit comments