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 ( # "
โœ… Dummy output table
", # HTML string # gr.update(visible=True), # Group visibility # gr.update(visible=False), # Download file # "**0** samples used.", # Markdown # "โœ… Done", # Status string # "Processing finished." # Progress string # ) # def classify_mulAcc(file, text, resume, email, log_callback=None, log_collector=None): # stop_flag.value = False # return threaded_batch_runner(file, text, resume, email, status, stop_flag, log_callback=log_callback, log_collector=log_collector) def make_html_table(rows): # html = """ #
#
# # # # """ html = """
""" headers = ["No.", "Sample ID", "Predicted Country", "Country Explanation", "Predicted Sample Type", "Sample Type Explanation", "Sources", "Time cost"] html += "".join( f"" for h in headers ) html += "" for idx, row in enumerate(rows, 1): # start numbering from 1 html += "" html += f"" # "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"" if header == "Sources" and isinstance(col, str): links = [f"{url.strip()}" for url in col.strip().split("\n") if url.strip()] col = "- "+"
- ".join(links) elif isinstance(col, str): # lines = [] # for line in col.split("\n"): # line = line.strip() # if not line: # continue # if line.lower().startswith("rag_llm-"): # content = line[len("rag_llm-"):].strip() # line = f"{content} (Method: RAG_LLM)" # lines.append(f"- {line}") col = col.replace("\n", "
") #col = col.replace("\t", "    ") #col = "
".join(lines) html += f"" html += "" html += "
{h}
{idx}{col}{col}
" return html # def reset_fields(): # global_stop_flag.value = False # ๐Ÿ’ก Add this to reset the flag # return ( # #gr.update(value=""), # single_accession # gr.update(value=""), # raw_text # gr.update(value=None), # file_upload # #gr.update(value=None), # resume_file # #gr.update(value="Single Accession"), # inputMode # gr.update(value=[], visible=True), # output_table # # gr.update(value="", visible=True), # output_summary # # gr.update(value="", visible=True), # output_flag # gr.update(visible=False), # status # gr.update(visible=False), # results_group # gr.update(value="", visible=False), # usage_display # gr.update(value="", visible=False), # progress_box # ) # def reset_fields(): # global_stop_flag.value = True # Reset the stop flag # return ( # gr.update(value=""), # raw_text # gr.update(value=None), # file_upload # gr.update(value=[], visible=True), # output_table # gr.update(value="", visible=True), # status โ€” reset and make visible again # gr.update(visible=False), # results_group # gr.update(value="", visible=True), # usage_display โ€” reset and make visible again # gr.update(value="", visible=True), # progress_box โ€” reset AND visible! # # report-related reset below # gr.update(value="", visible=False), # report_textbox # gr.update(visible=False), # submit_report_button # gr.update(value="", visible=False), # status_report # gr.update(value=0), # nps_slider # gr.update(value="", visible=False) # nps_output # ) def reset_fields(): global_stop_flag.value = True # Stop any running job return ( gr.update(value="", visible=True), # raw_text gr.update(value=None, visible=True), # file_upload gr.update(value=[], visible=True), # output_table gr.update(value="", visible=True), # status gr.update(visible=False), # results_group gr.update(value="", visible=True), # usage_display gr.update(value="", visible=True), # progress_box gr.update(value="", visible=False), # report_textbox gr.update(visible=False), # submit_report_button gr.update(value="", visible=False), # status_report gr.update(value="", visible=False), # processed_info gr.update(visible=False), # hide NPS modal gr.update(visible=True), # run_button โœ… restore gr.update(visible=False) # stop button ) #inputMode.change(fn=toggle_input_mode, inputs=inputMode, outputs=[single_input_group, batch_input_group]) #run_button.click(fn=classify_with_loading, inputs=[], outputs=[status]) # run_button.click( # fn=classify_dynamic, # inputs=[single_accession, file_upload, raw_text, resume_file,user_email,inputMode], # outputs=[output_table, # #output_summary, output_flag, # results_group, download_file, usage_display,status, progress_box] # ) # run_button.click( # fn=threaded_batch_runner, # #inputs=[file_upload, raw_text, resume_file, user_email], # inputs=[file_upload, raw_text, user_email], # outputs=[output_table, results_group, download_file, usage_display, status, progress_box] # ) # run_button.click( # fn=threaded_batch_runner, # inputs=[file_upload, raw_text, user_email], # outputs=[output_table, results_group, download_file, usage_display, status, progress_box], # every=0.5 # <-- this tells Gradio to expect streaming # ) # output_table = gr.HTML() # results_group = gr.Group(visible=False) # download_file = gr.File(visible=False) # usage_display = gr.Markdown(visible=False) # status = gr.Markdown(visible=False) # progress_box = gr.Textbox(visible=False) # run_button.click( # fn=threaded_batch_runner, # inputs=[file_upload, raw_text, user_email], # outputs=[output_table, results_group, download_file, usage_display, status, progress_box], # every=0.5, # streaming enabled # show_progress="full" # ) # interface.stream( # fn=threaded_batch_runner, # inputs=[file_upload, raw_text, user_email], # outputs=[output_table, results_group, download_file, usage_display, status, progress_box], # trigger=run_button, # every=0.5, # show_progress="full", # ) interface.queue() # No arguments here! # run_button.click( # fn=threaded_batch_runner, # inputs=[file_upload, raw_text, user_email], # outputs=[output_table, results_group, download_file, usage_display, status, progress_box], # concurrency_limit=1, # โœ… correct in Gradio 5.x # queue=True, # โœ… ensure the queue is used # #every=0.5 # ) # Link the button to the function # sign_in_button.click( # fn=show_email_textbox, # outputs=[user_email, output_message] # The outputs are the components to be updated # ) run_button.click( fn=threaded_batch_runner, inputs=[file_upload, raw_text, user_email], outputs=[ output_table, # 1 results_group, # 2 download_file, # 3 usage_display, # 4 status, # 5 progress_box, # 6 run_button, # 7 stop_button, # 8 reset_button, # 9 raw_text, # 10 file_upload, # 11 processed_info, # 12 nps_modal # 13 ], concurrency_limit=1, queue=True ) stop_button.click(fn=stop_batch, inputs=[], outputs=[status]) # reset_button.click( # #fn=reset_fields, # fn=lambda: ( # gr.update(value=""), gr.update(value=""), gr.update(value=None), gr.update(value=None), gr.update(value="Single Accession"), # gr.update(value=[], visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(value="", visible=False), gr.update(value="", visible=False) # ), # inputs=[], # outputs=[ # single_accession, raw_text, file_upload, resume_file,inputMode, # output_table,# output_summary, output_flag, # status, results_group, usage_display, progress_box # ] # ) #stop_button.click(fn=lambda sf: (gr.update(value="โŒ Stopping...", visible=True), setattr(sf, "value", True) or sf), inputs=[gr.State(stop_flag)], outputs=[status, gr.State(stop_flag)]) # reset_button.click( # fn=reset_fields, # inputs=[], # #outputs=[raw_text, file_upload, resume_file, output_table, status, results_group, usage_display, progress_box] # outputs=[raw_text, file_upload, output_table, status, results_group, usage_display, progress_box, # report_textbox, # submit_report_button, # status_report, nps_slider, nps_output] # ) reset_button.click( fn=reset_fields, inputs=[], outputs=[ raw_text, file_upload, output_table, status, results_group, usage_display, progress_box, report_textbox, submit_report_button, status_report, processed_info, nps_modal, run_button, stop_button ] ) # download_button.click( # fn=mtdna_backend.save_batch_output, # #inputs=[output_table, output_summary, output_flag, output_type], # inputs=[output_table, output_type], # outputs=[download_file]) # submit_feedback.click( # fn=mtdna_backend.store_feedback_to_google_sheets, # inputs=[single_accession, q1, q2, contact], outputs=feedback_status # ) report_button.click(fn=show_report_ui, outputs=[report_textbox, submit_report_button, status_report]) submit_report_button.click(fn=handle_submission, inputs=[report_textbox, user_email], outputs=[status_report, report_textbox, submit_report_button]) # submit_feedback.click( # fn=mtdna_backend.store_feedback_to_google_sheets, # inputs=[raw_text, q1, q2, contact], # outputs=[feedback_status] # ) nps_submit.click(fn=submit_nps, inputs=[user_email, nps_radio], outputs=[nps_output]) # Link each button to submit function gr.HTML(""" """) # # Custom CSS styles # gr.HTML(""" # # """) with gr.Tab("CURIOUS ABOUT THIS PRODUCT?"): gr.HTML(value=flow_chart) with gr.Tab("PRICING"): gr.HTML(value=pricing_html) interface.launch(share=True,debug=True)