Apurva Umredkar commited on
Commit
669b6be
·
1 Parent(s): cd87b87

added frontend files

Browse files
Dockerfile CHANGED
@@ -2,4 +2,8 @@ FROM python:3.9-slim
2
  WORKDIR /app
3
  COPY requirements.txt ./
4
  RUN pip install --no-cache-dir -r requirements.txt
5
- RUN echo "Installed required Python packages."
 
 
 
 
 
2
  WORKDIR /app
3
  COPY requirements.txt ./
4
  RUN pip install --no-cache-dir -r requirements.txt
5
+ RUN echo "Installed required Python packages."
6
+
7
+ COPY frontend/ ./frontend
8
+ EXPOSE 7860
9
+ CMD ["python", "frontend/flask_app.py"]
frontend/app.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import requests
4
+ from datetime import datetime
5
+ import streamlit as st
6
+ import pandas as pd
7
+
8
+ # API configuration
9
+ API_URL = "http://localhost:8000/api"
10
+
11
+ # Set page config
12
+ st.set_page_config(
13
+ page_title="BuffaloRAG - UB International Student Assistant",
14
+ page_icon="🦬",
15
+ layout="wide",
16
+ initial_sidebar_state="expanded"
17
+ )
18
+
19
+ # Styling
20
+ st.markdown("""
21
+ <style>
22
+ .main {
23
+ background-color: #f0f2f6;
24
+ }
25
+ .sidebar .sidebar-content {
26
+ background-color: #2d51a3;
27
+ color: white;
28
+ }
29
+ .stButton>button {
30
+ background-color: #2d51a3;
31
+ color: white;
32
+ }
33
+ .source-card {
34
+ background-color: white;
35
+ padding: 10px;
36
+ border-radius: 5px;
37
+ margin-bottom: 10px;
38
+ border-left: 4px solid #2d51a3;
39
+ }
40
+ </style>
41
+ """, unsafe_allow_html=True)
42
+
43
+ # App title
44
+ st.title("🦬 BuffaloRAG - UB International Student Assistant")
45
+ st.markdown("Ask questions about resources, policies, and procedures for international students at the University at Buffalo.")
46
+
47
+ # Initialize session state
48
+ if 'messages' not in st.session_state:
49
+ st.session_state.messages = []
50
+
51
+ if 'sources' not in st.session_state:
52
+ st.session_state.sources = []
53
+
54
+ # Sidebar content
55
+ with st.sidebar:
56
+ st.image("https://www.buffalo.edu/content/www/brand/identity/university-logo/_jcr_content/bottompar/image.img.1280.high.jpg/1629127701437.jpg", width=200)
57
+ st.header("Useful Links")
58
+
59
+ # Common categories
60
+ categories = {
61
+ "Immigration": [
62
+ ("I-20 Information", "https://www.buffalo.edu/international-student-services/immigration/maintainingVisaStatus/i-20.html"),
63
+ ("SEVIS Transfer", "https://www.buffalo.edu/international-student-services/immigration/sevis-transfer.html"),
64
+ ("Visa Information", "https://www.buffalo.edu/international-student-services/immigration/visa.html")
65
+ ],
66
+ "Employment": [
67
+ ("OPT Information", "https://www.buffalo.edu/international-student-services/immigration/f1-student/employment/opt.html"),
68
+ ("CPT Information", "https://www.buffalo.edu/international-student-services/immigration/f1-student/employment/cpt.html"),
69
+ ("On-Campus Employment", "https://www.buffalo.edu/international-student-services/immigration/f1-student/employment/on-campus.html")
70
+ ],
71
+ "Student Life": [
72
+ ("Housing", "https://www.buffalo.edu/campusliving.html"),
73
+ ("Dining", "https://www.buffalo.edu/campusdining.html"),
74
+ ("Student Clubs", "https://www.buffalo.edu/studentlife/life-on-campus/clubs.html")
75
+ ],
76
+ "Academics": [
77
+ ("Academic Calendar", "https://registrar.buffalo.edu/calendars/academic/"),
78
+ ("Course Registration", "https://registrar.buffalo.edu/registration/"),
79
+ ("Library", "https://library.buffalo.edu/")
80
+ ]
81
+ }
82
+
83
+ # Display links by category
84
+ for category, links in categories.items():
85
+ st.subheader(category)
86
+ for name, url in links:
87
+ st.markdown(f"[{name}]({url})")
88
+
89
+ # Admin section (hidden in collapsed section)
90
+ with st.expander("Admin Tools"):
91
+ if st.button("Scrape New Content"):
92
+ with st.spinner("Starting scraper..."):
93
+ response = requests.post(f"{API_URL}/scrape", json={
94
+ "seed_url": "https://www.buffalo.edu/international-student-services.html",
95
+ "max_pages": 100
96
+ })
97
+ if response.status_code == 200:
98
+ st.success("Scraping started successfully!")
99
+ else:
100
+ st.error(f"Error starting scraper: {response.text}")
101
+
102
+ if st.button("Refresh Index"):
103
+ with st.spinner("Refreshing index..."):
104
+ response = requests.post(f"{API_URL}/refresh-index")
105
+ if response.status_code == 200:
106
+ st.success("Index refresh started successfully!")
107
+ else:
108
+ st.error(f"Error refreshing index: {response.text}")
109
+
110
+ # Main content area - Chat interface
111
+ col1, col2 = st.columns([2, 1])
112
+
113
+ with col1:
114
+ # Display chat messages
115
+ for message in st.session_state.messages:
116
+ with st.chat_message(message["role"]):
117
+ st.markdown(message["content"])
118
+
119
+ # Input box for new questions
120
+ if prompt := st.chat_input("Ask a question about UB international student services..."):
121
+ # Add user message to chat history
122
+ st.session_state.messages.append({"role": "user", "content": prompt})
123
+
124
+ # Display user message
125
+ with st.chat_message("user"):
126
+ st.markdown(prompt)
127
+
128
+ # Call API to get response
129
+ with st.spinner("Thinking..."):
130
+ try:
131
+ response = requests.post(f"{API_URL}/ask", json={"query": prompt})
132
+
133
+ if response.status_code == 200:
134
+ result = response.json()
135
+ answer = result["response"]
136
+ sources = result["sources"]
137
+
138
+ # Store sources in session state
139
+ st.session_state.sources = sources
140
+
141
+ # Add assistant message to chat history
142
+ st.session_state.messages.append({"role": "assistant", "content": answer})
143
+
144
+ # Display assistant message
145
+ with st.chat_message("assistant"):
146
+ st.markdown(answer)
147
+ else:
148
+ st.error(f"Error: {response.text}")
149
+ except Exception as e:
150
+ st.error(f"Error: {str(e)}")
151
+
152
+ with col2:
153
+ # Display sources for the latest response
154
+ st.subheader("Sources")
155
+
156
+ if st.session_state.sources:
157
+ for i, source in enumerate(st.session_state.sources):
158
+ st.markdown(f"""
159
+ <div class="source-card">
160
+ <h4>{source['title']}</h4>
161
+ <p><a href="{source['url']}" target="_blank">View source</a></p>
162
+ <small>Relevance: {source['score']:.2f}</small>
163
+ </div>
164
+ """, unsafe_allow_html=True)
165
+ else:
166
+ st.info("Ask a question to see relevant sources.")
167
+
168
+ # Footer
169
+ st.markdown("---")
170
+ st.markdown("Built with BuffaloRAG - AI Assistant for International Students at UB")
frontend/flask_app.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import requests
3
+ import os
4
+ import json
5
+ from datetime import datetime
6
+
7
+ app = Flask(__name__)
8
+
9
+ # API configuration
10
+ API_URL = "http://localhost:8000/api"
11
+
12
+ @app.route('/')
13
+ def index():
14
+ """Render the main page."""
15
+ return render_template('index.html')
16
+
17
+ @app.route('/api/ask', methods=['POST'])
18
+ def ask():
19
+ """Proxy API call to the FastAPI backend."""
20
+ data = request.json
21
+ response = requests.post(f"{API_URL}/ask", json=data)
22
+ return jsonify(response.json())
23
+
24
+ @app.route('/api/scrape', methods=['POST'])
25
+ def scrape():
26
+ """Proxy API call to trigger web scraping."""
27
+ data = request.json
28
+ response = requests.post(f"{API_URL}/scrape", json=data)
29
+ return jsonify(response.json())
30
+
31
+ @app.route('/api/refresh-index', methods=['POST'])
32
+ def refresh_index():
33
+ """Proxy API call to refresh the vector index."""
34
+ response = requests.post(f"{API_URL}/refresh-index")
35
+ return jsonify(response.json())
36
+
37
+ if __name__ == '__main__':
38
+ app.run(debug=True, port=5000)
frontend/static/css/styles.css ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ /* Variables */
3
+ :root {
4
+ --primary-color: #005bbb;
5
+ --primary-dark: #004798;
6
+ --secondary-color: #e87722;
7
+ --background-color: #f5f7fa;
8
+ --sidebar-color: #f0f5ff;
9
+ --right-panel-color: #f8fafd;
10
+ --border-color: #e1e5eb;
11
+ --text-color: #333333;
12
+ --text-secondary: #666666;
13
+ --card-color: #ffffff;
14
+ --hover-color: #f8f9fa;
15
+ --disabled-color: #cccccc;
16
+ }
17
+
18
+ /* Global Styles */
19
+ * {
20
+ box-sizing: border-box;
21
+ margin: 0;
22
+ padding: 0;
23
+ }
24
+
25
+ body {
26
+ font-family: 'Inter', sans-serif;
27
+ background-color: var(--background-color);
28
+ color: var(--text-color);
29
+ line-height: 1.5;
30
+ }
31
+
32
+ h1, h2, h3, h4, h5, h6 {
33
+ font-weight: 600;
34
+ }
35
+
36
+ /* App Container */
37
+ .app-container {
38
+ display: grid;
39
+ grid-template-columns: 280px 1fr 300px;
40
+ height: 100vh;
41
+ overflow: hidden;
42
+ }
43
+
44
+ /* Sidebar */
45
+ .sidebar {
46
+ background-color: var(--sidebar-color);
47
+ padding: 1.5rem;
48
+ overflow-y: auto;
49
+ border-right: 1px solid var(--border-color);
50
+ }
51
+
52
+ .logo {
53
+ display: flex;
54
+ align-items: center;
55
+ margin-bottom: 2rem;
56
+ }
57
+
58
+ .logo img {
59
+ width: 40px;
60
+ height: 40px;
61
+ margin-right: 10px;
62
+ }
63
+
64
+ .logo h1 {
65
+ font-size: 1.2rem;
66
+ color: var(--primary-color);
67
+ margin: 0;
68
+ }
69
+
70
+ .search-box {
71
+ display: flex;
72
+ margin-bottom: 1.5rem;
73
+ }
74
+
75
+ .search-box input {
76
+ flex: 1;
77
+ padding: 0.75rem;
78
+ border: 1px solid var(--border-color);
79
+ border-radius: 8px 0 0 8px;
80
+ font-size: 0.9rem;
81
+ outline: none;
82
+ }
83
+
84
+ .search-box input:focus {
85
+ border-color: var(--primary-color);
86
+ }
87
+
88
+ .search-box button {
89
+ background-color: var(--primary-color);
90
+ color: white;
91
+ border: none;
92
+ border-radius: 0 8px 8px 0;
93
+ padding: 0 1rem;
94
+ cursor: pointer;
95
+ }
96
+
97
+ .search-box button:hover {
98
+ background-color: var(--primary-dark);
99
+ }
100
+
101
+ .category {
102
+ margin-bottom: 1.5rem;
103
+ }
104
+
105
+ .category h3 {
106
+ background-color: var(--primary-color);
107
+ color: white;
108
+ padding: 0.5rem 1rem;
109
+ border-radius: 6px;
110
+ font-size: 1rem;
111
+ margin-bottom: 0.75rem;
112
+ }
113
+
114
+ .link-card {
115
+ display: block;
116
+ padding: 0.75rem;
117
+ background-color: white;
118
+ border-left: 3px solid var(--secondary-color);
119
+ border-radius: 4px;
120
+ margin-bottom: 0.5rem;
121
+ text-decoration: none;
122
+ color: var(--text-color);
123
+ transition: transform 0.2s, box-shadow 0.2s;
124
+ }
125
+
126
+ .link-card:hover {
127
+ transform: translateY(-2px);
128
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
129
+ background-color: var(--hover-color);
130
+ }
131
+
132
+ .link-card h4 {
133
+ font-size: 0.9rem;
134
+ margin: 0 0 0.25rem 0;
135
+ color: var(--primary-color);
136
+ }
137
+
138
+ .link-card p {
139
+ font-size: 0.8rem;
140
+ margin: 0;
141
+ color: var(--text-secondary);
142
+ }
143
+
144
+ /* Main Content */
145
+ .main-content {
146
+ display: flex;
147
+ flex-direction: column;
148
+ height: 100vh;
149
+ }
150
+
151
+ header {
152
+ padding: 1rem 1.5rem;
153
+ background-color: white;
154
+ border-bottom: 1px solid var(--border-color);
155
+ }
156
+
157
+ header h1 {
158
+ font-size: 1.25rem;
159
+ display: flex;
160
+ align-items: center;
161
+ }
162
+
163
+ header h1 i {
164
+ margin-right: 0.5rem;
165
+ color: var(--primary-color);
166
+ }
167
+
168
+ .chat-container {
169
+ flex: 1;
170
+ padding: 1.5rem;
171
+ overflow-y: auto;
172
+ scroll-behavior: smooth;
173
+ }
174
+
175
+ .empty-chat {
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ justify-content: center;
180
+ height: 100%;
181
+ padding: 2rem;
182
+ text-align: center;
183
+ }
184
+
185
+ .empty-chat h2 {
186
+ font-size: 1.5rem;
187
+ margin-bottom: 1rem;
188
+ color: var(--primary-color);
189
+ }
190
+
191
+ .empty-chat p {
192
+ color: var(--text-secondary);
193
+ margin-bottom: 2rem;
194
+ max-width: 500px;
195
+ }
196
+
197
+ .quick-questions {
198
+ display: flex;
199
+ flex-wrap: wrap;
200
+ justify-content: center;
201
+ gap: 0.5rem;
202
+ }
203
+
204
+ .quick-question {
205
+ background-color: rgba(232, 119, 34, 0.1);
206
+ color: var(--secondary-color);
207
+ border: 1px solid rgba(232, 119, 34, 0.3);
208
+ border-radius: 8px;
209
+ padding: 0.5rem 1rem;
210
+ font-size: 0.85rem;
211
+ cursor: pointer;
212
+ transition: all 0.2s;
213
+ }
214
+
215
+ .quick-question:hover {
216
+ background-color: rgba(232, 119, 34, 0.2);
217
+ }
218
+
219
+ .messages {
220
+ display: flex;
221
+ flex-direction: column;
222
+ }
223
+
224
+ .message {
225
+ margin-bottom: 1rem;
226
+ max-width: 85%;
227
+ animation: fadeIn 0.3s ease-in-out;
228
+ }
229
+
230
+ @keyframes fadeIn {
231
+ from { opacity: 0; transform: translateY(20px); }
232
+ to { opacity: 1; transform: translateY(0); }
233
+ }
234
+
235
+ .message.user {
236
+ margin-left: auto;
237
+ }
238
+
239
+ .message.assistant {
240
+ margin-right: auto;
241
+ }
242
+
243
+ .bubble {
244
+ padding: 1rem;
245
+ border-radius: 1.2rem;
246
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
247
+ }
248
+
249
+ .message.user .bubble {
250
+ background-color: var(--primary-color);
251
+ color: white;
252
+ border-bottom-right-radius: 0.25rem;
253
+ }
254
+
255
+ .message.assistant .bubble {
256
+ background-color: white;
257
+ border: 1px solid var(--border-color);
258
+ border-bottom-left-radius: 0.25rem;
259
+ }
260
+
261
+ .meta {
262
+ font-size: 0.75rem;
263
+ margin-top: 0.25rem;
264
+ color: var(--text-secondary);
265
+ }
266
+
267
+ .message.user .meta {
268
+ text-align: right;
269
+ }
270
+
271
+ .message.assistant .meta {
272
+ text-align: left;
273
+ }
274
+
275
+ .input-container {
276
+ border-top: 1px solid var(--border-color);
277
+ padding: 1rem 1.5rem;
278
+ background-color: white;
279
+ }
280
+
281
+ .message-input {
282
+ display: flex;
283
+ background-color: white;
284
+ border-radius: 8px;
285
+ overflow: hidden;
286
+ border: 1px solid var(--border-color);
287
+ }
288
+
289
+ .message-input input {
290
+ flex: 1;
291
+ padding: 1rem;
292
+ border: none;
293
+ outline: none;
294
+ font-size: 0.95rem;
295
+ }
296
+
297
+ .message-input button {
298
+ background-color: var(--primary-color);
299
+ color: white;
300
+ border: none;
301
+ padding: 0 1.25rem;
302
+ cursor: pointer;
303
+ transition: background-color 0.2s;
304
+ }
305
+
306
+ .message-input button:hover {
307
+ background-color: var(--primary-dark);
308
+ }
309
+
310
+ .message-input button:disabled {
311
+ background-color: var(--disabled-color);
312
+ cursor: not-allowed;
313
+ }
314
+
315
+ /* Right Panel */
316
+ .right-panel {
317
+ background-color: var(--right-panel-color);
318
+ padding: 1.5rem;
319
+ overflow-y: auto;
320
+ border-left: 1px solid var(--border-color);
321
+ }
322
+
323
+ .right-panel h3 {
324
+ margin-bottom: 1rem;
325
+ font-size: 1.1rem;
326
+ }
327
+
328
+ .source-card {
329
+ background-color: white;
330
+ border-radius: 8px;
331
+ padding: 1rem;
332
+ margin-bottom: 1rem;
333
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
334
+ }
335
+
336
+ .source-card h4 {
337
+ font-size: 0.95rem;
338
+ margin: 0 0 0.5rem 0;
339
+ color: var(--primary-color);
340
+ }
341
+
342
+ .source-card .link {
343
+ display: flex;
344
+ align-items: center;
345
+ font-size: 0.8rem;
346
+ color: var(--secondary-color);
347
+ text-decoration: none;
348
+ margin-bottom: 0.25rem;
349
+ }
350
+
351
+ .source-card .link i {
352
+ margin-left: 0.25rem;
353
+ }
354
+
355
+ .source-card .score {
356
+ font-size: 0.75rem;
357
+ color: var(--text-secondary);
358
+ }
359
+
360
+ .admin-tools {
361
+ margin-top: 2rem;
362
+ padding-top: 1rem;
363
+ border-top: 1px solid var(--border-color);
364
+ }
365
+
366
+ .admin-btn {
367
+ display: block;
368
+ width: 100%;
369
+ padding: 0.75rem;
370
+ margin-bottom: 0.75rem;
371
+ background-color: var(--primary-color);
372
+ color: white;
373
+ border: none;
374
+ border-radius: 6px;
375
+ cursor: pointer;
376
+ transition: background-color 0.2s;
377
+ }
378
+
379
+ .admin-btn:hover {
380
+ background-color: var(--primary-dark);
381
+ }
382
+
383
+ /* Responsive */
384
+ @media (max-width: 1024px) {
385
+ .app-container {
386
+ grid-template-columns: 0px 1fr 0px;
387
+ }
388
+
389
+ .sidebar, .right-panel {
390
+ display: none;
391
+ }
392
+ }
393
+
frontend/static/img/ub.png ADDED
frontend/static/js/app.js ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ // DOM Elements
3
+ const messagesContainer = document.getElementById('messages');
4
+ const messageInput = document.getElementById('message-input');
5
+ const sendButton = document.getElementById('send-button');
6
+ const emptyChat = document.getElementById('empty-chat');
7
+ const chatContainer = document.getElementById('chat-container');
8
+ const sourcesContainer = document.getElementById('sources-container');
9
+ const noSources = document.getElementById('no-sources');
10
+ const scrapeBtn = document.getElementById('scrape-btn');
11
+ const refreshBtn = document.getElementById('refresh-btn');
12
+
13
+ // State
14
+ let messages = [];
15
+ let sources = [];
16
+
17
+ // Initialize
18
+ document.addEventListener('DOMContentLoaded', () => {
19
+ // Enable/disable send button based on input
20
+ messageInput.addEventListener('input', () => {
21
+ sendButton.disabled = !messageInput.value.trim();
22
+ });
23
+
24
+ // Send message on button click
25
+ sendButton.addEventListener('click', sendMessage);
26
+
27
+ // Send message on Enter key
28
+ messageInput.addEventListener('keypress', (e) => {
29
+ if (e.key === 'Enter') {
30
+ sendMessage();
31
+ }
32
+ });
33
+
34
+ // Admin buttons
35
+ scrapeBtn.addEventListener('click', triggerScrape);
36
+ refreshBtn.addEventListener('click', triggerRefreshIndex);
37
+ });
38
+
39
+ // Format date
40
+ function formatDate() {
41
+ return new Date().toLocaleString();
42
+ }
43
+
44
+ // Create message element
45
+ function createMessageElement(message) {
46
+ const messageEl = document.createElement('div');
47
+ messageEl.className = `message ${message.role}`;
48
+
49
+ const bubble = document.createElement('div');
50
+ bubble.className = 'bubble';
51
+ bubble.textContent = message.content;
52
+
53
+ const meta = document.createElement('div');
54
+ meta.className = 'meta';
55
+ meta.textContent = formatDate();
56
+
57
+ messageEl.appendChild(bubble);
58
+ messageEl.appendChild(meta);
59
+
60
+ return messageEl;
61
+ }
62
+
63
+ // Update sources panel
64
+ function updateSources(sources) {
65
+ sourcesContainer.innerHTML = '';
66
+
67
+ if (sources.length === 0) {
68
+ sourcesContainer.appendChild(noSources);
69
+ return;
70
+ }
71
+
72
+ sources.forEach(source => {
73
+ const card = document.createElement('div');
74
+ card.className = 'source-card';
75
+
76
+ const title = document.createElement('h4');
77
+ title.textContent = source.title;
78
+
79
+ const link = document.createElement('a');
80
+ link.className = 'link';
81
+ link.href = source.url;
82
+ link.target = '_blank';
83
+ link.innerHTML = `View source <i class="fas fa-external-link-alt"></i>`;
84
+
85
+ const score = document.createElement('div');
86
+ score.className = 'score';
87
+ score.textContent = `Relevance: ${source.score.toFixed(2)}`;
88
+
89
+ card.appendChild(title);
90
+ card.appendChild(link);
91
+ card.appendChild(score);
92
+
93
+ sourcesContainer.appendChild(card);
94
+ });
95
+ }
96
+
97
+ // Send message
98
+ async function sendMessage() {
99
+ const content = messageInput.value.trim();
100
+ if (!content) return;
101
+
102
+ // Hide empty chat if visible
103
+ if (emptyChat.style.display !== 'none') {
104
+ emptyChat.style.display = 'none';
105
+ }
106
+
107
+ // Add user message
108
+ const userMessage = { role: 'user', content };
109
+ messages.push(userMessage);
110
+ messagesContainer.appendChild(createMessageElement(userMessage));
111
+
112
+ // Clear input
113
+ messageInput.value = '';
114
+ sendButton.disabled = true;
115
+
116
+ // Show thinking message
117
+ const thinkingEl = document.createElement('div');
118
+ thinkingEl.className = 'message assistant';
119
+
120
+ const thinkingBubble = document.createElement('div');
121
+ thinkingBubble.className = 'bubble';
122
+ thinkingBubble.textContent = 'Thinking...';
123
+
124
+ thinkingEl.appendChild(thinkingBubble);
125
+ messagesContainer.appendChild(thinkingEl);
126
+
127
+ // Scroll to bottom
128
+ chatContainer.scrollTop = chatContainer.scrollHeight;
129
+
130
+ try {
131
+ // Call API
132
+ const response = await fetch('/api/ask', {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ },
137
+ body: JSON.stringify({
138
+ query: content,
139
+ k: 5
140
+ }),
141
+ });
142
+
143
+ if (!response.ok) {
144
+ throw new Error('API request failed');
145
+ }
146
+
147
+ const data = await response.json();
148
+
149
+ // Remove thinking message
150
+ messagesContainer.removeChild(thinkingEl);
151
+
152
+ // Add assistant message
153
+ const assistantMessage = { role: 'assistant', content: data.response };
154
+ messages.push(assistantMessage);
155
+ messagesContainer.appendChild(createMessageElement(assistantMessage));
156
+
157
+ // Update sources
158
+ sources = data.sources;
159
+ updateSources(sources);
160
+
161
+ // Scroll to bottom
162
+ chatContainer.scrollTop = chatContainer.scrollHeight;
163
+
164
+ } catch (error) {
165
+ console.error('Error:', error);
166
+
167
+ // Remove thinking message
168
+ messagesContainer.removeChild(thinkingEl);
169
+
170
+ // Add error message
171
+ const errorMessage = {
172
+ role: 'assistant',
173
+ content: "I'm sorry, I encountered an error. Please try again later or contact UB International Student Services directly."
174
+ };
175
+ messages.push(errorMessage);
176
+ messagesContainer.appendChild(createMessageElement(errorMessage));
177
+
178
+ // Scroll to bottom
179
+ chatContainer.scrollTop = chatContainer.scrollHeight;
180
+ }
181
+ }
182
+
183
+ // Quick questions
184
+ async function askQuickQuestion(question) {
185
+ // Set the question in the input field
186
+ messageInput.value = question;
187
+
188
+ // Send the message
189
+ await sendMessage();
190
+ }
191
+
192
+ // Admin functions
193
+ async function triggerScrape() {
194
+ scrapeBtn.disabled = true;
195
+ scrapeBtn.textContent = 'Scraping...';
196
+
197
+ try {
198
+ const response = await fetch('/api/scrape', {
199
+ method: 'POST',
200
+ headers: {
201
+ 'Content-Type': 'application/json',
202
+ },
203
+ body: JSON.stringify({
204
+ seed_url: 'https://www.buffalo.edu/international-student-services.html',
205
+ max_pages: 100
206
+ }),
207
+ });
208
+
209
+ if (!response.ok) {
210
+ throw new Error('API request failed');
211
+ }
212
+
213
+ const data = await response.json();
214
+ alert(`${data.message}`);
215
+
216
+ } catch (error) {
217
+ console.error('Error:', error);
218
+ alert('Error starting scraper. Please check console for details.');
219
+
220
+ } finally {
221
+ scrapeBtn.disabled = false;
222
+ scrapeBtn.textContent = 'Scrape New Content';
223
+ }
224
+ }
225
+
226
+ async function triggerRefreshIndex() {
227
+ refreshBtn.disabled = true;
228
+ refreshBtn.textContent = 'Refreshing...';
229
+
230
+ try {
231
+ const response = await fetch('/api/refresh-index', {
232
+ method: 'POST',
233
+ headers: {
234
+ 'Content-Type': 'application/json',
235
+ },
236
+ });
237
+
238
+ if (!response.ok) {
239
+ throw new Error('API request failed');
240
+ }
241
+
242
+ const data = await response.json();
243
+ alert(`${data.message}`);
244
+
245
+ } catch (error) {
246
+ console.error('Error:', error);
247
+ alert('Error refreshing index. Please check console for details.');
248
+
249
+ } finally {
250
+ refreshBtn.disabled = false;
251
+ refreshBtn.textContent = 'Refresh Index';
252
+ }
253
+ }
254
+
frontend/templates/index.html ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>BuffaloRAG - UB International Student Assistant</title>
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap">
9
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
10
+ </head>
11
+ <body>
12
+ <div class="app-container">
13
+ <!-- Left Sidebar -->
14
+ <div class="sidebar">
15
+ <div class="logo">
16
+ <img src="{{ url_for('static', filename='img/ub-logo.png') }}" alt="UB Logo">
17
+ <h1>BuffaloRAG</h1>
18
+ </div>
19
+
20
+ <div class="search-box">
21
+ <input type="text" id="resource-search" placeholder="Search resources...">
22
+ <button id="search-btn"><i class="fas fa-search"></i></button>
23
+ </div>
24
+
25
+ <!-- Categories -->
26
+ <div class="categories">
27
+ <div class="category">
28
+ <h3>Immigration</h3>
29
+ <div class="links">
30
+ <a href="https://www.buffalo.edu/international-student-services/immigration/visa.html" target="_blank" class="link-card">
31
+ <h4>Visa Requirements</h4>
32
+ <p>Information about F-1 and J-1 visas</p>
33
+ </a>
34
+ <a href="https://www.buffalo.edu/international-student-services/immigration/maintainingVisaStatus/i-20.html" target="_blank" class="link-card">
35
+ <h4>I-20 Documents</h4>
36
+ <p>Understanding your I-20 form</p>
37
+ </a>
38
+ <a href="https://www.buffalo.edu/international-student-services/immigration/sevis-transfer.html" target="_blank" class="link-card">
39
+ <h4>SEVIS Transfers</h4>
40
+ <p>Transferring your SEVIS record</p>
41
+ </a>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="category">
46
+ <h3>Employment</h3>
47
+ <div class="links">
48
+ <a href="https://www.buffalo.edu/international-student-services/immigration/f1-student/employment/opt.html" target="_blank" class="link-card">
49
+ <h4>OPT Guidelines</h4>
50
+ <p>Optional Practical Training information</p>
51
+ </a>
52
+ <a href="https://www.buffalo.edu/international-student-services/immigration/f1-student/employment/cpt.html" target="_blank" class="link-card">
53
+ <h4>CPT Information</h4>
54
+ <p>Curricular Practical Training details</p>
55
+ </a>
56
+ <a href="https://www.buffalo.edu/international-student-services/immigration/f1-student/employment/on-campus.html" target="_blank" class="link-card">
57
+ <h4>On-Campus Jobs</h4>
58
+ <p>Working on UB campus</p>
59
+ </a>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="category">
64
+ <h3>Student Life</h3>
65
+ <div class="links">
66
+ <a href="https://www.buffalo.edu/campusliving.html" target="_blank" class="link-card">
67
+ <h4>Housing Options</h4>
68
+ <p>On and off-campus housing</p>
69
+ </a>
70
+ <a href="https://www.buffalo.edu/international-student-services/life-in-buffalo/health-insurance.html" target="_blank" class="link-card">
71
+ <h4>Health Insurance</h4>
72
+ <p>Insurance requirements and options</p>
73
+ </a>
74
+ <a href="https://www.buffalo.edu/studentlife.html" target="_blank" class="link-card">
75
+ <h4>Student Services</h4>
76
+ <p>Campus resources and services</p>
77
+ </a>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Main Content -->
84
+ <div class="main-content">
85
+ <header>
86
+ <h1><i class="fas fa-university"></i> BuffaloRAG - UB International Student Assistant</h1>
87
+ </header>
88
+
89
+ <div class="chat-container" id="chat-container">
90
+ <div class="empty-chat" id="empty-chat">
91
+ <h2>Welcome to BuffaloRAG</h2>
92
+ <p>
93
+ I'm here to help answer your questions about resources, policies, and procedures
94
+ for international students at the University at Buffalo.
95
+ </p>
96
+ <div class="quick-questions">
97
+ <button class="quick-question" onclick="askQuickQuestion('How do I apply for OPT?')">How do I apply for OPT?</button>
98
+ <button class="quick-question" onclick="askQuickQuestion('What documents do I need for my I-20?')">What documents do I need for my I-20?</button>
99
+ <button class="quick-question" onclick="askQuickQuestion('What are the working hour limits for international students?')">What are the working hour limits for international students?</button>
100
+ <button class="quick-question" onclick="askQuickQuestion('How can I extend my student visa?')">How can I extend my student visa?</button>
101
+ <button class="quick-question" onclick="askQuickQuestion('Where can I find housing as an international student?')">Where can I find housing as an international student?</button>
102
+ <button class="quick-question" onclick="askQuickQuestion('What health insurance options are available?')">What health insurance options are available?</button>
103
+ </div>
104
+ </div>
105
+
106
+ <div class="messages" id="messages"></div>
107
+ </div>
108
+
109
+ <div class="input-container">
110
+ <div class="message-input">
111
+ <input type="text" id="message-input" placeholder="Ask a question...">
112
+ <button id="send-button" disabled><i class="fas fa-paper-plane"></i></button>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ <!-- Right Panel -->
118
+ <div class="right-panel">
119
+ <h3>Sources & References</h3>
120
+ <div id="sources-container">
121
+ <p id="no-sources">Ask a question to see relevant sources.</p>
122
+ </div>
123
+
124
+ <h3>Important Resources</h3>
125
+ <div class="source-card">
126
+ <h4>UB International Student Services</h4>
127
+ <a class="link" href="https://www.buffalo.edu/international-student-services.html" target="_blank">
128
+ Visit website <i class="fas fa-external-link-alt"></i>
129
+ </a>
130
+ </div>
131
+ <div class="source-card">
132
+ <h4>Immigration Forms & Documents</h4>
133
+ <a class="link" href="https://www.buffalo.edu/international-student-services/immigration/forms.html" target="_blank">
134
+ View forms <i class="fas fa-external-link-alt"></i>
135
+ </a>
136
+ </div>
137
+
138
+ <div class="admin-tools">
139
+ <h3>Admin Tools</h3>
140
+ <button id="scrape-btn" class="admin-btn">Scrape New Content</button>
141
+ <button id="refresh-btn" class="admin-btn">Refresh Index</button>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
147
+ <script src="{{ url_for('static', filename='js/app.js') }}"></script>
148
+ </body>
149
+ </html>
150
+
requirements.txt CHANGED
@@ -1,4 +1,9 @@
1
  torch
2
  transformers
3
  sentence-transformers
4
- faiss-cpu
 
 
 
 
 
 
1
  torch
2
  transformers
3
  sentence-transformers
4
+ faiss-cpu
5
+
6
+ flask
7
+ fastapi
8
+ requests
9
+ json