const API_BASE = "http://127.0.0.1:8000"; const USER_ID_STORAGE_KEY = "healthRecordAgent_userId"; const LAST_DIET_RUN_KEY = "healthRecordAgent_lastDietRunId"; const DEV_MODE_STORAGE_KEY = "healthRecordAgent_devMode"; /** 兼容旧版「技术详情」开关 */ const LEGACY_TECH_STORAGE_KEY = "healthRecordAgent_showTech"; function isDeveloperMode() { const el = document.getElementById("devModeToggle"); return !!(el && el.checked); } function getUserIdOrEmpty() { return document.getElementById("userId")?.value?.trim() || ""; } /** 体检分析进度:默认对用户显示中文步骤名 */ function getHealthProgressAgents() { if (isDeveloperMode()) { return [ { key: "PlannerAgent", label: "PlannerAgent 规划" }, { key: "HealthIndicatorAgent", label: "HealthIndicatorAgent 指标" }, { key: "RiskAssessmentAgent", label: "RiskAssessmentAgent 风险" }, { key: "AdviceAgent", label: "AdviceAgent 建议" }, { key: "ReportAgent", label: "ReportAgent 报告" }, ]; } return [ { key: "PlannerAgent", label: "规划" }, { key: "HealthIndicatorAgent", label: "指标解读" }, { key: "RiskAssessmentAgent", label: "风险评估" }, { key: "AdviceAgent", label: "建议" }, { key: "ReportAgent", label: "汇总报告" }, ]; } function getUserId() { const el = document.getElementById("userId"); const raw = el ? el.value.trim() : ""; if (!raw) { alert("请填写用户 ID"); return null; } try { localStorage.setItem(USER_ID_STORAGE_KEY, raw); } catch (_) { /* ignore */ } return raw; } function setTab(name) { const tabs = ["analysis", "diet", "history"]; const n = tabs.includes(name) ? name : "analysis"; tabs.forEach((t) => { const panel = document.getElementById(`tab-${t}`); if (panel) panel.classList.toggle("hidden", t !== n); }); document.querySelectorAll(".tab-segment [role='tab']").forEach((btn) => { const on = btn.dataset.tab === n; btn.setAttribute("aria-selected", on ? "true" : "false"); }); if (`#${n}` !== location.hash) { history.replaceState(null, "", `#${n}`); } if (n === "diet") { refreshReflectRunOptions(); } } function tabFromHash() { const h = (location.hash || "").replace(/^#/, "").toLowerCase(); if (h === "diet" || h === "history" || h === "analysis") return h; return "analysis"; } document.addEventListener("DOMContentLoaded", () => { const el = document.getElementById("userId"); if (el) { try { const saved = localStorage.getItem(USER_ID_STORAGE_KEY); if (saved) el.value = saved; } catch (_) { /* ignore */ } } setTab(tabFromHash()); window.addEventListener("hashchange", () => setTab(tabFromHash())); document.querySelectorAll(".tab-segment [data-tab]").forEach((btn) => { btn.addEventListener("click", () => setTab(btn.dataset.tab || "analysis")); }); const devCb = document.getElementById("devModeToggle"); if (devCb) { try { const dm = localStorage.getItem(DEV_MODE_STORAGE_KEY); const legacy = localStorage.getItem(LEGACY_TECH_STORAGE_KEY); if (dm === "1" || legacy === "1") devCb.checked = true; } catch (_) { /* ignore */ } devCb.addEventListener("change", () => { try { localStorage.setItem(DEV_MODE_STORAGE_KEY, devCb.checked ? "1" : "0"); } catch (_) { /* ignore */ } refreshReflectRunOptions(); }); } const dlg = document.getElementById("reflectPromptDialog"); const go = document.getElementById("reflectDialogGo"); const later = document.getElementById("reflectDialogLater"); if (go) { go.addEventListener("click", () => { if (dlg && typeof dlg.close === "function") dlg.close(); focusFeedbackSection(); }); } if (later) { later.addEventListener("click", () => { if (dlg && typeof dlg.close === "function") dlg.close(); }); } document.querySelectorAll('input[name="reflectFollowedChoice"]').forEach((el) => { el.addEventListener("change", syncReflectReasonVisibility); }); syncReflectReasonVisibility(); }); /** 选「否」时展示未执行原因;选「是」时隐藏并清空原因(后端会将 reason 置为 executed_ok)。 */ function syncReflectReasonVisibility() { const yes = document.getElementById("reflectFollowedYes"); const no = document.getElementById("reflectFollowedNo"); const block = document.getElementById("reflectReasonBlock"); const sel = document.getElementById("reflectReasonCode"); const detail = document.getElementById("reflectDetail"); if (!block || !sel) return; if (no?.checked) { block.classList.remove("hidden"); } else { block.classList.add("hidden"); sel.value = ""; if (detail) detail.value = ""; } } /** 拉取近期饮食推荐,填充「反馈」下拉的选项;preferredRunId 优先选中(如刚生成的一条)。 */ async function refreshReflectRunOptions(preferredRunId) { const sel = document.getElementById("reflectRunSelect"); if (!sel) return; const userId = getUserIdOrEmpty(); sel.innerHTML = ""; const addPlaceholder = (text, disabled = true) => { const o = document.createElement("option"); o.value = ""; o.textContent = text; if (disabled) o.disabled = true; o.selected = true; sel.appendChild(o); }; if (!userId) { addPlaceholder("请先填写用户 ID"); return; } try { const res = await fetch( `${API_BASE}/api/diet/users/${encodeURIComponent(userId)}/runs?limit=20` ); const data = await res.json().catch(() => ({})); const items = data.items || []; if (!items.length) { addPlaceholder("暂无推荐记录,请先生成一次饮食推荐"); return; } const dev = isDeveloperMode(); items.forEach((row) => { const o = document.createElement("option"); o.value = row.run_id; let label = ""; try { const t = row.created_at ? new Date(row.created_at).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }) : ""; const tp = row.total_protein != null ? `约 ${row.total_protein} g 蛋白` : "饮食推荐"; label = t ? `${t} · ${tp}` : tp; if (dev) label += ` · ${row.run_id}`; } catch (_) { label = row.run_id; } o.textContent = label; sel.appendChild(o); }); const pick = preferredRunId || (() => { try { return localStorage.getItem(LAST_DIET_RUN_KEY); } catch (_) { return null; } })(); if (pick && Array.from(sel.options).some((opt) => opt.value === pick)) { sel.value = pick; } } catch (e) { console.error(e); addPlaceholder("加载推荐列表失败,请稍后重试"); } } function openReflectPromptDialog() { const dlg = document.getElementById("reflectPromptDialog"); if (dlg && typeof dlg.showModal === "function") { dlg.showModal(); } else { focusFeedbackSection(); } } function focusFeedbackSection() { const h = document.getElementById("feedbackSectionTitle"); h?.scrollIntoView({ behavior: "smooth", block: "start" }); const first = document.getElementById("reflectRunSelect"); if (first) { setTimeout(() => first.focus(), 400); } } function renderMealPlan(mp) { if (!mp) return "
(无 meal_plan)
"; const tips = Array.isArray(mp.tips) ? mp.tips.filter(Boolean).join(";") : ""; let h = `估算总蛋白:${mp.total_est_protein_g ?? "—"} g
提示:${escapeHtml(tips)}
`; return h; } function escapeHtml(s) { if (!s) return ""; const div = document.createElement("div"); div.textContent = s; return div.innerHTML; } async function recommendDiet() { const userId = getUserId(); if (!userId) return; const statusEl = document.getElementById("dietStatus"); const outEl = document.getElementById("dietResult"); if (!statusEl || !outEl) return; statusEl.textContent = isDeveloperMode() ? "⏳ 正在调用 Planning + ReAct(可能需多次 LLM,请稍候)…" : "⏳ 正在生成推荐,请稍候…"; outEl.classList.add("hidden"); outEl.innerHTML = ""; const foodLog = document.getElementById("dietFoodLog")?.value?.trim() || ""; if (!foodLog) { statusEl.textContent = "⚠️ 请先填写今天吃了什么"; return; } const body = { user_id: userId, context: { today_food_log_text: foodLog, goal: document.getElementById("dietGoal")?.value || "muscle_gain", channels: ["convenience_store", "delivery"], activity_context: document.getElementById("dietActivityContext")?.value?.trim() || "", free_notes: document.getElementById("dietNotes")?.value?.trim() || "", }, }; try { const res = await fetch(`${API_BASE}/api/diet/recommend`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data.detail ? JSON.stringify(data.detail) : `HTTP ${res.status}`); } const runId = data.run_id; try { localStorage.setItem(LAST_DIET_RUN_KEY, runId); } catch (_) { /* ignore */ } const planning = data.planning || {}; const ver = data.schema_version || "1"; const mode = data.pipeline_mode || "legacy"; const tech = isDeveloperMode(); let html = ""; if (tech) { html += `run_id:${escapeHtml(runId)} schema=${escapeHtml(String(ver))} / ${escapeHtml(String(mode))}
Planning(Nutritionist 摘要):${escapeHtml(planning.reasoning)}
` : `营养分析摘要:${escapeHtml(planning.reasoning)}
`; } const ns = data.nutrition_summary || {}; if (!tech) { html += `今日营养估算:蛋白 ${escapeHtml(String(ns.protein_g ?? 0))}g,碳水 ${escapeHtml(String(ns.carb_g ?? 0))}g,脂肪 ${escapeHtml(String(ns.fat_g ?? 0))}g,热量 ${escapeHtml(String(ns.calories_kcal ?? 0))} kcal
`; } else { html += `${escapeHtml(JSON.stringify({ food_parse: data.food_parse, nutrition_summary: data.nutrition_summary }, null, 2))}Habit · Reflect 对齐:${escapeHtml(hx.reflect_alignment)}
` : `与历史反馈对齐:${escapeHtml(hx.reflect_alignment)}
`; if (hx.execution_hints && hx.execution_hints.length) { html += `执行提示:${escapeHtml(hx.execution_hints.join(";"))}
`; } } html += `${escapeHtml(JSON.stringify(data.errors, null, 2))}${escapeHtml(String(data.reflect_memory_used))}${escapeHtml(JSON.stringify(data.react_trace, null, 2))}❌ 未返回报告内容
"; reportDiv.innerHTML = typeof summary === "string" ? summary : JSON.stringify(summary, null, 2); analysisDiv.innerText = doneText; resultCard.classList.remove("hidden"); } catch (error) { const errorMessage = error?.message || JSON.stringify(error); console.error("任务提交或轮询出错:", errorMessage); reportDiv.innerHTML = `❌ ${errorText}: ${errorMessage}
`; analysisDiv.innerText = `❌ ${errorText}`; progressList.innerHTML = ""; } } // 文本报告分析 async function analyze() { const userId = getUserId(); if (!userId) return; const reportText = document.getElementById("reportText").value; if (!reportText) { alert("请输入体检报告内容"); return; } const resultCard = document.getElementById("resultCard"); const reportDiv = document.getElementById("report"); const analysisDiv = document.getElementById("analysis"); const progressList = document.getElementById("progressList"); const agents = getHealthProgressAgents(); await submitAndPollTask( `${API_BASE}/api/health/analysis`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ report_text: reportText, user_id: userId }) }, agents, resultCard, reportDiv, analysisDiv, progressList, isDeveloperMode() ? "⏳ 正在分析文本报告,请稍候…" : "⏳ 正在分析,请稍候…", "✅ 分析完成", "报告生成失败" ); } // PDF报告分析 async function uploadPDF() { const userId = getUserId(); if (!userId) return; const fileInput = document.getElementById("pdfFile"); const file = fileInput.files[0]; if (!file) { alert("请选择PDF文件"); return; } const formData = new FormData(); formData.append("user_id", userId); formData.append("file", file); const resultCard = document.getElementById("resultCard"); const reportDiv = document.getElementById("report"); const analysisDiv = document.getElementById("analysis"); const progressList = document.getElementById("progressList"); const agents = getHealthProgressAgents(); await submitAndPollTask( `${API_BASE}/api/health/analysis/pdf`, { method: "POST", body: formData }, agents, resultCard, reportDiv, analysisDiv, progressList, isDeveloperMode() ? "⏳ 正在分析 PDF 报告,请稍候…" : "⏳ 正在分析 PDF,请稍候…", "✅ 分析完成", "上传失败" ); }