Skip to content

Commit b57623a

Browse files
committed
Update web app, bouncer, and intelligence modules; add Chrome extension
1 parent ec8c3cf commit b57623a

36 files changed

Lines changed: 876 additions & 8824 deletions

app/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta charset="UTF-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
77
<title>Subway Scholars - Focus Run</title>
8-
<link rel="stylesheet" href="/app/style.css">
8+
<link rel="stylesheet" href="./style.css">
99
<!-- Font Awesome for Icons -->
1010
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
1111
</head>
@@ -130,7 +130,7 @@ <h2 class="notif-title"><i class="fa-solid fa-envelope-open-text"></i> Held Noti
130130
</div>
131131
</div>
132132

133-
<script src="/app/script.js"></script>
133+
<script src="./script.js"></script>
134134
</body>
135135

136136
</html>

bouncer/blocker_ui.py

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ def __init__(self, topic: str):
2020
self.root.title("Focus Blocker")
2121

2222
# Make it 700x600 and center it
23-
window_width = 700
24-
window_height = 600
23+
window_width = 750
24+
window_height = 650
2525
screen_width = self.root.winfo_screenwidth()
2626
screen_height = self.root.winfo_screenheight()
2727
center_x = int(screen_width/2 - window_width / 2)
@@ -35,9 +35,9 @@ def __init__(self, topic: str):
3535
self.root.protocol("WM_DELETE_WINDOW", self.disable_event)
3636

3737
# Fonts & Colors
38-
self.title_font = ("Consolas", 32, "bold")
39-
self.body_font = ("Arial", 16)
40-
self.btn_font = ("Arial", 14, "bold")
38+
self.title_font = ("Impact", 34, "normal")
39+
self.body_font = ("Segoe UI", 16)
40+
self.btn_font = ("Segoe UI", 14, "bold")
4141
self.color_danger = "#ff3b3b"
4242
self.color_primary = "#FF9800"
4343
self.color_bg = "#111827"
@@ -55,14 +55,19 @@ def build_ui_loading(self):
5555
"""Displays a loading screen while Gemini generates the 3 questions."""
5656
self.clear_ui()
5757

58-
title_lbl = tk.Label(self.root, text="DISTRACTION DETECTED!", font=self.title_font, fg=self.color_danger, bg=self.color_bg)
59-
title_lbl.pack(pady=(100, 20))
58+
self.canvas = tk.Canvas(self.root, highlightthickness=0)
59+
self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
60+
colors = ["#171e2e", "#131926"]
61+
for i in range(30):
62+
y = i * 30
63+
self.canvas.create_rectangle(0, y, 1000, y+30, fill=colors[i%2], outline="")
64+
65+
self.canvas.create_text(375, 150, text="DISTRACTION DETECTED!", font=self.title_font, fill=self.color_danger)
6066

61-
info_lbl = tk.Label(self.root, text=f"You must answer 3 questions about '{self.topic}' to unlock.", font=self.body_font, fg="#fff", bg=self.color_bg)
62-
info_lbl.pack()
67+
txt = f"You must answer 3 questions about '{self.topic}' to unlock."
68+
self.canvas.create_text(375, 250, text=txt, font=self.body_font, fill="#fff", width=600, justify="center")
6369

64-
loading_lbl = tk.Label(self.root, text="Loading questions...", font=("Arial", 14), fg=self.color_primary, bg=self.color_bg)
65-
loading_lbl.pack(pady=20)
70+
self.canvas.create_text(375, 350, text="Loading questions...", font=("Segoe UI", 14), fill=self.color_primary)
6671

6772
def fetch_questions(self):
6873
"""Runs concurrently to fetch questions from Gemini."""
@@ -97,32 +102,30 @@ def build_ui_question(self):
97102

98103
current_mcq = self.mcqs[self.current_q_index]
99104

100-
# Top Banner
101-
header = tk.Frame(self.root, bg="#000", height=100)
102-
header.pack(fill="x", side="top")
103-
104-
title_lbl = tk.Label(header, text="FOCUS PENALTY", font=("Helvetica", 24, "bold"), fg=self.color_danger, bg="#000")
105-
title_lbl.pack(pady=30)
105+
self.canvas = tk.Canvas(self.root, highlightthickness=0)
106+
self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
107+
colors = ["#171e2e", "#131926"]
108+
for i in range(30):
109+
y = i * 30
110+
self.canvas.create_rectangle(0, y, 1000, y+30, fill=colors[i%2], outline="")
111+
112+
# Top banner background
113+
self.canvas.create_rectangle(0, 0, 1000, 80, fill="#0f172a", outline="")
114+
self.canvas.create_text(375, 40, text="FOCUS PENALTY", font=("Impact", 28, "normal"), fill=self.color_primary)
106115

107-
progress_lbl = tk.Label(self.root, text=f"Question {self.current_q_index + 1} of {self.correct_answers_needed}", font=("Arial", 12, "bold"), fg=self.color_primary, bg=self.color_bg)
108-
progress_lbl.pack(pady=20)
109-
110-
# Question Text
111-
q_text = tk.Label(self.root, text=current_mcq.question, font=("Arial", 16), fg="#fff", bg=self.color_bg, wraplength=600, justify="center")
112-
q_text.pack(pady=20)
113-
114-
# Container for evenly spaced buttons
115-
btn_frame = tk.Frame(self.root, bg=self.color_bg)
116-
btn_frame.pack(pady=20)
116+
# Details & text
117+
self.canvas.create_text(375, 120, text=f"Question {self.current_q_index + 1} of {self.correct_answers_needed}", font=("Segoe UI", 12, "bold"), fill=self.color_primary)
118+
self.canvas.create_text(375, 180, text=current_mcq.question, font=("Segoe UI", 16), fill="#fff", width=650, justify="center")
117119

118-
# Create buttons
119-
for opt in current_mcq.options:
120+
# Buttons placed evenly
121+
start_y = 280
122+
for i, opt in enumerate(current_mcq.options):
120123
btn = tk.Button(
121-
btn_frame,
124+
self.root,
122125
text=opt,
123-
font=("Arial", 11, "bold"),
124-
bg="#333",
125-
fg="#fff",
126+
font=("Segoe UI", 12, "bold"),
127+
bg="#334155",
128+
fg="#f8fafc",
126129
activebackground=self.color_primary,
127130
activeforeground="#000",
128131
width=50,
@@ -131,10 +134,9 @@ def build_ui_question(self):
131134
cursor="hand2",
132135
command=lambda o=opt: self.check_answer(o, current_mcq)
133136
)
134-
btn.pack(pady=5)
137+
self.canvas.create_window(375, start_y + (i * 65), window=btn)
135138

136-
self.feedback_lbl = tk.Label(self.root, text="", font=self.btn_font, bg=self.color_bg)
137-
self.feedback_lbl.pack(pady=10)
139+
self.feedback_text = self.canvas.create_text(375, 570, text="", font=self.btn_font, fill=self.color_danger, width=650, justify="center")
138140

139141

140142

@@ -143,12 +145,12 @@ def check_answer(self, selected: str, mcq):
143145
if selected == mcq.correct_answer:
144146
self.current_q_index += 1
145147
if self.current_q_index >= self.correct_answers_needed:
146-
self.feedback_lbl.config(text="Challenge Passed. Unlocking...", fg="#00E676")
148+
self.canvas.itemconfig(self.feedback_text, text="Challenge Passed. Unlocking...", fill="#00E676")
147149
self.root.after(1000, self.unlock_system)
148150
else:
149151
self.build_ui_question()
150152
else:
151-
self.feedback_lbl.config(text=f"WRONG! {mcq.explanation}", fg=self.color_danger, wraplength=800)
153+
self.canvas.itemconfig(self.feedback_text, text=f"WRONG! {mcq.explanation}", fill=self.color_danger)
152154

153155
def unlock_system(self):
154156
"""Destroys the tkinter blocking window entirely."""

bouncer/main.py

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,41 @@ async def lock_system(request: dict = None):
110110
print(f"System locked. Monitoring active for topic: {topic}", flush=True)
111111
return {"success": True, "message": "System locked. Monitoring resumed."}
112112

113+
# --- Extension Integration Endpoints ---
114+
115+
@app.get("/api/status")
116+
async def get_system_status():
117+
"""Returns basic state for the Chrome extension to display."""
118+
if system_state["monitoring_active"] and not system_state["unlocked"]:
119+
return {"active_sprint": {"suggested_topic": system_state["active_topic"]}}
120+
return {"active_sprint": None}
121+
122+
@app.get("/api/block")
123+
async def trigger_extension_block(url: str = None, topic: str = "General Study"):
124+
"""Triggers the Python tk popup window synchronously when called by the extension."""
125+
if not system_state["is_blocking"]:
126+
system_state["is_blocking"] = True
127+
print(f"Browser distraction detected on {url}. Launching popup...", flush=True)
128+
threading.Thread(
129+
target=launch_os_blocker_sync,
130+
args=(topic,),
131+
daemon=True
132+
).start()
133+
134+
# Return a simple HTML page telling the user they are blocked
135+
html_content = f"""
136+
<html>
137+
<head><title>BLOCKED</title></head>
138+
<body style="background-color: #111827; color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: 'Segoe UI', sans-serif;">
139+
<h1 style="color: #ff3b3b; font-size: 3rem;">FOCUS PENALTY</h1>
140+
<p style="font-size: 1.5rem;">You tried to visit a distracting site during your '{topic}' sprint.</p>
141+
<p>Please complete the challenge on your screen to proceed.</p>
142+
</body>
143+
</html>
144+
"""
145+
from fastapi.responses import HTMLResponse
146+
return HTMLResponse(content=html_content, status_code=200)
147+
113148
# --- Brain Integration Endpoints ---
114149

115150
class QuizRequest(BaseModel):
@@ -166,6 +201,18 @@ async def filter_notification_endpoint(request: NotificationRequest):
166201
is_relevant = intelligence.filter_notification(request.notification_text, request.current_topic)
167202
return {"is_relevant": is_relevant}
168203

204+
class WindowRequest(BaseModel):
205+
window_title: str
206+
current_topic: str
207+
208+
@app.post("/api/brain/filter_window")
209+
async def filter_window_endpoint(request: WindowRequest):
210+
"""Determines if a window/tab title is relevant to the topic."""
211+
if not intelligence:
212+
return {"is_relevant": False}
213+
is_relevant = intelligence.is_window_relevant(request.window_title, request.current_topic)
214+
return {"is_relevant": is_relevant}
215+
169216
# --- Notification Monitor Endpoints ---
170217

171218
@app.get("/api/notifications/held")
@@ -253,6 +300,9 @@ def distraction_monitor_loop():
253300
"""Background thread that continuously scans active windows for blacklisted apps.
254301
Only runs when monitoring_active is True (after user clicks a sprint card)."""
255302
print("Distraction Monitor Thread Started.", flush=True)
303+
pending_distraction_start = None
304+
last_relevant_title = None
305+
256306
while True:
257307
try:
258308
if system_state["monitoring_active"] and not system_state["unlocked"] and not system_state["is_blocking"]:
@@ -263,27 +313,52 @@ def distraction_monitor_loop():
263313
if active_window is not None:
264314
title = active_window.title.lower()
265315
if any(app_name in title for app_name in BLACKLIST):
266-
print(f"Distraction detected: '{title}'. Notifying clients!", flush=True)
267-
system_state["is_blocking"] = True
268-
# Send BLOCK event to all connected UI clients
269-
import asyncio as _asyncio
270-
try:
271-
loop = _asyncio.get_event_loop()
272-
if loop.is_running():
273-
_asyncio.ensure_future(broadcast_event({
274-
"event": "BLOCK",
275-
"app": title,
276-
"is_blocking": True,
277-
"active_topic": system_state["active_topic"]
278-
}))
279-
except Exception as e:
280-
print(f"WS broadcast error: {e}", flush=True)
281-
# Also launch OS blocker as fallback
282-
threading.Thread(
283-
target=launch_os_blocker_sync,
284-
args=(system_state["active_topic"],),
285-
daemon=True
286-
).start()
316+
if title == last_relevant_title:
317+
pass # Already approved this specific window/video
318+
elif pending_distraction_start is None:
319+
pending_distraction_start = time.time()
320+
print(f"Blacklisted app detected: '{title}'. Starting 15s grace period...", flush=True)
321+
elif time.time() - pending_distraction_start > 15:
322+
print(f"Grace period over. Checking relevance for: '{title}'...", flush=True)
323+
324+
# AI Check for Window Relevance
325+
is_relevant = False
326+
if intelligence:
327+
try:
328+
is_relevant = intelligence.is_window_relevant(title, system_state["active_topic"])
329+
except Exception as e:
330+
print(f"Window relevance check failed: {e}", flush=True)
331+
332+
if not is_relevant:
333+
print(f"Distraction confirmed: '{title}'. Notifying clients!", flush=True)
334+
system_state["is_blocking"] = True
335+
pending_distraction_start = None
336+
# Send BLOCK event to all connected UI clients
337+
import asyncio as _asyncio
338+
try:
339+
loop = _asyncio.get_event_loop()
340+
if loop.is_running():
341+
_asyncio.ensure_future(broadcast_event({
342+
"event": "BLOCK",
343+
"app": title,
344+
"is_blocking": True,
345+
"active_topic": system_state["active_topic"]
346+
}))
347+
except Exception as e:
348+
print(f"WS broadcast error: {e}", flush=True)
349+
# Also launch OS blocker as fallback
350+
threading.Thread(
351+
target=launch_os_blocker_sync,
352+
args=(system_state["active_topic"],),
353+
daemon=True
354+
).start()
355+
else:
356+
print(f"Window '{title}' deemed relevant to topic '{system_state['active_topic']}'. Allowing.", flush=True)
357+
last_relevant_title = title
358+
pending_distraction_start = None
359+
else:
360+
pending_distraction_start = None
361+
# Don't reset last_relevant_title immediately if they tab out for a second
287362
except Exception as e:
288363
print(f"Monitor error: {e}", flush=True)
289364
time.sleep(1)

0 commit comments

Comments
 (0)