Thomas (Tom) Gardos commited on
Commit
bc703df
·
unverified ·
2 Parent(s): 465c1fa 66d8970

Merge pull request #75 from DL4DS/tracking_tokens

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
@@ -34,9 +43,10 @@ templates = Jinja2Templates(directory="templates")
34
  session_store = {}
35
  CHAINLIT_PATH = "/chainlit_tutor"
36
 
 
37
  USER_ROLES = {
38
  "[email protected]": ["instructor", "bu"],
39
- "[email protected]": ["instructor", "bu"],
40
  "[email protected]": ["instructor", "bu"],
41
  "[email protected]": ["guest"],
42
  # Add more users and roles as needed
@@ -71,7 +81,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 +93,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 +110,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 +148,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")
@@ -150,6 +169,7 @@ async def auth_google(request: Request):
150
  email = user_info["email"]
151
  name = user_info.get("name", "")
152
  profile_image = user_info.get("picture", "")
 
153
 
154
  session_token = secrets.token_hex(16)
155
  session_store[session_token] = {
@@ -159,25 +179,71 @@ 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 +255,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 +276,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 +296,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
 
43
  session_store = {}
44
  CHAINLIT_PATH = "/chainlit_tutor"
45
 
46
+ # only admin is given any additional permissions for now -- no limits on tokens
47
  USER_ROLES = {
48
  "[email protected]": ["instructor", "bu"],
49
+ "[email protected]": ["admin", "instructor", "bu"],
50
  "[email protected]": ["instructor", "bu"],
51
  "[email protected]": ["guest"],
52
  # Add more users and roles as needed
 
81
  return USER_ROLES.get(username, ["student"]) # Default to "student" role
82
 
83
 
84
+ async def get_user_info_from_cookie(request: Request):
85
  user_info_encoded = request.cookies.get("X-User-Info")
86
  if user_info_encoded:
87
  try:
 
93
  return None
94
 
95
 
96
+ async def del_user_info_from_cookie(request: Request, response: Response):
97
+ response.delete_cookie("X-User-Info")
98
+ response.delete_cookie("session_token")
99
+ session_token = request.cookies.get("session_token")
100
+ if session_token:
101
+ del session_store[session_token]
102
+
103
+
104
  def get_user_info(request: Request):
105
  session_token = request.cookies.get("session_token")
106
  if session_token and session_token in session_store:
 
110
 
111
  @app.get("/", response_class=HTMLResponse)
112
  async def login_page(request: Request):
113
+ user_info = await get_user_info_from_cookie(request)
114
  if user_info and user_info.get("google_signed_in"):
115
  return RedirectResponse("/post-signin")
116
+ return templates.TemplateResponse(
117
+ "login.html",
118
+ {"request": request, "GITHUB_REPO": GITHUB_REPO, "DOCS_WEBSITE": DOCS_WEBSITE},
119
+ )
120
+
121
+
122
+ # @app.get("/login/guest")
123
+ # async def login_guest():
124
+ # username = "guest"
125
+ # session_token = secrets.token_hex(16)
126
+ # unique_session_id = secrets.token_hex(8)
127
+ # username = f"{username}_{unique_session_id}"
128
+ # session_store[session_token] = {
129
+ # "email": username,
130
+ # "name": "Guest",
131
+ # "profile_image": "",
132
+ # "google_signed_in": False, # Ensure guest users do not have this flag
133
+ # }
134
+ # user_info_json = json.dumps(session_store[session_token])
135
+ # user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
136
+
137
+ # # Set cookies
138
+ # response = RedirectResponse(url="/post-signin", status_code=303)
139
+ # response.set_cookie(key="session_token", value=session_token)
140
+ # response.set_cookie(key="X-User-Info", value=user_info_encoded, httponly=True)
141
+ # return response
142
 
143
 
144
  @app.get("/login/google")
 
148
  response.delete_cookie(key="session_token")
149
  response.delete_cookie(key="X-User-Info")
150
 
151
+ user_info = await get_user_info_from_cookie(request)
 
152
  # Check if user is already signed in using Google
153
  if user_info and user_info.get("google_signed_in"):
154
  return RedirectResponse("/post-signin")
 
169
  email = user_info["email"]
170
  name = user_info.get("name", "")
171
  profile_image = user_info.get("picture", "")
172
+ role = get_user_role(email)
173
 
174
  session_token = secrets.token_hex(16)
175
  session_store[session_token] = {
 
179
  "google_signed_in": True, # Set this flag to True for Google-signed users
180
  }
181
 
182
+ # add literalai user info to session store to be sent to chainlit
183
+ literalai_user = await get_user_details(email)
184
+ session_store[session_token]["literalai_info"] = literalai_user.to_dict()
185
+ session_store[session_token]["literalai_info"]["metadata"]["role"] = role
186
+
187
  user_info_json = json.dumps(session_store[session_token])
188
  user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
189
 
190
  # Set cookies
191
  response = RedirectResponse(url="/post-signin", status_code=303)
192
  response.set_cookie(key="session_token", value=session_token)
193
+ response.set_cookie(
194
+ key="X-User-Info", value=user_info_encoded
195
+ ) # TODO: is the flag httponly=True necessary?
196
  return response
197
  except Exception as e:
198
  print(f"Error during Google OAuth callback: {e}")
199
  return RedirectResponse(url="/", status_code=302)
200
 
201
 
202
+ @app.get("/cooldown")
203
+ async def cooldown(request: Request):
204
+ user_info = await get_user_info_from_cookie(request)
205
+ user_details = await get_user_details(user_info["email"])
206
+ current_datetime = get_time()
207
+ cooldown, cooldown_end_time = check_user_cooldown(user_details, current_datetime)
208
+ print(f"User in cooldown: {cooldown}")
209
+ print(f"Cooldown end time: {cooldown_end_time}")
210
+ if cooldown and "admin" not in get_user_role(user_info["email"]):
211
+ return templates.TemplateResponse(
212
+ "cooldown.html",
213
+ {
214
+ "request": request,
215
+ "username": user_info["email"],
216
+ "role": get_user_role(user_info["email"]),
217
+ "cooldown_end_time": cooldown_end_time,
218
+ },
219
+ )
220
+ else:
221
+ await update_user_info(user_details)
222
+ await reset_tokens_for_user(user_details)
223
+ return RedirectResponse("/post-signin")
224
+
225
+
226
  @app.get("/post-signin", response_class=HTMLResponse)
227
  async def post_signin(request: Request):
228
+ user_info = await get_user_info_from_cookie(request)
229
  if not user_info:
230
  user_info = get_user_info(request)
231
+ user_details = await get_user_details(user_info["email"])
232
+ current_datetime = get_time()
233
+ user_details.metadata["last_login"] = current_datetime
234
+ # if new user, set the number of tries
235
+ if "tokens_left" not in user_details.metadata:
236
+ await reset_tokens_for_user(user_details)
237
+
238
+ if "last_message_time" in user_details.metadata and "admin" not in get_user_role(
239
+ user_info["email"]
240
+ ):
241
+ cooldown, _ = check_user_cooldown(user_details, current_datetime)
242
+ if cooldown:
243
+ return RedirectResponse("/cooldown")
244
+ else:
245
+ await reset_tokens_for_user(user_details)
246
+
247
  if user_info:
248
  username = user_info["email"]
249
  role = get_user_role(username)
 
255
  "username": username,
256
  "role": role,
257
  "jwt_token": jwt_token,
258
+ "tokens_left": user_details.metadata["tokens_left"],
259
  },
260
  )
261
  return RedirectResponse("/")
262
 
263
 
264
+ @app.get("/start-tutor")
265
  @app.post("/start-tutor")
266
  async def start_tutor(request: Request):
267
+ user_info = await get_user_info_from_cookie(request)
268
  if user_info:
269
  user_info_json = json.dumps(user_info)
270
  user_info_encoded = base64.b64encode(user_info_json.encode()).decode()
 
276
  return RedirectResponse(url="/")
277
 
278
 
279
+ @app.exception_handler(HTTPException)
280
+ async def http_exception_handler(request: Request, exc: HTTPException):
281
+ if exc.status_code == 404:
282
+ return templates.TemplateResponse(
283
+ "error_404.html", {"request": request}, status_code=404
284
+ )
285
+ return templates.TemplateResponse(
286
+ "error.html",
287
+ {"request": request, "error": str(exc)},
288
+ status_code=exc.status_code,
289
+ )
290
+
291
+
292
  @app.exception_handler(Exception)
293
  async def exception_handler(request: Request, exc: Exception):
294
  return templates.TemplateResponse(
 
296
  )
297
 
298
 
299
+ @app.get("/logout", response_class=HTMLResponse)
300
+ async def logout(request: Request, response: Response):
301
+ await del_user_info_from_cookie(request=request, response=response)
302
+ response = RedirectResponse(url="/", status_code=302)
303
+ # Set cookies to empty values and expire them immediately
304
+ response.set_cookie(key="session_token", value="", expires=0)
305
+ response.set_cookie(key="X-User-Info", value="", expires=0)
306
+ return response
 
 
 
307
 
308
 
309
  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,18 @@ 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 +298,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 +314,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 +358,49 @@ 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 +408,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 +416,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 +446,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 +478,12 @@ 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 +505,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
 
@@ -473,19 +524,22 @@ class Chatbot:
473
  ).decode()
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):
 
485
  message = await cl.Message(
486
  content=action.description,
487
  type="user_message",
488
- author=self.user["user_id"],
489
  ).send()
490
  async with cl.Step(
491
  name="on_follow_up", type="run", parent_id=message.id
 
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
+ from datetime import datetime, timezone
31
 
32
  USER_TIMEOUT = 60_000
33
  SYSTEM = "System"
 
298
 
299
  await self.make_llm_settings_widgets(self.config) # Reload the settings widgets
300
 
 
301
  user = cl.user_session.get("user")
302
 
303
+ # TODO: remove self.user with cl.user_session.get("user")
304
  try:
305
  self.user = {
306
  "user_id": user.identifier,
 
314
  }
315
 
316
  memory = cl.user_session.get("memory", [])
 
 
317
  self.llm_tutor = LLMTutor(self.config, user=self.user)
318
 
319
  self.chain = self.llm_tutor.qa_bot(
 
358
  start_time = time.time()
359
 
360
  chain = cl.user_session.get("chain")
361
+ token_count = 0 # initialize token count
362
+ if not chain:
363
+ await self.start() # start the chatbot if the chain is not present
364
+ chain = cl.user_session.get("chain")
365
+
366
+ # update user info with last message time
367
+ user = cl.user_session.get("user")
368
+
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 and "admin" not in user.metadata["role"]:
374
+ current_datetime = get_time()
375
+ cooldown, cooldown_end_time = check_user_cooldown(user, current_datetime)
376
+ if cooldown:
377
+ # get time left in cooldown
378
+ # convert both to datetime objects
379
+ cooldown_end_time = datetime.fromisoformat(cooldown_end_time).replace(
380
+ tzinfo=timezone.utc
381
+ )
382
+ current_datetime = datetime.fromisoformat(current_datetime).replace(
383
+ tzinfo=timezone.utc
384
+ )
385
+ cooldown_time_left = cooldown_end_time - current_datetime
386
+ # Get the total seconds
387
+ total_seconds = int(cooldown_time_left.total_seconds())
388
+ # Calculate hours, minutes, and seconds
389
+ hours, remainder = divmod(total_seconds, 3600)
390
+ minutes, seconds = divmod(remainder, 60)
391
+ # Format the time as 00 hrs 00 mins 00 secs
392
+ formatted_time = f"{hours:02} hrs {minutes:02} mins {seconds:02} secs"
393
+ await update_user_info(user)
394
+ await cl.Message(
395
+ content=(
396
+ "Ah, seems like you have run out of tokens...Click "
397
+ '<a href="/cooldown" style="color: #0000CD; text-decoration: none;" target="_self">here</a> for more info. Please come back after {}'.format(
398
+ formatted_time
399
+ )
400
+ ),
401
+ author=SYSTEM,
402
+ ).send()
403
+ return
404
 
405
  llm_settings = cl.user_session.get("llm_settings", {})
406
  view_sources = llm_settings.get("view_sources", False)
 
408
  stream = False # Fix streaming
409
  user_query_dict = {"input": message.content}
410
  # Define the base configuration
411
+ cb = cl.AsyncLangchainCallbackHandler()
412
  chain_config = {
413
  "configurable": {
414
  "user_id": self.user["user_id"],
 
416
  "memory_window": self.config["llm_params"]["memory_window"],
417
  },
418
  "callbacks": (
419
+ [cb]
420
  if cl_data._data_layer and self.config["chat_logging"]["callbacks"]
421
  else None
422
  ),
423
  }
424
 
425
+ with get_openai_callback() as token_count_cb:
426
+ if stream:
427
+ res = chain.stream(user_query=user_query_dict, config=chain_config)
428
+ res = await self.stream_response(res)
429
+ else:
430
+ res = await chain.invoke(
431
+ user_query=user_query_dict,
432
+ config=chain_config,
433
+ )
434
+ token_count += token_count_cb.total_tokens
435
 
436
  answer = res.get("answer", res.get("result"))
437
 
 
446
 
447
  if self.config["llm_params"]["generate_follow_up"]:
448
  start_time = time.time()
449
+ cb_follow_up = cl.AsyncLangchainCallbackHandler()
450
  config = {
451
  "callbacks": (
452
+ [cb_follow_up]
453
  if cl_data._data_layer and self.config["chat_logging"]["callbacks"]
454
  else None
455
  )
456
  }
457
+ with get_openai_callback() as token_count_cb:
458
+ list_of_questions = await self.question_generator.generate_questions(
459
+ query=user_query_dict["input"],
460
+ response=answer,
461
+ chat_history=res.get("chat_history"),
462
+ context=res.get("context"),
463
+ config=config,
464
+ )
465
 
466
+ token_count += token_count_cb.total_tokens
 
 
 
 
 
 
467
 
468
  for question in list_of_questions:
469
 
 
478
 
479
  print("Time taken to generate questions: ", time.time() - start_time)
480
 
481
+ # # update user info with token count
482
+ if "admin" not in user.metadata["role"]:
483
+ user.metadata["tokens_left"] = user.metadata["tokens_left"] - token_count
484
+ user.metadata["last_message_time"] = get_time()
485
+ await update_user_info(user)
486
+
487
  await cl.Message(
488
  content=answer_with_sources,
489
  elements=source_elements,
 
505
  cl.user_session.set("memory", conversation_list)
506
  await self.start(config=thread_config)
507
 
 
 
 
 
 
 
 
 
 
508
  @cl.header_auth_callback
509
  def header_auth_callback(headers: dict) -> Optional[cl.User]:
510
 
 
524
  ).decode()
525
  decoded_user_info = json.loads(decoded_user_info)
526
 
527
+ print(
528
+ f"\n\n USER ROLE: {decoded_user_info['literalai_info']['metadata']['role']} \n\n"
529
+ )
530
+
531
  return cl.User(
532
+ id=decoded_user_info["literalai_info"]["id"],
533
+ identifier=decoded_user_info["literalai_info"]["identifier"],
534
+ metadata=decoded_user_info["literalai_info"]["metadata"],
 
 
535
  )
536
 
537
  async def on_follow_up(self, action: cl.Action):
538
+ user = cl.user_session.get("user")
539
  message = await cl.Message(
540
  content=action.description,
541
  type="user_message",
542
+ author=user.identifier,
543
  ).send()
544
  async with cl.Step(
545
  name="on_follow_up", type="run", parent_id=message.id
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,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
160
+ # Check if no tokens left
161
+ tokens_left = user_info.metadata.get("tokens_left", 0)
162
+ if tokens_left > 0:
163
+ return False, None
164
+
165
+ user_info = convert_to_dict(user_info)
166
+ last_message_time_str = user_info["metadata"].get("last_message_time")
167
+
168
+ # Convert from ISO format string to datetime object and ensure UTC timezone
169
+ last_message_time = datetime.fromisoformat(last_message_time_str).replace(
170
+ tzinfo=timezone.utc
171
+ )
172
+ current_time = datetime.fromisoformat(current_time).replace(tzinfo=timezone.utc)
173
+
174
+ # Calculate the elapsed time
175
+ elapsed_time = current_time - last_message_time
176
+ elapsed_time_in_seconds = elapsed_time.total_seconds()
177
+
178
+ # Calculate when the cooldown period ends
179
+ cooldown_end_time = last_message_time + timedelta(seconds=COOLDOWN_TIME)
180
+ cooldown_end_time_iso = cooldown_end_time.isoformat()
181
+
182
+ # Debug: Print the cooldown end time
183
+ print(f"Cooldown end time (ISO): {cooldown_end_time_iso}")
184
+
185
+ # Check if the user is still in cooldown
186
+ if elapsed_time_in_seconds < COOLDOWN_TIME:
187
+ return True, cooldown_end_time_iso # Return in ISO 8601 format
188
+
189
+ return False, None
190
+
191
+
192
+ async def reset_tokens_for_user(user_info):
193
+ user_info = convert_to_dict(user_info)
194
+ user_info["metadata"]["tokens_left"] = TOKENS_LEFT
195
+ await update_user_info(user_info)
196
+
197
+
198
+ async def get_thread_step_info(thread_id):
199
+ step = await literal_client.api.get_step(thread_id)
200
+ return step
201
+
202
+
203
+ def get_num_tokens(text, model):
204
+ encoding = tiktoken.encoding_for_model(model)
205
+ tokens = encoding.encode(text)
206
+ 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,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ display: block; /* Ensure this button is always visible */
94
+ }
95
+
96
+ .sign-out-button:hover {
97
+ background-color: #ffe6e6; /* Light red on hover */
98
+ border-color: #e04343; /* Darker red for hover */
99
+ color: #e04343; /* Red text on hover */
100
+ }
101
+
102
+ #countdown {
103
+ font-size: 14px;
104
+ color: #555;
105
+ margin-bottom: 20px;
106
+ }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <div class="container">
111
+ <img src="/public/avatars/ai_tutor.png" alt="AI Tutor Avatar" class="avatar">
112
+ <h1>Hello, {{ username }}</h1>
113
+ <p>It seems like you need to wait a bit before starting a new session.</p>
114
+ <p class="cooldown-message">Time remaining until the cooldown period ends:</p>
115
+ <p id="countdown"></p>
116
+ <button id="startTutorBtn" class="button start-tutor" onclick="startTutor()">Start AI Tutor</button>
117
+ <form action="/logout" method="get">
118
+ <button type="submit" class="button sign-out-button">Sign Out</button>
119
+ </form>
120
+ </div>
121
+ <script>
122
+ function startCountdown(endTime) {
123
+ const countdownElement = document.getElementById('countdown');
124
+ const startTutorBtn = document.getElementById('startTutorBtn');
125
+ const endTimeDate = new Date(endTime); // Parse the cooldown end time
126
+
127
+ function updateCountdown() {
128
+ const now = new Date(); // Get the current time
129
+ const timeLeft = endTimeDate.getTime() - now.getTime(); // Calculate the remaining time in milliseconds
130
+
131
+ if (timeLeft <= 0) {
132
+ countdownElement.textContent = "Cooldown period has ended.";
133
+ startTutorBtn.style.display = "block"; // Show the start tutor button
134
+ } else {
135
+ const hours = Math.floor(timeLeft / 1000 / 60 / 60);
136
+ const minutes = Math.floor((timeLeft / 1000 / 60) % 60);
137
+ const seconds = Math.floor((timeLeft / 1000) % 60);
138
+ countdownElement.textContent = `${hours}h ${minutes}m ${seconds}s`;
139
+ }
140
+ }
141
+
142
+ updateCountdown(); // Initial call to set the countdown
143
+ setInterval(updateCountdown, 1000); // Update the countdown every second
144
+ }
145
+
146
+ function startTutor() {
147
+ // Redirect to AI Tutor session start or any other logic to start the tutor
148
+ window.location.href = "/start-tutor";
149
+ }
150
+
151
+ // Pass the cooldown_end_time to the script in ISO format with 'Z' to indicate UTC
152
+ startCountdown("{{ cooldown_end_time }}");
153
+ </script>
154
+ </body>
155
+ </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>