MissSqui commited on
Commit
479e992
·
verified ·
1 Parent(s): 4481683

Update Bsb

Browse files
Files changed (1) hide show
  1. Bsb +841 -654
Bsb CHANGED
@@ -1,660 +1,847 @@
1
- import os
2
- import psycopg2
3
- import psycopg2.extras
4
- import json
5
- import re
6
- from logging import getLogger
7
- from fastapi import HTTPException
8
- from dotenv import load_dotenv
9
- from pydantic import BaseModel
10
- from typing import List
11
- from logging_log.logger import logger
12
-
13
- # Load environment variables from the .env file
14
- load_dotenv()
15
-
16
- #logger = getLogger(__name__)
17
-
18
- # Pydantic model
19
- class UserCreate(BaseModel):
20
- user_id: str
21
- password: str
22
- ad_groups: List[str]
23
- is_admin: bool = False
24
-
25
- # Database connection function
26
- def get_db_connection():
27
- try:
28
- # Create the connection
29
- conn = psycopg2.connect(
30
- host=os.getenv("HOSTNAME"),
31
- database=os.getenv("DATABASE"),
32
- user=os.getenv("DB_USERNAME"),
33
- password=os.getenv("DB_PASSWORD"),
34
- port=os.getenv("PORT")
35
- )
36
- logger.info("Database connection established.")
37
- return conn
38
- except Exception as e:
39
- logger.error(f"Error connecting to database: {str(e)}")
40
- raise e
41
-
42
- # Validate user ID
43
- def validate_user_id(user_id: str):
44
- if not re.match(r'^[a-zA-Z0-9]{5,}$', user_id):
45
- raise HTTPException(status_code=400, detail="Invalid user ID format")
46
- if not user_id:
47
- raise HTTPException(status_code=400, detail="User ID cannot be empty")
48
-
49
- # Validate notebook ID
50
- def validate_notebook_id(notebook_id: str):
51
- if not notebook_id.isdigit():
52
- raise HTTPException(status_code=406, detail="Invalid notebook ID format")
53
-
54
- # Create a new user in the DB
55
- def create_user(user_id: str, password: str, ad_groups: List[str], is_admin: bool):
56
-
57
- insert_query = """
58
- INSERT INTO users (user_id, password, ad_groups, is_admin)
59
- VALUES (%s, %s, %s, %s)
60
- """
61
-
62
- try:
63
- with get_db_connection() as conn:
64
- with conn.cursor() as curr:
65
- curr.execute(insert_query, (user_id, password, ad_groups, is_admin))
66
- conn.commit()
67
- logger.info(f"User {user_id} created")
68
- except Exception as e:
69
- conn.rollback()
70
- logger.error(f"Error creating new user {user_id}")
71
-
72
- # Update an existing user in the DB
73
- def update_user(user_id: str, password: str, ad_groups: List[str], is_admin: bool):
74
- update_query = """
75
- UPDATE users
76
- SET password = %s,
77
- ad_groups = %s,
78
- is_admin = %s
79
- WHERE user_id = %s
80
- """
81
- try:
82
- with get_db_connection() as conn:
83
- with conn.cursor() as curr:
84
- curr.execute(update_query, (password, ad_groups, is_admin, user_id))
85
- conn.commit()
86
- logger.info(f"User {user_id} updated")
87
- except Exception as e:
88
- conn.rollback()
89
- logger.error(f"Error updating user {user_id}: {str(e)}")
90
- raise
91
-
92
- # delete a user in the DB
93
- def delete_user(user_id: str):
94
- delete_query = """
95
- DELETE FROM users WHERE user_id = %s;
96
- """
97
- try:
98
- with get_db_connection() as conn:
99
- with conn.cursor() as curr:
100
- curr.execute(delete_query, (user_id, ))
101
- conn.commit()
102
- logger.info(f"User {user_id} deleted")
103
- except Exception as e:
104
- conn.rollback()
105
- logger.error(f"Error deleting user {user_id}: {str(e)}")
106
- raise
107
-
108
- def get_user_credentials(user_id: str):
109
- select_query = """
110
- SELECT * FROM users WHERE user_id = %s;
111
- """
112
- try:
113
- with get_db_connection() as conn:
114
- with conn.cursor() as curr:
115
- curr.execute(select_query, (user_id,))
116
- user = curr.fetchone()
117
- if user:
118
- logger.info(f"User {user_id} fetched")
119
- return {
120
- "user_id": user[0],
121
- "password": user[1],
122
- "ad_groups": user[2],
123
- "is_admin": user[3]
124
- }
125
- else:
126
- logger.warning(f"User {user_id} not found")
127
- return {"error": "User not found"}
128
- except Exception as e:
129
- logger.error(f"Error fetching user {user_id}: {str(e)}")
130
- raise
131
-
132
- # Fetch user details
133
- def get_user_details(User_ID):
134
- validate_user_id(User_ID)
135
- conn = get_db_connection()
136
- cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
137
- try:
138
- cursor.execute(
139
- "SELECT ad_groups, is_admin FROM users WHERE user_id=%s",
140
- (User_ID, )
141
- )
142
- user_row = cursor.fetchone()
143
- if not user_row:
144
- raise HTTPException(status_code=404, detail="User not found")
145
-
146
- if user_row:
147
- ad_groups, is_admin = user_row
148
-
149
- cursor.execute("SELECT id, name, files, permission FROM efforts")
150
- efforts_data = cursor.fetchall()
151
-
152
- permissions = {
153
- "permissions": [],
154
- "efforts": [],
155
- "notebooks": {},
156
- "ezones": {},
157
- "global_ezones": [],
158
- "files": {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
160
- """
161
- if ad_group in user_permissions.get("can_create_effort", []):
162
- permissions["permissions"].append("can_create_effort")
163
- if ad_group in user_permissions.get("can_view_effort", []):
164
- permissions["permissions"].append("can_view_effort")
165
- if ad_group in user_permissions.get("can_revoke_effort", []):
166
- permissions["permissions"].append("can_revoke_effort")
167
- if ad_group in user_permissions.get("can_modify_effort", []):
168
- permissions["permissions"].append("can_modify_effort")
169
- """
170
-
171
- if is_admin:
172
- permissions["permissions"].append("can_create_efforts")
173
- permissions["permissions"].append("can_view_efforts")
174
- permissions["permissions"].append("can_revoke_efforts")
175
- permissions["permissions"].append("can_modify_efforts")
176
-
177
- cursor.execute("SELECT id, effort_id, name, config FROM zones")
178
- zone_data = cursor.fetchall()
179
-
180
- effort_id_to_name = {effort["id"]: effort["name"] for effort in efforts_data}
181
-
182
- for zone in zone_data:
183
- zone_id = zone["id"]
184
- zone_name = zone["name"]
185
- zone_effort = zone["effort_id"]
186
- zone_config = zone["config"]
187
-
188
- zone_effort_name = effort_id_to_name.get(zone_effort, "Unknown Effort")
189
-
190
- if zone_config["is_global"]:
191
- permissions["global_ezones"].append({"name": zone_name, "id": zone_id, "effort_id": zone_effort, "effort_name": zone_effort_name})
192
-
193
- for effort in efforts_data:
194
-
195
- effort_id = effort["id"]
196
- effort_name = effort["name"]
197
- files = effort["files"] if effort["files"] else []
198
- permission_json = effort["permission"]
199
- effort_permissions = []
200
-
201
- if "can_view_effort" not in effort_permissions:
202
- effort_permissions.append("can_view_effort")
203
- permissions["efforts"].append({"name": effort_name, "id": effort_id})
204
- permissions["files"][effort_id] = files
205
-
206
- cursor.execute("SELECT notebook_id, notebook_name FROM notebook_details WHERE effort_id=%s", (effort_id,))
207
- notebook_details = cursor.fetchall()
208
- permissions["notebooks"][effort_id] = [{"name": note["notebook_name"], "id": note["notebook_id"]} for note in notebook_details]
209
-
210
- cursor.execute("SELECT id, name FROM zones WHERE effort_id=%s", (effort_id,))
211
- zones = cursor.fetchall()
212
- permissions["ezones"][effort_id] = [{"name": zone["name"], "id": zone["id"]} for zone in zones]
213
-
214
-
215
- if "can_create_zones" not in effort_permissions:
216
- effort_permissions.append("can_create_zones")
217
- if "can_modify_zones" not in effort_permissions:
218
- effort_permissions.append("can_modify_zones")
219
- if "can_delete_zones" not in effort_permissions:
220
- effort_permissions.append("can_delete_zones")
221
-
222
- if "can_create_notebooks" not in effort_permissions:
223
- effort_permissions.append("can_create_notebooks")
224
- if "can_delete_notebooks" not in effort_permissions:
225
- effort_permissions.append("can_delete_notebooks")
226
-
227
- if effort_permissions:
228
- permissions["permissions"].append({effort_id: effort_permissions})
229
-
230
- else:
231
- permissions["permissions"].append("can_view_efforts")
232
-
233
- cursor.execute("SELECT id, effort_id, name, config FROM zones")
234
- zone_data = cursor.fetchall()
235
-
236
- effort_id_to_name = {effort["id"]: effort["name"] for effort in efforts_data}
237
-
238
- for zone in zone_data:
239
- zone_id = zone["id"]
240
- zone_name = zone["name"]
241
- zone_effort = zone["effort_id"]
242
- zone_config = zone["config"]
243
-
244
- zone_effort_name = effort_id_to_name.get(zone_effort, "Unknown Effort")
245
-
246
- if zone_config["is_global"]:
247
- permissions["global_ezones"].append({"name": zone_name, "id": zone_id, "effort_id": zone_effort, "effort_name": zone_effort_name})
248
-
249
- for effort in efforts_data:
250
-
251
- effort_id = effort["id"]
252
- effort_name = effort["name"]
253
- files = effort["files"] if effort["files"] else []
254
- permission_json = effort["permission"]
255
- effort_permissions = []
256
-
257
- """
258
- if ad_group in permission_json.get("can_create_zones", []):
259
- effort_permissions.append("can_create_zones")
260
- if ad_group in permission_json.get("can_modify_zones", []):
261
- effort_permissions.append("can_modify_zones")
262
- if ad_group in permission_json.get("can_delete_zones", []):
263
- effort_permissions.append("can_delete_zones")
264
- if ad_group in permission_json.get("can_create_notebooks", []):
265
- effort_permissions.append("can_create_notebooks")
266
- if ad_group in permission_json.get("can_delete_notebooks", []):
267
- effort_permissions.append("can_delete_notebooks")
268
- """
269
-
270
- for ad_group in ad_groups:
271
-
272
- if ad_group in permission_json.get("can_view_effort", []):
273
- if "can_view_effort" not in effort_permissions:
274
- effort_permissions.append("can_view_effort")
275
- permissions["efforts"].append({"name": effort_name, "id": effort_id})
276
- permissions["files"][effort_id] = files
277
-
278
- cursor.execute("SELECT notebook_id, notebook_name FROM notebook_details WHERE effort_id=%s", (effort_id,))
279
- notebook_details = cursor.fetchall()
280
- permissions["notebooks"][effort_id] = [{"name": note["notebook_name"], "id": note["notebook_id"]} for note in notebook_details]
281
-
282
- cursor.execute("SELECT id, name FROM zones WHERE effort_id=%s", (effort_id,))
283
- zones = cursor.fetchall()
284
- permissions["ezones"][effort_id] = [{"name": zone["name"], "id": zone["id"]} for zone in zones]
285
-
286
- if ad_group in permission_json.get("can_create_and_modify_zones", []):
287
- if "can_create_zones" not in effort_permissions:
288
- effort_permissions.append("can_create_zones")
289
- if "can_modify_zones" not in effort_permissions:
290
- effort_permissions.append("can_modify_zones")
291
- if "can_delete_zones" not in effort_permissions:
292
- effort_permissions.append("can_delete_zones")
293
-
294
- if ad_group in permission_json.get("can_create_and_modify_notebooks", []):
295
- if "can_create_notebooks" not in effort_permissions:
296
- effort_permissions.append("can_create_notebooks")
297
- if "can_delete_notebooks" not in effort_permissions:
298
- effort_permissions.append("can_delete_notebooks")
299
-
300
- if effort_permissions:
301
- permissions["permissions"].append({effort_id: effort_permissions})
302
-
303
- return {
304
- "permissions": permissions["permissions"],
305
- "efforts": permissions["efforts"],
306
- "notebooks": permissions["notebooks"],
307
- "ezones": permissions["ezones"],
308
- "global_ezones": permissions["global_ezones"],
309
- "files": permissions["files"]
310
  }
311
- except Exception as e:
312
- logger.error(f"Error fetching user details: {str(e)}")
313
- raise e
314
- finally:
315
- cursor.close()
316
- conn.close()
317
-
318
-
319
- # Fetch notebook details
320
- def get_notebook_details(notebook_id):
321
- validate_notebook_id(notebook_id)
322
- conn = get_db_connection()
323
- cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
324
- try:
325
- cursor.execute(
326
- "SELECT notebook_id, notebook_name, chat_history, created_at, effort_id FROM notebook_details WHERE notebook_id=%s",
327
- (notebook_id,)
328
- )
329
- notebook = cursor.fetchone()
330
- if not notebook:
331
- raise HTTPException(status_code=404, detail="Notebook not found")
332
-
333
- cursor.execute("SELECT name FROM efforts WHERE id=%s", (notebook["effort_id"],))
334
- effort = cursor.fetchone()
335
-
336
- return {
337
- "id": notebook["notebook_id"],
338
- "name": notebook["notebook_name"],
339
- "chat": notebook["chat_history"],
340
- "Created at": notebook["created_at"],
341
- "effort_name": effort["name"] if effort else None
342
  }
343
- except Exception as e:
344
- logger.error(f"Error fetching notebook details: {str(e)}")
345
- raise HTTPException(status_code=500, detail=f"Error fetching notebook details: {str(e)}")
346
- finally:
347
- cursor.close()
348
- conn.close()
349
-
350
- # Effort Management
351
-
352
- # Create effort
353
- def create_effort(name, permission):
354
- conn = None
355
- cursor = None
356
- try:
357
- conn = get_db_connection()
358
- cursor = conn.cursor()
359
- cursor.execute(
360
- "INSERT INTO efforts (name, permission) VALUES (%s, %s)",
361
- (name, psycopg2.extras.Json(permission))
362
- )
363
- conn.commit()
364
- logger.info(f"Effort '{name}' created.")
365
- except Exception as e:
366
- if conn:
367
- conn.rollback()
368
- logger.error(f"Error creating effort '{name}': {str(e)}")
369
- raise e
370
- finally:
371
- if cursor:
372
- cursor.close()
373
- if conn:
374
- conn.close()
375
-
376
- # Delete effort
377
- def delete_effort(effort_id):
378
- conn = None
379
- cursor = None
380
- try:
381
- conn = get_db_connection()
382
- cursor = conn.cursor()
383
- cursor.execute("DELETE FROM efforts WHERE id = %s", (effort_id,))
384
- conn.commit()
385
- if cursor.rowcount == 0:
386
- raise ValueError(f"Effort with ID {effort_id} not found")
387
- logger.info(f"Effort with ID {effort_id} deleted successfully.")
388
- except Exception as e:
389
- if conn:
390
- conn.rollback()
391
- logger.error(f"Error deleting effort with ID {effort_id}: {str(e)}")
392
- raise e
393
- finally:
394
- if cursor:
395
- cursor.close()
396
- if conn:
397
- conn.close()
398
-
399
- # Modify effort
400
- def modify_effort(effort_id, name, permission):
401
- conn = None
402
- cursor = None
403
- try:
404
- conn = get_db_connection()
405
- cursor = conn.cursor()
406
- cursor.execute(
407
- "UPDATE efforts SET name = %s, permission = %s WHERE id = %s",
408
- (name, psycopg2.extras.Json(permission), effort_id)
409
- )
410
- conn.commit()
411
- if cursor.rowcount == 0:
412
- raise ValueError(f"Effort with ID {effort_id} not found")
413
- logger.info(f"Effort with ID {effort_id} updated successfully.")
414
- except Exception as e:
415
- if conn:
416
- conn.rollback()
417
- logger.error(f"Error modifying effort with ID {effort_id}: {str(e)}")
418
- raise e
419
- finally:
420
- if cursor:
421
- cursor.close()
422
- if conn:
423
- conn.close()
424
-
425
- # List effort details
426
- def list_effort_details(effort_id):
427
- conn = None
428
- cursor = None
429
- try:
430
- conn = get_db_connection()
431
- cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
432
- cursor.execute("SELECT * FROM efforts WHERE id = %s", (effort_id,))
433
- effort_details = cursor.fetchone()
434
- if effort_details:
435
- logger.info(f"Effort details fetched for ID {effort_id}")
436
- return dict(effort_details)
437
- else:
438
- raise ValueError(f"Effort with ID {effort_id} not found")
439
- except Exception as e:
440
- logger.error(f"Error fetching effort details for ID {effort_id}: {str(e)}")
441
- raise e
442
- finally:
443
- if cursor:
444
- cursor.close()
445
- if conn:
446
- conn.close()
447
-
448
-
449
- # Zone Management
450
-
451
- # Delete experiment zone
452
- def delete_experiment_zone(zone_id: int):
453
- try:
454
- conn = get_db_connection()
455
- cursor = conn.cursor()
456
- cursor.execute("DELETE FROM zones WHERE id = %s", (zone_id,))
457
- conn.commit()
458
- if cursor.rowcount == 0:
459
- raise ValueError(f"Zone with ID {zone_id} not found")
460
- logger.info(f"Experiment zone with ID {zone_id} deleted successfully.")
461
- except Exception as e:
462
- logger.error(f"Error while deleting experiment zone: {str(e)}")
463
- raise e
464
- finally:
465
- cursor.close()
466
- conn.close()
467
-
468
- # Fetch zone details
469
- def get_zone_details(zone_id: int):
470
- try:
471
- conn = get_db_connection()
472
- cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
473
- cursor.execute("SELECT id, name, config, created_at FROM zones WHERE id = %s", (zone_id,))
474
- zone = cursor.fetchone()
475
- if not zone:
476
- logger.warning(f"No experiment zone found with ID {zone_id}.")
477
- raise ValueError(f"Zone with ID {zone_id} not found")
478
- logger.info(f"Details for experiment zone ID {zone_id} fetched successfully.")
479
- return zone
480
- except Exception as e:
481
- logger.error(f"Error while fetching zone details: {str(e)}")
482
- raise e
483
- finally:
484
- cursor.close()
485
- conn.close()
486
-
487
- # Create experiment zone
488
- def create_experiment_zone(effort_id: int, name: str, vector_db: str, llm_model: str, chunking_strategy: str, embedding_model: str, indexer: str, top_k: int, overlap: int, chunk_size: int, description: str, is_global: bool):
489
- try:
490
- conn = get_db_connection()
491
- cursor = conn.cursor()
492
- if not name:
493
- raise HTTPException(status_code=400, detail="Zone name cannot be empty")
494
-
495
- config = {
496
- "llm_model": llm_model,
497
- "embedding_model": embedding_model,
498
- "vector_db": vector_db,
499
- "chunking_strategy": chunking_strategy,
500
- "indexer": indexer,
501
- "top_k": top_k,
502
- "overlap": overlap,
503
- "chunk_size": chunk_size,
504
- "description": description,
505
- "is_global": is_global
 
 
 
 
 
 
 
506
  }
507
- cursor.execute(
508
- "INSERT INTO zones (effort_id, name, config) VALUES (%s, %s, %s) RETURNING id",
509
- (effort_id, name, json.dumps(config))
510
- )
511
- zone_id = cursor.fetchone()[0]
512
- conn.commit()
513
- logger.info(f"Experiment zone '{name}' created with ID {zone_id}.")
514
- return {"zone_id": zone_id}
515
- except Exception as e:
516
- logger.error(f"Error while creating experiment zone: {str(e)}")
517
- raise e
518
- finally:
519
- cursor.close()
520
- conn.close()
521
-
522
- # Modify experiment zone
523
- def modify_experiment_zone(zone_id: int, name: str, vector_db: str, llm_model: str, chunking_strategy: str, embedding_model: str, indexer: str, top_k: int, overlap: int, chunk_size: int, description: str, is_global: bool):
524
- try:
525
-
526
- conn = get_db_connection()
527
- cursor = conn.cursor()
528
- if not name:
529
- raise HTTPException(status_code=400, detail="Zone name cannot be empty")
530
-
531
- cursor.execute("SELECT effort_id FROM zones WHERE id = %s", (zone_id,))
532
-
533
- if cursor.rowcount == 0:
534
- raise ValueError(f"Experiment zone with ID {zone_id} not found")
535
-
536
- effort_id = cursor.fetchone()[0]
537
-
538
- config = {
539
- "llm_model": llm_model,
540
- "embedding_model": embedding_model,
541
- "vector_db": vector_db,
542
- "chunking_strategy": chunking_strategy,
543
- "indexer": indexer,
544
- "top_k": top_k,
545
- "overlap": overlap,
546
- "chunk_size": chunk_size,
547
- "description": description,
548
- "is_global": is_global
549
  }
550
 
551
- config_json = json.dumps(config)
552
- cursor.execute(
553
- "INSERT INTO zones (effort_id, name, config) VALUES (%s, %s, %s) RETURNING id",
554
- (effort_id, name, config_json)
555
- )
556
- new_zone_id = cursor.fetchone()[0]
557
-
558
- cursor.execute("""
559
- DELETE FROM zones
560
- WHERE id = %s
561
- """, (zone_id,))
562
-
563
- cursor.execute("""
564
- DELETE FROM chunk_embeddings
565
- WHERE zone_id = %s
566
- """, (zone_id,))
567
-
568
- cursor.execute("""
569
- DELETE FROM file_chunks
570
- WHERE zone_id = %s
571
- """, (zone_id,))
572
-
573
- conn.commit()
574
-
575
- logger.info(f"Experiment zone with ID {zone_id} modified successfully. New zone ID is {new_zone_id}")
576
-
577
- return new_zone_id
578
-
579
- except Exception as e:
580
- logger.error(f"Error while modifying experiment zone: {str(e)}")
581
- raise e
582
- finally:
583
- cursor.close()
584
- conn.close()
585
-
586
- #feedback function <-------->
587
-
588
- # def create_feedback(user_id: str, notebook_id: int, effort_id: int, zone_id: int, rating: int = None, comment: str= None):
589
-
590
- # insert_query = """
591
- # INSERT INTO feedback(user_id, notebook_id, effort_id, zone_id, rating, comment)
592
- # VALUES(%s, %s, %s, %s, %s, %s)
593
- # """
594
- # try:
595
- # with get_db_connection() as conn:
596
- # with conn.cursor() as curr:
597
- # curr.execute(insert_query,(user_id,notebook_id, effort_id, rating, comment))
598
- # conn.commit()
599
- # logger.info(f"Feedback submitted by the user {user_id} for Notebook {notebook_id}")
600
- # except Exception as e:
601
- # conn.rollback()
602
- # logger.error(f"Error while modifying experiment zone: {str(e)}")
603
- # raise e
604
-
605
- def feedback_exists(user_id: str, notebook_id: int, effort_id: int, zone_id: int):
606
- query = """
607
- SELECT id FROM feedback
608
- WHERE user_id = %s AND notebook_id = %s AND effort_id = %s AND zone_id = %s
609
- LIMIT 1
610
- """
611
- with get_db_connection() as conn:
612
- with conn.cursor() as cur:
613
- cur.execute(query, (user_id, notebook_id, effort_id, zone_id))
614
- result = cur.fetchone()
615
- return result[0] if result else None
616
-
617
- def create_or_update_feedback(
618
- user_id: str, notebook_id: int, effort_id: int, zone_id: int, rating: int = None, comment: str = None
619
- ):
620
- feedback_id = feedback_exists(user_id, notebook_id, effort_id, zone_id)
621
- try:
622
- with get_db_connection() as conn:
623
- with conn.cursor() as curr:
624
- if feedback_id:
625
- update_query = """
626
- UPDATE feedback
627
- SET rating = %s, comment = %s, created_at = NOW()
628
- WHERE id = %s
629
- """
630
- curr.execute(update_query, (rating, comment, feedback_id))
631
- logger.info(f"Feedback updated for user [{user_id}] in notebook [{notebook_id}] zone [{zone_id}]")
632
- else:
633
- insert_query = """
634
- INSERT INTO feedback(user_id, notebook_id, effort_id, zone_id, rating, comment)
635
- VALUES(%s, %s, %s, %s, %s, %s)
636
- """
637
- curr.execute(insert_query, (user_id, notebook_id, effort_id, zone_id, rating, comment))
638
- logger.info(f"Feedback created for user [{user_id}] in notebook [{notebook_id}] zone [{zone_id}]")
639
- conn.commit()
640
- except Exception as e:
641
- conn.rollback()
642
- logger.error(f"Error while saving feedback: {str(e)}")
643
- raise e
644
-
645
- def get_feedback(user_id: str, notebook_id: int, zone_id: int):
646
- query = """
647
- SELECT rating, comment FROM feedback
648
- WHERE user_id=%s AND notebook_id=%s AND zone_id=%s LIMIT 1;
649
- """
650
- try:
651
- with get_db_connection() as conn:
652
- with conn.cursor() as cur:
653
- cur.execute(query,(user_id, notebook_id,zone_id))
654
- result = cur.fetchone()
655
- if result:
656
- return{"rating": result[0], "comment": result[1]}
657
- return None
658
- except Exception as e:
659
- logger.error(f"Error retriving feedback: {str(e)}")
660
- return None
 
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>Chat Interface</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
9
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <style>
12
+ /* Unified CSS for Chat, EffortsHomePage, and NotebookPage */
13
+
14
+ body {
15
+ background-color: #f5f8fc;
16
+ color: #1a1a1a;
17
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
18
+ padding-bottom: 4rem;
19
+ }
20
+
21
+ .bg-gradient-to-r {
22
+ background: linear-gradient(90deg, #4f46e5, #3b82f6) !important;
23
+ }
24
+
25
+ .text-white {
26
+ color: #fff !important;
27
+ }
28
+
29
+ .card.card-item,
30
+ .w-1\/4.bg-white,
31
+ .w-3\/4.bg-white {
32
+ background-color: #ffffff !important;
33
+ border: 1px solid #d0d7e2 !important;
34
+ color: #1a1a1a;
35
+ border-radius: 10px !important;
36
+ box-shadow: 0 1px 4px rgba(0,0,0,0.05) !important;
37
+ padding: 1rem 1.5rem !important;
38
+ }
39
+
40
+ .shadow-md, .shadow {
41
+ box-shadow: 0 1px 4px rgba(0,0,0,0.05) !important;
42
+ }
43
+
44
+ .rounded-2xl, .rounded-lg {
45
+ border-radius: 10px !important;
46
+ }
47
+
48
+ #chat-box {
49
+ background: #f8f9fc !important;
50
+ border: 1px solid #d0d7e2 !important;
51
+ border-radius: 10px !important;
52
+ color: #1a1a1a;
53
+ height: 26rem;
54
+ }
55
+
56
+ input.form-control, .form-control, select.form-control,
57
+ #persona, #message, #summary-type {
58
+ background: #f8f9fc !important;
59
+ color: #1a1a1a !important;
60
+ border: 1px solid #d0d7e2 !important;
61
+ border-radius: 6px !important;
62
+ }
63
+
64
+ input.form-control::placeholder, .form-control::placeholder,
65
+ #persona::placeholder, #message::placeholder {
66
+ color: #9aa5bc !important;
67
+ }
68
+
69
+ .btn-gradient, .upload-button, .submit-btn, .bg-blue-600, .bg-green-600 {
70
+ background: linear-gradient(90deg, #4f46e5, #3b82f6) !important;
71
+ color: white !important;
72
+ border: none !important;
73
+ border-radius: 8px !important;
74
+ font-weight: 600;
75
+ transition: background 0.2s;
76
+ }
77
+
78
+ .btn-gradient:hover, .upload-button:hover, .submit-btn:hover, .bg-blue-700, .bg-green-700 {
79
+ background: linear-gradient(90deg, #4338ca, #2563eb) !important;
80
+ }
81
+
82
+ .btn-danger, .btn-outline-danger {
83
+ background-color: #d9534f !important;
84
+ color: white !important;
85
+ border: none !important;
86
+ border-radius: 8px !important;
87
+ font-weight: 600;
88
+ }
89
+
90
+ .btn-danger:hover, .btn-outline-danger:hover {
91
+ background-color: #c9302c !important;
92
+ }
93
+
94
+ #file-list > li {
95
+ border-radius: 10px !important;
96
+ border: 1px solid #d0d7e2 !important;
97
+ background: #fff !important;
98
+ box-shadow: 0 1px 4px rgba(0,0,0,0.05) !important;
99
+ padding: 1rem 1.5rem !important;
100
+ margin-bottom: 1rem !important;
101
+ transition: box-shadow 0.2s, border-color 0.2s;
102
+ }
103
+
104
+ #file-list > li:hover {
105
+ border-color: #a0b4e3 !important;
106
+ background-color: #f0f4ff !important;
107
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06) !important;
108
+ }
109
+
110
+ .loading-dots {
111
+ display: inline-block;
112
+ text-align: left;
113
+ }
114
+ .loading-dots span {
115
+ display: inline-block;
116
+ width: 8px;
117
+ height: 8px;
118
+ margin: 0 2px;
119
+ background-color: #555;
120
+ border-radius: 50%;
121
+ animation: blink 1.4s infinite both;
122
+ }
123
+ .loading-dots span:nth-child(2) { animation-delay: 0.2s; }
124
+ .loading-dots span:nth-child(3) { animation-delay: 0.4s; }
125
+ @keyframes blink {
126
+ 0%, 80%, 100% { opacity: 0; }
127
+ 40% { opacity: 1; }
128
+ }
129
+
130
+ #uploadModal .modal-content,
131
+ .modal-content {
132
+ background: #ffffff !important;
133
+ color: #1a1a1a;
134
+ border-radius: 12px !important;
135
+ border: 1px solid #d0d7e2 !important;
136
+ }
137
+
138
+ #uploadModal .drop-zone {
139
+ border-radius: 10px !important;
140
+ border: 2px dashed #d0d7e2 !important;
141
+ background: #f8f9fc !important;
142
+ color: #9aa5bc !important;
143
+ transition: border-color 0.2s, color 0.2s;
144
+ }
145
+
146
+ #uploadModal .drop-zone:hover, #uploadModal .drop-zone.border-blue-500 {
147
+ border-color: #4f46e5 !important;
148
+ color: #4f46e5 !important;
149
+ }
150
+
151
+ #effort-path, #notebook-path, #file-path {
152
+ font-size: 1rem;
153
+ font-weight: 600;
154
+ }
155
+
156
+ @media (max-width: 900px) {
157
+ .w-1\/4, .w-3\/4 {
158
+ width: 100% !important;
159
+ display: block !important;
160
+ }
161
+ .flex {
162
+ flex-direction: column !important;
163
+ }
164
+ }
165
+
166
+ /* Header Styles */
167
+ .chat-header {
168
+ background: #fff;
169
+ border-radius: 12px;
170
+ box-shadow: 0 1px 4px rgba(0,0,0,0.05);
171
+ padding: 1.25rem 2rem;
172
+ display: flex;
173
+ justify-content: space-between;
174
+ align-items: center;
175
+ margin-bottom: 2rem;
176
+ border: 1px solid #d0d7e2;
177
+ }
178
+ .chat-header-title {
179
+ font-size: 1.75rem; /* Matches h2 in your other HTML files */
180
+ background: linear-gradient(90deg, #4f46e5, #3b82f6);
181
+ background-clip: text;
182
+ -webkit-background-clip: text;
183
+ -webkit-text-fill-color: transparent;
184
+ }
185
+ .chat-header .no-style {
186
+ background: none;
187
+ background-clip: initial;
188
+ -webkit-background-clip: initial;
189
+ -webkit-text-fill-color: initial;
190
+ color: inherit;
191
+ }
192
+ </style>
193
+ </head>
194
+ <body class="font-sans">
195
+
196
+ <!-- Header -->
197
+ <div class="chat-header container-title mb-4">
198
+ <div class="d-flex align-items-center gap-3">
199
+ <button
200
+ onclick="window.location.href='/notebook/?effort_id={{ effort_id }}&user_id={{ user_id }}'"
201
+ id = "backBtn"
202
+ class="btn btn-outline-primary"
203
+ aria-label="Go back"
204
+ style="padding: 0.375rem 0.75rem;"
205
+ >
206
+ <i class="bi bi-arrow-left"></i>
207
+ </button>
208
+ <h2 class="fw-bold mb-0 chat-header-title">
209
+ <span class="no-style">📚</span> Document Chat & Summarization
210
+ </h2>
211
+ </div>
212
+ <div class="position-relative" style="min-width: 14rem;">
213
+ <select id="zone-dropdown"
214
+ class="form-control bg-white text-dark shadow-sm"
215
+ style="padding-right: 2.5rem;">
216
+ </select>
217
+ <span class="position-absolute top-50 end-0 translate-middle-y pe-3">
218
+ <svg class="w-4 h-4 text-secondary" fill="none" stroke="currentColor" stroke-width="2"
219
+ viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" style="width: 1.25rem; height: 1.25rem;">
220
+ <polyline points="6 9 12 15 18 9"></polyline>
221
+ </svg>
222
+ </span>
223
+ </div>
224
+ </div>
225
+
226
+ <!-- Directory Path Display -->
227
+ <div class="max-w-7xl mx-auto mt-6 px-4 text-gray-700 text-sm font-medium select-none">
228
+ <span id="effort-path" class="text-gray-800 font-semibold">{{ effort_name }}</span>
229
+ <span class="mx-2 text-gray-500">→</span>
230
+ <span id="notebook-path" class="text-gray-800 font-semibold">{{ notebook_name }}</span>
231
+ <span class="mx-2 text-gray-500">→</span>
232
+ <span id="file-path" class="text-blue-700 font-semibold">No File Selected</span>
233
+ </div>
234
+
235
+ <!-- Main Layout -->
236
+ <div class="flex max-w-7xl mx-auto mt-6 gap-6 px-4">
237
+ <!-- LEFT: Sidebar -->
238
+ <div class="w-1/4 bg-white rounded-2xl shadow-md p-4 h-[34rem] overflow-y-auto">
239
+ <button class="upload-button w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded mb-4"
240
+ onclick="openModal()">Upload File(s)</button>
241
+ <h3 class="text-lg font-semibold mb-2 text-gray-700">📂 Available Files</h3>
242
+ <ul id="file-list" class="space-y-3 text-gray-700 text-sm">
243
+ <!-- Files injected here -->
244
+ </ul>
245
+ </div>
246
+
247
+ <!-- RIGHT: Chat Panel -->
248
+ <div class="w-3/4 bg-white rounded-2xl shadow-md p-6 flex flex-col">
249
+ <div id="chat-box" class="overflow-y-scroll border rounded-lg p-4 bg-gray-50 space-y-3 mb-4">
250
+ <!-- Chat messages will appear here -->
251
+ </div>
252
+
253
+ <div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 mt-auto">
254
+ <input id="persona" type="text" placeholder="Enter persona..." value="You are a helpful assistant"
255
+ class="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" />
256
+
257
+ <input id="message" type="text" placeholder="Type your message..."
258
+ class="flex-grow px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
259
+
260
+ <button onclick="sendMessage()"
261
+ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition">
262
+ Send
263
+ </button>
264
+
265
+ <select id="summary-type"
266
+ class="px-3 py-2 border rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-green-500">
267
+ <option value="brief">Brief</option>
268
+ <option value="detailed">Detailed</option>
269
+ </select>
270
+
271
+ <button onclick="summarize()"
272
+ class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition">
273
+ Summarize
274
+ </button>
275
+ </div>
276
+ </div>
277
+ </div>
278
+
279
+ <!-- Upload Modal -->
280
+ <div id="uploadModal" class="modal fixed z-50 inset-0 hidden bg-black bg-opacity-50 flex justify-center items-center">
281
+ <div class="modal-content bg-white p-6 rounded-xl w-[90%] max-w-md relative">
282
+ <span class="close absolute top-3 right-4 text-xl cursor-pointer" onclick="closeModal()">&times;</span>
283
+ <h2 class="text-xl font-bold text-gray-800">NotebookLM</h2>
284
+ <p class="text-sm text-gray-600 mt-1">Add sources (PDF only)</p>
285
+ <div id="dropZone" class="drop-zone mt-4 border-2 border-dashed border-gray-300 p-6 rounded-lg text-gray-500 text-sm text-center cursor-pointer hover:border-blue-500 hover:text-blue-500"
286
+ onclick="triggerFileInput()">Drag & drop or <u>choose files</u><br><small>Supported: PDF</small></div>
287
+ <input type="file" id="fileInput" accept=".pdf" multiple class="hidden" />
288
+ <div id="selectedFilesList" class="mt-4 text-gray-700 text-sm space-y-1 max-h-40 overflow-y-auto">
289
+ <!-- File names shown here -->
290
+ </div>
291
+ <div class="flex justify-center mt-4">
292
+ <button class="submit-btn bg-blue-600 text-white px-4 py-2 rounded" onclick="submitFile()">Upload</button>
293
+ </div>
294
+ </div>
295
+ </div>
296
+
297
+ <!-- Delete File Confirmation Modal -->
298
+ <div class="modal fade" id="deleteFileModal" tabindex="-1" aria-labelledby="deleteFileModalLabel" aria-hidden="true">
299
+ <div class="modal-dialog">
300
+ <div class="modal-content">
301
+ <div class="modal-header">
302
+ <h5 class="modal-title" id="deleteFileModalLabel">Confirm Delete</h5>
303
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
304
+ </div>
305
+ <div class="modal-body">
306
+ Are you sure you want to delete <strong id="deleteFileName"></strong>?
307
+ </div>
308
+ <div class="modal-footer">
309
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
310
+ <button type="button" class="btn btn-danger" onclick="confirmDeleteFile()">Delete</button>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ <!-- FEEDBACK MODAL (Bootstrap) -->
316
+ <div class="modal fade" id="feedbackModal" tabindex="-1" aria-labelledby="feedbackModalLabel" aria-hidden="true">
317
+ <div class="modal-dialog" role="document">
318
+ <div class="modal-content" style="background-color: white; color: black;">
319
+ <div class="modal-header">
320
+ <h5 class="modal-title" id="feedbackModalLabel">Rate Your Experience</h5>
321
+ <!-- Proper Bootstrap “close” button -->
322
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
323
+ </div>
324
+ <div class="modal-body">
325
+ <label for="feedbackLevel">Rating (optional):</label>
326
+ <select id="feedbackLevel" class="form-control">
327
+ <option value="">-- Select --</option>
328
+ <option value="1">Very Bad</option>
329
+ <option value="2">Bad</option>
330
+ <option value="3">Neutral</option>
331
+ <option value="4">Good</option>
332
+ <option value="5">Very Good</option>
333
+ </select>
334
+
335
+ <label for="comment" class="mt-2">Comments (optional):</label>
336
+ <textarea id="comment" class="form-control" rows="3" placeholder="Write your feedback here..."></textarea>
337
+ <div id ="updateNotice" class = "text-secondary mt-2" style="display:none;font-size:0.9em;">
338
+ (Your Previously Submitted Feedback. You can update it if needed.)
339
+ </div>
340
+ </div>
341
+ <div class="modal-footer">
342
+ <button id="submitFeedback" class="btn btn-primary">Submit</button>
343
+ <button id="skipFeedback" type="button" class="btn btn-secondary">Skip</button>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+
349
+ <script>
350
+ const userId = "{{ user_id }}";
351
+ const effortId = "{{ effort_id }}";
352
+ const notebookId = "{{ notebook_id }}"
353
+ const effortName = "{{ effort_name }}";
354
+ const notebookName = "{{ notebook_name}}"
355
+ const preloadedFiles = {{ files | tojson }};
356
+ const preloadedZones = {{ zones | tojson }};
357
+ const globalZones = {{ global_ezones | tojson }};
358
+ const chatHistory = {{ chat_history | tojson }};
359
+ const chatBox = document.getElementById("chat-box");
360
+ const zoneDropdown = document.getElementById("zone-dropdown");
361
+ const fileList = document.getElementById("file-list");
362
+ const modal = document.getElementById("uploadModal");
363
+ const fileInput = document.getElementById("fileInput");
364
+
365
+ //Feedback modal -----
366
+ const backButton = document.getElementById("backBtn");
367
+ const feedbackModalEl = document.getElementById("feedbackModal");
368
+ const bootstrapFeedbackModal = new bootstrap.Modal(feedbackModalEl);
369
+
370
+ fileInput.addEventListener("change", () => {
371
+ const list = document.getElementById("selectedFilesList");
372
+ list.innerHTML = "";
373
+
374
+ const files = fileInput.files;
375
+ if (!files.length) {
376
+ list.textContent = "No files selected.";
377
+ return;
378
+ }
379
+
380
+ for (let file of files) {
381
+ const item = document.createElement("div");
382
+ item.className = "flex items-center gap-2";
383
+ item.innerHTML = `📄 <span class="truncate">${file.name}</span>`;
384
+ list.appendChild(item);
385
+ }
386
+ });
387
+
388
+ let currentZoneId = null;
389
+ let currentFileUri = "";
390
+ let selectedFileElement = null;
391
+
392
+ function appendMessage(sender, message, color, alignRight = false) {
393
+ const wrapper = document.createElement("div");
394
+ wrapper.className = alignRight ? "flex justify-end" : "flex justify-start";
395
+
396
+ const bubble = document.createElement("div");
397
+ bubble.className = `max-w-xl px-4 py-2 rounded-lg ${color} text-white`;
398
+
399
+ // Use <pre> tag to preserve formatting in the message
400
+ bubble.innerHTML = `<strong>${sender}:</strong><br><pre class="whitespace-pre-wrap break-words">${message}</pre>`;
401
+
402
+ wrapper.appendChild(bubble);
403
+ chatBox.appendChild(wrapper);
404
+ chatBox.scrollTop = chatBox.scrollHeight;
405
+ }
406
+
407
+ function loadZones() {
408
+ zoneDropdown.innerHTML = "";
409
+ const defaultOpt = document.createElement("option");
410
+ defaultOpt.disabled = true;
411
+ defaultOpt.selected = true;
412
+ defaultOpt.textContent = "Select a Configuration";
413
+ zoneDropdown.appendChild(defaultOpt);
414
+
415
+ try {
416
+ const addedZoneIds = new Set();
417
+
418
+ preloadedZones.forEach(zone => {
419
+ if (!addedZoneIds.has(zone.id)) {
420
+ const opt = document.createElement("option");
421
+ opt.value = zone.id;
422
+ opt.textContent = zone.name;
423
+ zoneDropdown.appendChild(opt);
424
+ addedZoneIds.add(zone.id);
425
+ }
426
+ });
427
+
428
+ globalZones.forEach(zone => {
429
+ if (!addedZoneIds.has(zone.id)) {
430
+ const opt = document.createElement("option");
431
+ opt.value = zone.id;
432
+ opt.textContent = zone.name + " (Global)";
433
+ zoneDropdown.appendChild(opt);
434
+ addedZoneIds.add(zone.id);
435
+ }
436
+ });
437
+
438
+ const divider = document.createElement("option");
439
+ divider.disabled = true;
440
+ divider.textContent = "──────────";
441
+ zoneDropdown.appendChild(divider);
442
+ } catch (err) {
443
+ console.error("Error loading zones:", err);
444
+ }
445
+
446
+ const manage = document.createElement("option");
447
+ manage.value = "manage";
448
+ manage.textContent = "➕ Manage Configurations";
449
+ zoneDropdown.appendChild(manage);
450
+
451
+ zoneDropdown.addEventListener("change", (e) => {
452
+ if (e.target.value === "manage") {
453
+ const form = document.createElement("form");
454
+ form.method = "POST";
455
+ form.action = "/chat_feature/manage_zones";
456
+
457
+ const userInput = document.createElement("input");
458
+ userInput.type = "hidden";
459
+ userInput.name = "user_id";
460
+ userInput.value = userId;
461
+ form.appendChild(userInput);
462
+
463
+ const effortInput = document.createElement("input");
464
+ effortInput.type = "hidden";
465
+ effortInput.name = "effort_id";
466
+ effortInput.value = effortId;
467
+ form.appendChild(effortInput);
468
+
469
+ const notebookInput = document.createElement("input");
470
+ notebookInput.type = "hidden";
471
+ notebookInput.name = "notebook_id";
472
+ notebookInput.value = notebookId;
473
+ form.appendChild(notebookInput);
474
+
475
+ document.body.appendChild(form);
476
+ form.submit();
477
+ } else {
478
+ currentZoneId = parseInt(e.target.value);
479
+ console.log("Zone selected, currentZoneID:", currentZoneId);
480
  }
481
+ });
482
+ }
483
+
484
+ //feedback Modal POP-UP on back Button
485
+ backButton.addEventListener("click", async function(event){
486
+ console.log("Back button clicked"); // Debugging line
487
+ event.preventDefault();
488
+
489
+ if(!currentZoneId){
490
+ console.log("no configuration selected, going back directly")
491
+ window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`;
492
+ return;
493
+ }
494
+
495
+ console.log("Debug BackBtn:", {userId, notebookId, currentZoneId, effortId});
496
+ try {
497
+ const response = await fetch(`/chat_feature/user_feedback_eligibility?user_id=${userId}&notebook_id=${notebookId}&zone_id=${currentZoneId}`);
498
+ const data = await response.json();
499
+
500
+ if (data.eligible_for_feedback) {
501
+ console.log("User is eligible for feedback:", data);// Debugging line
502
+ if(data.has_feedback){
503
+ document.getElementById("feedbackLevel").value = data.rating ||"";
504
+ document.getElementById("comment").value = data.comment ||"";
505
+ document.getElementById("updateNotice").style.display = "block";
506
+
507
+ }
508
+ else{
509
+ document.getElementById("feedbackLevel").value = data.rating ||"";
510
+ document.getElementById("comment").value = data.comment ||"";
511
+ document.getElementById("updateNotice").style.display = "none";
512
+
513
+ }
514
+ bootstrapFeedbackModal.show();
515
+ } else {
516
+ window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  }
518
+ } catch (error) {
519
+ console.error('Error checking eligibility:', error);
520
+ window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`;
521
+ }
522
+ });
523
+ // Feedback Submit
524
+ document.getElementById("submitFeedback").addEventListener("click", async function () {
525
+ const rating = document.getElementById("feedbackLevel").value;
526
+ const comment = document.getElementById("comment").value;
527
+
528
+ const payload = {
529
+ user_id: userId,
530
+ notebook_id: parseInt(notebookId),
531
+ effort_id: parseInt(effortId),
532
+ zone_id: parseInt(currentZoneId),
533
+ rating: rating ? parseInt(rating) : null,
534
+ comment: comment
535
+ };
536
+
537
+ try {
538
+ const response = await fetch("/chat_feature/submit_feedback", {
539
+ method: "POST",
540
+ headers: { "Content-Type": "application/json" },
541
+ body: JSON.stringify(payload)
542
+ });
543
+
544
+ if (response.ok) {
545
+ bootstrapFeedbackModal.hide();
546
+ window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`;
547
+ } else {
548
+ alert("Error submitting feedback");
549
  }
550
+ } catch (error) {
551
+ console.error("Error submitting feedback:", error);
552
+ alert("Error submitting feedback");
553
+ }
554
+ });
555
+
556
+ // Skip feedback
557
+ document.getElementById("skipFeedback").addEventListener("click", function () {
558
+ bootstrapFeedbackModal.hide();
559
+ window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`;
560
+ });
561
+
562
+ function loadFiles() {
563
+ const files = preloadedFiles || [];
564
+ fileList.innerHTML = "";
565
+
566
+ files.forEach(file => {
567
+ const li = document.createElement("li");
568
+ li.className =
569
+ "flex items-center gap-3 p-4 rounded-2xl border border-gray-200 bg-white hover:bg-blue-50 hover:shadow-lg transition shadow group";
570
+
571
+ const fileIcon = document.createElement("div");
572
+ fileIcon.className = "flex-shrink-0 text-blue-600 text-lg";
573
+ fileIcon.textContent = "📄";
574
+
575
+ const fileName = document.createElement("div");
576
+ fileName.className = "flex-1 min-w-0 cursor-pointer";
577
+ fileName.innerHTML = `
578
+ <p class="font-medium truncate group-hover:whitespace-normal group-hover:break-all" title="${file.name}">
579
+ ${file.name}
580
+ </p>
581
+ `;
582
+ fileName.onclick = () => {
583
+ currentFileUri = file.uri;
584
+ appendMessage("System", `Selected file: ${file.name}`, "bg-purple-600");
585
+
586
+ document.getElementById("notebook-path").textContent = notebookName;
587
+ document.getElementById("effort-path").textContent = effortName;
588
+ document.getElementById("file-path").textContent = file.name;
589
+
590
+ if (selectedFileElement) {
591
+ selectedFileElement.classList.remove("border-blue-500", "bg-blue-100", "ring", "ring-blue-300", "shadow-lg", "transform", "scale-105");
592
+ }
593
+
594
+ li.classList.add("border-blue-500", "bg-blue-100", "ring", "ring-blue-300", "shadow-lg", "transform", "scale-105");
595
+ selectedFileElement = li;
596
+ };
597
+
598
+ const deleteButton = document.createElement("button");
599
+ deleteButton.className = "btn btn-sm btn-outline-danger";
600
+ deleteButton.setAttribute("data-bs-toggle", "modal");
601
+ deleteButton.setAttribute("data-bs-target", "#deleteFileModal");
602
+ deleteButton.innerHTML = `<i class="bi bi-trash"></i>`;
603
+ deleteButton.onclick = (e) => {
604
+ e.stopPropagation();
605
+ setDeleteFile(file.uri, file.name);
606
+ };
607
+
608
+ li.appendChild(fileIcon);
609
+ li.appendChild(fileName);
610
+ li.appendChild(deleteButton);
611
+
612
+ fileList.appendChild(li);
613
+ });
614
+ }
615
+
616
+
617
+ async function sendMessage() {
618
+ const message = document.getElementById("message").value;
619
+ const persona = document.getElementById("persona").value;
620
+ if (!message.trim() || !currentFileUri) {
621
+ alert("Please select a file and type a message.");
622
+ return;
623
+ }
624
+ if (!currentZoneId) {
625
+ alert("Please select a zone before sending a message.");
626
+ return;
627
+ }
628
+
629
+ appendMessage("You", message, "bg-blue-500", true); // Right aligned
630
+
631
+ const loadingId = "loading-msg";
632
+ const loadingWrapper = document.createElement("div");
633
+ loadingWrapper.id = loadingId;
634
+ loadingWrapper.className = "flex justify-start";
635
+
636
+ const loadingBubble = document.createElement("div");
637
+ loadingBubble.className = "max-w-xl px-4 py-2 rounded-lg bg-gray-300 text-gray-800";
638
+ loadingBubble.innerHTML = `<strong>Assistant:</strong> <span class="loading-dots"><span></span><span></span><span></span></span>`;
639
+ loadingWrapper.appendChild(loadingBubble);
640
+ chatBox.appendChild(loadingWrapper);
641
+ chatBox.scrollTop = chatBox.scrollHeight;
642
+
643
+
644
+ const res = await fetch("/chat_feature/send_message", {
645
+ method: "POST",
646
+ headers: { "Content-Type": "application/json" },
647
+ body: JSON.stringify({
648
+ message,
649
+ persona,
650
+ file_uri: currentFileUri,
651
+ experiment_zone_id: currentZoneId,
652
+ effortId: effortId,
653
+ notebookId: notebookId
654
+ })
655
+ });
656
+
657
+ const data = await res.json();
658
+ document.getElementById(loadingId)?.remove();
659
+ appendMessage("Assistant", data.message || "No response", "bg-gray-700");
660
+ document.getElementById("message").value = "";
661
+ }
662
+
663
+ async function summarize() {
664
+ const summaryType = document.getElementById("summary-type").value;
665
+ if (!currentFileUri) {
666
+ alert("Please select a file and summarize.");
667
+ return;
668
+ }
669
+
670
+ if (!currentZoneId) {
671
+ alert("Please select a zone before summarizing.");
672
+ return;
673
+ }
674
+
675
+ appendMessage("You", `Summarize (${summaryType})`, "bg-green-500", true); // Right aligned
676
+
677
+ const loadingId = "loading-msg";
678
+ const loadingWrapper = document.createElement("div");
679
+ loadingWrapper.id = loadingId;
680
+ loadingWrapper.className = "flex justify-start";
681
+
682
+ const loadingBubble = document.createElement("div");
683
+ loadingBubble.className = "max-w-xl px-4 py-2 rounded-lg bg-gray-300 text-gray-800";
684
+ loadingBubble.innerHTML = `<strong>Assistant:</strong> <span class="loading-dots"><span></span><span></span><span></span></span>`;
685
+ loadingWrapper.appendChild(loadingBubble);
686
+ chatBox.appendChild(loadingWrapper);
687
+ chatBox.scrollTop = chatBox.scrollHeight;
688
+
689
+
690
+ const res = await fetch("/chat_feature/summarize", {
691
+ method: "POST",
692
+ headers: { "Content-Type": "application/json" },
693
+ body: JSON.stringify({
694
+ summary_type: summaryType,
695
+ file_uri: currentFileUri,
696
+ experiment_zone_id: currentZoneId,
697
+ effortId: effortId,
698
+ notebookId: notebookId
699
+ })
700
+ });
701
+
702
+ const data = await res.json();
703
+ document.getElementById(loadingId)?.remove();
704
+ appendMessage("Summary", data.message || "No summary", "bg-gray-700");
705
+ }
706
+
707
+ function openModal() { modal.classList.remove("hidden"); }
708
+ function closeModal() { modal.classList.add("hidden"); fileInput.value = ""; }
709
+ function triggerFileInput() { fileInput.click(); }
710
+
711
+ async function submitFile() {
712
+ const files = fileInput.files;
713
+ if (!files.length) return alert("Choose at least one file.");
714
+
715
+ const formData = new FormData();
716
+ for (let file of files) {
717
+ if (file.type !== "application/pdf") {
718
+ alert(`Invalid file type: ${file.name}`);
719
+ return;
720
  }
721
+ formData.append("files", file);
722
+ }
723
+
724
+ formData.append("effort_id", effortId);
725
+
726
+ try {
727
+ const res = await fetch("/chat_feature/upload_file", {
728
+ method: "POST",
729
+ body: formData
730
+ });
731
+
732
+ const contentType = res.headers.get("content-type") || "";
733
+ let responseData = contentType.includes("application/json") ? await res.json() : await res.text();
734
+
735
+ if (!res.ok) {
736
+ const errorMessage = typeof responseData === "string"
737
+ ? responseData
738
+ : (responseData.error || JSON.stringify(responseData));
739
+ alert(`Upload failed: ${errorMessage}`);
740
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  }
742
 
743
+ alert("Upload successful!");
744
+ location.reload(); // Refresh to get updated file list from server
745
+ } catch (err) {
746
+ alert("Error: " + err.message);
747
+ }
748
+ }
749
+
750
+ window.onclick = function (event) {
751
+ if (event.target == modal) closeModal();
752
+ }
753
+
754
+ let deleteFileUriToConfirm = "";
755
+ let deleteFileName = ""
756
+
757
+ function setDeleteFile(fileUri, fileName) {
758
+ deleteFileUriToConfirm = fileUri;
759
+ deleteFileName = fileName
760
+ document.getElementById("deleteFileName").textContent = fileName;
761
+ }
762
+
763
+ async function confirmDeleteFile() {
764
+ try {
765
+ const res = await fetch("/chat_feature/delete_file", {
766
+ method: "POST",
767
+ headers: { "Content-Type": "application/json" },
768
+ body: JSON.stringify({
769
+ file_uri: deleteFileUriToConfirm,
770
+ effort_id: effortId,
771
+ filename: deleteFileName
772
+ })
773
+ });
774
+
775
+ if (!res.ok) {
776
+ const text = await res.text();
777
+ alert("Failed to delete: " + text);
778
+ } else {
779
+ const modal = bootstrap.Modal.getInstance(document.getElementById("deleteFileModal"));
780
+ modal.hide();
781
+ alert("File deleted.");
782
+ location.reload();
783
+ }
784
+ } catch (err) {
785
+ alert("Error deleting file: " + err.message);
786
+ }
787
+ }
788
+
789
+ const dropZone = document.querySelector(".drop-zone");
790
+
791
+ dropZone.addEventListener("dragover", (e) => {
792
+ e.preventDefault();
793
+ dropZone.classList.add("border-blue-500", "text-blue-500");
794
+ });
795
+
796
+ dropZone.addEventListener("dragleave", () => {
797
+ dropZone.classList.remove("border-blue-500", "text-blue-500");
798
+ });
799
+
800
+ dropZone.addEventListener("drop", (e) => {
801
+ e.preventDefault();
802
+ dropZone.classList.remove("border-blue-500", "text-blue-500");
803
+
804
+ const files = e.dataTransfer.files;
805
+ fileInput.files = files;
806
+
807
+ // Optional: Display file names immediately
808
+ const list = document.getElementById("selectedFilesList");
809
+ list.innerHTML = "";
810
+ for (const file of files) {
811
+ const p = document.createElement("p");
812
+ p.textContent = file.name;
813
+ list.appendChild(p);
814
+ }
815
+ });
816
+
817
+ window.onload = function () {
818
+
819
+ loadFiles();
820
+ loadZones();
821
+
822
+ let historyFilename = ""
823
+
824
+ if (chatHistory && chatHistory.length > 0) {
825
+ chatHistory.forEach(entry => {
826
+
827
+ if (entry.filename && (entry.filename !== historyFilename || historyFilename === "")) {
828
+ historyFilename = entry.filename;
829
+ appendMessage("System", `Selected file: ${historyFilename}`, "bg-purple-600");
830
+ }
831
+
832
+ // User message
833
+ const userMessage = entry.question;
834
+ appendMessage("You", userMessage, "bg-blue-500", true);
835
+ const personaMessage = entry.persona;
836
+ appendMessage("Persona", personaMessage, "bg-blue-500", true);
837
+ // Assistant response
838
+ const botMessage = entry.response;
839
+ appendMessage("Assistant", botMessage, "bg-gray-700", false);
840
+ });
841
+ }
842
+ };
843
+
844
+ </script>
845
+
846
+ </body>
847
+ </html>