confluent/ConfluentTranslator/public/index.html
StillHammer 894645e640 Implémentation du système de prompt contextuel intelligent
Nouveau système qui analyse le texte français et génère des prompts optimisés en incluant uniquement le vocabulaire pertinent du lexique, réduisant drastiquement le nombre de tokens.

# Backend

- contextAnalyzer.js : Analyse contextuelle avec lemmatisation française
  - Tokenization avec normalisation des accents
  - Recherche intelligente (correspondances exactes, synonymes, formes conjuguées)
  - Calcul dynamique du nombre max d'entrées selon longueur (30/50/100)
  - Expansion sémantique niveau 1 (modulaire pour futur)
  - Fallback racines (309 racines si mots inconnus)

- promptBuilder.js : Génération de prompts optimisés
  - Templates de base sans lexique massif
  - Injection ciblée du vocabulaire pertinent
  - Formatage par type (racines sacrées, standards, verbes)
  - Support fallback avec toutes les racines

- server.js : Intégration API avec structure 3 layers
  - Layer 1: Traduction pure
  - Layer 2: Métadonnées contextuelles (mots trouvés, optimisation)
  - Layer 3: Explications du LLM (décomposition, notes)

- lexiqueLoader.js : Fusion du lexique simple data/lexique-francais-confluent.json
  - Charge 636 entrées (516 ancien + 120 merged)

# Frontend

- index.html : Interface 3 layers collapsibles
  - Layer 1 (toujours visible) : Traduction avec mise en valeur
  - Layer 2 (collapsible) : Contexte lexical + statistiques d'optimisation
  - Layer 3 (collapsible) : Explications linguistiques du LLM
  - Design dark complet (fix fond blanc + listes déroulantes)
  - Animations smooth pour expand/collapse

# Documentation

- docs/PROMPT_CONTEXTUEL_INTELLIGENT.md : Plan complet validé
  - Architecture technique détaillée
  - Cas d'usage et décisions de design
  - Métriques de succès

# Tests

- Tests exhaustifs avec validation exigeante
- Économie moyenne : 81% de tokens
- Économie minimale : 52% (même avec fallback)
- Context skimming opérationnel et validé

# Corrections

- ancien-confluent/lexique/02-racines-standards.json : Fix erreur JSON ligne 527

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 11:08:45 +08:00

688 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>ConfluentTranslator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html {
background: #1a1a1a;
min-height: 100%;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a1a;
color: #e0e0e0;
padding: 20px;
line-height: 1.6;
min-height: 100vh;
}
.container { max-width: 900px; margin: 0 auto; }
h1 {
text-align: center;
margin-bottom: 30px;
color: #4a9eff;
font-size: 2em;
}
.panel {
background: #2a2a2a;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #3a3a3a;
}
.panel h2 {
font-size: 1.2em;
margin-bottom: 15px;
color: #4a9eff;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #b0b0b0;
font-size: 0.9em;
}
select, input, textarea {
width: 100%;
padding: 10px;
background: #1a1a1a;
border: 1px solid #3a3a3a;
border-radius: 4px;
color: #e0e0e0;
font-family: inherit;
}
select option {
background: #1a1a1a;
color: #e0e0e0;
padding: 10px;
}
select:focus {
outline: none;
border-color: #4a9eff;
}
textarea {
resize: vertical;
min-height: 120px;
font-size: 0.95em;
}
button {
background: #4a9eff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: background 0.2s;
}
button:hover { background: #357abd; }
button:disabled { background: #555; cursor: not-allowed; }
.row { display: flex; gap: 15px; }
.row .form-group { flex: 1; }
/* Tabs */
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #3a3a3a;
}
.tab {
background: transparent;
padding: 10px 20px;
border: none;
color: #b0b0b0;
cursor: pointer;
font-weight: 600;
border-bottom: 3px solid transparent;
transition: all 0.2s;
}
.tab:hover { color: #e0e0e0; }
.tab.active {
color: #4a9eff;
border-bottom-color: #4a9eff;
}
.tab-content { display: none; }
.tab-content.active { display: block; }
/* 3 LAYERS SYSTEM */
.layer {
background: #2a2a2a;
border-radius: 8px;
margin-bottom: 15px;
border: 1px solid #3a3a3a;
overflow: hidden;
}
/* Layer 1 - Always visible */
.layer1 {
padding: 20px;
background: #1a1a1a;
border: 2px solid #4a9eff;
}
.layer1-title {
font-size: 0.9em;
color: #4a9eff;
margin-bottom: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.layer1-content {
font-size: 1.3em;
color: #4a9eff;
font-family: 'Courier New', monospace;
line-height: 1.8;
padding: 10px 0;
}
/* Layer 2 & 3 - Collapsible */
.layer-header {
padding: 15px 20px;
background: #2a2a2a;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
user-select: none;
}
.layer-header:hover {
background: #333;
}
.layer-title {
font-size: 1em;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.layer-icon {
font-size: 1.2em;
}
.layer-arrow {
transition: transform 0.3s;
font-size: 1.2em;
color: #4a9eff;
}
.layer-arrow.expanded {
transform: rotate(180deg);
}
.layer-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
background: #1a1a1a;
}
.layer-content.expanded {
max-height: 1000px;
transition: max-height 0.5s ease-in;
}
.layer-content-inner {
padding: 20px;
}
/* Layer 2 specific styles */
.context-item {
margin-bottom: 15px;
}
.context-label {
color: #b0b0b0;
font-size: 0.85em;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.word-match {
display: inline-block;
background: #2a2a2a;
padding: 5px 10px;
margin: 3px;
border-radius: 4px;
font-size: 0.9em;
border-left: 3px solid #4a9eff;
}
.word-match-input {
color: #e0e0e0;
}
.word-match-arrow {
color: #4a9eff;
margin: 0 5px;
}
.word-match-output {
color: #4a9eff;
font-family: monospace;
}
.word-match-score {
color: #888;
font-size: 0.85em;
margin-left: 5px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-top: 10px;
}
.stat-box {
background: #2a2a2a;
padding: 10px;
border-radius: 4px;
border-left: 3px solid #4a9eff;
}
.stat-value {
font-size: 1.5em;
color: #4a9eff;
font-weight: 700;
}
.stat-label {
color: #b0b0b0;
font-size: 0.85em;
}
/* Layer 3 specific styles */
.decomposition-line {
font-family: 'Courier New', monospace;
margin: 5px 0;
padding-left: 10px;
border-left: 2px solid #3a3a3a;
color: #e0e0e0;
}
.notes-text {
color: #b0b0b0;
line-height: 1.8;
}
/* Empty state */
.empty-state {
color: #b0b0b0;
text-align: center;
padding: 30px;
font-style: italic;
}
/* Lexique styles */
.lexique-results {
max-height: 400px;
overflow-y: auto;
margin-top: 15px;
}
.lexique-item {
background: #1a1a1a;
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
border-left: 3px solid #4a9eff;
display: flex;
justify-content: space-between;
align-items: center;
}
.lexique-fr {
color: #e0e0e0;
font-weight: 500;
}
.lexique-cf {
color: #4a9eff;
font-family: monospace;
font-size: 1.1em;
}
.lexique-count {
color: #b0b0b0;
font-size: 0.9em;
margin-top: 10px;
}
.no-results {
color: #b0b0b0;
text-align: center;
padding: 20px;
font-style: italic;
}
.error { color: #ff4a4a; }
</style>
</head>
<body>
<div class="container">
<h1>ConfluentTranslator</h1>
<!-- Tabs -->
<div class="tabs">
<button class="tab active" data-tab="traduction">Traduction</button>
<button class="tab" data-tab="lexique">Lexique</button>
</div>
<!-- Tab: Traduction -->
<div id="tab-traduction" class="tab-content active">
<div class="panel">
<h2>Configuration</h2>
<div class="row">
<div class="form-group">
<label>Provider</label>
<select id="provider">
<option value="anthropic">Anthropic</option>
<option value="openai">OpenAI</option>
</select>
</div>
<div class="form-group">
<label>Modèle</label>
<select id="model">
<option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
<option value="claude-haiku-4-5-20251001">Claude Haiku 4.5</option>
<option value="gpt-4o-mini">GPT-4o Mini</option>
<option value="o1-mini">GPT o1-mini</option>
</select>
</div>
</div>
</div>
<div class="panel">
<h2>Traduction</h2>
<div class="form-group">
<label>Langue cible</label>
<select id="target">
<option value="proto">Proto-Confluent</option>
<option value="ancien">Ancien Confluent</option>
</select>
</div>
<div class="form-group">
<label>Texte français</label>
<textarea id="input" placeholder="Entrez votre texte en français..."></textarea>
</div>
<button id="translate">Traduire</button>
</div>
<!-- LAYER 1: TRADUCTION (Always visible) -->
<div id="result-container" style="display: none;">
<div class="layer layer1">
<div class="layer1-title">Traduction</div>
<div id="layer1-content" class="layer1-content"></div>
</div>
<!-- LAYER 2: CONTEXTE (Collapsible) -->
<div class="layer">
<div class="layer-header" onclick="toggleLayer('layer2')">
<div class="layer-title">
<span class="layer-icon">📚</span>
<span>Contexte Lexical</span>
</div>
<span class="layer-arrow" id="layer2-arrow"></span>
</div>
<div id="layer2-content" class="layer-content">
<div class="layer-content-inner">
<div class="context-item">
<div class="context-label">Mots trouvés dans le lexique</div>
<div id="layer2-words"></div>
</div>
<div class="context-item">
<div class="context-label">Optimisation</div>
<div class="stats-grid" id="layer2-stats"></div>
</div>
</div>
</div>
</div>
<!-- LAYER 3: EXPLICATIONS LLM (Collapsible) -->
<div class="layer">
<div class="layer-header" onclick="toggleLayer('layer3')">
<div class="layer-title">
<span class="layer-icon">💡</span>
<span>Explications</span>
</div>
<span class="layer-arrow" id="layer3-arrow"></span>
</div>
<div id="layer3-content" class="layer-content">
<div class="layer-content-inner">
<div class="context-item">
<div class="context-label">Décomposition</div>
<div id="layer3-decomposition"></div>
</div>
<div class="context-item" id="layer3-notes-container" style="display: none;">
<div class="context-label">Notes linguistiques</div>
<div id="layer3-notes" class="notes-text"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Lexique -->
<div id="tab-lexique" class="tab-content">
<div class="panel">
<h2>Recherche dans le lexique</h2>
<div class="form-group">
<label>Niveau de langue</label>
<select id="lexique-niveau">
<option value="proto">Proto-Confluent</option>
<option value="ancien">Ancien Confluent</option>
</select>
</div>
<div class="form-group">
<label>Rechercher un mot français</label>
<input type="text" id="lexique-search" placeholder="Tapez un mot en français...">
</div>
<div class="lexique-count" id="lexique-count">0 résultat(s)</div>
<div class="lexique-results" id="lexique-results">
<div class="no-results">Commencez à taper pour rechercher...</div>
</div>
</div>
</div>
</div>
<script>
// Lexique data
let lexiqueData = null;
// Load lexique
const loadLexique = async () => {
try {
const response = await fetch('/lexique');
lexiqueData = await response.json();
} catch (error) {
console.error('Error loading lexique:', error);
}
};
// Toggle layer (expand/collapse)
function toggleLayer(layerId) {
const content = document.getElementById(`${layerId}-content`);
const arrow = document.getElementById(`${layerId}-arrow`);
content.classList.toggle('expanded');
arrow.classList.toggle('expanded');
}
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const tabName = tab.dataset.tab;
document.getElementById(`tab-${tabName}`).classList.add('active');
});
});
// Lexique search
const searchLexique = () => {
const query = document.getElementById('lexique-search').value.toLowerCase().trim();
const niveau = document.getElementById('lexique-niveau').value;
const resultsDiv = document.getElementById('lexique-results');
const countDiv = document.getElementById('lexique-count');
if (!lexiqueData || !lexiqueData.dictionnaire) {
resultsDiv.innerHTML = '<div class="no-results">Lexique en cours de chargement...</div>';
return;
}
if (!query) {
resultsDiv.innerHTML = '<div class="no-results">Commencez à taper pour rechercher...</div>';
countDiv.textContent = '0 résultat(s)';
return;
}
const dict = lexiqueData.dictionnaire;
const results = [];
for (const [key, data] of Object.entries(dict)) {
if (key.includes(query) || (data.mot_francais && data.mot_francais.toLowerCase().includes(query))) {
if (data.is_synonym_of) continue;
const cf = data.traductions && data.traductions.length > 0
? data.traductions[0].confluent
: '?';
results.push({
fr: data.mot_francais || key,
cf: cf,
type: data.traductions?.[0]?.type || '',
domaine: data.traductions?.[0]?.domaine || ''
});
}
}
results.sort((a, b) => {
const aExact = a.fr.toLowerCase() === query;
const bExact = b.fr.toLowerCase() === query;
if (aExact && !bExact) return -1;
if (!aExact && bExact) return 1;
const aStarts = a.fr.toLowerCase().startsWith(query);
const bStarts = b.fr.toLowerCase().startsWith(query);
if (aStarts && !bStarts) return -1;
if (!aStarts && bStarts) return 1;
return a.fr.localeCompare(b.fr);
});
if (results.length === 0) {
resultsDiv.innerHTML = '<div class="no-results">Aucun résultat trouvé</div>';
countDiv.textContent = '0 résultat(s)';
} else {
const html = results.map(r => `
<div class="lexique-item">
<span class="lexique-fr">${r.fr}</span>
<span class="lexique-cf">${r.cf}</span>
</div>
`).join('');
resultsDiv.innerHTML = html;
countDiv.textContent = `${results.length} résultat(s)`;
}
};
document.getElementById('lexique-search').addEventListener('input', searchLexique);
document.getElementById('lexique-niveau').addEventListener('change', searchLexique);
// Load/Save config
const loadConfig = () => {
const config = JSON.parse(localStorage.getItem('confluentConfig') || '{}');
if (config.provider) document.getElementById('provider').value = config.provider;
if (config.model) document.getElementById('model').value = config.model;
if (config.target) document.getElementById('target').value = config.target;
};
const saveConfig = () => {
const config = {
provider: document.getElementById('provider').value,
model: document.getElementById('model').value,
target: document.getElementById('target').value,
};
localStorage.setItem('confluentConfig', JSON.stringify(config));
};
// Update model options
document.getElementById('provider').addEventListener('change', (e) => {
const modelSelect = document.getElementById('model');
modelSelect.innerHTML = '';
if (e.target.value === 'anthropic') {
modelSelect.innerHTML = `
<option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
<option value="claude-haiku-4-5-20251001">Claude Haiku 4.5</option>
`;
} else if (e.target.value === 'openai') {
modelSelect.innerHTML = `
<option value="gpt-4o-mini">GPT-4o Mini</option>
<option value="o1-mini">GPT o1-mini</option>
`;
}
saveConfig();
});
document.getElementById('model').addEventListener('change', saveConfig);
document.getElementById('target').addEventListener('change', saveConfig);
// Translation with 3 layers
document.getElementById('translate').addEventListener('click', async () => {
const button = document.getElementById('translate');
const text = document.getElementById('input').value.trim();
const resultContainer = document.getElementById('result-container');
if (!text) {
alert('Veuillez entrer un texte.');
return;
}
button.disabled = true;
resultContainer.style.display = 'none';
// Show loading in layer 1
resultContainer.style.display = 'block';
document.getElementById('layer1-content').textContent = 'Traduction en cours...';
const config = {
text,
target: document.getElementById('target').value,
provider: document.getElementById('provider').value,
model: document.getElementById('model').value,
};
try {
const response = await fetch('/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
const data = await response.json();
if (response.ok) {
// LAYER 1: Translation
document.getElementById('layer1-content').textContent = data.layer1.translation;
// LAYER 2: Context
if (data.layer2) {
const wordsHtml = data.layer2.wordsFound && data.layer2.wordsFound.length > 0
? data.layer2.wordsFound.map(w => `
<div class="word-match">
<span class="word-match-input">${w.input}</span>
<span class="word-match-arrow">→</span>
<span class="word-match-output">${w.confluent}</span>
<span class="word-match-score">(${w.score.toFixed(2)})</span>
</div>
`).join('')
: '<div class="no-results">Aucun mot trouvé (fallback racines activé)</div>';
document.getElementById('layer2-words').innerHTML = wordsHtml;
const statsHtml = `
<div class="stat-box">
<div class="stat-value">${data.layer2.entriesUsed || 0}</div>
<div class="stat-label">Entrées utilisées</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.layer2.tokensSaved || 0}</div>
<div class="stat-label">Tokens économisés</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.layer2.savingsPercent || 0}%</div>
<div class="stat-label">Économie</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.layer2.useFallback ? 'OUI' : 'NON'}</div>
<div class="stat-label">Fallback racines</div>
</div>
`;
document.getElementById('layer2-stats').innerHTML = statsHtml;
}
// LAYER 3: Explanations
if (data.layer3) {
const decompHtml = data.layer3.decomposition
? data.layer3.decomposition.split('\n').map(line =>
`<div class="decomposition-line">${line}</div>`
).join('')
: '<div class="no-results">Pas de décomposition disponible</div>';
document.getElementById('layer3-decomposition').innerHTML = decompHtml;
if (data.layer3.notes) {
document.getElementById('layer3-notes').textContent = data.layer3.notes;
document.getElementById('layer3-notes-container').style.display = 'block';
} else {
document.getElementById('layer3-notes-container').style.display = 'none';
}
}
} else {
document.getElementById('layer1-content').innerHTML = `<span class="error">Erreur: ${data.error}</span>`;
}
} catch (error) {
document.getElementById('layer1-content').innerHTML = `<span class="error">Erreur: ${error.message}</span>`;
} finally {
button.disabled = false;
}
});
// Initialize
loadConfig();
loadLexique();
</script>
</body>
</html>