竞赛演示模式:首屏降噪与预置导览

- demo_mode 配置项(默认开):不弹初始设置,LLM/语义警告横幅降级为顶栏状态点
- 顶栏新增'开始演示'导览(驾驶舱→新岗位发现→Java演化→匹配诊断四步预置路线)
- 图谱默认降噪:场景/行业图层默认关、关系标签默认关(悬停/缩放显示)
- 演化页默认锁定 Java 强样例;数据版本徽标标注'(模拟)';联系方式移入设置页
- start.bat/start.sh 启动脚本
This commit is contained in:
RoyirinRosmontis
2026-08-12 16:25:37 +08:00
parent 33aee6b8ae
commit 3329c55e06
6 changed files with 637 additions and 535 deletions
+8 -3
View File
@@ -93,6 +93,7 @@ def health() -> dict:
"llm_ready": ready, "offline_fallback": not ready, "llm_ready": ready, "offline_fallback": not ready,
"embed": semantic.status(_cfg), "embed": semantic.status(_cfg),
"guided": bool(_cfg.get("guided")), "guided": bool(_cfg.get("guided")),
"demo_mode": bool(_cfg.get("demo_mode", True)),
"mode": "online" if ready else "offline"}) "mode": "online" if ready else "offline"})
@@ -221,6 +222,7 @@ class ConfigBody(BaseModel):
embed_api_base_url: str | None = None embed_api_base_url: str | None = None
embed_api_model: str | None = None embed_api_model: str | None = None
guided: bool | None = None # 初始设置引导完成标记 guided: bool | None = None # 初始设置引导完成标记
demo_mode: bool | None = None # 竞赛演示模式开关
mode: str | None = None # demo | live mode: str | None = None # demo | live
@@ -243,6 +245,7 @@ def _public_config() -> dict:
}, },
"embed": semantic.status(_cfg), "embed": semantic.status(_cfg),
"mode": str(_cfg.get("mode", "demo")), "mode": str(_cfg.get("mode", "demo")),
"demo_mode": bool(_cfg.get("demo_mode", True)),
"offline_fallback": not ready, "offline_fallback": not ready,
} }
@@ -290,15 +293,17 @@ def save_config(body: ConfigBody) -> dict:
updates["embed_api_model"] = body.embed_api_model.strip() updates["embed_api_model"] = body.embed_api_model.strip()
if body.guided is not None: if body.guided is not None:
updates["guided"] = bool(body.guided) updates["guided"] = bool(body.guided)
if body.demo_mode is not None:
updates["demo_mode"] = bool(body.demo_mode)
save_runtime_settings(updates) save_runtime_settings(updates)
# 热生效:令牌、模式与 LLM/语义模型配置立即更新到运行配置(下次调用即读取) # 热生效:令牌、模式与 LLM/语义模型配置立即更新到运行配置(下次调用即读取)
for k in ("access_token", "mode", "llm_api_key", "llm_base_url", "llm_model", for k in ("access_token", "mode", "llm_api_key", "llm_base_url", "llm_model",
"embed_model_path", "embed_provider", "embed_api_key", "embed_model_path", "embed_provider", "embed_api_key",
"embed_api_base_url", "embed_api_model", "guided"): "embed_api_base_url", "embed_api_model", "guided", "demo_mode"):
if k in updates: if k in updates:
if updates[k]: if updates[k] or isinstance(updates[k], bool):
_cfg[k] = updates[k] _cfg[k] = updates[k] # 布尔 False 也要写入(如关闭演示模式)
else: else:
_cfg.pop(k, None) _cfg.pop(k, None)
logger.info("运行时设置已保存到 config.json: %s", sorted(updates)) logger.info("运行时设置已保存到 config.json: %s", sorted(updates))
+2 -1
View File
@@ -17,11 +17,12 @@ RUNTIME_KEYS = {
"llm_api_key", "llm_base_url", "llm_model", "llm_timeout", "llm_api_key", "llm_base_url", "llm_model", "llm_timeout",
"embed_model_path", "embed_provider", "embed_api_key", "embed_model_path", "embed_provider", "embed_api_key",
"embed_api_base_url", "embed_api_model", "embed_api_base_url", "embed_api_model",
"guided", "jd_extra_dirs", "passive_sources", "guided", "jd_extra_dirs", "passive_sources", "demo_mode",
} }
DEFAULTS: dict = { DEFAULTS: dict = {
"mode": "demo", # demo | live "mode": "demo", # demo | live
"demo_mode": True, # 竞赛演示模式:不弹初始设置、警告降级为状态点
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 9008, "port": 9008,
"data_dir": "data", "data_dir": "data",
+79 -10
View File
@@ -255,7 +255,7 @@ async function loadDashboard() {
CN.graphData = graph; CN.graphData = graph;
const s = health.graph || {}; const s = health.graph || {};
updateBanners(health); updateBanners(health);
$("#data-version").textContent = `数据版本 v${(graph.events.at(-1) || {}).window || "demo"} · ${s.nodes ?? 0} 节点`; $("#data-version").textContent = `数据更新至 ${(graph.events.at(-1) || {}).window || "2026Q3"}(模拟)`;
/* 指标卡 */ /* 指标卡 */
const series = quarterlyCounts(graph.events || []); const series = quarterlyCounts(graph.events || []);
@@ -1421,21 +1421,31 @@ $("#btn-export-path").onclick = () => {
toast("已导出文本", "success"); toast("已导出文本", "success");
}; };
/* ================= 系统设置 / 状态横幅 ================= */ /* ================= 系统设置 / 状态横幅 / 演示模式 ================= */
let lastHealth = null; // 最近一次 health 响应(横幅状态源) let lastHealth = null; // 最近一次 health 响应(横幅与状态徽标的数据源)
function updateBanners(health) { function updateBanners(health) {
if (health) lastHealth = health; if (health) lastHealth = health;
const h = lastHealth || {}; const h = lastHealth || {};
const showOffline = !!h.offline_fallback const demo = h.demo_mode !== false;
&& sessionStorage.getItem("cn_banner_dismissed") !== "1";
const embed = h.embed || {}; const embed = h.embed || {};
const showEmbed = !embed.ready const degraded = !!h.offline_fallback || !embed.ready;
// 演示模式:警告横幅降级为顶栏状态徽标(首屏不出现负面字眼)
const showOffline = !demo && !!h.offline_fallback
&& sessionStorage.getItem("cn_banner_dismissed") !== "1";
const showEmbed = !demo && !embed.ready
&& sessionStorage.getItem("cn_banner2_dismissed") !== "1"; && sessionStorage.getItem("cn_banner2_dismissed") !== "1";
$("#offline-banner").classList.toggle("hidden", !showOffline); $("#offline-banner").classList.toggle("hidden", !showOffline);
$("#embed-banner").classList.toggle("hidden", !showEmbed); $("#embed-banner").classList.toggle("hidden", !showEmbed);
const n = (showOffline ? 1 : 0) + (showEmbed ? 1 : 0); const n = (showOffline ? 1 : 0) + (showEmbed ? 1 : 0);
document.body.classList.toggle("has-banner", n === 1); document.body.classList.toggle("has-banner", n === 1);
document.body.classList.toggle("has-banner2", n === 2); document.body.classList.toggle("has-banner2", n === 2);
// 顶栏状态徽标:绿=全就绪,黄=离线回退/语义未就绪
const badge = $("#demo-badge");
badge.classList.toggle("warn", degraded);
$("#demo-badge-text").textContent = degraded ? "离线演示" : (demo ? "演示模式" : "在线");
badge.title = degraded
? "离线回退中(LLM/语义模型未配置),点击查看设置"
: "全部通道就绪,点击查看设置";
} }
function dismissBanner(id, key) { function dismissBanner(id, key) {
sessionStorage.setItem(key, "1"); sessionStorage.setItem(key, "1");
@@ -1445,6 +1455,44 @@ $("#banner-close").onclick = () => dismissBanner("offline-banner", "cn_banner_di
$("#embed-banner-close").onclick = () => dismissBanner("embed-banner", "cn_banner2_dismissed"); $("#embed-banner-close").onclick = () => dismissBanner("embed-banner", "cn_banner2_dismissed");
$("#banner-config").onclick = () => openConfigModal(false); $("#banner-config").onclick = () => openConfigModal(false);
$("#embed-banner-config").onclick = () => openConfigModal(false); $("#embed-banner-config").onclick = () => openConfigModal(false);
$("#demo-badge").onclick = () => openConfigModal(false);
/* ================= 演示导览(预置路线,一键串起完整闭环) ================= */
const DEMO_STEPS = [
{ title: "① 数据驾驶舱:多源数据总览", desc: "JD 规模、岗位/技能/事件总量、演化趋势、清洗与审核状态",
go: () => switchView("dashboard") },
{ title: "② 新岗位发现:候选→审核→入图", desc: "图谱定位「大模型应用工程师」:四指标证据、人工确认、证据 JD 回查",
go: () => { switchView("graph"); if (CN.gotoNode) CN.gotoNode("job_大模型应用工程师"); } },
{ title: "③ 既有岗位演化:Java 编年史", desc: "时点对比、变更堆叠图、生命周期甘特、事件证据链",
go: () => {
switchView("evolution");
$("#evo-job-select").value = "job_Java开发工程师";
$("#evo-job-select").dispatchEvent(new Event("change"));
} },
{ title: "④ 人岗匹配诊断 + 学习路径", desc: "载入示例简历 → 确认解析 → 总分/四维/差距清单 → 一键生成学习路径",
go: () => {
switchView("match");
$$(".match-tab").forEach((x) => x.classList.toggle("active", x.dataset.mtab === "single"));
$("#match-single").classList.remove("hidden");
$("#match-batch").classList.add("hidden");
$("#btn-load-sample").click();
} },
];
$("#btn-demo-tour").onclick = () => {
openModal("演示导览(预置路线)", `
<div class="tour-intro">按预置路线依次演示完整闭环,每步点击「前往」自动跳转到位:</div>
${DEMO_STEPS.map((s, i) => `
<div class="tour-step">
<div class="tour-step-body">
<div class="tour-step-title">${esc(s.title)}</div>
<div class="tour-step-desc">${esc(s.desc)}</div>
</div>
<button class="btn btn-primary btn-sm" data-step="${i}">前往</button>
</div>`).join("")}`);
$$("#modal-body [data-step]").forEach((b) => {
b.onclick = () => { closeModal(); DEMO_STEPS[+b.dataset.step].go(); };
});
};
function embedStatusHtml(emb) { function embedStatusHtml(emb) {
if (emb.ready) { if (emb.ready) {
@@ -1506,6 +1554,13 @@ async function openConfigModal(needToken, firstRun) {
<button class="btn btn-secondary btn-sm" id="cfg-token-show">显示</button> <button class="btn btn-secondary btn-sm" id="cfg-token-show">显示</button>
</div> </div>
</div> </div>
<div class="form-item">
<label class="form-label">演示模式<span class="form-label-tip">开启后不弹初始设置、警告降级为顶栏状态点</span></label>
<div class="seg" id="cfg-demo">
<button class="seg-item ${cfg.demo_mode !== false ? "active" : ""}" data-demo="1">开启(评审演示)</button>
<button class="seg-item ${cfg.demo_mode === false ? "active" : ""}" data-demo="0">关闭(开发调试)</button>
</div>
</div>
</div> </div>
<div class="cfg-sec"> <div class="cfg-sec">
<div class="cfg-sec-title">LLM API(星火 / OpenAI 兼容,保存即热生效)</div> <div class="cfg-sec-title">LLM API(星火 / OpenAI 兼容,保存即热生效)</div>
@@ -1584,7 +1639,8 @@ async function openConfigModal(needToken, firstRun) {
</ol> </ol>
</div> </div>
</div> </div>
</div>`, </div>
<div class="cfg-foot-note">如遇问题,请联系 support@rrrosmontis.icu</div>`,
`${firstRun ? `<button class="btn btn-secondary" id="cfg-skip">跳过,使用离线演示模式</button>` : ""} `${firstRun ? `<button class="btn btn-secondary" id="cfg-skip">跳过,使用离线演示模式</button>` : ""}
<button class="btn btn-secondary" id="cfg-save-restart">保存并重启</button> <button class="btn btn-secondary" id="cfg-save-restart">保存并重启</button>
<button class="btn btn-primary" id="cfg-save">保存设置</button>`); <button class="btn btn-primary" id="cfg-save">保存设置</button>`);
@@ -1598,6 +1654,14 @@ async function openConfigModal(needToken, firstRun) {
}; };
}); });
let embedProvider = cfg.embed.provider === "api" ? "api" : "local"; let embedProvider = cfg.embed.provider === "api" ? "api" : "local";
let demoMode = cfg.demo_mode !== false;
$$("#cfg-demo .seg-item").forEach((b) => {
b.onclick = () => {
$$("#cfg-demo .seg-item").forEach((x) => x.classList.remove("active"));
b.classList.add("active");
demoMode = b.dataset.demo === "1";
};
});
$$("#cfg-embed-provider .seg-item").forEach((b) => { $$("#cfg-embed-provider .seg-item").forEach((b) => {
b.onclick = () => { b.onclick = () => {
$$("#cfg-embed-provider .seg-item").forEach((x) => x.classList.remove("active")); $$("#cfg-embed-provider .seg-item").forEach((x) => x.classList.remove("active"));
@@ -1629,6 +1693,7 @@ async function openConfigModal(needToken, firstRun) {
embed_model_path: $("#cfg-embed-path").value.trim(), embed_model_path: $("#cfg-embed-path").value.trim(),
embed_api_base_url: $("#cfg-embed-base").value.trim(), embed_api_base_url: $("#cfg-embed-base").value.trim(),
embed_api_model: $("#cfg-embed-model").value.trim(), embed_api_model: $("#cfg-embed-model").value.trim(),
demo_mode: demoMode,
guided: true, // 保存即视为完成初始设置 guided: true, // 保存即视为完成初始设置
}; };
const tok = $("#cfg-token").value; const tok = $("#cfg-token").value;
@@ -1643,7 +1708,7 @@ async function openConfigModal(needToken, firstRun) {
else localStorage.removeItem("cn_access_token"); else localStorage.removeItem("cn_access_token");
} }
lastHealth = Object.assign({}, lastHealth, { lastHealth = Object.assign({}, lastHealth, {
offline_fallback: saved.offline_fallback, embed: saved.embed, offline_fallback: saved.offline_fallback, embed: saved.embed, demo_mode: saved.demo_mode,
}); });
updateBanners(); updateBanners();
return saved; return saved;
@@ -1716,6 +1781,8 @@ window.addEventListener("resize", () => {
CN.jobs = await api("/api/jobs"); CN.jobs = await api("/api/jobs");
const opts = CN.jobs.map((j) => `<option value="${esc(j.job_id)}">${esc(j.title)}</option>`).join(""); const opts = CN.jobs.map((j) => `<option value="${esc(j.job_id)}">${esc(j.title)}</option>`).join("");
["#evo-job-select", "#match-job-select", "#path-job-select", "#batch-job-select"].forEach((s) => { $(s).innerHTML = opts; }); ["#evo-job-select", "#match-job-select", "#path-job-select", "#batch-job-select"].forEach((s) => { $(s).innerHTML = opts; });
$("#evo-job-select").value = "job_Java开发工程师"; // 演化页默认锁定最强样例
$("#match-job-select").value = "job_Java开发工程师";
await loadDashboard(); // 先加载图谱缓存(演化页甘特图依赖) await loadDashboard(); // 先加载图谱缓存(演化页甘特图依赖)
await loadEvolution(); await loadEvolution();
renderEvoHeatmap(); renderEvoHeatmap();
@@ -1723,8 +1790,10 @@ window.addEventListener("resize", () => {
const hash = new URLSearchParams(location.hash.slice(1)); const hash = new URLSearchParams(location.hash.slice(1));
if (hash.get("view")) switchView(hash.get("view")); if (hash.get("view")) switchView(hash.get("view"));
if (hash.get("node") && CN.gotoNode) CN.gotoNode(hash.get("node")); if (hash.get("node") && CN.gotoNode) CN.gotoNode(hash.get("node"));
/* 首次使用:引导完成基础设置(完成标记记录在服务端 config.json) */ /* 首次使用:演示模式下不弹初始设置(评审口径),非演示模式才引导 */
if (!lastHealth || !lastHealth.guided) openConfigModal(false, true); if ((!lastHealth || !lastHealth.guided) && lastHealth && lastHealth.demo_mode === false) {
openConfigModal(false, true);
}
} catch (e) { } catch (e) {
toast("后端连接失败:" + e.message, "error"); toast("后端连接失败:" + e.message, "error");
} }
+4 -4
View File
@@ -10,8 +10,8 @@
const G = { const G = {
nodes: [], edges: [], events: [], idx: {}, nodes: [], edges: [], events: [], idx: {},
cam: { x: 0, y: 0, k: 1 }, // 相机:世界 -> 屏幕 screen = (world - cam.pan) * k ... 使用 cam.x/y 为平移(屏幕像素) cam: { x: 0, y: 0, k: 1 }, // 相机:世界 -> 屏幕 screen = (world - cam.pan) * k ... 使用 cam.x/y 为平移(屏幕像素)
showLabels: true, showLabels: false, // 默认关闭:悬停/选中/缩放时才显示
filters: { types: { Job: true, Skill: true, Industry: true, Scenario: true }, category: "", level: "", filters: { types: { Job: true, Skill: true, Industry: false, Scenario: false }, category: "", level: "",
edges: { required: true, preferred: true, applies: true, belongs: true } }, edges: { required: true, preferred: true, applies: true, belongs: true } },
selected: null, // 选中节点 id selected: null, // 选中节点 id
soloJob: null, // 岗位子图模式:只看该岗位及其一跳邻居 soloJob: null, // 岗位子图模式:只看该岗位及其一跳邻居
@@ -444,7 +444,7 @@
$("#fp-category").onchange = (e) => { G.filters.category = e.target.value; requestDraw(); drawMinimap(); }; $("#fp-category").onchange = (e) => { G.filters.category = e.target.value; requestDraw(); drawMinimap(); };
$("#fp-level").onchange = (e) => { G.filters.level = e.target.value; requestDraw(); drawMinimap(); }; $("#fp-level").onchange = (e) => { G.filters.level = e.target.value; requestDraw(); drawMinimap(); };
$("#fp-reset").onclick = () => { $("#fp-reset").onclick = () => {
G.filters = { types: { Job: true, Skill: true, Industry: true, Scenario: true }, category: "", level: "", G.filters = { types: { Job: true, Skill: true, Industry: false, Scenario: false }, category: "", level: "",
edges: { required: true, preferred: true, applies: true, belongs: true } }; edges: { required: true, preferred: true, applies: true, belongs: true } };
$$("#graph-filter-panel input[data-ftype]").forEach((cb) => { cb.checked = true; }); $$("#graph-filter-panel input[data-ftype]").forEach((cb) => { cb.checked = true; });
$$("#graph-filter-panel input[data-etype]").forEach((cb) => { cb.checked = true; }); $$("#graph-filter-panel input[data-etype]").forEach((cb) => { cb.checked = true; });
@@ -487,7 +487,7 @@
if (!v) return false; if (!v) return false;
G.cam = v.cam; G.cam = v.cam;
G.filters = Object.assign( G.filters = Object.assign(
{ types: { Job: true, Skill: true, Industry: true, Scenario: true }, category: "", level: "", { types: { Job: true, Skill: true, Industry: false, Scenario: false }, category: "", level: "",
edges: { required: true, preferred: true, applies: true, belongs: true } }, edges: { required: true, preferred: true, applies: true, belongs: true } },
v.filters); v.filters);
G.filters.edges = Object.assign({ required: true, preferred: true, applies: true, belongs: true }, G.filters.edges = Object.assign({ required: true, preferred: true, applies: true, belongs: true },
+521 -517
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -924,3 +924,26 @@ svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width:
width: 18px; height: 18px; border-radius: 50%; cursor: pointer; line-height: 1; width: 18px; height: 18px; border-radius: 50%; cursor: pointer; line-height: 1;
} }
.solo-badge button:hover { background: rgba(255,255,255,.45); } .solo-badge button:hover { background: rgba(255,255,255,.45); }
/* 演示模式状态徽标 / 演示导览 */
.demo-badge {
display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
font-size: 12px; padding: 3px 10px; border-radius: 999px;
background: var(--success-light); color: var(--success); border: 1px solid #b7eb8f;
white-space: nowrap;
}
.demo-badge .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); }
.demo-badge.warn { background: var(--warning-light); color: #ad4e00; border-color: #ffd591; }
.demo-badge.warn .dot { background: var(--warning); }
#btn-demo-tour svg { width: 13px; height: 13px; fill: currentColor; }
.tour-intro { font-size: 13px; color: var(--text-2); margin-bottom: 12px; }
.tour-step {
display: flex; align-items: center; gap: 12px;
border: 1px solid var(--border); border-radius: var(--radius);
padding: 12px 14px; margin-bottom: 10px;
}
.tour-step:hover { border-color: var(--primary); }
.tour-step-body { flex: 1; }
.tour-step-title { font-size: 13px; font-weight: 600; color: var(--text-1); margin-bottom: 2px; }
.tour-step-desc { font-size: 12px; color: var(--text-3); }
.cfg-foot-note { text-align: center; font-size: 12px; color: var(--text-3); padding: 4px 0 2px; }