瀏覽代碼

create initial version

Y 3 月之前
當前提交
221e9f6e6c
共有 9 個文件被更改,包括 1027 次插入0 次删除
  1. 11 0
      .dockerignore
  2. 21 0
      Dockerfile
  3. 73 0
      README.MD
  4. 12 0
      deploy.sh
  5. 14 0
      docker-compose.yml
  6. 140 0
      qa_content.py
  7. 69 0
      qa_info.py
  8. 680 0
      qa_ui.py
  9. 7 0
      requirements.txt

+ 11 - 0
.dockerignore

@@ -0,0 +1,11 @@
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.Python
+.git/
+.gitignore
+.env
+temp_file.xlsx
+temp_stats_file.xlsx
+temp_analysis_file.xlsx

+ 21 - 0
Dockerfile

@@ -0,0 +1,21 @@
+# 使用Python 3.9作为基础镜像
+FROM python:3.10-slim
+
+# 设置工作目录
+WORKDIR /app
+
+# 复制依赖文件并安装依赖
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
+
+# 复制应用代码
+COPY . .
+
+# 设置环境变量
+ENV PYTHONUNBUFFERED=1
+
+# 暴露streamlit默认端口
+EXPOSE 8501
+
+# 设置容器启动命令
+CMD ["streamlit", "run", "qa_ui.py", "--server.address=0.0.0.0", "--server.port=8501"]

+ 73 - 0
README.MD

@@ -0,0 +1,73 @@
+# AI对话分析系统
+
+这是一个基于Streamlit的AI对话分析系统,用于分析和可视化对话数据。
+
+## 功能特点
+
+- 对话内容分析
+- 客户消息分析
+- 数据可视化
+- 交互式用户界面
+- Docker容器化部署
+
+## 安装说明
+
+1. 克隆项目到本地:
+```bash
+git clone [项目地址]
+cd [项目目录]
+```
+
+2. 创建并激活虚拟环境(推荐):
+```bash
+python -m venv venv
+source venv/bin/activate  # Linux/Mac
+# 或
+.\venv\Scripts\activate  # Windows
+```
+
+3. 安装依赖:
+```bash
+pip install -r requirements.txt
+```
+
+## 运行项目
+
+1. 本地运行:
+```bash
+streamlit run qa_ui.py
+```
+
+2. Docker运行:
+```bash
+docker-compose up -d
+```
+
+## 项目结构
+
+```
+.
+├── qa_ui.py          # 主界面应用
+├── qa_content.py     # 对话内容分析模块
+├── qa_info.py        # 数据分析模块
+├── data/             # 数据目录
+├── temp_files/       # 临时文件目录
+├── Dockerfile        # Docker构建文件
+├── docker-compose.yml # Docker编排配置
+└── requirements.txt  # 项目依赖
+```
+
+## 使用说明
+
+1. 启动应用后,在浏览器中访问 `http://localhost:8501`
+2. 上传Excel格式的对话数据文件
+3. 使用界面上的各种分析工具进行数据分析
+4. 查看生成的分析报告和可视化图表
+
+## 注意事项
+
+- 确保上传的数据文件格式正确
+- 建议使用Chrome或Firefox浏览器
+- 首次运行可能需要较长时间加载依赖
+
+## 许可证

+ 12 - 0
deploy.sh

@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# 构建并启动Docker容器
+echo "开始构建并启动AI对话分析系统..."
+docker-compose up -d --build
+
+# 检查容器是否成功启动
+echo "检查容器状态..."
+sleep 5
+docker-compose ps
+
+echo "部署完成!服务已在 http://localhost:1111 启动"

+ 14 - 0
docker-compose.yml

@@ -0,0 +1,14 @@
+version: '3.8'
+
+services:
+  aiqa-app:
+    build: .
+    container_name: aiqa-app
+    ports:
+      - "1111:8501"
+    volumes:
+      - ./data:/app/data
+      - ./:/app
+    restart: unless-stopped
+    environment:
+      - TZ=Asia/Shanghai

+ 140 - 0
qa_content.py

@@ -0,0 +1,140 @@
+import json
+import pandas as pd
+
+def analyze_customer_messages(excel_path):
+    """分析不满意客户的所有对话记录"""
+    
+    # 读取Excel文件
+    df = pd.read_excel(excel_path)
+    
+    # 1. 找出所有不满意评价的客户
+    unsatisfied_customers = df[df['分类是否正确'] == '不满意']['客户用户名'].unique()
+    
+    results = []
+    
+    # 2. 处理每个客户的对话记录
+    for customer in unsatisfied_customers:
+
+        # 获取该客户的所有对话记录
+        customer_df = df[df['客户用户名'] == customer].copy()
+        
+        # 按时间排序,当时间相同时,按对话方向排序(呼入排在呼出前面)
+        customer_df['信息时间'] = pd.to_datetime(customer_df['信息时间'])
+        # 创建一个排序键,使得"呼入"优先级高于"呼出"
+        customer_df['对话方向优先级'] = customer_df['对话方向'].map(lambda x: 0 if x == "呼入" else 1)
+        customer_df = customer_df.sort_values(['信息时间', '对话方向优先级'])
+        # 删除临时列
+        customer_df = customer_df.drop('对话方向优先级', axis=1)
+        
+        # 构建该客户的消息列表
+        messages = []
+        for _, row in customer_df.iterrows():
+            message = {
+                "自动回复意图": str(row['自动回复意图']),
+                "自动回复类型": str(row['自动回复类型']),
+                "客户用户名": str(row['客户用户名']),
+                "客服用户名": str(row['客服用户名'].split(':')[-1]),
+                "信息时间": str(row['信息时间']),
+                "消息内容": str(row['消息内容']),
+                "对话方向": str(row['对话方向']),
+                "分类是否正确": str(row['分类是否正确']),
+                "机器人自动回复状态": str(row['机器人自动回复状态'])
+            }
+            messages.append(message)
+        
+        # 添加到结果列表
+        customer_data = {
+            "用户": customer,
+            "消息列表": messages
+        }
+        results.append(customer_data)
+    
+    return results
+
+
+def filter_context_messages(results, context_size=10):
+    """
+    筛选不满意消息的上下文
+    
+    Args:
+        results: 原始结果列表
+        context_size: 前后文数量,默认10条
+    
+    Returns:
+        filtered_results: 筛选后的结果列表
+    """
+    filtered_results = []
+    
+    for customer_data in results:
+        messages = customer_data["消息列表"]
+        filtered_messages = []
+        
+        # 找出所有不满意消息的索引
+        unsatisfied_indices = [
+            i for i, msg in enumerate(messages) 
+            if msg["分类是否正确"] == "不满意"
+        ]
+        
+        # 对每个不满意消息获取上下文
+        processed_indices = set()  # 用于记录已处理的消息索引
+        
+        for idx in unsatisfied_indices:
+            # 计算上下文范围
+            start_idx = max(0, idx - context_size)
+            end_idx = min(len(messages), idx + context_size + 1)
+            
+            # 添加范围内的所有消息
+            for i in range(start_idx, end_idx):
+                if i not in processed_indices:
+                    filtered_messages.append(messages[i])
+                    processed_indices.add(i)
+        
+        # 如果有筛选出的消息,则添加到结果中
+        if filtered_messages:
+            filtered_results.append({
+                "用户": customer_data["用户"],
+                "消息列表": sorted(filtered_messages, 
+                                key=lambda x: messages.index(x))  # 保持原有顺序
+            })
+    
+    return filtered_results
+
+def format_convert(results):
+    converted_result = []
+
+    for result in results:
+        messages = result['消息列表']
+        for i, message in enumerate(messages):
+            call_intent = message['自动回复意图']
+            call_type = message['自动回复类型']
+            call_user = message['客户用户名']
+            call_servicer = message['客服用户名']
+            call_direction = message['对话方向']
+            call_message = message['消息内容']
+            call_result = message['分类是否正确']
+            call_statue = message['机器人自动回复状态']
+
+
+# 完整的处理流程
+def process_excel_data(excel_path):
+    """完整的数据处理流程"""
+    # 1. 首先获取所有不满意客户的对话记录
+    results = analyze_customer_messages(excel_path)
+    
+    # 2. 筛选不满意消息的上下文
+    filtered_results = filter_context_messages(results)
+    
+    # return json.dumps(filtered_results, ensure_ascii=False, indent=4)
+    return filtered_results
+
+
+# 使用示例
+if __name__ == "__main__":
+
+
+    excel_path = "./data/聊天历史统计表_04_02.xlsx"
+
+    final_results = process_excel_data(excel_path)
+    
+
+    print(final_results)

+ 69 - 0
qa_info.py

@@ -0,0 +1,69 @@
+import pandas as pd
+
+import warnings
+warnings.filterwarnings('ignore')
+
+# 创建筛选条件
+def analyze_data(excel_path):
+
+    df = pd.read_excel(excel_path)
+
+    result = {}
+
+    # 1. 呼出且与AI对话的数量
+    total_calls = len(df[(df['对话方向'] == '呼出')])
+    ai_calls = len(df[(df['对话方向'] == '呼出') & (df['是否在和AI对话'] == '是')])
+    
+    # 2. 呼出且非AI对话的数量
+    non_ai_calls = len(df[(df['对话方向'] == '呼出') & (df['是否在和AI对话'] == '否')])
+    
+    # 3. 呼出且AI对话的满意度分类
+    ai_calls_df = df[(df['对话方向'] == '呼出') & (df['是否在和AI对话'] == '是')]
+    satisfied = len(ai_calls_df[ai_calls_df['分类是否正确'] == '满意'])
+    unsatisfied = len(ai_calls_df[ai_calls_df['分类是否正确'] == '不满意'])
+    not_rated = len(ai_calls_df[ai_calls_df['分类是否正确'] == '未评价'])
+
+    # 4. 比率计算
+    satisfaction_rate = f"{((satisfied + not_rated) / ai_calls * 100):.2f}%"
+    not_rated_rate = f"{(not_rated / ai_calls * 100):.2f}%"
+    auto_call_rate = f"{(ai_calls / total_calls * 100):.2f}%"
+
+    # 5. 人工计算
+
+    result["总呼出"] = total_calls
+    result["人呼出"] = non_ai_calls
+    result["AI呼出"] = ai_calls
+    result["满意数"] = satisfied
+    result["不满意"] = unsatisfied
+    result["未评价"] = not_rated
+    result["未评价率"] = not_rated_rate
+    result["满意率"] = satisfaction_rate
+    result["AI回复率"] = auto_call_rate
+
+    # print(result)
+
+
+    # 筛选条件:呼出 + AI对话 + 未评价
+    filtered_df = df[
+        (df['对话方向'] == '呼出') & 
+        (df['是否在和AI对话'] == '是') & 
+        (df['分类是否正确'] == '未评价')
+    ]
+
+    # 按客服用户名分组统计
+    user_counts = filtered_df['客服用户名'].value_counts()
+    
+    unrate_info = {}
+    for user, count in user_counts.items():
+        unrate_info[user.split(':')[-1]] = count    
+    result["未评价统计"] = unrate_info
+
+    return result
+
+# 使用示例
+if __name__ == "__main__":
+
+    excel_path = "./data/聊天历史统计表_04_02.xlsx"
+    result = analyze_data(excel_path)
+
+    print(result)

+ 680 - 0
qa_ui.py

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

+ 7 - 0
requirements.txt

@@ -0,0 +1,7 @@
+pandas>=2.0.0
+streamlit>=1.30.0
+openpyxl>=3.1.0
+python-dotenv>=1.0.0
+numpy>=1.24.0
+matplotlib>=3.7.0
+seaborn>=0.12.0