Files
AIRegulation-DocAnalysis/frontend/index.html
2026-04-28 11:29:33 +08:00

1150 lines
37 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>AI+合规智能中枢 - 文档测试平台</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0a0f1a;
--bg-secondary: #121a2e;
--bg-card: #1a2438;
--bg-hover: #243050;
--text-primary: #e8edf5;
--text-secondary: #8a9bb0;
--text-muted: #5a6b80;
--accent-gold: #d4a54a;
--accent-gold-dim: #8a6d2a;
--accent-cyan: #3dd9d0;
--accent-coral: #e85555;
--border-subtle: #2a3a52;
--border-glow: #3dd9d040;
--shadow-deep: 0 8px 32px rgba(0, 0, 0, 0.4);
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 20px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans SC', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
}
/* Background atmosphere */
.bg-layer {
position: fixed;
inset: 0;
z-index: -1;
background:
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(212, 165, 74, 0.08), transparent),
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(61, 217, 208, 0.05), transparent),
linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
}
/* Grid pattern overlay */
.grid-pattern {
position: fixed;
inset: 0;
z-index: -1;
opacity: 0.03;
background-image:
linear-gradient(var(--border-subtle) 1px, transparent 1px),
linear-gradient(90deg, var(--border-subtle) 1px, transparent 1px);
background-size: 60px 60px;
}
/* Header */
.header {
padding: 24px 40px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-subtle);
backdrop-filter: blur(12px);
position: sticky;
top: 0;
z-index: 100;
}
.logo {
display: flex;
align-items: center;
gap: 16px;
}
.logo-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, var(--accent-gold), var(--accent-gold-dim));
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 20px;
color: var(--bg-primary);
}
.logo-text {
font-family: 'JetBrains Mono', monospace;
font-size: 18px;
font-weight: 600;
letter-spacing: 2px;
}
.logo-sub {
font-size: 11px;
color: var(--text-muted);
letter-spacing: 1px;
}
/* Status indicator */
.status-group {
display: flex;
align-items: center;
gap: 24px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
position: relative;
}
.status-dot.online {
background: var(--accent-cyan);
animation: pulse 2s infinite;
}
.status-dot.offline {
background: var(--accent-coral);
}
.status-dot.loading {
background: var(--accent-gold);
animation: blink 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(61, 217, 208, 0.4); }
50% { opacity: 0.8; box-shadow: 0 0 0 8px rgba(61, 217, 208, 0); }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.status-label {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-secondary);
letter-spacing: 1px;
}
/* Main layout */
.main {
max-width: 1200px;
margin: 0 auto;
padding: 40px;
}
/* Section cards */
.section {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: 32px;
margin-bottom: 32px;
position: relative;
overflow: hidden;
}
.section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent-gold), transparent 60%);
}
.section-title {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 600;
color: var(--accent-gold);
letter-spacing: 3px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 12px;
}
.section-title::before {
content: '';
width: 24px;
height: 2px;
background: var(--accent-gold);
}
/* Upload zone */
.upload-zone {
border: 2px dashed var(--border-subtle);
border-radius: var(--radius-md);
padding: 48px;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
background: var(--bg-secondary);
}
.upload-zone:hover,
.upload-zone.dragover {
border-color: var(--accent-gold);
background: var(--bg-hover);
}
.upload-zone.dragover {
transform: scale(1.02);
}
.upload-zone.dragover::after {
content: '';
position: absolute;
inset: 0;
background: rgba(212, 165, 74, 0.1);
border-radius: var(--radius-md);
}
.upload-icon {
width: 64px;
height: 64px;
margin: 0 auto 20px;
opacity: 0.6;
}
.upload-icon svg {
width: 100%;
height: 100%;
stroke: var(--text-secondary);
}
.upload-text {
font-size: 16px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.upload-hint {
font-size: 13px;
color: var(--text-muted);
}
.upload-types {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 24px;
}
.type-tag {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
padding: 6px 12px;
background: var(--bg-primary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
color: var(--text-muted);
}
/* Form fields */
.form-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 24px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-label {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-muted);
letter-spacing: 1px;
}
.form-input {
background: var(--bg-primary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 12px 16px;
color: var(--text-primary);
font-size: 14px;
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--accent-gold);
}
/* Buttons */
.btn {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
font-weight: 600;
padding: 14px 28px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s ease;
letter-spacing: 1px;
display: inline-flex;
align-items: center;
gap: 10px;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-gold), var(--accent-gold-dim));
color: var(--bg-primary);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(212, 165, 74, 0.3);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: var(--bg-primary);
color: var(--text-secondary);
border: 1px solid var(--border-subtle);
}
.btn-secondary:hover {
border-color: var(--text-secondary);
}
/* Pipeline visualization */
.pipeline {
display: flex;
justify-content: center;
gap: 8px;
margin: 32px 0;
opacity: 0;
transition: opacity 0.3s;
}
.pipeline.active {
opacity: 1;
}
.pipeline-step {
display: flex;
align-items: center;
gap: 8px;
}
.step-node {
width: 48px;
height: 48px;
border-radius: var(--radius-sm);
background: var(--bg-primary);
border: 2px solid var(--border-subtle);
display: flex;
align-items: center;
justify-content: center;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-muted);
transition: all 0.3s;
}
.step-node.active {
border-color: var(--accent-gold);
color: var(--accent-gold);
background: rgba(212, 165, 74, 0.1);
}
.step-node.done {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: rgba(61, 217, 208, 0.1);
}
.step-connector {
width: 24px;
height: 2px;
background: var(--border-subtle);
position: relative;
}
.step-connector.active {
background: var(--accent-gold);
}
.step-connector.active::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
background: var(--accent-gold);
border-radius: 50%;
animation: flowRight 1s infinite;
}
@keyframes flowRight {
0% { opacity: 0; transform: translateY(-50%) translateX(-12px); }
50% { opacity: 1; }
100% { opacity: 0; transform: translateY(-50%) translateX(0); }
}
/* Results */
.result-panel {
background: var(--bg-secondary);
border-radius: var(--radius-md);
padding: 24px;
margin-top: 24px;
display: none;
}
.result-panel.show {
display: block;
animation: slideUp 0.4s ease;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.result-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.result-icon {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
}
.result-icon.success {
background: rgba(61, 217, 208, 0.2);
color: var(--accent-cyan);
}
.result-icon.error {
background: rgba(232, 85, 85, 0.2);
color: var(--accent-coral);
}
.result-title {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
}
.result-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.stat-item {
background: var(--bg-primary);
border-radius: var(--radius-sm);
padding: 16px;
}
.stat-label {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--text-muted);
letter-spacing: 1px;
margin-bottom: 6px;
}
.stat-value {
font-family: 'JetBrains Mono', monospace;
font-size: 18px;
color: var(--accent-cyan);
}
/* Search section */
.search-input-group {
display: flex;
gap: 16px;
}
.search-input {
flex: 1;
background: var(--bg-primary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 14px 20px;
color: var(--text-primary);
font-size: 14px;
}
.search-input:focus {
outline: none;
border-color: var(--accent-gold);
}
.search-input::placeholder {
color: var(--text-muted);
}
/* Search results */
.search-results {
margin-top: 24px;
}
.result-card {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 12px;
transition: all 0.2s;
cursor: pointer;
}
.result-card:hover {
border-color: var(--accent-gold);
background: var(--bg-hover);
}
.result-meta {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.score-bar {
width: 80px;
height: 6px;
background: var(--bg-primary);
border-radius: 3px;
overflow: hidden;
}
.score-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-gold), var(--accent-cyan));
border-radius: 3px;
}
.score-value {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--accent-gold);
}
.doc-name {
font-size: 12px;
color: var(--text-muted);
}
.result-content {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.8;
}
.result-footer {
display: flex;
gap: 16px;
margin-top: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-muted);
}
/* Empty state */
.empty-state {
text-align: center;
padding: 48px;
color: var(--text-muted);
}
.empty-icon {
width: 48px;
height: 48px;
margin: 0 auto 16px;
opacity: 0.4;
}
/* File info */
.file-info {
display: none;
background: var(--bg-secondary);
border-radius: var(--radius-md);
padding: 16px 20px;
margin-top: 16px;
align-items: center;
gap: 16px;
}
.file-info.show {
display: flex;
}
.file-icon {
width: 40px;
height: 40px;
background: var(--bg-primary);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
font-weight: 600;
color: var(--accent-gold);
}
.file-details {
flex: 1;
}
.file-name {
font-size: 14px;
color: var(--text-primary);
margin-bottom: 4px;
}
.file-size {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-muted);
}
.file-remove {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
background: transparent;
border: 1px solid var(--border-subtle);
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.file-remove:hover {
border-color: var(--accent-coral);
color: var(--accent-coral);
}
/* Responsive */
@media (max-width: 768px) {
.main {
padding: 20px;
}
.form-row {
grid-template-columns: 1fr;
}
.result-stats {
grid-template-columns: repeat(2, 1fr);
}
.header {
padding: 16px 20px;
}
.logo-text {
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="bg-layer"></div>
<div class="grid-pattern"></div>
<!-- Header -->
<header class="header">
<div class="logo">
<div class="logo-icon">AI</div>
<div>
<div class="logo-text">合规智能中枢</div>
<div class="logo-sub">COMPLIANCE INTELLIGENCE HUB</div>
</div>
</div>
<div class="status-group">
<div class="status-item">
<div class="status-dot" id="apiStatus"></div>
<span class="status-label">API</span>
</div>
<div class="status-item">
<div class="status-dot" id="milvusStatus"></div>
<span class="status-label">MILVUS</span>
</div>
</div>
</header>
<!-- Main content -->
<main class="main">
<!-- Upload section -->
<section class="section">
<div class="section-title">文档上传测试</div>
<div class="upload-zone" id="uploadZone">
<div class="upload-icon">
<svg viewBox="0 0 24 24" fill="none" stroke-width="1.5">
<path d="M12 16V4m0 0L8 8m4-4l4 4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2" stroke-linecap="round"/>
</svg>
</div>
<div class="upload-text">拖拽文件到此处,或点击选择</div>
<div class="upload-hint">支持上传法规文档进行智能解析</div>
<div class="upload-types">
<span class="type-tag">.PDF</span>
<span class="type-tag">.DOCX</span>
<span class="type-tag">.DOC</span>
</div>
<input type="file" id="fileInput" accept=".pdf,.docx,.doc" hidden>
</div>
<div class="file-info" id="fileInfo">
<div class="file-icon" id="fileExt">PDF</div>
<div class="file-details">
<div class="file-name" id="fileName">-</div>
<div class="file-size" id="fileSize">-</div>
</div>
<button class="file-remove" id="fileRemove">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="form-row">
<div class="form-field">
<label class="form-label">文档名称</label>
<input type="text" class="form-input" id="docName" placeholder="可选,默认为文件名">
</div>
<div class="form-field">
<label class="form-label">法规类型</label>
<input type="text" class="form-input" id="regType" placeholder="如:车辆安全、数据安全">
</div>
<div class="form-field">
<label class="form-label">文档版本</label>
<input type="text" class="form-input" id="docVersion" placeholder="如GB/T 12345-2024">
</div>
</div>
<!-- Pipeline visualization -->
<div class="pipeline" id="pipeline">
<div class="pipeline-step">
<div class="step-node" id="step1">解析</div>
<div class="step-connector" id="conn1"></div>
</div>
<div class="pipeline-step">
<div class="step-node" id="step2">分块</div>
<div class="step-connector" id="conn2"></div>
</div>
<div class="pipeline-step">
<div class="step-node" id="step3">嵌入</div>
<div class="step-connector" id="conn3"></div>
</div>
<div class="pipeline-step">
<div class="step-node" id="step4">入库</div>
</div>
</div>
<div style="display: flex; gap: 16px; justify-content: center; margin-top: 24px;">
<button class="btn btn-primary" id="uploadBtn" disabled>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 2L11 13M22 2l-7 20-4-4-4 4-7-20 15 7 7-7z" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
开始处理
</button>
<button class="btn btn-secondary" id="clearBtn">
清除结果
</button>
</div>
<div class="result-panel" id="uploadResult">
<div class="result-header">
<div class="result-icon success" id="resultIcon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="result-title" id="resultTitle">处理成功</div>
</div>
<div class="result-stats">
<div class="stat-item">
<div class="stat-label">DOC ID</div>
<div class="stat-value" id="statDocId">-</div>
</div>
<div class="stat-item">
<div class="stat-label">CHUNKS</div>
<div class="stat-value" id="statChunks">-</div>
</div>
<div class="stat-item">
<div class="stat-label">STATUS</div>
<div class="stat-value" id="statStatus">-</div>
</div>
<div class="stat-item">
<div class="stat-label">TIME</div>
<div class="stat-value" id="statTime">-</div>
</div>
</div>
</div>
</section>
<!-- Search section -->
<section class="section">
<div class="section-title">法规检索测试</div>
<div class="search-input-group">
<input type="text" class="search-input" id="searchQuery" placeholder="输入检索关键词,如:机动车安全标准、数据合规要求...">
<button class="btn btn-primary" id="searchBtn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35" stroke-linecap="round"/>
</svg>
检索
</button>
</div>
<div class="search-results" id="searchResults">
<div class="empty-state" id="searchEmpty">
<div class="empty-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35" stroke-linecap="round"/>
</svg>
</div>
<div>请输入关键词检索法规内容</div>
</div>
</div>
</section>
</main>
<script>
// 动态获取API地址使用当前页面的host
// 前端和API在同一服务器时自动适配
const API_HOST = window.location.hostname;
const API_URL = `http://${API_HOST}:8000`;
// Elements
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
const fileInfo = document.getElementById('fileInfo');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const fileExt = document.getElementById('fileExt');
const fileRemove = document.getElementById('fileRemove');
const uploadBtn = document.getElementById('uploadBtn');
const clearBtn = document.getElementById('clearBtn');
const pipeline = document.getElementById('pipeline');
const uploadResult = document.getElementById('uploadResult');
const searchQuery = document.getElementById('searchQuery');
const searchBtn = document.getElementById('searchBtn');
const searchResults = document.getElementById('searchResults');
const searchEmpty = document.getElementById('searchEmpty');
// Status elements
const apiStatus = document.getElementById('apiStatus');
const milvusStatus = document.getElementById('milvusStatus');
let selectedFile = null;
// Check API health
async function checkHealth() {
apiStatus.className = 'status-dot loading';
try {
const res = await fetch(`${API_URL}/health`);
const data = await res.json();
apiStatus.className = 'status-dot online';
// Check Milvus status from health response
if (data.milvus && data.milvus.connected) {
milvusStatus.className = 'status-dot online';
} else {
milvusStatus.className = 'status-dot offline';
}
} catch (e) {
apiStatus.className = 'status-dot offline';
milvusStatus.className = 'status-dot offline';
}
}
// Initial health check
checkHealth();
setInterval(checkHealth, 30000);
// Upload zone interactions
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
function handleFile(file) {
const ext = file.name.split('.').pop().toUpperCase();
if (!['PDF', 'DOCX', 'DOC'].includes(ext)) {
alert('不支持的文件类型,请上传 PDF、DOCX 或 DOC 文件');
return;
}
selectedFile = file;
fileName.textContent = file.name;
fileSize.textContent = formatSize(file.size);
fileExt.textContent = ext;
fileInfo.classList.add('show');
uploadBtn.disabled = false;
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
fileRemove.addEventListener('click', () => {
selectedFile = null;
fileInput.value = '';
fileInfo.classList.remove('show');
uploadBtn.disabled = true;
});
// Upload document
uploadBtn.addEventListener('click', async () => {
if (!selectedFile) return;
uploadBtn.disabled = true;
uploadResult.classList.remove('show');
pipeline.classList.add('active');
const startTime = Date.now();
// Animate pipeline steps
const steps = ['step1', 'step2', 'step3', 'step4'];
const conns = ['conn1', 'conn2', 'conn3'];
for (let i = 0; i < steps.length; i++) {
document.getElementById(steps[i]).classList.add('active');
if (i < conns.length) {
document.getElementById(conns[i]).classList.add('active');
}
await new Promise(r => setTimeout(r, 500));
}
try {
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('doc_name', document.getElementById('docName').value || selectedFile.name);
formData.append('regulation_type', document.getElementById('regType').value || '');
formData.append('version', document.getElementById('docVersion').value || '');
const res = await fetch(`${API_URL}/api/v1/documents/upload`, {
method: 'POST',
body: formData
});
const data = await res.json();
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
// Mark all steps as done
steps.forEach(id => {
document.getElementById(id).classList.remove('active');
document.getElementById(id).classList.add('done');
});
conns.forEach(id => document.getElementById(id).classList.remove('active'));
if (res.ok && data.status === 'success') {
document.getElementById('resultIcon').className = 'result-icon success';
document.getElementById('resultIcon').innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
document.getElementById('resultTitle').textContent = '处理成功';
document.getElementById('statDocId').textContent = data.doc_id;
document.getElementById('statChunks').textContent = data.num_chunks || 0;
document.getElementById('statStatus').textContent = 'SUCCESS';
document.getElementById('statTime').textContent = elapsed + 's';
} else {
document.getElementById('resultIcon').className = 'result-icon error';
document.getElementById('resultIcon').innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>';
document.getElementById('resultTitle').textContent = '处理失败';
document.getElementById('statDocId').textContent = '-';
document.getElementById('statChunks').textContent = '-';
document.getElementById('statStatus').textContent = 'ERROR';
document.getElementById('statTime').textContent = elapsed + 's';
}
uploadResult.classList.add('show');
} catch (e) {
document.getElementById('resultIcon').className = 'result-icon error';
document.getElementById('resultTitle').textContent = '请求失败: ' + e.message;
uploadResult.classList.add('show');
}
uploadBtn.disabled = false;
});
// Clear results
clearBtn.addEventListener('click', () => {
uploadResult.classList.remove('show');
pipeline.classList.remove('active');
['step1', 'step2', 'step3', 'step4'].forEach(id => {
document.getElementById(id).classList.remove('active', 'done');
});
['conn1', 'conn2', 'conn3'].forEach(id => {
document.getElementById(id).classList.remove('active');
});
});
// Search
searchBtn.addEventListener('click', async () => {
const query = searchQuery.value.trim();
if (!query) {
alert('请输入检索关键词');
return;
}
searchBtn.disabled = true;
searchEmpty.style.display = 'none';
try {
const res = await fetch(`${API_URL}/api/v1/knowledge/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, top_k: 10 })
});
const data = await res.json();
// Clear previous results
searchResults.innerHTML = '';
if (data.results && data.results.length > 0) {
data.results.forEach(item => {
const scorePercent = Math.round(item.score * 100);
const card = document.createElement('div');
card.className = 'result-card';
card.innerHTML = `
<div class="result-meta">
<div class="score-bar">
<div class="score-fill" style="width: ${scorePercent}%"></div>
</div>
<span class="score-value">${scorePercent}%</span>
<span class="doc-name">${item.metadata.doc_name || '-'}</span>
</div>
<div class="result-content">${item.content.substring(0, 300)}${item.content.length > 300 ? '...' : ''}</div>
<div class="result-footer">
<span>ID: ${item.id}</span>
<span>章节: ${item.metadata.section_title || '-'}</span>
<span>条款: ${item.metadata.clause_number || '-'}</span>
<span>页码: ${item.metadata.page_number || '-'}</span>
</div>
`;
searchResults.appendChild(card);
});
} else {
searchResults.innerHTML = `
<div class="empty-state">
<div class="empty-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M9 12h6M12 9v6" stroke-linecap="round"/>
<circle cx="12" cy="12" r="10"/>
</svg>
</div>
<div>未找到相关法规内容</div>
</div>
`;
}
} catch (e) {
searchResults.innerHTML = `
<div class="empty-state">
<div style="color: var(--accent-coral);">检索失败: ${e.message}</div>
</div>
`;
}
searchBtn.disabled = false;
});
// Enter key for search
searchQuery.addEventListener('keypress', (e) => {
if (e.key === 'Enter') searchBtn.click();
});
</script>
</body>
</html>