first commit
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
// 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('');
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user