index.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  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">
  6. <title>申勤物业知识助手</title>
  7. <style>
  8. :root {
  9. --bg: #f5f7fb;
  10. --panel: #ffffff;
  11. --border: #d9e1ec;
  12. --primary: #2f5597;
  13. --primary-dark: #24426f;
  14. --text: #22303f;
  15. --muted: #7a8794;
  16. --user-bubble: #e7f0ff;
  17. --assistant-bubble: #ffffff;
  18. --danger: #c0392b;
  19. --warn-bg: #fff7e6;
  20. }
  21. * { box-sizing: border-box; }
  22. body {
  23. margin: 0;
  24. background: var(--bg);
  25. color: var(--text);
  26. font-family: "Microsoft YaHei", "PingFang SC", system-ui, sans-serif;
  27. height: 100vh;
  28. display: flex;
  29. justify-content: center;
  30. align-items: center;
  31. }
  32. .app {
  33. width: min(960px, 96vw);
  34. height: min(900px, 94vh);
  35. background: var(--panel);
  36. border: 1px solid var(--border);
  37. border-radius: 16px;
  38. box-shadow: 0 18px 50px rgba(35, 48, 63, 0.12);
  39. display: flex;
  40. flex-direction: column;
  41. overflow: hidden;
  42. }
  43. header {
  44. padding: 14px 18px;
  45. border-bottom: 1px solid var(--border);
  46. background: #ffffff;
  47. }
  48. header h1 {
  49. margin: 0 0 10px;
  50. font-size: 18px;
  51. color: var(--primary-dark);
  52. }
  53. .controls {
  54. display: flex;
  55. flex-wrap: wrap;
  56. gap: 10px;
  57. align-items: center;
  58. }
  59. .controls .spacer {
  60. flex: 1;
  61. }
  62. label {
  63. display: flex;
  64. flex-direction: column;
  65. gap: 6px;
  66. font-size: 13px;
  67. color: var(--muted);
  68. }
  69. input[type="text"],
  70. input[type="url"],
  71. textarea {
  72. width: 100%;
  73. border: 1px solid var(--border);
  74. border-radius: 9px;
  75. padding: 9px 10px;
  76. font-size: 14px;
  77. font-family: inherit;
  78. resize: vertical;
  79. background: #fff;
  80. }
  81. input:focus,
  82. textarea:focus {
  83. outline: none;
  84. border-color: var(--primary);
  85. box-shadow: 0 0 0 3px rgba(47, 85, 151, 0.12);
  86. }
  87. .check-line {
  88. flex-direction: row;
  89. align-items: center;
  90. gap: 7px;
  91. color: var(--text);
  92. font-size: 13px;
  93. white-space: nowrap;
  94. }
  95. .composer {
  96. display: flex;
  97. gap: 10px;
  98. padding: 12px 16px;
  99. border-top: 1px solid var(--border);
  100. background: #ffffff;
  101. }
  102. .composer textarea {
  103. flex: 1;
  104. min-height: 42px;
  105. max-height: 150px;
  106. }
  107. .btn {
  108. border: 0;
  109. border-radius: 9px;
  110. padding: 10px 14px;
  111. font-size: 14px;
  112. cursor: pointer;
  113. transition: background 0.2s ease;
  114. font-family: inherit;
  115. white-space: nowrap;
  116. }
  117. .btn-primary {
  118. background: var(--primary);
  119. color: #ffffff;
  120. }
  121. .btn-primary:hover { background: var(--primary-dark); }
  122. .btn-primary:disabled { opacity: 0.55; cursor: not-allowed; }
  123. .btn-ghost {
  124. background: #eef2f7;
  125. color: var(--text);
  126. }
  127. .btn-ghost:hover { background: #e0e7f0; }
  128. #chat {
  129. flex: 1;
  130. overflow-y: auto;
  131. padding: 22px;
  132. background: var(--bg);
  133. position: relative;
  134. }
  135. .message-row {
  136. position: relative;
  137. z-index: 2;
  138. }
  139. .message-row {
  140. display: flex;
  141. align-items: flex-start;
  142. gap: 10px;
  143. margin-bottom: 16px;
  144. }
  145. .message-row.user {
  146. flex-direction: row-reverse;
  147. }
  148. .avatar {
  149. width: 34px;
  150. height: 34px;
  151. border-radius: 50%;
  152. object-fit: cover;
  153. flex: none;
  154. border: 1px solid var(--border);
  155. }
  156. .user-avatar {
  157. display: flex;
  158. align-items: center;
  159. justify-content: center;
  160. background: var(--primary);
  161. color: #fff;
  162. font-size: 13px;
  163. }
  164. .message {
  165. max-width: 78%;
  166. padding: 12px 14px;
  167. border-radius: 12px;
  168. line-height: 1.65;
  169. font-size: 14px;
  170. white-space: pre-wrap;
  171. word-break: break-word;
  172. border: 1px solid var(--border);
  173. }
  174. .message.user {
  175. background: var(--user-bubble);
  176. border-color: #c9dcff;
  177. }
  178. .message.assistant {
  179. background: var(--assistant-bubble);
  180. border-color: var(--border);
  181. }
  182. .message.error {
  183. background: #fdecea;
  184. border-color: #f5c6c0;
  185. color: var(--danger);
  186. }
  187. .message.system {
  188. max-width: 100%;
  189. background: #ffffff;
  190. color: var(--muted);
  191. font-size: 13px;
  192. border-style: dashed;
  193. }
  194. #status {
  195. padding: 8px 22px;
  196. min-height: 26px;
  197. border-top: 1px solid var(--border);
  198. color: var(--muted);
  199. font-size: 13px;
  200. background: #ffffff;
  201. }
  202. .progress-flow {
  203. display: flex;
  204. flex-wrap: wrap;
  205. gap: 6px;
  206. padding: 0 0 8px;
  207. background: transparent;
  208. border: none;
  209. }
  210. .answer-text {
  211. min-height: 1em;
  212. }
  213. .step-chip {
  214. display: inline-flex;
  215. align-items: center;
  216. gap: 4px;
  217. border: 1px solid var(--border);
  218. border-radius: 999px;
  219. padding: 4px 9px;
  220. font-size: 12px;
  221. color: var(--muted);
  222. background: #ffffff;
  223. }
  224. .step-chip.active {
  225. border-color: var(--primary);
  226. color: var(--primary-dark);
  227. background: #e7f0ff;
  228. }
  229. .step-chip.done {
  230. opacity: 0.72;
  231. background: #f0f3f7;
  232. }
  233. #confirmPanel {
  234. display: none;
  235. margin: 0 22px 16px;
  236. padding: 14px;
  237. border: 1px solid #f1d18b;
  238. background: var(--warn-bg);
  239. border-radius: 12px;
  240. }
  241. #confirmPanel.show { display: block; }
  242. #confirmMessage {
  243. white-space: pre-wrap;
  244. margin-bottom: 12px;
  245. font-size: 14px;
  246. line-height: 1.6;
  247. }
  248. .option-list {
  249. display: flex;
  250. flex-wrap: wrap;
  251. gap: 8px;
  252. margin-bottom: 12px;
  253. }
  254. .option-chip {
  255. border: 1px solid #d9b76c;
  256. background: #fff8e1;
  257. border-radius: 999px;
  258. padding: 6px 10px;
  259. font-size: 13px;
  260. cursor: pointer;
  261. }
  262. .option-chip:hover { background: #ffedb3; }
  263. .option-chip.selected {
  264. color: #5f4300;
  265. border-color: #b57b00;
  266. background: #ffd86b;
  267. box-shadow: 0 0 0 2px rgba(181, 123, 0, .12);
  268. }
  269. .confirm-actions {
  270. display: flex;
  271. gap: 8px;
  272. align-items: center;
  273. }
  274. .confirm-actions input {
  275. flex: 1;
  276. }
  277. </style>
  278. <link rel="stylesheet" href="data_quality.css?v=20260904">
  279. </head>
  280. <body>
  281. <main class="app">
  282. <header>
  283. <h1>申勤物业知识助手</h1>
  284. <div class="controls">
  285. <label class="check-line" title="开启后系统自动确认查询条件与查询计划;关闭后需要你在页面确认或修改。">
  286. <input id="autoConfirm" type="checkbox" autocomplete="off" />
  287. <span>自动确认</span>
  288. </label>
  289. <label class="check-line" title="开启后优先判断是否可复用同一会话中的历史回答;关闭后每次都重新查询知识图谱。">
  290. <input id="reuseCheck" type="checkbox" checked />
  291. <span>复用历史</span>
  292. </label>
  293. <div class="spacer"></div>
  294. <button id="newSessionBtn" class="btn btn-ghost">新会话</button>
  295. </div>
  296. </header>
  297. <div id="chat" aria-live="polite"></div>
  298. <div id="confirmPanel">
  299. <div id="confirmMessage"></div>
  300. <div id="confirmOptions" class="option-list"></div>
  301. <div class="confirm-actions">
  302. <input id="confirmReply" type="text" placeholder="回复:确认 / 或说明修改" />
  303. <button id="confirmSendBtn" class="btn btn-primary">继续</button>
  304. </div>
  305. </div>
  306. <footer class="composer">
  307. <textarea id="questionInput" rows="1" placeholder="输入问题,Ctrl + Enter 发送"></textarea>
  308. <button id="sendBtn" class="btn btn-primary">发送</button>
  309. </footer>
  310. <div id="status">准备就绪</div>
  311. </main>
  312. <link rel="stylesheet" href="answer_markdown.css?v=20260904-qa">
  313. <script src="vendor/marked.umd.js?v=17.0.5"></script>
  314. <script src="answer_markdown.js?v=20260904-qa"></script>
  315. <link rel="stylesheet" href="answer_cache.css?v=20260907-cache">
  316. <script src="answer_cache.js?v=20260907-cache"></script>
  317. <script>
  318. const $ = (id) => document.getElementById(id);
  319. const chatEl = $("chat");
  320. const statusEl = $("status");
  321. const sendBtn = $("sendBtn");
  322. const questionInput = $("questionInput");
  323. const autoConfirmInput = $("autoConfirm");
  324. const reuseCheckInput = $("reuseCheck");
  325. const confirmPanel = $("confirmPanel");
  326. function resetAutoConfirmDefault() {
  327. autoConfirmInput.checked = false;
  328. }
  329. resetAutoConfirmDefault();
  330. window.addEventListener("pageshow", resetAutoConfirmDefault);
  331. let activeProgressEl = null;
  332. const THREAD_KEY = "kg_thread_id";
  333. function getThreadId() {
  334. let id = sessionStorage.getItem(THREAD_KEY);
  335. if (!id) {
  336. id = typeof crypto !== "undefined" && crypto.randomUUID
  337. ? crypto.randomUUID()
  338. : "thread-" + Date.now() + "-" + Math.random().toString(16).slice(2);
  339. sessionStorage.setItem(THREAD_KEY, id);
  340. }
  341. return id;
  342. }
  343. function newThreadId() {
  344. const id = typeof crypto !== "undefined" && crypto.randomUUID
  345. ? crypto.randomUUID()
  346. : "thread-" + Date.now() + "-" + Math.random().toString(16).slice(2);
  347. sessionStorage.setItem(THREAD_KEY, id);
  348. return id;
  349. }
  350. function apiBase() {
  351. if (location.protocol === "http:" || location.protocol === "https:") {
  352. return location.origin;
  353. }
  354. return "http://127.0.0.1:8000";
  355. }
  356. function setStatus(text) {
  357. statusEl.textContent = text;
  358. }
  359. function addProgressStep(node, label, container) {
  360. const chips = Array.from(container.querySelectorAll(".step-chip"));
  361. chips.forEach((chip) => chip.classList.remove("active"));
  362. chips.forEach((chip) => chip.classList.add("done"));
  363. const chip = document.createElement("span");
  364. chip.className = "step-chip active";
  365. chip.textContent = label || node;
  366. chip.title = node;
  367. container.appendChild(chip);
  368. container.scrollLeft = container.scrollWidth;
  369. }
  370. function finishProgressFlow(container, label) {
  371. const chips = Array.from(container.querySelectorAll(".step-chip"));
  372. chips.forEach((chip) => chip.classList.remove("active"));
  373. chips.forEach((chip) => chip.classList.add("done"));
  374. const chip = document.createElement("span");
  375. chip.className = "step-chip done";
  376. chip.textContent = label || "完成";
  377. container.appendChild(chip);
  378. container.scrollLeft = container.scrollWidth;
  379. }
  380. function appendMessage(role, text) {
  381. const row = document.createElement("div");
  382. row.className = `message-row ${role}`;
  383. if (role === "assistant") {
  384. const img = document.createElement("img");
  385. img.className = "avatar";
  386. img.src = "微信图片_20260817170012_14_2.png";
  387. img.alt = "AI";
  388. row.appendChild(img);
  389. } else if (role === "user") {
  390. const avatar = document.createElement("div");
  391. avatar.className = "avatar user-avatar";
  392. avatar.textContent = "我";
  393. row.appendChild(avatar);
  394. }
  395. const div = document.createElement("div");
  396. div.className = `message ${role}`;
  397. if (role === "assistant") renderAnswer(div, text);
  398. else div.textContent = text || "";
  399. row.appendChild(div);
  400. chatEl.appendChild(row);
  401. chatEl.scrollTop = chatEl.scrollHeight;
  402. return div;
  403. }
  404. function appendSystem(text) {
  405. return appendMessage("system", text);
  406. }
  407. function appendError(text) {
  408. const div = appendMessage("error", text);
  409. setStatus("处理失败");
  410. return div;
  411. }
  412. function hideConfirm() {
  413. confirmPanel.classList.remove("show");
  414. $("confirmReply").value = "";
  415. $("confirmOptions").innerHTML = "";
  416. }
  417. function showConfirm(message, options, confirmType = "") {
  418. $("confirmMessage").textContent = message || "请确认查询条件";
  419. const box = $("confirmOptions");
  420. const replyInput = $("confirmReply");
  421. const multiple = confirmType === "clarify_value";
  422. replyInput.placeholder = multiple ? "可多选候选,或输入多个序号(逗号分隔)" : "回复:确认 / 或说明修改";
  423. const selected = new Set();
  424. box.innerHTML = "";
  425. replyInput.value = "";
  426. (options || []).forEach((opt) => {
  427. const chip = document.createElement("button");
  428. chip.type = "button";
  429. chip.className = "option-chip";
  430. chip.textContent = opt;
  431. chip.setAttribute("aria-pressed", "false");
  432. chip.onclick = () => {
  433. if (!multiple) {
  434. replyInput.value = opt;
  435. return;
  436. }
  437. if (opt === "都不是") {
  438. selected.clear();
  439. box.querySelectorAll(".option-chip").forEach(item => {
  440. item.classList.remove("selected");
  441. item.setAttribute("aria-pressed", "false");
  442. });
  443. selected.add(opt);
  444. } else {
  445. selected.delete("都不是");
  446. const decline = Array.from(box.children).find(item => item.textContent === "都不是");
  447. if (decline) {
  448. decline.classList.remove("selected");
  449. decline.setAttribute("aria-pressed", "false");
  450. }
  451. if (selected.has(opt)) selected.delete(opt);
  452. else selected.add(opt);
  453. }
  454. chip.classList.toggle("selected", selected.has(opt));
  455. chip.setAttribute("aria-pressed", selected.has(opt) ? "true" : "false");
  456. replyInput.value = Array.from(selected).join(",");
  457. };
  458. box.appendChild(chip);
  459. });
  460. confirmPanel.classList.add("show");
  461. replyInput.focus();
  462. setStatus("等待人工确认");
  463. confirmPanel.scrollIntoView({ behavior: "smooth", block: "center" });
  464. }
  465. function parseSseLine(line) {
  466. if (line.startsWith("event: ")) return ["event", line.slice(7).trim()];
  467. if (line.startsWith("data: ")) return ["data", line.slice(6)];
  468. return null;
  469. }
  470. async function askStream(question, autoConfirm, reuseCheck) {
  471. const threadId = getThreadId();
  472. const controller = new AbortController();
  473. const response = await fetch(`${apiBase()}/ask/stream`, {
  474. method: "POST",
  475. headers: { "Content-Type": "application/json" },
  476. body: JSON.stringify({
  477. thread_id: threadId,
  478. query: question,
  479. auto_confirm: autoConfirm,
  480. reuse_check: reuseCheck,
  481. }),
  482. signal: controller.signal,
  483. });
  484. if (!response.ok || !response.body) {
  485. throw new Error(`HTTP ${response.status}`);
  486. }
  487. const reader = response.body.getReader();
  488. const decoder = new TextDecoder();
  489. let buffer = "";
  490. let event = "";
  491. let data = "";
  492. const assistantRow = document.createElement("div");
  493. assistantRow.className = "message-row assistant";
  494. const avatar = document.createElement("img");
  495. avatar.className = "avatar";
  496. avatar.src = "微信图片_20260817170012_14_2.png";
  497. avatar.alt = "AI";
  498. assistantRow.appendChild(avatar);
  499. const assistantBubble = document.createElement("div");
  500. assistantBubble.className = "message assistant";
  501. const progressEl = document.createElement("div");
  502. progressEl.className = "progress-flow";
  503. activeProgressEl = progressEl;
  504. const answerEl = document.createElement("div");
  505. answerEl.className = "answer-text";
  506. assistantBubble.appendChild(progressEl);
  507. assistantBubble.appendChild(answerEl);
  508. assistantRow.appendChild(assistantBubble);
  509. chatEl.appendChild(assistantRow);
  510. chatEl.scrollTop = chatEl.scrollHeight;
  511. let gotAnswer = false;
  512. let answerText = "";
  513. const handleEvent = () => {
  514. if (!event) return;
  515. if (event === "progress") {
  516. try {
  517. const payload = JSON.parse(data);
  518. addProgressStep(payload.node, payload.label, progressEl);
  519. setStatus(payload.label || "处理中");
  520. } catch (_) {}
  521. } else if (event === "answer_chunk") {
  522. try {
  523. const payload = JSON.parse(data);
  524. if (!gotAnswer) {
  525. addProgressStep("answer", "正在生成回答", progressEl);
  526. setStatus("正在生成回答");
  527. }
  528. answerText += payload.text || "";
  529. renderAnswer(answerEl, answerText);
  530. chatEl.scrollTop = chatEl.scrollHeight;
  531. gotAnswer = true;
  532. } catch (_) {}
  533. } else if (event === "confirm") {
  534. try {
  535. const payload = JSON.parse(data);
  536. addProgressStep("confirm", "等待人工确认", progressEl);
  537. showConfirm(payload.message, payload.options, payload.confirm_type || payload.type);
  538. } catch (_) {}
  539. } else if (event === "done") {
  540. try {
  541. const payload = JSON.parse(data);
  542. if (typeof payload.answer === "string") answerText = payload.answer;
  543. renderAnswer(answerEl, answerText);
  544. AnswerCacheUI.attach(assistantBubble, payload, threadId, apiBase());
  545. finishProgressFlow(progressEl, "完成");
  546. setStatus(`完成,耗时 ${payload.elapsed_sec || 0} 秒`);
  547. } catch (_) {}
  548. } else if (event === "error") {
  549. try {
  550. const payload = JSON.parse(data);
  551. assistantBubble.className = "message error";
  552. progressEl.style.display = "none";
  553. answerEl.textContent = payload.message || "请求失败";
  554. finishProgressFlow(progressEl, "失败");
  555. progressEl.style.display = "flex";
  556. setStatus("处理失败");
  557. } catch (_) {}
  558. }
  559. event = "";
  560. data = "";
  561. };
  562. while (true) {
  563. const { value, done } = await reader.read();
  564. if (done) break;
  565. buffer += decoder.decode(value, { stream: true });
  566. let idx;
  567. while ((idx = buffer.indexOf("\n\n")) >= 0) {
  568. const raw = buffer.slice(0, idx);
  569. buffer = buffer.slice(idx + 2);
  570. const lines = raw.split("\n");
  571. event = "";
  572. data = "";
  573. for (const line of lines) {
  574. const parsed = parseSseLine(line);
  575. if (!parsed) continue;
  576. if (parsed[0] === "event") event = parsed[1];
  577. else if (parsed[0] === "data") data = parsed[1];
  578. }
  579. handleEvent();
  580. }
  581. }
  582. // 处理最后可能没有结尾空行的数据
  583. if (buffer.trim()) {
  584. const parsed = parseSseLine(buffer.trim());
  585. if (parsed) {
  586. if (parsed[0] === "event") event = parsed[1];
  587. else if (parsed[0] === "data") data = parsed[1];
  588. handleEvent();
  589. }
  590. }
  591. return { threadId };
  592. }
  593. async function resumeThread(reply) {
  594. const threadId = getThreadId();
  595. const res = await fetch(`${apiBase()}/threads/${encodeURIComponent(threadId)}/resume`, {
  596. method: "POST",
  597. headers: { "Content-Type": "application/json" },
  598. body: JSON.stringify({ reply }),
  599. });
  600. const payload = await res.json();
  601. if (payload.status === "need_confirm") {
  602. if (activeProgressEl) addProgressStep("confirm", "等待人工确认", activeProgressEl);
  603. showConfirm(payload.message, payload.options, payload.confirm_type || payload.type);
  604. } else if (payload.status === "ok") {
  605. hideConfirm();
  606. if (activeProgressEl) finishProgressFlow(activeProgressEl, "完成");
  607. const answerBubble = appendMessage("assistant", payload.answer || "");
  608. AnswerCacheUI.attach(answerBubble, payload, threadId, apiBase());
  609. setStatus(`完成,耗时 ${payload.elapsed_sec || 0} 秒`);
  610. } else {
  611. hideConfirm();
  612. if (activeProgressEl) finishProgressFlow(activeProgressEl, "失败");
  613. appendError(payload.message || "恢复会话失败");
  614. }
  615. }
  616. async function sendQuestion() {
  617. const question = questionInput.value.trim();
  618. if (!question) return;
  619. appendMessage("user", question);
  620. questionInput.value = "";
  621. hideConfirm();
  622. sendBtn.disabled = true;
  623. setStatus("正在发送...");
  624. try {
  625. await askStream(question, autoConfirmInput.checked, reuseCheckInput.checked);
  626. } catch (err) {
  627. appendError(err.message || String(err));
  628. } finally {
  629. sendBtn.disabled = false;
  630. questionInput.focus();
  631. }
  632. }
  633. sendBtn.addEventListener("click", sendQuestion);
  634. questionInput.addEventListener("keydown", (e) => {
  635. if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
  636. e.preventDefault();
  637. sendQuestion();
  638. }
  639. });
  640. $("confirmSendBtn").addEventListener("click", async () => {
  641. const btn = $("confirmSendBtn");
  642. btn.disabled = true;
  643. btn.textContent = "处理中...";
  644. const reply = $("confirmReply").value.trim() || "确认";
  645. setStatus("正在恢复会话...");
  646. try {
  647. await resumeThread(reply);
  648. } catch (err) {
  649. appendError(err.message || String(err));
  650. } finally {
  651. btn.disabled = false;
  652. btn.textContent = "继续";
  653. }
  654. });
  655. $("newSessionBtn").addEventListener("click", () => {
  656. newThreadId();
  657. chatEl.innerHTML = "";
  658. hideConfirm();
  659. setStatus(`新会话已创建:${getThreadId()}`);
  660. appendSystem("已创建新会话。当前 thread_id 会在本浏览器标签页内保持不变。");
  661. });
  662. appendSystem(`当前会话 thread_id:${getThreadId()}`);
  663. </script>
  664. <script src="data_quality.js?v=20260904"></script>
  665. </body>
  666. </html>