feat: redesign showcase section with clickable demo cards

Replace static showcase layout with interactive demo grid featuring 4 real example cases (complex formulas, distorted text, handwriting, text layout). Add demo image loading via sessionStorage when users click "Try it now". New CSS grid with hover effects, overlay buttons, and responsive design. Update testimonials with new user feedback.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 01:22:41 +08:00
parent 6e4df89b23
commit b761f4ccbf
9 changed files with 302 additions and 90 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
public/fomula_demo/mix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,8 +1,53 @@
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useLanguage } from '../../contexts/LanguageContext';
const DEMO_CASES = [
{
key: 'complex',
src: '/fomula_demo/complex.png',
tag: { zh: '复杂公式', en: 'Complex' },
title: { zh: '嵌套数学公式', en: 'Nested Math' },
desc: { zh: 'arXiv 论文截图', en: 'arXiv paper screenshot' },
color: 'var(--primary)',
colorBg: 'rgba(200,98,42,0.10)',
},
{
key: 'deformity',
src: '/fomula_demo/deformity.png',
tag: { zh: '扭曲变形', en: 'Distorted' },
title: { zh: '倾斜扫描识别', en: 'Skewed Scan' },
desc: { zh: '低质量扫描文档', en: 'Low-quality scanned doc' },
color: 'var(--teal)',
colorBg: 'rgba(140,201,190,0.13)',
},
{
key: 'maual',
src: '/fomula_demo/maual.png',
tag: { zh: '手写公式', en: 'Handwriting Formula' },
title: { zh: '手写转 LaTeX', en: 'Handwriting → LaTeX' },
desc: { zh: '手机拍摄课堂笔记', en: 'Phone-captured notes' },
color: 'var(--gold)',
colorBg: 'rgba(243,201,106,0.15)',
},
{
key: 'mix',
src: '/fomula_demo/mix.png',
tag: { zh: '文字混排', en: 'Text Layout' },
title: { zh: '文字混排识别', en: 'Text + Formula' },
desc: { zh: '论文正文截图', en: 'Paper body with equations' },
color: 'var(--lavender)',
colorBg: 'rgba(183,175,232,0.15)',
},
] as const;
export default function ShowcaseSection() {
const { language } = useLanguage();
const navigate = useNavigate();
const handleTryIt = (src: string) => {
sessionStorage.setItem('texpixel_demo_image', src);
navigate('/app');
};
return (
<section className="showcase">
@@ -14,71 +59,56 @@ export default function ShowcaseSection() {
</h2>
<p className="section-desc">
{language === 'zh'
? '看看 TexPixel 如何处理真实的论文截图与手写笔记。'
: 'See how TexPixel handles real paper screenshots and handwritten notes.'}
? '点击任意案例,直接体验 TexPixel 的识别效果。'
: 'Click any example to instantly try TexPixel on real formula images.'}
</p>
</div>
<div className="showcase-cards">
<div className="showcase-card reveal reveal-delay-1">
<div className="showcase-header">
<div className="showcase-tag">{language === 'zh' ? '复杂公式' : 'Complex Formula'}</div>
<div className="showcase-title">
{language === 'zh' ? '论文截图到可复制 LaTeX' : 'Paper Screenshot to Copyable LaTeX'}
<div className="demo-grid">
{DEMO_CASES.map((c, i) => (
<div
key={c.key}
className={`demo-card reveal reveal-delay-${i + 1}`}
>
<div className="demo-img-wrap">
<img src={c.src} alt={c.tag[language]} className="demo-img" />
<div className="demo-img-overlay">
<button
className="demo-try-btn"
onClick={() => handleTryIt(c.src)}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
<path d="M5 3l14 9-14 9V3z" />
</svg>
{language === 'zh' ? '立即试用' : 'Try it now'}
</button>
</div>
</div>
<div className="showcase-sub">arXiv PDF · 0.41s</div>
</div>
<div className="showcase-body">
<div className="sc-input">
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '15px', opacity: 0.6 }}>
¹/² · · · [{language === 'zh' ? '图片占位' : 'image placeholder'}]
</span>
</div>
<div className="sc-arrow"></div>
<div
className="sc-output"
dangerouslySetInnerHTML={{
__html: '<span class="kw">\\sum</span>_{i=<span class="num">1</span>}^{n}<span class="kw">\\frac</span>{<span class="num">1</span>}{i^<span class="num">2</span>}',
}}
/>
</div>
<div className="showcase-footer">
<Link to="/app" className="btn btn-secondary" style={{ height: '44px', fontSize: '14px', padding: '0 20px' }}>
{language === 'zh' ? '试试这个例子 →' : 'Try this example →'}
</Link>
</div>
</div>
<div className="showcase-card reveal reveal-delay-2">
<div className="showcase-header">
<div className="showcase-tag" style={{ background: 'rgba(140,201,190,0.15)', color: 'var(--teal)' }}>
{language === 'zh' ? '手写公式' : 'Handwritten Formula'}
</div>
<div className="showcase-title">
{language === 'zh' ? '课堂笔记到标准表达式' : 'Classroom Notes to Standard Expression'}
</div>
<div className="showcase-sub">{language === 'zh' ? '手机拍摄笔记 · 0.38s' : 'Phone photo of notes · 0.38s'}</div>
</div>
<div className="showcase-body">
<div className="sc-input">
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '15px', opacity: 0.6 }}>
lim sin(x)/x [{language === 'zh' ? '手写' : 'handwritten'}]
<div className="demo-meta">
<span
className="demo-tag"
style={{ color: c.color, background: c.colorBg }}
>
{c.tag[language]}
</span>
<div className="demo-title">{c.title[language]}</div>
<div className="demo-desc">{c.desc[language]}</div>
</div>
<div className="demo-footer">
<button
className="demo-cta"
onClick={() => handleTryIt(c.src)}
>
{language === 'zh' ? '试试这个例子' : 'Try this example'}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
</button>
</div>
<div className="sc-arrow"></div>
<div
className="sc-output"
dangerouslySetInnerHTML={{
__html: '<span class="kw">\\lim</span>_{x<span class="br">\\to</span> <span class="num">0</span>}<span class="kw">\\frac</span>{<span class="kw">\\sin</span> x}{x}',
}}
/>
</div>
<div className="showcase-footer">
<Link to="/app" className="btn btn-secondary" style={{ height: '44px', fontSize: '14px', padding: '0 20px' }}>
{language === 'zh' ? '试试这个例子 →' : 'Try this example →'}
</Link>
</div>
</div>
))}
</div>
</div>
</section>

View File

@@ -4,58 +4,67 @@ import { useLanguage } from '../../contexts/LanguageContext';
const TESTIMONIALS = [
{
quote: '写论文的时候截图粘进去LaTeX 就出来了。以前每个公式都要手敲,现在一个截图解决,省了我大概 40% 的时间。',
name: '林同学',
role: '数学系研究生 · 北京大学',
name: '纪**',
role: '研究生 · 上海交通大学',
avatarBg: 'var(--teal)',
avatarColor: '#1a5c54',
avatarLetter: '',
avatarLetter: '',
stars: '★★★★★',
},
{
quote: '手写推导拍一张,马上就是干净的 LaTeX。对我这种每天要整理大量笔记的人来说真的是刚需级别的工具。',
name: '田晓雯',
role: '物理学博士候选人 · 清华大学',
name: '李**',
role: '研究生 · 北京航空航天大学',
avatarBg: 'var(--lavender)',
avatarColor: '#3d3870',
avatarLetter: '',
avatarLetter: '',
stars: '★★★★★',
},
{
quote: "I use it every day for my thesis. The accuracy on complex integrals and matrix expressions is surprisingly good — way better than anything I've tried before.",
name: 'Sarah M.',
role: 'Applied Math PhD · MIT',
name: 'E. ***',
role: 'Graduate Student · National University of Singapore',
avatarBg: 'var(--rose)',
avatarColor: '#7a2e1e',
avatarLetter: 'S',
avatarLetter: 'E',
stars: '★★★★★',
},
{
quote: 'Desktop 版的离线功能对我来说很重要,论文数据不想上传到云端。买断价格也合理,不用每个月担心订阅。',
name: '陈博士',
role: '生物信息学研究员 · 中科院',
quote: '教材扫描件里的公式以前完全没法用,现在截图一框就搞定。连复杂数学公式都能识别,超出我的预期。',
name: '孟**',
role: '本科生 · 同济大学',
avatarBg: 'var(--gold)',
avatarColor: '#6f5800',
avatarLetter: '',
avatarLetter: '',
stars: '★★★★☆',
},
{
quote: '做毕设推导的时候,把草稿纸拍照上传,几秒就能得到完整的 LaTeX 代码,再也不用对着键盘一个符号一个符号敲了。',
name: '任**',
role: '本科生 · 天津大学',
avatarBg: '#A8C5A0',
avatarColor: '#1e4a18',
avatarLetter: '任',
stars: '★★★★★',
},
{
quote: 'The browser extension is a game changer. I copy equations from Claude or ChatGPT straight into Word as native math — no more reformatting nightmares.',
name: 'Alex K.',
role: 'Engineering Student · TU Berlin',
quote: '识别精度真的超预期,矩阵、积分、偏导数全都能准确处理,我们组里几个同学现在都在用。',
name: '姚**',
role: '研究生 · 中山大学',
avatarBg: '#B8A9D9',
avatarColor: '#2e1e5c',
avatarLetter: '姚',
stars: '★★★★★',
},
{
quote: '科研文档里经常遇到各种冷门符号,之前其他工具基本认不出来,这个都能处理,识别结果直接可用。',
name: '肖**',
role: '研究生 · 国防科技大学',
avatarBg: '#8CB4C9',
avatarColor: '#1a3d52',
avatarLetter: 'A',
avatarLetter: '',
stars: '★★★★★',
},
{
quote: '教材扫描件里的公式以前完全没法用,现在截图一框就搞定。连化学方程式都能识别,超出我的预期。',
name: '王梓涵',
role: '化学工程本科 · 浙江大学',
avatarBg: '#C9A88C',
avatarColor: '#4a2e14',
avatarLetter: '王',
stars: '★★★★☆',
},
];
const VISIBLE = 3;
@@ -116,7 +125,7 @@ export default function TestimonialsSection() {
{language === 'zh' ? '全球学生都在用' : 'Loved by students worldwide'}
</h2>
<p className="section-desc">
{language === 'zh' ? '来自真实用户的反馈。' : 'Feedback from real users.'}
{language === 'zh' ? '来自用户的使用体验分享。' : 'What users are saying about their experience.'}
</p>
</div>

View File

@@ -21,15 +21,11 @@ export default function HomePage() {
path="/"
/>
<HeroSection />
<div className="section-divider" />
<ProductSuiteSection />
<FeaturesSection />
<ShowcaseSection />
<div className="section-divider" />
<TestimonialsSection />
<div className="section-divider" />
<PricingSection />
<div className="section-divider" />
<DocsSeoSection />
</>
);

View File

@@ -45,6 +45,7 @@ export default function WorkspacePage() {
const selectedFileIdRef = useRef<string | null>(null);
const resultsCache = useRef<Record<string, RecognitionResult>>({});
const hasLoadedFiles = useRef(false);
const demoLoadedRef = useRef(false);
const selectedFile = files.find((f) => f.id === selectedFileId) || null;
const canUploadAnonymously = !user && guestUsageCount < GUEST_USAGE_LIMIT;
@@ -74,6 +75,26 @@ export default function WorkspacePage() {
return () => window.removeEventListener('start-user-guide', handleStartGuide);
}, []);
useEffect(() => {
if (initializing) return;
if (demoLoadedRef.current) return;
const demoImageSrc = sessionStorage.getItem('texpixel_demo_image');
if (!demoImageSrc) return;
demoLoadedRef.current = true;
sessionStorage.removeItem('texpixel_demo_image');
fetch(demoImageSrc)
.then((res) => res.blob())
.then((blob) => {
const fileName = demoImageSrc.split('/').pop() || 'demo.png';
const file = new File([blob], fileName, { type: blob.type || 'image/png' });
handleUpload([file]);
})
.catch((err) => console.error('Failed to load demo image:', err));
}, [initializing]);
useEffect(() => {
if (!initializing && user && !hasLoadedFiles.current) {
hasLoadedFiles.current = true;

View File

@@ -1029,6 +1029,158 @@
padding: 0 28px 28px;
}
/* ── DEMO GRID ── */
.demo-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 20px;
margin-top: 12px;
}
.demo-card {
background: var(--elevated);
border: 1.5px solid var(--border);
border-radius: var(--r-xl);
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-soft);
transition: box-shadow 0.25s, transform 0.25s;
}
.demo-card:hover {
box-shadow: var(--shadow-float);
transform: translateY(-3px);
}
.demo-img-wrap {
position: relative;
overflow: hidden;
background: #f7f3ef;
}
.demo-img {
width: 100%;
height: auto;
display: block;
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.demo-card:hover .demo-img {
transform: scale(1.02);
}
.demo-img-overlay {
position: absolute;
inset: 0;
background: rgba(31, 26, 23, 0.45);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.25s;
backdrop-filter: blur(2px);
}
.demo-card:hover .demo-img-overlay {
opacity: 1;
}
.demo-try-btn {
display: inline-flex;
align-items: center;
gap: 7px;
background: white;
color: var(--text-strong);
border: none;
border-radius: var(--r-pill);
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
transition: transform 0.15s, box-shadow 0.15s;
}
.demo-try-btn:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(0,0,0,0.22);
}
.demo-meta {
padding: 12px 14px 6px;
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.demo-tag {
display: inline-block;
padding: 2px 8px;
border-radius: var(--r-pill);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.3px;
align-self: flex-start;
white-space: nowrap;
}
.demo-title {
font-family: 'Lora', serif;
font-size: 13px;
font-weight: 700;
color: var(--text-strong);
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.demo-desc {
font-size: 11.5px;
color: var(--text-muted);
line-height: 1.45;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.demo-footer {
padding: 8px 14px 14px;
border-top: 1px solid var(--border);
}
.demo-cta {
display: inline-flex;
align-items: center;
gap: 5px;
background: none;
border: none;
color: var(--primary);
font-size: 12px;
font-weight: 600;
cursor: pointer;
padding: 0;
transition: gap 0.15s;
}
.demo-cta:hover {
gap: 9px;
}
@media (max-width: 900px) {
.demo-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 560px) {
.demo-grid {
grid-template-columns: 1fr;
}
}
/* ── USER LOVE ── */
.user-love {
padding: 96px 0;
@@ -1808,7 +1960,7 @@
.testimonial-track {
display: grid;
grid-template-columns: repeat(6, calc(33.333% - 14px));
grid-template-columns: repeat(7, calc(33.333% - 14px));
gap: 20px;
transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -1959,6 +2111,10 @@
transition-delay: 0.30s;
}
.reveal-delay-4 {
transition-delay: 0.40s;
}
@keyframes blink {
0%,