qa_ui.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. import os
  2. import time
  3. import pandas as pd
  4. import streamlit as st
  5. from qa_content import filter_context_messages, analyze_customer_messages
  6. from qa_info import analyze_data
  7. # 设置页面配置
  8. st.set_page_config(
  9. page_title="AI对话分析系统",
  10. page_icon="💬",
  11. layout="wide",
  12. initial_sidebar_state="expanded"
  13. )
  14. # 自定义CSS样式
  15. st.markdown("""
  16. <style>
  17. /* 导入字体 */
  18. @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
  19. /* 全局样式 */
  20. html, body, [class*="css"] {
  21. font-family: 'Inter', sans-serif;
  22. }
  23. /* 设置基础变量 */
  24. :root {
  25. --primary: #2e7eed;
  26. --primary-light: #e8f1fd;
  27. --secondary: #6c757d;
  28. --success: #28a745;
  29. --danger: #dc3545;
  30. --warning: #ffc107;
  31. --info: #17a2b8;
  32. --dark: #343a40;
  33. --light: #f8f9fa;
  34. --white: #ffffff;
  35. --border: #e9ecef;
  36. --shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
  37. }
  38. /* 调整整体间距 */
  39. .block-container {
  40. padding-top: 1rem;
  41. padding-bottom: 1rem;
  42. max-width: 100%;
  43. }
  44. .main {
  45. background-color: #f8f9fa;
  46. }
  47. /* 侧边栏样式 - 加宽 */
  48. section[data-testid="stSidebar"] {
  49. min-width: 330px !important;
  50. width: 330px !important;
  51. background-color: var(--black);
  52. border-right: 1px solid var(--border);
  53. }
  54. /* 侧边栏内部样式 */
  55. .css-hxt7ib {
  56. padding-top: 1rem;
  57. padding-left: 1rem;
  58. padding-right: 1rem;
  59. }
  60. /* 标题样式 */
  61. .sidebar-header {
  62. font-size: 1.2rem;
  63. font-weight: 600;
  64. color: var(--dark);
  65. margin-bottom: 0.75rem;
  66. padding-bottom: 0.75rem;
  67. border-bottom: 1px solid var(--border);
  68. }
  69. .sidebar-section {
  70. font-size: 0.9rem;
  71. font-weight: 600;
  72. color: var(--dark);
  73. margin-top: 1rem;
  74. margin-bottom: 0.5rem;
  75. }
  76. /* 主内容标题 */
  77. .main-header {
  78. font-size: 1.2rem;
  79. font-weight: 600;
  80. color: var(--dark);
  81. margin-bottom: 1rem;
  82. padding-bottom: 0.5rem;
  83. border-bottom: 1px solid var(--border);
  84. }
  85. /* 卡片样式 */
  86. .card {
  87. background-color: var(--white);
  88. border-radius: 6px;
  89. padding: 1rem;
  90. box-shadow: var(--shadow);
  91. margin-bottom: 1rem;
  92. border: 1px solid var(--border);
  93. }
  94. .card-header {
  95. font-size: 0.9rem;
  96. font-weight: 600;
  97. color: var(--dark);
  98. margin-bottom: 0.75rem;
  99. padding-bottom: 0.5rem;
  100. border-bottom: 1px solid var(--border);
  101. }
  102. /* 聊天容器 */
  103. .chat-container {
  104. height: 75vh;
  105. overflow-y: auto;
  106. padding: 1rem;
  107. background: var(--black);
  108. border-radius: 6px;
  109. border: 1px solid var(--border);
  110. box-shadow: var(--shadow);
  111. }
  112. /* 用户消息样式 */
  113. .user-message {
  114. display: flex;
  115. margin-bottom: 0.75rem;
  116. }
  117. .user-avatar {
  118. width: 32px;
  119. height: 32px;
  120. background-color: var(--primary);
  121. border-radius: 50%;
  122. display: flex;
  123. align-items: center;
  124. justify-content: center;
  125. color: var(--white);
  126. font-weight: 500;
  127. font-size: 0.8rem;
  128. margin-right: 0.5rem;
  129. flex-shrink: 0;
  130. }
  131. .user-content {
  132. background-color: var(--primary-light);
  133. padding: 0.6rem 0.8rem;
  134. border-radius: 0.8rem 0.8rem 0.8rem 0.2rem;
  135. max-width: 80%;
  136. font-size: 0.85rem;
  137. }
  138. .message-time {
  139. font-size: 0.7rem;
  140. color: var(--secondary);
  141. margin-bottom: 0.25rem;
  142. }
  143. .intent-content {
  144. font-size: 0.7rem;
  145. color: var(--secondary);
  146. margin-bottom: 0.25rem;
  147. }
  148. .call-intent-tag {
  149. background-color: #fff0f0;
  150. color: var(--danger);
  151. }
  152. /* 客服消息样式 */
  153. .service-message {
  154. display: flex;
  155. justify-content: flex-end;
  156. margin-bottom: 0.75rem;
  157. }
  158. .service-avatar {
  159. width: 32px;
  160. height: 32px;
  161. background-color: var(--danger);
  162. border-radius: 50%;
  163. display: flex;
  164. align-items: center;
  165. justify-content: center;
  166. color: var(--white);
  167. font-weight: 500;
  168. font-size: 0.8rem;
  169. margin-left: 0.5rem;
  170. flex-shrink: 0;
  171. }
  172. .service-content {
  173. background-color: var(--light);
  174. padding: 0.6rem 0.8rem;
  175. border-radius: 0.8rem 0.8rem 0.2rem 0.8rem;
  176. max-width: 80%;
  177. text-align: left;
  178. font-size: 0.85rem;
  179. }
  180. .tag {
  181. display: inline-block;
  182. font-size: 0.65rem;
  183. font-weight: 500;
  184. padding: 0.15rem 0.4rem;
  185. border-radius: 3px;
  186. margin-left: 0.4rem;
  187. }
  188. .call-type-tag {
  189. background-color: #fff0f0;
  190. color: var(--danger);
  191. }
  192. .bot-tag {
  193. background-color: #fff0f0;
  194. color: var(--danger);
  195. }
  196. .ok-tag {
  197. background-color: #f0fff0;
  198. color: var(--success);
  199. }
  200. .warning-tag {
  201. background-color: #fffff0;
  202. color: var(--warning);
  203. }
  204. /* 空状态提示 */
  205. .empty-state {
  206. display: flex;
  207. flex-direction: column;
  208. height: 70vh;
  209. justify-content: center;
  210. align-items: center;
  211. color: var(--secondary);
  212. padding: 2rem;
  213. text-align: center;
  214. }
  215. .empty-icon {
  216. font-size: 2.5rem;
  217. margin-bottom: 1rem;
  218. color: #dce0e5;
  219. }
  220. .empty-title {
  221. font-size: 1.2rem;
  222. font-weight: 500;
  223. color: var(--dark);
  224. margin-bottom: 0.5rem;
  225. }
  226. .empty-desc {
  227. font-size: 0.9rem;
  228. color: var(--secondary);
  229. max-width: 400px;
  230. }
  231. /* 数据指标样式 */
  232. .metric-container {
  233. background-color: var(--white);
  234. border-radius: 6px;
  235. padding: 0.75rem;
  236. box-shadow: var(--shadow);
  237. border: 1px solid var(--border);
  238. text-align: center;
  239. height: 100%;
  240. }
  241. .metric-value {
  242. font-size: 1.4rem;
  243. font-weight: 600;
  244. color: var(--primary);
  245. margin-bottom: 0.25rem;
  246. }
  247. .metric-label {
  248. font-size: 0.75rem;
  249. color: var(--secondary);
  250. }
  251. /* 修改组件样式 */
  252. .sidebar-widget {
  253. margin-bottom: 0.75rem;
  254. }
  255. .sidebar-text {
  256. font-size: 0.8rem;
  257. color: var(--secondary);
  258. margin-bottom: 0.5rem;
  259. }
  260. /* 修改分割线样式 */
  261. hr {
  262. margin: 1rem 0;
  263. border-color: var(--border);
  264. opacity: 0.5;
  265. }
  266. /* 表格样式 */
  267. .dataframe {
  268. font-size: 0.85rem;
  269. }
  270. /* 选项卡容器 */
  271. [data-testid="stHorizontalBlock"] > div {
  272. flex: 1;
  273. max-width: 100%;
  274. }
  275. </style>
  276. """, unsafe_allow_html=True)
  277. # 初始化session_state
  278. if 'view_mode' not in st.session_state:
  279. st.session_state.view_mode = "chat_analysis" # 默认视图模式:'chat_analysis' 或 'stats_analysis'
  280. if 'chat_data' not in st.session_state:
  281. st.session_state.chat_data = []
  282. if 'selected_user' not in st.session_state:
  283. st.session_state.selected_user = None
  284. if 'user_list' not in st.session_state:
  285. st.session_state.user_list = []
  286. if 'stats_data' not in st.session_state:
  287. st.session_state.stats_data = {}
  288. if 'file_processed' not in st.session_state:
  289. st.session_state.file_processed = False
  290. # 侧边栏操作区域
  291. with st.sidebar:
  292. st.markdown("<div class='sidebar-header'>AI对话分析系统</div>", unsafe_allow_html=True)
  293. # 视图选择
  294. st.markdown("<div class='sidebar-section'>选择查看内容</div>", unsafe_allow_html=True)
  295. view_options = ["历史对话内容", "历史对话数量"]
  296. selected_view = st.radio("",
  297. options=view_options,
  298. index=0 if st.session_state.view_mode == "chat_analysis" else 1,
  299. label_visibility="collapsed")
  300. # 根据选择更新视图模式
  301. if selected_view == "历史对话内容" and st.session_state.view_mode != "chat_analysis":
  302. st.session_state.view_mode = "chat_analysis"
  303. st.rerun()
  304. elif selected_view == "历史对话数量" and st.session_state.view_mode != "stats_analysis":
  305. st.session_state.view_mode = "stats_analysis"
  306. st.rerun()
  307. # 分割线
  308. st.markdown("<hr>", unsafe_allow_html=True)
  309. # 上传文件部分 - 同时处理两种分析
  310. st.markdown("<div class='sidebar-section'>上传数据分析</div>", unsafe_allow_html=True)
  311. # st.markdown("<div class='sidebar-text'>上传Excel文件进行全面分析</div>", unsafe_allow_html=True)
  312. # 文件上传
  313. uploaded_file = st.file_uploader("上传Excel文件", type=["xlsx", "xls"])
  314. # 修改临时文件路径,确保在Docker环境中有权限写入
  315. TEMP_FILE_DIR = "./temp_files"
  316. os.makedirs(TEMP_FILE_DIR, exist_ok=True)
  317. # 如果有上传文件并且未处理
  318. if uploaded_file is not None and not st.session_state.file_processed:
  319. # 显示处理状态
  320. st.markdown("<div class='sidebar-text'>正在处理文件...</div>", unsafe_allow_html=True)
  321. progress = st.progress(0)
  322. # # 保存上传的文件
  323. # with open("temp_analysis_file.xlsx", "wb") as f:
  324. # f.write(uploaded_file.getvalue())
  325. # 保存上传的文件
  326. temp_file_path = os.path.join(TEMP_FILE_DIR, "temp_analysis_file.xlsx")
  327. with open(temp_file_path, "wb") as f:
  328. f.write(uploaded_file.getvalue())
  329. try:
  330. # 更新进度
  331. progress.progress(20)
  332. time.sleep(0.2)
  333. # 分析对话数据
  334. # results = analyze_customer_messages("temp_analysis_file.xlsx")
  335. results = analyze_customer_messages(temp_file_path)
  336. # 更新进度
  337. progress.progress(40)
  338. time.sleep(0.2)
  339. # 筛选不满意对话结果
  340. filtered_results = filter_context_messages(results)
  341. # 更新进度
  342. progress.progress(60)
  343. time.sleep(0.2)
  344. # 分析统计数据
  345. # stats_result = analyze_data("temp_analysis_file.xlsx")
  346. stats_result = analyze_data(temp_file_path)
  347. # 更新进度
  348. progress.progress(90)
  349. time.sleep(0.2)
  350. # 将分析结果保存到session_state中
  351. if filtered_results:
  352. st.session_state.chat_data = filtered_results
  353. st.session_state.user_list = [entry["用户"] for entry in filtered_results]
  354. st.session_state.selected_user = st.session_state.user_list[0]
  355. st.session_state.stats_data = stats_result
  356. # 处理完成
  357. progress.progress(100)
  358. time.sleep(0.3)
  359. progress.empty()
  360. st.session_state.file_processed = True
  361. # 显示成功消息
  362. if filtered_results:
  363. chat_msg = f"✓ 已分析 {len(filtered_results)} 位用户的对话"
  364. else:
  365. chat_msg = "未找到符合条件的对话记录"
  366. st.success(f"数据分析完成!\n{chat_msg}")
  367. st.rerun()
  368. except Exception as e:
  369. progress.empty()
  370. st.error(f"处理文件出错: {str(e)}")
  371. st.session_state.file_processed = True
  372. # 如果已有对话数据,显示用户选择列表
  373. if st.session_state.view_mode == "chat_analysis" and st.session_state.user_list:
  374. st.markdown("<hr>", unsafe_allow_html=True)
  375. st.markdown("<div class='sidebar-section'>选择用户</div>", unsafe_allow_html=True)
  376. selected_user = st.selectbox(
  377. "选择要查看的用户",
  378. options=st.session_state.user_list,
  379. 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
  380. )
  381. if selected_user != st.session_state.selected_user:
  382. st.session_state.selected_user = selected_user
  383. st.rerun()
  384. # 重置按钮(始终显示在侧边栏底部)
  385. st.markdown("<hr>", unsafe_allow_html=True)
  386. if st.button("重置分析数据", type="primary", use_container_width=True):
  387. for key in ['chat_data', 'selected_user', 'user_list', 'file_processed', 'stats_data']:
  388. if key in st.session_state:
  389. del st.session_state[key]
  390. # # 尝试删除临时文件
  391. # try:
  392. # if os.path.exists("temp_analysis_file.xlsx"):
  393. # os.remove("temp_analysis_file.xlsx")
  394. # except:
  395. # pass
  396. # 尝试删除临时文件
  397. try:
  398. temp_file_path = os.path.join(TEMP_FILE_DIR, "temp_analysis_file.xlsx")
  399. if os.path.exists(temp_file_path):
  400. os.remove(temp_file_path)
  401. except:
  402. pass
  403. st.rerun()
  404. # 版权信息
  405. # 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)
  406. # 主界面 - 仅显示结果
  407. if st.session_state.view_mode == "chat_analysis":
  408. # 历史对话内容结果显示
  409. st.markdown("<div class='main-header'>历史对话内容</div>", unsafe_allow_html=True)
  410. # 检查是否有数据可显示
  411. if not st.session_state.chat_data or not st.session_state.selected_user:
  412. # 显示空状态
  413. st.markdown("<div class='empty-state'>", unsafe_allow_html=True)
  414. st.markdown("<div class='empty-icon'>💬</div>", unsafe_allow_html=True)
  415. st.markdown("<div class='empty-title'>欢迎使用历史对话内容</div>", unsafe_allow_html=True)
  416. st.markdown("<div class='empty-desc'>请在左侧边栏上传Excel文件,系统会自动分析不满意对话并在此处显示结果。</div>", unsafe_allow_html=True)
  417. st.markdown("</div>", unsafe_allow_html=True)
  418. else:
  419. # 显示用户信息
  420. st.markdown(f"<div class='card'>正在查看用户 <b>{st.session_state.selected_user}</b> 的对话记录</div>", unsafe_allow_html=True)
  421. # 找到当前选择的用户对话
  422. selected_chat = None
  423. for entry in st.session_state.chat_data:
  424. if entry["用户"] == st.session_state.selected_user:
  425. selected_chat = entry["消息列表"]
  426. break
  427. if selected_chat:
  428. # 显示所有消息
  429. for message in selected_chat:
  430. try:
  431. user = message.get('客户用户名', '未知')
  432. servicer = message.get('客服用户名', '未知')
  433. content = message.get("消息内容", "")
  434. direction = message.get("对话方向", "")
  435. time_str = message.get("信息时间", "")
  436. call_type = message.get("自动回复类型", "")
  437. call_intent = message.get('自动回复意图', '')
  438. call_type_tag = f'<span class="tag call-type-tag">{call_type}</span>'
  439. call_intent_tag = f'<span class="tag call-intent-tag">{call_intent}</span>'
  440. # 如果消息为空,则跳过
  441. if not content:
  442. continue
  443. # 根据对话方向判断是客户还是客服
  444. if direction == "呼入": # 客户消息
  445. st.markdown(f"""
  446. <div class="user-message">
  447. <div class="user-avatar">{user[:1]}</div>
  448. <div class="user-content">
  449. <div class="message-time">{time_str}{call_type_tag}</div>
  450. {content}
  451. <div class="intent-content">{call_intent_tag}</div>
  452. </div>
  453. </div>
  454. """, unsafe_allow_html=True)
  455. else: # 客服消息(呼出)
  456. # 判断是否是机器人回复
  457. is_bot = str(message.get("机器人自动回复状态", "")) == "是"
  458. is_ok = message.get('分类是否正确', "")
  459. # 标签
  460. bot_tag = '<span class="tag bot-tag">机器人</span>' if is_bot else ''
  461. # 根据分类是否正确显示不同颜色的标签
  462. ok_tag = ""
  463. if is_bot and is_ok == "满意":
  464. ok_tag = '<span class="tag ok-tag">满意</span>'
  465. elif is_bot and is_ok == "不满意":
  466. ok_tag = '<span class="tag bot-tag">不满意</span>'
  467. elif is_bot and is_ok == "未评价":
  468. ok_tag = '<span class="tag warning-tag">未评价</span>'
  469. st.markdown(f"""
  470. <div class="service-message">
  471. <div class="service-content">
  472. <div class="message-time">{time_str} {bot_tag} {ok_tag}</div>
  473. {content}
  474. </div>
  475. <div class="service-avatar">{servicer[:]}</div>
  476. </div>
  477. """, unsafe_allow_html=True)
  478. except Exception as e:
  479. st.error(f"显示消息出错: {str(e)}")
  480. else:
  481. st.markdown('<div class="empty-state"><div class="empty-title">未找到对话记录</div></div>', unsafe_allow_html=True)
  482. st.markdown("</div>", unsafe_allow_html=True)
  483. else: # stats_analysis 模式
  484. # 历史对话数量结果显示
  485. st.markdown("<div class='main-header'>历史对话数量分析</div>", unsafe_allow_html=True)
  486. # 检查是否有数据可显示
  487. if not st.session_state.stats_data:
  488. # 显示空状态
  489. st.markdown("<div class='empty-state'>", unsafe_allow_html=True)
  490. st.markdown("<div class='empty-icon'>📊</div>", unsafe_allow_html=True)
  491. st.markdown("<div class='empty-title'>欢迎使用历史对话数量</div>", unsafe_allow_html=True)
  492. st.markdown("<div class='empty-desc'>请在左侧边栏上传Excel文件,系统会自动分析对话数据并在此处显示统计结果。</div>", unsafe_allow_html=True)
  493. st.markdown("</div>", unsafe_allow_html=True)
  494. else:
  495. # 显示统计摘要 - 关键指标
  496. col_metrics = st.columns(4)
  497. with col_metrics[0]:
  498. st.markdown("<div class='metric-container'>", unsafe_allow_html=True)
  499. st.markdown(f"<div class='metric-value'>{st.session_state.stats_data.get('总呼出', 0)}</div>", unsafe_allow_html=True)
  500. st.markdown(f"<div class='metric-label'>总呼出数量</div>", unsafe_allow_html=True)
  501. st.markdown("</div>", unsafe_allow_html=True)
  502. with col_metrics[1]:
  503. st.markdown("<div class='metric-container'>", unsafe_allow_html=True)
  504. st.markdown(f"<div class='metric-value'>{st.session_state.stats_data.get('AI呼出', 0)}</div>", unsafe_allow_html=True)
  505. st.markdown(f"<div class='metric-label'>AI对话数量</div>", unsafe_allow_html=True)
  506. st.markdown("</div>", unsafe_allow_html=True)
  507. with col_metrics[2]:
  508. st.markdown("<div class='metric-container'>", unsafe_allow_html=True)
  509. st.markdown(f"<div class='metric-value'>{st.session_state.stats_data.get('满意率', '0%')}</div>", unsafe_allow_html=True)
  510. st.markdown(f"<div class='metric-label'>满意率</div>", unsafe_allow_html=True)
  511. st.markdown("</div>", unsafe_allow_html=True)
  512. with col_metrics[3]:
  513. st.markdown("<div class='metric-container'>", unsafe_allow_html=True)
  514. st.markdown(f"<div class='metric-value'>{st.session_state.stats_data.get('未评价率', '0%')}</div>", unsafe_allow_html=True)
  515. st.markdown(f"<div class='metric-label'>未评价率</div>", unsafe_allow_html=True)
  516. st.markdown("</div>", unsafe_allow_html=True)
  517. # 详细统计数据
  518. st.markdown("<div style='height: 20px;'></div>", unsafe_allow_html=True) # 间距
  519. col1, col2 = st.columns(2)
  520. with col1:
  521. st.markdown('<div class="card">', unsafe_allow_html=True)
  522. st.markdown('<div class="card-header">通话统计</div>', unsafe_allow_html=True)
  523. # 使用表格呈现
  524. data = [
  525. ["总呼出数量", st.session_state.stats_data.get('总呼出', 0)],
  526. ["AI呼出数量", st.session_state.stats_data.get('AI呼出', 0)],
  527. ["人工呼出数量", st.session_state.stats_data.get('人呼出', 0)]
  528. ]
  529. # 添加AI占比
  530. total = st.session_state.stats_data.get('总呼出', 0)
  531. ai = st.session_state.stats_data.get('AI呼出', 0)
  532. ai_percentage = f"{(ai/total*100):.1f}%" if total > 0 else "0%"
  533. data.append(["AI占比", ai_percentage])
  534. # 创建DataFrame并显示
  535. df1 = pd.DataFrame(data, columns=["指标", "数值"])
  536. st.dataframe(df1, hide_index=True, use_container_width=True)
  537. st.markdown('</div>', unsafe_allow_html=True)
  538. with col2:
  539. st.markdown('<div class="card">', unsafe_allow_html=True)
  540. st.markdown('<div class="card-header">满意度分析</div>', unsafe_allow_html=True)
  541. # 使用表格呈现
  542. data = [
  543. ["满意数量", st.session_state.stats_data.get('满意数', 0)],
  544. ["不满意数量", st.session_state.stats_data.get('不满意', 0)],
  545. ["未评价数量", st.session_state.stats_data.get('未评价', 0)],
  546. ["满意率", st.session_state.stats_data.get('满意率', '0%')]
  547. ]
  548. # 创建DataFrame并显示
  549. df2 = pd.DataFrame(data, columns=["指标", "数值"])
  550. st.dataframe(df2, hide_index=True, use_container_width=True)
  551. st.markdown('</div>', unsafe_allow_html=True)
  552. # 未评价详细统计
  553. st.markdown('<div class="card">', unsafe_allow_html=True)
  554. st.markdown('<div class="card-header">未评价客服分布</div>', unsafe_allow_html=True)
  555. unrated_stats = st.session_state.stats_data.get('未评价统计', {})
  556. if unrated_stats:
  557. # 将字典转换为DataFrame
  558. data = [[user, count] for user, count in unrated_stats.items()]
  559. df3 = pd.DataFrame(data, columns=["客服", "未评价数量"])
  560. # 按未评价数量降序排序
  561. df3 = df3.sort_values(by="未评价数量", ascending=False)
  562. # 显示表格
  563. st.dataframe(df3, hide_index=True, use_container_width=True)
  564. else:
  565. st.info("没有未评价数据")
  566. st.markdown('</div>', unsafe_allow_html=True)