Some checks failed
CI - SharePoint Plugin with SonarQube / Test and SonarQube Analysis (push) Has been cancelled
1418 lines
61 KiB
HTML
1418 lines
61 KiB
HTML
<!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>
|