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""" """Hash password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest() return hashlib.sha256(password.encode()).hexdigest()
def associate_user_with_manager(users, user_assignment_manager): 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)}") print(f"registering--Associating users with manager: {list(user_list)}")
user_assignment_manager.register_active_users(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""" """Register a new user"""
if not username or not password: if not username or not password:
return "", "Username and password cannot be empty!", None return "", "Username and password cannot be empty!", None
@ -53,7 +54,7 @@ def register_user(username, password, confirm_password,user_assignment_manager):
try: try:
associate_user_with_manager(users, user_assignment_manager) associate_user_with_manager(users, user_assignment_manager)
except Exception as e: 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 return "", f"✅ Registration successful! You can now login.", None
@ -125,3 +126,44 @@ def protected_content(state):
if state.get("logged_in"): if state.get("logged_in"):
return f"You are logged as {state.get('username')}\n" return f"You are logged as {state.get('username')}\n"
return "Please login to access this content." 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", assignments_xlsx_path: str = "alt_text_assignments_output_target_overlap.xlsx",
target_overlap: int = 2, target_overlap: int = 2,
seed: int = 42, seed: int = 42,
): ):
""" """
Initialize the User Assignment Manager. Initialize the User Assignment Manager.
@ -86,8 +87,8 @@ class UserAssignmentManager:
# Initialize database # Initialize database
self._init_database() self._init_database()
# Load existing assignments from JSON if available # Load existing assignments to db from JSON if available
self._load_existing_assignments() #self._load_existing_assignments()
def _load_sites_config(self) -> List[SiteConfig]: def _load_sites_config(self) -> List[SiteConfig]:
"""Load site configuration from JSON.""" """Load site configuration from JSON."""
@ -160,7 +161,7 @@ class UserAssignmentManager:
conn.commit() conn.commit()
conn.close() 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.""" """Load existing assignments from JSON file into database if not already there."""
if not self.assignments_json_path.exists(): if not self.assignments_json_path.exists():
return return
@ -172,17 +173,15 @@ class UserAssignmentManager:
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() 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 user_id, sites_dict in assignments.items():
for site_url, image_indices in sites_dict.items(): try:
# print(f"[DB] Loading assignment for user {user_id}, site {site_url}, " for site_url, image_indices in sites_dict.items():
# f"{image_indices} images")
try: print(
''' f"[DB] Loading assignment for user {user_id}, site {site_url}, "
cursor.execute(""" f"{image_indices} images"
INSERT OR IGNORE INTO user_assignments )
(user_id, site_url, image_indices)
VALUES (?, ?, ?)
""", (user_id, site_url, json.dumps(image_indices)))'''
cursor.execute( cursor.execute(
""" """
@ -195,28 +194,26 @@ class UserAssignmentManager:
(user_id, site_url, json.dumps(image_indices)), (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" 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) INSERT INTO user_info (user_id, user_name)
VALUES (?, ?) VALUES (?, ?)
ON CONFLICT(user_id) DO UPDATE SET ON CONFLICT(user_id) DO UPDATE SET
user_name = excluded.user_name 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")
active_user_names[int(user_id[4:]) - 1] else None
if active_user_names and user_id.startswith("user")
else None
),
), ),
) ),
)
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
print( print(f"[DB] Error. Skipping existing assignment for user {user_id}")
f"[DB] Error. Skipping existing assignment for user {user_id}, site {site_url}" pass
)
pass
conn.commit() conn.commit()
conn.close() conn.close()
@ -243,7 +240,7 @@ class UserAssignmentManager:
if from_user_name: if from_user_name:
print(f"[DB] Looking up user_id for user_name: {user_id}") print(f"[DB] Looking up user_id for user_name: {user_id}")
cursor.execute( cursor.execute(
""" """
SELECT user_id SELECT user_id

View File

@ -18,7 +18,9 @@ from dependences.utils import (
db_persistence_startup, db_persistence_startup,
db_persistence_insert, db_persistence_insert,
return_from_env_valid, return_from_env_valid,
) )
from dependences_ui.utils import * from dependences_ui.utils import *
import logging import logging
import time import time
@ -31,18 +33,37 @@ import sqlite3
from user_task_assignment.user_assignment_manager import UserAssignmentManager 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( user_assignment_manager = UserAssignmentManager(
db_path="persistence/wcag_validator_ui.db", db_path="persistence/wcag_validator_ui.db",
config_json_path="user_task_assignment/sites_config.json", config_json_path="user_task_assignment/sites_config.json",
assignments_json_path="user_task_assignment/alt_text_assignments_output_target_overlap.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 # Get current managed users
managed_users = user_assignment_manager.get_all_user_ids() managed_users_number = user_assignment_manager.get_managed_user_count()
print(f"Currently managed users from db: {managed_users}") print(f"Currently managed users from db: {managed_users_number}")
print(f"Total managed users from db: {user_assignment_manager.get_managed_user_count()}\n") 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() user_assignment_stats = user_assignment_manager.get_statistics()
print(f"Current assignment stats:{user_assignment_stats} \n") 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")] 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: if user_state and "username" in user_state:
username = user_state["username"] 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}") print(f"Fetching assignment for user: {username}")
assignments = user_assignment_manager.get_user_assignments(username, from_user_name=True) 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( data_frame.append(
{ {
"Website URL": url, "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={}): 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) # accept different input forms from UI (DataFrame, JSON string, or list of dicts)
try: try:
@ -95,23 +189,23 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu
elif isinstance(updated_df, list): elif isinstance(updated_df, list):
updated_df = pd.DataFrame(updated_df) updated_df = pd.DataFrame(updated_df)
except Exception as e: 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"]: for column_rating_name in ["User Assessment for LLM Proposal 1", "User Assessment for LLM Proposal 2"]:
# Get the assessment column # Get the assessment column
try: try:
updated_df[column_rating_name] = updated_df[column_rating_name].astype(int) updated_df[column_rating_name] = updated_df[column_rating_name].astype(int)
except ValueError: 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: 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: 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 ( if (updated_df[column_rating_name] < 1).any() or (
updated_df[column_rating_name] > 5 updated_df[column_rating_name] > 5
).any(): ).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") dataframe_json = updated_df.to_json(orient="records")
connection_db = sqlite3.connect(db_path) 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, page_url=url,
user=json_user_str, user=json_user_str,
llm_model="", 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, json_out_str=dataframe_json,
table="wcag_user_assessments", table="wcag_user_assessments",
) )
@ -135,11 +229,27 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu
finally: finally:
if connection_db: if connection_db:
connection_db.close() 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""" """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: try:
data = json_input data = json_input
@ -250,6 +360,17 @@ def load_images_from_json(json_input):
url = img_data.get("url", "") url = img_data.get("url", "")
alt_text = img_data.get("alt_text", "No description") 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""" html += f"""
<div class="image-card"> <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'"> <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 panel = document.getElementById('panel-{idx}');
const checkedCount = document.querySelectorAll('.image-checkbox:checked').length; const checkedCount = document.querySelectorAll('.image-checkbox:checked').length;
if (this.checked) {{ if (this.checked) {{
if (checkedCount > 3) {{ if (checkedCount > 6) {{
this.checked = false; this.checked = false;
alert('Maximum 3 images can be selected!'); alert('Maximum 6 images can be selected!');
return; return;
}} }}
panel.classList.add('visible'); panel.classList.add('visible');
@ -270,7 +391,7 @@ def load_images_from_json(json_input):
panel.classList.remove('visible'); panel.classList.remove('visible');
}} }}
"> ">
Select #{idx + 1} Select #{idx + 1}<span>{assigned_text}</span>
</label> </label>
<div class="alt-text">Current alt_text: {alt_text}</div> <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> <span class="radio-label">2</span>
</label> </label>
<label class="radio-option"> <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> <span class="radio-label">3</span>
</label> </label>
<label class="radio-option"> <label class="radio-option">
@ -443,8 +564,8 @@ def make_alttext_llm_assessment_api_call(
if not selected_images or len(selected_images) == 0: if not selected_images or len(selected_images) == 0:
info_text = "No images selected" info_text = "No images selected"
print("LLM assessment not started because no valid images were selected.")
return "LLM assessment not started", pd.DataFrame(), {} return "LLM assessment not started", pd.DataFrame(), {},False
# prepare data for insertion # prepare data for insertion
json_in_str = {} json_in_str = {}
@ -465,8 +586,8 @@ def make_alttext_llm_assessment_api_call(
selected_image_id.append( selected_image_id.append(
int(img["image_index"]) + 1 int(img["image_index"]) + 1
) # add the id selected (+1 for index alignment) ) # add the id selected (+1 for index alignment)
user_assessments_llm_proposal_1.append(3) # default value for now user_assessments_llm_proposal_1.append(0) # default value for now
user_assessments_llm_proposal_2.append(3) # 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_urls"] = selected_urls
json_in_str["images_alt_text_original"] = selected_alt_text_original json_in_str["images_alt_text_original"] = selected_alt_text_original
json_out_str["user_assessments"] = user_assessments json_out_str["user_assessments"] = user_assessments
@ -531,13 +652,14 @@ def make_alttext_llm_assessment_api_call(
finally: finally:
if connection_db: if connection_db:
connection_db.close() connection_db.close()
return "LLM assessment completed", info_dataframe, response return "LLM assessment completed", info_dataframe, response, True
def make_image_extraction_api_call( def make_image_extraction_api_call(
url, url,
number_of_images=30, number_of_images=30,
wcag_rest_server_url="http://localhost:8000", wcag_rest_server_url="http://localhost:8000",
user_assignment_current_status={},
): ):
print( print(
f"Making API call for image_extraction for {url} to {wcag_rest_server_url}/extract_images" 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, headers=WCAG_VALIDATOR_RESTSERVER_HEADERS,
) )
# return response # 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 return info_text, gallery_images
except Exception as e: except Exception as e:
@ -561,97 +683,101 @@ def make_image_extraction_api_call(
def render_alttext_form(df): def render_alttext_form(df):
"""Render a pandas DataFrame (or list/dict) into an editable HTML form.""" """Render a pandas DataFrame (or list/dict) into an editable HTML form."""
try: try:
if df is None: if df is None or df.empty:
return "" print("No data to render in form.")
if isinstance(df, str): html=""
df = pd.read_json(df, orient="records") return gr.update(value=html), html # return empty form
if isinstance(df, dict): if isinstance(df, str):
df = pd.DataFrame(df) df = pd.read_json(df, orient="records")
if isinstance(df, list): if isinstance(df, dict):
df = pd.DataFrame(df) df = pd.DataFrame(df)
if isinstance(df, list):
df = pd.DataFrame(df)
html = """ html = """
<style> <style>
.alttext-table { width:100%; border-collapse: collapse; } .alttext-table { width:100%; border-collapse: collapse; }
.alttext-table th, .alttext-table td { border:1px solid #ddd; padding:8px; } .alttext-table th, .alttext-table td { border:1px solid #ddd; padding:8px; }
.alttext-table th { background:#f5f5f5; } .alttext-table th { background:#f5f5f5; }
.alttext-row td { vertical-align: top; } .alttext-row td { vertical-align: top; }
.llm-select { width:auto; } .llm-select { width:auto; }
</style> </style>
<table class="alttext-table"> <table class="alttext-table">
<thead> <thead>
<tr> <tr>
<th>Image #</th> <th>Image #</th>
<th>Original Alt Text</th> <th>Original Alt Text</th>
<th>User Assessment</th> <th>User Assessment</th>
<th>User Proposed Alt Text</th> <th>User Proposed Alt Text</th>
<th>LLM Assessment 1</th> <th>LLM Assessment 1</th>
<th>LLM Proposed Alt Text 1</th> <th>LLM Proposed Alt Text 1</th>
<th>User Assessment for LLM Proposal 1</th> <th>User Assessment for LLM Proposal 1</th>
<th>LLM Assessment 2</th> <th>LLM Assessment 2</th>
<th>LLM Proposed Alt Text 2</th> <th>LLM Proposed Alt Text 2</th>
<th>User Assessment for LLM Proposal 2</th> <th>User Assessment for LLM Proposal 2</th>
</tr> </tr>
</thead> </thead>
<tbody> <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(): html += """
imgnum = row.get("Image #", "") </tbody>
orig = row.get("Original Alt Text", "") </table>
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", 3) return gr.update(value=html), html
user_llm2_ass = row.get("User Assessment for LLM Proposal 2", 3) except Exception as e:
return f"Error rendering form: {str(e)}"
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)}"
# ------- Gradio Interface -------# # ------- Gradio Interface -------#
@ -682,6 +808,9 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
llm_response_output = gr.State() llm_response_output = gr.State()
alttext_popup_html_state = gr.State("") alttext_popup_html_state = gr.State("")
user_assignment_manager_state = gr.State(value=user_assignment_manager) 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: with Modal(visible=False, allow_user_close=False) as alttext_modal:
gr.Markdown("## Alt Text LLMs Assessment Results") 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: with gr.Column(visible=False) as protected_section:
content_display = gr.Textbox( content_display = gr.Textbox(
label="Your account", lines=5, interactive=False label="Your account", lines=2, 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,
) )
logout_btn = gr.Button("Logout", variant="stop") logout_btn = gr.Button("Logout", variant="stop")
gr.Markdown("### Your Assignment")
user_assignment_status =gr.HTML(label="Your Assignment")
# end login section # 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 # Store the DataFrame in state and render a clear HTML form for user edits
alttext_info_state = gr.State() alttext_info_state = gr.State()
alttext_form = gr.HTML(label="Assessment Form") 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(): with gr.Row():
@ -835,7 +957,7 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
], ],
).then( ).then(
make_image_extraction_api_call, 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], outputs=[image_info_output, gallery_html],
).then( ).then(
fn=lambda: gr.Button(interactive=True), 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, wcag_rest_server_url_state,
user_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=""" js="""
(url_input,gallery_html) => { (url_input, gallery_html) => {
const checkboxes = document.querySelectorAll('.image-checkbox:checked'); const checkboxes = document.querySelectorAll('.image-checkbox:checked');
if (checkboxes.length === 0) { if (checkboxes.length === 0) {
alert('Please select at least one image!'); alert('Please select at least one image!');
return [url_input,JSON.stringify([])]; return [url_input, JSON.stringify([])];
} }
if (checkboxes.length > 3) { if (checkboxes.length > 6) {
alert('Please select maximum 3 images!'); alert('Please select maximum 6 images!');
return [url_input,JSON.stringify([])]; return [url_input, JSON.stringify([])];
} }
const selectedData = []; const selectedData = [];
let hasError = false; // flag to handle missing assessment
checkboxes.forEach(checkbox => { checkboxes.forEach(checkbox => {
if (hasError) return; // skip remaining iterations if error found
const index = checkbox.dataset.index; const index = checkbox.dataset.index;
const imageUrl = checkbox.dataset.imgurl; const imageUrl = checkbox.dataset.imgurl;
const originalAlt = document.querySelector('.original-alt[data-index="' + index + '"]').value; 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({ selectedData.push({
image_index: index, image_index: index,
image_url: imageUrl, image_url: imageUrl,
@ -881,56 +1016,82 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
new_alt_text: newAltText 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( ).then(
fn=render_alttext_form, fn=render_alttext_form,
inputs=[alttext_info_state], inputs=[alttext_info_state],
outputs=[alttext_form,alttext_popup_html_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( ).then(
fn=lambda html: (gr.update(value=html), Modal(visible=True)), fn=lambda html: (gr.update(value=html)),
inputs=[alttext_popup_html_state], 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 close_modal_btn.click( #the close button now save
fn=process_dataframe, fn=process_dataframe,
inputs=[db_path_state, url_input, alttext_form_data, user_state,llm_response_output], 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=""" 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 rows = document.querySelectorAll('.alttext-row');
const selectedData = []; 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 => { rows.forEach(row => {
const imgNum = row.querySelector('.img-num')?.innerText || ''; const imgNum = row.querySelector('.img-num')?.innerText || '';
const origAlt = row.querySelector('.orig-alt')?.innerText || ''; const origAlt = row.querySelector('.orig-alt')?.innerText || '';
const userAssessment = row.querySelector('.user-assessment')?.innerText || '3'; const userAssessment = row.querySelector('.user-assessment')?.innerText || '3';
const userProposed = row.querySelector('.user-proposed')?.innerText || ''; const userProposed = row.querySelector('.user-proposed')?.innerText || '';
const user_llm1_ass = row.querySelector('.user_llm1_ass')?.value || '3'; const user_llm1_ass = row.querySelector('.user_llm1_ass')?.value || '0';
const user_llm2_ass = row.querySelector('.user_llm2_ass')?.value || '3'; const user_llm2_ass = row.querySelector('.user_llm2_ass')?.value || '0';
selectedData.push({ selectedData.push({
"Image #": imgNum, "Image #": imgNum,
"Original Alt Text": origAlt, "Original Alt Text": origAlt,
"User Assessment": parseInt(userAssessment)||3, "User Assessment": parseInt(userAssessment) || 3,
"User Proposed Alt Text": userProposed, "User Proposed Alt Text": userProposed,
"User Assessment for LLM Proposal 1": parseInt(user_llm1_ass), "User Assessment for LLM Proposal 1": parseInt(user_llm1_ass),
"User Assessment for LLM Proposal 2": parseInt(user_llm2_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]; return [db_path_state, url_input, selectedData, user_state, llm_response_output];
} }
""", """,
).then( # Close button dismisses the modal ).then( # Close button dismisses the modal
fn=lambda: Modal(visible=False), fn=maybe_close_modal,
inputs=[], inputs=[process_dataframe_output_state], # gr.State that holds your condition
outputs=[alttext_modal], outputs=[alttext_modal],
js=""" 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'); const btn = document.querySelector('.close-modal-btn');
// Change button text // 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)); 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 # placed here at the end to give full contents visibility to events
# Event handlers # Event handlers
@ -961,7 +1125,7 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
alttext_assessment, alttext_assessment,
register_and_login, 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( reg_btn.click(
fn=register_user, fn=register_user,
@ -979,7 +1143,16 @@ with gr.Blocks(theme=gr.themes.Glass(), title="WCAG AI Validator") as demo:
protected_section, protected_section,
alttext_assessment, 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__": if __name__ == "__main__":