Update app.py
Browse files
app.py
CHANGED
|
@@ -16,6 +16,8 @@ import plotly.io as pio
|
|
| 16 |
import tempfile
|
| 17 |
import os
|
| 18 |
import requests
|
|
|
|
|
|
|
| 19 |
|
| 20 |
DESIGN_SYSTEM = {
|
| 21 |
'colors': {
|
|
@@ -89,117 +91,20 @@ def load_css():
|
|
| 89 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 90 |
margin-bottom: 1rem;
|
| 91 |
}}
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
gap: 16px;
|
| 98 |
-
margin-top: 20px;
|
| 99 |
-
}}
|
| 100 |
-
.quality-card {{
|
| 101 |
-
background: white;
|
| 102 |
-
border: 1px solid {DESIGN_SYSTEM['colors']['border']};
|
| 103 |
-
border-radius: 12px;
|
| 104 |
-
padding: 24px;
|
| 105 |
-
min-height: 140px;
|
| 106 |
-
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
| 107 |
-
border-left: 4px solid transparent;
|
| 108 |
-
transition: all 0.3s ease;
|
| 109 |
-
position: relative;
|
| 110 |
-
}}
|
| 111 |
-
.quality-card:hover {{
|
| 112 |
-
transform: translateY(-2px);
|
| 113 |
-
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
| 114 |
-
}}
|
| 115 |
-
.card-warning {{
|
| 116 |
-
border-left-color: {DESIGN_SYSTEM['colors']['warning']};
|
| 117 |
-
}}
|
| 118 |
-
.card-success {{
|
| 119 |
-
border-left-color: {DESIGN_SYSTEM['colors']['success']};
|
| 120 |
-
}}
|
| 121 |
-
.card-title {{
|
| 122 |
-
font-size: 16px;
|
| 123 |
-
font-weight: 600;
|
| 124 |
-
color: {DESIGN_SYSTEM['colors']['text']};
|
| 125 |
-
margin-bottom: 12px;
|
| 126 |
-
}}
|
| 127 |
-
.status-badge {{
|
| 128 |
-
display: inline-block;
|
| 129 |
-
padding: 6px 12px;
|
| 130 |
-
border-radius: 16px;
|
| 131 |
-
font-size: 12px;
|
| 132 |
-
font-weight: 600;
|
| 133 |
-
text-transform: uppercase;
|
| 134 |
-
letter-spacing: 0.5px;
|
| 135 |
-
margin-bottom: 12px;
|
| 136 |
-
}}
|
| 137 |
-
.status-warning {{
|
| 138 |
-
background: rgba(217, 119, 6, 0.15);
|
| 139 |
-
color: {DESIGN_SYSTEM['colors']['warning']};
|
| 140 |
-
}}
|
| 141 |
-
.status-success {{
|
| 142 |
-
background: rgba(16, 185, 129, 0.15);
|
| 143 |
-
color: {DESIGN_SYSTEM['colors']['success']};
|
| 144 |
-
}}
|
| 145 |
-
.outliers-info {{
|
| 146 |
-
display: flex;
|
| 147 |
-
align-items: baseline;
|
| 148 |
-
margin-bottom: 12px;
|
| 149 |
-
}}
|
| 150 |
-
.outliers-count {{
|
| 151 |
-
font-size: 24px;
|
| 152 |
-
font-weight: 700;
|
| 153 |
-
margin-right: 8px;
|
| 154 |
-
}}
|
| 155 |
-
.outliers-count.warning {{
|
| 156 |
-
color: {DESIGN_SYSTEM['colors']['warning']};
|
| 157 |
-
}}
|
| 158 |
-
.outliers-count.success {{
|
| 159 |
color: {DESIGN_SYSTEM['colors']['success']};
|
| 160 |
}}
|
| 161 |
-
.
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
}}
|
| 165 |
-
.range-info {{
|
| 166 |
-
background: #F8F9FA;
|
| 167 |
-
padding: 12px;
|
| 168 |
border-radius: 8px;
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
color: #495057;
|
| 172 |
-
}}
|
| 173 |
-
.range-label {{
|
| 174 |
-
font-weight: 600;
|
| 175 |
-
color: {DESIGN_SYSTEM['colors']['text']};
|
| 176 |
-
margin-bottom: 4px;
|
| 177 |
-
}}
|
| 178 |
-
.dates-section {{
|
| 179 |
-
font-size: 12px;
|
| 180 |
-
color: #6B7280;
|
| 181 |
-
}}
|
| 182 |
-
.dates-label {{
|
| 183 |
-
font-size: 11px;
|
| 184 |
-
color: #9CA3AF;
|
| 185 |
-
text-transform: uppercase;
|
| 186 |
-
letter-spacing: 0.5px;
|
| 187 |
-
margin-bottom: 8px;
|
| 188 |
-
font-weight: 600;
|
| 189 |
-
}}
|
| 190 |
-
.dates-list {{
|
| 191 |
-
line-height: 1.6;
|
| 192 |
-
}}
|
| 193 |
-
.success-center {{
|
| 194 |
-
text-align: center;
|
| 195 |
-
margin-top: 16px;
|
| 196 |
-
}}
|
| 197 |
-
.success-message {{
|
| 198 |
-
font-size: 14px;
|
| 199 |
-
color: {DESIGN_SYSTEM['colors']['success']};
|
| 200 |
-
font-weight: 500;
|
| 201 |
}}
|
| 202 |
-
|
| 203 |
.stButton > button {{
|
| 204 |
background: {DESIGN_SYSTEM['colors']['primary']};
|
| 205 |
color: white;
|
|
@@ -223,10 +128,21 @@ def load_css():
|
|
| 223 |
|
| 224 |
@st.cache_resource
|
| 225 |
def init_ai():
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
if api_key:
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
return None
|
| 231 |
|
| 232 |
@st.cache_data
|
|
@@ -256,7 +172,7 @@ def generate_sample_data(year):
|
|
| 256 |
dates = pd.date_range(start=start_date, end=end_date, freq='D')
|
| 257 |
weekdays = dates[dates.weekday < 5]
|
| 258 |
data = []
|
| 259 |
-
materials = ['steel', 'aluminum', '
|
| 260 |
shifts = ['day', 'night']
|
| 261 |
for date in weekdays:
|
| 262 |
for material in materials:
|
|
@@ -264,7 +180,7 @@ def generate_sample_data(year):
|
|
| 264 |
base_weight = {
|
| 265 |
'steel': 1500,
|
| 266 |
'aluminum': 800,
|
| 267 |
-
'
|
| 268 |
'copper': 400
|
| 269 |
}[material]
|
| 270 |
weight = base_weight + np.random.normal(0, base_weight * 0.2)
|
|
@@ -426,145 +342,82 @@ def detect_outliers(df):
|
|
| 426 |
return outliers
|
| 427 |
|
| 428 |
def generate_ai_summary(model, df, stats, outliers):
|
| 429 |
-
"""Generate professional data analysis with actionable insights"""
|
| 430 |
if not model:
|
| 431 |
-
return "AI analysis unavailable - API key not configured"
|
| 432 |
-
|
| 433 |
try:
|
| 434 |
-
total_production = stats['_total_']['total']
|
| 435 |
-
work_days = stats['_total_']['work_days']
|
| 436 |
-
daily_avg = stats['_total_']['daily_avg']
|
| 437 |
materials = [k for k in stats.keys() if k != '_total_']
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
night_total = df[df['shift'] == 'night']['weight_kg'].sum()
|
| 449 |
-
day_pct = (day_total / total_production) * 100
|
| 450 |
-
night_pct = (night_total / total_production) * 100
|
| 451 |
-
shift_info = f"Day shift: {day_pct:.1f}%, Night shift: {night_pct:.1f}%"
|
| 452 |
-
|
| 453 |
-
context = f"""
|
| 454 |
-
Production Analysis Data:
|
| 455 |
-
- Period: {df['date'].min().strftime('%B %d, %Y')} to {df['date'].max().strftime('%B %d, %Y')}
|
| 456 |
-
- Total production: {total_production:,.0f} kg over {work_days} work days
|
| 457 |
-
- Daily average: {daily_avg:,.0f} kg
|
| 458 |
-
- Materials tracked: {', '.join([m.title() for m in materials])}
|
| 459 |
-
- Top material: {top_material.title()} ({top_percentage:.1f}% of total)
|
| 460 |
-
- Quality issues: {total_outliers} outliers detected across all materials
|
| 461 |
-
- {shift_info}
|
| 462 |
-
|
| 463 |
-
Material breakdown:
|
| 464 |
-
"""
|
| 465 |
for material in materials:
|
| 466 |
info = stats[material]
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
prompt = f"""
|
| 471 |
-
{
|
| 472 |
|
| 473 |
-
|
| 474 |
|
| 475 |
-
|
| 476 |
-
One paragraph overview emphasizing production strengths and improvement opportunities (not problems).
|
| 477 |
|
| 478 |
-
**
|
| 479 |
-
|
| 480 |
|
| 481 |
-
**
|
| 482 |
-
|
| 483 |
|
| 484 |
-
|
| 485 |
-
|
| 486 |
|
| 487 |
-
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
| 489 |
|
|
|
|
|
|
|
| 490 |
response = model.generate_content(prompt)
|
| 491 |
return response.text
|
| 492 |
-
|
| 493 |
except Exception as e:
|
| 494 |
return f"AI analysis error: {str(e)}"
|
| 495 |
|
| 496 |
-
def render_ai_section(df, stats, model):
|
| 497 |
-
"""Showcase our AI capabilities and technical achievements"""
|
| 498 |
-
|
| 499 |
-
if model:
|
| 500 |
-
st.markdown('<div class="section-header">AI-Powered Solution</div>', unsafe_allow_html=True)
|
| 501 |
-
|
| 502 |
-
# Demonstrate our capabilities
|
| 503 |
-
st.markdown("### **Nilsen Service & Consulting - Technical Achievements**")
|
| 504 |
-
outliers = detect_outliers(df)
|
| 505 |
-
with st.spinner("Demonstrating our AI capabilities..."):
|
| 506 |
-
ai_summary = generate_ai_summary(model, df, stats, outliers)
|
| 507 |
-
st.success(ai_summary)
|
| 508 |
-
|
| 509 |
-
# Show our system's intelligence
|
| 510 |
-
st.markdown("### **Experience Our Advanced Analytics**")
|
| 511 |
-
st.markdown("*See how our AI system provides professional insights for your operations:*")
|
| 512 |
-
|
| 513 |
-
demo_questions = [
|
| 514 |
-
"How does our monitoring system ensure production quality?",
|
| 515 |
-
"What operational advantages does our platform provide?",
|
| 516 |
-
"How does our AI deliver cost savings and efficiency gains?"
|
| 517 |
-
]
|
| 518 |
-
|
| 519 |
-
cols = st.columns(len(demo_questions))
|
| 520 |
-
for i, question in enumerate(demo_questions):
|
| 521 |
-
with cols[i]:
|
| 522 |
-
if st.button(f"{question}", key=f"demo_q_{i}"):
|
| 523 |
-
with st.spinner("Our AI analyzing..."):
|
| 524 |
-
answer = query_ai(model, stats, question, df)
|
| 525 |
-
st.info(f"**Nilsen AI Response:** {answer}")
|
| 526 |
-
|
| 527 |
-
# Interactive demonstration
|
| 528 |
-
st.markdown("### **Test Our AI Intelligence**")
|
| 529 |
-
col1, col2 = st.columns([3, 1])
|
| 530 |
-
|
| 531 |
-
with col1:
|
| 532 |
-
custom_question = st.text_input("Ask our AI system about production management:",
|
| 533 |
-
placeholder="e.g., 'How can your system improve our operational efficiency?'",
|
| 534 |
-
key="demo_question")
|
| 535 |
-
with col2:
|
| 536 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 537 |
-
if st.button("See Our AI in Action", key="demo_btn", type="primary"):
|
| 538 |
-
if custom_question:
|
| 539 |
-
with st.spinner("Nilsen AI processing..."):
|
| 540 |
-
answer = query_ai(model, stats, custom_question, df)
|
| 541 |
-
st.success(f"**Your Question:** {custom_question}")
|
| 542 |
-
st.info(f"**Our AI Solution:** {answer}")
|
| 543 |
-
else:
|
| 544 |
-
st.warning("Please enter a question to see our AI capabilities")
|
| 545 |
-
|
| 546 |
-
# Value proposition
|
| 547 |
-
st.markdown("---")
|
| 548 |
-
st.markdown("""
|
| 549 |
-
<div style="background: linear-gradient(135deg, #1E40AF15, #05966925); padding: 1rem; border-radius: 8px; border: 1px solid #1E40AF;">
|
| 550 |
-
<h4>Why Choose Nilsen Service & Consulting?</h4>
|
| 551 |
-
<ul>
|
| 552 |
-
<li><strong>Advanced AI Integration:</strong> Cutting-edge analytics for your operations</li>
|
| 553 |
-
<li><strong>Professional Reliability:</strong> Enterprise-grade monitoring systems</li>
|
| 554 |
-
<li><strong>Proven Results:</strong> Data-driven insights that deliver ROI</li>
|
| 555 |
-
<li><strong>Complete Solution:</strong> From data processing to actionable recommendations</li>
|
| 556 |
-
</ul>
|
| 557 |
-
</div>
|
| 558 |
-
""", unsafe_allow_html=True)
|
| 559 |
-
|
| 560 |
-
else:
|
| 561 |
-
st.markdown('<div class="section-header">AI-Powered Solution</div>', unsafe_allow_html=True)
|
| 562 |
-
st.error("AI demonstration requires API configuration")
|
| 563 |
-
st.info("**Our AI system provides:** Advanced analytics, predictive insights, automated reporting, and intelligent recommendations for production optimization.")
|
| 564 |
-
|
| 565 |
def query_ai(model, stats, question, df=None):
|
| 566 |
if not model:
|
| 567 |
-
return "AI assistant not available"
|
| 568 |
context_parts = [
|
| 569 |
"Production Data Summary:",
|
| 570 |
*[f"- {mat.title()}: {info['total']:,.0f}kg ({info['percentage']:.1f}%)"
|
|
@@ -584,8 +437,8 @@ def query_ai(model, stats, question, df=None):
|
|
| 584 |
try:
|
| 585 |
response = model.generate_content(context)
|
| 586 |
return response.text
|
| 587 |
-
except:
|
| 588 |
-
return "Error getting AI response"
|
| 589 |
|
| 590 |
def save_plotly_as_image(fig, filename):
|
| 591 |
try:
|
|
@@ -815,7 +668,7 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
|
|
| 815 |
else:
|
| 816 |
elements.append(PageBreak())
|
| 817 |
elements.append(Paragraph("AI Analysis", subtitle_style))
|
| 818 |
-
elements.append(Paragraph("AI analysis unavailable - API key not configured. Please configure
|
| 819 |
elements.append(Spacer(1, 30))
|
| 820 |
footer_text = f"""
|
| 821 |
<para alignment="center">
|
|
@@ -844,7 +697,7 @@ def create_csv_export(df, stats):
|
|
| 844 |
return summary_df
|
| 845 |
|
| 846 |
def add_export_section(df, stats, outliers, model):
|
| 847 |
-
st.markdown('<div class="section-header">Export Reports</div>', unsafe_allow_html=True)
|
| 848 |
if 'export_ready' not in st.session_state:
|
| 849 |
st.session_state.export_ready = False
|
| 850 |
if 'pdf_buffer' not in st.session_state:
|
|
@@ -858,13 +711,13 @@ def add_export_section(df, stats, outliers, model):
|
|
| 858 |
with st.spinner("Generating PDF with AI analysis..."):
|
| 859 |
st.session_state.pdf_buffer = create_enhanced_pdf_report(df, stats, outliers, model)
|
| 860 |
st.session_state.export_ready = True
|
| 861 |
-
st.success("PDF report with AI analysis generated successfully!")
|
| 862 |
except Exception as e:
|
| 863 |
-
st.error(f"PDF generation failed: {str(e)}")
|
| 864 |
st.session_state.export_ready = False
|
| 865 |
if st.session_state.export_ready and st.session_state.pdf_buffer:
|
| 866 |
st.download_button(
|
| 867 |
-
label="Download PDF Report",
|
| 868 |
data=st.session_state.pdf_buffer,
|
| 869 |
file_name=f"production_report_ai_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
|
| 870 |
mime="application/pdf",
|
|
@@ -874,13 +727,13 @@ def add_export_section(df, stats, outliers, model):
|
|
| 874 |
if st.button("Generate CSV Summary", key="generate_csv_btn", type="primary"):
|
| 875 |
try:
|
| 876 |
st.session_state.csv_data = create_csv_export(df, stats)
|
| 877 |
-
st.success("CSV summary generated successfully!")
|
| 878 |
except Exception as e:
|
| 879 |
-
st.error(f"CSV generation failed: {str(e)}")
|
| 880 |
if st.session_state.csv_data is not None:
|
| 881 |
csv_string = st.session_state.csv_data.to_csv(index=False)
|
| 882 |
st.download_button(
|
| 883 |
-
label="Download CSV Summary",
|
| 884 |
data=csv_string,
|
| 885 |
file_name=f"production_summary_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
|
| 886 |
mime="text/csv",
|
|
@@ -900,29 +753,26 @@ def main():
|
|
| 900 |
load_css()
|
| 901 |
st.markdown("""
|
| 902 |
<div class="main-header">
|
| 903 |
-
<div class="main-title">Production Monitor with AI Insights</div>
|
| 904 |
<div class="main-subtitle">Nilsen Service & Consulting AS | Real-time Production Analytics & Recommendations</div>
|
| 905 |
</div>
|
| 906 |
""", unsafe_allow_html=True)
|
| 907 |
-
|
| 908 |
model = init_ai()
|
| 909 |
-
|
| 910 |
if 'current_df' not in st.session_state:
|
| 911 |
st.session_state.current_df = None
|
| 912 |
if 'current_stats' not in st.session_state:
|
| 913 |
st.session_state.current_stats = None
|
| 914 |
-
|
| 915 |
with st.sidebar:
|
| 916 |
-
st.markdown("### Data Source")
|
| 917 |
uploaded_file = st.file_uploader("Upload Production Data", type=['csv'])
|
| 918 |
st.markdown("---")
|
| 919 |
-
st.markdown("### Quick Load")
|
| 920 |
col1, col2 = st.columns(2)
|
| 921 |
with col1:
|
| 922 |
-
if st.button("2024 Data", type="primary", key="load_2024"):
|
| 923 |
st.session_state.load_preset = "2024"
|
| 924 |
with col2:
|
| 925 |
-
if st.button("2025 Data", type="primary", key="load_2025"):
|
| 926 |
st.session_state.load_preset = "2025"
|
| 927 |
st.markdown("---")
|
| 928 |
st.markdown("""
|
|
@@ -933,22 +783,21 @@ def main():
|
|
| 933 |
- `shift`: day/night (optional)
|
| 934 |
""")
|
| 935 |
if model:
|
| 936 |
-
st.success("AI Assistant Ready")
|
| 937 |
else:
|
| 938 |
-
st.warning("AI Assistant Unavailable")
|
| 939 |
-
|
| 940 |
df = st.session_state.current_df
|
| 941 |
stats = st.session_state.current_stats
|
| 942 |
-
|
| 943 |
if uploaded_file:
|
| 944 |
try:
|
| 945 |
df = load_data(uploaded_file)
|
| 946 |
stats = get_material_stats(df)
|
| 947 |
st.session_state.current_df = df
|
| 948 |
st.session_state.current_stats = stats
|
| 949 |
-
st.success("Data uploaded successfully!")
|
| 950 |
except Exception as e:
|
| 951 |
-
st.error(f"Error loading uploaded file: {str(e)}")
|
| 952 |
elif 'load_preset' in st.session_state:
|
| 953 |
year = st.session_state.load_preset
|
| 954 |
try:
|
|
@@ -958,42 +807,34 @@ def main():
|
|
| 958 |
stats = get_material_stats(df)
|
| 959 |
st.session_state.current_df = df
|
| 960 |
st.session_state.current_stats = stats
|
| 961 |
-
st.success(f"{year} data loaded successfully!")
|
| 962 |
except Exception as e:
|
| 963 |
-
st.error(f"Error loading {year} data: {str(e)}")
|
| 964 |
finally:
|
| 965 |
del st.session_state.load_preset
|
| 966 |
-
|
| 967 |
if df is not None and stats is not None:
|
| 968 |
-
st.markdown('<div class="section-header">Material Overview</div>', unsafe_allow_html=True)
|
| 969 |
materials = [k for k in stats.keys() if k != '_total_']
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
label=item.replace('_', ' ').title(),
|
| 991 |
-
value=f"{info['total']:,.0f} kg",
|
| 992 |
-
delta=f"{info['percentage']:.1f}% of total"
|
| 993 |
-
)
|
| 994 |
-
st.caption(f"Daily avg: {info['daily_avg']:,.0f} kg")
|
| 995 |
-
|
| 996 |
-
st.markdown('<div class="section-header">Production Trends</div>', unsafe_allow_html=True)
|
| 997 |
col1, col2 = st.columns([3, 1])
|
| 998 |
with col2:
|
| 999 |
time_view = st.selectbox("Time Period", ["daily", "weekly", "monthly"], key="time_view_select")
|
|
@@ -1003,8 +844,7 @@ def main():
|
|
| 1003 |
total_chart = create_total_production_chart(df, time_view)
|
| 1004 |
st.plotly_chart(total_chart, use_container_width=True)
|
| 1005 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 1006 |
-
|
| 1007 |
-
st.markdown('<div class="section-header">Materials Analysis</div>', unsafe_allow_html=True)
|
| 1008 |
col1, col2 = st.columns([3, 1])
|
| 1009 |
with col2:
|
| 1010 |
selected_materials = st.multiselect(
|
|
@@ -1020,77 +860,29 @@ def main():
|
|
| 1020 |
materials_chart = create_materials_trend_chart(df, time_view, selected_materials)
|
| 1021 |
st.plotly_chart(materials_chart, use_container_width=True)
|
| 1022 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 1023 |
-
|
| 1024 |
if 'shift' in df.columns:
|
| 1025 |
-
st.markdown('<div class="section-header">Shift Analysis</div>', unsafe_allow_html=True)
|
| 1026 |
with st.container():
|
| 1027 |
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 1028 |
shift_chart = create_shift_trend_chart(df, time_view)
|
| 1029 |
st.plotly_chart(shift_chart, use_container_width=True)
|
| 1030 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 1031 |
-
|
| 1032 |
-
# Updated Quality Check Section
|
| 1033 |
-
st.markdown('<div class="section-header">Quality Check</div>', unsafe_allow_html=True)
|
| 1034 |
outliers = detect_outliers(df)
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
cols_per_row = min(3, num_materials) if num_materials <= 6 else 4
|
| 1042 |
-
|
| 1043 |
-
for i in range(0, num_materials, cols_per_row):
|
| 1044 |
-
row_materials = materials_list[i:i+cols_per_row]
|
| 1045 |
-
cols = st.columns(len(row_materials))
|
| 1046 |
-
|
| 1047 |
-
for j, (material, info) in enumerate(row_materials):
|
| 1048 |
-
with cols[j]:
|
| 1049 |
-
material_title = material.replace("_", " ").title()
|
| 1050 |
-
|
| 1051 |
-
if info['count'] > 0:
|
| 1052 |
-
# Show all dates
|
| 1053 |
-
dates_display = ', '.join(info['dates'])
|
| 1054 |
-
|
| 1055 |
-
card_html = f'''
|
| 1056 |
-
<div class="quality-card card-warning">
|
| 1057 |
-
<div class="card-title">{material_title}</div>
|
| 1058 |
-
<div class="status-badge status-warning">Warning</div>
|
| 1059 |
-
<div class="outliers-info">
|
| 1060 |
-
<span class="outliers-count warning">{info["count"]}</span>
|
| 1061 |
-
<span class="outliers-text">Outliers</span>
|
| 1062 |
-
</div>
|
| 1063 |
-
<div class="range-info">
|
| 1064 |
-
<div class="range-label">Normal Range</div>
|
| 1065 |
-
<div>{info["range"]}</div>
|
| 1066 |
-
</div>
|
| 1067 |
-
<div class="dates-section">
|
| 1068 |
-
<div class="dates-label">Dates</div>
|
| 1069 |
-
<div class="dates-list">{dates_display}</div>
|
| 1070 |
-
</div>
|
| 1071 |
-
</div>
|
| 1072 |
-
'''
|
| 1073 |
else:
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
<div class="outliers-info">
|
| 1079 |
-
<span class="outliers-count success">0</span>
|
| 1080 |
-
<span class="outliers-text">Outliers</span>
|
| 1081 |
-
</div>
|
| 1082 |
-
<div class="success-center">
|
| 1083 |
-
<div class="success-message">All values within normal range</div>
|
| 1084 |
-
</div>
|
| 1085 |
-
</div>
|
| 1086 |
-
'''
|
| 1087 |
-
|
| 1088 |
-
st.markdown(card_html, unsafe_allow_html=True)
|
| 1089 |
-
|
| 1090 |
add_export_section(df, stats, outliers, model)
|
| 1091 |
-
|
| 1092 |
if model:
|
| 1093 |
-
st.markdown('<div class="section-header">AI Insights</div>', unsafe_allow_html=True)
|
| 1094 |
quick_questions = [
|
| 1095 |
"How does production distribution on weekdays compare to weekends?",
|
| 1096 |
"Which material exhibits the most volatility in our dataset?",
|
|
@@ -1103,7 +895,6 @@ def main():
|
|
| 1103 |
with st.spinner("Analyzing..."):
|
| 1104 |
answer = query_ai(model, stats, q, df)
|
| 1105 |
st.info(answer)
|
| 1106 |
-
|
| 1107 |
custom_question = st.text_input("Ask about your production data:",
|
| 1108 |
placeholder="e.g., 'Compare steel vs aluminum last month'",
|
| 1109 |
key="custom_ai_question")
|
|
@@ -1112,12 +903,33 @@ def main():
|
|
| 1112 |
answer = query_ai(model, stats, custom_question, df)
|
| 1113 |
st.success(f"**Q:** {custom_question}")
|
| 1114 |
st.write(f"**A:** {answer}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
else:
|
| 1116 |
-
st.markdown('<div class="section-header">How to Use This Platform</div>', unsafe_allow_html=True)
|
| 1117 |
col1, col2 = st.columns(2)
|
| 1118 |
with col1:
|
| 1119 |
st.markdown("""
|
| 1120 |
-
### Quick Start
|
| 1121 |
1. Upload your TSV data in the sidebar
|
| 1122 |
2. Or click Quick Load buttons for preset data
|
| 1123 |
3. View production by material type
|
|
@@ -1128,7 +940,7 @@ def main():
|
|
| 1128 |
""")
|
| 1129 |
with col2:
|
| 1130 |
st.markdown("""
|
| 1131 |
-
### Key Features
|
| 1132 |
- Real-time interactive charts
|
| 1133 |
- One-click preset data loading
|
| 1134 |
- Time-period comparisons
|
|
@@ -1137,7 +949,7 @@ def main():
|
|
| 1137 |
- AI-powered PDF reports
|
| 1138 |
- Intelligent recommendations
|
| 1139 |
""")
|
| 1140 |
-
st.info("Ready to start? Upload your production data or use Quick Load buttons to begin analysis!")
|
| 1141 |
|
| 1142 |
if __name__ == "__main__":
|
| 1143 |
main()
|
|
|
|
| 16 |
import tempfile
|
| 17 |
import os
|
| 18 |
import requests
|
| 19 |
+
import warnings
|
| 20 |
+
warnings.filterwarnings("ignore", message=".*secrets.*")
|
| 21 |
|
| 22 |
DESIGN_SYSTEM = {
|
| 23 |
'colors': {
|
|
|
|
| 91 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 92 |
margin-bottom: 1rem;
|
| 93 |
}}
|
| 94 |
+
.alert-success {{
|
| 95 |
+
background: linear-gradient(135deg, {DESIGN_SYSTEM['colors']['success']}15, {DESIGN_SYSTEM['colors']['success']}25);
|
| 96 |
+
border: 1px solid {DESIGN_SYSTEM['colors']['success']};
|
| 97 |
+
border-radius: 8px;
|
| 98 |
+
padding: 1rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
color: {DESIGN_SYSTEM['colors']['success']};
|
| 100 |
}}
|
| 101 |
+
.alert-warning {{
|
| 102 |
+
background: linear-gradient(135deg, {DESIGN_SYSTEM['colors']['warning']}15, {DESIGN_SYSTEM['colors']['warning']}25);
|
| 103 |
+
border: 1px solid {DESIGN_SYSTEM['colors']['warning']};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
border-radius: 8px;
|
| 105 |
+
padding: 1rem;
|
| 106 |
+
color: {DESIGN_SYSTEM['colors']['warning']};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}}
|
|
|
|
| 108 |
.stButton > button {{
|
| 109 |
background: {DESIGN_SYSTEM['colors']['primary']};
|
| 110 |
color: white;
|
|
|
|
| 128 |
|
| 129 |
@st.cache_resource
|
| 130 |
def init_ai():
|
| 131 |
+
"""Initialize AI model with proper error handling for secrets"""
|
| 132 |
+
try:
|
| 133 |
+
# Try to get API key from Streamlit secrets
|
| 134 |
+
api_key = st.secrets.get("GOOGLE_API_KEY", "")
|
| 135 |
+
except (FileNotFoundError, KeyError, AttributeError):
|
| 136 |
+
# If secrets file doesn't exist or key not found, try environment variable
|
| 137 |
+
api_key = os.environ.get("GOOGLE_API_KEY", "")
|
| 138 |
+
|
| 139 |
if api_key:
|
| 140 |
+
try:
|
| 141 |
+
genai.configure(api_key=api_key)
|
| 142 |
+
return genai.GenerativeModel('gemini-1.5-flash')
|
| 143 |
+
except Exception as e:
|
| 144 |
+
st.error(f"AI configuration failed: {str(e)}")
|
| 145 |
+
return None
|
| 146 |
return None
|
| 147 |
|
| 148 |
@st.cache_data
|
|
|
|
| 172 |
dates = pd.date_range(start=start_date, end=end_date, freq='D')
|
| 173 |
weekdays = dates[dates.weekday < 5]
|
| 174 |
data = []
|
| 175 |
+
materials = ['steel', 'aluminum', 'plastic', 'copper']
|
| 176 |
shifts = ['day', 'night']
|
| 177 |
for date in weekdays:
|
| 178 |
for material in materials:
|
|
|
|
| 180 |
base_weight = {
|
| 181 |
'steel': 1500,
|
| 182 |
'aluminum': 800,
|
| 183 |
+
'plastic': 600,
|
| 184 |
'copper': 400
|
| 185 |
}[material]
|
| 186 |
weight = base_weight + np.random.normal(0, base_weight * 0.2)
|
|
|
|
| 342 |
return outliers
|
| 343 |
|
| 344 |
def generate_ai_summary(model, df, stats, outliers):
|
|
|
|
| 345 |
if not model:
|
| 346 |
+
return "AI analysis unavailable - Google API key not configured. Please set the GOOGLE_API_KEY environment variable or in Streamlit secrets to enable AI insights."
|
|
|
|
| 347 |
try:
|
|
|
|
|
|
|
|
|
|
| 348 |
materials = [k for k in stats.keys() if k != '_total_']
|
| 349 |
+
context_parts = [
|
| 350 |
+
"# Production Data Analysis Context",
|
| 351 |
+
f"## Overview",
|
| 352 |
+
f"- Total Production: {stats['_total_']['total']:,.0f} kg",
|
| 353 |
+
f"- Production Period: {stats['_total_']['work_days']} working days",
|
| 354 |
+
f"- Daily Average: {stats['_total_']['daily_avg']:,.0f} kg",
|
| 355 |
+
f"- Materials Tracked: {len(materials)}",
|
| 356 |
+
"",
|
| 357 |
+
"## Material Breakdown:"
|
| 358 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
for material in materials:
|
| 360 |
info = stats[material]
|
| 361 |
+
context_parts.append(f"- {material.title()}: {info['total']:,.0f} kg ({info['percentage']:.1f}%), avg {info['daily_avg']:,.0f} kg/day")
|
| 362 |
+
daily_data = df.groupby('date')['weight_kg'].sum()
|
| 363 |
+
trend_direction = "increasing" if daily_data.iloc[-1] > daily_data.iloc[0] else "decreasing"
|
| 364 |
+
volatility = daily_data.std() / daily_data.mean() * 100
|
| 365 |
+
context_parts.extend([
|
| 366 |
+
"",
|
| 367 |
+
"## Trend Analysis:",
|
| 368 |
+
f"- Overall trend: {trend_direction}",
|
| 369 |
+
f"- Production volatility: {volatility:.1f}% coefficient of variation",
|
| 370 |
+
f"- Peak production: {daily_data.max():,.0f} kg",
|
| 371 |
+
f"- Lowest production: {daily_data.min():,.0f} kg"
|
| 372 |
+
])
|
| 373 |
+
total_outliers = sum(info['count'] for info in outliers.values())
|
| 374 |
+
context_parts.extend([
|
| 375 |
+
"",
|
| 376 |
+
"## Quality Control:",
|
| 377 |
+
f"- Total outliers detected: {total_outliers}",
|
| 378 |
+
f"- Materials with quality issues: {sum(1 for info in outliers.values() if info['count'] > 0)}"
|
| 379 |
+
])
|
| 380 |
+
if 'shift' in df.columns:
|
| 381 |
+
shift_stats = df.groupby('shift')['weight_kg'].sum()
|
| 382 |
+
context_parts.extend([
|
| 383 |
+
"",
|
| 384 |
+
"## Shift Performance:",
|
| 385 |
+
f"- Day shift: {shift_stats.get('day', 0):,.0f} kg",
|
| 386 |
+
f"- Night shift: {shift_stats.get('night', 0):,.0f} kg"
|
| 387 |
+
])
|
| 388 |
+
context_text = "\n".join(context_parts)
|
| 389 |
prompt = f"""
|
| 390 |
+
{context_text}
|
| 391 |
|
| 392 |
+
As an expert AI analyst embedded within the "Production Monitor with AI Insights" platform, provide a comprehensive analysis based on the data provided. Your tone should be professional and data-driven. Your primary goal is to highlight how the platform's features reveal critical insights.
|
| 393 |
|
| 394 |
+
Structure your response in the following format:
|
|
|
|
| 395 |
|
| 396 |
+
**PRODUCTION ASSESSMENT**
|
| 397 |
+
Evaluate the overall production status (Excellent/Good/Needs Attention). Briefly justify your assessment using key metrics from the data summary.
|
| 398 |
|
| 399 |
+
**KEY FINDINGS**
|
| 400 |
+
Identify 3-4 of the most important insights. For each finding, explicitly mention the platform feature that made the discovery possible. Use formats like "(revealed by the 'Quality Check' module)" or "(visualized in the 'Production Trend' chart)".
|
| 401 |
|
| 402 |
+
Example Finding format:
|
| 403 |
+
β’ Finding X: [Your insight, e.g., "Liquid-Ctu production shows high volatility..."] (as identified by the 'Materials Analysis' view).
|
| 404 |
|
| 405 |
+
**RECOMMENDATIONS**
|
| 406 |
+
Provide 2-3 actionable recommendations. Frame these as steps the management can take, encouraging them to use the platform for further investigation.
|
| 407 |
+
|
| 408 |
+
Example Recommendation format:
|
| 409 |
+
β’ Recommendation Y: [Your recommendation, e.g., "Investigate the root causes of the 11 outliers..."] We recommend using the platform's interactive charts to drill down into the specific dates identified by the 'Quality Check' module.
|
| 410 |
|
| 411 |
+
Keep the entire analysis concise and under 300 words.
|
| 412 |
+
"""
|
| 413 |
response = model.generate_content(prompt)
|
| 414 |
return response.text
|
|
|
|
| 415 |
except Exception as e:
|
| 416 |
return f"AI analysis error: {str(e)}"
|
| 417 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
def query_ai(model, stats, question, df=None):
|
| 419 |
if not model:
|
| 420 |
+
return "AI assistant not available - Please configure Google API key"
|
| 421 |
context_parts = [
|
| 422 |
"Production Data Summary:",
|
| 423 |
*[f"- {mat.title()}: {info['total']:,.0f}kg ({info['percentage']:.1f}%)"
|
|
|
|
| 437 |
try:
|
| 438 |
response = model.generate_content(context)
|
| 439 |
return response.text
|
| 440 |
+
except Exception as e:
|
| 441 |
+
return f"Error getting AI response: {str(e)}"
|
| 442 |
|
| 443 |
def save_plotly_as_image(fig, filename):
|
| 444 |
try:
|
|
|
|
| 668 |
else:
|
| 669 |
elements.append(PageBreak())
|
| 670 |
elements.append(Paragraph("AI Analysis", subtitle_style))
|
| 671 |
+
elements.append(Paragraph("AI analysis unavailable - Google API key not configured. Please set the GOOGLE_API_KEY environment variable or configure it in Streamlit secrets to enable intelligent insights.", styles['Normal']))
|
| 672 |
elements.append(Spacer(1, 30))
|
| 673 |
footer_text = f"""
|
| 674 |
<para alignment="center">
|
|
|
|
| 697 |
return summary_df
|
| 698 |
|
| 699 |
def add_export_section(df, stats, outliers, model):
|
| 700 |
+
st.markdown('<div class="section-header">π Export Reports</div>', unsafe_allow_html=True)
|
| 701 |
if 'export_ready' not in st.session_state:
|
| 702 |
st.session_state.export_ready = False
|
| 703 |
if 'pdf_buffer' not in st.session_state:
|
|
|
|
| 711 |
with st.spinner("Generating PDF with AI analysis..."):
|
| 712 |
st.session_state.pdf_buffer = create_enhanced_pdf_report(df, stats, outliers, model)
|
| 713 |
st.session_state.export_ready = True
|
| 714 |
+
st.success("β
PDF report with AI analysis generated successfully!")
|
| 715 |
except Exception as e:
|
| 716 |
+
st.error(f"β PDF generation failed: {str(e)}")
|
| 717 |
st.session_state.export_ready = False
|
| 718 |
if st.session_state.export_ready and st.session_state.pdf_buffer:
|
| 719 |
st.download_button(
|
| 720 |
+
label="πΎ Download PDF Report",
|
| 721 |
data=st.session_state.pdf_buffer,
|
| 722 |
file_name=f"production_report_ai_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
|
| 723 |
mime="application/pdf",
|
|
|
|
| 727 |
if st.button("Generate CSV Summary", key="generate_csv_btn", type="primary"):
|
| 728 |
try:
|
| 729 |
st.session_state.csv_data = create_csv_export(df, stats)
|
| 730 |
+
st.success("β
CSV summary generated successfully!")
|
| 731 |
except Exception as e:
|
| 732 |
+
st.error(f"β CSV generation failed: {str(e)}")
|
| 733 |
if st.session_state.csv_data is not None:
|
| 734 |
csv_string = st.session_state.csv_data.to_csv(index=False)
|
| 735 |
st.download_button(
|
| 736 |
+
label="πΎ Download CSV Summary",
|
| 737 |
data=csv_string,
|
| 738 |
file_name=f"production_summary_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
|
| 739 |
mime="text/csv",
|
|
|
|
| 753 |
load_css()
|
| 754 |
st.markdown("""
|
| 755 |
<div class="main-header">
|
| 756 |
+
<div class="main-title">π Production Monitor with AI Insights</div>
|
| 757 |
<div class="main-subtitle">Nilsen Service & Consulting AS | Real-time Production Analytics & Recommendations</div>
|
| 758 |
</div>
|
| 759 |
""", unsafe_allow_html=True)
|
|
|
|
| 760 |
model = init_ai()
|
|
|
|
| 761 |
if 'current_df' not in st.session_state:
|
| 762 |
st.session_state.current_df = None
|
| 763 |
if 'current_stats' not in st.session_state:
|
| 764 |
st.session_state.current_stats = None
|
|
|
|
| 765 |
with st.sidebar:
|
| 766 |
+
st.markdown("### π Data Source")
|
| 767 |
uploaded_file = st.file_uploader("Upload Production Data", type=['csv'])
|
| 768 |
st.markdown("---")
|
| 769 |
+
st.markdown("### π Quick Load")
|
| 770 |
col1, col2 = st.columns(2)
|
| 771 |
with col1:
|
| 772 |
+
if st.button("π 2024 Data", type="primary", key="load_2024"):
|
| 773 |
st.session_state.load_preset = "2024"
|
| 774 |
with col2:
|
| 775 |
+
if st.button("π 2025 Data", type="primary", key="load_2025"):
|
| 776 |
st.session_state.load_preset = "2025"
|
| 777 |
st.markdown("---")
|
| 778 |
st.markdown("""
|
|
|
|
| 783 |
- `shift`: day/night (optional)
|
| 784 |
""")
|
| 785 |
if model:
|
| 786 |
+
st.success("π€ AI Assistant Ready")
|
| 787 |
else:
|
| 788 |
+
st.warning("β οΈ AI Assistant Unavailable")
|
| 789 |
+
st.info("To enable AI features, set GOOGLE_API_KEY as environment variable or in Streamlit secrets")
|
| 790 |
df = st.session_state.current_df
|
| 791 |
stats = st.session_state.current_stats
|
|
|
|
| 792 |
if uploaded_file:
|
| 793 |
try:
|
| 794 |
df = load_data(uploaded_file)
|
| 795 |
stats = get_material_stats(df)
|
| 796 |
st.session_state.current_df = df
|
| 797 |
st.session_state.current_stats = stats
|
| 798 |
+
st.success("β
Data uploaded successfully!")
|
| 799 |
except Exception as e:
|
| 800 |
+
st.error(f"β Error loading uploaded file: {str(e)}")
|
| 801 |
elif 'load_preset' in st.session_state:
|
| 802 |
year = st.session_state.load_preset
|
| 803 |
try:
|
|
|
|
| 807 |
stats = get_material_stats(df)
|
| 808 |
st.session_state.current_df = df
|
| 809 |
st.session_state.current_stats = stats
|
| 810 |
+
st.success(f"β
{year} data loaded successfully!")
|
| 811 |
except Exception as e:
|
| 812 |
+
st.error(f"β Error loading {year} data: {str(e)}")
|
| 813 |
finally:
|
| 814 |
del st.session_state.load_preset
|
|
|
|
| 815 |
if df is not None and stats is not None:
|
| 816 |
+
st.markdown('<div class="section-header">π Material Overview</div>', unsafe_allow_html=True)
|
| 817 |
materials = [k for k in stats.keys() if k != '_total_']
|
| 818 |
+
cols = st.columns(4)
|
| 819 |
+
for i, material in enumerate(materials[:3]):
|
| 820 |
+
info = stats[material]
|
| 821 |
+
with cols[i]:
|
| 822 |
+
st.metric(
|
| 823 |
+
label=material.replace('_', ' ').title(),
|
| 824 |
+
value=f"{info['total']:,.0f} kg",
|
| 825 |
+
delta=f"{info['percentage']:.1f}% of total"
|
| 826 |
+
)
|
| 827 |
+
st.caption(f"Daily avg: {info['daily_avg']:,.0f} kg")
|
| 828 |
+
if len(materials) >= 3:
|
| 829 |
+
total_info = stats['_total_']
|
| 830 |
+
with cols[3]:
|
| 831 |
+
st.metric(
|
| 832 |
+
label="Total Production",
|
| 833 |
+
value=f"{total_info['total']:,.0f} kg",
|
| 834 |
+
delta="100% of total"
|
| 835 |
+
)
|
| 836 |
+
st.caption(f"Daily avg: {total_info['daily_avg']:,.0f} kg")
|
| 837 |
+
st.markdown('<div class="section-header">π Production Trends</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 838 |
col1, col2 = st.columns([3, 1])
|
| 839 |
with col2:
|
| 840 |
time_view = st.selectbox("Time Period", ["daily", "weekly", "monthly"], key="time_view_select")
|
|
|
|
| 844 |
total_chart = create_total_production_chart(df, time_view)
|
| 845 |
st.plotly_chart(total_chart, use_container_width=True)
|
| 846 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 847 |
+
st.markdown('<div class="section-header">π·οΈ Materials Analysis</div>', unsafe_allow_html=True)
|
|
|
|
| 848 |
col1, col2 = st.columns([3, 1])
|
| 849 |
with col2:
|
| 850 |
selected_materials = st.multiselect(
|
|
|
|
| 860 |
materials_chart = create_materials_trend_chart(df, time_view, selected_materials)
|
| 861 |
st.plotly_chart(materials_chart, use_container_width=True)
|
| 862 |
st.markdown('</div>', unsafe_allow_html=True)
|
|
|
|
| 863 |
if 'shift' in df.columns:
|
| 864 |
+
st.markdown('<div class="section-header">π Shift Analysis</div>', unsafe_allow_html=True)
|
| 865 |
with st.container():
|
| 866 |
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 867 |
shift_chart = create_shift_trend_chart(df, time_view)
|
| 868 |
st.plotly_chart(shift_chart, use_container_width=True)
|
| 869 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 870 |
+
st.markdown('<div class="section-header">β οΈ Quality Check</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
| 871 |
outliers = detect_outliers(df)
|
| 872 |
+
cols = st.columns(len(outliers))
|
| 873 |
+
for i, (material, info) in enumerate(outliers.items()):
|
| 874 |
+
with cols[i]:
|
| 875 |
+
if info['count'] > 0:
|
| 876 |
+
if len(info['dates']) <= 5:
|
| 877 |
+
dates_str = ", ".join(info['dates'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
else:
|
| 879 |
+
dates_str = f"{', '.join(info['dates'][:3])}, +{len(info['dates'])-3} more"
|
| 880 |
+
st.markdown(f'<div class="alert-warning"><strong>{material.title()}</strong><br>{info["count"]} outliers detected<br>Normal range: {info["range"]}<br><small>Dates: {dates_str}</small></div>', unsafe_allow_html=True)
|
| 881 |
+
else:
|
| 882 |
+
st.markdown(f'<div class="alert-success"><strong>{material.title()}</strong><br>All values normal</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
add_export_section(df, stats, outliers, model)
|
|
|
|
| 884 |
if model:
|
| 885 |
+
st.markdown('<div class="section-header">π€ AI Insights</div>', unsafe_allow_html=True)
|
| 886 |
quick_questions = [
|
| 887 |
"How does production distribution on weekdays compare to weekends?",
|
| 888 |
"Which material exhibits the most volatility in our dataset?",
|
|
|
|
| 895 |
with st.spinner("Analyzing..."):
|
| 896 |
answer = query_ai(model, stats, q, df)
|
| 897 |
st.info(answer)
|
|
|
|
| 898 |
custom_question = st.text_input("Ask about your production data:",
|
| 899 |
placeholder="e.g., 'Compare steel vs aluminum last month'",
|
| 900 |
key="custom_ai_question")
|
|
|
|
| 903 |
answer = query_ai(model, stats, custom_question, df)
|
| 904 |
st.success(f"**Q:** {custom_question}")
|
| 905 |
st.write(f"**A:** {answer}")
|
| 906 |
+
else:
|
| 907 |
+
st.markdown('<div class="section-header">π€ AI Configuration</div>', unsafe_allow_html=True)
|
| 908 |
+
st.info("""
|
| 909 |
+
**AI Assistant is currently unavailable.**
|
| 910 |
+
|
| 911 |
+
To enable AI features, you need to configure your Google AI API key:
|
| 912 |
+
|
| 913 |
+
**Option 1: Environment Variable**
|
| 914 |
+
```bash
|
| 915 |
+
export GOOGLE_API_KEY="your_api_key_here"
|
| 916 |
+
```
|
| 917 |
+
|
| 918 |
+
**Option 2: Streamlit Secrets**
|
| 919 |
+
Create `.streamlit/secrets.toml`:
|
| 920 |
+
```toml
|
| 921 |
+
GOOGLE_API_KEY = "your_api_key_here"
|
| 922 |
+
```
|
| 923 |
+
|
| 924 |
+
**Option 3: Azure App Service**
|
| 925 |
+
Set environment variable in Azure portal under Configuration > Application settings.
|
| 926 |
+
""")
|
| 927 |
else:
|
| 928 |
+
st.markdown('<div class="section-header">π How to Use This Platform</div>', unsafe_allow_html=True)
|
| 929 |
col1, col2 = st.columns(2)
|
| 930 |
with col1:
|
| 931 |
st.markdown("""
|
| 932 |
+
### π Quick Start
|
| 933 |
1. Upload your TSV data in the sidebar
|
| 934 |
2. Or click Quick Load buttons for preset data
|
| 935 |
3. View production by material type
|
|
|
|
| 940 |
""")
|
| 941 |
with col2:
|
| 942 |
st.markdown("""
|
| 943 |
+
### π Key Features
|
| 944 |
- Real-time interactive charts
|
| 945 |
- One-click preset data loading
|
| 946 |
- Time-period comparisons
|
|
|
|
| 949 |
- AI-powered PDF reports
|
| 950 |
- Intelligent recommendations
|
| 951 |
""")
|
| 952 |
+
st.info("π Ready to start? Upload your production data or use Quick Load buttons to begin analysis!")
|
| 953 |
|
| 954 |
if __name__ == "__main__":
|
| 955 |
main()
|