Spaces:
Sleeping
Sleeping
Upload Advisernote.py
Browse files- Advisernote.py +274 -6
Advisernote.py
CHANGED
|
@@ -136,7 +136,9 @@ if 'case_comments' not in st.session_state: st.session_state['case_comments'] =
|
|
| 136 |
# =========================================================================
|
| 137 |
# [보μ λνμ] μ΅μ΄ λ‘κ·ΈμΈ λΉλ² μΈν
& κ΄λ¦¬μ μ€μκ° λκΈ°ν/μμ ν΅μ μμ€ν
|
| 138 |
# =========================================================================
|
| 139 |
-
|
|
|
|
|
|
|
| 140 |
|
| 141 |
def init_user_db(seed_users):
|
| 142 |
conn = sqlite3.connect(USER_DB_FILE)
|
|
@@ -391,9 +393,227 @@ def fetch_notion_comments(page_id):
|
|
| 391 |
comments_list.append(f"π¬ [{created_at}] {c_text}")
|
| 392 |
return "\n".join(comments_list) if comments_list else ""
|
| 393 |
except:
|
| 394 |
-
pass
|
| 395 |
-
return ""
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
def fetch_client_tasks_from_notion(client_name):
|
| 398 |
# .streamlit/secrets.toml νμΌμμ 보μ μ 보λ₯Ό μμ νκ² λ‘λν©λλ€.
|
| 399 |
notion_token = NOTION_TOKEN
|
|
@@ -1317,7 +1537,7 @@ if check_password():
|
|
| 1317 |
st.session_state['active_mode'] = "Notion"
|
| 1318 |
|
| 1319 |
# π‘ [ν μμ νμ ] λ
Έμ
νμ λ°°μ΄μ 맨 μμΌλ‘ λ°°μΉνμ¬ μΉ μ μ μ 첫 νλ©΄μ 무쑰건 λ
Έμ
μ
λ ₯μ°½μ΄ λ¨Όμ λ¨λλ‘ μ μ΄ν©λλ€.
|
| 1320 |
-
tab_notion, tab_email = st.tabs(["π Pull Notion", "βοΈ Scan Emails"])
|
| 1321 |
|
| 1322 |
# TAB 1: λ
Έμ
κ²μμ°½
|
| 1323 |
with tab_notion:
|
|
@@ -1359,7 +1579,55 @@ if check_password():
|
|
| 1359 |
</div>
|
| 1360 |
""", unsafe_allow_html=True)
|
| 1361 |
# TAB 2: μ΄λ©μΌ κ²μμ°½
|
| 1362 |
-
with
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1363 |
with st.container(border=True):
|
| 1364 |
|
| 1365 |
st.session_state['folder_path'] = st.text_input("π G: Drive Folder Path", value=st.session_state['folder_path'], key="fixed_mail_path")
|
|
|
|
| 136 |
# =========================================================================
|
| 137 |
# [보μ λνμ] μ΅μ΄ λ‘κ·ΈμΈ λΉλ² μΈν
& κ΄λ¦¬μ μ€μκ° λκΈ°ν/μμ ν΅μ μμ€ν
|
| 138 |
# =========================================================================
|
| 139 |
+
PERSISTENT_DATA_DIR = os.environ.get("ADVISERNOTE_DATA_DIR", "/data")
|
| 140 |
+
USING_PERSISTENT_STORAGE = os.path.isdir(PERSISTENT_DATA_DIR) and os.access(PERSISTENT_DATA_DIR, os.W_OK)
|
| 141 |
+
USER_DB_FILE = os.path.join(PERSISTENT_DATA_DIR, "user_passwords.db") if USING_PERSISTENT_STORAGE else "user_passwords.db"
|
| 142 |
|
| 143 |
def init_user_db(seed_users):
|
| 144 |
conn = sqlite3.connect(USER_DB_FILE)
|
|
|
|
| 393 |
comments_list.append(f"π¬ [{created_at}] {c_text}")
|
| 394 |
return "\n".join(comments_list) if comments_list else ""
|
| 395 |
except:
|
| 396 |
+
pass
|
| 397 |
+
return ""
|
| 398 |
+
|
| 399 |
+
def notion_api_url(endpoint_path):
|
| 400 |
+
return "https://api.notion.com/" + endpoint_path.lstrip("/")
|
| 401 |
+
|
| 402 |
+
def extract_notion_plain_text(prop):
|
| 403 |
+
if not isinstance(prop, dict):
|
| 404 |
+
return ""
|
| 405 |
+
|
| 406 |
+
prop_type = prop.get("type")
|
| 407 |
+
if prop_type in ("title", "rich_text"):
|
| 408 |
+
parts = prop.get(prop_type, [])
|
| 409 |
+
return "".join(item.get("plain_text", "") for item in parts if isinstance(item, dict)).strip()
|
| 410 |
+
if prop_type == "phone_number":
|
| 411 |
+
return str(prop.get("phone_number") or "").strip()
|
| 412 |
+
if prop_type == "email":
|
| 413 |
+
return str(prop.get("email") or "").strip()
|
| 414 |
+
if prop_type == "select" and prop.get("select"):
|
| 415 |
+
return str(prop["select"].get("name", "")).strip()
|
| 416 |
+
if prop_type == "multi_select":
|
| 417 |
+
return ", ".join(item.get("name", "") for item in prop.get("multi_select", []) if item.get("name")).strip()
|
| 418 |
+
if prop_type == "people":
|
| 419 |
+
return ", ".join(item.get("name", "") for item in prop.get("people", []) if item.get("name")).strip()
|
| 420 |
+
if prop_type == "number":
|
| 421 |
+
value = prop.get("number")
|
| 422 |
+
return "" if value is None else str(value).strip()
|
| 423 |
+
return ""
|
| 424 |
+
|
| 425 |
+
def find_notion_property_value(props, preferred_keys, fallback_types=None):
|
| 426 |
+
normalized_lookup = {str(key).strip().lower(): value for key, value in props.items()}
|
| 427 |
+
for key in preferred_keys:
|
| 428 |
+
key_l = key.lower()
|
| 429 |
+
for prop_name, prop in normalized_lookup.items():
|
| 430 |
+
if key_l in prop_name:
|
| 431 |
+
value = extract_notion_plain_text(prop)
|
| 432 |
+
if value:
|
| 433 |
+
return value
|
| 434 |
+
|
| 435 |
+
if fallback_types:
|
| 436 |
+
for prop in props.values():
|
| 437 |
+
if isinstance(prop, dict) and prop.get("type") in fallback_types:
|
| 438 |
+
value = extract_notion_plain_text(prop)
|
| 439 |
+
if value:
|
| 440 |
+
return value
|
| 441 |
+
return ""
|
| 442 |
+
|
| 443 |
+
def fetch_client_contacts_from_notion(search_text=""):
|
| 444 |
+
notion_token = NOTION_TOKEN
|
| 445 |
+
target_data_source_id = NOTION_DB_ID
|
| 446 |
+
|
| 447 |
+
if not notion_token:
|
| 448 |
+
return [], "Missing NOTION_API_TOKEN in secrets.toml"
|
| 449 |
+
if not target_data_source_id:
|
| 450 |
+
return [], "Missing NOTION_DATABASE_ID in secrets.toml"
|
| 451 |
+
|
| 452 |
+
headers = {
|
| 453 |
+
"Authorization": f"Bearer {notion_token.strip()}",
|
| 454 |
+
"Content-Type": "application/json",
|
| 455 |
+
"Notion-Version": "2025-09-03"
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
name_keys = ["case name", "client name", "applicant", "customer", "borrower", "name", "κ³ κ°", "μ΄λ¦"]
|
| 459 |
+
nickname_keys = ["nickname", "nick name", "preferred name", "alias", "short name", "λλ€μ", "λ³λͺ
"]
|
| 460 |
+
phone_keys = ["phone", "mobile", "telephone", "contact number", "cell", "μ ν", "μ°λ½", "ν΄λ"]
|
| 461 |
+
query_url = notion_api_url(f"v1/data_sources/{target_data_source_id.strip()}/query/")
|
| 462 |
+
contacts = []
|
| 463 |
+
start_cursor = None
|
| 464 |
+
|
| 465 |
+
try:
|
| 466 |
+
while True:
|
| 467 |
+
payload = {"page_size": 100}
|
| 468 |
+
if start_cursor:
|
| 469 |
+
payload["start_cursor"] = start_cursor
|
| 470 |
+
|
| 471 |
+
response = requests.post(query_url, json=payload, headers=headers, proxies={"http": None, "https": None}, timeout=20)
|
| 472 |
+
if response.status_code != 200:
|
| 473 |
+
return [], f"Notion API Failed: {response.status_code} - {response.text[:200]}"
|
| 474 |
+
|
| 475 |
+
data = response.json()
|
| 476 |
+
for item in data.get("results", []):
|
| 477 |
+
props = item.get("properties", {})
|
| 478 |
+
client_name = find_notion_property_value(props, name_keys, fallback_types={"title"})
|
| 479 |
+
nickname = find_notion_property_value(props, nickname_keys)
|
| 480 |
+
phone_number = find_notion_property_value(props, phone_keys, fallback_types={"phone_number"})
|
| 481 |
+
|
| 482 |
+
if not client_name and not nickname and not phone_number:
|
| 483 |
+
continue
|
| 484 |
+
|
| 485 |
+
searchable = " ".join([client_name, nickname, phone_number]).lower()
|
| 486 |
+
if search_text and search_text.strip().lower() not in searchable:
|
| 487 |
+
continue
|
| 488 |
+
|
| 489 |
+
contacts.append({
|
| 490 |
+
"Client Name": client_name,
|
| 491 |
+
"Nickname": nickname,
|
| 492 |
+
"Phone": phone_number
|
| 493 |
+
})
|
| 494 |
+
|
| 495 |
+
if not data.get("has_more"):
|
| 496 |
+
break
|
| 497 |
+
start_cursor = data.get("next_cursor")
|
| 498 |
+
|
| 499 |
+
except Exception as exc:
|
| 500 |
+
return [], f"Notion contact pull failed: {exc}"
|
| 501 |
+
|
| 502 |
+
contacts = sorted(contacts, key=lambda row: row.get("Client Name", "").lower())
|
| 503 |
+
return contacts, "Success"
|
| 504 |
+
|
| 505 |
+
NOTION_CONTACT_FIELD_CONFIG = {
|
| 506 |
+
"Client Name": {
|
| 507 |
+
"keys": ["case name", "client name", "applicant", "customer", "borrower", "name", "κ³ κ°", "μ΄λ¦"],
|
| 508 |
+
"fallback_types": {"title"},
|
| 509 |
+
},
|
| 510 |
+
"Nickname": {
|
| 511 |
+
"keys": ["nickname", "nick name", "preferred name", "alias", "short name", "λλ€μ", "λ³λͺ
"],
|
| 512 |
+
"fallback_types": set(),
|
| 513 |
+
},
|
| 514 |
+
"Phone": {
|
| 515 |
+
"keys": ["phone", "mobile", "telephone", "contact number", "cell", "μ ν", "μ°λ½", "ν΄λ"],
|
| 516 |
+
"fallback_types": {"phone_number"},
|
| 517 |
+
},
|
| 518 |
+
"Email": {
|
| 519 |
+
"keys": ["email", "e-mail", "mail", "μ΄λ©μΌ"],
|
| 520 |
+
"fallback_types": {"email"},
|
| 521 |
+
},
|
| 522 |
+
"Status": {
|
| 523 |
+
"keys": ["status", "stage", "progress", "μν", "μ§ν"],
|
| 524 |
+
"fallback_types": set(),
|
| 525 |
+
},
|
| 526 |
+
"Adviser": {
|
| 527 |
+
"keys": ["adviser", "advisor", "broker", "owner", "λ΄λΉ", "μ΄λλ°μ΄μ "],
|
| 528 |
+
"fallback_types": set(),
|
| 529 |
+
},
|
| 530 |
+
"Case Number": {
|
| 531 |
+
"keys": ["case number", "case no", "ref", "reference", "application number", "λ²νΈ"],
|
| 532 |
+
"fallback_types": set(),
|
| 533 |
+
},
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
def find_notion_property_by_exact_or_contains(props, property_name):
|
| 537 |
+
target = str(property_name or "").strip().lower()
|
| 538 |
+
if not target:
|
| 539 |
+
return ""
|
| 540 |
+
|
| 541 |
+
for key, prop in props.items():
|
| 542 |
+
if str(key).strip().lower() == target:
|
| 543 |
+
return extract_notion_plain_text(prop)
|
| 544 |
+
|
| 545 |
+
for key, prop in props.items():
|
| 546 |
+
if target in str(key).strip().lower():
|
| 547 |
+
return extract_notion_plain_text(prop)
|
| 548 |
+
return ""
|
| 549 |
+
|
| 550 |
+
def fetch_client_contacts_from_notion(search_text="", selected_fields=None, custom_fields=None):
|
| 551 |
+
notion_token = NOTION_TOKEN
|
| 552 |
+
target_data_source_id = NOTION_DB_ID
|
| 553 |
+
|
| 554 |
+
if not notion_token:
|
| 555 |
+
return [], "Missing NOTION_API_TOKEN in secrets.toml"
|
| 556 |
+
if not target_data_source_id:
|
| 557 |
+
return [], "Missing NOTION_DATABASE_ID in secrets.toml"
|
| 558 |
+
|
| 559 |
+
headers = {
|
| 560 |
+
"Authorization": f"Bearer {notion_token.strip()}",
|
| 561 |
+
"Content-Type": "application/json",
|
| 562 |
+
"Notion-Version": "2025-09-03"
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
selected_fields = selected_fields or ["Client Name", "Nickname", "Phone"]
|
| 566 |
+
custom_fields = [field.strip() for field in (custom_fields or []) if field.strip()]
|
| 567 |
+
output_fields = list(dict.fromkeys(selected_fields + custom_fields))
|
| 568 |
+
query_url = notion_api_url(f"v1/data_sources/{target_data_source_id.strip()}/query/")
|
| 569 |
+
contacts = []
|
| 570 |
+
start_cursor = None
|
| 571 |
+
|
| 572 |
+
try:
|
| 573 |
+
while True:
|
| 574 |
+
payload = {"page_size": 100}
|
| 575 |
+
if start_cursor:
|
| 576 |
+
payload["start_cursor"] = start_cursor
|
| 577 |
+
|
| 578 |
+
response = requests.post(query_url, json=payload, headers=headers, proxies={"http": None, "https": None}, timeout=20)
|
| 579 |
+
if response.status_code != 200:
|
| 580 |
+
return [], f"Notion API Failed: {response.status_code} - {response.text[:200]}"
|
| 581 |
+
|
| 582 |
+
data = response.json()
|
| 583 |
+
for item in data.get("results", []):
|
| 584 |
+
props = item.get("properties", {})
|
| 585 |
+
row = {}
|
| 586 |
+
for field_name in output_fields:
|
| 587 |
+
config = NOTION_CONTACT_FIELD_CONFIG.get(field_name)
|
| 588 |
+
if config:
|
| 589 |
+
row[field_name] = find_notion_property_value(
|
| 590 |
+
props,
|
| 591 |
+
config["keys"],
|
| 592 |
+
fallback_types=config["fallback_types"],
|
| 593 |
+
)
|
| 594 |
+
else:
|
| 595 |
+
row[field_name] = find_notion_property_by_exact_or_contains(props, field_name)
|
| 596 |
+
|
| 597 |
+
if not any(str(value).strip() for value in row.values()):
|
| 598 |
+
continue
|
| 599 |
+
|
| 600 |
+
searchable = " ".join(str(value) for value in row.values()).lower()
|
| 601 |
+
if search_text and search_text.strip().lower() not in searchable:
|
| 602 |
+
continue
|
| 603 |
+
|
| 604 |
+
contacts.append(row)
|
| 605 |
+
|
| 606 |
+
if not data.get("has_more"):
|
| 607 |
+
break
|
| 608 |
+
start_cursor = data.get("next_cursor")
|
| 609 |
+
|
| 610 |
+
except Exception as exc:
|
| 611 |
+
return [], f"Notion contact pull failed: {exc}"
|
| 612 |
+
|
| 613 |
+
sort_field = "Client Name" if "Client Name" in output_fields else output_fields[0]
|
| 614 |
+
contacts = sorted(contacts, key=lambda row: str(row.get(sort_field, "")).lower())
|
| 615 |
+
return contacts, "Success"
|
| 616 |
+
|
| 617 |
def fetch_client_tasks_from_notion(client_name):
|
| 618 |
# .streamlit/secrets.toml νμΌμμ 보μ μ 보λ₯Ό μμ νκ² λ‘λν©λλ€.
|
| 619 |
notion_token = NOTION_TOKEN
|
|
|
|
| 1537 |
st.session_state['active_mode'] = "Notion"
|
| 1538 |
|
| 1539 |
# π‘ [ν μμ νμ ] λ
Έμ
νμ λ°°μ΄μ 맨 μμΌλ‘ λ°°μΉνμ¬ μΉ μ μ μ 첫 νλ©΄μ 무쑰건 λ
Έμ
μ
λ ₯μ°½μ΄ λ¨Όμ λ¨λλ‘ μ μ΄ν©λλ€.
|
| 1540 |
+
tab_notion, tab_contacts, tab_email = st.tabs(["π Pull Notion", "π₯ Notion Contacts", "βοΈ Scan Emails"])
|
| 1541 |
|
| 1542 |
# TAB 1: λ
Έμ
κ²μμ°½
|
| 1543 |
with tab_notion:
|
|
|
|
| 1579 |
</div>
|
| 1580 |
""", unsafe_allow_html=True)
|
| 1581 |
# TAB 2: μ΄λ©μΌ κ²μμ°½
|
| 1582 |
+
with tab_contacts:
|
| 1583 |
+
with st.container(border=True):
|
| 1584 |
+
st.caption("Pull customer name, nickname, and phone number from Notion.")
|
| 1585 |
+
contact_col1, contact_col2 = st.columns([5, 1])
|
| 1586 |
+
contact_search = contact_col1.text_input(
|
| 1587 |
+
"Search customer / nickname / phone",
|
| 1588 |
+
key="notion_contact_search",
|
| 1589 |
+
placeholder="Leave blank to pull all contacts"
|
| 1590 |
+
)
|
| 1591 |
+
contact_pull_btn = contact_col2.button("Pull Contacts", use_container_width=True, key="notion_contact_pull")
|
| 1592 |
+
|
| 1593 |
+
selected_contact_fields = st.multiselect(
|
| 1594 |
+
"Fields to pull",
|
| 1595 |
+
options=list(NOTION_CONTACT_FIELD_CONFIG.keys()),
|
| 1596 |
+
default=["Client Name", "Nickname", "Phone"],
|
| 1597 |
+
key="notion_contact_fields"
|
| 1598 |
+
)
|
| 1599 |
+
custom_contact_fields_raw = st.text_input(
|
| 1600 |
+
"Additional Notion property names",
|
| 1601 |
+
key="notion_contact_custom_fields",
|
| 1602 |
+
placeholder="Example: DOB; Address; Loan Amount"
|
| 1603 |
+
)
|
| 1604 |
+
custom_contact_fields = [
|
| 1605 |
+
field.strip()
|
| 1606 |
+
for field in custom_contact_fields_raw.replace(",", ";").split(";")
|
| 1607 |
+
if field.strip()
|
| 1608 |
+
]
|
| 1609 |
+
|
| 1610 |
+
if contact_pull_btn:
|
| 1611 |
+
with st.status("Pulling Notion contacts...", expanded=True) as status:
|
| 1612 |
+
contact_rows, contact_msg = fetch_client_contacts_from_notion(
|
| 1613 |
+
contact_search,
|
| 1614 |
+
selected_fields=selected_contact_fields,
|
| 1615 |
+
custom_fields=custom_contact_fields,
|
| 1616 |
+
)
|
| 1617 |
+
if contact_rows:
|
| 1618 |
+
st.session_state["notion_contacts"] = pd.DataFrame(contact_rows)
|
| 1619 |
+
status.update(label=f"Loaded {len(contact_rows)} contacts.", state="complete")
|
| 1620 |
+
elif contact_msg == "Success":
|
| 1621 |
+
st.session_state["notion_contacts"] = pd.DataFrame(columns=selected_contact_fields + custom_contact_fields)
|
| 1622 |
+
status.update(label="No contacts found.", state="complete")
|
| 1623 |
+
else:
|
| 1624 |
+
status.update(label=f"Failed: {contact_msg}", state="error")
|
| 1625 |
+
|
| 1626 |
+
contacts_df = st.session_state.get("notion_contacts")
|
| 1627 |
+
if isinstance(contacts_df, pd.DataFrame):
|
| 1628 |
+
st.dataframe(contacts_df, use_container_width=True, hide_index=True)
|
| 1629 |
+
|
| 1630 |
+
with tab_email:
|
| 1631 |
with st.container(border=True):
|
| 1632 |
|
| 1633 |
st.session_state['folder_path'] = st.text_input("π G: Drive Folder Path", value=st.session_state['folder_path'], key="fixed_mail_path")
|