JC321 commited on
Commit
8f0908c
·
1 Parent(s): f833e71

change api to mcp

Browse files
Files changed (5) hide show
  1. EasyFinancialAgent/chat_direct.py +52 -4
  2. app copy 2.py +1915 -0
  3. app copy 3.py +1657 -0
  4. app.py +200 -376
  5. service/report_service.py +3 -3
EasyFinancialAgent/chat_direct.py CHANGED
@@ -61,22 +61,27 @@ except ImportError as e:
61
 
62
  def search_company_direct(company_input):
63
  """
64
- 搜索公司信息(直接调用)
 
 
65
 
66
  Args:
67
- company_input: 公司名称或代码
68
 
69
  Returns:
70
- 搜索结果
71
 
72
  Example:
73
  result = search_company_direct("Apple")
 
 
74
  """
75
  if not MCP_DIRECT_AVAILABLE:
76
  return {"error": "MCP functions not available"}
77
 
78
  try:
79
- return _advanced_search_company(company_input)
 
80
  except Exception as e:
81
  return {"error": str(e)}
82
 
@@ -125,6 +130,49 @@ def get_company_filings_direct(cik):
125
  return {"error": str(e)}
126
 
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  # ============================================================
129
  # 便捷方法 - 财务数据相关
130
  # ============================================================
 
61
 
62
  def search_company_direct(company_input):
63
  """
64
+ 批量搜索公司信息(直接调用)
65
+
66
+ 使用 advanced_search_company 工具,支持公司名称、Ticker 或 CIK 代码
67
 
68
  Args:
69
+ company_input: 公司名称、Ticker 代码或 CIK 代码
70
 
71
  Returns:
72
+ 批量搜索结果
73
 
74
  Example:
75
  result = search_company_direct("Apple")
76
+ result = search_company_direct("AAPL")
77
+ result = search_company_direct("0000320193")
78
  """
79
  if not MCP_DIRECT_AVAILABLE:
80
  return {"error": "MCP functions not available"}
81
 
82
  try:
83
+ result = _advanced_search_company(company_input)
84
+ return [result]
85
  except Exception as e:
86
  return {"error": str(e)}
87
 
 
130
  return {"error": str(e)}
131
 
132
 
133
+ def advanced_search_company_detailed(company_input):
134
+ """
135
+ 高级公司搜索 - 支持公司名称、Ticker 或 CIK 的强大搜索方法
136
+
137
+ 不同于 search_company_direct,该方法来自 EasyReportDataMCP 中的 mcp_server_fastmcp
138
+ 更具有灵活性,可以自动检测输入的类型
139
+
140
+ Args:
141
+ company_input: 公司名称 ("Tesla", "Apple Inc")
142
+ Ticker 代码 ("TSLA", "AAPL", "MSFT")
143
+ CIK 代码 ("0001318605", "0000320193")
144
+
145
+ Returns:
146
+ dict: 包含以下信息:
147
+ - cik: 公司的 Central Index Key
148
+ - name: 办公室注册名称
149
+ - tickers: 股票代码
150
+ - sic: Standard Industrial Classification 代码
151
+ - sic_description: 行业/行业描述
152
+
153
+ Example:
154
+ # 按公司名称搜索
155
+ result = advanced_search_company_detailed("Tesla")
156
+ # 按 Ticker 搜索
157
+ result = advanced_search_company_detailed("TSLA")
158
+ # 按 CIK 搜索
159
+ result = advanced_search_company_detailed("0001318605")
160
+ """
161
+ if not MCP_DIRECT_AVAILABLE:
162
+ return {"error": "MCP functions not available"}
163
+
164
+ try:
165
+ # 直接调用 advanced_search_company 工具
166
+ result = _advanced_search_company(company_input)
167
+ return result
168
+ except Exception as e:
169
+ import traceback
170
+ return {
171
+ "error": str(e),
172
+ "traceback": traceback.format_exc()
173
+ }
174
+
175
+
176
  # ============================================================
177
  # 便捷方法 - 财务数据相关
178
  # ============================================================
app copy 2.py ADDED
@@ -0,0 +1,1915 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import datetime
4
+ import re
5
+ import pandas as pd
6
+ from sqlalchemy import true
7
+ # from dotenv import load_dotenv
8
+
9
+ # # 加载.env文件中的环境变量
10
+ # load_dotenv()
11
+ # from EasyFinancialAgent.chat import query_company
12
+ from chatbot.chat_main import respond
13
+ import globals as g
14
+ from service.mysql_service import get_companys, insert_company, get_company_by_name
15
+ from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot
16
+ from service.company import check_company_exists
17
+ from service.hf_upload import get_hf_files_with_links
18
+ from MarketandStockMCP.news_quote_mcp import get_company_news, get_quote
19
+ from EasyReportDataMCP.report_mcp import query_financial_data
20
+ from service.report_service import get_report_data, query_company_advanced
21
+ from service.report_tools import build_financial_metrics_three_year_data, calculate_yoy_comparison, extract_financial_table, extract_last_three_with_fallback, get_yearly_data
22
+ from service.three_year_table_tool import build_table_format
23
+ from service.three_year_tool import process_financial_data_with_metadata
24
+ from service.tool_processor import get_stock_price
25
+
26
+
27
+ get_companys_state = True
28
+ my_companies = []
29
+ # JavaScript代码用于读取和存储数据
30
+ js_code = """
31
+ function handleStorage(operation, key, value) {
32
+ if (operation === 'set') {
33
+ localStorage.setItem(key, value);
34
+ return `已存储: ${key} = ${value}`;
35
+ } else if (operation === 'get') {
36
+ let storedValue = localStorage.getItem(key);
37
+ if (storedValue === null) {
38
+ return `未找到键: ${key}`;
39
+ }
40
+ return `读取到: ${key} = ${storedValue}`;
41
+ } else if (operation === 'clear') {
42
+ localStorage.removeItem(key);
43
+ return `已清除: ${key}`;
44
+ } else if (operation === 'clearAll') {
45
+ localStorage.clear();
46
+ return '已清除所有数据';
47
+ }
48
+ }
49
+ """
50
+ custom_css = """
51
+ /* 匹配所有以 gradio-container- 开头的类 */
52
+ div[class^="gradio-container-"],
53
+ div[class*=" gradio-container-"] {
54
+ -webkit-text-size-adjust: 100% !important;
55
+ line-height: 1.5 !important;
56
+ font-family: unset !important;
57
+ -moz-tab-size: 4 !important;
58
+ tab-size: 4 !important;
59
+ }
60
+
61
+ .company-list-container {
62
+ background-color: white;
63
+ border-radius: 0.5rem;
64
+ padding: 0.75rem;
65
+ margin-bottom: 0.75rem;
66
+ border: 1px solid #e5e7eb;
67
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
68
+ width: 100%;
69
+ }
70
+
71
+ /* 隐藏单选框 */
72
+ .company-list-container input[type="radio"] {
73
+ display: none;
74
+ }
75
+
76
+ /* 自定义选项样式 */
77
+ .company-list-container label {
78
+ display: block;
79
+ padding: 0.75rem 1rem;
80
+ margin: 0.25rem 0;
81
+ border-radius: 0.375rem;
82
+ cursor: pointer;
83
+ transition: all 0.2s ease;
84
+ background-color: #f9fafb;
85
+ border: 1px solid #e5e7eb;
86
+ font-size: 1rem;
87
+ text-align: left;
88
+ width: 100%;
89
+ box-sizing: border-box;
90
+ }
91
+
92
+ /* 悬停效果 */
93
+ .company-list-container label:hover {
94
+ background-color: #f3f4f6;
95
+ border-color: #d1d5db;
96
+ }
97
+
98
+ /* 选中效果 - 确保背景色充满整个选项 */
99
+ .company-list-container input[type="radio"]:checked + span {
100
+ # background: #3b82f6 !important;
101
+ color: white !important;
102
+ font-weight: 600 !important;
103
+ display: block;
104
+ width: 100%;
105
+ height: 100%;
106
+ padding: 0.75rem 1rem;
107
+ margin: -0.75rem -1rem;
108
+ border-radius: 0.375rem;
109
+ }
110
+
111
+ .company-list-container span {
112
+ display: block;
113
+ padding: 0;
114
+ border-radius: 0.375rem;
115
+ width: 100%;
116
+ }
117
+
118
+ /* 确保每行只有一个选项 */
119
+ .company-list-container .wrap {
120
+ display: block !important;
121
+ }
122
+
123
+ .company-list-container .wrap li {
124
+ display: block !important;
125
+ width: 100% !important;
126
+ }
127
+ label.selected {
128
+ background: #3b82f6 !important;
129
+ color: white !important;
130
+ }
131
+ """
132
+
133
+ # 全局变量用于存储公司映射关系
134
+ companies_map = {}
135
+
136
+ # 根据公司名称获取股票代码的函数
137
+ def get_stock_code_by_company_name(company_name):
138
+ """根据公司名称获取股票代码"""
139
+ if company_name in companies_map and "CODE" in companies_map[company_name]:
140
+ return companies_map[company_name]["CODE"]
141
+ return "" # 默认返回
142
+
143
+ # 创建一个简单的函数来获取公司列表
144
+ def get_company_list_choices():
145
+ choices = []
146
+ print(f"Getting init add company list choices...{get_companys_state}")
147
+ if not get_companys_state:
148
+ return gr.update(choices=choices)
149
+ try:
150
+ # companies_data = get_companys()
151
+ companies_data = my_companies
152
+ print(f"Getting init add company list choices...companies_data: {companies_data}")
153
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
154
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
155
+ else:
156
+ choices = []
157
+ except:
158
+ choices = []
159
+
160
+ return gr.update(choices=choices)
161
+
162
+ # Sidebar service functions
163
+
164
+ # 处理公司点击事件的函数
165
+ def handle_company_click(company_name):
166
+ """处理公司点击事件,先判断是否已经入库,如果没有则进行入库操作,然后刷新公司列表"""
167
+ print(f"Handling click for company: {company_name}")
168
+
169
+ # 1. 判断是否已经入库
170
+ if not check_company_exists(my_companies, company_name):
171
+ # 2. 如果没有入库,则进行入库操作
172
+ # 获取股票代码(如果有的话)
173
+ stock_code = companies_map.get(company_name, {}).get("CODE", "Unknown")
174
+ print(f"Inserting company {company_name} with code {stock_code}")
175
+
176
+ # 插入公司到数据库
177
+ # success = insert_company(company_name, stock_code)
178
+ my_companies.append({"company_name": company_name, "stock_code": stock_code})
179
+ print(f"Successfully inserted company: {company_name}") # 直接更新companies_map,而不是重新加载整个映射
180
+ # 直接更新companies_map,而不是重新加载整个映射
181
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
182
+ # 使用Gradio的成功提示
183
+ gr.Info(f"Successfully added company: {company_name}")
184
+ # 返回True表示添加成功,需要刷新列表
185
+ return True
186
+ else:
187
+ print(f"Company {company_name} already exists in database")
188
+ # 使用Gradio的警告提示
189
+ gr.Warning(f"Company '{company_name}' already exists")
190
+
191
+ # 3. 返回成功响应
192
+ return None
193
+
194
+ def get_company_list_html(selected_company=""):
195
+ try:
196
+ # 从数据库获取所有公司
197
+ # companies_data = get_companys()
198
+ companies_data = my_companies
199
+ # 检查是否为错误信息
200
+ if isinstance(companies_data, str):
201
+ if "查询执行失败" in companies_data:
202
+ return "<div class='text-red-500'>获取公司列表失败</div>"
203
+ else:
204
+ # 如果是字符串但不是错误信息,可能需要特殊处理
205
+ return ""
206
+
207
+ # 检查是否为DataFrame且为空
208
+ if not isinstance(companies_data, pd.DataFrame) or companies_data.empty:
209
+ return ""
210
+
211
+ # 生成HTML列表
212
+ html_items = []
213
+ for _, row in companies_data.iterrows():
214
+ company_name = row.get('company_name', 'Unknown')
215
+ # 根据是否选中添加不同的样式类
216
+ css_class = "company-item"
217
+ if company_name == selected_company:
218
+ css_class += " selected-company"
219
+ # 使用button元素来确保可点击性
220
+ html_items.append(f'<button class="{css_class}" data-company="{company_name}" style="width:100%; text-align:left; border:none; background:none;">{company_name}</button>')
221
+
222
+ return "\n".join(html_items)
223
+ except Exception as e:
224
+ return f"<div class='text-red-500'>生成公司列表失败: {str(e)}</div>"
225
+
226
+ def initialize_company_list(selected_company=""):
227
+ return get_company_list_html(selected_company)
228
+
229
+ def refresh_company_list(selected_company=""):
230
+ """刷新公司列表,返回最新的HTML内容,带loading效果"""
231
+ # 先返回loading状态
232
+ loading_html = '''
233
+ <div style="display: flex; justify-content: center; align-items: center; height: 100px;">
234
+ <div class="loading-spinner" style="width: 24px; height: 24px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite;"></div>
235
+ <style>
236
+ @keyframes spin {
237
+ 0% { transform: rotate(0deg); }
238
+ 100% { transform: rotate(360deg); }
239
+ }
240
+ </style>
241
+ </div>
242
+ '''
243
+ yield loading_html
244
+
245
+ # 然后返回实际的数据
246
+ yield get_company_list_html(selected_company)
247
+
248
+ # 新增函数:处理公司选择事件
249
+ def select_company(company_name):
250
+ """处理公司选择事件,更新全局状态并返回更新后的公司列表"""
251
+ # 更新全局变量
252
+ g.SELECT_COMPANY = company_name if company_name else ""
253
+ # 对于Radio组件,我们只需要返回更新后的选项列表
254
+ try:
255
+ # companies_data = get_companys()
256
+ companies_data = my_companies
257
+ if isinstance(companies_data, list) and len(companies_data) > 0:
258
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
259
+ choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
260
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
261
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
262
+ else:
263
+ choices = []
264
+ except:
265
+ choices = []
266
+ return gr.update(choices=choices, value=company_name)
267
+
268
+ def initialize_companies_map():
269
+ """初始化 companies_map 字典"""
270
+ global companies_map
271
+ companies_map = {} # 清空之前的映射
272
+
273
+ print("Initializing companies map...")
274
+
275
+ try:
276
+ # 获取预定义的公司列表
277
+ predefined_companies = [
278
+ { "NAME": "Alibaba", "CODE": "BABA" },
279
+ { "NAME": "NVIDIA", "CODE": "NVDA" },
280
+ { "NAME": "Amazon", "CODE": "AMZN" },
281
+ { "NAME": "Intel", "CODE": "INTC" },
282
+ { "NAME": "Meta", "CODE": "META" },
283
+ { "NAME": "Google", "CODE": "GOOGL" },
284
+ { "NAME": "Apple", "CODE": "AAPL" },
285
+ { "NAME": "Tesla", "CODE": "TSLA" },
286
+ { "NAME": "AMD", "CODE": "AMD" },
287
+ { "NAME": "Microsoft", "CODE": "MSFT" },
288
+ { "NAME": "ASML", "CODE": "ASML" }
289
+ ]
290
+
291
+ # 将预定义公司添加到映射中
292
+ for company in predefined_companies:
293
+ companies_map[company["NAME"]] = {"NAME": company["NAME"], "CODE": company["CODE"]}
294
+
295
+ print(f"Predefined companies added: {len(predefined_companies)}")
296
+
297
+ # 从数据库获取公司数据
298
+ # companies_data = get_companys()
299
+ companies_data = my_companies
300
+ # companies_data = window.cachedCompanies or []
301
+
302
+
303
+ print(f"Companies data from DB: {companies_data}")
304
+
305
+ # 如果数据库中有公司数据,则添加到映射中(去重)
306
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
307
+ print(f"Adding {len(companies_data)} companies from database")
308
+ for _, row in companies_data.iterrows():
309
+ company_name = row.get('company_name', 'Unknown')
310
+ stock_code = row.get('stock_code', '')
311
+
312
+ # 确保company_name和stock_code都是字符串类型
313
+ company_name = str(company_name) if company_name is not None else 'Unknown'
314
+ stock_code = str(stock_code) if stock_code is not None else ''
315
+
316
+ # 检查是否已存在于映射中(通过股票代码判断)
317
+ is_duplicate = False
318
+ for existing_company in companies_map.values():
319
+ if existing_company["CODE"] == stock_code:
320
+ is_duplicate = True
321
+ break
322
+
323
+ # 如果不重复,则添加到映射中
324
+ if not is_duplicate:
325
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
326
+ # print(f"Added company: {company_name}")
327
+ else:
328
+ print("No companies found in database")
329
+
330
+ print(f"Final companies map: {companies_map}")
331
+ except Exception as e:
332
+ # 错误处理
333
+ print(f"Error initializing companies map: {str(e)}")
334
+ pass
335
+
336
+ # Sidebar company selector functions
337
+ def update_company_choices(user_input: str):
338
+ """更新公司选择列表"""
339
+ # 第一次 yield:立即显示 modal + loading 提示
340
+ yield gr.update(
341
+ choices=["Searching..."],
342
+ visible=True
343
+ ), gr.update(visible=False, value="") # 添加第二个返回值
344
+
345
+ # 第二次:执行耗时操作(调用 LLM)
346
+ choices = search_company(user_input) # 这是你原来的同步函数
347
+
348
+ # 检查choices是否为错误信息
349
+ if len(choices) > 0 and isinstance(choices[0], str) and not choices[0].startswith("Searching"):
350
+ # 如果是错误信息或非正常格式,显示提示消息
351
+ error_message = choices[0] if len(choices) > 0 else "未知错误"
352
+ # 使用Ant Design风格的错误提示
353
+ error_html = f'''
354
+ <div class="ant-message ant-message-error" style="
355
+ position: fixed;
356
+ top: 20px;
357
+ left: 50%;
358
+ transform: translateX(-50%);
359
+ z-index: 10000;
360
+ padding: 10px 16px;
361
+ border-radius: 4px;
362
+ background: #fff;
363
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
364
+ display: flex;
365
+ align-items: center;
366
+ pointer-events: all;
367
+ animation: messageFadeIn 0.3s ease-in-out;
368
+ ">
369
+ <div style="
370
+ width: 16px;
371
+ height: 16px;
372
+ background: #ff4d4f;
373
+ border-radius: 50%;
374
+ position: relative;
375
+ margin-right: 8px;
376
+ "></div>
377
+ <span>{error_message}</span>
378
+ </div>
379
+ <script>
380
+ setTimeout(function() {{
381
+ var msg = document.querySelector('.ant-message-error');
382
+ if (msg) {{
383
+ msg.style.animation = 'messageFadeOut 0.3s ease-in-out';
384
+ setTimeout(function() {{ msg.remove(); }}, 3000);
385
+ }}
386
+ }}, 3000);
387
+ </script>
388
+ '''
389
+ yield gr.update(choices=["No results found"], visible=True), gr.update(visible=True, value=error_html)
390
+ else:
391
+ # 第三次:更新为真实结果
392
+ yield gr.update(
393
+ choices=choices,
394
+ visible=len(choices) > 0
395
+ ), gr.update(visible=False, value="")
396
+
397
+ def add_company(selected, current_list):
398
+ """添加选中的公司"""
399
+ if selected == "No results found":
400
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
401
+ if selected:
402
+ # print(f"Selected company====: {selected}")
403
+ # 从选择的文本中提取公司名称和股票代码
404
+ # 假设格式为 "公司名称 (股票代码)"
405
+ selected_clean = selected.strip()
406
+ match = re.match(r"^(.+?)\s*\(([^)]+)\)$", selected_clean)
407
+ if match:
408
+ company_name = match.group(1)
409
+ stock_code = match.group(2)
410
+ elif companies_map.get(selected_clean):
411
+ company_name = selected_clean
412
+ stock_code = companies_map[selected_clean]["CODE"]
413
+ else:
414
+ company_name = selected_clean
415
+ stock_code = "Unknown"
416
+
417
+ # print(f"Company name: {company_name}, Stock code: {stock_code}")
418
+ # print(f"Company exists: {check_company_exists(company_name)}")
419
+
420
+ if not check_company_exists(my_companies, company_name):
421
+ # 入库
422
+ # success = insert_company(company_name, stock_code)
423
+ my_companies.append({"company_name": company_name, "stock_code": stock_code})
424
+ # 从数据库获取更新后的公司列表
425
+ try:
426
+ # companies_data = get_companys()
427
+ companies_data = my_companies
428
+ if isinstance(companies_data, list) and len(companies_data) > 0:
429
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
430
+ updated_list = [str(item.get('company_name', 'Unknown')) for item in companies_data]
431
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
432
+ updated_list = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
433
+ else:
434
+ updated_list = []
435
+ except:
436
+ updated_list = []
437
+
438
+ # 添加默认公司选项
439
+ if not updated_list:
440
+ updated_list = ['Alibaba', '腾讯控股', 'Tencent', '阿里巴巴-W', 'Apple']
441
+
442
+ # 成功插入后清除状态消息,并更新Radio组件的选项,同时默认选中刚添加的公司
443
+ # 通过设置value参数,会自动触发change事件来加载数据
444
+ return gr.update(visible=False), gr.update(choices=updated_list, value=company_name), gr.update(visible=False, value="")
445
+
446
+ else:
447
+ # 公司已存在,使用Gradio内置的警告消息
448
+ gr.Warning(f"公司 '{company_name}' 已存在")
449
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
450
+
451
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
452
+
453
+ # Sidebar report section functions
454
+ # 创建一个全局变量来存储公司按钮组件
455
+ company_buttons = {}
456
+
457
+ def create_company_buttons():
458
+ """创建公司按钮组件"""
459
+ # 确保companies_map已被初始化
460
+ if not companies_map:
461
+ initialize_companies_map()
462
+
463
+ # 显示companies_map中的公司列表
464
+ companies = list(companies_map.keys())
465
+
466
+ # 添加调试信息
467
+ print(f"Companies in map: {companies}")
468
+
469
+ # 清空之前的按钮
470
+ company_buttons.clear()
471
+
472
+ if not companies:
473
+ # 如果没有公司,返回一个空的列
474
+ with gr.Column():
475
+ gr.Markdown("暂无公司数据")
476
+ else:
477
+ # 使用Gradio按钮组件创建公司列表
478
+ with gr.Column(elem_classes=["home-company-list"]):
479
+ # 按每行两个公司进行分组
480
+ for i in range(0, len(companies), 2):
481
+ # 检查是否是最后一行且只有一个元素
482
+ if i + 1 < len(companies):
483
+ # 有两个元素
484
+ with gr.Row(elem_classes=["home-company-item-box"]):
485
+ btn1 = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"])
486
+ btn2 = gr.Button(companies[i + 1], elem_classes=["home-company-item", "gradio-button"])
487
+ # 保存按钮引用
488
+ company_buttons[companies[i]] = btn1
489
+ company_buttons[companies[i + 1]] = btn2
490
+ else:
491
+ # 只有一个元素
492
+ with gr.Row(elem_classes=["home-company-item-box", "single-item"]):
493
+ btn = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"])
494
+ # 保存按钮引用
495
+ company_buttons[companies[i]] = btn
496
+
497
+ # 返回按钮字典
498
+ return company_buttons
499
+ def update_report_section(selected_company, report_data, stock_code):
500
+ """根据选中的公司更新报告部分"""
501
+ print(f"Updating report (报告部��): {selected_company}") # 添加调试信息
502
+
503
+ if selected_company == "" or selected_company is None or selected_company == "Unknown":
504
+ # 没有选中的公司,显示公司列表
505
+ # html_content = get_initial_company_list_content()
506
+ # 暂时返回空内容,稍后会用Gradio组件替换
507
+ html_content = ""
508
+ return gr.update(value=html_content, visible=True)
509
+ else:
510
+ # 有选中的公司,显示相关报告
511
+ try:
512
+ # prmpt = f"""
513
+
514
+ # """
515
+ stock_code = get_stock_code_by_company_name(selected_company)
516
+ result = get_report_data(stock_code)
517
+ print(f"get_report_data=====================: {result}")
518
+ report_data = query_financial_data(stock_code, "5-Year")
519
+ # report_data = process_financial_data_with_metadata(financial_metrics_pre)
520
+
521
+ # 检查 report_data 是否是列表且第一个元素是字典
522
+ if not isinstance(report_data, list) or len(report_data) == 0:
523
+ return gr.update(value="<div>暂无报告数据</div>", visible=True)
524
+
525
+ # 检查第一个元素是否是字典
526
+ if not isinstance(report_data[0], dict):
527
+ return gr.update(value="<div>数据格式不正常</div>", visible=True)
528
+
529
+ html_content = '<div class="report-list-box bg-white">'
530
+ html_content += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>Financial Reports</h3></div>'
531
+ for report in report_data:
532
+ source_url = report.get('source_url', '#')
533
+ period = report.get('period', 'N/A')
534
+ source_form = report.get('source_form', 'N/A')
535
+ html_content += f'''
536
+ <div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{source_url}', '_blank')">
537
+ <div class="report-item-content">
538
+ <span class="text-gray-800">{period}-{stock_code}-{source_form}</span>
539
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor">
540
+ <path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 10-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l-1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd" />
541
+ </svg>
542
+ </div>
543
+ </div>
544
+ '''
545
+
546
+ html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>'
547
+ html_content += '</div>'
548
+
549
+ return gr.update(value=html_content, visible=True)
550
+ except Exception as e:
551
+ print(f"Error in update_report_section: {str(e)}")
552
+ return gr.update(value=f"<div>报告载入失败: {str(e)}</div>", visible=True)
553
+ def update_news_section(selected_company):
554
+ """根据选中的公司更新报告部分"""
555
+ html_content = ""
556
+ if selected_company == "" or selected_company is None:
557
+ # 没有选中的公司,显示公司列表
558
+ # html_content = get_initial_company_list_content()
559
+ # 暂时返回空内容,稍后会用Gradio组件替换
560
+ return gr.update(value=html_content, visible=True)
561
+ else:
562
+ try:
563
+ stock_code = get_stock_code_by_company_name(selected_company)
564
+ report_data = get_company_news(stock_code, None, None)
565
+ # print(f"新闻列表: {report_data['articles']}")
566
+ # report_data = search_news(selected_company)
567
+ if (report_data['articles']):
568
+ report_data = report_data['articles']
569
+ news_html = "<div class='news-list-box bg-white'>"
570
+ news_html += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>News</h3></div>'
571
+ from datetime import datetime
572
+
573
+ for news in report_data:
574
+ published_at = news['published']
575
+
576
+ # 解析 ISO 8601 时间字符串(注意:strptime 不直接支持 'Z',需替换或使用 fromisoformat)
577
+ dt = datetime.fromisoformat(published_at.replace("Z", "+00:00"))
578
+
579
+ # 格式化为 YYYY.MM.DD
580
+ formatted_date = dt.strftime("%Y.%m.%d")
581
+ news_html += f'''
582
+ <div class="news-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{news['url']}', '_blank')">
583
+ <div class="news-item-content">
584
+ <span class="text-xs text-gray-500">[{formatted_date}]</span>
585
+ <span class="text-gray-800">{news['headline']}</span>
586
+ </div>
587
+ </div>
588
+ '''
589
+ news_html += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}条新闻</span></div>'
590
+ news_html += '</div>'
591
+ html_content += news_html
592
+ except Exception as e:
593
+ print(f"Error updating report section: {str(e)}")
594
+
595
+ return gr.update(value=html_content, visible=True)
596
+
597
+ # Component creation functions
598
+ def create_header():
599
+ """创建头部组件"""
600
+ # 获取当前时间
601
+ current_time = datetime.datetime.now().strftime("%B %d, %Y - Market Data Updated Today")
602
+
603
+ with gr.Row(elem_classes=["header"]):
604
+ # 左侧:图标和标题
605
+ with gr.Column(scale=8):
606
+ # 使用圆柱体SVG图标表示数据库
607
+ gr.HTML('''
608
+ <div class="top-logo-box">
609
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 48 48">
610
+ <g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4">
611
+ <path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11"></path>
612
+ <path d="M44 29c0 3.314-8.954 6-20 6S4 32.314 4 29m40-9c0 3.314-8.954 6-20 6S4 23.314 4 20"></path>
613
+ <ellipse cx="24" cy="10" rx="20" ry="6"></ellipse>
614
+ </g>
615
+ </svg>
616
+ <span class="logo-title">Easy Financial Report Dashboard</span>
617
+ </div>
618
+ ''', elem_classes=["text-2xl"])
619
+
620
+ # 右侧:时间信息
621
+ with gr.Column(scale=2):
622
+ gr.Markdown(current_time, elem_classes=["text-sm-top-time"])
623
+
624
+ def create_company_list(get_companys_state):
625
+ """创建公司列表组件"""
626
+ try:
627
+ # 获取公司列表数据
628
+ # companies_data = get_companys()
629
+ companies_data = my_companies
630
+ print(f"创建公司列表组件 - Companies data: {companies_data}")
631
+ if isinstance(companies_data, list) and len(companies_data) > 0:
632
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
633
+ choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
634
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
635
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
636
+ else:
637
+ choices = []
638
+ except Exception as e:
639
+ print(f"Error creating company list: {str(e)}")
640
+ choices = []
641
+
642
+ # 添加默认公司选项
643
+ if not choices:
644
+ choices = []
645
+
646
+ # 使用Radio组件显示公司列表,不显示标签
647
+ company_list = gr.Radio(
648
+ choices=choices,
649
+ label="",
650
+ interactive=True,
651
+ elem_classes=["company-list-container"],
652
+ container=False, # 不显示外部容器边框
653
+ visible=True
654
+ )
655
+
656
+ return company_list
657
+
658
+ def create_company_selector():
659
+ """创建公司选择器组件"""
660
+ company_input = gr.Textbox(
661
+ show_label=False,
662
+ placeholder="Add Company",
663
+ elem_classes=["company-input-box"],
664
+ lines=1,
665
+ max_lines=1,
666
+ # container=False
667
+ )
668
+
669
+ # 状态消息显示区域
670
+ status_message = gr.HTML(
671
+ "",
672
+ elem_classes=["status-message"],
673
+ visible=False
674
+ )
675
+
676
+ # 弹窗选择列表
677
+ company_modal = gr.Radio(
678
+ show_label=False,
679
+ choices=[],
680
+ visible=False,
681
+ elem_classes=["company-modal"]
682
+ )
683
+
684
+ return company_input, status_message, company_modal
685
+
686
+ def create_report_section():
687
+ """创建报告部分组件"""
688
+ # 创建一个用于显示报告列表的组件,初始显示公司列表
689
+ # initial_content = get_initial_company_list_content()
690
+ # 暂时返回空内容,稍后会用Gradio组件替换
691
+ initial_content = ""
692
+ # print(f"Initial content: {initial_content}") # 添加调试信息
693
+
694
+ report_display = gr.HTML(initial_content)
695
+ return report_display
696
+
697
+ def create_news_section():
698
+ """创建新闻部分组件"""
699
+ initial_content = ""
700
+ news_display = gr.HTML(initial_content)
701
+ return news_display
702
+
703
+ def format_financial_metrics(data: dict, prev_data: dict = None) -> list: # pyright: ignore[reportArgumentType]
704
+ """
705
+ 将原始财务数据转换为 financial_metrics 格式。
706
+
707
+ Args:
708
+ data (dict): 当前财年数据(必须包含 total_revenue, net_income 等字段)
709
+ prev_data (dict, optional): 上一财年数据,用于计算 change。若未提供,change 设为 "--"
710
+
711
+ Returns:
712
+ list[dict]: 符合 financial_metrics 格式的列表
713
+ """
714
+
715
+ def format_currency(value: float) -> str:
716
+ """将数字格式化为 $XB / $XM / $XK"""
717
+ if value >= 1e9:
718
+ return f"${value / 1e9:.2f}B"
719
+ elif value >= 1e6:
720
+ return f"${value / 1e6:.2f}M"
721
+ elif value >= 1e3:
722
+ return f"${value / 1e3:.2f}K"
723
+ else:
724
+ return f"${value:.2f}"
725
+
726
+ def calculate_change(current: float, previous: float) -> tuple:
727
+ """计算变化百分比和颜色"""
728
+ if previous == 0:
729
+ return "--", "gray"
730
+ change_pct = (current - previous) / abs(previous) * 100
731
+ sign = "+" if change_pct >= 0 else ""
732
+ color = "green" if change_pct >= 0 else "red"
733
+ return f"{sign}{change_pct:.1f}%", color
734
+
735
+ # 定义指标映射
736
+ metrics_config = [
737
+ {
738
+ "key": "total_revenue",
739
+ "label": "Total Revenue",
740
+ "is_currency": True,
741
+ "eps_like": False
742
+ },
743
+ {
744
+ "key": "net_income",
745
+ "label": "Net Income",
746
+ "is_currency": True,
747
+ "eps_like": False
748
+ },
749
+ {
750
+ "key": "earnings_per_share",
751
+ "label": "Earnings Per Share",
752
+ "is_currency": False, # EPS 不用 B/M 单位
753
+ "eps_like": True
754
+ },
755
+ {
756
+ "key": "operating_expenses",
757
+ "label": "Operating Expenses",
758
+ "is_currency": True,
759
+ "eps_like": False
760
+ },
761
+ {
762
+ "key": "operating_cash_flow",
763
+ "label": "Cash Flow",
764
+ "is_currency": True,
765
+ "eps_like": False
766
+ }
767
+ ]
768
+
769
+ result = []
770
+ for item in metrics_config:
771
+ key = item["key"]
772
+ current_val = data.get(key)
773
+ if current_val is None:
774
+ continue
775
+
776
+ # 格式化 value
777
+ if item["eps_like"]:
778
+ value_str = f"${current_val:.2f}"
779
+ elif item["is_currency"]:
780
+ value_str = format_currency(current_val)
781
+ else:
782
+ value_str = str(current_val)
783
+
784
+ # 计算 change(如果有上期数据)
785
+ if prev_data and key in prev_data:
786
+ prev_val = prev_data[key]
787
+ change_str, color = calculate_change(current_val, prev_val)
788
+ else:
789
+ change_str = "--"
790
+ color = "gray"
791
+
792
+ result.append({
793
+ "label": item["label"],
794
+ "value": value_str,
795
+ "change": change_str,
796
+ "color": color
797
+ })
798
+
799
+ return result
800
+
801
+
802
+ def create_sidebar():
803
+ """创建侧边栏组件"""
804
+ # 初始化 companies_map
805
+ initialize_companies_map()
806
+
807
+ with gr.Column(elem_classes=["sidebar"]):
808
+ # 公司选择
809
+ with gr.Group(elem_classes=["card"]):
810
+ gr.Markdown("### Select Company", elem_classes=["card-title", "left-card-title"])
811
+ with gr.Column():
812
+ company_list = create_company_list(get_companys_state)
813
+
814
+ # 创建公司列表
815
+ # if not get_companys_state:
816
+ # getCompanyFromStorage = gr.Button("读取")
817
+ # getCompanyFromStorage.click(
818
+ # fn=create_company_list(True),
819
+ # inputs=[],
820
+ # outputs=[company_list, status_message]
821
+ # )
822
+
823
+ # 创建公司选择器
824
+ company_input, status_message, company_modal = create_company_selector()
825
+
826
+ # 绑定事件
827
+ company_input.submit(
828
+ fn=update_company_choices,
829
+ inputs=[company_input],
830
+ outputs=[company_modal, status_message]
831
+ )
832
+
833
+ company_modal.change(
834
+ fn=add_company,
835
+ inputs=[company_modal, company_list],
836
+ outputs=[company_modal, company_list, status_message]
837
+ )
838
+
839
+ # 创建公司按钮组件
840
+ company_buttons = create_company_buttons()
841
+
842
+ # 为每个公司按钮绑定点击事件
843
+ def make_click_handler(company_name):
844
+ def handler():
845
+ result = handle_company_click(company_name)
846
+ # 如果添加成功,刷新Select Company列表并默认选中刚添加的公司
847
+ if result is True:
848
+ # 正确地刷新通过create_company_list()创建的Radio组件
849
+ try:
850
+ # companies_data = get_companys()
851
+ companies_data = my_companies
852
+ if isinstance(companies_data, list) and len(companies_data) > 0:
853
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
854
+ updated_choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
855
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
856
+ updated_choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
857
+ else:
858
+ updated_choices = []
859
+ except:
860
+ updated_choices = []
861
+ # 使用gr.update来正确更新Radio组件,并默认选中刚添加的公司
862
+ # 同时触发change事件来加载数据
863
+ return gr.update(choices=updated_choices, value=company_name)
864
+ return None
865
+ return handler
866
+
867
+ for company_name, button in company_buttons.items():
868
+ button.click(
869
+ fn=make_click_handler(company_name),
870
+ inputs=[],
871
+ outputs=[company_list]
872
+ )
873
+
874
+ # 创建一个容器来容纳报告部分,初始时隐藏
875
+ with gr.Group(elem_classes=["report-news-box"]) as report_section_group:
876
+ # gr.Markdown("### Financial Reports", elem_classes=["card-title", "left-card-title"])
877
+ report_display = create_report_section()
878
+ news_display = create_news_section()
879
+
880
+
881
+ # 处理公司选择事件
882
+ def select_company_handler(company_name):
883
+ """处理公司选择事件的处理器"""
884
+ # 更新全局变量
885
+ g.SELECT_COMPANY = company_name if company_name else ""
886
+
887
+ # 更新报告部分的内容
888
+ updated_report_display = update_report_section(company_name, None, None)
889
+
890
+ updated_news_display = update_news_section(company_name)
891
+ # 根据是否选择了公司来决定显示/隐藏报告部分
892
+ if company_name:
893
+ # 有选中的公司,显示报告部分
894
+ return gr.update(visible=True), updated_report_display, updated_news_display
895
+ else:
896
+ # 没有选中的公司,隐藏报告部分
897
+ return gr.update(visible=False), updated_report_display, updated_news_display
898
+
899
+ company_list.change(
900
+ fn=select_company_handler,
901
+ inputs=[company_list],
902
+ outputs=[report_section_group, report_display, news_display]
903
+ )
904
+
905
+ # 返回公司列表组件和报告部分组件
906
+ return company_list, report_section_group, report_display, news_display
907
+
908
+ def build_income_table(table_data):
909
+ # 兼容两种数据结构:
910
+ # 1. 新结构:包含 list_data 和 yoy_rates 的字典
911
+ # 2. 旧结构:直接是二维数组
912
+ if isinstance(table_data, dict) and "list_data" in table_data:
913
+ # 新结构
914
+ income_statement = table_data["list_data"]
915
+ yoy_rates = table_data["yoy_rates"] or []
916
+ else:
917
+ # 旧结构,直接使用传入的数据
918
+ income_statement = table_data
919
+ yoy_rates = []
920
+
921
+ # 创建一个映射,将年份列索引映射到增长率
922
+ yoy_map = {}
923
+ if len(yoy_rates) > 1 and len(yoy_rates[0]) > 1:
924
+ # 获取增长率表头(跳过第一列"Category")
925
+ yoy_headers = yoy_rates[0][1:]
926
+
927
+ # 为每个指标行创建增长率映射
928
+ for i, yoy_row in enumerate(yoy_rates[1:], 1): # 跳过标题行
929
+ category = yoy_row[0]
930
+ yoy_map[category] = {}
931
+ for j, rate in enumerate(yoy_row[1:]):
932
+ if j < len(yoy_headers):
933
+ yoy_map[category][yoy_headers[j]] = rate
934
+
935
+ table_rows = ""
936
+ header_row = income_statement[0]
937
+
938
+ for i, row in enumerate(income_statement):
939
+ if i == 0:
940
+ row_style = "background-color: #f5f5f5; font-weight: 500;"
941
+ else:
942
+ row_style = "background-color: #f9f9f9;"
943
+ cells = ""
944
+
945
+ for j, cell in enumerate(row):
946
+ if j == 0:
947
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
948
+ else:
949
+ # 添加增长率箭头(如果有的话)
950
+ growth = None
951
+ category = row[0]
952
+ # j是当前单元格索引,0是类别列,1,2,3...是数据列
953
+ # yoy_map的键是年份,例如"2024/FY"
954
+ if i > 0 and category in yoy_map and j > 0 and j < len(header_row):
955
+ year_header = header_row[j]
956
+ if year_header in yoy_map[category]:
957
+ growth = yoy_map[category][year_header]
958
+
959
+ if growth and growth != "N/A":
960
+ arrow = "▲" if growth.startswith("+") else "▼"
961
+ color = "green" if growth.startswith("+") else "red"
962
+ cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px; position: relative;'>
963
+ <div>{cell}</div>
964
+ <div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div>
965
+ </td>"""
966
+ else:
967
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
968
+ table_rows += f"<tr style='{row_style}'>{cells}</tr>"
969
+
970
+ html = f"""
971
+ <div style="min-width: 400px;max-width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
972
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
973
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
974
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
975
+ </svg>
976
+ <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
977
+ </div>
978
+ <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
979
+ {table_rows}
980
+ </table>
981
+ </div>
982
+ """
983
+ return html
984
+ def create_metrics_dashboard():
985
+ """创建指标仪表板组件"""
986
+ with gr.Row(elem_classes=["metrics-dashboard"]):
987
+ card_custom_style = '''
988
+ background-color: white;
989
+ border-radius: 0.5rem;
990
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
991
+ padding: 1.25rem;
992
+ min-height: 250px !important;
993
+ text-align: center;
994
+ '''
995
+
996
+ # 模拟数据
997
+ company_info = {
998
+ "name": "N/A",
999
+ "symbol": "NYSE:N/A",
1000
+ "price": 0,
1001
+ "change": 0,
1002
+ "change_percent": 0.41,
1003
+ "open": 165.20,
1004
+ "high": 166.37,
1005
+ "low": 156.15,
1006
+ "prev_close": 157.01,
1007
+ "volume": "27.10M"
1008
+ }
1009
+
1010
+ # financial_metrics = query_financial_data("NVDA", "最新财务数据")
1011
+ # print(f"最新财务数据: {financial_metrics}")
1012
+ financial_metrics = [
1013
+ {"label": "Total Revenue", "value": "N/A", "change": "N/A", "color": "grey"},
1014
+ {"label": "Net Income", "value": "N/A", "change": "N/A", "color": "grey"},
1015
+ {"label": "Earnings Per Share", "value": "N/A", "change": "N/A", "color": "grey"},
1016
+ {"label": "Operating Expenses", "value": "N/A", "change": "N/A", "color": "grey"},
1017
+ {"label": "Cash Flow", "value": "N/A", "change": "N/A", "color": "grey"}
1018
+ ]
1019
+ # income_statement = [
1020
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1021
+ # ["Total", "130350M", "126491M", "134567M"],
1022
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1023
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1024
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1025
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1026
+ # ]
1027
+ income_statement = {
1028
+ "list_data": [
1029
+ ["Category", "N/A/FY", "N/A/FY", "N/A/FY"],
1030
+ ["Total", "N/A", "N/A", "N/A"],
1031
+ ["Net Income", "N/A", "N/A", "N/A.4M"],
1032
+ ["Earnings Per Share", "N/A", "N/A", "N/A"],
1033
+ ["Operating Expenses", "N/A", "N/A", "N/A"],
1034
+ ["Cash Flow", "N/A", "N/A", "N/A"]
1035
+ ],
1036
+ "yoy_rates": []
1037
+ # "yoy_rates": [
1038
+ # ["Category", "N/A/FY", "N/A/FY"],
1039
+ # ["Total", "N/A", "N/A"],
1040
+ # ["Net Income", "+3.05%", "-6.00%"],
1041
+ # ["Earnings Per Share", "+3.05%", "-6.00%"],
1042
+ # ["Operating Expenses", "+29.17%", "-6.00%"],
1043
+ # ["Cash Flow", "-13.05%", "-6.00%"]
1044
+ # ]
1045
+ }
1046
+ yearly_data = 'N/A'
1047
+ # 增长变化的 HTML 字符(箭头+百分比)
1048
+ def render_change(change: str, color: str):
1049
+ if change.startswith("+"):
1050
+ return f'<span style="color:{color};">▲{change}</span>'
1051
+ else:
1052
+ return f'<span style="color:{color};">▼{change}</span>'
1053
+
1054
+ # 构建左侧卡片
1055
+ def build_stock_card():
1056
+ html = f"""
1057
+ <div style="width: 250px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1058
+ <div style="font-size: 14px; color: #555;">N/A</div>
1059
+ <div style="font-size: 12px; color: #888;">N/A</div>
1060
+ <div style="font-size: 32px; font-weight: bold; margin: 8px 0;">N/A</div>
1061
+ <div style="font-size: 14px; margin: 8px 0;">N/A</div>
1062
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1063
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1064
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1065
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1066
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1067
+
1068
+ </div>
1069
+ </div>
1070
+ """
1071
+ return html
1072
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1073
+ # 构建中间卡片
1074
+ def build_financial_metrics():
1075
+ metrics_html = ""
1076
+ for item in financial_metrics:
1077
+ change_html = render_change(item["change"], item["color"])
1078
+ metrics_html += f"""
1079
+ <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
1080
+ <div style="font-size: 14px; color: #555;">{item['label']}</div>
1081
+ <div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div>
1082
+ </div>
1083
+ """
1084
+
1085
+ html = f"""
1086
+ <div style="min-width: 300px;max-width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1087
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;">
1088
+ <div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;">
1089
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1090
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1091
+ </svg>
1092
+ <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1093
+ </div>
1094
+ <div style="font-size: 16px; color: #8f8f8f;">
1095
+ YTD data
1096
+ </div>
1097
+ </div>
1098
+ {metrics_html}
1099
+ </div>
1100
+ """
1101
+ return html
1102
+
1103
+
1104
+ # 主函数:返回所有 HTML 片段
1105
+ def get_dashboard():
1106
+ with gr.Row():
1107
+ with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]):
1108
+ stock_card_html = gr.HTML(build_stock_card(), elem_classes=["metric-card-left"])
1109
+ with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]):
1110
+ financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"])
1111
+ with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]):
1112
+ # 传递income_statement参数
1113
+ income_table_html = gr.HTML(build_income_table(income_statement), elem_classes=["metric-card-right"])
1114
+ return stock_card_html, financial_metrics_html, income_table_html
1115
+
1116
+ # 创建指标仪表板并保存引用
1117
+ stock_card_component, financial_metrics_component, income_table_component = get_dashboard()
1118
+
1119
+ # 将组件引用保存到全局变量,以便在其他地方使用
1120
+ global metrics_dashboard_components
1121
+ metrics_dashboard_components = (stock_card_component, financial_metrics_component, income_table_component)
1122
+
1123
+ # 更新指标仪表板的函数
1124
+ def update_metrics_dashboard(company_name):
1125
+ """根据选择的公司更新指标仪表板"""
1126
+ # 模拟数据
1127
+ # company_info = {
1128
+ # "name": company_name,
1129
+ # "symbol": "NYSE:BABA",
1130
+ # "price": 157.65,
1131
+ # "change": 0.64,
1132
+ # "change_percent": 0.41,
1133
+ # "open": 165.20,
1134
+ # "high": 166.37,
1135
+ # "low": 156.15,
1136
+ # "prev_close": 157.01,
1137
+ # "volume": "27.10M"
1138
+ # }
1139
+ company_info = {}
1140
+ # 尝试获取股票价格数据,但不中断程序执行
1141
+ stock_code = ""
1142
+ try:
1143
+ # 根据选择的公司获取股票代码
1144
+ stock_code = get_stock_code_by_company_name(company_name)
1145
+ # result = get_quote(company_name.strip())
1146
+
1147
+ # company_info2 = get_stock_price(stock_code)
1148
+ # company_info2 = get_stock_price_from_bailian(stock_code)
1149
+ # print(f"股票价格数据: {company_info2}")
1150
+ company_info = get_quote(stock_code.strip())
1151
+ company_info['company'] = company_name
1152
+ print(f"股票价格数据====: {company_info}")
1153
+ # 查询结果:{
1154
+ # "company": "阿里巴巴",
1155
+ # "symbol": "BABA",
1156
+ # "open": "159.09",
1157
+ # "high": "161.46",
1158
+ # "low": "150.00",
1159
+ # "price": "157.60",
1160
+ # "volume": "21453064",
1161
+ # "latest trading day": "2025-11-27",
1162
+ # "previous close": "157.01",
1163
+ # "change": "+0.59",
1164
+ # "change_percent": "+0.38%"
1165
+ # }BABA
1166
+ # 如果成功获取数据,则用实际数据替换模拟数据
1167
+ # if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
1168
+ # import json
1169
+ # # 解析返回的JSON数据
1170
+ # data_text = company_info2["content"][0]["text"]
1171
+ # stock_data = json.loads(data_text)
1172
+
1173
+ # # 提取数据
1174
+ # quote = stock_data["Global Quote"]
1175
+
1176
+ # # 转换交易量单位
1177
+ # volume = int(quote['06. volume'])
1178
+ # if volume >= 1000000:
1179
+ # volume_str = f"{volume / 1000000:.2f}M"
1180
+ # elif volume >= 1000:
1181
+ # volume_str = f"{volume / 1000:.2f}K"
1182
+ # else:
1183
+ # volume_str = str(volume)
1184
+
1185
+ # company_info = {
1186
+ # "name": company_name,
1187
+ # "symbol": f"NYSE:{quote['01. symbol']}",
1188
+ # "price": float(quote['05. price']),
1189
+ # "change": float(quote['09. change']),
1190
+ # "change_percent": float(quote['10. change percent'].rstrip('%')),
1191
+ # "open": float(quote['02. open']),
1192
+ # "high": float(quote['03. high']),
1193
+ # "low": float(quote['04. low']),
1194
+ # "prev_close": float(quote['08. previous close']),
1195
+ # "volume": volume_str
1196
+ # }
1197
+ except Exception as e:
1198
+ print(f"获取股票价格数据失败: {e}")
1199
+ company_info2 = None
1200
+
1201
+ # financial_metrics = [
1202
+ # {"label": "Total Revenue", "value": "$2.84B", "change": "+12.4%", "color": "green"},
1203
+ # {"label": "Net Income", "value": "$685M", "change": "-3.2%", "color": "red"},
1204
+ # {"label": "Earnings Per Share", "value": "$2.15", "change": "-3.2%", "color": "red"},
1205
+ # {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
1206
+ # {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
1207
+ # ]
1208
+ financial_metrics_pre = query_financial_data(stock_code, "5-Year")
1209
+ # financial_metrics_pre = query_financial_data(company_name, "5年趋势")
1210
+ # print(f"最新财务数据: {financial_metrics_pre}")
1211
+ # financial_metrics = format_financial_metrics(financial_metrics_pre)
1212
+
1213
+
1214
+ # financial_metrics_pre_2 = extract_last_three_with_fallback(financial_metrics_pre)
1215
+ # print(f"提取的3年数据: {financial_metrics_pre_2}")
1216
+ # financial_metrics_pre = {
1217
+ # "metrics": financial_metrics_pre_2
1218
+ # }
1219
+ financial_metrics = []
1220
+ # try:
1221
+ # # financial_metrics = calculate_yoy_comparison(financial_metrics_pre)
1222
+ # financial_metrics = build_financial_metrics_three_year_data(financial_metrics_pre)
1223
+ # print(f"格式化后的财务数据: {financial_metrics}")
1224
+ # except Exception as e:
1225
+ # print(f"Error calculating YOY comparison: {e}")
1226
+ year_data = None
1227
+ three_year_data = None
1228
+ try:
1229
+ # financial_metrics = process_financial_data_with_metadata(financial_metrics_pre)
1230
+ result = process_financial_data_with_metadata(financial_metrics_pre)
1231
+
1232
+ # 按需提取字段
1233
+ financial_metrics = result["financial_metrics"]
1234
+ year_data = result["year_data"]
1235
+ three_year_data = result["three_year_data"]
1236
+ print(f"格式化后的财务数据: {financial_metrics}")
1237
+ # 拿report数据
1238
+ # try:
1239
+ # # 从 result 中获取报告数据
1240
+ # if 'report_data' in result: # 假设 result 中包含 report_data 键
1241
+ # report_data = result['report_data']
1242
+ # else:
1243
+ # # 如果 result 中没有直接包含 report_data,则从其他键中获取
1244
+ # # 这需要根据实际的 result 数据结构来调整
1245
+ # report_data = result.get('reports', []) # 示例:假设数据在 'reports' 键下
1246
+
1247
+ # 更新报告部分的内容
1248
+ # 这里需要调用 update_report_section 函数并传入 report_data
1249
+ # 注意:update_report_section 可能需要修改以接受 report_data 参��
1250
+ # updated_report_content = update_report_section(company_name, report_data, stock_code)
1251
+
1252
+ # 然后将 updated_report_content 返回,以便在 UI 中更新
1253
+ # 这需要修改函数的返回值以包含报告内容
1254
+
1255
+ # except Exception as e:
1256
+ # print(f"Error updating report section with result data: {e}")
1257
+ # updated_report_content = "<div>Failed to load report data</div>"
1258
+ except Exception as e:
1259
+ print(f"Error process_financial_data: {e}")
1260
+
1261
+
1262
+ # income_statement = [
1263
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1264
+ # ["Total", "130350M", "126491M", "134567M"],
1265
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1266
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1267
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1268
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1269
+ # ]
1270
+
1271
+ # table_data = None
1272
+ # try:
1273
+ # table_data = extract_financial_table(financial_metrics_pre)
1274
+ # print(table_data)
1275
+ # except Exception as e:
1276
+ # print(f"Error extract_financial_table: {e}")
1277
+ # yearly_data = None
1278
+ # try:
1279
+ # yearly_data = get_yearly_data(financial_metrics_pre)
1280
+ # except Exception as e:
1281
+ # print(f"Error get_yearly_data: {e}")
1282
+
1283
+ # ======
1284
+ # table_data = [
1285
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1286
+ # ["Total", "130350M", "126491M", "134567M"],
1287
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1288
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1289
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1290
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1291
+ # ]
1292
+ yearly_data = year_data
1293
+ table_data = build_table_format(three_year_data)
1294
+ # print(f"table_data: {table_data}")
1295
+ # yearly_data = None
1296
+ # try:
1297
+ # yearly_data = get_yearly_data(financial_metrics_pre)
1298
+ # except Exception as e:
1299
+ # print(f"Error get_yearly_data: {e}")
1300
+ #=======
1301
+
1302
+ # exp = {
1303
+ # "list_data": [
1304
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1305
+ # ["Total", "130350M", "126491M", "134567M"],
1306
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1307
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1308
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1309
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1310
+ # ],
1311
+ # "yoy_rates": [
1312
+ # ["Category", "2024/FY", "2023/FY"],
1313
+ # ["Total", "+3.05%", "-6.00%"],
1314
+ # ["Net Income", "+3.05%", "-6.00%"],
1315
+ # ["Earnings Per Share", "+3.05%", "-6.00%"],
1316
+ # ["Operating Expenses", "+29.17%", "-6.00%"],
1317
+ # ["Cash Flow", "-13.05%", "-6.00%"]
1318
+ # ]
1319
+ # }
1320
+
1321
+ # 增长变化的 HTML 字符(箭头+百分比)
1322
+ def render_change(change: str, color: str):
1323
+ if change.startswith("+"):
1324
+ return f'<span style="color:{color};">▲{change}</span>'
1325
+ else:
1326
+ return f'<span style="color:{color};">▼{change}</span>'
1327
+
1328
+ # 构建左侧卡片
1329
+ def build_stock_card(company_info):
1330
+ try:
1331
+ if not company_info or not isinstance(company_info, dict):
1332
+ company_name = "N/A"
1333
+ symbol = "N/A"
1334
+ price = "N/A"
1335
+ change_html = '<span style="color:#888;">N/A</span>'
1336
+ open_val = high_val = low_val = prev_close_val = volume_display = "N/A"
1337
+ else:
1338
+ company_name = company_info.get("company", "N/A")
1339
+ symbol = company_info.get("symbol", "N/A")
1340
+ price = company_info.get("current_price", "N/A")
1341
+
1342
+ # 解析 change
1343
+ change_str = company_info.get("change", "0")
1344
+ try:
1345
+ change = float(change_str)
1346
+ except (ValueError, TypeError):
1347
+ change = 0.0
1348
+
1349
+ # 解析 change_percent
1350
+ change_percent = company_info.get("percent_change", "0%")
1351
+ # try:
1352
+ # change_percent = float(change_percent_str.rstrip('%'))
1353
+ # except (ValueError, TypeError):
1354
+ # change_percent = 0.0
1355
+
1356
+ change_color = "green" if change >= 0 else "red"
1357
+ sign = "+" if change >= 0 else ""
1358
+ change_html = f'<span style="color:{change_color};">{sign}{change:.2f} ({change_percent:+.2f}%)</span>'
1359
+
1360
+ # 其他价格字段(可选:也可格式化为 2 位小数)
1361
+ open_val = company_info.get("open", "N/A")
1362
+ high_val = company_info.get("high", "N/A")
1363
+ low_val = company_info.get("low", "N/A")
1364
+ prev_close_val = company_info.get("previous_close", "N/A")
1365
+ # raw_volume = company_info.get("volume", "N/A")
1366
+ # volume_display = format_volume(raw_volume)
1367
+
1368
+ html = f"""
1369
+ <div style="width: 250px; height: 300px !important; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1370
+ <div style="font-size: 16px; color: #555; font-weight: 500;">{company_name}</div>
1371
+ <div style="font-size: 12px; color: #888;">NYSE:{symbol}</div>
1372
+ <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1373
+ <div style="font-size: 32px; font-weight: bold;">{price}</div>
1374
+ <div style="font-size: 14px;">{change_html}</div>
1375
+ </div>
1376
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1377
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{open_val}</div>
1378
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{high_val}</div>
1379
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{low_val}</div>
1380
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{prev_close_val}</div>
1381
+ </div>
1382
+ </div>
1383
+ """
1384
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{volume_display}</div>
1385
+
1386
+ return html
1387
+
1388
+ except Exception as e:
1389
+ print(f"Error building stock card: {e}")
1390
+ return '<div style="width:250px; padding:16px; color:red;">Error loading stock data</div>'
1391
+ # 构建中间卡片
1392
+ def build_financial_metrics(yearly_data):
1393
+ metrics_html = ""
1394
+ for item in financial_metrics:
1395
+ change_html = render_change(item["change"], item["color"])
1396
+ metrics_html += f"""
1397
+ <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
1398
+ <div style="font-size: 14px; color: #555;">{item['label']}</div>
1399
+ <div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div>
1400
+ </div>
1401
+ """
1402
+
1403
+ html = f"""
1404
+ <div style="width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1405
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;">
1406
+ <div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;">
1407
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1408
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1409
+ </svg>
1410
+ <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1411
+ </div>
1412
+ <div style="font-size: 16px; color: #8f8f8f;">
1413
+ YTD data
1414
+ </div>
1415
+ </div>
1416
+ {metrics_html}
1417
+ </div>
1418
+ """
1419
+ return html
1420
+
1421
+ # 构建右侧表格
1422
+ # def build_income_table(income_statement):
1423
+ # table_rows = ""
1424
+ # for i, row in enumerate(income_statement):
1425
+ # if i == 0:
1426
+ # row_style = "background-color: #f5f5f5; font-weight: 500;"
1427
+ # else:
1428
+ # row_style = "background-color: #f9f9f9;"
1429
+ # cells = ""
1430
+ # for j, cell in enumerate(row):
1431
+ # if j == 0:
1432
+ # cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
1433
+ # else:
1434
+ # # 添加增长箭头(模拟数据)
1435
+ # growth = None
1436
+ # if i == 1 and j == 1: growth = "+3.05%"
1437
+ # elif i == 1 and j == 2: growth = "-6.00%"
1438
+ # elif i == 2 and j == 1: growth = "+3.05%"
1439
+ # elif i == 2 and j == 2: growth = "-6.00%"
1440
+ # elif i == 3 and j == 1: growth = "+3.05%"
1441
+ # elif i == 3 and j == 2: growth = "-6.00%"
1442
+ # elif i == 4 and j == 1: growth = "+29.17%"
1443
+ # elif i == 4 and j == 2: growth = "+29.17%"
1444
+ # elif i == 5 and j == 1: growth = "-13.05%"
1445
+ # elif i == 5 and j == 2: growth = "+29.17%"
1446
+
1447
+ # if growth:
1448
+ # arrow = "▲" if growth.startswith("+") else "▼"
1449
+ # color = "green" if growth.startswith("+") else "red"
1450
+ # cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px; position: relative;'>
1451
+ # <div>{cell}</div>
1452
+ # <div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div>
1453
+ # </td>"""
1454
+ # else:
1455
+ # cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
1456
+ # table_rows += f"<tr style='{row_style}'>{cells}</tr>"
1457
+
1458
+ # html = f"""
1459
+ # <div style="width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1460
+ # <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1461
+ # <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1462
+ # <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1463
+ # </svg>
1464
+ # <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
1465
+ # </div>
1466
+ # <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
1467
+ # {table_rows}
1468
+ # </table>
1469
+ # </div>
1470
+ # """
1471
+ # return html
1472
+
1473
+ # 返回三个HTML组件的内容
1474
+ return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data)
1475
+
1476
+ def create_tab_content(tab_name, company_name):
1477
+ """创建Tab内容组件"""
1478
+ if tab_name == "summary":
1479
+ print(f"company_name: {company_name}")
1480
+ # content = get_invest_suggest(company_name)
1481
+ gr.Markdown("# 11111", elem_classes=["invest-suggest-md-box"])
1482
+ # gr.Markdown(content, elem_classes=["invest-suggest-md-box"])
1483
+ # gr.Markdown("""
1484
+ # ## Investment Suggestions
1485
+
1486
+ # ### Company Overview
1487
+
1488
+ # GlobalTech inc. is a leading technology company with strong performance in the Q3 2025 period. The companyshows consistent revenue growth and maintains a healthy fnancial position.
1489
+
1490
+ # ### Key Strengths
1491
+
1492
+ # - Revenue Growth: 12.4% year-over-year increase demonstrates strong market demandDiversifed Portfolio: Multiple revenue streams reduce business risk
1493
+
1494
+ # - Innovation Focus: Continued investment in R&D drives future growth potential
1495
+
1496
+ # ### Financial Health Indicators
1497
+
1498
+ # - Liquidity: Current ratio of 1.82 indicates good short-term fnancial health
1499
+
1500
+ # - Proftability: Net income of $685M, though down slightly quarter-over-quarter0
1501
+ # - Cash Flow: Strong operating cash flow of $982M supports operations and growth initiatives
1502
+
1503
+ # ### Investment Recommendation
1504
+
1505
+ # BUY - GlobalTech Inc. presents a solid investment opportunity with:
1506
+ # - Consistent revenue growth trajectory
1507
+ # - Strong market position in key technology segments
1508
+ # - Healthy balance sheet and cash flow generation
1509
+
1510
+ # ### Risk Considerations
1511
+
1512
+ # Quarterly net income decline warrants monitoring
1513
+ # | Category | Q3 2025 | Q2 2025 | YoY % |
1514
+ # |--------------------|-----------|-----------|----------|
1515
+ # | Total Revenue | $2,842M | $2,712M | +12.4% |
1516
+ # | Gross Profit | $1,203M | $1,124M | +7.0% |
1517
+ # | Operating Income | $742M | $798M | -7.0% |
1518
+ # | Net Income | $685M | $708M | -3.2% |
1519
+ # | Earnings Per Share | $2.15 | $2.22 | -3.2% |
1520
+ # """, elem_classes=["invest-suggest-md-box"])
1521
+
1522
+
1523
+ elif tab_name == "detailed":
1524
+ with gr.Column(elem_classes=["tab-content"]):
1525
+ gr.Markdown("Financial Statements", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"])
1526
+
1527
+ with gr.Row(elem_classes=["gap-6"]):
1528
+ # 收入报表 (3/5宽度)
1529
+ with gr.Column(elem_classes=["w-3/5", "bg-gray-50", "rounded-xl", "p-4"]):
1530
+ gr.Markdown("Income Statement", elem_classes=["font-medium", "mb-3"])
1531
+ # 这里将显示收入报表表格
1532
+
1533
+ # 资产负债表和现金流量表 (2/5宽度)
1534
+ with gr.Column(elem_classes=["w-2/5", "flex", "flex-col", "gap-6"]):
1535
+ # 资产负债表
1536
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1537
+ gr.Markdown("Balance Sheet Summary", elem_classes=["font-medium", "mb-3"])
1538
+ # 这里��显示资产负债表图表
1539
+
1540
+ # 现金流量表
1541
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1542
+ with gr.Row(elem_classes=["justify-between", "items-start"]):
1543
+ gr.Markdown("Cash Flow Statement", elem_classes=["font-medium"])
1544
+ gr.Markdown("View Detailed", elem_classes=["text-xs", "text-blue-600", "font-medium"])
1545
+
1546
+ with gr.Column(elem_classes=["mt-4", "space-y-3"]):
1547
+ # 经营现金流
1548
+ with gr.Column():
1549
+ with gr.Row(elem_classes=["justify-between"]):
1550
+ gr.Markdown("Operating Cash Flow")
1551
+ gr.Markdown("$982M", elem_classes=["font-medium"])
1552
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1553
+ with gr.Column(elem_classes=["bg-green-500", "h-1.5", "rounded-full"], scale=85):
1554
+ gr.Markdown("")
1555
+
1556
+ # 投资现金流
1557
+ with gr.Column():
1558
+ with gr.Row(elem_classes=["justify-between"]):
1559
+ gr.Markdown("Investing Cash Flow")
1560
+ gr.Markdown("-$415M", elem_classes=["font-medium"])
1561
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1562
+ with gr.Column(elem_classes=["bg-blue-500", "h-1.5", "rounded-full"], scale=42):
1563
+ gr.Markdown("")
1564
+
1565
+ # 融资现金流
1566
+ with gr.Column():
1567
+ with gr.Row(elem_classes=["justify-between"]):
1568
+ gr.Markdown("Financing Cash Flow")
1569
+ gr.Markdown("-$212M", elem_classes=["font-medium"])
1570
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1571
+ with gr.Column(elem_classes=["bg-red-500", "h-1.5", "rounded-full"], scale=25):
1572
+ gr.Markdown("")
1573
+
1574
+ elif tab_name == "comparative":
1575
+ with gr.Column(elem_classes=["tab-content"]):
1576
+ gr.Markdown("Industry Benchmarking", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"])
1577
+
1578
+ # 收入增长对比
1579
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4", "mb-6"]):
1580
+ gr.Markdown("Revenue Growth - Peer Comparison", elem_classes=["font-medium", "mb-3"])
1581
+ # 这里将显示对比图表
1582
+
1583
+ # 利润率和报告预览网格
1584
+ with gr.Row(elem_classes=["grid-cols-2", "gap-6"]):
1585
+ # 利润率表格
1586
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1587
+ gr.Markdown("Profitability Ratios", elem_classes=["font-medium", "mb-3"])
1588
+ # 这里将显示利润率表格
1589
+
1590
+ # 报告预览
1591
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1592
+ gr.Markdown("Report Preview", elem_classes=["font-medium", "mb-3"])
1593
+ # 这里将显示报告预览
1594
+
1595
+ def create_chat_panel():
1596
+ """创建聊天面板组件"""
1597
+ # with gr.Column(elem_classes=["chat-panel"]):
1598
+ # 聊天头部
1599
+ # with gr.Row(elem_classes=["p-4", "border-b", "border-gray-200", "items-center", "gap-2"]):
1600
+ # gr.Markdown("🤖", elem_classes=["text-xl", "text-blue-600"])
1601
+ # gr.Markdown("Financial Assistant", elem_classes=["font-medium"])
1602
+
1603
+ # 聊天区域
1604
+ # 一行代码嵌入!
1605
+ # chat_component = create_financial_chatbot()
1606
+ # chat_component.render()
1607
+ # create_financial_chatbot()
1608
+ # gr.LoginButton()
1609
+ # chatbot = gr.Chatbot(
1610
+ # value=[
1611
+ # {"role": "assistant", "content": "I'm your financial assistant, how can I help you today?"},
1612
+
1613
+ # # {"role": "assistant", "content": "Hello! I can help you analyze financial data. Ask questions like \"Show revenue trends\" or \"Compare profitability ratios\""},
1614
+ # # {"role": "user", "content": "Show revenue trends for last 4 quarters"},
1615
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1616
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1617
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1618
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"}
1619
+ # ],
1620
+ # type="messages",
1621
+ # # elem_classes=["min-h-0", "overflow-y-auto", "space-y-4", "chat-content-box"],
1622
+ # show_label=False,
1623
+ # autoscroll=True,
1624
+ # show_copy_button=True,
1625
+ # height=400,
1626
+ # container=False,
1627
+ # )
1628
+
1629
+ # # 输入区域
1630
+ # with gr.Row(elem_classes=["border-t", "border-gray-200", "gap-2"]):
1631
+ # msg = gr.Textbox(
1632
+ # placeholder="Ask a financial question...",
1633
+ # elem_classes=["flex-1", "border", "border-gray-300", "rounded-lg", "px-4", "py-2", "focus:border-blue-500"],
1634
+ # show_label=False,
1635
+ # lines=1,
1636
+ # submit_btn=True,
1637
+ # container=False,
1638
+ # )
1639
+ # msg.submit(
1640
+ # chat_bot,
1641
+ # [msg, chatbot],
1642
+ # [msg, chatbot],
1643
+ # queue=True,
1644
+ # )
1645
+
1646
+ # def load_css_files(css_dir, filenames):
1647
+ # css_content = ""
1648
+ # for filename in filenames:
1649
+ # path = os.path.join(css_dir, filename)
1650
+ # if os.path.exists(path):
1651
+ # with open(path, "r", encoding="utf-8") as f:
1652
+ # css_content += f.read() + "\n"
1653
+ # else:
1654
+ # print(f"⚠️ CSS file not found: {path}")
1655
+ # return css_content
1656
+ def main():
1657
+ # 获取当前目录
1658
+ current_dir = os.path.dirname(os.path.abspath(__file__))
1659
+ css_dir = os.path.join(current_dir, "css")
1660
+
1661
+ # def load_css_files(css_dir, filenames):
1662
+ # """读取多个 CSS 文件并合并为一个字符串"""
1663
+ # css_content = ""
1664
+ # for filename in filenames:
1665
+ # path = os.path.join(css_dir, filename)
1666
+ # if os.path.exists(path):
1667
+ # with open(path, "r", encoding="utf-8") as f:
1668
+ # css_content += f.read() + "\n"
1669
+ # else:
1670
+ # print(f"Warning: CSS file not found: {path}")
1671
+ # return css_content
1672
+ # 设置CSS路径
1673
+ css_paths = [
1674
+ os.path.join(css_dir, "main.css"),
1675
+ os.path.join(css_dir, "components.css"),
1676
+ os.path.join(css_dir, "layout.css")
1677
+ ]
1678
+ # css_dir = "path/to/your/css/folder" # 替换为你的实际路径
1679
+ # 自动定位 css 文件夹(与 app.py 同级)
1680
+ # BASE_DIR = os.path.dirname(os.path.abspath(__file__))
1681
+ # CSS_DIR = os.path.join(BASE_DIR, "css")
1682
+
1683
+ # css_files = ["main.css", "components.css", "layout.css"]
1684
+ # combined_css = load_css_files(CSS_DIR, css_files)
1685
+ # print(combined_css)
1686
+
1687
+ with gr.Blocks(
1688
+ title="Financial Analysis Dashboard",
1689
+ css_paths=css_paths,
1690
+ css=custom_css,
1691
+ # css=combined_css
1692
+ ) as demo:
1693
+
1694
+ # 添加处理公司点击事件的路由
1695
+ # 创建一个状态组件来跟踪选中的公司
1696
+ selected_company_state = gr.State("")
1697
+
1698
+ with gr.Column(elem_classes=["container", "container-h"]):
1699
+ # 头部
1700
+ create_header()
1701
+
1702
+ # 创建主布局
1703
+ with gr.Row(elem_classes=["main-content-box"]):
1704
+ # 左侧边栏
1705
+ with gr.Column(scale=1, min_width=350):
1706
+ # 获取company_list组件的引用
1707
+ company_list_component, report_section_component, report_display_component, news_display_component = create_sidebar()
1708
+
1709
+ # 主内容区域
1710
+ with gr.Column(scale=9):
1711
+
1712
+ # 指标仪表板
1713
+ create_metrics_dashboard()
1714
+
1715
+ with gr.Row(elem_classes=["main-content-box"]):
1716
+ with gr.Column(scale=8):
1717
+ # Tab内容
1718
+ with gr.Tabs():
1719
+ with gr.TabItem("Investment Suggestion", elem_classes=["tab-item"]):
1720
+ # 创建一个用于显示公司名称的组件
1721
+ # company_display = gr.Markdown("# Please select a company")
1722
+ # 创建一个占位符用于显示tab内容
1723
+ tab_content = gr.Markdown(elem_classes=["invest-suggest-md-box"])
1724
+
1725
+ # 当选中的公司改变时,更新显示
1726
+ # selected_company_state.change(
1727
+ # fn=lambda company: f"# Investment Suggestions for {company}" if company else "# Please select a company",
1728
+ # inputs=[selected_company_state],
1729
+ # outputs=[company_display]
1730
+ # )
1731
+
1732
+ # 当选中的公司改变时,重新加载tab内容
1733
+ def update_tab_content(company):
1734
+ if company:
1735
+ # 显示loading状态
1736
+ loading_html = f'''
1737
+ <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
1738
+ <div style="text-align: center;">
1739
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1740
+ <p style="margin-top: 20px; color: #666;">Loading investment suggestions for {company}...</p>
1741
+ <style>
1742
+ @keyframes spin {{
1743
+ 0% {{ transform: rotate(0deg); }}
1744
+ 100% {{ transform: rotate(360deg); }}
1745
+ }}
1746
+ </style>
1747
+ </div>
1748
+ </div>
1749
+ '''
1750
+ yield loading_html
1751
+
1752
+ # 获取投资建议数据
1753
+ try:
1754
+ # content = get_invest_suggest(company)
1755
+ stock_code = get_stock_code_by_company_name(company)
1756
+ yield query_company_advanced(stock_code, "suggestion")
1757
+ # yield content
1758
+ except Exception as e:
1759
+ error_html = f'''
1760
+ <div style="padding: 20px; text-align: center; color: #666;">
1761
+ <p>Error loading investment suggestions: {str(e)}</p>
1762
+ <p>Please try again later.</p>
1763
+ </div>
1764
+ '''
1765
+ yield error_html
1766
+ else:
1767
+ yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1768
+
1769
+ selected_company_state.change(
1770
+ fn=update_tab_content,
1771
+ inputs=[selected_company_state],
1772
+ outputs=[tab_content],
1773
+ )
1774
+ with gr.TabItem("Analysis Report", elem_classes=["tab-item"]):
1775
+ # 创建一个用于显示公司名称的组件
1776
+ # analysis_company_display = gr.Markdown("# Please select a company")
1777
+ # 创建一个占位符用于显示tab内容
1778
+ analysis_tab_content = gr.Markdown(elem_classes=["analysis-report-md-box"])
1779
+
1780
+ # 当选中的公司改变时,更新显示
1781
+ # selected_company_state.change(
1782
+ # fn=lambda company: f"# Analysis Report for {company}" if company else "# Please select a company",
1783
+ # inputs=[selected_company_state],
1784
+ # outputs=[analysis_company_display]
1785
+ # )
1786
+
1787
+ # 当选中的公司改变时,重新加载tab内容
1788
+ def update_analysis_tab_content(company):
1789
+ if company:
1790
+ # 显示loading状态
1791
+ loading_html = f'''
1792
+ <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
1793
+ <div style="text-align: center;">
1794
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1795
+ <p style="margin-top: 20px; color: #666;">Loading analysis report for {company}...</p>
1796
+ <style>
1797
+ @keyframes spin {{
1798
+ 0% {{ transform: rotate(0deg); }}
1799
+ 100% {{ transform: rotate(360deg); }}
1800
+ }}
1801
+ </style>
1802
+ </div>
1803
+ </div>
1804
+ '''
1805
+ yield loading_html
1806
+
1807
+ # 获取分析报告数据
1808
+ try:
1809
+ # 这里应该调用获取详细分析报告的函数
1810
+ # 暂时使用占位内容,您需要替换为实际的函数调用
1811
+ # content = f"# Analysis Report for {company}\n\nDetailed financial analysis for {company} will be displayed here."
1812
+ stock_code = get_stock_code_by_company_name(company)
1813
+ # result = query_company_advanced(stock_code)
1814
+ # print(f"Result=====================: {result}")
1815
+ # yield get_analysis_report(company)
1816
+ yield query_company_advanced(stock_code, "report")
1817
+ except Exception as e:
1818
+ error_html = f'''
1819
+ <div style="padding: 20px; text-align: center; color: #666;">
1820
+ <p>Error loading analysis report: {str(e)}</p>
1821
+ <p>Please try again later.</p>
1822
+ </div>
1823
+ '''
1824
+ yield error_html
1825
+ else:
1826
+ yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1827
+
1828
+ selected_company_state.change(
1829
+ fn=update_analysis_tab_content,
1830
+ inputs=[selected_company_state],
1831
+ outputs=[analysis_tab_content]
1832
+ )
1833
+ # with gr.TabItem("Comparison", elem_classes=["tab-item"]):
1834
+ # create_tab_content("comparison")
1835
+ with gr.Column(scale=2, min_width=400):
1836
+ # 聊天面板
1837
+
1838
+ # chatbot.render()
1839
+ # gr.LoginButton()
1840
+ gr.ChatInterface(
1841
+ respond,
1842
+ title="Easy Financial Report",
1843
+ additional_inputs=[
1844
+ gr.State(value=""), # CRITICAL: Store session URL across turns (hidden from UI)
1845
+ gr.State(value={}) # CRITICAL: Store agent context across turns (hidden from UI)
1846
+ ],
1847
+ additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
1848
+ )
1849
+
1850
+ # 在页面加载时自动刷新公司列表,确保显示最新的数据
1851
+ # demo.load(
1852
+ # fn=get_company_list_choices,
1853
+ # inputs=[],
1854
+ # outputs=[company_list_component],
1855
+ # concurrency_limit=None,
1856
+ # )
1857
+
1858
+ # 绑定公司选择事件到状态更新
1859
+ # 注意:这里需要确保create_sidebar中没有重复绑定相同的事件
1860
+ company_list_component.change(
1861
+ fn=lambda x: x, # 直接返回选中的公司名称
1862
+ inputs=[company_list_component],
1863
+ outputs=[selected_company_state],
1864
+ concurrency_limit=None
1865
+ )
1866
+
1867
+ # 绑定公司选择事件到指标仪表板更新
1868
+ def update_metrics_dashboard_wrapper(company_name):
1869
+ if company_name:
1870
+ # 显示loading状态
1871
+ loading_html = f'''
1872
+ <div style="display: flex; justify-content: center; align-items: center; height: 300px;">
1873
+ <div style="text-align: center;">
1874
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1875
+ <p style="margin-top: 20px; color: #666;">Loading financial data for {company_name}...</p>
1876
+ <style>
1877
+ @keyframes spin {{
1878
+ 0% {{ transform: rotate(0deg); }}
1879
+ 100% {{ transform: rotate(360deg); }}
1880
+ }}
1881
+ </style>
1882
+ </div>
1883
+ </div>
1884
+ '''
1885
+ yield loading_html, loading_html, loading_html
1886
+
1887
+ # 获取更新后的数据
1888
+ try:
1889
+ stock_card_html, financial_metrics_html, income_table_html = update_metrics_dashboard(company_name)
1890
+ yield stock_card_html, financial_metrics_html, income_table_html
1891
+ except Exception as e:
1892
+ error_html = f'''
1893
+ <div style="padding: 20px; text-align: center; color: #666;">
1894
+ <p>Error loading financial data: {str(e)}</p>
1895
+ <p>Please try again later.</p>
1896
+ </div>
1897
+ '''
1898
+ yield error_html, error_html, error_html
1899
+ else:
1900
+ # 如果没有选择公司,返回空内容
1901
+ empty_html = "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1902
+ yield empty_html, empty_html, empty_html
1903
+
1904
+ selected_company_state.change(
1905
+ fn=update_metrics_dashboard_wrapper,
1906
+ inputs=[selected_company_state],
1907
+ outputs=list(metrics_dashboard_components),
1908
+ concurrency_limit=None
1909
+ )
1910
+
1911
+ return demo
1912
+
1913
+ if __name__ == "__main__":
1914
+ demo = main()
1915
+ demo.launch(share=True)
app copy 3.py ADDED
@@ -0,0 +1,1657 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import datetime
4
+ import re
5
+ import pandas as pd
6
+ from sqlalchemy import true
7
+ # from dotenv import load_dotenv
8
+
9
+ # # 加载.env文件中的环境变量
10
+ # load_dotenv()
11
+ # from EasyFinancialAgent.chat import query_company
12
+ from chatbot.chat_main import respond
13
+ import globals as g
14
+ from service.mysql_service import get_companys, insert_company, get_company_by_name
15
+ from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot
16
+ from service.company import check_company_exists
17
+ from service.hf_upload import get_hf_files_with_links
18
+ from MarketandStockMCP.news_quote_mcp import get_company_news, get_quote
19
+ from EasyReportDataMCP.report_mcp import query_financial_data
20
+ from service.report_service import get_report_data, query_company_advanced
21
+ from service.report_tools import build_financial_metrics_three_year_data, calculate_yoy_comparison, extract_financial_table, extract_last_three_with_fallback, get_yearly_data
22
+ from service.three_year_table_tool import build_table_format
23
+ from service.three_year_tool import process_financial_data_with_metadata
24
+ from service.tool_processor import get_stock_price
25
+
26
+
27
+ get_companys_state = True
28
+ my_companies = [
29
+ {'company_name': 'Alibaba', 'stock_code': 'BABA'},
30
+ {'company_name': 'NVIDIA', 'stock_code': 'NVDA'},
31
+ {'company_name': 'Amazon', 'stock_code': 'AMZN'},
32
+ {'company_name': 'Intel', 'stock_code': 'INTC'},
33
+ {'company_name': 'Meta', 'stock_code': 'META'},
34
+ {'company_name': 'Google', 'stock_code': 'GOOGL'},
35
+ {'company_name': 'Apple', 'stock_code': 'AAPL'},
36
+ {'company_name': 'Tesla', 'stock_code': 'TSLA'},
37
+ {'company_name': 'AMD', 'stock_code': 'AMD'},
38
+ {'company_name': 'Microsoft', 'stock_code': 'MSFT'}
39
+ ]
40
+ # JavaScript代码用于读取和存储数据
41
+ js_code = """
42
+ function handleStorage(operation, key, value) {
43
+ if (operation === 'set') {
44
+ localStorage.setItem(key, value);
45
+ return `已存储: ${key} = ${value}`;
46
+ } else if (operation === 'get') {
47
+ let storedValue = localStorage.getItem(key);
48
+ if (storedValue === null) {
49
+ return `未找到键: ${key}`;
50
+ }
51
+ return `读取到: ${key} = ${storedValue}`;
52
+ } else if (operation === 'clear') {
53
+ localStorage.removeItem(key);
54
+ return `已清除: ${key}`;
55
+ } else if (operation === 'clearAll') {
56
+ localStorage.clear();
57
+ return '已清除所有数据';
58
+ }
59
+ }
60
+ """
61
+ custom_css = """
62
+ /* 匹配所有以 gradio-container- 开头的类 */
63
+ div[class^="gradio-container-"],
64
+ div[class*=" gradio-container-"] {
65
+ -webkit-text-size-adjust: 100% !important;
66
+ line-height: 1.5 !important;
67
+ font-family: unset !important;
68
+ -moz-tab-size: 4 !important;
69
+ tab-size: 4 !important;
70
+ }
71
+
72
+ .company-list-container {
73
+ background-color: white;
74
+ border-radius: 0.5rem;
75
+ padding: 0.75rem;
76
+ margin-bottom: 0.75rem;
77
+ border: 1px solid #e5e7eb;
78
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
79
+ width: 100%;
80
+ }
81
+
82
+ /* 隐藏单选框 */
83
+ .company-list-container input[type="radio"] {
84
+ display: none;
85
+ }
86
+
87
+ /* 自定义选项样式 */
88
+ .company-list-container label {
89
+ display: block;
90
+ padding: 0.75rem 1rem;
91
+ margin: 0.25rem 0;
92
+ border-radius: 0.375rem;
93
+ cursor: pointer;
94
+ transition: all 0.2s ease;
95
+ background-color: #f9fafb;
96
+ border: 1px solid #e5e7eb;
97
+ font-size: 1rem;
98
+ text-align: left;
99
+ width: 100%;
100
+ box-sizing: border-box;
101
+ }
102
+
103
+ /* 悬停效果 */
104
+ .company-list-container label:hover {
105
+ background-color: #f3f4f6;
106
+ border-color: #d1d5db;
107
+ }
108
+
109
+ /* 选中效果 - 确保背景色充满整个选项 */
110
+ .company-list-container input[type="radio"]:checked + span {
111
+ # background: #3b82f6 !important;
112
+ color: white !important;
113
+ font-weight: 600 !important;
114
+ display: block;
115
+ width: 100%;
116
+ height: 100%;
117
+ padding: 0.75rem 1rem;
118
+ margin: -0.75rem -1rem;
119
+ border-radius: 0.375rem;
120
+ }
121
+
122
+ .company-list-container span {
123
+ display: block;
124
+ padding: 0;
125
+ border-radius: 0.375rem;
126
+ width: 100%;
127
+ }
128
+
129
+ /* 确保每行只有一个选项 */
130
+ .company-list-container .wrap {
131
+ display: block !important;
132
+ }
133
+
134
+ .company-list-container .wrap li {
135
+ display: block !important;
136
+ width: 100% !important;
137
+ }
138
+ label.selected {
139
+ background: #3b82f6 !important;
140
+ color: white !important;
141
+ }
142
+ """
143
+
144
+ # 全局变量用于存储公司映射关系
145
+ companies_map = {}
146
+
147
+ # 根据公司名称获取股票代码的函数
148
+ def get_stock_code_by_company_name(company_name):
149
+ """根据公司名称获取股票代码"""
150
+ if company_name in companies_map and "CODE" in companies_map[company_name]:
151
+ return companies_map[company_name]["CODE"]
152
+ return "" # 默认返回
153
+
154
+ # 创建一个简单的函数来获取公司列表
155
+ def get_company_list_choices():
156
+ choices = []
157
+ print(f"Getting init add company list choices...{get_companys_state}")
158
+ if not get_companys_state:
159
+ return gr.update(choices=choices)
160
+ try:
161
+ # companies_data = get_companys()
162
+ companies_data = my_companies
163
+ print(f"Getting init add company list choices...companies_data: {companies_data}")
164
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
165
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
166
+ else:
167
+ choices = []
168
+ except:
169
+ choices = []
170
+
171
+ return gr.update(choices=choices)
172
+
173
+ # Sidebar service functions
174
+
175
+ # 处理公司点击事件的函数
176
+ def handle_company_click(company_name):
177
+ """处理公司点击事件,先判断是否已经入库,如果没有则进行入库操作,然后刷新公司列表"""
178
+ print(f"Handling click for company: {company_name}")
179
+
180
+ # 1. 判断是否已经入库
181
+ if not check_company_exists(my_companies, company_name):
182
+ # 2. 如果没有入库,则进行入库操作
183
+ # 获取股票代码(如果有的话)
184
+ stock_code = companies_map.get(company_name, {}).get("CODE", "Unknown")
185
+ print(f"Inserting company {company_name} with code {stock_code}")
186
+
187
+ # 插入公司到数据库
188
+ # success = insert_company(company_name, stock_code)
189
+ my_companies.append({"company_name": company_name, "stock_code": stock_code})
190
+ print(f"Successfully inserted company: {company_name}") # 直接更新companies_map,而不是重新加载整个映射
191
+ # 直接更新companies_map,而不是重新加载整个映射
192
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
193
+ # 使用Gradio的成功提示
194
+ gr.Info(f"Successfully added company: {company_name}")
195
+ # 返回True表示添加成功,需要刷新列表
196
+ return True
197
+ else:
198
+ print(f"Company {company_name} already exists in database")
199
+ # 使用Gradio的警告提示
200
+ gr.Warning(f"Company '{company_name}' already exists")
201
+
202
+ # 3. 返回成功响应
203
+ return None
204
+
205
+ def get_company_list_html(selected_company=""):
206
+ try:
207
+ # 从数据库获取所有公司
208
+ # companies_data = get_companys()
209
+ companies_data = my_companies
210
+ # 检查是否为错误信息
211
+ if isinstance(companies_data, str):
212
+ if "查询执行失败" in companies_data:
213
+ return "<div class='text-red-500'>获取公司列表失败</div>"
214
+ else:
215
+ # 如果是字符串但不是错误信息,可能需要特殊处理
216
+ return ""
217
+
218
+ # 检查是否为DataFrame且为空
219
+ if not isinstance(companies_data, pd.DataFrame) or companies_data.empty:
220
+ return ""
221
+
222
+ # 生成HTML列表
223
+ html_items = []
224
+ for _, row in companies_data.iterrows():
225
+ company_name = row.get('company_name', 'Unknown')
226
+ # 根据是否选中添加不同的样式类
227
+ css_class = "company-item"
228
+ if company_name == selected_company:
229
+ css_class += " selected-company"
230
+ # 使用button元素来确保可点击性
231
+ html_items.append(f'<button class="{css_class}" data-company="{company_name}" style="width:100%; text-align:left; border:none; background:none;">{company_name}</button>')
232
+
233
+ return "\n".join(html_items)
234
+ except Exception as e:
235
+ return f"<div class='text-red-500'>生成公司列表失败: {str(e)}</div>"
236
+
237
+ def initialize_company_list(selected_company=""):
238
+ return get_company_list_html(selected_company)
239
+
240
+ def refresh_company_list(selected_company=""):
241
+ """刷新公司列表,返回最新的HTML内容,带loading效果"""
242
+ # 先返回loading状态
243
+ loading_html = '''
244
+ <div style="display: flex; justify-content: center; align-items: center; height: 100px;">
245
+ <div class="loading-spinner" style="width: 24px; height: 24px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite;"></div>
246
+ <style>
247
+ @keyframes spin {
248
+ 0% { transform: rotate(0deg); }
249
+ 100% { transform: rotate(360deg); }
250
+ }
251
+ </style>
252
+ </div>
253
+ '''
254
+ yield loading_html
255
+
256
+ # 然后返回实际的数据
257
+ yield get_company_list_html(selected_company)
258
+
259
+ # 新增函数:处理公司选择事件
260
+ def select_company(company_name):
261
+ """处理公司选择事件,更新全局状态并返回更新后的公司列表"""
262
+ # 更新全局变量
263
+ g.SELECT_COMPANY = company_name if company_name else ""
264
+ # 对于Radio组件,我们只需要返回更新后的选项列表
265
+ try:
266
+ # companies_data = get_companys()
267
+ companies_data = my_companies
268
+ if isinstance(companies_data, list) and len(companies_data) > 0:
269
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
270
+ choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
271
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
272
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
273
+ else:
274
+ choices = []
275
+ except:
276
+ choices = []
277
+ return gr.update(choices=choices, value=company_name)
278
+
279
+ def initialize_companies_map():
280
+ """初始化 companies_map 字典"""
281
+ global companies_map
282
+ companies_map = {} # 清空之前的映射
283
+
284
+ print("Initializing companies map...")
285
+
286
+ try:
287
+ # 获取预定义的公司列表
288
+ # predefined_companies = [
289
+ # { "NAME": "Alibaba", "CODE": "BABA" },
290
+ # { "NAME": "NVIDIA", "CODE": "NVDA" },
291
+ # { "NAME": "Amazon", "CODE": "AMZN" },
292
+ # { "NAME": "Intel", "CODE": "INTC" },
293
+ # { "NAME": "Meta", "CODE": "META" },
294
+ # { "NAME": "Google", "CODE": "GOOGL" },
295
+ # { "NAME": "Apple", "CODE": "AAPL" },
296
+ # { "NAME": "Tesla", "CODE": "TSLA" },
297
+ # { "NAME": "AMD", "CODE": "AMD" },
298
+ # { "NAME": "Microsoft", "CODE": "MSFT" },
299
+ # ]
300
+
301
+ # 将预定义公司添加到映射中
302
+ # for company in predefined_companies:
303
+ # companies_map[company["NAME"]] = {"NAME": company["NAME"], "CODE": company["CODE"]}
304
+
305
+ # print(f"Predefined companies added: {len(predefined_companies)}")
306
+
307
+ # 从数据库获取公司数据
308
+ # companies_data = get_companys()
309
+ companies_data = my_companies
310
+ # companies_data = window.cachedCompanies or []
311
+
312
+
313
+ print(f"Companies data from DB: {companies_data}")
314
+
315
+ # 如果数据库中有公司数据,则添加到映射中(去重)
316
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
317
+ print(f"Adding {len(companies_data)} companies from database")
318
+ for _, row in companies_data.iterrows():
319
+ company_name = row.get('company_name', 'Unknown')
320
+ stock_code = row.get('stock_code', '')
321
+
322
+ # 确保company_name和stock_code都是字符串类型
323
+ company_name = str(company_name) if company_name is not None else 'Unknown'
324
+ stock_code = str(stock_code) if stock_code is not None else ''
325
+
326
+ # 检查是否已存在于映射中(通过股票代码判断)
327
+ is_duplicate = False
328
+ for existing_company in companies_map.values():
329
+ if existing_company["CODE"] == stock_code:
330
+ is_duplicate = True
331
+ break
332
+
333
+ # 如果不重复,则添加到映射中
334
+ if not is_duplicate:
335
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
336
+ # print(f"Added company: {company_name}")
337
+ else:
338
+ print("No companies found in database")
339
+
340
+ print(f"Final companies map: {companies_map}")
341
+ except Exception as e:
342
+ # 错误处理
343
+ print(f"Error initializing companies map: {str(e)}")
344
+ pass
345
+
346
+ # Sidebar company selector functions
347
+ def update_company_choices(user_input: str):
348
+ """更新公司选择列表"""
349
+ # 第一次 yield:立即显示 modal + loading 提示
350
+ yield gr.update(
351
+ choices=["Searching..."],
352
+ visible=True
353
+ ), gr.update(visible=False, value="") # 添加第二个返回值
354
+
355
+ # 第二次:执行耗时操作(调用 LLM)
356
+ choices = search_company(user_input) # 这是你原来的同步函数
357
+
358
+ # 检查choices是否为错误信息
359
+ if len(choices) > 0 and isinstance(choices[0], str) and not choices[0].startswith("Searching"):
360
+ # 如果是错误信息或非正常格式,显示提示消息
361
+ error_message = choices[0] if len(choices) > 0 else "未知错误"
362
+ # 使用Ant Design风格的错误提示
363
+ error_html = f'''
364
+ <div class="ant-message ant-message-error" style="
365
+ position: fixed;
366
+ top: 20px;
367
+ left: 50%;
368
+ transform: translateX(-50%);
369
+ z-index: 10000;
370
+ padding: 10px 16px;
371
+ border-radius: 4px;
372
+ background: #fff;
373
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
374
+ display: flex;
375
+ align-items: center;
376
+ pointer-events: all;
377
+ animation: messageFadeIn 0.3s ease-in-out;
378
+ ">
379
+ <div style="
380
+ width: 16px;
381
+ height: 16px;
382
+ background: #ff4d4f;
383
+ border-radius: 50%;
384
+ position: relative;
385
+ margin-right: 8px;
386
+ "></div>
387
+ <span>{error_message}</span>
388
+ </div>
389
+ <script>
390
+ setTimeout(function() {{
391
+ var msg = document.querySelector('.ant-message-error');
392
+ if (msg) {{
393
+ msg.style.animation = 'messageFadeOut 0.3s ease-in-out';
394
+ setTimeout(function() {{ msg.remove(); }}, 3000);
395
+ }}
396
+ }}, 3000);
397
+ </script>
398
+ '''
399
+ yield gr.update(choices=["No results found"], visible=True), gr.update(visible=True, value=error_html)
400
+ else:
401
+ # 第三次:更新为真实结果
402
+ yield gr.update(
403
+ choices=choices,
404
+ visible=len(choices) > 0
405
+ ), gr.update(visible=False, value="")
406
+
407
+ def add_company(selected, current_list):
408
+ """添加选中的公司"""
409
+ if selected == "No results found":
410
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
411
+ if selected:
412
+ # print(f"Selected company====: {selected}")
413
+ # 从选择的文本中提取公司名称和股票代码
414
+ # 假设格式为 "公司名称 (股票代码)"
415
+ selected_clean = selected.strip()
416
+ match = re.match(r"^(.+?)\s*\(([^)]+)\)$", selected_clean)
417
+ if match:
418
+ company_name = match.group(1)
419
+ stock_code = match.group(2)
420
+ elif companies_map.get(selected_clean):
421
+ company_name = selected_clean
422
+ stock_code = companies_map[selected_clean]["CODE"]
423
+ else:
424
+ company_name = selected_clean
425
+ stock_code = "Unknown"
426
+
427
+ # print(f"Company name: {company_name}, Stock code: {stock_code}")
428
+ # print(f"Company exists: {check_company_exists(company_name)}")
429
+
430
+ if not check_company_exists(my_companies, company_name):
431
+ # 入库
432
+ # success = insert_company(company_name, stock_code)
433
+ my_companies.append({"company_name": company_name, "stock_code": stock_code})
434
+ # 从数据库获取更新后的公司列表
435
+ try:
436
+ # companies_data = get_companys()
437
+ companies_data = my_companies
438
+ if isinstance(companies_data, list) and len(companies_data) > 0:
439
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
440
+ updated_list = [str(item.get('company_name', 'Unknown')) for item in companies_data]
441
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
442
+ updated_list = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
443
+ else:
444
+ updated_list = []
445
+ except:
446
+ updated_list = []
447
+
448
+ # 添加默认公司选项
449
+ if not updated_list:
450
+ updated_list = ['Alibaba', '腾讯控股', 'Tencent', '阿里巴巴-W', 'Apple']
451
+
452
+ # 成功插入后清除状态消息,并更新Radio组件的选项,同时默认选中刚添加的公司
453
+ # 通过设置value参数,会自动触发change事件来加载数据
454
+ return gr.update(visible=False), gr.update(choices=updated_list, value=company_name), gr.update(visible=False, value="")
455
+
456
+ else:
457
+ # 公司已存在,使用Gradio内置的警告消息
458
+ gr.Warning(f"公司 '{company_name}' 已存在")
459
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
460
+
461
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
462
+
463
+ # Sidebar report section functions
464
+ # 创建一个全局变量来存储公司按钮组件
465
+ company_buttons = {}
466
+
467
+ def create_company_buttons():
468
+ """创建公司按钮组件"""
469
+ # 确保companies_map已被初始化
470
+ if not companies_map:
471
+ initialize_companies_map()
472
+
473
+ # 显示companies_map中的公司列表
474
+ companies = list(companies_map.keys())
475
+
476
+ # 添加调试信息
477
+ print(f"Companies in map: {companies}")
478
+
479
+ # 清空之前的按钮
480
+ company_buttons.clear()
481
+
482
+ if not companies:
483
+ # 如果没有公司,返回一个空的列
484
+ with gr.Column():
485
+ gr.Markdown("暂无公司数据")
486
+ else:
487
+ # 使用Gradio按钮组件创建公司列表
488
+ with gr.Column(elem_classes=["home-company-list"]):
489
+ # 按每行两个公司进行分组
490
+ for i in range(0, len(companies), 2):
491
+ # 检查是否是最后一行且只有一个元素
492
+ if i + 1 < len(companies):
493
+ # 有两个元素
494
+ with gr.Row(elem_classes=["home-company-item-box"]):
495
+ btn1 = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"])
496
+ btn2 = gr.Button(companies[i + 1], elem_classes=["home-company-item", "gradio-button"])
497
+ # 保存按钮引用
498
+ company_buttons[companies[i]] = btn1
499
+ company_buttons[companies[i + 1]] = btn2
500
+ else:
501
+ # 只有一个元素
502
+ with gr.Row(elem_classes=["home-company-item-box", "single-item"]):
503
+ btn = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"])
504
+ # 保存按钮引用
505
+ company_buttons[companies[i]] = btn
506
+
507
+ # 返回按钮字典
508
+ return company_buttons
509
+ def update_report_section(selected_company, report_data, stock_code):
510
+ """根据选中的公司更新报告部分"""
511
+ print(f"Updating report (报告部分): {selected_company}") # 添加调试信息
512
+
513
+ if selected_company == "" or selected_company is None or selected_company == "Unknown":
514
+ # 没有选中的公司,显示公司列表
515
+ # html_content = get_initial_company_list_content()
516
+ # 暂时返回空内容,稍后会用Gradio组件替换
517
+ html_content = ""
518
+ return gr.update(value=html_content, visible=True)
519
+ else:
520
+ # 有选中的公司,显示相关报告
521
+ try:
522
+ # prmpt = f"""
523
+
524
+ # """
525
+ stock_code = get_stock_code_by_company_name(selected_company)
526
+ # result = get_report_data(stock_code)
527
+ # print(f"get_report_data=====================: {result}")
528
+ report_data = query_financial_data(stock_code, "5-Year")
529
+ # report_data = process_financial_data_with_metadata(financial_metrics_pre)
530
+
531
+ # 检查 report_data 是否是列表且第一个元素是字典
532
+ if not isinstance(report_data, list) or len(report_data) == 0:
533
+ return gr.update(value="<div>暂无报告数据</div>", visible=True)
534
+
535
+ # 检查第一个元素是否是字典
536
+ if not isinstance(report_data[0], dict):
537
+ return gr.update(value="<div>数据格式不正常</div>", visible=True)
538
+
539
+ html_content = '<div class="report-list-box bg-white">'
540
+ html_content += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>Financial Reports</h3></div>'
541
+ for report in report_data:
542
+ source_url = report.get('source_url', '#')
543
+ period = report.get('period', 'N/A')
544
+ source_form = report.get('source_form', 'N/A')
545
+ html_content += f'''
546
+ <div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{source_url}', '_blank')">
547
+ <div class="report-item-content">
548
+ <span class="text-gray-800">{period}-{stock_code}-{source_form}</span>
549
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor">
550
+ <path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 10-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l-1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd" />
551
+ </svg>
552
+ </div>
553
+ </div>
554
+ '''
555
+
556
+ html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>'
557
+ html_content += '</div>'
558
+
559
+ return gr.update(value=html_content, visible=True)
560
+ except Exception as e:
561
+ print(f"Error in update_report_section: {str(e)}")
562
+ return gr.update(value=f"<div>报告载入失败: {str(e)}</div>", visible=True)
563
+ def update_news_section(selected_company):
564
+ """根据选中的公司更新报告部分"""
565
+ html_content = ""
566
+ if selected_company == "" or selected_company is None:
567
+ # 没有选中的公司,显示公司列表
568
+ # html_content = get_initial_company_list_content()
569
+ # 暂时返回空内容,稍后会用Gradio组件替换
570
+ return gr.update(value=html_content, visible=True)
571
+ else:
572
+ try:
573
+ stock_code = get_stock_code_by_company_name(selected_company)
574
+ report_data = get_company_news(stock_code, None, None)
575
+ # print(f"新闻列表: {report_data['articles']}")
576
+ # report_data = search_news(selected_company)
577
+ if (report_data['articles']):
578
+ report_data = report_data['articles']
579
+ news_html = "<div class='news-list-box bg-white'>"
580
+ news_html += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>News</h3></div>'
581
+ from datetime import datetime
582
+
583
+ for news in report_data:
584
+ published_at = news['published']
585
+
586
+ # 解析 ISO 8601 时间字符串(注意:strptime 不直接支持 'Z',需替换或使用 fromisoformat)
587
+ dt = datetime.fromisoformat(published_at.replace("Z", "+00:00"))
588
+
589
+ # 格式化为 YYYY.MM.DD
590
+ formatted_date = dt.strftime("%Y.%m.%d")
591
+ news_html += f'''
592
+ <div class="news-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{news['url']}', '_blank')">
593
+ <div class="news-item-content">
594
+ <span class="text-xs text-gray-500">[{formatted_date}]</span>
595
+ <span class="text-gray-800">{news['headline']}</span>
596
+ </div>
597
+ </div>
598
+ '''
599
+ news_html += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}条新闻</span></div>'
600
+ news_html += '</div>'
601
+ html_content += news_html
602
+ except Exception as e:
603
+ print(f"Error updating report section: {str(e)}")
604
+
605
+ return gr.update(value=html_content, visible=True)
606
+
607
+ # Component creation functions
608
+ def create_header():
609
+ """创建头部组件"""
610
+ # 获取当前时间
611
+ current_time = datetime.datetime.now().strftime("%B %d, %Y - Market Data Updated Today")
612
+
613
+ with gr.Row(elem_classes=["header"]):
614
+ # 左侧:图标和标题
615
+ with gr.Column(scale=8):
616
+ # 使用圆柱体SVG图标表示数据库
617
+ gr.HTML('''
618
+ <div class="top-logo-box">
619
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 48 48">
620
+ <g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4">
621
+ <path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11"></path>
622
+ <path d="M44 29c0 3.314-8.954 6-20 6S4 32.314 4 29m40-9c0 3.314-8.954 6-20 6S4 23.314 4 20"></path>
623
+ <ellipse cx="24" cy="10" rx="20" ry="6"></ellipse>
624
+ </g>
625
+ </svg>
626
+ <span class="logo-title">Easy Financial Report Dashboard</span>
627
+ </div>
628
+ ''', elem_classes=["text-2xl"])
629
+
630
+ # 右侧:时间信息
631
+ with gr.Column(scale=2):
632
+ gr.Markdown(current_time, elem_classes=["text-sm-top-time"])
633
+
634
+ def create_company_list(get_companys_state):
635
+ """创建公司列表组件"""
636
+ try:
637
+ # 获取公司列表数据
638
+ # companies_data = get_companys()
639
+ companies_data = my_companies
640
+ print(f"创建公司列表组件 - Companies data: {companies_data}")
641
+ if isinstance(companies_data, list) and len(companies_data) > 0:
642
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
643
+ choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
644
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
645
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
646
+ else:
647
+ choices = []
648
+ except Exception as e:
649
+ print(f"Error creating company list: {str(e)}")
650
+ choices = []
651
+
652
+ # 添加默认公司选项
653
+ if not choices:
654
+ choices = []
655
+
656
+ # 使用Radio组件显示公司列表,不显示标签
657
+ company_list = gr.Radio(
658
+ choices=choices,
659
+ label="",
660
+ interactive=True,
661
+ elem_classes=["company-list-container"],
662
+ container=False, # 不显示外部容器边框
663
+ visible=True
664
+ )
665
+
666
+ return company_list
667
+
668
+ def create_company_selector():
669
+ """创建公司选择器组件"""
670
+ company_input = gr.Textbox(
671
+ show_label=False,
672
+ placeholder="Add Company",
673
+ elem_classes=["company-input-box"],
674
+ lines=1,
675
+ max_lines=1,
676
+ # container=False
677
+ )
678
+
679
+ # 状态消息显示区域
680
+ status_message = gr.HTML(
681
+ "",
682
+ elem_classes=["status-message"],
683
+ visible=False
684
+ )
685
+
686
+ # 弹窗选择列表
687
+ company_modal = gr.Radio(
688
+ show_label=False,
689
+ choices=[],
690
+ visible=False,
691
+ elem_classes=["company-modal"]
692
+ )
693
+
694
+ return company_input, status_message, company_modal
695
+
696
+ def create_report_section():
697
+ """创建报告部分组件"""
698
+ # 创建一个用于显示报告列表的组件,初始显示公司列表
699
+ # initial_content = get_initial_company_list_content()
700
+ # 暂时返回空内容,稍后会用Gradio组件替换
701
+ initial_content = ""
702
+ # print(f"Initial content: {initial_content}") # 添加调试信息
703
+
704
+ report_display = gr.HTML(initial_content)
705
+ return report_display
706
+
707
+ def create_news_section():
708
+ """创建新闻部分组件"""
709
+ initial_content = ""
710
+ news_display = gr.HTML(initial_content)
711
+ return news_display
712
+
713
+ def format_financial_metrics(data: dict, prev_data: dict = None) -> list: # pyright: ignore[reportArgumentType]
714
+ """
715
+ 将原始财务数据转换为 financial_metrics 格式。
716
+
717
+ Args:
718
+ data (dict): 当前财年数据(必须包含 total_revenue, net_income 等字段)
719
+ prev_data (dict, optional): 上一财年数据,用于计算 change。若未提供,change 设为 "--"
720
+
721
+ Returns:
722
+ list[dict]: 符合 financial_metrics 格式的列表
723
+ """
724
+
725
+ def format_currency(value: float) -> str:
726
+ """将数字格式化为 $XB / $XM / $XK"""
727
+ if value >= 1e9:
728
+ return f"${value / 1e9:.2f}B"
729
+ elif value >= 1e6:
730
+ return f"${value / 1e6:.2f}M"
731
+ elif value >= 1e3:
732
+ return f"${value / 1e3:.2f}K"
733
+ else:
734
+ return f"${value:.2f}"
735
+
736
+ def calculate_change(current: float, previous: float) -> tuple:
737
+ """计算变化百分比和颜色"""
738
+ if previous == 0:
739
+ return "--", "gray"
740
+ change_pct = (current - previous) / abs(previous) * 100
741
+ sign = "+" if change_pct >= 0 else ""
742
+ color = "green" if change_pct >= 0 else "red"
743
+ return f"{sign}{change_pct:.1f}%", color
744
+
745
+ # 定义指标映射
746
+ metrics_config = [
747
+ {
748
+ "key": "total_revenue",
749
+ "label": "Total Revenue",
750
+ "is_currency": True,
751
+ "eps_like": False
752
+ },
753
+ {
754
+ "key": "net_income",
755
+ "label": "Net Income",
756
+ "is_currency": True,
757
+ "eps_like": False
758
+ },
759
+ {
760
+ "key": "earnings_per_share",
761
+ "label": "Earnings Per Share",
762
+ "is_currency": False, # EPS 不用 B/M 单位
763
+ "eps_like": True
764
+ },
765
+ {
766
+ "key": "operating_expenses",
767
+ "label": "Operating Expenses",
768
+ "is_currency": True,
769
+ "eps_like": False
770
+ },
771
+ {
772
+ "key": "operating_cash_flow",
773
+ "label": "Cash Flow",
774
+ "is_currency": True,
775
+ "eps_like": False
776
+ }
777
+ ]
778
+
779
+ result = []
780
+ for item in metrics_config:
781
+ key = item["key"]
782
+ current_val = data.get(key)
783
+ if current_val is None:
784
+ continue
785
+
786
+ # 格式化 value
787
+ if item["eps_like"]:
788
+ value_str = f"${current_val:.2f}"
789
+ elif item["is_currency"]:
790
+ value_str = format_currency(current_val)
791
+ else:
792
+ value_str = str(current_val)
793
+
794
+ # 计算 change(如果有上期数据)
795
+ if prev_data and key in prev_data:
796
+ prev_val = prev_data[key]
797
+ change_str, color = calculate_change(current_val, prev_val)
798
+ else:
799
+ change_str = "--"
800
+ color = "gray"
801
+
802
+ result.append({
803
+ "label": item["label"],
804
+ "value": value_str,
805
+ "change": change_str,
806
+ "color": color
807
+ })
808
+
809
+ return result
810
+
811
+
812
+ def create_sidebar():
813
+ """创建侧边栏组件"""
814
+ # 初始化 companies_map
815
+ initialize_companies_map()
816
+
817
+ with gr.Column(elem_classes=["sidebar"]):
818
+ # 公司选择
819
+ with gr.Group(elem_classes=["card"]):
820
+ gr.Markdown("### Select Company", elem_classes=["card-title", "left-card-title"])
821
+ with gr.Column():
822
+ company_list = create_company_list(get_companys_state)
823
+
824
+ # 创建公司列表
825
+ # if not get_companys_state:
826
+ # getCompanyFromStorage = gr.Button("读取")
827
+ # getCompanyFromStorage.click(
828
+ # fn=create_company_list(True),
829
+ # inputs=[],
830
+ # outputs=[company_list, status_message]
831
+ # )
832
+
833
+ # 创建公司选择器
834
+ company_input, status_message, company_modal = create_company_selector()
835
+
836
+ # 绑定事件
837
+ company_input.submit(
838
+ fn=update_company_choices,
839
+ inputs=[company_input],
840
+ outputs=[company_modal, status_message]
841
+ )
842
+
843
+ company_modal.change(
844
+ fn=add_company,
845
+ inputs=[company_modal, company_list],
846
+ outputs=[company_modal, company_list, status_message]
847
+ )
848
+
849
+ # 创建公司按钮组件
850
+ # # company_buttons = create_company_buttons()
851
+
852
+ # # 为每个公司按钮绑定点击事件
853
+ # def make_click_handler(company_name):
854
+ # def handler():
855
+ # result = handle_company_click(company_name)
856
+ # # 如果添加成功,刷新Select Company列表并默认选中刚添加的公司
857
+ # if result is True:
858
+ # # 正确地刷新通过create_company_list()创建的Radio组件
859
+ # try:
860
+ # # companies_data = get_companys()
861
+ # companies_data = my_companies
862
+ # if isinstance(companies_data, list) and len(companies_data) > 0:
863
+ # # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
864
+ # updated_choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
865
+ # elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
866
+ # updated_choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
867
+ # else:
868
+ # updated_choices = []
869
+ # except:
870
+ # updated_choices = []
871
+ # # 使用gr.update来正确更新Radio组件,并默认选中刚添加的公司
872
+ # # 同时触发change事件来加载数据
873
+ # return gr.update(choices=updated_choices, value=company_name)
874
+ # return None
875
+ # return handler
876
+
877
+ # for company_name, button in company_buttons.items():
878
+ # button.click(
879
+ # fn=make_click_handler(company_name),
880
+ # inputs=[],
881
+ # outputs=[company_list]
882
+ # )
883
+
884
+ # 创建一个容器来容纳报告部分,初始时隐藏
885
+ with gr.Group(elem_classes=["report-news-box"]) as report_section_group:
886
+ # gr.Markdown("### Financial Reports", elem_classes=["card-title", "left-card-title"])
887
+ report_display = create_report_section()
888
+ news_display = create_news_section()
889
+
890
+
891
+ # 处理公司选择事件
892
+ def select_company_handler(company_name):
893
+ """处理公司选择事件的处理器"""
894
+ # 更新全局变量
895
+ g.SELECT_COMPANY = company_name if company_name else ""
896
+
897
+ # 更新报告部分的内容
898
+ updated_report_display = update_report_section(company_name, None, None)
899
+
900
+ updated_news_display = update_news_section(company_name)
901
+ # 根据是否选择了公司来决定显示/隐藏报告部分
902
+ if company_name:
903
+ # 有选中的公司,显示报告部分
904
+ return gr.update(visible=True), updated_report_display, updated_news_display
905
+ else:
906
+ # 没有选中的公司,隐藏报告部分
907
+ return gr.update(visible=False), updated_report_display, updated_news_display
908
+
909
+ company_list.change(
910
+ fn=select_company_handler,
911
+ inputs=[company_list],
912
+ outputs=[report_section_group, report_display, news_display]
913
+ )
914
+
915
+ # 返回公司列表组件和报告部分组件
916
+ return company_list, report_section_group, report_display, news_display
917
+
918
+ def build_income_table(table_data):
919
+ # 兼容两种数据结构:
920
+ # 1. 新结构:包含 list_data 和 yoy_rates 的字典
921
+ # 2. 旧结构:直接是二维数组
922
+ if isinstance(table_data, dict) and "list_data" in table_data:
923
+ # 新结构
924
+ income_statement = table_data["list_data"]
925
+ yoy_rates = table_data["yoy_rates"] or []
926
+ else:
927
+ # 旧结构,直接使用传入的数据
928
+ income_statement = table_data
929
+ yoy_rates = []
930
+
931
+ # 创建一个映射,将年份列索引映射到增长率
932
+ yoy_map = {}
933
+ if len(yoy_rates) > 1 and len(yoy_rates[0]) > 1:
934
+ # 获取增长率表头(跳过第一列"Category")
935
+ yoy_headers = yoy_rates[0][1:]
936
+
937
+ # 为每个指标行创建增长率映射
938
+ for i, yoy_row in enumerate(yoy_rates[1:], 1): # 跳过标题行
939
+ category = yoy_row[0]
940
+ yoy_map[category] = {}
941
+ for j, rate in enumerate(yoy_row[1:]):
942
+ if j < len(yoy_headers):
943
+ yoy_map[category][yoy_headers[j]] = rate
944
+
945
+ table_rows = ""
946
+ header_row = income_statement[0]
947
+
948
+ for i, row in enumerate(income_statement):
949
+ if i == 0:
950
+ row_style = "background-color: #f5f5f5; font-weight: 500;"
951
+ else:
952
+ row_style = "background-color: #f9f9f9;"
953
+ cells = ""
954
+
955
+ for j, cell in enumerate(row):
956
+ if j == 0:
957
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
958
+ else:
959
+ # 添加增长率箭头(如果有的话)
960
+ growth = None
961
+ category = row[0]
962
+ # j是当前单元格索引,0是类别列,1,2,3...是数据列
963
+ # yoy_map的键是年份,例如"2024/FY"
964
+ if i > 0 and category in yoy_map and j > 0 and j < len(header_row):
965
+ year_header = header_row[j]
966
+ if year_header in yoy_map[category]:
967
+ growth = yoy_map[category][year_header]
968
+
969
+ if growth and growth != "N/A":
970
+ arrow = "▲" if growth.startswith("+") else "▼"
971
+ color = "green" if growth.startswith("+") else "red"
972
+ cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px; position: relative;'>
973
+ <div>{cell}</div>
974
+ <div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div>
975
+ </td>"""
976
+ else:
977
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
978
+ table_rows += f"<tr style='{row_style}'>{cells}</tr>"
979
+
980
+ html = f"""
981
+ <div style="min-width: 400px;max-width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
982
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
983
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
984
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
985
+ </svg>
986
+ <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
987
+ </div>
988
+ <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
989
+ {table_rows}
990
+ </table>
991
+ </div>
992
+ """
993
+ return html
994
+ def create_metrics_dashboard():
995
+ """创建指标仪表板组件"""
996
+ with gr.Row(elem_classes=["metrics-dashboard"]):
997
+ card_custom_style = '''
998
+ background-color: white;
999
+ border-radius: 0.5rem;
1000
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
1001
+ padding: 1.25rem;
1002
+ min-height: 250px !important;
1003
+ text-align: center;
1004
+ '''
1005
+
1006
+ # 模拟数据
1007
+ company_info = {
1008
+ "name": "N/A",
1009
+ "symbol": "NYSE:N/A",
1010
+ "price": 0,
1011
+ "change": 0,
1012
+ "change_percent": 0.41,
1013
+ "open": 165.20,
1014
+ "high": 166.37,
1015
+ "low": 156.15,
1016
+ "prev_close": 157.01,
1017
+ "volume": "27.10M"
1018
+ }
1019
+ financial_metrics = [
1020
+ {"label": "Total Revenue", "value": "N/A", "change": "N/A", "color": "grey"},
1021
+ {"label": "Net Income", "value": "N/A", "change": "N/A", "color": "grey"},
1022
+ {"label": "Earnings Per Share", "value": "N/A", "change": "N/A", "color": "grey"},
1023
+ {"label": "Operating Expenses", "value": "N/A", "change": "N/A", "color": "grey"},
1024
+ {"label": "Cash Flow", "value": "N/A", "change": "N/A", "color": "grey"}
1025
+ ]
1026
+ income_statement = {
1027
+ "list_data": [
1028
+ ["Category", "N/A/FY", "N/A/FY", "N/A/FY"],
1029
+ ["Total", "N/A", "N/A", "N/A"],
1030
+ ["Net Income", "N/A", "N/A", "N/A.4M"],
1031
+ ["Earnings Per Share", "N/A", "N/A", "N/A"],
1032
+ ["Operating Expenses", "N/A", "N/A", "N/A"],
1033
+ ["Cash Flow", "N/A", "N/A", "N/A"]
1034
+ ],
1035
+ "yoy_rates": []
1036
+ # "yoy_rates": [
1037
+ # ["Category", "N/A/FY", "N/A/FY"],
1038
+ # ["Total", "N/A", "N/A"],
1039
+ # ["Net Income", "+3.05%", "-6.00%"],
1040
+ # ["Earnings Per Share", "+3.05%", "-6.00%"],
1041
+ # ["Operating Expenses", "+29.17%", "-6.00%"],
1042
+ # ["Cash Flow", "-13.05%", "-6.00%"]
1043
+ # ]
1044
+ }
1045
+ yearly_data = 'N/A'
1046
+ # 增长变化的 HTML 字符(箭头+百分比)
1047
+ def render_change(change: str, color: str):
1048
+ if change.startswith("+"):
1049
+ return f'<span style="color:{color};">▲{change}</span>'
1050
+ else:
1051
+ return f'<span style="color:{color};">▼{change}</span>'
1052
+
1053
+ # 构建左侧卡片
1054
+ def build_stock_card():
1055
+ html = f"""
1056
+ <div style="width: 250px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1057
+ <div style="font-size: 14px; color: #555;">N/A</div>
1058
+ <div style="font-size: 12px; color: #888;">N/A</div>
1059
+ <div style="font-size: 32px; font-weight: bold; margin: 8px 0;">N/A</div>
1060
+ <div style="font-size: 14px; margin: 8px 0;">N/A</div>
1061
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1062
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1063
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1064
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1065
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1066
+
1067
+ </div>
1068
+ </div>
1069
+ """
1070
+ return html
1071
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1072
+ # 构建中间卡片
1073
+ def build_financial_metrics():
1074
+ metrics_html = ""
1075
+ for item in financial_metrics:
1076
+ change_html = render_change(item["change"], item["color"])
1077
+ metrics_html += f"""
1078
+ <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
1079
+ <div style="font-size: 14px; color: #555;">{item['label']}</div>
1080
+ <div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div>
1081
+ </div>
1082
+ """
1083
+
1084
+ html = f"""
1085
+ <div style="min-width: 300px;max-width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1086
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;">
1087
+ <div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;">
1088
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1089
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1090
+ </svg>
1091
+ <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1092
+ </div>
1093
+ <div style="font-size: 16px; color: #8f8f8f;">
1094
+ YTD data
1095
+ </div>
1096
+ </div>
1097
+ {metrics_html}
1098
+ </div>
1099
+ """
1100
+ return html
1101
+
1102
+
1103
+ # 主函数:返回所有 HTML 片段
1104
+ def get_dashboard():
1105
+ with gr.Row():
1106
+ with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]):
1107
+ stock_card_html = gr.HTML(build_stock_card(), elem_classes=["metric-card-left"])
1108
+ with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]):
1109
+ financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"])
1110
+ with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]):
1111
+ # 传递income_statement参数
1112
+ income_table_html = gr.HTML(build_income_table(income_statement), elem_classes=["metric-card-right"])
1113
+ return stock_card_html, financial_metrics_html, income_table_html
1114
+
1115
+ # 创建指标仪表板并保存引用
1116
+ stock_card_component, financial_metrics_component, income_table_component = get_dashboard()
1117
+
1118
+ # 将组件引用保存到全局变量,以便在其他地方使用
1119
+ global metrics_dashboard_components
1120
+ metrics_dashboard_components = (stock_card_component, financial_metrics_component, income_table_component)
1121
+
1122
+ # 更新指标仪表板的函数
1123
+ def update_metrics_dashboard(company_name):
1124
+ """根据选择的公司更新指标仪表板"""
1125
+ company_info = {}
1126
+ # 尝试获取股票价格数据,但不中断程序执行
1127
+ stock_code = ""
1128
+ try:
1129
+ # 根据选择的公司获取股票代码
1130
+ stock_code = get_stock_code_by_company_name(company_name)
1131
+ company_info = get_quote(stock_code.strip())
1132
+ company_info['company'] = company_name
1133
+ print(f"股票价格数据 {company_info}")
1134
+ except Exception as e:
1135
+ print(f"获取股票价格数据失败: {e}")
1136
+
1137
+ financial_metrics_pre = query_financial_data(stock_code, "5-Year")
1138
+ financial_metrics = []
1139
+ year_data = None
1140
+ three_year_data = None
1141
+ try:
1142
+ # financial_metrics = process_financial_data_with_metadata(financial_metrics_pre)
1143
+ result = process_financial_data_with_metadata(financial_metrics_pre)
1144
+
1145
+ # 按需提取字段
1146
+ financial_metrics = result["financial_metrics"]
1147
+ year_data = result["year_data"]
1148
+ three_year_data = result["three_year_data"]
1149
+ print(f"格式化后的财务数据: {financial_metrics}")
1150
+ except Exception as e:
1151
+ print(f"Error process_financial_data: {e}")
1152
+
1153
+ yearly_data = year_data
1154
+ table_data = build_table_format(three_year_data)
1155
+
1156
+ # 增长变化的 HTML 字符(箭头+百分比)
1157
+ def render_change(change: str, color: str):
1158
+ if change.startswith("+"):
1159
+ return f'<span style="color:{color};">▲{change}</span>'
1160
+ else:
1161
+ return f'<span style="color:{color};">▼{change}</span>'
1162
+
1163
+ # 构建左侧卡片
1164
+ def build_stock_card(company_info):
1165
+ try:
1166
+ if not company_info or not isinstance(company_info, dict):
1167
+ company_name = "N/A"
1168
+ symbol = "N/A"
1169
+ price = "N/A"
1170
+ change_html = '<span style="color:#888;">N/A</span>'
1171
+ open_val = high_val = low_val = prev_close_val = volume_display = "N/A"
1172
+ else:
1173
+ company_name = company_info.get("company", "N/A")
1174
+ symbol = company_info.get("symbol", "N/A")
1175
+ price = company_info.get("current_price", "N/A")
1176
+
1177
+ # 解析 change
1178
+ change_str = company_info.get("change", "0")
1179
+ try:
1180
+ change = float(change_str)
1181
+ except (ValueError, TypeError):
1182
+ change = 0.0
1183
+
1184
+ # 解析 change_percent
1185
+ change_percent = company_info.get("percent_change", "0%")
1186
+ # try:
1187
+ # change_percent = float(change_percent_str.rstrip('%'))
1188
+ # except (ValueError, TypeError):
1189
+ # change_percent = 0.0
1190
+
1191
+ change_color = "green" if change >= 0 else "red"
1192
+ sign = "+" if change >= 0 else ""
1193
+ change_html = f'<span style="color:{change_color};">{sign}{change:.2f} ({change_percent:+.2f}%)</span>'
1194
+
1195
+ # 其他价格字段(可选:也可格式化为 2 位小数)
1196
+ open_val = company_info.get("open", "N/A")
1197
+ high_val = company_info.get("high", "N/A")
1198
+ low_val = company_info.get("low", "N/A")
1199
+ prev_close_val = company_info.get("previous_close", "N/A")
1200
+ # raw_volume = company_info.get("volume", "N/A")
1201
+ # volume_display = format_volume(raw_volume)
1202
+
1203
+ html = f"""
1204
+ <div style="width: 250px; height: 300px !important; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1205
+ <div style="font-size: 16px; color: #555; font-weight: 500;">{company_name}</div>
1206
+ <div style="font-size: 12px; color: #888;">NYSE:{symbol}</div>
1207
+ <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1208
+ <div style="font-size: 32px; font-weight: bold;">{price}</div>
1209
+ <div style="font-size: 14px;">{change_html}</div>
1210
+ </div>
1211
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1212
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{open_val}</div>
1213
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{high_val}</div>
1214
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{low_val}</div>
1215
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{prev_close_val}</div>
1216
+ </div>
1217
+ </div>
1218
+ """
1219
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{volume_display}</div>
1220
+
1221
+ return html
1222
+
1223
+ except Exception as e:
1224
+ print(f"Error building stock card: {e}")
1225
+ return '<div style="width:250px; padding:16px; color:red;">Error loading stock data</div>'
1226
+ # 构建中间卡片
1227
+ def build_financial_metrics(yearly_data):
1228
+ metrics_html = ""
1229
+ for item in financial_metrics:
1230
+ change_html = render_change(item["change"], item["color"])
1231
+ metrics_html += f"""
1232
+ <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
1233
+ <div style="font-size: 14px; color: #555;">{item['label']}</div>
1234
+ <div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div>
1235
+ </div>
1236
+ """
1237
+
1238
+ html = f"""
1239
+ <div style="width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1240
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;">
1241
+ <div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;">
1242
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1243
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1244
+ </svg>
1245
+ <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1246
+ </div>
1247
+ <div style="font-size: 16px; color: #8f8f8f;">
1248
+ YTD data
1249
+ </div>
1250
+ </div>
1251
+ {metrics_html}
1252
+ </div>
1253
+ """
1254
+ return html
1255
+ # 返回三个HTML组件的内容
1256
+ return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data)
1257
+
1258
+ def create_tab_content(tab_name, company_name):
1259
+ """创建Tab内容组件"""
1260
+ if tab_name == "summary":
1261
+ print(f"company_name: {company_name}")
1262
+ # content = get_invest_suggest(company_name)
1263
+ gr.Markdown("# 11111", elem_classes=["invest-suggest-md-box"])
1264
+
1265
+ elif tab_name == "detailed":
1266
+ with gr.Column(elem_classes=["tab-content"]):
1267
+ gr.Markdown("Financial Statements", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"])
1268
+
1269
+ with gr.Row(elem_classes=["gap-6"]):
1270
+ # 收入报表 (3/5宽度)
1271
+ with gr.Column(elem_classes=["w-3/5", "bg-gray-50", "rounded-xl", "p-4"]):
1272
+ gr.Markdown("Income Statement", elem_classes=["font-medium", "mb-3"])
1273
+ # 这里将显示收入报表表格
1274
+
1275
+ # 资产负债表和现金流量表 (2/5宽度)
1276
+ with gr.Column(elem_classes=["w-2/5", "flex", "flex-col", "gap-6"]):
1277
+ # 资产负债表
1278
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1279
+ gr.Markdown("Balance Sheet Summary", elem_classes=["font-medium", "mb-3"])
1280
+ # 这里将显示资产负债表图表
1281
+
1282
+ # 现金流量表
1283
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1284
+ with gr.Row(elem_classes=["justify-between", "items-start"]):
1285
+ gr.Markdown("Cash Flow Statement", elem_classes=["font-medium"])
1286
+ gr.Markdown("View Detailed", elem_classes=["text-xs", "text-blue-600", "font-medium"])
1287
+
1288
+ with gr.Column(elem_classes=["mt-4", "space-y-3"]):
1289
+ # 经营现金流
1290
+ with gr.Column():
1291
+ with gr.Row(elem_classes=["justify-between"]):
1292
+ gr.Markdown("Operating Cash Flow")
1293
+ gr.Markdown("$982M", elem_classes=["font-medium"])
1294
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1295
+ with gr.Column(elem_classes=["bg-green-500", "h-1.5", "rounded-full"], scale=85):
1296
+ gr.Markdown("")
1297
+
1298
+ # 投资现金流
1299
+ with gr.Column():
1300
+ with gr.Row(elem_classes=["justify-between"]):
1301
+ gr.Markdown("Investing Cash Flow")
1302
+ gr.Markdown("-$415M", elem_classes=["font-medium"])
1303
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1304
+ with gr.Column(elem_classes=["bg-blue-500", "h-1.5", "rounded-full"], scale=42):
1305
+ gr.Markdown("")
1306
+
1307
+ # 融资现金流
1308
+ with gr.Column():
1309
+ with gr.Row(elem_classes=["justify-between"]):
1310
+ gr.Markdown("Financing Cash Flow")
1311
+ gr.Markdown("-$212M", elem_classes=["font-medium"])
1312
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1313
+ with gr.Column(elem_classes=["bg-red-500", "h-1.5", "rounded-full"], scale=25):
1314
+ gr.Markdown("")
1315
+
1316
+ elif tab_name == "comparative":
1317
+ with gr.Column(elem_classes=["tab-content"]):
1318
+ gr.Markdown("Industry Benchmarking", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"])
1319
+
1320
+ # 收入增长对比
1321
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4", "mb-6"]):
1322
+ gr.Markdown("Revenue Growth - Peer Comparison", elem_classes=["font-medium", "mb-3"])
1323
+ # 这里将显示对比图表
1324
+
1325
+ # 利润率和报告预览网格
1326
+ with gr.Row(elem_classes=["grid-cols-2", "gap-6"]):
1327
+ # 利润率表格
1328
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1329
+ gr.Markdown("Profitability Ratios", elem_classes=["font-medium", "mb-3"])
1330
+ # 这里将显示利润率表格
1331
+
1332
+ # 报告预览
1333
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1334
+ gr.Markdown("Report Preview", elem_classes=["font-medium", "mb-3"])
1335
+ # 这里将显示报告预览
1336
+
1337
+ def create_chat_panel():
1338
+ """创建聊天面板组件"""
1339
+ # with gr.Column(elem_classes=["chat-panel"]):
1340
+ # 聊天头部
1341
+ # with gr.Row(elem_classes=["p-4", "border-b", "border-gray-200", "items-center", "gap-2"]):
1342
+ # gr.Markdown("🤖", elem_classes=["text-xl", "text-blue-600"])
1343
+ # gr.Markdown("Financial Assistant", elem_classes=["font-medium"])
1344
+
1345
+ # 聊天区域
1346
+ # 一行代码嵌入!
1347
+ # chat_component = create_financial_chatbot()
1348
+ # chat_component.render()
1349
+ # create_financial_chatbot()
1350
+ # gr.LoginButton()
1351
+ # chatbot = gr.Chatbot(
1352
+ # value=[
1353
+ # {"role": "assistant", "content": "I'm your financial assistant, how can I help you today?"},
1354
+
1355
+ # # {"role": "assistant", "content": "Hello! I can help you analyze financial data. Ask questions like \"Show revenue trends\" or \"Compare profitability ratios\""},
1356
+ # # {"role": "user", "content": "Show revenue trends for last 4 quarters"},
1357
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1358
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1359
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1360
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"}
1361
+ # ],
1362
+ # type="messages",
1363
+ # # elem_classes=["min-h-0", "overflow-y-auto", "space-y-4", "chat-content-box"],
1364
+ # show_label=False,
1365
+ # autoscroll=True,
1366
+ # show_copy_button=True,
1367
+ # height=400,
1368
+ # container=False,
1369
+ # )
1370
+
1371
+ # # 输入区域
1372
+ # with gr.Row(elem_classes=["border-t", "border-gray-200", "gap-2"]):
1373
+ # msg = gr.Textbox(
1374
+ # placeholder="Ask a financial question...",
1375
+ # elem_classes=["flex-1", "border", "border-gray-300", "rounded-lg", "px-4", "py-2", "focus:border-blue-500"],
1376
+ # show_label=False,
1377
+ # lines=1,
1378
+ # submit_btn=True,
1379
+ # container=False,
1380
+ # )
1381
+ # msg.submit(
1382
+ # chat_bot,
1383
+ # [msg, chatbot],
1384
+ # [msg, chatbot],
1385
+ # queue=True,
1386
+ # )
1387
+
1388
+ # def load_css_files(css_dir, filenames):
1389
+ # css_content = ""
1390
+ # for filename in filenames:
1391
+ # path = os.path.join(css_dir, filename)
1392
+ # if os.path.exists(path):
1393
+ # with open(path, "r", encoding="utf-8") as f:
1394
+ # css_content += f.read() + "\n"
1395
+ # else:
1396
+ # print(f"⚠️ CSS file not found: {path}")
1397
+ # return css_content
1398
+ def main():
1399
+ # 获取当前目录
1400
+ current_dir = os.path.dirname(os.path.abspath(__file__))
1401
+ css_dir = os.path.join(current_dir, "css")
1402
+
1403
+ # def load_css_files(css_dir, filenames):
1404
+ # """读取多个 CSS 文件并合并为一个字符串"""
1405
+ # css_content = ""
1406
+ # for filename in filenames:
1407
+ # path = os.path.join(css_dir, filename)
1408
+ # if os.path.exists(path):
1409
+ # with open(path, "r", encoding="utf-8") as f:
1410
+ # css_content += f.read() + "\n"
1411
+ # else:
1412
+ # print(f"Warning: CSS file not found: {path}")
1413
+ # return css_content
1414
+ # 设置CSS路径
1415
+ css_paths = [
1416
+ os.path.join(css_dir, "main.css"),
1417
+ os.path.join(css_dir, "components.css"),
1418
+ os.path.join(css_dir, "layout.css")
1419
+ ]
1420
+ # css_dir = "path/to/your/css/folder" # 替换为你的实际路径
1421
+ # 自动定位 css 文件夹(与 app.py 同级)
1422
+ # BASE_DIR = os.path.dirname(os.path.abspath(__file__))
1423
+ # CSS_DIR = os.path.join(BASE_DIR, "css")
1424
+
1425
+ # css_files = ["main.css", "components.css", "layout.css"]
1426
+ # combined_css = load_css_files(CSS_DIR, css_files)
1427
+ # print(combined_css)
1428
+
1429
+ with gr.Blocks(
1430
+ title="Financial Analysis Dashboard",
1431
+ css_paths=css_paths,
1432
+ css=custom_css,
1433
+ # css=combined_css
1434
+ ) as demo:
1435
+
1436
+ # 添加处理公司点击事件的路由
1437
+ # 创建一个状态组件来跟踪选中的公司
1438
+ selected_company_state = gr.State("")
1439
+
1440
+ with gr.Column(elem_classes=["container", "container-h"]):
1441
+ # 头部
1442
+ create_header()
1443
+
1444
+ # 创建主布局
1445
+ with gr.Row(elem_classes=["main-content-box"]):
1446
+ # 左侧边栏
1447
+ with gr.Column(scale=1, min_width=350):
1448
+ # 获取company_list组件的引用
1449
+ company_list_component, report_section_component, report_display_component, news_display_component = create_sidebar()
1450
+
1451
+ # 主内容区域
1452
+ with gr.Column(scale=9):
1453
+
1454
+ # 指标仪表板
1455
+ create_metrics_dashboard()
1456
+
1457
+ with gr.Row(elem_classes=["main-content-box"]):
1458
+ with gr.Column(scale=8):
1459
+ # Tab内容
1460
+ with gr.Tabs():
1461
+ with gr.TabItem("Investment Suggestion", elem_classes=["tab-item"]):
1462
+ # 创建一个用于显示公司名称的组件
1463
+ # company_display = gr.Markdown("# Please select a company")
1464
+ # 创建一个占位符用于显示tab内容
1465
+ tab_content = gr.Markdown(elem_classes=["invest-suggest-md-box"])
1466
+
1467
+ # 当选中的公司改变时,更新显示
1468
+ # selected_company_state.change(
1469
+ # fn=lambda company: f"# Investment Suggestions for {company}" if company else "# Please select a company",
1470
+ # inputs=[selected_company_state],
1471
+ # outputs=[company_display]
1472
+ # )
1473
+
1474
+ # 当选中的公司改变时,重新加载tab内容
1475
+ def update_tab_content(company):
1476
+ if company:
1477
+ # 显示loading状态
1478
+ loading_html = f'''
1479
+ <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
1480
+ <div style="text-align: center;">
1481
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1482
+ <p style="margin-top: 20px; color: #666;">Loading investment suggestions for {company}...</p>
1483
+ <style>
1484
+ @keyframes spin {{
1485
+ 0% {{ transform: rotate(0deg); }}
1486
+ 100% {{ transform: rotate(360deg); }}
1487
+ }}
1488
+ </style>
1489
+ </div>
1490
+ </div>
1491
+ '''
1492
+ yield loading_html
1493
+
1494
+ # 获取投资建议数据
1495
+ try:
1496
+ # content = get_invest_suggest(company)
1497
+ stock_code = get_stock_code_by_company_name(company)
1498
+ yield query_company_advanced(stock_code, "suggestion")
1499
+ # yield content
1500
+ except Exception as e:
1501
+ error_html = f'''
1502
+ <div style="padding: 20px; text-align: center; color: #666;">
1503
+ <p>Error loading investment suggestions: {str(e)}</p>
1504
+ <p>Please try again later.</p>
1505
+ </div>
1506
+ '''
1507
+ yield error_html
1508
+ else:
1509
+ yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1510
+
1511
+ selected_company_state.change(
1512
+ fn=update_tab_content,
1513
+ inputs=[selected_company_state],
1514
+ outputs=[tab_content],
1515
+ )
1516
+ with gr.TabItem("Analysis Report", elem_classes=["tab-item"]):
1517
+ # 创建一个用于显示公司名称的组件
1518
+ # analysis_company_display = gr.Markdown("# Please select a company")
1519
+ # 创建一个占位符用于显示tab内容
1520
+ analysis_tab_content = gr.Markdown(elem_classes=["analysis-report-md-box"])
1521
+
1522
+ # 当选中的公司改变时,更新显示
1523
+ # selected_company_state.change(
1524
+ # fn=lambda company: f"# Analysis Report for {company}" if company else "# Please select a company",
1525
+ # inputs=[selected_company_state],
1526
+ # outputs=[analysis_company_display]
1527
+ # )
1528
+
1529
+ # 当选中的公司改变时,重新加载tab内容
1530
+ def update_analysis_tab_content(company):
1531
+ if company:
1532
+ # 显示loading状态
1533
+ loading_html = f'''
1534
+ <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
1535
+ <div style="text-align: center;">
1536
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1537
+ <p style="margin-top: 20px; color: #666;">Loading analysis report for {company}...</p>
1538
+ <style>
1539
+ @keyframes spin {{
1540
+ 0% {{ transform: rotate(0deg); }}
1541
+ 100% {{ transform: rotate(360deg); }}
1542
+ }}
1543
+ </style>
1544
+ </div>
1545
+ </div>
1546
+ '''
1547
+ yield loading_html
1548
+
1549
+ # 获取分析报告数据
1550
+ try:
1551
+ # 这里应该调用获取详细分析报告的函数
1552
+ # 暂时使用占位内容,您需要替换为实际的函数调用
1553
+ # content = f"# Analysis Report for {company}\n\nDetailed financial analysis for {company} will be displayed here."
1554
+ stock_code = get_stock_code_by_company_name(company)
1555
+ # result = query_company_advanced(stock_code)
1556
+ # print(f"Result=====================: {result}")
1557
+ # yield get_analysis_report(company)
1558
+ yield query_company_advanced(stock_code, "report")
1559
+ except Exception as e:
1560
+ error_html = f'''
1561
+ <div style="padding: 20px; text-align: center; color: #666;">
1562
+ <p>Error loading analysis report: {str(e)}</p>
1563
+ <p>Please try again later.</p>
1564
+ </div>
1565
+ '''
1566
+ yield error_html
1567
+ else:
1568
+ yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1569
+
1570
+ selected_company_state.change(
1571
+ fn=update_analysis_tab_content,
1572
+ inputs=[selected_company_state],
1573
+ outputs=[analysis_tab_content]
1574
+ )
1575
+ # with gr.TabItem("Comparison", elem_classes=["tab-item"]):
1576
+ # create_tab_content("comparison")
1577
+ with gr.Column(scale=2, min_width=400):
1578
+ # 聊天面板
1579
+
1580
+ # chatbot.render()
1581
+ # gr.LoginButton()
1582
+ gr.ChatInterface(
1583
+ respond,
1584
+ title="Easy Financial Report",
1585
+ additional_inputs=[
1586
+ gr.State(value=""), # CRITICAL: Store session URL across turns (hidden from UI)
1587
+ gr.State(value={}) # CRITICAL: Store agent context across turns (hidden from UI)
1588
+ ],
1589
+ additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
1590
+ )
1591
+
1592
+ # 在页面加载时自动刷新公司列表,确保显示最新的数据
1593
+ # demo.load(
1594
+ # fn=get_company_list_choices,
1595
+ # inputs=[],
1596
+ # outputs=[company_list_component],
1597
+ # concurrency_limit=None,
1598
+ # )
1599
+
1600
+ # 绑定公司选择事件到状态更新
1601
+ # 注意:这里需要确保create_sidebar中没有重复绑定相同的事件
1602
+ company_list_component.change(
1603
+ fn=lambda x: x, # 直接返回选中的公司名称
1604
+ inputs=[company_list_component],
1605
+ outputs=[selected_company_state],
1606
+ concurrency_limit=None
1607
+ )
1608
+
1609
+ # 绑定公司选择事件到指标仪表板更新
1610
+ def update_metrics_dashboard_wrapper(company_name):
1611
+ if company_name:
1612
+ # 显示loading状态
1613
+ loading_html = f'''
1614
+ <div style="display: flex; justify-content: center; align-items: center; height: 300px;">
1615
+ <div style="text-align: center;">
1616
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1617
+ <p style="margin-top: 20px; color: #666;">Loading financial data for {company_name}...</p>
1618
+ <style>
1619
+ @keyframes spin {{
1620
+ 0% {{ transform: rotate(0deg); }}
1621
+ 100% {{ transform: rotate(360deg); }}
1622
+ }}
1623
+ </style>
1624
+ </div>
1625
+ </div>
1626
+ '''
1627
+ yield loading_html, loading_html, loading_html
1628
+
1629
+ # 获取更新后的数据
1630
+ try:
1631
+ stock_card_html, financial_metrics_html, income_table_html = update_metrics_dashboard(company_name)
1632
+ yield stock_card_html, financial_metrics_html, income_table_html
1633
+ except Exception as e:
1634
+ error_html = f'''
1635
+ <div style="padding: 20px; text-align: center; color: #666;">
1636
+ <p>Error loading financial data: {str(e)}</p>
1637
+ <p>Please try again later.</p>
1638
+ </div>
1639
+ '''
1640
+ yield error_html, error_html, error_html
1641
+ else:
1642
+ # 如果没有选择公司,返回空内容
1643
+ empty_html = "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1644
+ yield empty_html, empty_html, empty_html
1645
+
1646
+ selected_company_state.change(
1647
+ fn=update_metrics_dashboard_wrapper,
1648
+ inputs=[selected_company_state],
1649
+ outputs=list(metrics_dashboard_components),
1650
+ concurrency_limit=None
1651
+ )
1652
+
1653
+ return demo
1654
+
1655
+ if __name__ == "__main__":
1656
+ demo = main()
1657
+ demo.launch(share=True)
app.py CHANGED
@@ -9,6 +9,7 @@ from sqlalchemy import true
9
  # # 加载.env文件中的环境变量
10
  # load_dotenv()
11
  # from EasyFinancialAgent.chat import query_company
 
12
  from chatbot.chat_main import respond
13
  import globals as g
14
  from service.mysql_service import get_companys, insert_company, get_company_by_name
@@ -25,7 +26,18 @@ from service.tool_processor import get_stock_price
25
 
26
 
27
  get_companys_state = True
28
- my_companies = []
 
 
 
 
 
 
 
 
 
 
 
29
  # JavaScript代码用于读取和存储数据
30
  js_code = """
31
  function handleStorage(operation, key, value) {
@@ -276,7 +288,6 @@ def initialize_companies_map():
276
  # 获取预定义的公司列表
277
  predefined_companies = [
278
  { "NAME": "Alibaba", "CODE": "BABA" },
279
- { "NAME": "阿里巴巴-W", "CODE": "09988" },
280
  { "NAME": "NVIDIA", "CODE": "NVDA" },
281
  { "NAME": "Amazon", "CODE": "AMZN" },
282
  { "NAME": "Intel", "CODE": "INTC" },
@@ -286,14 +297,13 @@ def initialize_companies_map():
286
  { "NAME": "Tesla", "CODE": "TSLA" },
287
  { "NAME": "AMD", "CODE": "AMD" },
288
  { "NAME": "Microsoft", "CODE": "MSFT" },
289
- { "NAME": "ASML", "CODE": "ASML" }
290
  ]
291
 
292
  # 将预定义公司添加到映射中
293
  for company in predefined_companies:
294
  companies_map[company["NAME"]] = {"NAME": company["NAME"], "CODE": company["CODE"]}
295
 
296
- print(f"Predefined companies added: {len(predefined_companies)}")
297
 
298
  # 从数据库获取公司数据
299
  # companies_data = get_companys()
@@ -344,6 +354,11 @@ def update_company_choices(user_input: str):
344
  ), gr.update(visible=False, value="") # 添加第二个返回值
345
 
346
  # 第二次:执行耗时操作(调用 LLM)
 
 
 
 
 
347
  choices = search_company(user_input) # 这是你原来的同步函数
348
 
349
  # 检查choices是否为错误信息
@@ -514,14 +529,14 @@ def update_report_section(selected_company, report_data, stock_code):
514
 
515
  # """
516
  stock_code = get_stock_code_by_company_name(selected_company)
517
- result = get_report_data(stock_code)
518
- print(f"get_report_data=====================: {result}")
519
  report_data = query_financial_data(stock_code, "5-Year")
520
  # report_data = process_financial_data_with_metadata(financial_metrics_pre)
521
 
522
  # 检查 report_data 是否是列表且第一个元素是字典
523
  if not isinstance(report_data, list) or len(report_data) == 0:
524
- return gr.update(value="<div>暂无报告数据</div>", visible=True)
525
 
526
  # 检查第一个元素是否是字典
527
  if not isinstance(report_data[0], dict):
@@ -687,10 +702,16 @@ def create_company_selector():
687
  def create_report_section():
688
  """创建报告部分组件"""
689
  # 创建一个用于显示报告列表的组件,初始显示公司列表
690
- # initial_content = get_initial_company_list_content()
691
- # 暂时返回空内容,稍后会用Gradio组件替换
692
  initial_content = ""
693
- # print(f"Initial content: {initial_content}") # 添加调试信息
 
 
 
 
 
 
 
694
 
695
  report_display = gr.HTML(initial_content)
696
  return report_display
@@ -838,39 +859,39 @@ def create_sidebar():
838
  )
839
 
840
  # 创建公司按钮组件
841
- company_buttons = create_company_buttons()
842
 
843
- # 为每个公司按钮绑定点击事件
844
- def make_click_handler(company_name):
845
- def handler():
846
- result = handle_company_click(company_name)
847
- # 如果添加成功,刷新Select Company列表并默认选中刚添加的公司
848
- if result is True:
849
- # 正确地刷新通过create_company_list()创建的Radio组件
850
- try:
851
- # companies_data = get_companys()
852
- companies_data = my_companies
853
- if isinstance(companies_data, list) and len(companies_data) > 0:
854
- # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
855
- updated_choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
856
- elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
857
- updated_choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
858
- else:
859
- updated_choices = []
860
- except:
861
- updated_choices = []
862
- # 使用gr.update来正确更新Radio组件,并默认选中刚添加的公司
863
- # 同时触发change事件来加载数据
864
- return gr.update(choices=updated_choices, value=company_name)
865
- return None
866
- return handler
867
 
868
- for company_name, button in company_buttons.items():
869
- button.click(
870
- fn=make_click_handler(company_name),
871
- inputs=[],
872
- outputs=[company_list]
873
- )
874
 
875
  # 创建一个容器来容纳报告部分,初始时隐藏
876
  with gr.Group(elem_classes=["report-news-box"]) as report_section_group:
@@ -994,22 +1015,73 @@ def create_metrics_dashboard():
994
  text-align: center;
995
  '''
996
 
997
- # 模拟数据
998
- company_info = {
999
- "name": "N/A",
1000
- "symbol": "NYSE:N/A",
1001
- "price": 0,
1002
- "change": 0,
1003
- "change_percent": 0.41,
1004
- "open": 165.20,
1005
- "high": 166.37,
1006
- "low": 156.15,
1007
- "prev_close": 157.01,
1008
- "volume": "27.10M"
1009
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
 
1011
- # financial_metrics = query_financial_data("NVDA", "最新财务数据")
1012
- # print(f"最新财务数据: {financial_metrics}")
 
 
 
 
1013
  financial_metrics = [
1014
  {"label": "Total Revenue", "value": "N/A", "change": "N/A", "color": "grey"},
1015
  {"label": "Net Income", "value": "N/A", "change": "N/A", "color": "grey"},
@@ -1017,33 +1089,17 @@ def create_metrics_dashboard():
1017
  {"label": "Operating Expenses", "value": "N/A", "change": "N/A", "color": "grey"},
1018
  {"label": "Cash Flow", "value": "N/A", "change": "N/A", "color": "grey"}
1019
  ]
1020
- # income_statement = [
1021
- # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1022
- # ["Total", "130350M", "126491M", "134567M"],
1023
- # ["Net Income", "11081", "10598M", "9818.4M"],
1024
- # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1025
- # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1026
- # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1027
- # ]
1028
  income_statement = {
1029
- "list_data": [
1030
- ["Category", "N/A/FY", "N/A/FY", "N/A/FY"],
1031
- ["Total", "N/A", "N/A", "N/A"],
1032
- ["Net Income", "N/A", "N/A", "N/A.4M"],
1033
- ["Earnings Per Share", "N/A", "N/A", "N/A"],
1034
- ["Operating Expenses", "N/A", "N/A", "N/A"],
1035
- ["Cash Flow", "N/A", "N/A", "N/A"]
1036
- ],
1037
- "yoy_rates": []
1038
- # "yoy_rates": [
1039
- # ["Category", "N/A/FY", "N/A/FY"],
1040
- # ["Total", "N/A", "N/A"],
1041
- # ["Net Income", "+3.05%", "-6.00%"],
1042
- # ["Earnings Per Share", "+3.05%", "-6.00%"],
1043
- # ["Operating Expenses", "+29.17%", "-6.00%"],
1044
- # ["Cash Flow", "-13.05%", "-6.00%"]
1045
- # ]
1046
- }
1047
  yearly_data = 'N/A'
1048
  # 增长变化的 HTML 字符(箭头+百分比)
1049
  def render_change(change: str, color: str):
@@ -1051,30 +1107,22 @@ def create_metrics_dashboard():
1051
  return f'<span style="color:{color};">▲{change}</span>'
1052
  else:
1053
  return f'<span style="color:{color};">▼{change}</span>'
1054
-
1055
- # 构建左侧卡片
1056
- def build_stock_card():
1057
- html = f"""
1058
- <div style="width: 250px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1059
- <div style="font-size: 14px; color: #555;">N/A</div>
1060
- <div style="font-size: 12px; color: #888;">N/A</div>
1061
- <div style="font-size: 32px; font-weight: bold; margin: 8px 0;">N/A</div>
1062
- <div style="font-size: 14px; margin: 8px 0;">N/A</div>
1063
- <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1064
- <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1065
- <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1066
- <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1067
- <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1068
-
1069
- </div>
1070
- </div>
1071
- """
1072
- return html
1073
- # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1074
  # 构建中间卡片
1075
  def build_financial_metrics():
 
 
 
 
 
 
 
 
 
 
 
 
1076
  metrics_html = ""
1077
- for item in financial_metrics:
1078
  change_html = render_change(item["change"], item["color"])
1079
  metrics_html += f"""
1080
  <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
@@ -1090,7 +1138,7 @@ def create_metrics_dashboard():
1090
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1091
  <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1092
  </svg>
1093
- <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1094
  </div>
1095
  <div style="font-size: 16px; color: #8f8f8f;">
1096
  YTD data
@@ -1104,14 +1152,25 @@ def create_metrics_dashboard():
1104
 
1105
  # 主函数:返回所有 HTML 片段
1106
  def get_dashboard():
 
 
 
 
 
 
 
 
 
 
 
1107
  with gr.Row():
1108
  with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]):
1109
  stock_card_html = gr.HTML(build_stock_card(), elem_classes=["metric-card-left"])
1110
  with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]):
1111
  financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"])
1112
  with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]):
1113
- # 传递income_statement参数
1114
- income_table_html = gr.HTML(build_income_table(income_statement), elem_classes=["metric-card-right"])
1115
  return stock_card_html, financial_metrics_html, income_table_html
1116
 
1117
  # 创建指标仪表板并保存引用
@@ -1124,106 +1183,20 @@ def create_metrics_dashboard():
1124
  # 更新指标仪表板的函数
1125
  def update_metrics_dashboard(company_name):
1126
  """根据选择的公司更新指标仪表板"""
1127
- # 模拟数据
1128
- # company_info = {
1129
- # "name": company_name,
1130
- # "symbol": "NYSE:BABA",
1131
- # "price": 157.65,
1132
- # "change": 0.64,
1133
- # "change_percent": 0.41,
1134
- # "open": 165.20,
1135
- # "high": 166.37,
1136
- # "low": 156.15,
1137
- # "prev_close": 157.01,
1138
- # "volume": "27.10M"
1139
- # }
1140
  company_info = {}
1141
  # 尝试获取股票价格数据,但不中断程序执行
1142
  stock_code = ""
1143
  try:
1144
  # 根据选择的公司获取股票代码
1145
  stock_code = get_stock_code_by_company_name(company_name)
1146
- # result = get_quote(company_name.strip())
1147
-
1148
- # company_info2 = get_stock_price(stock_code)
1149
- # company_info2 = get_stock_price_from_bailian(stock_code)
1150
- # print(f"股票价格数据: {company_info2}")
1151
  company_info = get_quote(stock_code.strip())
1152
  company_info['company'] = company_name
1153
- print(f"股票价格数据====: {company_info}")
1154
- # 查询结果:{
1155
- # "company": "阿里巴巴",
1156
- # "symbol": "BABA",
1157
- # "open": "159.09",
1158
- # "high": "161.46",
1159
- # "low": "150.00",
1160
- # "price": "157.60",
1161
- # "volume": "21453064",
1162
- # "latest trading day": "2025-11-27",
1163
- # "previous close": "157.01",
1164
- # "change": "+0.59",
1165
- # "change_percent": "+0.38%"
1166
- # }BABA
1167
- # 如果成功获取数据,则用实际数据替换模拟数据
1168
- # if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
1169
- # import json
1170
- # # 解析返回的JSON数据
1171
- # data_text = company_info2["content"][0]["text"]
1172
- # stock_data = json.loads(data_text)
1173
-
1174
- # # 提取数据
1175
- # quote = stock_data["Global Quote"]
1176
-
1177
- # # 转换交易量单位
1178
- # volume = int(quote['06. volume'])
1179
- # if volume >= 1000000:
1180
- # volume_str = f"{volume / 1000000:.2f}M"
1181
- # elif volume >= 1000:
1182
- # volume_str = f"{volume / 1000:.2f}K"
1183
- # else:
1184
- # volume_str = str(volume)
1185
-
1186
- # company_info = {
1187
- # "name": company_name,
1188
- # "symbol": f"NYSE:{quote['01. symbol']}",
1189
- # "price": float(quote['05. price']),
1190
- # "change": float(quote['09. change']),
1191
- # "change_percent": float(quote['10. change percent'].rstrip('%')),
1192
- # "open": float(quote['02. open']),
1193
- # "high": float(quote['03. high']),
1194
- # "low": float(quote['04. low']),
1195
- # "prev_close": float(quote['08. previous close']),
1196
- # "volume": volume_str
1197
- # }
1198
  except Exception as e:
1199
  print(f"获取股票价格数据失败: {e}")
1200
- company_info2 = None
1201
-
1202
- # financial_metrics = [
1203
- # {"label": "Total Revenue", "value": "$2.84B", "change": "+12.4%", "color": "green"},
1204
- # {"label": "Net Income", "value": "$685M", "change": "-3.2%", "color": "red"},
1205
- # {"label": "Earnings Per Share", "value": "$2.15", "change": "-3.2%", "color": "red"},
1206
- # {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
1207
- # {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
1208
- # ]
1209
  financial_metrics_pre = query_financial_data(stock_code, "5-Year")
1210
- # financial_metrics_pre = query_financial_data(company_name, "5年趋势")
1211
- # print(f"最新财务数据: {financial_metrics_pre}")
1212
- # financial_metrics = format_financial_metrics(financial_metrics_pre)
1213
-
1214
-
1215
- # financial_metrics_pre_2 = extract_last_three_with_fallback(financial_metrics_pre)
1216
- # print(f"提取的3年数据: {financial_metrics_pre_2}")
1217
- # financial_metrics_pre = {
1218
- # "metrics": financial_metrics_pre_2
1219
- # }
1220
  financial_metrics = []
1221
- # try:
1222
- # # financial_metrics = calculate_yoy_comparison(financial_metrics_pre)
1223
- # financial_metrics = build_financial_metrics_three_year_data(financial_metrics_pre)
1224
- # print(f"格式化后的财务数据: {financial_metrics}")
1225
- # except Exception as e:
1226
- # print(f"Error calculating YOY comparison: {e}")
1227
  year_data = None
1228
  three_year_data = None
1229
  try:
@@ -1235,89 +1208,11 @@ def update_metrics_dashboard(company_name):
1235
  year_data = result["year_data"]
1236
  three_year_data = result["three_year_data"]
1237
  print(f"格式化后的财务数据: {financial_metrics}")
1238
- # 拿report数据
1239
- # try:
1240
- # # 从 result 中获取报告数据
1241
- # if 'report_data' in result: # 假设 result 中包含 report_data 键
1242
- # report_data = result['report_data']
1243
- # else:
1244
- # # 如果 result 中没有直接包含 report_data,则从其他键中获取
1245
- # # 这需要根据实际的 result 数据结构来调整
1246
- # report_data = result.get('reports', []) # 示例:假设数据在 'reports' 键下
1247
-
1248
- # 更新报告部分的内容
1249
- # 这里需要调用 update_report_section 函数并传入 report_data
1250
- # 注意:update_report_section 可能需要修改以接受 report_data 参数
1251
- # updated_report_content = update_report_section(company_name, report_data, stock_code)
1252
-
1253
- # 然后将 updated_report_content 返回,以便在 UI 中更新
1254
- # 这需要修改函数的返回值以包含报告内容
1255
-
1256
- # except Exception as e:
1257
- # print(f"Error updating report section with result data: {e}")
1258
- # updated_report_content = "<div>Failed to load report data</div>"
1259
  except Exception as e:
1260
  print(f"Error process_financial_data: {e}")
1261
-
1262
-
1263
- # income_statement = [
1264
- # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1265
- # ["Total", "130350M", "126491M", "134567M"],
1266
- # ["Net Income", "11081", "10598M", "9818.4M"],
1267
- # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1268
- # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1269
- # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1270
- # ]
1271
-
1272
- # table_data = None
1273
- # try:
1274
- # table_data = extract_financial_table(financial_metrics_pre)
1275
- # print(table_data)
1276
- # except Exception as e:
1277
- # print(f"Error extract_financial_table: {e}")
1278
- # yearly_data = None
1279
- # try:
1280
- # yearly_data = get_yearly_data(financial_metrics_pre)
1281
- # except Exception as e:
1282
- # print(f"Error get_yearly_data: {e}")
1283
-
1284
- # ======
1285
- # table_data = [
1286
- # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1287
- # ["Total", "130350M", "126491M", "134567M"],
1288
- # ["Net Income", "11081", "10598M", "9818.4M"],
1289
- # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1290
- # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1291
- # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1292
- # ]
1293
  yearly_data = year_data
1294
  table_data = build_table_format(three_year_data)
1295
- # print(f"table_data: {table_data}")
1296
- # yearly_data = None
1297
- # try:
1298
- # yearly_data = get_yearly_data(financial_metrics_pre)
1299
- # except Exception as e:
1300
- # print(f"Error get_yearly_data: {e}")
1301
- #=======
1302
-
1303
- # exp = {
1304
- # "list_data": [
1305
- # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1306
- # ["Total", "130350M", "126491M", "134567M"],
1307
- # ["Net Income", "11081", "10598M", "9818.4M"],
1308
- # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1309
- # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1310
- # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1311
- # ],
1312
- # "yoy_rates": [
1313
- # ["Category", "2024/FY", "2023/FY"],
1314
- # ["Total", "+3.05%", "-6.00%"],
1315
- # ["Net Income", "+3.05%", "-6.00%"],
1316
- # ["Earnings Per Share", "+3.05%", "-6.00%"],
1317
- # ["Operating Expenses", "+29.17%", "-6.00%"],
1318
- # ["Cash Flow", "-13.05%", "-6.00%"]
1319
- # ]
1320
- # }
1321
 
1322
  # 增长变化的 HTML 字符(箭头+百分比)
1323
  def render_change(change: str, color: str):
@@ -1418,59 +1313,6 @@ def update_metrics_dashboard(company_name):
1418
  </div>
1419
  """
1420
  return html
1421
-
1422
- # 构建右侧表格
1423
- # def build_income_table(income_statement):
1424
- # table_rows = ""
1425
- # for i, row in enumerate(income_statement):
1426
- # if i == 0:
1427
- # row_style = "background-color: #f5f5f5; font-weight: 500;"
1428
- # else:
1429
- # row_style = "background-color: #f9f9f9;"
1430
- # cells = ""
1431
- # for j, cell in enumerate(row):
1432
- # if j == 0:
1433
- # cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
1434
- # else:
1435
- # # 添加增长箭头(模拟数据)
1436
- # growth = None
1437
- # if i == 1 and j == 1: growth = "+3.05%"
1438
- # elif i == 1 and j == 2: growth = "-6.00%"
1439
- # elif i == 2 and j == 1: growth = "+3.05%"
1440
- # elif i == 2 and j == 2: growth = "-6.00%"
1441
- # elif i == 3 and j == 1: growth = "+3.05%"
1442
- # elif i == 3 and j == 2: growth = "-6.00%"
1443
- # elif i == 4 and j == 1: growth = "+29.17%"
1444
- # elif i == 4 and j == 2: growth = "+29.17%"
1445
- # elif i == 5 and j == 1: growth = "-13.05%"
1446
- # elif i == 5 and j == 2: growth = "+29.17%"
1447
-
1448
- # if growth:
1449
- # arrow = "▲" if growth.startswith("+") else "▼"
1450
- # color = "green" if growth.startswith("+") else "red"
1451
- # cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px; position: relative;'>
1452
- # <div>{cell}</div>
1453
- # <div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div>
1454
- # </td>"""
1455
- # else:
1456
- # cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
1457
- # table_rows += f"<tr style='{row_style}'>{cells}</tr>"
1458
-
1459
- # html = f"""
1460
- # <div style="width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1461
- # <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1462
- # <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1463
- # <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1464
- # </svg>
1465
- # <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
1466
- # </div>
1467
- # <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
1468
- # {table_rows}
1469
- # </table>
1470
- # </div>
1471
- # """
1472
- # return html
1473
-
1474
  # 返回三个HTML组件的内容
1475
  return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data)
1476
 
@@ -1480,46 +1322,6 @@ def create_tab_content(tab_name, company_name):
1480
  print(f"company_name: {company_name}")
1481
  # content = get_invest_suggest(company_name)
1482
  gr.Markdown("# 11111", elem_classes=["invest-suggest-md-box"])
1483
- # gr.Markdown(content, elem_classes=["invest-suggest-md-box"])
1484
- # gr.Markdown("""
1485
- # ## Investment Suggestions
1486
-
1487
- # ### Company Overview
1488
-
1489
- # GlobalTech inc. is a leading technology company with strong performance in the Q3 2025 period. The companyshows consistent revenue growth and maintains a healthy fnancial position.
1490
-
1491
- # ### Key Strengths
1492
-
1493
- # - Revenue Growth: 12.4% year-over-year increase demonstrates strong market demandDiversifed Portfolio: Multiple revenue streams reduce business risk
1494
-
1495
- # - Innovation Focus: Continued investment in R&D drives future growth potential
1496
-
1497
- # ### Financial Health Indicators
1498
-
1499
- # - Liquidity: Current ratio of 1.82 indicates good short-term fnancial health
1500
-
1501
- # - Proftability: Net income of $685M, though down slightly quarter-over-quarter0
1502
- # - Cash Flow: Strong operating cash flow of $982M supports operations and growth initiatives
1503
-
1504
- # ### Investment Recommendation
1505
-
1506
- # BUY - GlobalTech Inc. presents a solid investment opportunity with:
1507
- # - Consistent revenue growth trajectory
1508
- # - Strong market position in key technology segments
1509
- # - Healthy balance sheet and cash flow generation
1510
-
1511
- # ### Risk Considerations
1512
-
1513
- # Quarterly net income decline warrants monitoring
1514
- # | Category | Q3 2025 | Q2 2025 | YoY % |
1515
- # |--------------------|-----------|-----------|----------|
1516
- # | Total Revenue | $2,842M | $2,712M | +12.4% |
1517
- # | Gross Profit | $1,203M | $1,124M | +7.0% |
1518
- # | Operating Income | $742M | $798M | -7.0% |
1519
- # | Net Income | $685M | $708M | -3.2% |
1520
- # | Earnings Per Share | $2.15 | $2.22 | -3.2% |
1521
- # """, elem_classes=["invest-suggest-md-box"])
1522
-
1523
 
1524
  elif tab_name == "detailed":
1525
  with gr.Column(elem_classes=["tab-content"]):
@@ -1685,6 +1487,9 @@ def main():
1685
  # combined_css = load_css_files(CSS_DIR, css_files)
1686
  # print(combined_css)
1687
 
 
 
 
1688
  with gr.Blocks(
1689
  title="Financial Analysis Dashboard",
1690
  css_paths=css_paths,
@@ -1694,7 +1499,7 @@ def main():
1694
 
1695
  # 添加处理公司点击事件的路由
1696
  # 创建一个状态组件来跟踪选中的公司
1697
- selected_company_state = gr.State("")
1698
 
1699
  with gr.Column(elem_classes=["container", "container-h"]):
1700
  # 头部
@@ -1752,8 +1557,10 @@ def main():
1752
 
1753
  # 获取投资建议数据
1754
  try:
1755
- content = get_invest_suggest(company)
1756
- yield content
 
 
1757
  except Exception as e:
1758
  error_html = f'''
1759
  <div style="padding: 20px; text-align: center; color: #666;">
@@ -1846,13 +1653,30 @@ def main():
1846
  additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
1847
  )
1848
 
1849
- # 在页面加载时自动刷新公司列表,确保显示最新的数据
1850
- # demo.load(
1851
- # fn=get_company_list_choices,
1852
- # inputs=[],
1853
- # outputs=[company_list_component],
1854
- # concurrency_limit=None,
1855
- # )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1856
 
1857
  # 绑定公司选择事件到状态更新
1858
  # 注意:这里需要确保create_sidebar中没有重复绑定相同的事件
 
9
  # # 加载.env文件中的环境变量
10
  # load_dotenv()
11
  # from EasyFinancialAgent.chat import query_company
12
+ from EasyFinancialAgent.chat_direct import advanced_search_company_detailed, search_company_direct
13
  from chatbot.chat_main import respond
14
  import globals as g
15
  from service.mysql_service import get_companys, insert_company, get_company_by_name
 
26
 
27
 
28
  get_companys_state = True
29
+ my_companies = [
30
+ {'company_name': 'Alibaba', 'stock_code': 'BABA', "cik": "0001577552"},
31
+ {'company_name': 'NVIDIA', 'stock_code': 'NVDA', "cik": "0001045810"},
32
+ {'company_name': 'Amazon', 'stock_code': 'AMZN', "cik": "0001018724"},
33
+ {'company_name': 'Intel', 'stock_code': 'INTC', "cik": "0000050863"},
34
+ {'company_name': 'Meta', 'stock_code': 'META', "cik": "0001326801"},
35
+ {'company_name': 'Google', 'stock_code': 'GOOGL', "cik": "0001652044"},
36
+ {'company_name': 'Apple', 'stock_code': 'AAPL', "cik": "0000320193"},
37
+ {'company_name': 'Tesla', 'stock_code': 'TSLA', "cik": "0001318605"},
38
+ {'company_name': 'AMD', 'stock_code': 'AMD', "cik": "0000002488"},
39
+ {'company_name': 'Microsoft', 'stock_code': 'MSFT', "cik": "0000789019"}
40
+ ]
41
  # JavaScript代码用于读取和存储数据
42
  js_code = """
43
  function handleStorage(operation, key, value) {
 
288
  # 获取预定义的公司列表
289
  predefined_companies = [
290
  { "NAME": "Alibaba", "CODE": "BABA" },
 
291
  { "NAME": "NVIDIA", "CODE": "NVDA" },
292
  { "NAME": "Amazon", "CODE": "AMZN" },
293
  { "NAME": "Intel", "CODE": "INTC" },
 
297
  { "NAME": "Tesla", "CODE": "TSLA" },
298
  { "NAME": "AMD", "CODE": "AMD" },
299
  { "NAME": "Microsoft", "CODE": "MSFT" },
 
300
  ]
301
 
302
  # 将预定义公司添加到映射中
303
  for company in predefined_companies:
304
  companies_map[company["NAME"]] = {"NAME": company["NAME"], "CODE": company["CODE"]}
305
 
306
+ # print(f"Predefined companies added: {len(predefined_companies)}")
307
 
308
  # 从数据库获取公司数据
309
  # companies_data = get_companys()
 
354
  ), gr.update(visible=False, value="") # 添加第二个返回值
355
 
356
  # 第二次:执行耗时操作(调用 LLM)
357
+ choices = search_company_direct(user_input)
358
+ print(f"新接口1查到的公司: {choices}")
359
+ choices = search_company_direct(user_input)
360
+ choices = advanced_search_company_detailed(user_input)
361
+ print(f"新接口2查到的公司: {choices}")
362
  choices = search_company(user_input) # 这是你原来的同步函数
363
 
364
  # 检查choices是否为错误信息
 
529
 
530
  # """
531
  stock_code = get_stock_code_by_company_name(selected_company)
532
+ # result = get_report_data(stock_code)
533
+ # print(f"get_report_data=====================: {result}")
534
  report_data = query_financial_data(stock_code, "5-Year")
535
  # report_data = process_financial_data_with_metadata(financial_metrics_pre)
536
 
537
  # 检查 report_data 是否是列表且第一个元素是字典
538
  if not isinstance(report_data, list) or len(report_data) == 0:
539
+ return gr.update(value="", visible=True)
540
 
541
  # 检查第一个元素是否是字典
542
  if not isinstance(report_data[0], dict):
 
702
  def create_report_section():
703
  """创建报告部分组件"""
704
  # 创建一个用于显示报告列表的组件,初始显示公司列表
705
+ # 先加载默认公司的报告数据
 
706
  initial_content = ""
707
+ try:
708
+ if my_companies and len(my_companies) > 0:
709
+ default_company = my_companies[0]['company_name']
710
+ initial_content_result = update_report_section(default_company, None, None)
711
+ # update_report_section 返回 gr.update() 字典,提取 value 字段
712
+ initial_content = initial_content_result.get('value', '') if isinstance(initial_content_result, dict) else ""
713
+ except:
714
+ initial_content = ""
715
 
716
  report_display = gr.HTML(initial_content)
717
  return report_display
 
859
  )
860
 
861
  # 创建公司按钮组件
862
+ # # company_buttons = create_company_buttons()
863
 
864
+ # # 为每个公司按钮绑定点击事件
865
+ # def make_click_handler(company_name):
866
+ # def handler():
867
+ # result = handle_company_click(company_name)
868
+ # # 如果添加成功,刷新Select Company列表并默认选中刚添加的公司
869
+ # if result is True:
870
+ # # 正确地刷新通过create_company_list()创建的Radio组件
871
+ # try:
872
+ # # companies_data = get_companys()
873
+ # companies_data = my_companies
874
+ # if isinstance(companies_data, list) and len(companies_data) > 0:
875
+ # # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
876
+ # updated_choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
877
+ # elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
878
+ # updated_choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
879
+ # else:
880
+ # updated_choices = []
881
+ # except:
882
+ # updated_choices = []
883
+ # # 使用gr.update来正确更新Radio组件,并默认选中刚添加的公司
884
+ # # 同时触发change事件来加载数据
885
+ # return gr.update(choices=updated_choices, value=company_name)
886
+ # return None
887
+ # return handler
888
 
889
+ # for company_name, button in company_buttons.items():
890
+ # button.click(
891
+ # fn=make_click_handler(company_name),
892
+ # inputs=[],
893
+ # outputs=[company_list]
894
+ # )
895
 
896
  # 创建一个容器来容纳报告部分,初始时隐藏
897
  with gr.Group(elem_classes=["report-news-box"]) as report_section_group:
 
1015
  text-align: center;
1016
  '''
1017
 
1018
+ # ... existing code ...
1019
+ # 构建左侧卡���
1020
+ def build_stock_card():
1021
+ # 尝试加载默认公司的数据
1022
+ default_company = my_companies[0]['company_name'] if my_companies else "N/A"
1023
+ try:
1024
+ stock_code = get_stock_code_by_company_name(default_company)
1025
+ company_info = get_quote(stock_code.strip())
1026
+ company_info['company'] = default_company
1027
+ except:
1028
+ company_info = {}
1029
+
1030
+ try:
1031
+ if not company_info or not isinstance(company_info, dict):
1032
+ company_name = "N/A"
1033
+ symbol = "N/A"
1034
+ price = "N/A"
1035
+ change_html = '<span style="color:#888;">N/A</span>'
1036
+ open_val = high_val = low_val = prev_close_val = volume_display = "N/A"
1037
+ else:
1038
+ company_name = company_info.get("company", "N/A")
1039
+ symbol = company_info.get("symbol", "N/A")
1040
+ price = company_info.get("current_price", "N/A")
1041
+
1042
+ # 解析 change
1043
+ change_str = company_info.get("change", "0")
1044
+ try:
1045
+ change = float(change_str)
1046
+ except (ValueError, TypeError):
1047
+ change = 0.0
1048
+
1049
+ # 解析 change_percent
1050
+ change_percent = company_info.get("percent_change", "0%")
1051
+
1052
+ change_color = "green" if change >= 0 else "red"
1053
+ sign = "+" if change >= 0 else ""
1054
+ change_html = f'<span style="color:{change_color};">{sign}{change:.2f} ({change_percent:+.2f}%)</span>'
1055
+
1056
+ # 其他价格字段
1057
+ open_val = company_info.get("open", "N/A")
1058
+ high_val = company_info.get("high", "N/A")
1059
+ low_val = company_info.get("low", "N/A")
1060
+ prev_close_val = company_info.get("previous_close", "N/A")
1061
+
1062
+ html = f"""
1063
+ <div style="width: 250px; height: 300px !important; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1064
+ <div style="font-size: 16px; color: #555; font-weight: 500;">{company_name}</div>
1065
+ <div style="font-size: 12px; color: #888;">NYSE:{symbol}</div>
1066
+ <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1067
+ <div style="font-size: 32px; font-weight: bold;">{price}</div>
1068
+ <div style="font-size: 14px;">{change_html}</div>
1069
+ </div>
1070
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1071
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{open_val}</div>
1072
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{high_val}</div>
1073
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{low_val}</div>
1074
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{prev_close_val}</div>
1075
+ </div>
1076
+ </div>
1077
+ """
1078
 
1079
+ return html
1080
+
1081
+ except Exception as e:
1082
+ print(f"Error building stock card: {e}")
1083
+ return '<div style="width:250px; padding:16px; color:red;">Error loading stock data</div>'
1084
+ # 模拟数据 - 第一次打开页面时的默认值
1085
  financial_metrics = [
1086
  {"label": "Total Revenue", "value": "N/A", "change": "N/A", "color": "grey"},
1087
  {"label": "Net Income", "value": "N/A", "change": "N/A", "color": "grey"},
 
1089
  {"label": "Operating Expenses", "value": "N/A", "change": "N/A", "color": "grey"},
1090
  {"label": "Cash Flow", "value": "N/A", "change": "N/A", "color": "grey"}
1091
  ]
 
 
 
 
 
 
 
 
1092
  income_statement = {
1093
+ "list_data": [
1094
+ ["Category", "N/A/FY", "N/A/FY", "N/A/FY"],
1095
+ ["Total", "N/A", "N/A", "N/A"],
1096
+ ["Net Income", "N/A", "N/A", "N/A.4M"],
1097
+ ["Earnings Per Share", "N/A", "N/A", "N/A"],
1098
+ ["Operating Expenses", "N/A", "N/A", "N/A"],
1099
+ ["Cash Flow", "N/A", "N/A", "N/A"]
1100
+ ],
1101
+ "yoy_rates": []
1102
+ }
 
 
 
 
 
 
 
 
1103
  yearly_data = 'N/A'
1104
  # 增长变化的 HTML 字符(箭头+百分比)
1105
  def render_change(change: str, color: str):
 
1107
  return f'<span style="color:{color};">▲{change}</span>'
1108
  else:
1109
  return f'<span style="color:{color};">▼{change}</span>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1110
  # 构建中间卡片
1111
  def build_financial_metrics():
1112
+ # 尝试加载默认公司的财务指标数据
1113
+ default_company = my_companies[0]['company_name'] if my_companies else "N/A"
1114
+ try:
1115
+ stock_code = get_stock_code_by_company_name(default_company)
1116
+ financial_metrics_pre = query_financial_data(stock_code, "5-Year")
1117
+ result = process_financial_data_with_metadata(financial_metrics_pre)
1118
+ default_financial_metrics = result["financial_metrics"]
1119
+ default_yearly_data = result["year_data"]
1120
+ except:
1121
+ default_financial_metrics = financial_metrics
1122
+ default_yearly_data = yearly_data
1123
+
1124
  metrics_html = ""
1125
+ for item in default_financial_metrics:
1126
  change_html = render_change(item["change"], item["color"])
1127
  metrics_html += f"""
1128
  <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
 
1138
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1139
  <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1140
  </svg>
1141
+ <span style="margin-left: 10px;">{default_yearly_data} Financial Metrics</span>
1142
  </div>
1143
  <div style="font-size: 16px; color: #8f8f8f;">
1144
  YTD data
 
1152
 
1153
  # 主函数:返回所有 HTML 片段
1154
  def get_dashboard():
1155
+ # 尝试加载默认公司的收入表数据
1156
+ default_company = my_companies[0]['company_name'] if my_companies else "N/A"
1157
+ try:
1158
+ stock_code = get_stock_code_by_company_name(default_company)
1159
+ financial_metrics_pre = query_financial_data(stock_code, "5-Year")
1160
+ result = process_financial_data_with_metadata(financial_metrics_pre)
1161
+ default_three_year_data = result["three_year_data"]
1162
+ default_table_data = build_table_format(default_three_year_data)
1163
+ except:
1164
+ default_table_data = income_statement
1165
+
1166
  with gr.Row():
1167
  with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]):
1168
  stock_card_html = gr.HTML(build_stock_card(), elem_classes=["metric-card-left"])
1169
  with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]):
1170
  financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"])
1171
  with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]):
1172
+ # 传递default_table_data参数
1173
+ income_table_html = gr.HTML(build_income_table(default_table_data), elem_classes=["metric-card-right"])
1174
  return stock_card_html, financial_metrics_html, income_table_html
1175
 
1176
  # 创建指标仪表板并保存引用
 
1183
  # 更新指标仪表板的函数
1184
  def update_metrics_dashboard(company_name):
1185
  """根据选择的公司更新指标仪表板"""
 
 
 
 
 
 
 
 
 
 
 
 
 
1186
  company_info = {}
1187
  # 尝试获取股票价格数据,但不中断程序执行
1188
  stock_code = ""
1189
  try:
1190
  # 根据选择的公司获取股票代码
1191
  stock_code = get_stock_code_by_company_name(company_name)
 
 
 
 
 
1192
  company_info = get_quote(stock_code.strip())
1193
  company_info['company'] = company_name
1194
+ print(f"股票价格数据 {company_info}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1195
  except Exception as e:
1196
  print(f"获取股票价格数据失败: {e}")
1197
+
 
 
 
 
 
 
 
 
1198
  financial_metrics_pre = query_financial_data(stock_code, "5-Year")
 
 
 
 
 
 
 
 
 
 
1199
  financial_metrics = []
 
 
 
 
 
 
1200
  year_data = None
1201
  three_year_data = None
1202
  try:
 
1208
  year_data = result["year_data"]
1209
  three_year_data = result["three_year_data"]
1210
  print(f"格式化后的财务数据: {financial_metrics}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1211
  except Exception as e:
1212
  print(f"Error process_financial_data: {e}")
1213
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
  yearly_data = year_data
1215
  table_data = build_table_format(three_year_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1216
 
1217
  # 增长变化的 HTML 字符(箭头+百分比)
1218
  def render_change(change: str, color: str):
 
1313
  </div>
1314
  """
1315
  return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1316
  # 返回三个HTML组件的内容
1317
  return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data)
1318
 
 
1322
  print(f"company_name: {company_name}")
1323
  # content = get_invest_suggest(company_name)
1324
  gr.Markdown("# 11111", elem_classes=["invest-suggest-md-box"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1325
 
1326
  elif tab_name == "detailed":
1327
  with gr.Column(elem_classes=["tab-content"]):
 
1487
  # combined_css = load_css_files(CSS_DIR, css_files)
1488
  # print(combined_css)
1489
 
1490
+ # 获取默认选中的公司(第一个)
1491
+ default_company = my_companies[0]['company_name'] if my_companies else ""
1492
+
1493
  with gr.Blocks(
1494
  title="Financial Analysis Dashboard",
1495
  css_paths=css_paths,
 
1499
 
1500
  # 添加处理公司点击事件的路由
1501
  # 创建一个状态组件来跟踪选中的公司
1502
+ selected_company_state = gr.State(default_company)
1503
 
1504
  with gr.Column(elem_classes=["container", "container-h"]):
1505
  # 头部
 
1557
 
1558
  # 获取投资建议数据
1559
  try:
1560
+ # content = get_invest_suggest(company)
1561
+ stock_code = get_stock_code_by_company_name(company)
1562
+ yield query_company_advanced(stock_code, "suggestion")
1563
+ # yield content
1564
  except Exception as e:
1565
  error_html = f'''
1566
  <div style="padding: 20px; text-align: center; color: #666;">
 
1653
  additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
1654
  )
1655
 
1656
+ # 在页面加载时设置默认选中的公司并加载数据
1657
+ def load_default_company():
1658
+ # 获取公司列表选项
1659
+ try:
1660
+ companies_data = my_companies
1661
+ if isinstance(companies_data, list) and len(companies_data) > 0:
1662
+ choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
1663
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
1664
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
1665
+ else:
1666
+ choices = []
1667
+ except:
1668
+ choices = []
1669
+
1670
+ if default_company:
1671
+ return default_company, gr.update(choices=choices, value=default_company)
1672
+ return "", gr.update(choices=choices)
1673
+
1674
+ demo.load(
1675
+ fn=load_default_company,
1676
+ inputs=[],
1677
+ outputs=[selected_company_state, company_list_component],
1678
+ concurrency_limit=None,
1679
+ )
1680
 
1681
  # 绑定公司选择事件到状态更新
1682
  # 注意:这里需要确保create_sidebar中没有重复绑定相同的事件
service/report_service.py CHANGED
@@ -288,7 +288,7 @@ def query_company_advanced(company_input: str, type: str):
288
  You are an expert investment advisor who provides data-driven recommendations based on a company’s financials, news, and market data.
289
 
290
  Task:
291
- Analyze the following for ${company_input}:
292
  Financial metrics – revenue, profit, debt, cash flow, etc.
293
  Recent news – assess risks and opportunities.
294
  Stock data – price trend, volume, etc.
@@ -308,7 +308,7 @@ def query_company_advanced(company_input: str, type: str):
308
 
309
  """
310
  prompt_report = f"""
311
- Analyze the following for ${company_input}:
312
  3 Years Financial metrics – revenue, profit, debt, cash flow, etc.
313
  Recent news – assess risks and opportunities.
314
  Stock data – price trend, volume, etc.
@@ -321,7 +321,7 @@ def query_company_advanced(company_input: str, type: str):
321
  Be objective, concise, and professional—avoid speculation or unsupported claims.
322
  Output Format (Markdown Only):
323
  Markdown
324
- # Financial Analysis Report: ${company_input}
325
  ## Executive Summary
326
  - Analyze the company’s current financial health using **Total Revenue**, **Net Income**, **Gross Profit Margin**, and **Current Ratio**.
327
  - Include narrative + chart descriptions for **Revenue Performance** and **Earnings Growth** (e.g., YoY/QoQ trends).
 
288
  You are an expert investment advisor who provides data-driven recommendations based on a company’s financials, news, and market data.
289
 
290
  Task:
291
+ Analyze the following for {company_input}:
292
  Financial metrics – revenue, profit, debt, cash flow, etc.
293
  Recent news – assess risks and opportunities.
294
  Stock data – price trend, volume, etc.
 
308
 
309
  """
310
  prompt_report = f"""
311
+ Analyze the following for {company_input}:
312
  3 Years Financial metrics – revenue, profit, debt, cash flow, etc.
313
  Recent news – assess risks and opportunities.
314
  Stock data – price trend, volume, etc.
 
321
  Be objective, concise, and professional—avoid speculation or unsupported claims.
322
  Output Format (Markdown Only):
323
  Markdown
324
+ # Financial Analysis Report: {company_input}
325
  ## Executive Summary
326
  - Analyze the company’s current financial health using **Total Revenue**, **Net Income**, **Gross Profit Margin**, and **Current Ratio**.
327
  - Include narrative + chart descriptions for **Revenue Performance** and **Earnings Growth** (e.g., YoY/QoQ trends).