|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
from src.data_loader import ( |
|
|
load_sdg_data, |
|
|
get_country_list, |
|
|
filter_data, |
|
|
get_latest_data, |
|
|
get_country_metrics, |
|
|
get_data_summary, |
|
|
) |
|
|
from src.viz_engine import ( |
|
|
create_world_map, |
|
|
create_radar_chart, |
|
|
create_trend_chart, |
|
|
create_detailed_trend_chart, |
|
|
) |
|
|
from src.ai_engine import SDG_AI_Report_Engine |
|
|
import os |
|
|
import re |
|
|
|
|
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) |
|
|
|
|
|
from src.config_manager import get_config |
|
|
from datetime import datetime |
|
|
|
|
|
|
|
|
config = get_config() |
|
|
|
|
|
|
|
|
default_sdr2025 = os.path.join(SCRIPT_DIR, 'data', 'SDR2025-data.xlsx') |
|
|
DATA_PATH = config.get('data_sources.primary_data_path') or default_sdr2025 |
|
|
if not DATA_PATH or not os.path.exists(DATA_PATH): |
|
|
if os.path.exists(default_sdr2025): |
|
|
DATA_PATH = default_sdr2025 |
|
|
|
|
|
|
|
|
config_valid = config.get('ai_engine.enabled', True) and (config.get('ai_engine.api_key') or config.get('ai_engine.base_url')) |
|
|
ai_engine = SDG_AI_Report_Engine(config.get('ai_engine.base_url'), config.get('ai_engine.api_key')) if config_valid else None |
|
|
|
|
|
|
|
|
SELECTED_MODEL_KEY = 'azure-gpt-4.1' |
|
|
|
|
|
@st.cache_data(ttl=config.get('data_sources.cache_ttl_seconds', 3600)) |
|
|
def get_data(): |
|
|
df = load_sdg_data(DATA_PATH) |
|
|
summary = get_data_summary(df) |
|
|
return df, summary |
|
|
|
|
|
df, data_summary = get_data() |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title=config.get('ui.page_title', 'Global SDG Tracker AI'), |
|
|
page_icon=config.get('ui.page_icon', '🌍'), |
|
|
layout=config.get('ui.layout', 'wide'), |
|
|
) |
|
|
|
|
|
|
|
|
css_path = os.path.join(SCRIPT_DIR, 'assets', 'style.css') |
|
|
if os.path.exists(css_path): |
|
|
with open(css_path) as f: |
|
|
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.markdown('### 🌍 Global Settings') |
|
|
|
|
|
|
|
|
country_list = get_country_list(df) |
|
|
if not country_list: |
|
|
st.warning('No countries available in the data source. Please check your data configuration.') |
|
|
selected_country = st.selectbox('Select Country', [], key='country_select') |
|
|
else: |
|
|
selected_country = st.selectbox('Select Country', country_list, index=0, key='country_select') |
|
|
|
|
|
|
|
|
if data_summary and data_summary.get('year_range'): |
|
|
min_year, max_year = data_summary['year_range'] |
|
|
else: |
|
|
min_year, max_year = 2000, 2025 |
|
|
|
|
|
|
|
|
if min_year == max_year: |
|
|
|
|
|
year_range = (min_year, max_year) |
|
|
st.info(f'📅 Data available for year: {min_year} (single year dataset)') |
|
|
else: |
|
|
|
|
|
default_start = max(min_year, max_year - 10) |
|
|
year_range = st.slider('Year Range', min_year, max_year, (default_start, max_year), key='year_slider') |
|
|
|
|
|
st.divider() |
|
|
st.markdown('### 📊 Project Info') |
|
|
st.info('Global SDG Tracker AI v2025.1\nData: SDR 2025 (SDSN)') |
|
|
|
|
|
|
|
|
tab_names = ['Global Overview', 'Country Analysis', 'AI Report'] |
|
|
if 'active_tab' not in st.session_state: |
|
|
st.session_state['active_tab'] = tab_names[0] |
|
|
|
|
|
st.session_state['active_tab'] = st.radio('Navigation', tab_names, index=tab_names.index(st.session_state['active_tab']), horizontal=True, label_visibility='collapsed') |
|
|
active_tab = st.session_state['active_tab'] |
|
|
|
|
|
if active_tab == 'Global Overview': |
|
|
st.header('Global SDG Progress (SDR 2025)') |
|
|
latest_df = get_latest_data(df) |
|
|
fig_map = create_world_map(latest_df) |
|
|
st.plotly_chart(fig_map, use_container_width=True) |
|
|
|
|
|
st.subheader('Top Performers (Latest Year, SDR 2025)') |
|
|
top_10 = latest_df.sort_values('sdg_index_score', ascending=False).head(10)[['country', 'sdg_index_score']] |
|
|
st.table(top_10) |
|
|
|
|
|
elif active_tab == 'Country Analysis': |
|
|
st.header(f'Projected Performance: {selected_country} (SDR 2025)') |
|
|
|
|
|
latest_year = year_range[1] |
|
|
metrics = get_country_metrics(df, selected_country, latest_year) |
|
|
|
|
|
if not metrics: |
|
|
st.warning(f"⚠️ Data for '{selected_country}' in {latest_year} is not available or incomplete.") |
|
|
else: |
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
|
|
prev_year = latest_year - 1 |
|
|
prev_metrics = get_country_metrics(df, selected_country, prev_year) |
|
|
score_delta = (metrics.get('score', 0) - prev_metrics.get('score', 0)) if prev_metrics else 0 |
|
|
|
|
|
col1.metric('Latest SDG Score', f"{metrics.get('score', 0):.1f}", delta=f"{score_delta:+.1f}") |
|
|
col2.metric('Global Rank', f"{metrics.get('rank', 'N/A')} / {metrics.get('country_count', 'N/A')}") |
|
|
col3.metric('Global Average', f"{metrics.get('global_avg', 0):.1f}") |
|
|
diff = metrics.get('score', 0) - metrics.get('global_avg', 0) |
|
|
col4.metric('Performance vs Avg', f"{diff:+.1f}", delta_color='normal') |
|
|
|
|
|
filtered_df = filter_data(df, selected_country, year_range) |
|
|
|
|
|
col_left, col_right = st.columns([1, 1]) |
|
|
with col_left: |
|
|
st.subheader('SDG Goal Multi-dimensional Analysis') |
|
|
fig_radar = create_radar_chart(df, selected_country, latest_year) |
|
|
if fig_radar: |
|
|
st.plotly_chart(fig_radar, use_container_width=True) |
|
|
|
|
|
with col_right: |
|
|
st.subheader('Historical Trend') |
|
|
fig_trend = create_trend_chart(filtered_df) |
|
|
st.plotly_chart(fig_trend, use_container_width=True) |
|
|
|
|
|
st.subheader('Detailed Goals Trends') |
|
|
fig_det_trend = create_detailed_trend_chart(filtered_df) |
|
|
st.plotly_chart(fig_det_trend, use_container_width=True) |
|
|
|
|
|
|
|
|
st.divider() |
|
|
csv = filtered_df.to_csv(index=False).encode('utf-8') if not filtered_df.empty else b'' |
|
|
st.download_button(label='📥 Export Country Data to CSV', data=csv, file_name=f'SDG_Data_{selected_country}_{latest_year}.csv', mime='text/csv') |
|
|
|
|
|
elif active_tab == 'AI Report': |
|
|
st.header('🤖 AI-Powered Strategic Analysis (SDR 2025)') |
|
|
|
|
|
|
|
|
latest_year = year_range[1] |
|
|
metrics = get_country_metrics(df, selected_country, latest_year) |
|
|
filtered_df = filter_data(df, selected_country, year_range) |
|
|
|
|
|
|
|
|
if metrics is None or filtered_df.empty: |
|
|
st.error(f"⚠️ 國家 '{selected_country}' 在 {latest_year} 年 SDG 數據中不存在或無完整資料,請選擇其他國家(例如 Finland、Sweden、China 等)。") |
|
|
st.stop() |
|
|
|
|
|
if not config_valid: |
|
|
st.warning('⚠️ AI report functionality is not configured. Set environment variables to enable it.') |
|
|
else: |
|
|
|
|
|
col_lang, col_depth = st.columns(2) |
|
|
with col_lang: |
|
|
report_lang = st.selectbox('Language / 報告語言', ['Chinese', 'English', 'Japanese'], key='lang_select') |
|
|
with col_depth: |
|
|
report_depth = st.selectbox('Report Depth / 報告深度', ['Short', 'Standard', 'Detailed'], index=0, key='depth_select') |
|
|
depth_map = {'Short': 300, 'Standard': 800, 'Detailed': 1500} |
|
|
selected_length = depth_map[report_depth] |
|
|
|
|
|
|
|
|
if selected_length == 1500: |
|
|
selected_type = 'professional' |
|
|
elif selected_length == 800: |
|
|
selected_type = 'policy' |
|
|
else: |
|
|
selected_type = 'summary' |
|
|
|
|
|
if st.button('🤖 Generate Professional Report / 生成專業報告', help='點擊生成基於AI的專業SDG分析報告'): |
|
|
try: |
|
|
with st.spinner('AI is analyzing SDG data and generating strategic insights...'): |
|
|
meta = { |
|
|
'country': selected_country, |
|
|
'start_year': year_range[0], |
|
|
'end_year': year_range[1], |
|
|
'report_type': selected_type, |
|
|
'latest_score': f"{metrics.get('score', 'N/A'):.1f}" if isinstance(metrics.get('score'), (int, float)) else metrics.get('score', 'N/A'), |
|
|
'rank': metrics.get('rank', 'N/A'), |
|
|
'total_countries': metrics.get('country_count', 'N/A'), |
|
|
'global_avg': f"{metrics.get('global_avg', 'N/A'):.1f}" if isinstance(metrics.get('global_avg'), (int, float)) else metrics.get('global_avg', 'N/A'), |
|
|
'length': selected_length if 'selected_length' in locals() else 800 |
|
|
} |
|
|
|
|
|
report = ai_engine.generate_report(filtered_df, meta, language=report_lang) |
|
|
|
|
|
if isinstance(report, str): |
|
|
|
|
|
report = re.sub(r'<!--.*?-->', '', report, flags=re.DOTALL).strip() |
|
|
|
|
|
st.session_state['current_report'] = report |
|
|
except Exception as e: |
|
|
st.error(f"❌ AI報告生成失敗:{str(e)}") |
|
|
|
|
|
if 'current_report' in st.session_state: |
|
|
st.divider() |
|
|
st.subheader(f"📝 策略分析報告: {selected_country}") |
|
|
|
|
|
|
|
|
with st.container(border=True): |
|
|
st.markdown(st.session_state['current_report']) |
|
|
|
|
|
|
|
|
report_text = st.session_state['current_report'] |
|
|
st.download_button( |
|
|
label="📥 下載完整報告 (Markdown)", |
|
|
data=report_text, |
|
|
file_name=f"SDG_Report_{selected_country}_{datetime.now().strftime('%Y%V%d')}.md", |
|
|
mime="text/markdown" |
|
|
) |
|
|
|
|
|
|
|
|
st.divider() |
|
|
st.markdown( |
|
|
""" |
|
|
<div class="footer"> |
|
|
<p><b>Global SDG Tracker AI v2025.1</b></p> |
|
|
<p>Developed by Senior AI & Environmental Systems Engineer</p> |
|
|
<p>Data Source: <b>Sustainable Development Report 2025 (SDSN & Dublin University Press)</b> | Released: June 2025</p> |
|
|
<p><i>Note: The 2025 data follows the latest UN SDG indicator framework and official SDSN computations.</i></p> |
|
|
</div> |
|
|
""", |
|
|
unsafe_allow_html=True, |
|
|
) |
|
|
|