${escapeHtml(log.extractor_name)}
${escapeHtml(log.provider)}
${escapeHtml(validation.claim_status ?? "")}
accepted ${validation.accepted_claim_count ?? 0}
rejected ${validation.rejected_claim_count ?? 0}
${escapeHtml(context.page_type ?? "")} / ${escapeHtml(context.crawl_status ?? "")} / ${escapeHtml(context.extraction_status ?? "")}
${escapeHtml(log.page_url ?? "")}
${escapeHtml(rejectedPreview)}
`;
}
async function runResearch() {
if (!state.selectedProject) return;
$("researchResult").textContent = t("status.research_running");
const seedEntityRaw = $("researchSeedEntityId").value.trim();
try {
const result = await api("/research/run", {
method: "POST",
body: JSON.stringify({
...requestBase(),
project_name: state.selectedProject,
seed_entity_id: seedEntityRaw ? Number(seedEntityRaw) : null,
goal: $("researchGoal").value.trim() || "Semantic ontology exploration",
max_depth: Number($("siteMaxDepth").value || 2),
max_steps: Number($("researchMaxSteps").value || 8),
max_branch: 8,
min_relevance: Number($("researchMinRelevance").value || 0.35),
same_domain_only: $("sameDomainOnly").checked,
analyze_page_types: ["ProductPage", "BrandStoryPage", "ReviewPage"],
}),
});
$("researchResult").textContent =
`Research ${result.status}: explored ${result.explored_count}, analyzed ${result.analyzed_count}, queued ${result.queued_count}, skipped ${result.skipped_count}`;
toast(t("toast.research_completed"));
const event = new CustomEvent("workspace:refresh");
window.dispatchEvent(event);
} catch (error) {
$("researchResult").textContent = `${t("toast.research_failed")}: ${error.message}`;
toast(t("toast.research_failed"));
}
}
export async function loadResearchSessions() {
if (!state.selectedProject) return;
const sessions = await api(`/projects/${encodeURIComponent(state.selectedProject)}/research/sessions?limit=20`);
$("researchSessions").innerHTML = sessions.map(renderResearchSession).join("");
}
function renderResearchSession(session) {
const history = session.history ?? [];
const latest = history.length ? history[history.length - 1] : null;
const memory = session.memory ?? {};
const conflicts = memory.conflicts ?? [];
return `
${escapeHtml(session.name ?? "Research session")}
${escapeHtml(session.status ?? "")}
history ${history.length}
queue ${(session.queue ?? []).length}
conflicts ${conflicts.length}
${escapeHtml(session.goal ?? "")}
seed ${escapeHtml(session.seed ?? "")}
${escapeHtml(latest?.outcome?.reason ?? latest?.outcome?.status ?? "")}
`;
}
export async function loadGraphQuery() {
if (!state.selectedProject) return;
const kind = $("graphQueryKind")?.value ?? "trend_summary";
const text = $("graphQueryText")?.value.trim() ?? "";
let query = `kind=${encodeURIComponent(kind)}`;
if (kind === "brand_products" && text) query += `&brand=${encodeURIComponent(text)}`;
if (kind === "products_by_tag" && text) query += `&tag=${encodeURIComponent(text)}`;
const rows = await api(`/projects/${encodeURIComponent(state.selectedProject)}/graph/query?${query}`);
$("graphQueryTable").innerHTML = renderGraphRows(kind, rows);
}
function renderGraphRows(kind, rows) {
if (kind === "brand_products") {
return table(
[t("table.product"), t("table.type"), t("table.brand"), t("table.confidence")],
rows.map((row) => [row.product, row.product_type, row.brand ?? "", Math.round(row.confidence * 100) + "%"]),
);
}
if (kind === "products_by_tag") {
return table(
[t("table.product"), t("table.predicate"), t("table.tag"), t("table.confidence")],
rows.map((row) => [row.product, row.predicate, row.tag, Math.round(row.confidence * 100) + "%"]),
);
}
if (kind === "relation_summary") {
return table(
[t("table.predicate"), t("table.status"), t("table.support"), t("table.confidence")],
rows.map((row) => [row.predicate, row.status, row.support_count, Math.round((row.max_confidence ?? 0) * 100) + "%"]),
);
}
if (kind === "entity_type_summary") {
return table(
[t("table.type"), t("table.count")],
rows.map((row) => [row.entity_type, row.count]),
);
}
return table(
[t("table.predicate"), t("table.name"), t("table.type"), t("table.support"), t("table.confidence")],
rows.map((row) => [
row.predicate, row.name, row.entity_type, row.support_count,
Math.round((row.max_confidence ?? 0) * 100) + "%",
]),
);
}
export async function loadTags() {
if (!state.selectedProject) return;
const tags = await api(`/projects/${encodeURIComponent(state.selectedProject)}/recommendation-tags`);
$("tagTable").innerHTML = table(
[t("table.predicate"), t("table.type"), t("table.name"), t("table.support"), t("table.confidence")],
tags.map((tag) => [
tag.predicate, tag.type, tag.name, tag.support_count,
Math.round((tag.max_confidence ?? 0) * 100) + "%",
]),
);
}
async function recommend() {
if (!state.selectedProject) return;
try {
const result = await api("/recommend", {
method: "POST",
body: JSON.stringify({
project_name: state.selectedProject,
target_entity_type: state.projectDetail?.config?.recommendation?.target_entity_type ?? "Perfume",
preferences: {
preferred_notes: csv($("preferredNotes").value),
avoided_notes: csv($("avoidedNotes").value),
preferred_moods: csv($("preferredMoods").value),
season_context: $("seasonContext").value.trim() || null,
occasion_context: $("occasionContext").value.trim() || null,
},
limit: 10,
}),
});
$("recommendTable").innerHTML = table(
[t("table.name"), t("table.type"), t("table.score"), t("table.reasons")],
result.map((item) => [item.name, item.entity_type, item.score, (item.reasons ?? []).join(", ")]),
);
} catch (error) {
toast(error.message);
}
}