JC321 commited on
Commit
781b9d3
·
1 Parent(s): e91674b
Files changed (1) hide show
  1. app.py +1570 -0
app.py ADDED
@@ -0,0 +1,1570 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import datetime
4
+ import re
5
+ import pandas as pd
6
+ import plotly.graph_objects as go
7
+ import plotly.express as px
8
+ import globals as g
9
+ from service.mysql_service import get_companys, insert_company, get_company_by_name
10
+ from service.chat_service import get_analysis_report, search_company, search_news, get_invest_suggest, chat_bot
11
+ from service.company import check_company_exists
12
+ from service.hf_upload import get_hf_files_with_links
13
+ from service.tool_processor import get_stock_price
14
+
15
+ custom_css = """
16
+ /* 匹配所有以 gradio-container- 开头的类 */
17
+ div[class^="gradio-container-"],
18
+ div[class*=" gradio-container-"] {
19
+ -webkit-text-size-adjust: 100% !important;
20
+ line-height: 1.5 !important;
21
+ font-family: unset !important;
22
+ -moz-tab-size: 4 !important;
23
+ tab-size: 4 !important;
24
+ }
25
+
26
+ .company-list-container {
27
+ background-color: white;
28
+ border-radius: 0.5rem;
29
+ padding: 0.75rem;
30
+ margin-bottom: 0.75rem;
31
+ border: 1px solid #e5e7eb;
32
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
33
+ width: 100%;
34
+ }
35
+
36
+ /* 隐藏单选框 */
37
+ .company-list-container input[type="radio"] {
38
+ display: none;
39
+ }
40
+
41
+ /* 自定义选项样式 */
42
+ .company-list-container label {
43
+ display: block;
44
+ padding: 0.75rem 1rem;
45
+ margin: 0.25rem 0;
46
+ border-radius: 0.375rem;
47
+ cursor: pointer;
48
+ transition: all 0.2s ease;
49
+ background-color: #f9fafb;
50
+ border: 1px solid #e5e7eb;
51
+ font-size: 1rem;
52
+ text-align: left;
53
+ width: 100%;
54
+ box-sizing: border-box;
55
+ }
56
+
57
+ /* 悬停效果 */
58
+ .company-list-container label:hover {
59
+ background-color: #f3f4f6;
60
+ border-color: #d1d5db;
61
+ }
62
+
63
+ /* 选中效果 - 确保背景色充满整个选项 */
64
+ .company-list-container input[type="radio"]:checked + span {
65
+ # background: #3b82f6 !important;
66
+ color: white !important;
67
+ font-weight: 600 !important;
68
+ display: block;
69
+ width: 100%;
70
+ height: 100%;
71
+ padding: 0.75rem 1rem;
72
+ margin: -0.75rem -1rem;
73
+ border-radius: 0.375rem;
74
+ }
75
+
76
+ .company-list-container span {
77
+ display: block;
78
+ padding: 0;
79
+ border-radius: 0.375rem;
80
+ width: 100%;
81
+ }
82
+
83
+ /* 确保每行只有一个选项 */
84
+ .company-list-container .wrap {
85
+ display: block !important;
86
+ }
87
+
88
+ .company-list-container .wrap li {
89
+ display: block !important;
90
+ width: 100% !important;
91
+ }
92
+ label.selected {
93
+ background: #3b82f6 !important;
94
+ color: white !important;
95
+ }
96
+ """
97
+
98
+ # 全局变量用于存储公司映射关系
99
+ companies_map = {}
100
+
101
+ # 根据公司名称获取股票代码的函数
102
+ def get_stock_code_by_company_name(company_name):
103
+ """根据公司名称获取股票代码"""
104
+ if company_name in companies_map and "CODE" in companies_map[company_name]:
105
+ return companies_map[company_name]["CODE"]
106
+ return "" # 默认返回
107
+
108
+ # 创建一个简单的函数来获取公司列表
109
+ def get_company_list_choices():
110
+ try:
111
+ companies_data = get_companys()
112
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
113
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
114
+ else:
115
+ choices = []
116
+ except:
117
+ choices = []
118
+ return gr.update(choices=choices)
119
+
120
+ # Sidebar service functions
121
+
122
+ # 处理公司点击事件的函数
123
+ def handle_company_click(company_name):
124
+ """处理公司点击事件,先判断是否已经入库,如果没有则进行入库操作,然后刷新公司列表"""
125
+ print(f"Handling click for company: {company_name}")
126
+
127
+ # 1. 判断是否已经入库
128
+ if not check_company_exists(company_name):
129
+ # 2. 如果没有入库,则进行入库操作
130
+ # 获取股票代码(如果有的话)
131
+ stock_code = companies_map.get(company_name, {}).get("CODE", "Unknown")
132
+ print(f"Inserting company {company_name} with code {stock_code}")
133
+
134
+ # 插入公司到数据库
135
+ success = insert_company(company_name, stock_code)
136
+ if success:
137
+ print(f"Successfully inserted company: {company_name}")
138
+ # 直接更新companies_map,而不是重新加载整个映射
139
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
140
+ # 使用Gradio的成功提示
141
+ gr.Info(f"Successfully added company: {company_name}")
142
+ # 返回True表示添加成功,需要刷新列表
143
+ return True
144
+ else:
145
+ print(f"Failed to insert company: {company_name}")
146
+ # 使用Gradio的错误提示
147
+ gr.Error(f"Failed to insert company: {company_name}")
148
+ return False
149
+ else:
150
+ print(f"Company {company_name} already exists in database")
151
+ # 使用Gradio的警告提示
152
+ gr.Warning(f"Company '{company_name}' already exists")
153
+
154
+ # 3. 返回成功响应
155
+ return None
156
+
157
+ def get_company_list_html(selected_company=""):
158
+ try:
159
+ # 从数据库获取所有公司
160
+ companies_data = get_companys()
161
+ # 检查是否为错误信息
162
+ if isinstance(companies_data, str):
163
+ if "查询执行失败" in companies_data:
164
+ return "<div class='text-red-500'>获取公司列表失败</div>"
165
+ else:
166
+ # 如果是字符串但不是错误信息,可能需要特殊处理
167
+ return ""
168
+
169
+ # 检查是否为DataFrame且为空
170
+ if not isinstance(companies_data, pd.DataFrame) or companies_data.empty:
171
+ return ""
172
+
173
+ # 生成HTML列表
174
+ html_items = []
175
+ for _, row in companies_data.iterrows():
176
+ company_name = row.get('company_name', 'Unknown')
177
+ # 根据是否选中添加不同的样式类
178
+ css_class = "company-item"
179
+ if company_name == selected_company:
180
+ css_class += " selected-company"
181
+ # 使用button元素来确保可点击性
182
+ 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>')
183
+
184
+ return "\n".join(html_items)
185
+ except Exception as e:
186
+ return f"<div class='text-red-500'>生成公司列表失败: {str(e)}</div>"
187
+
188
+ def initialize_company_list(selected_company=""):
189
+ return get_company_list_html(selected_company)
190
+
191
+ def refresh_company_list(selected_company=""):
192
+ """刷新公司列表,返回最新的HTML内容,带loading效果"""
193
+ # 先返回loading状态
194
+ loading_html = '''
195
+ <div style="display: flex; justify-content: center; align-items: center; height: 100px;">
196
+ <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>
197
+ <style>
198
+ @keyframes spin {
199
+ 0% { transform: rotate(0deg); }
200
+ 100% { transform: rotate(360deg); }
201
+ }
202
+ </style>
203
+ </div>
204
+ '''
205
+ yield loading_html
206
+
207
+ # 然后返回实际的数据
208
+ yield get_company_list_html(selected_company)
209
+
210
+ # 新增函数:处理公司选择事件
211
+ def select_company(company_name):
212
+ """处理公司选择事件,更新全局状态并返回更新后的公司列表"""
213
+ # 更新全局变量
214
+ g.SELECT_COMPANY = company_name if company_name else ""
215
+ # 对于Radio组件,我们只需要返回更新后的选项列表
216
+ try:
217
+ companies_data = get_companys()
218
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
219
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
220
+ else:
221
+ choices = []
222
+ except:
223
+ choices = []
224
+ return gr.update(choices=choices, value=company_name)
225
+
226
+ def initialize_companies_map():
227
+ """初始化 companies_map 字典"""
228
+ global companies_map
229
+ companies_map = {} # 清空之前的映射
230
+
231
+ print("Initializing companies map...")
232
+
233
+ try:
234
+ # 获取预定义的公司列表
235
+ predefined_companies = [
236
+ { "NAME": "Alibaba", "CODE": "BABA" },
237
+ { "NAME": "阿里巴巴-W", "CODE": "09988" },
238
+ { "NAME": "NVIDIA", "CODE": "NVDA" },
239
+ { "NAME": "Amazon", "CODE": "AMZN" },
240
+ { "NAME": "Intel", "CODE": "INTC" },
241
+ { "NAME": "Meta", "CODE": "META" },
242
+ { "NAME": "Google", "CODE": "GOOGL" },
243
+ { "NAME": "Apple", "CODE": "AAPL" },
244
+ { "NAME": "Tesla", "CODE": "TSLA" },
245
+ { "NAME": "AMD", "CODE": "AMD" },
246
+ { "NAME": "Microsoft", "CODE": "MSFT" },
247
+ { "NAME": "ASML", "CODE": "ASML" }
248
+ ]
249
+
250
+ # 将预定义公司添加到映射中
251
+ for company in predefined_companies:
252
+ companies_map[company["NAME"]] = {"NAME": company["NAME"], "CODE": company["CODE"]}
253
+
254
+ print(f"Predefined companies added: {len(predefined_companies)}")
255
+
256
+ # 从数据库获取公司数据
257
+ companies_data = get_companys()
258
+ print(f"Companies data from DB: {companies_data}")
259
+
260
+ # 如果数据库中有公司数据,则添加到映射中(去重)
261
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
262
+ print(f"Adding {len(companies_data)} companies from database")
263
+ for _, row in companies_data.iterrows():
264
+ company_name = row.get('company_name', 'Unknown')
265
+ stock_code = row.get('stock_code', '')
266
+
267
+ # 确保company_name和stock_code都是字符串类型
268
+ company_name = str(company_name) if company_name is not None else 'Unknown'
269
+ stock_code = str(stock_code) if stock_code is not None else ''
270
+
271
+ # 检查是否已存在于映射中(通过股票代码判断)
272
+ is_duplicate = False
273
+ for existing_company in companies_map.values():
274
+ if existing_company["CODE"] == stock_code:
275
+ is_duplicate = True
276
+ break
277
+
278
+ # 如果不重复,则添加到映射中
279
+ if not is_duplicate:
280
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
281
+ # print(f"Added company: {company_name}")
282
+ else:
283
+ print("No companies found in database")
284
+
285
+ print(f"Final companies map: {companies_map}")
286
+ except Exception as e:
287
+ # 错误处理
288
+ print(f"Error initializing companies map: {str(e)}")
289
+ pass
290
+
291
+ # Sidebar company selector functions
292
+ def update_company_choices(user_input: str):
293
+ """更新公司选择列表"""
294
+ # 第一次 yield:立即显示 modal + loading 提示
295
+ yield gr.update(
296
+ choices=["Searching..."],
297
+ visible=True
298
+ ), gr.update(visible=False, value="") # 添加第二个返回值
299
+
300
+ # 第二次:执行耗时操作(调用 LLM)
301
+ choices = search_company(user_input) # 这是你原来的同步函数
302
+
303
+ # 检查choices是否为错误信息
304
+ if len(choices) > 0 and isinstance(choices[0], str) and not choices[0].startswith("Searching"):
305
+ # 如果是错误信息或非正常格式,显示提示消息
306
+ error_message = choices[0] if len(choices) > 0 else "未知错误"
307
+ # 使用Ant Design风格的错误提示
308
+ error_html = f'''
309
+ <div class="ant-message ant-message-error" style="
310
+ position: fixed;
311
+ top: 20px;
312
+ left: 50%;
313
+ transform: translateX(-50%);
314
+ z-index: 10000;
315
+ padding: 10px 16px;
316
+ border-radius: 4px;
317
+ background: #fff;
318
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
319
+ display: flex;
320
+ align-items: center;
321
+ pointer-events: all;
322
+ animation: messageFadeIn 0.3s ease-in-out;
323
+ ">
324
+ <div style="
325
+ width: 16px;
326
+ height: 16px;
327
+ background: #ff4d4f;
328
+ border-radius: 50%;
329
+ position: relative;
330
+ margin-right: 8px;
331
+ "></div>
332
+ <span>{error_message}</span>
333
+ </div>
334
+ <script>
335
+ setTimeout(function() {{
336
+ var msg = document.querySelector('.ant-message-error');
337
+ if (msg) {{
338
+ msg.style.animation = 'messageFadeOut 0.3s ease-in-out';
339
+ setTimeout(function() {{ msg.remove(); }}, 3000);
340
+ }}
341
+ }}, 3000);
342
+ </script>
343
+ '''
344
+ yield gr.update(choices=["No results found"], visible=True), gr.update(visible=True, value=error_html)
345
+ else:
346
+ # 第三次:更新为真实结果
347
+ yield gr.update(
348
+ choices=choices,
349
+ visible=len(choices) > 0
350
+ ), gr.update(visible=False, value="")
351
+
352
+ def add_company(selected, current_list):
353
+ """添加选中的公司"""
354
+ if selected == "No results found":
355
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
356
+ if selected:
357
+ # print(f"Selected company====: {selected}")
358
+ # 从选择的文本中提取公司名称和股票代码
359
+ # 假设格式为 "公司名称 (股票代码)"
360
+ selected_clean = selected.strip()
361
+ match = re.match(r"^(.+?)\s*\(([^)]+)\)$", selected_clean)
362
+ if match:
363
+ company_name = match.group(1)
364
+ stock_code = match.group(2)
365
+ elif companies_map.get(selected_clean):
366
+ company_name = selected_clean
367
+ stock_code = companies_map[selected_clean]["CODE"]
368
+ else:
369
+ company_name = selected_clean
370
+ stock_code = "Unknown"
371
+
372
+ # print(f"Company name: {company_name}, Stock code: {stock_code}")
373
+ # print(f"Company exists: {check_company_exists(company_name)}")
374
+
375
+ if not check_company_exists(company_name):
376
+ # 入库
377
+ success = insert_company(company_name, stock_code)
378
+ if success:
379
+ # 从数据库获取更新后的公司列表
380
+ try:
381
+ companies_data = get_companys()
382
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
383
+ updated_list = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
384
+ else:
385
+ updated_list = []
386
+ except:
387
+ updated_list = []
388
+
389
+ # 添加默认公司选项
390
+ if not updated_list:
391
+ updated_list = ['Alibaba', '腾讯控股', 'Tencent', '阿里巴巴-W', 'Apple']
392
+
393
+ # 成功插入后清除状态消息,并更新Radio组件的选项,同时默认选中刚添加的公司
394
+ # 通过设置value参数,会自动触发change事件来加载数据
395
+ return gr.update(visible=False), gr.update(choices=updated_list, value=company_name), gr.update(visible=False, value="")
396
+ else:
397
+ # 插入失败显示错误消息,使用Gradio内置的错误提示
398
+ gr.Error("插入公司失败")
399
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
400
+ else:
401
+ # 公司已存在,使用Gradio内置的警告消息
402
+ gr.Warning(f"公司 '{company_name}' 已存在")
403
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
404
+
405
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
406
+
407
+ # Sidebar report section functions
408
+ # 创建一个全局变量来存储公司按钮组件
409
+ company_buttons = {}
410
+
411
+ def create_company_buttons():
412
+ """创建公司按钮组件"""
413
+ # 确保companies_map已被初始化
414
+ if not companies_map:
415
+ initialize_companies_map()
416
+
417
+ # 显示companies_map中的公司列表
418
+ companies = list(companies_map.keys())
419
+
420
+ # 添加调试信息
421
+ print(f"Companies in map: {companies}")
422
+
423
+ # 清空之前的按钮
424
+ company_buttons.clear()
425
+
426
+ if not companies:
427
+ # 如果没有公司,返回一个空的列
428
+ with gr.Column():
429
+ gr.Markdown("暂无公司数据")
430
+ else:
431
+ # 使用Gradio按钮组件创建公司列表
432
+ with gr.Column(elem_classes=["home-company-list"]):
433
+ # 按每行两个公司进行分组
434
+ for i in range(0, len(companies), 2):
435
+ # 检查是否是最后一行且只有一个元素
436
+ if i + 1 < len(companies):
437
+ # 有两个元素
438
+ with gr.Row(elem_classes=["home-company-item-box"]):
439
+ btn1 = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"])
440
+ btn2 = gr.Button(companies[i + 1], elem_classes=["home-company-item", "gradio-button"])
441
+ # 保存按钮引用
442
+ company_buttons[companies[i]] = btn1
443
+ company_buttons[companies[i + 1]] = btn2
444
+ else:
445
+ # 只有一个元素
446
+ with gr.Row(elem_classes=["home-company-item-box", "single-item"]):
447
+ btn = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"])
448
+ # 保存按钮引用
449
+ company_buttons[companies[i]] = btn
450
+
451
+ # 返回按钮字典
452
+ return company_buttons
453
+
454
+ def update_report_section(selected_company):
455
+ """根据选中的公司更新报告部分"""
456
+ if selected_company == "" or selected_company is None:
457
+ # 没有选中的公司,显示公司列表
458
+ # html_content = get_initial_company_list_content()
459
+ # 暂时返回空内容,稍后会用Gradio组件替换
460
+ html_content = ""
461
+ return gr.update(value=html_content, visible=True)
462
+ else:
463
+ # 有选中的公司,显示相关报告
464
+ try:
465
+ # 尝试从Hugging Face获取文件列表
466
+ report_data = get_hf_files_with_links("JC321/files-world")
467
+ except Exception as e:
468
+ # 如果获取失败,使用模拟数据并显示错误消息
469
+ print(f"获取Hugging Face文件列表失败: {str(e)}")
470
+ report_data = []
471
+
472
+ html_content = '<div class="report-list-box bg-white">'
473
+ 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>'
474
+ for report in report_data:
475
+ html_content += f'''
476
+ <div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{report['link']}', '_blank')">
477
+ <div class="report-item-content">
478
+ <span class="text-gray-800">{report['title']}</span>
479
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor">
480
+ <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" />
481
+ </svg>
482
+ </div>
483
+ </div>
484
+ '''
485
+
486
+ html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>'
487
+ html_content += '</div>'
488
+
489
+ try:
490
+ # report_data = [
491
+ # {
492
+ # "title": "Alibaba Reports Q2 FY2026 Revenue of RMB 247.8 Billion, Up 15% YoY",
493
+ # "url": "https://www.alibabagroup.com/en/news/article?news=p251125",
494
+ # "publishedAt": "2025-11-25T08:00:00Z"
495
+ # },
496
+ # {
497
+ # "title": "Alibaba Cloud Revenue Surges 34% Amid Strong AI Demand",
498
+ # "url": "https://www.reuters.com/technology/alibaba-cloud-revenue-jumps-34-percent-ai-push-2025-11-25/",
499
+ # "publishedAt": "2025-11-25T09:30:00Z"
500
+ # }
501
+ # ]
502
+ report_data = search_news(selected_company)
503
+ news_html = "<div class='news-list-box bg-white'>"
504
+ 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>'
505
+ from datetime import datetime
506
+
507
+ for news in report_data:
508
+ published_at = news['publishedAt']
509
+
510
+ # 解析 ISO 8601 时间字符串(注意:strptime 不直接支持 'Z',需替换或使用 fromisoformat)
511
+ dt = datetime.fromisoformat(published_at.replace("Z", "+00:00"))
512
+
513
+ # 格式化为 YYYY.MM.DD
514
+ formatted_date = dt.strftime("%Y.%m.%d")
515
+ news_html += f'''
516
+ <div class="news-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{news['url']}', '_blank')">
517
+ <div class="news-item-content">
518
+ <span class="text-xs text-gray-500">[{formatted_date}]</span>
519
+ <span class="text-gray-800">{news['title']}</span>
520
+ </div>
521
+ </div>
522
+ '''
523
+ news_html += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}条新闻</span></div>'
524
+ news_html += '</div>'
525
+ html_content += news_html
526
+ except Exception as e:
527
+ print(f"Error updating report section: {str(e)}")
528
+
529
+ return gr.update(value=html_content, visible=True)
530
+
531
+ # Component creation functions
532
+ def create_header():
533
+ """创建头部组件"""
534
+ # 获取当前时间
535
+ current_time = datetime.datetime.now().strftime("%B %d, %Y - Market Data Updated Today")
536
+
537
+ with gr.Row(elem_classes=["header"]):
538
+ # 左侧:图标和标题
539
+ with gr.Column(scale=8):
540
+ # 使用圆柱体SVG图标表示数据库
541
+ gr.HTML('''
542
+ <div class="top-logo-box">
543
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 48 48">
544
+ <g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4">
545
+ <path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11"></path>
546
+ <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>
547
+ <ellipse cx="24" cy="10" rx="20" ry="6"></ellipse>
548
+ </g>
549
+ </svg>
550
+ <span class="logo-title">Easy Financial Report Dashboard</span>
551
+ </div>
552
+ ''', elem_classes=["text-2xl"])
553
+
554
+ # 右侧:时间信息
555
+ with gr.Column(scale=2):
556
+ gr.Markdown(current_time, elem_classes=["text-sm-top-time"])
557
+
558
+ def create_company_list():
559
+ """创建公司列表组件"""
560
+ # 获取公司列表数据
561
+ try:
562
+ companies_data = get_companys()
563
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
564
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
565
+ else:
566
+ choices = []
567
+ except:
568
+ choices = []
569
+
570
+ # 添加默认公司选项
571
+ if not choices:
572
+ choices = []
573
+
574
+ # 使用Radio组件显示公司列表,不显示标签
575
+ company_list = gr.Radio(
576
+ choices=choices,
577
+ label="",
578
+ interactive=True,
579
+ elem_classes=["company-list-container"],
580
+ container=False, # 不显示外部容器边框
581
+ visible=True
582
+ )
583
+
584
+ return company_list
585
+
586
+ def create_company_selector():
587
+ """创建公司选择器组件"""
588
+ company_input = gr.Textbox(
589
+ show_label=False,
590
+ placeholder="Add Company",
591
+ elem_classes=["company-input-box"],
592
+ lines=1,
593
+ max_lines=1,
594
+ # container=False
595
+ )
596
+
597
+ # 状态消息显示区域
598
+ status_message = gr.HTML(
599
+ "",
600
+ elem_classes=["status-message"],
601
+ visible=False
602
+ )
603
+
604
+ # 弹窗选择列表
605
+ company_modal = gr.Radio(
606
+ show_label=False,
607
+ choices=[],
608
+ visible=False,
609
+ elem_classes=["company-modal"]
610
+ )
611
+
612
+ return company_input, status_message, company_modal
613
+
614
+ def create_report_section():
615
+ """创建报告部分组件"""
616
+ # 创建一个用于显示报告列表的组件,初���显示公司列表
617
+ # initial_content = get_initial_company_list_content()
618
+ # 暂时返回空内容,稍后会用Gradio组件替换
619
+ initial_content = ""
620
+ # print(f"Initial content: {initial_content}") # 添加调试信息
621
+
622
+ report_display = gr.HTML(initial_content)
623
+ return report_display
624
+
625
+ def create_sidebar():
626
+ """创建侧边栏组件"""
627
+ # 初始化 companies_map
628
+ initialize_companies_map()
629
+
630
+ with gr.Column(elem_classes=["sidebar"]):
631
+ # 公司选择
632
+ with gr.Group(elem_classes=["card"]):
633
+ gr.Markdown("### Select Company", elem_classes=["card-title", "left-card-title"])
634
+ with gr.Column():
635
+ # 创建公司列表
636
+ company_list = create_company_list()
637
+
638
+ # 创建公司选择器
639
+ company_input, status_message, company_modal = create_company_selector()
640
+
641
+ # 绑定事件
642
+ company_input.submit(
643
+ fn=update_company_choices,
644
+ inputs=[company_input],
645
+ outputs=[company_modal, status_message]
646
+ )
647
+
648
+ company_modal.change(
649
+ fn=add_company,
650
+ inputs=[company_modal, company_list],
651
+ outputs=[company_modal, company_list, status_message]
652
+ )
653
+
654
+ # 创建公司按钮组件
655
+ company_buttons = create_company_buttons()
656
+
657
+ # 为每个公司按钮绑定点击事件
658
+ def make_click_handler(company_name):
659
+ def handler():
660
+ result = handle_company_click(company_name)
661
+ # 如果添加成功,刷新Select Company列表并默认选中刚添加的公司
662
+ if result is True:
663
+ # 正确地刷新通过create_company_list()创建的Radio组件
664
+ try:
665
+ companies_data = get_companys()
666
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
667
+ updated_choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
668
+ else:
669
+ updated_choices = []
670
+ except:
671
+ updated_choices = []
672
+ # 使用gr.update来正确更新Radio组件,并默认选中刚添加的公司
673
+ # 同时触发change事件来加载数据
674
+ return gr.update(choices=updated_choices, value=company_name)
675
+ return None
676
+ return handler
677
+
678
+ for company_name, button in company_buttons.items():
679
+ button.click(
680
+ fn=make_click_handler(company_name),
681
+ inputs=[],
682
+ outputs=[company_list]
683
+ )
684
+
685
+ # 创建一个容器来容纳报告部分,初始时隐藏
686
+ with gr.Group(elem_classes=["report-news-box"]) as report_section_group:
687
+ # gr.Markdown("### Financial Reports", elem_classes=["card-title", "left-card-title"])
688
+ report_display = create_report_section()
689
+
690
+ # 处理公司选择事件
691
+ def select_company_handler(company_name):
692
+ """处理公司选择事件的处理器"""
693
+ # 更新全局变量
694
+ g.SELECT_COMPANY = company_name if company_name else ""
695
+
696
+ # 更新报告部分的内容
697
+ updated_report_display = update_report_section(company_name)
698
+
699
+ # 根据是否选择了公司来决定显示/隐藏报告部分
700
+ if company_name:
701
+ # 有选中的公司,显示报告部分
702
+ return gr.update(visible=True), updated_report_display
703
+ else:
704
+ # 没有选中的公司,隐藏报告部分
705
+ return gr.update(visible=False), updated_report_display
706
+
707
+ company_list.change(
708
+ fn=select_company_handler,
709
+ inputs=[company_list],
710
+ outputs=[report_section_group, report_display]
711
+ )
712
+
713
+ # 返回公司列表组件和报告部分组件
714
+ return company_list, report_section_group, report_display
715
+
716
+ def create_metrics_dashboard():
717
+ """创建指标仪表板组件"""
718
+ with gr.Row(elem_classes=["metrics-dashboard"]):
719
+ card_custom_style = '''
720
+ background-color: white;
721
+ border-radius: 0.5rem;
722
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
723
+ padding: 1.25rem;
724
+ min-height: 250px !important;
725
+ text-align: center;
726
+ '''
727
+
728
+ # 模拟数据
729
+ company_info = {
730
+ "name": "N/A",
731
+ "symbol": "NYSE:N/A",
732
+ "price": 0,
733
+ "change": 0,
734
+ "change_percent": 0.41,
735
+ "open": 165.20,
736
+ "high": 166.37,
737
+ "low": 156.15,
738
+ "prev_close": 157.01,
739
+ "volume": "27.10M"
740
+ }
741
+
742
+ financial_metrics = [
743
+ {"label": "Total Revenue", "value": "$2.84B", "change": "+12.4%", "color": "green"},
744
+ {"label": "Net Income", "value": "$685M", "change": "-3.2%", "color": "red"},
745
+ {"label": "Earnings Per Share", "value": "$2.15", "change": "-3.2%", "color": "red"},
746
+ {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
747
+ {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
748
+ ]
749
+
750
+ income_statement = [
751
+ ["Category", "2024/FY", "2023/FY", "2022/FY"],
752
+ ["Total", "130350M", "126491M", "134567M"],
753
+ ["Net Income", "11081", "10598M", "9818.4M"],
754
+ ["Earnings Per Share", "4.38", "4.03", "3.62"],
755
+ ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
756
+ ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
757
+ ]
758
+
759
+ # 增长变化的 HTML 字符(箭头+百分比)
760
+ def render_change(change: str, color: str):
761
+ if change.startswith("+"):
762
+ return f'<span style="color:{color};">▲{change}</span>'
763
+ else:
764
+ return f'<span style="color:{color};">▼{change}</span>'
765
+
766
+ # 构建左侧卡片
767
+ def build_stock_card():
768
+ html = f"""
769
+ <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;">
770
+ <div style="font-size: 14px; color: #555;">N/A</div>
771
+ <div style="font-size: 12px; color: #888;">N/A</div>
772
+ <div style="font-size: 32px; font-weight: bold; margin: 8px 0;">N/A</div>
773
+ <div style="font-size: 14px; margin: 8px 0;">N/A</div>
774
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
775
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
776
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
777
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
778
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
779
+ <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
780
+ </div>
781
+ </div>
782
+ """
783
+ return html
784
+
785
+ # 构建中间卡片
786
+ def build_financial_metrics():
787
+ metrics_html = ""
788
+ for item in financial_metrics:
789
+ change_html = render_change(item["change"], item["color"])
790
+ metrics_html += f"""
791
+ <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
792
+ <div style="font-size: 14px; color: #555;">{item['label']}</div>
793
+ <div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div>
794
+ </div>
795
+ """
796
+
797
+ html = f"""
798
+ <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;">
799
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
800
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
801
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
802
+ </svg>
803
+ <div style="font-size: 18px; font-weight: 600;">2025 Q3 Financial Metrics</div>
804
+ </div>
805
+ {metrics_html}
806
+ </div>
807
+ """
808
+ return html
809
+
810
+ # 构建右侧表格
811
+ def build_income_table():
812
+ table_rows = ""
813
+ for i, row in enumerate(income_statement):
814
+ if i == 0:
815
+ row_style = "background-color: #f5f5f5; font-weight: 500;"
816
+ else:
817
+ row_style = "background-color: #f9f9f9;"
818
+ cells = ""
819
+ for j, cell in enumerate(row):
820
+ if j == 0:
821
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: left; font-size: 14px;'>{cell}</td>"
822
+ else:
823
+ # 添加增长箭头(模拟数据)
824
+ growth = None
825
+ if i == 1 and j == 1: growth = "+3.05%"
826
+ elif i == 1 and j == 2: growth = "-6.00%"
827
+ elif i == 2 and j == 1: growth = "+3.05%"
828
+ elif i == 2 and j == 2: growth = "-6.00%"
829
+ elif i == 3 and j == 1: growth = "+3.05%"
830
+ elif i == 3 and j == 2: growth = "-6.00%"
831
+ elif i == 4 and j == 1: growth = "+29.17%"
832
+ elif i == 4 and j == 2: growth = "+29.17%"
833
+ elif i == 5 and j == 1: growth = "-13.05%"
834
+ elif i == 5 and j == 2: growth = "+29.17%"
835
+
836
+ if growth:
837
+ arrow = "▲" if growth.startswith("+") else "▼"
838
+ color = "green" if growth.startswith("+") else "red"
839
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: right; font-size: 14px; position: relative;'><div>{cell}</div><div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div></td>"
840
+ else:
841
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: right; font-size: 14px;'>{cell}</td>"
842
+ table_rows += f"<tr style='{row_style}'>{cells}</tr>"
843
+
844
+ html = f"""
845
+ <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;">
846
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
847
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
848
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
849
+ </svg>
850
+ <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
851
+ </div>
852
+ <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
853
+ {table_rows}
854
+ </table>
855
+ </div>
856
+ """
857
+ return html
858
+ # 主函数:返回所有 HTML 片段
859
+ def get_dashboard():
860
+ with gr.Row():
861
+ with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]):
862
+ stock_card_html = gr.HTML(build_stock_card(), elem_classes=["metric-card-left"])
863
+ with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]):
864
+ financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"])
865
+ with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]):
866
+ income_table_html = gr.HTML(build_income_table(), elem_classes=["metric-card-right"])
867
+ return stock_card_html, financial_metrics_html, income_table_html
868
+
869
+ # 创建指标仪表板并保存引用
870
+ stock_card_component, financial_metrics_component, income_table_component = get_dashboard()
871
+
872
+ # 将组件引用保存到全局变量,以便在其他地方使用
873
+ global metrics_dashboard_components
874
+ metrics_dashboard_components = (stock_card_component, financial_metrics_component, income_table_component)
875
+
876
+ # 更新指标仪表板的函数
877
+ def update_metrics_dashboard(company_name):
878
+ """根据选择的公司更新指标仪表板"""
879
+ # 模拟数据
880
+ company_info = {
881
+ "name": company_name,
882
+ "symbol": "NYSE:BABA",
883
+ "price": 157.65,
884
+ "change": 0.64,
885
+ "change_percent": 0.41,
886
+ "open": 165.20,
887
+ "high": 166.37,
888
+ "low": 156.15,
889
+ "prev_close": 157.01,
890
+ "volume": "27.10M"
891
+ }
892
+
893
+ # 尝试获取股票价格数据,但不中断程序执行
894
+ try:
895
+ # 根据选择的公司获取股票代码
896
+ stock_code = get_stock_code_by_company_name(company_name)
897
+ company_info2 = get_stock_price(stock_code)
898
+ print(f"股票价格数据: {company_info2}")
899
+
900
+ # 如果成功获取数据,则用实际数据替换模拟数据
901
+ if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
902
+ import json
903
+ # 解析返回的JSON数据
904
+ data_text = company_info2["content"][0]["text"]
905
+ stock_data = json.loads(data_text)
906
+
907
+ # 提取数据
908
+ quote = stock_data["Global Quote"]
909
+
910
+ # 转换交易量单位
911
+ volume = int(quote['06. volume'])
912
+ if volume >= 1000000:
913
+ volume_str = f"{volume / 1000000:.2f}M"
914
+ elif volume >= 1000:
915
+ volume_str = f"{volume / 1000:.2f}K"
916
+ else:
917
+ volume_str = str(volume)
918
+
919
+ company_info = {
920
+ "name": company_name,
921
+ "symbol": f"NYSE:{quote['01. symbol']}",
922
+ "price": float(quote['05. price']),
923
+ "change": float(quote['09. change']),
924
+ "change_percent": float(quote['10. change percent'].rstrip('%')),
925
+ "open": float(quote['02. open']),
926
+ "high": float(quote['03. high']),
927
+ "low": float(quote['04. low']),
928
+ "prev_close": float(quote['08. previous close']),
929
+ "volume": volume_str
930
+ }
931
+ except Exception as e:
932
+ print(f"获取股票价格数据失败: {e}")
933
+ company_info2 = None
934
+
935
+ financial_metrics = [
936
+ {"label": "Total Revenue", "value": "$2.84B", "change": "+12.4%", "color": "green"},
937
+ {"label": "Net Income", "value": "$685M", "change": "-3.2%", "color": "red"},
938
+ {"label": "Earnings Per Share", "value": "$2.15", "change": "-3.2%", "color": "red"},
939
+ {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
940
+ {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
941
+ ]
942
+
943
+ income_statement = [
944
+ ["Category", "2024/FY", "2023/FY", "2022/FY"],
945
+ ["Total", "130350M", "126491M", "134567M"],
946
+ ["Net Income", "11081", "10598M", "9818.4M"],
947
+ ["Earnings Per Share", "4.38", "4.03", "3.62"],
948
+ ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
949
+ ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
950
+ ]
951
+
952
+ # 增长变化的 HTML 字符(箭头+百分比)
953
+ def render_change(change: str, color: str):
954
+ if change.startswith("+"):
955
+ return f'<span style="color:{color};">▲{change}</span>'
956
+ else:
957
+ return f'<span style="color:{color};">▼{change}</span>'
958
+
959
+ # 构建左侧卡片
960
+ def build_stock_card():
961
+ # 检查是否获取到了股票数据,如果没有则显示N/A
962
+ if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
963
+ price = company_info["price"]
964
+ change = company_info["change"]
965
+ change_percent = company_info["change_percent"]
966
+
967
+ # 格式化价格变动
968
+ change_color = "green" if change > 0 else "red"
969
+ change_html = f'<span style="color:{change_color};">+{change:.2f}({change_percent:+.2f}%)</span>' if change > 0 else \
970
+ f'<span style="color:{change_color};">{change:.2f}({change_percent:+.2f}%)</span>'
971
+ else:
972
+ # 如果没有获取到数据,所有数值显示N/A
973
+ price = "N/A"
974
+ change = 0
975
+ change_percent = 0
976
+ change_html = "<span style=\"color:#888;\">N/A</span>"
977
+
978
+ html = f"""
979
+ <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;">
980
+ <div style="font-size: 16px; color: #555;font-weight: 500;">{company_info['name']}</div>
981
+ <div style="font-size: 12px; color: #888;">{company_info['symbol'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'NYSE:N/A'}</div>
982
+ <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
983
+ <div style="font-size: 32px; font-weight: bold;">{price}</div>
984
+ <div style="font-size: 14px;">{change_html}</div>
985
+ </div>
986
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
987
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['open'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
988
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['high'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
989
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['low'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
990
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['prev_close'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
991
+ <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['volume'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
992
+ </div>
993
+ </div>
994
+ """
995
+ return html
996
+
997
+ # 构建中间卡片
998
+ def build_financial_metrics():
999
+ metrics_html = ""
1000
+ for item in financial_metrics:
1001
+ change_html = render_change(item["change"], item["color"])
1002
+ metrics_html += f"""
1003
+ <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
1004
+ <div style="font-size: 14px; color: #555;">{item['label']}</div>
1005
+ <div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div>
1006
+ </div>
1007
+ """
1008
+
1009
+ html = f"""
1010
+ <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;">
1011
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1012
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1013
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1014
+ </svg>
1015
+ <div style="font-size: 18px; font-weight: 600;">2025 Q3 Financial Metrics</div>
1016
+ </div>
1017
+ {metrics_html}
1018
+ </div>
1019
+ """
1020
+ return html
1021
+
1022
+ # 构建右侧表格
1023
+ def build_income_table():
1024
+ table_rows = ""
1025
+ for i, row in enumerate(income_statement):
1026
+ if i == 0:
1027
+ row_style = "background-color: #f5f5f5; font-weight: 500;"
1028
+ else:
1029
+ row_style = "background-color: #f9f9f9;"
1030
+ cells = ""
1031
+ for j, cell in enumerate(row):
1032
+ if j == 0:
1033
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: left; font-size: 14px;'>{cell}</td>"
1034
+ else:
1035
+ # 添加增长箭头(模拟数据)
1036
+ growth = None
1037
+ if i == 1 and j == 1: growth = "+3.05%"
1038
+ elif i == 1 and j == 2: growth = "-6.00%"
1039
+ elif i == 2 and j == 1: growth = "+3.05%"
1040
+ elif i == 2 and j == 2: growth = "-6.00%"
1041
+ elif i == 3 and j == 1: growth = "+3.05%"
1042
+ elif i == 3 and j == 2: growth = "-6.00%"
1043
+ elif i == 4 and j == 1: growth = "+29.17%"
1044
+ elif i == 4 and j == 2: growth = "+29.17%"
1045
+ elif i == 5 and j == 1: growth = "-13.05%"
1046
+ elif i == 5 and j == 2: growth = "+29.17%"
1047
+
1048
+ if growth:
1049
+ arrow = "▲" if growth.startswith("+") else "▼"
1050
+ color = "green" if growth.startswith("+") else "red"
1051
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: right; font-size: 14px; position: relative;'><div>{cell}</div><div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div></td>"
1052
+ else:
1053
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: right; font-size: 14px;'>{cell}</td>"
1054
+ table_rows += f"<tr style='{row_style}'>{cells}</tr>"
1055
+
1056
+ html = f"""
1057
+ <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;">
1058
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1059
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1060
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1061
+ </svg>
1062
+ <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
1063
+ </div>
1064
+ <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
1065
+ {table_rows}
1066
+ </table>
1067
+ </div>
1068
+ """
1069
+ return html
1070
+
1071
+ # 返回三个HTML组件的内容
1072
+ return build_stock_card(), build_financial_metrics(), build_income_table()
1073
+ # gr.Column(scale=1, min_width=250)
1074
+ # gr.HTML(f'''
1075
+ # <div class="metric-card-item" style="{card_custom_style}width:300px;">
1076
+ # <div class="" style="padding-bottom: 12px;">
1077
+ # <span class="" style="font-size: 24px;">NASDAQ100</span>
1078
+ # </div>
1079
+ # <div class="">
1080
+ # <p class="" style="font-size: 40px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p>
1081
+ # <div class="">
1082
+ # <span style="color: green;font-size: 26px;">+2.4%</span>
1083
+ # <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1084
+ # </div>
1085
+ # </div>
1086
+ # </div>
1087
+ # ''')
1088
+ # gr.HTML(f'''
1089
+ # <div class="metric-card-item" style="{card_custom_style}">
1090
+ # <div class="" style="padding-bottom: 12px;">
1091
+ # <span class="" style="font-size: 24px;">NASDAQ100</span>
1092
+ # </div>
1093
+ # <div class="">
1094
+ # <p class="" style="font-size: 40px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p>
1095
+ # <div class="">
1096
+ # <span style="color: green;font-size: 26px;">+2.4%</span>
1097
+ # <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1098
+ # </div>
1099
+ # </div>
1100
+ # </div>
1101
+ # ''')
1102
+ # gr.HTML(f'''
1103
+ # <div class="metric-card-item" style="{card_custom_style}">
1104
+ # <div class="" style="padding-bottom: 12px;">
1105
+ # <span class="" style="font-size: 24px;">NASDAQ100</span>
1106
+ # </div>
1107
+ # <div class="">
1108
+ # <p class="" style="font-size: 40px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p>
1109
+ # <div class="">
1110
+ # <span style="color: green;font-size: 26px;">+2.4%</span>
1111
+ # <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1112
+ # </div>
1113
+ # </div>
1114
+ # </div>
1115
+ # ''')
1116
+ # 总收入
1117
+ with gr.Column(scale=1, min_width=250, elem_classes=["metric-card"]):
1118
+ # with gr.Row(elem_classes=["justify-between"]):
1119
+ # gr.Markdown("Total Revenue", elem_classes=["font-semibold", "text-gray-700"])
1120
+ # gr.Markdown("💵", elem_classes=["text-green-500"])
1121
+ # with gr.Column(elem_classes=["mt-4"]):
1122
+ # gr.Markdown("$2.84B", elem_classes=["metric-value"])
1123
+ # with gr.Row(elem_classes=["metric-change", "positive", "mt-1"]):
1124
+ # gr.Markdown("+12.4% YoY")
1125
+ # gr.Markdown("↗️", elem_classes=["text-green-500"])
1126
+ gr.HTML(f'''
1127
+ <div class="metric-card-item" style="
1128
+ # width: 250px;
1129
+ height: 150px;
1130
+ # border: 1px solid red;
1131
+ # padding: 10px;
1132
+ ">
1133
+ <div class="" style="
1134
+ display: flex;
1135
+ justify-content: space-between;
1136
+ align-items: center;
1137
+ ">
1138
+ <span class="" style="font-size: 18px;">Real-time Stock Price</span>
1139
+ </div>
1140
+ <div class="">
1141
+ <p class="" style="font-size: 30px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p> <div class="">
1142
+ <span style="color: green;font-size: 18px;">+2.4%</span>
1143
+ <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1144
+ </div>
1145
+ </div>
1146
+ </div>
1147
+ ''')
1148
+ # Financial Metrics
1149
+
1150
+ with gr.Column(scale=9, elem_classes=["metric-card"]):
1151
+ # with gr.Row(elem_classes=["justify-between"]):
1152
+ # gr.Markdown("Total Revenue", elem_classes=["font-semibold", "text-gray-700"])
1153
+ # gr.Markdown("💵", elem_classes=["text-green-500"])
1154
+ # with gr.Column(elem_classes=["mt-4"]):
1155
+ # gr.Markdown("$2.84B", elem_classes=["metric-value"])
1156
+ # with gr.Row(elem_classes=["metric-change", "positive", "mt-1"]):
1157
+ # gr.Markdown("+12.4% YoY")
1158
+ # gr.Markdown("↗️", elem_classes=["text-green-500"])
1159
+ gr.HTML(f'''
1160
+ <div class="metric-card-item" style="
1161
+ # width: 250px;
1162
+ height: 150px;
1163
+ # border: 1px solid red;
1164
+ # padding: 10px;
1165
+ ">
1166
+ <div class="" style="
1167
+ display: flex;
1168
+ justify-content: space-between;
1169
+ align-items: center;
1170
+ ">
1171
+ <span class="" style="font-size: 18px;">Real-time Stock Price</span>
1172
+ </div>
1173
+ <div class="">
1174
+ <p class="" style="font-size: 30px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p> <div class="">
1175
+ <span style="color: green;font-size: 18px;">+2.4%</span>
1176
+ <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1177
+ </div>
1178
+ </div>
1179
+ </div>
1180
+ ''')
1181
+
1182
+ def create_tab_content(tab_name, company_name):
1183
+ """创建Tab内容组件"""
1184
+ if tab_name == "summary":
1185
+ print(f"company_name: {company_name}")
1186
+ # content = get_invest_suggest(company_name)
1187
+ gr.Markdown("# 11111", elem_classes=["invest-suggest-md-box"])
1188
+ # gr.Markdown(content, elem_classes=["invest-suggest-md-box"])
1189
+ # gr.Markdown("""
1190
+ # ## Investment Suggestions
1191
+
1192
+ # ### Company Overview
1193
+
1194
+ # 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.
1195
+
1196
+ # ### Key Strengths
1197
+
1198
+ # - Revenue Growth: 12.4% year-over-year increase demonstrates strong market demandDiversifed Portfolio: Multiple revenue streams reduce business risk
1199
+
1200
+ # - Innovation Focus: Continued investment in R&D drives future growth potential
1201
+
1202
+ # ### Financial Health Indicators
1203
+
1204
+ # - Liquidity: Current ratio of 1.82 indicates good short-term fnancial health
1205
+
1206
+ # - Proftability: Net income of $685M, though down slightly quarter-over-quarter0
1207
+ # - Cash Flow: Strong operating cash flow of $982M supports operations and growth initiatives
1208
+
1209
+ # ### Investment Recommendation
1210
+
1211
+ # BUY - GlobalTech Inc. presents a solid investment opportunity with:
1212
+ # - Consistent revenue growth trajectory
1213
+ # - Strong market position in key technology segments
1214
+ # - Healthy balance sheet and cash flow generation
1215
+
1216
+ # ### Risk Considerations
1217
+
1218
+ # Quarterly net income decline warrants monitoring
1219
+ # | Category | Q3 2025 | Q2 2025 | YoY % |
1220
+ # |--------------------|-----------|-----------|----------|
1221
+ # | Total Revenue | $2,842M | $2,712M | +12.4% |
1222
+ # | Gross Profit | $1,203M | $1,124M | +7.0% |
1223
+ # | Operating Income | $742M | $798M | -7.0% |
1224
+ # | Net Income | $685M | $708M | -3.2% |
1225
+ # | Earnings Per Share | $2.15 | $2.22 | -3.2% |
1226
+ # """, elem_classes=["invest-suggest-md-box"])
1227
+
1228
+
1229
+ elif tab_name == "detailed":
1230
+ with gr.Column(elem_classes=["tab-content"]):
1231
+ gr.Markdown("Financial Statements", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"])
1232
+
1233
+ with gr.Row(elem_classes=["gap-6"]):
1234
+ # 收入报表 (3/5宽度)
1235
+ with gr.Column(elem_classes=["w-3/5", "bg-gray-50", "rounded-xl", "p-4"]):
1236
+ gr.Markdown("Income Statement", elem_classes=["font-medium", "mb-3"])
1237
+ # 这里将显示收入报表表格
1238
+
1239
+ # 资产负债表和现金流量表 (2/5宽度)
1240
+ with gr.Column(elem_classes=["w-2/5", "flex", "flex-col", "gap-6"]):
1241
+ # 资产负债表
1242
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1243
+ gr.Markdown("Balance Sheet Summary", elem_classes=["font-medium", "mb-3"])
1244
+ # 这里将显示资产负债表图表
1245
+
1246
+ # 现金流量表
1247
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1248
+ with gr.Row(elem_classes=["justify-between", "items-start"]):
1249
+ gr.Markdown("Cash Flow Statement", elem_classes=["font-medium"])
1250
+ gr.Markdown("View Detailed", elem_classes=["text-xs", "text-blue-600", "font-medium"])
1251
+
1252
+ with gr.Column(elem_classes=["mt-4", "space-y-3"]):
1253
+ # 经营现金流
1254
+ with gr.Column():
1255
+ with gr.Row(elem_classes=["justify-between"]):
1256
+ gr.Markdown("Operating Cash Flow")
1257
+ gr.Markdown("$982M", elem_classes=["font-medium"])
1258
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1259
+ with gr.Column(elem_classes=["bg-green-500", "h-1.5", "rounded-full"], scale=85):
1260
+ gr.Markdown("")
1261
+
1262
+ # 投资现金流
1263
+ with gr.Column():
1264
+ with gr.Row(elem_classes=["justify-between"]):
1265
+ gr.Markdown("Investing Cash Flow")
1266
+ gr.Markdown("-$415M", elem_classes=["font-medium"])
1267
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1268
+ with gr.Column(elem_classes=["bg-blue-500", "h-1.5", "rounded-full"], scale=42):
1269
+ gr.Markdown("")
1270
+
1271
+ # 融资现金流
1272
+ with gr.Column():
1273
+ with gr.Row(elem_classes=["justify-between"]):
1274
+ gr.Markdown("Financing Cash Flow")
1275
+ gr.Markdown("-$212M", elem_classes=["font-medium"])
1276
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1277
+ with gr.Column(elem_classes=["bg-red-500", "h-1.5", "rounded-full"], scale=25):
1278
+ gr.Markdown("")
1279
+
1280
+ elif tab_name == "comparative":
1281
+ with gr.Column(elem_classes=["tab-content"]):
1282
+ gr.Markdown("Industry Benchmarking", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"])
1283
+
1284
+ # 收入增长对比
1285
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4", "mb-6"]):
1286
+ gr.Markdown("Revenue Growth - Peer Comparison", elem_classes=["font-medium", "mb-3"])
1287
+ # 这里将显示对比图表
1288
+
1289
+ # 利润率和报告预览网格
1290
+ with gr.Row(elem_classes=["grid-cols-2", "gap-6"]):
1291
+ # 利润率表格
1292
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1293
+ gr.Markdown("Profitability Ratios", elem_classes=["font-medium", "mb-3"])
1294
+ # 这里将显示利润率表格
1295
+
1296
+ # 报告预览
1297
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1298
+ gr.Markdown("Report Preview", elem_classes=["font-medium", "mb-3"])
1299
+ # 这里将显示报告预览
1300
+
1301
+ def create_chat_panel():
1302
+ """创建聊天面板组件"""
1303
+ with gr.Column(elem_classes=["chat-panel"]):
1304
+ # 聊天头部
1305
+ with gr.Row(elem_classes=["p-4", "border-b", "border-gray-200", "items-center", "gap-2"]):
1306
+ gr.Markdown("🤖", elem_classes=["text-xl", "text-blue-600"])
1307
+ gr.Markdown("Financial Assistant", elem_classes=["font-medium"])
1308
+
1309
+ # 聊天区域
1310
+ chatbot = gr.Chatbot(
1311
+ value=[
1312
+ {"role": "assistant", "content": "I'm your financial assistant, how can I help you today?"},
1313
+
1314
+ # {"role": "assistant", "content": "Hello! I can help you analyze financial data. Ask questions like \"Show revenue trends\" or \"Compare profitability ratios\""},
1315
+ # {"role": "user", "content": "Show revenue trends for last 4 quarters"},
1316
+ # {"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%)"},
1317
+ # {"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%)"},
1318
+ # {"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%)"},
1319
+ # {"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%)"}
1320
+ ],
1321
+ type="messages",
1322
+ # elem_classes=["min-h-0", "overflow-y-auto", "space-y-4", "chat-content-box"],
1323
+ show_label=False,
1324
+ autoscroll=True,
1325
+ show_copy_button=True,
1326
+ height=400,
1327
+ container=False,
1328
+ )
1329
+
1330
+ # 输入区域
1331
+ with gr.Row(elem_classes=["border-t", "border-gray-200", "gap-2"]):
1332
+ msg = gr.Textbox(
1333
+ placeholder="Ask a financial question...",
1334
+ elem_classes=["flex-1", "border", "border-gray-300", "rounded-lg", "px-4", "py-2", "focus:border-blue-500"],
1335
+ show_label=False,
1336
+ lines=1,
1337
+ submit_btn=True,
1338
+ container=False,
1339
+ )
1340
+ msg.submit(
1341
+ chat_bot,
1342
+ [msg, chatbot],
1343
+ [msg, chatbot],
1344
+ queue=True,
1345
+ )
1346
+
1347
+
1348
+ def main():
1349
+ # 获取当前目录
1350
+ current_dir = os.path.dirname(os.path.abspath(__file__))
1351
+ css_dir = os.path.join(current_dir, "css")
1352
+
1353
+ # 设置CSS路径
1354
+ css_paths = [
1355
+ os.path.join(css_dir, "main.css"),
1356
+ os.path.join(css_dir, "components.css"),
1357
+ os.path.join(css_dir, "layout.css")
1358
+ ]
1359
+
1360
+ with gr.Blocks(
1361
+ title="Financial Analysis Dashboard",
1362
+ css_paths=css_paths,
1363
+ css=custom_css,
1364
+ ) as demo:
1365
+
1366
+ # 添加处理公司点击事件的路由
1367
+ # 创建一个状态组件来跟踪选中的公司
1368
+ selected_company_state = gr.State("")
1369
+
1370
+ with gr.Column(elem_classes=["container", "container-h"]):
1371
+ # 头部
1372
+ create_header()
1373
+
1374
+ # 创建主布局
1375
+ with gr.Row(elem_classes=["main-content-box"]):
1376
+ # 左侧边栏
1377
+ with gr.Column(scale=1, min_width=350):
1378
+ # 获取company_list组件的引用
1379
+ company_list_component, report_section_component, report_display_component = create_sidebar()
1380
+
1381
+ # 主内容区域
1382
+ with gr.Column(scale=9):
1383
+
1384
+ # 指标仪表板
1385
+ create_metrics_dashboard()
1386
+
1387
+ with gr.Row(elem_classes=["main-content-box"]):
1388
+ with gr.Column(scale=9):
1389
+ # Tab内容
1390
+ with gr.Tabs():
1391
+ with gr.TabItem("Invest Suggest", elem_classes=["tab-item"]):
1392
+ # 创建一个用于显示公司名称的组件
1393
+ # company_display = gr.Markdown("# Please select a company")
1394
+ # 创建一个占位符用于显示tab内容
1395
+ tab_content = gr.Markdown(elem_classes=["invest-suggest-md-box"])
1396
+
1397
+ # 当选中的公司改变时,更新显示
1398
+ # selected_company_state.change(
1399
+ # fn=lambda company: f"# Investment Suggestions for {company}" if company else "# Please select a company",
1400
+ # inputs=[selected_company_state],
1401
+ # outputs=[company_display]
1402
+ # )
1403
+
1404
+ # 当选中的公司改变时,重新加载tab内容
1405
+ def update_tab_content(company):
1406
+ if company:
1407
+ # 显示loading状态
1408
+ loading_html = f'''
1409
+ <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
1410
+ <div style="text-align: center;">
1411
+ <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>
1412
+ <p style="margin-top: 20px; color: #666;">Loading investment suggestions for {company}...</p>
1413
+ <style>
1414
+ @keyframes spin {{
1415
+ 0% {{ transform: rotate(0deg); }}
1416
+ 100% {{ transform: rotate(360deg); }}
1417
+ }}
1418
+ </style>
1419
+ </div>
1420
+ </div>
1421
+ '''
1422
+ yield loading_html
1423
+
1424
+ # 获取投资建议数据
1425
+ try:
1426
+ content = get_invest_suggest(company)
1427
+ yield content
1428
+ except Exception as e:
1429
+ error_html = f'''
1430
+ <div style="padding: 20px; text-align: center; color: #666;">
1431
+ <p>Error loading investment suggestions: {str(e)}</p>
1432
+ <p>Please try again later.</p>
1433
+ </div>
1434
+ '''
1435
+ yield error_html
1436
+ else:
1437
+ yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1438
+
1439
+ selected_company_state.change(
1440
+ fn=update_tab_content,
1441
+ inputs=[selected_company_state],
1442
+ outputs=[tab_content],
1443
+ )
1444
+ with gr.TabItem("Analysis Report", elem_classes=["tab-item"]):
1445
+ # 创建一个用于显示公司名称的组件
1446
+ # analysis_company_display = gr.Markdown("# Please select a company")
1447
+ # 创建一个占位符用于显示tab内容
1448
+ analysis_tab_content = gr.Markdown(elem_classes=["analysis-report-md-box"])
1449
+
1450
+ # 当选中的公司改变时,更新显示
1451
+ # selected_company_state.change(
1452
+ # fn=lambda company: f"# Analysis Report for {company}" if company else "# Please select a company",
1453
+ # inputs=[selected_company_state],
1454
+ # outputs=[analysis_company_display]
1455
+ # )
1456
+
1457
+ # 当选中的公司改变时,重新加载tab内容
1458
+ def update_analysis_tab_content(company):
1459
+ if company:
1460
+ # 显示loading状态
1461
+ loading_html = f'''
1462
+ <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
1463
+ <div style="text-align: center;">
1464
+ <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>
1465
+ <p style="margin-top: 20px; color: #666;">Loading analysis report for {company}...</p>
1466
+ <style>
1467
+ @keyframes spin {{
1468
+ 0% {{ transform: rotate(0deg); }}
1469
+ 100% {{ transform: rotate(360deg); }}
1470
+ }}
1471
+ </style>
1472
+ </div>
1473
+ </div>
1474
+ '''
1475
+ yield loading_html
1476
+
1477
+ # 获取分析报告数据
1478
+ try:
1479
+ # 这里应该调用获取详细分析报告的函数
1480
+ # 暂时使用占位内容,您需要替换为实际的函数调用
1481
+ # content = f"# Analysis Report for {company}\n\nDetailed financial analysis for {company} will be displayed here."
1482
+ yield get_analysis_report(company)
1483
+ except Exception as e:
1484
+ error_html = f'''
1485
+ <div style="padding: 20px; text-align: center; color: #666;">
1486
+ <p>Error loading analysis report: {str(e)}</p>
1487
+ <p>Please try again later.</p>
1488
+ </div>
1489
+ '''
1490
+ yield error_html
1491
+ else:
1492
+ yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1493
+
1494
+ selected_company_state.change(
1495
+ fn=update_analysis_tab_content,
1496
+ inputs=[selected_company_state],
1497
+ outputs=[analysis_tab_content]
1498
+ )
1499
+ # with gr.TabItem("Comparison", elem_classes=["tab-item"]):
1500
+ # create_tab_content("comparison")
1501
+ with gr.Column(scale=1):
1502
+ # 聊天面板
1503
+ create_chat_panel()
1504
+
1505
+ # 在页面加载时自动刷新公司列表,确保显示最新的数据
1506
+ demo.load(
1507
+ fn=get_company_list_choices,
1508
+ inputs=[],
1509
+ outputs=[company_list_component],
1510
+ concurrency_limit=None
1511
+ )
1512
+
1513
+ # 绑定公司选择事件到状态更新
1514
+ # 注意:这里需要确保create_sidebar中没有重复绑定相同的事件
1515
+ company_list_component.change(
1516
+ fn=lambda x: x, # 直接返回选中的公司名称
1517
+ inputs=[company_list_component],
1518
+ outputs=[selected_company_state],
1519
+ concurrency_limit=None
1520
+ )
1521
+
1522
+ # 绑定公司选择事件到指标仪表板更新
1523
+ def update_metrics_dashboard_wrapper(company_name):
1524
+ if company_name:
1525
+ # 显示loading状态
1526
+ loading_html = '''
1527
+ <div style="display: flex; justify-content: center; align-items: center; height: 300px;">
1528
+ <div style="text-align: center;">
1529
+ <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>
1530
+ <p style="margin-top: 20px; color: #666;">Loading financial data for {company_name}...</p>
1531
+ <style>
1532
+ @keyframes spin {
1533
+ 0% { transform: rotate(0deg); }
1534
+ 100% { transform: rotate(360deg); }
1535
+ }
1536
+ </style>
1537
+ </div>
1538
+ </div>
1539
+ '''
1540
+ yield loading_html, loading_html, loading_html
1541
+
1542
+ # 获取更新后的数据
1543
+ try:
1544
+ stock_card_html, financial_metrics_html, income_table_html = update_metrics_dashboard(company_name)
1545
+ yield stock_card_html, financial_metrics_html, income_table_html
1546
+ except Exception as e:
1547
+ error_html = f'''
1548
+ <div style="padding: 20px; text-align: center; color: #666;">
1549
+ <p>Error loading financial data: {str(e)}</p>
1550
+ <p>Please try again later.</p>
1551
+ </div>
1552
+ '''
1553
+ yield error_html, error_html, error_html
1554
+ else:
1555
+ # 如果没有选择公司,返回空内容
1556
+ empty_html = "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1557
+ yield empty_html, empty_html, empty_html
1558
+
1559
+ selected_company_state.change(
1560
+ fn=update_metrics_dashboard_wrapper,
1561
+ inputs=[selected_company_state],
1562
+ outputs=list(metrics_dashboard_components),
1563
+ concurrency_limit=None
1564
+ )
1565
+
1566
+ return demo
1567
+
1568
+ if __name__ == "__main__":
1569
+ demo = main()
1570
+ demo.launch()