diff --git a/CHANGELOG.md b/CHANGELOG.md index c415ac6..30c7b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to [`codegraph-engine`](https://www.npmjs.com/package/codegr ## Unreleased ### Added +- **A proper website around the app.** A new homepage with an animated pipeline (get the code, parse it, link it, label every edge, show the blast radius), a product preview, features, the accuracy results with their caveats, and the CLI commands. Analyzing now shows the same pipeline live, stage by stage, with real progress. Fonts are bundled (no third-party requests), the page works on phones, respects reduced-motion settings and passes an automated accessibility check. - **The web app analyzes any repository, in the browser.** Paste a public GitHub repository (or `owner/repo@branch`, or a `github.com` link) or drop a project folder; the parser (WebAssembly) runs in a Web Worker and nothing is uploaded. Shareable `?repo=owner/repo` links, a "where to start" overview, search (`/`), a file tree, an inspector with the diff --git a/README.md b/README.md index 104daaf..db6ee15 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Impact 3 symbols in 6 files (3 resolved · 0 probable · 0 ambiguous) d3 resolved src/flask/app.py#Flask.__call__ ``` -Every edge is labelled **resolved / probable / ambiguous**, so you can see *how much* of a blast radius is proven vs. guessed, and filter accordingly. Checked against independent oracles on every call site of six repos CodeGraph was never tuned on, not a sample: +Every edge is labelled **resolved / probable / ambiguous**, so you can see *how much* of a blast radius is proven vs. guessed, and filter accordingly. Checked against independent oracles (the TypeScript compiler, and Jedi for Python) on every call site of six open-source repositories, not a sample. The repositories were chosen before measuring, but fixes the checks prompted are included, so a fresh repository may score a little lower: | | `resolved` | `probable` | `ambiguous` | |---|---|---|---| diff --git a/docs/ORACLE-EVAL.md b/docs/ORACLE-EVAL.md index a3c0cff..edbd198 100644 --- a/docs/ORACLE-EVAL.md +++ b/docs/ORACLE-EVAL.md @@ -3,8 +3,15 @@ The earlier evaluations ([`PRECISION.md`](PRECISION.md), [`EVALUATION.md`](EVALUATION.md)) were labeled by the analyzer author's LLM on a few hundred call sites. They were also partly on repos the resolver was tuned against. That shows CodeGraph working; it is not independent evidence that it is accurate. This evaluation swaps the labeler -for the **TypeScript compiler's own type checker**, scores **every** call site in each repo instead of a sample, and -runs on three repos that were **picked before any result was seen and never used during development**. +for the **TypeScript compiler's own type checker** (and, for Python, Jedi), and scores **every** call site in each +repo instead of a sample. The first measurement (the tables under "Results") was on repos that were **picked before +any result was seen and not used during development**. + +**Read the later numbers with this in mind.** The errors that first measurement exposed on those same repos drove +the fixes described in "Fix 1", "Fix 2" and "Fix 3", and Python's first run drove the return-type change. The +numbers after each fix are therefore not a blind test: they show that a fix removed the errors it targeted without +losing correct edges, and they overstate how a fresh repository would score. Run the scripts on a repo you care about +for a number that isn't tainted by this. What this is not: evidence of adoption. Nobody outside the project has used or reviewed these results, and the human-labeled sample C ([`EVALUATION.md`](EVALUATION.md#human-reviewed-validation-sample-c--pending)) is still diff --git a/docs/screenshots/landing.png b/docs/screenshots/landing.png index ea18cba..79d6a7e 100644 Binary files a/docs/screenshots/landing.png and b/docs/screenshots/landing.png differ diff --git a/package-lock.json b/package-lock.json index 641e53a..ec6fc83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -785,6 +785,24 @@ "node": ">=18" } }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2726,6 +2744,8 @@ "@codegraph/extractors": "*", "@codegraph/parser": "*", "@dagrejs/dagre": "^1.1.4", + "@fontsource-variable/inter": "5.3.0", + "@fontsource-variable/jetbrains-mono": "5.3.0", "@xyflow/react": "^12.3.0", "fflate": "0.8.2", "react": "^18.3.1", diff --git a/packages/web/package.json b/packages/web/package.json index d986624..ec64c73 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -10,6 +10,8 @@ "@codegraph/extractors": "*", "@codegraph/parser": "*", "@dagrejs/dagre": "^1.1.4", + "@fontsource-variable/inter": "5.3.0", + "@fontsource-variable/jetbrains-mono": "5.3.0", "@xyflow/react": "^12.3.0", "fflate": "0.8.2", "react": "^18.3.1", diff --git a/packages/web/public/preview.jpg b/packages/web/public/preview.jpg new file mode 100644 index 0000000..5833648 Binary files /dev/null and b/packages/web/public/preview.jpg differ diff --git a/packages/web/src/hooks.tsx b/packages/web/src/hooks.tsx new file mode 100644 index 0000000..381b28f --- /dev/null +++ b/packages/web/src/hooks.tsx @@ -0,0 +1,36 @@ +import { useEffect, useRef, useState, type CSSProperties, type ReactNode, type RefObject } from 'react'; + +const reducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; + +/** True once the element has scrolled into view (and stays true), for reveal-on-scroll. */ +export function useInView(): [RefObject, boolean] { + const ref = useRef(null); + const [seen, setSeen] = useState(false); + useEffect(() => { + const el = ref.current; + if (!el || seen) return; + if (typeof IntersectionObserver === 'undefined' || reducedMotion()) return setSeen(true); + const io = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setSeen(true); + io.disconnect(); + } + }, + { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }, + ); + io.observe(el); + return () => io.disconnect(); + }, [seen]); + return [ref, seen]; +} + +/** Fades and lifts its children in when scrolled into view. */ +export function Reveal({ children, delay = 0, className = '' }: { children: ReactNode; delay?: number; className?: string }) { + const [ref, seen] = useInView(); + return ( +
+ {children} +
+ ); +} diff --git a/packages/web/src/main.tsx b/packages/web/src/main.tsx index 5d031a5..7580979 100644 --- a/packages/web/src/main.tsx +++ b/packages/web/src/main.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; +import '@fontsource-variable/inter'; +import '@fontsource-variable/jetbrains-mono'; import { App } from './App'; import './styles.css'; diff --git a/packages/web/src/styles.css b/packages/web/src/styles.css index 96b0606..f07e251 100644 --- a/packages/web/src/styles.css +++ b/packages/web/src/styles.css @@ -24,8 +24,8 @@ --c-d1: #ff6b6b; --c-d2: #ff9f5a; --c-d3: #f5c04a; - --sans: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; + --sans: 'Inter Variable', Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --mono: 'JetBrains Mono Variable', ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; --radius: 10px; } @@ -177,36 +177,7 @@ button.link.more { border-radius: 6px; } -/* ------------------------------------------------------------------ landing */ -.landing { - min-height: 100%; - display: flex; - flex-direction: column; - background: - radial-gradient(900px 480px at 50% -120px, rgba(91, 140, 255, 0.2), transparent 70%), - radial-gradient(600px 400px at 85% 10%, rgba(45, 212, 191, 0.08), transparent 70%), - var(--bg); -} -.landing-top { - display: flex; - align-items: center; - justify-content: space-between; - padding: 18px clamp(16px, 4vw, 40px); -} -.landing-top nav { - display: flex; - gap: 22px; - color: var(--dim); - font-size: 13.5px; -} -.landing-top nav a:hover { - color: var(--text); -} -.with-icon { - display: inline-flex; - align-items: center; - gap: 6px; -} +/* ------------------------------------------------------------------ brand (site nav and workspace header) */ .brand { display: inline-flex; align-items: center; @@ -227,307 +198,6 @@ button.link.more { .brand-btn:hover { background: var(--panel-2); } -.landing-main { - width: min(780px, 100% - 32px); - margin: clamp(24px, 7vh, 80px) auto 0; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} -.eyebrow { - font-size: 12.5px; - color: var(--dim); - border: 1px solid var(--line-2); - background: rgba(23, 29, 41, 0.7); - border-radius: 999px; - padding: 4px 12px; -} -.landing h1 { - margin-top: 22px; - font-size: clamp(34px, 6.4vw, 58px); - line-height: 1.06; - letter-spacing: -0.03em; - font-weight: 750; - text-wrap: balance; -} -.landing h1 em { - font-style: normal; - background: linear-gradient(95deg, #7aa2ff, #2dd4bf); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} -.lede { - margin-top: 18px; - max-width: 640px; - font-size: clamp(15px, 2vw, 17.5px); - line-height: 1.6; - color: var(--dim); -} -.repo-form { - margin-top: 30px; - width: 100%; - display: flex; - align-items: center; - gap: 8px; - padding: 7px 7px 7px 16px; - background: var(--panel); - border: 1px solid var(--line-2); - border-radius: 14px; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35); - transition: border-color 0.15s, box-shadow 0.15s; -} -.repo-form:focus-within { - border-color: var(--accent); - box-shadow: 0 0 0 4px var(--accent-soft), 0 10px 40px rgba(0, 0, 0, 0.35); -} -.repo-form-icon { - color: var(--faint); - flex: none; -} -.repo-form input { - flex: 1; - min-width: 0; - background: transparent; - border: 0; - outline: 0; - color: var(--text); - font: 16px var(--sans); - padding: 10px 4px; -} -.repo-form input::placeholder { - color: var(--faint); -} -.callout { - margin-top: 14px; - width: 100%; - text-align: left; - border-radius: 10px; - padding: 11px 14px; - font-size: 13.5px; - line-height: 1.5; -} -.callout.error { - background: rgba(255, 107, 107, 0.09); - border: 1px solid rgba(255, 107, 107, 0.35); - color: #ffc9c9; -} -.callout.error strong { - color: #ffdede; -} -.examples { - margin-top: 18px; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: 8px; -} -.examples-label { - color: var(--faint); - font-size: 13px; - margin-right: 2px; -} -.chip-btn { - display: inline-flex; - align-items: baseline; - gap: 7px; - background: var(--panel); - border: 1px solid var(--line-2); - border-radius: 999px; - padding: 5px 12px; - font-size: 13px; - color: var(--text); - transition: border-color 0.15s, background 0.15s; -} -.chip-btn small { - color: var(--faint); - font-size: 11px; -} -.chip-btn:hover { - border-color: var(--accent); - background: var(--panel-2); -} -.or { - width: 100%; - margin: 26px 0 18px; - display: flex; - align-items: center; - gap: 14px; - color: var(--faint); - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.1em; -} -.or::before, -.or::after { - content: ''; - flex: 1; - border-top: 1px solid var(--line); -} -.drop { - width: 100%; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 14px 16px; - text-align: left; - padding: 20px 22px; - background: rgba(17, 22, 31, 0.6); - border: 1.5px dashed var(--line-2); - border-radius: 14px; - color: var(--dim); - transition: border-color 0.15s, background 0.15s; -} -.drop-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-left: auto; -} -.drop:hover { - border-color: var(--accent); - background: var(--accent-soft); -} -.drop svg { - flex: none; - color: var(--accent-2); -} -.drop-text { - flex: 1 1 280px; - display: flex; - flex-direction: column; - gap: 2px; - font-size: 13.5px; -} -.drop-text strong { - color: var(--text); - font-size: 15px; -} -.landing-foot { - margin: auto auto 0; - padding: 56px 16px 28px; - width: min(700px, 100%); - text-align: center; - display: flex; - flex-direction: column; - gap: 10px; - font-size: 13px; - line-height: 1.6; - color: var(--faint); -} -.t-resolved { - color: var(--good); -} -.t-probable { - color: var(--warn); -} -.t-ambiguous { - color: var(--bad); -} -.drop-overlay { - position: fixed; - inset: 0; - z-index: 50; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 10px; - background: rgba(10, 13, 19, 0.88); - backdrop-filter: blur(6px); - border: 3px dashed var(--accent); - margin: 12px; - border-radius: 20px; - color: var(--accent-2); - pointer-events: none; -} -.drop-overlay strong { - font-size: 22px; - color: var(--text); -} -.drop-overlay span { - color: var(--dim); -} - -/* ------------------------------------------------------------------ loading */ -.loading-main { - margin-top: clamp(40px, 14vh, 140px); - gap: 16px; -} -.loading-main h2 { - font-size: 26px; - letter-spacing: -0.02em; -} -.bar { - width: min(520px, 100%); - height: 8px; - border-radius: 999px; - background: var(--panel-3); - overflow: hidden; -} -.bar > span { - display: block; - height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, #5b8cff, #2dd4bf); - transition: width 0.15s ease-out; -} -.bar.indeterminate > span { - width: 36%; - animation: slide 1.2s ease-in-out infinite; -} -@keyframes slide { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(380%); - } -} -.status { - color: var(--dim); - min-height: 1.5em; -} -.steps { - list-style: none; - margin: 6px 0 10px; - padding: 0; - display: flex; - gap: 22px; - color: var(--faint); - font-size: 13.5px; -} -.steps li { - display: flex; - align-items: center; - gap: 8px; -} -.step-mark { - display: inline-grid; - place-items: center; - width: 20px; - height: 20px; - border-radius: 50%; - border: 1px solid var(--line-2); - font-size: 11px; -} -.steps li.now { - color: var(--text); -} -.steps li.now .step-mark { - border-color: var(--accent); - color: var(--accent-2); -} -.steps li.done { - color: var(--dim); -} -.steps li.done .step-mark { - background: var(--good); - border-color: var(--good); - color: #06241a; -} /* ------------------------------------------------------------------ workspace shell */ .workspace { @@ -1578,25 +1248,6 @@ button.crumb:hover { } } @media (max-width: 560px) { - .landing-top nav a:not(.with-icon) { - display: none; - } - .repo-form { - flex-wrap: wrap; - padding: 10px; - } - .repo-form input { - flex-basis: 70%; - } - .repo-form button { - flex: 1; - justify-content: center; - } - .steps { - flex-direction: column; - gap: 8px; - align-items: flex-start; - } .ws-head button.ghost { display: none; } diff --git a/packages/web/src/ui/Pipeline.tsx b/packages/web/src/ui/Pipeline.tsx new file mode 100644 index 0000000..43807cd --- /dev/null +++ b/packages/web/src/ui/Pipeline.tsx @@ -0,0 +1,108 @@ +import type { CSSProperties, ReactNode, SVGProps } from 'react'; +import { IconCheck } from './icons'; + +type P = SVGProps; +const g = (p: P): P => ({ width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, ...p }); + +const IconSource = (p: P) => ( + + + + +); +const IconParse = (p: P) => ( + + + +); +const IconLink = (p: P) => ( + + + + + + +); +const IconLabel = (p: P) => ( + + + + +); +const IconImpact = (p: P) => ( + + + + + + + +); + +export interface Stage { + id: string; + title: string; + text: string; + /** A concrete example of what this stage produces. */ + sample: string; + icon: ReactNode; +} + +/** What happens between "here is a repository" and "here is what a change would break". */ +export const STAGES: Stage[] = [ + { id: 'get', title: 'Get the code', text: 'A GitHub repository, a folder, loose files or a .zip. Read straight into your tab.', sample: 'pallets/flask · 83 files', icon: }, + { id: 'parse', title: 'Parse it', text: 'Tree-sitter, compiled to WebAssembly, turns every file into a syntax tree in a background thread.', sample: '1,580 symbols found', icon: }, + { id: 'link', title: 'Link it', text: 'Imports, calls, methods and type hints are followed across files to connect the pieces.', sample: '1,555 calls linked', icon: }, + { id: 'label', title: 'Label every edge', text: 'Each link is marked resolved, probable or ambiguous, so a guess never looks like a fact.', sample: '52% proven · 48% flagged', icon: }, + { id: 'impact', title: 'Show the blast radius', text: 'Pick any function: see what could break, how sure we are, and which tests to run.', sample: '40 symbols · 8 tests', icon: }, +]; + +/** Stages of a live analysis, shown while it runs. */ +export const LIVE_STAGES: Pick[] = [ + { id: 'get', title: 'Get the files', icon: }, + { id: 'parse', title: 'Parse the code', icon: }, + { id: 'link', title: 'Link the calls', icon: }, + { id: 'ready', title: 'Ready', icon: }, +]; + +/** + * A row of stages joined by a track. `ambient` is the homepage explainer: a packet travels the track and each stage + * lights up as it passes. `live` shows real progress: finished stages are ticked, the current one pulses and its + * outgoing track fills with `progress` (0 to 1). + */ +export function Pipeline({ + stages, + mode, + current = 0, + progress = 0, + detail = true, +}: { + stages: (Pick & Partial)[]; + mode: 'ambient' | 'live'; + current?: number; + progress?: number; + detail?: boolean; +}) { + const n = stages.length; + return ( +
    + {mode === 'ambient' && } + {stages.map((s, i) => { + const state = mode === 'live' ? (i < current ? 'done' : i === current ? 'now' : 'todo') : ''; + return ( +
  1. + {state === 'done' ? : s.icon} + {i < n - 1 && ( + + + + )} +

    {s.title}

    + {detail && s.text &&

    {s.text}

    } + {detail && s.sample && {s.sample}} +
  2. + ); + })} +
+ ); +} diff --git a/packages/web/src/views/Landing.tsx b/packages/web/src/views/Landing.tsx index a81fcbd..d007af4 100644 --- a/packages/web/src/views/Landing.tsx +++ b/packages/web/src/views/Landing.tsx @@ -1,6 +1,8 @@ import { useEffect, useRef, useState, type FormEvent } from 'react'; -import { IconArrowRight, IconGithub, IconLogo, IconUpload } from '../ui/icons'; +import { IconArrowRight, IconGithub, IconUpload } from '../ui/icons'; import { entriesFromDrop, entriesFromFileList, type Picked } from '../sources/local'; +import { Accuracy, Cta, Features, Footer, HowItWorks, Nav, Preview, Terminal } from './landing/sections'; +import './landing/landing.css'; export interface LandingError { message: string; @@ -16,8 +18,6 @@ const EXAMPLES: { repo: string; lang: string; note?: string }[] = [ { repo: 'colinhacks/zod', lang: 'TypeScript', note: 'large' }, ]; -const REPO_URL = 'https://github.com/harsharajkumar-273/CodeGraph'; - export function Landing({ error, initialValue = '', @@ -34,12 +34,13 @@ export function Landing({ }) { const [value, setValue] = useState(initialValue); const [dragging, setDragging] = useState(false); + const inputRef = useRef(null); const folderInput = useRef(null); const filesInput = useRef(null); const graphInput = useRef(null); const depth = useRef(0); - // A dropped folder anywhere on the page is accepted, and the browser must not navigate to it. + // A dropped folder, files or zip anywhere on the page is accepted, and the browser must not navigate to them. useEffect(() => { const hasFiles = (e: DragEvent) => Array.from(e.dataTransfer?.types ?? []).includes('Files'); const enter = (e: DragEvent) => { @@ -82,131 +83,123 @@ export function Landing({ e.preventDefault(); onRepo(value); }; - - const shown = error ?? null; + const focusInput = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + setTimeout(() => inputRef.current?.focus({ preventScroll: true }), 350); + }; return ( -
-
-
- - CodeGraph -
- -
- -
- Runs in your browser · nothing is uploaded -

- See what breaks before you change it -

-

- Paste a GitHub repository, or drop a folder, some files or a .zip from your computer. CodeGraph maps the imports and calls across TypeScript, JavaScript and Python, then shows exactly which functions, files and tests a change could reach. -

- -
- - setValue(e.target.value)} - autoFocus - spellCheck={false} - autoCapitalize="off" - autoCorrect="off" - /> - - - - {shown && ( -
- {shown.message} - {shown.hint && {shown.hint}} -
- )} - -
- Try - {EXAMPLES.map((x) => ( - - ))} -
- -
- or -
- -
- - - Drop a folder, files or a .zip here - Read inside this tab and never uploaded. Dragging a folder in is faster than choosing it: node_modules and .git are skipped before anything is read. +
+
- )} - onChange={(e) => { - const files = Array.from(e.target.files ?? []); // copy: clearing the input below empties the live list - e.target.value = ''; - if (files.length) onPick(() => entriesFromFileList(files)); - }} - /> - { - const files = Array.from(e.target.files ?? []); - e.target.value = ''; - if (files.length) onPick(() => entriesFromFileList(files)); - }} - /> - { - const f = e.target.files?.[0]; - if (f) onGraphFile(f); - e.target.value = ''; - }} - /> -
+ + + {error && ( +
+ {error.message} + {error.hint && {error.hint}} +
+ )} + +
+ Try + {EXAMPLES.map((x) => ( + + ))} +
-
-

- Every call edge is labelled resolved, probable or ambiguous, so you can see how much of a blast radius is proven and how much is a guess. Checked against the TypeScript compiler and Jedi on six repositories, 98–99% of resolved edges were correct. -

-

- Already have a graph? from codegraph index. -

-
+
+ + + Or drop a folder, files or a .zip here + Read inside this tab, never uploaded. Dragging a folder in is faster than choosing it: node_modules and .git are skipped before anything is read. + + + + + +
+

+ Already have a graph? from codegraph index. +

+ + + + + + + + folderInput.current?.click()} /> + +