## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch) ### Architecture procédurale intelligente : - **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%) - **14 types d'erreurs** réalistes et subtiles - **Sélection procédurale** selon contexte (longueur, technique, heure) - **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article ### 1. Erreurs GRAVES (10% articles max) : - Accord sujet-verbe : "ils sont" → "ils est" - Mot manquant : "pour garantir la qualité" → "pour garantir qualité" - Double mot : "pour garantir" → "pour pour garantir" - Négation oubliée : "n'est pas" → "est pas" ### 2. Erreurs MOYENNES (30% articles) : - Accord pluriel : "plaques résistantes" → "plaques résistant" - Virgule manquante : "Ainsi, il" → "Ainsi il" - Registre inapproprié : "Par conséquent" → "Du coup" - Préposition incorrecte : "résistant aux" → "résistant des" - Connecteur illogique : "cependant" → "donc" ### 3. Erreurs LÉGÈRES (50% articles) : - Double espace : "de votre" → "de votre" - Trait d'union : "c'est-à-dire" → "c'est à dire" - Espace ponctuation : "qualité ?" → "qualité?" - Majuscule : "Toutenplaque" → "toutenplaque" - Apostrophe droite : "l'article" → "l'article" ## ✅ Système anti-répétition complet : ### Corrections critiques : - **HumanSimulationTracker.js** : Tracker centralisé global - **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson" - **Protection 30+ expressions idiomatiques** françaises - **Anti-répétition** : max 2× même mot, jamais 2× même développement - **Diversification** : 48 variantes (hésitations, développements, connecteurs) ### Nouvelle structure (comme SmartTouch) : ``` lib/human-simulation/ ├── error-profiles/ (NOUVEAU) │ ├── ErrorProfiles.js (définitions + probabilités) │ ├── ErrorGrave.js (10% articles) │ ├── ErrorMoyenne.js (30% articles) │ ├── ErrorLegere.js (50% articles) │ └── ErrorSelector.js (sélection procédurale) ├── HumanSimulationCore.js (orchestrateur) ├── HumanSimulationTracker.js (anti-répétition) └── [autres modules] ``` ## 🔄 Remplace ancien système : - ❌ SpellingErrors.js (basique, répétitif, "et" → "." × 8) - ✅ error-profiles/ (gradué, procédural, intelligent, diversifié) ## 🎲 Fonctionnalités procédurales : - Analyse contexte : longueur texte, complexité technique, heure rédaction - Multiplicateurs adaptatifs selon contexte - Conditions application intelligentes - Tracking global par batch (respecte limites 10%/30%/50%) ## 📊 Résultats validation : Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
614 lines
19 KiB
JavaScript
614 lines
19 KiB
JavaScript
/**
|
|
* Pipeline Builder - Client Side Logic
|
|
* Gestion de la construction interactive de pipelines modulaires
|
|
*/
|
|
|
|
// État global du builder
|
|
const state = {
|
|
pipeline: {
|
|
name: '',
|
|
description: '',
|
|
pipeline: [],
|
|
metadata: {
|
|
author: 'user',
|
|
created: new Date().toISOString(),
|
|
version: '1.0',
|
|
tags: []
|
|
}
|
|
},
|
|
modules: [],
|
|
templates: [],
|
|
llmProviders: [],
|
|
personalities: [], // ✅ NOUVEAU: Liste des personnalités disponibles
|
|
nextStepNumber: 1
|
|
};
|
|
|
|
// ====================
|
|
// INITIALIZATION
|
|
// ====================
|
|
|
|
window.onload = async function() {
|
|
await loadModules();
|
|
await loadTemplates();
|
|
await loadLLMProviders();
|
|
await loadPersonalities(); // ✅ NOUVEAU: Charger personnalités
|
|
updatePreview();
|
|
};
|
|
|
|
// Load available modules from API
|
|
async function loadModules() {
|
|
try {
|
|
const response = await fetch('/api/pipeline/modules');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
state.modules = data.modules;
|
|
renderModulesPalette();
|
|
}
|
|
} catch (error) {
|
|
showStatus(`Erreur chargement modules: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Load templates from API
|
|
async function loadTemplates() {
|
|
try {
|
|
const response = await fetch('/api/pipeline/templates');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
state.templates = data.templates;
|
|
renderTemplates();
|
|
}
|
|
} catch (error) {
|
|
showStatus(`Erreur chargement templates: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Load LLM providers from API
|
|
async function loadLLMProviders() {
|
|
try {
|
|
const response = await fetch('/api/pipeline/modules');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.llmProviders) {
|
|
state.llmProviders = data.llmProviders;
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur chargement LLM providers:', error);
|
|
// Fallback providers si l'API échoue (synchronisé avec LLMManager)
|
|
state.llmProviders = [
|
|
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', default: true },
|
|
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini' },
|
|
{ id: 'gemini-pro', name: 'Google Gemini Pro' },
|
|
{ id: 'deepseek-chat', name: 'Deepseek Chat' },
|
|
{ id: 'moonshot-v1-32k', name: 'Moonshot v1 32K' },
|
|
{ id: 'mistral-small', name: 'Mistral Small' }
|
|
];
|
|
}
|
|
}
|
|
|
|
// ✅ NOUVEAU: Load personalities from API
|
|
async function loadPersonalities() {
|
|
try {
|
|
const response = await fetch('/api/personalities');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.personalities) {
|
|
state.personalities = data.personalities;
|
|
console.log(`✅ ${state.personalities.length} personnalités chargées`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur chargement personnalités:', error);
|
|
state.personalities = [];
|
|
}
|
|
}
|
|
|
|
// ====================
|
|
// RENDERING
|
|
// ====================
|
|
|
|
function renderModulesPalette() {
|
|
const container = document.getElementById('modulesContainer');
|
|
container.innerHTML = '';
|
|
|
|
const categories = {
|
|
core: ['generation'],
|
|
enhancement: ['selective', 'smarttouch'], // ✅ AJOUTÉ: smarttouch
|
|
protection: ['adversarial', 'human', 'pattern']
|
|
};
|
|
|
|
const categoryLabels = {
|
|
core: '🎯 Génération',
|
|
enhancement: '✨ Enhancement',
|
|
protection: '🛡️ Protection'
|
|
};
|
|
|
|
Object.entries(categories).forEach(([catKey, moduleIds]) => {
|
|
const catDiv = document.createElement('div');
|
|
catDiv.className = 'module-category';
|
|
|
|
const catTitle = document.createElement('h3');
|
|
catTitle.textContent = categoryLabels[catKey];
|
|
catDiv.appendChild(catTitle);
|
|
|
|
moduleIds.forEach(moduleId => {
|
|
const module = state.modules.find(m => m.id === moduleId);
|
|
if (!module) return;
|
|
|
|
const moduleDiv = document.createElement('div');
|
|
moduleDiv.className = 'module-item';
|
|
moduleDiv.draggable = true;
|
|
moduleDiv.dataset.moduleId = module.id;
|
|
|
|
moduleDiv.innerHTML = `
|
|
<div class="module-name">${module.name}</div>
|
|
<div class="module-desc">${module.description}</div>
|
|
`;
|
|
|
|
// Drag events
|
|
moduleDiv.addEventListener('dragstart', handleDragStart);
|
|
moduleDiv.addEventListener('dragend', handleDragEnd);
|
|
|
|
// Click to add
|
|
moduleDiv.addEventListener('click', () => {
|
|
addStep(module.id, module.modes[0]);
|
|
});
|
|
|
|
catDiv.appendChild(moduleDiv);
|
|
});
|
|
|
|
container.appendChild(catDiv);
|
|
});
|
|
}
|
|
|
|
function renderTemplates() {
|
|
const container = document.getElementById('templatesContainer');
|
|
container.innerHTML = '';
|
|
|
|
state.templates.forEach(template => {
|
|
const templateDiv = document.createElement('div');
|
|
templateDiv.className = 'template-item';
|
|
|
|
templateDiv.innerHTML = `
|
|
<div class="template-name">${template.name}</div>
|
|
<div class="template-desc">${template.description.substring(0, 60)}...</div>
|
|
`;
|
|
|
|
templateDiv.addEventListener('click', () => loadTemplate(template.id));
|
|
|
|
container.appendChild(templateDiv);
|
|
});
|
|
}
|
|
|
|
function renderPipeline() {
|
|
const container = document.getElementById('stepsContainer');
|
|
const empty = document.getElementById('canvasEmpty');
|
|
|
|
if (state.pipeline.pipeline.length === 0) {
|
|
container.style.display = 'none';
|
|
empty.style.display = 'flex';
|
|
return;
|
|
}
|
|
|
|
empty.style.display = 'none';
|
|
container.style.display = 'block';
|
|
container.innerHTML = '';
|
|
|
|
state.pipeline.pipeline.forEach((step, index) => {
|
|
const stepDiv = createStepElement(step, index);
|
|
container.appendChild(stepDiv);
|
|
});
|
|
|
|
updatePreview();
|
|
}
|
|
|
|
function createStepElement(step, index) {
|
|
const module = state.modules.find(m => m.id === step.module);
|
|
if (!module) return document.createElement('div');
|
|
|
|
const stepDiv = document.createElement('div');
|
|
stepDiv.className = 'pipeline-step';
|
|
stepDiv.dataset.stepIndex = index;
|
|
|
|
stepDiv.innerHTML = `
|
|
<div class="step-header">
|
|
<div class="step-number">${step.step}</div>
|
|
<div class="step-title">${module.name} - ${step.mode}</div>
|
|
<div class="step-actions">
|
|
<button class="step-btn" onclick="moveStepUp(${index})" ${index === 0 ? 'disabled' : ''}>↑</button>
|
|
<button class="step-btn" onclick="moveStepDown(${index})" ${index === state.pipeline.pipeline.length - 1 ? 'disabled' : ''}>↓</button>
|
|
<button class="step-btn" onclick="duplicateStep(${index})">📋</button>
|
|
<button class="step-btn" onclick="deleteStep(${index})">🗑️</button>
|
|
</div>
|
|
</div>
|
|
<div class="step-config">
|
|
<div class="config-row">
|
|
<label>Mode:</label>
|
|
<select onchange="updateStepMode(${index}, this.value)">
|
|
${module.modes.map(mode =>
|
|
`<option value="${mode}" ${mode === step.mode ? 'selected' : ''}>${mode}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="config-row">
|
|
<label>Intensité:</label>
|
|
<input type="number" step="0.1" min="0.1" max="2.0" value="${step.intensity || 1.0}"
|
|
onchange="updateStepIntensity(${index}, parseFloat(this.value))">
|
|
</div>
|
|
${renderModuleParameters(step, index, module)}
|
|
</div>
|
|
`;
|
|
|
|
return stepDiv;
|
|
}
|
|
|
|
function renderModuleParameters(step, index, module) {
|
|
let html = '';
|
|
|
|
// Toujours afficher le dropdown LLM Provider en premier
|
|
const currentProvider = step.parameters?.llmProvider || module.defaultLLM || '';
|
|
const defaultProvider = module.defaultLLM || 'claude';
|
|
|
|
html += `
|
|
<div class="config-row">
|
|
<label>LLM:</label>
|
|
<select onchange="updateStepParameter(${index}, 'llmProvider', this.value)">
|
|
<option value="">Default (${defaultProvider})</option>
|
|
${state.llmProviders.map(provider =>
|
|
`<option value="${provider.id}" ${provider.id === currentProvider ? 'selected' : ''}>${provider.name}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
`;
|
|
|
|
// ✅ NOUVEAU: Afficher dropdown personnalité pour SmartTouch
|
|
if (step.module === 'smarttouch' && state.personalities.length > 0) {
|
|
const currentPersonality = step.parameters?.personalityName || '';
|
|
|
|
html += `
|
|
<div class="config-row">
|
|
<label>Personnalité:</label>
|
|
<select onchange="updateStepParameter(${index}, 'personalityName', this.value)">
|
|
<option value="">Auto (from csvData)</option>
|
|
${state.personalities.map(personality =>
|
|
`<option value="${personality.nom}" ${personality.nom === currentPersonality ? 'selected' : ''}>${personality.nom} (${personality.style})</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Autres paramètres du module (sauf llmProvider et personalityName qui sont déjà affichés)
|
|
if (module.parameters && Object.keys(module.parameters).length > 0) {
|
|
Object.entries(module.parameters).forEach(([paramName, paramConfig]) => {
|
|
// Skip llmProvider et personalityName car déjà affichés ci-dessus
|
|
if (paramName === 'llmProvider' || paramName === 'personalityName') return;
|
|
|
|
const value = step.parameters?.[paramName] || paramConfig.default || '';
|
|
|
|
if (paramConfig.enum) {
|
|
html += `
|
|
<div class="config-row">
|
|
<label>${paramName}:</label>
|
|
<select onchange="updateStepParameter(${index}, '${paramName}', this.value)">
|
|
${paramConfig.enum.map(opt =>
|
|
`<option value="${opt}" ${opt === value ? 'selected' : ''}>${opt}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
`;
|
|
} else if (paramConfig.type === 'number') {
|
|
html += `
|
|
<div class="config-row">
|
|
<label>${paramName}:</label>
|
|
<input type="number" step="${paramConfig.step || 0.1}"
|
|
min="${paramConfig.min || 0}" max="${paramConfig.max || 10}"
|
|
value="${value}"
|
|
onchange="updateStepParameter(${index}, '${paramName}', parseFloat(this.value))">
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// ====================
|
|
// PIPELINE OPERATIONS
|
|
// ====================
|
|
|
|
function addStep(moduleId, mode = null) {
|
|
const module = state.modules.find(m => m.id === moduleId);
|
|
if (!module) return;
|
|
|
|
const newStep = {
|
|
step: state.nextStepNumber++,
|
|
module: moduleId,
|
|
mode: mode || module.modes[0],
|
|
intensity: module.defaultIntensity || 1.0,
|
|
parameters: {},
|
|
enabled: true
|
|
};
|
|
|
|
state.pipeline.pipeline.push(newStep);
|
|
reorderSteps();
|
|
renderPipeline();
|
|
}
|
|
|
|
function deleteStep(index) {
|
|
state.pipeline.pipeline.splice(index, 1);
|
|
reorderSteps();
|
|
renderPipeline();
|
|
}
|
|
|
|
function duplicateStep(index) {
|
|
const step = state.pipeline.pipeline[index];
|
|
const duplicated = JSON.parse(JSON.stringify(step));
|
|
duplicated.step = state.nextStepNumber++;
|
|
|
|
state.pipeline.pipeline.splice(index + 1, 0, duplicated);
|
|
reorderSteps();
|
|
renderPipeline();
|
|
}
|
|
|
|
function moveStepUp(index) {
|
|
if (index === 0) return;
|
|
|
|
const temp = state.pipeline.pipeline[index];
|
|
state.pipeline.pipeline[index] = state.pipeline.pipeline[index - 1];
|
|
state.pipeline.pipeline[index - 1] = temp;
|
|
|
|
reorderSteps();
|
|
renderPipeline();
|
|
}
|
|
|
|
function moveStepDown(index) {
|
|
if (index === state.pipeline.pipeline.length - 1) return;
|
|
|
|
const temp = state.pipeline.pipeline[index];
|
|
state.pipeline.pipeline[index] = state.pipeline.pipeline[index + 1];
|
|
state.pipeline.pipeline[index + 1] = temp;
|
|
|
|
reorderSteps();
|
|
renderPipeline();
|
|
}
|
|
|
|
function updateStepMode(index, mode) {
|
|
state.pipeline.pipeline[index].mode = mode;
|
|
updatePreview();
|
|
}
|
|
|
|
function updateStepIntensity(index, intensity) {
|
|
state.pipeline.pipeline[index].intensity = intensity;
|
|
updatePreview();
|
|
}
|
|
|
|
function updateStepParameter(index, paramName, value) {
|
|
if (!state.pipeline.pipeline[index].parameters) {
|
|
state.pipeline.pipeline[index].parameters = {};
|
|
}
|
|
|
|
// Si value est vide/null/undefined, supprimer la clé pour utiliser le default
|
|
if (value === '' || value === null || value === undefined) {
|
|
delete state.pipeline.pipeline[index].parameters[paramName];
|
|
} else {
|
|
state.pipeline.pipeline[index].parameters[paramName] = value;
|
|
}
|
|
|
|
updatePreview();
|
|
}
|
|
|
|
function reorderSteps() {
|
|
state.pipeline.pipeline.forEach((step, index) => {
|
|
step.step = index + 1;
|
|
});
|
|
state.nextStepNumber = state.pipeline.pipeline.length + 1;
|
|
}
|
|
|
|
function clearPipeline() {
|
|
if (!confirm('Effacer tout le pipeline ?')) return;
|
|
|
|
state.pipeline.pipeline = [];
|
|
state.nextStepNumber = 1;
|
|
document.getElementById('pipelineName').value = '';
|
|
document.getElementById('pipelineDesc').value = '';
|
|
|
|
renderPipeline();
|
|
}
|
|
|
|
// ====================
|
|
// DRAG & DROP
|
|
// ====================
|
|
|
|
let draggedElement = null;
|
|
|
|
function handleDragStart(e) {
|
|
draggedElement = e.target;
|
|
e.target.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'copy';
|
|
e.dataTransfer.setData('moduleId', e.target.dataset.moduleId);
|
|
}
|
|
|
|
function handleDragEnd(e) {
|
|
e.target.classList.remove('dragging');
|
|
}
|
|
|
|
// Setup drop zone
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const canvas = document.getElementById('pipelineCanvas');
|
|
|
|
canvas.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'copy';
|
|
});
|
|
|
|
canvas.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
const moduleId = e.dataTransfer.getData('moduleId');
|
|
if (moduleId) {
|
|
addStep(moduleId);
|
|
}
|
|
});
|
|
|
|
// Add step button
|
|
document.getElementById('addStepBtn').addEventListener('click', () => {
|
|
const firstModule = state.modules[0];
|
|
if (firstModule) {
|
|
addStep(firstModule.id);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ====================
|
|
// TEMPLATES
|
|
// ====================
|
|
|
|
async function loadTemplate(templateId) {
|
|
try {
|
|
const response = await fetch(`/api/pipeline/templates/${templateId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
state.pipeline = data.template;
|
|
state.nextStepNumber = data.template.pipeline.length + 1;
|
|
|
|
document.getElementById('pipelineName').value = data.template.name;
|
|
document.getElementById('pipelineDesc').value = data.template.description || '';
|
|
|
|
renderPipeline();
|
|
showStatus(`Template "${data.template.name}" chargé`, 'success');
|
|
}
|
|
} catch (error) {
|
|
showStatus(`Erreur chargement template: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// ====================
|
|
// SAVE / VALIDATE / TEST
|
|
// ====================
|
|
|
|
async function savePipeline() {
|
|
const name = document.getElementById('pipelineName').value.trim();
|
|
const description = document.getElementById('pipelineDesc').value.trim();
|
|
|
|
if (!name) {
|
|
showStatus('Nom du pipeline requis', 'error');
|
|
return;
|
|
}
|
|
|
|
if (state.pipeline.pipeline.length === 0) {
|
|
showStatus('Pipeline vide, ajoutez au moins une étape', 'error');
|
|
return;
|
|
}
|
|
|
|
state.pipeline.name = name;
|
|
state.pipeline.description = description;
|
|
state.pipeline.metadata.saved = new Date().toISOString();
|
|
|
|
try {
|
|
const response = await fetch('/api/pipeline/save', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ pipelineDefinition: state.pipeline })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showStatus(`✅ Pipeline "${name}" sauvegardé`, 'success');
|
|
} else {
|
|
showStatus(`Erreur: ${data.error}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
showStatus(`Erreur sauvegarde: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function validatePipeline() {
|
|
const name = document.getElementById('pipelineName').value.trim();
|
|
if (!name) {
|
|
state.pipeline.name = 'Unnamed Pipeline';
|
|
} else {
|
|
state.pipeline.name = name;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/pipeline/validate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ pipelineDefinition: state.pipeline })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.valid) {
|
|
showStatus('✅ Pipeline valide', 'success');
|
|
} else {
|
|
showStatus(`❌ Erreurs: ${data.errors.join(', ')}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
showStatus(`Erreur validation: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function testPipeline() {
|
|
const name = document.getElementById('pipelineName').value.trim();
|
|
|
|
if (!name) {
|
|
showStatus('Nom du pipeline requis pour le test', 'error');
|
|
return;
|
|
}
|
|
|
|
state.pipeline.name = name;
|
|
state.pipeline.description = document.getElementById('pipelineDesc').value.trim();
|
|
|
|
const rowNumber = prompt('Numéro de ligne Google Sheets à tester ?', '2');
|
|
if (!rowNumber) return;
|
|
|
|
showStatus('🚀 Test en cours...', 'info');
|
|
|
|
try {
|
|
const response = await fetch('/api/pipeline/execute', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
pipelineConfig: state.pipeline,
|
|
rowNumber: parseInt(rowNumber)
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showStatus(`✅ Test réussi! Durée: ${data.result.stats.totalDuration}ms`, 'success');
|
|
console.log('Test result:', data.result);
|
|
} else {
|
|
showStatus(`❌ Test échoué: ${data.error}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
showStatus(`Erreur test: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// ====================
|
|
// PREVIEW & HELPERS
|
|
// ====================
|
|
|
|
function updatePreview() {
|
|
const preview = document.getElementById('previewJson');
|
|
preview.textContent = JSON.stringify(state.pipeline, null, 2);
|
|
}
|
|
|
|
function showStatus(message, type) {
|
|
const status = document.getElementById('status');
|
|
status.textContent = message;
|
|
status.className = `status ${type}`;
|
|
status.style.display = 'block';
|
|
|
|
setTimeout(() => {
|
|
status.style.display = 'none';
|
|
}, 5000);
|
|
}
|