mmeb/templates/local_index.html
2025-09-22 10:13:11 +00:00

996 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>本地多模态检索系统 - FAISS</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #2563eb;
--secondary-color: #64748b;
--success-color: #059669;
--warning-color: #d97706;
--danger-color: #dc2626;
--bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
body {
background: var(--bg-gradient);
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.main-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
margin: 20px auto;
max-width: 1200px;
}
.header {
background: linear-gradient(135deg, var(--primary-color), #3b82f6);
color: white;
padding: 2rem;
border-radius: 20px 20px 0 0;
text-align: center;
}
.mode-card {
background: white;
border-radius: 15px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.mode-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.mode-card.active {
border-color: var(--primary-color);
background: linear-gradient(135deg, #eff6ff, #dbeafe);
}
.mode-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
display: block;
}
.text-to-text { color: #059669; }
.text-to-image { color: #dc2626; }
.image-to-text { color: #d97706; }
.image-to-image { color: #7c3aed; }
.search-input {
border-radius: 12px;
border: 2px solid #e5e7eb;
padding: 12px 16px;
font-size: 16px;
transition: all 0.3s ease;
}
.search-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.btn-primary {
background: var(--primary-color);
border: none;
border-radius: 12px;
padding: 12px 24px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: #1d4ed8;
transform: translateY(-2px);
}
.file-upload-area {
border: 3px dashed #d1d5db;
border-radius: 12px;
padding: 3rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
}
.file-upload-area:hover {
border-color: var(--primary-color);
background: rgba(37, 99, 235, 0.05);
}
.file-upload-area.dragover {
border-color: var(--primary-color);
background: rgba(37, 99, 235, 0.1);
}
.result-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
border-left: 4px solid var(--primary-color);
}
.result-image {
max-width: 200px;
max-height: 150px;
border-radius: 8px;
object-fit: cover;
}
.score-badge {
background: var(--success-color);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
}
.loading-spinner {
display: none;
text-align: center;
padding: 2rem;
}
.status-indicator {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.query-image {
max-width: 300px;
max-height: 200px;
border-radius: 12px;
object-fit: cover;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body>
<!-- 状态指示器 -->
<div class="status-indicator">
<div id="statusBadge" class="badge bg-secondary">
<i class="fas fa-circle-notch fa-spin"></i> 未初始化
</div>
</div>
<div class="container-fluid">
<div class="main-container">
<!-- 头部 -->
<div class="header">
<h1><i class="fas fa-search"></i> 本地多模态检索系统</h1>
<p class="mb-0">基于本地模型和FAISS向量数据库支持文搜图、文搜文、图搜图、图搜文四种检索模式</p>
</div>
<div class="p-4">
<!-- 重新初始化按钮 -->
<div class="text-center mb-4">
<button id="reinitBtn" class="btn btn-warning">
<i class="fas fa-redo"></i> 重新初始化系统
</button>
</div>
<!-- 检索模式选择 -->
<div class="row mb-4" id="modeSelection">
<div class="col-md-3">
<div class="mode-card text-center" data-mode="text_to_text">
<i class="fas fa-file-text mode-icon text-to-text"></i>
<h5>文搜文</h5>
<p class="text-muted">文本查找相似文本</p>
</div>
</div>
<div class="col-md-3">
<div class="mode-card text-center" data-mode="text_to_image">
<i class="fas fa-image mode-icon text-to-image"></i>
<h5>文搜图</h5>
<p class="text-muted">文本查找相关图片</p>
</div>
</div>
<div class="col-md-3">
<div class="mode-card text-center" data-mode="image_to_text">
<i class="fas fa-comment mode-icon image-to-text"></i>
<h5>图搜文</h5>
<p class="text-muted">图片查找相关文本</p>
</div>
</div>
<div class="col-md-3">
<div class="mode-card text-center" data-mode="image_to_image">
<i class="fas fa-images mode-icon image-to-image"></i>
<h5>图搜图</h5>
<p class="text-muted">图片查找相似图片</p>
</div>
</div>
</div>
<!-- 数据管理界面 -->
<div class="row mb-4" id="dataManagement">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-database"></i> 数据管理</h5>
<small class="text-muted">上传和管理检索数据库</small>
</div>
<div class="card-body">
<div class="row">
<!-- 批量上传图片 -->
<div class="col-md-6">
<div class="upload-section">
<h6><i class="fas fa-images text-primary"></i> 批量上传图片</h6>
<div class="file-upload-area" id="batchImageUpload">
<i class="fas fa-cloud-upload-alt fa-2x text-muted mb-2"></i>
<p>拖拽多张图片到此处或点击选择</p>
<input type="file" id="batchImageFiles" multiple accept="image/*" style="display: none;">
<button class="btn btn-outline-primary btn-sm mt-2" onclick="document.getElementById('batchImageFiles').click()">
<i class="fas fa-folder-open"></i> 选择图片
</button>
</div>
<div id="imageUploadProgress" class="mt-2" style="display: none;">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted mt-1 d-block">上传进度: <span id="imageProgressText">0/0</span></small>
</div>
</div>
</div>
<!-- 批量上传文本 -->
<div class="col-md-6">
<div class="upload-section">
<h6><i class="fas fa-file-text text-success"></i> 批量上传文本</h6>
<div class="mb-3">
<textarea id="batchTextInput" class="form-control" rows="8"
placeholder="请输入文本数据,每行一条文本记录...&#10;例如:&#10;这是第一条文本记录&#10;这是第二条文本记录&#10;这是第三条文本记录"></textarea>
</div>
<div class="d-flex gap-2">
<button id="uploadTextsBtn" class="btn btn-success">
<i class="fas fa-upload"></i> 上传文本
</button>
<button class="btn btn-outline-secondary" onclick="document.getElementById('textFile').click()">
<i class="fas fa-file-import"></i> 从文件导入
</button>
<input type="file" id="textFile" accept=".txt,.csv" style="display: none;">
</div>
</div>
</div>
</div>
<!-- 数据统计和管理 -->
<div class="row mt-4">
<div class="col-md-8">
<div class="d-flex gap-3">
<!-- 移除构建索引按钮,改为自动构建 -->
<button id="viewDataBtn" class="btn btn-info">
<i class="fas fa-list"></i> 查看数据
</button>
<button id="clearDataBtn" class="btn btn-danger">
<i class="fas fa-trash"></i> 清空数据
</button>
</div>
</div>
<div class="col-md-4">
<div id="dataStats" class="text-end">
<small class="text-muted">
图片: <span id="imageCount">0</span> 张 |
文本: <span id="textCount">0</span>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 搜索界面 -->
<div id="searchInterface" style="display: none;">
<!-- 文本搜索 -->
<div id="textSearch" class="search-panel" style="display: none;">
<div class="row">
<div class="col-md-8">
<input type="text" id="textQuery" class="form-control search-input"
placeholder="请输入搜索文本...">
</div>
<div class="col-md-2">
<select id="textTopK" class="form-select search-input">
<option value="3">Top 3</option>
<option value="5" selected>Top 5</option>
<option value="10">Top 10</option>
</select>
</div>
<div class="col-md-2">
<button id="textSearchBtn" class="btn btn-primary w-100">
<i class="fas fa-search"></i> 搜索
</button>
</div>
</div>
</div>
<!-- 图片搜索 -->
<div id="imageSearch" class="search-panel" style="display: none;">
<div class="row">
<div class="col-md-8">
<div class="file-upload-area" id="fileUploadArea">
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
<h5>拖拽图片到此处或点击选择</h5>
<p class="text-muted">支持 PNG, JPG, JPEG, GIF, BMP, WebP 格式</p>
<input type="file" id="imageFile" accept="image/*" style="display: none;">
</div>
</div>
<div class="col-md-2">
<select id="imageTopK" class="form-select search-input">
<option value="3">Top 3</option>
<option value="5" selected>Top 5</option>
<option value="10">Top 10</option>
</select>
</div>
<div class="col-md-2">
<button id="imageSearchBtn" class="btn btn-primary w-100" disabled>
<i class="fas fa-search"></i> 搜索
</button>
</div>
</div>
</div>
</div>
<!-- 加载动画 -->
<div class="loading-spinner" id="loadingSpinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">正在搜索中...</p>
</div>
<!-- 搜索结果 -->
<div id="searchResults" class="mt-4"></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentMode = null;
let systemInitialized = false;
// 重新初始化系统
document.getElementById('reinitBtn').addEventListener('click', async function() {
const btn = this;
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重新初始化中...';
btn.disabled = true;
try {
const response = await fetch('/api/system_info', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const data = await response.json();
if (data.success) {
systemInitialized = true;
document.getElementById('statusBadge').innerHTML =
'<i class="fas fa-check-circle"></i> 已重新初始化';
document.getElementById('statusBadge').className = 'badge bg-success';
showAlert('success', `系统重新初始化成功GPU信息: ${data.gpu_info.length} 个, 向量数量: ${data.retrieval_info.total_vectors || 0}`);
} else {
throw new Error(data.message);
}
} catch (error) {
showAlert('danger', '重新初始化失败: ' + error.message);
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
});
// 模式选择
document.querySelectorAll('.mode-card').forEach(card => {
card.addEventListener('click', function() {
// 更新选中状态
document.querySelectorAll('.mode-card').forEach(c => c.classList.remove('active'));
this.classList.add('active');
currentMode = this.dataset.mode;
setupSearchInterface(currentMode);
});
});
// 设置搜索界面
function setupSearchInterface(mode) {
document.getElementById('searchInterface').style.display = 'block';
document.getElementById('textSearch').style.display = 'none';
document.getElementById('imageSearch').style.display = 'none';
document.getElementById('searchResults').innerHTML = '';
if (mode === 'text_to_text' || mode === 'text_to_image') {
document.getElementById('textSearch').style.display = 'block';
document.getElementById('textQuery').placeholder =
mode === 'text_to_text' ? '请输入要搜索的文本...' : '请输入要搜索图片的描述...';
} else {
document.getElementById('imageSearch').style.display = 'block';
}
}
// 文本搜索
document.getElementById('textSearchBtn').addEventListener('click', performTextSearch);
document.getElementById('textQuery').addEventListener('keypress', function(e) {
if (e.key === 'Enter') performTextSearch();
});
async function performTextSearch() {
const query = document.getElementById('textQuery').value.trim();
const topK = parseInt(document.getElementById('textTopK').value);
if (!query) {
showAlert('warning', '请输入搜索文本');
return;
}
showLoading(true);
try {
const endpoint = '/api/search_by_text';
const filter_type = currentMode === 'text_to_text' ? 'text' : 'image';
const response = await fetch(endpoint, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, k: topK, filter_type: filter_type})
});
const data = await response.json();
if (data.success) {
displayResults(data, currentMode);
} else {
throw new Error(data.message);
}
} catch (error) {
showAlert('danger', '搜索失败: ' + error.message);
} finally {
showLoading(false);
}
}
// 图片上传处理
const fileUploadArea = document.getElementById('fileUploadArea');
const imageFile = document.getElementById('imageFile');
fileUploadArea.addEventListener('click', () => imageFile.click());
fileUploadArea.addEventListener('dragover', handleDragOver);
fileUploadArea.addEventListener('drop', handleDrop);
imageFile.addEventListener('change', handleFileSelect);
function handleDragOver(e) {
e.preventDefault();
fileUploadArea.classList.add('dragover');
}
function handleDrop(e) {
e.preventDefault();
fileUploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
}
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) handleFile(file);
}
function handleFile(file) {
if (!file.type.startsWith('image/')) {
showAlert('warning', '请选择图片文件');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
fileUploadArea.innerHTML = `
<img src="${e.target.result}" class="query-image mb-3">
<p class="text-success"><i class="fas fa-check"></i> 图片已选择: ${file.name}</p>
`;
document.getElementById('imageSearchBtn').disabled = false;
};
reader.readAsDataURL(file);
}
// 图片搜索
document.getElementById('imageSearchBtn').addEventListener('click', async function() {
const file = imageFile.files[0];
const topK = parseInt(document.getElementById('imageTopK').value);
if (!file) {
showAlert('warning', '请选择图片文件');
return;
}
showLoading(true);
try {
const endpoint = '/api/search_by_image';
const filter_type = currentMode === 'image_to_text' ? 'text' : 'image';
const formData = new FormData();
formData.append('image', file);
formData.append('k', topK);
formData.append('filter_type', filter_type);
const response = await fetch(endpoint, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
displayResults(data, currentMode);
} else {
throw new Error(data.message);
}
} catch (error) {
showAlert('danger', '搜索失败: ' + error.message);
} finally {
showLoading(false);
}
});
// 显示结果
function displayResults(data, mode) {
const resultsContainer = document.getElementById('searchResults');
let html = `
<div class="fade-in">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4><i class="fas fa-search-plus"></i> 搜索结果</h4>
<div>
<span class="badge bg-info">找到 ${data.results?.length || 0} 个结果</span>
<span class="badge bg-secondary">耗时 ${data.search_time || data.time || '0.0'}s</span>
</div>
</div>
`;
if (data.query_image) {
const imageUrl = data.query_image.startsWith('data:') ? data.query_image : `data:image/jpeg;base64,${data.query_image}`;
html += `
<div class="result-card">
<h6><i class="fas fa-image"></i> 查询图片</h6>
<img src="${imageUrl}" class="query-image">
</div>
`;
}
if (data.query) {
html += `
<div class="result-card">
<h6><i class="fas fa-quote-left"></i> 查询文本</h6>
<p class="mb-0">"${data.query}"</p>
</div>
`;
}
data.results.forEach((result, index) => {
html += '<div class="result-card">';
if (mode === 'text_to_image' || mode === 'image_to_image') {
const imageUrl = result.image_base64 ? `data:image/jpeg;base64,${result.image_base64}` :
(result.image_url || `/temp/${result.filename || result.id}`);
const score = result.score || result.distance ?
(result.score ? (result.score * 100).toFixed(1) : (100 - result.distance * 100).toFixed(1)) : '95.0';
const title = result.title || result.filename || result.id || `结果 ${index + 1}`;
html += `
<div class="row">
<div class="col-md-3">
<img src="${imageUrl}" class="result-image" alt="Result ${index + 1}">
</div>
<div class="col-md-9">
<div class="d-flex justify-content-between align-items-start">
<h6><i class="fas fa-image"></i> ${title}</h6>
<span class="score-badge">相似度: ${score}%</span>
</div>
<p class="text-muted mb-0">类型: 图片 | ID: ${result.id || index}</p>
</div>
</div>
`;
} else {
const text = result.text || result.content || (typeof result === 'string' ? result : JSON.stringify(result));
const score = result.score || result.distance ?
(result.score ? (result.score * 100).toFixed(1) : (100 - result.distance * 100).toFixed(1)) : '95.0';
const title = result.title || `结果 ${index + 1}`;
html += `
<div class="d-flex justify-content-between align-items-start">
<div>
<h6><i class="fas fa-file-text"></i> ${title}</h6>
<p class="mb-0">${text}</p>
<p class="text-muted small mb-0">类型: 文本 | ID: ${result.id || index}</p>
</div>
<span class="score-badge">相似度: ${score}%</span>
</div>
`;
}
html += '</div>';
});
html += '</div>';
resultsContainer.innerHTML = html;
}
// 工具函数
function showLoading(show) {
document.getElementById('loadingSpinner').style.display = show ? 'block' : 'none';
}
function showAlert(type, message) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.main-container .p-4').insertBefore(alertDiv, document.querySelector('.main-container .p-4').firstChild);
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
// 检查系统状态
async function checkStatus() {
try {
const response = await fetch('/api/system_info');
const data = await response.json();
if (data.success) {
systemInitialized = true;
document.getElementById('statusBadge').innerHTML =
'<i class="fas fa-check-circle"></i> 已初始化';
document.getElementById('statusBadge').className = 'badge bg-success';
} else {
document.getElementById('statusBadge').innerHTML =
'<i class="fas fa-exclamation-triangle"></i> 未初始化';
document.getElementById('statusBadge').className = 'badge bg-warning';
}
} catch (error) {
console.log('Status check failed:', error);
document.getElementById('statusBadge').innerHTML =
'<i class="fas fa-times-circle"></i> 连接失败';
document.getElementById('statusBadge').className = 'badge bg-danger';
}
}
// 页面加载时检查状态
checkStatus();
// 设置数据管理功能事件绑定
setupDataManagement();
function setupDataManagement() {
// 批量图片上传事件
const batchImageUpload = document.getElementById('batchImageUpload');
const batchImageFiles = document.getElementById('batchImageFiles');
// 拖拽上传
batchImageUpload.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('dragover');
});
batchImageUpload.addEventListener('dragleave', function(e) {
e.preventDefault();
this.classList.remove('dragover');
});
batchImageUpload.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'));
if (files.length > 0) {
uploadBatchImages(files);
}
});
batchImageFiles.addEventListener('change', function(e) {
if (e.target.files.length > 0) {
uploadBatchImages(Array.from(e.target.files));
}
});
// 批量文本上传
document.getElementById('uploadTextsBtn').addEventListener('click', function() {
const textData = document.getElementById('batchTextInput').value.trim();
if (textData) {
const texts = textData.split('\n').filter(line => line.trim());
if (texts.length > 0) {
uploadBatchTexts(texts);
} else {
showAlert('warning', '请输入有效的文本数据');
}
} else {
showAlert('warning', '请输入文本数据');
}
});
// 从文件导入文本
document.getElementById('textFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('batchTextInput').value = e.target.result;
};
reader.readAsText(file);
}
});
// 移除构建索引按钮的事件监听器
// 查看数据
document.getElementById('viewDataBtn').addEventListener('click', viewData);
// 清空数据
document.getElementById('clearDataBtn').addEventListener('click', clearData);
// 初始化时更新数据统计
updateDataStats();
}
// 批量上传图片
async function uploadBatchImages(files) {
try {
const progressDiv = document.getElementById('imageUploadProgress');
const progressBar = progressDiv.querySelector('.progress-bar');
const progressText = document.getElementById('imageProgressText');
progressDiv.style.display = 'block';
progressText.textContent = `0/${files.length}`;
progressBar.style.width = '0%';
showAlert('info', `正在上传${files.length}张图片...`);
let successCount = 0;
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('image', files[i]);
const response = await fetch('/api/add_image', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
successCount++;
} else {
console.error(`图片 ${files[i].name} 上传失败: ${data.error}`);
}
// 更新进度
const progress = Math.round(((i + 1) / files.length) * 100);
progressBar.style.width = `${progress}%`;
progressText.textContent = `${i + 1}/${files.length}`;
}
showAlert('success', `成功上传 ${successCount}/${files.length} 张图片`);
// 自动保存索引
await autoSaveIndex();
updateDataStats();
} catch (error) {
showAlert('danger', `图片上传失败: ${error.message}`);
} finally {
setTimeout(() => {
document.getElementById('imageUploadProgress').style.display = 'none';
}, 2000);
}
// 旧代码已删除
// 旧代码已删除
}
// 批量上传文本
async function uploadBatchTexts(texts) {
try {
showAlert('info', `正在上传${texts.length}条文本...`);
for (let i = 0; i < texts.length; i++) {
const response = await fetch('/api/add_text', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: texts[i]})
});
const data = await response.json();
if (!data.success) {
throw new Error(`${i+1}条文本上传失败: ${data.error}`);
}
}
showAlert('success', `成功上传${texts.length}条文本`);
// 自动保存索引
await autoSaveIndex();
updateDataStats();
} catch (error) {
showAlert('danger', `文本上传失败: ${error.message}`);
}
// 已替换为新的API调用
// 旧代码已删除
// 已删除
}
// 自动保存索引函数
async function autoSaveIndex() {
try {
const response = await fetch('/api/save_index', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
console.log('索引自动保存成功');
} else {
console.error(`索引自动保存失败: ${data.message}`);
}
} catch (error) {
console.error(`索引自动保存错误: ${error.message}`);
}
}
// 查看数据
async function viewData() {
try {
const response = await fetch('/api/list_items');
const data = await response.json();
if (data.success) {
let content = '<div class="row">';
// 显示图片数据
if (data.items && data.items.filter(item => item.type === 'image').length > 0) {
const imageItems = data.items.filter(item => item.type === 'image');
content += '<div class="col-md-6"><h6>图片数据 (' + imageItems.length + ')</h6>';
content += '<div class="list-group" style="max-height: 300px; overflow-y: auto;">';
imageItems.forEach(item => {
content += `<div class="list-group-item d-flex justify-content-between align-items-center">
<span>${item.id}: ${item.metadata?.title || '无标题'}</span>
<img src="/temp/${item.filename || item.id}" class="img-thumbnail" style="width: 50px; height: 50px; object-fit: cover;">
</div>`;
});
content += '</div></div>';
}
// 显示文本数据
if (data.items && data.items.filter(item => item.type === 'text').length > 0) {
const textItems = data.items.filter(item => item.type === 'text');
content += '<div class="col-md-6"><h6>文本数据 (' + textItems.length + ')</h6>';
content += '<div class="list-group" style="max-height: 300px; overflow-y: auto;">';
textItems.forEach((item, index) => {
const text = item.content || item.text || '';
const shortText = text.length > 50 ? text.substring(0, 50) + '...' : text;
content += `<div class="list-group-item">
<small class="text-muted">#${item.id}</small><br>
${shortText}
</div>`;
});
content += '</div></div>';
}
content += '</div>';
showModal('数据列表', content);
} else {
showAlert('danger', `获取数据失败: ${data.message}`);
}
} catch (error) {
showAlert('danger', `获取数据错误: ${error.message}`);
}
}
// 清空数据
async function clearData() {
if (!confirm('确定要清空所有数据吗?此操作不可恢复!')) {
return;
}
try {
const response = await fetch('/api/clear_index', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showAlert('success', '数据已清空');
updateDataStats();
// 移除构建索引按钮的引用
} else {
showAlert('danger', `清空失败: ${data.message}`);
}
} catch (error) {
showAlert('danger', `清空错误: ${error.message}`);
}
}
// 更新数据统计
async function updateDataStats() {
try {
const response = await fetch('/api/system_info');
const data = await response.json();
if (data.success) {
const retrieval_info = data.retrieval_info || {};
document.getElementById('imageCount').textContent = retrieval_info.image_count || 0;
document.getElementById('textCount').textContent = retrieval_info.text_count || 0;
// 移除构建索引按钮的引用
}
} catch (error) {
console.log('获取数据统计失败:', error);
}
}
// 显示模态框
function showModal(title, content) {
const modalHtml = `
<div class="modal fade" id="dataModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
${content}
</div>
</div>
</div>
</div>
`;
// 移除已存在的模态框
const existingModal = document.getElementById('dataModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('dataModal'));
modal.show();
}
</script>
</body>
</html>