PluginLiveInterns
commited on
Commit
Β·
53cce60
1
Parent(s):
5647ca1
Add application file
Browse files- Dockerfile +63 -0
- app.py +662 -0
- app2.py +724 -0
- app_ref.py +600 -0
- requirements.txt +6 -0
Dockerfile
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
# Install Chrome with necessary dependencies
|
6 |
+
RUN apt-get update && apt-get install -y \
|
7 |
+
wget \
|
8 |
+
gnupg \
|
9 |
+
unzip \
|
10 |
+
curl \
|
11 |
+
fonts-liberation \
|
12 |
+
libasound2 \
|
13 |
+
libatk-bridge2.0-0 \
|
14 |
+
libatk1.0-0 \
|
15 |
+
libatspi2.0-0 \
|
16 |
+
libcups2 \
|
17 |
+
libdbus-1-3 \
|
18 |
+
libdrm2 \
|
19 |
+
libgbm1 \
|
20 |
+
libgtk-3-0 \
|
21 |
+
libnspr4 \
|
22 |
+
libnss3 \
|
23 |
+
libxcomposite1 \
|
24 |
+
libxdamage1 \
|
25 |
+
libxfixes3 \
|
26 |
+
libxkbcommon0 \
|
27 |
+
libxrandr2 \
|
28 |
+
xdg-utils \
|
29 |
+
libpci3 \
|
30 |
+
jq \
|
31 |
+
&& wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
|
32 |
+
&& apt-get install -y ./google-chrome-stable_current_amd64.deb \
|
33 |
+
&& rm google-chrome-stable_current_amd64.deb
|
34 |
+
|
35 |
+
# Install matching ChromeDriver
|
36 |
+
RUN CHROME_VERSION=$(google-chrome --version | awk '{print $3}' | cut -d. -f1) \
|
37 |
+
&& wget -q -O /tmp/latest_chromedriver_version.txt "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_VERSION}" \
|
38 |
+
&& CHROMEDRIVER_VERSION=$(cat /tmp/latest_chromedriver_version.txt) \
|
39 |
+
&& echo "Chrome version: ${CHROME_VERSION}, ChromeDriver version: ${CHROMEDRIVER_VERSION}" \
|
40 |
+
&& wget -q "https://storage.googleapis.com/chrome-for-testing-public/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip" -O /tmp/chromedriver.zip \
|
41 |
+
&& unzip /tmp/chromedriver.zip -d /tmp/ \
|
42 |
+
&& mv /tmp/chromedriver-linux64/chromedriver /usr/bin/chromedriver \
|
43 |
+
&& chmod +x /usr/bin/chromedriver \
|
44 |
+
&& rm -rf /tmp/chromedriver.zip /tmp/chromedriver-linux64 /tmp/latest_chromedriver_version.txt
|
45 |
+
|
46 |
+
# Copy requirements first for better caching
|
47 |
+
COPY requirements.txt .
|
48 |
+
RUN pip install -r requirements.txt
|
49 |
+
|
50 |
+
# Copy the application code
|
51 |
+
COPY . .
|
52 |
+
|
53 |
+
# Make modifications to app.py to handle headless browser better
|
54 |
+
RUN sed -i 's/chrome_options.add_argument("--headless")/chrome_options.add_argument("--headless=new")/g' app.py
|
55 |
+
|
56 |
+
# Expose the Streamlit port
|
57 |
+
EXPOSE 8501
|
58 |
+
|
59 |
+
# Set environment variables placeholder (actual key should be provided at runtime)
|
60 |
+
ENV GEMINI_API_KEY=""
|
61 |
+
|
62 |
+
# Run Streamlit
|
63 |
+
CMD ["streamlit", "run", "app2.py", "--server.address=0.0.0.0"]
|
app.py
ADDED
@@ -0,0 +1,662 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import nest_asyncio
|
3 |
+
import os
|
4 |
+
import re
|
5 |
+
import json
|
6 |
+
import time
|
7 |
+
import streamlit as st
|
8 |
+
import base64
|
9 |
+
from io import BytesIO
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
from selenium import webdriver
|
12 |
+
from selenium.webdriver.chrome.options import Options
|
13 |
+
from selenium.webdriver.common.by import By
|
14 |
+
from selenium.webdriver.common.keys import Keys
|
15 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
16 |
+
from selenium.webdriver.support import expected_conditions as EC
|
17 |
+
from google import genai
|
18 |
+
|
19 |
+
# Load environment variables from .env
|
20 |
+
load_dotenv()
|
21 |
+
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
|
22 |
+
if not GEMINI_API_KEY:
|
23 |
+
st.error("GEMINI_API_KEY environment variable not set. Please configure it properly.")
|
24 |
+
|
25 |
+
# Initialize asyncio for threaded environments
|
26 |
+
try:
|
27 |
+
asyncio.get_event_loop()
|
28 |
+
except RuntimeError:
|
29 |
+
# If there is no event loop in this thread, create one and make it current
|
30 |
+
asyncio.set_event_loop(asyncio.new_event_loop())
|
31 |
+
|
32 |
+
# Apply nest_asyncio to allow nested event loops
|
33 |
+
# (sometimes needed in Streamlit)
|
34 |
+
nest_asyncio.apply()
|
35 |
+
# --- Utility Functions ---
|
36 |
+
|
37 |
+
def take_screenshot(driver):
|
38 |
+
"""
|
39 |
+
Takes a screenshot of the current browser window and returns it as an image
|
40 |
+
that can be displayed in Streamlit.
|
41 |
+
"""
|
42 |
+
screenshot = driver.get_screenshot_as_png()
|
43 |
+
return screenshot
|
44 |
+
|
45 |
+
def extract_questions_from_fb_data(html):
|
46 |
+
"""
|
47 |
+
Parses the rendered HTML to extract questions and options from the
|
48 |
+
FB_PUBLIC_LOAD_DATA_ JavaScript variable.
|
49 |
+
"""
|
50 |
+
match = re.search(r'var\s+FB_PUBLIC_LOAD_DATA_\s*=\s*(\[.*?\]);</script>', html, re.DOTALL)
|
51 |
+
if not match:
|
52 |
+
st.error("FB_PUBLIC_LOAD_DATA_ not found in HTML.")
|
53 |
+
return []
|
54 |
+
raw_json = match.group(1)
|
55 |
+
# Replace common escaped sequences for valid JSON
|
56 |
+
replacements = {
|
57 |
+
r'\\n': '\n',
|
58 |
+
r'\\u003c': '<',
|
59 |
+
r'\\u003e': '>',
|
60 |
+
r'\\u0026': '&',
|
61 |
+
r'\\"': '"'
|
62 |
+
}
|
63 |
+
for old, new in replacements.items():
|
64 |
+
raw_json = raw_json.replace(old, new)
|
65 |
+
raw_json = re.sub(r'[\x00-\x08\x0B-\x1F\x7F]', '', raw_json)
|
66 |
+
try:
|
67 |
+
data = json.loads(raw_json)
|
68 |
+
except json.JSONDecodeError as e:
|
69 |
+
st.error(f"Error decoding FB_PUBLIC_LOAD_DATA_ JSON: {e}")
|
70 |
+
return []
|
71 |
+
|
72 |
+
# Typically, questions are stored in data[1][1]
|
73 |
+
questions = []
|
74 |
+
try:
|
75 |
+
questions_data = data[1][1]
|
76 |
+
except (IndexError, TypeError):
|
77 |
+
return questions
|
78 |
+
|
79 |
+
for item in questions_data:
|
80 |
+
if not isinstance(item, list) or len(item) < 2:
|
81 |
+
continue
|
82 |
+
q_text = item[1] if isinstance(item[1], str) else None
|
83 |
+
if not q_text:
|
84 |
+
continue
|
85 |
+
q_text = q_text.strip()
|
86 |
+
# For multiple-choice questions, options usually appear in item[4]
|
87 |
+
choices = []
|
88 |
+
if len(item) > 4 and isinstance(item[4], list):
|
89 |
+
for block in item[4]:
|
90 |
+
if isinstance(block, list) and len(block) > 1 and isinstance(block[1], list):
|
91 |
+
for opt in block[1]:
|
92 |
+
if isinstance(opt, list) and len(opt) > 0 and isinstance(opt[0], str):
|
93 |
+
choices.append(opt[0])
|
94 |
+
questions.append({
|
95 |
+
"question_text": q_text,
|
96 |
+
"options": choices
|
97 |
+
})
|
98 |
+
return questions
|
99 |
+
|
100 |
+
def generate_answers(questions, api_key):
|
101 |
+
"""
|
102 |
+
For each question, call Google Gemini to generate an answer that matches available options.
|
103 |
+
"""
|
104 |
+
try:
|
105 |
+
# Ensure we have an event loop in this thread
|
106 |
+
try:
|
107 |
+
loop = asyncio.get_event_loop()
|
108 |
+
except RuntimeError:
|
109 |
+
loop = asyncio.new_event_loop()
|
110 |
+
asyncio.set_event_loop(loop)
|
111 |
+
|
112 |
+
client = genai.Client(api_key=api_key)
|
113 |
+
|
114 |
+
for q in questions:
|
115 |
+
question_text = q["question_text"]
|
116 |
+
options = q["options"]
|
117 |
+
|
118 |
+
# Rest of your existing function code...
|
119 |
+
if options:
|
120 |
+
prompt = f"""
|
121 |
+
Question: {question_text}
|
122 |
+
|
123 |
+
These are the EXACT options (choose only one):
|
124 |
+
{', '.join([f'"{opt}"' for opt in options])}
|
125 |
+
|
126 |
+
Instructions:
|
127 |
+
1. Choose exactly ONE option from the list above
|
128 |
+
2. Return ONLY the exact text of the chosen option, nothing else
|
129 |
+
3. Do not add any explanation, just the option text
|
130 |
+
4. Do not add quotation marks around the option
|
131 |
+
5. Don not answer questions like "What is your name?","Rollno","PRN/GRN","Email","Mobile No","Address","DOB etc
|
132 |
+
|
133 |
+
Answer:
|
134 |
+
"""
|
135 |
+
else:
|
136 |
+
prompt = f"""
|
137 |
+
Question: {question_text}
|
138 |
+
|
139 |
+
Please provide a brief and direct answer to this question.
|
140 |
+
Keep your answer concise (1-2 sentences maximum).
|
141 |
+
|
142 |
+
Answer:
|
143 |
+
"""
|
144 |
+
|
145 |
+
try:
|
146 |
+
response = client.models.generate_content(
|
147 |
+
model="gemini-2.0-flash",
|
148 |
+
contents=prompt
|
149 |
+
)
|
150 |
+
|
151 |
+
answer = response.text.strip()
|
152 |
+
|
153 |
+
# For multiple choice, ensure it exactly matches one of the options
|
154 |
+
if options:
|
155 |
+
exact_match = False
|
156 |
+
for opt in options:
|
157 |
+
if opt.lower() == answer.lower():
|
158 |
+
answer = opt # Use the exact casing from the original option
|
159 |
+
exact_match = True
|
160 |
+
break
|
161 |
+
|
162 |
+
# If no exact match, use the most similar option
|
163 |
+
if not exact_match:
|
164 |
+
from difflib import SequenceMatcher
|
165 |
+
best_match = max(options, key=lambda opt: SequenceMatcher(None, opt.lower(), answer.lower()).ratio())
|
166 |
+
answer = best_match
|
167 |
+
|
168 |
+
q["gemini_answer"] = answer
|
169 |
+
|
170 |
+
except Exception as e:
|
171 |
+
q["gemini_answer"] = f"Error: {str(e)}"
|
172 |
+
st.error(f"Error generating answer: {str(e)}")
|
173 |
+
|
174 |
+
return questions
|
175 |
+
|
176 |
+
except Exception as e:
|
177 |
+
st.error(f"Error in generate_answers function: {str(e)}")
|
178 |
+
# Return questions with error messages
|
179 |
+
for q in questions:
|
180 |
+
if "gemini_answer" not in q:
|
181 |
+
q["gemini_answer"] = f"Error: Could not generate answer due to {str(e)}"
|
182 |
+
return questions
|
183 |
+
|
184 |
+
def fill_form(driver, questions):
|
185 |
+
"""
|
186 |
+
Fills the Google Form with generated answers using the provided driver.
|
187 |
+
"""
|
188 |
+
# Locate question containers (try different selectors)
|
189 |
+
question_containers = driver.find_elements(By.CSS_SELECTOR, "div.freebirdFormviewerViewItemsItemItem")
|
190 |
+
if not question_containers:
|
191 |
+
question_containers = driver.find_elements(By.CSS_SELECTOR, "div[role='listitem']")
|
192 |
+
if not question_containers:
|
193 |
+
st.error("Could not locate question containers in the form.")
|
194 |
+
return False
|
195 |
+
|
196 |
+
# Print total questions found for debugging
|
197 |
+
print(f"Found {len(question_containers)} question containers in the form")
|
198 |
+
print(f"We have {len(questions)} questions with answers to fill")
|
199 |
+
|
200 |
+
# Give the form time to fully render
|
201 |
+
time.sleep(2)
|
202 |
+
|
203 |
+
for idx, q in enumerate(questions):
|
204 |
+
if idx >= len(question_containers):
|
205 |
+
break
|
206 |
+
|
207 |
+
print(f"\n--------- Processing Question {idx+1} ---------")
|
208 |
+
container = question_containers[idx]
|
209 |
+
answer = q.get("gemini_answer", "").strip()
|
210 |
+
options = q.get("options", [])
|
211 |
+
|
212 |
+
# Print question and answer for debugging
|
213 |
+
print(f"Question: {q['question_text']}")
|
214 |
+
print(f"Generated Answer: {answer}")
|
215 |
+
|
216 |
+
if options:
|
217 |
+
try:
|
218 |
+
print(f"This is a multiple-choice question with {len(options)} options")
|
219 |
+
|
220 |
+
# Try multiple selector strategies to find radio buttons or checkboxes
|
221 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "div[role='radio']")
|
222 |
+
if not option_elements:
|
223 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "label")
|
224 |
+
if not option_elements:
|
225 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "div.appsMaterialWizToggleRadiogroupRadioButtonContainer")
|
226 |
+
if not option_elements:
|
227 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, ".docssharedWizToggleLabeledLabelWrapper")
|
228 |
+
|
229 |
+
if not option_elements:
|
230 |
+
st.warning(f"Could not find option elements for question {idx+1}")
|
231 |
+
print("No option elements found with any selector strategy")
|
232 |
+
continue
|
233 |
+
|
234 |
+
print(f"Found {len(option_elements)} option elements in the form")
|
235 |
+
|
236 |
+
# Normalize the answer text to make matching more robust
|
237 |
+
import re
|
238 |
+
normalized_answer = re.sub(r'[^\w\s]', '', answer.lower()).strip()
|
239 |
+
|
240 |
+
# First pass: Try exact matches
|
241 |
+
clicked = False
|
242 |
+
print("\nTrying exact matches...")
|
243 |
+
|
244 |
+
# Create a dictionary mapping option text to elements
|
245 |
+
option_dict = {}
|
246 |
+
|
247 |
+
# First extract all option texts
|
248 |
+
for i, opt_elem in enumerate(option_elements):
|
249 |
+
# Get text directly and from child elements if needed
|
250 |
+
opt_text = opt_elem.text.strip()
|
251 |
+
|
252 |
+
# If no text, try getting from child elements
|
253 |
+
if not opt_text:
|
254 |
+
for child in opt_elem.find_elements(By.XPATH, ".//div"):
|
255 |
+
child_text = child.text.strip()
|
256 |
+
if child_text:
|
257 |
+
opt_text = child_text
|
258 |
+
break
|
259 |
+
|
260 |
+
# Still no text? Try aria-label
|
261 |
+
if not opt_text:
|
262 |
+
opt_text = opt_elem.get_attribute("aria-label") or ""
|
263 |
+
|
264 |
+
# Store in dictionary for later use if we have text
|
265 |
+
if opt_text:
|
266 |
+
normalized_opt = re.sub(r'[^\w\s]', '', opt_text.lower()).strip()
|
267 |
+
option_dict[normalized_opt] = opt_elem
|
268 |
+
print(f"Option {i+1}: '{opt_text}' (normalized: '{normalized_opt}')")
|
269 |
+
else:
|
270 |
+
print(f"Option {i+1}: [NO TEXT FOUND]")
|
271 |
+
|
272 |
+
# Try exact match
|
273 |
+
if normalized_answer in option_dict:
|
274 |
+
print(f"Found exact match for: '{normalized_answer}'")
|
275 |
+
option_dict[normalized_answer].click()
|
276 |
+
clicked = True
|
277 |
+
else:
|
278 |
+
# Try substring matches
|
279 |
+
for opt_text, opt_elem in option_dict.items():
|
280 |
+
if opt_text in normalized_answer or normalized_answer in opt_text:
|
281 |
+
print(f"Found partial match: '{opt_text}' with answer '{normalized_answer}'")
|
282 |
+
opt_elem.click()
|
283 |
+
clicked = True
|
284 |
+
break
|
285 |
+
|
286 |
+
# Try matching with original options
|
287 |
+
if not clicked:
|
288 |
+
print("\nTrying to match with original options list...")
|
289 |
+
for i, original_opt in enumerate(options):
|
290 |
+
print(f"Original option {i+1}: '{original_opt}'")
|
291 |
+
normalized_orig = re.sub(r'[^\w\s]', '', original_opt.lower()).strip()
|
292 |
+
|
293 |
+
# First check direct equality
|
294 |
+
if normalized_orig == normalized_answer:
|
295 |
+
print(f"EXACT match with original option: '{original_opt}'")
|
296 |
+
|
297 |
+
# Find matching element in the dictionary or by position
|
298 |
+
if normalized_orig in option_dict:
|
299 |
+
option_dict[normalized_orig].click()
|
300 |
+
clicked = True
|
301 |
+
break
|
302 |
+
elif i < len(option_elements):
|
303 |
+
print(f"Clicking by position: element {i}")
|
304 |
+
option_elements[i].click()
|
305 |
+
clicked = True
|
306 |
+
break
|
307 |
+
|
308 |
+
# Then try substring matching
|
309 |
+
elif normalized_orig in normalized_answer or normalized_answer in normalized_orig:
|
310 |
+
print(f"PARTIAL match with original option: '{original_opt}'")
|
311 |
+
if i < len(option_elements):
|
312 |
+
option_elements[i].click()
|
313 |
+
clicked = True
|
314 |
+
break
|
315 |
+
|
316 |
+
# Try similarity matching as last resort
|
317 |
+
if not clicked:
|
318 |
+
print("\nNo direct matches found, trying similarity matching...")
|
319 |
+
from difflib import SequenceMatcher
|
320 |
+
|
321 |
+
# Try matching with form elements
|
322 |
+
best_score = 0
|
323 |
+
best_element = None
|
324 |
+
for opt_text, opt_elem in option_dict.items():
|
325 |
+
score = SequenceMatcher(None, opt_text, normalized_answer).ratio()
|
326 |
+
if score > best_score and score > 0.6: # Require at least 60% similarity
|
327 |
+
best_score = score
|
328 |
+
best_element = opt_elem
|
329 |
+
|
330 |
+
if best_element:
|
331 |
+
print(f"Best similarity match score: {best_score}")
|
332 |
+
best_element.click()
|
333 |
+
clicked = True
|
334 |
+
else:
|
335 |
+
# Try matching with original options
|
336 |
+
best_score = 0
|
337 |
+
best_idx = 0
|
338 |
+
for i, original_opt in enumerate(options):
|
339 |
+
normalized_orig = re.sub(r'[^\w\s]', '', original_opt.lower()).strip()
|
340 |
+
score = SequenceMatcher(None, normalized_orig, normalized_answer).ratio()
|
341 |
+
if score > best_score:
|
342 |
+
best_score = score
|
343 |
+
best_idx = i
|
344 |
+
|
345 |
+
if best_score > 0.5 and best_idx < len(option_elements): # 50% similarity threshold
|
346 |
+
print(f"Best similarity with original option: '{options[best_idx]}' (score: {best_score})")
|
347 |
+
option_elements[best_idx].click()
|
348 |
+
clicked = True
|
349 |
+
|
350 |
+
# Last resort: click first option if nothing matched
|
351 |
+
if not clicked and option_elements:
|
352 |
+
st.warning(f"No match found for question {idx+1}, selecting first option as fallback")
|
353 |
+
print("No suitable match found, clicking first option as fallback")
|
354 |
+
option_elements[0].click()
|
355 |
+
|
356 |
+
except Exception as e:
|
357 |
+
st.error(f"Error filling multiple-choice question {idx+1}: {e}")
|
358 |
+
print(f"Exception: {str(e)}")
|
359 |
+
else:
|
360 |
+
try:
|
361 |
+
print("This is a text question")
|
362 |
+
# For text questions, locate the text input or textarea and fill in the answer
|
363 |
+
input_elem = None
|
364 |
+
|
365 |
+
# Try multiple strategies to find the text input
|
366 |
+
try:
|
367 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "input[type='text']")
|
368 |
+
print("Found text input element")
|
369 |
+
except Exception:
|
370 |
+
try:
|
371 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "textarea")
|
372 |
+
print("Found textarea element")
|
373 |
+
except Exception:
|
374 |
+
try:
|
375 |
+
# Try more generic selectors
|
376 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "input")
|
377 |
+
print("Found generic input element")
|
378 |
+
except Exception:
|
379 |
+
try:
|
380 |
+
input_elem = container.find_element(By.TAG_NAME, "textarea")
|
381 |
+
print("Found generic textarea element")
|
382 |
+
except Exception:
|
383 |
+
st.error(f"Could not locate input element for question {idx+1}")
|
384 |
+
print("Failed to find any input element for this question")
|
385 |
+
|
386 |
+
if input_elem:
|
387 |
+
input_elem.clear()
|
388 |
+
input_elem.send_keys(answer)
|
389 |
+
print(f"Filled text answer: {answer}")
|
390 |
+
except Exception as e:
|
391 |
+
st.error(f"Error filling text question {idx+1}: {e}")
|
392 |
+
print(f"Exception: {str(e)}")
|
393 |
+
|
394 |
+
print("\n---------- Form filling completed ----------")
|
395 |
+
return True
|
396 |
+
|
397 |
+
def login_to_google(driver, email, password):
|
398 |
+
"""
|
399 |
+
Logs into Google account using the provided credentials.
|
400 |
+
"""
|
401 |
+
try:
|
402 |
+
# Navigate to Google login page
|
403 |
+
driver.get("https://accounts.google.com/signin")
|
404 |
+
time.sleep(2)
|
405 |
+
|
406 |
+
# Take screenshot to show the login page
|
407 |
+
screenshot = take_screenshot(driver)
|
408 |
+
st.image(screenshot, caption="Login Page", use_column_width=True)
|
409 |
+
|
410 |
+
# Enter email
|
411 |
+
email_input = WebDriverWait(driver, 10).until(
|
412 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='email']"))
|
413 |
+
)
|
414 |
+
email_input.clear()
|
415 |
+
email_input.send_keys(email)
|
416 |
+
email_input.send_keys(Keys.RETURN)
|
417 |
+
time.sleep(2)
|
418 |
+
|
419 |
+
# Take screenshot after email entry
|
420 |
+
screenshot = take_screenshot(driver)
|
421 |
+
st.image(screenshot, caption="Email Entered", use_column_width=True)
|
422 |
+
|
423 |
+
# Enter password
|
424 |
+
password_input = WebDriverWait(driver, 10).until(
|
425 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='password']"))
|
426 |
+
)
|
427 |
+
password_input.clear()
|
428 |
+
password_input.send_keys(password)
|
429 |
+
password_input.send_keys(Keys.RETURN)
|
430 |
+
time.sleep(5) # Wait for login to complete
|
431 |
+
|
432 |
+
# Take screenshot after login attempt
|
433 |
+
screenshot = take_screenshot(driver)
|
434 |
+
st.image(screenshot, caption="Login Attempt Result", use_column_width=True)
|
435 |
+
|
436 |
+
# Check if login was successful by looking for a common element on the Google account page
|
437 |
+
try:
|
438 |
+
WebDriverWait(driver, 5).until(
|
439 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "div[data-email]"))
|
440 |
+
)
|
441 |
+
return True
|
442 |
+
except:
|
443 |
+
# Check if we're no longer on the accounts.google.com/signin page
|
444 |
+
if "accounts.google.com/signin" not in driver.current_url:
|
445 |
+
return True
|
446 |
+
# Check for possible 2FA prompt
|
447 |
+
if "2-Step Verification" in driver.page_source or "verification" in driver.page_source.lower():
|
448 |
+
st.warning("Two-factor authentication detected. Please complete it in the browser window.")
|
449 |
+
return "2FA"
|
450 |
+
return False
|
451 |
+
|
452 |
+
except Exception as e:
|
453 |
+
st.error(f"Error during login: {str(e)}")
|
454 |
+
return False
|
455 |
+
def initialize_browser():
|
456 |
+
"""
|
457 |
+
Initialize a Chrome browser with Docker-compatible settings
|
458 |
+
"""
|
459 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
460 |
+
from selenium.webdriver.chrome.service import Service
|
461 |
+
|
462 |
+
chrome_options = Options()
|
463 |
+
chrome_options.add_argument("--headless=new") # Modern headless mode
|
464 |
+
chrome_options.add_argument("--no-sandbox")
|
465 |
+
chrome_options.add_argument("--disable-dev-shm-usage")
|
466 |
+
chrome_options.add_argument("--disable-gpu")
|
467 |
+
chrome_options.add_argument("--window-size=1920,1080")
|
468 |
+
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")
|
469 |
+
|
470 |
+
try:
|
471 |
+
# First attempt: Try using webdriver-manager
|
472 |
+
try:
|
473 |
+
service = Service(ChromeDriverManager().install())
|
474 |
+
driver = webdriver.Chrome(service=service, options=chrome_options)
|
475 |
+
return driver
|
476 |
+
except Exception as e1:
|
477 |
+
st.warning(f"First browser initialization attempt failed: {e1}")
|
478 |
+
|
479 |
+
# Second attempt: Try direct Chrome browser instance
|
480 |
+
try:
|
481 |
+
driver = webdriver.Chrome(options=chrome_options)
|
482 |
+
return driver
|
483 |
+
except Exception as e2:
|
484 |
+
st.error(f"Second browser initialization attempt failed: {e2}")
|
485 |
+
return None
|
486 |
+
except Exception as e:
|
487 |
+
st.error(f"Failed to initialize browser: {str(e)}")
|
488 |
+
return None
|
489 |
+
# --- Streamlit App ---
|
490 |
+
|
491 |
+
st.title("Google Form Auto Filler with Gemini")
|
492 |
+
st.write("""
|
493 |
+
This app uses a headless browser to help you fill Google Forms automatically with AI-generated answers.
|
494 |
+
You'll be able to see screenshots of what's happening in the browser as it progresses.
|
495 |
+
""")
|
496 |
+
|
497 |
+
# Initialize session state variables
|
498 |
+
if "driver" not in st.session_state:
|
499 |
+
st.session_state.driver = None
|
500 |
+
if "login_status" not in st.session_state:
|
501 |
+
st.session_state.login_status = None
|
502 |
+
if "form_filled" not in st.session_state:
|
503 |
+
st.session_state.form_filled = False
|
504 |
+
if "screenshot" not in st.session_state:
|
505 |
+
st.session_state.screenshot = None
|
506 |
+
|
507 |
+
# Step 1: Login to Google Account
|
508 |
+
st.header("Step 1: Login to Google Account")
|
509 |
+
|
510 |
+
# Collect Google credentials
|
511 |
+
with st.form("google_login"):
|
512 |
+
email = st.text_input("Google Email")
|
513 |
+
password = st.text_input("Google Password", type="password")
|
514 |
+
submit_button = st.form_submit_button("Login to Google")
|
515 |
+
|
516 |
+
if submit_button and email and password:
|
517 |
+
# Initialize browser using our Docker-compatible function
|
518 |
+
driver = initialize_browser()
|
519 |
+
|
520 |
+
if driver:
|
521 |
+
st.session_state.driver = driver
|
522 |
+
|
523 |
+
# Show initial browser window
|
524 |
+
screenshot = take_screenshot(driver)
|
525 |
+
st.session_state.screenshot = screenshot
|
526 |
+
st.image(screenshot, caption="Browser Started", use_column_width=True)
|
527 |
+
|
528 |
+
# Try to login
|
529 |
+
login_result = login_to_google(driver, email, password)
|
530 |
+
st.session_state.login_status = login_result
|
531 |
+
|
532 |
+
if login_result == True:
|
533 |
+
st.success("Login successful!")
|
534 |
+
elif login_result == "2FA":
|
535 |
+
st.warning("Two-factor authentication may be required. Check the screenshot for verification prompts.")
|
536 |
+
st.info("You might need to complete 2FA in the browser window. Screenshots will update as you proceed.")
|
537 |
+
else:
|
538 |
+
st.error("Login failed. Please check your credentials and try again.")
|
539 |
+
else:
|
540 |
+
st.error("Failed to initialize browser. Please check Docker configuration.")
|
541 |
+
|
542 |
+
|
543 |
+
# Add manual confirmation option for login
|
544 |
+
if st.session_state.login_status == False:
|
545 |
+
st.info("If you can see that you're actually logged in from the screenshot above, click the button below:")
|
546 |
+
if st.button("I'm actually logged in successfully"):
|
547 |
+
st.session_state.login_status = True
|
548 |
+
st.success("Login status manually confirmed! You can proceed to the form filling step.")
|
549 |
+
|
550 |
+
# Display a refreshing screenshot if 2FA is detected
|
551 |
+
if st.session_state.login_status == "2FA" and st.session_state.driver:
|
552 |
+
if st.button("Take New Screenshot (for 2FA completion check)"):
|
553 |
+
screenshot = take_screenshot(st.session_state.driver)
|
554 |
+
st.session_state.screenshot = screenshot
|
555 |
+
st.image(screenshot, caption="Current Browser State", use_column_width=True)
|
556 |
+
|
557 |
+
# Check if we're past the login page now
|
558 |
+
if "accounts.google.com/signin" not in st.session_state.driver.current_url:
|
559 |
+
st.success("Looks like you completed 2FA! You can proceed to the form filling step.")
|
560 |
+
st.session_state.login_status = True
|
561 |
+
|
562 |
+
if st.session_state.driver and (st.session_state.login_status == True or st.session_state.login_status == "2FA"):
|
563 |
+
st.header("Step 2: Fill Google Form")
|
564 |
+
|
565 |
+
# Make this more prominent
|
566 |
+
st.markdown("### Enter your Google Form URL below:")
|
567 |
+
form_url = st.text_input("Form URL:", key="form_url_input")
|
568 |
+
|
569 |
+
if form_url:
|
570 |
+
# Store form URL in session state so we can access it later
|
571 |
+
if "form_url" not in st.session_state:
|
572 |
+
st.session_state.form_url = form_url
|
573 |
+
|
574 |
+
# Process Form button
|
575 |
+
if st.button("Process Form", key="process_form_button") or "questions" in st.session_state:
|
576 |
+
driver = st.session_state.driver
|
577 |
+
|
578 |
+
# Only load the form if questions aren't already processed
|
579 |
+
if "questions" not in st.session_state:
|
580 |
+
driver.get(form_url)
|
581 |
+
time.sleep(5) # Allow the form to load completely
|
582 |
+
|
583 |
+
# Show the form
|
584 |
+
screenshot = take_screenshot(driver)
|
585 |
+
st.image(screenshot, caption="Google Form Loaded", use_column_width=True)
|
586 |
+
|
587 |
+
html = driver.page_source
|
588 |
+
|
589 |
+
# Extract questions from the form
|
590 |
+
questions = extract_questions_from_fb_data(html)
|
591 |
+
if not questions:
|
592 |
+
st.error("No questions extracted from the form.")
|
593 |
+
else:
|
594 |
+
st.success(f"Successfully extracted {len(questions)} questions from the form.")
|
595 |
+
|
596 |
+
# Generate answers using Google Gemini
|
597 |
+
with st.spinner("Generating answers with Gemini..."):
|
598 |
+
questions = generate_answers(questions, GEMINI_API_KEY)
|
599 |
+
|
600 |
+
# Store questions in session state
|
601 |
+
st.session_state.questions = questions
|
602 |
+
else:
|
603 |
+
# Use the stored questions
|
604 |
+
questions = st.session_state.questions
|
605 |
+
|
606 |
+
# Display the questions and answers
|
607 |
+
st.write("--- Generated Answers ---")
|
608 |
+
for idx, q in enumerate(questions, start=1):
|
609 |
+
st.write(f"**Question {idx}:** {q['question_text']}")
|
610 |
+
if q["options"]:
|
611 |
+
st.write("Options:", ", ".join(q["options"]))
|
612 |
+
else:
|
613 |
+
st.write("(No multiple-choice options)")
|
614 |
+
st.write("**Generated Answer:**", q["gemini_answer"])
|
615 |
+
st.write("---")
|
616 |
+
|
617 |
+
# Add a clear separation before form actions
|
618 |
+
st.markdown("### Form Actions")
|
619 |
+
|
620 |
+
# Fill form button - only show if form not already filled
|
621 |
+
if not st.session_state.get("form_filled", False):
|
622 |
+
if st.button("Fill Form with Generated Answers", key="fill_form_button"):
|
623 |
+
with st.spinner("Filling form..."):
|
624 |
+
# Navigate to the form again to ensure clean state
|
625 |
+
driver.get(st.session_state.form_url)
|
626 |
+
time.sleep(3)
|
627 |
+
|
628 |
+
if fill_form(driver, questions):
|
629 |
+
time.sleep(2) # Give time for all fields to be properly filled
|
630 |
+
|
631 |
+
# Take screenshot after filling
|
632 |
+
filled_screenshot = take_screenshot(driver)
|
633 |
+
st.session_state.filled_screenshot = filled_screenshot
|
634 |
+
st.session_state.form_filled = True
|
635 |
+
|
636 |
+
st.success("Form successfully filled with generated answers!")
|
637 |
+
st.image(filled_screenshot, caption="Form Filled with Answers", use_column_width=True)
|
638 |
+
|
639 |
+
# Show the filled form if it exists in session state
|
640 |
+
if st.session_state.get("form_filled", False) and "filled_screenshot" in st.session_state:
|
641 |
+
if not st.session_state.get("showing_filled_form", False):
|
642 |
+
st.image(st.session_state.filled_screenshot, caption="Form Filled with Generated Answers", use_column_width=True)
|
643 |
+
st.session_state.showing_filled_form = True
|
644 |
+
|
645 |
+
# Instruction message instead of submit button
|
646 |
+
st.success("β
Form has been filled with AI-generated answers!Just go and change your name and stuff")
|
647 |
+
st.info("π‘ You can check the answers generated by opening the form link on your browser.")
|
648 |
+
st.markdown(f"π **Form Link:** [Open in Browser]({form_url})")
|
649 |
+
|
650 |
+
# Option to close the browser
|
651 |
+
if st.session_state.driver:
|
652 |
+
st.markdown("---")
|
653 |
+
if st.button("Close Browser"):
|
654 |
+
st.session_state.driver.quit()
|
655 |
+
st.session_state.driver = None
|
656 |
+
st.session_state.login_status = None
|
657 |
+
st.session_state.form_filled = False
|
658 |
+
st.session_state.questions = None
|
659 |
+
st.session_state.form_url = None
|
660 |
+
st.session_state.filled_screenshot = None
|
661 |
+
st.session_state.showing_filled_form = False
|
662 |
+
st.success("Browser closed. All session data cleared.")
|
app2.py
ADDED
@@ -0,0 +1,724 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import nest_asyncio
|
3 |
+
import os
|
4 |
+
import re
|
5 |
+
import json
|
6 |
+
import time
|
7 |
+
import streamlit as st
|
8 |
+
import base64
|
9 |
+
from io import BytesIO
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
from selenium import webdriver
|
12 |
+
from selenium.webdriver.chrome.options import Options
|
13 |
+
from selenium.webdriver.common.by import By
|
14 |
+
from selenium.webdriver.common.keys import Keys
|
15 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
16 |
+
from selenium.webdriver.support import expected_conditions as EC
|
17 |
+
from google import genai
|
18 |
+
|
19 |
+
# Load environment variables from .env
|
20 |
+
load_dotenv()
|
21 |
+
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
|
22 |
+
if not GEMINI_API_KEY:
|
23 |
+
st.error("GEMINI_API_KEY environment variable not set. Please configure it properly.")
|
24 |
+
|
25 |
+
# Initialize asyncio for threaded environments
|
26 |
+
try:
|
27 |
+
asyncio.get_event_loop()
|
28 |
+
except RuntimeError:
|
29 |
+
# If there is no event loop in this thread, create one and make it current
|
30 |
+
asyncio.set_event_loop(asyncio.new_event_loop())
|
31 |
+
|
32 |
+
# Apply nest_asyncio to allow nested event loops
|
33 |
+
# (sometimes needed in Streamlit)
|
34 |
+
nest_asyncio.apply()
|
35 |
+
# --- Utility Functions ---
|
36 |
+
|
37 |
+
def take_screenshot(driver):
|
38 |
+
"""
|
39 |
+
Takes a screenshot of the current browser window and returns it as an image
|
40 |
+
that can be displayed in Streamlit.
|
41 |
+
"""
|
42 |
+
screenshot = driver.get_screenshot_as_png()
|
43 |
+
return screenshot
|
44 |
+
|
45 |
+
def extract_questions_from_fb_data(html):
|
46 |
+
"""
|
47 |
+
Parses the rendered HTML to extract questions and options from the
|
48 |
+
FB_PUBLIC_LOAD_DATA_ JavaScript variable.
|
49 |
+
"""
|
50 |
+
match = re.search(r'var\s+FB_PUBLIC_LOAD_DATA_\s*=\s*(\[.*?\]);</script>', html, re.DOTALL)
|
51 |
+
if not match:
|
52 |
+
st.error("FB_PUBLIC_LOAD_DATA_ not found in HTML.")
|
53 |
+
return []
|
54 |
+
raw_json = match.group(1)
|
55 |
+
# Replace common escaped sequences for valid JSON
|
56 |
+
replacements = {
|
57 |
+
r'\\n': '\n',
|
58 |
+
r'\\u003c': '<',
|
59 |
+
r'\\u003e': '>',
|
60 |
+
r'\\u0026': '&',
|
61 |
+
r'\\"': '"'
|
62 |
+
}
|
63 |
+
for old, new in replacements.items():
|
64 |
+
raw_json = raw_json.replace(old, new)
|
65 |
+
raw_json = re.sub(r'[\x00-\x08\x0B-\x1F\x7F]', '', raw_json)
|
66 |
+
try:
|
67 |
+
data = json.loads(raw_json)
|
68 |
+
except json.JSONDecodeError as e:
|
69 |
+
st.error(f"Error decoding FB_PUBLIC_LOAD_DATA_ JSON: {e}")
|
70 |
+
return []
|
71 |
+
|
72 |
+
# Typically, questions are stored in data[1][1]
|
73 |
+
questions = []
|
74 |
+
try:
|
75 |
+
questions_data = data[1][1]
|
76 |
+
except (IndexError, TypeError):
|
77 |
+
return questions
|
78 |
+
|
79 |
+
for item in questions_data:
|
80 |
+
if not isinstance(item, list) or len(item) < 2:
|
81 |
+
continue
|
82 |
+
q_text = item[1] if isinstance(item[1], str) else None
|
83 |
+
if not q_text:
|
84 |
+
continue
|
85 |
+
q_text = q_text.strip()
|
86 |
+
# For multiple-choice questions, options usually appear in item[4]
|
87 |
+
choices = []
|
88 |
+
if len(item) > 4 and isinstance(item[4], list):
|
89 |
+
for block in item[4]:
|
90 |
+
if isinstance(block, list) and len(block) > 1 and isinstance(block[1], list):
|
91 |
+
for opt in block[1]:
|
92 |
+
if isinstance(opt, list) and len(opt) > 0 and isinstance(opt[0], str):
|
93 |
+
choices.append(opt[0])
|
94 |
+
questions.append({
|
95 |
+
"question_text": q_text,
|
96 |
+
"options": choices
|
97 |
+
})
|
98 |
+
return questions
|
99 |
+
|
100 |
+
def generate_answers(questions, api_key):
|
101 |
+
"""
|
102 |
+
For each question, call Google Gemini to generate an answer that matches available options.
|
103 |
+
"""
|
104 |
+
try:
|
105 |
+
# Ensure we have an event loop in this thread
|
106 |
+
try:
|
107 |
+
loop = asyncio.get_event_loop()
|
108 |
+
except RuntimeError:
|
109 |
+
loop = asyncio.new_event_loop()
|
110 |
+
asyncio.set_event_loop(loop)
|
111 |
+
|
112 |
+
client = genai.Client(api_key=api_key)
|
113 |
+
|
114 |
+
# Check if we have user personal info to use
|
115 |
+
user_name = st.session_state.get("user_name", "")
|
116 |
+
user_roll_no = st.session_state.get("user_roll_no", "")
|
117 |
+
user_prn = st.session_state.get("user_prn", "")
|
118 |
+
|
119 |
+
for q in questions:
|
120 |
+
question_text = q["question_text"]
|
121 |
+
options = q["options"]
|
122 |
+
|
123 |
+
# Check if this appears to be a personal information field
|
124 |
+
q_lower = question_text.lower()
|
125 |
+
|
126 |
+
# Handle personal information fields first
|
127 |
+
if any(name_word in q_lower for name_word in ["name", "full name", "your name"]) and user_name:
|
128 |
+
q["gemini_answer"] = user_name
|
129 |
+
q["is_personal_info"] = True
|
130 |
+
continue
|
131 |
+
|
132 |
+
if any(roll_word in q_lower for roll_word in ["roll", "roll no", "roll number"]) and user_roll_no:
|
133 |
+
q["gemini_answer"] = user_roll_no
|
134 |
+
q["is_personal_info"] = True
|
135 |
+
continue
|
136 |
+
|
137 |
+
if any(prn_word in q_lower for prn_word in ["prn", "prn no", "prn number", "gr number", "grn", "registration"]) and user_prn:
|
138 |
+
q["gemini_answer"] = user_prn
|
139 |
+
q["is_personal_info"] = True
|
140 |
+
continue
|
141 |
+
|
142 |
+
# For email/phone/other common personal fields, mark them but don't auto-fill
|
143 |
+
if any(info_word in q_lower for info_word in ["email", "phone", "mobile", "address", "contact", "dob", "date of birth"]):
|
144 |
+
q["is_personal_info"] = True
|
145 |
+
q["gemini_answer"] = "[PLEASE FILL THIS FIELD MANUALLY]"
|
146 |
+
continue
|
147 |
+
|
148 |
+
# For other questions, proceed with regular answer generation
|
149 |
+
if options:
|
150 |
+
prompt = f"""
|
151 |
+
Question: {question_text}
|
152 |
+
|
153 |
+
These are the EXACT options (choose only one):
|
154 |
+
{', '.join([f'"{opt}"' for opt in options])}
|
155 |
+
|
156 |
+
Instructions:
|
157 |
+
1. Choose exactly ONE option from the list above
|
158 |
+
2. Return ONLY the exact text of the chosen option, nothing else
|
159 |
+
3. Do not add any explanation, just the option text
|
160 |
+
4. Do not add quotation marks around the option
|
161 |
+
5. Do not answer questions asking for personal information like name, roll number, PRN, email, etc.
|
162 |
+
|
163 |
+
Answer:
|
164 |
+
"""
|
165 |
+
else:
|
166 |
+
prompt = f"""
|
167 |
+
Question: {question_text}
|
168 |
+
|
169 |
+
Please provide a brief and direct answer to this question.
|
170 |
+
Keep your answer concise (1-2 sentences maximum).
|
171 |
+
Do not answer if this is asking for personal information like name, roll number, PRN, email, etc.
|
172 |
+
|
173 |
+
Answer:
|
174 |
+
"""
|
175 |
+
|
176 |
+
try:
|
177 |
+
# Rest of your existing function code...
|
178 |
+
response = client.models.generate_content(
|
179 |
+
model="gemini-2.0-flash",
|
180 |
+
contents=prompt
|
181 |
+
)
|
182 |
+
|
183 |
+
# Existing answer processing...
|
184 |
+
answer = response.text.strip()
|
185 |
+
|
186 |
+
# For multiple choice, ensure it exactly matches one of the options
|
187 |
+
if options:
|
188 |
+
exact_match = False
|
189 |
+
for opt in options:
|
190 |
+
if opt.lower() == answer.lower():
|
191 |
+
answer = opt # Use the exact casing from the original option
|
192 |
+
exact_match = True
|
193 |
+
break
|
194 |
+
|
195 |
+
# If no exact match, use the most similar option
|
196 |
+
if not exact_match:
|
197 |
+
from difflib import SequenceMatcher
|
198 |
+
best_match = max(options, key=lambda opt: SequenceMatcher(None, opt.lower(), answer.lower()).ratio())
|
199 |
+
answer = best_match
|
200 |
+
|
201 |
+
q["gemini_answer"] = answer
|
202 |
+
|
203 |
+
except Exception as e:
|
204 |
+
q["gemini_answer"] = f"Error: {str(e)}"
|
205 |
+
st.error(f"Error generating answer: {str(e)}")
|
206 |
+
|
207 |
+
return questions
|
208 |
+
|
209 |
+
except Exception as e:
|
210 |
+
st.error(f"Error in generate_answers function: {str(e)}")
|
211 |
+
# Return questions with error messages
|
212 |
+
for q in questions:
|
213 |
+
if "gemini_answer" not in q:
|
214 |
+
q["gemini_answer"] = f"Error: Could not generate answer due to {str(e)}"
|
215 |
+
return questions
|
216 |
+
|
217 |
+
def fill_form(driver, questions):
|
218 |
+
"""
|
219 |
+
Fills the Google Form with generated answers using the provided driver.
|
220 |
+
"""
|
221 |
+
# Locate question containers (try different selectors)
|
222 |
+
question_containers = driver.find_elements(By.CSS_SELECTOR, "div.freebirdFormviewerViewItemsItemItem")
|
223 |
+
if not question_containers:
|
224 |
+
question_containers = driver.find_elements(By.CSS_SELECTOR, "div[role='listitem']")
|
225 |
+
if not question_containers:
|
226 |
+
st.error("Could not locate question containers in the form.")
|
227 |
+
return False
|
228 |
+
|
229 |
+
# Print total questions found for debugging
|
230 |
+
print(f"Found {len(question_containers)} question containers in the form")
|
231 |
+
print(f"We have {len(questions)} questions with answers to fill")
|
232 |
+
|
233 |
+
# Give the form time to fully render
|
234 |
+
time.sleep(2)
|
235 |
+
|
236 |
+
for idx, q in enumerate(questions):
|
237 |
+
if idx >= len(question_containers):
|
238 |
+
break
|
239 |
+
|
240 |
+
print(f"\n--------- Processing Question {idx+1} ---------")
|
241 |
+
container = question_containers[idx]
|
242 |
+
answer = q.get("gemini_answer", "").strip()
|
243 |
+
options = q.get("options", [])
|
244 |
+
|
245 |
+
# Print question and answer for debugging
|
246 |
+
print(f"Question: {q['question_text']}")
|
247 |
+
print(f"Generated Answer: {answer}")
|
248 |
+
|
249 |
+
if options:
|
250 |
+
try:
|
251 |
+
print(f"This is a multiple-choice question with {len(options)} options")
|
252 |
+
|
253 |
+
# Try multiple selector strategies to find radio buttons or checkboxes
|
254 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "div[role='radio']")
|
255 |
+
if not option_elements:
|
256 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "label")
|
257 |
+
if not option_elements:
|
258 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "div.appsMaterialWizToggleRadiogroupRadioButtonContainer")
|
259 |
+
if not option_elements:
|
260 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, ".docssharedWizToggleLabeledLabelWrapper")
|
261 |
+
|
262 |
+
if not option_elements:
|
263 |
+
st.warning(f"Could not find option elements for question {idx+1}")
|
264 |
+
print("No option elements found with any selector strategy")
|
265 |
+
continue
|
266 |
+
|
267 |
+
print(f"Found {len(option_elements)} option elements in the form")
|
268 |
+
|
269 |
+
# Normalize the answer text to make matching more robust
|
270 |
+
import re
|
271 |
+
normalized_answer = re.sub(r'[^\w\s]', '', answer.lower()).strip()
|
272 |
+
|
273 |
+
# First pass: Try exact matches
|
274 |
+
clicked = False
|
275 |
+
print("\nTrying exact matches...")
|
276 |
+
|
277 |
+
# Create a dictionary mapping option text to elements
|
278 |
+
option_dict = {}
|
279 |
+
|
280 |
+
# First extract all option texts
|
281 |
+
for i, opt_elem in enumerate(option_elements):
|
282 |
+
# Get text directly and from child elements if needed
|
283 |
+
opt_text = opt_elem.text.strip()
|
284 |
+
|
285 |
+
# If no text, try getting from child elements
|
286 |
+
if not opt_text:
|
287 |
+
for child in opt_elem.find_elements(By.XPATH, ".//div"):
|
288 |
+
child_text = child.text.strip()
|
289 |
+
if child_text:
|
290 |
+
opt_text = child_text
|
291 |
+
break
|
292 |
+
|
293 |
+
# Still no text? Try aria-label
|
294 |
+
if not opt_text:
|
295 |
+
opt_text = opt_elem.get_attribute("aria-label") or ""
|
296 |
+
|
297 |
+
# Store in dictionary for later use if we have text
|
298 |
+
if opt_text:
|
299 |
+
normalized_opt = re.sub(r'[^\w\s]', '', opt_text.lower()).strip()
|
300 |
+
option_dict[normalized_opt] = opt_elem
|
301 |
+
print(f"Option {i+1}: '{opt_text}' (normalized: '{normalized_opt}')")
|
302 |
+
else:
|
303 |
+
print(f"Option {i+1}: [NO TEXT FOUND]")
|
304 |
+
|
305 |
+
# Try exact match
|
306 |
+
if normalized_answer in option_dict:
|
307 |
+
print(f"Found exact match for: '{normalized_answer}'")
|
308 |
+
option_dict[normalized_answer].click()
|
309 |
+
clicked = True
|
310 |
+
else:
|
311 |
+
# Try substring matches
|
312 |
+
for opt_text, opt_elem in option_dict.items():
|
313 |
+
if opt_text in normalized_answer or normalized_answer in opt_text:
|
314 |
+
print(f"Found partial match: '{opt_text}' with answer '{normalized_answer}'")
|
315 |
+
opt_elem.click()
|
316 |
+
clicked = True
|
317 |
+
break
|
318 |
+
|
319 |
+
# Try matching with original options
|
320 |
+
if not clicked:
|
321 |
+
print("\nTrying to match with original options list...")
|
322 |
+
for i, original_opt in enumerate(options):
|
323 |
+
print(f"Original option {i+1}: '{original_opt}'")
|
324 |
+
normalized_orig = re.sub(r'[^\w\s]', '', original_opt.lower()).strip()
|
325 |
+
|
326 |
+
# First check direct equality
|
327 |
+
if normalized_orig == normalized_answer:
|
328 |
+
print(f"EXACT match with original option: '{original_opt}'")
|
329 |
+
|
330 |
+
# Find matching element in the dictionary or by position
|
331 |
+
if normalized_orig in option_dict:
|
332 |
+
option_dict[normalized_orig].click()
|
333 |
+
clicked = True
|
334 |
+
break
|
335 |
+
elif i < len(option_elements):
|
336 |
+
print(f"Clicking by position: element {i}")
|
337 |
+
option_elements[i].click()
|
338 |
+
clicked = True
|
339 |
+
break
|
340 |
+
|
341 |
+
# Then try substring matching
|
342 |
+
elif normalized_orig in normalized_answer or normalized_answer in normalized_orig:
|
343 |
+
print(f"PARTIAL match with original option: '{original_opt}'")
|
344 |
+
if i < len(option_elements):
|
345 |
+
option_elements[i].click()
|
346 |
+
clicked = True
|
347 |
+
break
|
348 |
+
|
349 |
+
# Try similarity matching as last resort
|
350 |
+
if not clicked:
|
351 |
+
print("\nNo direct matches found, trying similarity matching...")
|
352 |
+
from difflib import SequenceMatcher
|
353 |
+
|
354 |
+
# Try matching with form elements
|
355 |
+
best_score = 0
|
356 |
+
best_element = None
|
357 |
+
for opt_text, opt_elem in option_dict.items():
|
358 |
+
score = SequenceMatcher(None, opt_text, normalized_answer).ratio()
|
359 |
+
if score > best_score and score > 0.6: # Require at least 60% similarity
|
360 |
+
best_score = score
|
361 |
+
best_element = opt_elem
|
362 |
+
|
363 |
+
if best_element:
|
364 |
+
print(f"Best similarity match score: {best_score}")
|
365 |
+
best_element.click()
|
366 |
+
clicked = True
|
367 |
+
else:
|
368 |
+
# Try matching with original options
|
369 |
+
best_score = 0
|
370 |
+
best_idx = 0
|
371 |
+
for i, original_opt in enumerate(options):
|
372 |
+
normalized_orig = re.sub(r'[^\w\s]', '', original_opt.lower()).strip()
|
373 |
+
score = SequenceMatcher(None, normalized_orig, normalized_answer).ratio()
|
374 |
+
if score > best_score:
|
375 |
+
best_score = score
|
376 |
+
best_idx = i
|
377 |
+
|
378 |
+
if best_score > 0.5 and best_idx < len(option_elements): # 50% similarity threshold
|
379 |
+
print(f"Best similarity with original option: '{options[best_idx]}' (score: {best_score})")
|
380 |
+
option_elements[best_idx].click()
|
381 |
+
clicked = True
|
382 |
+
|
383 |
+
# Last resort: click first option if nothing matched
|
384 |
+
if not clicked and option_elements:
|
385 |
+
st.warning(f"No match found for question {idx+1}, selecting first option as fallback")
|
386 |
+
print("No suitable match found, clicking first option as fallback")
|
387 |
+
option_elements[0].click()
|
388 |
+
|
389 |
+
except Exception as e:
|
390 |
+
st.error(f"Error filling multiple-choice question {idx+1}: {e}")
|
391 |
+
print(f"Exception: {str(e)}")
|
392 |
+
else:
|
393 |
+
try:
|
394 |
+
print("This is a text question")
|
395 |
+
# For text questions, locate the text input or textarea and fill in the answer
|
396 |
+
input_elem = None
|
397 |
+
|
398 |
+
# Try multiple strategies to find the text input
|
399 |
+
try:
|
400 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "input[type='text']")
|
401 |
+
print("Found text input element")
|
402 |
+
except Exception:
|
403 |
+
try:
|
404 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "textarea")
|
405 |
+
print("Found textarea element")
|
406 |
+
except Exception:
|
407 |
+
try:
|
408 |
+
# Try more generic selectors
|
409 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "input")
|
410 |
+
print("Found generic input element")
|
411 |
+
except Exception:
|
412 |
+
try:
|
413 |
+
input_elem = container.find_element(By.TAG_NAME, "textarea")
|
414 |
+
print("Found generic textarea element")
|
415 |
+
except Exception:
|
416 |
+
st.error(f"Could not locate input element for question {idx+1}")
|
417 |
+
print("Failed to find any input element for this question")
|
418 |
+
|
419 |
+
if input_elem:
|
420 |
+
input_elem.clear()
|
421 |
+
input_elem.send_keys(answer)
|
422 |
+
print(f"Filled text answer: {answer}")
|
423 |
+
except Exception as e:
|
424 |
+
st.error(f"Error filling text question {idx+1}: {e}")
|
425 |
+
print(f"Exception: {str(e)}")
|
426 |
+
|
427 |
+
print("\n---------- Form filling completed ----------")
|
428 |
+
return True
|
429 |
+
|
430 |
+
def login_to_google(driver, email, password):
|
431 |
+
"""
|
432 |
+
Logs into Google account using the provided credentials.
|
433 |
+
"""
|
434 |
+
try:
|
435 |
+
# Navigate to Google login page
|
436 |
+
driver.get("https://accounts.google.com/signin")
|
437 |
+
time.sleep(2)
|
438 |
+
|
439 |
+
# Take screenshot to show the login page
|
440 |
+
screenshot = take_screenshot(driver)
|
441 |
+
st.image(screenshot, caption="Login Page", use_column_width=True)
|
442 |
+
|
443 |
+
# Enter email
|
444 |
+
email_input = WebDriverWait(driver, 10).until(
|
445 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='email']"))
|
446 |
+
)
|
447 |
+
email_input.clear()
|
448 |
+
email_input.send_keys(email)
|
449 |
+
email_input.send_keys(Keys.RETURN)
|
450 |
+
time.sleep(2)
|
451 |
+
|
452 |
+
# Take screenshot after email entry
|
453 |
+
screenshot = take_screenshot(driver)
|
454 |
+
st.image(screenshot, caption="Email Entered", use_column_width=True)
|
455 |
+
|
456 |
+
# Enter password
|
457 |
+
password_input = WebDriverWait(driver, 10).until(
|
458 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='password']"))
|
459 |
+
)
|
460 |
+
password_input.clear()
|
461 |
+
password_input.send_keys(password)
|
462 |
+
password_input.send_keys(Keys.RETURN)
|
463 |
+
time.sleep(5) # Wait for login to complete
|
464 |
+
|
465 |
+
# Take screenshot after login attempt
|
466 |
+
screenshot = take_screenshot(driver)
|
467 |
+
st.image(screenshot, caption="Login Attempt Result", use_column_width=True)
|
468 |
+
|
469 |
+
# Check if login was successful by looking for a common element on the Google account page
|
470 |
+
try:
|
471 |
+
WebDriverWait(driver, 5).until(
|
472 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "div[data-email]"))
|
473 |
+
)
|
474 |
+
return True
|
475 |
+
except:
|
476 |
+
# Check if we're no longer on the accounts.google.com/signin page
|
477 |
+
if "accounts.google.com/signin" not in driver.current_url:
|
478 |
+
return True
|
479 |
+
# Check for possible 2FA prompt
|
480 |
+
if "2-Step Verification" in driver.page_source or "verification" in driver.page_source.lower():
|
481 |
+
st.warning("Two-factor authentication detected. Please complete it in the browser window.")
|
482 |
+
return "2FA"
|
483 |
+
return False
|
484 |
+
|
485 |
+
except Exception as e:
|
486 |
+
st.error(f"Error during login: {str(e)}")
|
487 |
+
return False
|
488 |
+
def initialize_browser():
|
489 |
+
"""
|
490 |
+
Initialize a Chrome browser with Docker-compatible settings
|
491 |
+
"""
|
492 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
493 |
+
from selenium.webdriver.chrome.service import Service
|
494 |
+
|
495 |
+
chrome_options = Options()
|
496 |
+
chrome_options.add_argument("--headless=new") # Modern headless mode
|
497 |
+
chrome_options.add_argument("--no-sandbox")
|
498 |
+
chrome_options.add_argument("--disable-dev-shm-usage")
|
499 |
+
chrome_options.add_argument("--disable-gpu")
|
500 |
+
chrome_options.add_argument("--window-size=1920,1080")
|
501 |
+
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")
|
502 |
+
|
503 |
+
try:
|
504 |
+
# First attempt: Try using webdriver-manager
|
505 |
+
try:
|
506 |
+
service = Service(ChromeDriverManager().install())
|
507 |
+
driver = webdriver.Chrome(service=service, options=chrome_options)
|
508 |
+
return driver
|
509 |
+
except Exception as e1:
|
510 |
+
st.warning(f"First browser initialization attempt failed: {e1}")
|
511 |
+
|
512 |
+
# Second attempt: Try direct Chrome browser instance
|
513 |
+
try:
|
514 |
+
driver = webdriver.Chrome(options=chrome_options)
|
515 |
+
return driver
|
516 |
+
except Exception as e2:
|
517 |
+
st.error(f"Second browser initialization attempt failed: {e2}")
|
518 |
+
return None
|
519 |
+
except Exception as e:
|
520 |
+
st.error(f"Failed to initialize browser: {str(e)}")
|
521 |
+
return None
|
522 |
+
# --- Streamlit App ---
|
523 |
+
|
524 |
+
st.title("Google Form Auto Filler with Gemini")
|
525 |
+
st.write("""
|
526 |
+
This app uses a headless browser to help you fill Google Forms automatically with AI-generated answers.
|
527 |
+
You'll be able to see screenshots of what's happening in the browser as it progresses.
|
528 |
+
""")
|
529 |
+
|
530 |
+
# NEW CODE: Add instructions for personal info feature
|
531 |
+
st.info("""
|
532 |
+
**NEW FEATURE**: You can now provide your name, roll number, and PRN number to auto-fill personal information fields in forms.
|
533 |
+
The app will detect fields asking for this information and use your provided details instead of AI-generated answers.
|
534 |
+
""")
|
535 |
+
|
536 |
+
# Initialize session state variables
|
537 |
+
if "driver" not in st.session_state:
|
538 |
+
st.session_state.driver = None
|
539 |
+
if "login_status" not in st.session_state:
|
540 |
+
st.session_state.login_status = None
|
541 |
+
if "form_filled" not in st.session_state:
|
542 |
+
st.session_state.form_filled = False
|
543 |
+
if "screenshot" not in st.session_state:
|
544 |
+
st.session_state.screenshot = None
|
545 |
+
|
546 |
+
# Step 1: Login to Google Account
|
547 |
+
st.header("Step 1: Login to Google Account")
|
548 |
+
|
549 |
+
# Collect Google credentials
|
550 |
+
with st.form("google_login"):
|
551 |
+
email = st.text_input("Google Email")
|
552 |
+
password = st.text_input("Google Password", type="password")
|
553 |
+
submit_button = st.form_submit_button("Login to Google")
|
554 |
+
|
555 |
+
if submit_button and email and password:
|
556 |
+
# Initialize browser using our Docker-compatible function
|
557 |
+
driver = initialize_browser()
|
558 |
+
|
559 |
+
if driver:
|
560 |
+
st.session_state.driver = driver
|
561 |
+
|
562 |
+
# Show initial browser window
|
563 |
+
screenshot = take_screenshot(driver)
|
564 |
+
st.session_state.screenshot = screenshot
|
565 |
+
st.image(screenshot, caption="Browser Started", use_column_width=True)
|
566 |
+
|
567 |
+
# Try to login
|
568 |
+
login_result = login_to_google(driver, email, password)
|
569 |
+
st.session_state.login_status = login_result
|
570 |
+
|
571 |
+
if login_result == True:
|
572 |
+
st.success("Login successful!")
|
573 |
+
elif login_result == "2FA":
|
574 |
+
st.warning("Two-factor authentication may be required. Check the screenshot for verification prompts.")
|
575 |
+
st.info("You might need to complete 2FA in the browser window. Screenshots will update as you proceed.")
|
576 |
+
else:
|
577 |
+
st.error("Login failed. Please check your credentials and try again.")
|
578 |
+
else:
|
579 |
+
st.error("Failed to initialize browser. Please check Docker configuration.")
|
580 |
+
|
581 |
+
|
582 |
+
# Add manual confirmation option for login
|
583 |
+
if st.session_state.login_status == False:
|
584 |
+
st.info("If you can see that you're actually logged in from the screenshot above, click the button below:")
|
585 |
+
if st.button("I'm actually logged in successfully"):
|
586 |
+
st.session_state.login_status = True
|
587 |
+
st.success("Login status manually confirmed! You can proceed to the form filling step.")
|
588 |
+
|
589 |
+
# Display a refreshing screenshot if 2FA is detected
|
590 |
+
if st.session_state.login_status == "2FA" and st.session_state.driver:
|
591 |
+
if st.button("Take New Screenshot (for 2FA completion check)"):
|
592 |
+
screenshot = take_screenshot(st.session_state.driver)
|
593 |
+
st.session_state.screenshot = screenshot
|
594 |
+
st.image(screenshot, caption="Current Browser State", use_column_width=True)
|
595 |
+
|
596 |
+
# Check if we're past the login page now
|
597 |
+
if "accounts.google.com/signin" not in st.session_state.driver.current_url:
|
598 |
+
st.success("Looks like you completed 2FA! You can proceed to the form filling step.")
|
599 |
+
st.session_state.login_status = True
|
600 |
+
|
601 |
+
if st.session_state.driver and (st.session_state.login_status == True or st.session_state.login_status == "2FA"):
|
602 |
+
st.header("Step 2: Fill Google Form")
|
603 |
+
|
604 |
+
# Make this more prominent
|
605 |
+
st.markdown("### Enter your Google Form URL below:")
|
606 |
+
form_url = st.text_input("Form URL:", key="form_url_input")
|
607 |
+
|
608 |
+
# Add this code after the Google Form URL input section, before the Process Form button
|
609 |
+
if form_url:
|
610 |
+
# Store form URL in session state so we can access it later
|
611 |
+
if "form_url" not in st.session_state:
|
612 |
+
st.session_state.form_url = form_url
|
613 |
+
|
614 |
+
# NEW CODE: Add personal information fields (FIXED VERSION)
|
615 |
+
st.markdown("### Personal Information")
|
616 |
+
st.info("If the form has fields for name, roll number or PRN, enter them below to auto-fill those fields.")
|
617 |
+
|
618 |
+
col1, col2 = st.columns(2)
|
619 |
+
|
620 |
+
with col1:
|
621 |
+
# Values will automatically be stored in session state with these keys
|
622 |
+
st.text_input("Your Name:", key="user_name")
|
623 |
+
|
624 |
+
with col2:
|
625 |
+
st.text_input("Roll Number:", key="user_roll_no")
|
626 |
+
|
627 |
+
st.text_input("PRN Number:", key="user_prn")
|
628 |
+
|
629 |
+
# Process Form button (existing code)
|
630 |
+
if st.button("Process Form", key="process_form_button") or "questions" in st.session_state:
|
631 |
+
|
632 |
+
driver = st.session_state.driver
|
633 |
+
|
634 |
+
# Only load the form if questions aren't already processed
|
635 |
+
if "questions" not in st.session_state:
|
636 |
+
driver.get(form_url)
|
637 |
+
time.sleep(5) # Allow the form to load completely
|
638 |
+
|
639 |
+
# Show the form
|
640 |
+
screenshot = take_screenshot(driver)
|
641 |
+
st.image(screenshot, caption="Google Form Loaded", use_column_width=True)
|
642 |
+
|
643 |
+
html = driver.page_source
|
644 |
+
|
645 |
+
# Extract questions from the form
|
646 |
+
questions = extract_questions_from_fb_data(html)
|
647 |
+
if not questions:
|
648 |
+
st.error("No questions extracted from the form.")
|
649 |
+
else:
|
650 |
+
st.success(f"Successfully extracted {len(questions)} questions from the form.")
|
651 |
+
|
652 |
+
# Generate answers using Google Gemini
|
653 |
+
with st.spinner("Generating answers with Gemini..."):
|
654 |
+
questions = generate_answers(questions, GEMINI_API_KEY)
|
655 |
+
|
656 |
+
# Store questions in session state
|
657 |
+
st.session_state.questions = questions
|
658 |
+
else:
|
659 |
+
# Use the stored questions
|
660 |
+
questions = st.session_state.questions
|
661 |
+
|
662 |
+
# Display the questions and answers
|
663 |
+
st.write("--- Generated Answers ---")
|
664 |
+
for idx, q in enumerate(questions, start=1):
|
665 |
+
st.write(f"**Question {idx}:** {q['question_text']}")
|
666 |
+
if q["options"]:
|
667 |
+
st.write("Options:", ", ".join(q["options"]))
|
668 |
+
else:
|
669 |
+
st.write("(No multiple-choice options)")
|
670 |
+
|
671 |
+
# Check if this was auto-filled with personal info
|
672 |
+
if q.get("is_personal_info", False):
|
673 |
+
st.write("**Personal Info Auto-Fill:** ", q["gemini_answer"])
|
674 |
+
st.info("This field was detected as personal information and will be filled with your provided details.")
|
675 |
+
else:
|
676 |
+
st.write("**Generated Answer:** ", q["gemini_answer"])
|
677 |
+
st.write("---")
|
678 |
+
|
679 |
+
# Add a clear separation before form actions
|
680 |
+
st.markdown("### Form Actions")
|
681 |
+
|
682 |
+
# Fill form button - only show if form not already filled
|
683 |
+
if not st.session_state.get("form_filled", False):
|
684 |
+
if st.button("Fill Form with Generated Answers", key="fill_form_button"):
|
685 |
+
with st.spinner("Filling form..."):
|
686 |
+
# Navigate to the form again to ensure clean state
|
687 |
+
driver.get(st.session_state.form_url)
|
688 |
+
time.sleep(3)
|
689 |
+
|
690 |
+
if fill_form(driver, questions):
|
691 |
+
time.sleep(2) # Give time for all fields to be properly filled
|
692 |
+
|
693 |
+
# Take screenshot after filling
|
694 |
+
filled_screenshot = take_screenshot(driver)
|
695 |
+
st.session_state.filled_screenshot = filled_screenshot
|
696 |
+
st.session_state.form_filled = True
|
697 |
+
|
698 |
+
st.success("Form successfully filled with generated answers!")
|
699 |
+
st.image(filled_screenshot, caption="Form Filled with Answers", use_column_width=True)
|
700 |
+
|
701 |
+
# Show the filled form if it exists in session state
|
702 |
+
if st.session_state.get("form_filled", False) and "filled_screenshot" in st.session_state:
|
703 |
+
if not st.session_state.get("showing_filled_form", False):
|
704 |
+
st.image(st.session_state.filled_screenshot, caption="Form Filled with Generated Answers", use_column_width=True)
|
705 |
+
st.session_state.showing_filled_form = True
|
706 |
+
|
707 |
+
# Instruction message instead of submit button
|
708 |
+
st.success("β
Form has been filled with AI-generated answers!Just go and change your name and stuff")
|
709 |
+
st.info("π‘ You can check the answers generated by opening the form link on your browser.")
|
710 |
+
st.markdown(f"π **Form Link:** [Open in Browser]({form_url})")
|
711 |
+
|
712 |
+
# Option to close the browser
|
713 |
+
if st.session_state.driver:
|
714 |
+
st.markdown("---")
|
715 |
+
if st.button("Close Browser"):
|
716 |
+
st.session_state.driver.quit()
|
717 |
+
st.session_state.driver = None
|
718 |
+
st.session_state.login_status = None
|
719 |
+
st.session_state.form_filled = False
|
720 |
+
st.session_state.questions = None
|
721 |
+
st.session_state.form_url = None
|
722 |
+
st.session_state.filled_screenshot = None
|
723 |
+
st.session_state.showing_filled_form = False
|
724 |
+
st.success("Browser closed. All session data cleared.")
|
app_ref.py
ADDED
@@ -0,0 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import re
|
3 |
+
import json
|
4 |
+
import time
|
5 |
+
import streamlit as st
|
6 |
+
import base64
|
7 |
+
from io import BytesIO
|
8 |
+
from dotenv import load_dotenv
|
9 |
+
from selenium import webdriver
|
10 |
+
from selenium.webdriver.chrome.options import Options
|
11 |
+
from selenium.webdriver.common.by import By
|
12 |
+
from selenium.webdriver.common.keys import Keys
|
13 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
14 |
+
from selenium.webdriver.support import expected_conditions as EC
|
15 |
+
from google import genai
|
16 |
+
|
17 |
+
# Load environment variables from .env
|
18 |
+
load_dotenv()
|
19 |
+
GEMINI_API_KEY = 'AIzaSyAOK9vRTSRQzd22B2gmbiuIePbZTDyaGYs'
|
20 |
+
|
21 |
+
# --- Utility Functions ---
|
22 |
+
|
23 |
+
def take_screenshot(driver):
|
24 |
+
"""
|
25 |
+
Takes a screenshot of the current browser window and returns it as an image
|
26 |
+
that can be displayed in Streamlit.
|
27 |
+
"""
|
28 |
+
screenshot = driver.get_screenshot_as_png()
|
29 |
+
return screenshot
|
30 |
+
|
31 |
+
def extract_questions_from_fb_data(html):
|
32 |
+
"""
|
33 |
+
Parses the rendered HTML to extract questions and options from the
|
34 |
+
FB_PUBLIC_LOAD_DATA_ JavaScript variable.
|
35 |
+
"""
|
36 |
+
match = re.search(r'var\s+FB_PUBLIC_LOAD_DATA_\s*=\s*(\[.*?\]);</script>', html, re.DOTALL)
|
37 |
+
if not match:
|
38 |
+
st.error("FB_PUBLIC_LOAD_DATA_ not found in HTML.")
|
39 |
+
return []
|
40 |
+
raw_json = match.group(1)
|
41 |
+
# Replace common escaped sequences for valid JSON
|
42 |
+
replacements = {
|
43 |
+
r'\\n': '\n',
|
44 |
+
r'\\u003c': '<',
|
45 |
+
r'\\u003e': '>',
|
46 |
+
r'\\u0026': '&',
|
47 |
+
r'\\"': '"'
|
48 |
+
}
|
49 |
+
for old, new in replacements.items():
|
50 |
+
raw_json = raw_json.replace(old, new)
|
51 |
+
raw_json = re.sub(r'[\x00-\x08\x0B-\x1F\x7F]', '', raw_json)
|
52 |
+
try:
|
53 |
+
data = json.loads(raw_json)
|
54 |
+
except json.JSONDecodeError as e:
|
55 |
+
st.error(f"Error decoding FB_PUBLIC_LOAD_DATA_ JSON: {e}")
|
56 |
+
return []
|
57 |
+
|
58 |
+
# Typically, questions are stored in data[1][1]
|
59 |
+
questions = []
|
60 |
+
try:
|
61 |
+
questions_data = data[1][1]
|
62 |
+
except (IndexError, TypeError):
|
63 |
+
return questions
|
64 |
+
|
65 |
+
for item in questions_data:
|
66 |
+
if not isinstance(item, list) or len(item) < 2:
|
67 |
+
continue
|
68 |
+
q_text = item[1] if isinstance(item[1], str) else None
|
69 |
+
if not q_text:
|
70 |
+
continue
|
71 |
+
q_text = q_text.strip()
|
72 |
+
# For multiple-choice questions, options usually appear in item[4]
|
73 |
+
choices = []
|
74 |
+
if len(item) > 4 and isinstance(item[4], list):
|
75 |
+
for block in item[4]:
|
76 |
+
if isinstance(block, list) and len(block) > 1 and isinstance(block[1], list):
|
77 |
+
for opt in block[1]:
|
78 |
+
if isinstance(opt, list) and len(opt) > 0 and isinstance(opt[0], str):
|
79 |
+
choices.append(opt[0])
|
80 |
+
questions.append({
|
81 |
+
"question_text": q_text,
|
82 |
+
"options": choices
|
83 |
+
})
|
84 |
+
return questions
|
85 |
+
|
86 |
+
def generate_answers(questions, api_key):
|
87 |
+
"""
|
88 |
+
For each question, call Google Gemini to generate an answer that matches available options.
|
89 |
+
"""
|
90 |
+
client = genai.Client(api_key=api_key)
|
91 |
+
for q in questions:
|
92 |
+
question_text = q["question_text"]
|
93 |
+
options = q["options"]
|
94 |
+
|
95 |
+
if options:
|
96 |
+
# For multiple choice questions, make prompt more specific to choose exactly one option
|
97 |
+
prompt = f"""
|
98 |
+
Question: {question_text}
|
99 |
+
|
100 |
+
These are the EXACT options (choose only one):
|
101 |
+
{', '.join([f'"{opt}"' for opt in options])}
|
102 |
+
|
103 |
+
Instructions:
|
104 |
+
1. Choose exactly ONE option from the list above
|
105 |
+
2. Return ONLY the exact text of the chosen option, nothing else
|
106 |
+
3. Do not add any explanation, just the option text
|
107 |
+
4. Do not add quotation marks around the option
|
108 |
+
5. Don not answer questions like "What is your name?","Rollno","PRN/GRN","Email","Mobile No","Address","DOB etc
|
109 |
+
|
110 |
+
Answer:
|
111 |
+
"""
|
112 |
+
else:
|
113 |
+
# For free-text questions, keep it simple
|
114 |
+
prompt = f"""
|
115 |
+
Question: {question_text}
|
116 |
+
|
117 |
+
Please provide a brief and direct answer to this question.
|
118 |
+
Keep your answer concise (1-2 sentences maximum).
|
119 |
+
|
120 |
+
Answer:
|
121 |
+
"""
|
122 |
+
|
123 |
+
try:
|
124 |
+
response = client.models.generate_content(
|
125 |
+
model="gemini-2.0-flash",
|
126 |
+
contents=prompt
|
127 |
+
)
|
128 |
+
|
129 |
+
answer = response.text.strip()
|
130 |
+
|
131 |
+
# For multiple choice, ensure it exactly matches one of the options
|
132 |
+
if options:
|
133 |
+
exact_match = False
|
134 |
+
for opt in options:
|
135 |
+
if opt.lower() == answer.lower():
|
136 |
+
answer = opt # Use the exact casing from the original option
|
137 |
+
exact_match = True
|
138 |
+
break
|
139 |
+
|
140 |
+
# If no exact match, use the most similar option
|
141 |
+
if not exact_match:
|
142 |
+
from difflib import SequenceMatcher
|
143 |
+
best_match = max(options, key=lambda opt: SequenceMatcher(None, opt.lower(), answer.lower()).ratio())
|
144 |
+
answer = best_match
|
145 |
+
|
146 |
+
q["gemini_answer"] = answer
|
147 |
+
|
148 |
+
except Exception as e:
|
149 |
+
q["gemini_answer"] = f"Error: {str(e)}"
|
150 |
+
|
151 |
+
return questions
|
152 |
+
|
153 |
+
def fill_form(driver, questions):
|
154 |
+
"""
|
155 |
+
Fills the Google Form with generated answers using the provided driver.
|
156 |
+
"""
|
157 |
+
# Locate question containers (try different selectors)
|
158 |
+
question_containers = driver.find_elements(By.CSS_SELECTOR, "div.freebirdFormviewerViewItemsItemItem")
|
159 |
+
if not question_containers:
|
160 |
+
question_containers = driver.find_elements(By.CSS_SELECTOR, "div[role='listitem']")
|
161 |
+
if not question_containers:
|
162 |
+
st.error("Could not locate question containers in the form.")
|
163 |
+
return False
|
164 |
+
|
165 |
+
# Print total questions found for debugging
|
166 |
+
print(f"Found {len(question_containers)} question containers in the form")
|
167 |
+
print(f"We have {len(questions)} questions with answers to fill")
|
168 |
+
|
169 |
+
# Give the form time to fully render
|
170 |
+
time.sleep(2)
|
171 |
+
|
172 |
+
for idx, q in enumerate(questions):
|
173 |
+
if idx >= len(question_containers):
|
174 |
+
break
|
175 |
+
|
176 |
+
print(f"\n--------- Processing Question {idx+1} ---------")
|
177 |
+
container = question_containers[idx]
|
178 |
+
answer = q.get("gemini_answer", "").strip()
|
179 |
+
options = q.get("options", [])
|
180 |
+
|
181 |
+
# Print question and answer for debugging
|
182 |
+
print(f"Question: {q['question_text']}")
|
183 |
+
print(f"Generated Answer: {answer}")
|
184 |
+
|
185 |
+
if options:
|
186 |
+
try:
|
187 |
+
print(f"This is a multiple-choice question with {len(options)} options")
|
188 |
+
|
189 |
+
# Try multiple selector strategies to find radio buttons or checkboxes
|
190 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "div[role='radio']")
|
191 |
+
if not option_elements:
|
192 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "label")
|
193 |
+
if not option_elements:
|
194 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, "div.appsMaterialWizToggleRadiogroupRadioButtonContainer")
|
195 |
+
if not option_elements:
|
196 |
+
option_elements = container.find_elements(By.CSS_SELECTOR, ".docssharedWizToggleLabeledLabelWrapper")
|
197 |
+
|
198 |
+
if not option_elements:
|
199 |
+
st.warning(f"Could not find option elements for question {idx+1}")
|
200 |
+
print("No option elements found with any selector strategy")
|
201 |
+
continue
|
202 |
+
|
203 |
+
print(f"Found {len(option_elements)} option elements in the form")
|
204 |
+
|
205 |
+
# Normalize the answer text to make matching more robust
|
206 |
+
import re
|
207 |
+
normalized_answer = re.sub(r'[^\w\s]', '', answer.lower()).strip()
|
208 |
+
|
209 |
+
# First pass: Try exact matches
|
210 |
+
clicked = False
|
211 |
+
print("\nTrying exact matches...")
|
212 |
+
|
213 |
+
# Create a dictionary mapping option text to elements
|
214 |
+
option_dict = {}
|
215 |
+
|
216 |
+
# First extract all option texts
|
217 |
+
for i, opt_elem in enumerate(option_elements):
|
218 |
+
# Get text directly and from child elements if needed
|
219 |
+
opt_text = opt_elem.text.strip()
|
220 |
+
|
221 |
+
# If no text, try getting from child elements
|
222 |
+
if not opt_text:
|
223 |
+
for child in opt_elem.find_elements(By.XPATH, ".//div"):
|
224 |
+
child_text = child.text.strip()
|
225 |
+
if child_text:
|
226 |
+
opt_text = child_text
|
227 |
+
break
|
228 |
+
|
229 |
+
# Still no text? Try aria-label
|
230 |
+
if not opt_text:
|
231 |
+
opt_text = opt_elem.get_attribute("aria-label") or ""
|
232 |
+
|
233 |
+
# Store in dictionary for later use if we have text
|
234 |
+
if opt_text:
|
235 |
+
normalized_opt = re.sub(r'[^\w\s]', '', opt_text.lower()).strip()
|
236 |
+
option_dict[normalized_opt] = opt_elem
|
237 |
+
print(f"Option {i+1}: '{opt_text}' (normalized: '{normalized_opt}')")
|
238 |
+
else:
|
239 |
+
print(f"Option {i+1}: [NO TEXT FOUND]")
|
240 |
+
|
241 |
+
# Try exact match
|
242 |
+
if normalized_answer in option_dict:
|
243 |
+
print(f"Found exact match for: '{normalized_answer}'")
|
244 |
+
option_dict[normalized_answer].click()
|
245 |
+
clicked = True
|
246 |
+
else:
|
247 |
+
# Try substring matches
|
248 |
+
for opt_text, opt_elem in option_dict.items():
|
249 |
+
if opt_text in normalized_answer or normalized_answer in opt_text:
|
250 |
+
print(f"Found partial match: '{opt_text}' with answer '{normalized_answer}'")
|
251 |
+
opt_elem.click()
|
252 |
+
clicked = True
|
253 |
+
break
|
254 |
+
|
255 |
+
# Try matching with original options
|
256 |
+
if not clicked:
|
257 |
+
print("\nTrying to match with original options list...")
|
258 |
+
for i, original_opt in enumerate(options):
|
259 |
+
print(f"Original option {i+1}: '{original_opt}'")
|
260 |
+
normalized_orig = re.sub(r'[^\w\s]', '', original_opt.lower()).strip()
|
261 |
+
|
262 |
+
# First check direct equality
|
263 |
+
if normalized_orig == normalized_answer:
|
264 |
+
print(f"EXACT match with original option: '{original_opt}'")
|
265 |
+
|
266 |
+
# Find matching element in the dictionary or by position
|
267 |
+
if normalized_orig in option_dict:
|
268 |
+
option_dict[normalized_orig].click()
|
269 |
+
clicked = True
|
270 |
+
break
|
271 |
+
elif i < len(option_elements):
|
272 |
+
print(f"Clicking by position: element {i}")
|
273 |
+
option_elements[i].click()
|
274 |
+
clicked = True
|
275 |
+
break
|
276 |
+
|
277 |
+
# Then try substring matching
|
278 |
+
elif normalized_orig in normalized_answer or normalized_answer in normalized_orig:
|
279 |
+
print(f"PARTIAL match with original option: '{original_opt}'")
|
280 |
+
if i < len(option_elements):
|
281 |
+
option_elements[i].click()
|
282 |
+
clicked = True
|
283 |
+
break
|
284 |
+
|
285 |
+
# Try similarity matching as last resort
|
286 |
+
if not clicked:
|
287 |
+
print("\nNo direct matches found, trying similarity matching...")
|
288 |
+
from difflib import SequenceMatcher
|
289 |
+
|
290 |
+
# Try matching with form elements
|
291 |
+
best_score = 0
|
292 |
+
best_element = None
|
293 |
+
for opt_text, opt_elem in option_dict.items():
|
294 |
+
score = SequenceMatcher(None, opt_text, normalized_answer).ratio()
|
295 |
+
if score > best_score and score > 0.6: # Require at least 60% similarity
|
296 |
+
best_score = score
|
297 |
+
best_element = opt_elem
|
298 |
+
|
299 |
+
if best_element:
|
300 |
+
print(f"Best similarity match score: {best_score}")
|
301 |
+
best_element.click()
|
302 |
+
clicked = True
|
303 |
+
else:
|
304 |
+
# Try matching with original options
|
305 |
+
best_score = 0
|
306 |
+
best_idx = 0
|
307 |
+
for i, original_opt in enumerate(options):
|
308 |
+
normalized_orig = re.sub(r'[^\w\s]', '', original_opt.lower()).strip()
|
309 |
+
score = SequenceMatcher(None, normalized_orig, normalized_answer).ratio()
|
310 |
+
if score > best_score:
|
311 |
+
best_score = score
|
312 |
+
best_idx = i
|
313 |
+
|
314 |
+
if best_score > 0.5 and best_idx < len(option_elements): # 50% similarity threshold
|
315 |
+
print(f"Best similarity with original option: '{options[best_idx]}' (score: {best_score})")
|
316 |
+
option_elements[best_idx].click()
|
317 |
+
clicked = True
|
318 |
+
|
319 |
+
# Last resort: click first option if nothing matched
|
320 |
+
if not clicked and option_elements:
|
321 |
+
st.warning(f"No match found for question {idx+1}, selecting first option as fallback")
|
322 |
+
print("No suitable match found, clicking first option as fallback")
|
323 |
+
option_elements[0].click()
|
324 |
+
|
325 |
+
except Exception as e:
|
326 |
+
st.error(f"Error filling multiple-choice question {idx+1}: {e}")
|
327 |
+
print(f"Exception: {str(e)}")
|
328 |
+
else:
|
329 |
+
try:
|
330 |
+
print("This is a text question")
|
331 |
+
# For text questions, locate the text input or textarea and fill in the answer
|
332 |
+
input_elem = None
|
333 |
+
|
334 |
+
# Try multiple strategies to find the text input
|
335 |
+
try:
|
336 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "input[type='text']")
|
337 |
+
print("Found text input element")
|
338 |
+
except Exception:
|
339 |
+
try:
|
340 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "textarea")
|
341 |
+
print("Found textarea element")
|
342 |
+
except Exception:
|
343 |
+
try:
|
344 |
+
# Try more generic selectors
|
345 |
+
input_elem = container.find_element(By.CSS_SELECTOR, "input")
|
346 |
+
print("Found generic input element")
|
347 |
+
except Exception:
|
348 |
+
try:
|
349 |
+
input_elem = container.find_element(By.TAG_NAME, "textarea")
|
350 |
+
print("Found generic textarea element")
|
351 |
+
except Exception:
|
352 |
+
st.error(f"Could not locate input element for question {idx+1}")
|
353 |
+
print("Failed to find any input element for this question")
|
354 |
+
|
355 |
+
if input_elem:
|
356 |
+
input_elem.clear()
|
357 |
+
input_elem.send_keys(answer)
|
358 |
+
print(f"Filled text answer: {answer}")
|
359 |
+
except Exception as e:
|
360 |
+
st.error(f"Error filling text question {idx+1}: {e}")
|
361 |
+
print(f"Exception: {str(e)}")
|
362 |
+
|
363 |
+
print("\n---------- Form filling completed ----------")
|
364 |
+
return True
|
365 |
+
|
366 |
+
def login_to_google(driver, email, password):
|
367 |
+
"""
|
368 |
+
Logs into Google account using the provided credentials.
|
369 |
+
"""
|
370 |
+
try:
|
371 |
+
# Navigate to Google login page
|
372 |
+
driver.get("https://accounts.google.com/signin")
|
373 |
+
time.sleep(2)
|
374 |
+
|
375 |
+
# Take screenshot to show the login page
|
376 |
+
screenshot = take_screenshot(driver)
|
377 |
+
st.image(screenshot, caption="Login Page", use_column_width=True)
|
378 |
+
|
379 |
+
# Enter email
|
380 |
+
email_input = WebDriverWait(driver, 10).until(
|
381 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='email']"))
|
382 |
+
)
|
383 |
+
email_input.clear()
|
384 |
+
email_input.send_keys(email)
|
385 |
+
email_input.send_keys(Keys.RETURN)
|
386 |
+
time.sleep(2)
|
387 |
+
|
388 |
+
# Take screenshot after email entry
|
389 |
+
screenshot = take_screenshot(driver)
|
390 |
+
st.image(screenshot, caption="Email Entered", use_column_width=True)
|
391 |
+
|
392 |
+
# Enter password
|
393 |
+
password_input = WebDriverWait(driver, 10).until(
|
394 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='password']"))
|
395 |
+
)
|
396 |
+
password_input.clear()
|
397 |
+
password_input.send_keys(password)
|
398 |
+
password_input.send_keys(Keys.RETURN)
|
399 |
+
time.sleep(5) # Wait for login to complete
|
400 |
+
|
401 |
+
# Take screenshot after login attempt
|
402 |
+
screenshot = take_screenshot(driver)
|
403 |
+
st.image(screenshot, caption="Login Attempt Result", use_column_width=True)
|
404 |
+
|
405 |
+
# Check if login was successful by looking for a common element on the Google account page
|
406 |
+
try:
|
407 |
+
WebDriverWait(driver, 5).until(
|
408 |
+
EC.presence_of_element_located((By.CSS_SELECTOR, "div[data-email]"))
|
409 |
+
)
|
410 |
+
return True
|
411 |
+
except:
|
412 |
+
# Check if we're no longer on the accounts.google.com/signin page
|
413 |
+
if "accounts.google.com/signin" not in driver.current_url:
|
414 |
+
return True
|
415 |
+
# Check for possible 2FA prompt
|
416 |
+
if "2-Step Verification" in driver.page_source or "verification" in driver.page_source.lower():
|
417 |
+
st.warning("Two-factor authentication detected. Please complete it in the browser window.")
|
418 |
+
return "2FA"
|
419 |
+
return False
|
420 |
+
|
421 |
+
except Exception as e:
|
422 |
+
st.error(f"Error during login: {str(e)}")
|
423 |
+
return False
|
424 |
+
|
425 |
+
# --- Streamlit App ---
|
426 |
+
|
427 |
+
st.title("Google Form Auto Filler with Gemini")
|
428 |
+
st.write("""
|
429 |
+
This app uses a headless browser to help you fill Google Forms automatically with AI-generated answers.
|
430 |
+
You'll be able to see screenshots of what's happening in the browser as it progresses.
|
431 |
+
""")
|
432 |
+
|
433 |
+
# Initialize session state variables
|
434 |
+
if "driver" not in st.session_state:
|
435 |
+
st.session_state.driver = None
|
436 |
+
if "login_status" not in st.session_state:
|
437 |
+
st.session_state.login_status = None
|
438 |
+
if "form_filled" not in st.session_state:
|
439 |
+
st.session_state.form_filled = False
|
440 |
+
if "screenshot" not in st.session_state:
|
441 |
+
st.session_state.screenshot = None
|
442 |
+
|
443 |
+
# Step 1: Login to Google Account
|
444 |
+
st.header("Step 1: Login to Google Account")
|
445 |
+
|
446 |
+
# Collect Google credentials
|
447 |
+
with st.form("google_login"):
|
448 |
+
email = st.text_input("Google Email")
|
449 |
+
password = st.text_input("Google Password", type="password")
|
450 |
+
submit_button = st.form_submit_button("Login to Google")
|
451 |
+
|
452 |
+
if submit_button and email and password:
|
453 |
+
# Initialize headless Chrome browser
|
454 |
+
chrome_options = Options()
|
455 |
+
chrome_options.add_argument("--headless")
|
456 |
+
chrome_options.add_argument("--no-sandbox")
|
457 |
+
chrome_options.add_argument("--disable-dev-shm-usage")
|
458 |
+
chrome_options.add_argument("--window-size=1920,1080")
|
459 |
+
|
460 |
+
# Start the browser
|
461 |
+
driver = webdriver.Chrome(options=chrome_options)
|
462 |
+
st.session_state.driver = driver
|
463 |
+
|
464 |
+
# Show initial browser window
|
465 |
+
screenshot = take_screenshot(driver)
|
466 |
+
st.session_state.screenshot = screenshot
|
467 |
+
st.image(screenshot, caption="Browser Started", use_column_width=True)
|
468 |
+
|
469 |
+
# Try to login
|
470 |
+
login_result = login_to_google(driver, email, password)
|
471 |
+
st.session_state.login_status = login_result
|
472 |
+
|
473 |
+
if login_result == True:
|
474 |
+
st.success("Login successful!")
|
475 |
+
elif login_result == "2FA":
|
476 |
+
st.warning("Two-factor authentication may be required. Check the screenshot for verification prompts.")
|
477 |
+
st.info("You might need to complete 2FA in the browser window. Screenshots will update as you proceed.")
|
478 |
+
else:
|
479 |
+
st.error("Login failed. Please check your credentials and try again.")
|
480 |
+
|
481 |
+
# Add manual confirmation option for login
|
482 |
+
if st.session_state.login_status == False:
|
483 |
+
st.info("If you can see that you're actually logged in from the screenshot above, click the button below:")
|
484 |
+
if st.button("I'm actually logged in successfully"):
|
485 |
+
st.session_state.login_status = True
|
486 |
+
st.success("Login status manually confirmed! You can proceed to the form filling step.")
|
487 |
+
|
488 |
+
# Display a refreshing screenshot if 2FA is detected
|
489 |
+
if st.session_state.login_status == "2FA" and st.session_state.driver:
|
490 |
+
if st.button("Take New Screenshot (for 2FA completion check)"):
|
491 |
+
screenshot = take_screenshot(st.session_state.driver)
|
492 |
+
st.session_state.screenshot = screenshot
|
493 |
+
st.image(screenshot, caption="Current Browser State", use_column_width=True)
|
494 |
+
|
495 |
+
# Check if we're past the login page now
|
496 |
+
if "accounts.google.com/signin" not in st.session_state.driver.current_url:
|
497 |
+
st.success("Looks like you completed 2FA! You can proceed to the form filling step.")
|
498 |
+
st.session_state.login_status = True
|
499 |
+
|
500 |
+
if st.session_state.driver and (st.session_state.login_status == True or st.session_state.login_status == "2FA"):
|
501 |
+
st.header("Step 2: Fill Google Form")
|
502 |
+
|
503 |
+
# Make this more prominent
|
504 |
+
st.markdown("### Enter your Google Form URL below:")
|
505 |
+
form_url = st.text_input("Form URL:", key="form_url_input")
|
506 |
+
|
507 |
+
if form_url:
|
508 |
+
# Store form URL in session state so we can access it later
|
509 |
+
if "form_url" not in st.session_state:
|
510 |
+
st.session_state.form_url = form_url
|
511 |
+
|
512 |
+
# Process Form button
|
513 |
+
if st.button("Process Form", key="process_form_button") or "questions" in st.session_state:
|
514 |
+
driver = st.session_state.driver
|
515 |
+
|
516 |
+
# Only load the form if questions aren't already processed
|
517 |
+
if "questions" not in st.session_state:
|
518 |
+
driver.get(form_url)
|
519 |
+
time.sleep(5) # Allow the form to load completely
|
520 |
+
|
521 |
+
# Show the form
|
522 |
+
screenshot = take_screenshot(driver)
|
523 |
+
st.image(screenshot, caption="Google Form Loaded", use_column_width=True)
|
524 |
+
|
525 |
+
html = driver.page_source
|
526 |
+
|
527 |
+
# Extract questions from the form
|
528 |
+
questions = extract_questions_from_fb_data(html)
|
529 |
+
if not questions:
|
530 |
+
st.error("No questions extracted from the form.")
|
531 |
+
else:
|
532 |
+
st.success(f"Successfully extracted {len(questions)} questions from the form.")
|
533 |
+
|
534 |
+
# Generate answers using Google Gemini
|
535 |
+
with st.spinner("Generating answers with Gemini..."):
|
536 |
+
questions = generate_answers(questions, GEMINI_API_KEY)
|
537 |
+
|
538 |
+
# Store questions in session state
|
539 |
+
st.session_state.questions = questions
|
540 |
+
else:
|
541 |
+
# Use the stored questions
|
542 |
+
questions = st.session_state.questions
|
543 |
+
|
544 |
+
# Display the questions and answers
|
545 |
+
st.write("--- Generated Answers ---")
|
546 |
+
for idx, q in enumerate(questions, start=1):
|
547 |
+
st.write(f"**Question {idx}:** {q['question_text']}")
|
548 |
+
if q["options"]:
|
549 |
+
st.write("Options:", ", ".join(q["options"]))
|
550 |
+
else:
|
551 |
+
st.write("(No multiple-choice options)")
|
552 |
+
st.write("**Generated Answer:**", q["gemini_answer"])
|
553 |
+
st.write("---")
|
554 |
+
|
555 |
+
# Add a clear separation before form actions
|
556 |
+
st.markdown("### Form Actions")
|
557 |
+
|
558 |
+
# Fill form button - only show if form not already filled
|
559 |
+
if not st.session_state.get("form_filled", False):
|
560 |
+
if st.button("Fill Form with Generated Answers", key="fill_form_button"):
|
561 |
+
with st.spinner("Filling form..."):
|
562 |
+
# Navigate to the form again to ensure clean state
|
563 |
+
driver.get(st.session_state.form_url)
|
564 |
+
time.sleep(3)
|
565 |
+
|
566 |
+
if fill_form(driver, questions):
|
567 |
+
time.sleep(2) # Give time for all fields to be properly filled
|
568 |
+
|
569 |
+
# Take screenshot after filling
|
570 |
+
filled_screenshot = take_screenshot(driver)
|
571 |
+
st.session_state.filled_screenshot = filled_screenshot
|
572 |
+
st.session_state.form_filled = True
|
573 |
+
|
574 |
+
st.success("Form successfully filled with generated answers!")
|
575 |
+
st.image(filled_screenshot, caption="Form Filled with Answers", use_column_width=True)
|
576 |
+
|
577 |
+
# Show the filled form if it exists in session state
|
578 |
+
if st.session_state.get("form_filled", False) and "filled_screenshot" in st.session_state:
|
579 |
+
if not st.session_state.get("showing_filled_form", False):
|
580 |
+
st.image(st.session_state.filled_screenshot, caption="Form Filled with Generated Answers", use_column_width=True)
|
581 |
+
st.session_state.showing_filled_form = True
|
582 |
+
|
583 |
+
# Instruction message instead of submit button
|
584 |
+
st.success("β
Form has been filled with AI-generated answers!Just go and change your name and stuff")
|
585 |
+
st.info("π‘ You can check the answers generated by opening the form link on your browser.")
|
586 |
+
st.markdown(f"π **Form Link:** [Open in Browser]({form_url})")
|
587 |
+
|
588 |
+
# Option to close the browser
|
589 |
+
if st.session_state.driver:
|
590 |
+
st.markdown("---")
|
591 |
+
if st.button("Close Browser"):
|
592 |
+
st.session_state.driver.quit()
|
593 |
+
st.session_state.driver = None
|
594 |
+
st.session_state.login_status = None
|
595 |
+
st.session_state.form_filled = False
|
596 |
+
st.session_state.questions = None
|
597 |
+
st.session_state.form_url = None
|
598 |
+
st.session_state.filled_screenshot = None
|
599 |
+
st.session_state.showing_filled_form = False
|
600 |
+
st.success("Browser closed. All session data cleared.")
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit==1.36.0
|
2 |
+
selenium==4.18.1
|
3 |
+
google-genai
|
4 |
+
python-dotenv==1.0.1
|
5 |
+
nest-asyncio==1.6.0
|
6 |
+
webdriver-manager==4.0.1
|