199 lines
7.2 KiB
JavaScript
199 lines
7.2 KiB
JavaScript
// Main UI management
|
|
const UI = {
|
|
async init() {
|
|
// Tab switching
|
|
$$(".tab").forEach(tab => {
|
|
tab.addEventListener("click", () => {
|
|
const target = tab.dataset.tab;
|
|
this.switchTab(target);
|
|
|
|
// Load tab data
|
|
if (target === "search") this.initSearch();
|
|
if (target === "memories") this.loadMemories();
|
|
if (target === "findings") this.loadFindings();
|
|
if (target === "pipeline" && STORE.currentSessionId) this.loadPipelineSession(STORE.currentSessionId);
|
|
});
|
|
});
|
|
|
|
// Pipeline mode toggle
|
|
const pipelineCheck = $("#pipeline-mode");
|
|
const pipelineModeSelect = $("#pipeline-output-mode");
|
|
if (pipelineCheck) {
|
|
pipelineCheck.addEventListener("change", () => {
|
|
const on = pipelineCheck.checked;
|
|
pipelineModeSelect.style.display = on ? "inline-block" : "none";
|
|
const toggles = document.querySelectorAll("#skill-toggles .skill-toggle");
|
|
toggles.forEach(t => {
|
|
const cb = t.querySelector("input");
|
|
if (cb) cb.disabled = on;
|
|
});
|
|
});
|
|
}
|
|
|
|
// Rebuild pipeline button
|
|
const rebuildBtn = $("#rebuild-pipeline");
|
|
if (rebuildBtn && STORE.currentSessionId) {
|
|
rebuildBtn.addEventListener("click", () => UI.loadPipelineSession(STORE.currentSessionId));
|
|
}
|
|
|
|
// Search
|
|
$("#execute-search").addEventListener("click", () => this.executeSearch());
|
|
$("#search-query").addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") this.executeSearch();
|
|
});
|
|
|
|
// Check models
|
|
$("#check-models").addEventListener("click", () => this.checkModels());
|
|
|
|
// Load state
|
|
await loadInitialState();
|
|
|
|
// Init sub-modules
|
|
DOC_VIEWER.init();
|
|
RESEARCH_UI.init();
|
|
DOC_UI.init();
|
|
},
|
|
|
|
switchTab(tabName) {
|
|
$$(".tab").forEach(t => t.classList.remove("active"));
|
|
$$(".tab-pane").forEach(p => p.classList.remove("active"));
|
|
document.querySelector(`[data-tab="${tabName}"]`).classList.add("active");
|
|
$(`#tab-${tabName}`).classList.add("active");
|
|
},
|
|
|
|
initSearch() {
|
|
// Search already bound in HTML
|
|
},
|
|
|
|
async executeSearch() {
|
|
const query = $("#search-query").value.trim();
|
|
if (!query) return showToast("Enter a search query", "error");
|
|
|
|
const type = $("#search-type").value;
|
|
const resultsEl = $("#search-results");
|
|
resultsEl.innerHTML = '<div class="spinner"></div> <span class="typing">Searching...</span>';
|
|
|
|
try {
|
|
let results;
|
|
if (type === "semantic") {
|
|
results = await RESEARCH_UI.runSemanticSearch(query, STORE.currentDocId);
|
|
} else {
|
|
results = await RESEARCH_UI.runTextSearch(query, STORE.currentDocId);
|
|
}
|
|
|
|
if (!results.length) {
|
|
resultsEl.innerHTML = '<div style="padding:20px;color:var(--text-muted);text-align:center">No results found</div>';
|
|
return;
|
|
}
|
|
|
|
resultsEl.innerHTML = results.map(r => {
|
|
const score = r.similarity ?? r.rank ?? 0;
|
|
const page = r.page_num != null ? `p${r.page_num}` : "";
|
|
const truncated = r.content ? r.content.substring(0, 200) : "";
|
|
|
|
// Highlight matching text
|
|
let content = truncated.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
if (query) {
|
|
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
content = content.replace(regex, '<mark>$1</mark>');
|
|
}
|
|
|
|
return `
|
|
<div class="search-result-item" data-chunk-id="${r.id}">
|
|
<div class="result-score">Score: ${score.toFixed(3)}</div>
|
|
<div class="result-content">${content}${truncated.length >= 200 ? '...' : ''}</div>
|
|
<div class="result-meta">${page} · ${r.chunk_type || 'text'}</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
// Click to show chunk in viewer
|
|
resultsEl.querySelectorAll(".search-result-item").forEach(item => {
|
|
item.addEventListener("click", () => {
|
|
const chunkId = item.dataset.chunkId;
|
|
if (STORE.currentDocId) {
|
|
DOC_VIEWER.showChunkInfo(chunkId);
|
|
}
|
|
});
|
|
});
|
|
|
|
} catch (e) {
|
|
resultsEl.innerHTML = `<p style="color:var(--accent-red);padding:20px">Search error: ${e.message}</p>`;
|
|
}
|
|
},
|
|
|
|
async loadMemories() {
|
|
const list = $("#memories-list");
|
|
list.innerHTML = '<div class="spinner"></div>';
|
|
|
|
try {
|
|
if (STORE.currentSessionId) {
|
|
const res = await API.get(`/api/research/memories?session_id=${STORE.currentSessionId}`);
|
|
list.innerHTML = (res.memories || []).map(m => `
|
|
<div class="memory-item">
|
|
<div class="memory-type">${m.memory_type || "fact"} (imp: ${m.importance || "?"})</div>
|
|
<div class="memory-content">${m.content ? m.content.substring(0, 300) : "(empty)"}</div>
|
|
</div>`).join('') || '<div style="color:var(--text-muted);padding:12px">No memories</div>';
|
|
} else {
|
|
list.innerHTML = '<div style="color:var(--text-muted);padding:12px">No active session</div>';
|
|
}
|
|
} catch (e) {
|
|
list.innerHTML = `<div style="color:var(--accent-red);padding:12px">${e.message}</div>`;
|
|
}
|
|
},
|
|
|
|
async loadFindings() {
|
|
const list = $("#findings-list");
|
|
list.innerHTML = '<div class="spinner"></div>';
|
|
|
|
try {
|
|
const findings = await RESEARCH_UI.getFindings(STORE.currentSessionId);
|
|
list.innerHTML = findings.length ? findings.map(f => `
|
|
<div class="finding-item">
|
|
<div class="finding-agent">${f.agent_name || "unknown"}</div>
|
|
<div class="finding-query">${f.question || "N/A"}</div>
|
|
<div class="finding-answer">${(f.answer || f.summary || "").substring(0, 400)}</div>
|
|
</div>`).join('') : '<div style="color:var(--text-muted);padding:12px">No findings yet</div>';
|
|
} catch (e) {
|
|
list.innerHTML = `<div style="color:var(--accent-red);padding:12px">${e.message}</div>`;
|
|
}
|
|
},
|
|
|
|
async checkModels() {
|
|
const list = $("#model-list");
|
|
list.innerHTML = '<div class="spinner"></div>';
|
|
|
|
try {
|
|
const res = await API.get("/api/ollama/models");
|
|
const models = res.models || [];
|
|
list.innerHTML = models.length ? models.map(m => `
|
|
<div style="font-size:11px;padding:3px 8px;color:var(--accent-green)">
|
|
${m.name || m.model}
|
|
</div>`).join('') : '<div style="font-size:11px;padding:3px 8px;color:var(--text-muted)">None found</div>';
|
|
} catch (e) {
|
|
list.innerHTML = `<div style="font-size:11px;padding:3px 8px;color:var(--accent-red)">Off: ${e.message}</div>`;
|
|
}
|
|
},
|
|
|
|
updateHealth(health) {
|
|
const dot = $("#health-indicator .health-dot");
|
|
const text = $("#health-indicator");
|
|
if (dot) {
|
|
dot.className = health.status === "ok" ? "health-dot healthy" : "health-dot";
|
|
text.textContent = health.status === "ok" ? "All systems operational" : "Systems degraded";
|
|
}
|
|
},
|
|
|
|
renderModels(models) {
|
|
if (!models?.models?.length) return;
|
|
const list = $("#model-list");
|
|
if (list) {
|
|
list.innerHTML = models.models.map(m => `
|
|
<div style="font-size:11px;padding:3px 8px;color:var(--accent-green)">
|
|
${m.name || m.model}
|
|
</div>`).join('');
|
|
}
|
|
},
|
|
};
|