|
|
|
|
|
from typing import Optional, Dict, Any |
|
|
import base64 |
|
|
import mimetypes |
|
|
from html import escape |
|
|
from pathlib import Path |
|
|
import streamlit as st |
|
|
|
|
|
|
|
|
BASE_DIR = Path(__file__).parent |
|
|
ASSETS = BASE_DIR / "assets" |
|
|
|
|
|
|
|
|
st.set_page_config(page_title="ST_GeoMech SUITE", layout="wide") |
|
|
|
|
|
|
|
|
|
|
|
CARDS_PER_ROW: int = 3 |
|
|
|
|
|
|
|
|
THEME: Dict[str, Any] = { |
|
|
"strip": { |
|
|
"gap": 10, |
|
|
"below_gap": 30, |
|
|
"pill_pad_v": 8, |
|
|
"pill_pad_h": 14, |
|
|
"pill_font": 16, |
|
|
"tagline_font": 15, |
|
|
"bg1": "#064E3B", |
|
|
"bg2": "#065F46", |
|
|
"text": "#FFFFFF", |
|
|
"tagline_color": "#000000", |
|
|
}, |
|
|
"page": { |
|
|
"top_padding": 50, |
|
|
"container_width": 1120, |
|
|
"bg_radial": True, |
|
|
}, |
|
|
"hero": { |
|
|
"width": 400, |
|
|
"margin_bottom": 30, |
|
|
"logo": ASSETS / "AI_Suite_GeoMech_logo.png", |
|
|
}, |
|
|
"grid": { |
|
|
"gap": 70, |
|
|
|
|
|
"card_width": 380, |
|
|
}, |
|
|
"card": { |
|
|
"radius": 22, |
|
|
"border_width": 2, |
|
|
"pad_v": 24, |
|
|
"pad_h": 20, |
|
|
"border": "#0B1220", |
|
|
"border_hover": "#243447", |
|
|
"bg_top": "#FFFFFF", |
|
|
"bg_bot": "#FBFCFE", |
|
|
"title_color": "#0B1220", |
|
|
"blurb_color": "#566275", |
|
|
}, |
|
|
"icon": { |
|
|
"diam": 118, |
|
|
"img": 106, |
|
|
"circle_bg": "#F1F5F9", |
|
|
"circle_border": "rgba(12,18,32,0.10)", |
|
|
}, |
|
|
"button": { |
|
|
"pad_v": 12, |
|
|
"pad_h": 20, |
|
|
"radius": 14, |
|
|
"bg1": "#0B1220", |
|
|
"bg2": "#162338", |
|
|
"text": "#FFFFFF", |
|
|
"border": "rgba(11,18,32,.55)", |
|
|
}, |
|
|
} |
|
|
|
|
|
|
|
|
CARDS = [ |
|
|
{ |
|
|
"title": " ST_GeoMEch_UCS", |
|
|
"blurb": "Real-time Uniconfined Compressive Strength Prediction.", |
|
|
"url": "https://smart-thinking-ucs.hf.space/", |
|
|
"icon": ASSETS / "UCS_logo.png", |
|
|
"style": {"bg_top": "#EAF7F1", "bg_bot": "#F6FBF8", "border": "#0F3D3E"}, |
|
|
}, |
|
|
{ |
|
|
"title": " ST_GeoMech_Ym", |
|
|
"blurb": "Real-time Static Young's Modulus Prediction.", |
|
|
"url": "https://smart-thinking-ym.hf.space", |
|
|
"icon": ASSETS / "Ym_logo.png", |
|
|
"style": {"bg_top": "#EAF7FD", "bg_bot": "#F5FBFF", "border": "#0E4A6E"}, |
|
|
}, |
|
|
{ |
|
|
"title": " ST_GeoMech_SMW", |
|
|
"blurb": "Real-Time Safe Mud Window Prediction", |
|
|
"url": "https://smart-thinking-smw.hf.space", |
|
|
"icon": ASSETS / "SMW_logo.png", |
|
|
"style": {"bg_top": "#EEF0FF", "bg_bot": "#F7F8FF", "border": "#3E4EB8"}, |
|
|
}, |
|
|
] |
|
|
|
|
|
|
|
|
def data_uri(path: Path) -> Optional[str]: |
|
|
if not path or not path.exists(): |
|
|
return None |
|
|
mime, _ = mimetypes.guess_type(path.name) |
|
|
if not mime: |
|
|
mime = "image/png" |
|
|
b64 = base64.b64encode(path.read_bytes()).decode("utf-8") |
|
|
return f"data:{mime};base64,{b64}" |
|
|
|
|
|
|
|
|
def img_tag(path: Path, alt: str, cls: str = "", style: str = "") -> str: |
|
|
uri = data_uri(path) |
|
|
if not uri: |
|
|
return "" |
|
|
cls_attr = f' class="{cls}"' if cls else "" |
|
|
style_attr = f' style="{style}"' if style else "" |
|
|
return f'<img{cls_attr}{style_attr} src="{uri}" alt="{escape(alt)}" />' |
|
|
|
|
|
|
|
|
|
|
|
strip = THEME["strip"] |
|
|
page = THEME["page"] |
|
|
hero = THEME["hero"] |
|
|
grid = THEME["grid"] |
|
|
card = THEME["card"] |
|
|
icon = THEME["icon"] |
|
|
button = THEME["button"] |
|
|
|
|
|
bg_radial_css = """ |
|
|
background: |
|
|
radial-gradient(980px 460px at 50% -140px, |
|
|
rgba(2,12,30,0.06) 0%, |
|
|
rgba(2,12,30,0.04) 28%, |
|
|
rgba(255,255,255,0.98) 60%, |
|
|
rgba(255,255,255,1) 100%); |
|
|
""" if page["bg_radial"] else "background: #fff;" |
|
|
|
|
|
st.markdown( |
|
|
f""" |
|
|
<style> |
|
|
:root {{ |
|
|
--top-pad: {page["top_padding"]}px; |
|
|
|
|
|
--strip-gap: {strip["gap"]}px; |
|
|
--strip-row-mb: {strip["below_gap"]}px; |
|
|
--strip-pill-pv: {strip["pill_pad_v"]}px; |
|
|
--strip-pill-ph: {strip["pill_pad_h"]}px; |
|
|
--strip-pill-fs: {strip["pill_font"]}px; |
|
|
--tagline-fs: {strip["tagline_font"]}px; |
|
|
|
|
|
--hero-w: {hero["width"]}px; |
|
|
--hero-mb: {hero["margin_bottom"]}px; |
|
|
|
|
|
--grid-gap: {grid["gap"]}px; |
|
|
--card-w: {grid["card_width"]}px; |
|
|
--card-r: {card["radius"]}px; |
|
|
--card-bw: {card["border_width"]}px; |
|
|
--card-pv: {card["pad_v"]}px; |
|
|
--card-ph: {card["pad_h"]}px; |
|
|
|
|
|
--icon-d: {icon["diam"]}px; |
|
|
--icon-img: {icon["img"]}px; |
|
|
|
|
|
--stripBg1: {strip["bg1"]}; |
|
|
--stripBg2: {strip["bg2"]}; |
|
|
--stripText: {strip["text"]}; |
|
|
--taglineColor: {strip["tagline_color"]}; |
|
|
|
|
|
--cardStroke: {card["border"]}; |
|
|
--cardStrokeHover: {card["border_hover"]}; |
|
|
|
|
|
--btn1: {button["bg1"]}; |
|
|
--btn2: {button["bg2"]}; |
|
|
--btnText: {button["text"]}; |
|
|
--btnBorder: {button["border"]}; |
|
|
|
|
|
/* NEW: cards per row control */ |
|
|
--cols: {CARDS_PER_ROW}; |
|
|
}} |
|
|
|
|
|
html, body, [data-testid="stAppViewContainer"] {{ height: 100%; }} |
|
|
[data-testid="stAppViewContainer"] > .main {{ padding-top: 0 !important; padding-bottom: 0 !important; }} |
|
|
|
|
|
.block-container {{ |
|
|
max-width: {page["container_width"]}px; |
|
|
min-height: 100vh; |
|
|
display: flex; flex-direction: column; |
|
|
gap: 14px; |
|
|
padding: var(--top-pad) 0 28px !important; |
|
|
{bg_radial_css} |
|
|
}} |
|
|
|
|
|
/* ===== TOP-LEFT STRIP ===== */ |
|
|
.suite-row {{ |
|
|
display:flex; align-items:center; gap: var(--strip-gap); |
|
|
justify-content:flex-start; flex-wrap: wrap; |
|
|
margin: 0 0 var(--strip-row-mb) 0; |
|
|
}} |
|
|
.suite-pill {{ |
|
|
background: linear-gradient(90deg, var(--stripBg1) 0%, var(--stripBg2) 100%); |
|
|
color: var(--stripText); |
|
|
padding: var(--strip-pill-pv) var(--strip-pill-ph); |
|
|
border-radius: 999px; |
|
|
font-weight: 800; letter-spacing: .25px; |
|
|
font-size: var(--strip-pill-fs); |
|
|
box-shadow: 0 6px 14px rgba(2,12,30,.18); |
|
|
white-space: nowrap; |
|
|
}} |
|
|
.suite-tagline {{ |
|
|
color: var(--taglineColor); font-weight: 600; opacity: .95; |
|
|
font-size: var(--tagline-fs); |
|
|
white-space: nowrap; |
|
|
}} |
|
|
|
|
|
/* ===== HERO ===== */ |
|
|
.hero {{ text-align:center; margin: 0 0 var(--hero-mb); }} |
|
|
.hero img {{ |
|
|
width: var(--hero-w); max-width: 92vw; height: auto; |
|
|
display:block; margin: 0 auto var(--hero-mb); |
|
|
filter: drop-shadow(0 6px 16px rgba(0,0,0,.10)); |
|
|
}} |
|
|
|
|
|
/* ===== GRID ===== |
|
|
Uses --cols to set the number of cards per row on desktop. |
|
|
Responsive fallbacks: 2 cols on tablets, 1 col on phones. */ |
|
|
.grid {{ |
|
|
display: grid; |
|
|
grid-template-columns: repeat(var(--cols), 1fr); |
|
|
gap: var(--grid-gap); |
|
|
justify-items: stretch; |
|
|
align-items: stretch; |
|
|
margin-top: 10px; |
|
|
}} |
|
|
@media (max-width: 1120px) {{ |
|
|
.grid {{ grid-template-columns: repeat(2, 1fr); gap: calc(var(--grid-gap) - 12px); }} |
|
|
}} |
|
|
@media (max-width: 720px) {{ |
|
|
.grid {{ grid-template-columns: 1fr; }} |
|
|
}} |
|
|
|
|
|
/* ===== CARD ===== */ |
|
|
.card {{ |
|
|
position: relative; |
|
|
width: 100%; |
|
|
max-width: var(--card-w); |
|
|
border-radius: var(--c-radius, var(--card-r)); |
|
|
padding: var(--c-pv, var(--card-pv)) var(--c-ph, var(--card-ph)); |
|
|
background: linear-gradient(180deg, var(--c-bg-top, {card["bg_top"]}) 0%, var(--c-bg-bot, {card["bg_bot"]}) 100%); |
|
|
border: var(--c-bw, var(--card-bw)) solid var(--c-border, var(--cardStroke)); |
|
|
box-shadow: |
|
|
0 14px 32px rgba(2,20,35,.12), |
|
|
0 1px 0 rgba(255,255,255,0.70) inset, |
|
|
0 -1px 6px rgba(255,255,255,0.18) inset; |
|
|
transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease, filter .18s ease; |
|
|
text-align:center; display:flex; flex-direction:column; gap:16px; align-items: center; |
|
|
margin: 0 auto; |
|
|
}} |
|
|
.card:hover {{ |
|
|
transform: translateY(-6px) scale(1.01); |
|
|
border-color: var(--cardStrokeHover); |
|
|
box-shadow: |
|
|
0 22px 56px rgba(2,20,35,.18), |
|
|
0 1px 0 rgba(255,255,255,.82) inset, |
|
|
0 -1px 10px rgba(255,255,255,.28) inset; |
|
|
filter: saturate(1.03); |
|
|
}} |
|
|
|
|
|
/* ===== ICON / LOGO ===== */ |
|
|
.icon-wrap {{ |
|
|
width: var(--c-icon-d, {icon["diam"]}px); height: var(--c-icon-d, {icon["diam"]}px); |
|
|
border-radius: 9999px; display:grid; place-items:center; |
|
|
background: var(--c-icon-bg, {icon["circle_bg"]}); |
|
|
border: 1px solid var(--c-icon-border, {icon["circle_border"]}); |
|
|
box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 10px 22px rgba(2,20,35,.07); |
|
|
}} |
|
|
.icon-wrap img {{ |
|
|
width: calc(var(--c-icon-img, {icon["img"]}px) - 12px); |
|
|
height: calc(var(--c-icon-img, {icon["img"]}px) - 12px); |
|
|
padding: 6px; box-sizing: border-box; object-fit: contain; |
|
|
background: #FFFFFF; border: 2px solid var(--c-border, var(--cardStroke)); border-radius: 14px; display:block; |
|
|
}} |
|
|
|
|
|
.card h3 {{ |
|
|
margin: 0; font-size: 25px; font-weight: 900; |
|
|
color: var(--c-title-color, {card["title_color"]}); letter-spacing: .15px; |
|
|
}} |
|
|
.card p {{ |
|
|
color: var(--c-blurb-color, {card["blurb_color"]}); min-height: 40px; margin: 0 10px; font-size: 16px; |
|
|
}} |
|
|
|
|
|
.btn {{ |
|
|
display:inline-block; padding: {button["pad_v"]}px {button["pad_h"]}px; |
|
|
border-radius: {button["radius"]}px; |
|
|
border: 1px solid {button["border"]}; |
|
|
background: linear-gradient(180deg, {button["bg1"]} 0%, {button["bg2"]} 100%); |
|
|
font-weight: 800; letter-spacing:.3px; font-size: 16px; |
|
|
margin: 6px auto 0; |
|
|
box-shadow: 0 12px 28px rgba(11,18,32,.28), inset 0 1px 0 rgba(255,255,255,.10); |
|
|
color: {button["text"]}; |
|
|
text-decoration: none; |
|
|
}} |
|
|
a.btn, a.btn:link, a.btn:visited, a.btn:hover, a.btn:active, a.btn:focus {{ |
|
|
color: {button["text"]} !important; text-decoration: none !important; |
|
|
}} |
|
|
.btn:hover {{ |
|
|
filter: brightness(1.03) saturate(1.04); |
|
|
transform: translateY(-1px); |
|
|
box-shadow: 0 16px 38px rgba(11,18,32,.36); |
|
|
}} |
|
|
|
|
|
.footer {{ text-align:center; color:#3f4a5a; font-size:0.96em; margin-top: 26px; }} |
|
|
.footer hr {{ margin: 12px 0; border-color: rgba(0,0,0,.08); }} |
|
|
</style> |
|
|
""", |
|
|
unsafe_allow_html=True, |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown( |
|
|
""" |
|
|
<div class="suite-row"> |
|
|
<span class="suite-pill">ST_GeoMech SUITE</span> |
|
|
<span class="suite-tagline">Predicting Rock Mechanical Behaviour While Drilling</span> |
|
|
</div> |
|
|
""", |
|
|
unsafe_allow_html=True, |
|
|
) |
|
|
|
|
|
|
|
|
hero_html = img_tag(THEME["hero"]["logo"], "ST_GeoMech SUITE") |
|
|
st.markdown(f"<div class='hero'>{hero_html}</div>", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
def build_card_vars(style: Dict[str, Any]) -> str: |
|
|
s = [] |
|
|
if "width" in style: |
|
|
s.append(f"--c-w:{int(style['width'])}px") |
|
|
if "radius" in style: |
|
|
s.append(f"--c-radius:{int(style['radius'])}px") |
|
|
if "pad_v" in style: |
|
|
s.append(f"--c-pv:{int(style['pad_v'])}px") |
|
|
if "pad_h" in style: |
|
|
s.append(f"--c-ph:{int(style['pad_h'])}px") |
|
|
if "bg_top" in style: |
|
|
s.append(f"--c-bg-top:{style['bg_top']}") |
|
|
if "bg_bot" in style: |
|
|
s.append(f"--c-bg-bot:{style['bg_bot']}") |
|
|
if "border" in style: |
|
|
s.append(f"--c-border:{style['border']}") |
|
|
if "border_width" in style: |
|
|
s.append(f"--c-bw:{int(style['border_width'])}px") |
|
|
if "title_color" in style: |
|
|
s.append(f"--c-title-color:{style['title_color']}") |
|
|
if "title_fs" in style: |
|
|
s.append(f"--c-title-fs:{int(style['title_fs'])}px") |
|
|
if "blurb_color" in style: |
|
|
s.append(f"--c-blurb-color:{style['blurb_color']}") |
|
|
if "blurb_fs" in style: |
|
|
s.append(f"--c-blurb-fs:{int(style['blurb_fs'])}px") |
|
|
if "btn_bg1" in style: |
|
|
s.append(f"--c-btn-bg1:{style['btn_bg1']}") |
|
|
if "btn_bg2" in style: |
|
|
s.append(f"--c-btn-bg2:{style['btn_bg2']}") |
|
|
if "btn_text" in style: |
|
|
s.append(f"--c-btn-text:{style['btn_text']}") |
|
|
if "btn_border" in style: |
|
|
s.append(f"--c-btn-border:{style['btn_border']}") |
|
|
if "btn_fs" in style: |
|
|
s.append(f"--c-btn-fs:{int(style['btn_fs'])}px") |
|
|
if "btn_pad_v" in style: |
|
|
s.append(f"--c-btn-pv:{int(style['btn_pad_v'])}px") |
|
|
if "btn_pad_h" in style: |
|
|
s.append(f"--c-btn-ph:{int(style['btn_pad_h'])}px") |
|
|
if "icon_diam" in style: |
|
|
s.append(f"--c-icon-d:{int(style['icon_diam'])}px") |
|
|
if "icon_img" in style: |
|
|
s.append(f"--c-icon-img:{int(style['icon_img'])}px") |
|
|
if "icon_bg" in style: |
|
|
s.append(f"--c-icon-bg:{style['icon_bg']}") |
|
|
if "icon_border" in style: |
|
|
s.append(f"--c-icon-border:{style['icon_border']}") |
|
|
return "; ".join(s) |
|
|
|
|
|
|
|
|
def app_card(card_cfg: Dict[str, Any]) -> str: |
|
|
style = card_cfg.get("style", {}) |
|
|
vars_inline = build_card_vars(style) |
|
|
icon_path = card_cfg.get("icon") |
|
|
icon_html = img_tag(icon_path, "icon") if isinstance(icon_path, Path) and icon_path.exists() else "" |
|
|
target = "_self" |
|
|
return ( |
|
|
f"<div class='card' style='{vars_inline}'>" |
|
|
+ f"<div class='icon-wrap'>{icon_html}</div>" |
|
|
+ f"<h3>{escape(card_cfg['title'])}</h3>" |
|
|
+ f"<p>{escape(card_cfg['blurb'])}</p>" |
|
|
+ f"<a class='btn' href='{escape(card_cfg['url'])}' target='{target}' rel='noopener'>Run App</a>" |
|
|
+ "</div>" |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown("<div class='grid'>" + "".join(app_card(c) for c in CARDS) + "</div>", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.markdown( |
|
|
""" |
|
|
<hr> |
|
|
<div class='footer'> |
|
|
© 2025 Smart Thinking AI-Solutions Team. All rights reserved.<br> |
|
|
Website: <a href="https://smartthinking.com.sa" target="_blank" rel="noopener noreferrer">smartthinking.com.sa</a> |
|
|
</div> |
|
|
""", |
|
|
unsafe_allow_html=True, |
|
|
) |
|
|
|