feat(knowledge-base): 增加全局知识库管理
This commit is contained in:
238
static/js/knowledge_base.js
Normal file
238
static/js/knowledge_base.js
Normal file
@@ -0,0 +1,238 @@
|
||||
(function () {
|
||||
var page = document.querySelector(".knowledge-page");
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
|
||||
var documentForm = document.getElementById("knowledgeDocumentForm");
|
||||
var documentStatus = document.getElementById("knowledgeDocumentStatus");
|
||||
var documentTable = document.getElementById("knowledgeDocumentTable");
|
||||
var documentSearch = document.getElementById("knowledgeDocumentSearch");
|
||||
var searchForm = document.getElementById("knowledgeSearchForm");
|
||||
var queryInput = document.getElementById("knowledgeSearchQuery");
|
||||
var results = document.getElementById("knowledgeSearchResults");
|
||||
var sourceSearch = document.getElementById("knowledgeSourceSearch");
|
||||
var sourceTable = document.getElementById("knowledgeSourceTable");
|
||||
var documentFileInput = document.getElementById("knowledgeDocumentFile");
|
||||
var uploadDropzone = document.getElementById("knowledgeUploadDropzone");
|
||||
|
||||
function csrfToken() {
|
||||
var cookie = document.cookie.split("; ").find(function (item) {
|
||||
return item.indexOf("csrftoken=") === 0;
|
||||
});
|
||||
return cookie ? decodeURIComponent(cookie.split("=")[1]) : "";
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
async function patchDocument(row, payload) {
|
||||
var response = await fetch(row.getAttribute("data-detail-url"), {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": csrfToken(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("知识库材料更新失败。");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function deleteDocument(row) {
|
||||
var response = await fetch(row.getAttribute("data-detail-url"), {
|
||||
method: "DELETE",
|
||||
headers: { "X-CSRFToken": csrfToken() },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("知识库材料删除失败。");
|
||||
}
|
||||
}
|
||||
|
||||
async function indexDocument(row) {
|
||||
var response = await fetch(row.getAttribute("data-index-url"), {
|
||||
method: "POST",
|
||||
headers: { "X-CSRFToken": csrfToken() },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("知识库材料解析入库失败。");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function renderResults(payload) {
|
||||
if (!results) {
|
||||
return;
|
||||
}
|
||||
if (payload.error_message) {
|
||||
results.innerHTML = '<p class="knowledge-search-error">' + escapeHtml(payload.error_message) + "</p>";
|
||||
return;
|
||||
}
|
||||
if (!payload.results || !payload.results.length) {
|
||||
results.innerHTML = '<p class="panel-empty">未检索到依据片段。</p>';
|
||||
return;
|
||||
}
|
||||
results.innerHTML = payload.results
|
||||
.map(function (item, index) {
|
||||
return [
|
||||
'<article class="knowledge-result">',
|
||||
"<header><strong>结果 " + (index + 1) + "</strong><span>" + escapeHtml(item.source || "法规材料") + "</span></header>",
|
||||
"<p>" + escapeHtml(item.text || "").slice(0, 600) + "</p>",
|
||||
item.score === null || item.score === undefined ? "" : "<em>score: " + escapeHtml(item.score) + "</em>",
|
||||
"</article>",
|
||||
].join("");
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
if (documentForm) {
|
||||
documentForm.addEventListener("submit", async function (event) {
|
||||
event.preventDefault();
|
||||
var formData = new FormData(documentForm);
|
||||
if (documentStatus) {
|
||||
documentStatus.textContent = "上传并解析入库中...";
|
||||
}
|
||||
try {
|
||||
var response = await fetch(page.getAttribute("data-document-url"), {
|
||||
method: "POST",
|
||||
headers: { "X-CSRFToken": csrfToken() },
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("新增材料失败。");
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
if (documentStatus) {
|
||||
documentStatus.textContent = error.message || "新增材料失败。";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (documentFileInput && documentStatus) {
|
||||
documentFileInput.addEventListener("change", function () {
|
||||
var file = documentFileInput.files && documentFileInput.files[0];
|
||||
documentStatus.textContent = file
|
||||
? "已选择:" + file.name
|
||||
: "上传后会进入当前账号的全局知识库。";
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadDropzone && documentFileInput) {
|
||||
uploadDropzone.addEventListener("click", function () {
|
||||
documentFileInput.click();
|
||||
});
|
||||
uploadDropzone.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
documentFileInput.click();
|
||||
}
|
||||
});
|
||||
["dragenter", "dragover"].forEach(function (eventName) {
|
||||
uploadDropzone.addEventListener(eventName, function (event) {
|
||||
event.preventDefault();
|
||||
uploadDropzone.classList.add("dragging");
|
||||
});
|
||||
});
|
||||
["dragleave", "drop"].forEach(function (eventName) {
|
||||
uploadDropzone.addEventListener(eventName, function (event) {
|
||||
event.preventDefault();
|
||||
uploadDropzone.classList.remove("dragging");
|
||||
});
|
||||
});
|
||||
uploadDropzone.addEventListener("drop", function (event) {
|
||||
var files = event.dataTransfer && event.dataTransfer.files;
|
||||
if (!files || !files.length) {
|
||||
return;
|
||||
}
|
||||
documentFileInput.files = files;
|
||||
documentFileInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
if (documentTable) {
|
||||
documentTable.addEventListener("click", async function (event) {
|
||||
var button = event.target.closest("[data-kb-action]");
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
var row = button.closest("tr[data-document-id]");
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
var action = button.getAttribute("data-kb-action");
|
||||
try {
|
||||
if (action === "edit") {
|
||||
var nameCell = row.querySelector(".attachment-name");
|
||||
var nextName = window.prompt("请输入新的材料名称", nameCell ? nameCell.textContent.trim() : "");
|
||||
if (nextName) {
|
||||
await patchDocument(row, { display_name: nextName });
|
||||
window.location.reload();
|
||||
}
|
||||
} else if (action === "toggle") {
|
||||
await patchDocument(row, { is_active: button.textContent.trim() === "启用" });
|
||||
window.location.reload();
|
||||
} else if (action === "index") {
|
||||
button.disabled = true;
|
||||
button.textContent = "解析中";
|
||||
await indexDocument(row);
|
||||
window.location.reload();
|
||||
} else if (action === "delete" && window.confirm("确认删除该知识库材料?")) {
|
||||
await deleteDocument(row);
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(error.message || "知识库材料操作失败。");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchForm && queryInput) {
|
||||
searchForm.addEventListener("submit", async function (event) {
|
||||
event.preventDefault();
|
||||
var query = queryInput.value.trim();
|
||||
if (!query) {
|
||||
renderResults({ error_message: "请输入检索问题。" });
|
||||
return;
|
||||
}
|
||||
results.innerHTML = '<p class="panel-empty">检索中...</p>';
|
||||
try {
|
||||
var response = await fetch(page.getAttribute("data-search-url"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": csrfToken(),
|
||||
},
|
||||
body: JSON.stringify({ query: query }),
|
||||
});
|
||||
renderResults(await response.json());
|
||||
} catch (error) {
|
||||
renderResults({ error_message: "检索失败,请稍后重试。" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindTableSearch(input, table, selector) {
|
||||
if (!input || !table) {
|
||||
return;
|
||||
}
|
||||
input.addEventListener("input", function () {
|
||||
var keyword = input.value.trim().toLowerCase();
|
||||
table.querySelectorAll(selector).forEach(function (row) {
|
||||
row.hidden = keyword && row.textContent.toLowerCase().indexOf(keyword) === -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bindTableSearch(documentSearch, documentTable, "tbody tr[data-document-id]");
|
||||
bindTableSearch(sourceSearch, sourceTable, "tbody tr[data-source-name]");
|
||||
})();
|
||||
Reference in New Issue
Block a user