File size: 13,777 Bytes
b7e98d4
320ec62
b7e98d4
 
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
 
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
 
320ec62
b7e98d4
 
 
 
 
320ec62
b7e98d4
 
 
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
 
 
320ec62
 
 
 
 
 
 
 
 
 
 
 
 
b7e98d4
 
 
 
 
 
 
320ec62
b7e98d4
 
 
 
320ec62
b7e98d4
 
 
 
320ec62
 
b7e98d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320ec62
b7e98d4
 
320ec62
b7e98d4
 
320ec62
b7e98d4
 
 
 
 
 
320ec62
b7e98d4
 
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320ec62
 
b7e98d4
 
 
 
320ec62
 
b7e98d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320ec62
b7e98d4
 
320ec62
b7e98d4
 
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
320ec62
b7e98d4
320ec62
b7e98d4
 
 
 
 
 
 
 
 
320ec62
b7e98d4
320ec62
b7e98d4
 
 
 
 
 
 
 
 
 
 
 
 
320ec62
 
b7e98d4
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
"""
Professional Nurse Advocate Assistant
Powered by Claude claude-opus-4-6 | A-EQUIP Model | NHS OGL v3.0

BYOK β€” bring your own Anthropic API key. Free forever on Hugging Face.
"""

import streamlit as st
from datetime import date

from pna.rag import PNAKnowledgeBase
from pna.claude_client import stream_response, generate_supervision_note
from pna.export import build_supervision_docx, build_cpd_docx, HAS_DOCX

# ─── Page config ─────────────────────────────────────────────────────────────

st.set_page_config(
    page_title="PNA Assistant | Professional Nurse Advocate",
    page_icon="πŸ‘¨πŸΎβ€βš•οΈ",
    layout="wide",
    initial_sidebar_state="expanded",
)

# ─── Custom CSS ──────────────────────────────────────────────────────────────

st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Fraunces:wght@600;700&family=Inter:wght@400;500;600&display=swap');

html, body, [class*="css"] { font-family: 'Inter', sans-serif; }

.hero-header {
    background: linear-gradient(135deg, #1a2460 0%, #2d3da0 60%, #0d9488 100%);
    padding: 2rem 2rem 1.5rem;
    border-radius: 12px;
    margin-bottom: 1.5rem;
    color: white;
}
.hero-header h1 {
    font-family: 'Fraunces', serif;
    font-size: 2rem;
    margin: 0 0 0.25rem 0;
    color: white;
}
.hero-header p { margin: 0; opacity: 0.85; font-size: 1rem; }

.pill {
    display: inline-block;
    background: rgba(255,255,255,0.15);
    border: 1px solid rgba(255,255,255,0.25);
    border-radius: 99px;
    padding: 0.2rem 0.75rem;
    font-size: 0.8rem;
    margin-top: 0.75rem;
    margin-right: 0.25rem;
}

.disclaimer {
    background: #f8fafc;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    padding: 0.75rem 1rem;
    font-size: 0.8rem;
    color: #64748b;
    margin-top: 1rem;
}

.ogl-notice {
    font-size: 0.7rem;
    color: #94a3b8;
    margin-top: 0.5rem;
}

[data-testid="stChatMessage"] { border-radius: 12px; }
</style>
""", unsafe_allow_html=True)

# ─── Session state ────────────────────────────────────────────────────────────

defaults = {
    "messages": [],
    "api_key": "",
    "nurse_name": "",
    "session_started": None,
}
for k, v in defaults.items():
    if k not in st.session_state:
        st.session_state[k] = v

# ─── Load RAG knowledge base ──────────────────────────────────────────────────

@st.cache_resource(show_spinner="Loading A-EQUIP knowledge base…")
def load_knowledge_base():
    return PNAKnowledgeBase()

kb = load_knowledge_base()

# ─── Sidebar ─────────────────────────────────────────────────────────────────

with st.sidebar:
    st.markdown("### πŸ‘¨πŸΎβ€βš•οΈ PNA Assistant")
    st.caption("A-EQUIP Model Β· Restorative Supervision")
    st.divider()

    # ── API Key (BYOK) ────────────────────────────────────────────────────────
    st.markdown("#### πŸ”‘ Your Anthropic API Key")
    api_key_input = st.text_input(
        "Enter key to start chatting",
        type="password",
        value=st.session_state.api_key,
        placeholder="sk-ant-api03-...",
        help="Used only for this session and never stored.",
    )
    if api_key_input:
        st.session_state.api_key = api_key_input
        st.success("βœ… Key entered")

    st.caption(
        "[Get a free API key β†’](https://console.anthropic.com) "
        "Β· Costs ~Β£0.01 per conversation"
    )

    st.divider()

    # ── Your details ──────────────────────────────────────────────────────────
    st.markdown("#### πŸ“‹ Your Details")
    st.caption("Optional β€” used in supervision note exports.")
    st.session_state.nurse_name = st.text_input(
        "Your name",
        value=st.session_state.nurse_name,
        placeholder="e.g. Nurse Jane Smith",
    )
    cpd_hours = st.number_input(
        "Session length (hours)",
        min_value=0.5, max_value=8.0,
        value=1.0, step=0.5,
    )

    st.divider()

    # ── Session controls ──────────────────────────────────────────────────────
    st.markdown("#### πŸ”„ Session")
    col1, col2 = st.columns(2)
    with col1:
        if st.button("πŸ—‘οΈ Clear", use_container_width=True):
            st.session_state.messages = []
            st.session_state.session_started = None
            st.rerun()
    with col2:
        if st.button("πŸ†• New", use_container_width=True):
            st.session_state.messages = []
            st.session_state.session_started = date.today().isoformat()
            st.rerun()

    # ── Exports ───────────────────────────────────────────────────────────────
    if st.session_state.messages:
        st.divider()
        st.markdown("#### πŸ“₯ Export")

        if st.button("πŸ“„ Generate supervision note", use_container_width=True):
            if not st.session_state.api_key:
                st.error("Please enter your API key first.")
            else:
                with st.spinner("Generating note with Claude…"):
                    note = generate_supervision_note(
                        st.session_state.api_key,
                        st.session_state.messages,
                    )
                    st.session_state["last_note"] = note

        if "last_note" in st.session_state:
            note = st.session_state["last_note"]
            st.text_area("Preview", note, height=200)

            if HAS_DOCX:
                docx_bytes = build_supervision_docx(note, st.session_state.nurse_name)
                if docx_bytes:
                    st.download_button(
                        "⬇️ Supervision note (.docx)",
                        data=docx_bytes,
                        file_name=f"PNA_Supervision_{date.today()}.docx",
                        mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                        use_container_width=True,
                    )

                cpd_bytes = build_cpd_docx(note, st.session_state.nurse_name, cpd_hours)
                if cpd_bytes:
                    st.download_button(
                        "⬇️ NMC CPD Record (.docx)",
                        data=cpd_bytes,
                        file_name=f"NMC_CPD_{date.today()}.docx",
                        mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                        use_container_width=True,
                    )
            else:
                st.download_button(
                    "⬇️ Download as .txt",
                    data=note,
                    file_name=f"PNA_Supervision_{date.today()}.txt",
                    mime="text/plain",
                    use_container_width=True,
                )

    st.divider()
    st.markdown(
        '<p class="ogl-notice">Contains public sector information licensed under the '
        '<a href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" '
        'target="_blank">Open Government Licence v3.0</a> β€” NHS England.</p>',
        unsafe_allow_html=True,
    )

# ─── Main content ─────────────────────────────────────────────────────────────

st.markdown("""
<div class="hero-header">
    <h1>πŸ‘¨πŸΎβ€βš•οΈ Professional Nurse Advocate Assistant</h1>
    <p>Your AI guide to the A-EQUIP model, restorative supervision, and quality improvement</p>
    <span class="pill">πŸ‡¬πŸ‡§ NHS England Β· A-EQUIP Model</span>
    <span class="pill">NMC Standards</span>
    <span class="pill">OGL v3.0</span>
</div>
""", unsafe_allow_html=True)

# ── Welcome / quick start ─────────────────────────────────────────────────────

if not st.session_state.messages:
    col1, col2, col3 = st.columns(3)
    with col1:
        st.markdown("""
**🌿 Restorative Supervision**
Start a reflective conversation to process the emotional impact of your clinical work.
""")
    with col2:
        st.markdown("""
**πŸ“š A-EQUIP Learning**
Ask about any of the four A-EQUIP functions or explore the PNA role in depth.
""")
    with col3:
        st.markdown("""
**βœ… PAQI Planning**
Get support developing your Personal Action for Quality Improvement.
""")

    st.markdown("#### πŸ’¬ Try asking…")
    examples = [
        "What is the A-EQUIP model and why does it matter for nurses?",
        "I've had a really difficult week on the ward. I don't know where to start.",
        "How do I facilitate a restorative supervision session for the first time?",
        "What does a PNA actually do day-to-day?",
        "Help me reflect on a challenging patient interaction using Gibbs cycle.",
        "What's the difference between the normative and formative functions?",
    ]
    cols = st.columns(2)
    for i, ex in enumerate(examples):
        with cols[i % 2]:
            if st.button(ex, key=f"ex_{i}", use_container_width=True):
                st.session_state.messages.append({"role": "user", "content": ex})
                st.rerun()

# ── Chat history ──────────────────────────────────────────────────────────────

for msg in st.session_state.messages:
    with st.chat_message(msg["role"], avatar="πŸ‘©β€βš•οΈ" if msg["role"] == "user" else "πŸ‘¨πŸΎβ€βš•οΈ"):
        st.markdown(msg["content"])

# ── Chat input ────────────────────────────────────────────────────────────────

prompt = st.chat_input(
    "Ask me about A-EQUIP, restorative supervision, or the PNA role…",
    disabled=not st.session_state.api_key,
)

if not st.session_state.api_key:
    st.info(
        "πŸ‘ˆ Enter your Anthropic API key in the sidebar to start chatting. "
        "[Get a free key β†’](https://console.anthropic.com)"
    )

if prompt and st.session_state.api_key:
    if st.session_state.session_started is None:
        st.session_state.session_started = date.today().isoformat()

    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user", avatar="πŸ‘©β€βš•οΈ"):
        st.markdown(prompt)

    context = kb.search(prompt) if kb.index is not None else ""

    with st.chat_message("assistant", avatar="πŸ‘¨πŸΎβ€βš•οΈ"):
        placeholder = st.empty()
        full_response = ""

        try:
            for chunk in stream_response(
                api_key=st.session_state.api_key,
                history=st.session_state.messages[:-1],
                user_message=prompt,
                context=context,
            ):
                full_response += chunk
                placeholder.markdown(full_response + "β–‹")

            placeholder.markdown(full_response)

        except Exception as e:
            err = str(e)
            if "authentication" in err.lower() or "api_key" in err.lower():
                full_response = "❌ Invalid API key. Please check your key in the sidebar."
            elif "rate" in err.lower():
                full_response = "⏳ Rate limit reached. Please wait a moment and try again."
            else:
                full_response = f"❌ An error occurred: {err}"
            placeholder.error(full_response)

    st.session_state.messages.append({"role": "assistant", "content": full_response})

# ─── Footer ───────────────────────────────────────────────────────────────────

st.divider()
st.markdown("""
<div class="disclaimer">
⚠️ <strong>Clinical Disclaimer:</strong> 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.
If you are experiencing a clinical emergency or safeguarding concern, follow your Trust's protocols immediately.
<br/><br/>
<span class="ogl-notice">
Contains public sector information licensed under the
<a href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" target="_blank">Open Government Licence v3.0</a>
β€” NHS England.
Built by <a href="https://nursingcitizendevelopment.com" target="_blank">Lincoln Gombedza</a> Β· CQAI Β·
<a href="https://github.com/Clinical-Quality-Artifical-Intelligence/Professional-Nurse-Advocate-Assistant" target="_blank">Open Source</a>
</span>
</div>
""", unsafe_allow_html=True)