LiamKhoaLe commited on
Commit
a30fd70
·
1 Parent(s): 158d492

Upd stylings

Browse files
Files changed (4) hide show
  1. static/auth.js +86 -38
  2. static/index.html +103 -33
  3. static/script.js +381 -72
  4. static/styles.css +793 -95
static/auth.js CHANGED
@@ -1,24 +1,32 @@
1
  // ────────────────────────────── static/auth.js ──────────────────────────────
2
  (function() {
3
  const modal = document.getElementById('auth-modal');
4
- const openBtn = document.getElementById('open-auth');
5
- const closeBtn = document.getElementById('close-auth');
6
  const logoutBtn = document.getElementById('logout');
7
- const authStatus = document.getElementById('auth-status');
 
 
8
  const tabs = document.querySelectorAll('.tab');
9
  const tabLogin = document.getElementById('tab-login');
10
  const tabSignup = document.getElementById('tab-signup');
11
- const themeToggle = document.getElementById('theme-toggle');
 
12
 
13
  function setAuthUI(user) {
14
  if (user && user.user_id) {
15
- authStatus.textContent = `Signed in as ${user.email}`;
16
- logoutBtn.style.display = '';
17
- openBtn.style.display = 'none';
 
 
 
 
18
  } else {
19
- authStatus.textContent = 'Not signed in';
20
- logoutBtn.style.display = 'none';
21
- openBtn.style.display = '';
 
 
 
22
  }
23
  }
24
 
@@ -38,10 +46,28 @@
38
  setAuthUI(null);
39
  }
40
 
41
- // Initialize UI
42
- setAuthUI(getUser());
 
 
 
 
 
 
 
43
 
44
- // Theme
 
 
 
 
 
 
 
 
 
 
 
45
  (function initTheme() {
46
  const saved = localStorage.getItem('sb_theme');
47
  if (saved === 'light') document.documentElement.classList.add('light');
@@ -55,19 +81,13 @@
55
  themeToggle.textContent = isLight ? '☀️' : '🌙';
56
  });
57
 
58
- openBtn.addEventListener('click', () => {
59
- modal.classList.remove('hidden');
60
- modal.setAttribute('aria-hidden', 'false');
61
- });
62
- closeBtn.addEventListener('click', () => {
63
- modal.classList.add('hidden');
64
- modal.setAttribute('aria-hidden', 'true');
65
- });
66
  logoutBtn.addEventListener('click', () => {
67
  clearUser();
68
- alert('You have been logged out.');
69
  });
70
 
 
71
  tabs.forEach(tab => tab.addEventListener('click', () => {
72
  tabs.forEach(t => t.classList.remove('active'));
73
  tab.classList.add('active');
@@ -86,40 +106,68 @@
86
  e.preventDefault();
87
  const email = document.getElementById('login-email').value.trim();
88
  const password = document.getElementById('login-password').value;
 
 
 
 
 
 
89
  const fd = new FormData();
90
  fd.append('email', email);
91
  fd.append('password', password);
92
- const res = await fetch('/auth/login', { method: 'POST', body: fd });
93
- if (!res.ok) {
94
- const err = await res.json().catch(() => ({}));
95
- alert(err.detail || 'Login failed');
96
- return;
 
 
 
 
 
 
 
 
97
  }
98
- const data = await res.json();
99
- setUser(data);
100
- modal.classList.add('hidden');
101
  });
102
 
103
  document.getElementById('signup-form').addEventListener('submit', async (e) => {
104
  e.preventDefault();
105
  const email = document.getElementById('signup-email').value.trim();
106
  const password = document.getElementById('signup-password').value;
 
 
 
 
 
 
 
 
 
 
 
107
  const fd = new FormData();
108
  fd.append('email', email);
109
  fd.append('password', password);
110
- const res = await fetch('/auth/signup', { method: 'POST', body: fd });
111
- if (!res.ok) {
112
- const err = await res.json().catch(() => ({}));
113
- alert(err.detail || 'Signup failed');
114
- return;
 
 
 
 
 
 
 
 
115
  }
116
- const data = await res.json();
117
- setUser(data);
118
- modal.classList.add('hidden');
119
  });
120
 
121
  // Expose helper to other scripts
122
  window.__sb_get_user = getUser;
 
123
  })();
124
 
125
 
 
1
  // ────────────────────────────── static/auth.js ──────────────────────────────
2
  (function() {
3
  const modal = document.getElementById('auth-modal');
 
 
4
  const logoutBtn = document.getElementById('logout');
5
+ const userInfo = document.getElementById('user-info');
6
+ const userEmail = document.getElementById('user-email');
7
+ const themeToggle = document.getElementById('theme-toggle');
8
  const tabs = document.querySelectorAll('.tab');
9
  const tabLogin = document.getElementById('tab-login');
10
  const tabSignup = document.getElementById('tab-signup');
11
+ const uploadSection = document.getElementById('upload-section');
12
+ const chatSection = document.getElementById('chat-section');
13
 
14
  function setAuthUI(user) {
15
  if (user && user.user_id) {
16
+ userInfo.style.display = 'flex';
17
+ userEmail.textContent = user.email;
18
+ // Enable app sections
19
+ uploadSection.style.display = 'block';
20
+ chatSection.style.display = 'block';
21
+ // Hide modal if it was open
22
+ modal.classList.add('hidden');
23
  } else {
24
+ userInfo.style.display = 'none';
25
+ // Disable app sections for unauthenticated users
26
+ uploadSection.style.display = 'none';
27
+ chatSection.style.display = 'none';
28
+ // Show auth modal
29
+ showAuthModal();
30
  }
31
  }
32
 
 
46
  setAuthUI(null);
47
  }
48
 
49
+ function showAuthModal() {
50
+ modal.classList.remove('hidden');
51
+ modal.setAttribute('aria-hidden', 'false');
52
+ }
53
+
54
+ function hideAuthModal() {
55
+ modal.classList.add('hidden');
56
+ modal.setAttribute('aria-hidden', 'true');
57
+ }
58
 
59
+ // Initialize UI and check auth status
60
+ (function init() {
61
+ const user = getUser();
62
+ if (!user) {
63
+ // No user found, show modal immediately
64
+ showAuthModal();
65
+ } else {
66
+ setAuthUI(user);
67
+ }
68
+ })();
69
+
70
+ // Theme management
71
  (function initTheme() {
72
  const saved = localStorage.getItem('sb_theme');
73
  if (saved === 'light') document.documentElement.classList.add('light');
 
81
  themeToggle.textContent = isLight ? '☀️' : '🌙';
82
  });
83
 
84
+ // Logout
 
 
 
 
 
 
 
85
  logoutBtn.addEventListener('click', () => {
86
  clearUser();
87
+ showAuthModal();
88
  });
89
 
90
+ // Tab switching
91
  tabs.forEach(tab => tab.addEventListener('click', () => {
92
  tabs.forEach(t => t.classList.remove('active'));
93
  tab.classList.add('active');
 
106
  e.preventDefault();
107
  const email = document.getElementById('login-email').value.trim();
108
  const password = document.getElementById('login-password').value;
109
+
110
+ if (!email || !password) {
111
+ alert('Please fill in all fields');
112
+ return;
113
+ }
114
+
115
  const fd = new FormData();
116
  fd.append('email', email);
117
  fd.append('password', password);
118
+
119
+ try {
120
+ const res = await fetch('/auth/login', { method: 'POST', body: fd });
121
+ if (!res.ok) {
122
+ const err = await res.json().catch(() => ({}));
123
+ alert(err.detail || 'Login failed');
124
+ return;
125
+ }
126
+ const data = await res.json();
127
+ setUser(data);
128
+ hideAuthModal();
129
+ } catch (error) {
130
+ alert('Network error. Please try again.');
131
  }
 
 
 
132
  });
133
 
134
  document.getElementById('signup-form').addEventListener('submit', async (e) => {
135
  e.preventDefault();
136
  const email = document.getElementById('signup-email').value.trim();
137
  const password = document.getElementById('signup-password').value;
138
+
139
+ if (!email || !password) {
140
+ alert('Please fill in all fields');
141
+ return;
142
+ }
143
+
144
+ if (password.length < 8) {
145
+ alert('Password must be at least 8 characters long');
146
+ return;
147
+ }
148
+
149
  const fd = new FormData();
150
  fd.append('email', email);
151
  fd.append('password', password);
152
+
153
+ try {
154
+ const res = await fetch('/auth/signup', { method: 'POST', body: fd });
155
+ if (!res.ok) {
156
+ const err = await res.json().catch(() => ({}));
157
+ alert(err.detail || 'Signup failed');
158
+ return;
159
+ }
160
+ const data = await res.json();
161
+ setUser(data);
162
+ hideAuthModal();
163
+ } catch (error) {
164
+ alert('Network error. Please try again.');
165
  }
 
 
 
166
  });
167
 
168
  // Expose helper to other scripts
169
  window.__sb_get_user = getUser;
170
+ window.__sb_show_auth_modal = showAuthModal;
171
  })();
172
 
173
 
static/index.html CHANGED
@@ -10,34 +10,82 @@
10
  <body>
11
  <div class="container">
12
  <header>
13
- <h1>📚 StudyBuddy</h1>
14
- <p>Upload your PDFs/DOCX, then chat with your materials. No hallucinations — answers only come from your files.</p>
 
 
 
 
 
15
  <div class="auth-controls">
16
- <span id="auth-status">Not signed in</span>
17
- <button id="open-auth">Sign in / Create account</button>
18
- <button id="logout" class="secondary" style="display:none;">Logout</button>
19
- <button id="theme-toggle" class="secondary" title="Toggle theme">🌙</button>
 
 
20
  </div>
21
  </header>
22
 
23
- <section class="card reveal">
24
- <h2>1/ Upload materials</h2>
 
 
 
25
  <form id="upload-form">
26
- <div class="hint">You must be signed in. Your user ID is managed automatically.</div>
27
- <label>Files (PDF/DOCX, multiple)</label>
28
- <input type="file" id="files" multiple accept=".pdf,.docx">
29
- <button type="submit">Upload</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  </form>
31
- <pre id="upload-log"></pre>
 
 
 
 
 
 
 
 
 
32
  </section>
33
 
34
- <section class="card reveal">
35
- <h2>2/ Ask questions</h2>
 
 
 
36
  <div id="chat">
37
  <div id="messages"></div>
38
- <div class="chat-controls">
39
- <input type="text" id="question" placeholder="Ask something about your files…">
40
- <button id="ask">Ask</button>
 
 
 
 
 
 
 
 
 
 
 
41
  </div>
42
  </div>
43
  </section>
@@ -50,31 +98,53 @@
50
  <!-- Auth Modal -->
51
  <div id="auth-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="auth-title">
52
  <div class="modal-content">
53
- <button class="modal-close" id="close-auth" aria-label="Close">×</button>
54
- <h2 id="auth-title">Welcome to StudyBuddy</h2>
55
- <p class="modal-subtitle">Create an account or sign in to continue</p>
 
 
56
  <div class="tabs">
57
- <button class="tab active" data-tab="login">Login</button>
58
- <button class="tab" data-tab="signup">Create account</button>
59
  </div>
60
  <div class="tab-body" id="tab-login">
61
  <form id="login-form">
62
- <label>Email</label>
63
- <input type="email" id="login-email" placeholder="you@example.com" required>
64
- <label>Password</label>
65
- <input type="password" id="login-password" placeholder="••••••••" required>
66
- <button type="submit">Sign in</button>
 
 
 
 
67
  </form>
68
  </div>
69
  <div class="tab-body hidden" id="tab-signup">
70
  <form id="signup-form">
71
- <label>Email</label>
72
- <input type="email" id="signup-email" placeholder="you@example.com" required>
73
- <label>Password</label>
74
- <input type="password" id="signup-password" placeholder="At least 8 characters" minlength="8" required>
75
- <button type="submit">Create account</button>
 
 
 
 
76
  </form>
77
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
78
  </div>
79
  </div>
80
 
 
10
  <body>
11
  <div class="container">
12
  <header>
13
+ <div class="logo">
14
+ <div class="logo-icon">📚</div>
15
+ <div class="logo-text">
16
+ <h1>StudyBuddy</h1>
17
+ <p>AI-powered document analysis & chat</p>
18
+ </div>
19
+ </div>
20
  <div class="auth-controls">
21
+ <div class="user-info" id="user-info" style="display:none;">
22
+ <span class="user-avatar">👤</span>
23
+ <span class="user-email" id="user-email"></span>
24
+ <button id="logout" class="btn-secondary">Logout</button>
25
+ </div>
26
+ <button id="theme-toggle" class="btn-icon" title="Toggle theme">🌙</button>
27
  </div>
28
  </header>
29
 
30
+ <section class="card reveal" id="upload-section">
31
+ <div class="card-header">
32
+ <h2>📁 Upload Documents</h2>
33
+ <p>Upload your PDFs and DOCX files to start chatting with your materials</p>
34
+ </div>
35
  <form id="upload-form">
36
+ <div class="file-drop-zone" id="file-drop-zone">
37
+ <div class="drop-zone-content">
38
+ <div class="drop-zone-icon">📄</div>
39
+ <p>Drag & drop files here or click to browse</p>
40
+ <span class="file-types">Supports: PDF, DOCX</span>
41
+ </div>
42
+ <input type="file" id="files" multiple accept=".pdf,.docx" hidden>
43
+ </div>
44
+ <div class="file-list" id="file-list" style="display:none;">
45
+ <h4>Selected Files:</h4>
46
+ <div class="file-items" id="file-items"></div>
47
+ </div>
48
+ <button type="submit" class="btn-primary" id="upload-btn">
49
+ <span class="btn-text">Upload Documents</span>
50
+ <span class="btn-loading" style="display:none;">
51
+ <div class="spinner"></div>
52
+ Processing...
53
+ </span>
54
+ </button>
55
  </form>
56
+ <div class="upload-progress" id="upload-progress" style="display:none;">
57
+ <div class="progress-header">
58
+ <h4>Processing Documents</h4>
59
+ <span class="progress-status" id="progress-status">Initializing...</span>
60
+ </div>
61
+ <div class="progress-bar">
62
+ <div class="progress-fill" id="progress-fill"></div>
63
+ </div>
64
+ <div class="progress-log" id="progress-log"></div>
65
+ </div>
66
  </section>
67
 
68
+ <section class="card reveal" id="chat-section">
69
+ <div class="card-header">
70
+ <h2>💬 Chat with Documents</h2>
71
+ <p>Ask questions about your uploaded materials and get AI-powered answers</p>
72
+ </div>
73
  <div id="chat">
74
  <div id="messages"></div>
75
+ <div class="chat-controls" id="chat-controls">
76
+ <div class="chat-input-wrapper">
77
+ <input type="text" id="question" placeholder="Ask something about your documents..." disabled>
78
+ <button id="ask" class="btn-primary" disabled>
79
+ <span class="btn-text">Ask</span>
80
+ <span class="btn-loading" style="display:none;">
81
+ <div class="spinner"></div>
82
+ Thinking...
83
+ </span>
84
+ </button>
85
+ </div>
86
+ <div class="chat-hint" id="chat-hint">
87
+ Upload documents first to start chatting
88
+ </div>
89
  </div>
90
  </div>
91
  </section>
 
98
  <!-- Auth Modal -->
99
  <div id="auth-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="auth-title">
100
  <div class="modal-content">
101
+ <div class="modal-header">
102
+ <div class="modal-logo">📚</div>
103
+ <h2 id="auth-title">Welcome to StudyBuddy</h2>
104
+ <p class="modal-subtitle">Your AI-powered document analysis companion</p>
105
+ </div>
106
  <div class="tabs">
107
+ <button class="tab active" data-tab="login">Sign In</button>
108
+ <button class="tab" data-tab="signup">Create Account</button>
109
  </div>
110
  <div class="tab-body" id="tab-login">
111
  <form id="login-form">
112
+ <div class="form-group">
113
+ <label>Email Address</label>
114
+ <input type="email" id="login-email" placeholder="you@example.com" required>
115
+ </div>
116
+ <div class="form-group">
117
+ <label>Password</label>
118
+ <input type="password" id="login-password" placeholder="••••••••" required>
119
+ </div>
120
+ <button type="submit" class="btn-primary">Sign in</button>
121
  </form>
122
  </div>
123
  <div class="tab-body hidden" id="tab-signup">
124
  <form id="signup-form">
125
+ <div class="form-group">
126
+ <label>Email Address</label>
127
+ <input type="email" id="signup-email" placeholder="you@example.com" required>
128
+ </div>
129
+ <div class="form-group">
130
+ <label>Password</label>
131
+ <input type="password" id="signup-password" placeholder="At least 8 characters" minlength="8" required>
132
+ </div>
133
+ <button type="submit" class="btn-primary">Create account</button>
134
  </form>
135
  </div>
136
+ <div class="modal-footer">
137
+ <p>By continuing, you agree to our Terms of Service and Privacy Policy</p>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Loading Overlay -->
143
+ <div id="loading-overlay" class="loading-overlay hidden">
144
+ <div class="loading-content">
145
+ <div class="loading-spinner"></div>
146
+ <h3>Processing...</h3>
147
+ <p id="loading-message">Please wait while we process your request</p>
148
  </div>
149
  </div>
150
 
static/script.js CHANGED
@@ -1,85 +1,394 @@
1
  // ────────────────────────────── static/script.js ──────────────────────────────
2
- const log = (msg) => {
3
- const el = document.getElementById("upload-log");
4
- el.textContent += msg + "\n";
5
- el.scrollTop = el.scrollHeight;
6
- };
7
-
8
- // Upload
9
- document.getElementById("upload-form").addEventListener("submit", async (e) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  e.preventDefault();
11
- const user = window.__sb_get_user && window.__sb_get_user();
12
- const user_id = user && user.user_id;
13
- const files = document.getElementById("files").files;
14
- if (!user_id || files.length === 0) {
15
- alert("Please sign in and select at least one file.");
 
 
 
 
 
16
  return;
17
  }
18
- const fd = new FormData();
19
- fd.append("user_id", user_id);
20
- for (let f of files) fd.append("files", f);
21
-
22
- log("Uploading " + files.length + " file(s)…");
23
- const res = await fetch("/upload", { method: "POST", body: fd });
24
- const data = await res.json();
25
- log("Upload accepted. Job: " + (data.job_id || "?") + " • status: " + (data.status || "?"));
26
- log("Processing in the background. You can start chatting meanwhile.");
27
- });
28
-
29
- // Chat
30
- document.getElementById("ask").addEventListener("click", async () => {
31
- const user = window.__sb_get_user && window.__sb_get_user();
32
- const user_id = user && user.user_id;
33
- const q = document.getElementById("question").value.trim();
34
- if (!user_id || !q) return;
35
- appendMessage("user", q);
36
- document.getElementById("question").value = "";
37
-
38
- const fd = new FormData();
39
- fd.append("user_id", user_id);
40
- fd.append("question", q);
41
- fd.append("k", "6");
42
-
43
  try {
44
- const res = await fetch("/chat", { method: "POST", body: fd });
45
- const data = await res.json();
46
- appendMessage("assistant", data.answer || "[no answer]");
47
- if (data.sources && data.sources.length) {
48
- appendSources(data.sources);
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
- } catch (e) {
51
- appendMessage("assistant", "⚠️ Error contacting server.");
 
 
 
 
 
52
  }
53
- });
54
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  function appendMessage(role, text) {
56
- const m = document.createElement("div");
57
- m.className = "msg " + role;
58
- m.textContent = text;
59
- document.getElementById("messages").appendChild(m);
60
- requestAnimationFrame(() => m.scrollIntoView({ behavior: "smooth", block: "end" }));
 
 
 
 
 
 
 
61
  }
62
-
63
  function appendSources(sources) {
64
- const wrap = document.createElement("div");
65
- wrap.className = "sources";
66
- wrap.innerHTML = "<strong>Sources:</strong> " + sources.map(s => {
67
- const f = s.filename || "unknown";
68
- const t = s.topic_name ? (" • " + s.topic_name) : "";
69
- const p = s.page_span ? (" [pp. " + s.page_span.join("-") + "]") : "";
70
- return `<span class="pill">${f}${t}${p}</span>`;
71
- }).join(" ");
72
- document.getElementById("messages").appendChild(wrap);
73
- requestAnimationFrame(() => wrap.scrollIntoView({ behavior: "smooth", block: "end" }));
74
- }
75
-
76
- // Reveal on scroll
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  const observer = new IntersectionObserver((entries) => {
78
- for (const e of entries) {
79
- if (e.isIntersecting) {
80
- e.target.classList.add('in');
81
- observer.unobserve(e.target);
82
  }
83
- }
84
  }, { threshold: 0.1 });
85
- document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
 
 
 
1
  // ────────────────────────────── static/script.js ──────────────────────────────
2
+ (function() {
3
+ // DOM elements
4
+ const fileDropZone = document.getElementById('file-drop-zone');
5
+ const fileInput = document.getElementById('files');
6
+ const fileList = document.getElementById('file-list');
7
+ const fileItems = document.getElementById('file-items');
8
+ const uploadBtn = document.getElementById('upload-btn');
9
+ const uploadProgress = document.getElementById('upload-progress');
10
+ const progressStatus = document.getElementById('progress-status');
11
+ const progressFill = document.getElementById('progress-fill');
12
+ const progressLog = document.getElementById('progress-log');
13
+ const questionInput = document.getElementById('question');
14
+ const askBtn = document.getElementById('ask');
15
+ const chatHint = document.getElementById('chat-hint');
16
+ const messages = document.getElementById('messages');
17
+ const loadingOverlay = document.getElementById('loading-overlay');
18
+ const loadingMessage = document.getElementById('loading-message');
19
+
20
+ // State
21
+ let selectedFiles = [];
22
+ let isUploading = false;
23
+ let isProcessing = false;
24
+
25
+ // Initialize
26
+ init();
27
+
28
+ function init() {
29
+ setupFileDropZone();
30
+ setupEventListeners();
31
+ checkUserAuth();
32
+ }
33
+
34
+ function setupFileDropZone() {
35
+ // Click to browse
36
+ fileDropZone.addEventListener('click', () => fileInput.click());
37
+
38
+ // Drag and drop
39
+ fileDropZone.addEventListener('dragover', (e) => {
40
+ e.preventDefault();
41
+ fileDropZone.classList.add('dragover');
42
+ });
43
+
44
+ fileDropZone.addEventListener('dragleave', () => {
45
+ fileDropZone.classList.remove('dragover');
46
+ });
47
+
48
+ fileDropZone.addEventListener('drop', (e) => {
49
+ e.preventDefault();
50
+ fileDropZone.classList.remove('dragover');
51
+ const files = Array.from(e.dataTransfer.files);
52
+ handleFileSelection(files);
53
+ });
54
+
55
+ // File input change
56
+ fileInput.addEventListener('change', (e) => {
57
+ const files = Array.from(e.target.files);
58
+ handleFileSelection(files);
59
+ });
60
+ }
61
+
62
+ function setupEventListeners() {
63
+ // Upload form
64
+ document.getElementById('upload-form').addEventListener('submit', handleUpload);
65
+
66
+ // Chat
67
+ askBtn.addEventListener('click', handleAsk);
68
+ questionInput.addEventListener('keypress', (e) => {
69
+ if (e.key === 'Enter' && !e.shiftKey) {
70
+ e.preventDefault();
71
+ handleAsk();
72
+ }
73
+ });
74
+ }
75
+
76
+ function handleFileSelection(files) {
77
+ const validFiles = files.filter(file => {
78
+ const isValid = file.type === 'application/pdf' ||
79
+ file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
80
+ if (!isValid) {
81
+ alert(`Unsupported file type: ${file.name}. Please upload PDF or DOCX files only.`);
82
+ }
83
+ return isValid;
84
+ });
85
+
86
+ if (validFiles.length === 0) return;
87
+
88
+ selectedFiles = validFiles;
89
+ updateFileList();
90
+ updateUploadButton();
91
+ }
92
+
93
+ function updateFileList() {
94
+ if (selectedFiles.length === 0) {
95
+ fileList.style.display = 'none';
96
+ return;
97
+ }
98
+
99
+ fileList.style.display = 'block';
100
+ fileItems.innerHTML = '';
101
+
102
+ selectedFiles.forEach((file, index) => {
103
+ const fileItem = document.createElement('div');
104
+ fileItem.className = 'file-item';
105
+
106
+ const icon = document.createElement('span');
107
+ icon.className = 'file-item-icon';
108
+ icon.textContent = file.type.includes('pdf') ? '📄' : '📝';
109
+
110
+ const name = document.createElement('span');
111
+ name.className = 'file-item-name';
112
+ name.textContent = file.name;
113
+
114
+ const size = document.createElement('span');
115
+ size.className = 'file-item-size';
116
+ size.textContent = formatFileSize(file.size);
117
+
118
+ const remove = document.createElement('button');
119
+ remove.className = 'file-item-remove';
120
+ remove.textContent = '×';
121
+ remove.title = 'Remove file';
122
+ remove.addEventListener('click', () => removeFile(index));
123
+
124
+ fileItem.appendChild(icon);
125
+ fileItem.appendChild(name);
126
+ fileItem.appendChild(size);
127
+ fileItem.appendChild(remove);
128
+ fileItems.appendChild(fileItem);
129
+ });
130
+ }
131
+
132
+ function removeFile(index) {
133
+ selectedFiles.splice(index, 1);
134
+ updateFileList();
135
+ updateUploadButton();
136
+ }
137
+
138
+ function updateUploadButton() {
139
+ const hasFiles = selectedFiles.length > 0;
140
+ uploadBtn.disabled = !hasFiles || isUploading;
141
+
142
+ if (hasFiles) {
143
+ uploadBtn.querySelector('.btn-text').textContent = `Upload ${selectedFiles.length} Document${selectedFiles.length > 1 ? 's' : ''}`;
144
+ } else {
145
+ uploadBtn.querySelector('.btn-text').textContent = 'Upload Documents';
146
+ }
147
+ }
148
+
149
+ function formatFileSize(bytes) {
150
+ if (bytes === 0) return '0 Bytes';
151
+ const k = 1024;
152
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
153
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
154
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
155
+ }
156
+
157
+ async function handleUpload(e) {
158
  e.preventDefault();
159
+
160
+ if (selectedFiles.length === 0) {
161
+ alert('Please select files to upload');
162
+ return;
163
+ }
164
+
165
+ const user = window.__sb_get_user();
166
+ if (!user) {
167
+ alert('Please sign in to upload files');
168
+ window.__sb_show_auth_modal();
169
  return;
170
  }
171
+
172
+ isUploading = true;
173
+ updateUploadButton();
174
+ showUploadProgress();
175
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  try {
177
+ const formData = new FormData();
178
+ formData.append('user_id', user.user_id);
179
+ selectedFiles.forEach(file => formData.append('files', file));
180
+
181
+ const response = await fetch('/upload', { method: 'POST', body: formData });
182
+ const data = await response.json();
183
+
184
+ if (response.ok) {
185
+ updateProgressStatus('Upload successful! Processing documents...');
186
+ updateProgressFill(50);
187
+ logProgress(`Job ID: ${data.job_id}`);
188
+ logProgress('Files uploaded successfully');
189
+
190
+ // Simulate processing progress
191
+ simulateProcessing();
192
+ } else {
193
+ throw new Error(data.detail || 'Upload failed');
194
  }
195
+ } catch (error) {
196
+ logProgress(`Error: ${error.message}`);
197
+ updateProgressStatus('Upload failed');
198
+ setTimeout(() => hideUploadProgress(), 3000);
199
+ } finally {
200
+ isUploading = false;
201
+ updateUploadButton();
202
  }
203
+ }
204
+
205
+ function showUploadProgress() {
206
+ uploadProgress.style.display = 'block';
207
+ updateProgressStatus('Uploading files...');
208
+ updateProgressFill(0);
209
+ progressLog.innerHTML = '';
210
+ }
211
+
212
+ function hideUploadProgress() {
213
+ uploadProgress.style.display = 'none';
214
+ selectedFiles = [];
215
+ updateFileList();
216
+ updateUploadButton();
217
+ }
218
+
219
+ function updateProgressStatus(status) {
220
+ progressStatus.textContent = status;
221
+ }
222
+
223
+ function updateProgressFill(percentage) {
224
+ progressFill.style.width = `${percentage}%`;
225
+ }
226
+
227
+ function logProgress(message) {
228
+ const timestamp = new Date().toLocaleTimeString();
229
+ progressLog.innerHTML += `[${timestamp}] ${message}\n`;
230
+ progressLog.scrollTop = progressLog.scrollHeight;
231
+ }
232
+
233
+ function simulateProcessing() {
234
+ let progress = 50;
235
+ const interval = setInterval(() => {
236
+ progress += Math.random() * 10;
237
+ if (progress >= 100) {
238
+ progress = 100;
239
+ clearInterval(interval);
240
+ updateProgressStatus('Processing complete!');
241
+ logProgress('All documents processed successfully');
242
+ logProgress('You can now start chatting with your documents');
243
+ setTimeout(() => hideUploadProgress(), 2000);
244
+ enableChat();
245
+ } else {
246
+ updateProgressFill(progress);
247
+ updateProgressStatus(`Processing documents... ${Math.round(progress)}%`);
248
+ }
249
+ }, 500);
250
+ }
251
+
252
+ function enableChat() {
253
+ questionInput.disabled = false;
254
+ askBtn.disabled = false;
255
+ chatHint.style.display = 'none';
256
+ }
257
+
258
+ async function handleAsk() {
259
+ const question = questionInput.value.trim();
260
+ if (!question) return;
261
+
262
+ const user = window.__sb_get_user();
263
+ if (!user) {
264
+ alert('Please sign in to chat');
265
+ window.__sb_show_auth_modal();
266
+ return;
267
+ }
268
+
269
+ // Add user message
270
+ appendMessage('user', question);
271
+ questionInput.value = '';
272
+
273
+ // Add thinking message
274
+ const thinkingMsg = appendMessage('thinking', 'Thinking...');
275
+
276
+ // Disable input during processing
277
+ questionInput.disabled = true;
278
+ askBtn.disabled = true;
279
+ showButtonLoading(askBtn, true);
280
+
281
+ try {
282
+ const formData = new FormData();
283
+ formData.append('user_id', user.user_id);
284
+ formData.append('question', question);
285
+ formData.append('k', '6');
286
+
287
+ const response = await fetch('/chat', { method: 'POST', body: formData });
288
+ const data = await response.json();
289
+
290
+ if (response.ok) {
291
+ // Remove thinking message
292
+ thinkingMsg.remove();
293
+
294
+ // Add assistant response
295
+ appendMessage('assistant', data.answer || 'No answer received');
296
+
297
+ // Add sources if available
298
+ if (data.sources && data.sources.length > 0) {
299
+ appendSources(data.sources);
300
+ }
301
+ } else {
302
+ throw new Error(data.detail || 'Failed to get answer');
303
+ }
304
+ } catch (error) {
305
+ thinkingMsg.remove();
306
+ appendMessage('assistant', `⚠️ Error: ${error.message}`);
307
+ } finally {
308
+ // Re-enable input
309
+ questionInput.disabled = false;
310
+ askBtn.disabled = false;
311
+ showButtonLoading(askBtn, false);
312
+ questionInput.focus();
313
+ }
314
+ }
315
+
316
  function appendMessage(role, text) {
317
+ const messageDiv = document.createElement('div');
318
+ messageDiv.className = `msg ${role}`;
319
+ messageDiv.textContent = text;
320
+
321
+ messages.appendChild(messageDiv);
322
+
323
+ // Scroll to bottom
324
+ requestAnimationFrame(() => {
325
+ messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
326
+ });
327
+
328
+ return messageDiv;
329
  }
330
+
331
  function appendSources(sources) {
332
+ const sourcesDiv = document.createElement('div');
333
+ sourcesDiv.className = 'sources';
334
+
335
+ const sourcesList = sources.map(source => {
336
+ const filename = source.filename || 'unknown';
337
+ const topic = source.topic_name ? ` ${source.topic_name}` : '';
338
+ const pages = source.page_span ? ` [pp. ${source.page_span.join('-')}]` : '';
339
+ const score = source.score ? ` (${source.score.toFixed(2)})` : '';
340
+
341
+ return `<span class="pill">${filename}${topic}${pages}${score}</span>`;
342
+ }).join(' ');
343
+
344
+ sourcesDiv.innerHTML = `<strong>Sources:</strong> ${sourcesList}`;
345
+ messages.appendChild(sourcesDiv);
346
+
347
+ requestAnimationFrame(() => {
348
+ sourcesDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
349
+ });
350
+ }
351
+
352
+ function showButtonLoading(button, isLoading) {
353
+ const textSpan = button.querySelector('.btn-text');
354
+ const loadingSpan = button.querySelector('.btn-loading');
355
+
356
+ if (isLoading) {
357
+ textSpan.style.display = 'none';
358
+ loadingSpan.style.display = 'inline-flex';
359
+ button.disabled = true;
360
+ } else {
361
+ textSpan.style.display = 'inline';
362
+ loadingSpan.style.display = 'none';
363
+ button.disabled = false;
364
+ }
365
+ }
366
+
367
+ function showLoading(message = 'Processing...') {
368
+ loadingMessage.textContent = message;
369
+ loadingOverlay.classList.remove('hidden');
370
+ }
371
+
372
+ function hideLoading() {
373
+ loadingOverlay.classList.add('hidden');
374
+ }
375
+
376
+ function checkUserAuth() {
377
+ const user = window.__sb_get_user();
378
+ if (user) {
379
+ enableChat();
380
+ }
381
+ }
382
+
383
+ // Reveal animations
384
  const observer = new IntersectionObserver((entries) => {
385
+ entries.forEach(entry => {
386
+ if (entry.isIntersecting) {
387
+ entry.target.classList.add('in');
388
+ observer.unobserve(entry.target);
389
  }
390
+ });
391
  }, { threshold: 0.1 });
392
+
393
+ document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
394
+ })();
static/styles.css CHANGED
@@ -1,112 +1,810 @@
1
  /* ────────────────────────────── static/styles.css ────────────────────────────── */
2
  :root {
3
- --bg: #0b1020;
4
- --card: #12193a;
5
- --text: #e6ecff;
6
- --muted: #9bb0ff;
7
- --accent: #7aa2ff;
8
- --pill: #1f2a5c;
9
- --green: #41d6a5;
10
- --shadow: 0 12px 40px rgba(0,0,0,0.35);
11
- --border: #1f2750;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
- :root.light {
14
- --bg: #f6f8ff;
15
- --card: #ffffff;
16
- --text: #0c1636;
17
- --muted: #5c6c9a;
18
- --accent: #4c79ff;
19
- --pill: #e9edff;
20
- --green: #16a085;
21
- --shadow: 0 12px 30px rgba(0,0,0,0.12);
22
- --border: #e6ecff;
23
  }
24
-
25
- * { box-sizing: border-box; }
26
-
27
- body {
28
- margin: 0;
29
- font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
30
- color: var(--text);
31
- background: radial-gradient(1200px 600px at 20% -10%, #18225a, var(--bg));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  .container {
35
- max-width: 960px;
36
- margin: 0 auto;
37
- padding: 24px;
38
  }
39
 
40
- header h1 { margin: 0 0 8px; }
41
- header p { color: var(--muted); margin: 0 0 16px; }
42
- .auth-controls { display:flex; gap:8px; align-items:center; }
43
- .auth-controls #auth-status { color: var(--muted); margin-right: 8px; }
44
- .auth-controls .secondary { background: #1b2a58; color: #cfe0ff; }
45
 
46
  .card {
47
- background: var(--card);
48
- border: 1px solid var(--border);
49
- border-radius: 16px;
50
- padding: 16px;
51
- margin: 16px 0;
52
- box-shadow: var(--shadow);
53
- transition: transform .25s ease, box-shadow .25s ease, background .25s ease, border-color .25s ease;
54
  }
55
- .card:hover { transform: translateY(-2px); box-shadow: 0 16px 54px rgba(0,0,0,0.45); }
56
 
57
- label { display: block; margin: 8px 0 6px; color: var(--muted); }
58
- input[type="text"], input[type="file"] {
59
- width: 100%; padding: 10px 12px; border-radius: 12px; border: 1px solid #2a3570;
60
- background: #0f1430; color: var(--text);
61
  }
62
- button {
63
- margin-top: 12px;
64
- background: linear-gradient(135deg, var(--accent), #5bc7ff);
65
- color: #0a0f25; border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600;
66
- cursor: pointer;
67
- }
68
- button:hover { filter: brightness(1.07); }
69
- button.secondary { background:#1f2a5c; color:#d5e0ff; }
70
 
71
- #upload-log {
72
- height: 120px; overflow: auto; background: #0f1430; padding: 10px; border-radius: 12px; border: 1px solid #2a3570;
73
- color: #b9c7ff;
 
 
 
 
 
 
 
74
  }
75
 
76
- #chat { display: flex; flex-direction: column; gap: 12px; }
77
- #messages {
78
- height: 300px; overflow: auto; background: #0f1430; padding: 12px; border-radius: 12px; border: 1px solid #2a3570;
79
- scroll-behavior: smooth;
80
  }
81
- .msg { padding: 10px 12px; border-radius: 12px; margin: 6px 0; max-width: 80%; white-space: pre-wrap; }
82
- .msg.user { margin-left: auto; background: #173361; }
83
- .msg.assistant { background: #0f244d; border: 1px solid #243a7a; }
84
- .sources { margin: 8px 0; }
85
- .pill { display: inline-block; background: var(--pill); padding: 4px 8px; border-radius: 999px; margin: 2px; color: #cbd6ff; border: 1px solid #304088; }
86
- footer { text-align: center; color: var(--muted); margin-top: 24px; }
87
-
88
- /* Modal */
89
- .modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(4,8,20,0.7); z-index: 1000; }
90
- .modal.hidden { display: none; }
91
- .modal-content { width: 100%; max-width: 440px; background: var(--card); border: 1px solid #2a3470; border-radius: 16px; padding: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.4); }
92
- .modal-close { float: right; background: #1a2557; color: #cfe0ff; border: 1px solid #2f3a7a; border-radius: 10px; padding: 6px 10px; cursor: pointer; }
93
- .modal-subtitle { color: var(--muted); margin-top: 0; }
94
- .tabs { display: flex; gap: 8px; margin: 12px 0; }
95
- .tab { background: #15214f; border: 1px solid #2a3470; color: #cfe0ff; border-radius: 999px; padding: 8px 12px; cursor: pointer; }
96
- .tab.active { background: linear-gradient(135deg, var(--accent), #5bc7ff); color: #0a0f25; border-color: transparent; }
97
- .tab-body { margin-top: 8px; }
98
- .hidden { display: none; }
99
- .hint { color: var(--muted); font-size: 12px; margin-bottom: 6px; }
100
-
101
- /* Reveal animations */
102
- .reveal { opacity: 0; transform: translateY(12px); transition: opacity .6s ease, transform .6s ease; }
103
- .reveal.in { opacity: 1; transform: none; }
104
-
105
- /* Sticky chat controls */
106
- .chat-controls { position: sticky; bottom: 0; background: linear-gradient(180deg, rgba(0,0,0,0), var(--card) 40%); padding-bottom: 6px; }
107
-
108
- /* Reduced motion */
109
- @media (prefers-reduced-motion: reduce) {
110
- * { transition: none !important; animation: none !important; }
111
- .reveal { opacity: 1; transform: none; }
112
- }
 
1
  /* ────────────────────────────── static/styles.css ────────────────────────────── */
2
  :root {
3
+ --bg: #0a0e1a;
4
+ --bg-secondary: #111827;
5
+ --card: #1f2937;
6
+ --card-hover: #374151;
7
+ --text: #f9fafb;
8
+ --text-secondary: #d1d5db;
9
+ --muted: #9ca3af;
10
+ --accent: #3b82f6;
11
+ --accent-hover: #2563eb;
12
+ --success: #10b981;
13
+ --warning: #f59e0b;
14
+ --error: #ef4444;
15
+ --pill: #374151;
16
+ --border: #374151;
17
+ --border-light: #4b5563;
18
+ --shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
19
+ --shadow-lg: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
20
+ --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
21
+ --gradient-accent: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
22
+ --gradient-success: linear-gradient(135deg, #10b981 0%, #059669 100%);
23
+ --radius: 12px;
24
+ --radius-lg: 16px;
25
+ --radius-xl: 20px;
26
+ }
27
+
28
+ :root.light {
29
+ --bg: #ffffff;
30
+ --bg-secondary: #f9fafb;
31
+ --card: #ffffff;
32
+ --card-hover: #f3f4f6;
33
+ --text: #111827;
34
+ --text-secondary: #374151;
35
+ --muted: #6b7280;
36
+ --accent: #3b82f6;
37
+ --accent-hover: #2563eb;
38
+ --success: #10b981;
39
+ --warning: #f59e0b;
40
+ --error: #ef4444;
41
+ --pill: #e5e7eb;
42
+ --border: #e5e7eb;
43
+ --border-light: #d1d5db;
44
+ --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
45
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
46
+ }
47
+
48
+ * {
49
+ box-sizing: border-box;
50
+ margin: 0;
51
+ padding: 0;
52
+ }
53
+
54
+ body {
55
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
56
+ color: var(--text);
57
+ background: var(--bg);
58
+ line-height: 1.6;
59
+ transition: background-color 0.3s ease, color 0.3s ease;
60
+ }
61
+
62
+ .container {
63
+ max-width: 1200px;
64
+ margin: 0 auto;
65
+ padding: 24px;
66
+ min-height: 100vh;
67
+ }
68
+
69
+ /* Header */
70
+ header {
71
+ display: flex;
72
+ justify-content: space-between;
73
+ align-items: center;
74
+ margin-bottom: 48px;
75
+ padding: 24px 0;
76
+ border-bottom: 1px solid var(--border);
77
+ }
78
+
79
+ .logo {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 16px;
83
+ }
84
+
85
+ .logo-icon {
86
+ font-size: 48px;
87
+ background: var(--gradient-primary);
88
+ -webkit-background-clip: text;
89
+ -webkit-text-fill-color: transparent;
90
+ background-clip: text;
91
+ }
92
+
93
+ .logo-text h1 {
94
+ font-size: 32px;
95
+ font-weight: 800;
96
+ background: var(--gradient-primary);
97
+ -webkit-background-clip: text;
98
+ -webkit-text-fill-color: transparent;
99
+ background-clip: text;
100
+ margin: 0;
101
+ }
102
+
103
+ .logo-text p {
104
+ color: var(--muted);
105
+ font-size: 16px;
106
+ margin: 4px 0 0 0;
107
+ }
108
+
109
+ .auth-controls {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 16px;
113
+ }
114
+
115
+ .user-info {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 12px;
119
+ padding: 8px 16px;
120
+ background: var(--card);
121
+ border-radius: var(--radius);
122
+ border: 1px solid var(--border);
123
+ }
124
+
125
+ .user-avatar {
126
+ font-size: 20px;
127
+ }
128
+
129
+ .user-email {
130
+ color: var(--text-secondary);
131
+ font-size: 14px;
132
+ }
133
+
134
+ /* Buttons */
135
+ .btn-primary, .btn-secondary, .btn-icon {
136
+ padding: 12px 24px;
137
+ border: none;
138
+ border-radius: var(--radius);
139
+ font-weight: 600;
140
+ font-size: 14px;
141
+ cursor: pointer;
142
+ transition: all 0.2s ease;
143
+ display: inline-flex;
144
+ align-items: center;
145
+ gap: 8px;
146
+ text-decoration: none;
147
+ }
148
+
149
+ .btn-primary {
150
+ background: var(--gradient-accent);
151
+ color: white;
152
+ box-shadow: var(--shadow);
153
+ }
154
+
155
+ .btn-primary:hover:not(:disabled) {
156
+ transform: translateY(-1px);
157
+ box-shadow: var(--shadow-lg);
158
+ }
159
+
160
+ .btn-primary:disabled {
161
+ opacity: 0.6;
162
+ cursor: not-allowed;
163
+ transform: none;
164
+ }
165
+
166
+ .btn-secondary {
167
+ background: var(--card);
168
+ color: var(--text-secondary);
169
+ border: 1px solid var(--border);
170
+ }
171
+
172
+ .btn-secondary:hover {
173
+ background: var(--card-hover);
174
+ border-color: var(--border-light);
175
+ }
176
+
177
+ .btn-icon {
178
+ padding: 12px;
179
+ background: var(--card);
180
+ color: var(--text-secondary);
181
+ border: 1px solid var(--border);
182
+ }
183
+
184
+ .btn-icon:hover {
185
+ background: var(--card-hover);
186
+ color: var(--text);
187
+ }
188
+
189
+ /* Cards */
190
+ .card {
191
+ background: var(--card);
192
+ border: 1px solid var(--border);
193
+ border-radius: var(--radius-xl);
194
+ padding: 32px;
195
+ margin: 32px 0;
196
+ box-shadow: var(--shadow);
197
+ transition: all 0.3s ease;
198
+ position: relative;
199
+ overflow: hidden;
200
+ }
201
+
202
+ .card::before {
203
+ content: '';
204
+ position: absolute;
205
+ top: 0;
206
+ left: 0;
207
+ right: 0;
208
+ height: 4px;
209
+ background: var(--gradient-primary);
210
+ opacity: 0;
211
+ transition: opacity 0.3s ease;
212
+ }
213
+
214
+ .card:hover::before {
215
+ opacity: 1;
216
+ }
217
+
218
+ .card:hover {
219
+ transform: translateY(-4px);
220
+ box-shadow: var(--shadow-lg);
221
+ border-color: var(--border-light);
222
+ }
223
+
224
+ .card-header {
225
+ margin-bottom: 24px;
226
+ text-align: center;
227
+ }
228
+
229
+ .card-header h2 {
230
+ font-size: 28px;
231
+ font-weight: 700;
232
+ margin-bottom: 8px;
233
+ background: var(--gradient-primary);
234
+ -webkit-background-clip: text;
235
+ -webkit-text-fill-color: transparent;
236
+ background-clip: text;
237
+ }
238
+
239
+ .card-header p {
240
+ color: var(--muted);
241
+ font-size: 16px;
242
+ }
243
+
244
+ /* File Drop Zone */
245
+ .file-drop-zone {
246
+ border: 2px dashed var(--border);
247
+ border-radius: var(--radius-lg);
248
+ padding: 48px 24px;
249
+ text-align: center;
250
+ cursor: pointer;
251
+ transition: all 0.3s ease;
252
+ background: var(--bg-secondary);
253
+ margin-bottom: 24px;
254
+ }
255
+
256
+ .file-drop-zone:hover {
257
+ border-color: var(--accent);
258
+ background: var(--card);
259
+ }
260
+
261
+ .file-drop-zone.dragover {
262
+ border-color: var(--accent);
263
+ background: var(--card);
264
+ transform: scale(1.02);
265
+ }
266
+
267
+ .drop-zone-content {
268
+ display: flex;
269
+ flex-direction: column;
270
+ align-items: center;
271
+ gap: 16px;
272
+ }
273
+
274
+ .drop-zone-icon {
275
+ font-size: 48px;
276
+ opacity: 0.7;
277
+ }
278
+
279
+ .drop-zone-content p {
280
+ font-size: 18px;
281
+ font-weight: 500;
282
+ color: var(--text);
283
+ }
284
+
285
+ .file-types {
286
+ font-size: 14px;
287
+ color: var(--muted);
288
+ padding: 8px 16px;
289
+ background: var(--card);
290
+ border-radius: var(--radius);
291
+ border: 1px solid var(--border);
292
+ }
293
+
294
+ /* File List */
295
+ .file-list {
296
+ margin-bottom: 24px;
297
+ }
298
+
299
+ .file-list h4 {
300
+ margin-bottom: 16px;
301
+ color: var(--text-secondary);
302
+ font-size: 16px;
303
+ }
304
+
305
+ .file-items {
306
+ display: flex;
307
+ flex-direction: column;
308
+ gap: 8px;
309
+ }
310
+
311
+ .file-item {
312
+ display: flex;
313
+ align-items: center;
314
+ gap: 12px;
315
+ padding: 12px 16px;
316
+ background: var(--bg-secondary);
317
+ border-radius: var(--radius);
318
+ border: 1px solid var(--border);
319
+ }
320
+
321
+ .file-item-icon {
322
+ font-size: 20px;
323
+ }
324
+
325
+ .file-item-name {
326
+ flex: 1;
327
+ color: var(--text);
328
+ font-size: 14px;
329
+ }
330
+
331
+ .file-item-size {
332
+ color: var(--muted);
333
+ font-size: 12px;
334
+ }
335
+
336
+ .file-item-remove {
337
+ background: none;
338
+ border: none;
339
+ color: var(--error);
340
+ cursor: pointer;
341
+ padding: 4px;
342
+ border-radius: 4px;
343
+ transition: background 0.2s ease;
344
+ }
345
+
346
+ .file-item-remove:hover {
347
+ background: rgba(239, 68, 68, 0.1);
348
+ }
349
+
350
+ /* Upload Progress */
351
+ .upload-progress {
352
+ margin-top: 24px;
353
+ padding: 24px;
354
+ background: var(--bg-secondary);
355
+ border-radius: var(--radius-lg);
356
+ border: 1px solid var(--border);
357
+ }
358
+
359
+ .progress-header {
360
+ display: flex;
361
+ justify-content: space-between;
362
+ align-items: center;
363
+ margin-bottom: 16px;
364
+ }
365
+
366
+ .progress-header h4 {
367
+ color: var(--text);
368
+ font-size: 18px;
369
+ }
370
+
371
+ .progress-status {
372
+ color: var(--muted);
373
+ font-size: 14px;
374
+ }
375
+
376
+ .progress-bar {
377
+ height: 8px;
378
+ background: var(--card);
379
+ border-radius: 4px;
380
+ overflow: hidden;
381
+ margin-bottom: 16px;
382
+ }
383
+
384
+ .progress-fill {
385
+ height: 100%;
386
+ background: var(--gradient-success);
387
+ width: 0%;
388
+ transition: width 0.3s ease;
389
+ }
390
+
391
+ .progress-log {
392
+ max-height: 120px;
393
+ overflow-y: auto;
394
+ background: var(--card);
395
+ padding: 16px;
396
+ border-radius: var(--radius);
397
+ border: 1px solid var(--border);
398
+ font-family: 'Monaco', 'Menlo', monospace;
399
+ font-size: 12px;
400
+ color: var(--text-secondary);
401
+ }
402
+
403
+ /* Chat */
404
+ #chat {
405
+ display: flex;
406
+ flex-direction: column;
407
+ height: 500px;
408
+ }
409
+
410
+ #messages {
411
+ flex: 1;
412
+ overflow-y: auto;
413
+ padding: 16px;
414
+ background: var(--bg-secondary);
415
+ border-radius: var(--radius-lg);
416
+ border: 1px solid var(--border);
417
+ margin-bottom: 16px;
418
+ scroll-behavior: smooth;
419
+ }
420
+
421
+ .msg {
422
+ padding: 16px 20px;
423
+ border-radius: var(--radius-lg);
424
+ margin: 16px 0;
425
+ max-width: 85%;
426
+ white-space: pre-wrap;
427
+ animation: messageSlideIn 0.3s ease;
428
+ position: relative;
429
+ }
430
+
431
+ .msg.user {
432
+ margin-left: auto;
433
+ background: var(--gradient-accent);
434
+ color: white;
435
+ box-shadow: var(--shadow);
436
+ }
437
+
438
+ .msg.assistant {
439
+ background: var(--card);
440
+ border: 1px solid var(--border);
441
+ color: var(--text);
442
+ }
443
+
444
+ .msg.thinking {
445
+ background: var(--card);
446
+ border: 1px solid var(--border);
447
+ color: var(--muted);
448
+ font-style: italic;
449
+ }
450
+
451
+ @keyframes messageSlideIn {
452
+ from {
453
+ opacity: 0;
454
+ transform: translateY(10px);
455
  }
456
+ to {
457
+ opacity: 1;
458
+ transform: translateY(0);
 
 
 
 
 
 
 
459
  }
460
+ }
461
+
462
+ .sources {
463
+ margin: 16px 0;
464
+ padding: 16px;
465
+ background: var(--bg-secondary);
466
+ border-radius: var(--radius);
467
+ border: 1px solid var(--border);
468
+ }
469
+
470
+ .sources strong {
471
+ color: var(--text-secondary);
472
+ display: block;
473
+ margin-bottom: 12px;
474
+ }
475
+
476
+ .pill {
477
+ display: inline-block;
478
+ background: var(--pill);
479
+ padding: 6px 12px;
480
+ border-radius: 999px;
481
+ margin: 4px;
482
+ color: var(--text-secondary);
483
+ border: 1px solid var(--border);
484
+ font-size: 12px;
485
+ transition: all 0.2s ease;
486
+ }
487
+
488
+ .pill:hover {
489
+ background: var(--card-hover);
490
+ border-color: var(--border-light);
491
+ }
492
+
493
+ /* Chat Controls */
494
+ .chat-controls {
495
+ position: sticky;
496
+ bottom: 0;
497
+ background: linear-gradient(180deg, transparent, var(--bg) 20%);
498
+ padding: 16px 0;
499
+ }
500
+
501
+ .chat-input-wrapper {
502
+ display: flex;
503
+ gap: 12px;
504
+ margin-bottom: 8px;
505
+ }
506
+
507
+ #question {
508
+ flex: 1;
509
+ padding: 16px 20px;
510
+ border-radius: var(--radius-lg);
511
+ border: 2px solid var(--border);
512
+ background: var(--card);
513
+ color: var(--text);
514
+ font-size: 16px;
515
+ transition: all 0.2s ease;
516
+ }
517
+
518
+ #question:focus {
519
+ outline: none;
520
+ border-color: var(--accent);
521
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
522
+ }
523
+
524
+ #question:disabled {
525
+ opacity: 0.6;
526
+ cursor: not-allowed;
527
+ }
528
+
529
+ .chat-hint {
530
+ text-align: center;
531
+ color: var(--muted);
532
+ font-size: 14px;
533
+ padding: 12px;
534
+ background: var(--card);
535
+ border-radius: var(--radius);
536
+ border: 1px solid var(--border);
537
+ }
538
+
539
+ /* Modal */
540
+ .modal {
541
+ position: fixed;
542
+ inset: 0;
543
+ display: flex;
544
+ align-items: center;
545
+ justify-content: center;
546
+ background: rgba(0, 0, 0, 0.8);
547
+ z-index: 1000;
548
+ backdrop-filter: blur(8px);
549
+ animation: modalFadeIn 0.3s ease;
550
+ }
551
+
552
+ .modal.hidden {
553
+ display: none;
554
+ }
555
+
556
+ @keyframes modalFadeIn {
557
+ from {
558
+ opacity: 0;
559
+ backdrop-filter: blur(0px);
560
  }
561
+ to {
562
+ opacity: 1;
563
+ backdrop-filter: blur(8px);
564
+ }
565
+ }
566
+
567
+ .modal-content {
568
+ width: 100%;
569
+ max-width: 480px;
570
+ background: var(--card);
571
+ border: 1px solid var(--border);
572
+ border-radius: var(--radius-xl);
573
+ box-shadow: var(--shadow-lg);
574
+ animation: modalSlideIn 0.3s ease;
575
+ overflow: hidden;
576
+ }
577
+
578
+ @keyframes modalSlideIn {
579
+ from {
580
+ opacity: 0;
581
+ transform: translateY(20px) scale(0.95);
582
+ }
583
+ to {
584
+ opacity: 1;
585
+ transform: translateY(0) scale(1);
586
+ }
587
+ }
588
+
589
+ .modal-header {
590
+ text-align: center;
591
+ padding: 32px 32px 24px;
592
+ background: var(--bg-secondary);
593
+ border-bottom: 1px solid var(--border);
594
+ }
595
+
596
+ .modal-logo {
597
+ font-size: 48px;
598
+ margin-bottom: 16px;
599
+ }
600
+
601
+ .modal-header h2 {
602
+ font-size: 24px;
603
+ font-weight: 700;
604
+ margin-bottom: 8px;
605
+ color: var(--text);
606
+ }
607
+
608
+ .modal-subtitle {
609
+ color: var(--muted);
610
+ font-size: 16px;
611
+ }
612
+
613
+ .tabs {
614
+ display: flex;
615
+ gap: 4px;
616
+ margin: 24px 32px 0;
617
+ padding: 4px;
618
+ background: var(--bg-secondary);
619
+ border-radius: var(--radius);
620
+ }
621
+
622
+ .tab {
623
+ flex: 1;
624
+ background: transparent;
625
+ border: none;
626
+ color: var(--muted);
627
+ border-radius: var(--radius);
628
+ padding: 12px 16px;
629
+ cursor: pointer;
630
+ font-weight: 500;
631
+ transition: all 0.2s ease;
632
+ }
633
+
634
+ .tab.active {
635
+ background: var(--gradient-accent);
636
+ color: white;
637
+ box-shadow: var(--shadow);
638
+ }
639
+
640
+ .tab:hover:not(.active) {
641
+ color: var(--text);
642
+ }
643
+
644
+ .tab-body {
645
+ padding: 24px 32px;
646
+ }
647
+
648
+ .tab-body.hidden {
649
+ display: none;
650
+ }
651
+
652
+ .form-group {
653
+ margin-bottom: 20px;
654
+ }
655
+
656
+ .form-group label {
657
+ display: block;
658
+ margin-bottom: 8px;
659
+ color: var(--text-secondary);
660
+ font-weight: 500;
661
+ font-size: 14px;
662
+ }
663
+
664
+ .form-group input {
665
+ width: 100%;
666
+ padding: 14px 16px;
667
+ border-radius: var(--radius);
668
+ border: 2px solid var(--border);
669
+ background: var(--bg-secondary);
670
+ color: var(--text);
671
+ font-size: 16px;
672
+ transition: all 0.2s ease;
673
+ }
674
+
675
+ .form-group input:focus {
676
+ outline: none;
677
+ border-color: var(--accent);
678
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
679
+ }
680
+
681
+ .modal-footer {
682
+ padding: 24px 32px;
683
+ text-align: center;
684
+ border-top: 1px solid var(--border);
685
+ background: var(--bg-secondary);
686
+ }
687
+
688
+ .modal-footer p {
689
+ color: var(--muted);
690
+ font-size: 12px;
691
+ line-height: 1.5;
692
+ }
693
+
694
+ /* Loading Overlay */
695
+ .loading-overlay {
696
+ position: fixed;
697
+ inset: 0;
698
+ background: rgba(0, 0, 0, 0.9);
699
+ z-index: 2000;
700
+ display: flex;
701
+ align-items: center;
702
+ justify-content: center;
703
+ backdrop-filter: blur(12px);
704
+ }
705
+
706
+ .loading-overlay.hidden {
707
+ display: none;
708
+ }
709
+
710
+ .loading-content {
711
+ text-align: center;
712
+ color: white;
713
+ }
714
+
715
+ .loading-spinner {
716
+ width: 60px;
717
+ height: 60px;
718
+ border: 4px solid rgba(255, 255, 255, 0.3);
719
+ border-top: 4px solid white;
720
+ border-radius: 50%;
721
+ animation: spin 1s linear infinite;
722
+ margin: 0 auto 24px;
723
+ }
724
+
725
+ @keyframes spin {
726
+ 0% { transform: rotate(0deg); }
727
+ 100% { transform: rotate(360deg); }
728
+ }
729
+
730
+ .loading-content h3 {
731
+ font-size: 24px;
732
+ margin-bottom: 8px;
733
+ }
734
+
735
+ .loading-content p {
736
+ color: rgba(255, 255, 255, 0.8);
737
+ font-size: 16px;
738
+ }
739
+
740
+ /* Spinner */
741
+ .spinner {
742
+ width: 16px;
743
+ height: 16px;
744
+ border: 2px solid rgba(255, 255, 255, 0.3);
745
+ border-top: 2px solid white;
746
+ border-radius: 50%;
747
+ animation: spin 1s linear infinite;
748
+ display: inline-block;
749
+ }
750
+
751
+ /* Reveal animations */
752
+ .reveal {
753
+ opacity: 0;
754
+ transform: translateY(20px);
755
+ transition: opacity 0.8s ease, transform 0.8s ease;
756
+ }
757
+
758
+ .reveal.in {
759
+ opacity: 1;
760
+ transform: none;
761
+ }
762
+
763
+ /* Footer */
764
+ footer {
765
+ text-align: center;
766
+ color: var(--muted);
767
+ margin-top: 48px;
768
+ padding: 24px 0;
769
+ border-top: 1px solid var(--border);
770
+ font-size: 14px;
771
+ }
772
+
773
+ /* Responsive */
774
+ @media (max-width: 768px) {
775
  .container {
776
+ padding: 16px;
 
 
777
  }
778
 
779
+ header {
780
+ flex-direction: column;
781
+ gap: 24px;
782
+ text-align: center;
783
+ }
784
 
785
  .card {
786
+ padding: 24px;
 
 
 
 
 
 
787
  }
 
788
 
789
+ .modal-content {
790
+ margin: 16px;
791
+ max-width: none;
 
792
  }
 
 
 
 
 
 
 
 
793
 
794
+ .chat-input-wrapper {
795
+ flex-direction: column;
796
+ }
797
+ }
798
+
799
+ /* Reduced motion */
800
+ @media (prefers-reduced-motion: reduce) {
801
+ * {
802
+ transition: none !important;
803
+ animation: none !important;
804
  }
805
 
806
+ .reveal {
807
+ opacity: 1;
808
+ transform: none;
 
809
  }
810
+ }