import gradio as gr import mtdna_backend import json import data_preprocess, model, pipeline import os import hashlib import threading # Gradio UI #stop_flag = gr.State(value=False) class StopFlag: def __init__(self): self.value = False global_stop_flag = StopFlag() # Shared between run + stop with open("offer.html", "r", encoding="utf-8") as f: pricing_html = f.read() with open("mtdna_tool_explainer_updated.html", "r", encoding="utf-8") as f: flow_chart = f.read() # css = """ # /* NPS container for a unified background */ # #nps-container { # background-color: #333; # padding: 20px; # border-radius: 8px; # display: flex; # flex-direction: column; # width: 100%; # } # /* Question markdown styling */ # #nps-container .gr-markdown h3 { # margin-bottom: 20px; /* Adds space between the question and the numbers */ # } # /* The container for the radio buttons */ # #nps-radio-container .gr-radio-group { # display: flex; # flex-direction: row; # justify-content: space-between; # gap: 5px; # flex-wrap: nowrap; # width: 100%; # } # /* Styling for each individual button */ # #nps-radio-container .gr-radio-label { # display: flex; # justify-content: center; # align-items: center; # width: 35px; # height: 35px; # border-radius: 4px; # background-color: #555; # color: white; # font-weight: bold; # cursor: pointer; # transition: background-color 0.2s ease; # font-size: 14px; # } # #nps-radio-container .gr-radio-label:hover { # background-color: #777; # } # #nps-radio-container input[type="radio"]:checked + .gr-radio-label { # background-color: #999; # border: 2px solid white; # } # #nps-radio-container .gr-radio-input { # display: none; # } # /* Adjusting the text labels for "Not likely" and "Extremely likely" */ # #nps-labels-row { # display: flex; # justify-content: space-between; # margin-top: 15px; /* Adds more space below the numbers */ # color: #ccc; # width: 100%; # } # #nps-labels-row p { # margin: 0; # font-size: 1.0em; # white-space: nowrap; # width: 50%; /* Ensures each label takes up half the row */ # } # #nps-labels-row p:first-child { # text-align: left; # } # #nps-labels-row p:last-child { # text-align: right; # } # #nps-submit-button { # margin-top: 25px; /* Adds a larger space above the submit button */ # width: 100%; # } # #nps-submit-button:active { # border-color: white !important; # box-shadow: 0 0 5px white inset; # }""" css = """ /* The main container for the entire NPS section */ #nps-container { background-color: #333; padding: 20px; border-radius: 8px; display: flex; flex-direction: column; width: 100%; } /* Ensure the question text is properly spaced */ #nps-container h3 { color: #fff; margin-bottom: 20px; /* Space between question and buttons */ text-align: center; /* Center the question text */ } /* Flexbox container for the radio buttons */ #nps-radio-container { width: 100%; display: flex; justify-content: space-between; align-items: center; } /* Ensure the inner Gradio radio group stretches to fill the container */ #nps-radio-container > div.gr-radio-group { width: 100% !important; display: flex !important; justify-content: space-between !important; } /* Styling for each individual button */ #nps-radio-container .gr-radio-label { display: flex; justify-content: center; align-items: center; width: 35px; height: 35px; border-radius: 4px; background-color: #555; color: white; font-weight: bold; cursor: pointer; transition: background-color 0.2s ease; font-size: 14px; margin: 0; /* Remove default button margins */ } #nps-radio-container .gr-radio-label:hover { background-color: #777; } #nps-radio-container input[type="radio"]:checked + .gr-radio-label { background-color: #999; border: 2px solid white; } #nps-radio-container .gr-radio-input { display: none; } /* The row for the "Not likely" and "Extremely likely" labels */ #nps-labels-row { display: flex; justify-content: space-between; margin-top: 15px; /* Adds space below the number buttons */ width: 100%; /* Force labels row to take full width */ } #nps-labels-row .gr-markdown p { margin: 0; font-size: 1.0em; color: #ccc; white-space: nowrap; width: 50%; } #nps-labels-row .gr-markdown:first-child p { text-align: left; } #nps-labels-row .gr-markdown:last-child p { text-align: right; } /* Submit button styling */ #nps-submit-button { margin-top: 25px; /* Adds space above the submit button */ width: 100%; } #nps-submit-button:active { border-color: white !important; box-shadow: 0 0 5px white inset; } """ with gr.Blocks() as interface: # with gr.Tab("CURIOUS ABOUT THIS PRODUCT?"): # gr.HTML(value=pricing_html) with gr.Tab("๐งฌ Classifier"): gr.Markdown("# ๐งฌ mtDNA Location Classifier (MVP)") #inputMode = gr.Radio(choices=["Single Accession", "Batch Input"], value="Single Accession", label="Choose Input Mode") user_email = gr.Textbox(label="๐ง Your email (used to track free quota). ", placeholder="Enter your email and click Submit and Classify button below to run accessions.\nYou'll get +20 extra free queries and Excel-formatted results.") # sign_in_button = gr.Button("Sign in to Download") # user_email = gr.Textbox( # label="๐ง Your email (used to track free quota)", # visible=False # ) # # The output will be used to display a message to the user # output_message = gr.Textbox(visible=False, interactive=False) usage_display = gr.Markdown("", visible=False) # with gr.Group() as single_input_group: # single_accession = gr.Textbox(label="Enter Single Accession (e.g., KU131308)") # with gr.Group(visible=False) as batch_input_group: # raw_text = gr.Textbox(label="๐งฌ Paste Accession Numbers (e.g., MF362736.1,MF362738.1,KU131308,MW291678)") # resume_file = gr.File(label="๐๏ธ Previously saved Excel output (optional)", file_types=[".xlsx"], interactive=True) # gr.HTML("""Download Example CSV Format""") # gr.HTML("""Download Example Excel Format""") # file_upload = gr.File(label="๐ Or Upload CSV/Excel File", file_types=[".csv", ".xlsx"], interactive=True, elem_id="file-upload-box") raw_text = gr.Textbox(label="๐ง Input Accession Number(s) (single (KU131308) or comma-separated (e.g., MF362736.1,MF362738.1,KU131308,MW291678))") #resume_file = gr.File(label="๐๏ธ Previously saved Excel output (optional)", file_types=[".xlsx"], interactive=True) gr.HTML("""Example Excel Input Template""") file_upload = gr.File(label="๐ Or Upload Excel File", file_types=[".xlsx"], interactive=True) processed_info = gr.Markdown(visible=False) # new placeholder for processed list with gr.Row(): run_button = gr.Button("๐ Submit and Classify", elem_id="run-btn") stop_button = gr.Button("โ Stop Batch", visible=False, elem_id="stop-btn") reset_button = gr.Button("๐ Reset", elem_id="reset-btn") status = gr.Markdown(visible=False) # with gr.Group(visible=False, elem_id="nps-overlay") as nps_modal: # with gr.Column(elem_id="nps-box"): # gr.Markdown("### How likely are you to recommend this tool to a colleague or peer?") # nps_slider = gr.Slider(minimum=0, maximum=10, step=1, label="Select score: 0-10 (0-6: not likely or low; 7-8: neutral; 9-10: likely or highly)") # nps_submit = gr.Button("Submit") # nps_output = gr.Textbox(label="", interactive=False, visible=True) # Start empty with gr.Group(visible=False) as results_group: # with gr.Accordion("Open to See the Result", open=False) as results: # with gr.Row(): # output_summary = gr.Markdown(elem_id="output-summary") # output_flag = gr.Markdown(elem_id="output-flag") # gr.Markdown("---") with gr.Accordion("Open to See the Output Table", open=True) as table_accordion: output_table = gr.HTML(render=True) #with gr.Row(): #output_type = gr.Dropdown(choices=["Excel", "JSON", "TXT"], label="Select Output Format", value="Excel") #download_button = gr.Button("โฌ๏ธ Download Output") #download_file = gr.File(label="Download File Here",visible=False) # Use gr.Markdown to add a visual space gr.Markdown(" ") # A simple blank markdown can create space report_button = gr.Button("Report inaccurate output to receive 1 extra free query",elem_id="run-btn") report_textbox = gr.Textbox( label="Describe the issue", lines=4, placeholder="e.g. DQ981467: it gives me unknown when I can in fact search it on NCBI \n DQ981467: cannot find the result in batch output when the live processing did show already processed", visible=False) submit_report_button = gr.Button("Submit", visible=False, elem_id="run-btn") status_report = gr.Markdown(visible=False) # Use gr.Markdown to add a visual space gr.Markdown(" ") # A simple blank markdown can create space download_file = gr.File(label="Download File Here", visible=False, interactive=True) gr.Markdown(" ") # A simple blank markdown can create space with gr.Group(visible=True, elem_id="nps-overlay") as nps_modal: #with gr.Column(elem_id="nps-box"): with gr.Group(elem_id="nps-container"): gr.Markdown("### How likely are you to recommend this tool to a colleague or peer?") # # Use gr.Radio to create clickable buttons with gr.Column(elem_id="nps-radio-container"): nps_radio = gr.Radio( choices=[str(i) for i in range(11)], label="Select score:", interactive=True, container=False ) # The "Not likely" and "Extremely likely" labels with gr.Row(elem_id="nps-labels-row"): gr.Markdown("Not likely") gr.Markdown("Extremely likely") nps_submit = gr.Button("Submit", elem_id="nps-submit-button") nps_output = gr.Textbox(label="", interactive=False, visible=True) gr.Markdown(" ") # A simple blank markdown can create space progress_box = gr.Textbox(label="Live Processing Log", lines=20, interactive=False) gr.Markdown("---") # gr.Markdown("### ๐ฌ Feedback (required)") # q1 = gr.Textbox(label="1๏ธโฃ Was the inferred location accurate or helpful? Please explain.") # q2 = gr.Textbox(label="2๏ธโฃ What would improve your experience with this tool?") # contact = gr.Textbox(label="๐ง Your email or institution (optional)") # submit_feedback = gr.Button("โ Submit Feedback") # feedback_status = gr.Markdown() # Functions # def toggle_input_mode(mode): # if mode == "Single Accession": # return gr.update(visible=True), gr.update(visible=False) # else: # return gr.update(visible=False), gr.update(visible=True) # def show_email_textbox(): # # Return a gr.update() to make the textbox visible and set the message # return gr.update(visible=True), gr.update(value="Give your email to download excel output and 20+ more free samples", visible=True) def classify_with_loading(): return gr.update(value="โณ Please wait... processing...",visible=True) # Show processing message # def classify_dynamic(single_accession, file, text, resume, email, mode): # if mode == "Single Accession": # return classify_main(single_accession) + (gr.update(visible=False),) # else: # #return summarize_batch(file, text) + (gr.update(visible=False),) # Hide processing message # return classify_mulAcc(file, text, resume) + (gr.update(visible=False),) # Hide processing message # Logging helpers defined early to avoid NameError # def classify_dynamic(single_accession, file, text, resume, email, mode): # if mode == "Single Accession": # return classify_main(single_accession) + (gr.update(value="", visible=False),) # else: # return classify_mulAcc(file, text, resume, email, log_callback=real_time_logger, log_collector=log_collector) # for single accession # def classify_main(accession): # #table, summary, labelAncient_Modern, explain_label = mtdna_backend.summarize_results(accession) # table = mtdna_backend.summarize_results(accession) # #flag_output = f"### ๐บ Ancient/Modern Flag\n**{labelAncient_Modern}**\n\n_Explanation:_ {explain_label}" # return ( # #table, # make_html_table(table), # # summary, # # flag_output, # gr.update(visible=True), # gr.update(visible=False), # gr.update(visible=False) # ) #stop_flag = gr.State(value=False) #stop_flag = StopFlag() # def stop_batch(stop_flag): # stop_flag.value = True # return gr.update(value="โ Stopping...", visible=True), stop_flag def stop_batch(): global_stop_flag.value = True return gr.update(value="โ Stopping...", visible=True) # def threaded_batch_runner(file, text, email): # global_stop_flag.value = False # log_lines = [] # def update_log(line): # log_lines.append(line) # yield ( # gr.update(visible=False), # output_table (not yet) # gr.update(visible=False), # results_group # gr.update(visible=False), # download_file # gr.update(visible=False), # usage_display # gr.update(value="โณ Still processing...", visible=True), # status # gr.update(value="\n".join(log_lines)) # progress_box # ) # # Start a dummy update to say "Starting..." # yield from update_log("๐ Starting batch processing...") # rows, file_path, count, final_log, warning = mtdna_backend.summarize_batch( # file=file, # raw_text=text, # resume_file=None, # user_email=email, # stop_flag=global_stop_flag, # yield_callback=lambda line: (yield from update_log(line)) # ) # html = make_html_table(rows) # file_update = gr.update(value=file_path, visible=True) if os.path.exists(file_path) else gr.update(visible=False) # usage_or_warning_text = f"**{count}** samples used by this email." if email.strip() else warning # yield ( # html, # gr.update(visible=True), # results_group # file_update, # download_file # gr.update(value=usage_or_warning_text, visible=True), # gr.update(value="โ Done", visible=True), # gr.update(value=final_log) # ) # def threaded_batch_runner(file=None, text="", email=""): # print("๐ง EMAIL RECEIVED:", email) # import tempfile # from mtdna_backend import ( # extract_accessions_from_input, # summarize_results, # save_to_excel, # hash_user_id, # increment_usage, # ) # import os # global_stop_flag.value = False # reset stop flag # tmp_dir = tempfile.mkdtemp() # output_file_path = os.path.join(tmp_dir, "batch_output_live.xlsx") # limited_acc = 50 + (10 if email.strip() else 0) # # Step 1: Parse input # accessions, error = extract_accessions_from_input(file, text) # print(accessions) # if error: # yield ( # "", # output_table # gr.update(visible=False), # results_group # gr.update(visible=False), # download_file # "", # usage_display # "โ Error", # status # str(error) # progress_box # ) # return # total = len(accessions) # if total > limited_acc: # accessions = accessions[:limited_acc] # warning = f"โ ๏ธ Only processing first {limited_acc} accessions." # else: # warning = f"โ All {total} accessions will be processed." # all_rows = [] # processed_accessions = 0 # โ tracks how many accessions were processed # email_tracked = False # log_lines = [] # # Step 2: Loop through accessions # for i, acc in enumerate(accessions): # if global_stop_flag.value: # log_lines.append(f"๐ Stopped at {acc} ({i+1}/{total})") # usage_text = "" # if email.strip() and not email_tracked: # # user_hash = hash_user_id(email) # # usage_count = increment_usage(user_hash, len(all_rows)) # print("print(processed_accessions at stop) ",processed_accessions) # usage_count = increment_usage(email, processed_accessions) # email_tracked = True # usage_text = f"**{usage_count}** samples used by this email. Ten more samples are added first (you now have 60 limited accessions), then wait we will contact you via this email." # else: # usage_text = f"The limited accession is 50. The user has used {processed_accessions}, and only {50-processed_accessions} left." # yield ( # make_html_table(all_rows), # gr.update(visible=True), # gr.update(value=output_file_path, visible=True), # gr.update(value=usage_text, visible=True), # "๐ Stopped", # "\n".join(log_lines) # ) # return # log_lines.append(f"[{i+1}/{total}] Processing {acc}") # yield ( # make_html_table(all_rows), # gr.update(visible=True), # gr.update(visible=False), # "", # "โณ Processing...", # "\n".join(log_lines) # ) # try: # print(acc) # rows = summarize_results(acc) # all_rows.extend(rows) # processed_accessions += 1 # โ count only successful accessions # save_to_excel(all_rows, "", "", output_file_path, is_resume=False) # log_lines.append(f"โ Processed {acc} ({i+1}/{total})") # except Exception as e: # log_lines.append(f"โ Failed to process {acc}: {e}") # yield ( # make_html_table(all_rows), # gr.update(visible=True), # gr.update(visible=False), # "", # "โณ Processing...", # "\n".join(log_lines) # ) # # Final update # usage_text = "" # if email.strip() and not email_tracked: # # user_hash = hash_user_id(email) # # usage_count = increment_usage(user_hash, len(all_rows)) # print("print(processed_accessions final) ",processed_accessions) # usage_count = increment_usage(email, processed_accessions) # usage_text = f"**{usage_count}** samples used by this email. Ten more samples are added first (you now have 60 limited accessions), then wait we will contact you via this email." # elif not email.strip(): # usage_text = f"The limited accession is 50. The user has used {processed_accessions}, and only {50-processed_accessions} left." # yield ( # make_html_table(all_rows), # gr.update(visible=True), # gr.update(value=output_file_path, visible=True), # gr.update(value=usage_text, visible=True), # "โ Done", # "\n".join(log_lines) # ) def submit_nps(email,nps_score): if nps_score is None: return "โ Please select a score before submitting." log_submission_to_gsheet(email, [], nps_score) return "โ Thanks for submitting your feedback!" def log_submission_to_gsheet(email, samples, nps_score=None): from datetime import datetime, timezone import json, os, gspread from oauth2client.service_account import ServiceAccountCredentials import uuid timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") if not email.strip(): email = f"anonymous_{str(uuid.uuid4())[:8]}" try: creds_dict = json.loads(os.environ["GCP_CREDS_JSON"]) scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"] creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope) client = gspread.authorize(creds) sheet = client.open("user_usage_log") worksheet = sheet.sheet1 # Main sheet data = worksheet.get_all_values() headers = data[0] email_col = headers.index("email") samples_col = headers.index("samples") recent_time_col = headers.index("recent_time") nps_col = headers.index("nps_score") if "nps_score" in headers else -1 print("this is nps col: ", nps_col) # Step 1: Find row matching the email for i, row in enumerate(data[1:], start=2): # start=2 for correct row indexing if row[email_col].strip().lower() == email.strip().lower(): old_samples = row[samples_col].strip() if len(row) > samples_col else "" old_sample_list = [s.strip() for s in old_samples.split(",") if s.strip()] all_samples = list(dict.fromkeys(old_sample_list + samples)) # deduplicate while preserving order new_sample_string = ", ".join(all_samples) # Update recent_time to store history old_timestamp = row[recent_time_col].strip() if len(row) > recent_time_col else "" if old_timestamp: new_timestamp = f"{old_timestamp}, {timestamp}" else: new_timestamp = timestamp worksheet.update_cell(i, samples_col + 1, new_sample_string) worksheet.update_cell(i, recent_time_col + 1, str(new_timestamp)) if nps_score is not None: print("this is nps score:", nps_score) old_nps = row[nps_col].strip() if len(row) > nps_col else "" if old_nps: new_nps = f"{old_nps},{nps_score}" else: new_nps = str(nps_score) worksheet.update_cell(i, nps_col + 1, str(new_nps)) print(f"โ Updated existing user row for: {email}") return # Step 2: If email not found, add new row new_row = [""] * len(headers) new_row[email_col] = email new_row[samples_col] = ", ".join(samples) new_row[recent_time_col] = timestamp if nps_col != -1: if len(new_row) <= nps_col: new_row.extend([""] * (nps_col + 1 - len(new_row))) new_row[nps_col] = str(nps_score) if nps_score is not None else "" worksheet.append_row(new_row) print(f"โ Appended new user row for: {email}") except Exception as e: print(f"โ Failed to log submission to Google Sheets: {e}") import multiprocessing import time def run_with_timeout(func, args=(), kwargs={}, timeout=30, stop_value=None): """ Runs func in a separate process with optional timeout. If stop_value is provided and becomes True during execution, the process is killed early. """ def wrapper(q, *args, **kwargs): try: result = func(*args, **kwargs) q.put((True, result)) except Exception as e: q.put((False, e)) q = multiprocessing.Queue() p = multiprocessing.Process(target=wrapper, args=(q, *args), kwargs=kwargs) p.start() start_time = time.time() while p.is_alive(): # Timeout check if timeout is not None and (time.time() - start_time) > timeout: p.terminate() p.join() print(f"โฑ๏ธ Timeout exceeded ({timeout} sec) โ function killed.") return False, None # Stop flag check # if stop_value is not None and stop_value.value: # p.terminate() # p.join() # print("๐ Stop flag detected โ function killed early.") # return False, None if stop_value is not None and stop_value.value: print("๐ Stop flag detected โ waiting for child to exit gracefully.") p.join(timeout=3) # short wait for graceful exit if p.is_alive(): print("โ ๏ธ Child still alive, forcing termination.") p.terminate() p.join() return False, None time.sleep(0.1) # avoid busy waiting # Process finished naturally if not q.empty(): success, result = q.get() if success: return True, result else: raise result return False, None def threaded_batch_runner(file=None, text="", email=""): print("๐ง EMAIL RECEIVED:", repr(email)) import tempfile from mtdna_backend import ( extract_accessions_from_input, summarize_results, save_to_excel, increment_usage, ) import os global_stop_flag.value = False # reset stop flag tmp_dir = tempfile.mkdtemp() output_file_path = os.path.join(tmp_dir, "batch_output_live.xlsx") #output_file_path = "/mnt/data/batch_output_live.xlsx" all_rows = [] processed_accessions = 0 # โ track successful accessions email_tracked = False log_lines = [] usage_text = "" processed_info = "" if not email.strip(): output_file_path = None#"Write your email so that you can download the outputs." log_lines.append("๐ฅ Provide your email to receive a downloadable Excel report and get 20 more free queries.") limited_acc = 30 if email.strip(): usage_count, max_allowed = increment_usage(email, processed_accessions) if int(usage_count) >= int(max_allowed): log_lines.append("โ You have reached your quota. Please contact us to unlock more.") # Minimal blank yield to trigger UI rendering yield ( make_html_table([]), # 1 output_table gr.update(visible=True), # 2 results_group gr.update(visible=False), # 3 download_file gr.update(value="", visible=True), # 4 usage_display "โ๏ธ Quota limit", # 5 status "โ๏ธ Quota limit", # 6 progress_box gr.update(visible=True), # 7 run_button gr.update(visible=False), # 8 stop_button gr.update(visible=True), # 9 reset_button gr.update(visible=True), # 10 raw_text gr.update(visible=True), # 11 file_upload gr.update(value=processed_info, visible=False), # 12 processed_info gr.update(visible=False) # 13 nps_modal ) # Actual warning frame yield ( make_html_table([]), gr.update(visible=False), gr.update(visible=False), gr.update(value="โ You have reached your quota. Please contact us to unlock more.", visible=True), "โ Quota Exceeded", "\n".join(log_lines), gr.update(visible=True), gr.update(visible=False), gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(value="", visible=False), gr.update(visible=False) ) return limited_acc = int(max_allowed-usage_count) # Step 1: Parse input accessions, error = extract_accessions_from_input(file, text) total = len(accessions) print("total len original accessions: ", total) if total > limited_acc: accessions = accessions[:limited_acc] warning = f"โ ๏ธ Only processing first {limited_acc} accessions." else: warning = f"โ All {total} accessions will be processed." if len(accessions) == 1: processed_info = warning + "\n" +f"Processed accessions: {accessions[0]}" else: processed_info = warning + "\n" +f"Processed accessions: {accessions[0]}...{accessions[-1]}" ### NEW: Hide inputs, show processed_info at start yield ( make_html_table(all_rows), # output_table gr.update(visible=False), # results_group gr.update(visible=False), # download_file "", # usage_display "โณ Processing...", # status "", # progess_box gr.update(visible=False), # run_button, gr.update(visible=True), # show stop button gr.update(visible=True), # show reset button gr.update(visible=True), # hide raw_text gr.update(visible=True), # hide file_upload gr.update(value=processed_info, visible=True), # processed_info gr.update(visible=False) # hide NPS modal at start ) log_submission_to_gsheet(email, accessions) print("๐งช Accessions received:", accessions) if error: yield ( "", # 1 output_table gr.update(visible=False), # 2 results_group gr.update(visible=False), # 3 download_file "", # 4 usage_display "โ Error", # 5 status str(error), # 6 progress_box gr.update(visible=True), # 7 run_button gr.update(visible=False), # 8 stop_button gr.update(visible=True), # 9 reset_button gr.update(visible=True), # 10 raw_text gr.update(visible=True), # 11 file_upload gr.update(value="", visible=False), # 12 processed_info gr.update(visible=False) # 13 nps_modal ) return # all_rows = [] # processed_accessions = 0 # โ track successful accessions # email_tracked = False # log_lines = [] # if not email.strip(): # output_file_path = None#"Write your email so that you can download the outputs." # log_lines.append("๐ฅ Provide your email to receive a downloadable Excel report and get 20 more free queries.") # if email.strip(): # usage_count, max_allowed = increment_usage(email, processed_accessions) # if int(usage_count) > int(max_allowed): # log_lines.append("โ You have reached your quota. Please contact us to unlock more.") # # Minimal blank yield to trigger UI rendering # yield ( # make_html_table([]), # gr.update(visible=True), # gr.update(visible=False), # gr.update(value="", visible=True), # "โ๏ธ Quota limit", # "โ๏ธ Quota limit" # ) # # Actual warning frame # yield ( # make_html_table([]), # gr.update(visible=False), # gr.update(visible=False), # gr.update(value="โ You have reached your quota. Please contact us to unlock more.", visible=True), # "โ Quota Exceeded", # "\n".join(log_lines) # ) # return # Step 2: Loop through accessions for i, acc in enumerate(accessions): try: if global_stop_flag.value: log_lines.append(f"๐ Stopped at {acc} ({i+1}/{total})") usage_text = "" if email.strip() and not email_tracked: print(f"๐งช increment_usage at STOP: {email=} {processed_accessions=}") usage_count, max_allowed = increment_usage(email, processed_accessions) email_tracked = True usage_text = f"**{usage_count}**/{max_allowed} allowed samples used by this email." #Ten more samples are added first (you now have 60 limited accessions), then wait we will contact you via this email." else: usage_text = f"The limited accession is 30. The user has used {processed_accessions}, and only {30 - processed_accessions} left." # yield ( # make_html_table(all_rows), # gr.update(visible=True), # #gr.update(value=output_file_path, visible=True), # gr.update(value=output_file_path, visible=bool(output_file_path)), # gr.update(value=usage_text, visible=True), # "๐ Stopped", # "\n".join(log_lines) # ) yield ( make_html_table(all_rows), gr.update(visible=True), # results_group gr.update(value=output_file_path, visible=bool(output_file_path)), # download_file gr.update(value=usage_text, visible=True), # usage_display "๐ Stopped", # "โ Done" or "๐ Stopped" "\n".join(log_lines), gr.update(visible=False), # run_button gr.update(visible=False), # stop_button gr.update(visible=True), # reset_button gr.update(visible=True), # raw_text gr.update(visible=True), # file_upload gr.update(value=processed_info, visible=False), # processed_info gr.update(visible=True) # NPS modal now visible ) return log_lines.append(f"[{i+1}/{total}] Processing {acc}") # yield ( # make_html_table(all_rows), # gr.update(visible=True), # gr.update(visible=False), # "", # "โณ Processing...", # "\n".join(log_lines) # ) # Hide inputs, show processed_info at start yield ( make_html_table(all_rows), # output_table gr.update(visible=True), # results_group gr.update(visible=False), # download_file "", # usage_display "โณ Processing...", # status "\n".join(log_lines), # progress_box gr.update(visible=False), # run_button gr.update(visible=True), # stop_button gr.update(visible=True), # reset_button gr.update(visible=True), # hide raw_text gr.update(visible=True), # hide file_upload gr.update(value=processed_info, visible=True), # processed_info gr.update(visible=False) # hide NPS modal at start ) # try: # print("๐ Processing accession:", acc) # rows = summarize_results(acc) # all_rows.extend(rows) # processed_accessions += 1 # โ only count success # if email.strip(): # save_to_excel(all_rows, "", "", output_file_path, is_resume=False) # log_lines.append(f"โ Processed {acc} ({i+1}/{total})") print("๐ Processing accession:", acc) # --- Before calling summarize_results --- samples_left = total - i # including current one estimated_seconds_left = samples_left * 100 # your observed average per sample log_lines.append( f"Running... usually ~100s per sample" ) log_lines.append( f"โณ Estimated time left: ~{estimated_seconds_left} seconds ({samples_left} sample{'s' if samples_left > 1 else ''} remaining)" ) # Yield update to UI before the heavy pipeline call yield ( make_html_table(all_rows), gr.update(visible=True), # results_group gr.update(visible=False), # download_file "", # usage_display "โณ Processing...", # status "\n".join(log_lines), # progress_box gr.update(visible=False), # run_button gr.update(visible=True), # stop_button gr.update(visible=True), # reset_button gr.update(visible=True), # raw_text gr.update(visible=True), # file_upload gr.update(value=processed_info, visible=True), # processed_info gr.update(visible=False) # hide NPS modal ) # Run summarize_results in a separate process with stop flag support success, rows = run_with_timeout( summarize_results, args=(acc,), timeout=None, # or set max seconds per sample if you want stop_value=global_stop_flag ) # If stop was pressed during this accession if not success and global_stop_flag.value: log_lines.append(f"๐ Cancelled {acc} before completion") # yield ( # make_html_table(all_rows), # gr.update(visible=True), # gr.update(visible=False), # "", # "๐ Stopped", # "\n".join(log_lines) # ) yield ( make_html_table(all_rows), gr.update(visible=True), # results_group gr.update(value=output_file_path, visible=bool(output_file_path)), # download_file gr.update(value=usage_text, visible=True), # usage_display "๐ Stopped", # "โ Done" or "๐ Stopped" "\n".join(log_lines), gr.update(visible=False), # run_button gr.update(visible=False), # stop_button gr.update(visible=True), # reset_button gr.update(visible=True), # raw_text gr.update(visible=True), # file_upload gr.update(value="", visible=False), # processed_info gr.update(visible=True) # NPS modal now visible ) break # stop processing entirely # If it finished normally if success and rows: all_rows.extend(rows) processed_accessions += 1 if email.strip(): save_to_excel(all_rows, "", "", output_file_path, is_resume=False) log_lines.append(f"โ Processed {acc} ({i+1}/{total})") else: # If it failed due to timeout or other error if not global_stop_flag.value: log_lines.append(f"โ ๏ธ Skipped {acc} due to timeout or error") # Always yield updated logs after each attempt # yield ( # make_html_table(all_rows), # gr.update(visible=True), # gr.update(visible=False), # "", # "โณ Processing...", # "\n".join(log_lines) # ) yield ( make_html_table(all_rows), # output_table gr.update(visible=True), # results_group gr.update(visible=False), # download_file "", # usage_display "โณ Processing...", # status "\n".join(log_lines), # progress_box gr.update(visible=False), # run_button gr.update(visible=True), # stop_button gr.update(visible=True), # reset_button gr.update(visible=True), # hide raw_text gr.update(visible=True), # hide file_upload gr.update(value=processed_info, visible=True), # processed_info gr.update(visible=False) # hide NPS modal at start ) except Exception as e: log_lines.append(f"โ Failed to process {acc}: {e}. Report on the box above so that we won't count this bad one for you (email required).") yield ( make_html_table(all_rows), # output_table gr.update(visible=True), # results_group gr.update(visible=False), # download_file "", # usage_display "โณ Processing...", # status "\n".join(log_lines), # progress_box gr.update(visible=False), # run_button gr.update(visible=True), # stop_button gr.update(visible=True), # reset_button gr.update(visible=True), # hide raw_text gr.update(visible=True), # hide file_upload gr.update(value=processed_info, visible=True), # processed_info gr.update(visible=False) # hide NPS modal at start ) # Step 3: Final usage update usage_text = "" if email.strip() and not email_tracked: print(f"๐งช increment_usage at END: {email=} {processed_accessions=}") usage_count, max_allowed = increment_usage(email, processed_accessions) email_tracked = True usage_text = f"**{usage_count}**/{max_allowed} allowed samples used by this email." #Ten more samples are added first (you now have 60 limited accessions), then wait we will contact you via this email." elif not email.strip(): usage_text = f"The limited accession is 30. The user has used {processed_accessions}, and only {30 - processed_accessions} left." # yield ( # make_html_table(all_rows), # gr.update(visible=True), # #gr.update(value=output_file_path, visible=True), # gr.update(value=output_file_path, visible=bool(output_file_path)), # gr.update(value=usage_text, visible=True), # "โ Done", # "\n".join(log_lines) # ) yield ( make_html_table(all_rows), gr.update(visible=True), # results_group gr.update(value=output_file_path, visible=bool(output_file_path)), # download_file gr.update(value=usage_text, visible=True), # usage_display "โ Done", # "โ Done" or "๐ Stopped" "\n".join(log_lines), gr.update(visible=False), # run_button gr.update(visible=False), # stop_button gr.update(visible=True), # reset_button gr.update(visible=True), # raw_text gr.update(visible=True), # file_upload gr.update(value=processed_info, visible=True), # processed_info gr.update(visible=True) # NPS modal now visible ) # SUBMIT REPORT UI # 1. Google Sheets setup def get_worksheet(sheet_name="Report"): import os, json import gspread from oauth2client.service_account import ServiceAccountCredentials try: creds_dict = json.loads(os.environ["GCP_CREDS_JSON"]) scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"] creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope) client = gspread.authorize(creds) sheet = client.open(sheet_name).sheet1 return sheet except Exception as e: print(f"โ Error loading Google Sheet '{sheet_name}':", e) return None # 2. Submit function to send report to the Google Sheet def submit_report(report_text,user_email=""): try: sheet = get_worksheet() # โ Parse the report_text (each line like 'ACCESSION: message') lines = report_text.strip().split('\n') user = "" if user_email.strip(): user = user_email for line in lines: if ':' in line: accession, message = line.split(':', 1) sheet.append_row([accession.strip(), message.strip(), user.strip()]) return "โ Report submitted successfully!" except Exception as e: return f"โ Error submitting report: {str(e)}" def show_report_ui(): return gr.update(visible=True), gr.update(visible=True), gr.update(visible=False) def handle_submission(text,user_email): msg = submit_report(text, user_email) return gr.update(value=msg, visible=True), gr.update(visible=False), gr.update(visible=False) # def threaded_batch_runner(file=None, text="", email=""): # global_stop_flag.value = False # # Dummy test output that matches expected schema # return ( # "
{h} | " for h in headers ) html += "" for idx, row in enumerate(rows, 1): # start numbering from 1 html += "||
---|---|---|
{idx} | " # "No." column for i, col in enumerate(row): header = headers[i] style = "padding: 10px; border: 1px solid #555; vertical-align: top;" # For specific columns like Haplogroup, force nowrap if header in ["Country Explanation", "Sample Type Explanation"]: style += " max-width: 400px; word-wrap: break-word; white-space: normal;" elif header in ["Sample ID", "Predicted Country", "Predicted Sample Type", "Time cost"]: style += " white-space: nowrap; text-overflow: ellipsis; max-width: 200px; overflow: hidden;" # if header == "Sources" and isinstance(col, str) and col.strip().lower().startswith("http"): # col = f"{col}" #html += f"{col} | " if header == "Sources" and isinstance(col, str): links = [f"{url.strip()}" for url in col.strip().split("\n") if url.strip()] col = "- "+"{col} | " html += "