MSU576 commited on
Commit
d48d29a
·
verified ·
1 Parent(s): 81788a3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +230 -45
app.py CHANGED
@@ -1864,16 +1864,201 @@ REPORT_FIELDS = [
1864
  ("Allowable Bearing (kPa)", "kPa"),
1865
  ]
1866
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1867
  def reports_page():
1868
  st.header("📑 Reports — Classification & Full Geotechnical")
1869
  site = st.session_state["sites"][st.session_state["active_site"]]
1870
 
 
1871
  st.subheader("Classification-only report")
1872
  if site.get("classifier_decision"):
1873
  st.markdown("You have a saved classification for this site.")
1874
  if st.button("Generate Classification PDF"):
1875
  fname = f"classification_{site['Site Name'].replace(' ','_')}.pdf"
1876
- # simple PDF
1877
  buffer = io.BytesIO()
1878
  doc = SimpleDocTemplate(buffer, pagesize=A4)
1879
  elems = []
@@ -1883,25 +2068,34 @@ def reports_page():
1883
  elems.append(Spacer(1,6))
1884
  elems.append(Paragraph("Classification result:", getSampleStyleSheet()['Heading2']))
1885
  elems.append(Paragraph(site.get("classifier_decision","-"), getSampleStyleSheet()['BodyText']))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1886
  doc.build(elems)
1887
  buffer.seek(0)
1888
  st.download_button("Download Classification PDF", buffer, file_name=fname, mime="application/pdf")
1889
  else:
1890
  st.info("No classification saved for this site yet. Use the Classifier page.")
1891
 
1892
- # --------- Dynamic form for quick report inputs + LLM analysis ----------
1893
  st.markdown("### Quick report form (edit values and request LLM analysis)")
1894
- site = st.session_state["sites"][st.session_state["active_site"]]
1895
-
1896
- # Build form-style table
1897
  with st.form(key="report_quick_form"):
1898
- cols = st.columns([2,1,1]) # name, value, unit/notes
1899
- # header row visually
1900
  cols[0].markdown("**Parameter**")
1901
  cols[1].markdown("**Value**")
1902
  cols[2].markdown("**Unit / Notes**")
1903
 
1904
- # build inputs dynamically from REPORT_FIELDS
1905
  inputs = {}
1906
  for (fld, unit) in REPORT_FIELDS:
1907
  c1, c2, c3 = st.columns([2,1,1])
@@ -1917,10 +2111,9 @@ def reports_page():
1917
  site[fld] = val if val != "" else "Not provided"
1918
  st.success("Saved quick report values to active site.")
1919
 
1920
- # LLM analysis button
1921
  st.markdown("#### LLM-powered analysis")
1922
  if st.button("Ask GeoMate (generate analysis & recommendations)"):
1923
- # prepare context for the LLM from the site
1924
  context = {
1925
  "site_name": site.get("Site Name"),
1926
  "project": site.get("Project Name"),
@@ -1938,58 +2131,45 @@ def reports_page():
1938
  "options and 4) short design notes. Provide any numeric outputs in the format [[FIELD: value unit]].\n\n"
1939
  f"Context: {json.dumps(context)}\n\nAnswer concisely and professionally."
1940
  )
1941
- # NOTE: Assuming groq_generate is a function you have defined elsewhere
1942
  # resp = groq_generate(prompt, model=st.session_state["llm_model"], max_tokens=600)
1943
- resp = "This is a placeholder response for demonstration." # Placeholder
1944
-
1945
- # display
1946
  st.markdown("**GeoMate analysis**")
1947
  st.markdown(resp)
1948
- # try to extract numeric fields (same bracket format as elsewhere)
1949
  matches = re.findall(r"\[\[([A-Za-z0-9 _/-]+):\s*([0-9.+-eE]+)\s*([A-Za-z%\/]*)\]\]", resp)
1950
  for m in matches:
1951
- field = m[0].strip()
1952
- val = m[1].strip()
1953
- unit = m[2].strip()
1954
- # map likely field names:
1955
  if "bearing" in field.lower():
1956
  site["Load Bearing Capacity"] = f"{val} {unit}"
1957
  elif "skin" in field.lower():
1958
  site["Skin Shear Strength"] = f"{val} {unit}"
1959
  elif "compaction" in field.lower():
1960
  site["Relative Compaction"] = f"{val} {unit}"
1961
- # store the analysis text so it can be included in the PDF later
1962
  site["LLM_Report_Text"] = resp
1963
  st.success("LLM analysis saved to site under 'LLM_Report_Text'.")
1964
 
 
1965
  st.markdown("---")
1966
  st.subheader("Full Geotechnical Report (chatbot will gather missing fields)")
1967
  if st.button("Start Report Chatbot"):
1968
  st.session_state["sites"][st.session_state["active_site"]]["report_convo_state"] = 0
1969
  st.rerun()
1970
 
1971
- # Conversational data collection
1972
  state = site.get("report_convo_state", -1)
1973
  if state >= 0:
1974
  st.markdown("Chatbot will ask for missing fields. You can answer or type 'skip' to leave blank.")
1975
- # Show current known fields
1976
- st.write("Current key parameters (live):")
1977
- show_table = []
1978
- for f,_ in REPORT_FIELDS:
1979
- show_table.append((f, site.get(f, "Not provided")))
1980
  st.table(show_table)
1981
-
1982
- # continue conversation step-by-step
1983
  if state < len(REPORT_FIELDS):
1984
  field, unit = REPORT_FIELDS[state]
1985
  ans = st.text_input(f"GeoMate — Please provide '{field}' ({unit})", key=f"report_in_{state}")
1986
  c1, c2 = st.columns([1,1])
1987
  with c1:
1988
  if st.button("Submit", key=f"report_submit_{state}"):
1989
- if ans.strip().lower() in ("skip","don't know","dont know","na","n/a",""):
1990
- site[field] = "Not provided"
1991
- else:
1992
- site[field] = ans.strip()
1993
  site["report_convo_state"] = state + 1
1994
  st.rerun()
1995
  with c2:
@@ -1999,20 +2179,25 @@ def reports_page():
1999
  st.rerun()
2000
  else:
2001
  st.success("All report questions asked. You can generate the full report now.")
2002
- if st.button("Generate Full Geotechnical Report PDF"):
2003
- # Prepare ext_refs
2004
- ext_ref_text = st.text_area("Optional: External references (one per line)", value="")
2005
- ext_refs = [r.strip() for r in ext_ref_text.splitlines() if r.strip()]
2006
- # Build PDF using reportlab builder
2007
- outname = f"Full_Geotech_Report_{site.get('Site Name','site')}.pdf"
2008
- # include map image bytes if available
2009
- mapimg = site.get("map_snapshot")
2010
- # NOTE: Assuming build_full_geotech_pdf is defined elsewhere
2011
- # build_full_geotech_pdf(site, outname, include_map_image=mapimg, ext_refs=ext_refs)
2012
- st.info("Placeholder for PDF generation.") # Placeholder
2013
- # with open(outname, "rb") as f:
2014
- # st.download_button("Download Full Geotechnical Report", f, file_name=outname, mime="application/pdf")
 
 
2015
 
 
 
 
2016
  # 8) Page router
2017
  if "page" not in st.session_state:
2018
  st.session_state["page"] = "Home"
 
1864
  ("Allowable Bearing (kPa)", "kPa"),
1865
  ]
1866
 
1867
+ # -------------------------------
1868
+ # Imports
1869
+ # -------------------------------
1870
+ import io, re, json, tempfile
1871
+ from datetime import datetime
1872
+ from typing import Dict, Any, Optional, List
1873
+
1874
+ import streamlit as st
1875
+ from reportlab.platypus import (
1876
+ SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle, Image as RLImage
1877
+ )
1878
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
1879
+ from reportlab.lib import colors
1880
+ from reportlab.lib.pagesizes import A4
1881
+ from reportlab.lib.units import mm
1882
+
1883
+ # -------------------------------
1884
+ # PDF Builder
1885
+ # -------------------------------
1886
+ def build_full_geotech_pdf(
1887
+ site: Dict[str, Any],
1888
+ filename: str,
1889
+ include_map_image: Optional[bytes] = None,
1890
+ ext_refs: Optional[List[str]] = None
1891
+ ):
1892
+ """
1893
+ Build a professional PDF report using site data + references.
1894
+ """
1895
+ styles = getSampleStyleSheet()
1896
+ title_style = ParagraphStyle(
1897
+ "title", parent=styles["Title"], fontSize=20, alignment=1,
1898
+ textColor=colors.HexColor("#FF7A00")
1899
+ )
1900
+ h1 = ParagraphStyle(
1901
+ "h1", parent=styles["Heading1"], fontSize=14,
1902
+ textColor=colors.HexColor("#1F4E79"), spaceAfter=6
1903
+ )
1904
+ body = ParagraphStyle("body", parent=styles["BodyText"], fontSize=10.5, leading=13)
1905
+ bullet = ParagraphStyle("bullet", parent=body, leftIndent=12, bulletIndent=6)
1906
+
1907
+ doc = SimpleDocTemplate(
1908
+ filename, pagesize=A4,
1909
+ leftMargin=18*mm, rightMargin=18*mm,
1910
+ topMargin=18*mm, bottomMargin=18*mm
1911
+ )
1912
+ elems = []
1913
+
1914
+ # --- Title page ---
1915
+ elems.append(Paragraph("GEOTECHNICAL INVESTIGATION REPORT", title_style))
1916
+ elems.append(Spacer(1, 12))
1917
+ company = site.get("Company Name", "Client / Company: Not provided")
1918
+ contact = site.get("Company Contact", "")
1919
+ elems.append(Paragraph(f"<b>{company}</b>", body))
1920
+ if contact:
1921
+ elems.append(Paragraph(contact, body))
1922
+ elems.append(Spacer(1, 12))
1923
+ elems.append(Paragraph(f"<b>Project:</b> {site.get('Project Name','-')}", body))
1924
+ elems.append(Paragraph(f"<b>Site:</b> {site.get('Site Name','-')}", body))
1925
+ elems.append(Paragraph(f"<b>Date:</b> {datetime.today().strftime('%Y-%m-%d')}", body))
1926
+ elems.append(PageBreak())
1927
+
1928
+ # --- Table of contents ---
1929
+ elems.append(Paragraph("TABLE OF CONTENTS", h1))
1930
+ toc_items = [
1931
+ "1.0 Introduction",
1932
+ "2.0 Site description and geology",
1933
+ "3.0 Field investigation & laboratory testing",
1934
+ "4.0 Evaluation of geotechnical properties",
1935
+ "5.0 Provisional site classification",
1936
+ "6.0 Recommendations",
1937
+ "7.0 LLM Analysis",
1938
+ "8.0 Figures & Tables",
1939
+ "9.0 Appendices & References"
1940
+ ]
1941
+ for i, t in enumerate(toc_items, start=1):
1942
+ elems.append(Paragraph(f"{i}. {t}", body))
1943
+ elems.append(PageBreak())
1944
+
1945
+ # --- Summary ---
1946
+ elems.append(Paragraph("SUMMARY", h1))
1947
+ summary_bullets = [
1948
+ f"Site: {site.get('Site Name','-')}.",
1949
+ f"General geology: {site.get('Soil Profile','Not provided')}.",
1950
+ f"Key lab tests: {', '.join([r.get('sampleId','') for r in site.get('Laboratory Results',[])]) if site.get('Laboratory Results') else 'No lab results provided.'}",
1951
+ f"Classification: USCS = {site.get('USCS','Not provided')}; AASHTO = {site.get('AASHTO','Not provided')}.",
1952
+ "Primary recommendation: See Recommendations section."
1953
+ ]
1954
+ for s in summary_bullets:
1955
+ elems.append(Paragraph(f"• {s}", bullet))
1956
+ elems.append(PageBreak())
1957
+
1958
+ # --- Introduction ---
1959
+ elems.append(Paragraph("1.0 INTRODUCTION", h1))
1960
+ intro_text = site.get("Project Description", "Project description not provided.")
1961
+ elems.append(Paragraph(intro_text, body))
1962
+
1963
+ # --- Site description & geology ---
1964
+ elems.append(Paragraph("2.0 SITE DESCRIPTION AND GEOLOGY", h1))
1965
+ site_geo = [
1966
+ f"Topography: {site.get('Topography','Not provided')}",
1967
+ f"Drainage: {site.get('Drainage','Not provided')}",
1968
+ f"Current land use: {site.get('Current Land Use','Not provided')}",
1969
+ f"Regional geology: {site.get('Regional Geology','Not provided')}"
1970
+ ]
1971
+ for t in site_geo:
1972
+ elems.append(Paragraph(t, body))
1973
+ elems.append(PageBreak())
1974
+
1975
+ # --- Field & lab testing ---
1976
+ elems.append(Paragraph("3.0 FIELD INVESTIGATION & LABORATORY TESTING", h1))
1977
+ if site.get("Field Investigation"):
1978
+ for item in site["Field Investigation"]:
1979
+ elems.append(Paragraph(f"<b>{item.get('id','Test')}</b> — depth {item.get('depth','-')}", body))
1980
+ for layer in item.get("layers", []):
1981
+ elems.append(Paragraph(f"- {layer.get('depth','')} : {layer.get('description','')}", body))
1982
+ else:
1983
+ elems.append(Paragraph("No field investigation data supplied.", body))
1984
+
1985
+ lab_rows = site.get("Laboratory Results", [])
1986
+ if lab_rows:
1987
+ elems.append(Spacer(1, 6))
1988
+ elems.append(Paragraph("Laboratory Results", h1))
1989
+ data = [["Sample ID","Material","LL","PI","Linear Shrinkage","%Clay","%Silt","%Sand","%Gravel","Expansiveness"]]
1990
+ for r in lab_rows:
1991
+ data.append([
1992
+ r.get("sampleId","-"), r.get("material","-"),
1993
+ str(r.get("liquidLimit","-")), str(r.get("plasticityIndex","-")),
1994
+ str(r.get("linearShrinkage","-")), str(r.get("percentClay","-")),
1995
+ str(r.get("percentSilt","-")), str(r.get("percentSand","-")),
1996
+ str(r.get("percentGravel","-")), r.get("potentialExpansiveness","-")
1997
+ ])
1998
+ t = Table(data, repeatRows=1, colWidths=[40*mm,40*mm,18*mm,18*mm,22*mm,20*mm,20*mm,20*mm,20*mm,30*mm])
1999
+ t.setStyle(TableStyle([
2000
+ ('BACKGROUND',(0,0),(-1,0),colors.HexColor("#1F4E79")),
2001
+ ('TEXTCOLOR',(0,0),(-1,0),colors.white),
2002
+ ('GRID',(0,0),(-1,-1),0.4,colors.grey),
2003
+ ('BOX',(0,0),(-1,-1),1,colors.HexColor("#FF7A00"))
2004
+ ]))
2005
+ elems.append(t)
2006
+ elems.append(PageBreak())
2007
+
2008
+ # --- Evaluation & classification ---
2009
+ elems.append(Paragraph("4.0 EVALUATION OF GEOTECHNICAL PROPERTIES", h1))
2010
+ elems.append(Paragraph(site.get("Evaluation","Evaluation not provided."), body))
2011
+ elems.append(Paragraph("5.0 PROVISIONAL SITE CLASSIFICATION", h1))
2012
+ elems.append(Paragraph(site.get("Provisional Classification","Not provided."), body))
2013
+ elems.append(Paragraph("6.0 RECOMMENDATIONS", h1))
2014
+ elems.append(Paragraph(site.get("Recommendations","Not provided."), body))
2015
+
2016
+ # --- LLM Analysis ---
2017
+ elems.append(Paragraph("7.0 LLM ANALYSIS (GeoMate)", h1))
2018
+ llm_text = site.get("LLM_Report_Text", None)
2019
+ if llm_text:
2020
+ elems.append(Paragraph(llm_text.replace("\n","\n\n"), body))
2021
+ else:
2022
+ elems.append(Paragraph("No LLM analysis saved for this site.", body))
2023
+
2024
+ # --- Map snapshot ---
2025
+ if include_map_image:
2026
+ try:
2027
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
2028
+ tmp.write(include_map_image)
2029
+ tmp.flush()
2030
+ elems.append(PageBreak())
2031
+ elems.append(Paragraph("Map Snapshot", h1))
2032
+ elems.append(RLImage(tmp.name, width=160*mm, height=90*mm))
2033
+ except Exception:
2034
+ pass
2035
+
2036
+ # --- References ---
2037
+ elems.append(PageBreak())
2038
+ elems.append(Paragraph("9.0 APPENDICES & REFERENCES", h1))
2039
+ if ext_refs:
2040
+ for r in ext_refs:
2041
+ elems.append(Paragraph(f"- {r}", body))
2042
+ else:
2043
+ elems.append(Paragraph("- No external references provided.", body))
2044
+
2045
+ doc.build(elems)
2046
+ return filename
2047
+
2048
+
2049
+ # -------------------------------
2050
+ # Reports Page
2051
+ # -------------------------------
2052
  def reports_page():
2053
  st.header("📑 Reports — Classification & Full Geotechnical")
2054
  site = st.session_state["sites"][st.session_state["active_site"]]
2055
 
2056
+ # ---------------- Classification Report ----------------
2057
  st.subheader("Classification-only report")
2058
  if site.get("classifier_decision"):
2059
  st.markdown("You have a saved classification for this site.")
2060
  if st.button("Generate Classification PDF"):
2061
  fname = f"classification_{site['Site Name'].replace(' ','_')}.pdf"
 
2062
  buffer = io.BytesIO()
2063
  doc = SimpleDocTemplate(buffer, pagesize=A4)
2064
  elems = []
 
2068
  elems.append(Spacer(1,6))
2069
  elems.append(Paragraph("Classification result:", getSampleStyleSheet()['Heading2']))
2070
  elems.append(Paragraph(site.get("classifier_decision","-"), getSampleStyleSheet()['BodyText']))
2071
+
2072
+ # Add FAISS citations if present in rag_history
2073
+ if "rag_history" in st.session_state and site.get("Site ID") in st.session_state["rag_history"]:
2074
+ refs = []
2075
+ for h in st.session_state["rag_history"][site["Site ID"]]:
2076
+ if h["who"]=="bot" and "[ref:" in h["text"]:
2077
+ for m in re.findall(r"\[ref:([^\]]+)\]", h["text"]):
2078
+ refs.append(m)
2079
+ if refs:
2080
+ elems.append(Spacer(1,12))
2081
+ elems.append(Paragraph("References:", getSampleStyleSheet()['Heading2']))
2082
+ for r in set(refs):
2083
+ elems.append(Paragraph(f"- {r}", getSampleStyleSheet()['Normal']))
2084
+
2085
  doc.build(elems)
2086
  buffer.seek(0)
2087
  st.download_button("Download Classification PDF", buffer, file_name=fname, mime="application/pdf")
2088
  else:
2089
  st.info("No classification saved for this site yet. Use the Classifier page.")
2090
 
2091
+ # ---------------- Quick Report Form ----------------
2092
  st.markdown("### Quick report form (edit values and request LLM analysis)")
 
 
 
2093
  with st.form(key="report_quick_form"):
2094
+ cols = st.columns([2,1,1])
 
2095
  cols[0].markdown("**Parameter**")
2096
  cols[1].markdown("**Value**")
2097
  cols[2].markdown("**Unit / Notes**")
2098
 
 
2099
  inputs = {}
2100
  for (fld, unit) in REPORT_FIELDS:
2101
  c1, c2, c3 = st.columns([2,1,1])
 
2111
  site[fld] = val if val != "" else "Not provided"
2112
  st.success("Saved quick report values to active site.")
2113
 
2114
+ # ---------------- LLM Analysis ----------------
2115
  st.markdown("#### LLM-powered analysis")
2116
  if st.button("Ask GeoMate (generate analysis & recommendations)"):
 
2117
  context = {
2118
  "site_name": site.get("Site Name"),
2119
  "project": site.get("Project Name"),
 
2131
  "options and 4) short design notes. Provide any numeric outputs in the format [[FIELD: value unit]].\n\n"
2132
  f"Context: {json.dumps(context)}\n\nAnswer concisely and professionally."
2133
  )
 
2134
  # resp = groq_generate(prompt, model=st.session_state["llm_model"], max_tokens=600)
2135
+ resp = "This is a placeholder response with citations [ref:Soil_Manual_2020] [[Load Bearing Capacity: 180 kPa]]"
2136
+
 
2137
  st.markdown("**GeoMate analysis**")
2138
  st.markdown(resp)
2139
+
2140
  matches = re.findall(r"\[\[([A-Za-z0-9 _/-]+):\s*([0-9.+-eE]+)\s*([A-Za-z%\/]*)\]\]", resp)
2141
  for m in matches:
2142
+ field, val, unit = m[0].strip(), m[1].strip(), m[2].strip()
 
 
 
2143
  if "bearing" in field.lower():
2144
  site["Load Bearing Capacity"] = f"{val} {unit}"
2145
  elif "skin" in field.lower():
2146
  site["Skin Shear Strength"] = f"{val} {unit}"
2147
  elif "compaction" in field.lower():
2148
  site["Relative Compaction"] = f"{val} {unit}"
2149
+
2150
  site["LLM_Report_Text"] = resp
2151
  st.success("LLM analysis saved to site under 'LLM_Report_Text'.")
2152
 
2153
+ # ---------------- Full Report Chatbot ----------------
2154
  st.markdown("---")
2155
  st.subheader("Full Geotechnical Report (chatbot will gather missing fields)")
2156
  if st.button("Start Report Chatbot"):
2157
  st.session_state["sites"][st.session_state["active_site"]]["report_convo_state"] = 0
2158
  st.rerun()
2159
 
 
2160
  state = site.get("report_convo_state", -1)
2161
  if state >= 0:
2162
  st.markdown("Chatbot will ask for missing fields. You can answer or type 'skip' to leave blank.")
2163
+ show_table = [(f, site.get(f, "Not provided")) for f,_ in REPORT_FIELDS]
 
 
 
 
2164
  st.table(show_table)
2165
+
 
2166
  if state < len(REPORT_FIELDS):
2167
  field, unit = REPORT_FIELDS[state]
2168
  ans = st.text_input(f"GeoMate — Please provide '{field}' ({unit})", key=f"report_in_{state}")
2169
  c1, c2 = st.columns([1,1])
2170
  with c1:
2171
  if st.button("Submit", key=f"report_submit_{state}"):
2172
+ site[field] = ans.strip() if ans.strip() not in ("skip","don't know","dont know","na","n/a","") else "Not provided"
 
 
 
2173
  site["report_convo_state"] = state + 1
2174
  st.rerun()
2175
  with c2:
 
2179
  st.rerun()
2180
  else:
2181
  st.success("All report questions asked. You can generate the full report now.")
2182
+ ext_ref_text = st.text_area("Optional: External references (one per line)", value="")
2183
+ ext_refs = [r.strip() for r in ext_ref_text.splitlines() if r.strip()]
2184
+
2185
+ faiss_refs = []
2186
+ if "rag_history" in st.session_state and site.get("Site ID") in st.session_state["rag_history"]:
2187
+ for h in st.session_state["rag_history"][site["Site ID"]]:
2188
+ if h["who"]=="bot" and "[ref:" in h["text"]:
2189
+ for m in re.findall(r"\[ref:([^\]]+)\]", h["text"]):
2190
+ faiss_refs.append(m)
2191
+ all_refs = list(set(ext_refs + faiss_refs))
2192
+
2193
+ outname = f"Full_Geotech_Report_{site.get('Site Name','site')}.pdf"
2194
+ mapimg = site.get("map_snapshot")
2195
+
2196
+ build_full_geotech_pdf(site, outname, include_map_image=mapimg, ext_refs=all_refs)
2197
 
2198
+ with open(outname, "rb") as f:
2199
+ st.download_button("Download Full Geotechnical Report", f, file_name=outname, mime="application/pdf")
2200
+
2201
  # 8) Page router
2202
  if "page" not in st.session_state:
2203
  st.session_state["page"] = "Home"