feat: restore mentor page to final day/night spec
Rebuild the mentor workspace per the approved final design (day.png/night.png): - mentorView gets a dedicated full-width immersive shell (independent 56px top bar with page title slot + theme mode text, hidden module-nav/market tape/overview/status bar) scoped to body[data-active-view=mentorView]. - Assistant messages become borderless body text with name/time above; only user messages use a blue bubble; keep loading/error/streaming caret states. - 4 quick topics stay visible with history; composer restored to a framed ~94px card with bottom-left shortcut hint and bottom-right send button. - Directory tools merged into one row (search + filter menu + sort); the filter menu still offers all/A/B/C; list selected state is an inset rounded fill; contact rows are borderless 75px items. - Chat header always shows pin/note/profile/clear; pin reuses /api/mentors/ preferences; theme toggle stays the single #themeToggle. - Night tokens match the spec including the two distinct blues (#316FEF link, #5B8DEF quote/selected icon). Mobile (<768) stacks panels with no horizontal overflow; 1024+ follows the desktop spec. - Update stale test baselines (300px sidebar, 94px composer, hint presence, night bubble color, centered disclaimer) and regenerate the architecture inventory. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
multica-agent
parent
33f9db43b1
commit
8ac3adbb5d
@@ -35,23 +35,63 @@ 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("currentPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`);
|
||||
setText("activeMentorName", 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("");
|
||||
const pinButton = document.querySelector("#mentorPinButton");
|
||||
if (pinButton) {
|
||||
pinButton.classList.toggle("active", Boolean(selected?.pinned));
|
||||
pinButton.setAttribute("aria-label", selected?.pinned ? "取消置顶当前思维模型" : "置顶当前思维模型");
|
||||
pinButton.setAttribute("aria-pressed", String(Boolean(selected?.pinned)));
|
||||
}
|
||||
populateMentorDialogs(selected);
|
||||
renderMentorDirectory();
|
||||
renderMentorMessages();
|
||||
}
|
||||
|
||||
function populateMentorDialogs(selected) {
|
||||
const noteInput = document.querySelector("#mentorNoteInput");
|
||||
if (noteInput) {
|
||||
noteInput.value = loadMentorNote(selected);
|
||||
}
|
||||
setText("mentorProfileDialogName", selected?.name || "--");
|
||||
setText("mentorProfileDialogTagline", selected?.tagline || selected?.description || "--");
|
||||
setText("mentorProfileDialogSource", selected?.evidence?.label || "公开资料整理");
|
||||
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) : "";
|
||||
document.querySelector("#mentorProfileDialogFocus").innerHTML = (selected?.focus || []).slice(0, 4)
|
||||
.map((item) => `<span>${escapeHtml(item)}</span>`).join("");
|
||||
}
|
||||
|
||||
function mentorNoteStorageKey(selected) {
|
||||
const accountId = state.user?.id || state.user?.username || "anon";
|
||||
return `xiaobai-mentor-note-${accountId}-${String(selected?.id || "")}`;
|
||||
}
|
||||
|
||||
function loadMentorNote(selected) {
|
||||
if (!selected) return "";
|
||||
try {
|
||||
return window.localStorage.getItem(mentorNoteStorageKey(selected)) || "";
|
||||
} catch (_error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function saveMentorNote() {
|
||||
const selected = selectedMentor();
|
||||
if (!selected) return;
|
||||
const input = document.querySelector("#mentorNoteInput");
|
||||
if (!input) return;
|
||||
try {
|
||||
window.localStorage.setItem(mentorNoteStorageKey(selected), input.value);
|
||||
} catch (_error) {
|
||||
showToast("备注保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
function renderMentorDirectory() {
|
||||
const mentors = state.mentorSetup?.mentors || [];
|
||||
const query = state.mentorQuery;
|
||||
@@ -70,15 +110,22 @@ 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));
|
||||
sortToggle.querySelector("span").textContent = state.mentorSortMode ? "完成" : "整理";
|
||||
document.querySelector("#mentorSortHint").hidden = !state.mentorSortMode;
|
||||
document.querySelector("#mentorSearchInput").disabled = state.mentorSortMode;
|
||||
document.querySelector("#mentorFilterToggle").disabled = state.mentorSortMode;
|
||||
document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
|
||||
button.disabled = state.mentorSortMode;
|
||||
});
|
||||
const filterOptions = document.querySelector("#mentorFilterOptions");
|
||||
if (state.mentorSortMode && filterOptions && !filterOptions.hidden) {
|
||||
filterOptions.hidden = true;
|
||||
document.querySelector("#mentorFilterToggle").setAttribute("aria-expanded", "false");
|
||||
}
|
||||
const container = document.querySelector("#mentorList");
|
||||
container.classList.toggle("is-sorting", state.mentorSortMode);
|
||||
container.innerHTML = filtered.map((mentor) => {
|
||||
@@ -143,6 +190,36 @@ function toggleMentorSortMode() {
|
||||
renderMentorDirectory();
|
||||
}
|
||||
|
||||
function toggleMentorFilterMenu() {
|
||||
if (state.mentorSortMode || state.mentorLoading) return;
|
||||
const options = document.querySelector("#mentorFilterOptions");
|
||||
const toggle = document.querySelector("#mentorFilterToggle");
|
||||
if (!options || !toggle) return;
|
||||
const open = options.hidden;
|
||||
options.hidden = !open;
|
||||
toggle.setAttribute("aria-expanded", String(open));
|
||||
if (open) {
|
||||
const active = options.querySelector("[data-mentor-grade].active");
|
||||
(active || options.querySelector("[data-mentor-grade]"))?.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
function closeMentorFilterMenu() {
|
||||
const options = document.querySelector("#mentorFilterOptions");
|
||||
const toggle = document.querySelector("#mentorFilterToggle");
|
||||
if (!options || options.hidden) return;
|
||||
options.hidden = true;
|
||||
toggle?.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
function selectMentorGrade(grade) {
|
||||
state.mentorGrade = grade || "all";
|
||||
document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.mentorGrade === state.mentorGrade);
|
||||
});
|
||||
renderMentorDirectory();
|
||||
}
|
||||
|
||||
async function toggleMentorPin(mentorId) {
|
||||
if (state.mentorSavingPreferences) return;
|
||||
const mentors = state.mentorSetup?.mentors || [];
|
||||
@@ -262,6 +339,14 @@ function mentorAvatarText(mentor) {
|
||||
return Array.from(String(mentor?.name || "师").trim())[0] || "师";
|
||||
}
|
||||
|
||||
function mentorMessageTime(message) {
|
||||
const parsed = new Date(String(message?.created_at || ""));
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
}
|
||||
return parsed.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
}
|
||||
|
||||
async function selectMentor(mentorId) {
|
||||
if (mentorId === state.selectedMentorId) return;
|
||||
state.selectedMentorId = mentorId;
|
||||
@@ -289,7 +374,7 @@ function renderMentorMessages() {
|
||||
<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>
|
||||
<div class="mentor-message-body">
|
||||
<div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
|
||||
<div class="mentor-message-label">${message.role === "user" ? "我" : 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>
|
||||
@@ -300,7 +385,7 @@ function renderMentorMessages() {
|
||||
</article>
|
||||
`).join("");
|
||||
}
|
||||
document.querySelector("#mentorQuickPrompts").hidden = state.mentorMessages.length > 0 || state.mentorLoading;
|
||||
document.querySelector("#mentorQuickPrompts").hidden = false;
|
||||
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;
|
||||
@@ -342,6 +427,7 @@ async function sendMentorQuestion(event) {
|
||||
const responseMessage = { role: "assistant", content: "", streaming: true, meta: "", followUps: [] };
|
||||
state.mentorMessages.push(responseMessage);
|
||||
input.value = "";
|
||||
syncMentorComposerHeight();
|
||||
state.mentorLoading = true;
|
||||
state.mentorController = new AbortController();
|
||||
hideMentorNotice();
|
||||
@@ -431,6 +517,7 @@ async function streamMentorRequest(body, signal, onDelta, onMeta) {
|
||||
function useMentorQuickPrompt(prompt) {
|
||||
const input = document.querySelector("#mentorQuestion");
|
||||
input.value = prompt || "";
|
||||
syncMentorComposerHeight();
|
||||
input.focus();
|
||||
}
|
||||
|
||||
@@ -529,19 +616,21 @@ function bindMentorEvents() {
|
||||
document.querySelector("#stopMentorQuestion").addEventListener("click", stopMentorGeneration);
|
||||
document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation);
|
||||
document.querySelector("#mentorSortToggle").addEventListener("click", toggleMentorSortMode);
|
||||
document.querySelector("#mentorFilterToggle").addEventListener("click", toggleMentorFilterMenu);
|
||||
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();
|
||||
selectMentorGrade(button.dataset.mentorGrade);
|
||||
closeMentorFilterMenu();
|
||||
});
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target.closest(".mentor-filter-menu")) return;
|
||||
closeMentorFilterMenu();
|
||||
});
|
||||
document.querySelectorAll("[data-mentor-prompt]").forEach((button) => {
|
||||
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorPrompt));
|
||||
});
|
||||
@@ -550,4 +639,70 @@ function bindMentorEvents() {
|
||||
event.preventDefault();
|
||||
document.querySelector("#mentorChatForm").requestSubmit();
|
||||
});
|
||||
document.querySelector("#mentorQuestion").addEventListener("input", syncMentorComposerHeight);
|
||||
document.querySelector("#mentorNoteButton").addEventListener("click", openMentorNoteDialog);
|
||||
document.querySelector("#mentorProfileButton").addEventListener("click", openMentorProfileDialog);
|
||||
document.querySelector("#mentorPinButton").addEventListener("click", () => toggleMentorPin(state.selectedMentorId));
|
||||
document.querySelector("#mentorNoteInput").addEventListener("input", saveMentorNote);
|
||||
document.querySelectorAll("[data-mentor-dialog-close]").forEach((button) => {
|
||||
button.addEventListener("click", () => closeMentorDialog(button.dataset.mentorDialogClose));
|
||||
});
|
||||
window.addEventListener("resize", centerOpenMentorDialogs);
|
||||
}
|
||||
|
||||
function selectedMentor() {
|
||||
return (state.mentorSetup?.mentors || []).find((item) => item.id === state.selectedMentorId) || null;
|
||||
}
|
||||
|
||||
function syncMentorComposerHeight() {
|
||||
const input = document.querySelector("#mentorQuestion");
|
||||
if (!input) return;
|
||||
input.style.height = "0";
|
||||
const nextHeight = Math.min(input.scrollHeight, 168);
|
||||
input.style.height = `${nextHeight}px`;
|
||||
input.style.overflowY = nextHeight >= 168 ? "auto" : "hidden";
|
||||
}
|
||||
|
||||
function openMentorDialog(dialogId) {
|
||||
const dialog = document.querySelector(`#${dialogId}`);
|
||||
if (!(dialog instanceof HTMLDialogElement)) return;
|
||||
if (!dialog.open) dialog.showModal();
|
||||
positionMentorDialog(dialog);
|
||||
}
|
||||
|
||||
function closeMentorDialog(dialogId) {
|
||||
const dialog = document.querySelector(`#${dialogId}`);
|
||||
if (dialog instanceof HTMLDialogElement && dialog.open) dialog.close();
|
||||
}
|
||||
|
||||
function openMentorNoteDialog() {
|
||||
const input = document.querySelector("#mentorNoteInput");
|
||||
if (input) input.value = loadMentorNote(selectedMentor());
|
||||
openMentorDialog("mentorNoteDialog");
|
||||
const noteInput = document.querySelector("#mentorNoteInput");
|
||||
if (noteInput) requestAnimationFrame(() => noteInput.focus());
|
||||
}
|
||||
|
||||
function openMentorProfileDialog() {
|
||||
populateMentorDialogs(selectedMentor());
|
||||
openMentorDialog("mentorProfileDialog");
|
||||
}
|
||||
|
||||
function positionMentorDialog(dialog) {
|
||||
if (!(dialog instanceof HTMLDialogElement) || !dialog.open) return;
|
||||
const viewportPadding = 12;
|
||||
const rect = dialog.getBoundingClientRect();
|
||||
const width = Math.min(400, window.innerWidth - viewportPadding * 2);
|
||||
const height = Math.min(rect.height, window.innerHeight - viewportPadding * 2);
|
||||
dialog.style.top = `${Math.max(viewportPadding, Math.round((window.innerHeight - height) / 2))}px`;
|
||||
dialog.style.left = `${Math.max(viewportPadding, Math.round((window.innerWidth - width) / 2))}px`;
|
||||
dialog.style.right = "auto";
|
||||
dialog.style.bottom = "auto";
|
||||
dialog.style.margin = "0";
|
||||
}
|
||||
|
||||
function centerOpenMentorDialogs() {
|
||||
document.querySelectorAll("#mentorNoteDialog, #mentorProfileDialog").forEach((dialog) => {
|
||||
if (dialog instanceof HTMLDialogElement && dialog.open) positionMentorDialog(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user