Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import matplotlib.patches as patches | |
| import math | |
| import matplotlib.transforms as transforms | |
| import sqlite3 | |
| # Import FPSO-specific modules | |
| from clv import * | |
| from paz import * | |
| from dal import * | |
| from gir import * | |
| # Import shared utilities | |
| # Remove these imports: | |
| # from utils import preprocess_keywords, extract_ni_nc_keywords, extract_location_keywords | |
| # --- UI CONFIG & STYLE --- | |
| st.set_page_config(page_title="B17 - Notifications", layout="wide") | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.cdnfonts.com/css/tw-cen-mt'); | |
| * { | |
| font-family: 'Tw Cen MT', sans-serif !important; | |
| } | |
| /* Sidebar arrow fix */ | |
| section[data-testid="stSidebar"] [data-testid="stSidebarNav"]::before { | |
| content: "βΆ"; | |
| font-size: 1.3rem; | |
| margin-right: 0.4rem; | |
| } | |
| /* Fix sidebar expander layout */ | |
| section[data-testid="stSidebar"] [data-testid="stExpander"] { | |
| margin-bottom: 1rem; | |
| } | |
| section[data-testid="stSidebar"] [data-testid="stExpander"] [data-testid="stExpanderHeader"] { | |
| padding: 0.5rem 0.75rem; | |
| font-size: 0.9rem; | |
| line-height: 1.2; | |
| word-wrap: break-word; | |
| overflow-wrap: break-word; | |
| } | |
| section[data-testid="stSidebar"] [data-testid="stExpander"] [data-testid="stExpanderContent"] { | |
| padding: 0.5rem 0.75rem; | |
| } | |
| /* Ensure proper spacing for sidebar elements */ | |
| section[data-testid="stSidebar"] .stMarkdown { | |
| margin-bottom: 0.5rem; | |
| } | |
| section[data-testid="stSidebar"] .stButton { | |
| margin-top: 0.5rem; | |
| } | |
| /* Ensure sidebar has proper width */ | |
| section[data-testid="stSidebar"] { | |
| min-width: 300px; | |
| } | |
| /* Improve expander content readability */ | |
| section[data-testid="stSidebar"] [data-testid="stExpander"] .stMarkdown { | |
| font-size: 0.85rem; | |
| line-height: 1.3; | |
| } | |
| section[data-testid="stSidebar"] [data-testid="stExpander"] .stMarkdown p { | |
| margin-bottom: 0.25rem; | |
| } | |
| /* Top-right logo placement - responsive to scrolling */ | |
| .logo-container { | |
| position: absolute; | |
| top: 1rem; | |
| right: 2rem; | |
| z-index: 1000; | |
| transition: all 0.3s ease; | |
| } | |
| /* Adjust logo position when scrolling */ | |
| .logo-container.scrolled { | |
| position: fixed; | |
| top: 0.5rem; | |
| right: 1rem; | |
| transform: scale(0.8); | |
| } | |
| /* Ensure main content doesn't overlap with logo */ | |
| .main .block-container { | |
| padding-top: 2rem !important; | |
| } | |
| /* Smooth transitions for logo */ | |
| .logo-container img { | |
| transition: all 0.3s ease; | |
| } | |
| /* Logo hover effect */ | |
| .logo-container:hover { | |
| transform: scale(1.05); | |
| } | |
| .logo-container.scrolled:hover { | |
| transform: scale(0.85); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Display logo (responsive to scrolling) | |
| st.markdown( | |
| """ | |
| <div class="logo-container" id="logo-container"> | |
| <img src="https://github.com/valonys/DigiTwin/blob/29dd50da95bec35a5abdca4bdda1967f0e5efff6/ValonyLabs_Logo.png?raw=true" width="70"> | |
| </div> | |
| <script> | |
| // Handle logo positioning on scroll | |
| window.addEventListener('scroll', function() { | |
| const logo = document.getElementById('logo-container'); | |
| if (window.scrollY > 100) { | |
| logo.classList.add('scrolled'); | |
| } else { | |
| logo.classList.remove('scrolled'); | |
| } | |
| }); | |
| // Initial check for scroll position | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const logo = document.getElementById('logo-container'); | |
| if (window.scrollY > 100) { | |
| logo.classList.add('scrolled'); | |
| } | |
| }); | |
| </script> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| st.title("π DigiTwin - The Inspekta Deck") | |
| # --- AVATARS --- | |
| USER_AVATAR = "https://raw.githubusercontent.com/achilela/vila_fofoka_analysis/9904d9a0d445ab0488cf7395cb863cce7621d897/USER_AVATAR.png" | |
| BOT_AVATAR = "https://raw.githubusercontent.com/achilela/vila_fofoka_analysis/991f4c6e4e1dc7a8e24876ca5aae5228bcdb4dba/Ataliba_Avatar.jpg" | |
| # --- FAST LOCAL PREPROCESSING FUNCTIONS --- | |
| def preprocess_keywords(description): | |
| description = str(description).upper() | |
| for lq_variant in clv_living_quarters_keywords: | |
| if lq_variant != 'LQ': | |
| description = description.replace(lq_variant, 'LQ') | |
| for module in clv_module_keywords: | |
| number = module[1:] | |
| if number in description: | |
| description = description.replace(number, module) | |
| for module in paz_module_keywords: | |
| if module in description: | |
| description = description.replace(module, module) | |
| for rack in paz_rack_keywords: | |
| if rack in description: | |
| description = description.replace(rack, rack) | |
| for module in dal_module_keywords: | |
| if module in description: | |
| description = description.replace(module, module) | |
| for rack in dal_rack_keywords: | |
| if rack in description: | |
| description = description.replace(rack, rack) | |
| # If you use NI_keyword_map and NC_keyword_map, add them here as well | |
| return description | |
| def extract_ni_nc_keywords(row, notif_type_col, desc_col): | |
| description = preprocess_keywords(row[desc_col]) | |
| notif_type = row[notif_type_col] | |
| if notif_type == 'NI': | |
| keywords = [kw for kw in NI_keywords if kw in description] | |
| elif notif_type == 'NC': | |
| keywords = [kw for kw in NC_keywords if kw in description] | |
| else: | |
| keywords = [] | |
| return ', '.join(keywords) if keywords else 'None' | |
| def extract_location_keywords(row, desc_col, keyword_list): | |
| description = preprocess_keywords(row[desc_col]) | |
| if keyword_list == clv_living_quarters_keywords: | |
| return 'LQ' if any(kw in description for kw in clv_living_quarters_keywords) else 'None' | |
| else: | |
| locations = [kw for kw in keyword_list if kw in description] | |
| return ', '.join(locations) if locations else 'None' | |
| def create_pivot_table(df, index, columns, aggfunc='size', fill_value=0): | |
| """Create pivot table from dataframe""" | |
| df_exploded = df.assign(Keywords=df[columns].str.split(', ')).explode('Keywords') | |
| df_exploded = df_exploded[df_exploded['Keywords'] != 'None'] | |
| pivot = pd.pivot_table(df_exploded, index=index, columns='Keywords', aggfunc=aggfunc, fill_value=fill_value) | |
| return pivot | |
| def apply_fpso_colors(df): | |
| """Apply color styling to FPSO dataframe""" | |
| styles = pd.DataFrame('', index=df.index, columns=df.columns) | |
| color_map = {'GIR': '#FFA07A', 'DAL': '#ADD8E6', 'PAZ': '#D8BFD8', 'CLV': '#90EE90'} | |
| for fpso, color in color_map.items(): | |
| if fpso in df.index: | |
| styles.loc[fpso] = f'background-color: {color}' | |
| return styles | |
| def add_rectangle(ax, xy, width, height, **kwargs): | |
| rectangle = patches.Rectangle(xy, width, height, **kwargs) | |
| ax.add_patch(rectangle) | |
| def add_chamfered_rectangle(ax, xy, width, height, chamfer, **kwargs): | |
| x, y = xy | |
| coords = [ | |
| (x + chamfer, y), | |
| (x + width - chamfer, y), | |
| (x + width, y + chamfer), | |
| (x + width, y + height - chamfer), | |
| (x + width - chamfer, y + height), | |
| (x + chamfer, y + height), | |
| (x, y + height - chamfer), | |
| (x, y + chamfer) | |
| ] | |
| polygon = patches.Polygon(coords, closed=True, **kwargs) | |
| ax.add_patch(polygon) | |
| def add_hexagon(ax, xy, radius, **kwargs): | |
| x, y = xy | |
| vertices = [(x + radius * math.cos(2 * math.pi * n / 6), y + radius * math.sin(2 * math.pi * n / 6)) for n in range(6)] | |
| hexagon = patches.Polygon(vertices, closed=True, **kwargs) | |
| ax.add_patch(hexagon) | |
| def add_fwd(ax, xy, width, height, **kwargs): | |
| x, y = xy | |
| top_width = width * 0.80 | |
| coords = [ | |
| (0, 0), | |
| (width, 0), | |
| (width - (width - top_width) / 2, height), | |
| ((width - top_width) / 2, height) | |
| ] | |
| trapezoid = patches.Polygon(coords, closed=True, **kwargs) | |
| t = transforms.Affine2D().rotate_deg(90).translate(x, y) | |
| trapezoid.set_transform(t + ax.transData) | |
| ax.add_patch(trapezoid) | |
| text_t = transforms.Affine2D().rotate_deg(90).translate(x + height / 2, y + width / 2) | |
| ax.text(0, -1, "FWD", ha='center', va='center', fontsize=7, weight='bold', transform=text_t + ax.transData) | |
| # Sidebar file upload and FPSO selection | |
| st.sidebar.title("Upload Notifications Dataset") | |
| # Add database loading option | |
| load_from_db = st.sidebar.checkbox("Load from Database", help="Load previously uploaded data from database") | |
| # Add preprocessing option | |
| enable_preprocessing = st.sidebar.checkbox("Enable Data Preprocessing", value=True, | |
| help="Remove unnecessary columns and optimize memory usage") | |
| uploaded_file = st.sidebar.file_uploader("Choose an Excel file", type=["xlsx"]) | |
| # Add FPSO selection dropdown in the sidebar | |
| selected_fpso = st.sidebar.selectbox("Select FPSO for Layout", ['GIR', 'DAL', 'PAZ', 'CLV']) | |
| # NI/NC keywords (if not already in utils.py, move them there) | |
| NI_keywords = ['WRAP', 'WELD', 'TBR', 'PACH', 'PATCH', 'OTHE', 'CLMP', 'REPL', | |
| 'BOND', 'BOLT', 'SUPP', 'OT', 'GASK', 'CLAMP'] | |
| NC_keywords = ['COA', 'ICOA', 'CUSP', 'WELD', 'REPL', 'CUSP1', 'CUSP2'] | |
| DB_PATH = 'notifs_data.db' | |
| TABLE_NAME = 'notifications' | |
| # Utility to save DataFrame to SQLite | |
| def save_df_to_db(df, db_path=DB_PATH, table_name=TABLE_NAME): | |
| with sqlite3.connect(db_path) as conn: | |
| df.to_sql(table_name, conn, if_exists='replace', index=False) | |
| # Save timestamp | |
| from datetime import datetime | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| conn.execute("CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT)") | |
| conn.execute("INSERT OR REPLACE INTO metadata VALUES (?, ?)", ('last_updated', timestamp)) | |
| # Utility to load DataFrame from SQLite | |
| def load_df_from_db(db_path=DB_PATH, table_name=TABLE_NAME): | |
| with sqlite3.connect(db_path) as conn: | |
| try: | |
| return pd.read_sql(f'SELECT * FROM {table_name}', conn) | |
| except Exception: | |
| return None | |
| # Utility to get last update timestamp | |
| def get_last_update_time(db_path=DB_PATH): | |
| with sqlite3.connect(db_path) as conn: | |
| try: | |
| result = conn.execute("SELECT value FROM metadata WHERE key = 'last_updated'").fetchone() | |
| return result[0] if result else None | |
| except Exception: | |
| return None | |
| # Data Preprocessing Function | |
| def preprocess_notifications_data(df): | |
| """ | |
| Preprocess notification data to reduce size and improve performance | |
| by removing unnecessary columns and optimizing memory usage. | |
| """ | |
| # Store original shape for comparison | |
| original_shape = df.shape | |
| original_memory = df.memory_usage(deep=True).sum() | |
| # Remove unnecessary columns to improve memory footprint | |
| columns_to_remove = [ | |
| 'Priority', # Redundant priority information | |
| 'Notification', # Duplicate notification data | |
| 'Order', # Order information not needed for analytics | |
| 'Planner group' # Planner group metadata | |
| ] | |
| # Remove specified columns (ignore if they don't exist) | |
| df_cleaned = df.drop(columns=columns_to_remove, errors='ignore') | |
| # Remove columns with high percentage of null values (>80%) | |
| null_percentage = df_cleaned.isnull().sum() / len(df_cleaned) * 100 | |
| high_null_columns = null_percentage[null_percentage > 80].index.tolist() | |
| df_cleaned = df_cleaned.drop(columns=high_null_columns) | |
| # Remove duplicate rows | |
| df_cleaned = df_cleaned.drop_duplicates() | |
| # Optimize data types for memory efficiency | |
| for col in df_cleaned.columns: | |
| if df_cleaned[col].dtype == 'object': | |
| # Convert object columns to category if they have few unique values | |
| if df_cleaned[col].nunique() / len(df_cleaned) < 0.5: | |
| df_cleaned[col] = df_cleaned[col].astype('category') | |
| elif df_cleaned[col].dtype == 'int64': | |
| # Downcast integers | |
| df_cleaned[col] = pd.to_numeric(df_cleaned[col], downcast='integer') | |
| elif df_cleaned[col].dtype == 'float64': | |
| # Downcast floats | |
| df_cleaned[col] = pd.to_numeric(df_cleaned[col], downcast='float') | |
| # Calculate improvements | |
| final_shape = df_cleaned.shape | |
| final_memory = df_cleaned.memory_usage(deep=True).sum() | |
| # Create summary of preprocessing results | |
| preprocessing_summary = { | |
| 'original_rows': original_shape[0], | |
| 'original_cols': original_shape[1], | |
| 'final_rows': final_shape[0], | |
| 'final_cols': final_shape[1], | |
| 'rows_removed': original_shape[0] - final_shape[0], | |
| 'cols_removed': original_shape[1] - final_shape[1], | |
| 'original_memory_mb': original_memory / 1024 / 1024, | |
| 'final_memory_mb': final_memory / 1024 / 1024, | |
| 'memory_reduction_mb': (original_memory - final_memory) / 1024 / 1024, | |
| 'memory_reduction_percent': ((original_memory - final_memory) / original_memory) * 100, | |
| 'removed_columns': columns_to_remove + high_null_columns | |
| } | |
| return df_cleaned, preprocessing_summary | |
| # Data Management Section | |
| st.sidebar.markdown("---") | |
| st.sidebar.subheader("Data Management") | |
| # Check if data exists in database | |
| existing_data = load_df_from_db() | |
| if existing_data is not None: | |
| st.sidebar.info(f"π Database contains {len(existing_data)} records") | |
| # Show last update time | |
| last_update = get_last_update_time() | |
| if last_update: | |
| st.sidebar.caption(f"π Last updated: {last_update}") | |
| # Show data summary | |
| with st.sidebar.expander("Data Summary"): | |
| if 'FPSO' in existing_data.columns: | |
| fpsos = existing_data['FPSO'].value_counts() | |
| st.write("**FPSO Distribution:**") | |
| for fpso, count in fpsos.items(): | |
| st.write(f"β’ {fpso}: {count}") | |
| if 'Notifictn type' in existing_data.columns: | |
| notif_types = existing_data['Notifictn type'].value_counts() | |
| st.write("**Notification Types:**") | |
| for ntype, count in notif_types.items(): | |
| st.write(f"β’ {ntype}: {count}") | |
| # Add clear database option | |
| if st.sidebar.button("ποΈ Clear Database"): | |
| import os | |
| if os.path.exists(DB_PATH): | |
| os.remove(DB_PATH) | |
| st.sidebar.success("Database cleared successfully!") | |
| st.rerun() | |
| else: | |
| st.sidebar.warning("No data in database") | |
| # Main app logic | |
| if uploaded_file is not None or load_from_db: | |
| try: | |
| if load_from_db: | |
| df = load_df_from_db() | |
| if df is None: | |
| st.warning("No data found in the database. Please upload a new file or ensure it's saved.") | |
| st.stop() | |
| else: | |
| st.success("π Data loaded from database successfully!") | |
| else: | |
| # Read the Excel file | |
| df = pd.read_excel(uploaded_file, sheet_name='Global Notifications') | |
| # Apply data preprocessing if enabled | |
| if enable_preprocessing: | |
| st.info("π Preprocessing data to optimize performance...") | |
| df, preprocessing_summary = preprocess_notifications_data(df) | |
| # Display preprocessing results | |
| with st.expander("π Data Preprocessing Summary", expanded=True): | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Rows", f"{preprocessing_summary['final_rows']:,}", | |
| f"-{preprocessing_summary['rows_removed']:,}") | |
| with col2: | |
| st.metric("Columns", f"{preprocessing_summary['final_cols']}", | |
| f"-{preprocessing_summary['cols_removed']}") | |
| with col3: | |
| st.metric("Memory", f"{preprocessing_summary['final_memory_mb']:.1f} MB", | |
| f"-{preprocessing_summary['memory_reduction_mb']:.1f} MB") | |
| st.write(f"**Memory reduction:** {preprocessing_summary['memory_reduction_percent']:.1f}%") | |
| if preprocessing_summary['removed_columns']: | |
| st.write("**Removed columns:**") | |
| for col in preprocessing_summary['removed_columns']: | |
| st.write(f"β’ {col}") | |
| # Save preprocessed data to DB for persistence | |
| save_df_to_db(df) | |
| st.success("β Data preprocessed and saved to database!") | |
| else: | |
| # Save original data to DB for persistence | |
| save_df_to_db(df) | |
| st.success("β Data uploaded and saved to database!") | |
| # Strip whitespace from column names | |
| df.columns = df.columns.str.strip() | |
| # Define expected columns with corrected spelling | |
| expected_columns = { | |
| 'Notifictn type': 'Notifictn type', # Corrected spelling | |
| 'Created on': 'Created on', # Corrected spelling | |
| 'Description': 'Description', | |
| 'FPSO': 'FPSO' | |
| } | |
| # Check if all expected columns are present and map them | |
| missing_columns = [] | |
| column_mapping = {} | |
| for expected, actual in expected_columns.items(): | |
| if actual in df.columns: | |
| column_mapping[expected] = actual | |
| else: | |
| missing_columns.append(actual) | |
| if missing_columns: | |
| st.error(f"The following expected columns are missing: {missing_columns}") | |
| st.write("Please ensure your Excel file contains these columns with the exact names.") | |
| st.stop() | |
| # Rename columns for consistency in processing | |
| df = df[list(column_mapping.values())] | |
| df.columns = list(expected_columns.keys()) | |
| # Ensure df is a DataFrame after slicing | |
| if not isinstance(df, pd.DataFrame): | |
| df = pd.DataFrame(df) | |
| # Preprocess FPSO: Keep only GIR, DAL, PAZ, CLV | |
| valid_fpsos = ['GIR', 'DAL', 'PAZ', 'CLV'] | |
| df = df[df['FPSO'].isin(valid_fpsos)] | |
| if not isinstance(df, pd.DataFrame): | |
| df = pd.DataFrame(df) | |
| # Extract NI/NC keywords | |
| df['Extracted_Keywords'] = df.apply(extract_ni_nc_keywords, axis=1, args=('Notifictn type', 'Description')) | |
| # Extract location keywords (modules, racks, etc.) | |
| df['Extracted_Modules'] = df.apply(extract_location_keywords, axis=1, args=('Description', clv_module_keywords)) | |
| df['Extracted_Racks'] = df.apply(extract_location_keywords, axis=1, args=('Description', clv_rack_keywords)) | |
| df['Extracted_LivingQuarters'] = df.apply(extract_location_keywords, axis=1, args=('Description', clv_living_quarters_keywords)) | |
| df['Extracted_Flare'] = df.apply(extract_location_keywords, axis=1, args=('Description', clv_flare_keywords)) | |
| df['Extracted_FWD'] = df.apply(extract_location_keywords, axis=1, args=('Description', clv_fwd_keywords)) | |
| df['Extracted_HeliDeck'] = df.apply(extract_location_keywords, axis=1, args=('Description', clv_hexagons_keywords)) | |
| # Extract PAZ-specific location keywords | |
| df['Extracted_PAZ_Modules'] = df.apply(extract_location_keywords, axis=1, args=('Description', paz_module_keywords)) | |
| df['Extracted_PAZ_Racks'] = df.apply(extract_location_keywords, axis=1, args=('Description', paz_rack_keywords)) | |
| df['Extracted_PAZ_LivingQuarters'] = df.apply(extract_location_keywords, axis=1, args=('Description', paz_living_quarters_keywords)) | |
| df['Extracted_PAZ_Flare'] = df.apply(extract_location_keywords, axis=1, args=('Description', paz_flare_keywords)) | |
| df['Extracted_PAZ_FWD'] = df.apply(extract_location_keywords, axis=1, args=('Description', paz_fwd_keywords)) | |
| df['Extracted_PAZ_HeliDeck'] = df.apply(extract_location_keywords, axis=1, args=('Description', paz_hexagons_keywords)) | |
| # Extract DAL-specific location keywords | |
| df['Extracted_DAL_Modules'] = df.apply(extract_location_keywords, axis=1, args=('Description', dal_module_keywords)) | |
| df['Extracted_DAL_Racks'] = df.apply(extract_location_keywords, axis=1, args=('Description', dal_rack_keywords)) | |
| df['Extracted_DAL_LivingQuarters'] = df.apply(extract_location_keywords, axis=1, args=('Description', dal_living_quarters_keywords)) | |
| df['Extracted_DAL_Flare'] = df.apply(extract_location_keywords, axis=1, args=('Description', dal_flare_keywords)) | |
| df['Extracted_DAL_FWD'] = df.apply(extract_location_keywords, axis=1, args=('Description', dal_fwd_keywords)) | |
| df['Extracted_DAL_HeliDeck'] = df.apply(extract_location_keywords, axis=1, args=('Description', dal_hexagons_keywords)) | |
| # Split dataframe into NI and NC | |
| df_ni = df[df['Notifictn type'] == 'NI'].copy() | |
| if not isinstance(df_ni, pd.DataFrame): | |
| df_ni = pd.DataFrame(df_ni) | |
| df_nc = df[df['Notifictn type'] == 'NC'].copy() | |
| if not isinstance(df_nc, pd.DataFrame): | |
| df_nc = pd.DataFrame(df_nc) | |
| # Create tabs | |
| tab1, tab2, tab3, tab4, tab5 = st.tabs(["NI Notifications", "NC Notifications", "Summary Stats", "FPSO Layout", "π€ RAG Assistant"]) | |
| # NI Notifications Tab | |
| with tab1: | |
| st.subheader("NI Notifications Analysis") | |
| if not df_ni.empty: | |
| ni_pivot = create_pivot_table(df_ni, index='FPSO', columns='Extracted_Keywords') | |
| st.write("Pivot Table (Count of Keywords by FPSO):") | |
| styled_ni_pivot = ni_pivot.style.apply(apply_fpso_colors, axis=None) | |
| st.dataframe(styled_ni_pivot) | |
| st.write(f"Total NI Notifications: {df_ni.shape[0]}") | |
| else: | |
| st.write("No NI notifications found in the dataset.") | |
| # NC Notifications Tab | |
| with tab2: | |
| st.subheader("NC Notifications Analysis") | |
| if not df_nc.empty: | |
| nc_pivot = create_pivot_table(df_nc, index='FPSO', columns='Extracted_Keywords') | |
| st.write("Pivot Table (Count of Keywords by FPSO):") | |
| styled_nc_pivot = nc_pivot.style.apply(apply_fpso_colors, axis=None) | |
| st.dataframe(styled_nc_pivot) | |
| st.write(f"Total NC Notifications: {df_nc.shape[0]}") | |
| else: | |
| st.write("No NC notifications found in the dataset.") | |
| # NI Summary 2025 Tab | |
| with tab3: | |
| st.subheader("2025 Raised") | |
| # Filter for notifications in 2025 | |
| created_on_series = pd.to_datetime(df['Created on']) | |
| df_2025 = df[created_on_series.dt.year == 2025].copy() | |
| if not df_2025.empty: | |
| # Add 'Month' column for monthly analysis | |
| df_2025['Month'] = pd.to_datetime(df_2025['Created on']).dt.strftime('%b') | |
| months_order = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] | |
| df_2025['Month'] = pd.Categorical(df_2025['Month'], categories=months_order, ordered=True) | |
| # Group by FPSO, Month, and Notification Type | |
| summary = df_2025.groupby(['FPSO', 'Month', 'Notifictn type']).size().unstack(fill_value=0) | |
| # Reshape the data for NI and NC notifications | |
| ni_summary = summary['NI'].unstack(level='Month') if 'NI' in summary else pd.DataFrame(index=pd.Index([]), columns=pd.Index(months_order)) | |
| nc_summary = summary['NC'].unstack(level='Month') if 'NC' in summary else pd.DataFrame(index=pd.Index([]), columns=pd.Index(months_order)) | |
| ni_summary = ni_summary.reindex(columns=pd.Index(months_order), fill_value=0) if not ni_summary.empty else pd.DataFrame(index=pd.Index([]), columns=pd.Index(months_order)) | |
| nc_summary = nc_summary.reindex(columns=pd.Index(months_order), fill_value=0) if not nc_summary.empty else pd.DataFrame(index=pd.Index([]), columns=pd.Index(months_order)) | |
| # Display NI Summary Table | |
| st.write("NI's:") | |
| st.dataframe( | |
| ni_summary.style.set_table_styles([ | |
| {'selector': 'thead', 'props': [('display', 'none')]} | |
| ]).set_properties(**{'text-align': 'center'}) | |
| ) | |
| # Display NC Summary Table | |
| st.write("NC's:") | |
| st.dataframe( | |
| nc_summary.style.set_table_styles([ | |
| {'selector': 'thead', 'props': [('display', 'none')]} | |
| ]).set_properties(**{'text-align': 'center'}) | |
| ) | |
| # Calculate totals | |
| total_ni = df_2025[df_2025['Notifictn type'] == 'NI'].shape[0] | |
| total_nc = df_2025[df_2025['Notifictn type'] == 'NC'].shape[0] | |
| st.write(f"Grand Total NI Notifications: {total_ni}") | |
| st.write(f"Grand Total NC Notifications: {total_nc}") | |
| else: | |
| st.write("No notifications found for 2025 in the dataset.") | |
| with tab4: | |
| st.subheader("FPSO Layout Visualization") | |
| notification_type = st.radio("Select Notification Type", ['NI', 'NC']) | |
| # Count NI or NC notifications for each location type for the selected FPSO (CLV, PAZ, DAL) | |
| df_selected = df[df['FPSO'] == selected_fpso].copy() | |
| if notification_type == 'NI': | |
| df_selected = df_selected[df_selected['Notifictn type'] == 'NI'] | |
| else: # NC | |
| df_selected = df_selected[df_selected['Notifictn type'] == 'NC'] | |
| # Initialize counts for all location types | |
| location_counts = { | |
| 'Modules': pd.DataFrame(index=pd.Index(clv_module_keywords), columns=['Count']).fillna(0), | |
| 'Racks': pd.DataFrame(index=pd.Index(clv_rack_keywords), columns=['Count']).fillna(0), | |
| 'LivingQuarters': pd.DataFrame(index=pd.Index(clv_living_quarters_keywords), columns=['Count']).fillna(0), | |
| 'Flare': pd.DataFrame(index=pd.Index(clv_flare_keywords), columns=['Count']).fillna(0), | |
| 'FWD': pd.DataFrame(index=pd.Index(clv_fwd_keywords), columns=['Count']).fillna(0), | |
| 'HeliDeck': pd.DataFrame(index=pd.Index(clv_hexagons_keywords), columns=['Count']).fillna(0) | |
| } | |
| paz_location_counts = { | |
| 'PAZ_Modules': pd.DataFrame(index=pd.Index(paz_module_keywords), columns=['Count']).fillna(0), | |
| 'PAZ_Racks': pd.DataFrame(index=pd.Index(paz_rack_keywords), columns=['Count']).fillna(0), | |
| 'LivingQuarters': pd.DataFrame(index=pd.Index(paz_living_quarters_keywords), columns=['Count']).fillna(0), | |
| 'Flare': pd.DataFrame(index=pd.Index(paz_flare_keywords), columns=['Count']).fillna(0), | |
| 'FWD': pd.DataFrame(index=pd.Index(paz_fwd_keywords), columns=['Count']).fillna(0), | |
| 'HeliDeck': pd.DataFrame(index=pd.Index(paz_hexagons_keywords), columns=['Count']).fillna(0) | |
| } | |
| dal_location_counts = { | |
| 'DAL_Modules': pd.DataFrame(index=pd.Index(dal_module_keywords), columns=['Count']).fillna(0), | |
| 'DAL_Racks': pd.DataFrame(index=pd.Index(dal_rack_keywords), columns=['Count']).fillna(0), | |
| 'LivingQuarters': pd.DataFrame(index=pd.Index(dal_living_quarters_keywords), columns=['Count']).fillna(0), | |
| 'Flare': pd.DataFrame(index=pd.Index(dal_flare_keywords), columns=['Count']).fillna(0), | |
| 'FWD': pd.DataFrame(index=pd.Index(dal_fwd_keywords), columns=['Count']).fillna(0), | |
| 'HeliDeck': pd.DataFrame(index=pd.Index(dal_hexagons_keywords), columns=['Count']).fillna(0) | |
| } | |
| # Count notifications for each location type and placement | |
| for location_type, keywords in [ | |
| ('Modules', clv_module_keywords), | |
| ('Racks', clv_rack_keywords), | |
| ('LivingQuarters', clv_living_quarters_keywords), | |
| ('Flare', clv_flare_keywords), | |
| ('FWD', clv_fwd_keywords), | |
| ('HeliDeck', clv_hexagons_keywords) | |
| ]: | |
| for keyword in keywords: | |
| count = df_selected[f'Extracted_{location_type}'].str.contains(keyword, na=False).sum() | |
| location_counts[location_type].loc[keyword, 'Count'] = count | |
| for location_type, keywords in [ | |
| ('PAZ_Modules', paz_module_keywords), | |
| ('PAZ_Racks', paz_rack_keywords), | |
| ('LivingQuarters', paz_living_quarters_keywords), | |
| ('Flare', paz_flare_keywords), | |
| ('FWD', paz_fwd_keywords), | |
| ('HeliDeck', paz_hexagons_keywords) | |
| ]: | |
| for keyword in keywords: | |
| if location_type == 'PAZ_Modules': | |
| count = df_selected['Extracted_PAZ_Modules'].str.contains(keyword, na=False).sum() | |
| paz_location_counts[location_type].loc[keyword, 'Count'] = count | |
| elif location_type == 'PAZ_Racks': | |
| count = df_selected['Extracted_PAZ_Racks'].str.contains(keyword, na=False).sum() | |
| paz_location_counts[location_type].loc[keyword, 'Count'] = count | |
| else: | |
| count = df_selected[f'Extracted_{location_type}'].str.contains(keyword, na=False).sum() | |
| paz_location_counts[location_type].loc[keyword, 'Count'] = count | |
| for location_type, keywords in [ | |
| ('DAL_Modules', dal_module_keywords), | |
| ('DAL_Racks', dal_rack_keywords), | |
| ('LivingQuarters', dal_living_quarters_keywords), | |
| ('Flare', dal_flare_keywords), | |
| ('FWD', dal_fwd_keywords), | |
| ('HeliDeck', dal_hexagons_keywords) | |
| ]: | |
| for keyword in keywords: | |
| if location_type == 'DAL_Modules': | |
| count = df_selected['Extracted_DAL_Modules'].str.contains(keyword, na=False).sum() | |
| dal_location_counts[location_type].loc[keyword, 'Count'] = count | |
| elif location_type == 'DAL_Racks': | |
| count = df_selected['Extracted_DAL_Racks'].str.contains(keyword, na=False).sum() | |
| dal_location_counts[location_type].loc[keyword, 'Count'] = count | |
| else: | |
| count = df_selected[f'Extracted_{location_type}'].str.contains(keyword, na=False).sum() | |
| dal_location_counts[location_type].loc[keyword, 'Count'] = count | |
| total_lq_count = sum( | |
| df_selected['Extracted_LivingQuarters'].str.contains(keyword, na=False).sum() | |
| for keyword in clv_living_quarters_keywords | |
| ) | |
| # Draw the FPSO layout and overlay notification counts | |
| def draw_fpso_layout(selected_unit): | |
| fig, ax = plt.subplots(figsize=(13, 8)) | |
| ax.set_xlim(0, 13.5) | |
| ax.set_ylim(0, 3.5) | |
| ax.set_aspect('equal') | |
| ax.grid(False) | |
| ax.set_facecolor('#E6F3FF') | |
| # Remove axes for cleaner visualization | |
| ax.set_xticks([]) | |
| ax.set_yticks([]) | |
| ax.spines['top'].set_visible(False) | |
| ax.spines['right'].set_visible(False) | |
| ax.spines['bottom'].set_visible(False) | |
| ax.spines['left'].set_visible(False) | |
| if selected_unit == 'CLV': | |
| draw_clv(ax, add_chamfered_rectangle, add_rectangle, add_hexagon, add_fwd) | |
| elif selected_unit == 'PAZ': | |
| draw_paz(ax, add_chamfered_rectangle, add_rectangle, add_hexagon, add_fwd) | |
| elif selected_unit == 'DAL': | |
| draw_dal(ax, add_chamfered_rectangle, add_rectangle, add_hexagon, add_fwd) | |
| elif selected_unit == 'GIR': | |
| draw_gir(ax, add_chamfered_rectangle, add_rectangle, add_hexagon, add_fwd) | |
| return fig | |
| fig = draw_fpso_layout(selected_fpso) | |
| ax = fig.gca() | |
| # Overlay notification counts on locations for CLV and PAZ | |
| if selected_fpso == 'CLV': | |
| # Modules | |
| for module, (row, col) in clv_modules.items(): | |
| if module in clv_module_keywords: | |
| count = int(location_counts['Modules'].loc[module, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the module text for clarity >> col moves horizontally in x axis whilst row moves vertically in y axis | |
| ax.text(col + 0.8, row + 0.8, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Racks | |
| for rack, (row, col) in clv_racks.items(): | |
| if rack in clv_rack_keywords: | |
| count = int(location_counts['Racks'].loc[rack, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the rack text | |
| ax.text(col + 0.7, row + 0.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Living Quarters (with total count) | |
| for lq, (row, col) in clv_living_quarters.items(): | |
| if total_lq_count > 0: | |
| # Position count slightly above and to the right of the LQ text | |
| ax.text(col + 0.7, row + 1.4, f"{total_lq_count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Flare | |
| for flare_loc, (row, col) in clv_flare.items(): | |
| if flare_loc in clv_flare_keywords: | |
| count = int(location_counts['Flare'].loc[flare_loc, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the flare text | |
| ax.text(col + 0.7, row + 0.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # FWD | |
| for fwd_loc, (row, col) in clv_fwd.items(): | |
| if fwd_loc in clv_fwd_keywords: | |
| count = int(location_counts['FWD'].loc[fwd_loc, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the left of the FWD text (adjusted for rotation) | |
| ax.text(col + 0.75, row + 1.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Heli-deck | |
| for hexagon, (row, col) in clv_hexagons.items(): | |
| if hexagon in clv_hexagons_keywords: | |
| count = int(location_counts['HeliDeck'].loc[hexagon, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the heli-deck text | |
| ax.text(col + 0.2, row + 0.2, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Total counts at the bottom (matching your image) | |
| total_ni = df_selected[df_selected['Notifictn type'] == 'NI'].shape[0] | |
| total_nc = df_selected[df_selected['Notifictn type'] == 'NC'].shape[0] | |
| ax.text(6, 0.25, f"NI: {total_ni}\nNC: {total_nc}", ha='center', va='center', fontsize=8, weight='bold', color='red') | |
| elif selected_fpso == 'PAZ': | |
| # PAZ Modules | |
| for module, (row, col) in paz_modules.items(): | |
| if module in paz_module_keywords: | |
| count = int(paz_location_counts['PAZ_Modules'].loc[module, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the module text | |
| ax.text(col + 0.8, row + 0.8, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # PAZ Racks | |
| for rack, (row, col) in paz_racks.items(): | |
| if rack in paz_rack_keywords: | |
| count = int(paz_location_counts['PAZ_Racks'].loc[rack, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the rack text | |
| ax.text(col + 0.7, row + 0.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Living Quarters (with total count) | |
| for lq, (row, col) in paz_living_quarters.items(): | |
| if total_lq_count > 0: | |
| # Position count slightly above and to the right of the LQ text | |
| ax.text(col + 0.7, row + 1.4, f"{total_lq_count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Flare | |
| for flare_loc, (row, col) in paz_flare.items(): | |
| if flare_loc in paz_flare_keywords: | |
| count = int(paz_location_counts['Flare'].loc[flare_loc, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the flare text | |
| ax.text(col + 0.7, row + 0.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # FWD | |
| for fwd_loc, (row, col) in paz_fwd.items(): | |
| if fwd_loc in paz_fwd_keywords: | |
| count = int(paz_location_counts['FWD'].loc[fwd_loc, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the left of the FWD text (adjusted for rotation) | |
| ax.text(col + 0.75, row + 1.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Heli-deck | |
| for hexagon, (row, col) in paz_hexagons.items(): | |
| if hexagon in paz_hexagons_keywords: | |
| count = int(paz_location_counts['HeliDeck'].loc[hexagon, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the heli-deck text | |
| ax.text(col + 0.2, row + 0.2, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Total counts at the bottom | |
| total_ni = df_selected[df_selected['Notifictn type'] == 'NI'].shape[0] | |
| total_nc = df_selected[df_selected['Notifictn type'] == 'NC'].shape[0] | |
| ax.text(6, 0.25, f"NI: {total_ni}\nNC: {total_nc}", ha='center', va='center', fontsize=8, weight='bold', color='red') | |
| elif selected_fpso == 'DAL': | |
| # DAL Modules | |
| for module, (row, col) in dal_modules.items(): | |
| if module in dal_module_keywords: | |
| count = int(dal_location_counts['DAL_Modules'].loc[module, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the module text | |
| ax.text(col + 0.8, row + 0.8, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # DAL Racks | |
| for rack, (row, col) in dal_racks.items(): | |
| if rack in dal_rack_keywords: | |
| count = int(dal_location_counts['DAL_Racks'].loc[rack, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the rack text | |
| ax.text(col + 0.7, row + 0.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Living Quarters (with total count) | |
| for lq, (row, col) in dal_living_quarters.items(): | |
| if total_lq_count > 0: | |
| # Position count slightly above and to the right of the LQ text | |
| ax.text(col + 0.7, row + 1.4, f"{total_lq_count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Flare | |
| for flare_loc, (row, col) in dal_flare.items(): | |
| if flare_loc in dal_flare_keywords: | |
| count = int(dal_location_counts['Flare'].loc[flare_loc, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the flare text | |
| ax.text(col + 0.7, row + 0.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # FWD | |
| for fwd_loc, (row, col) in dal_fwd.items(): | |
| if fwd_loc in dal_fwd_keywords: | |
| count = int(dal_location_counts['FWD'].loc[fwd_loc, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the left of the FWD text (adjusted for rotation) | |
| ax.text(col + 0.75, row + 1.4, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Heli-deck | |
| for hexagon, (row, col) in dal_hexagons.items(): | |
| if hexagon in dal_hexagons_keywords: | |
| count = int(dal_location_counts['HeliDeck'].loc[hexagon, 'Count']) | |
| if count > 0: | |
| # Position count slightly above and to the right of the heli-deck text | |
| ax.text(col + 0.2, row + 0.2, f"{count}", | |
| ha='center', va='center', fontsize=6, weight='bold', color='red') | |
| # Total counts at the bottom | |
| total_ni = df_selected[df_selected['Notifictn type'] == 'NI'].shape[0] | |
| total_nc = df_selected[df_selected['Notifictn type'] == 'NC'].shape[0] | |
| ax.text(6, 0.25, f"NI: {total_ni}\nNC: {total_nc}", ha='center', va='center', fontsize=8, weight='bold', color='red') | |
| else: | |
| # Display placeholder text for non-implemented FPSOs | |
| ax.text(6, 1.75, f"{selected_fpso} Layout\n(Implementation work in progress...)", ha='center', va='center', fontsize=16, weight='bold') | |
| plt.title(f"FPSO Visualization - {selected_fpso}", fontsize=16) | |
| st.pyplot(fig) | |
| plt.close(fig) # Close the figure to free memory | |
| # RAG Assistant Tab | |
| with tab5: | |
| st.subheader("π€ DigiTwin RAG Assistant") | |
| st.markdown("Ask me anything about your FPSO notifications data!") | |
| # Import and initialize RAG system | |
| try: | |
| from rag_chatbot import DigiTwinRAG, render_chat_interface | |
| # Initialize RAG system | |
| if 'rag_system' not in st.session_state: | |
| with st.spinner("Initializing RAG system..."): | |
| st.session_state.rag_system = DigiTwinRAG() | |
| # Render chat interface | |
| render_chat_interface(st.session_state.rag_system) | |
| except ImportError as e: | |
| st.error(f"β RAG module not available: {e}") | |
| st.info("π‘ To enable RAG functionality, install the required dependencies:") | |
| st.code("pip install -r requirements_rag.txt") | |
| # Show sample questions | |
| st.markdown("### π‘ Sample Questions You Can Ask:") | |
| sample_questions = [ | |
| "Which FPSO has the most NI notifications?", | |
| "What are the common keywords in PAZ notifications?", | |
| "Show me all safety-related notifications from last month", | |
| "Compare notification patterns between GIR and DAL", | |
| "What equipment has the most maintenance issues?", | |
| "Which work centers require immediate attention?" | |
| ] | |
| for question in sample_questions: | |
| st.write(f"β’ {question}") | |
| except Exception as e: | |
| st.error(f"β Error initializing RAG system: {e}") | |
| st.info("Please check your LLM configuration and vector database setup.") | |
| except Exception as e: | |
| st.error(f"An error occurred: {e}") | |
| else: | |
| st.write('Please upload an Excel file to proceed.') | |
| # Add footer with rocket emojis and branding | |
| st.markdown("---") | |
| st.markdown( | |
| """ | |
| <div style="text-align: center; padding: 20px; border-radius: 10px; margin-top: 30px;"> | |
| <p style="font-size: 14px; color: #6c757d; margin: 0;"> | |
| π Built with Pride - STP/INSP/MET | Powered by <a href="https://www.valonylabs.com" target="_blank" style="color: #007bff; text-decoration: none; font-weight: bold;">ValonyLabs</a> π | |
| </p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |