Spaces:
Runtime error
Runtime error
| const sampleResume = `张同学 | 计算机科学与技术 | 2026届硕士 | |
| 求职方向:大模型应用算法 / 推荐算法实习生,期望城市深圳或北京。 | |
| 技能:Python、PyTorch、Transformer、RAG、Agent、Embedding、FAISS、LangChain、推荐系统、召回排序、NDCG、A/B Test、SQL。 | |
| 项目经历: | |
| 1. GenAdRec 生成式广告推荐项目:基于 Transformer 建模用户行为序列,将广告候选集转化为生成式 likelihood rerank 问题;构建 Semantic ID 表示广告 item,结合多兴趣召回提升 NDCG@10。 | |
| 2. LLM 求职助手 Demo:使用 DeepSeek API 和 bge embedding 实现 JD 检索、简历关键词诊断、Prompt 模板优化,支持输出岗位匹配解释。 | |
| 3. MIND 多兴趣推荐复现:复现 capsule routing 用户多兴趣建模,在公开数据集上对比召回 HitRate 与 NDCG。 | |
| 实习经历: | |
| 曾参与推荐系统离线评估脚本开发,负责样本构造、特征清洗和模型结果分析。 | |
| 补充:希望找能结合 LLM、Agent、RAG 和推荐排序的算法岗位。`; | |
| const jobs = [ | |
| { | |
| id: "llm-agent", | |
| title: "大模型应用算法实习生", | |
| company: "腾讯云智能", | |
| city: "深圳", | |
| direction: "大模型应用算法", | |
| stage: "实习", | |
| skills: ["LLM", "RAG", "Agent", "Embedding", "Python", "Prompt", "LangChain", "FAISS"], | |
| projectSignals: ["RAG", "Agent", "Embedding", "Prompt", "检索", "简历", "JD"], | |
| jd: "负责企业知识库问答、RAG 检索增强、Agent 工具调用、多轮对话效果优化,要求熟悉 Python、Embedding、Prompt Engineering 和 LLM 应用评估。", | |
| interviewThemes: ["RAG 召回与重排", "Agent 工具调用失败兜底", "Prompt 结构化输出", "Embedding 评估指标"] | |
| }, | |
| { | |
| id: "rec-llm", | |
| title: "LLM 推荐算法实习生", | |
| company: "内容平台事业群", | |
| city: "北京", | |
| direction: "推荐算法", | |
| stage: "实习", | |
| skills: ["推荐系统", "Transformer", "召回", "排序", "Embedding", "NDCG", "PyTorch", "A/B Test"], | |
| projectSignals: ["推荐", "Transformer", "Semantic ID", "rerank", "NDCG", "召回"], | |
| jd: "负责内容推荐召回排序模型优化,探索 LLM 与推荐系统结合,包括语义表征、用户兴趣建模、候选集重排和离线指标评估。", | |
| interviewThemes: ["召回排序链路", "多兴趣建模", "Semantic ID", "NDCG 与线上指标"] | |
| }, | |
| { | |
| id: "search-rag", | |
| title: "智能搜索算法实习生", | |
| company: "微信事业群", | |
| city: "广州", | |
| direction: "大模型应用算法", | |
| stage: "实习", | |
| skills: ["搜索", "RAG", "向量检索", "Embedding", "重排", "Python", "评估"], | |
| projectSignals: ["检索", "RAG", "Embedding", "FAISS", "重排", "TopK"], | |
| jd: "建设面向业务知识的智能搜索系统,优化 query 理解、向量召回、cross-encoder 重排和答案生成质量。", | |
| interviewThemes: ["向量召回 TopK", "重排模型", "搜索相关性评估", "幻觉控制"] | |
| }, | |
| { | |
| id: "backend-ai", | |
| title: "AI 平台后端研发实习生", | |
| company: "混元平台", | |
| city: "深圳", | |
| direction: "后端研发", | |
| stage: "实习", | |
| skills: ["Python", "FastAPI", "服务部署", "SQL", "Redis", "API", "Docker"], | |
| projectSignals: ["API", "后端", "部署", "服务", "SQL"], | |
| jd: "负责大模型平台后端服务开发、API 编排、任务队列、日志监控和模型服务接入。", | |
| interviewThemes: ["API 设计", "异步任务", "缓存策略", "服务稳定性"] | |
| }, | |
| { | |
| id: "data-analyst", | |
| title: "校招数据分析实习生", | |
| company: "HR Tech", | |
| city: "上海", | |
| direction: "数据分析", | |
| stage: "实习", | |
| skills: ["SQL", "Python", "可视化", "指标体系", "漏斗分析", "A/B Test"], | |
| projectSignals: ["SQL", "指标", "分析", "A/B", "可视化"], | |
| jd: "围绕校招转化漏斗、渠道质量和简历初筛效果进行数据分析,建设可视化看板并输出业务洞察。", | |
| interviewThemes: ["漏斗指标", "归因分析", "SQL 窗口函数", "实验设计"] | |
| }, | |
| { | |
| id: "product-ai", | |
| title: "AI 产品经理实习生", | |
| company: "企服产品部", | |
| city: "深圳", | |
| direction: "产品经理", | |
| stage: "实习", | |
| skills: ["用户研究", "Prompt", "产品设计", "数据分析", "原型", "LLM"], | |
| projectSignals: ["用户", "产品", "Prompt", "Demo", "需求"], | |
| jd: "负责 AI 助手类产品需求分析、Prompt 配置、原型设计、效果验收和用户反馈闭环。", | |
| interviewThemes: ["需求拆解", "Prompt 评估", "用户体验", "产品指标"] | |
| }, | |
| { | |
| id: "campus-rec", | |
| title: "校招人岗匹配算法实习生", | |
| company: "招聘技术中心", | |
| city: "北京", | |
| direction: "大模型应用算法", | |
| stage: "实习", | |
| skills: ["人岗匹配", "推荐系统", "Embedding", "LLM", "排序", "可解释性", "Python"], | |
| projectSignals: ["岗位", "简历", "匹配", "推荐", "排序", "Embedding"], | |
| jd: "基于简历和 JD 构建人岗匹配模型,优化候选人召回、岗位推荐排序、匹配解释和简历优化建议。", | |
| interviewThemes: ["人岗匹配建模", "冷启动", "特征交叉", "可解释排序"] | |
| }, | |
| { | |
| id: "game-ai", | |
| title: "游戏 AI 算法实习生", | |
| company: "互动娱乐事业群", | |
| city: "上海", | |
| direction: "大模型应用算法", | |
| stage: "校招", | |
| skills: ["LLM", "强化学习", "Agent", "Python", "多模态", "评估"], | |
| projectSignals: ["Agent", "多模态", "评估", "LLM"], | |
| jd: "探索游戏 NPC Agent、剧情生成、行为决策和多模态内容理解,要求有 LLM Agent 或强化学习项目经验。", | |
| interviewThemes: ["Agent 记忆机制", "行为决策", "多模态理解", "自动评估"] | |
| } | |
| ]; | |
| const agents = [ | |
| ["Resume Parser", "抽取教育、技能、项目、经历"], | |
| ["JD Understanding", "解析岗位技能与业务要求"], | |
| ["Matching Ranker", "召回 TopK 并多维排序"], | |
| ["Gap Analyzer", "定位能力缺口与简历风险"], | |
| ["Resume Coach", "生成改写建议与面试计划"] | |
| ]; | |
| const skillCatalog = [ | |
| "LLM", "RAG", "Agent", "Embedding", "FAISS", "LangChain", "Prompt", "Python", | |
| "PyTorch", "Transformer", "推荐系统", "召回", "排序", "NDCG", "A/B Test", | |
| "SQL", "FastAPI", "Docker", "Redis", "搜索", "重排", "向量检索", "多模态", | |
| "强化学习", "产品设计", "可视化", "指标体系" | |
| ]; | |
| const state = { | |
| results: [], | |
| selectedIndex: 0, | |
| activeTab: "strengths" | |
| }; | |
| const el = { | |
| resumeInput: document.querySelector("#resumeInput"), | |
| targetRole: document.querySelector("#targetRole"), | |
| targetCity: document.querySelector("#targetCity"), | |
| careerStage: document.querySelector("#careerStage"), | |
| runBtn: document.querySelector("#runBtn"), | |
| clearBtn: document.querySelector("#clearBtn"), | |
| sampleBtn: document.querySelector("#sampleBtn"), | |
| agentStrip: document.querySelector("#agentStrip"), | |
| jobList: document.querySelector("#jobList"), | |
| bestTitle: document.querySelector("#bestTitle"), | |
| bestCompany: document.querySelector("#bestCompany"), | |
| bestScore: document.querySelector("#bestScore"), | |
| skillCount: document.querySelector("#skillCount"), | |
| skillPreview: document.querySelector("#skillPreview"), | |
| detailTitle: document.querySelector("#detailTitle"), | |
| detailMeta: document.querySelector("#detailMeta"), | |
| scoreBreakdown: document.querySelector("#scoreBreakdown"), | |
| radarCanvas: document.querySelector("#radarCanvas"), | |
| tabContent: document.querySelector("#tabContent"), | |
| tabs: Array.from(document.querySelectorAll(".tab")) | |
| }; | |
| function normalize(text) { | |
| return (text || "").toLowerCase(); | |
| } | |
| function includesTerm(text, term) { | |
| return normalize(text).includes(term.toLowerCase()); | |
| } | |
| function unique(items) { | |
| return Array.from(new Set(items)).filter(Boolean); | |
| } | |
| function clampScore(score) { | |
| return Math.max(0, Math.min(100, Math.round(score))); | |
| } | |
| function extractProfile(resume) { | |
| const foundSkills = skillCatalog.filter((skill) => includesTerm(resume, skill)); | |
| const projectSignals = unique([ | |
| ...foundSkills, | |
| ...["Semantic ID", "rerank", "MIND", "简历", "JD", "岗位", "检索", "评估", "Demo"].filter((term) => | |
| includesTerm(resume, term) | |
| ) | |
| ]); | |
| return { | |
| foundSkills, | |
| projectSignals, | |
| hasMetric: /ndcg|hitrate|auc|准确率|召回率|提升|%|topk/i.test(resume), | |
| hasLLMProject: /llm|rag|agent|prompt|deepseek|openai|通义|混元/i.test(resume), | |
| hasRecProject: /推荐|召回|排序|mind|semantic id|rerank|用户兴趣/i.test(resume) | |
| }; | |
| } | |
| function overlapCount(required, actual, resume) { | |
| return required.filter((term) => actual.includes(term) || includesTerm(resume, term)).length; | |
| } | |
| function scoreJob(job, profile, resume, preferences) { | |
| const skillHits = overlapCount(job.skills, profile.foundSkills, resume); | |
| const projectHits = overlapCount(job.projectSignals, profile.projectSignals, resume); | |
| const skillScore = clampScore((skillHits / job.skills.length) * 100); | |
| const projectBase = job.projectSignals.length ? (projectHits / job.projectSignals.length) * 100 : 60; | |
| const projectBonus = profile.hasMetric ? 8 : 0; | |
| const projectScore = clampScore(projectBase + projectBonus); | |
| const experienceScore = clampScore(job.stage === preferences.stage ? 86 : 68); | |
| const directionScore = clampScore( | |
| job.direction === preferences.role ? 92 : includesTerm(job.direction, preferences.role) ? 80 : 55 | |
| ); | |
| const cityScore = clampScore( | |
| preferences.city === "不限" ? 88 : job.city === preferences.city ? 96 : 62 | |
| ); | |
| const preferenceScore = clampScore(directionScore * 0.66 + cityScore * 0.34); | |
| const growthScore = clampScore( | |
| 48 + | |
| Math.min(profile.foundSkills.length, 10) * 3 + | |
| (profile.hasLLMProject ? 12 : 0) + | |
| (profile.hasRecProject ? 8 : 0) | |
| ); | |
| const total = clampScore( | |
| 0.32 * skillScore + | |
| 0.24 * projectScore + | |
| 0.18 * experienceScore + | |
| 0.16 * preferenceScore + | |
| 0.1 * growthScore | |
| ); | |
| const matchedSkills = job.skills.filter((term) => includesTerm(resume, term)); | |
| const missingSkills = job.skills.filter((term) => !includesTerm(resume, term)); | |
| return { | |
| ...job, | |
| total, | |
| matchedSkills, | |
| missingSkills, | |
| dimensions: { | |
| "技能匹配": skillScore, | |
| "项目相关": projectScore, | |
| "经历阶段": experienceScore, | |
| "偏好一致": preferenceScore, | |
| "成长潜力": growthScore | |
| }, | |
| strengths: buildStrengths(job, matchedSkills, profile), | |
| gaps: buildGaps(job, missingSkills, profile), | |
| rewrites: buildRewrites(job, matchedSkills, missingSkills, profile), | |
| plan: buildPlan(job, missingSkills) | |
| }; | |
| } | |
| function buildStrengths(job, matchedSkills, profile) { | |
| const strengths = []; | |
| if (matchedSkills.length) { | |
| strengths.push(`命中岗位核心关键词:${matchedSkills.slice(0, 6).join("、")},具备进入精排池的基础。`); | |
| } | |
| if (profile.hasLLMProject && job.direction.includes("大模型")) { | |
| strengths.push("简历中已经出现 LLM/RAG/Agent 相关项目,可直接对齐大模型应用算法岗位。"); | |
| } | |
| if (profile.hasRecProject && (job.direction.includes("推荐") || job.jd.includes("匹配"))) { | |
| strengths.push("推荐系统、召回排序和语义表征经历能自然迁移到人岗匹配或内容推荐场景。"); | |
| } | |
| if (profile.hasMetric) { | |
| strengths.push("项目描述中包含 NDCG、HitRate、TopK 等指标,便于证明算法效果。"); | |
| } | |
| if (!strengths.length) { | |
| strengths.push("简历具备基础技术栈,但需要补充更明确的项目证据和岗位关键词。"); | |
| } | |
| return strengths; | |
| } | |
| function buildGaps(job, missingSkills, profile) { | |
| const gaps = []; | |
| if (missingSkills.length) { | |
| gaps.push(`岗位还要求 ${missingSkills.slice(0, 5).join("、")},建议在项目或技能栏补充真实经历。`); | |
| } | |
| if (!profile.hasMetric) { | |
| gaps.push("项目缺少可量化指标,容易被认为只是功能实现,建议补充离线评估或效果提升。"); | |
| } | |
| if (job.jd.includes("Agent") && !includesTerm(profile.projectSignals.join(" "), "Agent")) { | |
| gaps.push("Agent 工作流表达不够突出,需要写清任务拆解、工具调用、失败兜底和评估方式。"); | |
| } | |
| if (job.jd.includes("评估") && !/评估|指标|NDCG|HitRate|A\/B/i.test(profile.projectSignals.join(" "))) { | |
| gaps.push("岗位强调效果评估,简历需补充准确率、相关性、召回率或人工验收指标。"); | |
| } | |
| return gaps; | |
| } | |
| function buildRewrites(job, matchedSkills, missingSkills, profile) { | |
| const mainSkill = matchedSkills[0] || job.skills[0]; | |
| const missing = missingSkills[0] || job.skills[job.skills.length - 1]; | |
| return [ | |
| { | |
| before: "负责推荐系统建模和模型结果分析。", | |
| after: `围绕 ${job.direction} 场景,使用 ${mainSkill} 构建候选召回与精排链路,并通过 NDCG@10 / TopK 命中率评估模型收益。` | |
| }, | |
| { | |
| before: "做过一个 LLM 求职助手 Demo。", | |
| after: `设计多 Agent 求职匹配流程,将简历解析、JD 理解、岗位排序、能力缺口诊断和简历优化拆成可解释节点,提升输出稳定性。` | |
| }, | |
| { | |
| before: "熟悉大模型相关技术。", | |
| after: `熟悉 ${job.skills.slice(0, 4).join("、")};可独立完成 Prompt 模板、结构化输出、检索增强和结果评估配置。` | |
| }, | |
| { | |
| before: "技能栏可以继续补充。", | |
| after: `若确有实践,建议补充 ${missing},并用一次实验、接口调用或离线评估证明不是仅停留在概念层。` | |
| } | |
| ]; | |
| } | |
| function buildPlan(job, missingSkills) { | |
| const focus = missingSkills.slice(0, 3); | |
| return [ | |
| `第 1 天:复盘 ${job.direction} 岗位 JD,整理岗位关键词和自己项目的对应证据。`, | |
| `第 2 天:准备 ${job.interviewThemes[0]},用一个项目讲清输入、模型、输出、指标。`, | |
| `第 3 天:补齐 ${focus[0] || job.skills[0]} 的实践案例,写成可追问的简历 bullet。`, | |
| `第 4 天:准备 ${job.interviewThemes[1]},重点讲失败 case 和优化方案。`, | |
| `第 5 天:针对 ${job.company} 场景模拟 3 个业务问题,练习算法方案设计。`, | |
| "第 6 天:做一次 20 分钟项目深挖 mock,覆盖数据、模型、评估、工程落地。", | |
| "第 7 天:压缩成 1 分钟自我介绍、3 分钟项目介绍和 5 个高频追问答案。" | |
| ]; | |
| } | |
| function runMatch() { | |
| const resume = el.resumeInput.value.trim(); | |
| if (!resume) { | |
| renderEmpty("请先粘贴简历文本,或点击“填充示例”。"); | |
| return; | |
| } | |
| renderAgents(true); | |
| const preferences = { | |
| role: el.targetRole.value, | |
| city: el.targetCity.value, | |
| stage: el.careerStage.value | |
| }; | |
| const profile = extractProfile(resume); | |
| state.results = jobs | |
| .map((job) => scoreJob(job, profile, resume, preferences)) | |
| .sort((a, b) => b.total - a.total); | |
| state.selectedIndex = 0; | |
| state.activeTab = "strengths"; | |
| renderAll(profile); | |
| } | |
| function renderAll(profile) { | |
| renderAgents(false); | |
| renderSummary(profile); | |
| renderJobList(); | |
| renderDetail(); | |
| } | |
| function renderAgents(active) { | |
| el.agentStrip.innerHTML = agents | |
| .map( | |
| ([name, desc], index) => ` | |
| <div class="agent-step ${active || state.results.length ? "active" : ""}"> | |
| <strong>${index + 1}. ${name}</strong> | |
| <span>${desc}</span> | |
| </div> | |
| ` | |
| ) | |
| .join(""); | |
| } | |
| function renderSummary(profile) { | |
| const best = state.results[0]; | |
| el.bestTitle.textContent = best.title; | |
| el.bestCompany.textContent = `${best.company} · ${best.city} · ${best.stage}`; | |
| el.bestScore.textContent = `${best.total}/100`; | |
| el.skillCount.textContent = `${profile.foundSkills.length} 个`; | |
| el.skillPreview.textContent = profile.foundSkills.slice(0, 5).join("、") || "未命中明显技能词"; | |
| } | |
| function renderJobList() { | |
| el.jobList.innerHTML = state.results | |
| .map((job, index) => { | |
| const tags = job.skills | |
| .slice(0, 5) | |
| .map((skill) => `<span class="tag ${job.matchedSkills.includes(skill) ? "hit" : ""}">${skill}</span>`) | |
| .join(""); | |
| return ` | |
| <article class="job-card ${index === state.selectedIndex ? "active" : ""}" data-index="${index}"> | |
| <div class="job-row-top"> | |
| <div class="job-title">${job.title}</div> | |
| <div class="score-pill">${job.total}</div> | |
| </div> | |
| <div class="job-meta">${job.company} · ${job.city} · ${job.direction}</div> | |
| <div class="job-tags">${tags}</div> | |
| </article> | |
| `; | |
| }) | |
| .join(""); | |
| document.querySelectorAll(".job-card").forEach((card) => { | |
| card.addEventListener("click", () => { | |
| state.selectedIndex = Number(card.dataset.index); | |
| state.activeTab = "strengths"; | |
| renderJobList(); | |
| renderDetail(); | |
| }); | |
| }); | |
| } | |
| function renderDetail() { | |
| const job = state.results[state.selectedIndex]; | |
| if (!job) { | |
| renderEmpty("运行匹配后查看岗位诊断。"); | |
| return; | |
| } | |
| el.detailTitle.textContent = job.title; | |
| el.detailMeta.textContent = `${job.company} · ${job.city} · ${job.stage}`; | |
| renderBreakdown(job); | |
| drawRadar(job.dimensions); | |
| updateTabs(); | |
| renderTabContent(job); | |
| } | |
| function renderBreakdown(job) { | |
| el.scoreBreakdown.innerHTML = Object.entries(job.dimensions) | |
| .map( | |
| ([name, value]) => ` | |
| <div class="score-line"> | |
| <span>${name}</span> | |
| <div class="bar"><span style="width:${value}%"></span></div> | |
| <strong>${value}</strong> | |
| </div> | |
| ` | |
| ) | |
| .join(""); | |
| } | |
| function drawRadar(dimensions) { | |
| const canvas = el.radarCanvas; | |
| const ctx = canvas.getContext("2d"); | |
| const width = canvas.width; | |
| const height = canvas.height; | |
| const cx = width / 2; | |
| const cy = height / 2 + 4; | |
| const radius = 82; | |
| const entries = Object.entries(dimensions); | |
| ctx.clearRect(0, 0, width, height); | |
| ctx.strokeStyle = "#d9e0e8"; | |
| ctx.fillStyle = "#667085"; | |
| ctx.font = "12px Microsoft YaHei, sans-serif"; | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "middle"; | |
| for (let level = 1; level <= 4; level += 1) { | |
| drawPolygon(ctx, entries.length, cx, cy, (radius * level) / 4, false); | |
| } | |
| entries.forEach(([label], index) => { | |
| const angle = (Math.PI * 2 * index) / entries.length - Math.PI / 2; | |
| const x = cx + Math.cos(angle) * (radius + 24); | |
| const y = cy + Math.sin(angle) * (radius + 18); | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy); | |
| ctx.lineTo(cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius); | |
| ctx.stroke(); | |
| ctx.fillText(label, x, y); | |
| }); | |
| const points = entries.map(([, value], index) => { | |
| const angle = (Math.PI * 2 * index) / entries.length - Math.PI / 2; | |
| const pointRadius = (radius * value) / 100; | |
| return [cx + Math.cos(angle) * pointRadius, cy + Math.sin(angle) * pointRadius]; | |
| }); | |
| ctx.beginPath(); | |
| points.forEach(([x, y], index) => { | |
| if (index === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| }); | |
| ctx.closePath(); | |
| ctx.fillStyle = "rgba(15, 143, 114, 0.18)"; | |
| ctx.strokeStyle = "#0f8f72"; | |
| ctx.lineWidth = 2; | |
| ctx.fill(); | |
| ctx.stroke(); | |
| } | |
| function drawPolygon(ctx, sides, cx, cy, radius, fill) { | |
| ctx.beginPath(); | |
| for (let i = 0; i < sides; i += 1) { | |
| const angle = (Math.PI * 2 * i) / sides - Math.PI / 2; | |
| const x = cx + Math.cos(angle) * radius; | |
| const y = cy + Math.sin(angle) * radius; | |
| if (i === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| } | |
| ctx.closePath(); | |
| if (fill) ctx.fill(); | |
| ctx.stroke(); | |
| } | |
| function updateTabs() { | |
| el.tabs.forEach((tab) => { | |
| tab.classList.toggle("active", tab.dataset.tab === state.activeTab); | |
| }); | |
| } | |
| function renderTabContent(job) { | |
| if (state.activeTab === "strengths") { | |
| el.tabContent.innerHTML = listHtml(job.strengths); | |
| return; | |
| } | |
| if (state.activeTab === "gaps") { | |
| el.tabContent.innerHTML = listHtml(job.gaps, "warning"); | |
| return; | |
| } | |
| if (state.activeTab === "rewrite") { | |
| el.tabContent.innerHTML = ` | |
| <div class="rewrite-block"> | |
| ${job.rewrites | |
| .map( | |
| (item) => ` | |
| <div class="rewrite-card"> | |
| <span>原表达</span> | |
| <p>${item.before}</p> | |
| </div> | |
| <div class="rewrite-card"> | |
| <span>建议改写</span> | |
| <p>${item.after}</p> | |
| </div> | |
| ` | |
| ) | |
| .join("")} | |
| </div> | |
| `; | |
| return; | |
| } | |
| el.tabContent.innerHTML = listHtml(job.plan); | |
| } | |
| function listHtml(items, className = "") { | |
| return ` | |
| <ul class="insight-list"> | |
| ${items.map((item) => `<li class="${className}">${item}</li>`).join("")} | |
| </ul> | |
| `; | |
| } | |
| function renderEmpty(message) { | |
| el.jobList.innerHTML = `<div class="empty-state">${message}</div>`; | |
| el.tabContent.innerHTML = `<div class="empty-state">${message}</div>`; | |
| } | |
| el.runBtn.addEventListener("click", runMatch); | |
| el.sampleBtn.addEventListener("click", () => { | |
| el.resumeInput.value = sampleResume; | |
| runMatch(); | |
| }); | |
| el.clearBtn.addEventListener("click", () => { | |
| el.resumeInput.value = ""; | |
| state.results = []; | |
| el.bestTitle.textContent = "等待分析"; | |
| el.bestCompany.textContent = "填写简历后运行"; | |
| el.bestScore.textContent = "--"; | |
| el.skillCount.textContent = "--"; | |
| el.skillPreview.textContent = "待解析"; | |
| renderAgents(false); | |
| renderEmpty("请先粘贴简历文本,或点击“填充示例”。"); | |
| drawRadar({ "技能匹配": 0, "项目相关": 0, "经历阶段": 0, "偏好一致": 0, "成长潜力": 0 }); | |
| }); | |
| el.tabs.forEach((tab) => { | |
| tab.addEventListener("click", () => { | |
| state.activeTab = tab.dataset.tab; | |
| updateTabs(); | |
| const job = state.results[state.selectedIndex]; | |
| if (job) renderTabContent(job); | |
| }); | |
| }); | |
| el.resumeInput.value = sampleResume; | |
| renderAgents(false); | |
| runMatch(); | |