完善会话草稿恢复与附件保留,统一人工服务时区

This commit is contained in:
2026-10-30 15:32:31 +08:00
parent 78797d8eca
commit b764fcb33c
20 changed files with 446 additions and 34 deletions
+77 -12
View File
@@ -6,6 +6,28 @@ const sendBtn = document.getElementById("send-button");
const uploadBtn = document.getElementById("upload-btn");
const uploadInput = document.getElementById("upload-input");
const composerStatus = document.getElementById("composer-status");
const drafts = ChatDrafts.create({scope:VisitorSession.localKey(),storage:()=>localStorage,tabStorage:()=>sessionStorage});
let draftTimer = null;
let draftSnapshot = null;
function flushDraft(){
if(draftTimer !== null){ clearTimeout(draftTimer); draftTimer = null; }
const saved = drafts.write(currentConvId, inputDom.value);
drafts.active(currentConvId);
if(!saved && inputDom.value && !composerStatus.textContent) setComposerStatus(t('draftSaveFailed'), true);
}
function restoreDraft(){
inputDom.value = drafts.read(currentConvId);
draftSnapshot = String(currentConvId) + '\n' + inputDom.value;
drafts.active(currentConvId);
autoGrow();
}
function queueDraft(){
const snapshot = String(currentConvId) + '\n' + inputDom.value;
if(snapshot === draftSnapshot) return;
draftSnapshot = snapshot;
if(draftTimer !== null) clearTimeout(draftTimer);
draftTimer = setTimeout(flushDraft, 250);
}
function focusComposer(force){
if(force || !isNarrow() || document.activeElement === inputDom){
inputDom.focus({preventScroll:true});
@@ -29,6 +51,7 @@ let humanSendingToken = null;
let humanClosing = false;
window.humanSid = 0;
const emojiPicker = SupportUI.initEmoji(document.getElementById('emoji-toggle'), document.getElementById('emoji-panel'), inputDom, function(){ autoGrow(); });
SupportUI.initAttachments(chatDom, ()=>t('attachmentUnavailable'));
document.getElementById('notifications-toggle').onclick = function(){ SupportUI.enableNotifications(this); };
function setComposerStatus(message, isError){
@@ -217,7 +240,7 @@ const I18N = {
busyRetry: "服务繁忙,请稍后重试",
sessionFail: "会话创建失败,请确认服务已启动",
autoWelcome: "您好!欢迎咨询柏悦物流智能客服,可以咨询运费、时效或联系人工客服。订单查询将在客户账号登录并关联后开放。",
humanHours: "人工客服工作时间:每天 8:00-22:00。当前为非工作时间,请留言,工作时间会尽快回复您。",
humanHours: "人工客服工作时间:每天北京时间 8:00-22:00。当前为非工作时间,请留言,工作时间会尽快回复您。",
offlinePrompt: "请输入您的留言:",
offlineSaved: "留言已提交,工作时间(8:00-22:00)会尽快回复您。",
humanClosed: "人工客服已结束,您可以继续向 AI 提问。",
@@ -233,6 +256,10 @@ const I18N = {
humanPollFailed: "人工客服连接中断,正在重试。您可以返回 AI 继续咨询。",
humanReturnFailed: "暂时无法结束人工会话,请重试。",
historyLoading: "正在加载这段会话,请稍候…",
historyLoadFailed: "会话未加载成功,请重新选择该会话重试,或新建对话。",
draftRestored: "已恢复这段会话的未发送草稿。",
draftSaveFailed: "草稿暂时无法保存到当前浏览器,刷新前请保留输入内容。",
attachmentUnavailable: "图片暂时无法访问,请重新发送附件。",
recalled: "该消息已撤回",
ratingQuestion: "本次服务满意吗?",
ratingThanks: "感谢您的评价!",
@@ -330,7 +357,7 @@ const I18N = {
busyRetry: "Service busy, please try again later",
sessionFail: "Failed to create session, please check the service is running",
autoWelcome: "Welcome to Baiyue Logistics. Ask about rates, transit times or human support. Order lookup will be available after signing in and linking a customer account.",
humanHours: "Our team is available daily, 08:00–22:00. Please leave a message and we will reply during service hours.",
humanHours: "Our team is available daily, 08:00–22:00 China Standard Time (UTC+8). Please leave a message and we will reply during service hours.",
offlinePrompt: "Please leave your message:",
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.",
@@ -346,6 +373,10 @@ const I18N = {
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…",
historyLoadFailed: "Conversation could not load. Select it again to retry, or start a new chat.",
draftRestored: "Your unsent draft has been restored.",
draftSaveFailed: "Your browser could not save this draft. Keep a copy before refreshing.",
attachmentUnavailable: "Image unavailable. Please upload the attachment again.",
recalled: "This message was recalled",
ratingQuestion: "How was your support experience?",
ratingThanks: "Thank you for your feedback!",
@@ -653,11 +684,12 @@ clearAllBtn.onclick = function(){
if(newChatBtn){
newChatBtn.onclick = function(){
if(openingConv === 'clearing') return;
const alreadyNew = currentConvId === null;
resetConversation(null);
inputDom.value = "";
if(alreadyNew) inputDom.value = "";
autoGrow();
clearAttachment();
setComposerStatus("");
setComposerStatus(inputDom.value ? t('draftRestored') : '');
chatDom.innerHTML = welcomeHTML();
chatDom.scrollTop = 0;
followLatest = true;
@@ -673,10 +705,13 @@ function loadHistory(){
historyList = data;
renderSideHistory();
renderRecentChat();
}).catch(function(){ /* 保留上次成功加载的历史,避免网络错误清空侧栏。 */ });
return data;
}).catch(function(){ return null; /* 保留上次成功加载的历史。 */ });
}
function removeCachedHistory(id){
if(id === undefined){ drafts.clear(); flushDraft(); }
else drafts.remove(id);
historyRequestVersion += 1;
historyList = id === undefined ? [] : historyList.filter(item => item.id !== id);
renderSideHistory();
@@ -823,6 +858,8 @@ function ensureConv(){
throw error;
}
currentConvId = data.id;
drafts.remove('new');
flushDraft();
if(data.welcome) addMsg(t('autoWelcome'), false);
return currentConvId;
}).finally(function(){ if(creatingConv === pending) creatingConv = null; });
@@ -831,6 +868,7 @@ function ensureConv(){
}
function resetConversation(id){
flushDraft();
emojiPicker.close();
if(voiceInput) voiceInput.cancel();
if(cancelActive){ cancelActive(); cancelActive = null; }
@@ -839,6 +877,7 @@ function resetConversation(id){
clearAttachment();
stopHumanMode();
currentConvId = id;
restoreDraft();
openingConv = null;
creatingConv = null;
sendPreparing = false;
@@ -856,6 +895,7 @@ function openConversation(id){
const version = resetConversation(id);
if(isNarrow()){ sideOpen = false; renderSidebar(); }
openingConv = id;
chatDom.innerHTML = '';
setComposerStatus(t('historyLoading'));
renderSideHistory();
requestJson("/api/get_messages?conv_id=" + id)
@@ -863,7 +903,7 @@ function openConversation(id){
if(version !== conversationVersion) return;
if(!Array.isArray(msgs)) throw new Error('History failed');
openingConv = null;
setComposerStatus('');
setComposerStatus(inputDom.value ? t('draftRestored') : '');
chatDom.innerHTML = "";
if(!msgs.length){
chatDom.innerHTML = welcomeHTML();
@@ -886,8 +926,8 @@ function openConversation(id){
requestAnimationFrame(function(){ scrollChatToEnd(true); });
}).catch(function(){
if(version !== conversationVersion) return;
openingConv = null;
setComposerStatus(t('networkFail'), true);
openingConv = 'failed';
setComposerStatus(t('historyLoadFailed'), true);
});
}
@@ -1047,7 +1087,30 @@ function addTrackCard(card, timeStr){
return bubble;
}
loadHistory();
const restoreId = drafts.active();
currentConvId = restoreId === 'new' ? null : Number(restoreId);
restoreDraft();
if(currentConvId){
const id = currentConvId, version = conversationVersion, initialDraft = inputDom.value;
openingConv = 'restoring';
loadHistory().then(function(history){
if(version !== conversationVersion || currentConvId !== id || openingConv !== 'restoring') return;
if(Array.isArray(history) && !history.some(item=>item.id === id)){
if(inputDom.value !== initialDraft){
openingConv = 'failed';
flushDraft();
setComposerStatus(t('historyLoadFailed'), true);
return;
}
drafts.remove(id);
currentConvId = null;
restoreDraft();
openingConv = null;
return;
}
openConversation(id);
});
}else loadHistory();
window.fillInput = function(words){
inputDom.value = words;
@@ -1113,11 +1176,10 @@ async function humanRequest(path, data){
window.showHuman = async function(){
if(voiceInput) voiceInput.cancel();
if(openingConv === 'clearing') return;
if(openingConv !== null){ setComposerStatus(t(openingConv === 'failed' ? 'historyLoadFailed' : 'historyLoading'), true); 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; }
@@ -1530,7 +1592,7 @@ async function doSend(retryText){
var fullContent = sendWord;
if(imgUrl) fullContent += (fullContent ? " " : "") + tag + imgUrl;
setComposerStatus("");
if(openingConv !== null){ setComposerStatus(t('historyLoading')); return; }
if(openingConv !== null){ setComposerStatus(t(openingConv === 'failed' ? 'historyLoadFailed' : 'historyLoading'), true); return; }
if(humanRequestToken || humanClosing){ updateHumanMode(); return; }
// 人工客服模式:直接发给客服,不走AI
if(window.humanSid && humanConvId === currentConvId) return sendHumanMessage(fullContent);
@@ -1764,6 +1826,7 @@ sendBtn.addEventListener("click", function(){
// ===== 多行输入框:自动增高 =====
function autoGrow(){
queueDraft();
if(!inputDom.value){
inputDom.style.height = '';
return;
@@ -1775,6 +1838,8 @@ function autoGrow(){
inputDom.style.height = Math.min(inputDom.scrollHeight, limit) + "px";
}
inputDom.addEventListener("input", autoGrow);
window.addEventListener('pagehide', flushDraft);
document.addEventListener('visibilitychange', function(){ if(document.hidden) flushDraft(); });
inputDom.addEventListener("keydown",(e)=>{
// Enter 发送,Shift+Enter 换行