Spaces:
Running
Running
NEW UI and UPdated RAG modelx
Browse files- rag/chunker.py +0 -87
- rag/embeddings.py +0 -98
- rag/generator.py +0 -75
- rag/retriever.py +0 -71
- static/profile_pics/param20h.jpeg +0 -3
- static/script.js +0 -17
- static/style.css +0 -614
- templates/admin.html +0 -106
- templates/chat.html +0 -162
- templates/index.html +0 -243
- templates/login.html +0 -52
- templates/profile.html +0 -208
- templates/register.html +0 -41
rag/chunker.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
import fitz # PyMuPDF
|
| 2 |
-
import docx
|
| 3 |
-
from config import CHUNK_SIZE, CHUNK_OVERLAP
|
| 4 |
-
|
| 5 |
-
# ── Extract Text from PDF ────────────────────────────
|
| 6 |
-
def load_pdf(filepath):
|
| 7 |
-
doc = fitz.open(filepath)
|
| 8 |
-
text_pages = []
|
| 9 |
-
|
| 10 |
-
for page_num, page in enumerate(doc):
|
| 11 |
-
text = page.get_text()
|
| 12 |
-
text_pages.append({
|
| 13 |
-
"text": text,
|
| 14 |
-
"page": page_num + 1
|
| 15 |
-
})
|
| 16 |
-
|
| 17 |
-
doc.close()
|
| 18 |
-
return text_pages
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
# ── Extract Text from DOCX ───────────────────────────
|
| 22 |
-
def load_docx(filepath):
|
| 23 |
-
doc = docx.Document(filepath)
|
| 24 |
-
text_pages = []
|
| 25 |
-
full_text = ""
|
| 26 |
-
|
| 27 |
-
for para in doc.paragraphs:
|
| 28 |
-
full_text += para.text + "\n"
|
| 29 |
-
|
| 30 |
-
text_pages.append({
|
| 31 |
-
"text": full_text,
|
| 32 |
-
"page": 1
|
| 33 |
-
})
|
| 34 |
-
|
| 35 |
-
return text_pages
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# ── Extract Text from TXT ────────────────────────────
|
| 39 |
-
def load_txt(filepath):
|
| 40 |
-
with open(filepath, "r", encoding="utf-8") as f:
|
| 41 |
-
text = f.read()
|
| 42 |
-
|
| 43 |
-
return [{
|
| 44 |
-
"text": text,
|
| 45 |
-
"page": 1
|
| 46 |
-
}]
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
# ── Split Text into Chunks ───────────────────────────
|
| 50 |
-
def split_text(text, page_num):
|
| 51 |
-
chunks = []
|
| 52 |
-
start = 0
|
| 53 |
-
|
| 54 |
-
while start < len(text):
|
| 55 |
-
end = start + CHUNK_SIZE
|
| 56 |
-
chunk = text[start:end]
|
| 57 |
-
|
| 58 |
-
if chunk.strip():
|
| 59 |
-
chunks.append({
|
| 60 |
-
"text": chunk,
|
| 61 |
-
"page": page_num
|
| 62 |
-
})
|
| 63 |
-
|
| 64 |
-
start = end - CHUNK_OVERLAP
|
| 65 |
-
|
| 66 |
-
return chunks
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
# ── Main Function ────────────────────────────────────
|
| 70 |
-
def load_and_chunk(filepath):
|
| 71 |
-
ext = filepath.rsplit(".", 1)[1].lower()
|
| 72 |
-
|
| 73 |
-
if ext == "pdf":
|
| 74 |
-
pages = load_pdf(filepath)
|
| 75 |
-
elif ext == "docx":
|
| 76 |
-
pages = load_docx(filepath)
|
| 77 |
-
elif ext in ["txt", "md"]:
|
| 78 |
-
pages = load_txt(filepath)
|
| 79 |
-
else:
|
| 80 |
-
raise ValueError(f"Unsupported file type: {ext}")
|
| 81 |
-
|
| 82 |
-
all_chunks = []
|
| 83 |
-
for page in pages:
|
| 84 |
-
chunks = split_text(page["text"], page["page"])
|
| 85 |
-
all_chunks.extend(chunks)
|
| 86 |
-
|
| 87 |
-
return all_chunks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rag/embeddings.py
DELETED
|
@@ -1,98 +0,0 @@
|
|
| 1 |
-
from google import genai
|
| 2 |
-
from google.genai import types
|
| 3 |
-
from pinecone import Pinecone
|
| 4 |
-
from config import CHUNK_SIZE, CHUNK_OVERLAP
|
| 5 |
-
|
| 6 |
-
# ── Gemini Embedding ─────────────────────────────────
|
| 7 |
-
def embed_text(text, gemini_key):
|
| 8 |
-
"""Generate embedding using Gemini's free gemini-embedding-001 model."""
|
| 9 |
-
client = genai.Client(api_key=gemini_key)
|
| 10 |
-
result = client.models.embed_content(
|
| 11 |
-
model="gemini-embedding-001",
|
| 12 |
-
contents=text
|
| 13 |
-
)
|
| 14 |
-
return result.embeddings[0].values # 3072-dimensional vector
|
| 15 |
-
|
| 16 |
-
# ── Get Pinecone Index ───────────────────────────────
|
| 17 |
-
def get_pinecone_index(pinecone_key, index_name):
|
| 18 |
-
"""Connect to user's Pinecone index."""
|
| 19 |
-
pc = Pinecone(api_key=pinecone_key)
|
| 20 |
-
return pc.Index(index_name)
|
| 21 |
-
|
| 22 |
-
# ── Store Embeddings in Pinecone ─────────────────────
|
| 23 |
-
def store_embeddings(chunks, filename, user):
|
| 24 |
-
"""Embed chunks using Gemini and upsert into user's Pinecone index."""
|
| 25 |
-
gemini_key = user.get_gemini_key()
|
| 26 |
-
pinecone_key = user.get_pinecone_key()
|
| 27 |
-
index_name = user.pinecone_index_name
|
| 28 |
-
|
| 29 |
-
if not gemini_key:
|
| 30 |
-
raise ValueError("Gemini API key is required for embeddings. Please add it in your Profile.")
|
| 31 |
-
if not pinecone_key or not index_name:
|
| 32 |
-
raise ValueError("Pinecone API key and index name are required. Please add them in your Profile.")
|
| 33 |
-
|
| 34 |
-
index = get_pinecone_index(pinecone_key, index_name)
|
| 35 |
-
namespace = user.username
|
| 36 |
-
|
| 37 |
-
# Batch upsert vectors
|
| 38 |
-
batch_size = 50
|
| 39 |
-
for i in range(0, len(chunks), batch_size):
|
| 40 |
-
batch = chunks[i:i + batch_size]
|
| 41 |
-
vectors = []
|
| 42 |
-
|
| 43 |
-
for j, chunk in enumerate(batch):
|
| 44 |
-
embedding = embed_text(chunk["text"], gemini_key)
|
| 45 |
-
vector_id = f"{filename}_{i + j}"
|
| 46 |
-
|
| 47 |
-
vectors.append({
|
| 48 |
-
"id": vector_id,
|
| 49 |
-
"values": embedding,
|
| 50 |
-
"metadata": {
|
| 51 |
-
"text": chunk["text"],
|
| 52 |
-
"filename": filename,
|
| 53 |
-
"page": chunk["page"],
|
| 54 |
-
"chunk_index": i + j
|
| 55 |
-
}
|
| 56 |
-
})
|
| 57 |
-
|
| 58 |
-
index.upsert(vectors=vectors, namespace=namespace)
|
| 59 |
-
|
| 60 |
-
# ── Delete Vectors by Filename ───────────────────────
|
| 61 |
-
def delete_embeddings(filename, user):
|
| 62 |
-
"""Delete all vectors for a specific file from user's Pinecone index."""
|
| 63 |
-
pinecone_key = user.get_pinecone_key()
|
| 64 |
-
index_name = user.pinecone_index_name
|
| 65 |
-
|
| 66 |
-
if not pinecone_key or not index_name:
|
| 67 |
-
return
|
| 68 |
-
|
| 69 |
-
index = get_pinecone_index(pinecone_key, index_name)
|
| 70 |
-
namespace = user.username
|
| 71 |
-
|
| 72 |
-
try:
|
| 73 |
-
dummy_vector = [0.0] * 3072
|
| 74 |
-
results = index.query(
|
| 75 |
-
vector=dummy_vector,
|
| 76 |
-
top_k=10000,
|
| 77 |
-
namespace=namespace,
|
| 78 |
-
filter={"filename": {"$eq": filename}},
|
| 79 |
-
include_metadata=False
|
| 80 |
-
)
|
| 81 |
-
|
| 82 |
-
if results.matches:
|
| 83 |
-
ids_to_delete = [match.id for match in results.matches]
|
| 84 |
-
index.delete(ids=ids_to_delete, namespace=namespace)
|
| 85 |
-
except Exception as e:
|
| 86 |
-
print(f"Error deleting embeddings: {e}")
|
| 87 |
-
|
| 88 |
-
# ── Clear All Vectors for User ───────────────────────
|
| 89 |
-
def clear_all_embeddings(user):
|
| 90 |
-
"""Delete all vectors in user's namespace."""
|
| 91 |
-
pinecone_key = user.get_pinecone_key()
|
| 92 |
-
index_name = user.pinecone_index_name
|
| 93 |
-
|
| 94 |
-
if not pinecone_key or not index_name:
|
| 95 |
-
return
|
| 96 |
-
|
| 97 |
-
index = get_pinecone_index(pinecone_key, index_name)
|
| 98 |
-
index.delete(delete_all=True, namespace=user.username)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rag/generator.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from google import genai
|
| 3 |
-
from google.genai import types
|
| 4 |
-
from dotenv import load_dotenv
|
| 5 |
-
|
| 6 |
-
load_dotenv()
|
| 7 |
-
|
| 8 |
-
from groq import Groq
|
| 9 |
-
from config import GROQ_MODEL
|
| 10 |
-
|
| 11 |
-
def get_groq_client(user_key):
|
| 12 |
-
if not user_key:
|
| 13 |
-
raise ValueError("No Groq API Key available. Please add it to your profile.")
|
| 14 |
-
return Groq(api_key=user_key)
|
| 15 |
-
|
| 16 |
-
def generate_answer(question, context_chunks, user=None):
|
| 17 |
-
try:
|
| 18 |
-
if not context_chunks:
|
| 19 |
-
context = "No specific document context found for this query."
|
| 20 |
-
else:
|
| 21 |
-
context = "\n\n".join([
|
| 22 |
-
f"📄 File: {chunk['filename']} | Page: {chunk['page']}\n{chunk['text']}"
|
| 23 |
-
for chunk in context_chunks
|
| 24 |
-
])
|
| 25 |
-
|
| 26 |
-
prompt = f"""You are a helpful AI assistant. Answer the user's question based on the provided document context.
|
| 27 |
-
|
| 28 |
-
Document Context:
|
| 29 |
-
{context}
|
| 30 |
-
|
| 31 |
-
User Question: {question}
|
| 32 |
-
|
| 33 |
-
Instructions:
|
| 34 |
-
- If the question is a greeting or general chat (like "hi" or "how are you"), just reply naturally and explain you are here to help with their PDF documents.
|
| 35 |
-
- If the question is about the document, use the provided context to answer.
|
| 36 |
-
- If the context doesn't contain the answer, just say you couldn't find it in the document.
|
| 37 |
-
- Try to be clear, helpful, and concise.
|
| 38 |
-
|
| 39 |
-
Answer:"""
|
| 40 |
-
|
| 41 |
-
pref_model = user.preferred_model if user else "groq"
|
| 42 |
-
|
| 43 |
-
if pref_model == "gemini":
|
| 44 |
-
key = user.get_gemini_key() if user else None
|
| 45 |
-
if not key:
|
| 46 |
-
return "❌ No Gemini API key available. Please add it in your Profile settings."
|
| 47 |
-
|
| 48 |
-
client = genai.Client(api_key=key, http_options=types.HttpOptions(api_version="v1"))
|
| 49 |
-
response = client.models.generate_content(
|
| 50 |
-
model="gemini-2.0-flash",
|
| 51 |
-
contents=prompt
|
| 52 |
-
)
|
| 53 |
-
return response.text
|
| 54 |
-
else:
|
| 55 |
-
key = user.get_groq_key() if user else None
|
| 56 |
-
client = get_groq_client(key)
|
| 57 |
-
response = client.chat.completions.create(
|
| 58 |
-
model=GROQ_MODEL,
|
| 59 |
-
messages=[
|
| 60 |
-
{
|
| 61 |
-
"role": "system",
|
| 62 |
-
"content": "You are a helpful document assistant."
|
| 63 |
-
},
|
| 64 |
-
{
|
| 65 |
-
"role": "user",
|
| 66 |
-
"content": prompt
|
| 67 |
-
}
|
| 68 |
-
],
|
| 69 |
-
temperature=0.3,
|
| 70 |
-
max_tokens=1024,
|
| 71 |
-
)
|
| 72 |
-
return response.choices[0].message.content
|
| 73 |
-
|
| 74 |
-
except Exception as e:
|
| 75 |
-
return f"❌ Error generating answer: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rag/retriever.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
| 1 |
-
from google import genai
|
| 2 |
-
from google.genai import types
|
| 3 |
-
from pinecone import Pinecone
|
| 4 |
-
from config import TOP_K
|
| 5 |
-
|
| 6 |
-
# ── Gemini Embedding ─────────────────────────────────
|
| 7 |
-
def embed_query(query, gemini_key):
|
| 8 |
-
"""Generate query embedding using Gemini's gemini-embedding-001."""
|
| 9 |
-
client = genai.Client(api_key=gemini_key)
|
| 10 |
-
result = client.models.embed_content(
|
| 11 |
-
model="gemini-embedding-001",
|
| 12 |
-
contents=query
|
| 13 |
-
)
|
| 14 |
-
return result.embeddings[0].values
|
| 15 |
-
|
| 16 |
-
# ── Retrieve Chunks from Pinecone ────────────────────
|
| 17 |
-
def retrieve_chunks(query, filename=None, user=None):
|
| 18 |
-
"""Query user's Pinecone index for relevant chunks."""
|
| 19 |
-
if not user:
|
| 20 |
-
return []
|
| 21 |
-
|
| 22 |
-
gemini_key = user.get_gemini_key()
|
| 23 |
-
pinecone_key = user.get_pinecone_key()
|
| 24 |
-
index_name = user.pinecone_index_name
|
| 25 |
-
|
| 26 |
-
if not gemini_key or not pinecone_key or not index_name:
|
| 27 |
-
return []
|
| 28 |
-
|
| 29 |
-
try:
|
| 30 |
-
# Generate query embedding
|
| 31 |
-
query_embedding = embed_query(query, gemini_key)
|
| 32 |
-
|
| 33 |
-
# Connect to Pinecone
|
| 34 |
-
pc = Pinecone(api_key=pinecone_key)
|
| 35 |
-
index = pc.Index(index_name)
|
| 36 |
-
|
| 37 |
-
# Build metadata filter
|
| 38 |
-
filter_dict = None
|
| 39 |
-
if filename:
|
| 40 |
-
filter_dict = {"filename": {"$eq": filename}}
|
| 41 |
-
|
| 42 |
-
# Query Pinecone
|
| 43 |
-
results = index.query(
|
| 44 |
-
vector=query_embedding,
|
| 45 |
-
top_k=TOP_K,
|
| 46 |
-
namespace=user.username,
|
| 47 |
-
filter=filter_dict,
|
| 48 |
-
include_metadata=True
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
# Format results
|
| 52 |
-
chunks = []
|
| 53 |
-
if results.matches:
|
| 54 |
-
max_score = max(m.score for m in results.matches) if results.matches else 1
|
| 55 |
-
|
| 56 |
-
for match in results.matches:
|
| 57 |
-
confidence = round((match.score / max_score) * 100, 2) if max_score > 0 else 0
|
| 58 |
-
|
| 59 |
-
chunks.append({
|
| 60 |
-
"text": match.metadata.get("text", ""),
|
| 61 |
-
"filename": match.metadata.get("filename", ""),
|
| 62 |
-
"page": match.metadata.get("page", 1),
|
| 63 |
-
"score": round(match.score, 4),
|
| 64 |
-
"confidence": confidence
|
| 65 |
-
})
|
| 66 |
-
|
| 67 |
-
return chunks
|
| 68 |
-
|
| 69 |
-
except Exception as e:
|
| 70 |
-
print(f"Retrieval error: {e}")
|
| 71 |
-
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/profile_pics/param20h.jpeg
DELETED
Git LFS Details
|
static/script.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
// Check for saved user preference, if any, on load of the website
|
| 2 |
-
document.addEventListener("DOMContentLoaded", () => {
|
| 3 |
-
const currentTheme = localStorage.getItem("theme");
|
| 4 |
-
if (currentTheme === "light") {
|
| 5 |
-
document.body.classList.add("light-mode");
|
| 6 |
-
}
|
| 7 |
-
});
|
| 8 |
-
|
| 9 |
-
// Expose toggle function to global scope
|
| 10 |
-
function toggleLightMode() {
|
| 11 |
-
document.body.classList.toggle("light-mode");
|
| 12 |
-
let theme = "dark";
|
| 13 |
-
if (document.body.classList.contains("light-mode")) {
|
| 14 |
-
theme = "light";
|
| 15 |
-
}
|
| 16 |
-
localStorage.setItem("theme", theme);
|
| 17 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/style.css
DELETED
|
@@ -1,614 +0,0 @@
|
|
| 1 |
-
/* ── Google Fonts ───────────────────────────────────── */
|
| 2 |
-
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
|
| 3 |
-
|
| 4 |
-
/* ── Global Variables ───────────────────────────────── */
|
| 5 |
-
:root {
|
| 6 |
-
--bg-gradient: #09090b;
|
| 7 |
-
--glass-bg: rgba(255, 255, 255, 0.02);
|
| 8 |
-
--glass-border: rgba(255, 255, 255, 0.08);
|
| 9 |
-
--glass-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
| 10 |
-
|
| 11 |
-
--primary-color: #fafafa;
|
| 12 |
-
--primary-hover: #e4e4e7;
|
| 13 |
-
--primary-text: #09090b;
|
| 14 |
-
--accent-color: #3b82f6;
|
| 15 |
-
--danger-color: #ef4444;
|
| 16 |
-
--danger-hover: #dc2626;
|
| 17 |
-
--success-color: #10b981;
|
| 18 |
-
|
| 19 |
-
--text-main: #fafafa;
|
| 20 |
-
--text-muted: #a1a1aa;
|
| 21 |
-
|
| 22 |
-
--transition-speed: 0.25s;
|
| 23 |
-
|
| 24 |
-
--header-bg: rgba(9, 9, 11, 0.85);
|
| 25 |
-
--input-bg: rgba(255, 255, 255, 0.03);
|
| 26 |
-
--item-hover-bg: rgba(255, 255, 255, 0.05);
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
body.light-mode {
|
| 30 |
-
--bg-gradient: #ffffff;
|
| 31 |
-
--glass-bg: #ffffff;
|
| 32 |
-
--glass-border: rgba(0, 0, 0, 0.1);
|
| 33 |
-
--glass-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
|
| 34 |
-
|
| 35 |
-
--primary-color: #18181b;
|
| 36 |
-
--primary-hover: #27272a;
|
| 37 |
-
--primary-text: #fafafa;
|
| 38 |
-
|
| 39 |
-
--text-main: #18181b;
|
| 40 |
-
--text-muted: #71717a;
|
| 41 |
-
|
| 42 |
-
--header-bg: rgba(255, 255, 255, 0.85);
|
| 43 |
-
--input-bg: #fdfdfd;
|
| 44 |
-
--item-hover-bg: #f4f4f5;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
/* ── Global Styles ──────────────────────────────────── */
|
| 48 |
-
* {
|
| 49 |
-
margin: 0;
|
| 50 |
-
padding: 0;
|
| 51 |
-
box-sizing: border-box;
|
| 52 |
-
font-family: 'Outfit', sans-serif;
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
body {
|
| 56 |
-
background: var(--bg-gradient);
|
| 57 |
-
color: var(--text-main);
|
| 58 |
-
min-height: 100vh;
|
| 59 |
-
display: flex;
|
| 60 |
-
flex-direction: column;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
/* ── Header ─────────────────────────────────────────── */
|
| 64 |
-
header {
|
| 65 |
-
display: flex;
|
| 66 |
-
justify-content: space-between;
|
| 67 |
-
align-items: center;
|
| 68 |
-
padding: 15px 40px;
|
| 69 |
-
background: var(--header-bg);
|
| 70 |
-
backdrop-filter: blur(12px);
|
| 71 |
-
border-bottom: 1px solid var(--glass-border);
|
| 72 |
-
box-shadow: var(--glass-shadow);
|
| 73 |
-
position: sticky;
|
| 74 |
-
top: 0;
|
| 75 |
-
z-index: 100;
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
header h1 {
|
| 79 |
-
font-size: 24px;
|
| 80 |
-
font-weight: 600;
|
| 81 |
-
margin: 0;
|
| 82 |
-
background: linear-gradient(to right, #8b5cf6, #0ea5e9);
|
| 83 |
-
-webkit-background-clip: text;
|
| 84 |
-
background-clip: text;
|
| 85 |
-
-webkit-text-fill-color: transparent;
|
| 86 |
-
display: flex;
|
| 87 |
-
align-items: center;
|
| 88 |
-
gap: 8px;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
header a {
|
| 92 |
-
color: var(--text-muted);
|
| 93 |
-
text-decoration: none;
|
| 94 |
-
font-size: 15px;
|
| 95 |
-
font-weight: 500;
|
| 96 |
-
transition: color var(--transition-speed) ease;
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
header a:hover {
|
| 100 |
-
color: var(--text-main);
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
.header-right {
|
| 104 |
-
display: flex;
|
| 105 |
-
align-items: center;
|
| 106 |
-
gap: 16px;
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
/* ── Container ──────────────────────────────────────── */
|
| 110 |
-
.container {
|
| 111 |
-
max-width: 850px;
|
| 112 |
-
width: 100%;
|
| 113 |
-
margin: 40px auto;
|
| 114 |
-
padding: 0 20px;
|
| 115 |
-
flex-grow: 1;
|
| 116 |
-
animation: fadeIn 0.6s ease-out;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
@keyframes fadeIn {
|
| 120 |
-
from {
|
| 121 |
-
opacity: 0;
|
| 122 |
-
transform: translateY(10px);
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
to {
|
| 126 |
-
opacity: 1;
|
| 127 |
-
transform: translateY(0);
|
| 128 |
-
}
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
-
/* ── Glassmorphism Cards ────────────────────────────── */
|
| 132 |
-
.upload-box,
|
| 133 |
-
.files-box,
|
| 134 |
-
.clear-box,
|
| 135 |
-
.auth-box,
|
| 136 |
-
.chat-box {
|
| 137 |
-
background: var(--glass-bg);
|
| 138 |
-
backdrop-filter: blur(16px);
|
| 139 |
-
-webkit-backdrop-filter: blur(16px);
|
| 140 |
-
border: 1px solid var(--glass-border);
|
| 141 |
-
border-radius: 16px;
|
| 142 |
-
padding: 30px;
|
| 143 |
-
box-shadow: var(--glass-shadow);
|
| 144 |
-
margin-bottom: 25px;
|
| 145 |
-
transition: transform var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
.upload-box:hover,
|
| 149 |
-
.files-box:hover,
|
| 150 |
-
.clear-box:hover {
|
| 151 |
-
transform: translateY(-2px);
|
| 152 |
-
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.45);
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
.upload-box h2,
|
| 156 |
-
.files-box h2,
|
| 157 |
-
.clear-box h2,
|
| 158 |
-
.auth-box h2 {
|
| 159 |
-
margin-bottom: 20px;
|
| 160 |
-
color: var(--text-main);
|
| 161 |
-
font-weight: 600;
|
| 162 |
-
font-size: 20px;
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
/* ── Form Inputs & Uploads ──────────────────────────── */
|
| 166 |
-
input[type="file"] {
|
| 167 |
-
display: block;
|
| 168 |
-
width: 100%;
|
| 169 |
-
margin: 0 auto 20px auto;
|
| 170 |
-
padding: 12px;
|
| 171 |
-
background: var(--input-bg);
|
| 172 |
-
border: 1px dashed var(--glass-border);
|
| 173 |
-
border-radius: 8px;
|
| 174 |
-
color: var(--text-muted);
|
| 175 |
-
cursor: pointer;
|
| 176 |
-
transition: all var(--transition-speed);
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
input[type="file"]:hover {
|
| 180 |
-
border-color: var(--primary-color);
|
| 181 |
-
background: var(--item-hover-bg);
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
input[type="text"],
|
| 185 |
-
input[type="password"],
|
| 186 |
-
input[type="email"] {
|
| 187 |
-
width: 100%;
|
| 188 |
-
padding: 14px 16px;
|
| 189 |
-
margin-bottom: 16px;
|
| 190 |
-
background: var(--input-bg);
|
| 191 |
-
border: 1px solid var(--glass-border);
|
| 192 |
-
border-radius: 10px;
|
| 193 |
-
color: var(--text-main);
|
| 194 |
-
font-size: 15px;
|
| 195 |
-
outline: none;
|
| 196 |
-
transition: all var(--transition-speed) ease;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
input[type="text"]:focus,
|
| 200 |
-
input[type="password"]:focus,
|
| 201 |
-
input[type="email"]:focus {
|
| 202 |
-
border-color: var(--primary-color);
|
| 203 |
-
box-shadow: 0 0 0 2px var(--glass-border);
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
/* ── Buttons ────────────────────────────────────────── */
|
| 207 |
-
button {
|
| 208 |
-
background: var(--primary-color);
|
| 209 |
-
color: var(--primary-text);
|
| 210 |
-
border: 1px solid var(--glass-border);
|
| 211 |
-
padding: 12px 24px;
|
| 212 |
-
border-radius: 8px;
|
| 213 |
-
cursor: pointer;
|
| 214 |
-
font-size: 14px;
|
| 215 |
-
font-weight: 500;
|
| 216 |
-
transition: all var(--transition-speed) ease;
|
| 217 |
-
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
button:hover {
|
| 221 |
-
transform: translateY(-1px);
|
| 222 |
-
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
| 223 |
-
background: var(--primary-hover);
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
#askBtn {
|
| 227 |
-
border-radius: 8px;
|
| 228 |
-
padding: 12px 20px;
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
#clearBtn {
|
| 232 |
-
background: rgba(255, 255, 255, 0.1);
|
| 233 |
-
box-shadow: none;
|
| 234 |
-
color: var(--text-muted);
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
#clearBtn:hover {
|
| 238 |
-
background: rgba(255, 255, 255, 0.15);
|
| 239 |
-
color: var(--text-main);
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
.logout-btn {
|
| 243 |
-
background: var(--danger-color);
|
| 244 |
-
color: white;
|
| 245 |
-
border-color: transparent;
|
| 246 |
-
box-shadow: none;
|
| 247 |
-
padding: 8px 16px;
|
| 248 |
-
font-size: 14px;
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
.logout-btn:hover {
|
| 252 |
-
background: var(--danger-hover);
|
| 253 |
-
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
.delete-btn {
|
| 257 |
-
background: transparent;
|
| 258 |
-
color: var(--danger-color);
|
| 259 |
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 260 |
-
padding: 6px 12px;
|
| 261 |
-
border-radius: 6px;
|
| 262 |
-
font-size: 13px;
|
| 263 |
-
box-shadow: none;
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
.delete-btn:hover {
|
| 267 |
-
background: var(--danger-color);
|
| 268 |
-
color: white;
|
| 269 |
-
transform: none;
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
.clear-vector-btn {
|
| 273 |
-
background: rgba(239, 68, 68, 0.1);
|
| 274 |
-
color: var(--danger-color);
|
| 275 |
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 276 |
-
box-shadow: none;
|
| 277 |
-
margin-top: 10px;
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
.clear-vector-btn:hover {
|
| 281 |
-
background: var(--danger-color);
|
| 282 |
-
color: white;
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
.google-btn {
|
| 286 |
-
background: white;
|
| 287 |
-
color: #333;
|
| 288 |
-
width: 100%;
|
| 289 |
-
display: flex;
|
| 290 |
-
justify-content: center;
|
| 291 |
-
align-items: center;
|
| 292 |
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
.google-btn:hover {
|
| 296 |
-
background: #f1f5f9;
|
| 297 |
-
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
/* ── Nav Button ─────────────────────────────────────── */
|
| 301 |
-
.nav-btn {
|
| 302 |
-
text-align: center;
|
| 303 |
-
margin-top: 30px;
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
.nav-btn button {
|
| 307 |
-
padding: 14px 32px;
|
| 308 |
-
font-size: 16px;
|
| 309 |
-
border-radius: 30px;
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
/* ── Status Texts ───────────────────────────────────── */
|
| 313 |
-
#uploadStatus,
|
| 314 |
-
#clearStatus {
|
| 315 |
-
margin-top: 15px;
|
| 316 |
-
font-size: 14px;
|
| 317 |
-
font-weight: 500;
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
/* ── Files List ─────────────────────────────────────── */
|
| 321 |
-
.files-box ul {
|
| 322 |
-
list-style: none;
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
.file-item {
|
| 326 |
-
display: flex;
|
| 327 |
-
justify-content: space-between;
|
| 328 |
-
align-items: center;
|
| 329 |
-
padding: 14px 18px;
|
| 330 |
-
background: var(--input-bg);
|
| 331 |
-
border: 1px solid var(--glass-border);
|
| 332 |
-
margin-bottom: 10px;
|
| 333 |
-
border-radius: 10px;
|
| 334 |
-
font-size: 15px;
|
| 335 |
-
transition: all var(--transition-speed) ease;
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
.file-item.active-file {
|
| 339 |
-
background: rgba(139, 92, 246, 0.25);
|
| 340 |
-
border-color: var(--primary-color);
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
.file-item:last-child {
|
| 344 |
-
margin-bottom: 0;
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
.file-item:hover {
|
| 348 |
-
background: var(--item-hover-bg);
|
| 349 |
-
border-color: rgba(139, 92, 246, 0.3);
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
.file-item span {
|
| 353 |
-
display: flex;
|
| 354 |
-
align-items: center;
|
| 355 |
-
gap: 10px;
|
| 356 |
-
}
|
| 357 |
-
|
| 358 |
-
/* ── Chat Box ───────────────────────────────────────── */
|
| 359 |
-
.chat-box {
|
| 360 |
-
height: 500px;
|
| 361 |
-
overflow-y: auto;
|
| 362 |
-
display: flex;
|
| 363 |
-
flex-direction: column;
|
| 364 |
-
padding: 20px 25px;
|
| 365 |
-
gap: 16px;
|
| 366 |
-
margin-bottom: 20px;
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
/* Custom Scrollbar for Chat */
|
| 370 |
-
.chat-box::-webkit-scrollbar {
|
| 371 |
-
width: 6px;
|
| 372 |
-
}
|
| 373 |
-
|
| 374 |
-
.chat-box::-webkit-scrollbar-track {
|
| 375 |
-
background: rgba(0, 0, 0, 0.1);
|
| 376 |
-
border-radius: 10px;
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
.chat-box::-webkit-scrollbar-thumb {
|
| 380 |
-
background: var(--text-muted);
|
| 381 |
-
border-radius: 10px;
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
.chat-box::-webkit-scrollbar-thumb:hover {
|
| 385 |
-
background: var(--primary-color);
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
/* ── Messages ───────────────────────────────────────── */
|
| 389 |
-
.message {
|
| 390 |
-
padding: 14px 18px;
|
| 391 |
-
border-radius: 14px;
|
| 392 |
-
line-height: 1.6;
|
| 393 |
-
max-width: 85%;
|
| 394 |
-
font-size: 15px;
|
| 395 |
-
animation: messagePop 0.3s ease-out;
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
@keyframes messagePop {
|
| 399 |
-
from {
|
| 400 |
-
opacity: 0;
|
| 401 |
-
transform: scale(0.95) translateY(10px);
|
| 402 |
-
}
|
| 403 |
-
|
| 404 |
-
to {
|
| 405 |
-
opacity: 1;
|
| 406 |
-
transform: scale(1) translateY(0);
|
| 407 |
-
}
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
.message.user {
|
| 411 |
-
background: var(--primary-color);
|
| 412 |
-
color: var(--primary-text);
|
| 413 |
-
border: 1px solid var(--glass-border);
|
| 414 |
-
align-self: flex-end;
|
| 415 |
-
border-bottom-right-radius: 4px;
|
| 416 |
-
}
|
| 417 |
-
|
| 418 |
-
.message.bot {
|
| 419 |
-
background: var(--input-bg);
|
| 420 |
-
border: 1px solid var(--glass-border);
|
| 421 |
-
align-self: flex-start;
|
| 422 |
-
border-bottom-left-radius: 4px;
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
.message.error {
|
| 426 |
-
background: rgba(239, 68, 68, 0.1);
|
| 427 |
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 428 |
-
color: var(--danger-color);
|
| 429 |
-
align-self: flex-start;
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
/* ── Input Area ─────────────────────────────────────── */
|
| 433 |
-
.input-area {
|
| 434 |
-
display: flex;
|
| 435 |
-
gap: 12px;
|
| 436 |
-
background: var(--glass-bg);
|
| 437 |
-
padding: 15px;
|
| 438 |
-
border-radius: 16px;
|
| 439 |
-
border: 1px solid var(--glass-border);
|
| 440 |
-
backdrop-filter: blur(16px);
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
.input-area input {
|
| 444 |
-
flex: 1;
|
| 445 |
-
margin-bottom: 0;
|
| 446 |
-
border: none;
|
| 447 |
-
background: var(--input-bg);
|
| 448 |
-
}
|
| 449 |
-
|
| 450 |
-
#loader {
|
| 451 |
-
text-align: center;
|
| 452 |
-
font-size: 14px;
|
| 453 |
-
color: var(--primary-color);
|
| 454 |
-
margin-top: -10px;
|
| 455 |
-
margin-bottom: 10px;
|
| 456 |
-
font-weight: 500;
|
| 457 |
-
animation: pulse 1.5s infinite;
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
@keyframes pulse {
|
| 461 |
-
0% {
|
| 462 |
-
opacity: 0.6;
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
-
50% {
|
| 466 |
-
opacity: 1;
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
100% {
|
| 470 |
-
opacity: 0.6;
|
| 471 |
-
}
|
| 472 |
-
}
|
| 473 |
-
|
| 474 |
-
/* ── Sources & Badge ────────────────────────────────── */
|
| 475 |
-
.sources {
|
| 476 |
-
margin-top: 10px;
|
| 477 |
-
background: var(--input-bg);
|
| 478 |
-
border-left: 3px solid var(--accent-color);
|
| 479 |
-
padding: 12px 16px;
|
| 480 |
-
border-radius: 6px;
|
| 481 |
-
font-size: 13px;
|
| 482 |
-
color: var(--text-muted);
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
.sources ul {
|
| 486 |
-
margin-top: 8px;
|
| 487 |
-
padding-left: 0;
|
| 488 |
-
list-style: none;
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
.sources li {
|
| 492 |
-
display: flex;
|
| 493 |
-
justify-content: space-between;
|
| 494 |
-
padding: 6px 0;
|
| 495 |
-
border-bottom: 1px solid var(--glass-border);
|
| 496 |
-
}
|
| 497 |
-
|
| 498 |
-
.sources li:last-child {
|
| 499 |
-
border-bottom: none;
|
| 500 |
-
padding-bottom: 0;
|
| 501 |
-
}
|
| 502 |
-
|
| 503 |
-
.confidence {
|
| 504 |
-
color: var(--success-color);
|
| 505 |
-
font-weight: 600;
|
| 506 |
-
}
|
| 507 |
-
|
| 508 |
-
.file-badge {
|
| 509 |
-
text-align: center;
|
| 510 |
-
padding: 10px 15px;
|
| 511 |
-
border-radius: 20px;
|
| 512 |
-
display: inline-block;
|
| 513 |
-
margin: 0 auto 20px;
|
| 514 |
-
font-size: 14px;
|
| 515 |
-
font-weight: 500;
|
| 516 |
-
background: rgba(16, 185, 129, 0.2);
|
| 517 |
-
border: 1px solid rgba(16, 185, 129, 0.3);
|
| 518 |
-
color: var(--success-color);
|
| 519 |
-
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
| 520 |
-
}
|
| 521 |
-
|
| 522 |
-
/* ── Auth Box ───────────────────────────────────────── */
|
| 523 |
-
.auth-box {
|
| 524 |
-
max-width: 420px;
|
| 525 |
-
margin: 60px auto;
|
| 526 |
-
text-align: center;
|
| 527 |
-
}
|
| 528 |
-
|
| 529 |
-
.auth-box p {
|
| 530 |
-
margin-top: 20px;
|
| 531 |
-
font-size: 14px;
|
| 532 |
-
color: var(--text-muted);
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
.auth-box a {
|
| 536 |
-
color: var(--primary-color);
|
| 537 |
-
font-weight: 600;
|
| 538 |
-
text-decoration: none;
|
| 539 |
-
transition: color var(--transition-speed);
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
-
.auth-box a:hover {
|
| 543 |
-
color: var(--primary-hover);
|
| 544 |
-
text-decoration: underline;
|
| 545 |
-
}
|
| 546 |
-
|
| 547 |
-
.error-msg {
|
| 548 |
-
background: rgba(239, 68, 68, 0.1);
|
| 549 |
-
color: var(--danger-color);
|
| 550 |
-
padding: 12px;
|
| 551 |
-
border-radius: 8px;
|
| 552 |
-
margin-bottom: 20px;
|
| 553 |
-
font-size: 14px;
|
| 554 |
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 555 |
-
}
|
| 556 |
-
|
| 557 |
-
.divider {
|
| 558 |
-
display: flex;
|
| 559 |
-
align-items: center;
|
| 560 |
-
margin: 20px 0;
|
| 561 |
-
color: var(--text-muted);
|
| 562 |
-
font-size: 13px;
|
| 563 |
-
}
|
| 564 |
-
|
| 565 |
-
.divider::before,
|
| 566 |
-
.divider::after {
|
| 567 |
-
content: "";
|
| 568 |
-
flex: 1;
|
| 569 |
-
border-bottom: 1px solid var(--glass-border);
|
| 570 |
-
margin: 0 15px;
|
| 571 |
-
}
|
| 572 |
-
|
| 573 |
-
/* ── Profile Pic ────────────────────────────────────── */
|
| 574 |
-
.profile-container {
|
| 575 |
-
position: relative;
|
| 576 |
-
display: inline-block;
|
| 577 |
-
}
|
| 578 |
-
|
| 579 |
-
.profile-pic {
|
| 580 |
-
width: 44px;
|
| 581 |
-
height: 44px;
|
| 582 |
-
border-radius: 50%;
|
| 583 |
-
object-fit: cover;
|
| 584 |
-
border: 2px solid var(--primary-color);
|
| 585 |
-
cursor: pointer;
|
| 586 |
-
transition: all 0.3s ease;
|
| 587 |
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
| 588 |
-
}
|
| 589 |
-
|
| 590 |
-
.profile-pic:hover {
|
| 591 |
-
transform: scale(1.05);
|
| 592 |
-
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
| 593 |
-
filter: brightness(0.8);
|
| 594 |
-
}
|
| 595 |
-
|
| 596 |
-
.profile-pic:hover::after {
|
| 597 |
-
content: "Edit";
|
| 598 |
-
position: absolute;
|
| 599 |
-
bottom: 0;
|
| 600 |
-
left: 0;
|
| 601 |
-
background: var(--input-bg);
|
| 602 |
-
color: var(--text-main);
|
| 603 |
-
font-size: 11px;
|
| 604 |
-
width: 100%;
|
| 605 |
-
text-align: center;
|
| 606 |
-
border-radius: 0 0 50% 50%;
|
| 607 |
-
pointer-events: none;
|
| 608 |
-
}
|
| 609 |
-
|
| 610 |
-
.username-text {
|
| 611 |
-
font-size: 16px;
|
| 612 |
-
color: var(--text-main);
|
| 613 |
-
font-weight: 500;
|
| 614 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin.html
DELETED
|
@@ -1,106 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8">
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>Admin Dashboard</title>
|
| 8 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 9 |
-
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 10 |
-
<style>
|
| 11 |
-
.admin-table {
|
| 12 |
-
width: 100%;
|
| 13 |
-
border-collapse: collapse;
|
| 14 |
-
margin-top: 20px;
|
| 15 |
-
color: var(--text-main);
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.admin-table th,
|
| 19 |
-
.admin-table td {
|
| 20 |
-
padding: 12px;
|
| 21 |
-
border: 1px solid var(--glass-border);
|
| 22 |
-
text-align: left;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
.admin-table th {
|
| 26 |
-
background: rgba(139, 92, 246, 0.2);
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
.admin-table td {
|
| 30 |
-
background: var(--input-bg);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
.file-list {
|
| 34 |
-
margin: 0;
|
| 35 |
-
padding-left: 20px;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
.file-link {
|
| 39 |
-
color: var(--primary-color);
|
| 40 |
-
text-decoration: none;
|
| 41 |
-
font-weight: bold;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
.file-link:hover {
|
| 45 |
-
text-decoration: underline;
|
| 46 |
-
}
|
| 47 |
-
</style>
|
| 48 |
-
</head>
|
| 49 |
-
|
| 50 |
-
<body>
|
| 51 |
-
<header>
|
| 52 |
-
<h1>🛡️ Admin Dashboard</h1>
|
| 53 |
-
<div class="header-right">
|
| 54 |
-
<button class="logout-btn" style="background-color: var(--primary-color);"
|
| 55 |
-
onclick="toggleLightMode()">🌗</button>
|
| 56 |
-
<a href="/">← Dashboard</a>
|
| 57 |
-
<a href="/logout"><button class="logout-btn">Logout</button></a>
|
| 58 |
-
</div>
|
| 59 |
-
</header>
|
| 60 |
-
|
| 61 |
-
<div class="container" style="max-width: 1000px;">
|
| 62 |
-
<div class="upload-box" style="text-align: left;">
|
| 63 |
-
<h2>👥 Registered Users & Files</h2>
|
| 64 |
-
<table class="admin-table">
|
| 65 |
-
<thead>
|
| 66 |
-
<tr>
|
| 67 |
-
<th>ID</th>
|
| 68 |
-
<th>User</th>
|
| 69 |
-
<th>Email</th>
|
| 70 |
-
<th>Preferred Model</th>
|
| 71 |
-
<th>Files (Click to Download)</th>
|
| 72 |
-
</tr>
|
| 73 |
-
</thead>
|
| 74 |
-
<tbody>
|
| 75 |
-
{% for user in users %}
|
| 76 |
-
<tr>
|
| 77 |
-
<td>{{ user.id }}</td>
|
| 78 |
-
<td>
|
| 79 |
-
{% if user.is_admin %}⭐{% endif %}
|
| 80 |
-
{{ user.username }}
|
| 81 |
-
</td>
|
| 82 |
-
<td>{{ user.email }}</td>
|
| 83 |
-
<td>{{ user.preferred_model }}</td>
|
| 84 |
-
<td>
|
| 85 |
-
{% if user_files[user.username] %}
|
| 86 |
-
<ul class="file-list">
|
| 87 |
-
{% for f in user_files[user.username] %}
|
| 88 |
-
<li>
|
| 89 |
-
📄 <a href="/download/{{ user.username }}/{{ f }}" class="file-link"
|
| 90 |
-
title="Download {{ f }}">{{ f }}</a>
|
| 91 |
-
</li>
|
| 92 |
-
{% endfor %}
|
| 93 |
-
</ul>
|
| 94 |
-
{% else %}
|
| 95 |
-
<span style="color: var(--text-muted);">No files</span>
|
| 96 |
-
{% endif %}
|
| 97 |
-
</td>
|
| 98 |
-
</tr>
|
| 99 |
-
{% endfor %}
|
| 100 |
-
</tbody>
|
| 101 |
-
</table>
|
| 102 |
-
</div>
|
| 103 |
-
</div>
|
| 104 |
-
</body>
|
| 105 |
-
|
| 106 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/chat.html
DELETED
|
@@ -1,162 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8">
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>RAG Chat</title>
|
| 8 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 9 |
-
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 10 |
-
</head>
|
| 11 |
-
|
| 12 |
-
<body>
|
| 13 |
-
|
| 14 |
-
<!-- ── Header ── -->
|
| 15 |
-
<header>
|
| 16 |
-
<h1>💬 RAG PDF Assistant</h1>
|
| 17 |
-
<div class="header-right">
|
| 18 |
-
<button class="logout-btn" style="background-color: var(--primary-color);"
|
| 19 |
-
onclick="toggleLightMode()">🌗</button>
|
| 20 |
-
<a href="/profile">👤 Profile</a>
|
| 21 |
-
<a href="/">← Dashboard</a>
|
| 22 |
-
</div>
|
| 23 |
-
</header>
|
| 24 |
-
|
| 25 |
-
<!-- ── Chat Box ── -->
|
| 26 |
-
<div class="container"
|
| 27 |
-
style="max-width: 1000px; display: flex; flex-direction: column; height: 85vh; margin-top: 20px;">
|
| 28 |
-
|
| 29 |
-
<!-- ── Current File Badge ── -->
|
| 30 |
-
<div id="currentFileBadge" class="file-badge" style="align-self: flex-start; margin-bottom: 10px;">
|
| 31 |
-
📄 No file selected
|
| 32 |
-
</div>
|
| 33 |
-
|
| 34 |
-
<div class="chat-box" id="chatBox"
|
| 35 |
-
style="flex-grow: 1; height: auto; border-radius: 16px 16px 0 0; border-bottom: none; margin-bottom: 0;">
|
| 36 |
-
<!-- Messages appear here -->
|
| 37 |
-
</div>
|
| 38 |
-
|
| 39 |
-
<!-- ── Input Area ── -->
|
| 40 |
-
<div class="input-area"
|
| 41 |
-
style="border-radius: 0 0 16px 16px; border-top: 1px solid var(--glass-border); padding: 20px;">
|
| 42 |
-
<input type="text" id="questionInput" placeholder="Ask a question about your document..."
|
| 43 |
-
style="font-size: 16px; padding: 15px;" />
|
| 44 |
-
<button id="askBtn" style="padding: 15px 30px; font-size: 16px; min-width: 100px;">Ask</button>
|
| 45 |
-
<button id="clearBtn" style="padding: 15px 20px;">Clear</button>
|
| 46 |
-
</div>
|
| 47 |
-
|
| 48 |
-
<!-- ── Loading Spinner ── -->
|
| 49 |
-
<div id="loader" style="display:none; margin-top: 15px;">
|
| 50 |
-
Generating response...
|
| 51 |
-
</div>
|
| 52 |
-
</div>
|
| 53 |
-
|
| 54 |
-
<script>
|
| 55 |
-
// ── Store Current Filename ───────────────────────
|
| 56 |
-
let currentFile = localStorage.getItem("currentFile") || ""
|
| 57 |
-
|
| 58 |
-
// ── Show Current File Badge ──────────────────────
|
| 59 |
-
const badge = document.getElementById("currentFileBadge")
|
| 60 |
-
if (currentFile) {
|
| 61 |
-
badge.innerText = `📄 Asking from: ${currentFile}`
|
| 62 |
-
badge.style.backgroundColor = "#eafaf1"
|
| 63 |
-
badge.style.color = "green"
|
| 64 |
-
} else {
|
| 65 |
-
badge.innerText = "⚠️ No file selected - Go back and upload a PDF"
|
| 66 |
-
badge.style.backgroundColor = "#fadbd8"
|
| 67 |
-
badge.style.color = "red"
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
// ── Ask Question ─────────────────────────────────
|
| 71 |
-
document.getElementById("askBtn").addEventListener("click", async () => {
|
| 72 |
-
const question = document.getElementById("questionInput").value.trim()
|
| 73 |
-
const chatBox = document.getElementById("chatBox")
|
| 74 |
-
const loader = document.getElementById("loader")
|
| 75 |
-
|
| 76 |
-
if (!question) {
|
| 77 |
-
alert("Please enter a question!")
|
| 78 |
-
return
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
if (!currentFile) {
|
| 82 |
-
chatBox.innerHTML += `
|
| 83 |
-
<div class="message error">
|
| 84 |
-
❌ Please go back and upload a PDF first!
|
| 85 |
-
</div>
|
| 86 |
-
`
|
| 87 |
-
document.getElementById("questionInput").value = ""
|
| 88 |
-
chatBox.scrollTop = chatBox.scrollHeight
|
| 89 |
-
return
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
// ── Show user message ──
|
| 93 |
-
chatBox.innerHTML += `
|
| 94 |
-
<div class="message user">
|
| 95 |
-
<strong>You:</strong> ${question}
|
| 96 |
-
</div>
|
| 97 |
-
`
|
| 98 |
-
|
| 99 |
-
document.getElementById("questionInput").value = ""
|
| 100 |
-
loader.style.display = "block"
|
| 101 |
-
chatBox.scrollTop = chatBox.scrollHeight
|
| 102 |
-
|
| 103 |
-
try {
|
| 104 |
-
const response = await fetch("/ask", {
|
| 105 |
-
method: "POST",
|
| 106 |
-
headers: { "Content-Type": "application/json" },
|
| 107 |
-
body: JSON.stringify({
|
| 108 |
-
question: question,
|
| 109 |
-
filename: currentFile
|
| 110 |
-
})
|
| 111 |
-
})
|
| 112 |
-
|
| 113 |
-
const data = await response.json()
|
| 114 |
-
loader.style.display = "none"
|
| 115 |
-
|
| 116 |
-
if (response.ok) {
|
| 117 |
-
// ── Show answer ──
|
| 118 |
-
chatBox.innerHTML += `
|
| 119 |
-
<div class="message bot">
|
| 120 |
-
<strong>Assistant:</strong> ${data.answer}
|
| 121 |
-
</div>
|
| 122 |
-
`
|
| 123 |
-
} else {
|
| 124 |
-
chatBox.innerHTML += `
|
| 125 |
-
<div class="message error">
|
| 126 |
-
❌ Error: ${data.error || "Unknown error"}
|
| 127 |
-
</div>
|
| 128 |
-
`
|
| 129 |
-
}
|
| 130 |
-
} catch (error) {
|
| 131 |
-
loader.style.display = "none"
|
| 132 |
-
chatBox.innerHTML += `
|
| 133 |
-
<div class="message error">
|
| 134 |
-
❌ Something went wrong.
|
| 135 |
-
</div>
|
| 136 |
-
`
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
// ── Auto scroll to bottom ──
|
| 140 |
-
chatBox.scrollTop = chatBox.scrollHeight
|
| 141 |
-
})
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
// ── Clear History ─────────────────────────────────
|
| 145 |
-
document.getElementById("clearBtn").addEventListener("click", async () => {
|
| 146 |
-
if (!confirm("Are you sure you want to clear chat history? think again")) return
|
| 147 |
-
await fetch("/clear", { method: "POST" })
|
| 148 |
-
document.getElementById("chatBox").innerHTML = ""
|
| 149 |
-
})
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
// ── Enter Key Support ─────────────────────────────
|
| 153 |
-
document.getElementById("questionInput").addEventListener("keypress", (e) => {
|
| 154 |
-
if (e.key === "Enter") {
|
| 155 |
-
document.getElementById("askBtn").click()
|
| 156 |
-
}
|
| 157 |
-
})
|
| 158 |
-
</script>
|
| 159 |
-
|
| 160 |
-
</body>
|
| 161 |
-
|
| 162 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/index.html
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8">
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>RAG Application</title>
|
| 8 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 9 |
-
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 10 |
-
</head>
|
| 11 |
-
|
| 12 |
-
<body>
|
| 13 |
-
|
| 14 |
-
<!-- ── Header ── -->
|
| 15 |
-
<header>
|
| 16 |
-
<h1>📄 RAG Assistant</h1>
|
| 17 |
-
<div class="header-right">
|
| 18 |
-
|
| 19 |
-
<!-- ── Profile Pic ── -->
|
| 20 |
-
<div class="profile-container">
|
| 21 |
-
<img id="profilePic"
|
| 22 |
-
src="{{ current_user.profile_pic or url_for('static', filename='default_avatar.png') }}"
|
| 23 |
-
alt="Profile" class="profile-pic" onclick="document.getElementById('picInput').click()"
|
| 24 |
-
title="Click to change profile picture" />
|
| 25 |
-
<input type="file" id="picInput" accept="image/*" style="display:none"
|
| 26 |
-
onchange="uploadProfilePic(this)" />
|
| 27 |
-
</div>
|
| 28 |
-
|
| 29 |
-
<!-- ── Username ── -->
|
| 30 |
-
<a href="/profile" style="text-decoration: none; display: flex; align-items: center;">
|
| 31 |
-
<span class="username-text" style="cursor: pointer;" title="Go to Profile">{{ current_user.username
|
| 32 |
-
}}</span>
|
| 33 |
-
</a>
|
| 34 |
-
|
| 35 |
-
<!-- ── Admin Button ── -->
|
| 36 |
-
{% if current_user.is_admin %}
|
| 37 |
-
<a href="/admin">
|
| 38 |
-
<button class="logout-btn" style="background-color: var(--danger-color); margin-right: 10px;">🛡️ Admin
|
| 39 |
-
Node</button>
|
| 40 |
-
</a>
|
| 41 |
-
{% endif %}
|
| 42 |
-
|
| 43 |
-
<!-- ── Light Mode Toggle ── -->
|
| 44 |
-
<button class="logout-btn" style="background-color: var(--primary-color);"
|
| 45 |
-
onclick="toggleLightMode()">🌗</button>
|
| 46 |
-
|
| 47 |
-
<!-- ── Logout ── -->
|
| 48 |
-
<a href="/logout">
|
| 49 |
-
<button class="logout-btn">Logout</button>
|
| 50 |
-
</a>
|
| 51 |
-
|
| 52 |
-
</div>
|
| 53 |
-
</header>
|
| 54 |
-
|
| 55 |
-
<div class="container" style="max-width: 1100px;">
|
| 56 |
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px;">
|
| 57 |
-
<div class="left-panel">
|
| 58 |
-
<!-- ── Upload Section ── -->
|
| 59 |
-
<div class="upload-box">
|
| 60 |
-
<h2>Upload PDF</h2>
|
| 61 |
-
<form id="uploadForm">
|
| 62 |
-
<input type="file" id="pdfFile" accept=".pdf,.docx,.txt,.md" required />
|
| 63 |
-
<button style="width: 100%;" type="submit">Upload & Index</button>
|
| 64 |
-
</form>
|
| 65 |
-
<div id="uploadStatus"></div>
|
| 66 |
-
</div>
|
| 67 |
-
|
| 68 |
-
<!-- ── Navigate to Chat ── -->
|
| 69 |
-
<div class="nav-btn" style="text-align: left; margin-top: 20px;">
|
| 70 |
-
<a href="/chat" style="display: block;">
|
| 71 |
-
<button style="width: 100%; background: var(--accent-color); color: white; padding: 18px; font-size: 18px;">Go to Chat →</button>
|
| 72 |
-
</a>
|
| 73 |
-
</div>
|
| 74 |
-
</div>
|
| 75 |
-
|
| 76 |
-
<div class="right-panel">
|
| 77 |
-
<!-- ── Uploaded Files List ── -->
|
| 78 |
-
<div class="files-box" style="height: 100%; display: flex; flex-direction: column;">
|
| 79 |
-
<h2>📋 Uploaded Files</h2>
|
| 80 |
-
<ul id="filesList" style="flex-grow: 1; overflow-y: auto; max-height: 400px; padding-right: 10px;">
|
| 81 |
-
<!-- Files appear here -->
|
| 82 |
-
</ul>
|
| 83 |
-
|
| 84 |
-
<div class="clear-box" style="margin-top: 20px; margin-bottom: 0;">
|
| 85 |
-
<h2 style="font-size: 16px;">🔄 Reset Vector Store</h2>
|
| 86 |
-
<button id="clearVectorBtn" class="clear-vector-btn" style="width: 100%;">
|
| 87 |
-
🔄 Clear Vector Store
|
| 88 |
-
</button>
|
| 89 |
-
<div id="clearStatus"></div>
|
| 90 |
-
</div>
|
| 91 |
-
</div>
|
| 92 |
-
</div>
|
| 93 |
-
</div>
|
| 94 |
-
</div>
|
| 95 |
-
|
| 96 |
-
<script>
|
| 97 |
-
// ── Load Files on Page Load ───────────────────────
|
| 98 |
-
async function loadFiles() {
|
| 99 |
-
const response = await fetch("/files")
|
| 100 |
-
const data = await response.json()
|
| 101 |
-
const filesList = document.getElementById("filesList")
|
| 102 |
-
|
| 103 |
-
filesList.innerHTML = ""
|
| 104 |
-
|
| 105 |
-
if (data.files.length === 0) {
|
| 106 |
-
filesList.innerHTML = "<li>No files uploaded yet</li>"
|
| 107 |
-
return
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
data.files.forEach(file => {
|
| 111 |
-
let icon = "📄"
|
| 112 |
-
if (file.endsWith(".docx")) icon = "📝"
|
| 113 |
-
if (file.endsWith(".txt")) icon = "📃"
|
| 114 |
-
|
| 115 |
-
const isSelected = localStorage.getItem("currentFile") === file ? "checked" : ""
|
| 116 |
-
const activeClass = localStorage.getItem("currentFile") === file ? "active-file" : ""
|
| 117 |
-
|
| 118 |
-
filesList.innerHTML += `
|
| 119 |
-
<li class="file-item ${activeClass}" style="cursor: pointer;" onclick="selectFile('${file}')">
|
| 120 |
-
<span>
|
| 121 |
-
<input type="radio" name="selectedFile" value="${file}" ${isSelected} class="file-radio" onclick="event.stopPropagation(); selectFile('${file}')">
|
| 122 |
-
${icon} ${file}
|
| 123 |
-
</span>
|
| 124 |
-
<button
|
| 125 |
-
class="delete-btn"
|
| 126 |
-
onclick="event.stopPropagation(); deleteFile('${file}')">
|
| 127 |
-
🗑️ Delete
|
| 128 |
-
</button>
|
| 129 |
-
</li>
|
| 130 |
-
`
|
| 131 |
-
})
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
function selectFile(filename) {
|
| 135 |
-
localStorage.setItem("currentFile", filename)
|
| 136 |
-
loadFiles() // Refresh list to show active selection
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
async function uploadProfilePic(input) {
|
| 140 |
-
const file = input.files[0]
|
| 141 |
-
if (!file) return
|
| 142 |
-
|
| 143 |
-
const formData = new FormData()
|
| 144 |
-
formData.append("profile_pic", file)
|
| 145 |
-
|
| 146 |
-
const response = await fetch("/upload_profile_pic", {
|
| 147 |
-
method: "POST",
|
| 148 |
-
body: formData
|
| 149 |
-
})
|
| 150 |
-
|
| 151 |
-
const data = await response.json()
|
| 152 |
-
|
| 153 |
-
if (response.ok) {
|
| 154 |
-
// ── Update pic instantly ──
|
| 155 |
-
document.getElementById("profilePic").src = data.profile_pic + "?t=" + Date.now()
|
| 156 |
-
} else {
|
| 157 |
-
alert("❌ " + data.error)
|
| 158 |
-
}
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
// ── Upload PDF ────────────────────────────────────
|
| 162 |
-
document.getElementById("uploadForm").addEventListener("submit", async (e) => {
|
| 163 |
-
e.preventDefault()
|
| 164 |
-
|
| 165 |
-
const fileInput = document.getElementById("pdfFile")
|
| 166 |
-
const statusDiv = document.getElementById("uploadStatus")
|
| 167 |
-
const formData = new FormData()
|
| 168 |
-
|
| 169 |
-
formData.append("pdf", fileInput.files[0])
|
| 170 |
-
statusDiv.innerText = "Uploading..."
|
| 171 |
-
|
| 172 |
-
const response = await fetch("/upload", {
|
| 173 |
-
method: "POST",
|
| 174 |
-
body: formData
|
| 175 |
-
})
|
| 176 |
-
|
| 177 |
-
const data = await response.json()
|
| 178 |
-
|
| 179 |
-
if (response.ok) {
|
| 180 |
-
localStorage.setItem("currentFile", fileInput.files[0].name)
|
| 181 |
-
statusDiv.innerText = "✅ " + data.message
|
| 182 |
-
statusDiv.style.color = "green"
|
| 183 |
-
loadFiles() // Refresh files list
|
| 184 |
-
} else {
|
| 185 |
-
statusDiv.innerText = "❌ " + data.error
|
| 186 |
-
statusDiv.style.color = "red"
|
| 187 |
-
}
|
| 188 |
-
})
|
| 189 |
-
|
| 190 |
-
document.getElementById("clearVectorBtn").addEventListener("click", async () => {
|
| 191 |
-
if (!confirm("Are you sure? This will delete ALL embeddings!")) return
|
| 192 |
-
|
| 193 |
-
const clearStatus = document.getElementById("clearStatus")
|
| 194 |
-
clearStatus.innerText = "Clearing..."
|
| 195 |
-
|
| 196 |
-
const response = await fetch("/clear_vectorstore", {
|
| 197 |
-
method: "POST"
|
| 198 |
-
})
|
| 199 |
-
|
| 200 |
-
const data = await response.json()
|
| 201 |
-
|
| 202 |
-
if (response.ok) {
|
| 203 |
-
clearStatus.innerText = "✅ " + data.message
|
| 204 |
-
clearStatus.style.color = "green"
|
| 205 |
-
localStorage.removeItem("currentFile")
|
| 206 |
-
loadFiles()
|
| 207 |
-
} else {
|
| 208 |
-
clearStatus.innerText = "❌ " + data.error
|
| 209 |
-
clearStatus.style.color = "red"
|
| 210 |
-
}
|
| 211 |
-
})
|
| 212 |
-
|
| 213 |
-
// ── Load Files on Page Load ───────────────────────
|
| 214 |
-
async function deleteFile(filename) {
|
| 215 |
-
if (!confirm(`Are you sure you want to delete ${filename}?`)) return
|
| 216 |
-
|
| 217 |
-
const response = await fetch("/delete", {
|
| 218 |
-
method: "POST",
|
| 219 |
-
headers: { "Content-Type": "application/json" },
|
| 220 |
-
body: JSON.stringify({ filename: filename })
|
| 221 |
-
})
|
| 222 |
-
|
| 223 |
-
const data = await response.json()
|
| 224 |
-
|
| 225 |
-
if (response.ok) {
|
| 226 |
-
alert("✅ " + data.message)
|
| 227 |
-
|
| 228 |
-
// Clear localStorage if deleted file was selected
|
| 229 |
-
if (localStorage.getItem("currentFile") === filename) {
|
| 230 |
-
localStorage.removeItem("currentFile")
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
loadFiles() // Refresh list
|
| 234 |
-
} else {
|
| 235 |
-
alert("❌ " + data.error)
|
| 236 |
-
}
|
| 237 |
-
}
|
| 238 |
-
loadFiles()
|
| 239 |
-
</script>
|
| 240 |
-
|
| 241 |
-
</body>
|
| 242 |
-
|
| 243 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/login.html
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8">
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>Login</title>
|
| 8 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 9 |
-
</head>
|
| 10 |
-
|
| 11 |
-
<body>
|
| 12 |
-
<header>
|
| 13 |
-
<h1>📄 PDF Assistant RAG</h1>
|
| 14 |
-
</header>
|
| 15 |
-
|
| 16 |
-
<div class="container">
|
| 17 |
-
<div class="auth-box">
|
| 18 |
-
<h2>Welcome Back!</h2>
|
| 19 |
-
|
| 20 |
-
{% if error %}
|
| 21 |
-
<div class="error-msg">❌ {{ error }}</div>
|
| 22 |
-
{% endif %}
|
| 23 |
-
|
| 24 |
-
<form method="POST" action="/login">
|
| 25 |
-
<input type="text" name="username" placeholder="Username" required />
|
| 26 |
-
<input type="password" name="password" placeholder="Password" required />
|
| 27 |
-
<button type="submit">Login</button>
|
| 28 |
-
</form>
|
| 29 |
-
|
| 30 |
-
<div class="divider">
|
| 31 |
-
<span>
|
| 32 |
-
OR
|
| 33 |
-
</span>
|
| 34 |
-
</div>
|
| 35 |
-
|
| 36 |
-
<a href="{{ url_for('google.login') }}">
|
| 37 |
-
<button class="google-btn">
|
| 38 |
-
🌐 Login with Google
|
| 39 |
-
</button>
|
| 40 |
-
</a>
|
| 41 |
-
|
| 42 |
-
<p>Don't have an account? <a href="/register">Register</a></p>
|
| 43 |
-
</div>
|
| 44 |
-
</div>
|
| 45 |
-
|
| 46 |
-
<!-- ── Footer ── -->
|
| 47 |
-
<footer style="text-align: center; padding: 20px; color: var(--text-muted); margin-top: 40px; font-size: 14px;">
|
| 48 |
-
<p>© 2026 RAG Assistant. Param20h</p>
|
| 49 |
-
</footer>
|
| 50 |
-
</body>
|
| 51 |
-
|
| 52 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/profile.html
DELETED
|
@@ -1,208 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8">
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>User Profile</title>
|
| 8 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 9 |
-
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 10 |
-
</head>
|
| 11 |
-
|
| 12 |
-
<body>
|
| 13 |
-
|
| 14 |
-
<!-- ── Header ── -->
|
| 15 |
-
<header>
|
| 16 |
-
<h1>👤 My Profile</h1>
|
| 17 |
-
<div class="header-right">
|
| 18 |
-
<button class="logout-btn" style="background-color: var(--primary-color);"
|
| 19 |
-
onclick="toggleLightMode()">🌗</button>
|
| 20 |
-
<a href="/">← Back to Dashboard</a>
|
| 21 |
-
<a href="/logout"><button class="logout-btn">Logout</button></a>
|
| 22 |
-
</div>
|
| 23 |
-
</header>
|
| 24 |
-
|
| 25 |
-
<div class="container">
|
| 26 |
-
|
| 27 |
-
<!-- ── User Profile ── -->
|
| 28 |
-
<div class="upload-box" style="display: flex; align-items: center; gap: 20px; text-align: left;">
|
| 29 |
-
<div class="profile-container" style="display:inline-block;">
|
| 30 |
-
<img id="profilePic"
|
| 31 |
-
src="{{ current_user.profile_pic or url_for('static', filename='default_avatar.png') }}"
|
| 32 |
-
alt="Profile" class="profile-pic" style="width: 80px; height: 80px;"
|
| 33 |
-
onclick="document.getElementById('picInput').click()" title="Click to change profile picture" />
|
| 34 |
-
<input type="file" id="picInput" accept="image/*" style="display:none"
|
| 35 |
-
onchange="uploadProfilePic(this)" />
|
| 36 |
-
</div>
|
| 37 |
-
<div>
|
| 38 |
-
<h2 style="margin-bottom: 5px;">{{ current_user.username }}</h2>
|
| 39 |
-
<p style="color: var(--text-muted);">{{ current_user.email }}</p>
|
| 40 |
-
</div>
|
| 41 |
-
</div>
|
| 42 |
-
|
| 43 |
-
<!-- ── Settings / Custom APIs ── -->
|
| 44 |
-
<div class="upload-box" style="text-align: left;">
|
| 45 |
-
<h2>⚙️ API Settings</h2>
|
| 46 |
-
<form id="settingsForm">
|
| 47 |
-
<label style="color: var(--text-main); font-weight: 500; font-size: 14px;">Select Preferred
|
| 48 |
-
Model</label>
|
| 49 |
-
<select id="preferredModel" name="preferred_model"
|
| 50 |
-
style="width: 100%; padding: 12px; margin: 10px 0 20px; border-radius: 8px; border: 1px solid var(--glass-border); background: var(--input-bg); color: var(--text-main); outline: none;">
|
| 51 |
-
<option value="groq" {% if current_user.preferred_model=='groq' %}selected{% endif %}>Groq (Llama 3)
|
| 52 |
-
</option>
|
| 53 |
-
<option value="gemini" {% if current_user.preferred_model=='gemini' %}selected{% endif %}>Gemini
|
| 54 |
-
(Google)</option>
|
| 55 |
-
</select>
|
| 56 |
-
|
| 57 |
-
<label style="color: var(--text-main); font-weight: 500; font-size: 14px;">Groq API Key
|
| 58 |
-
(Optional — for chat generation)</label>
|
| 59 |
-
<input type="password" id="groqKey" name="groq_key" placeholder="Enter your Groq API Key..."
|
| 60 |
-
value="" data-has-key="{{ 'true' if current_user.groq_api_key else 'false' }}" style="margin-top: 10px;">
|
| 61 |
-
|
| 62 |
-
<label style="color: var(--text-main); font-weight: 500; font-size: 14px;">Gemini API Key
|
| 63 |
-
(Required — used for embeddings & chat)</label>
|
| 64 |
-
<input type="password" id="geminiKey" name="gemini_key" placeholder="Enter your Gemini API Key..."
|
| 65 |
-
value="" data-has-key="{{ 'true' if current_user.gemini_api_key else 'false' }}" style="margin-top: 10px;">
|
| 66 |
-
|
| 67 |
-
<hr style="border: 1px solid var(--glass-border); margin: 20px 0;">
|
| 68 |
-
|
| 69 |
-
<h3 style="margin-bottom: 10px; color: var(--text-main);">🌲 Pinecone Vector Database</h3>
|
| 70 |
-
<p style="color: var(--text-muted); font-size: 13px; margin-bottom: 15px;">
|
| 71 |
-
Create a free index at <a href="https://app.pinecone.io" target="_blank" style="color: var(--primary-color);">pinecone.io</a>
|
| 72 |
-
(Serverless, Dimension: 768, Metric: Cosine)
|
| 73 |
-
</p>
|
| 74 |
-
|
| 75 |
-
<label style="color: var(--text-main); font-weight: 500; font-size: 14px;">Pinecone API Key
|
| 76 |
-
(Required)</label>
|
| 77 |
-
<input type="password" id="pineconeKey" name="pinecone_key" placeholder="Enter your Pinecone API Key..."
|
| 78 |
-
value="" data-has-key="{{ 'true' if current_user.pinecone_api_key else 'false' }}" style="margin-top: 10px;">
|
| 79 |
-
|
| 80 |
-
<label style="color: var(--text-main); font-weight: 500; font-size: 14px;">Pinecone Index Name
|
| 81 |
-
(Required)</label>
|
| 82 |
-
<input type="text" id="pineconeIndex" name="pinecone_index" placeholder="e.g. rag-app"
|
| 83 |
-
value="{{ current_user.pinecone_index_name or '' }}" style="margin-top: 10px;">
|
| 84 |
-
|
| 85 |
-
<button type="submit" style="width: 100%; margin-top: 15px;">Save Settings</button>
|
| 86 |
-
</form>
|
| 87 |
-
<div id="settingsStatus" style="margin-top: 15px; font-weight: 500;"></div>
|
| 88 |
-
</div>
|
| 89 |
-
|
| 90 |
-
<!-- ── Uploaded Files List ── -->
|
| 91 |
-
<div class="files-box">
|
| 92 |
-
<h2>📋 My Uploaded Files</h2>
|
| 93 |
-
<ul id="filesList">
|
| 94 |
-
<!-- Files appear here -->
|
| 95 |
-
</ul>
|
| 96 |
-
</div>
|
| 97 |
-
</div>
|
| 98 |
-
|
| 99 |
-
<script>
|
| 100 |
-
async function uploadProfilePic(input) {
|
| 101 |
-
const file = input.files[0]
|
| 102 |
-
if (!file) return
|
| 103 |
-
|
| 104 |
-
const formData = new FormData()
|
| 105 |
-
formData.append("profile_pic", file)
|
| 106 |
-
|
| 107 |
-
const response = await fetch("/upload_profile_pic", {
|
| 108 |
-
method: "POST",
|
| 109 |
-
body: formData
|
| 110 |
-
})
|
| 111 |
-
|
| 112 |
-
const data = await response.json()
|
| 113 |
-
|
| 114 |
-
if (response.ok) {
|
| 115 |
-
document.getElementById("profilePic").src = data.profile_pic + "?t=" + Date.now()
|
| 116 |
-
} else {
|
| 117 |
-
alert("❌ " + data.error)
|
| 118 |
-
}
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
// Show placeholder for existing keys
|
| 122 |
-
document.querySelectorAll('[data-has-key="true"]').forEach(input => {
|
| 123 |
-
input.placeholder = "•••••••• (key saved — leave blank to keep)"
|
| 124 |
-
})
|
| 125 |
-
|
| 126 |
-
document.getElementById("settingsForm").addEventListener("submit", async (e) => {
|
| 127 |
-
e.preventDefault()
|
| 128 |
-
const statusDiv = document.getElementById("settingsStatus")
|
| 129 |
-
statusDiv.innerText = "Saving..."
|
| 130 |
-
statusDiv.style.color = "var(--primary-color)"
|
| 131 |
-
|
| 132 |
-
const preferredModel = document.getElementById("preferredModel").value
|
| 133 |
-
const groqKey = document.getElementById("groqKey").value.trim()
|
| 134 |
-
const geminiKey = document.getElementById("geminiKey").value.trim()
|
| 135 |
-
const pineconeKey = document.getElementById("pineconeKey").value.trim()
|
| 136 |
-
const pineconeIndex = document.getElementById("pineconeIndex").value.trim()
|
| 137 |
-
|
| 138 |
-
// Only send keys if user actually typed a new value
|
| 139 |
-
const payload = { preferred_model: preferredModel, pinecone_index: pineconeIndex }
|
| 140 |
-
if (groqKey) payload.groq_key = groqKey
|
| 141 |
-
if (geminiKey) payload.gemini_key = geminiKey
|
| 142 |
-
if (pineconeKey) payload.pinecone_key = pineconeKey
|
| 143 |
-
|
| 144 |
-
const response = await fetch("/update_settings", {
|
| 145 |
-
method: "POST",
|
| 146 |
-
headers: { "Content-Type": "application/json" },
|
| 147 |
-
body: JSON.stringify(payload)
|
| 148 |
-
})
|
| 149 |
-
|
| 150 |
-
const data = await response.json()
|
| 151 |
-
if (response.ok) {
|
| 152 |
-
statusDiv.innerText = "✅ " + data.message
|
| 153 |
-
statusDiv.style.color = "var(--success-color)"
|
| 154 |
-
} else {
|
| 155 |
-
statusDiv.innerText = "❌ " + data.error
|
| 156 |
-
statusDiv.style.color = "var(--danger-color)"
|
| 157 |
-
}
|
| 158 |
-
})
|
| 159 |
-
|
| 160 |
-
async function loadFiles() {
|
| 161 |
-
const response = await fetch("/files")
|
| 162 |
-
const data = await response.json()
|
| 163 |
-
const filesList = document.getElementById("filesList")
|
| 164 |
-
filesList.innerHTML = ""
|
| 165 |
-
|
| 166 |
-
if (data.files.length === 0) {
|
| 167 |
-
filesList.innerHTML = "<li>No files uploaded yet</li>"
|
| 168 |
-
return
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
data.files.forEach(file => {
|
| 172 |
-
let icon = "📄"
|
| 173 |
-
if (file.endsWith(".docx")) icon = "📝"
|
| 174 |
-
if (file.endsWith(".txt")) icon = "📃"
|
| 175 |
-
if (file.endsWith(".md")) icon = "📑"
|
| 176 |
-
|
| 177 |
-
filesList.innerHTML += `
|
| 178 |
-
<li class="file-item">
|
| 179 |
-
<span>${icon} ${file}</span>
|
| 180 |
-
<button class="delete-btn" onclick="deleteFile('${file}')">🗑️ Delete</button>
|
| 181 |
-
</li>
|
| 182 |
-
`
|
| 183 |
-
})
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
async function deleteFile(filename) {
|
| 187 |
-
if (!confirm(`Are you sure you want to delete ${filename}?`)) return
|
| 188 |
-
const response = await fetch("/delete", {
|
| 189 |
-
method: "POST",
|
| 190 |
-
headers: { "Content-Type": "application/json" },
|
| 191 |
-
body: JSON.stringify({ filename: filename })
|
| 192 |
-
})
|
| 193 |
-
const data = await response.json()
|
| 194 |
-
if (response.ok) {
|
| 195 |
-
if (localStorage.getItem("currentFile") === filename) {
|
| 196 |
-
localStorage.removeItem("currentFile")
|
| 197 |
-
}
|
| 198 |
-
loadFiles()
|
| 199 |
-
} else {
|
| 200 |
-
alert("❌ " + data.error)
|
| 201 |
-
}
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
loadFiles()
|
| 205 |
-
</script>
|
| 206 |
-
</body>
|
| 207 |
-
|
| 208 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/register.html
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8">
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>Register</title>
|
| 8 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 9 |
-
</head>
|
| 10 |
-
|
| 11 |
-
<body>
|
| 12 |
-
<header>
|
| 13 |
-
<h1>📄 PDF Assistant RAG</h1>
|
| 14 |
-
</header>
|
| 15 |
-
|
| 16 |
-
<div class="container">
|
| 17 |
-
<div class="auth-box">
|
| 18 |
-
<h2>Create Account</h2>
|
| 19 |
-
|
| 20 |
-
{% if error %}
|
| 21 |
-
<div class="error-msg">❌ {{ error }}</div>
|
| 22 |
-
{% endif %}
|
| 23 |
-
|
| 24 |
-
<form method="POST" action="/register">
|
| 25 |
-
<input type="text" name="username" placeholder="Username" required />
|
| 26 |
-
<input type="email" name="email" placeholder="Email" required />
|
| 27 |
-
<input type="password" name="password" placeholder="Password" required />
|
| 28 |
-
<button type="submit">Register</button>
|
| 29 |
-
</form>
|
| 30 |
-
|
| 31 |
-
<p>Already have an account? <a href="/login">Login</a></p>
|
| 32 |
-
</div>
|
| 33 |
-
</div>
|
| 34 |
-
|
| 35 |
-
<!-- ── Footer ── -->
|
| 36 |
-
<footer style="text-align: center; padding: 20px; color: var(--text-muted); margin-top: 40px; font-size: 14px;">
|
| 37 |
-
<p>© 2026 RAG Assistant. Param20h</p>
|
| 38 |
-
</footer>
|
| 39 |
-
</body>
|
| 40 |
-
|
| 41 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|