All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m31s
Backend: - Add source_language column to glossaries table - Add translations JSON column to glossary_terms table - Alembic migration for schema changes - format_glossary_for_prompt now language-aware: extracts correct translation per target language, falls back to EN reference for templates with only FR→EN data - CRUD routes accept/return source_language and translations - Pydantic schemas updated Frontend: - Types updated: GlossaryTerm now has translations: Record<string, string> - Glossary/GlossaryListItem now have source_language - Added SUPPORTED_LANGUAGES constant (13 languages) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
218 lines
6.2 KiB
TypeScript
218 lines
6.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useRef } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Download, Upload } from 'lucide-react';
|
|
import { TermEditor } from './TermEditor';
|
|
import { exportGlossaryToCsv, parseCsvToTerms } from './csvUtils';
|
|
import { useToast } from '@/components/ui/toast';
|
|
import type { Glossary, GlossaryTermInput } from './types';
|
|
import { MAX_TERMS_PER_GLOSSARY } from './types';
|
|
|
|
interface EditGlossaryDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
glossary: Glossary | null;
|
|
onSave: (id: string, data: { name: string; terms: GlossaryTermInput[] }) => Promise<void>;
|
|
isSaving: boolean;
|
|
}
|
|
|
|
export function EditGlossaryDialog({
|
|
open,
|
|
onOpenChange,
|
|
glossary,
|
|
onSave,
|
|
isSaving,
|
|
}: EditGlossaryDialogProps) {
|
|
const [name, setName] = useState('');
|
|
const [terms, setTerms] = useState<GlossaryTermInput[]>([]);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const isInitialized = useRef(false);
|
|
|
|
if (glossary && !isInitialized.current) {
|
|
setName(glossary.name);
|
|
setTerms(glossary.terms.map(t => ({ source: t.source, target: t.target })));
|
|
isInitialized.current = true;
|
|
}
|
|
|
|
if (!open && isInitialized.current) {
|
|
isInitialized.current = false;
|
|
}
|
|
|
|
const handleSave = useCallback(async () => {
|
|
if (!glossary || !name.trim()) return;
|
|
|
|
const validTerms = terms.filter(t => t.source.trim() && t.target.trim());
|
|
|
|
await onSave(glossary.id, {
|
|
name: name.trim(),
|
|
terms: validTerms,
|
|
});
|
|
}, [glossary, name, terms, onSave]);
|
|
|
|
const handleExport = useCallback(() => {
|
|
if (!glossary) return;
|
|
|
|
const glossaryWithCurrentTerms: Glossary = {
|
|
...glossary,
|
|
name,
|
|
terms: terms.map((t, i) => ({
|
|
id: `temp-${i}`,
|
|
source: t.source,
|
|
target: t.target,
|
|
translations: t.translations || {},
|
|
created_at: null,
|
|
})),
|
|
};
|
|
|
|
exportGlossaryToCsv(glossaryWithCurrentTerms);
|
|
}, [glossary, name, terms]);
|
|
|
|
const handleImportClick = useCallback(() => {
|
|
fileInputRef.current?.click();
|
|
}, []);
|
|
|
|
const { toast } = useToast();
|
|
|
|
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const text = event.target?.result;
|
|
if (typeof text === 'string') {
|
|
const importedTerms = parseCsvToTerms(text);
|
|
if (importedTerms.length > 0) {
|
|
if (importedTerms.length > MAX_TERMS_PER_GLOSSARY) {
|
|
toast({
|
|
variant: 'destructive',
|
|
title: 'Import failed',
|
|
description: `CSV contains ${importedTerms.length} terms, but maximum is ${MAX_TERMS_PER_GLOSSARY}. Please reduce the number of terms.`,
|
|
});
|
|
e.target.value = '';
|
|
return;
|
|
}
|
|
setTerms(importedTerms);
|
|
toast({
|
|
title: 'Import successful',
|
|
description: `${importedTerms.length} terms imported successfully.`,
|
|
});
|
|
} else {
|
|
toast({
|
|
variant: 'destructive',
|
|
title: 'Import failed',
|
|
description: 'No valid terms found in CSV file.',
|
|
});
|
|
}
|
|
}
|
|
};
|
|
reader.onerror = () => {
|
|
toast({
|
|
variant: 'destructive',
|
|
title: 'Import failed',
|
|
description: 'Failed to read CSV file.',
|
|
});
|
|
};
|
|
reader.readAsText(file);
|
|
|
|
e.target.value = '';
|
|
}, [toast]);
|
|
|
|
const validTermsCount = terms.filter(t => t.source.trim() && t.target.trim()).length;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Glossary</DialogTitle>
|
|
<DialogDescription>
|
|
Update the glossary name and term pairs.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="glossary-name">Glossary Name</Label>
|
|
<Input
|
|
id="glossary-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Enter glossary name..."
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Terms ({validTermsCount} valid)</Label>
|
|
<TermEditor
|
|
terms={terms}
|
|
onChange={setTerms}
|
|
disabled={isSaving}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 pt-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleExport}
|
|
disabled={isSaving || validTermsCount === 0}
|
|
className="gap-1.5"
|
|
>
|
|
<Download className="size-3.5" />
|
|
Export CSV
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleImportClick}
|
|
disabled={isSaving}
|
|
className="gap-1.5"
|
|
>
|
|
<Upload className="size-3.5" />
|
|
Import CSV
|
|
</Button>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept=".csv"
|
|
onChange={handleFileChange}
|
|
className="hidden"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={isSaving}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={isSaving || !name.trim()}
|
|
>
|
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|