907 lines
33 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Translation API - Interface de Test</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 40px 20px;
}
.header {
background: white;
padding: 30px 40px;
margin-bottom: 30px;
border-bottom: 1px solid #e1e8ed;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.header h1 {
font-size: 28px;
font-weight: 600;
color: #1a202c;
margin-bottom: 8px;
}
.header p {
font-size: 15px;
color: #718096;
}
.card {
background: white;
border-radius: 8px;
padding: 32px;
margin-bottom: 24px;
border: 1px solid #e1e8ed;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.card h2 {
color: #1a202c;
margin-bottom: 24px;
font-size: 20px;
font-weight: 600;
border-bottom: 1px solid #e1e8ed;
padding-bottom: 12px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #4a5568;
font-weight: 500;
font-size: 14px;
}
input[type="text"],
input[type="file"],
select {
width: 100%;
padding: 10px 14px;
border: 1px solid #cbd5e0;
border-radius: 6px;
font-size: 14px;
transition: all 0.2s;
background: #ffffff;
}
input[type="text"]:focus,
select:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
}
input[type="file"] {
padding: 8px;
cursor: pointer;
}
button {
background: #2563eb;
color: white;
padding: 10px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
margin-right: 10px;
margin-bottom: 10px;
}
button:hover {
background: #1e40af;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
background: #64748b;
}
.btn-secondary:hover {
background: #475569;
}
.btn-success {
background: #059669;
}
.btn-success:hover {
background: #047857;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.result {
margin-top: 20px;
padding: 16px;
border-radius: 6px;
border-left: 4px solid #cbd5e0;
background: #f7fafc;
}
.result.success {
background: #f0fdf4;
border-left-color: #10b981;
}
.result.error {
background: #fef2f2;
border-left-color: #ef4444;
}
.result h3 {
margin-bottom: 12px;
color: #1a202c;
font-size: 16px;
font-weight: 600;
}
.result pre {
background: white;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-size: 13px;
border: 1px solid #e1e8ed;
color: #2d3748;
}
.loading {
display: none;
text-align: center;
padding: 24px;
}
.loading.active {
display: block;
}
.spinner {
border: 3px solid #e1e8ed;
border-top: 3px solid #2563eb;
border-radius: 50%;
width: 36px;
height: 36px;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.progress-container {
width: 100%;
background: #e1e8ed;
border-radius: 8px;
height: 8px;
overflow: hidden;
margin: 16px 0;
display: none;
}
.progress-container.active {
display: block;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #2563eb 0%, #1e40af 100%);
width: 0%;
transition: width 0.3s ease;
border-radius: 8px;
}
.progress-text {
text-align: center;
margin-top: 8px;
color: #4a5568;
font-size: 14px;
font-weight: 500;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin-right: 8px;
background: #e0e7ff;
color: #3730a3;
}
.models-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.model-item {
background: #f1f5f9;
padding: 6px 12px;
border-radius: 4px;
font-size: 13px;
border: 1px solid #e1e8ed;
color: #475569;
}
.download-link {
display: inline-block;
margin-top: 12px;
padding: 10px 20px;
background: #059669;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
font-size: 14px;
}
.download-link:hover {
background: #047857;
}
@media (max-width: 768px) {
.grid-2 {
grid-template-columns: 1fr;
}
.container {
padding: 20px 15px;
}
.card {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Document Translation API</h1>
<p>Professional document translation service with format preservation</p>
</div>
<!-- Configuration Ollama -->
<div class="card">
<h2>Ollama Configuration</h2>
<div class="grid-2">
<div class="form-group">
<label for="ollama-url">URL Ollama</label>
<input type="text" id="ollama-url" value="http://localhost:11434" placeholder="http://localhost:11434">
</div>
<div class="form-group">
<label for="ollama-model">Modèle Ollama</label>
<input type="text" id="ollama-model" value="llama3.2" placeholder="llama3.2, mistral, etc.">
</div>
</div>
<button onclick="listOllamaModels()" class="btn-secondary">List Available Models</button>
<button onclick="configureOllama()" class="btn-success">Save Configuration</button>
<div id="models-result"></div>
</div>
<!-- System Prompt for LLM Translation -->
<div class="card">
<h2>Translation Context (Ollama / WebLLM)</h2>
<p style="font-size: 13px; color: #718096; margin-bottom: 15px;">
Provide context, technical glossary, or specific instructions to improve translation quality.
</p>
<div class="form-group">
<label for="system-prompt">System Prompt / Instructions</label>
<textarea id="system-prompt" rows="4" style="width: 100%; padding: 10px 14px; border: 1px solid #cbd5e0; border-radius: 6px; font-size: 14px; font-family: inherit; resize: vertical;" placeholder="Example: You are translating HVAC technical documents. Use these terms:
- Batterie (FR) = Coil (EN)
- Groupe froid (FR) = Chiller (EN)
- CTA (FR) = AHU (EN)"></textarea>
</div>
<div class="form-group">
<label for="glossary">Technical Glossary (one per line: source=target)</label>
<textarea id="glossary" rows="5" style="width: 100%; padding: 10px 14px; border: 1px solid #cbd5e0; border-radius: 6px; font-size: 13px; font-family: monospace; resize: vertical;" placeholder="batterie=coil
groupe froid=chiller
CTA=AHU
échangeur=heat exchanger
vanne 3 voies=3-way valve"></textarea>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button onclick="loadPreset('hvac')" class="btn-secondary" style="font-size: 12px;">HVAC Preset</button>
<button onclick="loadPreset('it')" class="btn-secondary" style="font-size: 12px;">IT Preset</button>
<button onclick="loadPreset('legal')" class="btn-secondary" style="font-size: 12px;">Legal Preset</button>
<button onclick="loadPreset('medical')" class="btn-secondary" style="font-size: 12px;">Medical Preset</button>
<button onclick="clearPrompt()" class="btn-secondary" style="font-size: 12px; background: #dc2626;">Clear</button>
</div>
</div>
<!-- Traduction de fichier -->
<div class="card">
<h2>Document Translation</h2>
<div class="form-group">
<label for="file-input">
Select file to translate
<span class="badge">XLSX</span>
<span class="badge">DOCX</span>
<span class="badge">PPTX</span>
</label>
<input type="file" id="file-input" accept=".xlsx,.docx,.pptx">
</div>
<div class="grid-2">
<div class="form-group">
<label for="target-lang">Target Language</label>
<select id="target-lang">
<option value="en">English (en)</option>
<option value="fa">Persian / Farsi (fa)</option>
<option value="es">Espagnol (es)</option>
<option value="fr">Français (fr)</option>
<option value="de">Allemand (de)</option>
<option value="it">Italien (it)</option>
<option value="pt">Portugais (pt)</option>
<option value="ru">Russe (ru)</option>
<option value="zh">Chinois (zh)</option>
<option value="ja">Japonais (ja)</option>
<option value="ko">Coréen (ko)</option>
<option value="ar">Arabe (ar)</option>
</select>
</div>
<div class="form-group">
<label for="provider">Translation Service</label>
<select id="provider" onchange="toggleProviderOptions()">
<option value="google">Google Translate (Default)</option>
<option value="ollama">Ollama LLM (Local Server)</option>
<option value="webllm">WebLLM (Browser - WebGPU)</option>
<option value="deepl">DeepL</option>
<option value="libre">LibreTranslate</option>
</select>
</div>
</div>
<div class="form-group" id="image-translation-option" style="display: none;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="translate-images" style="width: auto; margin-right: 10px;">
<span>Translate images with vision (use multimodal models: gemma3, qwen3-vl, llava, etc.)</span>
</label>
</div>
<div class="form-group" id="webllm-options" style="display: none; padding: 12px; background: #e0f2ff; border-radius: 6px; border-left: 4px solid #2563eb;">
<p style="margin: 0 0 10px 0; font-size: 13px; color: #1e40af;">
<strong>WebLLM Mode:</strong> Translation runs entirely in your browser using WebGPU. First use downloads the model.
</p>
<div style="display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: end;">
<div>
<label for="webllm-model" style="font-size: 12px; color: #4a5568; margin-bottom: 4px;">Select Model:</label>
<select id="webllm-model" style="width: 100%; padding: 6px; font-size: 13px; border: 1px solid #cbd5e0; border-radius: 4px;">
<option value="Llama-3.2-3B-Instruct-q4f32_1-MLC">Llama 3.2 3B (~2GB) - Recommended</option>
<option value="Llama-3.1-8B-Instruct-q4f32_1-MLC">Llama 3.1 8B (~4.5GB)</option>
<option value="Phi-3.5-mini-instruct-q4f16_1-MLC">Phi 3.5 Mini (~2.5GB)</option>
<option value="Mistral-7B-Instruct-v0.3-q4f16_1-MLC">Mistral 7B (~4.5GB)</option>
<option value="gemma-2-2b-it-q4f16_1-MLC">Gemma 2 2B (~1.5GB)</option>
</select>
</div>
<button onclick="clearWebLLMCache()" style="background: #dc2626; padding: 6px 12px; font-size: 13px; white-space: nowrap;">
Clear Cache
</button>
</div>
<div id="webllm-status" style="margin-top: 10px; font-size: 12px; color: #4a5568;"></div>
</div>
<button onclick="translateFile()">Translate Document</button>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Translation in progress, please wait...</p>
</div>
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="progress-text" id="progress-text"></div>
<div id="translate-result"></div>
</div>
<!-- Test de l'API -->
<div class="card">
<h2>API Health Check</h2>
<button onclick="checkHealth()">Check API Status</button>
<div id="health-result"></div>
</div>
</div>
<script>
const API_BASE = 'http://localhost:8000';
// Clear WebLLM cache
async function clearWebLLMCache() {
if (!confirm('This will delete all downloaded WebLLM models from your browser cache. Continue?')) {
return;
}
try {
// Clear IndexedDB cache used by WebLLM
const databases = await indexedDB.databases();
for (const db of databases) {
if (db.name && (db.name.includes('webllm') || db.name.includes('mlc'))) {
indexedDB.deleteDatabase(db.name);
}
}
// Clear Cache API
if ('caches' in window) {
const cacheNames = await caches.keys();
for (const name of cacheNames) {
if (name.includes('webllm') || name.includes('mlc')) {
await caches.delete(name);
}
}
}
alert('✅ WebLLM cache cleared successfully! Refresh the page.');
} catch (error) {
alert('❌ Error clearing cache: ' + error.message);
}
}
// Toggle provider options based on selection
// Preset templates for different domains
const presets = {
hvac: {
prompt: `You are translating HVAC (Heating, Ventilation, Air Conditioning) technical documents.
Use precise technical terminology. Maintain consistency with industry standards.
Keep unit measurements (kW, m³/h, Pa) unchanged.
Translate component names according to the glossary provided.`,
glossary: `batterie=coil
groupe froid=chiller
CTA=AHU (Air Handling Unit)
échangeur=heat exchanger
vanne 3 voies=3-way valve
détendeur=expansion valve
compresseur=compressor
évaporateur=evaporator
condenseur=condenser
fluide frigorigène=refrigerant
débit d'air=airflow
pression statique=static pressure
récupérateur=heat recovery unit
ventilo-convecteur=fan coil unit
gaine=duct
diffuseur=diffuser
registre=damper`
},
it: {
prompt: `You are translating IT and software documentation.
Keep technical terms, code snippets, and variable names unchanged.
Translate UI labels and user-facing text appropriately.
Maintain formatting markers like **bold** and \`code\`.`,
glossary: `serveur=server
base de données=database
requête=query
sauvegarde=backup
mise à jour=update
télécharger=download
téléverser=upload
mot de passe=password
identifiant=username
pare-feu=firewall
réseau=network
stockage=storage
conteneur=container
déploiement=deployment`
},
legal: {
prompt: `You are translating legal documents.
Use formal legal terminology. Be precise and unambiguous.
Maintain references to laws, articles, and clauses in their original form.
Use standard legal phrases for the target language.`,
glossary: `contrat=contract
clause=clause
partie=party
signataire=signatory
résiliation=termination
préavis=notice period
dommages et intérêts=damages
responsabilité=liability
juridiction=jurisdiction
arbitrage=arbitration
avenant=amendment
ayant droit=beneficiary`
},
medical: {
prompt: `You are translating medical and healthcare documents.
Use standard medical terminology (Latin/Greek roots when appropriate).
Keep drug names, dosages, and medical codes unchanged.
Be precise with anatomical terms and procedures.`,
glossary: `patient=patient
ordonnance=prescription
posologie=dosage
effet secondaire=side effect
contre-indication=contraindication
diagnostic=diagnosis
symptôme=symptom
traitement=treatment
chirurgie=surgery
anesthésie=anesthesia
perfusion=infusion
prélèvement=sample collection`
}
};
function loadPreset(presetName) {
const preset = presets[presetName];
if (preset) {
document.getElementById('system-prompt').value = preset.prompt;
document.getElementById('glossary').value = preset.glossary;
}
}
function clearPrompt() {
document.getElementById('system-prompt').value = '';
document.getElementById('glossary').value = '';
}
function getFullSystemPrompt() {
let prompt = document.getElementById('system-prompt').value || '';
const glossary = document.getElementById('glossary').value || '';
if (glossary.trim()) {
prompt += '\n\nGLOSSARY (use these exact translations):\n' + glossary;
}
return prompt;
}
function toggleProviderOptions() {
const provider = document.getElementById('provider').value;
const imageOption = document.getElementById('image-translation-option');
const webllmOptions = document.getElementById('webllm-options');
// Hide all options first
imageOption.style.display = 'none';
webllmOptions.style.display = 'none';
document.getElementById('translate-images').checked = false;
if (provider === 'ollama') {
imageOption.style.display = 'block';
} else if (provider === 'webllm') {
webllmOptions.style.display = 'block';
}
}
// WebLLM engine instance
let webllmEngine = null;
let webllmReady = false;
// Initialize WebLLM
async function initWebLLM(modelId) {
const statusDiv = document.getElementById('webllm-status');
statusDiv.innerHTML = '⏳ Loading WebLLM...';
try {
// Dynamically import WebLLM
const webllm = await import('https://esm.run/@mlc-ai/web-llm');
statusDiv.innerHTML = '⏳ Downloading model (this may take a while on first use)...';
webllmEngine = await webllm.CreateMLCEngine(modelId, {
initProgressCallback: (progress) => {
statusDiv.innerHTML = `${progress.text}`;
}
});
webllmReady = true;
statusDiv.innerHTML = '✅ Model loaded and ready!';
return true;
} catch (error) {
statusDiv.innerHTML = `❌ Error: ${error.message}`;
console.error('WebLLM init error:', error);
return false;
}
}
// Translate text with WebLLM
async function translateWithWebLLM(text, targetLang) {
if (!webllmEngine) return text;
try {
// Build system prompt with custom context and glossary
let systemPrompt = `You are a translator. Translate the user's text to ${targetLang}. Return ONLY the translation, nothing else.`;
const customPrompt = getFullSystemPrompt();
if (customPrompt.trim()) {
systemPrompt = `You are a translator. Translate the user's text to ${targetLang}. Return ONLY the translation, nothing else.
ADDITIONAL CONTEXT AND INSTRUCTIONS:
${customPrompt}`;
}
const response = await webllmEngine.chat.completions.create({
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: text }
],
temperature: 0.3,
max_tokens: 500
});
return response.choices[0].message.content.trim();
} catch (error) {
console.error('WebLLM translation error:', error);
return text;
}
}
// Liste des modèles Ollama
async function listOllamaModels() {
const url = document.getElementById('ollama-url').value;
const resultDiv = document.getElementById('models-result');
try {
const response = await fetch(`${API_BASE}/ollama/models?base_url=${encodeURIComponent(url)}`);
const data = await response.json();
if (data.models && data.models.length > 0) {
resultDiv.innerHTML = `
<div class="result success">
<h3>${data.count} model(s) available</h3>
<div class="models-list">
${data.models.map(model => `<span class="model-item">${model}</span>`).join('')}
</div>
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="result error">
<h3>No models found</h3>
<p>Make sure Ollama is running and accessible at ${url}</p>
</div>
`;
}
} catch (error) {
resultDiv.innerHTML = `
<div class="result error">
<h3>Connection error</h3>
<pre>${error.message}</pre>
</div>
`;
}
}
// Configurer Ollama
async function configureOllama() {
const url = document.getElementById('ollama-url').value;
const model = document.getElementById('ollama-model').value;
const resultDiv = document.getElementById('models-result');
try {
const formData = new FormData();
formData.append('base_url', url);
formData.append('model', model);
const response = await fetch(`${API_BASE}/ollama/configure`, {
method: 'POST',
body: formData
});
const data = await response.json();
resultDiv.innerHTML = `
<div class="result success">
<h3>Configuration saved</h3>
<p><strong>URL:</strong> ${data.ollama_url}</p>
<p><strong>Model:</strong> ${data.model}</p>
</div>
`;
} catch (error) {
resultDiv.innerHTML = `
<div class="result error">
<h3>Error</h3>
<pre>${error.message}</pre>
</div>
`;
}
}
// Traduire un fichier
async function translateFile() {
const fileInput = document.getElementById('file-input');
const targetLang = document.getElementById('target-lang').value;
const provider = document.getElementById('provider').value;
const translateImages = document.getElementById('translate-images').checked;
const resultDiv = document.getElementById('translate-result');
const loadingDiv = document.getElementById('loading');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
if (!fileInput.files || fileInput.files.length === 0) {
alert('Please select a file');
return;
}
// Get Ollama model from configuration field (used for both text and vision)
const ollamaModel = document.getElementById('ollama-model').value || 'llama3.2';
// Get custom system prompt with glossary
const systemPrompt = getFullSystemPrompt();
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('target_language', targetLang);
formData.append('provider', provider);
formData.append('translate_images', translateImages);
formData.append('ollama_model', ollamaModel);
formData.append('system_prompt', systemPrompt);
loadingDiv.classList.add('active');
progressContainer.classList.add('active');
resultDiv.innerHTML = '';
// Better progress simulation with timeout protection
let progress = 0;
let progressSpeed = 8; // Start at 8% increments
const progressInterval = setInterval(() => {
if (progress < 30) {
progress += progressSpeed;
} else if (progress < 60) {
progressSpeed = 4; // Slower
progress += progressSpeed;
} else if (progress < 85) {
progressSpeed = 2; // Even slower
progress += progressSpeed;
} else if (progress < 95) {
progressSpeed = 0.5; // Very slow near the end
progress += progressSpeed;
}
progressBar.style.width = Math.min(progress, 98) + '%';
progressText.textContent = `Processing: ${Math.round(Math.min(progress, 98))}%`;
}, 800);
// Safety timeout: if takes more than 5 minutes, show error
const safetyTimeout = setTimeout(() => {
clearInterval(progressInterval);
loadingDiv.classList.remove('active');
progressContainer.classList.remove('active');
progressBar.style.width = '0%';
progressText.textContent = '';
resultDiv.innerHTML = `
<div class="result error">
<h3>Request timeout</h3>
<p>Translation is taking longer than expected. This might be due to:</p>
<ul>
<li>Large file size</li>
<li>Ollama model not responding (check if Ollama is running)</li>
<li>Network issues with translation service</li>
</ul>
<p>Please try again or use a different provider.</p>
</div>
`;
}, 300000); // 5 minutes
try {
const response = await fetch(`${API_BASE}/translate`, {
method: 'POST',
body: formData
});
clearInterval(progressInterval);
clearTimeout(safetyTimeout);
progressBar.style.width = '100%';
progressText.textContent = 'Complete: 100%';
setTimeout(() => {
loadingDiv.classList.remove('active');
progressContainer.classList.remove('active');
progressBar.style.width = '0%';
progressText.textContent = '';
}, 500);
if (response.ok) {
const blob = await response.blob();
const filename = response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || 'translated_file';
// Créer un lien de téléchargement
const url = window.URL.createObjectURL(blob);
resultDiv.innerHTML = `
<div class="result success">
<h3>Translation completed successfully</h3>
<p><strong>File:</strong> ${fileInput.files[0].name}</p>
<p><strong>Target language:</strong> ${targetLang}</p>
<p><strong>Service:</strong> ${provider}</p>
${translateImages ? '<p><strong>Images:</strong> Translated with Ollama Vision</p>' : ''}
<a href="${url}" download="${filename}" class="download-link">Download translated file</a>
</div>
`;
} else {
const error = await response.json();
resultDiv.innerHTML = `
<div class="result error">
<h3>Translation error</h3>
<pre>${JSON.stringify(error, null, 2)}</pre>
</div>
`;
}
} catch (error) {
clearInterval(progressInterval);
clearTimeout(safetyTimeout);
loadingDiv.classList.remove('active');
progressContainer.classList.remove('active');
progressBar.style.width = '0%';
progressText.textContent = '';
resultDiv.innerHTML = `
<div class="result error">
<h3>Error</h3>
<pre>${error.message}</pre>
</div>
`;
}
}
// Vérifier la santé de l'API
async function checkHealth() {
const resultDiv = document.getElementById('health-result');
try {
const response = await fetch(`${API_BASE}/health`);
const data = await response.json();
resultDiv.innerHTML = `
<div class="result success">
<h3>API operational</h3>
<pre>${JSON.stringify(data, null, 2)}</pre>
</div>
`;
} catch (error) {
resultDiv.innerHTML = `
<div class="result error">
<h3>API not accessible</h3>
<pre>${error.message}</pre>
</div>
`;
}
}
</script>
</body>
</html>