Files
linear-coding-agent/generations/library_rag/templates/chat.html
David Blanc Brioir 48470236da Amélioration majeure du système RAG avec diversification par auteur
## Nouvelles fonctionnalités

### 1. Recherche RAG avec diversification par auteur (flask_app.py)
- Fonction `diverse_author_search()` : agrégation intelligente par auteur
- Résout le problème de biais corpus (auteurs prolifiques vs peu représentés)
- Allocation adaptative :
  * 1 auteur → jusqu'à 25 chunks pour contexte riche
  * 2-3 auteurs → distribution équitable (12 chunks/auteur)
  * 4+ auteurs → limitation à 3 chunks/auteur pour diversité
- Pool initial de 200 chunks pour identifier tous les auteurs pertinents

### 2. Re-ranking LLM amélioré (flask_app.py)
- Prompt ultra-strict : force réponse sans markdown ni explications
- Parsing robuste : nettoie markdown (**texte**, __texte__)
- Fallback intelligent : garde tous les chunks si re-ranking trop strict (<50%)
- Logs détaillés des chunks exclus pour debugging

### 3. Interface utilisateur améliorée (chat.html)
- **Accordéon pour chunks RAG** : expansion/collapse avec chevron
- **Reformulation avec choix utilisateur** :
  * Endpoint `/chat/reformulate` séparé
  * Affichage côte-à-côte (originale vs reformulée)
  * Boutons de sélection avant lancement RAG
  * Badge "✓ Utilisée" sur version choisie
- **Layout full-width** : 60% conversation / 40% contexte RAG
- **Sidebar navigation** : menu hamburger avec overlay

### 4. Logs et debugging
- Logs détaillés à chaque étape du pipeline
- Affichage des auteurs trouvés et scores moyens
- Liste des chunks exclus par re-ranking avec extraits

## Améliorations techniques
- Reformulation expansive 4-6 lignes (concepts, filiations, contextes)
- Re-ranking avec minimum 8 chunks garantis
- Gestion des modèles GPT-5.x et o1 (max_completion_tokens)
- Prompts optimisés pour réponses longues (500-800 mots)

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 22:46:39 +01:00

1172 lines
35 KiB
HTML

{% extends "base.html" %}
{% block title %}Conversation RAG{% endblock %}
{% block content %}
<style>
/* Override wrapper max-width for full-screen chat */
.wrapper {
max-width: 100% !important;
padding-left: 80px !important; /* Space for hamburger button */
padding-right: 1.5rem !important;
}
@media (min-width: 992px) {
.wrapper {
padding-left: 80px !important;
padding-right: 2rem !important;
}
}
/* Remove header margin for full-width layout */
.site-header {
margin-left: 0 !important;
}
/* Chat-specific styles */
.chat-container {
display: grid;
grid-template-columns: 60% 40%;
gap: 1.5rem;
height: calc(100vh - 300px);
min-height: 600px;
width: 100%;
}
@media (max-width: 992px) {
.chat-container {
grid-template-columns: 1fr;
height: auto;
}
.context-sidebar {
order: -1;
max-height: 300px;
}
}
/* Chat area */
.chat-main {
display: flex;
flex-direction: column;
background-color: rgba(255, 255, 255, 0.06);
border-radius: 12px;
border: 1px solid rgba(125, 110, 88, 0.25);
overflow: hidden;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.75rem;
border-bottom: 1px solid rgba(125, 110, 88, 0.15);
background-color: var(--color-bg-secondary);
}
.chat-title {
font-family: var(--font-title);
font-size: 1.3rem;
font-weight: 600;
color: var(--color-text-strong);
display: flex;
align-items: center;
gap: 0.5rem;
}
.model-selector-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
}
.model-selector-label {
font-family: var(--font-title);
font-size: 0.85rem;
font-weight: 500;
color: var(--color-accent-alt);
text-transform: uppercase;
letter-spacing: 0.05em;
}
#model-selector {
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid rgba(125, 110, 88, 0.3);
background-color: #fff;
font-family: var(--font-body);
font-size: 0.9rem;
color: var(--color-text-main);
cursor: pointer;
min-width: 200px;
}
/* Messages area */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.message {
display: flex;
flex-direction: column;
max-width: 85%;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-user {
align-self: flex-end;
}
.message-assistant {
align-self: flex-start;
}
.message-content {
padding: 1rem 1.25rem;
border-radius: 12px;
font-family: var(--font-body);
font-size: 1rem;
line-height: 1.6;
}
.message-user .message-content {
background-color: #e3f2fd;
color: var(--color-text-main);
border: 1px solid rgba(33, 150, 243, 0.2);
}
.message-assistant .message-content {
background-color: rgba(255, 255, 255, 0.5);
color: var(--color-text-main);
border: 1px solid rgba(125, 110, 88, 0.2);
}
.message-label {
font-family: var(--font-title);
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-accent-alt);
margin-bottom: 0.4rem;
padding-left: 0.25rem;
}
/* Loading indicator */
.typing-indicator {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.75rem 1rem;
}
.typing-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--color-accent);
animation: typing 1.4s infinite;
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
opacity: 0.3;
transform: scale(0.8);
}
30% {
opacity: 1;
transform: scale(1);
}
}
/* Input area */
.chat-input-area {
padding: 1.25rem 1.75rem;
border-top: 1px solid rgba(125, 110, 88, 0.15);
background-color: var(--color-bg-secondary);
}
.input-wrapper {
display: flex;
gap: 0.75rem;
align-items: flex-end;
}
#chat-input {
flex: 1;
padding: 0.85rem 1rem;
border-radius: 8px;
border: 1px solid rgba(125, 110, 88, 0.3);
background-color: #fff;
font-family: var(--font-body);
font-size: 1rem;
color: var(--color-text-main);
resize: vertical;
min-height: 50px;
max-height: 150px;
}
#chat-input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px rgba(125, 110, 88, 0.1);
}
#send-btn {
padding: 0.85rem 1.75rem;
white-space: nowrap;
}
#send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.character-count {
font-family: var(--font-body);
font-size: 0.75rem;
color: var(--color-accent);
text-align: right;
margin-top: 0.25rem;
}
/* Context sidebar */
.context-sidebar {
display: flex;
flex-direction: column;
background-color: rgba(255, 255, 255, 0.06);
border-radius: 12px;
border: 1px solid rgba(125, 110, 88, 0.25);
overflow: hidden;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid rgba(125, 110, 88, 0.15);
background-color: var(--color-bg-secondary);
}
.sidebar-title {
font-family: var(--font-title);
font-size: 1rem;
font-weight: 600;
color: var(--color-text-strong);
display: flex;
align-items: center;
gap: 0.5rem;
}
.collapse-btn {
background: none;
border: none;
color: var(--color-accent);
cursor: pointer;
font-size: 1.2rem;
padding: 0.25rem;
transition: transform 0.3s ease;
}
.collapse-btn:hover {
transform: scale(1.1);
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.sidebar-empty {
text-align: center;
padding: 2rem 1rem;
color: var(--color-accent-alt);
font-family: var(--font-body);
font-size: 0.9rem;
font-style: italic;
}
.context-chunk {
background-color: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 0;
margin-bottom: 0.75rem;
border: 1px solid rgba(125, 110, 88, 0.15);
border-left: 3px solid var(--color-accent-alt);
transition: box-shadow 0.2s ease;
}
.context-chunk:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.chunk-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 1rem;
cursor: pointer;
user-select: none;
}
.chunk-header:hover {
background-color: rgba(125, 110, 88, 0.03);
}
.chunk-badges {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
flex: 1;
}
.chunk-chevron {
font-size: 1rem;
color: var(--color-accent);
transition: transform 0.3s ease;
flex-shrink: 0;
margin-left: 0.5rem;
}
.chunk-chevron.expanded {
transform: rotate(180deg);
}
.chunk-body {
padding: 0 1rem 1rem 1rem;
display: block;
}
.chunk-text {
font-family: var(--font-body);
font-size: 0.85rem;
line-height: 1.5;
color: var(--color-text-main);
margin-bottom: 0.5rem;
white-space: pre-wrap;
}
.chunk-text.collapsed {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.chunk-meta {
font-family: var(--font-body);
font-size: 0.75rem;
color: var(--color-accent);
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid rgba(125, 110, 88, 0.1);
}
/* Reformulation notice */
.reformulation-notice {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1.25rem;
margin: 0.75rem auto;
max-width: 85%;
background-color: rgba(125, 110, 88, 0.08);
border-left: 3px solid var(--color-accent);
border-radius: 8px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reformulation-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.reformulation-icon {
font-size: 1.25rem;
flex-shrink: 0;
}
.reformulation-title {
font-family: var(--font-title);
font-size: 0.95rem;
font-weight: 600;
color: var(--color-accent);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.reformulation-comparison {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.reformulation-item {
background-color: rgba(255, 255, 255, 0.5);
border-radius: 6px;
padding: 0.85rem;
border: 1px solid rgba(125, 110, 88, 0.15);
}
.reformulation-item.selected {
border: 2px solid var(--color-accent);
background-color: rgba(125, 110, 88, 0.05);
}
.reformulation-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.reformulation-label {
font-family: var(--font-title);
font-size: 0.75rem;
font-weight: 500;
color: var(--color-accent-alt);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.reformulation-badge {
font-size: 0.7rem;
font-weight: 600;
color: #fff;
background-color: var(--color-accent);
padding: 0.2rem 0.5rem;
border-radius: 4px;
text-transform: uppercase;
}
.reformulation-text {
font-family: var(--font-body);
font-size: 0.9rem;
color: var(--color-text-main);
line-height: 1.5;
}
.reformulation-item.original .reformulation-text {
font-style: normal;
}
.reformulation-item.reformulated .reformulation-text {
font-style: italic;
}
.reformulation-actions {
display: flex;
gap: 0.75rem;
margin-top: 0.75rem;
}
.reformulation-btn {
flex: 1;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-family: var(--font-title);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.reformulation-btn.btn-original {
background-color: rgba(125, 110, 88, 0.15);
color: var(--color-text-strong);
border: 1px solid rgba(125, 110, 88, 0.3);
}
.reformulation-btn.btn-original:hover {
background-color: rgba(125, 110, 88, 0.25);
}
.reformulation-btn.btn-reformulated {
background-color: var(--color-accent);
color: white;
}
.reformulation-btn.btn-reformulated:hover {
background-color: #6d5f4c;
}
.reformulation-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Empty state */
.chat-empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1.5rem;
color: var(--color-accent-alt);
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.3;
}
.empty-text {
font-family: var(--font-title);
font-size: 1.1rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.empty-hint {
font-family: var(--font-body);
font-size: 0.9rem;
opacity: 0.7;
}
/* Scrollbar styling */
.chat-messages::-webkit-scrollbar,
.sidebar-content::-webkit-scrollbar {
width: 8px;
}
.chat-messages::-webkit-scrollbar-track,
.sidebar-content::-webkit-scrollbar-track {
background: rgba(125, 110, 88, 0.05);
border-radius: 4px;
}
.chat-messages::-webkit-scrollbar-thumb,
.sidebar-content::-webkit-scrollbar-thumb {
background: rgba(125, 110, 88, 0.3);
border-radius: 4px;
}
.chat-messages::-webkit-scrollbar-thumb:hover,
.sidebar-content::-webkit-scrollbar-thumb:hover {
background: rgba(125, 110, 88, 0.5);
}
</style>
<div class="chat-container">
<!-- Main chat area -->
<div class="chat-main">
<!-- Header with model selector -->
<div class="chat-header">
<div class="chat-title">
<span>💬</span>
<span>Conversation RAG</span>
</div>
<div class="model-selector-wrapper">
<label class="model-selector-label" for="model-selector">Modèle</label>
<select id="model-selector" class="form-control">
<optgroup label="OpenAI API (Recommandé)">
<option value="openai:gpt-5.2" selected>ChatGPT 5.2 - Dernier modèle ⭐</option>
<option value="openai:gpt-4o">GPT-4o</option>
<option value="openai:gpt-4o-mini">GPT-4o Mini</option>
<option value="openai:o1-preview">o1-preview (Raisonnement)</option>
</optgroup>
<optgroup label="Anthropic API">
<option value="anthropic:claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
<option value="anthropic:claude-opus-4-5-20251101">Claude Opus 4.5</option>
</optgroup>
<optgroup label="Mistral API">
<option value="mistral:mistral-small-latest">Mistral Small</option>
<option value="mistral:mistral-large-latest">Mistral Large</option>
</optgroup>
<optgroup label="Ollama (Local - Gratuit)">
<option value="ollama:qwen2.5:7b">Qwen 2.5 7B</option>
<option value="ollama:deepseek-r1:14b">DeepSeek R1 14B</option>
</optgroup>
</select>
</div>
</div>
<!-- Messages area -->
<div class="chat-messages" id="chat-messages">
<!-- Empty state -->
<div class="chat-empty-state" id="empty-state">
<div class="empty-icon">💭</div>
<div class="empty-text">Bienvenue dans la Conversation RAG</div>
<div class="empty-hint">Posez une question pour commencer la recherche sémantique</div>
</div>
</div>
<!-- Input area -->
<div class="chat-input-area">
<div class="input-wrapper">
<textarea
id="chat-input"
placeholder="Posez votre question sur la philosophie..."
rows="2"
></textarea>
<button id="send-btn" class="btn btn-primary">
Envoyer
</button>
</div>
<div class="character-count" id="char-count">0 / 2000</div>
</div>
</div>
<!-- Context sidebar -->
<div class="context-sidebar" id="context-sidebar">
<div class="sidebar-header">
<div class="sidebar-title">
<span>📚</span>
<span>Contexte RAG</span>
</div>
<button class="collapse-btn" id="collapse-btn" title="Réduire"></button>
</div>
<div class="sidebar-content" id="sidebar-content">
<div class="sidebar-empty">
Posez une question pour voir le contexte utilisé
</div>
</div>
</div>
</div>
<!-- Markdown and syntax highlighting libraries -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script>
// DOM elements
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const charCount = document.getElementById('char-count');
const emptyState = document.getElementById('empty-state');
const modelSelector = document.getElementById('model-selector');
const sidebarContent = document.getElementById('sidebar-content');
const collapseBtn = document.getElementById('collapse-btn');
const contextSidebar = document.getElementById('context-sidebar');
// State
let isGenerating = false;
let currentEventSource = null;
// Configure marked for markdown rendering
marked.setOptions({
breaks: true,
gfm: true,
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
}
});
// Character counter
chatInput.addEventListener('input', () => {
const length = chatInput.value.length;
charCount.textContent = `${length} / 2000`;
sendBtn.disabled = length === 0 || length > 2000 || isGenerating;
});
// Auto-resize textarea
chatInput.addEventListener('input', () => {
chatInput.style.height = 'auto';
chatInput.style.height = Math.min(chatInput.scrollHeight, 150) + 'px';
});
// Send message on Enter (without Shift)
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Send button click
sendBtn.addEventListener('click', sendMessage);
// Collapse sidebar
collapseBtn.addEventListener('click', () => {
const isCollapsed = sidebarContent.style.display === 'none';
sidebarContent.style.display = isCollapsed ? 'block' : 'none';
collapseBtn.textContent = isCollapsed ? '▼' : '▲';
collapseBtn.title = isCollapsed ? 'Réduire' : 'Développer';
});
async function sendMessage() {
const question = chatInput.value.trim();
if (!question || isGenerating) return;
// Parse model selector (format: "provider:model")
const selectedValue = modelSelector.value;
const [provider, model] = selectedValue.split(':');
if (!provider || !model) {
addErrorMessage('Erreur: Format de modèle invalide');
return;
}
// Hide empty state
if (emptyState) {
emptyState.remove();
}
// Add user message to chat
addMessage('user', question);
// Clear input
chatInput.value = '';
chatInput.style.height = 'auto';
charCount.textContent = '0 / 2000';
// Disable send button
isGenerating = true;
sendBtn.disabled = true;
sendBtn.textContent = 'Reformulation...';
// Show typing indicator
const typingId = addTypingIndicator();
try {
// Step 1: POST /chat/reformulate to get reformulated question
const reformulateResponse = await fetch('/chat/reformulate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question: question,
provider: provider,
model: model
})
});
if (!reformulateResponse.ok) {
const error = await reformulateResponse.json();
throw new Error(error.error || 'Erreur de reformulation');
}
const reformulateData = await reformulateResponse.json();
const original = reformulateData.original;
const reformulated = reformulateData.reformulated;
// Remove typing indicator
removeTypingIndicator(typingId);
// Step 2: Display reformulation with choice buttons
displayReformulationChoice(original, reformulated, provider, model);
// Re-enable send button
isGenerating = false;
sendBtn.disabled = false;
sendBtn.textContent = 'Envoyer';
} catch (error) {
console.error('Error:', error);
removeTypingIndicator(typingId);
addErrorMessage(`Erreur: ${error.message}`);
isGenerating = false;
sendBtn.disabled = false;
sendBtn.textContent = 'Envoyer';
}
}
function displayReformulationChoice(original, reformulated, provider, model) {
// Create comparison with choice buttons
const reformulationDiv = document.createElement('div');
reformulationDiv.className = 'reformulation-notice';
reformulationDiv.innerHTML = `
<div class="reformulation-header">
<div class="reformulation-icon">🔄</div>
<div class="reformulation-title">Reformulation de la question</div>
</div>
<div class="reformulation-comparison">
<div class="reformulation-item original">
<div class="reformulation-item-header">
<div class="reformulation-label">Question Originale</div>
</div>
<div class="reformulation-text">${original}</div>
</div>
<div class="reformulation-item reformulated">
<div class="reformulation-item-header">
<div class="reformulation-label">Version Reformulée</div>
</div>
<div class="reformulation-text">${reformulated}</div>
</div>
</div>
<div class="reformulation-actions">
<button class="reformulation-btn btn-original" data-question="${original.replace(/"/g, '&quot;')}">
Utiliser l'originale
</button>
<button class="reformulation-btn btn-reformulated" data-question="${reformulated.replace(/"/g, '&quot;')}">
Utiliser la reformulation (Recommandé)
</button>
</div>
`;
chatMessages.appendChild(reformulationDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
// Add click handlers to buttons
const buttons = reformulationDiv.querySelectorAll('.reformulation-btn');
buttons.forEach(btn => {
btn.addEventListener('click', async () => {
const chosenQuestion = btn.getAttribute('data-question');
const isReformulated = btn.classList.contains('btn-reformulated');
// Disable both buttons
buttons.forEach(b => b.disabled = true);
// Mark chosen version
if (isReformulated) {
reformulationDiv.querySelector('.reformulation-item.reformulated').classList.add('selected');
reformulationDiv.querySelector('.reformulation-item.reformulated .reformulation-item-header').innerHTML += `
<div class="reformulation-badge">✓ Utilisée</div>
`;
} else {
reformulationDiv.querySelector('.reformulation-item.original').classList.add('selected');
reformulationDiv.querySelector('.reformulation-item.original .reformulation-item-header').innerHTML += `
<div class="reformulation-badge">✓ Utilisée</div>
`;
}
// Remove action buttons
reformulationDiv.querySelector('.reformulation-actions').remove();
// Start RAG search with chosen question
await startRAGSearch(chosenQuestion, provider, model);
});
});
}
async function startRAGSearch(question, provider, model) {
// Disable send button
isGenerating = true;
sendBtn.disabled = true;
sendBtn.textContent = 'Génération...';
// Show typing indicator
const typingId = addTypingIndicator();
try {
// POST /chat/send with chosen question
const response = await fetch('/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question: question,
provider: provider,
model: model,
limit: 5,
use_reformulation: false // Reformulation already done
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Erreur HTTP');
}
const data = await response.json();
const sessionId = data.session_id;
// SSE stream
startSSEStream(sessionId, typingId);
} catch (error) {
console.error('Error:', error);
removeTypingIndicator(typingId);
addErrorMessage(`Erreur: ${error.message}`);
isGenerating = false;
sendBtn.disabled = false;
sendBtn.textContent = 'Envoyer';
}
}
function startSSEStream(sessionId, typingId) {
// Close previous EventSource if exists
if (currentEventSource) {
currentEventSource.close();
}
const eventSource = new EventSource(`/chat/stream/${sessionId}`);
currentEventSource = eventSource;
let assistantMessageDiv = null;
let assistantContentDiv = null;
let accumulatedText = '';
eventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'context') {
// Display RAG context in sidebar
displayContext(data.chunks);
}
else if (data.type === 'token') {
// Remove typing indicator on first token
if (typingId && document.getElementById(typingId)) {
removeTypingIndicator(typingId);
}
// Create assistant message on first token
if (!assistantMessageDiv) {
const result = createAssistantMessage();
assistantMessageDiv = result.messageDiv;
assistantContentDiv = result.contentDiv;
}
// Accumulate text
accumulatedText += data.content;
// Render markdown incrementally
assistantContentDiv.innerHTML = marked.parse(accumulatedText);
// Apply syntax highlighting to code blocks
assistantContentDiv.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
// Scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
}
else if (data.type === 'complete') {
// Generation complete
eventSource.close();
currentEventSource = null;
isGenerating = false;
sendBtn.disabled = false;
sendBtn.textContent = 'Envoyer';
}
else if (data.type === 'error') {
// Error occurred
removeTypingIndicator(typingId);
addErrorMessage(`Erreur: ${data.message}`);
eventSource.close();
currentEventSource = null;
isGenerating = false;
sendBtn.disabled = false;
sendBtn.textContent = 'Envoyer';
}
} catch (e) {
console.error('Parse error:', e);
}
};
eventSource.onerror = function(error) {
console.error('SSE error:', error);
removeTypingIndicator(typingId);
addErrorMessage('Erreur de connexion au serveur');
eventSource.close();
currentEventSource = null;
isGenerating = false;
sendBtn.disabled = false;
sendBtn.textContent = 'Envoyer';
};
}
function addMessage(role, content) {
const messageDiv = document.createElement('div');
messageDiv.className = `message message-${role}`;
const label = document.createElement('div');
label.className = 'message-label';
label.textContent = role === 'user' ? 'Vous' : 'Assistant';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
messageDiv.appendChild(label);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function createAssistantMessage() {
const messageDiv = document.createElement('div');
messageDiv.className = 'message message-assistant';
const label = document.createElement('div');
label.className = 'message-label';
label.textContent = 'Assistant';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
messageDiv.appendChild(label);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
return { messageDiv, contentDiv };
}
function addErrorMessage(message) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message message-assistant';
const label = document.createElement('div');
label.className = 'message-label';
label.textContent = 'Erreur';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.style.color = '#f44336';
contentDiv.style.borderColor = '#f44336';
contentDiv.textContent = message;
messageDiv.appendChild(label);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function addTypingIndicator() {
const messageDiv = document.createElement('div');
messageDiv.className = 'message message-assistant';
messageDiv.id = 'typing-indicator';
const label = document.createElement('div');
label.className = 'message-label';
label.textContent = 'Assistant';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
const typingDiv = document.createElement('div');
typingDiv.className = 'typing-indicator';
typingDiv.innerHTML = '<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>';
contentDiv.appendChild(typingDiv);
messageDiv.appendChild(label);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return 'typing-indicator';
}
function removeTypingIndicator(id) {
const indicator = document.getElementById(id);
if (indicator) {
indicator.remove();
}
}
function displayContext(chunks) {
sidebarContent.innerHTML = '';
if (!chunks || chunks.length === 0) {
sidebarContent.innerHTML = '<div class="sidebar-empty">Aucun contexte trouvé</div>';
return;
}
chunks.forEach((chunk, index) => {
const chunkDiv = document.createElement('div');
chunkDiv.className = 'context-chunk';
// Header (clickable)
const headerDiv = document.createElement('div');
headerDiv.className = 'chunk-header';
const badgesDiv = document.createElement('div');
badgesDiv.className = 'chunk-badges';
badgesDiv.innerHTML = `
<span class="badge badge-author">${chunk.author || 'Auteur inconnu'}</span>
<span class="badge badge-work">${chunk.work || 'Œuvre inconnue'}</span>
<span class="badge badge-similarity">⚡ ${chunk.similarity || 0}%</span>
`;
const chevron = document.createElement('span');
chevron.className = 'chunk-chevron';
chevron.textContent = '►';
headerDiv.appendChild(badgesDiv);
headerDiv.appendChild(chevron);
// Body (expandable)
const bodyDiv = document.createElement('div');
bodyDiv.className = 'chunk-body';
const textDiv = document.createElement('div');
textDiv.className = 'chunk-text collapsed'; // Collapsed by default (3 lines)
textDiv.textContent = chunk.text || '';
const metaDiv = document.createElement('div');
metaDiv.className = 'chunk-meta';
metaDiv.textContent = chunk.section || '';
bodyDiv.appendChild(textDiv);
bodyDiv.appendChild(metaDiv);
// Click handler for expand/collapse
headerDiv.addEventListener('click', () => {
const isExpanded = textDiv.classList.contains('collapsed');
if (isExpanded) {
// Expand - show full text
textDiv.classList.remove('collapsed');
chevron.classList.add('expanded');
chevron.textContent = '▼';
} else {
// Collapse - show only 3 lines
textDiv.classList.add('collapsed');
chevron.classList.remove('expanded');
chevron.textContent = '►';
}
});
chunkDiv.appendChild(headerDiv);
chunkDiv.appendChild(bodyDiv);
sidebarContent.appendChild(chunkDiv);
});
}
// Initialize
sendBtn.disabled = true;
</script>
{% endblock %}