一、前言

        在我国,糖尿病、高血压等慢病患者已超4亿人,居家慢病管理中 "咨询难、解答不专业、随访不及时" 成为普遍痛点。基层医生精力有限,大医院挂号难,很多患者的日常健康疑问得不到及时、专业的解答。

        今天,我们就从实际需求出发,结合安诊儿AntAngelMed医疗大模型的专业医疗能力、FastAPI的高性能后端框架,以及直观的前端交互页面,完整的搭建一套可直接落地的慢病管理 AI 助手。这个系统能 7×24 小时为患者提供专业的健康建议,既适合个人居家使用,也可部署到社区医院、互联网医疗平台。

二、项目核心价值

1. 解决的核心问题

  • 患者:随时咨询糖尿病/高血压饮食、运动、用药等问题,无需等待医生回复
  • 基层医疗机构:减轻医护人员重复解答基础问题的负担,提升服务效率
  • 开发者:掌握医疗大模型落地的全流程,从后端接口到前端交互的完整链路

2. 技术栈选型

技术组件 选型理由 核心价值
AntAngelMed 专为医疗领域优化的开源大模型,慢病管理专业度达 88.9% 临床一致性 确保健康建议科学、专业、安全
FastAPI 高性能异步 Python 框架,自动生成 API 文档,开发效率高 后端接口响应快,适配高并发场景
HTML/CSS/JS 原生前端技术,无框架依赖,部署简单 界面直观,适配不同设备,用户易上手
OpenAI SDK 兼容 AntAngelMed 的 OpenAI 接口规范 无需重复封装,快速调用模型能力

3. 系统整体架构

        用户通过前端页面输入问题 → FastAPI 后端接收请求并校验 → 调用 AntAngelMed 模型获取专业回答 → 后端返回结构化数据(含 Token 使用量) → 前端渲染回答并更新 Token 统计,全程异步处理,响应速度快。

请求响应流程说明:

  • 1. 用户前端页面
    • 用户输入医疗咨询问题
    • 构建POST请求发送至后端
    • 等待并接收结构化响应
  • 2. FastAPI后端服务
    • 接收 /api/chat 接口请求
    • 进行请求参数校验和预处理
    • 调用OpenAI兼容格式接口连接医疗大模型
    • 处理返回结果并结构化封装
    • 记录Token消耗数据
  • 3. AntAngelMed医疗大模型
    • 接收专业医疗咨询请求
    • 进行医学知识推理和回答生成
    • 返回专业回答内容
    • 附带Token使用统计信息
  • 4. Token使用统计
    • 本地存储每次请求的Token消耗量
    • 记录用户、时间、消耗数量
    • 支持用量分析和成本核算
  • 5. 用户交互界面/Token统计面板
    • 展示大模型返回的专业回答
    • 可视化展示Token消耗情况
    • 提供用量监控和预警

三、安诊儿AntAngelMed模型介绍

        AntAngelMed,蚂蚁・安诊儿医疗大模型,是由浙江省卫生健康信息中心、蚂蚁集团与浙江省安诊儿医学人工智能科技有限公司联合研发的超大规模开源医疗语言模型,于2026年1月7日正式发布开源。作为目前全球参数量最大、性能最强的开源医疗大模型之一,它以 1028.9 亿总参数、仅需激活 61 亿参数即可高效运行的特性,重新定义了 AI 医疗的专业边界与效率标准。

1. 模型定位

  • 核心定位:提供专业、安全、高效的医疗AI能力,填补开源医疗大模型在性能与实用性上的空白
  • 开发背景:针对医疗AI领域 "精度与效率难以协同" 的行业痛点,打造符合真实医疗场景需求的专业模型
  • 模型类型:基于Transformer架构的稀疏混合专家(MoE)大语言模型,专注医疗垂直领域
  • 开源时间:2026 年1月7日,开源仓库:GitHub@MedAIBase/AntAngelMed

2. 关键参数与规模

参数指标 具体数值 核心优势
总参数量 1028.9 亿(102.89B) 目前规模最大的开源医疗模型之一,知识容量远超同类模型
激活参数量 61 亿(6.1B) 每次推理仅激活约 5.9% 的参数,兼顾性能与效率
推理效率 较同等规模稠密模型提升 7 倍 MoE 架构 + Ling-flash-2.0 优化,H20 硬件上推理速度超 20 tokens / 秒
上下文窗口 支持 128K 长文本 可处理完整病历、多轮复杂问诊等长文本场景
量化支持 FP8 量化优化 进一步降低算力需求,适配边缘设备部署

3. 技术架构与创新

3.1 混合专家(MoE)架构设计

AntAngelMed 采用Ling-flash-2.0高效MoE架构,创新地将医疗知识领域划分为多个 "专家模块",实现类似 "多学科会诊" 的推理模式:

  • 模型包含128 个专家网络,每个专家专注特定医疗子领域(如心血管、内分泌、消化等)
  • 动态路由机制:根据输入问题的医疗属性,精准激活相关专家,避免冗余计算
  • 这种设计使模型在保持千亿级参数知识容量的同时,大幅降低推理算力消耗,解决了医疗大模型 "大而不优" 的核心问题

3.2 三阶段专业训练流程

模型通过严格的三阶段训练,打造兼具专业深度与人文关怀的医疗AI能力:

训练阶段 核心内容 训练目标
医疗语料持续预训练 注入海量权威医学数据(指南、教材、临床病例、医学期刊等) 构建扎实的医学知识基础,掌握专业术语与疾病机制
高质量指令监督微调 基于 2100 + 条医疗精数据与真实医患对话,优化回答结构与专业表达 使模型输出符合临床规范,提升可解释性与实用性
GRPO 强化学习优化 结合医疗专家反馈,优化模型的诊断推理与人文关怀能力 确保回答安全合规,强化 "医生思维" 与沟通技巧

训练数据特别强调权威性与合规性,严格遵循《中国心血管病防治指南》等 20 + 部国家级权威医疗规范,确保模型输出与临床标准对齐。

3.3 核心技术创新点

  • 1. 逻辑自洽与自我纠错:能识别输入信息中的矛盾点,自动修正推理路径,提升复杂病例诊断准确性
  • 2. 指南对齐分层诊断:严格按照临床指南进行疾病分级诊断,避免过度医疗或漏诊风险
  • 3. 主动追问机制:面对信息不全的问诊,能像真人医生一样引导用户补充关键症状信息
  • 4. 多模态支持:计划集成视觉能力,支持医学影像、皮肤疾病照片等多模态输入

3.4 临床实用性验证

  • 在慢病管理场景(糖尿病、高血压等)的干预建议与临床专家一致性达88.9%
  • 能准确解读 95% 以上常见体检报告,识别关键异常指标并提供专业解读
  • 多轮问诊中,问题引导的有效性与真实医生相比无显著差异,提升患者信息提供完整性

4. 核心医疗能力

  • 疾病诊断推理:基于症状描述进行鉴别诊断,提供可能病因与进一步检查建议
  • 慢病管理指导:为糖尿病、高血压等慢病患者提供个性化饮食、运动、用药建议
  • 健康科普教育:用通俗易懂的语言解释医学知识,提升公众健康素养
  • 医患沟通辅助:帮助医生快速生成问诊提纲,优化医患交流效率
  • 医疗文书处理:自动生成病历摘要、出院小结等,减轻医护文书负担

5. 典型应用场景

AntAngelMed 的应用覆盖从个人健康到临床医疗的全链路:

应用领域 具体场景 价值体现
个人健康管理 智能健康顾问、慢病自我管理、用药提醒、生活方式建议 提供 7×24 小时专业健康指导,降低医疗成本
基层医疗辅助 乡镇卫生院、社区诊所的诊断辅助、治疗方案建议 提升基层医疗服务能力,缓解优质医疗资源紧张
互联网医疗平台 在线问诊、预诊断、患者分流、随访管理 提升平台服务效率,改善用户就医体验
医学教育与培训 模拟病例分析、临床思维训练、医学知识考核 辅助医学生与年轻医生快速成长
医疗科研支持 文献综述、病例数据挖掘、临床试验设计辅助 加速医学科研成果转化

6. 模型调用方式

通过蚂蚁百宝箱,申请 API Key进行调用;每天有500000计算单元的免费额度。

在初始化客户端时,开发者需通过设置 base_url 参数,将其指向我们的 API 网关地址 https://api.tbox.cn/api/llm/v1/

from openai import OpenAI
client = OpenAI(
    # 将 base_url 指向我们的 API 地址
    base_url="https://api.tbox.cn/api/llm/v1/",
    # 使用您的访问令牌
    api_key="your-token-here" 
)

completion = client.chat.completions.create(...)

四、完整示例说明

1. 后端部分

基于 FastAPI 构建的轻量级后端服务,用于对接安诊儿大模型AntAngelMed提供慢病管理智能问答能力。

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from openai import OpenAI
from pydantic import BaseModel

# 初始化 FastAPI 应用
app = FastAPI()

# 配置跨域(允许前端页面访问)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 开发环境允许所有来源,生产环境请指定具体域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 初始化 OpenAI 客户端
client = OpenAI(
    base_url="https://api.tbox.cn/api/llm/v1/",
    api_key="sk-studio-e71****************be2f1"  # 请替换为你的实际 API Key
)

# 定义请求体模型
class ChatRequest(BaseModel):
    message: str

# 定义对话接口
@app.post("/api/chat")
async def chat(request: ChatRequest):
    try:
        # 调用 AntAngelMed 模型
        completion = client.chat.completions.create(
            model="AntAngelMed",
            messages=[
                {
                    "role": "system",
                    "content": "你是一名专业的慢病管理顾问,专注于糖尿病和高血压的健康管理。请用通俗易懂的语言回答用户的问题,提供科学、实用的建议,避免使用过于专业的术语,必要时给出具体的行动指导。\n\n输出格式要求:\n1. 使用二级标题 (##) 分隔不同主题\n2. 使用三级标题 (###) 分隔子主题\n3. 使用无序列表 (-) 列出要点\n4. 重点内容使用 **加粗** 标记\n5. 重要提示使用【注意】或【建议】开头\n6. 每个段落之间使用空行分隔"
                },
                {"role": "user", "content": request.message}
            ]
        )
        
        # 提取并返回回答
        response_content = completion.choices[0].message.content
        usage = completion.usage

        return {
            "success": True,
            "answer": response_content,
            "usage": {
                "prompt_tokens": usage.prompt_tokens,
                "completion_tokens": usage.completion_tokens,
                "total_tokens": usage.total_tokens
            }
        }
    
    except Exception as e:
        # 异常处理
        raise HTTPException(status_code=500, detail=f"调用模型失败:{str(e)}")

# 启动服务的入口
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

重点说明:

  • 使用 OpenAI SDK接入:通过指定 base_url 和 api_key,调用兼容 OpenAI 协议的 AntAngelMed 医疗大模型。
  • 系统提示词强约束:在请求中注入结构化 system prompt,强制模型以 Markdown 格式、通俗语言回答慢病管理问题,确保专业性与可读性。
  • CORS 全开放便于前端联调:开发阶段允许任意来源跨域访问,提升调试效率;
  • 接口输入输出标准化:POST /api/chat 接收用户消息,返回结构化回答及 token 消耗,便于前端解析和成本监控。
  • 异常统一捕获:所有调用错误被捕获并转为 HTTP 500 响应,附带错误信息,便于排查模型调用失败原因。
  • 独立运行快速部署:内置 Uvicorn 启动器,可直接运行或容器化;

AntAngelMed 模型调用关键参数:

  • model="AntAngelMed":指定使用医疗专用模型,确保回答的专业性
  • temperature=0.7:值越低回答越严谨,符合医疗场景的安全性要求
  • max_tokens=1000:限制回答长度,平衡详细度和 Token 成本

2. 前端部分

简单说明前端的框架结构,和后端的交互方式,以及模型返回的内容处理,完整的前端示例请参考附录部分;

2.1 整体布局:双栏结构(侧边栏 + 主聊天区)

<div class="main-container">
    <!-- 左侧常见问题 -->
    <div class="sidebar">
        ...
    </div>

    <!-- 右侧主界面 -->
    <div class="container">
        ...
    </div>
</div>

说明:采用 flex 布局实现左侧“快捷提问+Token统计”面板与右侧“聊天主窗口”的分离,兼顾功能引导与交互体验,适配桌面端宽屏显示。

2.2 快捷提问按钮区域(提升用户启动效率)

<div class="quick-questions">
    <button class="question-btn" onclick="askQuestion('高血压患者饮食需要注意什么?')">高血压患者饮食需要注意什么?</button>
    <button class="question-btn" onclick="askQuestion('糖尿病患者可以吃水果吗?')">糖尿病患者可以吃水果吗?</button>
    <!-- 其他预设问题... -->
</div>

说明:提供12个高频慢病问题一键发送,降低用户输入门槛,特别适合老年或非技术用户快速获取专业建议。

2.3 Token 使用统计面板(透明化资源消耗)

<div class="token-panel">
    <h3>Token 使用统计</h3>
    <div class="token-usage">
        <div class="token-usage-item">
            <span class="token-usage-label">本次输入</span>
            <span class="token-usage-value" id="prompt-tokens">0</span>
        </div>
        <div class="token-usage-item">
            <span class="token-usage-label">本次输出</span>
            <span class="token-usage-value" id="completion-tokens">0</span>
        </div>
        <div class="token-usage-item">
            <span class="token-usage-label">累计消耗</span>
            <span class="token-usage-value total" id="total-tokens">0</span>
        </div>
    </div>
    <div class="free-quota">
        <strong>每日Token免费额度累计 500,000</strong><br>
        <span id="quota-remaining">今日剩余: 500,000</span>
    </div>
</div>

说明:实时显示本次及累计 Token 消耗,并提示免费额度,增强用户对 AI 调用成本的感知,避免超额使用。

2.4 聊天消息容器(支持 Markdown 渲染)

<div class="chat-container" id="chat-container">
    <!-- 欢迎消息 -->
    <div class="message assistant-message">
        您好!我是您的慢病管理助手,专注于糖尿病和高血压的健康管理。请问您有什么想咨询的问题?
    </div>
</div>

说明:初始欢迎语引导用户提问;后续通过 JS 动态插入用户/助手消息,为结构化回答(标题、列表、加粗等)预留样式支持。

2.5 输入区域(支持回车发送)

<div class="input-area">
    <div class="input-container">
        <textarea id="message-input" placeholder="请输入您的问题,例如:高血压患者饮食需要注意什么?"></textarea>
        <button id="send-btn" onclick="sendMessage()">发送</button>
    </div>
</div>

说明:使用 <textarea> 支持多行输入,绑定 Enter 键发送(不换行),提升移动端与桌面端操作流畅性。

2.6 核心交互逻辑:调用后端 API 并渲染结构化回答

async function sendMessage() {
    const inputElement = document.getElementById("message-input");
    const sendBtn = document.getElementById("send-btn");
    const chatContainer = document.getElementById("chat-container");
    
    const message = inputElement.value.trim();
    if (!message) return;

    sendBtn.disabled = true;
    inputElement.value = "";

    // 添加用户消息
    const userMessageElement = document.createElement("div");
    userMessageElement.className = "message user-message";
    userMessageElement.textContent = message;
    chatContainer.appendChild(userMessageElement);

    // 添加加载动画
    const loadingElement = document.createElement("div");
    loadingElement.className = "message assistant-message";
    loadingElement.innerHTML = '<div class="loading"><div class="loading-dot"></div></div>';
    chatContainer.appendChild(loadingElement);
    chatContainer.scrollTop = chatContainer.scrollHeight;

    try {
        const response = await fetch(API_URL, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ message })
        });

        const result = await response.json();
        chatContainer.removeChild(loadingElement);

        if (result.success) {
            updateTokenStats(result.usage);
            const assistantMessageElement = document.createElement("div");
            assistantMessageElement.className = "message assistant-message";
            assistantMessageElement.innerHTML = formatMessage(result.answer); // 关键:格式化 Markdown
            chatContainer.appendChild(assistantMessageElement);
        } else {
            // 错误处理...
        }
    } catch (error) {
        // 网络错误处理...
    } finally {
        sendBtn.disabled = false;
        chatContainer.scrollTop = chatContainer.scrollHeight;
    }
}
  • 异步调用 FastAPI 后端 /api/chat 接口;
  • 显示加载动画提升等待体验;
  • 关键创新:通过 formatMessage() 将模型返回的 Markdown 文本转换为带样式的 HTML,实现 ## 标题、- 列表、加粗** 等结构化展示。

2.7 Markdown 到 HTML 的轻量级转换函数

function formatMessage(text) {
    let formatted = text;
    // 转义 HTML
    formatted = formatted.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    // 处理标题
    formatted = formatted.replace(/^### (.+)$/gm, '<h3>$1</h3>');
    formatted = formatted.replace(/^## (.+)$/gm, '<h2>$1</h2>');
    // 处理加粗、列表、分隔线、【注意】/【建议】等
    formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
    formatted = formatted.replace(/^- (.+)$/gm, '<li>$1</li>');
    formatted = formatted.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
    formatted = formatted.replace(/【注意】/g, '<div class="warning">注意:');
    formatted = formatted.replace(/【建议】/g, '<div class="tip">建议:');
    // 段落处理...
    return formatted;
}

说明:这是前端智能化的关键,将大模型按规范生成的 Markdown 文本,自动转为带 警告框(黄色)、提示框(蓝色)、层级标题、列表 的富文本,大幅提升可读性与专业感。

2.8 Token 统计持久化(本地存储)

function loadTokenStats() {
    const saved = localStorage.getItem("tokenStats");
    if (saved) {
        const stats = JSON.parse(saved);
        totalPromptTokens = stats.promptTokens || 0;
        totalCompletionTokens = stats.completionTokens || 0;
        // 更新 UI...
    }
}

function saveTokenStats() {
    localStorage.setItem("tokenStats", JSON.stringify({
        promptTokens: totalPromptTokens,
        completionTokens: totalCompletionTokens
    }));
}

window.addEventListener("beforeunload", saveTokenStats);

说明:利用 localStorage 保存当日 Token 消耗,即使刷新页面也不丢失统计,提升用户体验连续性。

3. 运行输出

3.1 首先启动后端服务

3.2 前端运行后的界面

左侧有常见问题和token消耗提示,右侧和模型交互返回的内容经过markdown处理后展示;

接口交互与结果返回:

4. 应用扩展

4.1 上下文对话支持    

# 后端新增会话管理
session_store = {}

@app.post("/api/chat")
async def chat(request: ChatRequest, session_id: str = Body(...)):
    # 初始化会话
    if session_id not in session_store:
        session_store[session_id] = [{"role": "system", "content": "你的system指令"}]
    # 添加用户消息
    session_store[session_id].append({"role": "user", "content": request.message})
    # 调用模型
    completion = client.chat.completions.create(
        model="AntAngelMed",
        messages=session_store[session_id]
    )
    # 保存助手消息
    session_store[session_id].append(completion.choices[0].message)

4.2 Token 限流

# 新增限流中间件
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware

class TokenQuotaMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 检查用户今日Token消耗是否超限
        user_id = request.headers.get("X-User-ID", "anonymous")
        total_used = get_user_token_usage(user_id)  # 从数据库/Redis获取
        if total_used > 500000:
            return JSONResponse(
                status_code=429,
                content={"success": False, "detail": "今日Token额度已用尽"}
            )
        response = await call_next(request)
        return response

app.add_middleware(TokenQuotaMiddleware)

五、总结

        简单来说,这次我们搭的糖尿病、高血压慢病管理 AI 助手,核心就是运用AntAngelMed 这个医疗大模型的专业能力。这个模型是专门做医疗领域的,尤其在慢病管理这块特别专业,给出的饮食、运动、用药建议都贴合临床规范,和医生的专业建议契合度特别高,完全不是普通大模型的泛泛回答。

        结合这个AI示例,不管是用户问的基础问题,还是个性化的慢病管理疑问,都是靠调用 AntAngelMed 来生成答案,它能按照我们设定的格式,用通俗的话讲专业知识,还会标重点、给明确建议,普通人也能看明白。也正是因为有这个模型的专业能力打底,这个 AI 助手才能真正帮上忙,不管是居家患者随时咨询,还是基层医疗机构减轻工作负担,都能给出靠谱的解答。该系统能切实解决慢病管理中的咨询痛点,适用于居家、社区、互联网医疗等多个场景。

附录:前端完整示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>糖尿病/高血压 慢病管理助手</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: "Microsoft YaHei", sans-serif;
        }

        body {
            background-color: #f5f7fa;
            padding: 20px;
            display: flex;
            justify-content: center;
            align-items: flex-start;
            min-height: 100vh;
        }

        .main-container {
            display: flex;
            gap: 20px;
            max-width: 1100px;
            width: 100%;
        }

        .sidebar {
            width: 280px;
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            padding: 15px;
            flex-shrink: 0;
            display: flex;
            flex-direction: column;
        }

        .sidebar h2 {
            font-size: 18px;
            color: #333;
            margin-bottom: 16px;
            padding-bottom: 10px;
            border-bottom: 2px solid #4a6cf7;
        }

        .quick-questions {
            display: flex;
            flex-direction: column;
            gap: 10px;
            flex: 1;
            overflow-y: auto;
            padding-right: 5px;
            max-height: calc(100vh - 400px);
        }

        /* 滚动条样式 */
        .quick-questions::-webkit-scrollbar {
            width: 6px;
        }

        .quick-questions::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 3px;
        }

        .quick-questions::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 3px;
        }

        .quick-questions::-webkit-scrollbar-thumb:hover {
            background: #a1a1a1;
        }

        .question-btn {
            background: #f8f9fa;
            border: 1px solid #e9ecef;
            border-radius: 8px;
            padding: 12px 16px;
            text-align: left;
            font-size: 14px;
            color: #495057;
            cursor: pointer;
            transition: all 0.2s;
            line-height: 1.4;
        }

        .question-btn:hover {
            background: #e7f1ff;
            border-color: #4a6cf7;
            color: #4a6cf7;
            transform: translateX(4px);
        }

        .token-panel {
            margin-top: auto;
            padding-top: 20px;
            border-top: 2px dashed #e9ecef;
        }

        .token-panel h3 {
            font-size: 14px;
            color: #333;
            margin-bottom: 12px;
            font-weight: 600;
        }

        .token-usage {
            background: linear-gradient(135deg, #f8f9fa, #e9ecef);
            border-radius: 8px;
            padding: 5px 12px;
        }

        .token-usage-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 8px 0;
            font-size: 13px;
        }

        .token-usage-item:first-child {
            border-bottom: 1px solid #dee2e6;
        }

        .token-usage-label {
            color: #6c757d;
        }

        .token-usage-value {
            font-weight: 600;
            color: #4a6cf7;
        }

        .token-usage-value.total {
            color: #e74c3c;
        }

        .free-quota {
            margin-top: 12px;
            padding: 10px 12px;
            background: linear-gradient(135deg, #d4edda, #c3e6cb);
            border-radius: 6px;
            font-size: 13px;
            color: #155724;
            border-left: 3px solid #28a745;
        }

        .free-quota strong {
            font-weight: 600;
        }

        .container {
            flex: 1;
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
            min-width: 0;
        }

        .header {
            background: linear-gradient(135deg, #4a6cf7, #35cfff);
            color: white;
            padding: 20px;
            text-align: center;
        }

        .header h1 {
            font-size: 24px;
            margin-bottom: 8px;
        }

        .header p {
            font-size: 14px;
            opacity: 0.9;
        }

        .chat-container {
            height: 600px;
            padding: 20px;
            overflow-y: auto;
            background-color: #f9f9f9;
        }

        .message {
            margin-bottom: 16px;
            max-width: 80%;
            animation: fadeIn 0.3s ease;
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .user-message {
            margin-left: auto;
            background-color: #4a6cf7;
            color: white;
            border-radius: 18px 18px 4px 18px;
            padding: 12px 18px;
        }

        .assistant-message {
            margin-right: auto;
            background-color: white;
            border: 1px solid #eee;
            border-radius: 18px 18px 18px 4px;
            padding: 16px 20px;
            line-height: 1.8;
            color: #333;
        }

        /* Markdown 格式样式 */
        .assistant-message h1,
        .assistant-message h2,
        .assistant-message h3 {
            margin: 12px 0 8px 0;
            color: #2c3e50;
            font-weight: 600;
        }

        .assistant-message h2 {
            font-size: 18px;
            padding-bottom: 6px;
            border-bottom: 2px solid #e9ecef;
        }

        .assistant-message h3 {
            font-size: 16px;
            color: #4a6cf7;
        }

        .assistant-message p {
            margin: 8px 0;
        }

        .assistant-message ul,
        .assistant-message ol {
            margin: 10px 0;
            padding-left: 24px;
        }

        .assistant-message li {
            margin: 6px 0;
            line-height: 1.7;
        }

        .assistant-message ul li::marker {
            color: #4a6cf7;
        }

        .assistant-message strong {
            color: #4a6cf7;
            font-weight: 600;
        }

        .assistant-message em {
            color: #666;
            font-style: italic;
        }

        .assistant-message code {
            background-color: #f1f3f5;
            padding: 2px 6px;
            border-radius: 4px;
            font-family: "Consolas", "Monaco", monospace;
            font-size: 14px;
            color: #e74c3c;
        }

        .assistant-message pre {
            background-color: #f8f9fa;
            border: 1px solid #e9ecef;
            border-radius: 8px;
            padding: 12px;
            margin: 10px 0;
            overflow-x: auto;
        }

        .assistant-message pre code {
            background-color: transparent;
            padding: 0;
            color: #333;
        }

        .assistant-message .warning {
            background-color: #fff3cd;
            border-left: 4px solid #ffc107;
            padding: 12px 16px;
            margin: 12px 0;
            border-radius: 4px;
        }

        .assistant-message .tip {
            background-color: #d1ecf1;
            border-left: 4px solid #4a6cf7;
            padding: 12px 16px;
            margin: 12px 0;
            border-radius: 4px;
        }

        .assistant-message .divider {
            height: 1px;
            background: linear-gradient(90deg, transparent, #e9ecef, transparent);
            margin: 16px 0;
        }

        .input-area {
            padding: 20px;
            background-color: white;
            border-top: 1px solid #eee;
        }



        .input-container {
            display: flex;
            gap: 10px;
        }

        #message-input {
            flex: 1;
            padding: 12px 16px;
            border: 1px solid #ddd;
            border-radius: 24px;
            font-size: 16px;
            outline: none;
            resize: none;
            height: 50px;
        }

        #message-input:focus {
            border-color: #4a6cf7;
        }

        #send-btn {
            background-color: #4a6cf7;
            color: white;
            border: none;
            border-radius: 24px;
            padding: 0 24px;
            font-size: 16px;
            cursor: pointer;
            transition: background-color 0.2s;
        }

        #send-btn:hover {
            background-color: #3a5ce7;
        }

        #send-btn:disabled {
            background-color: #999;
            cursor: not-allowed;
        }

        .loading {
            display: inline-block;
            width: 40px;
            height: 40px;
            position: relative;
        }

        .loading::before,
        .loading::after {
            content: '';
            position: absolute;
            border-radius: 50%;
            animation: bounce 0.6s infinite alternate-in-out;
        }

        .loading::before {
            width: 10px;
            height: 10px;
            background: #4a6cf7;
            left: 0;
            animation-delay: 0s;
        }

        .loading::after {
            width: 10px;
            height: 10px;
            background: #35cfff;
            right: 0;
            animation-delay: 0.3s;
        }

        .loading-dot {
            position: absolute;
            width: 10px;
            height: 10px;
            background: linear-gradient(135deg, #4a6cf7, #35cfff);
            border-radius: 50%;
            left: 50%;
            transform: translateX(-50%);
            animation: bounce 0.6s infinite alternate-in-out 0.15s;
        }

        @keyframes bounce {
            from { transform: translateY(0); }
            to { transform: translateY(-15px); }
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <div class="main-container">
        <!-- 左侧常见问题 -->
        <div class="sidebar">
            <h2>常见问题</h2>
            <div class="quick-questions">
                <button class="question-btn" onclick="askQuestion('高血压患者饮食需要注意什么?')">高血压患者饮食需要注意什么?</button>
                <button class="question-btn" onclick="askQuestion('糖尿病患者的血糖正常范围是多少?')">糖尿病患者的血糖正常范围是多少?</button>
                <button class="question-btn" onclick="askQuestion('高血压和糖尿病患者可以运动吗?')">高血压和糖尿病患者可以运动吗?</button>
                <button class="question-btn" onclick="askQuestion('糖尿病的早期症状有哪些?')">糖尿病的早期症状有哪些?</button>
                <button class="question-btn" onclick="askQuestion('高血压患者需要终身服药吗?')">高血压患者需要终身服药吗?</button>
                <button class="question-btn" onclick="askQuestion('糖尿病患者可以吃水果吗?')">糖尿病患者可以吃水果吗?</button>
                <button class="question-btn" onclick="askQuestion('高血压患者平时要注意哪些生活习惯?')">高血压患者平时要注意哪些生活习惯?</button>
                <button class="question-btn" onclick="askQuestion('糖尿病患者如何预防并发症?')">糖尿病患者如何预防并发症?</button>
                <button class="question-btn" onclick="askQuestion('高血压和糖尿病患者适合做什么运动?')">高血压和糖尿病患者适合做什么运动?</button>
                <button class="question-btn" onclick="askQuestion('糖尿病患者每天应该吃多少主食?')">糖尿病患者每天应该吃多少主食?</button>
                <button class="question-btn" onclick="askQuestion('高血压患者血压多少算是正常?')">高血压患者血压多少算是正常?</button>
                <button class="question-btn" onclick="askQuestion('糖尿病患者可以喝酒吗?')">糖尿病患者可以喝酒吗?</button>
            </div>

            <!-- Token 使用统计 -->
            <div class="token-panel">
                <h3>Token 使用统计</h3>
                <div class="token-usage">
                    <div class="token-usage-item">
                        <span class="token-usage-label">本次输入</span>
                        <span class="token-usage-value" id="prompt-tokens">0</span>
                    </div>
                    <div class="token-usage-item">
                        <span class="token-usage-label">本次输出</span>
                        <span class="token-usage-value" id="completion-tokens">0</span>
                    </div>
                    <div class="token-usage-item">
                        <span class="token-usage-label">累计消耗</span>
                        <span class="token-usage-value total" id="total-tokens">0</span>
                    </div>
                </div>
                <div class="free-quota">
                    <strong>每日Token免费额度累计 500,000</strong><br>
                    <span id="quota-remaining">今日剩余: 500,000</span>
                </div>
            </div>
        </div>

        <!-- 右侧主界面 -->
        <div class="container">
        <div class="header">
            <h1>糖尿病/高血压 慢病管理助手</h1>
            <p>专业解答糖尿病、高血压相关的健康问题,提供科学的管理建议</p>
        </div>
        
        <div class="chat-container" id="chat-container">
            <!-- 欢迎消息 -->
            <div class="message assistant-message">
                您好!我是您的慢病管理助手,专注于糖尿病和高血压的健康管理。请问您有什么想咨询的问题?
            </div>
        </div>
        
        <div class="input-area">
            <div class="input-container">
                <textarea id="message-input" placeholder="请输入您的问题,例如:高血压患者饮食需要注意什么?"></textarea>
                <button id="send-btn" onclick="sendMessage()">发送</button>
            </div>
        </div>
        </div>
    </div>

    <script>
        // 后端 API 地址
        const API_URL = "http://localhost:8000/api/chat";

        // Token 统计
        let totalPromptTokens = 0;
        let totalCompletionTokens = 0;
        const FREE_QUOTA = 500000;

        // 发送消息函数
        async function sendMessage() {
            const inputElement = document.getElementById("message-input");
            const sendBtn = document.getElementById("send-btn");
            const chatContainer = document.getElementById("chat-container");
            
            // 获取并清理输入内容
            const message = inputElement.value.trim();
            if (!message) return;

            // 禁用发送按钮,清空输入框
            sendBtn.disabled = true;
            inputElement.value = "";

            // 添加用户消息到聊天窗口
            const userMessageElement = document.createElement("div");
            userMessageElement.className = "message user-message";
            userMessageElement.textContent = message;
            chatContainer.appendChild(userMessageElement);

            // 添加加载状态
            const loadingElement = document.createElement("div");
            loadingElement.className = "message assistant-message";
            loadingElement.innerHTML = '<div class="loading"><div class="loading-dot"></div></div>';
            chatContainer.appendChild(loadingElement);
            
            // 滚动到底部
            chatContainer.scrollTop = chatContainer.scrollHeight;

            try {
                // 调用后端 API
                const response = await fetch(API_URL, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify({ message: message })
                });

                const result = await response.json();

                // 移除加载状态
                chatContainer.removeChild(loadingElement);

                if (result.success) {
                    // 更新 Token 统计
                    updateTokenStats(result.usage);
                    // 添加助手回复(美化格式)
                    const assistantMessageElement = document.createElement("div");
                    assistantMessageElement.className = "message assistant-message";
                    assistantMessageElement.innerHTML = formatMessage(result.answer);
                    chatContainer.appendChild(assistantMessageElement);
                } else {
                    // 显示错误信息
                    const errorElement = document.createElement("div");
                    errorElement.className = "message assistant-message";
                    errorElement.style.color = "#ff4444";
                    errorElement.textContent = "抱歉,处理您的请求时出错了,请稍后再试。";
                    chatContainer.appendChild(errorElement);
                }
            } catch (error) {
                // 网络错误处理
                chatContainer.removeChild(loadingElement);
                const errorElement = document.createElement("div");
                errorElement.className = "message assistant-message";
                errorElement.style.color = "#ff4444";
                errorElement.textContent = "网络错误,请检查后端服务是否正常运行。";
                chatContainer.appendChild(errorElement);
                console.error("请求错误:", error);
            } finally {
                // 恢复发送按钮
                sendBtn.disabled = false;
                // 滚动到底部
                chatContainer.scrollTop = chatContainer.scrollHeight;
            }
        }

        // 按回车键发送消息
        document.getElementById("message-input").addEventListener("keydown", function(e) {
            if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault();
                sendMessage();
            }
        });

        // 快速提问函数
        function askQuestion(question) {
            const inputElement = document.getElementById("message-input");
            inputElement.value = question;
            sendMessage();
        }

        // 格式化消息内容(将 Markdown 风格转换为 HTML)
        function formatMessage(text) {
            let formatted = text;

            // 转义 HTML 特殊字符
            formatted = formatted.replace(/&/g, "&amp;")
                                .replace(/</g, "&lt;")
                                .replace(/>/g, "&gt;");

            // 处理标题 ###
            formatted = formatted.replace(/^### (.+)$/gm, '<h3>$1</h3>');
            formatted = formatted.replace(/^## (.+)$/gm, '<h2>$1</h2>');
            formatted = formatted.replace(/^# (.+)$/gm, '<h1>$1</h1>');

            // 处理加粗 **text**
            formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');

            // 处理斜体 *text*
            formatted = formatted.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');

            // 处理行内代码 `code`
            formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');

            // 处理无序列表 - item
            formatted = formatted.replace(/^- (.+)$/gm, '<li>$1</li>');
            // 包装连续的 li
            formatted = formatted.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');

            // 处理有序列表 1. item
            formatted = formatted.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');

            // 处理分隔线 ---
            formatted = formatted.replace(/^---$/gm, '<div class="divider"></div>');

            // 处理警告框 【注意】或 ⚠️
            formatted = formatted.replace(/【注意】|⚠️|注意:/g, '<div class="warning">注意:');
            // 关闭警告框(在下一个标题或列表前)
            formatted = formatted.replace(/(<h[1-3]>|<ul>)/g, '</div>$1');

            // 处理提示框 【建议】或 💡
            formatted = formatted.replace(/【建议】|💡|建议:/g, '<div class="tip">建议:');
            // 关闭提示框
            formatted = formatted.replace(/(<h[1-3]>|<ul>)/g, '</div>$1');

            // 处理段落(换行)
            formatted = formatted.replace(/\n\n/g, '</p><p>');
            formatted = '<p>' + formatted + '</p>';

            // 清理空段落
            formatted = formatted.replace(/<p>\s*<\/p>/g, '');
            formatted = formatted.replace(/<p>(<h[1-3]>)/g, '$1');
            formatted = formatted.replace(/(<\/h[1-3]>)<\/p>/g, '$1');
            formatted = formatted.replace(/<p>(<ul>)/g, '$1');
            formatted = formatted.replace(/(<\/ul>)<\/p>/g, '$1');
            formatted = formatted.replace(/<p>(<div)/g, '$1');
            formatted = formatted.replace(/(<\/div>)<\/p>/g, '$1');

            return formatted;
        }

        // 更新 Token 统计
        function updateTokenStats(usage) {
            // 更新累计数据
            totalPromptTokens += usage.prompt_tokens;
            totalCompletionTokens += usage.completion_tokens;
            const totalUsed = totalPromptTokens + totalCompletionTokens;

            // 更新本次数据显示
            document.getElementById("prompt-tokens").textContent = usage.prompt_tokens;
            document.getElementById("completion-tokens").textContent = usage.completion_tokens;
            document.getElementById("total-tokens").textContent = totalUsed;

            // 更新剩余额度
            const remaining = Math.max(0, FREE_QUOTA - totalUsed);
            document.getElementById("quota-remaining").textContent = `今日剩余: ${remaining.toLocaleString()}`;
        }

        // 从 localStorage 加载历史统计
        function loadTokenStats() {
            const saved = localStorage.getItem("tokenStats");
            if (saved) {
                const stats = JSON.parse(saved);
                totalPromptTokens = stats.promptTokens || 0;
                totalCompletionTokens = stats.completionTokens || 0;
                const totalUsed = totalPromptTokens + totalCompletionTokens;
                document.getElementById("total-tokens").textContent = totalUsed;

                // 更新剩余额度
                const remaining = Math.max(0, FREE_QUOTA - totalUsed);
                document.getElementById("quota-remaining").textContent = `今日剩余: ${remaining.toLocaleString()}`;
            }
        }

        // 保存 Token 统计到 localStorage
        function saveTokenStats() {
            localStorage.setItem("tokenStats", JSON.stringify({
                promptTokens: totalPromptTokens,
                completionTokens: totalCompletionTokens
            }));
        }

        // 页面加载时初始化
        loadTokenStats();

        // 页面关闭前保存统计数据
        window.addEventListener("beforeunload", saveTokenStats);
    </script>
</body>
</html>
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐