完善会话草稿恢复与附件保留,统一人工服务时区
This commit is contained in:
@@ -11,6 +11,7 @@ var typeTimer = null;
|
||||
var toolsOpen = false;
|
||||
var toolsOpener = null;
|
||||
var replyInput = document.getElementById("replyText");
|
||||
SupportUI.initAttachments(document.getElementById('chatBox'), '图片暂时无法访问,请客户重新发送附件。');
|
||||
|
||||
function escapeText(value){
|
||||
return String(value == null ? "" : value).replace(/[&<>"']/g, function(character){
|
||||
|
||||
+77
-12
@@ -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 换行
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Text drafts stay in this visitor's browser, independently for each conversation.
|
||||
var ChatDrafts = {
|
||||
create(options){
|
||||
const scope = /^[0-9a-f]{64}$/.test(options.scope || '') ? options.scope : null;
|
||||
const prefix = scope ? 'baiyue-draft-v1:' + scope + ':' : null;
|
||||
const activeKey = scope ? 'baiyue-active-chat-v1:' + scope : null;
|
||||
const memory = new Map();
|
||||
const dirty = new Set();
|
||||
let current = 'new';
|
||||
function id(value){
|
||||
if(value === null || value === 'new') return 'new';
|
||||
const key = String(value);
|
||||
return /^[1-9][0-9]{0,18}$/.test(key) ? key : null;
|
||||
}
|
||||
function storage(name){
|
||||
try{ return options[name](); }catch(error){ return null; }
|
||||
}
|
||||
function read(value){
|
||||
const key = id(value);
|
||||
if(!key) return '';
|
||||
if(dirty.has(key)) return memory.get(key) || '';
|
||||
if(prefix){
|
||||
try{
|
||||
const saved = storage('storage').getItem(prefix + key);
|
||||
if(saved !== null){
|
||||
const data = JSON.parse(saved);
|
||||
if(data && data.version === 1 && typeof data.text === 'string'){
|
||||
memory.set(key, data.text);
|
||||
return data.text;
|
||||
}
|
||||
}
|
||||
memory.delete(key);
|
||||
return '';
|
||||
}catch(error){}
|
||||
}
|
||||
return memory.get(key) || '';
|
||||
}
|
||||
function write(value, text){
|
||||
const key = id(value);
|
||||
if(!key || typeof text !== 'string') return false;
|
||||
if(text) memory.set(key, text); else memory.delete(key);
|
||||
dirty.add(key);
|
||||
if(!prefix) return false;
|
||||
try{
|
||||
const target = storage('storage');
|
||||
if(text) target.setItem(prefix + key, JSON.stringify({version:1,text:text}));
|
||||
else target.removeItem(prefix + key);
|
||||
dirty.delete(key);
|
||||
return true;
|
||||
}catch(error){ return false; }
|
||||
}
|
||||
function active(value){
|
||||
if(arguments.length){
|
||||
current = id(value) || 'new';
|
||||
if(activeKey) try{ storage('tabStorage').setItem(activeKey, current); }catch(error){}
|
||||
}else if(activeKey){
|
||||
try{ const saved=storage('tabStorage').getItem(activeKey); if(saved !== null) current = id(saved) || current; }catch(error){}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function clear(){
|
||||
memory.clear();
|
||||
dirty.clear();
|
||||
if(prefix){
|
||||
try{
|
||||
const target = storage('storage');
|
||||
for(let index = target.length - 1; index >= 0; index--){
|
||||
const key = target.key(index);
|
||||
if(key && key.startsWith(prefix)) target.removeItem(key);
|
||||
}
|
||||
}catch(error){}
|
||||
}
|
||||
active('new');
|
||||
}
|
||||
return {read:read,write:write,active:active,remove:value=>write(value,''),clear:clear};
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,17 @@
|
||||
// Shared controls for the customer page and service workspace.
|
||||
var SupportUI = {
|
||||
initAttachments(container, label){
|
||||
container.addEventListener('error', function(event){
|
||||
const image = event.target;
|
||||
if(!image || image.tagName !== 'IMG' || !image.classList.contains('message-image') || image.dataset.unavailable) return;
|
||||
image.dataset.unavailable = '1';
|
||||
image.hidden = true;
|
||||
const hint = document.createElement('span');
|
||||
hint.className = 'attachment-unavailable';
|
||||
hint.textContent = typeof label === 'function' ? label() : label;
|
||||
image.after(hint);
|
||||
}, true);
|
||||
},
|
||||
async enableNotifications(button){
|
||||
if(!('Notification' in window)) return;
|
||||
try{
|
||||
|
||||
@@ -45,5 +45,5 @@ var VisitorSession = (function(){
|
||||
}
|
||||
return established ? send() : ensure().then(send);
|
||||
}
|
||||
return {fetch:request};
|
||||
return {fetch:request,localKey:localKey};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user