Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import os | |
| import json | |
| from datetime import datetime, timedelta | |
| import base64 | |
| import pandas as pd | |
| import pydeck as pdk | |
| from travel import ( | |
| destination_research_task, accommodation_task, transportation_task, | |
| activities_task, dining_task, itinerary_task, chatbot_task, | |
| run_task | |
| ) | |
| # st.set_page_config()는 다른 Streamlit 함수보다 가장 먼저 실행되어야 합니다. | |
| st.set_page_config( | |
| page_title="Your AI Agent for Travelling", | |
| page_icon="✈️", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # ------------------------------------------ | |
| # 다국어 지원을 위한 번역 사전 및 헬퍼 함수 | |
| # ------------------------------------------ | |
| translations = { | |
| "en": { | |
| "page_title": "Your AI Agent for Travelling", | |
| "header": "Your AI Agent for Travelling", | |
| "create_itinerary": "Create Your Itinerary", | |
| "trip_details": "Trip Details", | |
| "origin": "Origin", | |
| "destination": "Destination", | |
| "travel_dates": "Travel Dates", | |
| "duration": "Duration (days)", | |
| "preferences": "Preferences", | |
| "additional_preferences": "Additional Preferences", | |
| "interests": "Interests", | |
| "special_requirements": "Special Requirements", | |
| "submit": "🚀 Create My Personal Travel Itinerary", | |
| "request_details": "Your Travel Request", | |
| "from": "From", | |
| "when": "When", | |
| "budget": "Budget", | |
| "travel_style": "Travel Style", | |
| "live_agent_outputs": "Live Agent Outputs", | |
| "full_itinerary": "Full Itinerary", | |
| "details": "Details", | |
| "download_share": "Download & Share", | |
| "save_itinerary": "Save Your Itinerary", | |
| "plan_another_trip": "🔄 Plan Another Trip", | |
| "about": "About", | |
| "how_it_works": "How it works", | |
| "travel_agents": "Travel Agents", | |
| "share_itinerary": "Share Your Itinerary", | |
| "save_for_mobile": "Save for Mobile", | |
| "built_with": "Built with ❤️ for you", | |
| # 출력 관련 추가 텍스트 | |
| "itinerary_ready": "Your Travel Itinerary is Ready! 🎉", | |
| "personalized_experience": "We've created a personalized travel experience just for you. Explore your itinerary below.", | |
| "agent_activity": "Agent Activity", | |
| "error_origin_destination": "Please enter both origin and destination.", | |
| "your_itinerary_file": "Your Itinerary File", | |
| "text_format": "Text format - Can be opened in any text editor" | |
| }, | |
| "ko": { | |
| "page_title": "당신의 여행을 위한 AI 에이전트", | |
| "header": "당신의 여행을 위한 AI 에이전트", | |
| "create_itinerary": "여행 일정 생성", | |
| "trip_details": "여행 세부 정보", | |
| "origin": "출발지", | |
| "destination": "목적지", | |
| "travel_dates": "여행 날짜", | |
| "duration": "기간 (일수)", | |
| "preferences": "선호사항", | |
| "additional_preferences": "추가 선호사항", | |
| "interests": "관심사", | |
| "special_requirements": "특별 요구사항", | |
| "submit": "🚀 나만의 여행 일정 생성", | |
| "request_details": "여행 요청 정보", | |
| "from": "출발지", | |
| "when": "여행 기간", | |
| "budget": "예산", | |
| "travel_style": "여행 스타일", | |
| "live_agent_outputs": "실시간 에이전트 결과", | |
| "full_itinerary": "전체 일정", | |
| "details": "세부사항", | |
| "download_share": "다운로드 및 공유", | |
| "save_itinerary": "일정 저장", | |
| "plan_another_trip": "🔄 다른 여행 계획", | |
| "about": "소개", | |
| "how_it_works": "작동 방식", | |
| "travel_agents": "여행 에이전트", | |
| "share_itinerary": "일정 공유", | |
| "save_for_mobile": "모바일 저장", | |
| "built_with": "당신을 위해 ❤️ 만들어졌습니다", | |
| # 출력 관련 추가 텍스트 | |
| "itinerary_ready": "여행 일정이 준비되었습니다! 🎉", | |
| "personalized_experience": "당신만을 위한 맞춤형 여행 경험이 만들어졌습니다. 아래에서 일정을 확인하세요.", | |
| "agent_activity": "에이전트 활동", | |
| "error_origin_destination": "출발지와 목적지를 모두 입력하세요.", | |
| "your_itinerary_file": "당신의 여행 일정 파일", | |
| "text_format": "텍스트 형식 - 모든 텍스트 편집기에서 열 수 있습니다." | |
| }, | |
| "ja": { | |
| "page_title": "あなたの旅行のためのAIエージェント", | |
| "header": "あなたの旅行のためのAIエージェント", | |
| "create_itinerary": "旅行プラン作成", | |
| "trip_details": "旅行詳細", | |
| "origin": "出発地", | |
| "destination": "目的地", | |
| "travel_dates": "旅行日程", | |
| "duration": "期間(日数)", | |
| "preferences": "好み", | |
| "additional_preferences": "追加の好み", | |
| "interests": "興味", | |
| "special_requirements": "特別な要件", | |
| "submit": "🚀 私のための旅行プラン作成", | |
| "request_details": "旅行リクエスト", | |
| "from": "出発地", | |
| "when": "旅行期間", | |
| "budget": "予算", | |
| "travel_style": "旅行スタイル", | |
| "live_agent_outputs": "リアルタイムエージェント出力", | |
| "full_itinerary": "全行程", | |
| "details": "詳細", | |
| "download_share": "ダウンロードと共有", | |
| "save_itinerary": "旅行プランを保存", | |
| "plan_another_trip": "🔄 他の旅行を計画", | |
| "about": "概要", | |
| "how_it_works": "使い方", | |
| "travel_agents": "旅行エージェント", | |
| "share_itinerary": "旅行プランを共有", | |
| "save_for_mobile": "モバイル保存", | |
| "built_with": "愛を込めて作られました", | |
| # 출력 관련 추가 텍스트 | |
| "itinerary_ready": "旅行プランの準備ができました! 🎉", | |
| "personalized_experience": "あなたのためにパーソナライズされた旅行体験を作成しました。下のプランをご覧ください。", | |
| "agent_activity": "エージェントアクティビティ", | |
| "error_origin_destination": "出発地と目的地の両方を入力してください。", | |
| "your_itinerary_file": "あなたの旅行プランファイル", | |
| "text_format": "テキスト形式 - 任意のテキストエディタで開けます。" | |
| }, | |
| "zh": { | |
| "page_title": "您的旅行 AI 代理", | |
| "header": "您的旅行 AI 代理", | |
| "create_itinerary": "创建您的行程", | |
| "trip_details": "旅行详情", | |
| "origin": "出发地", | |
| "destination": "目的地", | |
| "travel_dates": "旅行日期", | |
| "duration": "天数", | |
| "preferences": "偏好", | |
| "additional_preferences": "其他偏好", | |
| "interests": "兴趣", | |
| "special_requirements": "特殊需求", | |
| "submit": "🚀 创建我的个性化行程", | |
| "request_details": "您的旅行请求", | |
| "from": "出发地", | |
| "when": "旅行时间", | |
| "budget": "预算", | |
| "travel_style": "旅行风格", | |
| "live_agent_outputs": "实时代理输出", | |
| "full_itinerary": "完整行程", | |
| "details": "详情", | |
| "download_share": "下载与分享", | |
| "save_itinerary": "保存行程", | |
| "plan_another_trip": "🔄 计划另一趟旅行", | |
| "about": "关于", | |
| "how_it_works": "工作原理", | |
| "travel_agents": "旅行代理", | |
| "share_itinerary": "分享行程", | |
| "save_for_mobile": "保存到手机", | |
| "built_with": "用❤️为您制作", | |
| # 출력 관련 추가 텍스트 | |
| "itinerary_ready": "您的旅行行程已准备就绪! 🎉", | |
| "personalized_experience": "我们已为您创建了个性化的旅行体验,请在下方查看您的行程。", | |
| "agent_activity": "代理活动", | |
| "error_origin_destination": "请输入出发地和目的地。", | |
| "your_itinerary_file": "您的行程文件", | |
| "text_format": "文本格式 - 可在任何文本编辑器中打开。" | |
| }, | |
| "es": { | |
| "page_title": " Tu Agente de IA para Viajar", | |
| "header": " Tu Agente de IA para Viajar", | |
| "create_itinerary": "Crea Tu Itinerario", | |
| "trip_details": "Detalles del Viaje", | |
| "origin": "Origen", | |
| "destination": "Destino", | |
| "travel_dates": "Fechas del Viaje", | |
| "duration": "Duración (días)", | |
| "preferences": "Preferencias", | |
| "additional_preferences": "Preferencias Adicionales", | |
| "interests": "Intereses", | |
| "special_requirements": "Requisitos Especiales", | |
| "submit": "🚀 Crea Mi Itinerario Personalizado", | |
| "request_details": "Tu Solicitud de Viaje", | |
| "from": "Desde", | |
| "when": "Cuándo", | |
| "budget": "Presupuesto", | |
| "travel_style": "Estilo de Viaje", | |
| "live_agent_outputs": "Salidas en Vivo del Agente", | |
| "full_itinerary": "Itinerario Completo", | |
| "details": "Detalles", | |
| "download_share": "Descargar y Compartir", | |
| "save_itinerary": "Guardar Itinerario", | |
| "plan_another_trip": "🔄 Planear Otro Viaje", | |
| "about": "Acerca de", | |
| "how_it_works": "Cómo Funciona", | |
| "travel_agents": "Agentes de Viaje", | |
| "share_itinerary": "Compartir Itinerario", | |
| "save_for_mobile": "Guardar para Móvil", | |
| "built_with": "Hecho con ❤️ para ti", | |
| # 출력 관련 추가 텍스트 | |
| "itinerary_ready": "¡Tu itinerario de viaje está listo! 🎉", | |
| "personalized_experience": "Hemos creado una experiencia de viaje personalizada solo para ti. Explora tu itinerario a continuación.", | |
| "agent_activity": "Actividad del Agente", | |
| "error_origin_destination": "Por favor, ingresa tanto el origen como el destino.", | |
| "your_itinerary_file": "Tu Archivo de Itinerario", | |
| "text_format": "Formato de texto - Se puede abrir en cualquier editor de texto." | |
| }, | |
| "fr": { | |
| "page_title": " Votre Agent IA pour Voyager", | |
| "header": " Votre Agent IA pour Voyager", | |
| "create_itinerary": "Créez Votre Itinéraire", | |
| "trip_details": "Détails du Voyage", | |
| "origin": "Origine", | |
| "destination": "Destination", | |
| "travel_dates": "Dates du Voyage", | |
| "duration": "Durée (jours)", | |
| "preferences": "Préférences", | |
| "additional_preferences": "Préférences Supplémentaires", | |
| "interests": "Centres d'intérêt", | |
| "special_requirements": "Exigences Spéciales", | |
| "submit": "🚀 Créez Mon Itinéraire Personnalisé", | |
| "request_details": "Votre Demande de Voyage", | |
| "from": "De", | |
| "when": "Quand", | |
| "budget": "Budget", | |
| "travel_style": "Style de Voyage", | |
| "live_agent_outputs": "Résultats en Direct de l'Agent", | |
| "full_itinerary": "Itinéraire Complet", | |
| "details": "Détails", | |
| "download_share": "Télécharger et Partager", | |
| "save_itinerary": "Enregistrer l'Itinéraire", | |
| "plan_another_trip": "🔄 Planifier un Autre Voyage", | |
| "about": "À Propos", | |
| "how_it_works": "Fonctionnement", | |
| "travel_agents": "Agents de Voyage", | |
| "share_itinerary": "Partager l'Itinéraire", | |
| "save_for_mobile": "Enregistrer pour Mobile", | |
| "built_with": "Conçu avec ❤️ pour vous", | |
| # 출력 관련 추가 텍스트 | |
| "itinerary_ready": "Votre itinéraire de voyage est prêt ! 🎉", | |
| "personalized_experience": "Nous avons créé une expérience de voyage personnalisée rien que pour vous. Découvrez votre itinéraire ci-dessous.", | |
| "agent_activity": "Activité de l'Agent", | |
| "error_origin_destination": "Veuillez saisir à la fois le lieu de départ et la destination.", | |
| "your_itinerary_file": "Votre Fichier d'Itinéraire", | |
| "text_format": "Format texte - Peut être ouvert dans n'importe quel éditeur de texte." | |
| }, | |
| "de": { | |
| "page_title": "Ihr KI-Reiseassistent", | |
| "header": " Ihr KI-Reiseassistent", | |
| "create_itinerary": "Erstellen Sie Ihre Reiseroute", | |
| "trip_details": "Reisedetails", | |
| "origin": "Abfahrtsort", | |
| "destination": "Zielort", | |
| "travel_dates": "Reisedaten", | |
| "duration": "Dauer (Tage)", | |
| "preferences": "Vorlieben", | |
| "additional_preferences": "Zusätzliche Vorlieben", | |
| "interests": "Interessen", | |
| "special_requirements": "Besondere Anforderungen", | |
| "submit": "🚀 Erstellen Sie meine personalisierte Reiseroute", | |
| "request_details": "Ihre Reiseanfrage", | |
| "from": "Von", | |
| "when": "Wann", | |
| "budget": "Budget", | |
| "travel_style": "Reisestil", | |
| "live_agent_outputs": "Live Agent Ausgaben", | |
| "full_itinerary": "Komplette Reiseroute", | |
| "details": "Details", | |
| "download_share": "Herunterladen & Teilen", | |
| "save_itinerary": "Reiseroute speichern", | |
| "plan_another_trip": "🔄 Plane eine weitere Reise", | |
| "about": "Über", | |
| "how_it_works": "Wie es funktioniert", | |
| "travel_agents": "Reiseassistenten", | |
| "share_itinerary": "Reiseroute teilen", | |
| "save_for_mobile": "Für Mobilgeräte speichern", | |
| "built_with": "Mit ❤️ für Sie gebaut", | |
| # 출력 관련 추가 텍스트 | |
| "itinerary_ready": "Ihre Reiseroute ist fertig! 🎉", | |
| "personalized_experience": "Wir haben eine personalisierte Reiseerfahrung nur für Sie erstellt. Entdecken Sie Ihre Reiseroute unten.", | |
| "agent_activity": "Agentenaktivität", | |
| "error_origin_destination": "Bitte geben Sie sowohl den Abfahrtsort als auch das Ziel ein.", | |
| "your_itinerary_file": "Ihre Reise-Datei", | |
| "text_format": "Textformat – Kann in jedem Texteditor geöffnet werden." | |
| }, | |
| "ar": { | |
| "page_title": " وكيل السفر الذكي الخاص بك", | |
| "header": " وكيل السفر الذكي الخاص بك", | |
| "create_itinerary": "إنشاء خط سير الرحلة", | |
| "trip_details": "تفاصيل الرحلة", | |
| "origin": "المغادرة من", | |
| "destination": "الوجهة", | |
| "travel_dates": "تواريخ السفر", | |
| "duration": "المدة (بالأيام)", | |
| "preferences": "التفضيلات", | |
| "additional_preferences": "تفضيلات إضافية", | |
| "interests": "الاهتمامات", | |
| "special_requirements": "المتطلبات الخاصة", | |
| "submit": "🚀 إنشاء خط سير الرحلة الشخصي", | |
| "request_details": "طلب السفر الخاص بك", | |
| "from": "من", | |
| "when": "متى", | |
| "budget": "الميزانية", | |
| "travel_style": "أسلوب السفر", | |
| "live_agent_outputs": "مخرجات الوكيل المباشرة", | |
| "full_itinerary": "خط سير الرحلة الكامل", | |
| "details": "التفاصيل", | |
| "download_share": "تنزيل ومشاركة", | |
| "save_itinerary": "حفظ خط سير الرحلة", | |
| "plan_another_trip": "🔄 خطط لرحلة أخرى", | |
| "about": "حول", | |
| "how_it_works": "كيف يعمل", | |
| "travel_agents": "وكلاء السفر", | |
| "share_itinerary": "شارك خط سير الرحلة", | |
| "save_for_mobile": "حفظ للهاتف المحمول", | |
| "built_with": "مصنوع بحب من أجلك", | |
| # 출력 관련 추가 텍스트 | |
| "itinerary_ready": "تم تجهيز خط سير رحلتك! 🎉", | |
| "personalized_experience": "لقد أنشأنا تجربة سفر مخصصة لك. استعرض خط سير رحلتك أدناه.", | |
| "agent_activity": "نشاط الوكيل", | |
| "error_origin_destination": "يرجى إدخال نقطة الانطلاق والوجهة.", | |
| "your_itinerary_file": "ملف خط سير رحلتك", | |
| "text_format": "تنسيق نصي - يمكن فتحه في أي محرر نصوص." | |
| } | |
| } | |
| def t(key): | |
| lang = st.session_state.get("selected_language", "en") | |
| return translations[lang].get(key, key) | |
| # --------------------------- | |
| # 세션 초기화 | |
| # --------------------------- | |
| if 'selected_language' not in st.session_state: | |
| st.session_state.selected_language = "en" # 기본은 영어 | |
| # ------------------------------------------ | |
| # 사이드바에 언어 선택 위젯 추가 | |
| # ------------------------------------------ | |
| with st.sidebar: | |
| language = st.selectbox( | |
| "Language / 언어 / 言語 / 语言 / Idioma / Langue / Sprache / اللغة", | |
| ["English", "한국어", "日本語", "中文", "Español", "Français", "Deutsch", "العربية"] | |
| ) | |
| lang_map = { | |
| "English": "en", | |
| "한국어": "ko", | |
| "日本語": "ja", | |
| "中文": "zh", | |
| "Español": "es", | |
| "Français": "fr", | |
| "Deutsch": "de", | |
| "العربية": "ar" | |
| } | |
| st.session_state.selected_language = lang_map.get(language, "en") | |
| # ------------------------------------------ | |
| # 이후 Streamlit UI 코드 시작 | |
| # ------------------------------------------ | |
| # Modern CSS with refined color scheme and sleek animations | |
| st.markdown(""" | |
| <style> | |
| /* Sleek Color Palette */ | |
| :root { | |
| --primary: #3a86ff; | |
| --primary-light: #4895ef; | |
| --primary-dark: #2667ff; | |
| --secondary: #4cc9f0; | |
| --accent: #4361ee; | |
| --background: #f8f9fa; | |
| --card-bg: #ffffff; | |
| --text: #212529; | |
| --text-light: #6c757d; | |
| --text-muted: #adb5bd; | |
| --border: #e9ecef; | |
| --success: #2ecc71; | |
| --warning: #f39c12; | |
| --info: #3498db; | |
| } | |
| /* Refined Animations */ | |
| @keyframes smoothFadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes slideInRight { | |
| from { opacity: 0; transform: translateX(20px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| .animate-in { | |
| animation: smoothFadeIn 0.5s cubic-bezier(0.215, 0.61, 0.355, 1); | |
| } | |
| .slide-in { | |
| animation: slideInRight 0.5s cubic-bezier(0.215, 0.61, 0.355, 1); | |
| } | |
| /* Sleek Header Styles */ | |
| .main-header { | |
| font-size: 2.5rem; | |
| color: var(--primary-dark); | |
| text-align: center; | |
| margin-bottom: 0.8rem; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| } | |
| .sub-header { | |
| font-size: 1.4rem; | |
| color: var(--accent); | |
| font-weight: 600; | |
| margin-top: 1.8rem; | |
| margin-bottom: 0.8rem; | |
| border-bottom: 1px solid var(--border); | |
| padding-bottom: 0.4rem; | |
| } | |
| /* Sleek Card Styles */ | |
| .modern-card { | |
| background-color: var(--card-bg); | |
| border-radius: 10px; | |
| padding: 1.2rem; | |
| margin-bottom: 1.2rem; | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); | |
| transition: all 0.25s ease; | |
| border: 1px solid var(--border); | |
| } | |
| .modern-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); | |
| } | |
| /* Refined Form Styles */ | |
| .stTextInput > div > div > input, | |
| .stDateInput > div > div > input, | |
| .stTextArea > div > div > textarea { | |
| border-radius: 6px; | |
| border: 1px solid var(--border); | |
| padding: 10px 12px; | |
| font-size: 14px; | |
| transition: all 0.2s ease; | |
| box-shadow: none; | |
| } | |
| .stTextInput > div > div > input:focus, | |
| .stDateInput > div > div > input:focus, | |
| .stTextArea > div > div > textarea:focus { | |
| border: 1px solid var(--primary); | |
| box-shadow: 0 0 0 1px rgba(58, 134, 255, 0.15); | |
| } | |
| /* Sleek Button Styles */ | |
| .stButton > button { | |
| background-color: var(--primary); | |
| color: white; | |
| font-weight: 500; | |
| padding: 0.5rem 1.2rem; | |
| border-radius: 6px; | |
| border: none; | |
| transition: all 0.2s ease; | |
| font-size: 14px; | |
| letter-spacing: 0.3px; | |
| } | |
| .stButton > button:hover { | |
| background-color: var(--primary-dark); | |
| transform: translateY(-1px); | |
| box-shadow: 0 3px 8px rgba(58, 134, 255, 0.25); | |
| } | |
| /* Sleek Tab Styles */ | |
| .stTabs [data-baseweb="tab-list"] { | |
| gap: 2px; | |
| background-color: var(--background); | |
| border-radius: 8px; | |
| padding: 2px; | |
| } | |
| .stTabs [data-baseweb="tab"] { | |
| border-radius: 6px; | |
| padding: 8px 16px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .stTabs [aria-selected="true"] { | |
| background-color: var(--primary); | |
| color: white !important; | |
| } | |
| /* Progress Bar Styles */ | |
| .stProgress > div > div > div > div { | |
| background-color: var(--primary); | |
| } | |
| /* Progress Styles */ | |
| .progress-container { | |
| margin: 1.2rem 0; | |
| background-color: var(--background); | |
| border-radius: 8px; | |
| padding: 0.8rem; | |
| border: 1px solid var(--border); | |
| } | |
| .step-complete { | |
| color: #4CAF50; | |
| font-weight: 600; | |
| } | |
| .step-pending { | |
| color: #9E9E9E; | |
| } | |
| .step-active { | |
| color: var(--primary); | |
| font-weight: 600; | |
| } | |
| /* Agent Output */ | |
| .agent-output { | |
| background-color: #f8f9fa; | |
| border-left: 5px solid var(--primary); | |
| padding: 1.2rem; | |
| margin: 1rem 0; | |
| border-radius: 10px; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| } | |
| /* Footer */ | |
| .footer { | |
| text-align: center; | |
| margin-top: 3rem; | |
| color: var(--text-light); | |
| font-size: 0.9rem; | |
| padding: 1rem; | |
| border-top: 1px solid #eaeaea; | |
| } | |
| /* Agent Log */ | |
| .agent-log { | |
| background-color: #F5F5F5; | |
| border-left: 3px solid var(--primary); | |
| padding: 0.5rem; | |
| margin-bottom: 0.5rem; | |
| font-family: monospace; | |
| border-radius: 4px; | |
| } | |
| /* Info and Success Boxes */ | |
| .info-box { | |
| background-color: var(--primary-light); | |
| color: white; | |
| padding: 1rem; | |
| border-radius: 0.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .success-box { | |
| background-color: #E8F5E9; | |
| padding: 1rem; | |
| border-radius: 0.5rem; | |
| margin-bottom: 1rem; | |
| border-left: 5px solid #4CAF50; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Helper function to download HTML file | |
| def get_download_link(text_content, filename): | |
| b64 = base64.b64encode(text_content.encode()).decode() | |
| href = f'<a class="download-link" href="data:text/plain;base64,{b64}" download="{filename}"><i>📥</i> {t("save_itinerary")}</a>' | |
| return href | |
| # Updated helper function to display modern progress with a single UI element | |
| def display_modern_progress(current_step, total_steps=6): | |
| if 'progress_steps' not in st.session_state: | |
| st.session_state.progress_steps = { | |
| 0: {'status': 'pending', 'name': t("trip_details")}, | |
| 1: {'status': 'pending', 'name': t("about")}, | |
| 2: {'status': 'pending', 'name': t("travel_style")}, | |
| 3: {'status': 'pending', 'name': t("live_agent_outputs")}, | |
| 4: {'status': 'pending', 'name': t("download_share")}, | |
| 5: {'status': 'pending', 'name': t("full_itinerary")} | |
| } | |
| for i in range(total_steps): | |
| if i < current_step: | |
| st.session_state.progress_steps[i]['status'] = 'complete' | |
| elif i == current_step: | |
| st.session_state.progress_steps[i]['status'] = 'active' | |
| else: | |
| st.session_state.progress_steps[i]['status'] = 'pending' | |
| progress_percentage = (current_step / total_steps) * 100 | |
| st.progress(progress_percentage / 100) | |
| st.markdown(""" | |
| <style> | |
| .compact-progress { | |
| background: white; | |
| border-radius: 10px; | |
| padding: 15px; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| margin-bottom: 20px; | |
| } | |
| .progress-title { | |
| font-size: 16px; | |
| font-weight: bold; | |
| margin-bottom: 15px; | |
| color: #333; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 10px; | |
| } | |
| .step-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 10px; | |
| } | |
| .step-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 8px 10px; | |
| border-radius: 6px; | |
| background: #f8f9fa; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
| } | |
| .step-item.complete { | |
| border-left: 3px solid #4CAF50; | |
| background: #f1f8e9; | |
| } | |
| .step-item.active { | |
| border-left: 3px solid #2196F3; | |
| background: #e3f2fd; | |
| font-weight: bold; | |
| } | |
| .step-item.pending { | |
| border-left: 3px solid #9e9e9e; | |
| opacity: 0.7; | |
| } | |
| .step-icon { | |
| margin-right: 8px; | |
| font-size: 14px; | |
| } | |
| .step-text { | |
| font-size: 13px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| </style> | |
| <div class="compact-progress"> | |
| """, unsafe_allow_html=True) | |
| st.markdown('<div class="step-grid">', unsafe_allow_html=True) | |
| for i, step_info in st.session_state.progress_steps.items(): | |
| status = step_info['status'] | |
| name = step_info['name'] | |
| if status == 'complete': | |
| icon = "✅" | |
| status_class = "complete" | |
| elif status == 'active': | |
| icon = "🔄" | |
| status_class = "active" | |
| else: | |
| icon = "⭕" | |
| status_class = "pending" | |
| st.markdown(f""" | |
| <div class="step-item {status_class}"> | |
| <span class="step-icon">{icon}</span> | |
| <span class="step-text">{name}</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown('</div></div>', unsafe_allow_html=True) | |
| return progress_percentage | |
| def update_step_status(step_index, status): | |
| if 'progress_steps' in st.session_state and step_index in st.session_state.progress_steps: | |
| st.session_state.progress_steps[step_index]['status'] = status | |
| def run_task_with_logs(task, input_text, log_container, output_container, results_key=None): | |
| log_message = f"🤖 Starting {task.agent.role}..." | |
| st.session_state.log_messages.append(log_message) | |
| with log_container: | |
| st.markdown("### " + t("agent_activity")) | |
| for msg in st.session_state.log_messages: | |
| st.markdown(msg) | |
| result = run_task(task, input_text) | |
| if results_key: | |
| st.session_state.results[results_key] = result | |
| log_message = f"✅ {task.agent.role} completed!" | |
| st.session_state.log_messages.append(log_message) | |
| with log_container: | |
| st.markdown("### " + t("agent_activity")) | |
| for msg in st.session_state.log_messages: | |
| st.markdown(msg) | |
| with output_container: | |
| st.markdown(f"### {task.agent.role} Output") | |
| st.markdown("<div class='agent-output'>" + result + "</div>", unsafe_allow_html=True) | |
| return result | |
| # ------------------------------------------ | |
| # Session state 초기화 | |
| # ------------------------------------------ | |
| if 'generated_itinerary' not in st.session_state: | |
| st.session_state.generated_itinerary = None | |
| if 'generation_complete' not in st.session_state: | |
| st.session_state.generation_complete = False | |
| if 'current_step' not in st.session_state: | |
| st.session_state.current_step = 0 | |
| if 'results' not in st.session_state: | |
| st.session_state.results = { | |
| "destination_info": "", | |
| "accommodation_info": "", | |
| "transportation_info": "", | |
| "activities_info": "", | |
| "dining_info": "", | |
| "itinerary": "", | |
| "final_itinerary": "" | |
| } | |
| if 'log_messages' not in st.session_state: | |
| st.session_state.log_messages = [] | |
| if 'current_output' not in st.session_state: | |
| st.session_state.current_output = None | |
| if 'form_submitted' not in st.session_state: | |
| st.session_state.form_submitted = False | |
| # Modern animated header | |
| st.markdown(f""" | |
| <div class="animate-in" style="text-align: center;"> | |
| <div style="margin-bottom: 20px;"> | |
| <img src="https://img.icons8.com/fluency/96/travel-card.png" width="90" style="filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));"> | |
| </div> | |
| <h1 class="main-header">{t("header")}</h1> | |
| <p style="font-size: 1.2rem; color: #6c757d; margin-bottom: 25px;"> | |
| ✨ Create your personalized AI-powered travel itinerary in minutes! ✨ | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown('<hr style="height:3px;border:none;background-color:#f0f0f0;margin-bottom:25px;">', unsafe_allow_html=True) | |
| with st.sidebar: | |
| st.markdown(""" | |
| <div style="text-align: center; padding: 20px 0; margin-bottom: 20px; border-bottom: 1px solid #eaeaea;"> | |
| <img src="https://img.icons8.com/fluency/96/travel-card.png" width="80" style="margin-bottom: 15px;"> | |
| <h3 style="margin-bottom: 5px; color: #4361ee;">Your AI Agent for Travelling</h3> | |
| <p style="color: #6c757d; font-size: 0.9rem;">AI-Powered Travel Planning</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown("### 🌟 " + t("about")) | |
| st.info("This AI-powered tool creates a personalized travel itinerary based on your preferences. Fill in the form and let our specialized travel agents plan your perfect trip!") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown("### 🔍 " + t("how_it_works")) | |
| st.markdown(""" | |
| <ol style="padding-left: 25px;"> | |
| <li><b>🖊️ Enter</b> your travel details</li> | |
| <li><b>🧠 AI analysis</b> of your preferences</li> | |
| <li><b>📋 Generate</b> comprehensive itinerary</li> | |
| <li><b>📥 Download</b> and enjoy your trip!</li> | |
| </ol> | |
| """, unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown("### 🤖 Travel Agents") | |
| agents = [ | |
| ("🔭 Research Specialist", "Finds the best destinations based on your preferences"), | |
| ("🏨 Accommodation Expert", "Suggests suitable hotels and stays"), | |
| ("🚆 Transportation Planner", "Plans efficient travel routes"), | |
| ("🎯 Activities Curator", "Recommends activities tailored to your interests"), | |
| ("🍽️ Dining Connoisseur", "Finds the best dining experiences"), | |
| ("📅 Itinerary Creator", "Puts everything together in a daily plan") | |
| ] | |
| for name, desc in agents: | |
| st.markdown("**" + name + "**") | |
| st.markdown("<small>" + desc + "</small>", unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| if not st.session_state.generation_complete: | |
| st.markdown('<div class="modern-card animate-in">', unsafe_allow_html=True) | |
| st.markdown("<h3 style='font-weight: 600; color: var(--primary-dark); display: flex; align-items: center; gap: 10px;'><span style='font-size: 20px;'>✈️</span> " + t("create_itinerary") + "</h3>", unsafe_allow_html=True) | |
| st.markdown(""" | |
| <p style="color: var(--text-light); margin-bottom: 16px; font-size: 14px; font-weight: 400;">Complete the form below for a personalized travel plan.</p> | |
| """, unsafe_allow_html=True) | |
| with st.form("travel_form"): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown('<p style="font-weight: 500; color: var(--primary); font-size: 14px; margin-bottom: 12px;">Trip Details</p>', unsafe_allow_html=True) | |
| origin = st.text_input(t("origin"), placeholder="e.g., New York, USA") | |
| destination = st.text_input(t("destination"), placeholder="e.g., Paris, France") | |
| st.markdown('<p style="margin-bottom: 5px; font-size: 14px;">Travel Dates</p>', unsafe_allow_html=True) | |
| start_date = st.date_input("Start Date", min_value=datetime.now(), label_visibility="collapsed") | |
| duration = st.slider(t("duration"), min_value=1, max_value=30, value=7) | |
| end_date = start_date + timedelta(days=duration-1) | |
| st.markdown('<p style="font-size: 13px; color: var(--text-muted); margin-top: 5px;">' + start_date.strftime("%b %d") + " - " + end_date.strftime("%b %d, %Y") + '</p>', unsafe_allow_html=True) | |
| with col2: | |
| st.markdown('<p style="font-weight: 500; color: var(--primary); font-size: 14px; margin-bottom: 12px;">Preferences</p>', unsafe_allow_html=True) | |
| travelers = st.number_input("Travelers", min_value=1, max_value=15, value=2) | |
| budget_options = ["Budget", "Moderate", "Luxury"] | |
| budget = st.selectbox("Budget", budget_options, help="Budget: Economy options | Moderate: Mid-range | Luxury: High-end experiences") | |
| travel_style = st.multiselect("🌈 Travel Style", options=["Culture", "Adventure", "Relaxation", "Food & Dining", "Nature", "Shopping", "Nightlife", "Family-friendly"], default=["Culture", "Food & Dining"]) | |
| with st.expander("Additional Preferences", expanded=False): | |
| preferences = st.text_area("Interests", placeholder="History museums, local cuisine, hiking, art...") | |
| special_requirements = st.text_area("Special Requirements", placeholder="Dietary restrictions, accessibility needs...") | |
| submit_button = st.form_submit_button(t("submit")) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| if submit_button: | |
| if not origin or not destination: | |
| st.error(t("error_origin_destination")) | |
| else: | |
| st.session_state.form_submitted = True | |
| user_input = { | |
| "origin": origin, | |
| "destination": destination, | |
| "duration": str(duration), | |
| "travel_dates": f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}", | |
| "travelers": str(travelers), | |
| "budget": budget.lower(), | |
| "travel_style": ", ".join(travel_style), | |
| "preferences": preferences, | |
| "special_requirements": special_requirements | |
| } | |
| # 기존의 여행 요청 프롬프트 | |
| input_context = f"""Travel Request Details: | |
| Origin: {user_input['origin']} | |
| Destination: {user_input['destination']} | |
| Duration: {user_input['duration']} days | |
| Travel Dates: {user_input['travel_dates']} | |
| Travelers: {user_input['travelers']} | |
| Budget Level: {user_input['budget']} | |
| Travel Style: {user_input['travel_style']} | |
| Preferences/Interests: {user_input['preferences']} | |
| Special Requirements: {user_input['special_requirements']} | |
| """ | |
| # LLM에 전달할 프롬프트에 언어 지시문 추가 | |
| llm_language_instructions = { | |
| "en": "Please output the response in English.", | |
| "ko": "한국어로 출력해 주세요.", | |
| "ja": "日本語で出力してください。", | |
| "zh": "请用中文输出。", | |
| "es": "Por favor, responda en español.", | |
| "fr": "Veuillez répondre en français.", | |
| "de": "Bitte antworten Sie auf Deutsch.", | |
| "ar": "يرجى الرد باللغة العربية." | |
| } | |
| selected_lang = st.session_state.get("selected_language", "en") | |
| language_instruction = llm_language_instructions.get(selected_lang, "Please output the response in English.") | |
| modified_input_context = language_instruction + "\n" + input_context | |
| st.markdown(""" | |
| <div class="sleek-processing-container"> | |
| <div class="pulse-container"> | |
| <div class="pulse-ring"></div> | |
| <div class="pulse-core"></div> | |
| </div> | |
| </div> | |
| <style> | |
| .sleek-processing-container { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px 0; | |
| } | |
| .pulse-container { | |
| position: relative; | |
| width: 50px; | |
| height: 50px; | |
| } | |
| .pulse-core { | |
| position: absolute; | |
| left: 50%; | |
| top: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 12px; | |
| height: 12px; | |
| background-color: #4361ee; | |
| border-radius: 50%; | |
| box-shadow: 0 0 8px rgba(67, 97, 238, 0.6); | |
| } | |
| .pulse-ring { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| border: 2px solid #4361ee; | |
| border-radius: 50%; | |
| animation: pulse 1.5s ease-out infinite; | |
| opacity: 0; | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(0.1); opacity: 0; } | |
| 50% { opacity: 0.5; } | |
| 100% { transform: scale(1); opacity: 0; } | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| progress_tab, logs_tab, details_tab = st.tabs(["📊 Progress", "🔄 Live Activity", "📋 " + t("request_details")]) | |
| with details_tab: | |
| st.markdown("#### " + t("request_details")) | |
| st.markdown("**" + t("destination") + ":** " + user_input['destination']) | |
| st.markdown("**" + t("from") + ":** " + user_input['origin']) | |
| st.markdown("**" + t("when") + ":** " + user_input['travel_dates'] + " (" + user_input['duration'] + " days)") | |
| st.markdown("**" + t("budget") + ":** " + user_input['budget'].title()) | |
| st.markdown("**" + t("travel_style") + ":** " + user_input['travel_style']) | |
| if user_input['preferences']: | |
| st.markdown("**Interests:** " + user_input['preferences']) | |
| if user_input['special_requirements']: | |
| st.markdown("**Special Requirements:** " + user_input['special_requirements']) | |
| with progress_tab: | |
| if 'progress_placeholder' not in st.session_state: | |
| st.session_state.progress_placeholder = st.empty() | |
| with st.session_state.progress_placeholder.container(): | |
| display_modern_progress(0) | |
| with logs_tab: | |
| log_container = st.container() | |
| st.session_state.log_messages = [] | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| output_container = st.container() | |
| with output_container: | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown("### 🌟 " + t("live_agent_outputs")) | |
| st.info("Our AI agents will show their work here as they create your itinerary") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.session_state.current_step = 0 | |
| update_step_status(0, 'active') | |
| with st.session_state.progress_placeholder.container(): | |
| display_modern_progress(st.session_state.current_step) | |
| destination_info = run_task_with_logs( | |
| destination_research_task, | |
| modified_input_context.format(destination=user_input['destination'], preferences=user_input['preferences']), | |
| log_container, | |
| output_container, | |
| "destination_info" | |
| ) | |
| update_step_status(0, 'complete') | |
| st.session_state.current_step = 1 | |
| update_step_status(1, 'active') | |
| with st.session_state.progress_placeholder.container(): | |
| display_modern_progress(st.session_state.current_step) | |
| accommodation_info = run_task_with_logs( | |
| accommodation_task, | |
| modified_input_context.format(destination=user_input['destination'], budget=user_input['budget'], preferences=user_input['preferences']), | |
| log_container, | |
| output_container, | |
| "accommodation_info" | |
| ) | |
| update_step_status(1, 'complete') | |
| st.session_state.current_step = 2 | |
| update_step_status(2, 'active') | |
| with st.session_state.progress_placeholder.container(): | |
| display_modern_progress(st.session_state.current_step) | |
| transportation_info = run_task_with_logs( | |
| transportation_task, | |
| modified_input_context.format(origin=user_input['origin'], destination=user_input['destination']), | |
| log_container, | |
| output_container, | |
| "transportation_info" | |
| ) | |
| update_step_status(2, 'complete') | |
| st.session_state.current_step = 3 | |
| update_step_status(3, 'active') | |
| with st.session_state.progress_placeholder.container(): | |
| display_modern_progress(st.session_state.current_step) | |
| activities_info = run_task_with_logs( | |
| activities_task, | |
| modified_input_context.format(destination=user_input['destination'], preferences=user_input['preferences']), | |
| log_container, | |
| output_container, | |
| "activities_info" | |
| ) | |
| update_step_status(3, 'complete') | |
| st.session_state.current_step = 4 | |
| update_step_status(4, 'active') | |
| with st.session_state.progress_placeholder.container(): | |
| display_modern_progress(st.session_state.current_step) | |
| dining_info = run_task_with_logs( | |
| dining_task, | |
| modified_input_context.format(destination=user_input['destination'], preferences=user_input['preferences']), | |
| log_container, | |
| output_container, | |
| "dining_info" | |
| ) | |
| update_step_status(4, 'complete') | |
| st.session_state.current_step = 5 | |
| update_step_status(5, 'active') | |
| with st.session_state.progress_placeholder.container(): | |
| display_modern_progress(st.session_state.current_step) | |
| combined_info = f"""{input_context} | |
| Destination Information: | |
| {destination_info} | |
| Accommodation Options: | |
| {accommodation_info} | |
| Transportation Plan: | |
| {transportation_info} | |
| Recommended Activities: | |
| {activities_info} | |
| Dining Recommendations: | |
| {dining_info} | |
| """ | |
| itinerary = run_task_with_logs( | |
| itinerary_task, | |
| combined_info.format(duration=user_input['duration'], origin=user_input['origin'], destination=user_input['destination']), | |
| log_container, | |
| output_container, | |
| "itinerary" | |
| ) | |
| update_step_status(5, 'complete') | |
| st.session_state.current_step = 6 | |
| with st.session_state.progress_placeholder.container(): | |
| display_modern_progress(st.session_state.current_step) | |
| st.session_state.generated_itinerary = itinerary | |
| st.session_state.generation_complete = True | |
| date_str = datetime.now().strftime("%Y-%m-%d") | |
| st.session_state.filename = f"{user_input['destination'].replace(' ', '_')}_{date_str}_itinerary.txt" | |
| if st.session_state.generation_complete: | |
| st.markdown(""" | |
| <div class="modern-card animate-in"> | |
| <div style="display: flex; justify-content: center; margin-bottom: 20px;"> | |
| <div class="success-animation"> | |
| <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52"> | |
| <circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none" /> | |
| <path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <h2 style="text-align: center; color: #4361ee;">""" + t("itinerary_ready") + """</h2> | |
| <p style="text-align: center; color: #6c757d; margin-bottom: 20px;">""" + t("personalized_experience") + """</p> | |
| </div> | |
| <style> | |
| .success-animation { | |
| width: 100px; | |
| height: 100px; | |
| position: relative; | |
| } | |
| .checkmark { | |
| width: 100px; | |
| height: 100px; | |
| border-radius: 50%; | |
| display: block; | |
| stroke-width: 2; | |
| stroke: #4361ee; | |
| stroke-miterlimit: 10; | |
| box-shadow: 0 0 20px rgba(67, 97, 238, 0.3); | |
| animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both; | |
| } | |
| .checkmark__circle { | |
| stroke-dasharray: 166; | |
| stroke-dashoffset: 166; | |
| stroke-width: 2; | |
| stroke-miterlimit: 10; | |
| stroke: #4361ee; | |
| fill: none; | |
| animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards; | |
| } | |
| .checkmark__check { | |
| transform-origin: 50% 50%; | |
| stroke-dasharray: 48; | |
| stroke-dashoffset: 48; | |
| animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards; | |
| } | |
| @keyframes stroke { | |
| 100% { stroke-dashoffset: 0; } | |
| } | |
| @keyframes scale { | |
| 0%, 100% { transform: none; } | |
| 50% { transform: scale3d(1.1, 1.1, 1); } | |
| } | |
| @keyframes fill { | |
| 100% { box-shadow: 0 0 20px rgba(67, 97, 238, 0.3); } | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # 추가된 탭: 전체 일정, 상세 정보, 다운로드/공유, 지도 및 시각화, AI 챗봇 인터페이스 | |
| itinerary_tab, details_tab, download_tab, map_tab, chatbot_tab = st.tabs([ | |
| "🗒️ " + t("full_itinerary"), | |
| "💼 " + t("details"), | |
| "💾 " + t("download_share"), | |
| "🗺️ 지도 및 시각화", | |
| "🤖 챗봇 인터페이스" | |
| ]) | |
| # 일정 탭 | |
| with itinerary_tab: | |
| st.text_area("Your Itinerary", st.session_state.generated_itinerary, height=600) | |
| # 상세 정보 탭 | |
| with details_tab: | |
| agent_tabs = st.tabs(["🌎 Destination", "🏨 Accommodation", "🚗 Transportation", "🎭 Activities", "🍽️ Dining"]) | |
| with agent_tabs[0]: | |
| st.markdown("### 🌎 Destination Research") | |
| st.markdown(st.session_state.results["destination_info"]) | |
| with agent_tabs[1]: | |
| st.markdown("### 🏨 Accommodation Options") | |
| st.markdown(st.session_state.results["accommodation_info"]) | |
| with agent_tabs[2]: | |
| st.markdown("### 🚗 Transportation Plan") | |
| st.markdown(st.session_state.results["transportation_info"]) | |
| with agent_tabs[3]: | |
| st.markdown("### 🎭 Recommended Activities") | |
| st.markdown(st.session_state.results["activities_info"]) | |
| with agent_tabs[4]: | |
| st.markdown("### 🍽️ Dining Recommendations") | |
| st.markdown(st.session_state.results["dining_info"]) | |
| # 다운로드 및 공유 탭 | |
| with download_tab: | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| st.markdown("### " + t("save_itinerary")) | |
| st.markdown("Download your personalized travel plan to access it offline or share with your travel companions.") | |
| st.markdown(""" | |
| <div style="background-color: #f8f9fa; padding: 15px; border-radius: 10px; margin-top: 20px;"> | |
| <h4 style="margin-top: 0;">""" + t("your_itinerary_file") + """</h4> | |
| <p style="font-size: 0.9rem; color: #6c757d;">""" + t("text_format") + """</p> | |
| """, unsafe_allow_html=True) | |
| st.markdown("<div style='margin: 10px 0;'>" + get_download_link(st.session_state.generated_itinerary, st.session_state.filename) + "</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown("### " + t("share_itinerary")) | |
| st.markdown("*Coming soon: Email your itinerary or share via social media.*") | |
| with col2: | |
| st.markdown("### " + t("save_for_mobile")) | |
| st.markdown("*Coming soon: QR code for easy access on your phone*") | |
| # 인터랙티브 지도 및 시각화 탭 | |
| with map_tab: | |
| st.markdown("### 목적지 지도") | |
| # 예시: 목적지 주변의 주요 명소 좌표 데이터 (실제 API나 DB를 통해 동적으로 가져올 수 있음) | |
| map_data = pd.DataFrame({ | |
| "lat": [48.8584, 48.8606, 48.8529], | |
| "lon": [2.2945, 2.3376, 2.3500], | |
| "name": ["Eiffel Tower", "Louvre Museum", "Notre Dame"] | |
| }) | |
| # 기본 지도 출력 (st.map) | |
| st.map(map_data) | |
| st.markdown("#### Pydeck을 활용한 인터랙티브 지도 예시") | |
| layer = pdk.Layer( | |
| "ScatterplotLayer", | |
| data=map_data, | |
| get_position='[lon, lat]', | |
| get_color='[200, 30, 0, 160]', | |
| get_radius=200, | |
| ) | |
| view_state = pdk.ViewState( | |
| latitude=48.8566, | |
| longitude=2.3522, | |
| zoom=12, | |
| pitch=50, | |
| ) | |
| deck_chart = pdk.Deck(layers=[layer], initial_view_state=view_state) | |
| st.pydeck_chart(deck_chart) | |
| # AI 챗봇 인터페이스 탭 (제미나이 적용) | |
| with chatbot_tab: | |
| st.markdown("### AI 챗봇 인터페이스") | |
| # 대화 기록을 세션 상태에 저장 (메시지, 발신자, 타임스탬프) | |
| if "chat_history" not in st.session_state: | |
| st.session_state.chat_history = [] | |
| # 사용자 입력창 및 전송 버튼 | |
| user_message = st.text_input("메시지를 입력하세요:", key="chat_input") | |
| if st.button("전송", key="send_button"): | |
| if user_message: | |
| # 제미나이 기반 챗봇 응답: run_task()를 활용하여 chatbot_task에 질의 | |
| response = run_task(chatbot_task, user_message) | |
| st.session_state.chat_history.append({ | |
| "speaker": "사용자", | |
| "message": user_message, | |
| "time": datetime.now() | |
| }) | |
| st.session_state.chat_history.append({ | |
| "speaker": "AI", | |
| "message": response, | |
| "time": datetime.now() | |
| }) | |
| # 대화 기록 출력 (타임스탬프 포함, 스크롤 가능한 영역) | |
| st.markdown("<div style='max-height:400px; overflow-y:auto; padding:10px; border:1px solid #eaeaea; border-radius:6px;'>", unsafe_allow_html=True) | |
| for chat in st.session_state.chat_history: | |
| time_str = chat["time"].strftime("%H:%M:%S") | |
| st.markdown(f"**{chat['speaker']}** ({time_str}): {chat['message']}") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div style="margin-top: 50px; text-align: center; padding: 20px; color: #6c757d; font-size: 0.8rem;"> | |
| <p>""" + t("built_with") + """</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |