hassan773 commited on
Commit
66f1f29
Β·
verified Β·
1 Parent(s): 9074f33

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +193 -141
app.py CHANGED
@@ -1,160 +1,212 @@
1
  import streamlit as st
2
  import pandas as pd
 
3
  import os
4
  import hashlib
 
 
 
 
 
 
 
 
5
  from groq import Groq
6
  import pdfplumber
7
 
8
- # --- 1. CONFIGURATION ---
9
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
10
- if not GROQ_API_KEY:
11
- st.error("⚠️ API Key Missing! Please add it to your environment variables.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  st.stop()
13
 
14
- # --- 2. THE DESIGN ENGINE (Fixed Frame & Static UI) ---
15
- def inject_assistant_design():
16
- st.markdown("""
17
- <style>
18
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap');
19
- html, body, [class*="css"] { font-family: 'Poppins', sans-serif; }
20
-
21
- /* 1. LOCK THE MAIN FRAME */
22
- .main { overflow: hidden; height: 100vh; }
23
-
24
- /* 2. GRADIENT SIDEBAR (STATIC) */
25
- [data-testid="stSidebar"] {
26
- background: linear-gradient(180deg, #0f172a 0%, #1e3a8a 100%);
27
- border-right: 1px solid rgba(255,255,255,0.1);
28
- }
29
- [data-testid="stSidebar"] * { color: #f8fafc !important; }
30
-
31
- /* 3. SCROLLABLE CHAT DASHBOARD */
32
- .chat-scroll-area {
33
- height: 62vh;
34
- overflow-y: auto;
35
- padding: 25px;
36
- background: #f8fafc;
37
- border-radius: 20px;
38
- border: 1px solid #e2e8f0;
39
- margin-bottom: 20px;
40
- display: flex;
41
- flex-direction: column;
42
- }
43
-
44
- /* 4. PREMIUM BUBBLES */
45
- .user-msg {
46
- background: #3b82f6; color: white; padding: 15px 20px;
47
- border-radius: 20px 20px 0 20px; align-self: flex-end;
48
- margin-bottom: 15px; max-width: 75%; box-shadow: 0 4px 10px rgba(59,130,246,0.1);
49
- }
50
- .ai-msg {
51
- background: #ffffff; color: #1e293b; padding: 15px 20px;
52
- border-radius: 20px 20px 20px 0; align-self: flex-start;
53
- margin-bottom: 15px; max-width: 75%; border: 1px solid #e2e8f0;
54
- position: relative;
55
- }
56
-
57
- /* 5. FIXED BOTTOM PROMPT BOX (THE GEMINI BAR) */
58
- div[data-testid="stChatInput"] {
59
- position: fixed !important;
60
- bottom: 35px !important;
61
- width: 65% !important;
62
- left: 20% !important;
63
- z-index: 1000 !important;
64
- border-radius: 30px !important;
65
- border: 2px solid #3b82f6 !important;
66
- background: white !important;
67
- box-shadow: 0 4px 20px rgba(0,0,0,0.05) !important;
68
- }
69
-
70
- /* 6. SPEAKER ICON */
71
- .voice-btn {
72
- background: none; border: none; cursor: pointer;
73
- font-size: 1.2rem; float: right; margin-left: 10px;
74
- }
75
- </style>
76
-
77
- <script>
78
- function speak(text) {
79
- window.speechSynthesis.cancel();
80
- const msg = new SpeechSynthesisUtterance(text);
81
- window.speechSynthesis.speak(msg);
82
- }
83
- </script>
84
- """, unsafe_allow_html=True)
85
-
86
- inject_assistant_design()
87
-
88
- # --- 3. SESSION STATE ---
89
- if "msgs" not in st.session_state: st.session_state.msgs = []
90
- if "active_doc" not in st.session_state: st.session_state.active_doc = None
91
-
92
- # --- 4. SIDEBAR NAVIGATION ---
93
  with st.sidebar:
94
- st.markdown(f"### πŸ‘€ {st.session_state.get('username', 'Hassan')}")
 
95
  st.divider()
96
- st.markdown("### AI Assistant Settings")
97
- if st.button("πŸ—‘οΈ Clear Chat History"):
98
- st.session_state.msgs = []
99
- st.rerun()
100
 
101
- # --- 5. THE ASSISTANT INTERFACE ---
102
- st.markdown("### πŸ’¬ Clinical Intelligence Assistant")
103
-
104
- # Only this part scrolls
105
- st.markdown('<div class="chat-scroll-area">', unsafe_allow_html=True)
106
- if not st.session_state.msgs:
107
- st.markdown('<div class="ai-msg">Hello. I am your clinical assistant. How can I help you today?</div>', unsafe_allow_html=True)
108
-
109
- for m in st.session_state.msgs:
110
- bubble = "user-msg" if m["role"] == "user" else "ai-msg"
111
- icon = f'<button class="voice-btn" onclick="speak(\'{m["content"].replace("'", "")}\')">πŸ”Š</button>' if m["role"] == "assistant" else ""
112
- st.markdown(f'<div class="{bubble}">{icon}{m["content"]}</div>', unsafe_allow_html=True)
113
- st.markdown('</div>', unsafe_allow_html=True)
114
-
115
- # THE UNIFIED INPUT SUITE (Plus + Text + Mic)
116
- # These are kept outside the sidebar for the wide look
117
- st.divider()
118
- p1, p2, p3 = st.columns([1, 8, 1])
119
-
120
- with p1:
121
- with st.popover("βž•"):
122
- up = st.file_uploader("Upload Medical PDF", type=['pdf'])
123
- if up:
124
- with pdfplumber.open(up) as f:
125
- st.session_state.active_doc = " ".join([page.extract_text() for page in f.pages])
126
- st.success("Document analyzed successfully.")
127
-
128
- with p2:
129
- q = st.chat_input("Ask a medical question...")
130
-
131
- with p3:
132
- v = st.audio_input("🎀", label_visibility="collapsed")
133
-
134
- # --- 6. AI RESPONSE ENGINE ---
135
- final_q = q if q else (v if v else None)
136
-
137
- if final_q:
138
- # Handle Audio to Text conversion
139
- if hasattr(final_q, 'getvalue'):
140
- with st.spinner("Transcribing voice..."):
141
- final_q = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create(
142
- file=("a.wav", final_q.getvalue()),
143
- model="whisper-large-v3",
144
- response_format="text"
145
- )
146
-
147
- st.session_state.msgs.append({"role": "user", "content": final_q})
148
 
149
- # Process with Groq
150
- with st.spinner("AI is thinking..."):
151
- sys_p = "You are a clinical assistant. Use general medical knowledge or provided PDF context. Strictly refuse non-medical chat."
152
- ctx = f"PDF CONTEXT: {st.session_state.active_doc}" if st.session_state.active_doc else "No PDF context."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
  ans = Groq(api_key=GROQ_API_KEY).chat.completions.create(
155
- model="llama-3.3-70b-versatile",
156
- messages=[{"role": "system", "content": sys_p}, {"role": "system", "content": ctx}] + st.session_state.msgs
157
  )
158
  st.session_state.msgs.append({"role": "assistant", "content": ans.choices[0].message.content})
 
 
 
 
 
 
159
 
160
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 fpdf import FPDF
9
+ import plotly.graph_objects as go
10
+ import google.generativeai as genai
11
+ import folium
12
+ from streamlit_folium import st_folium
13
+ from streamlit_geolocation import streamlit_geolocation
14
  from groq import Groq
15
  import pdfplumber
16
 
17
+ # --- 1. CORE SYSTEM CONFIG ---
18
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
19
+ GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
20
+
21
+ if not GROQ_API_KEY or not GOOGLE_API_KEY:
22
+ st.error("⚠️ API Keys Missing! Please set them in Space Secrets.")
23
+ st.stop()
24
+
25
+ st.set_page_config(page_title="IntelliCare Portal | Hassan Naseer", layout="wide", page_icon="πŸ₯")
26
+
27
+ # --- 2. THEME & PDF GENERATOR ---
28
+ if "theme" not in st.session_state: st.session_state.theme = "Light"
29
+
30
+ def generate_medical_pdf(chat_history, username):
31
+ pdf = FPDF()
32
+ pdf.add_page()
33
+ pdf.set_font("Arial", 'B', 16)
34
+ pdf.cell(200, 10, txt="IntelliCare Portal - Clinical Summary", ln=True, align='C')
35
+ pdf.set_font("Arial", size=10)
36
+ pdf.cell(200, 10, txt=f"Patient: {username} | Date: {datetime.now().strftime('%Y-%m-%d %H:%M')}", ln=True, align='C')
37
+ pdf.ln(10)
38
+ for msg in chat_history:
39
+ role = "PATIENT" if msg["role"] == "user" else "CLINICAL AI"
40
+ pdf.set_font("Arial", 'B', 10); pdf.cell(0, 10, txt=f"{role}:", ln=True)
41
+ pdf.set_font("Arial", size=10); pdf.multi_cell(0, 8, txt=msg["content"]); pdf.ln(4)
42
+ return pdf.output(dest='S').encode('latin-1')
43
+
44
+ def inject_theme():
45
+ bg = "#000000" if st.session_state.theme == "Dark" else "#FFFFFF"
46
+ txt = "#FFFFFF" if st.session_state.theme == "Dark" else "#000000"
47
+ card = "#111111" if st.session_state.theme == "Dark" else "#F0F2F6"
48
+ brd = "#FFFFFF" if st.session_state.theme == "Dark" else "#000000"
49
+ st.markdown(f"""<style>
50
+ .stApp {{ background-color: {bg} !important; color: {txt} !important; }}
51
+ .chat-bubble {{ padding: 15px; border-radius: 15px; margin-bottom: 10px; border: 1px solid {brd}; }}
52
+ .user-msg {{ background-color: rgba(59, 130, 246, 0.1); border-left: 5px solid #3b82f6; }}
53
+ .ai-msg {{ background-color: rgba(16, 185, 129, 0.1); border-left: 5px solid #10b981; }}
54
+ .clinical-card {{ background-color: {card}; border: 2px solid {brd}; padding: 25px; border-radius: 15px; color: {txt}; }}
55
+ </style>""", unsafe_allow_html=True)
56
+
57
+ inject_theme()
58
+
59
+ # --- 3. DATA PERSISTENCE ---
60
+ USER_DB, HISTORY_DB = "users_secure.csv", "clinical_history.csv"
61
+ def hash_pass(pwd): return hashlib.sha256(str.encode(pwd)).hexdigest()
62
+ def load_db(file, cols):
63
+ if os.path.exists(file): return pd.read_csv(file)
64
+ return pd.DataFrame(columns=cols)
65
+
66
+ def get_vector_db():
67
+ import chromadb
68
+ from chromadb.utils import embedding_functions
69
+ client = chromadb.PersistentClient(path="./med_vector_db")
70
+ emb_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="paraphrase-MiniLM-L3-v2")
71
+ main = client.get_or_create_collection(name="intellicare_vFinal", embedding_function=emb_fn)
72
+ signal = client.get_or_create_collection(name="signals_targeted", embedding_function=emb_fn)
73
+ return main, signal
74
+
75
+ # --- 4. AUTHENTICATION & SESSION ---
76
+ if "logged_in" not in st.session_state: st.session_state.logged_in = False
77
+ if "last_processed_audio" not in st.session_state: st.session_state.last_processed_audio = None
78
+ if "active_doc_text" not in st.session_state: st.session_state.active_doc_text = None
79
+ if "active_doc_name" not in st.session_state: st.session_state.active_doc_name = None
80
+
81
+ if not st.session_state.logged_in:
82
+ st.markdown("<h1 style='text-align: center;'>πŸ₯ IntelliCare Portal</h1>", unsafe_allow_html=True)
83
+ c2 = st.columns([1, 2, 1])[1]
84
+ with c2:
85
+ t1, t2 = st.tabs(["πŸ” Login", "πŸ“ Create Account"])
86
+ with t1:
87
+ u, p = st.text_input("Username", key="l_u"), st.text_input("Password", type="password", key="l_p")
88
+ if st.button("Sign In"):
89
+ users = load_db(USER_DB, ["username", "password", "role"])
90
+ match = users[(users['username'] == u) & (users['password'] == hash_pass(p))]
91
+ if not match.empty:
92
+ st.session_state.logged_in, st.session_state.username = True, u
93
+ st.session_state.role, st.session_state.msgs = match.iloc[0]['role'], []
94
+ st.rerun()
95
  st.stop()
96
 
97
+ # --- 5. NAVIGATION ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  with st.sidebar:
99
+ st.markdown(f"### πŸ‘€ {st.session_state.username}")
100
+ if st.button("Logout"): st.session_state.logged_in = False; st.rerun()
101
  st.divider()
102
+ 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"])
 
 
 
103
 
104
+ # --- 6. UNIFIED AI CHAT (DUAL-MODE) ---
105
+ if st.session_state.role == "Patient" and nav == "πŸ’¬ AI Chat":
106
+ st.markdown('<div class="clinical-card"><h3>πŸ’¬ Medical Intelligence Assistant</h3></div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ if st.session_state.active_doc_text:
109
+ c1, c2 = st.columns([8, 2])
110
+ with c1: st.success(f"πŸ“‚ **Active Document:** {st.session_state.active_doc_name}")
111
+ with c2:
112
+ if st.button("πŸ—‘οΈ Clear"):
113
+ st.session_state.active_doc_text = None
114
+ st.session_state.active_doc_name = None
115
+ st.rerun()
116
+
117
+ for m in st.session_state.msgs:
118
+ bubble = "user-msg" if m["role"] == "user" else "ai-msg"
119
+ st.markdown(f'<div class="chat-bubble {bubble}">{m["content"]}</div>', unsafe_allow_html=True)
120
+
121
+ st.divider()
122
+ in1, in2, in3 = st.columns([0.6, 0.6, 8])
123
+ with in1: v = st.audio_input("🎀", label_visibility="collapsed", key=f"v_{len(st.session_state.msgs)}")
124
+ with in2:
125
+ with st.popover("βž•"):
126
+ up_pdf = st.file_uploader("Upload PDF Document", type=['pdf'], key="doc_up")
127
+ with in3: q = st.chat_input("Ask a medical query or about your document...")
128
+
129
+ final_q = None
130
+ if up_pdf and st.session_state.active_doc_name != up_pdf.name:
131
+ with pdfplumber.open(up_pdf) as f:
132
+ st.session_state.active_doc_text = " ".join([p.extract_text() for p in f.pages if p.extract_text()])
133
+ st.session_state.active_doc_name = up_pdf.name
134
+ final_q = f"I have uploaded '{up_pdf.name}'. Analyze and explain what is written in this document in detail."
135
+ elif q: final_q = q
136
+ elif v:
137
+ v_hash = hashlib.md5(v.getvalue()).hexdigest()
138
+ if v_hash != st.session_state.last_processed_audio:
139
+ final_q = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create(file=("a.wav", v.getvalue()), model="whisper-large-v3", response_format="text")
140
+ st.session_state.last_processed_audio = v_hash
141
+
142
+ if final_q:
143
+ st.session_state.msgs.append({"role": "user", "content": final_q})
144
+ sys_p = (
145
+ "You are the IntelliCare Medical System. "
146
+ "RULE 1 (DOCUMENTS): If document context is provided, analyze and explain it fully. "
147
+ "RULE 2 (TEXT CHAT): For text queries, be a strict Medical Specialist. "
148
+ "If asked for Python, essays, or non-medical info, refuse and state you only provide medical assistance."
149
+ )
150
+ main_col, _ = get_vector_db()
151
+ res = main_col.query(query_texts=[final_q], n_results=1)
152
+ db_ctx = res['documents'][0][0] if (res.get('documents') and len(res['documents'][0]) > 0) else "N/A"
153
+ full_ctx = f"DOC: {st.session_state.active_doc_text}\nDB: {db_ctx}"
154
 
155
  ans = Groq(api_key=GROQ_API_KEY).chat.completions.create(
156
+ model="llama-3.3-70b-versatile",
157
+ messages=[{"role": "system", "content": sys_p}, {"role": "system", "content": f"CTX: {full_ctx}"}] + st.session_state.msgs
158
  )
159
  st.session_state.msgs.append({"role": "assistant", "content": ans.choices[0].message.content})
160
+ st.rerun()
161
+
162
+ # --- 7. HEALTH LAB TOOL LIBRARY ---
163
+ elif nav == "πŸ§ͺ Health Lab":
164
+ st.markdown('<div class="clinical-card"><h3>πŸ§ͺ Clinical Tool Library</h3></div>', unsafe_allow_html=True)
165
+ tool = st.selectbox("πŸ› οΈ Choose Tool", ["Select Tool", "βš–οΈ BMI Calculator", "🩸 Sugar Tracker", "πŸ«€ Heart Rate"])
166
 
167
+ if tool == "βš–οΈ BMI Calculator":
168
+
169
+ w, h = st.number_input("Weight (kg)", 30, 200, 70), st.number_input("Height (cm)", 100, 250, 175)
170
+ bmi = round(w / ((h/100)**2), 1)
171
+ st.plotly_chart(go.Figure(go.Indicator(mode="gauge+number", value=bmi, gauge={'axis': {'range': [10, 40]}, 'bar': {'color': "#10b981"}})), use_container_width=True)
172
+
173
+ elif tool == "🩸 Sugar Tracker":
174
+
175
+ sugar = st.number_input("Level (mg/dL)", 50, 500, 100)
176
+ st.area_chart(pd.DataFrame(np.random.randn(24, 1).cumsum() + 100), color="#ff9800")
177
+
178
+ elif tool == "πŸ«€ Heart Rate":
179
+
180
+ hr = st.slider("BPM", 40, 180, 72)
181
+ t = np.linspace(0, 2, 200)
182
+ y = np.sin(2 * np.pi * (hr/60) * t) + 0.5 * np.sin(4 * np.pi * (hr/60) * t)
183
+ st.plotly_chart(go.Figure(data=go.Scatter(y=y, mode='lines', line=dict(color='#ff4b4b'))), use_container_width=True)
184
+
185
+ # --- 8. MAPS & VIDEO ---
186
+ elif nav == "πŸ“ Nearby Hospitals":
187
+ loc = streamlit_geolocation()
188
+ if loc and loc.get("latitude"):
189
+ lat, lon = loc["latitude"], loc["longitude"]
190
+ m = folium.Map(location=[lat, lon], zoom_start=14)
191
+ st_folium(m, width=1200, height=500)
192
+
193
+ elif nav == "πŸ“ž Video Call":
194
+ docs = load_db(USER_DB, ["username", "role"])
195
+ sel = st.selectbox("Specialist", docs[docs['role'] == 'Doctor']['username'].tolist())
196
+ if st.button("Start Call"):
197
+ room = f"IntelliCare-{st.session_state.username}-{sel}"
198
+ _, signal_col = get_vector_db()
199
+ signal_col.upsert(documents=[f"CALL|{sel}|{room}|{st.session_state.username}"], ids=["latest"])
200
+ st.session_state.active_room = room
201
+ if "active_room" in st.session_state:
202
+ st.components.v1.html(f'<iframe src="https://meet.jit.si/{st.session_state.active_room}" width="100%" height="550px"></iframe>', height=600)
203
+
204
+ elif nav == "πŸ–₯️ Consultation Desk":
205
+ _, signal_col = get_vector_db()
206
+ res = signal_col.get(ids=["latest"])
207
+ if res.get('documents'):
208
+ parts = res['documents'][0].split("|")
209
+ if parts[1] == st.session_state.username:
210
+ if st.button("βœ… Join Call"): st.session_state.active_call = parts[2]
211
+ if "active_call" in st.session_state:
212
+ st.components.v1.html(f'<iframe src="https://meet.jit.si/{parts[2]}" width="100%" height="650px"></iframe>', height=650)