UI user task assignment

This commit is contained in:
Nicola Leonardi 2026-03-12 12:39:28 +01:00
parent b8fa6dfd13
commit ebbaa972e9
3 changed files with 399 additions and 187 deletions

View File

@ -25,13 +25,14 @@ def hash_password(password):
"""Hash password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest()
def associate_user_with_manager(users, user_assignment_manager):
user_list=users.keys()
user_list = users.keys()
print(f"registering--Associating users with manager: {list(user_list)}")
user_assignment_manager.register_active_users(list(user_list))
def register_user(username, password, confirm_password,user_assignment_manager):
def register_user(username, password, confirm_password, user_assignment_manager):
"""Register a new user"""
if not username or not password:
return "", "Username and password cannot be empty!", None
@ -53,7 +54,7 @@ def register_user(username, password, confirm_password,user_assignment_manager):
try:
associate_user_with_manager(users, user_assignment_manager)
except Exception as e:
print(f"Error associating user with manager: {e}")
print(f"Error associating user with manager: {e}")
return "", f"✅ Registration successful! You can now login.", None
@ -125,3 +126,44 @@ def protected_content(state):
if state.get("logged_in"):
return f"You are logged as {state.get('username')}\n"
return "Please login to access this content."
def get_user_assessments_done(connection_db, username):
"""
it returns:
{
"https://example.com/page1": [1, 3, 5],
"https://example.com/page2": [2, 4, 6],
}
"""
cursor = connection_db.cursor()
username = json.dumps({"username": username}, ensure_ascii=False)
cursor.execute(
"""
SELECT page_url, json_output_data
FROM wcag_user_assessments
WHERE user = ? AND insert_type = ?
ORDER BY page_url
""",
(username, "wcag_user_llm_alttext_assessments"),
)
rows = cursor.fetchall()
assessment_done = {} # dict: {page_url: sorted list of image numbers}
for row in rows:
page_url = row[0]
data = json.loads(row[1])
image_numbers = {int(item["Image #"]) for item in data} # set to deduplicate
if page_url not in assessment_done:
assessment_done[page_url] = image_numbers
else:
assessment_done[page_url].update(
image_numbers
) # merge if url appears multiple times
# Convert sets to sorted lists
assessment_done = {url: sorted(imgs) for url, imgs in assessment_done.items()}
return assessment_done

View File

@ -54,6 +54,7 @@ class UserAssignmentManager:
assignments_xlsx_path: str = "alt_text_assignments_output_target_overlap.xlsx",
target_overlap: int = 2,
seed: int = 42,
):
"""
Initialize the User Assignment Manager.
@ -86,8 +87,8 @@ class UserAssignmentManager:
# Initialize database
self._init_database()
# Load existing assignments from JSON if available
self._load_existing_assignments()
# Load existing assignments to db from JSON if available
#self._load_existing_assignments()
def _load_sites_config(self) -> List[SiteConfig]:
"""Load site configuration from JSON."""
@ -160,7 +161,7 @@ class UserAssignmentManager:
conn.commit()
conn.close()
def _load_existing_assignments(self, active_user_names: Optional[List[str]] = None):
def _load_existing_assignments(self, active_user_names: List[str] = []):
"""Load existing assignments from JSON file into database if not already there."""
if not self.assignments_json_path.exists():
return
@ -172,17 +173,15 @@ class UserAssignmentManager:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# nb: every service restart and user registration will trigger this (ONCONFLICT ensures no duplicates)
for user_id, sites_dict in assignments.items():
for site_url, image_indices in sites_dict.items():
# print(f"[DB] Loading assignment for user {user_id}, site {site_url}, "
# f"{image_indices} images")
try:
'''
cursor.execute("""
INSERT OR IGNORE INTO user_assignments
(user_id, site_url, image_indices)
VALUES (?, ?, ?)
""", (user_id, site_url, json.dumps(image_indices)))'''
try:
for site_url, image_indices in sites_dict.items():
print(
f"[DB] Loading assignment for user {user_id}, site {site_url}, "
f"{image_indices} images"
)
cursor.execute(
"""
@ -195,28 +194,26 @@ class UserAssignmentManager:
(user_id, site_url, json.dumps(image_indices)),
)
cursor.execute( # also update user_info table with user_name if active_user_names is provided and user_id starts with "user"
"""
INSERT INTO user_info (user_id, user_name)
VALUES (?, ?)
ON CONFLICT(user_id) DO UPDATE SET
user_name = excluded.user_name
""",
cursor.execute( # also update user_info table with user_name if active_user_names is provided and user_id starts with "user"
"""
INSERT INTO user_info (user_id, user_name)
VALUES (?, ?)
ON CONFLICT(user_id) DO UPDATE SET
user_name = excluded.user_name
""",
(
user_id,
(
user_id,
(
active_user_names[int(user_id[4:]) - 1]
if active_user_names and user_id.startswith("user")
else None
),
active_user_names[int(user_id[4:]) - 1]
if active_user_names and user_id.startswith("user")
else None
),
)
),
)
except sqlite3.IntegrityError:
print(
f"[DB] Error. Skipping existing assignment for user {user_id}, site {site_url}"
)
pass
except sqlite3.IntegrityError:
print(f"[DB] Error. Skipping existing assignment for user {user_id}")
pass
conn.commit()
conn.close()
@ -243,7 +240,7 @@ class UserAssignmentManager:
if from_user_name:
print(f"[DB] Looking up user_id for user_name: {user_id}")
cursor.execute(
"""
SELECT user_id

View File

@ -18,7 +18,9 @@ from dependences.utils import (
db_persistence_startup,
db_persistence_insert,
return_from_env_valid,
)
from dependences_ui.utils import *
import logging
import time
@ -31,18 +33,37 @@ import sqlite3
from user_task_assignment.user_assignment_manager import UserAssignmentManager
from dependences_ui.utils import load_users,get_user_assessments_done
users=load_users()
user_list=list(users.keys())
print(f"Loaded users from simple JSON: {len(user_list)}")
user_assignment_manager = UserAssignmentManager(
db_path="persistence/wcag_validator_ui.db",
config_json_path="user_task_assignment/sites_config.json",
assignments_json_path="user_task_assignment/alt_text_assignments_output_target_overlap.json",
assignments_xlsx_path="user_task_assignment/alt_text_assignments_output_target_overlap.xlsx"
assignments_xlsx_path="user_task_assignment/alt_text_assignments_output_target_overlap.xlsx",
)
# Get current managed users
managed_users = user_assignment_manager.get_all_user_ids()
print(f"Currently managed users from db: {managed_users}")
print(f"Total managed users from db: {user_assignment_manager.get_managed_user_count()}\n")
managed_users_number = user_assignment_manager.get_managed_user_count()
print(f"Currently managed users from db: {managed_users_number}")
if managed_users_number !=len(user_list):# rigenenerate files only if some user numbers disalignmnets. Avoid only updates on new user registration process
print(f"Warning: Number of users in db ({managed_users_number}) does not match number of users loaded from JSON ({len(user_list)}). Re-init user assignments files.")
user_assignment_manager.register_active_users(user_list)#on startup register users loaded from JSON into the manager (creating also assignments .json amd .xml files)
# Get current managed users after regsitration alignment
managed_users_number = user_assignment_manager.get_managed_user_count()
print(f"Currently managed users from db after alignment: {managed_users_number}")
# Get current managed users after regsitration alignment
print(f"Total managed users from db: {managed_users_number}\n")
if managed_users_number !=len(user_list):
print(f"Warning: Number of users in db ({managed_users_number}) does not match number of users loaded from JSON ({len(user_list)}). Check user assignment manager initialization.")
exit(1)
user_assignment_stats = user_assignment_manager.get_statistics()
print(f"Current assignment stats:{user_assignment_stats} \n")
@ -52,9 +73,81 @@ print(f"Current assignment stats:{user_assignment_stats} \n")
WCAG_VALIDATOR_RESTSERVER_HEADERS = [("Content-Type", "application/json")]
def display_user_assignment(user_state):
def maybe_close_modal(process_dataframe_output_state):
print("Checking if modal can be closed based on:",type(process_dataframe_output_state), process_dataframe_output_state)
if not process_dataframe_output_state:
print("Modal cannot be closed.")
return Modal(visible=True) # keep it open
return Modal(visible=False) # close it
def maybe_open_modal(make_alttext_llm_assessment_api_call_output_state):
print("Checking if modal can be opened based on:",type(make_alttext_llm_assessment_api_call_output_state), make_alttext_llm_assessment_api_call_output_state)
if not make_alttext_llm_assessment_api_call_output_state:
print("Modal cannot be opened.")
return Modal(visible=False)
return Modal(visible=True)
def render_user_assessmnet_status_table(df):
if df is None or df.empty:
return "<p>No assignments found.</p>"
total_work_to_be_done=[]
rows = ""
for _, row in df.iterrows():
url = row["Website URL"]
assigned = row["Assigned Image Number"]
work_done = row["Work Done on Image Number"]
work_to_be_done = [img for img in assigned if img not in work_done]
total_work_to_be_done+=work_to_be_done
rows += f"""
<tr>
<td style="padding:8px; border:1px solid #ddd; word-break:break-all;">
<a href="{url}" target="_blank">{url}</a>
</td>
<td style="padding:8px; border:1px solid #ddd; text-align:center;">
{assigned}
</td>
<td style="padding:8px; border:1px solid #ddd; text-align:center;">
{work_done}
</td>
<td style="padding:8px; border:1px solid #ddd; text-align:center;">
{work_to_be_done}
</td>
</tr>
"""
total_work_to_be_done_text =""
if len(total_work_to_be_done)==0:
total_work_to_be_done_text="All assigned work is done! Great job!"
return f"""
<div style="overflow-x:auto;">
<table style="width:100%; border-collapse:collapse; font-size:14px;">
<thead>
<tr style="background-color:#f2f2f2;">
<th style="padding:10px; border:1px solid #ddd; text-align:left;">Website URL</th>
<th style="padding:10px; border:1px solid #ddd; text-align:left;">Assigned Image Number</th>
<th style="padding:10px; border:1px solid #ddd; text-align:left;">Work Done on Image Number</th>
<th style="padding:10px; border:1px solid #ddd; text-align:left;">Work Still to be Done</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
<p style="color:green;font-size: large;font-weight: bold;">{total_work_to_be_done_text}</p>
"""
def display_user_assignment(db_path,user_state):
if user_state and "username" in user_state:
username = user_state["username"]
connection_db = sqlite3.connect(db_path)
user_assessment_work=get_user_assessments_done(connection_db ,username)
print(f"User {username} has done assessments for {user_assessment_work} images.")
print(f"Fetching assignment for user: {username}")
assignments = user_assignment_manager.get_user_assignments(username, from_user_name=True)
@ -70,7 +163,8 @@ def display_user_assignment(user_state):
data_frame.append(
{
"Website URL": url,
"Assigned Image Number List": assignments[url]
"Assigned Image Number": assignments[url],
"Work Done on Image Number":user_assessment_work[url] if url in user_assessment_work else [],
}
)
@ -83,7 +177,7 @@ def display_user_assignment(user_state):
def process_dataframe(db_path, url, updated_df, user_state={},llm_response_output={}):
print("Processing dataframe to adjust columns...type:",type(updated_df))
print("Processing dataframe to adjust columns...type:",type(updated_df),updated_df)
# accept different input forms from UI (DataFrame, JSON string, or list of dicts)
try:
@ -95,23 +189,23 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu
elif isinstance(updated_df, list):
updated_df = pd.DataFrame(updated_df)
except Exception as e:
return f"Error parsing updated data: {str(e)}"
return f"Error parsing updated data: {str(e)}" ,False
for column_rating_name in ["User Assessment for LLM Proposal 1", "User Assessment for LLM Proposal 2"]:
# Get the assessment column
try:
updated_df[column_rating_name] = updated_df[column_rating_name].astype(int)
except ValueError:
return "Error: User Assessment for LLM Proposal must be an integer"
return "Error: User Assessment for LLM Proposal must be an integer",False
except KeyError:
return f"No data Saved because no image selected. Please select at least one image."
return f"No data Saved because some images are not correcly managed. Please retry." ,False
except Exception as e:
return f"Error processing User Assessment for LLM Proposal: {str(e)}"
return f"Error processing User Assessment for LLM Proposal: {str(e)}" ,False
if (updated_df[column_rating_name] < 1).any() or (
updated_df[column_rating_name] > 5
).any():
return "Error: User Assessment for LLM Proposal must be between 1 and 5"
return "Error: User Assessment for LLM Proposal must be between 1 and 5",False
dataframe_json = updated_df.to_json(orient="records")
connection_db = sqlite3.connect(db_path)
@ -126,7 +220,7 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu
page_url=url,
user=json_user_str,
llm_model="",
json_in_str=llm_response_output_str,#dataframe_json, # to improve
json_in_str=llm_response_output_str,#dataframe_json,
json_out_str=dataframe_json,
table="wcag_user_assessments",
)
@ -135,11 +229,27 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu
finally:
if connection_db:
connection_db.close()
return "User assessment saved successfully!"
print("User assessment saved to database successfully.returning:", True)
return "User assessment saved successfully!",True
def load_images_from_json(json_input):
def load_images_from_json(json_input,user_assignment_current_status_df):
"""Extract URLs and alt text from JSON and create HTML gallery"""
if user_assignment_current_status_df is None or user_assignment_current_status_df.empty:
print("No user assignment status found. Displaying all images without assignment info.")
user_assignments={}
for _, row in user_assignment_current_status_df.iterrows():
url = row["Website URL"]
assigned = row["Assigned Image Number"]
work_done = row["Work Done on Image Number"]
user_assignments[url] = {
"assigned": assigned,
"work_done": work_done
}
#print(f"User assignments extracted for image loading: {user_assignments}")
try:
data = json_input
@ -250,6 +360,17 @@ def load_images_from_json(json_input):
url = img_data.get("url", "")
alt_text = img_data.get("alt_text", "No description")
page_url = img_data.get("page_url", "")
assigned=user_assignments.get(page_url,{}).get("assigned",[])
work_done=user_assignments.get(page_url,{}).get("work_done",[])
assigned_text=""
if idx+1 in assigned:
assigned_text="-(Assigned)"
if idx+1 in work_done:
assigned_text+="->(Already managed)"
if idx+1 in assigned and idx+1 in work_done:
assigned_text+="<span style='font-family: wingdings; font-size: large; font-weight: bold; color:green'>&#252;</span>"
html += f"""
<div class="image-card">
<img src="{url}" alt="{alt_text}" loading="lazy" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22200%22%3E%3Crect fill=%22%23ddd%22 width=%22200%22 height=%22200%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22%3EImage not found%3C/text%3E%3C/svg%3E'">
@ -260,9 +381,9 @@ def load_images_from_json(json_input):
const panel = document.getElementById('panel-{idx}');
const checkedCount = document.querySelectorAll('.image-checkbox:checked').length;
if (this.checked) {{
if (checkedCount > 3) {{
if (checkedCount > 6) {{
this.checked = false;
alert('Maximum 3 images can be selected!');
alert('Maximum 6 images can be selected!');
return;
}}
panel.classList.add('visible');
@ -270,7 +391,7 @@ def load_images_from_json(json_input):
panel.classList.remove('visible');
}}
">
Select #{idx + 1}
Select #{idx + 1}<span>{assigned_text}</span>
</label>
<div class="alt-text">Current alt_text: {alt_text}</div>
@ -287,7 +408,7 @@ def load_images_from_json(json_input):
<span class="radio-label">2</span>
</label>
<label class="radio-option">
<input type="radio" name="assessment-{idx}" value="3" data-index="{idx}" checked>
<input type="radio" name="assessment-{idx}" value="3" data-index="{idx}">
<span class="radio-label">3</span>
</label>
<label class="radio-option">
@ -443,8 +564,8 @@ def make_alttext_llm_assessment_api_call(
if not selected_images or len(selected_images) == 0:
info_text = "No images selected"
return "LLM assessment not started", pd.DataFrame(), {}
print("LLM assessment not started because no valid images were selected.")
return "LLM assessment not started", pd.DataFrame(), {},False
# prepare data for insertion
json_in_str = {}
@ -465,8 +586,8 @@ def make_alttext_llm_assessment_api_call(
selected_image_id.append(
int(img["image_index"]) + 1
) # add the id selected (+1 for index alignment)
user_assessments_llm_proposal_1.append(3) # default value for now
user_assessments_llm_proposal_2.append(3) # default value for now
user_assessments_llm_proposal_1.append(0) # default value for now
user_assessments_llm_proposal_2.append(0) # default value for now
json_in_str["images_urls"] = selected_urls
json_in_str["images_alt_text_original"] = selected_alt_text_original
json_out_str["user_assessments"] = user_assessments
@ -531,13 +652,14 @@ def make_alttext_llm_assessment_api_call(
finally:
if connection_db:
connection_db.close()
return "LLM assessment completed", info_dataframe, response
return "LLM assessment completed", info_dataframe, response, True
def make_image_extraction_api_call(
url,
number_of_images=30,
wcag_rest_server_url="http://localhost:8000",
user_assignment_current_status={},
):
print(
f"Making API call for image_extraction for {url} to {wcag_rest_server_url}/extract_images"
@ -553,7 +675,7 @@ def make_image_extraction_api_call(
headers=WCAG_VALIDATOR_RESTSERVER_HEADERS,
)
# return response
info_text, gallery_images = load_images_from_json(response)
info_text, gallery_images = load_images_from_json(response,user_assignment_current_status)
return info_text, gallery_images
except Exception as e:
@ -561,97 +683,101 @@ def make_image_extraction_api_call(
def render_alttext_form(df):
"""Render a pandas DataFrame (or list/dict) into an editable HTML form."""
try:
if df is None:
return ""
if isinstance(df, str):
df = pd.read_json(df, orient="records")
if isinstance(df, dict):
df = pd.DataFrame(df)
if isinstance(df, list):
df = pd.DataFrame(df)
"""Render a pandas DataFrame (or list/dict) into an editable HTML form."""
try:
if df is None or df.empty:
print("No data to render in form.")
html=""
return gr.update(value=html), html # return empty form
if isinstance(df, str):
df = pd.read_json(df, orient="records")
if isinstance(df, dict):
df = pd.DataFrame(df)
if isinstance(df, list):
df = pd.DataFrame(df)
html = """
<style>
.alttext-table { width:100%; border-collapse: collapse; }
.alttext-table th, .alttext-table td { border:1px solid #ddd; padding:8px; }
.alttext-table th { background:#f5f5f5; }
.alttext-row td { vertical-align: top; }
.llm-select { width:auto; }
</style>
<table class="alttext-table">
<thead>
<tr>
<th>Image #</th>
<th>Original Alt Text</th>
<th>User Assessment</th>
<th>User Proposed Alt Text</th>
<th>LLM Assessment 1</th>
<th>LLM Proposed Alt Text 1</th>
<th>User Assessment for LLM Proposal 1</th>
<th>LLM Assessment 2</th>
<th>LLM Proposed Alt Text 2</th>
<th>User Assessment for LLM Proposal 2</th>
</tr>
</thead>
<tbody>
html = """
<style>
.alttext-table { width:100%; border-collapse: collapse; }
.alttext-table th, .alttext-table td { border:1px solid #ddd; padding:8px; }
.alttext-table th { background:#f5f5f5; }
.alttext-row td { vertical-align: top; }
.llm-select { width:auto; }
</style>
<table class="alttext-table">
<thead>
<tr>
<th>Image #</th>
<th>Original Alt Text</th>
<th>User Assessment</th>
<th>User Proposed Alt Text</th>
<th>LLM Assessment 1</th>
<th>LLM Proposed Alt Text 1</th>
<th>User Assessment for LLM Proposal 1</th>
<th>LLM Assessment 2</th>
<th>LLM Proposed Alt Text 2</th>
<th>User Assessment for LLM Proposal 2</th>
</tr>
</thead>
<tbody>
"""
for _, row in df.iterrows():
imgnum = row.get("Image #", "")
orig = row.get("Original Alt Text", "")
user_ass = row.get("User Assessment", "")
user_prop = row.get("User Proposed Alt Text", "")
llm1_ass = row.get("LLM Assessment 1", "")
llm2_ass = row.get("LLM Assessment 2", "")
llm1_prop = row.get("LLM Proposed Alt Text 1", "")
llm2_prop = row.get("LLM Proposed Alt Text 2", "")
user_llm1_ass = row.get("User Assessment for LLM Proposal 1", 0)
user_llm2_ass = row.get("User Assessment for LLM Proposal 2", 0)
html += f"""
<tr class="alttext-row" data-index="{imgnum}">
<td class="img-num">{imgnum}</td>
<td class="orig-alt">{orig}</td>
<td class="user-assessment">{user_ass}</td>
<td class="user-proposed">{user_prop}</td>
<td >{llm1_ass}</td>
<td >{llm1_prop}</td>
<td>
<select class="user_llm1_ass llm-select">
<option value="0" {'selected' if int(user_llm1_ass)==0 else ''}>-- none --</option>
<option value="1" {'selected' if int(user_llm1_ass)==1 else ''}>1</option>
<option value="2" {'selected' if int(user_llm1_ass)==2 else ''}>2</option>
<option value="3" {'selected' if int(user_llm1_ass)==3 else ''}>3</option>
<option value="4" {'selected' if int(user_llm1_ass)==4 else ''}>4</option>
<option value="5" {'selected' if int(user_llm1_ass)==5 else ''}>5</option>
</select>
</td>
<td >{llm2_ass}</td>
<td >{llm2_prop}</td>
<td>
<select class="user_llm2_ass llm-select">
<option value="" {'selected' if int(user_llm1_ass)==0 else ''}>-- none --</option>
<option value="1" {'selected' if int(user_llm2_ass)==1 else ''}>1</option>
<option value="2" {'selected' if int(user_llm2_ass)==2 else ''}>2</option>
<option value="3" {'selected' if int(user_llm2_ass)==3 else ''}>3</option>
<option value="4" {'selected' if int(user_llm2_ass)==4 else ''}>4</option>
<option value="5" {'selected' if int(user_llm2_ass)==5 else ''}>5</option>
</select>
</td>
</tr>
"""
for _, row in df.iterrows():
imgnum = row.get("Image #", "")
orig = row.get("Original Alt Text", "")
user_ass = row.get("User Assessment", "")
user_prop = row.get("User Proposed Alt Text", "")
llm1_ass = row.get("LLM Assessment 1", "")
llm2_ass = row.get("LLM Assessment 2", "")
llm1_prop = row.get("LLM Proposed Alt Text 1", "")
llm2_prop = row.get("LLM Proposed Alt Text 2", "")
html += """
</tbody>
</table>
"""
user_llm1_ass = row.get("User Assessment for LLM Proposal 1", 3)
user_llm2_ass = row.get("User Assessment for LLM Proposal 2", 3)
html += f"""
<tr class="alttext-row" data-index="{imgnum}">
<td class="img-num">{imgnum}</td>
<td class="orig-alt">{orig}</td>
<td class="user-assessment">{user_ass}</td>
<td class="user-proposed">{user_prop}</td>
<td >{llm1_ass}</td>
<td >{llm1_prop}</td>
<td>
<select class="user_llm1_ass llm-select">
<option value="1" {'selected' if int(user_llm1_ass)==1 else ''}>1</option>
<option value="2" {'selected' if int(user_llm1_ass)==2 else ''}>2</option>
<option value="3" {'selected' if int(user_llm1_ass)==3 else ''}>3</option>
<option value="4" {'selected' if int(user_llm1_ass)==4 else ''}>4</option>
<option value="5" {'selected' if int(user_llm1_ass)==5 else ''}>5</option>
</select>
</td>
<td >{llm2_ass}</td>
<td >{llm2_prop}</td>
<td>
<select class="user_llm2_ass llm-select">
<option value="1" {'selected' if int(user_llm2_ass)==1 else ''}>1</option>
<option value="2" {'selected' if int(user_llm2_ass)==2 else ''}>2</option>
<option value="3" {'selected' if int(user_llm2_ass)==3 else ''}>3</option>
<option value="4" {'selected' if int(user_llm2_ass)==4 else ''}>4</option>
<option value="5" {'selected' if int(user_llm2_ass)==5 else ''}>5</option>
</select>
</td>
</tr>
"""
html += """
</tbody>
</table>
"""
return gr.update(value=html), html
except Exception as e:
return f"Error rendering form: {str(e)}"
return gr.update(value=html), html
except Exception as e:
return f"Error rendering form: {str(e)}"
# ------- Gradio Interface -------#
@ -682,6 +808,9 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
llm_response_output = gr.State()
alttext_popup_html_state = gr.State("")
user_assignment_manager_state = gr.State(value=user_assignment_manager)
user_assignment_current_status = gr.State()
process_dataframe_output_state = gr.State()
make_alttext_llm_assessment_api_call_output_state = gr.State()
with Modal(visible=False, allow_user_close=False) as alttext_modal:
gr.Markdown("## Alt Text LLMs Assessment Results")
@ -754,22 +883,14 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
with gr.Column(visible=False) as protected_section:
content_display = gr.Textbox(
label="Your account", lines=5, interactive=False
)
user_assignment_status = gr.DataFrame(
headers=[
"Website URL",
"Assigned Image Number List"
#"Assignment Status",
],
label="Your Current Assignment",
wrap=True, # Wrap text in cells
interactive=False,
scale=7,
label="Your account", lines=2, interactive=False
)
logout_btn = gr.Button("Logout", variant="stop")
gr.Markdown("### Your Assignment")
user_assignment_status =gr.HTML(label="Your Assignment")
# end login section
@ -816,7 +937,8 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
# Store the DataFrame in state and render a clear HTML form for user edits
alttext_info_state = gr.State()
alttext_form = gr.HTML(label="Assessment Form")
alttext_form_data = gr.JSON(visible=False)
alttext_form_data = gr.JSON(visible=False) ##gr.JSON(visible=False) because gr.State() components are not meant to receive values from JS returns
with gr.Row():
@ -835,7 +957,7 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
],
).then(
make_image_extraction_api_call,
inputs=[url_input, images_number, wcag_rest_server_url_state],
inputs=[url_input, images_number, wcag_rest_server_url_state,user_assignment_current_status],
outputs=[image_info_output, gallery_html],
).then(
fn=lambda: gr.Button(interactive=True),
@ -852,27 +974,40 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
wcag_rest_server_url_state,
user_state,
],
outputs=[image_info_output, alttext_info_state, llm_response_output],
outputs=[image_info_output, alttext_info_state, llm_response_output,make_alttext_llm_assessment_api_call_output_state],
js="""
(url_input,gallery_html) => {
(url_input, gallery_html) => {
const checkboxes = document.querySelectorAll('.image-checkbox:checked');
if (checkboxes.length === 0) {
alert('Please select at least one image!');
return [url_input,JSON.stringify([])];
return [url_input, JSON.stringify([])];
}
if (checkboxes.length > 3) {
alert('Please select maximum 3 images!');
return [url_input,JSON.stringify([])];
if (checkboxes.length > 6) {
alert('Please select maximum 6 images!');
return [url_input, JSON.stringify([])];
}
const selectedData = [];
let hasError = false; // flag to handle missing assessment
checkboxes.forEach(checkbox => {
if (hasError) return; // skip remaining iterations if error found
const index = checkbox.dataset.index;
const imageUrl = checkbox.dataset.imgurl;
const originalAlt = document.querySelector('.original-alt[data-index="' + index + '"]').value;
const assessment = document.querySelector('input[name="assessment-' + index + '"]:checked').value;
const newAltText = document.querySelector('.new-alt-text[data-index="' + index + '"]').value;
const assessmentInput = document.querySelector('input[name="assessment-' + index + '"]:checked');
const assessment = assessmentInput ? assessmentInput.value : null;
if (!assessment) {
alert('Please provide an assessment (1-5) for all selected images. Missing assessment for image index: ' + (parseInt(index) + 1));
hasError = true;
return; // exits forEach callback only
}
const newAltText = document.querySelector('.new-alt-text[data-index="' + index + '"]').value;
selectedData.push({
image_index: index,
image_url: imageUrl,
@ -881,56 +1016,82 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
new_alt_text: newAltText
});
});
return [url_input,JSON.stringify(selectedData)];
if (hasError) return [url_input, JSON.stringify([])]; // now actually exits outer function
return [url_input, JSON.stringify(selectedData)];
}
""",
).then(
fn=render_alttext_form,
inputs=[alttext_info_state],
outputs=[alttext_form,alttext_popup_html_state],
).then(fn=maybe_open_modal,#open modal
inputs=[make_alttext_llm_assessment_api_call_output_state], # gr.State that holds your condition
outputs=[alttext_modal]
).then(
fn=lambda html: (gr.update(value=html), Modal(visible=True)),
fn=lambda html: (gr.update(value=html)),
inputs=[alttext_popup_html_state],
outputs=[alttext_modal_content, alttext_modal], # ← populate + open modal
outputs=[alttext_modal_content], # ← populate modal
)
close_modal_btn.click( #the close button now save
fn=process_dataframe,
inputs=[db_path_state, url_input, alttext_form_data, user_state,llm_response_output],
outputs=[image_info_output],
outputs=[image_info_output,process_dataframe_output_state],
js="""
(db_path_state, url_input, alttext_form_html, user_state, llm_response_output) => {
(db_path_state, url_input, alttext_form_data, user_state, llm_response_output) => {
const rows = document.querySelectorAll('.alttext-row');
const selectedData = [];
// Check all rows first if any select is unset (0 or empty), return empty list
const hasUnset = Array.from(rows).some(row => {
const user_llm1_ass = parseInt(row.querySelector('.user_llm1_ass')?.value || '0');
const user_llm2_ass = parseInt(row.querySelector('.user_llm2_ass')?.value || '0');
return user_llm1_ass === 0 || user_llm2_ass === 0;
});
console.log("hasUnset:",hasUnset)
if (hasUnset)
{alert('Please provide an assessment (1-5) for all selected images for both models');
return [db_path_state, url_input, [], user_state, llm_response_output];}
rows.forEach(row => {
const imgNum = row.querySelector('.img-num')?.innerText || '';
const origAlt = row.querySelector('.orig-alt')?.innerText || '';
const userAssessment = row.querySelector('.user-assessment')?.innerText || '3';
const userProposed = row.querySelector('.user-proposed')?.innerText || '';
const user_llm1_ass = row.querySelector('.user_llm1_ass')?.value || '3';
const user_llm2_ass = row.querySelector('.user_llm2_ass')?.value || '3';
const user_llm1_ass = row.querySelector('.user_llm1_ass')?.value || '0';
const user_llm2_ass = row.querySelector('.user_llm2_ass')?.value || '0';
selectedData.push({
"Image #": imgNum,
"Original Alt Text": origAlt,
"User Assessment": parseInt(userAssessment)||3,
"User Assessment": parseInt(userAssessment) || 3,
"User Proposed Alt Text": userProposed,
"User Assessment for LLM Proposal 1": parseInt(user_llm1_ass),
"User Assessment for LLM Proposal 2": parseInt(user_llm2_ass)
});
});
console.log("selectedData:",selectedData);
return [db_path_state, url_input, selectedData, user_state, llm_response_output];
}
""",
).then( # Close button dismisses the modal
fn=lambda: Modal(visible=False),
inputs=[],
fn=maybe_close_modal,
inputs=[process_dataframe_output_state], # gr.State that holds your condition
outputs=[alttext_modal],
js="""
async () => {
async (is_valid) => {
console.log("is_valid animation:",is_valid) // da sistemare, animazione non gestita
if (!is_valid) {
console.log("skip animation");
return;} // Skip animation if not closing
const btn = document.querySelector('.close-modal-btn');
// Change button text
@ -945,7 +1106,10 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
await new Promise(resolve => setTimeout(resolve, 400));
}
"""
)
).then(# refresh the user assignment display after saving the assessment
fn=display_user_assignment, inputs=[db_path_state,user_state], outputs=[user_assignment_current_status]).then(fn=render_user_assessmnet_status_table, inputs=[user_assignment_current_status], outputs=[user_assignment_status]) # sync the user assignment display after saving the assessment
# placed here at the end to give full contents visibility to events
# Event handlers
@ -961,7 +1125,7 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
alttext_assessment,
register_and_login,
],
).then(fn=protected_content, inputs=[user_state], outputs=[content_display]).then(fn=display_user_assignment, inputs=[user_state], outputs=[user_assignment_status])
).then(fn=protected_content, inputs=[user_state], outputs=[content_display]).then(fn=display_user_assignment, inputs=[db_path_state,user_state], outputs=[user_assignment_current_status]).then(fn=render_user_assessmnet_status_table, inputs=[user_assignment_current_status], outputs=[user_assignment_status]) # display the user assignment after login
reg_btn.click(
fn=register_user,
@ -979,7 +1143,16 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
protected_section,
alttext_assessment,
],
)
).then(
fn=lambda: ("", "", gr.update(visible=False), gr.Button(interactive=False)),
inputs=[],
outputs=[
image_info_output,
gallery_html,
alttext_results_row,
alttext_api_call_btn,
],
)
if __name__ == "__main__":