inspekta_deck / src /streamlit_appppp.py
amiguel's picture
Rename src/streamlit_app.py to src/streamlit_appppp.py
443c7fa verified
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
)