diff --git a/docs/architecture.md b/docs/architecture.md index b6cce18..0b6a101 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -86,6 +86,7 @@ tests/ # 按业务模块组织的回归 - 单文件围绕一个职责;超过约 300–400 行时检查职责,而非按行数机械拆分。报价数据保持单一来源;兼容转发不复制字典。页面脚本按页面保留既有执行顺序和全局函数,不擅自改为 ES Modules。 - 配色、字体基线和通用控件放在 `theme.css`,页面布局放在对应 CSS。客户页通过 `body.dark` 覆盖主题令牌;新增可翻译文案加入 `chat.js` 的 `I18N`,静态文案用 `data-i18n`。调整样式后检查桌面、390px 窄屏、侧栏、聊天卡片和客服空/活动状态。 - 客户附件预览由 `clearAttachment` 管理,轨迹展开由 `fillTrackCard` 管理。客服 `drafts` 按 `sid` 保存草稿,发送成功后只清理已确认发送的内容;常用回复仅填入草稿。`renderList` 只在展示数据变化时重绘。 +- 客户页 `resetConversation` 统一处理新建、快捷问题和历史切换;人工路由同时绑定 `humanConvId` 与当前会话版本。旧建会话、历史、人工申请和轮询的异步结果不得覆盖新话题。三个业务快捷入口开启独立 AI 对话,联系人工入口才进入人工模式;排队/接待状态常驻输入框上方,可用「返回 AI 问答」结束人工会话。人工消息确认成功后才清空草稿,失败保留输入;服务端校验可选 `conv_id` 并拒绝已关闭会话。 - 客服工作台默认只展示会话列表和全高聊天区;统计使用原生 `details` 收在「数据概览」。`setSessionTab` 切换接待/排队列表,`setChatView` 与 `showSessionList` 控制 720px 及以下的整屏导航,不结束当前会话。`setToolsOpen` 控制默认关闭的服务资料:1200px 以上为侧栏,较窄屏为抽屉;列表入口让未选会话的手机用户也能查看留言。 ## 后续结构调整顺序 diff --git a/logistics_chat/human_service.py b/logistics_chat/human_service.py index ebd8787..e73c06b 100644 --- a/logistics_chat/human_service.py +++ b/logistics_chat/human_service.py @@ -42,11 +42,20 @@ def human_poll(sid, after, typing_map): def human_send(data): sid = data.get('sid') side = data.get('side', 'customer') - content = (data.get('content') or '').strip() - if not sid or not content: + content = data.get('content') or '' + if not sid or not isinstance(content, str) or not content.strip() or side not in ('customer', 'agent'): return ({'ok': False}, 400) + content = content.strip() now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') conn = connect() + conn.execute('BEGIN IMMEDIATE') + row = conn.execute('SELECT conv_id, status FROM human_sessions WHERE id=?', (sid,)).fetchone() + if not row: + conn.close() + return ({'ok': False, 'error': 'session_not_found'}, 404) + if row[1] == 'closed' or (data.get('conv_id') is not None and data['conv_id'] != row[0]): + conn.close() + return ({'ok': False, 'error': 'conversation_changed'}, 409) conn.execute('INSERT INTO human_messages(session_id, side, content, ctime) VALUES(?,?,?,?)', (sid, side, content, now)) conn.commit() conn.close() diff --git a/static/css/chat.css b/static/css/chat.css index 519ecf8..8321367 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -87,6 +87,8 @@ body { padding: 20px; height: 100dvh; overflow: hidden; } .recent-date { white-space: nowrap; color: var(--muted); font-size: 10px; } .composer { flex-shrink: 0; align-self: center; width: min(820px, calc(100% - 76px)); padding: 12px 0 15px; background: var(--surface); } .hot-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-bottom: 10px; } +.human-session { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; margin-bottom: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--brand-soft); color: var(--secondary); font-size: 12px; } +.human-session button { flex-shrink: 0; padding: 6px 9px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--brand-text); font-size: 12px; } .hot-bar-label { font-size: 10px; color: var(--muted); margin-right: 3px; } .hot-bar .hot-capsule { border: 0; padding: 4px 8px; background: var(--surface-soft); font-size: 10px; } .input-area { display: flex; align-items: flex-end; gap: 10px; border: 1px solid var(--border); border-radius: 12px; padding: 9px; background: var(--surface); box-shadow: 0 3px 15px rgba(28, 47, 80, .035); } diff --git a/static/js/chat.js b/static/js/chat.js index 3b0792a..1c5db12 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -10,6 +10,15 @@ let uploadVersion = 0; let followLatest = true; let creatingConv = null; let sendPreparing = false; +let conversationVersion = 0; +let activeQuickTopic = null; +let humanConvId = null; +let humanStatus = ''; +let humanRequestToken = null; +let humanPollTimer = null; +let humanSendingToken = null; +let humanClosing = false; +window.humanSid = 0; SupportUI.initEmoji(document.getElementById('emoji-toggle'), document.getElementById('emoji-panel'), inputDom, function(){ autoGrow(); }); document.getElementById('notifications-toggle').onclick = function(){ SupportUI.enableNotifications(this); }; @@ -175,6 +184,17 @@ const I18N = { offlineSaved: "留言已提交,工作时间(8:00-22:00)会尽快回复您。", humanClosed: "人工客服已结束,您可以继续向 AI 提问。", humanReply: "客服回复", + humanConnecting: "正在为您连接人工客服…", + humanWaiting: "正在排队,消息将由人工客服接入后回复。您也可以返回 AI 继续咨询。", + humanActive: "人工客服接待中,消息将发送给人工客服。", + returnAI: "返回 AI 问答", + humanSent: "已发送给人工客服", + humanSending: "正在发送给人工客服…", + humanSendFailed: "消息未发送成功,内容已保留,请重试。", + humanConnectFailed: "人工客服连接失败,请稍后重试。", + humanPollFailed: "人工客服连接中断,正在重试。您可以返回 AI 继续咨询。", + humanReturnFailed: "暂时无法结束人工会话,请重试。", + historyLoading: "正在加载这段会话,请稍候…", recalled: "该消息已撤回", ratingQuestion: "本次服务满意吗?", ratingThanks: "感谢您的评价!", @@ -248,6 +268,17 @@ const I18N = { offlineSaved: "Message submitted. Our team will reply during service hours (08:00–22:00).", humanClosed: "This support session has ended. You can continue chatting with the AI assistant.", humanReply: "Support reply", + humanConnecting: "Connecting you to our support team…", + humanWaiting: "You are in the queue. A team member will reply after joining, or you can return to AI chat.", + humanActive: "You are chatting with our support team. Messages will be sent to a team member.", + returnAI: "Return to AI chat", + humanSent: "Sent to our support team", + humanSending: "Sending to our support team…", + humanSendFailed: "Message not sent. Your text has been kept. Please retry.", + humanConnectFailed: "Could not connect to our support team. Please try again.", + humanPollFailed: "Support connection lost. Retrying, or you can return to AI chat.", + humanReturnFailed: "Could not end the support session. Please try again.", + historyLoading: "Loading this conversation. Please wait…", recalled: "This message was recalled", ratingQuestion: "How was your support experience?", ratingThanks: "Thank you for your feedback!", @@ -339,6 +370,7 @@ function applyLanguage(lang){ if(tb) tb.title = dict.toBottom; const tg = document.querySelector(".toggle-divider"); if(tg) tg.title = tg.getAttribute('aria-expanded') === 'false' ? dict.sideExpand : dict.sideCollapse; + updateHumanMode(); // 欢迎页(若当前显示的是欢迎页则重绘) if(chatDom.querySelector(".ai-welcome-card")){ @@ -495,12 +527,19 @@ function welcomeHTML(){ if(clearAllBtn){ clearAllBtn.onclick = function(){ if(!confirm(t("confirmClear"))) return; - if(cancelActive){ cancelActive(); cancelActive = null; } + const version = resetConversation(null); + openingConv = 'clearing'; fetch("/api/clear_all", {method:"DELETE"}) - .then(()=>{ - currentConvId = null; + .then(response=>{ + if(!response.ok) throw new Error('Clear failed'); + if(version !== conversationVersion) return; + openingConv = null; chatDom.innerHTML = welcomeHTML(); loadHistory(); + }).catch(function(){ + if(version !== conversationVersion) return; + openingConv = null; + setComposerStatus(t('networkFail'), true); }); } } @@ -508,13 +547,11 @@ clearAllBtn.onclick = function(){ // 新建对话:只清界面,等用户开口时再建会话 if(newChatBtn){ newChatBtn.onclick = function(){ - if(cancelActive){ cancelActive(); cancelActive = null; } - if(sseInstance){ sseInstance.close(); sseInstance = null; } + resetConversation(null); inputDom.value = ""; autoGrow(); clearAttachment(); setComposerStatus(""); - currentConvId = null; chatDom.innerHTML = welcomeHTML(); chatDom.scrollTop = 0; followLatest = true; @@ -647,33 +684,56 @@ function renderRecentChat(){ function ensureConv(){ if(currentConvId) return Promise.resolve(currentConvId); if(creatingConv) return creatingConv; - creatingConv = fetch("/api/new_chat", {method:"POST"}) + const version = conversationVersion; + const pending = fetch("/api/new_chat", {method:"POST"}) .then(async function(res){ const data = await res.json(); if(!res.ok || !data.id) throw new Error('Session failed'); return data; }) .then(data=>{ + if(version !== conversationVersion){ + const error = new Error('Conversation changed'); + error.name = 'AbortError'; + throw error; + } currentConvId = data.id; if(data.welcome) addMsg(t('autoWelcome'), false); return currentConvId; - }).finally(function(){ creatingConv = null; }); - return creatingConv; + }).finally(function(){ if(creatingConv === pending) creatingConv = null; }); + creatingConv = pending; + return pending; +} + +function resetConversation(id){ + if(cancelActive){ cancelActive(); cancelActive = null; } + if(sseInstance){ sseInstance.close(); sseInstance = null; } + conversationVersion += 1; + stopHumanMode(); + currentConvId = id; + openingConv = null; + creatingConv = null; + sendPreparing = false; + activeQuickTopic = null; + setBusy(false); + setComposerStatus(''); + return conversationVersion; } // 打开历史会话:还原整段对话,而不是把标题重发一遍 let openingConv = null; function openConversation(id){ if(openingConv === id) return; // 防连点 + const version = resetConversation(id); openingConv = id; - if(cancelActive){ cancelActive(); cancelActive = null; } - if(sseInstance){ sseInstance.close(); sseInstance = null; } - currentConvId = id; + setComposerStatus(t('historyLoading')); renderSideHistory(); fetch("/api/get_messages?conv_id=" + id) - .then(res=>res.json()) + .then(res=>{ if(!res.ok) throw new Error('History failed'); return res.json(); }) .then(msgs=>{ + if(version !== conversationVersion) return; openingConv = null; + setComposerStatus(''); chatDom.innerHTML = ""; if(!msgs.length){ chatDom.innerHTML = welcomeHTML(); @@ -693,7 +753,11 @@ function openConversation(id){ }); // 等渲染完再滚到底部 requestAnimationFrame(function(){ scrollChatToEnd(true); }); - })["catch"](function(){ openingConv = null; }); + }).catch(function(){ + if(version !== conversationVersion) return; + openingConv = null; + setComposerStatus(t('networkFail'), true); + }); } const CARD_PREFIX = "__CARD__"; @@ -749,12 +813,12 @@ function fillTrackCard(el, card){ } } -function createMessageRow(isUser){ +function createMessageRow(isUser, authorRole){ const row = document.createElement("div"); row.className = "msg-row " + (isUser ? "user-line" : "robot-line"); const author = document.createElement("div"); author.className = "message-author"; - const role = isUser ? "you" : (window.humanSid ? "humanAgent" : "assistant"); + const role = isUser ? "you" : (authorRole || "assistant"); author.innerHTML = (isUser ? "" : '' + welcomeIcon("box") + '') + '' + t(role) + ''; row.appendChild(author); return row; @@ -775,19 +839,20 @@ loadHistory(); window.fillInput = function(words){ inputDom.value = words; - doSend(); + return doSend(); } // 点快捷问题:另起一个会话,不同话题就不会堆进同一条历史 window.askNewTopic = function(words){ - if(cancelActive){ cancelActive(); cancelActive = null; } - currentConvId = null; // 下次发送时自动新建会话 + if(activeQuickTopic === words && (sendPreparing || window.__busy)) return; + resetConversation(null); + activeQuickTopic = words; // 新话题先重置界面,发送第一条消息时再收起欢迎内容。 if(!chatDom.querySelector('.ai-welcome-card')){ chatDom.innerHTML = welcomeHTML(); } inputDom.value = words; - doSend(); + return doSend(); } window.sendOrder = function(){ @@ -816,64 +881,161 @@ function myOrders(){ .catch(function(){ addMsg("查询失败,请重试", false); }); } +function updateHumanMode(){ + const banner = document.getElementById('human-session'); + const label = document.getElementById('human-session-label'); + const button = document.getElementById('return-ai'); + if(banner) banner.hidden = !window.humanSid && !humanRequestToken; + const text = t(humanRequestToken ? 'humanConnecting' : humanStatus === 'active' ? 'humanActive' : 'humanWaiting'); + if(label && label.textContent !== text) label.textContent = text; + if(button){ + button.textContent = t('returnAI'); + button.disabled = Boolean(humanRequestToken || humanClosing || humanSendingToken); + } + sendBtn.disabled = Boolean(humanRequestToken || humanClosing || humanSendingToken); +} + +function stopHumanMode(){ + if(humanPollTimer !== null) clearInterval(humanPollTimer); + humanPollTimer = null; + humanConvId = null; + humanStatus = ''; + humanRequestToken = null; + humanSendingToken = null; + humanClosing = false; + window.humanSid = 0; + const typing = document.getElementById('human-typing'); + if(typing) typing.remove(); + updateHumanMode(); +} + +async function humanRequest(path, data){ + const controller = new AbortController(); + const timer = setTimeout(function(){ controller.abort(); }, 10000); + try{ + const response = await fetch(path, {method:data ? 'POST' : 'GET', + headers:data ? {'Content-Type':'application/json'} : {}, + body:data ? JSON.stringify(data) : undefined, signal:controller.signal}); + const result = await response.json(); + if(!response.ok){ + const error = new Error('Human request failed'); + error.status = response.status; + throw error; + } + return result; + }finally{ clearTimeout(timer); } +} + window.showHuman = async function(){ - if(window.humanSid){ - addMsg("您已在人工客服会话中,请直接提问。", false); - return; + if(humanRequestToken || humanClosing) return; + if(window.humanSid && humanConvId === currentConvId){ updateHumanMode(); return; } + if(window.humanSid) stopHumanMode(); + const hour = new Date().getHours(); + if(hour < 8 || hour >= 22) return leaveOfflineMessage(); + const token = {version:conversationVersion}; + humanRequestToken = token; + if(cancelActive){ cancelActive(); cancelActive = null; } + setBusy(false); + updateHumanMode(); + try{ + const cid = await ensureConv(); + if(humanRequestToken !== token || token.version !== conversationVersion) return; + const data = await humanRequest('/api/human/request', {conv_id:cid}); + if(humanRequestToken !== token || token.version !== conversationVersion) return; + if(data.closed){ await leaveOfflineMessage(); return; } + if(!data.ok || !data.session_id) throw new Error('Human session failed'); + window.humanSid = data.session_id; + humanConvId = cid; + humanStatus = 'waiting'; + addMsg(t('humanWaiting'), false); + startHumanPoll(); + }catch(error){ + if(humanRequestToken === token && token.version === conversationVersion) setComposerStatus(t('humanConnectFailed'), true); + }finally{ + if(humanRequestToken === token){ humanRequestToken = null; updateHumanMode(); } + } +}; + +async function returnToAI(){ + if(!window.humanSid || humanClosing || humanSendingToken) return; + const sid = window.humanSid, version = conversationVersion; + humanClosing = true; + updateHumanMode(); + try{ + const result = await humanRequest('/api/human/close', {sid:sid}); + if(!result.ok) throw new Error('Close failed'); + if(version !== conversationVersion || window.humanSid !== sid) return; + stopHumanMode(); + setComposerStatus(''); + addMsg(t('humanClosed'), false); + inputDom.focus(); + }catch(error){ + if(version === conversationVersion) setComposerStatus(t('humanReturnFailed'), true); + }finally{ + if(version === conversationVersion){ humanClosing = false; updateHumanMode(); } + } +} +const returnAIButton = document.getElementById('return-ai'); +if(returnAIButton) returnAIButton.onclick = returnToAI; + +async function sendHumanMessage(content){ + if(humanSendingToken || humanClosing) return; + const sid = window.humanSid, cid = currentConvId, version = conversationVersion; + const draft = inputDom.value, attachment = window.__pendingImg; + const token = {}; + humanSendingToken = token; + updateHumanMode(); + setComposerStatus(t('humanSending')); + try{ + const result = await humanRequest('/api/human/send', {sid:sid, conv_id:cid, side:'customer', content:content}); + if(!result.ok) throw new Error('Send failed'); + if(version !== conversationVersion || humanSendingToken !== token) return; + addMsg(content, true); + if(inputDom.value === draft){ inputDom.value = ''; autoGrow(); } + if(window.__pendingImg === attachment) clearAttachment(); + setComposerStatus(t(humanStatus === 'active' ? 'humanSent' : 'humanWaiting')); + inputDom.focus(); + }catch(error){ + if(version !== conversationVersion || humanSendingToken !== token) return; + if(error.status === 404 || error.status === 409) stopHumanMode(); + setComposerStatus(t('humanSendFailed'), true); + }finally{ + if(humanSendingToken === token){ humanSendingToken = null; updateHumanMode(); } } - var now = new Date(); - var h = now.getHours(); - var work = h >= 8 && h < 22; - if(!work){ - leaveOfflineMessage(); - return; - } - addMsg("正在为您转接人工客服...", false); - if(!currentConvId){ - try{ await ensureConv(); }catch(e){ setComposerStatus(t('sessionFail'), true); return; } - } - fetch("/api/human/request",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({conv_id:currentConvId||0})}) - .then(function(r){return r.json()}) - .then(function(d){ - if(d.ok){ - window.humanSid = d.session_id; - addMsg("您已加入人工排队,请稍候...", false); - startHumanPoll(); - }else if(d.closed){ - leaveOfflineMessage(); - }else{ - addMsg("转接失败,请稍后再试。", false); - } - }) - .catch(function(){ - addMsg("网络错误,请稍后再试。", false); - }); } -window.humanSid = 0; async function leaveOfflineMessage(){ + const version = conversationVersion; addMsg(t('humanHours'), false); const message = window.prompt(t('offlinePrompt')); if(!message) return; try{ - const response = await fetch('/api/ticket', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({content:message, phone:''})}); - const data = await response.json(); - if(!response.ok || !data.ok) throw new Error('Ticket failed'); - addMsg(t('offlineSaved'), false); - }catch(error){ setComposerStatus(t('networkFail'), true); } + const data = await humanRequest('/api/ticket', {content:message, phone:''}); + if(!data.ok) throw new Error('Ticket failed'); + if(version === conversationVersion) addMsg(t('offlineSaved'), false); + }catch(error){ + if(version !== conversationVersion) return; + if(!inputDom.value) inputDom.value = message; + setComposerStatus(t('networkFail'), true); + } } function startHumanPoll(){ let last=0; let polling=false; const sid=window.humanSid; + const cid=humanConvId, version=conversationVersion; + let hadPollError=false; + if(humanPollTimer !== null) clearInterval(humanPollTimer); const pollTimer=setInterval(()=>{ - if(window.humanSid !== sid){ clearInterval(pollTimer); return; } + if(window.humanSid !== sid || version !== conversationVersion || currentConvId !== cid){ clearInterval(pollTimer); return; } if(polling) return; polling=true; - fetch("/api/human/poll?sid="+sid+"&after="+last) - .then(r=>r.json()) + humanRequest("/api/human/poll?sid="+sid+"&after="+last) .then(d=>{ - if(window.humanSid !== sid) return; + if(window.humanSid !== sid || version !== conversationVersion || currentConvId !== cid) return; + if(hadPollError){ setComposerStatus(''); hadPollError=false; } + humanStatus=d.status; + updateHumanMode(); d.messages.forEach(m=>{ if(m.id <= last) return; last = m.id; @@ -888,7 +1050,7 @@ function startHumanPoll(){ system.textContent = m.content; chatDom.appendChild(system); }else if(m.side === 'agent'){ - const bubble = addMsg(m.content, false); + const bubble = addMsg(m.content, false, 'humanAgent'); bubble.closest('.msg-row').setAttribute('data-human-message-id', String(m.id)); SupportUI.notify(t('humanReply'), m.content.substring(0, 50)); } @@ -896,12 +1058,9 @@ function startHumanPoll(){ if(typing) typing.remove(); }); if(d.status==="closed"){ - clearInterval(pollTimer); - window.humanSid=0; - window.__busy=false; - if(typeof setBusy==="function") setBusy(false); - const typing = document.getElementById('human-typing'); - if(typing) typing.remove(); + if(humanClosing) return; + stopHumanMode(); + setBusy(false); addMsg(t('humanClosed'), false); SupportUI.addRating(chatDom, sid, {question:t('ratingQuestion'), thanks:t('ratingThanks'), failed:t('ratingFailed')}); scrollChatToEnd(); @@ -920,13 +1079,18 @@ function startHumanPoll(){ oldT.remove(); } }) - .catch(()=>{}).finally(function(){ polling=false; }); + .catch(function(){ + if(version !== conversationVersion || window.humanSid !== sid) return; + hadPollError=true; + setComposerStatus(t('humanPollFailed'), true); + }).finally(function(){ polling=false; }); }, 1000); + humanPollTimer = pollTimer; } -function addMsg(content, isUser){ +function addMsg(content, isUser, authorRole){ if(chatDom.querySelector('.ai-welcome-card')) chatDom.innerHTML = ""; - const rowDiv = createMessageRow(isUser); + const rowDiv = createMessageRow(isUser, authorRole); let robotMsg; // 检测图片附件 let imgUrl = null; @@ -1134,35 +1298,31 @@ async function doSend(retryText){ return; } if(uploadPending){ setComposerStatus(t("uploading")); return; } + if(humanSendingToken || humanClosing) return; var fullContent = sendWord; if(imgUrl) fullContent += (fullContent ? " " : "") + tag + imgUrl; setComposerStatus(""); + if(openingConv !== null){ setComposerStatus(t('historyLoading')); return; } + if(humanRequestToken || humanClosing){ updateHumanMode(); return; } // 人工客服模式:直接发给客服,不走AI - if(window.humanSid){ - var content = sendWord; - if(imgUrl) content += (content ? " " : "") + tag + imgUrl; - if(!isRetry) addMsg(content, true); - inputDom.value = ""; - autoGrow(); - window.__pendingImg = ""; window.__pendingTag = "[图片]"; - var p = document.getElementById("imgPreview"); if(p) p.remove(); - inputDom.focus(); - fetch("/api/human/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:window.humanSid,side:"customer",content:content})}); - return; - } + if(window.humanSid && humanConvId === currentConvId) return sendHumanMessage(fullContent); + if(window.humanSid) stopHumanMode(); if(sendPreparing || (window.__busy && !retryText)) return; // 防连点 if(cancelActive){ cancelActive(); cancelActive = null; } sendPreparing = true; + const version = conversationVersion; + let convId; try{ - await ensureConv(); + convId = await ensureConv(); }catch(error){ - setComposerStatus(t('sessionFail'), true); + if(version === conversationVersion) setComposerStatus(t('sessionFail'), true); return; }finally{ - sendPreparing = false; + if(version === conversationVersion) sendPreparing = false; } + if(version !== conversationVersion || humanRequestToken) return; if(!isRetry){ addMsg(fullContent, true); inputDom.value = ""; @@ -1257,7 +1417,7 @@ async function doSend(retryText){ cancelActive = stop; function handleFrame(dataText){ - if(settled) return; + if(settled || version !== conversationVersion) return; let obj; try{ obj = JSON.parse(dataText); @@ -1309,7 +1469,7 @@ async function doSend(retryText){ fetch("/stream_chat", { method: "POST", headers: {"Content-Type": "application/json"}, - body: JSON.stringify({msg: fullContent, conv_id: currentConvId}), + body: JSON.stringify({msg: fullContent, conv_id: convId}), signal: controller.signal }).then(function(res){ if(!res.ok){ throw new Error("HTTP " + res.status); } diff --git a/templates/index.html b/templates/index.html index 385b20d..d39ba6f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -49,6 +49,7 @@