feat(ui,api): wave 3 — editorial pricing, real cancel, server history, DeepL purge
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m36s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m36s
Pricing: full editorial redesign — serif card headers with accent pills
replace the colored font-black blocks, tone sweep across toggle/metrics/
features/CTAs, PLAN_COLORS removed; one design system app-wide.
Translate: decorative titles one step down (CTA hierarchy restored);
glossary and image-translation blocks hidden entirely for free users
(progressive disclosure — three controls for free).
Reviews: XLIFF hint line explains the exchange format; backend errors
routed through a friendly mapper (session/not-found/rate-limit/server).
Landing: fabricated hero UI cards (fake 'Context Engine' overlay)
removed — the photo no longer promises screens that don't exist.
Nav: single DashboardNavLinks component shared by sidebar and mobile
drawer (was duplicated markup).
API: GET /api/v1/translations (user job history, paginated; completed
jobs retained 24h) and POST /api/v1/translations/{id}/cancel —
cooperative cancellation with worker checkpoints before dispatch and
before finalisation, reserved quota released immediately. Translate
monitor now offers a real 'Cancel translation' next to 'Back to start';
recent-jobs list reads server history first, localStorage fallback.
DeepL purge (backend): provider module, registry registration, config
attrs/defaults, dispatch branch, admin settings schema + test branch,
legacy availability block, validation rules, plan provider lists,
error-code mappings, MCP enums, translator prompt mention, related
tests updated/removed. Fallback resolver skips unknown providers, so
stale chains containing 'deepl' degrade gracefully.
Verified: backend 110 tests passed; frontend build exit 0, vitest 9/9,
0 missing i18n keys, eslint 63 errors (vs 64 at HEAD).
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useFileUpload } from './useFileUpload';
|
||||
import { useTranslationConfig } from './useTranslationConfig';
|
||||
import { useTranslationSubmit, getRecentJobs, type RecentJob } from './useTranslationSubmit';
|
||||
import { useTranslationSubmit, getRecentJobs, fetchServerHistory, type RecentJob } from './useTranslationSubmit';
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
import { ProviderSelector } from './ProviderSelector';
|
||||
import { GlossarySelector } from './GlossarySelector';
|
||||
@@ -122,9 +122,14 @@ export default function TranslatePage() {
|
||||
return () => { if (timerRef.current) clearInterval(timerRef.current); };
|
||||
}, [submit.status, submit.isSubmitting]);
|
||||
|
||||
// Recent jobs (client-side history) — loaded on mount, refreshed when a job completes
|
||||
// History: server list first (survives any device), localStorage as fallback
|
||||
useEffect(() => {
|
||||
setRecentJobs(getRecentJobs());
|
||||
let cancelled = false;
|
||||
fetchServerHistory().then((server) => {
|
||||
if (cancelled) return;
|
||||
setRecentJobs(server.length > 0 ? server : getRecentJobs());
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [submit.status]);
|
||||
|
||||
const handleTranslate = async () => {
|
||||
@@ -141,6 +146,17 @@ export default function TranslatePage() {
|
||||
await handleTranslate();
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
const ok = await submit.cancelJob();
|
||||
if (ok) {
|
||||
submit.reset();
|
||||
setElapsed(0);
|
||||
showError({ title: t('translate.cancelledTitle'), description: t('translate.cancelledDesc') });
|
||||
} else {
|
||||
showError({ title: t('translate.cancelFailedTitle'), description: t('translate.cancelFailedDesc') });
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewTranslation = () => { submit.reset(); upload.removeFile(); setElapsed(0); setRecentJobs(getRecentJobs()); };
|
||||
const handleDownload = async (jobId: string = submit.jobId ?? '') => {
|
||||
if (!jobId) return;
|
||||
@@ -220,7 +236,7 @@ export default function TranslatePage() {
|
||||
{showProcessing ? (
|
||||
<>
|
||||
<span className="accent-pill mb-4 block w-fit italic">{t('translate.header.processing')}</span>
|
||||
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||
<h1 className="text-3xl md:text-4xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||
<SplitTitle base={t('translate.header.aiActiveTitle')} accent={t('translate.header.aiActiveAccent')} />
|
||||
</h1>
|
||||
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">
|
||||
@@ -230,7 +246,7 @@ export default function TranslatePage() {
|
||||
) : showComplete ? (
|
||||
<>
|
||||
<span className="accent-pill mb-4 block w-fit italic">{t('translate.header.completed')}</span>
|
||||
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||
<h1 className="text-3xl md:text-4xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||
<SplitTitle base={t('translate.header.completedTitleBase')} accent={t('translate.header.completedTitleAccent')} />
|
||||
</h1>
|
||||
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed truncate max-w-xl">
|
||||
@@ -240,7 +256,7 @@ export default function TranslatePage() {
|
||||
) : (
|
||||
<>
|
||||
<span className="accent-pill mb-4 block w-fit">{t('translate.header.workspace')}</span>
|
||||
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||
<h1 className="text-3xl md:text-4xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||
<SplitTitle base={t('translate.header.translateDocBase')} accent={t('translate.header.translateDocAccent')} />
|
||||
</h1>
|
||||
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">
|
||||
@@ -596,7 +612,8 @@ export default function TranslatePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Glossary selector */}
|
||||
{/* Glossary selector — Pro only; hidden entirely for free users */}
|
||||
{config.isPro && (
|
||||
<GlossarySelector
|
||||
sourceLang={config.sourceLang}
|
||||
targetLang={config.targetLang}
|
||||
@@ -606,8 +623,10 @@ export default function TranslatePage() {
|
||||
onChange={config.setGlossaryId}
|
||||
disabled={submit.isSubmitting}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Translate Images */}
|
||||
{/* Translate Images — LLM mode only; hidden for free users */}
|
||||
{config.isPro && (
|
||||
<div className="bg-brand-muted/30 dark:bg-white/[0.02] border border-black/[0.03] dark:border-white/[0.03] p-4 rounded-xl space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -639,6 +658,8 @@ export default function TranslatePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
{/* PDF mode selector */}
|
||||
{isPdf && (
|
||||
<div className="space-y-2 text-left">
|
||||
@@ -741,11 +762,17 @@ export default function TranslatePage() {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleNewTranslation}
|
||||
title={t('translate.leaveScreenHint')}
|
||||
className="w-full mt-8 py-3.5 border border-black/10 dark:border-white/10 text-brand-dark/50 dark:text-white/50 rounded-2xl text-[10px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:bg-brand-muted/40 dark:hover:bg-white/5 hover:text-brand-dark dark:hover:text-white transition-all cursor-pointer"
|
||||
onClick={handleCancel}
|
||||
className="w-full mt-8 py-3.5 border border-red-200 dark:border-red-900/40 text-red-500 rounded-2xl text-[10px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:bg-red-50 dark:hover:bg-red-950/30 transition-all cursor-pointer"
|
||||
>
|
||||
<X size={13} />
|
||||
{t('translate.cancelAction')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNewTranslation}
|
||||
title={t('translate.leaveScreenHint')}
|
||||
className="w-full mt-2 py-2.5 text-[10px] font-bold uppercase tracking-[0.2em] text-brand-dark/50 dark:text-white/50 hover:text-brand-dark dark:hover:text-white transition-colors cursor-pointer"
|
||||
>
|
||||
{t('translate.leaveScreen')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user