dalinstone commited on
Commit
a55b667
·
verified ·
1 Parent(s): bb761ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -221
app.py CHANGED
@@ -1,5 +1,4 @@
1
  import gradio as gr
2
- import docx
3
  import asyncio
4
  from concurrent.futures import ThreadPoolExecutor
5
  from google import generativeai as genai
@@ -9,7 +8,7 @@ from typing import List, Dict, Optional
9
  import os
10
  from pathlib import Path
11
  import time
12
- import fitz
13
 
14
  GRADING_RUBRIC = """
15
  GRADING RUBRIC (Total 100 points)
@@ -82,49 +81,26 @@ Professor's Summary:
82
 
83
  """
84
 
 
85
  BASE_PROMPT_TEMPLATE = """
86
  You are about to assume a role. Carefully review the persona, context, task, and output requirements before proceeding.
87
 
88
  1. PERSONA
89
-
90
  You are Dr. Stone, a meticulous and experienced Associate Professor of Nursing at a major university. You have been teaching for over 20 years, with a specialization in medical-surgical nursing. You are known for your high standards, particularly regarding academic integrity and the strict application of the American Psychological Association (APA) 7th Edition formatting guidelines. Your feedback is always direct, precise, and aimed at preparing students for the rigorous documentation standards required in the healthcare profession. Your tone is professional, authoritative, and educational. You do not offer praise for meeting baseline expectations; you simply state that the requirements have been met. Your criticism is specific, referencing the exact rule or rubric criterion that was violated.
91
 
92
  2. CONTEXT
93
-
94
- You are grading an essay for your undergraduate course, NURS 310: Fundamentals of Nursing. The assignment is an essay on the topic of "Skin Lesions." Students were provided with a detailed grading rubric, which you must follow perfectly and without exception. The total possible score for this assignment is 100 points.
95
 
96
  3. PRIMARY TASK
97
-
98
- Your task is to receive a student's essay submission and grade it with absolute precision according to the provided rubric. You must function as a perfect and unflinchingly accurate grader. There is no room for subjective interpretation or leniency. You will identify every error in content, formatting, spelling, grammar, and citation, and assign points strictly based on the rubric's quantitative thresholds.
99
 
100
  Your output should be a single JSON format structure response as indicated by the example below.
101
 
102
  **DO NOT** provide any introductory text, conversational pleasantries, or explanations outside of the requested JSON structure. Your entire response must be a single, valid JSON object.
103
 
104
- **Use the following rubric to grade the essay:**
105
  {GRADING_RUBRIC}
106
 
107
- **For your reference, here is an example of a perfectly formatted paper. Use it as a guide for what correct formatting looks like:**
108
- ---
109
- **EXAMPLE PAPER START**
110
- {EXAMPLE_PAPER}
111
- **EXAMPLE PAPER END**
112
- ---
113
-
114
- **Instructions:**
115
- 1. Read the entire essay provided below.
116
- 2. Assess the essay against each category in the rubric.
117
- 3. Calculate the total points lost and the final grade out of 100.
118
- 4. Provide brief, specific comments explaining why points were deducted in each category.
119
- 5. Write a 2-3 sentence summary of the overall grade.
120
- 6. Format your entire output as a single JSON object with the following keys and value types:
121
- - `finalGrade`: (Integer) The final score from 0-100.
122
- - `pointDeductions`: (Object) An object where keys are the main rubric categories ("Content & Analysis", "Organization & Structure", "APA Formatting & Citations", "Clarity & Mechanics") and values are the integer number of points lost for that category.
123
- - `feedback`: (Object) An object with the same keys as `pointDeductions`, where values are brief string comments explaining the point deductions for that category. If no points are lost, the comment should be "No points deducted."
124
- - `summary`: (String) A 2-3 sentence summary of the paper's performance and the rationale for the grade.
125
-
126
- Do not deviate from this format.
127
-
128
  **Example of the required JSON output format:**
129
  {{
130
  "finalGrade": 88,
@@ -142,13 +118,8 @@ Do not deviate from this format.
142
  }},
143
  "summary": "This is a strong paper with excellent critical analysis. The final grade was primarily impacted by significant APA formatting errors, which should be the main focus for improvement."
144
  }}
145
-
146
- ---
147
- **ESSAY TO GRADE:**
148
-
149
  """
150
 
151
-
152
  @dataclass
153
  class GradingResult:
154
  """Holds the structured result of a single graded essay."""
@@ -160,94 +131,43 @@ class GradingResult:
160
  summary: Optional[str] = None
161
  error_message: Optional[str] = None
162
 
163
-
164
  # --- Core Logic Classes ---
165
-
166
- class EssayParser:
167
- """Parses text content from a .docx file."""
168
- @staticmethod
169
- def parse_docx(file_path: str) -> str:
170
- """Extracts all text from a Word document."""
171
- try:
172
- doc = docx.Document(file_path)
173
- return "\n".join([para.text for para in doc.paragraphs if para.text])
174
- except Exception as e:
175
- # Handles cases where the file is corrupted or not a valid docx
176
- raise IOError(
177
- f"Could not read file: {os.path.basename(file_path)}. Error: {e}")
178
-
179
- @staticmethod
180
- def parse_pdf(file_path: str) -> str:
181
- """Extracts all text from a PDF document."""
182
- try:
183
- doc = fitz.open(file_path)
184
- text = ""
185
- for page in doc:
186
- text += page.get_text()
187
- return text
188
- except Exception as e:
189
- raise IOError(
190
- f"Could not read PDF file: {os.path.basename(file_path)}. Error: {e}")
191
-
192
-
193
 
194
  class GeminiGrader:
195
  """Manages interaction with the Google Gemini API for grading."""
196
-
197
  def __init__(self, api_key: str):
198
  """Initializes the Gemini model."""
199
  try:
200
  genai.configure(api_key=api_key)
201
- # Configuration for safer, more deterministic output
202
- generation_config = {
203
- "temperature": 0.1,
204
- "top_p": 0.95,
205
- "top_k": 40,
206
- }
207
- # Safety settings to prevent the model from refusing to grade
208
- safety_settings = [
209
- {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
210
- {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
211
- {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
212
- "threshold": "BLOCK_NONE"},
213
- {"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
214
- "threshold": "BLOCK_NONE"},
215
- ]
216
- self.model = genai.GenerativeModel(
217
- model_name="gemini-1.5-pro-latest",
218
- generation_config=generation_config,
219
- safety_settings=safety_settings
220
- )
221
  except Exception as e:
222
  raise ValueError(f"Failed to configure Gemini API: {e}")
223
 
224
- def grade_essay(self, essay_text: str, file_name: str, example_paper_text: str) -> GradingResult:
225
  """
226
- Sends the essay to Gemini for grading and parses the JSON response.
227
- This is a synchronous method designed to be run in a thread pool.
228
  """
229
- # Inject the rubric and the example paper text into the main prompt template
230
- final_prompt = BASE_PROMPT_TEMPLATE.format(
231
- GRADING_RUBRIC=GRADING_RUBRIC,
232
- EXAMPLE_PAPER=example_paper_text
233
- )
 
 
234
 
235
- prompt_with_essay = f"{final_prompt}\n{essay_text}"
236
  try:
237
- response = self.model.generate_content(prompt_with_essay)
238
- # Clean the response to ensure it's valid JSON
239
  cleaned_response = response.text.strip().replace("```json", "").replace("```", "")
240
  data = json.loads(cleaned_response)
241
 
242
- # Validate the structure of the returned JSON
243
- required_keys = ["finalGrade",
244
- "pointDeductions", "feedback", "summary"]
245
  if not all(key in data for key in required_keys):
246
- raise KeyError(
247
- "The model's response was missing one or more required keys.")
248
 
249
  return GradingResult(
250
- file_name=file_name,
251
  success=True,
252
  grade=data["finalGrade"],
253
  deductions=data["pointDeductions"],
@@ -256,163 +176,139 @@ class GeminiGrader:
256
  )
257
  except json.JSONDecodeError:
258
  return GradingResult(
259
- file_name=file_name,
260
  success=False,
261
- error_message="Failed to parse the model's response. The output was not valid JSON."
262
  )
263
  except Exception as e:
264
  return GradingResult(
265
- file_name=file_name,
266
  success=False,
267
  error_message=f"An API or model error occurred: {str(e)}"
268
  )
269
 
270
-
271
  # --- Gradio Application ---
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  async def grade_papers_concurrently(
274
  files: List[gr.File], example_paper_file: gr.File, api_key: str, progress=gr.Progress(track_tqdm=True)
275
  ) -> (str, str):
276
  """
277
  The main asynchronous function that orchestrates the grading process.
278
- It's triggered by the Gradio button click.
279
  """
280
  start_time = time.time()
281
-
282
  if not api_key:
283
  raise gr.Error("Google API Key is required.")
284
  if not files:
285
- raise gr.Error("Please upload at least one Word document.")
286
-
287
- # Process the optional example paper
288
- example_paper_text = "No example paper provided."
289
- if example_paper_file:
290
- try:
291
- progress(0, desc="Parsing example paper...")
292
- example_paper_text = EssayParser.parse_pdf(example_paper_file.name)
293
- except IOError as e:
294
- raise gr.Error(f"Failed to process example PDF: {e}")
295
 
 
296
  try:
297
  grader = GeminiGrader(api_key)
298
- except ValueError as e:
299
- raise gr.Error(str(e))
300
-
301
- file_paths = [file.name for file in files]
302
- total_files = len(file_paths)
303
-
304
- # Use a ThreadPoolExecutor to run synchronous tasks concurrently
305
- with ThreadPoolExecutor(max_workers=1) as executor:
306
- # Create a future for each file processing task
307
- loop = asyncio.get_event_loop()
308
- tasks = [
309
- loop.run_in_executor(
310
- executor,
311
- process_single_file,
312
- file_path,
313
- grader,
314
- example_paper_text
315
- )
316
- for file_path in file_paths
317
- ]
318
-
319
- results = []
320
- # Process results as they are completed
321
- for i, future in enumerate(asyncio.as_completed(tasks)):
322
- progress(i + 1, desc=f"Grading paper {i+1}/{total_files}...")
323
- result = await future
324
- results.append(result)
325
-
326
- # --- Format the final output ---
327
- successful_grades = [res for res in results if res.success]
328
- failed_grades = [res for res in results if not res.success]
329
-
330
- output_markdown = ""
331
- for result in successful_grades:
332
- output_markdown += f"### ✅ Grade for: **{result.file_name}**\n"
333
- output_markdown += f"**Final Grade:** {result.grade}/100\n\n"
334
-
335
- # Format point deductions
336
- deductions_str = ""
337
- for category, points in result.deductions.items():
338
- if points > 0:
339
- deductions_str += f"- **{category}:** Lost {points} points. *Reason: {result.feedback.get(category, 'N/A')}*\n"
340
- if not deductions_str:
341
- deductions_str = "Excellent work! No points were deducted.\n"
342
-
343
- output_markdown += "**Point Deductions Breakdown:**\n" + deductions_str + "\n"
344
- output_markdown += f"**Summary:** {result.summary}\n"
345
- output_markdown += "---\n"
346
-
347
- if failed_grades:
348
- output_markdown += "### ❌ Failed Papers\n"
349
- for result in failed_grades:
350
- output_markdown += f"- **File:** {result.file_name}\n"
351
- output_markdown += f" - **Error:** {result.error_message}\n"
352
- output_markdown += "---\n"
353
-
354
- end_time = time.time()
355
- runtime = f"Total runtime: {end_time - start_time:.2f} seconds."
356
-
357
- status = (
358
- f"Grading complete. {len(successful_grades)} papers graded successfully, "
359
- f"{len(failed_grades)} failed."
360
- )
361
-
362
- return output_markdown, f"{status}\n{runtime}"
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
- def process_single_file(file_path: str, grader: GeminiGrader, example_paper_text: str) -> GradingResult:
366
- """
367
- Synchronous wrapper function to parse and grade one file.
368
- This function is what runs in each thread of the ThreadPoolExecutor.
369
- """
370
- file_name = os.path.basename(file_path)
371
- try:
372
- essay_text = EssayParser.parse_docx(file_path)
373
- if not essay_text.strip():
374
- return GradingResult(
375
- file_name=file_name,
376
- success=False,
377
- error_message="The document is empty or contains no readable text."
378
- )
379
- return grader.grade_essay(essay_text, file_name, example_paper_text)
380
- except Exception as e:
381
- return GradingResult(file_name=file_name, success=False, error_message=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
 
384
  # --- Build the Gradio Interface ---
385
-
386
  with gr.Blocks(theme=gr.themes.Soft(), title="Nursing Essay Grader") as demo:
387
  gr.Markdown(
388
  """
389
  # 📝 Gemini-Powered Nursing Essay Grader
390
- Upload one or more student essays in Word format (`.docx`) to have them graded by AI.
391
- 1. Enter your Google API Key (enabling the Gemini API in your Google Cloud project is required).
392
- 2. Upload the `.docx` files you want to grade.
393
- 3. Optionally, upload a single `.pdf` file as a "perfect" example for the AI to reference.
394
- 4. Click "Grade All Papers". The results will appear below.
395
  """
396
  )
397
- with gr.Row():
398
- api_key_input = gr.Textbox(
399
- label="Google API Key",
400
- placeholder="Enter your Google API Key here",
401
- type="password",
402
- scale=1
403
- )
404
  with gr.Row():
405
  file_uploads = gr.File(
406
- label="Upload Word Document Essays to Grade",
407
  file_count="multiple",
408
- file_types=[".docx"],
409
  type="filepath",
410
  scale=2
411
  )
412
  example_paper_upload = gr.File(
413
- label="Upload Example PDF Paper (Optional)",
414
  file_count="single",
415
- file_types=[".pdf"],
416
  type="filepath",
417
  scale=1
418
  )
@@ -421,11 +317,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Nursing Essay Grader") as demo:
421
  gr.Markdown("---")
422
  gr.Markdown("## 📊 Grading Results")
423
  results_output = gr.Markdown(label="Formatted Grades")
424
- status_output = gr.Textbox(
425
- label="Runtime Status",
426
- lines=2,
427
- interactive=False
428
- )
429
 
430
  grade_button.click(
431
  fn=grade_papers_concurrently,
 
1
  import gradio as gr
 
2
  import asyncio
3
  from concurrent.futures import ThreadPoolExecutor
4
  from google import generativeai as genai
 
8
  import os
9
  from pathlib import Path
10
  import time
11
+ # The docx and fitz imports are no longer needed for the core logic
12
 
13
  GRADING_RUBRIC = """
14
  GRADING RUBRIC (Total 100 points)
 
81
 
82
  """
83
 
84
+ # The prompt is now simpler, as the files are passed as arguments to the model, not as text.
85
  BASE_PROMPT_TEMPLATE = """
86
  You are about to assume a role. Carefully review the persona, context, task, and output requirements before proceeding.
87
 
88
  1. PERSONA
 
89
  You are Dr. Stone, a meticulous and experienced Associate Professor of Nursing at a major university. You have been teaching for over 20 years, with a specialization in medical-surgical nursing. You are known for your high standards, particularly regarding academic integrity and the strict application of the American Psychological Association (APA) 7th Edition formatting guidelines. Your feedback is always direct, precise, and aimed at preparing students for the rigorous documentation standards required in the healthcare profession. Your tone is professional, authoritative, and educational. You do not offer praise for meeting baseline expectations; you simply state that the requirements have been met. Your criticism is specific, referencing the exact rule or rubric criterion that was violated.
90
 
91
  2. CONTEXT
92
+ You are grading an essay for your undergraduate course, NURS 310: Fundamentals of Nursing. The assignment is an essay on the topic of "Skin Lesions." You will be provided with the student's paper as an attached file, and optionally an example paper file for reference.
 
93
 
94
  3. PRIMARY TASK
95
+ Your task is to analyze the attached student paper file and grade it with absolute precision according to the provided rubric. You must function as a perfect and unflinchingly accurate grader. There is no room for subjective interpretation or leniency. You will identify every error in content, formatting, spelling, grammar, and citation, and assign points strictly based on the rubric's quantitative thresholds.
 
96
 
97
  Your output should be a single JSON format structure response as indicated by the example below.
98
 
99
  **DO NOT** provide any introductory text, conversational pleasantries, or explanations outside of the requested JSON structure. Your entire response must be a single, valid JSON object.
100
 
101
+ **Use the following rubric to grade the attached student paper:**
102
  {GRADING_RUBRIC}
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  **Example of the required JSON output format:**
105
  {{
106
  "finalGrade": 88,
 
118
  }},
119
  "summary": "This is a strong paper with excellent critical analysis. The final grade was primarily impacted by significant APA formatting errors, which should be the main focus for improvement."
120
  }}
 
 
 
 
121
  """
122
 
 
123
  @dataclass
124
  class GradingResult:
125
  """Holds the structured result of a single graded essay."""
 
131
  summary: Optional[str] = None
132
  error_message: Optional[str] = None
133
 
 
134
  # --- Core Logic Classes ---
135
+ # EssayParser class is no longer needed and has been removed.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  class GeminiGrader:
138
  """Manages interaction with the Google Gemini API for grading."""
 
139
  def __init__(self, api_key: str):
140
  """Initializes the Gemini model."""
141
  try:
142
  genai.configure(api_key=api_key)
143
+ self.model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  except Exception as e:
145
  raise ValueError(f"Failed to configure Gemini API: {e}")
146
 
147
+ def grade_essay(self, student_paper_file: object, example_paper_file: Optional[object]) -> GradingResult:
148
  """
149
+ Sends files to Gemini for grading and parses the JSON response.
150
+ This method now takes API file objects directly.
151
  """
152
+ prompt_text = BASE_PROMPT_TEMPLATE.format(GRADING_RUBRIC=GRADING_RUBRIC)
153
+
154
+ # Build the content list for the API call
155
+ # It includes the prompt text and the file objects
156
+ content_list = [prompt_text, student_paper_file]
157
+ if example_paper_file:
158
+ content_list.append(example_paper_file)
159
 
 
160
  try:
161
+ response = self.model.generate_content(content_list)
 
162
  cleaned_response = response.text.strip().replace("```json", "").replace("```", "")
163
  data = json.loads(cleaned_response)
164
 
165
+ required_keys = ["finalGrade", "pointDeductions", "feedback", "summary"]
 
 
166
  if not all(key in data for key in required_keys):
167
+ raise KeyError("Model response missing required keys.")
 
168
 
169
  return GradingResult(
170
+ file_name=student_paper_file.display_name,
171
  success=True,
172
  grade=data["finalGrade"],
173
  deductions=data["pointDeductions"],
 
176
  )
177
  except json.JSONDecodeError:
178
  return GradingResult(
179
+ file_name=student_paper_file.display_name,
180
  success=False,
181
+ error_message="Failed to parse the model's JSON response."
182
  )
183
  except Exception as e:
184
  return GradingResult(
185
+ file_name=student_paper_file.display_name,
186
  success=False,
187
  error_message=f"An API or model error occurred: {str(e)}"
188
  )
189
 
 
190
  # --- Gradio Application ---
191
 
192
+ def process_single_file(file_path: str, grader: GeminiGrader, example_paper_file_obj: Optional[object]) -> GradingResult:
193
+ """
194
+ Uploads a single student paper and calls the grader.
195
+ This function is what runs in each thread of the ThreadPoolExecutor.
196
+ """
197
+ student_paper_file_obj = None
198
+ try:
199
+ # Upload the student paper file for this specific job
200
+ student_paper_file_obj = genai.upload_file(path=file_path, display_name=os.path.basename(file_path))
201
+ return grader.grade_essay(student_paper_file_obj, example_paper_file_obj)
202
+ except Exception as e:
203
+ return GradingResult(file_name=os.path.basename(file_path), success=False, error_message=str(e))
204
+ finally:
205
+ # Clean up the uploaded student paper file after processing
206
+ if student_paper_file_obj:
207
+ genai.delete_file(student_paper_file_obj.name)
208
+
209
  async def grade_papers_concurrently(
210
  files: List[gr.File], example_paper_file: gr.File, api_key: str, progress=gr.Progress(track_tqdm=True)
211
  ) -> (str, str):
212
  """
213
  The main asynchronous function that orchestrates the grading process.
 
214
  """
215
  start_time = time.time()
 
216
  if not api_key:
217
  raise gr.Error("Google API Key is required.")
218
  if not files:
219
+ raise gr.Error("Please upload at least one paper to grade.")
 
 
 
 
 
 
 
 
 
220
 
221
+ example_paper_file_obj = None
222
  try:
223
  grader = GeminiGrader(api_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
+ # Upload the example paper ONCE if it exists
226
+ if example_paper_file:
227
+ progress(0, desc="Uploading example paper...")
228
+ example_paper_file_obj = genai.upload_file(path=example_paper_file.name, display_name=os.path.basename(example_paper_file.name))
229
+
230
+ file_paths = [file.name for file in files]
231
+ total_files = len(file_paths)
232
+
233
+ with ThreadPoolExecutor(max_workers=1) as executor:
234
+ loop = asyncio.get_event_loop()
235
+ tasks = [
236
+ loop.run_in_executor(
237
+ executor,
238
+ process_single_file,
239
+ file_path,
240
+ grader,
241
+ example_paper_file_obj
242
+ )
243
+ for file_path in file_paths
244
+ ]
245
 
246
+ results = []
247
+ for i, future in enumerate(asyncio.as_completed(tasks)):
248
+ progress(i + 1, desc=f"Grading paper {i+1}/{total_files}...")
249
+ result = await future
250
+ results.append(result)
251
+
252
+ # --- Format the final output ---
253
+ successful_grades = [res for res in results if res.success]
254
+ failed_grades = [res for res in results if not res.success]
255
+ output_markdown = ""
256
+ for result in successful_grades:
257
+ output_markdown += f"### ✅ Grade for: **{result.file_name}**\n"
258
+ output_markdown += f"**Final Grade:** {result.grade}/100\n\n"
259
+ deductions_str = ""
260
+ for category, points in result.deductions.items():
261
+ if points > 0:
262
+ deductions_str += f"- **{category}:** Lost {points} points. *Reason: {result.feedback.get(category, 'N/A')}*\n"
263
+ if not deductions_str:
264
+ deductions_str = "Excellent work! No points were deducted.\n"
265
+ output_markdown += "**Point Deductions Breakdown:**\n" + deductions_str + "\n"
266
+ output_markdown += f"**Summary:** {result.summary}\n"
267
+ output_markdown += "---\n"
268
+ if failed_grades:
269
+ output_markdown += "### ❌ Failed Papers\n"
270
+ for result in failed_grades:
271
+ output_markdown += f"- **File:** {result.file_name}\n"
272
+ output_markdown += f" - **Error:** {result.error_message}\n"
273
+ output_markdown += "---\n"
274
+
275
+ end_time = time.time()
276
+ runtime = f"Total runtime: {end_time - start_time:.2f} seconds."
277
+ status = f"Grading complete. {len(successful_grades)} papers graded successfully, {len(failed_grades)} failed."
278
+ return output_markdown, f"{status}\n{runtime}"
279
+
280
+ finally:
281
+ # Clean up the uploaded example paper file at the very end
282
+ if example_paper_file_obj:
283
+ genai.delete_file(example_paper_file_obj.name)
284
 
285
 
286
  # --- Build the Gradio Interface ---
 
287
  with gr.Blocks(theme=gr.themes.Soft(), title="Nursing Essay Grader") as demo:
288
  gr.Markdown(
289
  """
290
  # 📝 Gemini-Powered Nursing Essay Grader
291
+ Upload one or more student essays to have them graded by AI.
292
+ 1. Enter your Google API Key.
293
+ 2. Upload the `.docx` or `.pdf` files you want to grade.
294
+ 3. Optionally, upload a single `.docx` or `.pdf` file as a "perfect" example.
295
+ 4. Click "Grade All Papers".
296
  """
297
  )
298
+ api_key_input = gr.Textbox(label="Google API Key", placeholder="Enter your Google API Key here", type="password")
299
+
 
 
 
 
 
300
  with gr.Row():
301
  file_uploads = gr.File(
302
+ label="Upload Essays to Grade",
303
  file_count="multiple",
304
+ file_types=['.pdf', '.docx'], # Allow both types
305
  type="filepath",
306
  scale=2
307
  )
308
  example_paper_upload = gr.File(
309
+ label="Upload Example Paper (Optional)",
310
  file_count="single",
311
+ file_types=['.pdf', '.docx'], # Allow both types
312
  type="filepath",
313
  scale=1
314
  )
 
317
  gr.Markdown("---")
318
  gr.Markdown("## 📊 Grading Results")
319
  results_output = gr.Markdown(label="Formatted Grades")
320
+ status_output = gr.Textbox(label="Runtime Status", lines=2, interactive=False)
 
 
 
 
321
 
322
  grade_button.click(
323
  fn=grade_papers_concurrently,