demo-predict / app.py
TrBn17
new app
1d364e7
# -*- 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
)