TransHub / client /src /pages /Toolkit.tsx
linguabot's picture
Upload folder using huggingface_hub
5a11b0a verified
import React, { useEffect, useMemo, useRef, useState } from 'react';
// Syntax Reorderer removed per request
import Refinity from '../components/Refinity';
import { api } from '../services/api';
import {
DocumentTextIcon,
WrenchScrewdriverIcon,
ArrowTopRightOnSquareIcon,
ClipboardIcon,
ArrowsRightLeftIcon
} from '@heroicons/react/24/outline';
type ToolKey = 'quality-lens' | 'mymemory' | 'dictionary' | 'mt' | 'links' | 'refinity';
interface MyMemoryResponse {
responseData?: {
translatedText?: string;
match?: number;
};
matches?: Array<{
translation: string;
quality?: string | number;
match?: number;
segment?: string;
reference?: string;
'created-by'?: string;
}>;
}
const languages = [
{ label: 'English', code: 'en' },
{ label: 'Chinese', code: 'zh-CN' }
];
type DictEntry = {
word: string;
entries: Array<{
language: { code: string; name: string };
partOfSpeech?: string;
pronunciations?: Array<{ type?: string; text?: string; tags?: string[] }>;
senses?: Array<{
definition?: string;
examples?: string[];
translations?: Array<{ language: { code: string; name: string }; word: string }>;
}>;
}>;
source?: { url?: string; license?: { name?: string; url?: string } };
};
const TOOL_URLS: Record<ToolKey, string> = {
'quality-lens': 'https://linguabot-quality-lens.hf.space',
'mymemory': '',
'dictionary': '',
'mt': '',
'links': '',
'refinity': ''
};
// Provided by user for MyMemory API
const MYMEMORY_KEY = '64031566ea7d91c9fe6b';
// Allowed by user (MT page hidden from visitors)
const DEEPL_KEY = '9dcb19a8-2c97-42e3-96c0-76c066822750:fx';
const GOOGLE_KEY = 'AIzaSyBPyhVuTBBUAM-yvvfO8FmxzyJPpFPZDDU';
const Toolkit: React.FC = () => {
const [links, setLinks] = useState<Array<{ _id?: string; title: string; url: string; desc?: string; order?: number }>>([]);
const [linksLoading, setLinksLoading] = useState(false);
const [linksError, setLinksError] = useState('');
const [isAdmin, setIsAdmin] = useState(false);
const [editItem, setEditItem] = useState<{ _id?: string; title?: string; url?: string; desc?: string; order?: number } | null>(null);
const [selectedTool, setSelectedTool] = useState<ToolKey>(() => {
const raw = (localStorage.getItem('selectedToolkitTool') as ToolKey) || 'refinity';
const allowed = new Set(['quality-lens','mymemory','dictionary','mt','links','refinity']);
return (allowed.has(raw as any) ? raw : 'refinity') as ToolKey;
});
const [isToolTransitioning, setIsToolTransitioning] = useState(false);
const [iframeLoading, setIframeLoading] = useState(true);
const [isVisitor, setIsVisitor] = useState<boolean>(true);
// MyMemory state
const [mmSource, setMmSource] = useState<string>(() => localStorage.getItem('mm_source') || '');
const [mmFrom, setMmFrom] = useState<string>(() => localStorage.getItem('mm_from') || 'en');
const [mmTo, setMmTo] = useState<string>(() => localStorage.getItem('mm_to') || 'zh-CN');
const [mmLoading, setMmLoading] = useState(false);
const [mmError, setMmError] = useState<string>('');
const [mmResults, setMmResults] = useState<Array<{ translation: string; score: number; source?: string }>>([]);
// Dictionary state (EN ⇄ ZH)
const [dictWord, setDictWord] = useState<string>('');
const [dictFrom, setDictFrom] = useState<'en' | 'zh'>('en');
const [dictTo, setDictTo] = useState<'en' | 'zh'>('zh');
const [dictLoading, setDictLoading] = useState<boolean>(false);
const [dictError, setDictError] = useState<string>('');
const [dictResults, setDictResults] = useState<DictEntry | null>(null);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [showWantWords, setShowWantWords] = useState<boolean>(false);
// MT state
const [mtSource, setMtSource] = useState<string>('');
const [mtFrom, setMtFrom] = useState<string>('en');
const [mtTo, setMtTo] = useState<string>('zh-CN');
const [mtProvider, setMtProvider] = useState<'deepl' | 'google'>('deepl');
const [mtLoading, setMtLoading] = useState(false);
const [mtError, setMtError] = useState<string>('');
const [mtResult, setMtResult] = useState<string>('');
useEffect(() => {
localStorage.setItem('selectedToolkitTool', selectedTool);
}, [selectedTool]);
useEffect(() => {
try {
const u = localStorage.getItem('user');
const role = u ? (JSON.parse(u)?.role || 'visitor') : 'visitor';
const viewMode = (localStorage.getItem('viewMode') || 'auto');
setIsVisitor(role === 'visitor');
setIsAdmin(viewMode === 'student' ? false : role === 'admin');
} catch {
setIsVisitor(true);
setIsAdmin(false);
}
}, []);
const tools = useMemo(() => {
const base = [
{ key: 'quality-lens' as ToolKey, name: 'Quality Lens', desc: 'BLASER/COMET QE + Hallucination', type: 'iframe' },
{ key: 'mymemory' as ToolKey, name: 'MyMemory', desc: 'Public MT memory lookup', type: 'native' },
{ key: 'dictionary' as ToolKey, name: 'Dictionary (EN⇄ZH)', desc: 'Iciba suggest', type: 'native' },
{ key: 'refinity' as ToolKey, name: 'Deep Revision', desc: 'Version flow, compare, and revise', type: 'native' },
{ key: 'links' as ToolKey, name: 'Useful Links', desc: 'Curated external resources', type: 'native' }
];
if (!isVisitor) base.splice(2, 0, { key: 'mt' as ToolKey, name: 'MT (DeepL/Google)', desc: 'Translate with MT engines', type: 'native' });
return base;
}, [isVisitor]);
// Useful Links: load on tab open
useEffect(() => {
const loadLinks = async () => {
try {
setLinksLoading(true); setLinksError('');
const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
const resp = await fetch(`${base}/api/links`, { method: 'GET' });
if (!resp.ok) throw new Error('Failed');
const data = await resp.json();
setLinks(Array.isArray(data) ? data : []);
} catch {
setLinksError('Failed to load links');
} finally {
setLinksLoading(false);
}
};
if (selectedTool === 'links') loadLinks();
}, [selectedTool]);
const handleToolChange = async (tool: ToolKey) => {
if (tool === selectedTool) return;
setIsToolTransitioning(true);
setSelectedTool(tool);
// small delay for smooth spinner; iframe will clear its own loading when onLoad fires
await new Promise(res => setTimeout(res, 200));
setIsToolTransitioning(false);
if (tool === 'quality-lens') {
setIframeLoading(true);
}
};
const fetchMyMemory = async () => {
setMmError('');
setMmResults([]);
if (!mmSource.trim()) {
setMmError('Please enter source text');
return;
}
try {
setMmLoading(true);
localStorage.setItem('mm_source', mmSource);
localStorage.setItem('mm_from', mmFrom);
localStorage.setItem('mm_to', mmTo);
const q = encodeURIComponent(mmSource.trim());
// Request MT+matches and DB-only in parallel for robustness
const urlMt = `https://api.mymemory.translated.net/get?q=${q}&langpair=${encodeURIComponent(mmFrom)}|${encodeURIComponent(mmTo)}&mt=1&key=${encodeURIComponent(MYMEMORY_KEY)}`;
const urlDb = `https://api.mymemory.translated.net/get?q=${q}&langpair=${encodeURIComponent(mmFrom)}|${encodeURIComponent(mmTo)}&mt=0&key=${encodeURIComponent(MYMEMORY_KEY)}`;
const [resMt, resDb] = await Promise.all([
fetch(urlMt, { method: 'GET' }),
fetch(urlDb, { method: 'GET' })
]);
const dataMt: MyMemoryResponse = await resMt.json();
const dataDb: MyMemoryResponse = await resDb.json();
// MT suggestion
const mtSuggestion = dataMt.responseData?.translatedText?.trim() || '';
const mtScore = typeof dataMt.responseData?.match === 'number' ? dataMt.responseData?.match : 0;
// Collect DB matches from db-only response (preferred), fallback to mt response
const sourceMatches = Array.isArray(dataDb.matches) && dataDb.matches.length > 0
? dataDb.matches
: (Array.isArray(dataMt.matches) ? dataMt.matches : []);
const machineRe = /(google|bing|mt)/i;
const seen = new Set<string>();
const dbMatches: Array<{ translation: string; score: number; source?: string }> = [];
sourceMatches.forEach((m) => {
const t = (m.translation || '').trim();
if (!t) return;
if (mtSuggestion && t === mtSuggestion) return; // skip identical to MT
if (seen.has(t)) return;
// Skip machine-created entries when possible
const createdBy = (m['created-by'] as unknown as string) || '';
if (machineRe.test(createdBy)) return;
seen.add(t);
const score = typeof m.match === 'number' ? m.match : (typeof m.quality === 'number' ? Number(m.quality) : 0);
dbMatches.push({ translation: t, score, source: m.reference || createdBy || 'MyMemory' });
});
dbMatches.sort((a, b) => (b.score || 0) - (a.score || 0));
const top5 = dbMatches.slice(0, 5);
const combined: Array<{ translation: string; score: number; source?: string }> = [];
if (mtSuggestion) {
combined.push({ translation: mtSuggestion, score: mtScore, source: 'Machine Translation' });
}
combined.push(...top5);
setMmResults(combined);
} catch (e: any) {
setMmError('Failed to fetch suggestions. Please try again.');
} finally {
setMmLoading(false);
}
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
} catch {}
};
const translateMT = async () => {
setMtError(''); setMtResult('');
const text = mtSource.trim();
if (!text) { setMtError('Please enter source text'); return; }
try {
setMtLoading(true);
const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
const url = `${base}/api/mt/${mtProvider}`;
const body: any = { text, source: mtFrom, target: mtTo };
if (mtProvider === 'deepl') body.key = DEEPL_KEY;
if (mtProvider === 'google') body.key = GOOGLE_KEY;
const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const data = await resp.json().catch(() => ({}));
if (!resp.ok) throw new Error(data?.error || 'MT failed');
setMtResult((data?.translation || '').trim());
} catch (e:any) {
setMtError('Failed to translate');
} finally {
setMtLoading(false);
}
};
const IcibaSuggest: React.FC<{ term: string }> = ({ term }) => {
const [items, setItems] = useState<Array<{ key: string; paraphrase?: string }>>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
useEffect(() => {
const t = term.trim();
if (!t) { setItems([]); setError(''); return; }
let aborted = false;
(async () => {
try {
setLoading(true); setError('');
const url = `${base}/api/iciba/suggest?word=${encodeURIComponent(t)}`;
const resp = await fetch(url);
const data = await resp.json().catch(() => ({}));
if (aborted) return;
const list = Array.isArray(data.message) ? data.message : [];
setItems(list.map((it: any) => ({ key: it.key, paraphrase: it.paraphrase })));
} catch (e) {
if (!aborted) setError('');
} finally {
if (!aborted) setLoading(false);
}
})();
return () => { aborted = true; };
}, [term]);
if (!term.trim()) return null;
if (loading) return <div className="text-sm text-gray-500">Loading…</div>;
if (error) return null;
if (!items.length) return <div className="text-sm text-gray-500">No suggestions.</div>;
return (
<div className="space-y-2">
{items.slice(0, 6).map((it, idx) => (
<div key={idx} className="flex items-center justify-between bg-white rounded border border-gray-200 px-3 py-2">
<div className="text-gray-900">
<span className="font-medium mr-2">{it.key}</span>
<span className="text-sm text-gray-600">{it.paraphrase}</span>
</div>
<button onClick={() => setDictWord(it.key)} className="text-xs text-gray-600 hover:text-gray-900">Use</button>
</div>
))}
</div>
);
};
const detectLang = (text: string): 'en' | 'zh' => /[\u4e00-\u9fff]/.test(text) ? 'zh' : 'en';
const codeMatches = (code: string | undefined, target: 'en' | 'zh') => {
if (!code) return false;
const lower = code.toLowerCase();
if (target === 'zh') return lower === 'zh' || lower.startsWith('zh');
return lower === 'en' || lower.startsWith('en');
};
const fetchDictionary = async () => {
setDictError('');
setDictResults(null);
const term = dictWord.trim();
if (!term) { setDictError('Please enter a word'); return; }
try {
setDictLoading(true);
// If user didn’t set explicitly, attempt auto-detect from input once
const detected = detectLang(term);
const from = dictFrom || detected;
// Use backend proxy to avoid CORS and CDN edge issues
const BACKEND_BASE = ((api.defaults as any)?.baseURL as string) || '';
const base = BACKEND_BASE.replace(/\/$/, '');
const urlPrimary = `${base}/api/dictionary/${encodeURIComponent(from)}/${encodeURIComponent(term)}?translations=true`;
const resPrimary = await fetch(urlPrimary, { method: 'GET' });
let data: DictEntry | null = null;
if (resPrimary.ok) {
try { data = await resPrimary.json(); } catch { data = null; }
}
// Fallback to language-agnostic search if nothing found
const hasEntries = !!(data && Array.isArray((data as any).entries) && (data as any).entries.length > 0);
if (!hasEntries) {
const urlAll = `${base}/api/dictionary/all/${encodeURIComponent(term)}?translations=true`;
const resAll = await fetch(urlAll, { method: 'GET' });
if (resAll.ok) {
try { data = await resAll.json(); } catch { data = null; }
}
}
if (!data || !Array.isArray((data as any).entries) || (data as any).entries.length === 0) {
throw new Error('No results');
}
setDictResults(data as DictEntry);
} catch (e: any) {
setDictError('No results found or service unavailable');
} finally {
setDictLoading(false);
}
};
return (
<div className="min-h-screen bg-white py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center mb-4">
<img src="/icons/toolkit.svg" alt="Toolkit" className="h-8 w-8 mr-3" />
<h1 className="text-3xl font-bold text-gray-900">Toolkit</h1>
</div>
<p className="text-gray-600">Helpful tools for evaluation and reference. Pick a tool below.</p>
</div>
{/* Tool Selector */}
<div className="mb-6">
<div className="flex space-x-3 overflow-x-auto pb-2">
{tools.map(t => {
const isActive = selectedTool === t.key;
return (
<button
key={t.key}
onClick={() => handleToolChange(t.key)}
className={`relative inline-flex items-center justify-center rounded-2xl px-4 py-1.5 whitespace-nowrap transition-all duration-300 ease-out ring-1 ring-inset ${isActive ? 'ring-white/50 backdrop-brightness-110 backdrop-saturate-150' : 'ring-white/30'} backdrop-blur-md isolate shadow-[inset_0_0.5px_0_rgba(255,255,255,0.5),inset_0_-1px_1.5px_rgba(0,0,0,0.12)]`}
style={{ background: 'rgba(255,255,255,0.10)' }}
>
{/* Rim washes */}
<div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
{/* Soft glossy wash */}
<div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
{/* Tiny hotspot near TL */}
<div className="pointer-events-none absolute rounded-full" style={{ width: '28px', height: '28px', left: '8px', top: '6px', background: 'radial-gradient(16px_16px_at_10px_10px,rgba(255,255,255,0.5),rgba(255,255,255,0)_60%)', opacity: 0.45 }} />
{/* Center darken for depth on unselected only */}
{!isActive && (
<div className="pointer-events-none absolute inset-0 rounded-2xl" style={{ background: 'radial-gradient(120%_120%_at_50%_55%,rgba(0,0,0,0.04),rgba(0,0,0,0)_60%)', opacity: 0.9 }} />
)}
{/* Tint overlay: Indigo family to match Toolkit */}
<div className={`pointer-events-none absolute inset-0 rounded-2xl ${isActive ? 'bg-indigo-600/70 mix-blend-normal opacity-100' : 'bg-indigo-500/30 mix-blend-overlay opacity-35'}`} />
<span className={`relative z-10 text-sm font-medium ${isActive ? 'text-white' : 'text-black'}`}>{t.name}</span>
</button>
);
})}
</div>
</div>
{/* Transition Spinner */}
{isToolTransitioning && (
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50">
<div className="bg-white rounded-lg shadow-lg p-4 flex items-center space-x-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-indigo-600"></div>
<span className="text-gray-700 font-medium">Loading...</span>
</div>
</div>
)}
{!isToolTransitioning && (
<>
{/* Content Panel */}
<div className="bg-white rounded-lg p-6 mb-8 border border-gray-200 shadow-sm">
{selectedTool === 'quality-lens' && (
<div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="bg-indigo-600 rounded-lg p-2">
<DocumentTextIcon className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-indigo-900 font-semibold text-xl">Quality Lens</h3>
<p className="text-gray-600 text-sm">BLASER/COMET Quality Estimation and Hallucination detection</p>
</div>
</div>
<a
href={TOOL_URLS['quality-lens']}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-700 hover:text-indigo-900 text-sm inline-flex items-center"
>
Open in new tab <ArrowTopRightOnSquareIcon className="h-4 w-4 ml-1" />
</a>
</div>
<div className="border rounded-lg overflow-hidden">
{iframeLoading && (
<div className="p-4 flex items-center space-x-3">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-indigo-600"></div>
<span className="text-gray-600 text-sm">Loading tool…</span>
</div>
)}
<iframe
ref={iframeRef}
src={TOOL_URLS['quality-lens']}
title="Quality Lens"
className="w-full"
style={{ minHeight: '1100px', border: '0' }}
onLoad={() => setIframeLoading(false)}
/>
</div>
</div>
)}
{selectedTool === 'mymemory' && (
<div>
<div className="flex items-center space-x-3 mb-4">
<div className="bg-purple-600 rounded-lg p-2">
<DocumentTextIcon className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-purple-900 font-semibold text-xl">MyMemory</h3>
<p className="text-gray-600 text-sm">Lookup translation memory suggestions</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Source Text</label>
<textarea
value={mmSource}
onChange={(e) => setMmSource(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg bg-white focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
rows={6}
placeholder="Enter text to translate..."
/>
</div>
<div>
<div className="mb-3">
<label className="block text-sm font-medium text-gray-700 mb-1">Languages</label>
<div className="relative grid grid-cols-1 sm:grid-cols-2 gap-6">
<select
value={mmFrom}
onChange={(e) => setMmFrom(e.target.value)}
className="w-full pl-3 pr-10 py-2 border border-gray-300 rounded-md bg-white"
>
{languages.map(l => <option key={l.code} value={l.code}>{l.label}</option>)}
</select>
<select
value={mmTo}
onChange={(e) => setMmTo(e.target.value)}
className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md bg-white"
>
{languages.map(l => <option key={l.code} value={l.code}>{l.label}</option>)}
</select>
<button
type="button"
onClick={()=>{ const a = mmFrom; setMmFrom(mmTo); setMmTo(a); }}
className="hidden sm:inline-flex items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 rounded-full border border-gray-300 bg-white shadow hover:bg-gray-50"
aria-label="Swap languages"
>
<ArrowsRightLeftIcon className="w-4 h-4 text-gray-600" />
</button>
</div>
</div>
<button
onClick={fetchMyMemory}
disabled={mmLoading}
className="w-full bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg"
>
{mmLoading ? 'Getting suggestion…' : 'Get suggestion'}
</button>
</div>
</div>
{mmError && (
<div className="mb-3 p-3 bg-red-50 text-red-700 rounded border border-red-200 text-sm">{mmError}</div>
)}
<div className="space-y-3">
{mmResults.length > 0 ? (
mmResults.map((r, idx) => (
<div key={idx} className={`p-4 rounded-lg border ${idx === 0 ? 'border-purple-300 bg-purple-50' : 'border-gray-200 bg-white'} flex items-start justify-between`}>
<div>
<div className="text-gray-900 mb-2">{r.translation}</div>
<div className="text-xs text-gray-600 space-x-2">
<span>Score: {(r.score * 100).toFixed(0)}%</span>
{r.source && <span>Source: {r.source}</span>}
</div>
</div>
<button
onClick={() => copyToClipboard(r.translation)}
className="text-gray-600 hover:text-gray-900 flex items-center text-xs"
>
<ClipboardIcon className="h-4 w-4 mr-1" /> Copy
</button>
</div>
))
) : (
<div className="text-sm text-gray-500">No results yet. Enter text and click Get suggestion.</div>
)}
</div>
</div>
)}
{selectedTool === 'mt' && !isVisitor && (
<div>
<div className="flex items-center space-x-3 mb-4">
<div className="bg-indigo-600 rounded-lg p-2">
<DocumentTextIcon className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-indigo-900 font-semibold text-xl">Machine Translation</h3>
<p className="text-gray-600 text-sm">Translate between English and Chinese with MT.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Source Text</label>
<textarea
value={mtSource}
onChange={(e) => setMtSource(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
rows={6}
placeholder="Enter text to translate..."
/>
</div>
<div>
<div className="mb-3">
<label className="block text-sm font-medium text-gray-700 mb-1">Languages</label>
<div className="relative grid grid-cols-1 sm:grid-cols-2 gap-6">
<select value={mtFrom} onChange={(e)=>setMtFrom(e.target.value)} className="w-full pl-3 pr-10 py-2 border border-gray-300 rounded-md bg-white">
{languages.map(l => <option key={l.code} value={l.code}>{l.label}</option>)}
</select>
<select value={mtTo} onChange={(e)=>setMtTo(e.target.value)} className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md bg-white">
{languages.map(l => <option key={l.code} value={l.code}>{l.label}</option>)}
</select>
<button type="button" onClick={()=>{ setMtFrom(mtTo); setMtTo(mtFrom); }} className="hidden sm:inline-flex items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 rounded-full border border-gray-300 bg-white shadow hover:bg-gray-50" aria-label="Swap languages">
<ArrowsRightLeftIcon className="w-4 h-4 text-gray-600" />
</button>
</div>
</div>
<div className="mb-3">
<label className="block text-sm font-medium text-gray-700 mb-1">Provider</label>
<select value={mtProvider} onChange={(e)=>setMtProvider(e.target.value as any)} className="w-full px-3 py-2 border border-gray-300 rounded-md bg-white">
<option value="deepl">DeepL</option>
<option value="google">Google</option>
</select>
</div>
<button
onClick={translateMT}
disabled={mtLoading}
className="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg"
>
{mtLoading ? 'Translating…' : 'Translate'}
</button>
</div>
</div>
{mtError && (
<div className="mb-3 p-3 bg-red-50 text-red-700 rounded border border-red-200 text-sm">{mtError}</div>
)}
<div className="space-y-3">
{mtResult ? (
<div className="p-4 rounded-lg border border-gray-200 bg-white flex items-start justify-between">
<div className="text-gray-900 mb-2 whitespace-pre-wrap break-words">{mtResult}</div>
<button onClick={()=>copyToClipboard(mtResult)} className="text-gray-600 hover:text-gray-900 flex items-center text-xs"><ClipboardIcon className="h-4 w-4 mr-1"/> Copy</button>
</div>
) : (
<div className="text-sm text-gray-500">No translation yet. Enter text and click Translate.</div>
)}
</div>
</div>
)}
{/* Syntax Reorderer removed */}
{selectedTool === 'refinity' && (
<div>
<div className="flex items-center space-x-3 mb-4">
<div className="bg-indigo-600 rounded-lg p-2">
<WrenchScrewdriverIcon className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-indigo-900 font-semibold text-xl">Deep Revision</h3>
<p className="text-gray-600 text-sm">Manage versions, compare, and export with track changes.</p>
</div>
</div>
<div className="border rounded-lg overflow-visible">
<div className="mx-auto w-full max-w-5xl">
<Refinity />
</div>
</div>
</div>
)}
{selectedTool === 'dictionary' && (
<div>
<div className="flex items-center space-x-3 mb-4">
<div className="bg-teal-600 rounded-lg p-2">
<DocumentTextIcon className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-teal-900 font-semibold text-xl">Dictionary (EN⇄ZH)</h3>
<p className="text-gray-600 text-sm">Lookup words and get quick suggestions.</p>
</div>
</div>
<div className="mb-4">
<div className="max-w-xl">
<input
type="text"
value={dictWord}
onChange={(e) => setDictWord(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg bg-white focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
placeholder="Enter an English or Chinese word"
/>
</div>
</div>
{/* Remove Wiktionary block; rely on Iciba for now */}
{dictError && (
<div className="mb-3 p-3 bg-red-50 text-red-700 rounded border border-red-200 text-sm">{dictError}</div>
)}
{/* Iciba suggestions (optional helper) */}
<div className="mt-6">
<div className="text-sm text-gray-600 mb-2">Suggestions (Iciba)</div>
<IcibaSuggest term={dictWord} />
</div>
{/* Reverse Dictionary: WantWords (additive, non-invasive) */}
<div className="mt-8">
<div className="flex items-center justify-between mb-3">
<div>
<div className="text-sm text-gray-600">Reverse Dictionary</div>
<div className="text-lg font-medium text-gray-900">WantWords</div>
</div>
<a
href="https://wantwords.net/"
target="_blank"
rel="noopener noreferrer"
className="text-teal-700 hover:text-teal-900 text-sm inline-flex items-center"
>
Open in new tab <ArrowTopRightOnSquareIcon className="h-4 w-4 ml-1" />
</a>
</div>
<div className="text-sm text-gray-600 mb-3">
Find words by description. This complements direct dictionary lookups.
</div>
<button
type="button"
onClick={() => setShowWantWords(v => !v)}
className="px-3 py-2 rounded-md border border-gray-300 text-sm text-gray-800 bg-white hover:bg-gray-50"
>
{showWantWords ? 'Hide embedded WantWords' : 'Show embedded WantWords'}
</button>
{showWantWords && (
<div className="mt-4 border rounded-lg overflow-hidden">
<iframe
src={`https://wantwords.net/`}
title="WantWords Reverse Dictionary"
className="w-full"
style={{ minHeight: '900px', border: '0' }}
/>
</div>
)}
</div>
</div>
)}
{selectedTool === 'links' && (
<div>
<div className="flex items-center space-x-3 mb-4">
<div className="bg-slate-600 rounded-lg p-2">
<DocumentTextIcon className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-slate-900 font-semibold text-xl">Useful Links</h3>
<p className="text-gray-600 text-sm">Short descriptions with quick access to external resources.</p>
</div>
</div>
{isAdmin && (
<div className="mb-4 p-4 border border-gray-200 rounded-lg bg-gray-50">
<div className="font-medium text-gray-800 mb-2">Manage Links</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-end">
<input value={editItem?.title || ''} onChange={(e)=>setEditItem({ ...(editItem||{}), title: e.target.value })} placeholder="Title" className="px-3 py-2 border border-gray-300 rounded-md bg-white" />
<input value={editItem?.url || ''} onChange={(e)=>setEditItem({ ...(editItem||{}), url: e.target.value })} placeholder="https://..." className="px-3 py-2 border border-gray-300 rounded-md bg-white" />
<input value={editItem?.desc || ''} onChange={(e)=>setEditItem({ ...(editItem||{}), desc: e.target.value })} placeholder="Short description" className="px-3 py-2 border border-gray-300 rounded-md bg-white" />
<div className="flex gap-2">
<button onClick={async()=>{ try{ const base=((api.defaults as any)?.baseURL as string||'').replace(/\/$/,''); const bodyObj:any={ title: editItem?.title||'', url: editItem?.url||'', desc: editItem?.desc||'' }; const method = editItem?._id ? 'PUT' : 'POST'; const url = editItem?._id ? `${base}/api/links/${encodeURIComponent(editItem._id)}` : `${base}/api/links`; const headers:any={ 'Content-Type':'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')||''}`, 'user-role': 'admin', 'user-info': localStorage.getItem('user')||'' }; const resp = await fetch(url,{ method, headers, body: JSON.stringify(bodyObj) }); const data = await resp.json().catch(()=>({})); if(!resp.ok) throw new Error(data?.error||'Save failed'); setEditItem(null); const reload = await fetch(`${base}/api/links`); const l = await reload.json(); setLinks(Array.isArray(l)?l:[]);}catch{}}} className="px-3 py-2 bg-indigo-600 text-white rounded-md">{editItem?._id?'Update':'Add'}</button>
{editItem?._id && <button onClick={()=>setEditItem(null)} className="px-3 py-2 border border-gray-300 rounded-md bg-white">Cancel</button>}
</div>
</div>
</div>
)}
{linksLoading && <div className="text-sm text-gray-600">Loading…</div>}
{linksError && <div className="text-sm text-red-600">{linksError}</div>}
<div className="space-y-3">
{links.map((l) => (
<div key={l._id || l.url} className="p-4 rounded-lg border border-gray-200 bg-white flex items-start justify-between">
<div>
<a href={l.url} target="_blank" rel="noopener noreferrer" className="text-indigo-700 hover:text-indigo-900 font-medium inline-flex items-center">
{l.title}
<ArrowTopRightOnSquareIcon className="h-4 w-4 ml-1" />
</a>
{l.desc && <div className="text-sm text-gray-600 mt-1">{l.desc}</div>}
</div>
<div className="flex gap-3">
<a href={l.url} target="_blank" rel="noopener noreferrer" className="text-indigo-700 hover:text-indigo-900 text-sm inline-flex items-center">Open</a>
{isAdmin && (
<>
<button onClick={()=>setEditItem(l)} className="text-sm text-gray-700 hover:text-gray-900">Edit</button>
<button onClick={async()=>{ try{ const base=((api.defaults as any)?.baseURL as string||'').replace(/\/$/,''); const headers:any={ 'Authorization': `Bearer ${localStorage.getItem('token')||''}`, 'user-role': 'admin', 'user-info': localStorage.getItem('user')||'' }; const resp=await fetch(`${base}/api/links/${encodeURIComponent(String(l._id))}`,{ method:'DELETE', headers }); if(!resp.ok) throw new Error('Delete failed'); setLinks((prev)=>prev.filter(it=>it._id!==l._id)); }catch{}}} className="text-sm text-red-600 hover:text-red-800">Delete</button>
</>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
</>
)}
</div>
</div>
);
};
export default Toolkit;