Files
office_translator/office-translator-landing-page/components/waitlist-section.tsx
sepehr 526c87348f
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s
feat(translation): quality pipeline overhaul + new features (audit 2026-08-29)
Translation quality & format preservation:
- Word: merge adjacent same-format runs into one unit (sentence-level
  coherence like inline-tag handling); translate comments/balloons;
  dedupe textbox collection (was translated twice); RTL no longer
  overrides center/justify alignment; CJK/Arabic font hints (eastAsia/cs)
- PPTX: chart translations now actually reach the output file
  (ChartPart.blob is read-only — rewrite chart XML in the saved ZIP);
  CJK typeface hints (a:ea)
- Excel: sheet renames no longer break references — rewrite cell
  formulas (3D/quoted), defined names, data validations, cond. formats
- PDF: bold/italic honored (hebo/heit/hebi); table cells never merge;
  unchanged blocks left untouched (typography preserved, fixes duplicate
  hyperlinks); attempted/changed stats + route gate now cover PDF;
  CJK font paths; scanned PDFs via Mistral OCR (detection + admin settings)

Features:
- formality param (formal/informal) + automatic regional-variant prompts
- output_mode=bilingual docx (source above translation)
- per-user translation memory on Redis (falls back to LRU), context-hashed
- QA report + 0-100 confidence score in job status; L0 on by default
- OpenAI-compatible providers: whole chunk in ONE numbered-JSON request
  (~15x fewer calls) with per-item fallback; base prompt always present
  (custom prompt no longer replaces translation instructions)

Infra & marketing alignment:
- plan-based engine gating + vision gating (closes paid-engine leak);
  /providers/available filtered per plan; 107 languages exposed
- zh-CN/zh-TW validation fixed; libmagic disabled on Windows (native crash)
- admin: Mistral OCR settings + engine status dashboard; httpx<0.28 pin
  (TestClient breakage); Prometheus test fixture fixed
- marketing docs aligned with code (PDF+OCR, retention, engines, pricing)
- security: .env.ionos/.env.production/provider_settings.json removed

Tests: 1173 passed / 0 failed (6 network tests deselected: free Google
endpoint temporarily blocked from this machine)
2026-08-29 18:38:09 +02:00

142 lines
5.1 KiB
TypeScript

"use client"
import { useState, useCallback } from "react"
import { Mail, Loader2, CheckCircle2, ArrowRight } from "lucide-react"
import { track } from "@vercel/analytics"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
type Status = "idle" | "loading" | "success" | "error"
// The backend is proxied under the same origin via the rewrites in
// next.config.mjs, so the form never needs the backend origin (no CORS).
const WAITLIST_ENDPOINT = "/api/v1/waitlist"
const INTERESTS = [
"I translate documents regularly",
"I run a translation agency",
"I manage a multilingual team",
"I'm a developer (API integration)",
"Just exploring",
]
export function WaitlistSection() {
const [email, setEmail] = useState("")
const [interest, setInterest] = useState(INTERESTS[0])
const [status, setStatus] = useState<Status>("idle")
const [message, setMessage] = useState("")
const submit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault()
if (!email || status === "loading") return
setStatus("loading")
setMessage("")
try {
const res = await fetch(`/api/v1/waitlist`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, interest }),
})
const data = await res.json().catch(() => ({}))
if (res.ok) {
setStatus("success")
track("waitlist_joined", { interest })
setMessage(
data?.data?.status === "already_joined"
? "You are already on the list. We will email you at launch."
: "You are on the list. We will email you at launch."
)
setEmail("")
} else {
setStatus("error")
track("waitlist_error", { status: res.status })
setMessage(data?.message ?? "Something went wrong. Please try again.")
}
} catch {
setStatus("error")
setMessage("Network error. Please try again later.")
}
},
[email, interest, status]
)
return (
<section
id="waitlist"
className="flex flex-col items-center gap-6 px-6 py-16 md:py-20"
>
<div className="flex w-full max-w-2xl flex-col items-center gap-4 rounded-2xl border border-border bg-card px-6 py-10 text-center shadow-lg md:px-12">
<div className="flex size-12 items-center justify-center rounded-xl bg-primary">
<Mail className="size-6 text-primary-foreground" />
</div>
<div className="flex flex-col gap-1.5">
<h2 className="text-balance text-2xl font-bold tracking-tight text-foreground md:text-3xl">
Be first in line at launch
</h2>
<p className="text-pretty text-sm text-muted-foreground">
Join the waitlist and get early access, launch-day pricing and the
&ldquo;keep your format&rdquo; playbook. No spam, one email at launch.
</p>
</div>
{status === "success" ? (
<div className="flex w-full max-w-md flex-col items-center gap-2 rounded-lg border border-success/40 bg-success/10 px-5 py-4">
<CheckCircle2 className="size-5 text-success" />
<p className="text-sm font-medium text-foreground">{message}</p>
</div>
) : (
<form onSubmit={submit} className="flex w-full max-w-md flex-col gap-3">
<div className="flex flex-col gap-3 sm:flex-row">
<Input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@company.com"
aria-label="Email address"
className="flex-1"
disabled={status === "loading"}
/>
<Button
type="submit"
size="lg"
disabled={status === "loading" || !email}
>
{status === "loading" ? (
<>
<Loader2 className="size-4 animate-spin" /> Joining
</>
) : (
<>
Join Waitlist <ArrowRight className="size-4" />
</>
)}
</Button>
</div>
<select
value={interest}
onChange={(e) => setInterest(e.target.value)}
aria-label="What best describes you?"
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring/50"
>
{INTERESTS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
{status === "error" && (
<p className="text-xs text-destructive">{message}</p>
)}
<p className="text-[11px] text-muted-foreground">
We only use your address to notify you at launch. Unsubscribe any
time.
</p>
</form>
)}
</div>
</section>
)
}