entropy25 commited on
Commit
5c19119
Β·
verified Β·
1 Parent(s): 42d6628

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +418 -605
app.py CHANGED
@@ -17,52 +17,24 @@ import tempfile
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,61 +43,68 @@ st.set_page_config(
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,10 +113,8 @@ def load_css():
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,22 +123,44 @@ def init_ai():
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,29 +169,14 @@ def generate_sample_data(year):
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,7 +185,6 @@ def get_material_stats(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,7 +196,6 @@ def get_material_stats(df):
213
  'work_days': work_days,
214
  'records': len(data)
215
  }
216
-
217
  stats['_total_'] = {
218
  'total': total,
219
  'percentage': 100.0,
@@ -223,101 +205,69 @@ def get_material_stats(df):
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,40 +275,53 @@ 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,24 +334,20 @@ def generate_ai_summary(model, df, stats, outliers):
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,18 +355,15 @@ def generate_ai_summary(model, df, stats, outliers):
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,7 +388,6 @@ Example Recommendation format:
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,35 +396,28 @@ Keep the entire analysis concise and under 300 words.
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,7 +430,6 @@ def save_plotly_as_image(fig, filename):
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,77 +442,51 @@ def save_plotly_as_image(fig, filename):
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,15 +510,10 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
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,59 +523,53 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
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,7 +577,6 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
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,79 +586,67 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
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,7 +656,6 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
759
  </para>
760
  """
761
  elements.append(Paragraph(footer_text, styles['Normal']))
762
-
763
  doc.build(elements)
764
  buffer.seek(0)
765
  return buffer
@@ -767,7 +663,7 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
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,165 +674,25 @@ def create_csv_export(df, stats):
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,14 +701,13 @@ def render_export_section(df, stats, outliers, model):
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,7 +717,6 @@ def render_export_section(df, stats, outliers, model):
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,126 +727,185 @@ def render_export_section(df, stats, outliers, model):
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()
 
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
  )
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
  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
  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
  '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
  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
  'work_days': work_days,
197
  'records': len(data)
198
  }
 
199
  stats['_total_'] = {
200
  'total': total,
201
  'percentage': 100.0,
 
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
  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
  "",
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
  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
 
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
  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
  '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
 
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
  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
  <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
  '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
  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
  </para>
657
  """
658
  elements.append(Paragraph(footer_text, styles['Normal']))
 
659
  doc.build(elements)
660
  buffer.seek(0)
661
  return buffer
 
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
  ])
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
  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
  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
  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()