diff --git a/pages/package.json b/pages/package.json index 4ca474a..f6a0775 100644 --- a/pages/package.json +++ b/pages/package.json @@ -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", diff --git a/pages/src/App.tsx b/pages/src/App.tsx index d5e58e0..7b2f761 100644 --- a/pages/src/App.tsx +++ b/pages/src/App.tsx @@ -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 ( - } /> - } /> + } /> + } /> + } /> + } /> ); }; diff --git a/pages/src/assets.d.ts b/pages/src/assets.d.ts index 3623b38..6ac3aff 100644 --- a/pages/src/assets.d.ts +++ b/pages/src/assets.d.ts @@ -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; +} diff --git a/pages/src/assets/icons/doc-check-circle.svg b/pages/src/assets/icons/doc-check-circle.svg new file mode 100644 index 0000000..151d981 --- /dev/null +++ b/pages/src/assets/icons/doc-check-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/doc-contents.svg b/pages/src/assets/icons/doc-contents.svg new file mode 100644 index 0000000..e240b3f --- /dev/null +++ b/pages/src/assets/icons/doc-contents.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/doc-copy.svg b/pages/src/assets/icons/doc-copy.svg new file mode 100644 index 0000000..bc0531f --- /dev/null +++ b/pages/src/assets/icons/doc-copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/doc-download.svg b/pages/src/assets/icons/doc-download.svg new file mode 100644 index 0000000..b06a6f3 --- /dev/null +++ b/pages/src/assets/icons/doc-download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/doc-edit.svg b/pages/src/assets/icons/doc-edit.svg new file mode 100644 index 0000000..2db55e4 --- /dev/null +++ b/pages/src/assets/icons/doc-edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-chevron-down.svg b/pages/src/assets/icons/icon-chevron-down.svg new file mode 100644 index 0000000..48f3a53 --- /dev/null +++ b/pages/src/assets/icons/icon-chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-chevron-right.svg b/pages/src/assets/icons/icon-chevron-right.svg new file mode 100644 index 0000000..6ed70a2 --- /dev/null +++ b/pages/src/assets/icons/icon-chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-copy.svg b/pages/src/assets/icons/icon-copy.svg new file mode 100644 index 0000000..8bc28fa --- /dev/null +++ b/pages/src/assets/icons/icon-copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-feature-architecture.svg b/pages/src/assets/icons/icon-feature-architecture.svg new file mode 100644 index 0000000..faaf445 --- /dev/null +++ b/pages/src/assets/icons/icon-feature-architecture.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-feature-compression.svg b/pages/src/assets/icons/icon-feature-compression.svg new file mode 100644 index 0000000..69e7ac1 --- /dev/null +++ b/pages/src/assets/icons/icon-feature-compression.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-feature-concurrent.svg b/pages/src/assets/icons/icon-feature-concurrent.svg new file mode 100644 index 0000000..dac64e2 --- /dev/null +++ b/pages/src/assets/icons/icon-feature-concurrent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-feature-multi-model.svg b/pages/src/assets/icons/icon-feature-multi-model.svg new file mode 100644 index 0000000..e47278a --- /dev/null +++ b/pages/src/assets/icons/icon-feature-multi-model.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-feature-positioning.svg b/pages/src/assets/icons/icon-feature-positioning.svg new file mode 100644 index 0000000..c1f115a --- /dev/null +++ b/pages/src/assets/icons/icon-feature-positioning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-feature-rules.svg b/pages/src/assets/icons/icon-feature-rules.svg new file mode 100644 index 0000000..4e6f6b0 --- /dev/null +++ b/pages/src/assets/icons/icon-feature-rules.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-github.svg b/pages/src/assets/icons/icon-github.svg new file mode 100644 index 0000000..d66421f --- /dev/null +++ b/pages/src/assets/icons/icon-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-language.svg b/pages/src/assets/icons/icon-language.svg new file mode 100644 index 0000000..69351c6 --- /dev/null +++ b/pages/src/assets/icons/icon-language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-play.svg b/pages/src/assets/icons/icon-play.svg new file mode 100644 index 0000000..98ff051 --- /dev/null +++ b/pages/src/assets/icons/icon-play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-sort.svg b/pages/src/assets/icons/icon-sort.svg new file mode 100644 index 0000000..b7334e8 --- /dev/null +++ b/pages/src/assets/icons/icon-sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-terminal-prompt.svg b/pages/src/assets/icons/icon-terminal-prompt.svg new file mode 100644 index 0000000..4efb04f --- /dev/null +++ b/pages/src/assets/icons/icon-terminal-prompt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-usecase-developer.svg b/pages/src/assets/icons/icon-usecase-developer.svg new file mode 100644 index 0000000..d6d0fef --- /dev/null +++ b/pages/src/assets/icons/icon-usecase-developer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-usecase-platform-a.svg b/pages/src/assets/icons/icon-usecase-platform-a.svg new file mode 100644 index 0000000..5a5d8a6 --- /dev/null +++ b/pages/src/assets/icons/icon-usecase-platform-a.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-usecase-platform-b.svg b/pages/src/assets/icons/icon-usecase-platform-b.svg new file mode 100644 index 0000000..475c382 --- /dev/null +++ b/pages/src/assets/icons/icon-usecase-platform-b.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-usecase-platform-c.svg b/pages/src/assets/icons/icon-usecase-platform-c.svg new file mode 100644 index 0000000..66185fe --- /dev/null +++ b/pages/src/assets/icons/icon-usecase-platform-c.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/icon-usecase-researcher.svg b/pages/src/assets/icons/icon-usecase-researcher.svg new file mode 100644 index 0000000..ed663dc --- /dev/null +++ b/pages/src/assets/icons/icon-usecase-researcher.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/trophy-bronze.svg b/pages/src/assets/icons/trophy-bronze.svg new file mode 100644 index 0000000..8b0e476 --- /dev/null +++ b/pages/src/assets/icons/trophy-bronze.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/trophy-gold.svg b/pages/src/assets/icons/trophy-gold.svg new file mode 100644 index 0000000..1ec91f8 --- /dev/null +++ b/pages/src/assets/icons/trophy-gold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/icons/trophy-silver.svg b/pages/src/assets/icons/trophy-silver.svg new file mode 100644 index 0000000..5e62c3a --- /dev/null +++ b/pages/src/assets/icons/trophy-silver.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/brandicon.svg b/pages/src/assets/images/brandicon.svg new file mode 100644 index 0000000..d4fe70e --- /dev/null +++ b/pages/src/assets/images/brandicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/icon-claude-code.svg b/pages/src/assets/images/icon-claude-code.svg new file mode 100644 index 0000000..2f23cf2 --- /dev/null +++ b/pages/src/assets/images/icon-claude-code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/icon-codex.svg b/pages/src/assets/images/icon-codex.svg new file mode 100644 index 0000000..4b48dad --- /dev/null +++ b/pages/src/assets/images/icon-codex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/provider-1.svg b/pages/src/assets/images/provider-1.svg new file mode 100644 index 0000000..ecc9fd1 --- /dev/null +++ b/pages/src/assets/images/provider-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/provider-2.svg b/pages/src/assets/images/provider-2.svg new file mode 100644 index 0000000..35b388d --- /dev/null +++ b/pages/src/assets/images/provider-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/provider-3.svg b/pages/src/assets/images/provider-3.svg new file mode 100644 index 0000000..e892e26 --- /dev/null +++ b/pages/src/assets/images/provider-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/provider-4.svg b/pages/src/assets/images/provider-4.svg new file mode 100644 index 0000000..ecf3ffd --- /dev/null +++ b/pages/src/assets/images/provider-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/provider-5.svg b/pages/src/assets/images/provider-5.svg new file mode 100644 index 0000000..992baf3 --- /dev/null +++ b/pages/src/assets/images/provider-5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/provider-6.svg b/pages/src/assets/images/provider-6.svg new file mode 100644 index 0000000..9038235 --- /dev/null +++ b/pages/src/assets/images/provider-6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/assets/images/provider-7.svg b/pages/src/assets/images/provider-7.svg new file mode 100644 index 0000000..16120ef --- /dev/null +++ b/pages/src/assets/images/provider-7.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/src/components/BenchmarkSection.tsx b/pages/src/components/BenchmarkSection.tsx index baf0096..9c4b1ef 100644 --- a/pages/src/components/BenchmarkSection.tsx +++ b/pages/src/components/BenchmarkSection.tsx @@ -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 = { - ocr: 'text-brand-400', - cc: 'text-[#D97757]', - codex: 'text-[#10a37f]', -}; -const sourceNameMap: Record = { - 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 = { - gold: '🥇', - silver: '🥈', - bronze: '🥉', -}; - -function computeMedals( - entries: BenchmarkEntry[], - field: 'precision' | 'recall' -): Map { - 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(); - 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(null); - const [sortField, setSortField] = useState('f1'); const { t } = useTranslation(); + const { isMobile, isTablet } = useResponsive(); + const [sortField, setSortField] = useState('f1'); + const [sortOrder, setSortOrder] = useState('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(); - 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 ( -
-
- -
-
-
-
-

- {t('benchmark.sectionLabel')} -

-

- {t('benchmark.title')} -

-

- {t('benchmark.subtitlePreRepos')} - 50 - {t('benchmark.subtitlePrePRs')} - 200 - {t('benchmark.subtitlePreLangs')} - 10 - {t('benchmark.subtitleEnd')} -

-
- - {/* Legend */} -
-
- - Open Code Review · {OCR_VERSION} -
-
- - Claude Code · {CC_VERSION} · /code-review -
-
- - Codex · {CODEX_VERSION} · /review -
-
- - {/* Table */} -
- {/* Header */} -
-
{t('benchmark.colRank')}
-
{t('benchmark.colModel')}
-
{t('benchmark.colSource')}
- {(['f1', 'precision', 'recall'] as const).map((field) => { - const labels: Record = { - f1: 'F1', - precision: t('benchmark.colPrecision'), - recall: t('benchmark.colRecall'), - }; - return ( -
setSortField(field)} - > - {labels[field]} - -
- ); - })} -
{t('benchmark.colAvgTime')}
-
{t('benchmark.colAvgToken')}
-
- - {/* 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); +
+
+ {/* Header */} +
+ + {t('benchmark.sectionLabel')} + +

+ {t('benchmark.title')} +

+

+ {t('benchmark.subtitle')} +

+
+ {/* Table */} +
+ {/* Table Header */} +
+ {sortableHeaders.map((col, i) => { + const isActive = col.field === sortField; + const isSortable = !!col.field; return (
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 */} -
- {entryRank != null ? ( - - {entryRank <= 3 ? medalIcons[['gold', 'silver', 'bronze'][entryRank - 1]] : ( - {entryRank} - )} - - ) : ( - - - - )} -
- - {/* Model + Company */} -
-
{entry.model}
-
{entry.company}
-
- - {/* Source */} -
- {entry.sourceType === 'ocr' && } - {entry.sourceType === 'cc' && } - {entry.sourceType === 'codex' && } - - {sourceNameMap[entry.sourceType]} - -
- - {/* F1 */} -
- {entry.f1 != null ? ( - - {entry.f1.toFixed(2)}% - - ) : ( - - - Running - - )} -
- - {/* Precision with medal */} -
- {entry.precision != null ? ( -
-
- {entry.precision.toFixed(2)}% - - {pMedal ? medalIcons[pMedal] : ''} - -
- {entry.precisionDetail && ( -
{entry.precisionDetail}
- )} -
- ) : ( -
-
-
- )} -
- - {/* Recall with medal */} -
- {entry.recall != null ? ( -
-
- {entry.recall.toFixed(2)}% - - {rMedal ? medalIcons[rMedal] : ''} - -
- {entry.recallDetail && ( -
{entry.recallDetail}
- )} -
- ) : ( -
-
-
- )} -
- - {/* Avg Time */} -
- {entry.avgTime ? ( - {entry.avgTime} - ) : ( -
-
-
- )} -
- - {/* Avg Token */} -
- {entry.avgTotalToken ? ( -
- {entry.avgTotalToken} - {entry.avgInputToken && ( -
{entry.avgInputToken} / {entry.avgOutputToken}
- )} -
- ) : ( -
-
-
- )} -
+ + {col.label} + + {isSortable && ( + + )}
); })}
+ {/* 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 ( +
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 */} +
+ + {rankDisplay} + +
+ {/* Model */} +
+ {row.model} + {row.provider} +
+ {/* Source */} +
+ {row.sourceIcon && } + {row.source} +
+ {/* F1 */} +
+ + {row.f1} + +
+ {/* Precision */} +
+
+ + {row.precision} + + {row.precisionMedal !== undefined && } +
+ {row.precisionDetail} +
+ {/* Recall */} +
+
+ + {row.recall} + + {row.recallMedal !== undefined && } +
+ {row.recallDetail} +
+ {/* Avg Time */} +
+ {row.avgTime} +
+ {/* Avg Token */} +
+ {row.avgToken} + {row.tokenDetail} +
+
+ ); + })}
-
+ {/* Footer note */} +

+ Open Code Review · v1.3.1 | Claude Code · v2.1.169 · /code-review | Codex · v0.140.0 · /review +

+
); }; diff --git a/pages/src/components/ColorBends.css b/pages/src/components/ColorBends.css new file mode 100644 index 0000000..b546eb2 --- /dev/null +++ b/pages/src/components/ColorBends.css @@ -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; +} diff --git a/pages/src/components/ColorBends.tsx b/pages/src/components/ColorBends.tsx new file mode 100644 index 0000000..7449fb0 --- /dev/null +++ b/pages/src/components/ColorBends.tsx @@ -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(null); + const rendererRef = useRef(null); + const rafRef = useRef(null); + const materialRef = useRef(null); + const resizeObserverRef = useRef(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
; +} diff --git a/pages/src/components/FadeInSection.tsx b/pages/src/components/FadeInSection.tsx new file mode 100644 index 0000000..ceb5994 --- /dev/null +++ b/pages/src/components/FadeInSection.tsx @@ -0,0 +1,43 @@ +import React, { useRef, useEffect, useState } from 'react'; + +interface FadeInSectionProps { + children: React.ReactNode; + delay?: number; // ms +} + +const FadeInSection: React.FC = ({ children, delay = 0 }) => { + const ref = useRef(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 ( +
+ {children} +
+ ); +}; + +export default FadeInSection; diff --git a/pages/src/components/FeaturesSection.tsx b/pages/src/components/FeaturesSection.tsx index 781dbf0..c17784d 100644 --- a/pages/src/components/FeaturesSection.tsx +++ b/pages/src/components/FeaturesSection.tsx @@ -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 ( -
- {/* Ambient glow */} -
-
+
+
+ {/* Header */} +
+ + {t('features.sectionBadge')} + +

+ {t('features.title')} +

+

+ {t('features.subtitle')} +

+
-
-
-
- {/* Section pill badge */} -
- {t('features.sectionBadge')} -
-

- {t('features.title')} -

-

- {t('features.subtitle')} -

-
- -
- {features.map((feature) => ( -
-
- -
-

{feature.title}

-

{feature.description}

+ {/* Grid */} +
+ {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 ( +
+
+
- ))} -
+
+

+ {feat.title} +

+

+ {feat.desc} +

+
+
+ ); + })}
diff --git a/pages/src/components/Footer.tsx b/pages/src/components/Footer.tsx new file mode 100644 index 0000000..48fb09c --- /dev/null +++ b/pages/src/components/Footer.tsx @@ -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(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 ( +
+
+ + + {t('footer.brand')} + + + {t('footer.copyright')} + + + {/* Language Switcher */} +
+ + {open && ( +
+ {LANG_OPTIONS.map(opt => ( + + ))} +
+ )} +
+
+
+ ); +}; + +export default Footer; diff --git a/pages/src/components/HeroSection.tsx b/pages/src/components/HeroSection.tsx index 54f022e..692403a 100644 --- a/pages/src/components/HeroSection.tsx +++ b/pages/src/components/HeroSection.tsx @@ -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: ( + + $ ocr re + v + iew --from + + mai + n --to feature-auth + + ), + }, + { + num: 2, + hasIcon: true, + content: ( + + [o + cr] R + e + v + iew + ing + + 5 file + (s) in /home/user/project + + ), + }, + { + num: 3, + hasIcon: true, + content: ( + + [o + cr] + + fi + l + e + _re + a + d + " + int + e + rna + l + /a + u + th/login.go" + + ), + }, + { + num: 4, + hasIcon: true, + content: ( + + [ocr + ] ✔ + f + ile + _ + r + ead + (1 + 5 + ms) + + ), + }, + { + num: 5, + hasIcon: true, + content: ( + + [ocr] + + co + de_ + s + e + arch + "p + a + s + swo + r + d.*hash" + + ), + }, + { + num: 6, + hasIcon: true, + content: ( + + [ocr] ✔ c + ode + _ + s + ear + ch + ( + 8 + ms) + + ), + }, + { num: 7, hasIcon: false, content: [ocr] Plan completed for internal/auth/login.go }, + { num: 8, hasIcon: false, content: ─── internal/auth/login.go:42-55 ─── }, + { num: 9, hasIcon: false, content: Consider using bcrypt cost factor ≥ 12 for password hashing. }, + { + num: 10, + hasIcon: false, + content: ( + + [o + cr] Su + m + mar + y: 5 file(s), 3 comment(s), ~8421 tokens, 12.5s + + ), + }, + { num: 11, hasIcon: false, content: }, ]; -const TerminalLine: React.FC<{ line: typeof terminalLines[0]; visible: boolean }> = ({ line, visible }) => { - const colorMap: Record = { - 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 ( -
- {line.text} -
- ); -}; - const HeroSection: React.FC = () => { - const [visibleLines, setVisibleLines] = useState([]); const { t } = useTranslation(); - - useEffect(() => { - const timers: ReturnType[] = []; - 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 ( -
- {/* Layered glow orbs */} -
-
-
+
+ {/* Shader Background */} + - {/* Decorative horizontal lines */} -
+ {/* Gradient overlay */} +
-
- {/* Left content */} -
-
-

- {t('hero.title')} -
- {t('hero.titleHighlight')} -

-

- {t('hero.description')} -

+ {/* Content */} +
+ {/* Title */} +
+

+ {t('hero.title').split('\n').map((line, i, arr) => ( + + {line} + {i < arr.length - 1 &&
} +
+ ))} +

+

+ {t('hero.description')} +

+
+ + {/* Buttons */} + + + {/* Terminal */} +
+ {/* Terminal header */} +
+ + {t('hero.terminal')} +
- - {/* Feature pills */} -
- {pills.map((item) => ( -
- - {item.label} + {/* Terminal body */} +
+ {terminalLines.map((line) => ( +
+
+ + {line.num} + + {line.hasIcon && } +
+ + {line.content} +
))}
- - {/* CTA buttons */} -
- - -
- - {/* Social proof */} -
-
- - {t('hero.users')} -
-
-
- - {t('hero.openSource')} -
-
- - {/* Right terminal */} -
- {/* Outer glow frame */} -
- -
- {/* Scan line effect */} -
- -
-
-
-
- {t('hero.terminal')} -
- - {terminalLines.map((line, index) => ( - - ))} - - {visibleLines.length < terminalLines.length && ( -
- )} -
- - {/* F1 badge */} -
-
-
F1: {t('highlights.stat4Value')}
-
{t('hero.badgeLabel')}
-
-
- - {/* Decorative accent line */} -
-
-
- - {/* Scroll indicator */} -
- Scroll -
); diff --git a/pages/src/components/HighlightsSection.tsx b/pages/src/components/HighlightsSection.tsx index 1e48f07..42c98ba 100644 --- a/pages/src/components/HighlightsSection.tsx +++ b/pages/src/components/HighlightsSection.tsx @@ -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(0); + const startTimeRef = useRef(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(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 ( -
-
- -
-
-
-
- {stats.map((stat, i) => ( -
-
- - - {stat.label} - -
-

- {stat.value} -

-

{stat.caption}

-
- ))} +
+
+ {stats.map((stat, i) => ( +
+ + + +
+

+ {stat.label} +

+

+ {stat.caption} +

-
+ ))}
); diff --git a/pages/src/components/LandingPage.tsx b/pages/src/components/LandingPage.tsx index 573ae7a..a011807 100644 --- a/pages/src/components/LandingPage.tsx +++ b/pages/src/components/LandingPage.tsx @@ -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 = ({ children }) => { return ( -
- - - - - - - +
+
+ + {children} +
); }; diff --git a/pages/src/components/Navbar.tsx b/pages/src/components/Navbar.tsx index 57cfc86..4488d75 100644 --- a/pages/src/components/Navbar.tsx +++ b/pages/src/components/Navbar.tsx @@ -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(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 ( ); }; diff --git a/pages/src/components/QuickStartSection.tsx b/pages/src/components/QuickStartSection.tsx index 8c6d750..8402ef0 100644 --- a/pages/src/components/QuickStartSection.tsx +++ b/pages/src/components/QuickStartSection.tsx @@ -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 }) => ( -
- {block.label && ( -

{block.label}

- )} -
-
-
{block.code}
-
- + copy +
); const QuickStartSection: React.FC = () => { - const [copiedIndex, setCopiedIndex] = useState(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 ( -
- {/* Ambient glow */} -
+
+
+ {/* Header */} +
+ + {t('quickstart.sectionLabel')} + +

+ {t('quickstart.title')} +

+

+ {t('quickstart.subtitle')} +

+
-
-
-
-
-

{t('quickstart.sectionLabel')}

-

- {t('quickstart.title')} -

-

- {t('quickstart.subtitle')} -

-
- -
- {steps.map((step, stepIndex) => ( -
- {/* Connector line between steps */} - {stepIndex < steps.length - 1 && ( -
- )} - - {/* Step header */} -
-
- {step.number} -
-
-
- -

{step.title}

-
-

{step.description}

-
+ {/* Steps */} +
+ {/* Step 1 */} +
+
+
+
+ 01
- - {/* Code blocks */} -
- {step.codeBlocks.map((block, blockIdx) => ( - - ))} +
+

{t('quickstart.step1Title')}

+

{t('quickstart.step1Desc')}

- ))} + +
+
+ + +
- {/* Zero-config callout */} -
-
-

- - {t('quickstart.zeroCfgTitle')} -

-

- {t('quickstart.zeroCfgDesc')} -

-
-
{`export ANTHROPIC_BASE_URL=https://api.anthropic.com
-export ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxx
-export ANTHROPIC_MODEL=claude-opus-4-6
+          {/* Step 2 */}
+          
+
+
+
+ 02 +
+
+

{t('quickstart.step2Title')}

+

{t('quickstart.step2Desc')}

+
+
+ +
+
+ + + +
+
-${t('quickstart.commentEnvAuto')} ✨`}
+ {/* Step 3 */} +
+
+
+
+ 03 +
+
+

{t('quickstart.step3Title')}

+

{t('quickstart.step3Desc')}

+
+
+ +
+
+
+
); }; diff --git a/pages/src/components/UseCasesSection.tsx b/pages/src/components/UseCasesSection.tsx new file mode 100644 index 0000000..c115303 --- /dev/null +++ b/pages/src/components/UseCasesSection.tsx @@ -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, 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 ( +
+
+ {/* Header */} +
+ + {t('usecases.sectionLabel')} + +

+ {t('usecases.title')} +

+
+ + {/* Cards */} +
+ {useCases.map((item, i) => ( +
{ 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 */} +
+ {i === 0 && ( + + )} + {i === 1 && ( +
+ + + +
+ )} + {i === 2 && ( + + )} +
+
+

+ {item.title} +

+

+ {item.desc} +

+
+
+ ))} +
+
+
+ ); +}; + +export default UseCasesSection; diff --git a/pages/src/hooks/useResponsive.ts b/pages/src/hooks/useResponsive.ts new file mode 100644 index 0000000..77bc8b5 --- /dev/null +++ b/pages/src/hooks/useResponsive.ts @@ -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(() => 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, + }; +} diff --git a/pages/src/i18n/context.tsx b/pages/src/i18n/context.tsx index 8550291..7024d17 100644 --- a/pages/src/i18n/context.tsx +++ b/pages/src/i18n/context.tsx @@ -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 = { en, zh }; +const translations: Record = { 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'; } diff --git a/pages/src/i18n/en.ts b/pages/src/i18n/en.ts index 18c3908..c1516c8 100644 --- a/pages/src/i18n/en.ts +++ b/pages/src/i18n/en.ts @@ -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 ocr) 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 ~/.opencodereview/config.json.', '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': 'ocr review 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: --from/--to and --commit cannot be used together. When specifying --from, --to 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 ocr config 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', }; diff --git a/pages/src/i18n/ja.ts b/pages/src/i18n/ja.ts new file mode 100644 index 0000000..f81319c --- /dev/null +++ b/pages/src/i18n/ja.ts @@ -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': '(略称 ocr)は 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 設定を管理します。~/.opencodereview/config.json に保存されます。', + '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': 'ocr review は 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': '注意:--from/--to--commit は同時に使用できません。--from を指定する場合、--to も指定する必要があります。', + 'docs.viewerTitle': 'ocr viewer', + 'docs.viewerDesc': 'WebUI セッションビューアーを起動し、Web インターフェースでレビュー結果を閲覧します。', + 'docs.viewerNote': '実行後、ローカル HTTP サーバーが起動し、レビュー結果を閲覧するためのビジュアルインターフェースを提供します。', + 'docs.envTitle': 'Claude Code 統合', + 'docs.envDesc': 'すでに Claude Code ユーザーで以下の環境変数を設定済みの場合、Open Code Review は自動的に認識します。追加設定は不要です:', + 'docs.envNote': 'ocr config を使用してこれらの設定を上書きまたは補完することもできます。', + 'docs.copy': 'コピー', +}; diff --git a/pages/src/i18n/types.ts b/pages/src/i18n/types.ts index ca793fb..bacbff0 100644 --- a/pages/src/i18n/types.ts +++ b/pages/src/i18n/types.ts @@ -1,3 +1,3 @@ -export type Language = 'en' | 'zh'; +export type Language = 'en' | 'zh' | 'ja'; export type TranslationKeys = Record; diff --git a/pages/src/i18n/zh.ts b/pages/src/i18n/zh.ts index 065b44d..fc91c87 100644 --- a/pages/src/i18n/zh.ts +++ b/pages/src/i18n/zh.ts @@ -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': '(简称 ocr)是一个 AI 驱动的代码评审 CLI 工具。', - 'docs.overviewFeatures': '核心特性:', - 'docs.overviewFeat1': '支持工作区变更、分支对比、单提交三种评审模式', + 'docs.overviewDesc': '(简称 ocr)是一款 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 的配置设置,存储在 ~/.opencodereview/config.json。', + 'docs.configDesc': '管理 CLI 配置,存储在 ~/.opencodereview/config.json。', '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': 'ocr review 是核心命令,用于发起 AI 代码评审。', - 'docs.reviewModes': '评审模式', + 'docs.reviewDesc': 'ocr review 是发起 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': '注意:--from/--to--commit 不可同时使用。指定 --from 时必须同时指定 --to。', - + 'docs.reviewNote': '注意:--from/--to--commit 不能同时使用。指定 --from 时,--to 也必须指定。', 'docs.viewerTitle': 'ocr viewer', - 'docs.viewerDesc': '启动 WebUI 会话查看器,用于以网页形式浏览评审结果。', - 'docs.viewerNote': '运行后会在本地启动一个 HTTP 服务器,提供可视化的评审结果浏览界面。', - - 'docs.envTitle': '适配 Claude Code', - 'docs.envDesc': '如果你已经是 Claude Code 用户并配置了以下环境变量,Open Code Review 会自动识别,无需任何额外配置:', - 'docs.envNote': '此外,工具也支持通过 ocr config 命令来覆盖或补充这些配置。', + 'docs.viewerDesc': '启动 WebUI 会话查看器,在 Web 界面中浏览审查结果。', + 'docs.viewerNote': '运行后将启动一个本地 HTTP 服务器,提供可视化界面浏览审查结果。', + 'docs.envTitle': 'Claude Code 集成', + 'docs.envDesc': '如果您已经是 Claude Code 用户并配置了以下环境变量,Open Code Review 将自动识别——无需额外配置:', + 'docs.envNote': '您也可以使用 ocr config 覆盖或补充这些设置。', 'docs.copy': '复制', - - // QuickStart code comments - 'quickstart.commentReview': '# 评审当前变更', - 'quickstart.commentBranch': '# 评审分支间的差异', - 'quickstart.commentCommit': '# 评审指定提交', - 'quickstart.commentEnvAuto': '# Open Code Review 自动识别这些变量', }; diff --git a/pages/src/index.tsx b/pages/src/index.tsx index 3c0df58..70557bc 100644 --- a/pages/src/index.tsx +++ b/pages/src/index.tsx @@ -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); diff --git a/pages/src/pages/BenchmarkPage.tsx b/pages/src/pages/BenchmarkPage.tsx new file mode 100644 index 0000000..12261b2 --- /dev/null +++ b/pages/src/pages/BenchmarkPage.tsx @@ -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 ( +
+ + + + +
+ +
+ ); +}; + +export default BenchmarkPage; diff --git a/pages/src/pages/DocsPage.tsx b/pages/src/pages/DocsPage.tsx index 150ee4d..9bae6c1 100644 --- a/pages/src/pages/DocsPage.tsx +++ b/pages/src/pages/DocsPage.tsx @@ -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( +
+ {message} +
, + 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 }) => ( -
-
-
{code}
-
+/* ─── Code block matching reference: black bg, 1px border, rounded 6px, copy icon right ─── */ +const CodeBlock: React.FC<{ code: string; onCopy?: () => void }> = ({ code, onCopy }) => ( +
+
+      {code}
+    
{onCopy && ( - + copy +
)}
); -const DocSection: React.FC<{ id: string; title: string; children: React.ReactNode }> = ({ id, title, children }) => ( -
-

{title}

- {children} -
+/* ─── Icon box (32x32, rgba(255,255,255,0.04) bg, rounded 6px) ─── */ +const IconBox: React.FC<{ icon: string }> = ({ icon }) => ( +
+ +
); const DocsPage: React.FC = () => { const [activeSection, setActiveSection] = useState('overview'); - const [mobileTocOpen, setMobileTocOpen] = useState(false); - const [copiedIndex, setCopiedIndex] = useState(null); + const [toastVisible, setToastVisible] = useState(false); const lockedRef = useRef(null); + const unlockTimerRef = useRef>(); 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>(); - 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 ( -
-
-
-
+
- + {/* Main layout: content + right sidebar */} +
+ {/* Main content area */} +
+
+ {/* Page title "Docs" */} +
+

{t('navbar.docs')}

+
- {/* Mobile TOC toggle */} -
- -
+ {/* ─── Overview ─── */} +
+

{t('docs.overviewTitle')}

+

+ Open Code Review {t('docs.overviewDesc').replace(/<\/?code>/g, '')} +

+

+ {t('docs.overviewFeatures')} +

+
+ + {'✔\n✔\n✔\n✔\n✔\n✔'.split('\n').map((c, i) => {c}
)} +
+ + {t('docs.overviewFeat1')}
+ {t('docs.overviewFeat2')}
+ {t('docs.overviewFeat3')}
+ {t('docs.overviewFeat4')}
+ {t('docs.overviewFeat5')}
+ {t('docs.overviewFeat6')} +
+
+
- {/* Mobile TOC dropdown */} - {mobileTocOpen && ( -
setMobileTocOpen(false)}> -
e.stopPropagation()} - > -
    - {sections.map((s) => ( -
  • - -
  • - ))} -
+ {/* ─── Install ─── */} +
+

{t('docs.installTitle')}

+ {/* Install item */} +
+
+ + {t('docs.installLabel')} +
+ handleCopy('npm i -g @alibaba-group/open-code-review')} /> +
+ {/* Verify item */} +
+
+ + {t('docs.installVerifyLabel')} +
+ handleCopy('ocr version')} /> +
+
+ + {/* ─── Configuration & Verification ─── */} +
+

{t('docs.configTitle')}

+

{t('docs.configDesc').replace(/<\/?code>/g, '')}

+ +

{t('docs.configInteractive')}

+

{t('docs.configInteractiveDesc')}

+ handleCopy('ocr config provider')} /> + +

{t('docs.configModelSelect')}

+

{t('docs.configModelSelectDesc')}

+ handleCopy('ocr config model')} /> + +

{t('docs.configListProviders')}

+

{t('docs.configListProvidersDesc')}

+ handleCopy('ocr llm providers')} /> + +

{t('docs.configManual')}

+

{t('docs.configCommand')}

+ '} /> + +

{t('docs.configExample')}

+ 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`)} + /> + +

{t('docs.configKeys')}

+
+ {/* 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) => ( +
+ {row.map(({ key, desc: d }) => ( +
+

+ {key} + {d} +

+
+ ))} +
+ ))} +
+ +

{t('docs.configVerify')}

+ handleCopy('ocr llm test')} + /> +

{t('docs.configVerifyDesc')}

+
+ + {/* ─── ocr review ─── */} +
+

{t('docs.reviewTitle')}

+

{t('docs.reviewDesc').replace(/<\/?code>/g, '')}

+ +

{t('docs.reviewModes')}

+ {/* Workspace Mode */} +
+
+ +
+ {t('docs.reviewWorkspace')} +

{t('docs.reviewWorkspaceDesc')}

+
+
+ handleCopy('ocr review')} /> +
+ {/* Branch Diff Mode */} +
+
+ +
+ {t('docs.reviewBranch')} +

{t('docs.reviewBranchDesc')}

+
+
+ handleCopy('ocr review --from master --to dev-ref')} /> +
+ {/* Single Commit Mode */} +
+
+ +
+ {t('docs.reviewCommit')} +

{t('docs.reviewCommitDesc')}

+
+
+ handleCopy('ocr review -c abc123')} /> +
+ +

{t('docs.reviewAdvanced')}

+ {/* Review with Requirement Context */} +
+
+ +
+ {t('docs.reviewBackground')} +

{t('docs.reviewBackgroundDesc')}

+
+
+ handleCopy('ocr review --background "requirement context"')} /> +
+ {/* JSON Output */} +
+
+ +
+ {t('docs.reviewJson')} +

{t('docs.reviewJsonDesc')}

+
+
+ handleCopy('ocr review --format json')} /> +
+ {/* Agent Mode */} +
+
+ +
+ {t('docs.reviewAgent')} +

{t('docs.reviewAgentDesc')}

+
+
+ handleCopy('ocr review --audience agent')} /> +
+ +

{t('docs.reviewFlags')}

+ {/* Flags table */} +
+ {/* Header */} +
+
{t('docs.reviewFlagCol1')}
+
{t('docs.reviewFlagCol2')}
+
{t('docs.reviewFlagCol3')}
+
+ {/* 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) => ( +
+
+ {flag} +
+
+ {d} +
+
+ {def} +
+
+ ))} +
+

+ {t('docs.reviewNote').replace(/<\/?code>/g, '')} +

+
+ + {/* ─── Viewer ─── */} +
+

{t('docs.viewerTitle')}

+

{t('docs.viewerDesc')}

+ handleCopy('ocr viewer')} /> +

{t('docs.viewerNote')}

+
+ + {/* ─── Claude Code Integration ─── */} +
+

{t('docs.envTitle')}

+

+ {t('docs.envDesc').replace(/<\/?code>/g, '')} +

+ handleCopy('export ANTHROPIC_BASE_URL=https://api.anthropic.com\nexport ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxx\nexport ANTHROPIC_MODEL=claude-opus-4-6')} + /> +

+ {t('docs.envNote').replace(/<\/?code>/g, '')} +

+
- )} -
- {/* Sidebar TOC — desktop */} - - - {/* Main content */} -
- {/* Overview */} - -

- Open Code Review{' '} - -

-
-

{t('docs.overviewFeatures')}

-
    - {(['docs.overviewFeat1', 'docs.overviewFeat2', 'docs.overviewFeat3', 'docs.overviewFeat4', 'docs.overviewFeat5', 'docs.overviewFeat6'] as const).map((key) => ( -
  • {t(key)}
  • - ))} -
-
-
- - {/* Install */} - -
-
-

- - {t('docs.installLabel')} -

- handleCopy('npm i -g @alibaba-group/open-code-review', 'install')} - copyLabel={t('docs.copy')} - /> -
-
-

- - {t('docs.installVerifyLabel')} -

- handleCopy('ocr version', 'install-verify')} - copyLabel={t('docs.copy')} - /> -
-
-
- - {/* Config */} - -

- -

{t('docs.configInteractive')}

-

{t('docs.configInteractiveDesc')}

-
- handleCopy('ocr config provider', 'config-provider')} - copyLabel={t('docs.copy')} - /> -
-
-

{t('docs.configModelSelect')}

-

{t('docs.configModelSelectDesc')}

- handleCopy('ocr config model', 'config-model')} - copyLabel={t('docs.copy')} - /> -
-
-

{t('docs.configListProviders')}

-

{t('docs.configListProvidersDesc')}

- handleCopy('ocr llm providers', 'config-providers-list')} - copyLabel={t('docs.copy')} - /> -
- -

{t('docs.configManual')}

- -

{t('docs.configCommand')}

- - -

{t('docs.configExample')}

-
- 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')} - /> -
- -

{t('docs.configKeys')}

-
- {[ - { 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 }) => ( -
- {key} - {t(descKey)} -
))}
- -

{t('docs.configVerify')}

- -

- {t('docs.configVerifyDesc')} -

-
- - {/* Review */} - -

- -

{t('docs.reviewModes')}

-
-
-

- - {t('docs.reviewWorkspace')} -

-

{t('docs.reviewWorkspaceDesc')}

- -
-
-

- - {t('docs.reviewBranch')} -

-

{t('docs.reviewBranchDesc')}

- handleCopy('ocr review --from master --to dev-ref', 'review-branch')} - copyLabel={t('docs.copy')} - /> -
-
-

- - {t('docs.reviewCommit')} -

-

{t('docs.reviewCommitDesc')}

- handleCopy('ocr review -c abc123', 'review-commit')} - copyLabel={t('docs.copy')} - /> -
-
- -

{t('docs.reviewAdvanced')}

-
-
-

- - {t('docs.reviewBackground')} -

-

{t('docs.reviewBackgroundDesc')}

- handleCopy('ocr review --background "requirement context"', 'review-background')} - copyLabel={t('docs.copy')} - /> -
-
-

- - {t('docs.reviewJson')} -

-

{t('docs.reviewJsonDesc')}

- handleCopy('ocr review --format json', 'review-json')} - copyLabel={t('docs.copy')} - /> -
-
-

- - {t('docs.reviewAgent')} -

-

{t('docs.reviewAgentDesc')}

- handleCopy('ocr review --audience agent', 'review-agent')} - copyLabel={t('docs.copy')} - /> -
-
- -

{t('docs.reviewFlags')}

-
- - - - - - - - - - {[ - ['-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]) => ( - - - - - - ))} - -
{t('docs.reviewFlagCol1')}{t('docs.reviewFlagCol2')}{t('docs.reviewFlagCol3')}
{flag}{desc}{def || '—'}
-
-

- - - {/* Viewer */} - -

- {t('docs.viewerDesc')} -

- - -

- {t('docs.viewerNote')} -

-
- - {/* Environment variables */} - -

- -

- - - {/* Footer spacer */} -

-
+
+ )}
+
+
); }; diff --git a/pages/src/pages/FeaturesPage.tsx b/pages/src/pages/FeaturesPage.tsx new file mode 100644 index 0000000..74068b0 --- /dev/null +++ b/pages/src/pages/FeaturesPage.tsx @@ -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 ( + <> + + + + + + + + + + + + + + + + + + + +