UCS2014's picture
Update app.py
95802d0 verified
# -*- coding: utf-8 -*-
from typing import Optional, Dict, Any
import base64
import mimetypes
from html import escape
from pathlib import Path
import streamlit as st
# ========= PATHS =========
BASE_DIR = Path(__file__).parent
ASSETS = BASE_DIR / "assets"
# ========= META =========
st.set_page_config(page_title="ST_GeoMech SUITE", layout="wide")
# ========= CARDS PER ROW (single control) =========
# Change this to any integer >= 1 (e.g., 3, 4, 5...)
CARDS_PER_ROW: int = 3
# ========= GLOBAL THEME (one place) =========
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 is now fluid; keep for max-width if you like
"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 (content + per-card overrides) =========
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"},
},
]
# ========= Helpers =========
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)}" />'
# ========= CSS (driven from THEME) =========
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,
)
# ========= Strip (top-left) =========
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 =========
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)
# ========= Footer =========
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,
)