| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>语义逻辑审查 — 前端测试</title>
- <style>
- * { box-sizing: border-box; margin: 0; padding: 0; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- background: #f5f7fa;
- color: #333;
- line-height: 1.6;
- }
- .container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 20px;
- }
- header {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: #fff;
- padding: 30px 20px;
- border-radius: 12px;
- margin-bottom: 24px;
- box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
- }
- header h1 {
- font-size: 26px;
- font-weight: 600;
- }
- header p {
- opacity: 0.9;
- margin-top: 6px;
- font-size: 14px;
- }
- /* 输入区域 */
- .input-section {
- background: #fff;
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 20px;
- box-shadow: 0 2px 12px rgba(0,0,0,0.06);
- }
- .input-section h2 {
- font-size: 16px;
- margin-bottom: 12px;
- color: #444;
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .input-section h2 .icon { font-size: 18px; }
- textarea {
- width: 100%;
- min-height: 280px;
- padding: 14px;
- border: 1px solid #e0e3e9;
- border-radius: 8px;
- font-size: 14px;
- line-height: 1.7;
- resize: vertical;
- transition: border-color 0.2s;
- font-family: inherit;
- }
- textarea:focus {
- outline: none;
- border-color: #667eea;
- box-shadow: 0 0 0 3px rgba(102,126,234,0.1);
- }
- /* 工具栏 */
- .toolbar {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- margin-top: 14px;
- align-items: center;
- }
- .btn {
- padding: 9px 18px;
- border: none;
- border-radius: 8px;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.2s;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-weight: 500;
- }
- .btn-primary {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: #fff;
- }
- .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }
- .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; }
- .btn-secondary {
- background: #f0f2f5;
- color: #555;
- border: 1px solid #e0e3e9;
- }
- .btn-secondary:hover { background: #e4e7ec; }
- .btn-sample {
- background: #e8f0fe;
- color: #1a73e8;
- border: 1px solid #c6dafc;
- }
- .btn-sample:hover { background: #d2e3fc; }
- .server-status {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 13px;
- color: #888;
- }
- .status-dot {
- width: 8px; height: 8px;
- border-radius: 50%;
- background: #ccc;
- }
- .status-dot.online { background: #34a853; }
- .status-dot.offline { background: #ea4335; }
- /* 加载动画 */
- .loading-overlay {
- display: none;
- position: fixed;
- top: 0; left: 0; right: 0; bottom: 0;
- background: rgba(255,255,255,0.85);
- z-index: 999;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- gap: 16px;
- }
- .loading-overlay.active { display: flex; }
- .spinner {
- width: 48px; height: 48px;
- border: 4px solid #e0e3e9;
- border-top-color: #667eea;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- }
- @keyframes spin { to { transform: rotate(360deg); } }
- .loading-text { color: #667eea; font-size: 15px; font-weight: 500; }
- /* 结果区域 */
- .result-section {
- background: #fff;
- border-radius: 12px;
- padding: 20px;
- box-shadow: 0 2px 12px rgba(0,0,0,0.06);
- }
- .result-section h2 {
- font-size: 16px;
- margin-bottom: 14px;
- color: #444;
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .result-meta {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
- gap: 12px;
- margin-bottom: 16px;
- }
- .meta-card {
- background: #f8f9fb;
- border-radius: 8px;
- padding: 12px 14px;
- border: 1px solid #eaecf0;
- }
- .meta-label { font-size: 12px; color: #888; margin-bottom: 4px; }
- .meta-value { font-size: 15px; font-weight: 600; color: #333; }
- .meta-value.success { color: #34a853; }
- .meta-value.error { color: #ea4335; }
- /* 审查结果内容 */
- .review-content-box {
- background: #f8f9fb;
- border: 1px solid #eaecf0;
- border-radius: 8px;
- padding: 16px;
- margin-bottom: 14px;
- white-space: pre-wrap;
- word-break: break-word;
- font-size: 14px;
- line-height: 1.8;
- max-height: 400px;
- overflow-y: auto;
- }
- .review-content-box::-webkit-scrollbar { width: 6px; }
- .review-content-box::-webkit-scrollbar-thumb { background: #d0d4dc; border-radius: 3px; }
- .result-label {
- font-size: 13px;
- font-weight: 600;
- color: #555;
- margin-bottom: 8px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
- /* JSON 查看器 */
- .json-viewer {
- background: #1e1e2e;
- color: #cdd6f4;
- border-radius: 8px;
- padding: 16px;
- overflow-x: auto;
- font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
- font-size: 13px;
- line-height: 1.6;
- max-height: 400px;
- overflow-y: auto;
- }
- .json-viewer::-webkit-scrollbar { width: 6px; height: 6px; }
- .json-viewer::-webkit-scrollbar-thumb { background: #45475a; border-radius: 3px; }
- .json-key { color: #f38ba8; }
- .json-string { color: #a6e3a1; }
- .json-number { color: #fab387; }
- .json-boolean { color: #89b4fa; }
- .json-null { color: #89b4fa; }
- .placeholder {
- text-align: center;
- color: #999;
- padding: 60px 20px;
- font-size: 14px;
- }
- /* 标签页 */
- .tabs {
- display: flex;
- gap: 4px;
- margin-bottom: 12px;
- border-bottom: 2px solid #f0f2f5;
- }
- .tab {
- padding: 8px 16px;
- font-size: 14px;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- margin-bottom: -2px;
- color: #888;
- transition: color 0.2s;
- }
- .tab:hover { color: #555; }
- .tab.active {
- color: #667eea;
- border-bottom-color: #667eea;
- font-weight: 600;
- }
- .tab-pane { display: none; }
- .tab-pane.active { display: block; }
- .no-result { color: #999; font-style: italic; }
- </style>
- </head>
- <body>
- <div class="container">
- <header>
- <h1>语义逻辑审查 — 前端测试</h1>
- <p>直接调用 SemanticLogicReviewer,无需文件上传流程 | 链路: prompt_loader → format_messages → model_client → ReviewResult</p>
- </header>
- <!-- 输入区域 -->
- <section class="input-section">
- <h2><span class="icon">📝</span> 待审查内容</h2>
- <textarea id="reviewInput" placeholder="在此输入施工方案内容,点击「执行语义逻辑审查」... 示例内容: 本工程计划工期为6个月,施工内容包括路基土石方200万方、桥梁5座、隧道3座。 采用先上后下的施工顺序,首先进行桥梁上部结构施工,然后进行桥墩基础施工。 因为天气晴朗,所以混凝土强度不足。 当温度低于5℃时,可正常进行混凝土浇筑施工。"></textarea>
- <div class="toolbar">
- <button class="btn btn-primary" id="runBtn" onclick="runSemanticLogicCheck()">
- ▶ 执行语义逻辑审查
- </button>
- <button class="btn btn-secondary" onclick="clearInput()">清空</button>
- <button class="btn btn-sample" onclick="loadSample('logic_error')">加载逻辑错误示例</button>
- <button class="btn btn-sample" onclick="loadSample('normal')">加载正常示例</button>
- <div class="server-status">
- <span class="status-dot offline" id="statusDot"></span>
- <span id="statusText">检测中...</span>
- </div>
- </div>
- </section>
- <!-- 结果区域 -->
- <section class="result-section" id="resultSection" style="display:none;">
- <h2><span class="icon">📋</span> 审查结果</h2>
- <div class="result-meta" id="resultMeta"></div>
- <div class="tabs">
- <div class="tab active" onclick="switchTab('response')">模型响应</div>
- <div class="tab" onclick="switchTab('json')">完整JSON</div>
- </div>
- <div class="tab-pane active" id="tab-response">
- <div class="result-label">AI 审查结果</div>
- <div class="review-content-box" id="responseBox"></div>
- </div>
- <div class="tab-pane" id="tab-json">
- <div class="result-label">原始响应 JSON</div>
- <pre class="json-viewer" id="jsonBox"></pre>
- </div>
- </section>
- <section class="result-section" id="placeholderSection">
- <div class="placeholder">
- <div style="font-size:48px; margin-bottom:12px;">🧪</div>
- <p>输入施工方案内容并点击「执行语义逻辑审查」开始测试</p>
- </div>
- </section>
- </div>
- <!-- 加载遮罩 -->
- <div class="loading-overlay" id="loadingOverlay">
- <div class="spinner"></div>
- <div class="loading-text">语义逻辑审查中,请稍候...</div>
- </div>
- <script>
- // ─── 示例数据 ─────────────────────────────────────────────────────────
- const SAMPLES = {
- logic_error: `第二章 施工方案
- 2.1 施工顺序
- 本工程采用先上后下的施工顺序,首先进行桥梁上部结构施工,然后进行桥墩基础施工。
- (逻辑错误:应先施工基础再施工上部结构)
- 2.2 工期与工序矛盾
- 本工程计划工期为6个月,施工内容包括路基土石方200万方、桥梁5座、隧道3座。
- (工期与工程量严重不匹配)
- 2.3 因果错误
- 因为天气晴朗,所以混凝土强度不足。
- (因果无关)
- 2.4 条件结论不匹配
- 当温度低于5℃时,可正常进行混凝土浇筑施工。
- (违背常识——低温不宜浇筑混凝土)
- 2.5 口语化表达
- 这事儿吧,咱就这么干,问题不大。
- (施工方案应为严肃规范的书面表达)`,
- normal: `第一章 工程概况
- 1.1 项目基本信息
- 本工程为四川省会理至禄劝高速公路项目,路线全长约120公里,设计速度80km/h,
- 路基宽度24.5米,双向四车道。主要工程内容包括路基工程、桥梁工程、隧道工程等。
- 1.2 施工范围
- 本标段起讫桩号为K0+000~K30+000,主要包括:
- - 路基土石方工程约200万立方米
- - 桥梁工程5座,总长约2000米
- - 涵洞工程20道
- - 路面工程约30公里
- 1.3 工期安排
- 计划工期:24个月
- 开工日期:2024年3月1日
- 竣工日期:2026年2月28日`,
- };
- // ─── 工具函数 ─────────────────────────────────────────────────────────
- function loadSample(type) {
- document.getElementById('reviewInput').value = SAMPLES[type] || '';
- }
- function clearInput() {
- document.getElementById('reviewInput').value = '';
- }
- function switchTab(name) {
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
- document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
- event.target.classList.add('active');
- document.getElementById('tab-' + name).classList.add('active');
- }
- function setServerStatus(online) {
- const dot = document.getElementById('statusDot');
- const text = document.getElementById('statusText');
- if (online) {
- dot.classList.remove('offline');
- dot.classList.add('online');
- text.textContent = '服务在线';
- } else {
- dot.classList.remove('online');
- dot.classList.add('offline');
- text.textContent = '服务离线';
- }
- }
- // 检测服务器状态
- async function checkHealth() {
- try {
- const res = await fetch('/api/health', { method: 'GET' });
- const data = await res.json();
- setServerStatus(data.status === 'ok');
- } catch (e) {
- setServerStatus(false);
- }
- }
- checkHealth();
- setInterval(checkHealth, 10000);
- // 高亮 JSON
- function highlightJson(json) {
- if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
- return json
- .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
- .replace(/("(?:\\.|[^"\\])*")/g, '<span class="json-string">$1</span>')
- .replace(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>')
- .replace(/\b(null)\b/g, '<span class="json-null">$1</span>')
- .replace(/\b(\d+(?:\.\d+)?)\b/g, '<span class="json-number">$1</span>')
- .replace(/("[^"]*")\s*:/g, '<span class="json-key">$1</span>:');
- }
- // ─── 执行审查 ──────────────────────────────────────────────────────────
- async function runSemanticLogicCheck() {
- const content = document.getElementById('reviewInput').value.trim();
- if (!content) {
- alert('请先输入待审查内容');
- return;
- }
- const btn = document.getElementById('runBtn');
- const overlay = document.getElementById('loadingOverlay');
- btn.disabled = true;
- overlay.classList.add('active');
- const startTime = performance.now();
- try {
- const res = await fetch('/api/semantic_logic', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ content }),
- });
- const data = await res.json();
- const clientTime = ((performance.now() - startTime) / 1000).toFixed(3);
- if (data.error) {
- showError(data.error);
- return;
- }
- showResult(data, clientTime);
- } catch (err) {
- showError('请求失败: ' + err.message);
- } finally {
- btn.disabled = false;
- overlay.classList.remove('active');
- }
- }
- function showResult(data, clientTime) {
- document.getElementById('placeholderSection').style.display = 'none';
- document.getElementById('resultSection').style.display = 'block';
- // 元数据卡片
- const successClass = data.success ? 'success' : 'error';
- const successText = data.success ? '成功' : '失败';
- document.getElementById('resultMeta').innerHTML = `
- <div class="meta-card">
- <div class="meta-label">执行状态</div>
- <div class="meta-value ${successClass}">${successText}</div>
- </div>
- <div class="meta-card">
- <div class="meta-label">模型耗时</div>
- <div class="meta-value">${(data.model_execution_time || 0).toFixed(3)}s</div>
- </div>
- <div class="meta-card">
- <div class="meta-label">端到端耗时</div>
- <div class="meta-value">${clientTime}s</div>
- </div>
- <div class="meta-card">
- <div class="meta-label">内容长度</div>
- <div class="meta-value">${data.content_length} 字符</div>
- </div>
- <div class="meta-card">
- <div class="meta-label">Trace ID</div>
- <div class="meta-value" style="font-size:12px; word-break:break-all;">${data.trace_id}</div>
- </div>
- `;
- // 模型响应内容
- const responseText = data.details?.response || data.error_message || '(空)';
- document.getElementById('responseBox').textContent = responseText;
- // JSON 查看器
- document.getElementById('jsonBox').innerHTML = highlightJson(data);
- }
- function showError(msg) {
- document.getElementById('placeholderSection').style.display = 'none';
- document.getElementById('resultSection').style.display = 'block';
- document.getElementById('resultMeta').innerHTML = `
- <div class="meta-card" style="border-color:#ea4335;">
- <div class="meta-label">执行状态</div>
- <div class="meta-value error">请求失败</div>
- </div>
- `;
- document.getElementById('responseBox').innerHTML = `<span style="color:#ea4335;">❌ ${msg}</span>`;
- document.getElementById('jsonBox').textContent = msg;
- }
- </script>
- </body>
- </html>
|