UI user task assignment
This commit is contained in:
parent
b8fa6dfd13
commit
ebbaa972e9
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'>ü</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__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue