/** * 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: [], nextStepNumber: 1 }; // ==================== // INITIALIZATION // ==================== window.onload = async function() { await loadModules(); await loadTemplates(); await loadLLMProviders(); 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 state.llmProviders = [ { id: 'claude', name: 'Claude (Anthropic)', default: true }, { id: 'openai', name: 'OpenAI GPT-4' }, { id: 'gemini', name: 'Google Gemini' }, { id: 'deepseek', name: 'Deepseek' }, { id: 'moonshot', name: 'Moonshot' }, { id: 'mistral', name: 'Mistral AI' } ]; } } // ==================== // RENDERING // ==================== function renderModulesPalette() { const container = document.getElementById('modulesContainer'); container.innerHTML = ''; const categories = { core: ['generation'], enhancement: ['selective'], 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 = `
${module.name}
${module.description}
`; // 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 = `
${template.name}
${template.description.substring(0, 60)}...
`; 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 = `
${step.step}
${module.name} - ${step.mode}
${renderModuleParameters(step, index, module)}
`; 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 += `
`; // Autres paramètres du module (sauf llmProvider qui est déjà affiché) if (module.parameters && Object.keys(module.parameters).length > 0) { Object.entries(module.parameters).forEach(([paramName, paramConfig]) => { // Skip llmProvider car déjà affiché ci-dessus if (paramName === 'llmProvider') return; const value = step.parameters?.[paramName] || paramConfig.default || ''; if (paramConfig.enum) { html += `
`; } else if (paramConfig.type === 'number') { html += `
`; } }); } 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); }