Spaces:
Sleeping
Sleeping
| # -*- coding: utf-8 -*- | |
| from __future__ import annotations | |
| import gradio as gr | |
| import plotly.graph_objects as go | |
| import numpy as np | |
| from datetime import datetime, timedelta | |
| import random | |
| import time | |
| import pandas as pd | |
| from typing import Callable, Any, List, Dict, Optional, Generator, Iterable, Tuple | |
| import re | |
| FOXAI_LOGO_URL = "data:image/png;base64,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" | |
| custom_css = """ | |
| .gradio-container { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 25%, #cbd5e1 50%, #94a3b8 75%, #64748b 100%); | |
| font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif !important; | |
| min-height: 100vh; | |
| } | |
| .header-container { | |
| text-align: center; | |
| margin: 20px 0; | |
| padding: 40px; | |
| background: linear-gradient(90deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%); | |
| border-radius: 24px; | |
| box-shadow: 0 20px 40px rgba(12, 83, 135, 0.15), 0 8px 16px rgba(0, 0, 0, 0.1); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .header-container::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); | |
| animation: shine 3s infinite; | |
| } | |
| @keyframes shine { | |
| 0% { left: -100%; } | |
| 100% { left: 100%; } | |
| } | |
| .foxai-logo { | |
| max-width: 320px; | |
| height: auto; | |
| margin: 15px auto 25px auto; | |
| display: block; | |
| filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15)) brightness(1.1); | |
| transition: all 0.4s ease; | |
| animation: float 4s ease-in-out infinite; | |
| } | |
| .foxai-logo:hover { | |
| transform: scale(1.05); | |
| filter: drop-shadow(0 8px 16px rgba(12, 83, 135, 0.3)) brightness(1.2); | |
| } | |
| .header-title { | |
| background: linear-gradient(90deg, #ffffff, #f1f5f9, #e2e8f0, #ffffff); | |
| background-size: 300% 300%; | |
| animation: gradient 8s ease infinite; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-align: center; | |
| font-size: 2.5em; | |
| font-weight: 700; | |
| margin: 15px auto; | |
| line-height: 1.3; | |
| text-shadow: 0 0 30px rgba(255, 255, 255, 0.5); | |
| letter-spacing: -0.01em; | |
| max-width: 900px; | |
| } | |
| @keyframes gradient { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| /* Unified loading container styling */ | |
| .loading-container { | |
| text-align: center; | |
| padding: 40px; | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| border-radius: 20px; | |
| color: white !important; | |
| margin: 20px 0; | |
| box-shadow: 0 8px 25px rgba(12, 83, 135, 0.25) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.3) !important; | |
| animation: loadingPulse 2s ease-in-out infinite alternate; | |
| } | |
| .loading-container * { | |
| color: white !important; | |
| } | |
| @keyframes loadingPulse { | |
| 0% { | |
| box-shadow: 0 15px 35px rgba(12, 83, 135, 0.3), 0 5px 15px rgba(0, 0, 0, 0.12); | |
| } | |
| 100% { | |
| box-shadow: 0 20px 45px rgba(12, 83, 135, 0.4), 0 8px 20px rgba(0, 0, 0, 0.15); | |
| } | |
| } | |
| .loading-spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #1A5F91; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 20px; | |
| background-color: rgba(255,255,255,0.3); | |
| border-radius: 10px; | |
| overflow: hidden; | |
| margin: 15px 0; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%); | |
| border-radius: 10px; | |
| animation: progress 3s ease-in-out; | |
| box-shadow: 0 0 20px rgba(12, 83, 135, 0.4); | |
| } | |
| @keyframes progress { | |
| 0% { width: 0%; } | |
| 25% { width: 30%; } | |
| 50% { width: 60%; } | |
| 75% { width: 85%; } | |
| 100% { width: 100%; } | |
| } | |
| .prediction-box { | |
| background: linear-gradient(90deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%); | |
| border-radius: 20px; | |
| padding: 25px; | |
| color: white !important; | |
| text-align: center; | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| box-shadow: 0 15px 35px rgba(12, 83, 135, 0.25), 0 5px 15px rgba(0, 0, 0, 0.12); | |
| margin: 15px 0; | |
| border: 1px solid rgba(255,255,255,0.2); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .prediction-box * { | |
| color: white !important; | |
| } | |
| .prediction-box::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 1px; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent); | |
| } | |
| .insight-box { | |
| background: linear-gradient(90deg, #cbd5e1 0%, #94a3b8 25%, #64748b 50%, #475569 75%, #334155 100%); | |
| border-radius: 20px; | |
| padding: 25px; | |
| color: white !important; | |
| margin: 15px 0; | |
| box-shadow: 0 15px 35px rgba(51, 65, 85, 0.25), 0 5px 15px rgba(0, 0, 0, 0.12); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .insight-box * { | |
| color: white !important; | |
| } | |
| .executive-summary { | |
| background: linear-gradient(90deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%); | |
| border-radius: 20px; | |
| padding: 30px; | |
| color: white !important; | |
| margin: 15px 0; | |
| box-shadow: 0 20px 40px rgba(12, 83, 135, 0.25), 0 8px 16px rgba(0, 0, 0, 0.12); | |
| border-left: 5px solid rgba(255, 255, 255, 0.3); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .executive-summary * { | |
| color: white !important; | |
| } | |
| .executive-summary::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| width: 100px; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1)); | |
| pointer-events: none; | |
| } | |
| .feature-box { | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| color: white !important; | |
| border-radius: 20px !important; | |
| padding: 25px !important; | |
| margin: 15px 0 !important; | |
| box-shadow: 0 8px 32px rgba(12, 83, 135, 0.25), 0 4px 16px rgba(0, 0, 0, 0.1) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.2) !important; | |
| backdrop-filter: blur(10px) !important; | |
| transition: all 0.3s ease !important; | |
| text-align: center !important; | |
| } | |
| .feature-box:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 12px 40px rgba(12, 83, 135, 0.35), 0 6px 20px rgba(0, 0, 0, 0.15) !important; | |
| } | |
| .feature-box h3 { | |
| margin: 0 !important; | |
| color: white !important; | |
| font-weight: 700 !important; | |
| text-shadow: 0 0 10px rgba(255, 255, 255, 0.3) !important; | |
| font-size: 1.2em !important; | |
| text-align: center !important; | |
| } | |
| .feature-box * { | |
| color: white !important; | |
| } | |
| /* ALL components get unified gradient and white text */ | |
| .system-status, | |
| .metric-card { | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| color: white !important; | |
| padding: 15px 25px; | |
| border-radius: 20px; | |
| margin: 15px 0; | |
| text-align: center; | |
| font-weight: 600; | |
| box-shadow: 0 8px 25px rgba(12, 83, 135, 0.25) !important; | |
| border: 1px solid rgba(255,255,255,0.3) !important; | |
| font-size: 0.95em; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .system-status *, | |
| .metric-card * { | |
| color: white !important; | |
| } | |
| .metric-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 12px 35px rgba(12, 83, 135, 0.3), 0 5px 12px rgba(0, 0, 0, 0.15); | |
| } | |
| .metric-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent); | |
| } | |
| /* Unified button styling */ | |
| .button-predict, | |
| .btn-modern { | |
| background: linear-gradient(135deg, #1A5F91, #8B8B8B) !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 25px !important; | |
| padding: 15px 30px !important; | |
| font-size: 16px !important; | |
| font-weight: bold !important; | |
| box-shadow: 0 8px 25px rgba(26, 95, 145, 0.4) !important; | |
| transition: all 0.3s ease !important; | |
| position: relative; | |
| overflow: hidden; | |
| font-size: 0.95em; | |
| text-transform: uppercase; | |
| letter-spacing: 1.2px; | |
| } | |
| .button-predict:hover, | |
| .btn-modern:hover { | |
| transform: translateY(-2px) scale(1.02) !important; | |
| box-shadow: 0 12px 35px rgba(26, 95, 145, 0.6) !important; | |
| } | |
| .btn-modern::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); | |
| transition: left 0.6s; | |
| } | |
| .btn-modern:hover::before { | |
| left: 100%; | |
| } | |
| .plot-container { | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.2) !important; | |
| border-radius: 20px !important; | |
| padding: 15px !important; | |
| margin: 15px 0 !important; | |
| box-shadow: 0 8px 32px rgba(12, 83, 135, 0.25), 0 4px 16px rgba(0, 0, 0, 0.1) !important; | |
| height: 550px !important; | |
| max-height: 650px !important; | |
| min-height: 400px !important; | |
| position: relative !important; | |
| overflow: hidden !important; | |
| backdrop-filter: blur(10px) !important; | |
| } | |
| /* Simplify chart container - remove ALL extra nesting and borders */ | |
| .plot-container .plotly-graph-div { | |
| position: relative !important; | |
| width: 100% !important; | |
| height: 500px !important; | |
| max-height: 600px !important; | |
| margin: 0 !important; | |
| padding: 0 !important; | |
| border-radius: 8px !important; | |
| background: rgba(255, 255, 255, 0.98) !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| overflow: hidden !important; | |
| } | |
| /* Remove ALL additional containers and nested divs that cause multiple frames */ | |
| .plot-container .plotly-graph-div > div, | |
| .plot-container .plotly-graph-div > div > div, | |
| .plot-container .plotly-graph-div > div > div > div { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| outline: none !important; | |
| } | |
| /* Ẩn hoàn toàn toolbar/config của biểu đồ */ | |
| .plot-container .modebar, | |
| .plot-container .modebar-container, | |
| .plotly .modebar, | |
| .js-plotly-plot .modebar, | |
| .plotly-graph-div .modebar, | |
| .plotly-graph-div .modebar-container, | |
| .svg-container .modebar, | |
| div[data-title="Plotly toolbar"], | |
| .modebar-group, | |
| .modebar-btn { | |
| display: none !important; | |
| visibility: hidden !important; | |
| opacity: 0 !important; | |
| pointer-events: none !important; | |
| height: 0 !important; | |
| width: 0 !important; | |
| overflow: hidden !important; | |
| position: absolute !important; | |
| top: -9999px !important; | |
| left: -9999px !important; | |
| } | |
| /* Cải thiện spacing cho text và labels - tránh dính chữ */ | |
| .plot-container .plotly-graph-div .svg-container { | |
| pointer-events: auto !important; | |
| overflow: hidden !important; | |
| padding: 10px !important; | |
| height: 100% !important; | |
| max-height: 480px !important; | |
| } | |
| .plot-container .plotly-graph-div .main-svg { | |
| max-width: 100% !important; | |
| height: 100% !important; | |
| max-height: 500px !important; | |
| overflow: hidden !important; | |
| padding: 5px !important; | |
| } | |
| /* Đảm bảo text không bị cắt và có spacing tốt */ | |
| .plot-container .plotly-graph-div text { | |
| font-family: "Inter", "Segoe UI", Arial, sans-serif !important; | |
| font-size: 12px !important; | |
| text-anchor: middle !important; | |
| } | |
| .plot-container .plotly-graph-div .xtick text, | |
| .plot-container .plotly-graph-div .ytick text { | |
| font-size: 11px !important; | |
| fill: #374151 !important; | |
| } | |
| /* Cải thiện title spacing - tránh dính với nội dung */ | |
| .plot-container .plotly-graph-div .gtitle { | |
| font-size: 16px !important; | |
| font-weight: 600 !important; | |
| fill: #1f2937 !important; | |
| dominant-baseline: text-before-edge !important; | |
| } | |
| /* Tối ưu responsive và tránh overflow */ | |
| .plot-container { | |
| width: 100% !important; | |
| max-width: 100% !important; | |
| overflow: visible !important; | |
| } | |
| /* Đảm bảo axis labels có đủ space */ | |
| .plot-container .plotly-graph-div .xaxislayer-above, | |
| .plot-container .plotly-graph-div .yaxislayer-above { | |
| overflow: visible !important; | |
| } | |
| /* CSS cho smooth scrolling và highlight */ | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| .plot-container.highlight { | |
| box-shadow: 0 0 15px rgba(26, 95, 145, 0.2) !important; | |
| transform: scale(1.01) !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| /* Hiệu ứng highlight cho loading panel */ | |
| #loading-panel.highlight { | |
| box-shadow: 0 0 20px rgba(26, 95, 145, 0.4) !important; | |
| transform: scale(1.02) !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| /* Fade-in animation cho LLM analysis */ | |
| .gradio-markdown { | |
| animation: fadeInUp 0.8s ease-out !important; | |
| opacity: 0 !important; | |
| animation-fill-mode: forwards !important; | |
| } | |
| @keyframes fadeInUp { | |
| 0% { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| 100% { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Smooth reveal cho các phần tử khác */ | |
| .plot-container, | |
| .info-card { | |
| animation: slideInFade 0.6s ease-out !important; | |
| } | |
| @keyframes slideInFade { | |
| 0% { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| 100% { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Cải thiện focus state cho biểu đồ */ | |
| .plot-container:focus-within { | |
| outline: 2px solid rgba(26, 95, 145, 0.3); | |
| outline-offset: 2px; | |
| } | |
| /* Unified color scheme - standardize ALL backgrounds to the main gradient */ | |
| .info-card, | |
| .feature-box, | |
| .prediction-box, | |
| .insight-box, | |
| .executive-summary, | |
| .modern-card, | |
| .glass-card { | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| color: white !important; | |
| border-radius: 20px !important; | |
| padding: 25px !important; | |
| margin: 15px 0 !important; | |
| border: 1px solid rgba(255, 255, 255, 0.3) !important; | |
| box-shadow: 0 8px 25px rgba(12, 83, 135, 0.25) !important; | |
| backdrop-filter: blur(10px) !important; | |
| } | |
| /* Ensure ALL text inside these containers is white */ | |
| .info-card *, | |
| .feature-box *, | |
| .prediction-box *, | |
| .insight-box *, | |
| .executive-summary *, | |
| .modern-card *, | |
| .glass-card * { | |
| color: white !important; | |
| } | |
| /* Unified header styling */ | |
| .info-card h3, | |
| .info-card h4, | |
| .feature-box h3, | |
| .prediction-box h3, | |
| .insight-box h3, | |
| .executive-summary h3, | |
| .modern-card h3, | |
| .glass-card h3 { | |
| color: white !important; | |
| text-shadow: 0 0 10px rgba(255, 255, 255, 0.3) !important; | |
| font-weight: 700 !important; | |
| } | |
| /* Unified text styling */ | |
| .info-card p, | |
| .info-card div, | |
| .feature-box p, | |
| .prediction-box p, | |
| .insight-box p, | |
| .executive-summary p, | |
| .modern-card p, | |
| .glass-card p { | |
| color: white !important; | |
| opacity: 0.95 !important; | |
| } | |
| /* Sửa lỗi font và text rendering */ | |
| * { | |
| font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif !important; | |
| text-rendering: optimizeLegibility !important; | |
| -webkit-font-smoothing: antialiased !important; | |
| -moz-osx-font-smoothing: grayscale !important; | |
| } | |
| /* Cải thiện hiển thị tiếng Việt và căn giữa */ | |
| .gradio-markdown, .gradio-html { | |
| font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif !important; | |
| line-height: 1.6 !important; | |
| word-wrap: break-word !important; | |
| overflow-wrap: break-word !important; | |
| text-align: center !important; | |
| } | |
| /* Căn giữa tất cả text */ | |
| .gradio-container .wrap { | |
| max-width: 100% !important; | |
| overflow-x: hidden !important; | |
| text-align: center !important; | |
| } | |
| /* Căn giữa các component */ | |
| .gradio-row { | |
| flex-wrap: wrap !important; | |
| justify-content: center !important; | |
| } | |
| .gradio-column { | |
| min-width: 0 !important; | |
| flex-shrink: 1 !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| align-items: center !important; | |
| } | |
| /* Chuẩn hóa màu sắc - text đậm trên nền sáng */ | |
| .gradio-container { | |
| color: #1e293b !important; | |
| } | |
| /* Chuẩn hóa size chữ - màu đậm cho dễ đọc */ | |
| h1, h2, h3, h4, h5, h6 { | |
| font-weight: 600 !important; | |
| color: #1A5F91 !important; | |
| text-align: center !important; | |
| margin: 0.5em auto !important; | |
| } | |
| p, div, span { | |
| color: #334155 !important; | |
| font-size: 1em !important; | |
| line-height: 1.6 !important; | |
| font-weight: 500 !important; | |
| } | |
| /* Chuẩn hóa labels và inputs - màu đậm */ | |
| label { | |
| font-weight: 600 !important; | |
| color: #1A5F91 !important; | |
| text-align: center !important; | |
| margin-bottom: 8px !important; | |
| font-size: 1em !important; | |
| } | |
| .gradio-textbox, .gradio-dropdown, .gradio-radio { | |
| text-align: center !important; | |
| color: #1e293b !important; | |
| } | |
| /* Buttons styling */ | |
| .gradio-button { | |
| font-weight: 600 !important; | |
| font-size: 1em !important; | |
| border-radius: 12px !important; | |
| margin: 8px auto !important; | |
| display: block !important; | |
| color: #1e293b !important; | |
| } | |
| /* Styling cho markdown báo cáo chiến lược */ | |
| .gradio-markdown { | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| color: white !important; | |
| border-radius: 20px !important; | |
| padding: 30px !important; | |
| margin: 20px 0 !important; | |
| box-shadow: 0 8px 32px rgba(12, 83, 135, 0.25), 0 4px 16px rgba(0, 0, 0, 0.1) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.2) !important; | |
| backdrop-filter: blur(10px) !important; | |
| } | |
| .gradio-markdown * { | |
| color: white !important; | |
| } | |
| .gradio-markdown h1 { | |
| color: white !important; | |
| font-size: 2.2em !important; | |
| font-weight: 800 !important; | |
| text-align: center !important; | |
| margin: 0 0 25px 0 !important; | |
| text-shadow: 0 0 15px rgba(255, 255, 255, 0.3) !important; | |
| border-bottom: 3px solid rgba(255, 255, 255, 0.3) !important; | |
| padding-bottom: 15px !important; | |
| } | |
| .gradio-markdown h2 { | |
| color: white !important; | |
| font-size: 1.6em !important; | |
| font-weight: 700 !important; | |
| margin: 25px 0 15px 0 !important; | |
| padding: 12px 20px !important; | |
| background: rgba(255, 255, 255, 0.1) !important; | |
| border-radius: 12px !important; | |
| border-left: 5px solid rgba(255, 255, 255, 0.3) !important; | |
| } | |
| .gradio-markdown h3 { | |
| color: white !important; | |
| font-size: 1.3em !important; | |
| font-weight: 650 !important; | |
| margin: 20px 0 12px 0 !important; | |
| } | |
| .gradio-markdown p { | |
| color: white !important; | |
| font-size: 1.1em !important; | |
| line-height: 1.8 !important; | |
| font-weight: 500 !important; | |
| margin: 12px 0 !important; | |
| text-align: left !important; | |
| opacity: 0.95 !important; | |
| } | |
| .gradio-markdown ul, .gradio-markdown ol { | |
| color: white !important; | |
| font-size: 1.1em !important; | |
| line-height: 1.8 !important; | |
| font-weight: 500 !important; | |
| margin: 15px 0 !important; | |
| padding-left: 25px !important; | |
| } | |
| .gradio-markdown li { | |
| color: white !important; | |
| font-size: 1.1em !important; | |
| font-weight: 500 !important; | |
| margin: 8px 0 !important; | |
| padding: 5px 0 !important; | |
| } | |
| .gradio-markdown strong { | |
| color: white !important; | |
| font-weight: 700 !important; | |
| text-shadow: 0 0 5px rgba(255, 255, 255, 0.3) !important; | |
| } | |
| .gradio-markdown em { | |
| color: white !important; | |
| font-style: italic !important; | |
| font-weight: 600 !important; | |
| } | |
| /* Highlight cho footer của báo cáo */ | |
| .gradio-markdown hr { | |
| border: 0 !important; | |
| height: 2px !important; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent) !important; | |
| margin: 25px 0 15px 0 !important; | |
| } | |
| /* Style cho phần footer AI */ | |
| .gradio-markdown p:last-child { | |
| text-align: center !important; | |
| font-style: italic !important; | |
| color: white !important; | |
| font-size: 1.1em !important; | |
| margin-top: 20px !important; | |
| padding: 15px !important; | |
| background: rgba(255, 255, 255, 0.1) !important; | |
| border-radius: 10px !important; | |
| border: 1px solid rgba(255, 255, 255, 0.2) !important; | |
| opacity: 0.9 !important; | |
| } | |
| /* Styling cho div footer trong markdown */ | |
| .gradio-markdown div { | |
| color: white !important; | |
| font-size: 1.1em !important; | |
| } | |
| /* Enhanced styling for better readability */ | |
| .gradio-markdown h3 { | |
| color: white !important; | |
| font-size: 1.5em !important; | |
| font-weight: 700 !important; | |
| margin: 25px 0 15px 0 !important; | |
| text-shadow: 0 0 10px rgba(255,255,255,0.3) !important; | |
| } | |
| .gradio-markdown h4 { | |
| color: white !important; | |
| font-size: 1.2em !important; | |
| font-weight: 650 !important; | |
| margin: 20px 0 12px 0 !important; | |
| } | |
| /* Improve list styling */ | |
| .gradio-markdown ul li { | |
| color: white !important; | |
| font-size: 1.15em !important; | |
| font-weight: 500 !important; | |
| margin: 10px 0 !important; | |
| padding: 8px 0 !important; | |
| line-height: 1.6 !important; | |
| } | |
| /* Glass morphism effects for modern look */ | |
| .glass-card { | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| color: white !important; | |
| backdrop-filter: blur(15px) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.2) !important; | |
| border-radius: 20px !important; | |
| padding: 20px !important; | |
| margin: 15px 0 !important; | |
| box-shadow: 0 8px 32px rgba(12, 83, 135, 0.25), 0 4px 16px rgba(0, 0, 0, 0.1) !important; | |
| } | |
| .glass-card * { | |
| color: white !important; | |
| } | |
| /* Remove extra borders and nested containers - CLEAN UP */ | |
| .info-card .gradio-group, | |
| .info-card .gradio-column, | |
| .info-card .gradio-row, | |
| .feature-box .gradio-group, | |
| .feature-box .gradio-column, | |
| .feature-box .gradio-row, | |
| .prediction-box .gradio-group, | |
| .prediction-box .gradio-column, | |
| .prediction-box .gradio-row, | |
| .plot-container .gradio-group, | |
| .plot-container .gradio-column, | |
| .plot-container .gradio-row, | |
| .glass-card .gradio-group, | |
| .glass-card .gradio-column, | |
| .glass-card .gradio-row { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| outline: none !important; | |
| } | |
| /* Simplify neon border effect - NO OVERLAPPING with other styles */ | |
| .neon-border { | |
| box-shadow: 0 0 15px rgba(12, 83, 135, 0.4) !important; | |
| border: 1px solid rgba(12, 83, 135, 0.6) !important; | |
| animation: neonPulse 3s ease-in-out infinite alternate !important; | |
| } | |
| @keyframes neonPulse { | |
| 0% { | |
| box-shadow: 0 0 15px rgba(12, 83, 135, 0.3); | |
| } | |
| 100% { | |
| box-shadow: 0 0 25px rgba(12, 83, 135, 0.5); | |
| } | |
| } | |
| /* Remove double borders on components - SIMPLIFIED */ | |
| .plot-container { | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.3) !important; | |
| border-radius: 20px !important; | |
| padding: 15px !important; | |
| margin: 15px 0 !important; | |
| box-shadow: 0 8px 32px rgba(12, 83, 135, 0.25) !important; | |
| height: 550px !important; | |
| max-height: 650px !important; | |
| min-height: 400px !important; | |
| position: relative !important; | |
| overflow: hidden !important; | |
| backdrop-filter: blur(10px) !important; | |
| } | |
| /* Remove conflicting styles from multiple classes */ | |
| .plot-container.glass-card, | |
| .plot-container.neon-border, | |
| .glass-card.neon-border { | |
| border: 1px solid rgba(255, 255, 255, 0.3) !important; | |
| box-shadow: 0 8px 32px rgba(12, 83, 135, 0.25) !important; | |
| } | |
| /* Remove all conflicting neon border animations */ | |
| .neon-border { | |
| box-shadow: | |
| 0 0 5px rgba(12, 83, 135, 0.3), | |
| 0 0 10px rgba(12, 83, 135, 0.3), | |
| 0 0 15px rgba(12, 83, 135, 0.3), | |
| 0 0 20px rgba(12, 83, 135, 0.2), | |
| inset 0 0 5px rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(12, 83, 135, 0.4); | |
| animation: neonPulse 2s ease-in-out infinite alternate; | |
| } | |
| /* REMOVE - this was causing duplication */ | |
| /* Modern button styling */ | |
| .btn-modern { | |
| background: linear-gradient(90deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%); | |
| border: none; | |
| border-radius: 16px; | |
| padding: 16px 32px; | |
| color: white; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1.2px; | |
| box-shadow: 0 8px 25px rgba(12, 83, 135, 0.3), 0 3px 8px rgba(0, 0, 0, 0.1); | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| overflow: hidden; | |
| font-size: 0.95em; | |
| } | |
| .btn-modern::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); | |
| transition: left 0.6s; | |
| } | |
| .btn-modern:hover { | |
| transform: translateY(-3px) scale(1.02); | |
| box-shadow: 0 15px 40px rgba(12, 83, 135, 0.4), 0 8px 16px rgba(0, 0, 0, 0.15); | |
| } | |
| .btn-modern:hover::before { | |
| left: 100%; | |
| } | |
| .btn-modern:active { | |
| transform: translateY(-1px) scale(1.01); | |
| } | |
| /* Animated background gradients */ | |
| .bg-blue-grad { | |
| background: linear-gradient(90deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%); | |
| background-size: 200% 200%; | |
| animation: gradientShift 4s ease-in-out infinite alternate; | |
| } | |
| .bg-gray-grad { | |
| background: linear-gradient(90deg, #cbd5e1 0%, #94a3b8 25%, #64748b 50%, #475569 75%, #334155 100%); | |
| background-size: 200% 200%; | |
| animation: gradientShift 4s ease-in-out infinite alternate; | |
| } | |
| @keyframes gradientShift { | |
| 0% { background-position: 0% 50%; } | |
| 100% { background-position: 100% 50%; } | |
| } | |
| /* Tech-style borders and effects */ | |
| .tech-border { | |
| position: relative; | |
| border: 2px solid transparent; | |
| border-radius: 16px; | |
| background: linear-gradient(45deg, #A9AAA9, #8194A0, #5A7E98, #33688F, #0C5387) border-box; | |
| mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); | |
| mask-composite: exclude; | |
| -webkit-mask-composite: xor; | |
| transition: all 0.3s ease; | |
| } | |
| .tech-border:hover { | |
| box-shadow: 0 0 20px rgba(12, 83, 135, 0.3); | |
| } | |
| /* Floating squares background animation */ | |
| .gradio-container::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -1; | |
| background-image: | |
| linear-gradient(45deg, rgba(26, 95, 145, 0.03) 25%, transparent 25%), | |
| linear-gradient(-45deg, rgba(26, 95, 145, 0.03) 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, rgba(26, 95, 145, 0.03) 75%), | |
| linear-gradient(-45deg, transparent 75%, rgba(26, 95, 145, 0.03) 75%); | |
| background-size: 60px 60px; | |
| background-position: 0 0, 0 30px, 30px -30px, -30px 0px; | |
| animation: floatingSquares 20s linear infinite; | |
| } | |
| @keyframes floatingSquares { | |
| 0% { transform: translateY(0px) rotate(0deg); } | |
| 100% { transform: translateY(-60px) rotate(360deg); } | |
| } | |
| /* Individual floating squares */ | |
| .floating-square { | |
| position: fixed; | |
| pointer-events: none; | |
| z-index: -1; | |
| opacity: 0.1; | |
| animation: floatUpDown 15s infinite ease-in-out; | |
| } | |
| .floating-square:nth-child(1) { | |
| width: 20px; | |
| height: 20px; | |
| background: rgba(26, 95, 145, 0.2); | |
| top: 10%; | |
| left: 10%; | |
| animation-delay: 0s; | |
| animation-duration: 12s; | |
| } | |
| .floating-square:nth-child(2) { | |
| width: 15px; | |
| height: 15px; | |
| background: rgba(139, 139, 139, 0.15); | |
| top: 20%; | |
| right: 15%; | |
| animation-delay: 2s; | |
| animation-duration: 18s; | |
| } | |
| .floating-square:nth-child(3) { | |
| width: 25px; | |
| height: 25px; | |
| background: rgba(26, 95, 145, 0.1); | |
| bottom: 30%; | |
| left: 20%; | |
| animation-delay: 4s; | |
| animation-duration: 14s; | |
| } | |
| .floating-square:nth-child(4) { | |
| width: 18px; | |
| height: 18px; | |
| background: rgba(169, 170, 169, 0.12); | |
| top: 50%; | |
| right: 25%; | |
| animation-delay: 6s; | |
| animation-duration: 16s; | |
| } | |
| .floating-square:nth-child(5) { | |
| width: 22px; | |
| height: 22px; | |
| background: rgba(26, 95, 145, 0.08); | |
| bottom: 20%; | |
| right: 10%; | |
| animation-delay: 8s; | |
| animation-duration: 20s; | |
| } | |
| .floating-square:nth-child(6) { | |
| width: 16px; | |
| height: 16px; | |
| background: rgba(90, 126, 152, 0.1); | |
| top: 70%; | |
| left: 50%; | |
| animation-delay: 10s; | |
| animation-duration: 13s; | |
| } | |
| @keyframes floatUpDown { | |
| 0%, 100% { | |
| transform: translateY(0px) rotate(0deg) scale(1); | |
| opacity: 0.1; | |
| } | |
| 25% { | |
| transform: translateY(-20px) rotate(90deg) scale(1.1); | |
| opacity: 0.2; | |
| } | |
| 50% { | |
| transform: translateY(-40px) rotate(180deg) scale(0.9); | |
| opacity: 0.15; | |
| } | |
| 75% { | |
| transform: translateY(-20px) rotate(270deg) scale(1.05); | |
| opacity: 0.18; | |
| } | |
| } | |
| /* Animated gradient background */ | |
| .gradio-container::after { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -2; | |
| background: | |
| radial-gradient(circle at 20% 20%, rgba(26, 95, 145, 0.05) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(139, 139, 139, 0.03) 0%, transparent 50%), | |
| radial-gradient(circle at 40% 60%, rgba(26, 95, 145, 0.02) 0%, transparent 50%); | |
| animation: gradientFloat 25s ease-in-out infinite; | |
| } | |
| @keyframes gradientFloat { | |
| 0%, 100% { | |
| transform: scale(1) rotate(0deg); | |
| opacity: 0.5; | |
| } | |
| 50% { | |
| transform: scale(1.1) rotate(180deg); | |
| opacity: 0.3; | |
| } | |
| } | |
| /* Particle effect overlay */ | |
| .particle-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -1; | |
| background-image: | |
| radial-gradient(2px 2px at 20px 30px, rgba(26, 95, 145, 0.1), transparent), | |
| radial-gradient(2px 2px at 40px 70px, rgba(139, 139, 139, 0.08), transparent), | |
| radial-gradient(1px 1px at 90px 40px, rgba(26, 95, 145, 0.06), transparent), | |
| radial-gradient(1px 1px at 130px 80px, rgba(169, 170, 169, 0.05), transparent), | |
| radial-gradient(2px 2px at 160px 30px, rgba(26, 95, 145, 0.07), transparent); | |
| background-repeat: repeat; | |
| background-size: 200px 100px, 180px 120px, 220px 90px, 190px 110px, 210px 95px; | |
| animation: particleDrift 30s linear infinite; | |
| } | |
| @keyframes particleDrift { | |
| 0% { | |
| transform: translateX(0) translateY(0); | |
| } | |
| 100% { | |
| transform: translateX(-200px) translateY(-100px); | |
| } | |
| } | |
| /* Additional geometric shapes */ | |
| .geometric-shapes { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -1; | |
| overflow: hidden; | |
| } | |
| .shape { | |
| position: absolute; | |
| opacity: 0.08; | |
| animation: float 20s infinite ease-in-out; | |
| } | |
| .shape.triangle { | |
| width: 0; | |
| height: 0; | |
| border-left: 10px solid transparent; | |
| border-right: 10px solid transparent; | |
| border-bottom: 20px solid rgba(26, 95, 145, 0.15); | |
| top: 15%; | |
| left: 80%; | |
| animation-delay: 1s; | |
| animation-duration: 25s; | |
| } | |
| .shape.circle { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| background: rgba(139, 139, 139, 0.1); | |
| top: 60%; | |
| left: 15%; | |
| animation-delay: 3s; | |
| animation-duration: 18s; | |
| } | |
| .shape.hexagon { | |
| width: 20px; | |
| height: 11.55px; | |
| background: rgba(26, 95, 145, 0.12); | |
| position: relative; | |
| top: 40%; | |
| right: 5%; | |
| animation-delay: 5s; | |
| animation-duration: 22s; | |
| } | |
| .shape.hexagon:before, | |
| .shape.hexagon:after { | |
| content: ""; | |
| position: absolute; | |
| width: 0; | |
| border-left: 10px solid transparent; | |
| border-right: 10px solid transparent; | |
| } | |
| .shape.hexagon:before { | |
| bottom: 100%; | |
| border-bottom: 5.77px solid rgba(26, 95, 145, 0.12); | |
| } | |
| .shape.hexagon:after { | |
| top: 100%; | |
| border-top: 5.77px solid rgba(26, 95, 145, 0.12); | |
| } | |
| .shape.diamond { | |
| width: 15px; | |
| height: 15px; | |
| background: rgba(169, 170, 169, 0.1); | |
| transform: rotate(45deg); | |
| top: 75%; | |
| left: 70%; | |
| animation-delay: 7s; | |
| animation-duration: 16s; | |
| } | |
| @keyframes float { | |
| 0%, 100% { | |
| transform: translateY(0px) rotate(0deg); | |
| opacity: 0.08; | |
| } | |
| 25% { | |
| transform: translateY(-30px) rotate(90deg); | |
| opacity: 0.15; | |
| } | |
| 50% { | |
| transform: translateY(-60px) rotate(180deg); | |
| opacity: 0.12; | |
| } | |
| 75% { | |
| transform: translateY(-30px) rotate(270deg); | |
| opacity: 0.18; | |
| } | |
| } | |
| /* Animated grid overlay */ | |
| .grid-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -2; | |
| background-image: | |
| linear-gradient(rgba(26, 95, 145, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(26, 95, 145, 0.03) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| animation: gridMove 40s linear infinite; | |
| } | |
| @keyframes gridMove { | |
| 0% { | |
| transform: translate(0, 0); | |
| } | |
| 100% { | |
| transform: translate(50px, 50px); | |
| } | |
| } | |
| /* Wave animation for bottom */ | |
| .wave-container { | |
| position: fixed; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100px; | |
| pointer-events: none; | |
| z-index: -1; | |
| overflow: hidden; | |
| } | |
| .wave { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 200%; | |
| height: 100px; | |
| background: linear-gradient(90deg, | |
| rgba(26, 95, 145, 0.03) 0%, | |
| rgba(139, 139, 139, 0.02) 50%, | |
| rgba(26, 95, 145, 0.03) 100%); | |
| border-radius: 100% 100% 0 0; | |
| animation: waveAnimation 15s infinite linear; | |
| } | |
| .wave:nth-child(2) { | |
| animation-delay: -5s; | |
| animation-duration: 20s; | |
| opacity: 0.5; | |
| height: 80px; | |
| } | |
| .wave:nth-child(3) { | |
| animation-delay: -10s; | |
| animation-duration: 25s; | |
| opacity: 0.3; | |
| height: 60px; | |
| } | |
| @keyframes waveAnimation { | |
| 0% { | |
| transform: translateX(-50%) rotate(0deg); | |
| } | |
| 100% { | |
| transform: translateX(-50%) rotate(360deg); | |
| } | |
| } | |
| /* Shooting stars effect */ | |
| .shooting-star { | |
| position: fixed; | |
| top: 20%; | |
| right: -10px; | |
| width: 2px; | |
| height: 2px; | |
| background: rgba(26, 95, 145, 0.8); | |
| border-radius: 50%; | |
| pointer-events: none; | |
| z-index: -1; | |
| animation: shootingStar 8s linear infinite; | |
| } | |
| .shooting-star::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| height: 2px; | |
| width: 50px; | |
| background: linear-gradient(90deg, rgba(26, 95, 145, 0.8), transparent); | |
| } | |
| .shooting-star:nth-child(1) { | |
| animation-delay: 0s; | |
| top: 15%; | |
| } | |
| .shooting-star:nth-child(2) { | |
| animation-delay: 3s; | |
| top: 25%; | |
| animation-duration: 12s; | |
| } | |
| .shooting-star:nth-child(3) { | |
| animation-delay: 6s; | |
| top: 35%; | |
| animation-duration: 10s; | |
| } | |
| @keyframes shootingStar { | |
| 0% { | |
| transform: translateX(0) translateY(0); | |
| opacity: 1; | |
| } | |
| 70% { | |
| opacity: 1; | |
| } | |
| 100% { | |
| transform: translateX(-100vw) translateY(50px); | |
| opacity: 0; | |
| } | |
| } | |
| /* Holographic effect */ | |
| .holographic { | |
| background: linear-gradient(45deg, #A9AAA9 0%, #8194A0 20%, #5A7E98 40%, #33688F 60%, #0C5387 80%, #A9AAA9 100%); | |
| background-size: 400% 400%; | |
| animation: hologram 6s ease-in-out infinite; | |
| } | |
| @keyframes hologram { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| /* Floating animation for cards */ | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-5px); } | |
| } | |
| .floating { | |
| animation: float 3s ease-in-out infinite; | |
| } | |
| /* Modern card design - SINGLE DEFINITION */ | |
| .modern-card { | |
| background: linear-gradient(135deg, #A9AAA9 0%, #8194A0 25%, #5A7E98 50%, #33688F 75%, #0C5387 100%) !important; | |
| color: white !important; | |
| backdrop-filter: blur(20px) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.3) !important; | |
| border-radius: 20px !important; | |
| padding: 25px !important; | |
| margin: 15px 0 !important; | |
| box-shadow: 0 8px 25px rgba(12, 83, 135, 0.25) !important; | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
| position: relative !important; | |
| overflow: hidden !important; | |
| } | |
| .modern-card * { | |
| color: white !important; | |
| } | |
| .modern-card::before { | |
| content: '' !important; | |
| position: absolute !important; | |
| top: 0 !important; | |
| left: 0 !important; | |
| right: 0 !important; | |
| height: 1px !important; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent) !important; | |
| } | |
| .modern-card:hover { | |
| transform: translateY(-5px) scale(1.02) !important; | |
| box-shadow: 0 15px 35px rgba(12, 83, 135, 0.35) !important; | |
| } | |
| /* Remove duplicate info card styling */ | |
| /* Info card styling already handled in unified section above */ | |
| /* Loading spinner enhancement - cải thiện animation */ | |
| .loading-spinner { | |
| border: 4px solid rgba(255, 255, 255, 0.1); | |
| border-top: 4px solid #00ff88; | |
| border-right: 4px solid #0099ff; | |
| border-radius: 50%; | |
| width: 60px; | |
| height: 60px; | |
| animation: spin 1.2s linear infinite, pulse 2s ease-in-out infinite; | |
| margin: 0 auto 25px; | |
| box-shadow: 0 0 30px rgba(0, 255, 136, 0.4), 0 0 60px rgba(0, 153, 255, 0.2); | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); box-shadow: 0 0 30px rgba(0, 255, 136, 0.4); } | |
| 50% { transform: scale(1.05); box-shadow: 0 0 40px rgba(0, 255, 136, 0.6), 0 0 80px rgba(0, 153, 255, 0.3); } | |
| } | |
| /* Cải thiện progress bar */ | |
| .progress-bar { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 15px; | |
| height: 8px; | |
| margin: 15px 0; | |
| overflow: hidden; | |
| box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2); | |
| } | |
| .progress-fill { | |
| background: linear-gradient(90deg, #00ff88, #0099ff, #00ff88); | |
| height: 100%; | |
| border-radius: 15px; | |
| transition: width 0.5s ease-out; | |
| animation: shimmer 2s infinite; | |
| box-shadow: 0 0 10px rgba(0, 255, 136, 0.5); | |
| } | |
| @keyframes shimmer { | |
| 0% { background-position: -200% 0; } | |
| 100% { background-position: 200% 0; } | |
| } | |
| /* Dropdown info text styling - make text white using #FFFFFF */ | |
| .gradio-dropdown .info, | |
| .gradio-dropdown .gr-form-info, | |
| .gradio-dropdown .svelte-1gfkn6j, | |
| label[data-testid="block-info"], | |
| .gr-form .info, | |
| .gradio-container .info, | |
| .gradio-container [data-testid="block-info"], | |
| .gradio-container .gr-form .info, | |
| .gradio-container .gr-form-info, | |
| .gradio-container .svelte-1gfkn6j, | |
| .gradio-container span.info, | |
| .gradio-container p.info, | |
| .gradio-container div.info, | |
| .gradio-container .block-info, | |
| .gradio-app .info, | |
| .gradio-app [data-testid="block-info"], | |
| .gradio-app .gr-form-info, | |
| .gradio-app .svelte-1gfkn6j { | |
| color: #FFFFFF !important; | |
| opacity: 1 !important; | |
| } | |
| /* More specific targeting for dropdown info */ | |
| .gr-dropdown .info, | |
| .gr-dropdown [data-testid="block-info"], | |
| .gr-textbox .info, | |
| .gr-textbox [data-testid="block-info"] { | |
| color: #FFFFFF !important; | |
| opacity: 1 !important; | |
| } | |
| /* Global override for all info elements */ | |
| * .info { | |
| color: #FFFFFF !important; | |
| } | |
| *[data-testid="block-info"] { | |
| color: #FFFFFF !important; | |
| } | |
| /* Specific class for dropdown with white info text */ | |
| .white-info-dropdown .info, | |
| .white-info-dropdown [data-testid="block-info"], | |
| .white-info-dropdown span, | |
| .white-info-dropdown p { | |
| color: #FFFFFF !important; | |
| opacity: 1 !important; | |
| } | |
| /* Force all Gradio info text to be white */ | |
| .gradio-container * { | |
| --block-info-text-color: #FFFFFF !important; | |
| } | |
| .gradio-container .block-info, | |
| .gradio-container .info-text, | |
| .gradio-container .description { | |
| color: #FFFFFF !important; | |
| } | |
| /* Specific targeting for spans with data-testid="block-info" */ | |
| span[data-testid="block-info"], | |
| div[data-testid="block-info"], | |
| label[data-testid="block-info"], | |
| .svelte-g2oxp3.has-info { | |
| color: #FFFFFF !important; | |
| background: transparent !important; | |
| text-shadow: 0 0 5px rgba(0, 0, 0, 0.5) !important; | |
| } | |
| /* Target the specific class mentioned */ | |
| .svelte-g2oxp3 { | |
| color: #FFFFFF !important; | |
| } | |
| /* Radio button and form labels improvements */ | |
| .gradio-radio label, | |
| .gradio-radio span, | |
| .gradio-dropdown label, | |
| .gradio-dropdown span { | |
| color: #FFFFFF !important; | |
| font-weight: 600 !important; | |
| text-shadow: 0 0 3px rgba(0, 0, 0, 0.3) !important; | |
| } | |
| /* Form component styling */ | |
| .gradio-radio, | |
| .gradio-dropdown { | |
| background: transparent !important; | |
| border: none !important; | |
| padding: 10px !important; | |
| } | |
| /* Remove extra backgrounds on form components */ | |
| .gradio-radio .form, | |
| .gradio-dropdown .form, | |
| .gradio-button .form { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| } | |
| /* Clean up nested containers */ | |
| .gradio-column > div, | |
| .gradio-row > div { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| } | |
| """ | |
| # ---------- MODULE STREAMING THỰC SỰ ---------- | |
| """ | |
| Hiệu ứng 'AI đang gõ' cho báo cáo CEO: | |
| - tao_bao_cao_noi_dung: tạo toàn văn báo cáo (markdown, thuần Việt) | |
| - tao_bao_cao_streaming: generator stream từng cụm có con trỏ nhấp nháy | |
| - tao_streaming_text: alias dùng cho tương thích ngược | |
| - xu_ly_du_doan_voi_loading: trả về (fig, tóm tắt, streamer hoặc full text) | |
| - bat_dau_loading / tinh_toan_ket_qua: workflow loading + stream | |
| - cap_nhat_thong_tin_case, tao_csv_gia_lap: tiện ích trình bày demo | |
| Lưu ý: | |
| - Mặc định trả về generator để front-end cập nhật liên tục. | |
| - Nếu framework của bạn không hỗ trợ generator, gọi với streaming=False | |
| để nhận về chuỗi đầy đủ rồi render một lần. | |
| """ | |
| # ---------- TIỆN ÍCH NHỎ ---------- | |
| def tao_loading_html_streaming(thong_diep: str = "🔎 Đang xử lý", do_mo: float = 0.15) -> str: | |
| """Khối HTML loading gọn, dễ nhúng.""" | |
| return f""" | |
| <div class="loading-card" style="padding:14px;border-radius:10px;background:rgba(255,255,255,{do_mo});border:1px solid rgba(255,255,255,0.25);"> | |
| <div style="display:flex;align-items:center;gap:10px;"> | |
| <div class="spinner" style="width:16px;height:16px;border:3px solid #ddd;border-top-color:#1A5F91;border-radius:50%;animation:spin 1s linear infinite;"></div> | |
| <strong>{thong_diep}</strong> | |
| </div> | |
| </div> | |
| <style> | |
| @keyframes spin {{ from {{transform:rotate(0deg)}} to {{transform:rotate(360deg)}} }} | |
| </style> | |
| """ | |
| def _pause_for_token(token: str, toc_do: float, he_so_dau_cau: float = 1.8) -> None: | |
| """Dừng lâu hơn sau dấu câu/kết dòng để cảm giác tự nhiên.""" | |
| if re.search(r"[.!?…:;。!?]$", token) or token.endswith("\n"): | |
| time.sleep(toc_do * he_so_dau_cau) | |
| else: | |
| time.sleep(toc_do) | |
| def _chunk_by_words(text: str, kich_thuoc: int = 3) -> Iterable[str]: | |
| """Chia text theo cụm ~N từ, giữ dấu câu.""" | |
| words = re.findall(r"\S+\s*", text) | |
| buf = [] | |
| for w in words: | |
| buf.append(w) | |
| if len(buf) >= kich_thuoc or w.endswith("\n"): | |
| yield "".join(buf) | |
| buf = [] | |
| if buf: | |
| yield "".join(buf) | |
| # ---------- NỘI DUNG BÁO CÁO ---------- | |
| def tao_bao_cao_noi_dung_streaming(ten_case: str) -> str: | |
| """ | |
| Tạo toàn văn báo cáo (markdown) từ TRUONG_HOP_DEMO[ten_case]. | |
| Nội dung thuần Việt, dễ đọc cho CEO. | |
| """ | |
| thong_tin_case = TRUONG_HOP_DEMO[ten_case] | |
| gd = thong_tin_case["thong_tin_giam_doc"] | |
| # Khối kết luận được Việt hóa gọn, tránh trộn Anh-Việt. | |
| ket_luan = ( | |
| "• Mức độ ưu tiên: cao\n" | |
| "• Thời điểm triển khai: ngay\n" | |
| "• Hiệu quả kỳ vọng: cải thiện 15–25% trong 6 tháng (ước tính)\n" | |
| "• Mức rủi ro: thấp nếu theo dõi chỉ số đều tay\n" | |
| ) | |
| noi_dung = ( | |
| f"# 🎯 KẾ HOẠCH HÀNH ĐỘNG CHO {ten_case.upper()}\n\n" | |
| "## 📈 Tóm tắt nhanh\n\n" | |
| f"{gd['tom_tat']}\n\n" | |
| "## 📊 Diễn giải theo biểu đồ\n\n" | |
| f"{gd['chi_tiet']}\n\n" | |
| "## 🚀 Việc cần làm ngay\n\n" | |
| f"{gd['hanh_dong']}\n\n" | |
| "## ✅ Kết luận\n\n" | |
| f"{ket_luan}\n" | |
| "---\n\n" | |
| "<div style='text-align:center;font-weight:600;padding:12px;" | |
| "background:rgba(255,255,255,0.12);border:1px solid rgba(255,255,255,0.2);" | |
| "border-radius:10px;'>🤖 FoxAI Business Intelligence • ⚡ Cập nhật theo thời gian thực</div>" | |
| ) | |
| return noi_dung | |
| # ---------- STREAMING ---------- | |
| def tao_bao_cao_streaming_moi( | |
| ten_case: str, | |
| kieu: str = "word", # "word" | "sentence" | "char" | |
| toc_do: float = 0.03, # giây mỗi cụm | |
| con_tro: bool = True, # hiển thị con trỏ nhấp nháy ▌ | |
| cum_tu: int = 3 # số từ mỗi cụm nếu kieu="word" | |
| ) -> Generator[str, None, None]: | |
| """ | |
| Generator gõ "từng cụm" như AI đang viết. | |
| Trả về các lần cập nhật tích lũy (phù hợp vùng text duy nhất trên UI). | |
| """ | |
| full_text = tao_bao_cao_noi_dung_streaming(ten_case) | |
| accumulated = "" | |
| cursor_on = True | |
| if kieu == "char": | |
| tokens = list(full_text) | |
| elif kieu == "sentence": | |
| # Chia theo câu, vẫn đảm bảo xuống dòng tự nhiên | |
| tokens = re.split(r"(?<=[.!?…:;。!?])\s+", full_text) | |
| tokens = [t + " " for t in tokens if t] | |
| else: | |
| tokens = list(_chunk_by_words(full_text, kich_thuoc=cum_tu)) | |
| for tk in tokens: | |
| accumulated += tk | |
| frame = accumulated | |
| if con_tro: | |
| frame = frame + (" ▌" if cursor_on else " ") | |
| cursor_on = not cursor_on | |
| yield frame | |
| _pause_for_token(tk, toc_do, he_so_dau_cau=1.8) | |
| # Khung cuối cùng tắt con trỏ để đỡ nhấp nháy sau khi xong | |
| if con_tro: | |
| yield accumulated | |
| def tao_streaming_text_moi( | |
| ten_case: str, | |
| kieu: str = "word", | |
| toc_do: float = 0.03, | |
| con_tro: bool = True, | |
| cum_tu: int = 3 | |
| ) -> Generator[str, None, None]: | |
| """Alias để tương thích ngược.""" | |
| return tao_bao_cao_streaming_moi( | |
| ten_case, kieu=kieu, toc_do=toc_do, con_tro=con_tro, cum_tu=cum_tu | |
| ) | |
| # ---------- WORKFLOW LOADING + STREAM ---------- | |
| def xu_ly_du_doan_voi_loading_moi( | |
| lua_chon_case: Optional[str], | |
| chu_ky: str, | |
| che_do_xem: str, | |
| streaming: bool = True, | |
| kieu: str = "word", | |
| toc_do: float = 0.03, | |
| cum_tu: int = 3 | |
| ): | |
| """ | |
| Xử lý dự đoán + chuẩn bị nội dung báo cáo. | |
| - streaming=True: trả về generator để UI cập nhật dần | |
| - streaming=False: trả về chuỗi đầy đủ | |
| """ | |
| if lua_chon_case is None: | |
| fig = tao_bieu_do_mac_dinh() | |
| tom_tat = tao_tom_tat_mac_dinh() | |
| bao_cao = "# 🎯 KẾ HOẠCH HÀNH ĐỘNG\n\nChọn mặt hàng để xem phân tích." | |
| return fig, tom_tat, bao_cao | |
| # Delay nhẹ để cảm giác "đang tính" | |
| time.sleep(0.25) | |
| bieu_do = tao_bieu_do_du_doan(lua_chon_case, chu_ky, che_do_xem) | |
| time.sleep(0.15) | |
| tom_tat = tao_thong_tin_tom_tat(lua_chon_case, chu_ky, che_do_xem) | |
| time.sleep(0.20) | |
| if streaming: | |
| bao_cao_gen = tao_bao_cao_streaming_moi( | |
| lua_chon_case, kieu=kieu, toc_do=toc_do, cum_tu=cum_tu | |
| ) | |
| return bieu_do, tom_tat, bao_cao_gen | |
| else: | |
| bao_cao_full = tao_bao_cao_noi_dung_streaming(lua_chon_case) | |
| return bieu_do, tom_tat, bao_cao_full | |
| def bat_dau_loading_moi() -> Tuple[go.Figure, str, str, str]: | |
| """ | |
| Bật panel loading trước khi stream để auto-scroll trên UI. | |
| Trả về: (fig_placeholder, tom_tat_placeholder, text_placeholder, html_loading) | |
| """ | |
| html = tao_loading_html_streaming("🔎 Đang kiểm tra dữ liệu", 0.12) | |
| return tao_bieu_do_mac_dinh(), tao_tom_tat_mac_dinh(), "## ⏳ Đang chuẩn bị...", html | |
| def tinh_toan_ket_qua_moi( | |
| lua_chon_case: str, | |
| chu_ky: str, | |
| che_do_xem: str, | |
| streaming: bool = True | |
| ) -> Tuple[go.Figure, str, Iterable[str] | str, str]: | |
| """ | |
| Tính toán kết quả thật sự (demo data). | |
| Trả về: (figure, html_tóm_tắt, báo_cáo_stream hoặc toàn_văn, html_info) | |
| """ | |
| bieu_do, tom_tat, bao_cao = xu_ly_du_doan_voi_loading_moi( | |
| lua_chon_case, chu_ky, che_do_xem, streaming=streaming | |
| ) | |
| csv_info = f""" | |
| <div class='info-card'> | |
| ✅ <strong>Đã phân tích dữ liệu chuỗi thời gian</strong><br/> | |
| 📦 <strong>SKU:</strong> {lua_chon_case}<br/> | |
| 📅 <strong>Chu kỳ:</strong> {chu_ky} | 🔍 <strong>Chế độ:</strong> {che_do_xem} | |
| </div> | |
| """.strip() | |
| return bieu_do, tom_tat, bao_cao, csv_info | |
| # ---------- TIỆN ÍCH DEMO ---------- | |
| def cap_nhat_thong_tin_case_moi(lua_chon_case: str) -> str: | |
| """Thẻ thông tin ngắn về case đã chọn.""" | |
| if lua_chon_case in TRUONG_HOP_DEMO: | |
| thong_tin = TRUONG_HOP_DEMO[lua_chon_case] | |
| return f""" | |
| <div class='info-card'> | |
| <h4>📋 Tình huống: {lua_chon_case}</h4> | |
| <p><strong>Sản phẩm:</strong> {thong_tin.get('san_pham', '')}</p> | |
| <p><strong>Mô tả:</strong> {thong_tin.get('mo_ta', '')}</p> | |
| </div> | |
| """ | |
| return "" | |
| def tao_csv_gia_lap_moi(ten_case: str, chu_ky: str, che_do_xem: str): | |
| """ | |
| Tạo DataFrame giả lập như CSV thật để demo. | |
| Trả về: (df, ten_cot_thoi_gian, ten_cot_gia_tri) | |
| """ | |
| thong_tin_case = TRUONG_HOP_DEMO[ten_case] | |
| nhan_thoi_gian, don_vi_ban = tao_du_lieu_demo(thong_tin_case, chu_ky, che_do_xem) | |
| san_pham = thong_tin_case.get("san_pham", "") | |
| if che_do_xem == "ngày": | |
| dates = [ | |
| (datetime.now() - timedelta(days=len(nhan_thoi_gian) - i - 1)).strftime("%Y-%m-%d") | |
| for i in range(len(nhan_thoi_gian)) | |
| ] | |
| df_fake = pd.DataFrame( | |
| {"Ngay": dates, "So_Cay": don_vi_ban, "San_Pham": [san_pham] * len(don_vi_ban)} | |
| ) | |
| return df_fake, "Ngay", "So_Cay" | |
| if che_do_xem == "tháng": # theo tuần | |
| dates = [ | |
| (datetime.now() - timedelta(weeks=len(nhan_thoi_gian) - i - 1)).strftime("%Y-%m-%d") | |
| for i in range(len(nhan_thoi_gian)) | |
| ] | |
| df_fake = pd.DataFrame( | |
| {"Tuan": dates, "So_Cay": don_vi_ban, "San_Pham": [san_pham] * len(don_vi_ban)} | |
| ) | |
| return df_fake, "Tuan", "So_Cay" | |
| # năm = theo tháng | |
| dates = [ | |
| (datetime.now() - timedelta(days=30 * (len(nhan_thoi_gian) - i - 1))).strftime("%Y-%m-%d") | |
| for i in range(len(nhan_thoi_gian)) | |
| ] | |
| df_fake = pd.DataFrame( | |
| {"Thang": dates, "So_Cay": don_vi_ban, "San_Pham": [san_pham] * len(don_vi_ban)} | |
| ) | |
| return df_fake, "Thang", "So_Cay" | |
| # ---------- KẾT THÚC MODULE STREAMING ---------- | |
| def tao_loading_html(thong_diep: str = "🔎 Đang xử lý", ti_le: float = 0.15) -> str: | |
| """Tạo HTML loading với thanh tiến độ theo tỷ lệ [0..1].""" | |
| pct = max(0, min(100, int(ti_le * 100))) | |
| # Thêm các thông điệp chi tiết dựa trên tiến độ | |
| chi_tiet_buoc = "" | |
| if ti_le <= 0.25: | |
| chi_tiet_buoc = "📡 Kết nối với AI Engine • 🔍 Phân tích cấu trúc dữ liệu" | |
| elif ti_le <= 0.50: | |
| chi_tiet_buoc = "🧹 Làm sạch và chuẩn hóa dữ liệu • 📈 Xác định xu hướng" | |
| elif ti_le <= 0.75: | |
| chi_tiet_buoc = "🧠 Huấn luyện mô hình ML • ⚡ Tối ưu hóa thuật toán" | |
| else: | |
| chi_tiet_buoc = "📊 Tạo insights • 🎯 Phân tích chiến lược • 📝 Hoàn thiện báo cáo" | |
| return f""" | |
| <div class="loading-container"> | |
| <div class="loading-spinner"></div> | |
| <h3 style="color: white; margin: 0 0 10px 0; font-size: 18px; font-weight: 600;">{thong_diep}...</h3> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" style="width:{pct}%"></div> | |
| </div> | |
| <p style="color: rgba(255,255,255,0.9); margin: 15px 0 5px 0; font-size: 13px;"> | |
| {chi_tiet_buoc} | |
| </p> | |
| <p style="color: rgba(255,255,255,0.7); margin: 5px 0; font-size: 12px;"> | |
| 🤖 FoxAI đang xử lý {pct}% • Vui lòng chờ trong giây lát... | |
| </p> | |
| </div> | |
| """ | |
| def hien_loading_tung_buoc(): | |
| """Generator: stream 6 bước loading trong khoảng 5–8 giây để cảm giác thực tế hơn.""" | |
| tong_thoi_gian = random.uniform(5.0, 8.0) # Tăng thời gian loading | |
| buoc = [ | |
| ("� Đang khởi tạo hệ thống AI", 0.10), | |
| ("📊 Đang phân tích dữ liệu", 0.25), | |
| ("🧹 Đang tiến hành làm sạch dữ liệu", 0.45), | |
| ("🧠 Đang huấn luyện mô hình ML", 0.65), | |
| ("⚡ Đang tối ưu thuật toán", 0.85), | |
| ("📝 Đang tạo báo cáo với AI", 0.95), | |
| ] | |
| # Thời gian ngẫu nhiên cho mỗi bước để tự nhiên hơn | |
| for i, (thong_diep, ti_le) in enumerate(buoc): | |
| yield tao_loading_html(thong_diep, ti_le) | |
| # Thời gian delay ngẫu nhiên cho mỗi bước - tăng thời gian để thực tế hơn | |
| if i < len(buoc) - 1: # Không delay ở bước cuối | |
| if i == 3: # Bước huấn luyện ML - delay lâu hơn | |
| buoc_delay = random.uniform(1.5, 2.5) | |
| elif i == 4: # Bước tối ưu - delay vừa phải | |
| buoc_delay = random.uniform(1.0, 1.8) | |
| else: | |
| buoc_delay = random.uniform(0.8, 1.4) | |
| time.sleep(buoc_delay) | |
| # Delay thêm một chút trước khi hoàn tất để user thấy tiến trình | |
| time.sleep(0.8) | |
| yield tao_loading_html("✅ Hoàn tất phân tích AI", 1.0) | |
| # Delay cuối để user thấy trạng thái hoàn tất rõ ràng | |
| time.sleep(0.6) | |
| def doc_csv(file_obj): | |
| """Đọc CSV, trả preview + gợi ý cột thời gian/giá trị + state df.""" | |
| if file_obj is None: | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(choices=[], value=None), | |
| gr.update(choices=[], value=None), | |
| None, | |
| "<div class='info-card'>📂 Chưa chọn CSV.</div>", | |
| ) | |
| try: | |
| df = pd.read_csv(file_obj.name) | |
| sample = df.head(100) | |
| cols = list(df.columns) | |
| # Đoán cột thời gian và giá trị | |
| guess_date = next( | |
| (c for c in cols if any(k in c.lower() for k in ["date", "time", "ngay", "thoi"])), | |
| cols[0] if cols else None, | |
| ) | |
| numeric_cols = [c for c in cols if pd.api.types.is_numeric_dtype(df[c])] | |
| guess_value = numeric_cols[0] if numeric_cols else (cols[1] if len(cols) > 1 else cols[0]) | |
| thong_bao = f"<div class='info-card'>✅ CSV đã tải: {len(df):,} dòng, {len(cols)} cột.</div>" | |
| return ( | |
| gr.update(visible=True, value=sample), | |
| gr.update(choices=cols, value=guess_date), | |
| gr.update(choices=cols, value=guess_value), | |
| df, | |
| thong_bao, | |
| ) | |
| except Exception as exc: | |
| msg = f"<div class='info-card'>❌ Lỗi đọc CSV: {exc}</div>" | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(choices=[], value=None), | |
| gr.update(choices=[], value=None), | |
| None, | |
| msg, | |
| ) | |
| def tao_chuoi_tu_csv( | |
| df_user: pd.DataFrame, | |
| cot_ngay: str, | |
| cot_gia_tri: str, | |
| che_do_xem: str, | |
| ): | |
| """ | |
| Từ CSV -> (nhãn thời gian, giá trị) theo chế độ xem: | |
| 'ngày' = theo ngày, 'tháng' = theo tuần, 'năm' = theo tháng. | |
| """ | |
| if df_user is None or not cot_ngay or not cot_gia_tri: | |
| return None, None | |
| df = df_user.copy() | |
| df[cot_ngay] = pd.to_datetime(df[cot_ngay], errors="coerce") | |
| df = df.dropna(subset=[cot_ngay, cot_gia_tri]) | |
| df = df.sort_values(cot_ngay) | |
| if df.empty: | |
| return None, None | |
| if che_do_xem == "ngày": | |
| ser = df.groupby(df[cot_ngay].dt.date)[cot_gia_tri].sum() | |
| nhan = [d.strftime("%d/%m") for d in pd.to_datetime(ser.index)] | |
| elif che_do_xem == "tháng": # theo tuần | |
| ser = df.set_index(cot_ngay)[cot_gia_tri].resample("W").sum() | |
| nhan = [idx.strftime("Tuần %W/%Y") for idx in ser.index] | |
| else: # 'năm' = theo tháng | |
| ser = df.set_index(cot_ngay)[cot_gia_tri].resample("M").sum() | |
| nhan = [idx.strftime("%m/%Y") for idx in ser.index] | |
| gia_tri = ser.astype(float).tolist() | |
| if len(gia_tri) < 3: | |
| # Dọn cho gọn, biểu đồ ít hơn 3 điểm nhìn hơi thảm | |
| return None, None | |
| return nhan, gia_tri | |
| """ | |
| Dataset demo 3 SKU. Giữ nguyên ý nghĩa trường cũ, thêm trường phân tích: | |
| - gia_ban, gia_doi_thu, do_co_gian_gia: phục vụ mô phỏng giá/khuyến mãi | |
| - muc_dich_vu, lead_time_ngay: tính tồn an toàn và ROP | |
| - ty_le_kenh: phân bổ theo kênh (phần trăm) | |
| - quan_he_cross_sub: ma trận thay thế chéo giữa 3 SKU | |
| - kpi_van_hanh: chỗ điền kết quả tính toán (tồn an toàn, ROP, thiếu hàng) | |
| """ | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Dataset DEMO 3 SKU. Đã chuẩn hóa chuỗi nhiều dòng cho 'thong_tin_giam_doc' | |
| để tránh lỗi cú pháp và bảo đảm 3 khóa SKU đều truy cập được. | |
| """ | |
| TRUONG_HOP_DEMO = { | |
| "Thủ Đô bao cứng (NH214/07)": { | |
| "san_pham": "Thủ Đô", | |
| "phan_loai": "bao_cung", | |
| "ma_sku": "NH214/07", | |
| "canh_bao": None, | |
| "don_vi_co_ban": 180, | |
| "don_vi_tinh": "cay/thang", | |
| "don_vi_quy_doi": {"1_cay": 10, "don_vi_con": "bao"}, | |
| "xu_huong": "on_dinh", | |
| "mo_ta": "Thủ Đô bao cứng NH214/07 phục vụ kênh GT/MT.", | |
| "kenh_ban": ["GT", "MT"], | |
| "ty_le_kenh": {"GT": 0.60, "MT": 0.40, "Online": 0.00}, | |
| "gia_ban": 100.0, | |
| "gia_doi_thu": 98.0, | |
| "do_co_gian_gia": -0.8, | |
| "muc_dich_vu": 0.95, | |
| "lead_time_ngay": 14, | |
| "quan_he_cross_sub": { | |
| "TL-COMPACT-HONG": -0.10, | |
| "TL-CB-RANG": -0.05, | |
| }, | |
| "kpi_van_hanh": { | |
| "ton_an_toan_tuan": None, | |
| "rop": None, | |
| "ngay_thieu_hang_thang": None, | |
| }, | |
| "thong_tin_giam_doc": { | |
| "tom_tat": ( | |
| "📌 Đường bán đi ngang, sóng thấp. Đây là sản phẩm giữ nhịp doanh thu ở kênh GT (~60%). " | |
| "Giá cao hơn đối thủ khoảng 2% nhưng nhu cầu không quá nhạy với giá. Nguy cơ chính là " | |
| "thiếu hàng cục bộ vào cuối tháng. Giữ mức phục vụ 95% và dự trữ tương đương 3–4 tuần bán." | |
| ), | |
| "chi_tiet": ( | |
| "• Đồ thị: dao động nhẹ, ít đỉnh bất thường; hay trồi sụt nhẹ tuần chốt tháng.\n" | |
| "• Khách hàng: trung thành, ít đổi sang biến thể khác khi trưng bày ổn.\n" | |
| "• Giá: ~100 so với đối thủ ~98; nếu tăng 1% có thể giảm ~0.8% sản lượng.\n" | |
| "• Kênh: GT 60%, MT 40%; khu vực tỉnh là chính, nhạy với mức chiết khấu tại điểm bán.\n" | |
| "• Liên đới: khi đẩy Thăng Long (Compact/Răng), Thủ Đô có thể sụt nhẹ.\n" | |
| "• Cần theo dõi: ngày thiếu hàng, tỷ lệ đơn phục vụ đủ, chênh tồn GT/MT." | |
| ), | |
| "hanh_dong": ( | |
| "• Trước ngày chốt tháng 10–14 ngày, bổ sung hàng về điểm bán đều tay; không chờ sát ngày.\n" | |
| "• Duy trì điểm đặt hàng lại; không để tồn thực tế xuống dưới mức an toàn.\n" | |
| "• Không mở khuyến mãi rộng; chỉ kích tại điểm có hiệu quả sau trừ chi phí tốt.\n" | |
| "• Trưng bày sạch, đủ mặt tiền ở GT; ưu tiên cấp hàng cho điểm bán ổn định." | |
| ), | |
| } | |
| }, | |
| "Thăng Long bao cứng Compact (cảnh báo họng)": { | |
| "san_pham": "Thăng Long", | |
| "phan_loai": "bao_cung_compact", | |
| "ma_sku": "TL-COMPACT-HONG", | |
| "canh_bao": "hong", | |
| "don_vi_co_ban": 150, | |
| "don_vi_tinh": "cay/thang", | |
| "don_vi_quy_doi": {"1_cay": 10, "don_vi_con": "bao"}, | |
| "xu_huong": "tang_nhe", | |
| "mo_ta": "Thăng Long compact, in cảnh báo họng.", | |
| "kenh_ban": ["GT", "MT", "Online"], | |
| "ty_le_kenh": {"GT": 0.50, "MT": 0.35, "Online": 0.15}, | |
| "gia_ban": 96.0, | |
| "gia_doi_thu": 97.0, | |
| "do_co_gian_gia": -1.1, | |
| "muc_dich_vu": 0.95, | |
| "lead_time_ngay": 10, | |
| "quan_he_cross_sub": { | |
| "NH214/07": -0.02, | |
| "TL-CB-RANG": -0.12, | |
| }, | |
| "kpi_van_hanh": { | |
| "ton_an_toan_tuan": None, | |
| "rop": None, | |
| "ngay_thieu_hang_thang": None, | |
| }, | |
| "thong_tin_giam_doc": { | |
| "tom_tat": ( | |
| "📈 Đường bán bò lên đều; mỗi đợt trưng bày tốt hay gói mua kèm đều tạo nhịp tăng. " | |
| "Mạnh ở đô thị, kênh MT và bán online. Giá đang thấp hơn đối thủ ~1%, nhu cầu khá nhạy giá. " | |
| "Cần tách lịch khuyến mãi với biến thể Răng để tránh khách đổi sang nhau." | |
| ), | |
| "chi_tiet": ( | |
| "• Đồ thị: xu hướng đi lên; đỉnh thường rơi vào cuối tuần/đợt lương.\n" | |
| "• Trưng bày: ở tầm mắt hoặc cạnh quầy giúp tăng bán rõ rệt; nên thử A/B vị trí.\n" | |
| "• Giá: ~96 so với 97; chỉnh giá 1% có thể đổi ~1.1% sản lượng.\n" | |
| "• Kênh: GT 50%, MT 35%, online 15%; online tạo sóng cuối tuần nhờ gói mua kèm.\n" | |
| "• Ăn lẫn nhau: khi Răng giảm giá cùng lúc, Compact bị kéo khách đáng kể.\n" | |
| "• Cần theo dõi: tỉ lệ trưng bày đạt chuẩn, số đơn từ gói mua kèm, hiệu quả khuyến mãi theo kênh." | |
| ), | |
| "hanh_dong": ( | |
| "• Chạy gói mua kèm vào cuối tuần và đợt lương; khung thời gian rõ để tạo khan hiếm.\n" | |
| "• Chọn 3 vị trí trưng bày then chốt ở MT; kiểm tra tuân thủ hàng tuần.\n" | |
| "• Giữ khoảng cách giá tối thiểu 3–5% với biến thể Răng; không khuyến mãi cùng thời điểm.\n" | |
| "• Dự trữ thêm trước cao điểm; ưu tiên cấp hàng cho MT và bán online." | |
| ), | |
| } | |
| }, | |
| "Thăng Long cảnh báo răng": { | |
| "san_pham": "Thăng Long", | |
| "phan_loai": "bao_cung", | |
| "ma_sku": "TL-CB-RANG", | |
| "canh_bao": "rang", | |
| "don_vi_co_ban": 140, | |
| "don_vi_tinh": "cay/thang", | |
| "don_vi_quy_doi": {"1_cay": 10, "don_vi_con": "bao"}, | |
| "xu_huong": "bien_dong", | |
| "mo_ta": "Thăng Long bao cứng, biến thể cảnh báo răng.", | |
| "kenh_ban": ["GT", "Chợ truyền thống"], | |
| "ty_le_kenh": {"GT": 0.80, "MT": 0.20, "Online": 0.00}, | |
| "gia_ban": 94.0, | |
| "gia_doi_thu": 95.0, | |
| "do_co_gian_gia": -1.3, | |
| "muc_dich_vu": 0.95, | |
| "lead_time_ngay": 7, | |
| "quan_he_cross_sub": { | |
| "NH214/07": -0.01, | |
| "TL-COMPACT-HONG": -0.08, | |
| }, | |
| "kpi_van_hanh": { | |
| "ton_an_toan_tuan": None, | |
| "rop": None, | |
| "ngay_thieu_hang_thang": None, | |
| }, | |
| "thong_tin_giam_doc": { | |
| "tom_tat": ( | |
| "↕️ Đường bán nhấp nhô mạnh; phụ thuộc chương trình ngắn ngày và độ phủ cửa hàng nhỏ. " | |
| "Trọng tâm GT (~80%). Giá thấp hơn đối thủ ~1% nhưng rất nhạy giá. Cần phân bổ theo " | |
| "bản đồ điểm nóng để tránh nơi thừa nơi thiếu." | |
| ), | |
| "chi_tiet": ( | |
| "• Đồ thị: biên độ cao; vừa có khuyến mãi là bật đỉnh, hết chương trình là hạ nhanh.\n" | |
| "• Giá: ~94 so với 95; chỉnh 1% có thể đổi ~1.3% sản lượng.\n" | |
| "• Kênh: GT là chủ lực; nên chia nhỏ theo phường/xã để cấp hàng đúng chỗ.\n" | |
| "• Rủi ro: đẩy hàng sát ngày chốt dễ phát sinh tồn cục bộ; khách dễ đổi sang Compact khi giá gần nhau.\n" | |
| "• Cần theo dõi: ngày thiếu hàng, tốc độ quay vòng, điểm nóng bán ra 4 tuần gần nhất." | |
| ), | |
| "hanh_dong": ( | |
| "• Chạy gói kích cầu 2–3 tuần; nếu hiệu quả thấp thì dừng sớm, tránh đốt ngân sách.\n" | |
| "• Cấp hàng dựa trên sức bán tuần; điều chuyển nhanh khỏi điểm chậm quay.\n" | |
| "• Trưng bày ngang tầm mắt, vật dụng gọn nhẹ, thay mới thường xuyên.\n" | |
| "• Không khuyến mãi trùng thời điểm với Compact; giữ khoảng giá đủ xa để hạn chế đổi sang nhau.\n" | |
| "• Lập lịch cấp hàng 2 lần/tuần ở khu vực biến động; bật cảnh báo khi tồn xuống dưới ngưỡng." | |
| ), | |
| } | |
| }, | |
| } | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Các hàm hiển thị/giả lập dữ liệu theo domain hiện tại: | |
| - SKU thuốc điếu: Thủ Đô NH214/07, Thăng Long Compact (họng), Thăng Long (răng) | |
| - Đơn vị: cây/bao, chu kỳ: ngày/tuần/tháng | |
| - Thông điệp CEO-friendly, kênh: GT/MT/Online | |
| """ | |
| from datetime import datetime, timedelta | |
| import random | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| def _z_from_service_level(p: float) -> float: | |
| """Ước lượng Z cho mức dịch vụ (để tính tồn an toàn/ROP).""" | |
| if p >= 0.99: | |
| return 2.33 | |
| if p >= 0.975: | |
| return 1.96 | |
| if p >= 0.95: | |
| return 1.65 | |
| if p >= 0.90: | |
| return 1.28 | |
| return 1.0 | |
| def _baseline_per_point(don_vi_co_ban: float, | |
| don_vi_tinh: str, | |
| che_do_xem: str) -> float: | |
| """ | |
| Chuẩn hóa baseline theo đơn vị thời gian hiển thị. | |
| Ví dụ: don_vi_co_ban=180 (cây/tháng) -> mỗi điểm ngày ≈ 180/30.44. | |
| """ | |
| don_vi_tinh = (don_vi_tinh or "").lower().strip() | |
| if "/thang" in don_vi_tinh or "/tháng" in don_vi_tinh: | |
| per_month = don_vi_co_ban | |
| if che_do_xem == "năm": | |
| return per_month # mỗi điểm là 1 tháng | |
| if che_do_xem == "tháng": | |
| return per_month / 4.33 # mỗi điểm là 1 tuần | |
| return per_month / 30.44 # ngày | |
| if "/tuan" in don_vi_tinh or "/tuần" in don_vi_tinh: | |
| per_week = don_vi_co_ban | |
| if che_do_xem == "năm": | |
| return per_week * 4.33 # gom thành tháng | |
| if che_do_xem == "tháng": | |
| return per_week | |
| return per_week / 7.0 | |
| if "/ngay" in don_vi_tinh or "/ngày" in don_vi_tinh: | |
| per_day = don_vi_co_ban | |
| if che_do_xem == "năm": | |
| return per_day * 30.44 | |
| if che_do_xem == "tháng": | |
| return per_day * 7.0 | |
| return per_day | |
| # Mặc định coi là theo tháng | |
| if che_do_xem == "năm": | |
| return don_vi_co_ban | |
| if che_do_xem == "tháng": | |
| return don_vi_co_ban / 4.33 | |
| return don_vi_co_ban / 30.44 | |
| def tao_bieu_do_mac_dinh(ds_sku=None): | |
| """ | |
| Tạo biểu đồ mặc định khi chưa dự đoán, hiển thị đúng domain SKU. | |
| ds_sku: optional list tên SKU để gợi ý chọn. | |
| """ | |
| fig = go.Figure() | |
| sku_text = "" | |
| if ds_sku: | |
| sku_text = " | ".join(ds_sku) | |
| else: | |
| try: | |
| # Lấy từ TRUONG_HOP_DEMO nếu có trong scope | |
| sku_text = " | ".join(list(TRUONG_HOP_DEMO.keys())[:3]) | |
| except Exception: | |
| sku_text = "Thủ Đô NH214/07 | TL Compact (họng) | TL (răng)" | |
| fig.add_annotation( | |
| text=( | |
| "📊 Chọn SKU và nhấn <b>“Tạo Dự Báo AI”</b><br>" | |
| "SKU mẫu: " + sku_text | |
| ), | |
| xref="paper", | |
| yref="paper", | |
| x=0.5, | |
| y=0.55, | |
| xanchor="center", | |
| yanchor="middle", | |
| showarrow=False, | |
| font=dict(size=18, color="#34495E", family="Arial"), | |
| bgcolor="rgba(255,255,255,0.9)", | |
| bordercolor="#BDC3C7", | |
| borderwidth=2, | |
| borderpad=20, | |
| ) | |
| fig.add_annotation( | |
| text="Đơn vị hiển thị: cây (1 cây ≈ 10 bao)", | |
| xref="paper", | |
| yref="paper", | |
| x=0.5, | |
| y=0.40, | |
| xanchor="center", | |
| yanchor="middle", | |
| showarrow=False, | |
| font=dict(size=13, color="#566573", family="Arial"), | |
| bgcolor="rgba(255,255,255,0.6)", | |
| bordercolor="#D5D8DC", | |
| borderwidth=1, | |
| borderpad=8, | |
| ) | |
| fig.update_layout( | |
| title="🎯 Bảng Điều Hành Dự Báo | Thủ Đô & Thăng Long", | |
| template="plotly_white", | |
| height=450, | |
| showlegend=False, | |
| xaxis=dict(showgrid=False, showticklabels=False, title="", automargin=True), | |
| yaxis=dict(showgrid=False, showticklabels=False, title="", automargin=True), | |
| plot_bgcolor="rgba(248,249,250,0.9)", | |
| paper_bgcolor="rgba(255,255,255,0.95)", | |
| font=dict(family="Inter, Arial, sans-serif", size=14), | |
| margin=dict(l=40, r=40, t=80, b=40), | |
| ) | |
| return fig | |
| def tao_tom_tat_mac_dinh(): | |
| """Tạo tóm tắt mặc định theo domain SKU.""" | |
| return """ | |
| <div class="prediction-box"> | |
| <h3 style="margin-top:0;color:white;">📋 Tóm Tắt Sẽ Hiển Thị Sau Khi Dự Báo</h3> | |
| <div style="text-align:center;padding:20px;"> | |
| <p style="font-size:18px;margin:10px 0;color:white;"> | |
| 🎯 Chọn SKU (Thủ Đô / Thăng Long) và nhấn "Tạo Dự Báo AI" | |
| </p> | |
| <p style="font-size:16px;margin:10px 0;color:#E8F6F3;"> | |
| Hệ thống sẽ tính xu hướng, tồn an toàn, ROP và cảnh báo kênh GT/MT | |
| </p> | |
| </div> | |
| </div> | |
| """ | |
| def tao_du_lieu_demo(thong_tin_case, chu_ky="3 tháng", che_do_xem="ngày"): | |
| """ | |
| Tạo dữ liệu demo đơn giản dựa trên case và chu kỳ dự báo. | |
| Domain: đơn vị 'cây'; tự động chuẩn hóa baseline theo thời gian hiển thị. | |
| """ | |
| don_vi_co_ban = float(thong_tin_case.get("don_vi_co_ban", 100)) | |
| xu_huong = (thong_tin_case.get("xu_huong") or "").lower().strip() | |
| don_vi_tinh = thong_tin_case.get("don_vi_tinh", "cay/thang") | |
| # Seed ổn định | |
| seed_value = hash( | |
| (thong_tin_case.get("ma_sku", ""), chu_ky, che_do_xem) | |
| ) & 0xFFFFFFFF | |
| rng = random.Random(seed_value) | |
| # Số điểm dữ liệu | |
| if chu_ky == "1 năm": | |
| so_diem = 12 if che_do_xem == "năm" else (52 if che_do_xem == "tháng" else 365) | |
| elif chu_ky == "6 tháng": | |
| so_diem = 6 if che_do_xem == "năm" else (26 if che_do_xem == "tháng" else 180) | |
| else: # "3 tháng" | |
| so_diem = 3 if che_do_xem == "năm" else (13 if che_do_xem == "tháng" else 90) | |
| # Nhãn thời gian | |
| if che_do_xem == "năm": | |
| nhan_thoi_gian = [f"Tháng {i + 1}" for i in range(so_diem)] | |
| elif che_do_xem == "tháng": | |
| nhan_thoi_gian = [f"Tuần {i + 1}" for i in range(so_diem)] | |
| else: # ngày | |
| ngay_bat_dau = datetime.now() | |
| nhan_thoi_gian = [ | |
| (ngay_bat_dau + timedelta(days=i)).strftime("%d/%m") for i in range(so_diem) | |
| ] | |
| # Baseline theo mỗi điểm | |
| base_per_point = _baseline_per_point(don_vi_co_ban, don_vi_tinh, che_do_xem) | |
| # Dữ liệu theo xu hướng domain | |
| don_vi_ban = [] | |
| for i in range(so_diem): | |
| if xu_huong in {"tang_dan", "tăng_dần"}: | |
| he_so = 1.0 + i * 0.20 / max(1, so_diem // 12 or 1) | |
| elif xu_huong in {"tang_nhe", "tăng_nhẹ"}: | |
| he_so = 1.0 + i * 0.08 / max(1, so_diem // 12 or 1) | |
| elif xu_huong in {"bien_dong", "biến_động"}: | |
| he_so = 1.0 + np.sin(i * 0.5) * 0.3 | |
| elif xu_huong in {"on_dinh", "ổn_định"}: | |
| he_so = 1.0 + rng.uniform(-0.1, 0.1) | |
| else: | |
| he_so = 1.0 | |
| noise = rng.randint(-3, 3) if che_do_xem == "ngày" else rng.randint(-7, 7) | |
| gia_tri = max(int(base_per_point * he_so + noise), 1) | |
| don_vi_ban.append(gia_tri) | |
| return nhan_thoi_gian, don_vi_ban | |
| def tao_bieu_do_du_doan( | |
| ten_case, | |
| chu_ky="3 tháng", | |
| che_do_xem="ngày", | |
| df_user=None, | |
| cot_ngay=None, | |
| cot_gia_tri=None, | |
| ): | |
| """ | |
| Tạo biểu đồ dự đoán cho SKU domain hiện tại. | |
| Luôn dùng dữ liệu demo (không phân tích CSV thật). | |
| """ | |
| thong_tin_case = TRUONG_HOP_DEMO[ten_case] | |
| nhan_thoi_gian, don_vi_ban = tao_du_lieu_demo( | |
| thong_tin_case, chu_ky, che_do_xem | |
| ) | |
| fig = go.Figure() | |
| # Biểu đồ chính | |
| fig.add_trace( | |
| go.Scatter( | |
| x=nhan_thoi_gian, | |
| y=don_vi_ban, | |
| mode="lines+markers", | |
| name="📦 Sản Lượng (cây)", | |
| line=dict(color="#1A5F91", width=4), | |
| marker=dict(size=10, color="#1A5F91", symbol="circle"), | |
| hovertemplate="<b>%{x}</b><br><b>Sản lượng:</b> %{y:,} cây<extra></extra>", | |
| ) | |
| ) | |
| # Vùng tin cậy ±15% | |
| upper_bound = [val * 1.15 for val in don_vi_ban] | |
| lower_bound = [val * 0.85 for val in don_vi_ban] | |
| fig.add_trace( | |
| go.Scatter( | |
| x=nhan_thoi_gian + nhan_thoi_gian[::-1], | |
| y=upper_bound + lower_bound[::-1], | |
| fill="toself", | |
| fillcolor="rgba(26, 95, 145, 0.2)", | |
| line=dict(color="rgba(255,255,255,0)"), | |
| name="📊 Vùng Tin Cậy", | |
| hoverinfo="skip", | |
| ) | |
| ) | |
| # Đường xu hướng tuyến tính | |
| x_so = list(range(len(nhan_thoi_gian))) | |
| if len(x_so) >= 2: | |
| z = np.polyfit(x_so, don_vi_ban, 1) | |
| duong_xu_huong = np.poly1d(z)(x_so) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=nhan_thoi_gian, | |
| y=duong_xu_huong, | |
| mode="lines", | |
| name="📈 Xu Hướng", | |
| line=dict(color="#8B8B8B", width=3, dash="dash"), | |
| hovertemplate="<b>Xu hướng:</b> %{y:.0f} cây<extra></extra>", | |
| ) | |
| ) | |
| # Đỉnh doanh số | |
| if don_vi_ban: | |
| chi_so = int(np.argmax(don_vi_ban)) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=[nhan_thoi_gian[chi_so]], | |
| y=[don_vi_ban[chi_so]], | |
| mode="markers", | |
| name="🔥 Đỉnh Doanh Số", | |
| marker=dict(size=18, color="#F39C12", symbol="star"), | |
| hovertemplate="<b>Đỉnh:</b> %{x}<br><b>Sản lượng:</b> %{y:,} cây<extra></extra>", | |
| ) | |
| ) | |
| # Tiêu đề theo chế độ xem | |
| tieu_de_che_do = {"ngày": "Theo Ngày", "tháng": "Theo Tuần", "năm": "Theo Tháng"} | |
| fig.update_layout( | |
| title=f"📊 Dự Báo Sản Lượng Tút - {tieu_de_che_do[che_do_xem]}: {ten_case} ({chu_ky})", | |
| xaxis_title="⏰ Thời Gian", | |
| yaxis_title="📦 Số Tút tiêu thụ", | |
| template="plotly_white", | |
| height=650, | |
| showlegend=True, | |
| hovermode="x unified", | |
| xaxis=dict( | |
| tickangle=45 if che_do_xem == "ngày" else 0, | |
| fixedrange=True, | |
| tickmode="auto", | |
| nticks=10, | |
| automargin=True, | |
| ), | |
| yaxis=dict(fixedrange=True, automargin=True), | |
| font=dict(size=12, family="Inter, Arial, sans-serif"), | |
| plot_bgcolor="rgba(248,249,250,0.9)", | |
| paper_bgcolor="rgba(255,255,255,0.95)", | |
| margin=dict(l=80, r=60, t=120, b=100), | |
| uirevision=f"{ten_case}|{chu_ky}|{che_do_xem}|{len(nhan_thoi_gian)}", | |
| ) | |
| # Ẩn toolbar/config | |
| # Lưu ý: biến `config` sẽ được truyền cho component Plotly khi render (tùy framework). | |
| fig._config = { | |
| "displayModeBar": False, | |
| "staticPlot": True, | |
| "displaylogo": False, | |
| "responsive": True, | |
| "editable": False, | |
| "showTips": False, | |
| "watermark": False, | |
| } | |
| return fig | |
| def tao_thong_tin_tom_tat( | |
| ten_case, | |
| chu_ky="3 tháng", | |
| che_do_xem="ngày", | |
| ): | |
| """ | |
| Tạo tóm tắt điều hành cho SKU domain hiện tại. | |
| Tính thêm tồn an toàn và ROP nếu có 'muc_dich_vu' và 'lead_time_ngay'. | |
| """ | |
| thong_tin_case = TRUONG_HOP_DEMO[ten_case] | |
| nhan_thoi_gian, don_vi_ban = tao_du_lieu_demo( | |
| thong_tin_case, chu_ky, che_do_xem | |
| ) | |
| tong_don_vi = int(sum(don_vi_ban)) | |
| trung_binh = float(tong_don_vi) / max(1, len(don_vi_ban)) | |
| dinh = int(max(don_vi_ban)) if don_vi_ban else 0 | |
| day_nhat = int(min(don_vi_ban)) if don_vi_ban else 0 | |
| ty_le_tang_truong = ( | |
| ((don_vi_ban[-1] - don_vi_ban[0]) / max(1, don_vi_ban[0])) * 100 | |
| if len(don_vi_ban) >= 2 | |
| else 0.0 | |
| ) | |
| # Ước lượng tồn an toàn/ROP theo chu kỳ hiển thị hiện tại | |
| muc_dich_vu = float(thong_tin_case.get("muc_dich_vu", 0.95)) | |
| lead_time_ngay = int(thong_tin_case.get("lead_time_ngay", 14)) | |
| z = _z_from_service_level(muc_dich_vu) | |
| # Ước lượng sigma theo chu kỳ hiện tại | |
| sigma = float(np.std(don_vi_ban)) if len(don_vi_ban) > 1 else 0.0 | |
| lt_weeks = max(1.0, lead_time_ngay / 7.0) | |
| ton_an_toan = z * sigma * np.sqrt(lt_weeks) | |
| rop = trung_binh * (lead_time_ngay / 7.0) + ton_an_toan | |
| # Kênh chủ lực | |
| ty_le_kenh = thong_tin_case.get("ty_le_kenh") | |
| if isinstance(ty_le_kenh, dict) and ty_le_kenh: | |
| kenh_chu_luc = max(ty_le_kenh.items(), key=lambda x: x[1])[0] | |
| else: | |
| ds_kenh = thong_tin_case.get("kenh_ban", []) | |
| kenh_chu_luc = ds_kenh[0] if ds_kenh else "GT" | |
| don_vi_thoi_gian = {"ngày": "ngày", "tháng": "tuần", "năm": "tháng"}[che_do_xem] | |
| san_pham = thong_tin_case.get("san_pham", "") | |
| canh_bao = thong_tin_case.get("canh_bao") | |
| canh_bao_txt = f" | Cảnh báo: {canh_bao}" if canh_bao else "" | |
| tom_tat_html = f""" | |
| <div class="prediction-box"> | |
| <h3 style="margin-top:0;color:white;font-size:1.8em;font-weight:700;text-align:center;margin-bottom:24px;"> | |
| 📋 Báo Cáo Điều Hành | {san_pham}{canh_bao_txt} | |
| </h3> | |
| <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:20px;margin:16px 0;"> | |
| <div style="background:rgba(255,255,255,0.25);padding:16px;border-radius:12px;text-align:center;border:1px solid rgba(255,255,255,0.3);"> | |
| <h4 style="margin:0 0 8px 0;color:white;font-size:1.1em;font-weight:600;">📦 Tổng Sản Lượng</h4> | |
| <p style="font-size:28px;margin:8px 0;font-weight:800;color:white;">{tong_don_vi:,} cây</p> | |
| </div> | |
| <div style="background:rgba(255,255,255,0.25);padding:16px;border-radius:12px;text-align:center;border:1px solid rgba(255,255,255,0.3);"> | |
| <h4 style="margin:0 0 8px 0;color:white;font-size:1.1em;font-weight:600;">📈 TB/{don_vi_thoi_gian.title()}</h4> | |
| <p style="font-size:28px;margin:8px 0;font-weight:800;color:white;">{trung_binh:,.0f} cây</p> | |
| </div> | |
| <div style="background:rgba(255,255,255,0.25);padding:16px;border-radius:12px;text-align:center;border:1px solid rgba(255,255,255,0.3);"> | |
| <h4 style="margin:0 0 8px 0;color:white;font-size:1.1em;font-weight:600;">🔥 Đỉnh</h4> | |
| <p style="font-size:28px;margin:8px 0;font-weight:800;color:white;">{dinh:,} cây</p> | |
| </div> | |
| <div style="background:rgba(255,255,255,0.25);padding:16px;border-radius:12px;text-align:center;border:1px solid rgba(255,255,255,0.3);"> | |
| <h4 style="margin:0 0 8px 0;color:white;font-size:1.1em;font-weight:600;">📊 Tăng Trưởng</h4> | |
| <p style="font-size:28px;margin:8px 0;font-weight:800;color:{'#4ADE80' if ty_le_tang_truong > 0 else '#F87171'};"> | |
| {ty_le_tang_truong:+.1f}% | |
| </p> | |
| </div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:8px;"> | |
| <div style="background:rgba(255,255,255,0.18);padding:14px;border-radius:10px;border:1px solid rgba(255,255,255,0.25);text-align:center;"> | |
| <div style="color:white;font-weight:600;">🏬 Kênh Chủ Lực</div> | |
| <div style="color:white;margin-top:6px;">{kenh_chu_luc}</div> | |
| </div> | |
| <div style="background:rgba(255,255,255,0.18);padding:14px;border-radius:10px;border:1px solid rgba(255,255,255,0.25);text-align:center;"> | |
| <div style="color:white;font-weight:600;">🛡️ Tồn An Toàn (ước tính)</div> | |
| <div style="color:white;margin-top:6px;">{ton_an_toan:,.1f} cây/tuần</div> | |
| </div> | |
| <div style="background:rgba(255,255,255,0.18);padding:14px;border-radius:10px;border:1px solid rgba(255,255,255,0.25);text-align:center;"> | |
| <div style="color:white;font-weight:600;">🔔 ROP (điểm đặt hàng lại)</div> | |
| <div style="color:white;margin-top:6px;">{rop:,.1f} cây</div> | |
| </div> | |
| </div> | |
| <div style="margin-top:18px;text-align:center;padding:12px;background:rgba(255,255,255,0.15);border-radius:10px;border:1px solid rgba(255,255,255,0.2);"> | |
| <p style="margin:0;font-size:1.05em;color:white;font-weight:600;"> | |
| 📅 Chu Kỳ Dự Báo: <strong>{chu_ky}</strong> | Mức dịch vụ mục tiêu: <strong>{muc_dich_vu:.0%}</strong> | LT: <strong>{lead_time_ngay} ngày</strong> | |
| </p> | |
| </div> | |
| </div> | |
| """ | |
| return tom_tat_html | |
| def tao_bao_cao_giam_doc(ten_case): | |
| """ | |
| Tạo báo cáo chi tiết dành cho giám đốc (Markdown), | |
| giữ nội dung domain từ TRUONG_HOP_DEMO. | |
| """ | |
| thong_tin_case = TRUONG_HOP_DEMO[ten_case] | |
| thong_tin_gd = thong_tin_case["thong_tin_giam_doc"] | |
| bao_cao_markdown = f""" | |
| # 🎯 CHIẾN LƯỢC ĐỀ XUẤT | |
| ## 📈 Tóm Tắt Điều Hành | |
| {thong_tin_gd['tom_tat']} | |
| ## 📊 Phân Tích Chi Tiết | |
| {thong_tin_gd['chi_tiet']} | |
| ## 🚀 Khuyến Nghị Hành Động | |
| {thong_tin_gd['hanh_dong']} | |
| --- | |
| *🤖 FoxAI Business Intelligence | ⚡ Cập nhật realtime | Đơn vị: cây (1 cây ≈ 10 bao)* | |
| """ | |
| return bao_cao_markdown | |
| def tao_loading_effect(): | |
| """Tạo hiệu ứng loading để có cảm giác như production""" | |
| loading_html = """ | |
| <div class="loading-container"> | |
| <div class="loading-spinner"></div> | |
| <h3 style="color: white; margin: 0;">🤖 Đang Xử Lý Dự Báo AI...</h3> | |
| <div class="progress-bar"> | |
| <div class="progress-fill"></div> | |
| </div> | |
| <p style="color: white; margin: 10px 0; opacity: 0.9;"> | |
| ⚡ Phân tích dữ liệu • 🧠 Machine Learning • 📊 Tạo insights • 🎯 Chiến lược AI | |
| </p> | |
| </div> | |
| """ | |
| return loading_html | |
| def tao_bao_cao_streaming(ten_case): | |
| """Tạo báo cáo với hiệu ứng streaming như AI đang viết - sử dụng module streaming mới""" | |
| # Trả về string thay vì generator để tương thích với Gradio | |
| return tao_bao_cao_noi_dung_streaming(ten_case) | |
| def tao_streaming_text(ten_case): | |
| """Generator để tạo hiệu ứng streaming - alias cho hàm mới""" | |
| # Trả về string thay vì generator để tương thích với Gradio | |
| return tao_bao_cao_noi_dung_streaming(ten_case) | |
| def xu_ly_du_doan_voi_loading(lua_chon_case, chu_ky, che_do_xem): | |
| """Xử lý dự đoán với hiệu ứng loading realistic và streaming - sử dụng module mới""" | |
| # Sử dụng hàm streaming mới với streaming=False để tương thích với Gradio | |
| return xu_ly_du_doan_voi_loading_moi(lua_chon_case, chu_ky, che_do_xem, streaming=False) | |
| def cap_nhat_thong_tin_case(lua_chon_case): | |
| """Cập nhật thông tin case khi chọn - sử dụng hàm mới""" | |
| return cap_nhat_thong_tin_case_moi(lua_chon_case) | |
| def tao_csv_gia_lap(ten_case, chu_ky, che_do_xem): | |
| """Tạo CSV giả lập để demo - sử dụng hàm mới""" | |
| return tao_csv_gia_lap_moi(ten_case, chu_ky, che_do_xem) | |
| def bat_dau_loading(): | |
| """Khởi động: show panel loading ngay để auto-scroll rồi mới stream - sử dụng hàm mới""" | |
| return bat_dau_loading_moi() | |
| def tinh_toan_ket_qua( | |
| lua_chon_case, chu_ky, che_do_xem | |
| ): | |
| """Tính toán kết quả thật sự - sử dụng hàm streaming mới""" | |
| return tinh_toan_ket_qua_moi(lua_chon_case, chu_ky, che_do_xem, streaming=False) | |
| # Tạo ứng dụng Gradio | |
| def tao_ung_dung(): | |
| """Tạo ứng dụng Gradio hoàn chỉnh bằng tiếng Việt""" | |
| with gr.Blocks( | |
| title="🚀 Hệ Thống Dự Báo Bán Hàng AI - FoxAI", | |
| theme=gr.themes.Soft(), | |
| css=custom_css | |
| ) as demo: | |
| gr.HTML(f""" | |
| <div class="header-container"> | |
| <!-- Floating squares background --> | |
| <div class="floating-square"></div> | |
| <div class="floating-square"></div> | |
| <div class="floating-square"></div> | |
| <div class="floating-square"></div> | |
| <div class="floating-square"></div> | |
| <div class="floating-square"></div> | |
| <!-- Geometric shapes --> | |
| <div class="geometric-shapes"> | |
| <div class="shape triangle"></div> | |
| <div class="shape circle"></div> | |
| <div class="shape hexagon"></div> | |
| <div class="shape diamond"></div> | |
| </div> | |
| <!-- Grid overlay --> | |
| <div class="grid-overlay"></div> | |
| <!-- Wave container --> | |
| <div class="wave-container"> | |
| <div class="wave"></div> | |
| <div class="wave"></div> | |
| <div class="wave"></div> | |
| </div> | |
| <!-- Shooting stars --> | |
| <div class="shooting-star"></div> | |
| <div class="shooting-star"></div> | |
| <div class="shooting-star"></div> | |
| <!-- Particle overlay --> | |
| <div class="particle-overlay"></div> | |
| <style> | |
| .header-container {{ | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 24px; | |
| position: relative; | |
| overflow: hidden; | |
| }} | |
| .foxai-logo {{ | |
| height: 64px; | |
| width: auto; | |
| filter: drop-shadow(0 6px 14px rgba(56,189,248,.45)); | |
| transition: transform .25s ease; | |
| }} | |
| .foxai-logo:hover {{ | |
| transform: scale(1.05); | |
| }} | |
| .header-title {{ | |
| margin-top: 12px; | |
| font-size: clamp(22px, 3vw, 34px); | |
| font-weight: 800; | |
| letter-spacing: .3px; | |
| background: linear-gradient(90deg, #a5b4fc, #60a5fa, #34d399, #60a5fa); | |
| background-size: 200% 100%; | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| animation: title-shift 6s ease-in-out infinite; | |
| }} | |
| @keyframes title-shift {{ | |
| 0% {{ background-position: 0% 50%; }} | |
| 50% {{ background-position: 100% 50%; }} | |
| 100% {{ background-position: 0% 50%; }} | |
| }} | |
| .header-container p {{ | |
| font-size: 1.05em; | |
| color: rgba(255,255,255,0.85); | |
| margin: 12px auto 10px; | |
| font-weight: 500; | |
| line-height: 1.55; | |
| max-width: 640px; | |
| text-align: center; | |
| }} | |
| .header-container p strong {{ | |
| color: #fff; | |
| }} | |
| .system-status {{ | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 14px; | |
| border-radius: 999px; | |
| border: 1px solid rgba(255,255,255,.1); | |
| background: rgba(255,255,255,.06); | |
| font-size: 0.9em; | |
| color: #fff; | |
| margin-top: 14px; | |
| }} | |
| .system-status::before {{ | |
| content: ""; | |
| width: 10px; height: 10px; | |
| border-radius: 50%; | |
| background: #22c55e; | |
| box-shadow: 0 0 0 0 rgba(34,197,94,.6); | |
| animation: pulse 2s infinite; | |
| }} | |
| @keyframes pulse {{ | |
| 0% {{ box-shadow: 0 0 0 0 rgba(34,197,94,.6); }} | |
| 70% {{ box-shadow: 0 0 0 10px rgba(34,197,94,0); }} | |
| 100% {{ box-shadow: 0 0 0 0 rgba(34,197,94,0); }} | |
| }} | |
| </style> | |
| <div style="text-align: center; margin-bottom: 12px;"> | |
| <img src="{FOXAI_LOGO_URL}" alt="FoxAI Logo" class="foxai-logo"/> | |
| </div> | |
| <div class="header-title">HỆ THỐNG DỰ BÁO BÁN HÀNG AI</div> | |
| <p> | |
| Phân tích thông minh với công nghệ <strong>FoxAI</strong> | |
| </p> | |
| <div class="system-status"> | |
| Hoạt động • Tối ưu mô hình • FoxAI Native | |
| </div> | |
| </div> | |
| """) | |
| gr.HTML(""" | |
| <div class='info-card' style=" | |
| padding: 35px; | |
| border-radius: 20px; | |
| border: 1px solid rgba(255,255,255,0.15); | |
| background: rgba(30,41,59,0.75); /* nền tối mờ, đồng bộ footer */ | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.25); | |
| text-align: center; | |
| "> | |
| <h3 style=" | |
| margin-bottom: 20px; | |
| font-size: 2em; | |
| font-weight: 800; | |
| color: #fff; /* chữ trắng rõ ràng */ | |
| text-shadow: 0 2px 6px rgba(0,0,0,0.25); | |
| "> | |
| 🎯 Hướng Dẫn Sử Dụng | |
| </h3> | |
| <p style=" | |
| font-size: 1.15em; | |
| margin-bottom: 30px; | |
| color: #f1f5f9; | |
| font-weight: 500; | |
| "> | |
| Chọn sản phẩm, chu kỳ dự báo và nhận phân tích AI chi tiết | |
| </p> | |
| <div style=" | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 25px; | |
| margin: 25px 0; | |
| "> | |
| <!-- Card 1 --> | |
| <div style=" | |
| padding: 24px; | |
| border-radius: 16px; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| background: rgba(17,25,40,0.8); /* slate đậm hơn chút */ | |
| box-shadow: 0 6px 16px rgba(0,0,0,0.25); | |
| "> | |
| <h4 style=" | |
| margin-bottom: 15px; | |
| font-size: 1.2em; | |
| font-weight: 700; | |
| color: #fff; | |
| ">🛍️ Danh Mục Sản Phẩm</h4> | |
| <div style=" | |
| color: #e2e8f0; | |
| line-height: 1.8; | |
| font-weight: 500; | |
| font-size: 1.05em; | |
| "> | |
| Thủ đô bao cứng<br/> | |
| Thăng Long bao cứng Compact (cảnh báo họng)<br/> | |
| Thăng Long cảnh báo răng | |
| </div> | |
| </div> | |
| <!-- Card 2 --> | |
| <div style=" | |
| padding: 24px; | |
| border-radius: 16px; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| background: rgba(17,25,40,0.8); | |
| box-shadow: 0 6px 16px rgba(0,0,0,0.25); | |
| "> | |
| <h4 style=" | |
| margin-bottom: 15px; | |
| font-size: 1.2em; | |
| font-weight: 700; | |
| color: #fff; | |
| ">📊 Chu Kỳ Dự Báo</h4> | |
| <div style=" | |
| color: #e2e8f0; | |
| line-height: 1.8; | |
| font-weight: 500; | |
| font-size: 1.05em; | |
| "> | |
| 📅 <strong>3 Tháng:</strong> Ngắn hạn<br/> | |
| 📆 <strong>6 Tháng:</strong> Trung hạn<br/> | |
| 🗓️ <strong>1 Năm:</strong> Dài hạn | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # Sử dụng Tabs để tổ chức giao diện | |
| with gr.Tabs(): | |
| # Tab 1: Dự báo chính | |
| with gr.Tab("📊 Dự Báo Bán Hàng"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML("<div class='feature-box floating' style='text-align: center;'><h3>⚙️ Cài Đặt</h3></div>") | |
| lua_chon_dropdown = gr.Dropdown( | |
| choices=list(TRUONG_HOP_DEMO.keys()), | |
| value=list(TRUONG_HOP_DEMO.keys())[0], | |
| label="🛍️ Chọn Sản Phẩm", | |
| info="Mỗi sản phẩm có insight riêng", | |
| elem_classes=["tech-border", "white-info-dropdown"] | |
| ) | |
| chu_ky_dropdown = gr.Dropdown( | |
| choices=[("📅 3 Tháng", "3 tháng"), ("📆 6 Tháng", "6 tháng"), ("🗓️ 1 Năm", "1 năm")], | |
| value="3 tháng", | |
| label="📊 Chu Kỳ Dự Báo", | |
| info="Chọn khoảng thời gian phù hợp", | |
| elem_classes=["tech-border", "white-info-dropdown"] | |
| ) | |
| nut_du_bao = gr.Button( | |
| "🚀 Tạo Dự Báo AI", | |
| variant="primary", | |
| size="lg", | |
| elem_classes=["btn-modern", "neon-border"] | |
| ) | |
| # Hiển thị thông tin CSV giả lập | |
| gr.HTML(""" | |
| <div class='info-card'> | |
| <p><strong>Trạng thái kết nối cơ sở dữ liệu:</strong> Ổn định</p> | |
| <p><strong>Định dạng:</strong> Time series data với cột thời gian và doanh thu</p> | |
| <p><strong>Cập nhật:</strong> Tự động sync theo mặt hàng được chọn</p> | |
| </div> | |
| """) | |
| # Hiển thị thông tin case | |
| hien_thi_thong_tin = gr.HTML() | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.HTML("<div class='feature-box floating' style='text-align: center;'><h3>📈 Kết Quả Dự Báo</h3></div>") | |
| # Chế độ hiển thị được đặt gần biểu đồ | |
| che_do_xem = gr.Radio( | |
| choices=[("📅 Theo Ngày", "ngày"), ("📆 Theo Tuần", "tháng"), ("🗓️ Theo Tháng", "năm")], | |
| value="ngày", | |
| label="📊 Chế Độ Hiển Thị", | |
| info="Thay đổi cách hiển thị biểu đồ", | |
| elem_classes=["tech-border"] | |
| ) | |
| bieu_do_du_bao = gr.Plot( | |
| label="Biểu Đồ Dự Báo Bán Hàng", | |
| elem_classes=["plot-container", "glass-card", "neon-border"], | |
| elem_id="chart-target" # Thêm ID để có thể cuộn tới | |
| ) | |
| # Panel loading | |
| loading_panel = gr.HTML(value="", elem_id="loading-panel") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML("<div class='feature-box floating' style='text-align: center;'><h3>📊 Thống Kê</h3></div>") | |
| hien_thi_tom_tat = gr.HTML() | |
| with gr.Column(scale=1): | |
| gr.HTML("<div class='feature-box floating' style='text-align: center;'><h3>🎯 Chiến Lược AI</h3></div>") | |
| # Button để tạo streaming effect | |
| nut_streaming = gr.Button( | |
| "🤖 Tạo Chiến Lược AI (Streaming)", | |
| variant="secondary", | |
| size="sm", | |
| elem_classes=["btn-modern"], | |
| visible=False | |
| ) | |
| hien_thi_bao_cao = gr.Markdown() | |
| # Tab 2: Các tính năng đang phát triển | |
| with gr.Tab("🔬 Tính Năng Đang Phát Triển"): | |
| gr.HTML(""" | |
| <div class='feature-box floating' style='text-align: center; margin-bottom: 30px;'> | |
| <h3>🔬 Các Tính Năng Đang Phát Triển</h3> | |
| <p style='color: white; font-size: 1.1em; margin-top: 15px;'> | |
| Những công nghệ tiên tiến đang được nghiên cứu và phát triển | |
| </p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| # Dự báo đa mô hình | |
| gr.HTML(""" | |
| <div class='modern-card holographic'> | |
| <h3 style='color: white; margin-bottom: 20px; font-size: 1.6em;'> | |
| 🧠 Dự báo đa mô hình (Ensemble Forecasting) | |
| </h3> | |
| <p style='color: white; font-size: 1.1em; line-height: 1.7; margin-bottom: 15px;'> | |
| Kết hợp nhiều kiến trúc AI (<strong>RNN, Transformer, Prophet, XGBoost...</strong>) | |
| để tăng độ chính xác và ổn định của dự báo. | |
| </p> | |
| <div style='background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px; margin-top: 15px;'> | |
| <strong style='color: #4ADE80;'>📈 Trạng thái:</strong> | |
| <span style='color: #FDE047;'>Đang phát triển - 70% hoàn thiện</span> | |
| </div> | |
| </div> | |
| """) | |
| # Adaptive Learning | |
| gr.HTML(""" | |
| <div class='modern-card bg-blue-grad'> | |
| <h3 style='color: white; margin-bottom: 20px; font-size: 1.6em;'> | |
| 🎯 Adaptive Learning theo thị trường | |
| </h3> | |
| <p style='color: white; font-size: 1.1em; line-height: 1.7; margin-bottom: 15px;'> | |
| Mô hình tự động học lại khi có thay đổi bất thường về nhu cầu | |
| (ví dụ mùa vụ, sự kiện kinh tế, khuyến mãi). | |
| </p> | |
| <div style='background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px; margin-top: 15px;'> | |
| <strong style='color: #4ADE80;'>📈 Trạng thái:</strong> | |
| <span style='color: #FDE047;'>Đang phát triển - 45% hoàn thiện</span> | |
| </div> | |
| </div> | |
| """) | |
| # Explainable AI | |
| gr.HTML(""" | |
| <div class='modern-card bg-gray-grad'> | |
| <h3 style='color: white; margin-bottom: 20px; font-size: 1.6em;'> | |
| 🔍 Explainable AI (XAI) | |
| </h3> | |
| <p style='color: white; font-size: 1.1em; line-height: 1.7; margin-bottom: 15px;'> | |
| Giải thích chi tiết tại sao hệ thống đưa ra dự báo cụ thể, hiển thị các yếu tố | |
| tác động chính (giá, mùa vụ, đối thủ cạnh tranh). | |
| </p> | |
| <div style='background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px; margin-top: 15px;'> | |
| <strong style='color: #4ADE80;'>📈 Trạng thái:</strong> | |
| <span style='color: #FDE047;'>Đang phát triển - 60% hoàn thiện</span> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Column(): | |
| # Real-time Forecast | |
| gr.HTML(""" | |
| <div class='modern-card tech-border'> | |
| <h3 style='color: white; margin-bottom: 20px; font-size: 1.6em;'> | |
| ⚡ Real-time Forecast & Alerting | |
| </h3> | |
| <p style='color: white; font-size: 1.1em; line-height: 1.7; margin-bottom: 15px;'> | |
| Cập nhật dự báo liên tục từ dòng dữ liệu trực tiếp, kèm cảnh báo | |
| khi có tín hiệu bất thường. | |
| </p> | |
| <div style='background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px; margin-top: 15px;'> | |
| <strong style='color: #4ADE80;'>📈 Trạng thái:</strong> | |
| <span style='color: #FDE047;'>Đang phát triển - 30% hoàn thiện</span> | |
| </div> | |
| </div> | |
| """) | |
| # Scenario Simulation | |
| gr.HTML(""" | |
| <div class='modern-card neon-border'> | |
| <h3 style='color: white; margin-bottom: 20px; font-size: 1.6em;'> | |
| 🎲 Scenario Simulation (What-if Analysis) | |
| </h3> | |
| <p style='color: white; font-size: 1.1em; line-height: 1.7; margin-bottom: 15px;'> | |
| Cho phép người dùng giả lập các kịch bản (tăng giá, thay đổi kênh phân phối, | |
| biến động thị trường) và xem tác động dự báo ngay. | |
| </p> | |
| <div style='background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px; margin-top: 15px;'> | |
| <strong style='color: #4ADE80;'>📈 Trạng thái:</strong> | |
| <span style='color: #FDE047;'>Đang phát triển - 25% hoàn thiện</span> | |
| </div> | |
| </div> | |
| """) | |
| # Forecast Granularity | |
| gr.HTML(""" | |
| <div class='modern-card floating'> | |
| <h3 style='color: white; margin-bottom: 20px; font-size: 1.6em;'> | |
| 📊 Forecast Granularity | |
| </h3> | |
| <p style='color: white; font-size: 1.1em; line-height: 1.7; margin-bottom: 15px;'> | |
| Dự báo chi tiết tới từng SKU, từng khu vực, thậm chí từng khách hàng trọng điểm. | |
| </p> | |
| <div style='background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px; margin-top: 15px;'> | |
| <strong style='color: #4ADE80;'>📈 Trạng thái:</strong> | |
| <span style='color: #FDE047;'>Đang phát triển - 55% hoàn thiện</span> | |
| </div> | |
| </div> | |
| """) | |
| # Generative Insights - Full width | |
| gr.HTML(""" | |
| <div class='modern-card holographic' style='margin-top: 25px;'> | |
| <h3 style='color: white; margin-bottom: 20px; font-size: 1.8em; text-align: center;'> | |
| 🤖 Generative Insights (AI Analyst) | |
| </h3> | |
| <p style='color: white; font-size: 1.2em; line-height: 1.8; margin-bottom: 20px; text-align: center;'> | |
| Tự động sinh báo cáo phân tích bằng ngôn ngữ tự nhiên (Natural Language Generation) | |
| → người dùng không cần đọc chart quá nhiều. | |
| </p> | |
| <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 25px;'> | |
| <div style='background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px; text-align: center;'> | |
| <h4 style='color: #4ADE80; margin-bottom: 10px;'>📝 Báo cáo tự động</h4> | |
| <p style='color: white; font-size: 0.95em;'>Tạo insights dạng văn bản từ dữ liệu</p> | |
| </div> | |
| <div style='background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px; text-align: center;'> | |
| <h4 style='color: #60A5FA; margin-bottom: 10px;'>🗣️ Voice Analytics</h4> | |
| <p style='color: white; font-size: 0.95em;'>Báo cáo bằng giọng nói tự nhiên</p> | |
| </div> | |
| <div style='background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px; text-align: center;'> | |
| <h4 style='color: #F472B6; margin-bottom: 10px;'>💬 Chat Interface</h4> | |
| <p style='color: white; font-size: 0.95em;'>Hỏi đáp trực tiếp với AI</p> | |
| </div> | |
| <div style='background: rgba(255,255,255,0.15); padding: 20px; border-radius: 12px; text-align: center;'> | |
| <h4 style='color: #34D399; margin-bottom: 10px;'>📧 Email Summary</h4> | |
| <p style='color: white; font-size: 0.95em;'>Gửi tóm tắt qua email tự động</p> | |
| </div> | |
| </div> | |
| <div style='background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; margin-top: 25px; text-align: center;'> | |
| <strong style='color: #4ADE80; font-size: 1.1em;'>📈 Trạng thái:</strong> | |
| <span style='color: #FDE047; font-size: 1.1em;'>Đang phát triển - 40% hoàn thiện</span> | |
| </div> | |
| </div> | |
| """) | |
| MAP = "https://raw.githubusercontent.com/your-repo/heatmap-2025/main/heatmap.png" # Thay bằng URL thực tế của heatmap | |
| # Roadmap timeline | |
| # Roadmap timeline (update tháng 8/2025 → Q3 highlight) | |
| gr.HTML(f""" | |
| <div class='modern-card' style='margin-top: 30px; text-align: center;'> | |
| <h3 style='color: white; margin-bottom: 20px; font-size: 1.8em;'> | |
| 🔥 Heatmap Phân Tích 2025 | |
| </h3> | |
| <img src="{MAP}" | |
| alt="Heatmap" | |
| style="max-width: 100%; border-radius: 12px; box-shadow: 0 6px 18px rgba(0,0,0,0.25);" /> | |
| </div> | |
| """) | |
| # Xử lý sự kiện streaming cho chiến lược | |
| def xu_ly_streaming(lua_chon_case): | |
| """Xử lý streaming effect cho chiến lược đề xuất""" | |
| if lua_chon_case is None: | |
| return "# 🎯 CHIẾN LƯỢC ĐỀ XUẤT\n\n📊 **Chọn mặt hàng để tạo chiến lược**" | |
| try: | |
| for partial_text in tao_streaming_text(lua_chon_case): | |
| yield partial_text | |
| except Exception as e: | |
| yield f"# ❌ Lỗi\n\nKhông thể tạo chiến lược: {str(e)}" | |
| # Button streaming sẽ chỉ hiện khi đã có dữ liệu | |
| def hien_thi_nut_streaming(lua_chon_case): | |
| """Hiển thị button streaming khi đã chọn mặt hàng""" | |
| return gr.update(visible=lua_chon_case is not None) | |
| # Xử lý sự kiện cho Tab 1: Dự báo chính | |
| lua_chon_dropdown.change( | |
| fn=lambda x: [cap_nhat_thong_tin_case(x), hien_thi_nut_streaming(x)], | |
| inputs=[lua_chon_dropdown], | |
| outputs=[hien_thi_thong_tin, nut_streaming] | |
| ) | |
| # Event chain mới: scroll → loading 3–5s → render kết quả | |
| # 1) Kickoff: bật panel loading + auto scroll | |
| evt0 = nut_du_bao.click( | |
| fn=bat_dau_loading, | |
| inputs=[], | |
| outputs=[bieu_do_du_bao, hien_thi_tom_tat, hien_thi_bao_cao, loading_panel], | |
| show_progress=False, | |
| scroll_to_output=False, # Không scroll tự động, sẽ dùng JS | |
| ) | |
| # Scroll tới loading panel thay vì chart | |
| JS_SCROLL_TO_LOADING = r""" | |
| (...args) => { | |
| const el = document.getElementById('loading-panel'); | |
| if (el) { | |
| el.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| el.classList.add('highlight'); | |
| setTimeout(() => el.classList.remove('highlight'), 1200); | |
| } | |
| return []; | |
| } | |
| """ | |
| try: | |
| evt0.then(fn=None, inputs=None, outputs=None, js=JS_SCROLL_TO_LOADING) | |
| except TypeError: | |
| evt0.then(fn=None, inputs=None, outputs=None, _js=JS_SCROLL_TO_LOADING) # type: ignore | |
| # 2) Stream 4 bước loading trong 3–5 giây (chỉ update loading_panel) | |
| evt1 = evt0.then( | |
| fn=hien_loading_tung_buoc, | |
| inputs=[], | |
| outputs=[loading_panel], | |
| show_progress=False, | |
| ) | |
| # 3) Tính toán thật, render kết quả + tắt loading | |
| evt2 = evt1.then( | |
| fn=tinh_toan_ket_qua, | |
| inputs=[lua_chon_dropdown, chu_ky_dropdown, che_do_xem], | |
| outputs=[bieu_do_du_bao, hien_thi_tom_tat, hien_thi_bao_cao, loading_panel], | |
| show_progress=False, | |
| ) | |
| # Thêm hiệu ứng fade-in cho LLM analysis sau khi render | |
| JS_FADE_IN_LLM = r""" | |
| (...args) => { | |
| setTimeout(() => { | |
| const markdownElements = document.querySelectorAll('.gradio-markdown'); | |
| markdownElements.forEach(el => { | |
| el.style.animation = 'fadeInUp 0.8s ease-out forwards'; | |
| }); | |
| }, 200); | |
| return []; | |
| } | |
| """ | |
| try: | |
| evt2.then(fn=None, inputs=None, outputs=None, js=JS_FADE_IN_LLM) | |
| except TypeError: | |
| evt2.then(fn=None, inputs=None, outputs=None, _js=JS_FADE_IN_LLM) # type: ignore | |
| # Streaming event cho chiến lược đề xuất | |
| nut_streaming.click( | |
| fn=xu_ly_streaming, | |
| inputs=[lua_chon_dropdown], | |
| outputs=[hien_thi_bao_cao], | |
| show_progress=False | |
| ) | |
| # Cập nhật biểu đồ khi thay đổi chế độ hiển thị - chỉ khi đã có dự đoán | |
| def cap_nhat_khi_doi_che_do(case, chu_ky, mode): | |
| try: | |
| if case and case != "": | |
| return [ | |
| tao_bieu_do_du_doan(case, chu_ky, mode), | |
| tao_thong_tin_tom_tat(case, chu_ky, mode) | |
| ] | |
| else: | |
| return [tao_bieu_do_mac_dinh(), tao_tom_tat_mac_dinh()] | |
| except: | |
| return [tao_bieu_do_mac_dinh(), tao_tom_tat_mac_dinh()] | |
| che_do_xem.change( | |
| fn=cap_nhat_khi_doi_che_do, | |
| inputs=[lua_chon_dropdown, chu_ky_dropdown, che_do_xem], | |
| outputs=[bieu_do_du_bao, hien_thi_tom_tat] | |
| ) | |
| # Tự động load case đầu tiên với biểu đồ mặc định | |
| demo.load( | |
| fn=lambda: [ | |
| cap_nhat_thong_tin_case(list(TRUONG_HOP_DEMO.keys())[0]), | |
| tao_bieu_do_mac_dinh(), | |
| tao_tom_tat_mac_dinh(), | |
| """# 🎯 CHIẾN LƯỢC ĐỀ XUẤT | |
| ## 📊 **Sẵn Sàng Phân Tích** | |
| **Nhấn 'Tạo Dự Báo AI' để xem phân tích chi tiết** | |
| Hệ thống AI sẽ tạo chiến lược đề xuất với: | |
| • **Phân tích xu hướng** thị trường | |
| • **Insights chi tiết** từ dữ liệu | |
| • **Khuyến nghị cụ thể** để tối ưa doanh thu | |
| • **Roadmap hành động** rõ ràng | |
| *🤖 Powered by FoxAI Intelligence*""" | |
| ], | |
| outputs=[hien_thi_thong_tin, bieu_do_du_bao, hien_thi_tom_tat, hien_thi_bao_cao] | |
| ) | |
| # Footer thông tin hệ thống với branding FoxAI | |
| gr.HTML(f""" | |
| <div class='info-card' style="margin-top: 40px; text-align: center;"> | |
| <!-- Logo Center --> | |
| <div style=" | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-bottom: 35px; | |
| "> | |
| <img src="{FOXAI_LOGO_URL}" alt="FoxAI" style=" | |
| height: 80px; | |
| filter: drop-shadow(0 3px 6px rgba(0,0,0,0.25)); | |
| "/> | |
| </div> | |
| <!-- Grid --> | |
| <div style=" | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 25px; | |
| margin: 30px 0; | |
| "> | |
| <div style=" | |
| padding: 25px; | |
| border-radius: 16px; | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| "> | |
| <h4 style="color: #fff; margin-bottom: 15px; font-size: 1.4em; font-weight: 700;"> | |
| 🎯 Tính Năng | |
| </h4> | |
| <div style="color: #f1f5f9; font-weight: 500; line-height: 1.8;"> | |
| ✨ AI Insights thông minh<br/> | |
| 📊 Multi-view linh hoạt<br/> | |
| 📈 Báo cáo chuyên nghiệp<br/> | |
| ⚡ Cập nhật real-time | |
| </div> | |
| </div> | |
| <div style=" | |
| padding: 25px; | |
| border-radius: 16px; | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| "> | |
| <h4 style="color: #fff; margin-bottom: 15px; font-size: 1.4em; font-weight: 700;"> | |
| 📊 Chất Lượng | |
| </h4> | |
| <div style="color: #f1f5f9; font-weight: 500; line-height: 1.8;"> | |
| 🎯 Độ chính xác cao<br/> | |
| 📅 Dự báo theo quý<br/> | |
| 🔄 Market intelligence<br/> | |
| 💼 Enterprise ready | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer strip --> | |
| <div style=" | |
| margin-top: 25px; | |
| padding: 20px; | |
| border-radius: 16px; | |
| background: rgba(30,41,59,0.7); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| "> | |
| <p style="color: #fff; font-size: 1.1em; font-weight: 600; margin: 0;"> | |
| 🦊 Powered by FoxAi • 🇻🇳 Made in Vietnam • 🤖 Advanced AI | |
| </p> | |
| <p style="color: #e2e8f0; margin-top: 8px; font-weight: 500;"> | |
| YOUR TRUSTED AI PARTNER | |
| </p> | |
| </div> | |
| </div> | |
| """) | |
| return demo | |
| # Chạy ứng dụng | |
| if __name__ == "__main__": | |
| demo = tao_ung_dung() | |
| demo.launch( | |
| share=True, | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| show_error=True | |
| ) | |