diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,1504 +1,1511 @@ -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 - active_processes = [] - 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) - active_processes.append(p) # โœ… track it - 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(timeout=2) - 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 cleanup_processes(): - global active_processes - print("inside cleanup process and number of active process: ", len(active_processes)) - for p in active_processes: - if p.is_alive(): - try: - p.terminate() - p.join(timeout=2) - except Exception: - pass - active_processes = [] - - - def threaded_batch_runner(file=None, text="", email=""): - print("clean everything remain before running") - cleanup_processes() - 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 - #active_processes = [] - - 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, invalid_accessions, error = extract_accessions_from_input(file, text) - total = len(accessions) - print("total len original accessions: ", total) - if total > 0: - if total > limited_acc: - accessions = accessions[:limited_acc] - if invalid_accessions: - warning = f"โš ๏ธ Only processing first {limited_acc} accessions. โš ๏ธ Invalid accessions: {', '.join(invalid_accessions)}." - - else: - warning = f"โš ๏ธ Only processing first {limited_acc} accessions." - else: - if invalid_accessions: - warning = f"โœ… All {total} accessions will be processed. โš ๏ธ Invalid accessions: {', '.join(invalid_accessions)}." - else: - warning = f"โœ… All {total} accessions will be processed." - else: - if invalid_accessions: - warning = f"โš ๏ธ Invalid accessions: {', '.join(invalid_accessions)}." - else: - warning = "Nothing to processing" - if len(accessions) == 1: - processed_info = warning + "\n" +f"Processed accessions: {accessions[0]}" - else: - if len(accessions) > 0: - processed_info = warning + "\n" +f"Processed accessions: {accessions[0]}...{accessions[-1]}" - elif len(accessions) == 0: - processed_info = warning - else: - processed_info = "โš ๏ธ Cannot process the input" - ### 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) - # ) - cleanup_processes() # โœ… hard kill anything left - 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) - # ) - cleanup_processes() # โœ… hard kill anything left - 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 - cleanup_processes() # โœ… same cleanup here - - 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) - - - - +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 + active_processes = [] + 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) + active_processes.append(p) # โœ… track it + 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(timeout=2) + 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 cleanup_processes(): + global active_processes + print("inside cleanup process and number of active process: ", len(active_processes)) + for p in active_processes: + if p.is_alive(): + try: + p.terminate() + p.join(timeout=2) + except Exception: + pass + active_processes = [] + + + def threaded_batch_runner(file=None, text="", email=""): + print("clean everything remain before running") + cleanup_processes() + 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 + #active_processes = [] + + 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, invalid_accessions, error = extract_accessions_from_input(file, text) + total = len(accessions) + print("total len original accessions: ", total) + if total > 0: + if total > limited_acc: + accessions = accessions[:limited_acc] + if invalid_accessions: + warning = f"โš ๏ธ Only processing first {limited_acc} accessions. โš ๏ธ Invalid accessions: {', '.join(invalid_accessions)}." + + else: + warning = f"โš ๏ธ Only processing first {limited_acc} accessions." + else: + if invalid_accessions: + warning = f"โœ… All {total} accessions will be processed. โš ๏ธ Invalid accessions: {', '.join(invalid_accessions)}." + else: + warning = f"โœ… All {total} accessions will be processed." + else: + if invalid_accessions: + warning = f"โš ๏ธ Invalid accessions: {', '.join(invalid_accessions)}." + else: + warning = "Nothing to processing" + if len(accessions) == 1: + processed_info = warning + "\n" +f"Processed accessions: {accessions[0]}" + else: + if len(accessions) > 0: + processed_info = warning + "\n" +f"Processed accessions: {accessions[0]}...{accessions[-1]}" + elif len(accessions) == 0: + processed_info = warning + else: + processed_info = "โš ๏ธ Cannot process the input" + ### 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) + # ) + cleanup_processes() # โœ… hard kill anything left + 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) + # ) + cleanup_processes() # โœ… hard kill anything left + 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;" + style += ( + " max-width: 400px;" + " white-space: normal;" + " word-wrap: break-word;" + " overflow-wrap: break-word;" + " word-break: break-word;" + ) + 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 + cleanup_processes() # โœ… same cleanup here + + 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) \ No newline at end of file