ceasonen
优化了处理效率
59a0302
<!DOCTYPE html>
<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 !important; }
.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 !important;
color: var(--primary-text) !important;
border: 1px solid var(--border-color) !important;
}
.home-btn:hover { background: var(--hover-bg) !important; }
.upload-section, .result-wrapper {
display: flex;
flex-direction: column;
height: 100%;
animation: fadeIn 0.5s ease;
}
/* 核心修复: 确保 hidden 属性能够强制隐藏元素 */
[hidden] {
display: none !important;
}
@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>