Lincoln Gombedza Claude Sonnet 4.6 commited on
Commit
320ec62
·
unverified ·
1 Parent(s): 400bccb

Simplify to BYOK-only for Hugging Face — remove Stripe/subscriptions

Browse files

All features (chat, exports, CPD records) now free for everyone.
Users bring their own Anthropic API key. Updated README with HF metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. README.md +31 -93
  2. streamlit_app.py +44 -107
README.md CHANGED
@@ -1,106 +1,44 @@
1
- # PNA Assistant — Professional Nurse Advocate AI
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- AI-powered educational tool for the A-EQUIP model and restorative clinical supervision.
4
 
5
- **Stack:** Python · Streamlit · Claude claude-opus-4-6 · FAISS RAG · Stripe · FastAPI webhook · Docker · AWS EC2
6
 
7
- ## Quick Start (local)
8
 
9
- ```bash
10
- pip install -r requirements.txt
11
 
12
- # Copy the A-EQUIP guide into assets/
13
- cp "Professional nurse advocate A-EQUIP model Guide.md" assets/aequip_guide.md
14
 
15
- streamlit run streamlit_app.py
16
- ```
 
 
 
17
 
18
- ## AWS Deployment
19
 
20
- ```bash
21
- # 1. Launch EC2 t3.small (Ubuntu 22.04, eu-west-2)
22
- # 2. SSH in and install Docker
23
- sudo apt update && sudo apt install -y docker.io docker-compose-plugin curl
24
 
25
- # 3. Clone repo
26
- git clone https://github.com/Clinical-Quality-Artifical-Intelligence/Professional-Nurse-Advocate-Assistant
27
- cd Professional-Nurse-Advocate-Assistant
28
 
29
- # 4. Set up env
30
- cp .env.example .env
31
- nano .env # fill in Stripe keys + webhook secret
32
 
33
- # 5. Copy the A-EQUIP guide
34
- cp "Professional nurse advocate A-EQUIP model Guide.md" assets/aequip_guide.md
35
 
36
- # 6. Deploy
37
- docker compose up -d
38
 
39
- # 7. SSL with Let's Encrypt
40
- sudo apt install -y certbot
41
- sudo certbot certonly --standalone -d pna.nursingcitizendevelopment.com
42
-
43
- # 8. Restart with SSL
44
- docker compose restart nginx
45
- ```
46
-
47
- ## Stripe Webhook Setup (5 minutes)
48
-
49
- After deploying to EC2:
50
-
51
- 1. Stripe Dashboard → **Developers** → **Webhooks** → **Add endpoint**
52
- 2. Endpoint URL: `https://pna.nursingcitizendevelopment.com/webhook`
53
- 3. Events to listen for: `checkout.session.completed`
54
- 4. Click **Add endpoint**, then reveal the **Signing secret**
55
- 5. Copy the signing secret (starts `whsec_...`)
56
- 6. SSH into EC2 and update `.env`:
57
- ```bash
58
- nano .env
59
- # Set: STRIPE_WEBHOOK_SECRET=whsec_your_real_secret
60
- docker compose restart pna-webhook
61
- ```
62
-
63
- When a customer pays, the webhook:
64
- - Generates a `PNA-PRO-XXXXX` activation code
65
- - Emails it to the customer via Amazon SES
66
- - Logs it to `/app/codes.jsonl` inside the container
67
-
68
- ### Amazon SES Setup (for automated emails)
69
-
70
- ```bash
71
- # Verify your sending domain in SES console (AWS Console → SES → Verified identities)
72
- # Then attach an IAM role to the EC2 instance with ses:SendEmail permission
73
- # No access keys needed — uses the instance role automatically
74
- ```
75
-
76
- Or check codes manually until SES is configured:
77
- ```bash
78
- docker exec pna-webhook cat /app/codes.jsonl
79
- ```
80
-
81
- ## Subscription Tiers
82
-
83
- | Tier | Price | Features |
84
- |------|-------|---------|
85
- | Free | £0 | Chat with BYOK API key, unlimited conversations |
86
- | Pro | £9.99/month | + Supervision notes (.docx), CPD logs, session history |
87
- | Institution | £99/month | + Multi-user, custom branding, bulk CPD |
88
-
89
- ## Environment Variables
90
-
91
- | Variable | Description |
92
- |----------|-------------|
93
- | `STRIPE_PRO_LINK` | Stripe Payment Link for Pro plan |
94
- | `STRIPE_INSTITUTION` | Stripe Payment Link for Institution plan |
95
- | `STRIPE_PUBLISHABLE_KEY` | Stripe public key (for future Stripe.js use) |
96
- | `STRIPE_SECRET_KEY` | Stripe secret key (future server-side use) |
97
- | `STRIPE_WEBHOOK_SECRET` | Webhook signing secret (`whsec_...`) |
98
- | `AWS_REGION` | AWS region for SES (default: `eu-west-2`) |
99
- | `SES_FROM_EMAIL` | Verified SES sender email |
100
-
101
- ## Copyright
102
-
103
- A-EQUIP model guide content — Contains public sector information licensed under the
104
- [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/) — NHS England.
105
-
106
- Application code — Apache 2.0 — © 2026 Lincoln Gombedza
 
1
+ ---
2
+ title: PNA Assistant
3
+ emoji: 👨🏾‍⚕️
4
+ colorFrom: indigo
5
+ colorTo: teal
6
+ sdk: streamlit
7
+ sdk_version: 1.35.0
8
+ app_file: streamlit_app.py
9
+ pinned: true
10
+ license: apache-2.0
11
+ short_description: AI guide to the A-EQUIP model for Professional Nurse Advocates
12
+ ---
13
 
14
+ # 👨🏾‍⚕️ PNA Assistant Professional Nurse Advocate AI
15
 
16
+ An educational AI assistant for nursing professionals learning the **A-EQUIP model** of clinical supervision and the **Professional Nurse Advocate (PNA)** role in NHS England.
17
 
18
+ **Free forever · BYOK · No data stored**
19
 
20
+ ---
 
21
 
22
+ ## What it does
 
23
 
24
+ - 🌿 **Restorative supervision** — Reflective conversations to process the emotional impact of clinical work
25
+ - 📚 **A-EQUIP learning** — Guides you through all four functions (Restorative, Normative, Formative, PAQI)
26
+ - 🔍 **Reflective frameworks** — Gibbs, Driscoll, Johns, SBAR support
27
+ - 📄 **Supervision note export** — Structured notes for your NMC revalidation portfolio (.docx)
28
+ - 📋 **CPD record export** — NMC-formatted continuing professional development log (.docx)
29
 
30
+ ## How to use
31
 
32
+ 1. Get a free API key at [console.anthropic.com](https://console.anthropic.com) (~£0.01 per conversation)
33
+ 2. Enter your key in the sidebar — it's used only for your session and never stored
34
+ 3. Start chatting about A-EQUIP, supervision, or your PNA practice
 
35
 
36
+ ## Clinical disclaimer
 
 
37
 
38
+ > This tool is for **educational purposes only**. It does not provide clinical advice, diagnosis, or treatment recommendations. It is not a replacement for human supervision or your employer's formal clinical governance processes.
 
 
39
 
40
+ ## Attribution
 
41
 
42
+ Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/) — NHS England.
 
43
 
44
+ Built by [Lincoln Gombedza](https://nursingcitizendevelopment.com) · [Clinical Quality AI (CQAI)](https://github.com/Clinical-Quality-Artifical-Intelligence) · NMC Standards aligned
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
streamlit_app.py CHANGED
@@ -1,16 +1,12 @@
1
  """
2
- Professional Nurse Advocate Assistant — SaaS Edition
3
  Powered by Claude claude-opus-4-6 | A-EQUIP Model | NHS OGL v3.0
4
 
5
- Architecture:
6
- - BYOK (Bring Your Own Key) — user enters Anthropic API key (Free tier)
7
- - Pro subscription via Stripe — unlocks session history, exports, and CPD logging
8
- - AWS-ready: runs in Docker on EC2, auth via Cognito (Phase 2)
9
  """
10
 
11
  import streamlit as st
12
  from datetime import date
13
- import os
14
 
15
  from pna.rag import PNAKnowledgeBase
16
  from pna.claude_client import stream_response, generate_supervision_note
@@ -25,11 +21,6 @@ st.set_page_config(
25
  initial_sidebar_state="expanded",
26
  )
27
 
28
- # ─── Stripe config (replace with your real Stripe Payment Links) ──────────────
29
-
30
- STRIPE_PRO_LINK = os.getenv("STRIPE_PRO_LINK", "https://buy.stripe.com/14A28t0P99Qmdph8v68og00")
31
- STRIPE_INSTITUTION = os.getenv("STRIPE_INSTITUTION", "https://buy.stripe.com/9B63cxfK3d2y3OHbHi8og01")
32
-
33
  # ─── Custom CSS ──────────────────────────────────────────────────────────────
34
 
35
  st.markdown("""
@@ -61,17 +52,9 @@ html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
61
  padding: 0.2rem 0.75rem;
62
  font-size: 0.8rem;
63
  margin-top: 0.75rem;
 
64
  }
65
 
66
- .tier-card {
67
- border: 2px solid #e5e7eb;
68
- border-radius: 12px;
69
- padding: 1.25rem;
70
- text-align: center;
71
- background: white;
72
- }
73
- .tier-card.pro { border-color: #4f63d2; }
74
-
75
  .disclaimer {
76
  background: #f8fafc;
77
  border: 1px solid #e2e8f0;
@@ -92,22 +75,19 @@ html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
92
  </style>
93
  """, unsafe_allow_html=True)
94
 
95
- # ─── Session state defaults ───────────────────────────────────────────────────
96
 
97
  defaults = {
98
  "messages": [],
99
  "api_key": "",
100
- "subscription": "free", # free | pro | institution
101
  "nurse_name": "",
102
  "session_started": None,
103
- "rag_loaded": False,
104
- "kb": None,
105
  }
106
  for k, v in defaults.items():
107
  if k not in st.session_state:
108
  st.session_state[k] = v
109
 
110
- # ─── Load RAG knowledge base (cached) ────────────────────────────────────────
111
 
112
  @st.cache_resource(show_spinner="Loading A-EQUIP knowledge base…")
113
  def load_knowledge_base():
@@ -119,17 +99,17 @@ kb = load_knowledge_base()
119
 
120
  with st.sidebar:
121
  st.markdown("### 👨🏾‍⚕️ PNA Assistant")
122
- st.caption("A-EQUIP Model | Restorative Supervision")
123
  st.divider()
124
 
125
  # ── API Key (BYOK) ────────────────────────────────────────────────────────
126
  st.markdown("#### 🔑 Your Anthropic API Key")
127
  api_key_input = st.text_input(
128
- "Enter key to activate",
129
  type="password",
130
  value=st.session_state.api_key,
131
  placeholder="sk-ant-api03-...",
132
- help="Your key is used only for this session. Never stored."
133
  )
134
  if api_key_input:
135
  st.session_state.api_key = api_key_input
@@ -142,69 +122,38 @@ with st.sidebar:
142
 
143
  st.divider()
144
 
145
- # ── Subscription ──────────────────────────────────────────────────────────
146
- st.markdown("#### 💳 Subscription")
147
- sub = st.session_state.subscription
148
-
149
- if sub == "free":
150
- st.info("**Free tier** — BYOK, unlimited chat")
151
- st.markdown(
152
- f"[⭐ Upgrade to Pro — £9.99/mo]({STRIPE_PRO_LINK})",
153
- unsafe_allow_html=False,
154
- )
155
- with st.expander("What's in Pro?"):
156
- st.markdown("""
157
- - 💾 Save session history
158
- - 📄 Download supervision notes (.docx)
159
- - 📋 NMC CPD log export (.docx)
160
- - 📧 Monthly PNA newsletter
161
- - 🏫 [Institution plan — £99/mo]({})
162
- """.format(STRIPE_INSTITUTION))
163
- # Manual activation after Stripe payment
164
- act_code = st.text_input("Activation code (from email after payment)")
165
- if act_code and act_code.startswith("PNA-PRO-"):
166
- st.session_state.subscription = "pro"
167
- st.success("Pro activated! 🎉")
168
- st.rerun()
169
-
170
- elif sub in ("pro", "institution"):
171
- label = "⭐ Pro" if sub == "pro" else "🏫 Institution"
172
- st.success(f"{label} — Active")
173
- if st.button("Manage subscription (Stripe)"):
174
- st.markdown(f"[Open Stripe portal →]({STRIPE_PRO_LINK})")
175
 
176
  st.divider()
177
 
178
- # ── Nurse info (Pro only) ─────────────────────────────────────────────────
179
- if sub in ("pro", "institution"):
180
- st.markdown("#### 📋 Your Details")
181
- st.session_state.nurse_name = st.text_input(
182
- "Your name (for CPD records)",
183
- value=st.session_state.nurse_name,
184
- placeholder="Nurse Jane Smith",
185
- )
186
- cpd_hours = st.number_input(
187
- "Session length (hours)", min_value=0.5, max_value=8.0,
188
- value=1.0, step=0.5
189
- )
190
- st.divider()
191
-
192
  # ── Session controls ──────────────────────────────────────────────────────
193
  st.markdown("#### 🔄 Session")
194
  col1, col2 = st.columns(2)
195
  with col1:
196
- if st.button("🗑️ Clear chat", use_container_width=True):
197
  st.session_state.messages = []
198
  st.session_state.session_started = None
199
  st.rerun()
200
  with col2:
201
- if st.button("📋 New session", use_container_width=True):
202
  st.session_state.messages = []
203
  st.session_state.session_started = date.today().isoformat()
204
  st.rerun()
205
 
206
- # ── Exports (Pro only) ────────────────────────────────────────────────────
207
- if sub in ("pro", "institution") and st.session_state.messages:
208
  st.divider()
209
  st.markdown("#### 📥 Export")
210
 
@@ -221,29 +170,23 @@ with st.sidebar:
221
 
222
  if "last_note" in st.session_state:
223
  note = st.session_state["last_note"]
224
- st.text_area("Supervision note preview", note, height=200)
225
 
226
  if HAS_DOCX:
227
- docx_bytes = build_supervision_docx(
228
- note, st.session_state.nurse_name
229
- )
230
  if docx_bytes:
231
  st.download_button(
232
- "⬇️ Download as .docx",
233
  data=docx_bytes,
234
  file_name=f"PNA_Supervision_{date.today()}.docx",
235
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
236
  use_container_width=True,
237
  )
238
 
239
- cpd_bytes = build_cpd_docx(
240
- note,
241
- st.session_state.nurse_name,
242
- cpd_hours if "cpd_hours" in dir() else 1.0,
243
- )
244
  if cpd_bytes:
245
  st.download_button(
246
- "⬇️ Download CPD Record (.docx)",
247
  data=cpd_bytes,
248
  file_name=f"NMC_CPD_{date.today()}.docx",
249
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -268,17 +211,18 @@ with st.sidebar:
268
 
269
  # ─── Main content ─────────────────────────────────────────────────────────────
270
 
271
- # Hero header
272
  st.markdown("""
273
  <div class="hero-header">
274
  <h1>👨🏾‍⚕️ Professional Nurse Advocate Assistant</h1>
275
  <p>Your AI guide to the A-EQUIP model, restorative supervision, and quality improvement</p>
276
- <span class="pill">🇬🇧 NHS England · A-EQUIP Model · NMC Standards</span>
 
277
  <span class="pill">OGL v3.0</span>
278
  </div>
279
  """, unsafe_allow_html=True)
280
 
281
- # Welcome / quick start (only when no messages yet)
 
282
  if not st.session_state.messages:
283
  col1, col2, col3 = st.columns(3)
284
  with col1:
@@ -306,7 +250,6 @@ Get support developing your Personal Action for Quality Improvement.
306
  "Help me reflect on a challenging patient interaction using Gibbs cycle.",
307
  "What's the difference between the normative and formative functions?",
308
  ]
309
-
310
  cols = st.columns(2)
311
  for i, ex in enumerate(examples):
312
  with cols[i % 2]:
@@ -314,14 +257,13 @@ Get support developing your Personal Action for Quality Improvement.
314
  st.session_state.messages.append({"role": "user", "content": ex})
315
  st.rerun()
316
 
317
- # ── Chat history ─────────────────────────────────────────────────────────────
318
 
319
  for msg in st.session_state.messages:
320
- with st.chat_message(msg["role"],
321
- avatar="👩‍⚕️" if msg["role"] == "user" else "👨🏾‍⚕️"):
322
  st.markdown(msg["content"])
323
 
324
- # ── Chat input ───────────────────────────────────────────────────────────────
325
 
326
  prompt = st.chat_input(
327
  "Ask me about A-EQUIP, restorative supervision, or the PNA role…",
@@ -335,21 +277,17 @@ if not st.session_state.api_key:
335
  )
336
 
337
  if prompt and st.session_state.api_key:
338
- # Record session start time
339
  if st.session_state.session_started is None:
340
  st.session_state.session_started = date.today().isoformat()
341
 
342
- # Add user message
343
  st.session_state.messages.append({"role": "user", "content": prompt})
344
  with st.chat_message("user", avatar="👩‍⚕️"):
345
  st.markdown(prompt)
346
 
347
- # Get RAG context
348
  context = kb.search(prompt) if kb.index is not None else ""
349
 
350
- # Stream response
351
  with st.chat_message("assistant", avatar="👨🏾‍⚕️"):
352
- response_placeholder = st.empty()
353
  full_response = ""
354
 
355
  try:
@@ -360,9 +298,9 @@ if prompt and st.session_state.api_key:
360
  context=context,
361
  ):
362
  full_response += chunk
363
- response_placeholder.markdown(full_response + "▋")
364
 
365
- response_placeholder.markdown(full_response)
366
 
367
  except Exception as e:
368
  err = str(e)
@@ -372,11 +310,9 @@ if prompt and st.session_state.api_key:
372
  full_response = "⏳ Rate limit reached. Please wait a moment and try again."
373
  else:
374
  full_response = f"❌ An error occurred: {err}"
375
- response_placeholder.error(full_response)
376
 
377
- st.session_state.messages.append(
378
- {"role": "assistant", "content": full_response}
379
- )
380
 
381
  # ─── Footer ───────────────────────────────────────────────────────────────────
382
 
@@ -390,7 +326,8 @@ If you are experiencing a clinical emergency or safeguarding concern, follow you
390
  <br/><br/>
391
  <span class="ogl-notice">
392
  Contains public sector information licensed under the
393
- <a href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" target="_blank">Open Government Licence v3.0</a> — NHS England.
 
394
  Built by <a href="https://nursingcitizendevelopment.com" target="_blank">Lincoln Gombedza</a> · CQAI ·
395
  <a href="https://github.com/Clinical-Quality-Artifical-Intelligence/Professional-Nurse-Advocate-Assistant" target="_blank">Open Source</a>
396
  </span>
 
1
  """
2
+ Professional Nurse Advocate Assistant
3
  Powered by Claude claude-opus-4-6 | A-EQUIP Model | NHS OGL v3.0
4
 
5
+ BYOK — bring your own Anthropic API key. Free forever on Hugging Face.
 
 
 
6
  """
7
 
8
  import streamlit as st
9
  from datetime import date
 
10
 
11
  from pna.rag import PNAKnowledgeBase
12
  from pna.claude_client import stream_response, generate_supervision_note
 
21
  initial_sidebar_state="expanded",
22
  )
23
 
 
 
 
 
 
24
  # ─── Custom CSS ──────────────────────────────────────────────────────────────
25
 
26
  st.markdown("""
 
52
  padding: 0.2rem 0.75rem;
53
  font-size: 0.8rem;
54
  margin-top: 0.75rem;
55
+ margin-right: 0.25rem;
56
  }
57
 
 
 
 
 
 
 
 
 
 
58
  .disclaimer {
59
  background: #f8fafc;
60
  border: 1px solid #e2e8f0;
 
75
  </style>
76
  """, unsafe_allow_html=True)
77
 
78
+ # ─── Session state ────────────────────────────────────────────────────────────
79
 
80
  defaults = {
81
  "messages": [],
82
  "api_key": "",
 
83
  "nurse_name": "",
84
  "session_started": None,
 
 
85
  }
86
  for k, v in defaults.items():
87
  if k not in st.session_state:
88
  st.session_state[k] = v
89
 
90
+ # ─── Load RAG knowledge base ──────────────────────────────────────────────────
91
 
92
  @st.cache_resource(show_spinner="Loading A-EQUIP knowledge base…")
93
  def load_knowledge_base():
 
99
 
100
  with st.sidebar:
101
  st.markdown("### 👨🏾‍⚕️ PNA Assistant")
102
+ st.caption("A-EQUIP Model · Restorative Supervision")
103
  st.divider()
104
 
105
  # ── API Key (BYOK) ────────────────────────────────────────────────────────
106
  st.markdown("#### 🔑 Your Anthropic API Key")
107
  api_key_input = st.text_input(
108
+ "Enter key to start chatting",
109
  type="password",
110
  value=st.session_state.api_key,
111
  placeholder="sk-ant-api03-...",
112
+ help="Used only for this session and never stored.",
113
  )
114
  if api_key_input:
115
  st.session_state.api_key = api_key_input
 
122
 
123
  st.divider()
124
 
125
+ # ── Your details ──────────────────────────────────────────────────────────
126
+ st.markdown("#### 📋 Your Details")
127
+ st.caption("Optional used in supervision note exports.")
128
+ st.session_state.nurse_name = st.text_input(
129
+ "Your name",
130
+ value=st.session_state.nurse_name,
131
+ placeholder="e.g. Nurse Jane Smith",
132
+ )
133
+ cpd_hours = st.number_input(
134
+ "Session length (hours)",
135
+ min_value=0.5, max_value=8.0,
136
+ value=1.0, step=0.5,
137
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  st.divider()
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # ── Session controls ──────────────────────────────────────────────────────
142
  st.markdown("#### 🔄 Session")
143
  col1, col2 = st.columns(2)
144
  with col1:
145
+ if st.button("🗑️ Clear", use_container_width=True):
146
  st.session_state.messages = []
147
  st.session_state.session_started = None
148
  st.rerun()
149
  with col2:
150
+ if st.button("🆕 New", use_container_width=True):
151
  st.session_state.messages = []
152
  st.session_state.session_started = date.today().isoformat()
153
  st.rerun()
154
 
155
+ # ── Exports ───────────────────────────────────────────────────────────────
156
+ if st.session_state.messages:
157
  st.divider()
158
  st.markdown("#### 📥 Export")
159
 
 
170
 
171
  if "last_note" in st.session_state:
172
  note = st.session_state["last_note"]
173
+ st.text_area("Preview", note, height=200)
174
 
175
  if HAS_DOCX:
176
+ docx_bytes = build_supervision_docx(note, st.session_state.nurse_name)
 
 
177
  if docx_bytes:
178
  st.download_button(
179
+ "⬇️ Supervision note (.docx)",
180
  data=docx_bytes,
181
  file_name=f"PNA_Supervision_{date.today()}.docx",
182
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
183
  use_container_width=True,
184
  )
185
 
186
+ cpd_bytes = build_cpd_docx(note, st.session_state.nurse_name, cpd_hours)
 
 
 
 
187
  if cpd_bytes:
188
  st.download_button(
189
+ "⬇️ NMC CPD Record (.docx)",
190
  data=cpd_bytes,
191
  file_name=f"NMC_CPD_{date.today()}.docx",
192
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
 
211
 
212
  # ─── Main content ─────────────────────────────────────────────────────────────
213
 
 
214
  st.markdown("""
215
  <div class="hero-header">
216
  <h1>👨🏾‍⚕️ Professional Nurse Advocate Assistant</h1>
217
  <p>Your AI guide to the A-EQUIP model, restorative supervision, and quality improvement</p>
218
+ <span class="pill">🇬🇧 NHS England · A-EQUIP Model</span>
219
+ <span class="pill">NMC Standards</span>
220
  <span class="pill">OGL v3.0</span>
221
  </div>
222
  """, unsafe_allow_html=True)
223
 
224
+ # ── Welcome / quick start ─────────────────────────────────────────────────────
225
+
226
  if not st.session_state.messages:
227
  col1, col2, col3 = st.columns(3)
228
  with col1:
 
250
  "Help me reflect on a challenging patient interaction using Gibbs cycle.",
251
  "What's the difference between the normative and formative functions?",
252
  ]
 
253
  cols = st.columns(2)
254
  for i, ex in enumerate(examples):
255
  with cols[i % 2]:
 
257
  st.session_state.messages.append({"role": "user", "content": ex})
258
  st.rerun()
259
 
260
+ # ── Chat history ─────────────────────────────────────────────────────────────
261
 
262
  for msg in st.session_state.messages:
263
+ with st.chat_message(msg["role"], avatar="👩‍⚕️" if msg["role"] == "user" else "👨🏾‍⚕️"):
 
264
  st.markdown(msg["content"])
265
 
266
+ # ── Chat input ───────────────────────────────────────────────────────────────
267
 
268
  prompt = st.chat_input(
269
  "Ask me about A-EQUIP, restorative supervision, or the PNA role…",
 
277
  )
278
 
279
  if prompt and st.session_state.api_key:
 
280
  if st.session_state.session_started is None:
281
  st.session_state.session_started = date.today().isoformat()
282
 
 
283
  st.session_state.messages.append({"role": "user", "content": prompt})
284
  with st.chat_message("user", avatar="👩‍⚕️"):
285
  st.markdown(prompt)
286
 
 
287
  context = kb.search(prompt) if kb.index is not None else ""
288
 
 
289
  with st.chat_message("assistant", avatar="👨🏾‍⚕️"):
290
+ placeholder = st.empty()
291
  full_response = ""
292
 
293
  try:
 
298
  context=context,
299
  ):
300
  full_response += chunk
301
+ placeholder.markdown(full_response + "▋")
302
 
303
+ placeholder.markdown(full_response)
304
 
305
  except Exception as e:
306
  err = str(e)
 
310
  full_response = "⏳ Rate limit reached. Please wait a moment and try again."
311
  else:
312
  full_response = f"❌ An error occurred: {err}"
313
+ placeholder.error(full_response)
314
 
315
+ st.session_state.messages.append({"role": "assistant", "content": full_response})
 
 
316
 
317
  # ─── Footer ───────────────────────────────────────────────────────────────────
318
 
 
326
  <br/><br/>
327
  <span class="ogl-notice">
328
  Contains public sector information licensed under the
329
+ <a href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" target="_blank">Open Government Licence v3.0</a>
330
+ — NHS England.
331
  Built by <a href="https://nursingcitizendevelopment.com" target="_blank">Lincoln Gombedza</a> · CQAI ·
332
  <a href="https://github.com/Clinical-Quality-Artifical-Intelligence/Professional-Nurse-Advocate-Assistant" target="_blank">Open Source</a>
333
  </span>