Update app.py
Browse files
app.py
CHANGED
|
@@ -591,21 +591,130 @@ if "sites" not in st.session_state:
|
|
| 591 |
}
|
| 592 |
]
|
| 593 |
|
|
|
|
|
|
|
|
|
|
| 594 |
if "active_site" not in st.session_state:
|
| 595 |
st.session_state["active_site"] = 0
|
| 596 |
|
| 597 |
if "llm_model" not in st.session_state:
|
| 598 |
st.session_state["llm_model"] = "groq/compound"
|
| 599 |
|
| 600 |
-
MAX_SITES = 10 # optional: maximum number of sites
|
| 601 |
|
| 602 |
# -------------------
|
| 603 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
# -------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
with st.sidebar:
|
| 606 |
st.markdown("<h2 style='color:#FF8C00;margin:8px 0'>GeoMate V2</h2>", unsafe_allow_html=True)
|
| 607 |
|
| 608 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
st.markdown("### Project Sites")
|
| 610 |
|
| 611 |
site_names = [s.get("Site Name", f"Site {i}") for i, s in enumerate(st.session_state["sites"])]
|
|
@@ -1314,66 +1423,65 @@ def locator_page():
|
|
| 1314 |
st.session_state["sites"][active]["map_snapshot"] = map_bytes
|
| 1315 |
st.image(map_bytes, caption="Map Snapshot", use_column_width=True)
|
| 1316 |
|
| 1317 |
-
import
|
| 1318 |
-
|
| 1319 |
-
# Custom theme colors (orange/black/gray)
|
| 1320 |
-
COLOR_SCHEME = ["#FF6600", "#333333", "#FF9933", "#666666", "#FFCC99"]
|
| 1321 |
|
| 1322 |
-
# ---
|
|
|
|
|
|
|
| 1323 |
st.subheader("π Summary")
|
| 1324 |
|
| 1325 |
-
|
| 1326 |
-
st.
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
|
| 1331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1332 |
if lc_stats:
|
| 1333 |
-
|
| 1334 |
-
labels = list(lc_stats.keys())
|
| 1335 |
values = list(lc_stats.values())
|
| 1336 |
-
fig
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
ax.grid(True, linestyle="--", alpha=0.6)
|
| 1352 |
-
st.pyplot(fig)
|
| 1353 |
|
| 1354 |
-
# ---
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1365 |
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
d, v = zip(*temp_ts)
|
| 1370 |
-
fig, ax = plt.subplots()
|
| 1371 |
-
ax.plot(d, v, marker="o", color=COLOR_SCHEME[3])
|
| 1372 |
-
ax.set_title("LST Temperature (K)", fontsize=12, color=COLOR_SCHEME[1])
|
| 1373 |
-
ax.set_xlabel("Date")
|
| 1374 |
-
ax.set_ylabel("Kelvin (K)")
|
| 1375 |
-
ax.grid(True, linestyle="--", alpha=0.6)
|
| 1376 |
-
st.pyplot(fig)
|
| 1377 |
|
| 1378 |
|
| 1379 |
# GeoMate Ask (RAG) β simple chat with memory per site and auto-extract numeric values
|
|
@@ -1904,6 +2012,27 @@ if "page" not in st.session_state:
|
|
| 1904 |
st.session_state["page"] = "Home"
|
| 1905 |
|
| 1906 |
# Build horizontal option menu
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1907 |
selected = option_menu(
|
| 1908 |
None,
|
| 1909 |
PAGES,
|
|
@@ -1930,6 +2059,7 @@ st.session_state["page"] = selected
|
|
| 1930 |
if selected == "Home":
|
| 1931 |
st.title("π Welcome to GeoMate")
|
| 1932 |
st.write("Your geotechnical AI copilot.")
|
|
|
|
| 1933 |
|
| 1934 |
elif selected == "Soil recognizer":
|
| 1935 |
st.title("π Soil Recognizer")
|
|
@@ -1954,10 +2084,12 @@ elif selected == "Locator":
|
|
| 1954 |
elif selected == "RAG":
|
| 1955 |
st.title("π€ Knowledge Assistant")
|
| 1956 |
st.write("Query soil and geotechnical references with AI.")
|
|
|
|
| 1957 |
|
| 1958 |
elif selected == "Reports":
|
| 1959 |
st.title("π Reports")
|
| 1960 |
st.write("Generate classification and full reports.")
|
|
|
|
| 1961 |
|
| 1962 |
|
| 1963 |
# Display page content
|
|
|
|
| 591 |
}
|
| 592 |
]
|
| 593 |
|
| 594 |
+
# -------------------
|
| 595 |
+
# Session state defaults
|
| 596 |
+
# -------------------
|
| 597 |
if "active_site" not in st.session_state:
|
| 598 |
st.session_state["active_site"] = 0
|
| 599 |
|
| 600 |
if "llm_model" not in st.session_state:
|
| 601 |
st.session_state["llm_model"] = "groq/compound"
|
| 602 |
|
|
|
|
| 603 |
|
| 604 |
# -------------------
|
| 605 |
+
# API Keys
|
| 606 |
+
# -------------------
|
| 607 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
| 608 |
+
GEM_API_KEY = None
|
| 609 |
+
if "GEM_API" in st.secrets:
|
| 610 |
+
GEM_API_KEY = st.secrets["GEM_API"]
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
# -------------------
|
| 614 |
+
# Universal LLM Call
|
| 615 |
+
# -------------------
|
| 616 |
+
def llm_generate(prompt: str, model: str = None, max_tokens: int = 512) -> str:
|
| 617 |
+
"""Universal LLM call for Groq, Gemini, DeepSeek."""
|
| 618 |
+
model_name = model or st.session_state["llm_model"]
|
| 619 |
+
|
| 620 |
+
try:
|
| 621 |
+
# ------------------- GEMINI -------------------
|
| 622 |
+
if model_name.lower().startswith("gemini"):
|
| 623 |
+
import requests
|
| 624 |
+
if not GEM_API_KEY:
|
| 625 |
+
return "[LLM error: No GEM_API key found in secrets]"
|
| 626 |
+
|
| 627 |
+
url = (
|
| 628 |
+
f"https://generativelanguage.googleapis.com/v1beta/models/"
|
| 629 |
+
f"{model_name}:generateContent?key={GEM_API_KEY}"
|
| 630 |
+
)
|
| 631 |
+
headers = {"Content-Type": "application/json"}
|
| 632 |
+
payload = {
|
| 633 |
+
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
|
| 634 |
+
"generationConfig": {
|
| 635 |
+
"temperature": 0.2,
|
| 636 |
+
"maxOutputTokens": max_tokens
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
resp = requests.post(url, headers=headers, json=payload, timeout=60)
|
| 640 |
+
resp.raise_for_status()
|
| 641 |
+
data = resp.json()
|
| 642 |
+
return data["candidates"][0]["content"]["parts"][0]["text"].strip()
|
| 643 |
+
|
| 644 |
+
# ------------------- GROQ / DEEPSEEK / LLAMA -------------------
|
| 645 |
+
elif "groq" in model_name or "llama" in model_name or "deepseek" in model_name:
|
| 646 |
+
from groq import Groq
|
| 647 |
+
if not GROQ_API_KEY:
|
| 648 |
+
return "[LLM error: No GROQ_API key found in secrets/env]"
|
| 649 |
+
|
| 650 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 651 |
+
completion = client.chat.completions.create(
|
| 652 |
+
model=model_name,
|
| 653 |
+
messages=[{"role": "user", "content": prompt}],
|
| 654 |
+
temperature=0.2,
|
| 655 |
+
max_tokens=max_tokens
|
| 656 |
+
)
|
| 657 |
+
return completion.choices[0].message.content
|
| 658 |
+
|
| 659 |
+
else:
|
| 660 |
+
return f"[LLM error: Unknown model {model_name}]"
|
| 661 |
+
|
| 662 |
+
except Exception as e:
|
| 663 |
+
return f"[LLM error: {e}]"
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
# -------------------
|
| 667 |
+
# UI helper: CSS
|
| 668 |
# -------------------
|
| 669 |
+
st.markdown("""
|
| 670 |
+
<style>
|
| 671 |
+
/* Background and card styling */
|
| 672 |
+
body { background: #0b0b0b; color: #e9eef6; }
|
| 673 |
+
.stApp > .main > .block-container { padding-top: 18px; }
|
| 674 |
+
|
| 675 |
+
/* Landing and cards */
|
| 676 |
+
.gm-card { background: linear-gradient(180deg, rgba(255,122,0,0.04), rgba(255,122,0,0.02));
|
| 677 |
+
border-radius:12px; padding:14px; border:1px solid rgba(255,122,0,0.06);}
|
| 678 |
+
.gm-cta { background: linear-gradient(90deg,#ff7a00,#ff3a3a); color:white;
|
| 679 |
+
padding:10px 14px; border-radius:10px; font-weight:700; }
|
| 680 |
+
|
| 681 |
+
/* Chat bubbles */
|
| 682 |
+
.chat-bot { background: #0f1720; border-left:4px solid #FF7A00;
|
| 683 |
+
padding:10px 12px; border-radius:12px; margin:6px 0; color:#e9eef6; }
|
| 684 |
+
.chat-user { background: #1a1f27; padding:10px 12px; border-radius:12px;
|
| 685 |
+
margin:6px 0; color:#cfe6ff; text-align:right;}
|
| 686 |
+
.small-muted { color:#9aa7bf; font-size:12px; }
|
| 687 |
+
</style>
|
| 688 |
+
""", unsafe_allow_html=True)
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
# -------------------
|
| 692 |
+
# Sidebar: Navigation + Model + Site Management
|
| 693 |
+
# -------------------
|
| 694 |
+
from streamlit_option_menu import option_menu
|
| 695 |
+
|
| 696 |
with st.sidebar:
|
| 697 |
st.markdown("<h2 style='color:#FF8C00;margin:8px 0'>GeoMate V2</h2>", unsafe_allow_html=True)
|
| 698 |
|
| 699 |
+
# -------------------
|
| 700 |
+
# LLM model selector
|
| 701 |
+
# -------------------
|
| 702 |
+
st.session_state["llm_model"] = st.selectbox("Select LLM model", options=[
|
| 703 |
+
"meta-llama/llama-4-maverick-17b-128e-instruct",
|
| 704 |
+
"llama-3.1-8b-instant",
|
| 705 |
+
"meta-llama/llama-guard-4-12b",
|
| 706 |
+
"llama-3.3-70b-versatile",
|
| 707 |
+
"groq/compound",
|
| 708 |
+
"deepseek-r1-distill-llama-70b", # β
DeepSeek
|
| 709 |
+
"gemini-1.5-pro", # β
Gemini Pro
|
| 710 |
+
"gemini-1.5-flash" # β
Gemini Flash
|
| 711 |
+
], index=0)
|
| 712 |
+
|
| 713 |
+
st.markdown("---")
|
| 714 |
+
|
| 715 |
+
# -------------------
|
| 716 |
+
# Site management
|
| 717 |
+
# -------------------
|
| 718 |
st.markdown("### Project Sites")
|
| 719 |
|
| 720 |
site_names = [s.get("Site Name", f"Site {i}") for i, s in enumerate(st.session_state["sites"])]
|
|
|
|
| 1423 |
st.session_state["sites"][active]["map_snapshot"] = map_bytes
|
| 1424 |
st.image(map_bytes, caption="Map Snapshot", use_column_width=True)
|
| 1425 |
|
| 1426 |
+
import plotly.express as px
|
| 1427 |
+
import plotly.graph_objects as go
|
|
|
|
|
|
|
| 1428 |
|
| 1429 |
+
# -------------------------------
|
| 1430 |
+
# π Display Summaries (Locator)
|
| 1431 |
+
# -------------------------------
|
| 1432 |
st.subheader("π Summary")
|
| 1433 |
|
| 1434 |
+
# --- Metric Cards
|
| 1435 |
+
c1, c2, c3 = st.columns(3)
|
| 1436 |
+
c1.metric("π€ Soil (%)", f"{soil_val:.2f}" if soil_val else "N/A", help="Soil clay content")
|
| 1437 |
+
c2.metric("β°οΈ Elevation (m)", f"{elev_val:.1f}" if elev_val else "N/A", help="Mean elevation")
|
| 1438 |
+
c3.metric("πͺοΈ Seismic PGA", f"{seismic_val:.3f}" if seismic_val else "N/A", help="Seismic hazard index")
|
| 1439 |
|
| 1440 |
+
c4, c5, c6 = st.columns(3)
|
| 1441 |
+
c4.metric("π Flood Occurrence", f"{flood_val:.2f}" if flood_val else "N/A")
|
| 1442 |
+
c5.metric("π¨ PM2.5 Index", f"{pm25_val:.2f}" if pm25_val else "N/A")
|
| 1443 |
+
c6.metric("π’ NDVI Count", f"{len(ndvi_ts)} pts" if ndvi_ts else "0")
|
| 1444 |
+
|
| 1445 |
+
# --- Pie Chart for Landcover
|
| 1446 |
+
# --- Donut Chart for Landcover
|
| 1447 |
if lc_stats:
|
| 1448 |
+
labels = list(map(str, lc_stats.keys()))
|
|
|
|
| 1449 |
values = list(lc_stats.values())
|
| 1450 |
+
fig = go.Figure(data=[go.Pie(
|
| 1451 |
+
labels=labels,
|
| 1452 |
+
values=values,
|
| 1453 |
+
hole=0.5, # donut
|
| 1454 |
+
textinfo="percent+label", # show % and class
|
| 1455 |
+
insidetextorientation="radial",
|
| 1456 |
+
marker=dict(colors=px.colors.sequential.Oranges_r)
|
| 1457 |
+
)])
|
| 1458 |
+
fig.update_layout(
|
| 1459 |
+
title="π Landcover Distribution",
|
| 1460 |
+
template="plotly_dark",
|
| 1461 |
+
margin=dict(l=20, r=20, t=40, b=20)
|
| 1462 |
+
)
|
| 1463 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 1464 |
+
|
|
|
|
|
|
|
| 1465 |
|
| 1466 |
+
# --- Time Series with Plotly
|
| 1467 |
+
def plot_timeseries(ts, title, ylab, color):
|
| 1468 |
+
if ts:
|
| 1469 |
+
dates, vals = zip(*ts)
|
| 1470 |
+
fig = go.Figure()
|
| 1471 |
+
fig.add_trace(go.Scatter(x=dates, y=vals, mode="lines+markers", line=dict(color=color)))
|
| 1472 |
+
fig.update_layout(
|
| 1473 |
+
title=title,
|
| 1474 |
+
xaxis_title="Date",
|
| 1475 |
+
yaxis_title=ylab,
|
| 1476 |
+
template="plotly_dark",
|
| 1477 |
+
hovermode="x unified",
|
| 1478 |
+
margin=dict(l=20, r=20, t=40, b=20)
|
| 1479 |
+
)
|
| 1480 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 1481 |
|
| 1482 |
+
plot_timeseries(ndvi_ts, "πΏ NDVI Trend (2 years)", "NDVI", "#FF7A00")
|
| 1483 |
+
plot_timeseries(precip_ts, "π§οΈ Precipitation Trend (1 year)", "mm/day", "#00BFFF")
|
| 1484 |
+
plot_timeseries(temp_ts, "π‘οΈ Land Surface Temp (1 year)", "K", "#FF3333")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1485 |
|
| 1486 |
|
| 1487 |
# GeoMate Ask (RAG) β simple chat with memory per site and auto-extract numeric values
|
|
|
|
| 2012 |
st.session_state["page"] = "Home"
|
| 2013 |
|
| 2014 |
# Build horizontal option menu
|
| 2015 |
+
# ===============================
|
| 2016 |
+
# Sidebar or Top-Bar Model Selector
|
| 2017 |
+
# ===============================
|
| 2018 |
+
with st.sidebar:
|
| 2019 |
+
st.markdown("### βοΈ LLM Model Selector")
|
| 2020 |
+
st.session_state["llm_model"] = st.selectbox(
|
| 2021 |
+
"Choose AI Model",
|
| 2022 |
+
[
|
| 2023 |
+
"groq/compound", # Groq general model
|
| 2024 |
+
"deepseek-r1-distill-llama-70b", # DeepSeek
|
| 2025 |
+
"gemini-1.5-pro", # Gemini
|
| 2026 |
+
],
|
| 2027 |
+
index=0 if "llm_model" not in st.session_state else
|
| 2028 |
+
["groq/compound", "deepseek-r1-distill-llama-70b", "gemini-1.5-pro"].index(
|
| 2029 |
+
st.session_state.get("llm_model", "groq/compound")
|
| 2030 |
+
)
|
| 2031 |
+
)
|
| 2032 |
+
|
| 2033 |
+
# ===============================
|
| 2034 |
+
# Page Menu (Horizontal)
|
| 2035 |
+
# ===============================
|
| 2036 |
selected = option_menu(
|
| 2037 |
None,
|
| 2038 |
PAGES,
|
|
|
|
| 2059 |
if selected == "Home":
|
| 2060 |
st.title("π Welcome to GeoMate")
|
| 2061 |
st.write("Your geotechnical AI copilot.")
|
| 2062 |
+
st.info(f"Currently using **{st.session_state['llm_model']}** for analysis.")
|
| 2063 |
|
| 2064 |
elif selected == "Soil recognizer":
|
| 2065 |
st.title("π Soil Recognizer")
|
|
|
|
| 2084 |
elif selected == "RAG":
|
| 2085 |
st.title("π€ Knowledge Assistant")
|
| 2086 |
st.write("Query soil and geotechnical references with AI.")
|
| 2087 |
+
st.caption(f"Model in use: {st.session_state['llm_model']}")
|
| 2088 |
|
| 2089 |
elif selected == "Reports":
|
| 2090 |
st.title("π Reports")
|
| 2091 |
st.write("Generate classification and full reports.")
|
| 2092 |
+
st.caption(f"Analysis will run with: {st.session_state['llm_model']}")
|
| 2093 |
|
| 2094 |
|
| 2095 |
# Display page content
|