- 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>
1300 lines
41 KiB
HTML
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>
|