File size: 24,085 Bytes
19a3093
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e0459c
19a3093
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
"""
NurseLex β€” Legal Literacy Agent for All Nurses and Nursing Students
Architecture:
  1. Local legislation.parquet β€” 219K health/social care Acts & SIs for browsing
  2. cached_legislation.py β€” 1,128 sections loaded from nursing_sections.json
  3. Gemini Flash REST API β€” Plain English explanations (with retry logic)
"""
import os
import asyncio
import httpx
import logging
import pandas as pd
import gradio as gr

from cached_legislation import search_cached
from local_search import search_scenarios_locally

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# --- Load local legislation index ---
PARQUET_PATH = os.path.join(os.path.dirname(__file__), "legislation.parquet")
try:
    LEG_DF = pd.read_parquet(PARQUET_PATH)
    logger.info(f"Loaded {len(LEG_DF)} legislation entries from parquet")
except Exception as e:
    logger.warning(f"Could not load parquet: {e}")
    LEG_DF = pd.DataFrame()

# --- Key nursing legislation IDs ---
NURSING_ACTS = {
    "Mental Health Act 1983": "ukpga/1983/20",
    "Mental Capacity Act 2005": "ukpga/2005/9",
    "Care Act 2014": "ukpga/2014/23",
    "Human Rights Act 1998": "ukpga/1998/42",
    "Equality Act 2010": "ukpga/2010/15",
    "Health and Social Care Act 2012": "ukpga/2012/7",
    "Mental Health Units (Use of Force) Act 2018": "ukpga/2018/27",
    "Autism Act 2009": "ukpga/2009/15",
    "Children Act 1989": "ukpga/1989/41",
    "Children Act 2004": "ukpga/2004/31",
    "Safeguarding Vulnerable Groups Act 2006": "ukpga/2006/47",
    "Health and Care Act 2022": "ukpga/2022/31",
}
REVERSE_ACTS = {v: k for k, v in NURSING_ACTS.items()}

# --- Gemini REST API ---
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
GEMINI_MODELS = ["gemini-2.0-flash-lite", "gemini-2.0-flash"]

SYSTEM_PROMPT = """You are NurseLex, a legal literacy assistant for all UK nurses and nursing students.

Your role:
1. Answer legal questions using ONLY the legislation text provided in the context.
2. Explain the law in clear, plain English suitable for all nurses and nursing students.
3. Always cite the specific Act, section number, and year.
4. If the context doesn't contain enough information, say so clearly.
5. Add practical nursing implications (e.g., "In practice, this means...").
6. Include professional reminders (e.g., NMC Code, duty of care).

Disclaimers to include:
- "This is for educational purposes only β€” always consult your trust's legal team for specific cases."
- "This reflects the legislation as written β€” local trust policies may add additional requirements."

Format with clear headings, bullet points, and bold key terms."""

QUICK_QUESTIONS = [
    "What is Section 5(4) of the Mental Health Act and when can a nurse use it?",
    "What does the Mental Capacity Act say about best interests decisions?",
    "When can a patient be detained under Section 2 vs Section 3?",
    "What are the legal requirements for using restraint?",
    "What does Section 117 aftercare mean and who is entitled?",
    "What are a nurse's legal duties under the Care Act 2014 for safeguarding?",
    "What is Deprivation of Liberty and when do DoLS apply?",
    "What rights does a patient have under Section 136?",
]

async def call_gemini(prompt: str) -> str:
    """Call Gemini via REST API with retry logic and model fallback."""
    if not GEMINI_API_KEY:
        return ""

    payload = {
        "system_instruction": {"parts": [{"text": SYSTEM_PROMPT}]},
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {"temperature": 0.3, "maxOutputTokens": 2048},
    }

    async with httpx.AsyncClient(timeout=60.0) as client:
        for model in GEMINI_MODELS:
            url = f"{GEMINI_BASE}/{model}:generateContent?key={GEMINI_API_KEY}"
            for attempt in range(3):
                try:
                    resp = await client.post(url, json=payload)
                    if resp.status_code == 429:
                        wait = 2 ** (attempt + 1)
                        logger.warning(f"Rate limited ({model}), retrying in {wait}s")
                        await asyncio.sleep(wait)
                        continue
                    resp.raise_for_status()
                    data = resp.json()
                    return data["candidates"][0]["content"]["parts"][0]["text"]
                except httpx.HTTPStatusError as e:
                    if e.response.status_code == 429:
                        wait = 2 ** (attempt + 1)
                        logger.warning(f"Rate limited ({model}), retrying in {wait}s")
                        await asyncio.sleep(wait)
                        continue
                    logger.error(f"Gemini API error ({model}): {e.response.status_code}")
                    break
                except Exception as e:
                    logger.error(f"Gemini error ({model}): {type(e).__name__}")
                    break
            logger.info(f"Model {model} exhausted, trying next...")

    logger.error("All Gemini models failed")
    return ""

def search_legislation_index(query: str, max_results: int = 10) -> pd.DataFrame:
    """Search the full legislation index parquet by title."""
    if LEG_DF.empty:
        return pd.DataFrame()

    mask = LEG_DF["title"].str.contains(query, case=False, na=False)
    results = LEG_DF[mask].sort_values("year", ascending=False).head(max_results)
    return results

async def query_and_respond(user_question: str, history: list) -> str:
    """Main RAG pipeline: local cached sections (1,128) + Gemini explanation."""
    if not user_question.strip():
        return "Please enter a question about UK healthcare legislation."

    # Step 1: Search local legislation sections
    sections = search_cached(user_question, max_results=5)
    logger.info(f"Local search returned {len(sections)} sections for: {user_question[:60]}")

    # Step 2: Search parquet index for related Acts
    related_acts = search_legislation_index(user_question, max_results=5)

    # Step 3: Build context
    context_parts = []
    for section in sections:
        title = section.get("title", "Untitled")
        text = section.get("text", "")
        leg_id = section.get("legislation_id", "")
        num = section.get("number", "")
        context_parts.append(f"### {title}\n**Source:** {leg_id}, Section {num}\n\n{text}\n")

    context = "\n---\n".join(context_parts) if context_parts else "No matching legislation sections found in cache."

    # Step 4: Generate Gemini response
    prompt = f"## Nurse's Question\n{user_question}\n\n## Relevant UK Legislation\n{context}\n\nPlease answer the nurse's question using the legislation above."
    
    answer = await call_gemini(prompt)

    if not answer:
        # Fallback: show raw legislation if Gemini fails or is missing key
        answer = _build_fallback(user_question, sections)
        if not GEMINI_API_KEY:
            answer += "\n\n⚠️ *Set `GEMINI_API_KEY` in Space secrets for AI-powered plain English explanations.*"
        elif "rate limit" in answer.lower():
             answer += "\n\n⚠️ *Gemini is currently rate limited, falling back to raw legislation.*"

    # Add source citations
    source_acts = set()
    for s in sections:
        leg_id = s.get("legislation_id", "")
        if leg_id:
            source_acts.add(leg_id)

    if source_acts:
        answer += "\n\n---\nπŸ“š **Sources:** "
        answer += " | ".join(f"[{sid}](https://www.legislation.gov.uk/id/{sid})" for sid in sorted(source_acts))

    # Add related Acts from parquet
    if not related_acts.empty:
        answer += "\n\nπŸ“– **Related legislation:** "
        act_links = []
        for _, row in related_acts.head(3).iterrows():
            uri = row.get("uri", "")
            title = row.get("title", "")
            if uri and title:
                act_links.append(f"[{title}]({uri})")
        if act_links:
            answer += " | ".join(act_links)

    answer += "\n\nπŸ›οΈ *Data from [legislation.gov.uk](https://www.legislation.gov.uk/) β€” Crown Copyright, OGL v3.0*"
    return answer

def _build_fallback(question: str, sections: list) -> str:
    """Show raw legislation without LLM."""
    response = f"## Legislation relevant to: *{question}*\n\n"

    if not sections:
        response += (
            "No matching sections found in cache. Try searching the full **Browse Legislation** tab for the Act title, or try specific terms like:\n"
            "- **\"Section 5(4)\"** or **\"nurse holding power\"**\n"
            "- **\"best interests\"** or **\"capacity\"**\n"
            "- **\"safeguarding\"** or **\"Section 42\"**\n"
            "- **\"Section 136\"** or **\"place of safety\"**\n"
        )
        return response

    for i, section in enumerate(sections[:5], 1):
        title = section.get("title", "Untitled")
        text = section.get("text", "No text available")
        leg_id = section.get("legislation_id", "")
        num = section.get("number", "")
        uri = section.get("uri", "")

        response += f"### {i}. {title}\n"
        response += f"**Act:** `{leg_id}` | **Section:** {num}\n\n"
        response += f"{text}\n\n"
        if uri:
            response += f"πŸ”— [View on legislation.gov.uk]({uri})\n\n"
        response += "---\n\n"

    return response

async def section_lookup(act_name: str, section_input: str) -> str:
    """Look up sections from cached legislation."""
    legislation_id = NURSING_ACTS.get(act_name)
    if not legislation_id:
        return f"❌ Act not found in NurseLex."

    cache_query = f"{act_name} section {section_input}" if section_input.strip() else act_name
    sections = search_cached(cache_query, max_results=10)
    sections = [s for s in sections if s.get("legislation_id") == legislation_id]

    if section_input.strip() and sections:
        try:
            target_num = int(section_input.strip().replace("Section ", "").replace("s.", "").replace("S.", ""))
            matching = [s for s in sections if s.get("number") == target_num]
            if matching:
                sections = matching
        except ValueError:
            pass

    if not sections:
        return (
            f"⏳ Section not found in cache for **{act_name}**.\n\n"
            f"Try the **Chat tab** for a broader search, or visit "
            f"[legislation.gov.uk](https://www.legislation.gov.uk/id/{legislation_id}) directly."
        )

    result = f"## {act_name}\n\n"
    for section in sections[:5]:
        title = section.get("title", "Untitled")
        text = section.get("text", "No text")
        num = section.get("number", "")
        uri = section.get("uri", "")

        result += f"### Section {num}: {title}\n\n{text}\n\n"
        if uri:
            result += f"πŸ”— [View on legislation.gov.uk]({uri})\n\n"
        result += "---\n\n"

    result += "\nπŸ›οΈ *Crown Copyright, OGL v3.0*"
    return result

async def fetch_explanatory_note(act_name: str, section_input: str) -> str:
    """Dynamically fetch Explanatory Notes from the i.AI Lex API."""
    if not section_input.strip():
        return "Please specify a section number to view its Explanatory Note."
        
    try:
        # Extract the digits
        section_number = "".join([c for c in section_input if c.isdigit()])
        if not section_number:
            return "Please enter a valid section number."
            
        url = 'https://lex.lab.i.ai.gov.uk/explanatory_note/section/search'
        payload = {
            'query': f'"{act_name}" Section {section_number}',
            'limit': 5
        }
        
        async with httpx.AsyncClient() as client:
            r = await client.post(url, json=payload, timeout=10.0)
            if r.status_code == 200:
                data = r.json()
                if isinstance(data, list):
                    parent_id = NURSING_ACTS.get(act_name, "")
                    for note in data:
                        if parent_id and parent_id in note.get('legislation_id', ''):
                            text = note.get('text', '')
                            if text:
                                return f"### Official Explanatory Note\n\n{text}\n\n*Source: i.AI Lex API*"
                                
        return f"No official Explanatory Note found for {act_name} Section {section_number}.\n\n*(Note: Acts passed prior to 1999 generally do not have Explanatory Notes).*."
    except httpx.TimeoutException:
        return "⏳ API Timeout while fetching Explanatory Note."
    except Exception as e:
        return f"Error fetching note: {str(e)}"

async def scenario_search(scenario_text: str) -> str:
    """Use local i-dot-ai vector search to map a clinical scenario to legal sections."""
    if not scenario_text.strip():
        return "Please describe a clinical scenario."
        
    try:
        results = search_scenarios_locally(scenario_text, top_k=5)
        
        if not results:
            return "No matching legislation found for this scenario in the local cache."
            
        result = f"## βš–οΈ Probable Legislation Matches for:\n*{scenario_text}*\n\n"
        
        for i, n in enumerate(results, 1):
            leg_id = n.get("legislation_id", "")
            
            # 1. Use the act_name from known mapping
            act_name = ""
            for known_id, known_name in REVERSE_ACTS.items():
                if known_id in leg_id:
                    act_name = known_name
                    break
            
            # 2. Final fallback: extract from the legislation_id URL
            if not act_name:
                act_name = leg_id.split("/id/")[-1] if "/id/" in leg_id else leg_id or "Legislation"
                    
            sec_num = n.get("number", "??")
            title = n.get("title", "Untitled Section")
            text = n.get("text", "")
            uri = n.get("uri", f"https://www.legislation.gov.uk/id/{leg_id}/section/{sec_num}")
            score = n.get("score", 0.0)
            
            result += f"### {i}. {act_name} β€” Section {sec_num}: {title} (Match Score: {score:.2f})\n"
            result += f"{text[:800]}...\n\n"
            result += f"πŸ”— [Read full text on legislation.gov.uk]({uri})\n\n---\n\n"
            
        return result
    except Exception as e:
        return f"Error during local scenario search: {str(e)}"

def browse_legislation(search_term: str, act_type: str) -> str:
    """Browse the legislation index from the parquet file."""
    if LEG_DF.empty:
        return "⚠️ Legislation index not loaded."

    filtered = LEG_DF.copy()

    if act_type != "All":
        type_map = {"Primary Acts": "ukpga", "Statutory Instruments": "uksi", "Scottish SIs": "ssi", "NI SRs": "nisr", "Welsh SIs": "wsi"}
        if act_type in type_map:
            filtered = filtered[filtered["type"] == type_map[act_type]]

    if search_term.strip():
        filtered = filtered[filtered["title"].str.contains(search_term, case=False, na=False)]

    filtered = filtered.sort_values("year", ascending=False).head(50)

    if filtered.empty:
        return f"No legislation found matching '{search_term}'."

    result = f"## πŸ“– Legislation Index ({len(filtered)} results)\n\n| Year | Title | Type |\n|---|---|---|\n"

    for _, row in filtered.iterrows():
        year = row.get("year", "β€”")
        title = row.get("title", "Untitled")
        uri = row.get("uri", "")
        leg_type = row.get("type", "")
        title_link = f"[{title}]({uri})" if uri else title
        result += f"| {year} | {title_link} | {leg_type} |\n"

    result += f"\n\n*Showing top 50 of {len(LEG_DF)} health & social care entries β€” {len(LEG_DF[LEG_DF['type']=='ukpga'])} Primary Acts*"
    result += "\n\nπŸ›οΈ *Data from i.AI Lex bulk downloads β€” Crown Copyright, OGL v3.0*"
    return result

# --- Gradio UI ---
THEME = gr.themes.Soft(
    primary_hue="indigo",
    secondary_hue="violet",
    neutral_hue="slate",
    font=gr.themes.GoogleFont("Inter"),
)

CSS = """
.gradio-container { max-width: 960px !important; }
.header-banner {
    background: linear-gradient(135deg, #312e81 0%, #4338ca 50%, #6366f1 100%);
    border-radius: 16px;
    padding: 28px 32px;
    margin-bottom: 16px;
    color: white;
}
.header-banner h1 { color: white; font-size: 2em; margin: 0 0 8px 0; }
.header-banner p { color: #c7d2fe; margin: 0; font-size: 1.05em; }
.disclaimer-box {
    background: #fef3c7;
    border-left: 4px solid #f59e0b;
    border-radius: 8px;
    padding: 12px 16px;
    margin-bottom: 12px;
    font-size: 0.9em;
    color: #92400e;
}
footer { display: none !important; }
"""

with gr.Blocks(theme=THEME, css=CSS, title="NurseLex β€” UK Law for All Nurses") as app:
    gr.HTML("""
    <div class="header-banner">
        <h1>πŸ›οΈ NurseLex</h1>
        <p>Legal literacy for all nurses and nursing students β€” powered by UK Government legislation data</p>
    </div>
    """)

    gr.HTML("""
    <div class="disclaimer-box">
        ⚠️ <strong>Educational tool only.</strong> NurseLex provides legislation text and AI-generated explanations for learning purposes.
        It does not constitute legal advice. Always consult your trust's legal/governance team for specific cases.
    </div>
    """)

    with gr.Tabs():
        # --- Tab 1: Chat ---
        with gr.TabItem("πŸ’¬ Ask a Legal Question", id="chat"):
            gr.Markdown("Ask about UK healthcare legislation β€” answers are grounded in **real statutory text**. (Cache: 1,128 Sections + 219K Acts)")

            chatbot = gr.Chatbot(
                label="NurseLex",
                height=480,
                type="messages",
                show_copy_button=True,
                avatar_images=(None, "https://em-content.zobj.net/source/twitter/376/classical-building_1f3db-fe0f.png"),
            )
            msg = gr.Textbox(
                label="Your question",
                placeholder="e.g., What is Section 5(4) of the Mental Health Act and when can a nurse use it?",
                lines=2,
            )
            with gr.Row():
                submit_btn = gr.Button("πŸ” Search Legislation", variant="primary", scale=2)
                clear_btn = gr.ClearButton([msg, chatbot], value="πŸ—‘οΈ Clear", scale=1)

            gr.Markdown("### πŸ’‘ Quick Questions")
            with gr.Row(equal_height=True):
                for i in range(0, 4):
                    gr.Button(
                        QUICK_QUESTIONS[i][:55] + "..." if len(QUICK_QUESTIONS[i]) > 55 else QUICK_QUESTIONS[i],
                        size="sm",
                        variant="secondary",
                    ).click(fn=lambda q=QUICK_QUESTIONS[i]: q, outputs=msg)
            with gr.Row(equal_height=True):
                for i in range(4, 8):
                    gr.Button(
                        QUICK_QUESTIONS[i][:55] + "..." if len(QUICK_QUESTIONS[i]) > 55 else QUICK_QUESTIONS[i],
                        size="sm",
                        variant="secondary",
                    ).click(fn=lambda q=QUICK_QUESTIONS[i]: q, outputs=msg)

            async def respond(message, history):
                history = history or []
                history.append({"role": "user", "content": message})
                answer = await query_and_respond(message, history)
                history.append({"role": "assistant", "content": answer})
                return "", history

            submit_btn.click(respond, [msg, chatbot], [msg, chatbot])
            msg.submit(respond, [msg, chatbot], [msg, chatbot])

        # --- Tab 2: Section Lookup ---
        with gr.TabItem("πŸ“– Section Lookup", id="lookup"):
            gr.Markdown("Look up a **specific section** of key nursing Acts. Includes **Official Explanatory Notes** where available.")

            with gr.Row():
                act_dropdown = gr.Dropdown(
                    choices=list(NURSING_ACTS.keys()),
                    label="Select Act",
                    value="Mental Health Act 1983",
                )
                section_input_box = gr.Textbox(
                    label="Section number",
                    placeholder="e.g., 5 or 117 or 136",
                )

            lookup_btn = gr.Button("πŸ” Look Up Law & Notes", variant="primary")
            
            with gr.Row():
                lookup_output = gr.Markdown(label="Statutory Text")
                note_output = gr.Markdown(label="Official Explanatory Note")

            lookup_btn.click(section_lookup, [act_dropdown, section_input_box], lookup_output)
            lookup_btn.click(fetch_explanatory_note, [act_dropdown, section_input_box], note_output)

        # --- Tab 3: Scenario Matcher ---
        with gr.TabItem("🧠 Scenario Matcher", id="scenario"):
            gr.Markdown("Describe a clinical scenario in plain English, and the **Lex Vector Search Engine** will map it to the most relevant UK laws.")
            
            with gr.Row():
                scenario_input = gr.Textbox(
                    label="Clinical Scenario",
                    placeholder="e.g. 'Patient wants to leave the ward but lacks capacity' or 'Doctor orders restraint without DoLS'",
                    lines=3
                )
            
            scenario_btn = gr.Button("πŸ€– Find Relevant Law", variant="primary")
            scenario_output = gr.Markdown(label="Semantic Search Results")
            
            scenario_btn.click(scenario_search, [scenario_input], scenario_output)

        # --- Tab 4: Browse Legislation ---
        with gr.TabItem("πŸ“š Browse Legislation", id="browse"):
            gr.Markdown(f"Browse **219,678** health & social care Acts and Statutory Instruments from the i.AI Lex dataset.")

            with gr.Row():
                browse_search = gr.Textbox(
                    label="Search legislation titles",
                    placeholder="e.g., mental health, safeguarding, disability",
                )
                browse_type = gr.Dropdown(
                    choices=["All", "Primary Acts", "Statutory Instruments", "Scottish SIs", "NI SRs", "Welsh SIs"],
                    label="Type",
                    value="All",
                )

            browse_btn = gr.Button("πŸ” Search", variant="primary")
            browse_output = gr.Markdown(label="Results")

            browse_btn.click(browse_legislation, [browse_search, browse_type], browse_output)

        # --- Tab 4: About ---
        with gr.TabItem("ℹ️ About", id="about"):
            gr.Markdown(f"""
## About NurseLex

**NurseLex** is a universal legal literacy tool for **all nurses and nursing students**.

### How It Works

1. **You ask a question** about UK healthcare law
2. **Cached legislation** provides the actual statutory text instantly
3. **Gemini Flash** explains it in plain English with practical nursing implications
4. **Every answer cites** the specific Act, section, and year

### Data

- **219,678 legislation entries** from the [i.AI Lex](https://lex.lab.i.ai.gov.uk/) bulk dataset
- **1,128 key sections** pre-cached with full text (MHA 1983, MCA 2005, Care Act 2014)
- **Crown Copyright** β€” Open Government Licence v3.0

### Key Acts Covered

| Act | Key Sections | Nursing Relevance |
|---|---|---|
| Mental Health Act 1983 | S.2, S.3, S.4, S.5(2), S.5(4), S.17, S.117, S.135, S.136 | Detention, holding powers, leave, aftercare |
| Mental Capacity Act 2005 | S.1 (Principles), S.2-3 (Capacity), S.4 (Best Interests), S.5 | Capacity assessments, best interests, DoLS |
| Care Act 2014 | S.42 (Safeguarding), S.67 (Advocacy) | Safeguarding adults, independent advocates |

### Built By

**NurseCitizenDeveloper** β€” NHS Registered Nurse building AI tools for nursing education.

πŸ€— [Hugging Face](https://huggingface.co/NurseCitizenDeveloper) Β· πŸ™ [GitHub](https://github.com/Clinical-Quality-Intelligence)
            """)

app.queue()

if __name__ == "__main__":
    app.launch(server_name="0.0.0.0", server_port=7860)