Files
CamTalk/frontend/src/components/LandingPage/index.tsx

391 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// LandingPage — 官网首页(含登录弹窗)
// 职责:未登录用户的落地页,展示产品介绍并提供登录/注册入口
// ============================================================
import { useCallback, useEffect, useRef, useState } from "react";
import { LoginModal } from "./LoginModal";
import "./LandingPage.css";
type ModalMode = "login" | "register";
export function LandingPage() {
const [modal, setModal] = useState<{ open: boolean; mode: ModalMode }>({
open: false,
mode: "login",
});
const openModal = useCallback((mode: ModalMode) => {
setModal({ open: true, mode });
}, []);
const closeModal = useCallback(() => {
setModal((prev) => ({ ...prev, open: false }));
}, []);
// ---- Scroll Reveal ----
const observerRef = useRef<IntersectionObserver | null>(null);
useEffect(() => {
observerRef.current = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("lp-visible");
}
});
},
{ threshold: 0.1, rootMargin: "0px 0px -40px 0px" }
);
document.querySelectorAll(".lp-fade-in").forEach((el) => {
observerRef.current?.observe(el);
});
return () => observerRef.current?.disconnect();
}, []);
return (
<div className="landing-page">
<div className="lp-bg-grid" />
{/* ========== Top Navigation ========== */}
<nav className="lp-nav">
<div className="lp-nav__logo">CamTalk</div>
<div className="lp-nav__links">
<a href="#problem"></a>
<a href="#features"></a>
<a href="#scenes"></a>
<a href="#tech"></a>
</div>
<div className="lp-nav__actions">
<button type="button" className="lp-nav__btn lp-nav__btn--ghost" onClick={() => openModal("login")}>
</button>
<button type="button" className="lp-nav__btn lp-nav__btn--primary" onClick={() => openModal("register")}>
</button>
</div>
</nav>
{/* ========== HERO ========== */}
<section className="lp-hero">
<div className="lp-container lp-hero-content">
<div className="lp-hero-badge">
<span className="lp-hero-badge__dot" />
<span>XEngineers</span>
</div>
<h1>
<span className="lp-gradient">CamTalk</span>
<br />
AI
</h1>
<p className="lp-hero__sub">
AI AI
</p>
<div className="lp-hero-actions">
<button type="button" className="lp-cta-btn" onClick={() => openModal("register")}>
CamTalk
</button>
<div className="lp-hero-actions__links">
<a href="https://www.bilibili.com/video/BV1dDJK6cE5S/" target="_blank" rel="noreferrer">
</a>
<a href="https://github.com/XEngineers/CamTalk" target="_blank" rel="noreferrer">
GitHub
</a>
</div>
</div>
{/* Device Mockup - Double Bezel */}
<div className="lp-hero-visual">
<div className="lp-hero-visual__inner">
<div className="lp-hero-visual__screen">
<div className="lp-screen-left">
<div className="lp-scan-line" />
<div className="lp-camera-ring">
<svg viewBox="0 0 24 24">
<path d="M23 7l-7 5 7 5V7z" />
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
</svg>
</div>
</div>
<div className="lp-screen-right">
<div className="lp-chat-bubble lp-chat-bubble--user"></div>
<div className="lp-chat-bubble lp-chat-bubble--ai">
x² - 5x + 6 = 0使
<div className="lp-typing">
<span /><span /><span />
</div>
</div>
<div className="lp-chat-bubble lp-chat-bubble--user"></div>
<div className="lp-audio-wave">
<span /><span /><span /><span /><span /><span /><span />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* ========== PROBLEM ========== */}
<section id="problem">
<div className="lp-container">
<div className="lp-section-header lp-fade-in">
<div className="lp-section-header__tag"></div>
<h2>AI </h2>
<p> AI </p>
</div>
<div className="lp-problem-grid">
<div className="lp-problem-card lp-fade-in">
<div className="lp-problem-card__icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
</svg>
</div>
<h3></h3>
<p>AI </p>
</div>
<div className="lp-problem-card lp-fade-in">
<div className="lp-problem-card__icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/>
</svg>
</div>
<h3></h3>
<p>AI </p>
</div>
<div className="lp-problem-card lp-fade-in">
<div className="lp-problem-card__icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
</div>
<h3></h3>
<p> + API $5,000</p>
</div>
</div>
</div>
</section>
{/* ========== SOLUTION ========== */}
<section id="solution">
<div className="lp-container">
<div className="lp-section-header lp-fade-in">
<h2> + + = AI</h2>
<p>CamTalk </p>
</div>
<div className="lp-flow-wrapper lp-fade-in">
<div className="lp-flow-steps">
{[
{ icon: "📷", label: "摄像头采集" },
{ icon: "🧠", label: "边缘预处理" },
{ icon: "🔌", label: "WebSocket" },
{ icon: "⚡", label: "Eino 编排" },
{ icon: "👁️", label: "视觉理解" },
{ icon: "💬", label: "流式回复" },
{ icon: "🔊", label: "语音输出" },
].map((step, i, arr) => (
<div key={i} style={{ display: "contents" }}>
<div className="lp-flow-step">
<div className="lp-flow-step__node">{step.icon}</div>
<div className="lp-flow-step__label">{step.label}</div>
</div>
{i < arr.length - 1 && <div className="lp-flow-arrow"></div>}
</div>
))}
</div>
</div>
</div>
</section>
{/* ========== FEATURES ========== */}
<section id="features">
<div className="lp-container">
<div className="lp-section-header lp-fade-in">
<div className="lp-section-header__tag"></div>
<h2></h2>
<p></p>
</div>
<div className="lp-features-grid">
{[
{
num: "01", icon: "⚡",
title: "流式并行推送",
desc: "LLM 文本流与 TTS 音频流并行输出。用户先看到文字,紧接着听到语音,感知延迟低于 0.5 秒,接近真人对话节奏。",
},
{
num: "02", icon: "🧠",
title: "声明式 AI 编排",
desc: "基于 CloudWeGo Eino Graph 的 7 节点 DAG 流水线STT → History → ChatModel → Splitter → TTS类型安全、可扩展、易测试。",
},
{
num: "03", icon: "💰",
title: "端云协同降本",
desc: "浏览器端 VAD 语音检测 + 关键帧像素比较 + 混合采样策略,节省 70% 带宽,月成本从 $5,000 降至 $300降幅 90%。",
},
{
num: "04", icon: "🎯",
title: "多场景智能模式",
desc: "5 种 AI 角色(自由对话 / 模拟面试 / 英语老师 / 辩论对手 / 同声翻译)× 3 种视觉模式 × 观察模式,灵活覆盖学习与工作。",
},
{
num: "05", icon: "🏗️",
title: "生产级工程架构",
desc: "三级存储自动降级Memory → Redis → PostgreSQL、JWT 双 token 认证、Docker Compose 一键部署、完善的错误处理与降级策略。",
},
].map((f) => (
<div className="lp-feature-card lp-fade-in" key={f.num}>
<div className="lp-feature-card__number">{f.num}</div>
<div className="lp-feature-card__icon">
{f.icon}
</div>
<h3>{f.title}</h3>
<p>{f.desc}</p>
</div>
))}
</div>
</div>
</section>
{/* ========== USERS ========== */}
<section id="users">
<div className="lp-container">
<div className="lp-section-header lp-fade-in">
<h2></h2>
<p> AI </p>
</div>
<div className="lp-users-grid">
{[
{ avatar: "🧑‍🎓", title: "语言学习者", desc: "对着课本或实物,与 AI 英语外教用英语自由对话,实时纠正语法和发音" },
{ avatar: "💼", title: "面试准备者", desc: "开启模拟面试模式AI 面试官通过摄像头观察你的表情与状态,给出针对性反馈" },
{ avatar: "🌍", title: "跨境交流者", desc: "出国旅行时对着外文菜单、路牌实时翻译AI 语音播报翻译结果" },
{ avatar: "👁️", title: "视障人士", desc: "AI 实时描述摄像头画面中的环境、障碍物和文字,提供无障碍信息辅助" },
{ avatar: "🔬", title: "学生 / 教师", desc: "对着题目问「怎么做AI 看到画面后逐步讲解,就像身边有一位私教" },
].map((u) => (
<div className="lp-user-card lp-fade-in" key={u.title}>
<div className="lp-user-card__avatar">{u.avatar}</div>
<h4>{u.title}</h4>
<p>{u.desc}</p>
</div>
))}
</div>
</div>
</section>
{/* ========== SCENES ========== */}
<section id="scenes">
<div className="lp-container">
<div className="lp-section-header lp-fade-in">
<h2></h2>
</div>
<div className="lp-scenes-list">
{[
{ icon: "💬", title: "自由对话", desc: "对着摄像头随意聊天AI 实时理解画面并语音回答", tag: "通用" },
{ icon: "🗣️", title: "英语老师", desc: "AI 外教结合摄像头场景进行英语口语教学,实时纠正语法", tag: "学习" },
{ icon: "🎤", title: "模拟面试", desc: "AI 面试官根据你的回答追问,通过摄像头观察你的表现", tag: "求职" },
{ icon: "⚔️", title: "辩论对手", desc: "AI 反驳你的观点,锻炼你的逻辑思维和表达能力", tag: "思维" },
{ icon: "🌐", title: "同声翻译", desc: "实时识别画面中的外语文字并语音翻译,口语化输出", tag: "工具" },
].map((s) => (
<div className="lp-scene-row lp-fade-in" key={s.title}>
<div className="lp-scene-row__icon">{s.icon}</div>
<div>
<h4>{s.title}</h4>
<div className="lp-scene-row__desc">{s.desc}</div>
</div>
<div className="lp-scene-row__tag">
{s.tag}
</div>
</div>
))}
</div>
</div>
</section>
{/* ========== METRICS ========== */}
<section id="metrics">
<div className="lp-container">
<div className="lp-section-header lp-fade-in">
<h2></h2>
</div>
<div className="lp-metrics-grid">
{[
{ value: "< 2s", label: "端到端响应延迟" },
{ value: "90%", label: "API 成本降幅" },
{ value: "70%", label: "带宽节省率" },
{ value: "5+3", label: "场景 × 视觉模式" },
].map((m) => (
<div className="lp-metric-card lp-fade-in" key={m.label}>
<div className="lp-metric-card__value">{m.value}</div>
<div className="lp-metric-card__label">{m.label}</div>
</div>
))}
</div>
</div>
</section>
{/* ========== TECH STACK ========== */}
<section id="tech">
<div className="lp-container">
<div className="lp-section-header lp-fade-in">
<h2></h2>
<p> Go AI </p>
</div>
<div className="lp-tech-layers">
{[
{ badge: "前端层", tags: ["React 18", "TypeScript", "Vite", "WebRTC VAD", "Canvas 关键帧检测", "WebSocket", "i18n (中/英/日)"] },
{ badge: "网关层", tags: ["Go + Gin", "gorilla/websocket", "Eino Graph", "JWT 双 Token", "Zap 日志", "Viper 配置"] },
{ badge: "存储层", tags: ["L1 Memory", "L2 Redis", "L3 PostgreSQL", "TieredManager 自动降级"] },
{ badge: "AI 服务", tags: ["qwen3-vl-plus (LLM)", "MiMo ASR (STT)", "MiMo TTS", "Docker Compose"] },
].map((layer) => (
<div className="lp-tech-layer lp-fade-in" key={layer.badge}>
<div>
<div className="lp-tech-layer__badge">
{layer.badge}
</div>
</div>
<div className="lp-tech-layer__tags">
{layer.tags.map((tag) => <span key={tag}>{tag}</span>)}
</div>
</div>
))}
</div>
</div>
</section>
{/* ========== CTA ========== */}
<section className="lp-cta-section">
<div className="lp-container lp-fade-in">
<h2> AI </h2>
<p>CamTalk</p>
<button type="button" className="lp-cta-btn" onClick={() => openModal("register")}>
CamTalk
</button>
<div className="lp-cta-links">
<a href="https://www.bilibili.com/video/BV1dDJK6cE5S/" target="_blank" rel="noreferrer">
</a>
<a href="https://github.com/XEngineers/CamTalk" target="_blank" rel="noreferrer">
GitHub
</a>
</div>
</div>
</section>
{/* ========== Footer ========== */}
<footer className="lp-footer">
<div className="lp-container">
CamTalk © 2026 XEngineers
</div>
</footer>
{/* ========== Login Modal ========== */}
{modal.open && (
<LoginModal initialMode={modal.mode} onClose={closeModal} />
)}
</div>
);
}