wayne-chi commited on
Commit
3aa6cd4
·
verified ·
1 Parent(s): 8e59bd9

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +1243 -390
streamlit_app.py CHANGED
@@ -5,15 +5,16 @@ import matplotlib.pyplot as plt
5
  import plotly.express as px
6
  import numpy as np
7
  import plotly.graph_objects as go
 
 
 
 
 
8
  # from blend_logic import run_dummy_prediction
9
 
10
  ##---- fucntions ------
11
- import pandas as pd
12
- import streamlit as st
13
- import os
14
-
15
  # Load fuel data from CSV (create this file if it doesn't exist)
16
- FUEL_CSV_PATH = "/tmp/data/fuel_properties.csv"
17
 
18
  def load_fuel_data():
19
  """Load fuel data from CSV or create default if not exists"""
@@ -200,36 +201,11 @@ st.markdown("""
200
  }
201
 
202
 
203
- /* Mobile responsive changes */
204
  @media only screen and (max-width: 768px) {
205
- .stTabs [data-baseweb="tab-list"] {
206
- flex-direction: column;
207
- gap: 4px;
208
- padding: 4px;
209
- width: 100% !important;
210
- }
211
-
212
- .stTabs [data-baseweb="tab"] {
213
- width: 100% !important;
214
- margin-bottom: 4px;
215
- padding: 10px;
216
- }
217
-
218
- /* Adjust chart containers */
219
- .stPlotlyChart {
220
- width: 100% !important;
221
- margin: 0 !important;
222
- }
223
-
224
- /* Stack columns vertically */
225
- .stHorizontalBlock > div {
226
- flex-direction: column !important;
227
- }
228
-
229
- /* Reduce padding in mobile */
230
- .stApp > div {
231
- padding: 0.5rem !important;
232
  }
 
233
 
234
  .stDataFrame {
235
  border-radius: 10px;
@@ -253,16 +229,14 @@ st.markdown("""
253
  margin: 2rem 0;
254
  }
255
 
256
-
257
- /* Consistent chart styling */
258
  .stPlotlyChart {
259
  border-radius: 10px;
260
- background: white;
261
  padding: 15px;
262
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
263
  margin-bottom: 25px;
264
  }
265
-
266
 
267
 
268
  /* Match number inputs */
@@ -318,287 +292,903 @@ st.markdown("""
318
  tabs = st.tabs([
319
  "📊 Dashboard",
320
  "🎛️ Blend Designer",
321
- "📤 Nothing For Now",
322
  "⚙️ Optimization Engine",
 
323
  "📚 Fuel Registry",
324
  "🧠 Model Insights"
325
  ])
326
 
327
- # ---------------------- Dashboard Tab ----------------------
328
 
329
- with tabs[0]:
330
- st.subheader("Performance Metrics")
331
- col1, col2, col3, col4 = st.columns(4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
- with col1:
334
- st.markdown("""
335
- <div class="metric-card">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  <div class="metric-label">Model Accuracy</div>
337
- <div class="metric-value">94.7%</div>
338
- <div class="metric-delta">R² Score</div>
339
  </div>
340
  """, unsafe_allow_html=True)
341
-
342
- with col2:
343
- st.markdown("""
344
- <div class="metric-card">
345
  <div class="metric-label">Predictions Made</div>
346
- <div class="metric-value">12,847</div>
347
- <div class="metric-delta">Today</div>
348
  </div>
349
  """, unsafe_allow_html=True)
350
-
351
- with col3:
352
- st.markdown("""
353
- <div class="metric-card">
354
  <div class="metric-label">Optimizations</div>
355
- <div class="metric-value">156</div>
356
  <div class="metric-delta">This Week</div>
357
  </div>
358
  """, unsafe_allow_html=True)
359
-
360
- with col4:
361
- st.markdown("""
362
- <div class="metric-card">
363
- <div class="metric-label">Cost Savings</div>
364
- <div class="metric-value">$2.4M</div>
365
- <div class="metric-delta">Estimated Annual</div>
366
  </div>
367
  """, unsafe_allow_html=True)
368
 
 
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
- st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
372
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
 
374
 
375
- st.subheader("Current Blend Properties")
376
- blend_props = {
377
- "Property 1": 0.847,
378
- "Property 2": 0.623,
379
- "Property 3": 0.734,
380
- "Property 4": 0.912,
381
- "Property 5": 0.456,
382
- "Property 6": -1.234,
383
- }
384
-
385
- # Enhanced dataframe display
386
- df = pd.DataFrame(blend_props.items(), columns=["Property", "Value"])
387
- # st.dataframe(
388
- # df.style
389
- # .background_gradient(cmap="YlOrBr", subset=["Value"])
390
- # .format({"Value": "{:.3f}"}),
391
- # use_container_width=True
392
- # )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
- st.markdown('<div class="table-container"><div class="table-inner">', unsafe_allow_html=True)
395
- st.dataframe(df, use_container_width=True)
396
- st.markdown('</div></div>', unsafe_allow_html=True)
397
 
 
398
 
 
 
 
 
 
 
 
 
399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  with tabs[1]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  col_header = st.columns([0.8, 0.2])
403
  with col_header[0]:
404
  st.subheader("🎛️ Blend Designer")
405
  with col_header[1]:
406
- batch_blend = st.checkbox("Batch Blend Mode", value=False,
407
- help="Switch between manual input and predefined fuel selection",
408
- key="batch_blend_mode")
409
-
410
- # Initialize session state
411
- if 'show_visualization' not in st.session_state:
412
- st.session_state.show_visualization = False
413
- if 'blended_value' not in st.session_state:
414
- st.session_state.blended_value = None
415
- if 'selected_property' not in st.session_state:
416
- st.session_state.selected_property = "Property1"
417
-
418
- # Batch mode file upload
419
  if batch_blend:
420
  st.subheader("📤 Batch Processing")
421
  uploaded_file = st.file_uploader("Upload CSV File", type=["csv"], key="Batch_upload")
422
- weights = [0.1, 0.2, 0.25, 0.15, 0.3] # Default weights for batch mode
423
-
424
- if not uploaded_file:
425
- st.warning("Please upload a CSV file for batch processing")
426
- data_input = None
427
- else:
428
- try:
429
- data_input = pd.read_csv(uploaded_file)
430
- st.success("File uploaded successfully")
431
- st.dataframe(data_input.head())
432
- except Exception as e:
433
- st.error(f"Error reading file: {str(e)}")
434
- data_input = None
435
  else:
436
- # Regular mode
437
- data_input = None
438
- weights, props = [], []
439
- col1, col2 = st.columns(2)
440
-
441
- with col1:
442
- st.markdown("##### ⚖️ Component Weights")
443
- for i in range(5):
444
- weight = st.number_input(
445
- f"Weight for Component {i+1}",
446
- min_value=0.0,
447
- max_value=1.0,
448
- value=0.2,
449
- step=0.01,
450
- key=f"w_{i}"
451
- )
452
- weights.append(weight)
453
-
454
- with col2:
455
- st.markdown("##### Fuel Selection")
456
- for i in range(5):
457
- fuel = st.selectbox(
458
- f"Component {i+1} Fuel Type",
459
- options=list(st.session_state.FUEL_PROPERTIES.keys()),
460
- key=f"fuel_{i}"
461
- )
462
- props.append(st.session_state.FUEL_PROPERTIES[fuel])
463
-
464
- if st.button("⚙️ Predict Blended Property", key="predict_btn"):
465
- if batch_blend:
466
- if data_input is None:
467
- st.error("⚠️ Please upload a valid CSV file first!")
468
- st.session_state.show_visualization = False
469
- else:
470
- st.session_state.show_visualization = True
471
- else:
472
- if abs(sum(weights) - 1.0) > 0.01:
473
- st.warning("⚠️ The total of weights must be **1.0**.")
474
- st.session_state.show_visualization = False
475
- else:
476
- st.session_state.show_visualization = True
477
 
478
- if st.session_state.show_visualization:
479
- # Show calculation details
480
- st.subheader("Blend Components Data")
481
-
482
- if not batch_blend:
483
- weights_data = {f"Component{i+1}_fraction": weights[i] for i in range(len(weights))}
484
- props_data = {f"Component{i+1}_{j}": props[i][j] for j in props[i].keys() for i in range(len(props))}
485
- combined = {**weights_data, **props_data}
486
- data_input = pd.DataFrame([combined])
487
-
488
- st.write("Properties:", data_input)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
- # Show visualization only if prediction was made
491
- if st.session_state.show_visualization:
492
- if not batch_blend:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
494
- st.subheader("Blend Visualization")
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
- components = [f"Component {i+1}" for i in range(5)]
 
 
 
 
 
 
 
 
 
 
 
497
 
498
- # 1. Weight Distribution Pie Chart
499
- col1, col2 = st.columns(2)
500
- with col1:
501
- fig1 = px.pie(
502
- names=components,
503
- values=weights,
504
- title="Weight Distribution",
505
- color_discrete_sequence=['#8B4513', '#CFB53B', '#654321'],
506
- hole=0.4
507
  )
508
- fig1.update_layout(
509
- margin=dict(t=50, b=10),
510
- showlegend=False
 
 
 
 
 
 
511
  )
512
- fig1.update_traces(
513
- textposition='inside',
514
- textinfo='percent+label',
515
- marker=dict(line=dict(color='#ffffff', width=1))
 
 
 
 
 
 
 
 
 
 
516
  )
517
- st.plotly_chart(fig1, use_container_width=True)
 
518
 
519
- # 2. Property Comparison Bar Chart
520
- with col2:
521
- # Property selection for fuel mode
522
- viz_property = st.selectbox(
523
- "Select Property to View",
524
- [f"Property{i+1}" for i in range(10)],
525
- key="viz_property"
526
- )
527
- bar_values = [p[viz_property] for p in props]
528
- blended_value = 123 #Modify
529
-
530
- fig2 = px.bar(
531
- x=components,
532
- y=bar_values,
533
- title=f"{viz_property} Values",
534
- color=bar_values,
535
- color_continuous_scale='YlOrBr'
536
  )
537
- fig2.update_layout(
538
- yaxis_title=viz_property,
539
- xaxis_title="Component",
540
- margin=dict(t=50, b=10),
541
- coloraxis_showscale=False
542
  )
 
 
 
 
 
 
 
 
 
 
543
 
544
- fig2.add_hline(
545
- y=blended_value,
546
- line_dash="dot",
547
- line_color="#ff6600",
548
- annotation_text="Blended Value",
549
- annotation_position="top right"
550
- )
551
- st.plotly_chart(fig2, use_container_width=True)
552
 
553
- # Display the calculated value prominently
554
- st.markdown(f"""
555
- <div style="
556
- background-color: #FAF3E6;
557
- border-left: 4px solid #8B4513;
558
- border-radius: 4px;
559
- padding: 12px;
560
- margin: 12px 0;
561
- ">
562
- <p style="margin: 0; color: #654321;
563
- font-size: 2.2rem;
564
- font-weight: 800;
565
- color: #000;
566
- text-align:center;">
567
- Calculated <strong>{viz_property}</strong> =
568
- <strong style="color: #000">{blended_value:.4f}</strong>
569
- </p>
570
- </div>
571
- """, unsafe_allow_html=True)
572
- else:
573
- # Batch mode visualization placeholder
574
- st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
575
- st.subheader("Batch Processing Results")
576
- st.dataframe(data_input, use_container_width=True)
577
- # st.info("Batch processing complete. Add custom visualizations here.")
578
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
 
580
  with tabs[2]:
581
- st.subheader("📤 Nothing FOr NOw")
582
- # uploaded_file = st.file_uploader("Upload CSV File", type=["csv"])
583
-
584
- # if uploaded_file:
585
- # df = pd.read_csv(uploaded_file)
586
- # st.success("File uploaded successfully")
587
- # st.dataframe(df.head())
588
-
589
- # if st.button("⚙️ Run Batch Prediction"):
590
- # result_df = df.copy()
591
- # # result_df["Predicted_Property"] = df.apply(
592
- # # lambda row: run_dummy_prediction(row.values[:5], row.values[5:10]), axis=1
593
- # # )
594
- # st.success("Batch prediction completed")
595
- # st.dataframe(result_df.head())
596
- # csv = result_df.to_csv(index=False).encode("utf-8")
597
- # st.download_button("Download Results", csv, "prediction_results.csv", "text/csv")
598
-
599
-
600
-
601
- with tabs[3]:
602
  st.subheader("⚙️ Optimization Engine")
603
 
604
  # Pareto frontier demo
@@ -665,154 +1255,417 @@ with tabs[3]:
665
  )
666
  st.plotly_chart(fig4, use_container_width=True)
667
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
 
 
 
 
 
 
 
669
 
670
  with tabs[4]:
671
- st.subheader("📚 Fuel Registry") # Changed to book emoji for registry
 
672
 
673
- # Button to add new fuel
674
- st.markdown("#### Add a New Fuel Type")
675
- with st.expander("Click to Add New Fuel", expanded=False):
676
- with st.form("new_fuel_form", clear_on_submit=False):
677
- fuel_name = st.text_input("Fuel Name", placeholder="e.g. Bioethanol")
678
-
679
- cols = st.columns(5)
680
- properties = {}
681
- for i in range(10):
682
- with cols[i % 5]:
683
- prop_val = st.number_input(
684
- f"Property {i+1}",
685
- min_value=0.0,
686
- step=0.1,
687
- key=f"prop_{i}",
688
- format="%.2f"
689
- )
690
- properties[f"Property{i+1}"] = round(prop_val, 2)
691
-
692
- col1, col2 = st.columns(2)
693
- with col1:
694
- submitted = st.form_submit_button("💾 Save Fuel", use_container_width=True)
695
- with col2:
696
- cancelled = st.form_submit_button("❌ Cancel", use_container_width=True)
697
 
698
- if submitted:
699
- if not fuel_name.strip():
700
- st.warning("Fuel name cannot be empty.")
701
- elif fuel_name in st.session_state.FUEL_PROPERTIES:
702
- st.error(f"{fuel_name} already exists in registry.")
703
- else:
704
- # Update both session state and CSV
705
- st.session_state.FUEL_PROPERTIES[fuel_name] = properties
706
- save_fuel_data()
707
- st.success(f"{fuel_name} successfully added!")
708
- st.rerun() # Refresh to show new fuel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
 
710
- if cancelled:
711
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
- with st.expander("Batch Add New Fuel", expanded=False):
714
- uploaded_file = st.file_uploader(
715
- "📤 Upload Fuel Batch (CSV)",
716
- type=['csv'],
717
- accept_multiple_files=False,
718
- key="fuel_uploader",
719
- help="Upload a CSV file with the same format as the exported registry"
720
- )
721
- if uploaded_file is not None:
722
- try:
723
- new_fuels = pd.read_csv(uploaded_file, index_col=0).to_dict('index')
724
-
725
- # Check for duplicates
726
- duplicates = [name for name in new_fuels if name in st.session_state.FUEL_PROPERTIES]
727
-
728
- if duplicates:
729
- st.warning(f"These fuels already exist and won't be updated: {', '.join(duplicates)}")
730
- # Only add new fuels
731
- new_fuels = {name: props for name, props in new_fuels.items()
732
- if name not in st.session_state.FUEL_PROPERTIES}
733
-
734
- if new_fuels:
735
- st.session_state.FUEL_PROPERTIES.update(new_fuels)
736
- save_fuel_data()
737
- st.success(f"Added {len(new_fuels)} new fuel(s) to registry!")
738
  st.rerun()
739
- else:
740
- st.info("No new fuels to add from the uploaded file.")
741
-
742
- except Exception as e:
743
- st.error(f"Error processing file: {str(e)}")
744
- st.error("Please ensure the file matches the expected format")
745
-
746
- # Display current fuel properties
747
- st.markdown("#### 🔍 Current Fuel Properties")
748
- st.dataframe(
749
- pd.DataFrame(st.session_state.FUEL_PROPERTIES).T.style
750
- .background_gradient(cmap="YlOrBr", axis=None)
751
- .format(precision=2),
752
- use_container_width=True,
753
- height=(len(st.session_state.FUEL_PROPERTIES) + 1) * 35 + 3,
754
- hide_index=False
755
- )
756
 
757
- # File operations section
758
 
 
 
759
 
760
- st.download_button(
761
- label="📥 Download Registry (CSV)",
762
- data=pd.DataFrame(st.session_state.FUEL_PROPERTIES).T.to_csv().encode('utf-8'),
763
- file_name='fuel_properties.csv',
764
- mime='text/csv',
765
- # use_container_width=True
766
- )
767
 
 
 
 
 
 
 
768
 
769
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
 
 
 
 
 
 
 
 
 
 
 
 
 
771
 
 
 
 
 
772
 
 
 
 
773
 
 
774
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775
 
 
776
 
 
 
777
 
778
- with tabs[5]:
779
- st.subheader("🧠 Model Insights")
780
-
781
- # Feature importance
782
- st.markdown("#### Property Importance")
783
- features = ['Property 1', 'Property 2', 'Property 3', 'Property 4', 'Property 5']
784
- importance = np.array([0.35, 0.25, 0.2, 0.15, 0.05])
785
-
786
- fig5 = px.bar(
787
- x=importance,
788
- y=features,
789
  orientation='h',
790
- title="Feature Importance for Blend Prediction",
791
- color=importance,
792
- color_continuous_scale='YlOrBr'
793
- )
794
- fig5.update_layout(
795
- xaxis_title="Importance Score",
796
- yaxis_title="Property",
797
- coloraxis_showscale=False
798
- )
799
- st.plotly_chart(fig5, use_container_width=True)
800
-
801
- # SHAP values demo
802
- st.markdown("#### Property Impact Direction")
803
- fig6 = px.scatter(
804
- x=np.random.randn(100),
805
- y=np.random.randn(100),
806
- color=np.random.choice(features, 100),
807
- title="SHAP Values (Simulated)",
808
- labels={'x': 'Impact on Prediction', 'y': 'Property Value'}
809
- )
810
- fig6.update_traces(
811
- marker=dict(size=10, opacity=0.7),
812
- selector=dict(mode='markers')
 
 
 
 
 
 
 
 
 
 
 
 
813
  )
814
- fig6.add_vline(x=0, line_width=1, line_dash="dash")
815
- st.plotly_chart(fig6, use_container_width=True)
 
816
 
817
 
818
 
 
5
  import plotly.express as px
6
  import numpy as np
7
  import plotly.graph_objects as go
8
+ import sqlite3
9
+ from typing import Optional, Dict, Any
10
+ from datetime import datetime, timedelta
11
+ import re
12
+ from pathlib import Path
13
  # from blend_logic import run_dummy_prediction
14
 
15
  ##---- fucntions ------
 
 
 
 
16
  # Load fuel data from CSV (create this file if it doesn't exist)
17
+ FUEL_CSV_PATH = "fuel_properties.csv"
18
 
19
  def load_fuel_data():
20
  """Load fuel data from CSV or create default if not exists"""
 
201
  }
202
 
203
 
 
204
  @media only screen and (max-width: 768px) {
205
+ .table-inner {
206
+ width: 90%; /* For mobile */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  }
208
+ }
209
 
210
  .stDataFrame {
211
  border-radius: 10px;
 
229
  margin: 2rem 0;
230
  }
231
 
232
+ /* Consistent chart styling --- THIS IS THE FIX --- */
 
233
  .stPlotlyChart {
234
  border-radius: 10px;
235
+
236
  padding: 15px;
237
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
238
  margin-bottom: 25px;
239
  }
 
240
 
241
 
242
  /* Match number inputs */
 
292
  tabs = st.tabs([
293
  "📊 Dashboard",
294
  "🎛️ Blend Designer",
 
295
  "⚙️ Optimization Engine",
296
+ "📤 Blend Comparison",
297
  "📚 Fuel Registry",
298
  "🧠 Model Insights"
299
  ])
300
 
 
301
 
302
+ def explode_blends_to_components(blends_df: pd.DataFrame,
303
+ n_components: int = 5,
304
+ keep_empty: bool = False,
305
+ blend_name_col: str = "blend_name") -> pd.DataFrame:
306
+ """
307
+ Convert a blends DataFrame into a components DataFrame.
308
+
309
+ Parameters
310
+ ----------
311
+ blends_df : pd.DataFrame
312
+ DataFrame with columns following the pattern:
313
+ Component1_fraction, Component1_Property1..Property10, Component1_unit_cost, ...
314
+ n_components : int
315
+ Number of components per blend (default 5).
316
+ blend_name_col : str
317
+ Column name in blends_df that stores the blend name.
318
+
319
+ Returns
320
+ -------
321
+ pd.DataFrame
322
+ components_df with columns:
323
+ ['blend_name', 'component_name', 'component_fraction',
324
+ 'property1', ..., 'property10', 'unit_cost']
325
+ """
326
+
327
+ components_rows = []
328
+ prop_names = [f"property{i}" for i in range(1, 11)]
329
+
330
+ for _, blend_row in blends_df.iterrows():
331
+ blend_name = blend_row.get(blend_name_col)
332
+ # Fallback if blend_name is missing/empty - keep index-based fallback
333
+ if not blend_name or str(blend_name).strip() == "":
334
+ # use the dataframe index + 1 to create a fallback name
335
+ blend_name = f"blend{int(blend_row.name) + 1}"
336
+
337
+ for i in range(1, n_components + 1):
338
+ # Build column keys
339
+ frac_col = f"Component{i}_fraction"
340
+ unit_cost_col = f"Component{i}_unit_cost"
341
+ prop_cols = [f"Component{i}_Property{j}" for j in range(1, 11)]
342
+
343
+ # Safely get values (if column missing, get NaN)
344
+ comp_frac = blend_row.get(frac_col, np.nan)
345
+ comp_unit_cost = blend_row.get(unit_cost_col, np.nan)
346
+ comp_props = [blend_row.get(pc, np.nan) for pc in prop_cols]
347
+
348
+ row = {
349
+ "blend_name": blend_name,
350
+ "component_name": f"{blend_name}_Component_{i}",
351
+ "component_fraction": comp_frac,
352
+ "unit_cost": comp_unit_cost
353
+ }
354
+ # add property1..property10
355
+ for j, v in enumerate(comp_props, start=1):
356
+ row[f"property{j}"] = v
357
+
358
+ components_rows.append(row)
359
+
360
+ components_df = pd.DataFrame(components_rows)
361
+
362
+ return components_df
363
+
364
+ # --- Updated add_blends (now also populates components) ---
365
+ def add_blends(df, db_path="eagleblend.db", n_components=5):
366
+ df = df.copy()
367
+
368
+ # 1) Ensure blend_name column
369
+ for col in list(df.columns):
370
+ low = col.strip().lower()
371
+ if low in ("blend_name", "blend name", "blendname"):
372
+ if col != "blend_name":
373
+ df = df.rename(columns={col: "blend_name"})
374
+ break
375
+ if "blend_name" not in df.columns:
376
+ df["blend_name"] = pd.NA
377
+
378
+ conn = sqlite3.connect(db_path)
379
+ cur = conn.cursor()
380
+
381
+ # 2) Determine next blend number
382
+ cur.execute("SELECT blend_name FROM blends WHERE blend_name LIKE 'blend%'")
383
+ nums = [int(m.group(1)) for (b,) in cur.fetchall() if (m := re.match(r"blend(\d+)$", str(b)))]
384
+ start_num = max(nums) if nums else 0
385
+
386
+ # 3) Fill missing blend_name
387
+ mask = df["blend_name"].isna() | (df["blend_name"].astype(str).str.strip() == "")
388
+ df.loc[mask, "blend_name"] = [f"blend{i}" for i in range(start_num + 1, start_num + 1 + mask.sum())]
389
+
390
+ # 4) Safe insert into blends
391
+ cur.execute("PRAGMA table_info(blends)")
392
+ db_cols = [r[1] for r in cur.fetchall()]
393
+ safe_df = df[[c for c in df.columns if c in db_cols]]
394
+ if not safe_df.empty:
395
+ safe_df.to_sql("blends", conn, if_exists="append", index=False)
396
+
397
+ # 5) Explode blends into components and insert into components table
398
+ components_df = explode_blends_to_components(df, n_components=n_components, keep_empty=False)
399
+ cur.execute("PRAGMA table_info(components)")
400
+ comp_cols = [r[1] for r in cur.fetchall()]
401
+ safe_components_df = components_df[[c for c in components_df.columns if c in comp_cols]]
402
+ if not safe_components_df.empty:
403
+ safe_components_df.to_sql("components", conn, if_exists="append", index=False)
404
+
405
+ conn.commit()
406
+ conn.close()
407
+
408
+ return {
409
+ "blends_inserted": int(safe_df.shape[0]),
410
+ "components_inserted": int(safe_components_df.shape[0])
411
+ }
412
+
413
+
414
+ # --- add_components function ---
415
+ def add_components(df, db_path="eagleblend.db"):
416
+ df = df.copy()
417
+
418
+ # Ensure blend_name exists
419
+ for col in list(df.columns):
420
+ low = col.strip().lower()
421
+ if low in ("blend_name", "blend name", "blendname"):
422
+ if col != "blend_name":
423
+ df = df.rename(columns={col: "blend_name"})
424
+ break
425
+ if "blend_name" not in df.columns:
426
+ df["blend_name"] = pd.NA
427
+
428
+ # Ensure component_name exists
429
+ if "component_name" not in df.columns:
430
+ df["component_name"] = pd.NA
431
+
432
+ conn = sqlite3.connect(db_path)
433
+ cur = conn.cursor()
434
+
435
+ # Fill missing component_name
436
+ mask = df["component_name"].isna() | (df["component_name"].astype(str).str.strip() == "")
437
+ df.loc[mask, "component_name"] = [
438
+ f"{bn}_Component_{i+1}"
439
+ for i, bn in enumerate(df["blend_name"].fillna("blend_unknown"))
440
+ ]
441
+
442
+ # Safe insert into components
443
+ cur.execute("PRAGMA table_info(components)")
444
+ db_cols = [r[1] for r in cur.fetchall()]
445
+ safe_df = df[[c for c in df.columns if c in db_cols]]
446
+ if not safe_df.empty:
447
+ safe_df.to_sql("components", conn, if_exists="append", index=False)
448
+
449
+ conn.commit()
450
+ conn.close()
451
+
452
+ return int(safe_df.shape[0])
453
+
454
+ def get_blends_overview(db_path: str = "eagleblend.db", last_n: int = 5) -> Dict[str, Any]:
455
+ """
456
+ Returns:
457
+ {
458
+ "max_saving": float | None, # raw numeric (PreOpt_Cost - Optimized_Cost)
459
+ "last_blends": pandas.DataFrame, # last_n rows of selected columns
460
+ "daily_counts": pandas.Series # counts per day, index = 'YYYY-MM-DD' (strings)
461
+ }
462
+ """
463
+ last_n = int(last_n)
464
+ comp_cols = [
465
+ "blend_name", "Component1_fraction", "Component2_fraction", "Component3_fraction",
466
+ "Component4_fraction", "Component5_fraction", "created_at"
467
+ ]
468
+ blend_props = [f"BlendProperty{i}" for i in range(1, 11)]
469
+ select_cols = comp_cols + blend_props
470
+ cols_sql = ", ".join(select_cols)
471
+
472
+ with sqlite3.connect(db_path) as conn:
473
+ # 1) scalar: max saving
474
+ max_saving = conn.execute(
475
+ "SELECT MAX(PreOpt_Cost - Optimized_Cost) "
476
+ "FROM blends "
477
+ "WHERE PreOpt_Cost IS NOT NULL AND Optimized_Cost IS NOT NULL"
478
+ ).fetchone()[0]
479
+
480
+ # 2) last N rows (only selected columns)
481
+ q_last = f"""
482
+ SELECT {cols_sql}
483
+ FROM blends
484
+ ORDER BY id DESC
485
+ LIMIT {last_n}
486
+ """
487
+ df_last = pd.read_sql_query(q_last, conn)
488
+
489
+ # 3) daily counts (group by date)
490
+ q_counts = """
491
+ SELECT date(created_at) AS day, COUNT(*) AS cnt
492
+ FROM blends
493
+ WHERE created_at IS NOT NULL
494
+ GROUP BY day
495
+ ORDER BY day DESC
496
+ """
497
+ df_counts = pd.read_sql_query(q_counts, conn)
498
+
499
+ # Convert counts to a Series with day strings as index (fast, small memory)
500
+ if not df_counts.empty:
501
+ daily_counts = pd.Series(df_counts["cnt"].values, index=df_counts["day"].astype(str))
502
+ daily_counts.index.name = "day"
503
+ daily_counts.name = "count"
504
+ else:
505
+ daily_counts = pd.Series(dtype=int, name="count")
506
+
507
+ return {"max_saving": max_saving, "last_blends": df_last, "daily_counts": daily_counts}
508
+
509
+
510
+ def get_activity_logs(db_path="eagleblend.db", timeframe="today", activity_type=None):
511
+ """
512
+ Get counts of activities from the activity_log table within a specified timeframe.
513
+
514
+ Args:
515
+ db_path (str): Path to the SQLite database file.
516
+ timeframe (str): Time period to filter ('today', 'this_week', 'this_month', or 'custom').
517
+ activity_type (str): Specific activity type to return count for. If None, return all counts.
518
 
519
+ Returns:
520
+ dict: Dictionary with counts per activity type OR a single integer if activity_type is specified.
521
+ """
522
+ # Calculate time filter
523
+ now = datetime.now()
524
+ if timeframe == "today":
525
+ start_time = now.replace(hour=0, minute=0, second=0, microsecond=0)
526
+ elif timeframe == "this_week":
527
+ start_time = now - timedelta(days=now.weekday()) # Monday of this week
528
+ start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0)
529
+ elif timeframe == "this_month":
530
+ start_time = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
531
+ else:
532
+ raise ValueError("Invalid timeframe. Use 'today', 'this_week', or 'this_month'.")
533
+
534
+ # Query database
535
+ conn = sqlite3.connect(db_path)
536
+ query = f"""
537
+ SELECT activity_type, COUNT(*) as count
538
+ FROM activity_log
539
+ WHERE timestamp >= ?
540
+ GROUP BY activity_type
541
+ """
542
+ df_counts = pd.read_sql_query(query, conn, params=(start_time.strftime("%Y-%m-%d %H:%M:%S"),))
543
+ conn.close()
544
+
545
+ # Convert to dictionary
546
+ counts_dict = dict(zip(df_counts["activity_type"], df_counts["count"]))
547
+
548
+ # If specific activity requested
549
+ if activity_type:
550
+ return counts_dict.get(activity_type, 0)
551
+
552
+ return counts_dict
553
+
554
+ # print(get_activity_logs(timeframe="today")) # All activities today
555
+ # print(get_activity_logs(timeframe="this_week")) # All activities this week
556
+ # print(get_activity_logs(timeframe="today", activity_type="optimization")) # Only optimization count today
557
+
558
+ # result = get_activity_logs(timeframe="this_week")
559
+ # result['optimization']
560
+ # result['prediction']
561
+
562
+
563
+ def get_model(db_path="eagleblend.db"):
564
+ """
565
+ Fetch the last model from the models_registry table.
566
+
567
+ Returns:
568
+ pandas.Series: A single row containing the last model's data.
569
+ """
570
+ conn = sqlite3.connect(db_path)
571
+ query = "SELECT * FROM models_registry ORDER BY id DESC LIMIT 1"
572
+ df_last = pd.read_sql_query(query, conn)
573
+ conn.close()
574
+
575
+ if not df_last.empty:
576
+ return df_last.iloc[0] # Return as a Series so you can access columns easily
577
+ else:
578
+ return None
579
+
580
+
581
+ # last_model = get_model()
582
+ # if last_model is not None:
583
+ # print("R2 Score:", last_model["R2_Score"])
584
+
585
+
586
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
587
+ # Dashboard Tab
588
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
589
+ with tabs[0]:
590
+ import math
591
+ import plotly.graph_objects as go
592
+
593
+ # NOTE: Assuming these functions are defined elsewhere in your application
594
+ # from your_utils import get_model, get_activity_logs, get_blends_overview
595
+
596
+ # ---------- formatting helpers ----------
597
+ def fmt_int(x):
598
+ try:
599
+ return f"{int(x):,}"
600
+ except Exception:
601
+ return "0"
602
+
603
+ def fmt_pct_from_r2(r2):
604
+ if r2 is None:
605
+ return "—"
606
+ try:
607
+ v = float(r2)
608
+ if v <= 1.5:
609
+ v *= 100.0
610
+ return f"{v:.1f}%"
611
+ except Exception:
612
+ return "—"
613
+
614
+ def fmt_currency(x):
615
+ try:
616
+ return f"${float(x):,.2f}"
617
+ except Exception:
618
+ return "—"
619
+
620
+ # ---------- pull live data (this_week only) ----------
621
+ # This block is assumed to be correct and functional
622
+ try:
623
+ last_model = get_model()
624
+ except Exception as e:
625
+ last_model = None
626
+ st.warning(f"Model lookup failed: {e}")
627
+
628
+ try:
629
+ activity_counts = get_activity_logs(timeframe="this_week")
630
+ except Exception as e:
631
+ activity_counts = {}
632
+ st.warning(f"Activity log lookup failed: {e}")
633
+
634
+ try:
635
+ overview = get_blends_overview(last_n=5)
636
+ except Exception as e:
637
+ overview = {"max_saving": None, "last_blends": pd.DataFrame(), "daily_counts": pd.Series(dtype=int)}
638
+ st.warning(f"Blends overview failed: {e}")
639
+
640
+
641
+ r2_display = fmt_pct_from_r2(None if last_model is None else last_model.get("R2_Score"))
642
+ preds = fmt_int(activity_counts.get("prediction", 0))
643
+ opts = fmt_int(activity_counts.get("optimization", 0))
644
+ max_saving_display = fmt_currency(overview.get("max_saving", None))
645
+
646
+ # ---------- KPI cards ----------
647
+ # FIXED: Replaced st.subheader with styled markdown for consistent color
648
+ st.markdown('<h2 style="color:#4a2f1f; font-size:1.75rem;">Performance Summary</h2>', unsafe_allow_html=True)
649
+ k1, k2, k3, k4 = st.columns(4)
650
+ with k1:
651
+ st.markdown(f"""
652
+ <div class="metric-card" style="padding:10px;">
653
  <div class="metric-label">Model Accuracy</div>
654
+ <div class="metric-value" style="font-size:1.3rem;">{r2_display}</div>
655
+ <div class="metric-delta">R² (latest)</div>
656
  </div>
657
  """, unsafe_allow_html=True)
658
+ with k2:
659
+ st.markdown(f"""
660
+ <div class="metric-card" style="padding:10px;">
 
661
  <div class="metric-label">Predictions Made</div>
662
+ <div class="metric-value" style="font-size:1.3rem;">{preds}</div>
663
+ <div class="metric-delta">This Week</div>
664
  </div>
665
  """, unsafe_allow_html=True)
666
+ with k3:
667
+ st.markdown(f"""
668
+ <div class="metric-card" style="padding:10px;">
 
669
  <div class="metric-label">Optimizations</div>
670
+ <div class="metric-value" style="font-size:1.3rem;">{opts}</div>
671
  <div class="metric-delta">This Week</div>
672
  </div>
673
  """, unsafe_allow_html=True)
674
+ with k4:
675
+ st.markdown(f"""
676
+ <div class="metric-card" style="padding:10px;">
677
+ <div class="metric-label">Highest Cost Savings</div>
678
+ <div class="metric-value" style="font-size:1.3rem;">{max_saving_display}</div>
679
+ <div class="metric-delta">Per unit fuel</div>
 
680
  </div>
681
  """, unsafe_allow_html=True)
682
 
683
+ st.markdown('<div style="height:8px;"></div>', unsafe_allow_html=True)
684
 
685
+ # ---------- Floating "How to Use" (bigger button + inline content) + compact CSS ----------
686
+ st.markdown("""
687
+ <style>
688
+ /* Floating help - larger button and panel */
689
+ #help-toggle{display:none;}
690
+ .help-button{
691
+ position:fixed; right:25px; bottom:25px; z-index:9999;
692
+ background:#8B4513; color:#FFD700; padding:16px 22px; font-size:17px;
693
+ border-radius:18px; font-weight:900; box-shadow:0 8px 22px rgba(0,0,0,0.2); cursor:pointer;
694
+ border:0;
695
+ }
696
+ .help-panel{
697
+ position:fixed; right:25px; bottom:100px; z-index:9998;
698
+ width:520px; max-height:70vh; overflow-y:auto;
699
+ background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
700
+ border:1px solid #CFB53B; border-radius:12px; padding:20px; box-shadow:0 14px 34px rgba(0,0,0,0.22);
701
+ color:#4a2f1f; transform: translateY(12px); opacity:0; visibility:hidden; transition: all .22s ease-in-out;
702
+ }
703
+ #help-toggle:checked + label.help-button + .help-panel{
704
+ opacity:1; visibility:visible; transform: translateY(0);
705
+ }
706
+ .help-panel .head{display:flex; justify-content:space-between; align-items:center; margin-bottom:12px}
707
+ .help-panel .title{font-weight:900; color:#654321; font-size:16px}
708
+ .help-close{background:#8B4513; color:#FFD700; padding:6px 10px; border-radius:8px; cursor:pointer; font-weight:800}
709
+ .help-body{font-size:14.5px; color:#4a2f1f; line-height:1.5}
710
+ .help-body b {color: #654321;}
711
+
712
+ /* compact recent blends styles - improved font sizes */
713
+ .recent-compact { padding-left:6px; padding-right:6px; }
714
+ .compact-card{
715
+ background: linear-gradient(180deg,#FFF8E1 0%, #FFF6EA 100%);
716
+ border:1px solid #E3C77A; border-radius:8px; padding:10px; margin-bottom:8px; color:#654321;
717
+ box-shadow: 0 2px 6px rgba(0,0,0,0.05);
718
+ }
719
+ .compact-top{display:flex; justify-content:space-between; align-items:center; margin-bottom:8px}
720
+ .compact-name{font-weight:800; font-size:15px}
721
+ .compact-ts{font-size:12px; color:#8B4513; opacity:0.95; font-weight:700}
722
+ .comp-pills{font-size:12.5px; margin-bottom:8px}
723
+ .comp-pill{
724
+ display:inline-block; padding:3px 8px; margin-right:6px; margin-bottom: 4px; border-radius:999px;
725
+ background:rgba(139,69,19,0.06); border:1px solid rgba(139,69,19,0.12);
726
+ font-weight:700; color:#654321;
727
+ }
728
+ .props-inline{
729
+ font-size:12px; color:#4a2f1f; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
730
+ }
731
+ .props-inline small{ font-size:11px; color:#4a2f1f; opacity:0.95; margin-right:8px; }
732
+ </style>
733
 
734
+ <input id="help-toggle" type="checkbox" />
735
+ <label for="help-toggle" class="help-button">💬 How to Use</label>
736
+
737
+ <div class="help-panel" aria-hidden="true">
738
+ <div class="head">
739
+ <div class="title">How to Use the Optimizer</div>
740
+ <label for="help-toggle" class="help-close">Close</label>
741
+ </div>
742
+ <div class="help-body">
743
+ <p><b>Performance Cards:</b> These show key metrics at a glance. "Model Accuracy" is the latest R² score. "Predictions" and "Optimizations" cover this week's activity. If a card shows "—", the underlying data may be missing.</p>
744
+ <p><b>Blend Entries Chart:</b> This chart tracks how many new blends are created each day. Spikes can mean heavy usage or batch imports, while gaps might point to data ingestion issues.</p>
745
+ <p><b>Recent Blends:</b> This is a live list of the newest blends. Each card displays the blend's name, creation time, component mix (C1-C5), and key properties (P1-P10). You can use the name and timestamp to find the full record in the database.</p>
746
+ <p><b>Operational Tips:</b> For best results, use consistent naming for your blends. Ensure your data includes cost fields for savings to be calculated correctly. Consider retraining your model if its accuracy drops.</p>
747
+ </div>
748
+ </div>
749
+ """, unsafe_allow_html=True)
750
+
751
+ # ---------- Main split (adjusted for better balance) ----------
752
+ left_col, right_col = st.columns([0.55, 0.45])
753
+
754
+ # --- LEFT: Blend entries line chart ---
755
+ with left_col:
756
+ # FIXED: Replaced st.subheader with styled markdown for consistent color
757
+ st.markdown('<h2 style="color:#4a2f1f; font-size:1.75rem;">Blend Entries Per Day</h2>', unsafe_allow_html=True)
758
+
759
+ # Using DUMMY DATA as per original snippet for illustration
760
+ today = pd.Timestamp.today().normalize()
761
+ dates = pd.date_range(end=today, periods=14)
762
+ ddf = pd.DataFrame({"day": dates, "Blends": np.array([2,3,1,5,6,2,4,9,3,4,2,1,5,6])})
763
+
764
+ fig_daily = go.Figure()
765
+ fig_daily.add_trace(go.Scatter(
766
+ x=ddf["day"], y=ddf["Blends"],
767
+ mode="lines+markers", line=dict(width=3, color="#8B4513"),
768
+ marker=dict(size=6), name="Blends"
769
+ ))
770
+ fig_daily.add_trace(go.Scatter(
771
+ x=ddf["day"], y=ddf["Blends"],
772
+ mode="lines", line=dict(width=0), fill="tozeroy",
773
+ fillcolor="rgba(207,181,59,0.23)", showlegend=False
774
+ ))
775
+ fig_daily.update_layout(
776
+ title="Recent Blend Creation (preview)",
777
+ xaxis_title="Date", yaxis_title="Number of Blends",
778
+ plot_bgcolor="white", paper_bgcolor="white", # Set background to white
779
+ margin=dict(t=40, r=10, b=36, l=50), # Tighter margins
780
+ font=dict(color="#4a2f1f") # Ensure text color is not white
781
+ )
782
+ fig_daily.update_xaxes(gridcolor="rgba(139,69,19,0.12)", tickfont=dict(color="#654321"))
783
+ fig_daily.update_yaxes(gridcolor="rgba(139,69,19,0.12)", tickfont=dict(color="#654321"))
784
+ st.plotly_chart(fig_daily, use_container_width=True)
785
 
786
+ # st.caption("Chart preview uses dummy data. To show live counts, uncomment the LIVE DATA block in the code.")
787
 
788
+ # --- RIGHT: Compact Recent Blends (with larger fonts and clear timestamp) ---
789
+ with right_col:
790
+ st.markdown('<div class="recent-compact">', unsafe_allow_html=True)
791
+ st.markdown('<div style="font-size: 1.15rem; font-weight:800; color:#654321; margin-bottom:12px;">🗒️ Recent Blends</div>', unsafe_allow_html=True)
792
+
793
+ df_recent = overview['last_blends'] #get("last_blends", pd.DataFrame())
794
+ if df_recent is None or df_recent.empty:
795
+ st.info("No blends yet. Start blending today!")
796
+ else:
797
+ if "created_at" in df_recent.columns and not pd.api.types.is_datetime64_any_dtype(df_recent["created_at"]):
798
+ with pd.option_context('mode.chained_assignment', None):
799
+ df_recent["created_at"] = pd.to_datetime(df_recent["created_at"], errors="coerce")
800
+
801
+ for _, row in df_recent.iterrows():
802
+ name = str(row.get("blend_name", "Untitled"))
803
+ created = row.get("created_at", "")
804
+ ts = "" if pd.isna(created) else pd.to_datetime(created).strftime("%Y-%m-%d %H:%M:%S")
805
+
806
+ comp_html = ""
807
+ for i in range(1, 6):
808
+ key = f"Component{i}_fraction"
809
+ val = row.get(key)
810
+ if val is None or (isinstance(val, float) and math.isnan(val)) or val == 0:
811
+ continue
812
+ comp_html += f'<span class="comp-pill">C{i}: {float(val)*100:.0f}%</span>'
813
+
814
+ props = []
815
+ for j in range(1, 11):
816
+ pj = row.get(f"BlendProperty{j}")
817
+ if pj is not None and not (isinstance(pj, float) and math.isnan(pj)):
818
+ props.append(f"P{j}:{float(pj):.3f}")
819
+ props_html = " · ".join(props) if props else "No properties available."
820
+
821
+
822
+ st.markdown(f"""
823
+ <div class="compact-card">
824
+ <div class="compact-top">
825
+ <div class="compact-name">{name}</div>
826
+ <div class="compact-ts">{ts}</div>
827
+ </div>
828
+ <div class="comp-pills">{comp_html}</div>
829
+ <div class="props-inline"><small>{props_html}</small></div>
830
+ </div>
831
+ """, unsafe_allow_html=True)
832
+
833
+ st.markdown('</div>', unsafe_allow_html=True)
834
+
835
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
836
+ # Blend Designer Tab
837
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
838
 
839
+ from inference import EagleBlendPredictor # Add this import at the top of your main script
 
 
840
 
841
+ # --- Add these new functions to your functions section ---
842
 
843
+ @st.cache_data
844
+ def get_components_from_db(db_path="eagleblend.db") -> pd.DataFrame:
845
+ """Fetches component data, sorted by the most recent entries."""
846
+ with sqlite3.connect(db_path) as conn:
847
+ # Assuming 'id' or a timestamp column indicates recency. Let's use 'id'.
848
+ query = "SELECT * FROM components ORDER BY id DESC"
849
+ df = pd.read_sql_query(query, conn)
850
+ return df
851
 
852
+ def log_activity(activity_type: str, details: str = "", db_path="eagleblend.db"):
853
+ """Logs an activity to the activity_log table."""
854
+ try:
855
+ with sqlite3.connect(db_path) as conn:
856
+ cur = conn.cursor()
857
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
858
+ cur.execute(
859
+ "INSERT INTO activity_log (timestamp, activity_type) VALUES (?, ?)",
860
+ (timestamp, activity_type)
861
+ )
862
+ conn.commit()
863
+ except Exception as e:
864
+ st.error(f"Failed to log activity: {e}")
865
+
866
+ # Instantiate the predictor once
867
+ if 'predictor' not in st.session_state:
868
+ st.session_state.predictor = EagleBlendPredictor()
869
 
870
  with tabs[1]:
871
+ # --- State Initialization ---
872
+ if 'prediction_made' not in st.session_state:
873
+ st.session_state.prediction_made = False
874
+ if 'prediction_results' not in st.session_state:
875
+ st.session_state.prediction_results = None
876
+ if 'preopt_cost' not in st.session_state:
877
+ st.session_state.preopt_cost = 0.0
878
+ if 'last_input_data' not in st.session_state:
879
+ st.session_state.last_input_data = {}
880
+
881
+ # --- Prediction & Saving Logic ---
882
+ def handle_prediction():
883
+ """
884
+ Gathers data from UI, formats it, runs prediction, and stores results.
885
+ """
886
+ log_activity("prediction", "User ran a new blend prediction.")
887
+
888
+ fractions = []
889
+ properties_by_comp = [[] for _ in range(5)]
890
+ unit_costs = []
891
+
892
+ # 1. Gather all inputs from session state
893
+ for i in range(5):
894
+ frac = st.session_state.get(f"c{i}_fraction", 0.0)
895
+ fractions.append(frac)
896
+ unit_costs.append(st.session_state.get(f"c{i}_cost", 0.0))
897
+ for j in range(1, 11):
898
+ prop = st.session_state.get(f"c{i}_prop{j}", 0.0)
899
+ properties_by_comp[i].append(prop)
900
+
901
+ # 2. Validate weights
902
+ if abs(sum(fractions) - 1.0) > 0.01:
903
+ st.warning("⚠️ Total of component fractions must sum to 1.0.")
904
+ st.session_state.prediction_made = False
905
+ return
906
+
907
+ # 3. Format DataFrame for the model
908
+ model_input_data = {"blend_name": [st.session_state.get("blend_name", "Untitled Blend")]}
909
+ # Add fractions first
910
+ for i in range(5):
911
+ model_input_data[f'Component{i+1}_fraction'] = [fractions[i]]
912
+ # Add properties in the required order (interleaved)
913
+ for j in range(10): # Property1, Property2, ...
914
+ for i in range(5): # Component1, Component2, ...
915
+ col_name = f'Component{i+1}_Property{j+1}'
916
+ model_input_data[col_name] = [properties_by_comp[i][j]]
917
+
918
+ df_model = pd.DataFrame(model_input_data)
919
+
920
+ # 4. Run prediction
921
+ predictor = st.session_state.predictor
922
+ results = predictor.predict_all(df_model.drop(columns=['blend_name']))
923
+ st.session_state.prediction_results = results[0] # Get the first (and only) row of results
924
+
925
+ # 5. Calculate cost
926
+ st.session_state.preopt_cost = sum(f * c for f, c in zip(fractions, unit_costs))
927
+
928
+ # 6. Store inputs for saving/downloading
929
+ st.session_state.last_input_data = model_input_data
930
+
931
+ st.session_state.prediction_made = True
932
+ st.success("Prediction complete!")
933
+
934
+ def handle_save_prediction():
935
+ """Formats the last prediction's data and saves it to the database."""
936
+ if not st.session_state.get('prediction_made', False):
937
+ st.error("Please run a prediction before saving.")
938
+ return
939
+
940
+ # Prepare DataFrame in the format expected by `add_blends`
941
+ save_df_data = st.session_state.last_input_data.copy()
942
+
943
+ # Add blend properties and cost
944
+ for i, prop_val in enumerate(st.session_state.prediction_results, 1):
945
+ save_df_data[f'BlendProperty{i}'] = [prop_val]
946
+
947
+ save_df_data['PreOpt_Cost'] = [st.session_state.preopt_cost]
948
+
949
+ # Add unit costs
950
+ for i in range(5):
951
+ save_df_data[f'Component{i+1}_unit_cost'] = st.session_state.get(f'c{i}_cost', 0.0)
952
+
953
+ save_df = pd.DataFrame(save_df_data)
954
+
955
+ try:
956
+ result = add_blends(save_df)
957
+ log_activity("save_prediction", f"Saved blend: {save_df['blend_name'].iloc[0]}")
958
+ st.success(f"Successfully saved blend '{save_df['blend_name'].iloc[0]}' to the database!")
959
+ except Exception as e:
960
+ st.error(f"Failed to save blend: {e}")
961
+
962
+
963
+ # --- UI Rendering ---
964
  col_header = st.columns([0.8, 0.2])
965
  with col_header[0]:
966
  st.subheader("🎛️ Blend Designer")
967
  with col_header[1]:
968
+ batch_blend = st.checkbox("Batch Blend Mode", value=False, key="batch_blend_mode")
969
+
 
 
 
 
 
 
 
 
 
 
 
970
  if batch_blend:
971
  st.subheader("📤 Batch Processing")
972
  uploaded_file = st.file_uploader("Upload CSV File", type=["csv"], key="Batch_upload")
973
+ if uploaded_file:
974
+ st.info("Batch processing functionality can be implemented here.")
975
+ # Add batch processing logic here
 
 
 
 
 
 
 
 
 
 
976
  else:
977
+ # --- Manual Blend Designer UI ---
978
+ all_components_df = get_components_from_db()
979
+ # st.text_input("Blend Name", "My New Blend", key="blend_name", help="Give your blend a unique name before saving.")
980
+ # st.markdown("---")
981
+
982
+ for i in range(5):
983
+ # Unique keys for each widget within the component expander
984
+ select_key = f"c{i}_select"
985
+ name_key = f"c{i}_name"
986
+ frac_key = f"c{i}_fraction"
987
+ cost_key = f"c{i}_cost"
988
+
989
+ # Check if a selection from dropdown was made
990
+ if select_key in st.session_state and st.session_state[select_key] != "---":
991
+ selected_name = st.session_state[select_key]
992
+ comp_data = all_components_df[all_components_df['component_name'] == selected_name].iloc[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993
 
994
+ # Auto-populate session state values
995
+ st.session_state[name_key] = comp_data['component_name']
996
+ st.session_state[frac_key] = comp_data.get('component_fraction', 0.2)
997
+ st.session_state[cost_key] = comp_data.get('unit_cost', 0.0)
998
+ for j in range(1, 11):
999
+ prop_key = f"c{i}_prop{j}"
1000
+ st.session_state[prop_key] = comp_data.get(f'property{j}', 0.0)
1001
+
1002
+ # Reset selectbox to avoid re-triggering
1003
+ st.session_state[select_key] = "---"
1004
+
1005
+ with st.expander(f"**Component {i+1}**", expanded=(i==0)):
1006
+ # --- This is the placeholder for your custom filter ---
1007
+ # Example: Only show components ending with a specific number
1008
+ # filter_condition = all_components_df['component_name'].str.endswith(str(i + 1))
1009
+ # For now, we show all components
1010
+ filter_condition = pd.Series([True] * len(all_components_df), index=all_components_df.index)
1011
+
1012
+ filtered_df = all_components_df[filter_condition]
1013
+ #component_options = ["---"] + filtered_df['component_name'].tolist()
1014
+ component_options = ["---"] + [m for m in filtered_df['component_name'].tolist() if m.endswith(f"Component_{i+1}") ]
1015
+
1016
+ st.selectbox(
1017
+ "Load from Registry",
1018
+ options=component_options,
1019
+ key=select_key,
1020
+ help="Select a saved component to auto-populate its properties."
1021
+ )
1022
 
1023
+ c1, c2, c3 = st.columns([1.5, 2, 2])
1024
+ with c1:
1025
+ st.text_input("Component Name", key=name_key)
1026
+ st.number_input("Fraction", min_value=0.0, max_value=1.0, step=0.01, key=frac_key, format="%.3f")
1027
+ st.number_input("Unit Cost ($)", min_value=0.0, step=0.01, key=cost_key, format="%.2f")
1028
+ with c2:
1029
+ for j in range(1, 6):
1030
+ st.number_input(f"Property {j}", key=f"c{i}_prop{j}", format="%.4f")
1031
+ with c3:
1032
+ for j in range(6, 11):
1033
+ st.number_input(f"Property {j}", key=f"c{i}_prop{j}", format="%.4f")
1034
+
1035
+ st.markdown('<div style="height:10px;"></div>', unsafe_allow_html=True)
1036
+ # st.button("🧪 Predict Blended Properties", on_click=handle_prediction, use_container_width=True, type="primary")
1037
+ # --- FIX: Changed button call to prevent page jumping ---
1038
+ if st.button("🧪 Predict Blended Properties", use_container_width=False, type="primary"):
1039
+ handle_prediction()
1040
+
1041
+ # --- Results Section ---
1042
+ if st.session_state.get('prediction_made', False):
1043
  st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
1044
+ st.subheader("📈 Prediction Results")
1045
+
1046
+ # KPI Cards for Cost and Blend Properties
1047
+ cost_val = st.session_state.get('preopt_cost', 0.0)
1048
+ results_array = st.session_state.get('prediction_results', np.zeros(10))
1049
+
1050
+ st.markdown(f"""
1051
+ <div class="metric-card" style="border-color: #8B4513; background: #FFF8E1;">
1052
+ <div class="metric-label">Predicted Blend Cost</div>
1053
+ <div class="metric-value" style="color: #654321;">${cost_val:,.2f}</div>
1054
+ <div class="metric-delta">Per unit fuel</div>
1055
+ </div>
1056
+ """, unsafe_allow_html=True)
1057
+ st.markdown('<div style="height:15px;"></div>', unsafe_allow_html=True)
1058
 
1059
+ kpi_cols = st.columns(5)
1060
+ for i in range(10):
1061
+ with kpi_cols[i % 5]:
1062
+ st.markdown(f"""
1063
+ <div class="metric-card" style="margin-bottom: 10px;">
1064
+ <div class="metric-label">Blend Property {i+1}</div>
1065
+ <div class="metric-value">{results_array[i]:.4f}</div>
1066
+ </div>
1067
+ """, unsafe_allow_html=True)
1068
+
1069
+ st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
1070
+ st.subheader("📊 Visualizations")
1071
 
1072
+ v1, v2 = st.columns(2)
1073
+ with v1:
1074
+ # Pie Chart for fractions
1075
+ fractions = [st.session_state.get(f"c{i}_fraction", 0.0) for i in range(5)]
1076
+ labels = [st.session_state.get(f"c{i}_name", f"Component {i+1}") for i in range(5)]
1077
+ pie_fig = px.pie(
1078
+ values=fractions, names=labels, title="Component Fractions",
1079
+ hole=0.4, color_discrete_sequence=px.colors.sequential.YlOrBr_r
 
1080
  )
1081
+ pie_fig.update_traces(textposition='inside', textinfo='percent+label')
1082
+ st.plotly_chart(pie_fig, use_container_width=True)
1083
+
1084
+ with v2:
1085
+ # Bar Chart for property comparison
1086
+ prop_to_view = st.selectbox(
1087
+ "Select Property to Visualize",
1088
+ options=[f"Property{j}" for j in range(1, 11)],
1089
+ key="viz_property_select"
1090
  )
1091
+ prop_idx = int(prop_to_view.replace("Property", "")) - 1
1092
+
1093
+ bar_values = [st.session_state.get(f"c{i}_prop{prop_idx+1}", 0.0) for i in range(5)]
1094
+ blend_prop_value = results_array[prop_idx]
1095
+
1096
+ bar_labels = [f"Comp {i+1}" for i in range(5)] + ["Blend"]
1097
+ all_values = bar_values + [blend_prop_value]
1098
+
1099
+ bar_df = pd.DataFrame({"Component": bar_labels, "Value": all_values})
1100
+
1101
+ bar_fig = px.bar(
1102
+ bar_df, x="Component", y="Value", title=f"Comparison for {prop_to_view}",
1103
+ color="Component",
1104
+ color_discrete_map={"Blend": "#654321"} # Highlight the blend property
1105
  )
1106
+ bar_fig.update_layout(showlegend=False)
1107
+ st.plotly_chart(bar_fig, use_container_width=True)
1108
 
1109
+ # --- Save and Download Buttons ---
1110
+
1111
+
1112
+ # --- FIX: New layout for saving and downloading ---
1113
+ save_col, download_col = st.columns(2)
1114
+
1115
+ with save_col:
1116
+ # Move Blend Name input here
1117
+ st.text_input(
1118
+ "Blend Name for Saving",
1119
+ "My New Blend",
1120
+ key="blend_name",
1121
+ help="Give your blend a unique name before saving."
 
 
 
 
1122
  )
1123
+ st.button(
1124
+ "💾 Save Prediction to Database",
1125
+ on_click=handle_save_prediction,
1126
+ use_container_width=True
 
1127
  )
1128
+
1129
+ with download_col:
1130
+ # Prepare CSV for download
1131
+ download_df = pd.DataFrame(st.session_state.last_input_data)
1132
+ # Use the blend_name from the input field for the file name
1133
+ file_name = st.session_state.get('blend_name', 'blend_results').replace(' ', '_')
1134
+ for i in range(5): # Add unit costs
1135
+ download_df[f'Component{i+1}_unit_cost'] = st.session_state.get(f'c{i}_cost', 0.0)
1136
+ for i, res in enumerate(results_array, 1): # Add results
1137
+ download_df[f'BlendProperty{i}'] = res
1138
 
1139
+ csv_data = download_df.to_csv(index=False).encode('utf-8')
 
 
 
 
 
 
 
1140
 
1141
+ st.download_button(
1142
+ label="📥 Download Results as CSV",
1143
+ data=csv_data,
1144
+ file_name=f"{file_name}.csv",
1145
+ mime='text/csv',
1146
+ use_container_width=True,
1147
+ # Move download button down slightly to align with save button
1148
+ help="Download all inputs and predicted outputs to a CSV file."
1149
+ )
1150
+ # This empty markdown is a trick to add vertical space
1151
+ st.markdown('<div style="height: 36px;"></div>', unsafe_allow_html=True)
1152
+
1153
+ # --- Floating "How to Use" button ---
1154
+ st.markdown("""
1155
+ <style>
1156
+ #help-toggle-designer{display:none;}
1157
+ .help-button-designer{
1158
+ position:fixed; right:25px; bottom:25px; z-index:999;
1159
+ background:#8B4513; color:#FFD700; padding:12px 18px;
1160
+ border-radius:50px; font-weight:bold; box-shadow:0 4px 12px rgba(0,0,0,0.2);
1161
+ cursor:pointer; border:0;
1162
+ }
1163
+ .help-panel-designer{
1164
+ display:none; position:fixed; right:25px; bottom:90px; z-index:998;
1165
+ width:450px; background: #FFFDF5; border:1px solid #CFB53B;
1166
+ border-radius:12px; padding:20px; box-shadow:0 8px 24px rgba(0,0,0,0.2);
1167
+ color:#4a2f1f;
1168
+ }
1169
+ #help-toggle-designer:checked ~ .help-panel-designer{display:block;}
1170
+ </style>
1171
+ <input id="help-toggle-designer" type="checkbox" />
1172
+ <label for="help-toggle-designer" class="help-button-designer">💬 How to Use</label>
1173
+ <div class="help-panel-designer">
1174
+ <h4 style="color:#654321; margin-top:0;">Using the Blend Designer</h4>
1175
+ <p><b>1. Name Your Blend:</b> Start by giving your new blend a unique name.</p>
1176
+ <p><b>2. Configure Components:</b> For each of the 5 components, you can either:</p>
1177
+ <ul>
1178
+ <li><b>Load from Registry:</b> Select a pre-saved component from the dropdown to automatically fill in all its properties.</li>
1179
+ <li><b>Manual Entry:</b> Manually type in the component name, its fraction in the blend, its unit cost, and its 10 physical properties.</li>
1180
+ </ul>
1181
+ <p><b>3. Predict:</b> Once all components are defined and their fractions sum to 1.0, click the <b>Predict</b> button. This will calculate the final blend's properties and cost.</p>
1182
+ <p><b>4. Analyze Results:</b> Review the KPI cards for the predicted properties and cost. Use the charts to visualize the blend's composition and compare component properties against the final blend.</p>
1183
+ <p><b>5. Save & Download:</b> If you are satisfied with the result, you can save the complete blend recipe to the database or download all the input and output data as a CSV file.</p>
1184
+ </div>
1185
+ """, unsafe_allow_html=True)
1186
+
1187
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
1188
+ # Optimization Engine Tab
1189
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
1190
 
1191
  with tabs[2]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1192
  st.subheader("⚙️ Optimization Engine")
1193
 
1194
  # Pareto frontier demo
 
1255
  )
1256
  st.plotly_chart(fig4, use_container_width=True)
1257
 
1258
+ # -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
1259
+ # Blend Comparison Tab
1260
+ # -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
1261
+
1262
+ with tabs[3]:
1263
+ st.subheader("📤 Nothing FOr NOw")
1264
+ # uploaded_file = st.file_uploader("Upload CSV File", type=["csv"])
1265
+
1266
+ # if uploaded_file:
1267
+ # df = pd.read_csv(uploaded_file)
1268
+ # st.success("File uploaded successfully")
1269
+ # st.dataframe(df.head())
1270
+
1271
+ # if st.button("⚙️ Run Batch Prediction"):
1272
+ # result_df = df.copy()
1273
+ # # result_df["Predicted_Property"] = df.apply(
1274
+ # # lambda row: run_dummy_prediction(row.values[:5], row.values[5:10]), axis=1
1275
+ # # )
1276
+ # st.success("Batch prediction completed")
1277
+ # st.dataframe(result_df.head())
1278
+ # csv = result_df.to_csv(index=False).encode("utf-8")
1279
+ # st.download_button("Download Results", csv, "prediction_results.csv", "text/csv")
1280
+
1281
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
1282
+ # Fuel Registry Tab
1283
+ # ---------------------------------------------------------------------------------------------------------------------------------------------
1284
+
1285
+
1286
+ def load_data(table_name: str, db_path="eagleblend.db") -> pd.DataFrame:
1287
+ """Loads data from a specified table in the database."""
1288
+ try:
1289
+ conn = sqlite3.connect(db_path)
1290
+ # Assuming each table has a unique ID column as the first column
1291
+ query = f"SELECT * FROM {table_name}"
1292
+ df = pd.read_sql_query(query, conn)
1293
+ return df
1294
+ except Exception as e:
1295
+ st.error(f"Failed to load data from table '{table_name}': {e}")
1296
+ return pd.DataFrame()
1297
+
1298
+ def delete_records(table_name: str, ids_to_delete: list, id_column: str, db_path="eagleblend.db"):
1299
+ """Deletes records from a table based on a list of IDs."""
1300
+ if not ids_to_delete:
1301
+ return
1302
+ conn = sqlite3.connect(db_path)
1303
+ cur = conn.cursor()
1304
+ try:
1305
+ placeholders = ','.join('?' for _ in ids_to_delete)
1306
+ query = f"DELETE FROM {table_name} WHERE {id_column} IN ({placeholders})"
1307
+ cur.execute(query, ids_to_delete)
1308
+ conn.commit()
1309
+ finally:
1310
+ conn.close()
1311
 
1312
+ @st.cache_data
1313
+ def get_template(file_path):
1314
+ """Loads a template file into bytes for downloading."""
1315
+ with open(file_path, 'rb') as f:
1316
+
1317
+ return f.read()
1318
 
1319
  with tabs[4]:
1320
+ st.subheader("📚 Fuel Registry")
1321
+ st.write("Manage fuel components and blends. Add new entries manually, upload in batches, or download templates.")
1322
 
1323
+ # --- State Initialization ---
1324
+ if 'components' not in st.session_state:
1325
+ st.session_state.components = load_data('components')
1326
+ if 'blends' not in st.session_state:
1327
+ st.session_state.blends = load_data('blends')
1328
+
1329
+ # --- Section 1: Data Management (Uploads & Manual Entry) ---
1330
+ col1, col2 = st.columns(2)
1331
+
1332
+ with col1:
1333
+ with st.container(border=True):
1334
+ st.markdown("#### ➕ Add Components")
 
 
 
 
 
 
 
 
 
 
 
 
1335
 
1336
+ # Manual entry for a single component
1337
+ with st.expander("Add a Single Component Manually"):
1338
+ with st.form("new_component_form", clear_on_submit=True):
1339
+ component_name = st.text_input("Component Name", placeholder="e.g., Reformate")
1340
+ # Add inputs for other key properties of a component
1341
+ # This example assumes a few common properties. Adjust as needed.
1342
+ c_cols = st.columns(2)
1343
+ component_fraction = c_cols[1].number_input("Component Fraction", value=0.0, step=0.1, format="%.2f")
1344
+ property1 = c_cols[0].number_input("Property1", value=0.0, step=0.1, format="%.2f")
1345
+ property2 = c_cols[1].number_input("Property2", value=0.0, step=0.1, format="%.2f")
1346
+ property3 = c_cols[0].number_input("Property3", value=0.0, step=0.1, format="%.2f")
1347
+ property4 = c_cols[1].number_input("Property4", value=0.0, step=0.1, format="%.2f")
1348
+ property5 = c_cols[0].number_input("Property5", value=0.0, step=0.1, format="%.2f")
1349
+ property6 = c_cols[1].number_input("Property6", value=0.0, step=0.1, format="%.2f")
1350
+ property7 = c_cols[0].number_input("Property 7", value=0.0, step=0.1, format="%.2f")
1351
+ property8 = c_cols[1].number_input("Property 8", value=0.0, step=0.1, format="%.2f")
1352
+ property9 = c_cols[0].number_input("Property 9", value=0.0, step=0.1, format="%.2f")
1353
+ property10 = c_cols[1].number_input("Property 10", value=0.0, step=0.1, format="%.2f")
1354
+ unit_cost = c_cols[0].number_input("unit_cost", value=0.0, step=0.1, format="%.2f")
1355
+ # property4 = c_cols[1].number_input("Unit Cost", value=0.0, step=0.1, format="%.2f")
1356
+
1357
+ if st.form_submit_button("💾 Save Component", use_container_width=True):
1358
+ if not component_name.strip():
1359
+ st.warning("Component Name cannot be empty.")
1360
+ else:
1361
+ new_component_df = pd.DataFrame([{
1362
+ "component_name": component_name,
1363
+ "RON": ron, "MON": mon, "RVP": rvp, "Cost": cost
1364
+ # Add other properties here
1365
+ }])
1366
+ rows_added = add_components(new_component_df)
1367
+ if rows_added > 0:
1368
+ st.success(f"Component '{component_name}' added successfully!")
1369
+ # Clear cache and rerun
1370
+ del st.session_state.components
1371
+ st.rerun()
1372
 
1373
+ # Batch upload for components
1374
+ st.markdown("---")
1375
+ st.markdown("**Batch Upload Components**")
1376
+ uploaded_components = st.file_uploader(
1377
+ "Upload Components CSV", type=['csv'], key="components_uploader",
1378
+ help="Upload a CSV file with component properties."
1379
+ )
1380
+ if uploaded_components:
1381
+ try:
1382
+ df = pd.read_csv(uploaded_components)
1383
+ rows_added = add_components(df)
1384
+ st.success(f"Successfully added {rows_added} new components to the registry!")
1385
+ del st.session_state.components # Force reload
1386
+ st.rerun()
1387
+ except Exception as e:
1388
+ st.error(f"Error processing file: {e}")
1389
+
1390
+ st.download_button(
1391
+ label="📥 Download Component Template",
1392
+ data=get_template('assets/components_template.csv'),
1393
+ file_name='components_template.csv',
1394
+ mime='text/csv',
1395
+ use_container_width=True
1396
+ )
1397
 
1398
+ with col2:
1399
+ with st.container(border=True):
1400
+ st.markdown("#### 🧬 Add Blends")
1401
+ st.info("Upload blend compositions via CSV. Manual entry is not supported for blends.", icon="ℹ️")
1402
+
1403
+ # Batch upload for blends
1404
+ uploaded_blends = st.file_uploader(
1405
+ "Upload Blends CSV", type=['csv'], key="blends_uploader",
1406
+ help="Upload a CSV file defining blend recipes."
1407
+ )
1408
+ if uploaded_blends:
1409
+ try:
1410
+ df = pd.read_csv(uploaded_blends)
1411
+ rows_added = add_blends(df) # Assumes you have an add_blends function
1412
+ st.success(f"Successfully added {rows_added} new blends to the registry!")
1413
+ del st.session_state.blends # Force reload
 
 
 
 
 
 
 
 
 
1414
  st.rerun()
1415
+ except Exception as e:
1416
+ st.error(f"Error processing file: {e}")
1417
+
1418
+ st.download_button(
1419
+ label="📥 Download Blend Template",
1420
+ data=get_template('assets/blends_template.csv'),
1421
+ file_name='blends_template.csv',
1422
+ mime='text/csv',
1423
+ use_container_width=True
1424
+ )
 
 
 
 
 
 
 
1425
 
1426
+ st.divider()
1427
 
1428
+ # --- Section 2: Data Display & Deletion ---
1429
+ st.markdown("#### 🔍 View & Manage Registry Data")
1430
 
1431
+ view_col1, view_col2 = st.columns([1, 2])
 
 
 
 
 
 
1432
 
1433
+ with view_col1:
1434
+ table_to_show = st.selectbox(
1435
+ "Select Table to View",
1436
+ ("Components", "Blends"),
1437
+ label_visibility="collapsed"
1438
+ )
1439
 
1440
+ with view_col2:
1441
+ search_query = st.text_input(
1442
+ "Search Table",
1443
+ placeholder=f"Type to search in {table_to_show}...",
1444
+ label_visibility="collapsed"
1445
+ )
1446
+
1447
+ # Determine which DataFrame to use
1448
+ if table_to_show == "Components":
1449
+ df_display = st.session_state.components.copy()
1450
+ id_column = "component_id" # Change if your ID column is named differently
1451
+ else:
1452
+ df_display = st.session_state.blends.copy()
1453
+ id_column = "blend_id" # Change if your ID column is named differently
1454
+
1455
+ # Apply search filter if query is provided
1456
+ if search_query:
1457
+ # A simple search across all columns
1458
+ df_display = df_display[df_display.apply(
1459
+ lambda row: row.astype(str).str.contains(search_query, case=False).any(),
1460
+ axis=1
1461
+ )]
1462
+
1463
+ if df_display.empty:
1464
+ st.warning(f"No {table_to_show.lower()} found matching your criteria.")
1465
+ else:
1466
+ # Add a "Select" column for deletion
1467
+ df_display.insert(0, "Select", False)
1468
+
1469
+ # Use data_editor to make the checkboxes interactive
1470
+ edited_df = st.data_editor(
1471
+ df_display,
1472
+ hide_index=True,
1473
+ use_container_width=True,
1474
+ disabled=df_display.columns.drop("Select"), # Make all columns except "Select" read-only
1475
+ key=f"editor_{table_to_show}"
1476
+ )
1477
+
1478
+ selected_rows = edited_df[edited_df["Select"]]
1479
 
1480
+ if not selected_rows.empty:
1481
+ if st.button(f"❌ Delete Selected {table_to_show} ({len(selected_rows)})", use_container_width=True, type="primary"):
1482
+ ids_to_del = selected_rows[id_column].tolist()
1483
+ delete_records(table_to_show.lower(), ids_to_del, id_column)
1484
+ st.success(f"Deleted {len(ids_to_del)} records from {table_to_show}.")
1485
+ # Force a data refresh
1486
+ if table_to_show == "Components":
1487
+ del st.session_state.components
1488
+ else:
1489
+ del st.session_state.blends
1490
+ st.rerun()
1491
+
1492
 
1493
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
1494
+ # Model Insights Tab
1495
+ # ----------------------------------------------------------------------------------------------------------------------------------------------
1496
+ with tabs[5]:
1497
 
1498
+ model_metrics = last_model[
1499
+ [f"BlendProperty{i}_Score" for i in range(1, 11)]
1500
+ ]
1501
 
1502
+ # --- UI Rendering Starts Here ---
1503
 
1504
+ # Inject CSS for consistent styling with the rest of the app
1505
+ st.markdown("""
1506
+ <style>
1507
+ /* Metric card styles */
1508
+ .metric-card {
1509
+ background: linear-gradient(180deg, #FFF8E1 0%, #FFF6EA 100%);
1510
+ border: 1px solid #E3C77A;
1511
+ border-radius: 8px;
1512
+ padding: 15px;
1513
+ text-align: center;
1514
+ color: #654321;
1515
+ box-shadow: 0 2px 6px rgba(0,0,0,0.05);
1516
+ }
1517
+ .metric-label {
1518
+ font-size: 14px;
1519
+ font-weight: 700;
1520
+ color: #8B4513;
1521
+ margin-bottom: 5px;
1522
+ }
1523
+ .metric-value {
1524
+ font-size: 1.8rem;
1525
+ font-weight: 900;
1526
+ color: #4a2f1f;
1527
+ }
1528
+ /* Floating help button and panel styles */
1529
+ #help-toggle{display:none;}
1530
+ .help-button{
1531
+ position:fixed; right:25px; bottom:25px; z-index:9999;
1532
+ background:#8B4513; color:#FFD700; padding:16px 22px; font-size:17px;
1533
+ border-radius:18px; font-weight:900; box-shadow:0 8px 22px rgba(0,0,0,0.2); cursor:pointer;
1534
+ border:0;
1535
+ }
1536
+ .help-panel{
1537
+ position:fixed; right:25px; bottom:100px; z-index:9998;
1538
+ width:520px; max-height:70vh; overflow-y:auto;
1539
+ background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
1540
+ border:1px solid #CFB53B; border-radius:12px; padding:20px; box-shadow:0 14px 34px rgba(0,0,0,0.22);
1541
+ color:#4a2f1f; transform: translateY(12px); opacity:0; visibility:hidden; transition: all .22s ease-in-out;
1542
+ }
1543
+ #help-toggle:checked + label.help-button + .help-panel{
1544
+ opacity:1; visibility:visible; transform: translateY(0);
1545
+ }
1546
+ .help-panel .head{display:flex; justify-content:space-between; align-items:center; margin-bottom:12px}
1547
+ .help-panel .title{font-weight:900; color:#654321; font-size:16px}
1548
+ .help-close{background:#8B4513; color:#FFD700; padding:6px 10px; border-radius:8px; cursor:pointer; font-weight:800}
1549
+ .help-body{font-size:14.5px; color:#4a2f1f; line-height:1.5}
1550
+ .help-body b {color: #654321;}
1551
+ </style>
1552
+ """, unsafe_allow_html=True)
1553
+
1554
+ # --- Floating "How to Use" Button and Panel ---
1555
+ st.markdown("""
1556
+ <input id="help-toggle" type="checkbox" />
1557
+ <label for="help-toggle" class="help-button">💬 How to Use</label>
1558
+
1559
+ <div class="help-panel" aria-hidden="true">
1560
+ <div class="head">
1561
+ <div class="title">Interpreting Model Insights</div>
1562
+ <label for="help-toggle" class="help-close">Close</label>
1563
+ </div>
1564
+ <div class="help-body">
1565
+ <p><b>KPI Cards:</b> These four cards give you a quick summary of the model's overall health.</p>
1566
+ <ul>
1567
+ <li><b>Overall R² Score:</b> Think of this as the model's accuracy grade. A score of 92.4% means the model's predictions are highly accurate.</li>
1568
+ <li><b>MSE (Mean Squared Error):</b> This measures the average size of the model's mistakes. A smaller number is better.</li>
1569
+ <li><b>MAPE (Mean Absolute % Error):</b> This tells you the average error in percentage terms. A value of 0.112 means predictions are off by about 11.2% on average.</li>
1570
+ </ul>
1571
+ <p><b>R² Score by Blend Property Chart:</b> This chart shows how well the model predicts each specific property.</p>
1572
+ <p>A <b>longer bar</b> means the model is very good at predicting that property. A <b>shorter bar</b> indicates a property that is harder for the model to predict accurately. This helps you trust predictions for some properties more than others.</p>
1573
+ </div>
1574
+ </div>
1575
+ """, unsafe_allow_html=True)
1576
+
1577
+ # --- Main Title ---
1578
+ st.markdown('<h2 style="color:#4a2f1f; font-size:1.75rem;">🧠 Model Insights</h2>', unsafe_allow_html=True)
1579
+
1580
+ # --- Fetch Model Data ---
1581
+ latest_model = get_model()
1582
+ model_name = latest_model.get("model_name", "N/A")
1583
+ r2_score = f'{latest_model.get("R2_Score", 0) * 100:.1f}%'
1584
+ mse = f'{latest_model.get("MSE", 0):.3f}'
1585
+ mape = f'{latest_model.get("MAPE", 0):.3f}'
1586
+
1587
+ # --- KPI Cards Section ---
1588
+ k1, k2, k3, k4 = st.columns(4)
1589
+ with k1:
1590
+ st.markdown(f"""
1591
+ <div class="metric-card">
1592
+ <div class="metric-label">Model Name</div>
1593
+ <div class="metric-value" style="font-size: 1.2rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{model_name}</div>
1594
+ </div>
1595
+ """, unsafe_allow_html=True)
1596
+ with k2:
1597
+ st.markdown(f"""
1598
+ <div class="metric-card">
1599
+ <div class="metric-label">Overall R² Score</div>
1600
+ <div class="metric-value">{r2_score}</div>
1601
+ </div>
1602
+ """, unsafe_allow_html=True)
1603
+ with k3:
1604
+ st.markdown(f"""
1605
+ <div class="metric-card">
1606
+ <div class="metric-label">Mean Squared Error</div>
1607
+ <div class="metric-value">{mse}</div>
1608
+ </div>
1609
+ """, unsafe_allow_html=True)
1610
+ with k4:
1611
+ st.markdown(f"""
1612
+ <div class="metric-card">
1613
+ <div class="metric-label">Mean Absolute % Error</div>
1614
+ <div class="metric-value">{mape}</div>
1615
+ </div>
1616
+ """, unsafe_allow_html=True)
1617
 
1618
+ st.markdown('<div style="height:20px;"></div>', unsafe_allow_html=True) # Spacer
1619
 
1620
+ # --- R2 Score by Property Chart ---
1621
+ st.markdown('<h3 style="color:#4a2f1f; font-size:1.5rem;">R² Score by Blend Property</h3>', unsafe_allow_html=True)
1622
 
1623
+ # Create the horizontal bar chart
1624
+ fig_r2 = go.Figure()
1625
+
1626
+ fig_r2.add_trace(go.Bar(
1627
+ y=model_metrics.index,
1628
+ x=model_metrics.values,
 
 
 
 
 
1629
  orientation='h',
1630
+ marker=dict(
1631
+ color=model_metrics.values,
1632
+ colorscale='YlOrBr',
1633
+ colorbar=dict(title="R² Score", tickfont=dict(color="#4a2f1f")),
1634
+ ),
1635
+ text=[f'{val:.2f}' for val in model_metrics.values],
1636
+ textposition='inside',
1637
+ insidetextanchor='middle',
1638
+ textfont=dict(color='#4a2f1f', size=12, family='Arial, sans-serif', weight='bold')
1639
+ ))
1640
+
1641
+ # This corrected block resolves the ValueError
1642
+ fig_r2.update_layout(
1643
+ xaxis_title="R² Score (Higher is Better)",
1644
+ yaxis_title="Blend Property",
1645
+ plot_bgcolor='rgba(0,0,0,0)',
1646
+ paper_bgcolor='rgba(0,0,0,0)',
1647
+ margin=dict(l=10, r=10, t=20, b=50),
1648
+ font=dict(
1649
+ family="Segoe UI, Arial, sans-serif",
1650
+ size=12,
1651
+ color="#4a2f1f"
1652
+ ),
1653
+ yaxis=dict(
1654
+ tickfont=dict(size=12, weight='bold'),
1655
+ automargin=True,
1656
+ # FIX: The title font styling is now correctly nested here
1657
+ title_font=dict(size=14)
1658
+ ),
1659
+ xaxis=dict(
1660
+ gridcolor="rgba(139, 69, 19, 0.2)",
1661
+ zerolinecolor="rgba(139, 69, 19, 0.3)",
1662
+ # FIX: The title font styling is now correctly nested here
1663
+ title_font=dict(size=14)
1664
+ )
1665
  )
1666
+
1667
+ st.plotly_chart(fig_r2, use_container_width=True)
1668
+
1669
 
1670
 
1671