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

- 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,
"embed": semantic.status(_cfg),
"guided": bool(_cfg.get("guided")),
"demo_mode": bool(_cfg.get("demo_mode", True)),
"mode": "online" if ready else "offline"})
@@ -221,6 +222,7 @@ class ConfigBody(BaseModel):
embed_api_base_url: str | None = None
embed_api_model: str | None = None
guided: bool | None = None # 初始设置引导完成标记
demo_mode: bool | None = None # 竞赛演示模式开关
mode: str | None = None # demo | live
@@ -243,6 +245,7 @@ def _public_config() -> dict:
},
"embed": semantic.status(_cfg),
"mode": str(_cfg.get("mode", "demo")),
"demo_mode": bool(_cfg.get("demo_mode", True)),
"offline_fallback": not ready,
}
@@ -290,15 +293,17 @@ def save_config(body: ConfigBody) -> dict:
updates["embed_api_model"] = body.embed_api_model.strip()
if body.guided is not None:
updates["guided"] = bool(body.guided)
if body.demo_mode is not None:
updates["demo_mode"] = bool(body.demo_mode)
save_runtime_settings(updates)
# 热生效:令牌、模式与 LLM/语义模型配置立即更新到运行配置(下次调用即读取)
for k in ("access_token", "mode", "llm_api_key", "llm_base_url", "llm_model",
"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 updates[k]:
_cfg[k] = updates[k]
if updates[k] or isinstance(updates[k], bool):
_cfg[k] = updates[k] # 布尔 False 也要写入(如关闭演示模式)
else:
_cfg.pop(k, None)
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",
"embed_model_path", "embed_provider", "embed_api_key",
"embed_api_base_url", "embed_api_model",
"guided", "jd_extra_dirs", "passive_sources",
"guided", "jd_extra_dirs", "passive_sources", "demo_mode",
}
DEFAULTS: dict = {
"mode": "demo", # demo | live
"demo_mode": True, # 竞赛演示模式:不弹初始设置、警告降级为状态点
"host": "127.0.0.1",
"port": 9008,
"data_dir": "data",
+79 -10
View File
@@ -255,7 +255,7 @@ async function loadDashboard() {
CN.graphData = graph;
const s = health.graph || {};
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 || []);
@@ -1421,21 +1421,31 @@ $("#btn-export-path").onclick = () => {
toast("已导出文本", "success");
};
/* ================= 系统设置 / 状态横幅 ================= */
let lastHealth = null; // 最近一次 health 响应(横幅状态源)
/* ================= 系统设置 / 状态横幅 / 演示模式 ================= */
let lastHealth = null; // 最近一次 health 响应(横幅与状态徽标的数据源)
function updateBanners(health) {
if (health) lastHealth = health;
const h = lastHealth || {};
const showOffline = !!h.offline_fallback
&& sessionStorage.getItem("cn_banner_dismissed") !== "1";
const demo = h.demo_mode !== false;
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";
$("#offline-banner").classList.toggle("hidden", !showOffline);
$("#embed-banner").classList.toggle("hidden", !showEmbed);
const n = (showOffline ? 1 : 0) + (showEmbed ? 1 : 0);
document.body.classList.toggle("has-banner", n === 1);
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) {
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");
$("#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) {
if (emb.ready) {
@@ -1506,6 +1554,13 @@ async function openConfigModal(needToken, firstRun) {
<button class="btn btn-secondary btn-sm" id="cfg-token-show">显示</button>
</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 class="cfg-sec">
<div class="cfg-sec-title">LLM API(星火 / OpenAI 兼容,保存即热生效)</div>
@@ -1584,7 +1639,8 @@ async function openConfigModal(needToken, firstRun) {
</ol>
</div>
</div>
</div>`,
</div>
<div class="cfg-foot-note">如遇问题,请联系 support@rrrosmontis.icu</div>`,
`${firstRun ? `<button class="btn btn-secondary" id="cfg-skip">跳过,使用离线演示模式</button>` : ""}
<button class="btn btn-secondary" id="cfg-save-restart">保存并重启</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 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) => {
b.onclick = () => {
$$("#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_api_base_url: $("#cfg-embed-base").value.trim(),
embed_api_model: $("#cfg-embed-model").value.trim(),
demo_mode: demoMode,
guided: true, // 保存即视为完成初始设置
};
const tok = $("#cfg-token").value;
@@ -1643,7 +1708,7 @@ async function openConfigModal(needToken, firstRun) {
else localStorage.removeItem("cn_access_token");
}
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();
return saved;
@@ -1716,6 +1781,8 @@ window.addEventListener("resize", () => {
CN.jobs = await api("/api/jobs");
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").value = "job_Java开发工程师"; // 演化页默认锁定最强样例
$("#match-job-select").value = "job_Java开发工程师";
await loadDashboard(); // 先加载图谱缓存(演化页甘特图依赖)
await loadEvolution();
renderEvoHeatmap();
@@ -1723,8 +1790,10 @@ window.addEventListener("resize", () => {
const hash = new URLSearchParams(location.hash.slice(1));
if (hash.get("view")) switchView(hash.get("view"));
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) {
toast("后端连接失败:" + e.message, "error");
}
+4 -4
View File
@@ -10,8 +10,8 @@
const G = {
nodes: [], edges: [], events: [], idx: {},
cam: { x: 0, y: 0, k: 1 }, // 相机:世界 -> 屏幕 screen = (world - cam.pan) * k ... 使用 cam.x/y 为平移(屏幕像素)
showLabels: true,
filters: { types: { Job: true, Skill: true, Industry: true, Scenario: true }, category: "", level: "",
showLabels: false, // 默认关闭:悬停/选中/缩放时才显示
filters: { types: { Job: true, Skill: true, Industry: false, Scenario: false }, category: "", level: "",
edges: { required: true, preferred: true, applies: true, belongs: true } },
selected: null, // 选中节点 id
soloJob: null, // 岗位子图模式:只看该岗位及其一跳邻居
@@ -444,7 +444,7 @@
$("#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-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 } };
$$("#graph-filter-panel input[data-ftype]").forEach((cb) => { cb.checked = true; });
$$("#graph-filter-panel input[data-etype]").forEach((cb) => { cb.checked = true; });
@@ -487,7 +487,7 @@
if (!v) return false;
G.cam = v.cam;
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 } },
v.filters);
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;
}
.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; }