Update app.py
Browse files
app.py
CHANGED
|
@@ -23,6 +23,13 @@ from reportlab.lib.units import mm
|
|
| 23 |
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image as RLImage, PageBreak
|
| 24 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
# Optional imports handled gracefully
|
| 27 |
try:
|
| 28 |
import geemap
|
|
@@ -485,6 +492,71 @@ def build_full_geotech_pdf(site: Dict[str, Any], filename: str, include_map_imag
|
|
| 485 |
bullet = ParagraphStyle("bullet", parent=body, leftIndent=12, bulletIndent=6)
|
| 486 |
doc = SimpleDocTemplate(filename, pagesize=A4, leftMargin=18*mm, rightMargin=18*mm, topMargin=18*mm, bottomMargin=18*mm)
|
| 487 |
elems = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
# Cover
|
| 489 |
elems.append(Paragraph("GEOTECHNICAL INVESTIGATION REPORT", title_style))
|
| 490 |
elems.append(Spacer(1,6))
|
|
@@ -839,6 +911,31 @@ with st.sidebar:
|
|
| 839 |
|
| 840 |
# 7) Pages implementation
|
| 841 |
def landing_page():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
st.markdown("<div style='display:flex;align-items:center;gap:12px'>"
|
| 843 |
"<div style='width:76px;height:76px;border-radius:14px;background:linear-gradient(135deg,#ff7a00,#ff3a3a);display:flex;align-items:center;justify-content:center;box-shadow:0 8px 24px rgba(0,0,0,0.6)'>"
|
| 844 |
"<span style='font-size:34px'>π°οΈ</span></div>"
|
|
@@ -1386,7 +1483,17 @@ def locator_page():
|
|
| 1386 |
pass
|
| 1387 |
|
| 1388 |
# Enable drawing
|
| 1389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1390 |
m.to_streamlit(height=700, responsive=True)
|
| 1391 |
st.markdown("π Draw a polygon/rectangle on the map (draw tool). After drawing click **Compute Summaries**.")
|
| 1392 |
|
|
@@ -1690,19 +1797,19 @@ def rag_page():
|
|
| 1690 |
site["Relative Compaction"] = f"{val} {unit}"
|
| 1691 |
st.success("Response saved and any recognized numeric fields auto-stored in the site data.")
|
| 1692 |
|
| 1693 |
-
# Reports page β conversational missing-parameter bot & PDF generation
|
| 1694 |
-
REPORT_FIELDS = [
|
| 1695 |
-
|
| 1696 |
-
|
| 1697 |
-
|
| 1698 |
-
|
| 1699 |
-
|
| 1700 |
-
|
| 1701 |
-
|
| 1702 |
-
|
| 1703 |
-
|
| 1704 |
-
|
| 1705 |
-
]
|
| 1706 |
|
| 1707 |
def reports_page():
|
| 1708 |
st.header("π Reports β Classification & Full Geotechnical")
|
|
@@ -1729,6 +1836,77 @@ def reports_page():
|
|
| 1729 |
else:
|
| 1730 |
st.info("No classification saved for this site yet. Use the Classifier page.")
|
| 1731 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1732 |
st.markdown("---")
|
| 1733 |
st.subheader("Full Geotechnical Report (chatbot will gather missing fields)")
|
| 1734 |
if st.button("Start Report Chatbot"):
|
|
|
|
| 23 |
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image as RLImage, PageBreak
|
| 24 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 25 |
|
| 26 |
+
# Ensure icon fonts load (fix desktop icon display for option_menu)
|
| 27 |
+
st.markdown("""
|
| 28 |
+
<!-- Load icon fonts used by streamlit_option_menu -->
|
| 29 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
| 30 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 31 |
+
""", unsafe_allow_html=True)
|
| 32 |
+
|
| 33 |
# Optional imports handled gracefully
|
| 34 |
try:
|
| 35 |
import geemap
|
|
|
|
| 492 |
bullet = ParagraphStyle("bullet", parent=body, leftIndent=12, bulletIndent=6)
|
| 493 |
doc = SimpleDocTemplate(filename, pagesize=A4, leftMargin=18*mm, rightMargin=18*mm, topMargin=18*mm, bottomMargin=18*mm)
|
| 494 |
elems = []
|
| 495 |
+
# --- Title page (collect company/user info from site if present) ---
|
| 496 |
+
elems.append(Paragraph("GEOTECHNICAL INVESTIGATION REPORT", title_style))
|
| 497 |
+
elems.append(Spacer(1,12))
|
| 498 |
+
# Company / client block (if present)
|
| 499 |
+
company = site.get("Company Name", "Client / Company: Not provided")
|
| 500 |
+
contact = site.get("Company Contact", "")
|
| 501 |
+
elems.append(Paragraph(f"<b>{company}</b>", body))
|
| 502 |
+
if contact:
|
| 503 |
+
elems.append(Paragraph(contact, body))
|
| 504 |
+
elems.append(Spacer(1,12))
|
| 505 |
+
elems.append(Paragraph(f"<b>Project:</b> {site.get('Project Name','-')}", body))
|
| 506 |
+
elems.append(Paragraph(f"<b>Site:</b> {site.get('Site Name','-')}", body))
|
| 507 |
+
elems.append(Paragraph(f"<b>Date:</b> {datetime.today().strftime('%Y-%m-%d')}", body))
|
| 508 |
+
elems.append(PageBreak())
|
| 509 |
+
|
| 510 |
+
# --- Table of contents (simple listing with section titles) ---
|
| 511 |
+
elems.append(Paragraph("TABLE OF CONTENTS", h1))
|
| 512 |
+
toc_items = [
|
| 513 |
+
"1.0 Introduction",
|
| 514 |
+
"2.0 Site description and geology",
|
| 515 |
+
"3.0 Field investigation & laboratory testing",
|
| 516 |
+
"4.0 Evaluation of geotechnical properties",
|
| 517 |
+
"5.0 Provisional site classification",
|
| 518 |
+
"6.0 Recommendations",
|
| 519 |
+
"7.0 Figures & Tables",
|
| 520 |
+
"8.0 Appendices & References"
|
| 521 |
+
]
|
| 522 |
+
for i, t in enumerate(toc_items, start=1):
|
| 523 |
+
elems.append(Paragraph(f"{i}. {t}", body))
|
| 524 |
+
elems.append(PageBreak())
|
| 525 |
+
|
| 526 |
+
# --- (existing summary and subsequent sections as before) ---
|
| 527 |
+
elems.append(Paragraph("SUMMARY", h1))
|
| 528 |
+
...
|
| 529 |
+
# after '6.0 RECOMMENDATIONS' insertion, include LLM analysis if present
|
| 530 |
+
elems.append(Paragraph("7.0 LLM Analysis (GeoMate)", h1))
|
| 531 |
+
llm_text = site.get("LLM_Report_Text", None)
|
| 532 |
+
if llm_text:
|
| 533 |
+
elems.append(Paragraph(llm_text.replace("\n","\n\n"), body))
|
| 534 |
+
else:
|
| 535 |
+
elems.append(Paragraph("No LLM analysis saved for this site.", body))
|
| 536 |
+
|
| 537 |
+
# --- Figures/Tables index (simple lists from site data, if present) ---
|
| 538 |
+
elems.append(PageBreak())
|
| 539 |
+
elems.append(Paragraph("List of Tables & Figures", h1))
|
| 540 |
+
# Example: list lab samples and any GSD figure
|
| 541 |
+
if site.get("Laboratory Results"):
|
| 542 |
+
elems.append(Paragraph("Tables:", body))
|
| 543 |
+
for r in site.get("Laboratory Results", []):
|
| 544 |
+
elems.append(Paragraph(f"- Laboratory sample: {r.get('sampleId','-')}", body))
|
| 545 |
+
if site.get("GSD"):
|
| 546 |
+
elems.append(Paragraph("- GSD Curve (see section 3)", body))
|
| 547 |
+
|
| 548 |
+
# --- Appendices & References ---
|
| 549 |
+
elems.append(PageBreak())
|
| 550 |
+
elems.append(Paragraph("Appendices", h1))
|
| 551 |
+
elems.append(Paragraph("A. Test certificates and raw data (provided as CSV or appendices).", body))
|
| 552 |
+
elems.append(PageBreak())
|
| 553 |
+
elems.append(Paragraph("References", h1))
|
| 554 |
+
if ext_refs:
|
| 555 |
+
for r in ext_refs:
|
| 556 |
+
elems.append(Paragraph(f"- {r}", body))
|
| 557 |
+
else:
|
| 558 |
+
elems.append(Paragraph("- No external references provided.", body))
|
| 559 |
+
|
| 560 |
# Cover
|
| 561 |
elems.append(Paragraph("GEOTECHNICAL INVESTIGATION REPORT", title_style))
|
| 562 |
elems.append(Spacer(1,6))
|
|
|
|
| 911 |
|
| 912 |
# 7) Pages implementation
|
| 913 |
def landing_page():
|
| 914 |
+
def landing_page():
|
| 915 |
+
# Background hero with placeholder image (replace BACKGROUND_URL with your image path or URL)
|
| 916 |
+
BACKGROUND_URL = "/app/background_placeholder.jpg" # <- replace this (or provide URL)
|
| 917 |
+
st.markdown(f"""
|
| 918 |
+
<div style="
|
| 919 |
+
background-image: url('{BACKGROUND_URL}');
|
| 920 |
+
background-size: cover;
|
| 921 |
+
background-position: center;
|
| 922 |
+
padding: 48px 28px;
|
| 923 |
+
border-radius: 12px;
|
| 924 |
+
margin-bottom: 18px;
|
| 925 |
+
position: relative;
|
| 926 |
+
">
|
| 927 |
+
<div style="background: rgba(11,11,11,0.55); padding:22px; border-radius:10px; max-width:900px;">
|
| 928 |
+
<h1 style='color:#FF8C00; margin:0'>GeoMate V2</h1>
|
| 929 |
+
<p style='color:#e8eef6; margin:6px 0 0; font-size:16px'>
|
| 930 |
+
AI geotechnical copilot β soil recognition, classification, locator (EE), RAG-powered Q&A, and dynamic reports.
|
| 931 |
+
</p>
|
| 932 |
+
<div style='margin-top:8px; color:#cfcfcf; font-size:13px'>
|
| 933 |
+
Quick: Classifier β’ GSD β’ Locator β’ RAG β’ Reports
|
| 934 |
+
</div>
|
| 935 |
+
</div>
|
| 936 |
+
</div>
|
| 937 |
+
""", unsafe_allow_html=True)
|
| 938 |
+
|
| 939 |
st.markdown("<div style='display:flex;align-items:center;gap:12px'>"
|
| 940 |
"<div style='width:76px;height:76px;border-radius:14px;background:linear-gradient(135deg,#ff7a00,#ff3a3a);display:flex;align-items:center;justify-content:center;box-shadow:0 8px 24px rgba(0,0,0,0.6)'>"
|
| 941 |
"<span style='font-size:34px'>π°οΈ</span></div>"
|
|
|
|
| 1483 |
pass
|
| 1484 |
|
| 1485 |
# Enable drawing
|
| 1486 |
+
from geemap import Draw
|
| 1487 |
+
# Add draw control (rectangle + polygon only)
|
| 1488 |
+
draw_control = Draw(
|
| 1489 |
+
export=False,
|
| 1490 |
+
draw_polygon=True,
|
| 1491 |
+
draw_rectangle=True,
|
| 1492 |
+
draw_circle=False,
|
| 1493 |
+
draw_polyline=False,
|
| 1494 |
+
draw_marker=False
|
| 1495 |
+
)
|
| 1496 |
+
m.add_control(draw_control)
|
| 1497 |
m.to_streamlit(height=700, responsive=True)
|
| 1498 |
st.markdown("π Draw a polygon/rectangle on the map (draw tool). After drawing click **Compute Summaries**.")
|
| 1499 |
|
|
|
|
| 1797 |
site["Relative Compaction"] = f"{val} {unit}"
|
| 1798 |
st.success("Response saved and any recognized numeric fields auto-stored in the site data.")
|
| 1799 |
|
| 1800 |
+
# Reports page β conversational missing-parameter bot & PDF generation
|
| 1801 |
+
REPORT_FIELDS = [
|
| 1802 |
+
("Load Bearing Capacity","kPa or psf"),
|
| 1803 |
+
("Skin Shear Strength","kPa"),
|
| 1804 |
+
("Relative Compaction","%"),
|
| 1805 |
+
("Rate of Consolidation","mm/yr or days"),
|
| 1806 |
+
("Nature of Construction","text"),
|
| 1807 |
+
("Borehole Count","number"),
|
| 1808 |
+
("Max Depth (m)","m"),
|
| 1809 |
+
("SPT N (avg)","blows/ft"),
|
| 1810 |
+
("CBR (%)","%"),
|
| 1811 |
+
("Allowable Bearing (kPa)","kPa")
|
| 1812 |
+
]
|
| 1813 |
|
| 1814 |
def reports_page():
|
| 1815 |
st.header("π Reports β Classification & Full Geotechnical")
|
|
|
|
| 1836 |
else:
|
| 1837 |
st.info("No classification saved for this site yet. Use the Classifier page.")
|
| 1838 |
|
| 1839 |
+
# --------- Dynamic form for quick report inputs + LLM analysis ----------
|
| 1840 |
+
st.markdown("### Quick report form (edit values and request LLM analysis)")
|
| 1841 |
+
site = st.session_state["sites"][st.session_state["active_site"]]
|
| 1842 |
+
|
| 1843 |
+
# Build form-style table
|
| 1844 |
+
with st.form(key="report_quick_form"):
|
| 1845 |
+
cols = st.columns([2,1,1]) # name, value, unit/notes
|
| 1846 |
+
# header row visually
|
| 1847 |
+
cols[0].markdown("**Parameter**")
|
| 1848 |
+
cols[1].markdown("**Value**")
|
| 1849 |
+
cols[2].markdown("**Unit / Notes**")
|
| 1850 |
+
|
| 1851 |
+
# build inputs dynamically from REPORT_FIELDS
|
| 1852 |
+
inputs = {}
|
| 1853 |
+
for (fld, unit) in REPORT_FIELDS:
|
| 1854 |
+
c1, c2, c3 = st.columns([2,1,1])
|
| 1855 |
+
c1.markdown(f"**{fld}**")
|
| 1856 |
+
default_val = site.get(fld, "")
|
| 1857 |
+
inputs[fld] = c2.text_input(fld, value=str(default_val), key=f"quick_{fld}")
|
| 1858 |
+
c3.markdown(unit)
|
| 1859 |
+
|
| 1860 |
+
submitted = st.form_submit_button("Save values to site")
|
| 1861 |
+
if submitted:
|
| 1862 |
+
for fld, _ in REPORT_FIELDS:
|
| 1863 |
+
val = inputs.get(fld, "").strip()
|
| 1864 |
+
site[fld] = val if val != "" else "Not provided"
|
| 1865 |
+
st.success("Saved quick report values to active site.")
|
| 1866 |
+
|
| 1867 |
+
# LLM analysis button
|
| 1868 |
+
st.markdown("#### LLM-powered analysis")
|
| 1869 |
+
if st.button("Ask GeoMate (generate analysis & recommendations)"):
|
| 1870 |
+
# prepare context for the LLM from the site
|
| 1871 |
+
context = {
|
| 1872 |
+
"site_name": site.get("Site Name"),
|
| 1873 |
+
"project": site.get("Project Name"),
|
| 1874 |
+
"site_summary": {
|
| 1875 |
+
"USCS": site.get("USCS"), "AASHTO": site.get("AASHTO"), "GI": site.get("GI"),
|
| 1876 |
+
"Soil Profile": site.get("Soil Profile"),
|
| 1877 |
+
"Key lab results": [r.get("sampleId") for r in site.get("Laboratory Results",[])]
|
| 1878 |
+
},
|
| 1879 |
+
"inputs": {fld: site.get(fld,"Not provided") for fld,_ in REPORT_FIELDS}
|
| 1880 |
+
}
|
| 1881 |
+
prompt = (
|
| 1882 |
+
"You are GeoMate AI, an engineering assistant. Given the following site context and "
|
| 1883 |
+
"engineering parameters (some may be 'Not provided'), produce:\n1) short executive summary, "
|
| 1884 |
+
"2) geotechnical interpretation (classification, key risks), 3) recommended remedial/improvement "
|
| 1885 |
+
"options and 4) short design notes. Provide any numeric outputs in the format [[FIELD: value unit]].\n\n"
|
| 1886 |
+
f"Context: {json.dumps(context)}\n\nAnswer concisely and professionally."
|
| 1887 |
+
)
|
| 1888 |
+
resp = groq_generate(prompt, model=st.session_state["llm_model"], max_tokens=600)
|
| 1889 |
+
# display
|
| 1890 |
+
st.markdown("**GeoMate analysis**")
|
| 1891 |
+
st.markdown(resp)
|
| 1892 |
+
# try to extract numeric fields (same bracket format as elsewhere)
|
| 1893 |
+
import re
|
| 1894 |
+
matches = re.findall(r"\[\[([A-Za-z0-9 _/-]+):\s*([0-9.+-eE]+)\s*([A-Za-z%\/]*)\]\]", resp)
|
| 1895 |
+
for m in matches:
|
| 1896 |
+
field = m[0].strip()
|
| 1897 |
+
val = m[1].strip()
|
| 1898 |
+
unit = m[2].strip()
|
| 1899 |
+
# map likely field names:
|
| 1900 |
+
if "bearing" in field.lower():
|
| 1901 |
+
site["Load Bearing Capacity"] = f"{val} {unit}"
|
| 1902 |
+
elif "skin" in field.lower():
|
| 1903 |
+
site["Skin Shear Strength"] = f"{val} {unit}"
|
| 1904 |
+
elif "compaction" in field.lower():
|
| 1905 |
+
site["Relative Compaction"] = f"{val} {unit}"
|
| 1906 |
+
# store the analysis text so it can be included in the PDF later
|
| 1907 |
+
site["LLM_Report_Text"] = resp
|
| 1908 |
+
st.success("LLM analysis saved to site under 'LLM_Report_Text'.")
|
| 1909 |
+
|
| 1910 |
st.markdown("---")
|
| 1911 |
st.subheader("Full Geotechnical Report (chatbot will gather missing fields)")
|
| 1912 |
if st.button("Start Report Chatbot"):
|