修复评价统计、留言校验和聊天操作反馈
This commit is contained in:
@@ -133,6 +133,8 @@ body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; }
|
||||
.msg-actions { display: flex; gap: 5px; flex-wrap: wrap; padding-top: 12px; margin-top: 10px; border-top: 1px solid var(--border); }
|
||||
.msg-action-btn { min-height: 30px; border: 0; padding: 3px 8px; color: var(--secondary); background: transparent; border-radius: 5px; font-size: 12px; }
|
||||
.msg-action-btn:hover, .msg-action-btn.voted { background: var(--surface-hover); color: var(--brand-text); }
|
||||
.vote-status { align-self: center; color: var(--muted); font-size: 11px; }
|
||||
.vote-status.is-error { color: var(--danger); }
|
||||
.error-bubble { border-color: #e9b4bb; background: var(--danger-soft); color: var(--danger); }
|
||||
.err-line { margin-bottom: 9px; }
|
||||
.retry-btn { border: 1px solid currentColor; border-radius: 6px; padding: 4px 9px; color: inherit; background: transparent; font-size: 11px; }
|
||||
|
||||
+67
-21
@@ -28,7 +28,7 @@ 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(); });
|
||||
const emojiPicker = SupportUI.initEmoji(document.getElementById('emoji-toggle'), document.getElementById('emoji-panel'), inputDom, function(){ autoGrow(); });
|
||||
document.getElementById('notifications-toggle').onclick = function(){ SupportUI.enableNotifications(this); };
|
||||
|
||||
function setComposerStatus(message, isError){
|
||||
@@ -245,6 +245,9 @@ const I18N = {
|
||||
stop: "停止",
|
||||
copy: "复制",
|
||||
copied: "已复制",
|
||||
copyFailed: "复制失败",
|
||||
voteUp: "这条回复有帮助",
|
||||
voteDown: "这条回复没有帮助",
|
||||
retry: "重答",
|
||||
retryAIOnly: "请先返回 AI 问答,再重答这条消息。",
|
||||
clickRetry: "点击重试",
|
||||
@@ -355,6 +358,9 @@ const I18N = {
|
||||
stop: "Stop",
|
||||
copy: "Copy",
|
||||
copied: "Copied",
|
||||
copyFailed: "Copy failed",
|
||||
voteUp: "This reply was helpful",
|
||||
voteDown: "This reply was not helpful",
|
||||
retry: "Regenerate",
|
||||
retryAIOnly: "Return to AI chat before regenerating this answer.",
|
||||
clickRetry: "Retry",
|
||||
@@ -483,6 +489,7 @@ function applyLanguage(lang){
|
||||
const n = b.dataset.action;
|
||||
if(n === "copy") b.textContent = dict.copy;
|
||||
if(n === "retry") b.textContent = dict.retry;
|
||||
if(n === 'voteUp' || n === 'voteDown') b.setAttribute('aria-label', dict[n]);
|
||||
});
|
||||
document.querySelectorAll(".retry-btn").forEach(function(b){
|
||||
if(!b.dataset.keep) b.textContent = dict.clickRetry;
|
||||
@@ -824,6 +831,7 @@ function ensureConv(){
|
||||
}
|
||||
|
||||
function resetConversation(id){
|
||||
emojiPicker.close();
|
||||
if(voiceInput) voiceInput.cancel();
|
||||
if(cancelActive){ cancelActive(); cancelActive = null; }
|
||||
if(sseInstance){ sseInstance.close(); sseInstance = null; }
|
||||
@@ -846,6 +854,7 @@ function openConversation(id){
|
||||
if(openingConv === 'clearing') return;
|
||||
if(openingConv === id) return; // 防连点
|
||||
const version = resetConversation(id);
|
||||
if(isNarrow()){ sideOpen = false; renderSidebar(); }
|
||||
openingConv = id;
|
||||
setComposerStatus(t('historyLoading'));
|
||||
renderSideHistory();
|
||||
@@ -1185,7 +1194,7 @@ async function sendHumanMessage(content){
|
||||
async function leaveOfflineMessage(){
|
||||
const version = conversationVersion;
|
||||
addMsg(t('humanHours'), false);
|
||||
const message = window.prompt(t('offlinePrompt'));
|
||||
const message = (window.prompt(t('offlinePrompt')) || '').trim();
|
||||
if(!message) return;
|
||||
try{
|
||||
const data = await humanRequest('/api/ticket', {content:message, phone:''});
|
||||
@@ -1193,8 +1202,8 @@ async function leaveOfflineMessage(){
|
||||
if(version === conversationVersion) addMsg(t('offlineSaved'), false);
|
||||
}catch(error){
|
||||
if(version !== conversationVersion) return;
|
||||
if(!inputDom.value) inputDom.value = message;
|
||||
setComposerStatus(t('networkFail'), true);
|
||||
if(!inputDom.value){ inputDom.value = message; autoGrow(); }
|
||||
setComposerStatus(error.status === 400 ? error.message : t('networkFail'), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1391,26 +1400,34 @@ function attachActions(bubble, rawText, question, showRetry){
|
||||
copyBtn.dataset.action = "copy";
|
||||
copyBtn.textContent = t("copy");
|
||||
copyBtn.onclick = function(){
|
||||
if(copyBtn.disabled) return;
|
||||
copyBtn.disabled = true;
|
||||
const plain = mdToPlain(rawText);
|
||||
const done = ()=> {
|
||||
copyBtn.textContent = t("copied");
|
||||
setTimeout(()=>{ copyBtn.textContent = t("copy"); }, 1200);
|
||||
const done = function(copied){
|
||||
copyBtn.disabled = false;
|
||||
copyBtn.textContent = t(copied ? 'copied' : 'copyFailed');
|
||||
setTimeout(()=>{ copyBtn.textContent = t("copy"); }, copied ? 1200 : 3000);
|
||||
};
|
||||
if(navigator.clipboard && navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(plain).then(done).catch(fallback);
|
||||
return navigator.clipboard.writeText(plain).then(()=>done(true)).catch(fallback);
|
||||
}else{
|
||||
fallback();
|
||||
return fallback();
|
||||
}
|
||||
function fallback(){
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = plain;
|
||||
ta.readOnly = true;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.opacity = "0";
|
||||
const previousFocus = document.activeElement;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try{ document.execCommand("copy"); }catch(e){}
|
||||
document.body.removeChild(ta);
|
||||
done();
|
||||
let copied = false;
|
||||
try{ ta.focus({preventScroll:true}); ta.select(); copied = Boolean(document.execCommand("copy")); }catch(e){}
|
||||
finally{
|
||||
ta.remove();
|
||||
if(previousFocus && previousFocus.isConnected) previousFocus.focus({preventScroll:true});
|
||||
}
|
||||
done(copied);
|
||||
}
|
||||
};
|
||||
bar.appendChild(copyBtn);
|
||||
@@ -1425,23 +1442,52 @@ function attachActions(bubble, rawText, question, showRetry){
|
||||
}
|
||||
|
||||
// 👍/👎 反馈
|
||||
let votePending = false;
|
||||
const voteButtons = [];
|
||||
const voteStatus = document.createElement('span');
|
||||
voteStatus.className = 'vote-status';
|
||||
voteStatus.setAttribute('role', 'status');
|
||||
function addVote(vote, label){
|
||||
const b = document.createElement("button");
|
||||
b.className = "msg-action-btn";
|
||||
b.textContent = label;
|
||||
b.onclick = function(){
|
||||
if(b.classList.contains("voted")) return;
|
||||
bar.querySelectorAll(".msg-action-btn").forEach(function(x){ x.classList.remove("voted"); });
|
||||
b.classList.add("voted");
|
||||
fetch("/api/feedback", {
|
||||
method:"POST", headers:{"Content-Type":"application/json"},
|
||||
body: JSON.stringify({question: question, answer: rawText, vote: vote})
|
||||
}).catch(function(){});
|
||||
b.dataset.action = vote === 'up' ? 'voteUp' : 'voteDown';
|
||||
b.setAttribute('aria-label', t(vote === 'up' ? 'voteUp' : 'voteDown'));
|
||||
b.setAttribute('aria-pressed', 'false');
|
||||
b.onclick = async function(){
|
||||
if(votePending || b.classList.contains('voted')) return;
|
||||
votePending = true;
|
||||
voteButtons.forEach(button => { button.disabled = true; });
|
||||
voteStatus.textContent = '';
|
||||
voteStatus.removeAttribute('data-i18n');
|
||||
voteStatus.classList.remove('is-error');
|
||||
try{
|
||||
const data = await requestJson('/api/feedback', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({question:question, answer:rawText, vote:vote})
|
||||
});
|
||||
if(!data.ok) throw new Error('Feedback failed');
|
||||
voteButtons.forEach(function(button){
|
||||
button.classList.toggle('voted', button === b);
|
||||
button.setAttribute('aria-pressed', String(button === b));
|
||||
});
|
||||
voteStatus.textContent = t('ratingThanks');
|
||||
voteStatus.setAttribute('data-i18n', 'ratingThanks');
|
||||
}catch(error){
|
||||
voteStatus.textContent = t('ratingFailed');
|
||||
voteStatus.setAttribute('data-i18n', 'ratingFailed');
|
||||
voteStatus.classList.add('is-error');
|
||||
}finally{
|
||||
votePending = false;
|
||||
voteButtons.forEach(button => { button.disabled = false; });
|
||||
}
|
||||
};
|
||||
voteButtons.push(b);
|
||||
bar.appendChild(b);
|
||||
}
|
||||
addVote("up", "👍");
|
||||
addVote("down", "👎");
|
||||
bar.appendChild(voteStatus);
|
||||
|
||||
bubble.appendChild(bar);
|
||||
}
|
||||
|
||||
+16
-8
@@ -58,6 +58,10 @@ var SupportUI = {
|
||||
},
|
||||
initEmoji(button, panel, input, onInsert){
|
||||
const emojis = ['😊', '😂', '👍', '❤️', '🙏', '😅', '😢', '🎉', '📦', '🚚'];
|
||||
function close(){
|
||||
panel.hidden = true;
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
button.onclick = function(){
|
||||
panel.hidden = !panel.hidden;
|
||||
button.setAttribute('aria-expanded', String(!panel.hidden));
|
||||
@@ -70,20 +74,24 @@ var SupportUI = {
|
||||
const start = input.selectionStart == null ? input.value.length : input.selectionStart;
|
||||
const end = input.selectionEnd == null ? start : input.selectionEnd;
|
||||
input.value = input.value.slice(0, start) + emoji + input.value.slice(end);
|
||||
panel.hidden = true;
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
input.focus();
|
||||
close();
|
||||
input.focus({preventScroll:true});
|
||||
input.setSelectionRange(start + emoji.length, start + emoji.length);
|
||||
if(onInsert) onInsert();
|
||||
};
|
||||
panel.appendChild(choice);
|
||||
});
|
||||
panel.addEventListener('keydown', function(event){
|
||||
if(event.key === 'Escape'){
|
||||
panel.hidden = true;
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
button.focus();
|
||||
['pointerdown','mousedown'].forEach(function(name){
|
||||
document.addEventListener(name, function(event){
|
||||
if(!panel.hidden && !panel.contains(event.target) && !button.contains(event.target)) close();
|
||||
});
|
||||
});
|
||||
document.addEventListener('keydown', function(event){
|
||||
if(event.key === 'Escape' && !panel.hidden){
|
||||
close();
|
||||
button.focus({preventScroll:true});
|
||||
}
|
||||
});
|
||||
return {close:close};
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user