Files
rpa_vision_v3/agent_chat/templates/chat.html
Dom 928b9e1065 feat: import Excel via chat Léa, suppression nœuds VWB, fix temperature 0.1
- Chat Léa : "importe patients.xlsx" → preview → confirmation → table SQLite
  Bouton 📎 pour upload fichier, "montre les tables", "info table X"
- VWB : suppression nœuds via touche Suppr/Backspace + bouton croix rouge
- Fix : toutes les températures VLM à 0.1 (qwen3-vl bloque à 0.0)
- Fix : capture VWB avec DISPLAY=:1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 07:18:51 +01:00

1300 lines
41 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RPA Vision V3 - Agent Assistant</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--bg-dark: #0f0f1a;
--bg-chat: #1a1a2e;
--bg-message-user: #6366f1;
--bg-message-bot: #2a2a4a;
--text-light: #e0e0e0;
--text-muted: #888;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
--border: #333355;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: var(--bg-dark);
color: var(--text-light);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
.header {
background: linear-gradient(135deg, var(--bg-chat) 0%, var(--bg-dark) 100%);
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 40px;
height: 40px;
background: var(--primary);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.header-title h1 {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-light);
}
.header-title span {
font-size: 0.75rem;
color: var(--text-muted);
}
.header-right {
display: flex;
align-items: center;
gap: 15px;
}
.status-pill {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(34, 197, 94, 0.15);
border-radius: 20px;
font-size: 0.8rem;
color: var(--success);
}
.status-pill.offline {
background: rgba(239, 68, 68, 0.15);
color: var(--error);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.mode-toggle {
display: flex;
background: var(--bg-dark);
border-radius: 8px;
padding: 3px;
}
.mode-btn {
padding: 8px 16px;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.mode-btn.active {
background: var(--primary);
color: white;
}
.mode-btn:hover:not(.active) {
color: var(--text-light);
}
/* Chat Container */
.chat-container {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Messages */
.message {
display: flex;
gap: 12px;
max-width: 85%;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.message.bot .message-avatar {
background: var(--primary);
}
.message.user .message-avatar {
background: var(--bg-message-bot);
}
.message-content {
background: var(--bg-message-bot);
padding: 12px 16px;
border-radius: 16px;
line-height: 1.5;
}
.message.user .message-content {
background: var(--bg-message-user);
border-bottom-right-radius: 4px;
}
.message.bot .message-content {
border-bottom-left-radius: 4px;
}
.message-time {
font-size: 0.7rem;
color: var(--text-muted);
margin-top: 4px;
}
/* Action Cards */
.action-card {
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
margin-top: 12px;
}
.action-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.action-card-title {
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.confidence-badge {
background: var(--primary);
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.75rem;
}
.action-params {
background: rgba(255,255,255,0.05);
border-radius: 8px;
padding: 10px;
font-size: 0.9rem;
margin-bottom: 12px;
}
.action-param {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
.action-param-key {
color: var(--text-muted);
}
.action-buttons {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--bg-message-bot);
color: var(--text-light);
}
.btn-secondary:hover {
background: var(--border);
}
.btn-danger {
background: rgba(239, 68, 68, 0.2);
color: var(--error);
}
/* Execution Progress */
.execution-progress {
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
margin-top: 12px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.progress-title {
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.progress-bar-container {
background: var(--bg-message-bot);
border-radius: 10px;
height: 8px;
overflow: hidden;
margin-bottom: 12px;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), #818cf8);
border-radius: 10px;
transition: width 0.3s ease;
}
.progress-steps {
max-height: 200px;
overflow-y: auto;
}
.progress-step {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
font-size: 0.9rem;
}
.progress-step-icon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.progress-step.completed .progress-step-icon {
background: var(--success);
color: white;
}
.progress-step.running .progress-step-icon {
background: var(--warning);
color: white;
animation: pulse 1s infinite;
}
.progress-step.pending .progress-step-icon {
background: var(--bg-message-bot);
color: var(--text-muted);
}
.progress-step.failed .progress-step-icon {
background: var(--error);
color: white;
}
/* Suggestions */
.suggestions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.suggestion {
padding: 8px 14px;
background: rgba(99, 102, 241, 0.1);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 20px;
color: var(--primary);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.suggestion:hover {
background: rgba(99, 102, 241, 0.2);
border-color: var(--primary);
}
/* Input Area */
.input-area {
padding: 20px;
background: var(--bg-chat);
border-top: 1px solid var(--border);
}
.input-container {
display: flex;
gap: 12px;
align-items: flex-end;
}
.input-wrapper {
flex: 1;
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 16px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 10px;
transition: border-color 0.2s;
}
.input-wrapper:focus-within {
border-color: var(--primary);
}
#messageInput {
flex: 1;
background: transparent;
border: none;
color: var(--text-light);
font-size: 1rem;
outline: none;
resize: none;
max-height: 120px;
line-height: 1.4;
}
#messageInput::placeholder {
color: var(--text-muted);
}
.attach-btn {
width: 48px;
height: 48px;
border-radius: 14px;
background: var(--bg-message-bot);
border: 1px solid var(--border);
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.2s;
}
.attach-btn:hover {
color: var(--primary);
border-color: var(--primary);
}
.send-btn {
width: 48px;
height: 48px;
border-radius: 14px;
background: var(--primary);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.2s;
}
.send-btn:hover {
background: var(--primary-dark);
transform: scale(1.05);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Typing Indicator */
.typing-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--bg-message-bot);
border-radius: 16px;
border-bottom-left-radius: 4px;
width: fit-content;
}
.typing-dots {
display: flex;
gap: 4px;
}
.typing-dot {
width: 8px;
height: 8px;
background: var(--text-muted);
border-radius: 50%;
animation: typingBounce 1.4s infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingBounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-6px); }
}
/* Welcome Screen */
.welcome-screen {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
}
.welcome-icon {
font-size: 64px;
margin-bottom: 20px;
}
.welcome-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 10px;
}
.welcome-subtitle {
color: var(--text-muted);
margin-bottom: 30px;
max-width: 400px;
}
.welcome-suggestions {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
max-width: 400px;
}
.welcome-suggestion {
padding: 16px 20px;
background: var(--bg-chat);
border: 1px solid var(--border);
border-radius: 12px;
cursor: pointer;
text-align: left;
transition: all 0.2s;
}
.welcome-suggestion:hover {
border-color: var(--primary);
transform: translateX(5px);
}
.welcome-suggestion-title {
font-weight: 500;
margin-bottom: 4px;
}
.welcome-suggestion-desc {
font-size: 0.85rem;
color: var(--text-muted);
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* Responsive */
@media (max-width: 600px) {
.header {
padding: 10px 15px;
}
.mode-toggle {
display: none;
}
.message {
max-width: 95%;
}
.chat-container {
padding: 15px;
}
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<div class="header-left">
<div class="logo">🤖</div>
<div class="header-title">
<h1>RPA Vision Assistant</h1>
<span>Agent autonome intelligent</span>
</div>
</div>
<div class="header-right">
<div class="mode-toggle">
<button class="mode-btn active" id="modeWorkflow">
💬 Assistant
</button>
</div>
<div class="status-pill" id="statusPill">
<span class="status-dot"></span>
<span id="statusText">Connecté</span>
</div>
</div>
</div>
<!-- Chat Container -->
<div class="chat-container" id="chatContainer">
<!-- Welcome Screen (shown initially) -->
<div class="welcome-screen" id="welcomeScreen">
<div class="welcome-icon">🤖</div>
<h2 class="welcome-title">Bienvenue !</h2>
<p class="welcome-subtitle">
Je suis votre assistant RPA. Décrivez ce que vous voulez faire en langage naturel.
</p>
<div class="welcome-suggestions">
<div class="welcome-suggestion" onclick="sendSuggestion('Ouvre YouTube et cherche une vidéo de jazz relaxant')">
<div class="welcome-suggestion-title">🎵 Lancer une vidéo YouTube</div>
<div class="welcome-suggestion-desc">Ouvre YouTube et cherche une vidéo de jazz relaxant</div>
</div>
<div class="welcome-suggestion" onclick="sendSuggestion('Facturer le client Acme Corp')">
<div class="welcome-suggestion-title">📄 Exécuter un workflow</div>
<div class="welcome-suggestion-desc">Facturer le client Acme Corp</div>
</div>
<div class="welcome-suggestion" onclick="sendSuggestion('Quels workflows sont disponibles ?')">
<div class="welcome-suggestion-title">📋 Voir les workflows</div>
<div class="welcome-suggestion-desc">Lister les workflows disponibles</div>
</div>
<div class="welcome-suggestion" onclick="sendSuggestion('Montre-moi les tables')">
<div class="welcome-suggestion-title">📊 Importer des données</div>
<div class="welcome-suggestion-desc">Importer un fichier Excel ou voir les tables existantes</div>
</div>
</div>
</div>
</div>
<!-- Input Area -->
<div class="input-area">
<div class="input-container">
<button class="attach-btn" onclick="document.getElementById('fileInput').click()" title="Joindre un fichier Excel">
<i class="bi bi-paperclip"></i>
</button>
<input type="file" id="fileInput" accept=".xlsx,.xls,.csv" style="display:none" onchange="handleFileUpload(event)">
<div class="input-wrapper">
<textarea
id="messageInput"
placeholder="Décrivez ce que vous voulez faire..."
rows="1"
onkeydown="handleKeyDown(event)"
oninput="autoResize(this)"
></textarea>
</div>
<button class="send-btn" onclick="sendMessage()" id="sendBtn">
<i class="bi bi-send-fill"></i>
</button>
</div>
</div>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
// =====================================================
// State
// =====================================================
const socket = io();
let currentMode = 'workflow'; // 'workflow' or 'agent'
let isProcessing = false;
let sessionId = null;
let pendingConfirmation = null;
// Elements
const chatContainer = document.getElementById('chatContainer');
const welcomeScreen = document.getElementById('welcomeScreen');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
// =====================================================
// Socket Events
// =====================================================
socket.on('connect', () => {
updateStatus(true);
});
socket.on('disconnect', () => {
updateStatus(false);
});
socket.on('execution_progress', (data) => {
updateExecutionProgress(data);
});
socket.on('execution_completed', (data) => {
completeExecution(data);
});
socket.on('agent_progress', (data) => {
updateAgentProgress(data);
});
// Copilot events
socket.on('copilot_step', (data) => {
showCopilotStep(data);
});
socket.on('copilot_step_result', (data) => {
updateCopilotStepResult(data);
});
socket.on('copilot_complete', (data) => {
completeCopilot(data);
});
socket.on('copilot_error', (data) => {
addMessage(`Copilot: ${data.message}`);
});
// =====================================================
// UI Functions
// =====================================================
function updateStatus(online) {
const pill = document.getElementById('statusPill');
const text = document.getElementById('statusText');
if (online) {
pill.classList.remove('offline');
text.textContent = 'Connecté';
} else {
pill.classList.add('offline');
text.textContent = 'Déconnecté';
}
}
function setMode(mode) {
currentMode = mode;
document.getElementById('modeWorkflow').classList.toggle('active', mode === 'workflow');
document.getElementById('modeAgent').classList.toggle('active', mode === 'agent');
// Update placeholder
if (mode === 'agent') {
messageInput.placeholder = "Décrivez une tâche à exécuter (ex: ouvre YouTube et cherche jazz)...";
} else {
messageInput.placeholder = "Décrivez ce que vous voulez faire...";
}
}
function hideWelcome() {
if (welcomeScreen) {
welcomeScreen.style.display = 'none';
}
}
function addMessage(content, type = 'bot', extra = null) {
hideWelcome();
const message = document.createElement('div');
message.className = `message ${type}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = type === 'bot' ? '🤖' : '👤';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
// Parse content for markdown-like formatting
let formattedContent = content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
contentDiv.innerHTML = formattedContent;
// Add extra content (action cards, progress, etc.)
if (extra) {
contentDiv.appendChild(extra);
}
message.appendChild(avatar);
message.appendChild(contentDiv);
chatContainer.appendChild(message);
scrollToBottom();
return message;
}
function addTypingIndicator() {
hideWelcome();
const indicator = document.createElement('div');
indicator.className = 'message bot';
indicator.id = 'typingIndicator';
indicator.innerHTML = `
<div class="message-avatar">🤖</div>
<div class="typing-indicator">
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
`;
chatContainer.appendChild(indicator);
scrollToBottom();
}
function removeTypingIndicator() {
const indicator = document.getElementById('typingIndicator');
if (indicator) indicator.remove();
}
function createActionCard(workflow, params, confidence) {
const card = document.createElement('div');
card.className = 'action-card';
let paramsHtml = '';
if (params && Object.keys(params).length > 0) {
paramsHtml = `
<div class="action-params">
${Object.entries(params).map(([k, v]) => `
<div class="action-param">
<span class="action-param-key">${k}</span>
<span>${v}</span>
</div>
`).join('')}
</div>
`;
}
card.innerHTML = `
<div class="action-card-header">
<div class="action-card-title">
📋 ${workflow}
<span class="confidence-badge">${Math.round(confidence * 100)}%</span>
</div>
</div>
${paramsHtml}
<div class="action-buttons">
<button class="btn btn-primary" onclick="confirmAction()">
<i class="bi bi-play-fill"></i> Exécuter
</button>
<button class="btn btn-secondary" onclick="modifyAction()">
<i class="bi bi-pencil"></i> Modifier
</button>
<button class="btn btn-danger" onclick="cancelAction()">
<i class="bi bi-x"></i> Annuler
</button>
</div>
`;
return card;
}
function createExecutionProgress() {
const progress = document.createElement('div');
progress.className = 'execution-progress';
progress.id = 'executionProgress';
progress.innerHTML = `
<div class="progress-header">
<div class="progress-title">
<i class="bi bi-gear-wide-connected" style="animation: spin 1s linear infinite;"></i>
Exécution en cours...
</div>
<button class="btn btn-danger btn-sm" onclick="cancelExecution()" style="padding: 6px 12px; font-size: 0.8rem;">
<i class="bi bi-x"></i> Annuler
</button>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progressBarFill" style="width: 0%"></div>
</div>
<div class="progress-steps" id="progressSteps"></div>
`;
// Add spin animation
const style = document.createElement('style');
style.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
document.head.appendChild(style);
return progress;
}
function updateExecutionProgress(data) {
const progressBar = document.getElementById('progressBarFill');
const progressSteps = document.getElementById('progressSteps');
if (progressBar) {
progressBar.style.width = `${data.percent || data.progress}%`;
}
if (progressSteps && data.message) {
const step = document.createElement('div');
step.className = 'progress-step running';
step.innerHTML = `
<div class="progress-step-icon"><i class="bi bi-arrow-right"></i></div>
<span>${data.message}</span>
`;
progressSteps.appendChild(step);
progressSteps.scrollTop = progressSteps.scrollHeight;
}
}
function updateAgentProgress(data) {
// Update step status in plan card
if (data.step !== undefined) {
const stepEl = document.getElementById(`step-${data.step - 1}`);
if (stepEl) {
stepEl.className = `progress-step ${data.status}`;
const icon = stepEl.querySelector('.progress-step-icon');
if (data.status === 'completed') {
icon.innerHTML = '<i class="bi bi-check"></i>';
} else if (data.status === 'running') {
icon.innerHTML = '<i class="bi bi-arrow-right"></i>';
} else if (data.status === 'failed') {
icon.innerHTML = '<i class="bi bi-x"></i>';
}
}
}
}
function completeExecution(data) {
const progressBar = document.getElementById('progressBarFill');
if (progressBar) {
progressBar.style.width = '100%';
}
const message = data.success ?
`✅ **${data.workflow || 'Tâche'}** terminé avec succès !` :
`❌ Erreur: ${data.message}`;
addMessage(message);
isProcessing = false;
updateInputState();
}
function addSuggestions(suggestions) {
const suggestionsDiv = document.createElement('div');
suggestionsDiv.className = 'suggestions';
suggestions.forEach(s => {
const btn = document.createElement('button');
btn.className = 'suggestion';
btn.textContent = s;
btn.onclick = () => sendSuggestion(s);
suggestionsDiv.appendChild(btn);
});
const lastMessage = chatContainer.querySelector('.message.bot:last-of-type .message-content');
if (lastMessage) {
lastMessage.appendChild(suggestionsDiv);
}
}
function scrollToBottom() {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
function updateInputState() {
sendBtn.disabled = isProcessing;
messageInput.disabled = isProcessing;
}
// =====================================================
// Actions
// =====================================================
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
function sendSuggestion(text) {
messageInput.value = text;
sendMessage();
}
async function sendMessage() {
const message = messageInput.value.trim();
if (!message || isProcessing) return;
// Clear input
messageInput.value = '';
messageInput.style.height = 'auto';
// Add user message
addMessage(message, 'user');
// Show typing indicator
isProcessing = true;
updateInputState();
addTypingIndicator();
try {
await sendChatRequest(message);
} catch (error) {
removeTypingIndicator();
addMessage(`❌ Erreur: ${error.message}`);
}
isProcessing = false;
updateInputState();
}
async function sendChatRequest(message) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, session_id: sessionId })
});
const data = await response.json();
removeTypingIndicator();
if (data.error) {
addMessage(`${data.error}`);
return;
}
sessionId = data.session_id;
// Handle different response types
if (data.result?.needs_confirmation && data.result?.preview) {
// Import de données — apercu avec demande de confirmation
addMessage(data.response.message);
addSuggestions(['oui', 'non']);
} else if (data.result?.needs_confirmation && data.result?.confirmation) {
pendingConfirmation = data.result.confirmation;
const card = createActionCard(
pendingConfirmation.workflow_name,
pendingConfirmation.parameters,
data.intent?.confidence || 0.9
);
addMessage(data.response.message, 'bot', card);
} else if (data.result?.gesture) {
// Geste primitif exécuté
addMessage(data.response.message);
} else if (data.result?.mode === 'copilot') {
// Mode copilot — les étapes arrivent via WebSocket
addMessage(data.response.message);
} else if (data.result?.success) {
const progress = createExecutionProgress();
addMessage(data.response.message, 'bot', progress);
} else if (data.result?.teach_me) {
// Workflow non trouvé — proposer l'apprentissage
const teachCard = document.createElement('div');
teachCard.className = 'action-card';
teachCard.innerHTML = `
<div class="action-card-header">
<div class="action-card-title">
Apprentissage disponible
</div>
</div>
<p style="margin: 8px 0; opacity: 0.8; font-size: 0.9em;">
Lancez l'enregistrement sur votre PC et montrez-moi comment faire.
</p>
<div class="action-buttons">
<button class="btn btn-primary" onclick="window.open('/api/help', '_blank')">
<i class="bi bi-mortarboard"></i> Comment m'apprendre ?
</button>
</div>
`;
addMessage(data.response.message, 'bot', teachCard);
} else if (data.result?.workflows) {
let msg = data.response.message + '\n\n';
data.result.workflows.slice(0, 5).forEach(w => {
msg += `• **${w.name}**: ${w.description || 'Pas de description'}\n`;
});
addMessage(msg);
} else if (data.result?.imported) {
// Import de données réussi
addMessage(data.response.message);
if (data.response.suggestions?.length > 0) {
addSuggestions(data.response.suggestions);
}
} else if (data.result?.tables_list !== undefined || data.result?.table_info) {
// Liste des tables ou info table
addMessage(data.response.message);
if (data.response.suggestions?.length > 0) {
addSuggestions(data.response.suggestions);
}
} else {
addMessage(data.response.message);
}
}
async function confirmAction() {
if (!pendingConfirmation) return;
isProcessing = true;
updateInputState();
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'oui', session_id: sessionId })
});
const data = await response.json();
// Show execution progress
const progress = createExecutionProgress();
addMessage("Execution en cours...", 'bot', progress);
pendingConfirmation = null;
}
function modifyAction() {
if (!pendingConfirmation) return;
addMessage("✏️ Modification non implémentée. Décrivez les changements souhaités.");
}
function cancelAction() {
pendingConfirmation = null;
addMessage("❌ Action annulée.");
}
function cancelExecution() {
socket.emit('cancel_execution');
addMessage("Demande d'annulation envoyée...");
}
// =====================================================
// File Upload
// =====================================================
async function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Afficher le message utilisateur
addMessage(`📎 ${file.name}`, 'user');
addTypingIndicator();
isProcessing = true;
updateInputState();
const formData = new FormData();
formData.append('file', file);
formData.append('session_id', sessionId || '');
try {
const response = await fetch('/api/chat/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
removeTypingIndicator();
if (data.error && !data.success) {
addMessage(`Erreur : ${data.error}`);
} else if (data.message) {
addMessage(data.message);
if (data.needs_confirmation) {
addSuggestions(['oui', 'non']);
}
} else {
addMessage(`Fichier ${file.name} recu.`);
}
} catch (error) {
removeTypingIndicator();
addMessage(`Erreur d'upload : ${error.message}`);
}
isProcessing = false;
updateInputState();
// Reset le champ fichier pour permettre de re-uploader le meme fichier
event.target.value = '';
}
// =====================================================
// Copilot Mode
// =====================================================
function showCopilotStep(data) {
const card = document.createElement('div');
card.className = 'action-card';
card.id = `copilot-step-${data.step_index}`;
card.innerHTML = `
<div class="action-card-header">
<div class="action-card-title">
Copilot - Étape ${data.step_index + 1}/${data.total}
</div>
<span style="font-size: 0.8em; opacity: 0.6;">${data.workflow}</span>
</div>
<p style="margin: 8px 0; font-size: 0.95em;">
<strong>${data.action.type}</strong>: ${data.action.description}
</p>
<div class="action-buttons" id="copilot-btns-${data.step_index}">
<button class="btn btn-primary" onclick="copilotApprove(${data.step_index})">
<i class="bi bi-check-lg"></i> Exécuter
</button>
<button class="btn btn-secondary" onclick="copilotSkip(${data.step_index})">
<i class="bi bi-skip-forward"></i> Passer
</button>
<button class="btn btn-danger" onclick="copilotAbort()">
<i class="bi bi-x-circle"></i> Annuler tout
</button>
</div>
`;
addMessage(`Copilot étape ${data.step_index + 1}/${data.total}`, 'bot', card);
}
function copilotApprove(stepIndex) {
socket.emit('copilot_approve');
const btns = document.getElementById(`copilot-btns-${stepIndex}`);
if (btns) btns.innerHTML = '<span style="color: var(--success);">Approuvé - en cours...</span>';
}
function copilotSkip(stepIndex) {
socket.emit('copilot_skip');
const btns = document.getElementById(`copilot-btns-${stepIndex}`);
if (btns) btns.innerHTML = '<span style="color: var(--warning);">Passé</span>';
}
function copilotAbort() {
socket.emit('copilot_abort');
}
function updateCopilotStepResult(data) {
const card = document.getElementById(`copilot-step-${data.step_index}`);
if (!card) return;
const btns = card.querySelector('.action-buttons') ||
document.getElementById(`copilot-btns-${data.step_index}`);
if (!btns) return;
if (data.status === 'completed') {
btns.innerHTML = '<span style="color: var(--success);">Réussi</span>';
} else if (data.status === 'failed') {
btns.innerHTML = `<span style="color: var(--error);">Échoué: ${data.message}</span>`;
} else if (data.status === 'skipped') {
btns.innerHTML = '<span style="color: var(--warning);">Passé</span>';
}
}
function completeCopilot(data) {
const statusColor = data.status === 'completed' ? 'var(--success)' :
data.status === 'aborted' ? 'var(--error)' : 'var(--warning)';
addMessage(`<span style="color: ${statusColor};">Copilot terminé: ${data.message}</span>`);
}
// =====================================================
// Init
// =====================================================
document.addEventListener('DOMContentLoaded', () => {
messageInput.focus();
});
</script>
</body>
</html>