feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -14,11 +14,17 @@ export function SectionHeader({ index, title, subtitle, className = '' }: Sectio
|
|||
</span>
|
||||
<div className="h-px flex-1 bg-white/[0.06]" />
|
||||
</div>
|
||||
<h2 className="font-display text-display font-bold text-neutral-50 tracking-tight">
|
||||
<h2
|
||||
className="font-display text-display font-bold text-neutral-50 tracking-tight break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="mt-3 max-w-xl text-base text-neutral-400 leading-relaxed">
|
||||
<p
|
||||
className="mt-3 max-w-xl text-base text-neutral-400 leading-relaxed break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -10,17 +10,17 @@ export const en: Translations = {
|
|||
download: 'Download',
|
||||
},
|
||||
hero: {
|
||||
badge: '100% LOCAL \u00b7 ZERO CLOUD',
|
||||
title1: 'LOCAL AI',
|
||||
title2: 'VOICE',
|
||||
title3: 'ASSISTANT.',
|
||||
subtitle: 'Whisper + Ollama powered voice-to-text pipeline running entirely on your machine. Dictation, AI polish, real-time captions, voice conversations \u2014 no internet required.',
|
||||
downloadBtn: 'Download for Windows',
|
||||
viewFeatures: 'View Features',
|
||||
badge: '100% ON-DEVICE · ZERO CLOUD',
|
||||
title1: 'Speak at the Speed of Thought.',
|
||||
title2: 'AI Polishes the Rest.',
|
||||
title3: 'D3RO Voice.',
|
||||
subtitle: '3x faster than typing. Whisper + Ollama voice-to-text pipeline running 100% on your machine. Zero cloud egress, unlimited dictation, and real-time meeting notes.',
|
||||
downloadBtn: 'Download for Windows — Free',
|
||||
viewFeatures: 'Explore Features',
|
||||
dataProcessing: 'Processing',
|
||||
dataCloudTraffic: 'Cloud Traffic',
|
||||
dataCloudTraffic: 'Cloud Egress',
|
||||
dataLatency: 'STT Latency',
|
||||
dataPrivacy: 'Privacy Score',
|
||||
dataPrivacy: 'Privacy Rating',
|
||||
systemStatus: 'SYSTEM STATUS: OPERATIONAL',
|
||||
protocol: 'PROTOCOL: 2025.04',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,17 +10,17 @@ export const ko: Translations = {
|
|||
download: '다운로드',
|
||||
},
|
||||
hero: {
|
||||
badge: '100% 로컬 \u00b7 클라우드 제로',
|
||||
title1: '로컬 AI',
|
||||
title2: '음성',
|
||||
title3: '어시스턴트.',
|
||||
subtitle: 'Whisper + Ollama 기반 음성-텍스트 파이프라인이 당신의 컴퓨터에서 완전히 작동합니다. 받아쓰기, AI 다듬기, 실시간 자막, 음성 대화 \u2014 인터넷이 필요 없습니다.',
|
||||
downloadBtn: 'Windows용 다운로드',
|
||||
viewFeatures: '기능 보기',
|
||||
badge: '100% 온디바이스 · 클라우드 제로',
|
||||
title1: '생각의 속도로 말하고,',
|
||||
title2: 'AI가 완벽히 다듬습니다.',
|
||||
title3: 'D3RO Voice.',
|
||||
subtitle: '타이핑보다 3배 빠른 100% 로컬 음성 비서. 클라우드 전송 0바이트, 무제한 받아쓰기부터 실시간 화자 분리 회의록까지 당신의 PC에서 안전하게.',
|
||||
downloadBtn: 'Windows용 무료 다운로드',
|
||||
viewFeatures: '주요 기능 둘러보기',
|
||||
dataProcessing: '처리 방식',
|
||||
dataCloudTraffic: '클라우드 트래픽',
|
||||
dataCloudTraffic: '클라우드 유출',
|
||||
dataLatency: 'STT 지연시간',
|
||||
dataPrivacy: '프라이버시 점수',
|
||||
dataPrivacy: '프라이버시 지수',
|
||||
systemStatus: '시스템 상태: 정상 가동',
|
||||
protocol: '프로토콜: 2025.04',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,31 +8,58 @@ export function CTA() {
|
|||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<section className="relative py-24 md:py-32">
|
||||
{/* Background glow */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[400px] bg-brand-amber/[0.03] rounded-full blur-[120px] pointer-events-none" />
|
||||
<section className="relative py-28 md:py-36 overflow-hidden">
|
||||
{/* Background radial glow */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[500px] bg-brand-amber/[0.045] rounded-full blur-[140px] pointer-events-none" />
|
||||
|
||||
<Container className="relative text-center">
|
||||
<Crosshair className="inline-block mb-8">
|
||||
<WaveBars className="h-8 px-2" />
|
||||
<WaveBars className="h-9 px-3" />
|
||||
</Crosshair>
|
||||
|
||||
<h2 className="font-display text-display font-bold text-neutral-50 mb-6 max-w-3xl mx-auto">
|
||||
<h2
|
||||
className="font-display text-display font-extrabold text-neutral-50 mb-6 max-w-3xl mx-auto tracking-tight break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{t.cta.title1} <span className="text-brand-amber">{t.cta.title2}</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-base md:text-lg text-neutral-400 max-w-xl mx-auto mb-10 leading-relaxed">
|
||||
<p
|
||||
className="text-base md:text-lg text-neutral-400 max-w-xl mx-auto mb-8 leading-relaxed break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{t.cta.subtitle1}
|
||||
<br />
|
||||
{t.cta.subtitle2}
|
||||
</p>
|
||||
|
||||
<GlowButton href="#" variant="primary" size="lg">
|
||||
{t.cta.downloadBtn}
|
||||
</GlowButton>
|
||||
{/* Hotkey Hint Box */}
|
||||
<div className="inline-flex items-center gap-2 mb-10 px-4 py-2 rounded-xl bg-surface-800/80 border border-white/[0.08] shadow-inner">
|
||||
<span className="font-mono text-nano text-neutral-400 uppercase tracking-widest">
|
||||
Push-to-Talk:
|
||||
</span>
|
||||
<div className="flex items-center gap-1 font-mono text-xs font-semibold text-neutral-200">
|
||||
<span className="px-2 py-0.5 rounded bg-surface-600 border border-white/[0.1]">Ctrl</span>
|
||||
<span className="text-neutral-500">+</span>
|
||||
<span className="px-2 py-0.5 rounded bg-surface-600 border border-white/[0.1]">Shift</span>
|
||||
<span className="text-neutral-500">+</span>
|
||||
<span className="px-2.5 py-0.5 rounded bg-brand-amber/20 border border-brand-amber/40 text-brand-amber">Space</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="font-mono text-nano text-neutral-600 mt-8 uppercase tracking-widest">
|
||||
{t.cta.systemReq}
|
||||
<div>
|
||||
<GlowButton
|
||||
href="https://github.com/user/D3ROVoice/releases"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="shadow-glow-md hover:shadow-glow-lg"
|
||||
>
|
||||
{t.cta.downloadBtn}
|
||||
</GlowButton>
|
||||
</div>
|
||||
|
||||
<p className="font-mono text-nano text-neutral-500 mt-8 uppercase tracking-widest">
|
||||
{t.cta.systemReq} · 100% OPEN ARCHITECTURE
|
||||
</p>
|
||||
</Container>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,19 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { SectionHeader } from '../components/SectionHeader'
|
||||
import { InstrumentCard } from '../components/InstrumentCard'
|
||||
import { Container } from '../components/Container'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
interface FeatureMeta {
|
||||
icon: ReactNode
|
||||
interface FeatureTile {
|
||||
id: string
|
||||
span: 'col-span-1' | 'col-span-1 md:col-span-2'
|
||||
tag: 'FREE' | 'PRO' | 'PRO+'
|
||||
title: string
|
||||
description: string
|
||||
highlight?: string
|
||||
icon: ReactNode
|
||||
previewNode?: ReactNode
|
||||
}
|
||||
|
||||
const featureMeta: FeatureMeta[] = [
|
||||
{ icon: <MicIcon />, tag: 'FREE' },
|
||||
{ icon: <SparklesIcon />, tag: 'FREE' },
|
||||
{ icon: <LanguageIcon />, tag: 'FREE' },
|
||||
{ icon: <CaptionIcon />, tag: 'PRO' },
|
||||
{ icon: <ConversationIcon />, tag: 'PRO+' },
|
||||
{ icon: <FileIcon />, tag: 'PRO+' },
|
||||
{ icon: <ChainIcon />, tag: 'PRO' },
|
||||
{ icon: <ContextIcon />, tag: 'PRO' },
|
||||
{ icon: <MemoIcon />, tag: 'PRO' },
|
||||
]
|
||||
|
||||
const tagStyles = {
|
||||
FREE: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20',
|
||||
PRO: 'text-brand-amber bg-brand-amber/10 border-brand-amber/20',
|
||||
|
|
@ -28,7 +21,112 @@ const tagStyles = {
|
|||
} as const
|
||||
|
||||
export function Features() {
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const bentoTiles: FeatureTile[] = [
|
||||
{
|
||||
id: 'hud',
|
||||
span: 'col-span-1 md:col-span-2',
|
||||
tag: 'FREE',
|
||||
title: locale === 'ko' ? '전역 Push-to-Talk 음성 캡슐 HUD' : 'Global Push-to-Talk Capsule HUD',
|
||||
description: locale === 'ko'
|
||||
? 'VS Code, Notion, Slack, Word 등 모든 프로그램 위에서 Ctrl+Shift+Space로 즉시 호출되는 초경량 플로팅 캡슐. 타이핑 없이 생각의 속도로 입력하세요.'
|
||||
: 'Lightweight floating HUD triggered with Ctrl+Shift+Space across any application. Dictate and transcribe at the speed of thought without leaving your workflow.',
|
||||
highlight: 'Global Hotkey & Floating Overlays',
|
||||
icon: <MicIcon />,
|
||||
previewNode: (
|
||||
<div className="mt-4 p-3 rounded-xl bg-surface-950 border border-white/[0.06] flex items-center justify-between shadow-inner">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-brand-amber animate-ping" />
|
||||
<div className="flex items-center gap-1 h-5">
|
||||
{[4, 12, 20, 16, 24, 14, 8, 18, 6].map((h, i) => (
|
||||
<div key={i} className="w-1 bg-brand-amber rounded-full" style={{ height: `${h}px` }} />
|
||||
))}
|
||||
</div>
|
||||
<span className="font-mono text-nano text-neutral-400 ml-2">0:04</span>
|
||||
</div>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
||||
AUTO-INJECTING
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'privacy',
|
||||
span: 'col-span-1',
|
||||
tag: 'FREE',
|
||||
title: locale === 'ko' ? '100% 로컬 보안 실드' : '100% On-Device Local Privacy',
|
||||
description: locale === 'ko'
|
||||
? '음성 및 전사 텍스트가 외부 클라우드 서버로 단 1바이트도 전송되지 않습니다. 에어갭 환경 및 사내 보안 규정 100% 준수.'
|
||||
: 'Zero cloud telemetry. Voice audio and transcriptions are strictly processed on your local machine using faster-whisper and Ollama.',
|
||||
highlight: 'Zero Cloud Egress',
|
||||
icon: <ShieldIcon />,
|
||||
},
|
||||
{
|
||||
id: 'meeting',
|
||||
span: 'col-span-1',
|
||||
tag: 'PRO',
|
||||
title: locale === 'ko' ? '회의 스튜디오 & 화자 분리' : 'Meeting Studio & Diarization',
|
||||
description: locale === 'ko'
|
||||
? '장시간 회의 녹음, 화자 자동 분리, 실시간 스크래치패드 및 마크다운 회의록(액션 아이템, 결정 사항) 자동 생성.'
|
||||
: 'Long-form meeting recording with speaker diarization, real-time memo scratchpad, and automatic structured Markdown summary generation.',
|
||||
highlight: 'Auto Markdown Action Items',
|
||||
icon: <UsersIcon />,
|
||||
},
|
||||
{
|
||||
id: 'chains',
|
||||
span: 'col-span-1 md:col-span-2',
|
||||
tag: 'PRO',
|
||||
title: locale === 'ko' ? '다단계 AI 파이프라인 체이닝 & 매크로' : 'Multi-Step LLM Pipeline Chaining',
|
||||
description: locale === 'ko'
|
||||
? '음성 입력 → 추임새 제거 → 전문 용어 사전 보정 → LLM 톤앤매너 변환 → 활성 창 자동 입력까지 1회 발화로 연속 실행하는 고성능 자동화 엔진.'
|
||||
: 'Chain multi-step prompts in a single voice command: Transcribe → Clean Fillers → Phonetic Dictionary Fix → LLM Transform → Active Cursor Injection.',
|
||||
highlight: 'Custom Prompts & Tone Shift',
|
||||
icon: <ChainIcon />,
|
||||
previewNode: (
|
||||
<div className="mt-4 grid grid-cols-4 gap-2 font-mono text-nano text-neutral-400 bg-surface-950 p-2.5 rounded-lg border border-white/[0.04]">
|
||||
<div className="text-center p-1.5 rounded bg-surface-800 border border-white/[0.05]">
|
||||
<span className="text-brand-amber block">01. STT</span>
|
||||
<span className="text-neutral-500">Whisper</span>
|
||||
</div>
|
||||
<div className="text-center p-1.5 rounded bg-surface-800 border border-white/[0.05]">
|
||||
<span className="text-emerald-400 block">02. DICT</span>
|
||||
<span className="text-neutral-500">Phonetic</span>
|
||||
</div>
|
||||
<div className="text-center p-1.5 rounded bg-surface-800 border border-white/[0.05]">
|
||||
<span className="text-violet-400 block">03. LLM</span>
|
||||
<span className="text-neutral-500">Ollama</span>
|
||||
</div>
|
||||
<div className="text-center p-1.5 rounded bg-surface-800 border border-white/[0.05]">
|
||||
<span className="text-neutral-200 block">04. INJECT</span>
|
||||
<span className="text-neutral-500">Cursor</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'rag',
|
||||
span: 'col-span-1',
|
||||
tag: 'PRO+',
|
||||
title: locale === 'ko' ? '로컬 문서 지식베이스 (RAG)' : 'Local Document Intelligence (RAG)',
|
||||
description: locale === 'ko'
|
||||
? '내 PC의 PDF, 메모, 사내 문서를 로컬 벡터 DB에 인덱싱하여 음성으로 검색하고 대화할 수 있는 비공개 지식 베이스.'
|
||||
: 'Index local PDFs, notes, and private documents into a local vector DB. Query your private knowledge base using voice commands.',
|
||||
highlight: 'sqlite-vec & Local Embeddings',
|
||||
icon: <DatabaseIcon />,
|
||||
},
|
||||
{
|
||||
id: 'performance',
|
||||
span: 'col-span-1',
|
||||
tag: 'FREE',
|
||||
title: locale === 'ko' ? '초저지연 고성능 아키텍처' : 'Sub-Second Local Latency',
|
||||
description: locale === 'ko'
|
||||
? 'C++ 기반 faster-whisper 백엔드로 일반 노트북 CPU에서도 1.2초 미만의 경이로운 응답 속도를 실현.'
|
||||
: 'Optimized C++ faster-whisper runtime delivers sub-1.2s transcription latency even on standard laptop CPUs without dedicated GPU.',
|
||||
highlight: '< 1.2s Real-time Latency',
|
||||
icon: <ZapIcon />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section id="features" className="relative py-24 md:py-32">
|
||||
|
|
@ -39,52 +137,61 @@ export function Features() {
|
|||
subtitle={t.features.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{t.features.items.map((item, i) => {
|
||||
const meta = featureMeta[i]
|
||||
return (
|
||||
<InstrumentCard key={i}>
|
||||
{/* Asymmetric Bento Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{bentoTiles.map((tile) => (
|
||||
<div
|
||||
key={tile.id}
|
||||
className={`relative noise-texture bg-surface-700/40 border border-white/[0.06] rounded-2xl p-6 md:p-7 shadow-chassis transition-all duration-300 hover:border-brand-amber/30 hover:bg-surface-700/60 hover:shadow-glow-sm flex flex-col justify-between ${tile.span}`}
|
||||
>
|
||||
<div>
|
||||
{/* Top row: icon + tag */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="w-10 h-10 rounded-card bg-surface-400 border border-white/[0.06] flex items-center justify-center text-brand-amber">
|
||||
{meta.icon}
|
||||
<div className="flex items-start justify-between mb-5">
|
||||
<div className="w-11 h-11 rounded-xl bg-surface-600 border border-white/[0.08] flex items-center justify-center text-brand-amber shadow-sm">
|
||||
{tile.icon}
|
||||
</div>
|
||||
<span className={`text-nano font-mono font-semibold tracking-widest uppercase px-2 py-0.5 border rounded-sm ${tagStyles[meta.tag]}`}>
|
||||
{meta.tag}
|
||||
<span className={`text-nano font-mono font-semibold tracking-widest uppercase px-2.5 py-0.5 border rounded-md ${tagStyles[tile.tag]}`}>
|
||||
{tile.tag}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="font-display text-lg font-semibold text-neutral-100 mb-2">
|
||||
{item.title}
|
||||
<h3 className="font-display text-xl font-bold text-neutral-100 mb-2.5">
|
||||
{tile.title}
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-400 leading-relaxed">
|
||||
{item.description}
|
||||
{tile.description}
|
||||
</p>
|
||||
|
||||
{/* Bottom arrow */}
|
||||
<div className="mt-5 flex justify-end">
|
||||
<div className="w-7 h-7 rounded-full border border-white/[0.06] flex items-center justify-center text-neutral-500 group-hover:text-brand-amber group-hover:border-brand-amber/20 transition-colors">
|
||||
{/* Optional interactive preview */}
|
||||
{tile.previewNode}
|
||||
</div>
|
||||
|
||||
{/* Bottom tag / detail */}
|
||||
{tile.highlight && (
|
||||
<div className="mt-6 pt-4 border-t border-white/[0.04] flex items-center justify-between">
|
||||
<span className="font-mono text-nano text-neutral-500 uppercase tracking-wider">
|
||||
{tile.highlight}
|
||||
</span>
|
||||
<div className="w-6 h-6 rounded-full border border-white/[0.08] flex items-center justify-center text-neutral-500 hover:text-brand-amber hover:border-brand-amber/30 transition-colors">
|
||||
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="7" y1="17" x2="17" y2="7" />
|
||||
<polyline points="7 7 17 7 17 17" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</InstrumentCard>
|
||||
)
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ── Inline SVG Icons ─────────────────────────────────── */
|
||||
|
||||
function MicIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="9" y="2" width="6" height="11" rx="3" />
|
||||
<path d="M5 10a7 7 0 0 0 14 0" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
|
|
@ -92,75 +199,49 @@ function MicIcon() {
|
|||
)
|
||||
}
|
||||
|
||||
function SparklesIcon() {
|
||||
function ShieldIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z" />
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
<path d="M9 12l2 2 4-4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function LanguageIcon() {
|
||||
function UsersIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 8l6 6" /><path d="M4 14l6-6 2-3" /><path d="M2 5h12" /><path d="M7 2h1" />
|
||||
<path d="M22 22l-5-10-5 10" /><path d="M14 18h6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CaptionIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
<path d="M7 15h4" /><path d="M13 15h4" /><path d="M7 11h10" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ConversationIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function FileIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ChainIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextIcon() {
|
||||
function DatabaseIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MemoIcon() {
|
||||
function ZapIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,23 +10,25 @@ export function Header() {
|
|||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 40)
|
||||
const onScroll = () => setScrolled(window.scrollY > 20)
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
return () => window.removeEventListener('scroll', onScroll)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 mix-blend-difference ${
|
||||
scrolled ? 'backdrop-blur-xl' : ''
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled
|
||||
? 'bg-surface-950/85 backdrop-blur-xl border-b border-white/[0.06] shadow-lg'
|
||||
: 'bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8 lg:px-12 h-16 flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<a href="#" className="flex items-center gap-2 group">
|
||||
<Led color="amber" pulse size="md" />
|
||||
<span className="font-mono text-sm font-semibold tracking-tight text-white">
|
||||
D3RO<span className="text-neutral-400">·</span>VOICE
|
||||
<span className="font-mono text-sm font-bold tracking-tight text-white">
|
||||
D3RO<span className="text-brand-amber">·</span>VOICE
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
|
@ -41,6 +43,13 @@ export function Header() {
|
|||
{t.nav[link.key]}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="/launch-readiness.html"
|
||||
className="font-mono text-nano uppercase tracking-widest text-cyan-400 hover:text-cyan-300 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse"></span>
|
||||
Launch Sandbox
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{/* Right side: language + CTA */}
|
||||
|
|
@ -48,7 +57,7 @@ export function Header() {
|
|||
<LanguageSwitcher />
|
||||
<a
|
||||
href="https://github.com/user/D3ROVoice/releases"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber/90 rounded-panel hover:bg-brand-amber transition-colors"
|
||||
className="glow-btn inline-flex items-center gap-2 px-4 py-2 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber rounded-panel shadow-sm hover:bg-brand-amber-light transition-colors"
|
||||
>
|
||||
{t.nav.download}
|
||||
</a>
|
||||
|
|
@ -57,7 +66,7 @@ export function Header() {
|
|||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
className="md:hidden w-9 h-9 flex items-center justify-center"
|
||||
className="md:hidden w-9 h-9 flex items-center justify-center text-neutral-300"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
|
|
@ -69,7 +78,7 @@ export function Header() {
|
|||
|
||||
{/* Mobile Nav */}
|
||||
{mobileOpen && (
|
||||
<div className="md:hidden bg-surface-900/95 backdrop-blur-xl border-t border-white/[0.06] px-5 py-6 space-y-1">
|
||||
<div className="md:hidden bg-surface-900/95 backdrop-blur-2xl border-t border-white/[0.06] px-5 py-6 space-y-1 shadow-2xl">
|
||||
{navItems.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
|
|
@ -84,7 +93,7 @@ export function Header() {
|
|||
<LanguageSwitcher />
|
||||
<a
|
||||
href="https://github.com/user/D3ROVoice/releases"
|
||||
className="flex-1 text-center px-5 py-2.5 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber rounded-panel"
|
||||
className="flex-1 text-center px-5 py-2.5 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber rounded-panel shadow-sm"
|
||||
>
|
||||
{t.nav.download}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,77 +1,271 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Badge } from '../components/Badge'
|
||||
import { Crosshair } from '../components/Crosshair'
|
||||
import { DataPoint } from '../components/DataPoint'
|
||||
import { Container } from '../components/Container'
|
||||
import { Led } from '../components/Led'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
const CJK_LOCALES = new Set(['ko', 'ja', 'zh'])
|
||||
|
||||
export function Hero() {
|
||||
const { t, locale } = useI18n()
|
||||
const isCJK = CJK_LOCALES.has(locale)
|
||||
|
||||
// Interactive Live Simulator State
|
||||
const [simState, setSimState] = useState<'idle' | 'recording' | 'processing' | 'done'>('idle')
|
||||
const [typedRaw, setTypedRaw] = useState('')
|
||||
const [typedClean, setTypedClean] = useState('')
|
||||
const [waveLevels, setWaveLevels] = useState([0.3, 0.5, 0.8, 1, 0.9, 0.7, 0.4, 0.6, 0.3])
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const sampleRawText = locale === 'ko'
|
||||
? '어... 이번 프로젝트 배포는 다음 주 금요일까지로 잡으면 될 것 같아요.'
|
||||
: 'Um... I think we can schedule the release for next Friday.'
|
||||
const sampleCleanText = locale === 'ko'
|
||||
? '이번 프로젝트 배포 일정은 다음 주 금요일로 확정하겠습니다.'
|
||||
: 'The project release is scheduled for next Friday.'
|
||||
|
||||
// Waveform animation during recording
|
||||
useEffect(() => {
|
||||
if (simState !== 'recording') return
|
||||
const interval = setInterval(() => {
|
||||
setWaveLevels(Array.from({ length: 9 }, () => 0.2 + Math.random() * 0.8))
|
||||
}, 90)
|
||||
return () => clearInterval(interval)
|
||||
}, [simState])
|
||||
|
||||
const startSimulation = () => {
|
||||
if (simState !== 'idle' && simState !== 'done') return
|
||||
setSimState('recording')
|
||||
setTypedRaw('')
|
||||
setTypedClean('')
|
||||
|
||||
let charIdx = 0
|
||||
const rawInterval = setInterval(() => {
|
||||
if (charIdx < sampleRawText.length) {
|
||||
setTypedRaw(sampleRawText.slice(0, charIdx + 1))
|
||||
charIdx++
|
||||
} else {
|
||||
clearInterval(rawInterval)
|
||||
setSimState('processing')
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
setSimState('done')
|
||||
let cleanIdx = 0
|
||||
const cleanInterval = setInterval(() => {
|
||||
if (cleanIdx < sampleCleanText.length) {
|
||||
setTypedClean(sampleCleanText.slice(0, cleanIdx + 1))
|
||||
cleanIdx++
|
||||
} else {
|
||||
clearInterval(cleanInterval)
|
||||
}
|
||||
}, 25)
|
||||
}, 600)
|
||||
}
|
||||
}, 45)
|
||||
}
|
||||
|
||||
const resetSimulation = () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current)
|
||||
setSimState('idle')
|
||||
setTypedRaw('')
|
||||
setTypedClean('')
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden bg-grid pt-16 pb-12">
|
||||
{/* Background effects */}
|
||||
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden bg-grid pt-28 pb-16">
|
||||
{/* Ambient background glow */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/3 left-1/2 -translate-x-1/2 w-[700px] h-[700px] rounded-full bg-brand-amber/[0.025] blur-[140px]" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand-amber/15 to-transparent" />
|
||||
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[800px] h-[800px] rounded-full bg-brand-amber/[0.035] blur-[160px]" />
|
||||
<div className="absolute top-1/2 right-10 w-[400px] h-[400px] rounded-full bg-blue-500/[0.02] blur-[140px]" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand-amber/20 to-transparent" />
|
||||
</div>
|
||||
|
||||
<Container className="relative flex-1 flex flex-col justify-center">
|
||||
{/* Meta corners */}
|
||||
<div className="hidden lg:block absolute top-8 left-5 md:left-8 lg:left-12">
|
||||
<Badge led ledColor="green">{t.hero.systemStatus}</Badge>
|
||||
</div>
|
||||
<div className="hidden lg:block absolute top-8 right-5 md:right-8 lg:right-12 font-mono text-nano uppercase tracking-widest text-neutral-500">
|
||||
{t.hero.protocol}
|
||||
</div>
|
||||
|
||||
{/* Main hero content */}
|
||||
<div className="max-w-5xl pt-12 md:pt-0">
|
||||
<Crosshair className="inline-block mb-8">
|
||||
<Badge led>{t.hero.badge}</Badge>
|
||||
</Crosshair>
|
||||
|
||||
<h1
|
||||
className="font-display text-hero font-bold text-neutral-50 mb-6 max-w-4xl"
|
||||
style={isCJK ? { lineHeight: 1.15 } : undefined}
|
||||
>
|
||||
<span className="block">{t.hero.title1}</span>
|
||||
<span className="block">{t.hero.title2}</span>
|
||||
<span className="block text-brand-amber">{t.hero.title3}</span>
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl text-base md:text-lg text-neutral-400 leading-relaxed mb-10">
|
||||
{t.hero.subtitle}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-start gap-4 mb-16">
|
||||
<a
|
||||
href="#"
|
||||
className="glow-btn inline-flex items-center gap-2 px-8 py-4 font-mono text-sm font-medium uppercase tracking-wider text-white bg-brand-amber rounded-panel"
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<DownloadIcon />
|
||||
{t.hero.downloadBtn}
|
||||
{/* Hero main grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8 items-center pt-4 mb-14">
|
||||
{/* Left Column: Typography & CTAs (7 cols) */}
|
||||
<div className="lg:col-span-7 max-w-2xl">
|
||||
{/* Status & Protocol Pill Row in Natural Document Flow */}
|
||||
<div className="flex flex-wrap items-center gap-2.5 mb-6">
|
||||
<Badge led ledColor="green">{t.hero.systemStatus}</Badge>
|
||||
<Badge led>{t.hero.badge}</Badge>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-400 bg-surface-700/60 px-2.5 py-1 rounded border border-white/[0.06]">
|
||||
ON-DEVICE V3
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="#features"
|
||||
className="inline-flex items-center gap-2 px-8 py-4 font-mono text-sm uppercase tracking-wider text-neutral-400 border border-white/[0.08] rounded-panel hover:border-white/[0.15] hover:text-neutral-200 transition-all"
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber bg-brand-amber/10 px-2.5 py-1 rounded border border-brand-amber/20">
|
||||
{t.hero.protocol}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Main Headline (2 Clean Lines with keep-all) */}
|
||||
<h1
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[3.4rem] font-bold text-neutral-50 mb-6 tracking-tight leading-[1.18] break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{t.hero.viewFeatures}
|
||||
<ArrowIcon />
|
||||
</a>
|
||||
<span className="block text-neutral-100">{t.hero.title1}</span>
|
||||
<span className="block text-brand-amber font-extrabold">{t.hero.title2}</span>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className="text-base md:text-lg text-neutral-400 leading-relaxed mb-8 max-w-xl break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{t.hero.subtitle}
|
||||
</p>
|
||||
|
||||
{/* Hotkey Callout */}
|
||||
<div className="flex items-center gap-3 mb-8 p-3 rounded-card bg-surface-700/40 border border-white/[0.05] max-w-md">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-400">
|
||||
GLOBAL PUSH-TO-TALK:
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 font-mono text-xs font-semibold text-neutral-200">
|
||||
<span className="px-2 py-0.5 rounded bg-surface-500 border border-white/[0.1] shadow-sm">Ctrl</span>
|
||||
<span className="text-neutral-500">+</span>
|
||||
<span className="px-2 py-0.5 rounded bg-surface-500 border border-white/[0.1] shadow-sm">Shift</span>
|
||||
<span className="text-neutral-500">+</span>
|
||||
<span className="px-2.5 py-0.5 rounded bg-brand-amber/20 border border-brand-amber/40 text-brand-amber shadow-sm">Space</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4 mb-3.5">
|
||||
<a
|
||||
href="https://github.com/user/D3ROVoice/releases"
|
||||
className="glow-btn inline-flex items-center justify-center gap-2.5 px-8 py-4 font-mono text-sm font-semibold uppercase tracking-wider text-white bg-brand-amber rounded-panel shadow-glow-sm hover:shadow-glow-md"
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<DownloadIcon />
|
||||
{t.hero.downloadBtn}
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="#features"
|
||||
className="inline-flex items-center justify-center gap-2 px-7 py-4 font-mono text-sm uppercase tracking-wider text-neutral-300 border border-white/[0.08] rounded-panel hover:border-brand-amber/30 hover:text-white bg-surface-800/40 backdrop-blur-sm transition-all"
|
||||
>
|
||||
{t.hero.viewFeatures}
|
||||
<ArrowIcon />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Friction-Reduction Trust Microcopy */}
|
||||
<div className="flex items-center gap-2 font-mono text-nano text-neutral-500">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
<span>{locale === 'ko' ? '영구 무료 · 회원가입 불필요 · 100% 오프라인 작동' : 'Free Forever · No Signup Required · 100% Offline'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Live Interactive Voice Capsule (5 cols) */}
|
||||
<div className="lg:col-span-5">
|
||||
<div className="relative p-6 md:p-7 rounded-2xl bg-surface-800/90 border border-white/[0.08] shadow-2xl backdrop-blur-2xl noise-texture">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-5 pb-3 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color={simState === 'recording' ? 'amber' : simState === 'processing' ? 'amber' : 'green'} pulse={simState !== 'idle'} />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-300">
|
||||
{simState === 'idle' ? 'INTERACTIVE SIMULATOR' : simState === 'recording' ? 'LISTENING & STREAMING' : simState === 'processing' ? 'AI POLISHING...' : 'PIPELINE COMPLETED'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-nano text-neutral-500 uppercase">
|
||||
ZERO CLOUD EGRESS
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Interactive Audio Capsule UI */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-surface-950 border border-white/[0.06] shadow-inner">
|
||||
{/* Waveform Bars */}
|
||||
<div className="flex items-center gap-1.5 h-8">
|
||||
{waveLevels.map((lvl, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="w-1.5 rounded-full transition-all duration-75"
|
||||
style={{
|
||||
height: `${Math.max(6, lvl * 32)}px`,
|
||||
backgroundColor: simState === 'recording' ? '#f25b29' : 'rgba(255,255,255,0.2)',
|
||||
boxShadow: simState === 'recording' ? '0 0 8px rgba(242,91,41,0.6)' : 'none',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Trigger Button */}
|
||||
<button
|
||||
onClick={simState === 'idle' || simState === 'done' ? startSimulation : resetSimulation}
|
||||
className={`px-4 py-2 rounded-lg font-mono text-xs font-semibold uppercase tracking-wider transition-all flex items-center gap-2 ${
|
||||
simState === 'recording'
|
||||
? 'bg-red-500/20 text-red-400 border border-red-500/30 animate-pulse'
|
||||
: 'bg-brand-amber text-white hover:bg-brand-amber-light shadow-glow-sm'
|
||||
}`}
|
||||
>
|
||||
<MicIcon />
|
||||
{simState === 'idle' ? 'Try Demo' : simState === 'recording' ? 'Listening...' : simState === 'processing' ? 'Polishing' : 'Test Again'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Real-time Transcription Stream */}
|
||||
<div className="min-h-[110px] p-4 rounded-xl bg-surface-900 border border-white/[0.04] flex flex-col justify-center">
|
||||
{simState === 'idle' && (
|
||||
<div className="text-center py-3">
|
||||
<p className="text-xs font-mono text-neutral-500">
|
||||
👆 Click 'Try Demo' to watch on-device speech-to-text & AI polish in action
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(simState === 'recording' || simState === 'processing') && (
|
||||
<div className="space-y-1.5">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/80 flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-brand-amber animate-ping" />
|
||||
Live Transcription:
|
||||
</span>
|
||||
<p className="text-sm font-sans text-neutral-200 leading-relaxed font-medium">
|
||||
{typedRaw}
|
||||
<span className="inline-block w-2 h-4 ml-1 bg-brand-amber animate-pulse align-middle" />
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{simState === 'done' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400 flex items-center gap-1">
|
||||
✓ AI Polish & Context Applied:
|
||||
</span>
|
||||
<span className="font-mono text-nano text-neutral-500">
|
||||
Inference: 0.84s
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-sans text-emerald-300 leading-relaxed font-medium bg-emerald-500/10 p-2.5 rounded-lg border border-emerald-500/20">
|
||||
"{typedClean}"
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Telemetry Hardware Stats */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-2 border-t border-white/[0.04] font-mono text-nano text-neutral-500">
|
||||
<div>
|
||||
<span className="block text-neutral-600">STT ENGINE</span>
|
||||
<span className="text-neutral-300 font-semibold">faster-whisper</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-neutral-600">LLM MODEL</span>
|
||||
<span className="text-neutral-300 font-semibold">Ollama Local</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-neutral-600">CLOUD LEAK</span>
|
||||
<span className="text-emerald-400 font-semibold">0.00 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data strip */}
|
||||
{/* Bottom Data Telemetry Strip */}
|
||||
<div className="border-t border-white/[0.06] pt-8 mt-auto">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
<DataPoint label={t.hero.dataProcessing} value="100%" unit="local" />
|
||||
<DataPoint label={t.hero.dataCloudTraffic} value="0" unit="bytes" />
|
||||
<DataPoint label={t.hero.dataLatency} value="<2s" />
|
||||
<DataPoint label={t.hero.dataLatency} value="<1.2s" />
|
||||
<DataPoint label={t.hero.dataPrivacy} value="10/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -82,7 +276,7 @@ export function Hero() {
|
|||
|
||||
function DownloadIcon() {
|
||||
return (
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
|
|
@ -92,9 +286,19 @@ function DownloadIcon() {
|
|||
|
||||
function ArrowIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="7" y1="17" x2="17" y2="7" />
|
||||
<polyline points="7 7 17 7 17 17" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MicIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="9" y="2" width="6" height="11" rx="3" />
|
||||
<path d="M5 10a7 7 0 0 0 14 0" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useState } from 'react'
|
||||
import { SectionHeader } from '../components/SectionHeader'
|
||||
import { Crosshair } from '../components/Crosshair'
|
||||
import { Container } from '../components/Container'
|
||||
|
|
@ -5,8 +6,61 @@ import { Led } from '../components/Led'
|
|||
import { WaveBars } from '../components/WaveBars'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
interface ScenarioPreset {
|
||||
id: string
|
||||
title: string
|
||||
tag: string
|
||||
raw: string
|
||||
clean: string
|
||||
latency: string
|
||||
model: string
|
||||
}
|
||||
|
||||
export function HowItWorks() {
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
const [activePreset, setActivePreset] = useState(0)
|
||||
|
||||
const presets: ScenarioPreset[] = [
|
||||
{
|
||||
id: 'business',
|
||||
title: locale === 'ko' ? '비즈니스 회의 요약 및 정제' : 'Business Polish',
|
||||
tag: 'LLM POLISH',
|
||||
raw: locale === 'ko'
|
||||
? '어... 그 다음 주 화요일 회의에서 마케팅 예산 편성안을 검토해보도록 합시다.'
|
||||
: 'Um... let us review the marketing budget allocation plan in next Tuesday\'s meeting.',
|
||||
clean: locale === 'ko'
|
||||
? '다음 주 화요일 회의 안건으로 마케팅 예산 편성안을 검토하겠습니다.'
|
||||
: 'Action: Review marketing budget allocation plan during Tuesday\'s meeting.',
|
||||
latency: '0.78s',
|
||||
model: 'gemma4:e4b (Local)',
|
||||
},
|
||||
{
|
||||
id: 'developer',
|
||||
title: locale === 'ko' ? '개발자 Git 커밋 메시지' : 'Developer Git Commit',
|
||||
tag: 'MACRO CHAIN',
|
||||
raw: locale === 'ko'
|
||||
? '로그인할 때 리프레시 토큰 만료 에러 핸들링 추가했어'
|
||||
: 'Add refresh token expiration error handling when logging in',
|
||||
clean: 'fix(auth): handle refresh token expiration error on user login',
|
||||
latency: '0.62s',
|
||||
model: 'qwen2.5-coder:7b',
|
||||
},
|
||||
{
|
||||
id: 'translate',
|
||||
title: locale === 'ko' ? '실시간 다국어 비즈니스 번역' : 'Real-time Translation',
|
||||
tag: 'TRANSLATION',
|
||||
raw: locale === 'ko'
|
||||
? '이번 분기 실적 보고서를 오늘 퇴근 전까지 공유해주세요.'
|
||||
: 'Please share this quarter\'s performance report before the end of the day.',
|
||||
clean: locale === 'ko'
|
||||
? 'Please share this quarter\'s performance report before the end of the day.'
|
||||
: '이번 분기 실적 보고서를 오늘 퇴근 전까지 전달 부탁드립니다.',
|
||||
latency: '0.91s',
|
||||
model: 'faster-whisper + Ollama',
|
||||
},
|
||||
]
|
||||
|
||||
const current = presets[activePreset]
|
||||
|
||||
return (
|
||||
<section id="pipeline" className="relative py-24 md:py-32">
|
||||
|
|
@ -19,72 +73,106 @@ export function HowItWorks() {
|
|||
subtitle={t.pipeline.subtitle}
|
||||
/>
|
||||
|
||||
{/* Preset Selector Bar */}
|
||||
<div className="flex items-center justify-center gap-2 mb-10 overflow-x-auto pb-2">
|
||||
{presets.map((p, idx) => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => setActivePreset(idx)}
|
||||
className={`px-4 py-2 rounded-panel font-mono text-xs uppercase tracking-wider transition-all flex items-center gap-2 ${
|
||||
activePreset === idx
|
||||
? 'bg-brand-amber text-white shadow-glow-sm font-semibold'
|
||||
: 'bg-surface-700/60 text-neutral-400 hover:text-neutral-200 border border-white/[0.04]'
|
||||
}`}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-current" />
|
||||
{p.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
|
||||
{/* Left: visual panel */}
|
||||
{/* Left: Interactive CRT Studio Screen */}
|
||||
<Crosshair className="order-2 lg:order-1">
|
||||
<div className="crt-screen rounded-card p-6 md:p-8 aspect-[4/3] flex flex-col justify-between">
|
||||
<div className="relative z-10 flex items-center justify-between mb-6">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60">
|
||||
{t.pipeline.panelTitle}
|
||||
</span>
|
||||
<div className="crt-screen rounded-card p-6 md:p-8 aspect-[4/3] flex flex-col justify-between noise-texture shadow-2xl">
|
||||
<div className="relative z-10 flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-500">{t.pipeline.panelActive}</span>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/80">
|
||||
{current.tag} PIPELINE
|
||||
</span>
|
||||
<span className="font-mono text-nano text-neutral-500 bg-surface-600/60 px-2 py-0.5 rounded border border-white/[0.04]">
|
||||
{current.model}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" pulse />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400">{t.pipeline.panelActive}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex-1 flex flex-col justify-center space-y-4">
|
||||
{/* Simulated terminal output */}
|
||||
<div className="font-mono text-xs text-neutral-500 leading-relaxed space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" size="sm" />
|
||||
<span className="text-emerald-400/70">{t.pipeline.sttReady}</span>
|
||||
<span className="text-neutral-600">faster-whisper base</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" size="sm" />
|
||||
<span className="text-emerald-400/70">{t.pipeline.llmConnected}</span>
|
||||
<span className="text-neutral-600">gemma4:e4b @ localhost:11434</span>
|
||||
</div>
|
||||
<div className="h-px bg-white/[0.04] my-3" />
|
||||
<div className="flex items-start gap-2">
|
||||
<Led color="amber" size="sm" pulse className="mt-1" />
|
||||
<div>
|
||||
<span className="text-brand-amber/50 block mb-1">{t.pipeline.transcription}</span>
|
||||
<span className="text-neutral-300">"{t.pipeline.sampleInput}"</span>
|
||||
{/* Simulated live pipeline flow */}
|
||||
<div className="font-mono text-xs text-neutral-500 leading-relaxed space-y-3">
|
||||
<div className="flex items-center justify-between text-neutral-400 bg-surface-950/60 p-2.5 rounded border border-white/[0.04]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" size="sm" />
|
||||
<span className="text-emerald-400/90 font-semibold">{t.pipeline.sttReady}</span>
|
||||
<span className="text-neutral-500">faster-whisper base</span>
|
||||
</div>
|
||||
<span className="text-nano text-neutral-600 font-mono">0ms</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Led color="green" size="sm" className="mt-1" />
|
||||
<div>
|
||||
<span className="text-emerald-400/50 block mb-1">{t.pipeline.aiPolish}</span>
|
||||
<span className="text-neutral-300">"{t.pipeline.sampleOutput}"</span>
|
||||
|
||||
<div className="p-3 rounded-lg bg-surface-900/90 border border-white/[0.06] space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/70 flex items-center gap-1.5">
|
||||
<Led color="amber" size="sm" pulse />
|
||||
Raw Voice STT Output:
|
||||
</span>
|
||||
<span className="font-mono text-nano text-neutral-600">~0.42s</span>
|
||||
</div>
|
||||
<p className="text-sm font-sans text-neutral-300 font-medium">
|
||||
"{current.raw}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-lg bg-emerald-500/10 border border-emerald-500/20 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400 flex items-center gap-1.5">
|
||||
<Led color="green" size="sm" />
|
||||
Transformed & Injected to Active Window:
|
||||
</span>
|
||||
<span className="font-mono text-nano text-emerald-400/80 font-bold">{current.latency}</span>
|
||||
</div>
|
||||
<p className="text-sm font-sans text-emerald-200 font-semibold">
|
||||
"{current.clean}"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex items-center justify-between pt-4 border-t border-white/[0.04]">
|
||||
<WaveBars className="h-6" />
|
||||
<span className="font-mono text-nano text-neutral-600">{t.pipeline.panelLatency}</span>
|
||||
<span className="font-mono text-nano text-neutral-400">
|
||||
Total Engine Latency: <strong className="text-white">{current.latency}</strong> (Zero Cloud Traffic)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Crosshair>
|
||||
|
||||
{/* Right: numbered steps */}
|
||||
<div className="order-1 lg:order-2 space-y-1">
|
||||
{/* Right: Numbered Architecture Steps */}
|
||||
<div className="order-1 lg:order-2 space-y-2">
|
||||
{t.pipeline.steps.map((step, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="group relative flex gap-5 p-5 rounded-card border border-transparent hover:border-white/[0.04] hover:bg-surface-700/30 transition-all"
|
||||
className="group relative flex gap-5 p-5 rounded-card border border-white/[0.04] bg-surface-700/20 hover:border-brand-amber/30 hover:bg-surface-700/40 transition-all"
|
||||
>
|
||||
{/* Number */}
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-card bg-surface-600 border border-white/[0.06] flex items-center justify-center">
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-card bg-surface-600 border border-white/[0.06] flex items-center justify-center shadow-sm">
|
||||
<span className="font-mono text-lg font-bold text-brand-amber">{String(i + 1).padStart(2, '0')}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60 block mb-1">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/70 block mb-1">
|
||||
{step.label}
|
||||
</span>
|
||||
<h3 className="font-display text-lg font-semibold text-neutral-100 mb-1.5">
|
||||
|
|
@ -93,14 +181,14 @@ export function HowItWorks() {
|
|||
<p className="text-sm text-neutral-400 leading-relaxed mb-2">
|
||||
{step.description}
|
||||
</p>
|
||||
<span className="inline-block font-mono text-nano text-neutral-600 bg-surface-600/60 px-2.5 py-1 rounded-sm border border-white/[0.03]">
|
||||
<span className="inline-block font-mono text-nano text-neutral-400 bg-surface-600/80 px-2.5 py-1 rounded-sm border border-white/[0.05]">
|
||||
{step.detail}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Connector */}
|
||||
{i < t.pipeline.steps.length - 1 && (
|
||||
<div className="absolute left-[42px] top-[72px] w-px h-5 bg-gradient-to-b from-surface-300 to-transparent hidden lg:block" />
|
||||
<div className="absolute left-[42px] top-[72px] w-px h-6 bg-gradient-to-b from-brand-amber/30 to-transparent hidden lg:block" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ function CellValue({ value }: { value: string | boolean }) {
|
|||
if (value === true) {
|
||||
return (
|
||||
<span className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_4px_rgba(52,211,153,0.6)]" />
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.8)]" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (value === false) {
|
||||
return (
|
||||
<span className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 rounded-full bg-neutral-600" />
|
||||
<span className="w-2 h-2 rounded-full bg-neutral-700" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -23,18 +23,111 @@ function CellValue({ value }: { value: string | boolean }) {
|
|||
}
|
||||
|
||||
const PRICES = {
|
||||
pro: { monthly: 9.9, annual: 99 },
|
||||
proPlus: { monthly: 19.9, annual: 199 },
|
||||
usd: {
|
||||
pro: { monthly: 9.9, annual: 99 },
|
||||
proPlus: { monthly: 19.9, annual: 199 },
|
||||
team: { monthly: 25, annual: 240 },
|
||||
},
|
||||
krw: {
|
||||
pro: { monthly: 12900, annual: 119000 },
|
||||
proPlus: { monthly: 24900, annual: 229000 },
|
||||
team: { monthly: 32000, annual: 299000 },
|
||||
}
|
||||
}
|
||||
|
||||
export function Pricing() {
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
const [isAnnual, setIsAnnual] = useState(true)
|
||||
const [currency, setCurrency] = useState<'usd' | 'krw'>(locale === 'ko' ? 'krw' : 'usd')
|
||||
|
||||
const proPrice = isAnnual ? PRICES.pro.annual : PRICES.pro.monthly
|
||||
const proPlusPrice = isAnnual ? PRICES.proPlus.annual : PRICES.proPlus.monthly
|
||||
const currencySymbol = currency === 'krw' ? '₩' : '$'
|
||||
const activePrices = PRICES[currency]
|
||||
|
||||
const formatPrice = (val: number) => {
|
||||
if (currency === 'krw') {
|
||||
return val.toLocaleString()
|
||||
}
|
||||
return val.toString()
|
||||
}
|
||||
|
||||
const proPrice = isAnnual ? activePrices.pro.annual : activePrices.pro.monthly
|
||||
const proPlusPrice = isAnnual ? activePrices.proPlus.annual : activePrices.proPlus.monthly
|
||||
const teamPrice = isAnnual ? activePrices.team.annual : activePrices.team.monthly
|
||||
const periodLabel = isAnnual ? t.pricing.annual : t.pricing.monthly
|
||||
|
||||
const tiers = [
|
||||
{
|
||||
id: 'free',
|
||||
name: t.pricing.free,
|
||||
price: `${currencySymbol}0`,
|
||||
period: t.pricing.forever,
|
||||
desc: locale === 'ko' ? '100% 온디바이스 무제한 로컬 음성 인식' : '100% on-device unlimited private dictation',
|
||||
popular: false,
|
||||
buttonText: t.pricing.downloadFree,
|
||||
buttonVariant: 'secondary' as const,
|
||||
features: [
|
||||
locale === 'ko' ? '무제한 on-device faster-whisper STT' : 'Unlimited faster-whisper local STT',
|
||||
locale === 'ko' ? '전역 Push-to-Talk HUD 캡슐' : 'Global Push-to-Talk capsule HUD',
|
||||
locale === 'ko' ? '로컬 Ollama LLM 무제한 연동' : 'Unlimited local Ollama LLM connect',
|
||||
locale === 'ko' ? '100% 온디바이스 제로 텔레메트리' : '100% On-device zero cloud leak',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pro',
|
||||
name: t.pricing.pro,
|
||||
price: `${currencySymbol}${formatPrice(proPrice)}`,
|
||||
period: periodLabel,
|
||||
subPrice: isAnnual ? `${currencySymbol}${formatPrice(Math.round(activePrices.pro.annual / 12))}${t.pricing.perMonth}` : null,
|
||||
desc: locale === 'ko' ? '회의 스튜디오 & 하이브리드 고속 AI' : 'Meeting studio & hybrid fast AI acceleration',
|
||||
popular: true,
|
||||
buttonText: t.pricing.getPro,
|
||||
buttonVariant: 'primary' as const,
|
||||
features: [
|
||||
locale === 'ko' ? 'Free 티어의 모든 기능 포함' : 'Includes all Free features',
|
||||
locale === 'ko' ? '회의 스튜디오 & 실시간 화자 분리' : 'Meeting Studio & Speaker Diarization',
|
||||
locale === 'ko' ? '마크다운 회의록 & 액션 아이템 자동 생성' : 'Auto Markdown Minutes & Action Items',
|
||||
locale === 'ko' ? '다단계 AI 파이프라인 체이닝 & 매크로' : 'Multi-step Prompt Chaining & Macros',
|
||||
locale === 'ko' ? '클라우드 LLM 하이브리드 초고속 다듬기' : 'Hybrid Cloud LLM Acceleration',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'proPlus',
|
||||
name: t.pricing.proPlus,
|
||||
price: `${currencySymbol}${formatPrice(proPlusPrice)}`,
|
||||
period: periodLabel,
|
||||
subPrice: isAnnual ? `${currencySymbol}${formatPrice(Math.round(activePrices.proPlus.annual / 12))}${t.pricing.perMonth}` : null,
|
||||
desc: locale === 'ko' ? '초저지연 클라우드 STT & 개인 문서 RAG' : 'Ultra-low latency STT & private vector RAG',
|
||||
popular: false,
|
||||
buttonText: t.pricing.getProPlus,
|
||||
buttonVariant: 'secondary' as const,
|
||||
features: [
|
||||
locale === 'ko' ? 'Pro 티어의 모든 기능 포함' : 'Includes all Pro features',
|
||||
locale === 'ko' ? '초저지연 Cloud STT (Deepgram Nova-3)' : 'Ultra-Low Latency Cloud STT',
|
||||
locale === 'ko' ? '로컬 문서 지식베이스 (sqlite-vec RAG)' : 'Private Document Knowledge Base (RAG)',
|
||||
locale === 'ko' ? '실시간 음성 양방향 대화 캔버스' : 'Real-time Voice Duplex Canvas',
|
||||
locale === 'ko' ? '우선 기술 지원 및 신기능 얼리액세스' : 'Priority Tech Support & Early Access',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'team',
|
||||
name: locale === 'ko' ? 'Team' : 'Team',
|
||||
price: `${currencySymbol}${formatPrice(teamPrice)}`,
|
||||
period: isAnnual ? `${t.pricing.annual} / ${locale === 'ko' ? '인' : 'seat'}` : `${t.pricing.monthly} / ${locale === 'ko' ? '인' : 'seat'}`,
|
||||
subPrice: isAnnual ? `${currencySymbol}${formatPrice(Math.round(activePrices.team.annual / 12))}${t.pricing.perMonth} / ${locale === 'ko' ? '인' : 'seat'}` : null,
|
||||
desc: locale === 'ko' ? '팀 공유 사전 & 지식 협업 & 엔터프라이즈 거버넌스' : 'Team shared dictionaries, knowledge & governance',
|
||||
popular: false,
|
||||
buttonText: locale === 'ko' ? '팀 도입 문의' : 'Contact Sales',
|
||||
buttonVariant: 'secondary' as const,
|
||||
features: [
|
||||
locale === 'ko' ? 'Pro+ 티어의 모든 기능 포함' : 'Includes all Pro+ features',
|
||||
locale === 'ko' ? '팀 공용 맞춤형 사전 및 도메인 용어집' : 'Shared Team Dictionaries & Jargon',
|
||||
locale === 'ko' ? '팀 워크스페이스 회의록 협업' : 'Team Meeting Collaboration Workspace',
|
||||
locale === 'ko' ? 'SAML 2.0 SSO 및 SCIM 자동 계정 관리' : 'SAML 2.0 SSO & SCIM Provisioning',
|
||||
locale === 'ko' ? 'Zero Data Retention (ZDR) 데이터 보안 협약' : 'Zero Data Retention (ZDR) Compliance',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section id="pricing" className="relative py-24 md:py-32">
|
||||
<Container>
|
||||
|
|
@ -44,77 +137,166 @@ export function Pricing() {
|
|||
subtitle={t.pricing.subtitle}
|
||||
/>
|
||||
|
||||
{/* Billing toggle */}
|
||||
<div className="flex items-center justify-center gap-3 mb-10">
|
||||
<button
|
||||
onClick={() => setIsAnnual(false)}
|
||||
className={`font-mono text-xs uppercase tracking-widest px-4 py-2 rounded-panel transition-all ${
|
||||
!isAnnual
|
||||
? 'bg-surface-500 text-neutral-100 border border-white/[0.12]'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{t.pricing.billingToggleMonthly}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsAnnual(true)}
|
||||
className={`font-mono text-xs uppercase tracking-widest px-4 py-2 rounded-panel transition-all relative ${
|
||||
isAnnual
|
||||
? 'bg-surface-500 text-neutral-100 border border-brand-amber/30'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{t.pricing.billingToggleAnnual}
|
||||
{isAnnual && (
|
||||
<span className="absolute -top-2.5 -right-2 px-1.5 py-0.5 rounded-full bg-brand-amber text-white text-[9px] font-mono font-semibold uppercase tracking-wider">
|
||||
{t.pricing.savePercent}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{/* Reverse Trial Banner */}
|
||||
<div className="mb-10 p-4 rounded-xl bg-gradient-to-r from-brand-amber/20 via-surface-700/60 to-brand-amber/20 border border-brand-amber/40 shadow-glow-sm flex flex-col md:flex-row items-center justify-between gap-4 max-w-4xl mx-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">✨</span>
|
||||
<div className="text-left">
|
||||
<p className="font-mono text-xs font-bold uppercase tracking-wider text-brand-amber">
|
||||
{locale === 'ko' ? '14일 Reverse-Trial 무료 체험' : '14-Day Free Reverse Trial'}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-300">
|
||||
{locale === 'ko'
|
||||
? '앱 설치 즉시 신용카드 등록 없이 Pro+의 모든 AI 기능을 14일 동안 무료로 체험할 수 있습니다.'
|
||||
: 'Experience full Pro+ capabilities for 14 days immediately after install — no credit card required.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-3 py-1 rounded-full bg-brand-amber text-neutral-950 font-mono text-[10px] font-bold uppercase tracking-widest whitespace-nowrap">
|
||||
{locale === 'ko' ? '자동 활성화' : 'Zero Friction'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Pricing table */}
|
||||
{/* Currency and Billing Toggles */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 mb-12">
|
||||
{/* Currency Toggle */}
|
||||
<div className="flex items-center rounded-panel bg-surface-800 p-1 border border-white/[0.08]">
|
||||
<button
|
||||
onClick={() => setCurrency('usd')}
|
||||
className={`font-mono text-xs px-3.5 py-1.5 rounded transition-all ${
|
||||
currency === 'usd'
|
||||
? 'bg-surface-500 text-neutral-100 font-bold shadow-sm'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
USD ($)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrency('krw')}
|
||||
className={`font-mono text-xs px-3.5 py-1.5 rounded transition-all ${
|
||||
currency === 'krw'
|
||||
? 'bg-surface-500 text-neutral-100 font-bold shadow-sm'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
KRW (₩)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Billing Cycle Toggle */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setIsAnnual(false)}
|
||||
className={`font-mono text-xs uppercase tracking-widest px-5 py-2.5 rounded-panel transition-all ${
|
||||
!isAnnual
|
||||
? 'bg-surface-500 text-neutral-100 border border-white/[0.12] shadow-sm'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{t.pricing.billingToggleMonthly}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsAnnual(true)}
|
||||
className={`font-mono text-xs uppercase tracking-widest px-5 py-2.5 rounded-panel transition-all relative ${
|
||||
isAnnual
|
||||
? 'bg-surface-500 text-neutral-100 border border-brand-amber/40 shadow-glow-sm'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{t.pricing.billingToggleAnnual}
|
||||
{isAnnual && (
|
||||
<span className="absolute -top-2.5 -right-2.5 px-2 py-0.5 rounded-full bg-brand-amber text-white text-[9px] font-mono font-bold uppercase tracking-wider shadow-sm">
|
||||
{t.pricing.savePercent}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4 Tier Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 mb-16">
|
||||
{tiers.map((tier) => (
|
||||
<div
|
||||
key={tier.id}
|
||||
className={`relative rounded-2xl p-6 flex flex-col justify-between noise-texture transition-all duration-300 ${
|
||||
tier.popular
|
||||
? 'bg-surface-700/70 border-2 border-brand-amber shadow-glow-md'
|
||||
: 'bg-surface-700/40 border border-white/[0.06] hover:border-white/[0.15]'
|
||||
}`}
|
||||
>
|
||||
{tier.popular && (
|
||||
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2">
|
||||
<span className="px-3.5 py-1 rounded-full bg-brand-amber text-white text-nano font-mono font-bold uppercase tracking-widest shadow-md">
|
||||
{t.pricing.popular}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className={`font-mono text-xs font-bold uppercase tracking-widest ${tier.popular ? 'text-brand-amber' : 'text-neutral-400'}`}>
|
||||
{tier.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex items-baseline gap-1.5">
|
||||
<span className="font-display text-3xl font-extrabold text-neutral-50">
|
||||
{tier.price}
|
||||
</span>
|
||||
<span className="font-sans text-xs text-neutral-400 font-normal">
|
||||
{tier.period}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{tier.subPrice && (
|
||||
<div className="font-mono text-nano text-neutral-500 mb-4">
|
||||
{tier.subPrice}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-neutral-400 mb-5 leading-relaxed min-h-[38px]">
|
||||
{tier.desc}
|
||||
</p>
|
||||
|
||||
<div className="h-px bg-white/[0.06] mb-5" />
|
||||
|
||||
<ul className="space-y-2.5 mb-7">
|
||||
{tier.features.map((feat, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2.5 text-xs text-neutral-300">
|
||||
<span className="text-emerald-400 font-bold mt-0.5">✓</span>
|
||||
<span>{feat}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<GlowButton href="#" variant={tier.buttonVariant} size="md" className="w-full text-xs">
|
||||
{tier.buttonText}
|
||||
</GlowButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Detailed Comparison Table */}
|
||||
<div className="overflow-x-auto -mx-5 md:mx-0">
|
||||
<table className="w-full min-w-[640px] border-collapse">
|
||||
<table className="w-full min-w-[700px] border-collapse bg-surface-800/30 rounded-xl overflow-hidden border border-white/[0.04]">
|
||||
{/* Header */}
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.08]">
|
||||
<th className="text-left py-4 px-4 font-mono text-nano uppercase tracking-widest text-neutral-500 w-[40%]">
|
||||
<tr className="border-b border-white/[0.08] bg-surface-700/50">
|
||||
<th className="text-left py-4 px-5 font-mono text-nano uppercase tracking-widest text-neutral-400 w-[35%]">
|
||||
{t.pricing.featureLabel}
|
||||
</th>
|
||||
<th className="text-center py-4 px-4 w-[20%]">
|
||||
<div className="font-mono text-nano uppercase tracking-widest text-neutral-500 mb-1">{t.pricing.free}</div>
|
||||
<div className="font-display text-xl font-bold text-neutral-300">$0</div>
|
||||
<div className="font-mono text-nano text-neutral-600">{t.pricing.forever}</div>
|
||||
<th className="text-center py-4 px-3 w-[15%] font-mono text-nano uppercase tracking-widest text-neutral-300">
|
||||
{t.pricing.free}
|
||||
</th>
|
||||
<th className="text-center py-4 px-4 w-[20%] relative">
|
||||
<div className="absolute inset-x-0 -top-3 flex justify-center">
|
||||
<span className="px-3 py-0.5 rounded-full bg-brand-amber text-white text-nano font-mono font-semibold uppercase tracking-widest">
|
||||
{t.pricing.popular}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-nano uppercase tracking-widest text-brand-amber mt-4 mb-1">{t.pricing.pro}</div>
|
||||
<div className="font-display text-xl font-bold text-neutral-100">
|
||||
${proPrice}
|
||||
<span className="text-sm font-normal text-neutral-500">{periodLabel}</span>
|
||||
</div>
|
||||
{isAnnual && (
|
||||
<div className="font-mono text-nano text-neutral-600">
|
||||
${(PRICES.pro.annual / 12).toFixed(1)}{t.pricing.perMonth}
|
||||
</div>
|
||||
)}
|
||||
<th className="text-center py-4 px-3 w-[18%] font-mono text-nano uppercase tracking-widest text-brand-amber bg-brand-amber/[0.04]">
|
||||
{t.pricing.pro}
|
||||
</th>
|
||||
<th className="text-center py-4 px-4 w-[20%]">
|
||||
<div className="font-mono text-nano uppercase tracking-widest text-neutral-500 mb-1">{t.pricing.proPlus}</div>
|
||||
<div className="font-display text-xl font-bold text-neutral-300">
|
||||
${proPlusPrice}
|
||||
<span className="text-sm font-normal text-neutral-500">{periodLabel}</span>
|
||||
</div>
|
||||
{isAnnual && (
|
||||
<div className="font-mono text-nano text-neutral-600">
|
||||
${(PRICES.proPlus.annual / 12).toFixed(1)}{t.pricing.perMonth}
|
||||
</div>
|
||||
)}
|
||||
<th className="text-center py-4 px-3 w-[16%] font-mono text-nano uppercase tracking-widest text-neutral-300">
|
||||
{t.pricing.proPlus}
|
||||
</th>
|
||||
<th className="text-center py-4 px-3 w-[16%] font-mono text-nano uppercase tracking-widest text-neutral-300">
|
||||
Team
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -128,47 +310,34 @@ export function Pricing() {
|
|||
i % 2 === 0 ? 'bg-transparent' : 'bg-surface-800/20'
|
||||
}`}
|
||||
>
|
||||
<td className="py-3 px-4 font-mono text-xs text-neutral-400">
|
||||
<td className="py-3 px-5 font-sans text-xs text-neutral-300 font-medium">
|
||||
{row.feature}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<td className="py-3 px-3 text-center">
|
||||
<CellValue value={row.free} />
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center bg-brand-amber-muted/30">
|
||||
<td className="py-3 px-3 text-center bg-brand-amber/[0.04]">
|
||||
<CellValue value={row.pro} />
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<td className="py-3 px-3 text-center">
|
||||
<CellValue value={row.proPlus} />
|
||||
</td>
|
||||
<td className="py-3 px-3 text-center">
|
||||
<CellValue value={true} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* CTA row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-8">
|
||||
<div className="flex justify-center">
|
||||
<GlowButton href="#" variant="secondary" size="md">
|
||||
{t.pricing.downloadFree}
|
||||
</GlowButton>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<GlowButton href="#" variant="primary" size="md">
|
||||
{t.pricing.getPro}
|
||||
</GlowButton>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<GlowButton href="#" variant="secondary" size="md">
|
||||
{t.pricing.getProPlus}
|
||||
</GlowButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-center font-mono text-nano text-neutral-600 mt-6 uppercase tracking-widest">
|
||||
{t.pricing.taxNote}
|
||||
<p className="text-center font-mono text-nano text-neutral-500 mt-8 uppercase tracking-widest">
|
||||
{locale === 'ko'
|
||||
? '모든 가격은 세금 별도입니다. Stripe & Toss Payments / PortOne / Payple을 통한 안전한 결제 및 세금계산서 자동 발행.'
|
||||
: 'All prices exclude tax. Secure global checkout via Stripe and localized recurring payments.'}
|
||||
</p>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,35 @@ import { Container } from '../components/Container'
|
|||
import { Led } from '../components/Led'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
const ledColors: Array<'amber' | 'green' | 'red'> = [
|
||||
'green', 'green', 'green', 'green', 'green', 'green',
|
||||
]
|
||||
|
||||
export function Privacy() {
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const comparisonRows = [
|
||||
{
|
||||
feature: locale === 'ko' ? '음성 데이터 외부 서버 전송' : 'Voice Uploaded to External Servers',
|
||||
d3ro: locale === 'ko' ? '0% (절대 전송 안 함)' : '0% (Never Uploaded)',
|
||||
cloud: locale === 'ko' ? '100% (클라우드 저장)' : '100% (Saved to Cloud)',
|
||||
d3roOk: true,
|
||||
},
|
||||
{
|
||||
feature: locale === 'ko' ? '오프라인 단독 동작 가능 여부' : '100% Offline / Air-Gapped Operation',
|
||||
d3ro: locale === 'ko' ? '완전 지원 (인터넷 불필요)' : 'Fully Supported (No Internet Needed)',
|
||||
cloud: locale === 'ko' ? '불가 (연결 끊기면 중단)' : 'Fails without Internet',
|
||||
d3roOk: true,
|
||||
},
|
||||
{
|
||||
feature: locale === 'ko' ? '네트워크 전송 지연 (RTT)' : 'Network Roundtrip Latency',
|
||||
d3ro: '0ms (On-Device Memory)',
|
||||
cloud: '600ms ~ 2,500ms',
|
||||
d3roOk: true,
|
||||
},
|
||||
{
|
||||
feature: locale === 'ko' ? '무제한 딕테이션 비용' : 'Unlimited Voice Dictation Cost',
|
||||
d3ro: locale === 'ko' ? '무료 ($0 Forever)' : '$0 Forever Free',
|
||||
cloud: '$15 ~ $30 / Month',
|
||||
d3roOk: true,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section id="privacy" className="relative py-24 md:py-32 overflow-hidden">
|
||||
|
|
@ -20,31 +43,31 @@ export function Privacy() {
|
|||
/>
|
||||
|
||||
{/* CRT instrument panel */}
|
||||
<div className="crt-screen rounded-card p-6 md:p-10 noise-texture mb-12">
|
||||
<div className="crt-screen rounded-2xl p-6 md:p-10 noise-texture mb-12 shadow-2xl border border-white/[0.08]">
|
||||
<div className="relative z-10">
|
||||
{/* Panel header */}
|
||||
<div className="flex items-center justify-between mb-8 pb-4 border-b border-white/[0.04]">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60">
|
||||
{t.privacy.monitorTitle}
|
||||
</span>
|
||||
<div className="flex items-center justify-between mb-8 pb-4 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" pulse />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400/60">
|
||||
{t.privacy.allClear}
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400">
|
||||
{t.privacy.monitorTitle}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-400 bg-surface-600/60 px-2.5 py-1 rounded border border-white/[0.04]">
|
||||
AIR-GAP COMPLIANT
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Metric grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6 md:gap-8">
|
||||
{t.privacy.metrics.map((metric, i) => (
|
||||
<div key={i} className="flex flex-col gap-2">
|
||||
<div key={i} className="flex flex-col gap-2 p-3 rounded-lg bg-surface-900/60 border border-white/[0.03]">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-500">
|
||||
{metric.label}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color={ledColors[i]} size="sm" />
|
||||
<span className="font-mono text-sm font-semibold text-neutral-200 tracking-wide">
|
||||
<Led color="green" size="sm" />
|
||||
<span className="font-mono text-sm font-semibold text-neutral-100 tracking-wide">
|
||||
{metric.value}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -54,19 +77,45 @@ export function Privacy() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comparison Matrix: D3RO Voice vs Cloud Services */}
|
||||
<div className="mb-12 overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse rounded-xl overflow-hidden bg-surface-800/40 border border-white/[0.06]">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.08] bg-surface-700/50 font-mono text-nano uppercase tracking-wider text-neutral-400">
|
||||
<th className="p-4 w-1/2">{locale === 'ko' ? '보안 및 프라이버시 비교 기준' : 'Security & Privacy Criterion'}</th>
|
||||
<th className="p-4 text-brand-amber font-bold w-1/4">D3RO VOICE (LOCAL)</th>
|
||||
<th className="p-4 text-neutral-500 w-1/4">CLOUD STT SERVICES</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.04] text-xs">
|
||||
{comparisonRows.map((row, idx) => (
|
||||
<tr key={idx} className="hover:bg-surface-700/30 transition-colors">
|
||||
<td className="p-4 font-sans font-medium text-neutral-200">{row.feature}</td>
|
||||
<td className="p-4 font-mono font-bold text-emerald-400 bg-emerald-500/[0.04]">
|
||||
✓ {row.d3ro}
|
||||
</td>
|
||||
<td className="p-4 font-mono text-neutral-500">
|
||||
{row.cloud}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Guarantees list */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{t.privacy.guarantees.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-start gap-3 p-4 rounded-card bg-surface-700/30 border border-white/[0.03]"
|
||||
className="flex items-start gap-3 p-4 rounded-xl bg-surface-700/40 border border-white/[0.04]"
|
||||
>
|
||||
<div className="w-5 h-5 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-3 h-3 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-300 leading-relaxed">{item}</span>
|
||||
<span className="text-sm text-neutral-300 leading-relaxed font-medium">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue