|
|
import gradio as gr |
|
|
import os |
|
|
import datetime |
|
|
import re |
|
|
import pandas as pd |
|
|
from sqlalchemy import true |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from chatbot.chat_main import respond |
|
|
import globals as g |
|
|
from service.mysql_service import get_companys, insert_company, get_company_by_name |
|
|
from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot |
|
|
from service.company import check_company_exists |
|
|
from service.hf_upload import get_hf_files_with_links |
|
|
from MarketandStockMCP.news_quote_mcp import get_company_news, get_quote |
|
|
from EasyReportDataMCP.report_mcp import query_financial_data |
|
|
from service.report_service import get_report_data, query_company_advanced |
|
|
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 |
|
|
from service.three_year_table_tool import build_table_format |
|
|
from service.three_year_tool import process_financial_data_with_metadata |
|
|
from service.tool_processor import get_stock_price |
|
|
|
|
|
|
|
|
get_companys_state = True |
|
|
my_companies = [ |
|
|
{'company_name': 'Alibaba', 'stock_code': 'BABA'}, |
|
|
{'company_name': 'NVIDIA', 'stock_code': 'NVDA'}, |
|
|
{'company_name': 'Amazon', 'stock_code': 'AMZN'}, |
|
|
{'company_name': 'Intel', 'stock_code': 'INTC'}, |
|
|
{'company_name': 'Meta', 'stock_code': 'META'}, |
|
|
{'company_name': 'Google', 'stock_code': 'GOOGL'}, |
|
|
{'company_name': 'Apple', 'stock_code': 'AAPL'}, |
|
|
{'company_name': 'Tesla', 'stock_code': 'TSLA'}, |
|
|
{'company_name': 'AMD', 'stock_code': 'AMD'}, |
|
|
{'company_name': 'Microsoft', 'stock_code': 'MSFT'} |
|
|
] |
|
|
|
|
|
js_code = """ |
|
|
function handleStorage(operation, key, value) { |
|
|
if (operation === 'set') { |
|
|
localStorage.setItem(key, value); |
|
|
return `已存储: ${key} = ${value}`; |
|
|
} else if (operation === 'get') { |
|
|
let storedValue = localStorage.getItem(key); |
|
|
if (storedValue === null) { |
|
|
return `未找到键: ${key}`; |
|
|
} |
|
|
return `读取到: ${key} = ${storedValue}`; |
|
|
} else if (operation === 'clear') { |
|
|
localStorage.removeItem(key); |
|
|
return `已清除: ${key}`; |
|
|
} else if (operation === 'clearAll') { |
|
|
localStorage.clear(); |
|
|
return '已清除所有数据'; |
|
|
} |
|
|
} |
|
|
""" |
|
|
custom_css = """ |
|
|
/* 匹配所有以 gradio-container- 开头的类 */ |
|
|
div[class^="gradio-container-"], |
|
|
div[class*=" gradio-container-"] { |
|
|
-webkit-text-size-adjust: 100% !important; |
|
|
line-height: 1.5 !important; |
|
|
font-family: unset !important; |
|
|
-moz-tab-size: 4 !important; |
|
|
tab-size: 4 !important; |
|
|
} |
|
|
|
|
|
.company-list-container { |
|
|
background-color: white; |
|
|
border-radius: 0.5rem; |
|
|
padding: 0.75rem; |
|
|
margin-bottom: 0.75rem; |
|
|
border: 1px solid #e5e7eb; |
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
/* 隐藏单选框 */ |
|
|
.company-list-container input[type="radio"] { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
/* 自定义选项样式 */ |
|
|
.company-list-container label { |
|
|
display: block; |
|
|
padding: 0.75rem 1rem; |
|
|
margin: 0.25rem 0; |
|
|
border-radius: 0.375rem; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s ease; |
|
|
background-color: #f9fafb; |
|
|
border: 1px solid #e5e7eb; |
|
|
font-size: 1rem; |
|
|
text-align: left; |
|
|
width: 100%; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
/* 悬停效果 */ |
|
|
.company-list-container label:hover { |
|
|
background-color: #f3f4f6; |
|
|
border-color: #d1d5db; |
|
|
} |
|
|
|
|
|
/* 选中效果 - 确保背景色充满整个选项 */ |
|
|
.company-list-container input[type="radio"]:checked + span { |
|
|
# background: #3b82f6 !important; |
|
|
color: white !important; |
|
|
font-weight: 600 !important; |
|
|
display: block; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
padding: 0.75rem 1rem; |
|
|
margin: -0.75rem -1rem; |
|
|
border-radius: 0.375rem; |
|
|
} |
|
|
|
|
|
.company-list-container span { |
|
|
display: block; |
|
|
padding: 0; |
|
|
border-radius: 0.375rem; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
/* 确保每行只有一个选项 */ |
|
|
.company-list-container .wrap { |
|
|
display: block !important; |
|
|
} |
|
|
|
|
|
.company-list-container .wrap li { |
|
|
display: block !important; |
|
|
width: 100% !important; |
|
|
} |
|
|
label.selected { |
|
|
background: #3b82f6 !important; |
|
|
color: white !important; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
companies_map = {} |
|
|
|
|
|
|
|
|
def get_stock_code_by_company_name(company_name): |
|
|
"""根据公司名称获取股票代码""" |
|
|
if company_name in companies_map and "CODE" in companies_map[company_name]: |
|
|
return companies_map[company_name]["CODE"] |
|
|
return "" |
|
|
|
|
|
|
|
|
def get_company_list_choices(): |
|
|
choices = [] |
|
|
print(f"Getting init add company list choices...{get_companys_state}") |
|
|
if not get_companys_state: |
|
|
return gr.update(choices=choices) |
|
|
try: |
|
|
|
|
|
companies_data = my_companies |
|
|
print(f"Getting init add company list choices...companies_data: {companies_data}") |
|
|
if isinstance(companies_data, pd.DataFrame) and not companies_data.empty: |
|
|
choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()] |
|
|
else: |
|
|
choices = [] |
|
|
except: |
|
|
choices = [] |
|
|
|
|
|
return gr.update(choices=choices) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def handle_company_click(company_name): |
|
|
"""处理公司点击事件,先判断是否已经入库,如果没有则进行入库操作,然后刷新公司列表""" |
|
|
print(f"Handling click for company: {company_name}") |
|
|
|
|
|
|
|
|
if not check_company_exists(my_companies, company_name): |
|
|
|
|
|
|
|
|
stock_code = companies_map.get(company_name, {}).get("CODE", "Unknown") |
|
|
print(f"Inserting company {company_name} with code {stock_code}") |
|
|
|
|
|
|
|
|
|
|
|
my_companies.append({"company_name": company_name, "stock_code": stock_code}) |
|
|
print(f"Successfully inserted company: {company_name}") |
|
|
|
|
|
companies_map[company_name] = {"NAME": company_name, "CODE": stock_code} |
|
|
|
|
|
gr.Info(f"Successfully added company: {company_name}") |
|
|
|
|
|
return True |
|
|
else: |
|
|
print(f"Company {company_name} already exists in database") |
|
|
|
|
|
gr.Warning(f"Company '{company_name}' already exists") |
|
|
|
|
|
|
|
|
return None |
|
|
|
|
|
def get_company_list_html(selected_company=""): |
|
|
try: |
|
|
|
|
|
|
|
|
companies_data = my_companies |
|
|
|
|
|
if isinstance(companies_data, str): |
|
|
if "查询执行失败" in companies_data: |
|
|
return "<div class='text-red-500'>获取公司列表失败</div>" |
|
|
else: |
|
|
|
|
|
return "" |
|
|
|
|
|
|
|
|
if not isinstance(companies_data, pd.DataFrame) or companies_data.empty: |
|
|
return "" |
|
|
|
|
|
|
|
|
html_items = [] |
|
|
for _, row in companies_data.iterrows(): |
|
|
company_name = row.get('company_name', 'Unknown') |
|
|
|
|
|
css_class = "company-item" |
|
|
if company_name == selected_company: |
|
|
css_class += " selected-company" |
|
|
|
|
|
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>') |
|
|
|
|
|
return "\n".join(html_items) |
|
|
except Exception as e: |
|
|
return f"<div class='text-red-500'>生成公司列表失败: {str(e)}</div>" |
|
|
|
|
|
def initialize_company_list(selected_company=""): |
|
|
return get_company_list_html(selected_company) |
|
|
|
|
|
def refresh_company_list(selected_company=""): |
|
|
"""刷新公司列表,返回最新的HTML内容,带loading效果""" |
|
|
|
|
|
loading_html = ''' |
|
|
<div style="display: flex; justify-content: center; align-items: center; height: 100px;"> |
|
|
<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> |
|
|
<style> |
|
|
@keyframes spin { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
</style> |
|
|
</div> |
|
|
''' |
|
|
yield loading_html |
|
|
|
|
|
|
|
|
yield get_company_list_html(selected_company) |
|
|
|
|
|
|
|
|
def select_company(company_name): |
|
|
"""处理公司选择事件,更新全局状态并返回更新后的公司列表""" |
|
|
|
|
|
g.SELECT_COMPANY = company_name if company_name else "" |
|
|
|
|
|
try: |
|
|
|
|
|
companies_data = my_companies |
|
|
if isinstance(companies_data, list) and len(companies_data) > 0: |
|
|
|
|
|
choices = [str(item.get('company_name', 'Unknown')) for item in companies_data] |
|
|
elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty: |
|
|
choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()] |
|
|
else: |
|
|
choices = [] |
|
|
except: |
|
|
choices = [] |
|
|
return gr.update(choices=choices, value=company_name) |
|
|
|
|
|
def initialize_companies_map(): |
|
|
"""初始化 companies_map 字典""" |
|
|
global companies_map |
|
|
companies_map = {} |
|
|
|
|
|
print("Initializing companies map...") |
|
|
|
|
|
try: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
companies_data = my_companies |
|
|
|
|
|
|
|
|
|
|
|
print(f"Companies data from DB: {companies_data}") |
|
|
|
|
|
|
|
|
if isinstance(companies_data, pd.DataFrame) and not companies_data.empty: |
|
|
print(f"Adding {len(companies_data)} companies from database") |
|
|
for _, row in companies_data.iterrows(): |
|
|
company_name = row.get('company_name', 'Unknown') |
|
|
stock_code = row.get('stock_code', '') |
|
|
|
|
|
|
|
|
company_name = str(company_name) if company_name is not None else 'Unknown' |
|
|
stock_code = str(stock_code) if stock_code is not None else '' |
|
|
|
|
|
|
|
|
is_duplicate = False |
|
|
for existing_company in companies_map.values(): |
|
|
if existing_company["CODE"] == stock_code: |
|
|
is_duplicate = True |
|
|
break |
|
|
|
|
|
|
|
|
if not is_duplicate: |
|
|
companies_map[company_name] = {"NAME": company_name, "CODE": stock_code} |
|
|
|
|
|
else: |
|
|
print("No companies found in database") |
|
|
|
|
|
print(f"Final companies map: {companies_map}") |
|
|
except Exception as e: |
|
|
|
|
|
print(f"Error initializing companies map: {str(e)}") |
|
|
pass |
|
|
|
|
|
|
|
|
def update_company_choices(user_input: str): |
|
|
"""更新公司选择列表""" |
|
|
|
|
|
yield gr.update( |
|
|
choices=["Searching..."], |
|
|
visible=True |
|
|
), gr.update(visible=False, value="") |
|
|
|
|
|
|
|
|
choices = search_company(user_input) |
|
|
|
|
|
|
|
|
if len(choices) > 0 and isinstance(choices[0], str) and not choices[0].startswith("Searching"): |
|
|
|
|
|
error_message = choices[0] if len(choices) > 0 else "未知错误" |
|
|
|
|
|
error_html = f''' |
|
|
<div class="ant-message ant-message-error" style=" |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
z-index: 10000; |
|
|
padding: 10px 16px; |
|
|
border-radius: 4px; |
|
|
background: #fff; |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
pointer-events: all; |
|
|
animation: messageFadeIn 0.3s ease-in-out; |
|
|
"> |
|
|
<div style=" |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
background: #ff4d4f; |
|
|
border-radius: 50%; |
|
|
position: relative; |
|
|
margin-right: 8px; |
|
|
"></div> |
|
|
<span>{error_message}</span> |
|
|
</div> |
|
|
<script> |
|
|
setTimeout(function() {{ |
|
|
var msg = document.querySelector('.ant-message-error'); |
|
|
if (msg) {{ |
|
|
msg.style.animation = 'messageFadeOut 0.3s ease-in-out'; |
|
|
setTimeout(function() {{ msg.remove(); }}, 3000); |
|
|
}} |
|
|
}}, 3000); |
|
|
</script> |
|
|
''' |
|
|
yield gr.update(choices=["No results found"], visible=True), gr.update(visible=True, value=error_html) |
|
|
else: |
|
|
|
|
|
yield gr.update( |
|
|
choices=choices, |
|
|
visible=len(choices) > 0 |
|
|
), gr.update(visible=False, value="") |
|
|
|
|
|
def add_company(selected, current_list): |
|
|
"""添加选中的公司""" |
|
|
if selected == "No results found": |
|
|
return gr.update(visible=False), current_list, gr.update(visible=False, value="") |
|
|
if selected: |
|
|
|
|
|
|
|
|
|
|
|
selected_clean = selected.strip() |
|
|
match = re.match(r"^(.+?)\s*\(([^)]+)\)$", selected_clean) |
|
|
if match: |
|
|
company_name = match.group(1) |
|
|
stock_code = match.group(2) |
|
|
elif companies_map.get(selected_clean): |
|
|
company_name = selected_clean |
|
|
stock_code = companies_map[selected_clean]["CODE"] |
|
|
else: |
|
|
company_name = selected_clean |
|
|
stock_code = "Unknown" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not check_company_exists(my_companies, company_name): |
|
|
|
|
|
|
|
|
my_companies.append({"company_name": company_name, "stock_code": stock_code}) |
|
|
|
|
|
try: |
|
|
|
|
|
companies_data = my_companies |
|
|
if isinstance(companies_data, list) and len(companies_data) > 0: |
|
|
|
|
|
updated_list = [str(item.get('company_name', 'Unknown')) for item in companies_data] |
|
|
elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty: |
|
|
updated_list = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()] |
|
|
else: |
|
|
updated_list = [] |
|
|
except: |
|
|
updated_list = [] |
|
|
|
|
|
|
|
|
if not updated_list: |
|
|
updated_list = ['Alibaba', '腾讯控股', 'Tencent', '阿里巴巴-W', 'Apple'] |
|
|
|
|
|
|
|
|
|
|
|
return gr.update(visible=False), gr.update(choices=updated_list, value=company_name), gr.update(visible=False, value="") |
|
|
|
|
|
else: |
|
|
|
|
|
gr.Warning(f"公司 '{company_name}' 已存在") |
|
|
return gr.update(visible=False), current_list, gr.update(visible=False, value="") |
|
|
|
|
|
return gr.update(visible=False), current_list, gr.update(visible=False, value="") |
|
|
|
|
|
|
|
|
|
|
|
company_buttons = {} |
|
|
|
|
|
def create_company_buttons(): |
|
|
"""创建公司按钮组件""" |
|
|
|
|
|
if not companies_map: |
|
|
initialize_companies_map() |
|
|
|
|
|
|
|
|
companies = list(companies_map.keys()) |
|
|
|
|
|
|
|
|
print(f"Companies in map: {companies}") |
|
|
|
|
|
|
|
|
company_buttons.clear() |
|
|
|
|
|
if not companies: |
|
|
|
|
|
with gr.Column(): |
|
|
gr.Markdown("暂无公司数据") |
|
|
else: |
|
|
|
|
|
with gr.Column(elem_classes=["home-company-list"]): |
|
|
|
|
|
for i in range(0, len(companies), 2): |
|
|
|
|
|
if i + 1 < len(companies): |
|
|
|
|
|
with gr.Row(elem_classes=["home-company-item-box"]): |
|
|
btn1 = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"]) |
|
|
btn2 = gr.Button(companies[i + 1], elem_classes=["home-company-item", "gradio-button"]) |
|
|
|
|
|
company_buttons[companies[i]] = btn1 |
|
|
company_buttons[companies[i + 1]] = btn2 |
|
|
else: |
|
|
|
|
|
with gr.Row(elem_classes=["home-company-item-box", "single-item"]): |
|
|
btn = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"]) |
|
|
|
|
|
company_buttons[companies[i]] = btn |
|
|
|
|
|
|
|
|
return company_buttons |
|
|
def update_report_section(selected_company, report_data, stock_code): |
|
|
"""根据选中的公司更新报告部分""" |
|
|
print(f"Updating report (报告部分): {selected_company}") |
|
|
|
|
|
if selected_company == "" or selected_company is None or selected_company == "Unknown": |
|
|
|
|
|
|
|
|
|
|
|
html_content = "" |
|
|
return gr.update(value=html_content, visible=True) |
|
|
else: |
|
|
|
|
|
try: |
|
|
|
|
|
|
|
|
|
|
|
stock_code = get_stock_code_by_company_name(selected_company) |
|
|
|
|
|
|
|
|
report_data = query_financial_data(stock_code, "5-Year") |
|
|
|
|
|
|
|
|
|
|
|
if not isinstance(report_data, list) or len(report_data) == 0: |
|
|
return gr.update(value="<div>暂无报告数据</div>", visible=True) |
|
|
|
|
|
|
|
|
if not isinstance(report_data[0], dict): |
|
|
return gr.update(value="<div>数据格式不正常</div>", visible=True) |
|
|
|
|
|
html_content = '<div class="report-list-box bg-white">' |
|
|
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>' |
|
|
for report in report_data: |
|
|
source_url = report.get('source_url', '#') |
|
|
period = report.get('period', 'N/A') |
|
|
source_form = report.get('source_form', 'N/A') |
|
|
html_content += f''' |
|
|
<div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{source_url}', '_blank')"> |
|
|
<div class="report-item-content"> |
|
|
<span class="text-gray-800">{period}-{stock_code}-{source_form}</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor"> |
|
|
<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" /> |
|
|
</svg> |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>' |
|
|
html_content += '</div>' |
|
|
|
|
|
return gr.update(value=html_content, visible=True) |
|
|
except Exception as e: |
|
|
print(f"Error in update_report_section: {str(e)}") |
|
|
return gr.update(value=f"<div>报告载入失败: {str(e)}</div>", visible=True) |
|
|
def update_news_section(selected_company): |
|
|
"""根据选中的公司更新报告部分""" |
|
|
html_content = "" |
|
|
if selected_company == "" or selected_company is None: |
|
|
|
|
|
|
|
|
|
|
|
return gr.update(value=html_content, visible=True) |
|
|
else: |
|
|
try: |
|
|
stock_code = get_stock_code_by_company_name(selected_company) |
|
|
report_data = get_company_news(stock_code, None, None) |
|
|
|
|
|
|
|
|
if (report_data['articles']): |
|
|
report_data = report_data['articles'] |
|
|
news_html = "<div class='news-list-box bg-white'>" |
|
|
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>' |
|
|
from datetime import datetime |
|
|
|
|
|
for news in report_data: |
|
|
published_at = news['published'] |
|
|
|
|
|
|
|
|
dt = datetime.fromisoformat(published_at.replace("Z", "+00:00")) |
|
|
|
|
|
|
|
|
formatted_date = dt.strftime("%Y.%m.%d") |
|
|
news_html += f''' |
|
|
<div class="news-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{news['url']}', '_blank')"> |
|
|
<div class="news-item-content"> |
|
|
<span class="text-xs text-gray-500">[{formatted_date}]</span> |
|
|
<span class="text-gray-800">{news['headline']}</span> |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
news_html += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}条新闻</span></div>' |
|
|
news_html += '</div>' |
|
|
html_content += news_html |
|
|
except Exception as e: |
|
|
print(f"Error updating report section: {str(e)}") |
|
|
|
|
|
return gr.update(value=html_content, visible=True) |
|
|
|
|
|
|
|
|
def create_header(): |
|
|
"""创建头部组件""" |
|
|
|
|
|
current_time = datetime.datetime.now().strftime("%B %d, %Y - Market Data Updated Today") |
|
|
|
|
|
with gr.Row(elem_classes=["header"]): |
|
|
|
|
|
with gr.Column(scale=8): |
|
|
|
|
|
gr.HTML(''' |
|
|
<div class="top-logo-box"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 48 48"> |
|
|
<g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"> |
|
|
<path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11"></path> |
|
|
<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> |
|
|
<ellipse cx="24" cy="10" rx="20" ry="6"></ellipse> |
|
|
</g> |
|
|
</svg> |
|
|
<span class="logo-title">Easy Financial Report Dashboard</span> |
|
|
</div> |
|
|
''', elem_classes=["text-2xl"]) |
|
|
|
|
|
|
|
|
with gr.Column(scale=2): |
|
|
gr.Markdown(current_time, elem_classes=["text-sm-top-time"]) |
|
|
|
|
|
def create_company_list(get_companys_state): |
|
|
"""创建公司列表组件""" |
|
|
try: |
|
|
|
|
|
|
|
|
companies_data = my_companies |
|
|
print(f"创建公司列表组件 - Companies data: {companies_data}") |
|
|
if isinstance(companies_data, list) and len(companies_data) > 0: |
|
|
|
|
|
choices = [str(item.get('company_name', 'Unknown')) for item in companies_data] |
|
|
elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty: |
|
|
choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()] |
|
|
else: |
|
|
choices = [] |
|
|
except Exception as e: |
|
|
print(f"Error creating company list: {str(e)}") |
|
|
choices = [] |
|
|
|
|
|
|
|
|
if not choices: |
|
|
choices = [] |
|
|
|
|
|
|
|
|
company_list = gr.Radio( |
|
|
choices=choices, |
|
|
label="", |
|
|
interactive=True, |
|
|
elem_classes=["company-list-container"], |
|
|
container=False, |
|
|
visible=True |
|
|
) |
|
|
|
|
|
return company_list |
|
|
|
|
|
def create_company_selector(): |
|
|
"""创建公司选择器组件""" |
|
|
company_input = gr.Textbox( |
|
|
show_label=False, |
|
|
placeholder="Add Company", |
|
|
elem_classes=["company-input-box"], |
|
|
lines=1, |
|
|
max_lines=1, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
status_message = gr.HTML( |
|
|
"", |
|
|
elem_classes=["status-message"], |
|
|
visible=False |
|
|
) |
|
|
|
|
|
|
|
|
company_modal = gr.Radio( |
|
|
show_label=False, |
|
|
choices=[], |
|
|
visible=False, |
|
|
elem_classes=["company-modal"] |
|
|
) |
|
|
|
|
|
return company_input, status_message, company_modal |
|
|
|
|
|
def create_report_section(): |
|
|
"""创建报告部分组件""" |
|
|
|
|
|
|
|
|
|
|
|
initial_content = "" |
|
|
|
|
|
|
|
|
report_display = gr.HTML(initial_content) |
|
|
return report_display |
|
|
|
|
|
def create_news_section(): |
|
|
"""创建新闻部分组件""" |
|
|
initial_content = "" |
|
|
news_display = gr.HTML(initial_content) |
|
|
return news_display |
|
|
|
|
|
def format_financial_metrics(data: dict, prev_data: dict = None) -> list: |
|
|
""" |
|
|
将原始财务数据转换为 financial_metrics 格式。 |
|
|
|
|
|
Args: |
|
|
data (dict): 当前财年数据(必须包含 total_revenue, net_income 等字段) |
|
|
prev_data (dict, optional): 上一财年数据,用于计算 change。若未提供,change 设为 "--" |
|
|
|
|
|
Returns: |
|
|
list[dict]: 符合 financial_metrics 格式的列表 |
|
|
""" |
|
|
|
|
|
def format_currency(value: float) -> str: |
|
|
"""将数字格式化为 $XB / $XM / $XK""" |
|
|
if value >= 1e9: |
|
|
return f"${value / 1e9:.2f}B" |
|
|
elif value >= 1e6: |
|
|
return f"${value / 1e6:.2f}M" |
|
|
elif value >= 1e3: |
|
|
return f"${value / 1e3:.2f}K" |
|
|
else: |
|
|
return f"${value:.2f}" |
|
|
|
|
|
def calculate_change(current: float, previous: float) -> tuple: |
|
|
"""计算变化百分比和颜色""" |
|
|
if previous == 0: |
|
|
return "--", "gray" |
|
|
change_pct = (current - previous) / abs(previous) * 100 |
|
|
sign = "+" if change_pct >= 0 else "" |
|
|
color = "green" if change_pct >= 0 else "red" |
|
|
return f"{sign}{change_pct:.1f}%", color |
|
|
|
|
|
|
|
|
metrics_config = [ |
|
|
{ |
|
|
"key": "total_revenue", |
|
|
"label": "Total Revenue", |
|
|
"is_currency": True, |
|
|
"eps_like": False |
|
|
}, |
|
|
{ |
|
|
"key": "net_income", |
|
|
"label": "Net Income", |
|
|
"is_currency": True, |
|
|
"eps_like": False |
|
|
}, |
|
|
{ |
|
|
"key": "earnings_per_share", |
|
|
"label": "Earnings Per Share", |
|
|
"is_currency": False, |
|
|
"eps_like": True |
|
|
}, |
|
|
{ |
|
|
"key": "operating_expenses", |
|
|
"label": "Operating Expenses", |
|
|
"is_currency": True, |
|
|
"eps_like": False |
|
|
}, |
|
|
{ |
|
|
"key": "operating_cash_flow", |
|
|
"label": "Cash Flow", |
|
|
"is_currency": True, |
|
|
"eps_like": False |
|
|
} |
|
|
] |
|
|
|
|
|
result = [] |
|
|
for item in metrics_config: |
|
|
key = item["key"] |
|
|
current_val = data.get(key) |
|
|
if current_val is None: |
|
|
continue |
|
|
|
|
|
|
|
|
if item["eps_like"]: |
|
|
value_str = f"${current_val:.2f}" |
|
|
elif item["is_currency"]: |
|
|
value_str = format_currency(current_val) |
|
|
else: |
|
|
value_str = str(current_val) |
|
|
|
|
|
|
|
|
if prev_data and key in prev_data: |
|
|
prev_val = prev_data[key] |
|
|
change_str, color = calculate_change(current_val, prev_val) |
|
|
else: |
|
|
change_str = "--" |
|
|
color = "gray" |
|
|
|
|
|
result.append({ |
|
|
"label": item["label"], |
|
|
"value": value_str, |
|
|
"change": change_str, |
|
|
"color": color |
|
|
}) |
|
|
|
|
|
return result |
|
|
|
|
|
|
|
|
def create_sidebar(): |
|
|
"""创建侧边栏组件""" |
|
|
|
|
|
initialize_companies_map() |
|
|
|
|
|
with gr.Column(elem_classes=["sidebar"]): |
|
|
|
|
|
with gr.Group(elem_classes=["card"]): |
|
|
gr.Markdown("### Select Company", elem_classes=["card-title", "left-card-title"]) |
|
|
with gr.Column(): |
|
|
company_list = create_company_list(get_companys_state) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
company_input, status_message, company_modal = create_company_selector() |
|
|
|
|
|
|
|
|
company_input.submit( |
|
|
fn=update_company_choices, |
|
|
inputs=[company_input], |
|
|
outputs=[company_modal, status_message] |
|
|
) |
|
|
|
|
|
company_modal.change( |
|
|
fn=add_company, |
|
|
inputs=[company_modal, company_list], |
|
|
outputs=[company_modal, company_list, status_message] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Group(elem_classes=["report-news-box"]) as report_section_group: |
|
|
|
|
|
report_display = create_report_section() |
|
|
news_display = create_news_section() |
|
|
|
|
|
|
|
|
|
|
|
def select_company_handler(company_name): |
|
|
"""处理公司选择事件的处理器""" |
|
|
|
|
|
g.SELECT_COMPANY = company_name if company_name else "" |
|
|
|
|
|
|
|
|
updated_report_display = update_report_section(company_name, None, None) |
|
|
|
|
|
updated_news_display = update_news_section(company_name) |
|
|
|
|
|
if company_name: |
|
|
|
|
|
return gr.update(visible=True), updated_report_display, updated_news_display |
|
|
else: |
|
|
|
|
|
return gr.update(visible=False), updated_report_display, updated_news_display |
|
|
|
|
|
company_list.change( |
|
|
fn=select_company_handler, |
|
|
inputs=[company_list], |
|
|
outputs=[report_section_group, report_display, news_display] |
|
|
) |
|
|
|
|
|
|
|
|
return company_list, report_section_group, report_display, news_display |
|
|
|
|
|
def build_income_table(table_data): |
|
|
|
|
|
|
|
|
|
|
|
if isinstance(table_data, dict) and "list_data" in table_data: |
|
|
|
|
|
income_statement = table_data["list_data"] |
|
|
yoy_rates = table_data["yoy_rates"] or [] |
|
|
else: |
|
|
|
|
|
income_statement = table_data |
|
|
yoy_rates = [] |
|
|
|
|
|
|
|
|
yoy_map = {} |
|
|
if len(yoy_rates) > 1 and len(yoy_rates[0]) > 1: |
|
|
|
|
|
yoy_headers = yoy_rates[0][1:] |
|
|
|
|
|
|
|
|
for i, yoy_row in enumerate(yoy_rates[1:], 1): |
|
|
category = yoy_row[0] |
|
|
yoy_map[category] = {} |
|
|
for j, rate in enumerate(yoy_row[1:]): |
|
|
if j < len(yoy_headers): |
|
|
yoy_map[category][yoy_headers[j]] = rate |
|
|
|
|
|
table_rows = "" |
|
|
header_row = income_statement[0] |
|
|
|
|
|
for i, row in enumerate(income_statement): |
|
|
if i == 0: |
|
|
row_style = "background-color: #f5f5f5; font-weight: 500;" |
|
|
else: |
|
|
row_style = "background-color: #f9f9f9;" |
|
|
cells = "" |
|
|
|
|
|
for j, cell in enumerate(row): |
|
|
if j == 0: |
|
|
cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>" |
|
|
else: |
|
|
|
|
|
growth = None |
|
|
category = row[0] |
|
|
|
|
|
|
|
|
if i > 0 and category in yoy_map and j > 0 and j < len(header_row): |
|
|
year_header = header_row[j] |
|
|
if year_header in yoy_map[category]: |
|
|
growth = yoy_map[category][year_header] |
|
|
|
|
|
if growth and growth != "N/A": |
|
|
arrow = "▲" if growth.startswith("+") else "▼" |
|
|
color = "green" if growth.startswith("+") else "red" |
|
|
cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; 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>""" |
|
|
else: |
|
|
cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>" |
|
|
table_rows += f"<tr style='{row_style}'>{cells}</tr>" |
|
|
|
|
|
html = f""" |
|
|
<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;"> |
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;"> |
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/> |
|
|
</svg> |
|
|
<div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div> |
|
|
</div> |
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;"> |
|
|
{table_rows} |
|
|
</table> |
|
|
</div> |
|
|
""" |
|
|
return html |
|
|
def create_metrics_dashboard(): |
|
|
"""创建指标仪表板组件""" |
|
|
with gr.Row(elem_classes=["metrics-dashboard"]): |
|
|
card_custom_style = ''' |
|
|
background-color: white; |
|
|
border-radius: 0.5rem; |
|
|
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.1) 0px 1px 2px -1px; |
|
|
padding: 1.25rem; |
|
|
min-height: 250px !important; |
|
|
text-align: center; |
|
|
''' |
|
|
|
|
|
|
|
|
company_info = { |
|
|
"name": "N/A", |
|
|
"symbol": "NYSE:N/A", |
|
|
"price": 0, |
|
|
"change": 0, |
|
|
"change_percent": 0.41, |
|
|
"open": 165.20, |
|
|
"high": 166.37, |
|
|
"low": 156.15, |
|
|
"prev_close": 157.01, |
|
|
"volume": "27.10M" |
|
|
} |
|
|
financial_metrics = [ |
|
|
{"label": "Total Revenue", "value": "N/A", "change": "N/A", "color": "grey"}, |
|
|
{"label": "Net Income", "value": "N/A", "change": "N/A", "color": "grey"}, |
|
|
{"label": "Earnings Per Share", "value": "N/A", "change": "N/A", "color": "grey"}, |
|
|
{"label": "Operating Expenses", "value": "N/A", "change": "N/A", "color": "grey"}, |
|
|
{"label": "Cash Flow", "value": "N/A", "change": "N/A", "color": "grey"} |
|
|
] |
|
|
income_statement = { |
|
|
"list_data": [ |
|
|
["Category", "N/A/FY", "N/A/FY", "N/A/FY"], |
|
|
["Total", "N/A", "N/A", "N/A"], |
|
|
["Net Income", "N/A", "N/A", "N/A.4M"], |
|
|
["Earnings Per Share", "N/A", "N/A", "N/A"], |
|
|
["Operating Expenses", "N/A", "N/A", "N/A"], |
|
|
["Cash Flow", "N/A", "N/A", "N/A"] |
|
|
], |
|
|
"yoy_rates": [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
yearly_data = 'N/A' |
|
|
|
|
|
def render_change(change: str, color: str): |
|
|
if change.startswith("+"): |
|
|
return f'<span style="color:{color};">▲{change}</span>' |
|
|
else: |
|
|
return f'<span style="color:{color};">▼{change}</span>' |
|
|
|
|
|
|
|
|
def build_stock_card(): |
|
|
html = f""" |
|
|
<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;"> |
|
|
<div style="font-size: 14px; color: #555;">N/A</div> |
|
|
<div style="font-size: 12px; color: #888;">N/A</div> |
|
|
<div style="font-size: 32px; font-weight: bold; margin: 8px 0;">N/A</div> |
|
|
<div style="font-size: 14px; margin: 8px 0;">N/A</div> |
|
|
<div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;"> |
|
|
<div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div> |
|
|
<div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div> |
|
|
<div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div> |
|
|
<div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
return html |
|
|
|
|
|
|
|
|
def build_financial_metrics(): |
|
|
metrics_html = "" |
|
|
for item in financial_metrics: |
|
|
change_html = render_change(item["change"], item["color"]) |
|
|
metrics_html += f""" |
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;"> |
|
|
<div style="font-size: 14px; color: #555;">{item['label']}</div> |
|
|
<div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
html = f""" |
|
|
<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;"> |
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;"> |
|
|
<div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;"> |
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/> |
|
|
</svg> |
|
|
<span style="margin-left: 10px;">{yearly_data} Financial Metrics</span> |
|
|
</div> |
|
|
<div style="font-size: 16px; color: #8f8f8f;"> |
|
|
YTD data |
|
|
</div> |
|
|
</div> |
|
|
{metrics_html} |
|
|
</div> |
|
|
""" |
|
|
return html |
|
|
|
|
|
|
|
|
|
|
|
def get_dashboard(): |
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]): |
|
|
stock_card_html = gr.HTML(build_stock_card(), elem_classes=["metric-card-left"]) |
|
|
with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]): |
|
|
financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"]) |
|
|
with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]): |
|
|
|
|
|
income_table_html = gr.HTML(build_income_table(income_statement), elem_classes=["metric-card-right"]) |
|
|
return stock_card_html, financial_metrics_html, income_table_html |
|
|
|
|
|
|
|
|
stock_card_component, financial_metrics_component, income_table_component = get_dashboard() |
|
|
|
|
|
|
|
|
global metrics_dashboard_components |
|
|
metrics_dashboard_components = (stock_card_component, financial_metrics_component, income_table_component) |
|
|
|
|
|
|
|
|
def update_metrics_dashboard(company_name): |
|
|
"""根据选择的公司更新指标仪表板""" |
|
|
company_info = {} |
|
|
|
|
|
stock_code = "" |
|
|
try: |
|
|
|
|
|
stock_code = get_stock_code_by_company_name(company_name) |
|
|
company_info = get_quote(stock_code.strip()) |
|
|
company_info['company'] = company_name |
|
|
print(f"股票价格数据 {company_info}") |
|
|
except Exception as e: |
|
|
print(f"获取股票价格数据失败: {e}") |
|
|
|
|
|
financial_metrics_pre = query_financial_data(stock_code, "5-Year") |
|
|
financial_metrics = [] |
|
|
year_data = None |
|
|
three_year_data = None |
|
|
try: |
|
|
|
|
|
result = process_financial_data_with_metadata(financial_metrics_pre) |
|
|
|
|
|
|
|
|
financial_metrics = result["financial_metrics"] |
|
|
year_data = result["year_data"] |
|
|
three_year_data = result["three_year_data"] |
|
|
print(f"格式化后的财务数据: {financial_metrics}") |
|
|
except Exception as e: |
|
|
print(f"Error process_financial_data: {e}") |
|
|
|
|
|
yearly_data = year_data |
|
|
table_data = build_table_format(three_year_data) |
|
|
|
|
|
|
|
|
def render_change(change: str, color: str): |
|
|
if change.startswith("+"): |
|
|
return f'<span style="color:{color};">▲{change}</span>' |
|
|
else: |
|
|
return f'<span style="color:{color};">▼{change}</span>' |
|
|
|
|
|
|
|
|
def build_stock_card(company_info): |
|
|
try: |
|
|
if not company_info or not isinstance(company_info, dict): |
|
|
company_name = "N/A" |
|
|
symbol = "N/A" |
|
|
price = "N/A" |
|
|
change_html = '<span style="color:#888;">N/A</span>' |
|
|
open_val = high_val = low_val = prev_close_val = volume_display = "N/A" |
|
|
else: |
|
|
company_name = company_info.get("company", "N/A") |
|
|
symbol = company_info.get("symbol", "N/A") |
|
|
price = company_info.get("current_price", "N/A") |
|
|
|
|
|
|
|
|
change_str = company_info.get("change", "0") |
|
|
try: |
|
|
change = float(change_str) |
|
|
except (ValueError, TypeError): |
|
|
change = 0.0 |
|
|
|
|
|
|
|
|
change_percent = company_info.get("percent_change", "0%") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
change_color = "green" if change >= 0 else "red" |
|
|
sign = "+" if change >= 0 else "" |
|
|
change_html = f'<span style="color:{change_color};">{sign}{change:.2f} ({change_percent:+.2f}%)</span>' |
|
|
|
|
|
|
|
|
open_val = company_info.get("open", "N/A") |
|
|
high_val = company_info.get("high", "N/A") |
|
|
low_val = company_info.get("low", "N/A") |
|
|
prev_close_val = company_info.get("previous_close", "N/A") |
|
|
|
|
|
|
|
|
|
|
|
html = f""" |
|
|
<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;"> |
|
|
<div style="font-size: 16px; color: #555; font-weight: 500;">{company_name}</div> |
|
|
<div style="font-size: 12px; color: #888;">NYSE:{symbol}</div> |
|
|
<div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;"> |
|
|
<div style="font-size: 32px; font-weight: bold;">{price}</div> |
|
|
<div style="font-size: 14px;">{change_html}</div> |
|
|
</div> |
|
|
<div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;"> |
|
|
<div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{open_val}</div> |
|
|
<div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{high_val}</div> |
|
|
<div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{low_val}</div> |
|
|
<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> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
return html |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Error building stock card: {e}") |
|
|
return '<div style="width:250px; padding:16px; color:red;">Error loading stock data</div>' |
|
|
|
|
|
def build_financial_metrics(yearly_data): |
|
|
metrics_html = "" |
|
|
for item in financial_metrics: |
|
|
change_html = render_change(item["change"], item["color"]) |
|
|
metrics_html += f""" |
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;"> |
|
|
<div style="font-size: 14px; color: #555;">{item['label']}</div> |
|
|
<div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
html = f""" |
|
|
<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;"> |
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;"> |
|
|
<div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;"> |
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/> |
|
|
</svg> |
|
|
<span style="margin-left: 10px;">{yearly_data} Financial Metrics</span> |
|
|
</div> |
|
|
<div style="font-size: 16px; color: #8f8f8f;"> |
|
|
YTD data |
|
|
</div> |
|
|
</div> |
|
|
{metrics_html} |
|
|
</div> |
|
|
""" |
|
|
return html |
|
|
|
|
|
return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data) |
|
|
|
|
|
def create_tab_content(tab_name, company_name): |
|
|
"""创建Tab内容组件""" |
|
|
if tab_name == "summary": |
|
|
print(f"company_name: {company_name}") |
|
|
|
|
|
gr.Markdown("# 11111", elem_classes=["invest-suggest-md-box"]) |
|
|
|
|
|
elif tab_name == "detailed": |
|
|
with gr.Column(elem_classes=["tab-content"]): |
|
|
gr.Markdown("Financial Statements", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"]) |
|
|
|
|
|
with gr.Row(elem_classes=["gap-6"]): |
|
|
|
|
|
with gr.Column(elem_classes=["w-3/5", "bg-gray-50", "rounded-xl", "p-4"]): |
|
|
gr.Markdown("Income Statement", elem_classes=["font-medium", "mb-3"]) |
|
|
|
|
|
|
|
|
|
|
|
with gr.Column(elem_classes=["w-2/5", "flex", "flex-col", "gap-6"]): |
|
|
|
|
|
with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]): |
|
|
gr.Markdown("Balance Sheet Summary", elem_classes=["font-medium", "mb-3"]) |
|
|
|
|
|
|
|
|
|
|
|
with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]): |
|
|
with gr.Row(elem_classes=["justify-between", "items-start"]): |
|
|
gr.Markdown("Cash Flow Statement", elem_classes=["font-medium"]) |
|
|
gr.Markdown("View Detailed", elem_classes=["text-xs", "text-blue-600", "font-medium"]) |
|
|
|
|
|
with gr.Column(elem_classes=["mt-4", "space-y-3"]): |
|
|
|
|
|
with gr.Column(): |
|
|
with gr.Row(elem_classes=["justify-between"]): |
|
|
gr.Markdown("Operating Cash Flow") |
|
|
gr.Markdown("$982M", elem_classes=["font-medium"]) |
|
|
with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]): |
|
|
with gr.Column(elem_classes=["bg-green-500", "h-1.5", "rounded-full"], scale=85): |
|
|
gr.Markdown("") |
|
|
|
|
|
|
|
|
with gr.Column(): |
|
|
with gr.Row(elem_classes=["justify-between"]): |
|
|
gr.Markdown("Investing Cash Flow") |
|
|
gr.Markdown("-$415M", elem_classes=["font-medium"]) |
|
|
with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]): |
|
|
with gr.Column(elem_classes=["bg-blue-500", "h-1.5", "rounded-full"], scale=42): |
|
|
gr.Markdown("") |
|
|
|
|
|
|
|
|
with gr.Column(): |
|
|
with gr.Row(elem_classes=["justify-between"]): |
|
|
gr.Markdown("Financing Cash Flow") |
|
|
gr.Markdown("-$212M", elem_classes=["font-medium"]) |
|
|
with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]): |
|
|
with gr.Column(elem_classes=["bg-red-500", "h-1.5", "rounded-full"], scale=25): |
|
|
gr.Markdown("") |
|
|
|
|
|
elif tab_name == "comparative": |
|
|
with gr.Column(elem_classes=["tab-content"]): |
|
|
gr.Markdown("Industry Benchmarking", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"]) |
|
|
|
|
|
|
|
|
with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4", "mb-6"]): |
|
|
gr.Markdown("Revenue Growth - Peer Comparison", elem_classes=["font-medium", "mb-3"]) |
|
|
|
|
|
|
|
|
|
|
|
with gr.Row(elem_classes=["grid-cols-2", "gap-6"]): |
|
|
|
|
|
with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]): |
|
|
gr.Markdown("Profitability Ratios", elem_classes=["font-medium", "mb-3"]) |
|
|
|
|
|
|
|
|
|
|
|
with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]): |
|
|
gr.Markdown("Report Preview", elem_classes=["font-medium", "mb-3"]) |
|
|
|
|
|
|
|
|
def create_chat_panel(): |
|
|
"""创建聊天面板组件""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
|
|
|
|
current_dir = os.path.dirname(os.path.abspath(__file__)) |
|
|
css_dir = os.path.join(current_dir, "css") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
css_paths = [ |
|
|
os.path.join(css_dir, "main.css"), |
|
|
os.path.join(css_dir, "components.css"), |
|
|
os.path.join(css_dir, "layout.css") |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
title="Financial Analysis Dashboard", |
|
|
css_paths=css_paths, |
|
|
css=custom_css, |
|
|
|
|
|
) as demo: |
|
|
|
|
|
|
|
|
|
|
|
selected_company_state = gr.State("") |
|
|
|
|
|
with gr.Column(elem_classes=["container", "container-h"]): |
|
|
|
|
|
create_header() |
|
|
|
|
|
|
|
|
with gr.Row(elem_classes=["main-content-box"]): |
|
|
|
|
|
with gr.Column(scale=1, min_width=350): |
|
|
|
|
|
company_list_component, report_section_component, report_display_component, news_display_component = create_sidebar() |
|
|
|
|
|
|
|
|
with gr.Column(scale=9): |
|
|
|
|
|
|
|
|
create_metrics_dashboard() |
|
|
|
|
|
with gr.Row(elem_classes=["main-content-box"]): |
|
|
with gr.Column(scale=8): |
|
|
|
|
|
with gr.Tabs(): |
|
|
with gr.TabItem("Investment Suggestion", elem_classes=["tab-item"]): |
|
|
|
|
|
|
|
|
|
|
|
tab_content = gr.Markdown(elem_classes=["invest-suggest-md-box"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_tab_content(company): |
|
|
if company: |
|
|
|
|
|
loading_html = f''' |
|
|
<div style="display: flex; justify-content: center; align-items: center; height: 200px;"> |
|
|
<div style="text-align: center;"> |
|
|
<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> |
|
|
<p style="margin-top: 20px; color: #666;">Loading investment suggestions for {company}...</p> |
|
|
<style> |
|
|
@keyframes spin {{ |
|
|
0% {{ transform: rotate(0deg); }} |
|
|
100% {{ transform: rotate(360deg); }} |
|
|
}} |
|
|
</style> |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
yield loading_html |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
stock_code = get_stock_code_by_company_name(company) |
|
|
yield query_company_advanced(stock_code, "suggestion") |
|
|
|
|
|
except Exception as e: |
|
|
error_html = f''' |
|
|
<div style="padding: 20px; text-align: center; color: #666;"> |
|
|
<p>Error loading investment suggestions: {str(e)}</p> |
|
|
<p>Please try again later.</p> |
|
|
</div> |
|
|
''' |
|
|
yield error_html |
|
|
else: |
|
|
yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>" |
|
|
|
|
|
selected_company_state.change( |
|
|
fn=update_tab_content, |
|
|
inputs=[selected_company_state], |
|
|
outputs=[tab_content], |
|
|
) |
|
|
with gr.TabItem("Analysis Report", elem_classes=["tab-item"]): |
|
|
|
|
|
|
|
|
|
|
|
analysis_tab_content = gr.Markdown(elem_classes=["analysis-report-md-box"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_analysis_tab_content(company): |
|
|
if company: |
|
|
|
|
|
loading_html = f''' |
|
|
<div style="display: flex; justify-content: center; align-items: center; height: 200px;"> |
|
|
<div style="text-align: center;"> |
|
|
<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> |
|
|
<p style="margin-top: 20px; color: #666;">Loading analysis report for {company}...</p> |
|
|
<style> |
|
|
@keyframes spin {{ |
|
|
0% {{ transform: rotate(0deg); }} |
|
|
100% {{ transform: rotate(360deg); }} |
|
|
}} |
|
|
</style> |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
yield loading_html |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
|
|
|
|
|
|
stock_code = get_stock_code_by_company_name(company) |
|
|
|
|
|
|
|
|
|
|
|
yield query_company_advanced(stock_code, "report") |
|
|
except Exception as e: |
|
|
error_html = f''' |
|
|
<div style="padding: 20px; text-align: center; color: #666;"> |
|
|
<p>Error loading analysis report: {str(e)}</p> |
|
|
<p>Please try again later.</p> |
|
|
</div> |
|
|
''' |
|
|
yield error_html |
|
|
else: |
|
|
yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>" |
|
|
|
|
|
selected_company_state.change( |
|
|
fn=update_analysis_tab_content, |
|
|
inputs=[selected_company_state], |
|
|
outputs=[analysis_tab_content] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Column(scale=2, min_width=400): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gr.ChatInterface( |
|
|
respond, |
|
|
title="Easy Financial Report", |
|
|
additional_inputs=[ |
|
|
gr.State(value=""), |
|
|
gr.State(value={}) |
|
|
], |
|
|
additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
company_list_component.change( |
|
|
fn=lambda x: x, |
|
|
inputs=[company_list_component], |
|
|
outputs=[selected_company_state], |
|
|
concurrency_limit=None |
|
|
) |
|
|
|
|
|
|
|
|
def update_metrics_dashboard_wrapper(company_name): |
|
|
if company_name: |
|
|
|
|
|
loading_html = f''' |
|
|
<div style="display: flex; justify-content: center; align-items: center; height: 300px;"> |
|
|
<div style="text-align: center;"> |
|
|
<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> |
|
|
<p style="margin-top: 20px; color: #666;">Loading financial data for {company_name}...</p> |
|
|
<style> |
|
|
@keyframes spin {{ |
|
|
0% {{ transform: rotate(0deg); }} |
|
|
100% {{ transform: rotate(360deg); }} |
|
|
}} |
|
|
</style> |
|
|
</div> |
|
|
</div> |
|
|
''' |
|
|
yield loading_html, loading_html, loading_html |
|
|
|
|
|
|
|
|
try: |
|
|
stock_card_html, financial_metrics_html, income_table_html = update_metrics_dashboard(company_name) |
|
|
yield stock_card_html, financial_metrics_html, income_table_html |
|
|
except Exception as e: |
|
|
error_html = f''' |
|
|
<div style="padding: 20px; text-align: center; color: #666;"> |
|
|
<p>Error loading financial data: {str(e)}</p> |
|
|
<p>Please try again later.</p> |
|
|
</div> |
|
|
''' |
|
|
yield error_html, error_html, error_html |
|
|
else: |
|
|
|
|
|
empty_html = "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>" |
|
|
yield empty_html, empty_html, empty_html |
|
|
|
|
|
selected_company_state.change( |
|
|
fn=update_metrics_dashboard_wrapper, |
|
|
inputs=[selected_company_state], |
|
|
outputs=list(metrics_dashboard_components), |
|
|
concurrency_limit=None |
|
|
) |
|
|
|
|
|
return demo |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo = main() |
|
|
demo.launch(share=True) |