|
@@ -0,0 +1,680 @@
|
|
|
+import os
|
|
|
+import time
|
|
|
+import pandas as pd
|
|
|
+import streamlit as st
|
|
|
+from qa_content import filter_context_messages, analyze_customer_messages
|
|
|
+from qa_info import analyze_data
|
|
|
+
|
|
|
+# 设置页面配置
|
|
|
+st.set_page_config(
|
|
|
+ page_title="AI对话分析系统",
|
|
|
+ page_icon="💬",
|
|
|
+ layout="wide",
|
|
|
+ initial_sidebar_state="expanded"
|
|
|
+)
|
|
|
+
|
|
|
+# 自定义CSS样式
|
|
|
+st.markdown("""
|
|
|
+<style>
|
|
|
+ /* 导入字体 */
|
|
|
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
|
+
|
|
|
+ /* 全局样式 */
|
|
|
+ html, body, [class*="css"] {
|
|
|
+ font-family: 'Inter', sans-serif;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 设置基础变量 */
|
|
|
+ :root {
|
|
|
+ --primary: #2e7eed;
|
|
|
+ --primary-light: #e8f1fd;
|
|
|
+ --secondary: #6c757d;
|
|
|
+ --success: #28a745;
|
|
|
+ --danger: #dc3545;
|
|
|
+ --warning: #ffc107;
|
|
|
+ --info: #17a2b8;
|
|
|
+ --dark: #343a40;
|
|
|
+ --light: #f8f9fa;
|
|
|
+ --white: #ffffff;
|
|
|
+ --border: #e9ecef;
|
|
|
+ --shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 调整整体间距 */
|
|
|
+ .block-container {
|
|
|
+ padding-top: 1rem;
|
|
|
+ padding-bottom: 1rem;
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .main {
|
|
|
+ background-color: #f8f9fa;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 侧边栏样式 - 加宽 */
|
|
|
+ section[data-testid="stSidebar"] {
|
|
|
+ min-width: 330px !important;
|
|
|
+ width: 330px !important;
|
|
|
+ background-color: var(--black);
|
|
|
+ border-right: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 侧边栏内部样式 */
|
|
|
+ .css-hxt7ib {
|
|
|
+ padding-top: 1rem;
|
|
|
+ padding-left: 1rem;
|
|
|
+ padding-right: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 标题样式 */
|
|
|
+ .sidebar-header {
|
|
|
+ font-size: 1.2rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--dark);
|
|
|
+ margin-bottom: 0.75rem;
|
|
|
+ padding-bottom: 0.75rem;
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-section {
|
|
|
+ font-size: 0.9rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--dark);
|
|
|
+ margin-top: 1rem;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 主内容标题 */
|
|
|
+ .main-header {
|
|
|
+ font-size: 1.2rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--dark);
|
|
|
+ margin-bottom: 1rem;
|
|
|
+ padding-bottom: 0.5rem;
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 卡片样式 */
|
|
|
+ .card {
|
|
|
+ background-color: var(--white);
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 1rem;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ margin-bottom: 1rem;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ font-size: 0.9rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--dark);
|
|
|
+ margin-bottom: 0.75rem;
|
|
|
+ padding-bottom: 0.5rem;
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 聊天容器 */
|
|
|
+ .chat-container {
|
|
|
+ height: 75vh;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 1rem;
|
|
|
+ background: var(--black);
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 用户消息样式 */
|
|
|
+ .user-message {
|
|
|
+ display: flex;
|
|
|
+ margin-bottom: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-avatar {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ background-color: var(--primary);
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: var(--white);
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 0.8rem;
|
|
|
+ margin-right: 0.5rem;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-content {
|
|
|
+ background-color: var(--primary-light);
|
|
|
+ padding: 0.6rem 0.8rem;
|
|
|
+ border-radius: 0.8rem 0.8rem 0.8rem 0.2rem;
|
|
|
+ max-width: 80%;
|
|
|
+ font-size: 0.85rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-time {
|
|
|
+ font-size: 0.7rem;
|
|
|
+ color: var(--secondary);
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .intent-content {
|
|
|
+ font-size: 0.7rem;
|
|
|
+ color: var(--secondary);
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .call-intent-tag {
|
|
|
+ background-color: #fff0f0;
|
|
|
+ color: var(--danger);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 客服消息样式 */
|
|
|
+ .service-message {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-bottom: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .service-avatar {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ background-color: var(--danger);
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: var(--white);
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 0.8rem;
|
|
|
+ margin-left: 0.5rem;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .service-content {
|
|
|
+ background-color: var(--light);
|
|
|
+ padding: 0.6rem 0.8rem;
|
|
|
+ border-radius: 0.8rem 0.8rem 0.2rem 0.8rem;
|
|
|
+ max-width: 80%;
|
|
|
+ text-align: left;
|
|
|
+ font-size: 0.85rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tag {
|
|
|
+ display: inline-block;
|
|
|
+ font-size: 0.65rem;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 0.15rem 0.4rem;
|
|
|
+ border-radius: 3px;
|
|
|
+ margin-left: 0.4rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .call-type-tag {
|
|
|
+ background-color: #fff0f0;
|
|
|
+ color: var(--danger);
|
|
|
+ }
|
|
|
+
|
|
|
+ .bot-tag {
|
|
|
+ background-color: #fff0f0;
|
|
|
+ color: var(--danger);
|
|
|
+ }
|
|
|
+
|
|
|
+ .ok-tag {
|
|
|
+ background-color: #f0fff0;
|
|
|
+ color: var(--success);
|
|
|
+ }
|
|
|
+
|
|
|
+ .warning-tag {
|
|
|
+ background-color: #fffff0;
|
|
|
+ color: var(--warning);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 空状态提示 */
|
|
|
+ .empty-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 70vh;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ color: var(--secondary);
|
|
|
+ padding: 2rem;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-icon {
|
|
|
+ font-size: 2.5rem;
|
|
|
+ margin-bottom: 1rem;
|
|
|
+ color: #dce0e5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-title {
|
|
|
+ font-size: 1.2rem;
|
|
|
+ font-weight: 500;
|
|
|
+ color: var(--dark);
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-desc {
|
|
|
+ font-size: 0.9rem;
|
|
|
+ color: var(--secondary);
|
|
|
+ max-width: 400px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 数据指标样式 */
|
|
|
+ .metric-container {
|
|
|
+ background-color: var(--white);
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 0.75rem;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ text-align: center;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-value {
|
|
|
+ font-size: 1.4rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--primary);
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-label {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ color: var(--secondary);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 修改组件样式 */
|
|
|
+ .sidebar-widget {
|
|
|
+ margin-bottom: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-text {
|
|
|
+ font-size: 0.8rem;
|
|
|
+ color: var(--secondary);
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 修改分割线样式 */
|
|
|
+ hr {
|
|
|
+ margin: 1rem 0;
|
|
|
+ border-color: var(--border);
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 表格样式 */
|
|
|
+ .dataframe {
|
|
|
+ font-size: 0.85rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 选项卡容器 */
|
|
|
+ [data-testid="stHorizontalBlock"] > div {
|
|
|
+ flex: 1;
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+</style>
|
|
|
+""", unsafe_allow_html=True)
|
|
|
+
|
|
|
+# 初始化session_state
|
|
|
+if 'view_mode' not in st.session_state:
|
|
|
+ st.session_state.view_mode = "chat_analysis" # 默认视图模式:'chat_analysis' 或 'stats_analysis'
|
|
|
+
|
|
|
+if 'chat_data' not in st.session_state:
|
|
|
+ st.session_state.chat_data = []
|
|
|
+
|
|
|
+if 'selected_user' not in st.session_state:
|
|
|
+ st.session_state.selected_user = None
|
|
|
+
|
|
|
+if 'user_list' not in st.session_state:
|
|
|
+ st.session_state.user_list = []
|
|
|
+
|
|
|
+if 'stats_data' not in st.session_state:
|
|
|
+ st.session_state.stats_data = {}
|
|
|
+
|
|
|
+if 'file_processed' not in st.session_state:
|
|
|
+ st.session_state.file_processed = False
|
|
|
+
|
|
|
+# 侧边栏操作区域
|
|
|
+with st.sidebar:
|
|
|
+ st.markdown("<div class='sidebar-header'>AI对话分析系统</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 视图选择
|
|
|
+ st.markdown("<div class='sidebar-section'>选择查看内容</div>", unsafe_allow_html=True)
|
|
|
+ view_options = ["历史对话内容", "历史对话数量"]
|
|
|
+ selected_view = st.radio("",
|
|
|
+ options=view_options,
|
|
|
+ index=0 if st.session_state.view_mode == "chat_analysis" else 1,
|
|
|
+ label_visibility="collapsed")
|
|
|
+
|
|
|
+ # 根据选择更新视图模式
|
|
|
+ if selected_view == "历史对话内容" and st.session_state.view_mode != "chat_analysis":
|
|
|
+ st.session_state.view_mode = "chat_analysis"
|
|
|
+ st.rerun()
|
|
|
+ elif selected_view == "历史对话数量" and st.session_state.view_mode != "stats_analysis":
|
|
|
+ st.session_state.view_mode = "stats_analysis"
|
|
|
+ st.rerun()
|
|
|
+
|
|
|
+ # 分割线
|
|
|
+ st.markdown("<hr>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 上传文件部分 - 同时处理两种分析
|
|
|
+ st.markdown("<div class='sidebar-section'>上传数据分析</div>", unsafe_allow_html=True)
|
|
|
+ # st.markdown("<div class='sidebar-text'>上传Excel文件进行全面分析</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 文件上传
|
|
|
+ uploaded_file = st.file_uploader("上传Excel文件", type=["xlsx", "xls"])
|
|
|
+
|
|
|
+
|
|
|
+ # 修改临时文件路径,确保在Docker环境中有权限写入
|
|
|
+ TEMP_FILE_DIR = "./temp_files"
|
|
|
+ os.makedirs(TEMP_FILE_DIR, exist_ok=True)
|
|
|
+
|
|
|
+
|
|
|
+ # 如果有上传文件并且未处理
|
|
|
+ if uploaded_file is not None and not st.session_state.file_processed:
|
|
|
+ # 显示处理状态
|
|
|
+ st.markdown("<div class='sidebar-text'>正在处理文件...</div>", unsafe_allow_html=True)
|
|
|
+ progress = st.progress(0)
|
|
|
+
|
|
|
+ # # 保存上传的文件
|
|
|
+ # with open("temp_analysis_file.xlsx", "wb") as f:
|
|
|
+ # f.write(uploaded_file.getvalue())
|
|
|
+
|
|
|
+ # 保存上传的文件
|
|
|
+ temp_file_path = os.path.join(TEMP_FILE_DIR, "temp_analysis_file.xlsx")
|
|
|
+ with open(temp_file_path, "wb") as f:
|
|
|
+ f.write(uploaded_file.getvalue())
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 更新进度
|
|
|
+ progress.progress(20)
|
|
|
+ time.sleep(0.2)
|
|
|
+
|
|
|
+ # 分析对话数据
|
|
|
+ # results = analyze_customer_messages("temp_analysis_file.xlsx")
|
|
|
+ results = analyze_customer_messages(temp_file_path)
|
|
|
+
|
|
|
+ # 更新进度
|
|
|
+ progress.progress(40)
|
|
|
+ time.sleep(0.2)
|
|
|
+
|
|
|
+ # 筛选不满意对话结果
|
|
|
+ filtered_results = filter_context_messages(results)
|
|
|
+
|
|
|
+ # 更新进度
|
|
|
+ progress.progress(60)
|
|
|
+ time.sleep(0.2)
|
|
|
+
|
|
|
+ # 分析统计数据
|
|
|
+ # stats_result = analyze_data("temp_analysis_file.xlsx")
|
|
|
+ stats_result = analyze_data(temp_file_path)
|
|
|
+
|
|
|
+ # 更新进度
|
|
|
+ progress.progress(90)
|
|
|
+ time.sleep(0.2)
|
|
|
+
|
|
|
+ # 将分析结果保存到session_state中
|
|
|
+ if filtered_results:
|
|
|
+ st.session_state.chat_data = filtered_results
|
|
|
+ st.session_state.user_list = [entry["用户"] for entry in filtered_results]
|
|
|
+ st.session_state.selected_user = st.session_state.user_list[0]
|
|
|
+
|
|
|
+ st.session_state.stats_data = stats_result
|
|
|
+
|
|
|
+ # 处理完成
|
|
|
+ progress.progress(100)
|
|
|
+ time.sleep(0.3)
|
|
|
+ progress.empty()
|
|
|
+
|
|
|
+ st.session_state.file_processed = True
|
|
|
+
|
|
|
+ # 显示成功消息
|
|
|
+ if filtered_results:
|
|
|
+ chat_msg = f"✓ 已分析 {len(filtered_results)} 位用户的对话"
|
|
|
+ else:
|
|
|
+ chat_msg = "未找到符合条件的对话记录"
|
|
|
+
|
|
|
+ st.success(f"数据分析完成!\n{chat_msg}")
|
|
|
+ st.rerun()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ progress.empty()
|
|
|
+ st.error(f"处理文件出错: {str(e)}")
|
|
|
+ st.session_state.file_processed = True
|
|
|
+
|
|
|
+ # 如果已有对话数据,显示用户选择列表
|
|
|
+ if st.session_state.view_mode == "chat_analysis" and st.session_state.user_list:
|
|
|
+ st.markdown("<hr>", unsafe_allow_html=True)
|
|
|
+ st.markdown("<div class='sidebar-section'>选择用户</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ selected_user = st.selectbox(
|
|
|
+ "选择要查看的用户",
|
|
|
+ options=st.session_state.user_list,
|
|
|
+ index=st.session_state.user_list.index(st.session_state.selected_user) if st.session_state.selected_user in st.session_state.user_list else 0
|
|
|
+ )
|
|
|
+
|
|
|
+ if selected_user != st.session_state.selected_user:
|
|
|
+ st.session_state.selected_user = selected_user
|
|
|
+ st.rerun()
|
|
|
+
|
|
|
+ # 重置按钮(始终显示在侧边栏底部)
|
|
|
+ st.markdown("<hr>", unsafe_allow_html=True)
|
|
|
+ if st.button("重置分析数据", type="primary", use_container_width=True):
|
|
|
+ for key in ['chat_data', 'selected_user', 'user_list', 'file_processed', 'stats_data']:
|
|
|
+ if key in st.session_state:
|
|
|
+ del st.session_state[key]
|
|
|
+
|
|
|
+ # # 尝试删除临时文件
|
|
|
+ # try:
|
|
|
+ # if os.path.exists("temp_analysis_file.xlsx"):
|
|
|
+ # os.remove("temp_analysis_file.xlsx")
|
|
|
+ # except:
|
|
|
+ # pass
|
|
|
+
|
|
|
+ # 尝试删除临时文件
|
|
|
+ try:
|
|
|
+ temp_file_path = os.path.join(TEMP_FILE_DIR, "temp_analysis_file.xlsx")
|
|
|
+ if os.path.exists(temp_file_path):
|
|
|
+ os.remove(temp_file_path)
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ st.rerun()
|
|
|
+
|
|
|
+ # 版权信息
|
|
|
+ # st.markdown("<div style='position: absolute; bottom: 0.5rem; font-size: 0.7rem; opacity: 0.5; padding: 0.5rem; color: #7f879a;'>© 2024 AI对话分析系统 v1.0</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+# 主界面 - 仅显示结果
|
|
|
+if st.session_state.view_mode == "chat_analysis":
|
|
|
+ # 历史对话内容结果显示
|
|
|
+ st.markdown("<div class='main-header'>历史对话内容</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 检查是否有数据可显示
|
|
|
+ if not st.session_state.chat_data or not st.session_state.selected_user:
|
|
|
+ # 显示空状态
|
|
|
+ st.markdown("<div class='empty-state'>", unsafe_allow_html=True)
|
|
|
+ st.markdown("<div class='empty-icon'>💬</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("<div class='empty-title'>欢迎使用历史对话内容</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("<div class='empty-desc'>请在左侧边栏上传Excel文件,系统会自动分析不满意对话并在此处显示结果。</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("</div>", unsafe_allow_html=True)
|
|
|
+ else:
|
|
|
+ # 显示用户信息
|
|
|
+ st.markdown(f"<div class='card'>正在查看用户 <b>{st.session_state.selected_user}</b> 的对话记录</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 找到当前选择的用户对话
|
|
|
+ selected_chat = None
|
|
|
+ for entry in st.session_state.chat_data:
|
|
|
+ if entry["用户"] == st.session_state.selected_user:
|
|
|
+ selected_chat = entry["消息列表"]
|
|
|
+ break
|
|
|
+
|
|
|
+ if selected_chat:
|
|
|
+ # 显示所有消息
|
|
|
+ for message in selected_chat:
|
|
|
+ try:
|
|
|
+ user = message.get('客户用户名', '未知')
|
|
|
+ servicer = message.get('客服用户名', '未知')
|
|
|
+ content = message.get("消息内容", "")
|
|
|
+ direction = message.get("对话方向", "")
|
|
|
+ time_str = message.get("信息时间", "")
|
|
|
+ call_type = message.get("自动回复类型", "")
|
|
|
+ call_intent = message.get('自动回复意图', '')
|
|
|
+
|
|
|
+ call_type_tag = f'<span class="tag call-type-tag">{call_type}</span>'
|
|
|
+ call_intent_tag = f'<span class="tag call-intent-tag">{call_intent}</span>'
|
|
|
+
|
|
|
+ # 如果消息为空,则跳过
|
|
|
+ if not content:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 根据对话方向判断是客户还是客服
|
|
|
+ if direction == "呼入": # 客户消息
|
|
|
+ st.markdown(f"""
|
|
|
+ <div class="user-message">
|
|
|
+ <div class="user-avatar">{user[:1]}</div>
|
|
|
+ <div class="user-content">
|
|
|
+ <div class="message-time">{time_str}{call_type_tag}</div>
|
|
|
+ {content}
|
|
|
+ <div class="intent-content">{call_intent_tag}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ """, unsafe_allow_html=True)
|
|
|
+ else: # 客服消息(呼出)
|
|
|
+ # 判断是否是机器人回复
|
|
|
+ is_bot = str(message.get("机器人自动回复状态", "")) == "是"
|
|
|
+ is_ok = message.get('分类是否正确', "")
|
|
|
+
|
|
|
+ # 标签
|
|
|
+ bot_tag = '<span class="tag bot-tag">机器人</span>' if is_bot else ''
|
|
|
+
|
|
|
+ # 根据分类是否正确显示不同颜色的标签
|
|
|
+ ok_tag = ""
|
|
|
+ if is_bot and is_ok == "满意":
|
|
|
+ ok_tag = '<span class="tag ok-tag">满意</span>'
|
|
|
+ elif is_bot and is_ok == "不满意":
|
|
|
+ ok_tag = '<span class="tag bot-tag">不满意</span>'
|
|
|
+ elif is_bot and is_ok == "未评价":
|
|
|
+ ok_tag = '<span class="tag warning-tag">未评价</span>'
|
|
|
+
|
|
|
+ st.markdown(f"""
|
|
|
+ <div class="service-message">
|
|
|
+ <div class="service-content">
|
|
|
+ <div class="message-time">{time_str} {bot_tag} {ok_tag}</div>
|
|
|
+ {content}
|
|
|
+ </div>
|
|
|
+ <div class="service-avatar">{servicer[:]}</div>
|
|
|
+ </div>
|
|
|
+ """, unsafe_allow_html=True)
|
|
|
+ except Exception as e:
|
|
|
+ st.error(f"显示消息出错: {str(e)}")
|
|
|
+ else:
|
|
|
+ st.markdown('<div class="empty-state"><div class="empty-title">未找到对话记录</div></div>', unsafe_allow_html=True)
|
|
|
+
|
|
|
+ st.markdown("</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+else: # stats_analysis 模式
|
|
|
+ # 历史对话数量结果显示
|
|
|
+ st.markdown("<div class='main-header'>历史对话数量分析</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 检查是否有数据可显示
|
|
|
+ if not st.session_state.stats_data:
|
|
|
+ # 显示空状态
|
|
|
+ st.markdown("<div class='empty-state'>", unsafe_allow_html=True)
|
|
|
+ st.markdown("<div class='empty-icon'>📊</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("<div class='empty-title'>欢迎使用历史对话数量</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("<div class='empty-desc'>请在左侧边栏上传Excel文件,系统会自动分析对话数据并在此处显示统计结果。</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("</div>", unsafe_allow_html=True)
|
|
|
+ else:
|
|
|
+ # 显示统计摘要 - 关键指标
|
|
|
+ col_metrics = st.columns(4)
|
|
|
+
|
|
|
+ with col_metrics[0]:
|
|
|
+ st.markdown("<div class='metric-container'>", unsafe_allow_html=True)
|
|
|
+ st.markdown(f"<div class='metric-value'>{st.session_state.stats_data.get('总呼出', 0)}</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown(f"<div class='metric-label'>总呼出数量</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ with col_metrics[1]:
|
|
|
+ st.markdown("<div class='metric-container'>", unsafe_allow_html=True)
|
|
|
+ st.markdown(f"<div class='metric-value'>{st.session_state.stats_data.get('AI呼出', 0)}</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown(f"<div class='metric-label'>AI对话数量</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ with col_metrics[2]:
|
|
|
+ st.markdown("<div class='metric-container'>", unsafe_allow_html=True)
|
|
|
+ st.markdown(f"<div class='metric-value'>{st.session_state.stats_data.get('满意率', '0%')}</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown(f"<div class='metric-label'>满意率</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ with col_metrics[3]:
|
|
|
+ st.markdown("<div class='metric-container'>", unsafe_allow_html=True)
|
|
|
+ st.markdown(f"<div class='metric-value'>{st.session_state.stats_data.get('未评价率', '0%')}</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown(f"<div class='metric-label'>未评价率</div>", unsafe_allow_html=True)
|
|
|
+ st.markdown("</div>", unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 详细统计数据
|
|
|
+ st.markdown("<div style='height: 20px;'></div>", unsafe_allow_html=True) # 间距
|
|
|
+
|
|
|
+ col1, col2 = st.columns(2)
|
|
|
+
|
|
|
+ with col1:
|
|
|
+ st.markdown('<div class="card">', unsafe_allow_html=True)
|
|
|
+ st.markdown('<div class="card-header">通话统计</div>', unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 使用表格呈现
|
|
|
+ data = [
|
|
|
+ ["总呼出数量", st.session_state.stats_data.get('总呼出', 0)],
|
|
|
+ ["AI呼出数量", st.session_state.stats_data.get('AI呼出', 0)],
|
|
|
+ ["人工呼出数量", st.session_state.stats_data.get('人呼出', 0)]
|
|
|
+ ]
|
|
|
+
|
|
|
+ # 添加AI占比
|
|
|
+ total = st.session_state.stats_data.get('总呼出', 0)
|
|
|
+ ai = st.session_state.stats_data.get('AI呼出', 0)
|
|
|
+ ai_percentage = f"{(ai/total*100):.1f}%" if total > 0 else "0%"
|
|
|
+ data.append(["AI占比", ai_percentage])
|
|
|
+
|
|
|
+ # 创建DataFrame并显示
|
|
|
+ df1 = pd.DataFrame(data, columns=["指标", "数值"])
|
|
|
+ st.dataframe(df1, hide_index=True, use_container_width=True)
|
|
|
+
|
|
|
+ st.markdown('</div>', unsafe_allow_html=True)
|
|
|
+
|
|
|
+ with col2:
|
|
|
+ st.markdown('<div class="card">', unsafe_allow_html=True)
|
|
|
+ st.markdown('<div class="card-header">满意度分析</div>', unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 使用表格呈现
|
|
|
+ data = [
|
|
|
+ ["满意数量", st.session_state.stats_data.get('满意数', 0)],
|
|
|
+ ["不满意数量", st.session_state.stats_data.get('不满意', 0)],
|
|
|
+ ["未评价数量", st.session_state.stats_data.get('未评价', 0)],
|
|
|
+ ["满意率", st.session_state.stats_data.get('满意率', '0%')]
|
|
|
+ ]
|
|
|
+
|
|
|
+ # 创建DataFrame并显示
|
|
|
+ df2 = pd.DataFrame(data, columns=["指标", "数值"])
|
|
|
+ st.dataframe(df2, hide_index=True, use_container_width=True)
|
|
|
+
|
|
|
+ st.markdown('</div>', unsafe_allow_html=True)
|
|
|
+
|
|
|
+ # 未评价详细统计
|
|
|
+ st.markdown('<div class="card">', unsafe_allow_html=True)
|
|
|
+ st.markdown('<div class="card-header">未评价客服分布</div>', unsafe_allow_html=True)
|
|
|
+
|
|
|
+ unrated_stats = st.session_state.stats_data.get('未评价统计', {})
|
|
|
+
|
|
|
+ if unrated_stats:
|
|
|
+ # 将字典转换为DataFrame
|
|
|
+ data = [[user, count] for user, count in unrated_stats.items()]
|
|
|
+ df3 = pd.DataFrame(data, columns=["客服", "未评价数量"])
|
|
|
+
|
|
|
+ # 按未评价数量降序排序
|
|
|
+ df3 = df3.sort_values(by="未评价数量", ascending=False)
|
|
|
+
|
|
|
+ # 显示表格
|
|
|
+ st.dataframe(df3, hide_index=True, use_container_width=True)
|
|
|
+ else:
|
|
|
+ st.info("没有未评价数据")
|
|
|
+
|
|
|
+ st.markdown('</div>', unsafe_allow_html=True)
|