一键全流程自动演示:影院字幕条+10镜头剧本(采集→发现→定义→审核→入图→演化→图谱四视角→时序生长→匹配→路径),自动运镜与倒计时进度条,支持暂停/步进/Esc退出;修复非JD来源标注显示与演示镜头选中淡出干扰

This commit is contained in:
FinStab
2026-08-31 19:40:41 +08:00
parent 6312dd5f4a
commit 0bd0e30aa0
4 changed files with 165 additions and 43 deletions
+132 -29
View File
@@ -166,8 +166,10 @@ async function fetchJd(sid) {
try { return await p; } catch (e) { _jdCache.delete(sid); throw e; }
}
/* 来源列表行内联元信息:填充为「企业 · 平台 · 日期」可读文案(不显示内部 source_id) */
/* 来源列表行内联元信息:填充为「企业 · 平台 · 日期」可读文案(不显示内部 source_id);
* 非 JD 证据来源(如「人工审核入库」等标注)直接原文展示,不走 JD 查询 */
CN.jdMetaLine = async function (el, sid) {
if (!/\.jsonl#/.test(String(sid))) { el.textContent = String(sid); return; }
try {
const r = await fetchJd(sid);
el.textContent = `${r.company || "企业未披露"} · ${r.source || "公开招聘平台"} · ${r.publish_date || "时间未知"}`;
@@ -181,7 +183,7 @@ CN.jdMetaLine = async function (el, sid) {
* ctx:{ skill, jobTitle, changeType, window, keyword } 用于生成可读结论与命中依据;
* keyword 命中词在 JD 原文中以 <mark> 高亮 */
async function showJdEvidence(sids, ctx = {}) {
const list = (Array.isArray(sids) ? sids : [sids]).filter(Boolean);
const list = (Array.isArray(sids) ? sids : [sids]).filter(Boolean).filter((s) => /\.jsonl#/.test(String(s)));
if (!list.length) return toast("该条目暂无关联的公开 JD 证据", "info");
let idx = 0;
const conclusion = ctx.skill
@@ -2151,41 +2153,93 @@ $("#banner-config").onclick = () => openConfigModal(false);
$("#embed-banner-config").onclick = () => openConfigModal(false);
$("#demo-badge").onclick = () => openConfigModal(false);
/* ================= 演示导览(6 步评委导览,右上角浮动步进条) ================= */
/* ================= 全流程自动演示导览(一键播放,录屏/评委演示用) =================
* 剧本聚焦赛题主线:多源采集 → 新岗位发现 → AI 生成定义 → 人工审核入图 →
* 既有岗位演化 → 图谱全景四视角 → 动态时序生长 → 人岗匹配 → 学习路径。
* 全程只读演示:不真实点击批准/退回,不改任何数据。 */
const DEMO_JOB = "job_Java开发工程师";
const DEMO_CANDIDATE = "job_大模型应用工程师";
const DEMO_CAND = "cand_提示词工程师"; // 待审核新岗位候选(发现→定义→审核镜头)
const DEMO_NEWJOB = "job_RAG工程师"; // 已入库新岗位(NEW 脉动标记镜头)
const zz = (ms) => new Promise((r) => setTimeout(r, ms));
const DEMO_STEPS = [
{ title: "数据驾驶舱:多源数据总览",
desc: "JD 规模、岗位/技能/事件总量、演化趋势、清洗与审核状态",
{ dur: 6000,
title: "多源数据汇聚与治理",
desc: "多平台真实招聘语料经清洗、结构化抽取、交叉验证后进入图谱管线",
go: () => switchView("dashboard") },
{ title: "新岗位候选:发现 → 人工审核 → 入图",
desc: "候选岗位不直接入图:四指标证据 + 人工确认,可回查证据 JD",
{ dur: 8000,
title: "新岗位发现:四指标聚类",
desc: "JD≥15 条 · 企业≥5 家 · 环比增长≥50% · 差异度≥0.4;候选以虚线节点呈现,绝不直接入图",
go: () => {
switchView("graph");
const locate = () => {
const has = CN.graph && CN.graph.has && CN.graph.has(DEMO_CANDIDATE);
if (CN.gotoNode) CN.gotoNode(has ? DEMO_CANDIDATE : DEMO_JOB);
const id = (CN.graph && CN.graph.has && CN.graph.has(DEMO_CAND)) ? DEMO_CAND : DEMO_JOB;
if (CN.gotoNode) CN.gotoNode(id);
};
if (CN.graph && CN.graph.ready) CN.graph.ready().then(locate);
else if (CN.gotoNode) CN.gotoNode(DEMO_CANDIDATE);
} },
{ title: "岗位局部图谱:Java 开发工程师",
desc: "技能点级局部图谱:必备/加分技能、支撑 JD 数与可读证据",
go: () => {
{ dur: 9000,
title: "大模型生成结构化岗位定义",
desc: "岗位名称 / 核心职责 / 必备技能 / 加分技能 / 典型场景自动归纳,四指标证据全部可回查原始 JD",
go: async (alive) => {
await zz(400); if (!alive()) return;
openReviewDesk((CN.graph && CN.graph.has && CN.graph.has(DEMO_CAND)) ? DEMO_CAND : undefined);
} },
{ dur: 10000,
title: "人工审核把关:批准后才入图",
desc: "人机协同防幻觉——批准 / 退回全程留痕;已入库新岗位(如 RAG 工程师)带 NEW 脉动标记进入全景图谱",
go: async (alive) => {
await zz(3600); if (!alive()) return;
const btn = document.querySelector('#review-desk-body [data-act="accept"]');
if (btn) btn.classList.add("tour-glow");
await zz(3600); if (!alive()) return;
if (btn) btn.classList.remove("tour-glow");
$("#review-desk-mask").classList.add("hidden");
switchView("graph");
const enter = () => { if (CN.graph && CN.graph.solo) CN.graph.solo(DEMO_JOB); };
const enter = () => { if (CN.gotoNode) CN.gotoNode(DEMO_NEWJOB); };
if (CN.graph && CN.graph.ready) CN.graph.ready().then(enter);
} },
{ title: "岗位演化报告:能力动态更新",
desc: "本期结论、新增/增强/减弱/退出、时点对比,每条可回查证据",
go: () => {
{ dur: 8500,
title: "既有岗位能力动态更新",
desc: "本期结论 + 四色 Diff:新增 / 增强 / 减弱 / 退出,每条结论可回查支撑 JD 证据链",
go: async (alive) => {
switchView("evolution");
$("#evo-job-select").value = DEMO_JOB;
$("#evo-job-select").dispatchEvent(new Event("change"));
await zz(1400); if (!alive()) return;
const d = $("#evo-diff"); if (d) d.scrollIntoView({ block: "center", behavior: "smooth" });
} },
{ title: "人岗诊断:示例简历自动跑通",
desc: "载入示例简历 → 解析确认 → 匹配结论 / 核心缺口 / 可迁移能力",
go: async () => {
{ dur: 12000,
title: "全景图谱:四种视角看岗位生态",
desc: "全景总览 → 技术栈气泡聚类 → 职级能力树,点击节点可下钻详情与证据",
go: async (alive) => {
switchView("graph");
if (CN.graph && CN.graph.ready) await CN.graph.ready();
if (!alive()) return;
if (CN.graph.deselect) CN.graph.deselect(); // 巡览全图前清选中,避免非相关节点淡出
const tab = (v) => { const b = document.querySelector(`[data-gview="${v}"]`); if (b) b.click(); };
tab("overview");
await zz(3400); if (!alive()) return;
tab("tech");
await zz(4000); if (!alive()) return;
tab("level");
} },
{ dur: 9500,
title: "动态时序:图谱生长回放",
desc: "拖动 / 播放时间轴,看岗位能力图谱从 2019 到 2026Q3 的逐年扩张,左栏同步解读当季变化",
go: async (alive) => {
switchView("graph");
if (CN.graph && CN.graph.ready) await CN.graph.ready();
if (!alive()) return;
const b = document.querySelector('[data-gview="timeline"]'); if (b) b.click();
if (CN.graph.deselect) CN.graph.deselect(); // 生长回放看全图,清选中避免淡出
await zz(700); if (!alive()) return;
const p = $("#gt-play"); if (p && p.textContent === "播放") p.click();
} },
{ dur: 13000,
title: "人岗匹配:示例简历自动跑通",
desc: "载入示例简历 → 解析确认 → 双雷达差距分析:命中 / 缺失 / 可迁移一目了然",
go: async (alive) => {
switchView("match");
$$("#view-match .match-tab").forEach((x) => x.classList.toggle("active", x.dataset.mtab === "single"));
$("#match-single").classList.remove("hidden");
@@ -2195,39 +2249,88 @@ const DEMO_STEPS = [
$("#btn-load-sample").click();
try {
const p = await post("/api/resume/parse", { text: $("#resume-text").value });
if (!alive()) return;
applyParsedProfile(p, "演示示例简历");
$("#btn-run-match").click();
} catch (e) { toast(friendlyErr(e), "error"); }
} },
{ title: "学习路径:可执行的三阶段路线",
{ dur: 7500,
title: "学习路径:可执行的三阶段路线",
desc: "自动带入岗位与技能缺口:基础补齐 → 岗位核心 → 项目验证",
go: () => {
switchView("path");
$("#path-job-select").value = DEMO_JOB;
loadPathJob(DEMO_JOB, matchState.skills).then(() => $("#btn-path").click());
} },
{ dur: 5500,
title: "全流程闭环",
desc: "采集 → 发现 → 定义 → 审核 → 演化 → 图谱 → 匹配 → 路径,全链路可追溯、可验证",
go: () => switchView("dashboard") },
];
let tourIdx = -1;
function tourGo(i) {
tourIdx = Math.max(0, Math.min(DEMO_STEPS.length - 1, i));
let tourToken = 0; // 场景令牌:切镜/退出时作废旧镜头的延时动作与计时器
let tourAuto = false; // 自动播放中
function updateTourPlayBtn() {
const b = $("#tour-play");
if (b) b.textContent = tourAuto ? "暂停" : "继续";
}
function tourGo(i, auto) {
if (i >= DEMO_STEPS.length) { endTour(); toast("全流程演示完毕", "success"); return; }
tourToken++; // 作废旧场景的所有延时动作
const tk = tourToken;
tourIdx = Math.max(0, i);
tourAuto = !!auto;
const step = DEMO_STEPS[tourIdx];
$("#tour-bar").classList.remove("hidden");
$("#tour-prog").textContent = `演示 ${tourIdx + 1}/${DEMO_STEPS.length}`;
$("#tour-prog").textContent = `${tourIdx + 1}/${DEMO_STEPS.length}`;
$("#tour-title").textContent = step.title;
$("#tour-desc").textContent = step.desc;
$("#tour-prev").disabled = tourIdx === 0;
$("#tour-next").textContent = tourIdx === DEMO_STEPS.length - 1 ? "完成" : "下一步";
Promise.resolve(step.go()).catch((e) => toast(friendlyErr(e), "error"));
$("#tour-fill").style.width = "0%";
updateTourPlayBtn();
const alive = () => tk === tourToken && tourIdx >= 0;
Promise.resolve(step.go(alive)).catch((e) => toast(friendlyErr(e), "error"));
if (auto) autoAdvance(tourIdx, tk);
}
/* 自动播放计时:暂停时冻结进度,令牌失效即废弃 */
async function autoAdvance(i, tk) {
const dur = DEMO_STEPS[i].dur || 6000;
const fill = $("#tour-fill");
let elapsed = 0;
while (elapsed < dur) {
await zz(120);
if (tk !== tourToken || tourIdx < 0) return;
if (!tourAuto) continue; // 暂停:不计时
elapsed += 120;
if (fill) fill.style.width = `${Math.min(100, (elapsed / dur) * 100)}%`;
}
if (tk === tourToken && tourIdx >= 0) tourGo(i + 1, true);
}
function endTour() {
tourIdx = -1;
tourToken++;
tourAuto = false;
$("#tour-bar").classList.add("hidden");
const mask = $("#review-desk-mask");
if (mask) mask.classList.add("hidden");
}
$("#btn-demo-tour").onclick = () => tourGo(0);
$("#tour-prev").onclick = () => tourGo(tourIdx - 1);
$("#tour-next").onclick = () => (tourIdx === DEMO_STEPS.length - 1 ? endTour() : tourGo(tourIdx + 1));
$("#btn-demo-tour").onclick = () => tourGo(0, true); // 一键自动播放全流程
$("#tour-play").onclick = () => {
tourAuto = !tourAuto;
updateTourPlayBtn();
if (tourAuto && tourIdx >= 0) autoAdvance(tourIdx, tourToken); // 从当前场景重新计时继续
};
$("#tour-prev").onclick = () => tourGo(tourIdx - 1, false);
$("#tour-next").onclick = () => tourGo(tourIdx + 1, false);
$("#tour-exit").onclick = endTour;
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && tourIdx >= 0) endTour(); });
function embedStatusHtml(emb) {
if (emb.ready) {