index.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>语义逻辑审查 — 前端测试</title>
  7. <style>
  8. * { box-sizing: border-box; margin: 0; padding: 0; }
  9. body {
  10. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  11. background: #f5f7fa;
  12. color: #333;
  13. line-height: 1.6;
  14. }
  15. .container {
  16. max-width: 1200px;
  17. margin: 0 auto;
  18. padding: 20px;
  19. }
  20. header {
  21. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  22. color: #fff;
  23. padding: 30px 20px;
  24. border-radius: 12px;
  25. margin-bottom: 24px;
  26. box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
  27. }
  28. header h1 {
  29. font-size: 26px;
  30. font-weight: 600;
  31. }
  32. header p {
  33. opacity: 0.9;
  34. margin-top: 6px;
  35. font-size: 14px;
  36. }
  37. /* 输入区域 */
  38. .input-section {
  39. background: #fff;
  40. border-radius: 12px;
  41. padding: 20px;
  42. margin-bottom: 20px;
  43. box-shadow: 0 2px 12px rgba(0,0,0,0.06);
  44. }
  45. .input-section h2 {
  46. font-size: 16px;
  47. margin-bottom: 12px;
  48. color: #444;
  49. display: flex;
  50. align-items: center;
  51. gap: 6px;
  52. }
  53. .input-section h2 .icon { font-size: 18px; }
  54. textarea {
  55. width: 100%;
  56. min-height: 280px;
  57. padding: 14px;
  58. border: 1px solid #e0e3e9;
  59. border-radius: 8px;
  60. font-size: 14px;
  61. line-height: 1.7;
  62. resize: vertical;
  63. transition: border-color 0.2s;
  64. font-family: inherit;
  65. }
  66. textarea:focus {
  67. outline: none;
  68. border-color: #667eea;
  69. box-shadow: 0 0 0 3px rgba(102,126,234,0.1);
  70. }
  71. /* 工具栏 */
  72. .toolbar {
  73. display: flex;
  74. flex-wrap: wrap;
  75. gap: 10px;
  76. margin-top: 14px;
  77. align-items: center;
  78. }
  79. .btn {
  80. padding: 9px 18px;
  81. border: none;
  82. border-radius: 8px;
  83. font-size: 14px;
  84. cursor: pointer;
  85. transition: all 0.2s;
  86. display: inline-flex;
  87. align-items: center;
  88. gap: 6px;
  89. font-weight: 500;
  90. }
  91. .btn-primary {
  92. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  93. color: #fff;
  94. }
  95. .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }
  96. .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; }
  97. .btn-secondary {
  98. background: #f0f2f5;
  99. color: #555;
  100. border: 1px solid #e0e3e9;
  101. }
  102. .btn-secondary:hover { background: #e4e7ec; }
  103. .btn-sample {
  104. background: #e8f0fe;
  105. color: #1a73e8;
  106. border: 1px solid #c6dafc;
  107. }
  108. .btn-sample:hover { background: #d2e3fc; }
  109. .server-status {
  110. margin-left: auto;
  111. display: flex;
  112. align-items: center;
  113. gap: 6px;
  114. font-size: 13px;
  115. color: #888;
  116. }
  117. .status-dot {
  118. width: 8px; height: 8px;
  119. border-radius: 50%;
  120. background: #ccc;
  121. }
  122. .status-dot.online { background: #34a853; }
  123. .status-dot.offline { background: #ea4335; }
  124. /* 加载动画 */
  125. .loading-overlay {
  126. display: none;
  127. position: fixed;
  128. top: 0; left: 0; right: 0; bottom: 0;
  129. background: rgba(255,255,255,0.85);
  130. z-index: 999;
  131. justify-content: center;
  132. align-items: center;
  133. flex-direction: column;
  134. gap: 16px;
  135. }
  136. .loading-overlay.active { display: flex; }
  137. .spinner {
  138. width: 48px; height: 48px;
  139. border: 4px solid #e0e3e9;
  140. border-top-color: #667eea;
  141. border-radius: 50%;
  142. animation: spin 1s linear infinite;
  143. }
  144. @keyframes spin { to { transform: rotate(360deg); } }
  145. .loading-text { color: #667eea; font-size: 15px; font-weight: 500; }
  146. /* 结果区域 */
  147. .result-section {
  148. background: #fff;
  149. border-radius: 12px;
  150. padding: 20px;
  151. box-shadow: 0 2px 12px rgba(0,0,0,0.06);
  152. }
  153. .result-section h2 {
  154. font-size: 16px;
  155. margin-bottom: 14px;
  156. color: #444;
  157. display: flex;
  158. align-items: center;
  159. gap: 6px;
  160. }
  161. .result-meta {
  162. display: grid;
  163. grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  164. gap: 12px;
  165. margin-bottom: 16px;
  166. }
  167. .meta-card {
  168. background: #f8f9fb;
  169. border-radius: 8px;
  170. padding: 12px 14px;
  171. border: 1px solid #eaecf0;
  172. }
  173. .meta-label { font-size: 12px; color: #888; margin-bottom: 4px; }
  174. .meta-value { font-size: 15px; font-weight: 600; color: #333; }
  175. .meta-value.success { color: #34a853; }
  176. .meta-value.error { color: #ea4335; }
  177. /* 审查结果内容 */
  178. .review-content-box {
  179. background: #f8f9fb;
  180. border: 1px solid #eaecf0;
  181. border-radius: 8px;
  182. padding: 16px;
  183. margin-bottom: 14px;
  184. white-space: pre-wrap;
  185. word-break: break-word;
  186. font-size: 14px;
  187. line-height: 1.8;
  188. max-height: 400px;
  189. overflow-y: auto;
  190. }
  191. .review-content-box::-webkit-scrollbar { width: 6px; }
  192. .review-content-box::-webkit-scrollbar-thumb { background: #d0d4dc; border-radius: 3px; }
  193. .result-label {
  194. font-size: 13px;
  195. font-weight: 600;
  196. color: #555;
  197. margin-bottom: 8px;
  198. text-transform: uppercase;
  199. letter-spacing: 0.5px;
  200. }
  201. /* JSON 查看器 */
  202. .json-viewer {
  203. background: #1e1e2e;
  204. color: #cdd6f4;
  205. border-radius: 8px;
  206. padding: 16px;
  207. overflow-x: auto;
  208. font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
  209. font-size: 13px;
  210. line-height: 1.6;
  211. max-height: 400px;
  212. overflow-y: auto;
  213. }
  214. .json-viewer::-webkit-scrollbar { width: 6px; height: 6px; }
  215. .json-viewer::-webkit-scrollbar-thumb { background: #45475a; border-radius: 3px; }
  216. .json-key { color: #f38ba8; }
  217. .json-string { color: #a6e3a1; }
  218. .json-number { color: #fab387; }
  219. .json-boolean { color: #89b4fa; }
  220. .json-null { color: #89b4fa; }
  221. .placeholder {
  222. text-align: center;
  223. color: #999;
  224. padding: 60px 20px;
  225. font-size: 14px;
  226. }
  227. /* 标签页 */
  228. .tabs {
  229. display: flex;
  230. gap: 4px;
  231. margin-bottom: 12px;
  232. border-bottom: 2px solid #f0f2f5;
  233. }
  234. .tab {
  235. padding: 8px 16px;
  236. font-size: 14px;
  237. cursor: pointer;
  238. border-bottom: 2px solid transparent;
  239. margin-bottom: -2px;
  240. color: #888;
  241. transition: color 0.2s;
  242. }
  243. .tab:hover { color: #555; }
  244. .tab.active {
  245. color: #667eea;
  246. border-bottom-color: #667eea;
  247. font-weight: 600;
  248. }
  249. .tab-pane { display: none; }
  250. .tab-pane.active { display: block; }
  251. .no-result { color: #999; font-style: italic; }
  252. </style>
  253. </head>
  254. <body>
  255. <div class="container">
  256. <header>
  257. <h1>语义逻辑审查 — 前端测试</h1>
  258. <p>直接调用 SemanticLogicReviewer,无需文件上传流程 | 链路: prompt_loader → format_messages → model_client → ReviewResult</p>
  259. </header>
  260. <!-- 输入区域 -->
  261. <section class="input-section">
  262. <h2><span class="icon">📝</span> 待审查内容</h2>
  263. <textarea id="reviewInput" placeholder="在此输入施工方案内容,点击「执行语义逻辑审查」...&#10;&#10;示例内容:&#10;本工程计划工期为6个月,施工内容包括路基土石方200万方、桥梁5座、隧道3座。&#10;采用先上后下的施工顺序,首先进行桥梁上部结构施工,然后进行桥墩基础施工。&#10;因为天气晴朗,所以混凝土强度不足。&#10;当温度低于5℃时,可正常进行混凝土浇筑施工。"></textarea>
  264. <div class="toolbar">
  265. <button class="btn btn-primary" id="runBtn" onclick="runSemanticLogicCheck()">
  266. ▶ 执行语义逻辑审查
  267. </button>
  268. <button class="btn btn-secondary" onclick="clearInput()">清空</button>
  269. <button class="btn btn-sample" onclick="loadSample('logic_error')">加载逻辑错误示例</button>
  270. <button class="btn btn-sample" onclick="loadSample('normal')">加载正常示例</button>
  271. <div class="server-status">
  272. <span class="status-dot offline" id="statusDot"></span>
  273. <span id="statusText">检测中...</span>
  274. </div>
  275. </div>
  276. </section>
  277. <!-- 结果区域 -->
  278. <section class="result-section" id="resultSection" style="display:none;">
  279. <h2><span class="icon">📋</span> 审查结果</h2>
  280. <div class="result-meta" id="resultMeta"></div>
  281. <div class="tabs">
  282. <div class="tab active" onclick="switchTab('response')">模型响应</div>
  283. <div class="tab" onclick="switchTab('json')">完整JSON</div>
  284. </div>
  285. <div class="tab-pane active" id="tab-response">
  286. <div class="result-label">AI 审查结果</div>
  287. <div class="review-content-box" id="responseBox"></div>
  288. </div>
  289. <div class="tab-pane" id="tab-json">
  290. <div class="result-label">原始响应 JSON</div>
  291. <pre class="json-viewer" id="jsonBox"></pre>
  292. </div>
  293. </section>
  294. <section class="result-section" id="placeholderSection">
  295. <div class="placeholder">
  296. <div style="font-size:48px; margin-bottom:12px;">🧪</div>
  297. <p>输入施工方案内容并点击「执行语义逻辑审查」开始测试</p>
  298. </div>
  299. </section>
  300. </div>
  301. <!-- 加载遮罩 -->
  302. <div class="loading-overlay" id="loadingOverlay">
  303. <div class="spinner"></div>
  304. <div class="loading-text">语义逻辑审查中,请稍候...</div>
  305. </div>
  306. <script>
  307. // ─── 示例数据 ─────────────────────────────────────────────────────────
  308. const SAMPLES = {
  309. logic_error: `第二章 施工方案
  310. 2.1 施工顺序
  311. 本工程采用先上后下的施工顺序,首先进行桥梁上部结构施工,然后进行桥墩基础施工。
  312. (逻辑错误:应先施工基础再施工上部结构)
  313. 2.2 工期与工序矛盾
  314. 本工程计划工期为6个月,施工内容包括路基土石方200万方、桥梁5座、隧道3座。
  315. (工期与工程量严重不匹配)
  316. 2.3 因果错误
  317. 因为天气晴朗,所以混凝土强度不足。
  318. (因果无关)
  319. 2.4 条件结论不匹配
  320. 当温度低于5℃时,可正常进行混凝土浇筑施工。
  321. (违背常识——低温不宜浇筑混凝土)
  322. 2.5 口语化表达
  323. 这事儿吧,咱就这么干,问题不大。
  324. (施工方案应为严肃规范的书面表达)`,
  325. normal: `第一章 工程概况
  326. 1.1 项目基本信息
  327. 本工程为四川省会理至禄劝高速公路项目,路线全长约120公里,设计速度80km/h,
  328. 路基宽度24.5米,双向四车道。主要工程内容包括路基工程、桥梁工程、隧道工程等。
  329. 1.2 施工范围
  330. 本标段起讫桩号为K0+000~K30+000,主要包括:
  331. - 路基土石方工程约200万立方米
  332. - 桥梁工程5座,总长约2000米
  333. - 涵洞工程20道
  334. - 路面工程约30公里
  335. 1.3 工期安排
  336. 计划工期:24个月
  337. 开工日期:2024年3月1日
  338. 竣工日期:2026年2月28日`,
  339. };
  340. // ─── 工具函数 ─────────────────────────────────────────────────────────
  341. function loadSample(type) {
  342. document.getElementById('reviewInput').value = SAMPLES[type] || '';
  343. }
  344. function clearInput() {
  345. document.getElementById('reviewInput').value = '';
  346. }
  347. function switchTab(name) {
  348. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  349. document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
  350. event.target.classList.add('active');
  351. document.getElementById('tab-' + name).classList.add('active');
  352. }
  353. function setServerStatus(online) {
  354. const dot = document.getElementById('statusDot');
  355. const text = document.getElementById('statusText');
  356. if (online) {
  357. dot.classList.remove('offline');
  358. dot.classList.add('online');
  359. text.textContent = '服务在线';
  360. } else {
  361. dot.classList.remove('online');
  362. dot.classList.add('offline');
  363. text.textContent = '服务离线';
  364. }
  365. }
  366. // 检测服务器状态
  367. async function checkHealth() {
  368. try {
  369. const res = await fetch('/api/health', { method: 'GET' });
  370. const data = await res.json();
  371. setServerStatus(data.status === 'ok');
  372. } catch (e) {
  373. setServerStatus(false);
  374. }
  375. }
  376. checkHealth();
  377. setInterval(checkHealth, 10000);
  378. // 高亮 JSON
  379. function highlightJson(json) {
  380. if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
  381. return json
  382. .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  383. .replace(/("(?:\\.|[^"\\])*")/g, '<span class="json-string">$1</span>')
  384. .replace(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>')
  385. .replace(/\b(null)\b/g, '<span class="json-null">$1</span>')
  386. .replace(/\b(\d+(?:\.\d+)?)\b/g, '<span class="json-number">$1</span>')
  387. .replace(/("[^"]*")\s*:/g, '<span class="json-key">$1</span>:');
  388. }
  389. // ─── 执行审查 ──────────────────────────────────────────────────────────
  390. async function runSemanticLogicCheck() {
  391. const content = document.getElementById('reviewInput').value.trim();
  392. if (!content) {
  393. alert('请先输入待审查内容');
  394. return;
  395. }
  396. const btn = document.getElementById('runBtn');
  397. const overlay = document.getElementById('loadingOverlay');
  398. btn.disabled = true;
  399. overlay.classList.add('active');
  400. const startTime = performance.now();
  401. try {
  402. const res = await fetch('/api/semantic_logic', {
  403. method: 'POST',
  404. headers: { 'Content-Type': 'application/json' },
  405. body: JSON.stringify({ content }),
  406. });
  407. const data = await res.json();
  408. const clientTime = ((performance.now() - startTime) / 1000).toFixed(3);
  409. if (data.error) {
  410. showError(data.error);
  411. return;
  412. }
  413. showResult(data, clientTime);
  414. } catch (err) {
  415. showError('请求失败: ' + err.message);
  416. } finally {
  417. btn.disabled = false;
  418. overlay.classList.remove('active');
  419. }
  420. }
  421. function showResult(data, clientTime) {
  422. document.getElementById('placeholderSection').style.display = 'none';
  423. document.getElementById('resultSection').style.display = 'block';
  424. // 元数据卡片
  425. const successClass = data.success ? 'success' : 'error';
  426. const successText = data.success ? '成功' : '失败';
  427. document.getElementById('resultMeta').innerHTML = `
  428. <div class="meta-card">
  429. <div class="meta-label">执行状态</div>
  430. <div class="meta-value ${successClass}">${successText}</div>
  431. </div>
  432. <div class="meta-card">
  433. <div class="meta-label">模型耗时</div>
  434. <div class="meta-value">${(data.model_execution_time || 0).toFixed(3)}s</div>
  435. </div>
  436. <div class="meta-card">
  437. <div class="meta-label">端到端耗时</div>
  438. <div class="meta-value">${clientTime}s</div>
  439. </div>
  440. <div class="meta-card">
  441. <div class="meta-label">内容长度</div>
  442. <div class="meta-value">${data.content_length} 字符</div>
  443. </div>
  444. <div class="meta-card">
  445. <div class="meta-label">Trace ID</div>
  446. <div class="meta-value" style="font-size:12px; word-break:break-all;">${data.trace_id}</div>
  447. </div>
  448. `;
  449. // 模型响应内容
  450. const responseText = data.details?.response || data.error_message || '(空)';
  451. document.getElementById('responseBox').textContent = responseText;
  452. // JSON 查看器
  453. document.getElementById('jsonBox').innerHTML = highlightJson(data);
  454. }
  455. function showError(msg) {
  456. document.getElementById('placeholderSection').style.display = 'none';
  457. document.getElementById('resultSection').style.display = 'block';
  458. document.getElementById('resultMeta').innerHTML = `
  459. <div class="meta-card" style="border-color:#ea4335;">
  460. <div class="meta-label">执行状态</div>
  461. <div class="meta-value error">请求失败</div>
  462. </div>
  463. `;
  464. document.getElementById('responseBox').innerHTML = `<span style="color:#ea4335;">❌ ${msg}</span>`;
  465. document.getElementById('jsonBox').textContent = msg;
  466. }
  467. </script>
  468. </body>
  469. </html>