Spaces:
Sleeping
Sleeping
Lincoln Gombedza
Simplify to BYOK-only for Hugging Face β remove Stripe/subscriptions
320ec62 unverified | """ | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |