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();
}
+29 -4
View File
@@ -805,14 +805,39 @@
<div id="mentorNotice" class="inline-notice" hidden></div>
<div class="mentor-layout">
<aside class="mentor-sidebar">
<div class="workspace-heading"><h3>思维模型</h3><span id="mentorCount">0 位</span></div>
<div id="mentorList" class="mentor-list"></div>
<button id="mentorDirectoryToggle" class="mentor-directory-toggle" type="button" aria-expanded="false" aria-controls="mentorDirectoryContent">
<span><i data-lucide="users-round"></i><span><small>当前思维模型</small><strong id="mobileActiveMentorName">--</strong></span></span>
<i data-lucide="chevron-up"></i>
</button>
<div id="mentorDirectoryBackdrop" class="mentor-directory-backdrop" hidden></div>
<div id="mentorDirectoryContent" class="mentor-directory-content">
<div class="workspace-heading mentor-directory-heading">
<div><h3>思维模型</h3><span id="mentorCount">0 位</span></div>
<button id="closeMentorDirectory" class="icon-button mentor-directory-close" type="button" aria-label="关闭思维模型目录" title="关闭"><i data-lucide="x"></i></button>
</div>
<label class="mentor-search-field">
<span class="visually-hidden">搜索思维模型</span>
<i data-lucide="search"></i>
<input id="mentorSearchInput" type="search" maxlength="50" placeholder="搜索姓名、模式或标签" autocomplete="off">
</label>
<div class="mentor-evidence-filters" role="group" aria-label="按素材等级筛选">
<button class="active" type="button" data-mentor-grade="all">全部</button>
<button type="button" data-mentor-grade="A">A 原始</button>
<button type="button" data-mentor-grade="B">B 多源</button>
<button type="button" data-mentor-grade="C">C 推演</button>
</div>
<div id="mentorList" class="mentor-list"></div>
<div id="mentorListEmpty" class="mentor-list-empty" hidden>没有符合条件的思维模型</div>
<p class="mentor-evidence-legend">素材等级反映蒸馏依据,不代表人物能力或收益水平。</p>
</div>
</aside>
<section class="mentor-chat-panel">
<header class="mentor-chat-header">
<div>
<div class="mentor-active-profile">
<span class="metric-label">当前问师</span>
<h3 id="activeMentorName">--</h3>
<div class="mentor-active-title"><h3 id="activeMentorName">--</h3><span id="activeMentorBadges" class="mentor-active-badges"></span></div>
<p id="activeMentorEvidence">--</p>
<div id="activeMentorFocus" class="mentor-active-focus"></div>
</div>
</header>
<div id="mentorMessages" class="mentor-messages" aria-live="polite"></div>
+467
View File
@@ -11753,3 +11753,470 @@ button.account-role-badge:focus-visible { outline: 2px solid var(--blue); outlin
@media (prefers-reduced-motion: reduce) {
.heaven-reading-mark { animation: none; }
}
/* Mentor directory: dense evidence-aware navigation for larger skill libraries. */
.mentor-layout {
height: clamp(620px, calc(100dvh - 198px), 760px);
min-height: 620px;
grid-template-columns: 320px minmax(0, 1fr);
overflow: hidden;
}
.mentor-sidebar {
min-height: 0;
position: relative;
padding: 0;
overflow: hidden;
}
.mentor-directory-toggle,
.mentor-directory-close,
.mentor-directory-backdrop {
display: none;
}
.mentor-directory-content {
height: 100%;
min-height: 0;
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
gap: 10px;
padding: 14px 12px 10px;
}
.mentor-directory-heading {
min-height: 34px;
padding: 0 2px;
}
.mentor-directory-heading > div {
display: flex;
align-items: baseline;
gap: 8px;
}
.mentor-directory-heading h3 {
margin: 0;
font-size: 14px;
}
.mentor-directory-heading span {
color: var(--text-muted);
font-size: 11px;
}
.mentor-search-field {
height: 40px;
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
align-items: center;
gap: 8px;
padding: 0 10px;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--surface);
color: var(--text-muted);
}
.mentor-search-field:focus-within {
border-color: var(--action);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--action) 14%, transparent);
}
.mentor-search-field .lucide {
width: 16px;
height: 16px;
}
.mentor-search-field input {
width: 100%;
min-width: 0;
height: 38px;
padding: 0;
border: 0;
outline: 0;
background: transparent;
color: var(--text);
font: inherit;
font-size: 12px;
}
.mentor-evidence-filters {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 4px;
padding: 3px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
}
.mentor-evidence-filters button {
min-width: 0;
min-height: 30px;
padding: 0 4px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
font: inherit;
font-size: 10px;
white-space: nowrap;
}
.mentor-evidence-filters button:hover,
.mentor-evidence-filters button.active {
background: var(--action-soft);
color: var(--action);
}
.mentor-evidence-filters button:focus-visible {
outline: 2px solid var(--action);
outline-offset: 1px;
}
.mentor-list {
min-height: 0;
display: grid;
align-content: start;
grid-template-columns: minmax(0, 1fr);
gap: 2px;
padding-right: 3px;
overflow-y: auto;
scrollbar-width: thin;
}
.mentor-option {
min-height: 66px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
padding: 8px 9px;
border: 1px solid transparent;
border-bottom-color: var(--border);
border-radius: 4px;
background: transparent;
transition: border-color var(--motion-fast) ease, background-color var(--motion-fast) ease;
}
.mentor-option:hover {
border-color: var(--border-strong);
background: var(--surface);
}
.mentor-option.active {
border-color: color-mix(in srgb, var(--action) 42%, var(--border));
background: var(--action-soft);
box-shadow: inset 3px 0 var(--action);
}
.mentor-option:focus-visible {
outline: 2px solid var(--action);
outline-offset: -2px;
}
.mentor-option-copy {
min-width: 0;
display: grid;
gap: 4px;
}
.mentor-option-copy strong {
overflow: hidden;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.mentor-option-copy em {
overflow: hidden;
color: var(--text-muted);
font-size: 10px;
font-style: normal;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
.mentor-option-badges,
.mentor-active-badges {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 4px;
}
.mentor-option-badges {
max-width: 76px;
}
.mentor-badge {
min-height: 21px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 3px;
padding: 0 6px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--text-secondary);
font-size: 9px;
font-style: normal;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.mentor-badge .lucide {
width: 11px;
height: 11px;
}
.mentor-badge.grade-a { border-color: #a9c9b9; background: #eef7f1; color: #2f6a50; }
.mentor-badge.grade-b { border-color: #b3c8db; background: #eff5fa; color: #315f83; }
.mentor-badge.grade-c { border-color: #d9c49c; background: #faf5e9; color: #805d24; }
.mentor-badge.private { border-color: #d7c28e; background: #fff8e5; color: #7a5d16; }
.mentor-badge.quality { background: var(--surface-muted); color: var(--text-muted); }
.mentor-badge.quality.conditional { border-style: dashed; color: #805d24; }
.mentor-list-empty {
padding: 28px 12px;
color: var(--text-muted);
font-size: 12px;
text-align: center;
}
.mentor-evidence-legend {
margin: 0;
padding: 7px 3px 0;
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 9px;
line-height: 1.55;
}
.mentor-chat-panel {
min-height: 0;
height: 100%;
grid-template-rows: auto minmax(0, 1fr) auto auto auto;
}
.mentor-chat-header {
min-height: 96px;
padding: 12px 18px;
}
.mentor-active-profile {
min-width: 0;
display: grid;
gap: 5px;
}
.mentor-active-title {
min-width: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.mentor-active-title h3 {
margin: 0;
}
.mentor-active-profile > p {
max-width: 760px;
margin: 0;
overflow: hidden;
color: var(--text-secondary);
font-size: 11px;
line-height: 1.45;
text-overflow: ellipsis;
white-space: nowrap;
}
.mentor-active-focus {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.mentor-active-focus span {
padding: 2px 6px;
border-radius: 3px;
background: var(--surface-muted);
color: var(--text-secondary);
font-size: 9px;
}
.mentor-messages {
max-height: none;
}
@media (min-width: 721px) and (max-width: 1023px) {
.mentor-layout {
grid-template-columns: 280px minmax(0, 1fr);
}
.mentor-sidebar {
max-height: none;
overflow: hidden;
border-right: 1px solid var(--line);
border-bottom: 0;
}
.mentor-list {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 720px) {
body.mentor-directory-open {
overflow: hidden;
}
.mentor-layout {
height: auto;
min-height: 580px;
grid-template-columns: minmax(0, 1fr);
overflow: visible;
}
.mentor-sidebar {
height: 56px;
min-height: 56px;
max-height: none;
overflow: visible;
border-right: 0;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.mentor-directory-toggle {
width: 100%;
min-height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 12px;
border: 0;
background: var(--surface);
color: var(--text);
cursor: pointer;
text-align: left;
}
.mentor-directory-toggle > span {
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}
.mentor-directory-toggle > span > span {
min-width: 0;
display: grid;
gap: 2px;
}
.mentor-directory-toggle small {
color: var(--text-muted);
font-size: 9px;
}
.mentor-directory-toggle strong {
overflow: hidden;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.mentor-directory-toggle > .lucide {
transition: transform var(--motion-medium) var(--ease-out);
}
.mentor-sidebar.is-open .mentor-directory-toggle > .lucide {
transform: rotate(180deg);
}
.mentor-directory-backdrop {
position: fixed;
inset: 0;
z-index: 79;
display: block;
background: rgba(20, 27, 33, 0.48);
}
.mentor-directory-content {
height: auto;
position: fixed;
inset: 66px 8px 72px;
z-index: 80;
padding: 14px 12px 10px;
overflow: hidden;
border: 1px solid var(--border-strong);
border-radius: 8px;
background: var(--surface-muted);
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transform: translateY(12px);
transition: opacity var(--motion-medium) ease, transform var(--motion-medium) var(--ease-out);
}
.mentor-sidebar.is-open .mentor-directory-content {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.mentor-directory-close {
width: 44px;
min-height: 44px;
display: grid;
}
.mentor-search-field {
height: 44px;
}
.mentor-search-field input {
height: 42px;
font-size: 16px;
}
.mentor-evidence-filters button {
min-height: 44px;
font-size: 11px;
}
.mentor-option {
min-height: 68px;
}
.mentor-chat-panel {
min-height: 580px;
}
.mentor-chat-header {
min-height: 104px;
padding: 11px 12px;
}
.mentor-active-profile > p {
max-width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.mentor-directory-content,
.mentor-directory-toggle > .lucide,
.mentor-option {
transition: none;
}
}