diff --git a/Frontend/public/landing-logos/git.svg b/Frontend/public/landing-logos/git.svg new file mode 100644 index 0000000..13af359 --- /dev/null +++ b/Frontend/public/landing-logos/git.svg @@ -0,0 +1 @@ +Git \ No newline at end of file diff --git a/Frontend/public/landing-logos/githubcopilot.svg b/Frontend/public/landing-logos/githubcopilot.svg new file mode 100644 index 0000000..f064947 --- /dev/null +++ b/Frontend/public/landing-logos/githubcopilot.svg @@ -0,0 +1 @@ +GitHub Copilot \ No newline at end of file diff --git a/Frontend/public/landing-logos/gitlens-mark.svg b/Frontend/public/landing-logos/gitlens-mark.svg new file mode 100644 index 0000000..a05137c --- /dev/null +++ b/Frontend/public/landing-logos/gitlens-mark.svg @@ -0,0 +1,8 @@ + + GitLens + + + diff --git a/Frontend/src/RootLayout.tsx b/Frontend/src/RootLayout.tsx index 0398022..ddfba6f 100644 --- a/Frontend/src/RootLayout.tsx +++ b/Frontend/src/RootLayout.tsx @@ -18,7 +18,7 @@ function MainChrome() { {hasNav && }
diff --git a/Frontend/src/components/ChatPanel.tsx b/Frontend/src/components/ChatPanel.tsx index f662ddf..7f10c10 100644 --- a/Frontend/src/components/ChatPanel.tsx +++ b/Frontend/src/components/ChatPanel.tsx @@ -260,7 +260,7 @@ export function ChatPanel({ onChatComplete }: { onChatComplete?: () => void } = } const armorNote = res.armorAgent && Array.isArray(res.enforcementLog) - ? `\n\n---\n_ArmorClaw:_ ${res.enforcementLog.length} policy check(s) — ${res.enforcementLog.filter((e) => e.action === "deny").length} blocked. See **ArmorClaw enforcement** below._` + ? `\n\n---\n_ArmorClaw:_ ${res.enforcementLog.length} policy check(s); ${res.enforcementLog.filter((e) => e.action === "deny").length} blocked. See **ArmorClaw enforcement** below._` : ""; setMessages((prev) => [ ...prev, @@ -414,7 +414,7 @@ export function ChatPanel({ onChatComplete }: { onChatComplete?: () => void } = Synthesis: offline (no API key) )} {msg.synthesis === "fallback_error" && ( - Synthesis: Gemini error — raw matches shown + Synthesis: Gemini error; raw matches shown )} )} diff --git a/Frontend/src/components/EnforcementLog.tsx b/Frontend/src/components/EnforcementLog.tsx index 035bc02..fad01e9 100644 --- a/Frontend/src/components/EnforcementLog.tsx +++ b/Frontend/src/components/EnforcementLog.tsx @@ -76,7 +76,7 @@ export function EnforcementLog({ refreshKey = 0 }: Props) { typeof parsed !== "object" || Array.isArray(parsed) ) { - setTestResult("Params must be a JSON object, e.g. {} or {\"path\":\"README.md\"} — not an array or null."); + setTestResult("Params must be a JSON object, e.g. {} or {\"path\":\"README.md\"}; not an array or null."); return; } params = parsed as Record; @@ -90,7 +90,7 @@ export function EnforcementLog({ refreshKey = 0 }: Props) { const repo = `${target.owner}/${target.name}`; const r = await postEnforcementTest({ tool: testTool, params, repo }); setTestResult( - `${r.allowed ? "ALLOWED" : "BLOCKED"} — ${r.policy_rule} (${r.risk_level})\n${r.reason}` + `${r.allowed ? "ALLOWED" : "BLOCKED"}: ${r.policy_rule} (${r.risk_level})\n${r.reason}` ); } catch (e) { setTestResult(e instanceof Error ? e.message : "Test failed"); diff --git a/Frontend/src/components/IngestButton.tsx b/Frontend/src/components/IngestButton.tsx index b34e1fd..5b77d29 100644 --- a/Frontend/src/components/IngestButton.tsx +++ b/Frontend/src/components/IngestButton.tsx @@ -19,7 +19,7 @@ export function IngestButton({ onComplete }: { onComplete?: () => void }) { useEffect(() => { if (prevStatus.current === "running" && status === "done") { toast({ - message: `Knowledge Graph built — ${nodeCount} decision${nodeCount === 1 ? "" : "s"} found`, + message: `Knowledge Graph built: ${nodeCount} decision${nodeCount === 1 ? "" : "s"} found`, type: "success", }); } @@ -105,7 +105,7 @@ export function IngestButton({ onComplete }: { onComplete?: () => void }) { } } catch (err) { setStatus("error"); - toast({ message: "Ingest failed — check API key", type: "error" }); + toast({ message: "Ingest failed. Check API key", type: "error" }); console.error("Ingest error:", err); } }; diff --git a/Frontend/src/components/KnowledgeDecisionsGraph.tsx b/Frontend/src/components/KnowledgeDecisionsGraph.tsx index 3fbd24d..bbe25a5 100644 --- a/Frontend/src/components/KnowledgeDecisionsGraph.tsx +++ b/Frontend/src/components/KnowledgeDecisionsGraph.tsx @@ -161,7 +161,7 @@ function nodeShape(n: KnowledgeLayoutResponse["nodes"][0], key: string, theme: T const accent = n.color; const label = n.label; const sub = n.sublabel ? `${n.sublabel.slice(0, 48)}${n.sublabel.length > 48 ? "…" : ""}` : ""; - const tooltip = [n.label, n.sublabel].filter(Boolean).join(" — "); + const tooltip = [n.label, n.sublabel].filter(Boolean).join(" · "); const L = theme === "light"; const textMain = L ? "#0f172a" : "#f8fafc"; @@ -866,7 +866,7 @@ export function KnowledgeDecisionsGraph({ refreshKey = 0 }: { refreshKey?: numbe

Knowledge graph

- Ingested PR decisions, themes, issues, authors, and merge history — the same evidence the side chat uses.{" "} + Ingested PR decisions, themes, issues, authors, and merge history, the same evidence the side chat uses.{" "} Drag to pan (on nodes: use Ctrl/Cmd+drag or middle-drag on empty area). Scroll zooms toward the pointer. Green links = shared closing issue; dotted = merge-time neighbors; violet dashed = PR → theme. @@ -975,7 +975,7 @@ export function KnowledgeDecisionsGraph({ refreshKey = 0 }: { refreshKey?: numbe issue   merge   shared issue   - time order — open nodes on GitHub. + time order: open nodes on GitHub.

)} @@ -995,7 +995,7 @@ export function KnowledgeDecisionsGraph({ refreshKey = 0 }: { refreshKey?: numbe >

- Knowledge graph — full view + Knowledge graph: full view

= { - gold: "bg-[var(--accent)]/14 ring-[var(--accent)]/20", - green: "bg-emerald-500/10 ring-emerald-500/20", - slate: "bg-[var(--text-secondary)]/10 ring-[var(--border)]", - violet: "bg-violet-500/10 ring-violet-500/25 dark:bg-violet-500/12", + gold: "bg-[var(--accent)]/14", + green: "bg-emerald-500/10", + slate: "bg-[var(--text-secondary)]/10", + violet: "bg-violet-500/10 dark:bg-violet-500/12", }; function BrandLogos({ logos, pair }: { logos: StackCard["logos"]; pair: boolean }) { @@ -116,18 +116,14 @@ function StackCardItem({ card }: { card: StackCard }) { const pair = card.logos.length > 1; return (
-
+
{/* Row 2 uses minmax(_,auto): short “why” copy (e.g. Hono) was shrinking that row and shifting the divider vs neighbors. Floor matches ~3 lines of body + label + padding. */}
@@ -152,15 +148,7 @@ function StackCardItem({ card }: { card: StackCard }) { const BuiltWith = () => { return ( -
-
+
diff --git a/Frontend/src/components/landing/Comparison.tsx b/Frontend/src/components/landing/Comparison.tsx index 2e8f600..8d13099 100644 --- a/Frontend/src/components/landing/Comparison.tsx +++ b/Frontend/src/components/landing/Comparison.tsx @@ -2,8 +2,8 @@ import { Check, X } from "lucide-react"; import { FadeIn } from "../effects/FadeIn"; const rows: { capability: string; blame: boolean; gitlens: boolean; copilot: boolean; gitlore: boolean; emphasize?: boolean }[] = [ - { capability: "Who changed it?", blame: true, gitlens: true, copilot: false, gitlore: true }, - { capability: "When was it changed?", blame: true, gitlens: true, copilot: false, gitlore: true }, + { capability: "Who changed it?", blame: true, gitlens: true, copilot: true, gitlore: true }, + { capability: "When was it changed?", blame: true, gitlens: true, copilot: true, gitlore: true }, { capability: "What does this code do?", blame: false, gitlens: false, copilot: true, gitlore: true }, { capability: "Why was it written this way?", @@ -53,7 +53,7 @@ function MarkCell({ "mx-auto flex h-9 w-9 items-center justify-center rounded-full border transition-colors md:h-10 md:w-10"; const yesClasses = supported ? isGitlore - ? `${baseWrap} border-[var(--accent)]/45 bg-[var(--accent)]/18 text-[var(--accent)] shadow-[0_0_20px_-4px_var(--accent)]` + ? `${baseWrap} border-[var(--accent)]/45 bg-[var(--accent)]/18 text-[var(--accent)]` : `${baseWrap} border-[color-mix(in_srgb,var(--success)_45%,transparent)] bg-[var(--success-dim)] text-[var(--success)]` : `${baseWrap} border-[var(--border-strong)] bg-[var(--surface-hover)]/85 text-[var(--text-secondary)]`; @@ -83,17 +83,9 @@ function MarkCell({ const Comparison = () => { return (
-
@@ -105,8 +97,8 @@ const Comparison = () => {

No. Here's the difference.

-
-
+
+
@@ -133,7 +125,7 @@ const Comparison = () => { Copilot
GitLore diff --git a/Frontend/src/components/landing/FinalCTA.tsx b/Frontend/src/components/landing/FinalCTA.tsx index c3631cd..f07ba12 100644 --- a/Frontend/src/components/landing/FinalCTA.tsx +++ b/Frontend/src/components/landing/FinalCTA.tsx @@ -7,29 +7,20 @@ const FinalCTA = () => { const label = user && !loading ? "Go to Dashboard" : "Connect GitHub Repo"; return ( -
-
+
-
-
-
-

- Your team made 1000 decisions. Make them searchable. -

- - {label} - -

- Free · Public repos · 2 minutes to your first answer -

-
-
+

+ Your team made 1000 decisions. +

+

+ Make them searchable. +

+ + {label} + +

+ Free · Public repos · 2 minutes to your first answer +

); diff --git a/Frontend/src/components/landing/HeroSection.tsx b/Frontend/src/components/landing/HeroSection.tsx index 15af9ce..72e0fa5 100644 --- a/Frontend/src/components/landing/HeroSection.tsx +++ b/Frontend/src/components/landing/HeroSection.tsx @@ -6,113 +6,148 @@ import { SplitText } from "../effects/SplitText"; import { TextScramble } from "../effects/TextScramble"; import HeroProductDemo from "./HeroProductDemo"; -/** Static code window — desktop accent beside hero copy. */ -const HeroCodeVisual = () => ( -
-
-
- - - - rate_limiter.py -
-
-
- 12 def is_allowed(self, client_id): -
-
- 13 now = time.time() -
-
- 14{" "} - if len(self.requests[client_id]) >= self.max_requests: -
-
- 15 return False -
-
- - memory: in-memory only - @review -
-
-
-); +/** Decorative index cards — suggests PR graph without duplicating the interactive demo. */ +const HERO_SIGNALS = [ + { + pr: "PR #847", + title: "Rate limiter: token bucket vs sliding window", + tag: "Decision", + }, + { + pr: "PR #412", + title: "Pin axios after npm incident: documented exception", + tag: "Security", + }, + { + pr: "PR #203", + title: "Reject Redis cluster: ops cost vs Memcached", + tag: "Architecture", + }, +] as const; const HeroSection = () => { const { user, loading } = useAuth(); const primaryCtaLabel = user ? "Go to Dashboard" : "Connect GitHub Repo"; return ( -
- {/* Local hero wash (adds depth on top of fixed backdrop) */} -
- +
-
-
- - - + {/* Large screens: only the two-column hero is vertically centered; live demo scrolls below */} +
+
+
+ {/* Primary story */} +
+
+
+ + + + + + From merged PRs and review threads, not from guesses. + +
-

- Your team made 1000 decisions. -

-

- - None of them are searchable. - -

-

- Until now. -

+

+ Your team made 1000 decisions. +

+

+ + None of them are + {" "} + searchable. +

+

+ Until now. +

- - - GitLore reads your entire PR history — titles, descriptions, review comments, debates — and builds a Knowledge Graph of every + + GitLore reads your entire PR history (titles, descriptions, review comments, debates) and builds a Knowledge Graph of every decision your team has ever made. Search it. Chat with it. Get cited answers in seconds. - - + -
- - - {loading ? "Connect GitHub Repo" : primaryCtaLabel} - - - - See how it works - -
- {user && !loading ? ( -

- Welcome back, @{user.username} -

- ) : null} +
+ + + {loading ? "Connect GitHub Repo" : primaryCtaLabel} + + + + See how it works + +
+ + {user && !loading ? ( +

+ Welcome back, @{user.username} +

+ ) : null} +
-
- + {/* Signal stack — desktop */} + + + {/* Same cards, horizontal scroll on small screens */} +
+

What disappears today

+
    + {HERO_SIGNALS.map((s) => ( +
  • +
    + {s.pr} + + {s.tag} + +
    +

    {s.title}

    +
  • + ))} +
+
+
Try it - +
diff --git a/Frontend/src/components/landing/HowItWorks.tsx b/Frontend/src/components/landing/HowItWorks.tsx index aecfd5f..f622784 100644 --- a/Frontend/src/components/landing/HowItWorks.tsx +++ b/Frontend/src/components/landing/HowItWorks.tsx @@ -7,7 +7,7 @@ const steps = [ }, { title: "Ingest", - body: "Fetches merged PRs via GitHub GraphQL — titles, descriptions, reviews, linked issues, changed files.", + body: "Fetches merged PRs via GitHub GraphQL: titles, descriptions, reviews, linked issues, changed files.", }, { title: "Extract", @@ -29,14 +29,7 @@ const steps = [ const HowItWorks = () => { return ( -
-
+
@@ -45,13 +38,13 @@ const HowItWorks = () => {

How It Works

-
+
{steps.map((step, i) => (
- + {String(i + 1).padStart(2, "0")}

diff --git a/Frontend/src/components/landing/KnowledgeGraph.tsx b/Frontend/src/components/landing/KnowledgeGraph.tsx index e4e4cd8..567ca75 100644 --- a/Frontend/src/components/landing/KnowledgeGraph.tsx +++ b/Frontend/src/components/landing/KnowledgeGraph.tsx @@ -13,8 +13,11 @@ type SimNode = { }; const N = 25; -/** Physics steps before the graph is “settled” (sparse layout needs a few more steps than a tight blob). */ -const SIM_FRAMES = 64; +/** Main phase: attraction + repulsion (graph structure). */ +const SIM_FRAMES = 56; +/** Final phase: repulsion + walls only — spreads nodes apart with visible gaps. */ +const SPARSE_FRAMES = 36; +const TOTAL_PHYSICS_FRAMES = SIM_FRAMES + SPARSE_FRAMES; const EDGE_OPACITY = 0.12; const MIN_SIZE = 64; @@ -26,8 +29,9 @@ function randomGraph(w: number, h: number): { nodes: SimNode[]; edges: [number, const cy = h / 2; const innerW = Math.max(40, w - pad * 2); const innerH = Math.max(40, h - pad * 2); - const spread = Math.min(innerW, innerH) * 0.42; - const golden = 2.39996322972865332; // ~π * (3 − √5) — fills disk evenly, avoids a tight center blob + /* Start from a wider disk so the sparse phase does not clip nodes at the walls. */ + const spread = Math.min(innerW, innerH) * 0.52; + const golden = 2.39996322972865332; // ~π * (3 − √5); fills disk evenly, avoids a tight center blob for (let i = 0; i < N; i++) { const roll = rand(); const color = roll < 0.8 ? "#34D399" : roll < 0.95 ? "#FBBF24" : "#F87171"; @@ -47,7 +51,7 @@ function randomGraph(w: number, h: number): { nodes: SimNode[]; edges: [number, r: baseR, baseR, color, - label: `auth_service.py — ${Math.floor(rand() * 80)} changes · ${2 + Math.floor(rand() * 5)} authors`, + label: `auth_service.py · ${Math.floor(rand() * 80)} changes · ${2 + Math.floor(rand() * 5)} authors`, }); } const edges: [number, number][] = []; @@ -72,10 +76,11 @@ type Sim = { function stepPhysics(sim: Sim, w: number, h: number) { const { nodes, edges } = sim; const pad = 26; - const kRep = 260; - const kAtt = 0.0055; - const kWall = 0.055; - const damp = 0.86; + const sparsePhase = sim.physicsSteps >= SIM_FRAMES; + const kRep = sparsePhase ? 520 : 300; + const kAtt = sparsePhase ? 0 : 0.0042; + const kWall = sparsePhase ? 0.08 : 0.055; + const damp = sparsePhase ? 0.82 : 0.86; for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { @@ -91,16 +96,18 @@ function stepPhysics(sim: Sim, w: number, h: number) { nodes[j].vy += fy; } } - for (const [a, b] of edges) { - const dx = nodes[b].x - nodes[a].x; - const dy = nodes[b].y - nodes[a].y; - nodes[a].vx += dx * kAtt; - nodes[a].vy += dy * kAtt; - nodes[b].vx -= dx * kAtt; - nodes[b].vy -= dy * kAtt; + if (!sparsePhase) { + for (const [a, b] of edges) { + const dx = nodes[b].x - nodes[a].x; + const dy = nodes[b].y - nodes[a].y; + nodes[a].vx += dx * kAtt; + nodes[a].vy += dy * kAtt; + nodes[b].vx -= dx * kAtt; + nodes[b].vy -= dy * kAtt; + } } for (const n of nodes) { - // No center gravity — that collapsed everything to the middle. Soft walls keep the graph in the frame + // No center gravity: that collapsed everything to the middle. Soft walls keep the graph in the frame // while repulsion keeps nodes visually sparse. if (n.x < pad) n.vx += (pad - n.x) * kWall; if (n.x > w - pad) n.vx -= (n.x - (w - pad)) * kWall; @@ -129,7 +136,8 @@ function renderGraph(canvas: HTMLCanvasElement, sim: Sim) { ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, w, h); - ctx.strokeStyle = `rgba(255,255,255,${EDGE_OPACITY})`; + const light = typeof document !== "undefined" && document.documentElement.getAttribute("data-theme") === "light"; + ctx.strokeStyle = light ? `rgba(0,0,0,${EDGE_OPACITY * 0.9})` : `rgba(255,255,255,${EDGE_OPACITY})`; ctx.lineWidth = 1; for (const [a, b] of edges) { ctx.beginPath(); @@ -170,7 +178,24 @@ function pickNodeIndex(sim: Sim, mx: number, my: number): number | null { return null; } -const KnowledgeGraph = () => { +export type KnowledgeGraphCanvasProps = { + /** Outer wrapper (sets available space for layout / resize) */ + className?: string; + /** Tailwind classes on the canvas (height, min-height, radius) */ + canvasClassName?: string; + showCaption?: boolean; + captionClassName?: string; + /** Pin caption over the bottom of the canvas so the graph can use full height */ + captionOverlay?: boolean; +}; + +export function KnowledgeGraphCanvas({ + className = "", + canvasClassName = "", + showCaption = true, + captionClassName = "mt-2 text-center font-body text-[11px] text-[var(--text-ghost)] md:text-[12px]", + captionOverlay = false, +}: KnowledgeGraphCanvasProps) { const canvasRef = useRef(null); const wrapRef = useRef(null); const simRef = useRef(null); @@ -190,14 +215,14 @@ const KnowledgeGraph = () => { const w = canvas.width / dpr; const h = canvas.height / dpr; - if (sim.physicsSteps < SIM_FRAMES) { + if (sim.physicsSteps < TOTAL_PHYSICS_FRAMES) { stepPhysics(sim, w, h); sim.physicsSteps += 1; } renderGraph(canvas, sim); - if (sim.physicsSteps >= SIM_FRAMES) { + if (sim.physicsSteps >= TOTAL_PHYSICS_FRAMES) { sim.settled = true; sim.raf = null; runningRef.current = false; @@ -421,6 +446,49 @@ const KnowledgeGraph = () => { }; }, [startSim, stopSim]); + const caption = showCaption ? ( +

+ Hover a node for details · Drag to pull files and watch how PRs link them +

+ ) : null; + + const graph = ( +
+ +
+ {captionOverlay && caption} +
+ ); + + if (captionOverlay) { + return
{graph}
; + } + + return ( + <> + {graph} + {caption} + + ); +} + +export default function KnowledgeGraph() { return (
@@ -431,27 +499,16 @@ const KnowledgeGraph = () => {

Your codebase as a knowledge graph.

-
- -
-
-

- Hover a node for details · Drag to pull files and watch how PRs link them -

+
); -}; - -export default KnowledgeGraph; +} diff --git a/Frontend/src/components/landing/LandingFooter.tsx b/Frontend/src/components/landing/LandingFooter.tsx index 57cb323..9c041eb 100644 --- a/Frontend/src/components/landing/LandingFooter.tsx +++ b/Frontend/src/components/landing/LandingFooter.tsx @@ -13,17 +13,14 @@ const FooterColTitle = ({ children }: { children: string }) => ( const LandingFooter = () => { return ( -

-
- {/* Pillar 1 — largest */} -
-
+
+
+
- +
-

+

Knowledge Graph

Build once. Search forever.

-

- One click to ingest your merged PRs. GitLore extracts every decision — what was chosen, what was rejected, who decided, and why. +

+ One click to ingest your merged PRs. GitLore extracts every decision: what was chosen, what was rejected, who decided, and why. Search with natural language. Chat with cited answers.

-
- +
+
-
-
-
-
- - - -
-

Chrome Extension

-

Right on GitHub. Zero switching.

-
+
+
+
+ + + +
+

Chrome Extension

+

Right on GitHub. Zero switching.

-

- A permanent floating button on every GitHub repo page. Click it — chat with the repo's Knowledge Graph instantly. No new - tabs. No context switching. -

-
+

+ A permanent floating button on every GitHub repo page. Click it to chat with the repo's Knowledge Graph instantly. No new + tabs. No context switching. +

+
+
-
-
-
- - - -
-

Automated PR Workflows

-

Every review explained. Every merge indexed.

-
+
+
+
+ + + +
+

Automated PR Workflows

+

Every review explained. Every merge indexed.

-

- Three automations via SuperPlane: review comments auto-explained with pattern name, fix, and confidence; merged PRs refresh the - Knowledge Graph; new PRs surface related past decisions proactively. -

-
+

+ Three automations via SuperPlane: review comments auto-explained with pattern name, fix, and confidence; merged PRs refresh the + Knowledge Graph; new PRs surface related past decisions proactively. +

+
+ +
diff --git a/Frontend/src/components/landing/WhyDifferentiatorFlow.tsx b/Frontend/src/components/landing/WhyDifferentiatorFlow.tsx new file mode 100644 index 0000000..ccceef0 --- /dev/null +++ b/Frontend/src/components/landing/WhyDifferentiatorFlow.tsx @@ -0,0 +1,227 @@ +import { Fragment } from "react"; +import { useTheme } from "@/context/ThemeContext"; + +const LOGO_BASE = `${import.meta.env.BASE_URL}landing-logos`; + +type ToolStep = { + kind: "tool"; + logoSrc: string; + logoAlt: string; + product: string; + dimension: string; +}; + +type GapStep = { + kind: "gap"; + title: string; + subtitle: string; +}; + +type GitLoreStep = { + kind: "gitlore"; + product: string; + dimension: string; + tagline: string; +}; + +const STEPS: (ToolStep | GapStep | GitLoreStep)[] = [ + { + kind: "tool", + logoSrc: `${LOGO_BASE}/git.svg`, + logoAlt: "Git", + product: "git blame", + dimension: "Who", + }, + { + kind: "tool", + logoSrc: `${LOGO_BASE}/githubcopilot.svg`, + logoAlt: "GitHub Copilot", + product: "GitHub Copilot", + dimension: "What", + }, + { + kind: "tool", + logoSrc: `${LOGO_BASE}/gitlens-mark.svg`, + logoAlt: "GitLens", + product: "GitLens", + dimension: "When", + }, + { kind: "gap", title: "Why?", subtitle: "Missing" }, + { kind: "gitlore", product: "GitLore", dimension: "Why", tagline: "Decisions, cited" }, +]; + +const CIRCLE = + "flex h-[48px] w-[48px] shrink-0 items-center justify-center rounded-full border bg-[var(--elevated)] sm:h-[52px] sm:w-[52px]"; + +function ToolCircle({ step }: { step: ToolStep }) { + const { theme } = useTheme(); + const monoNight = theme === "dark"; + + return ( +
+ +
+ ); +} + +function GapCircle({ step }: { step: GapStep }) { + return ( +
+ ? +
+ ); +} + +function GitLoreCircle() { + return ( +
+ +
+ ); +} + +function StepCopy({ step }: { step: ToolStep | GapStep | GitLoreStep }) { + if (step.kind === "tool") { + return ( + <> +

{step.product}

+

{step.dimension}

+ + ); + } + if (step.kind === "gap") { + return ( + <> +

{step.subtitle}

+

{step.title}

+ + ); + } + return ( + <> +

{step.tagline}

+

{step.dimension}

+

{step.product}

+ + ); +} + +function StepCircle({ step }: { step: ToolStep | GapStep | GitLoreStep }) { + if (step.kind === "tool") return ; + if (step.kind === "gap") return ; + return ; +} + +function ConnectorLine() { + /* Vertical center of ~52px circles */ + return
; +} + +function stepAriaLabel(step: ToolStep | GapStep | GitLoreStep): string { + if (step.kind === "tool") return `${step.logoAlt}: ${step.dimension}`; + if (step.kind === "gap") return "Why: not answered by blame, Copilot, or GitLens"; + return `${step.product}: ${step.dimension}`; +} + +/** + * Horizontal “who → what → when → gap → why” rail after the three pillars. + */ +export function DifferentiatorFlowSection() { + return ( +
+
+

Where GitLore fits

+
+

+ Familiar tools answer who, what, and when.{" "} + GitLore answers why. +

+

+ Blame, Copilot, and GitLens stay in your workflow. GitLore adds a searchable layer on top of merged PRs and review threads. +

+ +
+ {/* Desktop rail */} +
+
+ {STEPS.map((step, i) => ( + + {i > 0 ? : null} +
+ + +
+
+ ))} +
+
+ + {/* Mobile: stacked rows */} +
    + {STEPS.map((step, i) => ( +
  • + +
    + {step.kind === "tool" && ( + <> +

    {step.product}

    +

    {step.dimension}

    + + )} + {step.kind === "gap" && ( + <> +

    {step.subtitle}

    +

    {step.title}

    + + )} + {step.kind === "gitlore" && ( + <> +

    {step.tagline}

    +

    + {step.dimension} · {step.product} +

    + + )} +
    +
  • + ))} +
+
+ +

+ Git, GitHub, GitHub Copilot, and GitLens are trademarks of their respective owners. GitLore is not affiliated with or endorsed by them. +

+
+ ); +} diff --git a/Frontend/src/data/referencePatterns.ts b/Frontend/src/data/referencePatterns.ts index 452fd7f..013a725 100644 --- a/Frontend/src/data/referencePatterns.ts +++ b/Frontend/src/data/referencePatterns.ts @@ -30,7 +30,7 @@ export const REFERENCE_PATTERNS: ReferencePattern[] = [ }, { id: "ref-xss-innerhtml", - name: "XSS — innerHTML", + name: "XSS: innerHTML", langs: ["JavaScript", "TypeScript"], severity: "critical", category: "security", @@ -141,7 +141,7 @@ export const REFERENCE_PATTERNS: ReferencePattern[] = [ }, { id: "ref-race-setstate", - name: "Race Condition — setState", + name: "Race Condition: setState", langs: ["JavaScript", "TypeScript"], severity: "high", category: "reliability", @@ -150,7 +150,7 @@ export const REFERENCE_PATTERNS: ReferencePattern[] = [ }, { id: "ref-memory-leak-useeffect", - name: "Memory Leak — useEffect", + name: "Memory Leak: useEffect", langs: ["JavaScript", "TypeScript"], severity: "high", category: "reliability", diff --git a/Frontend/src/index.css b/Frontend/src/index.css index 6a2c0a3..d4978e6 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -152,6 +152,11 @@ html[data-theme="light"] { font-size: 0.875rem; } + #root { + min-height: 100dvh; + background: var(--bg); + } + @media (min-width: 768px) { body { font-size: 1rem; @@ -177,21 +182,6 @@ html[data-theme="light"] { overflow-x: auto; -webkit-overflow-scrolling: touch; } - - body::after { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 9999; - opacity: 0.02; - mix-blend-mode: overlay; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E"); - } - - html[data-theme="light"] body::after { - opacity: 0.01; - } } /* CodeMirror demo theme */ @@ -233,14 +223,15 @@ html[data-theme="light"] { color: var(--accent); } +/* Landing width + horizontal padding (matches gitlore-your-code-s-storyteller) */ .landing-container { box-sizing: border-box; width: 100%; max-width: 1100px; margin-left: auto; margin-right: auto; - padding-left: max(16px, env(safe-area-inset-left)); - padding-right: max(16px, env(safe-area-inset-right)); + padding-left: max(20px, env(safe-area-inset-left)); + padding-right: max(20px, env(safe-area-inset-right)); } @media (min-width: 768px) { @@ -267,24 +258,21 @@ html[data-theme="light"] { transform: translateY(0); } -/* BlurReveal */ +/* BlurReveal — fade/slide only (no filter blur: blur creates a soft halo on dark backgrounds) */ .blur-reveal { - filter: blur(12px); opacity: 0; - transform: translateY(8px); + transform: translateY(10px); transition: - filter 0.8s ease, - opacity 0.7s ease, - transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); + opacity 0.65s ease, + transform 0.65s cubic-bezier(0.16, 1, 0.3, 1); transition-delay: var(--blur-delay, 0ms); } .blur-reveal.blur-visible { - filter: blur(0); opacity: 1; transform: translateY(0); } -/* Landing navbar (scroll state) */ +/* Landing navbar (storyteller: transparent → frosted when scrolled) */ .landing-nav { backdrop-filter: blur(0px); -webkit-backdrop-filter: blur(0px); @@ -299,87 +287,24 @@ html[data-theme="light"] { -webkit-backdrop-filter: blur(16px) saturate(1.4); background: rgba(8, 8, 10, 0.88); border-bottom-color: rgba(255, 255, 255, 0.06); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04), 0 12px 40px -12px rgba(0, 0, 0, 0.5); } html[data-theme="light"] .landing-nav.scrolled { background: rgba(252, 252, 254, 0.9); border-bottom-color: rgba(0, 0, 0, 0.06); - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04), 0 8px 32px -8px rgba(0, 0, 0, 0.08); } -/* Landing page — fixed atmospheric layers (sits under .landing-content) */ -.landing-page { - isolation: isolate; -} -.landing-fixed-backdrop { - position: fixed; - inset: 0; - z-index: 0; - pointer-events: none; - overflow: hidden; -} -.landing-fixed-backdrop__glow { - position: absolute; - inset: 0; - background: - radial-gradient(ellipse 95% 60% at 50% -25%, var(--accent-glow), transparent 58%), - radial-gradient(ellipse 50% 45% at 0% 35%, rgba(129, 140, 248, 0.07), transparent 52%), - radial-gradient(ellipse 48% 42% at 100% 55%, rgba(201, 168, 76, 0.06), transparent 48%), - radial-gradient(ellipse 70% 50% at 50% 110%, rgba(201, 168, 76, 0.04), transparent 55%); -} -html[data-theme="light"] .landing-fixed-backdrop__glow { - background: - radial-gradient(ellipse 95% 55% at 50% -20%, var(--accent-glow), transparent 55%), - radial-gradient(ellipse 50% 40% at 0% 30%, rgba(91, 95, 199, 0.06), transparent 50%), - radial-gradient(ellipse 45% 38% at 100% 50%, rgba(154, 123, 46, 0.07), transparent 45%); -} -.landing-fixed-backdrop__grid { - position: absolute; - inset: 0; - background-image: - linear-gradient(rgba(255, 255, 255, 0.028) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.028) 1px, transparent 1px); - background-size: 56px 56px; - -webkit-mask-image: linear-gradient(to bottom, black 0%, black 40%, transparent 95%); - mask-image: linear-gradient(to bottom, black 0%, black 40%, transparent 95%); - opacity: 0.65; -} -html[data-theme="light"] .landing-fixed-backdrop__grid { - background-image: - linear-gradient(rgba(0, 0, 0, 0.04) 1px, transparent 1px), - linear-gradient(90deg, rgba(0, 0, 0, 0.04) 1px, transparent 1px); - opacity: 0.45; -} -.landing-fixed-backdrop__noise { - position: absolute; - inset: 0; - opacity: 0.035; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); -} -html[data-theme="light"] .landing-fixed-backdrop__noise { - opacity: 0.02; -} -.landing-content { - position: relative; - z-index: 1; -} - -/* Glass panels & demo chrome (landing) */ +/* Live demo / framed blocks — storyteller-style tight radius, border-led */ .landing-glass-panel { box-sizing: border-box; max-width: 100%; - border: 1px solid var(--border-strong); - border-radius: 1rem; - background: color-mix(in srgb, var(--surface) 55%, transparent); - -webkit-backdrop-filter: blur(14px) saturate(1.25); - backdrop-filter: blur(14px) saturate(1.25); - box-shadow: - 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent), - 0 24px 64px -24px rgba(0, 0, 0, 0.45); + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface); + box-shadow: none; } html[data-theme="light"] .landing-glass-panel { - background: color-mix(in srgb, var(--surface) 72%, transparent); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 15%, transparent), 0 20px 48px -16px rgba(0, 0, 0, 0.1); + background: var(--surface); + box-shadow: none; } /* Magnet */ @@ -403,124 +328,48 @@ html[data-theme="light"] .landing-glass-panel { transform: translate(0, 0); } -/* Section label + rule (landing editorial) */ +/* Section label + rule (storyteller: mono label + hairline, no dot) */ .section-label { display: flex; - flex-wrap: wrap; align-items: center; - gap: 12px 16px; + gap: 16px; margin-bottom: 32px; } .section-label p { - display: inline-flex; - align-items: center; - gap: 10px; - max-width: 100%; font-family: "JetBrains Mono", monospace; font-size: 10px; text-transform: uppercase; - letter-spacing: 0.18em; - color: var(--text-secondary); - white-space: normal; - line-height: 1.4; -} -@media (min-width: 480px) { - .section-label p { - letter-spacing: 3px; - white-space: nowrap; - } - .section-label { - flex-wrap: nowrap; - } -} -.section-label p::before { - content: ""; - width: 6px; - height: 6px; - flex-shrink: 0; - border-radius: 9999px; - background: var(--accent); - box-shadow: 0 0 14px var(--accent-glow); + letter-spacing: 3px; + color: var(--text-ghost); + white-space: nowrap; } .section-label::after { content: ""; flex: 1; - min-width: 48px; height: 1px; - background: linear-gradient(90deg, var(--border-strong), transparent); -} -@media (max-width: 479px) { - .section-label::after { - flex-basis: 100%; - min-width: 0; - } + background: var(--border); } -/* Bento / feature cards — depth + hover lift */ +/* Bento cells — storyteller: border hover only, no lift/shadow */ .bento-card { border: 1px solid var(--border); - transition: - border-color 0.35s ease, - box-shadow 0.35s ease, - transform 0.35s cubic-bezier(0.33, 1, 0.68, 1); - box-shadow: 0 4px 28px -8px rgba(0, 0, 0, 0.35); + transition: border-color 0.3s ease; } .bento-card:hover { - border-color: var(--border-accent); - box-shadow: - 0 24px 56px -16px rgba(0, 0, 0, 0.45), - 0 0 0 1px var(--accent-dim); - transform: translateY(-3px); -} -html[data-theme="light"] .bento-card { - box-shadow: 0 4px 24px -6px rgba(0, 0, 0, 0.08); -} -html[data-theme="light"] .bento-card:hover { - box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.12), 0 0 0 1px var(--accent-dim); -} -@media (prefers-reduced-motion: reduce) { - .bento-card { - transition: border-color 0.2s ease, box-shadow 0.2s ease; - } - .bento-card:hover { - transform: none; - } + border-color: var(--border-strong); } -/* Hero secondary CTA */ +/* Hero secondary CTA (storyteller: outline, transparent fill) */ .hero-secondary-cta { - -webkit-backdrop-filter: blur(10px); - backdrop-filter: blur(10px); - background: color-mix(in srgb, var(--surface) 40%, transparent) !important; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); + background: transparent !important; + box-shadow: none; } html[data-theme="light"] .hero-secondary-cta { - background: color-mix(in srgb, var(--surface) 65%, transparent) !important; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); + background: transparent !important; } .hero-secondary-cta:hover { - border-color: var(--border-accent) !important; + border-color: var(--text-secondary) !important; color: var(--text) !important; - background: color-mix(in srgb, var(--surface-hover) 55%, transparent) !important; -} - -/* Hero floating code preview card */ -@keyframes hero-code-float { - 0%, - 100% { - transform: translateY(0) rotate(0.45deg); - } - 50% { - transform: translateY(-10px) rotate(-0.45deg); - } -} -.hero-code-float { - animation: hero-code-float 7s ease-in-out infinite; -} -@media (prefers-reduced-motion: reduce) { - .hero-code-float { - animation: none; - } } /* === BentoGrid diff stagger (requires .fade-in.fade-visible ancestor) === */ diff --git a/Frontend/src/lib/gitloreApi.ts b/Frontend/src/lib/gitloreApi.ts index f83adc5..7186afc 100644 --- a/Frontend/src/lib/gitloreApi.ts +++ b/Frontend/src/lib/gitloreApi.ts @@ -911,7 +911,7 @@ export async function postVoiceTts( try { const j = (await res.json()) as { error?: string; message?: string; hint?: string }; const parts = [j.error || j.message, j.hint].filter(Boolean); - msg = parts.length ? parts.join(" — ") : msg; + msg = parts.length ? parts.join(". ") : msg; } catch { try { const t = await res.text(); diff --git a/Frontend/src/pages/AppView.tsx b/Frontend/src/pages/AppView.tsx index 3b8e582..9a640ad 100644 --- a/Frontend/src/pages/AppView.tsx +++ b/Frontend/src/pages/AppView.tsx @@ -343,47 +343,76 @@ const DiffViewer = ({ ); }; -const TIMELINE_FONT = 'Inter, system-ui, sans-serif'; - +/** + * HTML/CSS timeline — label type uses fixed px (Tailwind text-[12px] / text-[11px]) so resizing + * the /app split panes does not scale text. (SVG + viewBox + w-full made every user-unit—including + * fontSize—scale with container width.) + */ const StoryTimeline = ({ dots }: { dots: TimelineDot[] }) => { - const svgRef = useRef(null); - const containerWidth = 600; - const dotR = 6; - const spacing = dots.length > 1 ? (containerWidth - 40) / (dots.length - 1) : 0; + const rootRef = useRef(null); + const n = dots.length; useEffect(() => { - if (!svgRef.current) return; - const circles = svgRef.current.querySelectorAll(".timeline-dot"); + if (!rootRef.current) return; + const els = rootRef.current.querySelectorAll(".timeline-dot"); const ctx = gsap.context(() => { - gsap.from(circles, { scale: 0, opacity: 0, stagger: 0.2, duration: 0.4, ease: "back.out(1.7)", transformOrigin: "center" }); + gsap.from(els, { + scale: 0, + opacity: 0, + stagger: 0.2, + duration: 0.4, + ease: "back.out(1.7)", + transformOrigin: "center center", + }); }); return () => ctx.revert(); - }, []); + }, [dots]); + + if (n === 0) return null; return ( -
- - {dots.length > 1 && ( - +
+
+ {n > 1 && ( +
)} - {dots.map((dot, i) => { - const cx = dots.length > 1 ? 20 + i * spacing : containerWidth / 2; - return ( - - - - {dot.label} - - - {dot.sublabel} - - - {dot.date} - - - ); - })} - +
+ {dots.map((dot, i) => ( +
+
+
+ ))} + {dots.map((dot, i) => { + const align = + n === 1 ? "text-center" : i === 0 ? "text-left" : i === n - 1 ? "text-right" : "text-center"; + return ( +
+

+ {dot.label} +

+

+ {dot.sublabel} +

+

{dot.date}

+
+ ); + })} +
+
); }; @@ -1714,12 +1743,12 @@ const AppView = () => {
- {target.filePath || "— no file selected —"} + {target.filePath || "No file selected"}
- {target.filePath || "— no file selected —"} + {target.filePath || "No file selected"}
diff --git a/Frontend/src/pages/Landing.tsx b/Frontend/src/pages/Landing.tsx index 9595c8c..8f9ef34 100644 --- a/Frontend/src/pages/Landing.tsx +++ b/Frontend/src/pages/Landing.tsx @@ -21,21 +21,16 @@ const Landing = () => { }, [oauthError, searchParams, setSearchParams]); return ( -
-
-
-
-
-
+
{oauthError && (
GitHub sign-in was cancelled or failed ({oauthError}). Try again from Connect.
)} -
+
}> diff --git a/Frontend/src/pages/Overview.tsx b/Frontend/src/pages/Overview.tsx index bee7f2e..b806f40 100644 --- a/Frontend/src/pages/Overview.tsx +++ b/Frontend/src/pages/Overview.tsx @@ -226,13 +226,13 @@ const Overview = () => {
{[ - { value: stats ? fmt(stats.stars) : "—", label: "Stars" }, - { value: stats ? fmt(stats.forks) : "—", label: "Forks" }, - { value: stats ? fmt(stats.pullRequests) : "—", label: "Open + closed PRs (total)" }, - { value: stats ? fmt(stats.commits) : "—", label: "Commits (default branch)" }, - { value: stats?.issues != null ? fmt(stats.issues) : "—", label: "Issues" }, - { value: stats?.contributors != null ? fmt(stats.contributors) : "—", label: "Contributors" }, - { value: stats?.files != null ? fmt(stats.files) : "—", label: "Files (tree)" }, + { value: stats ? fmt(stats.stars) : "-", label: "Stars" }, + { value: stats ? fmt(stats.forks) : "-", label: "Forks" }, + { value: stats ? fmt(stats.pullRequests) : "-", label: "Open + closed PRs (total)" }, + { value: stats ? fmt(stats.commits) : "-", label: "Commits (default branch)" }, + { value: stats?.issues != null ? fmt(stats.issues) : "-", label: "Issues" }, + { value: stats?.contributors != null ? fmt(stats.contributors) : "-", label: "Contributors" }, + { value: stats?.files != null ? fmt(stats.files) : "-", label: "Files (tree)" }, ].map((stat) => (
{stat.value}
@@ -314,8 +314,8 @@ const Overview = () => { {liveEvents.map((ev, i) => (
  • - {ev.file_path || "—"} - {ev.line != null ? `:${ev.line}` : ""} — {ev.pattern_name || "Explanation cached"} + {ev.file_path || "-"} + {ev.line != null ? `:${ev.line}` : ""} · {ev.pattern_name || "Explanation cached"}
    {ev.confidence} confidence · {formatCacheEventTime(ev.timestamp)} @@ -377,7 +377,7 @@ const Overview = () => {
      {mongoAnalytics.patterns.slice(0, 5).map((p) => (
    • - {p._id || "—"} + {p._id || "-"} {p.count}×
    • ))} diff --git a/Frontend/src/pages/Patterns.tsx b/Frontend/src/pages/Patterns.tsx index 5542e9b..e2ff236 100644 --- a/Frontend/src/pages/Patterns.tsx +++ b/Frontend/src/pages/Patterns.tsx @@ -241,7 +241,7 @@ const Patterns = () => {

      Patterns & themes

      - Repo {repoFull || "—"} + Repo {repoFull || "-"} {repoPrimaryLang ? ( <> {" "} @@ -257,7 +257,7 @@ const Patterns = () => { labels, Analyze-line narratives) plus a lightweight code scan of up to 50 text files on the branch you selected in the header. Churn hotspots and decision timelines are derived from knowledge-graph nodes. At the bottom, reference cards are - curated examples — they light up with a gold border when the scan finds a matching rule id in your tree. + curated examples; they light up with a gold border when the scan finds a matching rule id in your tree.

      @@ -302,13 +302,13 @@ const Patterns = () => { to="/overview" className="rounded-sm border border-gitlore-accent/40 bg-gitlore-accent/10 px-3 py-1.5 font-medium text-gitlore-accent transition-colors hover:bg-gitlore-accent/20" > - Overview — build knowledge graph + Overview: build knowledge graph - Live repo — Explain comments & Analyze lines + Live repo: Explain comments & Analyze lines
    @@ -395,7 +395,7 @@ const Patterns = () => { Churn hotspots (files in many PRs)
  • - Paths that appear in changed_files on multiple ingested PRs — + Paths that appear in changed_files on multiple ingested PRs are good candidates for refactors or ownership discussion.

    @@ -427,7 +427,7 @@ const Patterns = () => { {h.file} - {h.prCount} PRs · {h.types.join(", ") || "—"} + {h.prCount} PRs · {h.types.join(", ") || "-"}
    @@ -494,7 +494,7 @@ const Patterns = () => { Possible decision shifts (heuristic)

    - Later PR decisions that share terms with earlier listed alternatives — not proof of reversal, but a cue + Later PR decisions that share terms with earlier listed alternatives are not proof of reversal, but a cue to read both threads.

    @@ -555,7 +555,7 @@ const Patterns = () => { {scan && !scanLoading && (

    - Branch {scan.branch || "—"} · {scan.fileCount} files touched ·{" "} + Branch {scan.branch || "-"} · {scan.fileCount} files touched ·{" "} {scan.cached ? "served from cache" : "fresh scan"} · {new Date(scan.scannedAt).toLocaleString()}

    )} @@ -739,7 +739,7 @@ const Patterns = () => {

    Reference examples

    Curated anti-pattern vs better-pattern snippets. Cards with a gold border had at least one matching hit in the - last code scan (rule ids line up with the scanner). Everything here is educational — always confirm in context. + last code scan (rule ids line up with the scanner). Everything here is educational; always confirm in context.

    @@ -851,7 +851,7 @@ const Patterns = () => { {p.correct}
    - Not a substitute for review — cross-check with the Live repo and your team's standards. + Not a substitute for review; cross-check with the Live repo and your team's standards.
    ); diff --git a/Frontend/src/pages/RepoSelect.tsx b/Frontend/src/pages/RepoSelect.tsx index 2a87b24..640a5fd 100644 --- a/Frontend/src/pages/RepoSelect.tsx +++ b/Frontend/src/pages/RepoSelect.tsx @@ -11,9 +11,9 @@ const PAGE_SIZE = 12; const FETCH_LIMIT = 100; function fmtUpdated(iso: string | null) { - if (!iso) return "—"; + if (!iso) return "-"; const d = new Date(iso); - if (Number.isNaN(d.getTime())) return "—"; + if (Number.isNaN(d.getTime())) return "-"; return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); } diff --git a/Frontend/vercel.json b/Frontend/vercel.json new file mode 100644 index 0000000..6cc304b --- /dev/null +++ b/Frontend/vercel.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "vite", + "buildCommand": "pnpm run build", + "outputDirectory": "dist", + "installCommand": "pnpm install", + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +} diff --git a/README.md b/README.md index 85555ef..8469940 100644 --- a/README.md +++ b/README.md @@ -1,233 +1,279 @@ -# GitLore +

    GitLore

    +

    Your codebase's institutional memory.

    +

    GitLore turns your GitHub PR history into a searchable Knowledge Graph — every decision your team ever made, findable in seconds. Search it. Chat with it. Get cited answers. Works on the web, on GitHub via Chrome Extension, and in your CI via PR Intelligence webhooks.

    +

    Built at HackByte 4.0 — IIITDM Jabalpur, April 2026

    -GitLore helps developers understand a codebase through **git history**, **pull requests**, and **review context**. It combines a React frontend with a Node (Hono) backend, **MongoDB** for storage, **GitHub OAuth** for sign-in, and optional **Google Gemini** for narratives, explanations, and **knowledge-graph Q&A**. +--- + +## The Problem + +Your team has made hundreds of decisions across merged PRs — architecture choices, rejected alternatives, tradeoffs. But when a new developer asks "why do we use Redis here?", the answer is buried in PR #247 from 18 months ago. + +**git blame** tells you WHO. **Copilot** tells you WHAT. **Nobody tells you WHY.** GitLore does. --- ## Features -- **GitHub sign-in** — Session-based auth; the API acts on behalf of the logged-in user’s token (where applicable). -- **Code insights** — Explore repositories in the app: narratives tied to history, search, analysis, and review-style explanations (Gemini-backed where configured). -- **Knowledge graph (per repo)** — Ingest **merged PRs** into structured **knowledge nodes** (decision, problem, quotes, issues, embeddings). Visualize relationships and run **chat** grounded in retrieved nodes. -- **Retrieval + chat** — Chat uses a **three-tier** search (Atlas **vector** search when configured, MongoDB **full-text**, then **regex** fallback), optional **in-memory** vector similarity, then **Gemini** synthesis with strict “nodes-only” instructions. Automatic **429** retries and **model fallbacks** for free-tier quotas. +**Core** +- **Knowledge Graph** — PR ingest, Gemini extraction, vector embeddings, 3-tier semantic search +- **Chat** — natural language questions, cited answers with PR references +- **Code Archaeology** — click any line, see full decision story from git history +- **Review Explainer** — pattern, fix, confidence for terse comments + +**Automation** +- **PR Intelligence** — CodeRabbit-style: enable once, every new PR auto-commented with duplicate detection + KG context, posted as bot +- **Auto-Fix Reviews** — classify + fix trivial comments (extract → rule → AI), raise draft PR +- **SuperPlane** — event-driven Slack notifications for review explanations + KG updates + +**Platform** +- **Chrome Extension** — floating chat button on GitHub, side panel, chat with KG without leaving GitHub +- **Voice** — English + Hindi TTS (ElevenLabs), WebRTC voice agent for hands-free Q&A +- **ArmorIQ Enforcement** — 18 tool actions, policy-based allow/deny, enforcement logging +- **Knowledge Suggestions** — zero-click related decisions while browsing files +- **Patterns** — static + repo-scanned anti-patterns with severity and category +- **Decision Search** — semantic search across all indexed decisions from navbar +- **KG Visualization** — interactive graph with node types, edge types, zoom, fullscreen +- **Repo Overview Dashboard** — health score, top anti-patterns, most changed files, stats --- -## Tech stack +## Tech Stack | Layer | Stack | -|--------|--------| -| Frontend | React 18, Vite 5, TypeScript, Tailwind CSS, TanStack Query, CodeMirror | -| Backend | Node 18+, Hono 4, TypeScript, MongoDB driver, Octokit GraphQL, Google Generative AI | -| Data | MongoDB (`gitlore` database): users, caches, `knowledge_nodes`, `knowledge_progress`, etc. | +|-------|-------| +| **Frontend** | React, TypeScript, Tailwind, CodeMirror, GSAP, anime.js, react-markdown | +| **Backend** | Hono, Node.js, TypeScript, Zod | +| **AI/ML** | Gemini 2.5 Flash + Flash-Lite, text-embedding-004 (768-dim vectors) | +| **Database** | MongoDB Atlas + Atlas Vector Search | +| **APIs** | GitHub GraphQL + REST + OAuth + Webhooks, ElevenLabs TTS + Voice Agent | +| **Security** | ArmorIQ (intent enforcement) | +| **Automation** | SuperPlane (event-driven workflows) | +| **Deployment** | Vercel (frontend), Vultr (backend + services) | +| **Extension** | Chrome Extension (Manifest V3, side panel, service worker) | --- -## Repository layout +## Architecture ``` -GitLore/ -├── Backend/ # Hono API (PORT from env, default 3001) -│ ├── src/ -│ │ ├── server.ts -│ │ ├── routes/ # auth, repo, ingest, chat, analyze, explain, search, … -│ │ ├── lib/ # mongo, gemini, ingest, github, … -│ │ └── middleware/ -│ └── .env.example -├── Frontend/ # Vite dev server on port 8080 -│ ├── src/ -│ └── vite.config.ts # proxies /api, /auth, /health → VITE_API_ORIGIN -└── README.md +User Layer: Browser | Chrome Extension | GitHub Webhook | Voice Agent + ↓ ↓ ↓ ↓ +Frontend: React + TypeScript + Tailwind (Vercel) + Overview | AppView | Chat | KG Viz | Voice | Patterns + ↓ +Backend: Hono + Node.js + TypeScript (Vultr) — 35+ endpoints + Auth | Analyze | Explain | Search | Chat | Ingest | Voice + Webhook (PR Intel) | AutoFix | Enforcement + gemini.ts | knowledgeSearch.ts | ingest.ts | githubApp.ts + Middleware: Cookie + API Key Auth | CORS | Webhook Signature | Zod + ↓ +External: Gemini 2.5 Flash | MongoDB Atlas | GitHub API | ElevenLabs | ArmorIQ + ↓ +Deployment: Vercel (Frontend) | Vultr Cloud (Backend) | MongoDB Atlas M0 ``` +**Data Flow Pipelines:** +- **KG Ingest**: GitHub GraphQL → Gemini Extract → Embed → MongoDB +- **Chat Query**: Question → Embed → 3-Tier Search → Gemini Synthesis +- **Code Archaeology**: Line Click → Blame → Commit → PR → Reviews → Narrative +- **PR Intelligence**: PR Opened → File Overlap + KG Search → Auto-Comment +- **Auto-Fix**: Classify → Extract/Rule/AI Fix → Validate → Draft PR +- **Voice**: TTS (EN/HI) + WebRTC Agent + Gemini Q&A + --- -## Prerequisites +## Quick Start -- **Node.js** ≥ 18 -- **MongoDB** (Atlas or local) — connection string with access to a database named **`gitlore`** (created/used automatically) -- **GitHub OAuth App** — callback URL must match how you run the frontend (see below) -- **Gemini API key** (optional but recommended for narratives, PR extraction, embeddings, and knowledge-graph chat) +### Prerequisites ---- +- Node.js >= 18 +- MongoDB Atlas account (free M0 tier works) +- GitHub OAuth App +- Gemini API key -## Quick start - -### 1. Clone and install +### 1. Clone and Install ```bash +git clone https://github.com/Codealpha07/GitLore.git cd GitLore/Backend && npm install cd ../Frontend && npm install ``` -### 2. Configure the backend - -Copy `Backend/.env.example` to `Backend/.env` and set at least: - -- `MONGODB_URI` — MongoDB connection string -- `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_CALLBACK_URL` -- `SESSION_SECRET` — long random string -- `GEMINI_API_KEY` — for AI features (ingest, chat, explain, etc.) +### 2. Configure Backend -See **Environment variables** below for optional tuning (`PORT`, `CORS_ORIGIN`, model names, embedding models). - -### 3. Configure the frontend (dev) - -If the API is **not** on `http://127.0.0.1:3001`, create `Frontend/.env`: +Copy `Backend/.env.example` to `Backend/.env` and set: ```env -VITE_API_ORIGIN=http://127.0.0.1:3001 +GEMINI_API_KEY=your_key +MONGODB_URI=your_mongodb_atlas_uri +GITHUB_CLIENT_ID=your_oauth_client_id +GITHUB_CLIENT_SECRET=your_oauth_client_secret +GITHUB_CALLBACK_URL=http://localhost:8080/auth/github/callback +SESSION_SECRET=your_random_64_char_secret +PORT=3001 +CORS_ORIGIN=http://localhost:8080 ``` -(`vite.config.ts` proxies `/api`, `/auth`, and `/health` to this origin.) - -### 4. GitHub OAuth callback - -For local dev with Vite on **port 8080**, set: - +Optional (for advanced features): ```env -GITHUB_CALLBACK_URL=http://localhost:8080/auth/github/callback +# PR Intelligence Webhook +GITHUB_WEBHOOK_SECRET=your_webhook_secret +BACKEND_PUBLIC_URL=http://your-server:3001 +SUPERPLANE_API_KEY=your_api_key +SUPERPLANE_SERVICE_USERNAME=your_github_username + +# GitHub App Bot Identity +GITHUB_APP_ID=123456 +GITHUB_APP_INSTALLATION_ID=12345678 +GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" + +# Voice +ELEVENLABS_API_KEY=your_key +ELEVENLABS_VOICE_ID=your_voice_id +ELEVENLABS_AGENT_ID=your_agent_id ``` -Register the same **Authorization callback URL** in your GitHub OAuth app settings. Ensure `CORS_ORIGIN` in the backend includes your frontend origin (default in `.env.example` includes `http://localhost:8080`). +### 3. Configure Frontend -### 5. Run in development - -**Terminal 1 — API** - -```bash -cd GitLore/Backend -npm run dev +If the backend is not on `http://127.0.0.1:3001`, create `Frontend/.env`: +```env +VITE_API_ORIGIN=http://your-backend-url:3001 ``` -**Terminal 2 — UI** +### 4. Run +**Terminal 1 — Backend:** ```bash -cd GitLore/Frontend -npm run dev +cd GitLore/Backend && npm run dev ``` -Open **http://localhost:8080**. Sign in with GitHub, select a repo, and use **Overview** for the knowledge graph and chat. - -### 6. Production-style run - +**Terminal 2 — Frontend:** ```bash -cd GitLore/Backend && npm run build && npm start -cd GitLore/Frontend && npm run build && npm run preview +cd GitLore/Frontend && npm run dev ``` -Point `VITE_API_ORIGIN` / deployment URLs and `CORS_ORIGIN` at your real origins. +Open **http://localhost:8080** → Sign in with GitHub → Select a repo → Build Knowledge Graph → Chat. --- -## Environment variables +## Chrome Extension -### Backend (`Backend/.env`) +The Chrome Extension adds a floating chat button on every GitHub repo page. -| Variable | Required | Description | -|----------|----------|-------------| -| `MONGODB_URI` | Yes | MongoDB connection string | -| `GITHUB_CLIENT_ID` | Yes* | OAuth client ID | -| `GITHUB_CLIENT_SECRET` | Yes* | OAuth client secret | -| `GITHUB_CALLBACK_URL` | Yes* | Must match GitHub app callback (e.g. `http://localhost:8080/auth/github/callback`) | -| `SESSION_SECRET` | Yes | Secret for session signing | -| `GEMINI_API_KEY` | For AI | PR extraction, embeddings, chat synthesis, explain/narrate flows | -| `PORT` | No | API port (default **3001**) | -| `NODE_ENV` | No | `development` / `production` | -| `CORS_ORIGIN` | No | Comma-separated allowed origins (default includes localhost:8080) | -| `FRONTEND_URL` | No | Optional; redirect hints if not derived from callback | -| `GEMINI_GENERATION_MODEL` | No | Default text model (default **gemini-2.5-flash-lite**) | -| `GEMINI_CHAT_MODEL` | No | Override model for knowledge-graph chat only | -| `GEMINI_CHAT_MODEL_FALLBACKS` | No | Comma-separated models tried on 429 after the primary chat model | -| `GEMINI_EMBEDDING_MODELS` | No | Comma-separated embedding model names to try | -| `GITHUB_PAT` | No | Optional fallback token for server-side GitHub calls | +1. Open `chrome://extensions` → Enable Developer Mode +2. Click "Load unpacked" → Select `gitlore-extension/` folder +3. Click the extension icon → Enter your GitLore API URL → Save +4. Go to any GitHub repo → Click the floating button → Chat with the KG -\*Required for OAuth flows used by the app. +--- -Full comments and examples: **`Backend/.env.example`**. +## PR Intelligence (CodeRabbit-style) -### Frontend (`Frontend/.env`) +Enable once, every new PR gets an automatic comment with duplicate detection and Knowledge Graph context. -| Variable | Description | -|----------|-------------| -| `VITE_API_ORIGIN` | Backend base URL for Vite proxy (default `http://127.0.0.1:3001`) | +1. Set `GITHUB_WEBHOOK_SECRET` and `BACKEND_PUBLIC_URL` in `.env` +2. Go to GitLore Overview → Click "Enable PR Intelligence" +3. Open a new PR → GitLore auto-comments with related PRs and past decisions -Never put `GEMINI_API_KEY` or `GITHUB_CLIENT_SECRET` in the frontend. +For bot identity (`gitlore[bot]` badge), configure the GitHub App env vars. --- -## Knowledge graph & MongoDB +## Deployment -### Ingest - -- Trigger **Build Knowledge Graph** from the **Overview** UI (or `POST /api/repo/:owner/:name/ingest` with a JSON body; optional `limit` 1–50, default 30). -- Ingestion runs in the background; progress is stored in **`knowledge_progress`**. -- Nodes are stored in **`knowledge_nodes`** (unique per `repo` + `pr_number`). +**Frontend → Vercel:** +```bash +cd Frontend && npm run build +# Deploy dist/ to Vercel +``` -### Indexes +**Backend → Vultr (or any VPS):** +```bash +ssh root@YOUR_SERVER_IP +git clone https://github.com/Codealpha07/GitLore.git +cd GitLore/Backend +npm install +# Create .env with production values +npm run build && npm start +# Or use PM2: pm2 start dist/server.js --name gitlore-backend +``` -On startup the backend creates indexes including: +Update `CORS_ORIGIN` to include your Vercel URL. +Update `GITHUB_CALLBACK_URL` to your production backend URL. -- Text index **`knowledge_text_search`** on `knowledge_nodes` (title, summary, problem, decision, full_narrative, topics, pr_author). - If you change indexed fields, you may need to **drop** the old text index in Atlas/shell and restart so MongoDB can recreate it. +--- -### Vector search (optional) +## Repository Layout -For the **vector** tier in chat (and efficient semantic retrieval), configure a MongoDB Atlas **vector index** on `knowledge_nodes.embedding` whose **dimensions** match your embedding model (often **768** — confirm in Atlas UI and Gemini embedding docs). Without Atlas vector search, the backend can fall back to **in-memory** similarity over a capped set of documents. +``` +GitLore/ +├── Backend/ +│ ├── src/ +│ │ ├── server.ts # Hono app, route mounting +│ │ ├── routes/ # auth, analyze, explain, search, chat, ingest, +│ │ │ # voice, guardrails, enforcement, autofix, webhooks +│ │ ├── lib/ # gemini, mongo, github, githubApp, ingest, +│ │ │ # knowledgeSearch, autofix, armorclaw, patternScanner +│ │ ├── middleware/ # auth (cookie + API key) +│ │ └── webhooks/github/ # signature, processPrWebhook, kgSearch, buildComment +│ └── .env.example +├── Frontend/ +│ ├── src/ +│ │ ├── pages/ # Landing, Overview, AppView, Patterns, RepoSelect +│ │ ├── components/ # ChatPanel, KnowledgeDecisionsGraph, StoryVoiceModal, +│ │ │ # IngestButton, PrIntelligenceButton, GuardrailsModal, +│ │ │ # KnowledgeSuggestions, EnforcementLog, Navbar +│ │ ├── context/ # Auth, Repo, Theme, Toast, RouteTransition +│ │ └── lib/ # gitloreApi, codemirror, parseUnifiedDiff +│ └── vite.config.ts +├── gitlore-extension/ # Chrome Extension (Manifest V3) +│ ├── manifest.json +│ ├── background/ # Service worker (API calls) +│ ├── content/ # Floating button on GitHub +│ ├── sidepanel/ # Chat side panel +│ ├── popup/ # Settings popup +│ └── utils/ # Auth, storage, GitHub API helpers +└── README.md +``` --- -## API overview (authenticated `/api/*`) +## API Overview -All `/api/*` routes use session auth except as noted. Examples: +All `/api/*` routes require authentication (cookie or `X-GitLore-API-Key` header). -| Area | Examples | +| Area | Endpoints | |------|-----------| -| Repo | Repo metadata and GitHub-backed operations under `/api/repo/...` | -| Ingest | `POST /api/repo/:owner/:name/ingest`, status/progress endpoints | -| Chat | `GET /api/repo/:owner/:name/chat/status`, `POST /api/repo/:owner/:name/chat` | -| Legacy / core | Analyze, explain, search, narrate, guardrails — see `Backend/src/routes/` | - -Public: `GET /health`, OAuth under `/auth/*`, test routes under `/test/*` as configured. - ---- - -## Frontend routes - -| Path | Purpose | -|------|---------| -| `/` | Landing | -| `/app` | Main app / repo workspace | -| `/overview` | Overview & knowledge graph experience | -| `/patterns` | Patterns UI | +| **Auth** | `GET /auth/github`, `GET /auth/me`, `POST /auth/logout` | +| **Analysis** | `POST /api/analyze`, `POST /api/explain`, `POST /api/search` | +| **Knowledge Graph** | `POST /api/repo/:o/:n/ingest`, `GET /api/repo/:o/:n/ingest/status`, `POST /api/repo/:o/:n/chat` | +| **Voice** | `GET /api/voice/status`, `POST /api/voice/tts`, `POST /api/voice/gemini-voice-reply` | +| **Security** | `GET /api/guardrails`, `POST /api/guardrails/test`, `POST /api/enforcement/log` | +| **Auto-Fix** | `POST /api/repo/:o/:n/pulls/:n/auto-fix/classify`, `POST /api/repo/:o/:n/pulls/:n/auto-fix/apply` | +| **Webhook** | `POST /webhooks/github` (signature-verified, unauthenticated) | +| **Health** | `GET /health` | --- ## Troubleshooting -- **CORS / cookies** — Backend `CORS_ORIGIN` must list your exact browser origin; use `credentials: true` compatible origins (no `*` with cookies). -- **Proxy / wrong API** — Set `VITE_API_ORIGIN` to the host:port where Hono listens (`PORT` in `.env`). -- **Gemini 429 / quota** — Free tier is per model and per minute/day; the chat route can fall back to other models via `GEMINI_CHAT_MODEL_FALLBACKS`. Reduce ingest frequency or enable billing if you need higher limits. -- **API key invalid / expired** — Regenerate in [Google AI Studio](https://aistudio.google.com/apikey) and update `GEMINI_API_KEY`. -- **Chat returns no matches** — Run ingest for the repo; try different wording; check that `knowledge_nodes` has documents for that `repo` key. -- **Text search oddities** — After changing text index fields, drop `knowledge_text_search` and restart the server. - ---- - -## Scripts reference - -**Backend:** `npm run dev` · `npm run build` · `npm start` · `npm run type-check` - -**Frontend:** `npm run dev` · `npm run build` · `npm run preview` · `npm run lint` · `npm test` +| Issue | Fix | +|-------|-----| +| CORS errors | Add your frontend URL to `CORS_ORIGIN` in backend `.env` | +| Chat returns "no knowledge graph" | Run ingest first from Overview page | +| Gemini 429 / quota | Free tier limits — reduce ingest frequency or use `GEMINI_CHAT_MODEL_FALLBACKS` | +| Webhook not firing | Check `BACKEND_PUBLIC_URL` is reachable, verify webhook in repo settings | +| Chrome extension no button | Verify extension loaded in `chrome://extensions`, must be on a GitHub repo page | --- -## Documentation in-repo +## Team -Implementation notes and follow-ups for the knowledge graph feature live next to this repo in the parent workspace as **`KNOWLEDGE_GRAPH_IMPLEMENTATION.md`** and **`KNOWLEDGE_GRAPH_FOLLOWUP.md`** (paths may vary if you cloned only `GitLore`). +Built by a team of 4 at HackByte 4.0, IIITDM Jabalpur, April 2026. --- ## License -No license file is included in this repository; add one if you intend to open-source or distribute the project. +MIT