Files
office_translator/office-translator-landing-page/components/glossary-context-card.tsx
Sepehr Ramezani 26bd096a06 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>
2026-04-25 15:01:47 +02:00

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>
)
}