XThomasBU commited on
Commit
1796f63
·
1 Parent(s): 465c1fa

initial commit for token tracking

Browse files
code/.chainlit/config.toml CHANGED
@@ -20,7 +20,7 @@ allow_origins = ["*"]
20
 
21
  [features]
22
  # Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
23
- unsafe_allow_html = false
24
 
25
  # Process and display mathematical expressions. This can clash with "$" characters in messages.
26
  latex = true
 
20
 
21
  [features]
22
  # Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
23
+ unsafe_allow_html = true
24
 
25
  # Process and display mathematical expressions. This can clash with "$" characters in messages.
26
  latex = true
code/app.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import FastAPI, Request, Response
2
  from fastapi.responses import HTMLResponse, RedirectResponse
3
  from fastapi.templating import Jinja2Templates
4
  from google.oauth2 import id_token
@@ -12,9 +12,18 @@ from modules.config.constants import (
12
  OAUTH_GOOGLE_CLIENT_ID,
13
  OAUTH_GOOGLE_CLIENT_SECRET,
14
  CHAINLIT_URL,
 
 
15
  )
16
  from fastapi.middleware.cors import CORSMiddleware
17
  from fastapi.staticfiles import StaticFiles
 
 
 
 
 
 
 
18
 
19
  GOOGLE_CLIENT_ID = OAUTH_GOOGLE_CLIENT_ID
20
  GOOGLE_CLIENT_SECRET = OAUTH_GOOGLE_CLIENT_SECRET
@@ -71,7 +80,7 @@ def get_user_role(username: str):
71
  return USER_ROLES.get(username, ["student"]) # Default to "student" role
72
 
73
 
74
- def get_user_info_from_cookie(request: Request):
75
  user_info_encoded = request.cookies.get("X-User-Info")
76
  if user_info_encoded:
77
  try:
@@ -83,6 +92,14 @@ def get_user_info_from_cookie(request: Request):
83
  return None
84
 
85
 
 
 
 
 
 
 
 
 
86
  def get_user_info(request: Request):
87
  session_token = request.cookies.get("session_token")
88
  if session_token and session_token in session_store:
@@ -92,33 +109,35 @@ def get_user_info(request: Request):
92
 
93
  @app.get("/", response_class=HTMLResponse)
94
  async def login_page(request: Request):
95
- user_info = get_user_info_from_cookie(request)
96
  if user_info and user_info.get("google_signed_in"):
97
  return RedirectResponse("/post-signin")
98
- return templates.TemplateResponse("login.html", {"request": request})
99
-
100
-
101
- @app.get("/login/guest")
102
- @app.post("/login/guest")
103
- async def login_guest():
104
- username = "guest"
105
- session_token = secrets.token_hex(16)
106
- unique_session_id = secrets.token_hex(8)
107
- username = f"{username}_{unique_session_id}"
108
- session_store[session_token] = {
109
- "email": username,
110
- "name": "Guest",
111
- "profile_image": "",
112
- "google_signed_in": False, # Ensure guest users do not have this flag
113
- }
114
- user_info_json = json.dumps(session_store[session_token])
115
- user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
116
-
117
- # Set cookies
118
- response = RedirectResponse(url="/post-signin", status_code=303)
119
- response.set_cookie(key="session_token", value=session_token)
120
- response.set_cookie(key="X-User-Info", value=user_info_encoded, httponly=True)
121
- return response
 
 
122
 
123
 
124
  @app.get("/login/google")
@@ -128,8 +147,7 @@ async def login_google(request: Request):
128
  response.delete_cookie(key="session_token")
129
  response.delete_cookie(key="X-User-Info")
130
 
131
- user_info = get_user_info_from_cookie(request)
132
- print(f"User info: {user_info}")
133
  # Check if user is already signed in using Google
134
  if user_info and user_info.get("google_signed_in"):
135
  return RedirectResponse("/post-signin")
@@ -159,25 +177,69 @@ async def auth_google(request: Request):
159
  "google_signed_in": True, # Set this flag to True for Google-signed users
160
  }
161
 
 
 
 
 
162
  user_info_json = json.dumps(session_store[session_token])
163
  user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
164
 
165
  # Set cookies
166
  response = RedirectResponse(url="/post-signin", status_code=303)
167
  response.set_cookie(key="session_token", value=session_token)
168
- response.set_cookie(key="X-User-Info", value=user_info_encoded, httponly=True)
 
 
169
  return response
170
  except Exception as e:
171
  print(f"Error during Google OAuth callback: {e}")
172
  return RedirectResponse(url="/", status_code=302)
173
 
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  @app.get("/post-signin", response_class=HTMLResponse)
176
  async def post_signin(request: Request):
177
- user_info = get_user_info_from_cookie(request)
178
  if not user_info:
179
  user_info = get_user_info(request)
180
- # if user_info and user_info.get("google_signed_in"):
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  if user_info:
182
  username = user_info["email"]
183
  role = get_user_role(username)
@@ -189,14 +251,16 @@ async def post_signin(request: Request):
189
  "username": username,
190
  "role": role,
191
  "jwt_token": jwt_token,
 
192
  },
193
  )
194
  return RedirectResponse("/")
195
 
196
 
 
197
  @app.post("/start-tutor")
198
  async def start_tutor(request: Request):
199
- user_info = get_user_info_from_cookie(request)
200
  if user_info:
201
  user_info_json = json.dumps(user_info)
202
  user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
@@ -208,6 +272,19 @@ async def start_tutor(request: Request):
208
  return RedirectResponse(url="/")
209
 
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  @app.exception_handler(Exception)
212
  async def exception_handler(request: Request, exc: Exception):
213
  return templates.TemplateResponse(
@@ -215,17 +292,14 @@ async def exception_handler(request: Request, exc: Exception):
215
  )
216
 
217
 
218
- @app.get("/chainlit_tutor/logout", response_class=HTMLResponse)
219
- @app.post("/chainlit_tutor/logout", response_class=HTMLResponse)
220
- async def app_logout(request: Request, response: Response):
221
- # Clear session cookies
222
- response.delete_cookie("session_token")
223
- response.delete_cookie("X-User-Info")
224
-
225
- print("logout_page called")
226
-
227
- # Redirect to the logout page with embedded JavaScript
228
- return RedirectResponse(url="/", status_code=302)
229
 
230
 
231
  mount_chainlit(app=app, target="main.py", path=CHAINLIT_PATH)
 
1
+ from fastapi import FastAPI, Request, Response, HTTPException
2
  from fastapi.responses import HTMLResponse, RedirectResponse
3
  from fastapi.templating import Jinja2Templates
4
  from google.oauth2 import id_token
 
12
  OAUTH_GOOGLE_CLIENT_ID,
13
  OAUTH_GOOGLE_CLIENT_SECRET,
14
  CHAINLIT_URL,
15
+ GITHUB_REPO,
16
+ DOCS_WEBSITE,
17
  )
18
  from fastapi.middleware.cors import CORSMiddleware
19
  from fastapi.staticfiles import StaticFiles
20
+ from modules.chat_processor.helpers import (
21
+ get_user_details,
22
+ get_time,
23
+ reset_tokens_for_user,
24
+ check_user_cooldown,
25
+ update_user_info,
26
+ )
27
 
28
  GOOGLE_CLIENT_ID = OAUTH_GOOGLE_CLIENT_ID
29
  GOOGLE_CLIENT_SECRET = OAUTH_GOOGLE_CLIENT_SECRET
 
80
  return USER_ROLES.get(username, ["student"]) # Default to "student" role
81
 
82
 
83
+ async def get_user_info_from_cookie(request: Request):
84
  user_info_encoded = request.cookies.get("X-User-Info")
85
  if user_info_encoded:
86
  try:
 
92
  return None
93
 
94
 
95
+ async def del_user_info_from_cookie(request: Request, response: Response):
96
+ response.delete_cookie("X-User-Info")
97
+ response.delete_cookie("session_token")
98
+ session_token = request.cookies.get("session_token")
99
+ if session_token:
100
+ del session_store[session_token]
101
+
102
+
103
  def get_user_info(request: Request):
104
  session_token = request.cookies.get("session_token")
105
  if session_token and session_token in session_store:
 
109
 
110
  @app.get("/", response_class=HTMLResponse)
111
  async def login_page(request: Request):
112
+ user_info = await get_user_info_from_cookie(request)
113
  if user_info and user_info.get("google_signed_in"):
114
  return RedirectResponse("/post-signin")
115
+ return templates.TemplateResponse(
116
+ "login.html",
117
+ {"request": request, "GITHUB_REPO": GITHUB_REPO, "DOCS_WEBSITE": DOCS_WEBSITE},
118
+ )
119
+
120
+
121
+ # @app.get("/login/guest")
122
+ # async def login_guest():
123
+ # username = "guest"
124
+ # session_token = secrets.token_hex(16)
125
+ # unique_session_id = secrets.token_hex(8)
126
+ # username = f"{username}_{unique_session_id}"
127
+ # session_store[session_token] = {
128
+ # "email": username,
129
+ # "name": "Guest",
130
+ # "profile_image": "",
131
+ # "google_signed_in": False, # Ensure guest users do not have this flag
132
+ # }
133
+ # user_info_json = json.dumps(session_store[session_token])
134
+ # user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
135
+
136
+ # # Set cookies
137
+ # response = RedirectResponse(url="/post-signin", status_code=303)
138
+ # response.set_cookie(key="session_token", value=session_token)
139
+ # response.set_cookie(key="X-User-Info", value=user_info_encoded, httponly=True)
140
+ # return response
141
 
142
 
143
  @app.get("/login/google")
 
147
  response.delete_cookie(key="session_token")
148
  response.delete_cookie(key="X-User-Info")
149
 
150
+ user_info = await get_user_info_from_cookie(request)
 
151
  # Check if user is already signed in using Google
152
  if user_info and user_info.get("google_signed_in"):
153
  return RedirectResponse("/post-signin")
 
177
  "google_signed_in": True, # Set this flag to True for Google-signed users
178
  }
179
 
180
+ # add literalai user info to session store to be sent to chainlit
181
+ literalai_user = await get_user_details(email)
182
+ session_store[session_token]["literalai_info"] = literalai_user.to_dict()
183
+
184
  user_info_json = json.dumps(session_store[session_token])
185
  user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
186
 
187
  # Set cookies
188
  response = RedirectResponse(url="/post-signin", status_code=303)
189
  response.set_cookie(key="session_token", value=session_token)
190
+ response.set_cookie(
191
+ key="X-User-Info", value=user_info_encoded
192
+ ) # TODO: is the flag httponly=True necessary?
193
  return response
194
  except Exception as e:
195
  print(f"Error during Google OAuth callback: {e}")
196
  return RedirectResponse(url="/", status_code=302)
197
 
198
 
199
+ @app.get("/cooldown")
200
+ async def cooldown(request: Request):
201
+ user_info = await get_user_info_from_cookie(request)
202
+ user_details = await get_user_details(user_info["email"])
203
+ current_datetime = get_time()
204
+ cooldown, cooldown_end_time = check_user_cooldown(user_details, current_datetime)
205
+ print(f"User in cooldown: {cooldown}")
206
+ print(f"Cooldown end time: {cooldown_end_time}")
207
+ if cooldown:
208
+ return templates.TemplateResponse(
209
+ "cooldown.html",
210
+ {
211
+ "request": request,
212
+ "username": user_info["email"],
213
+ "role": get_user_role(user_info["email"]),
214
+ "cooldown_end_time": cooldown_end_time, # Pass as ISO format string
215
+ },
216
+ )
217
+ else:
218
+ user_details.metadata["in_cooldown"] = False
219
+ await update_user_info(user_details)
220
+ await reset_tokens_for_user(user_details)
221
+ return RedirectResponse("/post-signin")
222
+
223
+
224
  @app.get("/post-signin", response_class=HTMLResponse)
225
  async def post_signin(request: Request):
226
+ user_info = await get_user_info_from_cookie(request)
227
  if not user_info:
228
  user_info = get_user_info(request)
229
+ user_details = await get_user_details(user_info["email"])
230
+ current_datetime = get_time()
231
+ user_details.metadata["last_login"] = current_datetime
232
+ # if new user, set the number of tries
233
+ if "tokens_left" not in user_details.metadata:
234
+ await reset_tokens_for_user(user_details)
235
+ user_details.metadata["all_time_tokens"] = 0 # Initialize all_time_tokens
236
+ if "last_message_time" in user_details.metadata:
237
+ cooldown, _ = check_user_cooldown(user_details, current_datetime)
238
+ if cooldown:
239
+ return RedirectResponse("/cooldown")
240
+ else:
241
+ user_details.metadata["in_cooldown"] = False
242
+ await reset_tokens_for_user(user_details)
243
  if user_info:
244
  username = user_info["email"]
245
  role = get_user_role(username)
 
251
  "username": username,
252
  "role": role,
253
  "jwt_token": jwt_token,
254
+ "tokens_left": user_details.metadata["tokens_left"],
255
  },
256
  )
257
  return RedirectResponse("/")
258
 
259
 
260
+ @app.get("/start-tutor")
261
  @app.post("/start-tutor")
262
  async def start_tutor(request: Request):
263
+ user_info = await get_user_info_from_cookie(request)
264
  if user_info:
265
  user_info_json = json.dumps(user_info)
266
  user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
 
272
  return RedirectResponse(url="/")
273
 
274
 
275
+ @app.exception_handler(HTTPException)
276
+ async def http_exception_handler(request: Request, exc: HTTPException):
277
+ if exc.status_code == 404:
278
+ return templates.TemplateResponse(
279
+ "error_404.html", {"request": request}, status_code=404
280
+ )
281
+ return templates.TemplateResponse(
282
+ "error.html",
283
+ {"request": request, "error": str(exc)},
284
+ status_code=exc.status_code,
285
+ )
286
+
287
+
288
  @app.exception_handler(Exception)
289
  async def exception_handler(request: Request, exc: Exception):
290
  return templates.TemplateResponse(
 
292
  )
293
 
294
 
295
+ @app.get("/logout", response_class=HTMLResponse)
296
+ async def logout(request: Request, response: Response):
297
+ await del_user_info_from_cookie(request=request, response=response)
298
+ response = RedirectResponse(url="/", status_code=302)
299
+ # Set cookies to empty values and expire them immediately
300
+ response.set_cookie(key="session_token", value="", expires=0)
301
+ response.set_cookie(key="X-User-Info", value="", expires=0)
302
+ return response
 
 
 
303
 
304
 
305
  mount_chainlit(app=app, target="main.py", path=CHAINLIT_PATH)
code/chainlit.md CHANGED
@@ -1,10 +1,5 @@
1
  # Welcome to DL4DS Tutor! 🚀🤖
2
 
3
- Hi there, this is an LLM chatbot designed to help answer questions on the course content, built using Langchain and Chainlit.
4
- This is still very much a Work in Progress.
5
 
6
  ### --- Please wait while the Tutor loads... ---
7
-
8
- ## Useful Links 🔗
9
-
10
- - **Documentation:** [Chainlit Documentation](https://docs.chainlit.io) 📚
 
1
  # Welcome to DL4DS Tutor! 🚀🤖
2
 
3
+ Hi there, this is an LLM chatbot designed to help answer questions on the course content.
 
4
 
5
  ### --- Please wait while the Tutor loads... ---
 
 
 
 
code/main.py CHANGED
@@ -16,11 +16,17 @@ from modules.chat.helpers import (
16
  get_history_setup_llm,
17
  get_last_config,
18
  )
 
 
 
 
 
19
  import copy
20
  from typing import Optional
21
  from chainlit.types import ThreadDict
22
  import time
23
  import base64
 
24
 
25
  USER_TIMEOUT = 60_000
26
  SYSTEM = "System"
@@ -291,9 +297,9 @@ class Chatbot:
291
 
292
  await self.make_llm_settings_widgets(self.config) # Reload the settings widgets
293
 
294
- await self.make_llm_settings_widgets(self.config)
295
  user = cl.user_session.get("user")
296
 
 
297
  try:
298
  self.user = {
299
  "user_id": user.identifier,
@@ -307,8 +313,6 @@ class Chatbot:
307
  }
308
 
309
  memory = cl.user_session.get("memory", [])
310
-
311
- cl.user_session.set("user", self.user)
312
  self.llm_tutor = LLMTutor(self.config, user=self.user)
313
 
314
  self.chain = self.llm_tutor.qa_bot(
@@ -353,6 +357,33 @@ class Chatbot:
353
  start_time = time.time()
354
 
355
  chain = cl.user_session.get("chain")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
  llm_settings = cl.user_session.get("llm_settings", {})
358
  view_sources = llm_settings.get("view_sources", False)
@@ -360,6 +391,7 @@ class Chatbot:
360
  stream = False # Fix streaming
361
  user_query_dict = {"input": message.content}
362
  # Define the base configuration
 
363
  chain_config = {
364
  "configurable": {
365
  "user_id": self.user["user_id"],
@@ -367,20 +399,22 @@ class Chatbot:
367
  "memory_window": self.config["llm_params"]["memory_window"],
368
  },
369
  "callbacks": (
370
- [cl.LangchainCallbackHandler()]
371
  if cl_data._data_layer and self.config["chat_logging"]["callbacks"]
372
  else None
373
  ),
374
  }
375
 
376
- if stream:
377
- res = chain.stream(user_query=user_query_dict, config=chain_config)
378
- res = await self.stream_response(res)
379
- else:
380
- res = await chain.invoke(
381
- user_query=user_query_dict,
382
- config=chain_config,
383
- )
 
 
384
 
385
  answer = res.get("answer", res.get("result"))
386
 
@@ -395,21 +429,24 @@ class Chatbot:
395
 
396
  if self.config["llm_params"]["generate_follow_up"]:
397
  start_time = time.time()
 
398
  config = {
399
  "callbacks": (
400
- [cl.LangchainCallbackHandler()]
401
  if cl_data._data_layer and self.config["chat_logging"]["callbacks"]
402
  else None
403
  )
404
  }
 
 
 
 
 
 
 
 
405
 
406
- list_of_questions = await self.question_generator.generate_questions(
407
- query=user_query_dict["input"],
408
- response=answer,
409
- chat_history=res.get("chat_history"),
410
- context=res.get("context"),
411
- config=config,
412
- )
413
 
414
  for question in list_of_questions:
415
 
@@ -424,6 +461,14 @@ class Chatbot:
424
 
425
  print("Time taken to generate questions: ", time.time() - start_time)
426
 
 
 
 
 
 
 
 
 
427
  await cl.Message(
428
  content=answer_with_sources,
429
  elements=source_elements,
@@ -445,15 +490,6 @@ class Chatbot:
445
  cl.user_session.set("memory", conversation_list)
446
  await self.start(config=thread_config)
447
 
448
- # @cl.oauth_callback
449
- # def auth_callback(
450
- # provider_id: str,
451
- # token: str,
452
- # raw_user_data: Dict[str, str],
453
- # default_user: cl.User,
454
- # ) -> Optional[cl.User]:
455
- # return default_user
456
-
457
  @cl.header_auth_callback
458
  def header_auth_callback(headers: dict) -> Optional[cl.User]:
459
 
@@ -474,11 +510,9 @@ class Chatbot:
474
  decoded_user_info = json.loads(decoded_user_info)
475
 
476
  return cl.User(
477
- identifier=decoded_user_info["email"],
478
- metadata={
479
- "name": decoded_user_info["name"],
480
- "avatar": decoded_user_info["profile_image"],
481
- },
482
  )
483
 
484
  async def on_follow_up(self, action: cl.Action):
 
16
  get_history_setup_llm,
17
  get_last_config,
18
  )
19
+ from modules.chat_processor.helpers import (
20
+ update_user_info,
21
+ get_time,
22
+ check_user_cooldown,
23
+ )
24
  import copy
25
  from typing import Optional
26
  from chainlit.types import ThreadDict
27
  import time
28
  import base64
29
+ from langchain_community.callbacks import get_openai_callback
30
 
31
  USER_TIMEOUT = 60_000
32
  SYSTEM = "System"
 
297
 
298
  await self.make_llm_settings_widgets(self.config) # Reload the settings widgets
299
 
 
300
  user = cl.user_session.get("user")
301
 
302
+ # TODO: remove self.user with cl.user_session.get("user")
303
  try:
304
  self.user = {
305
  "user_id": user.identifier,
 
313
  }
314
 
315
  memory = cl.user_session.get("memory", [])
 
 
316
  self.llm_tutor = LLMTutor(self.config, user=self.user)
317
 
318
  self.chain = self.llm_tutor.qa_bot(
 
357
  start_time = time.time()
358
 
359
  chain = cl.user_session.get("chain")
360
+ token_count = 0 # initialize token count
361
+ if not chain:
362
+ await self.start() # start the chatbot if the chain is not present
363
+ chain = cl.user_session.get("chain")
364
+
365
+ # update user info with last message time
366
+ user = cl.user_session.get("user")
367
+
368
+ print("\n\n User Token Usage: ", user.metadata["all_time_tokens"])
369
+ print("\n\n User Tokens Left: ", user.metadata["tokens_left"])
370
+
371
+ # see if user has token credits left
372
+ # if not, return message saying they have run out of tokens
373
+ if user.metadata["tokens_left"] <= 0:
374
+ current_datetime = get_time()
375
+ cooldown, cooldown_end_time = check_user_cooldown(user, current_datetime)
376
+ if cooldown:
377
+ user.metadata["in_cooldown"] = True
378
+ await update_user_info(user)
379
+ await cl.Message(
380
+ content=(
381
+ "Ah, seems like you have run out of tokens...Click "
382
+ '<a href="/cooldown" style="color: #0000CD; text-decoration: none;" target="_self">here</a> for more info.'
383
+ ),
384
+ author=SYSTEM,
385
+ ).send()
386
+ return
387
 
388
  llm_settings = cl.user_session.get("llm_settings", {})
389
  view_sources = llm_settings.get("view_sources", False)
 
391
  stream = False # Fix streaming
392
  user_query_dict = {"input": message.content}
393
  # Define the base configuration
394
+ cb = cl.AsyncLangchainCallbackHandler()
395
  chain_config = {
396
  "configurable": {
397
  "user_id": self.user["user_id"],
 
399
  "memory_window": self.config["llm_params"]["memory_window"],
400
  },
401
  "callbacks": (
402
+ [cb]
403
  if cl_data._data_layer and self.config["chat_logging"]["callbacks"]
404
  else None
405
  ),
406
  }
407
 
408
+ with get_openai_callback() as token_count_cb:
409
+ if stream:
410
+ res = chain.stream(user_query=user_query_dict, config=chain_config)
411
+ res = await self.stream_response(res)
412
+ else:
413
+ res = await chain.invoke(
414
+ user_query=user_query_dict,
415
+ config=chain_config,
416
+ )
417
+ token_count += token_count_cb.total_tokens
418
 
419
  answer = res.get("answer", res.get("result"))
420
 
 
429
 
430
  if self.config["llm_params"]["generate_follow_up"]:
431
  start_time = time.time()
432
+ cb_follow_up = cl.AsyncLangchainCallbackHandler()
433
  config = {
434
  "callbacks": (
435
+ [cb_follow_up]
436
  if cl_data._data_layer and self.config["chat_logging"]["callbacks"]
437
  else None
438
  )
439
  }
440
+ with get_openai_callback() as token_count_cb:
441
+ list_of_questions = await self.question_generator.generate_questions(
442
+ query=user_query_dict["input"],
443
+ response=answer,
444
+ chat_history=res.get("chat_history"),
445
+ context=res.get("context"),
446
+ config=config,
447
+ )
448
 
449
+ token_count += token_count_cb.total_tokens
 
 
 
 
 
 
450
 
451
  for question in list_of_questions:
452
 
 
461
 
462
  print("Time taken to generate questions: ", time.time() - start_time)
463
 
464
+ # # update user info with token count
465
+ user.metadata["tokens_left"] = user.metadata["tokens_left"] - token_count
466
+ user.metadata["all_time_tokens"] = (
467
+ user.metadata.get("all_time_tokens", 0) + token_count
468
+ )
469
+ user.metadata["last_message_time"] = get_time()
470
+ await update_user_info(user)
471
+
472
  await cl.Message(
473
  content=answer_with_sources,
474
  elements=source_elements,
 
490
  cl.user_session.set("memory", conversation_list)
491
  await self.start(config=thread_config)
492
 
 
 
 
 
 
 
 
 
 
493
  @cl.header_auth_callback
494
  def header_auth_callback(headers: dict) -> Optional[cl.User]:
495
 
 
510
  decoded_user_info = json.loads(decoded_user_info)
511
 
512
  return cl.User(
513
+ id=decoded_user_info["literalai_info"]["id"],
514
+ identifier=decoded_user_info["literalai_info"]["identifier"],
515
+ metadata=decoded_user_info["literalai_info"]["metadata"],
 
 
516
  )
517
 
518
  async def on_follow_up(self, action: cl.Action):
code/modules/chat/langchain/utils.py CHANGED
@@ -299,7 +299,7 @@ async def return_questions(query, response, chat_history_str, context, config):
299
  prompt = ChatPromptTemplate.from_messages(
300
  [
301
  ("system", system),
302
- ("human", "{chat_history_str}, {context}, {query}, {response}"),
303
  ]
304
  )
305
  llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
 
299
  prompt = ChatPromptTemplate.from_messages(
300
  [
301
  ("system", system),
302
+ # ("human", "{chat_history_str}, {context}, {query}, {response}"),
303
  ]
304
  )
305
  llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
code/modules/chat_processor/helpers.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from literalai import AsyncLiteralClient
3
+ from datetime import datetime, timedelta, timezone
4
+ from modules.config.constants import COOLDOWN_TIME, TOKENS_LEFT
5
+ from typing_extensions import TypedDict
6
+ import tiktoken
7
+ from typing import Any, Generic, List, Literal, Optional, TypeVar, Union
8
+
9
+ Field = TypeVar("Field")
10
+ Operators = TypeVar("Operators")
11
+ Value = TypeVar("Value")
12
+
13
+ BOOLEAN_OPERATORS = Literal["is", "nis"]
14
+ STRING_OPERATORS = Literal["eq", "neq", "ilike", "nilike"]
15
+ NUMBER_OPERATORS = Literal["eq", "neq", "gt", "gte", "lt", "lte"]
16
+ STRING_LIST_OPERATORS = Literal["in", "nin"]
17
+ DATETIME_OPERATORS = Literal["gte", "lte", "gt", "lt"]
18
+
19
+ OPERATORS = Union[
20
+ BOOLEAN_OPERATORS,
21
+ STRING_OPERATORS,
22
+ NUMBER_OPERATORS,
23
+ STRING_LIST_OPERATORS,
24
+ DATETIME_OPERATORS,
25
+ ]
26
+
27
+
28
+ class Filter(Generic[Field], TypedDict, total=False):
29
+ field: Field
30
+ operator: OPERATORS
31
+ value: Any
32
+ path: Optional[str]
33
+
34
+
35
+ class OrderBy(Generic[Field], TypedDict):
36
+ column: Field
37
+ direction: Literal["ASC", "DESC"]
38
+
39
+
40
+ threads_filterable_fields = Literal[
41
+ "id",
42
+ "createdAt",
43
+ "name",
44
+ "stepType",
45
+ "stepName",
46
+ "stepOutput",
47
+ "metadata",
48
+ "tokenCount",
49
+ "tags",
50
+ "participantId",
51
+ "participantIdentifiers",
52
+ "scoreValue",
53
+ "duration",
54
+ ]
55
+ threads_orderable_fields = Literal["createdAt", "tokenCount"]
56
+ threads_filters = List[Filter[threads_filterable_fields]]
57
+ threads_order_by = OrderBy[threads_orderable_fields]
58
+
59
+ steps_filterable_fields = Literal[
60
+ "id",
61
+ "name",
62
+ "input",
63
+ "output",
64
+ "participantIdentifier",
65
+ "startTime",
66
+ "endTime",
67
+ "metadata",
68
+ "parentId",
69
+ "threadId",
70
+ "error",
71
+ "tags",
72
+ ]
73
+ steps_orderable_fields = Literal["createdAt"]
74
+ steps_filters = List[Filter[steps_filterable_fields]]
75
+ steps_order_by = OrderBy[steps_orderable_fields]
76
+
77
+ users_filterable_fields = Literal[
78
+ "id",
79
+ "createdAt",
80
+ "identifier",
81
+ "lastEngaged",
82
+ "threadCount",
83
+ "tokenCount",
84
+ "metadata",
85
+ ]
86
+ users_filters = List[Filter[users_filterable_fields]]
87
+
88
+ scores_filterable_fields = Literal[
89
+ "id",
90
+ "createdAt",
91
+ "participant",
92
+ "name",
93
+ "tags",
94
+ "value",
95
+ "type",
96
+ "comment",
97
+ ]
98
+ scores_orderable_fields = Literal["createdAt"]
99
+ scores_filters = List[Filter[scores_filterable_fields]]
100
+ scores_order_by = OrderBy[scores_orderable_fields]
101
+
102
+ generation_filterable_fields = Literal[
103
+ "id",
104
+ "createdAt",
105
+ "model",
106
+ "duration",
107
+ "promptLineage",
108
+ "promptVersion",
109
+ "tags",
110
+ "score",
111
+ "participant",
112
+ "tokenCount",
113
+ "error",
114
+ ]
115
+ generation_orderable_fields = Literal[
116
+ "createdAt",
117
+ "tokenCount",
118
+ "model",
119
+ "provider",
120
+ "participant",
121
+ "duration",
122
+ ]
123
+ generations_filters = List[Filter[generation_filterable_fields]]
124
+ generations_order_by = OrderBy[generation_orderable_fields]
125
+
126
+ literal_client = AsyncLiteralClient(api_key=os.getenv("LITERAL_API_KEY_LOGGING"))
127
+
128
+
129
+ # For consistency, use dictionary for user_info
130
+ def convert_to_dict(user_info):
131
+ # if already a dictionary, return as is
132
+ if isinstance(user_info, dict):
133
+ return user_info
134
+ if hasattr(user_info, "__dict__"):
135
+ user_info = user_info.__dict__
136
+ return user_info
137
+
138
+
139
+ def get_time():
140
+ return datetime.now(timezone.utc).isoformat()
141
+
142
+
143
+ async def get_user_details(user_email_id):
144
+ user_info = await literal_client.api.get_user(identifier=user_email_id)
145
+ return user_info
146
+
147
+
148
+ async def update_user_info(user_info):
149
+ # if object type, convert to dictionary
150
+ user_info = convert_to_dict(user_info)
151
+ await literal_client.api.update_user(
152
+ id=user_info["id"],
153
+ identifier=user_info["identifier"],
154
+ metadata=user_info["metadata"],
155
+ )
156
+
157
+
158
+ def check_user_cooldown(user_info, current_time):
159
+ user_info = convert_to_dict(user_info)
160
+ last_message_time_str = user_info["metadata"].get("last_message_time")
161
+
162
+ # Convert from ISO format string to datetime object and ensure UTC timezone
163
+ last_message_time = datetime.fromisoformat(last_message_time_str).replace(
164
+ tzinfo=timezone.utc
165
+ )
166
+ current_time = datetime.fromisoformat(current_time).replace(tzinfo=timezone.utc)
167
+
168
+ # Calculate the elapsed time
169
+ elapsed_time = current_time - last_message_time
170
+ elapsed_time_in_seconds = elapsed_time.total_seconds()
171
+
172
+ # Calculate when the cooldown period ends
173
+ cooldown_end_time = last_message_time + timedelta(seconds=COOLDOWN_TIME)
174
+ cooldown_end_time_iso = cooldown_end_time.isoformat()
175
+
176
+ # Debug: Print the cooldown end time
177
+ print(f"Cooldown end time (ISO): {cooldown_end_time_iso}")
178
+
179
+ # Check if the user is still in cooldown
180
+ if elapsed_time_in_seconds < COOLDOWN_TIME:
181
+ return True, cooldown_end_time_iso # Return in ISO 8601 format
182
+
183
+ return False, None
184
+
185
+
186
+ async def reset_tokens_for_user(user_info):
187
+ user_info = convert_to_dict(user_info)
188
+ user_info["metadata"]["tokens_left"] = TOKENS_LEFT
189
+ await update_user_info(user_info)
190
+
191
+
192
+ async def get_thread_step_info(thread_id):
193
+ step = await literal_client.api.get_step(thread_id)
194
+ return step
195
+
196
+
197
+ def get_num_tokens(text, model):
198
+ encoding = tiktoken.encoding_for_model(model)
199
+ tokens = encoding.encode(text)
200
+ return len(tokens)
code/modules/config/constants.py CHANGED
@@ -4,6 +4,11 @@ import os
4
  load_dotenv()
5
 
6
  TIMEOUT = 60
 
 
 
 
 
7
 
8
  # API Keys - Loaded from the .env file
9
 
 
4
  load_dotenv()
5
 
6
  TIMEOUT = 60
7
+ COOLDOWN_TIME = 60
8
+ TOKENS_LEFT = 3000
9
+
10
+ GITHUB_REPO = "https://github.com/DL4DS/dl4ds_tutor"
11
+ DOCS_WEBSITE = "https://dl4ds.github.io/dl4ds_tutor/"
12
 
13
  # API Keys - Loaded from the .env file
14
 
code/templates/cooldown.html ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Cooldown Period | Terrier Tutor</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
9
+
10
+ body, html {
11
+ margin: 0;
12
+ padding: 0;
13
+ font-family: 'Inter', sans-serif;
14
+ background-color: #f7f7f7;
15
+ background-image: url('https://www.transparenttextures.com/patterns/cubes.png');
16
+ background-repeat: repeat;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ height: 100vh;
21
+ color: #333;
22
+ }
23
+
24
+ .container {
25
+ background: rgba(255, 255, 255, 0.9);
26
+ border: 1px solid #ddd;
27
+ border-radius: 8px;
28
+ width: 100%;
29
+ max-width: 400px;
30
+ padding: 50px;
31
+ box-sizing: border-box;
32
+ text-align: center;
33
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
34
+ backdrop-filter: blur(10px);
35
+ -webkit-backdrop-filter: blur(10px);
36
+ }
37
+
38
+ .avatar {
39
+ width: 90px;
40
+ height: 90px;
41
+ border-radius: 50%;
42
+ margin-bottom: 25px;
43
+ border: 2px solid #ddd;
44
+ }
45
+
46
+ .container h1 {
47
+ margin-bottom: 15px;
48
+ font-size: 24px;
49
+ font-weight: 600;
50
+ color: #1a1a1a;
51
+ }
52
+
53
+ .container p {
54
+ font-size: 16px;
55
+ color: #4a4a4a;
56
+ margin-bottom: 30px;
57
+ line-height: 1.5;
58
+ }
59
+
60
+ .cooldown-message {
61
+ font-size: 16px;
62
+ color: #333;
63
+ margin-bottom: 30px;
64
+ }
65
+
66
+ .button {
67
+ padding: 12px 0;
68
+ margin: 12px 0;
69
+ font-size: 14px;
70
+ border-radius: 6px;
71
+ cursor: pointer;
72
+ width: 100%;
73
+ border: 1px solid #4285F4; /* Button border color */
74
+ background-color: #fff; /* Button background color */
75
+ color: #4285F4; /* Button text color */
76
+ transition: background-color 0.3s ease, border-color 0.3s ease;
77
+ display: none; /* Initially hidden */
78
+ }
79
+
80
+ .button.start-tutor {
81
+ display: none; /* Initially hidden */
82
+ }
83
+
84
+ .button:hover {
85
+ background-color: #e0e0e0;
86
+ border-color: #357ae8; /* Darker blue for hover */
87
+ }
88
+
89
+ .sign-out-button {
90
+ border: 1px solid #FF4C4C;
91
+ background-color: #fff;
92
+ color: #FF4C4C;
93
+ }
94
+
95
+ .sign-out-button:hover {
96
+ background-color: #ffe6e6; /* Light red on hover */
97
+ border-color: #e04343; /* Darker red for hover */
98
+ color: #e04343; /* Red text on hover */
99
+ }
100
+
101
+ #countdown {
102
+ font-size: 14px;
103
+ color: #555;
104
+ margin-bottom: 20px;
105
+ }
106
+ </style>
107
+ </head>
108
+ <body>
109
+ <div class="container">
110
+ <img src="/public/avatars/ai_tutor.png" alt="AI Tutor Avatar" class="avatar">
111
+ <h1>Hello, {{ username }}</h1>
112
+ <p>It seems like you need to wait a bit before starting a new session.</p>
113
+ <p class="cooldown-message">Time remaining until the cooldown period ends:</p>
114
+ <p id="countdown"></p>
115
+ <button id="startTutorBtn" class="button start-tutor" onclick="startTutor()">Start AI Tutor</button>
116
+ <form action="/logout" method="get">
117
+ <button type="submit" class="button sign-out-button">Sign Out</button>
118
+ </form>
119
+ </div>
120
+ <script>
121
+ function startCountdown(endTime) {
122
+ const countdownElement = document.getElementById('countdown');
123
+ const startTutorBtn = document.getElementById('startTutorBtn');
124
+ const endTimeDate = new Date(endTime); // Parse the cooldown end time
125
+
126
+ function updateCountdown() {
127
+ const now = new Date(); // Get the current time
128
+ const timeLeft = endTimeDate.getTime() - now.getTime(); // Calculate the remaining time in milliseconds
129
+
130
+ if (timeLeft <= 0) {
131
+ countdownElement.textContent = "Cooldown period has ended.";
132
+ startTutorBtn.style.display = "block"; // Show the start tutor button
133
+ } else {
134
+ const hours = Math.floor(timeLeft / 1000 / 60 / 60);
135
+ const minutes = Math.floor((timeLeft / 1000 / 60) % 60);
136
+ const seconds = Math.floor((timeLeft / 1000) % 60);
137
+ countdownElement.textContent = `${hours}h ${minutes}m ${seconds}s`;
138
+ }
139
+ }
140
+
141
+ updateCountdown(); // Initial call to set the countdown
142
+ setInterval(updateCountdown, 1000); // Update the countdown every second
143
+ }
144
+
145
+ function startTutor() {
146
+ // Redirect to AI Tutor session start or any other logic to start the tutor
147
+ window.location.href = "/start-tutor";
148
+ }
149
+
150
+ // Pass the cooldown_end_time to the script in ISO format with 'Z' to indicate UTC
151
+ startCountdown("{{ cooldown_end_time }}");
152
+ </script>
153
+ </body>
154
+ </html>
code/templates/dashboard.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Dashboard</title>
7
  <style>
8
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
9
 
@@ -11,94 +11,99 @@
11
  margin: 0;
12
  padding: 0;
13
  font-family: 'Inter', sans-serif;
14
- background: url('./public/space.jpg') no-repeat center center fixed;
15
- background-size: cover;
16
- background-color: #1a1a1a;
17
  display: flex;
18
  align-items: center;
19
  justify-content: center;
20
  height: 100vh;
21
- color: #f5f5f5;
22
  }
23
 
24
  .container {
25
- background: linear-gradient(145deg, rgba(255, 255, 255, 0.85), rgba(240, 240, 240, 0.85));
26
- border: 1px solid rgba(255, 255, 255, 0.3);
27
- border-radius: 12px;
28
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37);
29
- backdrop-filter: blur(10px);
30
- -webkit-backdrop-filter: blur(10px);
31
  width: 100%;
32
  max-width: 400px;
33
- padding: 40px;
34
  box-sizing: border-box;
35
  text-align: center;
36
- position: relative;
37
- overflow: hidden;
38
- }
39
-
40
- .container:before {
41
- content: '';
42
- position: absolute;
43
- top: 0;
44
- left: 0;
45
- width: 100%;
46
- height: 100%;
47
- background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0) 70%);
48
- z-index: 1;
49
- pointer-events: none;
50
- }
51
-
52
- .container > * {
53
- position: relative;
54
- z-index: 2;
55
  }
56
 
57
  .avatar {
58
- width: 80px;
59
- height: 80px;
60
  border-radius: 50%;
61
- margin-bottom: 20px;
62
- border: 2px solid #fff;
63
  }
64
 
65
  .container h1 {
66
- margin-bottom: 20px;
67
- font-size: 26px;
68
  font-weight: 600;
69
- color: #333;
70
  }
71
 
72
  .container p {
73
- font-size: 15px;
74
- color: #666;
75
  margin-bottom: 30px;
 
 
 
 
 
 
 
 
76
  }
77
 
78
  .button {
79
  padding: 12px 0;
80
- margin: 10px 0;
81
- font-size: 16px;
82
- border: none;
83
- border-radius: 5px;
84
  cursor: pointer;
85
  width: 100%;
86
- transition: background-color 0.3s ease, color 0.3s ease;
87
- background-color: #FAFAFA;
88
- color: #333;
 
89
  }
90
 
91
  .button:hover {
92
- background-color: #f0f0f0;
 
93
  }
94
 
95
  .start-button {
96
- background-color: #4CAF50;
97
- color: #fff;
 
98
  }
99
 
100
  .start-button:hover {
101
- background-color: #45a049;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
  </style>
104
  </head>
@@ -107,9 +112,13 @@
107
  <img src="/public/avatars/ai_tutor.png" alt="AI Tutor Avatar" class="avatar">
108
  <h1>Welcome, {{ username }}</h1>
109
  <p>Ready to start your AI tutoring session?</p>
 
110
  <form action="/start-tutor" method="post">
111
  <button type="submit" class="button start-button">Start AI Tutor</button>
112
  </form>
 
 
 
113
  </div>
114
  <script>
115
  let token = "{{ jwt_token }}";
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Dashboard | Terrier Tutor</title>
7
  <style>
8
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
9
 
 
11
  margin: 0;
12
  padding: 0;
13
  font-family: 'Inter', sans-serif;
14
+ background-color: #f7f7f7; /* Light gray background */
15
+ background-image: url('https://www.transparenttextures.com/patterns/cubes.png'); /* Subtle geometric pattern */
16
+ background-repeat: repeat;
17
  display: flex;
18
  align-items: center;
19
  justify-content: center;
20
  height: 100vh;
21
+ color: #333;
22
  }
23
 
24
  .container {
25
+ background: rgba(255, 255, 255, 0.9);
26
+ border: 1px solid #ddd;
27
+ border-radius: 8px;
 
 
 
28
  width: 100%;
29
  max-width: 400px;
30
+ padding: 50px;
31
  box-sizing: border-box;
32
  text-align: center;
33
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
34
+ backdrop-filter: blur(10px);
35
+ -webkit-backdrop-filter: blur(10px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
 
38
  .avatar {
39
+ width: 90px;
40
+ height: 90px;
41
  border-radius: 50%;
42
+ margin-bottom: 25px;
43
+ border: 2px solid #ddd;
44
  }
45
 
46
  .container h1 {
47
+ margin-bottom: 15px;
48
+ font-size: 24px;
49
  font-weight: 600;
50
+ color: #1a1a1a;
51
  }
52
 
53
  .container p {
54
+ font-size: 16px;
55
+ color: #4a4a4a;
56
  margin-bottom: 30px;
57
+ line-height: 1.5;
58
+ }
59
+
60
+ .tokens-left {
61
+ font-size: 16px;
62
+ color: #333;
63
+ margin-bottom: 30px;
64
+ font-weight: 600;
65
  }
66
 
67
  .button {
68
  padding: 12px 0;
69
+ margin: 12px 0;
70
+ font-size: 14px;
71
+ border-radius: 6px;
 
72
  cursor: pointer;
73
  width: 100%;
74
+ border: 1px solid #4285F4; /* Button border color */
75
+ background-color: #fff; /* Button background color */
76
+ color: #4285F4; /* Button text color */
77
+ transition: background-color 0.3s ease, border-color 0.3s ease;
78
  }
79
 
80
  .button:hover {
81
+ background-color: #e0e0e0;
82
+ border-color: #357ae8; /* Darker blue for hover */
83
  }
84
 
85
  .start-button {
86
+ border: 1px solid #4285F4;
87
+ color: #4285F4;
88
+ background-color: #fff;
89
  }
90
 
91
  .start-button:hover {
92
+ background-color: #e0f0ff; /* Light blue on hover */
93
+ border-color: #357ae8; /* Darker blue for hover */
94
+ color: #357ae8; /* Blue text on hover */
95
+ }
96
+
97
+ .sign-out-button {
98
+ border: 1px solid #FF4C4C;
99
+ background-color: #fff;
100
+ color: #FF4C4C;
101
+ }
102
+
103
+ .sign-out-button:hover {
104
+ background-color: #ffe6e6; /* Light red on hover */
105
+ border-color: #e04343; /* Darker red for hover */
106
+ color: #e04343; /* Red text on hover */
107
  }
108
  </style>
109
  </head>
 
112
  <img src="/public/avatars/ai_tutor.png" alt="AI Tutor Avatar" class="avatar">
113
  <h1>Welcome, {{ username }}</h1>
114
  <p>Ready to start your AI tutoring session?</p>
115
+ <p class="tokens-left">Tokens Left: {{ tokens_left }}</p>
116
  <form action="/start-tutor" method="post">
117
  <button type="submit" class="button start-button">Start AI Tutor</button>
118
  </form>
119
+ <form action="/logout" method="get">
120
+ <button type="submit" class="button sign-out-button">Sign Out</button>
121
+ </form>
122
  </div>
123
  <script>
124
  let token = "{{ jwt_token }}";
code/templates/error.html ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Error | Terrier Tutor</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
9
+
10
+ body, html {
11
+ margin: 0;
12
+ padding: 0;
13
+ font-family: 'Inter', sans-serif;
14
+ background-color: #f7f7f7; /* Light gray background */
15
+ background-image: url('https://www.transparenttextures.com/patterns/cubes.png'); /* Subtle geometric pattern */
16
+ background-repeat: repeat;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ height: 100vh;
21
+ color: #333;
22
+ }
23
+
24
+ .container {
25
+ background: rgba(255, 255, 255, 0.9);
26
+ border: 1px solid #ddd;
27
+ border-radius: 8px;
28
+ width: 100%;
29
+ max-width: 400px;
30
+ padding: 50px;
31
+ box-sizing: border-box;
32
+ text-align: center;
33
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
34
+ backdrop-filter: blur(10px);
35
+ -webkit-backdrop-filter: blur(10px);
36
+ }
37
+
38
+ .container h1 {
39
+ margin-bottom: 20px;
40
+ font-size: 26px;
41
+ font-weight: 600;
42
+ color: #1a1a1a;
43
+ }
44
+
45
+ .container p {
46
+ font-size: 18px;
47
+ color: #4a4a4a;
48
+ margin-bottom: 35px;
49
+ line-height: 1.5;
50
+ }
51
+
52
+ .button {
53
+ padding: 14px 0;
54
+ margin: 12px 0;
55
+ font-size: 16px;
56
+ border-radius: 6px;
57
+ cursor: pointer;
58
+ width: 100%;
59
+ border: 1px solid #ccc;
60
+ background-color: #007BFF;
61
+ color: #fff;
62
+ transition: background-color 0.3s ease, border-color 0.3s ease;
63
+ }
64
+
65
+ .button:hover {
66
+ background-color: #0056b3;
67
+ border-color: #0056b3;
68
+ }
69
+
70
+ .error-box {
71
+ background-color: #2d2d2d;
72
+ color: #fff;
73
+ padding: 10px;
74
+ margin-top: 20px;
75
+ font-family: 'Courier New', Courier, monospace;
76
+ text-align: left;
77
+ overflow-x: auto;
78
+ white-space: pre-wrap;
79
+ border-radius: 5px;
80
+ }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div class="container">
85
+ <h1>Oops! Something went wrong...</h1>
86
+ <p>An unexpected error occurred. The details are below:</p>
87
+ <div class="error-box">
88
+ <code>{{ error }}</code>
89
+ </div>
90
+ <form action="/" method="get">
91
+ <button type="submit" class="button">Return to Home</button>
92
+ </form>
93
+ </div>
94
+ </body>
95
+ </html>
code/templates/error_404.html ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>404 - Not Found</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
9
+
10
+ body, html {
11
+ margin: 0;
12
+ padding: 0;
13
+ font-family: 'Inter', sans-serif;
14
+ background-color: #f7f7f7; /* Light gray background */
15
+ background-image: url('https://www.transparenttextures.com/patterns/cubes.png'); /* Subtle geometric pattern */
16
+ background-repeat: repeat;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ height: 100vh;
21
+ color: #333;
22
+ }
23
+
24
+ .container {
25
+ background: rgba(255, 255, 255, 0.9);
26
+ border: 1px solid #ddd;
27
+ border-radius: 8px;
28
+ width: 100%;
29
+ max-width: 400px;
30
+ padding: 50px;
31
+ box-sizing: border-box;
32
+ text-align: center;
33
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
34
+ backdrop-filter: blur(10px);
35
+ -webkit-backdrop-filter: blur(10px);
36
+ }
37
+
38
+ .container h1 {
39
+ margin-bottom: 20px;
40
+ font-size: 26px;
41
+ font-weight: 600;
42
+ color: #1a1a1a;
43
+ }
44
+
45
+ .container p {
46
+ font-size: 18px;
47
+ color: #4a4a4a;
48
+ margin-bottom: 35px;
49
+ line-height: 1.5;
50
+ }
51
+
52
+ .button {
53
+ padding: 14px 0;
54
+ margin: 12px 0;
55
+ font-size: 16px;
56
+ border-radius: 6px;
57
+ cursor: pointer;
58
+ width: 100%;
59
+ border: 1px solid #ccc;
60
+ background-color: #007BFF;
61
+ color: #fff;
62
+ transition: background-color 0.3s ease, border-color 0.3s ease;
63
+ }
64
+
65
+ .button:hover {
66
+ background-color: #0056b3;
67
+ border-color: #0056b3;
68
+ }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <div class="container">
73
+ <h1>You have ventured into the abyss...</h1>
74
+ <p>To get back to reality, click the button below.</p>
75
+ <form action="/" method="get">
76
+ <button type="submit" class="button">Return to Home</button>
77
+ </form>
78
+ </div>
79
+ </body>
80
+ </html>
code/templates/login.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Login</title>
7
  <style>
8
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
9
 
@@ -11,95 +11,97 @@
11
  margin: 0;
12
  padding: 0;
13
  font-family: 'Inter', sans-serif;
14
- background: url('/public/space.jpg') no-repeat center center fixed;
15
- background-size: cover;
16
- background-color: #1a1a1a;
17
  display: flex;
18
  align-items: center;
19
  justify-content: center;
20
  height: 100vh;
21
- color: #f5f5f5;
22
  }
23
 
24
  .container {
25
- background: linear-gradient(145deg, rgba(255, 255, 255, 0.85), rgba(240, 240, 240, 0.85));
26
- border: 1px solid rgba(255, 255, 255, 0.3);
27
- border-radius: 12px;
28
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37);
29
- backdrop-filter: blur(10px);
30
- -webkit-backdrop-filter: blur(10px);
31
  width: 100%;
32
  max-width: 400px;
33
- padding: 40px;
34
  box-sizing: border-box;
35
  text-align: center;
36
- position: relative;
37
- overflow: hidden;
38
- }
39
-
40
- .container:before {
41
- content: '';
42
- position: absolute;
43
- top: 0;
44
- left: 0;
45
- width: 100%;
46
- height: 100%;
47
- background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0) 70%);
48
- z-index: 1;
49
- pointer-events: none;
50
- }
51
-
52
- .container > * {
53
- position: relative;
54
- z-index: 2;
55
  }
56
 
57
  .avatar {
58
- width: 80px;
59
- height: 80px;
60
  border-radius: 50%;
61
- margin-bottom: 20px;
62
- border: 2px solid #fff;
63
  }
64
 
65
  .container h1 {
66
- margin-bottom: 20px;
67
- font-size: 26px;
68
  font-weight: 600;
69
- color: #333;
70
  }
71
 
72
  .container p {
73
- font-size: 15px;
74
- color: #666;
75
  margin-bottom: 30px;
 
76
  }
77
 
78
  .button {
79
  padding: 12px 0;
80
- margin: 10px 0;
81
- font-size: 16px;
82
- border: none;
83
- border-radius: 5px;
84
  cursor: pointer;
85
  width: 100%;
86
- transition: background-color 0.3s ease, color 0.3s ease;
87
- background-color: #FAFAFA;
88
- color: #333;
 
89
  }
90
 
91
  .button:hover {
92
- background-color: #f0f0f0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
94
 
95
- .google-button {
96
- background-color: #4285F4;
97
- color: #fff;
98
- border: none;
99
  }
100
 
101
- .google-button:hover {
102
- background-color: #357ae8;
 
103
  }
104
  </style>
105
  </head>
@@ -107,13 +109,24 @@
107
  <div class="container">
108
  <img src="/public/avatars/ai_tutor.png" alt="AI Tutor Avatar" class="avatar">
109
  <h1>Terrier Tutor</h1>
110
- <p>Hey! Welcome to the DS598 AI tutor</p>
111
- <form action="/login/guest" method="post">
112
- <button type="submit" class="button">Sign in as Guest</button>
113
- </form>
114
  <form action="/login/google" method="get">
115
- <button type="submit" class="button google-button">Sign in with Google</button>
116
  </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  </div>
118
  </body>
119
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Login | Terrier Tutor</title>
7
  <style>
8
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
9
 
 
11
  margin: 0;
12
  padding: 0;
13
  font-family: 'Inter', sans-serif;
14
+ background-color: #f7f7f7; /* Light gray background */
15
+ background-image: url('https://www.transparenttextures.com/patterns/cubes.png'); /* Subtle geometric pattern */
16
+ background-repeat: repeat;
17
  display: flex;
18
  align-items: center;
19
  justify-content: center;
20
  height: 100vh;
21
+ color: #333;
22
  }
23
 
24
  .container {
25
+ background: rgba(255, 255, 255, 0.9);
26
+ border: 1px solid #ddd;
27
+ border-radius: 8px;
 
 
 
28
  width: 100%;
29
  max-width: 400px;
30
+ padding: 50px;
31
  box-sizing: border-box;
32
  text-align: center;
33
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
34
+ backdrop-filter: blur(10px);
35
+ -webkit-backdrop-filter: blur(10px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
 
38
  .avatar {
39
+ width: 90px;
40
+ height: 90px;
41
  border-radius: 50%;
42
+ margin-bottom: 25px;
43
+ border: 2px solid #ddd;
44
  }
45
 
46
  .container h1 {
47
+ margin-bottom: 15px;
48
+ font-size: 24px;
49
  font-weight: 600;
50
+ color: #1a1a1a;
51
  }
52
 
53
  .container p {
54
+ font-size: 16px;
55
+ color: #4a4a4a;
56
  margin-bottom: 30px;
57
+ line-height: 1.5;
58
  }
59
 
60
  .button {
61
  padding: 12px 0;
62
+ margin: 12px 0;
63
+ font-size: 14px;
64
+ border-radius: 6px;
 
65
  cursor: pointer;
66
  width: 100%;
67
+ border: 1px solid #4285F4; /* Google button border color */
68
+ background-color: #fff; /* Guest button color */
69
+ color: #4285F4; /* Google button text color */
70
+ transition: background-color 0.3s ease, border-color 0.3s ease;
71
  }
72
 
73
  .button:hover {
74
+ background-color: #e0f0ff; /* Light blue on hover */
75
+ border-color: #357ae8; /* Darker blue for hover */
76
+ color: #357ae8; /* Blue text on hover */
77
+ }
78
+
79
+ .footer {
80
+ margin-top: 40px;
81
+ font-size: 15px;
82
+ color: #666;
83
+ text-align: center; /* Center the text in the footer */
84
+ }
85
+
86
+ .footer a {
87
+ color: #333;
88
+ text-decoration: none;
89
+ font-weight: 500;
90
+ display: inline-flex;
91
+ align-items: center;
92
+ justify-content: center; /* Center the content of the links */
93
+ transition: color 0.3s ease;
94
+ margin-bottom: 8px;
95
+ width: 100%; /* Make the link block level */
96
  }
97
 
98
+ .footer a:hover {
99
+ color: #000;
 
 
100
  }
101
 
102
+ .footer svg {
103
+ margin-right: 8px;
104
+ fill: currentColor;
105
  }
106
  </style>
107
  </head>
 
109
  <div class="container">
110
  <img src="/public/avatars/ai_tutor.png" alt="AI Tutor Avatar" class="avatar">
111
  <h1>Terrier Tutor</h1>
112
+ <p>Welcome to the DS598 AI Tutor. Please sign in to continue.</p>
 
 
 
113
  <form action="/login/google" method="get">
114
+ <button type="submit" class="button">Sign in with Google</button>
115
  </form>
116
+ <div class="footer">
117
+ <a href="{{ GITHUB_REPO }}" target="_blank">
118
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
119
+ <path d="M12 .5C5.596.5.5 5.596.5 12c0 5.098 3.292 9.414 7.852 10.94.574.105.775-.249.775-.553 0-.272-.01-1.008-.015-1.98-3.194.694-3.87-1.544-3.87-1.544-.521-1.324-1.273-1.676-1.273-1.676-1.04-.714.079-.7.079-.7 1.148.08 1.75 1.181 1.75 1.181 1.022 1.752 2.683 1.246 3.34.954.104-.74.4-1.246.73-1.533-2.551-.292-5.234-1.276-5.234-5.675 0-1.253.447-2.277 1.181-3.079-.12-.293-.51-1.47.113-3.063 0 0 .96-.307 3.15 1.174.913-.255 1.892-.383 2.867-.388.975.005 1.954.133 2.868.388 2.188-1.481 3.147-1.174 3.147-1.174.624 1.593.233 2.77.114 3.063.735.802 1.18 1.826 1.18 3.079 0 4.407-2.688 5.38-5.248 5.668.413.354.782 1.049.782 2.113 0 1.526-.014 2.757-.014 3.132 0 .307.198.662.783.553C20.21 21.411 23.5 17.096 23.5 12c0-6.404-5.096-11.5-11.5-11.5z"/>
120
+ </svg>
121
+ View on GitHub
122
+ </a>
123
+ <a href="{{ DOCS_WEBSITE }}" target="_blank">
124
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
125
+ <path d="M19 2H8c-1.103 0-2 .897-2 2v16c0 1.103.897 2 2 2h12c1.103 0 2-.897 2-2V7l-5-5zm0 2l.001 4H14V4h5zm-1 14H9V4h4v6h6v8zM7 4H6v16c0 1.654 1.346 3 3 3h9v-2H9c-.551 0-1-.449-1-1V4z"/>
126
+ </svg>
127
+ View Docs
128
+ </a>
129
+ </div>
130
  </div>
131
  </body>
132
  </html>