|
|
<!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> |
|
|
|
|
|
|
|
|
<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>'; |
|
|
|
|
|
|
|
|
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 |
|
|
}); |
|
|
|
|
|
|
|
|
console.log('正在转换图片为 base64...'); |
|
|
const base64Image = await fileToBase64(file); |
|
|
console.log('Base64 转换完成,长度:', base64Image.length); |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
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'; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
async function testJsonAPI() { |
|
|
const file = fileInput.files[0]; |
|
|
const prompt = document.getElementById('jsonPrompt').value; |
|
|
const resultDiv = document.getElementById('jsonResult'); |
|
|
|
|
|
if (!file) { |
|
|
alert('请先选择一张图片'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
|
|
|
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> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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> |
|
|
|