import { api } from "./api.js"; import { state } from "./state.js"; import { $, escapeHtml, chip, table, toast, csv } from "./utils.js"; import { t, applyI18n, onLangChange } from "./i18n.js"; import { requestBase } from "./sidebar.js"; import { refreshInspector } from "./inspector.js"; import { graphTabHtml, mountGraph, loadGraph } from "./graph.js"; import { workbenchHtml, mountWorkbench, loadWorkbench } from "./workbench.js"; import { pipelineHtml, mountPipeline, loadPipeline, renderPipelineSourceTable } from "./pipeline.js"; let host = null; export function mountWorkspace(target) { host = target; host.innerHTML = workspaceHtml(); applyI18n(host); document.querySelectorAll(".tab").forEach((tab) => { tab.addEventListener("click", () => { document.querySelectorAll(".tab").forEach((n) => n.classList.remove("active")); document.querySelectorAll(".tab-panel").forEach((n) => n.classList.remove("active")); tab.classList.add("active"); document.getElementById(tab.dataset.tab).classList.add("active"); }); }); mountGraph(); mountWorkbench(); mountPipeline(); $("loadOntologyRegistryBtn").addEventListener("click", loadOntologyRegistry); $("loadEntitiesBtn").addEventListener("click", loadEntities); $("mergeEntitiesBtn").addEventListener("click", mergeEntities); $("loadExtractionLogsBtn").addEventListener("click", loadExtractionLogs); $("runResearchBtn").addEventListener("click", runResearch); $("loadResearchBtn").addEventListener("click", loadResearchSessions); $("loadGraphQueryBtn").addEventListener("click", loadGraphQuery); $("loadTagsBtn").addEventListener("click", loadTags); $("recommendBtn").addEventListener("click", recommend); onLangChange(() => { applyI18n(host); renderOverview(); renderOntology(); loadPipeline().catch(() => {}); loadOntologyRegistry().catch(() => {}); loadEntities().catch(() => {}); loadWorkbench().catch(() => {}); loadExtractionLogs().catch(() => {}); loadResearchSessions().catch(() => {}); loadGraphQuery().catch(() => {}); loadTags().catch(() => {}); loadGraph().catch(() => {}); }); } function workspaceHtml() { return ` ${graphTabHtml()} ${pipelineHtml()}

${workbenchHtml()}

`; } export function renderOverview() { renderPipelineSourceTable(state.projectDetail); } export function renderOntology() { const entityTypes = state.ontology?.entity_types ?? []; const predicates = state.ontology?.predicates ?? []; $("ontologyEntities").innerHTML = entityTypes.map(chip).join(""); $("ontologyPredicates").innerHTML = predicates.map(chip).join(""); $("entityTypeFilter").innerHTML = `${entityTypes .map((type) => ``) .join("")}`; } export async function loadOntologyRegistry() { if (!state.selectedProject) return; const proj = encodeURIComponent(state.selectedProject); const [registry, proposals, gaps, triples] = await Promise.all([ api(`/projects/${proj}/ontology/registry`), api(`/projects/${proj}/ontology/proposals?limit=50`), api(`/projects/${proj}/knowledge-gaps?limit=50`), api(`/projects/${proj}/ontology/triples?limit=50`), ]); $("ontologyRegistryEntityTable").innerHTML = table( [t("table.type"), t("table.domain"), t("table.status"), t("table.confidence")], (registry.entity_types ?? []).map((row) => [ row.name, row.domain, row.status, Math.round((row.confidence ?? 0) * 100) + "%", ]), ); $("ontologyRegistryRelationTable").innerHTML = table( [t("table.relation"), t("table.domain"), t("table.subject"), t("table.object"), t("table.status")], (registry.relation_types ?? []).map((row) => [ row.name, row.domain, (row.allowed_subject_types ?? []).join(", "), (row.allowed_object_types ?? []).join(", ") || (row.semantic_constraints?.literal_value ? "literal" : ""), row.status, ]), ); $("ontologyProposalTable").innerHTML = table( [t("table.type"), t("table.name"), t("table.status"), t("table.reason")], (proposals ?? []).map((row) => [row.proposal_type, row.name, row.status, escapeHtml(row.reason ?? "")]), ); $("knowledgeGapTable").innerHTML = table( [t("table.gap"), t("table.target"), t("table.priority"), t("table.description")], (gaps ?? []).map((row) => [ row.gap_type, `${row.target_type ?? ""}: ${row.target_name ?? ""}`, Math.round((row.priority ?? 0) * 100) + "%", escapeHtml(row.description ?? ""), ]), ); $("ontologyTripleTable").innerHTML = table( [t("table.subject"), t("table.relation"), t("table.object"), t("table.status"), t("table.support")], (triples ?? []).map((row) => [ row.subject ?? "", row.predicate, row.object ?? JSON.stringify(row.object_value ?? ""), row.status, row.support_count, ]), ); } let lastEntities = []; export async function loadEntities() { if (!state.selectedProject) return; const type = $("entityTypeFilter").value; const query = type ? `?entity_type=${encodeURIComponent(type)}&limit=100` : "?limit=100"; const entities = await api(`/projects/${encodeURIComponent(state.selectedProject)}/entities${query}`); lastEntities = entities; $("metricEntities").textContent = entities.length; $("entityTable").innerHTML = table( [t("table.id"), t("table.type"), t("table.name"), t("table.metadata")], entities.map((entity) => [ ``, entity.type, escapeHtml(entity.name), `${escapeHtml(JSON.stringify(entity.metadata ?? {}))}`, ]), ); $("entityTable").querySelectorAll("[data-entity-id]").forEach((btn) => { btn.addEventListener("click", () => { const id = Number(btn.dataset.entityId); const e = lastEntities.find((x) => x.id === id); if (!e) return; state.selection = { kind: "entity", data: e }; refreshInspector(); }); }); } async function mergeEntities() { if (!state.selectedProject) return; const sourceId = Number($("mergeSourceId").value); const targetId = Number($("mergeTargetId").value); if (!sourceId || !targetId || sourceId === targetId) { toast(t("toast.merge_check")); return; } try { const result = await api("/entities/merge", { method: "POST", body: JSON.stringify({ project_name: state.selectedProject, source_entity_id: sourceId, target_entity_id: targetId, }), }); if (!result.ok) { toast(result.error ?? t("toast.merge_check")); return; } toast(t("toast.entity_merged")); $("mergeSourceId").value = ""; $("mergeTargetId").value = ""; const event = new CustomEvent("workspace:refresh"); window.dispatchEvent(event); } catch (error) { toast(error.message); } } export async function loadExtractionLogs() { if (!state.selectedProject) return; const logs = await api(`/projects/${encodeURIComponent(state.selectedProject)}/extraction-logs?limit=50`); $("extractionLogTable").innerHTML = logs.map(renderExtractionLog).join(""); } function renderExtractionLog(log) { const validation = log.validation ?? {}; const context = log.page_context ?? {}; const rejected = validation.rejected_claims ?? []; const rejectedPreview = rejected .slice(0, 3) .map((item) => `${item.predicate ?? ""}: ${item.reason ?? ""}`) .join(" | "); return `
${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); } }