PicExam / static /index.html
xwwww's picture
1
26b681b
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PicExam - Qwen-VL 图像理解</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.content {
padding: 30px;
}
.upload-area {
border: 3px dashed #ddd;
border-radius: 10px;
padding: 40px;
text-align: center;
margin-bottom: 20px;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #667eea;
background-color: #f8f9ff;
}
.upload-area.dragover {
border-color: #667eea;
background-color: #f0f2ff;
}
.upload-icon {
font-size: 3em;
color: #ddd;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
input[type="file"], textarea {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
input[type="file"]:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
textarea {
resize: vertical;
min-height: 80px;
}
.btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 15px 30px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.result {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
border-left: 5px solid #667eea;
}
.result h3 {
color: #333;
margin-bottom: 15px;
}
.result-content {
background: white;
padding: 15px;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
background: #ffe6e6;
border-left-color: #ff4757;
color: #c44569;
}
.preview-image {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
margin: 15px 0;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.status-bar {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #28a745;
}
.status-dot.loading {
background: #ffc107;
animation: pulse 1.5s infinite;
}
.status-dot.error {
background: #dc3545;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏆 PicExam</h1>
<p>基于 Qwen-VL 的智能图像理解系统</p>
<div style="margin-top: 15px; font-size: 0.9em;">
<a href="/docs" style="color: white; text-decoration: none; margin-right: 15px;">📚 API 文档</a>
<a href="/" style="color: white; text-decoration: none; margin-right: 15px;">🔗 API 端点</a>
<a href="/memory_status" style="color: white; text-decoration: none;">💾 内存状态</a>
</div>
</div>
<div class="content">
<div class="status-bar">
<div class="status-indicator">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">检查服务状态...</span>
</div>
<div>
<button onclick="checkStatus()" style="background: none; border: 1px solid #ddd; padding: 5px 10px; border-radius: 5px; cursor: pointer; margin-right: 10px;">刷新</button>
<button onclick="showDependencies()" style="background: none; border: 1px solid #ddd; padding: 5px 10px; border-radius: 5px; cursor: pointer; margin-right: 10px;">依赖状态</button>
<button onclick="testConnection()" style="background: none; border: 1px solid #ddd; padding: 5px 10px; border-radius: 5px; cursor: pointer;">连接测试</button>
</div>
</div>
<!-- 依赖状态显示区域 -->
<div id="dependencyStatus" style="display: none; background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<h4 style="margin-bottom: 10px;">📦 依赖状态</h4>
<div id="dependencyContent">加载中...</div>
</div>
<form id="uploadForm">
<div class="form-group">
<label for="imageFile">选择图片</label>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📷</div>
<p>点击选择图片或拖拽图片到此处</p>
<p style="font-size: 0.9em; color: #666; margin-top: 10px;">支持 JPG, PNG, WebP 格式</p>
</div>
<input type="file" id="imageFile" accept="image/*" style="display: none;">
<img id="previewImage" class="preview-image" style="display: none;">
</div>
<div class="form-group">
<label for="question">问题描述</label>
<textarea id="question" placeholder="请输入您想问的关于图片的问题,例如:请描述这张图片的内容、图片中有什么物体、图片的颜色如何等...">请描述这张图片的内容</textarea>
</div>
<button type="submit" class="btn" id="submitBtn">
🔍 分析图片
</button>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>正在分析图片,请稍候...</p>
</div>
<div id="result" style="display: none;"></div>
<!-- API 测试区域 -->
<div style="margin-top: 40px; padding-top: 30px; border-top: 2px solid #eee;">
<h2 style="color: #333; margin-bottom: 20px;">🔧 API 测试工具</h2>
<div style="background: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 20px;">
<h3 style="color: #555; margin-bottom: 15px;">JSON API 测试 (/analyze)</h3>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600;">提示词:</label>
<input type="text" id="jsonPrompt" value="请详细描述这张图片的内容" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
</div>
<button onclick="testJsonAPI()" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer;">测试 JSON API</button>
<div id="jsonResult" style="margin-top: 15px; display: none;"></div>
</div>
<div style="background: #f8f9fa; padding: 20px; border-radius: 10px;">
<h3 style="color: #555; margin-bottom: 15px;">API 端点信息</h3>
<button onclick="showAPIInfo()" style="background: #17a2b8; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer;">获取 API 信息</button>
<div id="apiInfo" style="margin-top: 15px; display: none;"></div>
</div>
</div>
</div>
</div>
<script>
// 检查服务状态
async function checkStatus() {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
statusDot.className = 'status-dot loading';
statusText.textContent = '检查中...';
try {
// 检查基本服务状态
console.log('正在检查服务状态...');
const response = await fetch('/', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('服务状态响应:', data);
// 检查依赖状态
console.log('正在检查依赖状态...');
const depsResponse = await fetch('/dependencies', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
if (!depsResponse.ok) {
console.warn('依赖检查失败:', depsResponse.status, depsResponse.statusText);
}
const depsData = depsResponse.ok ? await depsResponse.json() : null;
console.log('依赖状态响应:', depsData);
const modelLoaded = safeGet(data, 'status.model_loaded', false);
const depsAvailable = safeGet(data, 'status.dependencies_available', false);
if (modelLoaded) {
statusDot.className = 'status-dot';
statusText.textContent = '✅ 服务正常,模型已加载';
} else if (depsAvailable) {
statusDot.className = 'status-dot loading';
statusText.textContent = '⏳ 服务运行中,模型加载中...';
} else {
statusDot.className = 'status-dot error';
statusText.textContent = '⚠️ 服务运行中,依赖缺失';
}
// 显示详细的依赖信息
const missingQwen = safeGet(depsData, 'missing_qwen', []);
if (Array.isArray(missingQwen) && missingQwen.length > 0) {
const missingDeps = missingQwen.join(', ');
statusText.textContent += ` (缺失: ${missingDeps})`;
}
} catch (error) {
console.error('服务状态检查失败:', error);
statusDot.className = 'status-dot error';
statusText.textContent = `❌ 服务连接失败: ${error.message}`;
}
}
// 显示依赖状态
async function showDependencies() {
const statusDiv = document.getElementById('dependencyStatus');
const contentDiv = document.getElementById('dependencyContent');
statusDiv.style.display = 'block';
contentDiv.innerHTML = '🔄 检查依赖状态...';
try {
console.log('正在获取依赖状态...');
const response = await fetch('/dependencies');
console.log('依赖状态响应:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('依赖数据:', data);
// 确保数据结构完整
if (!data || typeof data !== 'object') {
throw new Error('无效的依赖数据格式');
}
// 使用安全的默认值
const basicDeps = safeGet(data, 'basic_dependencies', {});
const qwenDeps = safeGet(data, 'qwen_dependencies', {});
const missingBasic = safeGet(data, 'missing_basic', []);
const missingQwen = safeGet(data, 'missing_qwen', []);
const installCommands = safeGet(data, 'installation_commands', {});
let html = '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">';
// 基础依赖
html += '<div>';
html += '<h5 style="color: #333; margin-bottom: 10px;">🔧 基础依赖</h5>';
if (basicDeps && typeof basicDeps === 'object') {
for (const [dep, status] of Object.entries(basicDeps)) {
html += `<div style="margin-bottom: 5px;">${status ? '✅' : '❌'} ${dep}</div>`;
}
} else {
html += '<div style="margin-bottom: 5px;">⚠️ 无法获取基础依赖信息</div>';
}
html += '</div>';
// Qwen-VL 依赖
html += '<div>';
html += '<h5 style="color: #333; margin-bottom: 10px;">🤖 Qwen-VL 依赖</h5>';
if (qwenDeps && typeof qwenDeps === 'object') {
for (const [dep, status] of Object.entries(qwenDeps)) {
html += `<div style="margin-bottom: 5px;">${status ? '✅' : '❌'} ${dep}</div>`;
}
} else {
html += '<div style="margin-bottom: 5px;">⚠️ 无法获取 Qwen-VL 依赖信息</div>';
}
html += '</div>';
html += '</div>';
// 安装命令
const hasMissingBasic = Array.isArray(missingBasic) && missingBasic.length > 0;
const hasMissingQwen = Array.isArray(missingQwen) && missingQwen.length > 0;
if (hasMissingBasic || hasMissingQwen) {
html += '<hr style="margin: 15px 0;">';
html += '<h5 style="color: #333; margin-bottom: 10px;">💡 安装命令</h5>';
if (hasMissingBasic) {
const basicCmd = safeGet(installCommands, 'basic', `pip install ${missingBasic.join(' ')}`);
html += `<p><strong>基础依赖:</strong></p>`;
html += `<code style="background: #f1f1f1; padding: 5px; border-radius: 3px; display: block; margin: 5px 0;">${basicCmd}</code>`;
}
if (hasMissingQwen) {
const qwenCmd = safeGet(installCommands, 'qwen', `pip install ${missingQwen.join(' ')}`);
html += `<p><strong>Qwen-VL 依赖:</strong></p>`;
html += `<code style="background: #f1f1f1; padding: 5px; border-radius: 3px; display: block; margin: 5px 0;">${qwenCmd}</code>`;
}
}
// 状态总结
html += '<hr style="margin: 15px 0;">';
html += '<div style="display: flex; justify-content: space-between; align-items: center;">';
const basicReady = safeGet(data, 'basic_ready', false);
const qwenReady = safeGet(data, 'qwen_ready', false);
html += `<span><strong>状态:</strong> ${basicReady ? '✅ 基础就绪' : '❌ 基础缺失'} | ${qwenReady ? '✅ AI 就绪' : '❌ AI 缺失'}</span>`;
html += '<button onclick="document.getElementById(\'dependencyStatus\').style.display=\'none\'" style="background: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">关闭</button>';
html += '</div>';
contentDiv.innerHTML = html;
} catch (error) {
contentDiv.innerHTML = `❌ 获取依赖状态失败: ${error.message}`;
}
}
// 连接测试
async function testConnection() {
console.log('开始连接测试...');
console.log('当前页面 URL:', window.location.href);
console.log('当前域名:', window.location.origin);
const statusDiv = document.getElementById('dependencyStatus');
const contentDiv = document.getElementById('dependencyContent');
statusDiv.style.display = 'block';
contentDiv.innerHTML = '🔄 正在测试连接...';
const tests = [
{ name: '基本连接', url: '/' },
{ name: '健康检查', url: '/health' },
{ name: '依赖状态', url: '/dependencies' },
{ name: 'Web 界面', url: '/web' }
];
let results = '<h5>🔗 连接测试结果</h5>';
for (const test of tests) {
try {
console.log(`测试 ${test.name}: ${test.url}`);
const startTime = Date.now();
const response = await fetch(test.url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
timeout: 10000
});
const endTime = Date.now();
const duration = endTime - startTime;
if (response.ok) {
results += `<div style="margin-bottom: 5px;">✅ ${test.name}: 成功 (${duration}ms)</div>`;
console.log(`${test.name} 成功:`, response.status, duration + 'ms');
} else {
results += `<div style="margin-bottom: 5px;">❌ ${test.name}: HTTP ${response.status}</div>`;
console.log(`${test.name} 失败:`, response.status, response.statusText);
}
} catch (error) {
results += `<div style="margin-bottom: 5px;">❌ ${test.name}: ${error.message}</div>`;
console.error(`${test.name} 异常:`, error);
}
}
results += '<hr style="margin: 15px 0;">';
results += '<div style="display: flex; justify-content: space-between; align-items: center;">';
results += '<span><strong>测试完成</strong></span>';
results += '<button onclick="document.getElementById(\'dependencyStatus\').style.display=\'none\'" style="background: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">关闭</button>';
results += '</div>';
contentDiv.innerHTML = results;
}
// 安全的数据访问函数
function safeGet(obj, path, defaultValue = null) {
try {
const keys = path.split('.');
let result = obj;
for (const key of keys) {
if (result === null || result === undefined || typeof result !== 'object') {
return defaultValue;
}
result = result[key];
}
return result !== undefined ? result : defaultValue;
} catch (error) {
console.warn('安全访问失败:', path, error);
return defaultValue;
}
}
// 页面加载时检查状态
window.addEventListener('load', checkStatus);
// 文件上传处理
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('imageFile');
const previewImage = document.getElementById('previewImage');
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
handleFileSelect();
}
});
fileInput.addEventListener('change', handleFileSelect);
function handleFileSelect() {
const file = fileInput.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
previewImage.src = e.target.result;
previewImage.style.display = 'block';
uploadArea.innerHTML = `
<div class="upload-icon">✅</div>
<p>已选择: ${file.name}</p>
<p style="font-size: 0.9em; color: #666;">点击重新选择</p>
`;
};
reader.readAsDataURL(file);
}
}
// 表单提交处理
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const file = fileInput.files[0];
const question = document.getElementById('question').value;
if (!file) {
alert('请先选择一张图片');
return;
}
const submitBtn = document.getElementById('submitBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
// 显示加载状态
submitBtn.disabled = true;
loading.style.display = 'block';
result.style.display = 'none';
try {
console.log('开始图片分析...');
console.log('文件信息:', {
name: file.name,
size: file.size,
type: file.type
});
// 将图片转换为 base64
console.log('正在转换图片为 base64...');
const base64Image = await fileToBase64(file);
console.log('Base64 转换完成,长度:', base64Image.length);
// 优先使用 JSON API
console.log('发送分析请求到 /analyze...');
const requestData = {
image: base64Image,
prompt: question
};
console.log('请求数据:', {
prompt: question,
imageLength: base64Image.length,
imagePrefix: base64Image.substring(0, 50) + '...'
});
const response = await fetch('/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
console.log('收到响应:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
console.error('响应错误:', errorText);
throw new Error(`HTTP ${response.status}: ${response.statusText}\n${errorText}`);
}
const data = await response.json();
console.log('解析响应数据:', data);
if (data.success) {
result.innerHTML = `
<div class="result">
<h3>📝 分析结果</h3>
<div class="result-content">
<p><strong>问题:</strong> ${data.prompt}</p>
<p><strong>回答:</strong> ${data.response}</p>
<p><strong>处理时间:</strong> ${data.processing_time.toFixed(2)}秒</p>
<p><strong>图片信息:</strong> ${data.image_info.size} (${data.image_info.mode})</p>
</div>
</div>
`;
} else {
// 如果 JSON API 失败,显示详细错误信息
let errorHtml = `
<div class="result error">
<h3>❌ 分析失败</h3>
<div class="result-content">
<p><strong>错误:</strong> ${data.error}</p>
`;
// 如果是依赖问题,提供解决方案
if (data.error && data.error.includes('依赖')) {
errorHtml += `
<hr style="margin: 15px 0;">
<h4>💡 解决方案:</h4>
<p>请安装缺失的依赖:</p>
<code style="background: #f1f1f1; padding: 5px; border-radius: 3px; display: block; margin: 10px 0;">
pip install transformers accelerate qwen-vl-utils torchvision
</code>
<p>或使用自动安装脚本:</p>
<code style="background: #f1f1f1; padding: 5px; border-radius: 3px; display: block; margin: 10px 0;">
python install_and_start.py
</code>
`;
}
errorHtml += `
</div>
</div>
`;
result.innerHTML = errorHtml;
}
} catch (error) {
result.innerHTML = `
<div class="result error">
<h3>❌ 请求失败</h3>
<div class="result-content">
<p><strong>错误:</strong> ${error.message}</p>
<hr style="margin: 15px 0;">
<h4>🔧 可能的原因:</h4>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>服务器未启动或无法连接</li>
<li>模型依赖未安装</li>
<li>网络连接问题</li>
</ul>
<p>请检查服务器状态或查看控制台日志。</p>
</div>
</div>
`;
} finally {
submitBtn.disabled = false;
loading.style.display = 'none';
result.style.display = 'block';
}
});
// JSON API 测试
async function testJsonAPI() {
const file = fileInput.files[0];
const prompt = document.getElementById('jsonPrompt').value;
const resultDiv = document.getElementById('jsonResult');
if (!file) {
alert('请先选择一张图片');
return;
}
try {
// 将图片转换为 base64
const base64 = await fileToBase64(file);
const requestData = {
image: base64,
prompt: prompt
};
resultDiv.innerHTML = '<p>🔄 正在调用 JSON API...</p>';
resultDiv.style.display = 'block';
const response = await fetch('/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.success) {
resultDiv.innerHTML = `
<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px;">
<h4 style="color: #155724; margin-bottom: 10px;">✅ JSON API 调用成功</h4>
<p><strong>提示词:</strong> ${data.prompt}</p>
<p><strong>响应:</strong> ${data.response}</p>
<p><strong>处理时间:</strong> ${data.processing_time.toFixed(2)}秒</p>
<p><strong>图片信息:</strong> ${data.image_info.size} (${data.image_info.mode})</p>
</div>
`;
} else {
resultDiv.innerHTML = `
<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px;">
<h4 style="color: #721c24; margin-bottom: 10px;">❌ JSON API 调用失败</h4>
<p><strong>错误:</strong> ${data.error}</p>
</div>
`;
}
} catch (error) {
resultDiv.innerHTML = `
<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px;">
<h4 style="color: #721c24; margin-bottom: 10px;">❌ 请求失败</h4>
<p><strong>错误:</strong> ${error.message}</p>
</div>
`;
}
}
// 显示 API 信息
async function showAPIInfo() {
const infoDiv = document.getElementById('apiInfo');
try {
infoDiv.innerHTML = '<p>🔄 获取 API 信息...</p>';
infoDiv.style.display = 'block';
const response = await fetch('/');
const data = await response.json();
let endpointsHtml = '';
for (const [endpoint, info] of Object.entries(data.endpoints)) {
endpointsHtml += `
<div style="margin-bottom: 15px; padding: 10px; background: white; border-radius: 5px; border-left: 3px solid #667eea;">
<h5 style="color: #333; margin-bottom: 5px;">${endpoint}</h5>
<p style="color: #666; margin-bottom: 5px;">${info.description}</p>
${info.example ? `<code style="background: #f1f1f1; padding: 2px 5px; border-radius: 3px; font-size: 0.9em;">${info.example}</code>` : ''}
</div>
`;
}
infoDiv.innerHTML = `
<div style="background: #e7f3ff; border: 1px solid #b3d9ff; padding: 15px; border-radius: 5px;">
<h4 style="color: #0056b3; margin-bottom: 15px;">📋 API 端点信息</h4>
<p><strong>服务:</strong> ${data.service}</p>
<p><strong>版本:</strong> ${data.version}</p>
<p><strong>模型:</strong> ${data.model}</p>
<p><strong>状态:</strong> ${data.status.model_loaded ? '✅ 模型已加载' : '⏳ 模型加载中'}</p>
<hr style="margin: 15px 0; border: none; border-top: 1px solid #ccc;">
<h5 style="color: #333; margin-bottom: 10px;">可用端点:</h5>
${endpointsHtml}
</div>
`;
} catch (error) {
infoDiv.innerHTML = `
<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px;">
<h4 style="color: #721c24; margin-bottom: 10px;">❌ 获取 API 信息失败</h4>
<p><strong>错误:</strong> ${error.message}</p>
</div>
`;
}
}
// 文件转 base64 工具函数
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
</script>
</body>
</html>