window.XiaobaiPageModules.register("mentor", ["mentorView"], { enter: ["loadMentor"], }); /* PRESERVATION-SOURCE-BEGIN app.js:4337-4827 */ async function loadMentorSetup(force = false) { const requestedDate = elements.tradeDate.value.replaceAll("-", ""); if (!force && state.mentorSetup?.requestedDate === requestedDate) { renderMentorWorkspace(); return; } try { const query = new URLSearchParams({ trade_date: elements.tradeDate.value }); const payload = await apiRequest(`/api/mentors/setup?${query}`); payload.requestedDate = requestedDate; if (!payload.preferences_configured) { payload.mentors.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"); }); payload.mentors.forEach((mentor, index) => { mentor.sort_order = index; }); } state.mentorSetup = payload; const selectedExists = payload.mentors.some((item) => item.id === state.selectedMentorId); state.selectedMentorId = selectedExists ? state.selectedMentorId : payload.mentors[0]?.id || ""; state.mentorMessages = await loadMentorMessages(); renderMentorWorkspace(); } catch (error) { showMentorNotice(error.message || "问师模块加载失败"); showToast(error.message || "问师模块加载失败"); } } function renderMentorWorkspace() { const setup = state.mentorSetup; if (!setup) return; const selected = setup.mentors.find((item) => item.id === state.selectedMentorId) || null; setText("mentorDataDate", `数据日期 ${displayCompactDate(setup.trade_date)}`); setText("activeMentorName", selected?.name || "--"); 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) => `${escapeHtml(item)}`).join(""); renderMentorDirectory(); renderMentorMessages(); } function renderMentorDirectory() { const mentors = state.mentorSetup?.mentors || []; const query = state.mentorQuery; const filtered = mentors.filter((mentor) => { if (state.mentorSortMode) return true; 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); }); setText("mentorCount", filtered.length === mentors.length ? `${mentors.length} 位` : `${filtered.length} / ${mentors.length} 位`); const sortToggle = document.querySelector("#mentorSortToggle"); sortToggle.classList.toggle("active", state.mentorSortMode); sortToggle.setAttribute("aria-pressed", String(state.mentorSortMode)); sortToggle.querySelector("span").textContent = state.mentorSortMode ? "完成" : "整理"; document.querySelector("#mentorSortHint").hidden = !state.mentorSortMode; document.querySelector("#mentorSearchInput").disabled = state.mentorSortMode; document.querySelectorAll("[data-mentor-grade]").forEach((button) => { button.disabled = state.mentorSortMode; }); const container = document.querySelector("#mentorList"); container.classList.toggle("is-sorting", state.mentorSortMode); container.innerHTML = filtered.map((mentor) => { const group = mentors.filter((item) => Boolean(item.pinned) === Boolean(mentor.pinned)); const groupIndex = group.findIndex((item) => item.id === mentor.id); return `
${state.mentorSortMode ? ` ` : ""}
`; }).join(""); document.querySelector("#mentorListEmpty").hidden = filtered.length > 0; document.querySelectorAll("[data-mentor-id]").forEach((button) => { button.addEventListener("click", () => selectMentor(button.dataset.mentorId)); }); document.querySelectorAll("[data-mentor-pin]").forEach((button) => { button.addEventListener("click", () => toggleMentorPin(button.dataset.mentorPin)); }); document.querySelectorAll("[data-mentor-move]").forEach((button) => { button.addEventListener("click", () => moveMentor(button.dataset.mentorTarget, button.dataset.mentorMove)); }); document.querySelectorAll("[data-mentor-card]").forEach((card) => { card.addEventListener("dragstart", handleMentorDragStart); card.addEventListener("dragover", handleMentorDragOver); card.addEventListener("drop", handleMentorDrop); card.addEventListener("dragend", clearMentorDragState); }); refreshIcons(); } function toggleMentorSortMode() { state.mentorSortMode = !state.mentorSortMode; if (state.mentorSortMode) { state.mentorQuery = ""; state.mentorGrade = "all"; document.querySelector("#mentorSearchInput").value = ""; document.querySelectorAll("[data-mentor-grade]").forEach((button) => { button.classList.toggle("active", button.dataset.mentorGrade === "all"); }); } renderMentorDirectory(); } async function toggleMentorPin(mentorId) { if (state.mentorSavingPreferences) return; const mentors = state.mentorSetup?.mentors || []; const index = mentors.findIndex((item) => item.id === mentorId); if (index < 0) return; const [mentor] = mentors.splice(index, 1); mentor.pinned = !mentor.pinned; if (mentor.pinned) { mentors.unshift(mentor); } else { const firstUnpinned = mentors.findIndex((item) => !item.pinned); mentors.splice(firstUnpinned < 0 ? mentors.length : firstUnpinned, 0, mentor); } normalizeMentorOrder(); renderMentorWorkspace(); await persistMentorPreferences(); } async function moveMentor(mentorId, direction) { if (state.mentorSavingPreferences) return; const mentors = state.mentorSetup?.mentors || []; const index = mentors.findIndex((item) => item.id === mentorId); if (index < 0) return; const step = direction === "up" ? -1 : 1; const targetIndex = index + step; if (targetIndex < 0 || targetIndex >= mentors.length) return; if (Boolean(mentors[index].pinned) !== Boolean(mentors[targetIndex].pinned)) return; [mentors[index], mentors[targetIndex]] = [mentors[targetIndex], mentors[index]]; normalizeMentorOrder(); renderMentorDirectory(); await persistMentorPreferences(); } function handleMentorDragStart(event) { if (!state.mentorSortMode || state.mentorSavingPreferences) { event.preventDefault(); return; } state.mentorDragId = event.currentTarget.dataset.mentorCard || ""; event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", state.mentorDragId); event.currentTarget.classList.add("is-dragging"); } function handleMentorDragOver(event) { const source = state.mentorSetup?.mentors.find((item) => item.id === state.mentorDragId); const target = state.mentorSetup?.mentors.find((item) => item.id === event.currentTarget.dataset.mentorCard); if (!source || !target || Boolean(source.pinned) !== Boolean(target.pinned)) return; event.preventDefault(); event.dataTransfer.dropEffect = "move"; event.currentTarget.classList.add("is-drag-over"); } async function handleMentorDrop(event) { event.preventDefault(); const sourceId = state.mentorDragId || event.dataTransfer.getData("text/plain"); const targetId = event.currentTarget.dataset.mentorCard || ""; clearMentorDragState(); if (!sourceId || !targetId || sourceId === targetId) return; const mentors = state.mentorSetup?.mentors || []; const sourceIndex = mentors.findIndex((item) => item.id === sourceId); const targetIndex = mentors.findIndex((item) => item.id === targetId); if (sourceIndex < 0 || targetIndex < 0) return; if (Boolean(mentors[sourceIndex].pinned) !== Boolean(mentors[targetIndex].pinned)) return; const [mentor] = mentors.splice(sourceIndex, 1); const insertionIndex = mentors.findIndex((item) => item.id === targetId); mentors.splice(insertionIndex, 0, mentor); normalizeMentorOrder(); renderMentorDirectory(); await persistMentorPreferences(); } function clearMentorDragState() { state.mentorDragId = ""; document.querySelectorAll(".mentor-option.is-dragging, .mentor-option.is-drag-over").forEach((item) => { item.classList.remove("is-dragging", "is-drag-over"); }); } function normalizeMentorOrder() { (state.mentorSetup?.mentors || []).forEach((mentor, index) => { mentor.sort_order = index; }); } async function persistMentorPreferences() { const mentors = state.mentorSetup?.mentors || []; state.mentorSavingPreferences = true; renderMentorDirectory(); try { await apiRequest("/api/mentors/preferences", "POST", { order: mentors.map((item) => item.id), pinned: mentors.filter((item) => item.pinned).map((item) => item.id), }); } catch (error) { showToast(error.message || "问师顺序保存失败"); await loadMentorSetup(true); } finally { state.mentorSavingPreferences = false; renderMentorDirectory(); } } function renderMentorBadges(mentor, expanded = false) { const badges = []; if (mentor.private) { badges.push('仅自己'); } const grade = mentor.evidence?.grade; if (grade) { badges.push(`${escapeHtml(grade)}`); } 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) { toggleMentorDirectory(false); return; } state.selectedMentorId = mentorId; state.mentorMessages = []; hideMentorNotice(); renderMentorWorkspace(); toggleMentorDirectory(false); state.mentorMessages = await loadMentorMessages(); renderMentorMessages(); } function renderMentorMessages() { const container = document.querySelector("#mentorMessages"); const selected = state.mentorSetup?.mentors.find((item) => item.id === state.selectedMentorId); if (!state.mentorMessages.length && !state.mentorLoading) { container.innerHTML = `
向「${escapeHtml(selected?.name || "问师")}」请教

${escapeHtml(selected?.tagline || selected?.description || "选择一个问题开始对话")}

`; refreshIcons(); } else { container.innerHTML = state.mentorMessages.map((message) => `
${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}
${message.role === "assistant" ? formatMentorAnswer(message.content) : escapeHtml(message.content)}
${message.streaming ? '' : ""} ${message.meta && !message.streaming ? `${escapeHtml(message.meta)}` : ""}
`).join(""); if (state.mentorLoading && !state.mentorMessages.some((message) => message.streaming)) { container.insertAdjacentHTML("beforeend", `
${escapeHtml(selected?.name || "问师")}

正在读取复盘数据并推演...

`); } } document.querySelector("#clearMentorChatButton").disabled = !state.mentorMessages.length || state.mentorLoading; document.querySelector("#mentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId; document.querySelector("#sendMentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId; document.querySelector("#mentorSortToggle").disabled = state.mentorLoading; requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; }); } async function sendMentorQuestion(event) { event.preventDefault(); if (state.mentorLoading || !state.selectedMentorId) return; const input = document.querySelector("#mentorQuestion"); const question = input.value.trim(); if (!question) return; const history = state.mentorMessages.slice(-6).map((item) => ({ role: item.role, content: item.content.slice(0, 3500), })); state.mentorMessages.push({ role: "user", content: question }); const responseMessage = { role: "assistant", content: "", streaming: true, meta: "" }; state.mentorMessages.push(responseMessage); input.value = ""; state.mentorLoading = true; state.mentorController = new AbortController(); hideMentorNotice(); renderMentorMessages(); renderMentorDirectory(); setStatus("问师正在读取复盘数据"); try { await streamMentorRequest( { mentor_id: state.selectedMentorId, trade_date: elements.tradeDate.value, question, history, }, state.mentorController.signal, (chunk) => { responseMessage.content += chunk; scheduleMentorRender(); }, (meta) => { responseMessage.meta = `${displayCompactDate(meta.data_trade_date || elements.tradeDate.value)} · 回答完成`; if (meta.notice) showMentorNotice(meta.notice); }, ); responseMessage.streaming = false; setStatus("问师回答完成"); } catch (error) { responseMessage.streaming = false; responseMessage.error = true; if (!responseMessage.content) { state.mentorMessages = state.mentorMessages.filter((item) => item !== responseMessage); } showMentorNotice(error.message || "问师回答失败"); showToast(error.message || "问师回答失败"); setStatus("问师回答失败"); } finally { state.mentorLoading = false; state.mentorController = null; renderMentorMessages(); renderMentorDirectory(); input.focus(); } } let mentorRenderFrame = 0; function scheduleMentorRender() { if (mentorRenderFrame) return; mentorRenderFrame = requestAnimationFrame(() => { mentorRenderFrame = 0; renderMentorMessages(); }); } async function streamMentorRequest(body, signal, onDelta, onMeta) { await window.XiaobaiAPI.streamNdjson("/api/mentors/chat", { method: "POST", body, signal, errorMessage: "问师暂不可用", onEvent: (event) => { if (event.type === "delta") onDelta(String(event.content || "")); if (event.type === "meta") onMeta(event); }, }); } function useMentorQuickPrompt(prompt) { const input = document.querySelector("#mentorQuestion"); input.value = prompt || ""; input.focus(); } async function clearMentorConversation() { if (!state.mentorMessages.length || !window.confirm("确定清空当前老师的对话记录吗?")) return; try { const query = new URLSearchParams({ mentor_id: state.selectedMentorId, trade_date: state.mentorSetup?.trade_date || elements.tradeDate.value, }); await apiRequest(`/api/mentors/messages?${query}`, "DELETE"); state.mentorMessages = []; hideMentorNotice(); renderMentorMessages(); } catch (error) { showToast(error.message || "对话记录清空失败"); } } async function loadMentorMessages() { if (!state.selectedMentorId) return []; try { const query = new URLSearchParams({ mentor_id: state.selectedMentorId, trade_date: state.mentorSetup?.trade_date || elements.tradeDate.value, }); const payload = await apiRequest(`/api/mentors/messages?${query}`); return (payload.items || []).filter( (item) => ["user", "assistant"].includes(item?.role) && typeof item.content === "string", ).slice(-100); } catch (error) { showMentorNotice(error.message || "对话记录加载失败"); return []; } } function showMentorNotice(message) { const notice = document.querySelector("#mentorNotice"); notice.textContent = message; notice.hidden = false; } function hideMentorNotice() { document.querySelector("#mentorNotice").hidden = true; } function formatMentorAnswer(content) { const blocks = []; let listType = ""; let listItems = []; const flushList = () => { if (!listItems.length) return; blocks.push(`<${listType} class="mentor-answer-list">${listItems.map((item) => `
  • ${item}
  • `).join("")}`); listItems = []; listType = ""; }; String(content || "").replace(/\r\n?/g, "\n").replace(/\n{3,}/g, "\n\n").split("\n").forEach((rawLine) => { const line = rawLine.trim(); if (!line) { flushList(); return; } const heading = line.match(/^#{1,3}\s+(.+)$/); const bullet = line.match(/^[-*]\s+(.+)$/); const ordered = line.match(/^\d+[.、]\s*(.+)$/); if (heading) { flushList(); blocks.push(`${formatMentorInline(escapeHtml(heading[1]))}`); } else if (/^-{3,}$/.test(line)) { flushList(); blocks.push(''); } else if (line.startsWith("> ")) { flushList(); blocks.push(`${formatMentorInline(escapeHtml(line.slice(2)))}`); } else if (bullet || ordered) { const nextType = bullet ? "ul" : "ol"; if (listType && listType !== nextType) flushList(); listType = nextType; listItems.push(formatMentorInline(escapeHtml((bullet || ordered)[1]))); } else { flushList(); blocks.push(`

    ${formatMentorInline(escapeHtml(line))}

    `); } }); flushList(); return blocks.join(""); } function formatMentorInline(content) { return content.replace(/\*\*(.+?)\*\*/g, "$1"); } /* PRESERVATION-SOURCE-END app.js:4337-4827 */