Static_H / abc12
MissSqui's picture
Update abc12
cc2dcdf verified
Raw
History Blame Contribute Delete
51.7 kB
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 environment variables from the .env file
load_dotenv()
#logger = getLogger(__name__)
# Pydantic model
class UserCreate(BaseModel):
user_id: str
password: str
ad_groups: List[str]
is_admin: bool = False
# Database connection function
def get_db_connection():
try:
# Create the connection
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
# Validate user ID
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")
# Validate notebook ID
def validate_notebook_id(notebook_id: str):
if not notebook_id.isdigit():
raise HTTPException(status_code=406, detail="Invalid notebook ID format")
# Create a new user in the DB
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}")
# Update an existing user in the DB
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
# delete a user in the DB
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
# Fetch user details
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()
# Fetch notebook details
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()
# Effort Management
# Create effort
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()
# Delete effort
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()
# Modify effort
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()
# List effort details
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()
# Zone Management
# Delete experiment zone
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()
# Fetch zone details
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()
# Create experiment zone
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()
# Modify experiment zone
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: #f5f8fc;
color: #1a1a1a;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding-bottom: 4rem;
}
.bg-gradient-to-r {
background: linear-gradient(90deg, #4f46e5, #3b82f6) !important;
}
.text-white {
color: #fff !important;
}
.card.card-item,
.w-1\/4.bg-white,
.w-3\/4.bg-white {
background-color: #ffffff !important;
border: 1px solid #d0d7e2 !important;
color: #1a1a1a;
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;
}
#chat-box {
background: #f8f9fc !important;
border: 1px solid #d0d7e2 !important;
border-radius: 10px !important;
color: #1a1a1a;
height: 26rem;
}
input.form-control, .form-control, select.form-control,
#persona, #message, #summary-type {
background: #f8f9fc !important;
color: #1a1a1a !important;
border: 1px solid #d0d7e2 !important;
border-radius: 6px !important;
}
input.form-control::placeholder, .form-control::placeholder,
#persona::placeholder, #message::placeholder {
color: #9aa5bc !important;
}
.btn-gradient, .upload-button, .submit-btn, .bg-blue-600, .bg-green-600 {
background: linear-gradient(90deg, #4f46e5, #3b82f6) !important;
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, #4338ca, #2563eb) !important;
}
.btn-danger, .btn-outline-danger {
background-color: #d9534f !important;
color: white !important;
border: none !important;
border-radius: 8px !important;
font-weight: 600;
}
.btn-danger:hover, .btn-outline-danger:hover {
background-color: #c9302c !important;
}
#file-list > li {
border-radius: 10px !important;
border: 1px solid #d0d7e2 !important;
background: #fff !important;
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;
}
#file-list > li:hover {
border-color: #a0b4e3 !important;
background-color: #f0f4ff !important;
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: #555;
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; }
}
#uploadModal .modal-content,
.modal-content {
background: #ffffff !important;
color: #1a1a1a;
border-radius: 12px !important;
border: 1px solid #d0d7e2 !important;
}
#uploadModal .drop-zone {
border-radius: 10px !important;
border: 2px dashed #d0d7e2 !important;
background: #f8f9fc !important;
color: #9aa5bc !important;
transition: border-color 0.2s, color 0.2s;
}
#uploadModal .drop-zone:hover, #uploadModal .drop-zone.border-blue-500 {
border-color: #4f46e5 !important;
color: #4f46e5 !important;
}
#effort-path, #notebook-path, #file-path {
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: #fff;
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 #d0d7e2;
}
.chat-header-title {
font-size: 1.75rem; /* Matches h2 in your other HTML files */
background: linear-gradient(90deg, #4f46e5, #3b82f6);
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()">&times;</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}&notebook_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>