Spaces:
No application file
No application file
| """ | |
| Modern Gradio Application for Transportation Prediction | |
| Uses Gradio 5.x best practices with clean architecture | |
| """ | |
| import gradio as gr | |
| import requests | |
| import json | |
| import asyncio | |
| import aiohttp | |
| from datetime import datetime | |
| from typing import Optional, Dict, Any, List, AsyncGenerator | |
| import os | |
| from dotenv import load_dotenv | |
| # Load environment variables | |
| load_dotenv() | |
| # Configuration | |
| API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:3454/api") | |
| PREDICT_URL = f"{API_BASE_URL}/predict-transportation" | |
| OPTIONS_URL = f"{API_BASE_URL}/transportation-options" | |
| CHAT_URL = f"{API_BASE_URL}/chat" | |
| # Check if running on HF Spaces | |
| IS_HF_SPACES = os.getenv("SPACE_ID") is not None | |
| class APIClient: | |
| """Unified API client for all endpoints""" | |
| def __init__(self): | |
| self.session = requests.Session() | |
| self.session.timeout = 30 | |
| self.has_backend = False | |
| self.local_predictor = None | |
| # Always load options cache first | |
| self.options_cache = self._load_options() | |
| # Initialize local predictor for standalone mode | |
| self._init_local_predictor() | |
| def _init_local_predictor(self): | |
| """Initialize local predictor for standalone mode""" | |
| try: | |
| from src.app.api.predict import TransportationPredictor | |
| self.local_predictor = TransportationPredictor() | |
| print("✅ Local predictor initialized successfully") | |
| except Exception as e: | |
| print(f"⚠️ Could not initialize local predictor: {e}") | |
| print("Will use fallback prediction method") | |
| self.local_predictor = None | |
| def _load_options(self) -> Dict[str, List[str]]: | |
| """Load available options from API""" | |
| try: | |
| response = self.session.get(OPTIONS_URL) | |
| response.raise_for_status() | |
| self.has_backend = True | |
| return response.json() | |
| except Exception: | |
| self.has_backend = False | |
| return { | |
| "shipment_modes": ["Air", "Air Charter", "Ocean", "Truck"], | |
| "sample_vendors": ["ABBOTT LABORATORIES", "PFIZER", "MERCK"], | |
| "sample_countries": ["Vietnam", "Thailand", "Indonesia"], | |
| "sample_projects": ["100-CI-T01", "200-MA-T02", "300-VN-T03"] | |
| } | |
| def predict_transportation(self, **kwargs) -> str: | |
| """Predict transportation mode""" | |
| try: | |
| # Validate required fields | |
| required_fields = ["project_code", "country", "pack_price", "vendor"] | |
| for field in required_fields: | |
| if not kwargs.get(field): | |
| return f"❌ **Error:** Thiếu thông tin bắt buộc: {field}" | |
| if kwargs.get("pack_price", 0) <= 0: | |
| return "❌ **Error:** Giá gói phải lớn hơn 0" | |
| # Try backend API first | |
| if self.has_backend: | |
| return self._predict_via_api(**kwargs) | |
| # Fallback to local predictor for HF Spaces | |
| elif hasattr(self, 'local_predictor') and self.local_predictor: | |
| return self._predict_via_local(**kwargs) | |
| else: | |
| return "❌ **Error:** No prediction service available" | |
| except Exception as e: | |
| return f"❌ **Unexpected Error:** {str(e)}" | |
| def _predict_via_api(self, **kwargs) -> str: | |
| """Predict via backend API""" | |
| # Prepare request data | |
| request_data = { | |
| "project_code": kwargs["project_code"], | |
| "country": kwargs["country"], | |
| "pack_price": float(kwargs["pack_price"]), | |
| "vendor": kwargs["vendor"] | |
| } | |
| # Add optional fields | |
| optional_fields = ["weight_kg", "freight_cost_usd", "delivery_date", "line_item_quantity"] | |
| for field in optional_fields: | |
| value = kwargs.get(field) | |
| if value and (field in ["weight_kg", "freight_cost_usd", "line_item_quantity"] and value > 0) or field == "delivery_date": | |
| request_data[field] = value | |
| # Make API call | |
| response = self.session.post(PREDICT_URL, json=request_data) | |
| response.raise_for_status() | |
| result = response.json() | |
| return self._format_prediction_result(result) | |
| def _predict_via_local(self, **kwargs) -> str: | |
| """Predict via local predictor""" | |
| if self.local_predictor: | |
| try: | |
| from src.app.schema.transportation import TransportationRequest | |
| # Create request object | |
| request = TransportationRequest( | |
| project_code=kwargs["project_code"], | |
| country=kwargs["country"], | |
| pack_price=float(kwargs["pack_price"]), | |
| vendor=kwargs["vendor"], | |
| weight_kg=kwargs.get("weight_kg"), | |
| freight_cost_usd=kwargs.get("freight_cost_usd"), | |
| delivery_date=kwargs.get("delivery_date"), | |
| line_item_quantity=kwargs.get("line_item_quantity") | |
| ) | |
| # Get prediction | |
| result = self.local_predictor.predict_shipment_mode(request) | |
| # Convert response to dict format | |
| result_dict = { | |
| "predicted_shipment_mode": result.predicted_shipment_mode, | |
| "confidence_score": result.confidence_score, | |
| "alternative_modes": result.alternative_modes, | |
| "estimated_weight_kg": result.estimated_weight_kg, | |
| "estimated_freight_cost_usd": result.estimated_freight_cost_usd, | |
| "encoded_features": result.encoded_features | |
| } | |
| return self._format_prediction_result(result_dict) | |
| except Exception as e: | |
| print(f"Local predictor error: {e}") | |
| return self._fallback_prediction(**kwargs) | |
| else: | |
| return self._fallback_prediction(**kwargs) | |
| def _fallback_prediction(self, **kwargs) -> str: | |
| """Simple rule-based fallback prediction""" | |
| pack_price = float(kwargs.get("pack_price", 0)) | |
| weight = kwargs.get("weight_kg") or 10.0 # Default weight | |
| country = kwargs.get("country", "").lower() | |
| # Simple rules | |
| if pack_price > 100 or "urgent" in country: | |
| mode = "Air" | |
| confidence = 0.75 | |
| elif weight > 100 or pack_price < 30: | |
| mode = "Ocean" | |
| confidence = 0.70 | |
| elif any(c in country for c in ["vietnam", "thailand", "singapore"]): | |
| mode = "Truck" | |
| confidence = 0.65 | |
| else: | |
| mode = "Air Charter" | |
| confidence = 0.60 | |
| # Create fake result for display | |
| result_dict = { | |
| "predicted_shipment_mode": mode, | |
| "confidence_score": confidence, | |
| "alternative_modes": [ | |
| {"mode": "Ocean", "probability": 0.25}, | |
| {"mode": "Truck", "probability": 0.15} | |
| ], | |
| "estimated_weight_kg": weight, | |
| "estimated_freight_cost_usd": pack_price * 0.1, | |
| "encoded_features": { | |
| "Project_Code": 1, | |
| "Country": 1, | |
| "Vendor": 1 | |
| } | |
| } | |
| return self._format_prediction_result(result_dict) + """ | |
| <div style="background: #fef3c7; padding: 1rem; border-radius: 8px; border-left: 4px solid #f59e0b; margin: 1rem 0;"> | |
| <p style="margin: 0; color: #92400e;"><strong>⚠️ Demo Mode:</strong> Đang sử dụng rule-based prediction. Kết quả chỉ mang tính chất tham khảo.</p> | |
| </div> | |
| """ | |
| def _format_prediction_result(self, result: Dict) -> str: | |
| """Format prediction result for display with beautiful markdown""" | |
| confidence = result.get('confidence_score', 0) | |
| mode = result.get('predicted_shipment_mode', 'Unknown') | |
| # Emoji for transport modes | |
| mode_emoji = { | |
| 'Air': '✈️', 'Air Charter': '🛩️', 'Ocean': '🚢', | |
| 'Truck': '🚛', 'Rail': '🚆', 'Express': '📦' | |
| } | |
| # Confidence color coding | |
| conf_color = "#10b981" if confidence >= 0.8 else "#f59e0b" if confidence >= 0.6 else "#ef4444" | |
| conf_emoji = "🎯" if confidence >= 0.8 else "⚠️" if confidence >= 0.6 else "❌" | |
| output = f""" | |
| <div style="background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); padding: 1.5rem; border-radius: 12px; border-left: 4px solid {conf_color}; margin: 1rem 0;"> | |
| ## {mode_emoji.get(mode, '🚚')} **Kết quả dự đoán vận chuyển** | |
| <div style="display: flex; justify-content: space-between; align-items: center; background: white; padding: 1rem; border-radius: 8px; margin: 1rem 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1);"> | |
| <div> | |
| <h3 style="margin: 0; color: #1e293b;">🎯 <strong>Phương thức đề xuất</strong></h3> | |
| <p style="font-size: 1.4rem; font-weight: 700; color: {conf_color}; margin: 0.5rem 0;">{mode_emoji.get(mode, '🚚')} {mode}</p> | |
| </div> | |
| <div style="text-align: right;"> | |
| <h3 style="margin: 0; color: #1e293b;">{conf_emoji} <strong>Độ tin cậy</strong></h3> | |
| <p style="font-size: 1.4rem; font-weight: 700; color: {conf_color}; margin: 0.5rem 0;">{confidence:.1%}</p> | |
| </div> | |
| </div> | |
| """ | |
| # Add estimates if available | |
| if result.get('estimated_weight_kg') or result.get('estimated_freight_cost_usd'): | |
| output += """ | |
| ### 📊 **Ước tính chi phí & trọng lượng** | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0;"> | |
| """ | |
| if result.get('estimated_weight_kg'): | |
| weight = result['estimated_weight_kg'] | |
| output += f""" | |
| <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"> | |
| <div style="font-size: 2rem;">⚖️</div> | |
| <div style="font-weight: 600; color: #1e293b;">Khối lượng</div> | |
| <div style="font-size: 1.2rem; font-weight: 700; color: #0ea5e9;">{weight:.1f} kg</div> | |
| </div> | |
| """ | |
| if result.get('estimated_freight_cost_usd'): | |
| cost = result['estimated_freight_cost_usd'] | |
| output += f""" | |
| <div style="background: white; padding: 1rem; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"> | |
| <div style="font-size: 2rem;">💰</div> | |
| <div style="font-weight: 600; color: #1e293b;">Chi phí vận chuyển</div> | |
| <div style="font-size: 1.2rem; font-weight: 700; color: #10b981;">${cost:.2f}</div> | |
| </div> | |
| """ | |
| output += "</div>\n" | |
| # Add alternatives with beautiful styling | |
| alternatives = result.get('alternative_modes', []) | |
| if alternatives: | |
| output += """ | |
| ### 🔄 **Các lựa chọn khác** | |
| <div style="background: white; padding: 1rem; border-radius: 8px; margin: 1rem 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"> | |
| """ | |
| for i, alt in enumerate(alternatives[:3]): # Show top 3 alternatives | |
| alt_mode = alt.get('mode', 'Unknown') | |
| alt_prob = alt.get('probability', 0) | |
| alt_emoji = mode_emoji.get(alt_mode, '📦') | |
| bar_width = int(alt_prob * 100) | |
| bar_color = "#8b5cf6" if i == 0 else "#06b6d4" if i == 1 else "#84cc16" | |
| output += f""" | |
| <div style="margin: 0.5rem 0; padding: 0.5rem;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;"> | |
| <span style="font-weight: 600;">{alt_emoji} {alt_mode}</span> | |
| <span style="font-weight: 700; color: {bar_color};">{alt_prob:.1%}</span> | |
| </div> | |
| <div style="background: #e2e8f0; height: 8px; border-radius: 4px; overflow: hidden;"> | |
| <div style="background: {bar_color}; height: 100%; width: {bar_width}%; transition: width 0.5s ease;"></div> | |
| </div> | |
| </div> | |
| """ | |
| output += "</div>\n" | |
| # Add project details in a clean format | |
| if result.get('encoded_features'): | |
| output += """ | |
| ### 🔧 **Chi tiết dự án** | |
| <div style="background: white; padding: 1rem; border-radius: 8px; margin: 1rem 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;"> | |
| """ | |
| features = result['encoded_features'] | |
| feature_icons = { | |
| 'Project_Code': '🏷️', 'Country': '🌍', 'Vendor': '🏢', | |
| 'Pack_Price': '💰', 'Weight': '⚖️', 'Delivery_Date': '📅' | |
| } | |
| for key, value in features.items(): | |
| icon = feature_icons.get(key, '📊') | |
| display_key = key.replace('_', ' ').title() | |
| output += f""" | |
| <div style="text-align: center; padding: 0.5rem;"> | |
| <div style="font-size: 1.5rem; margin-bottom: 0.25rem;">{icon}</div> | |
| <div style="font-size: 0.85rem; color: #64748b; font-weight: 500;">{display_key}</div> | |
| <div style="font-weight: 600; color: #1e293b;">{value}</div> | |
| </div> | |
| """ | |
| output += """ | |
| </div> | |
| </div> | |
| """ | |
| output += """ | |
| </div> | |
| <div style="text-align: center; margin-top: 1rem; padding: 1rem; background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%); border-radius: 8px;"> | |
| <p style="margin: 0; color: #0369a1; font-weight: 600;">✨ Dự đoán được tạo bởi AI với độ chính xác cao</p> | |
| </div> | |
| """ | |
| return output | |
| async def chat_stream(self, message: str) -> AsyncGenerator[str, None]: | |
| """Stream chat with API backend or fallback to local chat""" | |
| if not message.strip(): | |
| yield "Vui lòng nhập tin nhắn" | |
| return | |
| # Try backend API first | |
| if self.has_backend: | |
| try: | |
| async for chunk in self._chat_via_api(message): | |
| yield chunk | |
| return | |
| except Exception as e: | |
| yield f"❌ Backend error, switching to local mode: {str(e)}" | |
| # Fallback to local chat | |
| async for chunk in self._chat_local(message): | |
| yield chunk | |
| async def _chat_via_api(self, message: str) -> AsyncGenerator[str, None]: | |
| """Chat via backend API""" | |
| async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60)) as session: | |
| request_data = {"message": message} | |
| async with session.post(CHAT_URL, json=request_data) as response: | |
| if response.status != 200: | |
| error_text = await response.text() | |
| raise Exception(f"API Error {response.status}: {error_text}") | |
| accumulated_text = "" | |
| async for line in response.content: | |
| line_str = line.decode('utf-8').strip() | |
| if line_str.startswith("data: "): | |
| data_str = line_str[6:].strip() | |
| if data_str == "[DONE]": | |
| break | |
| try: | |
| chunk_data = json.loads(data_str) | |
| event_type = chunk_data.get("event") | |
| if event_type == "delta": | |
| content = chunk_data.get("content", "") | |
| accumulated_text += content | |
| yield accumulated_text | |
| elif event_type == "status": | |
| stage = chunk_data.get("stage", "") | |
| message_text = chunk_data.get("message", "") | |
| if stage == "starting": | |
| yield f"🔄 {message_text}" | |
| elif stage == "error": | |
| yield f"❌ Error: {message_text}" | |
| elif event_type == "final": | |
| final_content = chunk_data.get("content", accumulated_text) | |
| if final_content and final_content.strip(): | |
| yield final_content | |
| else: | |
| yield accumulated_text or "✅ Đã xử lý xong" | |
| except json.JSONDecodeError: | |
| continue | |
| # Ensure we have some output | |
| if not accumulated_text.strip(): | |
| yield "✅ Đã xử lý xong" | |
| async def _chat_local(self, message: str) -> AsyncGenerator[str, None]: | |
| """Local chat fallback for HF Spaces""" | |
| yield "🤖 Đang suy nghĩ..." | |
| await asyncio.sleep(1) | |
| # Simple rule-based responses for demo | |
| message_lower = message.lower() | |
| if any(word in message_lower for word in ["air", "máy bay", "hàng không"]): | |
| response = """ | |
| ## ✈️ Air Transportation | |
| **Ưu điểm:** | |
| - Tốc độ nhanh nhất (1-3 ngày) | |
| - Phù hợp hàng hóa có giá trị cao | |
| - An toàn và bảo mật tốt | |
| **Nhược điểm:** | |
| - Chi phí cao nhất | |
| - Hạn chế về trọng lượng và kích thước | |
| - Phụ thuộc thời tiết | |
| **Khi nào nên dùng:** | |
| - Hàng hóa khẩn cấp | |
| - Sản phẩm có giá trị cao/kg | |
| - Khoảng cách xa (intercontinental) | |
| """ | |
| elif any(word in message_lower for word in ["ocean", "biển", "tàu"]): | |
| response = """ | |
| ## 🚢 Ocean Transportation | |
| **Ưu điểm:** | |
| - Chi phí thấp nhất cho hàng lớn | |
| - Có thể vận chuyển khối lượng lớn | |
| - Thân thiện môi trường | |
| **Nhược điểm:** | |
| - Thời gian lâu (2-6 tuần) | |
| - Chịu ảnh hưởng thời tiết biển | |
| - Cần cảng biển | |
| **Khi nào nên dùng:** | |
| - Hàng hóa không cấp bách | |
| - Khối lượng lớn, giá trị thấp/kg | |
| - Vận chuyển container | |
| """ | |
| elif any(word in message_lower for word in ["truck", "xe tải", "đường bộ"]): | |
| response = """ | |
| ## 🚛 Truck Transportation | |
| **Ưu điểm:** | |
| - Linh hoạt door-to-door | |
| - Kiểm soát tốt thời gian | |
| - Phù hợp khoảng cách ngắn-trung | |
| **Nhược điểm:** | |
| - Hạn chế về khoảng cách xa | |
| - Ảnh hưởng giao thông, thời tiết | |
| - Chi phí cao với khoảng cách xa | |
| **Khi nào nên dùng:** | |
| - Vận chuyển nội địa/khu vực | |
| - Hàng cần giao nhanh | |
| - Không có cảng/sân bay gần | |
| """ | |
| elif any(word in message_lower for word in ["dự đoán", "predict", "model"]): | |
| response = """ | |
| ## 🔮 AI Prediction Model | |
| Hệ thống sử dụng **XGBoost** để dự đoán phương thức vận chuyển tối ưu dựa trên: | |
| **Input Features:** | |
| - 🏷️ Project Code | |
| - 🌍 Destination Country | |
| - 💰 Pack Price | |
| - 🏢 Vendor | |
| - ⚖️ Weight (optional) | |
| - 🚢 Freight Cost (optional) | |
| **Model Performance:** | |
| - Độ chính xác: ~90% | |
| - Hỗ trợ 4 modes: Air, Air Charter, Ocean, Truck | |
| - Real-time prediction với confidence score | |
| """ | |
| else: | |
| response = f""" | |
| ## 💡 Transportation Assistant | |
| Xin chào! Tôi có thể giúp bạn về: | |
| 🔮 **Dự đoán vận chuyển** - Phân tích phương thức tối ưu | |
| 📊 **So sánh modes** - Air vs Ocean vs Truck | |
| 💰 **Tối ưu chi phí** - Cân bằng thời gian và chi phí | |
| 🌍 **Logistics quốc tế** - Quy trình xuất nhập khẩu | |
| **Câu hỏi của bạn:** "{message}" | |
| Hãy hỏi cụ thể hơn về Air, Ocean, Truck transportation hoặc prediction model! | |
| """ | |
| # Simulate streaming | |
| words = response.split() | |
| current_text = "" | |
| for i, word in enumerate(words): | |
| current_text += word + " " | |
| if i % 3 == 0: # Update every 3 words | |
| yield current_text | |
| await asyncio.sleep(0.1) | |
| yield response # Final complete response | |
| # Global API client | |
| api_client = APIClient() | |
| def create_prediction_tab(): | |
| """Create transportation prediction tab""" | |
| options = api_client.options_cache | |
| with gr.Column(elem_classes="prediction-container"): | |
| # Tab header | |
| gr.HTML(""" | |
| <div style="text-align: center; padding: 2rem; background: linear-gradient(135deg, #059669 0%, #10b981 50%, #34d399 100%); color: white; border-radius: 16px; margin-bottom: 2rem; box-shadow: 0 6px 24px rgba(5,150,105,0.2);"> | |
| <div style="display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 0.5rem;"> | |
| <div style="background: rgba(255,255,255,0.2); padding: 0.5rem; border-radius: 8px;"> | |
| <span style="font-size: 1.5rem;">🚚</span> | |
| </div> | |
| <h2 style="margin: 0; font-size: 1.8rem; font-weight: 700; text-shadow: 0 2px 4px rgba(0,0,0,0.2);">Smart Transportation Prediction</h2> | |
| </div> | |
| <p style="margin: 0; opacity: 0.95; font-size: 1rem; font-weight: 400;">AI-powered phương thức vận chuyển tối ưu cho đơn hàng của bạn</p> | |
| </div> | |
| """) | |
| with gr.Row(equal_height=True): | |
| # Input Form | |
| with gr.Column(scale=5, elem_classes="input-form"): | |
| with gr.Group(): | |
| gr.Markdown("### � **Thông tin đơn hàng**") | |
| with gr.Row(): | |
| project_code = gr.Dropdown( | |
| choices=options.get("sample_projects", []), | |
| label="🏷️ Project Code", | |
| allow_custom_value=True, | |
| value="100-CI-T01", | |
| info="Mã dự án vận chuyển", | |
| elem_classes="input-field" | |
| ) | |
| country = gr.Dropdown( | |
| choices=options.get("sample_countries", []), | |
| label="🌍 Destination Country", | |
| allow_custom_value=True, | |
| value="Vietnam", | |
| info="Quốc gia đích của đơn hàng", | |
| elem_classes="input-field" | |
| ) | |
| with gr.Row(): | |
| pack_price = gr.Number( | |
| label="💰 Pack Price (USD)", | |
| value=50.0, | |
| minimum=0.01, | |
| info="Giá mỗi gói hàng (USD)", | |
| elem_classes="input-field" | |
| ) | |
| vendor = gr.Dropdown( | |
| choices=options.get("sample_vendors", []), | |
| label="🏢 Vendor", | |
| allow_custom_value=True, | |
| value="ABBOTT LABORATORIES", | |
| info="Nhà cung cấp hàng hóa", | |
| elem_classes="input-field" | |
| ) | |
| # Advanced Options | |
| with gr.Accordion("⚙️ Thông tin chi tiết (Tùy chọn)", open=False): | |
| with gr.Row(): | |
| weight_kg = gr.Number( | |
| label="⚖️ Weight (kg)", | |
| value=None, | |
| minimum=0, | |
| info="Khối lượng hàng hóa", | |
| elem_classes="input-field" | |
| ) | |
| freight_cost_usd = gr.Number( | |
| label="🚢 Freight Cost (USD)", | |
| value=None, | |
| minimum=0, | |
| info="Chi phí vận chuyển ước tính", | |
| elem_classes="input-field" | |
| ) | |
| with gr.Row(): | |
| delivery_date = gr.Textbox( | |
| label="📅 Delivery Date", | |
| placeholder="YYYY-MM-DD (vd: 2025-08-20)", | |
| info="Ngày giao hàng mong muốn", | |
| elem_classes="input-field" | |
| ) | |
| line_item_quantity = gr.Number( | |
| label="📦 Quantity", | |
| value=100.0, | |
| minimum=0, | |
| info="Số lượng sản phẩm", | |
| elem_classes="input-field" | |
| ) | |
| # Action Button | |
| with gr.Row(): | |
| predict_btn = gr.Button( | |
| "🔮 Predict Transportation Mode", | |
| variant="primary", | |
| size="lg", | |
| elem_classes="btn-primary", | |
| scale=1 | |
| ) | |
| # Results Panel | |
| with gr.Column(scale=7, elem_classes="results-panel"): | |
| gr.Markdown("### 📊 **Kết quả dự đoán**") | |
| result_output = gr.HTML( | |
| value=""" | |
| <div style="text-align: center; padding: 3rem; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-radius: 12px; border: 2px dashed #cbd5e1;"> | |
| <div style="font-size: 3rem; margin-bottom: 1rem;">🎯</div> | |
| <h3 style="color: #64748b; margin-bottom: 0.5rem;">Sẵn sàng dự đoán</h3> | |
| <p style="color: #94a3b8; margin: 0;">Nhập thông tin đơn hàng và nhấn <strong>Predict</strong> để nhận kết quả AI</p> | |
| </div> | |
| """, | |
| elem_classes="results-display" | |
| ) | |
| # Event handler | |
| # Prediction wrapper function | |
| def predict_wrapper(*args): | |
| result = api_client.predict_transportation( | |
| project_code=args[0], country=args[1], pack_price=args[2], vendor=args[3], | |
| weight_kg=args[4], freight_cost_usd=args[5], delivery_date=args[6], | |
| line_item_quantity=args[7] | |
| ) | |
| return result | |
| predict_btn.click( | |
| fn=predict_wrapper, | |
| inputs=[project_code, country, pack_price, vendor, | |
| weight_kg, freight_cost_usd, delivery_date, line_item_quantity], | |
| outputs=result_output | |
| ) | |
| # Enhanced Examples section | |
| with gr.Accordion("💡 Ví dụ thực tế", open=False): | |
| gr.HTML(""" | |
| <div style="margin-bottom: 1rem;"> | |
| <h4 style="color: #4f46e5; margin-bottom: 0.5rem;">📋 Các trường hợp thường gặp:</h4> | |
| <p style="color: #64748b; font-size: 0.9rem;">Click vào ví dụ để tự động điền thông tin</p> | |
| </div> | |
| """) | |
| gr.Examples( | |
| examples=[ | |
| ["100-CI-T01", "Vietnam", 50.0, "ABBOTT LABORATORIES", None, None, "", 100.0], | |
| ["200-MA-T02", "Thailand", 75.0, "PFIZER", 25.0, 500.0, "2025-09-01", 150.0], | |
| ["300-VN-T03", "Indonesia", 30.0, "MERCK", None, None, "2025-08-25", 80.0], | |
| ["400-SG-T04", "Singapore", 120.0, "JOHNSON & JOHNSON", 15.0, 300.0, "2025-08-30", 200.0], | |
| ["500-MY-T05", "Malaysia", 85.0, "NOVARTIS", None, None, "", 75.0] | |
| ], | |
| inputs=[project_code, country, pack_price, vendor, | |
| weight_kg, freight_cost_usd, delivery_date, line_item_quantity], | |
| label="🎯 Scenario Templates", | |
| examples_per_page=5 | |
| ) | |
| def create_chat_tab(): | |
| """Create AI chat tab with real-time streaming""" | |
| def chat_response_streaming(message, history): | |
| """Handle chat response with real-time streaming""" | |
| if not message.strip(): | |
| yield history, "" | |
| return | |
| # Add user message to history | |
| if history is None: | |
| history = [] | |
| # Add user message with nice formatting | |
| history.append({ | |
| "role": "user", | |
| "content": message | |
| }) | |
| # Add initial AI message placeholder | |
| history.append({ | |
| "role": "assistant", | |
| "content": "🤖 Đang suy nghĩ..." | |
| }) | |
| yield history, "" | |
| # Get streaming response | |
| try: | |
| import asyncio | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| accumulated_content = "" | |
| async def stream_response(): | |
| nonlocal accumulated_content | |
| async for chunk in api_client.chat_stream(message): | |
| accumulated_content = chunk | |
| # Update the last message (AI response) in history | |
| history[-1] = { | |
| "role": "assistant", | |
| "content": accumulated_content | |
| } | |
| yield history, "" | |
| # Run the async generator | |
| async_gen = stream_response() | |
| try: | |
| while True: | |
| result = loop.run_until_complete(async_gen.__anext__()) | |
| yield result | |
| except StopAsyncIteration: | |
| pass | |
| finally: | |
| loop.close() | |
| except Exception as e: | |
| # Update with error message | |
| history[-1] = { | |
| "role": "assistant", | |
| "content": f"❌ **Lỗi:** {str(e)}\n\nVui lòng thử lại sau hoặc kiểm tra kết nối API." | |
| } | |
| yield history, "" | |
| with gr.Column(elem_classes="chat-container"): | |
| # Enhanced Chat header | |
| gr.HTML(""" | |
| <div style="text-align: center; padding: 2rem; background: linear-gradient(135deg, #7c3aed 0%, #a855f7 50%, #c084fc 100%); color: white; border-radius: 16px; margin-bottom: 2rem; box-shadow: 0 6px 24px rgba(124,58,237,0.2);"> | |
| <div style="display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 0.5rem;"> | |
| <div style="background: rgba(255,255,255,0.2); padding: 0.5rem; border-radius: 8px; display: flex; align-items: center; gap: 0.25rem;"> | |
| <span style="font-size: 1.25rem;">🤖</span> | |
| <span style="font-size: 1.25rem;">✨</span> | |
| </div> | |
| <h2 style="margin: 0; font-size: 1.8rem; font-weight: 700; text-shadow: 0 2px 4px rgba(0,0,0,0.2);">AI Transportation Assistant</h2> | |
| </div> | |
| <p style="margin: 0; opacity: 0.95; font-size: 1rem; font-weight: 400;">Trợ lý thông minh cho logistics và vận chuyển quốc tế</p> | |
| <div style="margin-top: 1.5rem; display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap;"> | |
| <span style="background: rgba(255,255,255,0.15); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.85rem; font-weight: 500;">💡 Tư vấn chiến lược</span> | |
| <span style="background: rgba(255,255,255,0.15); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.85rem; font-weight: 500;">📊 Phân tích dữ liệu</span> | |
| <span style="background: rgba(255,255,255,0.15); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.85rem; font-weight: 500;">🎯 Dự đoán chính xác</span> | |
| </div> | |
| </div> | |
| """) | |
| # Enhanced Chat interface | |
| chatbot = gr.Chatbot( | |
| label="", | |
| height=600, | |
| placeholder="💬 Bắt đầu cuộc trò chuyện với AI assistant. Tôi có thể giúp bạn về:\n\n🔮 Dự đoán phương thức vận chuyển\n📊 Phân tích chi phí logistics\n🌍 Tư vấn vận chuyển quốc tế\n💡 Tối ưu hóa quy trình\n\nHãy hỏi tôi bất cứ điều gì!", | |
| show_copy_button=True, | |
| type="messages", | |
| elem_classes="modern-chatbot", | |
| avatar_images=( | |
| "https://cdn-icons-png.flaticon.com/512/149/149071.png", # User avatar | |
| "https://cdn-icons-png.flaticon.com/512/4712/4712109.png" # Bot avatar | |
| ), | |
| show_share_button=False | |
| ) | |
| # Enhanced Input area | |
| with gr.Group(elem_classes="chat-input-group"): | |
| gr.HTML(""" | |
| <div style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); padding: 1rem; border-radius: 12px 12px 0 0; border-bottom: 2px solid #e2e8f0;"> | |
| <div style="display: flex; align-items: center; gap: 0.5rem; color: #64748b; font-size: 0.9rem;"> | |
| <span>💭</span> | |
| <span>Nhập câu hỏi của bạn về logistics, vận chuyển hoặc dự đoán AI...</span> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Row(elem_classes="chat-input-row"): | |
| msg = gr.Textbox( | |
| label="", | |
| placeholder="Ví dụ: 'Phân tích ưu nhược điểm của Air shipping vs Ocean shipping cho hàng hóa 50kg đến Việt Nam'", | |
| container=False, | |
| scale=5, | |
| elem_classes="chat-input", | |
| lines=1, | |
| max_lines=3 | |
| ) | |
| with gr.Column(scale=1, min_width=120): | |
| with gr.Row(): | |
| send_btn = gr.Button( | |
| "� Gửi", | |
| variant="primary", | |
| size="lg", | |
| elem_classes="send-button" | |
| ) | |
| clear_btn = gr.Button( | |
| "🗑️", | |
| variant="secondary", | |
| size="sm", | |
| elem_classes="clear-button" | |
| ) | |
| # Event handlers for streaming | |
| msg.submit( | |
| chat_response_streaming, | |
| [msg, chatbot], | |
| [chatbot, msg], | |
| show_progress="hidden" | |
| ) | |
| send_btn.click( | |
| chat_response_streaming, | |
| [msg, chatbot], | |
| [chatbot, msg], | |
| show_progress="hidden" | |
| ) | |
| clear_btn.click( | |
| lambda: ([], ""), | |
| outputs=[chatbot, msg], | |
| show_progress="hidden" | |
| ) | |
| # Enhanced Examples with categories | |
| with gr.Accordion("💡 Câu hỏi mẫu & Chủ đề phổ biến", open=False): | |
| gr.HTML(""" | |
| <div style="margin-bottom: 1.5rem; text-align: center;"> | |
| <h4 style="color: #4f46e5; margin-bottom: 0.5rem; font-size: 1.1rem;">🎯 Khám phá các tính năng của AI Assistant</h4> | |
| <p style="color: #64748b; font-size: 0.9rem;">Click vào câu hỏi để bắt đầu cuộc trò chuyện</p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.HTML("<h5 style='color: #059669; margin-bottom: 1rem;'>🔮 Dự đoán & Phân tích</h5>") | |
| gr.Examples( | |
| examples=[ | |
| "Dự đoán phương thức vận chuyển cho project 100-CI-T01 với 50kg hàng đến Vietnam", | |
| "So sánh chi phí Air vs Ocean shipping cho hàng 25kg đến Thailand", | |
| "Phân tích yếu tố nào ảnh hưởng đến việc chọn phương thức vận chuyển", | |
| "Tại sao AI lại đề xuất Air shipping thay vì Ocean cho đơn hàng này?" | |
| ], | |
| inputs=msg, | |
| examples_per_page=4 | |
| ) | |
| with gr.Column(): | |
| gr.HTML("<h5 style='color: #dc2626; margin-bottom: 1rem;'>� Tư vấn & Chiến lược</h5>") | |
| gr.Examples( | |
| examples=[ | |
| "Làm thế nào để tối ưu hóa chi phí logistics cho doanh nghiệp?", | |
| "Xu hướng vận chuyển quốc tế hiện tại và tương lai như thế nào?", | |
| "Những thách thức chính trong vận chuyển hàng hóa sang ASEAN?", | |
| "Cách lựa chọn vendor và partner logistics phù hợp?" | |
| ], | |
| inputs=msg, | |
| examples_per_page=4 | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.HTML("<h5 style='color: #7c2d12; margin-bottom: 1rem;'>🎓 Kiến thức & Học hỏi</h5>") | |
| gr.Examples( | |
| examples=[ | |
| "Giải thích các thuật ngữ logistics: FOB, CIF, EXW là gì?", | |
| "Quy trình hải quan và giấy tờ cần thiết cho xuất nhập khẩu", | |
| "Sự khác biệt giữa Air Charter và Regular Air shipping", | |
| "Các yếu tố địa lý ảnh hưởng đến chi phí vận chuyển" | |
| ], | |
| inputs=msg, | |
| examples_per_page=4 | |
| ) | |
| with gr.Column(): | |
| gr.HTML("<h5 style='color: #1e40af; margin-bottom: 1rem;'>📊 Dữ liệu & Báo cáo</h5>") | |
| gr.Examples( | |
| examples=[ | |
| "Thống kê xu hướng vận chuyển từ dataset hiện tại", | |
| "Phân tích mối tương quan giữa trọng lượng và chi phí", | |
| "So sánh performance các vendor trong hệ thống", | |
| "Dự báo chi phí vận chuyển cho quý tới" | |
| ], | |
| inputs=msg, | |
| examples_per_page=4 | |
| ) | |
| # Chat tips | |
| gr.HTML(""" | |
| <div style="margin-top: 1.5rem; padding: 1rem; background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border-radius: 12px; border-left: 4px solid #f59e0b;"> | |
| <h4 style="margin: 0 0 0.5rem 0; color: #92400e;">💡 Mẹo sử dụng AI Assistant</h4> | |
| <ul style="margin: 0; color: #92400e; font-size: 0.9rem;"> | |
| <li>Đưa ra thông tin chi tiết để được tư vấn chính xác hơn</li> | |
| <li>Hỏi về các case study cụ thể từ dữ liệu thực tế</li> | |
| <li>Yêu cầu so sánh và phân tích đa chiều</li> | |
| <li>Chat hỗ trợ streaming real-time cho trải nghiệm mượt mà</li> | |
| </ul> | |
| </div> | |
| """) | |
| def create_app(): | |
| """Create the main Gradio application""" | |
| # Enhanced Custom CSS for modern UI | |
| custom_css = """ | |
| /* Import modern fonts */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| margin: auto; | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); | |
| min-height: 100vh; | |
| } | |
| /* Main Header */ | |
| .main-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 2rem; | |
| border-radius: 16px; | |
| margin-bottom: 2rem; | |
| text-align: center; | |
| box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); | |
| } | |
| /* Tab Navigation */ | |
| .tab-nav .tab-nav-button { | |
| font-weight: 600 !important; | |
| padding: 0.75rem 1.5rem !important; | |
| border-radius: 10px !important; | |
| margin: 0 0.25rem !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .tab-nav .tab-nav-button.selected { | |
| background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important; | |
| color: white !important; | |
| box-shadow: 0 4px 15px rgba(79, 70, 229, 0.4) !important; | |
| } | |
| /* Card Styling */ | |
| .block, .gr-box { | |
| border-radius: 12px !important; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important; | |
| border: 1px solid #e2e8f0 !important; | |
| transition: all 0.3s ease !important; | |
| background: white !important; | |
| } | |
| .block:hover, .gr-box:hover { | |
| box-shadow: 0 8px 30px rgba(0,0,0,0.12) !important; | |
| transform: translateY(-2px) !important; | |
| } | |
| /* Button Styling */ | |
| .btn-primary, .gr-button.primary { | |
| background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important; | |
| border: none !important; | |
| border-radius: 10px !important; | |
| font-weight: 600 !important; | |
| padding: 0.75rem 1.5rem !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 4px 15px rgba(79, 70, 229, 0.3) !important; | |
| } | |
| .btn-primary:hover, .gr-button.primary:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4) !important; | |
| } | |
| .send-button { | |
| background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; | |
| border: none !important; | |
| color: white !important; | |
| font-weight: 600 !important; | |
| border-radius: 10px !important; | |
| padding: 0.75rem 1rem !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .send-button:hover { | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4) !important; | |
| } | |
| .clear-button { | |
| background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important; | |
| border: none !important; | |
| color: white !important; | |
| border-radius: 8px !important; | |
| padding: 0.5rem !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| /* Input Field Styling */ | |
| .gr-textbox, .gr-dropdown, .gr-number { | |
| border-radius: 10px !important; | |
| border: 2px solid #e2e8f0 !important; | |
| transition: all 0.3s ease !important; | |
| font-family: 'Inter', sans-serif !important; | |
| } | |
| .gr-textbox:focus, .gr-dropdown:focus, .gr-number:focus { | |
| border-color: #4f46e5 !important; | |
| box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1) !important; | |
| } | |
| /* Chat Styling */ | |
| .modern-chatbot { | |
| border-radius: 12px !important; | |
| background: linear-gradient(135deg, #fafafa 0%, #f3f4f6 100%) !important; | |
| border: 1px solid #e2e8f0 !important; | |
| } | |
| .chat-input { | |
| border-radius: 12px !important; | |
| border: 2px solid #e2e8f0 !important; | |
| background: white !important; | |
| padding: 1rem !important; | |
| font-size: 1rem !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .chat-input:focus { | |
| border-color: #6366f1 !important; | |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1) !important; | |
| } | |
| .chat-input-group { | |
| background: white !important; | |
| border-radius: 12px !important; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important; | |
| border: 1px solid #e2e8f0 !important; | |
| overflow: hidden !important; | |
| } | |
| .chat-input-row { | |
| padding: 1rem !important; | |
| gap: 1rem !important; | |
| } | |
| /* Message Bubbles */ | |
| .message.user { | |
| background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important; | |
| color: white !important; | |
| border-radius: 18px 18px 4px 18px !important; | |
| padding: 1rem 1.25rem !important; | |
| margin: 0.5rem 0 !important; | |
| max-width: 80% !important; | |
| margin-left: auto !important; | |
| } | |
| .message.bot { | |
| background: white !important; | |
| color: #1e293b !important; | |
| border: 1px solid #e2e8f0 !important; | |
| border-radius: 18px 18px 18px 4px !important; | |
| padding: 1rem 1.25rem !important; | |
| margin: 0.5rem 0 !important; | |
| max-width: 80% !important; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; | |
| } | |
| /* Prediction Results */ | |
| .prediction-container { | |
| background: white !important; | |
| border-radius: 12px !important; | |
| padding: 1.5rem !important; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important; | |
| border: 1px solid #e2e8f0 !important; | |
| } | |
| /* Input Form */ | |
| .input-form { | |
| background: white !important; | |
| border-radius: 12px !important; | |
| padding: 1.5rem !important; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important; | |
| border: 1px solid #e2e8f0 !important; | |
| } | |
| .input-field { | |
| margin-bottom: 1rem !important; | |
| } | |
| /* Results Panel */ | |
| .result-panel { | |
| background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%) !important; | |
| border-radius: 12px !important; | |
| padding: 1.5rem !important; | |
| border: 1px solid #7dd3fc !important; | |
| min-height: 400px !important; | |
| } | |
| /* Examples */ | |
| .example-group { | |
| background: white !important; | |
| border-radius: 8px !important; | |
| padding: 1rem !important; | |
| border: 1px solid #e2e8f0 !important; | |
| margin-bottom: 1rem !important; | |
| } | |
| .example-group .gr-button { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important; | |
| border: 1px solid #cbd5e1 !important; | |
| color: #475569 !important; | |
| border-radius: 8px !important; | |
| padding: 0.75rem 1rem !important; | |
| margin: 0.25rem !important; | |
| transition: all 0.3s ease !important; | |
| font-size: 0.9rem !important; | |
| text-align: left !important; | |
| } | |
| .example-group .gr-button:hover { | |
| background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%) !important; | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1) !important; | |
| } | |
| /* Accordion */ | |
| .gr-accordion { | |
| border-radius: 12px !important; | |
| border: 1px solid #e2e8f0 !important; | |
| background: white !important; | |
| overflow: hidden !important; | |
| } | |
| .gr-accordion summary { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important; | |
| padding: 1rem 1.5rem !important; | |
| font-weight: 600 !important; | |
| color: #374151 !important; | |
| border-bottom: 1px solid #e2e8f0 !important; | |
| } | |
| /* Animations */ | |
| @keyframes slideUp { | |
| from { transform: translateY(20px); opacity: 0; } | |
| to { transform: translateY(0); opacity: 1; } | |
| } | |
| .block, .gr-box { | |
| animation: slideUp 0.3s ease-out !important; | |
| } | |
| /* Success/Error Styling */ | |
| .success { | |
| background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; | |
| color: white !important; | |
| padding: 1rem !important; | |
| border-radius: 10px !important; | |
| margin: 0.5rem 0 !important; | |
| box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important; | |
| } | |
| .error { | |
| background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important; | |
| color: white !important; | |
| padding: 1rem !important; | |
| border-radius: 10px !important; | |
| margin: 0.5rem 0 !important; | |
| box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3) !important; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .gradio-container { | |
| padding: 1rem !important; | |
| } | |
| .main-header { | |
| padding: 1.5rem !important; | |
| } | |
| .chat-input-row { | |
| flex-direction: column !important; | |
| } | |
| .send-button, .clear-button { | |
| width: 100% !important; | |
| margin-top: 0.5rem !important; | |
| } | |
| } | |
| /* Dark mode support */ | |
| @media (prefers-color-scheme: dark) { | |
| .gradio-container { | |
| background: linear-gradient(135deg, #1e293b 0%, #334155 100%) !important; | |
| } | |
| .block, .gr-box { | |
| background: #334155 !important; | |
| border-color: #475569 !important; | |
| color: #f1f5f9 !important; | |
| } | |
| .gr-textbox, .gr-dropdown, .gr-number { | |
| background: #475569 !important; | |
| border-color: #64748b !important; | |
| color: #f1f5f9 !important; | |
| } | |
| } | |
| """ | |
| # Create modern theme | |
| theme = gr.themes.Soft( | |
| primary_hue=gr.themes.colors.violet, | |
| secondary_hue=gr.themes.colors.blue, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "Consolas", "monospace"] | |
| ).set( | |
| body_background_fill="linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)", | |
| body_text_color="#1e293b", | |
| button_primary_background_fill="linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)", | |
| button_primary_text_color="white", | |
| block_background_fill="white", | |
| block_border_color="#e2e8f0", | |
| block_border_width="1px", | |
| block_radius="12px" | |
| ) | |
| with gr.Blocks( | |
| title="🚚 Transportation AI | Prediction & Assistant", | |
| theme=theme, | |
| css=custom_css, | |
| analytics_enabled=False | |
| ) as app: | |
| # Modern Header | |
| with gr.Row(elem_classes="main-header"): | |
| gr.HTML(""" | |
| <div style="background: linear-gradient(135deg, #1e40af 0%, #3b82f6 50%, #06b6d4 100%); padding: 2.5rem 1rem; border-radius: 16px; margin-bottom: 2rem; box-shadow: 0 8px 32px rgba(0,0,0,0.1);"> | |
| <div style="text-align: center; color: white;"> | |
| <div style="display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 1rem;"> | |
| <div style="background: rgba(255,255,255,0.2); padding: 0.75rem; border-radius: 12px; backdrop-filter: blur(10px);"> | |
| <span style="font-size: 2rem;">🚚</span> | |
| </div> | |
| <h1 style="font-size: 2.5rem; font-weight: 800; margin: 0; text-shadow: 0 2px 4px rgba(0,0,0,0.3);"> | |
| Transportation AI Platform | |
| </h1> | |
| </div> | |
| <p style="font-size: 1.1rem; opacity: 0.95; margin-bottom: 2rem; font-weight: 400;"> | |
| Hệ thống dự đoán phương thức vận chuyển thông minh với AI | |
| </p> | |
| <div style="display: flex; justify-content: center; gap: 2.5rem; flex-wrap: wrap;"> | |
| <div style="background: rgba(255,255,255,0.15); padding: 0.75rem 1.5rem; border-radius: 25px; backdrop-filter: blur(10px); display: flex; align-items: center; gap: 0.75rem; transition: all 0.3s ease;"> | |
| <span style="font-size: 1.25rem;">🔮</span> | |
| <span style="font-weight: 600; font-size: 0.95rem;">Smart Prediction</span> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.15); padding: 0.75rem 1.5rem; border-radius: 25px; backdrop-filter: blur(10px); display: flex; align-items: center; gap: 0.75rem; transition: all 0.3s ease;"> | |
| <span style="font-size: 1.25rem;">🤖</span> | |
| <span style="font-weight: 600; font-size: 0.95rem;">AI Assistant</span> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.15); padding: 0.75rem 1.5rem; border-radius: 25px; backdrop-filter: blur(10px); display: flex; align-items: center; gap: 0.75rem; transition: all 0.3s ease;"> | |
| <span style="font-size: 1.25rem;">📊</span> | |
| <span style="font-weight: 600; font-size: 0.95rem;">Real-time Analytics</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # Status indicator | |
| with gr.Row(): | |
| gr.HTML(""" | |
| <div style="text-align: center; padding: 1rem; background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border-radius: 8px; margin: 1rem 0;"> | |
| <strong>✅ API Server Status:</strong> Transportation Prediction API v2.0 - Ready | |
| </div> | |
| """) | |
| # Main tabs with improved styling | |
| with gr.Tabs(elem_classes="main-tabs") as tabs: | |
| with gr.Tab("🔮 Smart Prediction", elem_id="prediction-tab"): | |
| create_prediction_tab() | |
| with gr.Tab("🤖 AI Assistant", elem_id="chat-tab"): | |
| create_chat_tab() | |
| # Enhanced Footer | |
| with gr.Row(): | |
| gr.Markdown(""" | |
| <div style="text-align: center; padding: 2rem; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-radius: 12px; margin-top: 2rem;"> | |
| <h3 style="color: #475569; margin-bottom: 1rem;">💡 Hướng dẫn sử dụng</h3> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; max-width: 800px; margin: 0 auto;"> | |
| <div style="text-align: left;"> | |
| <h4 style="color: #7c3aed; margin-bottom: 0.5rem;">🔮 Smart Prediction</h4> | |
| <p style="color: #64748b; font-size: 0.9rem;">Nhập thông tin đơn hàng để AI dự đoán phương thức vận chuyển tối ưu nhất</p> | |
| </div> | |
| <div style="text-align: left;"> | |
| <h4 style="color: #7c3aed; margin-bottom: 0.5rem;">🤖 AI Assistant</h4> | |
| <p style="color: #64748b; font-size: 0.9rem;">Chat với AI để được tư vấn chuyên sâu về logistics và vận chuyển</p> | |
| </div> | |
| </div> | |
| <div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #cbd5e1;"> | |
| <p style="color: #64748b; font-size: 0.85rem;"> | |
| <strong>🔧 API Endpoint:</strong> <code style="background: #f1f5f9; padding: 0.2rem 0.5rem; border-radius: 4px;">{}</code> | |
| </p> | |
| </div> | |
| </div> | |
| """.format(API_BASE_URL)) | |
| return app | |
| def main(): | |
| """Main function to run the application""" | |
| # Check if running on HF Spaces | |
| is_hf_spaces = os.getenv("SPACE_ID") is not None | |
| # Create and launch app | |
| app = create_app() | |
| # Always use port 7860 for HF Spaces compatibility | |
| print("🚀 Starting Gradio app on port 7860...") | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| show_error=True | |
| ) | |
| if __name__ == "__main__": | |
| main() | |