| import asyncio |
| import nest_asyncio |
| import os |
| import re |
| import json |
| import time |
| import streamlit as st |
| import base64 |
| from io import BytesIO |
| from dotenv import load_dotenv |
| from selenium import webdriver |
| from selenium.webdriver.chrome.options import Options |
| from selenium.webdriver.common.by import By |
| from selenium.webdriver.common.keys import Keys |
| from selenium.webdriver.support.ui import WebDriverWait |
| from selenium.webdriver.support import expected_conditions as EC |
| from google import genai |
|
|
| |
| load_dotenv() |
| GEMINI_API_KEY = 'AIzaSyAOK9vRTSRQzd22B2gmbiuIePbZTDyaGYs' |
| if not GEMINI_API_KEY: |
| st.error("GEMINI_API_KEY environment variable not set. Please configure it properly.") |
|
|
| |
| try: |
| asyncio.get_event_loop() |
| except RuntimeError: |
| |
| asyncio.set_event_loop(asyncio.new_event_loop()) |
| |
| |
| |
| nest_asyncio.apply() |
| |
|
|
| def take_screenshot(driver): |
| """ |
| Takes a screenshot of the current browser window and returns it as an image |
| that can be displayed in Streamlit. |
| """ |
| screenshot = driver.get_screenshot_as_png() |
| return screenshot |
|
|
| def extract_questions_from_fb_data(html): |
| """ |
| Parses the rendered HTML to extract questions and options from the |
| FB_PUBLIC_LOAD_DATA_ JavaScript variable. |
| """ |
| match = re.search(r'var\s+FB_PUBLIC_LOAD_DATA_\s*=\s*(\[.*?\]);</script>', html, re.DOTALL) |
| if not match: |
| st.error("FB_PUBLIC_LOAD_DATA_ not found in HTML.") |
| return [] |
| raw_json = match.group(1) |
| |
| replacements = { |
| r'\\n': '\n', |
| r'\\u003c': '<', |
| r'\\u003e': '>', |
| r'\\u0026': '&', |
| r'\\"': '"' |
| } |
| for old, new in replacements.items(): |
| raw_json = raw_json.replace(old, new) |
| raw_json = re.sub(r'[\x00-\x08\x0B-\x1F\x7F]', '', raw_json) |
| try: |
| data = json.loads(raw_json) |
| except json.JSONDecodeError as e: |
| st.error(f"Error decoding FB_PUBLIC_LOAD_DATA_ JSON: {e}") |
| return [] |
| |
| |
| questions = [] |
| try: |
| questions_data = data[1][1] |
| except (IndexError, TypeError): |
| return questions |
|
|
| for item in questions_data: |
| if not isinstance(item, list) or len(item) < 2: |
| continue |
| q_text = item[1] if isinstance(item[1], str) else None |
| if not q_text: |
| continue |
| q_text = q_text.strip() |
| |
| choices = [] |
| if len(item) > 4 and isinstance(item[4], list): |
| for block in item[4]: |
| if isinstance(block, list) and len(block) > 1 and isinstance(block[1], list): |
| for opt in block[1]: |
| if isinstance(opt, list) and len(opt) > 0 and isinstance(opt[0], str): |
| choices.append(opt[0]) |
| questions.append({ |
| "question_text": q_text, |
| "options": choices |
| }) |
| return questions |
|
|
| def generate_answers(questions, api_key): |
| """ |
| For each question, call Google Gemini to generate an answer that matches available options. |
| """ |
| try: |
| |
| try: |
| loop = asyncio.get_event_loop() |
| except RuntimeError: |
| loop = asyncio.new_event_loop() |
| asyncio.set_event_loop(loop) |
| |
| client = genai.Client(api_key=api_key) |
| |
| |
| user_name = st.session_state.get("user_name", "") |
| user_roll_no = st.session_state.get("user_roll_no", "") |
| user_prn = st.session_state.get("user_prn", "") |
| |
| for q in questions: |
| question_text = q["question_text"] |
| options = q["options"] |
| |
| |
| q_lower = question_text.lower() |
| |
| |
| if any(name_word in q_lower for name_word in ["name", "full name", "your name"]) and user_name: |
| q["gemini_answer"] = user_name |
| q["is_personal_info"] = True |
| continue |
| |
| if any(roll_word in q_lower for roll_word in ["roll", "roll no", "roll number"]) and user_roll_no: |
| q["gemini_answer"] = user_roll_no |
| q["is_personal_info"] = True |
| continue |
| |
| if any(prn_word in q_lower for prn_word in ["prn", "prn no", "prn number", "gr number", "grn", "registration"]) and user_prn: |
| q["gemini_answer"] = user_prn |
| q["is_personal_info"] = True |
| continue |
| |
| |
| if any(info_word in q_lower for info_word in ["email", "phone", "mobile", "address", "contact", "dob", "date of birth"]): |
| q["is_personal_info"] = True |
| q["gemini_answer"] = "[PLEASE FILL THIS FIELD MANUALLY]" |
| continue |
| |
| |
| if options: |
| prompt = f""" |
| Question: {question_text} |
| |
| These are the EXACT options (choose only one): |
| {', '.join([f'"{opt}"' for opt in options])} |
| |
| Instructions: |
| 1. Choose exactly ONE option from the list above |
| 2. Return ONLY the exact text of the chosen option, nothing else |
| 3. Do not add any explanation, just the option text |
| 4. Do not add quotation marks around the option |
| 5. Do not answer questions asking for personal information like name, roll number, PRN, email, etc. |
| |
| Answer: |
| """ |
| else: |
| prompt = f""" |
| Question: {question_text} |
| |
| Please provide a brief and direct answer to this question. |
| Keep your answer concise (1-2 sentences maximum). |
| Do not answer if this is asking for personal information like name, roll number, PRN, email, etc. |
| |
| Answer: |
| """ |
| |
| try: |
| |
| response = client.models.generate_content( |
| model="gemini-2.0-flash", |
| contents=prompt |
| ) |
| |
| |
| answer = response.text.strip() |
| |
| |
| if options: |
| exact_match = False |
| for opt in options: |
| if opt.lower() == answer.lower(): |
| answer = opt |
| exact_match = True |
| break |
| |
| |
| if not exact_match: |
| from difflib import SequenceMatcher |
| best_match = max(options, key=lambda opt: SequenceMatcher(None, opt.lower(), answer.lower()).ratio()) |
| answer = best_match |
| |
| q["gemini_answer"] = answer |
| |
| except Exception as e: |
| q["gemini_answer"] = f"Error: {str(e)}" |
| st.error(f"Error generating answer: {str(e)}") |
| |
| return questions |
| |
| except Exception as e: |
| st.error(f"Error in generate_answers function: {str(e)}") |
| |
| for q in questions: |
| if "gemini_answer" not in q: |
| q["gemini_answer"] = f"Error: Could not generate answer due to {str(e)}" |
| return questions |
|
|
| def fill_form(driver, questions): |
| """ |
| Fills the Google Form with generated answers using the provided driver. |
| """ |
| |
| question_containers = driver.find_elements(By.CSS_SELECTOR, "div.freebirdFormviewerViewItemsItemItem") |
| if not question_containers: |
| question_containers = driver.find_elements(By.CSS_SELECTOR, "div[role='listitem']") |
| if not question_containers: |
| st.error("Could not locate question containers in the form.") |
| return False |
| |
| |
| print(f"Found {len(question_containers)} question containers in the form") |
| print(f"We have {len(questions)} questions with answers to fill") |
|
|
| |
| time.sleep(2) |
|
|
| for idx, q in enumerate(questions): |
| if idx >= len(question_containers): |
| break |
| |
| print(f"\n--------- Processing Question {idx+1} ---------") |
| container = question_containers[idx] |
| answer = q.get("gemini_answer", "").strip() |
| options = q.get("options", []) |
| |
| |
| print(f"Question: {q['question_text']}") |
| print(f"Generated Answer: {answer}") |
| |
| if options: |
| try: |
| print(f"This is a multiple-choice question with {len(options)} options") |
| |
| |
| option_elements = container.find_elements(By.CSS_SELECTOR, "div[role='radio']") |
| if not option_elements: |
| option_elements = container.find_elements(By.CSS_SELECTOR, "label") |
| if not option_elements: |
| option_elements = container.find_elements(By.CSS_SELECTOR, "div.appsMaterialWizToggleRadiogroupRadioButtonContainer") |
| if not option_elements: |
| option_elements = container.find_elements(By.CSS_SELECTOR, ".docssharedWizToggleLabeledLabelWrapper") |
| |
| if not option_elements: |
| st.warning(f"Could not find option elements for question {idx+1}") |
| print("No option elements found with any selector strategy") |
| continue |
| |
| print(f"Found {len(option_elements)} option elements in the form") |
| |
| |
| import re |
| normalized_answer = re.sub(r'[^\w\s]', '', answer.lower()).strip() |
| |
| |
| clicked = False |
| print("\nTrying exact matches...") |
| |
| |
| option_dict = {} |
| |
| |
| for i, opt_elem in enumerate(option_elements): |
| |
| opt_text = opt_elem.text.strip() |
| |
| |
| if not opt_text: |
| for child in opt_elem.find_elements(By.XPATH, ".//div"): |
| child_text = child.text.strip() |
| if child_text: |
| opt_text = child_text |
| break |
| |
| |
| if not opt_text: |
| opt_text = opt_elem.get_attribute("aria-label") or "" |
| |
| |
| if opt_text: |
| normalized_opt = re.sub(r'[^\w\s]', '', opt_text.lower()).strip() |
| option_dict[normalized_opt] = opt_elem |
| print(f"Option {i+1}: '{opt_text}' (normalized: '{normalized_opt}')") |
| else: |
| print(f"Option {i+1}: [NO TEXT FOUND]") |
| |
| |
| if normalized_answer in option_dict: |
| print(f"Found exact match for: '{normalized_answer}'") |
| option_dict[normalized_answer].click() |
| clicked = True |
| else: |
| |
| for opt_text, opt_elem in option_dict.items(): |
| if opt_text in normalized_answer or normalized_answer in opt_text: |
| print(f"Found partial match: '{opt_text}' with answer '{normalized_answer}'") |
| opt_elem.click() |
| clicked = True |
| break |
| |
| |
| if not clicked: |
| print("\nTrying to match with original options list...") |
| for i, original_opt in enumerate(options): |
| print(f"Original option {i+1}: '{original_opt}'") |
| normalized_orig = re.sub(r'[^\w\s]', '', original_opt.lower()).strip() |
| |
| |
| if normalized_orig == normalized_answer: |
| print(f"EXACT match with original option: '{original_opt}'") |
| |
| |
| if normalized_orig in option_dict: |
| option_dict[normalized_orig].click() |
| clicked = True |
| break |
| elif i < len(option_elements): |
| print(f"Clicking by position: element {i}") |
| option_elements[i].click() |
| clicked = True |
| break |
| |
| |
| elif normalized_orig in normalized_answer or normalized_answer in normalized_orig: |
| print(f"PARTIAL match with original option: '{original_opt}'") |
| if i < len(option_elements): |
| option_elements[i].click() |
| clicked = True |
| break |
| |
| |
| if not clicked: |
| print("\nNo direct matches found, trying similarity matching...") |
| from difflib import SequenceMatcher |
| |
| |
| best_score = 0 |
| best_element = None |
| for opt_text, opt_elem in option_dict.items(): |
| score = SequenceMatcher(None, opt_text, normalized_answer).ratio() |
| if score > best_score and score > 0.6: |
| best_score = score |
| best_element = opt_elem |
| |
| if best_element: |
| print(f"Best similarity match score: {best_score}") |
| best_element.click() |
| clicked = True |
| else: |
| |
| best_score = 0 |
| best_idx = 0 |
| for i, original_opt in enumerate(options): |
| normalized_orig = re.sub(r'[^\w\s]', '', original_opt.lower()).strip() |
| score = SequenceMatcher(None, normalized_orig, normalized_answer).ratio() |
| if score > best_score: |
| best_score = score |
| best_idx = i |
| |
| if best_score > 0.5 and best_idx < len(option_elements): |
| print(f"Best similarity with original option: '{options[best_idx]}' (score: {best_score})") |
| option_elements[best_idx].click() |
| clicked = True |
| |
| |
| if not clicked and option_elements: |
| st.warning(f"No match found for question {idx+1}, selecting first option as fallback") |
| print("No suitable match found, clicking first option as fallback") |
| option_elements[0].click() |
| |
| except Exception as e: |
| st.error(f"Error filling multiple-choice question {idx+1}: {e}") |
| print(f"Exception: {str(e)}") |
| else: |
| try: |
| print("This is a text question") |
| |
| input_elem = None |
| |
| |
| try: |
| input_elem = container.find_element(By.CSS_SELECTOR, "input[type='text']") |
| print("Found text input element") |
| except Exception: |
| try: |
| input_elem = container.find_element(By.CSS_SELECTOR, "textarea") |
| print("Found textarea element") |
| except Exception: |
| try: |
| |
| input_elem = container.find_element(By.CSS_SELECTOR, "input") |
| print("Found generic input element") |
| except Exception: |
| try: |
| input_elem = container.find_element(By.TAG_NAME, "textarea") |
| print("Found generic textarea element") |
| except Exception: |
| st.error(f"Could not locate input element for question {idx+1}") |
| print("Failed to find any input element for this question") |
| |
| if input_elem: |
| input_elem.clear() |
| input_elem.send_keys(answer) |
| print(f"Filled text answer: {answer}") |
| except Exception as e: |
| st.error(f"Error filling text question {idx+1}: {e}") |
| print(f"Exception: {str(e)}") |
| |
| print("\n---------- Form filling completed ----------") |
| return True |
|
|
| def login_to_google(driver, email, password): |
| """ |
| Logs into Google account using the provided credentials. |
| """ |
| try: |
| |
| driver.get("https://accounts.google.com/signin") |
| time.sleep(2) |
| |
| |
| screenshot = take_screenshot(driver) |
| st.image(screenshot, caption="Login Page", use_column_width=True) |
| |
| |
| email_input = WebDriverWait(driver, 10).until( |
| EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='email']")) |
| ) |
| email_input.clear() |
| email_input.send_keys(email) |
| email_input.send_keys(Keys.RETURN) |
| time.sleep(2) |
| |
| |
| screenshot = take_screenshot(driver) |
| st.image(screenshot, caption="Email Entered", use_column_width=True) |
| |
| |
| password_input = WebDriverWait(driver, 10).until( |
| EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='password']")) |
| ) |
| password_input.clear() |
| password_input.send_keys(password) |
| password_input.send_keys(Keys.RETURN) |
| time.sleep(5) |
| |
| |
| screenshot = take_screenshot(driver) |
| st.image(screenshot, caption="Login Attempt Result", use_column_width=True) |
| |
| |
| try: |
| WebDriverWait(driver, 5).until( |
| EC.presence_of_element_located((By.CSS_SELECTOR, "div[data-email]")) |
| ) |
| return True |
| except: |
| |
| if "accounts.google.com/signin" not in driver.current_url: |
| return True |
| |
| if "2-Step Verification" in driver.page_source or "verification" in driver.page_source.lower(): |
| st.warning("Two-factor authentication detected. Please complete it in the browser window.") |
| return "2FA" |
| return False |
| |
| except Exception as e: |
| st.error(f"Error during login: {str(e)}") |
| return False |
| def initialize_browser(): |
| """ |
| Initialize a Chrome browser with Docker-compatible settings |
| """ |
| from webdriver_manager.chrome import ChromeDriverManager |
| from selenium.webdriver.chrome.service import Service |
| |
| chrome_options = Options() |
| chrome_options.add_argument("--headless=new") |
| chrome_options.add_argument("--no-sandbox") |
| chrome_options.add_argument("--disable-dev-shm-usage") |
| chrome_options.add_argument("--disable-gpu") |
| chrome_options.add_argument("--window-size=1920,1080") |
| chrome_options.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36") |
| |
| try: |
| |
| try: |
| service = Service(ChromeDriverManager().install()) |
| driver = webdriver.Chrome(service=service, options=chrome_options) |
| return driver |
| except Exception as e1: |
| st.warning(f"First browser initialization attempt failed: {e1}") |
| |
| |
| try: |
| driver = webdriver.Chrome(options=chrome_options) |
| return driver |
| except Exception as e2: |
| st.error(f"Second browser initialization attempt failed: {e2}") |
| return None |
| except Exception as e: |
| st.error(f"Failed to initialize browser: {str(e)}") |
| return None |
| |
|
|
| st.title("Google Form Auto Filler with Gemini") |
| st.write(""" |
| This app uses a headless browser to help you fill Google Forms automatically with AI-generated answers. |
| You'll be able to see screenshots of what's happening in the browser as it progresses. |
| """) |
|
|
| |
| st.info(""" |
| **NEW FEATURE**: You can now provide your name, roll number, and PRN number to auto-fill personal information fields in forms. |
| The app will detect fields asking for this information and use your provided details instead of AI-generated answers. |
| """) |
|
|
| |
| if "driver" not in st.session_state: |
| st.session_state.driver = None |
| if "login_status" not in st.session_state: |
| st.session_state.login_status = None |
| if "form_filled" not in st.session_state: |
| st.session_state.form_filled = False |
| if "screenshot" not in st.session_state: |
| st.session_state.screenshot = None |
|
|
| |
| st.header("Step 1: Login to Google Account") |
|
|
| |
| with st.form("google_login"): |
| email = st.text_input("Google Email") |
| password = st.text_input("Google Password", type="password") |
| submit_button = st.form_submit_button("Login to Google") |
| |
| if submit_button and email and password: |
| |
| driver = initialize_browser() |
| |
| if driver: |
| st.session_state.driver = driver |
| |
| |
| screenshot = take_screenshot(driver) |
| st.session_state.screenshot = screenshot |
| st.image(screenshot, caption="Browser Started", use_column_width=True) |
| |
| |
| login_result = login_to_google(driver, email, password) |
| st.session_state.login_status = login_result |
| |
| if login_result == True: |
| st.success("Login successful!") |
| elif login_result == "2FA": |
| st.warning("Two-factor authentication may be required. Check the screenshot for verification prompts.") |
| st.info("You might need to complete 2FA in the browser window. Screenshots will update as you proceed.") |
| else: |
| st.error("Login failed. Please check your credentials and try again.") |
| else: |
| st.error("Failed to initialize browser. Please check Docker configuration.") |
|
|
|
|
| |
| if st.session_state.login_status == False: |
| st.info("If you can see that you're actually logged in from the screenshot above, click the button below:") |
| if st.button("I'm actually logged in successfully"): |
| st.session_state.login_status = True |
| st.success("Login status manually confirmed! You can proceed to the form filling step.") |
|
|
| |
| if st.session_state.login_status == "2FA" and st.session_state.driver: |
| if st.button("Take New Screenshot (for 2FA completion check)"): |
| screenshot = take_screenshot(st.session_state.driver) |
| st.session_state.screenshot = screenshot |
| st.image(screenshot, caption="Current Browser State", use_column_width=True) |
| |
| |
| if "accounts.google.com/signin" not in st.session_state.driver.current_url: |
| st.success("Looks like you completed 2FA! You can proceed to the form filling step.") |
| st.session_state.login_status = True |
|
|
| if st.session_state.driver and (st.session_state.login_status == True or st.session_state.login_status == "2FA"): |
| st.header("Step 2: Fill Google Form") |
| |
| |
| st.markdown("### Enter your Google Form URL below:") |
| form_url = st.text_input("Form URL:", key="form_url_input") |
| |
| |
| if form_url: |
| |
| if "form_url" not in st.session_state: |
| st.session_state.form_url = form_url |
| |
| |
| st.markdown("### Personal Information") |
| st.info("If the form has fields for name, roll number or PRN, enter them below to auto-fill those fields.") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| |
| st.text_input("Your Name:", key="user_name") |
| |
| with col2: |
| st.text_input("Roll Number:", key="user_roll_no") |
| |
| st.text_input("PRN Number:", key="user_prn") |
| |
| |
| if st.button("Process Form", key="process_form_button") or "questions" in st.session_state: |
| |
| driver = st.session_state.driver |
| |
| |
| if "questions" not in st.session_state: |
| driver.get(form_url) |
| time.sleep(5) |
| |
| |
| screenshot = take_screenshot(driver) |
| st.image(screenshot, caption="Google Form Loaded", use_column_width=True) |
| |
| html = driver.page_source |
| |
| |
| questions = extract_questions_from_fb_data(html) |
| if not questions: |
| st.error("No questions extracted from the form.") |
| else: |
| st.success(f"Successfully extracted {len(questions)} questions from the form.") |
| |
| |
| with st.spinner("Generating answers with Gemini..."): |
| questions = generate_answers(questions, GEMINI_API_KEY) |
| |
| |
| st.session_state.questions = questions |
| else: |
| |
| questions = st.session_state.questions |
| |
| |
| st.write("--- Generated Answers ---") |
| for idx, q in enumerate(questions, start=1): |
| st.write(f"**Question {idx}:** {q['question_text']}") |
| if q["options"]: |
| st.write("Options:", ", ".join(q["options"])) |
| else: |
| st.write("(No multiple-choice options)") |
| |
| |
| if q.get("is_personal_info", False): |
| st.write("**Personal Info Auto-Fill:** ", q["gemini_answer"]) |
| st.info("This field was detected as personal information and will be filled with your provided details.") |
| else: |
| st.write("**Generated Answer:** ", q["gemini_answer"]) |
| st.write("---") |
| |
| |
| st.markdown("### Form Actions") |
| |
| |
| if not st.session_state.get("form_filled", False): |
| if st.button("Fill Form with Generated Answers", key="fill_form_button"): |
| with st.spinner("Filling form..."): |
| |
| driver.get(st.session_state.form_url) |
| time.sleep(3) |
| |
| if fill_form(driver, questions): |
| time.sleep(2) |
| |
| |
| filled_screenshot = take_screenshot(driver) |
| st.session_state.filled_screenshot = filled_screenshot |
| st.session_state.form_filled = True |
| |
| st.success("Form successfully filled with generated answers!") |
| st.image(filled_screenshot, caption="Form Filled with Answers", use_column_width=True) |
| |
| |
| if st.session_state.get("form_filled", False) and "filled_screenshot" in st.session_state: |
| if not st.session_state.get("showing_filled_form", False): |
| st.image(st.session_state.filled_screenshot, caption="Form Filled with Generated Answers", use_column_width=True) |
| st.session_state.showing_filled_form = True |
| |
| |
| st.success("โ
Form has been filled with AI-generated answers!Just go and change your name and stuff") |
| st.info("๐ก You can check the answers generated by opening the form link on your browser.") |
| st.markdown(f"๐ **Form Link:** [Open in Browser]({form_url})") |
|
|
| |
| if st.session_state.driver: |
| st.markdown("---") |
| if st.button("Close Browser"): |
| st.session_state.driver.quit() |
| st.session_state.driver = None |
| st.session_state.login_status = None |
| st.session_state.form_filled = False |
| st.session_state.questions = None |
| st.session_state.form_url = None |
| st.session_state.filled_screenshot = None |
| st.session_state.showing_filled_form = False |
| st.success("Browser closed. All session data cleared.") |