Spaces:
Sleeping
Sleeping
| 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(""" | |
| <style> | |
| /* 全局字体设置:优先使用楷体/宋体 */ | |
| html, body, [class*="css"] { | |
| font-family: "KaiTi", "STKaiti", "SimSun", "Times New Roman", serif; | |
| } | |
| /* 背景图片:水墨山水风格 */ | |
| .stApp { | |
| background-image: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.9)), | |
| url("https://raw.githubusercontent.com/seblee424/libai_emotin_data/main/background.jpg"); | |
| background-size: cover; | |
| background-attachment: fixed; | |
| background-position: center; | |
| background-repeat: no-repeat; | |
| } | |
| /* 遮罩层:让文字在背景上更清晰 */ | |
| .main .block-container { | |
| background-color: rgba(255, 255, 255, 0.01); | |
| padding: 2rem; | |
| border-radius: 15px; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); | |
| margin-top: 1rem; | |
| border: 1px solid #e0e0e0; | |
| } | |
| /* 标题样式 */ | |
| h1, h2, h3 { | |
| color: #2c3e50; | |
| text-align: center; | |
| font-weight: bold; | |
| margin-bottom: 1rem; | |
| } | |
| h1 { | |
| text-shadow: 2px 2px 4px #cccccc; | |
| font-size: 3rem !important; | |
| background: linear-gradient(45deg, #2c3e50, #34495e); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| /* 侧边栏美化 */ | |
| section[data-testid="stSidebar"] { | |
| background-color: rgba(240, 242, 246, 0.95); | |
| border-right: 2px solid #b8c6db; | |
| backdrop-filter: blur(5px); | |
| } | |
| /* 按钮样式 */ | |
| .stButton>button { | |
| background: linear-gradient(45deg, #2c3e50, #34495e); | |
| color: white; | |
| border-radius: 25px; | |
| border: none; | |
| padding: 12px 28px; | |
| transition: all 0.3s ease; | |
| font-weight: bold; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.1); | |
| } | |
| .stButton>button:hover { | |
| background: linear-gradient(45deg, #34495e, #2c3e50); | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 12px rgba(0,0,0,0.2); | |
| } | |
| /* 诗词卡片样式 */ | |
| .poem-card { | |
| background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); | |
| border-left: 5px solid #2c3e50; | |
| padding: 20px; | |
| margin: 15px 0; | |
| font-size: 1.2rem; | |
| font-style: italic; | |
| color: #444; | |
| border-radius: 0 10px 10px 0; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| /* 数据框样式 */ | |
| .stDataFrame { | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| /* 选择框样式 */ | |
| .stSelectbox, .stTextInput { | |
| border-radius: 8px; | |
| } | |
| /* 聊天消息样式 */ | |
| .stChatMessage { | |
| border-radius: 15px; | |
| margin: 10px 0; | |
| } | |
| /* 进度条样式 */ | |
| .stSlider > div > div > div { | |
| background: linear-gradient(45deg, #2c3e50, #34495e); | |
| } | |
| </style> | |
| """, 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": ["当涂", "马鞍山"]}, | |
| } | |
| # 数据加载函数 - 增强错误处理 | |
| 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 | |
| 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) | |
| 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) | |
| def get_emotion_data(): | |
| return pd.DataFrame({ | |
| '意象': ['月亮', '酒', '剑', '水', '山', '花', '孤', '梦'], | |
| '频率': [120, 95, 40, 85, 110, 60, 55, 30], | |
| '情感色彩': ['思乡/孤独', '豪迈/解忧', '侠客/抱负', '流逝/愁苦', '归隐/壮阔', '美好/易逝', '寂寞', '虚幻'] | |
| }) | |
| # RAG Chatbot 函数 | |
| 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""" | |
| <div style="width: 250px;"> | |
| <h4 style="color: #2c3e50; margin-bottom: 10px;">{row.get(location_col, '未知地点')}</h4> | |
| <p><strong>📅 时间:</strong> {row.get('时间', '未知')}</p> | |
| <p><strong>📖 诗作/事件:</strong> {row.get(summary_col, '未知')}</p> | |
| <p><strong>📍 坐标:</strong> {row['Latitude']:.4f}, {row['Longitude']:.4f}</p> | |
| </div> | |
| """ | |
| 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 = "<br>".join([f"• {poem}" for poem in row['Title'][:5]]) # 最多显示5首诗 | |
| if len(row['Title']) > 5: | |
| poem_list += f"<br>• ...等 {len(row['Title'])} 首诗" | |
| popup_html = f""" | |
| <div style="width: 280px;"> | |
| <h4 style="color: #2c3e50; margin-bottom: 10px;">{row.get('Location', '未知地点')}</h4> | |
| <p><strong>📅 年份:</strong> {int(row['Year'])}</p> | |
| <p><strong>📖 同年诗作:</strong></p> | |
| <div style="max-height: 150px; overflow-y: auto;"> | |
| {poem_list} | |
| </div> | |
| </div> | |
| """ | |
| 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'<div style="font-size: 12px; color: {color}; font-weight: bold;">{int(row["Year"])}</div>' | |
| ) | |
| ).add_to(m) | |
| except Exception as e: | |
| continue | |
| # 添加当前年份的标题 | |
| title_html = f''' | |
| <h3 align="center" style="font-size:20px"><b>李白足迹时序图 (截至 {selected_year} 年)</b></h3> | |
| ''' | |
| 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"<b>{row.get('Title', '无题')}</b><br>{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(""" | |
| <div class="poem-card"> | |
| 君不见,黄河之水天上来,奔流到海不复回。<br> | |
| 君不见,高堂明镜悲白发,朝如青丝暮成雪。<br> | |
| 人生得意须尽欢,莫使金樽空对月。 | |
| </div> | |
| """, 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(""" | |
| <div style="text-align: center; color: grey;"> | |
| Designed for Li Bai Emotion Data Project | Created with Streamlit <br> | |
| UI Design Style: Ink & Cloud (云墨) | 融合GIS与RAG技术 | |
| </div> | |
| """, unsafe_allow_html=True) |