Spaces:
Running
Running
<html lang="zh-CN"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>糖尿病诊断系统</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<link rel="preconnect" href="https://fonts.googleapis.com"> | |
<link rel="preconnect" href="https://fonts.gstati // --- 核心修复:页面状态切换逻辑 --- | |
// 修复Jinja2模板语法与JavaScript解析器的冲突 | |
const hasResult = {% if has_result %}true{% else %}false{% endif %}; | |
if (hasResult) { | |
uploadSection.hidden = true; | |
resultSection.hidden = false; | |
} else { | |
uploadSection.hidden = false; | |
resultSection.hidden = true; | |
}sorigin> | |
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet"> | |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/tsparticles@2.12.0/tsparticles.bundle.min.js"></script> | |
<style> | |
:root { | |
/* 浅色模式变量 */ | |
--bg-image: url('https://images.unsplash.com/photo-1544197150-b99a580bb7a8?q=80&w=2070&auto=format&fit=crop'); | |
--panel-bg: rgba(255, 255, 255, 0.9); | |
--border-color: rgba(0, 0, 0, 0.1); | |
--primary-text: #2d3748; | |
--secondary-text: #718096; | |
--accent-color: #667eea; | |
--accent-gradient: linear-gradient(90deg, #667eea 0%, #764ba2 100%); | |
--shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1); | |
--border-radius: 1rem; | |
--bg-overlay: rgba(255, 255, 255, 0.1); | |
--card-bg: rgba(255, 255, 255, 0.8); | |
--chat-user-bg: #667eea; | |
--chat-assistant-bg: rgba(0, 0, 0, 0.05); | |
--input-bg: rgba(255, 255, 255, 0.8); | |
--hover-bg: rgba(0, 0, 0, 0.05); | |
} | |
/* 深色模式变量 */ | |
[data-theme="dark"] { | |
--bg-image: url('https://images.unsplash.com/photo-1554141316-1282c0a97c92?q=80&w=2070&auto=format&fit=crop'); | |
--panel-bg: rgba(30, 30, 45, 0.9); | |
--border-color: rgba(255, 255, 255, 0.2); | |
--primary-text: #f0f2f5; | |
--secondary-text: #a8b2d1; | |
--accent-color: #928CEE; | |
--accent-gradient: linear-gradient(90deg, #928CEE 0%, #a29dff 100%); | |
--shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); | |
--bg-overlay: rgba(10, 10, 20, 0.5); | |
--card-bg: rgba(0, 0, 0, 0.2); | |
--chat-user-bg: #928CEE; | |
--chat-assistant-bg: rgba(0, 0, 0, 0.3); | |
--input-bg: rgba(0, 0, 0, 0.2); | |
--hover-bg: rgba(255, 255, 255, 0.1); | |
} | |
* { box-sizing: border-box; margin: 0; padding: 0; } | |
html { height: 100%; } | |
body { | |
height: 100%; | |
font-family: 'Noto Sans SC', sans-serif; | |
background-color: var(--primary-text); | |
color: var(--primary-text); | |
position: relative; | |
overflow: hidden; | |
transition: all 0.3s ease; | |
} | |
[data-theme="dark"] body { | |
background-color: #111118; | |
} | |
body::before { | |
content: ''; | |
position: fixed; | |
top: 0; left: 0; | |
width: 100%; height: 100%; | |
background-image: var(--bg-image); | |
background-size: cover; | |
background-position: center; | |
background-attachment: fixed; | |
z-index: -2; | |
} | |
body::after { | |
content: ''; | |
position: fixed; | |
top: 0; left: 0; | |
width: 100%; height: 100%; | |
background-color: rgba(10, 10, 20, 0.5); | |
z-index: -2; | |
} | |
#tsparticles { | |
position: fixed; | |
top: 0; left: 0; | |
width: 100%; height: 100%; | |
z-index: -1; | |
} | |
.main-container { | |
display: flex; | |
height: 100vh; | |
padding: 1rem; | |
gap: 1.5rem; | |
} | |
.panel { | |
background: var(--panel-bg); | |
backdrop-filter: blur(14px); | |
-webkit-backdrop-filter: blur(14px); | |
border: 1px solid var(--border-color); | |
border-radius: var(--border-radius); | |
box-shadow: var(--shadow); | |
display: flex; | |
flex-direction: column; | |
overflow: hidden; | |
transition: all 0.3s ease; | |
padding: 1.5rem; | |
} | |
.panel ::-webkit-scrollbar { width: 6px; } | |
.panel ::-webkit-scrollbar-track { background: transparent; } | |
.panel ::-webkit-scrollbar-thumb { background-color: rgba(146, 140, 238, 0.5); border-radius: 10px; } | |
.panel ::-webkit-scrollbar-thumb:hover { background-color: rgba(146, 140, 238, 0.8); } | |
.left-panel { flex: 1.2; } | |
.right-panel { flex: 1; padding: 0; } | |
h1 { font-size: 1.6rem; font-weight: 700; margin-bottom: 0.5rem; } | |
.subtitle { font-size: 0.95rem; color: var(--secondary-text); margin-bottom: 1.5rem; } | |
.uploader-container { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
text-align: center; | |
border: 2px dashed var(--border-color); | |
border-radius: 0.75rem; | |
padding: 2rem 1.5rem; | |
cursor: pointer; | |
transition: background-color 0.2s, border-color 0.2s; | |
background-color: var(--card-bg); | |
position: relative; /* 为内部input定位做准备 */ | |
overflow: hidden; /* 隐藏任何可能溢出的子元素 */ | |
} | |
.uploader-container:hover { | |
background-color: var(--hover-bg); | |
border-color: var(--accent-color); | |
} | |
.uploader-container input[type="file"] { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
opacity: 0; /* 完全透明 */ | |
cursor: pointer; | |
} | |
.uploader-icon { font-size: 3rem; color: var(--accent-color); margin-bottom: 1rem; } | |
.uploader-text { font-size: 1.125rem; font-weight: 500; margin-bottom: 0.5rem; } | |
.uploader-hint { color: var(--secondary-text); } | |
.example-selector { margin-top: 1.5rem ; } | |
.submit-btn, .reset-btn, .example-btn, .home-btn { | |
width: 100%; | |
padding: 0.875rem; | |
margin-top: 1.25rem; | |
border: none; | |
border-radius: 0.5rem; | |
color: white; | |
font-size: 1rem; | |
font-weight: 500; | |
background: var(--accent-gradient); | |
cursor: pointer; | |
transition: all 0.2s ease; | |
transform: scale(1); | |
} | |
.submit-btn:hover, .reset-btn:hover, .example-btn:hover, .home-btn:hover { | |
opacity: 0.9; | |
transform: scale(1.02); | |
} | |
.home-btn { | |
background: transparent ; | |
color: var(--primary-text) ; | |
border: 1px solid var(--border-color) ; | |
} | |
.home-btn:hover { background: var(--hover-bg) ; } | |
.upload-section, .result-wrapper { | |
display: flex; | |
flex-direction: column; | |
height: 100%; | |
animation: fadeIn 0.5s ease; | |
} | |
/* 核心修复: 确保 hidden 属性能够强制隐藏元素 */ | |
[hidden] { | |
display: none ; | |
} | |
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
.result-content { flex-grow: 1; overflow-y: auto; padding-right: 15px; } | |
.image-preview { | |
width: 100%; | |
max-height: 300px; | |
object-fit: contain; | |
border-radius: 0.75rem; | |
margin-bottom: 1.5rem; | |
border: 1px solid var(--border-color); | |
background-color: var(--card-bg); | |
} | |
.result-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem; } | |
.result-card { background-color: var(--card-bg); border: 1px solid var(--border-color); padding: 1rem; border-radius: 0.625rem; } | |
.result-card .label { font-size: 0.875rem; color: var(--secondary-text); margin-bottom: 0.5rem; } | |
.result-card .value { font-size: 1.375rem; font-weight: 700; color: var(--accent-color); } | |
.ai-advice h3 { font-size: 1.125rem; margin-bottom: 0.75rem; } | |
.ai-advice-content { background-color: var(--card-bg); border: 1px solid var(--border-color); padding: 1rem; border-radius: 0.625rem; color: var(--secondary-text); font-size: 0.9375rem; line-height: 1.7; white-space: pre-line; } | |
.chat-header { padding: 1.5rem; font-size: 1.25rem; font-weight: 700; border-bottom: 1px solid var(--border-color); } | |
.chat-history { flex-grow: 1; padding: 1.5rem; overflow-y: auto; } | |
.chat-message { max-width: 85%; padding: 0.75rem 1rem; border-radius: 0.75rem; margin-bottom: 1rem; line-height: 1.6; animation: fadeIn 0.3s; } | |
.chat-message.user { background: var(--chat-user-bg); color: white; margin-left: auto; border-bottom-right-radius: 2px; } | |
.chat-message.assistant { background: var(--chat-assistant-bg); color: var(--primary-text); margin-right: auto; border-bottom-left-radius: 2px; } | |
.chat-message.assistant p { margin-bottom: 0.5em; } | |
.chat-message.assistant ul, .chat-message.assistant ol { padding-left: 20px; } | |
.chat-message.assistant li { margin-bottom: 0.2em; } | |
.chat-input-area { padding: 1rem 1.5rem; border-top: 1px solid var(--border-color); background-color: var(--card-bg); } | |
.chat-input-wrapper { display: flex; align-items: center; gap: 0.75rem; background-color: var(--input-bg); border: 1px solid var(--border-color); border-radius: 0.625rem; padding: 0.375rem; transition: border-color 0.2s; } | |
.chat-input-wrapper:focus-within { border-color: var(--accent-color); } | |
#ai-user-input { flex-grow: 1; border: none; outline: none; padding: 0.625rem; font-size: 0.9375rem; background: transparent; color: var(--primary-text); } | |
#send-to-ai { border: none; background: var(--accent-color); color: white; border-radius: 0.5rem; width: 2.5rem; height: 2.5rem; cursor: pointer; font-size: 1.25rem; display: flex; align-items: center; justify-content: center; transition: opacity 0.2s; } | |
#send-to-ai:hover { opacity: 0.9; } | |
@media (max-width: 900px) { | |
body { overflow: auto; } | |
.main-container { flex-direction: column; height: auto; padding: 1rem; gap: 1rem; } | |
/* 移动端性能优化 */ | |
.uploader-container { | |
padding: 1.5rem 1rem; | |
} | |
.image-preview { | |
max-height: 250px; | |
} | |
.loading-content { | |
padding: 1.5rem; | |
margin: 1rem; | |
} | |
/* 减少动画复杂度以提高性能 */ | |
.loading-spinner { | |
width: 1.5rem; | |
height: 1.5rem; | |
} | |
/* 优化触摸交互 */ | |
.submit-btn, .reset-btn, .example-btn, .home-btn { | |
min-height: 44px; /* iOS推荐的最小触摸目标 */ | |
font-size: 1.1rem; | |
} | |
} | |
/* 低性能设备优化 */ | |
@media (max-width: 600px) { | |
/* 减少粒子效果 */ | |
#tsparticles { | |
display: none; | |
} | |
/* 简化背景 */ | |
body::before { | |
background-size: cover; | |
filter: blur(2px); | |
} | |
/* 减少动画 */ | |
.submit-btn:hover, .reset-btn:hover, .example-btn:hover, .home-btn:hover { | |
transform: none; | |
opacity: 0.9; | |
} | |
} /* 主题切换按钮样式 */ | |
.theme-toggle { | |
position: fixed; | |
top: 1rem; | |
right: 1rem; | |
z-index: 1000; | |
background: var(--panel-bg); | |
border: 1px solid var(--border-color); | |
border-radius: 50%; | |
width: 3rem; | |
height: 3rem; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
font-size: 1.25rem; | |
transition: all 0.3s ease; | |
box-shadow: var(--shadow); | |
} | |
.theme-toggle:hover { | |
transform: scale(1.1); | |
background: var(--hover-bg); | |
} | |
.theme-toggle::before { | |
content: '🌙'; | |
} | |
[data-theme="dark"] .theme-toggle::before { | |
content: '☀️'; | |
} | |
/* 简单加载动画 */ | |
.loading-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: rgba(0, 0, 0, 0.6); | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
z-index: 9999; | |
opacity: 0; | |
visibility: hidden; | |
transition: opacity 0.3s ease; | |
} | |
.loading-overlay.show { | |
opacity: 1; | |
visibility: visible; | |
} | |
.loading-content { | |
background: var(--panel-bg); | |
border: 1px solid var(--border-color); | |
border-radius: 1rem; | |
padding: 2rem; | |
text-align: center; | |
box-shadow: var(--shadow); | |
} | |
.loading-spinner { | |
width: 2rem; | |
height: 2rem; | |
border: 2px solid var(--border-color); | |
border-top: 2px solid var(--accent-color); | |
border-radius: 50%; | |
animation: spin 1s linear infinite; | |
margin: 0 auto 1rem; | |
} | |
.loading-text { | |
color: var(--primary-text); | |
font-size: 1rem; | |
font-weight: 500; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
</style> | |
</head> | |
{% set has_result = (result is defined and result) or (error is defined and error) %} | |
<body> | |
<!-- 简单加载遮罩 --> | |
<div class="loading-overlay" id="loading-overlay"> | |
<div class="loading-content"> | |
<div class="loading-spinner"></div> | |
<div class="loading-text">正在分析您的眼底照片,请稍候...</div> | |
</div> | |
</div> | |
<div id="tsparticles"></div> | |
<!-- 主题切换按钮 --> | |
<button class="theme-toggle" id="theme-toggle" title="切换主题"></button> | |
<div class="main-container"> | |
<div class="panel left-panel"> | |
<div class="upload-section" id="upload-section"> | |
<h1>糖尿病视网膜病变智能诊断</h1> | |
<p class="subtitle">上传眼底照片,即刻获取AI分析报告与健康建议。</p> | |
<form action="/predict" method="post" enctype="multipart/form-data" id="upload-form"> | |
<label for="file-upload" class="uploader-container"> | |
<div class="uploader-icon">📤</div> | |
<div class="uploader-text" id="uploader-text">点击或拖拽上传眼底照片</div> | |
<div class="uploader-hint">支持 PNG, JPG, JPEG 格式</div> | |
</label> | |
<input id="file-upload" type="file" name="file" accept="image/*" required> | |
<button class="submit-btn" type="submit">开始诊断</button> | |
</form> | |
<div class="example-selector" style="margin-top: 2rem;"> | |
<h3 style="font-size: 1.125rem; margin-bottom: 1rem; color: var(--primary-text);">或选择示例照片进行演示</h3> | |
<select id="example-select" style="width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 0.5rem; margin-bottom: 1rem; font-size: 0.9375rem; background: var(--input-bg); color: var(--primary-text);"> | |
<option value="">请选择示例照片</option> | |
</select> | |
<button id="load-example" class="submit-btn" style="margin-top:0;">加载并诊断示例照片</button> | |
</div> | |
</div> | |
<div class="result-wrapper" id="result-section"> | |
<div class="result-content"> | |
{% if result and img_path %} | |
<img src="{{ img_path }}" alt="上传图片预览" class="image-preview"> | |
{% endif %} | |
{% if result %} | |
<div class="result-grid"> | |
<div class="result-card"> | |
<div class="label">分级结果</div> | |
<div class="value">{{ result.grading }} <span style="font-size: 0.8rem; color: var(--secondary-text); font-weight: normal;">(级别越高代表糖尿病越严重,共有0-4级)</span></div> | |
</div> | |
<div class="result-card"> | |
<div class="label">糖尿病风险</div> | |
<div class="value">{{ result.diabetic }}</div> | |
</div> | |
</div> | |
{% endif %} | |
{% if result and result.ai_advice %} | |
<div class="ai-advice"> | |
<h3>AI 健康建议</h3> | |
<div class="ai-advice-content" id="ai-advice-text">{{ result.ai_advice }}</div> | |
</div> | |
{% endif %} | |
{% if error %} | |
<div class="ai-advice" style="margin-top: 24px;"> | |
<h3 style="color: #ff8a8a;">诊断出错</h3> | |
<div class="ai-advice-content" style="border-color: #ff8a8a; background-color: rgba(220, 38, 38, 0.2); color: #f0f2f5;"> | |
{{ error }} | |
</div> | |
</div> | |
{% endif %} | |
</div> | |
<div class="action-buttons" style="display: flex; gap: 0.75rem; margin-top: 1.5rem; justify-content: center; flex-wrap: wrap;"> | |
<button class="reset-btn" id="reset-btn" style="flex: 1; min-width: 150px; max-width: 200px;">上传新图片</button> | |
<button class="example-btn" id="example-btn" style="flex: 1; min-width: 150px; max-width: 200px;">选择其他示例</button> | |
<button class="home-btn" id="home-btn" style="flex: 1; min-width: 150px; max-width: 200px;">回到起始界面</button> | |
</div> | |
</div> | |
</div> | |
<div class="panel right-panel"> | |
<div class="chat-header">AI 健康助手</div> | |
<div class="chat-history" id="chat-history"> | |
<div class="chat-message assistant"> | |
你好!我是您的AI健康助手。完成诊断后,您可以在此基于报告向我提问。 | |
</div> | |
</div> | |
<div class="chat-input-area"> | |
<div class="chat-input-wrapper"> | |
<input id="ai-user-input" type="text" placeholder="基于诊断结果进一步提问..."> | |
<button id="send-to-ai" title="发送">➔</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// --- 主题切换功能 --- | |
const themeToggle = document.getElementById('theme-toggle'); | |
const html = document.documentElement; | |
// 从localStorage获取保存的主题 | |
const savedTheme = localStorage.getItem('theme') || 'light'; | |
if (savedTheme === 'dark') { | |
html.setAttribute('data-theme', 'dark'); | |
} | |
// 主题切换事件 | |
themeToggle.addEventListener('click', () => { | |
const currentTheme = html.getAttribute('data-theme'); | |
if (currentTheme === 'dark') { | |
html.removeAttribute('data-theme'); | |
localStorage.setItem('theme', 'light'); | |
} else { | |
html.setAttribute('data-theme', 'dark'); | |
localStorage.setItem('theme', 'dark'); | |
} | |
}); | |
const uploadSection = document.getElementById('upload-section'); | |
const resultSection = document.getElementById('result-section'); | |
const fileInput = document.getElementById('file-upload'); | |
const uploaderText = document.getElementById('uploader-text'); | |
const exampleSelect = document.getElementById('example-select'); | |
const loadExampleBtn = document.getElementById('load-example'); | |
const resetBtn = document.getElementById('reset-btn'); | |
const homeBtn = document.getElementById('home-btn'); | |
const exampleBtn = document.getElementById('example-btn'); | |
const loadingOverlay = document.getElementById('loading-overlay'); | |
// 显示加载动画 | |
function showLoading() { | |
loadingOverlay.classList.add('show'); | |
} | |
// 隐藏加载动画 | |
function hideLoading() { | |
loadingOverlay.classList.remove('show'); | |
} | |
// 表单提交时显示加载动画 | |
document.getElementById('upload-form').addEventListener('submit', function() { | |
showLoading(); | |
}); | |
if (resetBtn) resetBtn.addEventListener('click', () => window.location.href = '/'); | |
if (homeBtn) homeBtn.addEventListener('click', () => window.location.href = '/'); | |
if (exampleBtn) exampleBtn.addEventListener('click', () => { | |
resultSection.hidden = true; | |
uploadSection.hidden = false; | |
fileInput.value = ''; | |
uploaderText.textContent = '点击或拖拽上传眼底照片'; | |
exampleSelect.value = ''; | |
}); | |
// --- 核心修复:页面状态切换逻辑 --- | |
// 使用DOM元素检查来避免Jinja2模板语法解析错误 | |
const resultCards = document.querySelectorAll('.result-card'); | |
const hasResult = resultCards.length > 0 || document.querySelector('.ai-advice') !== null; | |
if (hasResult) { | |
uploadSection.hidden = true; | |
resultSection.hidden = false; | |
// 如果页面已经有结果,隐藏加载动画 | |
hideLoading(); | |
} else { | |
uploadSection.hidden = false; | |
resultSection.hidden = true; | |
} | |
async function loadExamples() { | |
try { | |
const response = await fetch('/get_examples'); | |
const data = await response.json(); | |
data.examples.forEach(example => { | |
const option = document.createElement('option'); | |
option.value = example; | |
option.textContent = example; | |
exampleSelect.appendChild(option); | |
}); | |
} catch (error) { | |
console.error('Failed to load examples:', error); | |
} | |
} | |
loadExamples(); | |
loadExampleBtn.addEventListener('click', async () => { | |
const selectedExample = exampleSelect.value; | |
if (!selectedExample) { | |
alert('请先选择一个示例照片'); | |
return; | |
} | |
try { | |
showLoading(); | |
const response = await fetch("/examples/" + selectedExample); | |
const blob = await response.blob(); | |
const file = new File([blob], selectedExample, { type: blob.type }); | |
const dt = new DataTransfer(); | |
dt.items.add(file); | |
fileInput.files = dt.files; | |
uploaderText.textContent = selectedExample; | |
document.getElementById('upload-form').submit(); | |
} catch (error) { | |
console.error('Failed to load example:', error); | |
hideLoading(); | |
alert('加载示例照片失败,请检查网络或文件是否存在'); | |
} | |
}); | |
// 图像压缩函数 | |
function compressImage(file, maxWidth = 1024, quality = 0.8) { | |
return new Promise((resolve) => { | |
const canvas = document.createElement('canvas'); | |
const ctx = canvas.getContext('2d'); | |
const img = new Image(); | |
img.onload = () => { | |
// 计算压缩后的尺寸 | |
let { width, height } = img; | |
if (width > maxWidth) { | |
height = (height * maxWidth) / width; | |
width = maxWidth; | |
} | |
canvas.width = width; | |
canvas.height = height; | |
// 绘制并压缩 | |
ctx.drawImage(img, 0, 0, width, height); | |
canvas.toBlob(resolve, 'image/jpeg', quality); | |
}; | |
img.src = URL.createObjectURL(file); | |
}); | |
} | |
// 文件选择处理 | |
fileInput.addEventListener('change', async () => { | |
const file = fileInput.files[0]; | |
if (file) { | |
try { | |
// 显示加载动画 | |
showLoading(); | |
// 压缩图像 | |
const compressedBlob = await compressImage(file); | |
// 创建新的File对象 | |
const compressedFile = new File([compressedBlob], file.name, { | |
type: 'image/jpeg', | |
lastModified: Date.now() | |
}); | |
// 更新file input | |
const dt = new DataTransfer(); | |
dt.items.add(compressedFile); | |
fileInput.files = dt.files; | |
uploaderText.textContent = `已压缩: ${file.name} (${(compressedBlob.size / 1024).toFixed(1)}KB)`; | |
// 隐藏加载动画 | |
hideLoading(); | |
} catch (error) { | |
console.error('图像压缩失败:', error); | |
uploaderText.textContent = file.name; | |
hideLoading(); | |
} | |
} else { | |
uploaderText.textContent = '点击或拖拽上传眼底照片'; | |
} | |
}); | |
const adviceTextElement = document.getElementById('ai-advice-text'); | |
const userInput = document.getElementById('ai-user-input'); | |
const sendBtn = document.getElementById('send-to-ai'); | |
const chatHistory = document.getElementById('chat-history'); | |
let conversationHistory = []; | |
if (adviceTextElement && adviceTextElement.textContent.trim().length > 0) { | |
const initialAdvice = adviceTextElement.textContent.trim(); | |
chatHistory.innerHTML = ''; | |
const systemPrompt = `你是一位专业的医疗健康顾问。以下是用户通过眼底照片AI诊断系统得到的初步结果和建议,请基于此信息,用专业、严谨且易于理解的语言回答用户的后续提问。诊断报告: | |
${initialAdvice}`; | |
conversationHistory.push({ role: 'system', content: systemPrompt }); | |
const grading = "{{ result.grading if result and result.grading else 'N/A' }}"; | |
const diabetic = "{{ result.diabetic if result and result.diabetic else 'N/A' }}"; | |
const summaryContent = `我已收到您的诊断报告,摘要如下: | |
* **分级结果**: ${grading} | |
* **糖尿病风险**: ${diabetic} | |
您可以就此报告向我提问了。`; | |
addMessageToChat('assistant', summaryContent); | |
} | |
// 简单的缓存机制 | |
const responseCache = new Map(); | |
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 | |
function getCachedResponse(key) { | |
const cached = responseCache.get(key); | |
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { | |
return cached.data; | |
} | |
return null; | |
} | |
function setCachedResponse(key, data) { | |
responseCache.set(key, { | |
data: data, | |
timestamp: Date.now() | |
}); | |
} | |
async function callLargeModelAPI(messages) { | |
// 创建缓存键 | |
const cacheKey = JSON.stringify(messages); | |
// 检查缓存 | |
const cachedResponse = getCachedResponse(cacheKey); | |
if (cachedResponse) { | |
return cachedResponse; | |
} | |
try { | |
const response = await fetch('/chat', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ messages: messages }) | |
}); | |
if (!response.ok) { | |
const errorData = await response.json(); | |
return "抱歉,AI助手服务暂时出现问题。错误: " + (errorData.error || response.statusText); | |
} | |
const data = await response.json(); | |
const content = data.content || 'AI助手暂时无法回复。'; | |
// 缓存响应 | |
setCachedResponse(cacheKey, content); | |
return content; | |
} catch (error) { | |
console.error('API call failed:', error); | |
return '抱歉,无法连接到AI助手服务,请检查网络连接或联系管理员。'; | |
} | |
} | |
function addMessageToChat(role, content) { | |
const messageElement = document.createElement('div'); | |
messageElement.classList.add('chat-message', role); | |
if (typeof marked !== 'undefined' && marked.parse) { | |
messageElement.innerHTML = marked.parse(content); | |
} else { | |
messageElement.textContent = content; | |
} | |
chatHistory.appendChild(messageElement); | |
chatHistory.scrollTop = chatHistory.scrollHeight; | |
} | |
async function handleSendMessage() { | |
const userQuery = userInput.value.trim(); | |
if (!userQuery) return; | |
addMessageToChat('user', userQuery); | |
conversationHistory.push({ role: 'user', content: userQuery }); | |
userInput.value = ''; | |
const thinkingElement = document.createElement('div'); | |
thinkingElement.classList.add('chat-message', 'assistant'); | |
thinkingElement.innerHTML = "AI助手正在思考中..."; | |
chatHistory.appendChild(thinkingElement); | |
chatHistory.scrollTop = chatHistory.scrollHeight; | |
const aiResponse = await callLargeModelAPI(conversationHistory); | |
if (typeof marked !== 'undefined' && marked.parse) { | |
thinkingElement.innerHTML = marked.parse(aiResponse); | |
} else { | |
thinkingElement.textContent = aiResponse; | |
} | |
conversationHistory.push({ role: 'assistant', content: aiResponse }); | |
} | |
sendBtn.addEventListener('click', handleSendMessage); | |
userInput.addEventListener('keydown', (event) => { | |
if (event.key === 'Enter' && !event.shiftKey) { | |
event.preventDefault(); | |
handleSendMessage(); | |
} | |
}); | |
}); | |
</script> | |
<script> | |
if (typeof tsParticles !== 'undefined' && tsParticles.load) { | |
tsParticles.load("tsparticles", { | |
fpsLimit: 120, | |
interactivity: { | |
events: { | |
onHover: { | |
enable: true, | |
mode: "grab" // 核心改动:将交互模式设置为 "grab" | |
}, | |
onClick: { | |
enable: true, | |
mode: "push" | |
}, | |
resize: true | |
}, | |
modes: { | |
grab: { | |
distance: 200, // 鼠标与粒子产生连线的最大距离 | |
links: { | |
opacity: 0.8, | |
color: "#ffffff" // 连线的颜色 | |
} | |
}, | |
push: { | |
quantity: 4 | |
}, | |
// 保留 repulse 配置,方便未来可能切换回去 | |
repulse: { | |
distance: 100, | |
duration: 0.4 | |
} | |
} | |
}, | |
particles: { | |
color: { value: "#ffffff" }, | |
links: { color: "#ffffff", distance: 150, enable: true, opacity: 0.2, width: 1 }, | |
collisions: { enable: true }, | |
move: { direction: "none", enable: true, outModes: { default: "bounce" }, random: false, speed: 1, straight: false }, | |
number: { density: { enable: true, area: 800 }, value: 80 }, | |
opacity: { value: 0.3 }, | |
shape: { type: "circle" }, | |
size: { value: { min: 1, max: 3 } }, | |
}, | |
detectRetina: true, | |
}); | |
} else { | |
console.warn('tsParticles library not loaded, particle effects will be disabled.'); | |
} | |
</script> | |
</body> | |
</html> |