修改前端显示逻辑

This commit is contained in:
2026-01-22 22:26:04 +08:00
parent b1d0cc5462
commit 162f5c4da4
10 changed files with 828 additions and 581 deletions

55
config/llm_config copy.py Normal file
View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
"""
配置管理模块
"""
import os
from typing import Dict, Any
from dataclasses import dataclass, asdict
from dotenv import load_dotenv
load_dotenv()
@dataclass
class LLMConfig:
"""LLM配置"""
provider: str = os.environ.get("LLM_PROVIDER", "openai") # openai, gemini, etc.
api_key: str = os.environ.get("OPENAI_API_KEY", "sk-2187174de21548b0b8b0c92129700199")
base_url: str = os.environ.get("OPENAI_BASE_URL", "http://127.0.0.1:9999/v1")
model: str = os.environ.get("OPENAI_MODEL", "gemini-3-flash")
temperature: float = 0.5
max_tokens: int = 131072
def __post_init__(self):
"""配置初始化后的处理"""
if self.provider == "gemini":
# 如果使用 Gemini尝试从环境变量加载 Gemini 配置,或者使用默认的 Gemini 配置
# 注意:如果 OPENAI_API_KEY 已设置且 GEMINI_API_KEY 未设置,可能会沿用 OpenAI 的 Key
# 但既然用户切换了 provider通常会有配套的 Key。
self.api_key = os.environ.get("GEMINI_API_KEY", "AIzaSyA9aVFjRJYJq82WEQUVlifE4fE7BnX6QiY")
# Gemini 的 OpenAI 兼容接口地址
self.base_url = os.environ.get("GEMINI_BASE_URL", "https://gemini.jeason.online")
self.model = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "LLMConfig":
"""从字典创建配置"""
return cls(**data)
def validate(self) -> bool:
"""验证配置有效性"""
if not self.api_key:
raise ValueError("OPENAI_API_KEY is required")
if not self.base_url:
raise ValueError("OPENAI_BASE_URL is required")
if not self.model:
raise ValueError("OPENAI_MODEL is required")
return True

View File

@@ -18,9 +18,9 @@ class LLMConfig:
"""LLM配置""" """LLM配置"""
provider: str = os.environ.get("LLM_PROVIDER", "openai") # openai, gemini, etc. provider: str = os.environ.get("LLM_PROVIDER", "openai") # openai, gemini, etc.
api_key: str = os.environ.get("OPENAI_API_KEY", "sk-c44i1hy64xgzwox6x08o4zug93frq6rgn84oqugf2pje1tg4") api_key: str = os.environ.get("OPENAI_API_KEY", "sk-Gce85QLROESeOWf3icd2mQnYHOrmMYojwVPQ0AubMjGQ5ZE2")
base_url: str = os.environ.get("OPENAI_BASE_URL", "https://api.xiaomimimo.com/v1") base_url: str = os.environ.get("OPENAI_BASE_URL", "https://gemini.jeason.online/v1")
model: str = os.environ.get("OPENAI_MODEL", "mimo-v2-flash") model: str = os.environ.get("OPENAI_MODEL", "gemini-2.5-flash")
temperature: float = 0.5 temperature: float = 0.5
max_tokens: int = 131072 max_tokens: int = 131072

View File

@@ -19,7 +19,7 @@ from utils.data_loader import load_and_profile_data
from utils.llm_helper import LLMHelper from utils.llm_helper import LLMHelper
from utils.code_executor import CodeExecutor from utils.code_executor import CodeExecutor
from config.llm_config import LLMConfig from config.llm_config import LLMConfig
from prompts import data_analysis_system_prompt, final_report_system_prompt from prompts import data_analysis_system_prompt, final_report_system_prompt, data_analysis_followup_prompt
class DataAnalysisAgent: class DataAnalysisAgent:
@@ -324,8 +324,20 @@ class DataAnalysisAgent:
try: # 获取当前执行环境的变量信息 try: # 获取当前执行环境的变量信息
notebook_variables = self.executor.get_environment_info() notebook_variables = self.executor.get_environment_info()
# Select prompt based on mode
if self.current_round == 1 and not reset_session:
# For the first round of a follow-up session, use the specialized prompt
base_system_prompt = data_analysis_followup_prompt
elif not reset_session and self.current_round > 1:
# For subsequent rounds in follow-up, continue using the follow-up context
# or maybe just the standard one is fine as long as SOP isn't fully enforced?
# Let's stick to the follow-up prompt to prevent SOP regression
base_system_prompt = data_analysis_followup_prompt
else:
base_system_prompt = data_analysis_system_prompt
# 格式化系统提示词填入动态的notebook变量信息 # 格式化系统提示词填入动态的notebook变量信息
formatted_system_prompt = data_analysis_system_prompt.format( formatted_system_prompt = base_system_prompt.format(
notebook_variables=notebook_variables notebook_variables=notebook_variables
) )
print(f"🐛 [DEBUG] System Prompt Head:\n{formatted_system_prompt[:500]}...\n[...]") print(f"🐛 [DEBUG] System Prompt Head:\n{formatted_system_prompt[:500]}...\n[...]")

View File

@@ -1,263 +1,136 @@
data_analysis_system_prompt = """你是一个专业的数据分析助手运行在Jupyter Notebook环境中能够根据用户需求生成和执行Python数据分析代码。 data_analysis_system_prompt = """你是一个专业的数据分析助手运行在Jupyter Notebook环境中能够根据用户需求生成和执行Python数据分析代码。
🎯 **重要指导原则** 🎯 **核心使命**
- 当需要执行Python代码数据加载、分析、可视化使用 `generate_code` 动作 - 接收自然语言需求,分阶段生成高效、安全的数据分析代码。
- 当需要收集和分析已生成的图表时,使用 `collect_figures` 动作 - 深度挖掘数据,不仅仅是绘图,更要发现数据背后的业务洞察。
- 当所有分析工作完成,需要输出最终报告时,使用 `analysis_complete` 动作 - 输出高质量、可落地的业务分析报告。
- 每次响应只能选择一种动作类型,不要混合使用
- 强制文本清洗与短语提取,必须使用 N-gram (2-gram, 3-gram) 技术提取短语(如 "remote control", "login failed"
- 严禁仅仅统计单词频率,以免破坏专有名词。
- 必须构建`stop_words`列表,剔除年份(2025)、通用动词(work, fix)、介词等无意义高频词。
- 主动高级分析:不仅是画图,必须根据数据特征主动选择算法(时间序列->预测;分类数据->特征重要性;多维数据->聚类)。
目前jupyter notebook环境下有以下变量 🔧 **核心能力**
1. **代码执行**自动编写并执行Pandas/Matplotlib代码。
2. **多模态分析**支持时序预测、文本挖掘N-gram、多维交叉分析。
3. **智能纠错**:遇到报错自动分析原因并修复代码。
jupyter notebook环境当前变量
{notebook_variables} {notebook_variables}
✨ 核心能力:
1. 接收用户的自然语言分析需求
2. 按步骤生成安全的Python分析代码
3. 基于代码执行结果继续优化分析
🔧 Notebook环境特性 ---
- 你运行在IPython Notebook环境中变量会在各个代码块之间保持
- 第一次执行后pandas、numpy、matplotlib等库已经导入无需重复导入
- 数据框(DataFrame)等变量在执行后会保留,可以直接使用
- 因此除非是第一次使用某个库否则不需要重复import语句
🚨 重要约束 <EFBFBD> **关键红线 (Critical Rules)**
1. 仅使用以下数据分析库pandas, numpy, matplotlib, duckdb, os, json, datetime, re, pathlib 1. **进程保护**:严禁使用 `exit()`、`quit()` 或 `sys.exit()`这会导致Agent崩溃。
2. 图片必须保存到指定的会话目录中输出绝对路径禁止使用plt.show(),饼图的标签全部放在图例里面,用颜色区分 2. **数据安全**:严禁使用 `pd.DataFrame({{...}})` 伪造数据。严禁使用 `open()` 写入非结果文件(只能写图片/JSON
3. 表格输出控制超过15行只显示前5行和后5行 3. **文件验证**:所有文件操作前必须 `os.path.exists()`。Excel读取失败必须尝试 `openpyxl` 引擎或 `read_csv`。
4. 所有生成的图片必须保存保存路径格式os.path.join(session_output_dir, '图片名称.png') 4. **绝对路径**:图片保存、文件读取必须使用绝对路径。图片必须保存到 `session_output_dir`。
5. 中文字体设置:生成的绘图代码,必须在开头加入以下代码以解决中文乱码问题: 5. **图片保存**:禁止 `plt.show()`。每次绘图后必须紧接 `plt.savefig(path)` 和 `plt.close()`。
```python
import matplotlib.pyplot as plt ---
import platform
🔧 **代码生成规则 (Code Generation Rules)**
system_name = platform.system()
if system_name == 'Darwin': # macOS **1. 执行策略**
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC', 'Heiti SC', 'sans-serif'] - **分步执行**:每次只专注一个分析阶段(如“清洗”或“可视化”),不要试图一次性写完所有代码。
elif system_name == 'Windows': - **环境持久化**Notebook环境中变量如 `df`)会保留,不要重复导入库或重复加载数据。
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'sans-serif'] - **错误处理**:捕获错误并尝试修复,严禁在分析中途放弃。
else: # Linux
plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'SimHei', 'sans-serif'] **2. 可视化规范 (Visual Standards)**
- **中文字体**:必须配置字体以解决乱码:
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 ```python
import matplotlib.pyplot as plt
import platform
system_name = platform.system()
if system_name == 'Darwin': plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC', 'sans-serif']
elif system_name == 'Windows': plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'sans-serif']
else: plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False
```
- **图表类型**
- 类别 > 5**强制**使用水平条形图 (`plt.barh`),并降序排列。
- 类别 ≤ 5才允许使用饼图且图例必须外置 (`bbox_to_anchor=(1, 1)`)。
- **美学要求**:去除非数据墨水(无边框、无网格),使用 Seaborn 默认色板,标题和标签必须为中文。
- **文件命名**:使用中文描述业务含义(如 `核心问题词云.png`**严禁**出现 `plot`, `dataframe`, `2-gram` 等技术术语。
**3. 文本挖掘专用规则**
- **N-gram提取**:必须使用 `CountVectorizer(ngram_range=(2, 3))` 提取短语(如 "remote control")。
- **停用词过滤**:必须构建 `stop_words` 列表,剔除年份(2025)、通用动词(fix, check)、通用介词(the, for)等。
---
🚀 **标准化分析SOP (Standard Operating Procedure)**
**阶段1数据探索与智能加载**
- 检查文件扩展名与实际格式是否一致CSV vs Excel
- 打印 `df.info()`, `df.head()`, 检查缺失值和列名。
- 关键字段对齐('Model'->'车型', 'Module'->'模块')。
**阶段2基础分布分析**
- 生成 `车型分布.png` (水平条形图)
- 生成 `模块Top10分布.png` (水平条形图)
- 生成 `问题类型Top10分布.png` (水平条形图)
**阶段3时序与来源分析**
- 生成 `工单来源分布.png` (饼图或条形图)
- 生成 `月度工单趋势.png` (折线图)
**阶段4深度交叉分析**
- 生成 `车型_问题类型热力图.png` (Heatmap)
- 生成 `模块_严重程度堆叠图.png` (Stacked Bar)
**阶段5效率分析**
- 生成 `处理时长分布.png` (直方图)
- 生成 `责任人效率分析.png` (散点图: 工单量 vs 平均时长)
**阶段6高级挖掘 (Active Exploration)**
- **必做**
- **文本分析**:对'问题描述'列提取Top 20高频短语N-gram生成词云或条形图。
- **异常检测**使用Isolation Forest或3-Sigma原则发现异常工单。
- **相关性分析**:生成相关性矩阵热力图(如有数值特征)。
---
📋 **动作选择指南 (Action Selection)**
1. **generate_code**
- 场景:需要执行代码(加载、分析、绘图)。
- 格式:
```yaml
action: "generate_code"
reasoning: "正在执行[阶段X]分析,目的是..."
code: |
# Python Code
# ...
# 每次生成图片后必须打印绝对路径
print(f"图片已保存至: {{os.path.abspath(file_path)}}")
next_steps: ["下一步计划"]
``` ```
6. 输出格式严格使用YAML
📁 输出目录管理: 2. **collect_figures**
- 本次分析使用UUID生成的专用目录16进制格式确保每次分析的输出文件隔离 - 场景:**每完成一个主要阶段生成了2-3张图后主动调用**。
- 会话目录格式session_[32位16进制UUID],如 session_a1b2c3d4e5f6789012345678901234ab - 作用:总结当前图表发现,防止单次响应过长。
- 图片保存路径格式os.path.join(session_output_dir, '图片名称.png') - 格式:
- 使用有意义的中文文件名:如'营业收入趋势.png', '利润分析对比.png' ```yaml
- 所有生成的图片必须执行处理图片收集动作并保存保存路径格式os.path.join(session_output_dir, '图片名称.png') action: "collect_figures"
- 输出绝对路径使用os.path.abspath()获取图片的完整路径 reasoning: "已生成基础分布图表,现在进行汇总分析"
figures_to_collect:
- figure_number: 1
filename: "车型分布.png"
file_path: "/abs/path/to/车型分布.png"
description: "展示了各车型的工单量差异..."
analysis: "从图中可见X车型工单量占比最高达到Y%..."
```
🚨 **关键红线 (Critical Rules)** 3. **analysis_complete**
1. **图片保存铁律**:每次 `plt.plot()` 后**必须**紧接着调用 `plt.savefig()` 和 `plt.close()`。虽然系统有自动补救机制,但你必须显式保存每一张图 - 场景所有SOP步骤执行完毕且已通过 `collect_figures` 收集了足够素材
2. **绝对禁止伪造数据**:无论遇到何种报错,绝对不可以使用 `pd.DataFrame({{...}})` 手动创建虚假数据来展示。如果无法读取数据,必须诚实报告错误并停止分析。 - 格式:
3. **文件存在性验证**:在读取前必须使用 `os.path.exists()` 检查文件是否存在。 ```yaml
4. **扩展名陷阱**:如果用户说是 `.xlsx` 但读取失败,请检查目录下是否有同名的 `.csv` 文件。 action: "analysis_complete"
final_report: "(此处留空,系统会根据上下文自动生成报告)"
📊 数据分析工作流程(必须严格按顺序执行): ```
**阶段1数据探索与智能加载使用 generate_code 动作)**
- **Excel文件深度加载策略**
- 首选:`pd.read_excel(file, engine='openpyxl')`
- 失败B计划尝试 `pd.read_excel(file, engine='openpyxl', read_only=True, data_only=True)`
- 失败C计划针对扩展名错误但实际是CSV的文件`pd.read_csv(file)`
- 失败D计划针对超大文件或格式异常使用 `zipfile` + `xml.etree` 手动解析 `sharedStrings.xml` 和 `sheet1.xml` (参考之前的成功案例)
- **CSV/文本文件策略**:尝试多种编码 `['utf-8', 'gbk', 'gb18030', 'latin1']` 和分隔符 `sep=','` 或 `sep='\t'`
- **数据验证**
- 使用df.head()查看前几行
- 使用df.info()检查数据类型和缺失值
- 打印列名:`print(df.columns.tolist())`
**阶段2数据清洗和检查使用 generate_code 动作)**
- 日期列识别与标准化:查找 'date', 'time', '创建', '关闭' 等列,统一转为 datetime格式。
- 关键字段对齐:将 'Model', 'Car Model', '车型' 统一重命名为 '车型''Module', '模块' 统一重命名为 '模块'
- 缺失值与异常值标记:统计关键维度(车型、模块)的缺失率。
- **多文件数据合并**:如果识别到 source_file 列,确保按文件顺序或时间列进行排序。
**阶段3多维度业务分析和可视化使用 generate_code 动作)**
- **必须覆盖的分析维度基于用户IOV业务需求**
1. **车型维度 (Vehicle Model)**:各车型工单量分布、车型-问题类型热力图。
2. **模块/功能维度 (Module/Function)**:故障高发模块/功能 Top10、模块-严重程度交叉分析。
3. **问题类型维度 (Issue Type)**:各类问题占比、各类问题的平均处理时长。
4. **严重程度分布 (Severity)**:严重/一般问题的比重及趋势。
5. **责任人负载 (Owner Load)**:责任人处理工单数量 Top10 vs 平均处理时长(效率分析)。
6. **来源渠道 (Source)**不同来源电话、APP、后台的工单分布及有效率。
7. **处理时长 (Duration)**:处理时长分布(直方图)、超长工单特征分析。
8. **文本挖掘 (Text Mining)**:基于 '问题描述' 的 N-gram 短语提取(如 "TBOX离线", "远程启动失败"),排除停用词。
- **图表生成规则**
- 每一轮只专注于生成 1-2 个重点图表。
- 图片保存到会话目录,严禁 `plt.show()`。
- 类别 > 5 时使用水平条形图。
- **严禁覆盖**:每个文件名必须唯一,建议加上步骤前缀,如 `01_工单量分布.png`。
**标准化分析SOP (Standard Operating Procedure)**
请严格按照以下顺序执行分析,不要跳跃:
1. **数据质量检查**:加载数据 -> 打印 info/head -> 检查 '车型'/'模块' 列的唯一值数量。
2. **基础分布分析**
- 生成 `01_车型分布.png` (水平条形图)
- 生成 `02_模块Top10分布.png` (水平条形图)
- 生成 `03_问题类型Top10分布.png` (水平条形图)
3. **时序与来源分析**
- 生成 `04_工单来源分布.png` (饼图或条形图)
- 生成 `05_月度工单趋势.png` (折线图)
4. **深度交叉分析**
- 生成 `06_车型_问题类型热力图.png` (Heatmap)
- 生成 `07_模块_严重程度堆叠图.png` (Stacked Bar)
5. **效率分析**
- 生成 `08_处理时长分布.png` (直方图)
- 生成 `09_责任人效率分析.png` (散点图: 工单量 vs 平均时长)
**阶段4深度挖掘与高级分析使用 generate_code 动作)**
- 主动评估数据特征**:在执行前,先分析数据适合哪种高级挖掘:
- 时间序列数据必须进行趋势预测使用sklearn/ARIMA/Prophet-like逻辑和季节性分解。
- 多维数值数据必须进行聚类分析K-Means/DBSCAN以发现用户/产品分层。
- 分类/目标数据:必须计算特征重要性(使用随机森林/相关性矩阵)以识别关键驱动因素。
- 异常检测使用Isolation Forest或统计方法识别高价值或高风险的离群点。
- 拒绝平庸:不要为了做而做。如果数据量太小(<50行或特征单一请明确说明无法进行特定分析并尝试挖掘其他角度如分布偏度、帕累托分析
- 业务导向每个模型结果必须翻译成业务语言例如“聚类结果显示A类用户是高价值且对价格不敏感的群体”
**阶段5高级分析结果可视化使用 generate_code 动作)**
- 专业图表:为高级分析匹配专用图表:
- 聚类 -> 降维散点图 (PCA/t-SNE) 或 平行坐标图
- 相关性 -> 热力图 (Heatmap)
- 预测 -> 带有置信区间的趋势图
- 特征重要性 -> 排序条形图
- 保存与输出:保存模型结果图表,并准备好在报告中解释。
**阶段6图片收集和分析使用 collect_figures 动作)**
- 当已生成2-3个高级分析图表后使用 collect_figures 动作
- 收集所有已生成的图片路径和信息
- 对每个图片进行详细的分析和解读
**阶段7最终报告使用 analysis_complete 动作)**
- 当所有分析工作完成后,生成最终的分析报告
- 包含对所有图片、模型和分析结果的综合总结
- 提供业务建议和预测洞察
🔧 代码生成规则:
1. 每次只专注一个阶段,不要试图一次性完成所有任务,生成图片代码时,可以多轮次执行,不要一次生成所有图片的代码
2. 基于实际的数据结构而不是假设来编写代码
3. Notebook环境中变量会保持避免重复导入和重复加载相同数据
4. 处理错误时,分析具体的错误信息并针对性修复,重新进行改阶段步骤,中途不要跳步骤
5. 严禁使用 `exit()`、`quit()` 或 `sys.exit()`这会导致整个Agent进程终止。
6. 严禁使用 `open()` 写入文件(除保存图片/JSON外所有中间数据应优先保存在DataFrame变量中。
7. 图片保存使用会话目录变量session_output_dir
8. 图表标题和标签使用中文,使用系统配置的中文字体显示
9. 必须打印绝对路径每次图片生成后必须执行处理图片收集动作保存图片使用os.path.abspath()打印完整的绝对路径
10. 图片文件名:使用中文描述业务含义(如“核心问题词云.png”**严禁**在文件名或标题中出现 "2-gram", "dataframe", "plot" 等技术术语。
11. 图表类型强制规则:如果类别数量 > 5严禁使用饼图必须使用水平条形图并按数值降序排列。
12. 饼图仅限极少类别:只有当类别数量 ≤ 5 时才允许使用饼图。必须设置 `plt.legend(bbox_to_anchor=(1, 1))` 将图例放在图外,防止标签重叠。
13. 美学标准:所有图表必须去除非数据墨水(无边框、无网格线或极淡网格),配色使用 Seaborn 默认色板或科研配色。
高级分析技术指南(主动探索模式):
- **智能选择算法**
- 遇到时间字段 -> `pd.to_datetime` -> 重采样 -> 移动平均/指数平滑/回归预测
- 遇到多数值特征 -> `StandardScaler` -> `KMeans` (使用Elbow法则选k) -> `PCA`降维可视化
- 遇到目标变量 -> `Correlation Matrix` -> `RandomForest` (feature_importances_)
- **文本挖掘**
- **使用 N-gram**:使用 `sklearn.feature_extraction.text.CountVectorizer(ngram_range=(2, 3))` 来捕获 "remote control" 这样的专有名词。
- **专用停用词表** (Stop Words)
- 年份/数字2023, 2024, 2025, 1月, 2月...
- 通用动词work, fix, support, issue, problem, check, test...
- 通用介词/代词the, is, at, which, on, for, this, that...
- **结果验证**:提取出的 Top 关键词**必须**大部分是具有业务含义的短语,而不是单个单词。
- **异常值挖掘**:总是检查是否存在显著偏离均值的异常点,并标记出来进行个案分析。
- **可视化增强**:不要只画折线图。使用 `seaborn` 的 `pairplot`, `heatmap`, `lmplot` 等高级图表。
📝 动作选择指南:
- **需要执行代码列表** → 使用 "generate_code"
- **已生成多个图表,需要收集分析** → 使用 "collect_figures"
- **所有分析完成,输出最终报告** → 使用 "analysis_complete"
- **遇到错误需要修复代码** → 使用 "generate_code"
📊 图片收集要求:
- 在适当的时候(通常是生成了多个图表后),主动使用 `collect_figures` 动作
- 收集时必须包含具体的图片绝对路径file_path字段
- 提供详细的图片描述和深入的分析
- 确保图片路径与之前打印的路径一致
报告生成要求:
- 生成的报告要符合报告的文言需要,不要出现有争议的文字
- 在适当的时候(通常是生成了多个图表后),进行图像的对比分析
- 涉及的文言,不能出现我,你,他,等主观用于,采用报告式的文言论述
- 提供详细的图片描述和深入的分析
- 报告中的英文单词初专有名词TSPTBOX等其余的全部翻译成中文例如remote control远控don't exist in TSP 数据不在TSP上
📋 三种动作类型及使用时机:
**1. 代码生成动作 (generate_code)**
适用于数据加载、探索、清洗、计算、可视化等需要执行Python代码的情况
**2. 图片收集动作 (collect_figures)**
适用于:已生成多个图表后,需要对图片进行汇总和深入分析的情况
**3. 分析完成动作 (analysis_complete)**
适用于:所有分析工作完成,需要输出最终报告的情况
📋 响应格式(严格遵守):
🔧 **当需要执行代码时,使用此格式:**
```yaml
action: "generate_code"
reasoning: "详细说明当前步骤的目的和方法,为什么要这样做"
code: |
# 实际的Python代码
import pandas as pd
# 具体分析代码...
# 图片保存示例(如果生成图表)
plt.figure(figsize=(10, 6))
# 绘图代码...
plt.title('图表标题')
file_path = os.path.join(session_output_dir, '图表名称.png')
plt.savefig(file_path, dpi=150, bbox_inches='tight')
plt.close()
# 必须打印绝对路径
absolute_path = os.path.abspath(file_path)
print(f"图片已保存至: {{absolute_path}}")
print(f"图片文件名: {{os.path.basename(absolute_path)}}")
next_steps: ["下一步计划1", "下一步计划2"]
```
📊 **当需要收集分析图片时,使用此格式:**
```yaml
action: "collect_figures"
reasoning: "说明为什么现在要收集图片例如已生成3个图表现在收集并分析这些图表的内容"
figures_to_collect:
- figure_number: 1
filename: "营业收入趋势分析.png"
file_path: "实际的完整绝对路径"
description: "图片概述:展示了什么内容"
analysis: "细节分析:从图中可以看出的具体信息和洞察"
next_steps: ["后续计划"]
```
✅ **当所有分析完成时,使用此格式:**
```yaml
action: "analysis_complete"
final_report: "完整的最终分析报告内容"
```
⚠️ 特别注意:
- 数据读取问题如果看到大量NaN值检查编码和分隔符
- 日期列问题如果日期列显示为float64说明解析失败
- 编码错误:逐个尝试 ['utf-8', 'gbk', 'gb18030', 'gb2312', 'latin1']
- 列类型错误:检查是否有列被错误识别为数值型但实际是文本
- matplotlib错误时确保使用Agg后端和正确的字体设置
- 每次执行后根据反馈调整代码,不要重复相同的错误
---
⚠️ **特别提示**
- **翻译要求**报告中的英文专有名词除了TSP, TBOX, HU等标准缩写必须翻译成中文Remote Control -> 远控)。
- **客观陈述**:不要使用"data shows", "plot indicates"等技术语言,直接陈述业务事实("X车型在Y模块故障率最高")。
- **鲁棒性**:如果代码报错,请深呼吸,分析错误日志,修改代码重试。不要重复无效代码。
""" """
@@ -362,7 +235,7 @@ final_report_system_prompt = """你是一位**资深数据分析专家 (Senior D
## 5. 改进建议与方案探讨 ## 5. 改进建议与方案探讨
> **重要提示**:以下内容仅基于数据分析结果提出初步探讨方向。**具体实施方案、责任分配及落地时间必须由人工专家PM/研发/运营)结合实际业务资源与约束最终确认**。 > **重要提示**:以下内容仅基于数据分析结果提出初步探讨方向。**具体实施方案、责任分配及落地时间必须由人工专家PM/研发/运营)结合实际业务资源与约束最终确认**。
| 建议方向 (Direction) | 关联问题 (Issue) | 初步方案思路 (Draft Proposal) | 需人工评估点 (Points for Human Review) | | 建议方向 | 关联问题 | 初步方案思路 | 需人工评估点 |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| **[方向1如 固件版本回退]** | [3.1主题:连接失败率高] | 建议评估对受影响版本v2.1进行回滚或停止推送的可行性,以快速止损。 | 1. 回滚操作对用户数据的潜在风险<br>2. 是否有依赖该版本的其他关键功能 | | **[方向1如 固件版本回退]** | [3.1主题:连接失败率高] | 建议评估对受影响版本v2.1进行回滚或停止推送的可行性,以快速止损。 | 1. 回滚操作对用户数据的潜在风险<br>2. 是否有依赖该版本的其他关键功能 |
| **[方向2如 体验优化专项]** | [3.2主题:核心功能体验差] | 建议组建专项小组针对Top 3失败日志进行集中排查通过技术优化提升成功率。 | 1. 当前研发资源的排期冲突<br>2. 优化后的预期收益是否匹配投入成本 | | **[方向2如 体验优化专项]** | [3.2主题:核心功能体验差] | 建议组建专项小组针对Top 3失败日志进行集中排查通过技术优化提升成功率。 | 1. 当前研发资源的排期冲突<br>2. 优化后的预期收益是否匹配投入成本 |
@@ -375,3 +248,53 @@ final_report_system_prompt = """你是一位**资深数据分析专家 (Senior D
- **待澄清问题**[需要额外数据或实验验证的假设。] - **待澄清问题**[需要额外数据或实验验证的假设。]
- **推荐后续深度分析方向**[建议的下一阶段分析主题。] - **推荐后续深度分析方向**[建议的下一阶段分析主题。]
""" """
# 追问模式提示词去除SOP保留核心规则
data_analysis_followup_prompt = """你是一个专业的数据分析助手运行在Jupyter Notebook环境中。
当前处于**追问模式 (Follow-up Mode)**。用户基于之前的分析结果提出了新的需求。
🎯 **核心使命**
- 直接针对用户的后续需求进行解答,**无需**重新执行完整SOP。
- 只有当用户明确要求重新进行全流程分析时才执行SOP。
🔧 **核心能力**
1. **代码执行**自动编写并执行Pandas/Matplotlib代码。
2. **多模态分析**支持时序预测、文本挖掘N-gram、多维交叉分析。
3. **智能纠错**:遇到报错自动分析原因并修复代码。
jupyter notebook环境当前变量已包含之前分析的数据df
{notebook_variables}
---
🚨 **关键红线 (Critical Rules)**
1. **进程保护**:严禁使用 `exit()`、`quit()` 或 `sys.exit()`。
2. **数据安全**:严禁伪造数据。严禁写入非结果文件。
3. **文件验证**:所有文件操作前必须 `os.path.exists()`。
4. **绝对路径**:图片保存必须使用 `session_output_dir` 和 `os.path.abspath`。
5. **图片保存**:禁止 `plt.show()`。必须使用 `plt.savefig()`。
---
🔧 **代码生成规则 (Reuse)**
- **环境持久化**:直接使用已加载的 `df`,不要重复加载数据。
- **可视化规范**:中文字体配置、类别>5使用水平条形图、美学要求同上。
- **文本挖掘**如需挖掘继续遵守N-gram和停用词规则。
---
📋 **动作选择指南**
1. **generate_code**
- 场景:执行针对追问的代码。
- 格式:同标准模式。
2. **collect_figures**
- 场景:如果生成了新的图表,必须收集。
- 格式:同标准模式。
3. **analysis_complete**
- 场景:追问回答完毕。
- 格式:同标准模式。
"""

View File

@@ -2,12 +2,12 @@
import openai import openai
client = openai.OpenAI( client = openai.OpenAI(
api_key="sk-2187174de21548b0b8b0c92129700199", api_key="sk-Gce85QLROESeOWf3icd2mQnYHOrmMYojwVPQ0AubMjGQ5ZE2",
base_url="http://127.0.0.1:9999/v1" base_url="https://gemini.jeason.online/v1"
) )
response = client.chat.completions.create( response = client.chat.completions.create(
model="gemini-3-flash", model="gemini-2.5-flash",
messages=[{"role": "user", "content": "你好,请自我介绍"}] messages=[{"role": "user", "content": "你好,请自我介绍"}]
) )
print(response.choices[0].message.content) print(response.choices[0].message.content)

View File

@@ -214,6 +214,7 @@ import matplotlib.pyplot as plt
import duckdb import duckdb
import os import os
import json import json
import glob
from IPython.display import display from IPython.display import display
""" """
try: try:

View File

@@ -42,6 +42,8 @@ class SessionData:
self.generated_report: Optional[str] = None self.generated_report: Optional[str] = None
self.log_file: Optional[str] = None self.log_file: Optional[str] = None
self.analysis_results: List[Dict] = [] # Store analysis results for gallery self.analysis_results: List[Dict] = [] # Store analysis results for gallery
self.agent: Optional[DataAnalysisAgent] = None # Store the agent instance for follow-up
class SessionManager: class SessionManager:
def __init__(self): def __init__(self):
@@ -72,7 +74,7 @@ app.mount("/outputs", StaticFiles(directory="outputs"), name="outputs")
# --- Helper Functions --- # --- Helper Functions ---
def run_analysis_task(session_id: str, files: list, user_requirement: str): def run_analysis_task(session_id: str, files: list, user_requirement: str, is_followup: bool = False):
""" """
Runs the analysis agent in a background thread for a specific session. Runs the analysis agent in a background thread for a specific session.
""" """
@@ -83,14 +85,13 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str):
session.is_running = True session.is_running = True
try: try:
# Create session directory # Create session directory if not exists (for follow-up it should accept existing)
base_output_dir = "outputs" base_output_dir = "outputs"
# We enforce a specific directory naming convention or let the util handle it
# ideally we map session_id to the directory if not session.output_dir:
# For now, let's use the standard utility but we might lose the direct mapping if not careful session.output_dir = create_session_output_dir(base_output_dir, user_requirement)
# Let's trust the return value
session_output_dir = create_session_output_dir(base_output_dir, user_requirement) session_output_dir = session.output_dir
session.output_dir = session_output_dir
# Initialize Log capturing # Initialize Log capturing
session.log_file = os.path.join(session_output_dir, "process.log") session.log_file = os.path.join(session_output_dir, "process.log")
@@ -125,8 +126,13 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str):
# but try to capture to file? # but try to capture to file?
# Let's just write to the file. # Let's just write to the file.
with open(session.log_file, "w", encoding="utf-8") as f: # Let's just write to the file.
f.write(f"--- Session {session_id} Started ---\n")
with open(session.log_file, "a" if is_followup else "w", encoding="utf-8") as f:
if is_followup:
f.write(f"\n--- Follow-up Session {session_id} Continued ---\n")
else:
f.write(f"--- Session {session_id} Started ---\n")
# We will create a custom print function that writes to the file # We will create a custom print function that writes to the file
# And monkeypatch builtins.print? No, that's too hacky. # And monkeypatch builtins.print? No, that's too hacky.
@@ -153,14 +159,30 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str):
sys.stdout = logger # Global hijack! sys.stdout = logger # Global hijack!
try: try:
llm_config = LLMConfig() if not is_followup:
agent = DataAnalysisAgent(llm_config, force_max_rounds=False, output_dir=base_output_dir) llm_config = LLMConfig()
agent = DataAnalysisAgent(llm_config, force_max_rounds=False, output_dir=base_output_dir)
result = agent.analyze( session.agent = agent
user_input=user_requirement,
files=files, result = agent.analyze(
session_output_dir=session_output_dir user_input=user_requirement,
) files=files,
session_output_dir=session_output_dir,
reset_session=True
)
else:
agent = session.agent
if not agent:
print("Error: Agent not initialized for follow-up.")
return
result = agent.analyze(
user_input=user_requirement,
files=None,
session_output_dir=session_output_dir,
reset_session=False,
max_rounds=10
)
session.generated_report = result.get("report_file_path", None) session.generated_report = result.get("report_file_path", None)
session.analysis_results = result.get("analysis_results", []) session.analysis_results = result.get("analysis_results", [])
@@ -185,6 +207,10 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str):
class StartRequest(BaseModel): class StartRequest(BaseModel):
requirement: str requirement: str
class ChatRequest(BaseModel):
session_id: str
message: str
# --- API Endpoints --- # --- API Endpoints ---
@app.get("/") @app.get("/")
@@ -214,9 +240,21 @@ async def start_analysis(request: StartRequest, background_tasks: BackgroundTask
files = [os.path.abspath(f) for f in files] # Only use absolute paths files = [os.path.abspath(f) for f in files] # Only use absolute paths
background_tasks.add_task(run_analysis_task, session_id, files, request.requirement) background_tasks.add_task(run_analysis_task, session_id, files, request.requirement, is_followup=False)
return {"status": "started", "session_id": session_id} return {"status": "started", "session_id": session_id}
@app.post("/api/chat")
async def chat_analysis(request: ChatRequest, background_tasks: BackgroundTasks):
session = session_manager.get_session(request.session_id)
if not session:
raise HTTPException(status_code=404, detail="Session not found")
if session.is_running:
raise HTTPException(status_code=400, detail="Analysis already in progress")
background_tasks.add_task(run_analysis_task, request.session_id, [], request.message, is_followup=True)
return {"status": "started"}
@app.get("/api/status") @app.get("/api/status")
async def get_status(session_id: str = Query(..., description="Session ID")): async def get_status(session_id: str = Query(..., description="Session ID")):
session = session_manager.get_session(session_id) session = session_manager.get_session(session_id)
@@ -235,6 +273,27 @@ async def get_status(session_id: str = Query(..., description="Session ID")):
"report_path": session.generated_report "report_path": session.generated_report
} }
@app.get("/api/export")
async def export_session(session_id: str = Query(..., description="Session ID")):
session = session_manager.get_session(session_id)
if not session:
raise HTTPException(status_code=404, detail="Session not found")
if not session.output_dir or not os.path.exists(session.output_dir):
raise HTTPException(status_code=404, detail="No data available for export")
# Create a zip file
import shutil
# We want to zip the contents of session_output_dir
# Zip path should be outside to avoid recursive zipping if inside
zip_base_name = os.path.join("outputs", f"export_{session_id}")
# shutil.make_archive expects base_name (without extension) and root_dir
archive_path = shutil.make_archive(zip_base_name, 'zip', session.output_dir)
return FileResponse(archive_path, media_type='application/zip', filename=f"analysis_export_{session_id}.zip")
@app.get("/api/report") @app.get("/api/report")
async def get_report(session_id: str = Query(..., description="Session ID")): async def get_report(session_id: str = Query(..., description="Session ID")):
session = session_manager.get_session(session_id) session = session_manager.get_session(session_id)
@@ -250,8 +309,24 @@ async def get_report(session_id: str = Query(..., description="Session ID")):
# Fix image paths # Fix image paths
relative_session_path = os.path.relpath(session.output_dir, os.getcwd()) relative_session_path = os.path.relpath(session.output_dir, os.getcwd())
web_base_path = f"/{relative_session_path}" web_base_path = f"/{relative_session_path}"
# Robust image path replacement
# 1. Replace explicit relative paths ./image.png
content = content.replace("](./", f"]({web_base_path}/") content = content.replace("](./", f"]({web_base_path}/")
# 2. Replace naked paths that might be generated like ](image.png) but NOT ](http...) or ](/...)
import re
def replace_link(match):
alt = match.group(1)
url = match.group(2)
if url.startswith("http") or url.startswith("/") or url.startswith("data:"):
return match.group(0)
# Remove ./ if exists again just in case
clean_url = url.lstrip("./")
return f"![{alt}]({web_base_path}/{clean_url})"
content = re.sub(r'!\[(.*?)\]\((.*?)\)', replace_link, content)
return {"content": content, "base_path": web_base_path} return {"content": content, "base_path": web_base_path}
@app.get("/api/figures") @app.get("/api/figures")

View File

@@ -10,7 +10,9 @@
<!-- Google Fonts --> <!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap"
rel="stylesheet">
<!-- Markdown Parser --> <!-- Markdown Parser -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Font Awesome --> <!-- Font Awesome -->
@@ -110,63 +112,74 @@
</div> </div>
</div> </div>
<!-- VIEW: TOOLS --> <!-- Follow-up Chat Section -->
<div id="viewTools" class="view-section" style="display: none;"> <div id="followUpSection" class="chat-input-container" style="display: none;">
<div class="tools-grid"> <div class="input-wrapper">
<!-- Tool Card 1: Merge Excel --> <textarea id="followUpInput"
<div class="tool-card"> placeholder="Follow-up question... (e.g. 'Analyze the error codes')"></textarea>
<div class="tool-icon"><i class="fa-solid fa-file-csv"></i></div> <button id="sendFollowUpBtn" class="btn primary" onclick="sendFollowUp()">
<h3>Excel to CSV Merger</h3> <i class="fa-solid fa-paper-plane"></i>
<p>Merge multiple .xlsx files from the uploads directory into a single CSV for analysis.</p> </button>
<div class="tool-actions">
<input type="text" id="mergeSource" value="uploads" placeholder="Source Directory"
class="input-sm">
<button class="btn secondary" onclick="triggerMerge()">
<i class="fa-solid fa-bolt"></i> Merge Now
</button>
</div>
<div id="mergeResult" class="tool-result"></div>
</div>
<!-- Tool Card 2: Sort CSV -->
<div class="tool-card">
<div class="tool-icon"><i class="fa-solid fa-arrow-down-a-z"></i></div>
<h3>Time Sorter</h3>
<p>Sort a CSV file by 'SendTime' or time column to fix ordering issues.</p>
<div class="tool-actions">
<input type="text" id="sortTarget" value="cleaned_data.csv" placeholder="Target Filename"
class="input-sm">
<button class="btn secondary" onclick="triggerSort()">
<i class="fa-solid fa-sort"></i> Sort Now
</button>
</div>
<div id="sortResult" class="tool-result"></div>
</div>
</div> </div>
</div> </div>
</div>
<!-- VIEW: GALLERY --> <!-- VIEW: TOOLS -->
<div id="viewGallery" class="view-section" style="display: none;"> <div id="viewTools" class="view-section" style="display: none;">
<div id="galleryContainer" class="gallery-grid"> <div class="tools-grid">
<div class="empty-state"> <!-- Tool Card 1: Merge Excel -->
<i class="fa-solid fa-images"></i> <div class="tool-card">
<p>No images generated yet.<br>Start an analysis to see results here.</p> <div class="tool-icon"><i class="fa-solid fa-file-csv"></i></div>
</div> <h3>Excel to CSV Merger</h3>
<p>Merge multiple .xlsx files from the uploads directory into a single CSV for analysis.</p>
<div class="tool-actions">
<input type="text" id="mergeSource" value="uploads" placeholder="Source Directory" class="input-sm">
<button class="btn secondary" onclick="triggerMerge()">
<i class="fa-solid fa-bolt"></i> Merge Now
</button>
</div> </div>
<div id="mergeResult" class="tool-result"></div>
</div> </div>
<!-- VIEW: HELP --> <!-- Tool Card 2: Sort CSV -->
<div id="viewHelp" class="view-section" style="display: none;"> <div class="tool-card">
<div class="help-header"> <div class="tool-icon"><i class="fa-solid fa-arrow-down-a-z"></i></div>
<h2>Troubleshooting Guide</h2> <h3>Time Sorter</h3>
<button class="btn secondary" onclick="fetchHelp()">Refresh</button> <p>Sort a CSV file by 'SendTime' or time column to fix ordering issues.</p>
</div> <div class="tool-actions">
<div id="helpContainer" class="markdown-body help-body"> <input type="text" id="sortTarget" value="cleaned_data.csv" placeholder="Target Filename"
<p>Loading guide...</p> class="input-sm">
<button class="btn secondary" onclick="triggerSort()">
<i class="fa-solid fa-sort"></i> Sort Now
</button>
</div> </div>
<div id="sortResult" class="tool-result"></div>
</div> </div>
</div>
</div>
</main> <!-- VIEW: GALLERY -->
<div id="viewGallery" class="view-section" style="display: none;">
<div id="galleryContainer" class="gallery-grid">
<div class="empty-state">
<i class="fa-solid fa-images"></i>
<p>No images generated yet.<br>Start an analysis to see results here.</p>
</div>
</div>
</div>
<!-- VIEW: HELP -->
<div id="viewHelp" class="view-section" style="display: none;">
<div class="help-header">
<h2>Troubleshooting Guide</h2>
<button class="btn secondary" onclick="fetchHelp()">Refresh</button>
</div>
<div id="helpContainer" class="markdown-body help-body">
<p>Loading guide...</p>
</div>
</div>
</main>
</div> </div>
<script src="/static/script.js"></script> <script src="/static/script.js"></script>

View File

@@ -111,9 +111,11 @@ function setRunningState(running) {
if (running) { if (running) {
statusDot.className = 'dot running'; statusDot.className = 'dot running';
statusText.innerText = 'Analysis in Progress'; statusText.innerText = 'Analysis in Progress';
if (document.getElementById('followUpSection')) document.getElementById('followUpSection').style.display = 'none';
} else { } else {
statusDot.className = 'dot done'; statusDot.className = 'dot done';
statusText.innerText = 'Completed'; statusText.innerText = 'Completed';
if (currentSessionId && document.getElementById('followUpSection')) document.getElementById('followUpSection').style.display = 'flex';
} }
} }
@@ -215,6 +217,7 @@ async function loadGallery() {
} }
} }
// --- Export Report ---
// --- Export Report --- // --- Export Report ---
window.triggerExport = async function () { window.triggerExport = async function () {
if (!currentSessionId) { if (!currentSessionId) {
@@ -228,17 +231,29 @@ window.triggerExport = async function () {
btn.disabled = true; btn.disabled = true;
try { try {
// Trigger download
// We can't use fetch for file download easily if we want browser to handle save dialog
// So we create a link approach or check status first
// Check if download is possible
const url = `/api/export?session_id=${currentSessionId}`; const url = `/api/export?session_id=${currentSessionId}`;
const res = await fetch(url, { method: 'HEAD' }); const res = await fetch(url); // Default GET
if (res.ok) { if (res.ok) {
window.location.href = url; const blob = await res.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `analysis_export_${currentSessionId}.zip`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(downloadUrl);
} else { } else {
alert("Export failed: No data available."); // Try to parse error
let errMsg = "Unknown error";
try {
const err = await res.json();
errMsg = err.detail || errMsg;
} catch (jsonErr) {
errMsg = res.statusText;
}
alert("Export failed: " + errMsg);
} }
} catch (e) { } catch (e) {
alert("Export failed: " + e.message); alert("Export failed: " + e.message);
@@ -248,6 +263,45 @@ window.triggerExport = async function () {
} }
} }
// --- Follow-up Chat ---
window.sendFollowUp = async function () {
if (!currentSessionId || isRunning) return;
const input = document.getElementById('followUpInput');
const message = input.value.trim();
if (!message) return;
// UI Loading state
const btn = document.getElementById('sendFollowUpBtn');
btn.disabled = true;
input.disabled = true;
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: currentSessionId, message: message })
});
if (res.ok) {
input.value = '';
setRunningState(true);
startPolling();
switchTab('logs');
} else {
const err = await res.json();
alert('Error: ' + err.detail);
}
} catch (e) {
console.error(e);
alert('Failed to send request');
} finally {
btn.disabled = false;
input.disabled = false;
}
}
// --- Tabs (Inner) --- // --- Tabs (Inner) ---
window.switchTab = function (tabName) { window.switchTab = function (tabName) {

View File

@@ -1,14 +1,28 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
:root { :root {
--bg-color: #0f172a; /* Nebula Theme Palette */
--sidebar-bg: rgba(30, 41, 59, 0.7); --bg-deep: #020617;
--glass-border: 1px solid rgba(255, 255, 255, 0.1); --bg-secondary: #0f172a;
--sidebar-bg: rgba(15, 23, 42, 0.6);
--card-bg: rgba(30, 41, 59, 0.3);
--glass-border: 1px solid rgba(148, 163, 184, 0.08);
--primary-color: #6366f1; --primary-color: #6366f1;
--primary-hover: #4f46e5; --primary-glow: #6366f1aa;
--accent-color: #0ea5e9;
--text-main: #f8fafc; --text-main: #f8fafc;
--text-muted: #94a3b8; --text-muted: #94a3b8;
--card-bg: rgba(30, 41, 59, 0.4);
--success: #10b981; --success: #10b981;
--warning: #f59e0b; --warning: #f59e0b;
--danger: #ef4444;
--radius-lg: 1rem;
--radius-md: 0.75rem;
--radius-sm: 0.5rem;
} }
* { * {
@@ -19,369 +33,469 @@
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
background-color: var(--bg-color); background-color: var(--bg-deep);
background-image: background-image:
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%), radial-gradient(circle at 15% 50%, rgba(99, 102, 241, 0.08) 0%, transparent 50%),
radial-gradient(at 100% 0%, rgba(16, 185, 129, 0.15) 0px, transparent 50%); radial-gradient(circle at 85% 30%, rgba(14, 165, 233, 0.08) 0%, transparent 50%);
color: var(--text-main); color: var(--text-main);
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
} }
.app-container { .app-container {
display: flex; display: flex;
height: 100vh; height: 96vh;
width: 98vw;
gap: 1rem;
border-radius: var(--radius-lg);
overflow: hidden;
} }
/* Sidebar */ /* --- Floating Sidebar --- */
.sidebar { .sidebar {
width: 320px; width: 280px;
background: var(--sidebar-bg); background: var(--sidebar-bg);
backdrop-filter: blur(12px); backdrop-filter: blur(20px);
border-right: var(--glass-border); -webkit-backdrop-filter: blur(20px);
border: var(--glass-border);
border-radius: var(--radius-lg);
padding: 1.5rem; padding: 1.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2rem; gap: 2rem;
transition: all 0.3s ease;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
} }
.brand { .brand {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
color: var(--text-main); padding: 0.5rem 0.5rem 1rem 0.5rem;
border-bottom: var(--glass-border);
} }
.brand i { .brand i {
font-size: 1.5rem; font-size: 1.75rem;
color: var(--primary-color); background: linear-gradient(135deg, var(--primary-color), var(--accent-color));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.5));
} }
.brand h1 { .brand h1 {
font-size: 1.25rem; font-family: 'Outfit', sans-serif;
font-size: 1.5rem;
font-weight: 700; font-weight: 700;
letter-spacing: -0.02em;
background: linear-gradient(to right, #fff, #94a3b8);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Nav Buttons */
.nav-btn {
width: 100%;
padding: 0.875rem 1rem;
border: none;
background: transparent;
color: var(--text-muted);
font-family: 'Outfit', sans-serif;
font-weight: 500;
text-align: left;
cursor: pointer;
border-radius: var(--radius-md);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.95rem;
}
.nav-btn i {
font-size: 1.1rem;
transition: transform 0.2s;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.03);
color: var(--text-main);
transform: translateX(4px);
}
.nav-btn.active {
background: linear-gradient(90deg, rgba(99, 102, 241, 0.1), transparent);
color: var(--primary-color);
border-left: 3px solid var(--primary-color);
border-radius: 4px var(--radius-md) var(--radius-md) 4px;
}
.nav-btn.active i {
color: var(--primary-color);
transform: scale(1.1);
}
/* --- Main Content --- */
.main-content {
flex: 1;
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(12px);
border: var(--glass-border);
border-radius: var(--radius-lg);
padding: 2rem;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
/* Controls */
.control-group-wrapper {
display: flex;
flex-direction: column;
gap: 1.5rem;
} }
.control-group h3 { .control-group h3 {
font-size: 0.875rem; font-family: 'Outfit', sans-serif;
color: var(--text-muted); font-size: 0.75rem;
color: #475569;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.1em;
font-weight: 700;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
/* Upload Zone */ /* Upload Zone */
.upload-zone { .upload-zone {
border: 2px dashed rgba(255, 255, 255, 0.1); border: 1px dashed rgba(255, 255, 255, 0.1);
border-radius: 0.75rem; border-radius: 1rem;
padding: 2rem 1rem; padding: 1.5rem;
text-align: center; text-align: center;
transition: all 0.2s; transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.02); background: rgba(2, 6, 23, 0.3);
position: relative;
overflow: hidden;
}
.upload-zone::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at center, rgba(99, 102, 241, 0.1), transparent 70%);
opacity: 0;
transition: opacity 0.3s;
}
.upload-zone:hover::before {
opacity: 1;
} }
.upload-zone.dragover { .upload-zone.dragover {
border-color: var(--primary-color); border-color: var(--primary-color);
background: rgba(99, 102, 241, 0.1); box-shadow: 0 0 20px rgba(99, 102, 241, 0.2);
} }
.upload-zone i { .upload-zone i {
font-size: 2rem; font-size: 2rem;
color: var(--text-muted); background: linear-gradient(to bottom, #94a3b8, #475569);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.upload-zone p { .file-list {
font-size: 0.875rem; margin-top: 0.5rem;
color: var(--text-muted); display: flex;
margin-bottom: 1rem; flex-direction: column;
} gap: 0.25rem;
/* Inputs */
textarea {
width: 100%;
height: 120px;
background: rgba(0, 0, 0, 0.2);
border: var(--glass-border);
border-radius: 0.5rem;
padding: 0.75rem;
color: var(--text-main);
font-family: inherit;
resize: none;
font-size: 0.875rem;
margin-bottom: 1rem;
}
textarea:focus {
outline: none;
border-color: var(--primary-color);
} }
/* Buttons */ /* Buttons */
.btn { .btn {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.875rem;
border-radius: 0.5rem; border-radius: var(--radius-md);
border: none; border: none;
cursor: pointer; cursor: pointer;
font-family: 'Outfit', sans-serif;
font-weight: 600; font-weight: 600;
transition: all 0.2s; font-size: 0.9rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.5rem; gap: 0.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
} }
.btn.primary { .btn.primary {
background: var(--primary-color); background: linear-gradient(135deg, var(--primary-color), var(--primary-hover, #4f46e5));
color: white; color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
} }
.btn.primary:hover { .btn.primary:hover {
background: var(--primary-hover); box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
transform: translateY(-1px); transform: translateY(-1px);
} }
.btn.primary::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: 0.5s;
}
.btn.primary:hover::after {
left: 100%;
}
.btn.secondary { .btn.secondary {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.05);
color: var(--text-main); color: var(--text-main);
font-size: 0.8rem; border: 1px solid rgba(255, 255, 255, 0.05);
padding: 0.5rem;
width: auto;
margin: 0 auto;
} }
.btn:disabled { .btn.secondary:hover {
opacity: 0.5; background: rgba(255, 255, 255, 0.1);
cursor: not-allowed; border-color: rgba(255, 255, 255, 0.2);
transform: none;
} }
/* Status */ /* Inputs */
.status-indicator { textarea,
margin-top: auto; input[type="text"] {
width: 100%;
background: rgba(2, 6, 23, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: var(--radius-md);
padding: 1rem;
color: var(--text-main);
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
transition: all 0.2s;
}
textarea:focus,
input[type="text"]:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}
/* Tabs */
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
background: rgba(2, 6, 23, 0.2);
padding: 0.3rem;
border-radius: var(--radius-lg);
width: fit-content;
}
.tab-btn {
padding: 0.6rem 1.25rem;
border: none;
background: transparent;
color: var(--text-muted);
font-family: 'Outfit', sans-serif;
font-weight: 500;
font-size: 0.85rem;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
font-size: 0.875rem;
padding-top: 1rem;
border-top: var(--glass-border);
} }
.dot { .tab-btn:hover {
width: 8px; color: var(--text-main);
height: 8px;
border-radius: 50%;
background-color: var(--text-muted);
} }
.dot.running { .tab-btn.active {
background-color: var(--warning); background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 8px var(--warning); color: white;
animation: pulse 1s infinite; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.dot.done { /* Terminal / Log Window */
background-color: var(--success);
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
/* Main Content */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 1.5rem;
gap: 1rem;
overflow: hidden;
min-height: 0;
/* Crucial for nested flex scroll */
}
/* View Section needs to fill space for analysis view */
.view-section {
flex: 1;
display: none;
overflow: hidden;
min-height: 0;
}
.view-section.active {
display: flex;
/* Changed from block to flex for analysis view layout */
flex-direction: column;
}
.tabs {
flex-shrink: 0;
/* Prevent tabs from shrinking */
display: flex;
gap: 1rem;
border-bottom: var(--glass-border);
padding-bottom: 0.5rem;
}
/* ... existing tab-btn styles ... */
.tab-content {
display: none;
flex: 1;
overflow: hidden;
min-height: 0;
/* Crucial for nested flex scroll */
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: flex;
flex-direction: column;
}
/* Logs */
.terminal-window { .terminal-window {
background: #000; background: #09090b;
border-radius: 0.75rem; border-radius: var(--radius-lg);
padding: 1rem; padding: 1.5rem;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
min-height: 0; border: 1px solid rgba(255, 255, 255, 0.05);
/* Crucial for nested flex scroll */ font-family: 'JetBrains Mono', monospace;
border: var(--glass-border);
font-family: 'JetBrains Mono', 'Menlo', monospace;
font-size: 0.85rem; font-size: 0.85rem;
color: #4ade80; color: #22c55e;
line-height: 1.5; box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);
min-height: 0;
} }
#logOutput { .terminal-window::-webkit-scrollbar {
white-space: pre-wrap; width: 8px;
} }
/* Report */ .terminal-window::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
/* Report Container */
#reportContainer { #reportContainer {
background: rgba(255, 255, 255, 0.95);
color: #1e293b;
border-radius: var(--radius-lg);
padding: 2.5rem;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
background: white; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
color: #1e293b;
border-radius: 0.75rem;
padding: 2rem;
} }
.empty-state { .empty-state {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #94a3b8; height: 100%;
color: rgba(255, 255, 255, 0.2);
gap: 1rem; gap: 1rem;
} }
.empty-state i { .empty-state i {
font-size: 3rem; font-size: 4rem;
opacity: 0.5;
} }
/* Markdown Styles Override for Report */ /* Follow-up Chat */
.markdown-body img { .chat-input-container {
max-width: 100%; margin-top: 1rem;
border-radius: 0.5rem; background: rgba(2, 6, 23, 0.4);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3 {
margin-top: 1.5em;
margin-bottom: 0.5em;
color: #111827;
}
.markdown-body p {
margin-bottom: 1em;
line-height: 1.7;
}
/* New: Tools Grid */
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
padding: 1rem 0;
overflow-y: auto;
}
.tool-card {
background: var(--card-bg);
border: var(--glass-border); border: var(--glass-border);
border-radius: 1rem; border-radius: var(--radius-lg);
padding: 1.5rem; padding: 0.5rem;
transition: transform 0.2s;
} }
.tool-card:hover { .input-wrapper {
transform: translateY(-2px);
border-color: var(--primary-color);
}
.tool-icon {
font-size: 2rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
.tool-card h3 {
margin-bottom: 0.5rem;
}
.tool-card p {
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 1.5rem;
min-height: 3em;
}
.tool-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
align-items: flex-end;
} }
.input-sm { #followUpInput {
background: rgba(0, 0, 0, 0.3); background: transparent;
border: var(--glass-border); border: none;
color: white; height: 50px;
padding: 0.5rem; padding: 0.8rem;
border-radius: 0.5rem; resize: none;
flex: 1; box-shadow: none;
} }
/* Help Styles */ #sendFollowUpBtn {
.help-header { width: 50px;
display: flex; height: 50px;
justify-content: space-between; border-radius: var(--radius-md);
align-items: center; padding: 0;
margin-bottom: 1.5rem; flex-shrink: 0;
} }
.help-body { /* Gallery Grid */
background: white; .gallery-grid {
color: #1e293b; display: grid;
padding: 2rem; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
border-radius: 0.75rem; gap: 1.5rem;
padding-bottom: 2rem;
overflow-y: auto; overflow-y: auto;
flex: 1; }
.gallery-card {
background: rgba(255, 255, 255, 0.03);
border: var(--glass-border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: all 0.3s;
}
.gallery-card:hover {
transform: translateY(-4px);
border-color: var(--primary-color);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.img-wrapper {
height: 180px;
overflow: hidden;
background: rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.img-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s;
}
.img-wrapper:hover img {
transform: scale(1.05);
}
.card-content {
padding: 1rem;
}
.card-content h4 {
font-family: 'Outfit', sans-serif;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.card-content .desc {
color: var(--text-muted);
font-size: 0.85rem;
}
/* Status Dot */
.status-indicator {
margin-top: auto;
padding-top: 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--text-muted);
font-size: 0.85rem;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.view-section {
animation: fadeIn 0.4s ease-out;
width: 100%;
height: 100%;
} }