996 lines
42 KiB
HTML
996 lines
42 KiB
HTML
<!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="请输入文本数据,每行一条文本记录... 例如: 这是第一条文本记录 这是第二条文本记录 这是第三条文本记录"></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>
|