import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import folium
from streamlit_folium import st_folium
from folium.plugins import HeatMap
import requests
from openai import OpenAI
import json
import ast
from io import BytesIO
import random
from streamlit_option_menu import option_menu
# ---------------------------------------------------------
# 1. 页面配置与 CSS 样式 (云墨·太白风格)
# ---------------------------------------------------------
st.set_page_config(
page_title="云墨·太白 | 李白情感GIS与RAG系统",
page_icon="🍶",
layout="wide",
initial_sidebar_state="expanded"
)
# 自定义 CSS 注入 - 水墨风格
def local_css():
st.markdown("""
""", unsafe_allow_html=True)
local_css()
# ---------------------------------------------------------
# 2. 数据准备与功能函数
# ---------------------------------------------------------
# 初始化OpenAI客户端
client = OpenAI(
api_key="sk-72997944466a4af2bcd52a068895f8cf",
base_url="https://api.deepseek.com"
)
# 全局变量与配置 - 更新数据源
AI_DATA_URL = "https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/libai_location.xlsx"
EMOTION_DATA_URL = "https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/%E6%9D%8E%E7%99%BD%E8%AF%97%E6%AD%8C%E6%95%B0%E6%8D%AE%E6%95%B4%E7%90%86%20%E5%B9%B4%E4%BB%BD%2B%E5%9C%B0%E7%82%B9%2B%E7%AE%80%E4%BD%93%2B%E7%BB%8F%E7%BA%AC%E5%BA%A6%2B%E6%83%85%E6%84%9F%EF%BC%88%E7%AE%80%E4%BD%93%2B%E7%B9%81%E4%BD%93%E6%A0%87%E9%A2%98%2B%E7%B9%81%E4%BD%93%E6%AD%A3%E6%96%87%EF%BC%89.xlsx"
location_col = '地点(古称/今称)'
summary_col = '诗作/事件摘要'
LOCATION_COORDS = {
"碎叶城": {"lat": 42.8447, "lon": 75.1648, "match_keys": ["碎叶城"]},
"峨眉山": {"lat": 29.5807, "lon": 103.3592, "match_keys": ["峨眉山"]},
"蜀中": {"lat": 31.7828, "lon": 104.7570, "match_keys": ["蜀中", "江油"]},
"荆门/南津关": {"lat": 30.5667, "lon": 111.4500, "match_keys": ["荆门", "南津关"]},
"岳阳楼": {"lat": 29.3879, "lon": 113.1092, "match_keys": ["岳阳楼", "岳阳"]},
"安陆": {"lat": 31.3653, "lon": 113.7077, "match_keys": ["安陆"]},
"黄鹤楼": {"lat": 30.5484, "lon": 114.3168, "match_keys": ["黄鹤楼", "武汉"]},
"金陵(凤凰台)": {"lat": 32.0415, "lon": 118.7781, "match_keys": ["金陵", "凤凰台", "南京"]},
"庐山": {"lat": 29.5910, "lon": 115.9922, "match_keys": ["庐山", "九江"]},
"天姥山": {"lat": 29.5000, "lon": 120.8900, "match_keys": ["天姥山"]},
"金陵/长干里": {"lat": 32.0298, "lon": 118.7900, "match_keys": ["长干里"]},
"长安": {"lat": 34.2652, "lon": 108.9500, "match_keys": ["长安", "西安"]},
"长安/宫廷": {"lat": 34.2652, "lon": 108.9500, "match_keys": ["宫廷"]},
"长安/洛阳": {"lat": 34.6859, "lon": 112.4600, "match_keys": ["洛阳"]},
"桃花潭": {"lat": 30.4079, "lon": 118.4230, "match_keys": ["桃花潭", "泾县"]},
"敬亭山": {"lat": 30.9822, "lon": 118.7844, "match_keys": ["敬亭山", "宣城"]},
"天门山": {"lat": 31.4285, "lon": 118.3970, "match_keys": ["天门山", "芜湖"]},
"扬州/旅店": {"lat": 32.3934, "lon": 119.4290, "match_keys": ["扬州"]},
"夜郎": {"lat": 27.6888, "lon": 106.3773, "match_keys": ["夜郎", "桐梓"]},
"白帝城": {"lat": 31.0450, "lon": 109.5780, "match_keys": ["白帝城", "奉节"]},
"秋浦": {"lat": 30.6500, "lon": 117.4800, "match_keys": ["秋浦", "池州"]},
"当涂": {"lat": 31.5453, "lon": 118.4870, "match_keys": ["当涂", "马鞍山"]},
}
# 数据加载函数 - 增强错误处理
@st.cache_data(ttl=3600, show_spinner="正在从 GitHub 下载AI对话数据...")
def load_ai_data():
try:
response = requests.get(AI_DATA_URL, timeout=30)
response.raise_for_status()
df = pd.read_excel(BytesIO(response.content), sheet_name=0)
df.columns = df.columns.str.strip()
# 检查必要列是否存在
if location_col not in df.columns:
st.error(f"❌ 数据中缺少必要的列: {location_col}")
return pd.DataFrame()
except Exception as e:
st.error(f"❌ AI对话数据下载失败: {str(e)}")
st.info("💡 提示: 请检查网络连接或数据文件是否可访问")
return pd.DataFrame()
df['coords_key'] = '未知'
df['Latitude'] = 34.0478
df['Longitude'] = 108.4357
for idx, row in df.iterrows():
location_str = str(row.get(location_col, '')).strip()
for key, data in LOCATION_COORDS.items():
if location_str == key or any(k in location_str for k in data.get('match_keys', [])):
df.at[idx, 'coords_key'] = key
df.at[idx, 'Latitude'] = data['lat']
df.at[idx, 'Longitude'] = data['lon']
break
return df
@st.cache_data(ttl=3600, show_spinner="正在从 GitHub 下载情感数据...")
def load_emotion_data_from_github():
try:
response = requests.get(EMOTION_DATA_URL, timeout=30)
response.raise_for_status()
df = pd.read_excel(BytesIO(response.content))
df.columns = df.columns.astype(str).str.strip()
def get_first_matching_col(keywords):
for col in df.columns:
if any(k in col.lower() for k in keywords): return col
return None
rename_map = {}
if c := get_first_matching_col(['经', 'lon', 'longitude']): rename_map[c] = 'Longitude'
if c := get_first_matching_col(['纬', 'lat', 'latitude']): rename_map[c] = 'Latitude'
if c := get_first_matching_col(['year', '年', 'time']): rename_map[c] = 'Year'
if c := get_first_matching_col(['诗名', 'title', '题', '标题', 'name', '诗歌']): rename_map[c] = 'Title'
if c := get_first_matching_col(['地点', 'location', 'place', 'city']): rename_map[c] = 'Location'
emo_col = get_first_matching_col(['emotion_top3', 'top3']) or get_first_matching_col(['emotion', '情', 'sentiment'])
if emo_col: rename_map[emo_col] = 'Emotion_Raw'
df = df.rename(columns=rename_map)
required_cols = ['Title', 'Location', 'Emotion', 'Year', 'Latitude', 'Longitude']
for col in required_cols:
if col not in df.columns:
df[col] = '未知'
def extract_primary_emotion(val):
try:
if isinstance(val, str):
parsed = ast.literal_eval(val)
if isinstance(parsed, list) and len(parsed) > 0: return parsed[0][0]
elif isinstance(val, list) and len(val) > 0: return val[0][0]
except: pass
return str(val).split(' ')[0] if val else "未知"
if 'Emotion_Raw' in df.columns:
df['Emotion'] = df['Emotion_Raw'].apply(extract_primary_emotion)
df = df.dropna(subset=['Latitude', 'Longitude'])
return df
except Exception as e:
st.error(f"❌ 情感数据下载失败: {str(e)}")
return pd.DataFrame()
# 模拟数据函数(用于保持原有UI)
@st.cache_data
def get_travel_data():
data = {
'地点': ['长安', '成都', '洛阳', '金陵 (南京)', '扬州', '庐山', '宣城'],
'lat': [34.3416, 30.5728, 34.6197, 32.0603, 32.3945, 29.5643, 30.9407],
'lon': [108.9398, 104.0668, 112.4540, 118.7969, 119.4122, 115.9881, 118.7587],
'诗作数': [50, 20, 35, 45, 30, 15, 25],
'代表作': ['长相思', '蜀道难', '春夜洛城闻笛', '登金陵凤凰台', '黄鹤楼送孟浩然之广陵', '望庐山瀑布', '独坐敬亭山']
}
return pd.DataFrame(data)
@st.cache_data
def get_emotion_data():
return pd.DataFrame({
'意象': ['月亮', '酒', '剑', '水', '山', '花', '孤', '梦'],
'频率': [120, 95, 40, 85, 110, 60, 55, 30],
'情感色彩': ['思乡/孤独', '豪迈/解忧', '侠客/抱负', '流逝/愁苦', '归隐/壮阔', '美好/易逝', '寂寞', '虚幻']
})
# RAG Chatbot 函数
@st.cache_data(ttl=3600)
def get_cbdb_data(name="李白"):
try:
url = f"https://cbdb.fas.harvard.edu/cbdbapi/person.php?name={name}&o=json"
r = requests.get(url, headers={"User-Agent": "Streamlit App"}, timeout=5)
return r.json() if r.status_code == 200 else None
except: return None
def generate_poem_analysis(year, location, emotion, title, cbdb_data):
cbdb_text = json.dumps(cbdb_data, ensure_ascii=False)[:1000] if cbdb_data else "无"
system_prompt = (
"你是一位精通唐代文学与李白生平的专家AI。\n"
f"参考史料:{cbdb_text}\n"
"任务:用户将提供李白的一首诗及其背景(年份、地点、情感标签)。\n"
"请按以下格式输出(使用Markdown):\n"
"### 📜 全诗呈现\n"
"(请默写全诗,若不确定则注明)\n\n"
"### 🎭 情感深度解析\n"
"(结合标签分析诗句如何体现该情感)\n\n"
"### 🌍 时空与历史背景\n"
f"(简述李白在{year}年于{location}的人生境遇)\n"
)
user_prompt = f"请分析李白在 {year} 年,于 {location} 创作的《{title}》。该诗被标记为【{emotion}】情感。"
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.7
)
return response.choices[0].message.content.strip()
except Exception as e:
return f"AI 分析服务暂时不可用: {str(e)}"
def run_main_chatbot(cbdb_data, prompt):
if not prompt: return "请输入有效的问题"
cbdb_text = json.dumps(cbdb_data, ensure_ascii=False)[:3000] if cbdb_data else "无CBDB资料"
system_prompt = f"你是李白研究专家。史料参考:{cbdb_text}"
try:
messages = [{"role": "system", "content": system_prompt}]
messages.extend([msg for msg in st.session_state.chat_history[-5:] if msg.get("role") in ["user", "assistant"]])
messages.append({"role": "user", "content": prompt})
response = client.chat.completions.create(model="deepseek-chat", messages=messages, temperature=0.7)
answer = response.choices[0].message.content.strip()
highlight_key = None
if not st.session_state.ai_data_df.empty:
for key in st.session_state.ai_data_df['coords_key'].unique():
if key != '未知' and key in answer:
highlight_key = key
break
st.session_state.highlight_location_key = highlight_key
return answer
except Exception as e:
return f"Chatbot错误:{str(e)}"
# 地图绘制函数 - 中文版 (用于AI对话页面)
def create_main_map(df, highlight_key):
if df.empty:
return folium.Map(location=[34.0, 108.0], zoom_start=4)
try:
center_lat = df['Latitude'].mean()
center_lon = df['Longitude'].mean()
except:
center_lat, center_lon = 34.0, 108.0
# 使用标准地图底图
m = folium.Map(
location=[center_lat, center_lon],
zoom_start=4.5
)
points = df[['Latitude', 'Longitude']].dropna().values.tolist()
if len(points) > 1:
folium.PolyLine(points, color="#00AEEF", weight=3, opacity=0.5).add_to(m)
for idx, row in df.iterrows():
try:
if pd.isna(row['Latitude']): continue
is_highlighted = (row['coords_key'] == highlight_key)
color = 'orange' if is_highlighted else 'blue'
icon = 'fire' if is_highlighted else 'user'
# 创建详细的信息弹窗
popup_html = f"""
{row.get(location_col, '未知地点')}
📅 时间: {row.get('时间', '未知')}
📖 诗作/事件: {row.get(summary_col, '未知')}
📍 坐标: {row['Latitude']:.4f}, {row['Longitude']:.4f}
"""
folium.Marker(
[row['Latitude'], row['Longitude']],
popup=folium.Popup(popup_html, max_width=300),
icon=folium.Icon(color=color, icon=icon, prefix='fa'),
tooltip=row.get(location_col, '未知地点')
).add_to(m)
except: continue
return m
# 新增:时序地图函数 - 改进版 (用于足迹漫游页面)
def create_temporal_map(df, selected_year):
if df.empty:
return folium.Map(location=[34.0, 108.0], zoom_start=4)
try:
center_lat = df['Latitude'].mean()
center_lon = df['Longitude'].mean()
except:
center_lat, center_lon = 34.0, 108.0
# 使用标准地图底图
m = folium.Map(
location=[center_lat, center_lon],
zoom_start=4.5
)
# 过滤出选定年份及之前的所有数据点
filtered_df = df[df['Year'] <= selected_year]
# 按地点和年份分组,显示同年同地的所有诗名
grouped_data = filtered_df.groupby(['Latitude', 'Longitude', 'Location', 'Year']).agg({
'Title': lambda x: list(x.unique())
}).reset_index()
# 添加轨迹线(按时间顺序)
if len(filtered_df) > 1:
points = filtered_df.sort_values('Year')[['Latitude', 'Longitude']].values.tolist()
folium.PolyLine(points, color="#00AEEF", weight=3, opacity=0.5, popup=f"截至 {selected_year} 年的游历路线").add_to(m)
# 为每个地点添加标记
for idx, row in grouped_data.iterrows():
try:
if pd.isna(row['Latitude']): continue
# 根据年份设置颜色渐变(越晚越红)
year_norm = (row['Year'] - df['Year'].min()) / (df['Year'].max() - df['Year'].min()) if df['Year'].max() != df['Year'].min() else 0.5
red = int(255 * year_norm)
blue = int(255 * (1 - year_norm))
color = f'#{red:02x}00{blue:02x}'
# 显示同年同地的所有诗名
poem_list = "
".join([f"• {poem}" for poem in row['Title'][:5]]) # 最多显示5首诗
if len(row['Title']) > 5:
poem_list += f"
• ...等 {len(row['Title'])} 首诗"
popup_html = f"""
{row.get('Location', '未知地点')}
📅 年份: {int(row['Year'])}
📖 同年诗作:
{poem_list}
"""
folium.CircleMarker(
location=[row['Latitude'], row['Longitude']],
radius=8,
popup=folium.Popup(popup_html, max_width=300),
color=color,
fill=True,
fillColor=color,
fillOpacity=0.7,
weight=2,
tooltip=f"{row.get('Location', '未知地点')} ({int(row['Year'])})"
).add_to(m)
# 添加文字标注
folium.Marker(
location=[row['Latitude'] + 0.1, row['Longitude'] + 0.1],
icon=folium.DivIcon(
html=f'{int(row["Year"])}
'
)
).add_to(m)
except Exception as e:
continue
# 添加当前年份的标题
title_html = f'''
李白足迹时序图 (截至 {selected_year} 年)
'''
m.get_root().html.add_child(folium.Element(title_html))
return m
def create_emotion_heatmap(df, period_name):
if df.empty:
return folium.Map(location=[34.0, 108.0], zoom_start=4)
try:
center_lat = df['Latitude'].mean()
center_lon = df['Longitude'].mean()
except: center_lat, center_lon = 34.0, 108.0
# 使用标准地图底图
m = folium.Map(
location=[center_lat, center_lon],
zoom_start=5
)
heatmap_gradients = {
"豪放与激昂": {0.2: 'orange', 0.6: 'red', 1.0: 'darkred'},
"喜悦与欢快": {0.2: 'yellow', 0.6: 'orange', 1.0: '#d35400'},
"哀怨与悲伤": {0.2: 'cyan', 0.6: 'blue', 1.0: 'navy'},
"忧愁与苦闷": {0.2: 'lightblue', 0.6: 'royalblue', 1.0: '#1a5276'},
"孤独与寂寞": {0.2: 'plum', 0.6: 'purple', 1.0: '#4a235a'},
"思乡与怀古": {0.2: '#d7bde2', 0.6: '#8e44ad', 1.0: '#5b2c6f'},
"友情与知己": {0.2: 'lightgreen', 0.6: 'green', 1.0: 'darkgreen'},
"闲适与隐逸": {0.2: '#a3e4d7', 0.6: '#16a085', 1.0: '#0e6251'},
"未知": {0.4: 'gray', 0.8: 'white', 1.0: 'white'}
}
marker_colors = {
"豪放与激昂": "#e74c3c", "喜悦与欢快": "#e67e22", "哀怨与悲伤": "#3498db",
"忧愁与苦闷": "#2980b9", "孤独与寂寞": "#9b59b6", "思乡与怀古": "#8e44ad",
"友情与知己": "#2ecc71", "闲适与隐逸": "#1abc9c", "未知": "#95a5a6"
}
unique_emotions = df['Emotion'].fillna("未知").unique()
for emotion in unique_emotions:
fg = folium.FeatureGroup(name=str(emotion))
subset = df[df['Emotion'] == emotion]
if subset.empty: continue
heat_data = [[row['Latitude'], row['Longitude'], 1] for _, row in subset.iterrows()]
HeatMap(heat_data, radius=20, blur=15, min_opacity=0.4, gradient=heatmap_gradients.get(emotion, None), name=f"{emotion} (热力)").add_to(fg)
marker_color = marker_colors.get(emotion, "#ecf0f1")
for _, row in subset.iterrows():
folium.CircleMarker(
location=[row['Latitude'], row['Longitude']], radius=3, color=marker_color,
fill=True, fill_color=marker_color, fill_opacity=0.8, weight=0,
popup=folium.Popup(f"{row.get('Title', '无题')}
{emotion}", max_width=200),
tooltip=f"{row.get('Title', '无题')}"
).add_to(fg)
fg.add_to(m)
folium.LayerControl(collapsed=False).add_to(m)
return m
# AI分析卡片组件
def render_ai_analysis_card(df, period_name):
st.markdown("---")
st.subheader("🤖 智能诗歌检索与情感解析")
st.caption("选择下方的年份、地点与情感,AI 将为您深度解读李白的心境。")
with st.container():
col1, col2, col3, col4 = st.columns([1, 1, 1, 1])
available_years = sorted(df['Year'].dropna().unique())
with col1:
selected_year = st.selectbox("1️⃣ 选择年份", available_years, key=f"year_{period_name}")
year_subset = df[df['Year'] == selected_year]
available_locs = sorted(year_subset['Location'].dropna().unique())
with col2:
selected_loc = st.selectbox("2️⃣ 选择地点", available_locs, key=f"loc_{period_name}")
loc_subset = year_subset[year_subset['Location'] == selected_loc]
available_emotions = sorted(loc_subset['Emotion'].dropna().unique())
with col3:
selected_emotion = st.selectbox("3️⃣ 选择情感", available_emotions, key=f"emo_{period_name}")
final_subset = loc_subset[loc_subset['Emotion'] == selected_emotion]
available_titles = sorted(final_subset['Title'].astype(str).unique().tolist())
with col4:
if not available_titles:
st.warning("该组合下暂无数据")
selected_title = None
else:
selected_title = st.selectbox("4️⃣ 选择诗歌", available_titles, key=f"title_{period_name}")
if st.button("✨ 生成 AI 深度解析", key=f"btn_{period_name}", use_container_width=True):
if selected_title and selected_title != 'nan' and selected_title != '未知':
with st.spinner(f"DeepSeek 正在阅读《{selected_title}》并分析历史背景..."):
cbdb_data = get_cbdb_data("李白")
analysis = generate_poem_analysis(selected_year, selected_loc, selected_emotion, selected_title, cbdb_data)
st.markdown("---")
st.success("✅ 分析完成")
with st.container():
st.markdown(analysis)
else:
st.error("请先选择一首有效的诗歌。")
# ---------------------------------------------------------
# 3. 侧边栏导航 (云墨风格) - 调整顺序,太白生平在首页
# ---------------------------------------------------------
with st.sidebar:
st.image("https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/libai.jpg",
width=150, caption="诗仙·李白")
selected = option_menu(
"导航",
["太白生平", "AI对话", "情感图谱", "足迹漫游", "与仙对饮"], # 太白生平在首页
icons=['book', 'robot', 'bar-chart', 'map', 'chat-quote'], # 对应调整图标顺序
menu_icon="cast", default_index=0, # 默认选择太白生平
styles={
"container": {"padding": "0!important", "background-color": "transparent"},
"icon": {"color": "#2c3e50", "font-size": "18px"},
"nav-link": {"font-size": "16px", "text-align": "left", "margin":"0px", "--hover-color": "#eee"},
"nav-link-selected": {"background-color": "#2c3e50"},
}
)
st.markdown("---")
st.markdown("### 🎵 听琴")
st.audio("https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/chunjianghuayueye.mp3")
st.caption("古琴曲:春江花月夜")
# ---------------------------------------------------------
# 4. 主页面逻辑
# ---------------------------------------------------------
# 初始化session state
if "chat_history" not in st.session_state:
st.session_state.chat_history = []
if "highlight_location_key" not in st.session_state:
st.session_state.highlight_location_key = None
if "ai_data_df" not in st.session_state:
st.session_state.ai_data_df = load_ai_data()
if "selected_year" not in st.session_state:
st.session_state.selected_year = 701 # 李白出生年份
# --- 页面 1: 太白生平 (首页) ---
if selected == "太白生平":
st.title("☁️ 谪仙人:李白")
st.markdown("**\"绣口一吐,就半个盛唐。\"**")
col1, col2 = st.columns([1, 2])
with col1:
st.info("字:太白")
st.info("号:青莲居士")
st.info("朝代:唐朝")
st.info("评价:诗仙")
with col2:
st.markdown("""
君不见,黄河之水天上来,奔流到海不复回。
君不见,高堂明镜悲白发,朝如青丝暮成雪。
人生得意须尽欢,莫使金樽空对月。
""", unsafe_allow_html=True)
st.write("李白(701年-762年),字太白,号青莲居士,又号\"谪仙人\"。他是唐代伟大的浪漫主义诗人,被后人誉为\"诗仙\"。其诗以七言古诗和绝句成就最高,风格豪迈奔放,清新飘逸,想象丰富,意境奇妙,语言奇采,浪漫主义色彩浓厚。")
# --- 页面 2: AI对话 (RAG Chatbot) ---
elif selected == "AI对话":
st.title("🤖 AI太白对话")
st.write("与基于李白知识库的AI智能助手对话,探索诗仙的内心世界。")
cbdb_data = get_cbdb_data("李白")
# 使用李白人生重要节点数据
df_main = st.session_state.ai_data_df
col1, col2 = st.columns([1, 1.5], gap="large")
with col1:
st.subheader("💬 与AI太白对话")
if not cbdb_data:
st.warning("CBDB 连接失败,使用通用知识库。")
# 显示聊天历史
for msg in st.session_state.chat_history:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# 聊天输入
if prompt := st.chat_input("请输入问题(例如:李白在安陆有哪些经历?)"):
st.session_state.chat_history.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
with st.spinner("AI正在思考..."):
answer = run_main_chatbot(cbdb_data, prompt)
st.markdown(answer)
st.session_state.chat_history.append({"role": "assistant", "content": answer})
if st.session_state.highlight_location_key:
st.success(f"🗺️ 地图已高亮:{st.session_state.highlight_location_key}")
with col2:
st.subheader("🗺️ 实时足迹高亮")
if not df_main.empty:
# 创建地图
current_map = create_main_map(df_main, st.session_state.highlight_location_key)
st_folium(current_map, width=700, height=600)
# 显示高亮节点信息
if st.session_state.highlight_location_key:
highlight_df = df_main[df_main['coords_key'] == st.session_state.highlight_location_key]
if not highlight_df.empty:
with st.expander(f"📋 {st.session_state.highlight_location_key} 详情"):
for idx, row in highlight_df.iterrows():
st.markdown(f"""
**📍 {row.get(location_col, '未知地点')}**
- 📅 时间: {row.get('时间', '未知')}
- 📖 诗作/事件: {row.get(summary_col, '未知')}
""")
else:
st.error("❌ AI对话数据加载失败,无法显示地图")
st.info("💡 可能的原因:")
st.info("• 网络连接问题")
st.info("• GitHub 数据文件暂时不可访问")
st.info("• 数据文件格式发生变化")
if st.button("🔄 重新加载数据"):
st.session_state.ai_data_df = load_ai_data()
st.rerun()
# --- 页面 3: 情感图谱 (增强版可视化) ---
elif selected == "情感图谱":
st.title("📊 诗中的情感密码")
# 时期选择
period = st.radio("选择时期:", ["🌱 青年期 (<742年)", "🔥 中年期 (742-755年)", "🍂 晚年期 (>755年)"], horizontal=True)
df_emotion_full = load_emotion_data_from_github()
if not df_emotion_full.empty:
df_emotion_full['Year'] = pd.to_numeric(df_emotion_full['Year'], errors='coerce')
df_emotion_full = df_emotion_full.dropna(subset=['Year'])
if "青年期" in period:
filtered_df = df_emotion_full[df_emotion_full['Year'] < 742]
period_key = "youth"
elif "中年期" in period:
filtered_df = df_emotion_full[(df_emotion_full['Year'] >= 742) & (df_emotion_full['Year'] <= 755)]
period_key = "middle"
else:
filtered_df = df_emotion_full[df_emotion_full['Year'] > 755]
period_key = "old"
st.info(f"共检索到 {len(filtered_df)} 首相关诗作。")
# 情感热力图
emotion_map = create_emotion_heatmap(filtered_df, period_key)
st_folium(emotion_map, width="100%", height=500)
# 传统图表
col1, col2 = st.columns(2)
with col1:
st.subheader("高频意象统计")
df_simple_emotion = get_emotion_data()
fig_pie = px.pie(
df_simple_emotion,
values='频率',
names='意象',
title='李白最爱用的词',
color_discrete_sequence=px.colors.sequential.Teal,
hole=0.4
)
fig_pie.update_layout(paper_bgcolor='rgba(0,0,0,0)')
st.plotly_chart(fig_pie, use_container_width=True)
with col2:
st.subheader("意象背后的情感")
fig_bar = px.bar(
df_simple_emotion,
x='意象',
y='频率',
color='频率',
text='情感色彩',
title='意象与情感关联',
color_continuous_scale='Blues'
)
fig_bar.update_traces(textposition='outside')
fig_bar.update_layout(paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)')
st.plotly_chart(fig_bar, use_container_width=True)
# AI分析卡片
render_ai_analysis_card(filtered_df, period_key)
st.markdown("### 情感解读")
st.markdown("""
> **月亮** 是李白诗中最孤独的伴侣,出现了 120 次以上。它代表了乡愁与超越世俗的渴望。
>
> **酒** 则是他通向自由的钥匙,"百年三万六千日,一日须倾三百杯"。
""")
else:
st.error("❌ 情感数据加载失败")
# --- 页面 4: 足迹漫游 (时序地图可视化) ---
elif selected == "足迹漫游":
st.title("🗺️ 仗剑走天涯")
st.write("李白一生足迹遍布半个中国,从西域碎叶城到长安,从黄河到长江。")
# 使用情感数据用于时序地图
df_emotion = load_emotion_data_from_github()
if not df_emotion.empty:
# 处理年份数据
df_emotion['Year'] = pd.to_numeric(df_emotion['Year'], errors='coerce')
df_emotion = df_emotion.dropna(subset=['Year'])
# 双地图展示
col1, col2 = st.columns(2)
with col1:
st.subheader("📊 诗作分布热力图")
df_travel = get_travel_data()
fig = px.scatter_geo(
df_travel,
lat='lat',
lon='lon',
size='诗作数',
hover_name='地点',
hover_data=['代表作'],
scope='asia',
center=dict(lat=33, lon=110),
projection="natural earth",
color='诗作数',
color_continuous_scale='Tealgrn',
template='plotly_white',
title="李白游历热力图"
)
fig.update_layout(
geo=dict(
showland=True, landcolor="rgb(240, 240, 240)",
showcountries=True, countrycolor="rgb(200, 200, 200)",
fitbounds="locations"
),
margin={"r":0,"t":40,"l":0,"b":0},
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)'
)
st.plotly_chart(fig, use_container_width=True)
with col2:
st.subheader("🗺️ 时序GIS地图")
# 年份选择滑块
min_year = int(df_emotion['Year'].min())
max_year = int(df_emotion['Year'].max())
selected_year = st.slider(
"选择年份进度",
min_value=min_year,
max_value=max_year,
value=st.session_state.selected_year,
key="year_slider_travel"
)
st.session_state.selected_year = selected_year
# 显示当前年份的诗作数量
current_count = len(df_emotion[df_emotion['Year'] <= selected_year])
total_count = len(df_emotion)
st.info(f"**{selected_year}年** - 已创作 {current_count} 首诗作 (总计 {total_count} 首)")
# 创建时序地图
temporal_map = create_temporal_map(df_emotion, selected_year)
st_folium(temporal_map, width=600, height=500)
# 数据详情 - 使用AI数据源
with st.expander("📋 查看详细游历数据"):
if not st.session_state.ai_data_df.empty:
st.dataframe(st.session_state.ai_data_df, use_container_width=True)
else:
st.error("❌ 数据加载失败")
# --- 页面 5: 与仙对饮 (飞花令互动) ---
elif selected == "与仙对饮":
st.title("🍶 飞花令·互动")
st.write("告诉李白你现在的心情,他会回赠你一句诗。")
mood = st.selectbox("你现在的心情如何?", ["豪情万丈", "思念故乡", "怀才不遇", "享受自然", "感叹时光"])
if st.button("向太白敬酒", use_container_width=True):
st.toast("举杯邀明月,对影成三人...", icon="🥂")
st.markdown("---")
st.markdown("### 李白的回应:")
if mood == "豪情万丈":
st.success("飞流直下三千尺,疑是银河落九天!")
st.image("https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/飞流直下.jpg", caption="豪情万丈")
elif mood == "思念故乡":
st.info("举头望明月,低头思故乡。")
st.image("https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/明月思想.jpg", caption="明月寄相思")
elif mood == "怀才不遇":
st.warning("天生我材必有用,千金散尽还复来。")
st.image("https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/怀才不遇.jpg", caption="怀才不遇")
elif mood == "享受自然":
st.success("两岸猿声啼不住,轻舟已过万重山。")
st.image("https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/轻舟已过.jpg", caption="轻舟万重山")
elif mood == "感叹时光":
st.error("弃我去者,昨日之日不可留;乱我心者,今日之日多烦忧。")
st.image("https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/感叹时光.jpg", caption = "感叹时光")
st.markdown("---")
st.caption("输入框:写下你想对李白说的话")
user_input = st.text_area("", placeholder="太白兄,我想对你说...")
if user_input:
st.write(f"李白收到了你的信:*{user_input}*")
# ---------------------------------------------------------
# 页脚
# ---------------------------------------------------------
st.markdown("---")
st.markdown("""
Designed for Li Bai Emotion Data Project | Created with Streamlit
UI Design Style: Ink & Cloud (云墨) | 融合GIS与RAG技术
""", unsafe_allow_html=True)