File size: 27,965 Bytes
0158c20 5847257 0158c20 5847257 0158c20 5847257 0158c20 5847257 0158c20 5847257 0158c20 5847257 0158c20 cae2a28 0158c20 cae2a28 0158c20 | 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 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 | import gradio as gr
import tempfile
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from langgraph.graph import StateGraph, END
from typing import TypedDict, List, Dict, Any, Annotated
import json
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.prompts import ChatPromptTemplate
import html
import markdown
class EvaluationState(TypedDict):
raw_data: pd.DataFrame
processed_data: Dict[str, Any]
visualizations: List[Dict[str, Any]]
insights: str
report_html: str
# Node 1: Data Preprocessing
def preprocess_data(state: EvaluationState) -> EvaluationState:
"""Process the raw CSV/Excel data and extract key metrics."""
df = state["raw_data"]
# Basic cleaning
# Assuming columns are: subject, feedback_rating (1-5), instructor_rating (1-5)
# Convert ratings to numeric if they aren't already
if 'feedback_rating' in df.columns:
df['feedback_rating'] = pd.to_numeric(df['feedback_rating'], errors='coerce')
if 'instructor_rating' in df.columns:
df['instructor_rating'] = pd.to_numeric(df['instructor_rating'], errors='coerce')
# Calculate metrics
processed_data = {}
# For each subject, calculate distribution of feedback and instructor ratings
subjects = df['subject'].unique()
processed_data['subjects'] = []
for subject in subjects:
subject_data = df[df['subject'] == subject]
feedback_distribution = {}
instructor_distribution = {}
# Count ratings by level 1-5
for i in range(1, 6):
feedback_distribution[i] = int(subject_data['feedback_rating'].eq(i).sum())
instructor_distribution[i] = int(subject_data['instructor_rating'].eq(i).sum())
# Calculate averages
avg_feedback = subject_data['feedback_rating'].mean()
avg_instructor = subject_data['instructor_rating'].mean()
# Calculate correlation between feedback and instructor rating
correlation = subject_data['feedback_rating'].corr(subject_data['instructor_rating'])
processed_data['subjects'].append({
'name': subject,
'feedback_distribution': feedback_distribution,
'instructor_distribution': instructor_distribution,
'avg_feedback': avg_feedback,
'avg_instructor': avg_instructor,
'correlation': correlation if pd.notna(correlation) else 0.0, # Handle NaN correlation
'sample_size': len(subject_data)
})
# Overall metrics
processed_data['overall'] = {
'avg_feedback': df['feedback_rating'].mean(),
'avg_instructor': df['instructor_rating'].mean(),
'total_responses': len(df),
'correlation': df['feedback_rating'].corr(df['instructor_rating']) if pd.notna(df['feedback_rating'].corr(df['instructor_rating'])) else 0.0 # Handle NaN
}
state["processed_data"] = processed_data
return state
# Node 2: Create Visualizations
def create_visualizations(state: EvaluationState) -> EvaluationState:
"""Create plotly visualizations based on the processed data."""
processed_data = state["processed_data"]
visualizations = []
# 1. Overall distribution of feedback ratings
feedback_data = []
for subject_data in processed_data['subjects']:
for rating, count in subject_data['feedback_distribution'].items():
feedback_data.append({
'Subject': subject_data['name'],
'Rating': rating,
'Count': count,
'Type': 'Feedback'
})
if feedback_data: # Only create plot if data exists
feedback_df = pd.DataFrame(feedback_data)
fig_feedback = px.bar(
feedback_df,
x='Rating',
y='Count',
color='Subject',
title='Distribution of Feedback Ratings by Subject',
labels={'Rating': 'Rating (1-5)', 'Count': 'Number of Responses'},
barmode='group'
)
visualizations.append({
'title': 'feedback_distribution',
'fig': fig_feedback,
'description': 'Distribution of feedback ratings across different subjects'
})
# 2. Overall distribution of instructor ratings
instructor_data = []
for subject_data in processed_data['subjects']:
for rating, count in subject_data['instructor_distribution'].items():
instructor_data.append({
'Subject': subject_data['name'],
'Rating': rating,
'Count': count,
'Type': 'Instructor'
})
if instructor_data: # Only create plot if data exists
instructor_df = pd.DataFrame(instructor_data)
fig_instructor = px.bar(
instructor_df,
x='Rating',
y='Count',
color='Subject',
title='Distribution of Instructor Ratings by Subject',
labels={'Rating': 'Rating (1-5)', 'Count': 'Number of Responses'},
barmode='group'
)
visualizations.append({
'title': 'instructor_distribution',
'fig': fig_instructor,
'description': 'Distribution of instructor ratings across different subjects'
})
# 3. Radar chart comparing average ratings across subjects
categories = [subject['name'] for subject in processed_data['subjects']]
if categories: # Only create plot if categories exist
fig_radar = go.Figure()
fig_radar.add_trace(go.Scatterpolar(
r=[subject['avg_feedback'] for subject in processed_data['subjects']],
theta=categories,
fill='toself',
name='Avg. Feedback Rating'
))
fig_radar.add_trace(go.Scatterpolar(
r=[subject['avg_instructor'] for subject in processed_data['subjects']],
theta=categories,
fill='toself',
name='Avg. Instructor Rating'
))
fig_radar.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, 5]
)
),
title_text="Average Ratings Comparison by Subject" # Use title_text
)
visualizations.append({
'title': 'radar_comparison',
'fig': fig_radar,
'description': 'Comparison of average feedback and instructor ratings by subject'
})
# 4. Correlation scatter plot
correlation_data = []
for subject_data in processed_data['subjects']:
correlation_data.append({
'Subject': subject_data['name'],
'Correlation': subject_data['correlation'],
'Sample Size': subject_data['sample_size']
})
if correlation_data: # Only create plot if data exists
correlation_df = pd.DataFrame(correlation_data)
fig_corr = px.scatter(
correlation_df,
x='Subject',
y='Correlation',
size='Sample Size',
title='Correlation between Feedback and Instructor Ratings',
labels={'Correlation': 'Pearson Correlation Coefficient'},
color='Correlation',
color_continuous_scale=px.colors.diverging.RdBu,
color_continuous_midpoint=0,
# Ensure hover data is meaningful
hover_name='Subject',
hover_data={'Subject': False, 'Correlation': ':.2f', 'Sample Size': True}
)
visualizations.append({
'title': 'correlation_analysis',
'fig': fig_corr,
'description': 'Correlation between feedback and instructor ratings by subject'
})
state["visualizations"] = visualizations
return state
# Node 3: Generate Insights using LangChain + Google Generative AI
def generate_insights(state: EvaluationState) -> EvaluationState:
"""Generate narrative insights based on the data analysis using GenAI."""
processed_data = state["processed_data"]
# Ensure GOOGLE_API_KEY is set in your environment for this to work
# Example: export GOOGLE_API_KEY="your_actual_api_key"
# Or ensure os.environ["GOOGLE_API_KEY"] is set before calling the LLM
google_api_key = os.getenv("GOOGLE_API_KEY")
try:
if not google_api_key:
raise ValueError("GOOGLE_API_KEY not found in environment.")
# Prepare data for the LLM
subjects_info = []
for subject in processed_data['subjects']:
subject_info = f"Subject: {subject['name']}\n"
subject_info += f" Average Feedback Rating: {subject['avg_feedback']:.2f}/5\n"
subject_info += f" Average Instructor Rating: {subject['avg_instructor']:.2f}/5\n"
subject_info += f" Correlation between Feedback and Instructor Rating: {subject['correlation']:.2f}\n"
subject_info += f" Sample Size: {subject['sample_size']} responses\n"
subject_info += " Feedback Rating Distribution (Rating: Count): "
subject_info += ", ".join([f"{k}: {v}" for k, v in subject['feedback_distribution'].items()])
subject_info += "\n Instructor Rating Distribution (Rating: Count): "
subject_info += ", ".join([f"{k}: {v}" for k, v in subject['instructor_distribution'].items()])
subjects_info.append(subject_info)
overall_info = f"Overall Average Feedback Rating: {processed_data['overall']['avg_feedback']:.2f}/5\n"
overall_info += f"Overall Average Instructor Rating: {processed_data['overall']['avg_instructor']:.2f}/5\n"
overall_info += f"Overall Correlation: {processed_data['overall']['correlation']:.2f}\n"
overall_info += f"Total Responses: {processed_data['overall']['total_responses']}"
prompt = ChatPromptTemplate.from_messages([
("system", """You are an expert data analyst . Your task is to analyze employees evaluation data and provide comprehensive, actionable insights.
Format your response in Markdown.
Your analysis should cover:
1. **Executive Summary**: A brief overview of key findings.
2. **Overall Performance Patterns**: Discuss general trends, average ratings, and overall sentiment.
3. **Subject-Specific Analysis**:
* Highlight subjects with notably high or low ratings (both feedback and instructor).
* Discuss variations between subjects.
* Analyze the correlation between feedback and instructor ratings for each subject.
4. **Key Observations & Potential Issues**: Identify any outliers, significant discrepancies (e.g., large gap between feedback and instructor rating for a subject), or patterns that warrant attention.
5. **Actionable Recommendations**: Based on the data, provide specific, data-driven recommendations for improvement (e.g., for specific subjects, for instructor development, for curriculum adjustments).
Be specific, use the data provided, and ensure your insights are clear and well-structured.
"""),
("user", """Please analyze the following student evaluation data:
**Individual Subject Information:**
{subjects_info}
**Overall Statistics:**
{overall_info}
Provide your analysis in Markdown format.
""")
])
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash-latest", api_key=google_api_key) # Example model
chain = prompt | llm
response = chain.invoke({
"subjects_info": "\n\n".join(subjects_info),
"overall_info": overall_info
})
state["insights"] = response.content
except Exception as e:
import traceback
error_message = f"Error in generate_insights: {e}\n{traceback.format_exc()}"
print(error_message)
# Enhanced Fallback Insights (using Markdown)
fallback_insights = f"""
# AI-Generated Insights (Fallback)
Due to an issue connecting to the AI service, a basic analysis is provided below.
## Executive Summary
The overall average feedback rating is **{processed_data['overall']['avg_feedback']:.2f}/5**, and the overall average instructor rating is **{processed_data['overall']['avg_instructor']:.2f}/5**.
The correlation between feedback and instructor ratings across all subjects is **{processed_data['overall']['correlation']:.2f}**.
A total of **{processed_data['overall']['total_responses']}** responses were analyzed.
## Overall Performance Patterns
The data indicates a general performance level around the average ratings mentioned. Further subject-specific details are below.
## Subject-Specific Analysis
"""
for subject in processed_data['subjects']:
fallback_insights += f"""
### {subject['name']}
- **Average Feedback Rating**: {subject['avg_feedback']:.2f}/5
- **Average Instructor Rating**: {subject['avg_instructor']:.2f}/5
- **Correlation (Feedback vs. Instructor)**: {subject['correlation']:.2f}
- **Sample Size**: {subject['sample_size']} responses
- **Feedback Distribution** (Rating: Count): {', '.join([f'{k}: {v}' for k,v in subject['feedback_distribution'].items()])}
- **Instructor Distribution** (Rating: Count): {', '.join([f'{k}: {v}' for k,v in subject['instructor_distribution'].items()])}
"""
fallback_insights += """
## Recommendations (Generic)
1. **Investigate Low-Performing Subjects**: Focus on subjects with ratings significantly below average to identify areas for improvement.
2. **Analyze Rating Correlations**: Understand why correlations between student feedback and instructor ratings vary across subjects.
3. **Gather Qualitative Feedback**: For subjects with polarized or unexpectedly low ratings, consider gathering more detailed qualitative feedback.
4. **Share Best Practices**: Identify practices from highly-rated courses/instructors and explore ways to share them.
**Note**: This is a fallback analysis. For detailed, AI-powered insights, please ensure the Google Generative AI integration is correctly configured with a valid API key and model name.
"""
state["insights"] = fallback_insights
return state
# Node 4: Compile HTML Report
def compile_report(state: EvaluationState) -> EvaluationState:
"""Compile all analysis and visualizations into an HTML report."""
visualizations = state["visualizations"]
insights = state["insights"]
processed_data = state["processed_data"]
from datetime import datetime
date_generated = datetime.now().strftime("%B %d, %Y")
subject_rows = ""
for subject in processed_data['subjects']:
subject_rows += f"""
<tr>
<td>{html.escape(subject['name'])}</td>
<td>{subject['avg_feedback']:.2f}</td>
<td>{subject['avg_instructor']:.2f}</td>
<td>{subject['correlation']:.2f}</td>
<td>{subject['sample_size']}</td>
</tr>
"""
plotly_js_calls = []
visualizations_html_divs = [] # Renamed to avoid conflict
for i, viz_data in enumerate(visualizations):
div_id = f"viz-{i}"
visualizations_html_divs.append(f"""
<div class="visualization">
<h3>{html.escape(viz_data['description'])}</h3>
<div id="{div_id}" style="width: 100%; height: 500px; min-height: 400px;"></div>
</div>
""")
if viz_data['fig'] is not None:
try:
fig_json = viz_data['fig'].to_json()
plotly_js_calls.append(f"""
try {{
var figure_data_{i} = {fig_json};
Plotly.newPlot('{div_id}', figure_data_{i}.data, figure_data_{i}.layout, {{responsive: true}});
}} catch (plotError) {{
console.error('Error rendering plot {div_id}:', plotError);
document.getElementById('{div_id}').innerHTML = '<p style=\\"color:red; padding:10px;\\">Error rendering this chart: ' + plotError.message + '</p>';
}}
""")
except Exception as e:
print(f"Error converting figure {viz_data['title']} to JSON: {e}")
plotly_js_calls.append(f"""
document.getElementById('{div_id}').innerHTML = '<p style=\\"color:red; padding:10px;\\">Error preparing chart data for {html.escape(viz_data['description'])}.</p>';
""")
else:
plotly_js_calls.append(f"""
document.getElementById('{div_id}').innerHTML = '<p style=\\"color:orange; padding:10px;\\">No data to display for {html.escape(viz_data['description'])}.</p>';
""")
# Join all individual plot rendering calls
plotly_script_content = "\n".join(plotly_js_calls)
# Convert insights from Markdown to HTML
# The 'nl2br' extension converts single newlines to <br>, useful for LLM output.
# 'fenced_code' for code blocks, 'tables' for markdown tables.
insights_html_content = markdown.markdown(insights, extensions=['fenced_code', 'tables', 'nl2br', 'extra'])
report_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Akwel Evaluation Analysis Report</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f4f7f6; }}
h1, h2, h3 {{ color: #2c3e50; margin-top: 1.5em; margin-bottom: 0.5em; }}
h1 {{ font-size: 2.2em; border-bottom: 2px solid #3498db; padding-bottom: 0.3em; }}
h2 {{ font-size: 1.8em; border-bottom: 1px solid #b0bec5; padding-bottom: 0.2em; }}
h3 {{ font-size: 1.4em; }}
.visualization {{ margin: 30px 0; border: 1px solid #ddd; border-radius: 8px; padding: 20px; background-color: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }}
.stats-container {{ display: flex; flex-wrap: wrap; justify-content: space-around; margin-bottom: 30px; gap: 20px; }}
.stat-card {{ background-color: #ffffff; border-radius: 10px; padding: 25px; margin-bottom: 20px; flex: 1; min-width: 280px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); border-left: 5px solid #3498db; }}
.stat-card h3 {{ margin-top: 0; color: #3498db; }}
.stat-card p {{ font-size: 1.1em; }}
.insights {{ background-color: #eaf5ff; padding: 25px; border-radius: 10px; margin: 30px 0; border-left: 5px solid #1abc9c; }}
.insights h2 {{ color: #16a085; }}
.insights p, .insights li {{ font-size: 1.05em; }}
.insights ul, .insights ol {{ padding-left: 20px; }}
.insights strong {{ color: #2c3e50; }}
table {{ width: 100%; border-collapse: collapse; margin: 25px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }}
th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
th {{ background-color: #3498db; color: white; font-weight: bold; }}
tr:nth-child(even) {{ background-color: #f8f9fa; }}
tr:hover {{ background-color: #e9ecef; }}
footer {{ text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 0.9em; color: #777; }}
</style>
</head>
<body>
<h1>Employee Evaluation Analysis Report</h1>
<div class="stats-container">
<div class="stat-card">
<h3>Overall Feedback Rating</h3>
<p style="font-size: 28px; font-weight: bold;">{processed_data['overall']['avg_feedback']:.2f}/5</p>
<p>Based on {processed_data['overall']['total_responses']} total responses</p>
</div>
<div class="stat-card">
<h3>Overall Instructor Rating</h3>
<p style="font-size: 28px; font-weight: bold;">{processed_data['overall']['avg_instructor']:.2f}/5</p>
<p>Correlation with feedback: {processed_data['overall']['correlation']:.2f}</p>
</div>
</div>
<h2>Performance by Subject</h2>
<table>
<thead><tr>
<th>Subject</th>
<th>Avg. Feedback</th>
<th>Avg. Instructor</th>
<th>Correlation</th>
<th>Sample Size</th>
</tr></thead>
<tbody>
{subject_rows}
</tbody>
</table>
<h2>Visualizations</h2>
{''.join(visualizations_html_divs)}
<div class="insights">
<h2>AI-Generated Insights</h2>
{insights_html_content}
</div>
<footer>
<p>Report generated on {date_generated}</p>
</footer>
<script>
// Wait for the DOM to be fully loaded before trying to render plots
document.addEventListener('DOMContentLoaded', function() {{
if (typeof Plotly !== 'undefined') {{
console.log("Plotly object found. Attempting to render charts.");
// This is where the generated JS for all plots goes
{plotly_script_content}
console.log("Plotly chart rendering JS execution sequence initiated.");
}} else {{
console.error("Plotly.js is not loaded! Charts cannot be displayed.");
const vizDivs = document.querySelectorAll('.visualization div[id^="viz-"]');
vizDivs.forEach(div => {{
div.innerHTML = "<p style='color:red; padding:10px;'>Error: Chart library (Plotly.js) did not load. Charts cannot be displayed. Check internet connection or CDN link.</p>";
}});
}}
}});
</script>
</body>
</html>
"""
state["report_html"] = report_html
return state
# Define the LangGraph workflow
def create_evaluation_graph():
"""Create the LangGraph workflow for the evaluation analytics system."""
graph = StateGraph(EvaluationState)
graph.add_node("preprocess_data", preprocess_data)
graph.add_node("create_visualizations", create_visualizations)
graph.add_node("generate_insights", generate_insights)
graph.add_node("compile_report", compile_report)
graph.add_edge("preprocess_data", "create_visualizations")
graph.add_edge("create_visualizations", "generate_insights")
graph.add_edge("generate_insights", "compile_report")
graph.add_edge("compile_report", END)
graph.set_entry_point("preprocess_data")
return graph.compile()
evaluation_app = create_evaluation_graph()
def generate_report_gradio(file_obj):
if file_obj is None:
return "<p style='color:red; text-align:center; padding-top: 20px;'>Please upload a CSV or Excel file.</p>", None, gr.update(visible=False)
try:
file_path = file_obj.name
if file_path.lower().endswith('.csv'):
df = pd.read_csv(file_path)
elif file_path.lower().endswith(('.xls', '.xlsx')):
df = pd.read_excel(file_path)
else:
return "<p style='color:red; text-align:center; padding-top: 20px;'>Unsupported file type. Please upload a CSV or Excel file.</p>", None, gr.update(visible=False)
required_columns = ['subject', 'feedback_rating', 'instructor_rating']
if not all(col in df.columns for col in required_columns):
missing = [col for col in required_columns if col not in df.columns]
return f"<p style='color:red; text-align:center; padding-top: 20px;'>Missing required columns: {', '.join(missing)}.</p>", None, gr.update(visible=False)
except Exception as e:
return f"<p style='color:red; text-align:center; padding-top: 20px;'>Error reading file: {html.escape(str(e))}</p>", None, gr.update(visible=False)
initial_state = {"raw_data": df, "processed_data": {}, "visualizations": [], "insights": "", "report_html": ""}
try:
final_state = evaluation_app.invoke(initial_state)
report_html_content = final_state["report_html"]
except Exception as e:
import traceback
tb_str = traceback.format_exc()
error_msg = f"An error occurred during report generation: {html.escape(str(e))}"
print(f"{error_msg}\n{tb_str}")
return f"<p style='color:red; text-align:center; padding-top: 20px;'>{error_msg}</p>", None, gr.update(visible=False)
try:
# Use a more robust way to name the temporary file for download
base_name = os.path.splitext(os.path.basename(file_path))[0]
report_file_name = f"{base_name}_evaluation_report.html"
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html", prefix=f"{base_name}_report_", encoding="utf-8") as tmp_file:
tmp_file.write(report_html_content)
temp_file_path = tmp_file.name
return report_html_content, gr.update(value=temp_file_path, label=f"Download: {report_file_name}", visible=True), gr.update(visible=True)
except Exception as e:
return f"<p style='color:red; text-align:center; padding-top: 20px;'>Error creating download file: {html.escape(str(e))}</p>", None, gr.update(visible=False)
# --- GRADIO APP USING gr.Blocks ---
with gr.Blocks(
title="Evaluation Analytics System",
theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)
) as app:
gr.Markdown(
"""
# 📊 Formation Evaluation Analytics Dashboard
Upload evaluation data (CSV or Excel format) to generate an interactive report.
Required columns: `subject`, `feedback_rating` (1-5), `instructor_rating` (1-5).
"""
)
with gr.Row():
with gr.Column(scale=1, min_width=300): # Input column
file_input = gr.File(
label="Upload Evaluation Data",
file_types=['.csv', '.xls', '.xlsx'],
# type="filepath" # 'filepath' is often better for NamedTemporaryFile
)
generate_button = gr.Button("Generate Report", variant="primary")
# Placeholder for download link, initially hidden
download_file_output = gr.File(label="Download Full Report", visible=False, interactive=False)
with gr.Column(scale=3): # Output column - larger for report preview
report_output_html = gr.HTML(
label="Analysis Report Preview",
value="<p style='text-align:center; color:grey; padding-top:50px;'>Upload a file and click 'Generate Report' to see the analysis.</p>"
)
# Define interactions
generate_button.click(
fn=generate_report_gradio,
inputs=[file_input],
outputs=[report_output_html, download_file_output, download_file_output] # download_file_output is updated twice for value and visibility
)
app.launch(ssr_mode=False)
|