|
|
import os |
|
|
|
|
|
import psycopg2 |
|
|
|
|
|
import psycopg2.extras |
|
|
|
|
|
import json |
|
|
|
|
|
import re |
|
|
|
|
|
from logging import getLogger |
|
|
|
|
|
from fastapi import HTTPException |
|
|
|
|
|
from dotenv import load_dotenv |
|
|
|
|
|
from pydantic import BaseModel |
|
|
|
|
|
from typing import List |
|
|
|
|
|
from logging_log.logger import logger |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UserCreate(BaseModel): |
|
|
|
|
|
user_id: str |
|
|
|
|
|
password: str |
|
|
|
|
|
ad_groups: List[str] |
|
|
|
|
|
is_admin: bool = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_db_connection(): |
|
|
|
|
|
try: |
|
|
|
|
|
|
|
|
|
|
|
conn = psycopg2.connect( |
|
|
|
|
|
host=os.getenv("HOSTNAME"), |
|
|
|
|
|
database=os.getenv("DATABASE"), |
|
|
|
|
|
user=os.getenv("DB_USERNAME"), |
|
|
|
|
|
password=os.getenv("DB_PASSWORD"), |
|
|
|
|
|
port=os.getenv("PORT") |
|
|
|
|
|
) |
|
|
|
|
|
logger.info("Database connection established.") |
|
|
|
|
|
return conn |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error connecting to database: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_user_id(user_id: str): |
|
|
|
|
|
if not re.match(r'^[a-zA-Z0-9]{5,}$', user_id): |
|
|
|
|
|
raise HTTPException(status_code=400, detail="Invalid user ID format") |
|
|
|
|
|
if not user_id: |
|
|
|
|
|
raise HTTPException(status_code=400, detail="User ID cannot be empty") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_notebook_id(notebook_id: str): |
|
|
|
|
|
if not notebook_id.isdigit(): |
|
|
|
|
|
raise HTTPException(status_code=406, detail="Invalid notebook ID format") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_user(user_id: str, password: str, ad_groups: List[str], is_admin: bool): |
|
|
|
|
|
|
|
|
|
|
|
insert_query = """ |
|
|
|
|
|
INSERT INTO users (user_id, password, ad_groups, is_admin) |
|
|
|
|
|
VALUES (%s, %s, %s, %s) |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
with get_db_connection() as conn: |
|
|
|
|
|
with conn.cursor() as curr: |
|
|
|
|
|
curr.execute(insert_query, (user_id, password, ad_groups, is_admin)) |
|
|
|
|
|
conn.commit() |
|
|
|
|
|
logger.info(f"User {user_id} created") |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
conn.rollback() |
|
|
|
|
|
logger.error(f"Error creating new user {user_id}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_user(user_id: str, password: str, ad_groups: List[str], is_admin: bool): |
|
|
|
|
|
update_query = """ |
|
|
|
|
|
UPDATE users |
|
|
|
|
|
SET password = %s, |
|
|
|
|
|
ad_groups = %s, |
|
|
|
|
|
is_admin = %s |
|
|
|
|
|
WHERE user_id = %s |
|
|
|
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
with get_db_connection() as conn: |
|
|
|
|
|
with conn.cursor() as curr: |
|
|
|
|
|
curr.execute(update_query, (password, ad_groups, is_admin, user_id)) |
|
|
|
|
|
conn.commit() |
|
|
|
|
|
logger.info(f"User {user_id} updated") |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
conn.rollback() |
|
|
|
|
|
logger.error(f"Error updating user {user_id}: {str(e)}") |
|
|
|
|
|
raise |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_user(user_id: str): |
|
|
|
|
|
delete_query = """ |
|
|
|
|
|
DELETE FROM users WHERE user_id = %s; |
|
|
|
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
with get_db_connection() as conn: |
|
|
|
|
|
with conn.cursor() as curr: |
|
|
|
|
|
curr.execute(delete_query, (user_id, )) |
|
|
|
|
|
conn.commit() |
|
|
|
|
|
logger.info(f"User {user_id} deleted") |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
conn.rollback() |
|
|
|
|
|
logger.error(f"Error deleting user {user_id}: {str(e)}") |
|
|
|
|
|
raise |
|
|
|
|
|
|
|
|
|
|
|
def get_user_credentials(user_id: str): |
|
|
|
|
|
select_query = """ |
|
|
|
|
|
SELECT * FROM users WHERE user_id = %s; |
|
|
|
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
with get_db_connection() as conn: |
|
|
|
|
|
with conn.cursor() as curr: |
|
|
|
|
|
curr.execute(select_query, (user_id,)) |
|
|
|
|
|
user = curr.fetchone() |
|
|
|
|
|
if user: |
|
|
|
|
|
logger.info(f"User {user_id} fetched") |
|
|
|
|
|
return { |
|
|
|
|
|
"user_id": user[0], |
|
|
|
|
|
"password": user[1], |
|
|
|
|
|
"ad_groups": user[2], |
|
|
|
|
|
"is_admin": user[3] |
|
|
|
|
|
} |
|
|
|
|
|
else: |
|
|
|
|
|
logger.warning(f"User {user_id} not found") |
|
|
|
|
|
return {"error": "User not found"} |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error fetching user {user_id}: {str(e)}") |
|
|
|
|
|
raise |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_user_details(User_ID): |
|
|
|
|
|
validate_user_id(User_ID) |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) |
|
|
|
|
|
try: |
|
|
|
|
|
cursor.execute( |
|
|
|
|
|
"SELECT ad_groups, is_admin FROM users WHERE user_id=%s", |
|
|
|
|
|
(User_ID, ) |
|
|
|
|
|
) |
|
|
|
|
|
user_row = cursor.fetchone() |
|
|
|
|
|
if not user_row: |
|
|
|
|
|
raise HTTPException(status_code=404, detail="User not found") |
|
|
|
|
|
|
|
|
|
|
|
if user_row: |
|
|
|
|
|
ad_groups, is_admin = user_row |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT id, name, files, permission FROM efforts") |
|
|
|
|
|
efforts_data = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
|
|
permissions = { |
|
|
|
|
|
"permissions": [], |
|
|
|
|
|
"efforts": [], |
|
|
|
|
|
"notebooks": {}, |
|
|
|
|
|
"ezones": {}, |
|
|
|
|
|
"files": {} |
|
|
|
|
|
} |
|
|
|
|
|
""" |
|
|
|
|
|
if ad_group in user_permissions.get("can_create_effort", []): |
|
|
|
|
|
permissions["permissions"].append("can_create_effort") |
|
|
|
|
|
if ad_group in user_permissions.get("can_view_effort", []): |
|
|
|
|
|
permissions["permissions"].append("can_view_effort") |
|
|
|
|
|
if ad_group in user_permissions.get("can_revoke_effort", []): |
|
|
|
|
|
permissions["permissions"].append("can_revoke_effort") |
|
|
|
|
|
if ad_group in user_permissions.get("can_modify_effort", []): |
|
|
|
|
|
permissions["permissions"].append("can_modify_effort") |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
if is_admin: |
|
|
|
|
|
permissions["permissions"].append("can_create_efforts") |
|
|
|
|
|
permissions["permissions"].append("can_view_efforts") |
|
|
|
|
|
permissions["permissions"].append("can_revoke_efforts") |
|
|
|
|
|
permissions["permissions"].append("can_modify_efforts") |
|
|
|
|
|
|
|
|
|
|
|
for effort in efforts_data: |
|
|
|
|
|
|
|
|
|
|
|
effort_id = effort["id"] |
|
|
|
|
|
effort_name = effort["name"] |
|
|
|
|
|
files = effort["files"] if effort["files"] else [] |
|
|
|
|
|
permission_json = effort["permission"] |
|
|
|
|
|
effort_permissions = [] |
|
|
|
|
|
|
|
|
|
|
|
if "can_view_effort" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_view_effort") |
|
|
|
|
|
permissions["efforts"].append({"name": effort_name, "id": effort_id}) |
|
|
|
|
|
permissions["files"][effort_id] = files |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT notebook_id, notebook_name FROM notebook_details WHERE effort_id=%s", (effort_id,)) |
|
|
|
|
|
notebook_details = cursor.fetchall() |
|
|
|
|
|
permissions["notebooks"][effort_id] = [{"name": note["notebook_name"], "id": note["notebook_id"]} for note in notebook_details] |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT id, name FROM zones WHERE effort_id=%s", (effort_id,)) |
|
|
|
|
|
zones = cursor.fetchall() |
|
|
|
|
|
permissions["ezones"][effort_id] = [{"name": zone["name"], "id": zone["id"]} for zone in zones] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if "can_create_zones" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_create_zones") |
|
|
|
|
|
if "can_modify_zones" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_modify_zones") |
|
|
|
|
|
if "can_delete_zones" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_delete_zones") |
|
|
|
|
|
|
|
|
|
|
|
if "can_create_notebooks" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_create_notebooks") |
|
|
|
|
|
if "can_delete_notebooks" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_delete_notebooks") |
|
|
|
|
|
|
|
|
|
|
|
if effort_permissions: |
|
|
|
|
|
permissions["permissions"].append({effort_id: effort_permissions}) |
|
|
|
|
|
|
|
|
|
|
|
else: |
|
|
|
|
|
permissions["permissions"].append("can_view_efforts") |
|
|
|
|
|
|
|
|
|
|
|
for effort in efforts_data: |
|
|
|
|
|
|
|
|
|
|
|
effort_id = effort["id"] |
|
|
|
|
|
effort_name = effort["name"] |
|
|
|
|
|
files = effort["files"] if effort["files"] else [] |
|
|
|
|
|
permission_json = effort["permission"] |
|
|
|
|
|
effort_permissions = [] |
|
|
|
|
|
|
|
|
|
|
|
""" |
|
|
|
|
|
if ad_group in permission_json.get("can_create_zones", []): |
|
|
|
|
|
effort_permissions.append("can_create_zones") |
|
|
|
|
|
if ad_group in permission_json.get("can_modify_zones", []): |
|
|
|
|
|
effort_permissions.append("can_modify_zones") |
|
|
|
|
|
if ad_group in permission_json.get("can_delete_zones", []): |
|
|
|
|
|
effort_permissions.append("can_delete_zones") |
|
|
|
|
|
if ad_group in permission_json.get("can_create_notebooks", []): |
|
|
|
|
|
effort_permissions.append("can_create_notebooks") |
|
|
|
|
|
if ad_group in permission_json.get("can_delete_notebooks", []): |
|
|
|
|
|
effort_permissions.append("can_delete_notebooks") |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
for ad_group in ad_groups: |
|
|
|
|
|
|
|
|
|
|
|
if ad_group in permission_json.get("can_view_effort", []): |
|
|
|
|
|
if "can_view_effort" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_view_effort") |
|
|
|
|
|
permissions["efforts"].append({"name": effort_name, "id": effort_id}) |
|
|
|
|
|
permissions["files"][effort_id] = files |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT notebook_id, notebook_name FROM notebook_details WHERE effort_id=%s", (effort_id,)) |
|
|
|
|
|
notebook_details = cursor.fetchall() |
|
|
|
|
|
permissions["notebooks"][effort_id] = [{"name": note["notebook_name"], "id": note["notebook_id"]} for note in notebook_details] |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT id, name FROM zones WHERE effort_id=%s", (effort_id,)) |
|
|
|
|
|
zones = cursor.fetchall() |
|
|
|
|
|
permissions["ezones"][effort_id] = [{"name": zone["name"], "id": zone["id"]} for zone in zones] |
|
|
|
|
|
|
|
|
|
|
|
if ad_group in permission_json.get("can_create_and_modify_zones", []): |
|
|
|
|
|
if "can_create_zones" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_create_zones") |
|
|
|
|
|
if "can_modify_zones" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_modify_zones") |
|
|
|
|
|
if "can_delete_zones" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_delete_zones") |
|
|
|
|
|
|
|
|
|
|
|
if ad_group in permission_json.get("can_create_and_modify_notebooks", []): |
|
|
|
|
|
if "can_create_notebooks" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_create_notebooks") |
|
|
|
|
|
if "can_delete_notebooks" not in effort_permissions: |
|
|
|
|
|
effort_permissions.append("can_delete_notebooks") |
|
|
|
|
|
|
|
|
|
|
|
if effort_permissions: |
|
|
|
|
|
permissions["permissions"].append({effort_id: effort_permissions}) |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
"permissions": permissions["permissions"], |
|
|
|
|
|
"efforts": permissions["efforts"], |
|
|
|
|
|
"notebooks": permissions["notebooks"], |
|
|
|
|
|
"ezones": permissions["ezones"], |
|
|
|
|
|
"files": permissions["files"] |
|
|
|
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error fetching user details: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_notebook_details(notebook_id): |
|
|
|
|
|
validate_notebook_id(notebook_id) |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) |
|
|
|
|
|
try: |
|
|
|
|
|
cursor.execute( |
|
|
|
|
|
"SELECT notebook_id, notebook_name, chat_history, created_at, effort_id FROM notebook_details WHERE notebook_id=%s", |
|
|
|
|
|
(notebook_id,) |
|
|
|
|
|
) |
|
|
|
|
|
notebook = cursor.fetchone() |
|
|
|
|
|
if not notebook: |
|
|
|
|
|
raise HTTPException(status_code=404, detail="Notebook not found") |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT name FROM efforts WHERE id=%s", (notebook["effort_id"],)) |
|
|
|
|
|
effort = cursor.fetchone() |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
"id": notebook["notebook_id"], |
|
|
|
|
|
"name": notebook["notebook_name"], |
|
|
|
|
|
"chat": notebook["chat_history"], |
|
|
|
|
|
"Created at": notebook["created_at"], |
|
|
|
|
|
"effort_name": effort["name"] if effort else None |
|
|
|
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error fetching notebook details: {str(e)}") |
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Error fetching notebook details: {str(e)}") |
|
|
|
|
|
finally: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_effort(name, permission): |
|
|
|
|
|
conn = None |
|
|
|
|
|
cursor = None |
|
|
|
|
|
try: |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor() |
|
|
|
|
|
cursor.execute( |
|
|
|
|
|
"INSERT INTO efforts (name, permission) VALUES (%s, %s)", |
|
|
|
|
|
(name, psycopg2.extras.Json(permission)) |
|
|
|
|
|
) |
|
|
|
|
|
conn.commit() |
|
|
|
|
|
logger.info(f"Effort '{name}' created.") |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
if conn: |
|
|
|
|
|
conn.rollback() |
|
|
|
|
|
logger.error(f"Error creating effort '{name}': {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
if cursor: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
if conn: |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_effort(effort_id): |
|
|
|
|
|
conn = None |
|
|
|
|
|
cursor = None |
|
|
|
|
|
try: |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor() |
|
|
|
|
|
cursor.execute("DELETE FROM efforts WHERE id = %s", (effort_id,)) |
|
|
|
|
|
conn.commit() |
|
|
|
|
|
if cursor.rowcount == 0: |
|
|
|
|
|
raise ValueError(f"Effort with ID {effort_id} not found") |
|
|
|
|
|
logger.info(f"Effort with ID {effort_id} deleted successfully.") |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
if conn: |
|
|
|
|
|
conn.rollback() |
|
|
|
|
|
logger.error(f"Error deleting effort with ID {effort_id}: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
if cursor: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
if conn: |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def modify_effort(effort_id, name, permission): |
|
|
|
|
|
conn = None |
|
|
|
|
|
cursor = None |
|
|
|
|
|
try: |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor() |
|
|
|
|
|
cursor.execute( |
|
|
|
|
|
"UPDATE efforts SET name = %s, permission = %s WHERE id = %s", |
|
|
|
|
|
(name, psycopg2.extras.Json(permission), effort_id) |
|
|
|
|
|
) |
|
|
|
|
|
conn.commit() |
|
|
|
|
|
if cursor.rowcount == 0: |
|
|
|
|
|
raise ValueError(f"Effort with ID {effort_id} not found") |
|
|
|
|
|
logger.info(f"Effort with ID {effort_id} updated successfully.") |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
if conn: |
|
|
|
|
|
conn.rollback() |
|
|
|
|
|
logger.error(f"Error modifying effort with ID {effort_id}: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
if cursor: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
if conn: |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_effort_details(effort_id): |
|
|
|
|
|
conn = None |
|
|
|
|
|
cursor = None |
|
|
|
|
|
try: |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) |
|
|
|
|
|
cursor.execute("SELECT * FROM efforts WHERE id = %s", (effort_id,)) |
|
|
|
|
|
effort_details = cursor.fetchone() |
|
|
|
|
|
if effort_details: |
|
|
|
|
|
logger.info(f"Effort details fetched for ID {effort_id}") |
|
|
|
|
|
return dict(effort_details) |
|
|
|
|
|
else: |
|
|
|
|
|
raise ValueError(f"Effort with ID {effort_id} not found") |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error fetching effort details for ID {effort_id}: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
if cursor: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
if conn: |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_experiment_zone(zone_id: int): |
|
|
|
|
|
try: |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor() |
|
|
|
|
|
cursor.execute("DELETE FROM zones WHERE id = %s", (zone_id,)) |
|
|
|
|
|
conn.commit() |
|
|
|
|
|
if cursor.rowcount == 0: |
|
|
|
|
|
raise ValueError(f"Zone with ID {zone_id} not found") |
|
|
|
|
|
logger.info(f"Experiment zone with ID {zone_id} deleted successfully.") |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error while deleting experiment zone: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_zone_details(zone_id: int): |
|
|
|
|
|
try: |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) |
|
|
|
|
|
cursor.execute("SELECT id, name, config, created_at FROM zones WHERE id = %s", (zone_id,)) |
|
|
|
|
|
zone = cursor.fetchone() |
|
|
|
|
|
if not zone: |
|
|
|
|
|
logger.warning(f"No experiment zone found with ID {zone_id}.") |
|
|
|
|
|
raise ValueError(f"Zone with ID {zone_id} not found") |
|
|
|
|
|
logger.info(f"Details for experiment zone ID {zone_id} fetched successfully.") |
|
|
|
|
|
return zone |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error while fetching zone details: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
|
|
|
try: |
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor() |
|
|
|
|
|
if not name: |
|
|
|
|
|
raise HTTPException(status_code=400, detail="Zone name cannot be empty") |
|
|
|
|
|
|
|
|
|
|
|
config = { |
|
|
|
|
|
"llm_model": llm_model, |
|
|
|
|
|
"embedding_model": embedding_model, |
|
|
|
|
|
"vector_db": vector_db, |
|
|
|
|
|
"chunking_strategy": chunking_strategy, |
|
|
|
|
|
"indexer": indexer, |
|
|
|
|
|
"top_k": top_k, |
|
|
|
|
|
"overlap": overlap, |
|
|
|
|
|
"chunk_size": chunk_size |
|
|
|
|
|
} |
|
|
|
|
|
cursor.execute( |
|
|
|
|
|
"INSERT INTO zones (effort_id, name, config) VALUES (%s, %s, %s) RETURNING id", |
|
|
|
|
|
(effort_id, name, json.dumps(config)) |
|
|
|
|
|
) |
|
|
|
|
|
zone_id = cursor.fetchone()[0] |
|
|
|
|
|
conn.commit() |
|
|
|
|
|
logger.info(f"Experiment zone '{name}' created with ID {zone_id}.") |
|
|
|
|
|
return {"zone_id": zone_id} |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error while creating experiment zone: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
|
|
|
try: |
|
|
|
|
|
|
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
cursor = conn.cursor() |
|
|
|
|
|
if not name: |
|
|
|
|
|
raise HTTPException(status_code=400, detail="Zone name cannot be empty") |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT effort_id FROM zones WHERE id = %s", (zone_id,)) |
|
|
|
|
|
|
|
|
|
|
|
if cursor.rowcount == 0: |
|
|
|
|
|
raise ValueError(f"Experiment zone with ID {zone_id} not found") |
|
|
|
|
|
|
|
|
|
|
|
effort_id = cursor.fetchone()[0] |
|
|
|
|
|
|
|
|
|
|
|
config = { |
|
|
|
|
|
"llm_model": llm_model, |
|
|
|
|
|
"embedding_model": embedding_model, |
|
|
|
|
|
"vector_db": vector_db, |
|
|
|
|
|
"chunking_strategy": chunking_strategy, |
|
|
|
|
|
"indexer": indexer, |
|
|
|
|
|
"top_k": top_k, |
|
|
|
|
|
"overlap": overlap, |
|
|
|
|
|
"chunk_size": chunk_size |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
config_json = json.dumps(config) |
|
|
|
|
|
cursor.execute( |
|
|
|
|
|
"INSERT INTO zones (effort_id, name, config) VALUES (%s, %s, %s) RETURNING id", |
|
|
|
|
|
(effort_id, name, config_json) |
|
|
|
|
|
) |
|
|
|
|
|
new_zone_id = cursor.fetchone()[0] |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
|
DELETE FROM zones |
|
|
|
|
|
WHERE id = %s |
|
|
|
|
|
""", (zone_id,)) |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
|
DELETE FROM chunk_embeddings |
|
|
|
|
|
WHERE zone_id = %s |
|
|
|
|
|
""", (zone_id,)) |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
|
DELETE FROM file_chunks |
|
|
|
|
|
WHERE zone_id = %s |
|
|
|
|
|
""", (zone_id,)) |
|
|
|
|
|
|
|
|
|
|
|
conn.commit() |
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Experiment zone with ID {zone_id} modified successfully. New zone ID is {new_zone_id}") |
|
|
|
|
|
|
|
|
|
|
|
return new_zone_id |
|
|
|
|
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error while modifying experiment zone: {str(e)}") |
|
|
|
|
|
raise e |
|
|
|
|
|
finally: |
|
|
|
|
|
cursor.close() |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------------------- |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
|
|
<title>Chat Interface</title> |
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet"> |
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<style> |
|
|
/* Unified CSS for Chat, EffortsHomePage, and NotebookPage */ |
|
|
|
|
|
body { |
|
|
background-color: |
|
|
color: |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
padding-bottom: 4rem; |
|
|
} |
|
|
|
|
|
.bg-gradient-to-r { |
|
|
background: linear-gradient(90deg, |
|
|
} |
|
|
|
|
|
.text-white { |
|
|
color: |
|
|
} |
|
|
|
|
|
.card.card-item, |
|
|
.w-1\/4.bg-white, |
|
|
.w-3\/4.bg-white { |
|
|
background-color: |
|
|
border: 1px solid |
|
|
color: |
|
|
border-radius: 10px !important; |
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05) !important; |
|
|
padding: 1rem 1.5rem !important; |
|
|
} |
|
|
|
|
|
.shadow-md, .shadow { |
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05) !important; |
|
|
} |
|
|
|
|
|
.rounded-2xl, .rounded-lg { |
|
|
border-radius: 10px !important; |
|
|
} |
|
|
|
|
|
|
|
|
background: |
|
|
border: 1px solid |
|
|
border-radius: 10px !important; |
|
|
color: |
|
|
height: 26rem; |
|
|
} |
|
|
|
|
|
input.form-control, .form-control, select.form-control, |
|
|
|
|
|
background: |
|
|
color: |
|
|
border: 1px solid |
|
|
border-radius: 6px !important; |
|
|
} |
|
|
|
|
|
input.form-control::placeholder, .form-control::placeholder, |
|
|
|
|
|
color: |
|
|
} |
|
|
|
|
|
.btn-gradient, .upload-button, .submit-btn, .bg-blue-600, .bg-green-600 { |
|
|
background: linear-gradient(90deg, |
|
|
color: white !important; |
|
|
border: none !important; |
|
|
border-radius: 8px !important; |
|
|
font-weight: 600; |
|
|
transition: background 0.2s; |
|
|
} |
|
|
|
|
|
.btn-gradient:hover, .upload-button:hover, .submit-btn:hover, .bg-blue-700, .bg-green-700 { |
|
|
background: linear-gradient(90deg, |
|
|
} |
|
|
|
|
|
.btn-danger, .btn-outline-danger { |
|
|
background-color: |
|
|
color: white !important; |
|
|
border: none !important; |
|
|
border-radius: 8px !important; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.btn-danger:hover, .btn-outline-danger:hover { |
|
|
background-color: |
|
|
} |
|
|
|
|
|
|
|
|
border-radius: 10px !important; |
|
|
border: 1px solid |
|
|
background: |
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05) !important; |
|
|
padding: 1rem 1.5rem !important; |
|
|
margin-bottom: 1rem !important; |
|
|
transition: box-shadow 0.2s, border-color 0.2s; |
|
|
} |
|
|
|
|
|
|
|
|
border-color: |
|
|
background-color: |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06) !important; |
|
|
} |
|
|
|
|
|
.loading-dots { |
|
|
display: inline-block; |
|
|
text-align: left; |
|
|
} |
|
|
.loading-dots span { |
|
|
display: inline-block; |
|
|
width: 8px; |
|
|
height: 8px; |
|
|
margin: 0 2px; |
|
|
background-color: |
|
|
border-radius: 50%; |
|
|
animation: blink 1.4s infinite both; |
|
|
} |
|
|
.loading-dots span:nth-child(2) { animation-delay: 0.2s; } |
|
|
.loading-dots span:nth-child(3) { animation-delay: 0.4s; } |
|
|
@keyframes blink { |
|
|
0%, 80%, 100% { opacity: 0; } |
|
|
40% { opacity: 1; } |
|
|
} |
|
|
|
|
|
|
|
|
.modal-content { |
|
|
background: |
|
|
color: |
|
|
border-radius: 12px !important; |
|
|
border: 1px solid |
|
|
} |
|
|
|
|
|
|
|
|
border-radius: 10px !important; |
|
|
border: 2px dashed |
|
|
background: |
|
|
color: |
|
|
transition: border-color 0.2s, color 0.2s; |
|
|
} |
|
|
|
|
|
|
|
|
border-color: |
|
|
color: |
|
|
} |
|
|
|
|
|
|
|
|
font-size: 1rem; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
@media (max-width: 900px) { |
|
|
.w-1\/4, .w-3\/4 { |
|
|
width: 100% !important; |
|
|
display: block !important; |
|
|
} |
|
|
.flex { |
|
|
flex-direction: column !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Header Styles */ |
|
|
.chat-header { |
|
|
background: |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05); |
|
|
padding: 1.25rem 2rem; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 2rem; |
|
|
border: 1px solid |
|
|
} |
|
|
.chat-header-title { |
|
|
font-size: 1.75rem; /* Matches h2 in your other HTML files */ |
|
|
background: linear-gradient(90deg, |
|
|
background-clip: text; |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
} |
|
|
.chat-header .no-style { |
|
|
background: none; |
|
|
background-clip: initial; |
|
|
-webkit-background-clip: initial; |
|
|
-webkit-text-fill-color: initial; |
|
|
color: inherit; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="font-sans"> |
|
|
|
|
|
<!-- Header --> |
|
|
<div class="chat-header container-title mb-4"> |
|
|
<div class="d-flex align-items-center gap-3"> |
|
|
<button |
|
|
onclick="window.location.href='/notebook/?effort_id={{ effort_id }}&user_id={{ user_id }}'" |
|
|
id = "backBtn" |
|
|
class="btn btn-outline-primary" |
|
|
aria-label="Go back" |
|
|
style="padding: 0.375rem 0.75rem;" |
|
|
> |
|
|
<i class="bi bi-arrow-left"></i> |
|
|
</button> |
|
|
<h2 class="fw-bold mb-0 chat-header-title"> |
|
|
<span class="no-style">📚</span> Document Chat & Summarization |
|
|
</h2> |
|
|
</div> |
|
|
<div class="position-relative" style="min-width: 14rem;"> |
|
|
<select id="zone-dropdown" |
|
|
class="form-control bg-white text-dark shadow-sm" |
|
|
style="padding-right: 2.5rem;"> |
|
|
</select> |
|
|
<span class="position-absolute top-50 end-0 translate-middle-y pe-3"> |
|
|
<svg class="w-4 h-4 text-secondary" fill="none" stroke="currentColor" stroke-width="2" |
|
|
viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" style="width: 1.25rem; height: 1.25rem;"> |
|
|
<polyline points="6 9 12 15 18 9"></polyline> |
|
|
</svg> |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Directory Path Display --> |
|
|
<div class="max-w-7xl mx-auto mt-6 px-4 text-gray-700 text-sm font-medium select-none"> |
|
|
<span id="effort-path" class="text-gray-800 font-semibold">{{ effort_name }}</span> |
|
|
<span class="mx-2 text-gray-500">→</span> |
|
|
<span id="notebook-path" class="text-gray-800 font-semibold">{{ notebook_name }}</span> |
|
|
<span class="mx-2 text-gray-500">→</span> |
|
|
<span id="file-path" class="text-blue-700 font-semibold">No File Selected</span> |
|
|
</div> |
|
|
|
|
|
<!-- Main Layout --> |
|
|
<div class="flex max-w-7xl mx-auto mt-6 gap-6 px-4"> |
|
|
<!-- LEFT: Sidebar --> |
|
|
<div class="w-1/4 bg-white rounded-2xl shadow-md p-4 h-[34rem] overflow-y-auto"> |
|
|
<button class="upload-button w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded mb-4" |
|
|
onclick="openModal()">Upload File(s)</button> |
|
|
<h3 class="text-lg font-semibold mb-2 text-gray-700">📂 Available Files</h3> |
|
|
<ul id="file-list" class="space-y-3 text-gray-700 text-sm"> |
|
|
<!-- Files injected here --> |
|
|
</ul> |
|
|
</div> |
|
|
|
|
|
<!-- RIGHT: Chat Panel --> |
|
|
<div class="w-3/4 bg-white rounded-2xl shadow-md p-6 flex flex-col"> |
|
|
<div id="chat-box" class="overflow-y-scroll border rounded-lg p-4 bg-gray-50 space-y-3 mb-4"> |
|
|
<!-- Chat messages will appear here --> |
|
|
</div> |
|
|
|
|
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 mt-auto"> |
|
|
<input id="persona" type="text" placeholder="Enter persona..." value="You are a helpful assistant" |
|
|
class="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" /> |
|
|
|
|
|
<input id="message" type="text" placeholder="Type your message..." |
|
|
class="flex-grow px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" /> |
|
|
|
|
|
<button onclick="sendMessage()" |
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition"> |
|
|
Send |
|
|
</button> |
|
|
|
|
|
<select id="summary-type" |
|
|
class="px-3 py-2 border rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-green-500"> |
|
|
<option value="brief">Brief</option> |
|
|
<option value="detailed">Detailed</option> |
|
|
</select> |
|
|
|
|
|
<button onclick="summarize()" |
|
|
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition"> |
|
|
Summarize |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Upload Modal --> |
|
|
<div id="uploadModal" class="modal fixed z-50 inset-0 hidden bg-black bg-opacity-50 flex justify-center items-center"> |
|
|
<div class="modal-content bg-white p-6 rounded-xl w-[90%] max-w-md relative"> |
|
|
<span class="close absolute top-3 right-4 text-xl cursor-pointer" onclick="closeModal()">×</span> |
|
|
<h2 class="text-xl font-bold text-gray-800">NotebookLM</h2> |
|
|
<p class="text-sm text-gray-600 mt-1">Add sources (PDF only)</p> |
|
|
<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" |
|
|
onclick="triggerFileInput()">Drag & drop or <u>choose files</u><br><small>Supported: PDF</small></div> |
|
|
<input type="file" id="fileInput" accept=".pdf" multiple class="hidden" /> |
|
|
<div id="selectedFilesList" class="mt-4 text-gray-700 text-sm space-y-1 max-h-40 overflow-y-auto"> |
|
|
<!-- File names shown here --> |
|
|
</div> |
|
|
<div class="flex justify-center mt-4"> |
|
|
<button class="submit-btn bg-blue-600 text-white px-4 py-2 rounded" onclick="submitFile()">Upload</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Delete File Confirmation Modal --> |
|
|
<div class="modal fade" id="deleteFileModal" tabindex="-1" aria-labelledby="deleteFileModalLabel" aria-hidden="true"> |
|
|
<div class="modal-dialog"> |
|
|
<div class="modal-content"> |
|
|
<div class="modal-header"> |
|
|
<h5 class="modal-title" id="deleteFileModalLabel">Confirm Delete</h5> |
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|
|
</div> |
|
|
<div class="modal-body"> |
|
|
Are you sure you want to delete <strong id="deleteFileName"></strong>? |
|
|
</div> |
|
|
<div class="modal-footer"> |
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
|
|
<button type="button" class="btn btn-danger" onclick="confirmDeleteFile()">Delete</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<!-- FEEDBACK MODAL (Bootstrap) --> |
|
|
<div class="modal fade" id="feedbackModal" tabindex="-1" aria-labelledby="feedbackModalLabel" aria-hidden="true"> |
|
|
<div class="modal-dialog" role="document"> |
|
|
<div class="modal-content" style="background-color: white; color: black;"> |
|
|
<div class="modal-header"> |
|
|
<h5 class="modal-title" id="feedbackModalLabel">Rate Your Experience</h5> |
|
|
<!-- Proper Bootstrap “close” button --> |
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|
|
</div> |
|
|
<div class="modal-body"> |
|
|
<label for="feedbackLevel">Rating (optional):</label> |
|
|
<select id="feedbackLevel" class="form-control"> |
|
|
<option value="">-- Select --</option> |
|
|
<option value="1">Very Bad</option> |
|
|
<option value="2">Bad</option> |
|
|
<option value="3">Neutral</option> |
|
|
<option value="4">Good</option> |
|
|
<option value="5">Very Good</option> |
|
|
</select> |
|
|
|
|
|
<label for="comment" class="mt-2">Comments (optional):</label> |
|
|
<textarea id="comment" class="form-control" rows="3" placeholder="Write your feedback here..."></textarea> |
|
|
</div id ="updateNotice" class = "text-secondary mt-2" style="display:none;font-size:0.9em;"> |
|
|
(Your Previously Submitted Feedback. You can update it needed.) |
|
|
</div> |
|
|
</div> |
|
|
<div class="modal-footer"> |
|
|
<button id="submitFeedback" class="btn btn-primary">Submit</button> |
|
|
<button id="skipFeedback" type="button" class="btn btn-secondary">Skip</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
const userId = "{{ user_id }}"; |
|
|
const effortId = "{{ effort_id }}"; |
|
|
const notebookId = "{{ notebook_id }}" |
|
|
const effortName = "{{ effort_name }}"; |
|
|
const notebookName = "{{ notebook_name}}" |
|
|
const preloadedFiles = {{ files | tojson }}; |
|
|
const preloadedZones = {{ zones | tojson }}; |
|
|
const globalZones = {{ global_ezones | tojson }}; |
|
|
const chatHistory = {{ chat_history | tojson }}; |
|
|
const chatBox = document.getElementById("chat-box"); |
|
|
const zoneDropdown = document.getElementById("zone-dropdown"); |
|
|
const fileList = document.getElementById("file-list"); |
|
|
const modal = document.getElementById("uploadModal"); |
|
|
const fileInput = document.getElementById("fileInput"); |
|
|
|
|
|
//Feedback modal ----- |
|
|
const backButton = document.getElementById("backBtn"); |
|
|
const feedbackModalEl = document.getElementById("feedbackModal"); |
|
|
const bootstrapFeedbackModal = new bootstrap.Modal(feedbackModalEl); |
|
|
|
|
|
fileInput.addEventListener("change", () => { |
|
|
const list = document.getElementById("selectedFilesList"); |
|
|
list.innerHTML = ""; |
|
|
|
|
|
const files = fileInput.files; |
|
|
if (!files.length) { |
|
|
list.textContent = "No files selected."; |
|
|
return; |
|
|
} |
|
|
|
|
|
for (let file of files) { |
|
|
const item = document.createElement("div"); |
|
|
item.className = "flex items-center gap-2"; |
|
|
item.innerHTML = `📄 <span class="truncate">${file.name}</span>`; |
|
|
list.appendChild(item); |
|
|
} |
|
|
}); |
|
|
|
|
|
let currentZoneId = null; |
|
|
let currentFileUri = ""; |
|
|
let selectedFileElement = null; |
|
|
|
|
|
function appendMessage(sender, message, color, alignRight = false) { |
|
|
const wrapper = document.createElement("div"); |
|
|
wrapper.className = alignRight ? "flex justify-end" : "flex justify-start"; |
|
|
|
|
|
const bubble = document.createElement("div"); |
|
|
bubble.className = `max-w-xl px-4 py-2 rounded-lg ${color} text-white`; |
|
|
|
|
|
// Use <pre> tag to preserve formatting in the message |
|
|
bubble.innerHTML = `<strong>${sender}:</strong><br><pre class="whitespace-pre-wrap break-words">${message}</pre>`; |
|
|
|
|
|
wrapper.appendChild(bubble); |
|
|
chatBox.appendChild(wrapper); |
|
|
chatBox.scrollTop = chatBox.scrollHeight; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function loadZones() { |
|
|
zoneDropdown.innerHTML = ""; |
|
|
const defaultOpt = document.createElement("option"); |
|
|
defaultOpt.disabled = true; |
|
|
defaultOpt.selected = true; |
|
|
defaultOpt.textContent = "Select a Configuration"; |
|
|
zoneDropdown.appendChild(defaultOpt); |
|
|
|
|
|
try { |
|
|
const addedZoneIds = new Set(); |
|
|
|
|
|
preloadedZones.forEach(zone => { |
|
|
if (!addedZoneIds.has(zone.id)) { |
|
|
const opt = document.createElement("option"); |
|
|
opt.value = zone.id; |
|
|
opt.textContent = zone.name; |
|
|
zoneDropdown.appendChild(opt); |
|
|
addedZoneIds.add(zone.id); |
|
|
} |
|
|
}); |
|
|
|
|
|
globalZones.forEach(zone => { |
|
|
if (!addedZoneIds.has(zone.id)) { |
|
|
const opt = document.createElement("option"); |
|
|
opt.value = zone.id; |
|
|
opt.textContent = zone.name + " (Global)"; |
|
|
zoneDropdown.appendChild(opt); |
|
|
addedZoneIds.add(zone.id); |
|
|
} |
|
|
}); |
|
|
|
|
|
const divider = document.createElement("option"); |
|
|
divider.disabled = true; |
|
|
divider.textContent = "──────────"; |
|
|
zoneDropdown.appendChild(divider); |
|
|
} catch (err) { |
|
|
console.error("Error loading zones:", err); |
|
|
} |
|
|
|
|
|
const manage = document.createElement("option"); |
|
|
manage.value = "manage"; |
|
|
manage.textContent = "➕ Manage Configurations"; |
|
|
zoneDropdown.appendChild(manage); |
|
|
|
|
|
zoneDropdown.addEventListener("change", (e) => { |
|
|
if (e.target.value === "manage") { |
|
|
const form = document.createElement("form"); |
|
|
form.method = "POST"; |
|
|
form.action = "/chat_feature/manage_zones"; |
|
|
|
|
|
const userInput = document.createElement("input"); |
|
|
userInput.type = "hidden"; |
|
|
userInput.name = "user_id"; |
|
|
userInput.value = userId; |
|
|
form.appendChild(userInput); |
|
|
|
|
|
const effortInput = document.createElement("input"); |
|
|
effortInput.type = "hidden"; |
|
|
effortInput.name = "effort_id"; |
|
|
effortInput.value = effortId; |
|
|
form.appendChild(effortInput); |
|
|
|
|
|
const notebookInput = document.createElement("input"); |
|
|
notebookInput.type = "hidden"; |
|
|
notebookInput.name = "notebook_id"; |
|
|
notebookInput.value = notebookId; |
|
|
form.appendChild(notebookInput); |
|
|
|
|
|
document.body.appendChild(form); |
|
|
form.submit(); |
|
|
} else { |
|
|
currentZoneId = parseInt(e.target.value); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
function loadFiles() { |
|
|
const files = preloadedFiles || []; |
|
|
fileList.innerHTML = ""; |
|
|
|
|
|
files.forEach(file => { |
|
|
const li = document.createElement("li"); |
|
|
li.className = |
|
|
"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"; |
|
|
|
|
|
const fileIcon = document.createElement("div"); |
|
|
fileIcon.className = "flex-shrink-0 text-blue-600 text-lg"; |
|
|
fileIcon.textContent = "📄"; |
|
|
|
|
|
const fileName = document.createElement("div"); |
|
|
fileName.className = "flex-1 min-w-0 cursor-pointer"; |
|
|
fileName.innerHTML = ` |
|
|
<p class="font-medium truncate group-hover:whitespace-normal group-hover:break-all" title="${file.name}"> |
|
|
${file.name} |
|
|
</p> |
|
|
`; |
|
|
fileName.onclick = () => { |
|
|
currentFileUri = file.uri; |
|
|
appendMessage("System", `Selected file: ${file.name}`, "bg-purple-600"); |
|
|
|
|
|
document.getElementById("notebook-path").textContent = notebookName; |
|
|
document.getElementById("effort-path").textContent = effortName; |
|
|
document.getElementById("file-path").textContent = file.name; |
|
|
|
|
|
if (selectedFileElement) { |
|
|
selectedFileElement.classList.remove("border-blue-500", "bg-blue-100", "ring", "ring-blue-300", "shadow-lg", "transform", "scale-105"); |
|
|
} |
|
|
|
|
|
li.classList.add("border-blue-500", "bg-blue-100", "ring", "ring-blue-300", "shadow-lg", "transform", "scale-105"); |
|
|
selectedFileElement = li; |
|
|
}; |
|
|
|
|
|
const deleteButton = document.createElement("button"); |
|
|
deleteButton.className = "btn btn-sm btn-outline-danger"; |
|
|
deleteButton.setAttribute("data-bs-toggle", "modal"); |
|
|
deleteButton.setAttribute("data-bs-target", "#deleteFileModal"); |
|
|
deleteButton.innerHTML = `<i class="bi bi-trash"></i>`; |
|
|
deleteButton.onclick = (e) => { |
|
|
e.stopPropagation(); |
|
|
setDeleteFile(file.uri, file.name); |
|
|
}; |
|
|
|
|
|
li.appendChild(fileIcon); |
|
|
li.appendChild(fileName); |
|
|
li.appendChild(deleteButton); |
|
|
|
|
|
fileList.appendChild(li); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async function sendMessage() { |
|
|
const message = document.getElementById("message").value; |
|
|
const persona = document.getElementById("persona").value; |
|
|
if (!message.trim() || !currentFileUri) { |
|
|
alert("Please select a file and type a message."); |
|
|
return; |
|
|
} |
|
|
if (!currentZoneId) { |
|
|
alert("Please select a zone before sending a message."); |
|
|
return; |
|
|
} |
|
|
|
|
|
appendMessage("You", message, "bg-blue-500", true); // Right aligned |
|
|
|
|
|
const loadingId = "loading-msg"; |
|
|
const loadingWrapper = document.createElement("div"); |
|
|
loadingWrapper.id = loadingId; |
|
|
loadingWrapper.className = "flex justify-start"; |
|
|
|
|
|
const loadingBubble = document.createElement("div"); |
|
|
loadingBubble.className = "max-w-xl px-4 py-2 rounded-lg bg-gray-300 text-gray-800"; |
|
|
loadingBubble.innerHTML = `<strong>Assistant:</strong> <span class="loading-dots"><span></span><span></span><span></span></span>`; |
|
|
loadingWrapper.appendChild(loadingBubble); |
|
|
chatBox.appendChild(loadingWrapper); |
|
|
chatBox.scrollTop = chatBox.scrollHeight; |
|
|
|
|
|
|
|
|
const res = await fetch("/chat_feature/send_message", { |
|
|
method: "POST", |
|
|
headers: { "Content-Type": "application/json" }, |
|
|
body: JSON.stringify({ |
|
|
message, |
|
|
persona, |
|
|
file_uri: currentFileUri, |
|
|
experiment_zone_id: currentZoneId, |
|
|
effortId: effortId, |
|
|
notebookId: notebookId |
|
|
}) |
|
|
}); |
|
|
|
|
|
const data = await res.json(); |
|
|
document.getElementById(loadingId)?.remove(); |
|
|
appendMessage("Assistant", data.message || "No response", "bg-gray-700"); |
|
|
document.getElementById("message").value = ""; |
|
|
} |
|
|
|
|
|
async function summarize() { |
|
|
const summaryType = document.getElementById("summary-type").value; |
|
|
if (!currentFileUri) { |
|
|
alert("Please select a file and summarize."); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!currentZoneId) { |
|
|
alert("Please select a zone before summarizing."); |
|
|
return; |
|
|
} |
|
|
|
|
|
appendMessage("You", `Summarize (${summaryType})`, "bg-green-500", true); // Right aligned |
|
|
|
|
|
const loadingId = "loading-msg"; |
|
|
const loadingWrapper = document.createElement("div"); |
|
|
loadingWrapper.id = loadingId; |
|
|
loadingWrapper.className = "flex justify-start"; |
|
|
|
|
|
const loadingBubble = document.createElement("div"); |
|
|
loadingBubble.className = "max-w-xl px-4 py-2 rounded-lg bg-gray-300 text-gray-800"; |
|
|
loadingBubble.innerHTML = `<strong>Assistant:</strong> <span class="loading-dots"><span></span><span></span><span></span></span>`; |
|
|
loadingWrapper.appendChild(loadingBubble); |
|
|
chatBox.appendChild(loadingWrapper); |
|
|
chatBox.scrollTop = chatBox.scrollHeight; |
|
|
|
|
|
|
|
|
const res = await fetch("/chat_feature/summarize", { |
|
|
method: "POST", |
|
|
headers: { "Content-Type": "application/json" }, |
|
|
body: JSON.stringify({ |
|
|
summary_type: summaryType, |
|
|
file_uri: currentFileUri, |
|
|
experiment_zone_id: currentZoneId, |
|
|
effortId: effortId, |
|
|
notebookId: notebookId |
|
|
}) |
|
|
}); |
|
|
|
|
|
const data = await res.json(); |
|
|
document.getElementById(loadingId)?.remove(); |
|
|
appendMessage("Summary", data.message || "No summary", "bg-gray-700"); |
|
|
} |
|
|
|
|
|
function openModal() { modal.classList.remove("hidden"); } |
|
|
function closeModal() { modal.classList.add("hidden"); fileInput.value = ""; } |
|
|
function triggerFileInput() { fileInput.click(); } |
|
|
|
|
|
async function submitFile() { |
|
|
const files = fileInput.files; |
|
|
if (!files.length) return alert("Choose at least one file."); |
|
|
|
|
|
const formData = new FormData(); |
|
|
for (let file of files) { |
|
|
if (file.type !== "application/pdf") { |
|
|
alert(`Invalid file type: ${file.name}`); |
|
|
return; |
|
|
} |
|
|
formData.append("files", file); |
|
|
} |
|
|
|
|
|
formData.append("effort_id", effortId); |
|
|
|
|
|
try { |
|
|
const res = await fetch("/chat_feature/upload_file", { |
|
|
method: "POST", |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
const contentType = res.headers.get("content-type") || ""; |
|
|
let responseData = contentType.includes("application/json") ? await res.json() : await res.text(); |
|
|
|
|
|
if (!res.ok) { |
|
|
const errorMessage = typeof responseData === "string" |
|
|
? responseData |
|
|
: (responseData.error || JSON.stringify(responseData)); |
|
|
alert(`Upload failed: ${errorMessage}`); |
|
|
return; |
|
|
} |
|
|
|
|
|
alert("Upload successful!"); |
|
|
location.reload(); // Refresh to get updated file list from server |
|
|
} catch (err) { |
|
|
alert("Error: " + err.message); |
|
|
} |
|
|
} |
|
|
|
|
|
window.onclick = function (event) { |
|
|
if (event.target == modal) closeModal(); |
|
|
} |
|
|
|
|
|
let deleteFileUriToConfirm = ""; |
|
|
let deleteFileName = "" |
|
|
|
|
|
function setDeleteFile(fileUri, fileName) { |
|
|
deleteFileUriToConfirm = fileUri; |
|
|
deleteFileName = fileName |
|
|
document.getElementById("deleteFileName").textContent = fileName; |
|
|
} |
|
|
|
|
|
async function confirmDeleteFile() { |
|
|
try { |
|
|
const res = await fetch("/chat_feature/delete_file", { |
|
|
method: "POST", |
|
|
headers: { "Content-Type": "application/json" }, |
|
|
body: JSON.stringify({ |
|
|
file_uri: deleteFileUriToConfirm, |
|
|
effort_id: effortId, |
|
|
filename: deleteFileName |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!res.ok) { |
|
|
const text = await res.text(); |
|
|
alert("Failed to delete: " + text); |
|
|
} else { |
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById("deleteFileModal")); |
|
|
modal.hide(); |
|
|
alert("File deleted."); |
|
|
location.reload(); |
|
|
} |
|
|
} catch (err) { |
|
|
alert("Error deleting file: " + err.message); |
|
|
} |
|
|
} |
|
|
|
|
|
const dropZone = document.querySelector(".drop-zone"); |
|
|
|
|
|
dropZone.addEventListener("dragover", (e) => { |
|
|
e.preventDefault(); |
|
|
dropZone.classList.add("border-blue-500", "text-blue-500"); |
|
|
}); |
|
|
|
|
|
dropZone.addEventListener("dragleave", () => { |
|
|
dropZone.classList.remove("border-blue-500", "text-blue-500"); |
|
|
}); |
|
|
|
|
|
dropZone.addEventListener("drop", (e) => { |
|
|
e.preventDefault(); |
|
|
dropZone.classList.remove("border-blue-500", "text-blue-500"); |
|
|
|
|
|
const files = e.dataTransfer.files; |
|
|
fileInput.files = files; |
|
|
|
|
|
// Optional: Display file names immediately |
|
|
const list = document.getElementById("selectedFilesList"); |
|
|
list.innerHTML = ""; |
|
|
for (const file of files) { |
|
|
const p = document.createElement("p"); |
|
|
p.textContent = file.name; |
|
|
list.appendChild(p); |
|
|
} |
|
|
}); |
|
|
|
|
|
window.onload = function () { |
|
|
|
|
|
loadFiles(); |
|
|
loadZones(); |
|
|
|
|
|
let historyFilename = "" |
|
|
|
|
|
if (chatHistory && chatHistory.length > 0) { |
|
|
chatHistory.forEach(entry => { |
|
|
|
|
|
if (entry.filename && (entry.filename !== historyFilename || historyFilename === "")) { |
|
|
historyFilename = entry.filename; |
|
|
appendMessage("System", `Selected file: ${historyFilename}`, "bg-purple-600"); |
|
|
} |
|
|
|
|
|
// User message |
|
|
const userMessage = entry.question; |
|
|
appendMessage("You", userMessage, "bg-blue-500", true); |
|
|
const personaMessage = entry.persona; |
|
|
appendMessage("Persona", personaMessage, "bg-blue-500", true); |
|
|
// Assistant response |
|
|
const botMessage = entry.response; |
|
|
appendMessage("Assistant", botMessage, "bg-gray-700", false); |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
//feedback Modal POP-UP on back Button |
|
|
backButton.addEventListener("click", async function (event) { |
|
|
event.preventDefault(); |
|
|
|
|
|
if(!currentZoneId){ |
|
|
console.log("no configuration selected, going back directly") |
|
|
window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("Debug BackBtn:", {userId, notebookId, currentZoneId, effortId}); |
|
|
try { |
|
|
const response = await fetch(`/chat_feature/user_feedback_eligibility?user_id=${userId}¬ebook_id=${notebookId}&zone_id=${currentZoneId}`); |
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.eligible_for_feedback) { |
|
|
if(data.has_feedback){ |
|
|
document.getElementById("feedbackLevel").value = data.rating ||""; |
|
|
document.getElementById("comment").value = data.comment ||""; |
|
|
document.getElementById("updateNotice").style.display = "block"; |
|
|
|
|
|
} |
|
|
else{ |
|
|
document.getElementById("feedbackLevel").value = data.rating ||""; |
|
|
document.getElementById("comment").value = data.comment ||""; |
|
|
document.getElementById("updateNotice").style.display = "None"; |
|
|
|
|
|
} |
|
|
bootstrapFeedbackModal.show(); |
|
|
} else { |
|
|
window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error checking eligibility:', error); |
|
|
window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; |
|
|
} |
|
|
}); |
|
|
// Feedback Submit |
|
|
document.getElementById("submitFeedback").addEventListener("click", async function () { |
|
|
const rating = document.getElementById("feedbackLevel").value; |
|
|
const comment = document.getElementById("comment").value; |
|
|
|
|
|
const payload = { |
|
|
user_id: userId, |
|
|
notebook_id: parseInt(notebookId), |
|
|
effort_id: parseInt(effortId), |
|
|
zone_id: parseInt(currentZoneId), |
|
|
rating: rating ? parseInt(rating) : null, |
|
|
comment: comment |
|
|
}; |
|
|
|
|
|
try { |
|
|
const response = await fetch("/feedback", { |
|
|
method: "POST", |
|
|
headers: { "Content-Type": "application/json" }, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
bootstrapFeedbackModal.hide(); |
|
|
window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; |
|
|
} else { |
|
|
alert("Error submitting feedback"); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Error submitting feedback:", error); |
|
|
alert("Error submitting feedback"); |
|
|
} |
|
|
}); |
|
|
|
|
|
// Skip feedback |
|
|
document.getElementById("skipFeedback").addEventListener("click", function () { |
|
|
bootstrapFeedbackModal.hide(); |
|
|
window.location.href = `/notebook/?effort_id=${effortId}&user_id=${userId}`; |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |