123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680 |
- 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)
|