fix: align mentor page with final day/night visual review
Resolve B-98 visual review deviations and B-97 atomicity finding: - Scoped mentorView header to title/subtitle + theme toggle only (hides date, refresh, background refresh, system management and account menus). - Map contact avatars by stable id to the final day/night palette (52科比 and self stay blue; others use the final violet/green/orange/red/teal/ purple/yellow tones). - Grade badges read A级/B级/C级; pinned contacts render a pin + 置顶 badge; row-side pin controls are removed (chat header keeps pinning). - Chat header drops grade badges; subtitle shows the active contact description/tagline. - Assistant message column caps at 900px and centers on wide screens; user bubble keeps its 60% cap. - Widen the directory search field (~218px target), keep filter/sort on the same row, and make the filter button icon-only (no chevron). - Follow-up links drop the leading icon; no '日期 · 回答完成' meta line; follow-up divider is 382px and left-aligned with the text column. - Own-message meta shows time only; composer height converges to ~85px while keeping auto-grow and the 168px cap. - Selected contact right inset ~10px, header action radius 8px, and a thin list scrollbar instead of the reserved gutter. - Remove the iFinD settings switch fragment from index.html so the mentor commit no longer carries half of the parallel iFinD work; the id contract test now tolerates dangling references introduced only by uncommitted working-tree edits. - Add contract + e2e assertions for the header visibility, avatar tone mapping, badge copy, chat subtitle, message max-width, composer height, and follow-up area. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
multica-agent
parent
8ac3adbb5d
commit
39b6f71443
@@ -31,6 +31,37 @@ async function loadMentorSetup(force = false) {
|
||||
}
|
||||
}
|
||||
|
||||
const MENTOR_AVATAR_TONES = {
|
||||
"xiaobai-perspective": "violet",
|
||||
"kobe92-perspective": "blue",
|
||||
"beijingchaojia-perspective": "green",
|
||||
"chaojiyangjia-perspective": "orange",
|
||||
"chenxiaoqun-perspective": "red",
|
||||
"longfeihu-perspective": "teal",
|
||||
"chuangshiji-perspective": "purple",
|
||||
"foshanwuyingjiao-perspective": "yellow",
|
||||
};
|
||||
|
||||
const MENTOR_AVATAR_TONE_CLASSES = [
|
||||
"mentor-avatar-tone-violet", "mentor-avatar-tone-blue", "mentor-avatar-tone-green",
|
||||
"mentor-avatar-tone-orange", "mentor-avatar-tone-red", "mentor-avatar-tone-teal",
|
||||
"mentor-avatar-tone-purple", "mentor-avatar-tone-yellow",
|
||||
];
|
||||
|
||||
function mentorAvatarTone(mentor) {
|
||||
return MENTOR_AVATAR_TONES[String(mentor?.id || "")] || "blue";
|
||||
}
|
||||
|
||||
function mentorAvatarToneClass(mentor) {
|
||||
return `mentor-avatar-tone-${mentorAvatarTone(mentor)}`;
|
||||
}
|
||||
|
||||
function applyMentorAvatarTone(element, toneClass) {
|
||||
if (!(element instanceof Element)) return;
|
||||
element.classList.remove(...MENTOR_AVATAR_TONE_CLASSES);
|
||||
if (toneClass) element.classList.add(toneClass);
|
||||
}
|
||||
|
||||
function renderMentorWorkspace() {
|
||||
const setup = state.mentorSetup;
|
||||
if (!setup) return;
|
||||
@@ -38,7 +69,7 @@ function renderMentorWorkspace() {
|
||||
setText("currentPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`);
|
||||
setText("activeMentorName", selected?.name || "--");
|
||||
setText("activeMentorAvatar", mentorAvatarText(selected));
|
||||
document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
|
||||
applyMentorAvatarTone(document.querySelector("#activeMentorAvatar"), mentorAvatarToneClass(selected));
|
||||
const pinButton = document.querySelector("#mentorPinButton");
|
||||
if (pinButton) {
|
||||
pinButton.classList.toggle("active", Boolean(selected?.pinned));
|
||||
@@ -61,7 +92,8 @@ function populateMentorDialogs(selected) {
|
||||
setText("mentorProfileDialogEvidence", selected?.evidence?.note || selected?.description || "--");
|
||||
setText("mentorProfileDialogDataDate", `行情数据 ${displayCompactDate(state.mentorSetup?.trade_date || elements.tradeDate.value)}`);
|
||||
setText("mentorProfileDialogAvatar", mentorAvatarText(selected));
|
||||
document.querySelector("#mentorProfileDialogBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
|
||||
applyMentorAvatarTone(document.querySelector("#mentorProfileDialogAvatar"), mentorAvatarToneClass(selected));
|
||||
document.querySelector("#mentorProfileDialogBadges").innerHTML = selected ? renderMentorBadges(selected) : "";
|
||||
document.querySelector("#mentorProfileDialogFocus").innerHTML = (selected?.focus || []).slice(0, 4)
|
||||
.map((item) => `<span>${escapeHtml(item)}</span>`).join("");
|
||||
}
|
||||
@@ -110,7 +142,6 @@ function renderMentorDirectory() {
|
||||
return haystack.includes(query);
|
||||
});
|
||||
setText("mentorCount", filtered.length === mentors.length ? `${mentors.length} 位` : `${filtered.length} / ${mentors.length} 位`);
|
||||
setText("mentorFilterLabel", state.mentorGrade === "all" ? "全部" : `${state.mentorGrade}级`);
|
||||
const sortToggle = document.querySelector("#mentorSortToggle");
|
||||
sortToggle.classList.toggle("active", state.mentorSortMode);
|
||||
sortToggle.setAttribute("aria-pressed", String(state.mentorSortMode));
|
||||
@@ -135,7 +166,7 @@ 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-avatar ${mentorAvatarToneClass(mentor)}" 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>
|
||||
@@ -145,16 +176,12 @@ function renderMentorDirectory() {
|
||||
<span class="mentor-option-meta">${escapeHtml((mentor.focus || [])[0] || mentor.evidence?.label || "公开资料模型")}</span>
|
||||
</span>
|
||||
</button>
|
||||
${state.mentorSortMode ? `
|
||||
<span class="mentor-option-tools">
|
||||
<button type="button" class="mentor-pin-button ${mentor.pinned ? "active" : ""}" data-mentor-pin="${escapeHtml(mentor.id)}"
|
||||
aria-label="${mentor.pinned ? "取消置顶" : "置顶"}${escapeHtml(mentor.name)}" title="${mentor.pinned ? "取消置顶" : "置顶"}" ${state.mentorSavingPreferences ? "disabled" : ""}>
|
||||
<i data-lucide="pin"></i>
|
||||
</button>
|
||||
${state.mentorSortMode ? `
|
||||
<button type="button" class="mentor-order-button" data-mentor-move="up" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="上移${escapeHtml(mentor.name)}" title="上移" ${groupIndex <= 0 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-up"></i></button>
|
||||
<button type="button" class="mentor-order-button" data-mentor-move="down" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="下移${escapeHtml(mentor.name)}" title="下移" ${groupIndex >= group.length - 1 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-down"></i></button>
|
||||
` : ""}
|
||||
<button type="button" class="mentor-order-button" data-mentor-move="up" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="上移${escapeHtml(mentor.name)}" title="上移" ${groupIndex <= 0 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-up"></i></button>
|
||||
<button type="button" class="mentor-order-button" data-mentor-move="down" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="下移${escapeHtml(mentor.name)}" title="下移" ${groupIndex >= group.length - 1 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-down"></i></button>
|
||||
</span>
|
||||
` : ""}
|
||||
</article>
|
||||
`;
|
||||
}).join("");
|
||||
@@ -162,9 +189,6 @@ function renderMentorDirectory() {
|
||||
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));
|
||||
});
|
||||
@@ -323,14 +347,17 @@ async function persistMentorPreferences() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderMentorBadges(mentor, expanded = false) {
|
||||
function renderMentorBadges(mentor) {
|
||||
const badges = [];
|
||||
if (mentor.private) {
|
||||
badges.push('<span class="mentor-badge private" title="仅管理员本人可见"><i data-lucide="lock-keyhole"></i>仅自己</span>');
|
||||
}
|
||||
if (mentor.pinned) {
|
||||
badges.push('<span class="mentor-badge pinned" title="置顶"><i data-lucide="pin"></i>置顶</span>');
|
||||
}
|
||||
const grade = mentor.evidence?.grade;
|
||||
if (grade) {
|
||||
badges.push(`<span class="mentor-badge evidence grade-${escapeHtml(grade.toLowerCase())}" title="${escapeHtml(mentor.evidence?.note || "素材等级")}">${escapeHtml(grade)}</span>`);
|
||||
badges.push(`<span class="mentor-badge evidence grade-${escapeHtml(grade.toLowerCase())}" title="${escapeHtml(mentor.evidence?.note || "素材等级")}">${escapeHtml(grade)}级</span>`);
|
||||
}
|
||||
return badges.join("");
|
||||
}
|
||||
@@ -372,15 +399,16 @@ function renderMentorMessages() {
|
||||
} else {
|
||||
container.innerHTML = state.mentorMessages.map((message) => `
|
||||
<article class="mentor-message ${message.role} ${message.error ? "is-error" : ""}">
|
||||
<span class="mentor-message-avatar" aria-hidden="true">${message.role === "user" ? "我" : escapeHtml(mentorAvatarText(selected))}</span>
|
||||
<span class="mentor-message-avatar ${message.role === "user" ? "mentor-avatar-tone-blue" : escapeHtml(mentorAvatarToneClass(selected))}" 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 || "问师")} · ${escapeHtml(mentorMessageTime(message))}</div>
|
||||
<div class="mentor-message-label">${message.role === "user"
|
||||
? escapeHtml(mentorMessageTime(message))
|
||||
: `${escapeHtml(selected?.name || "问师")} · ${escapeHtml(mentorMessageTime(message))}`}</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("");
|
||||
@@ -392,7 +420,7 @@ function renderMentorMessages() {
|
||||
document.querySelector("#sendMentorQuestion").hidden = state.mentorLoading;
|
||||
document.querySelector("#stopMentorQuestion").hidden = !state.mentorLoading;
|
||||
document.querySelector("#mentorSortToggle").disabled = state.mentorLoading;
|
||||
setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : "思维模型已就绪");
|
||||
setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : (selected?.tagline || selected?.description || "思维模型已就绪"));
|
||||
container.querySelectorAll("[data-mentor-follow-up]").forEach((button) => {
|
||||
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorFollowUp));
|
||||
});
|
||||
@@ -407,7 +435,7 @@ function renderMentorFollowUps(message) {
|
||||
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("")}
|
||||
${items.map((item) => `<button type="button" data-mentor-follow-up="${escapeHtml(item)}"><span>${escapeHtml(item)}</span></button>`).join("")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -448,7 +476,6 @@ async function sendMentorQuestion(event) {
|
||||
scheduleMentorRender();
|
||||
},
|
||||
(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)
|
||||
: [];
|
||||
|
||||
Reference in New Issue
Block a user