diff --git a/src/pages/ops.astro b/src/pages/ops.astro index aa61cce..55f053b 100644 --- a/src/pages/ops.astro +++ b/src/pages/ops.astro @@ -8,13 +8,19 @@ import '@/styles/global.css';
-
-

Ops L'Electron Rare

-

Centre opérationnel · IA mascarade intégrée

+
+ +
+

Ops

+

L'Electron Rare · Centre opérationnel

+
-
- - Connexion... +
+
+ + Connexion... +
+ Intranet
@@ -34,149 +40,53 @@ import '@/styles/global.css';

Actions en cours

-
-
- - -
-

Urgent — Deadline proche

-
-
-
- - - 31 mars - -
-
-

Rédiger contrat partenariat formation avec Funky Audio Tools.

-
- -
-
- -
-
- - - 31 mars - -
-
-

Compléter le one-pager collectif avec Hémisphère.

-
-
-
+
+
+ +
- -
-

Prospection

-
-
-
- - - Prospection - -
-
-

Proposition formation Dispositifs Culturels. Portes ouvertes 31 mars.

-
- -
-
- -
-
- - - Formation - -
-
-
- -
-
- -
-
- - - 30 avril -
-
+ + - -
-

LinkedIn — Planning éditorial

-
-
+ +
Chargement des tâches...
- -
-

Technique restant

-
-
SEO

Se connecter à Google Search Console, ajouter www.lelectronrare.fr, copier le code TXT de vérification.

-
Config
-
KPI
-
-
- -
-

Lancement — 1er mai

-
-
1er mai
bash ~/lib/launch-site.sh

Rollback si problème :

bash ~/lib/rollback-site.sh
-
1er mai
-
1er mai
-
1er mai
bash ~/lib/deploy-site.sh
-
-
- - -
-

Complété — 20 mars

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+

Business Plan — Backlog maître

@@ -386,29 +296,39 @@ import '@/styles/global.css';
-

CRM Live

+
+

CRM Live

+ +
-
-
Leads total
-
-
Nouveaux
-
-
Score IA moyen
-
-
Leads chauds
+
📊
-
Leads total
+
🆕
-
Nouveaux
+
🤖
-
Score IA moyen
+
🔥
-
Leads chauds

Derniers leads

-
NomOrganisationSegmentScore IAUrgenceDate
-
- Ouvrir Frappe CRM - Dashboards Metabase - +
NomOrganisationSegmentScore IAUrgenceDate
Chargement...
+
-

Agents mascarade

+
+

Agents mascarade

+ 16 agents +
+

Cliquer sur un agent pour le tester dans le copilote.

-
@@ -454,6 +374,184 @@ import '@/styles/global.css'; const MASCARADE_KEY = '845f8173270c9c4eb051d645cc58ac6882933341e82bff218da48aebb6e5b029'; const CRM_API = 'https://api.lelectronrare.fr'; + const OPS_API = CRM_API + '/api/ops'; + + // --- Load tasks from DB --- + let allTasks = []; + const categoryLabels = { urgent:'Urgent', prospection:'Prospection', linkedin:'LinkedIn', technique:'Technique', lancement:'Lancement 1er mai', formation:'Formation', gie:'GIE Hémisphère', crm:'CRM', marketing:'Marketing' }; + const categoryColors = { urgent:'red', prospection:'amber', linkedin:'blue', technique:'cyan', lancement:'green', formation:'amber', gie:'amber', crm:'cyan', marketing:'blue' }; + const statusIcons = { 'À faire':'○', 'En cours':'◐', 'Terminé':'●', 'Reporté':'◌' }; + + async function loadTasks() { + const container = document.getElementById('tasks-container'); + try { + const res = await fetch(OPS_API + '/tasks', { headers: { 'Content-Type': 'application/json' } }); + const data = await res.json(); + allTasks = data.data || []; + renderTasks(); + } catch (e) { + container.innerHTML = '
Erreur chargement: ' + e.message + '
'; + } + } + + function renderTasks() { + const container = document.getElementById('tasks-container'); + const groups = {}; + const done = []; + allTasks.forEach(t => { + if (t.status === 'Terminé') { done.push(t); return; } + const cat = t.category || 'technique'; + if (!groups[cat]) groups[cat] = []; + groups[cat].push(t); + }); + + let html = ''; + // Active tasks by category + for (const [cat, tasks] of Object.entries(groups)) { + const color = categoryColors[cat] || 'cyan'; + const label = categoryLabels[cat] || cat; + html += '

' + label + ' (' + tasks.length + ')

'; + tasks.forEach(t => { html += renderTask(t); }); + html += '
'; + } + + // Completed + if (done.length > 0) { + html += '
Terminé (' + done.length + ')
'; + done.forEach(t => { html += renderTask(t, true); }); + html += '
'; + } + + // Stats + const stats = document.getElementById('workflow-stats'); + if (stats) stats.innerHTML = '' + done.length + '/' + allTasks.length + ' terminées'; + + container.innerHTML = html; + bindTaskEvents(); + } + + function renderTask(t, isDone) { + const overdue = t.deadline && new Date(t.deadline) < new Date() && t.status !== 'Terminé'; + const color = categoryColors[t.category] || 'cyan'; + const hasAI = t.ai_agent ? true : false; + + return '
' + + '
' + + '' + (statusIcons[t.status] || '○') + '' + + '' + (t.task_title || '') + '' + + (t.priority ? '' + t.priority + '' : '') + + (t.deadline ? '' + t.deadline + '' : '') + + (hasAI ? 'IA' : '') + + '
' + + '
' + + (t.description ? '

' + t.description + '

' : '') + + (t.ai_result ? '

Résultat IA

' + t.ai_result.replace(/
' : '') + + '
' + + '' + + '' + + '' + + (hasAI ? '' : '') + + '' + + '
' + + '
' + + '
' + + '
'; + } + + function bindTaskEvents() { + // Open/close + document.querySelectorAll('#tasks-container .task-header').forEach(h => { + h.addEventListener('click', () => h.closest('.task')?.classList.toggle('task--open')); + }); + + // Save (status + notes) + document.querySelectorAll('.task-save-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const id = btn.dataset.id; + const task = btn.closest('.task'); + const status = task.querySelector('.task-status-select')?.value; + const notes = task.querySelector('.task-note-input')?.value; + btn.textContent = '...'; + try { + await fetch(OPS_API + '/tasks/' + id, { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status, notes }) + }); + btn.textContent = '✓'; + setTimeout(() => loadTasks(), 500); + } catch { btn.textContent = 'Erreur'; } + }); + }); + + // Execute AI + document.querySelectorAll('.task-exec-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const id = btn.dataset.id; + const output = document.getElementById('ai-out-' + id); + if (output) { output.innerHTML = '
Exécution IA...
'; output.style.display = 'block'; } + btn.textContent = '...'; + try { + const res = await fetch(OPS_API + '/execute/' + id, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); + const data = await res.json(); + if (output) { + const text = data.result || data.error || 'Pas de résultat'; + output.innerHTML = '
' + text.replace(/
'; + output.querySelector('.copy-btn')?.addEventListener('click', function() { navigator.clipboard.writeText(text); this.textContent = 'Copié !'; }); + } + btn.textContent = 'Exécuter IA'; + loadTasks(); + } catch (err) { + if (output) output.innerHTML = '
Erreur: ' + err.message + '
'; + btn.textContent = 'Exécuter IA'; + } + }); + }); + + // Delete + document.querySelectorAll('.task-delete-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + if (!confirm('Supprimer cette tâche ?')) return; + await fetch(OPS_API + '/tasks/' + btn.dataset.id, { method: 'DELETE' }); + loadTasks(); + }); + }); + } + + // Add task form + document.getElementById('add-task-btn')?.addEventListener('click', () => { + document.getElementById('add-task-form').style.display = document.getElementById('add-task-form').style.display === 'none' ? 'block' : 'none'; + document.getElementById('new-task-title')?.focus(); + }); + document.getElementById('cancel-task-btn')?.addEventListener('click', () => { + document.getElementById('add-task-form').style.display = 'none'; + }); + document.getElementById('save-task-btn')?.addEventListener('click', async () => { + const title = document.getElementById('new-task-title')?.value?.trim(); + if (!title) return; + const data = { + task_title: title, + category: document.getElementById('new-task-category')?.value || 'technique', + priority: document.getElementById('new-task-priority')?.value || 'P2', + deadline: document.getElementById('new-task-deadline')?.value || null, + ai_agent: document.getElementById('new-task-agent')?.value || null, + status: 'À faire', + assigned_to: 'clement@lelectronrare.fr' + }; + await fetch(OPS_API + '/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); + document.getElementById('new-task-title').value = ''; + document.getElementById('add-task-form').style.display = 'none'; + loadTasks(); + }); + document.getElementById('refresh-tasks')?.addEventListener('click', loadTasks); + + // Initial load + loadTasks(); + // --- Tabs --- document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { @@ -507,36 +605,57 @@ import '@/styles/global.css'; { id:'li5', label:'#05 — Annonce lancement', date:'Mar 22 avril, 8h', due:'2026-04-22', text:'Le 1er mai, www.lelectronrare.fr sera en ligne.\n\n→ Missions techniques\n→ Catalogue formations\n→ Portfolio\n→ Contact direct\n\nSystèmes électroniques spécifiques. Du sur-mesure.' }, ]; const liContainer = document.getElementById('linkedin-tasks'); - liPosts.forEach(p => { + liPosts.forEach((p, i) => { const overdue = new Date(p.due) < new Date(); + const lines = p.text.split('\n').map(l => l ? `

${l.replace(/` : '

').join(''); liContainer.innerHTML += ` -
-
+
+
- - ${p.date} +
+ ${p.label} + ${p.date} +
+ ${i+1}/5
-
-
${p.text}
-
- - LinkedIn - +
+
+
+
CS
+
Clément Saillant
Ingénieur électronique · L'Electron Rare
+
+
${lines}
+
+
+ + Publier sur LinkedIn +
`; }); - // Re-bind events for generated tasks + // Bind LinkedIn card events liContainer.querySelectorAll('.task-check').forEach(cb => { const key = 'ops-' + cb.id; cb.checked = localStorage.getItem(key) === '1'; - if (cb.checked) cb.closest('.task')?.classList.add('task--done'); - cb.addEventListener('change', () => { localStorage.setItem(key, cb.checked ? '1' : '0'); cb.closest('.task')?.classList.toggle('task--done', cb.checked); updateStats(); }); + if (cb.checked) cb.closest('.li-card')?.classList.add('task--done'); + cb.addEventListener('change', () => { localStorage.setItem(key, cb.checked ? '1' : '0'); cb.closest('.li-card')?.classList.toggle('task--done', cb.checked); updateStats(); }); + }); + liContainer.querySelectorAll('.li-card-header').forEach(h => { + h.addEventListener('click', e => { + if (e.target.tagName === 'INPUT') return; + h.closest('.li-card')?.classList.toggle('li-card--open'); + }); + }); + liContainer.querySelectorAll('.copy-btn').forEach(btn => { + btn.addEventListener('click', () => { + navigator.clipboard.writeText(decodeURIComponent(btn.dataset.text)); + btn.textContent = 'Copié !'; + setTimeout(() => btn.textContent = 'Copier le texte', 2000); + }); }); - liContainer.querySelectorAll('.task-header').forEach(h => { h.addEventListener('click', e => { if (e.target.tagName === 'INPUT' || e.target.closest('button')) return; h.closest('.task')?.classList.toggle('task--open'); }); }); - liContainer.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', () => { navigator.clipboard.writeText(decodeURIComponent(btn.dataset.text)); btn.textContent = 'Copié !'; setTimeout(() => btn.textContent = 'Copier', 2000); }); }); // Duplicate LinkedIn posts in marketing tab const mkLi = document.getElementById('marketing-linkedin'); @@ -557,23 +676,169 @@ import '@/styles/global.css'; updateStats(); // --- Mascarade AI calls --- - async function callAgent(agent, prompt, outputId) { + // --- AI Workflow with conversation --- + const aiConversations = {}; + + async function aiSend(agent, messages) { + const res = await fetch(MASCARADE_API + '/api/ai/' + agent + '/run', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages }) + }); + const data = await res.json(); + return data.response || data.content || data.message || data.result || JSON.stringify(data); + } + + function renderConversation(outputId) { + const conv = aiConversations[outputId]; const output = document.getElementById('ai-output-' + outputId); - if (!output) return; - output.innerHTML = '
Génération en cours...
'; + if (!output || !conv) return; + + let html = '
'; + + // Messages + conv.messages.forEach((m, i) => { + if (m.role === 'user') { + html += `
Vous

${m.content.replace(/

`; + } else { + html += `
${conv.agent}
${m.content.replace(/
+          
+ + ${m.content.length > 50 ? `` : ''} +
+
`; + } + }); + + // Loading + if (conv.loading) { + html += '
' + conv.agent + '
Réflexion...
'; + } + + // Input area + if (!conv.loading) { + html += ` +
+
+ ${conv.messages.length === 0 ? ` + + + + + ` : ` + + + + + + + `} +
+
+ + + +
+
+ + +
+
`; + } + + html += '
'; + output.innerHTML = html; output.style.display = 'block'; - try { - const res = await fetch(MASCARADE_API + '/api/ai/' + agent + '/run', { - method: 'POST', - headers: { 'Authorization': 'Bearer ' + MASCARADE_KEY, 'Content-Type': 'application/json' }, - body: JSON.stringify({ messages: [{ role: 'user', content: prompt }] }) + + // Bind events + output.querySelectorAll('.copy-btn').forEach(btn => { + btn.addEventListener('click', () => { + const idx = parseInt(btn.dataset.idx); + navigator.clipboard.writeText(conv.messages[idx].content); + btn.textContent = 'Copié !'; + setTimeout(() => btn.textContent = 'Copier', 2000); }); - const data = await res.json(); - const text = data.response || data.content || data.message || data.result || JSON.stringify(data); - output.innerHTML = '
' + text.replace(/
'; - output.querySelector('.copy-btn').addEventListener('click', function() { navigator.clipboard.writeText(text); this.textContent = 'Copié !'; }); + }); + + output.querySelectorAll('.refine-btn').forEach(btn => { + btn.addEventListener('click', () => { + const ta = output.querySelector('.ai-conv-textarea'); + if (ta) { ta.value = 'Affine cette réponse : '; ta.focus(); } + }); + }); + + output.querySelectorAll('.ai-suggestion').forEach(btn => { + btn.addEventListener('click', () => sendConvMessage(outputId, btn.dataset.msg)); + }); + + const sendBtn = output.querySelector('.ai-conv-send'); + const textarea = output.querySelector('.ai-conv-textarea'); + if (sendBtn && textarea) { + sendBtn.addEventListener('click', () => { if (textarea.value.trim()) sendConvMessage(outputId, textarea.value.trim()); }); + textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (textarea.value.trim()) sendConvMessage(outputId, textarea.value.trim()); } }); + } + + const agentSel = output.querySelector('.ai-agent-select'); + if (agentSel) agentSel.addEventListener('change', () => { conv.agent = agentSel.value; }); + + output.querySelector('.ai-conv-close')?.addEventListener('click', () => { output.style.display = 'none'; }); + output.querySelector('.ai-conv-reset')?.addEventListener('click', () => { conv.messages = []; renderConversation(outputId); }); + + // Scroll to bottom + const convEl = output.querySelector('.ai-conv'); + if (convEl) convEl.scrollTop = convEl.scrollHeight; + } + + async function sendConvMessage(outputId, userMsg) { + const conv = aiConversations[outputId]; + conv.messages.push({ role: 'user', content: userMsg }); + conv.loading = true; + renderConversation(outputId); + + try { + const text = await aiSend(conv.agent, conv.messages); + conv.messages.push({ role: 'assistant', content: text }); } catch (e) { - output.innerHTML = '
Erreur: ' + e.message + '
'; + conv.messages.push({ role: 'assistant', content: 'Erreur: ' + e.message }); + } + conv.loading = false; + renderConversation(outputId); + } + + function callAgent(agent, prompt, outputId) { + // Initialize conversation with context + aiConversations[outputId] = { + agent, + messages: [], + loading: false, + context: prompt + }; + // Add system context as first user message + if (prompt) { + aiConversations[outputId].messages.push({ role: 'user', content: prompt }); + aiConversations[outputId].loading = true; + renderConversation(outputId); + aiSend(agent, [{ role: 'user', content: prompt }]).then(text => { + aiConversations[outputId].messages.push({ role: 'assistant', content: text }); + aiConversations[outputId].loading = false; + renderConversation(outputId); + }).catch(e => { + aiConversations[outputId].messages.push({ role: 'assistant', content: 'Erreur: ' + e.message }); + aiConversations[outputId].loading = false; + renderConversation(outputId); + }); + } else { + renderConversation(outputId); } } @@ -607,7 +872,7 @@ import '@/styles/global.css'; const agent = chatAgent.value; const res = await fetch(MASCARADE_API + '/api/ai/' + agent + '/run', { method: 'POST', - headers: { 'Authorization': 'Bearer ' + MASCARADE_KEY, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: msg }] }) }); const data = await res.json(); @@ -637,7 +902,7 @@ import '@/styles/global.css'; try { const res = await fetch(MASCARADE_API + '/api/ai/analyst/run', { method: 'POST', - headers: { 'Authorization': 'Bearer ' + MASCARADE_KEY, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: 'Requête Frappe CRM: donne-moi le nombre total de leads, le nombre de leads New, le score IA moyen, et le nombre de leads chauds (custom_ai_urgency=chaud). Aussi liste les 5 derniers leads avec nom, organisation, segment, score IA, urgence, date. Format JSON strict.' }] }) }); // Fallback: just show link to Metabase @@ -650,7 +915,7 @@ import '@/styles/global.css'; async function loadAgents() { const grid = document.getElementById('agents-grid'); try { - const res = await fetch(MASCARADE_API + '/api/ai/', { headers: { 'Content-Type': 'application/json' } }); + const res = await fetch(MASCARADE_API + '/api/ai/', { headers: { 'Content-Type': 'application/json' }}); const agents = await res.json(); const list = Array.isArray(agents) ? agents : agents.agents || []; grid.innerHTML = list.map(a => ` @@ -707,59 +972,71 @@ import '@/styles/global.css';