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>
247 lines
8.0 KiB
TypeScript
247 lines
8.0 KiB
TypeScript
"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>
|
|
)
|
|
}
|