entropy25 commited on
Commit
cbe0af5
·
verified ·
1 Parent(s): a06917d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +606 -419
app.py CHANGED
@@ -17,24 +17,52 @@ import tempfile
17
  import os
18
  import requests
19
 
20
- DESIGN_SYSTEM = {
21
- 'colors': {
22
- 'primary': '#1E40AF',
23
- 'secondary': '#059669',
24
- 'accent': '#DC2626',
25
- 'warning': '#D97706',
26
- 'success': '#10B981',
27
- 'background': '#F8FAFC',
28
- 'text': '#1F2937',
29
- 'border': '#E5E7EB'
30
- },
31
- 'fonts': {
32
- 'title': 'font-family: "Inter", sans-serif; font-weight: 700;',
33
- 'subtitle': 'font-family: "Inter", sans-serif; font-weight: 600;',
34
- 'body': 'font-family: "Inter", sans-serif; font-weight: 400;'
 
 
 
 
 
 
 
 
35
  }
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
 
38
  st.set_page_config(
39
  page_title="Production Monitor with AI Insights | Nilsen Service & Consulting",
40
  page_icon="🏭",
@@ -43,68 +71,61 @@ st.set_page_config(
43
  )
44
 
45
  def load_css():
46
- st.markdown(f"""
 
 
 
47
  <style>
48
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
 
49
  .main-header {{
50
- background: linear-gradient(135deg, {DESIGN_SYSTEM['colors']['primary']} 0%, {DESIGN_SYSTEM['colors']['secondary']} 100%);
51
  padding: 1.5rem 2rem;
52
  border-radius: 12px;
53
  margin-bottom: 2rem;
54
  color: white;
55
  text-align: center;
56
  }}
57
- .main-title {{
58
- {DESIGN_SYSTEM['fonts']['title']}
59
- font-size: 2.2rem;
60
- margin: 0;
61
- text-shadow: 0 2px 4px rgba(0,0,0,0.1);
62
- }}
63
- .main-subtitle {{
64
- {DESIGN_SYSTEM['fonts']['body']}
65
- font-size: 1rem;
66
- opacity: 0.9;
67
- margin-top: 0.5rem;
68
- }}
69
- .metric-card {{
70
  background: white;
71
- border: 1px solid {DESIGN_SYSTEM['colors']['border']};
72
  border-radius: 12px;
73
  padding: 1.5rem;
74
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
75
  transition: transform 0.2s ease;
 
76
  }}
 
77
  .section-header {{
78
- {DESIGN_SYSTEM['fonts']['subtitle']}
79
- color: {DESIGN_SYSTEM['colors']['text']};
80
  font-size: 1.4rem;
81
  margin: 2rem 0 1rem 0;
82
  padding-bottom: 0.5rem;
83
- border-bottom: 2px solid {DESIGN_SYSTEM['colors']['primary']};
84
- }}
85
- .chart-container {{
86
- background: white;
87
- border-radius: 12px;
88
- padding: 1rem;
89
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
90
- margin-bottom: 1rem;
91
  }}
92
- .alert-success {{
93
- background: linear-gradient(135deg, {DESIGN_SYSTEM['colors']['success']}15, {DESIGN_SYSTEM['colors']['success']}25);
94
- border: 1px solid {DESIGN_SYSTEM['colors']['success']};
95
  border-radius: 8px;
96
  padding: 1rem;
97
- color: {DESIGN_SYSTEM['colors']['success']};
98
  }}
99
- .alert-warning {{
100
- background: linear-gradient(135deg, {DESIGN_SYSTEM['colors']['warning']}15, {DESIGN_SYSTEM['colors']['warning']}25);
101
- border: 1px solid {DESIGN_SYSTEM['colors']['warning']};
102
- border-radius: 8px;
103
- padding: 1rem;
104
- color: {DESIGN_SYSTEM['colors']['warning']};
105
  }}
 
 
 
 
 
 
106
  .stButton > button {{
107
- background: {DESIGN_SYSTEM['colors']['primary']};
108
  color: white;
109
  border: none;
110
  border-radius: 8px;
@@ -113,8 +134,10 @@ def load_css():
113
  transition: all 0.2s ease;
114
  }}
115
  </style>
116
- """, unsafe_allow_html=True)
 
117
 
 
118
  @st.cache_resource
119
  def init_ai():
120
  api_key = st.secrets.get("GOOGLE_API_KEY", "")
@@ -123,44 +146,22 @@ def init_ai():
123
  return genai.GenerativeModel('gemini-1.5-flash')
124
  return None
125
 
126
- @st.cache_data
127
- def load_preset_data(year):
128
- urls = {
129
- "2024": "https://huggingface.co/spaces/entropy25/production-data-analysis/resolve/main/2024.csv",
130
- "2025": "https://huggingface.co/spaces/entropy25/production-data-analysis/resolve/main/2025.csv"
131
- }
132
- try:
133
- if year in urls:
134
- response = requests.get(urls[year], timeout=10)
135
- response.raise_for_status()
136
- df = pd.read_csv(io.StringIO(response.text), sep='\t')
137
- df['date'] = pd.to_datetime(df['date'], format='%m/%d/%Y')
138
- df['day_name'] = df['date'].dt.day_name()
139
- return df
140
- else:
141
- return generate_sample_data(year)
142
- except Exception as e:
143
- st.warning(f"Could not load remote {year} data: {str(e)}. Loading sample data instead.")
144
- return generate_sample_data(year)
145
-
146
  def generate_sample_data(year):
147
  np.random.seed(42 if year == "2024" else 84)
148
  start_date = f"01/01/{year}"
149
  end_date = f"12/31/{year}"
150
  dates = pd.date_range(start=start_date, end=end_date, freq='D')
151
  weekdays = dates[dates.weekday < 5]
 
152
  data = []
153
  materials = ['steel', 'aluminum', 'plastic', 'copper']
154
  shifts = ['day', 'night']
 
 
155
  for date in weekdays:
156
  for material in materials:
157
  for shift in shifts:
158
- base_weight = {
159
- 'steel': 1500,
160
- 'aluminum': 800,
161
- 'plastic': 600,
162
- 'copper': 400
163
- }[material]
164
  weight = base_weight + np.random.normal(0, base_weight * 0.2)
165
  weight = max(weight, base_weight * 0.3)
166
  data.append({
@@ -169,14 +170,29 @@ def generate_sample_data(year):
169
  'material_type': material,
170
  'shift': shift
171
  })
172
- df = pd.DataFrame(data)
173
- df['date'] = pd.to_datetime(df['date'], format='%m/%d/%Y')
174
- df['day_name'] = df['date'].dt.day_name()
175
- return df
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
  @st.cache_data
178
  def load_data(file):
179
  df = pd.read_csv(file, sep='\t')
 
 
 
180
  df['date'] = pd.to_datetime(df['date'], format='%m/%d/%Y')
181
  df['day_name'] = df['date'].dt.day_name()
182
  return df
@@ -185,6 +201,7 @@ def get_material_stats(df):
185
  stats = {}
186
  total = df['weight_kg'].sum()
187
  total_work_days = df['date'].nunique()
 
188
  for material in df['material_type'].unique():
189
  data = df[df['material_type'] == material]
190
  work_days = data['date'].nunique()
@@ -196,6 +213,7 @@ def get_material_stats(df):
196
  'work_days': work_days,
197
  'records': len(data)
198
  }
 
199
  stats['_total_'] = {
200
  'total': total,
201
  'percentage': 100.0,
@@ -205,69 +223,101 @@ def get_material_stats(df):
205
  }
206
  return stats
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  def get_chart_theme():
 
209
  return {
210
  'layout': {
211
  'plot_bgcolor': 'white',
212
  'paper_bgcolor': 'white',
213
- 'font': {'family': 'Inter, sans-serif', 'color': DESIGN_SYSTEM['colors']['text']},
214
- 'colorway': [DESIGN_SYSTEM['colors']['primary'], DESIGN_SYSTEM['colors']['secondary'],
215
- DESIGN_SYSTEM['colors']['accent'], DESIGN_SYSTEM['colors']['warning']],
216
- 'margin': {'t': 60, 'b': 40, 'l': 40, 'r': 40}
217
  }
218
  }
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  def create_total_production_chart(df, time_period='daily'):
221
  if time_period == 'daily':
222
  grouped = df.groupby('date')['weight_kg'].sum().reset_index()
223
  fig = px.line(grouped, x='date', y='weight_kg',
224
  title='Total Production Trend',
225
  labels={'weight_kg': 'Weight (kg)', 'date': 'Date'})
226
- elif time_period == 'weekly':
227
- df_copy = df.copy()
228
- df_copy['week'] = df_copy['date'].dt.isocalendar().week
229
- df_copy['year'] = df_copy['date'].dt.year
230
- grouped = df_copy.groupby(['year', 'week'])['weight_kg'].sum().reset_index()
231
- grouped['week_label'] = grouped['year'].astype(str) + '-W' + grouped['week'].astype(str)
232
- fig = px.bar(grouped, x='week_label', y='weight_kg',
233
- title='Total Production Trend (Weekly)',
234
- labels={'weight_kg': 'Weight (kg)', 'week_label': 'Week'})
235
  else:
236
- df_copy = df.copy()
237
- df_copy['month'] = df_copy['date'].dt.to_period('M')
238
- grouped = df_copy.groupby('month')['weight_kg'].sum().reset_index()
239
- grouped['month'] = grouped['month'].astype(str)
240
- fig = px.bar(grouped, x='month', y='weight_kg',
241
- title='Total Production Trend (Monthly)',
242
- labels={'weight_kg': 'Weight (kg)', 'month': 'Month'})
243
- fig.update_layout(**get_chart_theme()['layout'], height=400, showlegend=False)
 
 
 
 
 
 
 
244
  return fig
245
 
246
  def create_materials_trend_chart(df, time_period='daily', selected_materials=None):
247
  df_copy = df.copy()
248
  if selected_materials:
249
  df_copy = df_copy[df_copy['material_type'].isin(selected_materials)]
 
250
  if time_period == 'daily':
251
  grouped = df_copy.groupby(['date', 'material_type'])['weight_kg'].sum().reset_index()
252
  fig = px.line(grouped, x='date', y='weight_kg', color='material_type',
253
  title='Materials Production Trends',
254
  labels={'weight_kg': 'Weight (kg)', 'date': 'Date', 'material_type': 'Material'})
255
- elif time_period == 'weekly':
256
- df_copy['week'] = df_copy['date'].dt.isocalendar().week
257
- df_copy['year'] = df_copy['date'].dt.year
258
- grouped = df_copy.groupby(['year', 'week', 'material_type'])['weight_kg'].sum().reset_index()
259
- grouped['week_label'] = grouped['year'].astype(str) + '-W' + grouped['week'].astype(str)
260
- fig = px.bar(grouped, x='week_label', y='weight_kg', color='material_type',
261
- title='Materials Production Trends (Weekly)',
262
- labels={'weight_kg': 'Weight (kg)', 'week_label': 'Week', 'material_type': 'Material'})
263
  else:
264
- df_copy['month'] = df_copy['date'].dt.to_period('M')
265
- grouped = df_copy.groupby(['month', 'material_type'])['weight_kg'].sum().reset_index()
266
- grouped['month'] = grouped['month'].astype(str)
267
- fig = px.bar(grouped, x='month', y='weight_kg', color='material_type',
268
- title='Materials Production Trends (Monthly)',
269
- labels={'weight_kg': 'Weight (kg)', 'month': 'Month', 'material_type': 'Material'})
270
- fig.update_layout(**get_chart_theme()['layout'], height=400)
 
 
 
 
 
 
 
 
271
  return fig
272
 
273
  def create_shift_trend_chart(df, time_period='daily'):
@@ -275,53 +325,40 @@ def create_shift_trend_chart(df, time_period='daily'):
275
  grouped = df.groupby(['date', 'shift'])['weight_kg'].sum().reset_index()
276
  pivot_data = grouped.pivot(index='date', columns='shift', values='weight_kg').fillna(0)
277
  fig = go.Figure()
 
278
  if 'day' in pivot_data.columns:
279
  fig.add_trace(go.Bar(
280
  x=pivot_data.index, y=pivot_data['day'], name='Day Shift',
281
- marker_color=DESIGN_SYSTEM['colors']['warning'],
282
  text=pivot_data['day'].round(0), textposition='inside'
283
  ))
284
  if 'night' in pivot_data.columns:
285
  fig.add_trace(go.Bar(
286
  x=pivot_data.index, y=pivot_data['night'], name='Night Shift',
287
- marker_color=DESIGN_SYSTEM['colors']['primary'],
288
  base=pivot_data['day'] if 'day' in pivot_data.columns else 0,
289
  text=pivot_data['night'].round(0), textposition='inside'
290
  ))
 
291
  fig.update_layout(
292
- **get_chart_theme()['layout'],
293
  title='Daily Shift Production Trends (Stacked)',
294
  xaxis_title='Date', yaxis_title='Weight (kg)',
295
- barmode='stack', height=400, showlegend=True
296
  )
297
  else:
298
  grouped = df.groupby(['date', 'shift'])['weight_kg'].sum().reset_index()
299
  fig = px.bar(grouped, x='date', y='weight_kg', color='shift',
300
  title=f'{time_period.title()} Shift Production Trends',
301
  barmode='stack')
302
- fig.update_layout(**get_chart_theme()['layout'], height=400)
 
303
  return fig
304
 
305
- def detect_outliers(df):
306
- outliers = {}
307
- for material in df['material_type'].unique():
308
- material_data = df[df['material_type'] == material]
309
- data = material_data['weight_kg']
310
- Q1, Q3 = data.quantile(0.25), data.quantile(0.75)
311
- IQR = Q3 - Q1
312
- lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
313
- outlier_mask = (data < lower) | (data > upper)
314
- outlier_dates = material_data[outlier_mask]['date'].dt.strftime('%Y-%m-%d').tolist()
315
- outliers[material] = {
316
- 'count': len(outlier_dates),
317
- 'range': f"{lower:.0f} - {upper:.0f} kg",
318
- 'dates': outlier_dates
319
- }
320
- return outliers
321
-
322
  def generate_ai_summary(model, df, stats, outliers):
323
  if not model:
324
  return "AI analysis unavailable - API key not configured"
 
325
  try:
326
  materials = [k for k in stats.keys() if k != '_total_']
327
  context_parts = [
@@ -334,20 +371,24 @@ def generate_ai_summary(model, df, stats, outliers):
334
  "",
335
  "## Material Breakdown:"
336
  ]
 
337
  for material in materials:
338
  info = stats[material]
339
- context_parts.append(f"- {material.title()}: {info['total']:,.0f} kg ({info['percentage']:.1f}%), avg {info['daily_avg']:,.0f} kg/day")
 
340
  daily_data = df.groupby('date')['weight_kg'].sum()
341
  trend_direction = "increasing" if daily_data.iloc[-1] > daily_data.iloc[0] else "decreasing"
342
  volatility = daily_data.std() / daily_data.mean() * 100
 
343
  context_parts.extend([
344
  "",
345
  "## Trend Analysis:",
346
  f"- Overall trend: {trend_direction}",
347
  f"- Production volatility: {volatility:.1f}% coefficient of variation",
348
- f"- Peak production: {daily_data.max():,.0f} kg",
349
- f"- Lowest production: {daily_data.min():,.0f} kg"
350
  ])
 
351
  total_outliers = sum(info['count'] for info in outliers.values())
352
  context_parts.extend([
353
  "",
@@ -355,15 +396,18 @@ def generate_ai_summary(model, df, stats, outliers):
355
  f"- Total outliers detected: {total_outliers}",
356
  f"- Materials with quality issues: {sum(1 for info in outliers.values() if info['count'] > 0)}"
357
  ])
 
358
  if 'shift' in df.columns:
359
  shift_stats = df.groupby('shift')['weight_kg'].sum()
360
  context_parts.extend([
361
  "",
362
  "## Shift Performance:",
363
- f"- Day shift: {shift_stats.get('day', 0):,.0f} kg",
364
- f"- Night shift: {shift_stats.get('night', 0):,.0f} kg"
365
  ])
 
366
  context_text = "\n".join(context_parts)
 
367
  prompt = f"""
368
  {context_text}
369
 
@@ -388,6 +432,7 @@ Example Recommendation format:
388
 
389
  Keep the entire analysis concise and under 300 words.
390
  """
 
391
  response = model.generate_content(prompt)
392
  return response.text
393
  except Exception as e:
@@ -396,28 +441,35 @@ Keep the entire analysis concise and under 300 words.
396
  def query_ai(model, stats, question, df=None):
397
  if not model:
398
  return "AI assistant not available"
 
399
  context_parts = [
400
  "Production Data Summary:",
401
- *[f"- {mat.title()}: {info['total']:,.0f}kg ({info['percentage']:.1f}%)"
402
  for mat, info in stats.items() if mat != '_total_'],
403
- f"\nTotal Production: {stats['_total_']['total']:,.0f}kg across {stats['_total_']['work_days']} work days"
404
  ]
 
405
  if df is not None:
406
  available_cols = list(df.columns)
407
  context_parts.append(f"\nAvailable data fields: {', '.join(available_cols)}")
 
408
  if 'shift' in df.columns:
409
  shift_stats = df.groupby('shift')['weight_kg'].sum()
410
  context_parts.append(f"Shift breakdown: {dict(shift_stats)}")
 
411
  if 'day_name' in df.columns:
412
  day_stats = df.groupby('day_name')['weight_kg'].mean()
413
  context_parts.append(f"Average daily production: {dict(day_stats.round(0))}")
 
414
  context = "\n".join(context_parts) + f"\n\nQuestion: {question}\nAnswer based on available data:"
 
415
  try:
416
  response = model.generate_content(context)
417
  return response.text
418
  except:
419
  return "Error getting AI response"
420
 
 
421
  def save_plotly_as_image(fig, filename):
422
  try:
423
  temp_dir = tempfile.gettempdir()
@@ -430,6 +482,7 @@ def save_plotly_as_image(fig, filename):
430
  'margin': dict(t=50, b=40, l=40, r=40)
431
  })
432
  fig.update_layout(**theme)
 
433
  try:
434
  pio.write_image(fig, filepath, format='png', width=800, height=400, scale=2, engine='kaleido')
435
  if os.path.exists(filepath):
@@ -442,51 +495,77 @@ def save_plotly_as_image(fig, filename):
442
 
443
  def create_pdf_charts(df, stats):
444
  charts = {}
 
 
445
  try:
446
  materials = [k for k in stats.keys() if k != '_total_']
447
  values = [stats[mat]['total'] for mat in materials]
448
- labels = [mat.replace('_', ' ').title() for mat in materials]
 
449
  if len(materials) > 0 and len(values) > 0:
 
450
  try:
451
  fig_pie = px.pie(values=values, names=labels, title="Production Distribution by Material")
452
  charts['pie'] = save_plotly_as_image(fig_pie, "distribution.png")
453
  except:
454
  pass
455
- if len(df) > 0:
456
- try:
457
- daily_data = df.groupby('date')['weight_kg'].sum().reset_index()
458
- if len(daily_data) > 0:
459
- fig_trend = px.line(daily_data, x='date', y='weight_kg', title="Daily Production Trend",
460
- labels={'date': 'Date', 'weight_kg': 'Weight (kg)'},
461
- color_discrete_sequence=[DESIGN_SYSTEM['colors']['primary']])
462
- charts['trend'] = save_plotly_as_image(fig_trend, "trend.png")
463
- except:
464
- pass
465
- if len(materials) > 0 and len(values) > 0:
466
  try:
467
  fig_bar = px.bar(x=labels, y=values, title="Production by Material Type",
468
  labels={'x': 'Material Type', 'y': 'Weight (kg)'},
469
- color_discrete_sequence=[DESIGN_SYSTEM['colors']['primary']])
470
  charts['bar'] = save_plotly_as_image(fig_bar, "materials.png")
471
  except:
472
  pass
473
- if 'shift' in df.columns and len(df) > 0:
 
 
474
  try:
475
- shift_data = df.groupby('shift')['weight_kg'].sum().reset_index()
476
- if len(shift_data) > 0 and shift_data['weight_kg'].sum() > 0:
477
- fig_shift = px.pie(shift_data, values='weight_kg', names='shift', title="Production by Shift")
478
- charts['shift'] = save_plotly_as_image(fig_shift, "shifts.png")
 
 
479
  except:
480
  pass
 
 
 
 
 
 
 
 
 
 
481
  except Exception as e:
482
  pass
 
483
  return charts
484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  def create_enhanced_pdf_report(df, stats, outliers, model=None):
486
  buffer = io.BytesIO()
487
  doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50)
488
  elements = []
489
  styles = getSampleStyleSheet()
 
 
490
  title_style = ParagraphStyle(
491
  'CustomTitle',
492
  parent=styles['Heading1'],
@@ -510,10 +589,15 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
510
  leftIndent=20,
511
  textColor=colors.darkgreen
512
  )
513
- elements.append(Spacer(1, 100))
514
- elements.append(Paragraph("Production Monitor with AI Insights", title_style))
515
- elements.append(Paragraph("Comprehensive Production Analysis Report", styles['Heading3']))
516
- elements.append(Spacer(1, 50))
 
 
 
 
 
517
  report_info = f"""
518
  <para alignment="center">
519
  <b>Nilsen Service &amp; Consulting AS</b><br/>
@@ -523,53 +607,59 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
523
  <b>Total Records:</b> {len(df):,}
524
  </para>
525
  """
526
- elements.append(Paragraph(report_info, styles['Normal']))
527
- elements.append(PageBreak())
 
 
 
 
528
  elements.append(Paragraph("Executive Summary", subtitle_style))
529
  total_production = stats['_total_']['total']
530
  work_days = stats['_total_']['work_days']
531
  daily_avg = stats['_total_']['daily_avg']
 
532
  exec_summary = f"""
533
  <para>
534
  This report analyzes production data spanning <b>{work_days} working days</b>.
535
- Total output achieved: <b>{total_production:,.0f} kg</b> with an average
536
- daily production of <b>{daily_avg:,.0f} kg</b>.
537
  <br/><br/>
538
  <b>Key Highlights:</b><br/>
539
- • Total production: {total_production:,.0f} kg<br/>
540
- • Daily average: {daily_avg:,.0f} kg<br/>
541
  • Materials tracked: {len([k for k in stats.keys() if k != '_total_'])}<br/>
542
  • Data quality: {len(df):,} records processed
543
  </para>
544
  """
545
- elements.append(Paragraph(exec_summary, styles['Normal']))
546
- elements.append(Spacer(1, 20))
547
- elements.append(Paragraph("Production Summary", styles['Heading3']))
548
- summary_data = [['Material Type', 'Total (kg)', 'Share (%)', 'Daily Avg (kg)']]
 
 
 
 
549
  for material, info in stats.items():
550
  if material != '_total_':
551
  summary_data.append([
552
- material.replace('_', ' ').title(),
553
- f"{info['total']:,.0f}",
554
- f"{info['percentage']:.1f}%",
555
- f"{info['daily_avg']:,.0f}"
556
  ])
557
- summary_table = Table(summary_data, colWidths=[2*inch, 1.5*inch, 1*inch, 1.5*inch])
558
- summary_table.setStyle(TableStyle([
559
- ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
560
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
561
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
562
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
563
- ('GRID', (0, 0), (-1, -1), 1, colors.black),
564
- ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
565
- ]))
566
- elements.append(summary_table)
567
- elements.append(PageBreak())
568
  elements.append(Paragraph("Production Analysis Charts", subtitle_style))
569
- try:
570
- charts = create_pdf_charts(df, stats)
571
- except:
572
- charts = {}
573
  charts_added = False
574
  chart_insights = {
575
  'pie': "Material distribution shows production allocation across different materials. Balanced distribution indicates diversified production capabilities.",
@@ -577,6 +667,7 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
577
  'bar': "Material comparison highlights performance differences and production capacities. Top performers indicate optimization opportunities.",
578
  'shift': "Shift analysis reveals operational efficiency differences between day and night operations. Balance indicates effective resource utilization."
579
  }
 
580
  for chart_type, chart_title in [
581
  ('pie', "Production Distribution"),
582
  ('trend', "Production Trend"),
@@ -586,67 +677,79 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
586
  chart_path = charts.get(chart_type)
587
  if chart_path and os.path.exists(chart_path):
588
  try:
589
- elements.append(Paragraph(chart_title, styles['Heading3']))
590
- elements.append(Image(chart_path, width=6*inch, height=3*inch))
591
- insight_text = f"<i>Analysis: {chart_insights.get(chart_type, 'Chart analysis not available.')}</i>"
592
- elements.append(Paragraph(insight_text, ai_style))
593
- elements.append(Spacer(1, 20))
 
594
  charts_added = True
595
  except Exception as e:
596
  pass
 
597
  if not charts_added:
598
- elements.append(Paragraph("Charts Generation Failed", styles['Heading3']))
599
- elements.append(Paragraph("Production Data Summary:", styles['Normal']))
 
 
600
  for material, info in stats.items():
601
  if material != '_total_':
602
- summary_text = f"• {material.replace('_', ' ').title()}: {info['total']:,.0f} kg ({info['percentage']:.1f}%)"
603
  elements.append(Paragraph(summary_text, styles['Normal']))
604
  elements.append(Spacer(1, 20))
 
605
  elements.append(PageBreak())
 
 
606
  elements.append(Paragraph("Quality Control Analysis", subtitle_style))
607
- quality_data = [['Material', 'Outliers', 'Normal Range (kg)', 'Status']]
 
608
  for material, info in outliers.items():
609
- if info['count'] == 0:
610
- status = "GOOD"
611
- elif info['count'] <= 3:
612
- status = "MONITOR"
613
- else:
614
- status = "ATTENTION"
615
  quality_data.append([
616
- material.replace('_', ' ').title(),
617
  str(info['count']),
618
  info['range'],
619
  status
620
  ])
621
- quality_table = Table(quality_data, colWidths=[2*inch, 1*inch, 2*inch, 1.5*inch])
622
- quality_table.setStyle(TableStyle([
623
- ('BACKGROUND', (0, 0), (-1, 0), colors.darkred),
624
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
625
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
626
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
627
- ('GRID', (0, 0), (-1, -1), 1, colors.black),
628
- ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
629
- ]))
630
  elements.append(quality_table)
 
 
631
  if model:
632
- elements.append(PageBreak())
633
- elements.append(Paragraph("AI Intelligent Analysis", subtitle_style))
 
 
634
  try:
635
  ai_analysis = generate_ai_summary(model, df, stats, outliers)
636
  except:
637
  ai_analysis = "AI analysis temporarily unavailable."
 
638
  ai_paragraphs = ai_analysis.split('\n\n')
639
  for paragraph in ai_paragraphs:
640
  if paragraph.strip():
641
  formatted_text = paragraph.replace('**', '<b>', 1).replace('**', '</b>', 1) \
642
  .replace('•', ' •') \
643
  .replace('\n', '<br/>')
644
- elements.append(Paragraph(formatted_text, styles['Normal']))
645
- elements.append(Spacer(1, 8))
 
 
646
  else:
647
- elements.append(PageBreak())
648
- elements.append(Paragraph("AI Analysis", subtitle_style))
649
- elements.append(Paragraph("AI analysis unavailable - API key not configured. Please configure Google AI API key to enable intelligent insights.", styles['Normal']))
 
 
 
 
650
  elements.append(Spacer(1, 30))
651
  footer_text = f"""
652
  <para alignment="center">
@@ -656,6 +759,7 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
656
  </para>
657
  """
658
  elements.append(Paragraph(footer_text, styles['Normal']))
 
659
  doc.build(elements)
660
  buffer.seek(0)
661
  return buffer
@@ -663,7 +767,7 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
663
  def create_csv_export(df, stats):
664
  summary_df = pd.DataFrame([
665
  {
666
- 'Material': material.replace('_', ' ').title(),
667
  'Total_kg': info['total'],
668
  'Percentage': info['percentage'],
669
  'Daily_Average_kg': info['daily_avg'],
@@ -674,25 +778,165 @@ def create_csv_export(df, stats):
674
  ])
675
  return summary_df
676
 
677
- def add_export_section(df, stats, outliers, model):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  st.markdown('<div class="section-header">📄 Export Reports</div>', unsafe_allow_html=True)
679
- if 'export_ready' not in st.session_state:
680
- st.session_state.export_ready = False
681
- if 'pdf_buffer' not in st.session_state:
682
- st.session_state.pdf_buffer = None
683
- if 'csv_data' not in st.session_state:
684
- st.session_state.csv_data = None
685
  col1, col2, col3 = st.columns(3)
 
686
  with col1:
687
  if st.button("📊 Generate PDF Report with AI", key="generate_pdf_btn", type="primary"):
688
- try:
689
- with st.spinner("Generating PDF with AI analysis..."):
690
- st.session_state.pdf_buffer = create_enhanced_pdf_report(df, stats, outliers, model)
691
- st.session_state.export_ready = True
 
 
 
692
  st.success("✅ PDF report with AI analysis generated successfully!")
693
- except Exception as e:
694
- st.error(f"❌ PDF generation failed: {str(e)}")
695
  st.session_state.export_ready = False
 
696
  if st.session_state.export_ready and st.session_state.pdf_buffer:
697
  st.download_button(
698
  label="💾 Download PDF Report",
@@ -701,13 +945,14 @@ def add_export_section(df, stats, outliers, model):
701
  mime="application/pdf",
702
  key="download_pdf_btn"
703
  )
 
704
  with col2:
705
  if st.button("📈 Generate CSV Summary", key="generate_csv_btn"):
706
- try:
707
- st.session_state.csv_data = create_csv_export(df, stats)
 
708
  st.success("✅ CSV summary generated successfully!")
709
- except Exception as e:
710
- st.error(f"❌ CSV generation failed: {str(e)}")
711
  if st.session_state.csv_data is not None:
712
  csv_string = st.session_state.csv_data.to_csv(index=False)
713
  st.download_button(
@@ -717,6 +962,7 @@ def add_export_section(df, stats, outliers, model):
717
  mime="text/csv",
718
  key="download_csv_btn"
719
  )
 
720
  with col3:
721
  csv_string = df.to_csv(index=False)
722
  st.download_button(
@@ -727,185 +973,126 @@ def add_export_section(df, stats, outliers, model):
727
  key="download_raw_btn"
728
  )
729
 
730
- def main():
731
- load_css()
732
- st.markdown("""
733
- <div class="main-header">
734
- <div class="main-title">🏭 Production Monitor with AI Insights</div>
735
- <div class="main-subtitle">Nilsen Service & Consulting AS | Real-time Production Analytics & Recommendations</div>
736
- </div>
737
- """, unsafe_allow_html=True)
738
- model = init_ai()
739
- if 'current_df' not in st.session_state:
740
- st.session_state.current_df = None
741
- if 'current_stats' not in st.session_state:
742
- st.session_state.current_stats = None
743
- with st.sidebar:
744
- st.markdown("### 📊 Data Source")
745
- uploaded_file = st.file_uploader("Upload Production Data", type=['csv'])
746
- st.markdown("---")
747
- st.markdown("### 📊 Quick Load")
748
- col1, col2 = st.columns(2)
749
- with col1:
750
- if st.button("📊 2024 Data", type="primary", key="load_2024"):
751
- st.session_state.load_preset = "2024"
752
- with col2:
753
- if st.button("📊 2025 Data", type="primary", key="load_2025"):
754
- st.session_state.load_preset = "2025"
755
- st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
756
  st.markdown("""
757
- **Expected TSV format:**
758
- - `date`: MM/DD/YYYY
759
- - `weight_kg`: Production weight
760
- - `material_type`: Material category
761
- - `shift`: day/night (optional)
 
 
 
762
  """)
763
- if model:
764
- st.success("🤖 AI Assistant Ready")
765
- else:
766
- st.warning("⚠️ AI Assistant Unavailable")
767
- df = st.session_state.current_df
768
- stats = st.session_state.current_stats
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769
  if uploaded_file:
770
- try:
771
- df = load_data(uploaded_file)
772
- stats = get_material_stats(df)
773
- st.session_state.current_df = df
774
  st.session_state.current_stats = stats
775
  st.success("✅ Data uploaded successfully!")
776
- except Exception as e:
777
- st.error(f"❌ Error loading uploaded file: {str(e)}")
778
- elif 'load_preset' in st.session_state:
779
- year = st.session_state.load_preset
780
- try:
781
  with st.spinner(f"Loading {year} data..."):
782
- df = load_preset_data(year)
783
- if df is not None:
784
- stats = get_material_stats(df)
785
- st.session_state.current_df = df
786
  st.session_state.current_stats = stats
787
  st.success(f"✅ {year} data loaded successfully!")
788
- except Exception as e:
789
- st.error(f"❌ Error loading {year} data: {str(e)}")
790
- finally:
791
- del st.session_state.load_preset
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  if df is not None and stats is not None:
793
- st.markdown('<div class="section-header">📋 Material Overview</div>', unsafe_allow_html=True)
794
- materials = [k for k in stats.keys() if k != '_total_']
795
- cols = st.columns(4)
796
- for i, material in enumerate(materials[:3]):
797
- info = stats[material]
798
- with cols[i]:
799
- st.metric(
800
- label=material.replace('_', ' ').title(),
801
- value=f"{info['total']:,.0f} kg",
802
- delta=f"{info['percentage']:.1f}% of total"
803
- )
804
- st.caption(f"Daily avg: {info['daily_avg']:,.0f} kg")
805
- if len(materials) >= 3:
806
- total_info = stats['_total_']
807
- with cols[3]:
808
- st.metric(
809
- label="Total Production",
810
- value=f"{total_info['total']:,.0f} kg",
811
- delta="100% of total"
812
- )
813
- st.caption(f"Daily avg: {total_info['daily_avg']:,.0f} kg")
814
- st.markdown('<div class="section-header">📊 Production Trends</div>', unsafe_allow_html=True)
815
- col1, col2 = st.columns([3, 1])
816
- with col2:
817
- time_view = st.selectbox("Time Period", ["daily", "weekly", "monthly"], key="time_view_select")
818
- with col1:
819
- with st.container():
820
- st.markdown('<div class="chart-container">', unsafe_allow_html=True)
821
- total_chart = create_total_production_chart(df, time_view)
822
- st.plotly_chart(total_chart, use_container_width=True)
823
- st.markdown('</div>', unsafe_allow_html=True)
824
- st.markdown('<div class="section-header">🏷️ Materials Analysis</div>', unsafe_allow_html=True)
825
- col1, col2 = st.columns([3, 1])
826
- with col2:
827
- selected_materials = st.multiselect(
828
- "Select Materials",
829
- options=materials,
830
- default=materials,
831
- key="materials_select"
832
- )
833
- with col1:
834
- if selected_materials:
835
- with st.container():
836
- st.markdown('<div class="chart-container">', unsafe_allow_html=True)
837
- materials_chart = create_materials_trend_chart(df, time_view, selected_materials)
838
- st.plotly_chart(materials_chart, use_container_width=True)
839
- st.markdown('</div>', unsafe_allow_html=True)
840
- if 'shift' in df.columns:
841
- st.markdown('<div class="section-header">🌓 Shift Analysis</div>', unsafe_allow_html=True)
842
- with st.container():
843
- st.markdown('<div class="chart-container">', unsafe_allow_html=True)
844
- shift_chart = create_shift_trend_chart(df, time_view)
845
- st.plotly_chart(shift_chart, use_container_width=True)
846
- st.markdown('</div>', unsafe_allow_html=True)
847
- st.markdown('<div class="section-header">⚠️ Quality Check</div>', unsafe_allow_html=True)
848
- outliers = detect_outliers(df)
849
- cols = st.columns(len(outliers))
850
- for i, (material, info) in enumerate(outliers.items()):
851
- with cols[i]:
852
- if info['count'] > 0:
853
- if len(info['dates']) <= 5:
854
- dates_str = ", ".join(info['dates'])
855
- else:
856
- dates_str = f"{', '.join(info['dates'][:3])}, +{len(info['dates'])-3} more"
857
- 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)
858
- else:
859
- st.markdown(f'<div class="alert-success"><strong>{material.title()}</strong><br>All values normal</div>', unsafe_allow_html=True)
860
- add_export_section(df, stats, outliers, model)
861
- if model:
862
- st.markdown('<div class="section-header">🤖 AI Insights</div>', unsafe_allow_html=True)
863
- quick_questions = [
864
- "How does production distribution on weekdays compare to weekends?",
865
- "Which material exhibits the most volatility in our dataset?",
866
- "To improve stability, which material or shift needs immediate attention?"
867
- ]
868
- cols = st.columns(len(quick_questions))
869
- for i, q in enumerate(quick_questions):
870
- with cols[i]:
871
- if st.button(q, key=f"ai_q_{i}"):
872
- with st.spinner("Analyzing..."):
873
- answer = query_ai(model, stats, q, df)
874
- st.info(answer)
875
- custom_question = st.text_input("Ask about your production data:",
876
- placeholder="e.g., 'Compare steel vs aluminum last month'",
877
- key="custom_ai_question")
878
- if custom_question and st.button("Ask AI", key="ask_ai_btn"):
879
- with st.spinner("Analyzing..."):
880
- answer = query_ai(model, stats, custom_question, df)
881
- st.success(f"**Q:** {custom_question}")
882
- st.write(f"**A:** {answer}")
883
  else:
884
- st.markdown('<div class="section-header">📖 How to Use This Platform</div>', unsafe_allow_html=True)
885
- col1, col2 = st.columns(2)
886
- with col1:
887
- st.markdown("""
888
- ### 🚀 Quick Start
889
- 1. Upload your TSV data in the sidebar
890
- 2. Or click Quick Load buttons for preset data
891
- 3. View production by material type
892
- 4. Analyze trends (daily/weekly/monthly)
893
- 5. Check anomalies in Quality Check
894
- 6. Export reports (PDF with AI, CSV)
895
- 7. Ask the AI assistant for insights
896
- """)
897
- with col2:
898
- st.markdown("""
899
- ### 📊 Key Features
900
- - Real-time interactive charts
901
- - One-click preset data loading
902
- - Time-period comparisons
903
- - Shift performance analysis
904
- - Outlier detection with dates
905
- - AI-powered PDF reports
906
- - Intelligent recommendations
907
- """)
908
- st.info("📁 Ready to start? Upload your production data or use Quick Load buttons to begin analysis!")
909
 
910
  if __name__ == "__main__":
911
  main()
 
17
  import os
18
  import requests
19
 
20
+ # Configuration and Constants
21
+ class Config:
22
+ DESIGN_SYSTEM = {
23
+ 'colors': {
24
+ 'primary': '#1E40AF',
25
+ 'secondary': '#059669',
26
+ 'accent': '#DC2626',
27
+ 'warning': '#D97706',
28
+ 'success': '#10B981',
29
+ 'background': '#F8FAFC',
30
+ 'text': '#1F2937',
31
+ 'border': '#E5E7EB'
32
+ },
33
+ 'fonts': {
34
+ 'title': 'font-family: "Inter", sans-serif; font-weight: 700;',
35
+ 'subtitle': 'font-family: "Inter", sans-serif; font-weight: 600;',
36
+ 'body': 'font-family: "Inter", sans-serif; font-weight: 400;'
37
+ }
38
+ }
39
+
40
+ DATA_URLS = {
41
+ "2024": "https://huggingface.co/spaces/entropy25/production-data-analysis/resolve/main/2024.csv",
42
+ "2025": "https://huggingface.co/spaces/entropy25/production-data-analysis/resolve/main/2025.csv"
43
  }
44
+
45
+ CHART_HEIGHT = 400
46
+ PDF_CHART_SIZE = (6*inch, 3*inch)
47
+
48
+ # Utility Functions
49
+ def format_material_name(material):
50
+ return material.replace('_', ' ').title()
51
+
52
+ def format_weight(weight):
53
+ return f"{weight:,.0f} kg"
54
+
55
+ def format_percentage(percentage):
56
+ return f"{percentage:.1f}%"
57
+
58
+ def safe_execute(operation_name, func, *args, **kwargs):
59
+ try:
60
+ return func(*args, **kwargs)
61
+ except Exception as e:
62
+ st.error(f"❌ {operation_name} failed: {str(e)}")
63
+ return None
64
 
65
+ # Page Configuration and CSS
66
  st.set_page_config(
67
  page_title="Production Monitor with AI Insights | Nilsen Service & Consulting",
68
  page_icon="🏭",
 
71
  )
72
 
73
  def load_css():
74
+ colors = Config.DESIGN_SYSTEM['colors']
75
+ fonts = Config.DESIGN_SYSTEM['fonts']
76
+
77
+ css_styles = f"""
78
  <style>
79
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
80
+
81
  .main-header {{
82
+ background: linear-gradient(135deg, {colors['primary']} 0%, {colors['secondary']} 100%);
83
  padding: 1.5rem 2rem;
84
  border-radius: 12px;
85
  margin-bottom: 2rem;
86
  color: white;
87
  text-align: center;
88
  }}
89
+ .main-title {{ {fonts['title']} font-size: 2.2rem; margin: 0; text-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
90
+ .main-subtitle {{ {fonts['body']} font-size: 1rem; opacity: 0.9; margin-top: 0.5rem; }}
91
+
92
+ .metric-card, .chart-container {{
 
 
 
 
 
 
 
 
 
93
  background: white;
94
+ border: 1px solid {colors['border']};
95
  border-radius: 12px;
96
  padding: 1.5rem;
97
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
98
  transition: transform 0.2s ease;
99
+ margin-bottom: 1rem;
100
  }}
101
+
102
  .section-header {{
103
+ {fonts['subtitle']}
104
+ color: {colors['text']};
105
  font-size: 1.4rem;
106
  margin: 2rem 0 1rem 0;
107
  padding-bottom: 0.5rem;
108
+ border-bottom: 2px solid {colors['primary']};
 
 
 
 
 
 
 
109
  }}
110
+
111
+ .alert {{
 
112
  border-radius: 8px;
113
  padding: 1rem;
114
+ margin: 0.5rem 0;
115
  }}
116
+ .alert-success {{
117
+ background: linear-gradient(135deg, {colors['success']}15, {colors['success']}25);
118
+ border: 1px solid {colors['success']};
119
+ color: {colors['success']};
 
 
120
  }}
121
+ .alert-warning {{
122
+ background: linear-gradient(135deg, {colors['warning']}15, {colors['warning']}25);
123
+ border: 1px solid {colors['warning']};
124
+ color: {colors['warning']};
125
+ }}
126
+
127
  .stButton > button {{
128
+ background: {colors['primary']};
129
  color: white;
130
  border: none;
131
  border-radius: 8px;
 
134
  transition: all 0.2s ease;
135
  }}
136
  </style>
137
+ """
138
+ st.markdown(css_styles, unsafe_allow_html=True)
139
 
140
+ # Data Processing Functions
141
  @st.cache_resource
142
  def init_ai():
143
  api_key = st.secrets.get("GOOGLE_API_KEY", "")
 
146
  return genai.GenerativeModel('gemini-1.5-flash')
147
  return None
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  def generate_sample_data(year):
150
  np.random.seed(42 if year == "2024" else 84)
151
  start_date = f"01/01/{year}"
152
  end_date = f"12/31/{year}"
153
  dates = pd.date_range(start=start_date, end=end_date, freq='D')
154
  weekdays = dates[dates.weekday < 5]
155
+
156
  data = []
157
  materials = ['steel', 'aluminum', 'plastic', 'copper']
158
  shifts = ['day', 'night']
159
+ base_weights = {'steel': 1500, 'aluminum': 800, 'plastic': 600, 'copper': 400}
160
+
161
  for date in weekdays:
162
  for material in materials:
163
  for shift in shifts:
164
+ base_weight = base_weights[material]
 
 
 
 
 
165
  weight = base_weight + np.random.normal(0, base_weight * 0.2)
166
  weight = max(weight, base_weight * 0.3)
167
  data.append({
 
170
  'material_type': material,
171
  'shift': shift
172
  })
173
+
174
+ return process_raw_data(pd.DataFrame(data))
175
+
176
+ @st.cache_data
177
+ def load_preset_data(year):
178
+ try:
179
+ if year in Config.DATA_URLS:
180
+ response = requests.get(Config.DATA_URLS[year], timeout=10)
181
+ response.raise_for_status()
182
+ df = pd.read_csv(io.StringIO(response.text), sep='\t')
183
+ return process_raw_data(df)
184
+ else:
185
+ return generate_sample_data(year)
186
+ except Exception as e:
187
+ st.warning(f"Could not load remote {year} data: {str(e)}. Loading sample data instead.")
188
+ return generate_sample_data(year)
189
 
190
  @st.cache_data
191
  def load_data(file):
192
  df = pd.read_csv(file, sep='\t')
193
+ return process_raw_data(df)
194
+
195
+ def process_raw_data(df):
196
  df['date'] = pd.to_datetime(df['date'], format='%m/%d/%Y')
197
  df['day_name'] = df['date'].dt.day_name()
198
  return df
 
201
  stats = {}
202
  total = df['weight_kg'].sum()
203
  total_work_days = df['date'].nunique()
204
+
205
  for material in df['material_type'].unique():
206
  data = df[df['material_type'] == material]
207
  work_days = data['date'].nunique()
 
213
  'work_days': work_days,
214
  'records': len(data)
215
  }
216
+
217
  stats['_total_'] = {
218
  'total': total,
219
  'percentage': 100.0,
 
223
  }
224
  return stats
225
 
226
+ def detect_outliers(df):
227
+ outliers = {}
228
+ for material in df['material_type'].unique():
229
+ material_data = df[df['material_type'] == material]
230
+ data = material_data['weight_kg']
231
+ Q1, Q3 = data.quantile(0.25), data.quantile(0.75)
232
+ IQR = Q3 - Q1
233
+ lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
234
+ outlier_mask = (data < lower) | (data > upper)
235
+ outlier_dates = material_data[outlier_mask]['date'].dt.strftime('%Y-%m-%d').tolist()
236
+ outliers[material] = {
237
+ 'count': len(outlier_dates),
238
+ 'range': f"{lower:.0f} - {upper:.0f} kg",
239
+ 'dates': outlier_dates
240
+ }
241
+ return outliers
242
+
243
+ # Chart Functions
244
  def get_chart_theme():
245
+ colors = Config.DESIGN_SYSTEM['colors']
246
  return {
247
  'layout': {
248
  'plot_bgcolor': 'white',
249
  'paper_bgcolor': 'white',
250
+ 'font': {'family': 'Inter, sans-serif', 'color': colors['text']},
251
+ 'colorway': [colors['primary'], colors['secondary'], colors['accent'], colors['warning']],
252
+ 'margin': {'t': 60, 'b': 40, 'l': 40, 'r': 40'}
 
253
  }
254
  }
255
 
256
+ def prepare_time_data(df, time_period):
257
+ df_copy = df.copy()
258
+ if time_period == 'weekly':
259
+ df_copy['week'] = df_copy['date'].dt.isocalendar().week
260
+ df_copy['year'] = df_copy['date'].dt.year
261
+ df_copy['period_label'] = df_copy['year'].astype(str) + '-W' + df_copy['week'].astype(str)
262
+ return df_copy.groupby(['year', 'week']), 'week_label'
263
+ elif time_period == 'monthly':
264
+ df_copy['month'] = df_copy['date'].dt.to_period('M')
265
+ df_copy['period_label'] = df_copy['month'].astype(str)
266
+ return df_copy.groupby('month'), 'month'
267
+ else: # daily
268
+ return df_copy.groupby('date'), 'date'
269
+
270
  def create_total_production_chart(df, time_period='daily'):
271
  if time_period == 'daily':
272
  grouped = df.groupby('date')['weight_kg'].sum().reset_index()
273
  fig = px.line(grouped, x='date', y='weight_kg',
274
  title='Total Production Trend',
275
  labels={'weight_kg': 'Weight (kg)', 'date': 'Date'})
276
+ fig.update_layout(showlegend=False)
 
 
 
 
 
 
 
 
277
  else:
278
+ grouped_data, x_col = prepare_time_data(df, time_period)
279
+ if time_period == 'weekly':
280
+ grouped = grouped_data['weight_kg'].sum().reset_index()
281
+ grouped['week_label'] = grouped['year'].astype(str) + '-W' + grouped['week'].astype(str)
282
+ x_col = 'week_label'
283
+ else:
284
+ grouped = grouped_data['weight_kg'].sum().reset_index()
285
+ grouped['month'] = grouped['month'].astype(str)
286
+ x_col = 'month'
287
+
288
+ fig = px.bar(grouped, x=x_col, y='weight_kg',
289
+ title=f'Total Production Trend ({time_period.title()})',
290
+ labels={'weight_kg': 'Weight (kg)', x_col: time_period.title()})
291
+
292
+ fig.update_layout(**get_chart_theme()['layout'], height=Config.CHART_HEIGHT)
293
  return fig
294
 
295
  def create_materials_trend_chart(df, time_period='daily', selected_materials=None):
296
  df_copy = df.copy()
297
  if selected_materials:
298
  df_copy = df_copy[df_copy['material_type'].isin(selected_materials)]
299
+
300
  if time_period == 'daily':
301
  grouped = df_copy.groupby(['date', 'material_type'])['weight_kg'].sum().reset_index()
302
  fig = px.line(grouped, x='date', y='weight_kg', color='material_type',
303
  title='Materials Production Trends',
304
  labels={'weight_kg': 'Weight (kg)', 'date': 'Date', 'material_type': 'Material'})
 
 
 
 
 
 
 
 
305
  else:
306
+ grouped_data, x_col = prepare_time_data(df_copy, time_period)
307
+ if time_period == 'weekly':
308
+ grouped = grouped_data.agg({'weight_kg': 'sum', 'material_type': 'first'}).reset_index()
309
+ grouped['week_label'] = grouped['year'].astype(str) + '-W' + grouped['week'].astype(str)
310
+ x_col = 'week_label'
311
+ else:
312
+ grouped = grouped_data.agg({'weight_kg': 'sum', 'material_type': 'first'}).reset_index()
313
+ grouped['month'] = grouped['month'].astype(str)
314
+ x_col = 'month'
315
+
316
+ fig = px.bar(grouped, x=x_col, y='weight_kg', color='material_type',
317
+ title=f'Materials Production Trends ({time_period.title()})',
318
+ labels={'weight_kg': 'Weight (kg)', x_col: time_period.title(), 'material_type': 'Material'})
319
+
320
+ fig.update_layout(**get_chart_theme()['layout'], height=Config.CHART_HEIGHT)
321
  return fig
322
 
323
  def create_shift_trend_chart(df, time_period='daily'):
 
325
  grouped = df.groupby(['date', 'shift'])['weight_kg'].sum().reset_index()
326
  pivot_data = grouped.pivot(index='date', columns='shift', values='weight_kg').fillna(0)
327
  fig = go.Figure()
328
+
329
  if 'day' in pivot_data.columns:
330
  fig.add_trace(go.Bar(
331
  x=pivot_data.index, y=pivot_data['day'], name='Day Shift',
332
+ marker_color=Config.DESIGN_SYSTEM['colors']['warning'],
333
  text=pivot_data['day'].round(0), textposition='inside'
334
  ))
335
  if 'night' in pivot_data.columns:
336
  fig.add_trace(go.Bar(
337
  x=pivot_data.index, y=pivot_data['night'], name='Night Shift',
338
+ marker_color=Config.DESIGN_SYSTEM['colors']['primary'],
339
  base=pivot_data['day'] if 'day' in pivot_data.columns else 0,
340
  text=pivot_data['night'].round(0), textposition='inside'
341
  ))
342
+
343
  fig.update_layout(
 
344
  title='Daily Shift Production Trends (Stacked)',
345
  xaxis_title='Date', yaxis_title='Weight (kg)',
346
+ barmode='stack', showlegend=True
347
  )
348
  else:
349
  grouped = df.groupby(['date', 'shift'])['weight_kg'].sum().reset_index()
350
  fig = px.bar(grouped, x='date', y='weight_kg', color='shift',
351
  title=f'{time_period.title()} Shift Production Trends',
352
  barmode='stack')
353
+
354
+ fig.update_layout(**get_chart_theme()['layout'], height=Config.CHART_HEIGHT)
355
  return fig
356
 
357
+ # AI Functions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  def generate_ai_summary(model, df, stats, outliers):
359
  if not model:
360
  return "AI analysis unavailable - API key not configured"
361
+
362
  try:
363
  materials = [k for k in stats.keys() if k != '_total_']
364
  context_parts = [
 
371
  "",
372
  "## Material Breakdown:"
373
  ]
374
+
375
  for material in materials:
376
  info = stats[material]
377
+ context_parts.append(f"- {format_material_name(material)}: {format_weight(info['total'])} ({format_percentage(info['percentage'])}), avg {format_weight(info['daily_avg'])}/day")
378
+
379
  daily_data = df.groupby('date')['weight_kg'].sum()
380
  trend_direction = "increasing" if daily_data.iloc[-1] > daily_data.iloc[0] else "decreasing"
381
  volatility = daily_data.std() / daily_data.mean() * 100
382
+
383
  context_parts.extend([
384
  "",
385
  "## Trend Analysis:",
386
  f"- Overall trend: {trend_direction}",
387
  f"- Production volatility: {volatility:.1f}% coefficient of variation",
388
+ f"- Peak production: {format_weight(daily_data.max())}",
389
+ f"- Lowest production: {format_weight(daily_data.min())}"
390
  ])
391
+
392
  total_outliers = sum(info['count'] for info in outliers.values())
393
  context_parts.extend([
394
  "",
 
396
  f"- Total outliers detected: {total_outliers}",
397
  f"- Materials with quality issues: {sum(1 for info in outliers.values() if info['count'] > 0)}"
398
  ])
399
+
400
  if 'shift' in df.columns:
401
  shift_stats = df.groupby('shift')['weight_kg'].sum()
402
  context_parts.extend([
403
  "",
404
  "## Shift Performance:",
405
+ f"- Day shift: {format_weight(shift_stats.get('day', 0))}",
406
+ f"- Night shift: {format_weight(shift_stats.get('night', 0))}"
407
  ])
408
+
409
  context_text = "\n".join(context_parts)
410
+
411
  prompt = f"""
412
  {context_text}
413
 
 
432
 
433
  Keep the entire analysis concise and under 300 words.
434
  """
435
+
436
  response = model.generate_content(prompt)
437
  return response.text
438
  except Exception as e:
 
441
  def query_ai(model, stats, question, df=None):
442
  if not model:
443
  return "AI assistant not available"
444
+
445
  context_parts = [
446
  "Production Data Summary:",
447
+ *[f"- {format_material_name(mat)}: {format_weight(info['total'])} ({format_percentage(info['percentage'])})"
448
  for mat, info in stats.items() if mat != '_total_'],
449
+ f"\nTotal Production: {format_weight(stats['_total_']['total'])} across {stats['_total_']['work_days']} work days"
450
  ]
451
+
452
  if df is not None:
453
  available_cols = list(df.columns)
454
  context_parts.append(f"\nAvailable data fields: {', '.join(available_cols)}")
455
+
456
  if 'shift' in df.columns:
457
  shift_stats = df.groupby('shift')['weight_kg'].sum()
458
  context_parts.append(f"Shift breakdown: {dict(shift_stats)}")
459
+
460
  if 'day_name' in df.columns:
461
  day_stats = df.groupby('day_name')['weight_kg'].mean()
462
  context_parts.append(f"Average daily production: {dict(day_stats.round(0))}")
463
+
464
  context = "\n".join(context_parts) + f"\n\nQuestion: {question}\nAnswer based on available data:"
465
+
466
  try:
467
  response = model.generate_content(context)
468
  return response.text
469
  except:
470
  return "Error getting AI response"
471
 
472
+ # PDF Export Functions
473
  def save_plotly_as_image(fig, filename):
474
  try:
475
  temp_dir = tempfile.gettempdir()
 
482
  'margin': dict(t=50, b=40, l=40, r=40)
483
  })
484
  fig.update_layout(**theme)
485
+
486
  try:
487
  pio.write_image(fig, filepath, format='png', width=800, height=400, scale=2, engine='kaleido')
488
  if os.path.exists(filepath):
 
495
 
496
  def create_pdf_charts(df, stats):
497
  charts = {}
498
+ colors = Config.DESIGN_SYSTEM['colors']
499
+
500
  try:
501
  materials = [k for k in stats.keys() if k != '_total_']
502
  values = [stats[mat]['total'] for mat in materials]
503
+ labels = [format_material_name(mat) for mat in materials]
504
+
505
  if len(materials) > 0 and len(values) > 0:
506
+ # Distribution Chart
507
  try:
508
  fig_pie = px.pie(values=values, names=labels, title="Production Distribution by Material")
509
  charts['pie'] = save_plotly_as_image(fig_pie, "distribution.png")
510
  except:
511
  pass
512
+
513
+ # Material Comparison Chart
 
 
 
 
 
 
 
 
 
514
  try:
515
  fig_bar = px.bar(x=labels, y=values, title="Production by Material Type",
516
  labels={'x': 'Material Type', 'y': 'Weight (kg)'},
517
+ color_discrete_sequence=[colors['primary']])
518
  charts['bar'] = save_plotly_as_image(fig_bar, "materials.png")
519
  except:
520
  pass
521
+
522
+ if len(df) > 0:
523
+ # Trend Chart
524
  try:
525
+ daily_data = df.groupby('date')['weight_kg'].sum().reset_index()
526
+ if len(daily_data) > 0:
527
+ fig_trend = px.line(daily_data, x='date', y='weight_kg', title="Daily Production Trend",
528
+ labels={'date': 'Date', 'weight_kg': 'Weight (kg)'},
529
+ color_discrete_sequence=[colors['primary']])
530
+ charts['trend'] = save_plotly_as_image(fig_trend, "trend.png")
531
  except:
532
  pass
533
+
534
+ # Shift Chart
535
+ if 'shift' in df.columns:
536
+ try:
537
+ shift_data = df.groupby('shift')['weight_kg'].sum().reset_index()
538
+ if len(shift_data) > 0 and shift_data['weight_kg'].sum() > 0:
539
+ fig_shift = px.pie(shift_data, values='weight_kg', names='shift', title="Production by Shift")
540
+ charts['shift'] = save_plotly_as_image(fig_shift, "shifts.png")
541
+ except:
542
+ pass
543
  except Exception as e:
544
  pass
545
+
546
  return charts
547
 
548
+ def create_pdf_table(data, headers, col_widths, style_color):
549
+ """Create a standardized PDF table"""
550
+ table_data = [headers] + data
551
+ table = Table(table_data, colWidths=col_widths)
552
+ table.setStyle(TableStyle([
553
+ ('BACKGROUND', (0, 0), (-1, 0), style_color),
554
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
555
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
556
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
557
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
558
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
559
+ ]))
560
+ return table
561
+
562
  def create_enhanced_pdf_report(df, stats, outliers, model=None):
563
  buffer = io.BytesIO()
564
  doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50)
565
  elements = []
566
  styles = getSampleStyleSheet()
567
+
568
+ # Custom styles
569
  title_style = ParagraphStyle(
570
  'CustomTitle',
571
  parent=styles['Heading1'],
 
589
  leftIndent=20,
590
  textColor=colors.darkgreen
591
  )
592
+
593
+ # Title Page
594
+ elements.extend([
595
+ Spacer(1, 100),
596
+ Paragraph("Production Monitor with AI Insights", title_style),
597
+ Paragraph("Comprehensive Production Analysis Report", styles['Heading3']),
598
+ Spacer(1, 50)
599
+ ])
600
+
601
  report_info = f"""
602
  <para alignment="center">
603
  <b>Nilsen Service &amp; Consulting AS</b><br/>
 
607
  <b>Total Records:</b> {len(df):,}
608
  </para>
609
  """
610
+ elements.extend([
611
+ Paragraph(report_info, styles['Normal']),
612
+ PageBreak()
613
+ ])
614
+
615
+ # Executive Summary
616
  elements.append(Paragraph("Executive Summary", subtitle_style))
617
  total_production = stats['_total_']['total']
618
  work_days = stats['_total_']['work_days']
619
  daily_avg = stats['_total_']['daily_avg']
620
+
621
  exec_summary = f"""
622
  <para>
623
  This report analyzes production data spanning <b>{work_days} working days</b>.
624
+ Total output achieved: <b>{format_weight(total_production)}</b> with an average
625
+ daily production of <b>{format_weight(daily_avg)}</b>.
626
  <br/><br/>
627
  <b>Key Highlights:</b><br/>
628
+ • Total production: {format_weight(total_production)}<br/>
629
+ • Daily average: {format_weight(daily_avg)}<br/>
630
  • Materials tracked: {len([k for k in stats.keys() if k != '_total_'])}<br/>
631
  • Data quality: {len(df):,} records processed
632
  </para>
633
  """
634
+ elements.extend([
635
+ Paragraph(exec_summary, styles['Normal']),
636
+ Spacer(1, 20),
637
+ Paragraph("Production Summary", styles['Heading3'])
638
+ ])
639
+
640
+ # Summary Table
641
+ summary_data = []
642
  for material, info in stats.items():
643
  if material != '_total_':
644
  summary_data.append([
645
+ format_material_name(material),
646
+ format_weight(info['total']),
647
+ format_percentage(info['percentage']),
648
+ format_weight(info['daily_avg'])
649
  ])
650
+
651
+ summary_table = create_pdf_table(
652
+ summary_data,
653
+ ['Material Type', 'Total (kg)', 'Share (%)', 'Daily Avg (kg)'],
654
+ [2*inch, 1.5*inch, 1*inch, 1.5*inch],
655
+ colors.darkblue
656
+ )
657
+ elements.extend([summary_table, PageBreak()])
658
+
659
+ # Charts Section
 
660
  elements.append(Paragraph("Production Analysis Charts", subtitle_style))
661
+
662
+ charts = create_pdf_charts(df, stats)
 
 
663
  charts_added = False
664
  chart_insights = {
665
  'pie': "Material distribution shows production allocation across different materials. Balanced distribution indicates diversified production capabilities.",
 
667
  'bar': "Material comparison highlights performance differences and production capacities. Top performers indicate optimization opportunities.",
668
  'shift': "Shift analysis reveals operational efficiency differences between day and night operations. Balance indicates effective resource utilization."
669
  }
670
+
671
  for chart_type, chart_title in [
672
  ('pie', "Production Distribution"),
673
  ('trend', "Production Trend"),
 
677
  chart_path = charts.get(chart_type)
678
  if chart_path and os.path.exists(chart_path):
679
  try:
680
+ elements.extend([
681
+ Paragraph(chart_title, styles['Heading3']),
682
+ Image(chart_path, width=Config.PDF_CHART_SIZE[0], height=Config.PDF_CHART_SIZE[1]),
683
+ Paragraph(f"<i>Analysis: {chart_insights.get(chart_type, 'Chart analysis not available.')}</i>", ai_style),
684
+ Spacer(1, 20)
685
+ ])
686
  charts_added = True
687
  except Exception as e:
688
  pass
689
+
690
  if not charts_added:
691
+ elements.extend([
692
+ Paragraph("Charts Generation Failed", styles['Heading3']),
693
+ Paragraph("Production Data Summary:", styles['Normal'])
694
+ ])
695
  for material, info in stats.items():
696
  if material != '_total_':
697
+ summary_text = f"• {format_material_name(material)}: {format_weight(info['total'])} ({format_percentage(info['percentage'])})"
698
  elements.append(Paragraph(summary_text, styles['Normal']))
699
  elements.append(Spacer(1, 20))
700
+
701
  elements.append(PageBreak())
702
+
703
+ # Quality Control Analysis
704
  elements.append(Paragraph("Quality Control Analysis", subtitle_style))
705
+
706
+ quality_data = []
707
  for material, info in outliers.items():
708
+ status = "GOOD" if info['count'] == 0 else "MONITOR" if info['count'] <= 3 else "ATTENTION"
 
 
 
 
 
709
  quality_data.append([
710
+ format_material_name(material),
711
  str(info['count']),
712
  info['range'],
713
  status
714
  ])
715
+
716
+ quality_table = create_pdf_table(
717
+ quality_data,
718
+ ['Material', 'Outliers', 'Normal Range (kg)', 'Status'],
719
+ [2*inch, 1*inch, 2*inch, 1.5*inch],
720
+ colors.darkred
721
+ )
 
 
722
  elements.append(quality_table)
723
+
724
+ # AI Analysis Section
725
  if model:
726
+ elements.extend([
727
+ PageBreak(),
728
+ Paragraph("AI Intelligent Analysis", subtitle_style)
729
+ ])
730
  try:
731
  ai_analysis = generate_ai_summary(model, df, stats, outliers)
732
  except:
733
  ai_analysis = "AI analysis temporarily unavailable."
734
+
735
  ai_paragraphs = ai_analysis.split('\n\n')
736
  for paragraph in ai_paragraphs:
737
  if paragraph.strip():
738
  formatted_text = paragraph.replace('**', '<b>', 1).replace('**', '</b>', 1) \
739
  .replace('•', ' •') \
740
  .replace('\n', '<br/>')
741
+ elements.extend([
742
+ Paragraph(formatted_text, styles['Normal']),
743
+ Spacer(1, 8)
744
+ ])
745
  else:
746
+ elements.extend([
747
+ PageBreak(),
748
+ Paragraph("AI Analysis", subtitle_style),
749
+ Paragraph("AI analysis unavailable - API key not configured. Please configure Google AI API key to enable intelligent insights.", styles['Normal'])
750
+ ])
751
+
752
+ # Footer
753
  elements.append(Spacer(1, 30))
754
  footer_text = f"""
755
  <para alignment="center">
 
759
  </para>
760
  """
761
  elements.append(Paragraph(footer_text, styles['Normal']))
762
+
763
  doc.build(elements)
764
  buffer.seek(0)
765
  return buffer
 
767
  def create_csv_export(df, stats):
768
  summary_df = pd.DataFrame([
769
  {
770
+ 'Material': format_material_name(material),
771
  'Total_kg': info['total'],
772
  'Percentage': info['percentage'],
773
  'Daily_Average_kg': info['daily_avg'],
 
778
  ])
779
  return summary_df
780
 
781
+ # UI Components
782
+ def render_header():
783
+ st.markdown("""
784
+ <div class="main-header">
785
+ <div class="main-title">🏭 Production Monitor with AI Insights</div>
786
+ <div class="main-subtitle">Nilsen Service & Consulting AS | Real-time Production Analytics & Recommendations</div>
787
+ </div>
788
+ """, unsafe_allow_html=True)
789
+
790
+ def render_sidebar(model):
791
+ with st.sidebar:
792
+ st.markdown("### 📊 Data Source")
793
+ uploaded_file = st.file_uploader("Upload Production Data", type=['csv'])
794
+ st.markdown("---")
795
+ st.markdown("### 📊 Quick Load")
796
+
797
+ col1, col2 = st.columns(2)
798
+ load_2024 = col1.button("📊 2024 Data", type="primary", key="load_2024")
799
+ load_2025 = col2.button("📊 2025 Data", type="primary", key="load_2025")
800
+
801
+ st.markdown("---")
802
+ st.markdown("""
803
+ **Expected TSV format:**
804
+ - `date`: MM/DD/YYYY
805
+ - `weight_kg`: Production weight
806
+ - `material_type`: Material category
807
+ - `shift`: day/night (optional)
808
+ """)
809
+
810
+ if model:
811
+ st.success("🤖 AI Assistant Ready")
812
+ else:
813
+ st.warning("⚠️ AI Assistant Unavailable")
814
+
815
+ return uploaded_file, load_2024, load_2025
816
+
817
+ def render_metric_cards(stats):
818
+ st.markdown('<div class="section-header">📋 Material Overview</div>', unsafe_allow_html=True)
819
+ materials = [k for k in stats.keys() if k != '_total_']
820
+ cols = st.columns(4)
821
+
822
+ for i, material in enumerate(materials[:3]):
823
+ info = stats[material]
824
+ with cols[i]:
825
+ st.metric(
826
+ label=format_material_name(material),
827
+ value=format_weight(info['total']),
828
+ delta=f"{format_percentage(info['percentage'])} of total"
829
+ )
830
+ st.caption(f"Daily avg: {format_weight(info['daily_avg'])}")
831
+
832
+ if len(materials) >= 3:
833
+ total_info = stats['_total_']
834
+ with cols[3]:
835
+ st.metric(
836
+ label="Total Production",
837
+ value=format_weight(total_info['total']),
838
+ delta="100% of total"
839
+ )
840
+ st.caption(f"Daily avg: {format_weight(total_info['daily_avg'])}")
841
+
842
+ def render_charts_section(df, stats):
843
+ st.markdown('<div class="section-header">📊 Production Trends</div>', unsafe_allow_html=True)
844
+ col1, col2 = st.columns([3, 1])
845
+
846
+ with col2:
847
+ time_view = st.selectbox("Time Period", ["daily", "weekly", "monthly"], key="time_view_select")
848
+
849
+ with col1:
850
+ with st.container():
851
+ st.markdown('<div class="chart-container">', unsafe_allow_html=True)
852
+ total_chart = create_total_production_chart(df, time_view)
853
+ st.plotly_chart(total_chart, use_container_width=True)
854
+ st.markdown('</div>', unsafe_allow_html=True)
855
+
856
+ # Materials Analysis
857
+ st.markdown('<div class="section-header">🏷️ Materials Analysis</div>', unsafe_allow_html=True)
858
+ col1, col2 = st.columns([3, 1])
859
+
860
+ materials = [k for k in stats.keys() if k != '_total_']
861
+ with col2:
862
+ selected_materials = st.multiselect(
863
+ "Select Materials",
864
+ options=materials,
865
+ default=materials,
866
+ key="materials_select"
867
+ )
868
+
869
+ with col1:
870
+ if selected_materials:
871
+ with st.container():
872
+ st.markdown('<div class="chart-container">', unsafe_allow_html=True)
873
+ materials_chart = create_materials_trend_chart(df, time_view, selected_materials)
874
+ st.plotly_chart(materials_chart, use_container_width=True)
875
+ st.markdown('</div>', unsafe_allow_html=True)
876
+
877
+ # Shift Analysis
878
+ if 'shift' in df.columns:
879
+ st.markdown('<div class="section-header">🌓 Shift Analysis</div>', unsafe_allow_html=True)
880
+ with st.container():
881
+ st.markdown('<div class="chart-container">', unsafe_allow_html=True)
882
+ shift_chart = create_shift_trend_chart(df, time_view)
883
+ st.plotly_chart(shift_chart, use_container_width=True)
884
+ st.markdown('</div>', unsafe_allow_html=True)
885
+
886
+ def render_quality_check(df):
887
+ st.markdown('<div class="section-header">⚠️ Quality Check</div>', unsafe_allow_html=True)
888
+ outliers = detect_outliers(df)
889
+ cols = st.columns(len(outliers))
890
+
891
+ for i, (material, info) in enumerate(outliers.items()):
892
+ with cols[i]:
893
+ if info['count'] > 0:
894
+ dates_str = ", ".join(info['dates'][:3])
895
+ if len(info['dates']) > 3:
896
+ dates_str += f", +{len(info['dates'])-3} more"
897
+
898
+ alert_html = f"""
899
+ <div class="alert alert-warning">
900
+ <strong>{format_material_name(material)}</strong><br>
901
+ {info["count"]} outliers detected<br>
902
+ Normal range: {info["range"]}<br>
903
+ <small>Dates: {dates_str}</small>
904
+ </div>
905
+ """
906
+ else:
907
+ alert_html = f"""
908
+ <div class="alert alert-success">
909
+ <strong>{format_material_name(material)}</strong><br>
910
+ All values normal
911
+ </div>
912
+ """
913
+ st.markdown(alert_html, unsafe_allow_html=True)
914
+
915
+ return outliers
916
+
917
+ def render_export_section(df, stats, outliers, model):
918
  st.markdown('<div class="section-header">📄 Export Reports</div>', unsafe_allow_html=True)
919
+
920
+ # Initialize session state
921
+ for key in ['export_ready', 'pdf_buffer', 'csv_data']:
922
+ if key not in st.session_state:
923
+ st.session_state[key] = None if key != 'export_ready' else False
924
+
925
  col1, col2, col3 = st.columns(3)
926
+
927
  with col1:
928
  if st.button("📊 Generate PDF Report with AI", key="generate_pdf_btn", type="primary"):
929
+ result = safe_execute(
930
+ "PDF generation",
931
+ lambda: create_enhanced_pdf_report(df, stats, outliers, model)
932
+ )
933
+ if result:
934
+ st.session_state.pdf_buffer = result
935
+ st.session_state.export_ready = True
936
  st.success("✅ PDF report with AI analysis generated successfully!")
937
+ else:
 
938
  st.session_state.export_ready = False
939
+
940
  if st.session_state.export_ready and st.session_state.pdf_buffer:
941
  st.download_button(
942
  label="💾 Download PDF Report",
 
945
  mime="application/pdf",
946
  key="download_pdf_btn"
947
  )
948
+
949
  with col2:
950
  if st.button("📈 Generate CSV Summary", key="generate_csv_btn"):
951
+ result = safe_execute("CSV generation", lambda: create_csv_export(df, stats))
952
+ if result is not None:
953
+ st.session_state.csv_data = result
954
  st.success("✅ CSV summary generated successfully!")
955
+
 
956
  if st.session_state.csv_data is not None:
957
  csv_string = st.session_state.csv_data.to_csv(index=False)
958
  st.download_button(
 
962
  mime="text/csv",
963
  key="download_csv_btn"
964
  )
965
+
966
  with col3:
967
  csv_string = df.to_csv(index=False)
968
  st.download_button(
 
973
  key="download_raw_btn"
974
  )
975
 
976
+ def render_ai_section(df, stats, model):
977
+ if not model:
978
+ return
979
+
980
+ st.markdown('<div class="section-header">🤖 AI Insights</div>', unsafe_allow_html=True)
981
+
982
+ quick_questions = [
983
+ "How does production distribution on weekdays compare to weekends?",
984
+ "Which material exhibits the most volatility in our dataset?",
985
+ "To improve stability, which material or shift needs immediate attention?"
986
+ ]
987
+
988
+ cols = st.columns(len(quick_questions))
989
+ for i, question in enumerate(quick_questions):
990
+ with cols[i]:
991
+ if st.button(question, key=f"ai_q_{i}"):
992
+ with st.spinner("Analyzing..."):
993
+ answer = query_ai(model, stats, question, df)
994
+ st.info(answer)
995
+
996
+ custom_question = st.text_input(
997
+ "Ask about your production data:",
998
+ placeholder="e.g., 'Compare steel vs aluminum last month'",
999
+ key="custom_ai_question"
1000
+ )
1001
+
1002
+ if custom_question and st.button("Ask AI", key="ask_ai_btn"):
1003
+ with st.spinner("Analyzing..."):
1004
+ answer = query_ai(model, stats, custom_question, df)
1005
+ st.success(f"**Q:** {custom_question}")
1006
+ st.write(f"**A:** {answer}")
1007
+
1008
+ def render_welcome_page():
1009
+ st.markdown('<div class="section-header">📖 How to Use This Platform</div>', unsafe_allow_html=True)
1010
+ col1, col2 = st.columns(2)
1011
+
1012
+ with col1:
1013
  st.markdown("""
1014
+ ### 🚀 Quick Start
1015
+ 1. Upload your TSV data in the sidebar
1016
+ 2. Or click Quick Load buttons for preset data
1017
+ 3. View production by material type
1018
+ 4. Analyze trends (daily/weekly/monthly)
1019
+ 5. Check anomalies in Quality Check
1020
+ 6. Export reports (PDF with AI, CSV)
1021
+ 7. Ask the AI assistant for insights
1022
  """)
1023
+
1024
+ with col2:
1025
+ st.markdown("""
1026
+ ### 📊 Key Features
1027
+ - Real-time interactive charts
1028
+ - One-click preset data loading
1029
+ - Time-period comparisons
1030
+ - Shift performance analysis
1031
+ - Outlier detection with dates
1032
+ - AI-powered PDF reports
1033
+ - Intelligent recommendations
1034
+ """)
1035
+
1036
+ st.info("📁 Ready to start? Upload your production data or use Quick Load buttons to begin analysis!")
1037
+
1038
+ def handle_data_input():
1039
+ """Handle all data input scenarios"""
1040
+ uploaded_file, load_2024, load_2025 = render_sidebar(init_ai())
1041
+
1042
+ # Handle uploaded file
1043
  if uploaded_file:
1044
+ result = safe_execute("Data loading", load_data, uploaded_file)
1045
+ if result is not None:
1046
+ stats = get_material_stats(result)
1047
+ st.session_state.current_df = result
1048
  st.session_state.current_stats = stats
1049
  st.success("✅ Data uploaded successfully!")
1050
+ return result, stats
1051
+
1052
+ # Handle preset data loading
1053
+ for year, button_pressed in [("2024", load_2024), ("2025", load_2025)]:
1054
+ if button_pressed:
1055
  with st.spinner(f"Loading {year} data..."):
1056
+ result = safe_execute("Preset data loading", load_preset_data, year)
1057
+ if result is not None:
1058
+ stats = get_material_stats(result)
1059
+ st.session_state.current_df = result
1060
  st.session_state.current_stats = stats
1061
  st.success(f"✅ {year} data loaded successfully!")
1062
+ return result, stats
1063
+
1064
+ # Return session state data if available
1065
+ if hasattr(st.session_state, 'current_df') and st.session_state.current_df is not None:
1066
+ return st.session_state.current_df, st.session_state.current_stats
1067
+
1068
+ return None, None
1069
+
1070
+ def render_dashboard(df, stats, model):
1071
+ """Render the complete dashboard"""
1072
+ render_metric_cards(stats)
1073
+ render_charts_section(df, stats)
1074
+ outliers = render_quality_check(df)
1075
+ render_export_section(df, stats, outliers, model)
1076
+ render_ai_section(df, stats, model)
1077
+
1078
+ # Main Application
1079
+ def main():
1080
+ load_css()
1081
+ render_header()
1082
+
1083
+ model = init_ai()
1084
+
1085
+ # Initialize session state
1086
+ for key in ['current_df', 'current_stats']:
1087
+ if key not in st.session_state:
1088
+ st.session_state[key] = None
1089
+
1090
+ df, stats = handle_data_input()
1091
+
1092
  if df is not None and stats is not None:
1093
+ render_dashboard(df, stats, model)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1094
  else:
1095
+ render_welcome_page()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096
 
1097
  if __name__ == "__main__":
1098
  main()