Sohan Kshirsagar commited on
Commit
599da37
·
2 Parent(s): a3526d10f18a26

Merge branch 'main' into sohank-feature

Browse files
README.md CHANGED
@@ -1,3 +1,220 @@
1
- # Neon-AI-Project
2
 
3
- Multi-model LLM chatbot.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PhD Advisory Panel
2
 
3
+ An AI-powered academic guidance system that provides personalized advice through specialized advisor personas. Get diverse perspectives on your PhD journey from multiple AI advisors, each bringing unique expertise in methodology, theory, and practical guidance.
4
+
5
+ ## Features
6
+
7
+ - **Multiple AI Advisor Personas**: Chat with specialized advisors including Methodist (methodology expert), Theorist (conceptual frameworks), and Pragmatist (action-focused guidance)
8
+ - **Document Upload Support**: Upload PDFs, Word documents, and text files to provide context for your questions
9
+ - **Multi-LLM Backend**: Supports both Gemini API and Ollama models with seamless provider switching
10
+ - **Real-time Chat Interface**: Modern, responsive chat interface with advisor-specific styling
11
+ - **Context-Aware Responses**: Maintains conversation history and document context across the session
12
+ - **Sequential Advisor Responses**: Get input from all advisors in a structured sequence
13
+ - **Individual Advisor Chat**: Have focused conversations with specific advisors
14
+
15
+ ## Architecture
16
+
17
+ ### Frontend (React)
18
+ - **Technology**: React 18 with modern hooks and functional components
19
+ - **Styling**: CSS custom properties with dark/light theme support
20
+ - **Components**: Modular component architecture with reusable UI elements
21
+ - **State Management**: React hooks for local state management
22
+ - **Icons**: Lucide React for consistent iconography
23
+
24
+ ### Backend (FastAPI)
25
+ - **Framework**: FastAPI with automatic API documentation
26
+ - **LLM Integration**: Support for multiple providers (Gemini, Ollama)
27
+ - **Document Processing**: PDF, DOCX, and text file extraction
28
+ - **Session Management**: Global session context with file upload tracking
29
+ - **CORS Support**: Configured for React development server
30
+
31
+ ## Quick Start
32
+
33
+ ### Prerequisites
34
+ - Node.js 16+ and npm
35
+ - Python 3.8+
36
+ - (Optional) Gemini API key for Google's models
37
+ - (Optional) Ollama installation for local models
38
+
39
+ ### Backend Setup
40
+
41
+ 1. **Clone and navigate to backend directory**
42
+ ```bash
43
+ cd multi_llm_chatbot_backend
44
+ ```
45
+
46
+ 2. **Install dependencies**
47
+ ```bash
48
+ pip install -r requirements.txt
49
+ ```
50
+
51
+ 3. **Set up environment variables**
52
+ Create a `.env` file:
53
+ ```env
54
+ GEMINI_API_KEY=your_gemini_api_key_here
55
+ GEMINI_MODEL=gemini-2.0-flash
56
+ ```
57
+
58
+ 4. **Start the backend server**
59
+ ```bash
60
+ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
61
+ ```
62
+
63
+ The API will be available at `http://localhost:8000` with interactive docs at `http://localhost:8000/docs`
64
+
65
+ ### Frontend Setup
66
+
67
+ 1. **Navigate to frontend directory**
68
+ ```bash
69
+ cd phd-advisor-frontend
70
+ ```
71
+
72
+ 2. **Install dependencies**
73
+ ```bash
74
+ npm install
75
+ ```
76
+
77
+ 3. **Start the development server**
78
+ ```bash
79
+ npm start
80
+ ```
81
+
82
+ The application will open at `http://localhost:3000`
83
+
84
+ ## Advisor Personas
85
+
86
+ ### Methodist - Research Methodology Expert
87
+ - **Focus**: Research design, validity, sampling, methodological rigor
88
+ - **Style**: Precise, analytical, methodology-focused
89
+ - **Best for**: Research design questions, data collection methods, validity concerns
90
+
91
+ ### Theorist - Conceptual Frameworks Expert
92
+ - **Focus**: Theoretical positioning, epistemological assumptions, conceptual clarity
93
+ - **Style**: Thoughtful, intellectually rigorous, theory-oriented
94
+ - **Best for**: Literature review, theoretical frameworks, conceptual development
95
+
96
+ ### Pragmatist - Action-Focused Advisor
97
+ - **Focus**: Practical next steps, immediate actions, progress over perfection
98
+ - **Style**: Warm, motivational, results-oriented
99
+ - **Best for**: Getting unstuck, prioritizing tasks, actionable advice
100
+
101
+ ## API Endpoints
102
+
103
+ ### Core Chat Endpoints
104
+ - `POST /chat-sequential` - Get responses from all advisors in sequence
105
+ - `POST /chat/{persona_id}` - Chat with a specific advisor
106
+ - `POST /reply-to-advisor` - Reply to a specific advisor's message
107
+
108
+ ### Document Management
109
+ - `POST /upload-document` - Upload documents (PDF, DOCX, TXT)
110
+ - `GET /uploaded-files` - List uploaded filenames
111
+ - `GET /context` - View current session context
112
+
113
+ ### Provider Management
114
+ - `GET /current-provider` - Get current LLM provider info
115
+ - `POST /switch-provider` - Switch between Gemini and Ollama
116
+
117
+ ### System
118
+ - `GET /debug/personas` - Debug persona configurations
119
+ - `GET /` - API health check
120
+
121
+ ## Configuration
122
+
123
+ ### Supported LLM Providers
124
+
125
+ **Gemini (Default)**
126
+ - Requires GEMINI_API_KEY environment variable
127
+ - Uses gemini-2.0-flash model by default
128
+ - Cloud-based, requires internet connection
129
+
130
+ **Ollama (Local)**
131
+ - Requires Ollama installation
132
+ - Uses llama3.2:1b model by default
133
+ - Runs locally, no internet required
134
+
135
+ ### File Upload Limits
136
+ - Maximum file size: 10MB per file
137
+ - Total session limit: 50MB
138
+ - Supported formats: PDF, DOCX, TXT
139
+
140
+ ### Environment Variables
141
+ ```env
142
+ # Required for Gemini
143
+ GEMINI_API_KEY=your_api_key
144
+
145
+ # Optional configurations
146
+ GEMINI_MODEL=gemini-2.0-flash
147
+ OLLAMA_BASE_URL=http://localhost:11434
148
+ ```
149
+
150
+ ## Development
151
+
152
+ ### Project Structure
153
+ ```
154
+ phd-advisor-frontend/
155
+ ├── public/
156
+ ├── src/
157
+ │ ├── components/ # Reusable UI components
158
+ │ ├── data/ # Advisor configurations
159
+ │ ├── pages/ # Main page components
160
+ │ ├── styles/ # CSS stylesheets
161
+ │ └── utils/ # Helper functions
162
+
163
+ multi_llm_chatbot_backend/
164
+ ├── app/
165
+ │ ├── api/ # API routes
166
+ │ ├── core/ # Business logic
167
+ │ ├── llm/ # LLM client implementations
168
+ │ ├── models/ # Data models
169
+ │ ├── tests/ # Test files
170
+ │ └── utils/ # Utility functions
171
+ ```
172
+
173
+ ### Adding New Advisors
174
+ 1. Create persona in `app/api/routes.py` with system prompt
175
+ 2. Add advisor configuration in frontend `src/data/advisors.js`
176
+ 3. Update advisor styling in `src/styles/`
177
+
178
+ ### Testing Document Upload
179
+ Use the test script:
180
+ ```bash
181
+ cd multi_llm_chatbot_backend/app/tests
182
+ python test_document_upload.py
183
+ ```
184
+
185
+ ## Usage Examples
186
+
187
+ ### Starting a Conversation
188
+ 1. Navigate to the home page
189
+ 2. Click "Start Conversation"
190
+ 3. Type your PhD-related question
191
+ 4. Receive responses from all three advisors
192
+
193
+ ### Uploading Documents
194
+ 1. In the chat interface, click the upload button
195
+ 2. Select your PDF, DOCX, or TXT file
196
+ 3. The document content will be added to the conversation context
197
+ 4. Ask questions about your uploaded documents
198
+
199
+ ### Switching LLM Providers
200
+ ```bash
201
+ curl -X POST "http://localhost:8000/switch-provider" \
202
+ -H "Content-Type: application/json" \
203
+ -d '{"provider": "ollama"}'
204
+ ```
205
+
206
+ ## Contributing
207
+
208
+ 1. Fork the repository
209
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
210
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
211
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
212
+ 5. Open a Pull Request
213
+
214
+
215
+ ## Support
216
+
217
+ - Check the API documentation at `http://localhost:8000/docs`
218
+ - Review the debug endpoints for troubleshooting
219
+ - Ensure all environment variables are properly configured
220
+ - Verify that your LLM provider (Gemini/Ollama) is accessible
multi_llm_chatbot_backend/app/api/routes.py CHANGED
@@ -13,6 +13,9 @@ from pydantic import BaseModel
13
  from typing import Optional, List
14
  from fastapi import UploadFile, File
15
  from app.utils.document_extractor import extract_text_from_file
 
 
 
16
  from app.utils.file_limits import is_within_upload_limit
17
 
18
  router = APIRouter()
@@ -380,6 +383,22 @@ async def debug_personas():
380
  "current_provider": current_provider
381
  }
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  @router.get("/uploaded-files")
384
  def get_uploaded_filenames():
385
- return {"files": session_context.uploaded_files}
 
13
  from typing import Optional, List
14
  from fastapi import UploadFile, File
15
  from app.utils.document_extractor import extract_text_from_file
16
+ from app.core.orchestrator import answer_with_persona_context
17
+ from app.utils.chroma_client import add_persona_doc
18
+ import hashlib
19
  from app.utils.file_limits import is_within_upload_limit
20
 
21
  router = APIRouter()
 
383
  "current_provider": current_provider
384
  }
385
 
386
+ class PersonaQuery(BaseModel):
387
+ question: str
388
+ persona: str
389
+
390
+ @router.post("/ask/")
391
+ async def ask_question(query: PersonaQuery):
392
+ response = await answer_with_persona_context(query.question, query.persona)
393
+
394
+ # Store Q&A in vector DB
395
+ combined_text = f"Q: {query.question}\nA: {response}"
396
+ doc_id = hashlib.md5(combined_text.encode()).hexdigest() # Create a unique doc ID
397
+
398
+ add_persona_doc(combined_text, query.persona, doc_id)
399
+
400
+ return {"response": response}
401
+
402
  @router.get("/uploaded-files")
403
  def get_uploaded_filenames():
404
+ return {"files": session_context.uploaded_files}
multi_llm_chatbot_backend/app/core/orchestrator.py CHANGED
@@ -1,5 +1,8 @@
1
  from app.models.persona import Persona
2
  from typing import List
 
 
 
3
 
4
  class ChatOrchestrator:
5
  def __init__(self):
@@ -20,4 +23,19 @@ class ChatOrchestrator:
20
  # I have created this function to be a placeholder for the actual logic of response sequencing
21
  # This logic can be replaced with something smarter like a LLM deciding order based on chat context
22
 
23
- return self.personas
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from app.models.persona import Persona
2
  from typing import List
3
+ from app.utils.chroma_client import query_persona_knowledge
4
+ from app.llm.gemini_client import GeminiClient
5
+ from app.llm.llm_client import LLMClient
6
 
7
  class ChatOrchestrator:
8
  def __init__(self):
 
23
  # I have created this function to be a placeholder for the actual logic of response sequencing
24
  # This logic can be replaced with something smarter like a LLM deciding order based on chat context
25
 
26
+ return self.personas
27
+
28
+
29
+
30
+ async def answer_with_persona_context(question: str, persona: str) -> str:
31
+ llm: LLMClient = GeminiClient()
32
+ context_chunks = query_persona_knowledge(question, persona)
33
+ context = "\n".join(context_chunks) if context_chunks else "No relevant info found."
34
+
35
+ system_prompt = f"You are a helpful assistant responding as a {persona}."
36
+ message_context = [
37
+ {"role": "system", "content": system_prompt},
38
+ {"role": "user", "content": f"Context:\n{context}\n\nQuestion:\n{question}"}
39
+ ]
40
+
41
+ return await llm.generate(system_prompt, message_context)
multi_llm_chatbot_backend/app/llm/embedding_client.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers import SentenceTransformer
2
+ import os
3
+
4
+ API_KEY = os.getenv("GEMINI_API_KEY")
5
+
6
+ # Using a compact, fast model good for semantic search
7
+ model = SentenceTransformer("all-MiniLM-L6-v2")
8
+
9
+ def get_embedding(text: str) -> list[float]:
10
+ embedding = model.encode(text, convert_to_numpy=True)
11
+ return embedding.tolist()
multi_llm_chatbot_backend/app/utils/chroma_client.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import chromadb
2
+ from chromadb.config import Settings
3
+ from app.llm.embedding_client import get_embedding
4
+
5
+ client = chromadb.PersistentClient(path="./chroma_storage")
6
+
7
+ collection = client.get_or_create_collection("persona_knowledge")
8
+
9
+ def add_persona_doc(text: str, persona: str, doc_id: str):
10
+ embedding = get_embedding(text)
11
+ collection.add(
12
+ documents=[text],
13
+ embeddings=[embedding],
14
+ metadatas=[{"persona": persona}],
15
+ ids=[doc_id]
16
+ )
17
+
18
+ def query_persona_knowledge(query: str, persona: str):
19
+ query_embedding = get_embedding(query)
20
+ results = collection.query(
21
+ query_embeddings=[query_embedding],
22
+ n_results=3,
23
+ where={"persona": persona}
24
+ )
25
+ return results['documents'][0] if results['documents'] else []
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/data/advisors.js CHANGED
@@ -2,7 +2,7 @@ import { BookOpen, Target, Brain } from 'lucide-react';
2
 
3
  export const advisors = {
4
  methodist: {
5
- name: 'Dr. Methodist',
6
  role: 'Methodical Advisor',
7
  // Light theme colors
8
  color: '#3B82F6',
@@ -15,7 +15,7 @@ export const advisors = {
15
  description: 'Structured & Planning-focused'
16
  },
17
  theorist: {
18
- name: 'Dr. Theorist',
19
  role: 'Theoretical Advisor',
20
  // Light theme colors
21
  color: '#8B5CF6',
@@ -28,7 +28,7 @@ export const advisors = {
28
  description: 'Abstract & Conceptual'
29
  },
30
  pragmatist: {
31
- name: 'Dr. Pragmatist',
32
  role: 'Practical Advisor',
33
  // Light theme colors
34
  color: '#10B981',
 
2
 
3
  export const advisors = {
4
  methodist: {
5
+ name: 'Methodist',
6
  role: 'Methodical Advisor',
7
  // Light theme colors
8
  color: '#3B82F6',
 
15
  description: 'Structured & Planning-focused'
16
  },
17
  theorist: {
18
+ name: 'Theorist',
19
  role: 'Theoretical Advisor',
20
  // Light theme colors
21
  color: '#8B5CF6',
 
28
  description: 'Abstract & Conceptual'
29
  },
30
  pragmatist: {
31
+ name: 'Pragmatist',
32
  role: 'Practical Advisor',
33
  // Light theme colors
34
  color: '#10B981',
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}
@@ -355,7 +404,7 @@ const ChatPage = ({ onNavigateToHome }) => {
355
  </div>
356
  <div className="orchestrator-content">
357
  <div className="orchestrator-header">
358
- <span className="orchestrator-name">PhD Assistant</span>
359
  <span className="message-timestamp">
360
  {message.timestamp.toLocaleTimeString([], {
361
  hour: '2-digit',
@@ -378,6 +427,17 @@ const ChatPage = ({ onNavigateToHome }) => {
378
  </div>
379
  );
380
  }
 
 
 
 
 
 
 
 
 
 
 
381
 
382
  return (
383
  <MessageBubble
@@ -396,7 +456,7 @@ const ChatPage = ({ onNavigateToHome }) => {
396
  <MessageCircle size={20} />
397
  </div>
398
  <div className="thinking-content">
399
- <span className="thinking-label">PhD Assistant is thinking...</span>
400
  <div className="thinking-animation">
401
  <div className="dot"></div>
402
  <div className="dot"></div>
@@ -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}
 
404
  </div>
405
  <div className="orchestrator-content">
406
  <div className="orchestrator-header">
407
+ <span className="orchestrator-name">Orchestrator</span>
408
  <span className="message-timestamp">
409
  {message.timestamp.toLocaleTimeString([], {
410
  hour: '2-digit',
 
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
 
456
  <MessageCircle size={20} />
457
  </div>
458
  <div className="thinking-content">
459
+ <span className="thinking-label">Orchestrator is thinking...</span>
460
  <div className="thinking-animation">
461
  <div className="dot"></div>
462
  <div className="dot"></div>
 
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