harishvijayasarangan05 commited on
Commit
964d67a
·
verified ·
1 Parent(s): 8e8f469

Upload 9 files

Browse files
Files changed (10) hide show
  1. .gitattributes +2 -0
  2. Dockerfile +22 -0
  3. b.png +3 -0
  4. main.py +164 -0
  5. requirements.txt +13 -0
  6. static/.DS_Store +0 -0
  7. static/b.png +3 -0
  8. static/index.html +66 -0
  9. static/script.js +380 -0
  10. static/styles.css +690 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ b.png filter=lfs diff=lfs merge=lfs -text
37
+ static/b.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy requirements first for better caching
6
+ COPY requirements.txt .
7
+
8
+ # Install dependencies
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # Copy the rest of the application
12
+ COPY . .
13
+
14
+ # Create static directory
15
+ RUN mkdir -p static
16
+
17
+ # Set environment variables for HF Spaces
18
+ ENV HOST=0.0.0.0
19
+ ENV PORT=7860
20
+
21
+ # Command to run the application
22
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
b.png ADDED

Git LFS Details

  • SHA256: d6d2df84076d8ba7fa6f5c99c5a3c554e66f83f1c45e32775ec24ed6f115dcb1
  • Pointer size: 131 Bytes
  • Size of remote file: 414 kB
main.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import fitz # PyMuPDF
3
+ import uuid
4
+ from fastapi import FastAPI, UploadFile, File, Form, Request
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.staticfiles import StaticFiles
7
+ from fastapi.responses import HTMLResponse, JSONResponse
8
+ from pydantic import BaseModel
9
+ from typing import List
10
+ from dotenv import load_dotenv
11
+
12
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
13
+ from langchain_community.vectorstores import Chroma
14
+ from langchain_community.embeddings import HuggingFaceEmbeddings
15
+ from langchain_core.documents import Document
16
+
17
+ from anthropic import Anthropic
18
+
19
+ # ---- Load API Keys ----
20
+ load_dotenv()
21
+ ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
22
+ CLAUDE_MODEL = "claude-3-haiku-20240307"
23
+
24
+ # ---- App Init ----
25
+ app = FastAPI()
26
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
27
+
28
+ # Create static directory if it doesn't exist
29
+ os.makedirs(os.path.join(os.path.dirname(__file__), "static"), exist_ok=True)
30
+
31
+ # Mount static files directory
32
+ app.mount("/static", StaticFiles(directory="static"), name="static")
33
+
34
+ # ---- In-Memory Stores ----
35
+ db_store = {}
36
+ chat_store = {}
37
+ general_chat_sessions = {}
38
+
39
+ # ---- Utils ----
40
+
41
+ def extract_text_from_pdf(file) -> str:
42
+ """Extracts text from the first page of a PDF."""
43
+ doc = fitz.open(stream=file.file.read(), filetype="pdf")
44
+ return doc[0].get_text()
45
+
46
+ def build_vector_db(text: str, collection_name: str) -> Chroma:
47
+ """Chunks, embeds, and stores text in ChromaDB."""
48
+ splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
49
+ docs = splitter.create_documents([text])
50
+
51
+ # Using a standard model that should be available publicly
52
+ embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
53
+ vectordb = Chroma.from_documents(docs, embeddings, collection_name=collection_name)
54
+ return vectordb
55
+
56
+ def retrieve_context(vectordb: Chroma, query: str, k: int = 3) -> str:
57
+ """Fetches top-k similar chunks from the vector DB."""
58
+ docs = vectordb.similarity_search(query, k=k)
59
+ return "\n\n".join([d.page_content for d in docs])
60
+
61
+ def create_session(is_pdf: bool = True) -> str:
62
+ """Creates a new unique session ID."""
63
+ sid = str(uuid.uuid4())
64
+ chat_store[sid] = []
65
+
66
+ # Track if this is a general chat session (without PDF)
67
+ if not is_pdf:
68
+ general_chat_sessions[sid] = True
69
+
70
+ return sid
71
+
72
+ def append_chat(session_id: str, role: str, msg: str):
73
+ chat_store[session_id].append({"role": role, "text": msg})
74
+
75
+ def get_chat(session_id: str):
76
+ return chat_store.get(session_id, [])
77
+
78
+ def delete_session(session_id: str):
79
+ chat_store.pop(session_id, None)
80
+ db_store.pop(session_id, None)
81
+ general_chat_sessions.pop(session_id, None)
82
+
83
+ # ---- API Routes ----
84
+
85
+ @app.get("/", response_class=HTMLResponse)
86
+ async def get_home():
87
+ with open(os.path.join(os.path.dirname(__file__), "static", "index.html")) as f:
88
+ return f.read()
89
+
90
+ @app.post("/start-chat/")
91
+ async def start_general_chat():
92
+ """Starts a general chat session without PDF."""
93
+ session_id = create_session(is_pdf=False)
94
+ return {"session_id": session_id, "message": "General chat session started."}
95
+
96
+ @app.post("/upload/")
97
+ async def upload_pdf(file: UploadFile = File(...), current_session_id: str = Form(None)):
98
+ """Handles PDF upload and indexing with chat continuity."""
99
+ # Extract text from PDF
100
+ text = extract_text_from_pdf(file)
101
+
102
+ # Handle session continuity
103
+ if current_session_id and current_session_id in chat_store:
104
+ # Continue with existing session
105
+ session_id = current_session_id
106
+ # Remove from general chat sessions if it was one
107
+ if session_id in general_chat_sessions:
108
+ general_chat_sessions.pop(session_id)
109
+ else:
110
+ # Create a new session
111
+ session_id = create_session()
112
+
113
+ # Create and store the vector database
114
+ vectordb = build_vector_db(text, collection_name=session_id)
115
+ db_store[session_id] = vectordb
116
+
117
+ return {"session_id": session_id, "message": "PDF indexed."}
118
+
119
+ @app.post("/chat/")
120
+ async def chat(session_id: str = Form(...), prompt: str = Form(...)):
121
+ """Handles user chat prompt, fetches relevant info, calls Claude."""
122
+ # Check if this is a general chat or PDF chat
123
+ is_general_chat = session_id in general_chat_sessions
124
+ is_pdf_chat = session_id in db_store
125
+
126
+ if not is_general_chat and not is_pdf_chat:
127
+ return {"error": "Invalid session ID"}
128
+
129
+ append_chat(session_id, "user", prompt)
130
+
131
+ # Ensure we have an API key and initialize with proper parameters
132
+ if not ANTHROPIC_API_KEY:
133
+ return JSONResponse(status_code=500, content={"error": "Missing ANTHROPIC_API_KEY environment variable"})
134
+
135
+ client = Anthropic(api_key=ANTHROPIC_API_KEY.strip())
136
+
137
+ if is_general_chat:
138
+ # General chat without PDF context
139
+ response = client.messages.create(
140
+ model=CLAUDE_MODEL,
141
+ max_tokens=512,
142
+ temperature=0.5,
143
+ messages=[{"role": "user", "content": prompt}]
144
+ )
145
+ else:
146
+ # PDF-based chat with context
147
+ context = retrieve_context(db_store[session_id], prompt)
148
+ response = client.messages.create(
149
+ model=CLAUDE_MODEL,
150
+ max_tokens=512,
151
+ temperature=0.5,
152
+ messages=[{"role": "user", "content": f"Context:\n{context}\n\nQuestion:\n{prompt}"}]
153
+ )
154
+
155
+ answer = response.content[0].text
156
+ append_chat(session_id, "bot", answer)
157
+
158
+ return {"answer": answer, "chat_history": get_chat(session_id)}
159
+
160
+ @app.post("/end/")
161
+ async def end_chat(session_id: str = Form(...)):
162
+ """Ends session and deletes associated data."""
163
+ delete_session(session_id)
164
+ return {"message": "Session cleared."}
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pymupdf
4
+ langchain
5
+ langchain-text-splitters
6
+ langchain-community
7
+ chromadb
8
+ anthropic
9
+ huggingface_hub
10
+ sentence-transformers
11
+ tqdm
12
+ python-multipart
13
+ python-dotenv
static/.DS_Store ADDED
Binary file (6.15 kB). View file
 
static/b.png ADDED

Git LFS Details

  • SHA256: d195234b1d7a4581277ae27fe9db4fe480585ab001f9994a58d8915b77b60d83
  • Pointer size: 132 Bytes
  • Size of remote file: 2.12 MB
static/index.html ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>RAG - PDF Chat</title>
7
+ <link rel="stylesheet" href="/static/styles.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <!-- Full Width Chat Interface -->
13
+ <div class="chat-interface">
14
+ <div class="chat-header">
15
+ <div class="title-area">
16
+ <h1>RAG</h1>
17
+ <p class="tagline">Your PDF Assistant</p>
18
+ </div>
19
+ <div class="status" id="status">
20
+ <span class="status-dot offline"></span>
21
+ <span class="status-text">Starting chat...</span>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="chat-container">
26
+ <div class="chat-messages" id="chat-messages">
27
+ <div class="message bot-message">
28
+ <div class="message-content">
29
+ <p>Welcome ! I'm ready to chat. You can also upload a PDF to get context-aware answers.</p>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="pdf-indicator" id="pdf-info" style="display: none;">
35
+ <i class="fas fa-file-pdf"></i>
36
+ <span id="pdf-name">No file selected</span>
37
+ <button id="new-pdf-btn" class="new-pdf-btn">
38
+ <i class="fas fa-sync-alt"></i>
39
+ </button>
40
+ </div>
41
+
42
+ <div class="chat-input-container">
43
+ <div class="chat-actions">
44
+ <div class="pdf-arrow" id="pdf-arrow">
45
+ <i class="fas fa-arrow-down"></i>
46
+ </div>
47
+ <label for="pdf-upload" class="pdf-upload-btn" title="Upload PDF">
48
+ <i class="fas fa-file-pdf"></i>
49
+ </label>
50
+ <input type="file" id="pdf-upload" accept=".pdf" hidden>
51
+ </div>
52
+ <textarea id="chat-input" placeholder="Type your message..." disabled></textarea>
53
+ <button id="send-button" disabled>
54
+ <i class="fas fa-paper-plane"></i>
55
+ </button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Notifications -->
62
+ <div class="notification-container" id="notification-container"></div>
63
+
64
+ <script src="/static/script.js"></script>
65
+ </body>
66
+ </html>
static/script.js ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Global Variables
2
+ let sessionId = null;
3
+ let currentPdfName = '';
4
+
5
+ // DOM Elements
6
+ const chatMessages = document.getElementById('chat-messages');
7
+ const chatInput = document.getElementById('chat-input');
8
+ const sendButton = document.getElementById('send-button');
9
+ const pdfUpload = document.getElementById('pdf-upload');
10
+ const pdfInfo = document.getElementById('pdf-info');
11
+ const pdfNameElement = document.getElementById('pdf-name');
12
+ const newPdfButton = document.getElementById('new-pdf-btn');
13
+ const status = document.getElementById('status');
14
+ const notificationContainer = document.getElementById('notification-container');
15
+ const chatContainer = document.querySelector('.chat-container');
16
+
17
+ // Event Listeners
18
+ document.addEventListener('DOMContentLoaded', () => {
19
+ // Start general chat automatically
20
+ startGeneralChat();
21
+
22
+ // Add drop event to the entire chat container
23
+ chatContainer.addEventListener('dragover', (e) => {
24
+ e.preventDefault();
25
+ chatContainer.classList.add('dragover');
26
+ showNotification('Ready', 'Drop PDF here to upload', 'info', 1500);
27
+ });
28
+
29
+ chatContainer.addEventListener('dragleave', () => {
30
+ chatContainer.classList.remove('dragover');
31
+ });
32
+
33
+ chatContainer.addEventListener('drop', (e) => {
34
+ e.preventDefault();
35
+ chatContainer.classList.remove('dragover');
36
+
37
+ const files = e.dataTransfer.files;
38
+ if (files.length > 0 && files[0].type === 'application/pdf') {
39
+ handleFileUpload(files[0]);
40
+ } else {
41
+ showNotification('Error', 'Please upload a PDF file', 'error');
42
+ }
43
+ });
44
+
45
+ // Add click handler for the PDF upload button in the chat
46
+ document.querySelector('.pdf-upload-btn').addEventListener('click', (e) => {
47
+ e.preventDefault();
48
+ // Hide the arrow when PDF button is clicked
49
+ const pdfArrow = document.getElementById('pdf-arrow');
50
+ if (pdfArrow) {
51
+ pdfArrow.style.display = 'none';
52
+ }
53
+ pdfUpload.click();
54
+ });
55
+
56
+ pdfUpload.addEventListener('change', (e) => {
57
+ e.preventDefault(); // Prevent form submission
58
+ if (e.target.files.length > 0) {
59
+ handleFileUpload(e.target.files[0]);
60
+ }
61
+ return false;
62
+ });
63
+
64
+ // Chat Input
65
+ chatInput.addEventListener('keypress', (e) => {
66
+ if (e.key === 'Enter' && !e.shiftKey) {
67
+ e.preventDefault();
68
+ if (chatInput.value.trim() !== '') {
69
+ handleSendMessage();
70
+ }
71
+ }
72
+ });
73
+
74
+ sendButton.addEventListener('click', () => {
75
+ if (chatInput.value.trim() !== '') {
76
+ handleSendMessage();
77
+ }
78
+ });
79
+
80
+ // New PDF Upload
81
+ newPdfButton.addEventListener('click', () => {
82
+ resetChat();
83
+ showUploadInterface();
84
+ });
85
+ });
86
+
87
+ // Functions
88
+ function handleFileUpload(file) {
89
+ // Prevent default behaviors
90
+ event?.preventDefault?.();
91
+
92
+ // Update UI to show loading state
93
+ updateStatus('loading', 'Processing PDF...');
94
+ showNotification('Processing', 'Uploading and analyzing your PDF...', 'info');
95
+
96
+ const formData = new FormData();
97
+ formData.append('file', file);
98
+ formData.append('current_session_id', sessionId || ''); // Pass current session ID for continuity
99
+ currentPdfName = file.name;
100
+
101
+ // Add PDF loaded class to chat container
102
+ document.querySelector('.chat-interface').classList.add('pdf-loaded');
103
+
104
+ // Upload the file
105
+ fetch('/upload/', {
106
+ method: 'POST',
107
+ body: formData,
108
+ // Ensure we don't navigate away
109
+ redirect: 'follow',
110
+ mode: 'cors',
111
+ cache: 'no-cache'
112
+ })
113
+ .then(response => {
114
+ if (!response.ok) throw new Error('Failed to upload PDF');
115
+ return response.json();
116
+ })
117
+ .then(data => {
118
+ sessionId = data.session_id;
119
+
120
+ // Update UI
121
+ updateStatus('online', 'PDF Loaded');
122
+ showPdfInfo();
123
+ enableChat();
124
+ showNotification('Success', 'PDF successfully processed! You can now chat about its content.', 'success');
125
+
126
+ // Add system message to chat
127
+ addBotMessage('I\'ve analyzed your PDF and can now provide context-aware answers.');
128
+ })
129
+ .catch(error => {
130
+ console.error('Error:', error);
131
+ updateStatus('offline', 'Upload Failed');
132
+ showNotification('Error', 'Failed to process PDF. Please try again.', 'error');
133
+ });
134
+ }
135
+
136
+ function handleSendMessage() {
137
+ const message = chatInput.value.trim();
138
+ if (message === '' || !sessionId) return;
139
+
140
+ // Add user message to chat
141
+ addUserMessage(message);
142
+
143
+ // Clear input
144
+ chatInput.value = '';
145
+ chatInput.style.height = 'auto';
146
+
147
+ // Disable input while waiting for response
148
+ disableChat();
149
+
150
+ // Add loading indicator
151
+ const loadingMsgId = addBotLoadingMessage();
152
+
153
+ // Send message to API
154
+ const formData = new FormData();
155
+ formData.append('session_id', sessionId);
156
+ formData.append('prompt', message);
157
+
158
+ fetch('/chat/', {
159
+ method: 'POST',
160
+ body: formData
161
+ })
162
+ .then(response => {
163
+ if (!response.ok) throw new Error('Failed to get response');
164
+ return response.json();
165
+ })
166
+ .then(data => {
167
+ // Remove loading message
168
+ removeMessage(loadingMsgId);
169
+
170
+ // Add bot response
171
+ addBotMessage(data.answer);
172
+
173
+ // Enable input
174
+ enableChat();
175
+ })
176
+ .catch(error => {
177
+ console.error('Error:', error);
178
+ removeMessage(loadingMsgId);
179
+ addBotMessage('Sorry, I encountered an error while processing your request. Please try again.');
180
+ enableChat();
181
+ showNotification('Error', 'Failed to get a response. Please try again.', 'error');
182
+ });
183
+ }
184
+
185
+ function addUserMessage(message) {
186
+ const messageElement = document.createElement('div');
187
+ messageElement.classList.add('message', 'user-message');
188
+
189
+ const messageContent = document.createElement('div');
190
+ messageContent.classList.add('message-content');
191
+
192
+ const messageText = document.createElement('p');
193
+ messageText.textContent = message;
194
+
195
+ messageContent.appendChild(messageText);
196
+ messageElement.appendChild(messageContent);
197
+ chatMessages.appendChild(messageElement);
198
+
199
+ scrollToBottom();
200
+ }
201
+
202
+ function addBotMessage(message) {
203
+ const messageElement = document.createElement('div');
204
+ messageElement.classList.add('message', 'bot-message');
205
+
206
+ const messageContent = document.createElement('div');
207
+ messageContent.classList.add('message-content');
208
+
209
+ const messageText = document.createElement('p');
210
+ messageText.textContent = message;
211
+
212
+ messageContent.appendChild(messageText);
213
+ messageElement.appendChild(messageContent);
214
+ chatMessages.appendChild(messageElement);
215
+
216
+ scrollToBottom();
217
+ }
218
+
219
+ function addBotLoadingMessage() {
220
+ const messageId = 'msg-' + Date.now();
221
+
222
+ const messageElement = document.createElement('div');
223
+ messageElement.classList.add('message', 'bot-message');
224
+ messageElement.id = messageId;
225
+
226
+ const messageContent = document.createElement('div');
227
+ messageContent.classList.add('message-content');
228
+
229
+ const loadingSpinner = document.createElement('div');
230
+ loadingSpinner.classList.add('loading-spinner');
231
+
232
+ messageContent.appendChild(loadingSpinner);
233
+ messageElement.appendChild(messageContent);
234
+ chatMessages.appendChild(messageElement);
235
+
236
+ scrollToBottom();
237
+
238
+ return messageId;
239
+ }
240
+
241
+ function removeMessage(messageId) {
242
+ const message = document.getElementById(messageId);
243
+ if (message) message.remove();
244
+ }
245
+
246
+ function scrollToBottom() {
247
+ chatMessages.scrollTop = chatMessages.scrollHeight;
248
+ }
249
+
250
+ function updateStatus(state, text) {
251
+ const statusDot = status.querySelector('.status-dot');
252
+ const statusText = status.querySelector('.status-text');
253
+
254
+ statusDot.className = 'status-dot ' + state;
255
+ statusText.textContent = text;
256
+ }
257
+
258
+ function enableChat() {
259
+ chatInput.disabled = false;
260
+ sendButton.disabled = false;
261
+ chatInput.focus();
262
+ }
263
+
264
+ function disableChat() {
265
+ chatInput.disabled = true;
266
+ sendButton.disabled = true;
267
+ }
268
+
269
+ function resetChat() {
270
+ if (sessionId) {
271
+ // End the session on the server
272
+ const formData = new FormData();
273
+ formData.append('session_id', sessionId);
274
+
275
+ fetch('/end/', {
276
+ method: 'POST',
277
+ body: formData
278
+ }).catch(error => console.error('Error ending session:', error));
279
+ }
280
+
281
+ // Reset variables
282
+ sessionId = null;
283
+ currentPdfName = '';
284
+
285
+ // Clear chat
286
+ chatMessages.innerHTML = '';
287
+ addBotMessage('Welcome to RAG! I\'m ready to chat. You can also upload a PDF to get context-aware answers.');
288
+
289
+ // Start a new general chat automatically
290
+ startGeneralChat();
291
+ }
292
+
293
+ function showUploadInterface() {
294
+ uploadContainer.style.display = 'flex';
295
+ pdfInfo.style.display = 'none';
296
+ }
297
+
298
+ function startGeneralChat() {
299
+ // Update UI
300
+ updateStatus('online', 'Chat Ready');
301
+
302
+ // Make API call to start a general chat session
303
+ fetch('/start-chat/', {
304
+ method: 'POST'
305
+ })
306
+ .then(response => {
307
+ if (!response.ok) throw new Error('Failed to start chat');
308
+ return response.json();
309
+ })
310
+ .then(data => {
311
+ sessionId = data.session_id;
312
+ enableChat();
313
+ // Show welcome message indicating PDF upload option
314
+ addBotMessage('I\'m ready to help! You can chat with me directly or upload a PDF using the paperclip icon for context-aware responses.');
315
+ })
316
+ .catch(error => {
317
+ console.error('Error:', error);
318
+ showNotification('Error', 'Failed to start chat. Please try again.', 'error');
319
+ });
320
+ }
321
+
322
+ function showPdfInfo() {
323
+ // Truncate long filenames
324
+ const truncatedName = currentPdfName.length > 25 ?
325
+ currentPdfName.substring(0, 22) + '...' :
326
+ currentPdfName;
327
+
328
+ pdfNameElement.textContent = truncatedName;
329
+ pdfInfo.style.display = 'flex';
330
+
331
+ // Update status to show PDF is loaded
332
+ updateStatus('online', 'PDF loaded');
333
+
334
+ // Add tooltip for full filename
335
+ pdfNameElement.title = currentPdfName;
336
+ }
337
+
338
+ function showNotification(title, message, type = 'info', duration = 5000) {
339
+ const notificationId = Date.now();
340
+
341
+ const notification = document.createElement('div');
342
+ notification.classList.add('notification', type);
343
+ notification.id = `notification-${notificationId}`;
344
+
345
+ const notificationTitle = document.createElement('h4');
346
+ notificationTitle.textContent = title;
347
+
348
+ const notificationMessage = document.createElement('p');
349
+ notificationMessage.textContent = message;
350
+
351
+ const closeButton = document.createElement('button');
352
+ closeButton.classList.add('close-notification');
353
+ closeButton.innerHTML = '&times;';
354
+ closeButton.addEventListener('click', () => {
355
+ closeNotification(notificationId);
356
+ });
357
+
358
+ notification.appendChild(notificationTitle);
359
+ notification.appendChild(notificationMessage);
360
+ notification.appendChild(closeButton);
361
+
362
+ notificationContainer.appendChild(notification);
363
+
364
+ // Auto-close after specified duration
365
+ setTimeout(() => {
366
+ closeNotification(notificationId);
367
+ }, duration);
368
+
369
+ return notificationId;
370
+ }
371
+
372
+ function closeNotification(id) {
373
+ const notification = document.getElementById(`notification-${id}`);
374
+ if (notification) {
375
+ notification.classList.add('notification-hide');
376
+ setTimeout(() => {
377
+ notification.remove();
378
+ }, 300);
379
+ }
380
+ }
static/styles.css ADDED
@@ -0,0 +1,690 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Global Styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ font-family: 'Inter', 'Söhne', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
7
+ }
8
+
9
+ body {
10
+ background: url('/static/b.png?v=2');
11
+ background-size: cover;
12
+ background-position: center;
13
+ background-attachment: fixed;
14
+ height: 100vh;
15
+ overflow: hidden;
16
+ color: #333; /* Dark text instead of white */
17
+ line-height: 1.5;
18
+ font-size: 15px;
19
+ text-shadow: none; /* Removing text shadow */
20
+ }
21
+
22
+ .container {
23
+ height: 100vh;
24
+ background-color: transparent; /* No background */
25
+ overflow: hidden;
26
+ display: flex;
27
+ max-width: none; /* Full width */
28
+ width: 100%;
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ /* Chat Interface Styles */
34
+ .chat-interface {
35
+ width: 100%;
36
+ background-color: transparent;
37
+ color: #343541;
38
+ display: flex;
39
+ flex-direction: column;
40
+ height: 100%;
41
+ position: relative;
42
+ padding: 0;
43
+ margin: 0;
44
+ }
45
+
46
+ .chat-header {
47
+ padding: 15px 20px;
48
+ border-bottom: none;
49
+ display: flex;
50
+ justify-content: space-between;
51
+ align-items: center;
52
+ background-color: rgba(255, 255, 255, 0.7);
53
+ backdrop-filter: blur(10px);
54
+ -webkit-backdrop-filter: blur(10px);
55
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
56
+ z-index: 10;
57
+ position: relative;
58
+ border-radius: 0;
59
+ margin: 0;
60
+ width: 100%;
61
+ box-sizing: border-box;
62
+ border-top: 1px solid rgba(255, 255, 255, 0.5);
63
+ border-bottom: 1px solid rgba(225, 225, 225, 0.3);
64
+ }
65
+
66
+ .title-area {
67
+ display: flex;
68
+ align-items: center;
69
+ }
70
+
71
+ .title-area h1 {
72
+ font-size: 22px;
73
+ color: #111;
74
+ margin-right: 15px;
75
+ font-weight: 700;
76
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
77
+ letter-spacing: -0.5px;
78
+ }
79
+
80
+ .tagline {
81
+ font-size: 14px;
82
+ color: #333;
83
+ font-weight: 500;
84
+ }
85
+
86
+ .notification-message {
87
+ font-size: 14px;
88
+ color: #6e6e80;
89
+ line-height: 1.4;
90
+ }
91
+
92
+ /* Chat Container */
93
+ .chat-container {
94
+ display: flex;
95
+ flex-direction: column;
96
+ flex: 1;
97
+ overflow: hidden;
98
+ max-width: 100%;
99
+ margin: 0;
100
+ width: 100%;
101
+ padding-bottom: 80px; /* Space for fixed input */
102
+ padding-left: 0;
103
+ padding-right: 0;
104
+ padding-top: 15px; /* Additional spacing after header */
105
+ border-top: none;
106
+ }
107
+
108
+ .chat-messages {
109
+ flex: 1;
110
+ overflow-y: auto;
111
+ padding-bottom: 80px; /* Space for input */
112
+ padding: 0 20px; /* Match the header's horizontal padding */
113
+ margin: 20px 0 0 0; /* Add top margin to move content down */
114
+ scroll-behavior: smooth;
115
+ background-color: transparent;
116
+ width: 100%;
117
+ box-sizing: border-box;
118
+ }
119
+
120
+ .message {
121
+ display: flex;
122
+ padding: 6px 0;
123
+ max-width: 100%;
124
+ width: 100%;
125
+ margin: 0;
126
+ }
127
+
128
+ .user-message {
129
+ background-color: transparent;
130
+ justify-content: flex-end; /* Position at right */
131
+ }
132
+
133
+ .user-message:hover {
134
+ background-color: rgba(255,255,255,0.25);
135
+ }
136
+
137
+ .bot-message {
138
+ background-color: transparent;
139
+ border: none;
140
+ display: flex;
141
+ width: 100%;
142
+ padding: 0;
143
+ margin: 0;
144
+ align-items: flex-start;
145
+ justify-content: flex-start;
146
+ }
147
+
148
+ .message-content {
149
+ padding: 12px 16px;
150
+ max-width: 800px;
151
+ margin: 0 auto;
152
+ width: 100%;
153
+ border-radius: 8px; /* More rectangular shape */
154
+ }
155
+
156
+ .user-message .message-content {
157
+ color: black;
158
+ background-color: #e1f5f0;
159
+ border-radius: 18px;
160
+ margin-right: 0;
161
+ margin-left: auto;
162
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
163
+ width: auto;
164
+ max-width: 85%;
165
+ box-sizing: border-box;
166
+ }
167
+
168
+ .bot-message .message-content {
169
+ color: #343541;
170
+ background-color: #fff;
171
+ border-radius: 18px;
172
+ margin: 4px 10px 4px 0;
173
+ padding: 12px 16px;
174
+ width: auto;
175
+ max-width: 85%;
176
+ box-sizing: border-box;
177
+ border: 1px solid #e5e5e5;
178
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
179
+ }
180
+
181
+ .message p {
182
+ margin-bottom: 12px;
183
+ }
184
+
185
+ .message p:last-child {
186
+ margin-bottom: 0;
187
+ }
188
+
189
+ .chat-input-container {
190
+ padding: 10px 12px 15px;
191
+ border: none;
192
+ display: flex;
193
+ align-items: center;
194
+ background-color: transparent;
195
+ position: fixed;
196
+ bottom: 0;
197
+ left: 0;
198
+ right: 0;
199
+ margin: 0 auto;
200
+ width: calc(100% - 40px);
201
+ z-index: 10;
202
+ margin-bottom: 15px;
203
+ }
204
+
205
+ .pdf-indicator {
206
+ display: flex;
207
+ align-items: center;
208
+ padding: 12px 16px;
209
+ background-color: rgba(255,255,255,0.25);
210
+ border: 1px solid rgba(16, 163, 127, 0.2);
211
+ color: #343541;
212
+ }
213
+
214
+ .chat-actions {
215
+ display: flex;
216
+ align-items: center;
217
+ margin-right: 5px;
218
+ position: relative;
219
+ }
220
+
221
+ /* Bouncing Arrow */
222
+ .pdf-arrow {
223
+ position: absolute;
224
+ top: -50px;
225
+ left: 50%;
226
+ transform: translateX(-50%);
227
+ color: #10a37f;
228
+ font-size: 36px;
229
+ animation: bounce 1.5s infinite;
230
+ z-index: 10;
231
+ }
232
+
233
+ @keyframes bounce {
234
+ 0%, 20%, 50%, 80%, 100% {
235
+ transform: translateX(-50%) translateY(0);
236
+ }
237
+ 40% {
238
+ transform: translateX(-50%) translateY(-20px);
239
+ }
240
+ 60% {
241
+ transform: translateX(-50%) translateY(-10px);
242
+ }
243
+ }
244
+
245
+ .pdf-upload-btn {
246
+ cursor: pointer;
247
+ color: #6e6e80;
248
+ font-size: 26px;
249
+ transition: all 0.2s;
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ width: 44px;
254
+ height: 44px;
255
+ border-radius: 8px;
256
+ background-color: transparent;
257
+ }
258
+
259
+ .pdf-upload-btn:hover {
260
+ color: #202123;
261
+ background-color: rgba(0,0,0,0.05);
262
+ }
263
+
264
+ .pdf-loaded .pdf-upload-btn {
265
+ color: #10a37f;
266
+ }
267
+
268
+ #chat-input {
269
+ flex: 1;
270
+ padding: 10px 15px;
271
+ border: none;
272
+ outline: none;
273
+ font-size: 15px;
274
+ border-radius: 20px;
275
+ background-color: rgba(255,255,255,0.25);
276
+ backdrop-filter: blur(5px);
277
+ transition: all 0.3s ease;
278
+ width: calc(100% - 50px);
279
+ color: white;
280
+ }
281
+
282
+ #chat-input:focus {
283
+ border-color: rgba(16, 163, 127, 0.5);
284
+ box-shadow: 0 0 1px 2px rgba(16, 163, 127, 0.25);
285
+ }
286
+
287
+ #chat-input::placeholder {
288
+ color: #8e8ea0;
289
+ }
290
+
291
+ #send-button {
292
+ background-color: rgba(16, 163, 127, 0.7);
293
+ color: white;
294
+ border: none;
295
+ border-radius: 50%;
296
+ width: 36px;
297
+ height: 36px;
298
+ font-size: 15px;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ cursor: pointer;
303
+ transition: all 0.2s ease;
304
+ margin-left: 10px;
305
+ flex-shrink: 0;
306
+ backdrop-filter: blur(3px);
307
+ }
308
+
309
+ #send-button:hover {
310
+ background-color: #0e906f;
311
+ }
312
+
313
+ #send-button:disabled {
314
+ background-color: rgba(16, 163, 127, 0.5);
315
+ cursor: not-allowed;
316
+ }
317
+
318
+ #send-button:disabled::before {
319
+ display: none;
320
+ }
321
+
322
+ .new-pdf-btn {
323
+ margin-left: auto;
324
+ background: none;
325
+ border: none;
326
+ color: #10a37f;
327
+ cursor: pointer;
328
+ display: flex;
329
+ align-items: center;
330
+ justify-content: center;
331
+ }
332
+
333
+ .close-btn, .notification-close {
334
+ background: none;
335
+ border: none;
336
+ cursor: pointer;
337
+ color: #8e8ea0;
338
+ transition: all 0.2s;
339
+ font-size: 14px;
340
+ padding: 4px;
341
+ border-radius: 4px;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ }
346
+
347
+ .close-btn:hover, .notification-close:hover {
348
+ color: #343541;
349
+ background-color: rgba(0,0,0,0.05);
350
+ }
351
+
352
+ /* Main Content Area */
353
+ .main-content {
354
+ flex: 1;
355
+ display: flex;
356
+ flex-direction: column;
357
+ padding: 30px;
358
+ background-color: transparent;
359
+ overflow-y: auto;
360
+ }
361
+
362
+ .header {
363
+ display: flex;
364
+ justify-content: space-between;
365
+ align-items: center;
366
+ margin-bottom: 30px;
367
+ }
368
+
369
+ .header h2 {
370
+ font-size: 24px;
371
+ color: #333;
372
+ }
373
+
374
+ .status {
375
+ display: flex;
376
+ align-items: center;
377
+ color: #8e8ea0;
378
+ font-size: 14px;
379
+ gap: 6px;
380
+ }
381
+
382
+ .loading-dot {
383
+ width: 8px;
384
+ height: 8px;
385
+ border-radius: 50%;
386
+ background-color: rgba(16, 163, 127, 0.5);
387
+ animation: dotPulse 1.5s infinite ease-in-out;
388
+ }
389
+
390
+ .loading {
391
+ background-color: #fbbf24;
392
+ }
393
+
394
+ .offline {
395
+ background-color: #f87171;
396
+ }
397
+
398
+ .online {
399
+ background-color: #10a37f;
400
+ }
401
+
402
+ /* Upload Container */
403
+ .upload-container {
404
+ display: flex;
405
+ flex: 1;
406
+ align-items: center;
407
+ justify-content: center;
408
+ }
409
+
410
+ .upload-box {
411
+ display: flex;
412
+ flex-direction: column;
413
+ align-items: center;
414
+ justify-content: center;
415
+ padding: 40px;
416
+ border: 2px dashed #ccc;
417
+ border-radius: 10px;
418
+ text-align: center;
419
+ cursor: pointer;
420
+ transition: all 0.3s ease;
421
+ max-width: 500px;
422
+ background-color: transparent;
423
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
424
+ }
425
+
426
+ .upload-box:hover {
427
+ border-color: #e94560;
428
+ background-color: #fafafa;
429
+ }
430
+
431
+ .upload-box.dragover {
432
+ border-color: #e94560;
433
+ background-color: #fff7f8;
434
+ }
435
+
436
+ .upload-icon {
437
+ font-size: 48px;
438
+ margin-bottom: 20px;
439
+ color: #e94560;
440
+ }
441
+
442
+ .upload-box h3 {
443
+ margin-bottom: 10px;
444
+ color: #333;
445
+ }
446
+
447
+ .upload-btn {
448
+ margin-top: 15px;
449
+ padding: 10px 20px;
450
+ background-color: #e94560;
451
+ color: white;
452
+ border: none;
453
+ border-radius: 5px;
454
+ cursor: pointer;
455
+ display: inline-block;
456
+ transition: all 0.2s ease;
457
+ }
458
+
459
+ .upload-btn:hover {
460
+ background-color: #d13152;
461
+ }
462
+
463
+ /* Chat Options */
464
+ .chat-options {
465
+ margin-top: 30px;
466
+ display: flex;
467
+ flex-direction: column;
468
+ align-items: center;
469
+ }
470
+
471
+ .separator {
472
+ margin: 20px 0;
473
+ font-weight: 500;
474
+ color: #666;
475
+ position: relative;
476
+ text-align: center;
477
+ width: 100%;
478
+ }
479
+
480
+ .separator::before,
481
+ .separator::after {
482
+ content: '';
483
+ position: absolute;
484
+ top: 50%;
485
+ width: 40%;
486
+ height: 1px;
487
+ background-color: #ddd;
488
+ }
489
+
490
+ .separator::before {
491
+ left: 0;
492
+ }
493
+
494
+ .separator::after {
495
+ right: 0;
496
+ }
497
+
498
+ .start-chat-btn {
499
+ padding: 12px 25px;
500
+ background-color: #0f3460;
501
+ color: white;
502
+ border: none;
503
+ border-radius: 5px;
504
+ cursor: pointer;
505
+ font-size: 16px;
506
+ font-weight: 500;
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 10px;
510
+ transition: all 0.2s ease;
511
+ }
512
+
513
+ .start-chat-btn:hover {
514
+ background-color: #0a2448;
515
+ }
516
+
517
+ /* PDF Info */
518
+ .pdf-indicator {
519
+ display: flex;
520
+ align-items: center;
521
+ padding: 8px 16px;
522
+ background-color: transparent;
523
+ border-top: 1px solid rgba(0,0,0,0.1);
524
+ color: #343541;
525
+ font-size: 14px;
526
+ max-width: 800px;
527
+ margin: 0 auto;
528
+ width: 100%;
529
+ }
530
+
531
+ .pdf-indicator i {
532
+ color: #10a37f;
533
+ margin-right: 10px;
534
+ font-size: 16px;
535
+ }
536
+
537
+ #pdf-name {
538
+ color: #343541;
539
+ font-size: 14px;
540
+ max-width: 300px;
541
+ white-space: nowrap;
542
+ overflow: hidden;
543
+ text-overflow: ellipsis;
544
+ flex: 1;
545
+ }
546
+
547
+ .loading-dots {
548
+ display: flex;
549
+ gap: 4px;
550
+ padding: 8px 0;
551
+ }
552
+
553
+ .new-pdf-btn {
554
+ margin-left: auto;
555
+ background: none;
556
+ border: none;
557
+ color: #10a37f;
558
+ cursor: pointer;
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ }
563
+
564
+ /* Notifications */
565
+ .notification-container {
566
+ position: fixed;
567
+ top: 20px;
568
+ right: 20px;
569
+ z-index: 1000;
570
+ display: flex;
571
+ flex-direction: column;
572
+ gap: 10px;
573
+ }
574
+
575
+ .notification {
576
+ padding: 12px 15px;
577
+ margin: 15px 0;
578
+ border-radius: 8px;
579
+ background-color: transparent;
580
+ position: relative;
581
+ border-left: 3px solid;
582
+ font-size: 14px;
583
+ animation: fadeIn 0.3s ease;
584
+ font-weight: 500;
585
+ color: white;
586
+ text-shadow: 0px 0px 2px rgba(0,0,0,0.5);
587
+ }
588
+
589
+ .notification.success {
590
+ border-left: 4px solid #10a37f;
591
+ background-color: rgba(16, 163, 127, 0.05);
592
+ }
593
+
594
+ .notification.error {
595
+ border-left: 4px solid #f87171;
596
+ background-color: rgba(248, 113, 113, 0.05);
597
+ }
598
+
599
+ .notification.info {
600
+ border-left: 4px solid #60a5fa;
601
+ background-color: rgba(96, 165, 250, 0.05);
602
+ }
603
+
604
+ .notification.warning {
605
+ border-left: 4px solid #fbbf24;
606
+ background-color: rgba(251, 191, 36, 0.05);
607
+ }
608
+
609
+ .notification-icon {
610
+ font-size: 20px;
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: center;
614
+ }
615
+
616
+ .notification-content {
617
+ flex: 1;
618
+ }
619
+
620
+ .notification-title {
621
+ font-weight: 600;
622
+ margin-bottom: 5px;
623
+ color: #202123;
624
+ font-size: 14px;
625
+ }
626
+
627
+ .notification-close {
628
+ cursor: pointer;
629
+ font-size: 16px;
630
+ color: #999;
631
+ transition: color 0.2s;
632
+ }
633
+
634
+ .notification-close:hover {
635
+ color: #343541;
636
+ background-color: rgba(0,0,0,0.05);
637
+ }
638
+
639
+ @keyframes slideIn {
640
+ 0% {
641
+ transform: translateX(20px);
642
+ opacity: 0;
643
+ }
644
+ 100% {
645
+ transform: translateX(0);
646
+ opacity: 1;
647
+ }
648
+ }
649
+
650
+ @keyframes dotPulse {
651
+ 0%, 100% {
652
+ transform: scale(0.7);
653
+ opacity: 0.5;
654
+ }
655
+ 50% {
656
+ transform: scale(1);
657
+ opacity: 1;
658
+ }
659
+ }
660
+
661
+ /* Responsive */
662
+ @media (max-width: 768px) {
663
+ .container {
664
+ flex-direction: column;
665
+ }
666
+
667
+ .sidebar {
668
+ width: 100%;
669
+ height: 50%;
670
+ }
671
+
672
+ .main-content {
673
+ padding: 15px;
674
+ }
675
+ }
676
+
677
+ /* Loading animation */
678
+ .loading-spinner {
679
+ display: inline-block;
680
+ width: 20px;
681
+ height: 20px;
682
+ border: 3px solid rgba(255,255,255,.3);
683
+ border-radius: 50%;
684
+ border-top-color: white;
685
+ animation: spin 1s ease-in-out infinite;
686
+ }
687
+
688
+ @keyframes spin {
689
+ to { transform: rotate(360deg); }
690
+ }