Skip to content

Commit f2d2621

Browse files
committed
feat(web): refactor AnalysisClient to integrate new VCP components
- Replaced sections in AnalysisClient with new components: RepoIdentitySection, RepoAxesSection, RepoMetricsGrid, and ProfileContributionCard. - Updated share text generation to include tagline from share template. - Enhanced the overall structure and readability of the AnalysisClient by removing deprecated code and improving component usage. - Completed refactoring aligns with the VCP Display Unification initiative, ensuring a consistent user experience across Unified and Repo VCP pages.
1 parent 85ed9a2 commit f2d2621

2 files changed

Lines changed: 61 additions & 200 deletions

File tree

apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx

Lines changed: 44 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { computeShareCardMetrics } from "@/lib/vcp/metrics";
99
import { isVibeAxes } from "@/lib/vcp/validators";
1010
import { ShareCard, ShareActions } from "@/components/share";
1111
import type { ShareImageTemplate, ShareCardMetric } from "@/components/share";
12+
import {
13+
RepoIdentitySection,
14+
RepoAxesSection,
15+
RepoMetricsGrid,
16+
ProfileContributionCard,
17+
} from "@/components/vcp/repo";
1218

1319
type 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-
189186
function 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

Comments
 (0)