Spiritual_Health_Project / src /interface /chaplain_feedback_ui.py
DocUA's picture
feat: implement complete message review interface for Standard Verification
713cfc8
# chaplain_feedback_ui.py
"""
Gradio UI components for Chaplain Feedback & Tagging System.
Provides interface components for displaying classification flows,
collecting chaplain feedback, and displaying error patterns.
Requirements: 1.5, 2.3, 3.3, 4.1, 5.1, 5.3, 6.1, 6.3, 8.1, 8.2, 8.3, 10.1, 10.2, 10.3
"""
from __future__ import annotations
import gradio as gr
from typing import List, Dict, Tuple, Optional, Any
from dataclasses import dataclass
from src.core.chaplain_models import (
ClassificationFlowResult,
DistressIndicator,
FollowUpQuestion,
TaggingRecord,
CLASSIFICATION_SUBCATEGORIES,
QUESTION_ISSUE_TYPES,
REFERRAL_ISSUE_TYPES,
)
class ChaplainFeedbackUIComponents:
"""Manages Gradio UI components for chaplain feedback system."""
# Color mappings for classification badges
BADGE_COLORS = {
"red": "🔴",
"yellow": "🟡",
"green": "🟢",
}
BADGE_LABELS = {
"red": "RED - Severe Distress",
"yellow": "YELLOW - Potential Distress",
"green": "GREEN - No Distress",
}
# Severity color codes for indicators
SEVERITY_COLORS = {
"red": "#ea9999", # Red from definitions document
"yellow": "#ffe599", # Yellow from definitions document
}
@staticmethod
def create_classification_flow_display() -> Tuple[gr.Component, gr.Component, gr.Component, gr.Component]:
"""
Create ClassificationFlowDisplay component.
Displays RED/YELLOW/GREEN flow results with all generated content.
Returns:
Tuple of (classification_badge, explanation, content_section, indicators_section) components
Requirements: 1.5, 2.3, 3.3
"""
classification_badge = gr.Markdown(
value="🔄 Loading classification...",
label="Classification Result",
)
explanation = gr.Markdown(
value="",
label="Explanation",
)
content_section = gr.Markdown(
value="",
label="Generated Content",
)
indicators_section = gr.Markdown(
value="",
label="Detected Indicators",
)
return classification_badge, explanation, content_section, indicators_section
@staticmethod
def render_classification_flow(
flow_result: ClassificationFlowResult,
) -> Tuple[str, str, str, str]:
"""
Render complete classification flow result.
Args:
flow_result: ClassificationFlowResult with all flow data
Returns:
Tuple of (badge, explanation, content, indicators) markdown strings
"""
# Classification badge
badge_emoji = ChaplainFeedbackUIComponents.BADGE_COLORS.get(flow_result.classification, "❓")
badge_label = ChaplainFeedbackUIComponents.BADGE_LABELS.get(flow_result.classification, "UNKNOWN")
confidence_pct = int(round(flow_result.confidence * 100))
badge = f"## {badge_emoji} {badge_label}\n\n**Confidence:** {confidence_pct}%"
# Explanation
explanation = f"### Explanation\n\n{flow_result.explanation}"
# Generated content based on classification
content = ""
if flow_result.classification == "red":
content = ChaplainFeedbackUIComponents._render_red_flow_content(flow_result)
elif flow_result.classification == "yellow":
content = ChaplainFeedbackUIComponents._render_yellow_flow_content(flow_result)
elif flow_result.classification == "green":
content = ChaplainFeedbackUIComponents._render_green_flow_content(flow_result)
# Indicators
indicators = ChaplainFeedbackUIComponents._render_indicators(flow_result.indicators)
return badge, explanation, content, indicators
@staticmethod
def _render_red_flow_content(flow_result: ClassificationFlowResult) -> str:
"""Render RED flow content (permission check + referral message)."""
content = "### 🔴 RED FLAG - Severe Distress Detected\n\n"
if flow_result.permission_check_message:
content += "#### Patient Permission Check\n\n"
content += f"{flow_result.permission_check_message}\n\n"
if flow_result.consent_status:
content += f"**Consent Status:** {flow_result.consent_status}\n\n"
if flow_result.referral_message and flow_result.consent_status == "granted":
content += "#### Referral Message for Spiritual Care Team\n\n"
content += f"{flow_result.referral_message}\n\n"
elif flow_result.consent_status == "declined":
content += "**Status:** No further action - patient declined spiritual support referral\n\n"
return content
@staticmethod
def _render_yellow_flow_content(flow_result: ClassificationFlowResult) -> str:
"""Render YELLOW flow content (follow-up questions + re-evaluation)."""
content = "### 🟡 YELLOW FLAG - Potential Distress\n\n"
if flow_result.follow_up_questions:
content += "#### Follow-Up Questions\n\n"
for i, question in enumerate(flow_result.follow_up_questions, 1):
content += f"**Question {i}:** {question.question_text}\n\n"
content += f"*Purpose:* {question.purpose}\n\n"
if flow_result.patient_responses:
content += "#### Patient Responses\n\n"
for i, response in enumerate(flow_result.patient_responses, 1):
content += f"**Response {i}:** {response}\n\n"
if flow_result.re_evaluation_result:
content += f"#### Re-Evaluation Result\n\n"
if flow_result.re_evaluation_result == "red":
content += "🔴 **Escalated to RED** - Severe distress detected in responses\n\n"
elif flow_result.re_evaluation_result == "green":
content += "🟢 **Downgraded to GREEN** - No distress indicators in responses\n\n"
return content
@staticmethod
def _render_green_flow_content(flow_result: ClassificationFlowResult) -> str:
"""Render GREEN flow content (no distress)."""
content = "### 🟢 GREEN FLAG - No Distress Detected\n\n"
content += "**Status:** No further steps required\n\n"
content += "No spiritual distress indicators were detected in this message.\n\n"
return content
@staticmethod
def _render_indicators(indicators: List[DistressIndicator]) -> str:
"""Render detected indicators with categories and severity."""
if not indicators:
return "### Detected Indicators\n\nNo indicators detected"
content = "### Detected Indicators\n\n"
# Group by severity
red_indicators = [i for i in indicators if i.severity == "red"]
yellow_indicators = [i for i in indicators if i.severity == "yellow"]
if red_indicators:
content += "#### 🔴 RED Indicators (Severe)\n\n"
for indicator in red_indicators:
confidence_pct = int(round(indicator.confidence * 100))
content += f"• **{indicator.subcategory}** ({confidence_pct}% confidence)\n"
content += f" - Category: {indicator.category}\n"
content += f" - Reference: {indicator.definition_reference}\n\n"
if yellow_indicators:
content += "#### 🟡 YELLOW Indicators (Potential)\n\n"
for indicator in yellow_indicators:
confidence_pct = int(round(indicator.confidence * 100))
content += f"• **{indicator.subcategory}** ({confidence_pct}% confidence)\n"
content += f" - Category: {indicator.category}\n"
content += f" - Reference: {indicator.definition_reference}\n\n"
return content
@staticmethod
def create_tagging_interface() -> Tuple[gr.Component, gr.Component, gr.Component, gr.Component, gr.Component, gr.Component, gr.Component, gr.Component, gr.Component, gr.Component]:
"""
Create TaggingInterface component.
Provides classification subcategory selector, multi-select for issues,
and free-text comment fields.
Returns:
Tuple of individual tagging components for use in event handlers
Requirements: 4.1, 5.1, 5.3, 6.1, 6.3
"""
# Classification tagging components
is_correct = gr.Radio(
choices=[("✓ Correct", True), ("✗ Incorrect", False)],
label="Is the classification correct?",
interactive=True,
visible=False,
)
subcategory = gr.Dropdown(
choices=CLASSIFICATION_SUBCATEGORIES,
label="What type of error? (if incorrect)",
interactive=True,
visible=False,
)
correct_classification = gr.Radio(
choices=[
("🟢 GREEN - No Distress", "green"),
("🟡 YELLOW - Potential Distress", "yellow"),
("🔴 RED - Severe Distress", "red"),
],
label="What should the correct classification be?",
interactive=True,
visible=False,
)
# Follow-up question issues components
question_issues = gr.CheckboxGroup(
choices=QUESTION_ISSUE_TYPES,
label="Issues with follow-up questions (select all that apply)",
interactive=True,
visible=False,
)
question_comments = gr.Textbox(
label="Comments on questions",
placeholder="e.g., 'Too clinical', 'Not spiritually relevant'",
lines=2,
interactive=True,
visible=False,
)
# Referral message issues components
referral_issues = gr.CheckboxGroup(
choices=REFERRAL_ISSUE_TYPES,
label="Issues with referral message (select all that apply)",
interactive=True,
visible=False,
)
referral_comments = gr.Textbox(
label="Comments on referral message",
placeholder="e.g., 'Incomplete summary', 'Tone inappropriate'",
lines=2,
interactive=True,
visible=False,
)
# Indicator issues components
indicator_issues = gr.Textbox(
label="Incorrectly identified indicators",
placeholder="List indicator IDs or names that were incorrectly identified",
lines=2,
interactive=True,
visible=False,
)
indicator_comments = gr.Textbox(
label="Comments on indicators",
placeholder="e.g., 'Missed anxiety indicators', 'False positive on grief'",
lines=2,
interactive=True,
visible=False,
)
# General notes component
notes_section = gr.Textbox(
label="General Notes",
placeholder="Any additional feedback or observations",
lines=3,
interactive=True,
visible=False,
)
return is_correct, subcategory, correct_classification, question_issues, question_comments, referral_issues, referral_comments, indicator_issues, indicator_comments, notes_section
@staticmethod
def create_indicator_display() -> Tuple[gr.Component, gr.Component]:
"""
Create IndicatorDisplay component.
Shows indicators with categories and allows tagging incorrect indicators.
Returns:
Tuple of (indicators_display, indicator_tagging) components
Requirements: 8.1, 8.2, 8.3
"""
indicators_display = gr.Markdown(
value="No indicators to display",
label="Detected Indicators",
)
indicator_tagging = gr.Group(visible=False)
with indicator_tagging:
incorrect_indicators = gr.CheckboxGroup(
choices=[],
label="Select indicators that are incorrectly identified",
interactive=True,
)
indicator_notes = gr.Textbox(
label="Why are these indicators incorrect?",
placeholder="Explain why these indicators don't apply",
lines=2,
interactive=True,
)
return indicators_display, indicator_tagging
@staticmethod
def create_error_pattern_summary() -> Tuple[gr.Component, gr.Component, gr.Component]:
"""
Create ErrorPatternSummary component.
Displays error patterns grouped by type with frequent subcategories highlighted.
Returns:
Tuple of (error_patterns, subcategory_breakdown, recommendations) components
Requirements: 10.1, 10.2, 10.3
"""
error_patterns = gr.Markdown(
value="No error patterns yet",
label="Error Patterns",
)
subcategory_breakdown = gr.Markdown(
value="No data",
label="Subcategory Breakdown",
)
recommendations = gr.Markdown(
value="No recommendations yet",
label="Recommendations for Improvement",
)
return error_patterns, subcategory_breakdown, recommendations
@staticmethod
def render_error_patterns(
classification_errors: Dict[str, int],
question_errors: Dict[str, int],
referral_errors: Dict[str, int],
) -> Tuple[str, str, str]:
"""
Render error patterns summary.
Args:
classification_errors: Dict of classification error subcategories with counts
question_errors: Dict of question issue types with counts
referral_errors: Dict of referral issue types with counts
Returns:
Tuple of (patterns, breakdown, recommendations) markdown strings
"""
# Error patterns grouped by type
patterns = "### Error Patterns\n\n"
total_classification_errors = sum(classification_errors.values())
total_question_errors = sum(question_errors.values())
total_referral_errors = sum(referral_errors.values())
if total_classification_errors > 0:
patterns += f"#### Classification Errors: {total_classification_errors} total\n\n"
for subcategory, count in sorted(classification_errors.items(), key=lambda x: x[1], reverse=True):
patterns += f"• {subcategory}: {count}\n"
patterns += "\n"
if total_question_errors > 0:
patterns += f"#### Follow-Up Question Issues: {total_question_errors} total\n\n"
for issue_type, count in sorted(question_errors.items(), key=lambda x: x[1], reverse=True):
patterns += f"• {issue_type}: {count}\n"
patterns += "\n"
if total_referral_errors > 0:
patterns += f"#### Referral Message Issues: {total_referral_errors} total\n\n"
for issue_type, count in sorted(referral_errors.items(), key=lambda x: x[1], reverse=True):
patterns += f"• {issue_type}: {count}\n"
patterns += "\n"
# Subcategory breakdown
breakdown = "### Subcategory Breakdown\n\n"
if classification_errors:
breakdown += "**Classification Errors:**\n"
for subcategory, count in sorted(classification_errors.items(), key=lambda x: x[1], reverse=True):
breakdown += f"- {subcategory}: {count}\n"
breakdown += "\n"
if question_errors:
breakdown += "**Question Issues:**\n"
for issue_type, count in sorted(question_errors.items(), key=lambda x: x[1], reverse=True):
breakdown += f"- {issue_type}: {count}\n"
breakdown += "\n"
if referral_errors:
breakdown += "**Referral Issues:**\n"
for issue_type, count in sorted(referral_errors.items(), key=lambda x: x[1], reverse=True):
breakdown += f"- {issue_type}: {count}\n"
breakdown += "\n"
# Recommendations
recommendations = "### Recommendations for Improvement\n\n"
# Find most common errors
all_errors = {}
for subcategory, count in classification_errors.items():
all_errors[f"Classification: {subcategory}"] = count
for issue_type, count in question_errors.items():
all_errors[f"Questions: {issue_type}"] = count
for issue_type, count in referral_errors.items():
all_errors[f"Referral: {issue_type}"] = count
if all_errors:
sorted_errors = sorted(all_errors.items(), key=lambda x: x[1], reverse=True)
top_3 = sorted_errors[:3]
recommendations += "**Top areas for improvement:**\n\n"
for error_type, count in top_3:
recommendations += f"1. **{error_type}** ({count} occurrences)\n"
recommendations += f" - Review prompts and logic for this error type\n"
recommendations += f" - Consider additional training data\n\n"
else:
recommendations += "No errors detected yet. Great job!\n\n"
return patterns, breakdown, recommendations