feat: expand and secure mentor library

This commit is contained in:
leefer
2026-07-23 17:56:52 +08:00
parent d85529dce5
commit 771882ebf0
134 changed files with 4148 additions and 38 deletions
+103 -12
View File
@@ -62,6 +62,9 @@ const state = {
selectedMentorId: "",
mentorMessages: [],
mentorLoading: false,
mentorQuery: "",
mentorGrade: "all",
mentorDirectoryOpen: false,
heavenSetup: null,
heavenManualData: null,
personalField: null,
@@ -436,11 +439,13 @@ function bindEvents() {
if (event.key === "Escape") {
toggleHeaderCommandMenu(false);
toggleAccountDropdown(false, true);
toggleMentorDirectory(false);
}
handleAccountMenuKeydown(event);
});
window.addEventListener("resize", () => {
if (window.innerWidth > 720) toggleHeaderCommandMenu(false);
if (window.innerWidth > 720) toggleMentorDirectory(false);
if (!elements.stockPreview.hidden) closeStockPreview();
updateSidebarControl();
if (state.activeView === "dragonView") layoutDragonCards();
@@ -544,6 +549,24 @@ function bindEvents() {
document.querySelector("#runBacktestToggle").addEventListener("change", updateBacktestTaskStatus);
document.querySelector("#mentorChatForm").addEventListener("submit", sendMentorQuestion);
document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation);
document.querySelector("#mentorDirectoryToggle").addEventListener("click", () => {
toggleMentorDirectory(!state.mentorDirectoryOpen);
});
document.querySelector("#closeMentorDirectory").addEventListener("click", () => toggleMentorDirectory(false));
document.querySelector("#mentorDirectoryBackdrop").addEventListener("click", () => toggleMentorDirectory(false));
document.querySelector("#mentorSearchInput").addEventListener("input", (event) => {
state.mentorQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
renderMentorDirectory();
});
document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
button.addEventListener("click", () => {
state.mentorGrade = button.dataset.mentorGrade || "all";
document.querySelectorAll("[data-mentor-grade]").forEach((item) => {
item.classList.toggle("active", item === button);
});
renderMentorDirectory();
});
});
document.querySelectorAll("[data-mentor-prompt]").forEach((button) => {
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorPrompt));
});
@@ -2037,28 +2060,96 @@ function renderMentorWorkspace() {
const setup = state.mentorSetup;
if (!setup) return;
const selected = setup.mentors.find((item) => item.id === state.selectedMentorId) || null;
setText("mentorCount", `${setup.mentors.length}`);
setText("mentorDataDate", `数据日期 ${displayCompactDate(setup.trade_date)}`);
setText("activeMentorName", selected?.name || "--");
document.querySelector("#mentorList").innerHTML = setup.mentors.map((mentor) => `
<button type="button" class="mentor-option ${mentor.id === state.selectedMentorId ? "active" : ""}" data-mentor-id="${escapeHtml(mentor.id)}">
<strong>${escapeHtml(mentor.name)}</strong>
<span title="${escapeHtml(mentor.description)}">${escapeHtml(mentor.description || "--")}</span>
<small>${mentor.focus.slice(0, 3).map((item) => `<i>${escapeHtml(item)}</i>`).join("")}</small>
</button>
`).join("");
document.querySelectorAll("[data-mentor-id]").forEach((button) => {
button.addEventListener("click", () => selectMentor(button.dataset.mentorId));
});
setText("mobileActiveMentorName", selected?.name || "选择思维模型");
document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--");
document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4)
.map((item) => `<span>${escapeHtml(item)}</span>`).join("");
renderMentorDirectory();
renderMentorMessages();
}
function renderMentorDirectory() {
const mentors = state.mentorSetup?.mentors || [];
const query = state.mentorQuery;
const filtered = mentors.filter((mentor) => {
if (state.mentorGrade !== "all" && mentor.evidence?.grade !== state.mentorGrade) return false;
if (!query) return true;
const haystack = [
mentor.name,
mentor.description,
mentor.tagline,
mentor.evidence?.label,
mentor.evidence?.note,
...(mentor.focus || []),
].filter(Boolean).join(" ").toLocaleLowerCase("zh-CN");
return haystack.includes(query);
}).sort((first, second) => {
if (Boolean(first.private) !== Boolean(second.private)) return first.private ? -1 : 1;
return String(first.name || "").localeCompare(String(second.name || ""), "zh-CN");
});
setText("mentorCount", filtered.length === mentors.length ? `${mentors.length}` : `${filtered.length} / ${mentors.length}`);
const container = document.querySelector("#mentorList");
container.innerHTML = filtered.map((mentor) => `
<button type="button" class="mentor-option ${mentor.id === state.selectedMentorId ? "active" : ""}" data-mentor-id="${escapeHtml(mentor.id)}" aria-pressed="${mentor.id === state.selectedMentorId}">
<span class="mentor-option-copy">
<strong>${escapeHtml(mentor.name)}</strong>
<em title="${escapeHtml(mentor.description || "")}">${escapeHtml(mentor.description || mentor.tagline || "思维模型")}</em>
</span>
<span class="mentor-option-badges">${renderMentorBadges(mentor)}</span>
</button>
`).join("");
document.querySelector("#mentorListEmpty").hidden = filtered.length > 0;
document.querySelectorAll("[data-mentor-id]").forEach((button) => {
button.addEventListener("click", () => selectMentor(button.dataset.mentorId));
});
refreshIcons();
}
function renderMentorBadges(mentor, expanded = false) {
const badges = [];
if (mentor.private) {
badges.push('<span class="mentor-badge private" title="仅管理员本人可见"><i data-lucide="lock-keyhole"></i>仅自己</span>');
}
const grade = mentor.evidence?.grade;
if (grade) {
const label = expanded && mentor.evidence?.label ? `${grade} · ${mentor.evidence.label}` : grade;
badges.push(`<span class="mentor-badge evidence grade-${escapeHtml(grade.toLowerCase())}" title="${escapeHtml(mentor.evidence?.note || "素材等级")}">${escapeHtml(label)}</span>`);
}
const score = mentor.quality?.score;
const total = mentor.quality?.total;
if (Number.isInteger(score) && Number.isInteger(total)) {
const conditional = mentor.quality?.status === "conditional";
badges.push(`<span class="mentor-badge quality ${conditional ? "conditional" : ""}" title="结构质检${conditional ? ",有条件通过" : ""}">${score}/${total}</span>`);
}
return badges.join("");
}
function toggleMentorDirectory(open) {
const mobileOpen = Boolean(open) && window.innerWidth <= 720;
state.mentorDirectoryOpen = mobileOpen;
const sidebar = document.querySelector("#mentorView .mentor-sidebar");
const backdrop = document.querySelector("#mentorDirectoryBackdrop");
const toggle = document.querySelector("#mentorDirectoryToggle");
sidebar.classList.toggle("is-open", mobileOpen);
backdrop.hidden = !mobileOpen;
toggle.setAttribute("aria-expanded", String(mobileOpen));
document.body.classList.toggle("mentor-directory-open", mobileOpen);
if (mobileOpen) requestAnimationFrame(() => document.querySelector("#mentorSearchInput").focus());
}
async function selectMentor(mentorId) {
if (mentorId === state.selectedMentorId) return;
if (mentorId === state.selectedMentorId) {
toggleMentorDirectory(false);
return;
}
state.selectedMentorId = mentorId;
state.mentorMessages = [];
hideMentorNotice();
renderMentorWorkspace();
toggleMentorDirectory(false);
state.mentorMessages = await loadMentorMessages();
renderMentorMessages();
}