refactor: cleanup design drafts, rename Chinese assets, UI improvements (#239)
* feat(pages): redesign landing page UI with i18n support
- Redesign all sections based on design file: Navbar, Hero, Highlights, UseCases, Features, Benchmark, QuickStart, Footer
- Add scroll fade-in animation with IntersectionObserver
- Implement full i18n (English/Chinese) with language toggle button
- Add SVG icons and image assets
- Style refinements: table borders, step containers, button styles, spacing
* feat(pages): redesign Docs page with i18n, responsive sidebar, and aligned copy interaction
- Redesign DocsPage UI based on reference HTML design
- Add i18n support for all Docs content (en/zh/ja)
- Fix right sidebar CONTENTS nav with fixed positioning
- Add container backgrounds (4% opacity, 16% border)
- Hide CONTENTS sidebar on mobile instead of alternative UI
- Align copy icon and toast interaction with QuickStart section
- Add new SVG icon assets for docs page
- Add Japanese language support
- Add ColorBends animated background component
- Add responsive hooks and FadeInSection animation
- Add Benchmark, Features, QuickStart standalone pages
- Update Navbar, Footer, and LandingPage components
* refactor: cleanup design drafts, rename Chinese assets, UI improvements
- Remove unused design draft folders (html-files, html-files (1), inpt, doc)
- Rename Chinese-named image files (容器-4_*.svg → provider-*.svg)
- Add language switcher icon to Navbar with dynamic character display
- Align Footer padding with Navbar (32px desktop, 16px mobile)
- Fix CONTENTS sidebar position on wide screens
- Add i18n support for Docs CONTENTS title
- Fix clipboard copy fallback for HTTP/LAN environments
- Adjust Toast padding, benchmark medal sizes
- Swap highlight stats positions (Adoption Rate ↔ Active Users)
- Update stat4 value to 25.10% and stat5 caption to Battle-tested
- Unify Hero buttons size with Navbar Get Started button
* refactor: rename icon assets with semantic English names and cleanup unused files
- Rename all svg_*.svg icons to semantic names based on usage context:
- icon-feature-*.svg for feature section icons
- icon-usecase-*.svg for use case section icons
- icon-chevron-*.svg, icon-copy.svg, icon-play.svg for UI controls
- icon-github.svg, icon-language.svg, icon-sort.svg, icon-terminal-prompt.svg
- Remove unused assets: 5 unreferenced svg icons, image_9e7821.png, provider-8.svg
- Remove debug screenshots: debug-screenshot.png, debug-wide.png
- Remove .vsix build artifact and add *.vsix to .gitignore
- Update all import paths in components accordingly
* fix: address PR review feedback - bugs and improvements
- Fix copy-paste bug: step3Label1 had wrong text in all 3 languages
- en: 'Interactive Setup (Recommended)' → 'Review Commands'
- zh: '交互式设置(推荐)' → '运行审查命令'
- ja: '対話式セットアップ(推奨)' → 'レビュー実行'
- Fix inconsistent indentation (4 spaces → 2 spaces) in i18n files
- Add .catch() handler for navigator.clipboard.writeText() in QuickStartSection
- Check document.execCommand('copy') return value before showing toast
- Extract fallbackCopy helper to deduplicate clipboard logic
- Remove unused useCallback import from HighlightsSection
- Move @types/three from dependencies to devDependencies
- Add i18n key 'hero.terminal' for hardcoded Terminal label
* fix: revert vscode .gitignore change and remove duplicate root SVGs
- Revert extensions/vscode/.gitignore to upstream state (remove *.vsix rule)
- Remove root-level brandicon.svg, claude code icon.svg, codex icon.svg
(identical copies already exist in pages/src/assets/images/ with proper names)
* fix: reorder highlights stats to match design reference
- Reorder stats: 20K+ Active Users → > 30% Adoption Rate → 1M+ Tasks → 1/9 Token Cost → 25.10% AACR-BENCH
- Update stat1 value from 30K+ to 20K+
- Sync all three languages (en/zh/ja)
* chore: remove unused debug screenshots
* chore: remove unused image_9e7821.png
|
|
@ -10,7 +10,8 @@
|
|||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.0"
|
||||
"react-router-dom": "^6.8.0",
|
||||
"three": "^0.185.0"
|
||||
},
|
||||
"overrides": {
|
||||
"fast-uri": "^3.1.2"
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/three": "^0.185.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^6.8.1",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import LandingPage from './components/LandingPage';
|
||||
import FeaturesPage from './pages/FeaturesPage';
|
||||
import BenchmarkPage from './pages/BenchmarkPage';
|
||||
import QuickStartPage from './pages/QuickStartPage';
|
||||
import DocsPage from './pages/DocsPage';
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/docs" element={<DocsPage />} />
|
||||
<Route path="/" element={<LandingPage><FeaturesPage /></LandingPage>} />
|
||||
<Route path="/benchmark" element={<LandingPage><BenchmarkPage /></LandingPage>} />
|
||||
<Route path="/quickstart" element={<LandingPage><QuickStartPage /></LandingPage>} />
|
||||
<Route path="/docs" element={<LandingPage><DocsPage /></LandingPage>} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
10
pages/src/assets.d.ts
vendored
|
|
@ -2,3 +2,13 @@ declare module '*.svg' {
|
|||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
|
|
|||
1
pages/src/assets/icons/doc-check-circle.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><clipPath id="master_svg0_6_22560"><rect x="0" y="0" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_6_22560)"><path d="M7.999902248382568,14.000006198883057C11.31360194838257,14.000006198883057,13.999902248382568,11.313705898883057,13.999902248382568,8.000006198883057C13.999902248382568,4.686306498883057,11.31360194838257,2.0000061988830566,7.999902248382568,2.0000061988830566C4.686202048382569,2.0000061988830566,1.9999022483825684,4.686306498883057,1.9999022483825684,8.000006198883057C1.9999022483825684,11.313705898883057,4.6862025483825684,14.000006198883057,7.999902248382568,14.000006198883057ZM12.648852248382568,6.1595062988830565C12.882902248382567,6.750706198883057,12.999902248382568,7.364206298883056,12.999902248382568,8.000006198883057C12.999902248382568,8.635806598883057,12.882902248382567,9.249306198883056,12.648853248382569,9.840455498883056C12.394702248382568,10.482355598883057,12.023602248382568,11.047405698883056,11.535452348382568,11.535555398883057C11.047302748382569,12.023706198883056,10.482252548382569,12.394805198883057,9.840352548382569,12.648956198883056C9.249202248382568,12.883006198883056,8.63570264838257,13.000006198883057,7.999902248382568,13.000006198883057C7.364152448382568,13.000006198883057,6.7506022483825685,12.883006198883056,6.159402348382568,12.648957198883057C5.517551948382568,12.394806198883057,4.952502248382569,12.023706198883056,4.464352148382568,11.535556298883057C3.9762020483825684,11.047406698883057,3.6050522483825684,10.482356498883057,3.3509521483825684,9.840456498883057C3.1169023483825686,9.249306198883056,2.9999022483825684,8.635806598883057,2.9999022483825684,8.000006198883057C2.9999022483825684,7.3642563988830565,3.1169521483825684,6.750706198883057,3.3509521483825684,6.1595062988830565C3.6050522483825684,5.517655898883056,3.9762022483825685,4.952606198883057,4.4644022483825685,4.464456098883057C4.952502248382569,3.9763059988830567,5.517552348382568,3.6051561988830567,6.159402348382568,3.3510560988830567C6.7506022483825685,3.117006298883057,7.364102348382568,3.0000061988830566,7.999902248382568,3.0000061988830566C8.63570264838257,3.0000061988830566,9.249202248382568,3.1170560988830567,9.840351548382568,3.3510560988830567C10.482251648382569,3.6051561988830567,11.047301748382568,3.976306198883057,11.535451448382569,4.464506198883057C12.023602248382568,4.952606198883057,12.39470124838257,5.517656298883057,12.648852248382568,6.1595062988830565ZM10.753549148382568,6.753550098883057C10.847299148382568,6.659800098883057,10.899999148382568,6.532599898883056,10.899999148382568,6.400000098883057C10.899999148382568,6.123849898883057,10.676148848382569,5.900000098883057,10.399999148382568,5.900000098883057C10.267371648382568,5.900000098883057,10.140197248382568,5.952661498883057,10.046449148382568,6.046400498883057L7.199999848382569,8.892900498883057L5.953549848382568,7.646449998883057C5.859783148382569,7.552681398883057,5.732607348382569,7.500001898883057,5.599999948382568,7.499999998883057C5.323849648382568,7.499999998883057,5.099999948382568,7.723850298883057,5.099999948382568,7.999999998883057C5.099999948382568,8.132603598883057,5.152679448382568,8.259778998883057,5.246449948382568,8.353549998883057L6.846449848382568,9.953550298883057C7.041699848382568,10.148800398883056,7.358299748382568,10.148800398883056,7.553549748382569,9.953550298883057L10.753549148382568,6.753550098883057Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
1
pages/src/assets/icons/doc-contents.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><defs><clipPath id="master_svg0_6_07228"><rect x="0" y="0" width="20" height="20" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_6_07228)"><path d="M16.124999000000003,5.125C16.124999000000003,5.47018778,15.845186,5.75,15.499999,5.75L4.5,5.75C4.15481251,5.75,3.875,5.47018778,3.875,5.125C3.875,4.77981222,4.15481251,4.5,4.5,4.5L15.499999,4.5C15.845186,4.5,16.124999000000003,4.77981251,16.124999000000003,5.125ZM15.499999,10.625L9.4375,10.625C9.0923119,10.625,8.8125,10.3451872,8.8125,10C8.8125,9.6548128,9.0923119,9.375,9.4375,9.375L15.499999,9.375C15.845186,9.375,16.124999000000003,9.6548123,16.124999000000003,10C16.124999000000003,10.3451872,15.845186,10.625,15.499999,10.625ZM16.124999000000003,14.874999C16.124999000000003,15.220186,15.845186,15.499999,15.499999,15.499999L4.5,15.499999C4.15481251,15.499999,3.875,15.220186,3.875,14.874999C3.875,14.529812,4.15481251,14.25,4.5,14.25L15.499999,14.25C15.845186,14.25,16.124999000000003,14.529812,16.124999000000003,14.874999ZM7.8091254,9.3173122C8.3136253,9.605124499999999,8.3136253,10.332438,7.8091254,10.620249699999999L6.5135002,11.359436500000001L5.1976879,12.1100616C4.69768775,12.3953738,4.07600027,12.034311299999999,4.07600027,11.458686799999999L4.07600027,8.4788742C4.07600027,7.903249300000001,4.69768775,7.5421865,5.1976879,7.8274364L6.5135002,8.578185999999999L7.8091254,9.3173122Z" fill="#FFFFFF" fill-opacity="0.8999999761581421"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
pages/src/assets/icons/doc-copy.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><clipPath id="master_svg0_6_29185"><rect x="0" y="0" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_6_29185)"><path d="M13.160650171875,2.8393499853674316C13.024167171875,2.7022912453674315,12.862261171875,2.593170405367432,12.684000171875,2.5181000253674317C12.497900171875,2.4393501283674315,12.303250271875,2.4000000953674316,12.100000371875,2.4000000953674316L5.999999971875,2.4000000953674316C5.723849771875,2.4000000953674316,5.499999971875,2.6238501053674317,5.499999971875,2.9000000953674316C5.499999971875,3.1761500853674316,5.7238502718749995,3.4000000953674316,5.999999971875,3.4000000953674316L12.100000371875,3.4000000953674316C12.232676471875,3.399658675367432,12.359977771875,3.4523908953674316,12.453550171875,3.5464500953674314C12.547671171875,3.6399812953674315,12.600414171875,3.7673103953674314,12.600000171875,3.9000000953674316L12.600000171875,9.999999995367432C12.600000171875,10.27614979536743,12.823850171875,10.500000495367432,13.100000171875,10.500000495367432C13.376150171875,10.500000495367432,13.600000171875,10.27614979536743,13.600000171875,9.999999995367432L13.600000171875,3.9000000953674316C13.600527171875,3.6993503953674316,13.560349171875,3.5006787953674317,13.481900171875,3.315999985367432C13.406821171875,3.1377434753674316,13.297701171875,2.975838665367432,13.160650171875,2.8393499853674316ZM2.772152901875,4.672153495367432C2.665636061875,4.778211095367432,2.582235101875,4.905176595367432,2.527202844875,5.045053495367432C2.475702762875,5.175103195367432,2.4499528408051,5.3100528953674315,2.4499528408051,5.449953095367432L2.4499528408051,12.449953095367432C2.449709892275,12.588601095367432,2.475911140875,12.726023095367431,2.527152537875,12.854853095367432C2.582178111875,12.994746095367432,2.665599111875,13.121714095367432,2.772152661875,13.227753095367431C2.878206731875,13.334275095367431,3.005173201875,13.417677095367432,3.145052671875,13.472704095367432C3.275102611875,13.524203095367431,3.4100527718749998,13.549954095367431,3.5499527718749997,13.549954095367431L10.549953471875,13.549954095367431C10.688604371875,13.550185095367432,10.826026871875,13.523966095367431,10.954853071875,13.472702095367431C11.094730371875,13.417673095367432,11.221696871875,13.334271095367432,11.327753071875,13.227753095367431C11.434291871875,13.121714095367432,11.517696371875,12.994744095367432,11.572702371875,12.854853095367432C11.624202771875,12.724802095367432,11.649951971875,12.589802095367432,11.649951971875,12.449953095367432L11.649951971875,5.449953095367432C11.650188471875,5.311302195367432,11.623970071875,5.173878695367431,11.572702371875,5.045052995367431C11.517675371875,4.905173295367431,11.434274671875,4.778206395367432,11.327753071875,4.672152995367432C11.221710171875,4.565617995367432,11.094739871875,4.482214495367431,10.954853071875,4.427203195367431C10.824802371875,4.3757032953674315,10.689802171875,4.349953195367432,10.549953471875,4.349953195367432L3.5499527718749997,4.349953195367432C3.4113054318750002,4.349709495367431,3.273883821875,4.375910795367432,3.145052911875,4.4271535953674315C3.005171541875,4.482200195367431,2.878205541875,4.565617995367432,2.772152901875,4.672153495367432ZM10.549953471875,12.549954095367431L3.5499527718749997,12.549954095367431C3.522352671875,12.549954095367431,3.4987528718750003,12.540203095367431,3.479252771875,12.520653095367432C3.459702771875,12.501105095367432,3.4499528718750003,12.477555095367432,3.4499528718750003,12.449953095367432L3.4499528718750003,5.449953595367432C3.4499528718750003,5.422353295367431,3.4597527718750003,5.398753195367432,3.479252771875,5.3792533953674315C3.4987528718750003,5.359703095367432,3.522352671875,5.349953195367432,3.5499527718749997,5.349953195367432L10.549953471875,5.349953195367432C10.576553371875,5.349545995367432,10.602138471875,5.360149395367431,10.620653171875,5.379252895367431C10.640201571875,5.398753195367432,10.649951971875,5.422353295367431,10.649951971875,5.449953095367432L10.649951971875,12.449953095367432C10.649951971875,12.477553095367432,10.640203471875,12.501103095367432,10.620653171875,12.520653095367432C10.601102871875,12.540203095367431,10.577552771875,12.549954095367431,10.549953471875,12.549954095367431Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.4000000059604645"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
1
pages/src/assets/icons/doc-download.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><clipPath id="master_svg0_6_23920"><rect x="0" y="0" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_6_23920)"><path d="M7.997250093896485,2.4749999046325684C8.273400293896485,2.4749999046325684,8.497249593896484,2.6988499146325684,8.497249593896484,2.9749999046325684L8.497249593896484,8.795542704632568L9.346340193896484,7.946420704632568C9.541659393896484,7.7514181046325685,9.858038893896484,7.751551604632568,10.053192093896484,7.946718704632568C10.248346293896486,8.141886204632568,10.248456993896484,8.458265804632568,10.053440093896484,8.653570604632568L8.353440293896483,10.353570504632568C8.158190693896485,10.548820004632569,7.841590393896484,10.548820004632569,7.646340393896485,10.353570504632568L5.946340593896484,8.653570604632568C5.752546293896485,8.458060704632569,5.753241493896484,8.142679704632569,5.9478954938964845,7.948025704632569C6.142549493896484,7.753371704632569,6.457930593896484,7.752676504632569,6.653440493896484,7.946470704632568L7.497249593896484,8.790279904632568L7.497249593896484,2.9749999046325684C7.497249593896484,2.6988499146325684,7.721099393896484,2.4749999046325684,7.997250093896485,2.4749999046325684ZM13.599843493896485,12.100024704632569L13.599843493896485,8.000024304632568C13.599843493896485,7.723874104632569,13.375992493896485,7.500024304632569,13.099843493896485,7.500024304632569C12.823693493896485,7.500024304632569,12.599843493896485,7.723874604632568,12.599843493896485,8.000024304632568L12.599843493896485,12.100024704632569C12.600081493896484,12.166818104632569,12.586780493896484,12.232965904632568,12.560743493896485,12.294475104632568C12.535655493896485,12.353990104632569,12.499190493896485,12.408034804632567,12.453392493896484,12.453574704632569C12.359847993896484,12.547675904632568,12.232527693896484,12.600415904632568,12.099842993896484,12.60002490463257L3.899842493896484,12.60002490463257C3.833049993896484,12.600262904632569,3.766901993896484,12.586961904632568,3.7053925938964847,12.560924904632568C3.6458782938964847,12.535836904632568,3.5918342938964845,12.499370904632569,3.5462924938964844,12.453574704632569C3.4522094938964845,12.360018204632569,3.399473193896484,12.232705604632569,3.3998422938964845,12.100024704632569L3.3998422938964845,8.002374204632568C3.3998422938964845,7.726223904632568,3.1759924838964846,7.502374204632568,2.8998422638964843,7.502374204632568C2.623692513896484,7.502374204632568,2.399842262268084,7.7262244046325685,2.399842262268084,8.002374204632568L2.399842262268084,12.100024704632569C2.399326324466484,12.300674004632569,2.4395027158964844,12.499342904632568,2.5179424238964843,12.684023904632568C2.5930128138964843,12.862284904632569,2.7021336538964844,13.024190904632569,2.8391923938964845,13.160674904632568C2.9756808238964845,13.297724904632569,3.137585643896484,13.406845904632569,3.315842623896484,13.481924904632569C3.5019423938964844,13.560674904632569,3.696642393896484,13.60002490463257,3.899842493896484,13.60002490463257L12.099842993896484,13.60002490463257C12.300493193896484,13.600556904632569,12.499166493896483,13.560380904632568,12.683842493896485,13.481924904632569C12.862101493896484,13.406848904632568,13.024006493896485,13.297727904632568,13.160492493896484,13.160674904632568C13.297546493896485,13.024188904632569,13.406666493896484,12.862282904632568,13.481742493896485,12.684023904632568C13.560493493896484,12.497923904632568,13.599843493896485,12.303274604632568,13.599843493896485,12.100024704632569Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
1
pages/src/assets/icons/doc-edit.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><clipPath id="master_svg0_6_23876"><rect x="0" y="0" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_6_23876)"><path d="M2.9000000953674316,12.599999910736084L13.100001095367432,12.599999910736084C13.376151095367431,12.599999910736084,13.600001095367432,12.823849910736085,13.600001095367432,13.099999910736084C13.600001095367432,13.376149910736084,13.376151095367431,13.599999910736084,13.100001095367432,13.599999910736084L2.9000000953674316,13.599999910736084C2.6238501053674317,13.599999910736084,2.4000000953674316,13.376149910736084,2.4000000953674316,13.099999910736084C2.4000000953674316,12.823849910736085,2.6238501053674317,12.599999910736084,2.9000000953674316,12.599999910736084ZM4.398250095367432,8.051750210736085L9.792900095367433,2.657099960736084C10.183400195367431,2.266599893736084,10.816599395367431,2.266599893736084,11.207100395367432,2.657099960736084L13.328499095367432,4.7785001107360845C13.719000095367432,5.169000110736084,13.719000095367432,5.802150210736084,13.328499095367432,6.192699910736084L7.933750195367431,11.587300810736084C7.710872695367431,11.810168710736084,7.395966095367432,11.914914610736083,7.084000095367432,11.869950810736084L5.2299499953674315,11.602651110736083C4.790992695367432,11.539380510736084,4.446145995367432,11.194554810736085,4.382849695367431,10.755601410736084L4.115599595367431,8.901551210736084C4.070618595367431,8.589569110736084,4.175366395367432,8.274640110736083,4.398250095367432,8.051750210736085ZM5.372650195367432,10.612899310736085L7.226650195367432,10.880198910736084L12.621300095367431,5.4854998107360835L10.500000495367432,3.3642001107360837L5.1053499953674315,8.758850110736084L5.372650195367432,10.612899310736085Z" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
pages/src/assets/icons/icon-chevron-down.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><clipPath id="master_svg0_9_10695"><rect x="0" y="0" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_10695)"><path d="M7.997250093896485,2.4749999046325684C8.273400293896485,2.4749999046325684,8.497249593896484,2.6988499146325684,8.497249593896484,2.9749999046325684L8.497249593896484,8.795542704632568L9.346340193896484,7.946420704632568C9.541659393896484,7.7514181046325685,9.858038893896484,7.751551604632568,10.053192093896484,7.946718704632568C10.248346293896486,8.141886204632568,10.248456993896484,8.458265804632568,10.053440093896484,8.653570604632568L8.353440293896483,10.353570504632568C8.158190693896485,10.548820004632569,7.841590393896484,10.548820004632569,7.646340393896485,10.353570504632568L5.946340593896484,8.653570604632568C5.752546293896485,8.458060704632569,5.753241493896484,8.142679704632569,5.9478954938964845,7.948025704632569C6.142549493896484,7.753371704632569,6.457930593896484,7.752676504632569,6.653440493896484,7.946470704632568L7.497249593896484,8.790279904632568L7.497249593896484,2.9749999046325684C7.497249593896484,2.6988499146325684,7.721099393896484,2.4749999046325684,7.997250093896485,2.4749999046325684ZM13.599843493896485,12.100024704632569L13.599843493896485,8.000024304632568C13.599843493896485,7.723874104632569,13.375992493896485,7.500024304632569,13.099843493896485,7.500024304632569C12.823693493896485,7.500024304632569,12.599843493896485,7.723874604632568,12.599843493896485,8.000024304632568L12.599843493896485,12.100024704632569C12.600081493896484,12.166818104632569,12.586780493896484,12.232965904632568,12.560743493896485,12.294475104632568C12.535655493896485,12.353990104632569,12.499190493896485,12.408034804632567,12.453392493896484,12.453574704632569C12.359847993896484,12.547675904632568,12.232527693896484,12.600415904632568,12.099842993896484,12.60002490463257L3.899842493896484,12.60002490463257C3.833049993896484,12.600262904632569,3.766901993896484,12.586961904632568,3.7053925938964847,12.560924904632568C3.6458782938964847,12.535836904632568,3.5918342938964845,12.499370904632569,3.5462924938964844,12.453574704632569C3.4522094938964845,12.360018204632569,3.399473193896484,12.232705604632569,3.3998422938964845,12.100024704632569L3.3998422938964845,8.002374204632568C3.3998422938964845,7.726223904632568,3.1759924838964846,7.502374204632568,2.8998422638964843,7.502374204632568C2.623692513896484,7.502374204632568,2.399842262268084,7.7262244046325685,2.399842262268084,8.002374204632568L2.399842262268084,12.100024704632569C2.399326324466484,12.300674004632569,2.4395027158964844,12.499342904632568,2.5179424238964843,12.684023904632568C2.5930128138964843,12.862284904632569,2.7021336538964844,13.024190904632569,2.8391923938964845,13.160674904632568C2.9756808238964845,13.297724904632569,3.137585643896484,13.406845904632569,3.315842623896484,13.481924904632569C3.5019423938964844,13.560674904632569,3.696642393896484,13.60002490463257,3.899842493896484,13.60002490463257L12.099842993896484,13.60002490463257C12.300493193896484,13.600556904632569,12.499166493896483,13.560380904632568,12.683842493896485,13.481924904632569C12.862101493896484,13.406848904632568,13.024006493896485,13.297727904632568,13.160492493896484,13.160674904632568C13.297546493896485,13.024188904632569,13.406666493896484,12.862282904632568,13.481742493896485,12.684023904632568C13.560493493896484,12.497923904632568,13.599843493896485,12.303274604632568,13.599843493896485,12.100024704632569Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
1
pages/src/assets/icons/icon-chevron-right.svg
Normal file
|
After Width: | Height: | Size: 21 KiB |
1
pages/src/assets/icons/icon-copy.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><clipPath id="master_svg0_9_10710"><rect x="0" y="0" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_10710)"><path d="M13.160650171875,2.8393499853674316C13.024167171875,2.7022912453674315,12.862261171875,2.593170405367432,12.684000171875,2.5181000253674317C12.497900171875,2.4393501283674315,12.303250271875,2.4000000953674316,12.100000371875,2.4000000953674316L5.999999971875,2.4000000953674316C5.723849771875,2.4000000953674316,5.499999971875,2.6238501053674317,5.499999971875,2.9000000953674316C5.499999971875,3.1761500853674316,5.7238502718749995,3.4000000953674316,5.999999971875,3.4000000953674316L12.100000371875,3.4000000953674316C12.232676471875,3.399658675367432,12.359977771875,3.4523908953674316,12.453550171875,3.5464500953674314C12.547671171875,3.6399812953674315,12.600414171875,3.7673103953674314,12.600000171875,3.9000000953674316L12.600000171875,9.999999995367432C12.600000171875,10.27614979536743,12.823850171875,10.500000495367432,13.100000171875,10.500000495367432C13.376150171875,10.500000495367432,13.600000171875,10.27614979536743,13.600000171875,9.999999995367432L13.600000171875,3.9000000953674316C13.600527171875,3.6993503953674316,13.560349171875,3.5006787953674317,13.481900171875,3.315999985367432C13.406821171875,3.1377434753674316,13.297701171875,2.975838665367432,13.160650171875,2.8393499853674316ZM2.772152901875,4.672153495367432C2.665636061875,4.778211095367432,2.582235101875,4.905176595367432,2.527202844875,5.045053495367432C2.475702762875,5.175103195367432,2.4499528408051,5.3100528953674315,2.4499528408051,5.449953095367432L2.4499528408051,12.449953095367432C2.449709892275,12.588601095367432,2.475911140875,12.726023095367431,2.527152537875,12.854853095367432C2.582178111875,12.994746095367432,2.665599111875,13.121714095367432,2.772152661875,13.227753095367431C2.878206731875,13.334275095367431,3.005173201875,13.417677095367432,3.145052671875,13.472704095367432C3.275102611875,13.524203095367431,3.4100527718749998,13.549954095367431,3.5499527718749997,13.549954095367431L10.549953471875,13.549954095367431C10.688604371875,13.550185095367432,10.826026871875,13.523966095367431,10.954853071875,13.472702095367431C11.094730371875,13.417673095367432,11.221696871875,13.334271095367432,11.327753071875,13.227753095367431C11.434291871875,13.121714095367432,11.517696371875,12.994744095367432,11.572702371875,12.854853095367432C11.624202771875,12.724802095367432,11.649951971875,12.589802095367432,11.649951971875,12.449953095367432L11.649951971875,5.449953095367432C11.650188471875,5.311302195367432,11.623970071875,5.173878695367431,11.572702371875,5.045052995367431C11.517675371875,4.905173295367431,11.434274671875,4.778206395367432,11.327753071875,4.672152995367432C11.221710171875,4.565617995367432,11.094739871875,4.482214495367431,10.954853071875,4.427203195367431C10.824802371875,4.3757032953674315,10.689802171875,4.349953195367432,10.549953471875,4.349953195367432L3.5499527718749997,4.349953195367432C3.4113054318750002,4.349709495367431,3.273883821875,4.375910795367432,3.145052911875,4.4271535953674315C3.005171541875,4.482200195367431,2.878205541875,4.565617995367432,2.772152901875,4.672153495367432ZM10.549953471875,12.549954095367431L3.5499527718749997,12.549954095367431C3.522352671875,12.549954095367431,3.4987528718750003,12.540203095367431,3.479252771875,12.520653095367432C3.459702771875,12.501105095367432,3.4499528718750003,12.477555095367432,3.4499528718750003,12.449953095367432L3.4499528718750003,5.449953595367432C3.4499528718750003,5.422353295367431,3.4597527718750003,5.398753195367432,3.479252771875,5.3792533953674315C3.4987528718750003,5.359703095367432,3.522352671875,5.349953195367432,3.5499527718749997,5.349953195367432L10.549953471875,5.349953195367432C10.576553371875,5.349545995367432,10.602138471875,5.360149395367431,10.620653171875,5.379252895367431C10.640201571875,5.398753195367432,10.649951971875,5.422353295367431,10.649951971875,5.449953095367432L10.649951971875,12.449953095367432C10.649951971875,12.477553095367432,10.640203471875,12.501103095367432,10.620653171875,12.520653095367432C10.601102871875,12.540203095367431,10.577552771875,12.549954095367431,10.549953471875,12.549954095367431Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.4000000059604645"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
1
pages/src/assets/icons/icon-feature-architecture.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
pages/src/assets/icons/icon-feature-compression.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
pages/src/assets/icons/icon-feature-concurrent.svg
Normal file
|
After Width: | Height: | Size: 5 KiB |
1
pages/src/assets/icons/icon-feature-multi-model.svg
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
1
pages/src/assets/icons/icon-feature-positioning.svg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
1
pages/src/assets/icons/icon-feature-rules.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
1
pages/src/assets/icons/icon-github.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="18" height="18" viewBox="0 0 18 18"><defs><clipPath id="master_svg0_9_10702"><rect x="0" y="0" width="18" height="18" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_10702)"><path d="M9.00000038801117,1.4048436880111694C4.805324888011169,1.4048436880111694,1.4048436880111694,4.805324888011169,1.4048436880111694,9.00000038801117C1.4048436880111694,13.194731688011169,4.805324888011169,16.59521268801117,9.00000038801117,16.59521268801117C13.194731688011169,16.59521268801117,16.59521268801117,13.194731688011169,16.59521268801117,9.00000038801117C16.59521268801117,4.805324888011169,13.194731688011169,1.4048439562320694,9.00000038801117,1.4048436880111694ZM2.6707499880111696,9.00000038801117C2.6707499880111696,5.504399688011169,5.504343388011169,2.670693788011169,9.00000038801117,2.670693788011169C12.49565668801117,2.670693788011169,15.329249688011169,5.504400088011169,15.329249688011169,8.999943088011168C15.329249688011169,11.848444688011169,13.44768768801117,14.25735168801117,10.859625188011169,15.05171368801117C10.930386888011169,13.77241968801117,10.906144488011169,12.98002468801117,10.78695048801117,12.67447468801117C10.57865658801117,12.14066368801117,10.43409378801117,12.12823168801117,10.43409378801117,12.014269688011169C10.43409378801117,11.938330688011169,10.487531088011169,11.88320668801117,10.594518988011169,11.849118688011169C11.52708768801117,11.64301868801117,12.152418688011169,11.069887488011169,12.470567688011169,10.12972488801117C12.91134268801117,8.82720028801117,12.28213168801117,7.880061988011169,12.02759968801117,7.49683128801117C12.00913168801117,7.469234788011169,11.990925688011169,7.44146378801117,11.97298068801117,7.4135249880111695C11.77498168801117,7.100999188011169,12.03440568801117,6.340443988011169,12.344062688011169,5.731818588011169C11.29095018801117,5.52071178801117,10.888762788011169,6.01003108801117,10.658081388011169,6.290605388011169C10.563863188011169,6.405243288011169,10.498276088011169,6.485005688011169,10.42858158801117,6.46807418801117C10.18839398801117,6.409574388011169,9.87727488801117,6.13799938801117,9.00011288801117,6.13799938801117C8.12390598801117,6.13799938801117,7.569112688011169,6.47071878801117,7.5675376880111695,6.46807418801117C7.1438630880111695,5.87761868801117,6.54221288801117,5.63219968801117,5.762531688011169,5.731818588011169C5.977294288011169,6.589911788011169,5.993438088011169,7.150443388011169,5.81096298801117,7.4135249880111695C5.53730668801117,7.808006188011169,5.024250588011169,8.72572508801117,5.422499988011169,10.12972488801117C5.820750088011169,11.53383768801117,7.27070608801117,11.849118688011169,7.54666888801117,11.849118688011169C7.5468929880111695,11.84923068801117,7.547512388011169,11.849343688011169,7.548525188011169,11.84956868801117C7.570462088011169,11.85429368801117,7.773524688011169,11.89811168801117,7.71254958801117,12.069729688011169C7.6621497880111695,12.26182368801117,7.682962788011169,12.746811688011169,7.400250288011169,12.87708668801117C7.11748108801117,13.00736168801117,6.4937814880111695,13.078461688011169,6.232893788011169,12.91589968801117C5.97200618801117,12.75328068801117,5.93167528801117,12.05791768801117,5.104687288011169,11.80254368801117C4.27775608801117,11.54716768801117,4.32956228801117,11.884837688011169,4.5360000880111695,11.967693688011169C4.742381488011169,12.05049368801117,5.2535246880111695,12.109106688011169,5.483812688011169,12.47956868801117C5.71410028801117,12.850143688011169,5.97363798801117,13.375856688011169,6.232893788011169,13.54634968801117C6.40575058801117,13.65997468801117,6.820031488011169,13.70548068801117,7.47568168801117,13.68286968801117C7.447162988011169,14.301842688011169,7.43191948801117,14.78525668801117,7.430006888011169,15.13310568801117C4.693894788011169,14.434762688011169,2.6706941880111694,11.95363168801117,2.6707499880111696,9.00000038801117Z" fill="#FFFFFF" fill-opacity="0.8999999761581421"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1
pages/src/assets/icons/icon-language.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
pages/src/assets/icons/icon-play.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><clipPath id="master_svg0_9_10947"><rect x="0" y="0" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_10947)"><path d="M2.000000476837158,7.999999C2.000000715255738,4.6862907,4.686291676837158,2,7.999999976837159,2C11.313709776837158,2,14.000000476837158,4.6862907,14.000000476837158,7.999999C14.000000476837158,11.3137083,11.313709776837158,14,7.999999976837159,14C4.686291676837158,14,2.000000715255738,11.3137083,2.000000476837158,7.999999ZM3.000000476837158,7.999999C3.000000476837158,10.7614222,5.238576376837158,12.999998,7.999999976837159,12.999998C10.761423576837158,12.999998,12.999998476837158,10.7614231,13.000000476837158,7.999999C12.999046476837158,5.2392492,10.760750276837157,3.0017253999999998,7.999999976837159,3.0017253999999998C5.239250176837158,3.0017256999999997,3.0009536768371583,5.2392492,3.000000476837158,7.999999ZM7.536000276837158,6.1391993L8.767999676837158,6.8495994L9.999199876837158,7.5591993C10.192657976837157,7.6705608,10.311895876837157,7.8767796,10.311895876837157,8.0999994C10.311893976837158,8.3232193,10.192657976837157,8.529437999999999,9.999199876837158,8.6407995L8.767999676837158,9.3495998L7.5359997768371585,10.0607996C7.3428716768371585,10.1723022,7.104918976837158,10.1722641,6.911826576837158,10.0606995C6.7187337768371584,9.9491348,6.599856376837158,9.7430048,6.599999876837158,9.5199995L6.599999876837158,6.6799994C6.599999876837158,6.1999993,7.119999876837158,5.8991995,7.536000276837158,6.1391993Z" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
pages/src/assets/icons/icon-sort.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="14" height="14" viewBox="0 0 14 14"><defs><clipPath id="master_svg0_9_11215/5_07893"><rect x="0" y="0" width="14" height="14" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_11215/5_07893)"><path d="M4.28371077734375,5.9533253C4.18269211034375,5.7783251,4.18269211034375,5.6033249,4.28371077734375,5.4283252L5.39609817734375,3.5016627L6.5084857773437506,1.57499996C6.60950497734375,1.399999917,6.76105477734375,1.3125,6.96313617734375,1.3125C7.16521787734375,1.3125,7.31676747734375,1.400000021,7.4177860773437505,1.57499996L9.64256097734375,5.4283247C9.74357937734375,5.6033244,9.74357937734375,5.7783251,9.64256097734375,5.9533248C9.541498177343751,6.1283245,9.38994837734375,6.2158251,9.18786767734375,6.2158251L4.73840451734375,6.2158251C4.53632336734375,6.2158251,4.38477325734375,6.128325,4.28371077734375,5.9533253ZM6.96317957734375,2.5374999000000003L6.15393587734375,3.9391623L5.34464837734375,5.3408251L8.58171037734375,5.3408251L6.96317957734375,2.5374999000000003ZM9.26604847734375,7.4425316C9.66916127734375,7.4425316,9.92186117734375,7.8779755,9.72187997734375,8.2279749L8.59571127734375,10.198782L7.4765424773437505,12.157294C7.27498677734375,12.510006,6.76643607734375,12.510006,6.56488037734375,12.157294L5.44571157734375,10.198782L4.31958693734375,8.2279749C4.11956191034375,7.8779755,4.37226211734375,7.4425316,4.77537423734375,7.4425316L9.26604847734375,7.4425316Z" fill="#FFFFFF" fill-opacity="0.4000000059604645"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
pages/src/assets/icons/icon-terminal-prompt.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="15" height="15" viewBox="0 0 15 15"><defs><clipPath id="master_svg0_9_11069/3_4924"><rect x="0" y="0" width="15" height="15" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_11069/3_4924)"><path d="M11.69194221496582,12.683058261871338L15.00000051496582,9.374999961871339L11.69194221496582,6.066941741871338L12.57582569496582,5.183058261871338L16.76776791496582,9.374999961871339L12.57582569496582,13.566941761871337L11.69194221496582,12.683058261871338Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.4000000059604645" transform="matrix(0,1,-1,0,16.875000476837158,-6.508883953094482)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 729 B |
1
pages/src/assets/icons/icon-usecase-developer.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
pages/src/assets/icons/icon-usecase-platform-a.svg
Normal file
|
After Width: | Height: | Size: 25 KiB |
1
pages/src/assets/icons/icon-usecase-platform-b.svg
Normal file
|
After Width: | Height: | Size: 17 KiB |
1
pages/src/assets/icons/icon-usecase-platform-c.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
1
pages/src/assets/icons/icon-usecase-researcher.svg
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
pages/src/assets/icons/trophy-bronze.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><g><path d="M12.125,2.5L13.5625,2.5C13.744837,2.5,13.919704,2.57243276,14.048635,2.70136404C14.177567,2.8302953,14.25,3.0051636999999998,14.25,3.1875L14.25,5.5C14.25,6.7765627,13.3125,7.8375006,12.090625,8.03125C11.85,9.8468752,10.390625,11.2781258,8.5625,11.4765625L8.5625,13.120314L11.625,13.120314C11.901563,13.120314,12.125,13.34375,12.125,13.620314L12.125,14.125C12.125,14.19375,12.06875,14.25,12,14.25L4,14.25C3.9312501,14.25,3.875,14.19375,3.875,14.125L3.875,13.620314C3.875,13.34375,4.0984375,13.120314,4.375,13.120314L7.4375,13.120314L7.4375,11.4765625C5.609375,11.2781258,4.1500001,9.8468752,3.909375,8.03125C2.6875,7.8375006,1.75,6.7765627,1.75,5.5L1.75,3.1875C1.75,3.0051636999999998,1.822432786,2.8302953,1.95136404,2.70136404C2.08029529,2.57243276,2.25516367,2.5,2.4375,2.5L3.875,2.5L3.875,1.875C3.875,1.806249999,3.9312501,1.75,4,1.75L12,1.75C12.06875,1.75,12.125,1.806249999,12.125,1.875L12.125,2.5ZM3.875,6.8687506C3.2953126,6.6828127,2.875,6.1390629,2.875,5.5L2.875,3.625L3.875,3.625L3.875,6.8687506ZM13.125,5.5C13.125,6.140625,12.704688,6.6843753,12.125,6.8687506L12.125,3.625L13.125,3.625L13.125,5.5Z" fill-rule="evenodd" fill="#CD7F32" fill-opacity="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
pages/src/assets/icons/trophy-gold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><g><path d="M12.125,2.5L13.5625,2.5C13.744837,2.5,13.919704,2.57243276,14.048635,2.70136404C14.177567,2.8302953,14.25,3.0051636999999998,14.25,3.1875L14.25,5.5C14.25,6.7765627,13.3125,7.8375006,12.090625,8.03125C11.85,9.8468752,10.390625,11.2781258,8.5625,11.4765625L8.5625,13.120314L11.625,13.120314C11.901563,13.120314,12.125,13.34375,12.125,13.620314L12.125,14.125C12.125,14.19375,12.06875,14.25,12,14.25L4,14.25C3.9312501,14.25,3.875,14.19375,3.875,14.125L3.875,13.620314C3.875,13.34375,4.0984375,13.120314,4.375,13.120314L7.4375,13.120314L7.4375,11.4765625C5.609375,11.2781258,4.1500001,9.8468752,3.909375,8.03125C2.6875,7.8375006,1.75,6.7765627,1.75,5.5L1.75,3.1875C1.75,3.0051636999999998,1.822432786,2.8302953,1.95136404,2.70136404C2.08029529,2.57243276,2.25516367,2.5,2.4375,2.5L3.875,2.5L3.875,1.875C3.875,1.806249999,3.9312501,1.75,4,1.75L12,1.75C12.06875,1.75,12.125,1.806249999,12.125,1.875L12.125,2.5ZM3.875,6.8687506C3.2953126,6.6828127,2.875,6.1390629,2.875,5.5L2.875,3.625L3.875,3.625L3.875,6.8687506ZM13.125,5.5C13.125,6.140625,12.704688,6.6843753,12.125,6.8687506L12.125,3.625L13.125,3.625L13.125,5.5Z" fill-rule="evenodd" fill="#F5C518" fill-opacity="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
pages/src/assets/icons/trophy-silver.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><g><path d="M12.125,2.5L13.5625,2.5C13.744837,2.5,13.919704,2.57243276,14.048635,2.70136404C14.177567,2.8302953,14.25,3.0051636999999998,14.25,3.1875L14.25,5.5C14.25,6.7765627,13.3125,7.8375006,12.090625,8.03125C11.85,9.8468752,10.390625,11.2781258,8.5625,11.4765625L8.5625,13.120314L11.625,13.120314C11.901563,13.120314,12.125,13.34375,12.125,13.620314L12.125,14.125C12.125,14.19375,12.06875,14.25,12,14.25L4,14.25C3.9312501,14.25,3.875,14.19375,3.875,14.125L3.875,13.620314C3.875,13.34375,4.0984375,13.120314,4.375,13.120314L7.4375,13.120314L7.4375,11.4765625C5.609375,11.2781258,4.1500001,9.8468752,3.909375,8.03125C2.6875,7.8375006,1.75,6.7765627,1.75,5.5L1.75,3.1875C1.75,3.0051636999999998,1.822432786,2.8302953,1.95136404,2.70136404C2.08029529,2.57243276,2.25516367,2.5,2.4375,2.5L3.875,2.5L3.875,1.875C3.875,1.806249999,3.9312501,1.75,4,1.75L12,1.75C12.06875,1.75,12.125,1.806249999,12.125,1.875L12.125,2.5ZM3.875,6.8687506C3.2953126,6.6828127,2.875,6.1390629,2.875,5.5L2.875,3.625L3.875,3.625L3.875,6.8687506ZM13.125,5.5C13.125,6.140625,12.704688,6.6843753,12.125,6.8687506L12.125,3.625L13.125,3.625L13.125,5.5Z" fill-rule="evenodd" fill="#A8B8C8" fill-opacity="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
pages/src/assets/images/brandicon.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
pages/src/assets/images/icon-claude-code.svg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
1
pages/src/assets/images/icon-codex.svg
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
1
pages/src/assets/images/provider-1.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><defs><clipPath id="master_svg0_9_09843"><rect x="0" y="0" width="20" height="20" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_09843)"><path d="M15.858383,8.3814812L15.858386,4.9377025C15.858386,4.7806743,15.756289,4.6412884000000005,15.604979,4.5917476L11.0277624,3.093095593C10.6486506,2.968968133,10.2389331,2.968968133,9.8598213,3.093095593L5.2826078,4.5917476C5.1312997,4.6412884000000005,5.0292017,4.7806743,5.0292017,4.9377022L5.0292017,7.506969Q3.8717563200000003,8.3464007,3.017931415,9.4810395C2.987498142,9.521481999999999,2.9968221958,9.5783477,3.038174957,9.607934499999999L3.45383641,9.9053264C3.49518919,9.9349132,3.55343646,9.9253039,3.5839054,9.8848886Q4.2162914,9.046042,5.0292017,8.380754L5.0292017,8.381423999999999C5.03159,8.379245300000001,5.0339797,8.3770685,5.0363705,8.374893199999999Q5.7390797,7.8010173,6.5764248,7.3565779Q8.4464984,6.3639953,10.4030418,6.3411968000000005C10.4073219,6.3411469,10.4115305,6.3408155,10.4156494,6.3402205L10.4437613,6.3401744C12.4859657,6.3401744,14.419765,7.0692139,15.858383,8.3814812ZM10.4437981,12.4029608C11.6805811,12.4029608,12.6831913,11.4155884,12.6831913,10.197602700000001C12.6831913,9.8872766,12.6166878,9.580443899999999,12.4880257,9.297162499999999C12.4002562,9.6862941,12.0710697,9.977479500000001,11.6688128,10.0218062C11.6772318,10.079209299999999,11.6815891,10.137903699999999,11.6815891,10.1976023C11.6815891,10.8708282,11.127408,11.4165869,10.4437919,11.4165869C9.760175199999999,11.4165869,9.2059946,10.8708282,9.2059946,10.1976023C9.2059946,9.524375899999999,9.760175199999999,8.978617700000001,10.4437919,8.978617700000001C10.5044131,8.978617700000001,10.564016800000001,8.9829082,10.6223063,8.991200899999999C10.6673203,8.5950594,10.962998899999999,8.2708793,11.3581324,8.184443C11.0704803,8.0577388,10.7589135,7.9922452,10.4437981,7.9922452C9.2070155,7.9922452,8.204404799999999,8.979617600000001,8.204404799999999,10.197602700000001C8.204404799999999,11.4155884,9.2070155,12.4029608,10.4437981,12.4029608ZM11.4818153,10.0241165C11.0232706,9.9850245,10.659654100000001,9.6269312,10.619960800000001,9.1753569C10.5626764,9.1658525,10.5038199,9.1609049,10.4437919,9.1609049C9.8624024,9.1609049,9.391094200000001,9.6250496,9.391094200000001,10.1976023C9.391094200000001,10.7701545,9.8624024,11.2342997,10.4437919,11.2342997C11.0251808,11.2342997,11.4964886,10.7701545,11.4964886,10.1976023C11.4964886,10.138487300000001,11.4914656,10.080528300000001,11.4818153,10.0241165ZM13.294396,15.941701C14.475647,15.198715,15.312001,14.054902,15.66756,12.7545481Q16.800898,11.9509802,17.648513,10.8252654C17.678958,10.7848296,17.670237,10.7271562,17.628882,10.697569399999999L17.213222000000002,10.400178C17.171869,10.370591600000001,17.114452999999997,10.379085100000001,17.083982,10.4194994Q16.529601,11.1547842,15.842928,11.741127L15.842949,11.7408533L15.83811,11.7452383Q15.086056,12.386404,14.175531,12.8491545Q12.4923868,13.704568,10.622490899999999,13.766256C10.5630131,13.767498,10.5034332,13.76812,10.4437613,13.76812C8.4092484,13.76812,6.481997,13.044496,5.044641,11.7409153C5.1742574999999995,13.450135,6.1129189,15.010638,7.5931883,15.941701L10.2445235,17.609344C10.366035,17.685771000000003,10.5215478,17.685771000000003,10.6430597,17.609344L13.294396,15.941701Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
1
pages/src/assets/images/provider-2.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><defs><clipPath id="master_svg0_9_10308"><rect x="0" y="0" width="20" height="20" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_10308)"><path d="M15.858383,8.3814812L15.858386,4.9377025C15.858386,4.7806743,15.756289,4.6412884000000005,15.604979,4.5917476L11.0277624,3.093095593C10.6486506,2.968968133,10.2389331,2.968968133,9.8598213,3.093095593L5.2826078,4.5917476C5.1312997,4.6412884000000005,5.0292017,4.7806743,5.0292017,4.9377022L5.0292017,7.506969Q3.8717563200000003,8.3464007,3.017931415,9.4810395C2.987498142,9.521481999999999,2.9968221958,9.5783477,3.038174957,9.607934499999999L3.45383641,9.9053264C3.49518919,9.9349132,3.55343646,9.9253039,3.5839054,9.8848886Q4.2162914,9.046042,5.0292017,8.380754L5.0292017,8.381423999999999C5.03159,8.379245300000001,5.0339797,8.3770685,5.0363705,8.374893199999999Q5.7390797,7.8010173,6.5764248,7.3565779Q8.4464984,6.3639953,10.4030418,6.3411968000000005C10.4073219,6.3411469,10.4115305,6.3408155,10.4156494,6.3402205L10.4437613,6.3401744C12.4859657,6.3401744,14.419765,7.0692139,15.858383,8.3814812ZM10.4437981,12.4029608C11.6805811,12.4029608,12.6831913,11.4155884,12.6831913,10.197602700000001C12.6831913,9.8872766,12.6166878,9.580443899999999,12.4880257,9.297162499999999C12.4002562,9.6862941,12.0710697,9.977479500000001,11.6688128,10.0218062C11.6772318,10.079209299999999,11.6815891,10.137903699999999,11.6815891,10.1976023C11.6815891,10.8708282,11.127408,11.4165869,10.4437919,11.4165869C9.760175199999999,11.4165869,9.2059946,10.8708282,9.2059946,10.1976023C9.2059946,9.524375899999999,9.760175199999999,8.978617700000001,10.4437919,8.978617700000001C10.5044131,8.978617700000001,10.564016800000001,8.9829082,10.6223063,8.991200899999999C10.6673203,8.5950594,10.962998899999999,8.2708793,11.3581324,8.184443C11.0704803,8.0577388,10.7589135,7.9922452,10.4437981,7.9922452C9.2070155,7.9922452,8.204404799999999,8.979617600000001,8.204404799999999,10.197602700000001C8.204404799999999,11.4155884,9.2070155,12.4029608,10.4437981,12.4029608ZM11.4818153,10.0241165C11.0232706,9.9850245,10.659654100000001,9.6269312,10.619960800000001,9.1753569C10.5626764,9.1658525,10.5038199,9.1609049,10.4437919,9.1609049C9.8624024,9.1609049,9.391094200000001,9.6250496,9.391094200000001,10.1976023C9.391094200000001,10.7701545,9.8624024,11.2342997,10.4437919,11.2342997C11.0251808,11.2342997,11.4964886,10.7701545,11.4964886,10.1976023C11.4964886,10.138487300000001,11.4914656,10.080528300000001,11.4818153,10.0241165ZM13.294396,15.941701C14.475647,15.198715,15.312001,14.054902,15.66756,12.7545481Q16.800898,11.9509802,17.648513,10.8252654C17.678958,10.7848296,17.670237,10.7271562,17.628882,10.697569399999999L17.213222000000002,10.400178C17.171869,10.370591600000001,17.114452999999997,10.379085100000001,17.083982,10.4194994Q16.529601,11.1547842,15.842928,11.741127L15.842949,11.7408533L15.83811,11.7452383Q15.086056,12.386404,14.175531,12.8491545Q12.4923868,13.704568,10.622490899999999,13.766256C10.5630131,13.767498,10.5034332,13.76812,10.4437613,13.76812C8.4092484,13.76812,6.481997,13.044496,5.044641,11.7409153C5.1742574999999995,13.450135,6.1129189,15.010638,7.5931883,15.941701L10.2445235,17.609344C10.366035,17.685771000000003,10.5215478,17.685771000000003,10.6430597,17.609344L13.294396,15.941701Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
1
pages/src/assets/images/provider-3.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><defs><clipPath id="master_svg0_9_09915"><rect x="0" y="0" width="20" height="20" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_09915)"><path d="M15.858383,8.3814812L15.858386,4.9377025C15.858386,4.7806743,15.756289,4.6412884000000005,15.604979,4.5917476L11.0277624,3.093095593C10.6486506,2.968968133,10.2389331,2.968968133,9.8598213,3.093095593L5.2826078,4.5917476C5.1312997,4.6412884000000005,5.0292017,4.7806743,5.0292017,4.9377022L5.0292017,7.506969Q3.8717563200000003,8.3464007,3.017931415,9.4810395C2.987498142,9.521481999999999,2.9968221958,9.5783477,3.038174957,9.607934499999999L3.45383641,9.9053264C3.49518919,9.9349132,3.55343646,9.9253039,3.5839054,9.8848886Q4.2162914,9.046042,5.0292017,8.380754L5.0292017,8.381423999999999C5.03159,8.379245300000001,5.0339797,8.3770685,5.0363705,8.374893199999999Q5.7390797,7.8010173,6.5764248,7.3565779Q8.4464984,6.3639953,10.4030418,6.3411968000000005C10.4073219,6.3411469,10.4115305,6.3408155,10.4156494,6.3402205L10.4437613,6.3401744C12.4859657,6.3401744,14.419765,7.0692139,15.858383,8.3814812ZM10.4437981,12.4029608C11.6805811,12.4029608,12.6831913,11.4155884,12.6831913,10.197602700000001C12.6831913,9.8872766,12.6166878,9.580443899999999,12.4880257,9.297162499999999C12.4002562,9.6862941,12.0710697,9.977479500000001,11.6688128,10.0218062C11.6772318,10.079209299999999,11.6815891,10.137903699999999,11.6815891,10.1976023C11.6815891,10.8708282,11.127408,11.4165869,10.4437919,11.4165869C9.760175199999999,11.4165869,9.2059946,10.8708282,9.2059946,10.1976023C9.2059946,9.524375899999999,9.760175199999999,8.978617700000001,10.4437919,8.978617700000001C10.5044131,8.978617700000001,10.564016800000001,8.9829082,10.6223063,8.991200899999999C10.6673203,8.5950594,10.962998899999999,8.2708793,11.3581324,8.184443C11.0704803,8.0577388,10.7589135,7.9922452,10.4437981,7.9922452C9.2070155,7.9922452,8.204404799999999,8.979617600000001,8.204404799999999,10.197602700000001C8.204404799999999,11.4155884,9.2070155,12.4029608,10.4437981,12.4029608ZM11.4818153,10.0241165C11.0232706,9.9850245,10.659654100000001,9.6269312,10.619960800000001,9.1753569C10.5626764,9.1658525,10.5038199,9.1609049,10.4437919,9.1609049C9.8624024,9.1609049,9.391094200000001,9.6250496,9.391094200000001,10.1976023C9.391094200000001,10.7701545,9.8624024,11.2342997,10.4437919,11.2342997C11.0251808,11.2342997,11.4964886,10.7701545,11.4964886,10.1976023C11.4964886,10.138487300000001,11.4914656,10.080528300000001,11.4818153,10.0241165ZM13.294396,15.941701C14.475647,15.198715,15.312001,14.054902,15.66756,12.7545481Q16.800898,11.9509802,17.648513,10.8252654C17.678958,10.7848296,17.670237,10.7271562,17.628882,10.697569399999999L17.213222000000002,10.400178C17.171869,10.370591600000001,17.114452999999997,10.379085100000001,17.083982,10.4194994Q16.529601,11.1547842,15.842928,11.741127L15.842949,11.7408533L15.83811,11.7452383Q15.086056,12.386404,14.175531,12.8491545Q12.4923868,13.704568,10.622490899999999,13.766256C10.5630131,13.767498,10.5034332,13.76812,10.4437613,13.76812C8.4092484,13.76812,6.481997,13.044496,5.044641,11.7409153C5.1742574999999995,13.450135,6.1129189,15.010638,7.5931883,15.941701L10.2445235,17.609344C10.366035,17.685771000000003,10.5215478,17.685771000000003,10.6430597,17.609344L13.294396,15.941701Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
1
pages/src/assets/images/provider-4.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><defs><clipPath id="master_svg0_9_09771"><rect x="0" y="0" width="20" height="20" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_09771)"><path d="M15.858383,8.3814812L15.858386,4.9377025C15.858386,4.7806743,15.756289,4.6412884000000005,15.604979,4.5917476L11.0277624,3.093095593C10.6486506,2.968968133,10.2389331,2.968968133,9.8598213,3.093095593L5.2826078,4.5917476C5.1312997,4.6412884000000005,5.0292017,4.7806743,5.0292017,4.9377022L5.0292017,7.506969Q3.8717563200000003,8.3464007,3.017931415,9.4810395C2.987498142,9.521481999999999,2.9968221958,9.5783477,3.038174957,9.607934499999999L3.45383641,9.9053264C3.49518919,9.9349132,3.55343646,9.9253039,3.5839054,9.8848886Q4.2162914,9.046042,5.0292017,8.380754L5.0292017,8.381423999999999C5.03159,8.379245300000001,5.0339797,8.3770685,5.0363705,8.374893199999999Q5.7390797,7.8010173,6.5764248,7.3565779Q8.4464984,6.3639953,10.4030418,6.3411968000000005C10.4073219,6.3411469,10.4115305,6.3408155,10.4156494,6.3402205L10.4437613,6.3401744C12.4859657,6.3401744,14.419765,7.0692139,15.858383,8.3814812ZM10.4437981,12.4029608C11.6805811,12.4029608,12.6831913,11.4155884,12.6831913,10.197602700000001C12.6831913,9.8872766,12.6166878,9.580443899999999,12.4880257,9.297162499999999C12.4002562,9.6862941,12.0710697,9.977479500000001,11.6688128,10.0218062C11.6772318,10.079209299999999,11.6815891,10.137903699999999,11.6815891,10.1976023C11.6815891,10.8708282,11.127408,11.4165869,10.4437919,11.4165869C9.760175199999999,11.4165869,9.2059946,10.8708282,9.2059946,10.1976023C9.2059946,9.524375899999999,9.760175199999999,8.978617700000001,10.4437919,8.978617700000001C10.5044131,8.978617700000001,10.564016800000001,8.9829082,10.6223063,8.991200899999999C10.6673203,8.5950594,10.962998899999999,8.2708793,11.3581324,8.184443C11.0704803,8.0577388,10.7589135,7.9922452,10.4437981,7.9922452C9.2070155,7.9922452,8.204404799999999,8.979617600000001,8.204404799999999,10.197602700000001C8.204404799999999,11.4155884,9.2070155,12.4029608,10.4437981,12.4029608ZM11.4818153,10.0241165C11.0232706,9.9850245,10.659654100000001,9.6269312,10.619960800000001,9.1753569C10.5626764,9.1658525,10.5038199,9.1609049,10.4437919,9.1609049C9.8624024,9.1609049,9.391094200000001,9.6250496,9.391094200000001,10.1976023C9.391094200000001,10.7701545,9.8624024,11.2342997,10.4437919,11.2342997C11.0251808,11.2342997,11.4964886,10.7701545,11.4964886,10.1976023C11.4964886,10.138487300000001,11.4914656,10.080528300000001,11.4818153,10.0241165ZM13.294396,15.941701C14.475647,15.198715,15.312001,14.054902,15.66756,12.7545481Q16.800898,11.9509802,17.648513,10.8252654C17.678958,10.7848296,17.670237,10.7271562,17.628882,10.697569399999999L17.213222000000002,10.400178C17.171869,10.370591600000001,17.114452999999997,10.379085100000001,17.083982,10.4194994Q16.529601,11.1547842,15.842928,11.741127L15.842949,11.7408533L15.83811,11.7452383Q15.086056,12.386404,14.175531,12.8491545Q12.4923868,13.704568,10.622490899999999,13.766256C10.5630131,13.767498,10.5034332,13.76812,10.4437613,13.76812C8.4092484,13.76812,6.481997,13.044496,5.044641,11.7409153C5.1742574999999995,13.450135,6.1129189,15.010638,7.5931883,15.941701L10.2445235,17.609344C10.366035,17.685771000000003,10.5215478,17.685771000000003,10.6430597,17.609344L13.294396,15.941701Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
1
pages/src/assets/images/provider-5.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><defs><clipPath id="master_svg0_9_10203"><rect x="0" y="0" width="20" height="20" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_10203)"><path d="M15.858383,8.3814812L15.858386,4.9377025C15.858386,4.7806743,15.756289,4.6412884000000005,15.604979,4.5917476L11.0277624,3.093095593C10.6486506,2.968968133,10.2389331,2.968968133,9.8598213,3.093095593L5.2826078,4.5917476C5.1312997,4.6412884000000005,5.0292017,4.7806743,5.0292017,4.9377022L5.0292017,7.506969Q3.8717563200000003,8.3464007,3.017931415,9.4810395C2.987498142,9.521481999999999,2.9968221958,9.5783477,3.038174957,9.607934499999999L3.45383641,9.9053264C3.49518919,9.9349132,3.55343646,9.9253039,3.5839054,9.8848886Q4.2162914,9.046042,5.0292017,8.380754L5.0292017,8.381423999999999C5.03159,8.379245300000001,5.0339797,8.3770685,5.0363705,8.374893199999999Q5.7390797,7.8010173,6.5764248,7.3565779Q8.4464984,6.3639953,10.4030418,6.3411968000000005C10.4073219,6.3411469,10.4115305,6.3408155,10.4156494,6.3402205L10.4437613,6.3401744C12.4859657,6.3401744,14.419765,7.0692139,15.858383,8.3814812ZM10.4437981,12.4029608C11.6805811,12.4029608,12.6831913,11.4155884,12.6831913,10.197602700000001C12.6831913,9.8872766,12.6166878,9.580443899999999,12.4880257,9.297162499999999C12.4002562,9.6862941,12.0710697,9.977479500000001,11.6688128,10.0218062C11.6772318,10.079209299999999,11.6815891,10.137903699999999,11.6815891,10.1976023C11.6815891,10.8708282,11.127408,11.4165869,10.4437919,11.4165869C9.760175199999999,11.4165869,9.2059946,10.8708282,9.2059946,10.1976023C9.2059946,9.524375899999999,9.760175199999999,8.978617700000001,10.4437919,8.978617700000001C10.5044131,8.978617700000001,10.564016800000001,8.9829082,10.6223063,8.991200899999999C10.6673203,8.5950594,10.962998899999999,8.2708793,11.3581324,8.184443C11.0704803,8.0577388,10.7589135,7.9922452,10.4437981,7.9922452C9.2070155,7.9922452,8.204404799999999,8.979617600000001,8.204404799999999,10.197602700000001C8.204404799999999,11.4155884,9.2070155,12.4029608,10.4437981,12.4029608ZM11.4818153,10.0241165C11.0232706,9.9850245,10.659654100000001,9.6269312,10.619960800000001,9.1753569C10.5626764,9.1658525,10.5038199,9.1609049,10.4437919,9.1609049C9.8624024,9.1609049,9.391094200000001,9.6250496,9.391094200000001,10.1976023C9.391094200000001,10.7701545,9.8624024,11.2342997,10.4437919,11.2342997C11.0251808,11.2342997,11.4964886,10.7701545,11.4964886,10.1976023C11.4964886,10.138487300000001,11.4914656,10.080528300000001,11.4818153,10.0241165ZM13.294396,15.941701C14.475647,15.198715,15.312001,14.054902,15.66756,12.7545481Q16.800898,11.9509802,17.648513,10.8252654C17.678958,10.7848296,17.670237,10.7271562,17.628882,10.697569399999999L17.213222000000002,10.400178C17.171869,10.370591600000001,17.114452999999997,10.379085100000001,17.083982,10.4194994Q16.529601,11.1547842,15.842928,11.741127L15.842949,11.7408533L15.83811,11.7452383Q15.086056,12.386404,14.175531,12.8491545Q12.4923868,13.704568,10.622490899999999,13.766256C10.5630131,13.767498,10.5034332,13.76812,10.4437613,13.76812C8.4092484,13.76812,6.481997,13.044496,5.044641,11.7409153C5.1742574999999995,13.450135,6.1129189,15.010638,7.5931883,15.941701L10.2445235,17.609344C10.366035,17.685771000000003,10.5215478,17.685771000000003,10.6430597,17.609344L13.294396,15.941701Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
1
pages/src/assets/images/provider-6.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><defs><clipPath id="master_svg0_9_10364"><rect x="0" y="0" width="20" height="20" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_10364)"><path d="M15.858383,8.3814812L15.858386,4.9377025C15.858386,4.7806743,15.756289,4.6412884000000005,15.604979,4.5917476L11.0277624,3.093095593C10.6486506,2.968968133,10.2389331,2.968968133,9.8598213,3.093095593L5.2826078,4.5917476C5.1312997,4.6412884000000005,5.0292017,4.7806743,5.0292017,4.9377022L5.0292017,7.506969Q3.8717563200000003,8.3464007,3.017931415,9.4810395C2.987498142,9.521481999999999,2.9968221958,9.5783477,3.038174957,9.607934499999999L3.45383641,9.9053264C3.49518919,9.9349132,3.55343646,9.9253039,3.5839054,9.8848886Q4.2162914,9.046042,5.0292017,8.380754L5.0292017,8.381423999999999C5.03159,8.379245300000001,5.0339797,8.3770685,5.0363705,8.374893199999999Q5.7390797,7.8010173,6.5764248,7.3565779Q8.4464984,6.3639953,10.4030418,6.3411968000000005C10.4073219,6.3411469,10.4115305,6.3408155,10.4156494,6.3402205L10.4437613,6.3401744C12.4859657,6.3401744,14.419765,7.0692139,15.858383,8.3814812ZM10.4437981,12.4029608C11.6805811,12.4029608,12.6831913,11.4155884,12.6831913,10.197602700000001C12.6831913,9.8872766,12.6166878,9.580443899999999,12.4880257,9.297162499999999C12.4002562,9.6862941,12.0710697,9.977479500000001,11.6688128,10.0218062C11.6772318,10.079209299999999,11.6815891,10.137903699999999,11.6815891,10.1976023C11.6815891,10.8708282,11.127408,11.4165869,10.4437919,11.4165869C9.760175199999999,11.4165869,9.2059946,10.8708282,9.2059946,10.1976023C9.2059946,9.524375899999999,9.760175199999999,8.978617700000001,10.4437919,8.978617700000001C10.5044131,8.978617700000001,10.564016800000001,8.9829082,10.6223063,8.991200899999999C10.6673203,8.5950594,10.962998899999999,8.2708793,11.3581324,8.184443C11.0704803,8.0577388,10.7589135,7.9922452,10.4437981,7.9922452C9.2070155,7.9922452,8.204404799999999,8.979617600000001,8.204404799999999,10.197602700000001C8.204404799999999,11.4155884,9.2070155,12.4029608,10.4437981,12.4029608ZM11.4818153,10.0241165C11.0232706,9.9850245,10.659654100000001,9.6269312,10.619960800000001,9.1753569C10.5626764,9.1658525,10.5038199,9.1609049,10.4437919,9.1609049C9.8624024,9.1609049,9.391094200000001,9.6250496,9.391094200000001,10.1976023C9.391094200000001,10.7701545,9.8624024,11.2342997,10.4437919,11.2342997C11.0251808,11.2342997,11.4964886,10.7701545,11.4964886,10.1976023C11.4964886,10.138487300000001,11.4914656,10.080528300000001,11.4818153,10.0241165ZM13.294396,15.941701C14.475647,15.198715,15.312001,14.054902,15.66756,12.7545481Q16.800898,11.9509802,17.648513,10.8252654C17.678958,10.7848296,17.670237,10.7271562,17.628882,10.697569399999999L17.213222000000002,10.400178C17.171869,10.370591600000001,17.114452999999997,10.379085100000001,17.083982,10.4194994Q16.529601,11.1547842,15.842928,11.741127L15.842949,11.7408533L15.83811,11.7452383Q15.086056,12.386404,14.175531,12.8491545Q12.4923868,13.704568,10.622490899999999,13.766256C10.5630131,13.767498,10.5034332,13.76812,10.4437613,13.76812C8.4092484,13.76812,6.481997,13.044496,5.044641,11.7409153C5.1742574999999995,13.450135,6.1129189,15.010638,7.5931883,15.941701L10.2445235,17.609344C10.366035,17.685771000000003,10.5215478,17.685771000000003,10.6430597,17.609344L13.294396,15.941701Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
1
pages/src/assets/images/provider-7.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><defs><clipPath id="master_svg0_9_09828"><rect x="0" y="0" width="20" height="20" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_9_09828)"><path d="M15.858383,8.3814812L15.858386,4.9377025C15.858386,4.7806743,15.756289,4.6412884000000005,15.604979,4.5917476L11.0277624,3.093095593C10.6486506,2.968968133,10.2389331,2.968968133,9.8598213,3.093095593L5.2826078,4.5917476C5.1312997,4.6412884000000005,5.0292017,4.7806743,5.0292017,4.9377022L5.0292017,7.506969Q3.8717563200000003,8.3464007,3.017931415,9.4810395C2.987498142,9.521481999999999,2.9968221958,9.5783477,3.038174957,9.607934499999999L3.45383641,9.9053264C3.49518919,9.9349132,3.55343646,9.9253039,3.5839054,9.8848886Q4.2162914,9.046042,5.0292017,8.380754L5.0292017,8.381423999999999C5.03159,8.379245300000001,5.0339797,8.3770685,5.0363705,8.374893199999999Q5.7390797,7.8010173,6.5764248,7.3565779Q8.4464984,6.3639953,10.4030418,6.3411968000000005C10.4073219,6.3411469,10.4115305,6.3408155,10.4156494,6.3402205L10.4437613,6.3401744C12.4859657,6.3401744,14.419765,7.0692139,15.858383,8.3814812ZM10.4437981,12.4029608C11.6805811,12.4029608,12.6831913,11.4155884,12.6831913,10.197602700000001C12.6831913,9.8872766,12.6166878,9.580443899999999,12.4880257,9.297162499999999C12.4002562,9.6862941,12.0710697,9.977479500000001,11.6688128,10.0218062C11.6772318,10.079209299999999,11.6815891,10.137903699999999,11.6815891,10.1976023C11.6815891,10.8708282,11.127408,11.4165869,10.4437919,11.4165869C9.760175199999999,11.4165869,9.2059946,10.8708282,9.2059946,10.1976023C9.2059946,9.524375899999999,9.760175199999999,8.978617700000001,10.4437919,8.978617700000001C10.5044131,8.978617700000001,10.564016800000001,8.9829082,10.6223063,8.991200899999999C10.6673203,8.5950594,10.962998899999999,8.2708793,11.3581324,8.184443C11.0704803,8.0577388,10.7589135,7.9922452,10.4437981,7.9922452C9.2070155,7.9922452,8.204404799999999,8.979617600000001,8.204404799999999,10.197602700000001C8.204404799999999,11.4155884,9.2070155,12.4029608,10.4437981,12.4029608ZM11.4818153,10.0241165C11.0232706,9.9850245,10.659654100000001,9.6269312,10.619960800000001,9.1753569C10.5626764,9.1658525,10.5038199,9.1609049,10.4437919,9.1609049C9.8624024,9.1609049,9.391094200000001,9.6250496,9.391094200000001,10.1976023C9.391094200000001,10.7701545,9.8624024,11.2342997,10.4437919,11.2342997C11.0251808,11.2342997,11.4964886,10.7701545,11.4964886,10.1976023C11.4964886,10.138487300000001,11.4914656,10.080528300000001,11.4818153,10.0241165ZM13.294396,15.941701C14.475647,15.198715,15.312001,14.054902,15.66756,12.7545481Q16.800898,11.9509802,17.648513,10.8252654C17.678958,10.7848296,17.670237,10.7271562,17.628882,10.697569399999999L17.213222000000002,10.400178C17.171869,10.370591600000001,17.114452999999997,10.379085100000001,17.083982,10.4194994Q16.529601,11.1547842,15.842928,11.741127L15.842949,11.7408533L15.83811,11.7452383Q15.086056,12.386404,14.175531,12.8491545Q12.4923868,13.704568,10.622490899999999,13.766256C10.5630131,13.767498,10.5034332,13.76812,10.4437613,13.76812C8.4092484,13.76812,6.481997,13.044496,5.044641,11.7409153C5.1742574999999995,13.450135,6.1129189,15.010638,7.5931883,15.941701L10.2445235,17.609344C10.366035,17.685771000000003,10.5215478,17.685771000000003,10.6430597,17.609344L13.294396,15.941701Z" fill-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000011920929"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
|
@ -1,497 +1,262 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { OcrIcon, ClaudeIcon, OpenAIIcon } from './icons';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import ocrIcon1 from '../assets/images/provider-1.svg';
|
||||
import ocrIcon2 from '../assets/images/provider-2.svg';
|
||||
import ocrIcon3 from '../assets/images/provider-3.svg';
|
||||
import ocrIcon4 from '../assets/images/provider-4.svg';
|
||||
import ocrIcon5 from '../assets/images/provider-5.svg';
|
||||
import ocrIcon6 from '../assets/images/provider-6.svg';
|
||||
import ocrIcon7 from '../assets/images/provider-7.svg';
|
||||
import claudeCodeIcon from '../assets/images/icon-claude-code.svg';
|
||||
import codexIcon from '../assets/images/icon-codex.svg';
|
||||
import sortIcon from '../assets/icons/icon-sort.svg';
|
||||
import trophyGold from '../assets/icons/trophy-gold.svg';
|
||||
import trophySilver from '../assets/icons/trophy-silver.svg';
|
||||
import trophyBronze from '../assets/icons/trophy-bronze.svg';
|
||||
|
||||
interface BenchmarkEntry {
|
||||
const TROPHY_ICONS = [trophyGold, trophySilver, trophyBronze];
|
||||
|
||||
interface BenchmarkRow {
|
||||
model: string;
|
||||
company: string;
|
||||
sourceType: 'ocr' | 'cc' | 'codex';
|
||||
version: string;
|
||||
precision?: number;
|
||||
precisionDetail?: string;
|
||||
recall?: number;
|
||||
recallDetail?: string;
|
||||
f1?: number;
|
||||
avgTime?: string;
|
||||
avgInputToken?: string;
|
||||
avgOutputToken?: string;
|
||||
avgTotalToken?: string;
|
||||
}
|
||||
|
||||
const OCR_VERSION = 'v1.3.1';
|
||||
const CC_VERSION = 'v2.1.169';
|
||||
const CODEX_VERSION = 'v0.140.0';
|
||||
|
||||
const sourceColorMap: Record<string, string> = {
|
||||
ocr: 'text-brand-400',
|
||||
cc: 'text-[#D97757]',
|
||||
codex: 'text-[#10a37f]',
|
||||
};
|
||||
const sourceNameMap: Record<string, string> = {
|
||||
ocr: 'Open Code Review',
|
||||
cc: 'Claude Code',
|
||||
codex: 'Codex',
|
||||
};
|
||||
|
||||
const benchmarkData: BenchmarkEntry[] = [
|
||||
{
|
||||
model: 'Claude-4.6-Opus',
|
||||
company: 'Anthropic',
|
||||
sourceType: 'ocr',
|
||||
version: OCR_VERSION,
|
||||
precision: 33.90,
|
||||
precisionDetail: '301/889',
|
||||
recall: 20.00,
|
||||
recallDetail: '301/1505',
|
||||
f1: 25.10,
|
||||
avgTime: '1m23s',
|
||||
avgInputToken: '375K',
|
||||
avgOutputToken: '10K',
|
||||
avgTotalToken: '385K',
|
||||
},
|
||||
{
|
||||
model: 'Qwen3.7-Max',
|
||||
company: 'Alibaba',
|
||||
sourceType: 'ocr',
|
||||
version: OCR_VERSION,
|
||||
precision: 25.20,
|
||||
precisionDetail: '276/1096',
|
||||
recall: 18.30,
|
||||
recallDetail: '276/1505',
|
||||
f1: 21.20,
|
||||
avgTime: '4m41s',
|
||||
avgInputToken: '587K',
|
||||
avgOutputToken: '38K',
|
||||
avgTotalToken: '625K',
|
||||
},
|
||||
{
|
||||
model: 'GPT-5.5',
|
||||
company: 'OpenAI',
|
||||
sourceType: 'ocr',
|
||||
version: OCR_VERSION,
|
||||
precision: 32.10,
|
||||
precisionDetail: '234/728',
|
||||
recall: 15.50,
|
||||
recallDetail: '234/1505',
|
||||
f1: 21.00,
|
||||
avgTime: '2m51s',
|
||||
avgInputToken: '409K',
|
||||
avgOutputToken: '13K',
|
||||
avgTotalToken: '422K',
|
||||
},
|
||||
{
|
||||
model: 'Claude-4.8-Opus',
|
||||
company: 'Anthropic',
|
||||
sourceType: 'ocr',
|
||||
version: OCR_VERSION,
|
||||
precision: 37.80,
|
||||
precisionDetail: '176/465',
|
||||
recall: 11.70,
|
||||
recallDetail: '176/1505',
|
||||
f1: 17.90,
|
||||
avgTime: '1m6s',
|
||||
avgInputToken: '342K',
|
||||
avgOutputToken: '11K',
|
||||
avgTotalToken: '352K',
|
||||
},
|
||||
{
|
||||
model: 'Deepseek-V4-Pro',
|
||||
company: 'DeepSeek',
|
||||
sourceType: 'ocr',
|
||||
version: OCR_VERSION,
|
||||
precision: 30.60,
|
||||
precisionDetail: '191/624',
|
||||
recall: 12.70,
|
||||
recallDetail: '191/1505',
|
||||
f1: 17.90,
|
||||
avgTime: '6m28s',
|
||||
avgInputToken: '350K',
|
||||
avgOutputToken: '44K',
|
||||
avgTotalToken: '394K',
|
||||
},
|
||||
{
|
||||
model: 'GLM-5.1',
|
||||
company: 'Zhipu AI',
|
||||
sourceType: 'ocr',
|
||||
version: OCR_VERSION,
|
||||
precision: 28.90,
|
||||
precisionDetail: '237/820',
|
||||
recall: 15.70,
|
||||
recallDetail: '237/1505',
|
||||
f1: 20.40,
|
||||
avgTime: '4m11s',
|
||||
avgInputToken: '707K',
|
||||
avgOutputToken: '36K',
|
||||
avgTotalToken: '743K',
|
||||
},
|
||||
{
|
||||
model: 'GLM-5.2',
|
||||
company: 'Zhipu AI',
|
||||
sourceType: 'ocr',
|
||||
version: OCR_VERSION,
|
||||
precision: 32.30,
|
||||
precisionDetail: '239/741',
|
||||
recall: 15.90,
|
||||
recallDetail: '239/1505',
|
||||
f1: 21.30,
|
||||
avgTime: '7m58s',
|
||||
avgInputToken: '624K',
|
||||
avgOutputToken: '58K',
|
||||
avgTotalToken: '682K',
|
||||
},
|
||||
{
|
||||
model: 'Claude-4.6-Opus',
|
||||
company: 'Anthropic',
|
||||
sourceType: 'cc',
|
||||
version: CC_VERSION,
|
||||
precision: 7.23,
|
||||
precisionDetail: '435/5980',
|
||||
recall: 28.90,
|
||||
recallDetail: '435/1505',
|
||||
f1: 11.57,
|
||||
avgTime: '13m6s',
|
||||
avgInputToken: '5603K',
|
||||
avgOutputToken: '60K',
|
||||
avgTotalToken: '5664K',
|
||||
},
|
||||
{
|
||||
model: 'Qwen3.7-Max',
|
||||
company: 'Alibaba',
|
||||
sourceType: 'cc',
|
||||
version: CC_VERSION,
|
||||
precision: 8.23,
|
||||
precisionDetail: '351/4260',
|
||||
recall: 23.37,
|
||||
recallDetail: '351/1505',
|
||||
f1: 12.17,
|
||||
avgTime: '8m6s',
|
||||
avgInputToken: '5108K',
|
||||
avgOutputToken: '44K',
|
||||
avgTotalToken: '5153K',
|
||||
},
|
||||
{
|
||||
model: 'Claude-4.8-Opus',
|
||||
company: 'Anthropic',
|
||||
sourceType: 'cc',
|
||||
version: CC_VERSION,
|
||||
precision: 15.93,
|
||||
precisionDetail: '191/1200',
|
||||
recall: 12.70,
|
||||
recallDetail: '191/1505',
|
||||
f1: 14.13,
|
||||
avgTime: '5m38s',
|
||||
avgInputToken: '2,039K',
|
||||
avgOutputToken: '23K',
|
||||
avgTotalToken: '2,062K',
|
||||
},
|
||||
{
|
||||
model: 'Deepseek-V4-Pro',
|
||||
company: 'DeepSeek',
|
||||
sourceType: 'cc',
|
||||
version: CC_VERSION,
|
||||
precision: 8.27,
|
||||
precisionDetail: '243/2945',
|
||||
recall: 16.13,
|
||||
recallDetail: '243/1505',
|
||||
f1: 10.93,
|
||||
avgTime: '14m24s',
|
||||
avgInputToken: '5389K',
|
||||
avgOutputToken: '60K',
|
||||
avgTotalToken: '5450K',
|
||||
},
|
||||
{
|
||||
model: 'GLM-5.1',
|
||||
company: 'Zhipu AI',
|
||||
sourceType: 'cc',
|
||||
version: CC_VERSION,
|
||||
precision: 8.37,
|
||||
precisionDetail: '313/3742',
|
||||
recall: 20.80,
|
||||
recallDetail: '313/1505',
|
||||
f1: 11.93,
|
||||
avgTime: '14m10s',
|
||||
avgInputToken: '3,998K',
|
||||
avgOutputToken: '39K',
|
||||
avgTotalToken: '4,038K',
|
||||
},
|
||||
{
|
||||
model: 'GPT-5.5',
|
||||
company: 'OpenAI',
|
||||
sourceType: 'codex',
|
||||
version: CODEX_VERSION,
|
||||
precision: 27.82,
|
||||
precisionDetail: '74/266',
|
||||
recall: 4.92,
|
||||
recallDetail: '74/1505',
|
||||
f1: 8.36,
|
||||
avgTime: '2m58s',
|
||||
avgInputToken: '520K',
|
||||
avgOutputToken: '5K',
|
||||
avgTotalToken: '525K',
|
||||
},
|
||||
];
|
||||
|
||||
const medalIcons: Record<string, string> = {
|
||||
gold: '🥇',
|
||||
silver: '🥈',
|
||||
bronze: '🥉',
|
||||
};
|
||||
|
||||
function computeMedals(
|
||||
entries: BenchmarkEntry[],
|
||||
field: 'precision' | 'recall'
|
||||
): Map<number, string> {
|
||||
const indexed = entries
|
||||
.map((e, i) => ({ index: i, value: e[field] }))
|
||||
.filter((e): e is { index: number; value: number } => e.value !== undefined)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const medals = new Map<number, string>();
|
||||
const types = ['gold', 'silver', 'bronze'];
|
||||
indexed.slice(0, 3).forEach((item, i) => {
|
||||
medals.set(item.index, types[i]);
|
||||
});
|
||||
return medals;
|
||||
provider: string;
|
||||
source: string;
|
||||
sourceIcon?: string;
|
||||
f1: string;
|
||||
f1Value: number;
|
||||
precision: string;
|
||||
precisionValue: number;
|
||||
precisionDetail: string;
|
||||
recall: string;
|
||||
recallValue: number;
|
||||
recallDetail: string;
|
||||
avgTime: string;
|
||||
avgToken: string;
|
||||
tokenDetail: string;
|
||||
precisionMedal?: number; // 0=gold, 1=silver, 2=bronze
|
||||
recallMedal?: number; // 0=gold, 1=silver, 2=bronze
|
||||
}
|
||||
|
||||
type SortField = 'f1' | 'precision' | 'recall';
|
||||
type SortOrder = 'desc' | 'asc';
|
||||
|
||||
const rawRows: BenchmarkRow[] = [
|
||||
{ model: 'Claude-4.6-Opus', provider: 'Anthropic', source: 'Open Code Review', sourceIcon: ocrIcon1, f1: '25.10%', f1Value: 25.10, precision: '33.90%', precisionValue: 33.90, precisionDetail: '301/889', recall: '20.00%', recallValue: 20.00, recallDetail: '301/1505', avgTime: '1m23s', avgToken: '385K', tokenDetail: '375K / 10K', precisionMedal: 1 },
|
||||
{ model: 'GLM-5.2', provider: 'Zhipu AI', source: 'Open Code Review', sourceIcon: ocrIcon2, f1: '21.30%', f1Value: 21.30, precision: '32.30%', precisionValue: 32.30, precisionDetail: '239/741', recall: '15.90%', recallValue: 15.90, recallDetail: '239/1505', avgTime: '7m58s', avgToken: '682K', tokenDetail: '624K / 58K', precisionMedal: 2 },
|
||||
{ model: 'Qwen3.7-Max', provider: 'Alibaba', source: 'Open Code Review', sourceIcon: ocrIcon3, f1: '21.20%', f1Value: 21.20, precision: '25.20%', precisionValue: 25.20, precisionDetail: '276/1096', recall: '18.30%', recallValue: 18.30, recallDetail: '276/1505', avgTime: '4m41s', avgToken: '625K', tokenDetail: '587K / 38K' },
|
||||
{ model: 'GPT-5.5', provider: 'OpenAI', source: 'Open Code Review', sourceIcon: ocrIcon4, f1: '21.00%', f1Value: 21.00, precision: '32.10%', precisionValue: 32.10, precisionDetail: '234/728', recall: '15.50%', recallValue: 15.50, recallDetail: '234/1505', avgTime: '2m51s', avgToken: '422K', tokenDetail: '409K / 13K' },
|
||||
{ model: 'GLM-5.1', provider: 'Zhipu AI', source: 'Open Code Review', sourceIcon: ocrIcon5, f1: '20.40%', f1Value: 20.40, precision: '28.90%', precisionValue: 28.90, precisionDetail: '237/820', recall: '15.70%', recallValue: 15.70, recallDetail: '237/1505', avgTime: '4m11s', avgToken: '743K', tokenDetail: '707K / 36K' },
|
||||
{ model: 'Claude-4.8-Opus', provider: 'Anthropic', source: 'Open Code Review', sourceIcon: ocrIcon6, f1: '17.90%', f1Value: 17.90, precision: '37.80%', precisionValue: 37.80, precisionDetail: '176/465', recall: '11.70%', recallValue: 11.70, recallDetail: '176/1505', avgTime: '1m6s', avgToken: '352K', tokenDetail: '342K / 11K', precisionMedal: 0 },
|
||||
{ model: 'Deepseek-V4-Pro', provider: 'DeepSeek', source: 'Open Code Review', sourceIcon: ocrIcon7, f1: '17.90%', f1Value: 17.90, precision: '30.60%', precisionValue: 30.60, precisionDetail: '191/624', recall: '12.70%', recallValue: 12.70, recallDetail: '191/1505', avgTime: '6m28s', avgToken: '394K', tokenDetail: '350K / 44K' },
|
||||
{ model: 'Claude-4.8-Opus', provider: 'Anthropic', source: 'Claude Code', sourceIcon: claudeCodeIcon, f1: '14.13%', f1Value: 14.13, precision: '15.93%', precisionValue: 15.93, precisionDetail: '191/1200', recall: '12.70%', recallValue: 12.70, recallDetail: '191/1505', avgTime: '5m38s', avgToken: '2,062K', tokenDetail: '2,039K / 23K' },
|
||||
{ model: 'Qwen3.7-Max', provider: 'Alibaba', source: 'Claude Code', sourceIcon: claudeCodeIcon, f1: '12.17%', f1Value: 12.17, precision: '8.23%', precisionValue: 8.23, precisionDetail: '351/4260', recall: '23.37%', recallValue: 23.37, recallDetail: '351/1505', avgTime: '8m6s', avgToken: '5,153K', tokenDetail: '5,108K / 44K', recallMedal: 1 },
|
||||
{ model: 'GLM-5.1', provider: 'Zhipu AI', source: 'Claude Code', sourceIcon: claudeCodeIcon, f1: '11.93%', f1Value: 11.93, precision: '8.37%', precisionValue: 8.37, precisionDetail: '313/3742', recall: '20.80%', recallValue: 20.80, recallDetail: '313/1505', avgTime: '14m10s', avgToken: '4,038K', tokenDetail: '3,998K / 39K', recallMedal: 2 },
|
||||
{ model: 'Claude-4.6-Opus', provider: 'Anthropic', source: 'Claude Code', sourceIcon: claudeCodeIcon, f1: '11.57%', f1Value: 11.57, precision: '7.23%', precisionValue: 7.23, precisionDetail: '435/5980', recall: '28.90%', recallValue: 28.90, recallDetail: '435/1505', avgTime: '13m6s', avgToken: '5,664K', tokenDetail: '5,603K / 60K', recallMedal: 0 },
|
||||
{ model: 'Deepseek-V4-Pro', provider: 'DeepSeek', source: 'Claude Code', sourceIcon: claudeCodeIcon, f1: '10.93%', f1Value: 10.93, precision: '8.27%', precisionValue: 8.27, precisionDetail: '243/2945', recall: '16.13%', recallValue: 16.13, recallDetail: '243/1505', avgTime: '14m24s', avgToken: '5,450K', tokenDetail: '5,389K / 60K' },
|
||||
{ model: 'GPT-5.5', provider: 'OpenAI', source: 'Codex', sourceIcon: codexIcon, f1: '8.36%', f1Value: 8.36, precision: '27.82%', precisionValue: 27.82, precisionDetail: '74/266', recall: '4.92%', recallValue: 4.92, recallDetail: '74/1505', avgTime: '2m58s', avgToken: '525K', tokenDetail: '520K / 5K' },
|
||||
];
|
||||
|
||||
const MEDAL_RANKS = ['🥇', '🥈', '🥉'];
|
||||
|
||||
const BenchmarkSection: React.FC = () => {
|
||||
const [hoveredRow, setHoveredRow] = useState<number | null>(null);
|
||||
const [sortField, setSortField] = useState<SortField>('f1');
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isTablet } = useResponsive();
|
||||
const [sortField, setSortField] = useState<SortField>('f1');
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
|
||||
|
||||
const sortedData = [...benchmarkData].sort((a, b) => {
|
||||
const av = a[sortField];
|
||||
const bv = b[sortField];
|
||||
if (av == null && bv == null) return 0;
|
||||
if (av == null) return 1;
|
||||
if (bv == null) return -1;
|
||||
return bv - av;
|
||||
});
|
||||
const sortedRows = useMemo(() => {
|
||||
const key = `${sortField}Value` as keyof BenchmarkRow;
|
||||
const sorted = [...rawRows].sort((a, b) => {
|
||||
const aVal = a[key] as number;
|
||||
const bVal = b[key] as number;
|
||||
return sortOrder === 'desc' ? bVal - aVal : aVal - bVal;
|
||||
});
|
||||
return sorted;
|
||||
}, [sortField, sortOrder]);
|
||||
|
||||
const precisionMedals = computeMedals(sortedData, 'precision');
|
||||
const recallMedals = computeMedals(sortedData, 'recall');
|
||||
|
||||
const ranks = new Map<number, number>();
|
||||
let rank = 0;
|
||||
sortedData.forEach((entry, index) => {
|
||||
if (entry[sortField] != null) {
|
||||
rank++;
|
||||
ranks.set(index, rank);
|
||||
const handleSort = useCallback((field: SortField) => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(prev => prev === 'desc' ? 'asc' : 'desc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder('desc');
|
||||
}
|
||||
});
|
||||
}, [sortField]);
|
||||
|
||||
const sortableHeaders: { key: string; label: string; field?: SortField }[] = [
|
||||
{ key: 'rank', label: t('benchmark.colRank') },
|
||||
{ key: 'model', label: t('benchmark.colModel') },
|
||||
{ key: 'source', label: t('benchmark.colSource') },
|
||||
{ key: 'f1', label: t('benchmark.colF1'), field: 'f1' },
|
||||
{ key: 'precision', label: t('benchmark.colPrecision'), field: 'precision' },
|
||||
{ key: 'recall', label: t('benchmark.colRecall'), field: 'recall' },
|
||||
{ key: 'avgTime', label: t('benchmark.colAvgTime') },
|
||||
{ key: 'avgToken', label: t('benchmark.colAvgToken') },
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="benchmark" className="py-24 relative noise-overlay">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 w-[800px] h-[600px] rounded-full bg-brand-500/[0.02] blur-[140px] pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="section-divider mb-24"></div>
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="text-center mb-12">
|
||||
<p className="text-slate-500 text-sm font-mono uppercase tracking-widest mb-3">
|
||||
{t('benchmark.sectionLabel')}
|
||||
</p>
|
||||
<h2 className="text-4xl font-bold text-white mb-4">
|
||||
{t('benchmark.title')}
|
||||
</h2>
|
||||
<p className="text-slate-400 max-w-4xl mx-auto">
|
||||
{t('benchmark.subtitlePreRepos')}
|
||||
<span className="text-white font-semibold">50</span>
|
||||
{t('benchmark.subtitlePrePRs')}
|
||||
<span className="text-white font-semibold">200</span>
|
||||
{t('benchmark.subtitlePreLangs')}
|
||||
<span className="text-white font-semibold">10</span>
|
||||
{t('benchmark.subtitleEnd')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mb-6 flex items-center gap-5 flex-wrap">
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<OcrIcon className="w-4 h-4 rounded-sm" />
|
||||
<span className="text-brand-400 font-medium">Open Code Review · {OCR_VERSION}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<ClaudeIcon className="w-4 h-4" />
|
||||
<span className="text-[#D97757] font-medium">Claude Code · {CC_VERSION} · /code-review</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<OpenAIIcon className="w-4 h-4" />
|
||||
<span className="text-[#10a37f] font-medium">Codex · {CODEX_VERSION} · /review</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="rounded-2xl overflow-hidden glass-strong gradient-border shadow-2xl shadow-black/30">
|
||||
{/* Header */}
|
||||
<div className="grid grid-cols-[2.5rem_10rem_11rem_repeat(5,1fr)] gap-2 px-6 py-3 bg-dark-700/60 text-xs font-medium text-slate-500 uppercase tracking-wider">
|
||||
<div>{t('benchmark.colRank')}</div>
|
||||
<div>{t('benchmark.colModel')}</div>
|
||||
<div>{t('benchmark.colSource')}</div>
|
||||
{(['f1', 'precision', 'recall'] as const).map((field) => {
|
||||
const labels: Record<SortField, string> = {
|
||||
f1: 'F1',
|
||||
precision: t('benchmark.colPrecision'),
|
||||
recall: t('benchmark.colRecall'),
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={field}
|
||||
className={`cursor-pointer select-none transition-colors hover:text-slate-300 ${sortField === field ? 'text-brand-400' : ''}`}
|
||||
onClick={() => setSortField(field)}
|
||||
>
|
||||
{labels[field]}
|
||||
<span className={`ml-1 ${sortField === field ? 'opacity-100' : 'opacity-30'}`}>▼</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div>{t('benchmark.colAvgTime')}</div>
|
||||
<div>{t('benchmark.colAvgToken')}</div>
|
||||
</div>
|
||||
|
||||
{/* Rows */}
|
||||
{sortedData.map((entry, index) => {
|
||||
const entryRank = ranks.get(index);
|
||||
const hasData = entry.f1 != null;
|
||||
const pMedal = precisionMedals.get(index);
|
||||
const rMedal = recallMedals.get(index);
|
||||
<section
|
||||
id="benchmark"
|
||||
style={{ width: '100%', display: 'flex', justifyContent: 'center', padding: isMobile ? '60px 20px' : isTablet ? '80px 40px' : '80px 0', overflow: 'hidden' }}
|
||||
>
|
||||
<div style={{ width: '100%', maxWidth: 1200, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: isMobile ? 32 : 48 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
||||
<span style={{ color: '#2BDE5E', fontSize: 16, fontWeight: 500, letterSpacing: '0.48px' }}>
|
||||
{t('benchmark.sectionLabel')}
|
||||
</span>
|
||||
<h2 style={{ color: '#FFFFFF', fontSize: isMobile ? 28 : 48, fontWeight: 500, textAlign: 'center', lineHeight: isMobile ? '34px' : '52px', letterSpacing: '0.96px', margin: 0, maxWidth: 758 }}>
|
||||
{t('benchmark.title')}
|
||||
</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: 16, textAlign: 'center', lineHeight: '24px', margin: 0, maxWidth: 646 }}>
|
||||
{t('benchmark.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div style={{ width: '100%', display: 'flex', flexDirection: 'column', border: '1px solid rgba(255,255,255,0.16)', borderRadius: 8, overflow: isMobile ? 'auto' : 'hidden' }}>
|
||||
{/* Table Header */}
|
||||
<div style={{
|
||||
width: 1200,
|
||||
minWidth: 1200,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'rgba(255,255,255,0.16)',
|
||||
borderTopWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderRightWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
}}>
|
||||
{sortableHeaders.map((col, i) => {
|
||||
const isActive = col.field === sortField;
|
||||
const isSortable = !!col.field;
|
||||
return (
|
||||
<div
|
||||
key={`${entry.model}-${entry.sourceType}`}
|
||||
className={`leaderboard-row grid grid-cols-[2.5rem_10rem_11rem_repeat(5,1fr)] gap-2 px-6 py-4 items-center cursor-default ${
|
||||
hasData && entry.sourceType === 'ocr' ? 'bg-brand-500/3' : ''
|
||||
} ${hoveredRow === index ? 'bg-brand-500/6' : ''}`}
|
||||
onMouseEnter={() => setHoveredRow(index)}
|
||||
onMouseLeave={() => setHoveredRow(null)}
|
||||
key={col.key}
|
||||
onClick={isSortable ? () => handleSort(col.field!) : undefined}
|
||||
style={{
|
||||
width: i === 0 ? 120 : i === 1 ? 200 : i === 2 ? 200 : undefined,
|
||||
flex: i > 2 ? 1 : undefined,
|
||||
height: 44,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
padding: i === 0 ? '10px 20px' : '10px 12px',
|
||||
cursor: isSortable ? 'pointer' : 'default',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{/* Rank */}
|
||||
<div>
|
||||
{entryRank != null ? (
|
||||
<span className="text-lg w-6 inline-block text-center">
|
||||
{entryRank <= 3 ? medalIcons[['gold', 'silver', 'bronze'][entryRank - 1]] : (
|
||||
<span className="text-slate-500 font-mono text-sm">{entryRank}</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="w-6 inline-flex items-center justify-center">
|
||||
<span className="w-3.5 h-3.5 rounded-full border-2 border-slate-600 border-t-brand-400 animate-spin" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Model + Company */}
|
||||
<div>
|
||||
<div className="text-white text-sm font-medium">{entry.model}</div>
|
||||
<div className="text-slate-500 text-xs mt-0.5">{entry.company}</div>
|
||||
</div>
|
||||
|
||||
{/* Source */}
|
||||
<div className="flex items-center gap-2">
|
||||
{entry.sourceType === 'ocr' && <OcrIcon className="w-5 h-5 rounded shrink-0" />}
|
||||
{entry.sourceType === 'cc' && <ClaudeIcon className="w-5 h-5 shrink-0" />}
|
||||
{entry.sourceType === 'codex' && <OpenAIIcon className="w-5 h-5 shrink-0" />}
|
||||
<span className={`text-xs whitespace-nowrap ${sourceColorMap[entry.sourceType]}`}>
|
||||
{sourceNameMap[entry.sourceType]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* F1 */}
|
||||
<div>
|
||||
{entry.f1 != null ? (
|
||||
<span
|
||||
className={`text-sm font-bold ${
|
||||
sortField === 'f1' && entryRank != null && entryRank <= 3 ? 'text-brand-400' : 'text-white'
|
||||
}`}
|
||||
>
|
||||
{entry.f1.toFixed(2)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-500 text-xs font-medium flex items-center gap-1.5">
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-brand-400/60 animate-pulse" />
|
||||
Running
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Precision with medal */}
|
||||
<div>
|
||||
{entry.precision != null ? (
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<span className="text-slate-300 text-sm">{entry.precision.toFixed(2)}%</span>
|
||||
<span className="w-5 inline-block text-center text-lg leading-none">
|
||||
{pMedal ? medalIcons[pMedal] : ''}
|
||||
</span>
|
||||
</div>
|
||||
{entry.precisionDetail && (
|
||||
<div className="text-slate-600 text-xs font-mono mt-0.5">{entry.precisionDetail}</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4 w-16 rounded bg-slate-800 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-slate-700/50 to-transparent animate-[shimmer_2s_ease-in-out_infinite]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recall with medal */}
|
||||
<div>
|
||||
{entry.recall != null ? (
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<span className="text-slate-300 text-sm">{entry.recall.toFixed(2)}%</span>
|
||||
<span className="w-5 inline-block text-center text-lg leading-none">
|
||||
{rMedal ? medalIcons[rMedal] : ''}
|
||||
</span>
|
||||
</div>
|
||||
{entry.recallDetail && (
|
||||
<div className="text-slate-600 text-xs font-mono mt-0.5">{entry.recallDetail}</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4 w-16 rounded bg-slate-800 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-slate-700/50 to-transparent animate-[shimmer_2s_ease-in-out_infinite]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Avg Time */}
|
||||
<div>
|
||||
{entry.avgTime ? (
|
||||
<span className="text-slate-400 text-sm font-mono">{entry.avgTime}</span>
|
||||
) : (
|
||||
<div className="h-4 w-14 rounded bg-slate-800 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-slate-700/50 to-transparent animate-[shimmer_2s_ease-in-out_infinite]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Avg Token */}
|
||||
<div>
|
||||
{entry.avgTotalToken ? (
|
||||
<div>
|
||||
<span className="text-slate-400 text-sm font-mono">{entry.avgTotalToken}</span>
|
||||
{entry.avgInputToken && (
|
||||
<div className="text-slate-600 text-xs font-mono mt-0.5">{entry.avgInputToken} / {entry.avgOutputToken}</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4 w-14 rounded bg-slate-800 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-slate-700/50 to-transparent animate-[shimmer_2s_ease-in-out_infinite]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span style={{
|
||||
color: isActive ? '#2BDE5E' : 'rgba(255,255,255,0.5)',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.5px',
|
||||
transition: 'color 0.2s ease',
|
||||
}}>
|
||||
{col.label}
|
||||
</span>
|
||||
{isSortable && (
|
||||
<img
|
||||
src={sortIcon}
|
||||
alt=""
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
opacity: isActive ? 1 : 0.5,
|
||||
transform: isActive && sortOrder === 'asc' ? 'rotate(180deg)' : 'none',
|
||||
transition: 'transform 0.2s ease, opacity 0.2s ease',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Table Rows */}
|
||||
{sortedRows.map((row, idx) => {
|
||||
const bgOpacity = idx === 0 ? 0.25 : idx === 1 ? 0.18 : idx === 2 ? 0.1 : 0;
|
||||
const rankDisplay = idx < 3 ? MEDAL_RANKS[idx] : String(idx + 1);
|
||||
return (
|
||||
<div
|
||||
key={`${row.model}-${row.source}`}
|
||||
style={{
|
||||
width: 1200,
|
||||
minWidth: 1200,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: bgOpacity > 0 ? `rgba(43,222,94,${bgOpacity})` : 'transparent',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'rgba(255,255,255,0.16)',
|
||||
borderTopWidth: 0,
|
||||
borderBottomWidth: idx === sortedRows.length - 1 ? 0 : 1,
|
||||
borderRightWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Rank */}
|
||||
<div style={{ width: 120, padding: '10px 20px' }}>
|
||||
<span style={{ color: 'rgba(255,255,255,0.8)', fontSize: idx < 3 ? 18 : 14, fontFamily: 'Menlo, monospace' }}>
|
||||
{rankDisplay}
|
||||
</span>
|
||||
</div>
|
||||
{/* Model */}
|
||||
<div style={{ width: 200, padding: '10px 12px', display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={{ color: '#FFFFFF', fontSize: 14, fontWeight: 500 }}>{row.model}</span>
|
||||
<span style={{ color: 'rgba(255,255,255,0.4)', fontSize: 12 }}>{row.provider}</span>
|
||||
</div>
|
||||
{/* Source */}
|
||||
<div style={{ width: 200, padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{row.sourceIcon && <img src={row.sourceIcon} alt="" style={{ width: 20, height: 20 }} />}
|
||||
<span style={{ color: 'rgba(255,255,255,0.7)', fontSize: 13 }}>{row.source}</span>
|
||||
</div>
|
||||
{/* F1 */}
|
||||
<div style={{ flex: 1, padding: '10px 12px' }}>
|
||||
<span style={{ color: sortField === 'f1' ? '#2BDE5E' : '#FFFFFF', fontSize: 14, fontWeight: sortField === 'f1' ? 600 : 400 }}>
|
||||
{row.f1}
|
||||
</span>
|
||||
</div>
|
||||
{/* Precision */}
|
||||
<div style={{ flex: 1, padding: '10px 12px', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ color: sortField === 'precision' ? '#2BDE5E' : '#FFFFFF', fontSize: 14, fontWeight: sortField === 'precision' ? 600 : 400 }}>
|
||||
{row.precision}
|
||||
</span>
|
||||
{row.precisionMedal !== undefined && <img src={TROPHY_ICONS[row.precisionMedal]} alt="" style={{ width: 16, height: 16, marginLeft: 4 }} />}
|
||||
</div>
|
||||
<span style={{ color: 'rgba(255,255,255,0.4)', fontSize: 11 }}>{row.precisionDetail}</span>
|
||||
</div>
|
||||
{/* Recall */}
|
||||
<div style={{ flex: 1, padding: '10px 12px', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ color: sortField === 'recall' ? '#2BDE5E' : '#FFFFFF', fontSize: 14, fontWeight: sortField === 'recall' ? 600 : 400 }}>
|
||||
{row.recall}
|
||||
</span>
|
||||
{row.recallMedal !== undefined && <img src={TROPHY_ICONS[row.recallMedal]} alt="" style={{ width: 16, height: 16, marginLeft: 4 }} />}
|
||||
</div>
|
||||
<span style={{ color: 'rgba(255,255,255,0.4)', fontSize: 11 }}>{row.recallDetail}</span>
|
||||
</div>
|
||||
{/* Avg Time */}
|
||||
<div style={{ flex: 1, padding: '10px 12px' }}>
|
||||
<span style={{ color: '#FFFFFF', fontSize: 14 }}>{row.avgTime}</span>
|
||||
</div>
|
||||
{/* Avg Token */}
|
||||
<div style={{ flex: 1, padding: '10px 12px', display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={{ color: '#FFFFFF', fontSize: 14 }}>{row.avgToken}</span>
|
||||
<span style={{ color: 'rgba(255,255,255,0.4)', fontSize: 11 }}>{row.tokenDetail}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer note */}
|
||||
<p style={{ color: 'rgba(255,255,255,0.3)', fontSize: 12, textAlign: 'center', margin: 0, marginTop: -24 }}>
|
||||
Open Code Review · v1.3.1 | Claude Code · v2.1.169 · /code-review | Codex · v0.140.0 · /review
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
11
pages/src/components/ColorBends.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.color-bends-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.color-bends-container canvas {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
321
pages/src/components/ColorBends.tsx
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import './ColorBends.css';
|
||||
|
||||
const MAX_COLORS = 8;
|
||||
|
||||
const frag = `
|
||||
#define MAX_COLORS ${MAX_COLORS}
|
||||
uniform vec2 uCanvas;
|
||||
uniform float uTime;
|
||||
uniform float uSpeed;
|
||||
uniform vec2 uRot;
|
||||
uniform int uColorCount;
|
||||
uniform vec3 uColors[MAX_COLORS];
|
||||
uniform int uTransparent;
|
||||
uniform float uScale;
|
||||
uniform float uFrequency;
|
||||
uniform float uWarpStrength;
|
||||
uniform vec2 uPointer;
|
||||
uniform float uMouseInfluence;
|
||||
uniform float uParallax;
|
||||
uniform float uNoise;
|
||||
uniform int uIterations;
|
||||
uniform float uIntensity;
|
||||
uniform float uBandWidth;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
float t = uTime * uSpeed;
|
||||
vec2 p = vUv * 2.0 - 1.0;
|
||||
p += uPointer * uParallax * 0.1;
|
||||
vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);
|
||||
vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);
|
||||
q /= max(uScale, 0.0001);
|
||||
q /= 0.5 + 0.2 * dot(q, q);
|
||||
q += 0.2 * cos(t) - 7.56;
|
||||
vec2 toward = (uPointer - rp);
|
||||
q += toward * uMouseInfluence * 0.2;
|
||||
|
||||
for (int j = 0; j < 5; j++) {
|
||||
if (j >= uIterations - 1) break;
|
||||
vec2 rr = sin(1.5 * (q.yx * uFrequency) + 2.0 * cos(q * uFrequency));
|
||||
q += (rr - q) * 0.15;
|
||||
}
|
||||
|
||||
vec3 col = vec3(0.0);
|
||||
float a = 1.0;
|
||||
|
||||
if (uColorCount > 0) {
|
||||
vec2 s = q;
|
||||
vec3 sumCol = vec3(0.0);
|
||||
float cover = 0.0;
|
||||
for (int i = 0; i < MAX_COLORS; ++i) {
|
||||
if (i >= uColorCount) break;
|
||||
s -= 0.01;
|
||||
vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));
|
||||
float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);
|
||||
float kBelow = clamp(uWarpStrength, 0.0, 1.0);
|
||||
float kMix = pow(kBelow, 0.3);
|
||||
float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);
|
||||
vec2 disp = (r - s) * kBelow;
|
||||
vec2 warped = s + disp * gain;
|
||||
float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);
|
||||
float m = mix(m0, m1, kMix);
|
||||
float w = 1.0 - exp(-uBandWidth / exp(uBandWidth * m));
|
||||
sumCol += uColors[i] * w;
|
||||
cover = max(cover, w);
|
||||
}
|
||||
col = clamp(sumCol, 0.0, 1.0);
|
||||
a = uTransparent > 0 ? cover : 1.0;
|
||||
} else {
|
||||
vec2 s = q;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
s -= 0.01;
|
||||
vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));
|
||||
float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);
|
||||
float kBelow = clamp(uWarpStrength, 0.0, 1.0);
|
||||
float kMix = pow(kBelow, 0.3);
|
||||
float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);
|
||||
vec2 disp = (r - s) * kBelow;
|
||||
vec2 warped = s + disp * gain;
|
||||
float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);
|
||||
float m = mix(m0, m1, kMix);
|
||||
col[k] = 1.0 - exp(-uBandWidth / exp(uBandWidth * m));
|
||||
}
|
||||
a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;
|
||||
}
|
||||
|
||||
col *= uIntensity;
|
||||
|
||||
if (uNoise > 0.0001) {
|
||||
float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
col += (n - 0.5) * uNoise;
|
||||
col = clamp(col, 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 rgb = (uTransparent > 0) ? col * a : col;
|
||||
gl_FragColor = vec4(rgb, a);
|
||||
}
|
||||
`;
|
||||
|
||||
const vert = `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
interface ColorBendsProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
rotation?: number;
|
||||
speed?: number;
|
||||
colors?: string[];
|
||||
transparent?: boolean;
|
||||
autoRotate?: number;
|
||||
scale?: number;
|
||||
frequency?: number;
|
||||
warpStrength?: number;
|
||||
mouseInfluence?: number;
|
||||
parallax?: number;
|
||||
noise?: number;
|
||||
iterations?: number;
|
||||
intensity?: number;
|
||||
bandWidth?: number;
|
||||
}
|
||||
|
||||
export default function ColorBends({
|
||||
className = '',
|
||||
style,
|
||||
rotation = 90,
|
||||
speed = 0.2,
|
||||
colors = [],
|
||||
transparent = true,
|
||||
autoRotate = 0,
|
||||
scale = 1,
|
||||
frequency = 1,
|
||||
warpStrength = 1,
|
||||
mouseInfluence = 1,
|
||||
parallax = 0.5,
|
||||
noise = 0.15,
|
||||
iterations = 1,
|
||||
intensity = 1.5,
|
||||
bandWidth = 6,
|
||||
}: ColorBendsProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const materialRef = useRef<THREE.ShaderMaterial | null>(null);
|
||||
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||
const rotationRef = useRef(rotation);
|
||||
const autoRotateRef = useRef(autoRotate);
|
||||
const pointerTargetRef = useRef(new THREE.Vector2(0, 0));
|
||||
const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));
|
||||
const pointerSmoothRef = useRef(8);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
const geometry = new THREE.PlaneGeometry(2, 2);
|
||||
const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: vert,
|
||||
fragmentShader: frag,
|
||||
uniforms: {
|
||||
uCanvas: { value: new THREE.Vector2(1, 1) },
|
||||
uTime: { value: 0 },
|
||||
uSpeed: { value: speed },
|
||||
uRot: { value: new THREE.Vector2(1, 0) },
|
||||
uColorCount: { value: 0 },
|
||||
uColors: { value: uColorsArray },
|
||||
uTransparent: { value: transparent ? 1 : 0 },
|
||||
uScale: { value: scale },
|
||||
uFrequency: { value: frequency },
|
||||
uWarpStrength: { value: warpStrength },
|
||||
uPointer: { value: new THREE.Vector2(0, 0) },
|
||||
uMouseInfluence: { value: mouseInfluence },
|
||||
uParallax: { value: parallax },
|
||||
uNoise: { value: noise },
|
||||
uIterations: { value: iterations },
|
||||
uIntensity: { value: intensity },
|
||||
uBandWidth: { value: bandWidth },
|
||||
},
|
||||
premultipliedAlpha: true,
|
||||
transparent: true,
|
||||
});
|
||||
materialRef.current = material;
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: false,
|
||||
powerPreference: 'high-performance',
|
||||
alpha: true,
|
||||
});
|
||||
rendererRef.current = renderer;
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
renderer.setClearColor(0x000000, transparent ? 0 : 1);
|
||||
renderer.domElement.style.width = '100%';
|
||||
renderer.domElement.style.height = '100%';
|
||||
renderer.domElement.style.display = 'block';
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
const handleResize = () => {
|
||||
const w = container.clientWidth || 1;
|
||||
const h = container.clientHeight || 1;
|
||||
renderer.setSize(w, h, false);
|
||||
material.uniforms.uCanvas.value.set(w, h);
|
||||
};
|
||||
|
||||
handleResize();
|
||||
|
||||
if ('ResizeObserver' in window) {
|
||||
const ro = new ResizeObserver(handleResize);
|
||||
ro.observe(container);
|
||||
resizeObserverRef.current = ro;
|
||||
} else {
|
||||
(window as any).addEventListener('resize', handleResize);
|
||||
}
|
||||
|
||||
const loop = () => {
|
||||
const dt = clock.getDelta();
|
||||
const elapsed = clock.elapsedTime;
|
||||
material.uniforms.uTime.value = elapsed;
|
||||
|
||||
const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;
|
||||
const rad = (deg * Math.PI) / 180;
|
||||
const c = Math.cos(rad);
|
||||
const s = Math.sin(rad);
|
||||
material.uniforms.uRot.value.set(c, s);
|
||||
|
||||
const cur = pointerCurrentRef.current;
|
||||
const tgt = pointerTargetRef.current;
|
||||
const amt = Math.min(1, dt * pointerSmoothRef.current);
|
||||
cur.lerp(tgt, amt);
|
||||
material.uniforms.uPointer.value.copy(cur);
|
||||
renderer.render(scene, camera);
|
||||
rafRef.current = requestAnimationFrame(loop);
|
||||
};
|
||||
rafRef.current = requestAnimationFrame(loop);
|
||||
|
||||
return () => {
|
||||
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
||||
if (resizeObserverRef.current) resizeObserverRef.current.disconnect();
|
||||
else (window as any).removeEventListener('resize', handleResize);
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
renderer.dispose();
|
||||
renderer.forceContextLoss();
|
||||
if (renderer.domElement && renderer.domElement.parentElement === container) {
|
||||
container.removeChild(renderer.domElement);
|
||||
}
|
||||
};
|
||||
}, [bandWidth, frequency, intensity, iterations, mouseInfluence, noise, parallax, scale, speed, transparent, warpStrength]);
|
||||
|
||||
useEffect(() => {
|
||||
const material = materialRef.current;
|
||||
const renderer = rendererRef.current;
|
||||
if (!material) return;
|
||||
|
||||
rotationRef.current = rotation;
|
||||
autoRotateRef.current = autoRotate;
|
||||
material.uniforms.uSpeed.value = speed;
|
||||
material.uniforms.uScale.value = scale;
|
||||
material.uniforms.uFrequency.value = frequency;
|
||||
material.uniforms.uWarpStrength.value = warpStrength;
|
||||
material.uniforms.uMouseInfluence.value = mouseInfluence;
|
||||
material.uniforms.uParallax.value = parallax;
|
||||
material.uniforms.uNoise.value = noise;
|
||||
material.uniforms.uIterations.value = iterations;
|
||||
material.uniforms.uIntensity.value = intensity;
|
||||
material.uniforms.uBandWidth.value = bandWidth;
|
||||
|
||||
const toVec3 = (hex: string) => {
|
||||
const h = hex.replace('#', '').trim();
|
||||
const v =
|
||||
h.length === 3
|
||||
? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]
|
||||
: [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
||||
return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);
|
||||
};
|
||||
|
||||
const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);
|
||||
for (let i = 0; i < MAX_COLORS; i++) {
|
||||
const vec = material.uniforms.uColors.value[i];
|
||||
if (i < arr.length) vec.copy(arr[i]);
|
||||
else vec.set(0, 0, 0);
|
||||
}
|
||||
material.uniforms.uColorCount.value = arr.length;
|
||||
|
||||
material.uniforms.uTransparent.value = transparent ? 1 : 0;
|
||||
if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);
|
||||
}, [rotation, autoRotate, speed, scale, frequency, warpStrength, mouseInfluence, parallax, noise, iterations, intensity, bandWidth, colors, transparent]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;
|
||||
const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);
|
||||
pointerTargetRef.current.set(x, y);
|
||||
};
|
||||
|
||||
container.addEventListener('pointermove', handlePointerMove);
|
||||
return () => {
|
||||
container.removeEventListener('pointermove', handlePointerMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={containerRef} className={`color-bends-container ${className}`} style={style} />;
|
||||
}
|
||||
43
pages/src/components/FadeInSection.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React, { useRef, useEffect, useState } from 'react';
|
||||
|
||||
interface FadeInSectionProps {
|
||||
children: React.ReactNode;
|
||||
delay?: number; // ms
|
||||
}
|
||||
|
||||
const FadeInSection: React.FC<FadeInSectionProps> = ({ children, delay = 0 }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.08 }
|
||||
);
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transition: `opacity 0.6s ease ${delay}ms`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FadeInSection;
|
||||
|
|
@ -1,86 +1,101 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import icon1 from '../assets/icons/icon-feature-architecture.svg';
|
||||
import icon2 from '../assets/icons/icon-feature-positioning.svg';
|
||||
import icon3 from '../assets/icons/icon-feature-multi-model.svg';
|
||||
import icon4 from '../assets/icons/icon-feature-concurrent.svg';
|
||||
import icon5 from '../assets/icons/icon-feature-compression.svg';
|
||||
import icon6 from '../assets/icons/icon-feature-rules.svg';
|
||||
|
||||
const FeaturesSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isTablet } = useResponsive();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: 'fa-robot',
|
||||
color: 'text-brand-400',
|
||||
bgColor: 'bg-brand-500/10',
|
||||
title: t('features.feat1Title'),
|
||||
description: t('features.feat1Desc'),
|
||||
},
|
||||
{
|
||||
icon: 'fa-crosshairs',
|
||||
color: 'text-cyan-400',
|
||||
bgColor: 'bg-cyan-500/10',
|
||||
title: t('features.feat2Title'),
|
||||
description: t('features.feat2Desc'),
|
||||
},
|
||||
{
|
||||
icon: 'fa-link',
|
||||
color: 'text-purple-400',
|
||||
bgColor: 'bg-purple-500/10',
|
||||
title: t('features.feat3Title'),
|
||||
description: t('features.feat3Desc'),
|
||||
},
|
||||
{
|
||||
icon: 'fa-bolt',
|
||||
color: 'text-yellow-400',
|
||||
bgColor: 'bg-yellow-500/10',
|
||||
title: t('features.feat4Title'),
|
||||
description: t('features.feat4Desc'),
|
||||
},
|
||||
{
|
||||
icon: 'fa-brain',
|
||||
color: 'text-orange-400',
|
||||
bgColor: 'bg-orange-500/10',
|
||||
title: t('features.feat5Title'),
|
||||
description: t('features.feat5Desc'),
|
||||
},
|
||||
{
|
||||
icon: 'fa-scroll',
|
||||
color: 'text-pink-400',
|
||||
bgColor: 'bg-pink-500/10',
|
||||
title: t('features.feat6Title'),
|
||||
description: t('features.feat6Desc'),
|
||||
},
|
||||
{ icon: icon1, title: t('features.feat1Title'), desc: t('features.feat1Desc') },
|
||||
{ icon: icon2, title: t('features.feat2Title'), desc: t('features.feat2Desc') },
|
||||
{ icon: icon3, title: t('features.feat3Title'), desc: t('features.feat3Desc') },
|
||||
{ icon: icon4, title: t('features.feat4Title'), desc: t('features.feat4Desc') },
|
||||
{ icon: icon5, title: t('features.feat5Title'), desc: t('features.feat5Desc') },
|
||||
{ icon: icon6, title: t('features.feat6Title'), desc: t('features.feat6Desc') },
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="features" className="py-24 relative noise-overlay">
|
||||
{/* Ambient glow */}
|
||||
<div className="absolute top-1/3 right-1/4 w-[500px] h-[500px] rounded-full bg-purple-500/[0.02] blur-[100px] pointer-events-none"></div>
|
||||
<div className="absolute bottom-1/3 left-1/4 w-[400px] h-[400px] rounded-full bg-cyan-500/[0.02] blur-[80px] pointer-events-none"></div>
|
||||
<section
|
||||
id="features"
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: isMobile ? '60px 20px' : isTablet ? '80px 40px' : '80px 0',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', maxWidth: 1200, display: 'flex', flexDirection: 'column', gap: isMobile ? 32 : 48 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
||||
<span style={{ color: '#2BDE5E', fontSize: 16, fontWeight: 500, lineHeight: '22px', letterSpacing: '0.48px' }}>
|
||||
{t('features.sectionBadge')}
|
||||
</span>
|
||||
<h2 style={{ color: '#FFFFFF', fontSize: isMobile ? 28 : 48, fontWeight: 500, textAlign: 'center', lineHeight: isMobile ? '34px' : '52px', letterSpacing: '0.96px', margin: 0, maxWidth: 758 }}>
|
||||
{t('features.title')}
|
||||
</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: 16, textAlign: 'center', lineHeight: '24px', margin: 0, maxWidth: 646 }}>
|
||||
{t('features.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="text-center mb-16">
|
||||
{/* Section pill badge */}
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full border border-slate-700/50 bg-dark-700/30 backdrop-blur-sm mb-6">
|
||||
<span className="text-xs text-slate-400 font-medium tracking-wide">{t('features.sectionBadge')}</span>
|
||||
</div>
|
||||
<h2 className="text-4xl font-bold text-white mb-4">
|
||||
{t('features.title')}
|
||||
</h2>
|
||||
<p className="text-slate-400 max-w-2xl mx-auto">
|
||||
{t('features.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.icon} className="feature-card rounded-2xl p-6 group glass">
|
||||
<div className={`w-12 h-12 rounded-xl ${feature.bgColor} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300`}>
|
||||
<i className={`fa-solid ${feature.icon} ${feature.color} text-lg`}></i>
|
||||
</div>
|
||||
<h3 className="text-white font-semibold text-lg mb-2">{feature.title}</h3>
|
||||
<p className="text-slate-400 text-sm leading-relaxed">{feature.description}</p>
|
||||
{/* Grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : isTablet ? 'repeat(2, 1fr)' : 'repeat(3, 1fr)',
|
||||
border: '1px solid rgba(255,255,255,0.16)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
{features.map((feat, i) => {
|
||||
const cols = isMobile ? 1 : isTablet ? 2 : 3;
|
||||
const isLastCol = (i % cols) === cols - 1;
|
||||
const isLastRow = i >= features.length - (features.length % cols || cols);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
padding: isMobile ? '24px 20px' : '32px 28px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
borderRight: isLastCol ? 'none' : '1px solid rgba(255,255,255,0.16)',
|
||||
borderBottom: isLastRow ? 'none' : '1px solid rgba(255,255,255,0.16)',
|
||||
minHeight: isMobile ? undefined : 272,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
border: '1px solid rgba(255,255,255,0.16)',
|
||||
}}
|
||||
>
|
||||
<img src={feat.icon} alt="" style={{ width: 24, height: 24 }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<p style={{ color: '#FFFFFF', fontSize: 16, fontWeight: 500, margin: 0, maxWidth: 352 }}>
|
||||
{feat.title}
|
||||
</p>
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: 14, lineHeight: '20px', margin: 0, maxWidth: 352 }}>
|
||||
{feat.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
125
pages/src/components/Footer.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import githubIcon from '../assets/icons/icon-github.svg';
|
||||
import langIcon from '../assets/icons/icon-language.svg';
|
||||
import { useTranslation } from '../i18n/context';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import type { Language } from '../i18n/types';
|
||||
|
||||
const LANG_OPTIONS: { value: Language; label: string }[] = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
];
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const { language, setLanguage, t } = useTranslation();
|
||||
const { isMobile } = useResponsive();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, []);
|
||||
|
||||
const currentLabel = LANG_OPTIONS.find(o => o.value === language)?.label ?? 'English';
|
||||
return (
|
||||
<footer
|
||||
style={{
|
||||
width: '100%',
|
||||
borderTop: '1px solid rgba(255,255,255,0.12)',
|
||||
padding: isMobile ? '32px 16px' : '64px 32px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: isMobile ? 'flex-start' : 'center',
|
||||
gap: isMobile ? 16 : 0,
|
||||
maxWidth: 1440,
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
<a href="https://github.com/alibaba/open-code-review" target="_blank" rel="noopener noreferrer" style={{ display: 'flex', alignItems: 'center', gap: 6, textDecoration: 'none' }}>
|
||||
<img src={githubIcon} alt="" style={{ width: 18, height: 18 }} />
|
||||
<span style={{ color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>{t('footer.brand')}</span>
|
||||
</a>
|
||||
<span style={{ color: 'rgba(255,255,255,0.4)', fontSize: 13 }}>
|
||||
{t('footer.copyright')}
|
||||
</span>
|
||||
|
||||
{/* Language Switcher */}
|
||||
<div ref={ref} style={{ position: 'relative' }}>
|
||||
<button
|
||||
onClick={() => setOpen(v => !v)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
background: 'none',
|
||||
border: '1px solid rgba(255,255,255,0.17)',
|
||||
borderRadius: 6,
|
||||
padding: '6px 14px',
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
fontSize: 13,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<img src={langIcon} alt="" style={{ width: 14, height: 14 }} />
|
||||
{currentLabel}
|
||||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s' }}>
|
||||
<path d="M1 1L5 5L9 1" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
marginBottom: 6,
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{LANG_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => { setLanguage(opt.value); setOpen(false); }}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
background: opt.value === language ? 'rgba(255,255,255,0.08)' : 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
color: opt.value === language ? '#fff' : 'rgba(255,255,255,0.6)',
|
||||
fontSize: 13,
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -1,177 +1,356 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import ColorBends from './ColorBends';
|
||||
import lineIcon from '../assets/icons/icon-terminal-prompt.svg';
|
||||
|
||||
const terminalLines = [
|
||||
{ delay: 0, text: '$ ocr review --from main --to feature-auth', type: 'command' },
|
||||
{ delay: 600, text: '[ocr] Reviewing 5 file(s) in /home/user/project', type: 'info' },
|
||||
{ delay: 1200, text: '[ocr] ▶ file_read "internal/auth/login.go"', type: 'tool' },
|
||||
{ delay: 1600, text: '[ocr] ✔ file_read (15ms)', type: 'tool-ok' },
|
||||
{ delay: 2000, text: '[ocr] ▶ code_search "password.*hash"', type: 'tool' },
|
||||
{ delay: 2400, text: '[ocr] ✔ code_search (8ms)', type: 'tool-ok' },
|
||||
{ delay: 2800, text: '[ocr] Plan completed for internal/auth/login.go', type: 'info' },
|
||||
{ delay: 3200, text: '', type: 'separator' },
|
||||
{ delay: 3400, text: '─── internal/auth/login.go:42-55 ───', type: 'header' },
|
||||
{ delay: 3600, text: 'Consider using bcrypt cost factor ≥ 12 for password hashing.', type: 'comment' },
|
||||
{ delay: 4000, text: '', type: 'separator' },
|
||||
{ delay: 4200, text: '[ocr] Summary: 5 file(s), 3 comment(s), ~8421 tokens, 12.5s', type: 'result' }
|
||||
{
|
||||
num: 1,
|
||||
hasIcon: true,
|
||||
content: (
|
||||
<span>
|
||||
<span style={{ color: '#756BFF' }}>$ ocr re</span>
|
||||
<span style={{ color: '#e4e4e7' }}>v</span>
|
||||
<span style={{ color: '#E2BA64' }}>iew --from</span>
|
||||
<span style={{ color: '#e4e4e7' }}> </span>
|
||||
<span style={{ color: '#67BAFA' }}>mai</span>
|
||||
<span style={{ color: '#e4e4e7' }}>n --to feature-auth</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
hasIcon: true,
|
||||
content: (
|
||||
<span>
|
||||
<span style={{ color: '#e4e4e7' }}>[o</span>
|
||||
<span style={{ color: '#756BFF' }}>cr] R</span>
|
||||
<span style={{ color: '#e4e4e7' }}>e</span>
|
||||
<span style={{ color: '#67BAFA' }}>v</span>
|
||||
<span style={{ color: '#e4e4e7' }}>iew</span>
|
||||
<span style={{ color: '#67BAFA' }}>ing</span>
|
||||
<span style={{ color: '#e4e4e7' }}> </span>
|
||||
<span style={{ color: '#67BAFA' }}>5 file</span>
|
||||
<span style={{ color: '#e4e4e7' }}>(s) in /home/user/project</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
hasIcon: true,
|
||||
content: (
|
||||
<span>
|
||||
<span style={{ color: '#e4e4e7' }}>[o</span>
|
||||
<span style={{ color: '#D553F6' }}>cr]</span>
|
||||
<span style={{ color: '#e4e4e7' }}> ▶ </span>
|
||||
<span style={{ color: '#756BFF' }}>fi</span>
|
||||
<span style={{ color: '#e4e4e7' }}>l</span>
|
||||
<span style={{ color: '#67BAFA' }}>e</span>
|
||||
<span style={{ color: '#e4e4e7' }}>_re</span>
|
||||
<span style={{ color: '#48AA84' }}>a</span>
|
||||
<span style={{ color: '#e4e4e7' }}>d </span>
|
||||
<span style={{ color: '#67BAFA' }}>"</span>
|
||||
<span style={{ color: '#e4e4e7' }}>int</span>
|
||||
<span style={{ color: '#67BAFA' }}>e</span>
|
||||
<span style={{ color: '#e4e4e7' }}>rna</span>
|
||||
<span style={{ color: '#48AA84' }}>l</span>
|
||||
<span style={{ color: '#e4e4e7' }}>/a</span>
|
||||
<span style={{ color: '#67BAFA' }}>u</span>
|
||||
<span style={{ color: '#e4e4e7' }}>th/login.go"</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
hasIcon: true,
|
||||
content: (
|
||||
<span>
|
||||
<span style={{ color: '#e4e4e7' }}>[ocr</span>
|
||||
<span style={{ color: '#D553F6' }}> ] ✔</span>
|
||||
<span style={{ color: '#e4e4e7' }}> f</span>
|
||||
<span style={{ color: '#756BFF' }}>ile</span>
|
||||
<span style={{ color: '#e4e4e7' }}>_</span>
|
||||
<span style={{ color: '#67BAFA' }}>r</span>
|
||||
<span style={{ color: '#e4e4e7' }}>ead</span>
|
||||
<span style={{ color: '#e4e4e7' }}> (1</span>
|
||||
<span style={{ color: '#67BAFA' }}>5</span>
|
||||
<span style={{ color: '#e4e4e7' }}>ms)</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: 5,
|
||||
hasIcon: true,
|
||||
content: (
|
||||
<span>
|
||||
<span style={{ color: '#e4e4e7' }}>[ocr]</span>
|
||||
<span style={{ color: '#D553F6' }}> ▶</span>
|
||||
<span style={{ color: '#e4e4e7' }}> co</span>
|
||||
<span style={{ color: '#67BAFA' }}>de_</span>
|
||||
<span style={{ color: '#e4e4e7' }}>s</span>
|
||||
<span style={{ color: '#67BAFA' }}>e</span>
|
||||
<span style={{ color: '#e4e4e7' }}>arch</span>
|
||||
<span style={{ color: '#67BAFA' }}> "p</span>
|
||||
<span style={{ color: '#e4e4e7' }}>a</span>
|
||||
<span style={{ color: '#67BAFA' }}>s</span>
|
||||
<span style={{ color: '#e4e4e7' }}>swo</span>
|
||||
<span style={{ color: '#48AA84' }}>r</span>
|
||||
<span style={{ color: '#e4e4e7' }}>d.*hash"</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: 6,
|
||||
hasIcon: true,
|
||||
content: (
|
||||
<span>
|
||||
<span style={{ color: '#e4e4e7' }}>[ocr] ✔ c</span>
|
||||
<span style={{ color: '#67BAFA' }}>ode</span>
|
||||
<span style={{ color: '#e4e4e7' }}>_</span>
|
||||
<span style={{ color: '#67BAFA' }}>s</span>
|
||||
<span style={{ color: '#e4e4e7' }}>ear</span>
|
||||
<span style={{ color: '#67BAFA' }}>ch</span>
|
||||
<span style={{ color: '#e4e4e7' }}> (</span>
|
||||
<span style={{ color: '#67BAFA' }}>8</span>
|
||||
<span style={{ color: '#e4e4e7' }}>ms)</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ num: 7, hasIcon: false, content: <span style={{ color: '#e4e4e7' }}>[ocr] Plan completed for internal/auth/login.go</span> },
|
||||
{ num: 8, hasIcon: false, content: <span style={{ color: '#e4e4e7' }}>─── internal/auth/login.go:42-55 ───</span> },
|
||||
{ num: 9, hasIcon: false, content: <span style={{ color: '#e4e4e7' }}>Consider using bcrypt cost factor ≥ 12 for password hashing.</span> },
|
||||
{
|
||||
num: 10,
|
||||
hasIcon: false,
|
||||
content: (
|
||||
<span>
|
||||
<span style={{ color: '#e4e4e7' }}>[o</span>
|
||||
<span style={{ color: '#D553F6' }}>cr] Su</span>
|
||||
<span style={{ color: '#e4e4e7' }}>m</span>
|
||||
<span style={{ color: '#67BAFA' }}>mar</span>
|
||||
<span style={{ color: '#e4e4e7' }}>y: 5 file(s), 3 comment(s), ~8421 tokens, 12.5s</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ num: 11, hasIcon: false, content: <span style={{ color: '#e4e4e7' }}>|</span> },
|
||||
];
|
||||
|
||||
const TerminalLine: React.FC<{ line: typeof terminalLines[0]; visible: boolean }> = ({ line, visible }) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
command: 'text-brand-400 font-semibold',
|
||||
info: 'text-slate-400',
|
||||
tool: 'text-cyan-400',
|
||||
'tool-ok': 'text-green-400',
|
||||
separator: 'text-slate-600',
|
||||
header: 'text-slate-500 font-mono',
|
||||
comment: 'text-slate-300 text-xs',
|
||||
result: 'text-brand-400 font-medium'
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`font-mono text-sm transition-all duration-300 ${
|
||||
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'
|
||||
} ${colorMap[line.type]}`}
|
||||
>
|
||||
{line.text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HeroSection: React.FC = () => {
|
||||
const [visibleLines, setVisibleLines] = useState<number[]>([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const timers: ReturnType<typeof setTimeout>[] = [];
|
||||
terminalLines.forEach((line, index) => {
|
||||
const timer = setTimeout(() => {
|
||||
setVisibleLines((prev) => [...prev, index]);
|
||||
}, line.delay + 500);
|
||||
timers.push(timer);
|
||||
});
|
||||
return () => { timers.forEach((timer) => clearTimeout(timer)); };
|
||||
}, []);
|
||||
|
||||
const pills = [
|
||||
{ icon: 'fa-check-circle', color: 'text-brand-400', label: t('hero.pill1') },
|
||||
{ icon: 'fa-shield-halved', color: 'text-cyan-400', label: t('hero.pill2') },
|
||||
{ icon: 'fa-bolt', color: 'text-yellow-400', label: t('hero.pill3') }
|
||||
];
|
||||
const { isMobile, isTablet } = useResponsive();
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden grid-bg noise-overlay spotlight">
|
||||
{/* Layered glow orbs */}
|
||||
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[800px] h-[800px] rounded-full bg-brand-500/[0.04] blur-[120px] pointer-events-none"></div>
|
||||
<div className="absolute bottom-1/4 right-1/4 w-[500px] h-[500px] rounded-full bg-cyan-500/[0.03] blur-[100px] pointer-events-none"></div>
|
||||
<div className="absolute top-1/3 left-1/4 w-[300px] h-[300px] rounded-full bg-purple-500/[0.02] blur-[80px] pointer-events-none"></div>
|
||||
<section
|
||||
style={{
|
||||
width: '100vw',
|
||||
marginLeft: 'calc(-50vw + 50%)',
|
||||
height: isMobile ? 820 : isTablet ? 800 : 960,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{/* Shader Background */}
|
||||
<ColorBends
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 0,
|
||||
}}
|
||||
colors={['#0d750d', '#042e04', '#066020']}
|
||||
rotation={90}
|
||||
speed={0.23}
|
||||
scale={1.2}
|
||||
frequency={1}
|
||||
warpStrength={1}
|
||||
mouseInfluence={1}
|
||||
noise={0.33}
|
||||
parallax={0.45}
|
||||
iterations={1}
|
||||
intensity={0.8}
|
||||
bandWidth={6}
|
||||
transparent
|
||||
/>
|
||||
|
||||
{/* Decorative horizontal lines */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand-500/20 to-transparent"></div>
|
||||
{/* Gradient overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: 276,
|
||||
background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, #000000 100%)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-6 pt-24 pb-16 grid lg:grid-cols-2 gap-16 items-center">
|
||||
{/* Left content */}
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-5xl lg:text-5xl font-bold leading-tight tracking-tight">
|
||||
<span className="text-white">{t('hero.title')}</span>
|
||||
<br />
|
||||
<span className="bg-gradient-to-r from-brand-400 via-green-300 to-cyan-400 bg-clip-text text-transparent text-glow">{t('hero.titleHighlight')}</span>
|
||||
</h1>
|
||||
<p className="text-slate-400 text-lg leading-relaxed max-w-xl">
|
||||
{t('hero.description')}
|
||||
</p>
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
paddingTop: isMobile ? 100 : 180,
|
||||
paddingLeft: isMobile ? 20 : 0,
|
||||
paddingRight: isMobile ? 20 : 0,
|
||||
gap: isMobile ? 24 : 32,
|
||||
maxWidth: isMobile ? '100%' : 742,
|
||||
}}
|
||||
>
|
||||
{/* Title */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<h1
|
||||
style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: isMobile ? 28 : isTablet ? 36 : 48,
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
lineHeight: isMobile ? '34px' : isTablet ? '42px' : '52px',
|
||||
letterSpacing: '0.96px',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{t('hero.title').split('\n').map((line, i, arr) => (
|
||||
<React.Fragment key={i}>
|
||||
{line}
|
||||
{i < arr.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
fontSize: isMobile ? 14 : 16,
|
||||
textAlign: 'center',
|
||||
lineHeight: '24px',
|
||||
marginTop: 16,
|
||||
maxWidth: isMobile ? '100%' : 742,
|
||||
}}
|
||||
>
|
||||
{t('hero.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<a
|
||||
href="#quickstart"
|
||||
style={{
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '4px 12px',
|
||||
background: '#ffffff',
|
||||
border: '1px solid #EBEBEB',
|
||||
borderRadius: 6,
|
||||
color: 'rgba(0,0,0,0.77)',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{t('hero.quickStart')}
|
||||
</a>
|
||||
<a
|
||||
href="#/docs"
|
||||
style={{
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '4px 12px',
|
||||
background: 'rgba(0,0,0,0.9)',
|
||||
borderRadius: 6,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
border: '1px solid rgba(255,255,255,0.16)',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{t('hero.learnMore')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Terminal */}
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: isMobile ? '100%' : isTablet ? 560 : 692,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
}}
|
||||
>
|
||||
{/* Terminal header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
background: 'rgba(17,17,17,0.5)',
|
||||
borderTopLeftRadius: 8,
|
||||
borderTopRightRadius: 8,
|
||||
padding: '8px 15px',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontFamily: 'Menlo, monospace' }}>
|
||||
{t('hero.terminal')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Feature pills */}
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
{pills.map((item) => (
|
||||
<div key={item.label} className="flex items-center gap-2 text-slate-300 group">
|
||||
<i className={`fa-solid ${item.icon} ${item.color} group-hover:scale-110 transition-transform`}></i>
|
||||
<span className="group-hover:text-white transition-colors">{item.label}</span>
|
||||
{/* Terminal body */}
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 0',
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
borderBottomLeftRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
overflowX: 'hidden',
|
||||
}}
|
||||
>
|
||||
{terminalLines.map((line) => (
|
||||
<div
|
||||
key={line.num}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
padding: '5px 0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 58,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
paddingLeft: 15,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 19, color: 'rgba(255,255,255,0.3)', fontSize: 'clamp(10px, 1.8vw, 13px)', fontFamily: 'Menlo, monospace' }}>
|
||||
{line.num}
|
||||
</span>
|
||||
{line.hasIcon && <img src={lineIcon} alt="" style={{ width: 15, height: 15 }} />}
|
||||
</div>
|
||||
<span style={{ fontSize: 'clamp(10px, 1.8vw, 15px)', fontFamily: 'Menlo, monospace', lineHeight: '20px', whiteSpace: 'nowrap' }}>
|
||||
{line.content}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA buttons */}
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button
|
||||
onClick={() => document.getElementById('quickstart')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
className="btn-primary text-dark-900 font-semibold px-6 py-3 rounded-xl flex items-center gap-2 shadow-lg"
|
||||
>
|
||||
<i className="fa-solid fa-rocket"></i>
|
||||
{t('hero.cta1')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => document.getElementById('benchmark')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
className="btn-secondary text-brand-400 font-medium px-6 py-3 rounded-xl flex items-center gap-2"
|
||||
>
|
||||
<i className="fa-solid fa-chart-bar"></i>
|
||||
{t('hero.cta2')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Social proof */}
|
||||
<div className="flex items-center gap-6 pt-2">
|
||||
<div className="flex items-center gap-2 text-slate-500 text-sm">
|
||||
<i className="fa-solid fa-users text-slate-600"></i>
|
||||
<span>{t('hero.users')}</span>
|
||||
</div>
|
||||
<div className="w-px h-4 bg-dark-500"></div>
|
||||
<div className="flex items-center gap-2 text-slate-500 text-sm">
|
||||
<i className="fa-brands fa-github text-slate-600"></i>
|
||||
<span>{t('hero.openSource')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right terminal */}
|
||||
<div className="relative">
|
||||
{/* Outer glow frame */}
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-brand-500/10 via-cyan-500/10 to-purple-500/10 rounded-3xl blur-xl opacity-50 pointer-events-none"></div>
|
||||
|
||||
<div className="code-block rounded-2xl p-6 space-y-3 relative overflow-hidden glass-strong">
|
||||
{/* Scan line effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-brand-500/[0.02] to-transparent h-1/4 animate-scan-line pointer-events-none"></div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500/70"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500/70"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-brand-500/70"></div>
|
||||
<span className="ml-2 text-slate-500 text-xs font-mono">{t('hero.terminal')}</span>
|
||||
</div>
|
||||
|
||||
{terminalLines.map((line, index) => (
|
||||
<TerminalLine
|
||||
key={index}
|
||||
line={line}
|
||||
visible={visibleLines.includes(index)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{visibleLines.length < terminalLines.length && (
|
||||
<div className="font-mono text-sm text-brand-400 terminal-cursor"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* F1 badge */}
|
||||
<div className="absolute -top-4 -right-4 floating-badge">
|
||||
<div className="rank-badge px-3 py-2 rounded-xl text-xs font-mono text-brand-400 backdrop-blur-md">
|
||||
<div className="text-lg font-bold">F1: {t('highlights.stat4Value')}</div>
|
||||
<div className="text-slate-500">{t('hero.badgeLabel')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative accent line */}
|
||||
<div className="absolute -bottom-1 left-8 right-8 h-px bg-gradient-to-r from-brand-500/40 via-cyan-500/20 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 animate-bounce">
|
||||
<span className="text-slate-600 text-[10px] uppercase tracking-widest">Scroll</span>
|
||||
<i className="fa-solid fa-chevron-down text-slate-600 text-sm"></i>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,85 +1,162 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
|
||||
// 从字符串中解析数字和前后缀
|
||||
function parseStatValue(value: string): { prefix: string; number: number; suffix: string } {
|
||||
// 匹配 "> 30%" 等格式
|
||||
const match = value.match(/^([^\d]*?)(\d+)(.*)$/);
|
||||
if (match) {
|
||||
return { prefix: match[1], number: parseInt(match[2], 10), suffix: match[3] };
|
||||
}
|
||||
return { prefix: '', number: 0, suffix: value };
|
||||
}
|
||||
|
||||
function useCountUp(target: number, duration: number = 1200, isActive: boolean) {
|
||||
const [current, setCurrent] = useState(0);
|
||||
const rafRef = useRef<number>(0);
|
||||
const startTimeRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) return;
|
||||
startTimeRef.current = performance.now();
|
||||
|
||||
const animate = (now: number) => {
|
||||
const elapsed = now - startTimeRef.current;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
// easeOutExpo
|
||||
const eased = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress);
|
||||
setCurrent(Math.round(eased * target));
|
||||
if (progress < 1) {
|
||||
rafRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
rafRef.current = requestAnimationFrame(animate);
|
||||
return () => cancelAnimationFrame(rafRef.current);
|
||||
}, [isActive, target, duration]);
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
const CountUpValue: React.FC<{ value: string; isVisible: boolean }> = ({ value, isVisible }) => {
|
||||
const { prefix, number, suffix } = parseStatValue(value);
|
||||
const count = useCountUp(number, 2000, isVisible);
|
||||
|
||||
if (number === 0) {
|
||||
// 无法解析数字,直接显示原文
|
||||
return <>{value}</>;
|
||||
}
|
||||
|
||||
return <>{prefix}{isVisible ? count : 0}{suffix}</>;
|
||||
};
|
||||
|
||||
const HighlightsSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isTablet } = useResponsive();
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
observer.disconnect();
|
||||
setIsVisible(true);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
if (sectionRef.current) observer.observe(sectionRef.current);
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: t('highlights.stat1Label'),
|
||||
value: t('highlights.stat1Value'),
|
||||
caption: t('highlights.stat1Caption'),
|
||||
delay: 0,
|
||||
},
|
||||
{
|
||||
label: t('highlights.stat2Label'),
|
||||
value: t('highlights.stat2Value'),
|
||||
caption: t('highlights.stat2Caption'),
|
||||
delay: 100,
|
||||
},
|
||||
{
|
||||
label: t('highlights.stat3Label'),
|
||||
value: t('highlights.stat3Value'),
|
||||
caption: t('highlights.stat3Caption'),
|
||||
delay: 200,
|
||||
},
|
||||
{
|
||||
label: t('highlights.stat4Label'),
|
||||
value: t('highlights.stat4Value'),
|
||||
caption: t('highlights.stat4Caption'),
|
||||
delay: 300,
|
||||
},
|
||||
{ value: t('highlights.stat1Value'), label: t('highlights.stat1Label'), caption: t('highlights.stat1Caption') },
|
||||
{ value: t('highlights.stat2Value'), label: t('highlights.stat2Label'), caption: t('highlights.stat2Caption') },
|
||||
{ value: t('highlights.stat3Value'), label: t('highlights.stat3Label'), caption: t('highlights.stat3Caption') },
|
||||
{ value: t('highlights.stat4Value'), label: t('highlights.stat4Label'), caption: t('highlights.stat4Caption') },
|
||||
{ value: t('highlights.stat5Value'), label: t('highlights.stat5Label'), caption: t('highlights.stat5Caption') },
|
||||
];
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative py-16 overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-dark-900 via-dark-800/50 to-dark-900 pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-6">
|
||||
<div className="highlight-card rounded-2xl p-1">
|
||||
<div className="rounded-2xl bg-dark-900/80 backdrop-blur-md p-10 md:p-14">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-8 lg:gap-0 lg:divide-x lg:divide-dark-500/40">
|
||||
{stats.map((stat, i) => (
|
||||
<article
|
||||
key={i}
|
||||
className={`flex flex-col items-center lg:items-start lg:px-10 xl:px-14 first:lg:pl-0 last:lg:pr-0 transition-all duration-700 ${
|
||||
visible
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-6'
|
||||
}`}
|
||||
style={{ transitionDelay: `${stat.delay}ms` }}
|
||||
>
|
||||
<header className="flex items-center gap-3 mb-4">
|
||||
<span className="block w-8 h-px bg-gradient-to-r from-brand-400 to-transparent"></span>
|
||||
<span className="text-xs font-medium uppercase tracking-widest text-slate-500 whitespace-nowrap">
|
||||
{stat.label}
|
||||
</span>
|
||||
</header>
|
||||
<p className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-brand-400 to-cyan-400 bg-clip-text text-transparent leading-tight">
|
||||
{stat.value}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-slate-400">{stat.caption}</p>
|
||||
</article>
|
||||
))}
|
||||
<section
|
||||
ref={sectionRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: isMobile ? '60px 20px' : isTablet ? '80px 40px' : '80px 120px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 1200,
|
||||
display: 'flex',
|
||||
justifyContent: isMobile ? 'center' : 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
flexWrap: 'wrap',
|
||||
gap: isMobile ? 32 : isTablet ? 24 : 0,
|
||||
}}
|
||||
>
|
||||
{stats.map((stat, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: isMobile ? '45%' : 164,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 40,
|
||||
fontWeight: 600,
|
||||
lineHeight: '48px',
|
||||
}}
|
||||
>
|
||||
<CountUpValue value={stat.value} isVisible={isVisible} />
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
color: 'rgba(255,255,255,0.8)',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.5px',
|
||||
textTransform: 'uppercase',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{stat.label}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
color: 'rgba(255,255,255,0.4)',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
lineHeight: '16px',
|
||||
marginTop: 4,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{stat.caption}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,36 +1,31 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import Navbar from './Navbar';
|
||||
import HeroSection from './HeroSection';
|
||||
import HighlightsSection from './HighlightsSection';
|
||||
import WhySection from './WhySection';
|
||||
import FeaturesSection from './FeaturesSection';
|
||||
import BenchmarkSection from './BenchmarkSection';
|
||||
import QuickStartSection from './QuickStartSection';
|
||||
|
||||
const LandingPage: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const scrollTo = (location.state as { scrollTo?: string })?.scrollTo;
|
||||
if (scrollTo) {
|
||||
const el = document.getElementById(scrollTo);
|
||||
if (el) {
|
||||
setTimeout(() => el.scrollIntoView({ behavior: 'smooth' }), 100);
|
||||
}
|
||||
window.history.replaceState({}, '');
|
||||
}
|
||||
}, [location.state]);
|
||||
interface LandingPageProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const LandingPage: React.FC<LandingPageProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="min-h-screen bg-dark-900 noise-overlay">
|
||||
<Navbar />
|
||||
<HeroSection />
|
||||
<HighlightsSection />
|
||||
<WhySection />
|
||||
<FeaturesSection />
|
||||
<BenchmarkSection />
|
||||
<QuickStartSection />
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
background: '#000000',
|
||||
overflowX: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 1440,
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Navbar />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,146 +1,216 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from '../i18n';
|
||||
import logoSvg from '../../logo.svg';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import socialIcon from '../assets/icons/icon-github.svg';
|
||||
import brandIcon from '../assets/images/brandicon.svg';
|
||||
|
||||
import type { Language } from '../i18n/types';
|
||||
|
||||
const LANG_OPTIONS: { value: Language; label: string }[] = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
];
|
||||
|
||||
const navTabs = [
|
||||
{ path: '/', labelKey: 'navbar.features' },
|
||||
{ path: '/benchmark', labelKey: 'navbar.benchmark' },
|
||||
{ path: '/quickstart', labelKey: 'navbar.quickstart' },
|
||||
{ path: '/docs', labelKey: 'navbar.docs' },
|
||||
];
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const { language, setLanguage, t } = useTranslation();
|
||||
const { isMobile } = useResponsive();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const isDocsPage = location.pathname === '/docs';
|
||||
const { t, language, setLanguage } = useTranslation();
|
||||
|
||||
const navItems = [
|
||||
{ label: t('navbar.features'), id: 'features' },
|
||||
{ label: t('navbar.benchmark'), id: 'benchmark' },
|
||||
{ label: t('navbar.quickstart'), id: 'quickstart' }
|
||||
];
|
||||
|
||||
const navigateToSection = (sectionId: string) => {
|
||||
navigate('/', { state: { scrollTo: sectionId } });
|
||||
setIsMobileMenuOpen(false);
|
||||
};
|
||||
const [langOpen, setLangOpen] = useState(false);
|
||||
const langRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setIsScrolled(window.scrollY > 20);
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (langRef.current && !langRef.current.contains(e.target as Node)) setLangOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
setIsMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
const toggleLanguage = () => {
|
||||
setLanguage(language === 'en' ? 'zh' : 'en');
|
||||
};
|
||||
const currentPath = location.pathname;
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
isScrolled || isDocsPage
|
||||
? 'bg-dark-900/80 backdrop-blur-xl border-b border-dark-600/30 shadow-lg shadow-black/20'
|
||||
: 'bg-transparent'
|
||||
}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: isMobile ? 56 : 72,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid rgba(61,61,61,0.6)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 100,
|
||||
willChange: 'transform',
|
||||
}}
|
||||
>
|
||||
{/* Top edge glow */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand-500/10 to-transparent opacity-0 group-hover:opacity-100"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<Link to="/" className="flex items-center gap-3">
|
||||
<img src={logoSvg} alt="OpenCodeReview" className="w-8 h-8 rounded-lg" />
|
||||
<span className="font-bold text-lg tracking-tight">
|
||||
<span className="text-white">Open Code Review</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => isDocsPage ? navigateToSection(item.id) : scrollToSection(item.id)}
|
||||
className="nav-link text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
<Link
|
||||
to="/docs"
|
||||
className={`nav-link text-sm font-medium transition-colors flex items-center gap-1 ${
|
||||
isDocsPage ? 'text-brand-400' : 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<i className="fa-solid fa-book text-xs"></i>
|
||||
{t('navbar.docs')}
|
||||
</Link>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 1440,
|
||||
height: isMobile ? 56 : 72,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: isMobile ? '0 16px' : '0 32px',
|
||||
}}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
<img src={brandIcon} alt="Open Code Review" style={{ height: isMobile ? 20 : 24 }} />
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
className="text-slate-400 hover:text-white text-sm font-medium px-3 py-1.5 rounded-lg border border-dark-600/30 hover:border-slate-500/50 transition-all"
|
||||
>
|
||||
{language === 'en' ? '中文' : 'EN'}
|
||||
</button>
|
||||
{/* Nav Tabs - hidden on mobile */}
|
||||
{!isMobile && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{navTabs.map((tab) => {
|
||||
const isActive = currentPath === tab.path;
|
||||
return (
|
||||
<button
|
||||
key={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
borderRadius: 8,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
lineHeight: '20px',
|
||||
opacity: isActive ? 1 : 0.6,
|
||||
fontWeight: isActive ? 500 : 400,
|
||||
}}
|
||||
>
|
||||
{t(tab.labelKey)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right section */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'center', gap: 16 }}>
|
||||
{/* Language Switcher */}
|
||||
<div ref={langRef} style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
||||
<button
|
||||
onClick={() => setLangOpen(v => !v)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'none',
|
||||
border: '1px solid #FFFFFF',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
opacity: 0.6,
|
||||
padding: 0,
|
||||
width: 18,
|
||||
height: 18,
|
||||
boxSizing: 'border-box' as const,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
color: '#FFFFFF',
|
||||
lineHeight: '18px',
|
||||
textAlign: 'center' as const,
|
||||
width: '100%',
|
||||
fontFamily: language === 'ja' ? "'Hiragino Sans', sans-serif" : "'PingFang SC', -apple-system, sans-serif",
|
||||
}}>
|
||||
{language === 'en' ? 'En' : language === 'zh' ? '中' : 'あ'}
|
||||
</span>
|
||||
</button>
|
||||
{langOpen && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
right: 0,
|
||||
marginTop: 8,
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
zIndex: 200,
|
||||
minWidth: 100,
|
||||
}}
|
||||
>
|
||||
{LANG_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => { setLanguage(opt.value); setLangOpen(false); }}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
background: opt.value === language ? 'rgba(255,255,255,0.08)' : 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
color: opt.value === language ? '#fff' : 'rgba(255,255,255,0.6)',
|
||||
fontSize: 13,
|
||||
textAlign: 'left' as const,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/alibaba/open-code-review"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary text-brand-400 text-sm font-medium px-4 py-2 rounded-lg flex items-center gap-2"
|
||||
style={{ display: 'flex', alignItems: 'center', opacity: 0.6 }}
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
GitHub
|
||||
<img src={socialIcon} alt="Social" style={{ width: 20, height: 20 }} />
|
||||
</a>
|
||||
<button
|
||||
onClick={() => isDocsPage ? navigateToSection('quickstart') : scrollToSection('quickstart')}
|
||||
className="btn-primary text-dark-900 text-sm font-semibold px-4 py-2 rounded-lg"
|
||||
onClick={() => navigate('/quickstart')}
|
||||
style={{
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '4px 12px',
|
||||
background: '#ffffff',
|
||||
border: '1px solid #EBEBEB',
|
||||
borderRadius: 6,
|
||||
color: 'rgba(0,0,0,0.77)',
|
||||
fontSize: isMobile ? 12 : 14,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{t('navbar.getStarted')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="md:hidden text-slate-400 hover:text-white"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
>
|
||||
<i className={`fa-solid ${isMobileMenuOpen ? 'fa-xmark' : 'fa-bars'} text-lg`}></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden bg-dark-900/95 backdrop-blur-xl border-b border-dark-600/30 px-6 py-4 flex flex-col gap-4">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => isDocsPage ? navigateToSection(item.id) : scrollToSection(item.id)}
|
||||
className="text-slate-400 hover:text-white text-sm font-medium text-left transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
<Link
|
||||
to="/docs"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className={`text-sm font-medium text-left transition-colors flex items-center gap-1 ${
|
||||
isDocsPage ? 'text-brand-400' : 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<i className="fa-solid fa-book text-xs"></i>
|
||||
{t('navbar.docs')}
|
||||
</Link>
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
className="text-slate-400 hover:text-white text-sm font-medium text-left transition-colors"
|
||||
>
|
||||
{language === 'en' ? '切换到中文' : 'Switch to English'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,174 +1,205 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import copyIcon from '../assets/icons/icon-copy.svg';
|
||||
import chevronDown from '../assets/icons/icon-chevron-down.svg';
|
||||
import chevronRight from '../assets/icons/icon-chevron-right.svg';
|
||||
import playIcon from '../assets/icons/icon-play.svg';
|
||||
|
||||
const CodeBlock: React.FC<{
|
||||
block: { label?: string; code: string };
|
||||
index: string;
|
||||
copiedIndex: string | null;
|
||||
onCopy: (code: string, key: string) => void;
|
||||
copyLabel: string;
|
||||
}> = ({ block, index, copiedIndex, onCopy, copyLabel }) => (
|
||||
<div>
|
||||
{block.label && (
|
||||
<p className="text-slate-500 text-xs font-mono mb-2 ml-1">{block.label}</p>
|
||||
)}
|
||||
<div className="relative group/code">
|
||||
<div className="code-block rounded-xl p-4 overflow-x-auto group-hover/code:border-brand-500/30 transition-colors duration-300">
|
||||
<pre className="font-mono text-xs text-brand-400 whitespace-pre">{block.code}</pre>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onCopy(block.code, index)}
|
||||
className="absolute top-2 right-3 text-slate-600 hover:text-brand-400 transition-colors text-xs flex items-center gap-1 opacity-60 group-hover/code:opacity-100"
|
||||
/* Toast */
|
||||
const Toast: React.FC<{ message: string; visible: boolean }> = ({ message, visible }) =>
|
||||
ReactDOM.createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 88,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
padding: '5px 8px 5px 10px',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
pointerEvents: 'none',
|
||||
opacity: visible ? 1 : 0,
|
||||
transition: 'opacity 0.15s ease',
|
||||
zIndex: 9999,
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
|
||||
const CodeBlock: React.FC<{ label: string; code: string; multiline?: boolean; onCopy: (text: string) => void }> = ({ label, code, multiline, onCopy }) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: 12, margin: '0 0 6px 0' }}>{label}</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: multiline ? 'flex-start' : 'center',
|
||||
background: '#000000',
|
||||
borderRadius: 6,
|
||||
padding: '4px 16px',
|
||||
border: '1px solid rgba(255,255,255,0.16)',
|
||||
}}
|
||||
>
|
||||
<pre style={{ color: 'rgba(255,255,255,0.8)', fontSize: 13, fontFamily: 'Menlo, monospace', margin: 0, whiteSpace: 'pre-wrap', lineHeight: '22px' }}>
|
||||
{code}
|
||||
</pre>
|
||||
<div
|
||||
onClick={() => onCopy(code)}
|
||||
style={{ paddingTop: 4, paddingBottom: 4, cursor: 'pointer', flexShrink: 0 }}
|
||||
>
|
||||
<i className={`fa-solid ${copiedIndex === index ? 'fa-check text-brand-400' : 'fa-copy'}`}></i>
|
||||
{copiedIndex === index ? '' : copyLabel}
|
||||
</button>
|
||||
<img src={copyIcon} alt="copy" style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const QuickStartSection: React.FC = () => {
|
||||
const [copiedIndex, setCopiedIndex] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isTablet } = useResponsive();
|
||||
const [toastVisible, setToastVisible] = useState(false);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
number: '01',
|
||||
title: t('quickstart.step1Title'),
|
||||
icon: 'fa-solid fa-cloud-arrow-down',
|
||||
description: t('quickstart.step1Desc'),
|
||||
codeBlocks: [
|
||||
{ label: t('quickstart.step1Label1'), code: `npm i -g @alibaba-group/open-code-review` },
|
||||
{ label: t('quickstart.step1Label2'), code: `ocr version` }
|
||||
]
|
||||
},
|
||||
{
|
||||
number: '02',
|
||||
title: t('quickstart.step2Title'),
|
||||
icon: 'fa-solid fa-sliders',
|
||||
description: t('quickstart.step2Desc'),
|
||||
codeBlocks: [
|
||||
{
|
||||
label: t('quickstart.step2Label1'),
|
||||
code: `ocr config provider`
|
||||
},
|
||||
{
|
||||
label: t('quickstart.step2Label2'),
|
||||
code: `ocr config set llm.url https://api.anthropic.com \\\n && ocr config set llm.auth_token {{your-api-key}} \\\n && ocr config set llm.model claude-opus-4-6 \\\n && ocr config set llm.use_anthropic true`
|
||||
},
|
||||
{ label: t('quickstart.step2Label3'), code: `ocr llm test` }
|
||||
]
|
||||
},
|
||||
{
|
||||
number: '03',
|
||||
title: t('quickstart.step3Title'),
|
||||
icon: 'fa-solid fa-magnifying-glass-code',
|
||||
description: t('quickstart.step3Desc'),
|
||||
codeBlocks: [
|
||||
{
|
||||
code: `${t('quickstart.commentReview')}\nocr review\n\n${t('quickstart.commentBranch')}\nocr review --from main --to feature-auth\n\n${t('quickstart.commentCommit')}\nocr review --commit abc123`
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const handleCopy = (code: string, key: string) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
setCopiedIndex(key);
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
const handleCopy = useCallback((text: string) => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setToastVisible(true);
|
||||
}).catch(() => {
|
||||
fallbackCopy(text);
|
||||
});
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = code;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
setCopiedIndex(key);
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
fallbackCopy(text);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fallbackCopy = (text: string) => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const success = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
if (success) setToastVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!toastVisible) return;
|
||||
const timer = setTimeout(() => setToastVisible(false), 1200);
|
||||
return () => clearTimeout(timer);
|
||||
}, [toastVisible]);
|
||||
|
||||
return (
|
||||
<section id="quickstart" className="py-24 relative noise-overlay">
|
||||
{/* Ambient glow */}
|
||||
<div className="absolute bottom-0 left-1/4 w-[600px] h-[400px] rounded-full bg-brand-500/[0.02] blur-[100px] pointer-events-none"></div>
|
||||
<section
|
||||
id="quickstart"
|
||||
style={{ width: '100%', display: 'flex', justifyContent: 'center', padding: isMobile ? '60px 20px' : isTablet ? '80px 40px' : '80px 0', overflow: 'hidden' }}
|
||||
>
|
||||
<div style={{ width: '100%', maxWidth: 1200, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: isMobile ? 32 : 48 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
||||
<span style={{ color: '#2BDE5E', fontSize: 16, fontWeight: 500, letterSpacing: '0.48px' }}>
|
||||
{t('quickstart.sectionLabel')}
|
||||
</span>
|
||||
<h2 style={{ color: '#FFFFFF', fontSize: isMobile ? 28 : 48, fontWeight: 500, textAlign: 'center', lineHeight: isMobile ? '34px' : '52px', letterSpacing: '0.96px', margin: 0, maxWidth: 758 }}>
|
||||
{t('quickstart.title')}
|
||||
</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: 16, textAlign: 'center', lineHeight: '24px', margin: 0, maxWidth: 646 }}>
|
||||
{t('quickstart.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="section-divider mb-24"></div>
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<div className="text-center mb-16">
|
||||
<p className="text-slate-500 text-sm font-mono uppercase tracking-widest mb-3">{t('quickstart.sectionLabel')}</p>
|
||||
<h2 className="text-4xl font-bold text-white mb-4">
|
||||
{t('quickstart.title')}
|
||||
</h2>
|
||||
<p className="text-slate-400 max-w-xl mx-auto">
|
||||
{t('quickstart.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{steps.map((step, stepIndex) => (
|
||||
<div key={step.number} className="feature-card rounded-2xl p-6 relative glass">
|
||||
{/* Connector line between steps */}
|
||||
{stepIndex < steps.length - 1 && (
|
||||
<div className="absolute left-8 -bottom-8 w-px h-8 bg-gradient-to-b from-brand-500/20 to-transparent hidden lg:block"></div>
|
||||
)}
|
||||
|
||||
{/* Step header */}
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-xl bg-brand-500/10 border border-brand-500/20 flex items-center justify-center">
|
||||
<span className="font-mono text-brand-400 text-sm font-bold">{step.number}</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<i className={`${step.icon} text-slate-500 text-sm`}></i>
|
||||
<h3 className="text-white font-semibold text-lg">{step.title}</h3>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm">{step.description}</p>
|
||||
</div>
|
||||
{/* Steps */}
|
||||
<div style={{ width: isMobile ? '100%' : isTablet ? '100%' : 720, display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
{/* Step 1 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.16)', borderRadius: 8, padding: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 32, height: 32, display: 'flex', justifyContent: 'center', alignItems: 'center', background: 'rgba(255,255,255,0.04)', borderRadius: 6 }}>
|
||||
<span style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontWeight: 500 }}>01</span>
|
||||
</div>
|
||||
|
||||
{/* Code blocks */}
|
||||
<div className="space-y-3">
|
||||
{step.codeBlocks.map((block, blockIdx) => (
|
||||
<CodeBlock
|
||||
key={blockIdx}
|
||||
block={block}
|
||||
index={`${stepIndex}-${blockIdx}`}
|
||||
copiedIndex={copiedIndex}
|
||||
onCopy={handleCopy}
|
||||
copyLabel={t('quickstart.copy')}
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
<p style={{ color: '#FFFFFF', fontSize: 16, fontWeight: 500, margin: 0 }}>{t('quickstart.step1Title')}</p>
|
||||
<p style={{ color: 'rgba(255,255,255,0.4)', fontSize: 13, margin: 0 }}>{t('quickstart.step1Desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<img src={chevronDown} alt="" style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<CodeBlock label={t('quickstart.step1Label1')} code="npm i -g @alibaba-group/open-code-review" onCopy={handleCopy} />
|
||||
<CodeBlock label={t('quickstart.step1Label2')} code="ocr version" onCopy={handleCopy} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Zero-config callout */}
|
||||
<div className="mt-8 stat-card rounded-2xl p-6 border-brand-500/15 glass">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-brand-500/[0.03] via-transparent to-transparent rounded-2xl pointer-events-none"></div>
|
||||
<h3 className="text-white font-semibold mb-3 flex items-center gap-2 relative z-10">
|
||||
<i className="fa-solid fa-magic text-brand-400"></i>
|
||||
{t('quickstart.zeroCfgTitle')}
|
||||
</h3>
|
||||
<p className="text-slate-400 text-sm mb-3 relative z-10">
|
||||
{t('quickstart.zeroCfgDesc')}
|
||||
</p>
|
||||
<div className="code-block rounded-xl p-4 overflow-x-auto relative z-10">
|
||||
<pre className="font-mono text-xs text-brand-400 whitespace-pre">{`export ANTHROPIC_BASE_URL=https://api.anthropic.com
|
||||
export ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxx
|
||||
export ANTHROPIC_MODEL=claude-opus-4-6
|
||||
{/* Step 2 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.16)', borderRadius: 8, padding: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 32, height: 32, display: 'flex', justifyContent: 'center', alignItems: 'center', background: 'rgba(255,255,255,0.04)', borderRadius: 6 }}>
|
||||
<span style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontWeight: 500 }}>02</span>
|
||||
</div>
|
||||
<div>
|
||||
<p style={{ color: '#FFFFFF', fontSize: 16, fontWeight: 500, margin: 0 }}>{t('quickstart.step2Title')}</p>
|
||||
<p style={{ color: 'rgba(255,255,255,0.4)', fontSize: 13, margin: 0 }}>{t('quickstart.step2Desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<img src={chevronRight} alt="" style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<CodeBlock label={t('quickstart.step2Label1')} code="ocr config provider" onCopy={handleCopy} />
|
||||
<CodeBlock
|
||||
label={t('quickstart.step2Label2')}
|
||||
code={`ocr config set llm.url https://api.anthropic.com \\
|
||||
&& ocr config set llm.auth_token {{your-api-key}} \\
|
||||
&& ocr config set llm.model claude-opus-4-6 \\
|
||||
&& ocr config set llm.use_anthropic true`}
|
||||
multiline
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
<CodeBlock label={t('quickstart.step2Label3')} code="ocr llm test" onCopy={handleCopy} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${t('quickstart.commentEnvAuto')} ✨`}</pre>
|
||||
{/* Step 3 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.16)', borderRadius: 8, padding: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 32, height: 32, display: 'flex', justifyContent: 'center', alignItems: 'center', background: 'rgba(255,255,255,0.04)', borderRadius: 6 }}>
|
||||
<span style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontWeight: 500 }}>03</span>
|
||||
</div>
|
||||
<div>
|
||||
<p style={{ color: '#FFFFFF', fontSize: 16, fontWeight: 500, margin: 0 }}>{t('quickstart.step3Title')}</p>
|
||||
<p style={{ color: 'rgba(255,255,255,0.4)', fontSize: 13, margin: 0 }}>{t('quickstart.step3Desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<img src={playIcon} alt="" style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<CodeBlock
|
||||
label={t('quickstart.step3Label1')}
|
||||
code={`${t('quickstart.commentReview')}
|
||||
ocr review
|
||||
|
||||
${t('quickstart.commentBranch')}
|
||||
ocr review --from main --to feature-auth
|
||||
|
||||
${t('quickstart.commentCommit')}
|
||||
ocr review --commit abc123`}
|
||||
multiline
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toast message={t('quickstart.copied')} visible={toastVisible} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
148
pages/src/components/UseCasesSection.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import React, { useCallback, useRef } from 'react';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import iconCase1 from '../assets/icons/icon-usecase-developer.svg';
|
||||
import iconCase2a from '../assets/icons/icon-usecase-platform-a.svg';
|
||||
import iconCase2b from '../assets/icons/icon-usecase-platform-b.svg';
|
||||
import iconCase2c from '../assets/icons/icon-usecase-platform-c.svg';
|
||||
import iconCase3 from '../assets/icons/icon-usecase-researcher.svg';
|
||||
|
||||
const UseCasesSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isTablet } = useResponsive();
|
||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>, index: number) => {
|
||||
const card = cardRefs.current[index];
|
||||
if (!card) return;
|
||||
const rect = card.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
const angle = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI) + 90;
|
||||
card.style.setProperty('--sweep-angle', `${angle}deg`);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback((index: number) => {
|
||||
const card = cardRefs.current[index];
|
||||
if (!card) return;
|
||||
card.style.setProperty('--sweep-angle', '0deg');
|
||||
}, []);
|
||||
|
||||
const useCases = [
|
||||
{ title: t('usecases.case1Title'), desc: t('usecases.case1Desc') },
|
||||
{ title: t('usecases.case2Title'), desc: t('usecases.case2Desc') },
|
||||
{ title: t('usecases.case3Title'), desc: t('usecases.case3Desc') },
|
||||
];
|
||||
|
||||
return (
|
||||
<section
|
||||
id="usecases"
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: isMobile ? '60px 20px' : isTablet ? '80px 40px' : '80px 0',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', maxWidth: 1200, display: 'flex', flexDirection: 'column', gap: isMobile ? 32 : 48 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
||||
<span
|
||||
style={{
|
||||
color: '#2BDE5E',
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
letterSpacing: '0.48px',
|
||||
}}
|
||||
>
|
||||
{t('usecases.sectionLabel')}
|
||||
</span>
|
||||
<h2
|
||||
style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: isMobile ? 28 : 48,
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
lineHeight: isMobile ? '34px' : '52px',
|
||||
letterSpacing: '0.96px',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{t('usecases.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 16 }}>
|
||||
{useCases.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
ref={(el) => { cardRefs.current[i] = el; }}
|
||||
className="usecase-card"
|
||||
onMouseMove={(e) => handleMouseMove(e, i)}
|
||||
onMouseLeave={() => handleMouseLeave(i)}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '48px 32px',
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
marginBottom: 24,
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{i === 0 && (
|
||||
<img src={iconCase1} alt="" style={{ position: 'absolute', left: 5.33, top: 2, width: 53, height: 58 }} />
|
||||
)}
|
||||
{i === 1 && (
|
||||
<div style={{ position: 'absolute', left: 0, top: 0, width: 64, height: 64, transform: 'scale(1.1)', transformOrigin: 'center center' }}>
|
||||
<img src={iconCase2c} alt="" style={{ position: 'absolute', left: -28, top: -5, width: 48, height: 45, zIndex: 2 }} />
|
||||
<img src={iconCase2a} alt="" style={{ position: 'absolute', left: 4, top: 30, width: 48, height: 44, zIndex: 0 }} />
|
||||
<img src={iconCase2b} alt="" style={{ position: 'absolute', left: 40, top: -8, width: 48, height: 53, zIndex: 1 }} />
|
||||
</div>
|
||||
)}
|
||||
{i === 2 && (
|
||||
<img src={iconCase3} alt="" style={{ position: 'absolute', left: 3.67, top: 3, width: 57, height: 60 }} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, position: 'relative', zIndex: 2 }}>
|
||||
<p
|
||||
style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: 500,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
fontSize: 14,
|
||||
lineHeight: '20px',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default UseCasesSection;
|
||||
28
pages/src/hooks/useResponsive.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
export interface Breakpoints {
|
||||
isMobile: boolean; // < 768px
|
||||
isTablet: boolean; // 768px ~ 1024px
|
||||
isDesktop: boolean; // > 1024px
|
||||
}
|
||||
|
||||
export function useResponsive(): Breakpoints {
|
||||
const [bp, setBp] = useState<Breakpoints>(() => getBreakpoints());
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setBp(getBreakpoints());
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return bp;
|
||||
}
|
||||
|
||||
function getBreakpoints(): Breakpoints {
|
||||
const w = window.innerWidth;
|
||||
return {
|
||||
isMobile: w < 768,
|
||||
isTablet: w >= 768 && w <= 1024,
|
||||
isDesktop: w > 1024,
|
||||
};
|
||||
}
|
||||
|
|
@ -2,8 +2,9 @@ import React, { createContext, useContext, useState, useCallback } from 'react';
|
|||
import { Language, TranslationKeys } from './types';
|
||||
import { en } from './en';
|
||||
import { zh } from './zh';
|
||||
import { ja } from './ja';
|
||||
|
||||
const translations: Record<Language, TranslationKeys> = { en, zh };
|
||||
const translations: Record<Language, TranslationKeys> = { en, zh, ja };
|
||||
|
||||
interface LanguageContextValue {
|
||||
language: Language;
|
||||
|
|
@ -18,7 +19,7 @@ const STORAGE_KEY = 'ocr-lang';
|
|||
function getInitialLanguage(): Language {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'en' || stored === 'zh') return stored;
|
||||
if (stored === 'en' || stored === 'zh' || stored === 'ja') return stored;
|
||||
} catch {}
|
||||
return 'en';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,47 +6,42 @@ export const en: TranslationKeys = {
|
|||
'navbar.benchmark': 'Benchmark',
|
||||
'navbar.quickstart': 'Quick Start',
|
||||
'navbar.docs': 'Docs',
|
||||
'navbar.blog': 'Blog',
|
||||
'navbar.getStarted': 'Get Started',
|
||||
|
||||
// Hero
|
||||
'hero.title': 'AI Code Review',
|
||||
'hero.titleHighlight': 'Validated on Millions of Real-World Tasks',
|
||||
'hero.title': 'AI Code Review\nValidated on Millions of\nReal-World Tasks',
|
||||
'hero.description': 'Open Code Review brings Alibaba\'s battle-tested code review Agent into your workflow. Connect any LLM, keep data fully private, and get review comments developers actually adopt.',
|
||||
'hero.pill1': 'Adoption Rate > 30%',
|
||||
'hero.pill2': 'Data Stays Local',
|
||||
'hero.pill3': 'Token Cost Only 1/9',
|
||||
'hero.cta1': 'Quick Start',
|
||||
'hero.cta2': 'View Benchmark',
|
||||
'hero.users': '20K+ Active Users',
|
||||
'hero.openSource': 'Fully Open Source',
|
||||
'hero.terminal': 'ocr terminal',
|
||||
'hero.badgeLabel': 'Benchmark #1',
|
||||
'hero.quickStart': 'Quick Start',
|
||||
'hero.learnMore': 'Learn More',
|
||||
'hero.terminal': 'Terminal',
|
||||
|
||||
// Highlights
|
||||
'highlights.stat1Label': 'Active Users',
|
||||
'highlights.stat1Value': '20K+',
|
||||
'highlights.stat1Label': 'ACTIVE USERS',
|
||||
'highlights.stat1Caption': 'Battle-tested inside Alibaba Group',
|
||||
'highlights.stat2Label': 'Real-World Tasks',
|
||||
'highlights.stat2Value': '1M+',
|
||||
'highlights.stat2Caption': 'Code review tasks executed',
|
||||
'highlights.stat3Label': 'Token Cost',
|
||||
'highlights.stat3Value': '1/9',
|
||||
'highlights.stat3Caption': 'vs. Claude Code · 1,000 PRs',
|
||||
'highlights.stat4Label': 'AACR-Bench',
|
||||
'highlights.stat4Value': '25.1%',
|
||||
'highlights.stat4Caption': 'SEM.F1 benchmark score',
|
||||
'highlights.stat2Value': '> 30%',
|
||||
'highlights.stat2Label': 'ADOPTION RATE',
|
||||
'highlights.stat2Caption': 'Battle-tested inside Alibaba Group',
|
||||
'highlights.stat3Value': '1M+',
|
||||
'highlights.stat3Label': 'REAL-WORLD TASKS',
|
||||
'highlights.stat3Caption': 'Code review tasks executed',
|
||||
'highlights.stat4Value': '1/9',
|
||||
'highlights.stat4Label': 'TOKEN COST',
|
||||
'highlights.stat4Caption': 'vs. Claude Code · 1,000 PRs',
|
||||
'highlights.stat5Value': '25.10%',
|
||||
'highlights.stat5Label': 'AACR-BENCH',
|
||||
'highlights.stat5Caption': 'SEM.F1 benchmark score',
|
||||
'usecases.sectionLabel': 'USE CASES',
|
||||
'usecases.title': 'Who Is Open Code Review For?',
|
||||
'usecases.case1Title': 'Individual Developers',
|
||||
'usecases.case1Desc': 'Embed Open Code Review in your local AI-assisted development workflow for instant, high-quality code review feedback.',
|
||||
'usecases.case2Title': 'Platform Teams',
|
||||
'usecases.case2Desc': 'Seamlessly integrate into your internal systems with full control over data flow and review policies.',
|
||||
'usecases.case3Title': 'ML Researchers',
|
||||
'usecases.case3Desc': 'Use as a code quality verifier in RL training pipelines, providing reliable reward signals for code generation models.',
|
||||
|
||||
// Why Section
|
||||
'why.sectionLabel': 'Use Cases',
|
||||
'why.title': 'Who Is Open Code Review For?',
|
||||
'why.case1Title': 'Individual Developers',
|
||||
'why.case1Desc': 'Embed Open Code Review in your local AI-assisted development workflow for instant, high-quality code review feedback.',
|
||||
'why.case2Title': 'Platform Teams',
|
||||
'why.case2Desc': 'Seamlessly integrate into your internal systems with full control over data flow and review policies.',
|
||||
'why.case3Title': 'ML Researchers',
|
||||
'why.case3Desc': 'Use as a code quality verifier in RL training pipelines, providing reliable reward signals for code generation models.',
|
||||
|
||||
// Features Section
|
||||
// Features
|
||||
'features.sectionBadge': 'Core Features',
|
||||
'features.title': 'An Agent System Purpose-Built for Code Review',
|
||||
'features.subtitle': 'An AI Agent that reads files, searches the codebase, cross-references context, and applies fine-grained rules for deep review.',
|
||||
|
|
@ -63,29 +58,21 @@ export const en: TranslationKeys = {
|
|||
'features.feat6Title': 'Built-in Review Rules',
|
||||
'features.feat6Desc': 'Review rules validated through massive real-world scenarios, covering 10+ languages including Java, TypeScript, Go, Python, Kotlin, Rust, C++, C, with specialized rules for NPE, thread safety, XSS, SQL injection, and more.',
|
||||
|
||||
// Benchmark Section
|
||||
'benchmark.sectionLabel': 'Open Benchmark',
|
||||
// Benchmark
|
||||
'benchmark.sectionLabel': 'OPEN BENCHMARK',
|
||||
'benchmark.title': 'Cross-Validated by 80+ Senior Engineers',
|
||||
'benchmark.subtitlePreRepos': 'A real-world CodeReview benchmark selecting from ',
|
||||
'benchmark.subtitlePrePRs': ' popular open-source repositories, comprising ',
|
||||
'benchmark.subtitlePreLangs': ' real PullRequests, covering ',
|
||||
'benchmark.subtitleEnd': ' programming languages, diverse issue types, and varying changeset sizes.',
|
||||
'benchmark.legendOcr': 'Open Code Review',
|
||||
'benchmark.legendCc': 'Claude Code · /code-review',
|
||||
'benchmark.colRank': 'Rank',
|
||||
'benchmark.colModel': 'Model',
|
||||
'benchmark.colSource': 'Source',
|
||||
'benchmark.colVersion': 'Version',
|
||||
'benchmark.colPrecision': 'Precision',
|
||||
'benchmark.colRecall': 'Recall',
|
||||
'benchmark.colAvgTime': 'Avg Time',
|
||||
'benchmark.colAvgToken': 'Avg Token',
|
||||
'benchmark.tooltipInput': 'Input Token',
|
||||
'benchmark.tooltipOutput': 'Output Token',
|
||||
'benchmark.footer': '',
|
||||
'benchmark.subtitle': 'A real-world CodeReview benchmark selecting from 50 popular open-source repositories, comprising 200 real PullRequests, covering 10 programming languages, diverse issue types, and varying changeset sizes.',
|
||||
'benchmark.colRank': 'RANK',
|
||||
'benchmark.colModel': 'MODEL',
|
||||
'benchmark.colSource': 'SOURCE',
|
||||
'benchmark.colF1': 'F1',
|
||||
'benchmark.colPrecision': 'PRECISION',
|
||||
'benchmark.colRecall': 'RECALL',
|
||||
'benchmark.colAvgTime': 'AVG TIME',
|
||||
'benchmark.colAvgToken': 'AVG TOKEN',
|
||||
|
||||
// QuickStart Section
|
||||
'quickstart.sectionLabel': 'Quick Start',
|
||||
// QuickStart
|
||||
'quickstart.sectionLabel': 'QUICK START',
|
||||
'quickstart.title': 'Up and Running in Minutes',
|
||||
'quickstart.subtitle': 'No complex configuration. No third-party accounts needed beyond your chosen LLM provider.',
|
||||
'quickstart.step1Title': 'Install',
|
||||
|
|
@ -93,17 +80,23 @@ export const en: TranslationKeys = {
|
|||
'quickstart.step1Label1': 'Install',
|
||||
'quickstart.step1Label2': 'Verify Installation',
|
||||
'quickstart.step2Title': 'Configure',
|
||||
'quickstart.step2Desc': 'Set up your LLM provider interactively, or configure manually. Built-in support for Anthropic, OpenAI, DashScope, DeepSeek, and Z.AI.',
|
||||
'quickstart.step2Desc': 'Set up your LLM provider interactively, or configure manually.',
|
||||
'quickstart.step2Label1': 'Interactive Setup (Recommended)',
|
||||
'quickstart.step2Label2': 'Manual Config (Alternative)',
|
||||
'quickstart.step2Label3': 'Verify Configuration',
|
||||
'quickstart.step3Title': 'Run Review',
|
||||
'quickstart.step3Desc': 'Start your first code review.',
|
||||
'quickstart.zeroCfgTitle': 'Already using Claude Code? Zero extra config',
|
||||
'quickstart.zeroCfgDesc': 'If you\'ve already configured Claude Code environment variables (ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_MODEL), Open Code Review reads them automatically — no additional setup needed.',
|
||||
'quickstart.copy': 'Copy',
|
||||
'quickstart.step3Label1': 'Review Commands',
|
||||
'quickstart.commentReview': '# Review current changes',
|
||||
'quickstart.commentBranch': '# Review diff between branches',
|
||||
'quickstart.commentCommit': '# Review a specific commit',
|
||||
'quickstart.copied': 'Copied!',
|
||||
|
||||
// Docs Page
|
||||
// Footer
|
||||
'footer.brand': 'Open Code Review',
|
||||
'footer.copyright': '© Copyright 2026. All rights reserved.',
|
||||
|
||||
// Docs Page (keep existing)
|
||||
'docs.toc': 'Table of Contents',
|
||||
'docs.overview': 'Overview',
|
||||
'docs.install': 'Install',
|
||||
|
|
@ -111,7 +104,6 @@ export const en: TranslationKeys = {
|
|||
'docs.review': 'ocr review',
|
||||
'docs.viewer': 'ocr viewer',
|
||||
'docs.env': 'Claude Code Integration',
|
||||
|
||||
'docs.overviewTitle': 'Overview',
|
||||
'docs.overviewDesc': '(short <code>ocr</code>) is an AI-powered code review CLI tool.',
|
||||
'docs.overviewFeatures': 'Core Features:',
|
||||
|
|
@ -121,11 +113,9 @@ export const en: TranslationKeys = {
|
|||
'docs.overviewFeat4': 'Output format supports text and json',
|
||||
'docs.overviewFeat5': 'Zero config for Claude Code users',
|
||||
'docs.overviewFeat6': 'WebUI viewer for visualizing review results',
|
||||
|
||||
'docs.installTitle': 'Install',
|
||||
'docs.installLabel': 'Install',
|
||||
'docs.installVerifyLabel': 'Verify Installation',
|
||||
|
||||
'docs.configTitle': 'Configuration & Verification',
|
||||
'docs.configDesc': 'Manage CLI configuration settings, stored in <code>~/.opencodereview/config.json</code>.',
|
||||
'docs.configInteractive': 'Interactive Setup (Recommended)',
|
||||
|
|
@ -147,7 +137,6 @@ export const en: TranslationKeys = {
|
|||
'docs.configKeyTelemetry': 'Telemetry',
|
||||
'docs.configVerify': 'Verify Configuration',
|
||||
'docs.configVerifyDesc': 'Run this command after configuration to test the LLM connection and confirm settings are correct.',
|
||||
|
||||
'docs.reviewTitle': 'ocr review',
|
||||
'docs.reviewDesc': '<code>ocr review</code> is the core command for initiating AI code review.',
|
||||
'docs.reviewModes': 'Review Modes',
|
||||
|
|
@ -183,19 +172,11 @@ export const en: TranslationKeys = {
|
|||
'docs.reviewFlag10Default': 'Built-in',
|
||||
'docs.reviewFlag11Desc': 'Max concurrent git subprocesses',
|
||||
'docs.reviewNote': 'Note: <code>--from/--to</code> and <code>--commit</code> cannot be used together. When specifying <code>--from</code>, <code>--to</code> must also be specified.',
|
||||
|
||||
'docs.viewerTitle': 'ocr viewer',
|
||||
'docs.viewerDesc': 'Starts a WebUI session viewer for browsing review results in a web interface.',
|
||||
'docs.viewerNote': 'After running, a local HTTP server starts providing a visual interface for browsing review results.',
|
||||
|
||||
'docs.envTitle': 'Claude Code Integration',
|
||||
'docs.envDesc': 'If you are already a Claude Code user with the following environment variables configured, Open Code Review will recognize them automatically — no extra configuration needed:',
|
||||
'docs.envNote': 'You can also use <code>ocr config</code> to override or supplement these settings.',
|
||||
'docs.copy': 'Copy',
|
||||
|
||||
// QuickStart code comments
|
||||
'quickstart.commentReview': '# Review current changes',
|
||||
'quickstart.commentBranch': '# Review diff between branches',
|
||||
'quickstart.commentCommit': '# Review a specific commit',
|
||||
'quickstart.commentEnvAuto': '# Open Code Review auto-detects these variables',
|
||||
};
|
||||
|
|
|
|||
184
pages/src/i18n/ja.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { TranslationKeys } from './types';
|
||||
|
||||
export const ja: TranslationKeys = {
|
||||
// Navbar
|
||||
'navbar.features': '特徴',
|
||||
'navbar.benchmark': 'ベンチマーク',
|
||||
'navbar.quickstart': 'クイックスタート',
|
||||
'navbar.docs': 'ドキュメント',
|
||||
'navbar.blog': 'ブログ',
|
||||
'navbar.getStarted': '始める',
|
||||
|
||||
// Hero
|
||||
'hero.title': 'AIコードレビュー\n数百万の実タスクで検証済み',
|
||||
'hero.description': 'Open Code Review は、アリババの実戦検証済みコードレビュー Agent をワークフローに統合します。任意の LLM に接続し、データを完全にプライベートに保ち、開発者が実際に採用するレビューコメントを取得できます。',
|
||||
'hero.quickStart': 'クイックスタート',
|
||||
'hero.learnMore': '詳しく見る',
|
||||
'hero.terminal': 'ターミナル',
|
||||
|
||||
// Highlights
|
||||
'highlights.stat1Value': '20K+',
|
||||
'highlights.stat1Label': 'アクティブユーザー',
|
||||
'highlights.stat1Caption': 'Alibabaグループ内で実戦検証済み',
|
||||
'highlights.stat2Value': '> 30%',
|
||||
'highlights.stat2Label': '採用率',
|
||||
'highlights.stat2Caption': 'Alibabaグループ内で実戦検証済み',
|
||||
'highlights.stat3Value': '1M+',
|
||||
'highlights.stat3Label': '実タスク',
|
||||
'highlights.stat3Caption': '実行されたコードレビュータスク',
|
||||
'highlights.stat4Value': '1/9',
|
||||
'highlights.stat4Label': 'トークンコスト',
|
||||
'highlights.stat4Caption': 'Claude Code比較 · 1,000 PR',
|
||||
'highlights.stat5Value': '25.10%',
|
||||
'highlights.stat5Label': 'AACR-BENCH',
|
||||
'highlights.stat5Caption': 'SEM.F1 ベンチマークスコア',
|
||||
|
||||
// Use Cases
|
||||
'usecases.sectionLabel': 'ユースケース',
|
||||
'usecases.title': 'Open Code Review は誰のためのもの?',
|
||||
'usecases.case1Title': '個人開発者',
|
||||
'usecases.case1Desc': 'Open Code Review をローカルの AI 支援開発ワークフローに組み込み、即座に高品質なコードレビューフィードバックを取得できます。',
|
||||
'usecases.case2Title': 'プラットフォームチーム',
|
||||
'usecases.case2Desc': '内部システムにシームレスに統合し、データフローとレビューポリシーを完全に制御できます。',
|
||||
'usecases.case3Title': 'ML研究者',
|
||||
'usecases.case3Desc': 'RL トレーニングパイプラインのコード品質検証器として使用し、コード生成モデルに信頼性の高い報酬信号を提供します。',
|
||||
|
||||
// Features
|
||||
'features.sectionBadge': 'コア機能',
|
||||
'features.title': 'コードレビュー専用に構築された Agent システム',
|
||||
'features.subtitle': 'ファイルの読み取り、コードベースの検索、コンテキストの相互参照、きめ細かいルールの適用による深いレビューを行う AI Agent。',
|
||||
'features.feat1Title': 'ハイブリッドアーキテクチャ:確定性 + Agent',
|
||||
'features.feat1Desc': '確定性エンジニアリング(タスク分割、ファイルフィルタリング、行番号位置決め、ルールルーティング、非同期スケジューリング)と LLM Agent 推論(リスク検出、コンテキスト探索、問題分類)を分離し、品質・トークンコスト・速度を最適化します。',
|
||||
'features.feat2Title': '精密なコメント配置とリフレクション',
|
||||
'features.feat2Desc': '独立した行レベルのコメント配置モジュールと3段階の漸進的 LLM 戦略により、各コメントを正確な行番号に配置。独立したリフレクションモジュールが幻覚や知識のドリフトを早期に検出します。',
|
||||
'features.feat3Title': 'マルチモデルプロトコルサポート',
|
||||
'features.feat3Desc': 'Anthropic Messages API と OpenAI Chat Completions API の両方をサポート。Anthropic、OpenAI、DashScope、DeepSeek、Z.AI などのプリセットプロバイダーを内蔵し、カスタムモデルエンドポイントにも対応。',
|
||||
'features.feat4Title': '動的並行処理',
|
||||
'features.feat4Desc': '設定可能な goroutine ワーカー(デフォルト8)でサブタスクを動的に分割し並列レビュー。大規模な変更セットでも迅速に完了します。',
|
||||
'features.feat5Title': 'スマートメモリ圧縮',
|
||||
'features.feat5Desc': 'コードレビュー専用のメモリ圧縮方式。3段階パーティション(凍結/圧縮/アクティブ)コンテキスト管理により、トークン制限を突破して深いレビューを実現。',
|
||||
'features.feat6Title': '組み込みレビュールール',
|
||||
'features.feat6Desc': '大規模な実環境で検証されたレビュールール。Java、TypeScript、Go、Python、Kotlin、Rust、C++、C など10以上の言語をカバーし、NPE、スレッドセーフティ、XSS、SQLインジェクションなどの専門ルールを搭載。',
|
||||
|
||||
// Benchmark
|
||||
'benchmark.sectionLabel': 'オープンベンチマーク',
|
||||
'benchmark.title': '80名以上のシニアエンジニアによるクロスバリデーション',
|
||||
'benchmark.subtitle': '50の人気オープンソースリポジトリから選定した200の実PullRequestを含む、10のプログラミング言語、多様な問題タイプ、様々な変更規模をカバーする実世界のCodeReviewベンチマーク。',
|
||||
'benchmark.colRank': 'ランク',
|
||||
'benchmark.colModel': 'モデル',
|
||||
'benchmark.colSource': 'ソース',
|
||||
'benchmark.colF1': 'F1',
|
||||
'benchmark.colPrecision': '精度',
|
||||
'benchmark.colRecall': '再現率',
|
||||
'benchmark.colAvgTime': '平均時間',
|
||||
'benchmark.colAvgToken': '平均トークン',
|
||||
|
||||
// QuickStart
|
||||
'quickstart.sectionLabel': 'クイックスタート',
|
||||
'quickstart.title': '数分で使い始められます',
|
||||
'quickstart.subtitle': '複雑な設定は不要。選択した LLM プロバイダー以外のサードパーティアカウントは必要ありません。',
|
||||
'quickstart.step1Title': 'インストール',
|
||||
'quickstart.step1Desc': 'npm でワンコマンドグローバルインストール。',
|
||||
'quickstart.step1Label1': 'インストール',
|
||||
'quickstart.step1Label2': 'インストール確認',
|
||||
'quickstart.step2Title': '設定',
|
||||
'quickstart.step2Desc': 'LLM プロバイダーを対話的にセットアップ、または手動で設定。',
|
||||
'quickstart.step2Label1': '対話式セットアップ(推奨)',
|
||||
'quickstart.step2Label2': '手動設定(代替)',
|
||||
'quickstart.step2Label3': '設定確認',
|
||||
'quickstart.step3Title': 'レビュー実行',
|
||||
'quickstart.step3Desc': '最初のコードレビューを開始します。',
|
||||
'quickstart.step3Label1': 'レビュー実行',
|
||||
'quickstart.commentReview': '# 現在の変更をレビュー',
|
||||
'quickstart.commentBranch': '# ブランチ間の差分をレビュー',
|
||||
'quickstart.commentCommit': '# 特定のコミットをレビュー',
|
||||
'quickstart.copied': 'コピーしました!',
|
||||
|
||||
// Footer
|
||||
'footer.brand': 'Open Code Review',
|
||||
'footer.copyright': '© Copyright 2026. All rights reserved.',
|
||||
|
||||
// Docs Page
|
||||
'docs.toc': '目次',
|
||||
'docs.overview': '概要',
|
||||
'docs.install': 'インストール',
|
||||
'docs.config': 'ocr config',
|
||||
'docs.review': 'ocr review',
|
||||
'docs.viewer': 'ocr viewer',
|
||||
'docs.env': 'Claude Code 統合',
|
||||
'docs.overviewTitle': '概要',
|
||||
'docs.overviewDesc': '(略称 <code>ocr</code>)は AI 駆動のコードレビュー CLI ツールです。',
|
||||
'docs.overviewFeatures': 'コア機能:',
|
||||
'docs.overviewFeat1': 'ワークスペース変更、ブランチ差分、単一コミットレビューモードをサポート',
|
||||
'docs.overviewFeat2': 'Anthropic および OpenAI 互換プロトコルの LLM サービスをサポート',
|
||||
'docs.overviewFeat3': 'カスタマイズ可能なタイムアウト付き並行レビュー',
|
||||
'docs.overviewFeat4': '出力形式は text と json をサポート',
|
||||
'docs.overviewFeat5': 'Claude Code ユーザーはゼロ設定',
|
||||
'docs.overviewFeat6': 'WebUI ビューアーでレビュー結果を可視化',
|
||||
'docs.installTitle': 'インストール',
|
||||
'docs.installLabel': 'インストール',
|
||||
'docs.installVerifyLabel': 'インストール確認',
|
||||
'docs.configTitle': '設定と検証',
|
||||
'docs.configDesc': 'CLI 設定を管理します。<code>~/.opencodereview/config.json</code> に保存されます。',
|
||||
'docs.configInteractive': '対話式セットアップ(推奨)',
|
||||
'docs.configInteractiveDesc': '最も簡単な設定方法。内蔵プロバイダーから選択するか、カスタムプロバイダーを追加します。',
|
||||
'docs.configModelSelect': 'モデル選択',
|
||||
'docs.configModelSelectDesc': 'プロバイダーを選択した後、使用するモデルを選びます。',
|
||||
'docs.configListProviders': '内蔵プロバイダー一覧',
|
||||
'docs.configListProvidersDesc': '利用可能なすべての内蔵 LLM プロバイダーを表示します。',
|
||||
'docs.configManual': '手動設定',
|
||||
'docs.configCommand': 'コマンド',
|
||||
'docs.configExample': '例',
|
||||
'docs.configKeys': 'サポートされる設定キー',
|
||||
'docs.configKeyUrl': 'API アドレス',
|
||||
'docs.configKeyToken': 'API キー',
|
||||
'docs.configKeyModel': 'モデル名',
|
||||
'docs.configKeyAnthropic': 'Anthropic SDK を使用',
|
||||
'docs.configKeyExtraBody': 'ベンダー固有のリクエストボディフィールド (JSON)',
|
||||
'docs.configKeyLanguage': '出力言語',
|
||||
'docs.configKeyTelemetry': 'テレメトリ',
|
||||
'docs.configVerify': '設定確認',
|
||||
'docs.configVerifyDesc': '設定後にこのコマンドを実行して LLM 接続をテストし、設定が正しいことを確認します。',
|
||||
'docs.reviewTitle': 'ocr review',
|
||||
'docs.reviewDesc': '<code>ocr review</code> は AI コードレビューを開始するためのコアコマンドです。',
|
||||
'docs.reviewModes': 'レビューモード',
|
||||
'docs.reviewWorkspace': 'ワークスペースモード(デフォルト)',
|
||||
'docs.reviewWorkspaceDesc': '現在のワークスペースのステージ済み + 未ステージ + 未追跡の変更をレビューします。',
|
||||
'docs.reviewBranch': 'ブランチ差分モード',
|
||||
'docs.reviewBranchDesc': '2つの参照間の差分をレビューします(merge-base ベース)。',
|
||||
'docs.reviewCommit': '単一コミットモード',
|
||||
'docs.reviewCommitDesc': '特定のコミットをレビューします(親コミットとの差分)。',
|
||||
'docs.reviewAdvanced': '高度な使い方',
|
||||
'docs.reviewBackground': '要件コンテキスト付きレビュー',
|
||||
'docs.reviewBackgroundDesc': '要件コンテキスト(重要な境界条件を含む)を提供してレビュー品質を向上させます。',
|
||||
'docs.reviewJson': 'JSON 出力',
|
||||
'docs.reviewJsonDesc': '構造化 JSON 形式で結果を出力し、プログラム処理に対応します。',
|
||||
'docs.reviewAgent': 'Agent モード',
|
||||
'docs.reviewAgentDesc': '実行プロセスを表示せず最終サマリーのみ出力。自動化パイプラインに適しています。',
|
||||
'docs.reviewFlags': 'フラグリファレンス',
|
||||
'docs.reviewFlagCol1': 'フラグ',
|
||||
'docs.reviewFlagCol2': '説明',
|
||||
'docs.reviewFlagCol3': 'デフォルト',
|
||||
'docs.reviewFlag1Desc': 'レビューするコミットハッシュを指定',
|
||||
'docs.reviewFlag2Desc': '開始参照(差分の起点)',
|
||||
'docs.reviewFlag3Desc': 'ターゲット参照(差分の終点)',
|
||||
'docs.reviewFlag4Desc': '出力形式:text または json',
|
||||
'docs.reviewFlag5Desc': 'Git リポジトリルート',
|
||||
'docs.reviewFlag5Default': '現在のディレクトリ',
|
||||
'docs.reviewFlag6Desc': 'レビュールール JSON ファイルパス',
|
||||
'docs.reviewFlag6Default': '内蔵',
|
||||
'docs.reviewFlag7Desc': '最大並行ファイルレビュー数',
|
||||
'docs.reviewFlag8Desc': '単一タスクタイムアウト(分)',
|
||||
'docs.reviewFlag9Desc': '出力対象:human(プロセス表示)または agent(サマリーのみ)',
|
||||
'docs.reviewFlag10Desc': 'ファイルあたりの最大ツール呼び出しラウンド数',
|
||||
'docs.reviewFlag10Default': '内蔵',
|
||||
'docs.reviewFlag11Desc': '最大並行 git サブプロセス数',
|
||||
'docs.reviewNote': '注意:<code>--from/--to</code> と <code>--commit</code> は同時に使用できません。<code>--from</code> を指定する場合、<code>--to</code> も指定する必要があります。',
|
||||
'docs.viewerTitle': 'ocr viewer',
|
||||
'docs.viewerDesc': 'WebUI セッションビューアーを起動し、Web インターフェースでレビュー結果を閲覧します。',
|
||||
'docs.viewerNote': '実行後、ローカル HTTP サーバーが起動し、レビュー結果を閲覧するためのビジュアルインターフェースを提供します。',
|
||||
'docs.envTitle': 'Claude Code 統合',
|
||||
'docs.envDesc': 'すでに Claude Code ユーザーで以下の環境変数を設定済みの場合、Open Code Review は自動的に認識します。追加設定は不要です:',
|
||||
'docs.envNote': '<code>ocr config</code> を使用してこれらの設定を上書きまたは補完することもできます。',
|
||||
'docs.copy': 'コピー',
|
||||
};
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
export type Language = 'en' | 'zh';
|
||||
export type Language = 'en' | 'zh' | 'ja';
|
||||
|
||||
export type TranslationKeys = Record<string, string>;
|
||||
|
|
|
|||
|
|
@ -2,106 +2,101 @@ import { TranslationKeys } from './types';
|
|||
|
||||
export const zh: TranslationKeys = {
|
||||
// Navbar
|
||||
'navbar.features': '核心功能',
|
||||
'navbar.benchmark': '性能基准',
|
||||
'navbar.features': '核心特性',
|
||||
'navbar.benchmark': '排行榜',
|
||||
'navbar.quickstart': '快速开始',
|
||||
'navbar.docs': '使用文档',
|
||||
'navbar.getStarted': '立即开始',
|
||||
'navbar.docs': '文档',
|
||||
'navbar.blog': '博客',
|
||||
'navbar.getStarted': '开始使用',
|
||||
|
||||
// Hero
|
||||
'hero.title': 'AI 代码评审',
|
||||
'hero.titleHighlight': '数百万真实场景任务的验证',
|
||||
'hero.description': 'Open Code Review 将阿里巴巴经大规模生产验证的代码评审 Agent 带入你的工作流。接入任意 LLM,数据完全私有,获得开发者真正愿意采纳的评审意见。',
|
||||
'hero.pill1': '采纳率 > 30%',
|
||||
'hero.pill2': '数据本地闭环',
|
||||
'hero.pill3': 'Token 消耗仅 1/9',
|
||||
'hero.cta1': '快速开始',
|
||||
'hero.cta2': '查看基准测试',
|
||||
'hero.users': '2 万+ 活跃用户',
|
||||
'hero.openSource': '完全开源',
|
||||
'hero.terminal': 'ocr 终端',
|
||||
'hero.badgeLabel': '基准榜第 #1',
|
||||
'hero.title': 'AI 代码审查\n经百万真实任务验证',
|
||||
'hero.description': 'Open Code Review 将阿里巴巴经过实战检验的代码审查 Agent 引入您的工作流程。连接任意 LLM,数据完全私有,获得开发者真正采纳的审查意见。',
|
||||
'hero.quickStart': '快速开始',
|
||||
'hero.learnMore': '了解更多',
|
||||
'hero.terminal': '终端',
|
||||
|
||||
// Highlights
|
||||
'highlights.stat1Label': '活跃用户',
|
||||
'highlights.stat1Value': '20K+',
|
||||
'highlights.stat1Caption': '阿里集团内部生产验证',
|
||||
'highlights.stat2Label': '真实场景',
|
||||
'highlights.stat2Value': '1M+',
|
||||
'highlights.stat2Caption': '累计运行代码评审任务',
|
||||
'highlights.stat3Label': 'Token 效率',
|
||||
'highlights.stat3Value': '1/9',
|
||||
'highlights.stat3Caption': '相比 Claude Code · 基于 1,000 次 PR 评审',
|
||||
'highlights.stat4Label': 'AACR-Bench',
|
||||
'highlights.stat4Value': '25.1%',
|
||||
'highlights.stat4Caption': 'SEM.F1 基准测试得分',
|
||||
'highlights.stat1Label': '活跃用户',
|
||||
'highlights.stat1Caption': '经阿里巴巴集团内部实战验证',
|
||||
'highlights.stat2Value': '> 30%',
|
||||
'highlights.stat2Label': '采纳率',
|
||||
'highlights.stat2Caption': '经阿里巴巴集团内部实战验证',
|
||||
'highlights.stat3Value': '1M+',
|
||||
'highlights.stat3Label': '真实任务',
|
||||
'highlights.stat3Caption': '已执行的代码审查任务',
|
||||
'highlights.stat4Value': '1/9',
|
||||
'highlights.stat4Label': 'TOKEN 成本',
|
||||
'highlights.stat4Caption': '对比 Claude Code · 1,000 个 PR',
|
||||
'highlights.stat5Value': '25.10%',
|
||||
'highlights.stat5Label': 'AACR-BENCH',
|
||||
'highlights.stat5Caption': 'SEM.F1 基准得分',
|
||||
|
||||
// Why Section
|
||||
'why.sectionLabel': '适用场景',
|
||||
'why.title': 'Open Code Review 适合谁?',
|
||||
'why.case1Title': '个人开发者',
|
||||
'why.case1Desc': '将 Open Code Review 嵌入本地 AI 辅助研发工作流,获得即时、高质量的代码评审反馈。',
|
||||
'why.case2Title': '平台团队',
|
||||
'why.case2Desc': '无缝集成至您的内部系统,完全掌控数据流向与评审策略。',
|
||||
'why.case3Title': '模型训练研究者',
|
||||
'why.case3Desc': '作为强化学习训练流水线中的代码质量验证器,为代码生成模型提供可靠奖励信号。',
|
||||
// Use Cases
|
||||
'usecases.sectionLabel': '使用场景',
|
||||
'usecases.title': 'Open Code Review 适合谁?',
|
||||
'usecases.case1Title': '独立开发者',
|
||||
'usecases.case1Desc': '将 Open Code Review 嵌入你的本地 AI 辅助开发工作流,获得即时、高质量的代码审查反馈。',
|
||||
'usecases.case2Title': '平台团队',
|
||||
'usecases.case2Desc': '无缝集成到内部系统中,完全掌控数据流和审查策略。',
|
||||
'usecases.case3Title': 'ML 研究者',
|
||||
'usecases.case3Desc': '作为 RL 训练流水线中的代码质量验证器,为代码生成模型提供可靠的奖励信号。',
|
||||
|
||||
// Features Section
|
||||
// Features
|
||||
'features.sectionBadge': '核心特性',
|
||||
'features.title': '针对代码评审场景深度定制的 Agent 系统',
|
||||
'features.subtitle': '一个能读取文件、搜索代码库、交叉引用上下文、细粒度配置规则的 AI Agent,提供深度评审。',
|
||||
'features.feat1Title': '确定性工程与 Agent 协同的混合架构',
|
||||
'features.feat1Desc': '将确定性工程流程(任务的拆解与分发、文件过滤、行号定位、规则路由、异步调度等)与 LLM Agent 自主决策能力(风险识别、上下文探索、问题定性等)进行分层解耦与协同——让可确定化的环节由工程模块精确处理,需要语义理解的环节由 Agent 动态推理,从而在审查质量、Token 消耗与响应速度三个维度上达到更优。',
|
||||
'features.feat2Title': '精确的评论定位与反思',
|
||||
'features.feat2Desc': '独立的行级评论定位模块,基于三级递进策略的 LLM 评论精确定位机制,将每条评审意见都精确到具体行号,结构化输出让修复更高效;独立的评论反思模块,提前拦截模型发生的知识遗忘或逻辑幻觉等。',
|
||||
'features.title': '专为代码审查打造的 Agent 系统',
|
||||
'features.subtitle': '一个能读取文件、搜索代码库、交叉引用上下文并应用细粒度规则进行深度审查的 AI Agent。',
|
||||
'features.feat1Title': '混合架构:确定性 + Agent',
|
||||
'features.feat1Desc': '将确定性工程(任务拆分、文件过滤、行号定位、规则路由、异步调度)与 LLM Agent 推理(风险检测、上下文探索、问题分类)解耦——工程模块处理确定性,Agent 处理语义,在质量、Token 成本和速度之间取得最优平衡。',
|
||||
'features.feat2Title': '精准评论定位与反思',
|
||||
'features.feat2Desc': '独立的行级评论定位模块,采用三级渐进式 LLM 策略精确定位到具体行号。独立的反思模块提前拦截幻觉和知识偏移。',
|
||||
'features.feat3Title': '多模型协议支持',
|
||||
'features.feat3Desc': '同时支持 Anthropic Messages API 和 OpenAI Chat Completions API。内置 Anthropic、OpenAI、DashScope、DeepSeek、Z.AI 等预设供应商,开箱即用;同时支持接入任意自定义模型端点,灵活适配私有化部署场景。',
|
||||
'features.feat3Desc': '同时支持 Anthropic Messages API 和 OpenAI Chat Completions API。内置 Anthropic、OpenAI、DashScope、DeepSeek、Z.AI 等预设提供商,开箱即用,同时支持自定义模型端点用于私有化部署。',
|
||||
'features.feat4Title': '动态并发处理',
|
||||
'features.feat4Desc': '动态拆分子任务并行评审,可配置 goroutine worker 数量(默认 8 个)。大变更集也能快速完成。',
|
||||
'features.feat4Desc': '动态拆分子任务进行并行审查,支持可配置的 goroutine 工作线程(默认 8 个)。即使是大型变更集也能快速完成。',
|
||||
'features.feat5Title': '智能记忆压缩',
|
||||
'features.feat5Desc': '面向代码评审场景的智能记忆压缩,三层分区(frozen/compress/active)上下文管理,突破 token 限制,确保深度评审。',
|
||||
'features.feat6Title': '内置评审规则',
|
||||
'features.feat6Desc': '经过线上海量真实场景验证迭代而来的评审规则,覆盖 Java、TypeScript、Go、Python、Kotlin、Rust、C++、C 等 10+ 语言,专项规则涵盖 NPE、线程安全、XSS、SQL 注入等常见风险类型。',
|
||||
'features.feat5Desc': '专为代码审查构建的记忆压缩方案,采用三级分区(冻结/压缩/活跃)上下文管理,突破 Token 限制实现深度审查。',
|
||||
'features.feat6Title': '内置审查规则',
|
||||
'features.feat6Desc': '经海量真实场景验证的审查规则,覆盖 Java、TypeScript、Go、Python、Kotlin、Rust、C++、C 等 10+ 种语言,包含 NPE、线程安全、XSS、SQL 注入等专项规则。',
|
||||
|
||||
// Benchmark Section
|
||||
'benchmark.sectionLabel': '开放基准测试',
|
||||
'benchmark.title': '80+ 位资深工程师交叉标注验证',
|
||||
'benchmark.subtitlePreRepos': '基于真实场景的 CodeReview 基准测试,从 ',
|
||||
'benchmark.subtitlePrePRs': ' 个热门开源仓库中精选 ',
|
||||
'benchmark.subtitlePreLangs': ' 个真实的 PullRequest,覆盖 ',
|
||||
'benchmark.subtitleEnd': ' 种编程语言、多种问题类型与不同的变更规模。',
|
||||
'benchmark.legendOcr': 'Open Code Review',
|
||||
'benchmark.legendCc': 'Claude Code · /code-review',
|
||||
// Benchmark
|
||||
'benchmark.sectionLabel': '公开基准测试',
|
||||
'benchmark.title': '80+ 位资深工程师交叉验证',
|
||||
'benchmark.subtitle': '一个真实世界的 CodeReview 基准测试,从 50 个热门开源仓库中精选,包含 200 个真实 Pull Request,覆盖 10 种编程语言、多样的问题类型和不同规模的变更集。',
|
||||
'benchmark.colRank': '排名',
|
||||
'benchmark.colModel': '模型',
|
||||
'benchmark.colSource': '来源',
|
||||
'benchmark.colVersion': '版本',
|
||||
'benchmark.colPrecision': '准确率',
|
||||
'benchmark.colF1': 'F1',
|
||||
'benchmark.colPrecision': '精确率',
|
||||
'benchmark.colRecall': '召回率',
|
||||
'benchmark.colAvgTime': '平均耗时',
|
||||
'benchmark.colAvgToken': '平均 Token',
|
||||
'benchmark.tooltipInput': '输入 Token',
|
||||
'benchmark.tooltipOutput': '输出 Token',
|
||||
'benchmark.footer': '',
|
||||
|
||||
// QuickStart Section
|
||||
// QuickStart
|
||||
'quickstart.sectionLabel': '快速开始',
|
||||
'quickstart.title': '几分钟内跑起来',
|
||||
'quickstart.subtitle': '无需复杂配置,除所选 LLM 服务商外无需任何第三方账号。',
|
||||
'quickstart.title': '几分钟即可上手',
|
||||
'quickstart.subtitle': '无需复杂配置,除了您选择的 LLM 提供商外不需要第三方账户。',
|
||||
'quickstart.step1Title': '安装',
|
||||
'quickstart.step1Desc': '通过 npm 一键全局安装。',
|
||||
'quickstart.step1Desc': '通过 npm 一行命令全局安装。',
|
||||
'quickstart.step1Label1': '安装',
|
||||
'quickstart.step1Label2': '验证安装',
|
||||
'quickstart.step2Title': '配置',
|
||||
'quickstart.step2Desc': '通过交互式向导设置 LLM 供应商,或手动配置。内置支持 Anthropic、OpenAI、DashScope、DeepSeek 和 Z.AI。',
|
||||
'quickstart.step2Desc': '交互式设置 LLM 提供商,或手动配置。',
|
||||
'quickstart.step2Label1': '交互式设置(推荐)',
|
||||
'quickstart.step2Label2': '手动配置(备选)',
|
||||
'quickstart.step2Label3': '验证配置',
|
||||
'quickstart.step3Title': '运行评审',
|
||||
'quickstart.step3Desc': '开始你的第一次代码评审。',
|
||||
'quickstart.zeroCfgTitle': '已在使用 Claude Code?零额外配置',
|
||||
'quickstart.zeroCfgDesc': '如果你已经配置了 Claude Code 的环境变量(ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_MODEL),Open Code Review 会自动读取这些配置,无需任何额外设置即可直接使用。',
|
||||
'quickstart.copy': '复制',
|
||||
'quickstart.step3Title': '运行审查',
|
||||
'quickstart.step3Desc': '开始你的第一次代码审查。',
|
||||
'quickstart.step3Label1': '运行审查命令',
|
||||
'quickstart.commentReview': '# 审查当前变更',
|
||||
'quickstart.commentBranch': '# 审查分支间的差异',
|
||||
'quickstart.commentCommit': '# 审查特定提交',
|
||||
'quickstart.copied': '已复制!',
|
||||
|
||||
// Footer
|
||||
'footer.brand': 'Open Code Review',
|
||||
'footer.copyright': '© 版权所有 2026。保留所有权利。',
|
||||
|
||||
// Docs Page
|
||||
'docs.toc': '目录',
|
||||
|
|
@ -110,92 +105,80 @@ export const zh: TranslationKeys = {
|
|||
'docs.config': 'ocr config',
|
||||
'docs.review': 'ocr review',
|
||||
'docs.viewer': 'ocr viewer',
|
||||
'docs.env': '适配 Claude Code',
|
||||
|
||||
'docs.env': 'Claude Code 集成',
|
||||
'docs.overviewTitle': '概览',
|
||||
'docs.overviewDesc': '(简称 <code>ocr</code>)是一个 AI 驱动的代码评审 CLI 工具。',
|
||||
'docs.overviewFeatures': '核心特性:',
|
||||
'docs.overviewFeat1': '支持工作区变更、分支对比、单提交三种评审模式',
|
||||
'docs.overviewDesc': '(简称 <code>ocr</code>)是一款 AI 驱动的代码审查 CLI 工具。',
|
||||
'docs.overviewFeatures': '核心功能:',
|
||||
'docs.overviewFeat1': '支持工作区变更、分支差异和单次提交审查模式',
|
||||
'docs.overviewFeat2': '支持 Anthropic 和 OpenAI 兼容协议的 LLM 服务',
|
||||
'docs.overviewFeat3': '并发评审,可自定义超时时间',
|
||||
'docs.overviewFeat3': '并发审查与可自定义超时',
|
||||
'docs.overviewFeat4': '输出格式支持 text 和 json',
|
||||
'docs.overviewFeat5': 'Claude Code 用户零额外配置',
|
||||
'docs.overviewFeat6': 'WebUI 查看器可视化评审结果',
|
||||
|
||||
'docs.overviewFeat5': 'Claude Code 用户零配置',
|
||||
'docs.overviewFeat6': 'WebUI 查看器可视化审查结果',
|
||||
'docs.installTitle': '安装',
|
||||
'docs.installLabel': '安装',
|
||||
'docs.installVerifyLabel': '验证安装',
|
||||
|
||||
'docs.configTitle': '配置与验证',
|
||||
'docs.configDesc': '管理 CLI 的配置设置,存储在 <code>~/.opencodereview/config.json</code>。',
|
||||
'docs.configDesc': '管理 CLI 配置,存储在 <code>~/.opencodereview/config.json</code>。',
|
||||
'docs.configInteractive': '交互式设置(推荐)',
|
||||
'docs.configInteractiveDesc': '最简单的配置方式。从内置供应商中选择,或添加自定义供应商。',
|
||||
'docs.configInteractiveDesc': '最简单的配置方式。从内置提供商中选择或添加自定义提供商。',
|
||||
'docs.configModelSelect': '模型选择',
|
||||
'docs.configModelSelectDesc': '选择供应商后,选择要使用的模型。',
|
||||
'docs.configListProviders': '查看内置供应商',
|
||||
'docs.configListProvidersDesc': '查看所有可用的内置 LLM 供应商。',
|
||||
'docs.configModelSelectDesc': '选择提供商后,选择要使用的模型。',
|
||||
'docs.configListProviders': '列出内置提供商',
|
||||
'docs.configListProvidersDesc': '查看所有可用的内置 LLM 提供商。',
|
||||
'docs.configManual': '手动配置',
|
||||
'docs.configCommand': '命令',
|
||||
'docs.configExample': '示例',
|
||||
'docs.configKeys': '支持的配置 Key',
|
||||
'docs.configKeys': '支持的配置键',
|
||||
'docs.configKeyUrl': 'API 地址',
|
||||
'docs.configKeyToken': 'API 密钥',
|
||||
'docs.configKeyModel': '模型名称',
|
||||
'docs.configKeyAnthropic': '是否使用 Anthropic SDK',
|
||||
'docs.configKeyExtraBody': '厂商特定的请求体字段(JSON)',
|
||||
'docs.configKeyAnthropic': '使用 Anthropic SDK',
|
||||
'docs.configKeyExtraBody': '供应商特定的请求体字段 (JSON)',
|
||||
'docs.configKeyLanguage': '输出语言',
|
||||
'docs.configKeyTelemetry': '数据上报',
|
||||
'docs.configKeyTelemetry': '遥测',
|
||||
'docs.configVerify': '验证配置',
|
||||
'docs.configVerifyDesc': '完成配置后运行此命令,工具会根据当前配置尝试连接指定的 LLM 服务,帮助你确认配置是否正确。',
|
||||
|
||||
'docs.configVerifyDesc': '配置后运行此命令以测试 LLM 连接并确认设置正确。',
|
||||
'docs.reviewTitle': 'ocr review',
|
||||
'docs.reviewDesc': '<code>ocr review</code> 是核心命令,用于发起 AI 代码评审。',
|
||||
'docs.reviewModes': '评审模式',
|
||||
'docs.reviewDesc': '<code>ocr review</code> 是发起 AI 代码审查的核心命令。',
|
||||
'docs.reviewModes': '审查模式',
|
||||
'docs.reviewWorkspace': '工作区模式(默认)',
|
||||
'docs.reviewWorkspaceDesc': '评审当前工作区的暂存 + 未暂存 + 未跟踪的变更。',
|
||||
'docs.reviewBranch': '分支对比模式',
|
||||
'docs.reviewBranchDesc': '评审两个引用之间的差异(基于 merge-base)。',
|
||||
'docs.reviewCommit': '单提交模式',
|
||||
'docs.reviewCommitDesc': '评审指定的提交(与其父提交的差异)。',
|
||||
'docs.reviewWorkspaceDesc': '审查当前工作区中已暂存 + 未暂存 + 未跟踪的变更。',
|
||||
'docs.reviewBranch': '分支差异模式',
|
||||
'docs.reviewBranchDesc': '审查两个引用之间的差异(基于 merge-base)。',
|
||||
'docs.reviewCommit': '单次提交模式',
|
||||
'docs.reviewCommitDesc': '审查特定提交(与父提交的差异)。',
|
||||
'docs.reviewAdvanced': '高级用法',
|
||||
'docs.reviewBackground': '结合需求背景进行评审',
|
||||
'docs.reviewBackgroundDesc': '传入本次代码改动的需求背景(包括重要边界),提高代码评审效果。',
|
||||
'docs.reviewJson': 'JSON 格式输出',
|
||||
'docs.reviewJsonDesc': '以结构化 JSON 格式输出评审结果,便于后续程序化处理。',
|
||||
'docs.reviewBackground': '带需求上下文的审查',
|
||||
'docs.reviewBackgroundDesc': '提供需求上下文(包括重要边界条件)以提高审查质量。',
|
||||
'docs.reviewJson': 'JSON 输出',
|
||||
'docs.reviewJsonDesc': '以结构化 JSON 格式输出结果,便于程序化处理。',
|
||||
'docs.reviewAgent': 'Agent 模式',
|
||||
'docs.reviewAgentDesc': '仅输出最终摘要,不显示执行过程,适合作为自动化流程的一部分使用。',
|
||||
'docs.reviewFlags': 'Flag 参考',
|
||||
'docs.reviewFlagCol1': 'Flag',
|
||||
'docs.reviewFlagCol2': '说明',
|
||||
'docs.reviewAgentDesc': '仅输出最终摘要,不显示执行过程,适用于自动化流水线。',
|
||||
'docs.reviewFlags': '参数参考',
|
||||
'docs.reviewFlagCol1': '参数',
|
||||
'docs.reviewFlagCol2': '描述',
|
||||
'docs.reviewFlagCol3': '默认值',
|
||||
'docs.reviewFlag1Desc': '指定提交哈希进行评审',
|
||||
'docs.reviewFlag2Desc': '起始引用(diff 起点)',
|
||||
'docs.reviewFlag3Desc': '目标引用(diff 终点)',
|
||||
'docs.reviewFlag1Desc': '指定要审查的提交哈希',
|
||||
'docs.reviewFlag2Desc': '起始引用(差异起点)',
|
||||
'docs.reviewFlag3Desc': '目标引用(差异终点)',
|
||||
'docs.reviewFlag4Desc': '输出格式:text 或 json',
|
||||
'docs.reviewFlag5Desc': 'Git 仓库根目录',
|
||||
'docs.reviewFlag5Default': '当前目录',
|
||||
'docs.reviewFlag6Desc': '评审规则的 JSON 文件路径',
|
||||
'docs.reviewFlag6Desc': '审查规则 JSON 文件路径',
|
||||
'docs.reviewFlag6Default': '内置',
|
||||
'docs.reviewFlag7Desc': '最大并发评审文件数',
|
||||
'docs.reviewFlag8Desc': '单个并发任务超时时间(分钟)',
|
||||
'docs.reviewFlag7Desc': '最大并发文件审查数',
|
||||
'docs.reviewFlag8Desc': '单任务超时时间(分钟)',
|
||||
'docs.reviewFlag9Desc': '输出受众:human(显示过程)或 agent(仅摘要)',
|
||||
'docs.reviewFlag10Desc': '每个文件的最大工具调用轮次;仅在大于模板默认值时生效',
|
||||
'docs.reviewFlag10Desc': '每个文件的最大工具调用轮数;仅当大于模板默认值时生效',
|
||||
'docs.reviewFlag10Default': '内置',
|
||||
'docs.reviewFlag11Desc': '最大并发 git 子进程数',
|
||||
'docs.reviewNote': '注意:<code>--from/--to</code> 和 <code>--commit</code> 不可同时使用。指定 <code>--from</code> 时必须同时指定 <code>--to</code>。',
|
||||
|
||||
'docs.reviewNote': '注意:<code>--from/--to</code> 和 <code>--commit</code> 不能同时使用。指定 <code>--from</code> 时,<code>--to</code> 也必须指定。',
|
||||
'docs.viewerTitle': 'ocr viewer',
|
||||
'docs.viewerDesc': '启动 WebUI 会话查看器,用于以网页形式浏览评审结果。',
|
||||
'docs.viewerNote': '运行后会在本地启动一个 HTTP 服务器,提供可视化的评审结果浏览界面。',
|
||||
|
||||
'docs.envTitle': '适配 Claude Code',
|
||||
'docs.envDesc': '如果你已经是 Claude Code 用户并配置了以下环境变量,Open Code Review 会自动识别,无需任何额外配置:',
|
||||
'docs.envNote': '此外,工具也支持通过 <code>ocr config</code> 命令来覆盖或补充这些配置。',
|
||||
'docs.viewerDesc': '启动 WebUI 会话查看器,在 Web 界面中浏览审查结果。',
|
||||
'docs.viewerNote': '运行后将启动一个本地 HTTP 服务器,提供可视化界面浏览审查结果。',
|
||||
'docs.envTitle': 'Claude Code 集成',
|
||||
'docs.envDesc': '如果您已经是 Claude Code 用户并配置了以下环境变量,Open Code Review 将自动识别——无需额外配置:',
|
||||
'docs.envNote': '您也可以使用 <code>ocr config</code> 覆盖或补充这些设置。',
|
||||
'docs.copy': '复制',
|
||||
|
||||
// QuickStart code comments
|
||||
'quickstart.commentReview': '# 评审当前变更',
|
||||
'quickstart.commentBranch': '# 评审分支间的差异',
|
||||
'quickstart.commentCommit': '# 评审指定提交',
|
||||
'quickstart.commentEnvAuto': '# Open Code Review 自动识别这些变量',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ import App from './App';
|
|||
import { LanguageProvider } from './i18n';
|
||||
import './styles/index.css';
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
const rootElement = document.getElementById('root') as HTMLElement;
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
|
||||
|
|
|
|||
19
pages/src/pages/BenchmarkPage.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import BenchmarkSection from '../components/BenchmarkSection';
|
||||
import Footer from '../components/Footer';
|
||||
import FadeInSection from '../components/FadeInSection';
|
||||
|
||||
const BenchmarkPage: React.FC = () => {
|
||||
return (
|
||||
<div style={{ paddingTop: 72 }}>
|
||||
<FadeInSection>
|
||||
<BenchmarkSection />
|
||||
</FadeInSection>
|
||||
<FadeInSection>
|
||||
<Footer />
|
||||
</FadeInSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BenchmarkPage;
|
||||
|
|
@ -1,6 +1,41 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Navbar from '../components/Navbar';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useTranslation } from '../i18n';
|
||||
import Footer from '../components/Footer';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import copyIcon from '../assets/icons/icon-copy.svg';
|
||||
import docDownloadIcon from '../assets/icons/doc-download.svg';
|
||||
import docCheckCircleIcon from '../assets/icons/doc-check-circle.svg';
|
||||
import docEditIcon from '../assets/icons/doc-edit.svg';
|
||||
import docContentsIcon from '../assets/icons/doc-contents.svg';
|
||||
|
||||
/* Toast - same as QuickStartSection */
|
||||
const Toast: React.FC<{ message: string; visible: boolean }> = ({ message, visible }) =>
|
||||
ReactDOM.createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 88,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
padding: '5px 8px 5px 10px',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
pointerEvents: 'none',
|
||||
opacity: visible ? 1 : 0,
|
||||
transition: 'opacity 0.15s ease',
|
||||
zIndex: 9999,
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
|
|
@ -16,59 +51,81 @@ const sectionDefs: Section[] = [
|
|||
{ id: 'env', labelKey: 'docs.env' },
|
||||
];
|
||||
|
||||
const CodeBlock: React.FC<{ code: string; copied?: boolean; onCopy?: () => void; copyLabel?: string }> = ({ code, copied, onCopy, copyLabel }) => (
|
||||
<div className="relative group/code">
|
||||
<div className="code-block rounded-xl p-4 overflow-x-auto group-hover/code:border-brand-500/30 transition-colors duration-300">
|
||||
<pre className="font-mono text-xs text-brand-400 whitespace-pre">{code}</pre>
|
||||
</div>
|
||||
/* ─── Code block matching reference: black bg, 1px border, rounded 6px, copy icon right ─── */
|
||||
const CodeBlock: React.FC<{ code: string; onCopy?: () => void }> = ({ code, onCopy }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignSelf: 'stretch',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
background: '#000000',
|
||||
borderRadius: 6,
|
||||
padding: '4px 16px',
|
||||
border: '1px solid rgba(255,255,255,0.16)',
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, fontFamily: 'Menlo, Monaco, monospace', fontSize: 13, lineHeight: '24px', color: 'rgba(255,255,255,0.8)', whiteSpace: 'pre-wrap', wordBreak: 'break-all', flex: 1 }}>
|
||||
{code}
|
||||
</pre>
|
||||
{onCopy && (
|
||||
<button
|
||||
<div
|
||||
onClick={onCopy}
|
||||
className="absolute top-2 right-3 text-slate-600 hover:text-brand-400 transition-colors text-xs flex items-center gap-1 opacity-60 group-hover/code:opacity-100"
|
||||
style={{ paddingTop: 4, paddingBottom: 4, marginLeft: 12, cursor: 'pointer', flexShrink: 0 }}
|
||||
>
|
||||
<i className={`fa-solid ${copied ? 'fa-check text-brand-400' : 'fa-copy'}`}></i>
|
||||
{copied ? '' : (copyLabel || '')}
|
||||
</button>
|
||||
<img src={copyIcon} alt="copy" style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const DocSection: React.FC<{ id: string; title: string; children: React.ReactNode }> = ({ id, title, children }) => (
|
||||
<section id={id} className="mb-16 scroll-mt-24">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 pb-2 border-b border-dark-600/30">{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
/* ─── Icon box (32x32, rgba(255,255,255,0.04) bg, rounded 6px) ─── */
|
||||
const IconBox: React.FC<{ icon: string }> = ({ icon }) => (
|
||||
<div style={{ width: 32, height: 32, display: 'flex', flex: 'none', justifyContent: 'center', alignItems: 'center', background: 'rgba(255,255,255,0.04)', borderRadius: 6 }}>
|
||||
<img src={icon} style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const DocsPage: React.FC = () => {
|
||||
const [activeSection, setActiveSection] = useState('overview');
|
||||
const [mobileTocOpen, setMobileTocOpen] = useState(false);
|
||||
const [copiedIndex, setCopiedIndex] = useState<string | null>(null);
|
||||
const [toastVisible, setToastVisible] = useState(false);
|
||||
const lockedRef = useRef<string | null>(null);
|
||||
const unlockTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
const sections = sectionDefs.map(s => ({ ...s, label: t(s.labelKey) }));
|
||||
|
||||
const handleCopy = (code: string, key: string) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
setCopiedIndex(key);
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
const handleCopy = useCallback((text: string) => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setToastVisible(true);
|
||||
}).catch(() => {
|
||||
fallbackCopy(text);
|
||||
});
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = code;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
setCopiedIndex(key);
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
fallbackCopy(text);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fallbackCopy = (text: string) => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const success = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
if (success) setToastVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!toastVisible) return;
|
||||
const timer = setTimeout(() => setToastVisible(false), 1200);
|
||||
return () => clearTimeout(timer);
|
||||
}, [toastVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const THRESHOLD = 160;
|
||||
const handleScroll = () => {
|
||||
|
|
@ -93,363 +150,312 @@ const DocsPage: React.FC = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const unlockTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
lockedRef.current = id;
|
||||
clearTimeout(unlockTimerRef.current);
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' });
|
||||
window.history.pushState(null, '', `#${id}`);
|
||||
}
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
||||
setActiveSection(id);
|
||||
unlockTimerRef.current = setTimeout(() => {
|
||||
lockedRef.current = null;
|
||||
}, 800);
|
||||
unlockTimerRef.current = setTimeout(() => { lockedRef.current = null; }, 800);
|
||||
};
|
||||
|
||||
/* ─── Shared styles ─── */
|
||||
const fontFamily = 'PingFang SC, -apple-system, BlinkMacSystemFont, sans-serif';
|
||||
const sectionTitle: React.CSSProperties = { fontSize: 20, fontWeight: 600, color: '#FFFFFF', margin: '0 0 16px 0', lineHeight: '28px', fontFamily };
|
||||
const subTitle: React.CSSProperties = { fontSize: 15, fontWeight: 600, color: '#FFFFFF', margin: '24px 0 8px 0', lineHeight: '24px', fontFamily };
|
||||
const desc: React.CSSProperties = { fontSize: 14, color: 'rgba(255,255,255,0.6)', lineHeight: '24px', margin: '0 0 12px 0', fontFamily };
|
||||
const sectionSpacing: React.CSSProperties = { marginBottom: 56, display: 'flex', flexDirection: 'column' as const, alignItems: 'stretch' };
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-dark-900 relative noise-overlay pt-16">
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 left-1/4 w-[800px] h-[600px] rounded-full bg-brand-500/[0.02] blur-[120px]"></div>
|
||||
</div>
|
||||
<div style={{ minHeight: '100vh', background: '#000000', paddingTop: 72, fontFamily: 'PingFang SC, -apple-system, BlinkMacSystemFont, sans-serif' }}>
|
||||
|
||||
<Navbar />
|
||||
{/* Main layout: content + right sidebar */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'flex-start', gap: 40, padding: isMobile ? '0 20px' : '0 40px', paddingRight: isMobile ? 20 : 300 }}>
|
||||
{/* Main content area */}
|
||||
<div style={{ display: 'flex', flex: 1, flexShrink: 0, justifyContent: 'center', alignItems: 'flex-start' }}>
|
||||
<div style={{ maxWidth: 1080, display: 'flex', flex: 1, flexDirection: 'column', paddingTop: 40, paddingBottom: 80 }}>
|
||||
{/* Page title "Docs" */}
|
||||
<div style={{ marginBottom: 40 }}>
|
||||
<p style={{ fontSize: 36, fontWeight: 700, color: '#FFFFFF', margin: 0, lineHeight: '44px', fontFamily: 'PingFang SC, -apple-system, BlinkMacSystemFont, sans-serif' }}>{t('navbar.docs')}</p>
|
||||
</div>
|
||||
|
||||
{/* Mobile TOC toggle */}
|
||||
<div className="lg:hidden fixed top-16 right-4 z-50">
|
||||
<button
|
||||
className="text-slate-400 hover:text-white transition-colors text-sm flex items-center gap-2 bg-dark-900/80 backdrop-blur-xl border border-dark-600/30 rounded-lg px-3 py-1.5"
|
||||
onClick={() => setMobileTocOpen(!mobileTocOpen)}
|
||||
>
|
||||
<i className="fa-solid fa-list-ul"></i>
|
||||
{t('docs.toc')}
|
||||
</button>
|
||||
</div>
|
||||
{/* ─── Overview ─── */}
|
||||
<section id="overview" style={{ ...sectionSpacing, scrollMarginTop: 100 }}>
|
||||
<p style={sectionTitle}>{t('docs.overviewTitle')}</p>
|
||||
<p style={desc}>
|
||||
Open Code Review {t('docs.overviewDesc').replace(/<\/?code>/g, '')}
|
||||
</p>
|
||||
<p style={{ ...desc, fontWeight: 500, color: 'rgba(255,255,255,0.8)' }}>
|
||||
{t('docs.overviewFeatures')}
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignSelf: 'stretch', justifyContent: 'flex-start', alignItems: 'center', background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<span style={{ flexShrink: 0, color: '#2BDE5E', fontSize: 12, fontFamily: 'Menlo, monospace', lineHeight: '24px', marginRight: 12 }}>
|
||||
{'✔\n✔\n✔\n✔\n✔\n✔'.split('\n').map((c, i) => <React.Fragment key={i}>{c}<br /></React.Fragment>)}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, color: 'rgba(255,255,255,0.7)', lineHeight: '24px' }}>
|
||||
{t('docs.overviewFeat1')}<br />
|
||||
{t('docs.overviewFeat2')}<br />
|
||||
{t('docs.overviewFeat3')}<br />
|
||||
{t('docs.overviewFeat4')}<br />
|
||||
{t('docs.overviewFeat5')}<br />
|
||||
{t('docs.overviewFeat6')}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mobile TOC dropdown */}
|
||||
{mobileTocOpen && (
|
||||
<div className="lg:hidden fixed inset-0 z-[60] bg-black/60" onClick={() => setMobileTocOpen(false)}>
|
||||
<div
|
||||
className="bg-dark-900 border-r border-dark-600/30 w-64 max-h-full overflow-y-auto pt-16 pb-8 px-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ul className="space-y-1">
|
||||
{sections.map((s) => (
|
||||
<li key={s.id}>
|
||||
<button
|
||||
onClick={() => { scrollToSection(s.id); setMobileTocOpen(false); }}
|
||||
className={`block w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
activeSection === s.id ? 'text-brand-400 bg-brand-500/10 font-medium' : 'text-slate-400 hover:text-white hover:bg-dark-800/50'
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* ─── Install ─── */}
|
||||
<section id="install" style={{ ...sectionSpacing, scrollMarginTop: 100 }}>
|
||||
<p style={sectionTitle}>{t('docs.installTitle')}</p>
|
||||
{/* Install item */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: 16, background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<IconBox icon={docDownloadIcon} />
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#FFFFFF' }}>{t('docs.installLabel')}</span>
|
||||
</div>
|
||||
<CodeBlock code="npm i -g @alibaba-group/open-code-review" onCopy={() => handleCopy('npm i -g @alibaba-group/open-code-review')} />
|
||||
</div>
|
||||
{/* Verify item */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<IconBox icon={docCheckCircleIcon} />
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#FFFFFF' }}>{t('docs.installVerifyLabel')}</span>
|
||||
</div>
|
||||
<CodeBlock code="ocr version" onCopy={() => handleCopy('ocr version')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Configuration & Verification ─── */}
|
||||
<section id="config" style={{ ...sectionSpacing, scrollMarginTop: 100 }}>
|
||||
<p style={sectionTitle}>{t('docs.configTitle')}</p>
|
||||
<p style={desc}>{t('docs.configDesc').replace(/<\/?code>/g, '')}</p>
|
||||
|
||||
<p style={subTitle}>{t('docs.configInteractive')}</p>
|
||||
<p style={desc}>{t('docs.configInteractiveDesc')}</p>
|
||||
<CodeBlock code="ocr config provider" onCopy={() => handleCopy('ocr config provider')} />
|
||||
|
||||
<p style={subTitle}>{t('docs.configModelSelect')}</p>
|
||||
<p style={desc}>{t('docs.configModelSelectDesc')}</p>
|
||||
<CodeBlock code="ocr config model" onCopy={() => handleCopy('ocr config model')} />
|
||||
|
||||
<p style={subTitle}>{t('docs.configListProviders')}</p>
|
||||
<p style={desc}>{t('docs.configListProvidersDesc')}</p>
|
||||
<CodeBlock code="ocr llm providers" onCopy={() => handleCopy('ocr llm providers')} />
|
||||
|
||||
<p style={subTitle}>{t('docs.configManual')}</p>
|
||||
<p style={{ ...desc, fontWeight: 500, color: 'rgba(255,255,255,0.7)' }}>{t('docs.configCommand')}</p>
|
||||
<CodeBlock code={'ocr config set <key> <value>'} />
|
||||
|
||||
<p style={{ ...desc, fontWeight: 500, color: 'rgba(255,255,255,0.7)', marginTop: 16 }}>{t('docs.configExample')}</p>
|
||||
<CodeBlock
|
||||
code={`ocr config set llm.url https://api.anthropic.com \\\n && ocr config set llm.auth_token {{your-api-key}} \\\n && ocr config set llm.model claude-opus-4-6 \\\n && ocr config set llm.use_anthropic true \\\n && ocr config set language Chinese`}
|
||||
onCopy={() => handleCopy(`ocr config set llm.url https://api.anthropic.com \\\n && ocr config set llm.auth_token {{your-api-key}} \\\n && ocr config set llm.model claude-opus-4-6 \\\n && ocr config set llm.use_anthropic true \\\n && ocr config set language Chinese`)}
|
||||
/>
|
||||
|
||||
<p style={subTitle}>{t('docs.configKeys')}</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{/* 2-column grid of config keys */}
|
||||
{[
|
||||
[{ key: 'llm.url', desc: t('docs.configKeyUrl') }, { key: 'llm.auth_token', desc: t('docs.configKeyToken') }],
|
||||
[{ key: 'llm.model', desc: t('docs.configKeyModel') }, { key: 'llm.use_anthropic', desc: t('docs.configKeyAnthropic') }],
|
||||
[{ key: 'telemetry.enabled', desc: t('docs.configKeyTelemetry') }, { key: 'language', desc: t('docs.configKeyLanguage') }],
|
||||
[{ key: 'llm.extra_body', desc: t('docs.configKeyExtraBody') }],
|
||||
].map((row, ri) => (
|
||||
<div key={ri} style={{ display: 'flex', gap: 4, flexWrap: isMobile ? 'wrap' : 'nowrap' }}>
|
||||
{row.map(({ key, desc: d }) => (
|
||||
<div key={key} style={{ display: 'flex', flex: 1, justifyContent: 'space-between', alignItems: 'center', background: '#000000', borderRadius: 6, padding: '4px 16px', border: '1px solid rgba(255,255,255,0.16)', minWidth: isMobile ? '100%' : undefined }}>
|
||||
<p style={{ margin: 0, fontSize: 13, fontFamily: 'Menlo, monospace', color: 'rgba(255,255,255,0.8)' }}>
|
||||
<span style={{ color: '#2BDE5E' }}>{key}</span>
|
||||
<span style={{ color: 'rgba(255,255,255,0.4)', marginLeft: 8 }}>{d}</span>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p style={subTitle}>{t('docs.configVerify')}</p>
|
||||
<CodeBlock
|
||||
code={`# Test LLM connection\nocr llm test`}
|
||||
onCopy={() => handleCopy('ocr llm test')}
|
||||
/>
|
||||
<p style={{ ...desc, marginTop: 12 }}>{t('docs.configVerifyDesc')}</p>
|
||||
</section>
|
||||
|
||||
{/* ─── ocr review ─── */}
|
||||
<section id="review" style={{ ...sectionSpacing, scrollMarginTop: 100 }}>
|
||||
<p style={sectionTitle}>{t('docs.reviewTitle')}</p>
|
||||
<p style={desc}>{t('docs.reviewDesc').replace(/<\/?code>/g, '')}</p>
|
||||
|
||||
<p style={subTitle}>{t('docs.reviewModes')}</p>
|
||||
{/* Workspace Mode */}
|
||||
<div style={{ marginBottom: 12, background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<IconBox icon={docEditIcon} />
|
||||
<div style={{ display: 'flex', flex: 1, flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#FFFFFF' }}>{t('docs.reviewWorkspace')}</span>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'rgba(255,255,255,0.5)', lineHeight: '20px' }}>{t('docs.reviewWorkspaceDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CodeBlock code="ocr review" onCopy={() => handleCopy('ocr review')} />
|
||||
</div>
|
||||
{/* Branch Diff Mode */}
|
||||
<div style={{ marginBottom: 12, background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<IconBox icon={docDownloadIcon} />
|
||||
<div style={{ display: 'flex', flex: 1, flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#FFFFFF' }}>{t('docs.reviewBranch')}</span>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'rgba(255,255,255,0.5)', lineHeight: '20px' }}>{t('docs.reviewBranchDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CodeBlock code="ocr review --from master --to dev-ref" onCopy={() => handleCopy('ocr review --from master --to dev-ref')} />
|
||||
</div>
|
||||
{/* Single Commit Mode */}
|
||||
<div style={{ marginBottom: 12, background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<IconBox icon={docDownloadIcon} />
|
||||
<div style={{ display: 'flex', flex: 1, flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#FFFFFF' }}>{t('docs.reviewCommit')}</span>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'rgba(255,255,255,0.5)', lineHeight: '20px' }}>{t('docs.reviewCommitDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CodeBlock code="ocr review -c abc123" onCopy={() => handleCopy('ocr review -c abc123')} />
|
||||
</div>
|
||||
|
||||
<p style={subTitle}>{t('docs.reviewAdvanced')}</p>
|
||||
{/* Review with Requirement Context */}
|
||||
<div style={{ marginBottom: 12, background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<IconBox icon={docEditIcon} />
|
||||
<div style={{ display: 'flex', flex: 1, flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#FFFFFF' }}>{t('docs.reviewBackground')}</span>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'rgba(255,255,255,0.5)', lineHeight: '20px' }}>{t('docs.reviewBackgroundDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CodeBlock code={`ocr review --background "requirement context"\nocr review -b "requirement context"`} onCopy={() => handleCopy('ocr review --background "requirement context"')} />
|
||||
</div>
|
||||
{/* JSON Output */}
|
||||
<div style={{ marginBottom: 12, background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<IconBox icon={docDownloadIcon} />
|
||||
<div style={{ display: 'flex', flex: 1, flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#FFFFFF' }}>{t('docs.reviewJson')}</span>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'rgba(255,255,255,0.5)', lineHeight: '20px' }}>{t('docs.reviewJsonDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CodeBlock code={`ocr review --format json\nocr review -f json`} onCopy={() => handleCopy('ocr review --format json')} />
|
||||
</div>
|
||||
{/* Agent Mode */}
|
||||
<div style={{ marginBottom: 12, background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: 16, border: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<IconBox icon={docDownloadIcon} />
|
||||
<div style={{ display: 'flex', flex: 1, flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#FFFFFF' }}>{t('docs.reviewAgent')}</span>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'rgba(255,255,255,0.5)', lineHeight: '20px' }}>{t('docs.reviewAgentDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CodeBlock code="ocr review --audience agent" onCopy={() => handleCopy('ocr review --audience agent')} />
|
||||
</div>
|
||||
|
||||
<p style={subTitle}>{t('docs.reviewFlags')}</p>
|
||||
{/* Flags table */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', borderRadius: 8, border: '1px solid rgba(255,255,255,0.16)', overflow: 'hidden' }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid rgba(255,255,255,0.16)' }}>
|
||||
<div style={{ width: 120, flexShrink: 0, padding: '10px 12px' }}><span style={{ fontSize: 13, fontWeight: 500, color: 'rgba(255,255,255,0.6)' }}>{t('docs.reviewFlagCol1')}</span></div>
|
||||
<div style={{ flex: 1, padding: '10px 12px' }}><span style={{ fontSize: 13, fontWeight: 500, color: 'rgba(255,255,255,0.6)' }}>{t('docs.reviewFlagCol2')}</span></div>
|
||||
<div style={{ width: 120, flexShrink: 0, padding: '10px 12px' }}><span style={{ fontSize: 13, fontWeight: 500, color: 'rgba(255,255,255,0.6)' }}>{t('docs.reviewFlagCol3')}</span></div>
|
||||
</div>
|
||||
{/* Rows */}
|
||||
{[
|
||||
['-c, --commit', t('docs.reviewFlag1Desc'), '—'],
|
||||
['--from', t('docs.reviewFlag2Desc'), '—'],
|
||||
['--to', t('docs.reviewFlag3Desc'), '—'],
|
||||
['-f, --format', t('docs.reviewFlag4Desc'), 'text'],
|
||||
['--repo', t('docs.reviewFlag5Desc'), t('docs.reviewFlag5Default')],
|
||||
['--rule', t('docs.reviewFlag6Desc'), t('docs.reviewFlag6Default')],
|
||||
['--concurrency', t('docs.reviewFlag7Desc'), '8'],
|
||||
['--timeout', t('docs.reviewFlag8Desc'), '10'],
|
||||
['--audience', t('docs.reviewFlag9Desc'), 'human'],
|
||||
['--max-tools', t('docs.reviewFlag10Desc'), t('docs.reviewFlag10Default')],
|
||||
].map(([flag, d, def], idx, arr) => (
|
||||
<div key={idx} style={{ display: 'flex', borderBottom: idx < arr.length - 1 ? '1px solid rgba(255,255,255,0.16)' : 'none' }}>
|
||||
<div style={{ width: 120, height: 44, flexShrink: 0, display: 'flex', alignItems: 'center', padding: '10px 12px' }}>
|
||||
<span style={{ fontSize: 12, fontFamily: 'Menlo, monospace', color: 'rgba(255,255,255,0.7)' }}>{flag}</span>
|
||||
</div>
|
||||
<div style={{ flex: 1, height: 44, display: 'flex', alignItems: 'center', padding: '10px 12px' }}>
|
||||
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)' }}>{d}</span>
|
||||
</div>
|
||||
<div style={{ width: 120, height: 44, flexShrink: 0, display: 'flex', alignItems: 'center', padding: '10px 12px' }}>
|
||||
<span style={{ fontSize: 12, fontFamily: 'Menlo, monospace', color: 'rgba(255,255,255,0.5)' }}>{def}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p style={{ ...desc, marginTop: 16, fontSize: 12 }}>
|
||||
{t('docs.reviewNote').replace(/<\/?code>/g, '')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* ─── Viewer ─── */}
|
||||
<section id="viewer" style={{ ...sectionSpacing, scrollMarginTop: 100 }}>
|
||||
<p style={sectionTitle}>{t('docs.viewerTitle')}</p>
|
||||
<p style={desc}>{t('docs.viewerDesc')}</p>
|
||||
<CodeBlock code="ocr viewer" onCopy={() => handleCopy('ocr viewer')} />
|
||||
<p style={{ ...desc, marginTop: 12 }}>{t('docs.viewerNote')}</p>
|
||||
</section>
|
||||
|
||||
{/* ─── Claude Code Integration ─── */}
|
||||
<section id="env" style={{ ...sectionSpacing, scrollMarginTop: 100 }}>
|
||||
<p style={sectionTitle}>{t('docs.envTitle')}</p>
|
||||
<p style={desc}>
|
||||
{t('docs.envDesc').replace(/<\/?code>/g, '')}
|
||||
</p>
|
||||
<CodeBlock
|
||||
code={`export ANTHROPIC_BASE_URL=https://api.anthropic.com\nexport ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxx\nexport ANTHROPIC_MODEL=claude-opus-4-6\n\n# Open Code Review auto-detects these variables ✨`}
|
||||
onCopy={() => handleCopy('export ANTHROPIC_BASE_URL=https://api.anthropic.com\nexport ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxx\nexport ANTHROPIC_MODEL=claude-opus-4-6')}
|
||||
/>
|
||||
<p style={{ ...desc, marginTop: 12 }}>
|
||||
{t('docs.envNote').replace(/<\/?code>/g, '')}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-12 flex gap-12 relative z-10">
|
||||
{/* Sidebar TOC — desktop */}
|
||||
<aside className="hidden lg:block w-56 flex-shrink-0 sticky top-24 self-start max-h-[calc(100vh-120px)] overflow-y-auto">
|
||||
<p className="text-slate-500 text-xs font-mono uppercase tracking-widest mb-4">{t('docs.toc')}</p>
|
||||
<ul className="space-y-1 border-l border-dark-600/20 pl-4">
|
||||
{sections.map((s) => (
|
||||
<li key={s.id}>
|
||||
{/* ─── Right sidebar: CONTENTS (fixed) ─── */}
|
||||
{!isMobile && (
|
||||
<div style={{ position: 'fixed', top: 112, right: 'max(40px, calc((100vw - 1440px) / 2 + 32px))', width: 220, zIndex: 30 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||
<img src={docContentsIcon} style={{ width: 20, height: 20 }} />
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'rgba(255,255,255,0.5)', letterSpacing: '0.05em' }}>{t('docs.toc')}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{sections.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => scrollToSection(s.id)}
|
||||
className={`w-full text-left block py-1.5 text-sm transition-all border-l-2 -ml-4 pl-4 ${
|
||||
activeSection === s.id
|
||||
? 'text-brand-400 border-brand-500 font-medium'
|
||||
: 'text-slate-500 border-transparent hover:text-slate-300 hover:border-slate-700'
|
||||
}`}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontSize: 14,
|
||||
fontFamily: 'PingFang SC, -apple-system, sans-serif',
|
||||
fontWeight: activeSection === s.id ? 500 : 400,
|
||||
color: activeSection === s.id ? '#2BDE5E' : 'rgba(255,255,255,0.5)',
|
||||
lineHeight: '22px',
|
||||
padding: 0,
|
||||
transition: 'color 0.2s',
|
||||
}}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 min-w-0 max-w-3xl">
|
||||
{/* Overview */}
|
||||
<DocSection id="overview" title={t('docs.overviewTitle')}>
|
||||
<p className="text-slate-300 leading-relaxed mb-4">
|
||||
<code className="text-brand-400 bg-dark-800/50 px-1.5 py-0.5 rounded text-sm font-mono">Open Code Review</code>{' '}
|
||||
<span dangerouslySetInnerHTML={{ __html: t('docs.overviewDesc') }} />
|
||||
</p>
|
||||
<div className="glass rounded-xl p-5 mb-6">
|
||||
<p className="text-slate-400 text-sm mb-3"><strong className="text-white">{t('docs.overviewFeatures')}</strong></p>
|
||||
<ul className="space-y-2 text-sm text-slate-400">
|
||||
{(['docs.overviewFeat1', 'docs.overviewFeat2', 'docs.overviewFeat3', 'docs.overviewFeat4', 'docs.overviewFeat5', 'docs.overviewFeat6'] as const).map((key) => (
|
||||
<li key={key} className="flex items-start gap-2"><i className="fa-solid fa-check text-brand-500 mt-1 text-xs"></i>{t(key)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</DocSection>
|
||||
|
||||
{/* Install */}
|
||||
<DocSection id="install" title={t('docs.installTitle')}>
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="feature-card rounded-xl p-4 glass">
|
||||
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-download text-brand-400 text-sm"></i>
|
||||
{t('docs.installLabel')}
|
||||
</h4>
|
||||
<CodeBlock
|
||||
code="npm i -g @alibaba-group/open-code-review"
|
||||
copied={copiedIndex === 'install'}
|
||||
onCopy={() => handleCopy('npm i -g @alibaba-group/open-code-review', 'install')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
<div className="feature-card rounded-xl p-4 glass">
|
||||
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-circle-check text-brand-400 text-sm"></i>
|
||||
{t('docs.installVerifyLabel')}
|
||||
</h4>
|
||||
<CodeBlock
|
||||
code="ocr version"
|
||||
copied={copiedIndex === 'install-verify'}
|
||||
onCopy={() => handleCopy('ocr version', 'install-verify')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DocSection>
|
||||
|
||||
{/* Config */}
|
||||
<DocSection id="config" title={t('docs.configTitle')}>
|
||||
<p className="text-slate-300 leading-relaxed mb-6" dangerouslySetInnerHTML={{ __html: t('docs.configDesc') }} />
|
||||
|
||||
<h3 className="text-lg font-semibold text-white mb-3">{t('docs.configInteractive')}</h3>
|
||||
<p className="text-slate-400 text-sm mb-3">{t('docs.configInteractiveDesc')}</p>
|
||||
<div className="space-y-3 mb-6">
|
||||
<CodeBlock
|
||||
code="ocr config provider"
|
||||
copied={copiedIndex === 'config-provider'}
|
||||
onCopy={() => handleCopy('ocr config provider', 'config-provider')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3 mb-6">
|
||||
<p className="text-slate-500 text-xs font-mono mb-2 ml-1">{t('docs.configModelSelect')}</p>
|
||||
<p className="text-slate-400 text-sm mb-3">{t('docs.configModelSelectDesc')}</p>
|
||||
<CodeBlock
|
||||
code="ocr config model"
|
||||
copied={copiedIndex === 'config-model'}
|
||||
onCopy={() => handleCopy('ocr config model', 'config-model')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3 mb-8">
|
||||
<p className="text-slate-500 text-xs font-mono mb-2 ml-1">{t('docs.configListProviders')}</p>
|
||||
<p className="text-slate-400 text-sm mb-3">{t('docs.configListProvidersDesc')}</p>
|
||||
<CodeBlock
|
||||
code="ocr llm providers"
|
||||
copied={copiedIndex === 'config-providers-list'}
|
||||
onCopy={() => handleCopy('ocr llm providers', 'config-providers-list')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-white mb-3">{t('docs.configManual')}</h3>
|
||||
|
||||
<h4 className="text-base font-medium text-slate-300 mb-3">{t('docs.configCommand')}</h4>
|
||||
<CodeBlock code="ocr config set <key> <value>" />
|
||||
|
||||
<h4 className="text-base font-medium text-slate-300 mb-3 mt-6">{t('docs.configExample')}</h4>
|
||||
<div className="space-y-3 mb-8">
|
||||
<CodeBlock
|
||||
code={`ocr config set llm.url https://api.anthropic.com \\\n && ocr config set llm.auth_token {{your-api-key}} \\\n && ocr config set llm.model claude-opus-4-6 \\\n && ocr config set llm.use_anthropic true \\\n && ocr config set language Chinese`}
|
||||
copied={copiedIndex === 'config-examples'}
|
||||
onCopy={() => handleCopy(`ocr config set llm.url https://api.anthropic.com \\\n && ocr config set llm.auth_token {{your-api-key}} \\\n && ocr config set llm.model claude-opus-4-6 \\\n && ocr config set llm.use_anthropic true \\\n && ocr config set language Chinese`, 'config-examples')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-white mb-3">{t('docs.configKeys')}</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{[
|
||||
{ key: 'llm.url', descKey: 'docs.configKeyUrl' },
|
||||
{ key: 'llm.auth_token', descKey: 'docs.configKeyToken' },
|
||||
{ key: 'llm.model', descKey: 'docs.configKeyModel' },
|
||||
{ key: 'llm.use_anthropic', descKey: 'docs.configKeyAnthropic' },
|
||||
{ key: 'llm.extra_body', descKey: 'docs.configKeyExtraBody' },
|
||||
{ key: 'language', descKey: 'docs.configKeyLanguage' },
|
||||
{ key: 'telemetry.enabled', descKey: 'docs.configKeyTelemetry' },
|
||||
].map(({ key, descKey }) => (
|
||||
<div key={key} className="rounded-lg bg-dark-800/40 px-3 py-2 border border-dark-600/20">
|
||||
<code className="text-brand-400 font-mono text-sm">{key}</code>
|
||||
<span className="text-slate-500 text-sm ml-2">{t(descKey)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-white mb-3 mt-8">{t('docs.configVerify')}</h3>
|
||||
<CodeBlock
|
||||
code={`# Test LLM connection\nocr llm test`}
|
||||
/>
|
||||
<p className="text-slate-400 text-sm mt-4">
|
||||
{t('docs.configVerifyDesc')}
|
||||
</p>
|
||||
</DocSection>
|
||||
|
||||
{/* Review */}
|
||||
<DocSection id="review" title={t('docs.reviewTitle')}>
|
||||
<p className="text-slate-300 leading-relaxed mb-6" dangerouslySetInnerHTML={{ __html: t('docs.reviewDesc') }} />
|
||||
|
||||
<h3 className="text-lg font-semibold text-white mb-3">{t('docs.reviewModes')}</h3>
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="feature-card rounded-xl p-4 glass">
|
||||
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-pen-to-square text-brand-400 text-sm"></i>
|
||||
{t('docs.reviewWorkspace')}
|
||||
</h4>
|
||||
<p className="text-slate-400 text-sm mb-2">{t('docs.reviewWorkspaceDesc')}</p>
|
||||
<CodeBlock code="ocr review" />
|
||||
</div>
|
||||
<div className="feature-card rounded-xl p-4 glass">
|
||||
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-code-branch text-brand-400 text-sm"></i>
|
||||
{t('docs.reviewBranch')}
|
||||
</h4>
|
||||
<p className="text-slate-400 text-sm mb-2">{t('docs.reviewBranchDesc')}</p>
|
||||
<CodeBlock
|
||||
code="ocr review --from master --to dev-ref"
|
||||
copied={copiedIndex === 'review-branch'}
|
||||
onCopy={() => handleCopy('ocr review --from master --to dev-ref', 'review-branch')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
<div className="feature-card rounded-xl p-4 glass">
|
||||
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-code-commit text-brand-400 text-sm"></i>
|
||||
{t('docs.reviewCommit')}
|
||||
</h4>
|
||||
<p className="text-slate-400 text-sm mb-2">{t('docs.reviewCommitDesc')}</p>
|
||||
<CodeBlock
|
||||
code={`ocr review --commit abc123\nocr review -c abc123`}
|
||||
copied={copiedIndex === 'review-commit'}
|
||||
onCopy={() => handleCopy('ocr review -c abc123', 'review-commit')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-white mb-3">{t('docs.reviewAdvanced')}</h3>
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="feature-card rounded-xl p-4 glass">
|
||||
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-file-code text-brand-400 text-sm"></i>
|
||||
{t('docs.reviewBackground')}
|
||||
</h4>
|
||||
<p className="text-slate-400 text-sm mb-2">{t('docs.reviewBackgroundDesc')}</p>
|
||||
<CodeBlock
|
||||
code={`ocr review --background "requirement context"\nocr review -b "requirement context"`}
|
||||
copied={copiedIndex === 'review-background'}
|
||||
onCopy={() => handleCopy('ocr review --background "requirement context"', 'review-background')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
<div className="feature-card rounded-xl p-4 glass">
|
||||
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-file-code text-brand-400 text-sm"></i>
|
||||
{t('docs.reviewJson')}
|
||||
</h4>
|
||||
<p className="text-slate-400 text-sm mb-2">{t('docs.reviewJsonDesc')}</p>
|
||||
<CodeBlock
|
||||
code={`ocr review --format json\nocr review -f json`}
|
||||
copied={copiedIndex === 'review-json'}
|
||||
onCopy={() => handleCopy('ocr review --format json', 'review-json')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
<div className="feature-card rounded-xl p-4 glass">
|
||||
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-robot text-brand-400 text-sm"></i>
|
||||
{t('docs.reviewAgent')}
|
||||
</h4>
|
||||
<p className="text-slate-400 text-sm mb-2">{t('docs.reviewAgentDesc')}</p>
|
||||
<CodeBlock
|
||||
code="ocr review --audience agent"
|
||||
copied={copiedIndex === 'review-agent'}
|
||||
onCopy={() => handleCopy('ocr review --audience agent', 'review-agent')}
|
||||
copyLabel={t('docs.copy')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-white mb-3">{t('docs.reviewFlags')}</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-dark-600/30">
|
||||
<th className="text-left py-2 px-3 text-slate-400 font-mono text-xs">{t('docs.reviewFlagCol1')}</th>
|
||||
<th className="text-left py-2 px-3 text-slate-400 text-xs">{t('docs.reviewFlagCol2')}</th>
|
||||
<th className="text-left py-2 px-3 text-slate-400 text-xs">{t('docs.reviewFlagCol3')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
['-c, --commit', t('docs.reviewFlag1Desc'), ''],
|
||||
['--from', t('docs.reviewFlag2Desc'), ''],
|
||||
['--to', t('docs.reviewFlag3Desc'), ''],
|
||||
['-f, --format', t('docs.reviewFlag4Desc'), 'text'],
|
||||
['--repo', t('docs.reviewFlag5Desc'), t('docs.reviewFlag5Default')],
|
||||
['--rule', t('docs.reviewFlag6Desc'), t('docs.reviewFlag6Default')],
|
||||
['--concurrency', t('docs.reviewFlag7Desc'), '8'],
|
||||
['--timeout', t('docs.reviewFlag8Desc'), '10'],
|
||||
['--audience', t('docs.reviewFlag9Desc'), 'human'],
|
||||
['--max-tools', t('docs.reviewFlag10Desc'), t('docs.reviewFlag10Default')],
|
||||
['--max-git-procs', t('docs.reviewFlag11Desc'), t('docs.reviewFlag10Default')],
|
||||
].map(([flag, desc, def]) => (
|
||||
<tr key={flag} className="border-b border-dark-800/30 hover:bg-dark-800/20 transition-colors">
|
||||
<td className="py-2 px-3"><code className="text-brand-400 font-mono text-xs whitespace-nowrap">{flag}</code></td>
|
||||
<td className="py-2 px-3 text-slate-300">{desc}</td>
|
||||
<td className="py-2 px-3 text-slate-500 font-mono text-xs">{def || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs mt-3" dangerouslySetInnerHTML={{ __html: t('docs.reviewNote') }} />
|
||||
</DocSection>
|
||||
|
||||
{/* Viewer */}
|
||||
<DocSection id="viewer" title={t('docs.viewerTitle')}>
|
||||
<p className="text-slate-300 leading-relaxed mb-6">
|
||||
{t('docs.viewerDesc')}
|
||||
</p>
|
||||
|
||||
<CodeBlock code="ocr viewer" />
|
||||
<p className="text-slate-400 text-sm mt-4">
|
||||
{t('docs.viewerNote')}
|
||||
</p>
|
||||
</DocSection>
|
||||
|
||||
{/* Environment variables */}
|
||||
<DocSection id="env" title={t('docs.envTitle')}>
|
||||
<p className="text-slate-300 leading-relaxed mb-4" dangerouslySetInnerHTML={{ __html: t('docs.envDesc') }} />
|
||||
<CodeBlock
|
||||
code={`export ANTHROPIC_BASE_URL=https://api.anthropic.com
|
||||
export ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxx
|
||||
export ANTHROPIC_MODEL=claude-opus-4-6
|
||||
|
||||
${t('quickstart.commentEnvAuto')} ✨`}
|
||||
/>
|
||||
<p className="text-slate-400 text-sm mt-4" dangerouslySetInnerHTML={{ __html: t('docs.envNote') }} />
|
||||
</DocSection>
|
||||
|
||||
{/* Footer spacer */}
|
||||
<div className="h-32"></div>
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
<Toast message={t('quickstart.copied')} visible={toastVisible} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
39
pages/src/pages/FeaturesPage.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import HeroSection from '../components/HeroSection';
|
||||
import HighlightsSection from '../components/HighlightsSection';
|
||||
import UseCasesSection from '../components/UseCasesSection';
|
||||
import FeaturesSection from '../components/FeaturesSection';
|
||||
import BenchmarkSection from '../components/BenchmarkSection';
|
||||
import QuickStartSection from '../components/QuickStartSection';
|
||||
import Footer from '../components/Footer';
|
||||
import FadeInSection from '../components/FadeInSection';
|
||||
|
||||
const FeaturesPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<FadeInSection>
|
||||
<HeroSection />
|
||||
</FadeInSection>
|
||||
<FadeInSection delay={100}>
|
||||
<HighlightsSection />
|
||||
</FadeInSection>
|
||||
<FadeInSection delay={100}>
|
||||
<UseCasesSection />
|
||||
</FadeInSection>
|
||||
<FadeInSection delay={100}>
|
||||
<FeaturesSection />
|
||||
</FadeInSection>
|
||||
<FadeInSection delay={100}>
|
||||
<BenchmarkSection />
|
||||
</FadeInSection>
|
||||
<FadeInSection delay={100}>
|
||||
<QuickStartSection />
|
||||
</FadeInSection>
|
||||
<FadeInSection>
|
||||
<Footer />
|
||||
</FadeInSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesPage;
|
||||
19
pages/src/pages/QuickStartPage.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import QuickStartSection from '../components/QuickStartSection';
|
||||
import Footer from '../components/Footer';
|
||||
import FadeInSection from '../components/FadeInSection';
|
||||
|
||||
const QuickStartPage: React.FC = () => {
|
||||
return (
|
||||
<div style={{ paddingTop: 72 }}>
|
||||
<FadeInSection>
|
||||
<QuickStartSection />
|
||||
</FadeInSection>
|
||||
<FadeInSection>
|
||||
<Footer />
|
||||
</FadeInSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickStartPage;
|
||||
|
|
@ -2,426 +2,111 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #030712;
|
||||
color: #f8fafc;
|
||||
font-family: 'Inter', sans-serif;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Segoe UI', Roboto, sans-serif;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ========== Noise Texture Overlay ========== */
|
||||
.noise-overlay::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
opacity: 0.025;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
background-size: 256px 256px;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ========== Scrollbar ========== */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0a0f1e;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
background: rgba(255,255,255,0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #22c55e;
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
/* ========== Grid Background ========== */
|
||||
.grid-bg {
|
||||
background-image:
|
||||
linear-gradient(rgba(34, 197, 94, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(34, 197, 94, 0.03) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
/* UseCases card hover effect */
|
||||
@property --sweep-angle {
|
||||
syntax: '<angle>';
|
||||
initial-value: 0deg;
|
||||
inherits: true;
|
||||
}
|
||||
|
||||
/* ========== Radial Glow Orbs ========== */
|
||||
.glow-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ========== Glow Effects ========== */
|
||||
.glow-green {
|
||||
box-shadow:
|
||||
0 0 30px rgba(34, 197, 94, 0.25),
|
||||
0 0 60px rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.glow-green-sm {
|
||||
box-shadow:
|
||||
0 0 12px rgba(34, 197, 94, 0.3),
|
||||
0 0 24px rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.glow-card {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.3),
|
||||
0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.text-glow {
|
||||
text-shadow:
|
||||
0 0 20px rgba(34, 197, 94, 0.4),
|
||||
0 0 40px rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
.border-glow {
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(34, 197, 94, 0.1) inset,
|
||||
0 0 16px rgba(34, 197, 94, 0.06);
|
||||
}
|
||||
|
||||
/* ========== Glassmorphism ========== */
|
||||
.glass {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.glass-strong {
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* ========== Terminal Cursor Blink ========== */
|
||||
.terminal-cursor::after {
|
||||
content: '|';
|
||||
animation: blink 1s step-end infinite;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* ========== Stat Card (Enhanced) ========== */
|
||||
.stat-card {
|
||||
background: linear-gradient(145deg, rgba(15, 23, 42, 0.9) 0%, rgba(10, 15, 30, 0.95) 100%);
|
||||
border: 1px solid rgba(34, 197, 94, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
.usecase-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.17);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
transition: border-color 0.4s ease;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(34, 197, 94, 0.3), transparent);
|
||||
opacity: 0;
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: rgba(34, 197, 94, 0.3);
|
||||
transform: translateY(-4px);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(34, 197, 94, 0.12),
|
||||
0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ========== Feature Card (Enhanced) ========== */
|
||||
.feature-card {
|
||||
background: linear-gradient(160deg, rgba(15, 23, 42, 0.7) 0%, rgba(10, 15, 30, 0.85) 100%);
|
||||
border: 1px solid rgba(51, 65, 85, 0.4);
|
||||
backdrop-filter: blur(6px);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.feature-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(34, 197, 94, 0.08) 0%, transparent 60%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.feature-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: rgba(34, 197, 94, 0.25);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(34, 197, 94, 0.08),
|
||||
0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* ========== Nav Link ========== */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, #22c55e, #4ade80);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========== Primary Button (Enhanced) ========== */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
box-shadow:
|
||||
0 0 24px rgba(34, 197, 94, 0.5),
|
||||
0 0 48px rgba(34, 197, 94, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ========== Secondary Button ========== */
|
||||
.btn-secondary {
|
||||
border: 1px solid rgba(34, 197, 94, 0.4);
|
||||
background: rgba(34, 197, 94, 0.05);
|
||||
backdrop-filter: blur(4px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
border-color: rgba(34, 197, 94, 0.7);
|
||||
box-shadow: 0 0 16px rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
|
||||
/* ========== Rank Badge ========== */
|
||||
.rank-badge {
|
||||
background: linear-gradient(145deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.08) 100%);
|
||||
border: 1px solid rgba(34, 197, 94, 0.25);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* ========== Leaderboard Row ========== */
|
||||
.leaderboard-row {
|
||||
border-bottom: 1px solid rgba(51, 65, 85, 0.3);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.leaderboard-row:hover {
|
||||
background: rgba(34, 197, 94, 0.04);
|
||||
}
|
||||
|
||||
/* ========== Tags ========== */
|
||||
.tag-open {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #4ade80;
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.tag-closed {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #f87171;
|
||||
border: 1px solid rgba(239, 68, 68, 0.25);
|
||||
}
|
||||
|
||||
.tag-reproduced {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
color: #a5b4fc;
|
||||
border: 1px solid rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
/* ========== Progress Bar ========== */
|
||||
.progress-bar {
|
||||
background: linear-gradient(90deg, #22c55e, #4ade80);
|
||||
border-radius: 2px;
|
||||
height: 3px;
|
||||
transition: width 1s ease;
|
||||
}
|
||||
|
||||
/* ========== Section Divider (Enhanced) ========== */
|
||||
.section-divider {
|
||||
background: linear-gradient(90deg, transparent, rgba(34, 197, 94, 0.2), transparent);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.section-divider-soft {
|
||||
background: linear-gradient(90deg, transparent, rgba(51, 65, 85, 0.3), transparent);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* ========== Code Block ========== */
|
||||
.code-block {
|
||||
background: rgba(10, 15, 30, 0.9);
|
||||
border: 1px solid rgba(51, 65, 85, 0.5);
|
||||
border-left: 3px solid #22c55e;
|
||||
}
|
||||
|
||||
/* ========== Highlight Card ========== */
|
||||
.highlight-card {
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(6, 182, 212, 0.10), rgba(139, 92, 246, 0.08));
|
||||
border-radius: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.highlight-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.3), rgba(6, 182, 212, 0.2), rgba(139, 92, 246, 0.15));
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ========== Floating Badge ========== */
|
||||
.floating-badge {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
/* ========== Animated Gradient Border ========== */
|
||||
.gradient-border {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-border::after {
|
||||
.usecase-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: inherit;
|
||||
border-radius: 9px;
|
||||
padding: 1px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(135deg, #22c55e, #06b6d4, #8b5cf6, #22c55e);
|
||||
background-size: 300% 300%;
|
||||
animation: gradientShift 6s ease infinite;
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
background: conic-gradient(
|
||||
from var(--sweep-angle),
|
||||
transparent 0%,
|
||||
#31FF75 8%,
|
||||
#33DDF7 16%,
|
||||
rgba(55,252,170,0.79) 24%,
|
||||
transparent 40%
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.gradient-border:hover::after {
|
||||
.usecase-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.14) 0%, #141313 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.usecase-card:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.usecase-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
/* ========== Spotlight Effect ========== */
|
||||
.spotlight {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spotlight::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(34, 197, 94, 0.03) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
animation: spotlightRotate 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spotlightRotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ========== Shimmer Animation ========== */
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shimmer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(34, 197, 94, 0.03), transparent);
|
||||
animation: shimmer 3s ease-in-out infinite;
|
||||
.usecase-card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ module.exports = {
|
|||
{
|
||||
test: /\.svg$/,
|
||||
type: 'asset/resource'
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif)$/,
|
||||
type: 'asset/resource'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -40,10 +44,12 @@ module.exports = {
|
|||
},
|
||||
devServer: {
|
||||
port: 3030,
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: 'all',
|
||||
static: { directory: __dirname },
|
||||
historyApiFallback: {
|
||||
index: '/index.html',
|
||||
rewrites: [{ from: /^\/_p\/\d+\//, to: '/index.html' }]
|
||||
rewrites: [{ from: /^\/\_p\/\d+\//, to: '/index.html' }]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
|
|
|
|||