PluginLiveInterns commited on
Commit
53cce60
Β·
1 Parent(s): 5647ca1

Add application file

Browse files
Files changed (5) hide show
  1. Dockerfile +63 -0
  2. app.py +662 -0
  3. app2.py +724 -0
  4. app_ref.py +600 -0
  5. 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