Files
agentic/frontend/js/research.js
T
2026-06-10 08:20:27 +02:00

300 lines
11 KiB
JavaScript
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.
// Research orchestration UI
const RESEARCH_UI = {
async init() {
$("#run-research").addEventListener("click", () => this.runResearch());
$("#query-input").addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.ctrlKey) this.runResearch();
});
},
async runResearch() {
const query = $("#query-input").value.trim();
if (!query) return showToast("Enter a research query", "error");
// Get selected skills
const checked = [...document.querySelectorAll('.skill-toggle input:checked')].map(c => c.value);
const output = $("#research-output");
output.innerHTML = "";
showSpinner(output);
// Create session
let sessionId = STORE.currentSessionId;
if (!sessionId) {
try {
const session = await API.post("/api/research/session", { query });
STORE.currentSessionId = session.session_id;
sessionId = session.session_id;
} catch (e) {
showToast("Failed to create session", "error");
output.innerHTML = `<p class='typing'>Session error: ${e.message}</p>`;
return;
}
}
// Run research
try {
const results = await API.post("/api/research/run", {
query,
doc_id: STORE.currentDocId,
skills: checked.length ? checked : undefined,
});
output.innerHTML = "";
// Session info
const info = document.createElement("div");
info.className = "research-section";
info.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center">
<h3 style="color:var(--accent-blue)">Research Results</h3>
<span style="font-size:11px;color:var(--text-muted)">Session: ${sessionId.substring(0, 8)}</span>
</div>
<p style="color:var(--text-secondary)">Query: ${query}</p>
<hr style="border:none;border-top:1px solid var(--border-color);margin:8px 0">`;
output.appendChild(info);
// Results from each skill
if (results.results) {
for (const [skill, response] of Object.entries(results.results)) {
const section = document.createElement("div");
section.className = "research-section";
section.id = `section-${skill}`;
section.innerHTML = `<h3>${skill.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}</h3>`;
const content = document.createElement("div");
content.innerHTML = renderContent(response);
section.appendChild(content);
output.appendChild(section);
}
}
STORE.currentSessionId = sessionId;
showToast("Research complete", "info");
} catch (e) {
output.innerHTML = `<p class='typing' style="color:var(--accent-red)">Error: ${e.message}</p>`;
showToast("Research failed", "error");
}
},
async runSemanticSearch(query, docId = null) {
const results = await API.post("/api/research/semantic", { query, doc_id: docId, limit: 20 });
return results.results || [];
},
async runTextSearch(query, docId = null) {
const results = await API.post("/api/research/text-search", { query, doc_id: docId, limit: 20 });
return results.results || [];
},
async getFindings(sessionId) {
if (!sessionId) return [];
try {
const res = await API.get(`/api/research/findings?session_id=${sessionId}`);
return res.findings || [];
} catch { return []; }
},
async saveMemory(sessionId, content, type = "fact", importance = 3) {
if (!sessionId) return;
try {
await API.post("/api/memories/save", {
session_id: sessionId,
content,
memory_type: type,
importance,
source_doc_id: STORE.currentDocId,
});
showToast("Memory saved", "info");
} catch (e) { showToast("Save failed: " + e.message, "error"); }
},
async runPipeline() {
const query = $("#query-input").value.trim();
if (!query) return showToast("Enter a research query", "error");
const mode = $("#pipeline-output-mode").value || "";
// Create session
let sessionId = STORE.currentSessionId;
if (!sessionId) {
try {
const session = await API.post("/api/research/session", { query });
sessionId = session.session_id;
STORE.currentSessionId = sessionId;
} catch (e) {
showToast("Failed to create session", "error");
return;
}
}
const pipelineEl = $("#pipeline-output");
pipelineEl.innerHTML = '<div class="spinner"></div> <span class="typing">Running: triage → evidence extraction → synthesis...</span>';
// Run the pipeline
try {
const results = await API.post("/api/research/pipeline", {
query,
doc_ids: STORE.currentDocId ? [STORE.currentDocId] : undefined,
output_mode: mode || undefined,
});
// Save session to the first doc or temp session
STORE.currentSessionId = sessionId;
// Render pipeline sections
// First render: get structured sections from results
const sections = RESEARCH_UI.renderPipelineSections(results);
pipelineEl.innerHTML = sections.map(s => RESEARCH_UI.renderSectionHtml(s)).join('');
// Wire up collapse toggles
pipelineEl.querySelectorAll(".pipeline-stage-header").forEach(header => {
header.addEventListener("click", () => {
const body = header.nextElementSibling;
body.classList.toggle("collapsed");
const arrow = header.querySelector(".stage-arrow");
if (arrow) arrow.textContent = body.classList.contains("collapsed") ? "▶" : "▼";
});
});
showToast("Pipeline complete", "info");
} catch (e) {
pipelineEl.innerHTML = `<p class='typing' style="color:var(--accent-red)">Error: ${e.message}</p>`;
showToast("Pipeline failed", "error");
}
},
renderPipelineSections(results) {
const sections = [];
// Triage stage
if (results.triage_state) {
const plan = results.triage_state;
sections.push({
stage: "triage",
title: "Stage 1: Source Triage",
status: "complete",
sections: [
{ label: "Objective", content: plan.OBJECTIVE || "" },
{ label: "Sub-questions", content: plan["SUB-QUESTIONS"] || "" },
{ label: "Classification", content: plan.CLASSIFICATION || "" },
{ label: "Reading Order", content: plan.READING_ORDER || "" },
{ label: "Extraction Criteria", content: plan["EXTRACTION CRITERIA"] || "" },
{ label: "Risks & Gaps", content: plan["RISKS and GAPS"] || plan.RISKS_AND_GAPS || "" },
],
});
}
// Evidence stage
if (results.evidence_rows && results.evidence_rows.length > 0) {
sections.push({
stage: "evidence",
title: `Stage 2: Extracted Evidence (${results.evidence_rows.length} rows)`,
status: "complete",
sections: [
{ label: "", content: "table", rows: results.evidence_rows },
],
});
}
// Synthesis stage
if (results.synthesis) {
sections.push({
stage: "synthesis",
title: `Stage 3: Synthesis (mode: ${results.output_mode || "auto"})`,
status: "complete",
sections: [
{ label: "Result", content: results.synthesis, mode: results.output_mode },
],
});
}
return sections;
},
renderSectionHtml(section) {
let cardsHtml = "";
section.sections.forEach(sec => {
if (sec.content === "table") {
const rows = sec.rows || [];
const hasComparison = rows[0] && rows[0].source_a !== undefined;
if (hasComparison) {
cardsHtml += `<div class="pipeline-section-label">Cross-Comparison</div>`;
cardsHtml += `<table class="matrix-table"><thead><tr><th>Topic</th><th>Source A</th><th>Source B</th><th>Agreement</th><th>Conflict</th><th>Notes</th></tr></thead><tbody>`;
rows.forEach(r => {
cardsHtml += `<tr>
<td>${r.topic || ""}</td>
<td>${(r.source_a || "").substring(0, 200)}</td>
<td>${(r.source_b || "").substring(0, 200)}</td>
<td>${r.agreement || ""}</td>
<td>${r.conflict || ""}</td>
<td>${r.notes || ""}</td>
</tr>`;
});
cardsHtml += `</tbody></table>`;
} else {
const cols = ["#", "Topic", "Evidence Type", "Description", "Doc Ref", "Evidence", "Analyst Note", "Confidence", "Review"];
cardsHtml += `<div class="pipeline-section-label">Extracted Evidence</div>`;
cardsHtml += `<table class="evidence-table"><thead><tr>${cols.map(c => `<th>${c}</th>`).join("")}</tr></thead><tbody>`;
rows.forEach((r, i) => {
const confClass = (r.confidence || "medium").toLowerCase().replace(" ", "-");
cardsHtml += `<tr>
<td>${i + 1}</td>
<td>${r.topic || ""}</td>
<td>${r.evidence_type || ""}</td>
<td>${(r.description || "").substring(0, 150)}</td>
<td>${(r.trace_ref || "").substring(0, 150)}</td>
<td>${(r.evidence || "").substring(0, 200)}</td>
<td>${(r.analyst_note || "").substring(0, 150)}</td>
<td class="confidence-${confClass}">${r.confidence || "Medium"}</td>
<td>${r.review_needed || "No"}</td>
</tr>`;
});
cardsHtml += `</tbody></table>`;
}
} else {
cardsHtml += `<div class="pipeline-section-label">${sec.label || ""}</div>`;
cardsHtml += `<div class="pipeline-section-content">${sec.content}</div>`;
}
});
return `
<div class="pipeline-stage-card">
<div class="pipeline-stage-header" data-stage="${section.stage}">
<span class="stage-title">${section.title}</span>
<span><span class="stage-arrow" style="margin-right:6px;">▼</span><span class="stage-status ${section.status}">Complete</span></span>
</div>
<div class="stage-body">${cardsHtml}</div>
</div>`;
},
async loadPipelineSession(sessionId) {
if (!sessionId) return;
const pipelineEl = $("#pipeline-output");
try {
const res = await API.get(`/api/research/pipeline/${sessionId}/render`);
const sections = res.sections || [];
pipelineEl.innerHTML = sections.length > 0
? sections.map(s => this.renderSectionHtml(s)).join('')
: "<div class='empty-state'><div class='empty-icon'>️</div><div class='empty-text'>No pipeline data for this session</div></div>";
// Wire up collapse toggles
pipelineEl.querySelectorAll(".pipeline-stage-header").forEach(header => {
header.addEventListener("click", () => {
const body = header.nextElementSibling;
body.classList.toggle("collapsed");
const arrow = header.querySelector(".stage-arrow");
if (arrow) arrow.textContent = body.classList.contains("collapsed") ? "▶" : "▼";
});
});
} catch (e) {
pipelineEl.innerHTML = `<p class='typing' style="color:var(--accent-red)">Error loading pipeline: ${e.message}</p>`;
}
},
};