chandanasunkara commited on
Commit
108073e
·
2 Parent(s): c6ce4948180918

Merge branch 'main' into chroma_db

Browse files
multi_llm_chatbot_backend/app/api/routes.py CHANGED
@@ -6,6 +6,7 @@ from app.llm.gemini_client import GeminiClient
6
  from app.models.persona import Persona
7
  from app.core.orchestrator import ChatOrchestrator
8
  from app.core.seamless_orchestrator import SeamlessOrchestrator
 
9
  from pydantic import BaseModel
10
  from typing import Optional, List
11
  from fastapi import UploadFile, File
@@ -13,6 +14,7 @@ from app.utils.document_extractor import extract_text_from_file
13
  from app.core.orchestrator import answer_with_persona_context
14
  from app.utils.chroma_client import add_persona_doc
15
  import hashlib
 
16
 
17
  router = APIRouter()
18
 
@@ -138,20 +140,6 @@ llm = create_llm_client()
138
  chat_orchestrator = ChatOrchestrator()
139
  seamless_orchestrator = SeamlessOrchestrator(llm=llm)
140
 
141
- # Global context storage
142
- class GlobalSessionContext:
143
- def __init__(self):
144
- self.full_log: list[dict] = []
145
-
146
- def append(self, role: str, content: str):
147
- self.full_log.append({"role": role, "content": content})
148
-
149
- def filter_by_persona(self, persona_id: str):
150
- return self.full_log
151
-
152
- def clear(self):
153
- self.full_log = []
154
-
155
  session_context = GlobalSessionContext()
156
 
157
  def create_default_personas(llm_client: LLMClient):
@@ -501,6 +489,7 @@ async def get_current_model():
501
 
502
  @router.post("/upload-document")
503
  async def upload_document(file: UploadFile = File(...)):
 
504
  if file.content_type not in [
505
  "application/pdf",
506
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -509,16 +498,22 @@ async def upload_document(file: UploadFile = File(...)):
509
  raise HTTPException(status_code=400, detail="Unsupported file type.")
510
 
511
  try:
512
- # Read file content into memory
513
- contents = await file.read()
514
 
515
- # Now pass raw contents and file type to extractor
516
- content = extract_text_from_file(contents, file.content_type)
 
517
 
 
 
518
  if not content.strip():
519
  raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
520
 
 
521
  session_context.append("user", f"[Uploaded Document Content]\n{content.strip()}")
 
 
522
 
523
  return {"message": "Document uploaded and added to context successfully."}
524
 
@@ -553,4 +548,8 @@ async def ask_question(query: PersonaQuery):
553
 
554
  add_persona_doc(combined_text, query.persona, doc_id)
555
 
556
- return {"response": response}
 
 
 
 
 
6
  from app.models.persona import Persona
7
  from app.core.orchestrator import ChatOrchestrator
8
  from app.core.seamless_orchestrator import SeamlessOrchestrator
9
+ from app.core.context import GlobalSessionContext
10
  from pydantic import BaseModel
11
  from typing import Optional, List
12
  from fastapi import UploadFile, File
 
14
  from app.core.orchestrator import answer_with_persona_context
15
  from app.utils.chroma_client import add_persona_doc
16
  import hashlib
17
+ from app.utils.file_limits import is_within_upload_limit
18
 
19
  router = APIRouter()
20
 
 
140
  chat_orchestrator = ChatOrchestrator()
141
  seamless_orchestrator = SeamlessOrchestrator(llm=llm)
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  session_context = GlobalSessionContext()
144
 
145
  def create_default_personas(llm_client: LLMClient):
 
489
 
490
  @router.post("/upload-document")
491
  async def upload_document(file: UploadFile = File(...)):
492
+ # Validate file type
493
  if file.content_type not in [
494
  "application/pdf",
495
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
 
498
  raise HTTPException(status_code=400, detail="Unsupported file type.")
499
 
500
  try:
501
+ # Read file bytes
502
+ file_bytes = await file.read()
503
 
504
+ # Check file size limit
505
+ if not is_within_upload_limit("default", file_bytes, session_context):
506
+ raise HTTPException(status_code=400, detail="Upload exceeds session document size limit (10 MB).")
507
 
508
+ # Extract and validate text
509
+ content = extract_text_from_file(file_bytes, file.content_type)
510
  if not content.strip():
511
  raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
512
 
513
+ # Track file size and name
514
  session_context.append("user", f"[Uploaded Document Content]\n{content.strip()}")
515
+ session_context.uploaded_files.append(file.filename)
516
+ session_context.total_upload_size += len(file_bytes)
517
 
518
  return {"message": "Document uploaded and added to context successfully."}
519
 
 
548
 
549
  add_persona_doc(combined_text, query.persona, doc_id)
550
 
551
+ return {"response": response}
552
+
553
+ @router.get("/uploaded-files")
554
+ def get_uploaded_filenames():
555
+ return {"files": session_context.uploaded_files}
multi_llm_chatbot_backend/app/core/context.py CHANGED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Global context storage
2
+ class GlobalSessionContext:
3
+ def __init__(self):
4
+ self.full_log: list[dict] = []
5
+ self.uploaded_files: list[str] = []
6
+ self.total_upload_size: int = 0
7
+
8
+ def append(self, role: str, content: str):
9
+ self.full_log.append({"role": role, "content": content})
10
+
11
+ def filter_by_persona(self, persona_id: str):
12
+ return self.full_log
13
+
14
+ def clear(self):
15
+ self.full_log = []
16
+ self.uploaded_files = []
17
+ self.total_upload_size = 0
multi_llm_chatbot_backend/app/llm/gemini_client.py CHANGED
@@ -10,6 +10,7 @@ class GeminiClient(LLMClient):
10
 
11
  self.model_name = model_name
12
  self.api_key = os.getenv("GEMINI_API_KEY")
 
13
  if not self.api_key:
14
  raise ValueError("GEMINI_API_KEY environment variable is required")
15
 
 
10
 
11
  self.model_name = model_name
12
  self.api_key = os.getenv("GEMINI_API_KEY")
13
+ print("DEBUG: GEMINI_API_KEY =", os.getenv("GEMINI_API_KEY"))
14
  if not self.api_key:
15
  raise ValueError("GEMINI_API_KEY environment variable is required")
16
 
multi_llm_chatbot_backend/app/tests/test_context_propagation.py CHANGED
@@ -6,7 +6,7 @@ BASE_URL = "http://localhost:8000"
6
  def test_unified_chat():
7
  print("\nSending unified chat request to /chat...\n")
8
  payload = {
9
- "user_input": "I'm a second year PhD student in Machine Learning. Any advice for my research paper presentation?"
10
  }
11
 
12
  try:
 
6
  def test_unified_chat():
7
  print("\nSending unified chat request to /chat...\n")
8
  payload = {
9
+ "user_input": "I'm a second year PhD student in Machine Learning. Any advice for my research paper presentation? I am preparing for final QnA session."
10
  }
11
 
12
  try:
multi_llm_chatbot_backend/app/utils/document_extractor.py CHANGED
@@ -1,7 +1,7 @@
 
 
1
  import tempfile
2
  import docx2txt
3
- import PyPDF2
4
- from io import BytesIO
5
 
6
  def extract_text_from_file(file_bytes: bytes, content_type: str) -> str:
7
  if content_type == "application/pdf":
@@ -12,10 +12,7 @@ def extract_text_from_file(file_bytes: bytes, content_type: str) -> str:
12
  with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp:
13
  tmp.write(file_bytes)
14
  tmp_path = tmp.name
15
- try:
16
- return docx2txt.process(tmp_path)
17
- finally:
18
- os.unlink(tmp_path) # Clean up temp file
19
 
20
  elif content_type == "text/plain":
21
  return file_bytes.decode("utf-8")
 
1
+ from io import BytesIO
2
+ import PyPDF2
3
  import tempfile
4
  import docx2txt
 
 
5
 
6
  def extract_text_from_file(file_bytes: bytes, content_type: str) -> str:
7
  if content_type == "application/pdf":
 
12
  with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp:
13
  tmp.write(file_bytes)
14
  tmp_path = tmp.name
15
+ return docx2txt.process(tmp_path)
 
 
 
16
 
17
  elif content_type == "text/plain":
18
  return file_bytes.decode("utf-8")
multi_llm_chatbot_backend/app/utils/file_limits.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/file_limits.py
2
+ from app.core.context import GlobalSessionContext
3
+
4
+ MAX_SESSION_UPLOAD_SIZE_MB = 10
5
+
6
+
7
+ MAX_TOTAL_UPLOAD_MB = 10
8
+
9
+ def is_within_upload_limit(session_id: str, new_file_bytes: bytes, session_context: GlobalSessionContext) -> bool:
10
+ size_mb = (session_context.total_upload_size + len(new_file_bytes)) / (1024 * 1024)
11
+ return size_mb <= MAX_TOTAL_UPLOAD_MB
phd-advisor-frontend/src/components/EnhancedChatInput.js ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { Send, Paperclip, FileText, X, Trash2, Download } from 'lucide-react';
3
+ import FileUpload from './FileUpload';
4
+
5
+ const EnhancedChatInput = ({
6
+ onSendMessage,
7
+ onFileUploaded,
8
+ uploadedDocuments = [],
9
+ isLoading,
10
+ placeholder = "Ask your advisors anything about your PhD journey..."
11
+ }) => {
12
+ const [inputMessage, setInputMessage] = useState('');
13
+ const [showUpload, setShowUpload] = useState(false);
14
+ const [showDocuments, setShowDocuments] = useState(false);
15
+ const [isUploading, setIsUploading] = useState(false);
16
+ const textareaRef = useRef(null);
17
+
18
+ const handleSend = () => {
19
+ if (!inputMessage.trim() || isLoading || isUploading) return;
20
+
21
+ onSendMessage(inputMessage);
22
+ setInputMessage('');
23
+ if (textareaRef.current) {
24
+ textareaRef.current.style.height = 'auto';
25
+ }
26
+ };
27
+
28
+ const handleKeyPress = (e) => {
29
+ if (e.key === 'Enter' && !e.shiftKey) {
30
+ e.preventDefault();
31
+ handleSend();
32
+ }
33
+ };
34
+
35
+ const handleFileUploaded = (file, response) => {
36
+ setIsUploading(false);
37
+ setShowUpload(false);
38
+
39
+ if (onFileUploaded) {
40
+ onFileUploaded(file, response);
41
+ }
42
+ };
43
+
44
+ const handleUploadStart = () => {
45
+ setIsUploading(true);
46
+ };
47
+
48
+ const toggleUpload = () => {
49
+ if (!isUploading) {
50
+ setShowUpload(!showUpload);
51
+ setShowDocuments(false); // Close documents panel when opening upload
52
+ }
53
+ };
54
+
55
+ const toggleDocuments = () => {
56
+ setShowDocuments(!showDocuments);
57
+ setShowUpload(false); // Close upload panel when opening documents
58
+ };
59
+
60
+ // Auto-resize textarea
61
+ useEffect(() => {
62
+ if (textareaRef.current) {
63
+ textareaRef.current.style.height = 'auto';
64
+ textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
65
+ }
66
+ }, [inputMessage]);
67
+
68
+ const formatFileSize = (bytes) => {
69
+ if (bytes === 0) return '0 Bytes';
70
+ const k = 1024;
71
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
72
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
73
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
74
+ };
75
+
76
+ const getFileIcon = (type) => {
77
+ if (type.includes('pdf')) return '📄';
78
+ if (type.includes('word') || type.includes('document')) return '📝';
79
+ if (type.includes('text')) return '📃';
80
+ return '📄';
81
+ };
82
+
83
+ const formatUploadTime = (date) => {
84
+ return new Date(date).toLocaleString([], {
85
+ month: 'short',
86
+ day: 'numeric',
87
+ hour: '2-digit',
88
+ minute: '2-digit'
89
+ });
90
+ };
91
+
92
+ const isDisabled = isLoading || isUploading;
93
+ const canSend = inputMessage.trim() && !isDisabled;
94
+
95
+ return (
96
+ <div className="enhanced-chat-input-container">
97
+ {/* File Upload Area */}
98
+ {showUpload && (
99
+ <div className="floating-upload-section">
100
+ <FileUpload
101
+ onFileUploaded={handleFileUploaded}
102
+ isUploading={isUploading}
103
+ onUploadStart={handleUploadStart}
104
+ />
105
+ </div>
106
+ )}
107
+
108
+ {/* Documents Viewer Panel */}
109
+ {showDocuments && (
110
+ <div className="floating-documents-section">
111
+ <div className="documents-header">
112
+ <div className="documents-title">
113
+ <FileText size={16} />
114
+ <span>Uploaded Documents ({uploadedDocuments.length})</span>
115
+ </div>
116
+ <button
117
+ onClick={() => setShowDocuments(false)}
118
+ className="close-documents-btn"
119
+ >
120
+ <X size={16} />
121
+ </button>
122
+ </div>
123
+
124
+ <div className="documents-list">
125
+ {uploadedDocuments.length === 0 ? (
126
+ <div className="no-documents">
127
+ <FileText size={24} />
128
+ <p>No documents uploaded yet</p>
129
+ <span>Upload documents to reference them in your conversations</span>
130
+ </div>
131
+ ) : (
132
+ uploadedDocuments.map((doc) => (
133
+ <div key={doc.id} className="document-item">
134
+ <div className="document-icon">
135
+ {getFileIcon(doc.type)}
136
+ </div>
137
+ <div className="document-info">
138
+ <div className="document-name">{doc.name}</div>
139
+ <div className="document-details">
140
+ {formatFileSize(doc.size)} • {formatUploadTime(doc.uploadTime)}
141
+ </div>
142
+ </div>
143
+ <div className="document-actions">
144
+ <button
145
+ className="document-action-btn"
146
+ title="Remove document"
147
+ onClick={() => {
148
+ // TODO: Implement remove functionality
149
+ console.log('Remove document:', doc.id);
150
+ }}
151
+ >
152
+ <Trash2 size={14} />
153
+ </button>
154
+ </div>
155
+ </div>
156
+ ))
157
+ )}
158
+ </div>
159
+ </div>
160
+ )}
161
+
162
+ {/* Main Input Box */}
163
+ <div className="floating-input-box">
164
+ {/* Text Input Row */}
165
+ <div className="text-input-row">
166
+ <textarea
167
+ ref={textareaRef}
168
+ value={inputMessage}
169
+ onChange={(e) => setInputMessage(e.target.value)}
170
+ onKeyPress={handleKeyPress}
171
+ placeholder={placeholder}
172
+ className="main-textarea"
173
+ disabled={isDisabled}
174
+ rows={1}
175
+ />
176
+ </div>
177
+
178
+ {/* Controls Row */}
179
+ <div className="controls-row">
180
+ {/* Left - File Controls */}
181
+ <div className="file-controls">
182
+ <button
183
+ onClick={toggleUpload}
184
+ className={`add-docs-btn ${showUpload ? 'active' : ''}`}
185
+ disabled={isUploading}
186
+ type="button"
187
+ >
188
+ <Paperclip size={16} />
189
+ <span>Add documents</span>
190
+ </button>
191
+
192
+ {uploadedDocuments.length > 0 && (
193
+ <button
194
+ onClick={toggleDocuments}
195
+ className={`view-docs-btn ${showDocuments ? 'active' : ''}`}
196
+ type="button"
197
+ title={`View ${uploadedDocuments.length} uploaded document${uploadedDocuments.length !== 1 ? 's' : ''}`}
198
+ >
199
+ <FileText size={16} />
200
+ <span className="docs-count">{uploadedDocuments.length}</span>
201
+ </button>
202
+ )}
203
+ </div>
204
+
205
+ {/* Right - Send Button */}
206
+ <button
207
+ onClick={handleSend}
208
+ disabled={!canSend}
209
+ className={`send-button ${canSend ? 'enabled' : 'disabled'}`}
210
+ type="button"
211
+ >
212
+ <Send size={16} />
213
+ </button>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ );
218
+ };
219
+
220
+ export default EnhancedChatInput;
phd-advisor-frontend/src/components/FileUpload.js ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from 'react';
2
+ import { Upload, FileText, File, X, CheckCircle, AlertCircle } from 'lucide-react';
3
+ import { useTheme } from '../contexts/ThemeContext';
4
+ import '../styles/FileUpload.css'
5
+
6
+ const FileUpload = ({ onFileUploaded, isUploading, onUploadStart }) => {
7
+ const [dragActive, setDragActive] = useState(false);
8
+ const [uploadStatus, setUploadStatus] = useState(null); // 'success', 'error', null
9
+ const [uploadMessage, setUploadMessage] = useState('');
10
+ const [selectedFile, setSelectedFile] = useState(null);
11
+ const fileInputRef = useRef(null);
12
+ const { isDark } = useTheme();
13
+
14
+ const supportedTypes = {
15
+ 'application/pdf': { ext: 'PDF', icon: FileText, color: '#EF4444' },
16
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': { ext: 'DOCX', icon: File, color: '#3B82F6' },
17
+ 'text/plain': { ext: 'TXT', icon: FileText, color: '#10B981' }
18
+ };
19
+
20
+ const validateFile = (file) => {
21
+ if (!supportedTypes[file.type]) {
22
+ return { valid: false, error: 'Only PDF, DOCX, and TXT files are supported.' };
23
+ }
24
+
25
+ if (file.size > 10 * 1024 * 1024) { // 10MB limit
26
+ return { valid: false, error: 'File size must be less than 10MB.' };
27
+ }
28
+
29
+ return { valid: true };
30
+ };
31
+
32
+ const uploadFile = async (file) => {
33
+ const validation = validateFile(file);
34
+ if (!validation.valid) {
35
+ setUploadStatus('error');
36
+ setUploadMessage(validation.error);
37
+ return;
38
+ }
39
+
40
+ setSelectedFile(file);
41
+ onUploadStart && onUploadStart();
42
+
43
+ const formData = new FormData();
44
+ formData.append('file', file);
45
+
46
+ try {
47
+ const response = await fetch('http://localhost:8000/upload-document', {
48
+ method: 'POST',
49
+ body: formData,
50
+ });
51
+
52
+ if (response.ok) {
53
+ const data = await response.json();
54
+ setUploadStatus('success');
55
+ setUploadMessage(`${file.name} uploaded successfully and added to context.`);
56
+ onFileUploaded && onFileUploaded(file, data);
57
+
58
+ // Auto-clear success message after 5 seconds
59
+ setTimeout(() => {
60
+ setUploadStatus(null);
61
+ setSelectedFile(null);
62
+ setUploadMessage('');
63
+ }, 5000);
64
+ } else {
65
+ const errorData = await response.json();
66
+ throw new Error(errorData.detail || 'Upload failed');
67
+ }
68
+ } catch (error) {
69
+ setUploadStatus('error');
70
+ setUploadMessage(`Upload failed: ${error.message}`);
71
+ console.error('Upload error:', error);
72
+ }
73
+ };
74
+
75
+ const handleDrag = (e) => {
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+ if (e.type === 'dragenter' || e.type === 'dragover') {
79
+ setDragActive(true);
80
+ } else if (e.type === 'dragleave') {
81
+ setDragActive(false);
82
+ }
83
+ };
84
+
85
+ const handleDrop = (e) => {
86
+ e.preventDefault();
87
+ e.stopPropagation();
88
+ setDragActive(false);
89
+
90
+ if (isUploading) return;
91
+
92
+ const files = Array.from(e.dataTransfer.files);
93
+ if (files.length > 0) {
94
+ uploadFile(files[0]);
95
+ }
96
+ };
97
+
98
+ const handleFileSelect = (e) => {
99
+ const files = Array.from(e.target.files);
100
+ if (files.length > 0) {
101
+ uploadFile(files[0]);
102
+ }
103
+ };
104
+
105
+ const openFileDialog = () => {
106
+ if (!isUploading) {
107
+ fileInputRef.current?.click();
108
+ }
109
+ };
110
+
111
+ const clearStatus = () => {
112
+ setUploadStatus(null);
113
+ setSelectedFile(null);
114
+ setUploadMessage('');
115
+ };
116
+
117
+ const getFileIcon = (file) => {
118
+ const fileInfo = supportedTypes[file.type];
119
+ if (fileInfo) {
120
+ const Icon = fileInfo.icon;
121
+ return <Icon size={16} style={{ color: fileInfo.color }} />;
122
+ }
123
+ return <File size={16} />;
124
+ };
125
+
126
+ return (
127
+ <div className="file-upload-container">
128
+ {/* Upload Status Banner */}
129
+ {uploadStatus && (
130
+ <div className={`upload-status ${uploadStatus}`}>
131
+ <div className="status-content">
132
+ {uploadStatus === 'success' ? (
133
+ <CheckCircle size={16} className="status-icon success" />
134
+ ) : (
135
+ <AlertCircle size={16} className="status-icon error" />
136
+ )}
137
+ <span className="status-message">{uploadMessage}</span>
138
+ <button onClick={clearStatus} className="status-close">
139
+ <X size={14} />
140
+ </button>
141
+ </div>
142
+ </div>
143
+ )}
144
+
145
+ {/* Upload Area */}
146
+ <div
147
+ className={`file-upload-area ${dragActive ? 'drag-active' : ''} ${isUploading ? 'uploading' : ''}`}
148
+ onDragEnter={handleDrag}
149
+ onDragLeave={handleDrag}
150
+ onDragOver={handleDrag}
151
+ onDrop={handleDrop}
152
+ onClick={openFileDialog}
153
+ >
154
+ <input
155
+ ref={fileInputRef}
156
+ type="file"
157
+ onChange={handleFileSelect}
158
+ accept=".pdf,.docx,.txt"
159
+ style={{ display: 'none' }}
160
+ disabled={isUploading}
161
+ />
162
+
163
+ <div className="upload-content">
164
+ {isUploading ? (
165
+ <>
166
+ <div className="upload-spinner">
167
+ <div className="spinner"></div>
168
+ </div>
169
+ <p className="upload-text">Uploading {selectedFile?.name}...</p>
170
+ </>
171
+ ) : (
172
+ <>
173
+ <Upload size={24} className="upload-icon" />
174
+ <p className="upload-text">
175
+ <span className="upload-primary">Click to upload</span> or drag and drop
176
+ </p>
177
+ <p className="upload-secondary">PDF, DOCX, or TXT files only</p>
178
+ </>
179
+ )}
180
+ </div>
181
+ </div>
182
+
183
+ {/* Supported File Types */}
184
+ <div className="supported-types">
185
+ {Object.entries(supportedTypes).map(([mimeType, info]) => {
186
+ const Icon = info.icon;
187
+ return (
188
+ <div key={mimeType} className="file-type-chip">
189
+ <Icon size={12} style={{ color: info.color }} />
190
+ <span>{info.ext}</span>
191
+ </div>
192
+ );
193
+ })}
194
+ </div>
195
+ </div>
196
+ );
197
+ };
198
+
199
+ export default FileUpload;
phd-advisor-frontend/src/pages/ChatPage.js CHANGED
@@ -1,7 +1,7 @@
1
  // src/pages/ChatPage.js
2
  import React, { useState, useEffect, useRef } from 'react';
3
- import { Home, MessageCircle, Reply, X, Sparkles, Users, Settings2 } from 'lucide-react';
4
- import ChatInput from '../components/ChatInput';
5
  import MessageBubble from '../components/MessageBubble';
6
  import ThinkingIndicator from '../components/ThinkingIndicator';
7
  import SuggestionsPanel from '../components/SuggestionsPanel';
@@ -10,6 +10,7 @@ import ProviderDropdown from '../components/ProviderDropdown';
10
  import { advisors, getAdvisorColors } from '../data/advisors';
11
  import { useTheme } from '../contexts/ThemeContext';
12
  import '../styles/ChatPage.css';
 
13
 
14
  const ChatPage = ({ onNavigateToHome }) => {
15
  const [messages, setMessages] = useState([]);
@@ -19,6 +20,7 @@ const ChatPage = ({ onNavigateToHome }) => {
19
  const [replyingTo, setReplyingTo] = useState(null);
20
  const [currentProvider, setCurrentProvider] = useState('gemini');
21
  const [isProviderSwitching, setIsProviderSwitching] = useState(false);
 
22
  const messagesEndRef = useRef(null);
23
  const { isDark } = useTheme();
24
 
@@ -102,6 +104,39 @@ const ChatPage = ({ onNavigateToHome }) => {
102
  return Date.now().toString() + Math.random().toString(36).substr(2, 9);
103
  };
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  const handleSendMessage = async (inputMessage) => {
106
  if (replyingTo) {
107
  await handleReplyToAdvisor(inputMessage, replyingTo);
@@ -199,21 +234,17 @@ const ChatPage = ({ onNavigateToHome }) => {
199
  };
200
 
201
  const handleReplyToAdvisor = async (inputMessage, replyInfo) => {
202
- const userReplyMessage = {
203
  id: generateMessageId(),
204
  type: 'user',
205
  content: inputMessage,
206
- replyTo: {
207
- advisorId: replyInfo.advisorId,
208
- advisorName: replyInfo.advisorName,
209
- messageId: replyInfo.messageId
210
- },
211
- timestamp: new Date()
212
  };
213
- setMessages(prev => [...prev, userReplyMessage]);
214
-
215
- setThinkingAdvisors([replyInfo.advisorId]);
216
  setIsLoading(true);
 
217
 
218
  try {
219
  const response = await fetch('http://localhost:8000/reply-to-advisor', {
@@ -233,24 +264,34 @@ const ChatPage = ({ onNavigateToHome }) => {
233
  }
234
 
235
  const data = await response.json();
 
236
 
237
- const advisorReplyMessage = {
238
- id: generateMessageId(),
239
- type: 'advisor',
240
- advisorId: replyInfo.advisorId,
241
- content: data.response,
242
- isReply: true,
243
- timestamp: new Date()
244
- };
245
-
246
- setMessages(prev => [...prev, advisorReplyMessage]);
 
 
 
 
 
 
 
 
 
247
 
248
  } catch (error) {
249
  console.error('Error sending reply:', error);
250
  const errorMessage = {
251
  id: generateMessageId(),
252
  type: 'error',
253
- content: 'Sorry, I encountered an error with your reply. Please try again.',
254
  timestamp: new Date()
255
  };
256
  setMessages(prev => [...prev, errorMessage]);
@@ -322,6 +363,14 @@ const ChatPage = ({ onNavigateToHome }) => {
322
  </div>
323
 
324
  <div className="header-controls">
 
 
 
 
 
 
 
 
325
  <ProviderDropdown
326
  currentProvider={currentProvider}
327
  onProviderChange={handleProviderSwitch}
@@ -378,6 +427,17 @@ const ChatPage = ({ onNavigateToHome }) => {
378
  </div>
379
  );
380
  }
 
 
 
 
 
 
 
 
 
 
 
381
 
382
  return (
383
  <MessageBubble
@@ -417,7 +477,6 @@ const ChatPage = ({ onNavigateToHome }) => {
417
  )}
418
  </div>
419
 
420
- {/* Floating Input Area */}
421
  <div className="floating-input-area">
422
  {replyingTo && (
423
  <div className="reply-banner">
@@ -431,17 +490,17 @@ const ChatPage = ({ onNavigateToHome }) => {
431
  </div>
432
  )}
433
 
434
- <div className="input-wrapper">
435
- <ChatInput
436
- onSendMessage={handleSendMessage}
437
- isLoading={isLoading}
438
- placeholder={
439
- replyingTo
440
- ? `Reply to ${replyingTo.advisorName}...`
441
- : "Ask your advisors anything about your PhD journey..."
442
- }
443
- />
444
- </div>
445
  </div>
446
  </div>
447
  );
 
1
  // src/pages/ChatPage.js
2
  import React, { useState, useEffect, useRef } from 'react';
3
+ import { Home, MessageCircle, Reply, X, Sparkles, Users, Settings2, FileText } from 'lucide-react';
4
+ import EnhancedChatInput from '../components/EnhancedChatInput';
5
  import MessageBubble from '../components/MessageBubble';
6
  import ThinkingIndicator from '../components/ThinkingIndicator';
7
  import SuggestionsPanel from '../components/SuggestionsPanel';
 
10
  import { advisors, getAdvisorColors } from '../data/advisors';
11
  import { useTheme } from '../contexts/ThemeContext';
12
  import '../styles/ChatPage.css';
13
+ import '../styles/EnhancedChatInput.css';
14
 
15
  const ChatPage = ({ onNavigateToHome }) => {
16
  const [messages, setMessages] = useState([]);
 
20
  const [replyingTo, setReplyingTo] = useState(null);
21
  const [currentProvider, setCurrentProvider] = useState('gemini');
22
  const [isProviderSwitching, setIsProviderSwitching] = useState(false);
23
+ const [uploadedDocuments, setUploadedDocuments] = useState([]);
24
  const messagesEndRef = useRef(null);
25
  const { isDark } = useTheme();
26
 
 
104
  return Date.now().toString() + Math.random().toString(36).substr(2, 9);
105
  };
106
 
107
+ const handleFileUploaded = (file, response) => {
108
+ // Add the uploaded document to our list
109
+ const docInfo = {
110
+ id: generateMessageId(),
111
+ name: file.name,
112
+ size: file.size,
113
+ type: file.type,
114
+ uploadTime: new Date()
115
+ };
116
+ setUploadedDocuments(prev => [...prev, docInfo]);
117
+
118
+ // Add a system message about the upload
119
+ const uploadMessage = {
120
+ id: generateMessageId(),
121
+ type: 'document_upload',
122
+ content: `Successfully uploaded "${file.name}". Your advisors can now reference this document in their responses.`,
123
+ timestamp: new Date(),
124
+ documentInfo: docInfo
125
+ };
126
+
127
+ // Force update messages state
128
+ setMessages(prev => {
129
+ const newMessages = [...prev, uploadMessage];
130
+ console.log('Added upload message:', uploadMessage); // Debug log
131
+ return newMessages;
132
+ });
133
+
134
+ // Scroll to bottom to show the new message
135
+ setTimeout(() => {
136
+ scrollToBottom();
137
+ }, 100);
138
+ };
139
+
140
  const handleSendMessage = async (inputMessage) => {
141
  if (replyingTo) {
142
  await handleReplyToAdvisor(inputMessage, replyingTo);
 
234
  };
235
 
236
  const handleReplyToAdvisor = async (inputMessage, replyInfo) => {
237
+ const userMessage = {
238
  id: generateMessageId(),
239
  type: 'user',
240
  content: inputMessage,
241
+ timestamp: new Date(),
242
+ replyingTo: replyInfo
 
 
 
 
243
  };
244
+ setMessages(prev => [...prev, userMessage]);
245
+
 
246
  setIsLoading(true);
247
+ setThinkingAdvisors([replyInfo.advisorId]);
248
 
249
  try {
250
  const response = await fetch('http://localhost:8000/reply-to-advisor', {
 
264
  }
265
 
266
  const data = await response.json();
267
+ setThinkingAdvisors([]);
268
 
269
+ if (data.type === 'advisor_reply') {
270
+ const replyMessage = {
271
+ id: generateMessageId(),
272
+ type: 'advisor',
273
+ advisorId: replyInfo.advisorId,
274
+ content: data.response,
275
+ timestamp: new Date(),
276
+ isReply: true
277
+ };
278
+ setMessages(prev => [...prev, replyMessage]);
279
+ } else if (data.type === 'error') {
280
+ const errorMessage = {
281
+ id: generateMessageId(),
282
+ type: 'error',
283
+ content: data.response,
284
+ timestamp: new Date()
285
+ };
286
+ setMessages(prev => [...prev, errorMessage]);
287
+ }
288
 
289
  } catch (error) {
290
  console.error('Error sending reply:', error);
291
  const errorMessage = {
292
  id: generateMessageId(),
293
  type: 'error',
294
+ content: 'Sorry, I encountered an error. Please try again.',
295
  timestamp: new Date()
296
  };
297
  setMessages(prev => [...prev, errorMessage]);
 
363
  </div>
364
 
365
  <div className="header-controls">
366
+ {/* Document Count Indicator */}
367
+ {uploadedDocuments.length > 0 && (
368
+ <div className="document-indicator" title={`${uploadedDocuments.length} document(s) uploaded`}>
369
+ <FileText size={16} />
370
+ <span className="doc-count">{uploadedDocuments.length}</span>
371
+ </div>
372
+ )}
373
+
374
  <ProviderDropdown
375
  currentProvider={currentProvider}
376
  onProviderChange={handleProviderSwitch}
 
427
  </div>
428
  );
429
  }
430
+
431
+ if (message.type === 'document_upload') {
432
+ return (
433
+ <div key={message.id || index} className="document-upload-notification">
434
+ <div className="upload-notification-content">
435
+ <FileText size={16} className="upload-icon" />
436
+ <span>{message.content}</span>
437
+ </div>
438
+ </div>
439
+ );
440
+ }
441
 
442
  return (
443
  <MessageBubble
 
477
  )}
478
  </div>
479
 
 
480
  <div className="floating-input-area">
481
  {replyingTo && (
482
  <div className="reply-banner">
 
490
  </div>
491
  )}
492
 
493
+ <EnhancedChatInput
494
+ onSendMessage={handleSendMessage}
495
+ onFileUploaded={handleFileUploaded}
496
+ uploadedDocuments={uploadedDocuments}
497
+ isLoading={isLoading}
498
+ placeholder={
499
+ replyingTo
500
+ ? `Reply to ${replyingTo.advisorName}...`
501
+ : "Ask your advisors anything about your PhD journey..."
502
+ }
503
+ />
504
  </div>
505
  </div>
506
  );
phd-advisor-frontend/src/styles/ChatPage.css CHANGED
@@ -1,4 +1,4 @@
1
- /* ChatPage.css - Modern Chat Interface Styles */
2
 
3
  /* Modern Chat Page Layout */
4
  .modern-chat-page {
@@ -95,6 +95,12 @@
95
  gap: 20px;
96
  }
97
 
 
 
 
 
 
 
98
  /* Advisor Pills */
99
  .advisor-pills {
100
  display: flex;
@@ -113,42 +119,30 @@
113
  font-weight: 500;
114
  transition: all 0.3s ease;
115
  position: relative;
116
- overflow: hidden;
117
  }
118
 
119
  .advisor-pill.thinking {
120
- animation: pulse 2s infinite;
121
- }
122
-
123
- .advisor-pill span {
124
- font-weight: 600;
125
  }
126
 
127
  .thinking-pulse {
128
  position: absolute;
129
- right: 6px;
130
- top: 50%;
131
- transform: translateY(-50%);
132
- width: 6px;
133
- height: 6px;
134
  border-radius: 50%;
135
- animation: pulse-dot 1.5s infinite;
136
  }
137
 
138
  @keyframes pulse {
139
- 0%, 100% { transform: scale(1); }
140
- 50% { transform: scale(1.05); }
141
- }
142
-
143
- @keyframes pulse-dot {
144
  0%, 100% { opacity: 1; }
145
- 50% { opacity: 0.3; }
146
  }
147
 
148
- .header-controls {
149
- display: flex;
150
- align-items: center;
151
- gap: 12px;
152
  }
153
 
154
  /* Main Content Area */
@@ -156,25 +150,24 @@
156
  flex: 1;
157
  display: flex;
158
  flex-direction: column;
159
- padding: 32px 24px 120px 24px;
160
- max-width: 1200px;
161
- margin: 0 auto;
162
- width: 100%;
163
  }
164
 
165
- /* Welcome State - Simplified */
166
  .welcome-state {
 
167
  display: flex;
168
- flex-direction: column;
169
  align-items: center;
170
  justify-content: center;
171
- max-width: 900px;
172
- margin: 0 auto;
173
  min-height: 60vh;
174
  }
175
 
176
  .suggestions-container {
177
  width: 100%;
 
178
  }
179
 
180
  /* Chat State */
@@ -186,42 +179,33 @@
186
 
187
  .messages-area {
188
  flex: 1;
189
- background: var(--bg-primary);
190
- border-radius: 24px;
191
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
192
- border: 1px solid var(--border-primary);
193
  overflow: hidden;
194
- max-width: 900px;
195
- margin: 0 auto;
196
- width: 100%;
197
  }
198
 
199
  .messages-scroll {
200
  height: 100%;
201
  overflow-y: auto;
202
- padding: 32px;
203
- display: flex;
204
- flex-direction: column;
205
- gap: 24px;
206
- scroll-behavior: smooth;
207
  }
208
 
209
- /* Message Styles */
210
  .orchestrator-message {
211
  display: flex;
212
  gap: 16px;
 
213
  align-items: flex-start;
214
  }
215
 
216
  .orchestrator-avatar {
217
- width: 44px;
218
- height: 44px;
219
- background: var(--bg-tertiary);
220
- border-radius: 12px;
221
  display: flex;
222
  align-items: center;
223
  justify-content: center;
224
- color: var(--text-secondary);
225
  flex-shrink: 0;
226
  }
227
 
@@ -229,15 +213,16 @@
229
  flex: 1;
230
  background: var(--bg-secondary);
231
  border-radius: 16px;
232
- padding: 20px;
233
  border: 1px solid var(--border-primary);
 
234
  }
235
 
236
  .orchestrator-header {
237
  display: flex;
238
  align-items: center;
239
  justify-content: space-between;
240
- margin-bottom: 12px;
241
  }
242
 
243
  .orchestrator-name {
@@ -282,6 +267,7 @@
282
  gap: 16px;
283
  align-items: center;
284
  opacity: 0.8;
 
285
  }
286
 
287
  .thinking-content {
@@ -322,47 +308,57 @@
322
  40% { transform: scale(1); }
323
  }
324
 
325
- /* Floating Input Area */
326
  .floating-input-area {
327
  position: fixed;
328
- bottom: 0;
329
  left: 0;
330
  right: 0;
331
  z-index: 50;
332
- background: linear-gradient(to top, var(--bg-primary), transparent);
333
- padding: 20px 24px 32px 24px;
334
- backdrop-filter: blur(20px);
335
- -webkit-backdrop-filter: blur(20px);
336
- }
337
-
338
- .reply-banner {
339
  display: flex;
 
340
  align-items: center;
341
- justify-content: space-between;
342
- background: var(--bg-secondary);
 
 
 
 
 
343
  border: 1px solid var(--border-primary);
344
  border-radius: 12px;
345
  padding: 12px 16px;
346
- margin-bottom: 12px;
347
- max-width: 900px;
348
- margin-left: auto;
349
- margin-right: auto;
350
- margin-bottom: 12px;
 
 
 
 
 
 
 
351
  }
352
 
353
- .reply-info {
 
354
  display: flex;
355
  align-items: center;
356
  gap: 8px;
357
- font-size: 14px;
358
  color: var(--text-secondary);
 
359
  }
360
 
361
- .reply-info strong {
362
  color: var(--accent-primary);
363
  }
364
 
365
- .cancel-reply {
 
366
  background: none;
367
  border: none;
368
  color: var(--text-tertiary);
@@ -372,17 +368,50 @@
372
  transition: all 0.2s ease;
373
  }
374
 
375
- .cancel-reply:hover {
376
- background: var(--bg-tertiary);
377
  color: var(--text-secondary);
378
  }
379
 
380
- .input-wrapper {
381
- max-width: 900px;
382
- margin: 0 auto;
 
 
 
 
 
 
 
 
 
 
 
383
  }
384
 
385
- /* Provider Dropdown Styles */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  .provider-dropdown {
387
  position: relative;
388
  display: inline-block;
@@ -486,6 +515,10 @@
486
  -webkit-backdrop-filter: blur(20px);
487
  }
488
 
 
 
 
 
489
  .provider-option {
490
  width: 100%;
491
  display: flex;
@@ -579,7 +612,7 @@
579
  }
580
 
581
  .main-content {
582
- padding: 24px 16px 120px 16px;
583
  }
584
 
585
  .welcome-state {
@@ -587,10 +620,25 @@
587
  }
588
 
589
  .messages-scroll {
590
- padding: 20px;
 
591
  }
592
 
593
  .floating-input-area {
594
- padding: 16px 16px 24px 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  }
596
  }
 
1
+ /* ChatPage.css - Complete Fixed File */
2
 
3
  /* Modern Chat Page Layout */
4
  .modern-chat-page {
 
95
  gap: 20px;
96
  }
97
 
98
+ .header-controls {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 12px;
102
+ }
103
+
104
  /* Advisor Pills */
105
  .advisor-pills {
106
  display: flex;
 
119
  font-weight: 500;
120
  transition: all 0.3s ease;
121
  position: relative;
 
122
  }
123
 
124
  .advisor-pill.thinking {
125
+ animation: pulse 2s ease-in-out infinite;
 
 
 
 
126
  }
127
 
128
  .thinking-pulse {
129
  position: absolute;
130
+ top: -2px;
131
+ right: -2px;
132
+ width: 8px;
133
+ height: 8px;
 
134
  border-radius: 50%;
135
+ animation: pulse-dot 1.5s ease-in-out infinite;
136
  }
137
 
138
  @keyframes pulse {
 
 
 
 
 
139
  0%, 100% { opacity: 1; }
140
+ 50% { opacity: 0.7; }
141
  }
142
 
143
+ @keyframes pulse-dot {
144
+ 0%, 100% { transform: scale(1); opacity: 1; }
145
+ 50% { transform: scale(1.2); opacity: 0.8; }
 
146
  }
147
 
148
  /* Main Content Area */
 
150
  flex: 1;
151
  display: flex;
152
  flex-direction: column;
153
+ padding-top: 24px;
154
+ padding-bottom: 140px; /* Space for floating input */
155
+ padding-left: 16px;
156
+ padding-right: 16px;
157
  }
158
 
159
+ /* Welcome State */
160
  .welcome-state {
161
+ flex: 1;
162
  display: flex;
 
163
  align-items: center;
164
  justify-content: center;
 
 
165
  min-height: 60vh;
166
  }
167
 
168
  .suggestions-container {
169
  width: 100%;
170
+ max-width: 1200px;
171
  }
172
 
173
  /* Chat State */
 
179
 
180
  .messages-area {
181
  flex: 1;
 
 
 
 
182
  overflow: hidden;
 
 
 
183
  }
184
 
185
  .messages-scroll {
186
  height: 100%;
187
  overflow-y: auto;
188
+ padding: 20px;
189
+ padding-bottom: 40px; /* Extra space before floating input */
 
 
 
190
  }
191
 
192
+ /* Message Components */
193
  .orchestrator-message {
194
  display: flex;
195
  gap: 16px;
196
+ margin-bottom: 24px;
197
  align-items: flex-start;
198
  }
199
 
200
  .orchestrator-avatar {
201
+ width: 40px;
202
+ height: 40px;
203
+ border-radius: 20px;
204
+ background: var(--accent-gradient);
205
  display: flex;
206
  align-items: center;
207
  justify-content: center;
208
+ color: white;
209
  flex-shrink: 0;
210
  }
211
 
 
213
  flex: 1;
214
  background: var(--bg-secondary);
215
  border-radius: 16px;
216
+ padding: 16px 20px;
217
  border: 1px solid var(--border-primary);
218
+ max-width: 600px;
219
  }
220
 
221
  .orchestrator-header {
222
  display: flex;
223
  align-items: center;
224
  justify-content: space-between;
225
+ margin-bottom: 8px;
226
  }
227
 
228
  .orchestrator-name {
 
267
  gap: 16px;
268
  align-items: center;
269
  opacity: 0.8;
270
+ margin-bottom: 24px;
271
  }
272
 
273
  .thinking-content {
 
308
  40% { transform: scale(1); }
309
  }
310
 
311
+ /* Floating Input Area - FIXED VERSION */
312
  .floating-input-area {
313
  position: fixed;
314
+ bottom: 20px;
315
  left: 0;
316
  right: 0;
317
  z-index: 50;
318
+ background: transparent;
319
+ padding: 0;
 
 
 
 
 
320
  display: flex;
321
+ flex-direction: column;
322
  align-items: center;
323
+ max-width: 800px;
324
+ margin: 0 auto;
325
+ }
326
+
327
+ /* Reply Banner in ChatPage - matches input width and fixes dark mode */
328
+ .floating-input-area .reply-banner {
329
+ background: var(--bg-primary);
330
  border: 1px solid var(--border-primary);
331
  border-radius: 12px;
332
  padding: 12px 16px;
333
+ margin: 0 16px 12px 16px;
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: space-between;
337
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
338
+ width: 100%;
339
+ max-width: 768px;
340
+ box-sizing: border-box;
341
+ }
342
+
343
+ [data-theme="dark"] .floating-input-area .reply-banner {
344
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
345
  }
346
 
347
+ /* Reply Info - Fix dark mode text color */
348
+ .floating-input-area .reply-info {
349
  display: flex;
350
  align-items: center;
351
  gap: 8px;
 
352
  color: var(--text-secondary);
353
+ font-size: 14px;
354
  }
355
 
356
+ .floating-input-area .reply-info strong {
357
  color: var(--accent-primary);
358
  }
359
 
360
+ /* Cancel Reply Button */
361
+ .floating-input-area .cancel-reply {
362
  background: none;
363
  border: none;
364
  color: var(--text-tertiary);
 
368
  transition: all 0.2s ease;
369
  }
370
 
371
+ .floating-input-area .cancel-reply:hover {
372
+ background: var(--bg-secondary);
373
  color: var(--text-secondary);
374
  }
375
 
376
+ /* Document Indicator Styles */
377
+ .document-indicator {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 6px;
381
+ padding: 6px 12px;
382
+ background: var(--feature-bg); /* Use theme variable */
383
+ border: 1px solid var(--accent-primary); /* Use theme variable */
384
+ border-radius: 20px;
385
+ color: var(--accent-primary); /* Use theme variable */
386
+ font-size: 13px;
387
+ font-weight: 600;
388
+ cursor: default;
389
+ transition: all 0.2s ease;
390
  }
391
 
392
+ .document-indicator:hover {
393
+ background: var(--accent-primary);
394
+ color: white;
395
+ }
396
+
397
+ .doc-count {
398
+ background: var(--accent-primary);
399
+ color: white;
400
+ border-radius: 10px;
401
+ padding: 2px 6px;
402
+ font-size: 11px;
403
+ font-weight: 700;
404
+ min-width: 16px;
405
+ text-align: center;
406
+ line-height: 1.2;
407
+ }
408
+
409
+ .document-indicator:hover .doc-count {
410
+ background: white;
411
+ color: var(--accent-primary);
412
+ }
413
+
414
+ /* Provider Dropdown Styles - FIXED */
415
  .provider-dropdown {
416
  position: relative;
417
  display: inline-block;
 
515
  -webkit-backdrop-filter: blur(20px);
516
  }
517
 
518
+ [data-theme="dark"] .provider-dropdown-menu {
519
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
520
+ }
521
+
522
  .provider-option {
523
  width: 100%;
524
  display: flex;
 
612
  }
613
 
614
  .main-content {
615
+ padding: 24px 12px 120px 12px; /* Adjusted for mobile */
616
  }
617
 
618
  .welcome-state {
 
620
  }
621
 
622
  .messages-scroll {
623
+ padding: 16px;
624
+ padding-bottom: 30px;
625
  }
626
 
627
  .floating-input-area {
628
+ bottom: 10px;
629
+ }
630
+
631
+ .floating-input-area .reply-banner {
632
+ margin: 0 12px 12px 12px;
633
+ }
634
+
635
+ .orchestrator-content {
636
+ max-width: none;
637
+ }
638
+
639
+ .system-content {
640
+ max-width: 90%;
641
+ padding: 10px 16px;
642
+ font-size: 13px;
643
  }
644
  }
phd-advisor-frontend/src/styles/EnhancedChatInput.css ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Enhanced Chat Input Styles */
2
+ .enhanced-chat-input-container {
3
+ position: relative;
4
+ width: 100%;
5
+ max-width: 800px;
6
+ margin: 0 auto;
7
+ padding: 0 16px;
8
+ }
9
+
10
+ /* Reply Banner */
11
+ .reply-banner {
12
+ background: var(--bg-secondary);
13
+ border: 1px solid var(--border-primary);
14
+ border-radius: 12px 12px 0 0;
15
+ padding: 12px 16px;
16
+ margin: 0 16px;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: space-between;
20
+ font-size: 14px;
21
+ color: var(--text-secondary);
22
+ }
23
+
24
+ .reply-content {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 8px;
28
+ }
29
+
30
+ .cancel-reply {
31
+ background: none;
32
+ border: none;
33
+ color: var(--text-tertiary);
34
+ cursor: pointer;
35
+ padding: 4px;
36
+ border-radius: 6px;
37
+ transition: all 0.2s ease;
38
+ }
39
+
40
+ .cancel-reply:hover {
41
+ background: var(--bg-secondary);
42
+ color: var(--text-secondary);
43
+ }
44
+
45
+ /* Floating Upload Section */
46
+ .floating-upload-section {
47
+ background: var(--bg-primary);
48
+ border: 1px solid var(--border-primary);
49
+ border-radius: 16px;
50
+ margin-bottom: 12px;
51
+ padding: 20px;
52
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
53
+ animation: slideUpFade 0.2s ease-out;
54
+ }
55
+
56
+ [data-theme="dark"] .floating-upload-section {
57
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
58
+ }
59
+
60
+ /* Floating Documents Section */
61
+ .floating-documents-section {
62
+ background: var(--bg-primary);
63
+ border: 1px solid var(--border-primary);
64
+ border-radius: 16px;
65
+ margin-bottom: 12px;
66
+ max-height: 400px;
67
+ overflow: hidden;
68
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
69
+ animation: slideUpFade 0.2s ease-out;
70
+ }
71
+
72
+ [data-theme="dark"] .floating-documents-section {
73
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
74
+ }
75
+
76
+ .documents-header {
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: space-between;
80
+ padding: 16px 20px;
81
+ border-bottom: 1px solid var(--border-primary);
82
+ background: var(--bg-secondary);
83
+ }
84
+
85
+ .documents-title {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 8px;
89
+ font-weight: 500;
90
+ color: var(--text-primary);
91
+ font-size: 14px;
92
+ }
93
+
94
+ .close-documents-btn {
95
+ background: none;
96
+ border: none;
97
+ color: var(--text-tertiary);
98
+ cursor: pointer;
99
+ padding: 6px;
100
+ border-radius: 6px;
101
+ transition: all 0.2s ease;
102
+ }
103
+
104
+ .close-documents-btn:hover {
105
+ background: var(--bg-tertiary);
106
+ color: var(--text-secondary);
107
+ }
108
+
109
+ .documents-list {
110
+ max-height: 300px;
111
+ overflow-y: auto;
112
+ padding: 8px;
113
+ }
114
+
115
+ .no-documents {
116
+ display: flex;
117
+ flex-direction: column;
118
+ align-items: center;
119
+ justify-content: center;
120
+ padding: 40px 20px;
121
+ text-align: center;
122
+ color: var(--text-tertiary);
123
+ }
124
+
125
+ .no-documents svg {
126
+ margin-bottom: 12px;
127
+ opacity: 0.5;
128
+ }
129
+
130
+ .no-documents p {
131
+ margin: 8px 0;
132
+ font-weight: 500;
133
+ color: var(--text-primary);
134
+ }
135
+
136
+ .no-documents span {
137
+ font-size: 13px;
138
+ color: var(--text-tertiary);
139
+ }
140
+
141
+ .document-item {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 12px;
145
+ padding: 12px;
146
+ border-radius: 8px;
147
+ transition: background-color 0.2s ease;
148
+ }
149
+
150
+ .document-item:hover {
151
+ background: var(--bg-secondary);
152
+ }
153
+
154
+ .document-icon {
155
+ font-size: 20px;
156
+ flex-shrink: 0;
157
+ }
158
+
159
+ .document-info {
160
+ flex: 1;
161
+ min-width: 0;
162
+ }
163
+
164
+ .document-name {
165
+ font-weight: 500;
166
+ color: var(--text-primary);
167
+ font-size: 14px;
168
+ white-space: nowrap;
169
+ overflow: hidden;
170
+ text-overflow: ellipsis;
171
+ }
172
+
173
+ .document-details {
174
+ font-size: 12px;
175
+ color: var(--text-secondary);
176
+ margin-top: 2px;
177
+ }
178
+
179
+ .document-actions {
180
+ display: flex;
181
+ gap: 4px;
182
+ }
183
+
184
+ .document-action-btn {
185
+ background: none;
186
+ border: none;
187
+ color: var(--text-tertiary);
188
+ cursor: pointer;
189
+ padding: 6px;
190
+ border-radius: 4px;
191
+ transition: all 0.2s ease;
192
+ }
193
+
194
+ .document-action-btn:hover {
195
+ background: var(--bg-tertiary);
196
+ color: var(--text-secondary);
197
+ }
198
+
199
+ .document-action-btn:hover:has(svg[data-icon="trash-2"]) {
200
+ background: #FEF2F2;
201
+ color: #DC2626;
202
+ }
203
+
204
+ [data-theme="dark"] .document-action-btn:hover:has(svg[data-icon="trash-2"]) {
205
+ background: #7F1D1D;
206
+ color: #FCA5A5;
207
+ }
208
+
209
+ /* Main Input Box */
210
+ .floating-input-box {
211
+ background: var(--bg-primary);
212
+ border: 1px solid var(--border-primary);
213
+ border-radius: 16px;
214
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
215
+ backdrop-filter: blur(20px);
216
+ -webkit-backdrop-filter: blur(20px);
217
+ overflow: hidden;
218
+ transition: all 0.2s ease;
219
+ }
220
+
221
+ [data-theme="dark"] .floating-input-box {
222
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
223
+ }
224
+
225
+ .floating-input-box:focus-within {
226
+ border-color: var(--accent-primary);
227
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16), 0 0 0 1px var(--accent-primary);
228
+ }
229
+
230
+ [data-theme="dark"] .floating-input-box:focus-within {
231
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--accent-primary);
232
+ }
233
+
234
+ /* Text Input Row */
235
+ .text-input-row {
236
+ padding: 16px 20px 4px 20px;
237
+ }
238
+
239
+ .main-textarea {
240
+ width: 100%;
241
+ min-height: 24px;
242
+ max-height: 200px;
243
+ border: none;
244
+ outline: none;
245
+ resize: none;
246
+ background: transparent;
247
+ color: var(--text-primary);
248
+ font-size: 16px;
249
+ font-family: inherit;
250
+ line-height: 1.5;
251
+ overflow-y: auto;
252
+ }
253
+
254
+ .main-textarea::placeholder {
255
+ color: var(--text-tertiary);
256
+ }
257
+
258
+ /* Controls Row */
259
+ .controls-row {
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: space-between;
263
+ padding: 8px 20px 16px 20px;
264
+ }
265
+
266
+ /* File Controls - ensure buttons stay side by side */
267
+ .file-controls {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 8px;
271
+ flex-wrap: nowrap;
272
+ }
273
+
274
+ /* Add Documents Button */
275
+ .add-docs-btn {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 6px;
279
+ background: var(--bg-secondary);
280
+ border: 1px solid var(--border-secondary);
281
+ border-radius: 8px;
282
+ padding: 8px 12px;
283
+ font-size: 14px;
284
+ font-weight: 500;
285
+ color: var(--text-secondary);
286
+ cursor: pointer;
287
+ transition: all 0.2s ease;
288
+ }
289
+
290
+ .add-docs-btn:hover:not(:disabled) {
291
+ background: var(--bg-tertiary);
292
+ border-color: var(--border-primary);
293
+ color: var(--text-primary);
294
+ }
295
+
296
+ .add-docs-btn:disabled {
297
+ opacity: 0.5;
298
+ cursor: not-allowed;
299
+ }
300
+
301
+ .add-docs-btn.active {
302
+ background: var(--accent-primary);
303
+ border-color: var(--accent-primary);
304
+ color: white;
305
+ }
306
+
307
+ /* View Documents Button */
308
+ .view-docs-btn {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 6px;
312
+ background: var(--bg-secondary);
313
+ border: 1px solid var(--border-secondary);
314
+ border-radius: 8px;
315
+ padding: 8px 10px;
316
+ color: var(--text-secondary);
317
+ cursor: pointer;
318
+ transition: all 0.2s ease;
319
+ position: relative;
320
+ }
321
+
322
+ .view-docs-btn:hover {
323
+ background: var(--bg-tertiary);
324
+ border-color: var(--border-primary);
325
+ color: var(--text-primary);
326
+ }
327
+
328
+ .view-docs-btn.active {
329
+ background: var(--accent-primary);
330
+ border-color: var(--accent-primary);
331
+ color: white;
332
+ }
333
+
334
+ .docs-count {
335
+ background: var(--accent-primary);
336
+ color: white;
337
+ border-radius: 10px;
338
+ padding: 2px 6px;
339
+ font-size: 10px;
340
+ font-weight: 700;
341
+ min-width: 16px;
342
+ text-align: center;
343
+ line-height: 1.2;
344
+ }
345
+
346
+ .view-docs-btn:hover .docs-count,
347
+ .view-docs-btn.active .docs-count {
348
+ background: white;
349
+ color: var(--accent-primary);
350
+ }
351
+
352
+ /* Send Button - FIXED VERSION */
353
+ .send-button {
354
+ display: flex;
355
+ align-items: center;
356
+ justify-content: center;
357
+ width: 36px;
358
+ height: 36px;
359
+ border-radius: 8px;
360
+ border: none;
361
+ cursor: pointer;
362
+ transition: all 0.2s ease;
363
+ position: relative;
364
+ flex-shrink: 0;
365
+ }
366
+
367
+ /* Enabled state - clean approach */
368
+ .send-button.enabled {
369
+ background: #3B82F6;
370
+ color: #FFFFFF;
371
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
372
+ }
373
+
374
+ /* Icon styling - use currentColor for better inheritance */
375
+ .send-button svg {
376
+ width: 16px;
377
+ height: 16px;
378
+ color: currentColor;
379
+ }
380
+
381
+ .send-button.enabled:hover {
382
+ background: #2563EB;
383
+ transform: translateY(-1px);
384
+ box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
385
+ }
386
+
387
+ .send-button.enabled:active {
388
+ transform: translateY(0);
389
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
390
+ }
391
+
392
+ /* Disabled state */
393
+ .send-button.disabled {
394
+ background: #E5E7EB;
395
+ color: #9CA3AF;
396
+ cursor: not-allowed;
397
+ box-shadow: none;
398
+ }
399
+
400
+ [data-theme="dark"] .send-button.disabled {
401
+ background: #374151;
402
+ color: #6B7280;
403
+ }
404
+
405
+ /* Document Upload Notifications */
406
+ .document-upload-notification {
407
+ display: flex;
408
+ justify-content: center;
409
+ margin: 16px 0;
410
+ animation: slideUpFade 0.3s ease-out;
411
+ background: transparent;
412
+ box-shadow: none;
413
+ border: none;
414
+ padding: 0;
415
+ }
416
+
417
+ /* [data-theme="dark"] .document-upload-notification {
418
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
419
+ } */
420
+
421
+ .upload-notification-content {
422
+ display: flex;
423
+ align-items: center;
424
+ gap: 8px;
425
+ padding: 12px 16px;
426
+ background: var(--feature-bg); /* Change from fixed color */
427
+ border: 1px solid var(--accent-primary); /* Change from fixed color */
428
+ border-radius: 12px;
429
+ color: var(--accent-primary); /* Change from fixed color */
430
+ font-size: 14px;
431
+ font-weight: 500;
432
+ max-width: 80%;
433
+ }
434
+
435
+ .upload-notification-icon {
436
+ flex-shrink: 0;
437
+ }
438
+
439
+ .upload-notification-icon.success {
440
+ color: #10B981;
441
+ }
442
+
443
+ .upload-notification-icon.error {
444
+ color: #EF4444;
445
+ }
446
+
447
+ .upload-notification-text {
448
+ flex: 1;
449
+ }
450
+
451
+ .upload-notification-title {
452
+ font-weight: 500;
453
+ color: var(--text-primary);
454
+ margin: 0 0 2px 0;
455
+ font-size: 14px;
456
+ }
457
+
458
+ .upload-notification-message {
459
+ color: var(--text-secondary);
460
+ margin: 0;
461
+ font-size: 13px;
462
+ }
463
+
464
+ /* [data-theme="dark"] .upload-notification-content {
465
+ box-shadow: 0 4px 16px rgba(99, 102, 241, 0.25);
466
+ } */
467
+
468
+ .notification-close-btn {
469
+ background: none;
470
+ border: none;
471
+ color: var(--text-tertiary);
472
+ cursor: pointer;
473
+ padding: 4px;
474
+ border-radius: 6px;
475
+ transition: all 0.2s ease;
476
+ }
477
+
478
+ .notification-close-btn:hover {
479
+ background: var(--bg-secondary);
480
+ color: var(--text-secondary);
481
+ }
482
+
483
+ /* Animations */
484
+ @keyframes slideUpFade {
485
+ from {
486
+ opacity: 0;
487
+ transform: translateY(10px);
488
+ }
489
+ to {
490
+ opacity: 1;
491
+ transform: translateY(0);
492
+ }
493
+ }
494
+
495
+ /* Mobile Responsive */
496
+ @media (max-width: 768px) {
497
+ .enhanced-chat-input-container {
498
+ padding: 0 12px;
499
+ }
500
+
501
+ .text-input-row {
502
+ padding: 14px 16px 4px 16px;
503
+ }
504
+
505
+ .controls-row {
506
+ padding: 8px 16px 14px 16px;
507
+ }
508
+
509
+ .main-textarea {
510
+ font-size: 16px; /* Prevents zoom on iOS */
511
+ }
512
+
513
+ .add-docs-btn {
514
+ padding: 6px 10px;
515
+ font-size: 13px;
516
+ }
517
+
518
+ .add-docs-btn span {
519
+ display: none; /* Hide text on mobile, show only icon */
520
+ }
521
+
522
+ .view-docs-btn {
523
+ padding: 6px 8px;
524
+ }
525
+
526
+ .send-button {
527
+ width: 32px;
528
+ height: 32px;
529
+ }
530
+
531
+ .floating-upload-section,
532
+ .floating-documents-section {
533
+ padding: 16px;
534
+ }
535
+
536
+ .documents-header {
537
+ padding: 12px 16px;
538
+ }
539
+
540
+ .documents-list {
541
+ max-height: 250px;
542
+ }
543
+
544
+ .upload-notification-content {
545
+ max-width: 95%;
546
+ padding: 10px 14px;
547
+ font-size: 13px;
548
+ }
549
+
550
+ .reply-banner {
551
+ margin: 0 12px 12px 12px;
552
+ }
553
+ }
554
+
555
+ /* Focus and accessibility */
556
+ .add-docs-btn:focus-visible,
557
+ .view-docs-btn:focus-visible,
558
+ .send-button:focus-visible {
559
+ outline: 2px solid var(--accent-primary);
560
+ outline-offset: 2px;
561
+ }
562
+
563
+ .main-textarea:focus {
564
+ outline: none; /* Handled by parent container */
565
+ }
566
+
567
+ /* Loading states */
568
+ .floating-input-box.loading {
569
+ opacity: 0.7;
570
+ pointer-events: none;
571
+ }
572
+
573
+ .main-textarea.loading {
574
+ cursor: wait;
575
+ }
576
+
577
+ /* High contrast mode support */
578
+ @media (prefers-contrast: high) {
579
+ .floating-input-box {
580
+ border-width: 2px;
581
+ }
582
+
583
+ .send-button.enabled {
584
+ border: 2px solid #3B82F6;
585
+ }
586
+ }
587
+
588
+ /* Reduced motion support */
589
+ @media (prefers-reduced-motion: reduce) {
590
+ .floating-input-box,
591
+ .add-docs-btn,
592
+ .view-docs-btn,
593
+ .send-button,
594
+ .floating-upload-section,
595
+ .floating-documents-section {
596
+ transition: none;
597
+ }
598
+
599
+ .slideUpFade {
600
+ animation: none;
601
+ }
602
+ }
phd-advisor-frontend/src/styles/FileUpload.css ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* File Upload Container */
2
+ .file-upload-container {
3
+ width: 100%;
4
+ margin-bottom: 12px;
5
+ }
6
+
7
+ /* Upload Status Banner */
8
+ .upload-status {
9
+ padding: 8px 12px;
10
+ border-radius: 8px;
11
+ margin-bottom: 8px;
12
+ border: 1px solid;
13
+ animation: slideDown 0.3s ease-out;
14
+ }
15
+
16
+ .upload-status.success {
17
+ background-color: #DCFCE7;
18
+ border-color: #22C55E;
19
+ color: #15803D;
20
+ }
21
+
22
+ [data-theme="dark"] .upload-status.success {
23
+ background-color: #14532D;
24
+ border-color: #22C55E;
25
+ color: #22C55E;
26
+ }
27
+
28
+ .upload-status.error {
29
+ background-color: #FEF2F2;
30
+ border-color: #EF4444;
31
+ color: #DC2626;
32
+ }
33
+
34
+ [data-theme="dark"] .upload-status.error {
35
+ background-color: #7F1D1D;
36
+ border-color: #EF4444;
37
+ color: #FCA5A5;
38
+ }
39
+
40
+ .status-content {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 8px;
44
+ }
45
+
46
+ .status-icon {
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .status-message {
51
+ flex: 1;
52
+ font-size: 14px;
53
+ font-weight: 500;
54
+ }
55
+
56
+ .status-close {
57
+ background: none;
58
+ border: none;
59
+ padding: 2px;
60
+ cursor: pointer;
61
+ border-radius: 4px;
62
+ color: inherit;
63
+ opacity: 0.7;
64
+ transition: opacity 0.2s ease;
65
+ }
66
+
67
+ .status-close:hover {
68
+ opacity: 1;
69
+ }
70
+
71
+ /* Upload Area */
72
+ .file-upload-area {
73
+ border: 2px dashed #D1D5DB;
74
+ border-radius: 12px;
75
+ padding: 24px;
76
+ text-align: center;
77
+ cursor: pointer;
78
+ transition: all 0.3s ease;
79
+ background-color: #F9FAFB;
80
+ position: relative;
81
+ overflow: hidden;
82
+ }
83
+
84
+ [data-theme="dark"] .file-upload-area {
85
+ border-color: #374151;
86
+ background-color: #1F2937;
87
+ }
88
+
89
+ .file-upload-area:hover:not(.uploading) {
90
+ border-color: #3B82F6;
91
+ background-color: #EFF6FF;
92
+ transform: translateY(-1px);
93
+ }
94
+
95
+ [data-theme="dark"] .file-upload-area:hover:not(.uploading) {
96
+ border-color: #60A5FA;
97
+ background-color: #1E3A8A;
98
+ }
99
+
100
+ .file-upload-area.drag-active {
101
+ border-color: #3B82F6;
102
+ background-color: #EFF6FF;
103
+ transform: scale(1.02);
104
+ }
105
+
106
+ [data-theme="dark"] .file-upload-area.drag-active {
107
+ border-color: #60A5FA;
108
+ background-color: #1E3A8A;
109
+ }
110
+
111
+ .file-upload-area.uploading {
112
+ cursor: not-allowed;
113
+ opacity: 0.7;
114
+ }
115
+
116
+ .upload-content {
117
+ display: flex;
118
+ flex-direction: column;
119
+ align-items: center;
120
+ gap: 8px;
121
+ }
122
+
123
+ .upload-icon {
124
+ color: #6B7280;
125
+ margin-bottom: 4px;
126
+ }
127
+
128
+ [data-theme="dark"] .upload-icon {
129
+ color: #9CA3AF;
130
+ }
131
+
132
+ .file-upload-area:hover:not(.uploading) .upload-icon {
133
+ color: #3B82F6;
134
+ }
135
+
136
+ [data-theme="dark"] .file-upload-area:hover:not(.uploading) .upload-icon {
137
+ color: #60A5FA;
138
+ }
139
+
140
+ .upload-text {
141
+ margin: 0;
142
+ font-size: 14px;
143
+ color: #374151;
144
+ }
145
+
146
+ [data-theme="dark"] .upload-text {
147
+ color: #D1D5DB;
148
+ }
149
+
150
+ .upload-primary {
151
+ font-weight: 600;
152
+ color: #3B82F6;
153
+ }
154
+
155
+ [data-theme="dark"] .upload-primary {
156
+ color: #60A5FA;
157
+ }
158
+
159
+ .upload-secondary {
160
+ margin: 0;
161
+ font-size: 12px;
162
+ color: #6B7280;
163
+ margin-top: 4px;
164
+ }
165
+
166
+ [data-theme="dark"] .upload-secondary {
167
+ color: #9CA3AF;
168
+ }
169
+
170
+ /* Upload Spinner */
171
+ .upload-spinner {
172
+ display: flex;
173
+ justify-content: center;
174
+ align-items: center;
175
+ }
176
+
177
+ .spinner {
178
+ width: 24px;
179
+ height: 24px;
180
+ border: 2px solid #E5E7EB;
181
+ border-top: 2px solid #3B82F6;
182
+ border-radius: 50%;
183
+ animation: spin 1s linear infinite;
184
+ }
185
+
186
+ [data-theme="dark"] .spinner {
187
+ border-color: #374151;
188
+ border-top-color: #60A5FA;
189
+ }
190
+
191
+ /* Supported File Types */
192
+ .supported-types {
193
+ display: flex;
194
+ justify-content: center;
195
+ gap: 8px;
196
+ margin-top: 8px;
197
+ flex-wrap: wrap;
198
+ }
199
+
200
+ .file-type-chip {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 4px;
204
+ padding: 4px 8px;
205
+ background-color: #F3F4F6;
206
+ border-radius: 16px;
207
+ font-size: 11px;
208
+ font-weight: 500;
209
+ color: #6B7280;
210
+ border: 1px solid #E5E7EB;
211
+ }
212
+
213
+ [data-theme="dark"] .file-type-chip {
214
+ background-color: #374151;
215
+ border-color: #4B5563;
216
+ color: #9CA3AF;
217
+ }
218
+
219
+ /* Enhanced Chat Input with Upload */
220
+ .enhanced-chat-input {
221
+ display: flex;
222
+ flex-direction: column;
223
+ gap: 8px;
224
+ width: 100%;
225
+ }
226
+
227
+ .input-with-upload {
228
+ display: flex;
229
+ align-items: flex-end;
230
+ gap: 8px;
231
+ width: 100%;
232
+ }
233
+
234
+ .upload-toggle-btn {
235
+ background: #F3F4F6;
236
+ border: 1px solid #D1D5DB;
237
+ border-radius: 8px;
238
+ padding: 8px;
239
+ cursor: pointer;
240
+ transition: all 0.2s ease;
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: center;
244
+ min-width: 40px;
245
+ height: 40px;
246
+ }
247
+
248
+ [data-theme="dark"] .upload-toggle-btn {
249
+ background-color: #374151;
250
+ border-color: #4B5563;
251
+ }
252
+
253
+ .upload-toggle-btn:hover {
254
+ background-color: #E5E7EB;
255
+ border-color: #3B82F6;
256
+ }
257
+
258
+ [data-theme="dark"] .upload-toggle-btn:hover {
259
+ background-color: #4B5563;
260
+ border-color: #60A5FA;
261
+ }
262
+
263
+ .upload-toggle-btn.active {
264
+ background-color: #3B82F6;
265
+ border-color: #3B82F6;
266
+ color: white;
267
+ }
268
+
269
+ .upload-toggle-btn svg {
270
+ color: #6B7280;
271
+ }
272
+
273
+ [data-theme="dark"] .upload-toggle-btn svg {
274
+ color: #9CA3AF;
275
+ }
276
+
277
+ .upload-toggle-btn:hover svg,
278
+ .upload-toggle-btn.active svg {
279
+ color: currentColor;
280
+ }
281
+
282
+ /* Animations */
283
+ @keyframes slideDown {
284
+ from {
285
+ opacity: 0;
286
+ transform: translateY(-10px);
287
+ }
288
+ to {
289
+ opacity: 1;
290
+ transform: translateY(0);
291
+ }
292
+ }
293
+
294
+ @keyframes spin {
295
+ 0% { transform: rotate(0deg); }
296
+ 100% { transform: rotate(360deg); }
297
+ }
298
+
299
+ /* Compact upload area for chat input */
300
+ .file-upload-container.compact .file-upload-area {
301
+ padding: 16px;
302
+ font-size: 13px;
303
+ }
304
+
305
+ .file-upload-container.compact .upload-content {
306
+ gap: 6px;
307
+ }
308
+
309
+ .file-upload-container.compact .upload-icon {
310
+ width: 20px;
311
+ height: 20px;
312
+ }
313
+
314
+ .file-upload-container.compact .supported-types {
315
+ margin-top: 6px;
316
+ }
317
+
318
+ /* Responsive adjustments */
319
+ @media (max-width: 640px) {
320
+ .file-upload-area {
321
+ padding: 16px;
322
+ }
323
+
324
+ .supported-types {
325
+ gap: 6px;
326
+ }
327
+
328
+ .file-type-chip {
329
+ font-size: 10px;
330
+ padding: 3px 6px;
331
+ }
332
+ }
phd-advisor-frontend/src/styles/components.css CHANGED
@@ -1,5 +1,3 @@
1
- /* components.css - General Component Styles */
2
-
3
  /* CSS Custom Properties for Theming */
4
  :root[data-theme="light"] {
5
  /* Background Colors */
@@ -215,6 +213,7 @@
215
  line-height: 1.6;
216
  }
217
 
 
218
  .cta-button {
219
  display: inline-flex;
220
  align-items: center;
@@ -241,6 +240,11 @@
241
  height: 24px;
242
  }
243
 
 
 
 
 
 
244
  /* Advisors Grid */
245
  .advisors-grid {
246
  display: grid;
@@ -623,6 +627,15 @@
623
  margin: 0;
624
  }
625
 
 
 
 
 
 
 
 
 
 
626
  .thinking-dots {
627
  display: flex;
628
  gap: 4px;
@@ -641,7 +654,7 @@
641
  margin-bottom: 0;
642
  }
643
 
644
- /* Chat Input */
645
  .input-area {
646
  border-top: 1px solid var(--border-primary);
647
  padding: 16px;
@@ -675,7 +688,8 @@
675
  box-shadow: var(--input-focus-shadow);
676
  }
677
 
678
- .send-button {
 
679
  background: var(--accent-gradient);
680
  color: #FFFFFF;
681
  padding: 12px 24px;
@@ -689,23 +703,23 @@
689
  gap: 8px;
690
  }
691
 
692
- .send-button:hover:not(:disabled) {
693
  transform: translateY(-1px);
694
  box-shadow: var(--shadow-md);
695
  }
696
 
697
- .send-button:disabled {
698
  opacity: 0.5;
699
  cursor: not-allowed;
700
  transform: none;
701
  }
702
 
703
- .send-icon {
704
  width: 20px;
705
  height: 20px;
706
  }
707
 
708
- .send-text {
709
  display: none;
710
  }
711
 
@@ -755,7 +769,8 @@
755
  font-size: 13px;
756
  }
757
 
758
- .send-text {
 
759
  display: inline;
760
  }
761
 
 
 
 
1
  /* CSS Custom Properties for Theming */
2
  :root[data-theme="light"] {
3
  /* Background Colors */
 
213
  line-height: 1.6;
214
  }
215
 
216
+ /* CTA Button */
217
  .cta-button {
218
  display: inline-flex;
219
  align-items: center;
 
240
  height: 24px;
241
  }
242
 
243
+ .cta-arrow {
244
+ width: 20px;
245
+ height: 20px;
246
+ }
247
+
248
  /* Advisors Grid */
249
  .advisors-grid {
250
  display: grid;
 
627
  margin: 0;
628
  }
629
 
630
+ .thinking-indicator {
631
+ display: flex;
632
+ align-items: center;
633
+ gap: 8px;
634
+ color: var(--text-secondary);
635
+ font-style: italic;
636
+ margin-bottom: 8px;
637
+ }
638
+
639
  .thinking-dots {
640
  display: flex;
641
  gap: 4px;
 
654
  margin-bottom: 0;
655
  }
656
 
657
+ /* Basic Chat Input - SCOPED TO AVOID CONFLICTS */
658
  .input-area {
659
  border-top: 1px solid var(--border-primary);
660
  padding: 16px;
 
688
  box-shadow: var(--input-focus-shadow);
689
  }
690
 
691
+ /* IMPORTANT: Make send button styles specific to basic ChatInput only */
692
+ .input-container .send-button {
693
  background: var(--accent-gradient);
694
  color: #FFFFFF;
695
  padding: 12px 24px;
 
703
  gap: 8px;
704
  }
705
 
706
+ .input-container .send-button:hover:not(:disabled) {
707
  transform: translateY(-1px);
708
  box-shadow: var(--shadow-md);
709
  }
710
 
711
+ .input-container .send-button:disabled {
712
  opacity: 0.5;
713
  cursor: not-allowed;
714
  transform: none;
715
  }
716
 
717
+ .input-container .send-icon {
718
  width: 20px;
719
  height: 20px;
720
  }
721
 
722
+ .input-container .send-text {
723
  display: none;
724
  }
725
 
 
769
  font-size: 13px;
770
  }
771
 
772
+ /* Basic ChatInput mobile styles */
773
+ .input-container .send-text {
774
  display: inline;
775
  }
776