feat: Implement works filter UI for chat page (LRP-139, 140, 141, 143)
- Add works filter section HTML above Context RAG sidebar - Add CSS styles for works filter with checkboxes, badges, and collapse - Implement JavaScript for loading works from /api/get-works - Add localStorage persistence for selected works - Integrate selected_works parameter with /chat/send API call - Add Tout/Aucun buttons for quick selection - Add collapsible section with chevron toggle - Responsive design for mobile screens 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -645,6 +645,187 @@
|
||||
height: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Sidebar panel - container for both works filter and context */
|
||||
.sidebar-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Works filter section */
|
||||
.works-filter-section {
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(125, 110, 88, 0.25);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.works-filter-content {
|
||||
padding: 0.75rem 1rem 1rem 1rem;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.works-filter-content.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.works-filter-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-mini {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(125, 110, 88, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
color: var(--color-text-main);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-mini:hover {
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.works-count-badge {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.works-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.work-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(125, 110, 88, 0.15);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.work-item:hover {
|
||||
background-color: rgba(125, 110, 88, 0.08);
|
||||
border-color: rgba(125, 110, 88, 0.25);
|
||||
}
|
||||
|
||||
.work-item.selected {
|
||||
background-color: rgba(125, 110, 88, 0.1);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.work-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.work-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.work-title {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-strong);
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.work-author {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-accent-alt);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.work-count {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
background-color: rgba(125, 110, 88, 0.1);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Works filter scrollbar */
|
||||
.works-filter-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.works-filter-content::-webkit-scrollbar-track {
|
||||
background: rgba(125, 110, 88, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.works-filter-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(125, 110, 88, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.works-filter-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(125, 110, 88, 0.5);
|
||||
}
|
||||
|
||||
/* Update context sidebar to fill remaining space */
|
||||
.sidebar-panel .context-sidebar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Responsive - Mobile */
|
||||
@media (max-width: 992px) {
|
||||
.sidebar-panel {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.works-filter-section {
|
||||
order: -1;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.works-filter-content {
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.sidebar-panel .context-sidebar {
|
||||
order: -1;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chat-container">
|
||||
@@ -707,6 +888,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Works filter sidebar -->
|
||||
<div class="sidebar-panel" id="sidebar-panel">
|
||||
<!-- Works filter section -->
|
||||
<div class="works-filter-section" id="works-filter-section">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-title">
|
||||
<span>📚</span>
|
||||
<span>Filtrer par œuvres</span>
|
||||
<span class="works-count-badge" id="works-count-badge">0/0</span>
|
||||
</div>
|
||||
<button class="collapse-btn" id="works-collapse-btn" title="Réduire">▼</button>
|
||||
</div>
|
||||
<div class="works-filter-content" id="works-filter-content">
|
||||
<div class="works-filter-actions">
|
||||
<button class="btn-mini" id="select-all-works">Tout</button>
|
||||
<button class="btn-mini" id="select-none-works">Aucun</button>
|
||||
</div>
|
||||
<div class="works-list" id="works-list">
|
||||
<div class="sidebar-empty">Chargement des œuvres...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context sidebar -->
|
||||
<div class="context-sidebar" id="context-sidebar">
|
||||
<div class="sidebar-header">
|
||||
@@ -722,6 +926,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markdown and syntax highlighting libraries -->
|
||||
@@ -741,10 +946,159 @@
|
||||
const collapseBtn = document.getElementById('collapse-btn');
|
||||
const contextSidebar = document.getElementById('context-sidebar');
|
||||
|
||||
// Works filter DOM elements
|
||||
const worksFilterContent = document.getElementById('works-filter-content');
|
||||
const worksCollapseBtn = document.getElementById('works-collapse-btn');
|
||||
const worksCountBadge = document.getElementById('works-count-badge');
|
||||
const worksList = document.getElementById('works-list');
|
||||
const selectAllWorksBtn = document.getElementById('select-all-works');
|
||||
const selectNoneWorksBtn = document.getElementById('select-none-works');
|
||||
|
||||
// State
|
||||
let isGenerating = false;
|
||||
let currentEventSource = null;
|
||||
|
||||
// Works filter state
|
||||
let availableWorks = [];
|
||||
let selectedWorks = [];
|
||||
|
||||
// ========== WORKS FILTER FUNCTIONS ==========
|
||||
|
||||
async function loadAvailableWorks() {
|
||||
try {
|
||||
const response = await fetch('/api/get-works');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load works');
|
||||
}
|
||||
availableWorks = await response.json();
|
||||
|
||||
// Load saved selection from localStorage or select all by default
|
||||
const savedSelection = localStorage.getItem('selectedWorks');
|
||||
if (savedSelection) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedSelection);
|
||||
// Filter to only keep works that still exist
|
||||
const existingTitles = availableWorks.map(w => w.title);
|
||||
selectedWorks = parsed.filter(title => existingTitles.includes(title));
|
||||
} catch (e) {
|
||||
console.error('Error parsing saved selection:', e);
|
||||
selectedWorks = availableWorks.map(w => w.title);
|
||||
}
|
||||
} else {
|
||||
// Default: all works selected
|
||||
selectedWorks = availableWorks.map(w => w.title);
|
||||
}
|
||||
|
||||
// If savedSelection resulted in empty (all works removed), select all
|
||||
if (selectedWorks.length === 0 && availableWorks.length > 0) {
|
||||
selectedWorks = availableWorks.map(w => w.title);
|
||||
}
|
||||
|
||||
renderWorksList();
|
||||
updateWorksCount();
|
||||
saveSelectedWorksToStorage();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading works:', error);
|
||||
worksList.innerHTML = '<div class="sidebar-empty">Erreur de chargement des œuvres</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderWorksList() {
|
||||
worksList.innerHTML = '';
|
||||
|
||||
if (availableWorks.length === 0) {
|
||||
worksList.innerHTML = '<div class="sidebar-empty">Aucune œuvre disponible</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
availableWorks.forEach(work => {
|
||||
const isSelected = selectedWorks.includes(work.title);
|
||||
|
||||
const workItem = document.createElement('div');
|
||||
workItem.className = `work-item${isSelected ? ' selected' : ''}`;
|
||||
workItem.innerHTML = `
|
||||
<input type="checkbox" class="work-checkbox" ${isSelected ? 'checked' : ''} data-title="${work.title.replace(/"/g, '"')}">
|
||||
<div class="work-info">
|
||||
<div class="work-title" title="${work.title.replace(/"/g, '"')}">${work.title}</div>
|
||||
<div class="work-author">${work.author}</div>
|
||||
</div>
|
||||
<div class="work-count">${work.chunks_count} passages</div>
|
||||
`;
|
||||
|
||||
// Click on work item toggles checkbox
|
||||
workItem.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('work-checkbox')) return; // Let checkbox handle itself
|
||||
const checkbox = workItem.querySelector('.work-checkbox');
|
||||
checkbox.checked = !checkbox.checked;
|
||||
toggleWorkSelection(work.title, checkbox.checked);
|
||||
workItem.classList.toggle('selected', checkbox.checked);
|
||||
});
|
||||
|
||||
// Checkbox change event
|
||||
const checkbox = workItem.querySelector('.work-checkbox');
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
toggleWorkSelection(work.title, e.target.checked);
|
||||
workItem.classList.toggle('selected', e.target.checked);
|
||||
});
|
||||
|
||||
worksList.appendChild(workItem);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWorkSelection(title, isSelected) {
|
||||
if (isSelected) {
|
||||
if (!selectedWorks.includes(title)) {
|
||||
selectedWorks.push(title);
|
||||
}
|
||||
} else {
|
||||
selectedWorks = selectedWorks.filter(t => t !== title);
|
||||
}
|
||||
updateWorksCount();
|
||||
saveSelectedWorksToStorage();
|
||||
}
|
||||
|
||||
function updateWorksCount() {
|
||||
worksCountBadge.textContent = `${selectedWorks.length}/${availableWorks.length}`;
|
||||
}
|
||||
|
||||
function saveSelectedWorksToStorage() {
|
||||
try {
|
||||
localStorage.setItem('selectedWorks', JSON.stringify(selectedWorks));
|
||||
} catch (e) {
|
||||
console.error('Error saving to localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Select All button
|
||||
selectAllWorksBtn.addEventListener('click', () => {
|
||||
selectedWorks = availableWorks.map(w => w.title);
|
||||
renderWorksList();
|
||||
updateWorksCount();
|
||||
saveSelectedWorksToStorage();
|
||||
});
|
||||
|
||||
// Select None button
|
||||
selectNoneWorksBtn.addEventListener('click', () => {
|
||||
selectedWorks = [];
|
||||
renderWorksList();
|
||||
updateWorksCount();
|
||||
saveSelectedWorksToStorage();
|
||||
});
|
||||
|
||||
// Works filter collapse button
|
||||
worksCollapseBtn.addEventListener('click', () => {
|
||||
const isCollapsed = worksFilterContent.classList.contains('collapsed');
|
||||
worksFilterContent.classList.toggle('collapsed');
|
||||
worksCollapseBtn.textContent = isCollapsed ? '▼' : '▲';
|
||||
worksCollapseBtn.title = isCollapsed ? 'Réduire' : 'Développer';
|
||||
});
|
||||
|
||||
// Load works on page load
|
||||
loadAvailableWorks();
|
||||
|
||||
// ========== END WORKS FILTER FUNCTIONS ==========
|
||||
|
||||
// Configure marked for markdown rendering
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
@@ -950,7 +1304,7 @@
|
||||
const typingId = addTypingIndicator();
|
||||
|
||||
try {
|
||||
// POST /chat/send with chosen question
|
||||
// POST /chat/send with chosen question and selected works filter
|
||||
const response = await fetch('/chat/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -959,7 +1313,8 @@
|
||||
provider: provider,
|
||||
model: model,
|
||||
limit: 5,
|
||||
use_reformulation: false // Reformulation already done
|
||||
use_reformulation: false, // Reformulation already done
|
||||
selected_works: selectedWorks // Filter by selected works
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user