Daniel Grozdanovic bcd0f8a227
Some checks failed
CI - SharePoint Plugin with SonarQube / Test and SonarQube Analysis (push) Has been cancelled
Initial commit: SharePoint connector and ToothFairyAI integration
2026-02-22 17:58:45 +02:00

1418 lines
61 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SharePoint Connector</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<!-- Setup Form (shown if not configured) -->
<div id="setupFormContainer" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--bg-color); z-index: 9999; overflow: auto;">
<div style="max-width: 600px; margin: 50px auto; padding: 20px;">
<div id="setupForm" class="section">
<h2>Setup Azure Credentials</h2>
<p>Enter your Azure App Registration details to connect to SharePoint:</p>
<form onsubmit="saveCredentials(event)">
<div class="form-group">
<label for="clientId">Client ID</label>
<input type="text" id="clientId" required placeholder="12345678-1234-1234-1234-123456789abc">
<small>From Azure Portal → App registrations → Application (client) ID</small>
</div>
<div class="form-group">
<label for="clientSecret">Client Secret</label>
<input type="password" id="clientSecret" required placeholder="Your client secret value">
<small>From Azure Portal → Certificates & secrets</small>
</div>
<div class="form-group">
<label for="tenantId">Tenant ID</label>
<input type="text" id="tenantId" value="common" required>
<small>Use "common" for multi-tenant or your specific tenant ID</small>
</div>
<button type="submit" class="btn btn-primary">Save & Connect</button>
</form>
<div class="help-section">
<h3>Need help?</h3>
<ol>
<li>Go to <a href="https://portal.azure.com" target="_blank">Azure Portal</a></li>
<li>Navigate to Azure Active Directory → App registrations</li>
<li>Create or select your app</li>
<li>Copy the Application (client) ID and Tenant ID</li>
<li>Create a client secret in Certificates & secrets</li>
<li>Add API permissions: User.Read, Sites.Read.All, Files.Read.All, offline_access</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main Layout (shown after connection) -->
<div id="mainLayout" class="container" style="display: none;">
<!-- Sidebar -->
<div class="sidebar" id="mainSidebar">
<div class="sidebar-header">
<div class="sidebar-header-content">
<h1>📁 SharePoint</h1>
<p>Document Explorer</p>
</div>
<button class="sidebar-collapse-btn" onclick="toggleSidebar()" title="Toggle Sidebar">
</button>
</div>
<div class="sidebar-content">
<!-- Sites List -->
<div class="sidebar-section sites-section">
<div class="sidebar-section-header" onclick="toggleSidebarSection('sites')">
<h3>SharePoint Sites</h3>
<span class="sidebar-section-toggle" id="sitesToggle"></span>
</div>
<div class="sidebar-section-content" id="sitesContent">
<div id="sidebarSites">
<div class="loading">Loading sites...</div>
</div>
</div>
</div>
</div>
<div class="sidebar-footer">
<div id="sidebarConnectionStatus" style="margin-bottom: 10px; padding: 8px; background: #f1f8e9; border-radius: 4px; border: 1px solid var(--success-color); text-align: center;">
<div style="font-size: 0.85rem; color: var(--text-color);">
<span style="color: var(--success-color); font-weight: 600;"></span> <span id="connectionStatusText">Checking...</span>
</div>
</div>
<div class="connection-actions">
<button onclick="disconnectSharePoint()" class="btn btn-danger">Disconnect</button>
<button onclick="resetCredentials()" class="btn btn-secondary">Reset Credentials</button>
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="main-content">
<div class="main-header">
<h2 id="mainTitle">💬 Multi-Document Chat</h2>
<p id="mainSubtitle">Ask questions across all your indexed documents using semantic search</p>
</div>
<div class="main-body">
<!-- Multi-Document Chat Section -->
<div id="multiChatSection" class="chat-pane-full">
<!-- Tag Filter -->
<div class="tag-filter-section">
<div class="tag-filter-header" onclick="toggleTagFilter()">
<label>🏷️ Filter by Tags</label>
<span class="tag-filter-toggle" id="tagFilterToggle"></span>
</div>
<div class="tag-filter-content" id="tagFilterContent">
<div class="form-group" style="margin-bottom: 0;">
<input type="text" id="multiChatTags" placeholder="e.g., AI, HR, SALES (comma-separated)">
<small>Leave empty to search all documents, or click a tag below to add it</small>
<div id="availableTags" class="available-tags-compact"></div>
</div>
</div>
</div>
<!-- Chat Messages (scrollable) -->
<div class="chat-container-scroll">
<div style="padding: 0 0 15px 0; display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0;">💬 Conversation</h3>
<div id="multiLlmStatus" class="llm-status">
<span class="status-indicator"></span>
<span class="status-text">Checking...</span>
</div>
</div>
<div id="multiChatMessages" class="chat-messages"></div>
</div>
<!-- Chat Input (fixed at bottom) -->
<div class="chat-input-fixed">
<div class="chat-input-container">
<textarea
id="multiChatInput"
placeholder="Ask a question across all indexed documents..."
rows="3"
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMultiChatMessage(); }"
></textarea>
<div class="chat-actions">
<button onclick="sendMultiChatMessage()" class="btn btn-primary" id="multiSendBtn">Send</button>
<button onclick="clearMultiChat()" class="btn btn-secondary">Clear</button>
</div>
</div>
</div>
</div>
<!-- Files Section (shown when browsing a site) -->
<div id="filesSection" style="display: none;">
<div style="margin-bottom: 20px;">
<button onclick="backToChat()" class="btn btn-secondary">← Back to Chat</button>
</div>
<div class="breadcrumb" id="breadcrumb"></div>
<div id="filesContainer" class="content-area">
<div class="loading">Loading files...</div>
</div>
</div>
<!-- File Content Section (shown when viewing a document) -->
<div id="contentSection" style="display: none;">
<div class="section-header">
<h2>File Content</h2>
<button onclick="backToFiles()" class="btn btn-secondary">← Back to Files</button>
</div>
<div class="file-info" id="fileInfo"></div>
<!-- Document Tags Section -->
<div class="tags-section">
<h4>📌 Document Tags</h4>
<div class="tags-container">
<div id="documentTags" class="tags-display">
<span class="tag-placeholder">No tags yet</span>
</div>
<button onclick="showTagEditor()" class="btn btn-sm">✏️ Edit Tags</button>
</div>
<div id="tagEditor" class="tag-editor" style="display: none;">
<input type="text" id="tagInput" placeholder="Enter tags separated by commas (e.g., HR, SALES, Q4-2024)" />
<div class="tag-editor-actions">
<button onclick="saveTags()" class="btn btn-primary btn-sm">Save Tags</button>
<button onclick="hideTagEditor()" class="btn btn-secondary btn-sm">Cancel</button>
</div>
</div>
</div>
<!-- Two column layout: File content + Chat -->
<div class="content-layout">
<!-- File content -->
<div class="content-pane">
<h3>Document</h3>
<div id="contentContainer" class="content-area">
<pre id="fileContent"></pre>
</div>
</div>
<!-- Chat pane -->
<div class="chat-pane">
<div class="chat-header">
<h3>💬 Ask about this document</h3>
<div id="llmStatus" class="llm-status">
<span class="status-indicator"></span>
<span class="status-text">Checking LLM...</span>
</div>
</div>
<div id="chatMessages" class="chat-messages"></div>
<div class="chat-input-container">
<textarea
id="chatInput"
placeholder="Ask a question about this document..."
rows="3"
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendChatMessage(); }"
></textarea>
<div class="chat-actions">
<button onclick="sendChatMessage()" class="btn btn-primary" id="sendBtn">Send</button>
<button onclick="clearChat()" class="btn btn-secondary">Clear Chat</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tag Selection Modal -->
<div id="tagSelectionModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>🏷️ Select Tags for Indexing</h3>
<button onclick="closeTagSelectionModal()" class="btn btn-sm btn-secondary">×</button>
</div>
<div class="modal-body">
<p id="tagSelectionSiteName" class="status-site"></p>
<p style="margin-bottom: 15px; color: #666;">Add tags to organize indexed documents (optional)</p>
<div class="form-group">
<label for="indexingTagInput">Tags</label>
<input type="text" id="indexingTagInput" placeholder="e.g., HR, SALES, Q4-2024" />
<small>Separate tags with commas</small>
</div>
<div class="modal-actions">
<button onclick="startIndexingWithTags()" class="btn btn-primary">Start Indexing</button>
<button onclick="closeTagSelectionModal()" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Indexing Progress Modal -->
<div id="indexingModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>📊 Indexing Progress</h3>
<button onclick="closeIndexingModal()" class="btn btn-sm btn-secondary">Close</button>
</div>
<div class="modal-body">
<div id="indexingStatus" class="indexing-status">
<p id="indexingSiteName" class="status-site"></p>
<div class="progress-bar">
<div id="indexingProgress" class="progress-fill" style="width: 0%"></div>
</div>
<p id="indexingStats" class="status-stats"></p>
<p id="indexingCurrentFile" class="status-current"></p>
</div>
<div class="modal-actions">
<button onclick="cancelIndexing()" class="btn btn-danger" id="cancelIndexBtn">Cancel Indexing</button>
</div>
</div>
</div>
</div>
</div>
<script>
let currentConnectionId = null;
let currentSiteId = null;
let currentPath = "";
let currentSiteName = "";
let currentFilePath = null;
let currentDocumentId = null;
let currentTags = [];
let isChatAvailable = false;
let currentIndexingJobId = null;
let indexingPollInterval = null;
let pendingIndexingSiteId = null;
let pendingIndexingSiteName = null;
let currentIndexingSiteId = null;
let indexedSites = new Set(); // Track which sites have been indexed
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
checkConfiguration();
});
// Toggle entire sidebar collapse
function toggleSidebar() {
const sidebar = document.getElementById('mainSidebar');
sidebar.classList.toggle('collapsed');
}
// Toggle sidebar section collapse
function toggleSidebarSection(section) {
const content = document.getElementById(section + 'Content');
const toggle = document.getElementById(section + 'Toggle');
if (content.classList.contains('collapsed')) {
content.classList.remove('collapsed');
toggle.classList.remove('collapsed');
toggle.textContent = '▼';
} else {
content.classList.add('collapsed');
toggle.classList.add('collapsed');
toggle.textContent = '▼';
}
}
// Toggle tag filter section
function toggleTagFilter() {
const content = document.getElementById('tagFilterContent');
const toggle = document.getElementById('tagFilterToggle');
if (content.classList.contains('collapsed')) {
content.classList.remove('collapsed');
toggle.classList.remove('collapsed');
toggle.textContent = '▼';
} else {
content.classList.add('collapsed');
toggle.classList.add('collapsed');
toggle.textContent = '▼';
}
}
// Check if credentials are configured
async function checkConfiguration() {
try {
const response = await fetch('/api/config/check');
const data = await response.json();
if (data.configured) {
checkConnection();
} else {
showSetupForm();
}
} catch (error) {
console.error('Error checking configuration:', error);
showSetupForm();
}
}
// Show setup form
function showSetupForm() {
document.getElementById('setupFormContainer').style.display = 'block';
document.getElementById('mainLayout').style.display = 'none';
}
// Save credentials
async function saveCredentials(event) {
event.preventDefault();
const clientId = document.getElementById('clientId').value;
const clientSecret = document.getElementById('clientSecret').value;
const tenantId = document.getElementById('tenantId').value;
try {
const response = await fetch('/api/config/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
tenant_id: tenantId
})
});
const data = await response.json();
if (data.success) {
document.getElementById('setupFormContainer').style.display = 'none';
checkConnection();
} else {
alert('Failed to save credentials: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('Error saving credentials:', error);
alert('Failed to save credentials');
}
}
// Reset credentials
async function resetCredentials() {
if (!confirm('Are you sure you want to reset your Azure credentials? You will need to reconnect.')) {
return;
}
try {
await fetch('/api/config/reset', {method: 'POST'});
window.location.reload();
} catch (error) {
console.error('Error resetting credentials:', error);
alert('Failed to reset credentials');
}
}
// Check connection status
async function checkConnection() {
try {
const response = await fetch('/api/sharepoint/connections');
const data = await response.json();
if (data.connections && data.connections.length > 0) {
currentConnectionId = data.connections[0].id;
showConnected(data.connections[0]);
loadSites();
} else {
showNotConnected();
}
} catch (error) {
console.error('Error checking connection:', error);
showError('Failed to check connection status');
}
}
// Show connected status
function showConnected(connection) {
// Show main layout
document.getElementById('mainLayout').style.display = 'flex';
document.getElementById('setupFormContainer').style.display = 'none';
// Update connection status in footer
const statusText = document.getElementById('connectionStatusText');
const statusContainer = document.getElementById('sidebarConnectionStatus');
statusContainer.style.background = '#f1f8e9';
statusContainer.style.borderColor = 'var(--success-color)';
statusText.innerHTML = `<span style="color: var(--success-color); font-weight: 600;"></span> Connected`;
// Load available tags for multi-document chat
loadAvailableTags();
}
// Show not connected status
function showNotConnected() {
// Show main layout with connect button
document.getElementById('mainLayout').style.display = 'flex';
document.getElementById('setupFormContainer').style.display = 'none';
// Update connection status in footer
const statusText = document.getElementById('connectionStatusText');
const statusContainer = document.getElementById('sidebarConnectionStatus');
statusContainer.style.background = '#fef0f0';
statusContainer.style.borderColor = 'var(--danger-color)';
statusText.innerHTML = `<span style="color: var(--danger-color); font-weight: 600;"></span> Not Connected`;
// Add connect button to sites section
const sitesContainer = document.getElementById('sidebarSites');
sitesContainer.innerHTML = `
<div style="padding: 20px; text-align: center;">
<p style="margin-bottom: 15px; font-size: 0.9rem; color: var(--text-secondary);">Connect to SharePoint to access your sites</p>
<button onclick="connectSharePoint()" class="btn btn-primary" style="width: 100%;">Connect SharePoint</button>
</div>
`;
}
// Connect to SharePoint
function connectSharePoint() {
window.location.href = '/sharepoint/connect';
}
// Load sites
async function loadSites() {
const sitesContainer = document.getElementById('sidebarSites');
sitesContainer.innerHTML = '<div class="loading" style="text-align: center; padding: 10px; font-size: 0.85rem;">Loading...</div>';
try {
// Fetch sites and indexed site IDs in parallel
const [sitesResponse, indexedSitesResponse] = await Promise.all([
fetch(`/api/sharepoint/${currentConnectionId}/sites`),
fetch('/api/documents/indexed-sites')
]);
const sitesData = await sitesResponse.json();
const indexedSitesData = await indexedSitesResponse.json();
if (sitesData.error) {
sitesContainer.innerHTML = `<div style="padding: 10px; text-align: center; font-size: 0.85rem; color: var(--danger-color);">Error loading sites</div>`;
return;
}
if (!sitesData.sites || sitesData.sites.length === 0) {
sitesContainer.innerHTML = '<div style="padding: 10px; text-align: center; font-size: 0.85rem; color: var(--text-secondary);">No sites found</div>';
return;
}
// Get set of indexed site IDs from the database
const indexedSiteIds = new Set(indexedSitesData.site_ids || []);
// Update in-memory set for future use
indexedSites.clear();
indexedSiteIds.forEach(id => indexedSites.add(id));
let html = '';
sitesData.sites.forEach(site => {
const siteId = site.id;
const siteName = escapeHtml(site.displayName || site.name);
const isIndexed = indexedSiteIds.has(siteId);
html += `
<div class="site-list-item ${isIndexed ? 'indexed' : ''}">
<div class="site-list-item-header">
<div class="site-list-item-name" onclick="loadFiles('${siteId}', '${siteName}')">
🏢 ${siteName}
</div>
${isIndexed ? '<span class="site-indexed-badge">✓ Indexed</span>' : ''}
</div>
<div class="site-list-item-actions">
<button onclick="event.stopPropagation(); loadFiles('${siteId}', '${siteName}');" class="btn btn-secondary">Browse</button>
<button onclick="event.stopPropagation(); showIndexingDialog('${siteId}', '${siteName}');" class="btn btn-primary">Index</button>
</div>
</div>
`;
});
sitesContainer.innerHTML = html;
} catch (error) {
console.error('Error loading sites:', error);
sitesContainer.innerHTML = '<div style="padding: 10px; text-align: center; font-size: 0.85rem; color: var(--danger-color);">Failed to load</div>';
}
}
// Load files
async function loadFiles(siteId, siteName, path = '') {
currentSiteId = siteId;
currentPath = path;
currentSiteName = siteName;
// Update main header
document.getElementById('mainTitle').textContent = `📁 ${siteName}`;
document.getElementById('mainSubtitle').textContent = 'Browse files and folders';
// Show files section, hide others
document.getElementById('multiChatSection').style.display = 'none';
document.getElementById('filesSection').style.display = 'block';
document.getElementById('contentSection').style.display = 'none';
// Update breadcrumb
const breadcrumb = document.getElementById('breadcrumb');
let breadcrumbHtml = `<span class="breadcrumb-item" onclick="loadFiles('${siteId}', '${escapeHtml(siteName)}')" style="cursor: pointer;">${escapeHtml(siteName)}</span>`;
if (path) {
const parts = path.split('/');
let currentPath = '';
parts.forEach((part, index) => {
currentPath += (currentPath ? '/' : '') + part;
const pathToUse = currentPath;
breadcrumbHtml += ` / <span class="breadcrumb-item" onclick="loadFiles('${siteId}', '${escapeHtml(siteName)}', '${escapeHtml(pathToUse)}')" style="cursor: pointer;">${escapeHtml(part)}</span>`;
});
}
breadcrumb.innerHTML = breadcrumbHtml;
const filesContainer = document.getElementById('filesContainer');
filesContainer.innerHTML = '<div class="loading">Loading files...</div>';
try {
const url = `/api/sharepoint/${currentConnectionId}/files?site_id=${encodeURIComponent(siteId)}&path=${encodeURIComponent(path)}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
filesContainer.innerHTML = `<div class="error">Error: ${escapeHtml(data.error)}</div>`;
return;
}
if (!data.files || data.files.length === 0) {
filesContainer.innerHTML = '<div class="empty">No files found</div>';
return;
}
let html = '<div class="file-list">';
data.files.forEach(item => {
const isFolder = item.folder !== undefined;
const icon = isFolder ? '📁' : '📄';
const name = item.name;
const size = isFolder ? '' : formatBytes(item.size || 0);
if (isFolder) {
const newPath = path ? `${path}/${name}` : name;
html += `
<div class="file-item" onclick="loadFiles('${siteId}', '${escapeHtml(siteName)}', '${escapeHtml(newPath)}')">
<span class="file-icon">${icon}</span>
<span class="file-name">${escapeHtml(name)}</span>
<span class="file-size">${size}</span>
</div>
`;
} else {
const filePath = path ? `${path}/${name}` : name;
html += `
<div class="file-item" onclick="readFile('${siteId}', '${escapeHtml(filePath)}', '${escapeHtml(name)}')">
<span class="file-icon">${icon}</span>
<span class="file-name">${escapeHtml(name)}</span>
<span class="file-size">${size}</span>
</div>
`;
}
});
html += '</div>';
filesContainer.innerHTML = html;
} catch (error) {
console.error('Error loading files:', error);
filesContainer.innerHTML = '<div class="error">Failed to load files</div>';
}
}
// Read file content
async function readFile(siteId, filePath, fileName) {
// Update main header
document.getElementById('mainTitle').textContent = `📄 ${fileName}`;
document.getElementById('mainSubtitle').textContent = 'View and chat about this document';
// Show content section, hide others
document.getElementById('multiChatSection').style.display = 'none';
document.getElementById('filesSection').style.display = 'none';
document.getElementById('contentSection').style.display = 'block';
currentFilePath = filePath;
const fileInfo = document.getElementById('fileInfo');
fileInfo.innerHTML = `<h3>${escapeHtml(fileName)}</h3>`;
const contentContainer = document.getElementById('fileContent');
contentContainer.textContent = 'Loading file content...';
// Check LLM status
checkLLMStatus();
// Clear chat for new file
document.getElementById('chatMessages').innerHTML = '';
try {
const url = `/api/sharepoint/${currentConnectionId}/read?site_id=${encodeURIComponent(siteId)}&file_path=${encodeURIComponent(filePath)}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
contentContainer.textContent = `Error: ${data.error}`;
return;
}
contentContainer.textContent = data.content || '(empty file)';
// Store document ID if returned
if (data.document_id) {
currentDocumentId = data.document_id;
// Load current tags for this document
loadDocumentTags();
}
// Load chat history if exists
loadChatHistory();
} catch (error) {
console.error('Error reading file:', error);
contentContainer.textContent = 'Failed to read file';
}
}
// ============================================================================
// CHAT / LLM FUNCTIONS
// ============================================================================
// Check LLM status
async function checkLLMStatus() {
const statusIndicator = document.querySelector('.status-indicator');
const statusText = document.querySelector('.status-text');
try {
const response = await fetch('/api/llm/status');
const data = await response.json();
if (data.available) {
isChatAvailable = true;
statusIndicator.className = 'status-indicator status-online';
statusText.textContent = `${data.provider} ready`;
document.getElementById('chatInput').disabled = false;
document.getElementById('sendBtn').disabled = false;
} else {
isChatAvailable = false;
statusIndicator.className = 'status-indicator status-offline';
statusText.textContent = `${data.provider} unavailable`;
document.getElementById('chatInput').disabled = true;
document.getElementById('sendBtn').disabled = true;
}
} catch (error) {
console.error('Error checking LLM status:', error);
isChatAvailable = false;
statusIndicator.className = 'status-indicator status-offline';
statusText.textContent = 'LLM unavailable';
document.getElementById('chatInput').disabled = true;
document.getElementById('sendBtn').disabled = true;
}
}
// Load chat history
async function loadChatHistory() {
if (!currentSiteId || !currentFilePath) return;
try {
const url = `/api/chat/history?site_id=${encodeURIComponent(currentSiteId)}&file_path=${encodeURIComponent(currentFilePath)}`;
const response = await fetch(url);
const data = await response.json();
if (data.messages && data.messages.length > 0) {
const chatMessages = document.getElementById('chatMessages');
chatMessages.innerHTML = '';
data.messages.forEach(msg => {
appendMessage(msg.role, msg.content);
});
}
} catch (error) {
console.error('Error loading chat history:', error);
}
}
// Send chat message
async function sendChatMessage() {
if (!isChatAvailable) {
alert('LLM is not available. Please ensure Ollama is running.');
return;
}
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message) return;
// Disable input while processing
input.disabled = true;
document.getElementById('sendBtn').disabled = true;
// Add user message to UI
appendMessage('user', message);
// Clear input
input.value = '';
try {
const response = await fetch('/api/chat/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
site_id: currentSiteId,
file_path: currentFilePath,
message: message
})
});
const data = await response.json();
if (data.error) {
appendMessage('system', `Error: ${data.error}`);
} else {
appendMessage('assistant', data.response);
}
} catch (error) {
console.error('Error sending message:', error);
appendMessage('system', 'Failed to send message');
} finally {
// Re-enable input
input.disabled = false;
document.getElementById('sendBtn').disabled = false;
input.focus();
}
}
// Append message to chat
function appendMessage(role, content) {
const chatMessages = document.getElementById('chatMessages');
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message chat-message-${role}`;
const label = role === 'user' ? 'You' : role === 'assistant' ? 'AI' : 'System';
messageDiv.innerHTML = `
<div class="message-label">${label}</div>
<div class="message-content">${escapeHtml(content).replace(/\n/g, '<br>')}</div>
`;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Clear chat
async function clearChat() {
if (!confirm('Clear chat history for this document?')) return;
try {
await fetch('/api/chat/clear', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
site_id: currentSiteId,
file_path: currentFilePath
})
});
document.getElementById('chatMessages').innerHTML = '';
} catch (error) {
console.error('Error clearing chat:', error);
alert('Failed to clear chat');
}
}
// Navigation functions
function backToChat() {
// Update main header
document.getElementById('mainTitle').textContent = '💬 Multi-Document Chat';
document.getElementById('mainSubtitle').textContent = 'Ask questions across all your indexed documents using semantic search';
// Show chat section, hide others
document.getElementById('multiChatSection').style.display = 'block';
document.getElementById('filesSection').style.display = 'none';
document.getElementById('contentSection').style.display = 'none';
currentSiteId = null;
currentPath = "";
}
function backToFiles() {
// Update main header
document.getElementById('mainTitle').textContent = `📁 ${currentSiteName}`;
document.getElementById('mainSubtitle').textContent = 'Browse files and folders';
// Show files section, hide content
document.getElementById('multiChatSection').style.display = 'none';
document.getElementById('contentSection').style.display = 'none';
document.getElementById('filesSection').style.display = 'block';
}
// Disconnect SharePoint
async function disconnectSharePoint() {
if (!confirm('Are you sure you want to disconnect your SharePoint account?')) {
return;
}
try {
const response = await fetch(`/api/sharepoint/connections/${currentConnectionId}/disconnect`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
window.location.reload();
} else {
alert('Failed to disconnect: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('Error disconnecting:', error);
alert('Failed to disconnect');
}
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
function showError(message) {
alert(message);
}
// ============================================================================
// TAG MANAGEMENT FUNCTIONS
// ============================================================================
// Load document tags from vector store
async function loadDocumentTags() {
if (!currentDocumentId) return;
try {
// Try to get documents by this document's tags
// First, we need to get all tags to see if this document has any
const response = await fetch('/api/documents/tags');
const data = await response.json();
if (data.tags) {
// For now, we'll need to query the document via a different endpoint
// or we can just initialize with empty tags and let users add them
currentTags = [];
displayTags();
}
} catch (error) {
console.error('Error loading tags:', error);
currentTags = [];
displayTags();
}
}
// Display current tags
function displayTags() {
const tagsDisplay = document.getElementById('documentTags');
if (currentTags.length === 0) {
tagsDisplay.innerHTML = '<span class="tag-placeholder">No tags yet - click Edit Tags to add</span>';
} else {
tagsDisplay.innerHTML = currentTags.map(tag =>
`<span class="tag">${escapeHtml(tag)}</span>`
).join('');
}
}
// Show tag editor
function showTagEditor() {
document.getElementById('tagEditor').style.display = 'block';
document.getElementById('tagInput').value = currentTags.join(', ');
document.getElementById('tagInput').focus();
}
// Hide tag editor
function hideTagEditor() {
document.getElementById('tagEditor').style.display = 'none';
document.getElementById('tagInput').value = '';
}
// Save tags
async function saveTags() {
if (!currentDocumentId) {
alert('No document loaded');
return;
}
const input = document.getElementById('tagInput').value;
const tags = input.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
try {
const response = await fetch('/api/documents/update-tags', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
document_id: currentDocumentId,
tags: tags
})
});
const data = await response.json();
if (data.error) {
alert('Failed to save tags: ' + data.error);
return;
}
// Update current tags and display
currentTags = tags;
displayTags();
hideTagEditor();
// Show success message
const tagsSection = document.querySelector('.tags-section');
const successMsg = document.createElement('div');
successMsg.className = 'success-message';
successMsg.textContent = '✓ Tags saved successfully';
tagsSection.appendChild(successMsg);
setTimeout(() => {
successMsg.remove();
}, 3000);
} catch (error) {
console.error('Error saving tags:', error);
alert('Failed to save tags');
}
}
// Add keyboard shortcut for tag editor
document.addEventListener('keydown', function(event) {
// Press 't' to open tag editor when viewing a document
if (event.key === 't' && !event.ctrlKey && !event.metaKey && !event.altKey) {
const tagEditor = document.getElementById('tagEditor');
if (tagEditor && currentDocumentId && document.getElementById('contentSection').style.display !== 'none') {
const activeElement = document.activeElement;
// Don't trigger if user is typing in an input/textarea
if (activeElement.tagName !== 'INPUT' && activeElement.tagName !== 'TEXTAREA') {
showTagEditor();
event.preventDefault();
}
}
}
});
// ============================================================================
// MULTI-DOCUMENT CHAT FUNCTIONS
// ============================================================================
// Load available tags on page load
async function loadAvailableTags() {
try {
const response = await fetch('/api/documents/tags');
const data = await response.json();
if (data.tags && Object.keys(data.tags).length > 0) {
const tagsDiv = document.getElementById('availableTags');
let tagsHtml = '<strong>Available tags:</strong> ';
tagsHtml += Object.entries(data.tags)
.map(([tag, count]) => `<span class="tag" style="cursor: pointer;" onclick="addTagToFilter('${escapeHtml(tag)}')">${escapeHtml(tag)} (${count})</span>`)
.join(' ');
tagsDiv.innerHTML = tagsHtml;
} else {
document.getElementById('availableTags').innerHTML = '<em style="color: #999;">No documents indexed yet. Index a site to get started!</em>';
}
} catch (error) {
console.error('Error loading tags:', error);
}
}
// Add tag to filter input
function addTagToFilter(tag) {
const input = document.getElementById('multiChatTags');
const currentTags = input.value.split(',').map(t => t.trim()).filter(t => t);
if (!currentTags.includes(tag)) {
currentTags.push(tag);
input.value = currentTags.join(', ');
}
}
// Send multi-document chat message
async function sendMultiChatMessage() {
const input = document.getElementById('multiChatInput');
const message = input.value.trim();
if (!message) return;
// Get tags filter
const tagsInput = document.getElementById('multiChatTags').value;
const tags = tagsInput.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
// Validate that if tags are selected, we only search those tags
// The backend will filter by tags automatically
// Disable input while processing
input.disabled = true;
document.getElementById('multiSendBtn').disabled = true;
// Add user message to UI with tag context
let displayMessage = message;
if (tags.length > 0) {
displayMessage = `[Filtering by tags: ${tags.join(', ')}]\n${message}`;
}
appendMultiMessage('user', displayMessage);
// Clear input
input.value = '';
// Add thinking indicator
const thinkingId = appendMultiMessage('thinking', 'Thinking<span class="thinking-dots"></span>');
try {
// Use Server-Sent Events for streaming
const response = await fetch('/api/chat/multi/stream', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message: message,
tags: tags.length > 0 ? tags : null,
top_k: 5
})
});
if (!response.ok) {
throw new Error('Failed to send message');
}
// Remove thinking indicator and prepare for streaming response
removeMultiMessage(thinkingId);
let sources = null;
let responseText = '';
let assistantMsgId = null;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line in buffer
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.substring(6));
if (data.error) {
let errorMessage = data.error;
if (tags.length > 0 && data.error.includes('No relevant documents found')) {
errorMessage = `No documents found with tags: ${tags.join(', ')}. Please check your tag filter or index more documents.`;
}
appendMultiMessage('system', errorMessage);
break;
}
if (data.sources) {
sources = data.sources;
}
if (data.chunk) {
responseText += data.chunk;
// Update or create assistant message
if (!assistantMsgId) {
assistantMsgId = appendMultiMessage('assistant', responseText);
} else {
updateMultiMessage(assistantMsgId, responseText);
}
}
if (data.done && sources) {
// Add sources at the end
let finalText = responseText + '\n\n---\n**Sources:**\n';
sources.forEach((source, idx) => {
finalText += `${idx + 1}. ${source.filename} (${source.tags.join(', ')}) - Similarity: ${(source.similarity * 100).toFixed(1)}%\n`;
});
updateMultiMessage(assistantMsgId, finalText);
}
}
}
}
} catch (error) {
console.error('Error sending message:', error);
removeMultiMessage(thinkingId);
appendMultiMessage('system', 'Failed to send message');
} finally {
// Re-enable input
input.disabled = false;
document.getElementById('multiSendBtn').disabled = false;
input.focus();
}
}
// Append message to multi-chat
function appendMultiMessage(role, content) {
const chatMessages = document.getElementById('multiChatMessages');
const messageDiv = document.createElement('div');
const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
messageDiv.id = messageId;
messageDiv.className = `chat-message chat-message-${role}`;
const label = role === 'user' ? 'You' : role === 'assistant' ? 'AI' : role === 'thinking' ? 'AI' : 'System';
messageDiv.innerHTML = `
<div class="message-label">${label}</div>
<div class="message-content">${content.replace(/\n/g, '<br>')}</div>
`;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageId;
}
// Remove message from multi-chat
function removeMultiMessage(messageId) {
const messageDiv = document.getElementById(messageId);
if (messageDiv) {
messageDiv.remove();
}
}
// Update existing message in multi-chat
function updateMultiMessage(messageId, content) {
const messageDiv = document.getElementById(messageId);
if (messageDiv) {
const contentDiv = messageDiv.querySelector('.message-content');
if (contentDiv) {
contentDiv.innerHTML = content.replace(/\n/g, '<br>');
}
// Auto-scroll to bottom as content updates
const chatMessages = document.getElementById('multiChatMessages');
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
// Clear multi-chat
function clearMultiChat() {
document.getElementById('multiChatMessages').innerHTML = '';
}
// ============================================================================
// BACKGROUND INDEXING FUNCTIONS
// ============================================================================
// Show tag selection dialog
function showIndexingDialog(siteId, siteName) {
pendingIndexingSiteId = siteId;
pendingIndexingSiteName = siteName;
document.getElementById('tagSelectionModal').style.display = 'flex';
document.getElementById('tagSelectionSiteName').textContent = `Site: ${siteName}`;
document.getElementById('indexingTagInput').value = '';
document.getElementById('indexingTagInput').focus();
}
// Close tag selection dialog
function closeTagSelectionModal() {
document.getElementById('tagSelectionModal').style.display = 'none';
pendingIndexingSiteId = null;
pendingIndexingSiteName = null;
}
// Start indexing with selected tags
async function startIndexingWithTags() {
if (!pendingIndexingSiteId || !pendingIndexingSiteName) {
return;
}
// Save values before closing modal (which clears them)
const siteId = pendingIndexingSiteId;
const siteName = pendingIndexingSiteName;
// Get tags from input
const tagInput = document.getElementById('indexingTagInput').value;
const tags = tagInput.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
// Close tag selection modal
closeTagSelectionModal();
// Start indexing with saved values
await startIndexing(siteId, siteName, tags);
}
// Start indexing a site
async function startIndexing(siteId, siteName, tags = []) {
// Verify we have a connection ID
if (!currentConnectionId) {
alert('No active SharePoint connection found. Please refresh the page.');
return;
}
try {
console.log('Starting indexing with:', {
site_id: siteId,
site_name: siteName,
connection_id: currentConnectionId,
tags: tags
});
const response = await fetch('/api/indexing/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
site_id: siteId,
site_name: siteName,
connection_id: currentConnectionId,
tags: tags
})
});
const data = await response.json();
if (data.error) {
alert('Failed to start indexing: ' + data.error);
return;
}
// Store job ID and site ID, show progress modal
currentIndexingJobId = data.job_id;
currentIndexingSiteId = siteId;
showIndexingModal(siteName, tags);
// Start polling for status
startIndexingStatusPolling();
} catch (error) {
console.error('Error starting indexing:', error);
alert('Failed to start indexing');
}
}
// Show indexing modal
function showIndexingModal(siteName, tags = []) {
document.getElementById('indexingModal').style.display = 'flex';
let siteNameText = `Indexing: ${siteName}`;
if (tags && tags.length > 0) {
siteNameText += ` (Tags: ${tags.join(', ')})`;
}
document.getElementById('indexingSiteName').textContent = siteNameText;
document.getElementById('indexingProgress').style.width = '0%';
document.getElementById('indexingStats').textContent = 'Starting...';
document.getElementById('indexingCurrentFile').textContent = '';
}
// Close indexing modal
function closeIndexingModal() {
document.getElementById('indexingModal').style.display = 'none';
stopIndexingStatusPolling();
}
// Start polling for indexing status
function startIndexingStatusPolling() {
// Poll every 2 seconds
indexingPollInterval = setInterval(updateIndexingStatus, 2000);
updateIndexingStatus(); // Immediate first check
}
// Stop polling
function stopIndexingStatusPolling() {
if (indexingPollInterval) {
clearInterval(indexingPollInterval);
indexingPollInterval = null;
}
}
// Update indexing status
async function updateIndexingStatus() {
if (!currentIndexingJobId) return;
try {
const response = await fetch(`/api/indexing/status/${currentIndexingJobId}`);
const data = await response.json();
if (data.error) {
console.error('Error fetching status:', data.error);
return;
}
// Update progress bar
const progress = data.progress || 0;
document.getElementById('indexingProgress').style.width = `${progress}%`;
// Update stats
const stats = `${data.processed_files || 0} / ${data.total_files || 0} files processed (${data.successful_files || 0} success, ${data.failed_files || 0} failed)`;
document.getElementById('indexingStats').textContent = stats;
// Update current file
if (data.current_file) {
document.getElementById('indexingCurrentFile').textContent = `Current: ${data.current_file}`;
}
// Check if completed
if (data.status === 'completed') {
stopIndexingStatusPolling();
document.getElementById('indexingStats').textContent = `✅ Completed! ${data.successful_files} files indexed successfully`;
document.getElementById('indexingCurrentFile').textContent = '';
document.getElementById('cancelIndexBtn').style.display = 'none';
// Mark the site as indexed
if (currentIndexingSiteId) {
indexedSites.add(currentIndexingSiteId);
}
// Reload sites list to show indexed status
loadSites();
// Reload available tags
loadAvailableTags();
setTimeout(() => {
closeIndexingModal();
document.getElementById('cancelIndexBtn').style.display = 'block';
}, 3000);
} else if (data.status === 'failed') {
stopIndexingStatusPolling();
document.getElementById('indexingStats').textContent = `❌ Failed: ${data.error || 'Unknown error'}`;
document.getElementById('indexingCurrentFile').textContent = '';
} else if (data.status === 'cancelled') {
stopIndexingStatusPolling();
document.getElementById('indexingStats').textContent = '🛑 Cancelled by user';
document.getElementById('indexingCurrentFile').textContent = '';
}
} catch (error) {
console.error('Error updating status:', error);
}
}
// Cancel indexing
async function cancelIndexing() {
if (!currentIndexingJobId) return;
if (!confirm('Are you sure you want to cancel indexing?')) {
return;
}
try {
const response = await fetch(`/api/indexing/cancel/${currentIndexingJobId}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
stopIndexingStatusPolling();
document.getElementById('indexingStats').textContent = '🛑 Cancelling...';
}
} catch (error) {
console.error('Error cancelling indexing:', error);
alert('Failed to cancel indexing');
}
}
</script>
</body>
</html>