600 lines
20 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" placeholder="llama3, 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>
<!-- 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="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="toggleImageTranslation()">
<option value="google">Google Translate (Default)</option>
<option value="ollama">Ollama LLM</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 Ollama Vision (requires llava model)</span>
</label>
</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';
// Toggle image translation option based on provider
function toggleImageTranslation() {
const provider = document.getElementById('provider').value;
const imageOption = document.getElementById('image-translation-option');
if (provider === 'ollama') {
imageOption.style.display = 'block';
} else {
imageOption.style.display = 'none';
document.getElementById('translate-images').checked = false;
}
}
// 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;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('target_language', targetLang);
formData.append('provider', provider);
formData.append('translate_images', translateImages);
loadingDiv.classList.add('active');
progressContainer.classList.add('active');
resultDiv.innerHTML = '';
// Simulate progress (since we don't have real progress from backend)
let progress = 0;
const progressInterval = setInterval(() => {
progress += Math.random() * 15;
if (progress > 90) progress = 90;
progressBar.style.width = progress + '%';
progressText.textContent = `Processing: ${Math.round(progress)}%`;
}, 500);
try {
const response = await fetch(`${API_BASE}/translate`, {
method: 'POST',
body: formData
});
clearInterval(progressInterval);
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);
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>