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)