修复评价统计、留言校验和聊天操作反馈

This commit is contained in:
2026-10-30 14:38:25 +08:00
parent b8f6ca586d
commit 78797d8eca
9 changed files with 249 additions and 42 deletions
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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};
}
};