Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -372,6 +372,63 @@ def inject_custom_css():
|
|
| 372 |
|
| 373 |
inject_custom_css()
|
| 374 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
# 全局游客标识(session_state 在单次渲染中不变)
|
| 376 |
IS_GUEST = not bool(st.session_state.get("current_user"))
|
| 377 |
|
|
@@ -1709,11 +1766,13 @@ def _chat_fragment():
|
|
| 1709 |
q = st.session_state.pop("_quick_question")
|
| 1710 |
|
| 1711 |
with chat_box:
|
| 1712 |
-
for m in st.session_state.messages:
|
| 1713 |
with st.chat_message(m["role"]):
|
| 1714 |
-
if m["role"] == "assistant"
|
|
|
|
|
|
|
| 1715 |
st.markdown(
|
| 1716 |
-
m["content"] +
|
| 1717 |
unsafe_allow_html=True,
|
| 1718 |
)
|
| 1719 |
else:
|
|
@@ -1768,12 +1827,14 @@ def _chat_fragment():
|
|
| 1768 |
llm_answer(q, relevant_docs, _model_name, _web_on, kb_mode=_kb_mode, source_files=source_files)
|
| 1769 |
)
|
| 1770 |
meta_info = st.session_state.get("last_meta", "")
|
| 1771 |
-
#
|
| 1772 |
-
|
| 1773 |
-
|
| 1774 |
-
|
| 1775 |
-
|
| 1776 |
-
|
|
|
|
|
|
|
| 1777 |
st.session_state.messages.append(
|
| 1778 |
{"role": "assistant", "content": full_response, "meta": meta_info}
|
| 1779 |
)
|
|
|
|
| 372 |
|
| 373 |
inject_custom_css()
|
| 374 |
|
| 375 |
+
|
| 376 |
+
def _render_action_buttons(content, msg_id):
|
| 377 |
+
"""生成消息底部的复制/分享/播报按钮"""
|
| 378 |
+
# 转义内容用于 JavaScript
|
| 379 |
+
escaped = content.replace("\\", "\\\\").replace("`", "\\`").replace("$", "\\$")
|
| 380 |
+
return f'''
|
| 381 |
+
<div style="display:flex; gap:12px; margin-top:8px; padding-top:6px; border-top:1px solid #eee;">
|
| 382 |
+
<button onclick="copyText_{msg_id}()" style="background:none; border:none; cursor:pointer; color:#666; font-size:13px; padding:4px 8px; border-radius:4px;" onmouseover="this.style.background='#f0f0f0'" onmouseout="this.style.background='none'">📋 复制</button>
|
| 383 |
+
<button onclick="shareText_{msg_id}()" style="background:none; border:none; cursor:pointer; color:#666; font-size:13px; padding:4px 8px; border-radius:4px;" onmouseover="this.style.background='#f0f0f0'" onmouseout="this.style.background='none'">🔗 分享</button>
|
| 384 |
+
<button id="tts_btn_{msg_id}" onclick="speakText_{msg_id}()" style="background:none; border:none; cursor:pointer; color:#666; font-size:13px; padding:4px 8px; border-radius:4px;" onmouseover="this.style.background='#f0f0f0'" onmouseout="this.style.background='none'">🔊 播报</button>
|
| 385 |
+
</div>
|
| 386 |
+
<script>
|
| 387 |
+
const msgContent_{msg_id} = `{escaped}`;
|
| 388 |
+
let speaking_{msg_id} = false;
|
| 389 |
+
|
| 390 |
+
function copyText_{msg_id}() {{
|
| 391 |
+
navigator.clipboard.writeText(msgContent_{msg_id}).then(() => {{
|
| 392 |
+
alert('已复制到剪贴板');
|
| 393 |
+
}}).catch(() => {{
|
| 394 |
+
alert('复制失败,请手动复制');
|
| 395 |
+
}});
|
| 396 |
+
}}
|
| 397 |
+
|
| 398 |
+
function shareText_{msg_id}() {{
|
| 399 |
+
if (navigator.share) {{
|
| 400 |
+
navigator.share({{
|
| 401 |
+
title: '智答AI助手',
|
| 402 |
+
text: msgContent_{msg_id}
|
| 403 |
+
}}).catch(() => {{}});
|
| 404 |
+
}} else {{
|
| 405 |
+
navigator.clipboard.writeText(msgContent_{msg_id}).then(() => {{
|
| 406 |
+
alert('链接已复制,可粘贴分享');
|
| 407 |
+
}});
|
| 408 |
+
}}
|
| 409 |
+
}}
|
| 410 |
+
|
| 411 |
+
function speakText_{msg_id}() {{
|
| 412 |
+
const btn = document.getElementById('tts_btn_{msg_id}');
|
| 413 |
+
if (speaking_{msg_id}) {{
|
| 414 |
+
window.speechSynthesis.cancel();
|
| 415 |
+
speaking_{msg_id} = false;
|
| 416 |
+
btn.textContent = '🔊 播报';
|
| 417 |
+
}} else {{
|
| 418 |
+
const utterance = new SpeechSynthesisUtterance(msgContent_{msg_id});
|
| 419 |
+
utterance.lang = 'zh-CN';
|
| 420 |
+
utterance.rate = 1.0;
|
| 421 |
+
utterance.onend = () => {{ speaking_{msg_id} = false; btn.textContent = '🔊 播报'; }};
|
| 422 |
+
utterance.onerror = () => {{ speaking_{msg_id} = false; btn.textContent = '🔊 播报'; }};
|
| 423 |
+
window.speechSynthesis.speak(utterance);
|
| 424 |
+
speaking_{msg_id} = true;
|
| 425 |
+
btn.textContent = '⏹️ 停止';
|
| 426 |
+
}}
|
| 427 |
+
}}
|
| 428 |
+
</script>
|
| 429 |
+
'''
|
| 430 |
+
|
| 431 |
+
|
| 432 |
# 全局游客标识(session_state 在单次渲染中不变)
|
| 433 |
IS_GUEST = not bool(st.session_state.get("current_user"))
|
| 434 |
|
|
|
|
| 1766 |
q = st.session_state.pop("_quick_question")
|
| 1767 |
|
| 1768 |
with chat_box:
|
| 1769 |
+
for idx, m in enumerate(st.session_state.messages):
|
| 1770 |
with st.chat_message(m["role"]):
|
| 1771 |
+
if m["role"] == "assistant":
|
| 1772 |
+
meta_html = f'\n\n<span style="color:#999;font-size:12px;">{m["meta"]}</span>' if m.get("meta") else ""
|
| 1773 |
+
action_btns = _render_action_buttons(m["content"], f"hist_{idx}")
|
| 1774 |
st.markdown(
|
| 1775 |
+
m["content"] + meta_html + action_btns,
|
| 1776 |
unsafe_allow_html=True,
|
| 1777 |
)
|
| 1778 |
else:
|
|
|
|
| 1827 |
llm_answer(q, relevant_docs, _model_name, _web_on, kb_mode=_kb_mode, source_files=source_files)
|
| 1828 |
)
|
| 1829 |
meta_info = st.session_state.get("last_meta", "")
|
| 1830 |
+
# 生成工具按钮(使用当前消息数作为ID)
|
| 1831 |
+
msg_idx = len(st.session_state.messages)
|
| 1832 |
+
action_btns = _render_action_buttons(full_response, f"new_{msg_idx}")
|
| 1833 |
+
meta_html = f'\n\n<span style="color:#999;font-size:12px;">{meta_info}</span>' if meta_info else ""
|
| 1834 |
+
response_container.markdown(
|
| 1835 |
+
full_response + meta_html + action_btns,
|
| 1836 |
+
unsafe_allow_html=True,
|
| 1837 |
+
)
|
| 1838 |
st.session_state.messages.append(
|
| 1839 |
{"role": "assistant", "content": full_response, "meta": meta_info}
|
| 1840 |
)
|