diff --git a/.gitignore b/.gitignore index ec1f391..dac105c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ supabase/.temp/ *.swp *.swo *~ +.cursor-memory # OS .DS_Store diff --git a/.release-please-manifest.develop.json b/.release-please-manifest.develop.json index 94d32c7..543ffd0 100644 --- a/.release-please-manifest.develop.json +++ b/.release-please-manifest.develop.json @@ -1,5 +1,5 @@ { - ".": "0.1.0-alpha.2", - "apps/web": "0.1.0-alpha.2", - "apps/worker": "0.1.0-alpha.1" + ".": "0.1.0-alpha.3", + "apps/web": "0.1.0-alpha.3", + "apps/worker": "0.1.0-alpha.2" } diff --git a/CHANGELOG-DEVELOP.md b/CHANGELOG-DEVELOP.md index a61cb70..e6d39ea 100644 --- a/CHANGELOG-DEVELOP.md +++ b/CHANGELOG-DEVELOP.md @@ -1,5 +1,28 @@ # Changelog +## [0.1.0-alpha.3](https://github.com/devakone/vibe-coding-profile/compare/vibed-coding-v0.1.0-alpha.2...vibed-coding-v0.1.0-alpha.3) (2026-01-22) + + +### Features + +* add implementation tracker for Information Architecture Restructure ([15f99fb](https://github.com/devakone/vibe-coding-profile/commit/15f99fb9928399ff9dc2e88f1f2447e28dc8d1d2)) +* add new persona aura images and update theme colors ([4ca44f2](https://github.com/devakone/vibe-coding-profile/commit/4ca44f2d7613031b3af8d319d5d423586d40bed1)) +* add Open Source Preparedness documentation ([6c1c40a](https://github.com/devakone/vibe-coding-profile/commit/6c1c40a49b0eae29977aa1b71f1cde75b2be9231)) +* implement VCP component system with primitives, blocks, and utilities ([bc6abdd](https://github.com/devakone/vibe-coding-profile/commit/bc6abdd8c577ecd9f4c3c2955fa421b4e182fddc)) +* introduce new PRDs for information architecture, multi-agent detection, and LLM provider abstraction ([7929886](https://github.com/devakone/vibe-coding-profile/commit/792988698317c87beb07a30eb7fce1e5b126ffe6)) +* **notifications:** add notification dropdown to header ([aadc064](https://github.com/devakone/vibe-coding-profile/commit/aadc064c2e26ae654e228f1e69912282863a8061)) +* update ShareCard component to incorporate persona aura backgrounds and icons ([ad2d4a0](https://github.com/devakone/vibe-coding-profile/commit/ad2d4a0db7b50a8f1299a05734e5ad93c58a5a68)) +* **vcp:** add unified and repo VCP display components (P3) ([b6a6179](https://github.com/devakone/vibe-coding-profile/commit/b6a617948ff68601f7b468edaa374cfd79c88004)) +* **web:** add tooltip component and integrate into VCP sections ([699433c](https://github.com/devakone/vibe-coding-profile/commit/699433c0f1a3fcae572d58133594d1e9223f6ebb)) +* **web:** enhance sharing features with LLM-generated taglines ([ef3b589](https://github.com/devakone/vibe-coding-profile/commit/ef3b589a6cdf71485c2ebef11f20a4692a49be13)) +* **web:** enhance user profile sharing and navigation updates ([85ed9a2](https://github.com/devakone/vibe-coding-profile/commit/85ed9a215cc9bd7b2a4a466a586a093614a33638)) +* **web:** implement P2 navigation & routes restructure ([9ad5734](https://github.com/devakone/vibe-coding-profile/commit/9ad5734ab6b462e576e81aba39051fb438b4484f)) +* **web:** implement redirects and update internal links for improved navigation ([a4ce715](https://github.com/devakone/vibe-coding-profile/commit/a4ce7153c638383b2949601052877d7aef111784)) +* **web:** integrate QR code generation and vertical story download feature ([975239d](https://github.com/devakone/vibe-coding-profile/commit/975239d3b4c683c64a4c083658289ac1840d6c0f)) +* **web:** refactor AnalysisClient to integrate new VCP components ([f2d2621](https://github.com/devakone/vibe-coding-profile/commit/f2d26211218a4f5d5b17b038db29bfde009edb3b)) +* **web:** refactor layout to improve component structure and user experience ([eb88fec](https://github.com/devakone/vibe-coding-profile/commit/eb88fecfa519b1e9b203c7d1b28d701f0c331a21)) +* **web:** update navigation labels and enhance analysis insights ([17af33d](https://github.com/devakone/vibe-coding-profile/commit/17af33d3a0a7adfa16210a6c8118dd448aded274)) + ## [0.1.0-alpha.2](https://github.com/devakone/vibed-coding/compare/vibed-coding-v0.1.0-alpha.1...vibed-coding-v0.1.0-alpha.2) (2026-01-21) diff --git a/apps/web/CHANGELOG-DEVELOP.md b/apps/web/CHANGELOG-DEVELOP.md index 41fe9e2..df4a5be 100644 --- a/apps/web/CHANGELOG-DEVELOP.md +++ b/apps/web/CHANGELOG-DEVELOP.md @@ -1,5 +1,25 @@ # Changelog +## [0.1.0-alpha.3](https://github.com/devakone/vibe-coding-profile/compare/web-v0.1.0-alpha.2...web-v0.1.0-alpha.3) (2026-01-22) + + +### Features + +* add new persona aura images and update theme colors ([4ca44f2](https://github.com/devakone/vibe-coding-profile/commit/4ca44f2d7613031b3af8d319d5d423586d40bed1)) +* implement VCP component system with primitives, blocks, and utilities ([bc6abdd](https://github.com/devakone/vibe-coding-profile/commit/bc6abdd8c577ecd9f4c3c2955fa421b4e182fddc)) +* **notifications:** add notification dropdown to header ([aadc064](https://github.com/devakone/vibe-coding-profile/commit/aadc064c2e26ae654e228f1e69912282863a8061)) +* update ShareCard component to incorporate persona aura backgrounds and icons ([ad2d4a0](https://github.com/devakone/vibe-coding-profile/commit/ad2d4a0db7b50a8f1299a05734e5ad93c58a5a68)) +* **vcp:** add unified and repo VCP display components (P3) ([b6a6179](https://github.com/devakone/vibe-coding-profile/commit/b6a617948ff68601f7b468edaa374cfd79c88004)) +* **web:** add tooltip component and integrate into VCP sections ([699433c](https://github.com/devakone/vibe-coding-profile/commit/699433c0f1a3fcae572d58133594d1e9223f6ebb)) +* **web:** enhance sharing features with LLM-generated taglines ([ef3b589](https://github.com/devakone/vibe-coding-profile/commit/ef3b589a6cdf71485c2ebef11f20a4692a49be13)) +* **web:** enhance user profile sharing and navigation updates ([85ed9a2](https://github.com/devakone/vibe-coding-profile/commit/85ed9a215cc9bd7b2a4a466a586a093614a33638)) +* **web:** implement P2 navigation & routes restructure ([9ad5734](https://github.com/devakone/vibe-coding-profile/commit/9ad5734ab6b462e576e81aba39051fb438b4484f)) +* **web:** implement redirects and update internal links for improved navigation ([a4ce715](https://github.com/devakone/vibe-coding-profile/commit/a4ce7153c638383b2949601052877d7aef111784)) +* **web:** integrate QR code generation and vertical story download feature ([975239d](https://github.com/devakone/vibe-coding-profile/commit/975239d3b4c683c64a4c083658289ac1840d6c0f)) +* **web:** refactor AnalysisClient to integrate new VCP components ([f2d2621](https://github.com/devakone/vibe-coding-profile/commit/f2d26211218a4f5d5b17b038db29bfde009edb3b)) +* **web:** refactor layout to improve component structure and user experience ([eb88fec](https://github.com/devakone/vibe-coding-profile/commit/eb88fecfa519b1e9b203c7d1b28d701f0c331a21)) +* **web:** update navigation labels and enhance analysis insights ([17af33d](https://github.com/devakone/vibe-coding-profile/commit/17af33d3a0a7adfa16210a6c8118dd448aded274)) + ## [0.1.0-alpha.2](https://github.com/devakone/vibed-coding/compare/web-v0.1.0-alpha.1...web-v0.1.0-alpha.2) (2026-01-21) diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 1e02585..2f4163f 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -5,6 +5,33 @@ const nextConfig: NextConfig = { turbopack: { root: path.join(__dirname, "../.."), }, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + }, + ], + }, + async redirects() { + return [ + { + source: "/reports", + destination: "/vibes", + permanent: true, + }, + { + source: "/reports/:jobId", + destination: "/analysis/:jobId", + permanent: true, + }, + { + source: "/repos", + destination: "/settings/repos", + permanent: true, + }, + ]; + }, }; export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json index ce3d07d..593c382 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,8 +12,10 @@ "dependencies": { "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-tooltip": "^1.2.8", "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.47.14", + "@vercel/og": "^0.8.6", "@vibed/core": "*", "@vibed/db": "*", "class-variance-authority": "^0.7.1", @@ -22,6 +24,7 @@ "inngest": "^3.49.3", "lucide-react": "^0.562.0", "next": "16.1.2", + "qrcode": "^1.5.4", "react": "19.2.3", "react-dom": "19.2.3", "tailwind-merge": "^3.4.0" @@ -29,6 +32,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/qrcode": "^1.5.6", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", diff --git a/apps/web/public/aura-backgrounds/bg-guardrailed-viber-v2.webp b/apps/web/public/aura-backgrounds/bg-guardrailed-viber-v2.webp new file mode 100644 index 0000000..ceb9749 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-guardrailed-viber-v2.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-guardrailed-viber.webp b/apps/web/public/aura-backgrounds/bg-guardrailed-viber.webp new file mode 100644 index 0000000..019269d Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-guardrailed-viber.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-methodical-architect-v2.webp b/apps/web/public/aura-backgrounds/bg-methodical-architect-v2.webp new file mode 100644 index 0000000..000c14c Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-methodical-architect-v2.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-methodical-architect.webp b/apps/web/public/aura-backgrounds/bg-methodical-architect.webp new file mode 100644 index 0000000..ebbb877 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-methodical-architect.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-orchestrator-factory.webp b/apps/web/public/aura-backgrounds/bg-orchestrator-factory.webp new file mode 100644 index 0000000..1b770ba Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-orchestrator-factory.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-orchestrator-v2.webp b/apps/web/public/aura-backgrounds/bg-orchestrator-v2.webp new file mode 100644 index 0000000..8ba8662 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-orchestrator-v2.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-orchestrator-v3.webp b/apps/web/public/aura-backgrounds/bg-orchestrator-v3.webp new file mode 100644 index 0000000..6f69e76 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-orchestrator-v3.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-orchestrator-v5.webp b/apps/web/public/aura-backgrounds/bg-orchestrator-v5.webp new file mode 100644 index 0000000..d1c4025 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-orchestrator-v5.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-prompt-sprinter-v2.webp b/apps/web/public/aura-backgrounds/bg-prompt-sprinter-v2.webp new file mode 100644 index 0000000..866bc0e Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-prompt-sprinter-v2.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-prompt-sprinter.webp b/apps/web/public/aura-backgrounds/bg-prompt-sprinter.webp new file mode 100644 index 0000000..1258761 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-prompt-sprinter.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-rapid-risk-taker-v2.webp b/apps/web/public/aura-backgrounds/bg-rapid-risk-taker-v2.webp new file mode 100644 index 0000000..8c4f021 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-rapid-risk-taker-v2.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-rapid-risk-taker.webp b/apps/web/public/aura-backgrounds/bg-rapid-risk-taker.webp new file mode 100644 index 0000000..9402a5f Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-rapid-risk-taker.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-reflective-balancer-v2.webp b/apps/web/public/aura-backgrounds/bg-reflective-balancer-v2.webp new file mode 100644 index 0000000..660bb07 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-reflective-balancer-v2.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-reflective-balancer.webp b/apps/web/public/aura-backgrounds/bg-reflective-balancer.webp new file mode 100644 index 0000000..9135ba2 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-reflective-balancer.webp differ diff --git a/apps/web/public/aura-backgrounds/bg-vertical-slice-shipper.webp b/apps/web/public/aura-backgrounds/bg-vertical-slice-shipper.webp new file mode 100644 index 0000000..dff12d2 Binary files /dev/null and b/apps/web/public/aura-backgrounds/bg-vertical-slice-shipper.webp differ diff --git a/apps/web/public/aura-icons/icon-balanced-builder.webp b/apps/web/public/aura-icons/icon-balanced-builder.webp new file mode 100644 index 0000000..12c2e71 Binary files /dev/null and b/apps/web/public/aura-icons/icon-balanced-builder.webp differ diff --git a/apps/web/public/aura-icons/icon-fix-loop-hacker.webp b/apps/web/public/aura-icons/icon-fix-loop-hacker.webp new file mode 100644 index 0000000..407e765 Binary files /dev/null and b/apps/web/public/aura-icons/icon-fix-loop-hacker.webp differ diff --git a/apps/web/public/aura-icons/icon-guardrailed-viber.webp b/apps/web/public/aura-icons/icon-guardrailed-viber.webp new file mode 100644 index 0000000..384f9ec Binary files /dev/null and b/apps/web/public/aura-icons/icon-guardrailed-viber.webp differ diff --git a/apps/web/public/aura-icons/icon-infra-weaver.webp b/apps/web/public/aura-icons/icon-infra-weaver.webp new file mode 100644 index 0000000..17a0b84 Binary files /dev/null and b/apps/web/public/aura-icons/icon-infra-weaver.webp differ diff --git a/apps/web/public/aura-icons/icon-methodical-architect.webp b/apps/web/public/aura-icons/icon-methodical-architect.webp new file mode 100644 index 0000000..2be377a Binary files /dev/null and b/apps/web/public/aura-icons/icon-methodical-architect.webp differ diff --git a/apps/web/public/aura-icons/icon-orchestrator.webp b/apps/web/public/aura-icons/icon-orchestrator.webp new file mode 100644 index 0000000..6a11291 Binary files /dev/null and b/apps/web/public/aura-icons/icon-orchestrator.webp differ diff --git a/apps/web/public/aura-icons/icon-prompt-sprinter.webp b/apps/web/public/aura-icons/icon-prompt-sprinter.webp new file mode 100644 index 0000000..4d51277 Binary files /dev/null and b/apps/web/public/aura-icons/icon-prompt-sprinter.webp differ diff --git a/apps/web/public/aura-icons/icon-rapid-risk-taker.webp b/apps/web/public/aura-icons/icon-rapid-risk-taker.webp new file mode 100644 index 0000000..142e160 Binary files /dev/null and b/apps/web/public/aura-icons/icon-rapid-risk-taker.webp differ diff --git a/apps/web/public/aura-icons/icon-reflective-balancer.webp b/apps/web/public/aura-icons/icon-reflective-balancer.webp new file mode 100644 index 0000000..8011b1f Binary files /dev/null and b/apps/web/public/aura-icons/icon-reflective-balancer.webp differ diff --git a/apps/web/public/aura-icons/icon-spec-first-director.webp b/apps/web/public/aura-icons/icon-spec-first-director.webp new file mode 100644 index 0000000..7c62dd3 Binary files /dev/null and b/apps/web/public/aura-icons/icon-spec-first-director.webp differ diff --git a/apps/web/public/aura-icons/icon-toolsmith-viber.webp b/apps/web/public/aura-icons/icon-toolsmith-viber.webp new file mode 100644 index 0000000..64a3444 Binary files /dev/null and b/apps/web/public/aura-icons/icon-toolsmith-viber.webp differ diff --git a/apps/web/public/aura-icons/icon-vertical-slice-shipper.webp b/apps/web/public/aura-icons/icon-vertical-slice-shipper.webp new file mode 100644 index 0000000..595a22a Binary files /dev/null and b/apps/web/public/aura-icons/icon-vertical-slice-shipper.webp differ diff --git a/apps/web/public/auras/aura-guardrailed-viber.png b/apps/web/public/auras/aura-guardrailed-viber.png new file mode 100644 index 0000000..e69f47c Binary files /dev/null and b/apps/web/public/auras/aura-guardrailed-viber.png differ diff --git a/apps/web/public/auras/aura-methodical-architect.png b/apps/web/public/auras/aura-methodical-architect.png new file mode 100644 index 0000000..27e3e99 Binary files /dev/null and b/apps/web/public/auras/aura-methodical-architect.png differ diff --git a/apps/web/public/auras/aura-prompt-sprinter.png b/apps/web/public/auras/aura-prompt-sprinter.png new file mode 100644 index 0000000..f2d5b8c Binary files /dev/null and b/apps/web/public/auras/aura-prompt-sprinter.png differ diff --git a/apps/web/public/auras/aura-rapid-risk-taker.png b/apps/web/public/auras/aura-rapid-risk-taker.png new file mode 100644 index 0000000..e09ea7a Binary files /dev/null and b/apps/web/public/auras/aura-rapid-risk-taker.png differ diff --git a/apps/web/public/auras/aura-reflective-balancer.png b/apps/web/public/auras/aura-reflective-balancer.png new file mode 100644 index 0000000..7ec00b4 Binary files /dev/null and b/apps/web/public/auras/aura-reflective-balancer.png differ diff --git a/apps/web/public/auras/aura-vertical-slice-shipper.png b/apps/web/public/auras/aura-vertical-slice-shipper.png new file mode 100644 index 0000000..dbe7422 Binary files /dev/null and b/apps/web/public/auras/aura-vertical-slice-shipper.png differ diff --git a/apps/web/src/app/AppHeader.tsx b/apps/web/src/app/AppHeader.tsx index 271f669..c00c31c 100644 --- a/apps/web/src/app/AppHeader.tsx +++ b/apps/web/src/app/AppHeader.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { wrappedTheme } from "@/lib/theme"; -import { useJobsOptional } from "@/contexts/JobsContext"; +import { NotificationDropdown } from "@/components/notifications"; function isActiveLink(pathname: string, href: string): boolean { if (href === "/") return pathname === "/"; @@ -18,17 +18,14 @@ export default function AppHeader(props: { signOut: () => Promise; }) { const pathname = usePathname(); - const jobsContext = useJobsOptional(); - const unreadCount = jobsContext?.unreadCount ?? 0; if (pathname === "/login") return null; const baseLinks = props.isAuthed ? [ { href: "/", label: "My VCP" }, - { href: "/repos", label: "Repos" }, - { href: "/analysis", label: "Reports" }, - { href: "/settings/llm-keys", label: "Settings" }, + { href: "/vibes", label: "Repo VCPs" }, + { href: "/settings/repos", label: "Settings" }, { href: "/methodology", label: "Methodology" }, { href: "/security", label: "Security" }, ] @@ -44,8 +41,8 @@ export default function AppHeader(props: { return (
-
-
+
+
{links.map((l) => { const isActive = isActiveLink(pathname, l.href); - const showBadge = l.href === "/analysis" && unreadCount > 0; return ( {isActive && ( - + )} - - {l.label} - {showBadge && ( - - {unreadCount > 9 ? "9+" : unreadCount} - - )} - + {l.label} ); })} @@ -89,18 +78,21 @@ export default function AppHeader(props: {
{props.isAuthed ? ( -
- -
+ <> + +
+ +
+ ) : ( Sign in diff --git a/apps/web/src/app/admin/llm-status/page.tsx b/apps/web/src/app/admin/llm-status/page.tsx index 4aed6e3..74e036e 100644 --- a/apps/web/src/app/admin/llm-status/page.tsx +++ b/apps/web/src/app/admin/llm-status/page.tsx @@ -221,7 +221,7 @@ export default async function AdminLLMStatusPage() {
diff --git a/apps/web/src/app/admin/users/[userId]/page.tsx b/apps/web/src/app/admin/users/[userId]/page.tsx index 475ba21..fdd526c 100644 --- a/apps/web/src/app/admin/users/[userId]/page.tsx +++ b/apps/web/src/app/admin/users/[userId]/page.tsx @@ -128,7 +128,7 @@ export default async function AdminUserDetailPage({
diff --git a/apps/web/src/app/analysis/AnalysisListClient.tsx b/apps/web/src/app/analysis/AnalysisListClient.tsx index 951bd55..05e6da7 100644 --- a/apps/web/src/app/analysis/AnalysisListClient.tsx +++ b/apps/web/src/app/analysis/AnalysisListClient.tsx @@ -157,7 +157,7 @@ export default function AnalysisListClient({ initialReports, initialJobs }: Anal

No completed reports yet. Run your first vibe check.

- + Pick a repo
@@ -181,7 +181,7 @@ export default function AnalysisListClient({ initialReports, initialJobs }: Anal {r.repoName ?? "Repository"}

{isUnread && ( - + )}

@@ -243,7 +243,7 @@ export default function AnalysisListClient({ initialReports, initialJobs }: Anal

No analysis jobs yet.

- + Start an analysis
diff --git a/apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx b/apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx index 83305f8..6cfbc93 100644 --- a/apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx +++ b/apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx @@ -5,8 +5,16 @@ import { useEffect, useMemo, useState } from "react"; import { computeAnalysisInsights } from "@vibed/core"; import type { AnalysisInsights, AnalysisMetrics, CommitEvent } from "@vibed/core"; import { formatMetricLabel, formatMetricValue } from "@/lib/format-labels"; +import { computeShareCardMetrics } from "@/lib/vcp/metrics"; +import { isVibeAxes } from "@/lib/vcp/validators"; import { ShareCard, ShareActions } from "@/components/share"; import type { ShareImageTemplate, ShareCardMetric } from "@/components/share"; +import { + RepoIdentitySection, + RepoAxesSection, + RepoMetricsGrid, + ProfileContributionCard, +} from "@/components/vcp/repo"; type Job = { id: string; @@ -25,6 +33,8 @@ type ApiResponse = { insights: unknown | null; profileContribution?: unknown | null; userAvatarUrl?: string | null; + userId?: string | null; + vibeInsights?: VibeInsightsRow | null; }; type StoryMeta = { @@ -86,6 +96,15 @@ type InsightsRow = { generated_at?: string; }; +type VibeInsightsRow = { + axes_json: unknown; + persona_id: string; + persona_name: string; + persona_tagline: string | null; + persona_confidence: string; + persona_score: number | null; +}; + type InsightHistoryEntry = { job_id: string; status: string; @@ -164,6 +183,18 @@ function isNarrativeJson(v: unknown): v is NarrativeJson { return true; } +function isVibeInsightsRow(v: unknown): v is VibeInsightsRow { + if (!isRecord(v)) return false; + if (!isVibeAxes(v.axes_json)) return false; + if (typeof v.persona_id !== "string") return false; + if (typeof v.persona_name !== "string") return false; + if (typeof v.persona_confidence !== "string") return false; + const tagline = v.persona_tagline; + if (tagline !== null && typeof tagline !== "string") return false; + if (typeof v.persona_score !== "number" && v.persona_score !== null) return false; + return true; +} + function isReportRow(v: unknown): v is ReportRow { if (!isRecord(v)) return false; const bt = v.vibe_type; @@ -349,10 +380,6 @@ function fmtDate(iso: string | null): string { } -function weekdayName(dow: number): string { - return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][dow] ?? "—"; -} - export default function AnalysisClient({ jobId }: { jobId: string }) { const [data, setData] = useState(null); const [error, setError] = useState(null); @@ -409,6 +436,7 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { const parsedReport = data && isReportRow(data.report) ? data.report : null; const parsedMetrics = data && isMetricsRow(data.metrics) ? data.metrics : null; const parsedInsightsRow = data && isInsightsRow(data.insights) ? data.insights : null; + const parsedVibeInsights = data && isVibeInsightsRow(data.vibeInsights) ? data.vibeInsights : null; const profileContribution = data && isProfileContribution(data.profileContribution) ? data.profileContribution : null; @@ -424,19 +452,14 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { const persona = wrapped.persona; const shareTemplate = wrapped.share_template; - const profileContributionLabel = (() => { - if (!profileContribution) return null; - if (profileContribution.includedInProfile === true) return "Included in your Unified VCP"; - if (profileContribution.includedInProfile === false) return "Not yet included in your Unified VCP"; - return "Unified VCP impact"; - })(); const shareText = useMemo(() => { if (!shareTemplate) return ""; const metricsLine = shareTemplate.metrics .map((metric) => `${metric.label}: ${metric.value}`) .join(" · "); - return `${shareTemplate.headline}\n${shareTemplate.subhead}\n${metricsLine}\n#VCP`; + const taglineLine = shareTemplate.tagline ?? shareTemplate.subhead; + return `${shareTemplate.headline}\n${taglineLine}\n${metricsLine}\n#VCP`; }, [shareTemplate]); const shareUrl = useMemo(() => { @@ -450,53 +473,61 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { .slice(0, 3) .map((metric) => `${metric.label}: ${metric.value}`) .join(" · "); - return `${shareTemplate.headline} — ${shareTemplate.subhead}\n${metricsLine}\n#VCP`; + const taglineLine = shareTemplate.tagline ?? shareTemplate.subhead; + return `${shareTemplate.headline} — ${taglineLine}\n${metricsLine}\n#VCP`; }, [shareTemplate]); + const storyEndpoint = data?.userId + ? `/api/share/story/${data.userId}?jobId=${jobId}` + : undefined; + // Build share template for ShareActions component + // Build metrics for ShareCard + const shareCardMetrics: ShareCardMetric[] = useMemo(() => { + if (parsedVibeInsights && isVibeAxes(parsedVibeInsights.axes_json)) { + const axes = parsedVibeInsights.axes_json; + const peakWindow = isAnalysisInsights(insightsJson) + ? insightsJson.timing.peak_window + : null; + const computed = computeShareCardMetrics(axes, peakWindow ?? null); + return [ + { label: "Strongest", value: computed.strongest }, + { label: "Style", value: computed.style }, + { label: "Rhythm", value: computed.rhythm }, + { label: "Peak", value: computed.peak }, + ]; + } + + const chunkinessLabel = wrapped.chunkiness.label + ? `${wrapped.chunkiness.label.charAt(0).toUpperCase()}${wrapped.chunkiness.label.slice(1)}` + : "Balanced"; + const peakWindowLabel = wrapped.timing.peak_window + ? formatMetricLabel(wrapped.timing.peak_window) + : "Varied"; + return [ + { label: "Strongest", value: persona?.label ?? "Vibe coder" }, + { label: "Style", value: chunkinessLabel }, + { label: "Rhythm", value: peakWindowLabel }, + { label: "Peak", value: peakWindowLabel }, + ]; + }, [persona, parsedVibeInsights, wrapped, insightsJson]); + + const repoAxes = + parsedVibeInsights && isVibeAxes(parsedVibeInsights.axes_json) + ? parsedVibeInsights.axes_json + : null; + const shareImageTemplate: ShareImageTemplate | null = shareTemplate ? { colors: shareTemplate.colors, headline: shareTemplate.headline, subhead: shareTemplate.subhead, - metrics: shareTemplate.metrics, + tagline: shareTemplate.tagline ?? shareTemplate.subhead, + metrics: shareCardMetrics, persona_archetype: shareTemplate.persona_archetype, } : null; - // Build metrics for ShareCard - const shareCardMetrics: ShareCardMetric[] = useMemo(() => { - const metrics: ShareCardMetric[] = []; - metrics.push({ - label: "Longest Streak", - value: `${wrapped.streak.longest_days} day${wrapped.streak.longest_days === 1 ? "" : "s"}`, - }); - if (wrapped.timing.peak_window) { - metrics.push({ - label: "Peak Window", - value: formatMetricLabel(wrapped.timing.peak_window), - }); - } - metrics.push({ - label: "Commit Style", - value: wrapped.chunkiness.label ?? "—", - }); - if (wrapped.commits.features_per_fix !== null) { - metrics.push({ - label: "Feature / Fix", - value: `${formatMetricValue(wrapped.commits.features_per_fix, 1)} : 1`, - }); - } else if (wrapped.commits.fixes_per_feature !== null) { - metrics.push({ - label: "Feature / Fix", - value: `1 : ${formatMetricValue(wrapped.commits.fixes_per_feature, 1)}`, - }); - } else { - metrics.push({ label: "Feature / Fix", value: "Balanced" }); - } - return metrics; - }, [wrapped]); - const triggerProfileRebuild = async () => { setRebuildingProfile(true); setProfileRebuildStatus("idle"); @@ -701,7 +732,7 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { +
+
+ )} + + + {/* Connected Repos Section */} +
+
+

+ Your Repos ({initialConnected.length}) +

+
+ {lastSyncedAt && ( + Synced {new Date(lastSyncedAt).toLocaleDateString()} + )} + +
+
+ + {error &&

{error}

} + + {initialConnected.length === 0 ? ( +

+ No repos connected yet. Add one below to start building your profile. +

+ ) : ( +
    + {initialConnected.map((r) => ( +
  • + {r.full_name} + +
  • + ))} +
+ )} +
+ + {/* Add Repo Section */} +
+

Connect a Repo

+ {repos === null ? ( +

Loading your repositories...

+ ) : repos.length === 0 ? ( +

No repositories found in your GitHub account.

+ ) : ( +
+ + + + + + + + + No matches. + + {repos.map((r) => { + const connectedRepoId = connectedByFullName.get(r.full_name); + const lastTouch = formatWhen(r.pushed_at ?? r.updated_at); + return ( + { + setSelectedFullName(r.full_name); + setIsPickerOpen(false); + }} + > + +
+
+ {r.full_name} + {connectedRepoId && ( + + Connected + + )} +
+

+ {r.private ? "Private" : "Public"} + {r.language ? ` · ${r.language}` : ""} + {lastTouch ? ` · ${lastTouch}` : ""} +

+
+
+ ); + })} +
+
+
+
+
+ + +
+ )} +
+
+ ); +} diff --git a/apps/web/src/app/settings/repos/page.tsx b/apps/web/src/app/settings/repos/page.tsx new file mode 100644 index 0000000..08b7cdc --- /dev/null +++ b/apps/web/src/app/settings/repos/page.tsx @@ -0,0 +1,75 @@ +import { redirect } from "next/navigation"; +import Link from "next/link"; +import { createSupabaseServerClient } from "@/lib/supabase/server"; +import { wrappedTheme } from "@/lib/theme"; +import RepoSettingsClient from "./RepoSettingsClient"; + +export const runtime = "nodejs"; + +export const metadata = { + title: "Repos · Settings · Vibe Coding Profile", + description: "Manage your connected GitHub repositories", +}; + +export default async function RepoSettingsPage() { + const supabase = await createSupabaseServerClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) redirect("/login"); + + const { data } = await supabase + .from("user_repos") + .select("repo_id, repos(full_name)") + .eq("user_id", user.id) + .is("disconnected_at", null); + + const rows = (data ?? []) as unknown as Array<{ + repo_id: string; + repos: { full_name: string } | null; + }>; + + const initialConnected = rows + .filter((r) => Boolean(r.repos?.full_name)) + .map((r) => ({ repo_id: r.repo_id, full_name: r.repos!.full_name })); + + return ( +
+
+ {/* Header */} +
+
+ + Settings + + / + Repos +
+

+ Connected Repos +

+

+ Manage the GitHub repositories connected to your Vibe Coding Profile. +

+
+ + {/* Settings Tabs */} +
+ + LLM Keys + + + Repos + +
+ + {/* Main Content */} + +
+
+ ); +} diff --git a/apps/web/src/app/vibes/VibesClient.tsx b/apps/web/src/app/vibes/VibesClient.tsx new file mode 100644 index 0000000..39ae9ee --- /dev/null +++ b/apps/web/src/app/vibes/VibesClient.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { ChevronRight, ChevronDown } from "lucide-react"; +import { wrappedTheme } from "@/lib/theme"; +import { toast } from "@/components/ui/use-toast"; +import { ToastAction } from "@/components/ui/toast"; + +type VibeVersion = { + jobId: string; + personaLabel: string | null; + personaConfidence: string | null; + commitCount: number | null; + generatedAt: string | null; +}; + +type RepoVibe = { + repoId: string; + repoName: string; + latestVibe: VibeVersion | null; + versions: VibeVersion[]; + isAnalyzed: boolean; +}; + +interface VibesClientProps { + repos: RepoVibe[]; +} + +function formatDate(dateStr: string | null): string { + if (!dateStr) return ""; + const date = new Date(dateStr); + return date.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }); +} + +export default function VibesClient({ repos }: VibesClientProps) { + const router = useRouter(); + const [expandedRepos, setExpandedRepos] = useState>(new Set()); + const [loadingRepoId, setLoadingRepoId] = useState(null); + + const toggleExpanded = (repoId: string) => { + setExpandedRepos((prev) => { + const next = new Set(prev); + if (next.has(repoId)) { + next.delete(repoId); + } else { + next.add(repoId); + } + return next; + }); + }; + + const startAnalysis = async (repoId: string, repoName: string) => { + setLoadingRepoId(repoId); + try { + const res = await fetch("/api/analysis/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ repo_id: repoId }), + }); + const data = (await res.json()) as { job_id?: string; error?: string }; + if (!res.ok || !data.job_id) { + throw new Error(data.error || "Failed to start analysis"); + } + toast({ + title: "Vibe check started", + description: repoName, + action: ( + router.push(`/analysis/${data.job_id}`)} + > + Open + + ), + }); + router.refresh(); + } catch (e) { + toast({ + variant: "destructive", + title: "Failed to start analysis", + description: e instanceof Error ? e.message : "Please try again", + }); + } finally { + setLoadingRepoId(null); + } + }; + + if (repos.length === 0) { + return ( +
+

No repos connected yet.

+

+ Connect a GitHub repository to discover your Vibe Coding Profile. +

+ + Connect a Repo + +
+ ); + } + + return ( +
+
+ {repos.map((repo) => { + const isExpanded = expandedRepos.has(repo.repoId); + const isLoading = loadingRepoId === repo.repoId; + + return ( +
+ {/* Main row */} +
+ {/* Expand button */} + + + {/* Repo info */} +
+

+ {repo.repoName} +

+ {repo.latestVibe ? ( +

+ {repo.latestVibe.personaLabel ?? "Analyzing..."} ·{" "} + {repo.versions.length} version + {repo.versions.length !== 1 ? "s" : ""} + {repo.latestVibe.commitCount + ? ` · ${repo.latestVibe.commitCount} commits` + : ""} +

+ ) : ( +

Not analyzed yet

+ )} +
+ + {/* Actions */} +
+ {repo.latestVibe ? ( + <> + + View + + + + ) : ( + + )} +
+
+ + {/* Expanded version history */} + {isExpanded && repo.versions.length > 0 && ( +
+ + + + + + + + + + + + {repo.versions.map((version, idx) => ( + + + + + + + + ))} + +
VersionDatePersonaConfidenceAction
v{repo.versions.length - idx}{formatDate(version.generatedAt)} + {version.personaLabel ?? "—"} + {version.personaConfidence ?? "—"} + + View + +
+
+ )} +
+ ); + })} +
+
+ ); +} diff --git a/apps/web/src/app/vibes/page.tsx b/apps/web/src/app/vibes/page.tsx new file mode 100644 index 0000000..3394343 --- /dev/null +++ b/apps/web/src/app/vibes/page.tsx @@ -0,0 +1,177 @@ +import { redirect } from "next/navigation"; +import Link from "next/link"; +import { createSupabaseServerClient } from "@/lib/supabase/server"; +import { wrappedTheme } from "@/lib/theme"; +import VibesClient from "./VibesClient"; + +export const runtime = "nodejs"; + +export const metadata = { + title: "Repo VCPs · Vibe Coding Profile", + description: "View your Vibe Coding Profiles organized by repository", +}; + +type JobRow = { + id: string; + repo_id: string; + status: string; + created_at: string; +}; + +type InsightRow = { + job_id: string; + persona_label: string | null; + persona_confidence: string | null; + generated_at: string | null; +}; + +type MetricsRow = { + job_id: string; + commit_count: number | null; +}; + +export default async function VibesPage() { + const supabase = await createSupabaseServerClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) redirect("/login"); + + // Fetch connected repos + const { data: userReposData } = await supabase + .from("user_repos") + .select("repo_id, repos(id, full_name)") + .eq("user_id", user.id) + .is("disconnected_at", null); + + const userRepos = (userReposData ?? []) as unknown as Array<{ + repo_id: string; + repos: { id: string; full_name: string } | null; + }>; + + const connectedRepos = userRepos + .filter((r) => r.repos?.full_name) + .map((r) => ({ + repoId: r.repo_id, + repoName: r.repos!.full_name, + })); + + const repoIds = connectedRepos.map((r) => r.repoId); + + // Fetch all completed jobs for these repos + const { data: jobsData } = + repoIds.length > 0 + ? await supabase + .from("analysis_jobs") + .select("id, repo_id, status, created_at") + .eq("user_id", user.id) + .eq("status", "done") + .in("repo_id", repoIds) + .order("created_at", { ascending: false }) + : { data: [] }; + + const jobs = (jobsData ?? []) as JobRow[]; + const jobIds = jobs.map((j) => j.id); + + // Fetch insights for completed jobs + const { data: insightsData } = + jobIds.length > 0 + ? await supabase + .from("analysis_insights") + .select("job_id, persona_label, persona_confidence, generated_at") + .in("job_id", jobIds) + : { data: [] }; + + const insightByJobId = new Map(); + for (const row of (insightsData ?? []) as InsightRow[]) { + insightByJobId.set(row.job_id, row); + } + + // Fetch metrics for commit counts + const { data: metricsData } = + jobIds.length > 0 + ? await supabase + .from("analysis_metrics") + .select("job_id, commit_count") + .in("job_id", jobIds) + : { data: [] }; + + const metricsByJobId = new Map(); + for (const row of (metricsData ?? []) as MetricsRow[]) { + metricsByJobId.set(row.job_id, row); + } + + // Group jobs by repo + const jobsByRepoId = new Map(); + for (const job of jobs) { + const existing = jobsByRepoId.get(job.repo_id) ?? []; + existing.push(job); + jobsByRepoId.set(job.repo_id, existing); + } + + // Build the repos data structure + const repos = connectedRepos.map((repo) => { + const repoJobs = jobsByRepoId.get(repo.repoId) ?? []; + const versions = repoJobs.map((job) => { + const insight = insightByJobId.get(job.id); + const metrics = metricsByJobId.get(job.id); + return { + jobId: job.id, + personaLabel: insight?.persona_label ?? null, + personaConfidence: insight?.persona_confidence ?? null, + commitCount: metrics?.commit_count ?? null, + generatedAt: insight?.generated_at ?? job.created_at, + }; + }); + + return { + repoId: repo.repoId, + repoName: repo.repoName, + latestVibe: versions[0] ?? null, + versions, + isAnalyzed: versions.length > 0, + }; + }); + + // Sort: analyzed repos first, then by latest analysis date + repos.sort((a, b) => { + if (a.isAnalyzed && !b.isAnalyzed) return -1; + if (!a.isAnalyzed && b.isAnalyzed) return 1; + if (a.latestVibe && b.latestVibe) { + return ( + new Date(b.latestVibe.generatedAt ?? 0).getTime() - + new Date(a.latestVibe.generatedAt ?? 0).getTime() + ); + } + return a.repoName.localeCompare(b.repoName); + }); + + return ( +
+
+
+
+

+ Repo VCPs +

+

+ Vibe Coding Profiles by Repo +

+

+ Your VCPs organized by repository. Expand to see version history. +

+
+ + + Add Repo + +
+ + +
+
+ ); +} diff --git a/apps/web/src/components/notifications/NotificationBadge.tsx b/apps/web/src/components/notifications/NotificationBadge.tsx new file mode 100644 index 0000000..b843962 --- /dev/null +++ b/apps/web/src/components/notifications/NotificationBadge.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +interface NotificationBadgeProps { + /** Number of unread notifications */ + count: number; + /** Maximum count to display before showing "9+" */ + maxCount?: number; + /** Size variant */ + size?: "sm" | "md"; + /** Optional additional classes */ + className?: string; +} + +/** + * NotificationBadge - Displays unread notification count + * + * Shows a gradient pill with the count, or "9+" if over max. + * Returns null if count is 0. + */ +export function NotificationBadge({ + count, + maxCount = 9, + size = "md", + className, +}: NotificationBadgeProps) { + if (count <= 0) return null; + + const displayCount = count > maxCount ? `${maxCount}+` : count.toString(); + + const sizeClasses = { + sm: "h-4 min-w-4 px-1 text-[9px]", + md: "h-5 min-w-5 px-1.5 text-[10px]", + }; + + return ( + + {displayCount} + + ); +} diff --git a/apps/web/src/components/notifications/NotificationDropdown.tsx b/apps/web/src/components/notifications/NotificationDropdown.tsx new file mode 100644 index 0000000..5b3e157 --- /dev/null +++ b/apps/web/src/components/notifications/NotificationDropdown.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { useJobs } from "@/contexts/JobsContext"; +import { NotificationBadge } from "./NotificationBadge"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +/** + * NotificationDropdown - Bell icon with dropdown showing job activity + * + * Shows in-progress and completed jobs with unread indicators. + * Clicking a job navigates to its VCP and marks it as read. + */ +export function NotificationDropdown() { + const { jobs, unreadReportIds, unreadCount, markReportAsRead, markAllAsRead, isPolling } = useJobs(); + const [isOpen, setIsOpen] = useState(false); + + // Sort jobs: in-progress first, then by date + const sortedJobs = [...jobs].sort((a, b) => { + const aActive = a.status === "pending" || a.status === "running"; + const bActive = b.status === "pending" || b.status === "running"; + if (aActive && !bActive) return -1; + if (!aActive && bActive) return 1; + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); + + // Limit to recent 10 jobs + const recentJobs = sortedJobs.slice(0, 10); + + const formatTimeAgo = (dateStr: string) => { + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return "just now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "pending": + case "running": + return ( + + + + + ); + case "done": + return ( + + + + ); + case "failed": + return ( + + + + ); + default: + return null; + } + }; + + const handleJobClick = (jobId: string) => { + markReportAsRead(jobId); + setIsOpen(false); + }; + + return ( + + + + + + + {/* Header */} +
+
+

Activity

+ {isPolling && ( + + + + )} +
+ {unreadCount > 0 && ( + + )} +
+ + {/* Jobs list */} +
+ {recentJobs.length === 0 ? ( +
+ No recent activity +
+ ) : ( +
    + {recentJobs.map((job) => { + const isUnread = unreadReportIds.has(job.id); + const isActive = job.status === "pending" || job.status === "running"; + const isDone = job.status === "done"; + const isFailed = job.status === "failed"; + + return ( +
  • + {isDone ? ( + handleJobClick(job.id)} + className={cn( + "flex items-start gap-3 px-4 py-3 transition-colors hover:bg-zinc-50", + isUnread && "bg-violet-50/50" + )} + > +
    + {getStatusIcon(job.status)} +
    +
    +
    +

    + {job.repoName ?? "Repository"} +

    + {isUnread && ( + + )} +
    +

    + Analysis complete · {formatTimeAgo(job.completedAt ?? job.createdAt)} +

    +
    + + + + + ) : ( +
    +
    + {getStatusIcon(job.status)} +
    +
    +

    + {job.repoName ?? "Repository"} +

    +

    + {isActive ? "Analyzing..." : isFailed ? "Failed" : job.status} + {isActive && " · Started " + formatTimeAgo(job.startedAt ?? job.createdAt)} + {isFailed && job.errorMessage && ( + {job.errorMessage} + )} +

    +
    +
    + )} +
  • + ); + })} +
+ )} +
+ + {/* Footer */} + {recentJobs.length > 0 && ( +
+ setIsOpen(false)} + className="block text-center text-xs font-medium text-zinc-500 hover:text-zinc-700" + > + View Repo VCPs → + +
+ )} +
+
+ ); +} diff --git a/apps/web/src/components/notifications/index.ts b/apps/web/src/components/notifications/index.ts new file mode 100644 index 0000000..5621732 --- /dev/null +++ b/apps/web/src/components/notifications/index.ts @@ -0,0 +1,8 @@ +/** + * Notification Components + * + * Components for displaying job activity and unread notifications. + */ + +export { NotificationBadge } from "./NotificationBadge"; +export { NotificationDropdown } from "./NotificationDropdown"; diff --git a/apps/web/src/components/share/ProfileShareSection.tsx b/apps/web/src/components/share/ProfileShareSection.tsx index 028bc14..cd34dee 100644 --- a/apps/web/src/components/share/ProfileShareSection.tsx +++ b/apps/web/src/components/share/ProfileShareSection.tsx @@ -1,6 +1,8 @@ "use client"; import { useMemo, useSyncExternalStore } from "react"; +import { computeShareCardMetrics } from "@/lib/vcp/metrics"; +import type { VibeAxes } from "@vibed/core"; import { ShareCard, ShareActions } from "./index"; import type { ShareCardMetric, ShareImageTemplate } from "./types"; @@ -13,26 +15,38 @@ function useOrigin(): string { ); } -// Default persona colors for profile share cards +/** + * Persona colors for profile share cards + * + * All personas now use the unified violet-indigo palette with subtle variations + * to maintain brand consistency while still providing visual differentiation. + */ const PERSONA_COLORS: Record = { - "prompt_sprinter": { primary: "#c026d3", accent: "#7c3aed" }, - "guardrailed_viber": { primary: "#0891b2", accent: "#4f46e5" }, - "spec_first_director": { primary: "#4f46e5", accent: "#7c3aed" }, - "vertical_slice_shipper": { primary: "#7c3aed", accent: "#0ea5e9" }, - "fix_loop_hacker": { primary: "#dc2626", accent: "#f97316" }, - "toolsmith_viber": { primary: "#059669", accent: "#0891b2" }, - "infra_weaver": { primary: "#475569", accent: "#0891b2" }, - "rapid_risk_taker": { primary: "#f97316", accent: "#dc2626" }, - "balanced_builder": { primary: "#6366f1", accent: "#8b5cf6" }, - // Fallback for older persona IDs - "vibe-prototyper": { primary: "#c026d3", accent: "#7c3aed" }, - "test-validator": { primary: "#0891b2", accent: "#4f46e5" }, + // Dynamic/fast personas - brighter violet + "prompt_sprinter": { primary: "#7c3aed", accent: "#6366f1" }, // violet-600 → indigo-500 + "rapid_risk_taker": { primary: "#8b5cf6", accent: "#6366f1" }, // violet-500 → indigo-500 + "fix_loop_hacker": { primary: "#7c3aed", accent: "#818cf8" }, // violet-600 → indigo-400 + + // Balanced/thoughtful personas - deeper tones + "guardrailed_viber": { primary: "#6366f1", accent: "#7c3aed" }, // indigo-500 → violet-600 + "spec_first_director": { primary: "#4f46e5", accent: "#7c3aed" }, // indigo-600 → violet-600 + "balanced_builder": { primary: "#6366f1", accent: "#8b5cf6" }, // indigo-500 → violet-500 + + // Technical/structured personas - cooler indigo + "vertical_slice_shipper": { primary: "#6366f1", accent: "#818cf8" }, // indigo-500 → indigo-400 + "toolsmith_viber": { primary: "#4f46e5", accent: "#6366f1" }, // indigo-600 → indigo-500 + "infra_weaver": { primary: "#4338ca", accent: "#6366f1" }, // indigo-700 → indigo-500 + + // Fallback for older persona IDs - all mapped to unified palette + "vibe-prototyper": { primary: "#7c3aed", accent: "#6366f1" }, + "test-validator": { primary: "#6366f1", accent: "#7c3aed" }, "spec-architect": { primary: "#4f46e5", accent: "#7c3aed" }, - "agent-orchestrator": { primary: "#7c3aed", accent: "#0ea5e9" }, + "agent-orchestrator": { primary: "#6366f1", accent: "#818cf8" }, "reflective-balancer": { primary: "#6366f1", accent: "#8b5cf6" }, }; -const DEFAULT_COLORS = { primary: "#c026d3", accent: "#06b6d4" }; +// Default uses the primary brand gradient +const DEFAULT_COLORS = { primary: "#7c3aed", accent: "#6366f1" }; interface ProfileShareSectionProps { personaName: string; @@ -47,6 +61,8 @@ interface ProfileShareSectionProps { score: number; }>; insight: string; + axes: VibeAxes; + userId: string; } export function ProfileShareSection({ @@ -59,63 +75,70 @@ export function ProfileShareSection({ clarity, topAxes, insight, + axes, + userId, }: ProfileShareSectionProps) { const shareOrigin = useOrigin(); const colors = PERSONA_COLORS[personaId ?? ""] ?? DEFAULT_COLORS; const shareCardMetrics: ShareCardMetric[] = useMemo(() => { - const metrics: ShareCardMetric[] = [ - { label: "Repos", value: String(totalRepos) }, - { label: "Commits", value: totalCommits.toLocaleString() }, - { label: "Clarity", value: `${clarity}%` }, + const computed = computeShareCardMetrics(axes); + return [ + { label: "Strongest", value: computed.strongest }, + { label: "Style", value: computed.style }, + { label: "Rhythm", value: computed.rhythm }, + { label: "Peak", value: computed.peak }, ]; - // Add top axis if available - if (topAxes.length > 0) { - metrics.push({ label: topAxes[0].name, value: String(topAxes[0].score) }); - } - return metrics; - }, [totalRepos, totalCommits, clarity, topAxes]); + }, [axes]); const shareUrl = useMemo(() => { if (!shareOrigin) return ""; return shareOrigin; // Profile is at root }, [shareOrigin]); + const shareTagline = insight ?? personaTagline ?? ""; + const shareText = useMemo(() => { const metricsLine = `${totalRepos} repos · ${totalCommits.toLocaleString()} commits · ${clarity}% clarity`; - return `My Unified VCP: ${personaName}\n${personaTagline ?? ""}\n${metricsLine}\n#VCP`; - }, [personaName, personaTagline, totalRepos, totalCommits, clarity]); + return `My Unified VCP: ${personaName}\n${shareTagline}\n${metricsLine}\n#VCP`; + }, [personaName, shareTagline, totalRepos, totalCommits, clarity]); const shareCaption = useMemo(() => { - return `My Unified VCP: ${personaName} — ${personaTagline ?? ""}. ${totalRepos} repos, ${totalCommits.toLocaleString()} commits. #VCP`; - }, [personaName, personaTagline, totalRepos, totalCommits]); + const taglineSegment = shareTagline ? ` — ${shareTagline}` : ""; + return `My Unified VCP: ${personaName}${taglineSegment}. ${totalRepos} repos, ${totalCommits.toLocaleString()} commits. #VCP`; + }, [personaName, shareTagline, totalRepos, totalCommits]); const shareImageTemplate: ShareImageTemplate = useMemo(() => { return { colors, headline: `Unified VCP: ${personaName}`, subhead: personaTagline ?? `${personaConfidence} confidence`, + tagline: shareTagline || `${personaConfidence} confidence`, metrics: shareCardMetrics, persona_archetype: { label: personaName, archetypes: topAxes.slice(0, 3).map((a) => `${a.name}: ${a.score}`), }, }; - }, [personaName, personaTagline, personaConfidence, colors, shareCardMetrics, topAxes]); + }, [personaName, personaTagline, personaConfidence, colors, shareCardMetrics, topAxes, shareTagline]); return (
); diff --git a/apps/web/src/components/share/ShareActions.tsx b/apps/web/src/components/share/ShareActions.tsx index 9455783..7f72e35 100644 --- a/apps/web/src/components/share/ShareActions.tsx +++ b/apps/web/src/components/share/ShareActions.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Copy, Check, Link2, Share2, Download } from "lucide-react"; -import { SHARE_FORMATS, downloadSharePng, downloadShareSvg } from "./share-image"; +import { SHARE_FORMATS, downloadSharePng, downloadShareSvg, downloadBlob } from "./share-image"; import type { ShareActionsProps, ShareFormat } from "./types"; // Brand icons (Lucide doesn't include these) @@ -47,6 +47,7 @@ export function ShareActions({ shareTemplate, entityId, disabled = false, + storyEndpoint, }: ShareActionsProps) { const [copied, setCopied] = useState(false); const [copiedLink, setCopiedLink] = useState(false); @@ -54,6 +55,7 @@ export function ShareActions({ const [downloadError, setDownloadError] = useState(null); const [downloading, setDownloading] = useState(false); const [supportsNativeShare, setSupportsNativeShare] = useState(false); + const [storyDownloading, setStoryDownloading] = useState(false); useEffect(() => { setSupportsNativeShare(typeof navigator !== "undefined" && "share" in navigator); @@ -155,6 +157,22 @@ export function ShareActions({ downloadShareSvg(shareTemplate, shareFormat, entityId); }; + const handleDownloadStory = async () => { + if (!storyEndpoint) return; + setDownloadError(null); + setStoryDownloading(true); + try { + const res = await fetch(storyEndpoint, { cache: "no-store" }); + if (!res.ok) throw new Error("story_failed"); + const blob = await res.blob(); + downloadBlob(blob, `${entityId}-story.png`); + } catch (e) { + setDownloadError(e instanceof Error ? e.message : "story_download_failed"); + } finally { + setStoryDownloading(false); + } + }; + const isDisabled = disabled || !shareUrl; return ( @@ -193,6 +211,18 @@ export function ShareActions({ SVG + {storyEndpoint ? ( + + ) : null}
) : null} diff --git a/apps/web/src/components/share/ShareCard.tsx b/apps/web/src/components/share/ShareCard.tsx index c9e38cb..a1e16a7 100644 --- a/apps/web/src/components/share/ShareCard.tsx +++ b/apps/web/src/components/share/ShareCard.tsx @@ -1,6 +1,7 @@ "use client"; import Image from "next/image"; +import { getPersonaAura } from "@/lib/persona-auras"; import type { ShareCardProps } from "./types"; /** @@ -11,24 +12,40 @@ import type { ShareCardProps } from "./types"; */ export function ShareCard({ variant, + personaId, persona, metrics, footer, colors, avatarUrl, headerLabel, + tagline, }: ShareCardProps) { const defaultHeader = variant === "profile" ? "My Unified VCP" : "My Repo VCP"; const header = headerLabel ?? defaultHeader; + const aura = getPersonaAura(personaId); return (
-
+ {/* Aura background overlay */} +
+ + {/* Readability overlay */} +
+
+ +
{/* Header */}
@@ -47,7 +64,7 @@ export function ShareCard({

) : null}
-
+
{avatarUrl ? ( - ) : ( -
- V -
- )} + ) : null} +
+ {aura.alt} +
+
+ {tagline ? ( +
+ {tagline} +
+ ) : null} + {/* Metrics Grid */} -
+
{metrics.slice(0, 4).map((metric, idx) => (
-

+

{metric.label}

-

+

{metric.value}

+ {metric.detail ? ( +

+ {metric.detail} +

+ ) : null}
))}
diff --git a/apps/web/src/components/share/share-image.ts b/apps/web/src/components/share/share-image.ts index 48ac996..a2b7aad 100644 --- a/apps/web/src/components/share/share-image.ts +++ b/apps/web/src/components/share/share-image.ts @@ -114,14 +114,15 @@ export function createShareSvg( const metricsChars = format === "og" ? 72 : format === "square" ? 60 : 46; const headlineWrapped = wrapTextLines(template.headline, headlineChars, 2); - const subheadWrapped = wrapTextLines(template.subhead, subheadChars, 2); + const taglineText = template.tagline ?? template.subhead; + const taglineWrapped = wrapTextLines(taglineText, subheadChars, 2); const metricsWrapped = wrapTextLines(metricsText, metricsChars, format === "story" ? 2 : 1); const headlineLines = headlineWrapped.lines.map((l, idx) => escapeXml(idx === headlineWrapped.lines.length - 1 && headlineWrapped.truncated ? withEllipsis(l) : l) ); - const subheadLines = subheadWrapped.lines.map((l, idx) => - escapeXml(idx === subheadWrapped.lines.length - 1 && subheadWrapped.truncated ? withEllipsis(l) : l) + const taglineLines = taglineWrapped.lines.map((l, idx) => + escapeXml(idx === taglineWrapped.lines.length - 1 && taglineWrapped.truncated ? withEllipsis(l) : l) ); const metricsLines = metricsWrapped.lines.map((l, idx) => escapeXml(idx === metricsWrapped.lines.length - 1 && metricsWrapped.truncated ? withEllipsis(l) : l) @@ -131,8 +132,8 @@ export function createShareSvg( format === "story" ? Math.round(cfg.height * 0.34) : cfg.pad + Math.round(cfg.headlineSize * 0.9); const headlineY = startY; - const subheadY = headlineY + Math.round(cfg.headlineSize * 0.95) + 18; - const metricsY = subheadY + Math.round(cfg.subheadSize * 0.95) + 18; + const taglineY = headlineY + Math.round(cfg.headlineSize * 0.95) + 18; + const metricsY = taglineY + Math.round(cfg.subheadSize * 0.95) + 18; const metaY = format === "story" ? Math.round(cfg.height * 0.72) : metricsY + Math.round(cfg.metricsSize * 0.95) + 28; const hashY = metaY + Math.round(cfg.metaSize * 1.6); @@ -144,7 +145,7 @@ export function createShareSvg( const headlineTspans = headlineLines .map((line, idx) => `${line}`) .join(""); - const subheadTspans = subheadLines + const taglineTspans = taglineLines .map((line, idx) => `${line}`) .join(""); const metricsTspans = metricsLines @@ -163,8 +164,8 @@ export function createShareSvg( ${headlineTspans} - - ${subheadTspans} + + ${taglineTspans} ${metricsTspans} diff --git a/apps/web/src/components/share/types.ts b/apps/web/src/components/share/types.ts index df930c6..ca507bb 100644 --- a/apps/web/src/components/share/types.ts +++ b/apps/web/src/components/share/types.ts @@ -5,6 +5,7 @@ export interface ShareCardMetric { label: string; value: string; + detail?: string; } export interface ShareCardColors { @@ -22,6 +23,8 @@ export interface ShareCardPersona { export interface ShareCardProps { /** Variant determines layout differences */ variant: "repo" | "profile"; + /** Optional persona ID for aura background + icon */ + personaId?: string; /** Persona information */ persona: ShareCardPersona; /** Up to 4 metrics to display */ @@ -37,6 +40,8 @@ export interface ShareCardProps { avatarUrl?: string | null; /** Optional header label override */ headerLabel?: string; + /** Optional tagline row (placed between header and metrics) */ + tagline?: string | null; } export interface ShareActionsProps { @@ -54,12 +59,15 @@ export interface ShareActionsProps { entityId: string; /** Whether share is disabled */ disabled?: boolean; + /** Optional story download endpoint */ + storyEndpoint?: string; } export interface ShareImageTemplate { colors: ShareCardColors; headline: string; subhead: string; + tagline?: string | null; metrics: ShareCardMetric[]; persona_archetype: { label: string; diff --git a/apps/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..a4e90d4 --- /dev/null +++ b/apps/web/src/components/ui/tooltip.tsx @@ -0,0 +1,61 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/apps/web/src/components/vcp/AxisInfoTooltip.tsx b/apps/web/src/components/vcp/AxisInfoTooltip.tsx new file mode 100644 index 0000000..dbf9c14 --- /dev/null +++ b/apps/web/src/components/vcp/AxisInfoTooltip.tsx @@ -0,0 +1,49 @@ +import { Info } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import type { AxisMetadata } from "./constants"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + +type AxisInfoTooltipVariant = "light" | "dark"; + +interface AxisInfoTooltipProps { + meta: AxisMetadata; + variant?: AxisInfoTooltipVariant; + className?: string; +} + +const VARIANT_STYLES: Record = { + light: "text-zinc-400 hover:text-zinc-600", + dark: "text-white/40 hover:text-white/70", +}; + +export function AxisInfoTooltip({ + meta, + variant = "light", + className, +}: AxisInfoTooltipProps) { + return ( + + + + + +

{meta.name}

+

{meta.longDescription}

+

+ Score is 0-100. Lower means {meta.lowLabel}, higher means {meta.highLabel}. +

+
+
+ ); +} diff --git a/apps/web/src/components/vcp/blocks/VCPAxesGrid.tsx b/apps/web/src/components/vcp/blocks/VCPAxesGrid.tsx new file mode 100644 index 0000000..3c7c21e --- /dev/null +++ b/apps/web/src/components/vcp/blocks/VCPAxesGrid.tsx @@ -0,0 +1,79 @@ +import { cn } from "@/lib/utils"; +import type { VCPAxesGridProps, AxisKey } from "../types"; +import { VCPProgressBar } from "../primitives"; +import { AXIS_METADATA, AXIS_ORDER } from "../constants"; +import { AxisInfoTooltip } from "../AxisInfoTooltip"; + +/** + * VCPAxesGrid - Grid display of all six vibe axes + * + * Shows axis name, score bar, and level for each axis. + */ +export function VCPAxesGrid({ + axes, + showDescriptions = false, + layout = "grid", + className, +}: VCPAxesGridProps) { + const axisEntries = AXIS_ORDER.map((key) => ({ + key, + ...axes[key], + meta: AXIS_METADATA[key], + })); + + if (layout === "list") { + return ( +
+ {axisEntries.map(({ key, score, level, meta }) => ( +
+
+
+ {meta.name} + +
+ {score} +
+ + {showDescriptions && ( +

{meta.description}

+ )} +
+ ))} +
+ ); + } + + return ( +
+ {axisEntries.map(({ key, score, level, meta }) => ( +
+
+
+ + {meta.name} + + +
+ {score} +
+ +
+ {meta.lowLabel} + {meta.highLabel} +
+ {showDescriptions && ( +

{meta.description}

+ )} +
+ ))} +
+ ); +} diff --git a/apps/web/src/components/vcp/blocks/VCPFooter.tsx b/apps/web/src/components/vcp/blocks/VCPFooter.tsx new file mode 100644 index 0000000..8a92c05 --- /dev/null +++ b/apps/web/src/components/vcp/blocks/VCPFooter.tsx @@ -0,0 +1,21 @@ +import { cn } from "@/lib/utils"; +import type { VCPFooterProps } from "../types"; + +/** + * VCPFooter - Card footer with left/right content + * + * Used for branding (vibed.dev) and context (repos/commits). + */ +export function VCPFooter({ left, right, className }: VCPFooterProps) { + return ( +
+
{left}
+
{right}
+
+ ); +} diff --git a/apps/web/src/components/vcp/blocks/VCPIdentityHeader.tsx b/apps/web/src/components/vcp/blocks/VCPIdentityHeader.tsx new file mode 100644 index 0000000..301d068 --- /dev/null +++ b/apps/web/src/components/vcp/blocks/VCPIdentityHeader.tsx @@ -0,0 +1,96 @@ +import Image from "next/image"; +import { cn } from "@/lib/utils"; +import type { VCPIdentityHeaderProps } from "../types"; +import { VCPBadge } from "../primitives"; +import { CONFIDENCE_LABELS, getPersonaIcon } from "../constants"; + +/** + * VCPIdentityHeader - Main identity display for VCP cards + * + * Shows persona name, tagline, confidence, and optional avatar/icon. + */ +export function VCPIdentityHeader({ + label, + persona, + statsLine, + avatarUrl, + personaIconUrl, + actions, + className, +}: VCPIdentityHeaderProps) { + const iconUrl = personaIconUrl ?? getPersonaIcon(persona.id); + const confidenceLabel = CONFIDENCE_LABELS[persona.confidence] ?? persona.confidence; + + return ( +
+
+
+ {/* Header label */} +

+ {label} +

+ + {/* Persona name */} +

+ {persona.name} +

+ + {/* Tagline */} +

+ {persona.tagline} +

+ + {/* Confidence + stats line */} +
+ + {confidenceLabel} + + {statsLine && ( + <> + · + {statsLine} + + )} +
+
+ + {/* Right side: avatar + persona icon */} +
+ {avatarUrl && ( + User avatar + )} +
+ {persona.name} +
+
+
+
+ + {/* Actions */} + {actions && ( +
{actions}
+ )} +
+ ); +} diff --git a/apps/web/src/components/vcp/blocks/VCPMatchedSignals.tsx b/apps/web/src/components/vcp/blocks/VCPMatchedSignals.tsx new file mode 100644 index 0000000..eff5bbb --- /dev/null +++ b/apps/web/src/components/vcp/blocks/VCPMatchedSignals.tsx @@ -0,0 +1,53 @@ +import { cn } from "@/lib/utils"; +import type { VCPMatchedSignalsProps } from "../types"; +import { VCPBadge } from "../primitives"; +import { AXIS_METADATA } from "../constants"; + +/** + * Format a matched rule string into a human-readable label. + * e.g., "A>=70" -> "Automation ≥ 70" + */ +function formatRule(rule: string): string { + const axisMap: Record = { + A: AXIS_METADATA.automation_heaviness.name, + B: AXIS_METADATA.guardrail_strength.name, + C: AXIS_METADATA.iteration_loop_intensity.name, + D: AXIS_METADATA.planning_signal.name, + E: AXIS_METADATA.surface_area_per_change.name, + F: AXIS_METADATA.shipping_rhythm.name, + }; + + // Parse patterns like "A>=70", "B<40", etc. + const match = rule.match(/^([A-F])([<>=]+)(\d+)$/); + if (!match) return rule; + + const [, axis, op, val] = match; + const axisName = axisMap[axis] ?? axis; + const opSymbol = op.replace(">=", "≥").replace("<=", "≤"); + + return `${axisName} ${opSymbol} ${val}`; +} + +/** + * VCPMatchedSignals - Display of matched persona rules + * + * Shows which conditions were satisfied for persona detection. + */ +export function VCPMatchedSignals({ + matchedRules, + className, +}: VCPMatchedSignalsProps) { + if (!matchedRules || matchedRules.length === 0) { + return null; + } + + return ( +
+ {matchedRules.map((rule, idx) => ( + + {formatRule(rule)} + + ))} +
+ ); +} diff --git a/apps/web/src/components/vcp/blocks/VCPMethodologyLink.tsx b/apps/web/src/components/vcp/blocks/VCPMethodologyLink.tsx new file mode 100644 index 0000000..9192d03 --- /dev/null +++ b/apps/web/src/components/vcp/blocks/VCPMethodologyLink.tsx @@ -0,0 +1,40 @@ +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import type { VCPMethodologyLinkProps } from "../types"; + +/** + * VCPMethodologyLink - Link to methodology page + * + * Consistent call-to-action for learning more about how VCP works. + */ +export function VCPMethodologyLink({ + variant = "inline", + text = "How we calculate your vibe", + className, +}: VCPMethodologyLinkProps) { + if (variant === "block") { + return ( + + {text} → + + ); + } + + return ( + + {text} + + ); +} diff --git a/apps/web/src/components/vcp/blocks/VCPNarrativeSection.tsx b/apps/web/src/components/vcp/blocks/VCPNarrativeSection.tsx new file mode 100644 index 0000000..e71fb10 --- /dev/null +++ b/apps/web/src/components/vcp/blocks/VCPNarrativeSection.tsx @@ -0,0 +1,73 @@ +import { cn } from "@/lib/utils"; +import type { VCPNarrativeSectionProps } from "../types"; +import { VCPSection, VCPInsightBox } from "../primitives"; + +/** + * VCPNarrativeSection - LLM-generated narrative display + * + * Shows AI-generated summary with paragraphs and highlights. + */ +export function VCPNarrativeSection({ + headline, + paragraphs, + highlights, + llmModel, + isLoading = false, + className, +}: VCPNarrativeSectionProps) { + if (isLoading) { + return ( + +
+
+
+
+
+ + ); + } + + if (!headline && !paragraphs?.length) { + return null; + } + + return ( + via {llmModel} + ) + } + className={className} + > + + {headline && ( +

{headline}

+ )} + {paragraphs?.map((p, idx) => ( +

+ {p} +

+ ))} +
+ + {highlights && highlights.length > 0 && ( +
+

+ Highlights +

+
    + {highlights.map((h, idx) => ( +
  • + + {h} +
  • + ))} +
+
+ )} +
+ ); +} diff --git a/apps/web/src/components/vcp/blocks/VCPStatsGrid.tsx b/apps/web/src/components/vcp/blocks/VCPStatsGrid.tsx new file mode 100644 index 0000000..5df1ddc --- /dev/null +++ b/apps/web/src/components/vcp/blocks/VCPStatsGrid.tsx @@ -0,0 +1,33 @@ +import { cn } from "@/lib/utils"; +import type { VCPStatsGridProps } from "../types"; +import { VCPStatCard } from "../primitives"; + +/** + * VCPStatsGrid - Grid of stat cards + * + * Flexible grid layout for displaying multiple stats. + */ +export function VCPStatsGrid({ + stats, + columns = 3, + className, +}: VCPStatsGridProps) { + const columnClasses = { + 2: "grid-cols-2", + 3: "grid-cols-2 sm:grid-cols-3", + 4: "grid-cols-2 sm:grid-cols-4", + }; + + return ( +
+ {stats.map((stat, idx) => ( + + ))} +
+ ); +} diff --git a/apps/web/src/components/vcp/blocks/VCPVersionHistory.tsx b/apps/web/src/components/vcp/blocks/VCPVersionHistory.tsx new file mode 100644 index 0000000..8944d3b --- /dev/null +++ b/apps/web/src/components/vcp/blocks/VCPVersionHistory.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import { cn } from "@/lib/utils"; +import type { VCPVersionHistoryProps, VCPVersionEntry } from "../types"; +import { VCPCollapsible } from "../primitives"; +import { getPersonaIcon } from "../constants"; + +/** + * VCPVersionHistory - Shared version/snapshot selector + * + * Used for both Unified VCP and Repo VCP to show historical versions. + */ +export function VCPVersionHistory({ + variant, + versions, + currentVersionId, + onVersionSelect, + showPreview = true, + className, +}: VCPVersionHistoryProps) { + const [hoveredId, setHoveredId] = useState(null); + + const currentVersion = versions.find((v) => v.id === currentVersionId); + const currentIndex = versions.findIndex((v) => v.id === currentVersionId); + + const triggerLabel = variant === "unified" + ? `Profile History (${versions.length} snapshots)` + : `Version History (${versions.length} versions)`; + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: date.getFullYear() !== new Date().getFullYear() ? "numeric" : undefined, + }); + }; + + return ( +
+ + + + + {triggerLabel} + {currentVersion && ( + + · {currentVersion.label} + + )} +
+ } + > +
+ {versions.map((version, idx) => { + const isSelected = version.id === currentVersionId; + const iconUrl = getPersonaIcon(version.personaId); + + return ( + + ); + })} +
+ + {/* Preview on hover (desktop) - could be enhanced */} + {showPreview && hoveredId && hoveredId !== currentVersionId && ( +
+ Click to view this version +
+ )} + +
+ ); +} diff --git a/apps/web/src/components/vcp/blocks/index.ts b/apps/web/src/components/vcp/blocks/index.ts new file mode 100644 index 0000000..d03153b --- /dev/null +++ b/apps/web/src/components/vcp/blocks/index.ts @@ -0,0 +1,14 @@ +/** + * VCP Blocks - Composite components + * + * These components compose primitives into reusable sections. + */ + +export { VCPIdentityHeader } from "./VCPIdentityHeader"; +export { VCPAxesGrid } from "./VCPAxesGrid"; +export { VCPStatsGrid } from "./VCPStatsGrid"; +export { VCPNarrativeSection } from "./VCPNarrativeSection"; +export { VCPMatchedSignals } from "./VCPMatchedSignals"; +export { VCPMethodologyLink } from "./VCPMethodologyLink"; +export { VCPFooter } from "./VCPFooter"; +export { VCPVersionHistory } from "./VCPVersionHistory"; diff --git a/apps/web/src/components/vcp/constants.ts b/apps/web/src/components/vcp/constants.ts new file mode 100644 index 0000000..7b4178b --- /dev/null +++ b/apps/web/src/components/vcp/constants.ts @@ -0,0 +1,156 @@ +/** + * VCP Component System Constants + * + * Shared constants for Vibe Coding Profile components. + */ + +import type { AxisKey } from "./types"; + +// ============================================================================= +// Axis Metadata +// ============================================================================= + +export interface AxisMetadata { + name: string; + shortName: string; + description: string; + longDescription: string; + lowLabel: string; + highLabel: string; +} + +export const AXIS_METADATA: Record = { + automation_heaviness: { + name: "Automation", + shortName: "Auto", + description: "How much you leverage AI/automation for large-scale changes", + longDescription: + 'How "agentic" your workflow looks, driven by large commits and chunky PRs. Higher scores indicate more automation-heavy changes.', + lowLabel: "Manual", + highLabel: "AI-Heavy", + }, + guardrail_strength: { + name: "Guardrails", + shortName: "Guard", + description: "How early and consistently tests/CI/docs appear in your workflow", + longDescription: + "How much you stabilize work with tests, CI, and docs early in the lifecycle. Higher scores mean guardrails appear sooner and more consistently.", + lowLabel: "Light", + highLabel: "Rigorous", + }, + iteration_loop_intensity: { + name: "Iteration", + shortName: "Iter", + description: "How quickly you cycle through fix-forward loops after changes", + longDescription: + 'How often you run rapid "generate → run → fix → run" cycles. Higher scores mean faster, denser iteration loops.', + lowLabel: "Stable", + highLabel: "Rapid", + }, + planning_signal: { + name: "Planning", + shortName: "Plan", + description: "How much upfront structure (issues, specs, docs-first) appears", + longDescription: + "How much intent and structure appear up front: linked issues, structured commits, and docs-first patterns. Higher scores indicate more planning.", + lowLabel: "Emergent", + highLabel: "Structured", + }, + surface_area_per_change: { + name: "Surface Area", + shortName: "Scope", + description: "How many subsystems your typical change touches", + longDescription: + "How broad each unit of work is across subsystems. Higher scores mean typical changes span more areas of the codebase.", + lowLabel: "Narrow", + highLabel: "Wide", + }, + shipping_rhythm: { + name: "Rhythm", + shortName: "Rhythm", + description: "Your shipping pattern — steady vs bursty", + longDescription: + "Bursty versus steady shipping over time. Higher scores indicate bursty sessions with gaps; lower scores indicate a steadier cadence.", + lowLabel: "Steady", + highLabel: "Bursty", + }, +}; + +// Ordered list of axis keys for consistent rendering +export const AXIS_ORDER: AxisKey[] = [ + "automation_heaviness", + "guardrail_strength", + "iteration_loop_intensity", + "planning_signal", + "surface_area_per_change", + "shipping_rhythm", +]; + +// ============================================================================= +// Confidence Labels +// ============================================================================= + +export const CONFIDENCE_LABELS: Record = { + high: "High Confidence", + medium: "Medium Confidence", + low: "Low Confidence", +}; + +export const CONFIDENCE_COLORS: Record = { + high: "text-green-400", + medium: "text-yellow-400", + low: "text-orange-400", +}; + +// ============================================================================= +// Level Labels +// ============================================================================= + +export const LEVEL_LABELS: Record = { + high: "High", + medium: "Medium", + low: "Low", +}; + +export const LEVEL_COLORS: Record = { + high: { text: "text-green-400", bg: "bg-green-500" }, + medium: { text: "text-yellow-400", bg: "bg-yellow-500" }, + low: { text: "text-red-400", bg: "bg-red-500" }, +}; + +// ============================================================================= +// Styling Constants +// ============================================================================= + +/** Standard card border radius */ +export const VCP_CARD_RADIUS = "rounded-2xl"; + +/** Standard card shadow */ +export const VCP_CARD_SHADOW = "shadow-lg"; + +/** Standard section padding */ +export const VCP_SECTION_PADDING = "p-6"; + +/** Standard title letter spacing */ +export const VCP_TITLE_TRACKING = "tracking-widest"; + +// ============================================================================= +// Persona Icon Mapping +// ============================================================================= + +export const PERSONA_ICON_MAP: Record = { + prompt_sprinter: "/aura-icons/icon-prompt-sprinter.webp", + guardrailed_viber: "/aura-icons/icon-guardrailed-viber.webp", + spec_first_director: "/aura-icons/icon-methodical-architect.webp", + vertical_slice_shipper: "/aura-icons/icon-vertical-slice-shipper.webp", + fix_loop_hacker: "/aura-icons/icon-fix-loop-hacker.webp", + rapid_risk_taker: "/aura-icons/icon-rapid-risk-taker.webp", + balanced_builder: "/aura-icons/icon-reflective-balancer.webp", + // Legacy/alias mappings + methodical_architect: "/aura-icons/icon-methodical-architect.webp", + reflective_balancer: "/aura-icons/icon-reflective-balancer.webp", +}; + +export function getPersonaIcon(personaId: string): string { + return PERSONA_ICON_MAP[personaId] ?? PERSONA_ICON_MAP.balanced_builder; +} diff --git a/apps/web/src/components/vcp/index.ts b/apps/web/src/components/vcp/index.ts new file mode 100644 index 0000000..e8b0f7b --- /dev/null +++ b/apps/web/src/components/vcp/index.ts @@ -0,0 +1,24 @@ +/** + * VCP Component System + * + * Unified components for displaying Vibe Coding Profiles. + * Used by both Unified VCP (profile page) and Repo VCP (analysis page). + */ + +// Types +export * from "./types"; + +// Constants +export * from "./constants"; + +// Primitives +export * from "./primitives"; + +// Blocks +export * from "./blocks"; + +// Unified VCP components (profile page) +export * from "./unified"; + +// Repo VCP components (analysis page) +export * from "./repo"; diff --git a/apps/web/src/components/vcp/primitives/VCPBadge.tsx b/apps/web/src/components/vcp/primitives/VCPBadge.tsx new file mode 100644 index 0000000..b36511a --- /dev/null +++ b/apps/web/src/components/vcp/primitives/VCPBadge.tsx @@ -0,0 +1,40 @@ +import { cn } from "@/lib/utils"; +import type { VCPBadgeProps } from "../types"; + +/** + * VCPBadge - Small label/tag component + * + * Used for confidence indicators, status labels, etc. + */ +export function VCPBadge({ + children, + variant = "default", + size = "md", + className, +}: VCPBadgeProps) { + const variantClasses = { + default: "bg-white/10 text-white/70", + success: "bg-green-500/20 text-green-400", + warning: "bg-yellow-500/20 text-yellow-400", + info: "bg-blue-500/20 text-blue-400", + muted: "bg-white/5 text-white/50", + }; + + const sizeClasses = { + sm: "px-1.5 py-0.5 text-[10px]", + md: "px-2 py-0.5 text-xs", + }; + + return ( + + {children} + + ); +} diff --git a/apps/web/src/components/vcp/primitives/VCPCard.tsx b/apps/web/src/components/vcp/primitives/VCPCard.tsx new file mode 100644 index 0000000..11858b9 --- /dev/null +++ b/apps/web/src/components/vcp/primitives/VCPCard.tsx @@ -0,0 +1,25 @@ +import { cn } from "@/lib/utils"; +import type { VCPCardProps } from "../types"; +import { VCP_CARD_RADIUS, VCP_CARD_SHADOW } from "../constants"; + +/** + * VCPCard - Base container component for VCP content + * + * Provides consistent styling for card-like containers across + * both Unified VCP and Repo VCP displays. + */ +export function VCPCard({ className, children, variant = "default" }: VCPCardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/web/src/components/vcp/primitives/VCPCollapsible.tsx b/apps/web/src/components/vcp/primitives/VCPCollapsible.tsx new file mode 100644 index 0000000..586507d --- /dev/null +++ b/apps/web/src/components/vcp/primitives/VCPCollapsible.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useState } from "react"; +import { cn } from "@/lib/utils"; +import type { VCPCollapsibleProps } from "../types"; + +/** + * VCPCollapsible - Expandable content section + * + * Used for "How we got this" and other optional detail sections. + */ +export function VCPCollapsible({ + trigger, + children, + defaultOpen = false, + className, +}: VCPCollapsibleProps) { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( +
+ +
+
+
{children}
+
+
+
+ ); +} diff --git a/apps/web/src/components/vcp/primitives/VCPInsightBox.tsx b/apps/web/src/components/vcp/primitives/VCPInsightBox.tsx new file mode 100644 index 0000000..3bf0ae5 --- /dev/null +++ b/apps/web/src/components/vcp/primitives/VCPInsightBox.tsx @@ -0,0 +1,49 @@ +import { cn } from "@/lib/utils"; +import type { VCPInsightBoxProps } from "../types"; + +/** + * VCPInsightBox - Highlighted insight/callout box + * + * Used for AI-generated insights, key highlights, etc. + */ +export function VCPInsightBox({ + type = "default", + label, + children, + bullets, + className, +}: VCPInsightBoxProps) { + const typeClasses = { + default: "border-l-4 border-white/20 bg-white/5", + ai: "border-l-4 border-purple-400/50 bg-purple-500/10", + highlight: "border-l-4 border-blue-400/50 bg-blue-500/10", + }; + + return ( +
+ {label && ( +
+ + {label} + + {type === "ai" && ( + + AI-Generated + + )} +
+ )} +
{children}
+ {bullets && bullets.length > 0 && ( +
    + {bullets.map((bullet, idx) => ( +
  • + + {bullet} +
  • + ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/vcp/primitives/VCPProgressBar.tsx b/apps/web/src/components/vcp/primitives/VCPProgressBar.tsx new file mode 100644 index 0000000..c621dd9 --- /dev/null +++ b/apps/web/src/components/vcp/primitives/VCPProgressBar.tsx @@ -0,0 +1,51 @@ +import { cn } from "@/lib/utils"; +import type { VCPProgressBarProps } from "../types"; + +/** + * VCPProgressBar - Visual progress/score indicator + * + * Used for displaying axis scores with gradient fill. + */ +export function VCPProgressBar({ + value, + label, + showValue = false, + size = "md", + variant = "default", + className, +}: VCPProgressBarProps) { + const clampedValue = Math.max(0, Math.min(100, value)); + + const sizeClasses = { + sm: "h-1.5", + md: "h-2", + lg: "h-3", + }; + + const variantClasses = { + default: "from-blue-500 to-purple-500", + success: "from-green-500 to-emerald-400", + warning: "from-yellow-500 to-orange-400", + danger: "from-red-500 to-pink-500", + }; + + return ( +
+ {(label || showValue) && ( +
+ {label && {label}} + {showValue && {clampedValue}} +
+ )} +
+
+
+
+ ); +} diff --git a/apps/web/src/components/vcp/primitives/VCPSection.tsx b/apps/web/src/components/vcp/primitives/VCPSection.tsx new file mode 100644 index 0000000..ea60f42 --- /dev/null +++ b/apps/web/src/components/vcp/primitives/VCPSection.tsx @@ -0,0 +1,34 @@ +import { cn } from "@/lib/utils"; +import type { VCPSectionProps } from "../types"; +import { VCPSectionTitle } from "./VCPSectionTitle"; + +/** + * VCPSection - Content section within a VCP card + * + * Provides consistent spacing, optional title, and border styling. + */ +export function VCPSection({ + title, + badge, + action, + children, + noBorder = false, + className, +}: VCPSectionProps) { + return ( +
+ {title && ( + + {title} + + )} + {children} +
+ ); +} diff --git a/apps/web/src/components/vcp/primitives/VCPSectionTitle.tsx b/apps/web/src/components/vcp/primitives/VCPSectionTitle.tsx new file mode 100644 index 0000000..770bb89 --- /dev/null +++ b/apps/web/src/components/vcp/primitives/VCPSectionTitle.tsx @@ -0,0 +1,36 @@ +import { cn } from "@/lib/utils"; +import type { VCPSectionTitleProps } from "../types"; +import { VCP_TITLE_TRACKING } from "../constants"; + +/** + * VCPSectionTitle - Consistent section header styling + * + * Small, uppercase, with letter-spacing for visual hierarchy. + */ +export function VCPSectionTitle({ + children, + badge, + action, + className, +}: VCPSectionTitleProps) { + return ( +
+
+

+ {children} +

+ {badge && ( + + {badge} + + )} +
+ {action &&
{action}
} +
+ ); +} diff --git a/apps/web/src/components/vcp/primitives/VCPStatCard.tsx b/apps/web/src/components/vcp/primitives/VCPStatCard.tsx new file mode 100644 index 0000000..e8470f6 --- /dev/null +++ b/apps/web/src/components/vcp/primitives/VCPStatCard.tsx @@ -0,0 +1,37 @@ +import { cn } from "@/lib/utils"; +import type { VCPStatCardProps } from "../types"; + +/** + * VCPStatCard - Individual stat/metric display card + * + * Used in grids to show key metrics like axis scores, commit counts, etc. + */ +export function VCPStatCard({ + label, + value, + subtitle, + variant = "default", + className, +}: VCPStatCardProps) { + return ( +
+

+ {label} +

+

+ {value} +

+ {subtitle && ( +

{subtitle}

+ )} +
+ ); +} diff --git a/apps/web/src/components/vcp/primitives/index.ts b/apps/web/src/components/vcp/primitives/index.ts new file mode 100644 index 0000000..8d289c5 --- /dev/null +++ b/apps/web/src/components/vcp/primitives/index.ts @@ -0,0 +1,14 @@ +/** + * VCP Primitives - Base UI components + * + * These are the atomic building blocks for VCP displays. + */ + +export { VCPCard } from "./VCPCard"; +export { VCPSection } from "./VCPSection"; +export { VCPSectionTitle } from "./VCPSectionTitle"; +export { VCPStatCard } from "./VCPStatCard"; +export { VCPProgressBar } from "./VCPProgressBar"; +export { VCPBadge } from "./VCPBadge"; +export { VCPCollapsible } from "./VCPCollapsible"; +export { VCPInsightBox } from "./VCPInsightBox"; diff --git a/apps/web/src/components/vcp/repo/ProfileContributionCard.tsx b/apps/web/src/components/vcp/repo/ProfileContributionCard.tsx new file mode 100644 index 0000000..73a968d --- /dev/null +++ b/apps/web/src/components/vcp/repo/ProfileContributionCard.tsx @@ -0,0 +1,121 @@ +"use client"; + +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +interface ProfileContribution { + repoName: string | null; + jobCommitCount: number | null; + includedInProfile: boolean | null; + profileTotalCommits: number | null; + profileTotalRepos: number | null; + profilePersonaName: string | null; + profileUpdatedAt: string | null; +} + +interface ProfileContributionCardProps { + /** Profile contribution data */ + contribution: ProfileContribution; + /** Whether profile is being rebuilt */ + isRebuilding?: boolean; + /** Rebuild status message */ + rebuildStatus?: "idle" | "success" | "error"; + /** Callback to trigger profile rebuild */ + onRebuild?: () => void; + /** Additional class names */ + className?: string; +} + +/** + * ProfileContributionCard - Shows how this repo contributes to the unified profile + */ +export function ProfileContributionCard({ + contribution, + isRebuilding = false, + rebuildStatus = "idle", + onRebuild, + className, +}: ProfileContributionCardProps) { + const { + repoName, + jobCommitCount, + includedInProfile, + profileTotalCommits, + profileTotalRepos, + profilePersonaName, + } = contribution; + + const contributionPercent = + jobCommitCount && profileTotalCommits && profileTotalCommits > 0 + ? Math.round((jobCommitCount / profileTotalCommits) * 100) + : null; + + return ( +
+
+
+

+ Profile Contribution +

+ {includedInProfile ? ( +

+ This repo contributes{" "} + + {contributionPercent !== null ? `${contributionPercent}%` : "—"} + {" "} + of your{" "} + + Unified VCP + + {profilePersonaName ? ( + + {" "} + (currently: {profilePersonaName}) + + ) : null} +

+ ) : ( +

+ This analysis is not yet part of your Unified VCP. +

+ )} + {profileTotalRepos ? ( +

+ {profileTotalRepos} repos · {profileTotalCommits?.toLocaleString()} commits total +

+ ) : null} +
+ + {onRebuild && !includedInProfile ? ( + + ) : null} +
+ + {rebuildStatus === "success" ? ( +

+ Profile updated successfully! +

+ ) : rebuildStatus === "error" ? ( +

+ Failed to update profile. Please try again. +

+ ) : null} +
+ ); +} diff --git a/apps/web/src/components/vcp/repo/RepoAxesSection.tsx b/apps/web/src/components/vcp/repo/RepoAxesSection.tsx new file mode 100644 index 0000000..7fbdccc --- /dev/null +++ b/apps/web/src/components/vcp/repo/RepoAxesSection.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import type { VibeAxes } from "@vibed/core"; +import { AXIS_METADATA, AXIS_ORDER } from "../constants"; +import { AxisInfoTooltip } from "../AxisInfoTooltip"; + +interface RepoAxesSectionProps { + /** Vibe axes data */ + axes: VibeAxes; + /** Additional class names */ + className?: string; +} + +/** + * RepoAxesSection - Grid display of the 6 vibe axes for a Repo VCP + * + * Similar to UnifiedAxesSection but with repo-specific styling + */ +export function RepoAxesSection({ axes, className }: RepoAxesSectionProps) { + return ( +
+

+ Your Axes +

+ +
+ {AXIS_ORDER.map((key) => { + const axis = axes[key]; + const meta = AXIS_METADATA[key]; + const score = axis?.score ?? 50; + const level = axis?.level ?? "medium"; + + return ( +
+
+
+
+

+ {meta.name} +

+ +
+

{meta.description}

+
+
+ {score} +

{level}

+
+
+
+
+
+
+ {meta.lowLabel} + {meta.highLabel} +
+
+ ); + })} +
+
+ ); +} diff --git a/apps/web/src/components/vcp/repo/RepoIdentitySection.tsx b/apps/web/src/components/vcp/repo/RepoIdentitySection.tsx new file mode 100644 index 0000000..befee8f --- /dev/null +++ b/apps/web/src/components/vcp/repo/RepoIdentitySection.tsx @@ -0,0 +1,106 @@ +"use client"; + +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { formatMetricLabel } from "@/lib/format-labels"; + +interface Persona { + label: string; + description: string; + archetypes: string[]; +} + +interface Narrative { + summary?: string | null; +} + +interface RepoIdentitySectionProps { + /** Detected persona */ + persona: Persona; + /** Optional narrative summary */ + narrative?: Narrative | null; + /** Matched criteria for persona detection */ + matchedCriteria: string[]; + /** Additional class names */ + className?: string; +} + +/** + * RepoIdentitySection - Main identity header for the Repo VCP + * + * Shows persona name, description, archetypes, and methodology collapsible. + */ +export function RepoIdentitySection({ + persona, + narrative, + matchedCriteria, + className, +}: RepoIdentitySectionProps) { + return ( +
+
+
+
+

+ Your vibe +

+

+ {persona.label} +

+

{persona.description}

+ {narrative?.summary ? ( +

+ {narrative.summary} +

+ ) : null} +
+ + {/* Methodology collapsible */} +
+
+ + How we got this + +
+

+ This report is inferred from Git/PR metadata (commit timing, commit size, + file paths, and message patterns). We do not use your prompts, IDE workflow, + PR comments, or code content—so this is an informed guess based on what lands + in Git. +

+
+

+ Matched criteria +

+ {matchedCriteria.length > 0 ? ( +
+ {matchedCriteria.map((c) => ( + + {formatMetricLabel(c)} + + ))} +
+ ) : ( +

+ This report didn't include explicit matched criteria. +

+ )} +
+
+ + Methodology + +
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/vcp/repo/RepoMetricsGrid.tsx b/apps/web/src/components/vcp/repo/RepoMetricsGrid.tsx new file mode 100644 index 0000000..238a08e --- /dev/null +++ b/apps/web/src/components/vcp/repo/RepoMetricsGrid.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +interface StreakInfo { + longest_days: number; + start_day?: string | null; + end_day?: string | null; +} + +interface TimingInfo { + peak_weekday: number | null; + peak_window?: string | null; +} + +interface CommitsInfo { + top_category: string | null; + category_counts: Record; + features_per_fix: number | null; + fixes_per_feature: number | null; +} + +interface ChunkinessInfo { + avg_files_changed: number | null; + label: string | null; +} + +interface TotalsInfo { + commits: number; +} + +interface RepoMetricsGridProps { + /** Streak information */ + streak: StreakInfo; + /** Timing information */ + timing: TimingInfo; + /** Commits information */ + commits: CommitsInfo; + /** Chunkiness information */ + chunkiness: ChunkinessInfo; + /** Total counts */ + totals: TotalsInfo; + /** Additional class names */ + className?: string; +} + +const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + +/** + * RepoMetricsGrid - 5-column grid of key repo metrics + * + * Shows: Streak, Peak Day, Focus, Build vs Fix, Scope + */ +export function RepoMetricsGrid({ + streak, + timing, + commits, + chunkiness, + totals, + className, +}: RepoMetricsGridProps) { + const weekdayName = (day: number | null): string => { + if (day === null || day < 0 || day > 6) return "—"; + return WEEKDAYS[day] ?? "—"; + }; + + const formatPeakWindow = (window: string | null | undefined): string => { + if (!window) return ""; + return window + .split("_") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); + }; + + const scopeLabel = (): string => { + if (chunkiness.label === "chunker") return "files/commit (wide)"; + if (chunkiness.label === "mixer") return "files/commit (balanced)"; + if (chunkiness.label === "slicer") return "files/commit (focused)"; + return "files/commit"; + }; + + return ( +
+ {/* Streak */} +
+

+ Streak +

+

+ {streak.longest_days} day{streak.longest_days === 1 ? "" : "s"} +

+ {streak.start_day && streak.end_day ? ( +

+ {streak.start_day} → {streak.end_day} +

+ ) : null} +
+ + {/* Peak Day */} +
+

+ Peak Day +

+

+ {weekdayName(timing.peak_weekday)} +

+ {timing.peak_window ? ( +

+ {formatPeakWindow(timing.peak_window)} (UTC) +

+ ) : null} +
+ + {/* Focus */} +
+

+ Focus +

+

+ {commits.top_category ?? "—"} +

+ {commits.top_category ? ( +

+ {commits.category_counts[commits.top_category] ?? 0} of {totals.commits} commits +

+ ) : null} +
+ + {/* Build vs Fix */} +
+

+ Build vs Fix +

+

+ {commits.features_per_fix !== null + ? `${commits.features_per_fix.toFixed(1)} : 1` + : commits.fixes_per_feature !== null + ? `1 : ${commits.fixes_per_feature.toFixed(1)}` + : "—"} +

+

+ {commits.features_per_fix !== null + ? "features per fix" + : commits.fixes_per_feature !== null + ? "fixes per feature" + : "balanced"} +

+
+ + {/* Scope */} +
+

+ Scope +

+

+ {chunkiness.avg_files_changed !== null + ? `${chunkiness.avg_files_changed.toFixed(1)}` + : "—"} +

+

{scopeLabel()}

+
+
+ ); +} diff --git a/apps/web/src/components/vcp/repo/index.ts b/apps/web/src/components/vcp/repo/index.ts new file mode 100644 index 0000000..6453eb1 --- /dev/null +++ b/apps/web/src/components/vcp/repo/index.ts @@ -0,0 +1,11 @@ +/** + * Repo VCP Components + * + * Components specific to the Repo VCP (analysis page). + * These display individual repo analysis results. + */ + +export { RepoIdentitySection } from "./RepoIdentitySection"; +export { RepoMetricsGrid } from "./RepoMetricsGrid"; +export { RepoAxesSection } from "./RepoAxesSection"; +export { ProfileContributionCard } from "./ProfileContributionCard"; diff --git a/apps/web/src/components/vcp/types.ts b/apps/web/src/components/vcp/types.ts new file mode 100644 index 0000000..a9cc32e --- /dev/null +++ b/apps/web/src/components/vcp/types.ts @@ -0,0 +1,301 @@ +/** + * VCP Component System Types + * + * Shared type definitions for Vibe Coding Profile components. + * Used across primitives, blocks, and page-specific components. + */ + +import type { VibeAxes, VibePersona, Level, Confidence } from "@vibed/core"; + +// ============================================================================= +// Axis Types +// ============================================================================= + +export type AxisKey = keyof VibeAxes; + +export interface AxisDisplayValue { + key: AxisKey; + name: string; + score: number; + level: Level; + description?: string; +} + +// ============================================================================= +// Persona Types +// ============================================================================= + +export interface PersonaDisplay { + id: string; + name: string; + tagline: string; + confidence: Confidence; + score: number; +} + +// ============================================================================= +// Card & Section Types +// ============================================================================= + +export interface VCPCardProps { + /** Optional additional CSS classes */ + className?: string; + /** Card content */ + children: React.ReactNode; + /** Variant affects default styling */ + variant?: "default" | "elevated" | "muted"; +} + +export interface VCPSectionProps { + /** Section title (optional - renders VCPSectionTitle if provided) */ + title?: string; + /** Optional badge next to title */ + badge?: string; + /** Optional right-aligned action/element */ + action?: React.ReactNode; + /** Section content */ + children: React.ReactNode; + /** Hide top border */ + noBorder?: boolean; + /** Optional additional CSS classes */ + className?: string; +} + +export interface VCPSectionTitleProps { + /** Title text */ + children: React.ReactNode; + /** Optional badge */ + badge?: string; + /** Optional right-aligned action */ + action?: React.ReactNode; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Stat & Metric Types +// ============================================================================= + +export interface VCPStatCardProps { + /** Stat label (displayed small, uppercase) */ + label: string; + /** Stat value (displayed large) */ + value: string | number; + /** Optional subtitle or additional context */ + subtitle?: string; + /** Visual variant */ + variant?: "default" | "highlight" | "muted"; + /** Optional additional CSS classes */ + className?: string; +} + +export interface VCPProgressBarProps { + /** Progress value 0-100 */ + value: number; + /** Optional label */ + label?: string; + /** Show value text */ + showValue?: boolean; + /** Bar size */ + size?: "sm" | "md" | "lg"; + /** Color variant */ + variant?: "default" | "success" | "warning" | "danger"; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Badge Types +// ============================================================================= + +export interface VCPBadgeProps { + /** Badge content */ + children: React.ReactNode; + /** Visual variant */ + variant?: "default" | "success" | "warning" | "info" | "muted"; + /** Size */ + size?: "sm" | "md"; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Collapsible Types +// ============================================================================= + +export interface VCPCollapsibleProps { + /** Trigger/header content */ + trigger: React.ReactNode; + /** Collapsible content */ + children: React.ReactNode; + /** Default open state */ + defaultOpen?: boolean; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Insight Box Types +// ============================================================================= + +export interface VCPInsightBoxProps { + /** Insight type for styling */ + type?: "default" | "ai" | "highlight"; + /** Optional label (e.g., "INSIGHT", "AI-GENERATED") */ + label?: string; + /** Main insight text */ + children: React.ReactNode; + /** Optional bullet points */ + bullets?: string[]; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Identity Header Types +// ============================================================================= + +export interface VCPIdentityHeaderProps { + /** Header label (e.g., "Your Unified VCP") */ + label: string; + /** Persona display info */ + persona: PersonaDisplay; + /** Optional stats line (e.g., "3 repos · 1,245 commits") */ + statsLine?: string; + /** Optional avatar URL */ + avatarUrl?: string | null; + /** Optional persona icon URL (overrides default) */ + personaIconUrl?: string; + /** Optional action buttons */ + actions?: React.ReactNode; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Axes Grid Types +// ============================================================================= + +export interface VCPAxesGridProps { + /** Vibe axes to display */ + axes: VibeAxes; + /** Show detailed descriptions */ + showDescriptions?: boolean; + /** Layout variant */ + layout?: "grid" | "list"; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Stats Grid Types +// ============================================================================= + +export interface VCPStatsGridProps { + /** Stats to display */ + stats: Array<{ + label: string; + value: string | number; + subtitle?: string; + }>; + /** Number of columns */ + columns?: 2 | 3 | 4; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Narrative Section Types +// ============================================================================= + +export interface VCPNarrativeSectionProps { + /** Narrative headline/summary */ + headline?: string; + /** Narrative paragraphs */ + paragraphs?: string[]; + /** Highlight bullets */ + highlights?: string[]; + /** LLM model used (for attribution) */ + llmModel?: string; + /** Whether narrative is loading */ + isLoading?: boolean; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Matched Signals Types +// ============================================================================= + +export interface VCPMatchedSignalsProps { + /** Matched rule identifiers */ + matchedRules: string[]; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Methodology Link Types +// ============================================================================= + +export interface VCPMethodologyLinkProps { + /** Link variant */ + variant?: "inline" | "block"; + /** Custom text */ + text?: string; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Footer Types +// ============================================================================= + +export interface VCPFooterProps { + /** Left content */ + left: React.ReactNode; + /** Right content */ + right: React.ReactNode; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Version History Types +// ============================================================================= + +export interface VCPVersionEntry { + /** Unique identifier (job_id) */ + id: string; + /** Display label (e.g., "v3", "Dec 15") */ + label: string; + /** Persona name at this version */ + personaName: string; + /** Persona ID for icon */ + personaId: string; + /** When this version was created */ + createdAt: string; + /** Whether this is the currently viewed version */ + isCurrent?: boolean; +} + +export interface VCPVersionHistoryProps { + /** Variant determines behavior */ + variant: "unified" | "repo"; + /** Available versions */ + versions: VCPVersionEntry[]; + /** Currently selected version ID */ + currentVersionId: string; + /** Callback when version is selected */ + onVersionSelect: (versionId: string) => void; + /** Whether to show preview on hover (desktop) */ + showPreview?: boolean; + /** Optional additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Re-exports for convenience +// ============================================================================= + +export type { VibeAxes, VibePersona, Level, Confidence }; diff --git a/apps/web/src/components/vcp/unified/EvolutionSection.tsx b/apps/web/src/components/vcp/unified/EvolutionSection.tsx new file mode 100644 index 0000000..8041d54 --- /dev/null +++ b/apps/web/src/components/vcp/unified/EvolutionSection.tsx @@ -0,0 +1,65 @@ +import { cn } from "@/lib/utils"; + +interface EvolutionSectionProps { + /** Number of completed repo VCPs */ + repoVcpCount: number; + /** Vibe shifts value (can be number, "New", or "Steady") */ + vibeShifts: string | number; + /** Dominant persona name (short, typically first word of persona) */ + dominantVibe: string | null; + /** Helper text explaining shift history */ + shiftHelper?: string; + /** Additional class names */ + className?: string; +} + +/** + * EvolutionSection - Shows how the user's vibe has developed over time + * + * Displays: Repo VCPs count, Vibe shifts, Dominant vibe + */ +export function EvolutionSection({ + repoVcpCount, + vibeShifts, + dominantVibe, + shiftHelper, + className, +}: EvolutionSectionProps) { + return ( +
+

+ Evolution +

+

+ How your vibe has developed over time +

+ +
+
+

{repoVcpCount}

+

+ Repo VCPs +

+
+
+

{vibeShifts}

+

+ Vibe Shifts +

+
+
+

+ {dominantVibe ?? "—"} +

+

+ Dominant Vibe +

+
+
+ + {shiftHelper ? ( +

{shiftHelper}

+ ) : null} +
+ ); +} diff --git a/apps/web/src/components/vcp/unified/RepoBreakdownSection.tsx b/apps/web/src/components/vcp/unified/RepoBreakdownSection.tsx new file mode 100644 index 0000000..a014321 --- /dev/null +++ b/apps/web/src/components/vcp/unified/RepoBreakdownSection.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +interface RepoPersona { + repoName: string; + personaId: string; + personaName: string; + commitCount: number; +} + +interface RepoBreakdownSectionProps { + /** List of repo personas */ + repoPersonas: RepoPersona[]; + /** Total commits across all repos (for percentage calculation) */ + totalCommits: number; + /** Additional class names */ + className?: string; +} + +/** + * RepoBreakdownSection - Shows how each repo contributes to the unified profile + * + * Displays repo names, personas, commit counts, and contribution percentages + */ +export function RepoBreakdownSection({ + repoPersonas, + totalCommits, + className, +}: RepoBreakdownSectionProps) { + if (repoPersonas.length === 0) { + return null; + } + + return ( +
+

+ Repo Breakdown +

+

+ Each repo contributes to your profile, weighted by commits +

+ +
+ {repoPersonas.map((repo, i) => { + const percentage = Math.round( + (repo.commitCount / Math.max(totalCommits, 1)) * 100 + ); + return ( +
+
+
+

+ {repo.repoName} +

+ {percentage}% +
+
+
+
+

+ {repo.personaName} · {repo.commitCount.toLocaleString()} commits +

+
+
+ ); + })} +
+
+ ); +} diff --git a/apps/web/src/components/vcp/unified/UnifiedAxesSection.tsx b/apps/web/src/components/vcp/unified/UnifiedAxesSection.tsx new file mode 100644 index 0000000..3ea0896 --- /dev/null +++ b/apps/web/src/components/vcp/unified/UnifiedAxesSection.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import type { VibeAxes } from "@vibed/core"; +import { AXIS_METADATA, AXIS_ORDER } from "../constants"; +import { AxisInfoTooltip } from "../AxisInfoTooltip"; + +interface UnifiedAxesSectionProps { + /** Vibe axes data */ + axes: VibeAxes; + /** Additional class names */ + className?: string; +} + +/** + * UnifiedAxesSection - Grid display of the 6 vibe axes for the unified VCP + * + * Uses the light theme styling (zinc colors for text, subtle backgrounds) + */ +export function UnifiedAxesSection({ + axes, + className, +}: UnifiedAxesSectionProps) { + return ( +
+

+ Your Axes +

+

+ The 6 signals that define your AI-assisted engineering (vibe coding) style +

+ +
+ {AXIS_ORDER.map((key) => { + const axis = axes[key]; + const meta = AXIS_METADATA[key]; + const score = axis?.score ?? 50; + + return ( +
+
+
+
+

+ {meta.name} +

+ +
+

{meta.description}

+
+ {score} +
+
+
+
+
+ ); + })} +
+
+ ); +} diff --git a/apps/web/src/components/vcp/unified/UnifiedIdentitySection.tsx b/apps/web/src/components/vcp/unified/UnifiedIdentitySection.tsx new file mode 100644 index 0000000..f704cfe --- /dev/null +++ b/apps/web/src/components/vcp/unified/UnifiedIdentitySection.tsx @@ -0,0 +1,139 @@ +"use client"; + +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +interface UnifiedIdentitySectionProps { + /** Persona name */ + personaName: string; + /** Persona tagline */ + personaTagline?: string | null; + /** Confidence level */ + confidence?: string; + /** Total repos count */ + totalRepos?: number; + /** Total commits count */ + totalCommits?: number; + /** Clarity percentage (0-100) */ + clarity?: number; + /** Number of completed analyses */ + completedJobs: number; + /** Fallback: analyzed repos count (for "profile forming" state) */ + analyzedRepos?: number; + /** Fallback: analyzed commits count (for "profile forming" state) */ + analyzedCommits?: number; + /** Fallback persona (from latest job, if no profile) */ + latestPersona?: { + label: string | null; + confidence: string | null; + repoName: string | null; + } | null; + /** Additional class names */ + className?: string; +} + +/** + * UnifiedIdentitySection - Main identity header for the Unified VCP + * + * Shows persona name, tagline, confidence, and stats. Styled with gradient background. + */ +export function UnifiedIdentitySection({ + personaName, + personaTagline, + confidence, + totalRepos, + totalCommits, + clarity, + completedJobs, + analyzedRepos, + analyzedCommits, + latestPersona, + className, +}: UnifiedIdentitySectionProps) { + const hasProfile = Boolean(confidence && totalRepos); + const isForming = !hasProfile && (analyzedRepos ?? 0) > 0; + + return ( +
+
+
+
+

+ Your Unified VCP +

+
+ {completedJobs > 0 ? ( + <> + + Add repo + + + View Repo VCPs + + + ) : ( + + Pick a repo + + )} +
+
+

+ {personaName} +

+ {personaTagline ? ( +

+ “{personaTagline}” +

+ ) : null} +
+ {hasProfile ? ( + <> + + {confidence} confidence + + {totalRepos} repos + · + {totalCommits?.toLocaleString()} commits + · + {clarity}% clarity + + ) : isForming ? ( + <> + {analyzedRepos} repos + · + {analyzedCommits?.toLocaleString()} commits + · + Profile forming + + ) : latestPersona ? ( + <> + + {latestPersona.confidence} + + {latestPersona.repoName ? ( + Based on {latestPersona.repoName} + ) : null} + + ) : ( + Run a vibe check to get your first read + )} +
+
+
+ ); +} diff --git a/apps/web/src/components/vcp/unified/UnifiedInsightSection.tsx b/apps/web/src/components/vcp/unified/UnifiedInsightSection.tsx new file mode 100644 index 0000000..425b673 --- /dev/null +++ b/apps/web/src/components/vcp/unified/UnifiedInsightSection.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +interface UnifiedInsightSectionProps { + /** Main headline/insight */ + headline: string; + /** Additional narrative paragraphs (LLM-generated) */ + paragraphs?: string[]; + /** Highlight bullet points (LLM-generated) */ + highlights?: string[]; + /** Whether the narrative is LLM-generated */ + isLLMGenerated?: boolean; + /** LLM model used */ + llmModel?: string | null; + /** Additional class names */ + className?: string; +} + +/** + * UnifiedInsightSection - Displays the main insight and LLM narrative + * + * Styled with violet left border to indicate AI-generated content + */ +export function UnifiedInsightSection({ + headline, + paragraphs = [], + highlights = [], + isLLMGenerated = false, + llmModel, + className, +}: UnifiedInsightSectionProps) { + return ( +
+
+
+

+ Insight +

+ {isLLMGenerated && llmModel ? ( + + AI-generated + + ) : null} +
+

+ {headline} +

+ {paragraphs.length > 0 ? ( +
+ {paragraphs.map((paragraph, idx) => ( +

+ {paragraph} +

+ ))} +
+ ) : null} + {highlights.length > 0 ? ( +
    + {highlights.map((highlight, idx) => ( +
  • + + {highlight} +
  • + ))} +
+ ) : null} +
+
+ ); +} diff --git a/apps/web/src/components/vcp/unified/UnifiedMethodologySection.tsx b/apps/web/src/components/vcp/unified/UnifiedMethodologySection.tsx new file mode 100644 index 0000000..bc26cc8 --- /dev/null +++ b/apps/web/src/components/vcp/unified/UnifiedMethodologySection.tsx @@ -0,0 +1,88 @@ +"use client"; + +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { formatMatchedRule, AXIS_LEGEND } from "@/lib/format-labels"; + +interface UnifiedMethodologySectionProps { + /** Matched rules from detectVibePersona (string array like ["A>=70", "C>=65"]) */ + matchedRules: string[]; + /** Caveats from detectVibePersona */ + caveats: string[]; + /** Additional class names */ + className?: string; +} + +/** + * UnifiedMethodologySection - Collapsible details about how the persona was determined + * + * Accepts data directly from detectVibePersona output + */ +export function UnifiedMethodologySection({ + matchedRules, + caveats, + className, +}: UnifiedMethodologySectionProps) { + return ( +
+
+ + How we got this + +
+
+

+ Matched signals +

+
+ {matchedRules.length > 0 ? ( + matchedRules.map((rule) => ( + + {formatMatchedRule(rule)} + + )) + ) : ( + + Selected by nearest-fit across all signals + + )} +
+
+ +
+ {Object.entries(AXIS_LEGEND).map(([k, v]) => ( + + {k} = {v} + + ))} +
+ + {caveats.length > 0 ? ( +
+

+ Caveats +

+
    + {caveats.map((caveat) => ( +
  • + • {caveat} +
  • + ))} +
+
+ ) : null} + + + View full methodology + +
+
+
+ ); +} diff --git a/apps/web/src/components/vcp/unified/index.ts b/apps/web/src/components/vcp/unified/index.ts new file mode 100644 index 0000000..1ed69a6 --- /dev/null +++ b/apps/web/src/components/vcp/unified/index.ts @@ -0,0 +1,13 @@ +/** + * Unified VCP Components + * + * Components specific to the Unified VCP (profile page). + * These use the light theme styling (white backgrounds, zinc text). + */ + +export { UnifiedIdentitySection } from "./UnifiedIdentitySection"; +export { UnifiedInsightSection } from "./UnifiedInsightSection"; +export { UnifiedAxesSection } from "./UnifiedAxesSection"; +export { EvolutionSection } from "./EvolutionSection"; +export { RepoBreakdownSection } from "./RepoBreakdownSection"; +export { UnifiedMethodologySection } from "./UnifiedMethodologySection"; diff --git a/apps/web/src/inngest/functions/analyze-repo.ts b/apps/web/src/inngest/functions/analyze-repo.ts index 7418630..ea7c01b 100644 --- a/apps/web/src/inngest/functions/analyze-repo.ts +++ b/apps/web/src/inngest/functions/analyze-repo.ts @@ -152,6 +152,7 @@ function computeEpisodeSummary(events: CommitEvent[]): Array<{ */ interface NarrativeResult { narrative: AnalysisReport["narrative"]; + tagline: string | null; inputTokens: number; outputTokens: number; } @@ -227,9 +228,10 @@ export async function generateNarrativeWithLLM(params: { "- Never infer intent, skill, or code quality. Avoid speculation and motivational language.", "- Every claim must cite at least one specific metric name and value (e.g. burstiness_score=0.42).", "- Each section must include evidence: 2-6 commit SHAs that support the section.", + "- Provide a concise tagline (<=60 characters) that describes the developer's vibe. This will be displayed on the share card and must remain observational.", "", "Output must be STRICT JSON with this schema:", - '{"summary":"...","sections":[{"title":"...","content":"...","evidence":["sha", "..."]}],"highlights":[{"metric":"...","value":"...","interpretation":"..."}]}', + '{"summary":"...","tagline":"...","sections":[{"title":"...","content":"...","evidence":["sha", "..."]}],"highlights":[{"metric":"...","value":"...","interpretation":"..."}]}', ].join("\n"); const userPrompt = [ @@ -307,6 +309,7 @@ export async function generateNarrativeWithLLM(params: { const obj = parsed as { summary?: unknown; + tagline?: unknown; sections?: unknown; highlights?: unknown; }; @@ -335,8 +338,13 @@ export async function generateNarrativeWithLLM(params: { highlights.push({ metric: hi.metric, value: hi.value, interpretation: hi.interpretation }); } + // Enforce 60-char limit as specified in the prompt + const rawTagline = typeof obj.tagline === "string" ? obj.tagline.trim() : null; + const tagline = rawTagline ? rawTagline.slice(0, 60) : null; + return { narrative: { summary: obj.summary, sections, highlights }, + tagline, inputTokens: response.inputTokens, outputTokens: response.outputTokens, }; @@ -1044,6 +1052,7 @@ export const analyzeRepo = inngest.createFunction( const llmResolution = await resolveLLMConfig(userId, repoId); let llmNarrative: AnalysisReport["narrative"] | null = null; let llmModelUsed: string | null = null; + let llmTagline: string | null = null; const llmKeySource: LLMKeySource = llmResolution.source; const llmConfig: LLMConfig | null = llmResolution.config; @@ -1070,6 +1079,7 @@ export const analyzeRepo = inngest.createFunction( if (result) { llmNarrative = result.narrative; llmModelUsed = candidate; + llmTagline = result.tagline; // Record successful usage with token counts await recordLLMUsage({ @@ -1137,22 +1147,28 @@ export const analyzeRepo = inngest.createFunction( ); if (reportError) throw new Error(`Failed to upsert report: ${reportError.message}`); + const personaTaglineFallback = insights.persona.description ?? ""; + const finalTagline = + llmTagline?.trim()?.length ? llmTagline.trim() : personaTaglineFallback; + insights.share_template.tagline = finalTagline; + // Save legacy insights const { error: insightsError } = await supabase.from("analysis_insights").upsert( - { - job_id: jobId, - insights_json: insights, - generator_version: ANALYZER_VERSION, - persona_id: insights.persona.id, - persona_label: insights.persona.label, - persona_confidence: insights.persona.confidence, - tech_signals: insights.tech_signals, - share_template: insights.share_template, - persona_delta: insights.persona_delta, - sources: insights.sources, - }, - { onConflict: "job_id" } - ); + { + job_id: jobId, + insights_json: insights, + generator_version: ANALYZER_VERSION, + persona_id: insights.persona.id, + persona_label: insights.persona.label, + persona_confidence: insights.persona.confidence, + tech_signals: insights.tech_signals, + share_template: insights.share_template, + persona_delta: insights.persona_delta, + sources: insights.sources, + tagline: finalTagline, + }, + { onConflict: "job_id" } + ); if (insightsError) throw new Error(`Failed to upsert insights: ${insightsError.message}`); // Save vibe v2 insights diff --git a/apps/web/src/lib/persona-auras.ts b/apps/web/src/lib/persona-auras.ts new file mode 100644 index 0000000..c6a3ea4 --- /dev/null +++ b/apps/web/src/lib/persona-auras.ts @@ -0,0 +1,183 @@ +/** + * Persona Aura Assets + * + * Each persona has visual assets that represent their coding style: + * - Background: Full-card background for share cards and profile sections + * - Icon: Compact badge/avatar for smaller UI elements + * + * Visual metaphors: + * - Rapid Risk-Taker: Flames, speed lines (bold, fast, reckless) + * - Prompt Sprinter: Lightning bolts (rapid prompting cycles) + * - Methodical Architect: Blueprint grid, building blocks (plans first) + * - Guardrailed Viber: Shield + safety net + rails (engineered guardrails) + * - Reflective Balancer: Yin-yang balance (equilibrium, thoughtful) + * - Orchestrator: Gas Town / Gas Pump (fueling agent workflows, inspired by Steve Yegge) + */ + +export interface PersonaAura { + /** Full-card background image */ + background: string; + /** Compact icon/badge image */ + icon: string; + /** Alt text describing the visual metaphor */ + alt: string; + /** Brief description of what the persona represents */ + description: string; +} + +const PERSONA_ALIASES: Record = { + // Core persona IDs (kebab-case) -> app persona IDs (snake_case) + "debugger-risk-taker": "rapid_risk_taker", + "vibe-prototyper": "prompt_sprinter", + "test-validator": "guardrailed_viber", + "spec-architect": "spec_first_director", + "agent-orchestrator": "vertical_slice_shipper", + "infra-architect": "infra_weaver", + "specialist-consultant": "spec_first_director", + "reflective-balancer": "reflective_balancer", +}; + +/** + * Maps persona IDs to their aura assets + */ +export const PERSONA_AURAS: Record = { + // Dynamic/fast personas + rapid_risk_taker: { + background: "/aura-backgrounds/bg-rapid-risk-taker-v2.webp", + icon: "/aura-icons/icon-rapid-risk-taker.webp", + alt: "Purple flames and speed lines suggesting bold, fast movement", + description: "Ships fast, skips guardrails, embraces risk", + }, + prompt_sprinter: { + background: "/aura-backgrounds/bg-prompt-sprinter-v2.webp", + icon: "/aura-icons/icon-prompt-sprinter.webp", + alt: "Lightning bolts and iteration circles suggesting rapid prompting", + description: "Rapid iteration cycles, fast prompting and fixing", + }, + fix_loop_hacker: { + background: "/aura-backgrounds/bg-prompt-sprinter-v2.webp", + icon: "/aura-icons/icon-fix-loop-hacker.webp", + alt: "Loop arrows with bug being squashed - rapid debug cycles", + description: "Quick fixes and rapid iteration loops", + }, + + // Balanced/thoughtful personas + balanced_builder: { + background: "/aura-backgrounds/bg-reflective-balancer-v2.webp", + icon: "/aura-icons/icon-balanced-builder.webp", + alt: "Balance scale weighing building blocks - thoughtful construction", + description: "Balanced approach between speed and safety", + }, + reflective_balancer: { + background: "/aura-backgrounds/bg-reflective-balancer-v2.webp", + icon: "/aura-icons/icon-reflective-balancer.webp", + alt: "Balance scales and yin-yang suggesting thoughtful equilibrium", + description: "Thoughtful consideration of tradeoffs", + }, + guardrailed_viber: { + background: "/aura-backgrounds/bg-guardrailed-viber-v2.webp", + icon: "/aura-icons/icon-guardrailed-viber.webp", + alt: "Shield with engineered guardrails and safety net mesh suggesting protection", + description: "Tests as contracts, safety nets before shipping", + }, + + // Structured/methodical personas + spec_first_director: { + background: "/aura-backgrounds/bg-methodical-architect-v2.webp", + icon: "/aura-icons/icon-spec-first-director.webp", + alt: "Clipboard with flowchart directing arrows outward", + description: "Specs and plans before implementation", + }, + methodical_architect: { + background: "/aura-backgrounds/bg-methodical-architect-v2.webp", + icon: "/aura-icons/icon-methodical-architect.webp", + alt: "Isometric blocks on blueprint grid suggesting structured planning", + description: "Plans carefully before building", + }, + + // Orchestration/coordination personas - split by type + // Gas Pump (Gas Town) for agent coordination + vertical_slice_shipper: { + background: "/aura-backgrounds/bg-orchestrator-v5.webp", + icon: "/aura-icons/icon-vertical-slice-shipper.webp", + alt: "Rocket carrying full stack - shipping complete end-to-end", + description: "Ships complete vertical slices end-to-end", + }, + // Factory for building/manufacturing personas + toolsmith_viber: { + background: "/aura-backgrounds/bg-orchestrator-factory.webp", + icon: "/aura-icons/icon-toolsmith-viber.webp", + alt: "Anvil forging a gear with sparks - crafting tools", + description: "Builds and coordinates tools and workflows", + }, + infra_weaver: { + background: "/aura-backgrounds/bg-orchestrator-factory.webp", + icon: "/aura-icons/icon-infra-weaver.webp", + alt: "Spider web network with glowing nodes - weaving infrastructure", + description: "Weaves infrastructure and systems together", + }, + + // Legacy persona IDs (for backwards compatibility) + "vibe-prototyper": { + background: "/aura-backgrounds/bg-prompt-sprinter-v2.webp", + icon: "/aura-icons/icon-prompt-sprinter.webp", + alt: "Creative burst pattern", + description: "Fast prototyping and experimentation", + }, + "test-validator": { + background: "/aura-backgrounds/bg-guardrailed-viber-v2.webp", + icon: "/aura-icons/icon-guardrailed-viber.webp", + alt: "Protective shield pattern", + description: "Tests first, validates before shipping", + }, + "spec-architect": { + background: "/aura-backgrounds/bg-methodical-architect-v2.webp", + icon: "/aura-icons/icon-methodical-architect.webp", + alt: "Blueprint grid pattern", + description: "Architecture and specs before code", + }, + "agent-orchestrator": { + background: "/aura-backgrounds/bg-orchestrator-v5.webp", + icon: "/aura-icons/icon-orchestrator.webp", + alt: "Gas Town - fueling station for agent coordination", + description: "Coordinates AI agents and workflows", + }, + "reflective-balancer": { + background: "/aura-backgrounds/bg-reflective-balancer-v2.webp", + icon: "/aura-icons/icon-reflective-balancer.webp", + alt: "Balance and equilibrium pattern", + description: "Balances competing priorities", + }, + "infra-architect": { + background: "/aura-backgrounds/bg-orchestrator-factory.webp", + icon: "/aura-icons/icon-infra-weaver.webp", + alt: "Factory infrastructure weaving pattern", + description: "Infrastructure governance and system design", + }, + "specialist-consultant": { + background: "/aura-backgrounds/bg-methodical-architect-v2.webp", + icon: "/aura-icons/icon-spec-first-director.webp", + alt: "Specification-driven direction and planning", + description: "Assigns roles and review steps with precision", + }, +}; + +// Default aura for unknown personas - use the balanced one as neutral +export const DEFAULT_AURA: PersonaAura = { + background: "/aura-backgrounds/bg-reflective-balancer-v2.webp", + icon: "/aura-icons/icon-reflective-balancer.webp", + alt: "Abstract balanced pattern", + description: "Your unique coding style", +}; + +/** + * Get the aura assets for a persona ID + */ +export function getPersonaAura(personaId: string | undefined): PersonaAura { + if (!personaId) return DEFAULT_AURA; + const normalized = personaId.trim().toLowerCase(); + const underscored = normalized.replace(/-/g, "_"); + const alias = PERSONA_ALIASES[normalized] ?? PERSONA_ALIASES[underscored]; + const key = alias ?? underscored ?? normalized; + return PERSONA_AURAS[key] ?? PERSONA_AURAS[normalized] ?? DEFAULT_AURA; +} diff --git a/apps/web/src/lib/theme.ts b/apps/web/src/lib/theme.ts index e7f905e..e6bdbde 100644 --- a/apps/web/src/lib/theme.ts +++ b/apps/web/src/lib/theme.ts @@ -1,29 +1,80 @@ +/** + * Vibed Coding Theme + * + * Visual Identity: "Developer Personality Discovery" + * - Unified purple-blue palette (violet-600 → indigo-600) + * - Flat, clean surfaces inspired by Remotion.dev + * - Gradients only for accents (buttons, progress bars, text highlights) + * - Fun, personality-test vibe — not terminal, not performance review + */ + +// Core brand colors +export const vibeColors = { + // Primary violet-blue spectrum + violetDeep: "#7c3aed", // violet-600 - Primary brand + indigo: "#6366f1", // indigo-500 - Secondary, gradients + blueSoft: "#818cf8", // indigo-400 - Lighter accents + lavender: "#c4b5fd", // violet-300 - Soft fills + violetMist: "#ede9fe", // violet-100 - Very light backgrounds + violetGhost: "#f5f3ff", // violet-50 - Subtle bg tint + + // Neutrals with purple undertone + ink: "#1e1b4b", // indigo-950 - Dark text/headings + slate: "#64748b", // slate-500 - Secondary text + mist: "#faf9fc", // Custom - Page background with violet tint + + // Accent (use sparingly) + coral: "#f97316", // orange-500 - CTAs only, never on content areas +} as const; + +// Gradient definitions +export const vibeGradients = { + // Primary brand gradient (buttons, progress bars, text) + primary: "from-violet-600 to-indigo-500", + // Softer version for larger surfaces + soft: "from-violet-100 to-indigo-100", + // Insight box background + insight: "from-violet-50 to-indigo-50", +} as const; + export const wrappedTheme = { - background: "bg-white", + background: "bg-[#faf9fc]", + // Clean background - no orbs, flat and modern backgroundOrbs: { wrapper: "pointer-events-none fixed inset-0 -z-10 overflow-hidden", + // Single subtle gradient for depth, not chaos orbA: - "absolute -left-24 top-28 h-[26rem] w-[26rem] rounded-full bg-gradient-to-br from-fuchsia-400/35 via-indigo-400/25 to-cyan-400/35 blur-3xl", + "absolute -left-48 -top-48 h-[40rem] w-[40rem] rounded-full bg-gradient-to-br from-violet-200/30 to-indigo-200/20 blur-3xl", orbB: - "absolute -right-24 top-10 h-[28rem] w-[28rem] rounded-full bg-gradient-to-br from-lime-300/30 via-emerald-300/22 to-cyan-300/30 blur-3xl", - orbC: - "absolute bottom-0 left-1/2 h-[32rem] w-[32rem] -translate-x-1/2 rounded-full bg-gradient-to-br from-amber-300/30 via-fuchsia-300/22 to-indigo-300/25 blur-3xl", - vignette: - "absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(0,0,0,0.05),transparent_60%)]", + "absolute -right-48 bottom-0 h-[35rem] w-[35rem] rounded-full bg-gradient-to-br from-indigo-200/25 to-violet-200/15 blur-3xl", + orbC: "hidden", // Removed third orb for cleaner look + vignette: "hidden", // Removed vignette }, container: "mx-auto max-w-6xl px-6 sm:px-10 lg:px-20", pageY: "py-12", - card: "rounded-3xl border border-black/5 bg-white/75 shadow-[0_25px_80px_rgba(2,6,23,0.08)] backdrop-blur", - cardInner: "rounded-2xl border border-black/5 bg-white/70 backdrop-blur", + card: "rounded-3xl border border-black/5 bg-white shadow-[0_25px_80px_rgba(30,27,75,0.06)]", + cardInner: "rounded-2xl border border-black/5 bg-white", + // Unified gradient text gradientText: - "bg-gradient-to-r from-fuchsia-600 via-indigo-600 to-cyan-600 bg-clip-text text-transparent", - dot: "h-2.5 w-2.5 rounded-full bg-gradient-to-r from-fuchsia-600 via-indigo-600 to-cyan-600 shadow-sm", + "bg-gradient-to-r from-violet-600 to-indigo-500 bg-clip-text text-transparent", + dot: "h-2.5 w-2.5 rounded-full bg-gradient-to-r from-violet-600 to-indigo-500 shadow-sm", + // Primary button with unified gradient primaryButton: - "rounded-full bg-gradient-to-r from-fuchsia-600 via-indigo-600 to-cyan-600 px-6 py-2 text-sm font-semibold text-white shadow-sm transition hover:brightness-110", + "rounded-full bg-gradient-to-r from-violet-600 to-indigo-500 px-6 py-2 text-sm font-semibold text-white shadow-sm transition hover:brightness-110", primaryButtonSm: - "rounded-full bg-gradient-to-r from-fuchsia-600 via-indigo-600 to-cyan-600 px-4 py-1.5 text-sm font-semibold text-white shadow-sm transition hover:brightness-110", + "rounded-full bg-gradient-to-r from-violet-600 to-indigo-500 px-4 py-1.5 text-sm font-semibold text-white shadow-sm transition hover:brightness-110", secondaryButton: - "rounded-full border border-zinc-300/80 bg-white/70 px-6 py-2 text-sm font-semibold text-zinc-950 shadow-sm backdrop-blur transition hover:border-zinc-400 hover:bg-white", + "rounded-full border border-violet-200 bg-white px-6 py-2 text-sm font-semibold text-violet-900 shadow-sm transition hover:border-violet-300 hover:bg-violet-50", pillLink: - "rounded-full px-3 py-1.5 text-sm font-medium text-zinc-800 transition hover:bg-zinc-950/5 hover:text-zinc-950", + "rounded-full px-3 py-1.5 text-sm font-medium text-slate-700 transition hover:bg-violet-50 hover:text-violet-900", + // Insight box (purple left border style) + insightBox: + "rounded-xl border-l-4 border-l-violet-500 bg-violet-50 px-5 py-4", + insightLabel: "text-xs font-semibold uppercase tracking-[0.3em] text-violet-600", + // Progress bars + progressBar: "h-1.5 rounded-full bg-gradient-to-r from-violet-500 to-indigo-500", + progressTrack: "h-1.5 w-full rounded-full bg-slate-100", + // Badges + badge: "rounded-full bg-violet-100 px-3 py-1 text-xs font-medium text-violet-700", + badgeOutline: "rounded-full border border-violet-200 bg-white px-3 py-1 text-xs font-medium text-violet-700", }; diff --git a/apps/web/src/lib/vcp/index.ts b/apps/web/src/lib/vcp/index.ts new file mode 100644 index 0000000..9f29696 --- /dev/null +++ b/apps/web/src/lib/vcp/index.ts @@ -0,0 +1,24 @@ +/** + * VCP Utilities + * + * Re-exports all VCP-related utility functions. + */ + +export { + computeStrongestAxis, + formatStrongestAxis, + computeStyleDescriptor, + computeRhythmLabel, + computePeakLabel, + analyzePeakWindow, + computeShareCardMetrics, + formatConfidence, + computeConsistencyLabel, + type StrongestAxis, + type StyleDescriptor, + type RhythmLabel, + type PeakWindow, + type PeakLabel, + type ShareCardMetrics, + type ConsistencyLabel, +} from "./metrics"; diff --git a/apps/web/src/lib/vcp/metrics.ts b/apps/web/src/lib/vcp/metrics.ts new file mode 100644 index 0000000..2662748 --- /dev/null +++ b/apps/web/src/lib/vcp/metrics.ts @@ -0,0 +1,263 @@ +/** + * VCP Metric Utilities + * + * Functions for computing ShareCard metrics from vibe data. + * Used by both Unified VCP and Repo VCP share sections. + */ + +import type { VibeAxes, Level } from "@vibed/core"; +import { AXIS_METADATA, AXIS_ORDER } from "@/components/vcp/constants"; +import type { AxisKey } from "@/components/vcp/types"; + +// ============================================================================= +// Metric 1: Strongest Axis +// ============================================================================= + +export interface StrongestAxis { + key: AxisKey; + name: string; + score: number; +} + +/** + * Compute the strongest (highest scoring) axis from vibe axes. + */ +export function computeStrongestAxis(axes: VibeAxes): StrongestAxis { + let strongest: StrongestAxis = { + key: "automation_heaviness", + name: AXIS_METADATA.automation_heaviness.name, + score: axes.automation_heaviness.score, + }; + + for (const key of AXIS_ORDER) { + const axis = axes[key]; + if (axis.score > strongest.score) { + strongest = { + key, + name: AXIS_METADATA[key].name, + score: axis.score, + }; + } + } + + return strongest; +} + +/** + * Format strongest axis for display (e.g., "Automation 78") + */ +export function formatStrongestAxis(axes: VibeAxes): string { + const { name, score } = computeStrongestAxis(axes); + return `${name} ${score}`; +} + +// ============================================================================= +// Metric 2: Style Descriptor +// ============================================================================= + +export type StyleDescriptor = + | "Fast Builder" + | "Safe Shipper" + | "Rapid Cycler" + | "Steady Hand" + | "Bold Mover" + | "Deep Planner" + | "Wide Scoper" + | "Balanced"; + +/** + * Compute a human-readable style descriptor from axis scores. + * + * Returns a memorable label that captures the user's overall approach. + */ +export function computeStyleDescriptor(axes: VibeAxes): StyleDescriptor { + const A = axes.automation_heaviness.score; + const B = axes.guardrail_strength.score; + const C = axes.iteration_loop_intensity.score; + const D = axes.planning_signal.score; + const E = axes.surface_area_per_change.score; + const F = axes.shipping_rhythm.score; + + // High automation + high iteration = Fast Builder + if (A >= 70 && C >= 65) return "Fast Builder"; + + // High guardrails + decent automation = Safe Shipper + if (B >= 70 && A >= 50) return "Safe Shipper"; + + // High rhythm + high iteration = Rapid Cycler + if (F >= 70 && C >= 60) return "Rapid Cycler"; + + // High guardrails + low iteration = Steady Hand + if (B >= 65 && C < 40) return "Steady Hand"; + + // High automation + low guardrails = Bold Mover + if (A >= 60 && B < 40) return "Bold Mover"; + + // High planning = Deep Planner + if (D >= 70) return "Deep Planner"; + + // High surface area = Wide Scoper + if (E >= 70) return "Wide Scoper"; + + // Default + return "Balanced"; +} + +// ============================================================================= +// Metric 3: Rhythm Label +// ============================================================================= + +export type RhythmLabel = "Bursty" | "Steady" | "Mixed"; + +/** + * Compute a rhythm label from shipping rhythm axis. + */ +export function computeRhythmLabel(axes: VibeAxes): RhythmLabel { + const rhythmScore = axes.shipping_rhythm.score; + const level = axes.shipping_rhythm.level; + + if (level === "high" || rhythmScore >= 65) return "Bursty"; + if (level === "low" || rhythmScore < 35) return "Steady"; + return "Mixed"; +} + +// ============================================================================= +// Metric 4: Peak Time +// ============================================================================= + +export type PeakWindow = "mornings" | "afternoons" | "evenings" | "late_nights" | null; +export type PeakLabel = "Mornings" | "Afternoons" | "Evenings" | "Late Nights" | "Varied"; + +/** + * Compute a peak time label from timing analysis. + * + * @param peakWindow - The peak window from analysis insights, or null if not available + */ +export function computePeakLabel(peakWindow: PeakWindow): PeakLabel { + switch (peakWindow) { + case "mornings": + return "Mornings"; + case "afternoons": + return "Afternoons"; + case "evenings": + return "Evenings"; + case "late_nights": + return "Late Nights"; + default: + return "Varied"; + } +} + +/** + * Analyze commit timestamps to determine peak working window. + * + * @param commitTimestamps - Array of ISO date strings + * @returns The most common time window, or null if insufficient data + */ +export function analyzePeakWindow(commitTimestamps: string[]): PeakWindow { + if (commitTimestamps.length < 10) return null; + + const windowCounts = { + mornings: 0, // 6am - 12pm + afternoons: 0, // 12pm - 6pm + evenings: 0, // 6pm - 10pm + late_nights: 0, // 10pm - 6am + }; + + for (const ts of commitTimestamps) { + const date = new Date(ts); + const hour = date.getHours(); + + if (hour >= 6 && hour < 12) { + windowCounts.mornings++; + } else if (hour >= 12 && hour < 18) { + windowCounts.afternoons++; + } else if (hour >= 18 && hour < 22) { + windowCounts.evenings++; + } else { + windowCounts.late_nights++; + } + } + + // Find the dominant window + const entries = Object.entries(windowCounts) as [PeakWindow, number][]; + const sorted = entries.sort((a, b) => (b[1] ?? 0) - (a[1] ?? 0)); + const top = sorted[0]; + const second = sorted[1]; + + if (!top || !second) return null; + + // Only return if there's a clear winner (>30% more than second place) + if (top[1] > second[1] * 1.3) { + return top[0]; + } + + return null; +} + +// ============================================================================= +// Complete ShareCard Metrics +// ============================================================================= + +export interface ShareCardMetrics { + strongest: string; // "Automation 78" + style: StyleDescriptor; // "Fast Builder" + rhythm: RhythmLabel; // "Bursty" + peak: PeakLabel; // "Afternoons" +} + +/** + * Compute all four ShareCard metrics. + */ +export function computeShareCardMetrics( + axes: VibeAxes, + peakWindow: PeakWindow = null +): ShareCardMetrics { + return { + strongest: formatStrongestAxis(axes), + style: computeStyleDescriptor(axes), + rhythm: computeRhythmLabel(axes), + peak: computePeakLabel(peakWindow), + }; +} + +// ============================================================================= +// Confidence Formatting +// ============================================================================= + +/** + * Format confidence level for display. + */ +export function formatConfidence(confidence: string): string { + switch (confidence) { + case "high": + return "High Confidence"; + case "medium": + return "Medium Confidence"; + case "low": + return "Low Confidence"; + default: + return confidence; + } +} + +// ============================================================================= +// Consistency/Stability Label +// ============================================================================= + +export type ConsistencyLabel = "Steady" | "New" | string; + +/** + * Compute consistency label based on persona history. + * + * @param personaShifts - Number of times the persona changed + * @param totalSnapshots - Total number of analysis snapshots + */ +export function computeConsistencyLabel( + personaShifts: number, + totalSnapshots: number +): ConsistencyLabel { + if (totalSnapshots < 2) return "New"; + if (personaShifts === 0) return "Steady"; + return `${personaShifts} Shift${personaShifts > 1 ? "s" : ""}`; +} diff --git a/apps/web/src/lib/vcp/validators.ts b/apps/web/src/lib/vcp/validators.ts new file mode 100644 index 0000000..5c63290 --- /dev/null +++ b/apps/web/src/lib/vcp/validators.ts @@ -0,0 +1,25 @@ +import type { Level, VibeAxes } from "@vibed/core"; + +const AXIS_KEYS: (keyof VibeAxes)[] = [ + "automation_heaviness", + "guardrail_strength", + "iteration_loop_intensity", + "planning_signal", + "surface_area_per_change", + "shipping_rhythm", +]; + +function isAxisValue(v: unknown): v is { score: number; level: Level; why: string[] } { + if (!v || typeof v !== "object") return false; + const record = v as Record; + if (typeof record.score !== "number") return false; + if (typeof record.level !== "string") return false; + if (!Array.isArray(record.why)) return false; + return record.why.every((item) => typeof item === "string"); +} + +export function isVibeAxes(v: unknown): v is VibeAxes { + if (!v || typeof v !== "object") return false; + const record = v as Record; + return AXIS_KEYS.every((key) => isAxisValue(record[key])); +} diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index 8aeca93..a63e6cf 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -50,7 +50,7 @@ export async function proxy(request: NextRequest) { } const path = request.nextUrl.pathname; - const protectedPrefixes = ["/repos", "/repo", "/analysis", "/profile"]; + const protectedPrefixes = ["/repos", "/repo", "/analysis", "/profile", "/vibes", "/settings"]; const isProtected = protectedPrefixes.some( (p) => path === p || path.startsWith(`${p}/`) diff --git a/apps/worker/CHANGELOG-DEVELOP.md b/apps/worker/CHANGELOG-DEVELOP.md index e0583d7..e3904ca 100644 --- a/apps/worker/CHANGELOG-DEVELOP.md +++ b/apps/worker/CHANGELOG-DEVELOP.md @@ -1,5 +1,12 @@ # Changelog +## [0.1.0-alpha.2](https://github.com/devakone/vibe-coding-profile/compare/worker-v0.1.0-alpha.1...worker-v0.1.0-alpha.2) (2026-01-22) + + +### Features + +* **web:** enhance sharing features with LLM-generated taglines ([ef3b589](https://github.com/devakone/vibe-coding-profile/commit/ef3b589a6cdf71485c2ebef11f20a4692a49be13)) + ## [0.1.0-alpha.1](https://github.com/devakone/vibed-coding/compare/worker-v0.1.0-alpha.0...worker-v0.1.0-alpha.1) (2026-01-20) diff --git a/apps/worker/package.json b/apps/worker/package.json index 8c7a7de..a1051e8 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -1,6 +1,6 @@ { "name": "@vibed/worker", - "version": "0.1.1", + "version": "0.1.0-alpha.2", "private": true, "scripts": { "dev": "tsx watch src/index.ts", diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 595bdbd..2b342b7 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -243,6 +243,7 @@ function computeEpisodeSummary(events: CommitEvent[]): Array<{ interface NarrativeResult { narrative: AnalysisReport["narrative"]; model: string; + tagline: string | null; } /** @@ -292,8 +293,9 @@ async function generateNarrativeWithLLM(params: { "Never infer intent, skill, or code quality. Avoid speculation and motivational language.", "Every claim must cite at least one specific metric name and value (e.g. burstiness_score=0.42) or a specific commit subject line provided.", "Each section must include evidence: 2-6 commit SHAs that support the section.", + "Provide a concise tagline (<=60 characters) that describes the developer's vibe. This tagline will be displayed on share cards and should remain observational.", "Output must be STRICT JSON with this schema:", - '{"summary":"...","sections":[{"title":"...","content":"...","evidence":["sha", "..."]}],"highlights":[{"metric":"...","value":"...","interpretation":"..."}]}', + '{"summary":"...","tagline":"...","sections":[{"title":"...","content":"...","evidence":["sha", "..."]}],"highlights":[{"metric":"...","value":"...","interpretation":"..."}]}', ].join("\n"); const userPrompt = [ @@ -375,6 +377,7 @@ async function generateNarrativeWithLLM(params: { const obj = parsed as { summary?: unknown; + tagline?: unknown; sections?: unknown; highlights?: unknown; }; @@ -407,9 +410,14 @@ async function generateNarrativeWithLLM(params: { } if (!highlightsValid) continue; + // Enforce 60-char limit as specified in the prompt + const rawTagline = typeof obj.tagline === "string" ? obj.tagline.trim() : null; + const tagline = rawTagline ? rawTagline.slice(0, 60) : null; + return { narrative: { summary: obj.summary, sections, highlights }, model, + tagline, }; } catch (error) { console.warn(`LLM model ${model} failed, trying next:`, error); @@ -567,6 +575,12 @@ async function processJob(jobId: string, config: WorkerConfig): Promise { ); if (reportError) throw new Error(`Failed to upsert report: ${reportError.message}`); + // Authoritative tagline: LLM-generated if available, else persona description + const personaTaglineFallback = insights.persona.description ?? ""; + const llmTagline = llmResult?.tagline?.trim(); + const finalTagline = llmTagline?.length ? llmTagline : personaTaglineFallback; + insights.share_template.tagline = finalTagline; + const { error: insightsError } = await supabase.from("analysis_insights").upsert( { job_id: jobId, @@ -577,6 +591,7 @@ async function processJob(jobId: string, config: WorkerConfig): Promise { persona_confidence: insights.persona.confidence, tech_signals: insights.tech_signals as unknown as Json, share_template: insights.share_template as unknown as Json, + tagline: finalTagline, persona_delta: insights.persona_delta as unknown as Json, sources: insights.sources, }, diff --git a/docs/implementation-trackers/information-architecture-restructure.md b/docs/implementation-trackers/information-architecture-restructure.md new file mode 100644 index 0000000..893e7c0 --- /dev/null +++ b/docs/implementation-trackers/information-architecture-restructure.md @@ -0,0 +1,447 @@ +# Implementation Tracker: Information Architecture Restructure + +**PRD Reference:** `docs/prd/ux/information-architecture-restructure.md` +**Status:** In Progress +**Last Updated:** 2026-01-22 + +--- + +## Overview + +This tracker covers all implementation work for the Information Architecture Restructure, including: +- Navigation and route changes +- VCP component unification +- ShareCard redesign (metrics, tagline, vertical format) +- Notification system + +--- + +## Phase Dependencies + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ FOUNDATIONAL (Sequential) │ +│ │ +│ F1: VCP Primitives ──► F2: VCP Composite Blocks ──► F3: Shared Utilities │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ PARALLEL WORKSTREAMS │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │ +│ │ P1: ShareCard │ │ P2: Navigation │ │ P3: VCP Display Unification │ │ +│ │ Redesign │ │ & Routes │ │ │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ P4: Notification│ │ P5: Vertical │ │ +│ │ System │ │ Stories Card │ │ +│ └─────────────────┘ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ FINALIZATION │ +│ │ +│ X1: Migration & Redirects ──► X2: Polish & Testing ──► X3: Documentation │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Foundational Phases (Sequential) + +### F1. VCP Primitives + +**Status:** `[x] Complete` +**Depends on:** None +**Blocks:** F2, P1, P3 + +| Task | Status | File(s) | +|------|--------|---------| +| Create `components/vcp/primitives/` directory | `[x]` | — | +| Implement `VCPCard` | `[x]` | `primitives/VCPCard.tsx` | +| Implement `VCPSection` | `[x]` | `primitives/VCPSection.tsx` | +| Implement `VCPSectionTitle` | `[x]` | `primitives/VCPSectionTitle.tsx` | +| Implement `VCPStatCard` | `[x]` | `primitives/VCPStatCard.tsx` | +| Implement `VCPProgressBar` | `[x]` | `primitives/VCPProgressBar.tsx` | +| Implement `VCPBadge` | `[x]` | `primitives/VCPBadge.tsx` | +| Implement `VCPCollapsible` | `[x]` | `primitives/VCPCollapsible.tsx` | +| Implement `VCPInsightBox` | `[x]` | `primitives/VCPInsightBox.tsx` | +| Create shared types | `[x]` | `vcp/types.ts` | +| Create constants (AXIS_METADATA, etc.) | `[x]` | `vcp/constants.ts` | +| Create barrel export | `[x]` | `primitives/index.ts` | + +**Success Criteria:** +- [x] All primitives render correctly in isolation +- [x] Types are properly exported +- [x] Consistent styling matches PRD specifications + +--- + +### F2. VCP Composite Blocks + +**Status:** `[x] Complete` +**Depends on:** F1 +**Blocks:** P1, P3 + +| Task | Status | File(s) | +|------|--------|---------| +| Create `components/vcp/blocks/` directory | `[x]` | — | +| Implement `VCPIdentityHeader` | `[x]` | `blocks/VCPIdentityHeader.tsx` | +| Implement `VCPAxesGrid` | `[x]` | `blocks/VCPAxesGrid.tsx` | +| Implement `VCPStatsGrid` | `[x]` | `blocks/VCPStatsGrid.tsx` | +| Implement `VCPNarrativeSection` | `[x]` | `blocks/VCPNarrativeSection.tsx` | +| Implement `VCPMatchedSignals` | `[x]` | `blocks/VCPMatchedSignals.tsx` | +| Implement `VCPMethodologyLink` | `[x]` | `blocks/VCPMethodologyLink.tsx` | +| Implement `VCPVersionHistory` | `[x]` | `blocks/VCPVersionHistory.tsx` | +| Implement `VCPFooter` | `[x]` | `blocks/VCPFooter.tsx` | +| Create barrel export | `[x]` | `blocks/index.ts` | + +**Success Criteria:** +- [x] Blocks compose primitives correctly +- [x] `VCPVersionHistory` works for both Unified and Repo variants +- [x] All blocks accept proper TypeScript interfaces + +--- + +### F3. Shared Utilities + +**Status:** `[x] Complete` +**Depends on:** None (can run parallel with F1/F2) +**Blocks:** P1, P5 + +| Task | Status | File(s) | +|------|--------|---------| +| Implement `computeStrongestAxis()` | `[x]` | `lib/vcp/metrics.ts` | +| Implement `computeStyleDescriptor()` | `[x]` | `lib/vcp/metrics.ts` | +| Implement `computeRhythmLabel()` | `[x]` | `lib/vcp/metrics.ts` | +| Implement `computePeakLabel()` | `[x]` | `lib/vcp/metrics.ts` | +| Add unit tests for metric functions | `[ ]` | `__tests__/metrics.test.ts` | + +**Success Criteria:** +- [x] All metric computation functions work correctly +- [x] Edge cases handled (missing data, null values) +- [ ] Tests pass (tests not yet written) + +--- + +## Parallel Workstreams + +### P1. ShareCard Redesign + +**Status:** `[x] Complete` +**Depends on:** F1, F2, F3 +**Blocks:** P5 + +| Task | Status | File(s) | +|------|--------|---------| +| Update `ShareCardMetric` interface | `[x]` | `components/share/types.ts` | +| Add `tagline` prop to `ShareCardProps` | `[x]` | `components/share/types.ts` | +| Refactor `ShareCard` with new metrics layout | `[x]` | `components/share/ShareCard.tsx` | +| Add tagline row to `ShareCard` | `[x]` | `components/share/ShareCard.tsx` | +| Update footer layout (NEXT_PUBLIC_APP_URL + context) | `[x]` | `ProfileShareSection.tsx`, `AnalysisClient.tsx` | +| Update `ProfileShareSection` metric computation | `[x]` | `components/share/ProfileShareSection.tsx` | +| Update Repo VCP share section metric computation | `[x]` | `app/analysis/[jobId]/AnalysisClient.tsx` | + +**Success Criteria:** +- ShareCard displays: Strongest, Style, Rhythm, Peak +- Tagline row renders when provided +- Footer shows hostname from `NEXT_PUBLIC_APP_URL` + repos/commits context + +--- + +### P2. Navigation & Routes + +**Status:** `[x] Complete` +**Depends on:** None +**Blocks:** X1 + +| Task | Status | File(s) | +|------|--------|---------| +| Rename "Reports" to "Vibes" in nav | `[x]` | `app/AppHeader.tsx` | +| Create `/vibes` route | `[x]` | `app/vibes/page.tsx`, `app/vibes/VibesClient.tsx` | +| Create `/vibes/[repoId]` route | `[-]` | Deferred - using expandable table instead | +| Create `/vibes/[repoId]/[jobId]` route | `[-]` | Deferred - linking to existing `/analysis/[jobId]` | +| Create `/settings/repos` route | `[x]` | `app/settings/repos/page.tsx`, `app/settings/repos/RepoSettingsClient.tsx` | +| Move repo connect/disconnect to Settings | `[x]` | `app/settings/repos/RepoSettingsClient.tsx` | +| Implement `VibesTable` component | `[x]` | `app/vibes/VibesClient.tsx` (expandable table) | +| Update navigation labels | `[x]` | `app/AppHeader.tsx` | + +**Success Criteria:** +- [x] New routes are functional +- [x] Navigation reflects new terminology (My Vibe, Vibes, Settings) +- [x] Repo management is in Settings + +--- + +### P3. VCP Display Unification + +**Status:** `[x] Complete` +**Depends on:** F1, F2 +**Blocks:** X2 + +| Task | Status | File(s) | +|------|--------|---------| +| Create `components/vcp/unified/` directory | `[x]` | — | +| Implement `UnifiedIdentitySection` | `[x]` | `unified/UnifiedIdentitySection.tsx` | +| Implement `UnifiedInsightSection` | `[x]` | `unified/UnifiedInsightSection.tsx` | +| Implement `UnifiedAxesSection` | `[x]` | `unified/UnifiedAxesSection.tsx` | +| Implement `EvolutionSection` | `[x]` | `unified/EvolutionSection.tsx` | +| Implement `RepoBreakdownSection` | `[x]` | `unified/RepoBreakdownSection.tsx` | +| Implement `UnifiedMethodologySection` | `[x]` | `unified/UnifiedMethodologySection.tsx` | +| Create `components/vcp/repo/` directory | `[x]` | — | +| Implement `RepoIdentitySection` | `[x]` | `repo/RepoIdentitySection.tsx` | +| Implement `RepoMetricsGrid` | `[x]` | `repo/RepoMetricsGrid.tsx` | +| Implement `RepoAxesSection` | `[x]` | `repo/RepoAxesSection.tsx` | +| Implement `ProfileContributionCard` | `[x]` | `repo/ProfileContributionCard.tsx` | +| Refactor `app/page.tsx` to use new components | `[x]` | `app/page.tsx` | +| Refactor `AnalysisClient.tsx` to use new components | `[x]` | `app/analysis/[jobId]/AnalysisClient.tsx` | + +**Success Criteria:** +- Both VCPs use shared primitives and blocks +- Visual parity with current designs +- Reduced code duplication + +**Notes:** +- Components created for both Unified VCP and Repo VCP +- Component interfaces updated to match actual page data shapes (Option A approach) +- Light theme styling used for Unified VCP (zinc text, subtle backgrounds) +- Components exported via barrel exports in `components/vcp/index.ts` +- Page refactoring complete — components now drive Unified/Repo VCP screens + +--- + +### P4. Notification System + +**Status:** `[x] Complete` +**Depends on:** None +**Blocks:** X2 + +| Task | Status | File(s) | +|------|--------|---------| +| Create `NotificationDropdown` component | `[x]` | `components/notifications/NotificationDropdown.tsx` | +| Create notification badge component | `[x]` | `components/notifications/NotificationBadge.tsx` | +| Integrate with `JobsContext` | `[x]` | `components/notifications/NotificationDropdown.tsx` | +| Add to navigation header | `[x]` | `app/AppHeader.tsx` | +| Implement "Clear all" functionality | `[x]` | `components/notifications/NotificationDropdown.tsx` | +| Remove Jobs tab from Reports page | `[x]` | N/A (Jobs tab doesn't exist as separate component) | + +**Success Criteria:** +- [x] Notifications show in-progress and completed jobs +- [x] Badge shows unread count +- [x] Clicking notification navigates to VCP + +--- + +### P5. Vertical Stories ShareCard + +**Status:** `[~] In Progress` +**Depends on:** P1, F3 +**Blocks:** X2 + +| Task | Status | File(s) | +|------|--------|---------| +| Create API route for story image generation | `[x]` | `app/api/share/story/[userId]/route.tsx` | +| Implement vertical layout with `@vercel/og` | `[x]` | `app/api/share/story/[userId]/route.tsx` | +| Add persona gradient themes | `[x]` | `apps/web/src/app/api/share/story/[userId]/route.tsx` | +| Integrate QR code generation | `[x]` | `apps/web/src/app/api/share/story/[userId]/route.tsx` | +| Add "Download for Stories" button to UI | `[x]` | `components/share/ProfileShareSection.tsx`, `components/share/ShareActions.tsx` | +| Add "Download for Stories" to Repo VCP | `[x]` | `app/analysis/[jobId]/AnalysisClient.tsx`, `components/share/ShareActions.tsx` | +| Test on various devices/platforms | `[ ]` | — | + +**Success Criteria:** +- Generates 1080x1920 PNG image +- QR code links to public profile +- Download works on desktop and mobile +- Renders correctly when shared to Instagram/TikTok + +--- + +### P6. LLM Tagline Generation + +**Status:** `[x] Complete` +**Depends on:** None +**Blocks:** P1 + +| Task | Status | File(s) | +|------|--------|---------| +| Add tagline prompt to LLM integration | `[x]` | `apps/web/src/inngest/functions/analyze-repo.ts`, `apps/worker/src/index.ts` | +| Add `tagline` field to analysis result schema | `[x]` | `supabase/migrations/0021_add_analysis_insights_tagline.sql`, `packages/db/src/database.types.ts`, `apps/web/src/inngest/functions/analyze-repo.ts` | +| Create migration for tagline column | `[x]` | `supabase/migrations/0021_add_analysis_insights_tagline.sql` | +| Generate tagline during VCP analysis | `[x]` | `apps/web/src/inngest/functions/analyze-repo.ts`, `apps/worker/src/index.ts` | +| Fallback to persona tagline if missing | `[x]` | `apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx`, `apps/web/src/components/share/share-image.ts`, `apps/web/src/components/share/ProfileShareSection.tsx` | + +**Success Criteria:** +- Tagline generated during analysis +- Max 60 characters +- Matches persona energy +- Stored and retrieved correctly + +--- + +## Finalization Phases (Sequential) + +### X1. Migration & Redirects + +**Status:** `[~] In Progress` +**Depends on:** P2 +**Blocks:** X2 + +| Task | Status | File(s) | +|------|--------|---------| +| Add redirect: `/reports` → `/vibes` | `[x]` | `next.config.ts` | +| Add redirect: `/reports/[id]` → `/analysis/[id]` | `[x]` | `next.config.ts` | +| Add redirect: `/repos` → `/settings/repos` | `[x]` | `next.config.ts` | +| Update all internal links | `[x]` | `app/analysis/AnalysisListClient.tsx` | +| Update email templates (if any) | `[ ]` | — | + +**Success Criteria:** +- Old URLs redirect correctly +- No broken links in the app +- External links (emails, docs) still work + +--- + +### X2. Polish & Testing + +**Status:** `[~] In Progress` +**Depends on:** P1, P3, P4, P5, X1 +**Blocks:** X3 + +| Task | Status | File(s) | +|------|--------|---------| +| Visual parity testing (Unified VCP) | `[ ]` | — | +| Visual parity testing (Repo VCP) | `[ ]` | — | +| Remove duplicated/dead code | `[ ]` | Various | +| Update component exports | `[ ]` | `index.ts` files | +| Cross-browser testing | `[ ]` | — | +| Mobile responsiveness testing | `[ ]` | — | +| Accessibility audit | `[ ]` | — | + +**Success Criteria:** +- No visual regressions +- All dead code removed +- Works on Chrome, Firefox, Safari +- Mobile-friendly + +--- + +### X3. Documentation + +**Status:** `[ ] Not Started` +**Depends on:** X2 +**Blocks:** None + +| Task | Status | File(s) | +|------|--------|---------| +| Update component documentation | `[ ]` | `docs/...` | +| Update route documentation | `[ ]` | `docs/...` | +| Update AGENTS.md if needed | `[ ]` | `AGENTS.md` | +| Archive old PRD sections (if applicable) | `[ ]` | — | +| Mark this tracker as complete | `[ ]` | This file | + +**Success Criteria:** +- Documentation reflects new architecture +- Agents.md updated with any new conventions + +--- + +## Summary + +| Phase | Tasks | Completed | Status | +|-------|-------|-----------|--------| +| F1: VCP Primitives | 12 | 12 | `[x] Complete` | +| F2: VCP Composite Blocks | 10 | 10 | `[x] Complete` | +| F3: Shared Utilities | 5 | 4 | `[~] In Progress` | +| P1: ShareCard Redesign | 7 | 7 | `[x] Complete` | +| P2: Navigation & Routes | 8 | 8 | `[x] Complete` | +| P3: VCP Display Unification | 14 | 14 | `[x] Complete` | +| P4: Notification System | 6 | 6 | `[x] Complete` | +| P5: Vertical Stories ShareCard | 7 | 6 | `[~] In Progress` | +| P6: LLM Tagline Generation | 5 | 5 | `[x] Complete` | +| X1: Migration & Redirects | 5 | 4 | `[~] In Progress` | +| X2: Polish & Testing | 7 | 0 | `[~] In Progress` | +| X3: Documentation | 5 | 0 | `[ ] Not Started` | +| **Total** | **91** | **68** | **75%** | + +--- + +## Notes + +- F1/F2/F3 are foundational and must complete before parallel workstreams +- P1-P6 can run in parallel once dependencies are met +- P6 (LLM Tagline) is a backend task that can start early +- X1/X2/X3 are finalization steps after parallel work completes + +--- + +## Changelog + +### 2026-01-22 +- **F1 Complete:** Implemented all VCP primitives (VCPCard, VCPSection, VCPStatCard, VCPProgressBar, VCPBadge, VCPCollapsible, VCPInsightBox) +- **F2 Complete:** Implemented all composite blocks (VCPIdentityHeader, VCPAxesGrid, VCPStatsGrid, VCPNarrativeSection, VCPMatchedSignals, VCPMethodologyLink, VCPFooter, VCPVersionHistory) +- **F3 In Progress:** Implemented metric utilities (computeStrongestAxis, computeStyleDescriptor, computeRhythmLabel, computePeakLabel, analyzePeakWindow). Unit tests pending. +- **P4 Complete:** Implemented notification system with: + - `NotificationDropdown` — Bell icon with popover showing job activity + - `NotificationBadge` — Gradient badge showing unread count + - Integrated into `AppHeader` (replaces old badge-on-link pattern) + - "Mark all read" functionality built into dropdown header + - Shows in-progress jobs with animated indicator, completed jobs with checkmarks +- **P2 Complete:** Implemented navigation and route changes: + - Created `/vibes` route with `VibesClient` — expandable table showing VCPs organized by repo + - Created `/settings/repos` route with `RepoSettingsClient` — repo connect/disconnect moved from `/repos` + - Updated `AppHeader` navigation: "My VCP", "Repo VCPs" (final naming) + - Added settings tabs (LLM Keys / Repos) for consistent settings navigation + - Deferred `/vibes/[repoId]` routes — using expandable rows and existing `/analysis/[jobId]` links instead + - Clicking completed job navigates to VCP and marks as read +- **P1 Complete:** ShareCard redesign fully implemented: + - Updated `ShareCardMetric` interface with optional `detail` property + - Added `tagline?: string | null` prop to `ShareCardProps` + - Added tagline row rendering in `ShareCard.tsx` + - Updated `ProfileShareSection` to use `computeShareCardMetrics()` with new metrics (Strongest, Style, Rhythm, Peak) + - Updated `AnalysisClient.tsx` (Repo VCP) to use `computeShareCardMetrics()` with new metrics + - Footer now uses `NEXT_PUBLIC_APP_URL` hostname + repo/commit context +- **Documentation Updated:** Updated ShareCard metrics documentation in: + - `docs/prd/ux/share-experience-improvements.md` — Added Appendix B with metrics specification + - `docs/prd/ux/information-architecture-restructure.md` — Added Section 4.3 for ShareCard metrics + - Updated mockups to show new metrics layout (Strongest, Style, Rhythm, Peak) +- **P3 Complete:** VCP Display Unification components created: + - Created `components/vcp/unified/` with 6 components: + - `UnifiedIdentitySection` — Main identity header with persona, tagline, stats + - `UnifiedInsightSection` — LLM narrative display with violet accent + - `UnifiedAxesSection` — 6-axis grid for Unified VCP + - `EvolutionSection` — Repo VCP count, vibe shifts, dominant vibe stats + - `RepoBreakdownSection` — Per-repo contribution breakdown + - `UnifiedMethodologySection` — Collapsible methodology/how we got this +- **P6 Complete:** LLM-generated taglines now feed share cards and analytics: + - Inngest worker and standalone worker prompts emit a `tagline` string as part of the narrative JSON. + - `analysis_insights` gained a `tagline` column via `supabase/migrations/0021_add_analysis_insights_tagline.sql` and the Supabase types were updated. + - Share cards and story exports use `shareTemplate.tagline` with persona fallback plus updated share-image tooling for the tagline row. + - Created `components/vcp/repo/` with 4 components: + - `RepoIdentitySection` — Repo VCP identity with methodology collapsible + - `RepoMetricsGrid` — 5-column metrics (Streak, Peak Day, Focus, Build vs Fix, Scope) + - `RepoAxesSection` — 6-axis grid for Repo VCP + - `ProfileContributionCard` — Shows repo contribution to unified profile + - Updated `components/vcp/index.ts` to export unified/ and repo/ components + - Page refactoring complete — components integrated into Unified/Repo pages + +### 2026-01-22 (continued) +- **P4 Fix:** Updated `NotificationDropdown` footer link from `/analysis` to `/vibes` +- **Type Errors Fixed:** + - `ShareActions.tsx`: Added missing `storyEndpoint` prop destructuring + - `page.tsx`: Added `userId` prop to `AuthenticatedDashboard` to fix scope issue + - `route.tsx`: Fixed Supabase client type with `any` cast +- **Route Updates:** Updated `/repos` and `/analysis` links in `page.tsx` to use `/settings/repos` and `/vibes` +- **P3 Component Interface Updates (Option A):** + - `UnifiedMethodologySection`: Changed to accept `matchedRules: string[]` and `caveats: string[]` directly from `detectVibePersona` output + - `UnifiedIdentitySection`: Added `analyzedRepos` and `analyzedCommits` props for "Profile forming" state + - `EvolutionSection`: Changed `vibeShifts` to accept `string | number` for "New"/"Steady" values +- Components now compatible with page data — ready for page refactoring +- **P3 Complete:** Unified/Repo VCP pages now use shared components: + - `app/page.tsx`: Replaced inline Unified VCP sections with unified components + - `AnalysisClient.tsx`: Swapped Repo VCP identity/metrics/contribution sections to repo components diff --git a/docs/prd/multi-agent-detection.md b/docs/prd/analysis/multi-agent-detection.md similarity index 94% rename from docs/prd/multi-agent-detection.md rename to docs/prd/analysis/multi-agent-detection.md index 958bf75..84be5cd 100644 --- a/docs/prd/multi-agent-detection.md +++ b/docs/prd/analysis/multi-agent-detection.md @@ -25,7 +25,7 @@ Implement three high-ROI detection capabilities that require no new data ingesti ## Research Summary -See: [multi-agent-vibe-coding-patterns.md](../research/multi-agent-vibe-coding-patterns.md) +See: [multi-agent-vibe-coding-patterns.md](../../research/multi-agent-vibe-coding-patterns.md) ### What We Have Today | Capability | Status | Location | @@ -220,7 +220,7 @@ These are documented but out of scope for this PRD: 2. **Branch topology** - parallel branches, naming conventions 3. **Commit-to-PR mapping** - squash detection, PR hygiene signals -See architecture spec: [vibe-metrics-v2.md](../architecture/vibe-metrics-v2.md) +See architecture spec: [vibe-metrics-v2.md](../../architecture/vibe-metrics-v2.md) --- @@ -251,7 +251,7 @@ Ingest enough pull request metadata to strengthen multi-agent orchestration dete ### Proposed Schema **Primary tables** - `pull_requests` - - Align with architecture sketch in [vibe-metrics-v2.md](../architecture/vibe-metrics-v2.md) + - Align with architecture sketch in [vibe-metrics-v2.md](../../architecture/vibe-metrics-v2.md) - Key columns: `repo_id`, `github_pr_number`, `title`, `body`, `state`, `merged`, `merged_at`, `created_at`, `closed_at`, size and collaboration counts, `merge_method` **Optional tables** @@ -264,14 +264,14 @@ Ingest enough pull request metadata to strengthen multi-agent orchestration dete - Run ingestion as part of analysis job execution, before computing `AnalysisInsights` ### Implementation Tracker (vNext) -See: [PR Metadata Ingestion Tracker](../implementation-trackers/multi-agent-pr-metadata-ingestion.md) +See: [PR Metadata Ingestion Tracker](../../implementation-trackers/multi-agent-pr-metadata-ingestion.md) --- ## References -- [Research: Multi-Agent Vibe Coding Patterns](../research/multi-agent-vibe-coding-patterns.md) -- [Architecture: Vibe Metrics v2](../architecture/vibe-metrics-v2.md) +- [Research: Multi-Agent Vibe Coding Patterns](../../research/multi-agent-vibe-coding-patterns.md) +- [Architecture: Vibe Metrics v2](../../architecture/vibe-metrics-v2.md) - [Addy Osmani: The future of agentic coding (conductors to orchestrators)](https://addyosmani.com/blog/future-agentic-coding/) - [GitHub Copilot Coding Agent](https://docs.github.com/en/copilot/concepts/coding-agent/coding-agent) - [Git Worktrees for AI Agents](https://nx.dev/blog/git-worktrees-ai-agents) diff --git a/docs/PRD-vibed.md b/docs/prd/core/PRD-vibed.md similarity index 100% rename from docs/PRD-vibed.md rename to docs/prd/core/PRD-vibed.md diff --git a/docs/PRD.md b/docs/prd/core/PRD.md similarity index 99% rename from docs/PRD.md rename to docs/prd/core/PRD.md index be1a2fd..5e4fb53 100644 --- a/docs/PRD.md +++ b/docs/prd/core/PRD.md @@ -5,11 +5,11 @@ **Status:** Draft v4 (Monorepo architecture) — Core features implemented **Audience:** Product, Engineering (human + AI agents) -> **Terminology Note:** This PRD uses the original "Bolokono" terminology. In the codebase and user-facing product, this has been rebranded to **Vibe Coding Profile**. "Bolokono types" are now called **Vibe Personas**, and "Bolokono profiles" are now **Vibe Coding Profiles (VCP)**. See `docs/PRD-vibed-ux.md` for the current UX direction. +> **Terminology Note:** This PRD uses the original "Bolokono" terminology. In the codebase and user-facing product, this has been rebranded to **Vibe Coding Profile**. "Bolokono types" are now called **Vibe Personas**, and "Bolokono profiles" are now **Vibe Coding Profiles (VCP)**. See [`PRD-vibed-ux.md`](../ux/PRD-vibed-ux.md) for the current UX direction. > **Reference Documentation:** For a complete explanation of how analysis works, see: -> - [How Vibe Coding Profile Works](./how-vibed-works.md) — Product-friendly overview -> - [Vibe Coding Profile Analysis Pipeline](./architecture/vibed-analysis-pipeline.md) — Technical architecture with diagrams +> - [How Vibe Coding Profile Works](../../how-vibed-works.md) — Product-friendly overview +> - [Vibe Coding Profile Analysis Pipeline](../../architecture/vibed-analysis-pipeline.md) — Technical architecture with diagrams --- diff --git a/docs/prd-llm-provider-abstraction.md b/docs/prd/infrastructure/prd-llm-provider-abstraction.md similarity index 100% rename from docs/prd-llm-provider-abstraction.md rename to docs/prd/infrastructure/prd-llm-provider-abstraction.md diff --git a/docs/prd-multi-platform-integration.md b/docs/prd/platform/prd-multi-platform-integration.md similarity index 100% rename from docs/prd-multi-platform-integration.md rename to docs/prd/platform/prd-multi-platform-integration.md diff --git a/docs/PRD-profile-aggregation.md b/docs/prd/profile/PRD-profile-aggregation.md similarity index 99% rename from docs/PRD-profile-aggregation.md rename to docs/prd/profile/PRD-profile-aggregation.md index fad8298..5b6aa94 100644 --- a/docs/PRD-profile-aggregation.md +++ b/docs/prd/profile/PRD-profile-aggregation.md @@ -11,7 +11,7 @@ We built personas per repo. The PRDs say personas should aggregate per user. ### What the PRDs Said -From `PRD-vibed-ux.md`: +From [`PRD-vibed-ux.md`](../ux/PRD-vibed-ux.md): > "Vibe Coding Profile builds a profile of your AI-era coding style **across projects**. One repo gives you a Repo VCP. Three repos show patterns. Five repos reveal your Unified VCP when you're building with AI tools." diff --git a/docs/PRD-vibed-ux.md b/docs/prd/ux/PRD-vibed-ux.md similarity index 99% rename from docs/PRD-vibed-ux.md rename to docs/prd/ux/PRD-vibed-ux.md index c4ccc8e..e8b62e2 100644 --- a/docs/PRD-vibed-ux.md +++ b/docs/prd/ux/PRD-vibed-ux.md @@ -989,4 +989,4 @@ Short code resolves to full vibe page with optional "shared view" mode (read-onl --- -*This PRD focuses on the user experience layer. See `PRD-vibed.md` for technical implementation details including database schema, API endpoints, and worker logic.* +*This PRD focuses on the user experience layer. See [`PRD-vibed.md`](../core/PRD-vibed.md) for technical implementation details including database schema, API endpoints, and worker logic.* diff --git a/docs/prd/ux/information-architecture-restructure.md b/docs/prd/ux/information-architecture-restructure.md new file mode 100644 index 0000000..772a442 --- /dev/null +++ b/docs/prd/ux/information-architecture-restructure.md @@ -0,0 +1,518 @@ +# PRD: Information Architecture Restructure + +**Status:** Draft +**Author:** Claude (AI Assistant) +**Date:** 2026-01-21 +**Version:** 1.0 + +--- + +## 1. Overview + +### 1.1 Problem Statement + +The current information architecture has several issues: + +1. **Naming confusion**: "Reports" is a misnomer - these are analyses that generate Vibe Coding Profiles (VCPs), not reports +2. **Flat organization**: All VCPs displayed as a flat grid regardless of which repo they belong to +3. **Page overlap**: The Repos and Reports pages have overlapping concerns and cross-related functionality +4. **Jobs as a page**: The Jobs tab takes up prime real estate for transient data that would be better served as notifications +5. **Inconsistent VCP display**: Repo VCPs and Unified VCPs use different display patterns and terminology + +### 1.2 Goals + +- Simplify the navigation by consolidating related functionality +- Organize VCP history by repository for better discoverability +- Convert jobs from a page/tab to a notification system +- Unify the display and terminology of Repo VCPs and Unified VCPs +- Move repo management (connect/disconnect) to Settings where it belongs + +### 1.3 Non-Goals + +- Changing the underlying data model or database schema +- Modifying the analysis/job processing logic +- Redesigning the Unified VCP (home page) layout +- Changing the share card system + +--- + +## 2. Current State + +### 2.1 Current Navigation (Authenticated) + +``` +My VCP | Repos | Reports | Settings | Methodology | Security +``` + +### 2.2 Current Page Responsibilities + +| Page | URL | Responsibilities | +|------|-----|------------------| +| My VCP | `/` | Display Unified VCP, share profile, view versions | +| Repos | `/repos` | Connect/disconnect repos, start analysis, LLM opt-in | +| Reports | `/analysis` | Tab 1: View completed VCPs as flat grid. Tab 2: View jobs list | +| Report Detail | `/analysis/[jobId]` | View single Repo VCP with full detail | +| Settings | `/settings/*` | LLM keys configuration | + +### 2.3 Current User Flows + +**Adding a repo and viewing its VCP:** +``` +Repos → Connect repo → Start analysis → Reports (Jobs tab) → +Poll for completion → Reports (Reports tab) → Find card → View detail +``` + +**Viewing VCP history for a specific repo:** +``` +Reports → Scroll through flat list → Find cards for target repo → +Click each to compare versions +``` + +--- + +## 3. Proposed Changes + +### 3.1 New Navigation + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ [Logo] Vibe Coding Profile │ +│ │ +│ My Vibe | Vibes | Settings | Methodology | Security 🔔 [Out] │ +└──────────────────────────────────────────────────────────────────┘ +``` + +| Old | New | Notes | +|-----|-----|-------| +| My VCP | **My Vibe** | Consistent branding | +| Repos | *Removed* | Merged into Settings | +| Reports | **Vibes** | VCP history organized by repo | +| Settings | **Settings** | Now includes repo management | +| — | **🔔 (Bell icon)** | Notification dropdown for jobs | + +### 3.2 New Route Structure + +``` +/ → Unified VCP (My Vibe) - unchanged +/vibes → VCP table organized by repo +/vibes/[repoId] → (Optional) Repo detail page with VCP history +/vibes/[repoId]/[jobId] → Individual VCP detail (was /analysis/[jobId]) +/settings/repos → Repo management (connect, disconnect, sync) +/settings/llm-keys → LLM keys - unchanged +/analysis/* → Redirect to /vibes/* for backwards compatibility +/repos → Redirect to /settings/repos +``` + +### 3.3 New Page Responsibilities + +#### 3.3.1 My Vibe (`/`) +**No changes to functionality.** + +Terminology update: +- "My VCP" → "My Vibe" in navigation + +#### 3.3.2 Vibes (`/vibes`) + +**Purpose:** View all VCPs organized by repository + +**Layout:** +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Your Vibes [+ Add Repo →] │ +│ │ +│ Filter: [All ▼] Sort: [Recent ▼] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ ▶ user/repo-alpha ││ +│ │ Latest: Prompt Sprinter (94% confidence) · Jan 20, 2026 ││ +│ │ 3 versions · 847 commits analyzed ││ +│ │ [View Latest] ││ +│ └─────────────────────────────────────────────────────────────┘│ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ ▼ user/repo-beta ││ +│ │ Latest: Orchestrator (87% confidence) · Jan 18, 2026 ││ +│ │ ┌─────────────────────────────────────────────────────┐ ││ +│ │ │ Version │ Date │ Persona │ Commits │ │ ││ +│ │ │ v3 │ Jan 18 │ Orchestrator │ 234 │ 👁 │ ││ +│ │ │ v2 │ Jan 10 │ Orchestrator │ 198 │ 👁 │ ││ +│ │ │ v1 │ Dec 28 │ Rapid Risk Taker│ 156 │ 👁 │ ││ +│ │ └─────────────────────────────────────────────────────┘ ││ +│ │ [Re-run Analysis] ││ +│ └─────────────────────────────────────────────────────────────┘│ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ ▶ user/repo-gamma ││ +│ │ Not analyzed yet ││ +│ │ [Get Vibe] ││ +│ └─────────────────────────────────────────────────────────────┘│ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Features:** +- Expandable rows showing VCP version history per repo +- Quick actions: View latest, Re-run analysis, Get Vibe (for unanalyzed) +- "Add Repo" link navigates to `/settings/repos` +- Filter by: All, Analyzed only, Not analyzed +- Sort by: Recent analysis, Repo name, Commit count + +**Empty State:** +``` +┌─────────────────────────────────────────────────────────────────┐ +│ │ +│ No repos connected yet │ +│ │ +│ Connect a GitHub repository to discover your │ +│ Vibe Coding Profile │ +│ │ +│ [Connect a Repo →] │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### 3.3.3 VCP Detail (`/vibes/[repoId]/[jobId]`) + +**Purpose:** View individual Repo VCP (same as current `/analysis/[jobId]`) + +**Changes:** +- URL structure change only +- Breadcrumb: `Vibes > repo-name > v3` +- Version selector shows all versions for THIS repo (already exists) + +#### 3.3.4 Settings - Repos (`/settings/repos`) + +**Purpose:** Manage connected repositories + +**Layout:** +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Settings │ +│ │ +│ [LLM Keys] [Repos] [Preferences] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Connected Repositories │ +│ ───────────────────── │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ 🔗 Connect from GitHub ││ +│ │ ┌─────────────────────────────────────────────────────┐ ││ +│ │ │ 🔍 Search your repositories... │ ││ +│ │ └─────────────────────────────────────────────────────┘ ││ +│ │ ││ +│ │ Available: ││ +│ │ ○ user/new-project [Connect] ││ +│ │ ○ user/another-repo [Connect] ││ +│ └─────────────────────────────────────────────────────────────┘│ +│ │ +│ Your Repos (3) Last synced: 2 min ago │ +│ ───────────────── [↻ Sync Now] │ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ user/repo-alpha Analyzed (3 versions) [Remove] │ │ +│ │ user/repo-beta Analyzed (2 versions) [Remove] │ │ +│ │ user/repo-gamma Not analyzed [Remove] │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +│ LLM Narratives │ +│ ────────────── │ +│ [✓] Enable AI-generated narratives for my analyses │ +│ Requires LLM API key configuration │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Features:** +- Connect new repos from GitHub (moved from `/repos`) +- Disconnect repos (with confirmation: "This will remove repo from your Unified Vibe") +- GitHub sync status and manual sync button +- LLM opt-in toggle (moved from `/repos`) + +### 3.4 Notification System (Jobs) + +**Purpose:** Replace Jobs tab with header notification dropdown + +**Location:** Bell icon in header, right side before Sign Out + +**States:** + +1. **No activity:** + ``` + 🔔 (no badge) + ``` + +2. **Jobs running:** + ``` + 🔔 (pulsing dot) + + Dropdown: + ┌─────────────────────────────────────┐ + │ Activity │ + ├─────────────────────────────────────┤ + │ ⏳ Analyzing user/repo-alpha... │ + │ Started 30s ago │ + ├─────────────────────────────────────┤ + │ ✓ user/repo-beta complete │ + │ Prompt Sprinter · 2 min ago │ + │ [View →] │ + └─────────────────────────────────────┘ + ``` + +3. **New results (unread):** + ``` + 🔔 (badge with count: 2) + + Dropdown: + ┌─────────────────────────────────────┐ + │ Activity [Mark read] │ + ├─────────────────────────────────────┤ + │ 🆕 user/repo-alpha complete │ + │ Orchestrator · Just now │ + │ [View →] │ + ├─────────────────────────────────────┤ + │ 🆕 user/repo-beta complete │ + │ Prompt Sprinter · 5 min ago │ + │ [View →] │ + ├─────────────────────────────────────┤ + │ ✓ user/repo-gamma complete │ + │ Reflective Balancer · 1 hr ago │ + └─────────────────────────────────────┘ + ``` + +4. **Error state:** + ``` + 🔔 (red badge) + + Dropdown: + ┌─────────────────────────────────────┐ + │ Activity │ + ├─────────────────────────────────────┤ + │ ⚠️ user/repo-alpha failed │ + │ Not enough commits · 5 min ago │ + │ [Retry →] │ + └─────────────────────────────────────┘ + ``` + +**Behavior:** +- Polls for job updates (existing JobsContext can be reused) +- Clicking "View" navigates to `/vibes/[repoId]/[jobId]` +- "Mark read" clears unread badges +- Shows last 10 activities, older ones auto-dismiss +- Persists unread state in localStorage (or context as currently done) + +--- + +## 4. Terminology Alignment + +### 4.1 Global Terminology + +| Old Term | New Term | Usage | +|----------|----------|-------| +| My VCP | My Vibe | Navigation, page title | +| Report | Vibe / VCP | When referring to an analysis result | +| Reports page | Vibes page | Navigation | +| Repo VCP | Repo Vibe | Individual repo analysis | +| Unified VCP | My Vibe / Unified Vibe | Aggregated profile | +| Get vibe | Get Vibe | Action to start analysis | +| View report | View Vibe | Action to view result | + +### 4.2 Display Consistency + +Both Repo Vibe and Unified Vibe share cards should use: +- Same persona color palette +- Same axes visualization (radar chart or bars) +- Same share card structure (with variant flag for context) + +### 4.3 ShareCard Metrics + +The ShareCard displays **computed metrics** (not raw axes): + +| Metric | Description | Example | +|--------|-------------|---------| +| **Strongest** | Highest-scoring axis + score | "Automation 78" | +| **Style** | 2-word descriptor from axis combo | "Fast Builder" | +| **Rhythm** | Shipping pattern label | "Bursty", "Steady" | +| **Peak** | Most active time of day | "Afternoons", "Night Owl" | + +See `docs/prd/ux/share-experience-improvements.md` Appendix B for detailed computation logic. + +--- + +## 5. Migration & Backwards Compatibility + +### 5.1 URL Redirects + +| Old URL | New URL | Type | +|---------|---------|------| +| `/repos` | `/settings/repos` | 301 Permanent | +| `/analysis` | `/vibes` | 301 Permanent | +| `/analysis/[jobId]` | `/vibes/[repoId]/[jobId]` | 301 Permanent (requires lookup) | + +### 5.2 Migration Steps + +1. Create new routes (`/vibes`, `/settings/repos`) +2. Build new components (VibesTable, NotificationDropdown) +3. Move repo management UI to Settings +4. Update navigation +5. Add redirects for old routes +6. Remove old pages after redirect period + +--- + +## 6. Component Changes + +### 6.1 New Components + +| Component | Location | Purpose | +|-----------|----------|---------| +| `VibesTable` | `/vibes` | Expandable table of repos with VCP history | +| `VibesTableRow` | `/vibes` | Single repo row with expand/collapse | +| `NotificationDropdown` | `AppHeader` | Jobs notification system | +| `NotificationItem` | `AppHeader` | Single notification entry | +| `RepoSettings` | `/settings/repos` | Repo management panel | + +### 6.2 Modified Components + +| Component | Changes | +|-----------|---------| +| `AppHeader` | New nav items, add notification bell | +| `SettingsLayout` | Add "Repos" tab | +| `JobsContext` | Adapt for notification system (may need unread persistence) | + +### 6.3 Deprecated Components + +| Component | Replacement | +|-----------|-------------| +| `ReposClient` | Split into `RepoSettings` + `VibesTable` | +| `AnalysisListClient` | Replaced by `VibesTable` | +| Jobs tab in `/analysis` | Replaced by `NotificationDropdown` | + +--- + +## 7. Data Requirements + +### 7.1 New API Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `GET /api/vibes` | List repos with their VCP history (grouped) | +| `GET /api/vibes/[repoId]` | Get VCP history for specific repo | + +### 7.2 Modified API Endpoints + +| Endpoint | Changes | +|----------|---------| +| `GET /api/analysis/[jobId]` | Add `repoId` to response for URL construction | + +### 7.3 No Database Changes Required + +The existing schema supports this restructure. We're changing how data is queried and displayed, not the underlying model. + +--- + +## 8. Open Questions + +1. **Repo detail page:** Should `/vibes/[repoId]` exist as a dedicated page, or is the expandable row sufficient? + - Recommendation: Start with expandable row only, add dedicated page if needed + +2. **Notification persistence:** Should unread state persist across sessions (localStorage/DB) or reset on refresh? + - Recommendation: localStorage for simplicity, upgrade to DB if users request + +3. **Admin jobs view:** Keep `/admin/jobs` as-is for system-wide monitoring? + - Recommendation: Yes, admin view serves different purpose (system health) + +4. **URL structure for VCP detail:** Use `jobId` or introduce `versionNumber`? + - Current: `/vibes/[repoId]/[jobId]` + - Alternative: `/vibes/[repoId]/v3` + - Recommendation: Keep jobId for simplicity, display as "v3" in UI + +--- + +## 9. Success Metrics + +- Reduced navigation clicks to view repo's VCP history (currently 3+ → target 2) +- Reduced confusion about "Reports" terminology (qualitative feedback) +- Faster discovery of new analysis results via notifications +- Cleaner separation of concerns (view vs. manage) + +--- + +## 10. Implementation Phases + +### Phase 1: Foundation +- Create `/vibes` route with VibesTable component +- Create `/settings/repos` with repo management UI +- Update navigation labels + +### Phase 2: Notifications +- Build NotificationDropdown component +- Integrate with JobsContext +- Remove Jobs tab from old Reports page + +### Phase 3: Migration +- Add URL redirects +- Update all internal links +- Deprecate old routes + +### Phase 4: Polish +- Terminology audit across all UI +- VCP display consistency review +- Documentation update + +--- + +## Appendix A: Wireframes + +### A.1 New Navigation +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ● Vibe Coding Profile My Vibe Vibes Settings Methodology Security │ +│ 🔔 [Sign out] │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### A.2 Notification Dropdown (Expanded) +``` + ┌──────────────────────┐ + │ Activity [Clear] │ + ├──────────────────────┤ + │ ⏳ repo-alpha │ + │ Analyzing... │ + ├──────────────────────┤ + │ 🆕 repo-beta │ + │ Prompt Sprinter │ + │ 2 min ago [View] │ + ├──────────────────────┤ + │ ✓ repo-gamma │ + │ Orchestrator │ + │ 1 hr ago [View] │ + └──────────────────────┘ +``` + +### A.3 Vibes Table (Collapsed) +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Your Vibes [+ Add Repo →] │ +├─────────────────────────────────────────────────────────────────────────┤ +│ ▶ user/repo-alpha Prompt Sprinter (94%) 3 versions [View] │ +│ ▶ user/repo-beta Orchestrator (87%) 2 versions [View] │ +│ ▶ user/repo-gamma Not analyzed — [Get Vibe] │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### A.4 Vibes Table (Expanded Row) +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ▼ user/repo-alpha Prompt Sprinter (94%) 3 versions │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Version │ Date │ Persona │ Confidence │ Commits │ │ +│ ├─────────┼────────────┼──────────────────┼────────────┼─────────┤ │ +│ │ v3 │ Jan 20 │ Prompt Sprinter │ 94% │ 847 │👁 │ +│ │ v2 │ Jan 15 │ Orchestrator │ 88% │ 723 │👁 │ +│ │ v1 │ Jan 10 │ Orchestrator │ 85% │ 612 │👁 │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ [Re-run Analysis] │ +└─────────────────────────────────────────────────────────────────────────┘ +``` diff --git a/docs/prd/share-experience-improvements.md b/docs/prd/ux/share-experience-improvements.md similarity index 81% rename from docs/prd/share-experience-improvements.md rename to docs/prd/ux/share-experience-improvements.md index a259649..73c520c 100644 --- a/docs/prd/share-experience-improvements.md +++ b/docs/prd/ux/share-experience-improvements.md @@ -113,6 +113,15 @@ Non-LLM users should get a great experience. LLM adds flavor but isn't required **Share Card Design:** +The ShareCard displays **computed metrics** derived from the 6 vibe axes, designed to be engaging and curiosity-inducing: + +| Metric | Description | Example Values | +|--------|-------------|----------------| +| **Strongest** | Highest-scoring axis + score | "Automation 78", "Planning 85" | +| **Style** | 2-word descriptor from axis combo | "Fast Builder", "Careful Planner" | +| **Rhythm** | Shipping pattern from bursty/steady | "Bursty", "Steady", "Mixed" | +| **Peak** | Most active time of day | "Mornings", "Afternoons", "Evenings", "Night Owl" | + ``` ┌─────────────────────────────────────────────────────────────────┐ │ ┌─────────────────────────────────────────────────────────┐ │ @@ -123,12 +132,17 @@ Non-LLM users should get a great experience. LLM adds flavor but isn't required │ │ 🎭 The Vibe Prototyper │ │ │ │ "You build to think — code is your sketchpad" │ │ │ │ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ [LLM-generated tagline if available] │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ -│ │ │ 23 days │ │ Friday │ │ Focused │ │ 2.1:1 │ │ │ -│ │ │ Streak │ │ 4-7pm │ │ Surgeon │ │ Build/Fix│ │ │ +│ │ │Automation│ │ Fast │ │ Bursty │ │Afternoons│ │ │ +│ │ │ 78 │ │ Builder │ │ │ │ │ │ │ +│ │ │ STRONGEST│ │ STYLE │ │ RHYTHM │ │ PEAK │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ -│ │ 847 commits · 42 active days · vibecoding.profile │ │ +│ │ vibed.dev 3 repos · 1,245 commits │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ [Copy text] [Copy link] [Download PNG ▾] [Twitter] [LinkedIn] │ @@ -136,6 +150,8 @@ Non-LLM users should get a great experience. LLM adds flavor but isn't required └─────────────────────────────────────────────────────────────────┘ ``` +**Note:** These ShareCard metrics are different from the 6 vibe axes displayed in the full VCP detail view. The axes (Automation, Guardrails, Iteration, Planning, Surface, Rhythm) show raw scores; the ShareCard metrics are computed summaries designed for quick scanning and social sharing. + **Requirements:** - [ ] Share card is the first thing users see after analysis completes - [ ] Card is designed to look good when screenshotted @@ -215,22 +231,23 @@ The profile page should have a share card that aggregates across all repos. │ ┌─────────────────────────────────────────────────────────┐ │ │ │ [Gradient background] │ │ │ │ │ │ -│ │ MY UNIFIED VCP │ │ +│ │ MY UNIFIED VCP │ │ │ │ │ │ │ │ 🎭 The Vibe Prototyper │ │ │ │ "You build to think — code is your sketchpad" │ │ │ │ High confidence │ │ │ │ │ │ -│ │ ┌──────────────────────────────────────────────────┐ │ │ -│ │ │ 5 repos · 2,341 commits · 78% clarity │ │ │ -│ │ └──────────────────────────────────────────────────┘ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ [LLM-generated tagline if available] │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ │ │ │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ -│ │ │ Automation│ │ Guardrails│ │ Rhythm │ │ │ -│ │ │ 72 │ │ 45 │ │ 68 │ │ │ -│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │Automation│ │ Fast │ │ Bursty │ │Afternoons│ │ │ +│ │ │ 78 │ │ Builder │ │ │ │ │ │ │ +│ │ │ STRONGEST│ │ STYLE │ │ RHYTHM │ │ PEAK │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ -│ │ vibecoding.profile │ │ +│ │ vibed.dev 5 repos · 2,341 commits │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ [Copy text] [Copy link] [Download PNG ▾] [Twitter] [LinkedIn] │ @@ -240,7 +257,9 @@ The profile page should have a share card that aggregates across all repos. **Requirements:** - [ ] Profile page has a share card as prominent as repo analysis -- [ ] Card includes: persona, tagline, confidence, repo count, commit count, top 3 axes +- [ ] Card displays new ShareCard metrics: Strongest, Style, Rhythm, Peak +- [ ] Optional LLM-generated tagline row between header and metrics +- [ ] Footer shows `vibed.dev` branding + repo/commit context - [ ] Same share actions as repo analysis page - [ ] Card is designed to look good when shared @@ -484,12 +503,15 @@ function createShareSvg( │ │ "You build to think — code is your sketchpad" │ │ │ │ 78% confidence │ │ │ │ │ │ +│ │ [Optional LLM tagline row] │ │ +│ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ -│ │ │23 days │ │ Friday │ │ Focused │ │ 2.1:1 │ │ │ -│ │ │ Streak │ │ 4-7pm │ │ Surgeon │ │Build/Fix│ │ │ +│ │ │Automaton│ │ Fast │ │ Bursty │ │Afternons│ │ │ +│ │ │ 78 │ │ Builder │ │ │ │ │ │ │ +│ │ │STRONGEST│ │ STYLE │ │ RHYTHM │ │ PEAK │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ -│ │ acme/dashboard · 847 commits · vibed.coding │ │ +│ │ vibed.dev 1 repo · 847 commits │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ @@ -532,14 +554,15 @@ function createShareSvg( │ │ "You build to think — code is your sketchpad" │ │ │ │ High confidence │ │ │ │ │ │ -│ │ 5 repos · 2,341 commits · 78% clarity │ │ +│ │ [Optional LLM tagline row] │ │ │ │ │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ -│ │ │Automation│ │Guardrails│ │ Rhythm │ │ │ -│ │ │ 72 │ │ 45 │ │ 68 │ │ │ -│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │Automation│ │ Fast │ │ Bursty │ │Afternoons│ │ │ +│ │ │ 72 │ │ Builder │ │ │ │ │ │ │ +│ │ │ STRONGEST│ │ STYLE │ │ RHYTHM │ │ PEAK │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ -│ │ vibecoding.profile │ │ +│ │ vibed.dev 5 repos · 2,341 commits │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ @@ -614,4 +637,72 @@ apps/web/src/app/page.tsx --- -*This PRD focuses on the share experience layer. See `PRD.md` for core analysis and `PRD-vibed-ux.md` for overall UX direction.* +## Appendix B: ShareCard Metrics Specification + +### Overview + +The ShareCard displays **4 computed metrics** derived from the 6 vibe axes. These metrics are designed to be: +- **Engaging**: Spark curiosity and conversation +- **Glanceable**: Understood in < 3 seconds +- **Unique**: Reflect individual vibe coding style, not generic stats + +### Metric Definitions + +| Metric | Label | Computation | Example Output | +|--------|-------|-------------|----------------| +| **Strongest** | `STRONGEST` | Highest-scoring axis name + score | "Automation 78" | +| **Style** | `STYLE` | 2-word descriptor from axis combo | "Fast Builder" | +| **Rhythm** | `RHYTHM` | Shipping pattern from `shipping_rhythm` axis | "Bursty", "Steady", "Mixed" | +| **Peak** | `PEAK` | Most active time of day (from commit timing) | "Mornings", "Afternoons", "Evenings", "Night Owl" | + +### Style Descriptor Logic + +The "Style" metric combines the top 2 axes to generate a 2-word descriptor: + +| Top Axis | Secondary Axis | Style Descriptor | +|----------|----------------|------------------| +| automation_heaviness | iteration_loop_intensity | "Fast Builder" | +| automation_heaviness | planning_signal | "Prompt Architect" | +| guardrail_strength | planning_signal | "Careful Planner" | +| planning_signal | surface_area_per_change | "Methodical Architect" | +| iteration_loop_intensity | guardrail_strength | "Iterative Validator" | +| *any other* | *any other* | "Balanced Builder" | + +### Rhythm Label Logic + +| Shipping Rhythm Score | Label | +|-----------------------|-------| +| >= 65 | "Bursty" | +| <= 35 | "Steady" | +| 36-64 | "Mixed" | + +### Peak Time Logic + +Based on the most common commit hour: +| Hour Range | Label | +|------------|-------| +| 5am - 11am | "Mornings" | +| 12pm - 4pm | "Afternoons" | +| 5pm - 8pm | "Evenings" | +| 9pm - 4am | "Night Owl" | + +### Implementation + +These metrics are computed in `apps/web/src/lib/vcp/metrics.ts`: + +```typescript +import { computeShareCardMetrics } from "@/lib/vcp/metrics"; + +const metrics = computeShareCardMetrics(axes); +// Returns: { strongest: "Automation 78", style: "Fast Builder", rhythm: "Bursty", peak: "Afternoons" } +``` + +### Footer Content + +The ShareCard footer displays: +- **Left**: `vibed.dev` (brand) +- **Right**: `{N} repos · {M} commits` (context) + +--- + +*This PRD focuses on the share experience layer. See [`PRD.md`](../core/PRD.md) for core analysis and [`PRD-vibed-ux.md`](./PRD-vibed-ux.md) for overall UX direction.* diff --git a/docs/security/open-source-preparedness.md b/docs/security/open-source-preparedness.md new file mode 100644 index 0000000..cb3a095 --- /dev/null +++ b/docs/security/open-source-preparedness.md @@ -0,0 +1,61 @@ +--- +title: Open Source Preparedness +date: 2026-01-22 +--- + +# Open Source Preparedness + +This document summarizes the current security posture of Vibed Coding and lays out the steps taken (and still pending) before the repository is made public. + +--- + +## 1. What we already have + +- **Secrets never committed.** All API keys (Supabase, GitHub OAuth, LLM) live in environment variables; `.env.example` documents requirements. +- **Encrypted key storage.** GitHub/Supabase/LLM tokens use AES-256-GCM helpers (`apps/web/src/lib/llmKey.ts`, `apps/web/src/lib/supabase/service.ts`), and Supabase migrations define strong RLS policies (`supabase/migrations/0013_create_llm_configs.sql`, `0014_create_llm_usage.sql`, `0019_add_pull_requests.sql`). +- **Metadata-only analysis.** PR ingestion now only stores derived signals (checklists, template markers, linked issue numbers, merge commitment) and never persists bodies. The worker ingests GitHub metadata without code content. +- **Scoped GitHub workflow permissions.** `release-pr.yml` explicitly limits `contents` and `pull-requests` to `write`, and actions run with GitHub's default `GITHUB_TOKEN`, minimizing exposure. +- **Build validation in CI.** `npm run build` (Turbo) runs across all packages; the repo also supports `turbo test`, `lint`, and `type-check` for OSS CI workflows. + +--- + +## 2. Remaining hardening tasks + +| Area | Action | +|------|--------| +| **Secrets/history** | Run a tool such as `trufflehog`/`git-secrets` across the git history before making the repo public. Rotate any secrets exposed during early development. | +| **Dependency vulnerabilities** | Schedule `npm audit` (or GitHub Dependabot) as part of CI and fix or mitigate outstanding alerts before release. | +| **Documentation** | Add `README.md`, `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, and a `SECURITY.md` with reporting instructions. Document all required env vars and key rotation procedures. | +| **Security scanning** | Add an optional GitHub Action that runs `npm audit --audit-level=moderate` + `trufflehog --entropy=False` on push/pull requests. | +| **Workflow permissions** | Confirm org/repo Actions permissions allow `pull-requests: write` (needed for release workflow). Document this requirement in `README`/`SECURITY.md`. | + +--- + +## 3. Risk profile when scanning begins + +### What scanners will look for + +1. **Secrets** – tools such as GitHub Secret Scanning, TruffleHog, and Gitleaks search for API keys or credentials. Keep sensitive tokens out of history and set up git hooks to block them in future commits. +2. **Vulnerabilities in dependencies** – Automated tools will flag outdated packages. Keep `package-lock.json` clean and set up Dependabot or similar. +3. **Configuration mistakes** – Look for permissive CORS, overly wide RLS, logging sensitive data, or credentials in workflows. The `release-pr` workflow already scopes permissions, but ensure no other workflow exposes secrets. +4. **Input abuse** – Review SSR/API endpoints (`/api/analysis/[id]/regenerate-story`, `/api/analysis/start`, `/api/settings/llm-keys`) for validation, rate limits, and safe error handling. Already check origin and rate limit regenerate route; keep logging aggressive errors server-side only. +5. **Data mishandling** – The product focuses on metadata. Continue documenting that raw code, PR bodies, or sensitive comments are never stored in the DB. + +### Suggested mitigations + +- Enforce secret scanning (pre-commit/CI). +- Regular dependency audits (`npm audit`, GitHub Dependabot). +- Harden API paths with rate limiting and origin checks (done for regenerate story). +- Document how to report vulnerabilities (`SECURITY.md`), including contact channel and expected response time. +- Retain trimmed commit history via `git filter-repo` if old secrets surface. + +--- + +## 4. Getting ready for release + +1. **Define license and governance.** Add `LICENSE` (choose MIT/Apache), `README`, `CONTRIBUTING.md`, and `CODE_OF_CONDUCT.md`. Mention required environment variables and LLM keys. +2. **Add security docs.** This file will live under `docs/security`. Include instructions for reporting vulnerabilities and how keys are stored/rotated. +3. **Revise CI.** (Optional) Add lint/test+`npm audit` workflow; include secret scans. +4. **Final checklist.** Run `npm run build`, `npm run lint`, `npm run test`, and a `trufflehog` scan. Make sure no `.env` files leak, and update docs with the open-source readiness summary above. + +Once these steps are done, the repository is ready for public release while keeping security safeguards in place. diff --git a/package-lock.json b/package-lock.json index c9cf877..6923cf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,14 @@ }, "apps/web": { "name": "@vibed/web", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-tooltip": "^1.2.8", "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.47.14", + "@vercel/og": "^0.8.6", "@vibed/core": "*", "@vibed/db": "*", "class-variance-authority": "^0.7.1", @@ -31,6 +33,7 @@ "inngest": "^3.49.3", "lucide-react": "^0.562.0", "next": "16.1.2", + "qrcode": "^1.5.4", "react": "19.2.3", "react-dom": "19.2.3", "tailwind-merge": "^3.4.0" @@ -38,6 +41,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/qrcode": "^1.5.6", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", @@ -49,7 +53,7 @@ }, "apps/worker": { "name": "@vibed/worker", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@supabase/supabase-js": "^2.47.14", "@vibed/core": "*", @@ -3706,6 +3710,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -3856,6 +3894,15 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@resvg/resvg-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", + "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", @@ -4213,6 +4260,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4738,6 +4801,16 @@ "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", @@ -5314,6 +5387,19 @@ "win32" ] }, + "node_modules/@vercel/og": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.8.6.tgz", + "integrity": "sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==", + "license": "MPL-2.0", + "dependencies": { + "@resvg/resvg-wasm": "2.4.0", + "satori": "0.16.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@vibed/core": { "resolved": "packages/core", "link": true @@ -5835,6 +5921,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.9.15", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", @@ -5997,6 +6092,24 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001764", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", @@ -6263,6 +6376,47 @@ "node": ">= 8" } }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -6348,6 +6502,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -6430,6 +6593,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -6472,6 +6641,15 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -6714,6 +6892,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7265,6 +7449,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7801,6 +7991,18 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8981,6 +9183,16 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9658,6 +9870,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9671,11 +9898,20 @@ "node": ">=6" } }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9797,6 +10033,15 @@ "pathe": "^2.0.1" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9879,6 +10124,12 @@ } } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -10009,6 +10260,162 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10207,6 +10614,12 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -10383,6 +10796,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/satori": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz", + "integrity": "sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.16", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout": "^3.2.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -10409,6 +10844,12 @@ "url": "https://github.com/sponsors/finwo" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10730,6 +11171,12 @@ "node": ">=8" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -11066,6 +11513,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -11603,6 +12056,16 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -12427,6 +12890,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", @@ -12622,6 +13091,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, "node_modules/zod": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a24ed8b..d12b687 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -166,6 +166,7 @@ export interface AnalysisInsightShareTemplate { label: string; archetypes: string[]; }; + tagline?: string | null; } export interface AnalysisInsightPersonaDelta { @@ -532,6 +533,13 @@ function detectPersona(args: PersonaDetectionArgs): PersonaDetectionResult { return { persona, delta }; } +/** + * Builds the share template for social sharing cards. + * + * NOTE: The `tagline` field is set to `null` here. Both the Inngest worker and + * standalone worker mutate `insights.share_template.tagline` after computing + * insights, using the LLM-generated tagline (with persona fallback). + */ function buildShareTemplate( persona: AnalysisInsightPersona, streak: ReturnType, @@ -579,6 +587,7 @@ function buildShareTemplate( label: persona.label, archetypes: persona.archetypes, }, + tagline: null, }; } diff --git a/packages/db/src/database.types.ts b/packages/db/src/database.types.ts index 87580dd..3de7e51 100644 --- a/packages/db/src/database.types.ts +++ b/packages/db/src/database.types.ts @@ -48,6 +48,7 @@ export type Database = { share_template: Json sources: Json tech_signals: Json + tagline: string | null } Insert: { generated_at?: string @@ -62,6 +63,7 @@ export type Database = { share_template?: Json sources?: Json tech_signals?: Json + tagline?: string | null } Update: { generated_at?: string @@ -76,6 +78,7 @@ export type Database = { share_template?: Json sources?: Json tech_signals?: Json + tagline?: string | null } Relationships: [ { @@ -777,4 +780,3 @@ export const Constants = { Enums: {}, }, } as const - diff --git a/release-please-config.develop.json b/release-please-config.develop.json index 3da61bb..328a2ea 100644 --- a/release-please-config.develop.json +++ b/release-please-config.develop.json @@ -26,7 +26,7 @@ ], "packages": { ".": { - "release-type": "node", + "release-type": "simple", "component": "vibed-coding", "changelog-path": "CHANGELOG-DEVELOP.md", "prerelease": true, @@ -34,7 +34,7 @@ "prerelease-type": "alpha" }, "apps/web": { - "release-type": "node", + "release-type": "simple", "component": "web", "changelog-path": "CHANGELOG-DEVELOP.md", "prerelease": true, @@ -42,7 +42,7 @@ "prerelease-type": "alpha" }, "apps/worker": { - "release-type": "node", + "release-type": "simple", "component": "worker", "changelog-path": "CHANGELOG-DEVELOP.md", "prerelease": true, diff --git a/supabase/migrations/0021_add_analysis_insights_tagline.sql b/supabase/migrations/0021_add_analysis_insights_tagline.sql new file mode 100644 index 0000000..b496021 --- /dev/null +++ b/supabase/migrations/0021_add_analysis_insights_tagline.sql @@ -0,0 +1,9 @@ +-- Add tagline column for LLM-generated share card taglines +-- The `tagline` column is the authoritative source; share_template.tagline +-- and insights_json.share_template.tagline are also updated for compatibility. +alter table public.analysis_insights + add column if not exists tagline text; + +comment on column public.analysis_insights.tagline is 'LLM-generated tagline for share cards (<=60 chars, fallbacks to persona description)'; + +-- DOWN: alter table public.analysis_insights drop column if exists tagline;