index.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  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. .answer-timing { margin-top: 10px; font-size: 12px; color: #64748b; }
  195. #status {
  196. padding: 8px 22px;
  197. min-height: 26px;
  198. border-top: 1px solid var(--border);
  199. color: var(--muted);
  200. font-size: 13px;
  201. background: #ffffff;
  202. }
  203. .progress-flow {
  204. display: flex;
  205. flex-wrap: wrap;
  206. gap: 6px;
  207. padding: 0 0 8px;
  208. background: transparent;
  209. border: none;
  210. }
  211. .answer-text {
  212. min-height: 1em;
  213. }
  214. .step-chip {
  215. display: inline-flex;
  216. align-items: center;
  217. gap: 4px;
  218. border: 1px solid var(--border);
  219. border-radius: 999px;
  220. padding: 4px 9px;
  221. font-size: 12px;
  222. color: var(--muted);
  223. background: #ffffff;
  224. }
  225. .step-chip.active {
  226. border-color: var(--primary);
  227. color: var(--primary-dark);
  228. background: #e7f0ff;
  229. }
  230. .step-chip.done {
  231. opacity: 0.72;
  232. background: #f0f3f7;
  233. }
  234. #confirmPanel {
  235. display: none;
  236. margin: 0 22px 16px;
  237. padding: 14px;
  238. border: 1px solid #f1d18b;
  239. background: var(--warn-bg);
  240. border-radius: 12px;
  241. }
  242. #confirmPanel.show { display: block; }
  243. #confirmMessage {
  244. white-space: pre-wrap;
  245. margin-bottom: 12px;
  246. font-size: 14px;
  247. line-height: 1.6;
  248. }
  249. .option-list {
  250. display: flex;
  251. flex-wrap: wrap;
  252. gap: 8px;
  253. margin-bottom: 12px;
  254. }
  255. .option-chip {
  256. border: 1px solid #d9b76c;
  257. background: #fff8e1;
  258. border-radius: 999px;
  259. padding: 6px 10px;
  260. font-size: 13px;
  261. cursor: pointer;
  262. }
  263. .option-chip:hover { background: #ffedb3; }
  264. .option-chip.selected {
  265. color: #5f4300;
  266. border-color: #b57b00;
  267. background: #ffd86b;
  268. box-shadow: 0 0 0 2px rgba(181, 123, 0, .12);
  269. }
  270. .confirm-actions {
  271. display: flex;
  272. gap: 8px;
  273. align-items: center;
  274. }
  275. .confirm-actions input {
  276. flex: 1;
  277. }
  278. </style>
  279. <link rel="stylesheet" href="data_quality.css?v=20260904">
  280. </head>
  281. <body>
  282. <main class="app">
  283. <header>
  284. <h1>申勤物业知识助手</h1>
  285. <div class="controls">
  286. <label class="check-line" title="开启后系统自动确认查询条件与查询计划;关闭后需要你在页面确认或修改。">
  287. <input id="autoConfirm" type="checkbox" autocomplete="off" />
  288. <span>自动确认</span>
  289. </label>
  290. <label class="check-line" title="开启后优先判断是否可复用同一会话中的历史回答;关闭后每次都重新查询知识图谱。">
  291. <input id="reuseCheck" type="checkbox" checked />
  292. <span>复用历史</span>
  293. </label>
  294. <div class="spacer"></div>
  295. <button id="newSessionBtn" class="btn btn-ghost">新会话</button>
  296. </div>
  297. </header>
  298. <div id="chat" aria-live="polite"></div>
  299. <div id="confirmPanel">
  300. <div id="confirmMessage"></div>
  301. <div id="confirmOptions" class="option-list"></div>
  302. <div class="confirm-actions">
  303. <input id="confirmReply" type="text" placeholder="回复:确认 / 或说明修改" />
  304. <button id="confirmSendBtn" class="btn btn-primary">继续</button>
  305. </div>
  306. </div>
  307. <footer class="composer">
  308. <textarea id="questionInput" rows="1" placeholder="输入问题,Ctrl + Enter 发送"></textarea>
  309. <button id="sendBtn" class="btn btn-primary">发送</button>
  310. </footer>
  311. <div id="status">准备就绪</div>
  312. </main>
  313. <link rel="stylesheet" href="answer_markdown.css?v=20260904-qa">
  314. <script src="vendor/marked.umd.js?v=17.0.5"></script>
  315. <script src="answer_markdown.js?v=20260904-qa"></script>
  316. <link rel="stylesheet" href="answer_cache.css?v=20260909-timing">
  317. <script src="answer_cache.js?v=20260909-timing"></script>
  318. <script>
  319. const $ = (id) => document.getElementById(id);
  320. const chatEl = $("chat");
  321. const statusEl = $("status");
  322. const sendBtn = $("sendBtn");
  323. const questionInput = $("questionInput");
  324. const autoConfirmInput = $("autoConfirm");
  325. const reuseCheckInput = $("reuseCheck");
  326. const confirmPanel = $("confirmPanel");
  327. function resetAutoConfirmDefault() {
  328. autoConfirmInput.checked = false;
  329. }
  330. resetAutoConfirmDefault();
  331. window.addEventListener("pageshow", resetAutoConfirmDefault);
  332. let activeProgressEl = null;
  333. const THREAD_KEY = "kg_thread_id";
  334. function getThreadId() {
  335. let id = sessionStorage.getItem(THREAD_KEY);
  336. if (!id) {
  337. id = typeof crypto !== "undefined" && crypto.randomUUID
  338. ? crypto.randomUUID()
  339. : "thread-" + Date.now() + "-" + Math.random().toString(16).slice(2);
  340. sessionStorage.setItem(THREAD_KEY, id);
  341. }
  342. return id;
  343. }
  344. function newThreadId() {
  345. const id = typeof crypto !== "undefined" && crypto.randomUUID
  346. ? crypto.randomUUID()
  347. : "thread-" + Date.now() + "-" + Math.random().toString(16).slice(2);
  348. sessionStorage.setItem(THREAD_KEY, id);
  349. return id;
  350. }
  351. function apiBase() {
  352. if (location.protocol === "http:" || location.protocol === "https:") {
  353. return location.origin;
  354. }
  355. return "http://127.0.0.1:8000";
  356. }
  357. function setStatus(text) {
  358. statusEl.textContent = text;
  359. }
  360. function addProgressStep(node, label, container) {
  361. const chips = Array.from(container.querySelectorAll(".step-chip"));
  362. if (chips.length && chips[chips.length - 1].title === node) return;
  363. chips.forEach((chip) => chip.classList.remove("active"));
  364. chips.forEach((chip) => chip.classList.add("done"));
  365. const chip = document.createElement("span");
  366. chip.className = "step-chip active";
  367. chip.textContent = label || node;
  368. chip.title = node;
  369. container.appendChild(chip);
  370. container.scrollLeft = container.scrollWidth;
  371. }
  372. function finishProgressFlow(container, label) {
  373. const chips = Array.from(container.querySelectorAll(".step-chip"));
  374. chips.forEach((chip) => chip.classList.remove("active"));
  375. chips.forEach((chip) => chip.classList.add("done"));
  376. const chip = document.createElement("span");
  377. chip.className = "step-chip done";
  378. chip.textContent = label || "完成";
  379. container.appendChild(chip);
  380. container.scrollLeft = container.scrollWidth;
  381. }
  382. function appendMessage(role, text) {
  383. const row = document.createElement("div");
  384. row.className = `message-row ${role}`;
  385. if (role === "assistant") {
  386. const img = document.createElement("img");
  387. img.className = "avatar";
  388. img.src = "微信图片_20260817170012_14_2.png";
  389. img.alt = "AI";
  390. row.appendChild(img);
  391. } else if (role === "user") {
  392. const avatar = document.createElement("div");
  393. avatar.className = "avatar user-avatar";
  394. avatar.textContent = "我";
  395. row.appendChild(avatar);
  396. }
  397. const div = document.createElement("div");
  398. div.className = `message ${role}`;
  399. if (role === "assistant") renderAnswer(div, text);
  400. else div.textContent = text || "";
  401. row.appendChild(div);
  402. chatEl.appendChild(row);
  403. chatEl.scrollTop = chatEl.scrollHeight;
  404. return div;
  405. }
  406. function attachAnswerTiming(bubble, payload) {
  407. bubble.querySelector('.answer-timing')?.remove();
  408. if (typeof payload.elapsed_sec !== "number" || !Number.isFinite(payload.elapsed_sec)) return;
  409. const timing = document.createElement("div");
  410. timing.className = "answer-timing";
  411. timing.textContent = `回答耗时 ${payload.elapsed_sec} 秒`;
  412. bubble.appendChild(timing);
  413. }
  414. function appendSystem(text) {
  415. return appendMessage("system", text);
  416. }
  417. function appendError(text) {
  418. const div = appendMessage("error", text);
  419. setStatus("处理失败");
  420. return div;
  421. }
  422. function hideConfirm() {
  423. confirmPanel.classList.remove("show");
  424. $("confirmReply").value = "";
  425. $("confirmOptions").innerHTML = "";
  426. }
  427. function showConfirm(message, options, confirmType = "") {
  428. $("confirmMessage").textContent = message || "请确认查询条件";
  429. const box = $("confirmOptions");
  430. const replyInput = $("confirmReply");
  431. const multiple = confirmType === "clarify_value";
  432. replyInput.placeholder = multiple ? "可多选候选,或输入多个序号(逗号分隔)" : "回复:确认 / 或说明修改";
  433. const selected = new Set();
  434. box.innerHTML = "";
  435. replyInput.value = "";
  436. (options || []).forEach((opt) => {
  437. const chip = document.createElement("button");
  438. chip.type = "button";
  439. chip.className = "option-chip";
  440. chip.textContent = opt;
  441. chip.setAttribute("aria-pressed", "false");
  442. chip.onclick = () => {
  443. if (!multiple) {
  444. replyInput.value = opt;
  445. return;
  446. }
  447. if (opt === "都不是") {
  448. selected.clear();
  449. box.querySelectorAll(".option-chip").forEach(item => {
  450. item.classList.remove("selected");
  451. item.setAttribute("aria-pressed", "false");
  452. });
  453. selected.add(opt);
  454. } else {
  455. selected.delete("都不是");
  456. const decline = Array.from(box.children).find(item => item.textContent === "都不是");
  457. if (decline) {
  458. decline.classList.remove("selected");
  459. decline.setAttribute("aria-pressed", "false");
  460. }
  461. if (selected.has(opt)) selected.delete(opt);
  462. else selected.add(opt);
  463. }
  464. chip.classList.toggle("selected", selected.has(opt));
  465. chip.setAttribute("aria-pressed", selected.has(opt) ? "true" : "false");
  466. replyInput.value = Array.from(selected).join(",");
  467. };
  468. box.appendChild(chip);
  469. });
  470. confirmPanel.classList.add("show");
  471. replyInput.focus();
  472. setStatus("等待人工确认");
  473. confirmPanel.scrollIntoView({ behavior: "smooth", block: "center" });
  474. }
  475. function parseSseLine(line) {
  476. if (line.startsWith("event: ")) return ["event", line.slice(7).trim()];
  477. if (line.startsWith("data: ")) return ["data", line.slice(6)];
  478. return null;
  479. }
  480. async function askStream(question, autoConfirm, reuseCheck) {
  481. const threadId = getThreadId();
  482. const controller = new AbortController();
  483. const response = await fetch(`${apiBase()}/ask/stream`, {
  484. method: "POST",
  485. headers: { "Content-Type": "application/json" },
  486. body: JSON.stringify({
  487. thread_id: threadId,
  488. query: question,
  489. auto_confirm: autoConfirm,
  490. reuse_check: reuseCheck,
  491. }),
  492. signal: controller.signal,
  493. });
  494. if (!response.ok || !response.body) {
  495. throw new Error(`HTTP ${response.status}`);
  496. }
  497. const reader = response.body.getReader();
  498. const decoder = new TextDecoder();
  499. let buffer = "";
  500. let event = "";
  501. let data = "";
  502. const assistantRow = document.createElement("div");
  503. assistantRow.className = "message-row assistant";
  504. const avatar = document.createElement("img");
  505. avatar.className = "avatar";
  506. avatar.src = "微信图片_20260817170012_14_2.png";
  507. avatar.alt = "AI";
  508. assistantRow.appendChild(avatar);
  509. const assistantBubble = document.createElement("div");
  510. assistantBubble.className = "message assistant";
  511. const progressEl = document.createElement("div");
  512. progressEl.className = "progress-flow";
  513. activeProgressEl = progressEl;
  514. const answerEl = document.createElement("div");
  515. answerEl.className = "answer-text";
  516. assistantBubble.appendChild(progressEl);
  517. assistantBubble.appendChild(answerEl);
  518. assistantRow.appendChild(assistantBubble);
  519. chatEl.appendChild(assistantRow);
  520. chatEl.scrollTop = chatEl.scrollHeight;
  521. let gotAnswer = false;
  522. let answerText = "";
  523. const handleEvent = () => {
  524. if (!event) return;
  525. if (event === "progress") {
  526. try {
  527. const payload = JSON.parse(data);
  528. addProgressStep(payload.node, payload.label, progressEl);
  529. setStatus(payload.label || "处理中");
  530. } catch (_) {}
  531. } else if (event === "answer_chunk") {
  532. try {
  533. const payload = JSON.parse(data);
  534. if (!gotAnswer) {
  535. addProgressStep("answer", "正在生成回答", progressEl);
  536. setStatus("正在生成回答");
  537. }
  538. answerText += payload.text || "";
  539. renderAnswer(answerEl, answerText);
  540. chatEl.scrollTop = chatEl.scrollHeight;
  541. gotAnswer = true;
  542. } catch (_) {}
  543. } else if (event === "confirm") {
  544. try {
  545. const payload = JSON.parse(data);
  546. addProgressStep("confirm", "等待人工确认", progressEl);
  547. showConfirm(payload.message, payload.options, payload.confirm_type || payload.type);
  548. } catch (_) {}
  549. } else if (event === "done") {
  550. try {
  551. const payload = JSON.parse(data);
  552. if (typeof payload.answer === "string") answerText = payload.answer;
  553. renderAnswer(answerEl, answerText);
  554. attachAnswerTiming(assistantBubble, payload);
  555. window.AnswerCacheUI?.attach(assistantBubble, payload, threadId, apiBase());
  556. finishProgressFlow(progressEl, "完成");
  557. setStatus(`完成,耗时 ${payload.elapsed_sec || 0} 秒`);
  558. } catch (_) {}
  559. } else if (event === "error") {
  560. try {
  561. const payload = JSON.parse(data);
  562. assistantBubble.className = "message error";
  563. progressEl.style.display = "none";
  564. answerEl.textContent = payload.message || "请求失败";
  565. finishProgressFlow(progressEl, "失败");
  566. progressEl.style.display = "flex";
  567. setStatus("处理失败");
  568. } catch (_) {}
  569. }
  570. event = "";
  571. data = "";
  572. };
  573. while (true) {
  574. const { value, done } = await reader.read();
  575. if (done) break;
  576. buffer += decoder.decode(value, { stream: true });
  577. let idx;
  578. while ((idx = buffer.indexOf("\n\n")) >= 0) {
  579. const raw = buffer.slice(0, idx);
  580. buffer = buffer.slice(idx + 2);
  581. const lines = raw.split("\n");
  582. event = "";
  583. data = "";
  584. for (const line of lines) {
  585. const parsed = parseSseLine(line);
  586. if (!parsed) continue;
  587. if (parsed[0] === "event") event = parsed[1];
  588. else if (parsed[0] === "data") data = parsed[1];
  589. }
  590. handleEvent();
  591. }
  592. }
  593. // 处理最后可能没有结尾空行的数据
  594. if (buffer.trim()) {
  595. const parsed = parseSseLine(buffer.trim());
  596. if (parsed) {
  597. if (parsed[0] === "event") event = parsed[1];
  598. else if (parsed[0] === "data") data = parsed[1];
  599. handleEvent();
  600. }
  601. }
  602. return { threadId };
  603. }
  604. async function resumeThread(reply) {
  605. const threadId = getThreadId();
  606. const res = await fetch(`${apiBase()}/threads/${encodeURIComponent(threadId)}/resume`, {
  607. method: "POST",
  608. headers: { "Content-Type": "application/json" },
  609. body: JSON.stringify({ reply }),
  610. });
  611. const payload = await res.json();
  612. if (payload.status === "need_confirm") {
  613. if (activeProgressEl) addProgressStep("confirm", "等待人工确认", activeProgressEl);
  614. showConfirm(payload.message, payload.options, payload.confirm_type || payload.type);
  615. } else if (payload.status === "ok") {
  616. hideConfirm();
  617. if (activeProgressEl) finishProgressFlow(activeProgressEl, "完成");
  618. const answerBubble = appendMessage("assistant", payload.answer || "");
  619. attachAnswerTiming(answerBubble, payload);
  620. window.AnswerCacheUI?.attach(answerBubble, payload, threadId, apiBase());
  621. setStatus(`完成,耗时 ${payload.elapsed_sec || 0} 秒`);
  622. } else {
  623. hideConfirm();
  624. if (activeProgressEl) finishProgressFlow(activeProgressEl, "失败");
  625. appendError(payload.message || "恢复会话失败");
  626. }
  627. }
  628. async function sendQuestion() {
  629. const question = questionInput.value.trim();
  630. if (!question) return;
  631. appendMessage("user", question);
  632. questionInput.value = "";
  633. hideConfirm();
  634. sendBtn.disabled = true;
  635. setStatus("正在发送...");
  636. try {
  637. await askStream(question, autoConfirmInput.checked, reuseCheckInput.checked);
  638. } catch (err) {
  639. appendError(err.message || String(err));
  640. } finally {
  641. sendBtn.disabled = false;
  642. questionInput.focus();
  643. }
  644. }
  645. sendBtn.addEventListener("click", sendQuestion);
  646. questionInput.addEventListener("keydown", (e) => {
  647. if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
  648. e.preventDefault();
  649. sendQuestion();
  650. }
  651. });
  652. $("confirmSendBtn").addEventListener("click", async () => {
  653. const btn = $("confirmSendBtn");
  654. btn.disabled = true;
  655. btn.textContent = "处理中...";
  656. const reply = $("confirmReply").value.trim() || "确认";
  657. setStatus("正在恢复会话...");
  658. try {
  659. await resumeThread(reply);
  660. } catch (err) {
  661. appendError(err.message || String(err));
  662. } finally {
  663. btn.disabled = false;
  664. btn.textContent = "继续";
  665. }
  666. });
  667. $("newSessionBtn").addEventListener("click", () => {
  668. newThreadId();
  669. chatEl.innerHTML = "";
  670. hideConfirm();
  671. setStatus(`新会话已创建:${getThreadId()}`);
  672. appendSystem("已创建新会话。当前 thread_id 会在本浏览器标签页内保持不变。");
  673. });
  674. appendSystem(`当前会话 thread_id:${getThreadId()}`);
  675. </script>
  676. <script src="data_quality.js?v=20260904"></script>
  677. </body>
  678. </html>