feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useCallback } from "react"
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from "@/components/ui/tooltip"
|
||||
import {
|
||||
BookText,
|
||||
Plus,
|
||||
Trash2,
|
||||
ArrowRight,
|
||||
Info,
|
||||
Save,
|
||||
RotateCcw,
|
||||
} from "lucide-react"
|
||||
|
||||
interface GlossaryEntry {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
}
|
||||
|
||||
const defaultSystemPrompt =
|
||||
"Translate using formal medical terminology. Maintain the original document structure and formatting. Use British English spelling conventions."
|
||||
|
||||
const defaultGlossary: GlossaryEntry[] = [
|
||||
{ id: "1", source: "Bilan", target: "Balance Sheet" },
|
||||
{ id: "2", source: "Chiffre d'affaires", target: "Revenue" },
|
||||
{ id: "3", source: "Compte de resultat", target: "Income Statement" },
|
||||
]
|
||||
|
||||
export function GlossaryContextCard() {
|
||||
const [systemPrompt, setSystemPrompt] = useState(defaultSystemPrompt)
|
||||
const [glossary, setGlossary] = useState<GlossaryEntry[]>(defaultGlossary)
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const addTerm = useCallback(() => {
|
||||
setGlossary((prev) => [
|
||||
...prev,
|
||||
{ id: String(Date.now()), source: "", target: "" },
|
||||
])
|
||||
}, [])
|
||||
|
||||
const removeTerm = useCallback((id: string) => {
|
||||
setGlossary((prev) => prev.filter((entry) => entry.id !== id))
|
||||
}, [])
|
||||
|
||||
const updateTerm = useCallback(
|
||||
(id: string, field: "source" | "target", value: string) => {
|
||||
setGlossary((prev) =>
|
||||
prev.map((entry) =>
|
||||
entry.id === id ? { ...entry, [field]: value } : entry
|
||||
)
|
||||
)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}, [])
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setSystemPrompt(defaultSystemPrompt)
|
||||
setGlossary(defaultGlossary)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex size-8 items-center justify-center rounded-lg bg-accent/10">
|
||||
<BookText className="size-4 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-base">LLM Context & Glossaries</CardTitle>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="border border-accent/20 bg-accent/10 text-accent text-[10px] px-1.5"
|
||||
>
|
||||
Pro
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Fine-tune translations with custom instructions and terminology
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
{/* System Prompt */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<label
|
||||
htmlFor="system-prompt"
|
||||
className="text-sm font-semibold text-foreground"
|
||||
>
|
||||
System Prompt
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button aria-label="What is a system prompt?" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
<Info className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Instructions given to the LLM engine before every translation. Affects tone, terminology, and style.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Textarea
|
||||
id="system-prompt"
|
||||
value={systemPrompt}
|
||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||
rows={4}
|
||||
className="resize-none font-mono text-xs leading-relaxed"
|
||||
placeholder="e.g., Translate using formal medical terminology..."
|
||||
/>
|
||||
<p className="mt-1.5 text-xs text-muted-foreground">
|
||||
This prompt is prepended to every Pro LLM translation request.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Glossary */}
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
Custom Glossary
|
||||
</h3>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{glossary.length} {glossary.length === 1 ? "term" : "terms"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Define exact translations for specific terms. These overrides take highest priority during translation.
|
||||
</p>
|
||||
|
||||
{/* Glossary header */}
|
||||
<div className="mb-2 grid grid-cols-[1fr_32px_1fr_36px] items-center gap-2 px-1">
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Source Term
|
||||
</span>
|
||||
<span />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Target Translation
|
||||
</span>
|
||||
<span />
|
||||
</div>
|
||||
|
||||
{/* Glossary entries */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{glossary.map((entry, index) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="group grid grid-cols-[1fr_32px_1fr_36px] items-center gap-2"
|
||||
>
|
||||
<Input
|
||||
value={entry.source}
|
||||
onChange={(e) =>
|
||||
updateTerm(entry.id, "source", e.target.value)
|
||||
}
|
||||
placeholder="Source term..."
|
||||
className="font-mono text-xs"
|
||||
aria-label={`Source term ${index + 1}`}
|
||||
/>
|
||||
<div className="flex items-center justify-center">
|
||||
<ArrowRight className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<Input
|
||||
value={entry.target}
|
||||
onChange={(e) =>
|
||||
updateTerm(entry.id, "target", e.target.value)
|
||||
}
|
||||
placeholder="Translation..."
|
||||
className="font-mono text-xs"
|
||||
aria-label={`Target translation ${index + 1}`}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => removeTerm(entry.id)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label={`Remove term ${index + 1}`}
|
||||
>
|
||||
<Trash2 className="size-3.5 text-muted-foreground hover:text-destructive" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Remove term</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add term button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addTerm}
|
||||
className="mt-3 gap-1.5 border-dashed"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
Add Term
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="ghost" size="sm" onClick={handleReset} className="gap-1.5 text-muted-foreground">
|
||||
<RotateCcw className="size-3.5" />
|
||||
Reset to defaults
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} className="gap-1.5">
|
||||
<Save className="size-3.5" />
|
||||
{saved ? "Saved!" : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user