hassan773 commited on
Commit
ec8d13e
Β·
verified Β·
1 Parent(s): 8239224

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -131
app.py CHANGED
@@ -1,12 +1,18 @@
1
  import streamlit as st
2
  import pandas as pd
3
- import os
4
  import numpy as np
5
- import google.generativeai as genai
6
- from datetime import datetime
7
  import hashlib
 
 
8
  from PIL import Image
 
9
  import plotly.graph_objects as go
 
 
 
 
 
10
 
11
  # --- 1. CORE SYSTEM CONFIG ---
12
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
@@ -21,35 +27,40 @@ vision_model = genai.GenerativeModel('gemini-2.0-flash')
21
 
22
  st.set_page_config(page_title="IntelliCare Portal | Hassan Naseer", layout="wide", page_icon="πŸ₯")
23
 
24
- # --- 2. THEME ENGINE (HIGH CONTRAST) ---
25
  if "theme" not in st.session_state: st.session_state.theme = "Light"
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def inject_theme():
28
  bg = "#000000" if st.session_state.theme == "Dark" else "#FFFFFF"
29
  txt = "#FFFFFF" if st.session_state.theme == "Dark" else "#000000"
30
  card = "#111111" if st.session_state.theme == "Dark" else "#F0F2F6"
31
  brd = "#FFFFFF" if st.session_state.theme == "Dark" else "#000000"
32
-
33
- st.markdown(f"""
34
- <style>
35
  .stApp {{ background-color: {bg} !important; color: {txt} !important; }}
36
- div[data-baseweb="input"], div[data-baseweb="textarea"], select {{
37
- border: 2px solid {brd} !important; border-radius: 10px !important;
38
- }}
39
  .chat-bubble {{ padding: 15px; border-radius: 15px; margin-bottom: 10px; border: 1px solid {brd}; }}
40
  .user-msg {{ background-color: rgba(59, 130, 246, 0.1); border-left: 5px solid #3b82f6; }}
41
  .ai-msg {{ background-color: rgba(16, 185, 129, 0.1); border-left: 5px solid #10b981; }}
42
  .clinical-card {{ background-color: {card}; border: 2px solid {brd}; padding: 25px; border-radius: 15px; color: {txt}; }}
43
- [data-testid="stSidebar"] * {{ color: {txt} !important; }}
44
- </style>
45
- """, unsafe_allow_html=True)
46
 
47
  inject_theme()
48
 
49
  # --- 3. DATA PERSISTENCE ---
50
- USER_DB = "users_secure.csv"
51
- HISTORY_DB = "clinical_history.csv"
52
-
53
  def hash_pass(pwd): return hashlib.sha256(str.encode(pwd)).hexdigest()
54
  def load_db(file, cols):
55
  if os.path.exists(file): return pd.read_csv(file)
@@ -64,18 +75,17 @@ def get_vector_db():
64
  signal = client.get_or_create_collection(name="signals_targeted", embedding_function=emb_fn)
65
  return main, signal
66
 
67
- # --- 4. AUTHENTICATION GATE ---
68
  if "logged_in" not in st.session_state: st.session_state.logged_in = False
69
  if "last_processed_audio" not in st.session_state: st.session_state.last_processed_audio = None
70
 
71
  if not st.session_state.logged_in:
72
  st.markdown("<h1 style='text-align: center;'>πŸ₯ IntelliCare Portal</h1>", unsafe_allow_html=True)
73
- c1, c2, c3 = st.columns([1, 2, 1])
74
  with c2:
75
- tab1, tab2 = st.tabs(["πŸ” Login", "πŸ“ Create Account"])
76
- with tab1:
77
- u = st.text_input("Username", key="l_u")
78
- p = st.text_input("Password", type="password", key="l_p")
79
  if st.button("Sign In"):
80
  users = load_db(USER_DB, ["username", "password", "role"])
81
  match = users[(users['username'] == u) & (users['password'] == hash_pass(p))]
@@ -83,121 +93,106 @@ if not st.session_state.logged_in:
83
  st.session_state.logged_in, st.session_state.username = True, u
84
  st.session_state.role, st.session_state.msgs = match.iloc[0]['role'], []
85
  st.rerun()
86
- else: st.error("Invalid Login")
87
- with tab2:
88
- nu = st.text_input("New Username", key="r_u")
89
- np = st.text_input("New Password", type="password", key="r_p")
90
- nr = st.selectbox("Role", ["Patient", "Doctor"], key="r_r")
91
  if st.button("Register"):
92
  df = load_db(USER_DB, ["username", "password", "role"])
93
- if nu and np:
94
- if nu not in df['username'].values:
95
- pd.concat([df, pd.DataFrame([{"username": nu, "password": hash_pass(np), "role": nr}])]).to_csv(USER_DB, index=False)
96
- st.success("βœ… Account Created! Now go to Login.")
97
- else: st.error("User exists.")
98
  st.stop()
99
 
100
  # --- 5. NAVIGATION ---
101
  with st.sidebar:
102
- st.markdown(f"### πŸ‘€ {st.session_state.username} ({st.session_state.role})")
103
  if st.button("Logout"): st.session_state.logged_in = False; st.rerun()
104
  st.divider()
105
- if st.session_state.role == "Patient":
106
- nav = st.radio("Menu", ["πŸ’¬ AI Chat", "πŸ§ͺ Health Lab", "πŸ“ Nearby Hospitals", "πŸ“Έ Vision Scanner", "πŸ“ž Video Call", "πŸ“œ My Records"])
107
- else:
108
- nav = st.radio("Menu", ["πŸ–₯️ Consultation Desk", "πŸ“‹ Patient Records Archive"])
109
-
110
- # --- 6. PATIENT PORTAL ---
111
- if st.session_state.role == "Patient":
112
- if nav == "πŸ’¬ AI Chat":
113
- st.markdown('<div class="clinical-card"><h3>πŸ’¬ Clinical AI Assistant</h3></div>', unsafe_allow_html=True)
114
- for m in st.session_state.msgs:
115
- bubble = "user-msg" if m["role"] == "user" else "ai-msg"
116
- st.markdown(f'<div class="chat-bubble {bubble}"><b>{m["role"].upper()}:</b><br>{m["content"]}</div>', unsafe_allow_html=True)
117
-
118
- q = st.chat_input("Explain symptoms...")
119
- c1, c2, c3 = st.columns(3)
120
- with c1: v = st.audio_input("🎀 Voice", key=f"v_{len(st.session_state.msgs)}")
121
- with c2: up_pdf = st.file_uploader("πŸ“„ PDF Report", type=['pdf'], key=f"p_{len(st.session_state.msgs)}")
122
- with c3: up_img = st.file_uploader("πŸ“Έ Image Scan", type=['png', 'jpg'], key=f"i_{len(st.session_state.msgs)}")
123
-
124
- final_q = q if q else None
125
- if up_pdf:
126
- import pdfplumber
127
- with pdfplumber.open(up_pdf) as f: final_q = "PDF: " + " ".join([p.extract_text() for p in f.pages if p.extract_text()])
128
- elif up_img:
129
- res = vision_model.generate_content(["Extract clinical text:", Image.open(up_img)])
130
- final_q = "Image Scan: " + res.text
131
- elif v:
132
- v_hash = hashlib.md5(v.getvalue()).hexdigest()
133
- if v_hash != st.session_state.last_processed_audio:
134
- from groq import Groq
135
- trans = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create(file=("a.wav", v.getvalue()), model="whisper-large-v3", response_format="text")
136
- final_q = trans
137
- st.session_state.last_processed_audio = v_hash
138
-
139
- if final_q:
140
- st.session_state.msgs.append({"role": "user", "content": final_q})
141
- from groq import Groq
142
- main_col, _ = get_vector_db()
143
- res = main_col.query(query_texts=[final_q], n_results=1)
144
- ctx = res['documents'][0][0] if (res.get('documents') and len(res['documents'][0]) > 0) else "N/A"
145
- ans = Groq(api_key=GROQ_API_KEY).chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": f"Context: {ctx}"}] + st.session_state.msgs)
146
- st.session_state.msgs.append({"role": "assistant", "content": ans.choices[0].message.content})
147
- st.rerun()
148
-
149
- elif nav == "πŸ§ͺ Health Lab":
150
- st.markdown('<div class="clinical-card"><h3>πŸ§ͺ Advanced Analytics</h3></div>', unsafe_allow_html=True)
151
- hr = st.slider("Heart Rate (BPM)", 40, 180, 72)
152
- # ECG Waveform simulation
153
- t = np.linspace(0, 2, 100)
154
- y = np.sin(2 * np.pi * (hr/60) * t) + 0.5 * np.sin(4 * np.pi * (hr/60) * t)
155
- fig = go.Figure(data=go.Scatter(y=y, mode='lines', line=dict(color='#ff4b4b', width=3)))
156
- fig.update_layout(height=250, margin=dict(l=0,r=0,t=0,b=0), paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)')
157
- st.plotly_chart(fig, use_container_width=True)
158
- st.metric("Pulse Status", f"{hr} BPM", delta="Normal" if 60<=hr<=100 else "Alert")
159
-
160
-
161
- elif nav == "πŸ“ Nearby Hospitals":
162
- st.markdown('<div class="clinical-card"><h3>πŸ“ Rawalpindi Hospital Intel</h3></div>', unsafe_allow_html=True)
163
- h_data = [{"Hospital": "Maryam Memorial", "Rating": "4.5", "Area": "Peshawar Rd"}, {"Hospital": "Medway Medical", "Rating": "4.6", "Area": "Satellite Town"}]
164
- st.table(pd.DataFrame(h_data))
165
-
166
- elif nav == "πŸ“ž Video Call":
167
- st.markdown('<div class="clinical-card"><h3>πŸ“ž Request a Specialist</h3></div>', unsafe_allow_html=True)
168
- users = load_db(USER_DB, ["username", "password", "role"])
169
- docs = users[users['role'] == 'Doctor']['username'].tolist()
170
- sel = st.selectbox("Search/Select Doctor", docs)
171
- if st.button("Initiate Connection"):
172
- room = f"IntelliCare-{st.session_state.username}-{sel}"
173
- _, signal_col = get_vector_db()
174
- signal_col.upsert(documents=[f"CALL|{sel}|{room}|{st.session_state.username}"], ids=["latest"])
175
- st.session_state.active_room = room
176
- if "active_room" in st.session_state:
177
- st.components.v1.html(f'<iframe src="https://meet.jit.si/{st.session_state.active_room}" width="100%" height="550px"></iframe>', height=600)
178
-
179
- elif nav == "πŸ“œ My Records":
180
- st.table(load_db(HISTORY_DB, ["Time", "Patient", "Doctor", "Status"]))
181
-
182
- # --- 7. DOCTOR PORTAL ---
183
- elif st.session_state.role == "Doctor":
184
- if nav == "πŸ–₯️ Consultation Desk":
185
- st.markdown('<div class="clinical-card"><h3>πŸ–₯️ Consultation Desk</h3></div>', unsafe_allow_html=True)
186
  _, signal_col = get_vector_db()
187
- res = signal_col.get(ids=["latest"])
188
- if res.get('documents'):
189
- parts = res['documents'][0].split("|")
190
- if parts[1] == st.session_state.username:
191
- st.warning(f"πŸ”” {parts[3]} is requesting a call.")
192
- if st.button("βœ… Join Call"): st.session_state.active_call = parts[2]
193
- if "active_call" in st.session_state:
194
- st.components.v1.html(f'<iframe src="https://meet.jit.si/{parts[2]}" width="100%" height="600px"></iframe>', height=650)
195
- if st.button("πŸ”΄ Archive & Log"):
196
- df = load_db(HISTORY_DB, ["Time", "Patient", "Doctor", "Status"])
197
- log = pd.DataFrame([{"Time": datetime.now().strftime("%Y-%m-%d %H:%M"), "Patient": parts[3], "Doctor": st.session_state.username, "Status": "Completed"}])
198
- pd.concat([df, log]).to_csv(HISTORY_DB, index=False)
199
- signal_col.delete(ids=["latest"]); del st.session_state.active_call; st.rerun()
200
- else: st.info("Waiting for patient requests...")
201
-
202
- elif nav == "πŸ“‹ Patient Records Archive":
203
- st.table(load_db(HISTORY_DB, ["Time", "Patient", "Doctor", "Status"]))
 
 
 
1
  import streamlit as st
2
  import pandas as pd
 
3
  import numpy as np
4
+ import os
 
5
  import hashlib
6
+ import requests
7
+ from datetime import datetime
8
  from PIL import Image
9
+ from fpdf import FPDF
10
  import plotly.graph_objects as go
11
+ import google.generativeai as genai
12
+ import folium
13
+ from streamlit_folium import st_folium
14
+ from streamlit_geolocation import streamlit_geolocation
15
+ from groq import Groq
16
 
17
  # --- 1. CORE SYSTEM CONFIG ---
18
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
 
27
 
28
  st.set_page_config(page_title="IntelliCare Portal | Hassan Naseer", layout="wide", page_icon="πŸ₯")
29
 
30
+ # --- 2. THEME & PDF GENERATOR ---
31
  if "theme" not in st.session_state: st.session_state.theme = "Light"
32
 
33
+ def generate_medical_pdf(chat_history, username):
34
+ pdf = FPDF()
35
+ pdf.add_page()
36
+ pdf.set_font("Arial", 'B', 16)
37
+ pdf.cell(200, 10, txt="IntelliCare Portal - Clinical Summary", ln=True, align='C')
38
+ pdf.set_font("Arial", size=10)
39
+ pdf.cell(200, 10, txt=f"Patient: {username} | Date: {datetime.now().strftime('%Y-%m-%d %H:%M')}", ln=True, align='C')
40
+ pdf.ln(10)
41
+ for msg in chat_history:
42
+ role = "PATIENT" if msg["role"] == "user" else "CLINICAL AI"
43
+ pdf.set_font("Arial", 'B', 10); pdf.cell(0, 10, txt=f"{role}:", ln=True)
44
+ pdf.set_font("Arial", size=10); pdf.multi_cell(0, 8, txt=msg["content"]); pdf.ln(4)
45
+ return pdf.output(dest='S').encode('latin-1')
46
+
47
  def inject_theme():
48
  bg = "#000000" if st.session_state.theme == "Dark" else "#FFFFFF"
49
  txt = "#FFFFFF" if st.session_state.theme == "Dark" else "#000000"
50
  card = "#111111" if st.session_state.theme == "Dark" else "#F0F2F6"
51
  brd = "#FFFFFF" if st.session_state.theme == "Dark" else "#000000"
52
+ st.markdown(f"""<style>
 
 
53
  .stApp {{ background-color: {bg} !important; color: {txt} !important; }}
 
 
 
54
  .chat-bubble {{ padding: 15px; border-radius: 15px; margin-bottom: 10px; border: 1px solid {brd}; }}
55
  .user-msg {{ background-color: rgba(59, 130, 246, 0.1); border-left: 5px solid #3b82f6; }}
56
  .ai-msg {{ background-color: rgba(16, 185, 129, 0.1); border-left: 5px solid #10b981; }}
57
  .clinical-card {{ background-color: {card}; border: 2px solid {brd}; padding: 25px; border-radius: 15px; color: {txt}; }}
58
+ </style>""", unsafe_allow_html=True)
 
 
59
 
60
  inject_theme()
61
 
62
  # --- 3. DATA PERSISTENCE ---
63
+ USER_DB, HISTORY_DB = "users_secure.csv", "clinical_history.csv"
 
 
64
  def hash_pass(pwd): return hashlib.sha256(str.encode(pwd)).hexdigest()
65
  def load_db(file, cols):
66
  if os.path.exists(file): return pd.read_csv(file)
 
75
  signal = client.get_or_create_collection(name="signals_targeted", embedding_function=emb_fn)
76
  return main, signal
77
 
78
+ # --- 4. AUTHENTICATION ---
79
  if "logged_in" not in st.session_state: st.session_state.logged_in = False
80
  if "last_processed_audio" not in st.session_state: st.session_state.last_processed_audio = None
81
 
82
  if not st.session_state.logged_in:
83
  st.markdown("<h1 style='text-align: center;'>πŸ₯ IntelliCare Portal</h1>", unsafe_allow_html=True)
84
+ c2 = st.columns([1, 2, 1])[1]
85
  with c2:
86
+ t1, t2 = st.tabs(["πŸ” Login", "πŸ“ Create Account"])
87
+ with t1:
88
+ u, p = st.text_input("Username", key="l_u"), st.text_input("Password", type="password", key="l_p")
 
89
  if st.button("Sign In"):
90
  users = load_db(USER_DB, ["username", "password", "role"])
91
  match = users[(users['username'] == u) & (users['password'] == hash_pass(p))]
 
93
  st.session_state.logged_in, st.session_state.username = True, u
94
  st.session_state.role, st.session_state.msgs = match.iloc[0]['role'], []
95
  st.rerun()
96
+ with t2:
97
+ nu, np, nr = st.text_input("New User"), st.text_input("New Pass", type="password"), st.selectbox("Role", ["Patient", "Doctor"])
 
 
 
98
  if st.button("Register"):
99
  df = load_db(USER_DB, ["username", "password", "role"])
100
+ if nu not in df['username'].values:
101
+ pd.concat([df, pd.DataFrame([{"username": nu, "password": hash_pass(np), "role": nr}])]).to_csv(USER_DB, index=False)
102
+ st.success("βœ… Registered!")
 
 
103
  st.stop()
104
 
105
  # --- 5. NAVIGATION ---
106
  with st.sidebar:
107
+ st.markdown(f"### πŸ‘€ {st.session_state.username}")
108
  if st.button("Logout"): st.session_state.logged_in = False; st.rerun()
109
  st.divider()
110
+ nav = st.radio("Menu", ["πŸ’¬ AI Chat", "πŸ§ͺ Health Lab", "πŸ“ Nearby Hospitals", "πŸ“ž Video Call", "πŸ“œ History"]) if st.session_state.role == "Patient" else st.radio("Menu", ["πŸ–₯️ Consultation Desk", "πŸ“‹ Patient Records"])
111
+
112
+ # --- 6. UNIFIED AI CHAT (GEMINI STYLE) ---
113
+ if st.session_state.role == "Patient" and nav == "πŸ’¬ AI Chat":
114
+ st.markdown('<div class="clinical-card"><h3>πŸ’¬ Clinical Assistant</h3></div>', unsafe_allow_html=True)
115
+ for m in st.session_state.msgs:
116
+ bubble = "user-msg" if m["role"] == "user" else "ai-msg"
117
+ st.markdown(f'<div class="chat-bubble {bubble}">{m["content"]}</div>', unsafe_allow_html=True)
118
+
119
+ # UNIFIED INPUT BAR
120
+ st.divider()
121
+ input_col1, input_col2, input_col3 = st.columns([0.6, 0.6, 8])
122
+ with input_col1:
123
+ v = st.audio_input("🎀", label_visibility="collapsed", key=f"v_{len(st.session_state.msgs)}")
124
+ with input_col2:
125
+ with st.popover("βž•"):
126
+ up_pdf = st.file_uploader("Upload PDF", type=['pdf'])
127
+ up_img = st.file_uploader("Scan Medicine", type=['png', 'jpg'])
128
+ with input_col3:
129
+ q = st.chat_input("Ask a medical question...")
130
+
131
+ final_q = q if q else None
132
+ if up_pdf:
133
+ import pdfplumber
134
+ with pdfplumber.open(up_pdf) as f: final_q = "Analyze PDF: " + " ".join([p.extract_text() for p in f.pages if p.extract_text()])
135
+ elif up_img:
136
+ res = vision_model.generate_content(["Extract medical text:", Image.open(up_img)])
137
+ final_q = "Image Scan: " + res.text
138
+ elif v:
139
+ v_hash = hashlib.md5(v.getvalue()).hexdigest()
140
+ if v_hash != st.session_state.last_processed_audio:
141
+ final_q = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create(file=("a.wav", v.getvalue()), model="whisper-large-v3", response_format="text")
142
+ st.session_state.last_processed_audio = v_hash
143
+
144
+ if final_q:
145
+ st.session_state.msgs.append({"role": "user", "content": final_q})
146
+ sys_p = "You are a Clinical AI. Only answer medical questions. Politely decline others. Use provided context."
147
+ main_col, _ = get_vector_db()
148
+ res = main_col.query(query_texts=[final_q], n_results=1)
149
+ ctx = res['documents'][0][0] if (res.get('documents') and len(res['documents'][0]) > 0) else "N/A"
150
+ ans = Groq(api_key=GROQ_API_KEY).chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": sys_p}, {"role": "system", "content": f"Context: {ctx}"}] + st.session_state.msgs)
151
+ st.session_state.msgs.append({"role": "assistant", "content": ans.choices[0].message.content})
152
+ st.rerun()
153
+
154
+ if st.session_state.msgs:
155
+ st.download_button("πŸ“₯ Download PDF Summary", generate_medical_pdf(st.session_state.msgs, st.session_state.username), "Summary.pdf", "application/pdf")
156
+
157
+ # --- 7. OTHER SECTIONS (LAB & MAP) ---
158
+ elif nav == "πŸ§ͺ Health Lab":
159
+ st.markdown('<div class="clinical-card"><h3>πŸ§ͺ Clinical Diagnostics</h3></div>', unsafe_allow_html=True)
160
+ w, h = st.number_input("Weight (kg)", 30, 200, 70), st.number_input("Height (cm)", 100, 250, 175)
161
+ bmi = round(w / ((h/100)**2), 1)
162
+ st.plotly_chart(go.Figure(go.Indicator(mode="gauge+number", value=bmi, domain={'x': [0, 1], 'y': [0, 1]}, gauge={'axis': {'range': [10, 40]}, 'bar': {'color': "#10b981"}, 'steps': [{'range': [10, 18.5], 'color': "lightblue"}, {'range': [25, 40], 'color': "orange"}]})), use_container_width=True)
163
+
164
+ elif nav == "πŸ“ Nearby Hospitals":
165
+ loc = streamlit_geolocation()
166
+ if loc and loc.get("latitude"):
167
+ lat, lon = loc["latitude"], loc["longitude"]
168
+ query = f'[out:json];node["amenity"~"hospital|clinic"](around:5000,{lat},{lon});out body;'
169
+ data = requests.get("http://overpass-api.de/api/interpreter", params={'data': query}).json()
170
+ m = folium.Map(location=[lat, lon], zoom_start=14)
171
+ for e in data.get('elements', []): folium.Marker([e['lat'], e['lon']], popup=e.get('tags', {}).get('name', 'Clinic'), icon=folium.Icon(color="red")).add_to(m)
172
+ st_folium(m, width=1200, height=500)
173
+
174
+ elif nav == "πŸ“ž Video Call":
175
+ docs = load_db(USER_DB, ["username", "role"])
176
+ sel = st.selectbox("Search Doctor", docs[docs['role'] == 'Doctor']['username'].tolist())
177
+ if st.button("Request Connection"):
178
+ room = f"IntelliCare-{st.session_state.username}-{sel}"
 
 
 
 
 
 
 
 
 
 
 
 
179
  _, signal_col = get_vector_db()
180
+ signal_col.upsert(documents=[f"CALL|{sel}|{room}|{st.session_state.username}"], ids=["latest"])
181
+ st.session_state.active_room = room
182
+ if "active_room" in st.session_state:
183
+ st.components.v1.html(f'<iframe src="https://meet.jit.si/{st.session_state.active_room}" width="100%" height="550px"></iframe>', height=600)
184
+
185
+ elif nav == "πŸ–₯️ Consultation Desk":
186
+ _, signal_col = get_vector_db()
187
+ res = signal_col.get(ids=["latest"])
188
+ if res.get('documents'):
189
+ parts = res['documents'][0].split("|")
190
+ if parts[1] == st.session_state.username:
191
+ st.warning(f"πŸ”” {parts[3]} is requesting a call.")
192
+ if st.button("βœ… Join"): st.session_state.active_call = parts[2]
193
+ if "active_call" in st.session_state:
194
+ st.components.v1.html(f'<iframe src="https://meet.jit.si/{parts[2]}" width="100%" height="600px"></iframe>', height=650)
195
+ if st.button("πŸ”΄ Archive"):
196
+ df = load_db(HISTORY_DB, ["Time", "Patient", "Doctor", "Status"])
197
+ pd.concat([df, pd.DataFrame([{"Time": datetime.now().strftime("%Y-%m-%d %H:%M"), "Patient": parts[3], "Doctor": st.session_state.username, "Status": "Completed"}])]).to_csv(HISTORY_DB, index=False)
198
+ signal_col.delete(ids=["latest"]); del st.session_state.active_call; st.rerun()