chore: create Multica handoff checkpoint

This commit is contained in:
leefer
2026-08-06 22:54:46 +08:00
parent bd97ba1829
commit 33f9db43b1
51 changed files with 3054 additions and 2883 deletions
+75 -51
View File
@@ -37,8 +37,14 @@ function renderMentorWorkspace() {
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 || "选择思维模型");
setText("mentorProfileName", selected?.name || "--");
setText("mentorProfileTagline", selected?.tagline || selected?.description || "--");
setText("mentorProfileSource", selected?.evidence?.label || "公开资料整理");
setText("mentorProfileDataDate", `行情数据 ${displayCompactDate(setup.trade_date)}`);
setText("activeMentorAvatar", mentorAvatarText(selected));
setText("mentorProfileAvatar", mentorAvatarText(selected));
document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
document.querySelector("#mentorProfileBadges").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("");
@@ -82,16 +88,14 @@ function renderMentorDirectory() {
<article class="mentor-option ${mentor.id === state.selectedMentorId ? "active" : ""} ${mentor.pinned ? "is-pinned" : ""}"
data-mentor-card="${escapeHtml(mentor.id)}" draggable="${state.mentorSortMode && !state.mentorSavingPreferences}">
<button type="button" class="mentor-option-main" data-mentor-id="${escapeHtml(mentor.id)}" aria-pressed="${mentor.id === state.selectedMentorId}" ${state.mentorLoading ? "disabled" : ""}>
<span class="mentor-avatar" data-grade="${escapeHtml(String(mentor.evidence?.grade || "").toLowerCase())}" aria-hidden="true">${escapeHtml(mentorAvatarText(mentor))}</span>
<span class="mentor-option-copy">
<span class="mentor-option-heading">
<strong>${escapeHtml(mentor.name)}</strong>
<span class="mentor-option-badges">${renderMentorBadges(mentor)}</span>
</span>
<em title="${escapeHtml(mentor.description || "")}">${escapeHtml(mentor.description || mentor.tagline || "思维模型")}</em>
<span class="mentor-option-meta">
${mentor.evidence?.label ? `<span class="mentor-evidence-source" title="${escapeHtml(mentor.evidence?.note || "素材说明")}">${escapeHtml(mentor.evidence.label)}</span>` : ""}
${(mentor.focus || []).slice(0, 2).map((item) => `<span>#${escapeHtml(item)}</span>`).join("")}
</span>
<span class="mentor-option-meta">${escapeHtml((mentor.focus || [])[0] || mentor.evidence?.label || "公开资料模型")}</span>
</span>
</button>
<span class="mentor-option-tools">
@@ -254,29 +258,16 @@ function renderMentorBadges(mentor, expanded = false) {
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());
function mentorAvatarText(mentor) {
return Array.from(String(mentor?.name || "师").trim())[0] || "师";
}
async function selectMentor(mentorId) {
if (mentorId === state.selectedMentorId) {
toggleMentorDirectory(false);
return;
}
if (mentorId === state.selectedMentorId) return;
state.selectedMentorId = mentorId;
state.mentorMessages = [];
hideMentorNotice();
renderMentorWorkspace();
toggleMentorDirectory(false);
state.mentorMessages = await loadMentorMessages();
renderMentorMessages();
}
@@ -289,35 +280,53 @@ function renderMentorMessages() {
<div class="mentor-empty-state">
<span class="mentor-empty-mark" aria-hidden="true"><i data-lucide="messages-square"></i></span>
<strong>向「${escapeHtml(selected?.name || "问师")}」请教</strong>
<p>${escapeHtml(selected?.tagline || selected?.description || "选择一个问题开始对话")}</p>
<p>${escapeHtml(selected?.tagline || selected?.description || "从一个具体问题开始对话")}</p>
</div>
`;
refreshIcons();
} else {
container.innerHTML = state.mentorMessages.map((message) => `
<article class="mentor-message ${message.role} ${message.error ? "is-error" : ""}">
<div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
<div class="mentor-message-content">${message.role === "assistant" ? formatMentorAnswer(message.content) : escapeHtml(message.content)}</div>
${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
${message.meta && !message.streaming ? `<small>${escapeHtml(message.meta)}</small>` : ""}
<span class="mentor-message-avatar" aria-hidden="true">${message.role === "user" ? "我" : escapeHtml(mentorAvatarText(selected))}</span>
<div class="mentor-message-body">
<div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
<div class="mentor-message-content">${message.role === "assistant"
? (message.content ? formatMentorAnswer(message.content) : '<p class="mentor-loading-copy">正在读取复盘数据并推演...</p>')
: escapeHtml(message.content)}</div>
${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
${renderMentorFollowUps(message)}
${message.meta && !message.streaming ? `<small>${escapeHtml(message.meta)}</small>` : ""}
</div>
</article>
`).join("");
if (state.mentorLoading && !state.mentorMessages.some((message) => message.streaming)) {
container.insertAdjacentHTML("beforeend", `
<article class="mentor-message assistant loading-message">
<div class="mentor-message-label">${escapeHtml(selected?.name || "问师")}</div>
<p>正在读取复盘数据并推演...</p>
</article>
`);
}
}
document.querySelector("#mentorQuickPrompts").hidden = state.mentorMessages.length > 0 || state.mentorLoading;
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("#sendMentorQuestion").hidden = state.mentorLoading;
document.querySelector("#stopMentorQuestion").hidden = !state.mentorLoading;
document.querySelector("#mentorSortToggle").disabled = state.mentorLoading;
setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : "思维模型已就绪");
container.querySelectorAll("[data-mentor-follow-up]").forEach((button) => {
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorFollowUp));
});
refreshIcons();
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
}
function renderMentorFollowUps(message) {
if (message.role !== "assistant" || message.streaming || message.error || !Array.isArray(message.followUps)) return "";
const items = message.followUps.filter(Boolean).slice(0, 3);
if (items.length < 2) return "";
return `
<div class="mentor-follow-ups" aria-label="继续追问">
<span>继续追问</span>
${items.map((item) => `<button type="button" data-mentor-follow-up="${escapeHtml(item)}"><i data-lucide="corner-down-right"></i><span>${escapeHtml(item)}</span></button>`).join("")}
</div>
`;
}
async function sendMentorQuestion(event) {
event.preventDefault();
if (state.mentorLoading || !state.selectedMentorId) return;
@@ -328,8 +337,9 @@ async function sendMentorQuestion(event) {
role: item.role,
content: item.content.slice(0, 3500),
}));
state.mentorMessages.forEach((message) => { delete message.followUps; });
state.mentorMessages.push({ role: "user", content: question });
const responseMessage = { role: "assistant", content: "", streaming: true, meta: "" };
const responseMessage = { role: "assistant", content: "", streaming: true, meta: "", followUps: [] };
state.mentorMessages.push(responseMessage);
input.value = "";
state.mentorLoading = true;
@@ -353,6 +363,9 @@ async function sendMentorQuestion(event) {
},
(meta) => {
responseMessage.meta = `${displayCompactDate(meta.data_trade_date || elements.tradeDate.value)} · 回答完成`;
responseMessage.followUps = Array.isArray(meta.follow_ups)
? meta.follow_ups.filter((item) => typeof item === "string" && item.trim()).slice(0, 3)
: [];
if (meta.notice) showMentorNotice(meta.notice);
},
);
@@ -360,13 +373,23 @@ async function sendMentorQuestion(event) {
setStatus("问师回答完成");
} catch (error) {
responseMessage.streaming = false;
responseMessage.error = true;
if (!responseMessage.content) {
state.mentorMessages = state.mentorMessages.filter((item) => item !== responseMessage);
responseMessage.followUps = [];
if (state.mentorController?.signal.aborted) {
if (responseMessage.content) {
responseMessage.meta = "生成已停止";
} else {
state.mentorMessages = state.mentorMessages.filter((item) => item !== responseMessage);
}
setStatus("已停止问师回答");
} else {
responseMessage.error = true;
if (!responseMessage.content) {
state.mentorMessages = state.mentorMessages.filter((item) => item !== responseMessage);
}
showMentorNotice(error.message || "问师回答失败");
showToast(error.message || "问师回答失败");
setStatus("问师回答失败");
}
showMentorNotice(error.message || "问师回答失败");
showToast(error.message || "问师回答失败");
setStatus("问师回答失败");
} finally {
state.mentorLoading = false;
state.mentorController = null;
@@ -376,6 +399,12 @@ async function sendMentorQuestion(event) {
}
}
function stopMentorGeneration() {
if (!state.mentorLoading || !state.mentorController) return;
state.mentorController.abort();
setText("activeMentorStatus", "正在停止...");
}
let mentorRenderFrame = 0;
function scheduleMentorRender() {
@@ -497,12 +526,8 @@ function formatMentorInline(content) {
function bindMentorEvents() {
document.querySelector("#mentorChatForm").addEventListener("submit", sendMentorQuestion);
document.querySelector("#stopMentorQuestion").addEventListener("click", stopMentorGeneration);
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("#mentorSortToggle").addEventListener("click", toggleMentorSortMode);
document.querySelector("#mentorSearchInput").addEventListener("input", (event) => {
state.mentorQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
@@ -520,10 +545,9 @@ function bindMentorEvents() {
document.querySelectorAll("[data-mentor-prompt]").forEach((button) => {
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorPrompt));
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") toggleMentorDirectory(false);
});
window.addEventListener("resize", () => {
if (window.innerWidth > 720) toggleMentorDirectory(false);
document.querySelector("#mentorQuestion").addEventListener("keydown", (event) => {
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
event.preventDefault();
document.querySelector("#mentorChatForm").requestSubmit();
});
}