1150 lines
37 KiB
HTML
1150 lines
37 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>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> |